4、组合逻辑设计实战:多路选择器、加法器、编码器、译码器、三态缓冲器

组合逻辑,说白了就是没有记忆的电路。输入一变,输出立马跟着变,中间不带任何状态存储。我刚开始做RTL设计那会儿,总觉得组合逻辑太简单,不就是几个门电路拼一拼嘛。结果呢?第一次综合出来的面积超标,时序也一塌糊涂。嗯,这里面的门道其实不少。

今天咱们就把组合逻辑里最常用的五个模块掰开揉碎讲清楚。多路选择器、加法器、编码器、译码器、三态缓冲器,这些你天天都在写,但你真的写对了吗?

核心原则:组合逻辑设计,代码风格直接决定综合质量。同样的功能,写法不同,面积和速度可能差好几倍。

4.1 多路选择器(MUX)—— 最容易被忽视的陷阱

多路选择器,就是根据选择信号从多个输入里挑一个出来。写起来简单,但坑不少。

先看最常见的写法:

// 2选1 MUX
always @(*) begin
    if (sel)
        out = a;
    else
        out = b;
end

// 4选1 MUX
always @(*) begin
    case (sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        2'b11: out = d;
        default: out = 'b0;  // 这个default很关键
    endcase
end

这里有个细节,我吃过亏。case语句如果不写default,综合器会给你生成一个锁存器!为什么?因为当sel取值为未列出的情况时,out要保持原值,这不就是锁存器的行为吗?

警告:组合逻辑中,if-else必须写全,case必须写default。否则综合出latch,时序分析会哭的。

我个人习惯,对于多路选择器,能用条件运算符(? :)就别用if-else。条件运算符综合出来的结构更规整,面积也小一些。

// 推荐写法:条件运算符
assign out = sel ? a : b;

// 多级嵌套
assign out = (sel == 2'b00) ? a :
             (sel == 2'b01) ? b :
             (sel == 2'b10) ? c : d;

我在项目中遇到过一件事:一个128选1的MUX,用case语句写,综合出来面积巨大。后来改成树形结构,用两级32选1再加一级4选1,面积直接砍了一半。你想想看,大MUX一定要拆成小MUX。

4.2 加法器 —— 从半加器到超前进位

加法器是算术运算的基础。最简单的就是半加器和全加器。

类型 输入 输出 说明
半加器 A, B S, Cout 不考虑进位输入
全加器 A, B, Cin S, Cout 考虑进位输入

实际项目中,我们很少手写半加器全加器。直接用 + 运算符,综合器会自动优化。但你要理解背后的原理。

// 8位加法器
assign sum = a + b;

// 带进位
assign {cout, sum} = a + b + cin;

这里有个性能问题。普通的行波进位加法器,进位要一级一级传,32位加法器延迟可能达到几十个门。高频设计根本扛不住。

我曾经在一个DSP项目中,需要做64位加法,频率要求1GHz。用默认的 + 综合出来,时序直接红了一片。后来改成超前进位加法器(CLA),进位路径从64级缩短到4级,时序就过了。

技巧:如果综合工具不支持自动超前进位,可以手动拆分加法器。比如32位加法拆成4个8位加法,中间用流水线寄存器隔开。

4.3 编码器与译码器 —— 一对好兄弟

编码器:多个输入,一个输出。比如8线-3线编码器。

译码器:一个输入,多个输出。比如3线-8线译码器。

先看编码器:

// 8线-3线优先编码器
always @(*) begin
    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:     out = 3'd0;
    endcase
end

注意这里用了 casez,把不关心的位用 ? 表示。这样综合出来的电路更简洁。我建议你多用 casez 而不是 casex,因为 casex 会把 X 态也当成不关心,仿真时容易掩盖问题。

再看译码器:

// 3线-8线译码器
always @(*) begin
    out = 8'b0;
    case (in)
        3'd0: out[0] = 1'b1;
        3'd1: out[1] = 1'b1;
        3'd2: out[2] = 1'b1;
        3'd3: out[3] = 1'b1;
        3'd4: out[4] = 1'b1;
        3'd5: out[5] = 1'b1;
        3'd6: out[6] = 1'b1;
        3'd7: out[7] = 1'b1;
    endcase
end

译码器还有一种更简洁的写法:

assign out = 1'b1 << in;

一行搞定!但要注意,这种写法只适用于输出为独热码的情况。我在项目中就喜欢用移位写法,代码量少,综合结果也不差。

4.4 三态缓冲器 —— 总线设计的核心

三态缓冲器,就是输出可以是0、1、高阻Z。主要用于总线共享场景。

// 三态缓冲器
assign data_bus = (enable) ? data_in : 1'bz;

这里有个重要原则:同一时刻只能有一个三态缓冲器被使能。否则多个驱动同时输出,轻则短路功耗大,重则烧芯片。

警告:FPGA内部尽量不要用三态缓冲器!FPGA的内部逻辑单元不支持三态,综合器会用MUX来模拟,反而浪费资源。三态缓冲器只建议用在芯片顶层IO上。

我曾经在一个SoC项目中,内部总线用了三态缓冲器,结果综合出来面积比预期大了30%。后来全部改成MUX结构,面积就正常了。嗯,这个教训挺深刻的。

三态缓冲器的典型应用场景是双向IO:

// 双向IO
assign pad_io = (output_en) ? output_data : 1'bz;
assign input_data = pad_io;

这样同一个引脚,既能当输入又能当输出。但要注意,切换方向时要留出足够的死区时间,防止上下管同时导通。

4.5 组合逻辑的通用设计原则

讲完这五个模块,我总结几条通用原则:

  1. 避免锁存器:组合逻辑中,所有分支必须覆盖所有情况。if要有else,case要有default。
  2. 控制代码深度:组合逻辑的级数不要太深。我一般控制在10级以内,超过就考虑插寄存器。
  3. 拆分大模块:大MUX拆小MUX,大加法器拆小加法器。这是时序收敛的捷径。
  4. 多用assign:能用连续赋值就别用always块。assign综合出来的电路更透明。
  5. 注意综合工具:同样的代码,DC和Vivado综合结果可能不同。要了解你用的工具的特性。

一句话总结:组合逻辑设计,代码即电路。你写的每一行RTL,都要能想象出它对应的门级结构。

好了,这一章的内容就到这里。多路选择器、加法器、编码器、译码器、三态缓冲器,这五个模块是数字芯片设计的基石。你写熟了,后面再复杂的电路都能拆解成这些基本单元。


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