1. 从零开始用两片74161搭建一个六十进制计数器大家好我是老张一个在数字电路和EDA工具里摸爬滚打了十多年的硬件工程师。今天想和大家聊聊一个非常经典但又特别容易踩坑的电路设计——用两片74161同步计数器芯片来搭建一个六十进制计数器。这个电路是很多数字钟、计时器项目的核心听起来简单但真做起来尤其是用EDA工具仿真时波形图上蹦出来的那些“毛刺”和“异常”能让你怀疑人生。我当年第一次做这个实验就被折腾得够呛明明逻辑看起来天衣无缝怎么一跑仿真计数到59之后跳回00的瞬间波形就跟抽风了一样如果你也遇到了类似问题别急这几乎是每个硬件新手的“必修课”。今天我就把自己从发现问题、分析波形到最终稳定优化的全过程掰开揉碎了讲给你听保证你看完就能上手避开我当年踩过的那些坑。我们先来搞清楚我们要做什么。74161是一颗经典的4位二进制同步计数器它本身是十六进制的0-15。要做一个六十进制的计数器也就是计数从00到59我们需要两片74161。一片负责计数的个位0-9十进制另一片负责计数的十位0-5六进制。个位的芯片我们需要把它改造成一个十进制计数器这通常利用它的异步清零端LDN低电平有效或者同步置数端来实现。当个位计到9二进制1001的下一个时钟脉冲我们产生一个信号让个位清零同时这个信号作为十位芯片的计数使能让十位加1。当十位计到5二进制0101且个位计到9时表示59到了下一个时钟脉冲两片芯片应该同时清零回归00。这个思路非常直观对吧用Multisim、Proteus或者Vivado、Quartus这类EDA工具画好原理图设置好反馈逻辑满怀期待地点下仿真运行……然后你可能就会看到让你头皮发麻的波形。2. 诡异的波形当计数器在59到00之间“卡顿”我记得特别清楚第一次仿真的时候我盯着波形显示器看着计数器的输出Q3Q2Q1Q0从0000一路稳稳地走到10019个位清零十位加一一切正常。当计数走到59也就是十位是0101个位是1001时我心里默念“关键帧来了”。时钟上升沿一到我预期会看到十位和个位所有的输出线都“唰”地一下变成0。但实际波形呢个位的输出确实归零了但十位的输出有那么几个纳秒的时间它似乎“犹豫”了一下。比如十位的Q1代表2那条线在清零瞬间并没有立刻变成低电平而是先出现了一个极窄的高电平脉冲就像一根细小的毛刺。更诡异的是有时候仿真会显示计数器短暂地跳到了一个非法的状态比如“60”或者“6A”然后才恢复正常。整个从59到00的过渡波形看起来拖泥带水一点也不干净利落。这种波形异常在数字电路里是大忌。它意味着电路存在潜在的竞争与冒险。在实际的FPGA或电路板上这种毛刺可能会导致后续的译码电路比如驱动数码管出现瞬间的错误显示或者触发不应该触发的后续逻辑。如果这是个产品那就是一个严重的bug。所以我们绝对不能对仿真器里的这些“红叉叉”或“异常跳变”视而不见必须深挖到底。当时我的第一反应是检查连接和电源但这是在EDA仿真环境里排除了物理接触不良的可能。那么问题一定出在我们的逻辑设计本身。2.1 深入分析对比“实际”与“预期”的真值表要定位问题最有效的方法之一就是抛开复杂的波形图回归到最本质的逻辑关系——真值表。我们自己心里有一个“预期的真值表”当前状态是59十位5个位9下一个时钟沿到来状态变为00十位0个位0。但EDA工具内部计算出来的“实际的真值表”或者说状态转换可能和我们想的不一样。我们来仔细看看在59那个关键时刻各个控制引脚的状态。以最常见的利用异步清零端LDN来实现归零的方案为例。我们通过一个与非门来检测59状态当十位是01015个位是10019时与非门输出低电平这个低电平同时连接到两片74161的LDN引脚上。根据74161的数据手册LDN是异步清零低电平有效。只要LDN0无论时钟和使能端是什么状态输出会立即被清零设为0000。同时我们一般会把计数使能端EN或叫CEP、CET接高电平让其一直处于允许计数状态。那么在时钟上升沿来临的瞬间逻辑是这样的检测电路看到59立即使LDN0。按照我们的“预期”芯片会无视其他一切立刻清零。但这里隐藏了一个关键冲突使能端EN的状态。在我们的简单连接里EN很可能被固定接为高电平EN1表示永远允许计数。而74161的工作时序是在时钟上升沿如果EN1它本应执行“计数”操作。现在同一个时钟沿你既给了它一个“清零”LDN0的命令又给了它一个“计数”EN1且时钟有效的条件。对于芯片内部的逻辑门电路来说这就相当于两个互相矛盾的指令同时到达它到底该听谁的这种信号间的“竞争”就导致了输出结果的不确定性和波形上的毛刺。那个短暂出现的非法状态可能就是计数器在“纠结”中短暂执行了一下计数操作比如从59变成60然后异步清零信号才“压倒”计数操作将其强行拉回00。由于门电路的延迟各不相同这个竞争过程就被波形图捕捉了下来。3. 问题根源使能端EN与清零信号LDN的“打架”上面我们提到了问题的核心矛盾清零指令和计数指令在同一个时钟沿发生了冲突。让我们把这个冲突再具象化一点。你可以把74161想象成一个非常听话但有点死板的工人。EN是他的工作许可开关LDN是一个红色的紧急停止按钮。正常计数时工作许可EN1开关打开每次时钟钟声上升沿一响他就按照规则加一个数。紧急按钮LDN1没按下一切正常。到达59时我们想清零我们的逻辑电路在钟声响起前的一刹那按下了紧急停止按钮LDN0。我们的“预期”是工人一听到钟声发现按钮被按下了就立刻把手头所有工作扔掉把计数器归零。但实际的冲突在于在钟声响起的那一精确时刻工人的眼睛既看到了被按下的红色按钮LDN0也看到了打开的工作许可开关EN1。他的操作手册上可能写着“如果按钮被按下立刻停止一切并归零”。但同时另一条规则又写着“如果许可开关打开且钟声响起就进行计数”。这两条规则在时序上产生了竞争。由于芯片内部从输入到输出的信号传递有微小的延迟称为传输延迟负责“计数”的逻辑路径和负责“清零”的逻辑路径它们的延迟时间可能略有不同。如果“清零”路径跑得快一点输出就立刻归零如果“计数”路径侥幸快了一点点输出就会先变成60然后才被后续到来的清零信号拉回0。在仿真中我们就能看到那个代表“60”的非法状态或者仅仅是一个毛刺。这就是所谓的“异步清零信号与同步计数使能之间的竞争冒险”。我最初的设计就是掉进了这个坑里。我只考虑了用59这个状态去触发清零却完全忽略了使能端EN在这个关键瞬间应该扮演什么角色。让EN在清零时刻依然为“1”相当于在命令工人“停下”的同时又给了他一个“前进”的模糊指令不出问题才怪。4. 优化实践为清零操作添加“EN约束”找到了病根开药方就清晰了。我们的目标是在需要执行清零操作的那个特定的时钟周期内消除“计数”这个竞争指令。也就是说当电路判断到当前状态是59下一个时钟沿要清零时我们不仅要让LDN0还要同时让EN0。这样在时钟上升沿到来的时刻工人74161看到的情况是红色紧急按钮被按下LDN0而且工作许可开关也被关闭了EN0。操作手册的逻辑就变得唯一且明确既然没有工作许可就不存在“计数”这个选项同时紧急按钮被按下所以执行清零。竞争消失了。这就是原文中那句非常精炼的改进方案“为清零时添加EN的约束即EN1时才能进行计数操作”我更倾向于表述为EN0时才能进行清零操作或者清零时必须确保EN0。我们如何用电路实现这个“约束”呢方法不止一种这里我分享两个最常用、最稳定的设计。4.1 方案一巧用使能端实现门控这个方案非常直观且只需增加一个逻辑门。我们回顾一下之前的错误连接两片74161的EN端通常直接接高电平VCC。现在我们把它改一下。个位芯片的EN可以保持不变接高电平或者由前级电路控制。十位芯片的EN它原本接的是个位芯片的进位输出当个位从9归零时产生一个脉冲。现在我们对其进行改造。关键改动将那个检测59状态的信号即连接到两片芯片LDN端的那个低电平信号取反。当不是59状态时这个取反后的信号为高电平当是59状态时它为低电平。然后将这个取反后的信号与个位芯片产生的原始进位信号进行一个“与”操作AND结果再作为十位芯片的EN。逻辑结果当计数在0-58之间时59检测信号为高取反后为低经过与门后十位EN0不对这里需要仔细设计。更常见的做法是直接用59检测信号低有效来控制一个门电路在非59状态时允许进位信号通过在59状态时封锁进位信号。例如将个位的进位信号和59检测信号的“非”相与。这样在59状态时59检测信号为低其“非”为高但此时个位是9不会产生进位脉冲所以十位EN仍为0。当下一个时钟沿到来状态要从59变00时由于十位EN0芯片不会执行计数只执行LDN0触发的异步清零完美规避竞争。这个方案的好处是改动小逻辑清晰。在EDA工具里你只需要在原理图上增加一个反相器和一个与门重新连接一下线然后再次仿真。你会惊喜地发现波形图上59到00的那次跳变变得干净利落所有的毛刺和非法状态都消失了。4.2 方案二基于状态机的同步清零思路方案一主要解决了异步清零下的竞争问题。这里我再提一个更现代、在FPGA设计中更常用的思路——同步清零。74161本身也有同步置数功能我们可以利用它来实现同步清零这能从根本上避免异步信号带来的时序烦恼。改变反馈逻辑我们不再使用LDN端而是使用它的同步置数端LOAD假设低有效和数据输入端D0-D3。检测状态同样用逻辑门检测59状态。执行清零当检测到59状态时将LOAD置为有效低电平并将数据输入端D0-D3全部设置为0。同时确保EN在此时为1允许操作。工作原理在时钟上升沿到来时芯片看到LOAD0且EN1它就不会执行计数而是执行“同步置数”操作将数据输入端的值0000载入到输出端。由于这个操作是与时钟同步的所有输出端的变化都严格在时钟边沿之后并且几乎是同时的取决于内部逻辑延迟因此不会产生竞争冒险波形非常干净。在像Vivado这样的高级EDA工具中使用同步设计是黄金准则。你可以用硬件描述语言如Verilog轻松实现这个逻辑always (posedge clk) begin if (reset) begin count_ten 4‘b0000; count_one 4’b0000; end else if (count_ten 4‘b0101 count_one 4’b1001) begin // 检测59 count_ten 4‘b0000; // 同步清零十位 count_one 4’b0000; // 同步清零个位 end else begin // ... 正常的计数逻辑 ... end end这段代码描述的就是一个标准的同步六十进制计数器其中清零操作是时钟沿触发的同步行为稳定且可靠。5. EDA仿真验证与调试技巧理论分析和电路改进之后我们必须回到EDA仿真环境中进行严格的验证。这里我以一些常用工具为例分享几个关键的调试技巧。首先仿真参数的设置至关重要。很多新手跑仿真只看结果不看设置。对于这类涉及竞争冒险的时序问题你一定要确保时间精度足够高在仿真设置里将最小时间步长Time Step设置为皮秒ps级别。默认的纳秒ns步长可能会掩盖那些只有几十皮秒宽的毛刺。使用高分辨率波形查看器放大59到00转换的那一段波形仔细观察每一根信号线的变化。使用游标Cursor测量关键信号如LDN、EN、时钟CLK之间的时间差。其次学会使用真值表或状态视图。大多数EDA工具除了波形图还提供列表视图可以像表格一样列出每个时钟沿前后所有信号的值。对比这个“实际仿真真值表”和你手画的“预期真值表”能非常精准地定位是哪一步出现了偏差。比如你可能会看到在状态59之后出现了一个短暂的“5A”或“60”状态这直接印证了竞争的存在。最后进行时序分析。如果你的EDA工具支持静态时序分析STA可以跑一下看看有没有建立时间/保持时间的违规。虽然对于这个简单的计数器可能不是必须但养成这个习惯对复杂设计大有裨益。在改进后的电路仿真中你应该关注清零信号无论是异步还是同步的路径延迟是否合理确保它在下一个时钟沿到来之前有足够的时间稳定传播到所有触发器。当我将“添加EN约束”的方案应用到原理图中重新运行仿真后波形图给了我一个完美的回应。计数器从00到59稳步递增在最后一个时钟沿所有输出信号同时干净地跳变为0没有任何毛刺或中间状态。那个曾经让人心烦意乱的“问题波形”终于被驯服了。这个优化实践虽然只增加了一两个逻辑门但它体现的是数字电路设计中一个非常重要的思想确保控制信号在关键操作时刻的无矛盾性和确定性。无论是异步清零还是同步置数都要仔细考虑使能信号、时钟信号和数据信号之间的时序关系避免任何可能的竞争条件。这次从74161波形异常到稳定清零的优化过程让我深刻体会到EDA工具不仅是画图布线的帮手更是验证设计思想、暴露潜在问题的“照妖镜”。一个在纸上逻辑完美的设计必须在仿真中经受住时序的考验。下次当你设计计数器、状态机或者任何带反馈的逻辑电路时如果仿真波形出现了不合预期的跳动不妨先检查一下在状态转换的临界点所有的控制信号是否都给出了明确且唯一的指令很多时候答案就藏在这些信号的约束关系里。