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、编码器、译码器、加法器是搭好的小模块。把这些小模块组合起来,就能实现复杂的功能。嗯,这里要注意:组合逻辑的输出只取决于当前输入,跟历史状态无关。这个特性决定了它适合做数据通路,不适合做状态机。

最后一句心里话:别小看这些基础模块。我见过很多工程师,一上来就写复杂算法,结果综合出来的电路又大又慢。回过头来,用几个MUX和加法器重新设计,性能翻倍。基础打牢了,上层建筑才稳当。


公众号:蓝海资料掘金营,微信deep3321