3. Verilog基础与流水线设计:组合逻辑与时序逻辑、流水线概念、单周期与多周期路径

各位同学,欢迎来到第三章。这一章我们聊聊Verilog里最核心的两个概念——组合逻辑和时序逻辑,以及它们如何催生出流水线设计。说实话,很多新手工程师在这上面栽过跟头。我自己刚入行时,也把组合逻辑的毛刺当成有效信号抓了好几天,最后发现是仿真波形没看仔细。

好,咱们直接进入正题。

3.1 组合逻辑 vs 时序逻辑:本质区别

先问个问题:你写的Verilog代码,综合出来到底是组合电路还是时序电路?

组合逻辑,说白了就是输入一变,输出立刻变。没有记忆,没有时钟。比如一个简单的与门:

assign y = a & b;

只要a或b变化,y马上跟着变。中间只有门延迟,没有时钟控制。

时序逻辑就不一样了。它靠时钟边沿触发,输出只在时钟上升沿或下降沿更新。比如:

always @(posedge clk) begin
    q <= d;
end

d再怎么变,q也只会在时钟上升沿才采样更新。这就是寄存器的行为。

核心区别一句话:组合逻辑是“即时响应”,时序逻辑是“时钟同步”。

我在项目中遇到过一位同事,把组合逻辑的输出直接连到另一个模块的时钟端口上,结果仿真没问题,上板子就乱跳。为什么?因为组合逻辑的毛刺被当成时钟沿采进去了。嗯,这就是没分清组合和时序的后果。

3.2 阻塞赋值与非阻塞赋值

这块是新手最容易混淆的地方。我建议你记住一个原则:

  • 组合逻辑用阻塞赋值(=)
  • 时序逻辑用非阻塞赋值(<=)

为什么?因为阻塞赋值是“立即生效”的。在同一个always块里,前面的赋值会立刻影响后面的语句。而非阻塞赋值是“并行生效”的,所有赋值在时钟沿同时更新。

看个例子:

// 错误写法:时序逻辑用了阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;  // 这里a已经是新值了,不是我们想要的
end

// 正确写法:时序逻辑用非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;  // 这里a还是旧值,符合寄存器行为
end

我曾经在调试一个SPI接口时,就因为把非阻塞写成了阻塞,导致数据移位寄存器总是少一位。查了两天才发现是赋值方式的问题。从那以后,我写代码前都会先问自己一句:这个always块是组合还是时序?

3.3 流水线概念:为什么需要它?

好,现在咱们聊聊流水线。你想想看,一个复杂的计算任务,比如乘法累加,如果在一个时钟周期内完成,路径会很长,频率就上不去。

流水线的思路很简单:把一个大任务拆成几个小阶段,每个阶段用一个寄存器隔开。这样每个阶段的门延迟变小了,时钟频率就能提上去。

我画了一张图,帮你理解流水线的结构:

三级流水线结构示意图 组合逻辑A D Q 组合逻辑B D Q 组合逻辑C D Q clk clk clk 每个时钟周期,数据向前流动一级 第1个数据经过3个周期后输出结果 但之后每个周期都能输出一个结果(吞吐率提升)

看到没?数据从左边进来,经过组合逻辑A,被寄存器1锁存,再经过组合逻辑B,被寄存器2锁存,最后经过组合逻辑C输出。每个时钟沿,数据往前推一级。

这样做的好处是:关键路径变短了。原来一个周期要完成A+B+C三段逻辑,现在每段单独在一个周期里完成。时钟频率可以提上去,吞吐率自然就高了。

个人经验:流水线不是越多越好。每加一级寄存器,就多一个时钟周期的延迟。如果你的应用对延迟敏感(比如实时控制),流水线深度要权衡。我做过一个视频处理项目,流水线深度从3级加到7级,频率从100MHz提到了250MHz,但延迟也多了4个时钟周期。好在视频流不在乎那几十纳秒的延迟。

3.4 单周期路径 vs 多周期路径

这两个概念跟时序约束密切相关。

单周期路径:数据从起点寄存器出发,经过组合逻辑,在下一个时钟沿之前必须到达终点寄存器。这是最常规的路径。

多周期路径:数据需要多个时钟周期才能到达终点。比如一个复杂的乘法器,可能需要2个或3个时钟周期才能算出结果。

看个表格对比一下:

特性 单周期路径 多周期路径
时序约束 默认情况,工具自动检查 需要手动声明(set_multicycle_path)
组合逻辑深度 较浅,一个周期内完成 较深,分多个周期完成
时钟频率 受限于最长的单周期路径 可以更高,因为路径被拆开了
典型应用 普通寄存器到寄存器 乘法器、除法器、大位宽加法器

我在做AI加速器时,经常遇到多周期路径。比如一个16x16的乘法器,如果直接放在单周期里,路径延迟可能超过5ns,200MHz都跑不到。但拆成两级流水线,每级2.5ns,400MHz轻松搞定。

注意:多周期路径一定要在时序约束文件里声明。否则时序分析工具会认为你的路径时序违例,然后疯狂报错。我曾经在综合一个DSP模块时,忘了加set_multicycle_path,结果工具报告了2000多条违例路径。排查了半天才发现是约束没写对。

3.5 实战:用流水线实现累加器

光说不练假把式。咱们写一个4级流水线的累加器,把8个数的加法拆成4个阶段。

module pipelined_adder (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [7:0]  data_in [0:7],  // 8个输入数据
    input  wire        valid_in,
    output reg  [11:0] sum_out,
    output reg         valid_out
);

    // 流水线寄存器
    reg [11:0] stage1_sum, stage2_sum, stage3_sum;
    reg        stage1_valid, stage2_valid, stage3_valid;

    // 第1级:两两相加
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            stage1_sum   <= 0;
            stage1_valid <= 0;
        end else begin
            stage1_sum   <= data_in[0] + data_in[1] + 
                           data_in[2] + data_in[3];
            stage1_valid <= valid_in;
        end
    end

    // 第2级:再加四个数
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            stage2_sum   <= 0;
            stage2_valid <= 0;
        end else begin
            stage2_sum   <= stage1_sum + 
                           data_in[4] + data_in[5] + 
                           data_in[6] + data_in[7];
            stage2_valid <= stage1_valid;
        end
    end

    // 第3级:合并结果
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            stage3_sum   <= 0;
            stage3_valid <= 0;
        end else begin
            stage3_sum   <= stage2_sum;
            stage3_valid <= stage2_valid;
        end
    end

    // 第4级:输出
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sum_out    <= 0;
            valid_out  <= 0;
        end else begin
            sum_out    <= stage3_sum;
            valid_out  <= stage3_valid;
        end
    end

endmodule

这个例子很简单,但体现了流水线的精髓:用寄存器把大逻辑拆成小逻辑。每个时钟周期只做一小部分工作,整体吞吐率却很高。

关键点:流水线设计时,一定要把valid信号也一起传递。否则你无法知道输出数据什么时候是有效的。我见过有人只传递数据不传递valid,结果下游模块一直在采样无效数据。

3.6 避坑指南

最后,分享几个我踩过的坑:

  • 组合逻辑环路:千万别写出组合逻辑反馈。比如 assign a = a + 1; 这会在综合时产生锁存器,而且仿真会跑飞。
  • 时钟域交叉:不同时钟域的信号直接连接,会导致亚稳态。要用两级同步器或异步FIFO。
  • 流水线深度不够:如果关键路径还是太长,别犹豫,再加一级流水线。频率上不去,一切都是白搭。

我曾经在一个项目里,为了省一级流水线,把组合逻辑压得很紧。结果时序分析显示setup time违例0.3ns。我硬着头皮改代码,加了三级流水线,频率从180MHz直接飙到350MHz。嗯,有时候多花几个寄存器,换来的是整个系统的性能提升,这笔账很划算。

好,这一章就到这里。记住:组合逻辑是基础,时序逻辑是核心,流水线是性能的关键。把这三点吃透了,后面的AI加速器设计才能游刃有余。


公众号:蓝海资料掘金营,微信deep3321