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;
知识体系总览
下面这张图展示了本章各个知识点之间的关系:
这五个模块,说白了就是数字电路的基本积木。加法器做运算,比较器做判断,多路选择器做数据路由,编码译码做信号转换,三态门做总线控制。把它们组合起来,就能搭出各种复杂的数字系统。
嗯,组合逻辑设计的关键,就是理解每个模块的功能和时序特性,然后根据实际需求选择合适的实现方式。别一味追求花哨的架构,简单可靠才是硬道理。
核心要点回顾:
- 加法器:行波进位简单但慢,超前进位快但复杂,FPGA中直接用 "+" 即可
- 比较器:逐位比较最可靠,减法比较要注意溢出
- 多路选择器:case语句实现,注意full case避免锁存器
- 编码器/译码器:优先编码器更实用,译码器本质是DEMUX
- 三态门:FPGA内部慎用,IO引脚上才是真功夫
可可精品资料库