3. Verilog/VHDL基础回顾:组合逻辑与时序逻辑、状态机设计、同步与异步设计

各位同学,咱们今天聊点实在的。做高频交易系统,FPGA就是你的“肌肉”,而Verilog/VHDL就是指挥肌肉的“神经”。这块基础不牢,后面写出来的代码跑在交易线上,那就是定时炸弹。我见过太多人,上来就撸状态机,结果时序一塌糊涂,最后板子跑起来数据全错。

今天这一讲,我带你快速过一遍三个核心概念:组合逻辑与时序逻辑状态机设计同步与异步设计。这些都是高频交易系统里天天要用的东西。

3.1 组合逻辑 vs 时序逻辑

说白了,组合逻辑就是“输入一变,输出立马变”。没有记忆功能,像一根直通的管子。时序逻辑呢?它靠时钟沿触发,有记忆,能存状态。

我在做行情解析模块时,经常遇到新手把组合逻辑和时序逻辑混着写。比如用组合逻辑去驱动一个需要时钟同步的FIFO,结果跑仿真没问题,上板子就丢数据。为什么?因为组合逻辑的毛刺直接灌进了时钟域。

核心区别一句话:

  • 组合逻辑:输出 = f(当前输入)
  • 时序逻辑:输出 = f(当前输入 + 之前状态)

看个简单的Verilog例子:

// 组合逻辑:一个简单的加法器
assign sum = a + b;

// 时序逻辑:一个带使能的寄存器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 0;
    else if (en)
        q <= d;
end

嗯,这里要注意。组合逻辑里用 always @(*) 或者 assign,时序逻辑里用 always @(posedge clk)。千万别搞混了。我见过有人把组合逻辑的敏感列表写成 always @(a or b),漏了一个信号,仿真结果对,综合出来完全不对。这种坑,踩一次就够了。

3.2 状态机设计:高频交易的核心骨架

状态机,说白了就是你的“大脑”。在期货高频交易里,一个订单的生命周期就是一台状态机:空闲→发单→等待成交→部分成交→全部成交→撤单→错误。每一步都不能乱。

我个人习惯用三段式状态机。为什么?因为可读性强,时序好收敛,调试方便。你想想看,如果所有逻辑都塞在一个always块里,出问题了你怎么定位?

三段式状态机的结构:

  • 第一段:时序逻辑,描述状态跳转(当前状态→下一状态)
  • 第二段:组合逻辑,描述跳转条件(下一状态 = f(当前状态, 输入))
  • 第三段:时序逻辑,描述输出(输出 = f(当前状态) 或 f(当前状态, 输入))

看个例子,一个简单的交易状态机:

// 三段式状态机示例
// 第一段:状态跳转
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        current_state <= IDLE;
    else
        current_state <= next_state;
end

// 第二段:下一状态逻辑(组合逻辑)
always @(*) begin
    case (current_state)
        IDLE: begin
            if (start_trade)
                next_state = SEND_ORDER;
            else
                next_state = IDLE;
        end
        SEND_ORDER: begin
            if (order_acked)
                next_state = WAIT_FILL;
            else if (timeout)
                next_state = ERROR;
            else
                next_state = SEND_ORDER;
        end
        WAIT_FILL: begin
            if (filled)
                next_state = DONE;
            else if (cancel_req)
                next_state = CANCEL;
            else
                next_state = WAIT_FILL;
        end
        // ... 其他状态
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑(时序逻辑,避免毛刺)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        send_order <= 0;
        cancel_order <= 0;
    end else begin
        case (current_state)
            IDLE: begin
                send_order <= 0;
                cancel_order <= 0;
            end
            SEND_ORDER: begin
                send_order <= 1;
                cancel_order <= 0;
            end
            CANCEL: begin
                send_order <= 0;
                cancel_order <= 1;
            end
            default: begin
                send_order <= 0;
                cancel_order <= 0;
            end
        endcase
    end
end

我的经验:第三段输出用时序逻辑,可以避免组合逻辑的毛刺直接驱动外部模块。比如你驱动一个DMA引擎,毛刺可能导致误触发。我曾经因为这个原因,在回测系统里丢了一整天的行情数据,排查了三天才找到问题。

3.3 同步设计与异步设计

同步设计,就是所有寄存器都在同一个时钟域里干活。异步设计,就是跨时钟域通信。在高频交易系统里,你会有多个时钟域:比如行情接收模块跑200MHz,订单管理模块跑156.25MHz,PCIe接口跑250MHz。这些模块之间怎么安全地传数据?

我建议你:能用同步设计,就别用异步设计。同步设计简单、可靠、时序容易收敛。但现实是,你不得不处理跨时钟域问题。

常见的异步处理方法:

  • 单比特信号:用两级同步器(打两拍)
  • 多比特信号:用异步FIFO
  • 控制信号:用握手协议

看一个两级同步器的例子:

// 两级同步器,用于跨时钟域单比特信号
always @(posedge clk_dst or negedge rst_n) begin
    if (!rst_n) begin
        sync_reg1 <= 0;
        sync_reg2 <= 0;
    end else begin
        sync_reg1 <= async_signal;
        sync_reg2 <= sync_reg1;
    end
end

assign synced_signal = sync_reg2;

注意:两级同步器只能解决亚稳态问题,不能保证信号“不漏采”。如果你的异步信号宽度小于目标时钟周期,它可能被完全错过。我在做行情快照同步时,就遇到过这种问题——一个脉冲信号在跨时钟域时消失了,导致行情快照丢失。解决方案是:先把脉冲展宽,再同步。

异步FIFO的设计就更复杂了。你需要处理空满标志的跨时钟域传递,通常用格雷码来减少多比特变化带来的风险。格雷码每次只变化1比特,同步时出错概率大大降低。

3.4 知识体系总览

下面这张图,是我自己总结的本章知识体系。你可以把它当作一个“地图”,学完这一章后,对照着检查自己是否都掌握了。

Verilog/VHDL基础回顾:知识体系 组合逻辑 vs 时序逻辑 状态机设计 同步与异步设计 组合逻辑:assign / always @(*) 时序逻辑:always @(posedge clk) 阻塞赋值(=) vs 非阻塞赋值(<=) 一段式:所有逻辑混在一起 二段式:状态跳转 + 输出 三段式:状态跳转 + 下一状态 + 输出 摩尔型 vs 米利型 同步设计:单时钟域 异步设计:跨时钟域 两级同步器(单比特) 异步FIFO(多比特) 握手协议(控制信号) 核心原则:同步优先,异步谨慎,状态机用三段式

3.5 避坑指南

最后,我把自己踩过的坑总结一下,希望能帮你省点时间:

  • 组合逻辑环路:写组合逻辑时,千万别让输出反馈回输入,形成组合环路。综合工具会报warning,但很多人直接忽略了。结果就是仿真跑得欢,上板子就震荡。
  • 状态机缺省状态:case语句一定要写default,否则综合出来会生成锁存器。锁存器在FPGA里是“毒药”,时序难收敛,功耗还大。
  • 异步复位同步释放:复位信号如果是异步的,一定要做同步释放处理。否则复位撤销时,不同寄存器可能在不同时钟沿退出复位,导致状态混乱。
  • 跨时钟域不要用组合逻辑:异步信号必须先同步,再进组合逻辑。否则组合逻辑的毛刺会直接传播到目标时钟域。

一个小技巧:写代码之前,先在纸上画出状态转移图。把每个状态、每个跳转条件、每个输出都标清楚。我每次做交易系统的新模块,都会花半小时画图。看起来慢,实际上后面调试能省几倍的时间。

好了,这一讲的内容就到这里。记住,FPGA设计没有捷径,但可以少走弯路。把组合逻辑、时序逻辑、状态机、同步异步这些基本功练扎实了,后面写交易系统才会得心应手。


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