3、RTL设计与仿真:编写Verilog代码、行为仿真、波形查看与调试
好了,咱们进入正题。这一章是FPGA设计的核心环节——写代码、跑仿真、看波形、找问题。说白了,就是把你脑子里的逻辑,变成机器能理解的语言,再验证它对不对。
我刚开始学的时候,总觉得仿真可有可无,代码写完直接上板子跑多痛快。结果呢?板子亮了,功能不对,找bug找到怀疑人生。后来我学乖了——仿真做得好,调试时间能省下一大半。
3.1 编写Verilog代码——从模块开始
Verilog的基本单元是模块(module)。一个模块就像一个小黑盒,有输入、有输出,里面装着你要实现的逻辑。
先看一个最简单的例子——D触发器:
module d_flip_flop (
input wire clk, // 时钟
input wire rst_n, // 异步复位,低有效
input wire d, // 数据输入
output reg q // 数据输出
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
endmodule
这里有几个关键点:
- 端口声明:input/output,wire/reg要分清。我见过有人把input声明成reg,仿真报错半天找不到原因。
- 时序逻辑:用
always @(posedge clk or negedge rst_n)描述。敏感列表里时钟沿和复位沿都要写全。 - 阻塞赋值(=) vs 非阻塞赋值(<=):时序逻辑用非阻塞,组合逻辑用阻塞。这是铁律,别问为什么,记住就行。
个人习惯:我写模块时,喜欢把输入信号全部声明为wire,输出信号如果是寄存器类型就声明为reg,组合逻辑输出也用wire。这样结构清晰,不容易混。
3.2 行为仿真——让代码动起来
代码写完了,怎么知道它对不对?写个testbench,给它喂激励,看它怎么反应。
Testbench也是一个module,但它没有输入输出端口。它的任务就是:
- 实例化被测试模块(DUT)
- 生成时钟和复位信号
- 施加测试激励
- 检查输出结果
来看刚才那个D触发器的testbench:
`timescale 1ns / 1ps
module tb_d_flip_flop;
reg clk;
reg rst_n;
reg d;
wire q;
// 实例化DUT
d_flip_flop u_dut (
.clk (clk),
.rst_n(rst_n),
.d (d),
.q (q)
);
// 生成时钟:周期10ns
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// 施加激励
initial begin
// 初始化
rst_n = 0;
d = 0;
#20;
// 释放复位
rst_n = 1;
#10;
// 改变数据
d = 1;
#10;
d = 0;
#10;
d = 1;
#20;
// 结束仿真
$finish;
end
// 监控输出
initial begin
$monitor("Time=%0t, clk=%b, rst_n=%b, d=%b, q=%b", $time, clk, rst_n, d, q);
end
endmodule
这里要注意:
`timescale定义了时间单位和精度。1ns/1ps表示时间单位1ns,精度1ps。initial块里的语句是顺序执行的,#5表示延迟5个时间单位。$monitor会在信号变化时自动打印,调试利器。
我曾经踩过的坑:写testbench时忘记加
$finish,仿真跑起来没完没了,电脑风扇呼呼转。后来养成习惯,每个testbench最后都加上$finish或设置仿真时间上限。
3.3 波形查看——用眼睛找bug
光看打印信息不够直观,波形才是王道。Vivado自带的仿真器(xsim)可以生成波形文件(.wcfg),用Vivado的波形窗口打开。
操作步骤:
- 在Vivado的Flow Navigator中点击Run Simulation → Run Behavioral Simulation
- 仿真启动后,在Scope窗口找到你要看的信号
- 右键信号,选择Add to Wave Window
- 点击Run All跑完仿真,波形就出来了
波形窗口里,你可以:
- 放大/缩小:用鼠标滚轮或工具栏的+/-按钮
- 测量时间:按住Ctrl键,在波形上拖拽,会显示时间差
- 添加标记:右键波形,选择Set Marker,方便定位关键时间点
- 改变显示进制:右键信号,选择Radix,可以切换二进制、十进制、十六进制
看波形的技巧:别从头看到尾,先找到关键事件点(比如复位释放、数据变化),然后看这些点前后的信号是否符合预期。我一般会先看时钟和复位,确认它们正常,再看数据路径。
3.4 调试——找到问题根源
波形出来了,功能不对,怎么办?别慌,按步骤来:
- 确认时钟和复位:时钟有没有起振?复位有没有释放?这是最基础的问题。
- 检查输入信号:testbench给的激励对不对?有没有毛刺?
- 跟踪关键信号:从输入到输出,一级一级看。比如计数器没计数,先看时钟,再看使能信号,再看复位。
- 对比预期值:用手算一遍,看波形上的值对不对得上。
举个例子,假设你写了一个计数器,预期是每个时钟周期加1,但波形显示它一直为0。你会怎么查?
- 先看时钟——有波形,没问题
- 再看复位——复位已经释放了,没问题
- 再看使能信号——嗯?使能一直为0?
- 找到问题了:testbench里忘记把使能拉高了
你看,一步步排查,问题很快就找到了。
我的调试习惯:在代码里加一些调试用的内部信号,比如状态机的当前状态、计数器的中间值。把这些信号也加到波形里,能帮你更快定位问题。但记得仿真完要删掉,别留到综合里。
3.5 知识结构总览
下面这张图总结了RTL设计与仿真的核心流程,你可以对照着看:
3.6 常见问题与避坑指南
| 问题 | 现象 | 解决方法 |
|---|---|---|
| 仿真结果全是X | 波形显示红色或未知态 | 检查复位是否释放、时钟是否起振、信号是否初始化 |
| 仿真结果全是Z | 波形显示高阻态 | 检查端口是否漏接、wire类型是否未驱动 |
| 仿真跑不完 | 仿真一直运行不停止 | 检查是否有死循环、是否漏了$finish |
| 波形与预期不符 | 输出值不对 | 逐级检查内部信号,确认逻辑是否正确 |
我曾经犯过的错:有一次仿真结果全是X,我查了半天,最后发现是testbench里时钟生成写成了
forever #5 clk = clk,忘记取反了。时钟一直为0,所有时序逻辑都处于未初始化状态。这种低级错误,写代码时多看一眼就能避免。
好了,这一章的内容就这些。记住:写代码是基础,仿真才是关键。多花时间在仿真上,后面会省很多事。去试试吧,把刚才的D触发器代码和testbench跑起来,看看波形是不是符合预期。