第4章:时序逻辑设计——触发器、寄存器、计数器与分频器
大家好,我是你们的FPGA讲师。今天我们来聊聊时序逻辑的核心——触发器、寄存器、计数器还有分频器。这些东西,说白了就是数字电路里的“记忆单元”。没有它们,你的电路就只能做组合逻辑,做完就忘,啥也存不住。
我个人习惯把时序逻辑比作“流水线上的工人”。组合逻辑是工人手里的工具,而触发器就是工人手里的“便签纸”——记下当前的状态,传给下一个工序。你想想看,没有便签纸,流水线怎么跑得起来?
核心要点:时序逻辑与组合逻辑最大的区别在于——它有时钟,有记忆。所有时序单元都在时钟沿(上升沿或下降沿)更新状态。
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、流水线设计都会轻松很多。
好了,这一章就到这里。代码示例我都放在课程配套资源里了,你可以下载下来跑一跑。有什么问题,欢迎在评论区留言。