4. 组合逻辑设计:基本门电路实现、多路选择器、加法器/减法器、编码器/译码器、比较器
各位同学,咱们今天聊聊组合逻辑。说实话,这是FPGA设计的基石,也是你写Verilog代码时最常打交道的东西。我见过不少新手,上来就写always块,结果综合出来一堆莫名其妙的LUT,其实就是组合逻辑没搞明白。
组合逻辑的特点是:输出只取决于当前输入,跟历史状态没关系。说白了,就是“输入一变,输出立马变”,没有记忆功能。你想想看,我们平时用的与门、或门、加法器,都属于这一类。
核心要点:组合逻辑没有时钟沿触发,没有寄存器,只有纯逻辑运算。综合工具会把它映射成查找表(LUT)和进位链。
4.1 基本门电路实现
基本门电路是组合逻辑的“原子操作”。在FPGA里,你写的每个逻辑表达式,最终都会被综合工具拆解成这些基本门。我个人习惯用 assign 语句来实现,简单直观。
// 基本门电路示例
module basic_gates(
input wire a, b,
output wire y_and, // 与门
output wire y_or, // 或门
output wire y_not, // 非门
output wire y_nand, // 与非门
output wire y_nor, // 或非门
output wire y_xor, // 异或门
output wire y_xnor // 同或门
);
assign y_and = a & b;
assign y_or = a | b;
assign y_not = ~a;
assign y_nand = ~(a & b);
assign y_nor = ~(a | b);
assign y_xor = a ^ b;
assign y_xnor = a ~^ b; // 或者 ~(a ^ b)
endmodule
小技巧:在FPGA中,异或门(XOR)的延迟通常比与门和或门大一些。如果你在做高速设计,尽量少用长链的异或运算。我曾经在一个视频处理项目里,就因为异或链太长导致时序违例,后来改成查表才解决。
4.2 多路选择器
多路选择器(MUX)在FPGA里太常见了。说白了,就是根据选择信号,从多个输入里挑一个输出。FPGA的LUT本质上就是一个小的MUX,所以综合工具对MUX的优化很成熟。
实现MUX有两种主流方式:case 语句和 if-else 语句。我建议你记住一个原则:case语句生成的是并行MUX,if-else生成的是优先级MUX。
// 4选1多路选择器 - 使用case语句
module mux4_case(
input wire [1:0] sel,
input wire [3:0] data_in, // 4位输入,每位对应一个通道
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; // 防止锁存器
endcase
end
endmodule
注意:写组合逻辑的always块时,一定要把所有分支覆盖完整。漏掉分支会综合出锁存器(latch),这在FPGA设计中是大忌。我刚开始学的时候,就因为这个吃过亏——仿真没问题,上板子就乱跳,查了两天才发现是latch在作怪。
4.3 加法器与减法器
加法器是算术运算的核心。FPGA里有专用的进位链(Carry Chain),所以加法器的实现效率很高。你想想看,一个32位加法器,如果用纯LUT实现,延迟会很大;但用进位链,每个bit只多一级进位延迟。
最简单的加法器是半加器,只考虑两个输入,不考虑进位。全加器则多了进位输入。实际项目中,我们通常直接用 + 运算符,综合工具会自动调用进位链。
// 全加器 - 行为级描述
module full_adder(
input wire a, b, cin,
output wire sum, cout
);
assign {cout, sum} = a + b + cin;
endmodule
// 8位加法器 - 直接用+运算符
module adder_8bit(
input wire [7:0] a, b,
input wire cin,
output wire [7:0] sum,
output wire cout
);
assign {cout, sum} = a + b + cin;
endmodule
减法器其实和加法器是一回事。在数字电路里,减法就是加补码。比如 a - b 等价于 a + (~b + 1)。所以很多FPGA设计里,加减法共用一套硬件,通过一个控制信号切换。
经验之谈:如果你需要同时做加法和减法,可以设计一个加减法共用模块。用 add_sub 信号控制:为1时做减法(对b取反再加1),为0时做加法。这样能省一半的LUT资源。
4.4 编码器与译码器
编码器和译码器是互逆操作。编码器把多个输入线压缩成较少的输出线;译码器则反过来,把较少的输入线扩展成多个输出线。
最常见的例子是优先编码器。我记得有一次做按键扫描,8个按键对应3位编码,用的就是优先编码器——哪个按键优先级高,就输出哪个的编码。
// 8线-3线优先编码器
module priority_encoder(
input wire [7:0] in, // 输入,低电平有效
output reg [2:0] out, // 编码输出
output reg valid // 是否有有效输入
);
always @(*) begin
casez(in)
8'b0???????: out = 3'b111; // 优先级最高
8'b10??????: out = 3'b110;
8'b110?????: out = 3'b101;
8'b1110????: out = 3'b100;
8'b11110???: out = 3'b011;
8'b111110??: out = 3'b010;
8'b1111110?: out = 3'b001;
8'b11111110: out = 3'b000;
default: begin
out = 3'b000;
valid = 1'b0;
end
endcase
valid = |in; // 只要有一位为0,valid就为1
end
endmodule
4.5 比较器
比较器用来判断两个数的大小关系。FPGA里实现比较器,可以用 >、<、== 这些运算符,也可以自己用减法器实现——比较 a - b 的符号位就能知道谁大谁小。
不过要注意,用减法器做比较会有延迟,而且会消耗进位链资源。如果只是简单的相等判断,用 == 就够了,综合工具会优化成LUT。
// 4位比较器
module comparator_4bit(
input wire [3:0] a, b,
output reg a_gt_b, // a大于b
output reg a_eq_b, // a等于b
output reg a_lt_b // a小于b
);
always @(*) begin
a_gt_b = (a > b);
a_eq_b = (a == b);
a_lt_b = (a < b);
end
endmodule
优化建议:如果你只需要判断是否相等,别用减法器。直接用 == 运算符,综合工具会生成一个异或树,比减法器快得多。我在做图像像素比较时,就吃过这个亏——用减法器做相等判断,白白浪费了进位链资源。
4.6 组合逻辑的常见陷阱
最后,我总结几个组合逻辑设计里容易踩的坑:
- 锁存器(Latch):always块里条件分支没写全,或者case没加default,都会生成latch。记住:组合逻辑的always块里,所有输出必须在所有分支里赋值。
- 组合逻辑环路:输出反馈到输入,中间没有寄存器。这会导致仿真振荡,综合报错。我曾经在调试一个握手协议时,不小心把ready信号绕回去了,仿真直接卡死。
- 毛刺(Glitch):组合逻辑的输出会有短暂的错误状态。如果后面接的是时钟采样,可能会采到错误值。解决办法是加寄存器打一拍,或者用格雷码。
- 扇出过大:一个信号驱动太多负载,会导致延迟增加。如果发现时序违例,可以手动复制寄存器来分担负载。
嗯,组合逻辑这部分就讲到这里。内容虽然基础,但真的很重要。你想想看,FPGA里90%的逻辑都可以用组合逻辑加寄存器来实现。把基础打牢了,后面学状态机、学时序分析才会轻松。