4. RTL设计基础与Verilog/SystemVerilog

在RISC-V CPU项目中,RTL代码不是“能仿真出波形”就算完成,而是要同时满足功能正确、时序可收敛、可综合、可复用、可验证五个条件。Verilog/SystemVerilog 是描述硬件结构的语言,不是软件编程语言;写RTL时,我们实际上是在描述寄存器、组合逻辑、连线与模块之间的数据流。本节围绕模块化设计、时序与组合逻辑划分、参数化复用以及可综合代码规范展开,为后续流水线、总线与存储子系统实现打基础。

一、模块化设计:先定接口,再写内部

RISC-V CPU通常由取指、译码、执行、访存、写回等模块组成。模块化设计的核心是接口稳定、职责单一、层次清晰。一个模块只做一件事,例如ALU只负责运算,寄存器堆只负责读写,译码器只负责产生控制信号。模块之间通过端口连接,避免跨层引用内部信号。

推荐采用“自顶向下划分、自底向上实现”的流程:先画模块框图,明确每个模块的输入输出、位宽、时钟与复位;再逐个实现并单独仿真;最后在顶层集成。接口命名要统一,例如时钟用 clk,同步复位用 rst_n,有效信号用 _n 或 _i/_o 区分方向。

RISC-V CPU 模块化层次与接口关系 取指 IF PC / IMEM 接口 译码 ID 指令解析 / 控制 执行 EX ALU / 分支判断 访存 MEM DMEM / 对齐 寄存器堆 x0~x31 / 双读单写 写回 WB 结果选择 / 提交 顶层 Top:时钟/复位、流水线寄存器、旁路与冒险控制、总线互联 接口统一:clk、rst_n、valid/ready、data、addr、we

二、组合逻辑与时序逻辑:边界必须清晰

组合逻辑的输出只取决于当前输入,用 assign 或 always_comb 描述;时序逻辑的输出在时钟边沿更新,用 always_ff 描述。两者混写是RTL中最常见的错误来源。例如在 always_ff 中同时写组合赋值,会导致仿真与综合结果不一致。

推荐写法:组合逻辑使用 always_comb 并对所有分支赋默认值,避免锁存器;时序逻辑使用非阻塞赋值 <=,组合逻辑使用阻塞赋值 =。下面是一个带参数化的ALU片段:

module alu #(
    parameter int WIDTH = 32
) (
    input  logic [WIDTH-1:0] a,
    input  logic [WIDTH-1:0] b,
    input  logic [3:0]       op,
    output logic [WIDTH-1:0] y,
    output logic             zero
);
    always_comb begin
        y = '0;
        unique case (op)
            4'b0000: y = a + b;
            4'b0001: y = a - b;
            4'b0010: y = a & b;
            4'b0011: y = a | b;
            4'b0100: y = a ^ b;
            4'b0101: y = (a < b) ? 1 : 0;
            default: y = '0;
        endcase
    end
    assign zero = (y == '0);
endmodule
组合逻辑中一定要给输出默认值,否则综合工具可能推断出锁存器(latch),在FPGA中会带来时序与毛刺问题。

三、参数化设计:让模块可复用、可扩展

RISC-V CPU中,数据位宽、寄存器数量、指令存储器深度、流水线级数都可能变化。参数化设计让同一份RTL适配不同配置。常用方式包括 parameter、localparam 和 generate。参数用于外部例化时覆盖,localparam用于内部常量,generate用于条件或循环生成硬件结构。

例如寄存器堆可以用 parameter int DATA_WIDTH = 32 和 parameter int ADDR_WIDTH = 5 描述,例化时通过 #(.DATA_WIDTH(64)) 覆盖。注意:参数化不是越多越好,过度参数化会降低可读性,建议只暴露真正需要变化的维度。

设计维度 推荐写法 常见错误
位宽 parameter int WIDTH = 32 直接写死 32,后续扩展困难
深度 parameter int DEPTH = 1024 用 magic number 表示地址范围
条件结构 generate if / for 用软件式 if 生成硬件
内部常量 localparam 定义状态编码 用 parameter 暴露内部状态

四、可综合代码规范与常见误区

可综合代码必须能被综合工具映射为真实硬件。以下规范建议在项目初期就纳入代码检查:

  • 使用 logic 替代 reg/wire 混用,接口方向明确。
  • 时序逻辑统一使用非阻塞赋值,组合逻辑使用阻塞赋值。
  • 避免在多个 always 块中驱动同一信号。
  • 避免使用 initial 赋初值(FPGA可支持,ASIC通常不可综合)。
  • 避免使用 #delay、fork/join、动态数组等不可综合结构。
  • 状态机使用三段式或两段式写法,状态编码用 localparam。
  • 复位策略统一:推荐同步复位、低有效,并在顶层统一同步化。
常见误区:在组合逻辑中漏写 default 分支,导致综合出锁存器;在时序逻辑中使用阻塞赋值,导致仿真波形与综合后时序不一致;以及跨模块引用内部信号,破坏模块边界。这些问题在RTL仿真阶段可能不明显,但在FPGA原型验证中会表现为时序违例或功能异常。

复盘建议:每完成一个模块,先做lint检查,再做单元仿真,最后做综合报告检查。重点关注锁存器推断、未连接端口、位宽不匹配和跨时钟域信号。只有把RTL设计基础打牢,后续的流水线、总线与FPGA原型验证才能稳定推进。