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 一行搞定,代码更清爽。

我的习惯: 如果表达式不超过 3 个操作符,我倾向于用 assign。超过 3 个,或者有 if-else 结构,我才用 always 块。

4.2 always@(*) 的妙用

always@(*) 是组合逻辑建模的主力。星号表示「敏感列表自动推导」,也就是说,只要等号右边出现的信号变了,这个块就会重新计算。

always @(*) begin
    y = a & b;
end

你想想看,如果手动写敏感列表,比如 always @(a or b),万一漏掉一个信号,综合出来的电路和仿真结果就不一致了。这就是所谓的「仿真-综合不匹配」。always@(*) 完美解决了这个问题。

注意: 在 always@(*) 块里,被赋值的变量必须是 reg 类型。但这只是语法要求,综合出来仍然是组合逻辑,别被 reg 这个关键字骗了。

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)或者纯组合逻辑。

哪些情况容易意外生成锁存器?我总结了三条:

  1. if 缺少 else —— 最常见的原因
  2. case 缺少 default —— 第二常见
  3. 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@(*) 块时,我习惯在块的开头给所有输出赋一个默认值。这样即使后面漏了分支,也不会生成锁存器。
// 默认赋值法 —— 我的最爱
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@(*) if-else / case 连续赋值,适合简单逻辑 三态门、数据选择器 敏感列表自动推导 reg 类型输出组合逻辑 必须覆盖所有分支 default 兜底不可少 ⚠️ 锁存器生成三大元凶 1. if 缺少 else 分支 2. case 缺少 default 3. 同一变量在 always 块中未完全赋值

这张图把今天讲的内容串起来了。左边是 assign,适合简单场景;中间是 always@(*),灵活但要注意写法;右边是分支语句,核心是完整性。底部那三个元凶,你写代码时多留个心眼。

一句话总结: 组合逻辑建模,选对工具(assign 还是 always),写全分支(if-else 和 case),给好默认值。做到这三点,锁存器就离你远远的。

公众号:蓝海资料掘金营,微信deep3321