第四章:时序逻辑设计——触发器、寄存器、计数器、分频器、时钟与复位

各位同学,欢迎来到第四章。前面我们聊了很多组合逻辑,说白了就是“输入一变,输出立马跟着变”。但真实的数字系统不是这样的——你得有个“节拍”,让数据在正确的时间点被锁存、传递、处理。这就是时序逻辑的活儿了。

我个人觉得,时序逻辑是FPGA设计的灵魂。你想想看,没有时序逻辑,你的电路就是一锅乱炖,信号到处乱窜。有了时钟和触发器,一切才变得井井有条。

4.1 触发器:时序逻辑的基本单元

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

D触发器长什么样?很简单:一个数据输入D,一个时钟输入CLK,一个输出Q。在时钟的上升沿(或者下降沿),D端的数据被“抓”到Q端,并保持住,直到下一个时钟沿到来。

我在项目中遇到过一个问题:有个同事把触发器的敏感列表写成了电平敏感,结果仿真死活不对。记住,触发器是边沿敏感的,不是电平敏感的。

// 一个简单的D触发器,Verilog描述
module d_flip_flop (
    input  wire clk,
    input  wire rst_n,
    input  wire d,
    output reg  q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 1'b0;
        else
            q <= d;
    end
endmodule

嗯,这里要注意:复位信号rst_n是低电平有效,而且我习惯用异步复位。为什么?因为上电瞬间,你希望寄存器能立刻进入一个已知状态,而不是等时钟来了才复位。这在很多实际场景下是必须的。

4.2 寄存器:多个触发器排排坐

一个触发器只能存1位数据。那要存8位、16位、32位呢?简单,把多个D触发器并排放,共享同一个时钟和复位,就成了寄存器。

说白了,寄存器就是一组触发器。在FPGA里,你写 reg [7:0] data_reg;,综合工具就会给你生成8个D触发器。

我建议初学者养成一个好习惯:所有跨时钟域的信号,一定要用寄存器打两拍。我曾经因为偷懒,只打了一拍,结果在高速采集系统里抓到了亚稳态,数据偶尔会跳变。排查了整整两天才找到原因。

// 8位寄存器示例
reg [7:0] data_reg;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        data_reg <= 8'b0;
    else
        data_reg <= data_in;
end

4.3 计数器:时序逻辑的经典应用

有了寄存器和加法器,就能做计数器了。计数器在FPGA里无处不在:分频、定时、地址生成、状态机跳转……

最简单的计数器就是每个时钟加1,加到最大值后回零。比如一个4位计数器,从0数到15,然后回到0。

// 4位二进制计数器
module counter_4bit (
    input  wire        clk,
    input  wire        rst_n,
    output reg  [3:0]  cnt
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 4'b0;
        else
            cnt <= cnt + 1'b1;
    end
endmodule

你可能会问:为什么用 cnt + 1'b1 而不是 cnt + 1?因为 1'b1 明确告诉综合工具这是一个1位宽度的常数,避免产生不必要的进位逻辑。这是个小细节,但能帮你省下不少LUT资源。

4.4 分频器:从高频到低频

FPGA开发板上的晶振通常是50MHz或100MHz。但你的外设可能只需要1MHz、10kHz甚至1Hz。怎么办?分频。

分频器本质上就是一个计数器。比如你想从50MHz得到1Hz,那就数25,000,000个时钟周期,翻转一次输出。这样输出就是50MHz / 50,000,000 = 1Hz。

不过,我个人不太建议用计数器分频来做时钟。为什么?因为分频出来的信号不是真正的时钟,它会有相位抖动。更好的做法是用PLL或者MMCM(Xilinx的叫法)来生成不同频率的时钟。但如果你只是驱动一个LED闪烁,用计数器分频完全够用。

// 分频器示例:从50MHz得到1Hz
reg [24:0] div_cnt;
reg        slow_clk;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        div_cnt <= 25'd0;
        slow_clk <= 1'b0;
    end else if (div_cnt == 25'd24_999_999) begin
        div_cnt <= 25'd0;
        slow_clk <= ~slow_clk;
    end else begin
        div_cnt <= div_cnt + 1'b1;
    end
end

4.5 时钟与复位:时序电路的“心跳”和“总开关”

时钟是时序电路的“心跳”。没有时钟,触发器就不知道什么时候该干活。时钟的质量直接决定了你的系统能跑多快。

复位则是“总开关”。上电时,所有寄存器必须进入一个已知状态,否则系统会乱套。

这里有几个关键点,我踩过坑,你们一定要记住:

  • 时钟域要统一:尽量不要在设计中混用多个不同源的时钟。如果必须跨时钟域,一定要做同步处理。
  • 复位策略要一致:要么全部用异步复位,要么全部用同步复位。混用会导致综合工具难以优化。
  • 不要用门控时钟:初学者喜欢用 assign clk_gated = clk & enable; 来省功耗。这在ASIC里可以,但在FPGA里会引入毛刺,导致时序问题。
警告: 千万不要在always块里用时钟的上升沿和下降沿同时触发!比如 always @(posedge clk or negedge clk)。这在FPGA里是不允许的,综合工具会报错。如果你需要双边沿采样,请用专用的IDDR原语。

4.6 知识体系总览

下面这张图是我画的本章知识结构,帮你理清思路:

时序逻辑设计知识体系 时序逻辑 触发器 (D-FF) 边沿敏感,存储1位数据 寄存器 多个触发器并行,存储N位数据 计数器 寄存器 + 加法器,循环计数 分频器 计数器的一种应用,产生低频信号 时钟与复位 时序电路的“心跳”和“总开关” 核心要点 边沿触发 · 同步设计 · 统一时钟域 · 可靠复位
小技巧: 在调试时序电路时,我习惯先在仿真里看波形。重点关注时钟沿和数据的对齐关系。如果数据在时钟沿附近有变化,说明可能有建立时间或保持时间问题。另外,复位信号一定要在仿真开始时拉低再拉高,否则所有寄存器都是X态,什么都看不到。

好了,这一章的内容就到这里。时序逻辑是FPGA设计的基石,触发器、寄存器、计数器、分频器这些基本单元,你会在后面的每个项目里反复用到。时钟和复位更是决定了你的系统能不能稳定工作。多写代码,多仿真,慢慢就会形成肌肉记忆。

本章核心总结:

  • 触发器是边沿敏感的存储单元,D触发器最常用
  • 寄存器 = 多个触发器并行,用于存储多位数据
  • 计数器 = 寄存器 + 加法器,用于计数、定时、分频
  • 分频器本质是计数器,但要注意时钟质量
  • 时钟和复位必须统一规划,避免亚稳态和复位混乱

可可精品资料库