第四章:FPGA数字逻辑基础

各位同学,欢迎来到第四章。这一章,咱们要啃的是FPGA设计的“地基”——数字逻辑基础。说白了,就是Verilog/VHDL语法、组合逻辑、时序逻辑、状态机,还有同步异步设计。这些东西,你玩透了,后面写复杂系统才不慌。

我刚开始带项目那会儿,有个小伙子写了个状态机,仿真怎么跑都对,一上板子就乱跳。查了三天,最后发现是组合逻辑输出没寄存,毛刺直接送进了状态跳转逻辑。嗯,这种坑,咱们今天就得提前填上。

核心要点:本章所有内容,最终都指向一个目标——让你写的代码,在FPGA里跑得稳、跑得快、不莫名其妙。

4.1 Verilog/VHDL语法入门——别被语法吓住

很多新手一上来就背语法,其实没必要。你想想看,FPGA设计不是写软件,我们是在描述硬件。Verilog和VHDL,说白了就是硬件描述语言,不是编程语言。

我个人习惯用Verilog,因为它简洁。但VHDL在军工、航天领域用得更多,因为语法严谨。你选哪个?我建议:先精通一个,另一个能看懂就行。

这里我列几个最常用的语法结构,你记牢了,80%的代码都能写:

语法元素 Verilog示例 VHDL示例 说明
模块定义 module counter(...) entity counter is 每个设计的基本单元
输入输出 input clk, output reg [7:0] q port (clk: in std_logic; q: out std_logic_vector) 端口方向与类型
always/process always @(posedge clk) process(clk) 时序逻辑的敏感列表
赋值 assign a = b & c; a <= b and c; 组合逻辑用阻塞赋值
非阻塞赋值 q <= d; q <= d; 时序逻辑用非阻塞赋值

我的小技巧:写Verilog时,always @(*) 用于组合逻辑,always @(posedge clk) 用于时序逻辑。千万别混用敏感列表,否则综合出来一堆奇怪的锁存器。我曾经见过一个项目,就因为少写了一个敏感信号,综合出了200多个latch,功耗直接翻倍。

4.2 组合逻辑与时序逻辑——两个世界的分界线

组合逻辑,输出只取决于当前输入。时序逻辑,输出还取决于过去的状态。这个区别,你写代码时必须刻在脑子里。

为什么?因为组合逻辑容易产生毛刺。你想想看,信号从A传到B,路径长度不一样,到达时间就有先后。这中间就会产生短暂的错误电平——毛刺。时序逻辑用时钟沿采样,天然免疫毛刺。

我给大家画个图,看看组合逻辑和时序逻辑在FPGA里是怎么工作的:

组合逻辑 输出 = f(当前输入) 时序逻辑 输出 = f(当前输入, 过去状态) 时钟沿触发 时钟 clk ⚠ 组合逻辑的毛刺问题 路径延迟不同 → 短暂错误电平 时序逻辑通过时钟沿采样 → 消除毛刺 ✅ 推荐做法 组合逻辑输出加一级寄存器 关键路径用流水线打拍

看明白了吗?组合逻辑就像一条直路,信号过去就过去了。时序逻辑像有个检查站,时钟沿来了才放行。所以,我建议你:所有跨时钟域的信号,必须经过时序逻辑同步。这是铁律。

避坑指南:我曾经接手过一个射频控制模块,原工程师把组合逻辑的输出直接连到了状态机的跳转条件上。结果FPGA在高温下频繁误触发,查了两个月。最后发现是组合逻辑的毛刺在作怪。从那以后,我所有组合逻辑输出后面,必加一级D触发器。

4.3 有限状态机(FSM)设计——系统的“大脑”

FSM是FPGA设计的核心技能。你写通信协议、控制逻辑、数据处理,都离不开它。FSM分两种:Moore型和Mealy型。Moore型的输出只取决于当前状态,Mealy型的输出还取决于输入。

我个人偏好Moore型,因为输出稳定,没有毛刺。但Mealy型更灵活,响应更快。怎么选?看你的应用场景。

这里我给出一个经典的三段式FSM写法,这是业界标准:

// 三段式FSM示例 - 一个简单的序列检测器
// 检测 "101" 序列

module seq_detector (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       din,
    output reg        dout
);

    // 第一段:状态编码
    localparam IDLE = 2'b00,
               S1   = 2'b01,
               S10  = 2'b10,
               S101 = 2'b11;
    
    reg [1:0] state, next_state;
    
    // 第二段:状态转移(时序逻辑)
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            state <= IDLE;
        else
            state <= next_state;
    end
    
    // 第三段:次态逻辑(组合逻辑)
    always @(*) begin
        next_state = state;
        case (state)
            IDLE: next_state = (din) ? S1 : IDLE;
            S1:   next_state = (din) ? S1 : S10;
            S10:  next_state = (din) ? S101 : IDLE;
            S101: next_state = (din) ? S1 : S10;
            default: next_state = IDLE;
        endcase
    end
    
    // 输出逻辑(时序逻辑,避免毛刺)
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            dout <= 1'b0;
        else if (state == S101)
            dout <= 1'b1;
        else
            dout <= 1'b0;
    end

endmodule

我的经验:三段式FSM的好处是,状态转移、次态逻辑、输出逻辑完全分离。调试时,你只需要看state寄存器的值,就知道系统走到哪一步了。我曾经用这个方法,半天就定位了一个复杂的握手协议bug。另外,状态编码尽量用独热码(one-hot),虽然多用了寄存器,但译码逻辑简单,跑得快。

4.4 同步与异步设计——时钟域的艺术

同步设计,所有寄存器用同一个时钟沿。异步设计,存在多个时钟域。在FPGA里,我们尽量用同步设计,因为时序分析简单,不容易出问题。

但现实是,你的系统里往往有多个时钟:ADC采样时钟、DAC转换时钟、处理器接口时钟、射频本振时钟...这些时钟之间怎么通信?这就是跨时钟域(CDC)问题。

我给大家总结几个CDC处理的黄金法则:

  1. 单比特信号:用两级同步器(两个D触发器串联)
  2. 多比特信号:用异步FIFO(双口RAM + 读写指针同步)
  3. 控制信号:用握手协议(req/ack)
  4. 绝对禁止:组合逻辑输出直接跨时钟域

这里我画一个两级同步器的结构图,这是最常用的CDC方法:

时钟域A clk_a D Q 源寄存器 时钟域边界 时钟域B clk_b D Q 第一级 D Q 第二级 目标逻辑 clk_b 亚稳态概率降低到10^-12以下

血的教训:我曾经设计一个射频收发系统,ADC时钟是122.88MHz,DAC时钟是245.76MHz。我偷懒,直接用组合逻辑把ADC数据送给了DAC。结果呢?误码率高达10^-3,完全不能用。后来老老实实加了异步FIFO,误码率直接降到10^-12以下。记住:跨时钟域,永远不要相信组合逻辑

4.5 本章小结——你该带走什么

这一章内容不少,但核心就几点:

  • 语法是工具,不是目的。别在语法上钻牛角尖,多想想你描述的是什么硬件。
  • 组合逻辑和时序逻辑要分清楚。组合逻辑输出加寄存器,这是最便宜的保险。
  • FSM用三段式。状态转移、次态逻辑、输出逻辑分开写,调试时你会感谢我的。
  • 跨时钟域必须同步。单比特用两级同步器,多比特用异步FIFO,别偷懒。

嗯,这些内容,你写代码时每一条都用得上。下一章,我们会把这些基础应用到实际的射频接口设计中。但今天,先把这些基础打牢。

最后说一句:FPGA设计没有捷径。你踩过的每一个坑,都会变成你简历上的亮点。我当年调试那个异步FIFO,整整熬了三个通宵。但搞明白之后,后面所有跨时钟域问题,我都能半小时内搞定。所以,别怕犯错,怕的是错了不知道为什么。


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