第四章:组合逻辑设计——从基本门到ALU的修炼之路

组合逻辑,说白了就是没有记忆的电路。输入一变,输出立马跟着变,不带半点犹豫。这就像你按开关,灯就亮;松手,灯就灭——中间没有“记住”这回事。

我在带新人时发现,很多人一上来就写always块,却连最基本的与门、或门都描述不清楚。这不行。万丈高楼平地起,咱们先把地基打牢。

4.1 基本门电路实现

Verilog描述基本门电路,其实就两种方式:连续赋值和always块。我个人习惯用连续赋值,简洁明了。

// 基本门电路示例
module basic_gates(
    input  a, b,
    output y_and, y_or, y_not, y_nand, y_nor, 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

小提示:我在项目中见过有人把“&”写成“&&”,前者是按位与,后者是逻辑与。在组合逻辑里,这两个差别大了去了。按位与是对每一位独立操作,逻辑与只返回0或1。你想想看,如果a和b都是多bit信号,用错运算符会出大问题。

4.2 多路选择器

多路选择器,简称MUX,是组合逻辑里的“交通警察”。它决定哪条路的数据能通过。最简单的就是2选1MUX。

// 2选1多路选择器
module mux2to1(
    input  [3:0] data0, data1,
    input        sel,
    output [3:0] y
);
    // 三目运算符,简洁高效
    assign y = sel ? data1 : data0;
endmodule

嗯,这里要注意:三目运算符虽然方便,但嵌套多了可读性会变差。我曾经接手过一个项目,里面有个8选1MUX用了7层三目运算符嵌套,看得我头皮发麻。后来我改成了case语句,代码清晰多了。

// 4选1多路选择器(case语句)
module mux4to1(
    input  [3:0] data0, data1, data2, data3,
    input  [1:0] sel,
    output reg [3:0] y
);
    always @(*) begin
        case(sel)
            2'b00: y = data0;
            2'b01: y = data1;
            2'b10: y = data2;
            2'b11: y = data3;
            default: y = 4'b0; // 防锁存器
        endcase
    end
endmodule

避坑指南:我曾经在写case语句时漏了default分支,结果综合出来一堆锁存器。锁存器在数字芯片里就像不定时炸弹,时序分析时能把你折腾死。记住:组合逻辑的case一定要写default,哪怕你觉得所有情况都覆盖了。

4.3 编码器与译码器

编码器把多个输入线压缩成较少的输出线,译码器则反过来。这俩就像压缩和解压缩的关系。

先看优先编码器——这是实际项目中最常用的。为什么叫“优先”?因为当多个输入同时有效时,它只认优先级最高的那个。

// 8线-3线优先编码器
module priority_encoder_8to3(
    input  [7:0] in,
    output reg [2:0] out,
    output reg valid
);
    always @(*) begin
        valid = 1'b1;
        casez(in)
            8'b1???????: out = 3'd7;
            8'b01??????: out = 3'd6;
            8'b001?????: out = 3'd5;
            8'b0001????: out = 3'd4;
            8'b00001???: out = 3'd3;
            8'b000001??: out = 3'd2;
            8'b0000001?: out = 3'd1;
            8'b00000001: out = 3'd0;
            default: begin
                out   = 3'd0;
                valid = 1'b0; // 无有效输入
            end
        endcase
    end
endmodule

译码器呢?3线-8线译码器是经典例子。我当年面试时,面试官让我手写3-8译码器,我刷刷刷就写出来了——因为这东西在地址译码里太常用了。

// 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

4.4 加法器与ALU设计

加法器是算术逻辑单元(ALU)的核心。从半加器到全加器,再到行波进位加法器,每一步都有讲究。

半加器只考虑两个输入,不考虑进位输入。全加器则多了进位输入,这才是实用的基础单元。

// 全加器
module full_adder(
    input  a, b, cin,
    output sum, cout
);
    assign {cout, sum} = a + b + cin;
endmodule

你看,Verilog里一个“+”就搞定了。但综合工具会把它映射成什么?这取决于你的综合约束。我建议初学者还是先理解门级实现,再谈行为级描述。

接下来是ALU——CPU的运算核心。一个简单的ALU至少支持加、减、与、或、异或、比较等操作。

// 简单ALU设计
module alu(
    input  [7:0] a, b,
    input  [2:0] opcode,
    output reg [7:0] result,
    output reg zero, carry
);
    wire [8:0] tmp;
    
    always @(*) begin
        case(opcode)
            3'b000: result = a + b;           // 加法
            3'b001: result = a - b;           // 减法
            3'b010: result = a & b;           // 按位与
            3'b011: result = a | b;           // 按位或
            3'b100: result = a ^ b;           // 按位异或
            3'b101: result = ~a;              // 取反
            3'b110: result = (a > b) ? 8'd1 : 8'd0; // 大于比较
            3'b111: result = (a == b) ? 8'd1 : 8'd0; // 等于比较
            default: result = 8'b0;
        endcase
        
        // 零标志位
        zero = (result == 8'b0);
        // 进位标志位(仅对加法有效)
        tmp  = a + b;
        carry = tmp[8];
    end
endmodule

核心要点:写ALU时,标志位的处理最容易出错。我在一个项目里debug了整整两天,最后发现是进位标志的计算逻辑写错了。记住:进位是加法运算的第9位,不是第8位。用9位宽的临时变量来捕获进位,这是最稳妥的做法。

知识体系总览

下面这张图是我画的组合逻辑知识体系,你可以把它当作学习路线图。

组合逻辑设计知识体系 组合逻辑 基本门电路 与门、或门、非门 与非门、或非门、异或门 连续赋值 vs always块 多路选择器 2选1、4选1、8选1 三目运算符 vs case语句 避免锁存器陷阱 编码器与译码器 优先编码器(casez) 3-8译码器(地址译码) 使能信号的处理 加法器与ALU 半加器、全加器 行波进位加法器 ALU:运算+标志位 从基本门到ALU:层层递进,步步为营

这张图把本章的知识点串起来了。你会发现,从基本门到ALU,其实是一个层层抽象的过程。基本门是砖瓦,MUX和编码器是预制件,ALU就是最终的建筑。理解了这个层次关系,你写代码时心里就有谱了。

我的建议:初学者别急着写复杂的ALU。先把基本门电路和MUX练熟,再尝试编码器、译码器,最后才是加法器和ALU。我在带团队时,要求新人必须手写一遍所有基本模块的RTL代码,再跑仿真验证。这个过程虽然枯燥,但能帮你建立扎实的“电路感”。

好了,组合逻辑的基础就讲到这里。记住:写Verilog不是写软件,你脑子里要时刻有电路图。每个assign语句背后都是一堆晶体管在干活。带着这个意识去写代码,你的设计水平会提升得很快。


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