4、组合逻辑设计:always @(*) 的正确用法,阻塞赋值与非阻塞赋值的陷阱

组合逻辑,说白了就是没有记忆功能的电路。输入一变,输出立马跟着变。很多新手写RTL时,总觉得组合逻辑简单,不就是几个与或非门嘛。但我在项目中见过太多因为组合逻辑写法不规范导致的bug,有些甚至到了流片前才被发现,那叫一个心惊肉跳。

今天咱们就聊聊 always @(*) 的正确用法,以及阻塞赋值和非阻塞赋值那些容易踩的坑。

4.1 always @(*) 到底该怎么用?

我个人习惯,组合逻辑只用 always @(*) 或 assign。为什么?因为 always @(敏感信号列表) 这种写法太容易出错了。你想想看,如果你漏写了一个敏感信号,综合前后仿真结果可能完全不一样。我在一个项目中就遇到过,同事写 always @(a or b),结果代码里用了c,但c不在敏感列表里。仿真时c变了,always块不执行,输出死活不对。查了两天才找到原因。

always @(*) 会自动推导敏感信号,省心又安全。来看个例子:

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

// 错误的写法——漏了敏感信号
always @(a or b or sel) begin  // 如果代码里用了其他信号,这里就漏了
    if (sel)
        y = a + b;
    else
        y = a - b;
end
我的小建议:组合逻辑一律用 always @(*),别偷懒去手写敏感列表。这不是炫技的地方,稳才是王道。

4.2 阻塞赋值 vs 非阻塞赋值——选错了就是灾难

这个问题,我每次面试新人都会问。能讲清楚的人,基本功基本过关。

核心原则其实就一句话:

  • 组合逻辑用阻塞赋值(=)
  • 时序逻辑用非阻塞赋值(<=)

为什么?我解释给你听。

阻塞赋值是“立即生效”的。在同一个always块里,前面的赋值结果可以被后面的语句立即读到。这正好符合组合逻辑的特性——信号传播是连续的。

非阻塞赋值是“延迟生效”的。所有赋值在块结束时才统一更新。这正好符合时序逻辑的特性——时钟沿到来时,所有寄存器同时采样。

如果你把两者搞混了,会怎样?

陷阱一:时序逻辑里用了阻塞赋值

// 错误示例——时序逻辑用了阻塞赋值
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        q1 = 0;
        q2 = 0;
    end else begin
        q1 = d;
        q2 = q1;  // 这里q1已经更新了,q2拿到的是新值!
    end
end

这段代码综合出来是什么?是两个寄存器吗?不是!综合工具会把它优化成一个寄存器,因为q2直接拿到了q1的新值,相当于q2 = d。你本来想要一个两级的流水线,结果变成了一级。我在一个高速接口项目中就见过这种写法,导致时序收敛死活过不了,因为流水级数少了一级,关键路径变长了。

陷阱二:组合逻辑里用了非阻塞赋值

// 错误示例——组合逻辑用了非阻塞赋值
always @(*) begin
    y1 <= a & b;
    y2 <= y1 | c;  // 这里y1还是旧值!
end

这段代码仿真时,y2拿到的y1是进入always块之前的值,不是当前a&b的结果。仿真行为和实际电路行为不一致。你想想看,这种bug多难查?仿真通过了,综合后功能却不对。

我曾经踩过的坑:有一次写一个复杂的组合逻辑,里面有好几级运算。我图省事,全用了非阻塞赋值。仿真怎么跑都对,结果综合后后仿直接挂了。查了三天,最后发现是赋值方式用错了。从那以后,我写代码前都会默念一遍:组合用等号,时序用小于等于。

4.3 组合逻辑中的“不完全赋值”陷阱

还有一个常见的坑,就是组合逻辑里没有把所有分支都覆盖到,或者没有给所有输出赋值。这会导致综合出锁存器(latch)。

// 会综合出锁存器的写法
always @(*) begin
    if (sel)
        y = a;
    // 没有else分支,sel=0时y保持原值——综合出latch!
end

// 正确的写法
always @(*) begin
    if (sel)
        y = a;
    else
        y = b;  // 所有分支都覆盖
end

或者:

// 也会综合出锁存器
always @(*) begin
    y = 0;  // 给默认值
    if (sel)
        y = a;
    // 虽然没有else,但有了默认赋值,就不会产生latch
end
我的习惯:写组合逻辑always块时,第一件事就是给所有输出赋默认值。这样后面不管if-else写得多复杂,都不会漏掉分支。这招是我从一位老工程师那里学来的,用了十几年,从来没因为latch出过问题。

4.4 知识体系总结

下面这张图,是我对组合逻辑设计核心要点的总结。你可以把它当作一个快速检查清单:

组合逻辑设计核心要点 组合逻辑设计 always @(*) 阻塞赋值(=) 避免锁存器 自动推导敏感信号 避免手写敏感列表 立即生效 仿真与综合一致 所有分支覆盖 赋默认值 黄金法则 组合逻辑用 always @(*) + 阻塞赋值 + 全覆盖

4.5 实战建议

说了这么多,总结几条我实际项目中的经验:

  1. 组合逻辑优先用 assign 语句——如果逻辑很简单(比如一个与门、一个多路选择器),assign 比 always 块更简洁、更不容易出错。
  2. 复杂的组合逻辑用 always @(*)——比如有 if-else 嵌套、case 分支,用 always 块可读性更好。
  3. always 块里第一行给默认值——这能从根本上杜绝锁存器。
  4. 严格遵守赋值规则——组合用 =,时序用 <=。别问为什么,照做就行。等你真正理解了背后的硬件原理,自然就明白为什么了。

记住:写RTL代码,本质上是在描述硬件电路。你写的每一行代码,都要能对应到实际的逻辑门和触发器上。如果代码和电路对不上,那仿真再漂亮也没用。

嗯,组合逻辑这块就聊到这儿。这些规则看起来简单,但真正写代码时很容易疏忽。我建议你每次写完代码,都回头检查一下:always块的敏感列表对不对?赋值方式用对了没有?有没有漏掉分支?养成这个习惯,能省去后面大量的调试时间。

可可精品资料库