别再傻傻分不清了!Verilog里always、assign和always@(*)到底怎么用?一个例子讲透

张开发
2026/4/21 17:22:10 15 分钟阅读

分享文章

别再傻傻分不清了!Verilog里always、assign和always@(*)到底怎么用?一个例子讲透
Verilog三大关键结构深度解析always、assign与always(*)实战指南在数字电路设计领域Verilog作为硬件描述语言的代表其核心建模结构直接影响着设计质量与仿真结果。对于初学者而言always、assign和always(*)这三个看似简单的结构却隐藏着诸多容易踩坑的细节。本文将从一个实际的多路选择器案例出发通过代码对比、波形分析和综合结果揭示三者的本质区别与应用场景。1. 基础概念与核心差异Verilog中的这三种结构代表了不同的硬件建模思路。assign语句是最直接的组合逻辑描述方式它相当于在电路中建立了一条永久的连接线。例如用assign实现一个与门wire a, b; wire and_result; assign and_result a b;这种写法明确表达了组合逻辑的并行特性——任何输入信号的变化都会立即反映在输出上。而always块则更为复杂它根据敏感列表的不同可以表现为两种形态电平敏感如always(*)或always(a,b)用于描述组合逻辑边沿敏感如always(posedge clk)用于描述时序逻辑初学者最容易混淆的是reg型变量在always块中的行为。需要特别注意的是在always(*)中定义的reg型变量并非真正的寄存器它只是语法要求的标识符。真正的寄存器特性需要通过边沿触发才能实现。2. 多路选择器的三种实现方式让我们通过一个2:1多路选择器的实现直观比较三种写法的差异。假设我们需要实现以下功能输入sel选择信号data0和data1数据输入输出当sel0时输出data0否则输出data12.1 assign实现方式module mux_assign( input sel, input data0, data1, output out ); assign out sel ? data1 : data0; endmodule这是最简洁的实现方式特点包括输出out必须声明为wire类型表达式右侧任何信号变化都会立即更新输出综合后通常生成一个多路选择器原语2.2 always(*)实现方式module mux_always_comb( input sel, input data0, data1, output reg out ); always(*) begin out sel ? data1 : data0; end endmodule这种写法的关键点输出out必须声明为reg类型尽管是组合逻辑敏感列表(*)让综合器自动推断所有输入信号必须使用阻塞赋值()与时序逻辑区分2.3 传统always实现方式module mux_always_manual( input sel, input data0, data1, output reg out ); always(sel or data0 or data1) begin out sel ? data1 : data0; end endmodule这种传统写法的特点需要手动列出所有敏感信号容易遗漏信号导致仿真与综合不匹配Verilog-2001标准后推荐使用always(*)替代3. 仿真波形中的关键差异通过仿真这三种实现方式我们可以观察到一些微妙但重要的区别。以下是在相同测试向量下的行为对比时间点seldata0data1assign输出always(*)输出传统always输出0ns01011110ns11000020ns1x000x30ns0x0xxx从表中可以看出两个关键现象初始状态差异assign和always(*)在仿真开始时就有确定值而传统always可能因为敏感信号列表不完整出现不定态X传播行为当输入出现不定态(x)时assign和always(*)表现一致但传统always可能因敏感列表问题导致异常特别值得注意的是always(*) b 1b0这种特殊情况的仿真行为reg b; always(*) b 1b0;这段代码会导致仿真开始时b为不定态(x)由于右侧常量不会变化敏感事件永远不会触发b将保持x状态而不会变为0综合后电路可能正常工作与assign等效但仿真不匹配4. 综合结果与硬件映射三种写法在综合后的硬件实现上也有细微差别。以Xilinx Vivado针对Artix-7设备的综合结果为例实现方式使用资源最大频率功耗估算assign1个LUT450MHz2mWalways(*)1个LUT450MHz2mW传统always1个LUT额外逻辑430MHz3mW虽然三种实现最终都映射到了查找表(LUT)上但传统always方式因为需要处理显式敏感列表可能会引入额外的控制逻辑。现代综合工具对always(*)有专门优化能生成与assign同样高效的电路。5. 实际应用中的选择策略基于以上分析我们可以总结出以下选用原则简单组合逻辑优先使用assign特别是单一表达式赋值不需要复杂过程控制的情况需要明确表达连续赋值语义时复杂组合逻辑使用always(*)当遇到需要if-else或case等多路选择需要临时变量辅助计算代码可读性更重要时避免使用的情况传统显式敏感列表的always易出错always中赋常量值仿真异常混合使用阻塞/非阻塞赋值时序逻辑必须用非阻塞对于时序逻辑必须使用边沿触发的always块always(posedge clk or negedge rst_n) begin if(!rst_n) begin q 1b0; end else begin q d; end end记住几个关键实践要点组合逻辑用阻塞赋值时序逻辑用非阻塞赋值敏感列表要么用(*)要么用明确的边沿信号变量类型遵循assign→wirealways→reg仿真与综合的差异需要特别关注6. 高级技巧与常见问题解决在实际工程中我们还会遇到一些更复杂的情况需要处理6.1 锁存器意外生成不完整的条件判断会导致意外的锁存器生成。例如always(*) begin if(enable) begin out data; end // 缺少else分支 end这种情况综合工具会生成锁存器来保持enable为低时的out值。解决方法补全所有条件分支或者初始化为默认值always(*) begin out 1b0; // 默认值 if(enable) begin out data; end end6.2 组合逻辑环路不恰当的顺序语句可能导致组合逻辑环路always(*) begin a b; b a; // 形成环路 end这种设计会导致仿真器陷入无限循环综合工具报错实际电路产生振荡解决方法检查变量间的依赖关系确保每个信号有明确的驱动源使用lint工具静态检查6.3 仿真与综合不一致除了之前提到的always(*)常量赋值问题还有其他常见的不一致情况初始化值差异reg a 1b0; // 仿真有效综合忽略解决方案使用复位信号明确初始化延时语句assign #5 out in; // 仿真有效综合忽略解决方案仅用于testbench不在RTL中使用不完全敏感列表always(a) begin out a b; // 遗漏b end解决方案统一使用always(*)7. 验证方法与调试技巧为了确保代码的正确性我们需要建立有效的验证方法静态检查使用lint工具如SpyGlass检查常见问题检查敏感列表完整性验证赋值类型是否匹配仿真验证测试所有条件分支验证初始状态检查X态传播波形调试技巧标记关键信号的变化点检查信号间的因果关系特别关注仿真与预期不符的时间点一个典型的测试平台结构module tb; reg sel, data0, data1; wire out_assign; reg out_always; // 实例化待测模块 mux_assign u1(sel, data0, data1, out_assign); mux_always_comb u2(sel, data0, data1, out_always); initial begin // 初始化 sel 0; data0 0; data1 0; // 测试用例1 #10 sel0; data01; data10; // 测试用例2 #10 sel1; data00; data11; // 结束仿真 #10 $finish; end // 波形记录 initial begin $dumpfile(wave.vcd); $dumpvars(0, tb); end endmodule在多年的项目实践中我发现最容易出错的地方往往是对reg型变量的误解。很多初学者认为reg就代表寄存器实际上只有在时序逻辑中才会真正综合出触发器。组合逻辑中的reg只是语法要求它描述的仍然是电平敏感的行为。

更多文章