Verilog-菜鸟教程-Notes

参考菜鸟教程

2.1 基本语法

Verilog 是区分大小写的.

可在一行内编写,也可跨多行编写。

每个语句必须以分号为结束符。空白符(换行、制表、空格)都没有实际的意义,在编译阶段可忽略。

不换行如:

1
wire [1:0]  results ;assign results = (a == 1'b0) ? 2'b01 : (b==1'b0) ? 2'b102'b11 ;

换行如:

1
2
3
4
wire [1:0]  results ;
assign results = (a == 1'b0) ? 2'b01
(b==1'b0) ? 2'b10
2'b11 ;

注释和 C 语言相同.

Verilog 中关键字全部为小写.

2.2 数值表示

四种基本值, 表示硬件电路中的电平逻辑:

  • 0: 假
  • 1: 真
  • x 或 X: 未知
  • z 或 Z: 高阻

整数数值表示方法

四种:

  • 'b'B, 二进制
  • 'o'O, 八进制
  • 'd'D, 十进制
  • 'h'H, 十六进制

可在其前面加上位数也可以不加, 如:

1
2
3
4
5
6
7
8
9
num = 'b100
num = 'o100
num = 'd100
num = 'h100

num = 10'b100
num = 8'o100
num = 16'd100
num = 32'h100

不加时, 默认位数根据编译器而定.

负数表示

- 号. 如:

1
-'d20

实数表示法

十进制

1
2
3
4
30.123
6.0
3.0
0.001

科学记数法

1
2
3
1.2e4         //大小为12000
1_0001e4 //大小为100010000
1E-3 //大小为0.001

字符串表示方法

字符串是由双引号包起来的字符队列。字符串不能多行书写,即字符串中不能包含回车符。Verilog 将字符串当做一系列的单字节 ASCII 字符队列

1
2
3
4
reg [0: 14*8-1]       str ;
initial begin
str = "www.runoob.com";
end

2.3 数据类型

最常用的 2 种数据类型就是线网(wire)与寄存器(reg),其余类型可以理解为这两种数据类型的扩展或辅助。

线网 (wire)

wire 类型表示硬件单元之间的物理连线.

声明如:

1
2
3
wire   interrupt ;
wire flag1, flag2 ;
wire gnd = 1'b0 ;

寄存器 (reg)

寄存器(reg)用来表示存储单元,它会保持数据原有的值,直到被改写, 声明如:

1
2
reg    clk_temp;
reg flag1, flag2 ;

寄存器的值可在任意时刻通过赋值操作进行改写, 如:

1
2
3
4
5
6
reg rstn ;
initial begin
rstn = 1'b0 ;
#100 ;
rstn = 1'b1 ;
end

向量

当位宽大于 1 时,wire 或 reg 即可声明为向量的形式.

如:

1
reg [3:0]      counter; 

其表示为 4bit 位宽的寄存器 counter.

其中, 3 表示最高位, 0 表示最低位. (可以理解为数组, index 最高为 3, 最低为 0)

同样:

1
2
3
wire [32-1:0]  gpio_data;   //声明32bit位宽的线型变量gpio_data
wire [8:2] addr; //声明7bit位宽的线型变量addr,位宽范围为8:2
reg [0:31] data; //声明32bit位宽的寄存器变量data, 最高有效位为0

可以指定向量的某一位或若干相邻位,作为其他逻辑使用:

1
2
wire [9:0]     data_low = data[0:9];
addr_temp[3:2] = addr[8:7] + 1'b1;

Verillog 还支持指定 bit 位后固定位宽的向量域选择访问。

  • [bit+: width] : 从起始 bit 位开始递增,位宽为 width。
  • [bit-: width] : 从起始 bit 位开始递减,位宽为 width。

如:

1
2
3
4
5
6
7
//下面 2 种赋值是等效的
A = data1[31-: 8];
A = data1[31:24];

//下面 2 种赋值是等效的
B = data1[0+ : 8];
B = data1[0:7];

可用大括号进行数据拼接:

1
2
3
wire [31:0]    temp1, temp2 ;
assign temp1 = {byte1[0][7:0], data1[31:8]}; //数据拼接
assign temp2 = {32{1'b0}}; //赋值32位的数值0

整数, 实数, 时间寄存器变量

整数,实数,时间等数据类型实际也属于寄存器类型.

整数 (integer)

整数类型用关键字 integer 来声明。声明时不用指明位宽,位宽和编译器有关,一般为32 bit。reg 型变量为无符号数,而 integer 型变量为有符号数.

如:

1
2
3
4
5
6
7
8
9
reg [31:0]      data1 ;
reg [3:0] byte1 [7: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

实数 (real)

实数用关键字 real 来声明,可用十进制或科学计数法来表示。实数声明不能带有范围,默认值为 0。如果将一个实数赋值给一个整数,则只有实数的整数部分会赋值给整数。

如:

1
2
3
4
5
6
7
8
9
10
real        data1 ;
integer temp ;
initial begin
data1 = 2e3 ;
data1 = 3.75 ;
end

initial begin
temp = data1 ; //temp 值的大小为3
end

时间 (time)

Verilog 使用特殊的时间寄存器 time 型变量,对仿真时间进行保存。其宽度一般为 64 bit,通过调用系统函数 $time 获取当前仿真时间.

如:

1
2
3
4
5
time       current_time ;
initial begin
#100 ;
current_time = $time ; //current_time 的大小为 100
end

数组

在 Verilog 中允许声明 reg, wire, integer, time, real 及其向量类型的数组。

数组维数没有限制

数组中的每个元素都可以作为一个标量或者向量,以同样的方式来使用.

声明如:

1
2
3
4
5
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数据变量数组

赋值如:

1
2
3
4
5
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

需区分向量和数组:

  • 向量是一个单独的元件,位宽为 n
  • 数组由多个元件组成,其中每个元件的位宽为 n 或 1

存储器

存储器变量就是一种寄存器数组,可用来描述 RAM 或 ROM 的行为.

如:

1
2
3
reg               membit[0:255] ;  //256bit的1bit存储器
reg [7:0] mem[0:1023] ; //1Kbyte存储器,位宽8bit
mem[511] = 8'b0 ; //令第512个8bit的存储单元值为0

参数

参数用来表示常量,用关键字 parameter 声明,只能赋值一次.

如:

1
2
3
parameter      data_width = 10'd32 ;
parameter i=1, j=2, k=3 ;
parameter mem_size = data_width * 10 ;

通过实例化的方式,可以更改参数在模块中的值.

局部参数用 localparam 来声明,其作用和用法与 parameter 相同,区别在于它的值不能被改变.

字符串

字符串保存在 reg 类型的变量中,每个字符占用一个字节(8bit)。因此寄存器变量的宽度应该足够大,以保证不会溢出。

字符串不能多行书写,即字符串中不能包含回车符。如果寄存器变量的宽度大于字符串的大小,则使用 0 来填充左边的空余位;如果寄存器变量的宽度小于字符串大小,则会截去字符串左边多余的数据.

如:

1
2
3
4
reg [0: 14*8-1]       str ;
initial begin
str = "run.runoob.com";
end

一些特殊字符:

2.4 表达式

操作符大体和 C 相同:

以下为几个不同.

等价操作符

全等 ===. 非全等 !==

全等比较:a === b 表示当且仅当 a 和 b 的值和类型都相等时,表达式的值为 1,否则为 0。

  • 不全等比较:a !== b 表示当且仅当 a 和 b 的值或类型至少有一个不相等时,表达式的值为 1,否则为 0。

如:

1
2
3
4
5
6
7
8
9
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

归约操作符

归约操作符只有一个操作数,它对这个向量操作数逐位进行操作,最终产生一个 1bit 结果。

包括:

  • & , 归约与
  • ~&, 归约与非
  • | , 归约或
  • ~|, 归约或非
  • ^ , 归约异或
  • ~^, 归约同或

如:

1
2
3
4
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

拼接操作符

也就是 {}, 如:

1
2
3
4
5
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,常用作寄存器初始化时匹配位宽的赋初值

2.5 编译指令

以反引号 ` 开始的某些标识符是 Verilog 系统编译指令.

`define, `undef

`define 和 C 中的 #define 类似.

如:

1
`define    DATA_DW     32

`undef 用于取消宏定义:

1
2
3
4
5
`define    DATA_DW     32
……
reg [DATA_DW-1:0] data_in ;
……
`undef DATA_DW

条件编译指令

1
2
3
4
5
6
7
8
9
10
11
12
13
`ifdef       MCU51
parameter DATA_DW = 8 ;
`elsif WINDOW
parameter DATA_DW = 64 ;
`else
parameter DATA_DW = 32 ;
`endif

`ifndef WINDOW
parameter DATA_DW = 32 ;
`else
parameter DATA_DW = 64 ;
`endif

`include

`include 和 C 中的 #include 类似, 用于编译时将一个 Verilog 文件内嵌到另一个 Verilog 文件中, 可以是相对路径, 也可以是绝对路径, 如:

1
2
`include         "../../param.v"
`include "header.v"

`timescale

`default_nettype

`resetall

3.1 连续赋值

assign

任何已经声明 wire 变量的连续赋值语句都是以 assign 开头, 如:

1
2
wire      Cout, A, B ;
assign Cout = A & B ; //实现计算A与B的功能

注意 :

  • 左值必须是一个标量或者线型向量,不能是寄存器类型
  • 右值没有类型要求

3.2 时延

用于控制任意操作数发生变化到语句左端赋予新值之间的时间延时.

连续赋值时延一般可分为:

  • 普通赋值时延
  • 隐式时延
  • 声明时延

如:

1
2
3
4
5
6
7
8
9
10
11
12
13

//普通时延,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

惯性时延

4.1 过程结构

过程结构语句有 2 种,initial 与 always 语句

一个模块中可以包含多个 initial 和 always 语句,但 2 种语句不能嵌套使用。

这些语句在模块间并行执行,与其在模块的前后顺序没有关系, 但是 initial 语句或 always 语句内部可以理解为是顺序执行的.

每个 initial 语句或 always 语句都会产生一个独立的控制流,执行时间都是从 0 时刻开始.

initial 语句

initial 语句从 0 时刻开始执行,只执行一次,多个 initial 块之间是相互独立的。

如果 initial 块内包含多个语句,需要使用关键字 begin 和 end 组成一个块语句。

如果 initial 块内只要一条语句,关键字 begin 和 end 可使用也可不使用.

如:

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
`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 语句

always 语句是重复执行的。always 语句块从 0 时刻开始执行其中的行为语句;当执行完最后一条语句后,便再次执行语句块中的第一条语句,如此循环反复.

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
`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

在Verilog中,@符号用于always块中的敏感列表中,表示该always块应该对列表中指定的变量进行敏感性检测。敏感性列表中的变量发生改变时,该always块将被激活并执行相应的操作。

具体来说,@(*)表示always块应该对敏感性列表中的所有变量进行敏感性检测,包括输入端口、输出端口和内部变量等。这种方式通常称为“敏感性列表自动感知”,它可以简化设计,并且可以在设计变更时自动更新敏感性列表,从而避免遗漏任何敏感变量。

需要注意的是,always块中的敏感性列表必须是完整的,否则可能导致行为与预期不符。因此,在设计时,应该仔细检查always块的敏感性列表,并确保它包含了所有必要的变量。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

4.2 过程赋值

过程性赋值是在 initial 或 always 语句块里的赋值,赋值对象是寄存器、整数、实数等类型。

这些变量在被赋值后,其值将保持不变,直到重新被赋予新值。

阻塞赋值

阻塞赋值属于顺序执行,即下一条语句执行前,当前语句一定会执行完毕。

阻塞赋值语句使用等号 = 作为赋值符。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
initial begin
ai = 4'd1 ; //(1)
bi = 4'd2 ; //(2)
ai2 = 4'd7 ; //(3)
bi2 = 4'd8 ; //(4)
#20 ; //(5)

//non-block-assigment with block-assignment
ai = 4'd3 ; //(6)
bi = 4'd4 ; //(7)
value_blk = ai + bi ; //(8)
value_non <= ai + bi ; //(9)

//non-block-assigment itself
ai2 <= 4'd5 ; //(10)
bi2 <= 4'd6 ; //(11)
value_non2 <= ai2 + bi2 ; //(12)
end

非阻塞赋值

非阻塞赋值属于并行执行语句,即下一条语句的执行和当前语句的执行是同时进行的,它不会阻塞位于同一个语句块中后面语句的执行

非阻塞赋值语句使用小于等于号 <= 作为赋值符

实际 Verilog 代码设计时,切记不要在一个过程结构中混合使用阻塞赋值与非阻塞赋值。两种赋值方式混用时,时序不容易控制,很容易得到意外的结果。

更多时候,在设计电路时,always 时序逻辑块中多用非阻塞赋值,always 组合逻辑块中多用阻塞赋值;在仿真电路时,initial 块中一般多用阻塞赋值。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
initial begin
ai = 4'd1 ; //(1)
bi = 4'd2 ; //(2)
ai2 = 4'd7 ; //(3)
bi2 = 4'd8 ; //(4)
#20 ; //(5)

//non-block-assigment with block-assignment
ai = 4'd3 ; //(6)
bi = 4'd4 ; //(7)
value_blk = ai + bi ; //(8)
value_non <= ai + bi ; //(9)

//non-block-assigment itself
ai2 <= 4'd5 ; //(10)
bi2 <= 4'd6 ; //(11)
value_non2 <= ai2 + bi2 ; //(12)
end

4.3 时序控制

4.4 语句块

顺序块

顺序块用关键字 begin 和 end 来表示。

顺序块中的语句是一条条执行的。当然,非阻塞赋值除外。

并行块

并行块有关键字 fork 和 join 来表示。

并行块中的语句是并行执行的,即便是阻塞形式的赋值。

如:

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
`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

嵌套块

顺序块和并行块还可以嵌套使用。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
`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

命名块

可以给块语句结构命名。

命名的块中可以声明局部变量,通过层次名引用的方法对变量进行访问。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
`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

可以用 disable 关键字来终止命名块的执行,可以用来从循环中退出、处理错误等。

与 C 语言中 break 类似,但是 break 只能退出当前所在循环,而 disable 可以禁用设计中任何一个命名的块。

如:

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
`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

4.5 条件语句

条件语句

条件表达式必须在圆括号中.

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

同样可以加上 beginend 来区分嵌套的 ifelse 的配对关系:

1
2
3
4
5
6
7
8
 if(en) begin
if(sel == 2'b1) begin
sout = p1s ;
end
else begin
sout = p0 ;
end
end

4.6 多路分支语句

也就是 case 语句.

如:

1
2
3
4
5
6
case(case_expr)
condition1 : true_statement1 ;
condition2 : true_statement2 ;
……
default : default_statement ;
endcase

casex/casez 语句

4.7 循环语句

while 循环

如:

1
2
3
while (condition) begin

end

for 循环

1
2
3
for(initial_assignment; condition ; step_assignment)  begin

end

repeat 循环

1
2
3
repeat (loop_times) begin

end

forever 循环

1
2
3
forever begin

end

4.8 过程连续赋值

assign, deassign

assign(过程赋值操作)与 deassign (取消过程赋值操作)表示第一类过程连续赋值语句。赋值对象只能是寄存器或寄存器组,而不能是 wire 型变量。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 型变量。

因为是无条件强制赋值,一般多用于交互式调试过程,不要在设计模块中使用。

如:

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
`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

5.1 模块与端口

结构建模方式有 3 类描述语句: Gate(门级)例化语句,UDP (用户定义原语)例化语句和 module (模块) 例化语句.

模块

模块是 Verilog 中基本单元的定义形式,是与外界交互的接口.

定义如:

1
2
3
4
5
module module_name 
#(parameter_list)
(port_list) ;
Declarations_and_Statements ;
endmodule

端口

端口是模块与外界交互的接口。对于外部环境来说,模块内部是不可见的,对模块的调用只能通过端口连接进行。

端口列表

模块的定义中包含一个可选的端口列表,一般将不带类型、不带位宽的信号变量罗列在模块声明里.

如:

1
2
3
module pad(
DIN, OEN, PULL,
DOUT, PAD);

一个模块如果和外部环境没有交互,则可以不用声明端口列表.

如:

1
2
3
module test ;  //直接分号结束
...... //数据流或行为级描述
endmodule

端口声明

端口信号在端口列表中罗列出来以后,就可以在模块实体中进行声明了。

根据端口的方向,端口类型有 3 种:

  • 输入 (input)
  • 输出 (output)
  • 双向端口(inout)

inputinout 类型不能声明为 reg 数据类型,因为 reg 类型是用于保存数值的,而输入端口只能反映与其相连的外部信号的变化,不能保存这些信号的值。

output 可以声明为 wire 或 reg 数据类型。

先声明端口类型, 再声明端口数据类型, 如:

1
2
3
4
5
6
7
8
9
10
11
//端口类型声明
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 ;

也可直接在端口列表中表明端口的类型声明:

1
2
3
4
5
6
module pad(
input DIN, OEN ,
input [1:0] PULL ,
inout PAD ,
output reg DOUT
);

5.2 模块例化

也就是调用一个模块. (在一个模块中调用另一个模块)

有点像面向对象中的创建实例.

Verilog 模块例化是指在 Verilog 代码中创建一个新的模块实例,并将其连接到当前模块中的信号线。在 Verilog 中,模块是一组硬件电路的描述,可以像子程序一样调用。模块例化可用于将多个模块组合在一起,以构建更复杂的硬件电路。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module my_module(input clk, input reset, output reg data);

// 在这里定义模块中的行为

endmodule

module top_module(input clk, input reset);

// 实例化 my_module
my_module my_instance(.clk(clk), .reset(reset), .data(output_data));

// 在这里定义 top_module 中的行为

endmodule

在这个例子中,my_module 是一个包含一个时钟输入 (clk),一个复位输入 (reset) 和一个数据输出 (data) 的模块。在 top_module 中,我们实例化了一个 my_module 模块,并将 clk 和 reset 输入连接到 top_module 的输入信号线上。输出信号线 output_data 必须先在 top_module 中定义,然后才能在实例化语句中使用。

(这里是命名端口连接)

input 端口正常悬空时,悬空信号的逻辑功能表现为高阻状态(逻辑值为 z)。

用 generate 进行模块例化

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
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 中,通过使用一连串的 . 符号对各个模块的标识符进行层次分隔连接.

如:

1
2
3
4
5
6
7
8
//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 ;

5.3 带参数例化

参数覆盖有两种方式:

  • 使用关键字 defparam
  • 带参数值模块例化

defparam 语句

如:

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
//instantiation
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_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

带参数模块例化

将新的参数值写入模块例化语句.

如:

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
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)
);

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

6.1 函数

函数

函数只能在模块中定义,位置任意,并在模块的任何地方引用,作用范围也局限于此模块.

特点如:

  • 不含有任何延迟、时序或时序控制逻辑
  • 至少有一个输入变量
  • 只有一个返回值,且没有输出
  • 不含有非阻塞赋值语句
  • 函数可以调用其他函数,但是不能调用任务

声明格式:

1
2
3
4
5
function [range-1:0]     function_id ;
input_declaration ;
other_declaration ;
procedural_statement ;
endfunction

调用格式:

1
function_id(input1, input2, …);

如:

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
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

automatic 函数

在 Verilog 中,一般函数的局部变量是静态的,即函数的每次调用,函数的局部变量都会使用同一个存储空间。若某个函数在两个不同的地方同时并发的调用,那么两个函数调用行为同时对同一块地址进行操作,会导致不确定的函数结果。

Verilog 用关键字 automatic 来对函数进行说明,此类函数在调用时是可以自动分配新的内存空间的,也可以理解为是可递归的.

如:

1
2
3
4
5
6
7
8
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

6.2 任务

任务更像一个过程,不仅能完成函数的功能,还可以包含时序控制逻辑.

和函数一样,任务(task)可以用来描述共同的代码段,并在模块内任意位置被调用.

任务和函数的区别:

声明格式:

1
2
3
4
task       task_id ;
port_declaration ;
procedural_statement ;
endtask

如:

1
2
3
4
5
6
task xor_oper_iner(
input [N-1:0] numa,
input [N-1:0] numb,
output [N-1:0] numco ) ;
#3 numco = numa ^ numb ;
endtask

调用格式:

1
task_id(input1, input2, …,outpu1, output2, …);

任务调用时,端口必须按顺序对应。

输入端连接的模块内信号可以是 wire 型,也可以是 reg 型。输出端连接的模块内信号要求一定是 reg 型.


Verilog-菜鸟教程-Notes
http://example.com/2023/03/21/Verilog-菜鸟教程-Notes/
作者
Jie
发布于
2023年3月21日
许可协议