很多学数字电路的朋友,一开始接触的都是分立的小规模逻辑门,比如74LS系列里的与非门、或非门、反相器。那时候觉得挺简单,一个芯片里就那么几个门,连线也不复杂。可一旦开始做课程设计、参加竞赛,或者接触稍微复杂一点的工程板卡,就会发现事情没那么简单了:一个功能模块动不动就要处理十几路信号、几十种状态,如果还抱着"一个功能画一片原理图"的老思路,板子会越画越乱,时序收敛更是遥遥无期。这时候,用排列组合的思路去重新审视逻辑器件的选型和连接方式,就成了一个非常实用的突破口。
我当年第一次认真琢磨这个主题,是在做一个多路数据采集板的时候。板卡上要同时处理8路模拟开关的通道切换、3组编码器的正交脉冲计数、还有一大堆状态指示LED。如果每个功能都用独立的逻辑芯片堆出来,光是器件数量就够让人头疼的。后来我换了个思路:先把需求抽象成真值表,再按排列组合的规律去合并、化简、复用逻辑资源,一下子就把设计简化了大半。这篇文章就把我这些年在这条路上攒下的经验做个系统梳理,从基本原理到器件选型,再到具体的组合设计案例和调试心得,一次说清楚。
1. 为什么"排列组合"是数字电路设计的隐藏主线
很多人看数字电路教材,开篇就是布尔代数、逻辑门电路,然后就是组合逻辑、时序逻辑。但教材里很少直说一个底层道理:数字电路里所有的组合逻辑,本质上都是在做排列组合的映射——输入信号的全排列,对应输出信号的全组合。你设计的每一张真值表,其实就是在穷举输入端所有可能的取值组合,然后为每一种组合指定输出电平。
1.1 排列组合与真值表的对应关系
先看一个最简单的例子:两个输入A和B,每个输入只有0和1两种状态,那么它们的组合一共有2²=4种。这就是排列组合里的乘法原理——每个输入自由变化,整体组合数是各输入状态数的乘积。
放在数字电路里,这个关系可以写成一个很直白的公式:n个输入变量,对应的全部输入组合数为2ⁿ种。这2ⁿ种组合恰好构成一张完整真值表的行数。比如:
- 2个输入:4行真值表,对应4种排列组合
- 3个输入:8行真值表,对应8种排列组合
- 4个输入:16行真值表,对应16种排列组合
也就是说,排列组合是描述组合逻辑输入空间的一种数学语言。你列的每一行真值表,都是对输入端某个具体排列组合状态的响应定义。不把这条主线理清楚,后面设计多输入逻辑函数时就容易挂一漏万——漏一项,功能就错了。
1.2 从组合数理解逻辑器件的"门数上限"
排列组合还决定了另一件重要的事:一个逻辑函数的复杂程度,由输入组合数决定。输入变量越多,真值表越庞大,对应的逻辑表达式就越长,需要的逻辑门就越多。这直接影响器件选型。
一个3输入的函数,理论上最多需要8个最小项;而一个6输入的函数,最小项数就膨胀到64个。你要是全用4输入与非门去搭,先不谈门数,光连线就可能把人绕晕。这也是为什么工程上超过4输入的组合逻辑,一般不建议用分立门电路去拼——排列组合的数量爆炸,让手工设计变得不现实,需要换用译码器、数据选择器、可编程逻辑器件等更高抽象层次的元件。
从这个角度看,排列组合不仅是一个题目里计算概率的公式,更是数字电路设计中的工程边界判断工具。先数一数输入变量的个数,再算一算组合数,你大概就能判断这个逻辑该用什么级别的器件了。
2. 用最小项展开式拆解任意逻辑需求
不管需求多复杂,组合逻辑设计的正规流程是先做逻辑抽象、列真值表、写逻辑函数、化简,最后映射到具体器件。这个流程的每一步,都在跟"排列组合"打交道。
2.1 从文字需求到真值表
举个例子,假设要设计一个三人表决器:3个输入A、B、C,输出Y,当2个或2个以上输入为1时输出1。这个需求听起来很生活化,但落到电路上,第一步就是把所有输入组合列全:
| A | B | C | Y | 说明 |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0人同意 |
| 0 | 0 | 1 | 0 | 1人同意 |
| 0 | 1 | 0 | 0 | 1人同意 |
| 0 | 1 | 1 | 1 | 2人同意 |
| 1 | 0 | 0 | 0 | 1人同意 |
| 1 | 0 | 1 | 1 | 2人同意 |
| 1 | 1 | 0 | 1 | 2人同意 |
| 1 | 1 | 1 | 1 | 3人同意 |
注意,这张表的行数正好就是2³=8种排列组合,一种不多一种不少。很多新手容易犯的错误是:只列自己关心的几种输入状态,比如"101输出1,110输出1,111输出1",剩下的组合不写,或者写了但对不齐,最后逻辑函数一化简就出错了。
我的习惯是,列真值表时永远从000开始,按二进制递增的顺序一直列到全1,像数数一样把每一个排列组合都列出来。这个方法笨,但最可靠,能保证输入空间一个不漏。
2.2 最小项与标准与或式的写法
真值表列好之后,接下来是把输出为1的那些行对应成最小项。三个变量的最小项有8个,分别是:
- m0 = A'B'C'(ABC=000)
- m1 = A'B'C(ABC=001)
- m2 = A'BC'(ABC=010)
- m3 = A'BC(ABC=011)
- m4 = AB'C'(ABC=100)
- m5 = AB'C(ABC=101)
- m6 = ABC'(ABC=110)
- m7 = ABC(ABC=111)
所谓最小项,就是包含了所有输入变量的一个与项,每个变量以原变量或反变量的形式出现且只出现一次。而最小项这个名字的来历,跟排列组合也有关:n个变量能组成的所有这样的与项,正好是2ⁿ个,每个与项对应着输入排列组合中的一种。
上面表决器的例子,Y为1的行是m3、m5、m6、m7,所以标准与或式就是:
Y = A'BC + AB'C + ABC' + ABC
这个式子可以直接用与门和或门搭出来,但显然不够简洁。这时候就要用到卡诺图化简,一种把几何相邻关系转化成逻辑化简的工具。排列组合在这里的妙处是:卡诺图的格子排列其实精心设计过,相邻格子的最小项只有一位变量不同。正因为有这个排列上的特性,你才能圈出那些可以合并、消掉变量的相邻最小项。
2.3 卡诺图化简的本质是寻找"组合冗余"
回到表决器例子,把8个最小项排进3变量卡诺图,你会看到m3、m5、m7可以圈成一个组,m6、m7也可以和m3、m5交叉圈出一个更大组。化简结果是:
Y = AB + BC + AC
对比一下标准与或式和化简后的式子,门数从4个与门加1个或门,变成3个与门加1个或门。看起来省得不多,但如果把输入变量扩展到5个、6个,化简带来的资源节省就是数量级的了。
我见过不少同学做实验,明明化简一步就能做出来的东西,非要按标准与或式硬搭,结果芯片多了好几片,板上飞线密密麻麻,还总出接触不良的故障。化简这步不光是省门,更是省事、省调试时间。我在实际操作中的经验是:化简以后尽量再看一眼化简结果的物理意义。比如AB+BC+AC这个东西,它语义上就是"任意两路为1就输出1",跟一开始的文字需求是对得上的。如果化简结果跟直觉明显不符,那大概率是之前真值表或者化简本身的某一步出了偏差。
3. 器件选型的排列组合策略:从小规模逻辑门到可编程器件
需求抽象和化简完成以后,接下来就是让逻辑落到实际的元器件上。这一节聊聊选型的问题:面对同一个真值表,你有多少种实现的手段,它们各自适合什么样的排列组合规模。
3.1 分立逻辑门:适合4变量以下的简单组合
小规模逻辑门芯片,比如74LS08(四重2输入与门)、74LS32(四重2输入或门)、74LS04(六反相器)、74LS00(四重2输入与非门),在变量少、逻辑简单的时候依然是很好使的。
拿表决器Y=AB+BC+AC来说,用3个2输入与门和1个3输入或门就能搭出来。如果是实验箱上验证,这是最直观的做法:每个与门对应一个乘积项,输入端接对应的输入变量,输出汇总到或门。但要注意,74LS系列里常见的或门是2输入的,3输入或门要另外找,或者用2输入或门级联——比如74LS32里两个2输入或门串联,也能做3输入或。这些都是小问题,真正需要警惕的是输入变量一多,门数需求立刻爆炸。
一个简单的估算方法:如果化简后的与或式里,总的乘积项数量超过10个,或者单个与项涉及的变量数超过4个,我就建议换一种实现方案了。因为分立门搭到这种复杂度,布线混乱、功耗上升、传输延迟不一致带来的毛刺问题都会冒出来。
3.2 译码器/数据选择器:天生就是排列组合的最佳载体
中规模集成电路里,3-8译码器(74LS138)和8选1数据选择器(74LS151)是我特别偏爱的一类器件,因为它们本身就建立在排列组合的基础上。
3-8译码器的工作原理就是:把3位输入的全排列组合(8种),一一对应到8个输出端。每一个输出端低电平有效,正好对应该输入组合下的一个最小项。换句话说,译码器可以直接输出全部最小项,你再把需要的几个最小项用与非门合并起来,就能得到任意3变量组合逻辑函数。
8选1数据选择器更有意思:它通过3个选择端从8个输入通道里选一个连接到输出。如果你把变量接到选择端,把常数0或1按真值表的输出规律接到数据输入端,那么这个芯片本身就实现了一个任意3变量逻辑函数。不用额外的门电路,一片74LS151加几个上拉/下拉电阻就搞定了。数据输入端接地表示逻辑0,接VCC表示逻辑1。
举个例子,还是表决器Y=AB+BC+AC。把A、B、C接到74LS151的选择端,数据输入端D0~D7分别接:
| 选择端C,B,A | 对应最小项 | Y值 | Dn接法 |
|---|---|---|---|
| 000 | m0 | 0 | 接地 |
| 001 | m1 | 0 | 接地 |
| 010 | m2 | 0 | 接地 |
| 011 | m3 | 1 | 接VCC |
| 100 | m4 | 0 | 接地 |
| 101 | m5 | 1 | 接VCC |
| 110 | m6 | 1 | 接VCC |
| 111 | m7 | 1 | 接VCC |
这样连完了以后,74LS151的输出端就直接等于表决器的输出了。整个过程连一个逻辑门都不用加,而且绝对不会出毛刺问题,因为选用的是同一片芯片内部的固有逻辑路径。这种"真值表直接映射到数据选择器"的方法,我觉得是理解排列组合在器件层面应用的最好例子。
3.3 可编程逻辑器件:组合数爆炸时的终极方案
当输入变量继续增加到5个、6个以上,真值表的排列组合数到了32、64种,无论是分立门还是数据选择器都会变得难以招架。这时候该上可编程逻辑器件了,从CPLD到FPGA,它们内部有大量可配置的逻辑单元,理论上可以表示任意复杂的组合逻辑函数。
FPGA里实现组合逻辑的底层结构叫查找表(LUT),这个东西的原理本质就是SRAM里存一张真值表。一个4输入LUT,里面有16个SRAM单元,正好对应4位输入的全排列组合(2⁴=16)。你在综合工具里写一句always组合逻辑或者assign语句,工具就会自动把你的逻辑函数编译成一张查找表的内容。所以从某种意义上说,FPGA设计组合逻辑,直接就是在"查排列组合表",硬件门级结构被软件隐藏了。
这就带来一个很实用的小技巧:你在写Verilog或VHDL的时候,与其费劲化简逻辑表达式,不如直接用case语句把真值表原样写出来,让综合工具帮你做优化。比如:
always @(*) begin case ({A, B, C}) 3'b011: Y = 1'b1; 3'b101: Y = 1'b1; 3'b110: Y = 1'b1; 3'b111: Y = 1'b1; default: Y = 1'b0; endcase end这种写法在可读性和可维护性上,比手推化简表达式要好得多。而且因为FPGA底层LUT本身就是一张真值表,case语句和LUT的映射关系非常直接,很少会出现综合结果不理想的情况。
4. 组合逻辑器件级联中的排列转移:多路选择与优先编码实战
学会了单个器件实现组合逻辑,下一个问题是:一个功能模块内部,器件与器件之间该怎么级联?这里同样藏着排列组合的思想,而且比单器件设计更容易踩坑。
4.1 多片数据选择器的扩展:从8选1到16选1
设计中经常遇到输入信号通道数超过单芯片能力的情况。比如8个传感器信号要选一路输出,用一片74LS151就够了;但项目里实际有16路信号,这时候就得扩展。
16选1的思路不复杂:用两片8选1,再加一个额外的选择位。16路信号分成两组,各接一片74LS151的数据输入。两个芯片的选择端A0、A1、A2并接在一起,作为16选1的低3位地址;第4位地址S接到其中一片的使能端(低有效)和另一片的使能端(通过反相器接高有效)。当S=0时,第一片工作,第二片禁止;S=1时反过来。最终两片芯片的输出再用一个2选1合并。
这种结构的本质是把16种排列组合拆分成两个8组的子排列。低3位地址在组内选择,最高位地址在组间选择。你可以在脑袋里把它理解成一个二维矩阵:4位地址构成了行和列组合寻址,这样去想就不会乱。
不过这里面有一个工程细节很容易翻车:2选1合并,用与门和或门搭的话,要么两个与门的传输延迟不一致导致尖峰毛刺,要么使能端翻转的瞬间两片芯片出现了短暂的同导通状态。我在实验里遇到过两片74LS151的使能端响应时间不完全一致的情况,导致输出低电平瞬间被短暂拉高,接到计数器上就产生了误计数。
更稳的做法是用三态门或者直接选用带扩展功能的数据选择器。74LS150是16选1的片子,内部已经处理好了扩展逻辑,一片搞定,省去级联的麻烦。所以我的建议是:能用单片高位数选择器,就别用多片低位数级联,芯片价格差距通常很小,但可靠性差距不小。
4.2 优先编码器的排列降维:几十种状态只需几位编码
再说说优先编码器,它是另一类与排列组合强相关的器件。74LS148是8-3优先编码器,输入8根线I0~I7(低有效),输出3位二进制码。它做的事情,是把输入端的"哪一根线有效"这一种排列状态,编码成对应的3位组合值。8种输入状态对应8种输出编码,本质上就是一组一一对应的排列映射。
优先编码器特别适合处理中断优先级、按键扫描这类场景。你不需要知道全部按键的状态,只需要知道优先级最高的那个是谁。电路里常见的一个问题是想扩展输入数量,比如16路中断源要用两片74LS148级联。级联的方式有点巧妙:两片的输出直接并接,低优先级片的EO(输出使能)接到高优先级片的EI(输入使能)。这样当高优先级片没有输入时,它允许低优先级片工作;一旦高优先级片有输入,就会禁止低优先级片。优先级的排列就这样被硬件实现了。
类似的还有二-十进制优先编码器74LS147,把10路输入编码成BCD码。它实际上是在做10取1到4位码的组合映射。虽然现在MCU很少用这种芯片了,但在硬件面试、竞赛方案中,依然能看到优先编码器处理输入压缩的影子。
4.3 奇偶校验器中的排列奇偶性
奇偶校验是一个很有代表性的组合逻辑应用,它直接用到了排列组合里"奇偶性"的概念。9位奇偶校验器(74LS280)可以接受9路输入,输出端分别给出"1的个数为奇数"和"1的个数为偶数"两个信号。
它的内部实现其实是把9个输入的所有排列分组:凡是输入中1的个数为奇数,归入一组,输出奇数标志;1的个数为偶数,归入另一组。这个分类完全是对输入排列组合的一种划分。用布尔代数的语言说,它实现了9个变量的异或运算——因为是逐级异或,而异或本身就是判断两个数是否不同的基本门。
在串行通信里,发送端和接收端都需要做奇偶校验。发送端计算数据位的奇偶性,把校验位附加到数据后面;接收端对整个数据加校验位做奇偶校验,如果结果与预定模式不符,就知道传输过程中有一位发生了翻转。这种在传输层保障数据完整性的做法,底层逻辑就是排列组合的奇偶性判断。写代码的话,一个for循环异或所有位就能实现,但在高速接口里,硬件逻辑器件处理这种排布方式是有明显速度优势的。
5. 时域上的排列组合:从竞争冒险到对数字电路音乐电路的启发
到现在为止,聊的都是静态的、纯组合的逻辑设计。但工程上有个绕不开的问题:组合逻辑虽然有输出,却不代表它一定稳定无毛刺。当输入信号不是同时跳变的时候,组合逻辑内部各路径的传输延迟不同,就会导致短暂的错误输出。这个现象叫竞争冒险,它同样可以用排列组合来理解。
5.1 竞争冒险:不同路径延迟的组合差异
考虑最简单的表达式Y = A + A'。按照逻辑,Y恒等于1,没有输入能让它变成0。实际电路里,A'由反相器产生,信号到达或门输入的时间会比原信号A晚一个反相器的传输延迟。在A从1跳变到0的那一瞬间,或门两个输入端可能出现一小段同时为0的情况,输出Y就会产生一个短暂的负脉冲毛刺。
这个现象的本质是:两种可能的信号路径(A直达路径和A经过反相器的路径),在时间轴上形成了不同延迟的"组合排列"。如果A的跳变速度足够慢,或者两条路径延迟恰好相差较大,毛刺就更明显。消除毛刺的常用办法是增加冗余项。比如表决器化简成Y=AB+BC+AC后,实际上还会在A、B、C变化的某些瞬间出现毛刺,加上AB这个冗余项通常来不及覆盖所有跳变。真正的可靠做法,要么是增加一个输出滤波电容,要么是在输出端用D触发器打一拍,让毛刺在时钟边沿到来之前消失。
说到这我想起一个很常见的实际场景:数字电路的实验箱上接了音乐电路。网上流传的"数字电路音乐电路设计",原理其实是用计数器产生不同分频,去控制蜂鸣器的输出频率。不同音符对应的是不同的计数循环长度,这本质上也是一种"计数排列"的变化。频率越高,计数值越小,输出翻转越快,音调越高。这些分频和计数值的排列组合,决定了乐曲能不能正确播放。
如果你在实验里遇到了"蜂鸣器声音不对""音符顺序乱了"这种问题,最值得怀疑的地方不是蜂鸣器本身,而是控制音符切换的逻辑。要是用组合逻辑直接驱动蜂鸣器,切换音符的瞬间,因为计数器的各个输出位不是同时翻转的,音符频率会瞬间乱跳,听起来就是"滋滋"的杂音。处理方法也很简单:让控制音符的寄存器只在时钟边沿更新,相当于把组合逻辑输出的毛刺与外部世界隔离,这就属于从竞争冒险角度去优化一个已经能工作的音乐电路。
5.2 时序逻辑中的状态计数:排列组合的循环
再往深一层,时序逻辑里也有排列组合的影子。在一个模8计数器中,Q2Q1Q0三个触发器的状态,从000到111循环,正好是3位二进制数的一个完整排列。格雷码计数器则是每次只改变一位,本质上是一种保证相邻状态距离为1的排列规则。这些状态本身,都是排列组合的具体实例。
设计有限状态机时,状态编码方案的选择,同样是一种排列组合。以四状态为例,状态码有000、001、010、011四种常用编码,以及格雷码00、01、11、10这四种相邻变化。不同编码方案的优劣,在组合逻辑资源消耗和时序可靠性上有明显差异。格雷码在电机编码器、异步FIFO指针等场景中是主流选择,因为它能保证快到快读的场景下不产生非法状态跳变。
另外,异步FIFO的老生常谈问题——读写指针跨时钟域同步——实际也是一种特殊的排列组合问题。两个时钟域的读写指针各自在自己的域内递增,只有在同步之后才能做比较。处理不好指针错位,FIFO就会溢出或者下溢。这也是为什么很多工程师提到cdc mailbox这个概念:跨时钟域传递邮箱消息,本质上要求消息在目的时钟域中稳定可见,而稳定的前提,是信号跳变时刻不能落在采样的触发沿上。排列组合在这里表现为什么时刻采样什么信号,是一种动态的时序组合。
6. 可复用的设计套路:从真值表到电路的提效方法
聊了这么多原理,接下来说几个可以直接抄作业的方法。这些套路是我在实验室和项目中反复验证过的,能实打实减少设计时间。
6.1 "最小项查表法"设计组合逻辑
当你拿到一个功能需求,先别急着画门电路。我的做法是严格按照下面四步走:
- 把需求抽象成输入输出信号,明确变量个数
- 列全真值表,一行不漏
- 在真值表里找到所有输出为1的行,写最小项
- 决定实现方式:表达式化简、数据选择器直连、还是写case语句
这个方法对新手特别友好,不容易出错。熟练了以后甚至可以跳过第3步的文字形式,直接盯着真值表,心中就有电路。
6.2 用数据选择器快速搭建任意3变量逻辑
如果手头正好有74LS151,而且逻辑函数不超过3个输入变量,就直接用查表法,一分钟接完电路:
- 把变量A、B、C接到选择端
- 对照真值表,把数据输入端D0~D7接VCC或GND
- 输出端Y就是函数值
这个方法比画门电路快得多,而且调试极其方便:只要检查数据端接对不对就行了。数据端接线一目了然,出错了也能瞬间定位到具体那一行真值表。
变量超过3个的时候也别急着放弃。4变量的函数可以用两片74LS151加额外逻辑实现,但性价比会下降。更实用的做法是:用手头现成的LUT型FPGA,写4行case语句。我经常拿着小脚丫FPGA或者Artix-7的开发板做原型验证,综合后的资源报告里能看到每个LUT的利用率,稍微优化就能得到漂亮的布线结果。
6.3 信号命名与模块划分中的组合管理
最后一个套路,听起来比较"软",但实际收益巨大。在模块化设计时,把相关联的组合逻辑信号按位宽分组命名,并对每组信号的排列顺序做统一约定。比如,多路选择器的选择端按A[2:0]命名,数据端按D[7:0]命名;所有真值表里,位序和总线位序保持一致。这样一来,写验证testbench的时候,穷举输入组合只需写一个循环和位移操作,不易出错;在仿真波形里观察信号,恢复起来也直观。
我在维护一个多人协作项目时深深体会过这个约定的价值。有人习惯把使能信号叫EN,有人叫en,有人叫ENABLE,信号位序有的从高到低,有的从低到高,结果组合逻辑的排布五花八门,最后联调时花了好多时间对齐接口。后来统一规范,所有使能叫en,所有数据总线按bit0在最右,所有状态标志按bit位与事件编号一致,整个项目的排错效率瞬间提升了一截。
7. 一次真实的调试经历:数据选择器级联中的地址组合错乱
前面讲了很多理论,最后分享一个实际的排查案例。这是我在设计一块数据采集板时遇到的,正好能串起排列组合与器件级联的所有要点。
7.1 现象:通道选择与预期不符的诡异错位
板卡上一共有16路模拟信号,我按前面的方案用了两片74LS151,通过一个反相器控制使能端,组成了16选1。测试时发现,选择地址0x0和0x1时,输出的是正确的通道;但选择了0x2之后,输出就错乱了,而且错乱规律看起来毫无章法——不是简单的高位地址反了,也不是个别通道接反。
这个现象让我第一反应就是查接线。但查了几遍数据端和选择端都没有发现焊错。最后用示波器逐个引脚测量,才定位到问题根源。
7.2 排查:从排列组合矩阵找突破口
我先列了一张表,把实际输出与预期通道一一对照,然后把错误模式写下来。结果发现:错误的通道选择和预期通道之间的差,总是一个固定的偏移量,而且只出现在高位地址为1的情况。
这个规律让我开始怀疑不是接线问题,而是两片芯片的使能逻辑有问题。翻看数据手册,74LS151的使能端是低电平有效,但它的输出并不是简单的三态——当使能无效时,输出并不是高阻态,而是固定逻辑0。我之前的设计是让两片输出直接短接,再用与门/或门合并。当使能无效的那片输出固定0,确实不会影响有效的另一片,这个思路本身没错。
但问题出在我用来生成使能信号的反相器上。我用的反相器是74LS04,输入从高到低跳变时,传输延迟大约十几纳秒;但另一个芯片的使能路径上经过了两个门的延迟。这样在高位地址S翻转的瞬间,两片芯片的使能端有一个短暂的重叠窗口,导致两片芯片同时处于有效状态。此时两片输出直接短接在一起,一个输出0一个输出1,低的被高的拉高了一瞬,或者高的被低的拉低了一瞬,产生了毛刺。如果这个毛刺落在后续电路采样的时钟沿上,就可能被记录成完全不同的通道地址。
7.3 修复:给使能路径做延迟对齐
定位到问题后,修复反而简单了。两个方案:
- 在输出端加一个8选1的额外组合逻辑,避免输出端直接短接。但这个方案会增加一片芯片,不值得。
- 把反相器换成同相缓冲器(74LS244),并人为地在另一条路径上也加一个同相缓冲器,保证两条使能路径延迟严格一致。这样无论地址怎么翻,两片芯片的使能状态不会出现同时有效的情况。
我采用了第二个方案,改完以后用示波器反复触发地址切换,再也没看到毛刺。后来我又把这套延迟对齐的思路用到其他项目里:只要是多条并行信号路径,无论它们是地址线、数据线还是控制线,都要尽量保证各条路径的传播延迟一致。这其实也是高速PCB设计中"等长布线"思想的逻辑门级版本。
8. 动手做一个小项目:用74LS151实现4变量多数表决器
理论说了不少,最后来一个可复现的小项目。4变量多数表决器,输入A、B、C、D,输出Y,要求4个输入中有3个或以上为1时输出1。这个项目用分立门做很麻烦,但用两片74LS151可以做得又快又好。
8.1 逻辑到芯片的映射
4变量函数如果用一片8选1,需要把4个变量都接进去,一片不够。我的做法是:用两片74LS151,把高两位变量C、D作为片选控制,低两位变量A、B作为每片内部的选择地址。更具体地,把两片的选择端A0、A1分别接A、B,然后用C、D经过简单组合逻辑切换两片的输出。
但这样连线依然复杂。另一个更优雅的做法是:利用数据选择器的高级用法——把一部分输入变量接到数据输入端,而不是选择端。这本质上是对排列组合的一种降维处理。
以4变量多数表决器为例,先固定两个变量(比如C、D)为"分组变量",它们形成4种组合(CD=00、01、10、11)。在每一种CD组合下,输出Y仅由A、B决定,于是可以写出一张关于A、B的小真值表:
- 当CD=00时,A、B无论怎样都不可能有3个1,所以Y恒为0
- 当CD=01时,要总共至少有3个1,需要A和B都为1,所以Y=AB
- 当CD=10时,同样需要A和B都为1,Y=AB
- 当CD=11时,已经有两个1了,再加A或B任意一个1即可,Y=A+B
这个分解很有意思:原本16种输入组合的4变量函数,被拆成了4组2变量的子函数。这4个子函数可以用74LS151的数据输入端来实现——每个数据输入端接一个关于A、B的组合逻辑。
8.2 具体接线步骤
手头有一片74LS151时,接线如下:
- 把A、B接到74LS151的选择端A0、A1
- 把D0接0(对应CD=00的情况)
- D1、D2通过一个2输入与门(74LS08)产生,A和B相与(对应CD=01、10)
- D3通过一个2输入或门(74LS32)产生,A和B相或(对应CD=11)
- C、D通过一个2线-4线译码器(或者用74LS139双2-4译码器)产生四路分组选通信号,分别接到74LS151的四个选通使能端
这个方案用了一片74LS151、一片74LS08、一片74LS32、一片74LS139,一共4片小芯片,比起直接用门电路搭4变量真值表(需要十几个门),已经省了不少。而且调试起来非常直观:通道0异常就查D0的接线,通道3异常就查D3的或门输出。
8.3 验证与测试建议
上电以后,建议按下面的表一项一项测试:
| A | B | C | D | Y |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 1 | 1 |
| 1 | 0 | 1 | 1 | 1 |
| 1 | 1 | 0 | 1 | 1 |
| 1 | 1 | 1 | 0 | 1 |
| 1 | 1 | 1 | 1 | 1 |
特别提醒:测试时不要跳过"恰好3个输入为1"和"全1"这两种情况。多数表决器最容易出错的边界就是这里。
9. 最后的经验补充:组合逻辑排布中的几个常见坑
文章最后,把这几年来在组合逻辑排布上踩过、见过的一些坑集中说一下,字不多,但每条都是真金白银。
9.1 使能端的悬空
数据选择器、译码器这类的使能端,无论你用不用,都一定要接到确定的电平上。悬空相当于输入浮空,噪声一拉就可能让芯片误动作。特别是CMOS器件,悬空输入还会导致静态功耗上升,更严重的可能会损伤芯片内部结构。这个经验我在一次多路采集板调试中体会很深,使能端悬空导致的偶发错通道故障,花费了我一整个下午去定位。
9.2 输出端不要长期短接
前面提到过,让两个芯片的输出端直接短接,在使能路径时间差存在的情况下是危险的。标准的做法是用三态门配合输出使能信号,或者用多路选择器在外部合并。如果实在要直接短接,一定要确认两片器件在任何时刻最多只有一片处于输出有效状态。
9.3 注意数据手册里的"输出特性"
很多器件的输出在高阻态时,其实不是真正的高阻,而是一个很大的电阻,输出端还会带上一点残余电压。在高速开关场景下,这个残余电压叠加到其他信号上,可能造成逻辑误判。所以我一般只在低频或纯数字逻辑场景里直接并接芯片输出,高速场景一定会加缓冲或三态隔离。
9.4 不要轻视真值表的完整性
最后还是回到这个主题的根本:一张完整的真值表,是你和电路之间的契约。任何一行遗漏,都会导致逻辑函数与真实需求不符。列真值表不是走过场,它本身就是设计的一半。我见过太多项目出问题,最后追根溯源,就是当初某个输入组合的输出值没定义清楚。
把排列组合这个概念吃透了,你会发现数字电路的世界并没有那么复杂。从最小的与非门,到上百万门的FPGA,底层遵循的规律其实是一样的:穷举输入空间,映射输出行为,再用合适的器件把这张映射表物化出来。希望这篇整理能对你理解数字电路、驾驭逻辑器件有所帮助。有疑问欢迎在评论区交流,我会尽量回复那些我实际碰到过、有具体场景的问题。