学习Verilog的基本语法.
电路的描述方式
verilog是描述电路的语言。为了实现系统的逻辑功能,在设计系统时可以采用多种描述方式进行建模。verilog
通常使用三种不同的风格描述电路。在设计电路的时候可以任意使用一种或混合使用多种描述方式来描述电路。经过综合工具综合后的结果一般都是门级结构描述。
- **结构描述方式:**调用其他已经定义好的低层模块对整个电路功能进行描述,或者直接调用
verilog
内部基本的门级元件描述电路的结构。 - **数据流描述方式:**设计者从数据在各存储单元之间进行流动和运算的角度对电路的功能进行描述。设计者可以使用
verilog
提供的高层次运算符,例如+
等直接对数据进行高层次的数学逻辑运算建模,而不用关心具体的门级电路结构。 - **行为描述方式:**直接根据电路的外部行为进行建模,与具体的硬件电路无关。
信号声明方式
数字表示方法
整形数字的定义格式为
[-][<位宽>]'<进制><数值>
其中进制的标识符分别为:二进制(b或B)、八进制(o或O)十进制(d或D)、十六进制(h或H)。若省略进制则默认为十进制。
负号的标识需要在位宽的前面
数值还可以使用符号表示不定态
- x(X)表示未知状态
- z(Z,?)表示高阻态
一个 x
或z
可以分别表示十六进制中的四位,八进制中的三位,二进制的一位
变量数据类型
verlog
中的常量有三种类型
parameter
wire
reg
parameter
类型parameter
用于定义在不变的值,例如状态机中的状态、数学运算中的常数等。常量的类型包括三种:- 整数型常量 (可逻辑综合)
- 实数型常量 (用于逻辑仿真)
- 字符串型常量 (用于逻辑仿真)
声明方式如下:
verilogparameter a=4'b10x0 // 4位二进制,第二位为不定值 parameter b=12'dz // 12位十进制,全位为高阻态 parameter c=8'h4x // 8位十六进制,低4位为不定值
wire
类型线网类型代表各个模块之间的物理连线,不能存储逻辑值,它的值由它的驱动元件决定,线网类型的默认值为高阻态z。
verilog
中所有的输入输出信号类型默认均为wire
类型。verilogwire [位宽] 线网名称; // 声明例子 wire a; // 定义一个 1位 的wire wire [7:0] b; // 定义一个8 位的wire wire [4:1] c, d; // 定义一个4 位的wire
reg
类型寄存器类型表示一个抽象的数据存储单元,它具有状态保存作用。寄存器类型只能在
initial
和always
内部被赋值。在赋值前默认值为x
。对于reg
类型,赋值语句的作用就像改变一组触发器的存储单元的值。可以用各种构造控制改变值的时机,如上升沿、下降沿等。单个reg
类型的声明格式如下,其中的 n 代表寄存器的位宽。verilogreg [位宽] 寄存器名称; reg [3:0] a; // 声明一个 4 位的寄存器 reg [4:1] b; // 声明一个 4 位的寄存器
$ git config --global init.defaultBranch main$ git branch -M main # 如果是已经新建了的仓库,修改当前的分支名称为 mainshell
reg[msb:lsb] name;
上面的msb
和lsb
分别代表向量的最高有效位和最低有效位。msb
和lsb
必须为常数、常数表达式或者parameter参数。
变量数组
线网和变量都可以声明为一个数组,数组中的每个元素可以是标量也可以是向量。数组的定义格式为:
数据类型 [位宽] 变量名称[地址范围]
// 例子
wire bus[5:0]; //声明 6个位宽为 1 的wire 类型数组
reg [7:0] data[3:0]; // 声明4个位宽为7 的reg 类型数组
通过使用下标可以访问数组中特定的单元
data[2] = 0;
data[3] = 8'hff;
助记:
位宽写在wire
或者reg
关键字后面则表示位宽,变量名后面则表示数组大小。如果位宽不写,则默认为1。
运算符
verilog中支持的运算符与c语言中的运算符基本一致,但是由于verilog
支持不定值的赋值方式,所以某些运算符的运算结果可能会不太一样。而且还有verilog
中的所特有的运算符这里也会列出来。
算术运算
- 在进程整数除法运算时,结果要略去小数的部分只取整数部分。
- 在进行取模运算时结果的符号位与第一个操作数的符号位一致。
注意:当所运算的操作数中含有不确定的值x
或者z
时算数运算的结果也是不确定的
逻辑运算
! && ||
这三个逻辑运算符只有作用于 全 0 的操作数时才认为该操作数具有逻辑零的值,操作数中有任意多个1则认为该操作数为1,当操作数中含有x
时则返回结果仍然为x
。- 判断逻辑相等和不相等分为两种 ,逻辑相等
== !=
和逻辑全等=== !==
,当进行相等运算时,两个操作数必须逐位相等,比较结果才为1(真),如果任意一个操作数含有不定态(X)或高祖态(Z),其相等比较的结果就会是不定值。进行全等运算时,对不定或高阻状态也进行比较,当两个操作数完全一致时,其结果才为1,否则为0.
位运算
在使用
~ & | ^ ^~
等位运算符时,若两个操作数的位宽不相等,则会自动将操作数在右端对齐,位数少的操作数相应的高位会使用0进行填充。移位操作,移位操作分为两种,移位运算符
>>
,<<
,算术移位运算符<<<
,>>>
。对于左移操作中出现的空位,都使用 0 开进行填充,在进行右移操作时,算术移位会使用符号位进行填充。
需要注意
>>>
作用于有符号数,左边会补符号位;<<<
作用于有符号数,右边补0
,符号位也被移走;注意事项:
左移会导致位宽增加,右移位宽不变。
拼接与缩位
下面列举verilog
中所特有的运算符。
拼接运算符:可以将两个或多个信号的某些为进行拼接表示一个整体信号。
verilog// 例子 {a, b[3:0], w, 3'b101}; // 将信号复制再进行拼接 {4{w}} // 等同于 {w, w, w, w};
缩位运算符:缩位运算符的符号与位运算符的符号一样,但缩位运算符是针对当个操作数的,对当个操作数从左到右按照位的顺序两两进行相应的运算。
veriloga = 4'b0101; &a // 0 |a // 1 ^a // 0
程序的基本结构
结构化描述
// 这里演示全加器的实现
// 可以按照以下格式声明端口
// 输入输出方向 信号类型[位宽] 信号名;
module fadder
(
i_A, i_B, i_Cin,
o_S, o_C
);
input i_A, i_B, i_Cin; // 输入端口
output o_S, o_C; // 输出端口
parameter s_delay=1, c_delay=1; // 定义两个参数,参数也可以代表常量
assign #s_delay o_S = i_A ^ i_B ^ i_Cin; // 延时s_delay
assign #c_delay o_C = (i_A ^ i_B) & i_Cin | (i_A & i_B) // 延时c_delay
endmodule
当需要调用已经写好的模块或者IP时(类似于函数调用),需要例化模块,并传递信号。
例化模块的基本语法如下:
模块名 实例名 (.端口名1(连线1), .端口名2(连线2), ...);
注意事项:
在顶层文件使用被调用模块的端口时,如果端口声明为input
,则向该端口传递的信号可以是wire
或者reg
类型。如果端口声明为output
,则向该端口传递的信号必须是wire
类型。
一个完整地例化模块调用过程:
module fadder_delay_1
(
i_A, i_B, i_Cin,
o_S, o_C
)
// 实例化一个模块并设置其中的参数
// 第一种方式
fadder fadder_delay_1_ins(.i_A(i_A), .i_B(i_B), .i_Cin(i_Cin), .o_S(o_S), o_C(o_C));
defparam fadder_delay_1_ins.s_delay=2, fadder_delay_1_ins.c_delay=3;
// 第二种方式
fadder #(2,3)
fadder_delay_1_ins(.i_A(i_A), .i_B(i_B), .i_Cin(i_Cin), .o_S(o_S), o_C(o_C));
//第三种方式
fadder #(.s_delay(2),.c_delay(3))
fadder_delay_1_ins(.i_A(i_A), .i_B(i_B), .i_Cin(i_Cin), .o_S(o_S), o_C(o_C));
endmodule
数据流描述
在使用数据流描述电路时通常使用连续赋值语句实现。连续赋值语句必须以 assign
开头,出现在和门单元实例化相同的代码层次,它的结构为
assign [delay] wire_type_var=expression;
使用数据流的方式描述一个4位的加法器
module adder(
input [3:0] a, input [3:0] b, input cin, output [3:0] sum, output cout;
)
assign {cout,sum} = a+b+cin;
endmodule
在进行逻辑综合时,综合器会自动优化综合出来的门电路。
行为描述
行为描述一般使用initial
和always
过程块结构。
initial过程块
主要用于仿真测试,用来对变量进行初始化或者产生激励波形,过程块中的内容只会被执行一次,一个
module
可以有多个initial
块结构,各个initial
过程块之间并行运行。若块中的语句没有添加延时控制,则可以延时指定的时钟周期后再执行后续指令。initial过程块不能进行逻辑综合
veriloginitial begin #延时数 行为语句; #延时数 行为语句; .. end // 例子 reg[3:0] a; initial begin a=4'b0000; // 缺省为在0时刻执行该代码 #5 a=4'b0001 // 经过5个时间单位后执行赋值 #5 a=4'b0010 // 经过 10 个时间单位后执行赋值 end
always 过程块,always 过程块会一致被重复执行,多个
always
过程块并行执行,与书写顺序无关。@
中包含的敏感信号可为电平触发或者逻辑变量触发,使用电平触发时可以使用posedge
和negedge
表示上升沿和下降沿。在
always
过程块中执行的赋值语句,等号左边必须为reg
类型。verilogalways @ (敏感信号列表) // always @(*) 代表任意信号变化(电平变化)时都会发生的操作 begin 行为语句1; 行为语句2; ... end // 例子 always @ (posedge clk) // 只在上升沿触发执行 always @ (posedge clk or negedge clk) // 在上升沿和下降沿都执行
赋值方式
由于verilog
程序所描述的是电路结构,具有天然的并行特性,因此对变量的赋值于软件编程语言存在非常大的不同。
使用准则:
- 在描述组合逻辑的
always
块中使用阻塞赋值=
, 综合成组合逻辑电路结构,这种电路的输出结果只和输入的电平变化有关。 - 在描述时序逻辑的
always
块中使用非阻塞赋值<=
, 综合成时序逻辑电路结构,这种电路的结构往往和触发沿有关,当触发沿满足条件时才发生变化。
注意事项:
- 在同一个 always 块中不能混用阻塞赋值和非阻塞赋值
- 不允许在多个 always 块中对同一个变量进行赋值
下面通过两个例子帮助理解两种赋值方式的不同。
阻塞赋值
所谓阻塞的概念是指,在同一个always块中,后面的赋值语句是在前一句赋值语句结束后才开始赋值的。例如下面的示例
always @(posedge CLK_i or negedge RSTn_i)
begin
if(!RSTn_i)
begin
a = 1;
b = 2;
c = 3;
end
else
begin
a = 0;
b = a;
c = b;
end
end
在复位结束后,有 a = 1; b = 2; c = 3;
第一个上升沿到来的时候,有a = 0; b = 0; c = 0;
,而后的结果就不会再发生变化了。
非阻塞赋值
所谓非阻塞的概念是指,在计算非阻塞赋值的RHS以及更新LHS期间,允许其他的非阻塞赋值语句同时计算RHS和更新LHS。
右边表达式是同一时间计算出来的,并同时更新左边的表达式, 即右边表达式通过组合逻辑计算出来后,马上所存到DFF中
备注: RHS - > 右边的表达式; LHS - > 左边的表达式;
always
开始时,先计算右边表达式的值always
结束后才更新左边表达式的值,只能下次always
执行时才能感知到变量的变化
将阻塞赋值的例子修改为:
always @(posedge CLK_i or negedge RSTn_i)
begin
if(!RSTn_i)
begin
a <= 1;
b <= 2;
c <= 3;
end
else
begin
a <= 0;
b <= a;
c <= b;
end
end
复位结束后,值与阻塞赋值相同,有 a = 1; b = 2; c = 3;
当第一个上升沿到达时,有a = 0; b = 1; c = 2;
当第二个上升沿到达时,有a = 0; b = 0; c = 1;
当第三个上升沿到达时,有a = 0; b = 0; c = 0;
,而后的结果就不会再发生变化了。
可以看到,当使用非阻塞赋值时,右侧的表达式的值是在同一时刻确定的,而阻塞赋值则存在一个先后次序关系。
条件判断
条件判断语句属于过程描述语句,不能单独用来描述组合逻辑电路,if
和case
语句只能出现在always
语句内,如果需要使用条件判断语句描述组合逻辑电路,可以在always @(*)
中进行书写,在综合时会生成多路选择器。
always @(*) begin
if (w1)
// expression
else
// expression
case (w2)
a1: //...
a2: // ...
endcase
end
如果只含有一个判断变量可以使用三元表达式进行描述,在综合的时候会生成 一个 2选1 的多路选择器。
assign w1 = (c1) ? a1 : a2; // c1 为真则选择 a1 输出,否则选择 a2
注意(避免生成Latch):
使用条件判断语句描述组合逻辑电路时需要对所有可能的情况都要进行描述描述,因为verilog
对于没有指定条件的输出采用的”维持输出不变“的原则,此时就需要电路具有状态保持功能,但是组合逻辑电路中并没有”记忆原件“,因此就会在综合的时候为组合逻辑电路加上锁存器(Latch)以”记住“此时的状态。
单分支
逻辑判断使用if () ... else if () ... else
语句表示。具体的语法和c 语言的差不多,这是在书写多条语句的时候需要使用 begin ... end
括起来。
if (condition)
expression;
if (condistion1)
expression1;
else
expression2;
if (condistion1)
expression1;
else if (condistion2)
expression2;
...
else
expression3;
多分支
多条件判断时可以使用 case
语句,它的使用格式是
always @(*)
begin
case (var)
condition1: expression1;
condition2: expression2;
defualt: defualt_expression;
endcase
end
// 支持不定态的casex
always @(*)
begin
casex (in)
4'bxxx1: pos <= 2'd0;
4'bxx10: pos <= 2'd1;
4'bx100: pos <= 2'd2;
4'b1000: pos <= 2'd3;
default: pos <= 2'd0;
endcase
end
尽量避免使用 casex
语句,可以使用casez
语句。
注意:
- 分支表达式的值互不相同
- 所有表达式的位宽必须相等,不3能使用 'bx 代替 n'bx
- 当不需要考虑 x 和 高阻时 使用
casex ... endcase
- 当不需要考虑 高阻时 使用
casez ... endcase
- 0 != x or z
循环语句
软件开发的高级语言大多提供了for
或while
等循环流程控制语句。verilog
中也提供了for
语句,但是在HDL语言中,循环执行的操作一般通过时序逻辑电路完成,for
语句一般只用在testbench 中进行数据生成,在RTL编程中很少进行使用。
主要原因就是for
循环会被综合器做并行展开,每个变量独立占用寄存器资源,如果不能很好地考虑运行逻辑,会造成资源的极大浪费。
常见使用格式如下:
reg [3:0] counter ;
initial begin
counter = 'b0 ;
while (counter<=10) begin
#10 ;
counter = counter + 1'b1 ;
end
end
生成语句
- 批量生成语句
2001标准中新加入的generate
语句可以批量例化多个模块,并设置其中的连线。genvar
声明了generatate
块中使用的变量名。
具体格式如下:
genvar i; // 标识变量
generate
for(init; condistion; next)begin:module_name
// expression
end
endgenerate
可以看到for
中的表达式和C语言中时一样的,只是关键字begin
后面需要跟一个module_name
,该名称指明了generate
所生成模块的名称,可以通过module_name[index]
的方式的访问实例化生成的模块。
例如:
genvar i;
generate
for (i = 1; i <= 510; i=i+1) begin :ut
relu110 u(.l(q[i+1]), .c(q[i]), .r(q[i-1]), .n(q_next[i]));
end
endgenerate
注意:不能将generate for
语句放在 always
语句中
- 条件生成语句
条件生成语句包括 generate if
和generate case
两种。
generate if
语句的格式
generate
if (condition) begin: Name1
//expression
end else begin: Name2
//expression
end
endgenerate
case
语句的格式
generate
case (<constant_expression>)
<value>: begin: <label_1>
<code>
end
<value>: begin: <label_2>
<code>
end
default: begin: <label_3>
<code>
end
endcase
endgenerate
注意: 这段表达是在编译时生效的,因此 condition 应该满足以下几个条件
条件表达式必须是参数或宏定义。
条件表达式必须在编译时求值。
- 生成语句的嵌套
generate for
语句中可以嵌套if
和case
语句用于判断,这样可以提升生成语句的灵活性。例如:
genvar i;
generate
for (i = 0; i <= 511; i=i+1) begin :ut
if (i == 0)
relu110 u(.l(q[i+1]), .c(q[i]), .r(1'b0), .n(q_next[i]));
else if (i == 511)
relu110 u(.l(1'b0), .c(q[i]), .r(q[i-1]), .n(q_next[i]));
else
relu110 u(.l(q[i+1]), .c(q[i]), .r(q[i-1]), .n(q_next[i]));
end
endgenerate
属性语法
属性语法不直接影响模块的逻辑行为,但它们可以提供有关模块和其实例的信息,以帮助综合工具和其他工具正确处理设计。
// 告诉综合工具保留指定的信号或模块,即使它们似乎没有被使用, 方便进行调试查看
(* KEEP="TRUE"*)
// 告诉综合工具不要修改指定的信号或模块,并且不要尝试进行优化。
(* DONT_TOUCH="TRUE"*)
函数
function integer func1(integer pa)
begin
// expresion
// 这是返回值
func1 = //expression;
end
endfunction
常见技巧
- 变量索引与部分选择
part-select 即引用数组中一个元素的某几位。
例:reg [9:0]
a[9:0]
引用第0个元素的后四位:a[0][0+:4]
或 a[0][3-:4]
,其中 -:
和+:
左边可以是常数或者变量,右边必须是常数。(这个会很有用,可以省很多时间写部分索引)
1’sb1
表示 -1,1’sb0
表示 0- 单纯的十进制数为 32bit interger 有符号数
±
运算赋值运算 所有操作数是有符号数,那么结果才是有符号数。
编程规范:
总结verilog
的编程规范,一共10条。
- 不要太多的 if—else 嵌套
- 不要编写过于庞大的状态机
- 复杂的状态机,可以采用 2 段式以上的实现
- 尽量使用时序逻辑完成编程
- 使用组合逻辑不要过于庞大
- 复杂代码拆分简单模块
- 复杂计算,增加流水设计
- 高速模块和低速模块搭配使用
- case 语句一定有 default
- 组合逻辑有 if 必须有 else 防止产生锁存器,锁存器所以出毛刺