第四章:组合逻辑设计——从基本门到实用电路
各位同学,欢迎来到第四章。这一章我们聊聊组合逻辑。说白了,组合逻辑就是那种“输入一变,输出立马跟着变”的电路,没有记忆功能,不依赖时钟。我刚开始学CPLD时,觉得这东西太简单了,不就是几个门嘛。后来做项目才发现,组合逻辑用好了,能解决很多实际问题。
4.1 基本门电路:与、或、非、异或
基本门电路是数字世界的“原子”。你想想看,再复杂的芯片,底层也就是这些门的组合。咱们一个一个过。
4.1.1 与门(AND)
与门的逻辑:所有输入为1时,输出才为1。用乘法表示就是 Y = A & B。我在项目中遇到过一个问题:两个使能信号必须同时有效才能启动某个模块,这时候用与门就对了。
// Verilog 与门
module and_gate(
input wire a,
input wire b,
output wire y
);
assign y = a & b;
endmodule
4.1.2 或门(OR)
或门的逻辑:只要有一个输入为1,输出就是1。用加法表示 Y = A | B。嗯,这里要注意:或门在控制信号合并时特别常用。比如多个中断源,任何一个触发都要通知CPU,用或门最直接。
assign y = a | b;
4.1.3 非门(NOT)
非门就是取反,输入0输出1,输入1输出0。Y = ~A。我个人习惯把非门叫做“反相器”。在CPLD里,非门经常用来产生互补信号。
assign y = ~a;
4.1.4 异或门(XOR)
异或门:输入不同时输出1,相同时输出0。Y = A ^ B。这个门很有意思。我曾经用它来做数据校验,两个数据流做异或,结果一致就说明传输没问题。
assign y = a ^ b;
重要提示: 在CPLD中,基本门电路通常会被综合工具自动映射到查找表(LUT)中。你写 &、|、~、^ 这些运算符,工具会帮你优化成最合适的实现方式。但理解底层逻辑,对排查问题很有帮助。
4.2 多路选择器(MUX)
多路选择器,说白了就是一个“数据开关”。从多个输入中选一个送到输出。2选1多路选择器是最基本的,控制信号sel为0选a,为1选b。
// 2选1多路选择器
module mux2to1(
input wire a,
input wire b,
input wire sel,
output wire y
);
assign y = sel ? b : a;
endmodule
为什么我要单独讲MUX?因为在实际项目中,MUX的使用频率非常高。我记得有一次做数据通道切换,需要从8路传感器数据中选一路处理,用8选1MUX,一行代码就搞定了。
多路选择器还可以用case语句实现,适合多路选择的情况:
// 4选1多路选择器
module mux4to1(
input wire [1:0] sel,
input wire [3:0] data_in,
output reg data_out
);
always @(*) begin
case(sel)
2'b00: data_out = data_in[0];
2'b01: data_out = data_in[1];
2'b10: data_out = data_in[2];
2'b11: data_out = data_in[3];
default: data_out = 1'b0;
endcase
end
endmodule
个人经验: 用case语句写MUX时,一定要写default分支。我曾经漏掉default,结果综合出来一堆锁存器,仿真没问题,上板子就乱跳。查了两天才发现是这个问题。记住:组合逻辑中,所有分支都要覆盖,否则会生成latch。
4.3 加法器
加法器是算术运算的基础。半加器只考虑两个输入相加,不考虑进位输入。全加器则考虑进位输入。
4.3.1 半加器
module half_adder(
input wire a,
input wire b,
output wire sum,
output wire carry
);
assign sum = a ^ b; // 和
assign carry = a & b; // 进位
endmodule
4.3.2 全加器
module full_adder(
input wire a,
input wire b,
input wire cin,
output wire sum,
output wire cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
你想想看,把多个全加器串联起来,就构成了行波进位加法器。但这里有个坑:进位是一级一级传下去的,位数多了延迟会很大。我在做高速设计时,就遇到过加法器成为关键路径的情况。解决办法是用超前进位加法器,但那个逻辑复杂一些,咱们后面再聊。
避坑指南: 我曾经在项目中直接用“+”运算符做32位加法,综合工具自动生成了行波进位加法器。结果时序分析报错,最高频率只能跑到50MHz,而设计要求是100MHz。后来我改成了流水线结构,分两级做加法,才满足时序要求。所以,大位宽加法器要注意时序问题。
4.4 译码器
译码器把输入的二进制编码转换成对应的输出信号。3-8译码器是最经典的:3位输入,8位输出,每个输出对应一个输入组合。
// 3-8译码器
module decoder3to8(
input wire [2:0] in,
output reg [7:0] out
);
always @(*) begin
case(in)
3'b000: out = 8'b00000001;
3'b001: out = 8'b00000010;
3'b010: out = 8'b00000100;
3'b011: out = 8'b00001000;
3'b100: out = 8'b00010000;
3'b101: out = 8'b00100000;
3'b110: out = 8'b01000000;
3'b111: out = 8'b10000000;
default: out = 8'b00000000;
endcase
end
endmodule
译码器在实际中有什么用?我举两个例子:一是地址译码,CPU访问外设时,通过地址线译码产生片选信号;二是七段数码管显示,把二进制数译成段码。
嗯,这里要提一下:译码器也可以用for循环实现,代码更简洁:
always @(*) begin
out = 8'b0;
for (int i = 0; i < 8; i++) begin
if (in == i)
out[i] = 1'b1;
end
end
核心总结: 组合逻辑设计的本质就是布尔代数。与、或、非是基础,MUX做选择,加法器做运算,译码器做转换。这些模块组合起来,就能实现复杂功能。我个人建议,初学者先把这几个基本电路吃透,多写代码、多仿真,后面学状态机、时序逻辑就轻松多了。
公众号:蓝海资料掘金营,微信deep3321