3. 组合逻辑电路:加法器、编码器、译码器、多路选择器(MUX)的原理与设计

各位同学,咱们今天聊组合逻辑。说白了,就是那种“输入一变,输出立马跟着变”的电路,没有记忆,不存状态。你给它什么,它立马还你什么,很实在。

我个人觉得,组合逻辑是数字世界的“肌肉”,负责干活。而时序逻辑是“大脑”,负责记忆和调度。今天咱们就把几个最常用的“肌肉块”拆开看看:加法器、编码器、译码器、多路选择器。

3.1 加法器:数字电路里的算盘

加法器这东西,CPU里到处都是。你写的 a + b,最后都是靠一堆门电路算出来的。

3.1.1 半加器

先看最简单的。两个1位二进制数相加,结果最多两位(一个和位S,一个进位C)。这就是半加器。

真值表很简单:

ABS(和)C(进位)
0000
0110
1010
1101

看出来没?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最低。

真值表(部分):

I7I6I5I4I3I2I1I0Y2Y1Y0
1xxxxxxx111
01xxxxxx110
001xxxxx101
.................................

你想想看,如果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。

真值表:

S1S0Y
00D0
01D1
10D2
11D3

实现方式有两种:

  • 直接法:用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