1. Verilog赋值的两种方式:阻塞与非阻塞
刚接触Verilog时,很多人都会被这两种赋值方式搞得晕头转向。我自己刚开始学的时候,就经常把阻塞赋值(=)和非阻塞赋值(<=)用混,结果仿真出来的波形完全不对,调试了大半天才发现问题所在。这两种赋值方式看似简单,但用错了会导致电路功能完全异常。
阻塞赋值就像排队买奶茶,你必须等前面的人买完才能轮到你。在Verilog中,这意味着当前语句执行完成后,才会执行下一条语句。比如下面这段代码:
always @(posedge clk) begin a = b; c = a; end这里c最终得到的值是b的值,因为a=b执行完后,a的值立即更新,然后c=a才会执行。
而非阻塞赋值则像自助餐厅,所有人可以同时取餐。在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可能看不到预期的值。这时应该使用$strobe:
always @(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设计中,我们就因为部分寄存器错误使用了阻塞赋值,导致时序无法收敛。改为非阻塞赋值后,时序立即满足了要求。