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

大家好,我是你们的FPGA讲师。今天我们来聊聊时序逻辑的核心——触发器、寄存器、计数器还有分频器。这些东西,说白了就是数字电路里的“记忆单元”。没有它们,你的电路就只能做组合逻辑,做完就忘,啥也存不住。

我个人习惯把时序逻辑比作“流水线上的工人”。组合逻辑是工人手里的工具,而触发器就是工人手里的“便签纸”——记下当前的状态,传给下一个工序。你想想看,没有便签纸,流水线怎么跑得起来?

核心要点:时序逻辑与组合逻辑最大的区别在于——它有时钟,有记忆。所有时序单元都在时钟沿(上升沿或下降沿)更新状态。

第4章:时序逻辑设计知识体系 时序逻辑基础 触发器 (FF) D触发器 T触发器 寄存器 普通寄存器 移位寄存器 计数器 二进制 BCD/环形 分频器 偶数分频 · 奇数分频 · 小数分频

4.1 触发器:数字电路的“记忆细胞”

触发器(Flip-Flop)是时序逻辑的最小单元。每个触发器可以存储1位数据(0或1)。我刚开始学的时候,总觉得触发器很神秘,后来发现它其实就是个“受时钟控制的锁存器”。

4.1.1 D触发器

D触发器是最常用的触发器。它的逻辑很简单:在每个时钟上升沿,把输入D的值“抓”到输出Q上。说白了就是——时钟来了,D是多少,Q就变成多少。

我的经验:在FPGA中,99%的时序逻辑都是用D触发器实现的。你写的 always @(posedge clk) 综合出来就是一堆D触发器。我曾经在项目中为了省资源,试图用锁存器代替触发器,结果时序一塌糊涂——嗯,从那以后我再也不敢乱用锁存器了。

// 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;      // 复位时输出0
    else
        q <= d;         // 时钟上升沿采样D
end

endmodule

4.1.2 T触发器

T触发器(Toggle Flip-Flop)的特点是:T=1时,输出翻转;T=0时,输出保持。说白了就是个“可控翻转器”。

你可能会问:“T触发器有什么用?” 我告诉你,计数器的基础就是T触发器。每个二进制位本质上就是一个T触发器——当低位翻转时,高位才翻转。

// T触发器 Verilog实现
module t_flip_flop (
    input  wire clk,
    input  wire rst_n,
    input  wire t,
    output reg  q
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else if (t)
        q <= ~q;        // T=1时翻转
    else
        q <= q;         // T=0时保持
end

endmodule

注意:在FPGA中,T触发器通常由D触发器加一个异或门组合实现。不要试图在代码里直接实例化T触发器——综合工具会帮你自动转换。你只需要写逻辑,工具负责映射。

4.2 寄存器:多个触发器组成的“数据仓库”

寄存器就是一组D触发器,共享同一个时钟和复位。比如8位寄存器就是8个D触发器并排。我在项目中经常用寄存器来暂存数据、做流水线打拍。

// 8位寄存器 Verilog实现
module reg_8bit (
    input  wire       clk,
    input  wire       rst_n,
    input  wire [7:0] d,
    output reg  [7:0] q
);

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

endmodule

4.2.1 移位寄存器

移位寄存器是寄存器的“升级版”。它不仅能存数据,还能在每个时钟周期把数据向左或向右移动一位。你想想看,串行通信(比如SPI、UART)的核心就是移位寄存器——把并行数据一位一位送出去。

// 8位左移移位寄存器
module shift_reg_left (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       din,      // 串行输入
    output reg  [7:0] q         // 并行输出
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 8'b0;
    else
        q <= {q[6:0], din};    // 左移,低位补入din
end

endmodule

避坑指南:我曾经在写移位寄存器时,把移位方向搞反了。结果串行数据出来全是乱的,调试了一整天。后来我养成了一个习惯——写移位操作时,先在纸上画一遍数据流,再写代码。你也不妨试试。

4.3 计数器:数字电路里的“节拍器”

计数器本质上就是一组寄存器,加上加法/减法逻辑。每来一个时钟,计数值加1或减1。计数器在FPGA里无处不在——定时、分频、状态机、地址生成……

4.3.1 二进制计数器

最基础的计数器。从0数到2^N-1,然后回0。比如4位二进制计数器,从0数到15。

// 4位二进制计数器
module counter_bin (
    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;    // 每个时钟加1
end

endmodule

4.3.2 BCD计数器

BCD(Binary-Coded Decimal)计数器,每10个时钟循环一次,输出0-9。常用于数码管显示、时钟芯片等场景。

// BCD计数器(0-9)
module counter_bcd (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [3:0] cnt,
    output reg        carry    // 进位信号
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt   <= 4'b0;
        carry <= 1'b0;
    end else if (cnt == 4'd9) begin
        cnt   <= 4'b0;
        carry <= 1'b1;        // 产生进位
    end else begin
        cnt   <= cnt + 1'b1;
        carry <= 1'b0;
    end
end

endmodule

4.3.3 环形计数器

环形计数器比较特殊——它只有一个1在循环移动。比如4位环形计数器:1000 -> 0100 -> 0010 -> 0001 -> 1000。这种计数器常用于状态机、顺序控制。

// 4位环形计数器
module counter_ring (
    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'b1000;       // 初始值:只有最高位为1
    else
        cnt <= {cnt[2:0], cnt[3]};  // 循环左移
end

endmodule

对比总结:

计数器类型计数范围典型应用
二进制计数器0 ~ 2^N-1通用计数、定时
BCD计数器0 ~ 9数码管、时钟显示
环形计数器N个状态,仅1位为1顺序控制、状态机

4.4 分频器:从高频到低频的“减速器”

分频器,说白了就是把高频时钟变成低频时钟。比如板子上有个50MHz晶振,但你的UART需要9600波特率——怎么办?分频!

4.4.1 偶数分频

最简单。比如2分频:每2个时钟输出翻转一次。N分频:计数器数到N/2-1时翻转。

// 偶数分频:以6分频为例
module div_even (
    input  wire       clk,
    input  wire       rst_n,
    output reg        clk_out
);

reg [2:0] cnt;  // 3位计数器,最大7

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt     <= 3'b0;
        clk_out <= 1'b0;
    end else if (cnt == 3'd2) begin  // 6/2 - 1 = 2
        cnt     <= 3'b0;
        clk_out <= ~clk_out;        // 翻转输出
    end else begin
        cnt     <= cnt + 1'b1;
    end
end

endmodule

4.4.2 奇数分频

奇数分频稍微麻烦一点。需要同时用上升沿和下降沿计数,然后把两个结果“或”起来。我刚开始做奇数分频时,总觉得没必要这么复杂——直到我试了试只用上升沿,结果占空比不是50%。

// 奇数分频:以5分频为例(占空比50%)
module div_odd (
    input  wire       clk,
    input  wire       rst_n,
    output wire       clk_out
);

reg [2:0] cnt_p, cnt_n;  // 上升沿和下降沿计数器
reg       clk_p, clk_n;

// 上升沿计数
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt_p <= 3'b0;
        clk_p <= 1'b0;
    end else if (cnt_p == 3'd4) begin
        cnt_p <= 3'b0;
        clk_p <= 1'b0;
    end else begin
        cnt_p <= cnt_p + 1'b1;
        if (cnt_p == 3'd2) clk_p <= 1'b1;  // 在中间点置高
    end
end

// 下降沿计数
always @(negedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt_n <= 3'b0;
        clk_n <= 1'b0;
    end else if (cnt_n == 3'd4) begin
        cnt_n <= 3'b0;
        clk_n <= 1'b0;
    end else begin
        cnt_n <= cnt_n + 1'b1;
        if (cnt_n == 3'd2) clk_n <= 1'b1;
    end
end

assign clk_out = clk_p | clk_n;  // 两个信号相或

endmodule

重要提醒:在FPGA中,尽量不要用分频后的时钟作为全局时钟。分频时钟会有相位噪声和抖动。我建议的做法是——用高频时钟作为全局时钟,分频信号只作为使能信号(enable)。这样时序更干净,综合工具也更容易优化。

4.5 本章小结

这一章我们聊了时序逻辑的四大核心:触发器、寄存器、计数器和分频器。它们之间其实是一脉相承的——触发器是最小单元,寄存器是触发器阵列,计数器是带加法逻辑的寄存器,分频器是特殊用途的计数器。

我个人觉得,学时序逻辑最重要的是理解“时钟沿”的概念。所有操作都发生在时钟沿上,其他时间电路是“静止”的。你把这个想通了,后面的状态机、FIFO、流水线设计都会轻松很多。

好了,这一章就到这里。代码示例我都放在课程配套资源里了,你可以下载下来跑一跑。有什么问题,欢迎在评论区留言。


专注资料整理