3. 组合逻辑电路:加法器、编码器、译码器、多路选择器(MUX)的原理与设计
各位同学,咱们今天聊组合逻辑。说白了,就是那种“输入一变,输出立马跟着变”的电路,没有记忆,不存状态。你给它什么,它立马还你什么,很实在。
我个人觉得,组合逻辑是数字世界的“肌肉”,负责干活。而时序逻辑是“大脑”,负责记忆和调度。今天咱们就把几个最常用的“肌肉块”拆开看看:加法器、编码器、译码器、多路选择器。
3.1 加法器:数字电路里的算盘
加法器这东西,CPU里到处都是。你写的 a + b,最后都是靠一堆门电路算出来的。
3.1.1 半加器
先看最简单的。两个1位二进制数相加,结果最多两位(一个和位S,一个进位C)。这就是半加器。
真值表很简单:
| A | B | S(和) | C(进位) |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 |
看出来没?S = A XOR B,C = A AND B。就两个门,搞定。
核心公式:
S = A ⊕ B
C = A · B
3.1.2 全加器
半加器有个问题——它不考虑来自低位的进位。实际做多位加法时,每一位都得考虑进位输入。所以有了全加器。
全加器有三个输入:A、B、Cin(进位输入)。两个输出:S、Cout。
逻辑表达式:
S = A ⊕ B ⊕ Cin
Cout = (A · B) + (Cin · (A ⊕ B))
嗯,这里要注意。Cout 的表达式有两种写法,上面这种是“先异或再与”,另一种是“直接与或”。我习惯用上面这种,因为综合出来的电路延迟更均衡。
3.1.3 行波进位加法器
把多个全加器串起来,前一级的Cout接到后一级的Cin,就是行波进位加法器(Ripple Carry Adder)。
比如一个4位加法器:
module adder_4bit (
input [3:0] A, B,
input Cin,
output [3:0] S,
output Cout
);
wire C1, C2, C3;
full_adder FA0 (.A(A[0]), .B(B[0]), .Cin(Cin), .S(S[0]), .Cout(C1));
full_adder FA1 (.A(A[1]), .B(B[1]), .Cin(C1), .S(S[1]), .Cout(C2));
full_adder FA2 (.A(A[2]), .B(B[2]), .Cin(C2), .S(S[2]), .Cout(C3));
full_adder FA3 (.A(A[3]), .B(B[3]), .Cin(C3), .S(S[3]), .Cout(Cout));
endmodule
我曾经踩过的坑:行波进位加法器虽然结构简单,但进位是一级一级传下去的。位数一多,延迟就上来了。32位加法器,最坏情况进位要从最低位传到最高位,延迟能到几十个门级。做高速设计时,千万别用这种结构,得用超前进位加法器(Carry Lookahead Adder)。
3.2 编码器与译码器:信号的“翻译官”
编码器是把“独热码”变成二进制码。译码器反过来,把二进制码变成“独热码”。
3.2.1 优先编码器
实际项目中,输入信号可能同时有效。这时候就得用优先编码器——谁优先级高,谁说了算。
比如8线-3线优先编码器(74LS148),输入I7优先级最高,I0最低。
真值表(部分):
| I7 | I6 | I5 | I4 | I3 | I2 | I1 | I0 | Y2 | Y1 | Y0 |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | x | x | x | x | x | x | x | 1 | 1 | 1 |
| 0 | 1 | x | x | x | x | x | x | 1 | 1 | 0 |
| 0 | 0 | 1 | x | x | x | x | x | 1 | 0 | 1 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
你想想看,如果I7=1,其他输入不管是什么,输出都是111(表示7)。这就是“优先”的含义。
我的习惯:写优先编码器时,用casez语句最方便。casez可以把高阻态z当成“不在乎”,配合优先级顺序,代码非常简洁。
3.2.2 3线-8线译码器
译码器就是编码器的逆过程。3位输入,8位输出,每次只有一位输出有效。
逻辑表达式:
Y0 = (~A2) & (~A1) & (~A0)
Y1 = (~A2) & (~A1) & ( A0)
Y2 = (~A2) & ( A1) & (~A0)
...
Y7 = ( A2) & ( A1) & ( A0)
译码器最常见的用途是地址译码。比如你有个存储器,地址线是A[2:0],用译码器产生8个片选信号,每个信号选通一个存储单元。
3.3 多路选择器(MUX):数据通道的“道岔”
MUX的作用,就是从多个输入信号中选一个送到输出。就像铁路上的道岔,把不同的轨道接到同一条线上。
3.3.1 2选1 MUX
最简单的MUX。两个输入D0、D1,一个选择信号S,一个输出Y。
逻辑表达式:Y = (S · D1) + (~S · D0)
用门电路实现:
module mux2to1 (
input D0, D1,
input S,
output Y
);
assign Y = S ? D1 : D0;
endmodule
你看,Verilog里一行搞定。但底层硬件其实就是一个与或结构。
3.3.2 4选1 MUX
两个选择信号S1、S0,四个输入D0~D3。
真值表:
| S1 | S0 | Y |
|---|---|---|
| 0 | 0 | D0 |
| 0 | 1 | D1 |
| 1 | 0 | D2 |
| 1 | 1 | D3 |
实现方式有两种:
- 直接法:用4个三态门,每个输入配一个与门,然后或起来。
- 树形法:用3个2选1 MUX级联。先选D0/D1,再选D2/D3,最后选这两个结果。
我建议:在FPGA设计中,尽量用现成的MUX原语,或者直接用case语句。综合工具会帮你优化成最合适的结构。别自己手搭门级电路,除非你在做ASIC标准单元库。
3.4 实战经验:组合逻辑的“坑”与“道”
聊了这么多理论,说点实际的。
第一,竞争冒险。组合逻辑因为门延迟不同,可能会出现短暂的错误输出。比如一个与门,输入从01变成10,理论上输出应该一直是0。但如果两个信号变化时间有先后,中间可能会瞬间出现11,导致输出出现一个毛刺。
我曾经遇到过一个案例:一个地址译码器,因为地址线到达时间不一致,导致多个片选信号同时有效,把两个存储单元同时选中了。结果读出来的数据是乱的。后来加了同步寄存器,问题才解决。
第二,扇出问题。一个信号驱动太多负载,会导致上升沿变缓,时序出问题。我一般会在高扇出的信号上加缓冲器,或者复制几份。
第三,不要用组合逻辑产生时钟。这是大忌。组合逻辑的输出有毛刺,用它做时钟,整个电路都会乱套。要用时钟,必须经过PLL或DCM。
一个小技巧:调试组合逻辑时,用示波器看波形,别只看仿真。仿真里看不到毛刺,但实际电路里到处都是。我习惯在关键节点上加一个RC滤波,把窄毛刺滤掉。
好了,这一章的内容就到这。组合逻辑是数字设计的基础,加法器、编码器、译码器、MUX这几个模块,你在项目中几乎天天都会碰到。下一章咱们聊时序逻辑,那才是真正让电路“活”起来的东西。
课后思考:如果让你设计一个32位的超前进位加法器,你会怎么处理进位链的传播延迟?试试看,用4位一组的分组超前进位,能不能把延迟从32级降到8级?
公众号:蓝海资料掘金营,微信deep3321