FPGA 入门 —— 代码规范与模块结构
FPGA 入门 —— 代码规范与模块结构
不可综合或不推荐使用的代码
代码 | 要求 |
---|---|
initial | 严谨在设计中使用,只能在测试文件中使用 |
task/function | 不推荐在设计中使用,在测试文件中使用 |
for | 在设计中、测试文件中均可以使用,但在设计中多数会将其用错,所以建议在初期设计时不使用,熟练后按规范使用 |
while/repeat/forever | 严禁在设计文件中使用,只能在测试文件中使用 |
integer | 不推荐在设计中使用 |
三态门 | 内部模块不能有三态接口,三态门只有顶层文件才使用,三态门目的时为了节省管脚,FPGA 内部完全没有必要使用 |
casex/casez | 设计代码内部不能有 X 态和 Z 态,因此 casez,casex 设计时不使用 |
force/wait/fork | 严禁在设计中使用,只能在测试文件中使用 |
#n | 严禁在设计中使用,只能在测试文件中使用 |
推荐使用的设计代码
代码 | 备注 |
---|---|
reg/wire | 设计中所有的信号类型定义,只有 reg 和 wire 两种 |
parmeter | 设计代码中所有的位宽、长度、状态机命名等,建议都用参数表示,阅读方便并且修改容易 |
assign/always | 程序块主要部分,至简设计法对 always 使用有严格规范 组合逻辑格式为: always@(*)begin 代码语句 end 或者使用 assign 时序逻辑格式为: always@(posedge clk or negedgerst_n) begin if(rst_n == 1'b0)begin 代码语句: end else begin 代码语句 end end end 时序逻辑中,敏感列表一定时 clk 的上升沿和复位的下降沿、最开始必须判读复位 |
if else 和 case | always 里面的语句,使用 if else 和 case 两种方法用来作选择判断,可以完成全部设计 |
算数运算符(+,-,x,/,%) | 可以直接综合出相对应的电路。但除法和求余运算的电路面积一般比较大,不建议直接使用除法和求余 |
赋值运算符(=,<=) | 时许逻辑用“<=”,组合逻辑用“=”,其他情况不存在 |
关系运算符(==,!,=,>,<,>=,<=) | |
逻辑运算符(&&, | |
位运算符(~, | ,^,&) |
位移运算符(<,>,>>) | fpga常用运算符 |
拼接运算符({}) | fpga常用运算符 |
模块结构
模块时 Verilog 的基本描述单位,是用于描述某个设计的功能或结构及与其他模块通信的外部端口,采用模块化设计让程序看起来更有提条理性,也便于仿和调试
模块就等同于一个器件,就如同调用通用器件(与门,三态门等),或通用宏单元(计数器、ALU、CPU 等),一个模块可以在另一个模块中进行调用,一个电路可以由多个模块组合而成
整个项目的设计思路就是模块套模块,自顶向下依次展开,在一个工程设计里,每个模块实现特定的功能,模块间可进行层次的嵌套,对大型的数字电路进行设计时,可以将其分割成大小不一的小模块,每个小模块实现特定的功能,最后通过由顶层模块调用子模块的方式来实现整体功能,这就是 Top-Down 思想
模块主要有五个部分:
-
端口定义
-
参数定义(可选)
-
I/O 说明
-
内部信号声明
-
功能定义
模块总是以关键词 module 开始,以关键词 endmodule 结尾,一般语法结构如下:
注意:这里的信号宽度都是定义为 [width-1 : 0] ,这是由于如果我们使用的是 8 位输入,那么我们的输入一般是 0-7 ,刚好是 8 位,所以这里是这样定义的
这里的端口定义、单数定义、信号定义都比较好理解,这里主要介绍信号类型和功能定义
信号类型简介
这里只进行简单介绍,后续博客会对这两种信号类型进行详细介绍
reg
reg[width-1 : 0]:reg 型表示的寄存器类型,用于 always 模块内被赋值的信号,必须定义为 reg 型,reg 表示一定要有触发,输出才会反映输入的状态。根据触发条件的不同,模块可以建模不同的硬件结构:如果这个条件是时钟的上升沿或下降沿,那么这个硬件模型就是一个触发器(时序逻辑);如果这个条件是某一信号的高电平或低电平,那么这个硬件模型就是一个锁存器(组合逻辑);如果这个条件是赋值语句右侧任意操作数的变化,那么这个硬件模型就是一个组合逻辑。
reg 型相对复杂些,其综合后的输出主要还看具体使用的场景:当在组合电路中使用 reg,合成后的仍然是 net 网络;当在时序电路中使用 reg 合成后的才是 register。
wire
wire[width-1 : 0]:wire 的本质是一条没有逻辑的连线,也就是说输入时什么输出也就是什么。wire 型数据常用来表示以 assign 关键字指定的组合逻辑。输入输出端口类型都默认为 wire 型,wire 相当于物理连线,默认初始值是 z(高组态)
如果你把 wire 定义的变量用在时序逻辑的语句中就会出现综合错误:
例如:
在 always 语句中使用 wire 型定义的变量赋值,综合器就会报错。
功能描述
全部由 always 、 assign 和例化模块组成,assign ,连续赋值,always ,敏感赋值
连续赋值,就是无条件全等,敏感赋值,就是有条件相等,assign 的对象是 wire ,always 的对象是 reg ,这就是语法约束
功能差异
assign 对应电路下连线操作。always 对应插⼊敏感控制连线。这⾥容易混淆的就是 assign 综合的⼀定是组合电路,但是 always 综合的不⼀定是时序电路。always 的敏感列表使⽤*号就可以等效综合为组合电路。如果使⽤的是电平触发,也使会综合成相应的组合电路。只有出现边沿触发时,才会综合成时序电路。⽽时序电路,基础就是时钟和使能两个关键信号。时钟在 always 模块中不再出现(时钟信号在敏感信号作⽤下的值⼀直相等,没有使⽤在内部的意义,当然可以转化后使⽤)。⽽对于使能信号,则是会有⼀个 if 判断语句,⽽且处于第⼀优先级。这就是异步复位。如果不在敏感列表⽽处于第⼀优先级,则是同步复位信号
从上⾯的描述可以看到: always 可以实现两种电路,是不是可以不⽤ assign 来实现设计?理论上可以,但是会加⼤设计的难度。⾸先要明确的是,always 只能对 reg 变量赋值,这导致 wire 变量赋值困难。如果没有 assign,每个 wire 变量都要加⼊⼀个 reg 缓冲。可以简单理解:开始时只有 always,可以实现基本的功能,然后将其中的组合逻辑提取出来构成 assign。同时引⼊了 wire。也就是 assign 是 always 的补充(只是⽅便理解,没有根据)
主要就是一下几点:
assign 语句使用时不能带时钟
always 语句可以带时钟,也可以不带时钟。在 always 不带时钟时,逻辑功能和 assign 完全一致,都是只产生组合逻辑,比较简单的组合推荐使用 assign 语句,比较复杂的组合逻辑推荐使用 always 语句
带时钟和不带时钟的 always:
always 可以带时钟,也可以不带时钟,在 always 不带时钟,逻辑功能和 assign 完全一致,虽然产生的信号定义还是 reg 类型,但该语句产生的还是组合逻辑
在 always 带时钟信号时,这个逻辑语句才能产生真正的寄存器