4. RTL设计基础与Verilog实战:组合逻辑与时序逻辑、状态机设计、同步与异步复位、代码风格与可综合设计
做RISC-V处理器核的FPGA原型验证,RTL设计是地基。地基不稳,后面验证做得再花哨也是白搭。这一章咱们就聊聊Verilog实战中那些绕不开的核心点——组合逻辑、时序逻辑、状态机、复位策略,还有代码风格。
我个人习惯把RTL设计看成两件事:连线和存值。组合逻辑就是连线,时序逻辑就是存值。搞明白这个,很多问题就简单了。
4.1 组合逻辑 vs 时序逻辑
先说说组合逻辑。它没有记忆功能,输出只取决于当前输入。说白了,就是一堆逻辑门连在一起,信号穿过去就完事。
组合逻辑的核心特征:
- 输出随输入变化立即变化(不考虑门延迟)
- 没有时钟沿触发
- 不能存储状态
- 典型电路:加法器、多路选择器、译码器
时序逻辑就不一样了。它靠时钟沿来更新状态,有记忆功能。我在项目中遇到过一个问题:一个同学写的RTL,组合逻辑里混了寄存器赋值,结果综合出来的电路面积大了三倍。嗯,这里要注意——组合逻辑用assign或always@(*),时序逻辑用always@(posedge clk),别混着用。
// 组合逻辑:多路选择器
assign data_out = (sel) ? data_a : data_b;
// 时序逻辑:D触发器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
你想想看,如果我把上面两个写法搞反了——把组合逻辑写成always@(posedge clk),那综合出来就是个寄存器,功能没错但面积和功耗都上去了。做RISC-V核的时候,这种问题会放大,因为核里动辄几千个寄存器。
4.2 状态机设计实战
状态机是RISC-V控制通路的核心。处理器取指、译码、执行、访存、写回,每一步都是一个状态。我习惯用三段式状态机,结构清晰,综合结果也稳定。
三段式状态机结构:
| 段落 | 功能 | 写法 |
|---|---|---|
| 第一段 | 状态转移(时序逻辑) | always@(posedge clk or negedge rst_n) |
| 第二段 | 次态逻辑(组合逻辑) | always@(*) 或 assign |
| 第三段 | 输出逻辑(组合逻辑) | always@(*) 或 assign |
// 三段式状态机示例:RISC-V取指状态机
localparam IDLE = 2'b00;
localparam FETCH = 2'b01;
localparam DECODE = 2'b10;
reg [1:0] state, next_state;
// 第一段:状态寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:次态逻辑
always @(*) begin
case (state)
IDLE: next_state = start ? FETCH : IDLE;
FETCH: next_state = mem_ready ? DECODE : FETCH;
DECODE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑
assign mem_req = (state == FETCH);
assign decode_en = (state == DECODE);
我的经验:三段式状态机的输出用组合逻辑,可以避免一个时钟周期的延迟。做RISC-V核的时候,流水线每一拍都很宝贵,少一个周期延迟,性能就上去一截。
我曾经踩过一个坑:状态机里漏写了default分支。综合工具会推断出latch,功能仿真没问题,但上板就跑飞了。从那以后,我写case语句必带default,哪怕只是赋个默认值。
4.3 同步复位 vs 异步复位
复位策略是个老生常谈的话题。我直接说结论:FPGA原型验证中,我推荐用异步复位、同步释放。
为什么?
- 异步复位:复位信号不依赖时钟,响应快。但复位释放时如果刚好在时钟沿附近,容易产生亚稳态。
- 同步复位:复位信号需要时钟沿采样,能避免亚稳态。但复位脉冲宽度必须大于一个时钟周期,否则采不到。
- 异步复位、同步释放:结合两者优点,复位时立即生效,释放时与时钟同步。
注意:FPGA内部的寄存器通常有专用的异步复位端口(比如Xilinx的CLR/SR引脚)。如果你用同步复位,综合工具会用数据路径来实现复位,会多占用LUT资源。做RISC-V核的时候,复位路径上的寄存器可能有几百个,资源浪费不容忽视。
// 异步复位、同步释放电路
reg rst_sync1, rst_sync2;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rst_sync1 <= 1'b0;
rst_sync2 <= 1'b0;
end
else begin
rst_sync1 <= 1'b1;
rst_sync2 <= rst_sync1;
end
end
// 用rst_sync2作为全局复位
always @(posedge clk or negedge rst_sync2) begin
if (!rst_sync2)
reg_file[i] <= 32'b0;
else
reg_file[i] <= data_in;
end
4.4 代码风格与可综合设计
代码风格这东西,看着虚,其实直接影响验证效率和综合结果。我总结了几条硬规矩:
| 规则 | 说明 | 为什么重要 |
|---|---|---|
| 命名规范 | 信号用snake_case,参数用大写 | 一眼能看出是信号还是常量 |
| 模块化 | 每个模块只做一件事 | 验证时好写testbench,复用性高 |
| 避免latch | 组合逻辑中所有分支都要赋值 | latch会导致时序分析困难 |
| 避免阻塞赋值混用 | 时序逻辑用<=,组合逻辑用= | 混用会导致仿真与综合不一致 |
| 参数化设计 | 用parameter定义位宽、深度 | 改参数就能适配不同配置 |
我记得有一次做RISC-V核的验证,一个同事把always块里的敏感列表写漏了一个信号。仿真跑了一整天,结果发现输出不对。查了半天,原来是组合逻辑的敏感列表不全。从那以后,我写组合逻辑一律用always@(*),省心。
可综合设计的黄金法则:
- 只用always、assign、实例化——别用initial、fork、wait
- 时序逻辑的敏感列表只包含时钟和异步复位
- 组合逻辑的敏感列表用@(*)
- 循环用generate for,别用for循环(除非在always块里)
- 别用#delay——综合工具会忽略它
下面这张图总结了本章的核心知识体系,你看看能不能帮你理清思路:
这张图把组合逻辑、时序逻辑、状态机、复位策略和代码风格串起来了。你写RTL的时候,脑子里要有这张图——每个模块属于哪个分支,该用什么写法,心里要有数。
一个小技巧:写RTL之前,先在纸上画个框图。组合逻辑画成云朵形状,时序逻辑画成方框。画完你就知道哪些信号是组合输出,哪些是寄存器输出。这个习惯帮我避免了很多低级错误。
好了,这一章的内容就到这儿。记住:RTL设计不是写代码,是画电路。你写的每一行Verilog,都要能对应到实际的逻辑门和触发器上。想清楚再写,比写完了再改要快得多。