3、第一个仿真:编写一个简单的Hello World Verilog模块、使用Radix ST编译与仿真、查看仿真日志
好,咱们正式开始动手了。
前面两章聊了 Radix ST 是什么、怎么装。说实话,光看不练,心里没底。我自己带新人时,第一件事就是让他们跑通一个最简单的仿真——说白了,就是让工具“动起来”。
这一章,咱们就写一个 Verilog 版的 “Hello World”。不搞复杂逻辑,就一个模块、一个测试台,然后编译、仿真、看日志。走完这一圈,Radix ST 的基本操作你就摸到门了。
3.1 编写一个最简单的 Verilog 模块
先写一个 D 触发器吧。简单,但五脏俱全。
我个人习惯,每个模块都带上时钟和复位。哪怕现在用不到,也先留着。为什么?因为我在项目中吃过亏——一开始图省事没加复位,后来要加的时候,改代码改得想哭。
// hello_world.v
module hello_world (
input wire clk,
input wire rst_n,
input wire din,
output reg dout
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
dout <= 1'b0;
else
dout <= din;
end
endmodule
嗯,这里要注意:复位是低电平有效,所以叫 rst_n。这是行业惯例,别写成 rst 然后搞反了极性。我曾经见过有人把复位搞反,仿真时波形全对,一上板子就乱跳……排查了两天才发现。
3.2 编写测试台(Testbench)
模块写好了,得有个东西去“激励”它。测试台就是干这个的。
测试台不需要端口,因为它是最顶层。我习惯把测试台和设计文件分开存放,方便管理。
// tb_hello_world.v
`timescale 1ns / 1ps
module tb_hello_world;
reg clk;
reg rst_n;
reg din;
wire dout;
// 实例化设计模块
hello_world u_hello_world (
.clk (clk),
.rst_n(rst_n),
.din (din),
.dout (dout)
);
// 生成时钟:周期 10ns
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// 测试激励
initial begin
// 初始化
rst_n = 0;
din = 0;
// 释放复位
#20 rst_n = 1;
// 输入数据变化
#10 din = 1;
#10 din = 0;
#10 din = 1;
#10 din = 0;
// 观察一段时间后结束
#50 $finish;
end
// 打印信息到日志
initial begin
$monitor("Time=%0t, rst_n=%b, din=%b, dout=%b", $time, rst_n, din, dout);
end
endmodule
这里我用 $monitor 来打印仿真过程中的信号变化。你想想看,如果没有这个,仿真跑完了你都不知道里面发生了什么。调试时,$monitor 和 $display 是我最常用的两个“老朋友”。
3.3 使用 Radix ST 编译与仿真
文件准备好了,接下来就是让 Radix ST 干活了。
Radix ST 的编译和仿真命令很简洁。我个人喜欢写一个简单的脚本,而不是每次都敲命令。但第一次,咱们还是手动来一遍,感受一下流程。
3.3.1 编译设计文件
radix st compile hello_world.v tb_hello_world.v
这条命令会把两个 Verilog 文件编译成 Radix ST 的内部格式。如果代码有语法错误,这里就会报错。我第一次用的时候,忘了写 endmodule,编译直接挂了。嗯,这种低级错误,谁还没犯过呢?
3.3.2 运行仿真
radix st simulate tb_hello_world
注意,这里指定的是测试台模块名 tb_hello_world,不是文件名。Radix ST 会自动找到顶层模块并开始仿真。
仿真跑完后,你会看到终端里打印出类似这样的信息:
Time=0, rst_n=0, din=0, dout=0
Time=20, rst_n=1, din=0, dout=0
Time=30, rst_n=1, din=1, dout=0
Time=35, rst_n=1, din=1, dout=1
Time=40, rst_n=1, din=0, dout=1
Time=45, rst_n=1, din=0, dout=0
Time=50, rst_n=1, din=1, dout=0
Time=55, rst_n=1, din=1, dout=1
Time=60, rst_n=1, din=0, dout=1
Time=65, rst_n=1, din=0, dout=0
看到 dout 在时钟上升沿跟随 din 变化了吗?这说明我们的 D 触发器工作正常。
3.4 查看仿真日志
Radix ST 默认会把仿真日志输出到终端。但如果你想保存下来慢慢看,可以用重定向:
radix st simulate tb_hello_world > sim.log
这样所有打印信息都会写入 sim.log 文件。我习惯每次仿真都保留日志,方便回溯。有一次我调一个 bug,调了三天没找到原因,最后翻出三天前的日志一对比,才发现是某个信号初始值设错了。你说冤不冤?
日志文件里除了 $monitor 打印的信息,还会包含 Radix ST 的版本信息、编译选项、仿真时间等。这些信息在排查环境问题时很有用。
radix st simulate tb_hello_world --quiet 只显示关键信息。但新手阶段,我建议还是看完整日志,有助于理解仿真流程。
3.5 本章知识体系
为了让你更直观地理解这一章的内容结构,我画了一张流程图。它展示了从编写代码到查看日志的完整链路。
这张图把咱们这一章做的事串起来了。从左到右,从上到下,每一步都很清晰。你以后做复杂项目时,流程也是一样的——只是文件更多、逻辑更复杂而已。
3.6 避坑指南
最后,分享几个我踩过的坑:
- 文件名和模块名不一致:Radix ST 默认会按文件名找模块。如果文件名是
hello_world.v,但模块名写成了hello_world_test,编译会报错。我刚开始就犯过这个错,找了半天原因。 - 忘记写
timescale:测试台里如果没有timescale,仿真时间单位会默认取一个值,可能导致时序不对。建议每个测试台都显式声明。 - 仿真时间不够长:有时候
$finish写得太早,还没看到结果仿真就结束了。我一般会在关键信号变化后再留 50~100ns 的余量。
好了,第一个仿真就跑通了。你现在应该能感受到,Radix ST 的操作并不复杂。核心就是三步:写代码、编译、仿真。后面的章节,我们会在这个基础上,逐步加入波形查看、断言、覆盖率分析等内容。
记住,调试能力是练出来的。多跑几次,多看看日志,慢慢就有感觉了。
公众号:蓝海资料掘金营,微信deep3321