第三讲:组合逻辑设计——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,先问自己:这个逻辑需要时钟吗?不需要?好,用 @(*)。
- 赋值符号别搞混。 组合逻辑用 =,时序逻辑用 <=。这个习惯要刻在脑子里。
- if 配 else,case 配 default。 这是防止锁存器的最有效手段。
- 仿真通过不代表综合正确。 组合逻辑的错误在仿真时可能不暴露,但综合后一定会现原形。
我曾经在一个项目中,就因为组合逻辑里少写了一个 else,导致芯片流片回来功能异常,最后花了三周才定位到问题。从那以后,我每次写组合逻辑都会多检查一遍分支完整性。嗯,希望大家不要重蹈我的覆辙。
好了,这一讲就到这里。组合逻辑看似简单,但细节决定成败。记住我今天说的这些,你写代码时就能少走很多弯路。