第三章:组合逻辑设计——从门电路到实用模块

各位同学,欢迎来到组合逻辑设计的实战环节。说实话,很多初学者觉得数字电路就是一堆门电路拼拼凑凑,没什么技术含量。但我在项目中吃过不少亏,才明白一个道理:组合逻辑设计,是FPGA设计的基石,也是最容易出bug的地方。今天我们就从最基础的门电路开始,一步步搭建出实用的功能模块。

3.1 基本门电路的Verilog实现

先说说最基本的与、或、非、异或门。这些门电路在Verilog里实现起来非常简单,但要注意的是——综合工具会怎么理解你的代码。我见过不少新手写出来的代码,仿真没问题,一上板子就翻车,问题往往就出在基本门电路的写法上。

核心原则:组合逻辑用 assign 连续赋值语句,或者 always @(*) 过程块。我个人习惯用 assign,代码更简洁,综合结果也更可控。

// 基本门电路示例
module basic_gates(
    input  wire a, b,
    output wire y_and,   // 与门输出
    output wire y_or,    // 或门输出
    output wire y_not,   // 非门输出
    output wire y_xor    // 异或门输出
);

// 使用 assign 实现组合逻辑
assign y_and = a & b;    // 与门
assign y_or  = a | b;    // 或门
assign y_not = ~a;       // 非门
assign y_xor = a ^ b;    // 异或门

endmodule

嗯,这里要注意一点:非门用 ~ 而不是 !~ 是按位取反,! 是逻辑非。在单bit信号上两者结果一样,但到了多bit总线,用 ! 就会出大问题。我曾经在调试一个8位数据总线时,就因为把 ~ 写成了 !,结果仿真通过了,上板子数据全乱套。排查了整整两天才找到这个低级错误。

3.2 多路选择器(MUX)

多路选择器,说白了就是一个数据开关。根据选择信号,从多个输入中选一个输出。实际项目中,MUX的用途非常广泛——比如在状态机里切换数据路径,或者做寄存器配置的地址译码。

实现MUX有两种主流写法:三目运算符case语句。我个人更推荐用case语句,尤其是当输入路数超过4路时,case语句的可读性和可维护性都更好。

// 4选1多路选择器 - 使用case语句
module mux4to1(
    input  wire [1:0] sel,   // 选择信号
    input  wire [3:0] data,  // 4路输入,每路1bit
    output reg       y       // 输出
);

always @(*) begin
    case(sel)
        2'b00: y = data[0];
        2'b01: y = data[1];
        2'b10: y = data[2];
        2'b11: y = data[3];
        default: y = 1'b0;   // 安全起见,加default
    endcase
end

endmodule

避坑指南:我曾经在写一个8选1的MUX时,漏掉了default分支。结果综合工具给我生成了一个锁存器(latch),导致整个模块时序全乱。记住:组合逻辑的case语句一定要写default,否则综合工具会推断出你不想要的锁存器。

3.3 加法器:半加器与全加器

加法器是算术运算的基础。半加器只能处理两个bit相加,不考虑进位输入;全加器则多了一个进位输入,可以级联成多位加法器。

先看半加器,逻辑很简单:和 = A XOR B,进位 = A AND B。

// 半加器
module half_adder(
    input  wire a, b,
    output wire sum,   // 和
    output wire carry  // 进位
);

assign sum   = a ^ b;
assign carry = a & b;

endmodule

全加器比半加器多了一个进位输入 cin。公式稍微复杂一点:

// 全加器
module full_adder(
    input  wire a, b, cin,
    output wire sum,
    output wire cout
);

assign sum   = a ^ b ^ cin;
assign cout  = (a & b) | (a & cin) | (b & cin);

endmodule

你想想看,为什么全加器的进位输出要用三个与项相或?说白了,就是只要任意两个输入为1,进位就为1。这个逻辑在FPGA里综合出来就是几个LUT,效率很高。

实际项目经验:我在做图像处理加速器时,需要做大量像素值的累加。如果用全加器逐级级联,延迟会很大。后来我改用进位保留加法器(CSA)结构,把三个数的加法变成两个数的加法,时序一下子就满足了。所以,基础模块的选型要结合具体场景,不是所有加法器都用全加器级联。

3.4 编码器与译码器

编码器和译码器是一对逆操作。编码器把多个输入信号压缩成较少的输出信号;译码器则把较少的输入信号扩展成多个输出信号。

先看一个4线-2线编码器:

// 4线-2线编码器(优先级编码)
module encoder_4to2(
    input  wire [3:0] in,   // 输入,高电平有效
    output reg  [1:0] out   // 编码输出
);

always @(*) begin
    casex(in)  // 使用casex,支持无关位
        4'b0001: out = 2'b00;
        4'b0010: out = 2'b01;
        4'b0100: out = 2'b10;
        4'b1000: out = 2'b11;
        default: out = 2'b00;
    endcase
end

endmodule

译码器正好反过来,2线-4线译码器:

// 2线-4线译码器
module decoder_2to4(
    input  wire [1:0] in,
    output reg  [3:0] out
);

always @(*) begin
    case(in)
        2'b00: out = 4'b0001;
        2'b01: out = 4'b0010;
        2'b10: out = 4'b0100;
        2'b11: out = 4'b1000;
        default: out = 4'b0000;
    endcase
end

endmodule

重要提醒:编码器要注意输入互斥的问题。如果多个输入同时有效,优先级编码器会按照case语句的顺序处理。我在一个中断控制器项目中,就遇到过两个中断同时触发,结果优先级低的那个被忽略了。解决方案是:要么保证输入互斥,要么显式定义优先级

3.5 本章知识体系总览

为了让大家更直观地理解本章的知识结构,我画了一张图:

第三章:组合逻辑设计知识体系 组合逻辑设计 基本门电路 与、或、非、异或 assign 连续赋值 多路选择器 (MUX) 三目运算符写法 case语句写法 加法器 半加器 (HA) 全加器 (FA) 编码器 & 译码器 编码器 (4-2线) 译码器 (2-4线) 核心:组合逻辑无记忆,输出只取决于当前输入

这张图把本章的四个核心模块串联起来了。你会发现,从基本门电路到MUX、加法器、编码器/译码器,本质上都是在做同一件事——用逻辑表达式描述输入和输出之间的关系。这就是组合逻辑的精髓。

3.6 实战要点总结

最后,我把自己这些年踩过的坑总结成几条铁律,你们记好了:

  • 组合逻辑一定要写全所有分支——case语句加default,if-else加else,否则综合出锁存器
  • 区分按位运算符和逻辑运算符——& vs &&| vs ||~ vs !
  • 多bit信号用向量,不要拆成单bit——比如 wire [3:0] datawire d0,d1,d2,d3 好维护得多
  • 仿真通过不代表综合通过——一定要跑综合后仿真,或者至少检查综合报告里的warning

嗯,说到综合报告,我建议你们每次综合完都看一眼「Inferred Latch」这个条目。如果发现综合工具推断出了锁存器,而你的设计里本不该有,那就要回去检查代码了。这个习惯帮我避免了好几次流片失败的风险。

好了,第三章的内容就到这里。这些基础模块虽然简单,但它们是后续所有复杂设计的基础。下一章我们会进入时序逻辑的世界,那才是真正考验功力的时候。


专注资料整理