第四章 组合逻辑设计:从基本门到复杂功能
组合逻辑,说白了就是「输入一变,输出立马变」的电路。没有时钟,没有记忆,纯粹靠逻辑门搭出来的功能模块。我在刚接触FPGA时,觉得这玩意儿太简单了——不就是与或非嘛。但真正做项目才发现,组合逻辑里的坑,一个比一个深。
4.1 基本门电路实现
FPGA里的基本门,其实不是真的用晶体管搭的。它用的是查找表(LUT)。但咱们写代码时,还是按传统逻辑门来思考。
看个最简单的例子:
// 基本门电路
module basic_gates(
input a, b,
output y_and,
output y_or,
output y_not,
output y_nand,
output y_nor,
output y_xor
);
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; // 异或门
endmodule
嗯,这里要注意:Verilog里的位运算符和逻辑运算符是两码事。&是位与,&&是逻辑与。我见过有人把这两个搞混,仿真没问题,上板子就出bug。
4.2 多路选择器
多路选择器,简称MUX。这东西在FPGA里太常见了。数据选择、地址译码、状态跳转,哪哪都有它。
实现方式有两种:
// 方式一:三目运算符
module mux_2to1(
input [7:0] data0, data1,
input sel,
output [7:0] y
);
assign y = sel ? data1 : data0;
endmodule
// 方式二:case语句
module mux_4to1(
input [7:0] data0, data1, data2, data3,
input [1:0] sel,
output reg [7:0] y
);
always @(*) begin
case(sel)
2'b00: y = data0;
2'b01: y = data1;
2'b10: y = data2;
2'b11: y = data3;
default: y = data0; // 防锁存器
endcase
end
endmodule
为什么会这样?因为综合工具很死板。它看到case分支没覆盖全,就会认为「某些输入组合下输出要保持原值」,于是给你生成锁存器。你想想看,这根本不是你想要的行为。
4.3 加法器
加法器是算术运算的基础。FPGA里做加法,最简单就是用+号。但如果你想深入理解,就得看看全加器是怎么搭的。
// 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 adder_4bit(
input [3:0] a, b,
input cin,
output [3:0] sum,
output cout
);
wire [3:0] c;
full_adder u0(.a(a[0]), .b(b[0]), .cin(cin), .sum(sum[0]), .cout(c[0]));
full_adder u1(.a(a[1]), .b(b[1]), .cin(c[0]), .sum(sum[1]), .cout(c[1]));
full_adder u2(.a(a[2]), .b(b[2]), .cin(c[1]), .sum(sum[2]), .cout(c[2]));
full_adder u3(.a(a[3]), .b(b[3]), .cin(c[2]), .sum(sum[3]), .cout(c[3]));
assign cout = c[3];
endmodule
行波加法器结构简单,但速度慢。为什么呢?进位是一级一级传下去的。4位还好,32位的话,进位链就太长了。我在项目中用过一次32位行波加法器,时序跑不到200MHz,后来换成超前进位加法器才解决。
4.4 编码器与译码器
编码器和译码器,这俩是互逆操作。编码器把多个输入线压缩成少数几位,译码器把少数几位展开成多个输出线。
// 8-3编码器(优先编码器)
module priority_encoder_8to3(
input [7:0] in,
output [2:0] out,
output valid
);
assign out = in[7] ? 3'd7 :
in[6] ? 3'd6 :
in[5] ? 3'd5 :
in[4] ? 3'd4 :
in[3] ? 3'd3 :
in[2] ? 3'd2 :
in[1] ? 3'd1 :
in[0] ? 3'd0 : 3'd0;
assign valid = |in; // 只要有一个输入为1,valid就为1
endmodule
// 3-8译码器
module decoder_3to8(
input [2:0] in,
input enable,
output reg [7:0] out
);
always @(*) begin
out = 8'b0;
if(enable) begin
case(in)
3'b000: out[0] = 1'b1;
3'b001: out[1] = 1'b1;
3'b010: out[2] = 1'b1;
3'b011: out[3] = 1'b1;
3'b100: out[4] = 1'b1;
3'b101: out[5] = 1'b1;
3'b110: out[6] = 1'b1;
3'b111: out[7] = 1'b1;
endcase
end
end
endmodule
优先编码器在实际中更常用。比如按键检测,多个按键同时按下时,优先编码器只响应优先级最高的那个。我做过一个键盘扫描模块,用的就是优先编码器,省了不少逻辑资源。
知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以看到,基本门是基础,往上搭出MUX和加法器,再往上就是编码器译码器这些功能模块。
避坑指南
做组合逻辑设计,有几个坑我反复踩过,今天一并告诉你:
- 锁存器陷阱:always @(*) 里 if 没有 else,case 没有 default,都会生成锁存器。综合报告里看到 "Latch inferred" 就要警惕。
- 组合逻辑环路:输出反馈到输入,中间没有寄存器。这种电路仿真可能振荡,综合可能报错。我见过有人把 assign a = b & c; assign c = a | d; 这么写,综合出来直接崩了。
- 位宽不匹配:比如 4 位加 4 位,结果用 4 位存,进位就丢了。要么扩位宽,要么单独处理进位。
- 敏感列表不全:always @(a or b) 里用了 c,但敏感列表没写 c。仿真时 c 变了不会触发 always,结果就错了。现在我都用 always @(*),省心。
组合逻辑是FPGA设计的基石。别看它简单,真正用好用对,需要积累经验。我建议你多写多练,把每个模块都自己敲一遍。遇到问题别怕,那是成长最快的时候。
可可精品资料库