4、组合逻辑设计:基本门电路实现、多路选择器、编码器与译码器、加法器设计
组合逻辑,说白了就是“输入一变,输出立马跟着变”的逻辑。没有时钟,没有状态,纯粹靠门电路搭出来的直来直去的逻辑。我刚开始学FPGA时,觉得这玩意儿太简单了,不就是与或非嘛。后来做项目才发现,越是基础的东西,坑越多。今天咱们就把这四大块掰开揉碎了聊一聊。
4.1 基本门电路实现
基本门电路是数字世界的“原子”。与门、或门、非门、与非门、或非门、异或门,就这六种。在FPGA里,你不需要真的去画晶体管,直接用Verilog描述就行。但理解它们的硬件行为,比背代码重要得多。
举个例子,与非门。它的特点是:只要有一个输入是0,输出就是1;只有全部输入是1,输出才是0。我当年调试一个SPI接口,发现时钟信号总是不对,查了半天,原来是一个与非门的输入顺序搞反了。嗯,这种低级错误,犯过一次就记住了。
核心要点:FPGA内部实际只有查找表(LUT),基本门电路是通过LUT配置实现的。你写的 assign y = a & b;,综合工具会自动映射到LUT里。
// 基本门电路 Verilog 实现
module basic_gates(
input a, b,
output and_out, or_out, not_out,
output nand_out, nor_out, xor_out
);
assign and_out = a & b; // 与门
assign or_out = a | b; // 或门
assign not_out = ~a; // 非门
assign nand_out = ~(a & b); // 与非门
assign nor_out = ~(a | b); // 或非门
assign xor_out = a ^ b; // 异或门
endmodule
个人习惯:我写组合逻辑时,尽量用 assign 而不是 always @(*)。原因很简单:assign 更直观,综合结果也更可控。除非需要 if-else 或 case 结构,我才用 always 块。
4.2 多路选择器
多路选择器(MUX)是组合逻辑里的“开关”。从多路输入中选一路输出。2选1、4选1、8选1,原理都一样。你想想看,CPU里的寄存器堆、数据通路,到处都是MUX的身影。
实现MUX有两种方式:三目运算符和case语句。我个人偏爱case语句,因为可读性更好,尤其是选择路数多的时候。
// 4选1多路选择器
module mux4_1(
input [1:0] sel,
input [3:0] data_in, // 4路输入,每路1位
output reg mux_out
);
always @(*) begin
case(sel)
2'b00: mux_out = data_in[0];
2'b01: mux_out = data_in[1];
2'b10: mux_out = data_in[2];
2'b11: mux_out = data_in[3];
default: mux_out = 1'b0; // 安全起见,加default
endcase
end
endmodule
避坑指南:我曾经在项目里漏写了default分支,结果综合出来一堆锁存器(latch)。锁存器在组合逻辑里是大忌,时序分析会变得一团糟。记住:case语句一定要写default,if-else一定要写else。
4.3 编码器与译码器
编码器和译码器,这俩是“互逆”的关系。编码器把多个输入线压缩成较少的输出线;译码器则反过来,把较少的输入线扩展成多个输出线。
最常见的例子:键盘扫描。按下一个键,编码器输出对应的键值。显示驱动里,译码器把二进制数转成七段数码管的段选信号。
| 类型 | 输入 | 输出 | 典型应用 |
|---|---|---|---|
| 4-2线编码器 | 4位(独热码) | 2位二进制 | 中断优先级编码 |
| 3-8线译码器 | 3位二进制 | 8位(独热码) | 地址译码、片选信号 |
// 3-8线译码器
module decoder3_8(
input [2:0] addr,
output reg [7:0] y
);
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;
default: y = 8'b0000_0000;
endcase
end
endmodule
经验之谈:编码器设计时,要注意输入是否互斥。如果多个输入同时有效,优先级编码器就派上用场了。我做过一个多传感器采集系统,8个中断源共用一根中断线,就是用优先级编码器解决的。
4.4 加法器设计
加法器是算术运算的基石。从最简单的半加器,到全加器,再到行波进位加法器、超前进位加法器,一层层递进。你可能会问:FPGA里不是直接用 + 就行了吗?没错,但理解底层实现,对优化性能很有帮助。
半加器:两个1位二进制数相加,产生和与进位。全加器:多了一个进位输入,可以级联成多位加法器。
// 1位全加器
module full_adder(
input a, b, cin,
output sum, cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
// 4位行波进位加法器
module ripple_carry_adder4(
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
性能对比:行波进位加法器结构简单,但进位像多米诺骨牌一样逐级传递,延迟随位宽线性增长。超前进位加法器通过并行计算进位,延迟只跟位宽的对数有关。在高速设计中,我建议用超前进位结构,或者直接用综合工具的 + 运算符,它会自动优化。
我记得有一次做图像处理加速器,需要做16位加法,频率要求200MHz。用行波进位结构,时序怎么都跑不过。换成超前进位后,问题迎刃而解。所以说,基础理论扎实了,遇到性能瓶颈时才知道往哪个方向使劲。
知识体系总览
下面这张图,把本章的核心逻辑串起来了。从基本门电路出发,组合成MUX、编码器、译码器、加法器。这些模块再往上,就是更复杂的运算单元和数据通路。
组合逻辑设计,说白了就是“搭积木”。基本门电路是积木块,MUX、编码器、译码器、加法器是搭好的小模块。把这些小模块组合起来,就能实现复杂的功能。嗯,这里要注意:组合逻辑的输出只取决于当前输入,跟历史状态无关。这个特性决定了它适合做数据通路,不适合做状态机。
最后一句心里话:别小看这些基础模块。我见过很多工程师,一上来就写复杂算法,结果综合出来的电路又大又慢。回过头来,用几个MUX和加法器重新设计,性能翻倍。基础打牢了,上层建筑才稳当。
公众号:蓝海资料掘金营,微信deep3321