这一篇在干嘛?

FPGA 设计里有三个都叫”快”的东西:吞吐(单位时间处理多少数据)、延迟(单个数据多久走完全程)、时序(电路能不能跑在目标时钟频率上)。本章用同一个 X³ 的例子,把这三者讲清楚,并给出五种改善时序的架构级手法——这些手法靠改代码结构就能见效,而不是指望工具自动优化。

背景:为什么"随便写写"跑不快?

EDA 综合工具确实会做很多自动优化,但如果你的代码风格(架构)一开始就不对,工具能救的程度非常有限。架构层面的优化永远是设计师的责任——这是全书的核心观点,也是这一章存在的理由。

1. 速度的三个定义

在 FPGA 里,“速度”这个词有三种完全不同的含义,混着谈会出大问题:

指标含义常用度量
吞吐量(Throughput)每个时钟周期(或每秒)能处理多少数据bits/second、bits/clock
延迟(Latency)数据从输入到处理完输出所花的时间时间、时钟周期数
时序(Timing)时序元件(触发器)之间的逻辑延迟是否满足约束时钟周期、频率

补充一个初学者必备的概念:**触发器(Flip-Flop)**是 FPGA 中最基本的存储单元,在时钟上升沿把 D 端的值锁存到 Q 端。当我们说一个设计”没收敛时序(not meet timing)“时,意思是:关键路径(critical path)——即任意两个触发器之间最长的那条路径(组合逻辑延迟 + 时钟到输出延迟 + 布线延迟 + 建立时间 + 时钟偏斜等)——的延迟超过了目标时钟周期,数据来不及在下一个时钟沿之前稳定下来。

注意三者的关系:吞吐高不等于延迟低,延迟低也不代表能跑高频。它们经常互相打架,后文会反复看到这种取舍(trade-off)。

2. 高吞吐:流水线(Pipeline)

高吞吐设计关心的是稳态数据率,不太在乎某个数据花多久走完。它的思想就是福特汽车流水线:原材料从入口进入,经过一道道工序加工,成品从出口出来——而且新数据不用等上一个走完就可以进来。

在数字设计里这个抽象概念叫流水线(pipeline)。CPU 指令流水线、网络协议栈、加密引擎,几乎所有高性能设计都在用。

2.1 迭代实现:软件思维的硬件翻译

流水线设计在算法层面的关键概念叫”展开循环(unrolling the loop)“。先看一个软件式的写法——求 X 的 3 次方,软件里通常是循环迭代:

XPower = 1;
for (i=0; i < 3; i++)
    XPower = X * XPower;

这是典型的迭代算法:反复读写同一批变量。微处理器一次只执行一条指令,没有并行可言。硬件也可以照搬这种思路(下面的 Verilog 就是个”硬件版软件”):

module power3(
    output [7:0] XPower,
    output finished,
    input [7:0] X,
    input clk, start); // the duration of start is a single clock
    reg [7:0] ncount;
    reg [7:0] XPower;
 
assign finished = (ncount == 0);
 
always@(posedge clk)
    if(start) begin
    XPower <= X;
    ncount <= 2;
    end
 
else if(!finished) begin
ncount <= ncount - 1;
XPower <= XPower * X;
end
endmodule

点评

  • startfinished 之间复用同一个寄存器 XPower 和同一个乘法器,乘 3 次才算完——结构和软件循环一模一样;
  • 需要握手信号(start / finished)来指示计算的开始和结束,外部模块也得配合这套握手传数据;
  • 最要命的:上一次计算没做完,下一次不能开始

它的性能:

Throughput = 8/3, or 2.7 bits/clock
Latency = 3 clocks
Timing = One multiplier delay in the critical path

图:迭代实现——同一套乘法器和寄存器被反复使用

2.2 流水线实现:展开循环

把循环展开成三级流水线,每级做一次乘法:

module power3(
    output reg [7:0] XPower,
    input clk,
    input [7:0] X
);
reg [7:0] XPower1, XPower2;
reg [7:0] X1, X2;
always @(posedge clk) begin
    // Pipeline stage 1
    X1 <= X;
    XPower1 <= X;
 
    // Pipeline stage 2
    X2 <= X1;
    XPower2 <= XPower1 * X1;
 
    // Pipeline stage 3
    XPower <= XPower2 * X2;
end
 
endmodule

点评

  • 不再有循环计数器和握手信号——数据像传送带一样一拍一拍往前走;
  • X1X2 是把 X 值逐级传递的”影子寄存器”,保证每级拿到的操作数是对应的;
  • 第一级还在处理新的 X 时,第二级和第三级同时在处理上一拍、上上拍的数据——三级并行。

图:流水线实现——三级各用独立资源,同时处理三个数据

性能对比:

Throughput = 8/1, or 8 bits/clock
Latency = 3 clocks
Timing = One multiplier delay in the critical path

吞吐量从 2.7 bits/clock 提升到 8 bits/clock,整整 3 倍。一般规律:一个需要 n 次迭代循环的算法,展开成流水线后吞吐量提升 n 倍。而且延迟没有变差(仍是 3 拍),时序也没有变差(关键路径仍只含一个乘法器)。

核心结论

展开迭代循环可以提高吞吐量;代价是面积按比例增加(迭代版只要 1 个乘法器 + 1 个寄存器,流水线版每一级都要独立的寄存器和乘法器)。面积优化正是第 2 章的主题。

3. 低延迟:拆掉流水线寄存器

低延迟设计的目标是让数据尽快从输入到输出,为此可以牺牲吞吐量或最高时钟频率,常用的手段是引入并行、去掉流水线、走逻辑捷径。

回到 X³ 例子:迭代实现没什么可优化的(每次乘法必须寄存后才能进行下一次);但流水线实现有明显优化空间——每一级的乘积都要等下一个时钟沿才能传给下一级。把流水线寄存器全部拆掉,改成纯组合逻辑:

module power3(
    output [7:0] XPower,
    input [7:0] X
);
reg [7:0] XPower1, XPower2;
reg [7:0] X1, X2;
 
assign XPower = XPower2 * X2;
 
always @* begin
    X1 = X;
    XPower1 = X;
end
 
always @* begin
    X2 = X1;
    XPower2 = XPower1*X1;
end
 
endmodule

点评

  • 所有的 <=(时序赋值)换成了 =(组合赋值),always @* 表示电平敏感的组合逻辑块;
  • 每一级只是上一级的组合表达式,中间没有任何寄存器,数据”一路平推”直达输出;
  • 副作用是 X1XPower1 这些中间信号退化成了普通的中间连线(综合后不会生成触发器)。

图:低延迟实现——寄存器全部拆掉,三级串成一条组合逻辑路径

性能:

Throughput = 8 bits/clock (assuming one new input per clock)
Latency = Between one and two multiplier delays, 0 clocks
Timing = Two multiplier delays in the critical path

延迟降到了不到一个时钟周期(纯组合路径)。但代价在时序上非常明显:之前的关键路径只有一个乘法器,现在变成了两个乘法器串联,系统时钟周期必须大于两个乘法器延迟之和——最高频率直接掉了一半以上。

核心结论

拆掉流水线寄存器可以降低延迟;代价是寄存器之间的组合逻辑延迟增大

4. 时序优化:五种架构手法

时序指的是设计的时钟速度。任意两个触发器之间的最大延迟决定了最高时钟频率。经典的最高频率公式(忽略时钟抖动):

各参数含义:

符号含义
F_max时钟允许的最高频率
T_clk-q时钟到达后数据出现在 Q 端的时间(触发器输出延迟)
T_logic触发器之间组合逻辑的传播延迟
T_routing触发器之间的布线延迟
T_setup建立时间——数据必须在下一个时钟上升沿之前提前稳定的最短时间
T_skew时钟从发射触发器传播到捕获触发器的延迟(为正时可”放宽”约束)

背景:建立时间(setup time)

触发器要求在时钟沿到来之前数据已经稳定存在一段时间,这就是建立时间。如果关键路径太长,数据在时钟沿到来时还没稳定,触发器就可能锁到错误的值。F_max 公式的分母越小,能跑的频率越高——下面五种手法全都是在”砍分母”。

4.1 增加寄存器层(Add Register Layers)

思路:在关键路径中间插入寄存器,把一条长路径切成两条短路径。适用于高度流水化、多加一拍延迟不影响功能的设计。

看一个 3 抽头 FIR(Finite Impulse Response,有限冲激响应)滤波器——把当前的输入 X 和前两拍的输入 X1、X2 分别乘上系数 A、B、C 再相加。假设下面这个实现不满足时序:

module fir(
    output [7:0] Y,
    input [7:0] A, B, C, X,
    input clk,
    input validsample);
    reg [7:0] X1, X2, Y;
always @(posedge clk)
    if (validsample) begin
    X1 <= X;
    X2 <= X1;
    Y <= A* X+B* X1+C* X2;
    end
endmodule

点评:所有乘法和加法都发生在一个时钟周期内(见图 1.4),关键路径是”一个乘法器 + 两个加法器”,很容易超标。假设延迟要求不是固定 1 拍,就可以加流水线。

图:MAC 运算全部挤在一个周期内的长路径

最简单的一层切法:在乘法器和加法器之间插一级寄存器:

module fir(
    output [7:0] Y,
    input [7:0] A, B, C, X,
    input clk,
    input validsample);
    reg [7:0] X1, X2, Y;
    reg [7:0] prod1, prod2, prod3;
 
always @ (posedge clk) begin
    if (validsample) begin
    X1 <= X;
    X2 <= X1;
    prod1 <= A * X;
    prod2 <= B * X1;
    prod3 <= C * X2;
    end
    Y <= prod1 + prod2 + prod3;
end
 
endmodule

点评:三个乘法各自先存进 prod1/prod2/prod3,加法在下一拍才做——乘法器和加法器被寄存器隔开,关键路径从”乘法器 + 加法器”缩成”乘法器”或”加法器”中较长的一个(见图 1.5)。乘法器是特别适合流水化的对象,因为它的计算天然可以拆成多级;拆开之后再加流水线就和上面一样简单了。

图:乘法与加法之间插入流水线寄存器

核心结论

增加寄存器层,把关键路径一分为二,每条都比原来短,时序即改善。

4.2 并行结构(Parallel Structures)

思路:把串行执行的一长串逻辑拆成可以同时运算的多个小函数,再合并结果。路径延迟变成”最长子结构”的延迟而不是”所有子结构之和”。

还是 X³ 流水线设计,假设它不满足时序。一个 8 位乘法器可以按半字节(nibble,4 bit)拆开:设 X = {A, B}(A 是高 4 位,B 是低 4 位),由于被乘数和乘数相同:

也就是把一个 8×8 乘法变成三个 4×4 乘法加移位合并。实现如下:

module power3(
    output [7:0] XPower,
    input [7:0] X,
    input clk);
reg [7:0] XPower1;
// partial product registers
reg [3:0] XPower2_ppAA, XPower2_ppAB, XPower2_ppBB;
reg [3:0] XPower3_ppAA, XPower3_ppAB, XPower3_ppBB;
reg [7:0] X1, X2;
wire [7:0] XPower2;
 
// nibbles for partial products (A is MS nibble, B is LS nibble)
wire [3:0] XPower1_A = XPower1[7:4];
wire [3:0] XPower1_B = XPower1[3:0];
wire [3:0] X1_A = X1[7:4];
wire [3:0] X1_B = X1[3:0];
wire [3:0] XPower2_A = XPower2[7:4];
wire [3:0] XPower2_B = XPower2[3:0];
wire [3:0] X2_A = X2[7:4];
wire [3:0] X2_B = X2[3:0];
 
// assemble partial products
assign XPower2 = (XPower2_ppAA << 8) +
(2*XPower2_ppAB << 4) +
XPower2_ppBB;
assign XPower = (XPower3_ppAA << 8) +
(2*XPower3_ppAB << 4) +
XPower3_ppBB;
 
always @(posedge clk) begin
    // Pipeline stage 1
    X1 <= X;
    XPower1 <= X;
 
    // Pipeline stage 2
    X2 <= X1;
    // create partial products
    XPower2_ppAA <= XPower1_A * X1_A;
    XPower2_ppAB <= XPower1_A * X1_B;
    XPower2_ppBB <= XPower1_B * X1_B;
 
    // Pipeline stage 3
    // create partial products
    XPower3_ppAA <= XPower2_A * X2_A;
    XPower3_ppAB <= XPower2_A * X2_B;
    XPower3_ppBB <= XPower2_B * X2_B;
end
 
endmodule

点评

  • 每级把 8 位乘法拆成 3 个 4 位”部分积”(partial product)并行计算,再在组合逻辑里用移位加权求和拼回 8 位结果;
  • (ppAA << 8) 对应高位对齐、2*ppAB << 4 对应交叉项的权位——这就是多项式展开的硬件化;
  • 这个示例没有处理溢出问题,只是为了说明思想(见图 1.6)。

图:乘法器被拆分成可并行的小乘法,级间仍保持流水线

核心结论

把一个逻辑函数拆成若干可并行的小函数,路径延迟降为最长子结构的延迟。

4.3 展平逻辑结构(Flatten Logic Structures)

思路:专门对付”优先级编码”造成的串行逻辑链。综合工具有能力复制逻辑来降低扇出(fanout,一个信号驱动的负载数量),但它看不穿你用 if-else 写出来的伪优先级,也没有足够信息判断优先级是否真的需要。

看一个写 4 个寄存器的地址译码控制:

module regwrite(
    output reg [3:0] rout,
    input clk, in,
    input [3:0] ctrl);
 
always @(posedge clk)
    if (ctrl[0]) rout[0] <= in;
    else if (ctrl[1]) rout[1] <= in;
    else if (ctrl[2]) rout[2] <= in;
    else if (ctrl[3]) rout[3] <= in;
endmodule

点评:if-else 链意味着 ctrl[0] 优先级最高——综合器会老老实实生成一条优先级判决链(见图 1.7),逻辑一级串一级。但如果这些 ctrl 是地址译码器输出的选通脉冲(strobe),彼此天然互斥(一个地址只对应一个寄存器),优先级根本就是多余的!

图:if-else 链综合出的优先级编码结构——逻辑串联

去掉优先级,把逻辑展平:

module_regwrite(
    output reg [3:0] rout,
    input clk, in,
    input [3:0] ctrl);
 
    always @(posedge clk) begin
    if(ctrl[0]) rout[0] <= in;
    if(ctrl[1]) rout[1] <= in;
    if(ctrl[2]) rout[2] <= in;
    if(ctrl[3]) rout[3] <= in;
    end
endmodule

点评:四个 if 相互独立(不再是 else-if 链),门级实现里没有优先级逻辑(见图 1.8),每个 ctrl 独立控制自己那一位。功能上与原来完全等价(因为信号互斥),但路径短多了。

图:无优先级的独立控制结构——逻辑展平

核心结论

在不需要优先级的地方去掉优先级编码,逻辑结构被展平,路径延迟降低。

4.4 寄存器平衡(Register Balancing)

思路:把组合逻辑在寄存器之间重新分配,让最坏路径的延迟最小化。适用于关键路径与相邻路径逻辑量严重不均衡的情况——时钟频率只由最坏路径决定,有时挪一小块逻辑就能救活整个设计。

综合工具一般也有叫”寄存器平衡”的自动优化,但它们只会按预定模式处理常见结构(如大乘法器),不会改你的自定义逻辑。所以必须自己掌握这个概念

看一个三输入加法器,输入先打一拍再相加:

module adder(
    output reg [7:0] Sum,
    input    [7:0] A, B, C,
    input    clk);
    reg    [7:0] rA, rB, rC;
always @(posedge clk) begin
    rA    <= A;
    rB    <= B;
    rC    <= C;
    Sum    <= rA + rB + rC;
end
 
endmodule

点评:第一级寄存器是 rA/rB/rC,第二级是 Sum;输入到第一级之间没有任何逻辑(假设上游输出已寄存),而第一级到第二级之间挤了整个三输入加法器(见图 1.9)——严重头轻脚重。

图:已寄存的加法器——逻辑全堆在第二级

把其中一次加法往回挪一级

module adder(
    output reg [7:0] Sum,
    input [7:0] A, B, C,
    input clk);
    reg [7:0] rABSum, rC;
 
always @(posedge clk) begin
    rABSum <= A + B;
    rC    <= C;
    Sum    <= rABSum + rC;
end
endmodule

点评A + B 提前到第一级寄存器前做,第二级只剩 rABSum + rC 一次加法。两级之间的逻辑负载均衡了,关键路径缩短(见图 1.10)。功能结果完全一样——加法满足结合律,先加哪两个都行。

图:寄存器平衡后——A+B 挪到前一级

核心结论

寄存器平衡通过把组合逻辑从关键路径挪到相邻路径来改善时序。

4.5 重排路径(Reorder Paths)

思路:当多条路径与关键路径汇合时,调整代码顺序让关键路径上的逻辑尽量靠近目的寄存器,而把慢的部分安排在非关键路径上。只关注任意两个寄存器之间的逻辑即可。

看这个多路选择逻辑(假设 C 到 Out 是关键路径:比较器串两个门再进判决多路器):

module randomlogic(
    output reg [7:0] Out,
    input    [7:0] A, B, C,
    input    clk,
    input    Cond1, Cond2);
    always @(posedge clk)
    if(Cond1)
    Out <= A;
    else if(Cond2 && (C < 8))
    Out <= B;
    else
    Out <= C;
endmodule

点评:这个写法里,选择 B 的条件 Cond2 && (C < 8) 里,比较器 C < 8 的结果还要再串一个 AND 门才到多路器判决端,而 C 本身又是延迟最大的信号(见图 1.11)——慢上加慢。

图:重排前的长关键路径——比较器与逻辑门串联

假设各条件不互斥,可以调整判决顺序:

module randomlogic(
    output reg [7:0] Out,
    input    [7:0] A, B, C,
    input    clk,
    input    Cond1, Cond2);
    wire CondB = (Cond2 & !Cond1);
 
    always @(posedge clk)
    if(CondB && (C < 8))
    Out <= B;
    else if(Cond1)
    Out <= A;
    else
    Out <= C;
endmodule

点评:把与门从比较器的串行链里拿出来,提前算成 CondB,这样 C 的比较器输出更贴近目的寄存器(见图 1.12),关键路径上少了一级门的延迟。功能等价的前提是各条件不互斥——这一点必须自己保证。

图:重排后关键路径缩短——比较器更靠近目的寄存器

核心结论

重排与关键路径汇合的各条路径,把关键路径上的一部分逻辑挪得更靠近目的寄存器,时序即改善。

5. 本章要点速查

  • 高吞吐 = 最大化每秒处理的比特数;展开迭代循环提升吞吐,代价是面积等比增加
  • 低延迟 = 最小化从模块输入到输出的延迟;去掉流水线寄存器可降延迟,代价是寄存器间组合延迟增大
  • 时序 = 设计能跑的时钟速度;任意两个时序元件间最大延迟小于最小时钟周期才算”收敛”。
  • 五种时序手法:加寄存器层(切分长路径)、并行结构(延迟取最长子结构)、展平逻辑(去伪优先级)、寄存器平衡(挪逻辑均负载)、重排路径(关键逻辑贴目的寄存器)。

常见坑

最经典的坑就是把”互斥信号”写成 if-else 优先级链(4.3 节的例子)。功能仿真完全正确,但综合出来的优先级链白白拉长了关键路径,时序报告里它成了瓶颈,你却以为”代码没问题,是工具不行”。反过来也有一个坑:随手把 if-else 改成独立 if 来”优化时序”,但如果信号并不互斥,多个 if 同时命中会改变电路行为——展平之前务必先确认互斥性。

通关标准:

学完本篇你应该能做到:

  1. 给别人讲清楚吞吐、延迟、时序三者的区别,并能算出 X³ 三种实现的性能指标;
  2. 把迭代式代码重构成流水线(unrolling),说出代价是什么;
  3. 面对时序违例,按”能否加拍 → 能否拆并行 → 是否伪优先级 → 能否平衡/重排”的顺序选择手法;
  4. 看到 F_max 公式,能指出每个分项对应电路里的哪段延迟。