第三讲:组合逻辑设计——always @(*)用法、阻塞赋值与非阻塞赋值、组合逻辑常见错误

各位同学,今天我们来聊聊组合逻辑设计。说实话,组合逻辑是数字电路最基础的东西,但恰恰是基础,最容易出问题。我见过太多项目,最后查bug查出来都是组合逻辑的锅。今天我就把这些年踩过的坑,一次性讲清楚。

一、always @(*) 到底怎么用?

先问大家一个问题:你写组合逻辑时,习惯用 assign 还是 always @(*)?我个人习惯用 always @(*),因为它更灵活,能写 if-else、case 这些控制结构。但要注意,always @(*) 的敏感列表是自动推导的,你不需要手动写。

核心原则:always @(*) 用于组合逻辑时,内部必须使用阻塞赋值(=),且所有输入信号都要出现在敏感列表中。

举个例子,一个简单的多路选择器:

// 正确的写法
always @(*) begin
    if (sel)
        y = a;
    else
        y = b;
end

// 错误的写法——敏感列表不全
always @(a or b) begin  // 漏了 sel!
    if (sel)
        y = a;
    else
        y = b;
end

嗯,这里要注意:虽然 Verilog 允许你手动写敏感列表,但我建议一律用 @(*)。为什么?因为省事,而且不会漏信号。我在项目中遇到过有人手动写敏感列表,结果加了个新信号忘记更新,仿真怎么都不对,查了两天才发现是敏感列表的问题。

二、阻塞赋值 vs 非阻塞赋值——别搞混了

这个知识点,说白了就是一句话:

赋值类型 符号 适用场景 执行特点
阻塞赋值 = 组合逻辑(always @(*)) 立即执行,顺序执行
非阻塞赋值 <= 时序逻辑(always @(posedge clk)) 并行执行,在 always 块结束时统一更新

你想想看,如果把非阻塞赋值用在组合逻辑里会怎样?我告诉你,仿真结果可能对,但综合出来的电路完全不是你想要的。我曾经接手过一个项目,前同事在组合逻辑里用了 <=,结果综合后多出了一堆没用的锁存器,面积直接翻倍。

警告:组合逻辑 always 块中不要使用非阻塞赋值(<=)。这是 RTL 编码的红线,踩了就出事。

再来看一个对比示例:

// 正确:组合逻辑用阻塞赋值
always @(*) begin
    temp = a & b;
    y = temp | c;
end

// 错误:组合逻辑用非阻塞赋值
always @(*) begin
    temp <= a & b;  // 这里会出问题
    y <= temp | c;  // y 拿到的 temp 是旧值!
end

为什么会这样?因为非阻塞赋值是并行执行的,两个语句同时计算右值,然后同时更新左值。在第二个例子中,y 拿到的 temp 其实是进入 always 块之前的旧值,而不是 a & b 的结果。这跟你的设计意图完全不符。

三、组合逻辑常见错误——我踩过的坑

做组合逻辑设计,有几个错误特别容易犯。我一个个说。

1. 锁存器(Latch)的意外生成

这是最常见的错误。当你写 if-else 或 case 语句时,如果条件没有覆盖所有分支,综合工具就会给你生成一个锁存器。

// 错误:缺少 else 分支
always @(*) begin
    if (en)
        q = d;
    // 没有 else,en=0 时 q 保持原值 → 生成锁存器
end

// 正确:补全所有分支
always @(*) begin
    if (en)
        q = d;
    else
        q = 0;  // 或者 q = q,但建议赋默认值
end

我曾经在做一个数据通路时,case 语句少写了一个 default,结果综合出来一堆锁存器,时序根本跑不过。后来花了整整两天才把所有锁存器清掉。所以我的建议是:写组合逻辑时,if 一定要配 else,case 一定要配 default。

2. 组合逻辑环路(Combinational Loop)

这个错误更隐蔽。当组合逻辑的输出直接或间接反馈到输入时,就形成了环路。仿真时可能表现为 X 态或震荡。

// 错误:组合逻辑环路
always @(*) begin
    y = a & y;  // y 依赖自身,形成环路
end

嗯,这种代码一看就有问题。但实际项目中,环路往往藏在多层逻辑里。我记得有一次,一个同事写了个状态机的下一状态逻辑,不小心把输出信号用在了组合逻辑的输入里,结果仿真时状态机乱跳,查了好久才发现是环路。

3. 敏感列表不完整

前面已经提过,这里再强调一次。用 always @(*) 可以避免这个问题,但如果你坚持手动写敏感列表,一定要把所有输入都列全。

小技巧:写组合逻辑时,统一用 always @(*),不要手动列敏感列表。这是最省心的做法。

四、知识体系总览

下面这张图总结了本章的核心内容,你可以对照着检查自己的理解:

组合逻辑设计知识体系 组合逻辑设计 always @(*) 阻塞赋值(=) 常见错误 敏感列表自动推导 必须使用阻塞赋值 立即执行,顺序执行 与非阻塞赋值严格区分 意外生成锁存器 组合逻辑环路 敏感列表不完整 核心:组合逻辑用 always @(*) + 阻塞赋值

五、避坑指南

最后,我把自己这些年总结的几条经验分享给大家:

  • 写组合逻辑时,先想清楚是组合还是时序。 别一上来就 always,先问自己:这个逻辑需要时钟吗?不需要?好,用 @(*)。
  • 赋值符号别搞混。 组合逻辑用 =,时序逻辑用 <=。这个习惯要刻在脑子里。
  • if 配 else,case 配 default。 这是防止锁存器的最有效手段。
  • 仿真通过不代表综合正确。 组合逻辑的错误在仿真时可能不暴露,但综合后一定会现原形。

我曾经在一个项目中,就因为组合逻辑里少写了一个 else,导致芯片流片回来功能异常,最后花了三周才定位到问题。从那以后,我每次写组合逻辑都会多检查一遍分支完整性。嗯,希望大家不要重蹈我的覆辙。

好了,这一讲就到这里。组合逻辑看似简单,但细节决定成败。记住我今天说的这些,你写代码时就能少走很多弯路。

可可精品资料库