1 Verilog语法注意点
过程赋值语句块------initial语句块 不可综合,,常用于测试文件中生成激励波形(如复位信号)作为电路的仿真信号

(1)功能描述
Verilog HDL 的三种主要描述方式,三种"思维层次"。
| 描述方式 | 英文 | 抽象层次 | 关注重点 | 类比 |
|---|---|---|---|---|
| 结构描述 | Structural | 最低 | 用了哪些门,怎么连接 | 搭积木 |
| 数据流描述 | Data Flow | 中等 | 信号如何变化 | 写数学公式 |
| 行为描述 | Behavioral | 最高 | 电路实现什么功能 | 写程序 |
结构描述(Structural)
不用告诉你它干什么,我直接告诉你:这里有几个门,它们怎么接。
比如你要做:Y = A \land B
结构描述的写法:
verilog
and u1(Y, A, B);
- 使用一个
and门 - 实例名叫
u1 - 输出接到
Y - 输入接
A和B
特点
- 最接近真实硬件
- 需要明确写出所有元件和连接
- 可读性差
- 适合底层设计
示例:半加器
verilog
module half_adder(
input a, b,
output sum, carry
);
xor u1(sum, a, b);
and u2(carry, a, b);
endmodule
电路结构
sum = a XOR bcarry = a AND b
数据流描述(Data Flow)
不管你内部用什么门,只告诉你输出和输入的逻辑关系。
主要语句assign
数学公式:y = x + 1
Verilog:
verilog
assign y = x + 1;
示例
verilog
assign Y = A & B;
意思:
- 只要
A或B变化 Y自动更新
半加器写法
verilog
module half_adder(
input a, b,
output sum, carry
);
assign sum = a ^ b;
assign carry = a & b;
endmodule
特点
- 比结构描述简单
- 接近逻辑表达式
- 常用于组合逻辑
行为描述(Behavioral)
我只描述电路应该如何工作,不关心内部结构。
主要语句 always initial
if case for while
示例:与门
verilog
always @(*) begin
Y = A & B;
end
特点
- 最像编程语言
- 可以写复杂逻辑
- 最常用
三种方式对比示例
实现:
Y = A \land B
- 结构描述
verilog
and u1(Y, A, B);
- 数据流描述
verilog
assign Y = A & B;
- 行为描述
verilog
always @(*) begin
Y = A & B;
end
task(任务)
类似"子程序",可以完成复杂操作,并且有多个输入和多个输出。
示例
verilog
task add;
input [3:0] a, b;
output [4:0] sum;
begin
sum = a + b;
end
endtask
调用:
verilog
add(x, y, z);
特点
- 可以有多个输出
- 可以包含时间控制(
#10) - 适合复杂操作
function(函数)
接收输入,返回一个值。
示例
verilog
function [4:0] add;
input [3:0] a, b;
begin
add = a + b;
end
endfunction
调用:
verilog
sum = add(x, y);
特点
- 只能返回一个值
- 不能包含延时
- 常用于表达式中
小总结
| 场景 | 推荐方式 |
|---|---|
| 门级设计 | 结构描述 |
| 简单组合逻辑 | 数据流描述 |
| 复杂组合逻辑 | 行为描述 |
| 时序电路 | 行为描述 |
| 可复用计算 | function |
| 多步骤操作 | task |
| 项目 | task | function |
|---|---|---|
| 返回值 | 多个 | 只有一个 |
| 是否可有输出参数 | 可以 | 不需要 |
| 是否可带延时 | 可以 | 不可以 |
| 调用方式 | 独立语句 | 表达式中 |
| 类比 | 子程序 | 数学函数 |
(2)系统函数
①display会在字符串末尾追加一个换行符,write则不会
连续监视器$monitor 函数,每当其参数列表中的变量或表达式发生更改时,会自动打印出变量或表达式的值
②打开和关闭文件向
文件中输入信息
读取文件

读取文件数据到存储器:
$readmemb ("<数据文件名>",<存储器名>);
$readmemh ("<数据文件名>",<存储器名>);
(3)代码示例
01 四位全加器

全加器完成这个计算:
text
a + b + cin
其中:
a:当前位第一个数b:当前位第二个数cin:来自低位的进位sum:当前位结果cout:向高位输出的进位
第1行
verilog
`timescale 1ns / 1ps
设置仿真时间单位:
1ns= 时间单位是纳秒1ps= 精度是皮秒
这行只影响仿真,不影响硬件。
第6行
verilog
);
端口列表结束。
第9行 全加器进位公式
verilog
assign cout = (a & b) | ((a ^ b) & cin);
计算进位。
含义:只要以下情况之一发生,就产生进位:
a=1 且 b=1cin=1且a,b中有一个是 1
第24行
verilog
assign cout = carry[3];
最高位的进位就是整个 4 位加法器的进位输出。
text
bit0 → bit1 → bit2 → bit3
│ │ │ │
c0 c1 c2 c3
│
cout

02 读写存储器


这张图详细展示了一个32×32位单端口同步写、异步读寄存器文件(Register File,简称regfile)的Verilog HDL设计实现,包含源码、RTL电路原理图、关键语法说明和初始化文件示例,是CPU通用寄存器堆的基础设计模板。
①整体设计概述
该模块实现了一个容量为 32个寄存器 × 每个寄存器32位 的可读写存储器,总容量1024位(128字节)。核心特性:
- 写操作:同步时钟上升沿触发 ,由写使能
wen控制 - 读操作:组合逻辑异步触发 ,由读使能
ren控制 - 地址空间:5位地址总线(2⁵=32),正好寻址全部32个寄存器
- 仅用于仿真的初始化功能:通过文件预加载寄存器初始值
②Verilog源码逐行解析
1. 时间尺度与模块接口(第1-11行)
verilog
`timescale 1ns / 1ps // 仿真时间尺度:1单位=1ns,精度1ps
module regfile(
input wire clk, // 系统时钟,写操作同步触发
input wire ren, // 读使能,高电平有效
input wire wen, // 写使能,高电平有效
input wire[31:0] wdata, // 32位写数据总线
input wire[4:0] addr, // 5位地址总线(读/写共用)
output wire[31:0] rdata // 32位读数据总线
);
2. 内部存储单元定义(第12行)
verilog
reg[31:0] regfile[31:0];
定义了一个二维寄存器数组:
- 第一个
[31:0]:每个寄存器的位宽为32位 - 第二个
[31:0]:总共有32个这样的寄存器 - 这是寄存器堆的核心存储体,综合后会映射为FPGA中的Block RAM或分布式RAM
3. 仿真初始化块(第13-16行)
verilog
initial begin
$readmemh("C:\\ram_data.txt", regfile);
end
⚠️ 关键注意事项:
initial语句仅在仿真开始时执行一次,不可综合(无法烧写到FPGA/ASIC硬件中)$readmemh是Verilog系统函数,用于将十六进制格式的文本文件数据按顺序加载到寄存器数组中- 配套的
ram_data.txt文件格式:每行一个32位十六进制数,第一行对应regfile[0],第二行对应regfile[1],以此类推
4. 异步读逻辑(第17-18行)
verilog
assign rdata = (ren == 1'b1) ? regfile[addr] : 32'b0;
- 纯组合逻辑实现,无时钟延迟 :只要
ren或addr变化,rdata立刻更新 - 三目运算符逻辑:
- 读使能
ren=1:输出地址addr对应的寄存器值 - 读使能
ren=0:输出32位全0,避免总线冲突
- 读使能
5. 同步写逻辑(第19-23行)
verilog
always @(posedge clk) begin
if (wen)
regfile[addr] = wdata;
end
- 时序逻辑实现,仅在时钟上升沿触发
- 只有当写使能
wen=1时,才会将wdata写入地址addr指定的寄存器 - 同步写保证了硬件的时序稳定性,是数字电路的标准设计规范
③RTL原理图与代码的对应关系
右边的电路图是上述Verilog代码综合后的RTL级结构,一一对应代码逻辑:
| 原理图模块 | 对应代码部分 | 功能说明 |
|---|---|---|
regfile_reg(RTL_RAM) |
reg[31:0] regfile[31:0]; + 写逻辑 |
核心存储体,实现同步写和异步读功能 |
WCLK |
clk |
写操作时钟输入 |
WE1 |
wen |
写使能输入 |
WA1[4:0] |
addr |
写地址输入 |
WD1[31:0] |
wdata |
写数据输入 |
RA1[4:0] |
addr |
读地址输入(与写地址共用) |
RO1[31:0] |
regfile[addr] |
寄存器读数据输出 |
RTL_MUX |
assign rdata = ... |
读使能控制的2选1多路选择器 |
选择端S |
ren |
读使能作为多路选择器的控制信号 |
I0[31:0] |
RO1 |
有效读数据输入 |
I1[31:0] |
接地(32'b0) | 无效读时的默认输出 |
O[31:0] |
rdata |
最终读数据输出 |
④设计特点与扩展方向
现有设计的特点
- 单端口结构:读和写共用同一个地址总线,同一时刻只能执行读或写操作
- 异步读+同步写:读操作无延迟,写操作时序稳定
- 带读使能控制,支持总线共享
常见扩展方向
- 双端口寄存器堆:分离读地址和写地址,支持同时读写(CPU标准实现)
- 多端口寄存器堆:支持多个读端口(如MIPS架构的2个读端口+1个写端口)
- 复位功能:增加复位信号,实现硬件上电复位(替代不可综合的initial语句)
- 字节写使能:支持按字节/半字写入,兼容不同位宽的数据操作
这张图是上一张单端口32×32位寄存器文件(regfile)的完整仿真验证方案 ,包含两部分:Verilog Testbench(测试平台)源码 和对应的仿真波形图,用于验证regfile设计的功能正确性。
⑤Testbench 仿真源码逐行解析
Testbench是数字IC设计中用于验证被测模块(DUT,Device Under Test)的专用代码,它不被综合,仅在仿真环境中运行,负责产生激励信号并观察输出。
通过这个Testbench,我们成功验证了regfile设计的4个核心功能:
- ✅ 上电初始化功能 :
$readmemh正确将文本文件中的十六进制数据加载到寄存器数组中 - ✅ 同步写功能 :只有在时钟上升沿且写使能
wen=1时,才会将数据写入指定地址 - ✅ 异步读功能 :读使能
ren=1时,立刻输出对应地址的寄存器值,无时钟延迟 - ✅ 使能控制功能 :
wen=0时禁止写操作,ren=0时输出全0,避免总线冲突
1. 基础设置与信号定义
verilog
`timescale 1ns / 1ps // 与被测模块保持一致的时间尺度
module testbench3(); // 测试平台无输入输出端口
// 激励信号:用reg类型,可在initial块中赋值
reg clk; // 系统时钟
reg ren; // 读使能
reg wen; // 写使能
reg[4:0] addr; // 5位地址总线
reg[31:0] wdata; // 32位写数据
// 观测信号:用wire类型,连接被测模块的输出
wire[31:0] rdata; // 32位读数据
2. 实例化被测模块
verilog
// 实例化上一张图的regfile模块,命名为regfile0
// 端口顺序严格对应regfile模块的定义:clk, ren, wen, wdata, addr, rdata
regfile regfile0(clk, ren, wen, wdata, addr, rdata);
3. 激励信号产生(initial块)
initial块在仿真开始时执行一次,按时间顺序产生所有测试激励:
verilog
initial begin
// 0ns时刻:初始化所有信号,避免出现不定态X
clk = 1'b0;
ren = 1'b0; // 初始关闭读使能
wen = 1'b0; // 初始关闭写使能
addr = 5'b00011;// 初始地址设为3(二进制00011)
wdata = 32'h0; // 初始写数据为0
#12; // 延迟12ns,等待时钟稳定
wen = 1'b1; // 打开写使能,准备写操作
wdata = 32'h0a0a0a0a; // 要写入的数据:0a0a0a0a
#5; // 再延迟5ns
wen = 1'b0; // 关闭写使能,结束写操作
#2; // 再延迟2ns
ren = 1'b1; // 打开读使能,开始读操作
end
4. 时钟信号产生
verilog
// 产生周期为10ns(频率100MHz)的方波时钟
// 每5ns翻转一次,0ns:0 → 5ns:1 → 10ns:0 → 15ns:1 → ...
always #5 clk = ~clk;
endmodule
⑥仿真波形图时序分析(核心重点)
右侧波形图是上述Testbench运行后的结果,横坐标是时间(单位ns),纵坐标是各个信号的值。黄色光标标记在15ns时刻,我们按时间轴逐段分析:
1. 0ns ~ 12ns:初始化与等待阶段
-
0ns时刻:
- 所有信号按initial块初始化:clk=0,ren=0,wen=0,addr=03,wdata=00000000
- 被测模块的内部寄存器
regfile已通过$readmemh完成初始化:regfile[0] = a0000000regfile[1] = a0000001regfile[2] = a0000002regfile[3] = a0000003(初始值)regfile[4] = a0000004
- 读使能
ren=0,所以输出rdata=00000000(符合设计中"ren=0时输出全0"的逻辑)
-
5ns时刻 :clk第一次上升沿。此时
wen=0,无写操作,寄存器值不变。 -
10ns时刻:clk第二次下降沿。
2. 12ns ~ 17ns:同步写操作阶段
-
12ns时刻:
- 激励变化:
wen=1,wdata=0a0a0a0a - ⚠️ 关键:此时clk是低电平,虽然wen变高,但写操作是同步时钟上升沿触发,所以此时不会写入数据。
- 激励变化:
-
15ns时刻(黄色光标处):
- clk第三次上升沿到来
- 此时
wen=1、addr=03,满足写操作条件 - 执行写操作:将
0a0a0a0a写入regfile[3] - 波形验证:
regfile[3]的值在15ns处从a0000003跳变为0a0a0a0a,完全符合同步写的设计。
-
17ns时刻:
- 激励变化:
wen=0,关闭写使能 - 写操作已完成,后续时钟上升沿不会再写入数据。
- 激励变化:
3. 19ns之后:异步读操作阶段
- 19ns时刻 :
- 激励变化:
ren=1,打开读使能 - ⚠️ 关键:读操作是异步组合逻辑,不需要等待时钟
- 执行读操作:输出
regfile[addr]即regfile[3]的值0a0a0a0a - 波形验证:
rdata在19ns处立刻从00000000跳变为0a0a0a0a,完全符合异步读的设计。
- 激励变化: