4、组合逻辑设计:基本门电路实现(与或非)、多路选择器、加法器、编码器与译码器
组合逻辑,说白了就是电路里最直来直去的那部分——输入一变,输出立马跟着变,不带半点延迟记忆。我刚开始用紫光同创的Pango Design Suite时,第一件事就是把基本门电路跑通。你想想看,再复杂的数字系统,底层都是这些与或非门搭起来的。
4.1 基本门电路:与、或、非
在FPGA里,基本门电路其实不是真的用分立晶体管搭的,而是通过查找表(LUT)来实现。但作为设计者,我们直接用Verilog描述就行。
先看最简单的——非门。我习惯叫它反相器:
// 非门实现
module not_gate(
input wire a,
output wire y
);
assign y = ~a;
endmodule
与门和或门就更直接了:
// 与门
module and_gate(
input wire a, b,
output wire y
);
assign y = a & b;
endmodule
// 或门
module or_gate(
input wire a, b,
output wire y
);
assign y = a | b;
endmodule
个人习惯:我写组合逻辑时,能用assign尽量用assign。为什么?因为代码更简洁,综合器也更容易优化。但如果是复杂逻辑,我会改用always @(*)块,方便加中间变量做调试。
嗯,这里要注意——紫光同创的Pango综合工具对连续赋值语句支持得很好,但如果你用always块,记得把敏感列表写完整。我见过有人漏了敏感信号,结果综合出来的电路和仿真对不上,查了半天才发现问题。
4.2 多路选择器:数据选择的艺术
多路选择器(MUX)在项目中太常用了。比如你要从多个传感器数据中选一个送给处理模块,或者做寄存器写回路径选择。
最简单的二选一MUX:
// 2选1多路选择器
module mux2to1(
input wire [7:0] data0, data1,
input wire sel,
output reg [7:0] y
);
always @(*) begin
case(sel)
1'b0: y = data0;
1'b1: y = data1;
default: y = data0;
endcase
end
endmodule
我在项目中遇到过一个问题:用case语句写MUX时,一定要加default分支。为什么?因为如果不加,综合器可能会推断出锁存器。锁存器这东西,在组合逻辑里出现就是个大坑——时序分析会变得一团糟。
四选一MUX也很常见:
// 4选1多路选择器
module mux4to1(
input wire [7:0] data0, data1, data2, data3,
input wire [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
避坑指南:我曾经在紫光同创的PDS项目里,用三目运算符写了一个8选1的MUX,结果综合出来的LUT数量比用case多了30%。后来我查了文档才知道,Pango对case语句的优化更好。所以,复杂MUX建议用case,别偷懒用三目。
4.3 加法器:从半加器到全加器
加法器是算术运算的基础。半加器只考虑两个输入相加,不考虑进位输入:
// 半加器
module half_adder(
input wire a, b,
output wire sum, cout
);
assign sum = a ^ b; // 异或得到和
assign cout = a & b; // 与得到进位
endmodule
全加器则多了进位输入:
// 全加器
module full_adder(
input wire a, b, cin,
output wire sum, cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
你想想看,如果要做8位加法器,最简单的方法就是把8个全加器串起来——这就是行波进位加法器。但问题是,进位要一级一级传,延迟会很大。
// 8位行波进位加法器
module ripple_carry_adder_8bit(
input wire [7:0] a, b,
input wire cin,
output wire [7:0] sum,
output wire cout
);
wire [7:0] c;
full_adder fa0 (a[0], b[0], cin, sum[0], c[0]);
full_adder fa1 (a[1], b[1], c[0], sum[1], c[1]);
full_adder fa2 (a[2], b[2], c[1], sum[2], c[2]);
full_adder fa3 (a[3], b[3], c[2], sum[3], c[3]);
full_adder fa4 (a[4], b[4], c[3], sum[4], c[4]);
full_adder fa5 (a[5], b[5], c[4], sum[5], c[5]);
full_adder fa6 (a[6], b[6], c[5], sum[6], c[6]);
full_adder fa7 (a[7], b[7], c[6], sum[7], c[7]);
assign cout = c[7];
endmodule
实际项目经验:我在做一款高速数据采集卡时,用了行波进位加法器做地址累加,结果时序跑不到200MHz。后来换成紫光同创IP核里的超前进位加法器,频率直接飙到350MHz。所以,如果对速度有要求,别自己手写加法器,直接用IP核或者用“+”运算符让综合器去优化。
4.4 编码器与译码器
编码器,说白了就是把多个输入信号压缩成更少的输出位。比如8线-3线优先编码器:
// 8线-3线优先编码器
module priority_encoder_8to3(
input wire [7:0] din,
output reg [2:0] dout,
output reg valid
);
always @(*) begin
casez(din)
8'b1???????: dout = 3'd7;
8'b01??????: dout = 3'd6;
8'b001?????: dout = 3'd5;
8'b0001????: dout = 3'd4;
8'b00001???: dout = 3'd3;
8'b000001??: dout = 3'd2;
8'b0000001?: dout = 3'd1;
8'b00000001: dout = 3'd0;
default: dout = 3'd0;
endcase
valid = |din; // 只要有一位输入有效,valid就为1
end
endmodule
译码器正好反过来,把少量输入展开成大量输出。3线-8线译码器:
// 3线-8线译码器
module decoder_3to8(
input wire [2:0] addr,
output reg [7:0] dout
);
always @(*) begin
case(addr)
3'b000: dout = 8'b00000001;
3'b001: dout = 8'b00000010;
3'b010: dout = 8'b00000100;
3'b011: dout = 8'b00001000;
3'b100: dout = 8'b00010000;
3'b101: dout = 8'b00100000;
3'b110: dout = 8'b01000000;
3'b111: dout = 8'b10000000;
default: dout = 8'b00000000;
endcase
end
endmodule
我建议:写译码器时,可以用移位操作来简化代码——dout = 1'b1 << addr。这样不仅代码短,而且综合出来的结果和case语句一样。但要注意,如果addr超出范围,移位操作可能会出问题,所以最好加个范围检查。
4.5 组合逻辑的常见陷阱
做组合逻辑设计,有几个坑我踩过,分享给你:
- 锁存器推断:在always块里,如果某个分支没给所有输出赋值,综合器就会推断出锁存器。解决办法是——要么在always块开头给所有输出赋默认值,要么确保case语句有default分支。
- 组合逻辑环路:千万别把组合逻辑的输出直接反馈到自己的输入。我曾经在调试一个状态机时,不小心把组合逻辑的输出又打回了输入,结果仿真直接卡死,综合也报错。
- 毛刺问题:组合逻辑的输出可能会有毛刺。如果后续接的是时钟触发的寄存器,问题不大。但如果直接驱动异步复位或时钟使能,那就麻烦了。我一般会在关键路径上加个输出寄存器来消除毛刺。
曾经踩过的坑:我在紫光同创的FPGA上做LED扫描显示时,直接用组合逻辑输出片选信号,结果LED老是闪烁。后来用示波器一看,片选信号上全是毛刺。加了一级寄存器打一拍,问题就解决了。所以,驱动外部引脚的信号,最好都经过寄存器输出。
好了,这一章的内容就这些。组合逻辑是数字设计的基础,看似简单,但细节很多。建议你拿到紫光同创的开发板后,先把这些基本门电路、MUX、加法器、编码器译码器都跑一遍仿真,再下载到板子上看看实际效果。下一章我们会讲时序逻辑,那才是FPGA设计的重头戏。