这一篇在干嘛?

面积是数字设计三大物理特性之二。本章讲如何从架构(拓扑)层面省面积:把上一章展开的流水线重新”卷起来”复用逻辑、用状态机控制复用、跨模块共享资源;以及一个反直觉的重点——复位策略会极大影响面积,错误的复位写法能让综合器放弃专用硬件资源,面积暴涨几个数量级。

背景:拓扑优化 vs 电路级优化

**拓扑(topology)**指设计的高层组织方式,与具体器件无关;而综合和布局布线工具做的”电路级优化”只是把局部设计的门数减到最少,还可能依赖具体器件。工具救不了错误的拓扑——省面积必须从架构选型入手。面积优化的核心思想是:让逻辑资源被最大程度复用,代价往往是吞吐量(速度)。这正是第 1 章流水线换面积的反向操作。

2.1 卷起流水线(Rolling Up the Pipeline)

上一章”展开循环”用面积换吞吐;想要小面积,就反过来——把流水线卷起来,让各级重复的逻辑复用同一份资源。适用于流水线各级有重复逻辑的设计。

看一个 8 位定点小数乘法器:A 是普通整数格式(定点小数点在 LSB 右侧),B 的小数点在 MSB 左侧——即 B 把 A 缩放到 0~1 之间。先看”一拍出结果”的常规实现:

module mult8(
    output [7:0] product,
    input [7:0] A,
    input [7:0] B,
    input clk);
    reg [15:0] prod16;
assign product = prod16[15:8];
always @(posedge clk)
    prod16 <= A * B;
endmodule

点评:每个时钟都能出一个个新乘积。表面上看这个设计没有明显的流水线寄存器,但乘法器本身就是一条很长的逻辑链,完全可以用中间寄存器层流水化——我们要”卷”的正是这个乘法器。把它改成移位-相加(shift and add)结构:

module mult8(
    output done,
    output reg [7:0] product,
    input [7:0] A,
    input [7:0] B,
    input clk,
    input start);
    reg [4:0] multcounter; // counter for number of shift/adds
reg [7:0] shiftB; // shift register for B
reg [7:0] shiftA; // shift register for A
 
wire adden; // enable addition
 
assign adden = shiftB[7] & !done;
assign done = multcounter[3];
 
always @(posedge clk) begin
    // increment multiply counter for shift/add ops
    if (start) multcounter <= 0;
    else if (!done) multcounter <= multcounter + 1;
 
    // shift register for B
    if (start) shiftB <= B;
    else shiftB[7:0] <= {shiftB[6:0], 1'b0};
 
    // shift register for A
    if (start) shiftA <= A;
    else shiftA[7:0] <= {shiftA[7], shiftA[7:1]};
    // calculate multiplication
    if (start) product <= 0;
    else if (adden) product <= product + shiftA;
end
 
endmodule

点评(结构见图 2.1):

  • 原理就是手算竖式乘法:B 的每一位是 1 时,就把(相应左移的)A 加进累加器;
  • shiftB 每拍左移 1 位({shiftB[6:0], 1'b0}),把各 bit 轮流顶到最高位;shiftA 每拍右移 1 位({shiftA[7], shiftA[7:1]}),保持权重对应;adden = shiftB[7] 决定本拍是否累加;
  • 一次性乘法器的那棵大逻辑树被彻底消灭,换成了几个移位寄存器 + 一个加法器,面积非常紧凑;
  • 代价:一次乘法要花 8 个时钟;还有 done 需要握手,吞吐量大幅下降;
  • 妙处在于:这个数据流有自然流动方向,靠一个计数器就能调度,不需要额外控制逻辑——下一节的例子就没这么幸运了。

图:移位-相加乘法器——用移位寄存器和累加器复用一个加法器

核心结论

卷起流水线可以优化各级逻辑重复的流水线设计的面积,代价是吞吐量与延迟。

2.2 基于控制的逻辑复用(Control-Based Logic Reuse)

当共享逻辑的面积大于控制逻辑的面积时,用控制电路调度复用就是划算的。上一节的乘法器有自然数据流,不需要控制;但更多应用里,被复用资源的输入来源复杂多变,就需要状态机来指挥。

看一个低通 FIR 滤波器:

三个乘积共用一个乘法器逐个算,需要一个状态机按顺序喂入系数和采样:

module lowpassfir(
    output reg [7:0] filtout,
    output reg done,
    input clk,
    input [7:0] datain, // X[0]
    input datavalid, // X[0] is valid
    input [7:0] coeffA, coeffB, coeffC); // coeffs for low pass filter
    // define input/output samples
    reg [7:0] X0, X1, X2;
    reg multdonedelay;
    reg multstart; // signal to multiplier to begin computation
    reg [7:0] multdat;
    reg [7:0] multcoeff; // the registers that are multiplied together
    reg [2:0] state; // holds state for sequencing through mults
    reg [7:0] accum; // accumulates multiplier products
    reg clearaccum; // sets accum to zero
    reg [7:0] accumsum;
    wire multdone; // multiplier has completed
    wire [7:0] multout; // multiplier product
    // shift-add multiplier for sample-coeff mults
    mult8 mult8 (.clk(clk), .dat1(multdat),
    .dat2(multcoeff), .start(multstart),
    .done(multdone), .multout(multout));
 
always @(posedge clk) begin
    multdonedelay <= multdone;
 
    // accumulates sample-coeff products
    accumsum <= accum + multout[7:0];
 
    // clearing and loading accumulator
    if (clearaccum)    accum <= 0;
    else if (multdonedelay) accum <= accumsum;
    // do not process state machine if multiply is not done
    case(state)
    0: begin
    // idle state
    if (datavalid) begin
    // if a new sample has arrived
    // shift samples
    X0    <= datain;
    X1    <= X0;
    X2    <= X1;
    multdat <= datain;    // load mult
    multcoeff <= coeffA;
    multstart <= 1;
    clearaccum <= 1; // clear accum
    state    <= 1;
    end
    else begin
    multstart <= 0;
    clearaccum <= 0;
    done    <= 0;
    end
    end
    1: begin
    if (multdonedelay) begin
    // A*X[0] is done, load B*X[1]
    multdat   <= X1;
    multcoeff <= coeffB;
    multstart <= 1;
    state    <= 2;
    end
    else begin
    multstart <= 0;
    clearaccum <= 0;
    done    <= 0;
    end
    end
    2: begin
    if (multdonedelay) begin
    // B*X[1] is done, load C*X[2]
    multdat   <= X2;
    multcoeff <= coeffC;
    multstart <= 1;
    state <= 3;
    end
    else begin
    multstart <= 0;
    clearaccum <= 0;
    done <= 0;
    end
    end
3: begin
if(multdonedelay) begin
    // C*X[2] is done, load output
    filtout <= accumsum;
    done <= 1;
    state <= 0;
end
else begin
    multstart <= 0;
    clearaccum <= 0;
    done <= 0;
end
end
default
    state <= 0;
endcase
end
endmodule

点评(结构见图 2.2):

  • 整个设计只有一个乘法器和一个累加器(MAC);状态机在 0→1→2→3 四个状态间循环,依次调度 coeffA×X[0]、coeffB×X[1]、coeffC×X[2] 三次乘法;
  • multdonedelaymultdone 打一拍的延迟——因为 accumsum 要等一拍才有效,状态机的跳转必须与之对齐,这是复用控制里最常见的时序对齐问题;
  • 为什么需要状态机?因为这里没有像移位-相加乘法器那样”自然流动”的递归数据流——输入是几个任意的寄存器(采样和系数),要按顺序组合出所有乘积,最高效的调度方式就是状态机。

图:FIR 滤波器只保留一个 MAC——状态机调度三次乘法

核心结论

当共享逻辑比控制逻辑大得多时,用控制(状态机)来指挥逻辑复用是划算的。

2.3 资源共享(Resource Sharing)

这里说的资源共享不是布局布线工具做的低级优化,而是更高层的架构级共享:把不同功能模块里”长得像”的资源拉到层次结构的全局点,供多个功能区共用。只要某个功能块在其他地方(甚至其他模块)也能用,就值得共享。

最简单的例子是系统计数器。很多设计里散落着大量计数器:定时器、序列发生器、状态机……它们常常可以抽到更高层次统一分发。比如模块 A 用 8 位自由运行计数器,每 256 个时钟翻转一次(100 MHz 下即每 2.56 μs 触发一次);模块 B 用 11 位计数器生成 5.5 kHz 定频、占空比可变的 PWM 脉冲(100 MHz 时钟下周期为十六进制 700 个时钟,计数到预设值 1666 复位)。

图:两个模块各自维护独立的计数器

两个模块的功能完全独立,计数器特性也不同(一个 8 位自由跑、一个 11 位到点清零),但它们完全可以合并成一个全局 11 位计数器,模块 A、B 各取所需:

图:合并为共享的全局计数器

核心结论

面积敏感的紧凑设计中,主动搜索其他模块里的”同类资源”,把它们上提到层次结构的全局点共享。

2.4 复位对面积的影响

一个常见误解是:复位结构是”纯全局”的东西,对设计规模影响不大。事实恰恰相反——坚持给每个触发器都定义全局复位,看似好习惯,实际可能让设计更大更慢。原因是:某些功能本可以按 FPGA 的细粒度架构(fine-grain architecture)优化映射到专用资源,但把复位引进每个同步元件,会迫使综合与映射工具退回更粗的实现。

核心结论

不当的复位策略会让设计无谓地变大,并抑制某些面积优化。

2.4.1 没有复位能力的资源(Resources Without Reset)

看一个最简单的 15 位移位寄存器的两种写法:

// IMPLEMENTATION 1: Synchronous Reset
always @(posedge iClk)
    if(!iReset) sr <= 0;
    else sr    <= {sr[14:0], iDat};
 
// IMPLEMENTATION 2: No Reset
always @(posedge iClk)
    sr <= {sr[14:0], iDat};

点评:两种写法功能上只差一个复位态,看起来无关痛痒。关键在于:Xilinx 器件里内置了 SRL16 移位寄存器专用资源,综合器要把代码”直接映射”上去的前提是代码形态匹配。而 SRL16 没有复位控制信号——一旦代码里定义了复位,SRL16 就用不了,只能退化成离散触发器实现:

图:无复位时映射到 SRL16 专用资源——仅占 1 个 slice

图:定义复位后只能用离散触发器实现——占 9 个 slice

资源对比(表 2.1):

实现SlicesFlip-flops
定义了复位916
无复位11

去掉复位信号后,9 个 slice / 16 个触发器缩减到 1 个 slice / 1 个触发器——同时拿到了最紧凑、最高速的移位寄存器实现。

核心结论

给专用资源分配了不兼容的复位,它就不会被使用,功能将退化为通用元件实现,占用更多面积。

2.4.2 没有置位能力的资源(Resources Without Set)

类似的问题也出现在”置位(set,复位到全 1 而不是全 0)“上。以 8×8 乘法器为例:

module mult8(
    output reg [15:0] oDat,
    input iReset, iClk,
    input [7:0] iDat1, iDat2
);
 
always @(posedge iClk)
    if(!iReset) oDat <= 16'hffff;
    else    oDat <= iDat1 * iDat2;
endmodule

点评:与移位寄存器例子的唯一区别是复位值——这里是 16'hffff(置位)而不是 0。大多数 FPGA 的乘法器硬核内置了复位资源,但没有置位资源。坚持要置位功能,综合器就只能像图 2.7 那样:乘法器输出每个位上外挂一个门电路,在复位有效时强行输出 1,乘法器自带的复位反而闲置浪费。

图:置位功能只能用外部逻辑在每个输出位上搭门实现

资源对比(表 2.2):

实现SlicesFlip-flopsLUTsMult16
Reset(复位到 0)91611
Set(置位到全 1)1111

把置位改成复位操作后,同样缩减到 1 个 slice / 1 个触发器,得到最紧凑高速的乘法器实现。设计时优先用”清零”而不是”置位”,就能直接映射硬件资源。

2.4.3 没有异步复位能力的资源(Resources Without Asynchronous Reset)

新一代高性能 FPGA 提供了大量多用途内置模块(DSP、BRAM 等),它们通常有复位功能,但对复位拓扑类型有限制。以 Xilinx Virtex-4 的 DSP 乘累加(MAC,Multiply-Accumulate)模块为例,其内部结构只有同步复位能力(见图 2.8)。如果代码非要写成异步复位:

module dspckt(
    output reg [15:0] oDat,
    input    iReset, iClk,
    input    [7:0] iDat1, iDat2);
    reg    [15:0] multifactor;
 
    always @(posedge iClk or negedge iReset)
    if (!iReset) begin
    multifactor <= 0;
    oDat    <= 0;
    end
    else begin
    multifactor <= (iDat1 * iDat2);
    oDat    <= multifactor + oDat;
end
 
endmodule

点评

  • always @(posedge iClk or negedge iReset)异步复位的经典写法——复位不吃时钟,立即生效;
  • 但 DSP 硬核的复位引脚只接受同步复位(见图 2.8),综合器只能在 DSP 核外面搭一堆额外的逻辑来模拟异步复位行为。

图:Xilinx DSP 块只支持同步复位

资源对比(表 2.3):

架构SlicesFlip-flopsLUTsDSPs
异步复位1732161
同步复位0001

同步复位版本可以让综合器直接用上 DSP 硬核,外围逻辑为 0;异步复位则白白多出 17 个 slice、16 个 LUT。

核心结论

DSP 等多功能内置资源对复位策略的适配性通常很差——用它们之前先查清支持哪种复位。

2.4.4 复位 RAM(Resetting RAM)

很多 FPGA 的内置 RAM 资源也只支持同步复位。对 RAM 模块强行做异步复位对面积是灾难性的:不像乘法器+加法器可以拼出 MAC,RAM 没有更小的基本元件可以”缝”出大 RAM(只有更小的 RAM),综合器也没法在输出端加几个门就模拟出异步复位功能。

module resetckt(
    output reg [15:0] oDat,
    input iReset, iClk, iWrEn,
    input [7:0] iAddr, oAddr,
    input [15:0] iDat);
    reg [15:0] memdat [0:255];
 
    always @(posedge iClk or negedge iReset)
    if (!iReset)
    oDat <= 0;
    else begin
    if (iWrEn)
    memdat[iAddr] <= iDat;
 
    oDat <= memdat[oAddr];
    end
 
endmodule

点评:这段代码就是一个 256×16 的 RAM 读写。在 Xilinx Virtex-4 上,BRAM(Block RAM,块存储器)只有同步复位。同步复位写法可以直接映射为一个 BRAM(见图 2.9);而上面这段异步复位代码,综合器只能用分布式小 RAM 块拼接出同等容量的 RAM,外加额外的译码逻辑和异步复位实现逻辑(见图 2.10)。

图:同步复位——直接映射到单个 BRAM

图:异步复位——分布式 RAM 拼接 + 额外逻辑

资源对比(表 2.4)——差距触目惊心:

实现SlicesFlip-flops4 输入 LUTsBRAMs
异步复位3415411223880
同步复位0001

核心结论

复位 RAM 通常是糟糕的设计实践,尤其是异步复位——不当的 RAM 复位对面积是灾难性的。

2.4.5 利用触发器的置位/复位引脚(Utilizing Set/Reset Flip-Flop Pins)

反过来看:set/reset 引脚也可以被综合器”征用”来实现部分组合逻辑,从而减少查找表(LUT,Look-Up Table,FPGA 里实现组合逻辑的基本单元)负担、加速数据路径。

比如图 2.11 的 OR 门逻辑,综合器可以选择用触发器的置位(set)引脚来实现,如图 2.12 所示——门被省掉了;同理,图 2.13 的 AND 门可以用触发器的复位(CLR)引脚吸收掉,如图 2.14 所示。

图:简单的带 OR 门同步逻辑

图:OR 门被触发器 set 引脚吸收

图:简单的带 AND 门同步逻辑

图:AND 门被触发器 CLR 引脚吸收

看一个具体例子(Xilinx Spartan-3)。先写带外部异步复位的版本:

module setreset(
    output reg oDat,
    input iReset, iClk,
    input iDat1, iDat2);
 
    always @(posedge iClk or negedge iReset)
    if (!iReset)
    oDat <= 0;
    else 
    oDat <= iDat1 | iDat2;
endmodule

点评:由于规定了异步复位,综合器只能用带异步复位的触发器(图 2.15),OR 功能用离散逻辑实现——逻辑元件不可避免。

图:带异步复位的实现——OR 门要独立搭

如果去掉复位、保留同样的逻辑功能,综合器可以选用 FDS 元件(带同步 set 和 reset 的触发器),把 OR 操作直接放进 set 引脚(图 2.16)——零逻辑元件实现。

图:去掉复位后的优化——OR 由 set 引脚实现,零逻辑

还可以更进一步,同时利用同步 set 和 reset。要实现逻辑:

可以这样写,让 set 和 reset 资源都派上用场:

module setreset (
    output reg oDat,
    input iClk,
    input iDat1, iDat2, iDat3);
always @(posedge iClk)
    if (iDat3)
    oDat <= 0;
    else if (iDat1)
    oDat <= 1;
    else
    oDat <= iDat2;
endmodule

点评iDat3 的优先级最高,类似触发器的复位引脚(清 0);iDat1 命中时置 1,对应 set 引脚;否则透传 iDat2。结果(图 2.17):取反、与、或三个逻辑操作全部由一个触发器实现,零 LUT。当然,这种优化在设计架构阶段往往不可预知——所以结论是:当面积是首要考虑时,尽量避免使用 set 或 reset

图:同时利用 set 和 reset 引脚——三个逻辑操作一个触发器搞定

2.5 本章要点速查

  • 卷起流水线可优化各级逻辑重复的流水线设计的面积。
  • 共享逻辑大于控制逻辑时,可用控制(状态机)指挥逻辑复用。
  • 面积优先时,主动把各模块的同类资源(如计数器)上提到全局共享。
  • 不当的复位策略会让设计无谓变大并抑制优化;给专用资源分配不兼容的复位会导致其退化成通用元件。
  • DSP 等多功能资源对复位策略适配性差;不当复位 RAM(尤其异步复位)是面积灾难。
  • set/reset 引脚可以被综合器用来吸收组合逻辑;但约束复位也会限制可用元件库——面积优先时尽量少用 set/reset。

常见坑

最隐蔽的坑是”为了安全”给每个寄存器都加复位,尤其是异步复位。后果是分层的:轻则像移位寄存器那样丢掉 SRL16(9 倍面积),重则像 RAM 例子那样直接损失一个 BRAM、多掏 3415 个 slice——工程后期发现资源不够时,回头改复位策略要动几乎所有模块。正确的做法是:架构阶段就明确哪些寄存器真正需要复位(控制通路、状态机),数据通路上的移位寄存器、流水线寄存器尽量不加。

通关标准:

学完本篇你应该能做到:

  1. 把一段一拍出结果的乘法(或 MAC)逻辑改写成移位-相加的复用结构,并算清它多花了几拍;
  2. 判断一个复用需求该用”自然数据流”还是”状态机调度”来实现;
  3. 拿到目标 FPGA 器件手册时,能查出 SRL16 / DSP / BRAM 分别支持哪种复位,并据此决定代码写法;
  4. 理解”复位到全 0”(reset)和”复位到全 1”(set)在硬件资源上的本质差别。