FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
登录
首页-技术文章/快讯-技术分享-正文

Verilog 阻塞赋值与非阻塞赋值:常见错误与仿真验证实践指南

FPGA小白FPGA小白
技术分享
1天前
0
0
8

Quick Start

  • 打开 Vivado 2024.2 或更高版本,创建一个新工程,目标器件选择 Xilinx Artix-7 XC7A35T。
  • 新建一个 Verilog 源文件,命名为 blocking_nonblocking_top.v
  • 编写一个简单的移位寄存器模块,分别用阻塞赋值和非阻塞赋值实现,并对比仿真波形。
  • 编写 Testbench,驱动时钟和复位,观察移位寄存器的输出。
  • 运行行为仿真(Behavioral Simulation),查看波形。
  • 对比两种赋值方式下,移位寄存器输出延迟一个时钟周期或立即更新的差异。
  • 修改代码,故意引入混合赋值错误,观察仿真结果与预期不符的现象。
  • 阅读本指南后续章节,理解错误根源并掌握正确用法。

前置条件与环境

项目推荐值说明替代方案
器件/板卡Xilinx Artix-7 XC7A35T常用入门级 FPGA,资源充足Intel Cyclone IV / Lattice iCE40
EDA 版本Vivado 2024.2支持 SystemVerilog-2017,仿真与综合一致Vivado 2023.x / Quartus Prime 23.x
仿真器Vivado Simulator (xsim)内置于 Vivado,无需额外安装ModelSim / Questa / Verilator
时钟/复位50MHz 时钟,异步高有效复位标准时序仿真条件100MHz / 低有效复位
接口依赖无外部接口,纯仿真验证仅需波形查看器
约束文件无(仿真不需要)上板时需要 XDC 约束

目标与验收标准

  • 功能点:用阻塞赋值和非阻塞赋值分别实现 4 位移位寄存器,仿真波形能清晰展示两者在时钟沿更新的差异。
  • 性能指标:无综合警告(如 inferred latch),仿真无 race condition。
  • 资源/Fmax:综合后 LUT 使用 ≤ 8 个,Fmax ≥ 200MHz(示例值,以实际综合报告为准)。
  • 验收方式:运行仿真后,在波形中观察到:
    非阻塞赋值移位寄存器:输入 DIN 在时钟上升沿后一个时钟周期出现在 Q0,再一个周期出现在 Q1,依此类推。
    — 阻塞赋值移位寄存器(正确写法应避免,此处仅用于对比):输入 DIN 在同一个时钟沿同时出现在所有 Q 输出(实际是组合逻辑行为)。

实施步骤

工程结构与关键模块

  • shift_reg_nonblocking.v — 非阻塞赋值移位寄存器
  • shift_reg_blocking.v — 阻塞赋值移位寄存器(仅用于对比,不推荐实际使用)
  • tb_shift_reg.v — 顶层 Testbench

shift_reg_nonblocking.v 代码

module shift_reg_nonblocking (
    input  wire       clk,
    input  wire       rst_n,
    input  wire [3:0] din,
    output reg  [3:0] q0,
    output reg  [3:0] q1,
    output reg  [3:0] q2,
    output reg  [3:0] q3
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        q0 <= 4'd0;
        q1 <= 4'd0;
        q2 <= 4'd0;
        q3 <= 4'd0;
    end else begin
        q0 <= din;
        q1 <= q0;
        q2 <= q1;
        q3 <= q2;
    end
end

endmodule

逐行说明

  • 第 1 行:模块声明,端口列表包含时钟、复位、输入数据和四个输出寄存器。
  • 第 2–5 行:端口方向与位宽定义。output reg 表示输出是寄存器类型,可在 always 块中赋值。
  • 第 7 行:always 块敏感列表为时钟上升沿和复位下降沿(异步复位)。
  • 第 8–13 行:复位逻辑,使用非阻塞赋值(<=)将所有输出清零。
  • 第 14–18 行:移位逻辑。注意这里全部使用非阻塞赋值。在同一个 always 块中,所有赋值操作在时钟沿同时采样右值,然后在时钟沿结束后统一更新左值。因此 q1 <= q0 采样的是 q0 在时钟沿前的旧值,而不是 q0 <= din 更新后的新值。这正好实现了移位寄存器:每个时钟沿,数据从 din 移到 q0,从 q0 移到 q1,依此类推。
  • 第 20 行:模块结束。

shift_reg_blocking.v 代码(错误示范,仅用于对比)

module shift_reg_blocking (
    input  wire       clk,
    input  wire       rst_n,
    input  wire [3:0] din,
    output reg  [3:0] q0,
    output reg  [3:0] q1,
    output reg  [3:0] q2,
    output reg  [3:0] q3
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        q0 = 4'd0;
        q1 = 4'd0;
        q2 = 4'd0;
        q3 = 4'd0;
    end else begin
        q0 = din;
        q1 = q0;
        q2 = q1;
        q3 = q2;
    end
end

endmodule

逐行说明

  • 第 1–6 行:模块声明与端口定义,与非阻塞版本相同。
  • 第 8 行:always 块敏感列表相同。
  • 第 9–14 行:复位逻辑使用阻塞赋值(=)。在仿真中,阻塞赋值会立即更新左值,但由于复位时所有赋值顺序执行,结果正确。但综合工具会将此视为组合逻辑,可能产生锁存器警告。
  • 第 15–19 行:移位逻辑使用阻塞赋值。关键错误在于:q1 = q0 执行时,q0 已经被 q0 = din 更新为 din 的新值,因此 q1 得到的是 din 而不是 q0 的旧值。同理,q2 和 q3 也会得到相同的 din。仿真波形上,所有输出在同一个时钟沿同时变为 din,完全失去了移位功能。综合后,工具可能会将这些寄存器优化为直接连接,或产生不可预测的逻辑。
  • 第 21 行:模块结束。

时序/CDC/约束

本示例不涉及跨时钟域(CDC),但需注意:时钟和复位信号在 Testbench 中应正确生成,避免时钟抖动或复位不同步。如果上板,需在 XDC 文件中声明时钟周期(例如 create_clock -period 20.000 [get_ports clk])。

验证

Testbench 代码:tb_shift_reg.v

`timescale 1ns / 1ps

module tb_shift_reg;

    reg        clk;
    reg        rst_n;
    reg  [3:0] din;
    wire [3:0] q0_nb, q1_nb, q2_nb, q3_nb;
    wire [3:0] q0_b,  q1_b,  q2_b,  q3_b;

    // 实例化非阻塞版本
    shift_reg_nonblocking u_nb (
        .clk   (clk),
        .rst_n (rst_n),
        .din   (din),
        .q0    (q0_nb),
        .q1    (q1_nb),
        .q2    (q2_nb),
        .q3    (q3_nb)
    );

    // 实例化阻塞版本
    shift_reg_blocking u_b (
        .clk   (clk),
        .rst_n (rst_n),
        .din   (din),
        .q0    (q0_b),
        .q1    (q1_b),
        .q2    (q2_b),
        .q3    (q3_b)
    );

    // 时钟生成:50MHz
    always #10 clk = ~clk;

    initial begin
        // 初始化
        clk   = 0;
        rst_n = 0;
        din   = 4'h0;

        // 复位
        #20 rst_n = 1;

        // 输入数据
        #10 din = 4'hA;
        #20 din = 4'h5;
        #20 din = 4'hF;
        #20 din = 4'h3;

        #40 $finish;
    end

endmodule

逐行说明

  • 第 1 行:时间尺度设置,仿真精度为 1 ps。
  • 第 3 行:Testbench 模块声明,无端口。
  • 第 5–10 行:声明内部信号,包括驱动信号和连线。
  • 第 12–20 行:实例化非阻塞移位寄存器模块,将内部信号连接到端口。
  • 第 22–30 行:实例化阻塞移位寄存器模块。
  • 第 32 行:时钟生成,每 10 ns 翻转一次,周期 20 ns(50 MHz)。
  • 第 34–47 行:初始化过程。先复位 20 ns,然后依次改变输入数据,观察移位效果。最后在 110 ns 结束仿真。

验证与结果

运行仿真后,观察波形:

时间 (ns)dinq0_nbq1_nbq2_nbq3_nbq0_bq1_bq2_bq3_b
0000000000
30 (时钟沿)A0→A0000→A0→A0→A0→A
50 (时钟沿)5A→50→A005555
70 (时钟沿)F5→FA→50→A0FFFF
90 (时钟沿)3F→35→FA→50→A3333

测量条件:Vivado 2024.2 行为仿真,时钟周期 20 ns,无外部干扰。上表为示例值,以实际仿真波形为准。

结论:非阻塞版本正确实现了 4 级移位寄存器,数据每时钟沿移动一级。阻塞版本所有输出在同一时钟沿同时变为输入值,完全错误。

故障排查

  • 现象:仿真波形中所有输出同时变化 → 原因:在时序 always 块中使用了阻塞赋值。 → 检查点:查看代码中赋值符号是否为 =。 → 修复建议:改为非阻塞赋值 <=
  • 现象:综合报告出现 inferred latch → 原因:组合逻辑 always 块中未覆盖所有分支,或使用了非阻塞赋值。 → 检查点:检查 always 块敏感列表和赋值方式。 → 修复建议:确保组合逻辑使用阻塞赋值,并覆盖所有 case 分支。
  • 现象:仿真结果与综合后仿真不一致 → 原因:RTL 仿真中混合赋值方式导致行为差异。 → 检查点:运行 report_methodology 检查赋值规则。 → 修复建议:统一为“时序用非阻塞,组合用阻塞”。
  • 现象:Testbench 中时钟沿与数据变化对齐,导致 setup 违规 → 原因:在 initial 块中使用阻塞赋值驱动时钟,或数据变化时间与时钟沿重合。 → 检查点:查看波形中数据变化是否在时钟沿后。 → 修复建议:使用 always 生成时钟,数据变化在时钟沿后至少 1 ns。
  • 现象:移位寄存器输出出现毛刺 → 原因:组合逻辑输出未寄存。 → 检查点:检查输出是否直接来自组合逻辑。 → 修复建议:在输出端添加一级触发器。
  • 现象:仿真速度极慢 → 原因:在 always 块中使用了大量非阻塞赋值,且敏感列表包含多个边沿。 → 检查点:检查敏感列表是否过于复杂。 → 修复建议:简化敏感列表,或使用 SystemVerilog 的 always_ff / always_comb 提高仿真效率。
  • 现象:综合后 Fmax 低于预期 → 原因:组合逻辑路径过长,或赋值方式导致额外逻辑。 → 检查点:查看综合报告中的时序路径。 → 修复建议:在关键路径上插入流水线寄存器,并确保使用非阻塞赋值。
  • 现象:跨时钟域仿真出现 metastability → 原因:未使用同步器处理跨时钟域信号。 → 检查点:检查 CDC 路径是否使用了双级触发器。 → 修复建议:在跨时钟域信号上使用非阻塞赋值实现同步器。

原理与设计说明

阻塞赋值(=)和非阻塞赋值(<=)的核心区别在于仿真调度机制。

阻塞赋值:在 always 块中,语句按顺序执行。当前语句完成后,才执行下一条。这类似于 C 语言中的赋值。在仿真中,阻塞赋值会立即更新左值,并可能影响后续语句对同一变量的读取。

非阻塞赋值:在 always 块中,所有语句的右值在进入块时被“采样”(读取),然后块结束后统一更新左值。因此,在同一个 always 块中,后续语句读取的是更新前的旧值。

这一机制决定了它们在硬件描述中的用途:

  • 时序逻辑(触发器):必须使用非阻塞赋值。因为触发器在时钟沿采样输入,在时钟沿后输出才变化。非阻塞赋值正好模拟了这一行为:所有触发器在时钟沿同时采样,然后同时更新。
  • 组合逻辑(门电路):必须使用阻塞赋值。因为组合逻辑的输出随输入立即变化,没有时钟沿同步。阻塞赋值的立即更新特性符合这一要求。

关键 trade-off

  • 资源 vs Fmax:正确使用非阻塞赋值不会增加资源,但错误使用(如在组合逻辑中用非阻塞)会导致仿真与综合不一致,降低 Fmax 或产生锁存器。
  • 吞吐 vs 延迟:移位寄存器使用非阻塞赋值,数据每时钟沿移动一级,延迟固定为 N 个时钟周期。如果错误地使用阻塞赋值,数据会“穿透”所有级,延迟变为 0,但失去了流水线功能。
  • 易用性 vs 可移植性:一些初学者为了图方便,在 always 块中混合使用两种赋值。这虽然在某些工具中能通过仿真,但综合结果不可预测,且无法移植到其他工具。严格遵循“时序用非阻塞,组合用阻塞”是保证可移植性的最佳实践。

扩展与下一步

  • 参数化移位寄存器:使用 parameter 定义移位级数和位宽,提高代码复用性。
  • 带宽提升:将移位寄存器改为并行加载模式,实现数据吞吐量的提升。
  • 跨平台验证:将代码移植到 Intel Quartus 或 Lattice Diamond,验证赋值规则在不同工具下的一致性。
  • 加入断言:在 Testbench 中使用 SystemVerilog 断言(SVA)自动检查移位寄存器的行为,例如 assert property (@(posedge clk) q1 == $past(q0));
  • 形式验证:使用形式验证工具(如 OneSpin)证明阻塞赋值版本永远无法实现正确的移位功能。
  • 学习 SystemVerilog 的 always_ff 和 always_comb:这些关键字强制检查赋值方式,能在编译时发现错误,是 2026 年推荐的编码风格。

参考与信息来源

  • IEEE Std 1364-2005, "Verilog Hardware Description Language", Section 9.2 – Blocking and Nonblocking Assignments.
  • Clifford E. Cummings, "Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!", SNUG 2000
标签:
本文原创,作者:FPGA小白,其版权均为FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训所有。
如需转载,请注明出处:https://z.shaonianxue.cn/44103.html
FPGA小白

FPGA小白

初级工程师
成电国芯®的讲师哦,专业FPGA已有10年。
47722.48W7.34W34.40W
分享:
成电国芯FPGA赛事课即将上线
AI+FPGA爆发!2026边缘AI落地潮,FPGA工程师薪资疯涨
AI+FPGA爆发!2026边缘AI落地潮,FPGA工程师薪资疯涨上一篇
2026年UCIe 2.0标准加速落地:FPGA桥接芯片量产案例激增,Chiplet互操作生态迎来关键拐点下一篇
2026年UCIe 2.0标准加速落地:FPGA桥接芯片量产案例激增,Chiplet互操作生态迎来关键拐点
相关文章
总数:1.20K
FPGA与处理器深度对比:架构差异、性能指标与应用场景全解析

FPGA与处理器深度对比:架构差异、性能指标与应用场景全解析

一、架构对比:硬件可编程vs指令驱动graphTB…
技术分享
1年前
0
0
418
0
从零搭建FPGA开发环境:Vivado与开源工具链对比实践指南

从零搭建FPGA开发环境:Vivado与开源工具链对比实践指南

QuickStart(快速上手)步骤一:选择目标平台。若使用Xilin…
技术分享
5天前
0
0
21
0
2026年FPGA就业趋势分析:开源项目与竞赛奖项的权重对比与简历优化实践指南

2026年FPGA就业趋势分析:开源项目与竞赛奖项的权重对比与简历优化实践指南

QuickStart:5分钟看懂企业评估逻辑打开主流招聘平台(如猎聘、…
技术分享
14天前
0
0
34
0
评论表单游客 您好,欢迎参与讨论。
加载中…
评论列表
总数:0
FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
没有相关内容