1. RTL设计基础回顾

这章咱们不搞花架子,直接回顾RTL设计的几个核心地基。我个人做验证和物理设计这些年,发现很多后期翻车其实都源自最基础的概念没吃透。你想想看,逻辑门、触发器、状态机这些老伙计,要是理解不到位,后续的时序收敛、面积优化全是扯淡。

好,咱们开始一个个过,带着我踩过的坑一起看。

1.1 数字逻辑基础——别只看符号,要看行为

数字逻辑说白了就是0和1的天下。但我觉得很多新手把问题想太简单了。比如一个简单的与门:

assign y = a & b;

这行代码背后其实包含了传播延迟、毛刺、驱动强度这些物理问题。我在一个高速接口项目中遇到过,就是因为忽略了一个简单与门的组合路径延迟,导致后仿时序违例。

逻辑门符号Verilog表达常见陷阱
与门&y = a & b多驱动时产生X态
或门|y = a | b扇出过大导致延迟剧增
非门~y = ~a与时钟配合不当产生毛刺
异或门^y = a ^ b被当成X传播源
我个人习惯把基础门电路的行为模型脑补成开关级传递,这样在分析毛刺时会更直观。

1.2 组合逻辑——组合不等于杂乱

组合逻辑的输出只依赖当前输入,没有记忆。听起来简单?但实际项目中组合逻辑的“糊墙”现象见过太多次了。说白了就是不管什么信号全往always块里塞,结果综合出一堆latch。

来看一个经典的组合逻辑例子:

always @(*) begin
    case (sel)
        2'b00: out = a + b;
        2'b01: out = a - b;
        2'b10: out = a & b;
        default: out = '0;
    endcase
end

注意啊,@(*) 触发条件一定要覆盖所有输入信号。我曾经见过有人漏掉sel本身,导致仿真和综合结果不一致。

组合逻辑中如果敏感列表不完整,在仿真时会错过事件触发,但综合工具会默默帮你补全——这就是RTL仿真和门级仿真对不上的罪魁祸首之一。

组合逻辑设计的几个要点,我总结成清单:

  • 任何组合路径必须从寄存器输出到寄存器输入,不要穿过多个组合块再直接接IO。
  • 避免冗长的组合链(深度超过10级),否则时序很难收敛。
  • case语句务必写全分支或加default,避免产生latch。

1.3 同步时序——时钟是设计的灵魂

同步时序逻辑靠时钟驱动,所有状态变化发生在时钟边沿。这个大家都懂,但我想说的是:时钟域设计才是真正的分水岭。

先看一个最简单的D触发器描述:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

这里大家可能觉得不就是清零赋值吗?但我建议你注意异步复位和同步复位的区别。异步复位在复位信号有效时立刻置零,不受时钟约束;同步复位必须在时钟沿才能生效。我在一个低功耗项目中选择了异步复位,结果综合后复位网络产生了巨大的毛刺,差点导致芯片死机。

同步时序设计原则(我自己一直贴在工位上的):

  1. 所有触发器必须在同一个时钟域内,除非做跨时钟域同步。
  2. 时钟信号不能作为普通数据使用,必须由专用PLL或时钟模块驱动。
  3. 每个always块只描述一个功能(要么纯组合,要么纯时序),不要混合。

一个很容易被忽略的点:同步复位虽然更安全,但会消耗更多的时序路径资源。如果你是做低功耗设计,可以折中采用异步复位、同步释放的方式。

1.4 状态机——三段的优雅与实战

状态机是数字设计的灵魂。我个人最推崇三段式状态机,因为它把组合逻辑、时序逻辑和输出逻辑彻底分开,可读性和可维护性都很好。看图:

状态机设计核心框架 状态跳转逻辑 组合逻辑 (next_state) 时序逻辑 (current_state) 输出逻辑 (outputs) always @(*) case (state) 下一状态 always @(posedge clk) state <= next_state (含复位) always @(*) 组合输出赋值 或 Moore/Mealy 典型三段式:组合→时序→组合,每段职责清晰,便于验证和物理实现 尤其在时钟树综合阶段,时序逻辑集中更容易做clock gating

你看,三段式结构非常清晰:

  • 第一段(组合):根据当前状态和输入,计算出下一状态。
  • 第二段(时序):在时钟沿把下一状态锁存为当前状态。
  • 第三段(组合):根据当前状态(或者状态+输入)产生输出。

我写一个Moore型状态机的标准模板给大家参考:

module fsm_moore (
    input clk, rst_n,
    input [1:0] in,
    output reg [1:0] out
);
    reg [2:0] state, next_state;
    parameter IDLE = 3'b001, RUN = 3'b010, DONE = 3'b100;

    // 第一段:时序逻辑
    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 = (in == 2'b01) ? RUN : IDLE;
            RUN:  next_state = (in == 2'b10) ? DONE : RUN;
            DONE: next_state = IDLE;
            default: next_state = IDLE;
        endcase
    end

    // 第三段:组合逻辑—输出
    always @(*) begin
        case (state)
            IDLE: out = 2'b00;
            RUN:  out = 2'b01;
            DONE: out = 2'b10;
            default: out = 2'b00;
        endcase
    end
endmodule
我个人建议:哪怕你的状态机只有两个状态,也坚持用三段式。因为后期ECO(工程变更)时,改组合逻辑和时序逻辑完全隔离,风险最小。
我曾经在一个项目中发现,状态机的default分支只是随便写了个IDLE,结果综合后出现了无用的状态转移路径,导致功耗增加了15%。所以default一定要认真考虑,最好能覆盖所有无效状态并跳转到安全状态。

1.5 再唠叨两句

回顾完这些基础,你可能会觉得:这都老掉牙了。但我在做RTL布局CI验证的时候,每次遇到综合后面积异常、时序违例、仿真对不上,最后追根溯源十有八九都跟这些基础概念有关系。比如组合逻辑里的latch、状态机未覆盖全的case、异步复位没处理好等等。

所以,把基础打牢,后面咱们讲CI流水线、物理设计约束才有底气。嗯,这是第一章,内容不多,但都很关键。后面几章我们开始搭完整的RTL布局CI环境,到时候你会感谢今天花时间回顾的这些老伙计。