第四章:时序逻辑设计——触发器、寄存器、计数器、分频器、时钟与复位
各位同学,欢迎来到第四章。前面我们聊了很多组合逻辑,说白了就是“输入一变,输出立马跟着变”。但真实的数字系统不是这样的——你得有个“节拍”,让数据在正确的时间点被锁存、传递、处理。这就是时序逻辑的活儿了。
我个人觉得,时序逻辑是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 @(posedge clk or negedge clk)。这在FPGA里是不允许的,综合工具会报错。如果你需要双边沿采样,请用专用的IDDR原语。
4.6 知识体系总览
下面这张图是我画的本章知识结构,帮你理清思路:
好了,这一章的内容就到这里。时序逻辑是FPGA设计的基石,触发器、寄存器、计数器、分频器这些基本单元,你会在后面的每个项目里反复用到。时钟和复位更是决定了你的系统能不能稳定工作。多写代码,多仿真,慢慢就会形成肌肉记忆。
本章核心总结:
- 触发器是边沿敏感的存储单元,D触发器最常用
- 寄存器 = 多个触发器并行,用于存储多位数据
- 计数器 = 寄存器 + 加法器,用于计数、定时、分频
- 分频器本质是计数器,但要注意时钟质量
- 时钟和复位必须统一规划,避免亚稳态和复位混乱