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

各位同学,欢迎来到第四章。这一章我们聊聊时序逻辑。说白了,就是带“记忆”的电路。

组合逻辑大家应该熟悉了,输入一变,输出立马跟着变。但实际工程里,哪有这么简单?信号要同步,数据要暂存,状态要跳转——这些都得靠时序逻辑。我个人习惯把时序逻辑比作“数字电路的心跳”,没有它,整个系统就是一盘散沙。

4.1 触发器:时序逻辑的最小单元

触发器,英文叫 Flip-Flop,简称 FF。它是时序逻辑里最基础、最核心的元件。FPGA 里用的最多的就是 D 触发器。

D 触发器有个特点:在时钟上升沿(或下降沿)到来时,它把输入 D 的值“拍”到输出 Q 上。其他时间,Q 保持不变。这就是“记忆”的本质。

我在项目中遇到过一个问题:一个同事写的代码里,信号赋值没用时钟沿,结果仿真时波形乱跳,板子跑起来完全不对。后来一查,就是没搞清楚组合逻辑和时序逻辑的区别。嗯,这里要特别注意。

核心要点:D 触发器的输出 Q 只在时钟沿变化,其他时间保持。这是所有时序逻辑的基础。

Verilog 里描述一个 D 触发器很简单:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

注意这里用的是 非阻塞赋值 <=,不是 =。为什么?因为时序逻辑里,多个赋值要同时发生,不能有先后顺序。你想想看,如果两个触发器级联,用阻塞赋值就会出问题。

4.2 寄存器:多个触发器打包

寄存器其实就是一组 D 触发器。8 位寄存器就是 8 个 D 触发器并排,共享同一个时钟和复位。说白了,就是“批量处理”。

FPGA 里到处都是寄存器。比如你要存一个 16 位的计数器值,那就得用 16 个触发器。我刚开始做设计时,总觉得寄存器就是“存数据”的,后来才发现,它还能做延迟、做同步、做流水线——功能多着呢。

举个例子,跨时钟域同步:

reg sig_a_d1;
reg sig_a_d2;

always @(posedge clk_b) begin
    sig_a_d1 <= sig_a;
    sig_a_d2 <= sig_a_d1;
end

这就是用两级寄存器做同步,消除亚稳态。我当年第一次做跨时钟域设计时,没加这玩意儿,结果板子偶尔跑飞,查了三天才找到原因。避坑指南:跨时钟域信号,一定要做同步处理。

4.3 计数器:时序逻辑的“节拍器”

计数器,说白了就是“数数”。但你别小看它,雷达信号处理里,计数器无处不在。脉冲积累、距离门控制、FFT 地址生成——哪样离得开计数器?

一个简单的 8 位计数器:

reg [7:0] cnt;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        cnt <= 8'd0;
    else if (cnt == 8'd255)
        cnt <= 8'd0;
    else
        cnt <= cnt + 1'b1;
end

这里有个细节:计数器清零的条件。我见过有人用 cnt == 8'd256 做清零,结果仿真时永远清不了零。为什么?因为 8 位计数器最大值是 255,256 根本不会出现。这种低级错误,我年轻时也犯过。

实战技巧:计数器清零时,建议用“等于最大值”作为条件,而不是“等于目标值+1”。这样更直观,也更容易维护。

在弹载雷达里,计数器经常用来产生各种控制信号。比如,每 1000 个时钟周期产生一个脉冲,用来触发 AD 采样。这种场景,计数器就是“节拍器”。

4.4 分频器:时钟的“减速器”

分频器,就是把高频时钟变成低频时钟。FPGA 里分频有两种方式:

  • 偶数分频:用计数器翻转即可
  • 奇数分频:需要两个计数器,一个上升沿触发,一个下降沿触发,然后组合

偶数分频最简单。比如 4 分频:

reg clk_div4;
reg [1:0] cnt;

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

注意,这里计数器计到 1 就翻转,不是计到 3。为什么?因为 4 分频需要 2 个时钟周期高电平、2 个时钟周期低电平,所以每 2 个时钟翻转一次。嗯,这个逻辑要理清楚。

警告:FPGA 内部尽量不要用分频产生的时钟作为全局时钟。因为分频时钟的占空比和相位可能不稳定,容易导致时序问题。建议用 PLL 或 MMCM 生成多频率时钟。

我记得有一次做项目,为了省资源,直接用分频器产生 10MHz 时钟给 ADC 用。结果 ADC 采样数据老是出错,查了半天才发现是分频时钟抖动太大。后来换成 PLL,问题就解决了。避坑指南:关键时钟,一定要用 PLL。

4.5 状态机:时序逻辑的“大脑”

状态机,英文叫 Finite State Machine,FSM。它是数字系统里最灵活、最强大的设计方法。说白了,就是“根据当前状态和输入,决定下一步做什么”。

状态机分两种:

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

我个人习惯用 Moore 型,因为输出稳定,不容易出现毛刺。但 Mealy 型响应更快,适合对延迟敏感的场景。

一个简单的状态机例子——雷达脉冲控制:

localparam IDLE    = 2'd0;
localparam TX      = 2'd1;
localparam RX      = 2'd2;
localparam WAIT    = 2'd3;

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
    case (state)
        IDLE:   next_state = start ? TX : IDLE;
        TX:     next_state = tx_done ? RX : TX;
        RX:     next_state = rx_done ? WAIT : RX;
        WAIT:   next_state = wait_done ? IDLE : WAIT;
        default: next_state = IDLE;
    endcase
end

// 输出逻辑
always @(*) begin
    tx_en = 1'b0;
    rx_en = 1'b0;
    case (state)
        TX:   tx_en = 1'b1;
        RX:   rx_en = 1'b1;
        default: ;
    endcase
end

这里用了三段式状态机。为什么用三段?因为组合逻辑和时序逻辑分开,代码清晰,综合结果也好。我见过有人把状态机写成一坨 always,结果状态跳转乱成一锅粥。避坑指南:状态机一定要用三段式,这是工程规范。

实战经验:状态机设计时,一定要考虑“非法状态”。如果状态机跑飞了,能不能回到正常状态?建议加一个 default 分支,回到初始状态。这叫“看门狗”思想。

在弹载雷达里,状态机用来控制整个信号处理流程:发射、接收、脉冲压缩、MTI、CFAR……每个阶段都是一个状态。状态机设计得好,整个系统就稳如泰山。

4.6 本章小结

这一章我们讲了时序逻辑的四个基本元件:

元件 作用 典型应用
触发器 存储 1 位数据 同步、延迟、寄存器
寄存器 存储多位数据 数据缓存、流水线
计数器 计数、定时 脉冲积累、地址生成
分频器 产生低频时钟 时钟生成(建议用 PLL)
状态机 控制逻辑 流程控制、协议解析

这些元件,说白了就是 FPGA 设计的“砖块”。你把这些砖块用好,就能搭出任何复杂的系统。我做了十几年雷达信号处理,回过头来看,最基础的东西往往最重要。

下一章,我们会讲同步设计与异步设计。到时候我会分享一个我当年踩过的坑——异步复位导致的数据丢失。嗯,那是个好故事。

课后练习:设计一个 8 位计数器,每计到 100 时产生一个脉冲,同时清零重新计数。用三段式状态机实现一个“发射-接收-处理”的雷达脉冲控制流程。

好了,这一章就到这里。有什么问题,欢迎在课程群里讨论。记住,FPGA 设计没有捷径,多写代码、多仿真、多调试,才是正道。