4. 组合逻辑设计:加法器、比较器、多路选择器、编码器与译码器、三态门

组合逻辑,说白了就是没有记忆功能的电路。输入一变,输出立马跟着变,不带任何延迟记忆。今天咱们聊聊几个最常用的组合逻辑模块——加法器、比较器、多路选择器、编码器、译码器,还有那个让人又爱又恨的三态门。

这些模块,我几乎每个项目都会用到。你想想看,一个稍微复杂点的数字系统,哪个能离得开它们?

4.1 加法器:从半加器到超前进位

加法器是算术运算的基石。最简单的加法器叫半加器,它只处理两个1位二进制数相加,输出和与进位。

半加器的真值表很简单:

A B S(和) C(进位)
0 0 0 0
0 1 1 0
1 0 1 0
1 1 0 1

逻辑表达式也很直观:S = A ⊕ B,C = A & B。

但实际项目中,我们很少用半加器。为啥?因为它不考虑来自低位的进位。全加器才是正经干活的东西。全加器有三个输入:A、B 和进位输入 Cin,输出和 S 与进位输出 Cout。

全加器的逻辑表达式:

S = A ⊕ B ⊕ Cin
Cout = (A & B) | (A & Cin) | (B & Cin)

多个全加器级联起来,就构成了行波进位加法器。我刚开始做设计时,觉得这玩意儿挺好用,结构简单嘛。但后来发现,位宽一大,延迟就受不了。32位的行波进位加法器,进位信号要从最低位一直传到最高位,延迟累加起来,时钟频率根本上不去。

关键点:行波进位加法器的延迟与位宽成正比。每增加一位,延迟增加一个全加器的进位传播时间。

那怎么办?用超前进位加法器。它的核心思想是提前计算出进位,而不是等前一级算完。通过进位生成信号 G = A & B 和进位传播信号 P = A ⊕ B,我们可以并行计算出所有位的进位。

4位超前进位加法器的进位逻辑:

C1 = G0 | (P0 & C0)
C2 = G1 | (P1 & G0) | (P1 & P0 & C0)
C3 = G2 | (P2 & G1) | (P2 & P1 & G0) | (P2 & P1 & P0 & C0)
C4 = G3 | (P3 & G2) | (P3 & P2 & G1) | (P3 & P2 & P1 & G0) | (P3 & P2 & P1 & P0 & C0)

嗯,这里要注意,超前进位逻辑的扇入会随着位宽增加而急剧增大。位宽超过8位时,逻辑门的扇入就太大了,实际实现时通常采用分组超前进位。

我的经验:在FPGA中实现加法器,我一般直接用 "+" 运算符,综合工具会自动选择最优结构。但如果你在做ASIC或者对时序有极致要求,手动实现超前进位加法器还是有必要的。我曾经在一个高速数据通路项目中,手动优化了加法器的进位链,把关键路径缩短了15%。

4.2 比较器:相等与大小判断

比较器用来判断两个数的大小关系或是否相等。实现方式有两种:一种是逐位比较,另一种是利用减法结果。

相等比较最简单:对每一位做异或,然后所有结果相与。如果所有位都相等,输出1。

// 4位相等比较器
assign eq = ~(a[0] ^ b[0]) & ~(a[1] ^ b[1]) & ~(a[2] ^ b[2]) & ~(a[3] ^ b[3]);

大小比较稍微复杂点。从最高位开始比较,如果某一位不同,该位的大小就决定了最终结果。这其实就是字典序比较的思路。

我习惯用减法来判断大小:计算 A - B,看结果的符号位。如果结果为负,说明 A < B。但要注意,这种方法在 A 和 B 相差很大时可能溢出,需要额外处理。

避坑指南:我曾经在一个项目中直接用减法做比较,结果没考虑有符号数的溢出问题,导致在某些边界条件下比较结果出错。后来我改用逐位比较的方式,虽然代码长了点,但逻辑清晰,不容易出bug。

4.3 多路选择器:数据选择的艺术

多路选择器(MUX)从多个输入中选择一个输出。2选1 MUX是最基本的,它的逻辑表达式是:Y = S ? A : B。

用Verilog实现很简单:

assign y = sel ? a : b;

多路选择器可以级联成更大的选择器。比如,4选1 MUX可以用三个2选1 MUX构成。但实际项目中,我更推荐直接用 case 语句或三元运算符,综合工具会帮你优化。

多路选择器在FPGA中实现时,会用到LUT(查找表)。一个6输入LUT可以实现一个4选1 MUX(2位选择信号 + 4位数据输入,但数据输入只有4位,选择信号2位,共6位输入,刚好一个LUT搞定)。

我的建议:在写代码时,尽量用 case 语句实现多路选择器,可读性好。但要注意,case 语句要写 full case,否则会综合出锁存器。我见过不少新手在这里栽跟头。

4.4 编码器与译码器

编码器将多个输入线编码为较少的输出线。比如,8线-3线编码器,8个输入中只有一个为1,输出对应的3位二进制编码。

优先编码器更实用——当多个输入同时有效时,优先选择优先级最高的那个。这在中断控制器中很常见。

译码器是编码器的逆过程。3线-8线译码器,3位输入对应8个输出中的一个有效。这在地址译码中经常用到。

译码器的Verilog实现:

// 3线-8线译码器
always @(*) begin
  case(addr)
    3'b000: y = 8'b0000_0001;
    3'b001: y = 8'b0000_0010;
    3'b010: y = 8'b0000_0100;
    3'b011: y = 8'b0000_1000;
    3'b100: y = 8'b0001_0000;
    3'b101: y = 8'b0010_0000;
    3'b110: y = 8'b0100_0000;
    3'b111: y = 8'b1000_0000;
  endcase
end

你想想看,译码器本质上就是一个多路分配器(DEMUX)——把输入信号分配到多个输出中的一个。

4.5 三态门:共享总线的关键

三态门有三种输出状态:0、1 和高阻(Z)。高阻状态意味着输出引脚与内部电路断开,相当于没接任何东西。这让多个设备可以共享一条总线。

三态门的真值表:

使能(EN) 输入(IN) 输出(OUT)
0 X Z(高阻)
1 0 0
1 1 1

在Verilog中,三态门用连续赋值语句实现:

assign data_bus = en ? data_out : 1'bz;

重要提醒:三态门在FPGA内部使用要非常小心。大多数FPGA内部没有真正的三态总线,综合工具会用MUX来模拟。只有在芯片的IO引脚上,三态门才是真正有效的。我曾经在一个项目中试图在FPGA内部用三态门实现总线共享,结果综合出来的资源消耗比用MUX还大,时序也差。后来我改用MUX + 仲裁器的方式,效果好了很多。

三态门最典型的应用是双向IO口。比如,一个GPIO引脚既可以作为输入,也可以作为输出,通过方向控制信号来切换。

// 双向IO口示例
assign gpio_pin = (dir) ? data_out : 1'bz;
assign data_in = gpio_pin;

知识体系总览

下面这张图展示了本章各个知识点之间的关系:

组合逻辑设计 加法器 比较器 多路选择器 编码器 译码器 三态门 半加器 / 全加器 行波进位 / 超前进位 相等比较 / 大小比较 2选1 / 4选1 / 级联 普通编码 / 优先编码 二进制译码 / 地址译码 高阻态 / 双向IO / 总线共享

这五个模块,说白了就是数字电路的基本积木。加法器做运算,比较器做判断,多路选择器做数据路由,编码译码做信号转换,三态门做总线控制。把它们组合起来,就能搭出各种复杂的数字系统。

嗯,组合逻辑设计的关键,就是理解每个模块的功能和时序特性,然后根据实际需求选择合适的实现方式。别一味追求花哨的架构,简单可靠才是硬道理。

核心要点回顾:

  • 加法器:行波进位简单但慢,超前进位快但复杂,FPGA中直接用 "+" 即可
  • 比较器:逐位比较最可靠,减法比较要注意溢出
  • 多路选择器:case语句实现,注意full case避免锁存器
  • 编码器/译码器:优先编码器更实用,译码器本质是DEMUX
  • 三态门:FPGA内部慎用,IO引脚上才是真功夫

可可精品资料库