verilog语法
综合:编译 RTL 代码,从库里选择用到的门器件,把这些器件按照“逻辑”搭建成电路
不可综合:找不到对应的器件来实现相应的代码。
在设计的时候要确保所写的代码是可以综合的。
不可综合或不推荐使用的代码
| 代码 | 要求 |
|---|---|
initial | 严禁在设计中使用,只能在测试文件中使用 |
task/function | 不推荐在设计中使用,在测试文件中可用 |
for | 在设计中、测试文件中均可使用。但在设计中多数会将其用错,所以建议在初期设计时不使用,熟练后按规范使用 |
while/repeat/forever | 严禁在设计中使用,只能在测试文件中使用,产生激励信号 |
integer(整数) | 不推荐在设计中使用 |
| 三态门 | 内部模块不能有三态接口,三态门只有顶层文件中才使用。三态门目的是为了节省管脚 |
casex/casez | (设计代码内部不能有 X 态和 Z 态,因此 casez、casex 设计时不使用)待定 |
force/wait/fork | 严禁在设计中使用,只能在测试文件中使用 |
#n(延迟多久) | 严禁在设计中使用,只能在测试文件中使用 |
task
可综合与不可综合
测试文件中 task 使用方法
1 | //部分代码 |
实际模块中可以综合语句使用
1 | reg WE; |
可综合:
- 不包含时序控制(如
#delay或wait) - 仅实现组合逻辑且无输入输出端口
不可综合:
- 包含仿真时序控制或系统任务(如
$display) - 典型应用:Testbench 中的激励生成
推荐使用的代码及其说明
| 代码 | 备注 |
|---|---|
reg/wire | 设计中所有的信号类型定义,只有 reg 和 wire,always 中使用 reg,assign 中使用 wire |
parameter | 设计代码中所有的位宽、长度、状态机命名等、建议都用参数表示,阅读方便并且修改容易 使用方式如下: module a (#parameter SIZE = 6,WIDTH = 8)(input .. ,output ..);endmodule |
assign/always | 程序块主要部分 组合逻辑格式为: always@(*) beginend或者用 assign时序逻辑格式为: always@(posedge clk or negedge rst) beginif(rst==1'b0) begin/*CODE*/endelse begin/*CODE*/endend时序逻辑中,敏感列表一定是 clk 的上升沿和复位的下降沿,最开始必须判断复位 |
if else 和 case | always 里面的语句,使用 if else 和 case 两种方法用来作选择判断,可以完成全部设计 |
| 算术运算符 ( +、-、*、/、%) | 可以直接综合出相对应的电路。但除法和求余数运算的电路面积一般比较大,不建议直接使用除法和求余数 乘法电路是由多个加法电路实现的,乘法电路比加法电路更大 |
| 赋值运算符 ( =、<=) | 时序逻辑用 <=,组合逻辑用 =(时序逻辑也可以用,但是会阻碍),其他情况不存在 |
| 关系运算符 ( ==、!=、>、<、>=、<=) | |
| 逻辑运算符 ( &&、||、!) | |
| 位运算符 ( ~、|、^、&) | |
| 移位运算符 ( <<、>>) | |
| 拼接运算符 ( {}) |
模块
模块(module)是 verilog 的基本描述单元,模块相当于一个原理图,例化相当于制造这个模块。
例化模块:推荐按名字关联,按位置关联。
模块A
1 | module A(...); |
模块B 例化模块A
1 |
|
信号类型
verilog HDL 的信号类型有很多种,主要包括两种数据类型:线网类型、寄存器类型。在进行工程设计的过程也只会使用到这两个类型的信号。
位宽表示
wire [7:0] aaa; //位宽为 8
线网类型 wire
通常用 assign 进行赋值。
如果没有定义位宽范围,缺省值为 1。
信号数据类型没有定义,默认 wire 类型。
寄存器类型 reg
注意:原文此小节暂无展开内容,可参考下方 wire 和 reg 的区别;reg 并不一定生成寄存器,关键看使用场景(always 块内被赋值的信号必须声明为 reg)。
wire 和 reg 的区别
reg 型信号并不一定生成寄存器。如下面代码1所示;
可以这么使用,在模块中使用 always 设计的信号都定义为 reg 型,其他信号都定义为 wire 型;
代码1
1 | always @(*)begin |
综合成组合电路,对 X 进行设计,综合出的电路里面是没有寄存器的,但是它在 always 中使用,所以还是要 reg 定义,虽然没有生成寄存器,但是还是要使用寄存器类型,这也是语法识别出来必须这样子。
代码2
1 | ass ass1( |
代码2是例化代码,out 是例化模块输出,由于顶层模块 out 不是 always 产生的,而是例化产生的,因此顶层 out 要定义成 wire 型。
程序语句
描述功能语句就会使用 assign、always。
assign 语句
assign 语句是连续赋值语句,一般是将一个变量的值不间断的赋值给另一个变量,两个变量之间就类似于被导线连接在了一起,习惯上当做连线用。assign 与 assign 之间是并行的,独立的。
always(总是)语句
always 语句是条件循环语句,执行机制是:对一个称为敏感变量表(敏感列表)发生条件满足事件后驱动程序执行。
当敏感事件的条件满足时,就执行一次程序语句,敏感列表中信号,一定会包含当前整个 always 用到的输入信号。
当 always 很复杂,有很多敏感信号,可能会遗漏,为了避免这种情况可以用 * 来代替,这个 * 是指“程序语句”中所有的条件信号,如下代码,* 代表 a、b、d、sel,不包含 c。
1 | always @(*) begin |
这种条件信号变化结果称为“组合逻辑”。
always 有时钟信号,由 1 变 0 或者由 0 变 1 的瞬间执行一下程序代码,输出才变化,其他时间输出不变的,称为“时序逻辑电路”。
信号边沿触发,即信号上升沿或下降沿才变化的 always,被称为“时序逻辑”。
注意:识别信号是不是时钟不是看名字,而是看这个信号放在哪里,只有放在敏感列表并且是边沿触发的才是时钟。而信号 rst 是复位信号,同样也不是看名字来判断,而是放在敏感列表中并且同样边沿触发。
设计时需要注意以下几点(有待参考)。
组合逻辑的 always 语句中敏感变量必须写全,或者用 * 代替。
组合逻辑器件的赋值采用阻塞赋值 =,时序逻辑器件赋值语句采用非阻塞赋值 <=,原因 阻塞赋值和非阻塞赋值。
数字进制
verilog 中数字表示方式,最常用的格式是:<位宽>'<基数><数值>,如 4'b1001;
位宽:描述数值转换成二进制数后存储需要占用的位数,位宽是一个十进制的整数,例如 4'b1001 中 4 就是位宽。如果没有该项可以通过常量值的进行推断,例如 'b10010 可推断位宽为 5。
基数:表示数值是用多少进制数显示出来。基数的表达形式有:b、B、o、O、d、D、h、H,分别表示二进制、十进制、八进制、十六进制。如果没有此项默认为十进制数,例如,二进制的 4'b1001 可以写成十进制 4'd9 等,还可以不写基数直接写成 9。只要底层二进制相同,无论写成十进制、八进制、十六进制都是表示同样的数字。
数值:是由基数(表示用什么进制表示)所决定的表示常量真实值的一串 ASCII 码。
如果基数定义为 b 或 B,数值可以是 0、1、x、X(不定态)、z、Z(高阻态)。
如果基数定义为 o 或 O,数值可以是 0、1、2、3、4、5、6、7。
如果基数定义为 h 或 H,数值可以是 0、1、2、3、4、5、6、7、9、A、B、C、D、E、F、a、b、c、d、e、f。
如果基数是 d 或 D,数值符可以是任意的十进制数 0 到 9,但不可以是 x 或 z。
如果出现位宽小于数值,那么数据大小以位宽为准,例如:4'b001001 =>4'b1001
如果出现位宽大于数值,则高位补 0,例如:32'h 12 => 32'h00000012
二进制是基础
二进制表示小数,转到其他文章。
不定态
数字电路只有高电平和低电平,分别是 1 和 0。但代码中经常能看到 x 和 z,如 1'bz。x 和 z 是什么电平,没有实际的电平来对应两者,x 和 z 更多地是用来表示设计者的意图或者用于仿真目的(转换成实际电路就会变成 1 或 0)。告诉仿真器和综合器如何解释这段代码。
X 态:不定态,常用于判断条件,告诉综合工具设计者不关心它的电平是多少,0 或 1 都可以。
代码,这里只是语法上面的设计意图
1 | always(posedge clk or negedge rst) begin |
仿真器或综合器生成如下代码,再去生成实际电路(高亮第 4-6、11 行)
1 | always(posedge clk or negedge rst) begin |
仿真过程中有些信号产生了不定态,设计者就要认真分析这个不定态是否合理。如果真的不关心是 0 还是 1,那么可以不解决。建议所有信号都处于已知状态。可以减少思考时间。
高阻态
Z 态,高阻态。表示不驱动该信号(既不给 0 也不给 1)(FPGA 内部信号不会这样,内部一般都是固定值),通常用于三态门接口当中。
三态门做的接口,既做输入也做输出,是双向接口,一般硬件电路中会将该线接上一个上拉电阻(弱上拉)或下拉电阻(弱下拉)。
FPGA 设计中是如何做到“不驱动”这一行为呢?FPGA 内部有三态门。
在 verilog 中三态门代码实现
1 | assign data = (wr_en == 1)?wr_data:1'bz; |
综合器看到这两行代码则知道要合成三态门了,高阻 z 的作用在于此,硬件上用三态门是为了减少管脚,而在 FPGA 内部没有必要减少连线,所以使用三态信号是没有意义的。
建议设计时不要在 FPGA 内部使用高阻态“z”,没有必要给自己添加“思考”麻烦。如果设计中使用了高阻态也不会报错,可以实现功能。
高阻态“z”表示不驱动总线,实际电路就是高电平或者低电平。
高阻态在语法上表现一个行为,硬件上有对应电路。
算术运算符
| 分类 | 功能 | 代码 | 电路示意图 | 备注 |
|---|---|---|---|---|
| 加法器 | 加 | always@(*) beginc = a+b;end | 逻辑运算都是有与门、或门等门电路搭建起来的电路, 1 位加法器 C = A ^ B cout(进位)= A && B | |
| 减法器 | 减 | always@(*) beginc = a-b;end | C = A & B | |
| 乘法器 | 乘 | always@(*) beginc = a*b;end | 二进制运算中,乘法运算实质上就是加法运算, 例 1111*111 = (1111) + (11110) + (1111000)所以乘法器会比加法器消耗的资源多 | |
| 除法器 | 除 | always@(*) beginc = a/b;end | 二进制运算中,除法和求余涉及到加法、减法和移位等运算, 所以除法和求余数电路资源都非常大,设计时要尽量避免除法和求余。 一定要用到除法,尽量让除数为 2 的 N 次方,如 2、4、8、16 等。 | |
| a/2 就是 a 向右移动 1 位 a/4 就是 a 向右移动 2 位 a/8 就是 a 向右移动 3 位 移位运算是不消耗资源的 | ||||
| 求余器 | 余数 | always@(*) beginc = a%b;end |
可以直接写的算术运算符:+、-、*(对应硬件电路比较小);除法和求余就不能直接写,对应的硬件电路比较大;慎用除法。
加法运算符号
verilog 代码中可以直接使用符号 +;组合逻辑。
1 | assign C = A + B; |
减法运算符
1 | C = A - B; |
乘法运算符
1 | C = A *B |
多位二进制的乘法与十进制计算相同
1 | 1 1 |
求余和除法运算
没有直接的除法器,仿真测试中,是可以使用除法和求余,不用综合成电路,实际设计中,尽量不写。
位宽截断规则
写代码时,需要注意信号的位宽,最终的结果取决于 “=” 号左边信号的位宽,保存低位,丢弃高位。
1 | wire c; |
信号 c 的位宽为 1 位,运算结果最终保留 1 位,c = 1'b0;
信号 d 的位宽为 2 位,运算结果最终保留 2 位,d = 2'b10;
信号 e 的位宽为 3 位,运算结果最终保留 3 位,e = 3'b010;1 默认是 32 位,1 + 1 的结果也是 32 位,由于 f 的位宽只有 3 位,所以运算的结果可以保留低 3 位,f = 3'b010;
减法运算也是相同道理
1 | wire c; |
0 - 1 得到的二进制是 1111111...,保存结果取决于 = 号左边信号的位宽:
- c 的位宽有 1 位,保留低 1 位,c 的值
1'b1; - d 的位宽有 2 位,保留低 2 位,d 的值
2'b11; - e 的位宽有 3 位,保留低 3 位,e 的值
3'b111; - f 的位宽有 4 位,保留低 4 位,f 的值
4'b1111;
乘法也是如此,结果取决于等号左边信号的位宽,保存低,丢弃高位。
1 | wire [4:0] h; |
2'b11 * 3'b101 值是 4'b1111
h 该信号有 5 位,4'b1111 赋给 5 位信号,结果高位补 0,结果为 5'b01111。
补码由来
加法出现进位的时候,计算的结果是不正确的,只有保留了进位计算的结果才是正确的。
二进制减法
1 | 、 、 |
二进制减法结果是负数结果,就要用取反加一变成正数,得到负数也是取反加 1,负数的表示方式是从:
| 二进制 | 值 |
|---|---|
1111 | -1 |
1110 | -2 |
1101 | -3 |
1100 | -4 |
1011 | -5 |
1010 | -6 |
1001 | -7 |
1000 | -8 |
10111 | -9 |
10110 | -10 |
逻辑运算符
| 类型 | 情况 | 功能 | verilog代码 | 电路示意图 | 备注 |
|---|---|---|---|---|---|
| 与门 | 1 位逻辑与 (符号: &&) | A 和 B 都为 1, C 为 1,否则 0 | reg A,B;always@(*) beginC = A&&B;end | 以后换成链接 | 注意:FPGA 支持多输入的与门, 例如四输入与门,输入可为 ABCD, 输出为 E,当 ABCD 同时为 1 时, E 为 1。 |
| 与门 | 多位逻辑与 (符号 &&) | A 或 B 都不为 0 时 C 为 1,否则为 0; | reg [2:0] A,B,C;always@(*) beginC = A&&Bend综合电路会是如下操作: A 是多位,会先对 A 的多位 进行逻辑或,B 也是如此 然后再对 A 和 B 进行与运算 | 以后换成链接 | 多位信号之间的逻辑与, 很容易引起歧义,设计最好不要用 多位数的逻辑与。 如果要实现上面功能, 建议代码改为如下: always@(*) beginC=(A!=0)&&(B!=0);end |
| 或门 | 1 位逻辑或 (符号 ||) | A 和 B 其中 1 个为 1, C 为 1;否则 C 为 0; | reg A,B;always@(*) beginC = A||B;end | 注意:FPGA 支持多输入的或门 例如四输入或门,输入可为 ABCD, 输出为 E,当 ABCD 任意一个输入 为 1 时,E 为 1; | |
| 或门 | 多位逻辑或 (符号: ||) | A 和 B 其中 1 个非 0, C 为 1;否则 C 为 0; | reg[2:0]A,B,C;always@(*) beginC = A||B;end |
verilog HDL 语言中逻辑运算符,分别是:
&&:逻辑与;||:逻辑或;!:逻辑非。
与、或:分为 1 位的逻辑与、或和多位的逻辑与、或;