4. 组合逻辑设计实践:多路选择器、加法器、编码器、译码器的RTL实现

组合逻辑,说白了就是没有记忆的电路。输入一变,输出立马跟着变,不带半点延迟(当然,实际物理门是有延迟的,但那是后话)。今天咱们就把几个最常用的组合逻辑模块,从RTL代码到上板验证,掰开了揉碎了讲一遍。

核心要点:组合逻辑的RTL描述,核心就是 assign 连续赋值语句和 always @(*) 过程块。记住,组合逻辑里不能出现寄存器(reg)被综合成锁存器的情况。

4.1 多路选择器(MUX)

多路选择器,就是数据通道的“道岔”。从多路输入中选一路送到输出。最简单的就是二选一MUX。

RTL实现(二选一):

// 方式一:使用三目运算符
module mux2_1 (
    input       [7:0] a, b,
    input             sel,
    output reg  [7:0] y
);
    always @(*) begin
        y = sel ? a : b;
    end
endmodule

// 方式二:使用case语句(适合多路)
module mux4_1 (
    input       [7:0] d0, d1, d2, d3,
    input       [1:0] sel,
    output reg  [7:0] y
);
    always @(*) begin
        case (sel)
            2'b00:   y = d0;
            2'b01:   y = d1;
            2'b10:   y = d2;
            2'b11:   y = d3;
            default: y = 8'h00;  // 安全起见,加default
        endcase
    end
endmodule

我的经验:写case语句时,default分支一定要加。我曾经在一个项目里漏了default,结果综合出来一个巨大的锁存器,面积直接翻倍。排查了整整半天才找到原因。

上板验证思路:把sel接到拨码开关,a和b接到不同的LED灯模式,观察输出是否随sel切换而变化。

4.2 加法器

加法器是算术运算的基石。最简单的就是全加器,但实际项目中我们直接用 + 运算符,综合工具会自动优化。

RTL实现(参数化加法器):

module adder #(
    parameter WIDTH = 8
) (
    input  [WIDTH-1:0] a, b,
    input              cin,        // 进位输入
    output [WIDTH-1:0] sum,
    output             cout        // 进位输出
);
    assign {cout, sum} = a + b + cin;
endmodule

你可能会问:直接用 + 会不会综合出很差的电路?其实不会。现代综合工具对加法器的优化非常成熟,它会自动选择进位链结构(比如超前进位、进位选择等)。

注意:如果你需要高性能加法器(比如在CPU的ALU里),建议手动实例化工艺库里的专用加法器单元。直接用 + 虽然方便,但综合工具不一定能选到最优结构。

上板验证:用两个8位拨码开关输入a和b,把sum接到8个LED上,cout接到一个单独的LED。手动计算几个值验证一下。

4.3 编码器

编码器,就是把多个输入线“压缩”成较少的输出线。最典型的就是优先编码器——多个输入同时有效时,只编码优先级最高的那个。

RTL实现(8线-3线优先编码器):

module priority_encoder_83 (
    input  [7:0] din,      // 输入,高电平有效
    output reg [2:0] dout, // 编码输出
    output reg          valid // 是否有有效输入
);
    always @(*) begin
        casez (din)
            8'b1??????? : dout = 3'd7;
            8'b01?????? : dout = 3'd6;
            8'b001????? : dout = 3'd5;
            8'b0001???? : dout = 3'd4;
            8'b00001??? : dout = 3'd3;
            8'b000001?? : dout = 3'd2;
            8'b0000001? : dout = 3'd1;
            8'b00000001 : dout = 3'd0;
            default     : dout = 3'd0;
        endcase
        valid = |din;  // 只要有一位为1,valid就为1
    end
endmodule

这里用了 casez,? 表示不关心该位。嗯,这个语法在综合时是安全的,放心用。

避坑指南:我曾经在编码器里忘了加 valid 信号。结果上板后发现,当所有输入都为0时,输出竟然是3'd0,和输入为第0位有效时完全一样。这就尴尬了——你根本分不清是“没有输入”还是“第0位输入有效”。所以,valid 信号一定要加。

4.4 译码器

译码器是编码器的逆过程。把较少的输入线“展开”成较多的输出线。3线-8线译码器是最经典的。

RTL实现(3线-8线译码器):

module decoder_38 (
    input  [2:0] din,
    output reg [7:0] dout
);
    always @(*) begin
        case (din)
            3'b000: dout = 8'b0000_0001;
            3'b001: dout = 8'b0000_0010;
            3'b010: dout = 8'b0000_0100;
            3'b011: dout = 8'b0000_1000;
            3'b100: dout = 8'b0001_0000;
            3'b101: dout = 8'b0010_0000;
            3'b110: dout = 8'b0100_0000;
            3'b111: dout = 8'b1000_0000;
            default: dout = 8'b0000_0000;
        endcase
    end
endmodule

其实也可以用移位操作:dout = 1 << din;。但要注意,如果din超出范围,移位操作会出问题。所以case写法更安全。

4.5 本章知识体系

下面这张图,把四种组合逻辑模块的关系和典型应用串起来了。你仔细看看,会发现它们之间其实有内在联系。

组合逻辑模块知识体系 多路选择器 数据通道选择 加法器 算术运算核心 编码器 多线→少线压缩 译码器 少线→多线展开 互逆 数据源 典型应用场景 CPU数据通路 | 地址译码 | 指令译码 | 算术逻辑单元(ALU) | 数据选择网络 RTL实现方式 assign连续赋值 | always @(*) 组合逻辑块 | case/casez 分支 | 三目运算符 ? : 上板验证方法 拨码开关输入 → 逻辑分析仪/ILA抓波形 → LED/数码管显示输出 → 手动计算对比验证

4.6 上板联调要点

写代码只是第一步,上板验证才是见真章的时候。我总结了几条经验:

  1. 先仿真,后上板。 别偷懒。一个简单的testbench就能帮你发现80%的语法错误和逻辑错误。
  2. 用ILA抓内部信号。 如果板子上有逻辑分析仪IP核(比如Xilinx的ILA),一定要用。它能让你看到芯片内部的波形,比用LED灯看输出靠谱多了。
  3. 边界条件要测全。 比如加法器,要测最大值+1、0+0、全1+1这些边界情况。编码器要测所有输入都为0的情况。
  4. 注意毛刺。 组合逻辑的输出可能会有毛刺(glitch)。如果输出要接到时钟域或者触发器的使能端,一定要做同步处理。

一个小技巧:上板调试时,我习惯把关键信号(比如加法器的进位链)引到空闲的GPIO上,用示波器看。虽然现在ILA很方便,但示波器能看到真实的模拟波形,有时候能发现一些ILA看不到的毛刺问题。

好了,这四种组合逻辑模块,从RTL代码到上板验证,基本就这些内容。你写代码的时候,多想想“这个电路综合出来长什么样”,慢慢就有感觉了。