4、组合逻辑建模:assign语句、always@(*)用法、组合逻辑中的if-else与case、避免锁存器
组合逻辑,说白了就是没有记忆功能的电路。输入一变,输出立马跟着变,中间不带任何存储元件。今天咱们就把这块彻底聊透。
4.1 assign语句:最直接的组合逻辑
assign 语句,我个人认为是 Verilog 里最直观的建模方式。它就像一根导线,把输入和输出直接连起来。
// 一个简单的与门
assign y = a & b;
// 三态门
assign data_out = (enable) ? data_in : 1'bz;
我在项目中遇到过不少新手,喜欢把所有逻辑都塞进 always 块里。其实没必要。简单的逻辑运算,用 assign 一行搞定,代码更清爽。
4.2 always@(*) 的妙用
always@(*) 是组合逻辑建模的主力。星号表示「敏感列表自动推导」,也就是说,只要等号右边出现的信号变了,这个块就会重新计算。
always @(*) begin
y = a & b;
end
你想想看,如果手动写敏感列表,比如 always @(a or b),万一漏掉一个信号,综合出来的电路和仿真结果就不一致了。这就是所谓的「仿真-综合不匹配」。always@(*) 完美解决了这个问题。
4.3 组合逻辑中的 if-else 与 case
组合逻辑里用 if-else 和 case,核心原则就一条:所有分支必须覆盖完整。否则,综合工具会给你「补」一个锁存器出来。
4.3.1 if-else 的完整分支
// 不完整的 if-else —— 会生成锁存器!
always @(*) begin
if (sel)
y = a;
// 缺少 else 分支
end
// 完整的 if-else —— 纯组合逻辑
always @(*) begin
if (sel)
y = a;
else
y = b;
end
我曾经在一个项目里,就因为少写了一个 else,导致后仿死活不对。查了两天才发现,综合工具默默给我插了个锁存器。嗯,从那以后,我写 if-else 都会检查一遍分支完整性。
4.3.2 case 的完整分支
case 语句也一样。要么列出所有可能的值,要么用 default 兜底。
// 不完整的 case —— 锁存器警告!
always @(*) begin
case (sel)
2'b00: y = a;
2'b01: y = b;
// 缺少 2'b10 和 2'b11
endcase
end
// 完整的 case —— 安全
always @(*) begin
case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
2'b11: y = d;
endcase
end
// 或者用 default 兜底
always @(*) begin
case (sel)
2'b00: y = a;
2'b01: y = b;
default: y = 'b0;
endcase
end
4.4 避免锁存器 —— 实战避坑指南
锁存器(Latch)在 ASIC 设计里是个麻烦精。它电平敏感,容易受毛刺干扰,时序分析也麻烦。FPGA 里虽然可以用,但大多数情况下我们想要的是寄存器(Flip-Flop)或者纯组合逻辑。
哪些情况容易意外生成锁存器?我总结了三条:
- if 缺少 else —— 最常见的原因
- case 缺少 default —— 第二常见
- always 块里对同一个变量在多处赋值 —— 这个比较隐蔽
// 隐蔽的锁存器生成
always @(*) begin
if (sel) begin
y = a;
z = b;
end else begin
y = c;
// 忘记给 z 赋值了!
end
end
你看,上面这个例子,if 分支里给 y 和 z 都赋值了,但 else 分支里只给了 y。z 在 else 分支下没有被赋值,综合工具就会给 z 生成一个锁存器。
// 默认赋值法 —— 我的最爱
always @(*) begin
y = 'b0; // 先给默认值
z = 'b0;
if (sel) begin
y = a;
z = b;
end else begin
y = c;
// z 保持默认值,没问题
end
end
4.5 知识体系总览
下面这张图,把组合逻辑建模的核心脉络梳理清楚了。你可以对照着检查自己的代码习惯。
这张图把今天讲的内容串起来了。左边是 assign,适合简单场景;中间是 always@(*),灵活但要注意写法;右边是分支语句,核心是完整性。底部那三个元凶,你写代码时多留个心眼。
公众号:蓝海资料掘金营,微信deep3321