组合逻辑编码:always @(*) 的正确用法、阻塞赋值与非阻塞赋值的陷阱、避免锁存器

组合逻辑,说白了就是没有记忆功能的电路。输入一变,输出立马跟着变,中间不带任何存储元件。听起来简单吧?但我在项目里见过太多人在这上面翻车了。今天咱们就把组合逻辑编码的三大核心问题掰扯清楚。

always @(*) 的正确用法

很多新手写组合逻辑时,喜欢用 assign 一把梭。但遇到复杂逻辑,always @(*) 才是更灵活的选择。它的敏感列表用 * 表示「所有输入信号」,综合工具会自动推导。

核心原则:组合逻辑的 always 块中,所有被读取的信号都必须出现在敏感列表里。用 @(*) 最安全,别自己手写敏感列表。

我见过有人这么写:

// 错误示范:敏感列表不完整
always @(a or b) begin
    c = a & b;
    d = c | e;  // e 不在敏感列表里!
end

仿真时 e 变了,d 不会更新。综合结果却可能正确。这种仿真与综合不一致的 bug,我曾经调了整整两天才找到根源。

正确的写法就简单了:

// 正确用法:用 @(*)
always @(*) begin
    c = a & b;
    d = c | e;
end

嗯,这里要注意:always @(*) 对 wire 和 reg 都适用。输出声明成 reg 只是语法要求,不代表综合出寄存器。

阻塞赋值与非阻塞赋值的陷阱

这是 RTL 设计里最容易踩的坑,没有之一。我每次带新人,第一件事就是让他们背下这条铁律:

铁律:组合逻辑用阻塞赋值 =,时序逻辑用非阻塞赋值 <=。别问为什么,先记住。

但光记住不够,你得理解背后的原理。咱们看个反面教材:

// 错误:时序逻辑用了阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;
end

你期望的是 a 和 c 都在时钟沿更新为 b 的值。但阻塞赋值是顺序执行的,c 拿到的是 a 的新值,相当于 c 比 a 晚了一个赋值周期。综合出来可能多一级寄存器,或者产生竞争。

反过来,组合逻辑用非阻塞赋值也会出问题:

// 错误:组合逻辑用了非阻塞赋值
always @(*) begin
    a <= b & c;
    d <= a | e;
end

非阻塞赋值在组合逻辑里,仿真时 d 拿到的可能是 a 的旧值,导致仿真结果和实际电路行为不一致。你想想看,这种 bug 多难查。

场景 赋值方式 原因
组合逻辑 =(阻塞) 赋值立即生效,符合组合逻辑的纯计算特性
时序逻辑 <=(非阻塞) 所有赋值在时钟沿同时更新,避免竞争
锁存器 =(阻塞) 条件不全时,阻塞赋值会保持旧值,形成锁存

个人习惯:我写代码时,看到 always @(posedge clk) 就条件反射用 <=;看到 always @(*) 就自动用 =。形成肌肉记忆,比每次去查规则靠谱。

避免锁存器

锁存器(Latch)本身不是恶魔。但你不小心综合出来的锁存器,往往是时序问题的根源。为什么?因为锁存器是电平敏感,组合逻辑是纯计算,两者混在一起,时序分析工具很难处理。

锁存器是怎么产生的?说白了就一句话:组合逻辑中,条件分支没有覆盖所有情况。

// 锁存器制造机
always @(*) begin
    if (sel)
        q = a;
    // 没有 else!
end

当 sel = 0 时,q 要保持原值。组合逻辑没有存储能力,综合工具只好给你插一个锁存器。我曾经在代码 review 时看到过这种写法,问对方为什么这么写,他说「忘了加 else」。嗯,这一忘,流片回来芯片功能就不对了。

避免锁存器的方法很简单:

  • if-else 必须配对:每个 if 都要有对应的 else
  • case 要加 default:覆盖所有未列出的分支
  • 组合逻辑中不要读自己的输出:那会形成反馈环路
// 安全写法
always @(*) begin
    if (sel)
        q = a;
    else
        q = b;  // 明确赋值,不会产生锁存器
end

// case 也要加 default
always @(*) begin
    case (sel)
        2'b00: q = a;
        2'b01: q = b;
        2'b10: q = c;
        default: q = d;  // 覆盖 2'b11
    endcase
end

避坑指南:我曾经接手过一个项目,代码里有个组合逻辑块,if 条件嵌套了 5 层,中间漏了两个 else。综合报告里报了 3 个锁存器 warning,但没人当回事。结果芯片在特定输入组合下,输出一直保持上一个值,导致整个模块死锁。从那以后,我每次写完组合逻辑,都会用 lint 工具检查一遍,确保零锁存器 warning。

下面这张图总结了组合逻辑编码的核心要点:

组合逻辑编码核心要点 always @(*) 阻塞 vs 非阻塞 避免锁存器 敏感列表用 @(*) 不要手写敏感列表 输出声明为 reg 组合逻辑用 = 时序逻辑用 <= 形成肌肉记忆 if-else 要配对 case 加 default 不读自身输出 仿真与综合一致 避免仿真陷阱 避免竞争与冒险 仿真行为正确 零锁存器 warning lint 检查不可少 目标:可综合、可仿真、可维护的组合逻辑

最后说一句,组合逻辑写得好不好,直接决定了你代码的可读性和可维护性。我见过有人一个 always 块里塞了上百行代码,if-else 嵌套七八层,别说 review 了,自己过两天都看不懂。我的建议是:一个 always 块只做一件事。复杂逻辑拆成多个小块,每个块用清晰的命名和注释说明用途。这样不仅自己舒服,同事 review 时也会感谢你。