3. RTL设计与仿真基础:Verilog基础回顾、RISC-V处理器核微架构简介、RTL仿真流程、波形查看与调试
各位同学,欢迎来到第三章。这一章咱们要打好地基。说白了,RTL设计和仿真就是芯片验证的“练功房”。你设计写得再花哨,仿真跑不通,一切都是白搭。我个人习惯,在开始一个新项目前,一定会花时间把这块的基础再过一遍,磨刀不误砍柴工嘛。
3.1 Verilog基础回顾:别小看这些“老伙计”
Verilog这东西,大家在学校都学过。但真正到了项目里,你会发现很多坑。我刚开始做验证那会儿,就因为在组合逻辑里不小心用了阻塞赋值,导致仿真结果和预期差了十万八千里,查了整整两天。
这里我挑几个最核心、也最容易出问题的点,咱们快速过一遍。
3.1.1 阻塞赋值与非阻塞赋值
这是Verilog的“灵魂拷问”。
- 阻塞赋值 (=):顺序执行,像C语言一样。用在组合逻辑里。
- 非阻塞赋值 (<=):并行执行,所有赋值同时发生。用在时序逻辑里。
为什么会这样?因为硬件本身就是并行的。你想想看,一个时钟沿到来,所有寄存器同时采样,怎么可能有先后顺序?
核心原则:
- 写组合逻辑(always @(*))用阻塞赋值。
- 写时序逻辑(always @(posedge clk))用非阻塞赋值。
- 千万别混用!我曾经见过一个同事,在同一个always块里又用=又用<=,结果仿真波形看起来对,但综合出来的电路逻辑完全错了。
3.1.2 组合逻辑与时序逻辑的建模
说白了,组合逻辑就是“线”,输入一变,输出立刻变。时序逻辑就是“寄存器”,只在时钟沿变化。
举个例子,一个简单的D触发器:
// 时序逻辑:D触发器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
// 组合逻辑:多路选择器
always @(*) begin
if (sel)
y = a;
else
y = b;
end
嗯,这里要注意,组合逻辑的敏感列表一定要写全。如果你漏了某个输入信号,仿真时可能不会报错,但综合出来的电路会多一个锁存器(latch)。这玩意儿在数字电路里基本是“毒瘤”,能不用就不用。
3.1.3 状态机设计
RISC-V处理器里到处都是状态机。比如取指、译码、执行、访存、写回,这就是一个典型的状态机。
我个人习惯用三段式状态机:
- 第一段:时序逻辑,描述状态跳转。
- 第二段:组合逻辑,描述下一状态的条件。
- 第三段:组合逻辑,描述输出。
这样写出来的代码结构清晰,可读性强,也方便调试。我曾经接手过一个项目,里面全是用一段式写的状态机,代码又长又乱,改一个bug能改出三个新bug,那叫一个痛苦。
3.2 RISC-V处理器核微架构简介
咱们这门课主要讲验证,但你不了解处理器是怎么工作的,验证就无从下手。这里我以最简单的单周期RISC-V核为例,带你快速过一遍微架构。
提示:单周期核虽然性能差,但结构清晰,非常适合用来理解指令执行流程。
3.2.1 五级流水线基础
现代RISC-V处理器基本都是流水线结构。最经典的就是五级流水线:
每个阶段干的事很明确:
- IF:从指令存储器里把指令取出来。
- ID:解析指令,读出寄存器堆里的操作数。
- EX:执行运算,比如加法、移位、比较。
- MEM:访问数据存储器(只有load/store指令需要)。
- WB:把结果写回寄存器堆。
你想想看,如果一条加法指令后面紧跟着一条减法指令,减法指令要用加法指令的结果,但加法指令的结果还没写回寄存器,这就产生了数据冒险。怎么解决?后面章节我们会详细讲。
3.2.2 控制通路与数据通路
处理器核里,说白了就两大部分:
- 数据通路:负责数据的流动和运算。包括寄存器堆、ALU、存储器接口等。
- 控制通路:负责指挥数据通路怎么干活。包括译码器、状态机、冒险检测单元等。
验证的时候,这两部分要分开测,也要合起来测。我一般先测数据通路,确保每条指令的运算结果是对的,再测控制通路,看看分支跳转、异常处理这些控制逻辑有没有问题。
3.3 RTL仿真流程
仿真流程,说白了就是“写代码 -> 编译 -> 运行 -> 看结果 -> 改代码”的循环。但这里有几个关键步骤,我建议你养成习惯。
3.3.1 仿真环境搭建
一个完整的仿真环境,至少包括:
- Testbench:顶层模块,负责实例化DUT(Design Under Test),提供时钟和复位。
- 激励生成器:产生输入信号,比如指令序列、数据。
- 参考模型:用高级语言(如C、SystemVerilog)实现一个“黄金”模型,用来比对结果。
- 检查器:自动比对DUT输出和参考模型输出,发现不一致就报错。
警告:千万别用手动比对波形的方式来做回归测试!人眼是会疲劳的,而且效率极低。一定要写自动检查脚本。
3.3.2 编译与运行
以常用的VCS或Iverilog为例,流程大致如下:
# 1. 编译RTL代码
vcs -full64 -sverilog +v2k -f filelist.f -l compile.log
# 2. 运行仿真
./simv -l run.log +ntb_random_seed=42
# 3. 查看波形
dve -vpd vcdplus.vpd &
嗯,这里要注意,仿真种子(random seed)一定要记录下来。如果某个测试用例失败了,你可以用同样的种子复现,方便调试。
3.4 波形查看与调试
波形是验证工程师的“眼睛”。不会看波形,就像瞎子摸象。我个人习惯用GTKWave或者DVE,这里以GTKWave为例。
3.4.1 波形文件格式
常见的波形格式有:
| 格式 | 特点 | 适用场景 |
|---|---|---|
| VCD | 标准格式,文件大 | 小规模设计,兼容性好 |
| FSDB | 压缩率高,速度快 | 大规模设计,Synopsys工具 |
| VPD | VCS专用,功能丰富 | VCS用户 |
3.4.2 调试技巧
看波形不是瞎看,要有方法。我总结了几条经验:
- 先看全局,再看局部:先看看时钟、复位、关键控制信号是否正常,再深入看具体数据。
- 善用标记:在波形上标记出关键时间点,比如指令开始执行、异常触发等。
- 分组显示:把相关的信号放在一组,比如“取指阶段信号”、“译码阶段信号”,方便对比。
- 使用表达式:GTKWave支持自定义表达式,比如把几个信号拼成一个总线来看。
小技巧:我曾经遇到一个bug,仿真结果总是差一个时钟周期。查了半天,最后发现是testbench里时钟的相位和DUT里时钟的相位差了180度。所以,看波形时第一件事就是检查时钟沿对齐。
好了,这一章的内容就到这里。RTL设计和仿真是个熟能生巧的活,多练、多踩坑,自然就熟练了。下一章我们会深入RISC-V指令集,看看每条指令在硬件里是怎么实现的。
公众号:蓝海资料掘金营,微信deep3321