3. FPGA数字设计基础:Verilog/VHDL基础回顾、组合逻辑与时序逻辑、流水线设计思想、同步复位与异步复位

好,咱们正式开始FPGA数字设计的基础部分。说实话,这部分内容很多教材讲得又臭又长,我尽量挑干货说。你想想看,做RISC-V浮点运算单元,本质上就是在FPGA里搭一堆数字电路。基础不牢,后面写浮点乘加器的时候肯定要踩坑。

3.1 Verilog/VHDL基础回顾——我选Verilog

先聊语言。VHDL和Verilog都能用,但我个人习惯用Verilog。为什么?因为RISC-V社区的开源项目,十有八九都是Verilog写的。你去看SweRV-EH、Rocket Chip、VexRiscv,清一色Verilog/SystemVerilog。VHDL当然也行,但生态上吃亏。

Verilog的核心就三样东西:模块连线过程块。我见过不少新手把Verilog当C语言写,结果综合出来一堆莫名其妙的锁存器。记住一条铁律:硬件描述语言描述的是硬件,不是软件

核心要点:写Verilog时,脑子里要有电路图。每写一行代码,你都得知道它对应什么门、什么触发器。

来个最简单的D触发器例子:

module d_flip_flop (
    input  wire clk,
    input  wire rst_n,
    input  wire d,
    output reg  q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 1'b0;
        else
            q <= d;
    end
endmodule

这段代码,敏感列表里写了posedge clknegedge rst_n,综合出来就是带异步复位的D触发器。你要是把rst_n从敏感列表里删掉,综合出来就是同步复位——电路结构完全不一样。我在项目中遇到过有人把异步复位写成了同步复位,结果复位信号毛刺导致整个系统跑飞,查了两天才找到原因。

3.2 组合逻辑与时序逻辑——本质区别

这个区分太重要了。我直接说结论:

  • 组合逻辑:输出只取决于当前输入,没有记忆功能。比如加法器、多路选择器、译码器。
  • 时序逻辑:输出不仅取决于当前输入,还取决于历史状态。比如计数器、状态机、寄存器。

在FPGA里,组合逻辑用LUT(查找表)实现,时序逻辑用触发器实现。你写assign或者always @(*),综合出来就是组合逻辑。你写always @(posedge clk),综合出来就是时序逻辑。

个人经验:我建议初学者养成一个习惯——写代码前先画个简单的时序图。哪怕在草稿纸上画几个波形,都能帮你理清信号之间的时序关系。我在做浮点加法器的时候,光时序图就画了七八张。

举个组合逻辑的例子——4位加法器:

module adder_4bit (
    input  [3:0] a,
    input  [3:0] b,
    output [4:0] sum
);
    assign sum = a + b;
endmodule

这个assign语句,综合出来就是一组LUT构成的加法器。没有时钟,没有复位,输入变了输出立刻变(当然有门延迟)。

再举个时序逻辑的例子——4位计数器:

module counter_4bit (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [3:0] cnt
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 4'd0;
        else
            cnt <= cnt + 1'b1;
    end
endmodule

这个计数器,每个时钟上升沿加1。复位时清零。综合出来就是4个触发器加一个加法器。

3.3 流水线设计思想——用面积换速度

流水线是FPGA设计的核心思想之一。说白了,就是把一个大的组合逻辑路径切成几段,每段中间插一级寄存器。这样做的目的是缩短关键路径,提高时钟频率。

为什么会这样?因为组合逻辑的延迟跟逻辑深度成正比。一个64位浮点加法器,如果不用流水线,组合逻辑路径可能长达几十级LUT,延迟可能到十几纳秒。但如果你切成4级流水线,每级只有几级LUT,延迟降到几纳秒,时钟频率就能翻倍。

我画个图帮你理解:

非流水线设计: 组合逻辑 (延迟大) 寄存器 时钟周期 = 组合逻辑延迟 + 寄存器建立时间 3级流水线设计: 逻辑1 REG 逻辑2 REG 逻辑3 REG 时钟周期 = max(逻辑1延迟, 逻辑2延迟, 逻辑3延迟) + 寄存器建立时间 时钟频率可以大幅提升!

看到没?非流水线设计里,组合逻辑一大坨,时钟周期被拖得很慢。流水线设计把逻辑切成了三段,每段都小,时钟周期就短了。

注意:流水线不是免费的午餐。它引入了延迟(latency)。3级流水线意味着从输入到输出需要3个时钟周期。在RISC-V浮点单元里,我们通常用流水线来提升吞吐量,但代价是增加了指令的等待时间。这是一个经典的trade-off。

我曾经在做一个浮点乘加器的时候,一开始用了6级流水线,频率跑到了500MHz,但延迟太大,导致分支预测错误时惩罚周期太多。后来改成4级流水线,频率降到400MHz,但整体性能反而提升了。所以流水线级数不是越多越好,得根据实际需求来定。

3.4 同步复位与异步复位——选哪个?

这个问题,每次面试我都喜欢问。答案其实不复杂,但很多人搞混。

特性 同步复位 异步复位
触发条件 仅在时钟沿有效时响应复位 复位信号变化立即响应,与时钟无关
优点 抗毛刺能力强,时序分析简单 复位及时,不依赖时钟
缺点 需要时钟才能复位,可能错过复位 对毛刺敏感,可能引起亚稳态
FPGA实现 使用LUT+触发器实现 直接使用触发器的CLR/PR引脚
推荐场景 大部分控制逻辑 上电初始化、紧急复位

我个人习惯是:能用异步复位就用异步复位。为什么?因为FPGA的触发器本身就带异步复位引脚(CLR),用同步复位反而要多消耗LUT资源。但异步复位有个大坑——复位释放必须同步

避坑指南:我曾经在一个项目里直接用外部按键作为异步复位信号,结果复位释放时刚好在时钟沿附近,导致系统进入亚稳态,整个状态机乱跳。后来加了一个两级同步器,问题就解决了。

正确的异步复位设计应该是这样的:

module rst_sync (
    input  wire clk,
    input  wire rst_async_n,
    output wire rst_sync_n
);
    reg [1:0] rst_meta;
    always @(posedge clk or negedge rst_async_n) begin
        if (!rst_async_n)
            rst_meta <= 2'b00;
        else
            rst_meta <= {rst_meta[0], 1'b1};
    end
    assign rst_sync_n = rst_meta[1];
endmodule

这个电路叫复位同步器。它把异步复位信号用两级触发器同步到时钟域,既保留了异步复位的快速响应,又避免了亚稳态。我在所有FPGA项目里都会加这个模块,算是标准配置了。

3.5 小结——打好基础再上路

嗯,这一章的内容就这些。说白了就是三件事:Verilog怎么写组合和时序怎么分流水线和复位怎么用。这些是FPGA设计的基石,后面做浮点运算单元的时候,每一步都会用到。

你想想看,浮点加法器里的对阶操作,就是组合逻辑;尾数相加后的规格化,也是组合逻辑;但中间插的寄存器,就是时序逻辑。整个浮点单元,其实就是一堆组合逻辑块和时序逻辑块拼起来的流水线结构。

下一章我们会深入RISC-V的浮点指令集架构,看看那些fadd.sfmul.s到底是怎么定义的。在那之前,建议你把今天讲的代码都跑一遍仿真,亲手看看波形。纸上得来终觉浅,绝知此事要躬行。


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