4. 组合逻辑电路设计:多路选择器、编码器、译码器、加法器、乘法器与ALU

各位同学,欢迎来到第四章。这一章我们聊聊组合逻辑。说白了,组合逻辑就是那种“输入一变,输出立马跟着变”的电路,没有时钟,没有寄存器,纯粹靠门电路搭出来的逻辑。

我个人觉得,组合逻辑是数字电路的基础,也是FPGA设计的“基本功”。你想想看,一个复杂的CPU里,除了寄存器,剩下的全是组合逻辑。RISC-V内核里的ALU、译码器、地址计算,哪个离得开这些基础模块?

组合逻辑电路设计 MUX / 编码器 / 译码器 数据选择 优先级编码 加法器 行波进位 超前进位 乘法器 / ALU 组合乘法 ALU设计 RISC-V内核中的组合逻辑应用

4.1 多路选择器(MUX)

多路选择器,说白了就是一个“数据开关”。你给它几个输入,它根据选择信号,只让其中一个通过。

我在项目中遇到过一个问题:用if-else写MUX,综合出来竟然是一大堆LUT,延迟很大。后来改成case语句,立马清爽了。为什么?因为case语句综合工具能更好地推断出MUX结构。

核心要点: 在FPGA中,MUX通常用LUT(查找表)实现。一个4输入的LUT可以实现一个4:1 MUX。

看一个简单的2:1 MUX的Verilog代码:

module mux_2to1 (
    input  wire a, b,
    input  wire sel,
    output wire y
);
    assign y = sel ? b : a;
endmodule

嗯,这里要注意:条件运算符? : 综合出来就是一个MUX。如果你写if-else,记得要写完整,不然会综合出锁存器(latch)。

避坑指南: 我曾经在写一个4:1 MUX时,漏掉了case的default分支,结果综合出来多了一个latch。仿真没问题,上板子就出bug。从那以后,我写case必带default,写if-else必带else。

4.2 编码器与译码器

编码器和译码器,这俩是“反着来”的。编码器把多个输入线压缩成较少的输出线;译码器则反过来,把较少的输入线扩展成多个输出线。

你想想看,RISC-V的指令译码器,本质上就是一个巨大的译码器。它把32位的指令编码,翻译成各种控制信号。

先看一个8线-3线优先编码器:

module priority_encoder_8to3 (
    input  wire [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'b0000_1???: out = 3'd3;
            8'b0000_01??: out = 3'd2;
            8'b0000_001?: out = 3'd1;
            8'b0000_0001: out = 3'd0;
            default: begin
                out   = 3'd0;
                valid = 1'b0;
            end
        endcase
    end
endmodule

这里用了casez,?表示“不关心”。优先级最高的输入是in[7],最低的是in[0]。valid信号告诉你输入是否有效。

再看3线-8线译码器:

module decoder_3to8 (
    input  wire [2:0] in,
    input  wire       en,
    output reg  [7:0] out
);
    always @(*) begin
        out = 8'd0;
        if (en) begin
            case (in)
                3'd0: out = 8'b0000_0001;
                3'd1: out = 8'b0000_0010;
                3'd2: out = 8'b0000_0100;
                3'd3: out = 8'b0000_1000;
                3'd4: out = 8'b0001_0000;
                3'd5: out = 8'b0010_0000;
                3'd6: out = 8'b0100_0000;
                3'd7: out = 8'b1000_0000;
            endcase
        end
    end
endmodule
小技巧: 在RISC-V的译码器设计中,我习惯用casez来处理指令中的“don't care”位。比如某些指令的某些位是保留位,用?可以省去很多判断逻辑。

4.3 加法器

加法器是ALU的核心。这里我们聊两种:行波进位加法器和超前进位加法器。

4.3.1 行波进位加法器(Ripple Carry Adder)

行波进位,名字很形象。进位就像水波一样,从低位往高位“一波一波”地传。每个全加器都要等低位的进位算出来,才能算自己的结果。

优点是结构简单,缺点是慢。32位的行波进位加法器,延迟大约是32个全加器的延迟之和。

module ripple_carry_adder #(parameter WIDTH = 4) (
    input  wire [WIDTH-1:0] a, b,
    input  wire             cin,
    output wire [WIDTH-1:0] sum,
    output wire             cout
);
    wire [WIDTH:0] carry;
    assign carry[0] = cin;
    assign cout     = carry[WIDTH];

    genvar i;
    generate
        for (i = 0; i < WIDTH; i = i + 1) begin : fa_gen
            full_adder fa (
                .a   (a[i]),
                .b   (b[i]),
                .cin (carry[i]),
                .sum (sum[i]),
                .cout(carry[i+1])
            );
        end
    endgenerate
endmodule
注意: 行波进位加法器在FPGA中其实并不常用。因为FPGA的LUT和进位链(CARRY4)是专门优化过的,用行波进位反而浪费了硬件资源。

4.3.2 超前进位加法器(Carry Lookahead Adder)

超前进位加法器,说白了就是“不等了”。它通过额外的逻辑,提前算出每一位的进位,不用等低位传上来。

核心思想是引入两个信号:

  • 生成信号(Generate):g = a & b,只要a和b都是1,这一位肯定产生进位。
  • 传播信号(Propagate):p = a ^ b,只要a和b有一个是1,进位就能传过去。

然后进位可以写成:c[i+1] = g[i] | (p[i] & c[i])

展开后,每一位的进位只依赖于输入和cin,不依赖于中间进位。这就是“超前进位”的由来。

module carry_lookahead_adder #(parameter WIDTH = 4) (
    input  wire [WIDTH-1:0] a, b,
    input  wire             cin,
    output wire [WIDTH-1:0] sum,
    output wire             cout
);
    wire [WIDTH-1:0] g, p, c;

    assign g = a & b;
    assign p = a ^ b;

    // 超前进位逻辑
    assign c[0] = cin;
    assign c[1] = g[0] | (p[0] & c[0]);
    assign c[2] = g[1] | (p[1] & g[0]) | (p[1] & p[0] & c[0]);
    assign c[3] = g[2] | (p[2] & g[1]) | (p[2] & p[1] & g[0]) | (p[2] & p[1] & p[0] & c[0]);
    assign cout = g[3] | (p[3] & g[2]) | (p[3] & p[2] & g[1]) | (p[3] & p[2] & p[1] & g[0]) | (p[3] & p[2] & p[1] & p[0] & c[0]);

    assign sum = p ^ c;
endmodule
个人经验: 在RISC-V的ALU中,我一般用超前进位加法器。虽然逻辑多了点,但速度快。特别是做32位加法时,行波进位要等32级,超前进位只需要3-4级逻辑延迟。

4.4 乘法器(组合逻辑实现)

组合逻辑乘法器,说白了就是“用加法器搭出来的乘法”。原理和小学学的竖式乘法一模一样。

举个例子,4位乘法:a[3:0] * b[3:0]。把b的每一位分别和a相乘,得到4个部分积,然后加起来。

module comb_multiplier #(parameter WIDTH = 4) (
    input  wire [WIDTH-1:0] a, b,
    output wire [2*WIDTH-1:0] product
);
    wire [WIDTH-1:0] partial[WIDTH-1:0];
    wire [2*WIDTH-1:0] sum[WIDTH-1:0];

    genvar i, j;
    generate
        // 生成部分积
        for (i = 0; i < WIDTH; i = i + 1) begin : pp_gen
            for (j = 0; j < WIDTH; j = j + 1) begin : pp_bit
                assign partial[i][j] = a[j] & b[i];
            end
        end

        // 累加部分积
        assign sum[0] = partial[0];
        for (i = 1; i < WIDTH; i = i + 1) begin : add_stage
            assign sum[i] = sum[i-1] + (partial[i] << i);
        end
        assign product = sum[WIDTH-1];
    endgenerate
endmodule
优化建议: 实际项目中,我不会用这种纯组合逻辑做宽位乘法。面积太大!我一般用流水线乘法器,或者直接用FPGA内部的DSP48硬核。比如在Xilinx的器件里,一个DSP48就能做25x18的乘法,又快又省资源。

4.5 算术逻辑单元(ALU)设计

ALU是CPU的“计算核心”。RISC-V的ALU需要支持加减法、逻辑运算、移位、比较等操作。

我设计ALU时,习惯用一个操作码(opcode)来选择功能。每个功能对应一个组合逻辑模块,最后用MUX选出结果。

module alu #(parameter WIDTH = 32) (
    input  wire [WIDTH-1:0] a, b,
    input  wire [3:0]       alu_op,  // 操作码
    output reg  [WIDTH-1:0] result,
    output reg              zero,    // 结果为0标志
    output reg              carry,   // 进位标志
    output reg              negative // 负数标志
);
    wire [WIDTH-1:0] add_result, sub_result, and_result, or_result, xor_result;
    wire [WIDTH-1:0] slt_result;  // set less than

    // 加法
    assign add_result = a + b;
    // 减法
    assign sub_result = a - b;
    // 逻辑运算
    assign and_result = a & b;
    assign or_result  = a | b;
    assign xor_result = a ^ b;
    // 小于比较 (slt)
    assign slt_result = (a < b) ? 32'd1 : 32'd0;

    always @(*) begin
        case (alu_op)
            4'b0000: result = add_result;
            4'b0001: result = sub_result;
            4'b0010: result = and_result;
            4'b0011: result = or_result;
            4'b0100: result = xor_result;
            4'b0101: result = slt_result;
            default: result = {WIDTH{1'b0}};
        endcase

        zero     = (result == {WIDTH{1'b0}});
        negative = result[WIDTH-1];
        carry    = (alu_op == 4'b0000) ? (a[WIDTH-1] & b[WIDTH-1]) |
                                         (a[WIDTH-1] ^ b[WIDTH-1]) & result[WIDTH-1] : 1'b0;
    end
endmodule
RISC-V中的ALU: 在RISC-V内核中,ALU需要支持加减法、逻辑运算、移位(左移、右移、算术右移)、比较(slt, sltu)等。我一般把移位单独拿出来做,因为移位器用桶形移位器(barrel shifter)实现,和ALU的加法逻辑混在一起反而不好优化。
避坑指南: 我曾经在ALU的减法实现中,直接用a - b。但RISC-V的减法指令(SUB)实际上是a - b,而比较指令(SLT)需要的是有符号比较。如果不小心把无符号数和有符号数搞混了,结果就全错了。所以,我建议在ALU里明确区分有符号和无符号运算。

好了,这一章的内容就到这里。组合逻辑是数字电路的基础,也是FPGA设计的“基本功”。多写、多练、多仿真,慢慢就能找到感觉了。

课后练习: 试着用本章学到的知识,设计一个支持8种操作的ALU(加减、与或异或、左移右移、比较)。然后用testbench验证一下功能是否正确。

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