Veloris.
返回索引
概念基础 2026-02-23

if-else、case 和 generate:Verilog 行为级语句实战手册

6 分钟
2.0k words

if-else、case 和 generate:Verilog 行为级语句实战手册

💡 综合完成,你打开 Vivado 的 Warning 列表,看到一行刺眼的黄色:[Synth 8-3277] inferring latch for variable 'q'。锁存器——FPGA 设计中的头号公敌,悄悄潜入了你的代码。

罪魁祸首?一个 if 语句忘了写 else

这就是 Verilog 行为级语句的”陷阱”:if-elsecasefor 循环——它们长得和 C 语言一模一样,但硬件含义完全不同。if-else 综合出带优先级的 MUX 链,case 综合出不带优先级的并行 MUX,for 循环不是”重复执行 N 次”而是”展开成 N 份并行硬件”。写错一个分支,综合出来的可能就不是你想要的电路。

这篇文章会带你掌握 FPGA 开发中最常用的行为级语句和编译指令,重点是避坑

📌 系列衔接:本文承接上一篇 Verilog 赋值机制与三种描述方式


目录


1. 语句组:begin…end

always 块内需要执行多条语句时,必须用 begin...end 包裹成语句组。

always @(posedge clk) begin
    if (rst) begin
        counter <= 8'd0;
        led     <= 1'b0;
    end else begin
        counter <= counter + 1'b1;
        led     <= counter[7];
    end
end

如果只有一条语句,begin...end 可以省略——但为了可读性和避免后续修改时遗漏,建议总是加上

Verilog 还有一种并行语句组 fork...join,其中的语句并行执行。但它不可综合,只用于仿真环境中生成测试激励:

// 仅用于仿真
initial fork
    #6 data = 8'hAA;
    #4 data = 8'h55;
    #2 data = 8'hFF;
join
// 三条语句同时开始计时,分别在2ns、4ns、6ns生效

2. if-else 条件语句

if-else 是 RTL 设计中使用频率最高的语句。但你必须理解一个关键点:if-else 综合出来的是带优先级的电路

基本语法

always @(posedge clk) begin
    if (rst)
        q <= 1'b0;
    else if (enable)
        q <= d;
    else
        q <= q;    // 保持不变
end

优先级含义

if-else 链中,前面的条件优先级高于后面的条件。综合工具会生成一个优先级 MUX 链:

rst ──→ [MUX] ──→ [MUX] ──→ Q
         ↑          ↑
        0值       enable

                   d值

第一个条件(rst)最先被检查,如果满足则直接输出,后面的条件不再评估。这在时序上意味着:优先级越高的路径延迟越短,越低的路径延迟越长

致命陷阱:不完整的 if-else 生成锁存器

组合逻辑中,如果 if 语句没有覆盖所有情况,综合工具会推断出锁存器(Latch)——这几乎总是一个 Bug。

// ❌ 危险:组合逻辑中缺少 else 分支
always @(*) begin
    if (enable)
        q = d;
    // enable=0 时 q 怎么办?→ 综合器推断为"保持"→ 锁存器!
end

// ✅ 正确:补全所有分支
always @(*) begin
    if (enable)
        q = d;
    else
        q = 1'b0;   // 明确 else 分支
end

注意:在时序逻辑中,不完整的 if 不会生成锁存器——因为寄存器本身就有”保持”行为。但为了代码清晰,仍然建议补全。

💡 工程师手记:我有一个代码审查习惯——每次看到 always @(*) 块,第一件事就是检查所有 if 是否有对应的 else,所有 case 是否有 default。这个习惯帮我避免了无数次锁存器 Bug。Vivado 的综合报告中,锁存器会以 Warning 的形式出现(Synth 8-3277: inferring latch),养成检查综合报告的习惯同样重要。


3. case 多路分支语句

case 语句用于多路选择,和 if-else 的关键区别是:case 是不带优先级的

基本语法

always @(*) begin
    case (sel)
        2'b00: out = in0;
        2'b01: out = in1;
        2'b10: out = in2;
        2'b11: out = in3;
        default: out = 1'b0;  // 永远要写 default!
    endcase
end

if-else vs case:优先级的差异

特性if-elsecase
优先级✅ 有(前面的条件优先)❌ 无(所有分支平等)
综合结构优先级 MUX 链并行 MUX
适用场景条件间有主次关系条件间互斥、权重相同
时序影响高优先级路径更快所有路径延迟相同

选择建议

  • 如果条件之间有明确的优先级关系(如复位 > 使能 > 正常操作),用 if-else
  • 如果条件互斥且权重相同(如状态机的状态跳转),用 case

casez 和 casex

Verilog 提供了 case 的两个变体,用于处理 “不关心”(don’t care)的情况:

语句不关心的值典型用途
casezz 视为不关心优先级编码器
casexxz 都视为不关心少用,易出问题
// 优先级编码器:casez 示例
always @(*) begin
    casez (request)
        4'b1???: grant = 4'b1000;  // 最高位优先
        4'b01??: grant = 4'b0100;
        4'b001?: grant = 4'b0010;
        4'b0001: grant = 4'b0001;
        default: grant = 4'b0000;
    endcase
end

锁存器陷阱(同样存在)

if-else 一样,case 在组合逻辑中如果缺少 default 或未覆盖所有分支,也会生成锁存器。

// ❌ 危险:只覆盖了2种情况,sel 有4种可能值
always @(*) begin
    case (sel)
        2'b00: q = a;
        2'b01: q = b;
        // 缺少 2'b10、2'b11 和 default → 锁存器!
    endcase
end

// ✅ 正确:用 default 覆盖剩余情况
always @(*) begin
    case (sel)
        2'b00: q = a;
        2'b01: q = b;
        default: q = 1'b0;
    endcase
end

4. for 循环语句

Verilog 的 for 循环和 C 语言的 for 循环长得一样,但含义完全不同

C 的 for 是”重复执行 N 次”——CPU 在时间上依次执行每次迭代。Verilog 的 for 是”展开成 N 份并行硬件”——综合工具在空间上生成 N 个相同的电路。

// 这段代码会被展开为 8 个独立的与门
integer i;
always @(*) begin
    for (i = 0; i < 8; i = i + 1) begin
        result[i] = a[i] & b[i];
    end
end

// 等价于手动写 8 行:
// result[0] = a[0] & b[0];
// result[1] = a[1] & b[1];
// ...
// result[7] = a[7] & b[7];

可综合 for 循环的条件

  • 循环次数必须在编译时可确定(不能依赖运行时信号)
  • 循环变量只能用于索引,不能用于复杂运算
  • 循环体内的逻辑必须是可综合的
// ✅ 可综合:循环次数固定
for (i = 0; i < WIDTH; i = i + 1)

// ❌ 不可综合:循环次数依赖运行时信号
for (i = 0; i < count; i = i + 1)

5. generate 生成语句

generate 语句是 for 循环的”升级版”——它不仅可以展开赋值语句,还可以批量生成模块实例化、always 块和 assign 语句

循环生成

genvar i;
generate
    for (i = 0; i < 8; i = i + 1) begin : gen_adder
        full_adder u_fa (
            .a   (a[i]),
            .b   (b[i]),
            .cin (carry[i]),
            .sum (sum[i]),
            .cout(carry[i+1])
        );
    end
endgenerate

注意:

  • 循环变量必须用 genvar 声明(不是 integer
  • begin 后面的 : gen_adder必须的标签名,用于在层次结构中标识每个生成实例

条件生成

generate
    if (WIDTH <= 8) begin : gen_small
        // 小位宽用LUT实现
        assign result = a + b;
    end else begin : gen_large
        // 大位宽用流水线加法器
        pipeline_adder u_add (.a(a), .b(b), .sum(result));
    end
endgenerate

case 生成

generate
    case (IMPL_TYPE)
        "LUT":  begin : gen_lut  /* LUT实现 */  end
        "DSP":  begin : gen_dsp  /* DSP实现 */  end
        default: begin : gen_def /* 默认实现 */ end
    endcase
endgenerate

💬 你可能会问:for 循环和 generate for 有什么区别?

for 循环只能在 always 块内使用,展开的是赋值语句。generate for 可以在模块级使用,展开的是实例化、always 块等模块级构造。简单说:需要批量生成”模块级的东西”时用 generate,只是批量赋值时用普通 for


6. 编译指令

Verilog 的编译指令以反引号 ` 开头,用于控制编译器行为。FPGA 开发中最常用的有以下几个:

`timescale:时间单位

`timescale 1ns/1ps    // 时间单位1ns,精度1ps
  • 第一个值是延时单位,第二个值是精度
  • 仅影响仿真,不影响综合
  • #101ns/1ps 下表示延时 10ns

define / undef:宏定义

`define BUS_WIDTH 16
reg [`BUS_WIDTH-1:0] data;   // 使用宏

`undef BUS_WIDTH              // 取消定义

`define vs parameter 的区别

特性`defineparameter
作用域全局(跨文件)模块级
可配置性编译时确定实例化时可修改
典型用途全局常量、编译开关模块的可配置参数

ifdef / else / `endif:条件编译

`ifdef SIMULATION
    // 仿真时使用的代码
    initial $display("Simulation mode");
`else
    // 综合时使用的代码
    // ...
`endif

条件编译常用于区分仿真代码和综合代码,或者在不同平台间切换配置。

`include:文件包含

`include "global_defines.vh"   // 包含全局宏定义文件

将另一个文件的内容嵌入当前位置,通常用于共享宏定义和参数。


7. 总结

语句硬件含义关键注意事项
if-else带优先级的 MUX 链组合逻辑中必须补全 else
case不带优先级的并行 MUX必须写 default
for展开为 N 份并行硬件循环次数必须编译时确定
generate批量生成模块级构造用 genvar,标签必须有
begin…end语句组建议总是加上

避免锁存器的三条铁律

  1. 组合逻辑 always @(*) 中的 if 必须有 else
  2. 组合逻辑 always @(*) 中的 case 必须有 default
  3. 组合逻辑中所有被赋值的信号,在每个分支都必须被赋值

给初学者的建议:每次写完 always @(*) 块,问自己三个问题——“if 有没有 else?case 有没有 default?每个分支是不是都给所有信号赋值了?“回答完这三个问题,你就能避免 90% 的锁存器 Bug。


常见问题

Q1:if-else 的优先级会影响电路性能吗?

会。优先级越低的路径,经过的 MUX 越多,延迟越大。如果某个条件对时序要求很高,应该把它放在 if 链的最前面(最高优先级)。或者考虑改用 case 消除优先级。

Q2:Verilog 的 for 循环能用变量控制次数吗?

不能用运行时信号控制——综合工具无法在编译时确定要生成多少硬件。但可以用 parameter 控制:for (i = 0; i < WIDTH; i = i + 1),因为 parameter 在编译时是确定值。

Q3:casez 中的 ?z 有什么区别?

没有区别,?zcasez 中的简写。写 4'b1???4'b1zzz 效果一样。推荐用 ?,可读性更好。

Q4:什么时候用 define,什么时候用 parameter?

如果常量需要在多个文件中共享,用 `define。如果常量只属于一个模块且需要在实例化时配置,用 parameter。简单规则:跨文件用宏,模块内用参数。


参考资料


系列导航:本文是「FPGA 入门系列」第 10 篇。

如果这篇文章对你有帮助,欢迎点赞、收藏。if-else 和 case 的区别、锁存器的避免方法,这些都是 FPGA 面试和实际开发中的高频考点。

End of file.