4、时序逻辑设计:触发器、寄存器、计数器、分频器、状态机基础

说实话,时序逻辑是FPGA设计的灵魂。组合逻辑只管当前输入,但时序逻辑能记住过去的状态。你想想看,交易所的协议解析,哪一步不需要记住前面收到了几个字节、当前在哪个状态?没有时序逻辑,一切都白搭。

我个人习惯把时序逻辑分成几个层次来理解。最底层是触发器,往上堆成寄存器,再往上就是计数器、分频器这些功能模块,最后是状态机这个集大成者。咱们一层层剥开来看。

4.1 触发器——时序逻辑的细胞

触发器,说白了就是能存1位数据的单元。FPGA里最常用的是D触发器。它的核心行为很简单:每个时钟上升沿,把输入D的值锁存到输出Q上。

我在项目中遇到过一个问题:两个模块之间跨时钟域传数据,直接连D触发器就出错了。为什么?因为D触发器对建立时间和保持时间有要求。信号变化太快,触发器就进入亚稳态,输出既不是0也不是1,像个墙头草一样摇摆不定。

// 一个标准的D触发器描述
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

注意这里的 non-blocking assignment (<=)。我刚开始学的时候老用阻塞赋值 =,结果仿真波形一塌糊涂。记住:时序逻辑用非阻塞赋值,这是铁律。

避坑指南:我曾经在同一个always块里混用阻塞和非阻塞赋值,综合出来的电路和仿真完全对不上。花了整整两天才定位到问题。从此以后,我坚持一个原则——时序逻辑只用非阻塞赋值,组合逻辑只用阻塞赋值,绝不混用。

4.2 寄存器——多位触发器的集合

寄存器就是多个D触发器并排站。8位寄存器就是8个触发器,16位就是16个。在FPGA里,寄存器是分布在各处的,不像ASIC那样有专门的寄存器阵列。

交易所协议解析里,寄存器用得最多的场景是:缓存一个数据包、保存当前解析到的偏移量、记录校验和累加值。比如解析UDP包时,我需要一个16位寄存器来暂存源端口号。

reg [15:0] src_port;  // 16位寄存器

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        src_port <= 16'd0;
    else if (data_valid && (byte_cnt == 0))
        src_port[15:8] <= data_in;  // 高字节
    else if (data_valid && (byte_cnt == 1))
        src_port[7:0] <= data_in;   // 低字节
end

你想想看,如果不用寄存器,你怎么记住第一个字节是什么?组合逻辑可没有记忆能力。

4.3 计数器——时序逻辑的节拍器

计数器本质上就是寄存器加加法器。每个时钟周期加1,加到某个值就归零。就这么简单,但用处太大了。

我做过一个项目,需要精确测量两个脉冲之间的时间差。用了一个32位计数器,时钟是200MHz,精度5纳秒。结果测出来和示波器对得上,心里那个爽。

reg [7:0] cnt;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        cnt <= 8'd0;
    else if (cnt == 8'd99)  // 计数到99归零
        cnt <= 8'd0;
    else
        cnt <= cnt + 1'b1;
end

经验之谈:计数器宽度要留够余量。我曾经算出来最大需要1000,就用了10位计数器(最大1023)。结果需求变更,需要计到1200,只能改代码重新综合。现在我的习惯是:多留2位,反正FPGA里寄存器多的是。

4.4 分频器——从高频到低频

分频器就是特殊的计数器。FPGA开发板上通常只有一个晶振,比如50MHz。但你的设计可能需要25MHz、10MHz、1Hz等各种时钟。怎么办?分频。

偶数分频最简单:计数器计到N/2-1就翻转输出。比如50MHz分频到25MHz,计到1就翻转。

reg clk_div2;
reg [1:0] cnt;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt <= 2'd0;
        clk_div2 <= 1'b0;
    end else begin
        cnt <= cnt + 1'b1;
        if (cnt == 2'd1) begin
            clk_div2 <= ~clk_div2;
            cnt <= 2'd0;
        end
    end
end

奇数分频稍微麻烦点,需要同时用上升沿和下降沿触发。我建议新手直接用PLL(锁相环)做分频,Xilinx和Altera都有现成的IP核,参数一配就行,比自己写的分频器稳定得多。

核心要点:分频得到的时钟,和原始时钟不是同源的。跨时钟域时一定要做同步处理。我见过有人直接把分频时钟当全局时钟用,结果时序收敛不了,整个项目重做。

4.5 状态机——时序逻辑的指挥官

状态机是时序逻辑的集大成者。它把组合逻辑和时序逻辑完美结合,用来控制复杂的流程。交易所协议解析,本质上就是一个巨大的状态机。

我个人习惯用三段式状态机。为什么?因为可读性强,维护方便,综合出来的电路也清晰。

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

// 第二段:次态逻辑(组合逻辑)
always @(*) begin
    case (state)
        IDLE:   next_state = start ? HEADER : IDLE;
        HEADER: next_state = header_done ? PAYLOAD : HEADER;
        PAYLOAD:next_state = payload_done ? CHECKSUM : PAYLOAD;
        CHECKSUM:next_state = IDLE;
        default:next_state = IDLE;
    endcase
end

// 第三段:输出逻辑(组合逻辑)
always @(*) begin
    case (state)
        IDLE:    data_valid = 1'b0;
        HEADER:  data_valid = 1'b0;
        PAYLOAD: data_valid = 1'b1;
        CHECKSUM:data_valid = 1'b0;
        default: data_valid = 1'b0;
    endcase
end

我在解析金融交易协议时,状态机通常有十几个状态:等待同步头、解析消息类型、解析消息长度、解析消息体、校验和验证等等。每个状态对应一个处理阶段,清晰得很。

避坑指南:我曾经写过一个两段式状态机,把次态逻辑和输出逻辑混在一起。结果后来要加一个新状态,改了一处忘了改另一处,仿真通过但上板就跑飞。从那以后,我坚持三段式,虽然代码多几行,但心里踏实。

4.6 本章知识体系

下面这张图是我画的时序逻辑知识体系,你看一眼就能明白各个概念之间的关系。

时序逻辑知识体系 时序逻辑 触发器 寄存器 计数器 分频器 组成 +加法器 特殊 D触发器 建立/保持时间 亚稳态 多位存储 数据缓存 流水线 累加计数 定时/延时 地址生成 偶数/奇数分频 PLL替代方案 跨时钟域 状态机(FSM) 三段式:状态转移 + 次态逻辑 + 输出逻辑 典型应用场景 协议解析 | 数据包处理 | 时序控制 | 接口协议

从这张图可以看得很清楚:触发器是最基本的单元,多个触发器组成寄存器,寄存器加上加法器就是计数器,计数器的一种特殊用法就是分频器。而状态机,则是把这些东西全部整合起来,形成完整的控制逻辑。

嗯,时序逻辑这块内容就这些。说白了就是记住过去、控制未来。你在做交易所协议解析时,每个数据包的解析过程,本质上都是在和时序逻辑打交道。把触发器、寄存器、计数器、分频器、状态机这五样东西吃透了,FPGA设计的大门就算真正迈进去了。

总结一下:时序逻辑的核心是「记忆」。触发器记1位,寄存器记多位,计数器记数值,分频器记周期,状态机记状态。层层递进,缺一不可。


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