Verilog阻塞与非阻塞赋值的实战应用与避坑指南

张开发
2026/6/25 16:06:45 15 分钟阅读
Verilog阻塞与非阻塞赋值的实战应用与避坑指南
1. Verilog赋值的两种方式阻塞与非阻塞刚接触Verilog时很多人都会被这两种赋值方式搞得晕头转向。我自己刚开始学的时候就经常把阻塞赋值和非阻塞赋值用混结果仿真出来的波形完全不对调试了大半天才发现问题所在。这两种赋值方式看似简单但用错了会导致电路功能完全异常。阻塞赋值就像排队买奶茶你必须等前面的人买完才能轮到你。在Verilog中这意味着当前语句执行完成后才会执行下一条语句。比如下面这段代码always (posedge clk) begin a b; c a; end这里c最终得到的值是b的值因为ab执行完后a的值立即更新然后ca才会执行。而非阻塞赋值则像自助餐厅所有人可以同时取餐。在Verilog中所有非阻塞赋值语句会同时计算右边的值然后在always块结束时统一更新左边的值。看这个例子always (posedge clk) begin a b; c a; end这种情况下c得到的是a原来的值而不是b的值因为a和c的更新是同时发生的。2. 阻塞赋值的深入解析与使用场景2.1 阻塞赋值的工作原理阻塞赋值的特点是立即生效。当执行到阻塞赋值语句时会立即计算右边的表达式并将结果赋给左边的变量。这个过程会阻塞后续语句的执行直到当前赋值完成。这种特性使得阻塞赋值非常适合用来描述组合逻辑。比如我们要实现一个简单的与门always (*) begin c a b; end这里使用阻塞赋值非常自然因为组合逻辑的输出应该立即响应输入的变化。2.2 阻塞赋值的常见陷阱虽然阻塞赋值用起来简单直接但有几个坑需要特别注意在时序逻辑中使用阻塞赋值这会导致仿真结果与综合后的实际电路行为不一致。比如always (posedge clk) begin a b; c a; end仿真时看起来可能没问题但实际电路会表现出不可预测的行为。多个always块对同一变量进行阻塞赋值这会产生多驱动问题导致综合错误。阻塞赋值的顺序依赖性由于阻塞赋值的顺序执行特性改变语句顺序可能会完全改变电路行为。我在一个项目中就遇到过这样的问题原本想用阻塞赋值实现一个简单的数据通路但因为语句顺序安排不当导致最终综合出来的电路完全不是我想要的。后来改用非阻塞赋值才解决了问题。3. 非阻塞赋值的深入解析与使用场景3.1 非阻塞赋值的工作原理非阻塞赋值的工作过程可以分为两个阶段计算阶段在always块执行时计算所有非阻塞赋值右边的表达式更新阶段在always块结束时统一更新左边的变量这种先计算后更新的机制完美模拟了时序电路中寄存器的工作方式。每个时钟沿到来时寄存器同时采样输入然后在时钟沿结束时更新输出。3.2 非阻塞赋值的优势非阻塞赋值最大的优势在于它能准确描述时序电路的行为。考虑一个简单的流水线寄存器always (posedge clk) begin stage1 input; stage2 stage1; stage3 stage2; end这种写法能正确实现三级流水线因为所有赋值都是同时更新的。如果改用阻塞赋值就会变成单级寄存器完全破坏了流水线的功能。另一个优势是代码的顺序不影响功能。由于所有更新都是同时进行的调整非阻塞赋值语句的顺序不会改变电路行为。这使得代码更易于维护和修改。4. 混合使用阻塞与非阻塞赋值的危险4.1 绝对禁止的混用模式在同一个always块中混合使用阻塞和非阻塞赋值是Verilog设计的大忌。比如always (posedge clk) begin a b; // 阻塞赋值 c d; // 非阻塞赋值 end这种写法会导致仿真和综合结果不一致可能产生难以调试的竞争条件。我在早期的一个项目中就犯过这个错误仿真时一切正常但烧写到FPGA后电路完全不能工作花了整整两天才找到这个混用赋值的问题。4.2 允许的混合使用场景唯一相对安全的混合使用场景是在不同的always块中分别用阻塞赋值描述组合逻辑用非阻塞赋值描述时序逻辑。例如// 组合逻辑部分 always (*) begin comb_out a b; // 阻塞赋值 end // 时序逻辑部分 always (posedge clk) begin reg_out comb_out; // 非阻塞赋值 end即使如此也要确保两个always块之间没有循环依赖否则仍可能导致问题。5. 实际工程中的最佳实践5.1 可综合代码的赋值选择原则根据多年项目经验我总结了以下黄金法则时序电路一律使用非阻塞赋值包括寄存器、状态机、计数器等。组合电路一律使用阻塞赋值如多路选择器、译码器等。不要在同一个always块中混合两种赋值这是万恶之源。尽量不在多个always块中对同一变量赋值即使使用非阻塞赋值也可能导致问题。5.2 常见错误案例分析案例1错误的移位寄存器实现// 错误写法 always (posedge clk) begin reg1 din; reg2 reg1; reg3 reg2; end这个移位寄存器实际上只会保存din的值因为阻塞赋值会立即更新reg1然后reg2得到的是更新后的reg1值。正确写法always (posedge clk) begin reg1 din; reg2 reg1; reg3 reg2; end案例2组合逻辑中的非阻塞赋值// 错误写法 always (*) begin out a b; end这会导致out不能及时响应a和b的变化违背了组合逻辑的特性。正确写法always (*) begin out a b; end6. 仿真与调试技巧6.1 使用$strobe观察非阻塞赋值由于非阻塞赋值的更新是延后的使用普通的$display可能看不到预期的值。这时应该使用$strobealways (posedge clk) begin a b; $strobe(At time %0t: a %b, $time, a); end$strobe会在所有非阻塞赋值完成后才执行因此能显示更新后的值。6.2 避免使用#0延迟新手常犯的一个错误是使用#0延迟来调整赋值时机always (posedge clk) begin a b; #0 c a; end这种做法极其危险会导致仿真与综合结果不一致应该完全避免。7. 从RTL到综合的思考7.1 赋值方式对综合结果的影响正确的赋值方式不仅影响仿真结果更直接影响综合出的电路结构。非阻塞赋值综合后通常对应触发器(Flip-Flop)而阻塞赋值综合后通常形成组合逻辑。我曾对比过两种写法综合后的网表使用非阻塞赋值的计数器综合出标准的寄存器链错误使用阻塞赋值的计数器则综合出一堆锁存器和组合环路7.2 性能考量在高速设计中赋值方式的选择还会影响时序性能。非阻塞赋值描述的时序电路更容易满足建立保持时间而阻塞赋值如果用在时序逻辑中可能导致保持时间违例。在一个400MHz的SerDes设计中我们就因为部分寄存器错误使用了阻塞赋值导致时序无法收敛。改为非阻塞赋值后时序立即满足了要求。

更多文章