3、寄存器与锁存器:D触发器原理、Verilog实现D触发器、锁存器(Latch)的生成与避免
好,咱们今天聊点硬核的——寄存器与锁存器。
说实话,这两个东西是时序逻辑的基石。你想想看,没有它们,你的电路连个状态都记不住,那还玩什么?我刚开始学FPGA那会儿,总觉得D触发器就是个简单的存储单元,没啥好研究的。直到有一次,我在一个高速接口项目里,因为没处理好锁存器的问题,整个板子跑起来数据全是乱的……嗯,从那以后,我再也不敢小看这玩意儿了。
3.1 D触发器的工作原理
D触发器,全名叫Data Flip-Flop,说白了就是一个边沿触发的存储单元。它只在时钟的上升沿(或下降沿)采样输入数据,然后把这个值锁住,直到下一个时钟沿到来。
它的核心结构其实很简单:
- D端:数据输入
- CLK端:时钟输入
- Q端:数据输出
- QN端:反相输出(有些器件有)
我习惯把D触发器想象成一个“听话的哨兵”。时钟沿一来,它就把D端的数据“咔嚓”一下拍下来,然后死死守住,直到下一个时钟沿。这中间不管D端怎么变,Q端纹丝不动。
关键特性:
- 边沿触发(上升沿或下降沿)
- 输出只在时钟沿变化
- 具有存储功能,能保持一个比特的状态
为什么会这样?因为D触发器内部是由两个锁存器级联组成的——主锁存器和从锁存器。时钟高电平时,主锁存器打开,从锁存器关闭;时钟低电平时,主锁存器关闭,从锁存器打开。这样一配合,就实现了边沿触发的效果。
3.2 Verilog实现D触发器
写Verilog实现D触发器,其实就几行代码的事。但这里有个坑,我当年踩过——敏感列表一定要写对。
先看一个最标准的上升沿D触发器:
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; // 时钟上升沿采样
end
endmodule
你看,这个写法里敏感列表包含了posedge clk和negedge rst_n。异步复位的意思是,不管时钟在什么状态,只要复位信号有效,输出立刻清零。这在很多实际项目中是必须的——上电瞬间,你得保证所有寄存器处于已知状态。
我个人习惯,所有时序逻辑都加上异步复位。虽然有些设计为了省资源会用同步复位,但我觉得,安全第一。你想想看,万一上电时时钟还没稳定,同步复位就失效了,那寄存器里的值就是X态,后面整个逻辑都乱了。
再来一个带使能端的D触发器:
module d_flip_flop_en (
input wire clk,
input wire rst_n,
input wire en, // 使能信号
input wire d,
output reg q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else if (en)
q <= d; // 使能有效时才更新
// 使能无效时,q保持不变
end
endmodule
这个使能端在实际中太常用了。比如你要做一个计数器,不是每个时钟周期都要计数的,那就需要一个使能信号来控制。我在项目中经常用这种结构来做数据流控制——数据准备好时,使能拉高,D触发器采样;没准备好时,保持原值。
3.3 锁存器(Latch)的生成与避免
锁存器,英文叫Latch。它和D触发器最大的区别是:锁存器是电平敏感,D触发器是边沿敏感。
锁存器在时钟高电平时透明(输出跟随输入),低电平时锁存。听起来好像也能用,但为什么我们通常要避免它?
原因有三:
- 时序分析困难:锁存器是电平触发的,静态时序分析工具对它的支持不如触发器好,容易产生时序违例。
- 毛刺敏感:锁存器在透明期间,输入的任何毛刺都会直接传到输出,这很要命。
- 综合不可控:很多综合工具对锁存器的优化策略不透明,你写出来的代码可能综合出你意想不到的电路。
我曾经踩过的坑:
有一次写一个状态机,在always块里用了组合逻辑写if-else,结果条件没写全,综合出来一堆锁存器。仿真时功能是对的,但上板跑起来,状态机乱跳。查了两天才发现是锁存器在作怪。从那以后,我写组合逻辑always块时,一定会把所有的条件分支写完整,或者给default赋值。
那么,锁存器是怎么生成的?说白了,就是组合逻辑中,信号在某些条件下没有被赋值。
看这个反面教材:
// 错误示例:会生成锁存器
always @(*) begin
if (sel)
q = a;
// 缺少else分支!当sel=0时,q保持原值
end
这个代码里,当sel=0时,q没有被赋值。综合工具就会推断:嗯,这里需要一个存储单元来保持原值。于是锁存器就生成了。
正确的写法应该是:
// 正确写法:避免锁存器
always @(*) begin
if (sel)
q = a;
else
q = b; // 给一个默认值
end
或者更稳妥的做法,在always块开头先给所有输出赋默认值:
always @(*) begin
q = 1'b0; // 先给默认值
if (sel)
q = a;
// else分支不需要了,因为默认值已经覆盖
end
这个技巧我一直在用,效果很好。你想想看,只要保证每个分支下所有信号都被赋值了,锁存器就不会凭空冒出来。
3.4 知识体系总览
为了让你更直观地理解这一章的内容,我画了一张图:
我的小建议:
写Verilog时,养成一个好习惯——时序逻辑用always @(posedge clk),组合逻辑用always @(*)。时序逻辑里用非阻塞赋值<=,组合逻辑里用阻塞赋值=。这个规矩别打破,能省掉你80%的debug时间。
好了,这一章的内容就这些。D触发器和锁存器,看似简单,但背后涉及的东西不少。你只要记住:做时序设计,优先用D触发器;写组合逻辑,务必把条件写全。这两条做到了,锁存器就不会来找你麻烦。
公众号:蓝海资料掘金营,微信deep3321