第四章:组合逻辑设计——从基本门到实用电路

各位同学,欢迎来到第四章。这一章我们聊聊组合逻辑。说白了,组合逻辑就是那种“输入一变,输出立马跟着变”的电路,没有记忆功能,不依赖时钟。我刚开始学CPLD时,觉得这东西太简单了,不就是几个门嘛。后来做项目才发现,组合逻辑用好了,能解决很多实际问题。

4.1 基本门电路:与、或、非、异或

基本门电路是数字世界的“原子”。你想想看,再复杂的芯片,底层也就是这些门的组合。咱们一个一个过。

4.1.1 与门(AND)

与门的逻辑:所有输入为1时,输出才为1。用乘法表示就是 Y = A & B。我在项目中遇到过一个问题:两个使能信号必须同时有效才能启动某个模块,这时候用与门就对了。

// Verilog 与门
module and_gate(
    input  wire a,
    input  wire b,
    output wire y
);
    assign y = a & b;
endmodule

4.1.2 或门(OR)

或门的逻辑:只要有一个输入为1,输出就是1。用加法表示 Y = A | B。嗯,这里要注意:或门在控制信号合并时特别常用。比如多个中断源,任何一个触发都要通知CPU,用或门最直接。

assign y = a | b;

4.1.3 非门(NOT)

非门就是取反,输入0输出1,输入1输出0。Y = ~A。我个人习惯把非门叫做“反相器”。在CPLD里,非门经常用来产生互补信号。

assign y = ~a;

4.1.4 异或门(XOR)

异或门:输入不同时输出1,相同时输出0。Y = A ^ B。这个门很有意思。我曾经用它来做数据校验,两个数据流做异或,结果一致就说明传输没问题。

assign y = a ^ b;

重要提示: 在CPLD中,基本门电路通常会被综合工具自动映射到查找表(LUT)中。你写 &、|、~、^ 这些运算符,工具会帮你优化成最合适的实现方式。但理解底层逻辑,对排查问题很有帮助。

4.2 多路选择器(MUX)

多路选择器,说白了就是一个“数据开关”。从多个输入中选一个送到输出。2选1多路选择器是最基本的,控制信号sel为0选a,为1选b。

// 2选1多路选择器
module mux2to1(
    input  wire a,
    input  wire b,
    input  wire sel,
    output wire y
);
    assign y = sel ? b : a;
endmodule

为什么我要单独讲MUX?因为在实际项目中,MUX的使用频率非常高。我记得有一次做数据通道切换,需要从8路传感器数据中选一路处理,用8选1MUX,一行代码就搞定了。

多路选择器还可以用case语句实现,适合多路选择的情况:

// 4选1多路选择器
module mux4to1(
    input  wire [1:0] sel,
    input  wire [3:0] data_in,
    output reg        data_out
);
    always @(*) begin
        case(sel)
            2'b00: data_out = data_in[0];
            2'b01: data_out = data_in[1];
            2'b10: data_out = data_in[2];
            2'b11: data_out = data_in[3];
            default: data_out = 1'b0;
        endcase
    end
endmodule

个人经验: 用case语句写MUX时,一定要写default分支。我曾经漏掉default,结果综合出来一堆锁存器,仿真没问题,上板子就乱跳。查了两天才发现是这个问题。记住:组合逻辑中,所有分支都要覆盖,否则会生成latch。

4.3 加法器

加法器是算术运算的基础。半加器只考虑两个输入相加,不考虑进位输入。全加器则考虑进位输入。

4.3.1 半加器

module half_adder(
    input  wire a,
    input  wire b,
    output wire sum,
    output wire carry
);
    assign sum   = a ^ b;   // 和
    assign carry = a & b;   // 进位
endmodule

4.3.2 全加器

module full_adder(
    input  wire a,
    input  wire b,
    input  wire cin,
    output wire sum,
    output wire cout
);
    assign sum   = a ^ b ^ cin;
    assign cout  = (a & b) | (a & cin) | (b & cin);
endmodule

你想想看,把多个全加器串联起来,就构成了行波进位加法器。但这里有个坑:进位是一级一级传下去的,位数多了延迟会很大。我在做高速设计时,就遇到过加法器成为关键路径的情况。解决办法是用超前进位加法器,但那个逻辑复杂一些,咱们后面再聊。

避坑指南: 我曾经在项目中直接用“+”运算符做32位加法,综合工具自动生成了行波进位加法器。结果时序分析报错,最高频率只能跑到50MHz,而设计要求是100MHz。后来我改成了流水线结构,分两级做加法,才满足时序要求。所以,大位宽加法器要注意时序问题。

4.4 译码器

译码器把输入的二进制编码转换成对应的输出信号。3-8译码器是最经典的:3位输入,8位输出,每个输出对应一个输入组合。

// 3-8译码器
module decoder3to8(
    input  wire [2:0] in,
    output reg  [7:0] out
);
    always @(*) begin
        case(in)
            3'b000: out = 8'b00000001;
            3'b001: out = 8'b00000010;
            3'b010: out = 8'b00000100;
            3'b011: out = 8'b00001000;
            3'b100: out = 8'b00010000;
            3'b101: out = 8'b00100000;
            3'b110: out = 8'b01000000;
            3'b111: out = 8'b10000000;
            default: out = 8'b00000000;
        endcase
    end
endmodule

译码器在实际中有什么用?我举两个例子:一是地址译码,CPU访问外设时,通过地址线译码产生片选信号;二是七段数码管显示,把二进制数译成段码。

嗯,这里要提一下:译码器也可以用for循环实现,代码更简洁:

always @(*) begin
    out = 8'b0;
    for (int i = 0; i < 8; i++) begin
        if (in == i)
            out[i] = 1'b1;
    end
end

核心总结: 组合逻辑设计的本质就是布尔代数。与、或、非是基础,MUX做选择,加法器做运算,译码器做转换。这些模块组合起来,就能实现复杂功能。我个人建议,初学者先把这几个基本电路吃透,多写代码、多仿真,后面学状态机、时序逻辑就轻松多了。

组合逻辑设计知识体系 组合逻辑设计 基本门电路 多路选择器 加法器 译码器 与门 或门 非门 异或门 2选1 MUX 4选1 MUX 8选1 MUX 半加器 全加器 行波进位 3-8译码器 地址译码

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