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设计基础与Verilog实战知识体系 RTL设计基础与Verilog实战知识体系 RTL设计 组合逻辑 时序逻辑 assign / always@(*) 避免latch 状态机设计 复位策略 三段式写法 default分支 异步复位同步释放 代码风格与可综合设计 命名规范 模块化 避免latch 阻塞赋值规范 参数化设计

这张图把组合逻辑、时序逻辑、状态机、复位策略和代码风格串起来了。你写RTL的时候,脑子里要有这张图——每个模块属于哪个分支,该用什么写法,心里要有数。

一个小技巧:写RTL之前,先在纸上画个框图。组合逻辑画成云朵形状,时序逻辑画成方框。画完你就知道哪些信号是组合输出,哪些是寄存器输出。这个习惯帮我避免了很多低级错误。

好了,这一章的内容就到这儿。记住:RTL设计不是写代码,是画电路。你写的每一行Verilog,都要能对应到实际的逻辑门和触发器上。想清楚再写,比写完了再改要快得多。