Verilog FPGA 快速上手·一日通 (在线/离线仿真)
参考文献:https://www.runoob.com/w3cnote/verilog-tutorial.html
一、Verilog 语言基础
1. 基础语法
1.0 语句
Verilog 是区分大小写的;
每个语句可以在一行内编写,也可跨多行编写 ;
除 endmodule、 begin...end、 fork...join 外, 必须以 分号 为结束符;
空白符(换行、制表、空格)都没有实际的意义。
1.1 标识符
verilog标识符的命名规则和C语言类似
(1) 用途: 标识符用于定义常数、变量、信号、端口、参数名、模块名等
(2) 组成: 字母、数字、$、_(下划线)任意组合而成
(3) 注意事项:
区分大小写(Verilog 和 verilog是不同的)
第一个字符只能是字母或下划线(123demo 是非法标识符)
1.2 关键字
关键字是 Verilog 中预留的用于定义语言结构的特殊标识符。
(1)注意事项:Verilog 中关键字全部为小写。
1.3 注释方式
Verilog中注释主要有行注释(//)和块注释(/* … */)两种,表示方法与C语言一致!
// 行注释 /* 块注释 */
2. 值与参数(常量)
与C语言类似,值主要有:逻辑型、整数型、实数型和字符串型四种
2.1 电平逻辑值
Verilog中有4中逻辑值:0、1、x、z
0: 低电平/逻辑假
1:高电平/逻辑真
x: 表示状态未知
z:表示高阻状态
注意:这里的z、x是不区分大小写的(X、Z也可)
2.2 整型数值
2.2.1 用十进制整数表示整型常量
(1) 正数: 直接写 10 表示位宽为32bit的十进制整数(系统默认)
(2) 负数: -10需要用二进制补码表示,多了一位符号位(1 1010)
2.2.2 用基数法表示整数型常量
基本格式:<换算成二进制数后的位宽>’ <数制符号><与数制对应的值>
在表示位宽的数字前面加一个减号来表示负数。
(1) 二进制(b): 8’b1000_1100
(2) 十六进制(h): 8’h8c
(3) 八进制(o): 8’o214
(4) 十进制(d): 8’140
注意事项:
当表示二进制时,最好每4位写一个下划线_以增强可读性:如8’b1000_1100 与8’b10001100 是一样的
基数表示法中遇到x时:十六进制表示4个x,八进制中表示3个x
当位宽大于二进制位数时左边自动补0,小于二进制数时2从左边截断!
2.3 实型数值
实数表示方法主要有两种方式
2.3.1 十进制表示
30.123 6.0 3.0 0.001
2.3.2 科学计数法
1.2e4 //大小为12000 1_0001e4 //大小为100010000 1E-3 //大小为0.001
2.4 字符串(用双引号)
(1) 字符串保存在 reg 类型的变量中, 每个字符由1个字节(8位)的ASCII码值表示,即需要1byte存储空间
(2) 如:“Hello world” 字符队列由11个ASCII符号构成,需要11byte存储单元
reg [0: 11*8-1] str ; initial begin str = “Hello world”; end
2.5 parameter 参数(常量)
(1) 参数用来表示常量,用关键字 parameter 声明,只能赋值一次。
类似于C语言宏定义,通常出现在module内部,常被用于定义状态、数据位宽等
(2) 只作用于声明的那个文件,且通过实例化的方式,可以灵活的更改参数在模块中的值!
parameter STATE = 1'b0;
(3) 局部参数用 localparam 声明,只在本模块中使用,其作用和用法与 parameter 相同,区别在于它的值不能被改变。
localparam STATE= 1'b1’;
(4) 参数的名称一般为大写,以区分其他变量
3. 变量(wire、reg)
《IEEE Standard for SystemVerilog》Chapter 6 Data types, 基于物理特性划分为两主要分组:变量分组(variable)和线网分组(net)。注意,这里指的是分组 group,而不是数据类型 data_type。
即,Verilog 两种基本物理信号类型,最常用的:一种是 wire(线网型),另一种是 reg(寄存器型);其余类型可以理解为这两种数据类型的扩展或辅助。
在数字电路中物理信号只有两种形态,一种是传输,一种是存储。传输是通过连接线, 存储是用寄存器。如下图

(1) wire 线网型
- 对线网的声明 net_declaration 进行简化,<net_type> + <data_type> + <list_of_net_decl_assignments>
即:net_type+list_of_net_identifiers,其中net_type包含我们常用的如wire、tri、wand、wor等等(参考完整的线网声明:net_declaration);
① wire 型变量,必须在 assign 中赋值(连续赋值);
② 线网net型(wire): 表示电路间的物理连接,wire定义的变量也可看成信号端口
③ 当两个wire信号被连续赋值时,在逻辑块中会被映射成真实的物理连线,此时这两个信号端口的变化是同步的!
wire a; wire b; assign b = a; // 表示a与b之间生成实际的物理连线
(2) reg 寄存器型
- 对变量的声明 variable_declaration 进行简化,[const][var] + <data_type> + <list_of_variable_decl_assignments>
即:reg | integer | time | real | realtime+list_of_variable_identifiers(参考完整的变量声明:variable_declaration)。
① reg 型变量,必须在 always 中赋值(过程赋值);
② 寄存器型(reg): 表示一个抽象的数据存储单元
③ reg 具有对某variable一时间点状态进行保持的功能
用法与注意事项
① 在 always、initial 语句中被赋值的变量(赋值号左边的变量)都是 reg 型变量
② 在 assign 语句中被赋值的变量,为 wire 型变量
③ 未被声明类型的变量默认是 wire 型
④ 组合逻辑使用阻塞赋值,时序逻辑用非阻塞赋值。组合逻辑是没用时钟沿控制的电路,时序逻辑是由时钟沿控制的电路。
3.1 信号结构--衍生型
由信号结构,可以衍生出以下变量类型:标量(scalar)、向量(vector)、数组(memory)
3.1.1 标量
在Verilog中,标量 ( scalar ) 指的是单个位的信号或者变量,与向量(vector)相对。标量通常只包含一个独立位,表示一个二进制值(0或1)或者一个逻辑状态(比如逻辑值true或false)。
与向量不同,标量没有范围或位宽,一个标量可以是一个寄存器、一个线网、一个参数或一个局部变量。以下是一些示例:
reg clk; //定义一个标量寄存器,表示时钟信号 wire out; //定义一个标量线网, 用于输出信号 always @(posedge clk) begin out <= ~out; //对输出信号进行取反 end
3.1.2 向量
vector(向量),通常指的是一个多位的信号,它可以是一个寄存器、一个线网、一个参数或一个局部变量。
通常,向量由多个连续的位组成,可以表示一个数据字、一个数据、一个地址或者其他多位信息
格式: [input|output|inout] wire|reg "["upper:lower"]" <vector_name>
在Verilog中,向量可以使用方括号表示其范围 range 。例如, 一个8位的向量可以表示为[7:0],其中7是最高有效位(MSB),0是最低有效位(LSB)。例如:
//输入输出型 input wire [7:0] a , b; output reg [7:0] out; //模块中间向量 wire [7:0] c , e; reg [7:0] d;
[upper : lower] 定义位宽,如[7 : 0]表示位宽为8bit,即upper = 7 ,lower = 0
vector_name 可以一次写多个向量
3.1.3 数组
在向量的声明中,向量索引是在向量名称之前编写的,如果括号写在变量名之后,即表示数组。在Verilog中,数组是一种数据结构,用于存储相同类型的元素。数组可以是一维的、多维的,也可以是packed(打包的)或unpacked(未打包的)。
一维数组:包含一组元素,每个元素都有一个唯一的索引。
多维数组:包含多个维度的元素,每个维度都有一个唯一的索引。
打包数组:将元素打包 packed 到一个连续的"位块"(bit blob)中,通常用于表示数据寄存器或信号; packed数组的维度是在数组名称之前声明的
未打包数组:元素不打包 unpacked 到一个连续的位块中,通常用于表示寄存器或多维数组; unpacked数组的维度是在数组名称之后声明的
// 数组定义 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
虽然数组与向量的访问方式在一定程度上类似,但不要将向量和数组混淆。向量是一个单独的元件,位宽为 n;数组由多个元件组成,其中每个元件的位宽为 n 或 1。它们在结构的定义上就有所区别。
3.1.3.1 存储器
存储器变量就是一种寄存器数组,可用来描述 RAM 或 ROM 的行为。例如:
reg membit[0:255] ; //256bit的1bit存储器 reg [7:0] mem[0:1023] ; //1Kbyte存储器,位宽8bit mem[511] = 8'b0 ; //令第512个8bit的存储单元值为0
3.2 组内抽象数据类型--衍生型
3.2.1 线型分组
net型用于连续赋值的赋值目标或门原语的输出,仿真时不需要分配内存空间,最常用的是wire
net_type,包括:
| wire(线型) | |||
|---|---|---|---|
| wand(线与) | tri(三态) | tri0(下拉电阻) | supply0(地) |
| wor(线或) | triand(三态与) | tri1(上拉电阻) | supply1(电源) |
| trior(三态或) | trireg(电容性线网) |
3.2.2 寄存器分组
variable用于过程赋值的赋值目标,仿真时需要分配内存空间,最常用的是reg
data_type,包括
| reg(寄存器型) | |
|---|---|
| integer(整型) | time(时间型) |
| real(实型) | realtime(实时间型) |
对于端口信号,input和inout信号必须定义成net型,output信号可以是net型也可以是variable型,取决于使用哪种赋值语句对其赋值。
3.2.2.1 整数(integer)
整数类型用关键字 integer 来声明。声明时不用指明位宽,位宽和编译器有关,一般为32 bit。reg 型变量为无符号数,而 integer 型变量为有符号数。例如:
实例
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
此例中,integer 信号 j 作为辅助信号,将 data1 的数据依次赋值给数组 byte1。综合后实际电路里并没有 j 这个信号,j 只是辅助生成相应的硬件电路。
3.2.2.2 实数(real)
实数用关键字 real 来声明,可用十进制或科学计数法来表示。实数声明不能带有范围,默认值为 0。如果将一个实数赋值给一个整数,则只有实数的整数部分会赋值给整数。例如:
实例
real data1 ;
integer temp ;
initial begin
data1 = 2e3 ;
data1 = 3.75 ;
end
initial begin
temp = data1 ; //temp 值的大小为3
end
3.2.2.3 时间(time)
Verilog 使用特殊的时间寄存器 time 型变量,对仿真时间进行保存。其宽度一般为 64 bit,通过调用系统函数 $time 获取当前仿真时间。例如:
实例
time current_time ;
initial begin
#100 ;
current_time = $time ; //current_time 的大小为 100
end
4. 运算符及优先级

这里只介绍一些容易误解的运算。
4.1 逻辑运算
(1) 逻辑运算符:&&(与)、==(相等)、||(或)、!=(不等)
如 m&&n : 判断m和n是否全为真**(非0即为真**),真则输出1’b1(1位二进制1),否则输出1’b0
例如:
4’b1010 && 4’b0101 = 1’b1
最后输出结果只有1bit
(2) 按位运算符: &、|、、^、&、^、|
如 m&n : 是把m的每一位与n的每一位按位做与运算
4’b1010 & 4’b0101 = 4’b0000
输出结果与m/n的bit数相同
(3) 规约(缩减)单目运算符: &、|、、^、&、^、~|
只有一个参量参与运算时( &为一元运算符),表示缩减与,即向量内部进行与运算
&a [3:0] // AND:a[3]&a[2]&a[1]&a [0]相当于(a[3:0]== 4'hf) |b [3:0] // OR: b[3]|b[2]|b[1]|b [0]相当于(b[3:0]!= 4'h0) ^c [2:0] // XOR:c[2]^c[1]^c[0]
即(&4’b0101 = 0 & 1 & 0 & 1 = 1’b0 )
最后输出结果只有1bit
4.2 拼接运算符({ , })
4.2.1 拼接
用一对花括号加逗号组成**“{ , }”拼接运算符,逗号隔开的数据按顺序拼接成新数据**!
wire [1 : 0] a; wire [3 : 0] b; wire [5 : 0] c; wire [11 : 0] d = {a , b , c};
4.2.2 通过拼接实现移位
在左边拼接实现右移,右边拼接实现左移!
always @(posedge clk) begin if(rst_n == 1'b0) out <= 4'b0; else out <= {in , out[3 : 1]}; //右移 end
4.2.3 连接符中重复多次的操作
语法:{重复次数{vector}}
{3{a}} = {a , a , a}
{3'd5 {2{3'd6}}} //9'b101110110
4.3 移位运算符
移位运算符用于将左边操作数左移或右移指定的位数!移位后空闲为用0填充。
左移运算符:<< 如: 4’b1101 << 3 结果为: 4’b1000 右移运算符:>> 如:4’b1101 >> 3 结果为:4’b0001
移位运算符其它用途:左移一位可以看成是乘以2,右移一位可以看成是除以2
移位运算符代替乘除法可以节省资源
4.4 向量片选
a[3 : 0] // 取向量a的0~3位 b[n] // 取向量b的第n位数据 c[-1 : -2] // 取向量c的最低2位数据 c[0 : 3] // 取向量c的最高4位数据
多路选择器的应用:实现一个256选1的选择器,sel信号作为选择信号,当sel = 0时选择in[3 : 0],sel = 1时选择in[7 : 4],以此类推
module top_module( input [1023 : 0] in, input [7 : 0] sel, output [3 : 0] out ); assign out = {in[sel*4+3], in[sel*4+2], in[sel*4+1], in[sel*4+0]}; //这里的{}是拼接运算符 //也可以写成如下写法 //assign out = in[sel * 4 +: 4]; //assign out = in[sel * 4 + 3 -: 4]; endmodule
片选信号sel输入为n位二进制数,当参与运算、充当索引时会自动转换成十进制数
该题所选取的信号片段为: in[sel * 4 + 3: sel * 4] , 但这不符合Verilog的片选语法规则故应写成:
in[sel * 4 +: 4] // 表示索引从sel4开始的高4bit信号 in[sel * 4 + 3 -: 4] // 表示索引从sel4+3开始的低4bit信号
或是直接选出需要的每一位,再用{ }拼接成新向量:
{in[sel * 4 + 3], in[sel * 4 + 2], in[sel * 4 + 1], in[sel * 4 + 0]}
5. 行为描述语句及可综合性
Verilog可综合性,指(能否生成实际的电路)的行为描述语句。

二、 组合逻辑与时序逻辑(串行与并行)
2.1 组合逻辑:
组合逻辑的特点是任意时刻的输出仅取决于该时刻的输入,与电路原本的状态无关,逻辑中不牵涉跳变沿信号的处理,组合逻辑的verilog描述方式有两种:
2.1.1 always@(电平敏感信号列表)
always模块的敏感信号列表为所有输入信号;
组合逻辑中的赋值建议使用阻塞赋值”=“;
always 模块中的信号必须定义为 reg 型,但最终的综合得到的组合逻辑电路中并没有寄存器。
2.1.2 assign 描述的连续赋值
连续赋值 就是一旦赋值,输出将随输入改变而变化,一旦修改输入则立刻体现在输出上。
input a,b;
wire out;
assign out = a & b;
- 要求赋值左侧必须为 net 型
- 关键词 assign
- 赋值过程类似于物理场景的导线连接,out将跟随a异或b的电路变化而变化,一旦a或b有修改则立刻体现在输出上。
- 连续赋值属于并发执行,与书写顺序无关,不论在何时赋值都会在一开始执行。
- 对于同一参数多次赋值,视情况和强度而定,但不要进行该操作。
- 常用于组合逻辑
- 不能在always块中使用
2.2 时序逻辑:
时序逻辑的特点为任意时刻的输出不仅取决于该时刻的输入,而且还和电路原来的状态有关。时序逻辑电路里面有存储元件,不管其输入如何变化,仅当时钟发生跳变时,输出才可能会变化。
2.2.1 always@(边沿敏感型信号列表)
时序逻辑的敏感信号列表为时钟信号(以及复位信号)和部分输入信号;
时序逻辑中的赋值建议使用非阻塞赋值“<=”。
并行与串行
Module模块中的assign语句、实例原件和always块是并行执行,但是always块中的语句是串行执行(对于非阻塞赋值是并行),但是对于多个always块来讲,它们之间的关系是并行的。
注意
(1) 在Verilog模块中所有过程块(如:initial块、always块)、连续赋值语句、实例引用都是并行的;
(2) 它们表示的是一种通过变量名互相连接的关系;
(3) 在同一模块中这三者出现的先后秩序没有关系;
(4) 只有连续赋值语句assign和实例引用语句可以独立于过程块而存在于模块的功能定义部分。
2.3 连续赋值 和 过程赋值(阻塞赋值 和 非阻塞赋值)
- 连续赋值(见,2.1.2 )
- 过程赋值,包括如下两种赋值:
1. 阻塞赋值 =
2. 非阻塞赋值 =>
2.3.1 过程赋值
过程赋值只会在赋值时刻进行改变,其余时间保持不变,而且根据使用的赋值符号 = 和 <=,分为阻塞赋值和非阻塞赋值。
2.3.1.1 阻塞赋值 =
阻塞赋值:右边表达式的计算和对左边寄存器变量的赋值是一个统一的原子操作中的两个动作,这两个动作之间不能再插入其他任何动作
简单概括就是,表达式计算完毕后立即将值赋给赋值目标。
//例1 initial begin ai = 4'd1 ; //(1) ai = 4'd2 ; //(2) //ai 最此刻为 2 #20 ; //(3) ai = 4'd3 ; //(4) bi = 4'd4 ; //(5) value_blk = ai + bi ; //(6) //value_blk 最终为7 #40; end
- 要求赋值左侧必须为reg、integer, real, time-variable, or memory(等非net型);
- 赋值过程在initial或always块中;
- 阻塞赋值类似于C语言的赋值方式,后面的赋值会覆盖掉前面的赋值,执行顺序为1->2->...->6,类似软件编程的赋值方式,随时赋值随时开始,但前一个赋值如果不结束,后面的赋值便不会来说,正因为如此被称为阻塞赋值。
- 最终结果与书写顺序相关.
- 常用于testbench测试用例/组合逻辑.
2.3.1.2 非阻塞赋值 <=
非阻塞赋值:首先按顺序计算右边表达式的值,但并不马上赋值,而是要等到过程结束时再按顺序赋值
简单概括就是,等所有表达式计算完毕之后同时赋值给赋值目标
//例2 initial begin ai = 4'd1 ; //(1) bi = 4'd2 ; //(2) //ai和bi的初始值分别为1,2 #20 ; //(3) //非阻塞赋值 ai <= 4'd3 ; //(4) bi <= 4'd4 ; //(5) value_nonblk <= ai + bi ; //(6) #40; //value_blk 最终为3 end
- 要求赋值左侧必须为reg型
- 关键词为initial或always
- 非阻塞赋值类似于物理电路中的时序电路,其中可以这样理解代码执行顺序,1->2->3之后其他的非阻塞赋值(4)(5)(6)不着急执行,而是列入到"事件队列"中,
一直存到#40需要被执行前,即下一个时刻需要执行前,(4)(5)(6)将会被同时执行,此时对于value_nonblk而言其对应的ai和bi依旧是1和2,因此结果为3,可用于时序电路建模。 - 非阻塞赋值是前一个赋值并不会阻塞后一个赋值,而是将所有赋值放到“事件队列”中,并在该时刻的“最后”同时执行,也就是说,赋值过程不会阻塞。
- 最终结果与书写顺序相关
- 常用于时序逻辑
注意:在一个always语句块中不能混合使用阻塞赋值和非阻塞赋值语句,设计组合电路时常用阻塞赋值,设计时序电路时常用非阻塞赋值。
在使用非阻塞与阻塞赋值时最好遵循以下原则:
1. 时序电路建模时,使用非阻塞赋值 ;
2. 锁存器电路建模时,使用非阻塞赋值 ;
3. 使用always块写组合逻辑电路时,采用阻塞赋值;
4. 在同一个always块中同时建立时序和组合逻辑电路时,用非阻塞赋值 ;
5. 在同一个always块中不要同时使用非阻塞赋值和阻塞赋值 ;
6. 不要在多个always块中,为一个变量赋值;
7. 用$strobe系统任务来显示用非阻塞赋值的变量值;
8. 在赋值时不要使用#0延迟;
遵循以上逻辑可以消除90%-100%的在仿真中产生的竞争冒险现象,有助于正确编写可综合硬件。
三、Verilog 三种建模描述方式
3.1 数据流建模
数据流建模只有一种描述方式,即通过连续赋值语句进行逻辑描述,其最基本的语句是由 assign 引导的。数据流建模可以描述所有的组合逻辑电路。
语法格式:
<net_declaration><range><name>;
assign #<delay><name> = Assignment expression;
net_declaration,连线型变量类型,缺省为wire
range,变量位宽,缺省为1bit
delay,延时,语法格式为:#(delay1, delay2, delay3)
“delay1”称为上升延时,是连线型变量转移到”1“状态的延时值
“delay2”称为下降延时,是连线型变量转移到”0“状态的延时值
“delay3”称为关闭延时,是连线型变量转移到”Z“状态的延时值
// eg. module example1_assignment(a,b,m,n,c,y) input[3:0]a,b,m,n; output[3:0]c,y; assign y = m|n; assign #(3,2,4)c = a&b; endmodule
(1)赋值目标只能是连线型(wire)
(2)在连续赋值中,只要赋值语句右边表达式任何一个变量有变化,表达式立即被计算,计算的结果立即赋值给左边的信号
(3)连续赋值语句不能出现在过程块(行为级建模会提到,initial/always)中
(4)多个连续赋值语句之间是并行关系,与位置顺序无关
(5)连续赋值语句中的延时具有硬件电路中惯性延时的特性,任何小于其延时量的信号变化脉冲都将被滤除,不会出现在输出端口
3.2 行为级建模
使用,Verilog可综合性(能否生成实际的电路)的行为描述语句(见,5. 行为描述语句及可综合性),进行建模。
3.3 结构化建模
主要有模块级建模、门级建模和开关级建模三种,开关级建模是Verilog HDL与VHDL之间的本质区别。
这里主要使用模块级建模。
附录 A 在线与离线仿真
A.1 基于 Iverilog 在线仿真
在线仿真网址:Iverilog - HDLBits (01xz.net)
把设计的.v文件和激励文件内容粘贴到文本框中,点击提交Submit,即可得到功能仿真波形。
也可以通过下面的上传按键,直接提交文件,但只能提交一个文件,所以还是要把测试激励和设计文件内容整合在一起。
注意:在线仿真时,默认的顶层激励module名称只能使用module top_module ();

A.2 基于开源工具iVerilog 和 iVerilog Assistant 的离线仿真
A.2.1 下载安装
下载地址:http://bleyer.org/icarus/ 建议: iverilog-v11-20210204-x64_setup.exe [44.1MB]
安装指导:全平台轻量开源verilog仿真工具iverilog+GTKWave使用教程
手把手的iVerilog仿真教程:基于开源工具iVerilog Assistant: https://blog.csdn.net/little_cats/article/details/113873714
浙公网安备 33010602011771号