第四章:时序逻辑与状态机——让FPGA拥有“记忆”和“思维”

各位同学,欢迎来到第四章。前面我们聊了不少组合逻辑,说白了就是“输入一变,输出立马跟着变”。但现实世界哪有这么简单?你想想看,一个交易系统判断“是否要下单”,它得记住上一笔订单的状态、当前的价格、还有时间戳。这些“记忆”从哪来?

答案就是——时序逻辑。这一章,我们来啃这块硬骨头。

4.1 触发器与寄存器:FPGA的“记忆细胞”

时序逻辑的核心元件,就是触发器(Flip-Flop)。在FPGA里,我们最常用的是D触发器。它的行为很简单:在每个时钟上升沿,把输入D的值“拍”到输出Q上

我刚开始学的时候,总觉得这玩意儿不就是个“延迟一拍”吗?没错,但这一拍,就是时序逻辑的根基。

多个D触发器并排放在一起,共享同一个时钟,就构成了寄存器。比如一个32位的寄存器,就是32个D触发器排排坐。

看个最简单的例子——一个带同步复位的D触发器:

always @(posedge clk) begin
    if (rst) begin
        q <= 1'b0;
    end else begin
        q <= d;
    end
end

这里有个细节:<=是非阻塞赋值。在时序逻辑里,永远用非阻塞赋值。这是无数前辈用血泪换来的教训。我曾经在项目里混用了阻塞和非阻塞,结果仿真怎么都对,上板子就乱跳。查了三天,最后发现是赋值方式的问题。

警告: 时序逻辑中,请始终使用非阻塞赋值(<=)。组合逻辑中,请始终使用阻塞赋值(=)。混用会导致仿真与综合结果不一致,这是新手最容易踩的坑。

4.2 同步复位 vs 异步复位:一个关于“安全感”的选择

复位,就是把电路恢复到初始状态。FPGA里主要有两种复位方式:同步复位和异步复位。

特性 同步复位 异步复位
复位条件 时钟沿 + 复位信号有效 复位信号有效(立即)
优点 抗毛刺能力强,时序分析简单 响应快,不需要时钟也能复位
缺点 需要时钟存在才能复位 容易受毛刺影响,存在亚稳态风险
代码风格 if (rst) 在 always 内部 always @(posedge clk or negedge rst_n)

我个人习惯,在高速交易系统里,尽量用同步复位。为什么?因为异步复位的释放时间如果离时钟沿太近,会产生亚稳态。高频交易里,一个亚稳态可能就意味着几百万的损失。

但有些场景,比如上电初始化,异步复位确实更方便。我的建议是:外部引脚用异步复位,内部逻辑全部转成同步复位。具体做法就是用一个两级寄存器打拍,把异步信号同步化。

小技巧: 如果你非要用异步复位,记得加一个“异步复位同步释放”电路。说白了就是:复位信号进来先过两级寄存器,再送给内部逻辑。这样既保留了异步复位的快速响应,又避免了亚稳态。

4.3 有限状态机:让电路“按剧本走”

状态机,是时序逻辑的“大脑”。它让电路不再是简单的“输入→输出”,而是有了“状态”这个概念。你想想看,一个TCP连接有CLOSED、SYN_SENT、ESTABLISHED等状态,这不就是状态机吗?

FSM分为两类:

  • Moore型:输出只取决于当前状态
  • Mealy型:输出取决于当前状态和输入

在高频交易中,我更喜欢用Moore型。为什么?因为它的输出更稳定,不会因为输入毛刺而抖动。你总不希望交易状态机因为一个毛刺,从“等待”直接跳到“已成交”吧?

下面是一个简单的状态机示例——一个检测“101”序列的检测器:

// 状态编码
localparam IDLE     = 2'b00;
localparam S1       = 2'b01;
localparam S10      = 2'b10;
localparam S101     = 2'b11;

reg [1:0] current_state, next_state;

// 第一段:状态转移
always @(posedge clk or posedge rst) begin
    if (rst)
        current_state <= IDLE;
    else
        current_state <= next_state;
end

// 第二段:次态逻辑
always @(*) begin
    case (current_state)
        IDLE:   next_state = (data_in) ? S1 : IDLE;
        S1:     next_state = (data_in) ? S1 : S10;
        S10:    next_state = (data_in) ? S101 : IDLE;
        S101:   next_state = (data_in) ? S1 : S10;
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑
assign detected = (current_state == S101);

4.4 三段式状态机:工程界的“标准答案”

上面这个写法,就是经典的三段式状态机。我强烈建议你,从一开始就养成这个习惯。

三段式把状态机拆成三个部分:

  1. 第一段:时序逻辑,描述状态寄存器——就是那个 current_state <= next_state
  2. 第二段:组合逻辑,描述次态转移条件——用 case 根据当前状态和输入,算出下一个状态
  3. 第三段:输出逻辑——可以是组合逻辑,也可以是时序逻辑

为什么我推崇三段式?因为它在工程上最清晰、最不容易出错。我曾经接手过一个项目,前任用了一段式状态机,所有逻辑揉在一个always块里。那代码,说实话,我看了一周才敢改。

三段式的另一个好处是:便于维护和调试。如果状态跳转错了,你只需要看第二段;如果输出不对,你只需要看第三段。各司其职,互不干扰。

核心要点: 三段式状态机的精髓在于“分离关注点”。状态寄存器、次态逻辑、输出逻辑,三者独立编写。这是工业级代码的标配,也是你从“学生代码”走向“工程代码”的第一步。

知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

时序逻辑与状态机知识体系 触发器与寄存器 复位策略 有限状态机 D触发器 · 非阻塞赋值 寄存器组 · 时钟沿采样 同步复位 · 异步复位 亚稳态 · 同步释放 Moore型 · Mealy型 状态编码 · 状态转移 三段式状态机写法 第一段:状态寄存器 第二段:次态逻辑 第三段:输出逻辑 核心思想:分离关注点 · 时序与组合分离 · 可读性与可维护性

这张图把本章的知识点串起来了。你从最底层的触发器开始,到复位策略的选择,再到状态机的设计,最后汇聚到三段式写法。每一步都是上一层的基石。

好了,这一章的内容就到这里。记住:时序逻辑是FPGA设计的灵魂,状态机是时序逻辑的骨架。把这两样东西吃透了,你就能写出稳定、高效、可维护的硬件代码。

课后思考: 为什么在高频交易系统中,我们更倾向于使用Moore型状态机而不是Mealy型?如果你能回答这个问题,说明你已经理解了状态机的精髓。

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