4. 组合逻辑设计:加法器、多路选择器、译码器、编码器的设计与实现
组合逻辑,说白了就是没有记忆功能的电路。输入一变,输出立马跟着变,不带半点延迟(当然,实际物理门电路是有延迟的,这个我们后面时序分析再聊)。今天咱们就把加法器、多路选择器、译码器、编码器这四个最常用的组合逻辑模块,从原理到实现,一次性讲透。
4.1 加法器:从半加器到超前进位
加法器是算术运算的基石。CPU里的ALU,说白了就是一堆加法器加上控制逻辑。我刚开始做设计时,总觉得加法器太简单,不就是个全加器嘛。直到有一次做FFT处理器,发现进位链太长导致时序不收敛,才意识到加法器的设计大有讲究。
4.1.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。
全加器则多了一个进位输入Cin。三个输入,两个输出。真值表我就不列了,直接给表达式:
S = A ⊕ B ⊕ Cin
Cout = (A & B) | (A & Cin) | (B & Cin)
嗯,这里要注意:全加器的进位逻辑,用卡诺图化简后就是上面这个形式。我见过有人直接用(A+B+Cin) >= 2来判断,虽然功能对,但综合出来的面积会大一些。
4.1.2 行波进位加法器
把N个全加器串起来,就构成了行波进位加法器(Ripple Carry Adder)。每个全加器的Cout接到下一个的Cin。
// 4位行波进位加法器
module rca_4bit (
input [3:0] a, b,
input cin,
output [3:0] sum,
output cout
);
wire c1, c2, c3;
full_adder fa0 (.a(a[0]), .b(b[0]), .cin(cin), .sum(sum[0]), .cout(c1));
full_adder fa1 (.a(a[1]), .b(b[1]), .cin(c1), .sum(sum[1]), .cout(c2));
full_adder fa2 (.a(a[2]), .b(b[2]), .cin(c2), .sum(sum[2]), .cout(c3));
full_adder fa3 (.a(a[3]), .b(b[3]), .cin(c3), .sum(sum[3]), .cout(cout));
endmodule
这种结构最大的问题是什么?进位要一级一级传。32位的加法器,最坏情况下进位要经过32级门延迟。我曾经在一个项目中用行波进位做64位加法,结果时序分析报告一片红。后来改成超前进位,频率直接从200MHz干到了500MHz。
4.1.3 超前进位加法器
超前进位(Carry Lookahead)的核心思想:提前算出进位,不用等前一级算完。
定义两个中间信号:
- 生成信号 G:G = A & B,只要A和B都是1,就一定产生进位
- 传播信号 P:P = A ⊕ B,只要A和B有一个是1,进位就能传过去
那么进位公式可以写成:
C1 = G0 | (P0 & C0)
C2 = G1 | (P1 & G0) | (P1 & P0 & C0)
C3 = G2 | (P2 & G1) | (P2 & P1 & G0) | (P2 & P1 & P0 & C0)
...
你看,每个进位只跟输入和C0有关,跟中间进位无关。这就是超前进位的精髓。
// 4位超前进位加法器
module cla_4bit (
input [3:0] a, b,
input cin,
output [3:0] sum,
output cout
);
wire [3:0] g, p, c;
assign g = a & b;
assign p = a ^ b;
assign c[0] = cin;
assign c[1] = g[0] | (p[0] & c[0]);
assign c[2] = g[1] | (p[1] & g[0]) | (p[1] & p[0] & c[0]);
assign c[3] = g[2] | (p[2] & g[1]) | (p[2] & p[1] & g[0]) | (p[2] & p[1] & p[0] & c[0]);
assign cout = g[3] | (p[3] & g[2]) | (p[3] & p[2] & g[1]) | (p[3] & p[2] & p[1] & g[0]) | (p[3] & p[2] & p[1] & p[0] & c[0]);
assign sum = p ^ c;
endmodule
4.2 多路选择器:数据通道的开关
多路选择器(MUX),说白了就是数据选择开关。2选1、4选1、8选1...选通信号决定哪路数据通过。
4.2.1 2选1多路选择器
module mux2_1 (
input a, b,
input sel,
output reg y
);
always @(*) begin
case (sel)
1'b0: y = a;
1'b1: y = b;
endcase
end
endmodule
也可以用三目运算符:assign y = sel ? b : a; 综合出来的结果是一样的。
4.2.2 多路选择器的级联与实现
4选1可以用3个2选1搭起来,也可以用case语句直接写。我个人习惯用case,代码可读性更好。
module mux4_1 (
input [3:0] data,
input [1:0] sel,
output reg y
);
always @(*) begin
case (sel)
2'b00: y = data[0];
2'b01: y = data[1];
2'b10: y = data[2];
2'b11: y = data[3];
default: y = 1'b0;
endcase
end
endmodule
4.3 译码器:地址到选通的映射
译码器,把N位输入翻译成2^N位输出,每次只有一位有效。最典型的就是3-8译码器。
4.3.1 3-8译码器实现
module decoder_3to8 (
input [2:0] addr,
output reg [7:0] y
);
always @(*) begin
y = 8'b0;
case (addr)
3'b000: y[0] = 1'b1;
3'b001: y[1] = 1'b1;
3'b010: y[2] = 1'b1;
3'b011: y[3] = 1'b1;
3'b100: y[4] = 1'b1;
3'b101: y[5] = 1'b1;
3'b110: y[6] = 1'b1;
3'b111: y[7] = 1'b1;
endcase
end
endmodule
也可以用移位操作:assign y = 1'b1 << addr; 这个写法更简洁,但要注意addr不能越界。
4.3.2 带使能的译码器
实际项目中,译码器通常带使能信号。使能无效时,所有输出为0。
module decoder_3to8_en (
input [2:0] addr,
input en,
output reg [7:0] y
);
always @(*) begin
y = 8'b0;
if (en) begin
case (addr)
3'b000: y[0] = 1'b1;
3'b001: y[1] = 1'b1;
// ... 省略中间
3'b111: y[7] = 1'b1;
endcase
end
end
endmodule
4.4 编码器:把独热码变成二进制
编码器是译码器的逆过程。输入是2^N位的独热码,输出是N位二进制数。
4.4.1 8-3编码器
module encoder_8to3 (
input [7:0] y,
output reg [2:0] addr
);
always @(*) begin
case (y)
8'b00000001: addr = 3'b000;
8'b00000010: addr = 3'b001;
8'b00000100: addr = 3'b010;
8'b00001000: addr = 3'b011;
8'b00010000: addr = 3'b100;
8'b00100000: addr = 3'b101;
8'b01000000: addr = 3'b110;
8'b10000000: addr = 3'b111;
default: addr = 3'b000;
endcase
end
endmodule
4.4.2 优先级编码器
实际应用中,输入可能不是严格的独热码。比如多个按键同时按下,怎么办?优先级编码器应运而生:优先级高的输入优先编码。
module priority_encoder_8to3 (
input [7:0] y,
output reg [2:0] addr,
output reg valid
);
always @(*) begin
valid = 1'b1;
casez (y)
8'b1???????: addr = 3'b111;
8'b01??????: addr = 3'b110;
8'b001?????: addr = 3'b101;
8'b0001????: addr = 3'b100;
8'b00001???: addr = 3'b011;
8'b000001??: addr = 3'b010;
8'b0000001?: addr = 3'b001;
8'b00000001: addr = 3'b000;
default: begin
addr = 3'b000;
valid = 1'b0;
end
endcase
end
endmodule
注意这里用了casez,问号表示不关心该位。优先级从高到低:y[7] > y[6] > ... > y[0]。
- 加法器:行波进位面积小但速度慢,超前进位速度快但面积大
- 多路选择器:case语句要写default,避免生成锁存器
- 译码器:输出是独热码,常用于地址译码和显示驱动
- 编码器:优先级编码器用casez实现,问号表示不关心位
好了,这四种组合逻辑模块,是数字电路设计的基本功。你想想看,一个CPU里,指令译码用译码器,寄存器堆选通用多路选择器,ALU里全是加法器,中断控制器里用优先级编码器。把这些模块吃透了,复杂设计也就水到渠成。
下一章我们讲时序逻辑,触发器、计数器、移位寄存器。到时候我会分享一个我做SPI控制器时遇到的坑——计数器没考虑格雷码转换,导致跨时钟域采样出错。嗯,到时候细聊。