3. RISC-V特权架构与异常中断
各位同学,今天我们来聊聊RTL设计中最基础、也最容易踩坑的几个问题。说实话,我见过太多工程师在组合逻辑和时序逻辑的边界上翻车,也在代码评审时反复纠正阻塞与非阻塞赋值的混用。这一章,咱们就把这些基本功打扎实。
3.1 组合逻辑 vs 时序逻辑:本质区别
先问个问题:你写的Verilog代码,综合出来到底是组合电路还是时序电路?
组合逻辑的输出只取决于当前输入,没有记忆功能。时序逻辑则依赖时钟边沿,能保存状态。我个人的习惯是:写代码前先想清楚这个模块是纯组合还是带时序,别上来就写always块。
| 特性 | 组合逻辑 | 时序逻辑 |
|---|---|---|
| 输出依赖 | 仅当前输入 | 当前输入 + 历史状态 |
| 时钟 | 不需要 | 需要时钟边沿触发 |
| 敏感列表 | 所有输入信号 | 时钟沿(posedge/negedge) |
| 典型结构 | assign、always @(*) | always @(posedge clk) |
| 综合结果 | 与门、或门、多路选择器等 | 触发器、寄存器、计数器 |
关键原则:组合逻辑的always块必须使用阻塞赋值(=),时序逻辑的always块必须使用非阻塞赋值(<=)。这条规则我建议你刻在脑子里。
3.2 阻塞赋值与非阻塞赋值:为什么不能混用?
阻塞赋值(=)是"立即生效"的,执行完当前语句后,后面的语句才能看到新值。非阻塞赋值(<=)是"延迟生效"的,所有赋值在always块结束时才统一更新。
我曾经在一个项目里看到同事把阻塞和非阻塞混用在同一个always块中,结果仿真和综合结果完全对不上。排查了整整两天,最后发现就是赋值方式用错了。
3.2.1 组合逻辑的正确写法
// 正确:组合逻辑用阻塞赋值
always @(*) begin
case (sel)
2'b00: out = a & b;
2'b01: out = a | b;
2'b10: out = a ^ b;
default: out = 0;
endcase
end
3.2.2 时序逻辑的正确写法
// 正确:时序逻辑用非阻塞赋值
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 0;
else if (en)
cnt <= cnt + 1;
end
避坑指南:我曾经见过有人这样写——在同一个always块里既用阻塞又用非阻塞。仿真能跑,但综合出来的电路完全不是你想要的样子。记住:组合逻辑用=,时序逻辑用<=,永远不要混用。
3.3 模块化设计原则:从"大泥球"到"乐高积木"
好的RTL设计就像搭乐高,每个模块功能单一、接口清晰。我刚开始做设计时也喜欢把所有逻辑塞进一个模块,结果调试时痛苦不堪。后来慢慢总结出几条原则:
- 单一职责:一个模块只做一件事,比如计数器模块只负责计数,不要在里面混入状态机逻辑。
- 接口最小化:模块的输入输出信号越少越好,能用一个信号解决的问题不要用两个。
- 层次化设计:顶层模块只做实例化和连线,具体功能交给子模块。
- 参数化:用parameter定义数据位宽、计数器深度等常量,方便复用。
我的经验:写模块前先画接口图,把输入输出信号列清楚。这样写出来的代码结构清晰,别人review起来也轻松。
3.4 知识体系结构图
下面这张图展示了本章的核心知识脉络,你可以对照着梳理自己的理解:
3.5 实战案例:一个简单的计数器模块
说了这么多理论,咱们来看个实际例子。下面是一个带使能和同步复位的计数器,我故意把组合逻辑和时序逻辑分开写:
// 计数器模块 - 模块化设计示例
module counter #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire en,
output reg [WIDTH-1:0] count
);
// 时序逻辑:寄存器更新
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 0;
else if (en)
count <= count + 1;
end
endmodule
个人建议:写模块时把parameter放在最前面,这样别人一看就知道这个模块的可配置参数有哪些。我在团队里推行这个习惯后,代码复用率提高了不少。
3.6 常见错误与调试技巧
最后分享几个我经常在代码评审中看到的错误:
- 敏感列表不全:组合逻辑的always块漏掉某个输入信号,导致仿真和综合结果不一致。
- 赋值方式混用:同一个always块里既有阻塞又有非阻塞,仿真能过但综合出问题。
- 组合逻辑环路:输出反馈到输入形成组合环路,综合工具会报warning甚至error。
- 模块接口过于复杂:一个模块有几十个输入输出,调试时根本理不清。
我曾经踩过的坑:有一次写一个状态机,把组合逻辑的状态转移和时序逻辑的状态更新写在了同一个always块里。仿真看起来没问题,但综合出来的电路多了一堆不必要的锁存器。后来我养成了习惯:状态机一定分成两个always块——一个纯组合做下一状态计算,一个纯时序做状态更新。
好了,这一章的内容就到这里。记住:基础不牢,地动山摇。组合逻辑和时序逻辑的区别、阻塞与非阻塞赋值的正确用法、模块化设计的原则,这些都是你后续做RISC-V微架构设计的基石。多写、多练、多review,慢慢就会形成肌肉记忆。