数字电路这门课,很多人学完就忘,原因不复杂:课本上讲完卡诺图、触发器、时序逻辑,紧接着就是一道"用与非门设计一个交通灯"的习题,做完也不知道自己到底造了个什么东西。我当年也是这样,直到有一次动手用分立逻辑门搭了一个能真正算数的东西——一个十进制四位加减乘除计算器,全部逻辑门加起来控制在500个以内。那个过程里踩的坑、想明白的事,比看十遍教材都管用。
这篇内容就是把这个项目从头到尾拆开讲。它适合两类人:一类是正在学数字逻辑、想找个能落地的项目把知识串起来的学生;另一类是对底层电路有好奇心、想搞明白"计算机到底怎么算数"的爱好者。核心关键词是逻辑门、十进制运算、四位计算器、加减乘除、BCD编码、组合逻辑、时序控制。我会讲清楚为什么选十进制而不是二进制、500个门这个预算怎么分配、加减乘除各自的电路怎么设计、显示和按键怎么处理,以及实测中那些文档里不会写的坑。
先说结论性的判断:这个项目真正的难点不在"算",而在"表示"和"控制"。十进制运算意味着你要处理BCD编码的进位修正,四位意味着你要做级联,加减乘除意味着你要在有限的门的预算里复用同一套加法器。把这三件事想通,剩下的就是耐心接线和调试。
1. 为什么是十进制四位,而不是二进制
1.1 十进制带来的表示成本与直觉收益
用二进制做计算器是最省门的做法,这一点毫无疑问。一个四位二进制加法器用74系列的话一片就够,进位链也简单。但问题在于人机接口:你要么在输入输出时做二进制与十进制的转换,要么让用户直接面对二进制。前者需要额外的转换电路,后者对使用者不友好。
十进制(BCD,Binary-Coded Decimal)的思路是每一位十进制数用四位二进制表示,0到9对应0000到1001,剩下1010到1111六个状态是非法码。这样做的好处是显示驱动极其直接——每个BCD码直接送七段译码器就能点亮数码管,不需要任何转换。代价是运算时要处理"逢十六进一"和"逢十进一"的差异,也就是所谓的BCD修正。
我选十进制,核心原因是这个项目的目标是"看得懂、算得对、显示直观",而不是追求门数极限。四位十进制能表示0000到9999,做加减乘除的演示完全够用,溢出边界也容易观察。如果做八位,门的预算立刻翻倍,500个门根本打不住。
1.2 四位字长的溢出边界与设计取舍
四位十进制最大9999,这个范围决定了运算的边界条件。加法超过9999要给出溢出指示,减法出现负数要处理借位或者用补码表示,乘法两个四位数相乘最大是9999×9999,结果接近一亿,八位都装不下,所以乘法必须做结果截断或者只允许较小操作数。除法同理,商和余数的位宽要提前规划。
这些边界不是缺陷,反而是教学设计上的优点——它逼着你在设计阶段就想清楚数据通路。我的处理方式是:加减法用四位全宽,溢出用最高位的进位标志指示;乘法限制操作数在两位十进制以内(0到99),结果四位显示;除法用被除数四位、除数两位,商两位、余数两位。这样500个门的预算才分配得开。
提示:位宽规划一定要在画第一根线之前定死。我见过有人边搭边改位宽,最后进位链全乱,返工的成本远大于前期多想半小时。
1.3 500个门的预算怎么切
500个逻辑门听起来不少,但分摊到加减乘除四套功能上就很紧张。我的分配大致是这样的:输入寄存与按键消抖约60门,BCD加法器核心约80门,BCD修正逻辑约50门,减法借位与补码转换约60门,乘法移位累加控制约90门,除法移位减法控制约90门,显示多路选择与译码约50门,控制状态机约20门。加起来正好卡在500附近。
这个预算逼出了几个关键决策:加法器必须复用,减法和乘法、除法都建立在同一个BCD加法器之上;显示必须用多路扫描而不是每位独立译码,否则译码器数量翻四倍;控制逻辑必须用状态机而不是纯组合逻辑,否则门的数量失控。这些决策后面会逐一展开。
2. BCD加法器:整个计算器的心脏
2.1 四位二进制加法与BCD修正的原理
BCD加法器的做法是"先当二进制加,再修正"。以一位BCD为例,两个BCD数字相加,结果范围是0到18。如果直接用四位二进制加法器加,当结果大于9时,二进制结果和正确的BCD结果之间差了6(因为四位二进制逢16进一,而十进制逢10进一,16-10=6)。所以修正规则是:如果二进制加法的和大于9,或者产生了进位,就给结果加6,同时向高一位进位。
这个"+6"不是随便来的,它是两种进制基数之差。理解这一点,后面所有修正逻辑都是自然推导。具体判断条件可以写成:若S3·S2 + S3·S1 + Cout为真,则需要修正。这里的S3、S2、S1是二进制和的高三位,Cout是进位输出。这个布尔表达式对应大约五六个门,是修正逻辑的核心。
2.2 一位BCD加法单元的完整门级实现
一位BCD加法单元我拆成三级:第一级是两个四位二进制加法(可以用超前进位或者行波进位,行波省门但慢,这个项目速度不敏感,选行波);第二级是修正判断,用上面那个布尔式;第三级是条件加6,用一个四位加法器把修正值0000或0110加上去。
行波进位的四位加法器,每位是一个全加器,全加器用两个异或门、两个与门、一个或门实现,大约5个门,四位就是20门,加上修正判断6门,条件加6又是一个四位加法器20门,一位BCD加法单元大约46门。四位级联就是184门,这已经超出我预算里加法器核心的80门了。
所以实际做法要优化:修正判断可以共享,条件加6可以用半加器简化(因为加的是常数0110,最低位加0等于不变,次低位加1可以用异或,高位加1要考虑进位传播)。优化后一位BCD加法单元能压到30门左右,四位约120门,再和减法复用,预算才勉强够。这个优化过程本身就是很好的组合逻辑练习。
2.3 级联时的进位传递与延迟
四位BCD级联时,低位的进位输出直接接高位的进位输入。行波进位的问题是延迟累积,四位十进制就是十六级全加器的延迟串联。在几十赫兹的按键操作下这完全不是问题,但如果要做连续运算或者高频扫描,就得考虑超前进位。
我实测下来,用行波进位在按键触发、结果稳定的场景下没有任何可见问题。真正需要注意的是进位链上的毛刺——当多个位同时变化时,中间状态可能短暂出现错误的进位。解决办法是在加法器输出后加一级寄存器,用时钟锁存,等信号稳定后再送显示。这一级寄存器大约16个触发器,要算进预算。
3. 减法、乘法、除法如何共用一套加法通路
3.1 减法转补码:借位就是加补数
减法最省门的做法是转成加法。A减B等于A加B的补码。在十进制里,补码的概念是"对10的幂取补",四位十进制就是A + (10000 - B),结果的低四位就是差,最高位的进位取反就是借位标志。求10000减B可以用"每位对9取补再加1"实现,每位对9取补就是一个四位减法器或者用异或加修正,成本不高。
这里有个容易踩的坑:BCD的取补不是简单按位取反。二进制取反加一得到补码,但BCD每位是0到9,对9取补才是正确操作(9减该位)。我一开始按二进制补码做,结果算出来全是乱码,排查了半天才反应过来是编码体系不同。对9取补可以用一个小的查找逻辑或者9减该位的减法器实现。
3.2 乘法:移位加与部分积的累加
乘法用移位加。两个两位数相乘,把乘数逐位取出,每一位与被乘数相乘得到一个部分积,部分积按位左移后累加。由于是十进制,每次"乘一位"实际上是0到9倍的被乘数,可以用查表或者重复加法实现。重复加法最省门但慢,查表需要存储,折中方案是用被乘数的1倍、2倍、4倍、8倍通过移位和加法组合出任意倍数。
控制部分是一个小状态机:取出乘数最低位、判断、累加、整体右移乘数、左移部分积、循环。两位数乘法最多循环两轮(每位十进制),每轮内部处理0到9的倍数。这个状态机大约20个门加几个触发器,是乘法模块的控制核心。
3.3 除法:移位减与余数恢复
除法是乘法的逆过程,用移位减。被除数逐位左移进入余数寄存器,每次比较余数与除数,够减就减并置商位为1,不够减就置0。十进制下每次移入一位,比较和减法是BCD运算。恢复余数法的变种是不恢复,直接在下一次移位时补偿,能省一些门。
除法的状态机比乘法稍复杂,因为要处理"试减失败"的分支。我的做法是先减,看借位标志,如果借位说明不够减,再把除数加回去恢复余数。这个"减了再加回"的操作多花一个加法周期,但逻辑简单,门的开销小,符合500门的预算约束。
3.4 共用加法器的总线设计与冲突避免
加减乘除共用一套BCD加法器,关键是设计一条结果总线和一个多路选择器,把不同运算的操作数送到加法器输入端。多路选择器每位需要几个门,四位十进制就是十六位宽的选择,成本要算清楚。冲突避免靠控制状态机保证同一时刻只有一个运算模块占用加法器,这要求状态机有明确的"占用"信号。
我踩过的坑是:最初没做总线仲裁,减法和乘法同时触发时加法器输入打架,结果完全随机。后来加了一个简单的互斥锁信号,任何运算开始前先检查加法器是否空闲,才彻底解决。这个教训说明,即使是小项目,资源竞争的问题也会真实出现。
4. 输入、显示与控制:被低估的半壁江山
4.1 按键消抖与输入寄存
机械按键的抖动是数字电路项目的经典坑。按下瞬间触点会在几毫秒内反复通断,如果不消抖,一次按键会被识别成好几次。消抖方案有两种:硬件RC加施密特触发器,或者软件(这里是逻辑)计数消抖。我用的是计数器消抖:检测到按键变化后启动一个计数器,连续若干个时钟周期电平稳定才确认,大约需要十几位计数器和几个门。
输入寄存用D触发器锁存当前操作数,四位十进制就是十六个触发器。这里要注意的是输入顺序——用户先按的位应该进入高位还是低位,要有明确的移位寄存器逻辑。我的做法是每次输入左移,新数字进最低位,符合计算器习惯。
4.2 七段译码与多路扫描显示
七段译码器把BCD码转成段码,0到9各对应一组七位输出。如果用专用译码芯片当然简单,但为了控制门数,我用组合逻辑实现,每个段是一个关于四位BCD输入的小布尔函数,用卡诺图化简后每段大约三到五个门,七段约25门。四位独立译码就是100门,太贵。
所以显示用多路扫描:一个译码器轮流为四位服务,靠位选信号快速切换,利用人眼视觉暂留。这样译码器只要一份,加上位选扫描计数器(两位)和段驱动,总共约40门。扫描频率要足够高,一般几百赫兹以上看不出闪烁,但也不能太高以免驱动能力不足。
4.3 控制状态机:运算流程的调度者
整个计算器的行为由一个状态机调度:等待输入、接收操作数、选择运算、执行、显示结果、等待下一次。状态数不多,大约六到八个状态,用三到四个触发器加组合逻辑译码实现,20门左右。状态机的设计要点是每个状态的进入条件和退出条件要写清楚,尤其是运算执行状态要等加法器结果稳定后才能跳转。
我建议把状态图画在纸上再动手,因为状态机的错误最难调——它不像组合逻辑那样能一眼看出对错,往往表现为"偶尔算错"或者"卡住不动"。有了状态图,配合几个指示LED观察状态跳转,排查效率高很多。
5. 实测中的坑与调试方法
5.1 毛刺、竞争与时钟域问题
组合逻辑的毛刺在多位同时变化时几乎必然出现。BCD修正逻辑里,判断条件依赖多个输入,输入变化的先后会导致判断信号短暂抖动,进而让条件加6误触发。解决办法一是加寄存器打拍,二是用格雷码或者一次性编码减少同时变化的位数。我最终是在修正判断后加了一级触发器,用系统时钟同步,毛刺被彻底滤掉。
时钟域问题在小项目里也值得警惕:按键消抖用的慢时钟和运算用的主时钟如果不同步,跨域信号会亚稳态。简单做法是全部用同一个时钟,消抖靠计数器分频而不是独立时钟源,这样就没有跨域问题。
5.2 用LED和逻辑笔做分段验证
500个门的电路不可能一次搭对。我的调试策略是分段验证:先单独验证BCD加法器,用拨码开关给输入,LED看输出,确认0到9加0到9全对;再验证减法、乘法、除法各自独立工作;最后联调。每段验证通过再往下走,问题定位范围就小。
逻辑笔是排查单点电平的好工具,比万用表快。对于总线信号,用一排LED显示十六位状态,能直观看到数据流动。这些土办法在门级调试里比任何高级工具都实用。
5.3 门数超标的取舍与优化顺序
实际搭下来很容易超预算。优化的优先级我总结为:先砍显示(多路扫描代替独立译码),再砍运算(共用加法器),最后砍控制(状态机合并状态)。不要反过来先砍控制,因为控制逻辑一旦简化过度,行为会变得难以预测,调试成本反而上升。
还有一个省门的技巧是用与非门和或非门作为通用门,因为实际芯片里与非门最便宜也最常用。把与、或逻辑都转成与非实现,物料成本能降不少,但可读性会变差,建议在原理图阶段用标准符号,落地时再转换。
6. 从门级到系统:这个项目真正教会我的事
搭完这个计算器,我对"抽象层次"有了切身体会。同样一个加法,在门级是进位链和修正逻辑,在寄存器级是一条数据通路,在指令级是一条ADD指令。层次越高越省心,但越高也越容易忘记底下发生了什么。这个项目逼着我在最低层把每一件事想清楚,之后再去看任何处理器架构,那些"为什么要有ALU""为什么要流水线"的问题都有了具体的答案。
另一个收获是关于预算和取舍。500个门是一个硬约束,它让我明白工程不是把所有功能都做满,而是在约束下找到能工作的最小方案。乘法限制操作数位宽、除法用恢复余数、显示用扫描,这些都是约束逼出来的设计,而不是教科书上的标准答案。真实工程里,约束往往比理论更能塑造方案。
如果你也想动手,我的建议是从一位BCD加法器开始,把它彻底调通,再逐步加位、加功能。不要一上来就画四位的完整图,那样错误会淹没你。每加一个模块就验证一次,让电路始终处于"已知正确"的状态。这个习惯,比这个项目本身更有价值。