从触发器到分频器:数字电路时序逻辑核心原理与实战设计 1. 从零搞懂触发器数字电路里最容易被低估的“记忆单元”很多人学数字电路一上来就被各种门电路、真值表、卡诺图绕晕好不容易把组合逻辑啃下来结果一到时序逻辑就彻底卡壳。而时序逻辑的第一道坎就是触发器。我见过太多人做实验的时候照着电路图连完线发现输出不对然后开始怀疑芯片坏了、怀疑电源不稳、怀疑人生其实根本原因就是没搞明白触发器到底在干什么。触发器说白了就是一个能记住一位二进制数的电路。组合逻辑是“你给它什么输入它立刻给你什么输出”没有记忆触发器不一样它能把某个时刻的状态锁住等到下一个时钟边沿再决定要不要更新。这个“锁住”的能力就是所有计数器、分频器、寄存器、状态机的基础。你手机里的时钟、电脑里的CPU、甚至微波炉上的倒计时底层都离不开触发器在撑着。这篇文章我打算把触发器、计数器、分频器这条线从头到尾捋一遍。不管你是正在上数字电路课的学生还是想用Verilog写个分频器但总是调不通的工程师或者只是想用555和CD4017做个流水灯玩玩的爱好者这里的内容应该都能帮到你。我会从触发器的基本原理讲起然后过渡到计数器设计再重点讲分频器的各种实现方式最后给几个可以直接抄作业的实操方案。中间会穿插我自己踩过的坑和一些教科书上不会写的经验。2. 触发器核心原理拆解为什么6个晶体管就能记住一个bit2.1 双稳态电路的本质两个反相器互相“掐架”触发器最底层的原理其实特别简单就是两个反相器首尾相连。你想象两个人吵架A说“B说的是错的”B说“A说的是错的”如果A输出高电平B就输出低电平B输出低电平又让A维持高电平这个状态就稳定了。反过来也一样A低B高也能稳定。这就是所谓的双稳态——电路有两个稳定状态分别代表0和1。用CMOS工艺来实现的话一个反相器需要2个晶体管一个PMOS一个NMOS两个反相器就是4个晶体管。再加上两个用于控制写入的传输门或者访问晶体管总共6个晶体管就能构成一个基本的SRAM存储单元。这也是为什么网上有人搜“6个晶体管如何实现双稳态触发器”——这个数字不是随便来的它就是最经典的6T SRAM单元结构。但要注意这种最简单的双稳态电路有个问题你没法控制它什么时候写入。它上电之后会随机落到某个状态你也没办法可靠地把它设成你想要的0或1。所以实际使用的触发器都会加上控制端口比如置位Set、复位Reset、时钟Clock、数据输入D等等。2.2 从RS锁存器到D触发器一步步搞懂边沿触发的意义先看最基础的RS锁存器。它有两个输入端R和SR是复位S是置位。当S1、R0时输出被置为1当S0、R1时输出被清为0当SR0时保持原状态。但问题来了当SR1时输出会同时往1和0跑最后结果不确定这是一个非法状态。在实际电路中这种不确定状态可能导致亚稳态让整个系统崩溃。为了解决这个问题人们在RS锁存器前面加了一个控制门做成了门控D锁存器。D锁存器只有一个数据输入D和一个使能端EN。当EN为高时输出跟随输入D变化当EN变低时输出锁存住最后的值。这比RS锁存器好用多了但仍然有一个致命缺陷它是电平敏感的。也就是说在EN为高的整个期间输出都会跟着D变化。如果D在EN为高的时候多次翻转输出也会跟着翻转多次。这就引出了边沿触发器的概念。边沿触发器只在时钟信号的上升沿或下降沿那一瞬间采样输入其他时间输出保持不变。这样一来一个时钟周期内不管D怎么变输出只会在边沿时刻更新一次。这个特性对于构建同步系统至关重要因为它让整个系统的行为变得可预测。D触发器D-FF是最常用的边沿触发器类型。它的符号很简单一个D输入、一个CLK输入、一个Q输出有时候还有Q反相输出、置位和复位端。工作规则也很简单在CLK的上升沿或下降沿取决于型号Q输出等于D输入的值其他时刻Q保持不变。我刚开始学的时候老是想不通为什么要有边沿触发这个东西电平触发不是挺好吗后来做实验才明白如果你用两个电平触发的锁存器级联一个用高电平使能一个用低电平使能就能构成一个边沿触发的效果。这就是所谓的主从触发器结构。实际芯片内部的D触发器大多就是这么实现的只是集成度更高、速度更快。2.3 触发器的关键参数建立时间、保持时间和传播延迟选触发器不能只看功能还要看时序参数。这三个参数你必须搞清楚建立时间Setup Timet_su在时钟边沿到来之前D输入必须保持稳定的最短时间。如果你在时钟边沿前太晚才改变D触发器可能采到旧值或者进入亚稳态。保持时间Hold Timet_h在时钟边沿到来之后D输入必须继续保持稳定的最短时间。如果你在边沿后立刻改变D触发器可能还没锁住就被你改掉了。传播延迟Propagation Delayt_pd从时钟边沿到Q输出稳定为新值所需的时间。这个参数决定了你的电路最高能跑多快的时钟。这三个参数的关系可以用一个简单类比来理解触发器就像一个拍照的相机。建立时间就是按下快门前你必须保持姿势不动的时间保持时间是快门按下后你还得再坚持一下的时间传播延迟就是相机处理照片并显示出来的时间。如果你动得太早或太晚照片就糊了。在实际设计中如果你的时钟频率太高导致时钟周期小于t_su t_pd 组合逻辑延迟那电路就会出错。这就是为什么高速电路设计里时序分析这么重要。注意很多初学者做实验时喜欢用按键手动产生时钟脉冲这时候按键抖动会导致一次按下产生多个边沿触发器就会误动作多次。正确做法是用施密特触发器消抖或者用RC滤波加施密特触发器。3. 计数器设计全解析从异步到同步从二进制到任意进制3.1 异步计数器 vs 同步计数器到底选哪个计数器本质上就是一组触发器级联每个触发器负责一位二进制数。最简单的做法是把前一级的输出接到后一级的时钟输入这就是异步计数器也叫行波计数器。异步计数器的优点是电路简单、用的门少。比如一个4位二进制计数器你只需要4个T触发器或者D触发器接成翻转模式把Q0接到第二级的时钟Q1接到第三级的时钟以此类推。但它的缺点也很明显因为时钟是逐级传递的每一级都有传播延迟所以高位的变化会滞后于低位。当计数器从0111变成1000的时候四个位几乎同时翻转但由于延迟不同中间会出现短暂的错误状态比如1111、1011之类的。这些毛刺在纯计数应用里可能无所谓但如果用来驱动译码器或者作为其他电路的时钟就会出问题。同步计数器就不一样了所有触发器共用同一个时钟每个触发器的输入由组合逻辑根据当前状态和计数使能决定。这样所有位同时翻转没有行波延迟的问题。代价就是需要额外的组合逻辑电路复杂一些。常见的同步计数器芯片有74HC161、74HC163等。我个人的建议是如果你只是做个实验或者对速度要求不高异步计数器够用了但如果是实际产品设计尤其是需要驱动数码管或者作为分频器使用尽量用同步计数器省心。3.2 用D触发器搭建一个4位二进制计数器下面我用D触发器来搭一个4位同步二进制计数器这样你能看清楚每一级的逻辑是怎么来的。一个D触发器要变成T触发器翻转触发器只需要把Q反相后接回D输入。这样每个时钟边沿Q都会翻转。对于同步计数器第n位的翻转条件是所有低位都为1且计数使能有效。具体逻辑如下第0位最低位每个时钟边沿都翻转D0 Q0反相第1位当Q01时翻转D1 Q1 XOR Q0第2位当Q0Q11时翻转D2 Q2 XOR (Q0 AND Q1)第3位当Q0Q1Q21时翻转D3 Q3 XOR (Q0 AND Q1 AND Q2)用Verilog来描述就更直观了module counter_4bit( input clk, input rst_n, input en, output reg [3:0] q ); always (posedge clk or negedge rst_n) begin if (!rst_n) q 4b0000; else if (en) q q 1b1; end endmodule这段代码综合出来就是一个标准的4位同步二进制计数器。注意这里用了非阻塞赋值这是写时序逻辑的铁律。如果你用阻塞赋值综合出来的电路可能和你想的完全不一样。3.3 任意进制计数器的设计方法实际应用中我们经常需要非2的幂次方的计数比如十进制计数器0-9、六进制计数器0-5、甚至模60计数器0-59。设计方法有两种复位法和置位法。复位法计数器正常计数当计到目标值1时通过组合逻辑产生复位信号让计数器回到0。比如设计一个模10计数器用4位二进制计数器当计到101010时产生复位信号。但这种方法有个毛刺问题1010这个状态会短暂出现如果这个状态被译码器捕捉到就会产生一个窄脉冲。解决办法是用时钟同步复位或者在输出加锁存器。置位法计数器从某个预设值开始计数计到最大值后自动回到预设值。比如用74HC163同步置位设计模10计数器把预置值设为01106让它从6计到15正好10个状态。这样输出就是6,7,8,9,10,11,12,13,14,15循环虽然不是从0开始但如果你只需要一个10分频的信号完全没问题。实操心得用复位法设计任意进制计数器时复位信号最好用同步复位而不是异步复位。异步复位虽然简单但容易受毛刺影响而且在高速电路中可能导致亚稳态。如果你用的是74HC161这类异步复位芯片可以在复位路径上加一个D触发器做同步化处理。3.4 用STC8G1K08单片机做一个0-999计数器网上有人搜“用stc8g1k08做一个计数器接四位共阴极数码管”这个需求很典型。STC8G1K08是一款51内核的单片机价格便宜资源对于这种应用绰绰有余。硬件连接方案P1.0接数码管的千位位选P1.1接百位位选P1.6接十位位选P1.7接个位位选P0口接数码管的段选a-g, dp注意这里位选和段选的分配是用户指定的实际接线时要注意共阴极数码管的位选是低电平有效也就是说你要让某一位亮对应的位选引脚要输出低电平。软件方面你需要做几件事定时器中断用定时器0产生1ms中断在中断里做数码管动态扫描和多路复用。计数逻辑用一个变量记录当前计数值每次计数脉冲到来时加1到999后回0。数码管译码把计数值的每一位拆出来查表得到对应的段码。段码表共阴极0-9const unsigned char seg_code[] { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F // 9 };动态扫描的核心思路是每1ms切换一位4ms完成一轮扫描。由于人眼有视觉暂留看起来四位都是同时亮的。但如果你扫描频率太低会看到闪烁太高又可能导致亮度不足。1-2ms切换一位是比较合适的。注意STC8G1K08的P0口是开漏输出如果你直接驱动共阴极数码管的段选需要外接上拉电阻。或者改用P2口、P3口等推挽输出口。这一点很多初学者会忽略导致数码管亮度不够或者完全不亮。4. 分频器实战从整数分频到超高频预分频4.1 分频器和计数器的关系一回事两种叫法分频器本质上就是一个计数器。一个模N计数器输出频率就是输入时钟频率的1/N。比如一个4位二进制计数器最高位Q3的输出频率就是时钟频率的1/16。所以你在设计分频器的时候完全可以套用计数器的设计方法。但分频器有一些特殊需求比如需要50%占空比的方波输出需要非整数分频比如1.5分频、2.5分频需要超高频输入比如射频信号需要可编程分频比这些需求催生了各种不同的分频器结构。4.2 偶数分频和奇数分频的实现差异偶数分频最简单。比如2分频就是一个T触发器每个时钟边沿翻转一次。4分频就是两个T触发器级联。8分频就是三个。对于2的幂次方分频直接用计数器的高位输出就行占空比天然是50%。奇数分频就麻烦一些。比如3分频你不能简单地用计数器高位输出因为那样占空比不是50%。要实现50%占空比的3分频通常需要用到双边沿或者两个计数器配合。一个经典的3分频50%占空比实现方案用上升沿触发的计数器计到1时翻转输出用下降沿触发的计数器计到1时翻转输出两个输出做或运算这样得到的输出频率是时钟的1/3占空比50%。具体逻辑可以用Verilog描述module div3( input clk, input rst_n, output clk_out ); reg [1:0] cnt_pos, cnt_neg; reg out_pos, out_neg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_pos 2d0; out_pos 1b0; end else begin if (cnt_pos 2d1) begin cnt_pos 2d0; out_pos ~out_pos; end else begin cnt_pos cnt_pos 1b1; end end end always (negedge clk or negedge rst_n) begin if (!rst_n) begin cnt_neg 2d0; out_neg 1b0; end else begin if (cnt_neg 2d1) begin cnt_neg 2d0; out_neg ~out_neg; end else begin cnt_neg cnt_neg 1b1; end end end assign clk_out out_pos | out_neg; endmodule这个思路可以推广到任意奇数分频用两个计数器分别在上边沿和下边沿计数各自产生一个占空比不是50%但相位差半个周期的信号然后做逻辑运算。4.3 小数分频和半整数分频的实用技巧半整数分频比如2.5分频意思是每2.5个输入时钟周期产生一个输出周期。实现方法是交替进行2分频和3分频。比如第一个周期2分频第二个周期3分频平均下来就是2.5分频。具体实现可以用一个模5计数器当计到0和2时翻转输出。这样输出周期是5个输入周期但输出翻转了2次所以输出频率是输入的2/5也就是2.5分频。占空比不是50%但如果你只需要频率正确这个方法非常简单。对于更一般的小数分频比如3.7分频可以用分数分频器fractional divider的思路用累加器控制分频比在3和4之间切换使得平均分频比为3.7。这种方法在锁相环PLL和频率合成器中非常常见。4.4 MB506超高频预分频器的应用场景MB506是一款经典的超高频预分频器芯片内部集成了一个模256或模64/128/256可选的预分频器工作频率可以到1GHz以上。它通常用在频率计或者接收机的前端把高频信号先分频到低频再送给后级计数器处理。为什么需要预分频器因为普通的CMOS计数器最高也就几十MHz而射频信号可能在几百MHz甚至GHz级别。预分频器用高速工艺比如ECL或者高速CMOS实现先把频率降下来后面的电路就好处理了。MB506的典型应用电路输入信号通过耦合电容送到MB506的输入端通过控制引脚选择分频比64/128/256输出送到单片机的计数器输入或者专用频率计芯片实操心得用MB506的时候输入信号的幅度很关键。如果信号太小芯片可能不响应如果太大可能损坏输入级。建议在输入端加一个限幅电路比如两个反向并联的肖特基二极管。另外MB506的电源去耦一定要做好否则高频噪声会让输出抖动严重。4.5 用Verilog实现一个可编程分频器在实际FPGA或ASIC设计中可编程分频器非常实用。下面是一个参数化的分频器模块分频比可以通过输入端口动态改变module programmable_divider #( parameter WIDTH 16 )( input clk, input rst_n, input [WIDTH-1:0] div_ratio, output reg clk_out ); reg [WIDTH-1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; clk_out 0; end else begin if (cnt div_ratio - 1) begin cnt 0; clk_out ~clk_out; end else begin cnt cnt 1b1; end end end endmodule这个模块的输出频率是输入频率的1/(2*div_ratio)占空比50%。如果你需要奇数分频可以把输出翻转条件改成cnt div_ratio但那样占空比就不是50%了。注意div_ratio不能为0否则计数器会一直满足条件输出会变成输入频率的一半。实际使用时要加保护逻辑。5. 常见问题与排查技巧实录5.1 触发器输出不稳定或振荡怎么办这是最常遇到的问题。可能的原因和排查方法现象可能原因排查方法解决方案输出高频振荡时钟信号有毛刺用示波器看时钟波形加施密特触发器或RC滤波输出随机跳变输入信号建立/保持时间不满足检查D输入和时钟的时序关系调整输入延迟或降低时钟频率上电后状态不确定没有复位电路检查是否有上电复位加RC复位或专用复位芯片输出电平在中间值亚稳态用示波器看电平是否在VDD/2附近加两级同步触发器亚稳态是触发器最隐蔽的问题。当D输入在时钟边沿附近变化时触发器内部的两个反相器可能进入一个既不是0也不是1的中间状态然后随机地落到0或1。这个随机过程可能需要很长时间才能稳定如果后级电路在这个不稳定期间采样就会得到错误结果。解决办法是使用同步器用两级D触发器级联第一级的输出可能进入亚稳态但经过一个时钟周期后第二级采样时第一级已经稳定了。虽然第二级仍然有极小概率进入亚稳态但概率已经大大降低。5.2 计数器计到某个值就卡住或跳变这种情况通常是复位或置位逻辑有问题。比如你用复位法设计模10计数器当计到1010时产生复位信号。但如果复位信号的毛刺太窄计数器可能还没复位就离开了1010状态导致计数错误。排查步骤用逻辑分析仪抓取计数器的所有输出位看卡住的时候是什么状态检查复位信号的产生逻辑看是否有竞争冒险如果是异步复位尝试改成同步复位检查电源是否稳定电源波动也可能导致计数器误动作另一个常见原因是时钟信号的质量。如果时钟上升沿太慢计数器可能在边沿附近多次翻转。用示波器看时钟波形上升时间应该在纳秒级别。如果上升时间超过几十纳秒就需要加缓冲器或者用专用时钟芯片。5.3 分频器输出占空比不对如果你用计数器的高位输出做分频占空比天然是50%对于2的幂次方分频。但如果你用复位法或者置位法做非2的幂次方分频占空比通常不是50%。比如用模10计数器最高位输出在0-4时为05-9时为1占空比正好50%。但如果你用模6计数器最高位输出在0-2时为03-5时为1占空比也是50%。实际上对于任何偶数模计数器最高位输出的占空比都是50%。但对于奇数模计数器最高位输出的占空比就不是50%了。如果你需要奇数分频且占空比50%就必须用前面提到的双边沿方法。或者用PLL倍频后再分频但那样成本高。5.4 高速计数器的中断程序怎么写有人搜“smart200高速计数器ch0中断程序”这是PLC领域的问题。高速计数器的中断程序和普通中断程序的区别在于高速计数器的中断频率可能非常高如果中断服务程序太长会导致CPU来不及响应下一次中断。写高速计数器中断程序的要点中断服务程序尽量短只做必要的操作比如更新一个标志位或者累加一个变量不要在中断里做浮点运算或者复杂的逻辑判断如果需要在中断里做大量数据处理可以用DMA或者把数据存到缓冲区在主循环里处理注意中断优先级高速计数器中断通常要设为最高优先级在西门子S7-200 SMART中高速计数器的配置步骤在系统块中使能高速计数器选择计数器模式单相、双相、AB相设置预设值和当前值使能中断选择中断事件比如计数等于预设值时触发在中断程序中编写处理逻辑注意高速计数器的输入滤波时间会影响最高计数频率。如果滤波时间设得太长高频脉冲会被滤掉。在S7-200 SMART中可以在系统块里调整输入滤波器的响应频率。6. 几个可以直接复现的实操方案6.1 用CD4017做10路流水灯CD4017是一个十进制计数器/分频器内部有5级约翰逊计数器输出是10个译码后的信号。用它做流水灯特别简单时钟输入接555振荡器复位引脚接低电平10个输出分别接LED串联限流电阻时钟使能引脚接低电平555产生方波CD4017每个时钟周期切换一个输出LED就依次点亮。调节555的频率就能改变流水速度。555的振荡频率计算公式f 1.44 / ((R1 2R2) * C)。比如R11kR210kC10uF频率大约是1.44/(210000.00001) ≈ 6.86Hz。这个速度看起来就是流水灯的效果。6.2 用74HC161设计一个模60计数器模60计数器可以用两片74HC161级联实现一片做个位模10一片做十位模6。个位计数器用复位法当计到1010时复位。十位计数器的时钟接个位的进位输出RCO。当个位从9变0时RCO产生一个脉冲十位加1。十位计到6时复位。具体接线个位74HC161的CEP和CET接高电平PE接高电平MR接复位逻辑个位的RCO接到十位的CEP和CET十位的MR接复位逻辑当十位计到0110时复位复位逻辑用与非门实现个位的复位信号是Q3 AND Q11010十位的复位信号是Q2 AND Q10110。6.3 用Verilog实现一个频率计频率计的核心是一个计数器和一个闸门信号。闸门信号通常是一个1秒的脉冲在闸门期间计数器对被测信号计数计数值就是频率。module freq_counter( input clk, // 系统时钟比如50MHz input rst_n, input sig_in, // 被测信号 output reg [31:0] freq ); reg [25:0] gate_cnt; reg gate; reg [31:0] cnt; // 产生1秒闸门 always (posedge clk or negedge rst_n) begin if (!rst_n) begin gate_cnt 0; gate 0; end else begin if (gate_cnt 26d49_999_999) begin gate_cnt 0; gate ~gate; end else begin gate_cnt gate_cnt 1b1; end end end // 在闸门期间计数 always (posedge sig_in or negedge rst_n) begin if (!rst_n) cnt 0; else if (gate) cnt cnt 1b1; end // 闸门结束时锁存结果 always (posedge clk or negedge rst_n) begin if (!rst_n) freq 0; else if (gate_cnt 26d49_999_999 gate) freq cnt; end endmodule这个频率计的精度取决于系统时钟的精度。如果用50MHz晶振闸门时间的误差在20ns以内对于大多数应用足够了。实操心得做频率计的时候被测信号的幅度和直流偏置很关键。如果信号幅度太小计数器可能不响应如果偏置不对可能一直在计数或者完全不计数。建议在输入端加一个比较器把信号整形成标准的方波。另外如果被测信号频率很高超过了系统时钟频率就需要用预分频器先降频。7. 一些教科书上不会写的经验触发器、计数器、分频器这些东西理论看起来不难但实际做的时候坑特别多。我总结几条自己踩过的坑第一永远不要相信仿真。仿真通过不代表实际电路能工作。仿真里没有寄生电容、没有电源噪声、没有信号完整性问题。我见过太多次仿真完美但实际板子一塌糊涂的案例。有条件的话关键电路一定要实际搭出来测。第二电源去耦不是可有可无的。每个芯片的电源引脚旁边都要放一个0.1uF的陶瓷电容越近越好。高速电路还要加10uF的钽电容。电源噪声是导致触发器误动作的头号原因而且很难排查。第三时钟信号要当大爷供着。时钟走线要尽量短、尽量直不要靠近其他信号线。如果时钟频率超过10MHz最好用专用的时钟缓冲芯片不要直接用FPGA或者单片机的IO口驱动长走线。第四复位电路要可靠。很多莫名其妙的故障都是复位不干净导致的。建议用专用的复位芯片比如MAX809或者IMP811不要用简单的RC复位。RC复位在电源缓慢上升的时候可能产生多次复位脉冲。第五测试点要留够。调试的时候你会发现没有测试点就像闭着眼睛修车。关键信号时钟、复位、计数器输出都要引出测试点方便用示波器或者逻辑分析仪观察。最后说一个我自己的习惯每次设计完电路我都会在脑子里过一遍“如果这个信号有毛刺会怎样”、“如果这个时钟延迟了会怎样”、“如果电源掉了一下会怎样”。这种“悲观设计法”帮我避免了很多潜在问题。数字电路不像模拟电路那样有渐进失效它要么正常工作要么完全乱套。所以设计的时候一定要把边界条件考虑清楚。