第三讲:Verilog基础与流水线思维

各位同学,今天咱们聊点硬核的。组合逻辑、时序逻辑、阻塞赋值、非阻塞赋值,还有流水线——这些概念是FPGA设计的基石。我做了十几年FPGA,说句实话,很多人栽跟头就栽在这些基础概念上。

你想想看,写Verilog和写软件最大的区别在哪?软件是顺序执行的,而硬件是并发的。这个思维转变,是入门的第一道坎。

组合逻辑与时序逻辑

先说说组合逻辑。说白了,就是输入一变,输出立马跟着变。没有时钟,没有存储,纯粹的门电路搭出来的东西。比如一个与门,输入A和B都是1,输出就是1,没有任何延迟的概念——当然,实际物理上是有门延迟的,但咱们做RTL设计时先不考虑这个。

时序逻辑就不一样了。它靠时钟驱动,在时钟沿到来的时候才更新状态。触发器(Flip-Flop)就是最典型的时序逻辑元件。我刚开始学的时候,总搞不清什么时候用组合逻辑,什么时候用时序逻辑。后来我总结了一个经验:凡是需要记住状态的,必须用时序逻辑

核心区别一句话总结:

  • 组合逻辑:输入决定输出,没有记忆
  • 时序逻辑:时钟沿触发,有记忆能力

来看个简单的例子。一个加法器,用组合逻辑实现:

// 组合逻辑加法器
module adder_comb (
    input  [7:0] a,
    input  [7:0] b,
    output [7:0] sum
);
    assign sum = a + b;
endmodule

这个代码里,a和b一变,sum立刻跟着变。没有时钟,没有寄存器。

那如果我想在每个时钟周期才更新一次结果呢?就得用时序逻辑:

// 时序逻辑加法器
module adder_seq (
    input         clk,
    input  [7:0]  a,
    input  [7:0]  b,
    output reg [7:0] sum
);
    always @(posedge clk) begin
        sum <= a + b;
    end
endmodule

看到了吗?多了个clk信号,而且用了always @(posedge clk)。这就是时序逻辑的标志。

阻塞赋值与非阻塞赋值

好,接下来这个坑,我敢说每个FPGA工程师都踩过——阻塞赋值(=)和非阻塞赋值(<=)的区别。

先记住一条铁律:组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值。别问为什么,先记住,后面慢慢理解。

阻塞赋值,顾名思义,它是"阻塞"的。在always块里,阻塞赋值是顺序执行的。上一句没执行完,下一句就等着。而非阻塞赋值是并行的,所有赋值操作在时钟沿同时发生。

我举个例子,你一看就明白了:

// 错误示范:时序逻辑用了阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;
end

这段代码,如果用阻塞赋值,会发生什么?在时钟沿到来时,先把b赋给a,然后把a(此时已经是b的值)赋给c。结果就是a和c都变成了b的值。但这是你想要的行为吗?通常不是。你想要的可能是a和c同时更新,各自取原来的值。

用非阻塞赋值就对了:

// 正确写法:时序逻辑用非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;
end

这样,在时钟沿到来时,a拿到b的值,c拿到a原来的值。两者是同时发生的。这就是硬件并发的本质。

我曾经踩过的坑:

有一次做高速接口设计,时序怎么都跑不过。查了两天,最后发现是一个always块里混用了阻塞和非阻塞赋值。仿真没问题,但综合出来的电路完全不是我想的那样。从那以后,我每个always块都严格检查赋值方式。

流水线设计思想

好了,重头戏来了。流水线(Pipeline)是FPGA加速的核心思想之一。说白了,就是把一个大的组合逻辑拆成几个小段,每段之间用寄存器隔开。

为什么要这么做?因为组合逻辑越大,路径延迟就越长,能跑的最高频率就越低。你想想看,一个复杂的计算,比如一个32位乘法器,组合逻辑延迟可能有10ns。那你的时钟周期就不能小于10ns,也就是频率不能超过100MHz。

但如果我把这个乘法器拆成5级流水线,每级只做一小部分工作,每级的延迟可能只有2ns。那时钟周期就可以做到2ns,频率直接飙到500MHz!

当然,代价是延迟增加了。原来1个时钟周期出结果,现在要5个时钟周期。但吞吐量上去了——每时钟周期都能出一个结果。

流水线的核心思想:

  • 用延迟换吞吐量
  • 用寄存器打断关键路径
  • 让每一级逻辑尽量均衡

来看一个具体的例子。假设我们要做一个4级流水线的加法器链:

// 4级流水线加法器链
module pipeline_adder (
    input         clk,
    input         rst_n,
    input  [7:0]  a,
    input  [7:0]  b,
    input  [7:0]  c,
    input  [7:0]  d,
    output reg [7:0] result
);

    reg [7:0] stage1_a, stage1_b;
    reg [7:0] stage2_sum1, stage2_c;
    reg [7:0] stage3_sum2, stage3_d;
    reg [7:0] stage4_sum3;

    // 第一级:输入寄存器
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            stage1_a <= 8'd0;
            stage1_b <= 8'd0;
        end else begin
            stage1_a <= a;
            stage1_b <= b;
        end
    end

    // 第二级:计算 a+b,同时寄存 c
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            stage2_sum1 <= 8'd0;
            stage2_c    <= 8'd0;
        end else begin
            stage2_sum1 <= stage1_a + stage1_b;
            stage2_c    <= c;
        end
    end

    // 第三级:计算 (a+b)+c,同时寄存 d
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            stage3_sum2 <= 8'd0;
            stage3_d    <= 8'd0;
        end else begin
            stage3_sum2 <= stage2_sum1 + stage2_c;
            stage3_d    <= d;
        end
    end

    // 第四级:计算最终结果
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            stage4_sum3 <= 8'd0;
        end else begin
            stage4_sum3 <= stage3_sum2 + stage3_d;
        end
    end

    assign result = stage4_sum3;

endmodule

这个例子,每一级只做一个加法,路径很短。4个时钟周期后,第一个结果出来,之后每个时钟周期都能出一个结果。

性能提升原理

流水线为什么能提升性能?咱们用数据说话。

假设一个组合逻辑的延迟是T,不流水线的话,最大时钟周期就是T,吞吐量是1/T。

如果分成N级流水线,每级延迟大约是T/N(加上寄存器延迟T_reg),时钟周期可以做到T/N + T_reg。吞吐量就是N/(T + N*T_reg)。

当N足够大时,吞吐量趋近于1/T_reg。也就是说,流水线的极限性能受限于寄存器本身的延迟

流水线级数 时钟周期 延迟 吞吐量
1级(无流水线) 10ns 10ns 100M结果/秒
2级 5.5ns 11ns 182M结果/秒
4级 3.25ns 13ns 308M结果/秒
8级 2.125ns 17ns 470M结果/秒

看到没?流水线级数越多,吞吐量越高,但延迟也在增加。这就是我说的"用延迟换吞吐量"。

个人经验:

我一般不会把流水线做到超过10级。级数太多,控制逻辑变复杂,而且寄存器延迟占比越来越大,收益递减。另外,流水线深度太深,芯片面积也受不了。做设计要权衡,不是越深越好。

流水线的实际应用

在大模型推理加速中,流水线无处不在。比如矩阵乘法、卷积运算、注意力机制,这些计算密集型操作,几乎都是用流水线实现的。

我记得有一次做Transformer的加速器,一个矩阵乘法模块,输入是512x512的矩阵。如果不用流水线,一个乘法器要算262144次乘法,延迟大得吓人。后来我用了8级流水线的乘法器阵列,每个时钟周期能出64个结果,吞吐量直接提升了8倍。

当然,流水线设计也有坑。比如流水线冒险(Hazard)——数据相关、控制相关,处理不好就会出bug。这个咱们后面会专门讲。

嗯,今天就先聊到这儿。流水线思维是FPGA加速的灵魂,你把它吃透了,后面学什么都快。

流水线设计核心逻辑 无流水线 组合逻辑(延迟10ns) 时钟周期 ≥ 10ns 2级流水线 逻辑1 逻辑2 REG REG 时钟周期 ≈ 5.5ns 4级流水线 逻辑1 逻辑2 逻辑3 逻辑4 R R R R 时钟周期 ≈ 3.25ns 性能对比 无流水线: 100M结果/秒 2级流水线: 182M结果/秒 4级流水线: 308M结果/秒

这张图很直观地展示了流水线的核心思想。无流水线时,一个大的组合逻辑块,时钟周期被延迟拖累。加了流水线后,逻辑被切碎,每级之间用寄存器隔开,时钟周期大幅缩短,吞吐量成倍提升。

记住,流水线不是银弹。它适合计算密集、数据流稳定的场景。如果控制逻辑复杂、分支多,流水线反而会引入冒险,得不偿失。这个咱们后面会详细讲。

好,今天就到这儿。下一讲咱们聊聊状态机和控制通路,这是把流水线串起来的关键。