这一篇在干嘛?

FPGA 设计越来越复杂,我们总希望能用”更接近人脑思考方式”的方式来描述电路,而不是纠缠在寄存器和门级细节里。本章讲三种被实践证明真正有效的抽象设计方法:图形化状态机、基于 MATLAB 的 DSP 设计、以及软硬件协同设计中的划分决策。

5.1 什么是真正有用的抽象

数学家莱布尼茨 17 世纪就说过:一个理论必须比它所描述的系统更简单,否则它就没有作为理论的价值。这句话放到工程上同样成立——如果一种新的”高层次设计方法”并不比现有方法更容易理解、更快出结果,那它就是伪抽象(作者称之为”横向抽象”,lateral abstraction),注定难以流行。

回顾历史:从原理图(schematic)设计迁移到 HDL(硬件描述语言,Hardware Description Language)设计是一场革命,因为 HDL 让设计者可以用行为级的方式描述模块,而且理论上与工艺无关。判断一种新技术是否值得学,标准很简单:它是否真的让你设计得更快、更不容易出错

本章讨论的三种技术都通过了这个检验:

  1. 图形化状态机:用画状态转移图的方式生成 RTL;
  2. DSP 设计:从 MATLAB 算法模型直接综合到 FPGA;
  3. 软硬件协同:在 C/软件和 HDL/硬件之间做正确的功能划分。

5.2 图形化状态机

为什么状态机适合图形化

回想大学里第一门逻辑设计课:设计状态机的标准流程就是先画状态转移图,再手工翻译成 HDL(或门电路)。状态图本来就是状态机最自然的抽象形式——它和我们脑子里的想象是一致的。

用 HDL 直接写状态机有几个痛点:

  • 编码方式太多:紧凑型设计有紧凑的编码(如二进制编码),高速设计有独热码(one-hot,每个状态用一个独立的触发器表示)等。选哪种往往取决于设计者偏好,也引入了人为出错和工具识别出错的可能。
  • 选型滞后:往往要等综合完才知道某种编码在你这个实现里是不是最优的。回头改编码很费时间,而且这项工作与状态机的功能毫无关系——纯粹的体力劳动。
  • 虽然大多数综合工具能自动识别状态机并按物理约束重新编码,但给综合工具最大的实现自由度通常才是最优策略。

一个例子:乘累加调度状态机

下面这个模块是低通 DSP 函数的调度状态机:它负责装载乘法器和累加器(乘法器、累加器本身在本模块外部)。代码前说明:这是一个典型的多状态时序调度器,每个状态完成一次乘数装载。

module shelflow(
    output reg    multstart,
    output reg [23:0] multdat,
    output reg [23:0] multcoeff,
    output reg    clearaccum,
    output reg [23:0] U0,
    input    CLK, RESET,
    input    [23:0] iData, // X[0]
    input    iWriteStrobe, // X[0] is valid
    input    [23:0] iALow, iCLow, // coeffs for low pass filter
    input    multdone,
    input    [23:0] accum);
// define input/output samples
reg    [23:0] X0, X1, U1;
// the registers that are multiplied together in mult24
reg    [2:0] state; // holds state for sequencing through mults
 
parameter    State0 = 0,
    State1 = 1,
    State2 = 2,
    State3 = 3;
 
always @(posedge CLK)
if(!RESET) begin
    X0    <= 0;
    X1    <= 0;
    U0    <= 0;
    U1    <= 0;
    multstart   <= 0;
    multdat   <= 0;
    multcoeff   <= 0;
    state    <= State0;
    clearaccum   <= 0;
end
else begin
// do not process state machine if multiply is not done
case(state)
State0: begin
    // idle state
    if(iWriteStrobe) begin
    // if a new sample has arrived
    // shift samples
    X0    <= iData;
    X1    <= X0;
    U1    <= U0;
    multdat   <= iData; // load mult
    multcoeff   <= iALow;
    multstart   <= 1;
    clearaccum   <= 1; // clear accum
    state    <= State1;
    end
    else begin
    multstart   <= 0;
    clearaccum   <= 0;
    end
end
State1: begin
    // A*X[0] is done, load A*X[1]
    if(multdone) begin
    multdat   <= X1;
    multcoeff   <= iALow;
    multstart <= 1;
    state <= State2;
    end
    else begin
    multstart <= 0;
    clearaccum <= 0;
    end
end
State2: begin
    // A*X[1] is done, load C*U[1]
    if(multdone) begin
    multdat <= U1;
    multcoeff <= iCLow;
    multstart <= 1;
    state <= State3;
    end
    else begin
    multstart <= 0;
    clearaccum <= 0;
    end
end
State3: begin
    // C*U[1] is done, load G*accum
    if(multdone) begin
    U0 <= accum;
    state <= State0;
    end
    else begin
    multstart <= 0;
    clearaccum <= 0;
    end
end
default
    state <= State0;
endcase
end
endmodule

代码点评:这个模块按 State0 → State1 → State2 → State3 循环,每一步等 multdone 有效后装载下一组乘数(multdat/multcoeff),并拉起 multstart 启动乘法。功能清晰,但四个状态的控制信号交织在一起,读代码时需要在脑中同时模拟”现在在哪个状态、哪些输出有效”,维护成本不低。

用图形化工具重画一遍

可以用 Xilinx 免费的状态机编辑器 State-CAD 把上述状态机”画”出来:

图5-1 图形化状态机设计(State-CAD 绘制的四状态转移图)

图 5-1:图形化状态机设计。一眼就能看出四个状态和转移条件,与我们在脑中想象状态机的方式完全一致,而且可以基于实现选项自动做速度/面积优化。

图形化表示的可读性远胜 Verilog 代码。但代价呢?工具生成的 RTL 几乎和网表一样难读。书中只展示了少量片段(Verilog-1995 格式导出的状态译码):

State Decoding:
if (~iWriteStrobe & STATE0 | multdone & STATE3 | RESET)
next_STATE0=1;
else next_STATE0=0;
 
if (~RESET & iWriteStrobe & STATE0 | ~RESET & ~multdone
& STATE1)
next_STATE1=1;
else next_STATE1=0;
 
if (~RESET & multdone & STATE1 | ~RESET & ~multdone & STATE2)
next_STATE2=1;
else next_STATE2=0;
 
if (~RESET & multdone & STATE2 | ~RESET & ~multdone & STATE3)
next_STATE3=1;
else next_STATE3=0;

下面是乘法器某输入的输出译码,可以直观感受一下”自动生成 RTL”的恐怖之处——原本一行 multcoeff <= iALow 展开成了庞大的与或逻辑:

multcoeff=( {24{STATE0}}&({24~RESET}}&({24~RESET}}&{24{iWRITESTROBE}})&({iALow23, iALow22, iALow21, iALow20,iALow19, iALow18, iALow17, iALow16, iALow15, iALow14,iALow13, iALow12, iALow11, iALow10, iALow9, iALow8,iALow7, iALow6, iALow5, iALow4, iALow3, iALow2, iALow1,iALow0})|({24{STATE0}}&{24~RESET}}&{24{~iWriteStrobe}})&('h0))|({24{STATE1}}&{24~RESET}}&{24~multdone}})&('h0))|({24{STATE1}}&({24~RESET}}&{24{multdone}})&({iALow23, iALow22, iALow21, iALow20,iALow19, iALow18, iALow17, iALow16, iALow15, iALow14,iALow13, iALow12, iALow11, iALow10, iALow9, iALow8,iALow7, iTalow6, iTalow5, iTalow4, iTalow3, iTalow2, iTalow1,iAlow0})|({24{STATE2}}&{24~RESET}{24~multdone}})&({iCLow23, iCLow22, iCLow21, iCLow20, iCLow19, iCLow18,iCLow17,iCLow16,iCLow15,iCLow14,iCLow13,iCLow12,iCLow11,iCLow10,iCLow9,iCLow8,iCLow7,iCLow6,iCLow5,iCLow4,iCLow3,iCLow2,iCLow1,iCLow0})|({24{STATE2}}&{24~RESET}}&{24~multdone}})&('h0))|({24{STATE3}}&{24~RESET}}&{24~multdone}})&('h0))|({24{STATE3}}&{24~RESET}}&{24~multdone}})&('h0))|({24{RESET}}}&('h0));

两种描述方式综合后的面积和性能基本相当,但 StateCad 版本可以按实现选项进一步针对性能或面积优化。

难读的自动生成 RTL 可以接受吗?

答案是:绝大多数情况下可以。回想我们从原理图迁移到 RTL 时,也曾放弃过对低层表示的掌控,把信任交给了工具,换来的是:更快地设计更复杂的电路、更抽象的设计层次、跨工艺可移植的源代码。我们仍然会分析部分门级设计来确认逻辑正确,但随着工具完善和经验积累,这种需求越来越少。

核心原则是两条:

  1. 我们可以分析并理解最高抽象层——这正是设计者描述设计的那一层;
  2. 我们有工具去验证较低抽象层是否按意图实现

最关键的是设计发生地的顶层抽象可读性;自动生成的 RTL 可读性则无关紧要。就像我们从来不可能看综合后的网表”看懂”设计意图一样,超 RTL 抽象的设计也只能回到设计者创建的那一层去理解。

顺带一提:至今仍有工程师抱着原理图方法不放、拒绝迁移到 RTL。对方法论过度舒适是一种危险的态度——它会让你作为工程师逐渐失效甚至被淘汰。

5.3 DSP 设计

DSP(Digital Signal Processing,数字信号处理)天生就是数学的,所以工程师早已在比 HDL 高得多的抽象层上工作:用 MATLAB 这类工具搭建算法、分析频响、相频特性、失真指标等等。

传统流程的断裂点在于:算法在 MATLAB 里验证完之后,设计者必须手工把算法转写成可综合的 RTL。高层次设计的使命就是把这个”桥”搭起来——直接从已有高层描述生成 FPGA 实现。

Synplicity 公司的 Synplify DSP 是这方面的代表工具。它作为 MATLAB 内的应用运行,与 MATLAB/Simulink 的建模能力紧密耦合。下图是一个基本 FIR 滤波器(Finite Impulse Response,有限冲激响应滤波器,数字滤波最常用的结构之一)的 Simulink 模型:

图5-2 FIR 滤波器的 Simulink 模型(Port In/Port Out 为 FPGA 接口,Sinewave 和 Scope 仅用于仿真)

图 5-2:FIR 滤波器的 Simulink 模型。其中 “Port In”/“Port Out” 块对应 FPGA 的真实 I/O,FIR 块是要综合到 FPGA 的 DSP 功能,Sinewave 和 Scope 块只参与 MATLAB/Simulink 仿真,最后用 FDA Tool 块对 FIR 做参数化:

图5-3 FIR 参数化界面(低通滤波器,归一化截止频率 0.1)

图 5-3:FIR 参数化。这里是一个低通 FIR,通带频率设为 0.1(相对采样频率归一化),任意特性改动都会实时反映在旁边的响应窗口里。参数定好后先跑 MATLAB/Simulink 仿真验证滤波器,然后 Synplify DSP 生成如下 Verilog:

// AUTO-GENERATED CODE FROM SYNPLIFY DSP
module FIR(clk, gReset, gEnable, rst, en, inp, outp);
parameter inpBitWidth = 16;
parameter inpFrac = 8;
parameter coefBitWidth = 10;
parameter coefFrac = 8;
parameter dpBitWidth = 17;
parameter dpFrac = 8;
parameter outBitWidth = 17;
parameter tapLen = 46;
parameter extraLatency = 0;
input clk;
input gReset;
input gEnable;
input rst;
input en;
input [inpBitWidth-1:0] inp;
output [outBitWidth-1:0] outp;
 
wire signed [coefBitWidth-1:0] CoefArr [0:tapLen + 0 - 1];
generate
begin: CoefArrGen
    assign CoefArr[0] = 10'b0000000000;
    ...
    assign CoefArr[45] = 10'b0000000000;
end
endgenerate
wire signed [inpBitWidth-1:0] multInp;
wire signed [coefBitWidth + inpBitWidth-1:0] multOuts
    [0:tapLen-1];
wire signed [coefBitWidth + inpBitWidth-1:0] multBufs1
    [0:tapLen-1];
wire signed [coefBitWidth + inpBitWidth-1:0] multBufs1pre
    [0:tapLen-1];
wire rstBuf;
wire enBuf;
reg signed [dpBitWidth-1:0] mem [0:tapLen-2];

核心运算部分如下,代码前说明:每个抽头做一次乘法并按小数位对齐右移,然后流水累加。

assign multInp = inp;
generate
    genvar i1;
    for(i1=0; i1<=tapLen-1; i1=i1+1)
    begin: multOuts_gen
    assign multOuts[i1] = multBufs1[i1];
    end
endgenerate
assign rstBuf = rst;
assign enBuf = en;
assign outp = multOuts[0] + mem[0];
generate
    genvar i2;
    for(i2=0; i2<=tapLen-1; i2=i2+1) begin: floop
    assign multBufs1pre[i2] = multInp * CoefArr[i2];
    assign multBufs1[i2] = multBufs1pre[i2] >>> (coefFrac+inpFrac-dpFrac);
    end
endgenerate
generate
    integer i3;
    begin: inner_floop
    always @(posedge clk) begin
    if((rstBuf==1) || (gReset==1)) begin
    for(i3 = 0; i3 <= tapLen-2; i3=i3+1) begin
    mem[i3] <= 0;
    end
    end // reset
    else if((enBuf==1) && (gEnable==1)) begin
    for(i3 = 0; i3 <= tapLen-3; i3=i3+1) begin
    mem[i3] <= mem[i3+1] + multOuts[i3+1];
    end
    mem[tapLen-2] <= multOuts[tapLen-1];
    end // enable
    end // always
    end // inner_floop
endgenerate
endmodule

代码点评:multBufs1pre[i2] = multInp * CoefArr[i2] 是每个抽头的乘法;>>> (coefFrac+inpFrac-dpFrac) 用算术右移来对齐定点小数位(输入 8 位小数 + 系数 8 位小数,乘积有 16 位小数,需移掉 8 位回到数据通路精度)——这就是工具替你处理的定点运算细节,手写时最容易错的地方。生成的架构是全流水版本,每个时钟沿都能送入一个新采样:

图5-4 自动生成的流水线 FIR 架构(每个抽头乘系数后加入对应流水级)

图 5-4:自动生成的流水线 FIR。输入采样与各系数相乘后加到流水线相应级,该拓扑对所有抽头一致。

全流水 vs 折叠:面积与吞吐的权衡

全流水实现结果如下:

资源/指标数值
Register(寄存器)806
LUT(查找表)828
Speed(速度)140 MHz

16 位数据通路下,FIR 可以处理 2.24 Gbps(2.24×10⁹ bits/s)的数据率——但代价是面积很大。很多 DSP 应用里每个采样周期有多个时钟(系统时钟高于采样频率),这时可以复用同一套 MAC(乘累加)硬件,用更紧凑的架构。

Synplify DSP 提供了 “folding”(折叠) 选项:把流水线折叠起来复用硬件资源。折叠倍数取决于每个采样周期内可用的时钟数,一般以最慢时钟周期与最快采样率的最坏比值作为最大折叠倍数的指导。例如上面的 FIR 定义为每采样周期 200 个时钟,一套 MAC 硬件就有足够时间算完 45 个抽头:

图5-5 面积优化的折叠 FIR 架构(ROM 依次送系数,输出移位寄存器流水累加)

图 5-5:折叠后的 FIR。45 阶滤波器的所有输出采样排在输出移位寄存器里;新采样到来时,ROM 开始逐个系数与输入相乘,移位寄存器同步移位累加;序列结束时所有输出样本已带着输入的缩放分量移到正确位置。

折叠版实现结果:

资源/指标数值
Registers(寄存器)938
LUT(查找表)249
Speed(速度)120 MHz

对比两张表:LUT 从 828 降到 249,总面积大幅缩减,代价是最大吞吐降为每 45 个时钟一个采样,即 42 Mbps(42×10⁶ bits/s)。关键在于:这类”流水线 vs 折叠”的架构权衡,在 Synplify DSP 里只是一个实现选项,算法模型一行都不用改。

5.4 软硬件协同设计

基于 C 语言的 FPGA 设计历来处于”有效抽象”的边界上。上世纪 90 年代,不少公司激进地推行 C 级综合来取代 HDL,甚至宣传能把符合 ANSI 标准的 C 代码直接转成可综合 HDL。问题在于:C 是顺序的、基于指令的语言,指令一条条执行;要转成可综合 HDL,就必须把指令序列转成状态机——对任何有规模的设计,产物都无法使用。

为了补救,这些公司发明了各种”基于周期(cycle-based)的 C 方言”,能顺利转成 HDL。但根本问题没变:这相当于新发明了一门 HDL,却并不比标准 HDL 更好用,还多了一道综合步骤。至于”周期 C 可以和其他 C 模型一起编译运行”的好处,对多数 HDL 设计者来说也并非刚需。所以 C 级设计至今没有成为 FPGA 主流。

C 级设计今天真正的价值是软硬件同环境联合仿真。即便如此,划清软硬件边界仍然靠人。设计者需要从以下五个维度判断一段功能放硬件还是放软件:

复杂度:需要递归、或迭代次数在执行前无法确定的算法,更适合软件。例如逐次逼近算法(通过监测误差函数决定何时停止,第 8 章会讲);又如浮点运算——如果边界条件预先明确,简单浮点可以硬件做,但要真正符合 IEEE 规范,浮点协处理器比专用硬件高效得多。

速度:需要极快执行的操作通常指向硬件。软件跑算法总有开销,还会被无关事件干扰延迟;硬件则可以针对高吞吐或低延迟做极致优化(见第 1 章)。

重复性:持续重复的任务(哪怕不复杂)也会拖慢处理器正常运转。比如持续扫描事件的函数、任何连续调制的输出,都适合卸载(off-load)到专用硬件。

实时精度:处理器按顺序执行指令,任务必须排队等待,高优先级事件一多就互相”踩踏”。如果某个功能必须精确到单个时钟周期,只有硬件能保证。

操作系统或用户界面:需要操作系统就得配(多半 32 位的)微处理器;哪怕只是通过常见总线格式做个简单用户界面,用一个带现成外设的嵌入式 8 位处理器也比从零设计(包括围绕外设 bit-banging)省事得多。

这些决策大多无法由机器自动完成,依赖设计者的工程判断。在可预见的未来,软硬件划分能力都会是系统工程师的必备技能。

常见坑:被"伪抽象"工具绑架

判断一个高层次工具是否值得投入,看它是否比现有方法真的更简单。警惕两类陷阱:一是”横向抽象”工具——换了个界面但抽象层级没变,还要多一道翻译步骤;二是自动生成的低层代码并不需要可读——如果你发现自己在手工修改工具生成的 RTL(而不是回到顶层抽象去改),说明工作流用反了,工具选型或用法有问题。

通关标准:

学完本篇你应该能做到:

  1. 说出判断”真抽象 vs 伪抽象”的标准(莱布尼茨准则),并解释为什么图形化状态机和 MATLAB→RTL 流程是真抽象,而 90 年代的 C 级综合不是;
  2. 解释图形化状态机的两大好处(可读性 + 自动速度/面积优化)以及”自动生成 RTL 难读但可接受”的两条依据;
  3. 读懂 Synplify DSP 生成的 FIR 代码中定点小数对齐(算术右移)的写法;
  4. 对同一个 FIR,用”每采样周期时钟数”推算流水线版和折叠版的吞吐(2.24 Gbps vs 42 Mbps)并说明取舍;
  5. 用复杂度、速度、重复性、实时精度、操作系统/UI 五个维度对一段功能做软硬件划分。