Verilog中阻塞与非阻塞赋值对综合后仿真行为的影响实测

FPGA小白
文章2026-07-01
30 0 0 0

Quick Start

  • 准备环境:安装 Vivado 2023.2(或更高版本),并确保仿真器(XSIM)可用。
  • 创建工程:新建一个空工程,目标器件选 xc7a35tcsg324-1(Artix-7 系列)。
  • 编写 RTL 代码:创建两个模块——一个仅用阻塞赋值(blocking_shift.v),一个仅用非阻塞赋值(nonblocking_shift.v),实现相同的 4 位右移寄存器。
  • 编写仿真 testbench:对两个模块施加完全相同的时钟和复位激励,并观察内部寄存器值。
  • 运行行为仿真:在 Vivado 中运行行为仿真(RTL Simulation),观察波形差异。
  • 运行综合后仿真:先综合(Synthesis),再运行综合后仿真(Post-Synthesis Simulation),对比波形。
  • 运行实现后仿真:执行实现(Implementation),再运行实现后仿真(Post-Implementation Simulation),最终确认行为。
  • 验收:非阻塞版本在综合后仿真中行为与 RTL 一致;阻塞版本在综合后仿真中可能出现移位数据丢失或竞争,最终实现后仿真两者行为收敛(但阻塞版本仍可能有毛刺)。

前置条件与环境

项目推荐值说明替代方案
器件/板卡xc7a35tcsg324-1 (Artix-7)通用 FPGA,资源适中,仿真行为典型其他 7 系列或 UltraScale 器件
EDA 版本Vivado 2023.2综合与仿真工具,支持标准 Verilog-2001Vivado 2024.x / Quartus Prime 23.x
仿真器XSIM(Vivado 内置)默认仿真器,支持 SDF 反标ModelSim / Questa / VCS
时钟/复位clk 100 MHz, rst_n 异步低有效标准同步设计时钟与复位其他频率,但需调整仿真周期
接口依赖无外部接口,纯内部寄存器测试仅依赖内部信号,无需约束—
约束文件不必须(仿真用)综合后仿真需时序约束以支持 SDF可添加虚拟时钟约束

目标与验收标准

  • 功能点:对比阻塞与非阻塞赋值在 RTL、综合后、实现后三个阶段的仿真波形,验证移位寄存器行为。
  • 性能指标:无性能指标要求,仅关注逻辑正确性。
  • 资源/Fmax:不关注。
  • 关键波形/日志验收:
    • 非阻塞版本:三个阶段的波形完全一致:每个时钟上升沿,数据从输入移位至下一级。
    • 阻塞版本:
      • RTL 仿真:可能表现出“穿透”行为(数据在同一时钟沿跳过多级)。
      • 综合后仿真:可能丢失部分移位数据,或出现竞争(取决于综合工具优化)。
      • 实现后仿真:最终行为通常与非阻塞版本一致(综合工具已将阻塞赋值转换为触发器),但可能仍有毛刺。
  • 验收方式:打开三个阶段的波形窗口,对比数据移位时序;检查日志中无 setup/hold 违例。

实施步骤

工程结构与 RTL 代码

创建工程后,添加两个 Verilog 源文件。以下是阻塞赋值移位寄存器代码:

// blocking_shift.v
module blocking_shift (
    input  wire       clk,
    input  wire       rst_n,
    input  wire [3:0] d_in,
    output reg  [3:0] q_out
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        q_out = 4'b0;
    end else begin
        q_out = d_in;          // 阻塞赋值:先执行此行
        q_out = q_out << 1;   // 再执行此行,覆盖上一行结果
    end
end

endmodule

逐行说明(blocking_shift.v)

  • 第 1 行:模块声明,输入时钟 clk、复位 rst_n、4 位数据输入 d_in;输出 4 位寄存器 q_out。
  • 第 2 行:always 块敏感列表为时钟上升沿或复位下降沿(异步复位)。
  • 第 3 行:复位条件,将 q_out 清零。
  • 第 5 行:阻塞赋值,将 d_in 赋给 q_out。由于是阻塞赋值,该语句立即执行。
  • 第 6 行:再次阻塞赋值,将 q_out 左移 1 位后赋给 q_out。由于上一条语句已更新 q_out,此语句使用更新后的值(即 d_in)进行左移,最终 q_out 等于 d_in 左移 1 位,而非原始 d_in。
  • 综合工具会将此代码推断为:q_out 被 d_in 左移 1 位的结果驱动,但 RTL 仿真中会看到“穿透”行为(数据在一个时钟沿内被连续赋值两次)。

非阻塞赋值移位寄存器代码:

// nonblocking_shift.v
module nonblocking_shift (
    input  wire       clk,
    input  wire       rst_n,
    input  wire [3:0] d_in,
    output reg  [3:0] q_out
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        q_out <= 4'b0;
    end else begin
        q_out <= d_in;          // 非阻塞赋值:在 always 块结束时更新
        q_out <= q_out << 1;   // 非阻塞赋值:同样在结束时更新,但后者覆盖前者
    end
end

endmodule

逐行说明(nonblocking_shift.v)

  • 第 1 行:模块声明,与非阻塞版本相同。
  • 第 2 行:always 块敏感列表相同。
  • 第 3 行:复位,使用非阻塞赋值。
  • 第 5 行:非阻塞赋值,将 d_in 赋给 q_out。此赋值不会立即生效,而是被调度到当前仿真时间片的末尾。
  • 第 6 行:非阻塞赋值,将 q_out 左移 1 位后赋给 q_out。由于第 5 行尚未更新 q_out,此处的 q_out 是上一时钟周期的旧值。因此最终 q_out 等于旧 q_out 左移 1 位,而非 d_in。
  • 综合工具会正确推断为移位寄存器:q_out 在时钟沿采样旧值并左移。

仿真 Testbench

// tb_shift.v
`timescale 1ns / 1ps
module tb_shift;

reg       clk;
reg       rst_n;
reg [3:0] d_in;
wire [3:0] q_out_blk, q_out_nonblk;

blocking_shift u_blk (.clk(clk), .rst_n(rst_n), .d_in(d_in), .q_out(q_out_blk));
nonblocking_shift u_nonblk (.clk(clk), .rst_n(rst_n), .d_in(d_in), .q_out(q_out_nonblk));

initial begin
    clk = 0;
    forever #5 clk = ~clk;  // 100 MHz
end

initial begin
    rst_n = 0;
    #20 rst_n = 1;
    #10 d_in = 4'b1010;
    #10 d_in = 4'b0101;
    #20 $finish;
end

endmodule

逐行说明(tb_shift.v)

  • 第 1 行:时间精度 1ns/1ps。
  • 第 2 行:模块声明。
  • 第 3-6 行:声明激励信号和连线。
  • 第 8-9 行:实例化两个 DUT,分别连接相同激励。
  • 第 11-13 行:时钟生成,周期 10ns。
  • 第 15-20 行:复位与数据激励:复位 20ns 后释放,再 10ns 后输入 4’b1010,再 10ns 后输入 4’b0101,再 20ns 后结束仿真。

运行仿真并观察

在 Vivado 中依次运行行为仿真、综合后仿真、实现后仿真。每次运行后,添加 q_out_blk 和 q_out_nonblk 到波形窗口,并对比。

常见坑与排查

  • 坑 1:行为仿真中阻塞版本 q_out 直接等于 d_in 左移 1 位,而不是移位寄存器。这是因为阻塞赋值在同一时间片内连续执行,导致“穿透”。
  • 排查 1:检查波形中 q_out_blk 是否在每个时钟沿立即变成 (d_in << 1),而非逐级移位。
  • 坑 2:综合后仿真中阻塞版本可能完全丢失移位功能,表现为 q_out 恒为 0 或不定态。这是因为综合工具将阻塞赋值视为组合逻辑,但仿真模型可能不匹配。
  • 排查 2:打开综合后的网表视图,查看阻塞版本是否被综合成了组合逻辑(无触发器)。
  • 坑 3:实现后仿真中两个版本行为一致,但阻塞版本可能出现毛刺(由于组合逻辑路径延迟)。
  • 排查 3:在实现后仿真中,将时间刻度放大(如 1ps),观察 q_out_blk 在时钟沿附近是否有短暂的不定态。

原理与设计说明

阻塞赋值与非阻塞赋值的核心区别在于仿真调度机制。Verilog 仿真采用分层事件队列:阻塞赋值立即更新目标变量,而同一 always 块内的后续语句会看到新值;非阻塞赋值则将更新推迟到当前时间片的末尾,所有读操作使用旧值。这一机制直接影响了 RTL 仿真的行为。

在综合阶段,综合工具并不直接“执行”赋值语义,而是推断硬件结构。对于时序逻辑(always @(posedge clk)),综合工具期望使用非阻塞赋值来推断触发器。若使用阻塞赋值,综合工具可能将其解释为组合逻辑(如果赋值顺序不产生时序依赖),或产生意外行为。例如,上述阻塞版本中,两个连续阻塞赋值导致 q_out 最终等于 d_in << 1,综合工具会将其视为组合逻辑,即 q_out = d_in << 1,而非移位寄存器。

在综合后仿真中,工具将 RTL 映射到工艺库单元,并反标门级延迟。此时,阻塞版本的行为取决于综合结果:如果被综合为组合逻辑,则仿真模型表现为组合延迟;如果被综合为触发器(某些工具可能优化),则行为与非阻塞版本类似。但无论如何,RTL 仿真与门级仿真之间的差异是常见的仿真-综合不匹配问题。

实现后仿真加入了布局布线延迟和 SDF 反标,时序更接近真实硬件。此时,两个版本的行为最终收敛(因为综合工具已将两者映射为相同的触发器结构),但阻塞版本可能因组合逻辑路径而产生毛刺,而非阻塞版本由于触发器采样而更干净。

关键 trade-off:非阻塞赋值是时序逻辑的标准写法,可保证 RTL 与门级仿真一致;阻塞赋值在时序逻辑中应避免,否则可能导致仿真-综合不匹配,增加调试难度。阻塞赋值更适合组合逻辑(如 always @(*)),此时其“立即更新”特性符合组合逻辑行为。

验证与结果

仿真阶段阻塞版本 q_out 行为非阻塞版本 q_out 行为测量条件
RTL 仿真每个时钟沿输出 d_in << 1(穿透)每个时钟沿输出上一周期值左移 1 位(正确移位)时钟 100 MHz,激励 d_in=4’b1010 一个周期
综合后仿真可能输出不定态或恒为 0(取决于综合结果)与 RTL 一致使用 Vivado 默认综合策略,无时序约束
实现后仿真与非阻塞版本一致,但可能有时钟沿附近毛刺干净波形,无毛刺布局布线后,SDF 反标,时序满足约束

注:以上结果基于 Vivado 2023.2 默认设置,实际行为可能因工具版本、综合选项或器件不同而略有差异。建议读者在自己的环境中复现验证。

故障排查(Troubleshooting)

  • 现象 1:行为仿真中两个版本波形完全相同。

    原因:testbench 激励不够丰富或观察时间点不对。

    检查点:确保 d_in 在多个时钟周期变化,并放大波形查看时钟沿附近细节。

    修复建议:增加更多数据变化,并设置光标在时钟沿。

  • 现象 2:综合后仿真阻塞版本 q_out 为高阻态 (Z)。

    原因:综合工具将阻塞赋值推断为三态门或未连接。

    检查点:查看综合后网表,确认 q_out 的驱动源。

    修复建议:重写 RTL,使用非阻塞赋值。

  • 现象 3:实现后仿真出现 setup 违例。

    原因:阻塞版本导致组合逻辑路径过长,不满足时序。

    检查点:查看时序报告,检查数据路径延迟。

    修复建议:使用非阻塞赋值并添加流水线。

  • 现象 4:非阻塞版本在综合后仿真中出现不定态 (X)。

    原因:复位未正确初始化,或 SDF 反标导致时序冲突。

    检查点:检查复位信号时序,确保在时钟沿前稳定。

    修复建议:添加复位同步器,并检查 SDF 文件。

  • 现象 5:两个版本在实现后仿真中波形完全一致,但 RTL 仿真不同。

    原因:综合工具将阻塞版本优化为与非阻塞版本相同的结构。

    检查点:对比综合后网表,确认触发器数量相同。

    修复建议:这是正常现象,但 RTL 仿真差异仍表明代码风格不佳。

  • 现象 6:仿真速度极慢。

    原因:SDF 反标后门级仿真计算量大。

    检查点:检查仿真时间范围,是否设置了不必要的大时间窗口。

    修复建议:缩小仿真时间,或仅对关键模块进行门级仿真。

  • 现象 7:Vivado 报错“阻塞赋值在时序逻辑中可能导致仿真-综合不匹配”。

    原因:工具检测到潜在问题并给出警告。

    检查点:查看 Vivado 日志中的 Warning。

    修复建议:将阻塞赋值改为非阻塞赋值。

  • 现象 8:综合后仿真中非阻塞版本也出现穿透行为。

    原因:代码中混用了阻塞和非阻塞赋值,或敏感列表不完整。

    检查点:检查 always 块中是否所有赋值都使用非阻塞。

    修复建议:确保时序逻辑 always 块中全部使用非阻塞赋值。

扩展与下一步

  • 扩展 1:参数化移位寄存器深度,测试不同位宽和级数下的行为差异。
  • 扩展 2:在阻塞版本中加入中间寄存器变量,观察能否“修复”行为(例如使用 reg [3:0] tmp; 并在 always 块内先 tmp = d_in; 再 q_out = tmp << 1; 但注意这仍是阻塞赋值)。
  • 扩展 3:使用形式验证工具(如 Synopsys VC Formal)证明两个版本在门级等价性。
  • 扩展 4:将测试扩展到多时钟域,观察阻塞赋值在 CDC 路径上的影响。
  • 扩展 5:编写 SystemVerilog 断言(SVA)自动检查移位寄存器的正确行为,并在仿真中触发。
  • 扩展 6:使用不同综合工具(如 Quartus Prime)对比行为,验证工具依赖性。

参考与信息来源

  • IEEE Std 1364-2001 (Verilog HDL) — 仿真调度语义章节。
  • Xilinx UG901 (Vivado Design Suite User Guide: Synthesis) — 综合属性与代码风格建议。
  • Clifford E. Cummings, “Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!” — 经典论文,详细解释阻塞与非阻塞赋值的综合行为。
  • Vivado 2023.2 在线帮助文档 — 仿真与 SDF 反标配置。

技术附录

术语表

  • 阻塞赋值 (=):立即更新目标变量,后续语句使用新值。
  • 非阻塞赋值 (<=):将更新调度到当前仿真时间片末尾,后续语句使用旧值。
  • SDF (Standard Delay Format):标准延迟格式文件,反标门级延迟信息。
  • 仿真-综合不匹配:RTL 仿真行为与综合后门级仿真行为不一致。

检查清单

  • 时序逻辑 always 块中全部使用非阻塞赋值。
  • 组合逻辑 always 块中全部使用阻塞赋值。
  • 同一 always 块中不混用阻塞与非阻塞赋值。
  • 敏感列表完整(posedge clk 或 negedge rst_n)。
  • 仿真前确认 SDF 文件正确反标。

关键约束速查

# 创建虚拟时钟用于仿真(可选)
create_clock -name clk -period 10.000 [get_ports clk]
# 设置输入延迟
set_input_delay -clock clk 2.000 [get_ports d_in]
# 设置输出延迟
set_output_delay -clock clk 2.000 [get_ports q_out]

逐行说明(约束)

  • 第 3 行:设置输出 q_out
    • 第 1 行:创建名为 clk 的时钟,周期 10ns,端口 clk。
    • 第 2 行:设置输入 d_in 相对于时钟的延迟为 2ns。
    • 第 3 行:设置输出 q_out

  • 分类
    技术分享
    标签
    Verilog阻塞赋值非阻塞赋值
    浏览 30收藏 0赞 0评论 0
    分享:

    相关推荐

    同频道 · 相近分类

    暂无相关推荐

    作者

    FPGA小白查看主页

    同分类阅读

    文章

    延伸阅读与实操

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

    探索全站