第三章:组合逻辑设计——从门电路到实用模块
各位同学,欢迎来到组合逻辑设计的实战环节。说实话,很多初学者觉得数字电路就是一堆门电路拼拼凑凑,没什么技术含量。但我在项目中吃过不少亏,才明白一个道理:组合逻辑设计,是FPGA设计的基石,也是最容易出bug的地方。今天我们就从最基础的门电路开始,一步步搭建出实用的功能模块。
3.1 基本门电路的Verilog实现
先说说最基本的与、或、非、异或门。这些门电路在Verilog里实现起来非常简单,但要注意的是——综合工具会怎么理解你的代码。我见过不少新手写出来的代码,仿真没问题,一上板子就翻车,问题往往就出在基本门电路的写法上。
核心原则:组合逻辑用 assign 连续赋值语句,或者 always @(*) 过程块。我个人习惯用 assign,代码更简洁,综合结果也更可控。
// 基本门电路示例
module basic_gates(
input wire a, b,
output wire y_and, // 与门输出
output wire y_or, // 或门输出
output wire y_not, // 非门输出
output wire y_xor // 异或门输出
);
// 使用 assign 实现组合逻辑
assign y_and = a & b; // 与门
assign y_or = a | b; // 或门
assign y_not = ~a; // 非门
assign y_xor = a ^ b; // 异或门
endmodule
嗯,这里要注意一点:非门用 ~ 而不是 !。 ~ 是按位取反,! 是逻辑非。在单bit信号上两者结果一样,但到了多bit总线,用 ! 就会出大问题。我曾经在调试一个8位数据总线时,就因为把 ~ 写成了 !,结果仿真通过了,上板子数据全乱套。排查了整整两天才找到这个低级错误。
3.2 多路选择器(MUX)
多路选择器,说白了就是一个数据开关。根据选择信号,从多个输入中选一个输出。实际项目中,MUX的用途非常广泛——比如在状态机里切换数据路径,或者做寄存器配置的地址译码。
实现MUX有两种主流写法:三目运算符和case语句。我个人更推荐用case语句,尤其是当输入路数超过4路时,case语句的可读性和可维护性都更好。
// 4选1多路选择器 - 使用case语句
module mux4to1(
input wire [1:0] sel, // 选择信号
input wire [3:0] data, // 4路输入,每路1bit
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; // 安全起见,加default
endcase
end
endmodule
避坑指南:我曾经在写一个8选1的MUX时,漏掉了default分支。结果综合工具给我生成了一个锁存器(latch),导致整个模块时序全乱。记住:组合逻辑的case语句一定要写default,否则综合工具会推断出你不想要的锁存器。
3.3 加法器:半加器与全加器
加法器是算术运算的基础。半加器只能处理两个bit相加,不考虑进位输入;全加器则多了一个进位输入,可以级联成多位加法器。
先看半加器,逻辑很简单:和 = A XOR B,进位 = A AND B。
// 半加器
module half_adder(
input wire a, b,
output wire sum, // 和
output wire carry // 进位
);
assign sum = a ^ b;
assign carry = a & b;
endmodule
全加器比半加器多了一个进位输入 cin。公式稍微复杂一点:
// 全加器
module full_adder(
input wire a, b, cin,
output wire sum,
output wire cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
你想想看,为什么全加器的进位输出要用三个与项相或?说白了,就是只要任意两个输入为1,进位就为1。这个逻辑在FPGA里综合出来就是几个LUT,效率很高。
实际项目经验:我在做图像处理加速器时,需要做大量像素值的累加。如果用全加器逐级级联,延迟会很大。后来我改用进位保留加法器(CSA)结构,把三个数的加法变成两个数的加法,时序一下子就满足了。所以,基础模块的选型要结合具体场景,不是所有加法器都用全加器级联。
3.4 编码器与译码器
编码器和译码器是一对逆操作。编码器把多个输入信号压缩成较少的输出信号;译码器则把较少的输入信号扩展成多个输出信号。
先看一个4线-2线编码器:
// 4线-2线编码器(优先级编码)
module encoder_4to2(
input wire [3:0] in, // 输入,高电平有效
output reg [1:0] out // 编码输出
);
always @(*) begin
casex(in) // 使用casex,支持无关位
4'b0001: out = 2'b00;
4'b0010: out = 2'b01;
4'b0100: out = 2'b10;
4'b1000: out = 2'b11;
default: out = 2'b00;
endcase
end
endmodule
译码器正好反过来,2线-4线译码器:
// 2线-4线译码器
module decoder_2to4(
input wire [1:0] in,
output reg [3:0] out
);
always @(*) begin
case(in)
2'b00: out = 4'b0001;
2'b01: out = 4'b0010;
2'b10: out = 4'b0100;
2'b11: out = 4'b1000;
default: out = 4'b0000;
endcase
end
endmodule
重要提醒:编码器要注意输入互斥的问题。如果多个输入同时有效,优先级编码器会按照case语句的顺序处理。我在一个中断控制器项目中,就遇到过两个中断同时触发,结果优先级低的那个被忽略了。解决方案是:要么保证输入互斥,要么显式定义优先级。
3.5 本章知识体系总览
为了让大家更直观地理解本章的知识结构,我画了一张图:
这张图把本章的四个核心模块串联起来了。你会发现,从基本门电路到MUX、加法器、编码器/译码器,本质上都是在做同一件事——用逻辑表达式描述输入和输出之间的关系。这就是组合逻辑的精髓。
3.6 实战要点总结
最后,我把自己这些年踩过的坑总结成几条铁律,你们记好了:
- 组合逻辑一定要写全所有分支——case语句加default,if-else加else,否则综合出锁存器
- 区分按位运算符和逻辑运算符——
&vs&&,|vs||,~vs! - 多bit信号用向量,不要拆成单bit——比如
wire [3:0] data比wire d0,d1,d2,d3好维护得多 - 仿真通过不代表综合通过——一定要跑综合后仿真,或者至少检查综合报告里的warning
嗯,说到综合报告,我建议你们每次综合完都看一眼「Inferred Latch」这个条目。如果发现综合工具推断出了锁存器,而你的设计里本不该有,那就要回去检查代码了。这个习惯帮我避免了好几次流片失败的风险。
好了,第三章的内容就到这里。这些基础模块虽然简单,但它们是后续所有复杂设计的基础。下一章我们会进入时序逻辑的世界,那才是真正考验功力的时候。