4、组合逻辑电路设计:多路选择器、译码器、加法器、乘法器的实现,case语句与if-else的优先级问题,LUT资源的利用

组合逻辑,说白了就是没有记忆功能的电路。输入一变,输出立马跟着变,不带半点延迟(当然,实际走线是有延迟的,但那是时序分析的事)。这一章我们聊聊几个最常用的组合逻辑模块:多路选择器、译码器、加法器、乘法器。还有两个绕不开的话题——case和if-else的优先级,以及LUT资源到底怎么用才划算。

核心要点:组合逻辑设计的关键在于“并行”和“优先级”的平衡。用对了,资源省、速度快;用错了,时序收敛能让你怀疑人生。

4.1 多路选择器(MUX)的实现

多路选择器,就是“选路”的。几路输入,一个选择信号,输出其中一路。最简单的就是二选一。

我个人习惯用assign加三元运算符写二选一,简洁明了:

assign out = sel ? a : b;

四选一呢?用case语句最直观:

always @(*) begin
    case (sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        2'b11: out = d;
        default: out = 1'b0;
    endcase
end

嗯,这里要注意:default分支一定要写。我在项目中遇到过,有人漏了default,结果综合出来一个锁存器(latch),整个逻辑行为全乱了。为什么?因为case没覆盖全,综合工具默认保持原值,就给你生成个锁存器。组合逻辑里出现锁存器,多半是bug。

小技巧:如果你确定输入不会出现未覆盖的情况,default可以赋一个确定值(比如0),这样综合工具不会推断锁存器。

4.2 译码器(Decoder)的实现

译码器,就是把一个较短的编码,翻译成一个独热码(one-hot)输出。比如3-8译码器,输入3位,输出8位,只有一位是1。

用case写最直接:

always @(*) begin
    case (addr)
        3'b000: y = 8'b0000_0001;
        3'b001: y = 8'b0000_0010;
        3'b010: y = 8'b0000_0100;
        // ... 省略中间
        3'b111: y = 8'b1000_0000;
        default: y = 8'b0000_0001;
    endcase
end

其实也可以用移位操作:y = 1 << addr;。但要注意,如果addr超出范围,移位结果可能不是你想要的。所以我还是推荐用case,可读性更好,也更容易维护。

我曾经在一个项目里用移位写译码器,结果addr输入了一个非法值,输出全0,导致后续模块直接罢工。后来改成case加default,问题就解决了。

4.3 加法器(Adder)的实现

加法器,最简单的就是直接用+运算符。综合工具会自动推断出合适的加法器结构(行波进位、超前进位等)。

assign sum = a + b;

但如果你要控制进位链,或者做多级流水,就得手动拆开。比如做一个带进位输入输出的加法器:

assign {cout, sum} = a + b + cin;

这里{cout, sum}是拼接操作,把进位和结果拼在一起。综合工具会把它映射到LUT加进位链(CARRY4)上。Xilinx的7系列器件里,一个CARRY4可以处理4位加法,效率很高。

注意:如果你用+做加法,但a和b的位宽不一样,综合工具会自动扩展位宽。这个扩展规则是:无符号数补0,有符号数补符号位。搞错了会出大问题。我建议所有加法操作前,先手动扩展到位宽一致。

4.4 乘法器(Multiplier)的实现

乘法器,直接用*运算符。综合工具会调用DSP48(如果器件有的话)或者用LUT拼。

assign prod = a * b;

但要注意:乘法器的资源消耗和位宽成平方关系。8位乘8位,结果16位,大概用几十个LUT。16位乘16位,结果32位,可能就要几百个LUT了。如果你对性能要求高,可以考虑用流水线拆开,或者直接用DSP硬核。

我记得有个项目,需要做24位乘24位的定点乘法,直接写*,综合出来时序不收敛。后来我改成三级流水线,每级做8位乘8位,再累加,时序就过了。说白了,就是拿面积换速度。

4.5 case语句与if-else的优先级问题

这个问题,很多新手会踩坑。先看if-else:

always @(*) begin
    if (sel_a)
        out = a;
    else if (sel_b)
        out = b;
    else
        out = c;
end

if-else是有优先级的。sel_a优先级最高,sel_b次之,最后是else。综合出来是一个优先级编码器(priority encoder),资源消耗和优先级级数成正比。

再看case:

always @(*) begin
    case (sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        2'b11: out = d;
    endcase
end

case默认是并行的,没有优先级。所有分支是平等的。综合出来是一个多路选择器,资源消耗和输入路数成正比。

那什么时候用if-else,什么时候用case?

  • 有优先级需求(比如中断控制器):用if-else。
  • 无优先级需求(比如地址译码):用case。
  • 分支条件复杂(比如多个条件组合):用if-else。
  • 分支条件简单且互斥(比如状态机):用case。

避坑指南:我曾经在一个项目里,用if-else写了16级优先级,结果综合出来LUT消耗巨大,时序也跑不动。后来改成case加独热码编码,资源省了一半,时序也收敛了。所以,能用case就别用if-else,除非你真的需要优先级。

4.6 LUT资源的利用

LUT(Look-Up Table)是FPGA的基本逻辑单元。一个LUT本质上就是一个小的RAM,输入地址,输出存储的值。比如4输入LUT,可以实现任意4输入的逻辑函数。

那怎么高效利用LUT?

  • 合并逻辑:多个小逻辑函数可以合并到一个LUT里。比如两个2输入与门,可以用一个4输入LUT实现。
  • 避免冗余:不要写无意义的逻辑。比如assign out = a & a;,这等于out = a,浪费一个LUT。
  • 利用LUT的输入复用:如果多个逻辑函数共享输入,可以放在同一个LUT里。
  • 注意LUT的输入数量:Xilinx 7系列是6输入LUT,但有些器件是4输入或5输入。超过输入数量的逻辑,会拆成多个LUT,增加延迟。

举个例子,实现一个全加器(full adder):

assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);

这个全加器需要两个LUT(一个用于sum,一个用于cout)。但如果用LUT的共享输入特性,可以把sum和cout放在同一个LUT里吗?不行,因为输出有两个。但你可以用两个LUT,每个LUT实现一个输出,输入共享。

个人经验:写RTL时,不要过度关注LUT怎么映射。那是综合工具的事。你只要保证逻辑正确、结构清晰、没有冗余就行。综合工具比你聪明得多。但如果你发现资源爆了,可以看看综合报告,哪些模块占LUT多,再针对性优化。

4.7 本章知识体系

下面这张图,帮你理清本章的核心逻辑:

组合逻辑电路设计知识体系 多路选择器 译码器 加法器 乘法器 assign + 三元运算符 case语句 if-else case语句 移位操作 default分支 + 运算符 进位链(CARRY4) 流水线拆分 * 运算符 DSP48硬核 流水线拆分 case vs if-else 优先级 LUT资源利用

这张图展示了本章的四个核心模块,以及它们的实现方式、关键问题和底层资源。你可以看到,无论是多路选择器还是乘法器,最终都归结到LUT资源的利用上。而case和if-else的优先级选择,直接影响综合出来的电路结构和资源消耗。

最后提醒一句:写组合逻辑时,脑子里要时刻想着“这个代码会综合成什么电路”。不要只盯着仿真波形看。仿真过了,综合出来可能是个锁存器,也可能是个巨大的优先级编码器。多看看综合报告,多想想LUT怎么用,这才是FPGA工程师的硬功夫。


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