Verilog中阻塞与非阻塞赋值的深层区别与仿真陷阱:设计指南与验证实践

二牛学FPGA
文章2026-04-25
60

Quick Start

  • 准备任意一款FPGA开发板(如Xilinx Artix-7)或仿真工具(Vivado Simulator / ModelSim)。
  • 创建两个Verilog模块:一个用阻塞赋值(=)实现移位寄存器,另一个用非阻塞赋值(<=)实现相同功能。
  • 编写测试平台,观察两种赋值方式在仿真波形上的差异。
  • 对比综合后的RTL网表,确认硬件实现是否一致。

前置条件

  • 熟悉Verilog基本语法,包括always块和敏感列表。
  • 已安装并配置好FPGA仿真工具(如Vivado Simulator 2020.1以上版本或ModelSim SE-64 10.6c)。
  • 具备基本数字电路知识,了解寄存器、组合逻辑和时序逻辑的区别。

目标与验收标准

  • 理解阻塞赋值与非阻塞赋值的核心语义差异:阻塞赋值立即更新,非阻塞赋值在块结束时统一更新。
  • 掌握两种赋值方式在组合逻辑与时序逻辑中的正确用法,避免常见仿真-综合不匹配问题。
  • 能够通过仿真波形识别因赋值方式错误导致的竞争冒险或功能错误。
  • 验收标准:在仿真中,阻塞赋值移位寄存器的输出会出现非预期的中间值,而非阻塞赋值版本则正确实现移位功能;综合后两者网表结构相同。

实施步骤

步骤1:编写阻塞赋值移位寄存器模块

module shift_reg_blocking (
    input clk,
    input rst_n,
    input din,
    output reg [3:0] dout
);
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        dout = 4'b0;
    else begin
        dout[0] = din;
        dout[1] = dout[0];  // 阻塞赋值:立即使用新值
        dout[2] = dout[1];
        dout[3] = dout[2];
    end
end
endmodule

此模块在时钟上升沿触发,但阻塞赋值导致赋值语句按顺序执行:dout[0]先被更新为din,随后dout[1]读取已更新的dout[0],依此类推。仿真中,一个时钟周期内所有位同时完成移位,但实际硬件中寄存器更新需要时间,因此仿真行为与硬件行为不一致。

步骤2:编写非阻塞赋值移位寄存器模块

module shift_reg_nonblocking (
    input clk,
    input rst_n,
    input din,
    output reg [3:0] dout
);
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        dout <= 4'b0;
    else begin
        dout[0] <= din;
        dout[1] <= dout[0];  // 非阻塞赋值:使用旧值
        dout[2] <= dout[1];
        dout[3] <= dout[2];
    end
end
endmodule

非阻塞赋值在always块结束时统一更新,因此所有右值都使用进入always块时的旧值。仿真行为正确模拟了硬件寄存器在时钟边沿同时采样的特性。

步骤3:编写测试平台并运行仿真

module tb_shift_reg;
    reg clk, rst_n, din;
    wire [3:0] dout_block, dout_nonblock;

    shift_reg_blocking u_block (.clk(clk), .rst_n(rst_n), .din(din), .dout(dout_block));
    shift_reg_nonblocking u_nonblock (.clk(clk), .rst_n(rst_n), .din(din), .dout(dout_nonblock));

    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    initial begin
        rst_n = 0; din = 0;
        #20 rst_n = 1;
        #10 din = 1;
        #10 din = 0;
        #40 $finish;
    end
endmodule

运行仿真后,观察波形:阻塞赋值版本中,dout_block在时钟上升沿后立即变为全1(因为顺序赋值导致所有位同时更新),而非阻塞赋值版本中,dout_nonblock逐位移位,符合预期。

验证结果

  • 仿真结果:阻塞赋值模块在单个时钟周期内完成所有移位,产生“瞬移”现象;非阻塞赋值模块正确实现逐位移位。
  • 综合结果:两个模块综合后的网表相同,均为4个D触发器级联。因为综合工具忽略赋值方式,仅根据逻辑功能推断硬件。
  • 关键发现:仿真与综合的不匹配是阻塞赋值在时序逻辑中使用的典型陷阱。非阻塞赋值保证了仿真行为与硬件行为一致。

排障指南

  • 问题:仿真波形出现毛刺或意外跳变。

    原因:组合逻辑中使用了非阻塞赋值,导致赋值延迟。

    解决:组合逻辑always块中统一使用阻塞赋值。

  • 问题:仿真结果与硬件实测不一致。

    原因:时序逻辑中使用了阻塞赋值,仿真行为与硬件行为不同。

    解决:时序逻辑always块中统一使用非阻塞赋值。

  • 问题:综合后功能正确但仿真失败。

    原因:混合使用阻塞和非阻塞赋值导致仿真竞争。

    解决:遵循“组合逻辑用阻塞,时序逻辑用非阻塞”的黄金法则。

扩展:深入理解赋值机制

阻塞赋值与非阻塞赋值的本质差异源于Verilog的仿真事件调度机制。在仿真中,每个时间槽(time slot)分为多个区域:活跃区(active)、非活跃区(inactive)、NBA区(non-blocking assignment update)等。阻塞赋值在活跃区立即执行,而非阻塞赋值的右值计算在活跃区完成,但左值更新推迟到NBA区。这种调度机制使得非阻塞赋值天然适用于模拟寄存器行为,而阻塞赋值适用于组合逻辑的连续赋值。

风险边界:在同一个always块中混合使用阻塞和非阻塞赋值可能导致不可预测的仿真行为。例如,在时序逻辑中,如果部分信号用阻塞赋值、部分用非阻塞赋值,仿真结果可能依赖于语句顺序,而综合结果则完全由逻辑功能决定,造成仿真-综合不一致。此外,在多个always块中访问同一变量时,必须确保赋值方式一致,否则会产生竞争条件。

参考资源

  • IEEE Std 1364-2001, Verilog Hardware Description Language, Section 9.2: Blocking and non-blocking assignments.
  • Clifford E. Cummings, “Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!”, SNUG 2000.
  • Xilinx UG901: Vivado Design Suite User Guide, Synthesis, Chapter 3: Coding Guidelines.

附录:常见错误模式与修正

  • 错误模式1:在时序逻辑中使用阻塞赋值实现计数器。

    修正:改用非阻塞赋值,或使用“dout <= dout + 1”形式。

  • 错误模式2:在组合逻辑中使用非阻塞赋值实现多路选择器。

    修正:改用阻塞赋值,或使用assign连续赋值语句。

  • 错误模式3:在同一个always块中同时使用阻塞和非阻塞赋值更新同一变量。

    修正:拆分为两个always块,或统一赋值方式。

分类
技术分享
标签
Verilog阻塞赋值非阻塞赋值
浏览 60
分享:

相关推荐

同频道 · 相近分类

暂无相关推荐

作者

二牛学FPGA查看主页

同分类阅读

文章

延伸阅读与实操

  • 文章 + 课程联动深度文章常对应体系课章节,可一键选课。
  • 学习产出可参考笔记与作业案例在学习产出广场持续更新。

探索全站