⭐️0. 前言
⭐️1. 环境准备
本次使用微软的 VS Code
进行学习,下面介绍开发环境搭建过程。
安装Verilog HDL
和WaveTrace
插件即可进行仿真和追波形,具体搭建过程请参照【Linux下使用VSCode+iVerilog进行Verilog开发】
⭐️2. verilog基础
2.1 数值表示
// 数值
0, 1, x, z
// 数据类型
4'b1011 // 2进制
6'o77 // 8进制
4'd15 // 10进制
32'h3022_c0de // 16进制
// 制定数据位宽
counter = 'd100 ; //一般会根据编译器自动分频位宽,常见的为32bit
counter = 100 ;
counter = 32'h64 ;
// 负数
-6'd15
-15
// 实数
// 普通表示
30.123
6.0
3.0
0.001
// 科学计数,e大小写不分
1.2e4 //大小为12000
1_0001e4 //大小为100010000
1E-3 //大小为0.001
// 字符串
// 一个ASCII占8个bit,共9*8bit
reg [0: 9*8-1] name;
initial begin
name = "liu zewen";
end
2.2 数据类型
1.线网wire
wire 类型表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动。如果没有驱动元件连接到 wire 型变量,缺省值一般为 “Z”。
// 线网wire
wire interrupt;
wire flag1, flag2;
wire gnd = 1'b0;
2.寄存器reg
寄存器(reg)用来表示存储单元,它会保持数据原有的值,直到被改写。通常作为always语句中被赋值的变量或者output变量。
// 寄存器reg
reg clk_temp;
reg flag1, flag2;
3.向量
当位宽大于 1 时,wire 或 reg 即可声明为向量的形式。类似一个python list。
// 向量
reg [3:0] counter ; //声明4bit位宽的寄存器counter
wire [32-1:0] gpio_data; //声明32bit位宽的线型变量gpio_data
wire [8:2] addr ; //声明7bit位宽的线型变量addr,位宽范围为8:2
reg [0:31] data ; //声明32bit位宽的寄存器变量data, 最高有效位为0
// 取位bit
wire [9:0] data_low = data[0:9] ;
addr_temp[3:2] = addr[8:7] + 1'b1 ;
// 可变的向量域选择(可选择任意bit宽度)
reg [31:0] data1 ;
reg [7:0] byte1 [3:0];
integer j ;
always@* begin
for (j=0; j<=3;j=j+1) begin
byte1[j] = data1[(j+1)*8-1 : j*8];
//把data1[7:0]…data1[31:24]依次赋值给byte1[0][7:0]…byte[3][7:0]
end
end
// 支持指定 bit 位后固定位宽的向量域选择访问,类似c的"a+=1"
// [bit+: width] : 从起始 bit 位开始递增,位宽为 width。
//下面 2 种赋值是等效的
B = data1[0+ : 8] ;
B = data1[0:7] ;
// [bit-: width] : 从起始 bit 位开始递减,位宽为 width。
//下面 2 种赋值是等效的
A = data1[31-: 8] ;
A = data1[31:24] ;
// 对信号重新进行组合成新的向量时,需要借助大括号。
wire [31:0] temp1, temp2 ;
assign temp1 = {byte1[0][7:0], data1[31:8]}; //数据拼接
assign temp2 = {32{1'b0}}; //赋值32位的数值0
4.整数,实数,时间寄存器变量
整数,实数,时间等数据类型实际也属于寄存器类型。
// 整数,实数,时间寄存器变量
// 整数使用关键字 integer 来声明,为有符号数,长度与编译器有关。
integer j ; //整型变量,用来辅助生成数字电路,无实际作用
for (j=0; j<=3;j=j+1) begin
...
end
// 实数(real),类似于#define
real data1 ;
integer temp ;
initial begin
data1 = 2e3 ;
data1 = 3.75 ;
end
initial begin
temp = data1 ; //temp 值的大小为3
end
// 时间(time),特殊的时间寄存器 time 型变量,宽度一般为 64 bit
time current_time ;
initial begin
#100 ;
//current_time 的大小为 100,系统函数 $time 获取当前仿真时间。
current_time = $time ;
end
5.数组
在 Verilog 中允许声明 reg, wire, integer, time, real 及其向量类型的数组。
数组维数没有限制。线网数组也可以用于连接实例模块的端口。数组中的每个元素都可以作为一个标量或者向量,以同样的方式来使用,形如:<数组名>[<下标>]。对于多维数组来讲,用户需要说明其每一维的索引。例如:
// 数组,最后一个索引才为变量的bit位访问
// 创建数组
integer flag [7:0] ; //8个整数组成的数组
reg [3:0] counter [3:0] ; //由4个4bit计数器组成的数组
wire [7:0] addr_bus [3:0] ; //由4个8bit wire型变量组成的数组
wire data_bit[7:0][5:0] ; //声明1bit wire型变量的二维数组
reg [31:0] data_4d[11:0][3:0][3:0][255:0] ; //声明4维的32bit数据变量数组
// 访问数组元素
flag [1] = 32'd0 ; //将flag数组中第二个元素赋值为32bit的0值
counter[3] = 4'hF ; //将数组counter中第4个元素的值赋值为4bit 十六进制数F,等效于counter[3][3:0] = 4'hF,即可省略宽度;
assign addr_bus[0] = 8'b0 ; //将数组addr_bus中第一个元素的值赋值为0
assign data_bit[0][1] = 1'b1; //将数组data_bit的第1行第2列的元素赋值为1,这里不能省略第二个访问标号,即 assign data_bit[0] = 1'b1; 是非法的。
data_4d[0][0][0][0][15:0] = 15'd3 ; //将数组data_4d中标号为[0][0][0][0]的寄存器单元的15~0bit赋值为3
6.存储器
存储器变量就是一种寄存器数组,可用来描述 RAM 或 ROM 的行为。
// 存储器
reg membit[0:255] ; //256bit的1bit存储器
reg [7:0] mem[0:1023] ; //1Kbyte存储器,位宽8bit,类似8位MCU,共1024个地址的寄存器
mem[511] = 8'b0 ; //令第512个8bit的存储单元值为0
7.参数
参数用来表示常量,用关键字 parameter 声明,只能赋值一次;局部参数用 localparam 来声明,其作用和用法与 parameter 相同,区别在于它的值不能被改变。
// 参数,类似于const,不可再写,但例化后可在外部改写
parameter data_width = 10'd32 ;
parameter i=1, j=2, k=3 ;
parameter mem_size = data_width * 10 ;
// 局部参数用 localparam 来声明,不可再修改。例化后也不行
localparam data;
8.字符串
字符串保存在 reg 类型的变量中,每个字符占用一个字节(8bit)。因此寄存器变量的宽度应该足够大,以保证不会溢出。
字符串不能多行书写,即字符串中不能包含回车符。如果寄存器变量的宽度大于字符串的大小,则使用 0 来填充左边的空余位;如果寄存器变量的宽度小于字符串大小,则会截去字符串左边多余的数据。
与C语言一样,verilog语言的字符串也存在转义字符,不再赘述。
转义字符 | 显示字符 |
---|---|
\n | 换行 |
\t | 制表符 |
%% | % |
\ | \ |
" | " |
\ooo | 1到3个8进制数字字符 |
举个栗子:
// 字符串
reg [0: 9*8-1] str ;// 需要 9*8bit 的存储单元
initial begin
str = "liu zewen";
end
2.3 表达式
1.表达式
表达式由操作符和操作数构成,其目的是根据操作符的意义得到一个计算结果,即算数。
a^b ; //a与b进行异或操作
address[9:0] + 10'b1 ; //地址累加
flag1 && flag2 ; //逻辑与操作
2.操作数
操作数可以是任意的数据类型,只是某些特定的语法结构要求使用特定类型的操作数。
操作数可以为常数,整数,实数,线网,寄存器,时间,位选,域选,存储器及函数调用等。
a^b ; //a与b进行异或操作
address[9:0] + 10'b1 ; //地址累加
flag1 && flag2 ; //逻辑与操作
3.操作符
Verilog 中提供了大约 9 种操作符,分别是算术、关系、等价、逻辑、按位、归约、移位、拼接、条件操作符。
大部分操作符与 C 语言中类似。同类型操作符之间,除条件操作符从右往左关联,其余操作符都是自左向右关联。圆括号内表达式优先执行。例如下面每组的 2 种写法都是等价的。
//自右向左关联,两种写法等价
A+B-C ;
(A+B)-C ;
//自右向左关联,两种写法等价,结果为 B、D 或 F
A ? B : C ? D : F ;
A ? B : (C ? D : F) ;
//自右向左关联,两种写法不等价
(A ? B : C) ? D : F ; //结果 D 或 F
A ? B : C ? D : F ; //结果为 B、D 或 F
- 操作符优先级,可以用圆括号括起来
操作符 | 操作符号 | 优先级 |
---|---|---|
单目运算 | + - ! ~ | 最高 |
乘、除、取模 | * / % | |
加减 | + - | |
移位 | << >> | |
关系 | < <= > >= | |
等价 | == != === !=== | |
归约 | & ~& | |
^ ~^ | ||
| ~| | ||
逻辑 | && | |
|| | ||
条件 | ?: | 最低 |
4.算术操作符
算术操作符包括单目操作符和双目操作符。
双目操作符对 2 个操作数进行算术运算,包括乘(*)、除(/)、加(+)、减(-)、求幂(**)、取模(%)。
// 算术操作符
reg [3:0] a, b;
reg [4:0] c ;
a = 4'b0010 ;
b = 4'b1001 ;
c = a+b; //结果为c=b'b1011
c = a/b; //结果为c=4,取整
// 如果操作数某一位为 X,则计算结果也会全部出现 X。未知算出未知。
b = 4'b100x ;
c = a+b ; //结果为c=4'bxxxx
5.关系操作符
关系操作符有大于(>),小于(<),大于等于(>=),小于等于(<=)。
关系操作符的正常结果有 2 种,真(1)或假(0)。
如果操作数中有一位为 x 或 z,则关系表达式的结果为 x。
A = 4 ;
B = 3 ;
X = 3'b1xx ;
A > B //为真
A <= B //为假
A >= Z //为X,不确定
6.等价操作符
等价操作符包括逻辑相等(),逻辑不等(!=),全等(=),非全等(!==)。
等价操作符的正常结果有 2 种:为真(1)或假(0)。
逻辑相等/不等操作符不能比较 x 或 z,当操作数包含一个 x 或 z,则结果为不确定值。
全等比较时,如果按位比较有相同的 x 或 z,返回结果也可以为 1,即全等比较可比较 x 或 z。所以,全等比较的结果一定不包含 x。举例如下:
A = 4 ;
B = 8'h04 ;
C = 4'bxxxx ;
D = 4'hx ;
A == B //为真
A == (B + 1) //为假
A == C //为X,不确定
A === C //为假,返回值为0
C === D //为真,返回值为1
7.逻辑操作符
逻辑操作符主要有 3 个:&&(逻辑与), ||(逻辑或),!(逻辑非)。
逻辑操作符的计算结果是一个 1bit 的值,0 表示假,1 表示真,x 表示不确定。
如果一个操作数不为 0,它等价于逻辑 1;如果一个操作数等于 0,它等价于逻辑 0。如果它任意一位为 x 或 z,它等价于 x。
如果任意一个操作数包含 x,逻辑操作符运算结果不一定为 x。
逻辑操作符的操作数可以为变量,也可以为表达式。例如:
A = 3;
B = 0;
C = 2'b1x ;
A && B // 为假
A || B // 为真
! A // 为假
! B // 为真
A && C // 为X,不确定
A || C // 为真,因为A为真
(A==2) && (! B) //为真,此时第一个操作数为表达式
8.按位操作符
按位操作符包括:取反(),与(&),或(|),异或(^),同或(^)。
按位操作符对 2 个操作数的每 1bit 数据进行按位操作。
如果 2 个操作数位宽不相等,则用 0 向左扩展补充较短的操作数。
取反操作符只有一个操作数,它对操作数的每 1bit 数据进行取反操作。
下图给出了按位操作符的逻辑规则。
&(与) | 0 | 1 | x | |(或) | 0 | 1 | x | |
---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 1 | x | |
1 | 0 | 1 | x | 1 | 1 | 1 | 1 | |
x | 0 | x | x | x | x | 1 | x |
^(异或) | 0 | 1 | x | ~^(同或) | 0 | 1 | x | |
---|---|---|---|---|---|---|---|---|
0 | 0 | 1 | x | 0 | 1 | 0 | x | |
1 | 1 | 0 | x | 1 | 0 | 1 | x | |
x | x | x | x | x | x | x | x |
A = 4'b0101 ;
B = 4'b1001 ;
C = 4'bx010 ;
~A //4'b1010
A & B //4'b0001
A | B //4'b1101
A^B //4'b1100
A ~^ B //4'b0011
B | C //4'b1011
B&C //4'bx000
9.归约操作符
归约操作符包括:归约与(&),归约与非(&),归约或(|),归约或非(|),归约异或(),归约同或(~)。
归约操作符只有一个操作数,它对这个向量操作数逐位进行操作,最终产生一个 1bit 结果。
逻辑操作符、按位操作符和归约操作符都使用相同的符号表示,因此有时候容易混淆。区分这些操作符的关键是分清操作数的数目,和计算结果的规则。
A = 4'b1010 ;
&A ; //结果为 1 & 0 & 1 & 0 = 1'b0,可用来判断变量A是否全1
~|A ; //结果为 ~(1 | 0 | 1 | 0) = 1'b0, 可用来判断变量A是否为全0
^A ; //结果为 1 ^ 0 ^ 1 ^ 0 = 1'b0
10.移位操作符
移位操作符包括左移(<<),右移(>>),算术左移(<<<),算术右移(>>>)。
移位操作符是双目操作符,两个操作数分别表示要进行移位的向量信号(操作符左侧)与移动的位数(操作符右侧)。
算术左移和逻辑左移时,右边低位会补 0。
逻辑右移时,左边高位会补 0;而算术右移时,左边高位会补充符号位,以保证数据缩小后值的正确性。
A = 4'b1100 ;
B = 4'b0010 ;
A = A >> 2 ; //结果为 4'b0011
A = A << 1; //结果为 4'b1000
A = A <<< 1 ; //结果为 4'b1000
C = B + (A>>>2); //结果为 2 + (-4/4) = 1, 4'b0001
11.拼接操作符
拼接操作符用大括号 {,} 来表示,用于将多个操作数(向量)拼接成新的操作数(向量),信号间用逗号隔开。
拼接符操作数必须指定位宽,常数的话也需要指定位宽。例如:
A = 4'b1010 ;
B = 1'b1 ;
Y1 = {B, A[3:2], A[0], 4'h3 }; //结果为Y1='b1100_0011
Y2 = {4{B}, 3'd4}; //结果为 Y2=7'b111_1100
Y3 = {32{1'b0}}; //结果为 Y3=32h0,常用作寄存器初始化时匹配位宽的赋初值
12.按位操作符
按位操作符包括:取反(),与(&),或(|),异或(^),同或(^)。
A = 4'b0101 ;
B = 4'b1001 ;
C = 4'bx010 ;
~A //4'b1010
A & B //4'b0001
A | B //4'b1101
A^B //4'b1100
A ~^ B //4'b0011
B | C //4'b1011
B&C //4'bx000
13.条件操作符
条件表达式有 3 个操作符,结构描述如下:
condition_expression ? true_expression : false_expression
计算时,如果 condition_expression 为真(逻辑值为 1),则运算结果为 true_expression;如果 condition_expression 为假(逻辑值为 0),则计算结果为 false_expression。
计算时,如果 condition_expression 为真(逻辑值为 1),则运算结果为 true_expression;如果 condition_expression 为假(逻辑值为 0),则计算结果为 false_expression。
其实,条件表达式类似于 2 路(或多路)选择器,其描述方式完全可以用 if-else 语句代替。
当然条件操作符也能进行嵌套,完成一个多次选择的逻辑。例如:
// a = b > c ? d : e;
assign hsel = (addr[9:8] == 2'b00) ? hsel_p1 :
(addr[9:8] == 2'b01) ? hsel_p2 :
(addr[9:8] == 2'b10) ? hsel_p3 :
(addr[9:8] == 2'b11) ? hsel_p4 ;
2.4编译指令
以反引号 `` `开始的某些标识符是 Verilog 系统编译指令。
编译指令为 Verilog 代码的撰写、编译、调试等提供了极大的便利。
下面介绍下完整的 8 种编译指令,其中前 4 种使用频率较高。
// 一旦 `define 指令被编译,其在整个编译过程中都会有效。
`define DATA_DW 32
`define S $stop;
//用`S来代替系统函数$stop; (包括分号)
`define WORD_DEF reg [31:0]
//可以用`WORD_DEF来声明32bit寄存器变量
// `undef 用来取消之前的宏定义。
`define DATA_DW 32
reg [DATA_DW-1:0] data_in ;
`undef DATA_DW
// 条件编译指令
`ifdef, `ifndef, `elsif, `else, `endif
`ifdef MCU51
parameter DATA_DW = 8 ;
`elsif WINDOW
parameter DATA_DW = 64 ;
`else
parameter DATA_DW = 32 ;
`endif
// `include引用文件
`include "../../param.v"
`include "header.v"
// `timescale用于定义时延、仿真的单位和精度。(`timescale time_unit / time_precision)
// time_unit 表示时间单位,time_precision 表示时间精度,它们均是由数字以及单位 s(秒),ms(毫秒),us(微妙),ns(纳秒),ps(皮秒)和 fs(飞秒)组成。
// 时间精度可以和时间单位一样,但是时间精度大小不能超过时间单位大小,例如下面例子中,输出端 Z 会延迟 5.21ns 输出 A&B 的结果。
`timescale 1ns/100ps //时间单位为1ns,精度为100ps,合法
//`timescale 100ps/1ns //不合法
module AndFunc(Z, A, B);
output Z;
input A, B ;
assign #5.207 Z = A & B
endmodule
// 在编译过程中,`timescale 指令会影响后面所有模块中的时延值,直至遇到另一个 `timescale 指令或 `resetall 指令。
// 由于在 Verilog 中没有默认的 `timescale,如果没有指定 `timescale,Verilog 模块就有会继承前面编译模块的 `timescale 参数。有可能导致设计出错。
// 如果一个设计中的多个模块都带有 `timescale 时,模拟器总是定位在所有模块的最小时延精度上,并且所有时延都相应地换算为最小时延精度,时延单位并不受影响。
// `default_nettype
// 该指令用于为隐式的线网变量指定为线网类型,即将没有被声明的连线定义为线网类型。
`default_nettype wand
// 该实例定义的缺省的线网为线与类型。因此,如果在此指令后面的任何模块中的连线没有说明,那么该线网被假定为线与类型。
`default_nettype none
// 该实例定义后,将不再自动产生 wire 型变量。
// 例如下面第一种写法编译时不会报 Error
// Z1 无定义就使用,系统默认Z1为wire型变量,有 Warning 无 Error。
module test_and(
input A,
input B,
output Z);
assign Z1 = A & B ;
endmodule
// 第二种写法编译将不会通过。
// Z1无定义就使用,由于编译指令的存在,系统会报Error,从而检查出书写错误
`default_nettype none
module test_and(
input A,
input B,
output Z);
assign Z1 = A & B ;
endmodule
// `resetall将所有的编译指令重新设置为缺省值(默认值)。
// `resetall 可以使得缺省连线类型为线网类型。
// 当 `resetall 加到模块最后时,可以将当前的 `timescale 取消防止进一步传递,只保证当前的 `timescale 在局部有效,避免 `timescale 的错误继承。
// `celldefine, `endcelldefine 用于将模块标记为单元模块,他们包含模块的定义。例如一些与、或、非门,一些 PLL 单元,PAD 模型,以及一些 Analog IP 等。
`celldefine
module (
input clk,
input rst,
output clk_pll,
output flag);
……
endmodule
`endcelldefine
// `unconnected_drive, `nounconnected_drive 在模块实例化中,出现在这两个编译指令间的任何未连接的输入端口,为正偏电路状态或者为反偏电路状态。
`unconnected_drive pull1
. . .
/ *在这两个程序指令间的所有未连接的输入端口为正偏电路状态(连接到高电平) * /
`nounconnected_drive
`unconnected_drive pull0
. . .
/ *在这两个程序指令间的所有未连接的输入端口为反偏电路状态(连接到低电平) * /
`nounconnected_drive
2.5连续赋值
assign, 全加器,连续赋值语句是 Verilog 数据流建模的基本语句,用于对 wire 型变量进行赋值,格式如下:
assign LHS_target = RHS_expression ;
- 其中
LHS_target
必须是一个标量或者线型向量,而不能是寄存器类型。 RHS_expression
的类型没有要求,可以是标量或线型或存器向量,也可以是函数调用。- 只要
RHS_expression
表达式的操作数有事件发生(值的变化)时,RHS_expression
就会立刻重新计算,同时赋值给LHS_target
。
举个栗子:
wire Cout, A, B ;
assign Cout = A & B ; //实现计算A与B的功能
当然也可在声明时赋值,但要注意,wire
型变量只能被赋值一次,因此该种连续赋值方式也只能有一次,效果与上面一致。
wire A, B ;
wire Cout = A & B ;
用连续赋值assign设计一个全加器:
// 设计一个全加器
// So = Ai ⊕ Bi ⊕ Ci ;
// Co = AiBi + Ci(Ai+Bi)
// 写法1
module full_adder1(
input Ai, Bi, Ci,
output So, Co);
assign So = Ai ^ Bi ^ Ci ;
assign Co = (Ai & Bi) | (Ci & (Ai | Bi));
endmodule
// 写法2
module full_adder1(
input Ai, Bi, Ci
output So, Co);
assign {Co, So} = Ai + Bi + Ci ;
endmodule
// testbench
`timescale 1ns/1ns
module test ;
reg Ai, Bi, Ci ;
wire So, Co ;
initial begin
{Ai, Bi, Ci} = 3'b0;
forever begin
#10 ;
{Ai, Bi, Ci} = {Ai, Bi, Ci} + 1'b1;
end
end
full_adder1 u_adder(
.Ai (Ai),
.Bi (Bi),
.Ci (Ci),
.So (So),
.Co (Co));
initial begin
forever begin
#100;
//$display("---gyc---%d", $time);
if ($time >= 1000) begin
$finish ;
end
end
end
endmodule
2.6时延
时延一般是不可综合的。
// 时延
// 时延一般不可综合
//普通时延,A&B计算结果延时10个时间单位赋值给Z
wire Z, A, B ;
assign #10 Z = A & B ;
//隐式时延,声明一个wire型变量时对其进行包含一定时延的连续赋值。
wire A, B;
wire #10 Z = A & B;
//声明时延,声明一个wire型变量是指定一个时延。因此对该变量所有的连续赋值都会被推迟到指定的时间。除非门级建模中,一般不推荐使用此类方法建模。
wire A, B;
wire #10 Z;
assign Z = A & B;
惯性时延
在上述例子中,A 或 B 任意一个变量发生变化,那么在 Z 得到新的值之前,会有 10 个时间单位的时延。如果在这 10 个时间单位内,即在 Z 获取新的值之前,A 或 B 任意一个值又发生了变化,那么计算 Z 的新值时会取 A 或 B 当前的新值。所以称之为惯性时延,即信号脉冲宽度小于时延时,对输出没有影响。 因此仿真时,时延一定要合理设置,防止某些信号不能进行有效的延迟。
2.7过程结构
-
过程结构语句有 2 种,initial 与 always 语句。它们是行为级建模的 2 种基本语句。
-
一个模块中可以包含多个 initial 和 always 语句,但 2 种语句不能嵌套使用。
-
这些语句在模块间并行执行,与其在模块的前后顺序没有关系。
-
但是 initial 语句或 always 语句内部可以理解为是顺序执行的(非阻塞赋值除外)。
-
每个 initial 语句或 always 语句都会产生一个独立的控制流,执行时间都是从 0 时刻开始。
// initial语句,里面是是串行的,单次执行
`timescale 1ns/1ns
module test ;
reg ai, bi ;
initial begin
ai = 0 ;
#25 ; ai = 1 ;
#35 ; ai = 0 ; //absolute 60ns
#40 ; ai = 1 ; //absolute 100ns
#10 ; ai = 0 ; //absolute 110ns
end
initial begin
bi = 1 ;
#70 ; bi = 0 ; //absolute 70ns
#20 ; bi = 1 ; //absolute 90ns
end
//at proper time stop the simulation
initial begin
forever begin
#100;
//$display("---gyc---%d", $time);
if ($time >= 1000) begin
$finish ;
end
end
end
endmodule
// always语句,阻塞性为顺序,循环执行
`timescale 1ns/1ns
module test ;
parameter CLK_FREQ = 100 ; //100MHz
parameter CLK_CYCLE = 1e9 / (CLK_FREQ * 1e6) ; //switch to ns
reg clk ;
initial clk = 1'b0 ; //clk is initialized to "0"
always # (CLK_CYCLE/2) clk = ~clk ; //generating a real clock by reversing
always begin
#10;
if ($time >= 1000) begin
$finish ;
end
end
endmodule
2.8过程赋值
- 分为阻塞赋值,非阻塞赋值,并行。
- 过程性赋值是在 initial 或 always 语句块里的赋值,赋值对象是寄存器、整数、实数等类型。
- 这些变量在被赋值后,其值将保持不变,直到重新被赋予新值。
- 连续性赋值总是处于激活状态,任何操作数的改变都会影响表达式的结果;过程赋值只有在语句执行的时候,才会起作用。这是连续性赋值与过程性赋值的区别。
- Verilog 过程赋值包括 2 种语句:阻塞赋值与非阻塞赋值。
- 不要在一个过程结构中混合使用阻塞赋值与非阻塞赋值。两种赋值方式混用时,时序不容易控制,很容易得到意外的结果。
- 在设计电路时,always 时序逻辑块中多用非阻塞赋值,always 组合逻辑块中多用阻塞赋值;在仿真电路时,initial 块中一般多用阻塞赋值。
// 阻塞性赋值
// 阻塞赋值属于顺序执行,即下一条语句执行前,当前语句一定会执行完毕。
// 阻塞赋值语句使用等号 = 作为赋值符。
a = b + c;
// 非阻塞赋值
// 非阻塞赋值属于并行执行语句,即下一条语句的执行和当前语句的执行是同时进行的,它不会阻塞位于同一个语句块中后面语句的执行。
// 非阻塞赋值语句使用小于等于号 <= 作为赋值符。
a <= b + c;
- 如何使用非阻塞赋值避免竞争冒险?
// 交换a和b的值
// 交换失败,ab相等
always @(posedge clk) begin
a = b ;
end
always @(posedge clk) begin
b = a;
end
// 交换成功,使用上一个时钟的旧值
always @(posedge clk) begin
a <= b ;
end
always @(posedge clk) begin
b <= a;
end
2.9时序控制
Verilog 提供了 2 大类时序控制方法:时延控制和事件控制。事件控制主要分为边沿触发事件控制与电平敏感事件控制。
// 时延控制,先等待,后计算。
reg value_test ;
reg value_general ;
// 写法1
#10 value_general = value_test ;
// 写法2
#10 ;
value_general = value_test ;
// 内嵌时延,先计算,延时后赋值
reg value_test ;
reg value_embed ;
value_embed = #10 value_test ;
// 边沿触发事件控制,事件控制用符号 @ 表示。
// 一般事件控制
// 双边沿
always @(clk) q <= d ;
// 正边沿
always @(posedge clk) q <= d ;
// 负边沿
always @(negedge clk) q <= d ;
// 立刻计算d的值,并在clk上升沿时刻赋值给q,不推荐这种写法
q = @(posedge clk) d ;
// 命名事件控制,类似于c中的一个标记位flag,命名事件用关键字 event 来声明,触发信号用 -> 表示。
event start_receiving ;// 标记位,事件
always @( posedge clk_samp) begin
-> start_receiving ; //采样时钟上升沿作为时间触发时刻
end
always @(start_receiving) begin
data_buf = {data_if[0], data_if[1]} ; //触发时刻,对多维数据整合
end
// 敏感列表,关键字 or 连接,也可以用逗号。
always @(posedge clk or negedge rstn) begin
//always @(posedge clk , negedge rstn) begin
//也可以使用逗号陈列多个事件触发
if(! rstn)begin
q <= 1'b ;
end
else begin
q <= d ;
end
end
// 当组合逻辑输入变量很多时,写法是 @* 或 @(*),表示对语句块中的所有输入变量的变化都是敏感的。
always @(*) begin
//always @(a, b, c, d, e, f, g, h, i, j, k, l, m) begin
//两种写法等价
assign s = a? b+c : d ? e+f : g ? h+i : j ? k+l : m ;
end
// 电平敏感事件控制,使用关键字 wait 来表示这种电平敏感情况。
initial begin
wait (start_enable) ; //等待 start 信号
forever begin
//start信号使能后,在clk_samp上升沿,对数据进行整合
@(posedge clk_samp) ;
data_buf = {data_if[0], data_if[1]} ;
end
end
2.10语句块
Verilog 语句块提供了将两条或更多条语句组成语法结构上相当于一条一句的机制。主要包括两种类型:顺序块和并行块。
// 顺序块用关键字 begin 和 end 来表示。
// 并行块有关键字 fork 和 join 来表示,并行块中的语句是并行执行的,即便是阻塞形式的赋值。
`timescale 1ns/1ns
module test ;
reg [3:0] ai_sequen, bi_sequen ;
reg [3:0] ai_paral, bi_paral ;
reg [3:0] ai_nonblk, bi_nonblk ;
//============================================================//
//(1)Sequence block
initial begin
#5 ai_sequen = 4'd5 ; //at 5ns
#5 bi_sequen = 4'd8 ; //at 10ns
end
//(2)fork block
initial fork
#5 ai_paral = 4'd5 ; //at 5ns
#5 bi_paral = 4'd8 ; //at 5ns
join
//(3)non-block block
initial fork
#5 ai_nonblk <= 4'd5 ; //at 5ns
#5 bi_nonblk <= 4'd8 ; //at 5ns
join
endmodule
// 其中顺序块和并行块还可以嵌套使用,让代码的某一部分同步进行。
`timescale 1ns/1ns
module test ;
reg [3:0] ai_sequen2, bi_sequen2 ;
reg [3:0] ai_paral2, bi_paral2 ;
initial begin
ai_sequen2 = 4'd5 ; //at 0ns
fork
#10 ai_paral2 = 4'd5 ; //at 10ns
#15 bi_paral2 = 4'd8 ; //at 15ns
join
#20 bi_sequen2 = 4'd8 ; //at 35ns
end
endmodule
// 命名块,可以给块语句结构命名。命名的块中可以声明局部变量,通过层次名引用的方法对变量进行访问。
`timescale 1ns/1ns
module test;
initial begin: runoob //命名模块名字为runoob,分号不能少
integer i ; //此变量可以通过test.runoob.i 被其他模块使用
i = 0 ;
forever begin
#10 i = i + 10 ;
end
end
reg stop_flag ;
initial stop_flag = 1'b0 ;
always begin : detect_stop
if ( test.runoob.i == 100) begin //i累加10次,即100ns时停止仿真
$display("Now you can stop the simulation!!!");
stop_flag = 1'b1 ;
end
#10 ;
end
endmodule
// 可以通过块名禁用块
`timescale 1ns/1ns
module test;
initial begin: runoob_d //命名模块名字为runoob_d
integer i_d ;
i_d = 0 ;
while(i_d<=100) begin: runoob_d2
# 10 ;
if (i_d >= 50) begin //累加5次停止累加
disable runoob_d3.clk_gen ;//stop 外部block: clk_gen
disable runoob_d2 ; //stop 当前block: runoob_d2
end
i_d = i_d + 10 ;
end
end
reg clk ;
initial begin: runoob_d3
while (1) begin: clk_gen //时钟产生模块
clk=1 ; #10 ;
clk=0 ; #10 ;
end
end
endmodule
2.11条件语句
条件(if)语句用于控制执行语句要根据条件判断来确定是否执行。
// if语句
module mux4to1(
input [1:0] sel ,
input [1:0] p0 ,
input [1:0] p1 ,
input [1:0] p2 ,
input [1:0] p3 ,
output [1:0] sout);
reg [1:0] sout_t ;
always @(*) begin
if (sel == 2'b00)
sout_t = p0 ;
else if (sel == 2'b01)
sout_t = p1 ;
else if (sel == 2'b10)
sout_t = p2 ;
else
sout_t = p3 ;
end
assign sout = sout_t ;
endmodule
// 条件语句中加入 begin 与 and 关键字就是一个很好的习惯。
if(en) begin
if(sel == 2'b1) begin
sout = p1s ;
end
else begin
sout = p0 ;
end
end
2.12多路分支语句
- case 语句是一种多路条件分支的形式,可以解决 if 语句中有多个条件选项时使用不方便的问题。
- casex、 casez 语句是 case 语句的变形,用来表示条件选项中的无关项。
- casex 用 “x” 来表示无关值,casez 用问号 “?” 来表示无关值。
- casex、casez 一般是不可综合的,多用于仿真。
// case多路选择器
module mux4to1(
input [1:0] sel ,
input [1:0] p0 ,
input [1:0] p1 ,
input [1:0] p2 ,
input [1:0] p3 ,
output [1:0] sout);
reg [1:0] sout_t ;
always @(*)
case(sel)
2'b00: begin
sout_t = p0 ;
end
2'b01: sout_t = p1 ;
2'b10: sout_t = p2 ;
default: sout_t = p3 ;
endcase
assign sout = sout_t ;
endmodule
// 判断的条件可以是多个,值也可以是x 或 z 。
case(sel)
2'b00: sout_t = p0 ;
2'b01: sout_t = p1 ;
2'b10: sout_t = p2 ;
2'b11: sout_t = p3 ;
2'bx0, 2'bx1, 2'bxz, 2'bxx, 2'b0x, 2'b1x, 2'bzx :
sout_t = 2'bxx ;
2'bz0, 2'bz1, 2'bzz, 2'b0z, 2'b1z :
sout_t = 2'bzz ;
default: $display("Unexpected input control!!!");
endcase
// casex,casez
module mux4to1(
input [3:0] sel ,
input [1:0] p0 ,
input [1:0] p1 ,
input [1:0] p2 ,
input [1:0] p3 ,
output [1:0] sout);
reg [1:0] sout_t ;
always @(*)
casez(sel)
4'b???1: sout_t = p0 ;
4'b??1?: sout_t = p1 ;
4'b?1??: sout_t = p2 ;
4'b1???: sout_t = p3 ;
default: sout_t = 2'b0 ;
endcase
// always @(*)
// casex(sel)
// 4'bxxx1: sout_t = p0 ;
// 4'bxx1x: sout_t = p1 ;
// 4'bx1xxx: sout_t = p2 ;
// 4'b1xxx: sout_t = p3 ;
// default: sout_t = 2'b0 ;
// endcase
assign sout = sout_t ;
endmodule
2.13循环语句
Verilog 循环语句有 4 种类型,分别是 while,for,repeat,和 forever 循环。循环语句只能在 always 或 initial 块中使用,但可以包含延迟表达式。
// while 循环
counter = 'b0 ;
while (counter<=10) begin
#10 ;
counter = counter + 1'b1 ;
end
// for 循环
integer i ;
reg [3:0] counter2 ;
initial begin
counter2 = 'b0 ;
for (i=0; i<=10; i=i+1) begin
#10 ;
counter2 = counter2 + 1'b1 ;
end
end
// repeat 循环,执行固定次数的循环,repeat 循环的次数必须是一个常量、变量或信号。
reg [3:0] counter3 ;
integer a = 8;
initial begin
counter3 = 'b0 ;
repeat (11) begin //重复11次
#10 ;
counter3 = counter3 + 1'b1 ;
end
// 即使运行过程中,a的值发生了变化,repeat也只执行8次,其取的repeat运行开始时a的值。
repeat (a) begin //重复8次
#10 ;
a = a + 1'b1 ;
end
end
// forever 循环,表示永久循环,不包含任何条件表达式,系统函数 $finish 可退出 forever。
reg clk ;
initial begin
clk = 0 ;
forever begin
clk = ~clk ;
#5 ;
end
end
reg clk ;
reg data_in, data_temp ;
initial begin
forever @(posedge clk) data_temp = data_in ;
end
2.14过程连续赋值
-
过程连续赋值是过程赋值的一种。这种赋值语句能够替换其他所有 wire 或 reg 的赋值,改写了 wire 或 reg 型变量的当前值。
-
与过程赋值不同的是,过程连续赋值的表达式能被连续的驱动到 wire 或 reg 型变量中,即过程连续赋值发生作用时,右端表达式中任意操作数的变化都会引起过程连续赋值语句的重新执行。
-
过程连续性赋值主要有 2 种,assign-deassign 和 force-release。
-
其中assign-deassign用于reg类型,可连续赋值和取消连续赋值。
-
force-release用于强制信号,reg信号在放开后不会立马被刷新,会等电路驱动时才会刷新,但是wire在放开后会立马回归原来的值。
// assign(过程赋值操作)与 deassign (取消过程赋值操作)表示第一类过程连续赋值语句。赋值对象只能是寄存器或寄存器组,而不能是 wire 型变量。
// 赋值过程中对寄存器连续赋值,寄存器中的值被保留直到被重新赋值。
// 一个带复位端的 D 触发器:
module dff_assign(
input rstn,
input clk,
input D,
output reg Q
);
always @(posedge clk) begin
Q <= D ; //Q = D at posedge of clock
end
always @(negedge rstn) begin
if(!rstn) begin
assign Q = 1'b0 ; //change Q value when reset effective
end
else begin //cancel the Q value overlay,
deassign Q ; //and Q remains 0-value until the coming of clock posedge
end
end
endmodule
// force (强制赋值操作)与 release(取消强制赋值)表示第二类过程连续赋值语句。
// 使用方法和效果,和 assign 与 deassign 类似,但赋值对象可以是 reg 型变量,也可以是 wire 型变量。
// 因为是无条件强制赋值,一般多用于交互式调试过程,不要在设计模块中使用。
// 当 force 作用在寄存器上时,寄存器当前值被覆盖;release 时该寄存器值将继续保留强制赋值时的值。之后,该寄存器的值可以被原有的过程赋值语句改变。
// 当 force 作用在线网上时,线网值也会被强制赋值。但是,一旦 release 该线网型变量,其值马上变为原有的驱动值。
`timescale 1ns/1ns
module test ;
reg rstn ;
reg clk ;
reg [3:0] cnt ;
wire cout ;
counter10 u_counter (
.rstn (rstn),
.clk (clk),
.cnt (cnt),
.cout (cout));
initial begin
clk = 0 ;
rstn = 0 ;
#10 ;
rstn = 1'b1 ;
wait (test.u_counter.cnt_temp == 4'd4) ;
@(negedge clk) ;
force test.u_counter.cnt_temp = 4'd6 ;
force test.u_counter.cout = 1'b1 ;
#40 ;
@(negedge clk) ;
release test.u_counter.cnt_temp ;
release test.u_counter.cout ;
end
initial begin
clk = 0 ;
forever #10 clk = ~ clk ;
end
//finish the simulation
always begin
#1000;
if ($time >= 1000) $finish ;
end
endmodule // test
2.15模块与端口
结构建模方式有 3 类描述语句: Gate(门级)例化语句,UDP (用户定义原语)例化语句和 module (模块) 例化语句。本次主要讲述使用最多的模块级例化语句。
// 模块
// 模块是 Verilog 中基本单元的定义形式,是与外界交互的接口。
module module_name (port_list) ;
Declarations_and_Statements ;
endmodule
// 端口
// 端口是模块与外界交互的接口。对于外部环境来说,模块内部是不可见的,对模块的调用只能通过端口连接进行。
// 模块的定义中包含一个可选的端口列表,一般将不带类型、不带位宽的信号变量罗列在模块声明里。下面是一个 PAD 模型的端口列表:
module pad(
DIN, OEN, PULL,
DOUT, PAD);
- 端口信号在端口列表中罗列出来以后,就可以在模块实体中进行声明了。根据端口的方向,端口类型有 3 种: 输入(input),输出(output)和双向端口(inout)。 input、inout 类型不能声明为 reg 数据类型,因为 reg 类型是用于保存数值的,而输入端口只能反映与其相连的外部信号的变化,不能保存这些信号的值。output 可以声明为 wire 或 reg 数据类型。
//端口类型声明
input DIN, OEN ;
input [1:0] PULL ; //(00,01-dispull, 11-pullup, 10-pulldown)
inout PAD ; //pad value
output DOUT ; //pad load when pad configured as input
//端口数据类型声明
wire DIN, OEN ;
wire [1:0] PULL ;
wire PAD ;
reg DOUT ;
- 在 Verilog 中,端口隐式的声明为 wire 型变量,即当端口具有 wire 属性时,不用再次声明端口类型为 wire 型。但是,当端口有 reg 属性时,则 reg 声明不可省略。
input DIN, OEN ;
input [1:0] PULL ;
inout PAD ;
output DOUT ;
reg DOUT ;
- 当然,信号 DOUT 的声明完全可以合并成一句:
output reg DOUT ;
- 还有一种更简洁且常用的方法来声明端口,即在 module 声明时就陈列出端口及其类型。reg 型端口要么在 module 声明时声明,要么在 module 实体中声明,例如以下 2 种写法是等效的。
module pad(
input DIN, OEN ,
input [1:0] PULL ,
inout PAD ,
output reg DOUT
);
module pad(
input DIN, OEN ,
input [1:0] PULL ,
inout PAD ,
output DOUT
);
reg DOUT ;
- 如何仿真包含有 inout 端口类型的 pad 模型?
module pad(
//DIN, pad driver when pad configured as output
//OEN, pad direction(1-input, o-output)
input DIN, OEN ,
//pull function (00,01-dispull, 10-pullup, 11-pulldown)
input [1:0] PULL ,
inout PAD ,
//pad load when pad configured as input
output reg DOUT
);
//input:(not effect pad external input logic), output: DIN->PAD
assign PAD = OEN? 'bz : DIN ;
//input:(PAD->DOUT)
always @(*) begin
if (OEN == 1) begin //input
DOUT = PAD ;
end
else begin
DOUT = 'bz ;
end
end
//use tristate gate in Verilog to realize pull up/down function
bufif1 puller(PAD, PULL[0], PULL[1]);
endmodule
`timescale 1ns/1ns
module test ;
reg DIN, OEN ;
reg [1:0] PULL ;
wire PAD ;
wire DOUT ;
reg PAD_REG ;
assign PAD = OEN ? PAD_REG : 1'bz ; //
initial begin
PAD_REG = 1'bz ; //pad with no dirve at first
OEN = 1'b1 ; //input simulation
#0 ; PULL = 2'b10 ; //pull down
#20 ; PULL = 2'b11 ; //pull up
#20 ; PULL = 2'b00 ; //dispull
#20 ; PAD_REG = 1'b0 ;
#20 ; PAD_REG = 1'b1 ;
#30 ; OEN = 1'b0 ; //output simulation
DIN = 1'bz ;
#15 ; DIN = 1'b0 ;
#15 ; DIN = 1'b1 ;
end
pad u_pad(
.DIN (DIN) ,
.OEN (OEN) ,
.PULL (PULL) ,
.PAD (PAD) ,
.DOUT (DOUT)
);
initial begin
forever begin
#100;
if ($time >= 1000) $finish ;
end
end
2.16模块例化
例化,generate,全加器,层次访问。在一个模块中引用另一个模块,对其端口进行相关连接,叫做模块例化。模块例化建立了描述的层次。信号端口可以通过位置或名称关联,端口连接也必须遵循一些规则。
-
输入端口
模块例化时,从模块外部来讲, input 端口可以连接 wire 或 reg 型变量。这与模块声明是不同的,从模块内部来讲,input 端口必须是 wire 型变量。
-
输出端口
模块例化时,从模块外部来讲,output 端口必须连接 wire 型变量。这与模块声明是不同的,从模块内部来讲,output 端口可以是 wire 或 reg 型变量。
-
输入输出端口
模块例化时,从模块外部来讲,inout 端口必须连接 wire 型变量。这与模块声明是相同的。
-
悬空端口
模块例化时,如果某些信号不需要与外部信号进行连接交互,我们可以将其悬空,即端口例化处保留空白即可,上述例子中有提及。
output 端口正常悬空时,我们甚至可以在例化时将其删除。
input 端口正常悬空时,悬空信号的逻辑功能表现为高阻状态(逻辑值为 z)。但是,例化时一般不能将悬空的 input 端口删除,否则编译会报错。
// 命名端口连接
// 通过名称连接
full_adder1 u_adder0(
.Ai (a[0]),
.Bi (b[0]),
.Ci (c==1'b1 ? 1'b0 : 1'b1),
.So (so_bit0),
.Co (co_temp[0]));
// input 端口在例化时不能删除,output 端口在例化时可以删除。
//output 端口 Co 悬空
full_adder1 u_adder0(
.Ai (a[0]),
.Bi (b[0]),
.Ci (c==1'b1 ? 1'b0 : 1'b1),
.So (so_bit0),
.Co ());
//output 端口 Co 删除
full_adder1 u_adder0(
.Ai (a[0]),
.Bi (b[0]),
.Ci (c==1'b1 ? 1'b0 : 1'b1),
.So (so_bit0));
// 通过顺序连接
full_adder1 u_adder1(
a[1], b[1], co_temp[0], so_bit1, co_temp[1]);
// 位宽匹配,下面会报错
full_adder4 u_adder4(
.a (a[1:0]), //input a[3:0]
.b (b[5:0]), //input b[3:0]
.c (1'b0),
.so (so),
.co (co));
用 generate 进行模块例化,用 generate 语句可进行多个模块的重复例化。
// 重复例化 4 个 1bit 全加器组成一个 4bit 全加器的代码如下:
module full_adder4(
input [3:0] a , //adder1
input [3:0] b , //adder2
input c , //input carry bit
output [3:0] so , //adding result
output co //output carry bit
);
wire [3:0] co_temp ;
//第一个例化模块一般格式有所差异,需要单独例化
full_adder1 u_adder0(
.Ai (a[0]),
.Bi (b[0]),
.Ci (c==1'b1 ? 1'b1 : 1'b0),
.So (so[0]),
.Co (co_temp[0]));
genvar i ;
generate
for(i=1; i<=3; i=i+1) begin: adder_gen
full_adder1 u_adder(
.Ai (a[i]),
.Bi (b[i]),
.Ci (co_temp[i-1]), //上一个全加器的溢位是下一个的进位
.So (so[i]),
.Co (co_temp[i]));
end
endgenerate
assign co = co_temp[3] ;
endmodule
层次访问
-
每一个例化模块的名字,每个模块的信号变量等,都使用一个特定的标识符进行定义。在整个层次设计中,每个标识符都具有唯一的位置与名字。
-
Verilog 中,通过使用一连串的 . 符号对各个模块的标识符进行层次分隔连接,就可以在任何地方通过指定完整的层次名对整个设计中的标识符进行访问。
-
层次访问多见于仿真中。
// 层次访问
//u_n1模块中访问u_n3模块信号:
a = top.u_m2.u_n3.c ;
//u_n1模块中访问top模块信号
if (top.p == 'b0) a = 1'b1 ;
//top模块中访问u_n4模块信号
assign p = top.u_m2.u_n4.d ;
wait (test.u_counter.cnt_temp == 4'd4) ;
2.17带参数例化
-
defparam,参数,例化,ram
-
当一个模块被另一个模块引用例化时,高层模块可以对低层模块的参数值进行改写。这样就允许在编译时将不同的参数传递给多个相同名字的模块,而不用单独为只有参数不同的多个模块再新建文件。
-
参数覆盖有 2 种方式:1)使用关键字 defparam,2)带参数值模块例化。
// defparam 语句,可以先改写参数再例化
module ram_4x4
(
input CLK ,
input [4-1:0] A ,
input [4-1:0] D ,
input EN ,
input WR , //1 for write and 0 for read
output reg [4-1:0] Q );
parameter MASK = 3 ;
reg [4-1:0] mem [0:(1<<4)-1] ;
always @(posedge CLK) begin
if (EN && WR) begin
mem[A] <= D & MASK;
end
else if (EN && !WR) begin
Q <= mem[A] & MASK;
end
end
endmodule
defparam u_ram_4x4.MASK = 7 ;
ram_4x4 u_ram_4x4
(
.CLK (clk),
.A (a[4-1:0]),
.D (d),
.EN (en),
.WR (wr), //1 for write and 0 for read
.Q (q) );
// 带参数模块例化
module ram
#( parameter AW = 2 ,
parameter DW = 3 )
(
input CLK ,
input [AW-1:0] A ,
input [DW-1:0] D ,
input EN ,
input WR , //1 for write and 0 for read
output reg [DW-1:0] Q
);
reg [DW-1:0] mem [0:(1<<AW)-1] ;
always @(posedge CLK) begin
if (EN && WR) begin
mem[A] <= D ;
end
else if (EN && !WR) begin
Q <= mem[A] ;
end
end
endmodule
ram #(.AW(4), .DW(4))
u_ram
(
.CLK (clk),
.A (a[AW-1:0]),
.D (d),
.EN (en),
.WR (wr), //1 for write and 0 for read
.Q (q)
);
// 区别与建议
// 和模块端口实例化一样,带参数例化时,也可以不指定原有参数名字,按顺序进行参数例化,例如 u_ram 的例化可以描述为:
ram #(4, 4) u_ram (......) ;
// 利用 defparam 也可以改写模块在端口声明时声明的参数,利用带参数例化也可以改写模块实体中声明的参数。例如 u_ram 和 u_ram_4x4 的例化分别可以描述为:
defparam u_ram.AW = 4 ;
defparam u_ram.DW = 4 ;
ram u_ram(......);
ram_4x4 #(.MASK(7)) u_ram_4x4(......);
// 当然,也可以混合
defparam u_ram.AW = 4 ;
ram #(.DW(4)) u_ram (......); //也只有我这么无聊才会实验这种写法
// defparam 一般也不可综合。
// 建议,模块在编写时,如果预知将被例化且有需要改写的参数,都将这些参数写入到模块端口声明之前的地方(用关键字井号 # 表示)。这样的代码格式不仅有很好的可读性,而且方便调试。
2.18函数
在 Verilog 中,可以利用任务(关键字为 task)或函数(关键字为 function),将重复性的行为级设计进行提取,并在多个地方调用,来避免重复代码的多次编写,使代码更加的简洁、易懂。
函数只能在模块中定义,位置任意,并在模块的任何地方引用,作用范围也局限于此模块。函数主要有以下几个特点:
- 1)不含有任何延迟、时序或时序控制逻辑
- 2)至少有一个输入变量
- 3)只有一个返回值,且没有输出
- 4)不含有非阻塞赋值语句
- 5)函数可以调用其他函数,但是不能调用任务
- 6)函数通过automatic可以声明为动态存储空间,以使用递归和同时调用。
// 函数,大小端转换,当输入为 4'b0011 时,输出可为 4'b1100。
module endian_rvs
#(parameter N = 4)
(
input en, //enable control
input [N-1:0] a ,
output [N-1:0] b
);
reg [N-1:0] b_temp ;
always @(*) begin
if (en) begin
b_temp = data_rvs(a);
end
else begin
b_temp = 0 ;
end
end
assign b = b_temp ;
//function entity
function [N-1:0] data_rvs ;
input [N-1:0] data_in ;
parameter MASK = 32'h3 ;
integer k ;
begin
for(k=0; k<N; k=k+1) begin
data_rvs[N-k-1] = data_in[k] ;
end
end
endfunction
endmodule
// 当然,输入参数可以括起来,其中data_rvs为返回值和函数名,data_in为返回值
function [N-1:0] data_rvs ;
(input [N-1:0] data_in);
parameter MASK = 32'h3 ;
integer k ;
begin
for(k=0; k<N; k=k+1) begin
data_rvs[N-k-1] = data_in[k] ;
end
end
endfunction
// 局部变量是静态的,即函数的每次调用,函数的局部变量都会使用同一个存储空间。
// 若某个函数在两个不同的地方同时并发的调用,那么两个函数调用行为同时对同一块地址进行操作,会导致不确定的函数结果。
// Verilog 用关键字 automatic 来对函数进行说明,此类函数在调用时是可以自动分配新的内存空间的,也可以理解为是可递归的。
// 因此,automatic 函数中声明的局部变量不能通过层次命名进行访问,但是 automatic 函数本身可以通过层次名进行调用。
// 可递归和多次调用
wire [31:0] results3 = factorial(4);
function automatic integer factorial ;
input integer data ;
integer i ;
begin
factorial = (data>=2)? data * factorial(data-1) : 1 ;
end
endfunction // factorial
2.19任务
任务与函数的区别:
和函数一样,任务(task)可以用来描述共同的代码段,并在模块内任意位置被调用,让代码更加的直观易读。函数一般用于组合逻辑的各种转换和计算,而任务更像一个过程,不仅能完成函数的功能,还可以包含时序控制逻辑。下面对任务与函数的区别进行概括:
比较点 | 函数 | 任务 |
---|---|---|
输入 | 函数至少有一个输入,端口声明不能包含 inout 型 | 任务可以没有或者有多个输入,且端口声明可以为 inout 型 |
输出 | 函数没有输出 | 任务可以没有或者有多个输出 |
返回值 | 函数至少有一个返回值 | 任务没有返回值 |
仿真时刻 | 函数总在零时刻就开始执行 | 任务可以在非零时刻执行 |
时序逻辑 | 函数不能包含任何时序控制逻辑 | 任务不能出现 always 语句,但可以包含其他时序控制,如延时语句 |
调用 | 函数只能调用函数,不能调用任务 | 任务可以调用函数和任务 |
书写规范 | 函数不能单独作为一条语句出现,只能放在赋值语言的右端 | 任务可以作为一条单独的语句出现语句块中 |
任务
任务声明
任务在模块中任意位置定义,并在模块内任意位置引用,作用范围也局限于此模块。
模块内子程序出现下面任意一个条件时,则必须使用任务而不能使用函数。
- 1)子程序中包含时序控制逻辑,例如延迟,事件控制等
- 2)没有输入变量
- 3)没有输出或输出端的数量大于 1
// task
// 特点:
// input 、inout 型端口将变量从任务外部传递到内部。
// output、inout 型端口将任务执行完毕时的结果传回到外部。
// 可以把 input 声明的端口变量看做 wire 型,把 output 声明的端口变量看做 reg 型。
// 不需要用 reg 对 output 端口再次说明,与moudle不太一样。
// 对 output 信号赋值时也不要用关键字 assign。为避免时序错乱,建议 output 信号采用阻塞赋值。
// 任务的 output 端信号返回时间是在任务中所有语句执行完毕之后。
// 定义一个带延时的异或功能 task
task xor_oper_iner(
input [N-1:0] numa,
input [N-1:0] numb,
output [N-1:0] numco);
#3 numco = numa ^ numb ;
endtask
// 如何调用task
input [N-1:0] a ,
input [N-1:0] b ,
output [N-1:0] co_t;
reg [N-1:0] co_t;
xor_oper_iner(a, b, co_t);
// 任务操作全局变量
//way2: use task to operate global varialbes to generating clk
reg clk_test2 ;
task clk_rvs_global;
// 操作全局变量
# 5 ; clk_test2 = 0 ;
# 5 ; clk_test2 = 1 ;
endtask // clk_rvs_iner
always clk_rvs_global;
// automatic 任务,动态分配内存,并发调用
task automatic test_flag;
input [3:0] cnti ;
input en ;
output [3:0] cnto ;
if (en) cnto = cnti ;
endtask
⭐️3. verilog进阶
3.1 状态机