if-else、case 和 generate:Verilog 行为级语句实战手册
💡 综合完成,你打开 Vivado 的 Warning 列表,看到一行刺眼的黄色:[Synth 8-3277] inferring latch for variable 'q'。锁存器——FPGA 设计中的头号公敌,悄悄潜入了你的代码。
罪魁祸首?一个 if 语句忘了写 else。
这就是 Verilog 行为级语句的”陷阱”:if-else、case、for 循环——它们长得和 C 语言一模一样,但硬件含义完全不同。if-else 综合出带优先级的 MUX 链,case 综合出不带优先级的并行 MUX,for 循环不是”重复执行 N 次”而是”展开成 N 份并行硬件”。写错一个分支,综合出来的可能就不是你想要的电路。
这篇文章会带你掌握 FPGA 开发中最常用的行为级语句和编译指令,重点是避坑。
📌 系列衔接:本文承接上一篇 Verilog 赋值机制与三种描述方式。
目录
- 1. 语句组:begin…end
- 2. if-else 条件语句
- 3. case 多路分支语句
- 4. for 循环语句
- 5. generate 生成语句
- 6. 编译指令
- 7. 总结
- 常见问题
- 参考资料
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-else | case |
|---|---|---|
| 优先级 | ✅ 有(前面的条件优先) | ❌ 无(所有分支平等) |
| 综合结构 | 优先级 MUX 链 | 并行 MUX |
| 适用场景 | 条件间有主次关系 | 条件间互斥、权重相同 |
| 时序影响 | 高优先级路径更快 | 所有路径延迟相同 |
选择建议:
- 如果条件之间有明确的优先级关系(如复位 > 使能 > 正常操作),用
if-else - 如果条件互斥且权重相同(如状态机的状态跳转),用
case
casez 和 casex
Verilog 提供了 case 的两个变体,用于处理 “不关心”(don’t care)的情况:
| 语句 | 不关心的值 | 典型用途 |
|---|---|---|
casez | z 视为不关心 | 优先级编码器 |
casex | x 和 z 都视为不关心 | 少用,易出问题 |
// 优先级编码器: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
- 第一个值是延时单位,第二个值是精度
- 仅影响仿真,不影响综合
#10在1ns/1ps下表示延时 10ns
define / undef:宏定义
`define BUS_WIDTH 16
reg [`BUS_WIDTH-1:0] data; // 使用宏
`undef BUS_WIDTH // 取消定义
`define vs parameter 的区别:
| 特性 | `define | parameter |
|---|---|---|
| 作用域 | 全局(跨文件) | 模块级 |
| 可配置性 | 编译时确定 | 实例化时可修改 |
| 典型用途 | 全局常量、编译开关 | 模块的可配置参数 |
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 | 语句组 | 建议总是加上 |
避免锁存器的三条铁律
- 组合逻辑
always @(*)中的if必须有else - 组合逻辑
always @(*)中的case必须有default - 组合逻辑中所有被赋值的信号,在每个分支都必须被赋值
给初学者的建议:每次写完 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 有什么区别?
没有区别,
?是z在casez中的简写。写4'b1???和4'b1zzz效果一样。推荐用?,可读性更好。
Q4:什么时候用 define,什么时候用 parameter?
如果常量需要在多个文件中共享,用
`define。如果常量只属于一个模块且需要在实例化时配置,用parameter。简单规则:跨文件用宏,模块内用参数。
参考资料
- IEEE Std 1364-2005: IEEE Standard for Verilog Hardware Description Language
- Xilinx/AMD,UG901: Vivado Design Suite User Guide - Synthesis(第 4 章)
- Pong P. Chu,FPGA Prototyping by Verilog Examples(第 3 章)
- 夏宇闻,《Verilog 数字系统设计教程》
系列导航:本文是「FPGA 入门系列」第 10 篇。
如果这篇文章对你有帮助,欢迎点赞、收藏。if-else 和 case 的区别、锁存器的避免方法,这些都是 FPGA 面试和实际开发中的高频考点。