verilog语法

综合:编译 RTL 代码,从库里选择用到的门器件,把这些器件按照“逻辑”搭建成电路

不可综合:找不到对应的器件来实现相应的代码。
在设计的时候要确保所写的代码是可以综合的。

不可综合或不推荐使用的代码

代码要求
initial严禁在设计中使用,只能在测试文件中使用
task/function不推荐在设计中使用,在测试文件中可用
for在设计中、测试文件中均可使用。但在设计中多数会将其用错,所以建议在初期设计时不使用,熟练后按规范使用
while/repeat/forever严禁在设计中使用,只能在测试文件中使用,产生激励信号
integer(整数)不推荐在设计中使用
三态门内部模块不能有三态接口,三态门只有顶层文件中才使用。三态门目的是为了节省管脚
casex/casez(设计代码内部不能有 X 态和 Z 态,因此 casez、casex 设计时不使用)待定
force/wait/fork严禁在设计中使用,只能在测试文件中使用
#n(延迟多久)严禁在设计中使用,只能在测试文件中使用

task

可综合与不可综合

测试文件中 task 使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//部分代码
initial begin
uart_tx_module(8'h12);
end

//串口的发送
reg uart_tx_bit;
task uart_tx_module;
input [7:0]data;
begin
uart_tx_bit = 1;
#1000;
uart_tx_bit = 0;
#1000;

uart_tx_bit = data[0];
#1000;

uart_tx_bit = data[1];
#1000;

uart_tx_bit = data[2];
#1000;

uart_tx_bit = data[3];
#1000;

uart_tx_bit = data[4];
#1000;

uart_tx_bit = data[5];
#1000;

uart_tx_bit = data[6];
#1000;

uart_tx_bit = data[7];
#1000;

uart_tx_bit = 1;
#1000;
end
endtask

实际模块中可以综合语句使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
reg WE;
reg ack;
reg [1:0]data

always@(posedge clk or negedge rst) begin
case (data)
2'b00: begin
data_read_moudle(data);
end

2'b01: begin
data_read_moudle(data);
end

2'b10: begin
data_read_moudle(data);
end

2'b11: begin
data_read_moudle(data);
end

default: begin
data_read_moudle(2'b00);
end
endcase
end

task data_read_moudle
input [1:0]data;
begin
WE=data[0];
ack=data[1];
end
endtask

可综合:

  • 不包含时序控制(如 #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@(*) begin
end
或者用 assign

时序逻辑格式为:
always@(posedge clk or negedge rst) begin
if(rst==1'b0) begin
/*CODE*/
end
else begin
/*CODE*/
end
end
时序逻辑中,敏感列表一定是 clk 的上升沿和复位的下降沿,最开始必须判断复位
if else 和 casealways 里面的语句,使用 if else 和 case 两种方法用来作选择判断,可以完成全部设计
算术运算符
(+、-、*、/、%)
可以直接综合出相对应的电路。但除法和求余数运算的电路面积一般比较大,不建议直接使用除法和求余数
乘法电路是由多个加法电路实现的,乘法电路比加法电路更大
赋值运算符
(=、<=)
时序逻辑用 <=,组合逻辑用 =(时序逻辑也可以用,但是会阻碍),其他情况不存在
关系运算符
(==、!=、>、<、>=、<=)
逻辑运算符
(&&、||、!)
位运算符
(~、|、^、&)
移位运算符
(<<、>>)
拼接运算符
({})

模块

模块(module)是 verilog 的基本描述单元,模块相当于一个原理图,例化相当于制造这个模块。
例化模块:推荐按名字关联,按位置关联。

模块A

1
2
3
module A(...);

endmodule

模块B 例化模块A

1
2
3
4
5
6
`include "A"
module B(...);

A a();

endmodule

信号类型

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
always @(*)begin	
if(cnt2==0) begin
x = 475_000;
end
else if(cnt2 == 1) begin
x = 425_000;
end
else if(cnt2 == 2) begin
x = 350_000;
end
else if(cnt2 == 3) begin
x = 250_000;
end
else if(cnt2 == 4) begin
x = 100_000;
end
else if(cnt2 == 5) begin
x = 90_000;
end
else if(cnt2 == 6) begin
x = 80_000;
end
else if(cnt2 == 7) begin
x = 40_000;
end
else if(cnt2 == 8) begin
x = 20_000;
end
else begin
x = 10_000;
end
end

综合成组合电路,对 X 进行设计,综合出的电路里面是没有寄存器的,但是它在 always 中使用,所以还是要 reg 定义,虽然没有生成寄存器,但是还是要使用寄存器类型,这也是语法识别出来必须这样子。

代码2

1
2
3
4
5
ass ass1(
.clk (clk),
.rst (rst),
.out (out)
);

代码2是例化代码,out 是例化模块输出,由于顶层模块 out 不是 always 产生的,而是例化产生的,因此顶层 out 要定义成 wire 型。

程序语句

描述功能语句就会使用 assign、always。

assign 语句

assign 语句是连续赋值语句,一般是将一个变量的值不间断的赋值给另一个变量,两个变量之间就类似于被导线连接在了一起,习惯上当做连线用。
assign 与 assign 之间是并行的,独立的。

always(总是)语句

always 语句是条件循环语句,执行机制是:对一个称为敏感变量表(敏感列表)发生条件满足事件后驱动程序执行。
当敏感事件的条件满足时,就执行一次程序语句,敏感列表中信号,一定会包含当前整个 always 用到的输入信号。
当 always 很复杂,有很多敏感信号,可能会遗漏,为了避免这种情况可以用 * 来代替,这个 * 是指“程序语句”中所有的条件信号,如下代码,* 代表 a、b、d、sel,不包含 c。

1
2
3
4
5
6
7
8
always @(*) begin
if(sel == 0) begin
c = a + b;
end
else begin
c = a + d;
end
end

这种条件信号变化结果称为“组合逻辑”。

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
2
3
4
5
6
7
8
9
10
11
always(posedge clk or negedge rst) begin
if(rst == 1'b0) begin
dout <= 0;
end
else if(din == 4'b10x0) begin //4'b1010 或 4'b1000 都可以满足条件
dout <= 1;
end
else begin

end
end

仿真器或综合器生成如下代码,再去生成实际电路(高亮第 4-6、11 行)

1
2
3
4
5
6
7
8
9
10
11
always(posedge clk or negedge rst) begin
if(rst == 1'b0) begin
dout <= 0;
end
else if(din == 4'b1010 || din == 4'b1000) begin //4'b1010 或 4'b1000 都可以满足条件
dout <= 1;
end
else begin

end
end

仿真过程中有些信号产生了不定态,设计者就要认真分析这个不定态是否合理。如果真的不关心是 0 还是 1,那么可以不解决。建议所有信号都处于已知状态。可以减少思考时间。

高阻态

Z 态,高阻态。表示不驱动该信号(既不给 0 也不给 1)(FPGA 内部信号不会这样,内部一般都是固定值),通常用于三态门接口当中。
三态门做的接口,既做输入也做输出,是双向接口,一般硬件电路中会将该线接上一个上拉电阻(弱上拉)或下拉电阻(弱下拉)。
FPGA 设计中是如何做到“不驱动”这一行为呢?FPGA 内部有三态门。

在 verilog 中三态门代码实现

1
2
assign data = (wr_en == 1)?wr_data:1'bz;
assign rd_data = data;

综合器看到这两行代码则知道要合成三态门了,高阻 z 的作用在于此,硬件上用三态门是为了减少管脚,而在 FPGA 内部没有必要减少连线,所以使用三态信号是没有意义的。
建议设计时不要在 FPGA 内部使用高阻态“z”,没有必要给自己添加“思考”麻烦。如果设计中使用了高阻态也不会报错,可以实现功能。
高阻态“z”表示不驱动总线,实际电路就是高电平或者低电平。
高阻态在语法上表现一个行为,硬件上有对应电路。

算术运算符

分类功能代码电路示意图备注
加法器加always@(*) begin
c = a+b;
end
逻辑运算都是有与门、或门等门电路搭建起来的电路,
1 位加法器 C = A ^ B cout(进位)= A && B
减法器减always@(*) begin
c = a-b;
end
C = A & B
乘法器乘always@(*) begin
c = a*b;
end
二进制运算中,乘法运算实质上就是加法运算,
例 1111*111 = (1111) + (11110) + (1111000)
所以乘法器会比加法器消耗的资源多
除法器除always@(*) begin
c = a/b;
end
二进制运算中,除法和求余涉及到加法、减法和移位等运算,
所以除法和求余数电路资源都非常大,设计时要尽量避免除法和求余。
一定要用到除法,尽量让除数为 2 的 N 次方,如 2、4、8、16 等。
a/2 就是 a 向右移动 1 位
a/4 就是 a 向右移动 2 位
a/8 就是 a 向右移动 3 位
移位运算是不消耗资源的
求余器余数always@(*) begin
c = a%b;
end

可以直接写的算术运算符:+、-、*(对应硬件电路比较小);除法和求余就不能直接写,对应的硬件电路比较大;慎用除法。

加法运算符号

verilog 代码中可以直接使用符号 +;组合逻辑。

1
assign C = A + B;

减法运算符

1
C = A - B;

乘法运算符

1
C = A *B

多位二进制的乘法与十进制计算相同

1
2
3
4
5
6
7
8
      1 1
x 1 0 1
------------
1 1
0 0
1 1
------------
1 1 1 1

求余和除法运算

没有直接的除法器,仿真测试中,是可以使用除法和求余,不用综合成电路,实际设计中,尽量不写。

位宽截断规则

写代码时,需要注意信号的位宽,最终的结果取决于 “=” 号左边信号的位宽,保存低位,丢弃高位。

1
2
3
4
5
6
7
8
9
wire c;
wire [1:0] d;
wire [2:0] e;
wire [2:0] f;

assign c = 1'b1 + 1'b1; //c = 1'b0
assign d = 1'b1 + 1'b1; //d = 2'b10
assign e = 1'b1 + 1'b1; //e = 3'b010
assign f = 1 + 1; //f = 3'b010

信号 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
2
3
4
5
6
7
8
9
wire c;
wire [1:0] d;
wire [2:0] e;
wire [2:0] f;

assign c = 0 - 1; //c = 1'b0 0-1 未写位宽默认是32位位宽
assign d = 0 - 1; //d = 2'b10
assign e = 0 - 1; //e = 3'b010
assign f = 0 - 1; //f = 3'b010

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
2
3
wire [4:0] h;

assign h = 2'b11 * 3'b101; //h结果 5’b01111

2'b11 * 3'b101 值是 4'b1111
h 该信号有 5 位,4'b1111 赋给 5 位信号,结果高位补 0,结果为 5'b01111。

补码由来

加法出现进位的时候,计算的结果是不正确的,只有保留了进位计算的结果才是正确的。

二进制减法

1
2
3
4
5
、 、 
0 0//二进制减法
- 0 1
------
1 1 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@(*) begin
C = A&&B;
end
以后换成链接注意:FPGA 支持多输入的与门,
例如四输入与门,输入可为 ABCD,
输出为 E,当 ABCD 同时为 1 时,
E 为 1。
与门多位逻辑与
(符号 &&)
A 或 B 都不为 0 时
C 为 1,否则为 0;
reg [2:0] A,B,C;
always@(*) begin
C = A&&B
end
综合电路会是如下操作:
A 是多位,会先对 A 的多位
进行逻辑或,B 也是如此
然后再对 A 和 B 进行与运算
以后换成链接多位信号之间的逻辑与,
很容易引起歧义,设计最好不要用
多位数的逻辑与。
如果要实现上面功能,
建议代码改为如下:
always@(*) begin
C=(A!=0)&&(B!=0);
end
或门1 位逻辑或
(符号 ||)
A 和 B 其中 1 个为 1,
C 为 1;否则 C 为 0;
reg A,B;
always@(*) begin
C = 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@(*) begin
C = A||B;
end

verilog HDL 语言中逻辑运算符,分别是:

  • &&:逻辑与;
  • ||:逻辑或;
  • !:逻辑非。

与、或:分为 1 位的逻辑与、或和多位的逻辑与、或;