4、时序逻辑电路:触发器(D、JK)、寄存器、计数器、时钟与同步设计

好,咱们今天聊聊时序逻辑。说实话,这是数字电路里真正开始「动脑子」的地方。组合逻辑只管当前输入,但时序逻辑不一样——它记住了过去的状态。你想想看,没有记忆能力的电路,能干什么?顶多算个计算器。但有了时序逻辑,我们才能做出CPU、状态机、通信协议。

我个人习惯把时序逻辑比作「有记忆的开关」。它不只看你现在按了什么,还记得你之前按过什么。这个「记忆」的核心,就是触发器。

4.1 触发器:数字电路的「记忆细胞」

触发器是时序逻辑的最小单元。一个触发器能存储1位二进制数据。说白了,就是能记住一个0或1。

我在项目中遇到过最坑的事,就是有人把锁存器(Latch)和触发器(Flip-Flop)混为一谈。锁存器是电平触发的,触发器是边沿触发的。电平触发意味着只要时钟高电平,输出就跟着输入变——这很容易产生毛刺。而边沿触发只在时钟上升沿或下降沿采样,稳定得多。

4.1.1 D触发器:最常用的触发器

D触发器是最简单的。它的逻辑是:在时钟上升沿,把输入D的值传给输出Q。其他时间,Q保持不变。

为什么D触发器最常用?因为它的行为最直观。你给它什么,它就在下一个时钟沿记住什么。没有多余的逻辑。

核心特性:

  • 时钟上升沿触发(也可以是下降沿,看设计)
  • 输出Q = 输入D(仅在时钟沿时刻)
  • 其他时间Q保持不变

来看一个简单的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

嗯,这里要注意:我用了posedge clk,表示上升沿触发。rst_n是异步复位,低电平有效。这是工程中最常用的写法。

4.1.2 JK触发器:更灵活的选择

JK触发器比D触发器多了一个「翻转」功能。它有J和K两个输入。当J=1、K=1时,输出翻转。当J=1、K=0时,输出置1。当J=0、K=1时,输出清0。当J=0、K=0时,输出保持不变。

我曾经在做一个分频器时用过JK触发器。当时需要实现一个2分频电路,用JK触发器只需要把J和K都接1,时钟输入接CLK,输出Q就是二分频。比用D触发器加反相器简单多了。

但说实话,现在FPGA开发中,D触发器是主流。因为综合工具对D触发器的优化最好。JK触发器更多出现在教材和分立元件设计中。

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

寄存器就是一组触发器。8位寄存器就是8个D触发器并排。它们共享同一个时钟和复位信号,但数据输入输出各自独立。

我在项目中调试过一个SPI接口,发现数据总是错位。查了半天,原来是寄存器位宽没对齐。发送端用8位寄存器,接收端用16位寄存器,结果数据全串了。嗯,这种低级错误,犯过一次就记住了。

寄存器的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

个人经验:写寄存器时,复位值一定要明确。有些新手不写复位,仿真时没问题,但上电后寄存器状态是未知的,很容易出bug。

4.3 计数器:数字电路的时间轴

计数器是寄存器的一种特殊应用。它每个时钟周期加1(或减1),直到达到某个值后归零或翻转。

计数器太常见了。分频、定时、状态机跳转、地址生成……几乎每个数字系统里都有计数器。

我记得有一次做LED呼吸灯效果,需要产生一个PWM信号。PWM的周期和占空比都是用计数器实现的。一个计数器产生周期,另一个计数器控制占空比。两个计数器配合,就能模拟出呼吸效果。

一个简单的二进制计数器:

module counter_8bit (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       en,
    output reg  [7:0] count
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 8'b0;
        else if (en)
            count <= count + 1'b1;
    end
endmodule

避坑指南:我曾经在计数器溢出时没做处理,导致计数到255后直接跳回0,中间产生了毛刺。后来我加了同步复位和溢出标志,问题才解决。计数器溢出一定要考虑清楚,是自动回绕还是产生中断。

4.4 时钟与同步设计:时序电路的「心跳」

时钟是时序电路的灵魂。没有时钟,触发器就不知道什么时候该采样。时钟信号就像乐队指挥的节拍,所有触发器都在这个节拍下统一行动。

同步设计,说白了就是所有触发器都使用同一个时钟源。这样整个电路的行为是确定的、可预测的。异步设计则不同,不同模块用不同时钟,需要额外的同步处理。

我刚开始做FPGA时,觉得异步设计很酷,能省功耗。结果有一次,两个异步时钟域之间传递数据,数据总是丢包。查了三天,最后发现是跨时钟域同步没做好。从那以后,我老老实实用同步设计,除非万不得已。

4.4.1 时钟抖动与时钟偏移

时钟不是完美的。时钟抖动(Jitter)是指时钟边沿在时间上的随机波动。时钟偏移(Skew)是指同一个时钟到达不同触发器的延迟差异。

这两个问题在高频设计中特别明显。我做过一个200MHz的设计,时钟抖动稍微大一点,时序就收敛不了。最后换了更好的时钟源,才解决问题。

参数 定义 影响
时钟抖动 时钟边沿的时间随机波动 降低时序裕量,可能导致建立/保持时间违例
时钟偏移 时钟到达不同触发器的延迟差 可能导致数据竞争,需要平衡时钟树

4.4.2 同步设计的基本原则

同步设计有几个铁律:

  • 单一时钟域:尽量使用同一个时钟源
  • 边沿触发:所有触发器都用时钟边沿触发
  • 避免组合逻辑反馈:组合逻辑的输出不要直接反馈到自己的输入
  • 同步复位:复位信号也要和时钟同步

你想想看,如果违反了这些原则,电路的行为就会变得不可预测。仿真时可能没问题,但实际芯片跑起来,温度、电压一变,问题就出来了。

核心总结:

  • 触发器是时序逻辑的基本单元,D触发器最常用
  • 寄存器是多个触发器的组合,用于存储多位数据
  • 计数器是寄存器的应用,用于计数和分频
  • 同步设计是时序电路的基础,时钟质量决定系统稳定性

好了,这一章的内容就到这里。下一章我们会深入讨论状态机的设计,那是时序逻辑的进阶应用。到时候我会分享一个我踩过的坑——状态机跑飞了,结果整个系统失控。嗯,那故事挺有意思的。