Verilog 阻塞与非阻塞赋值:行为差异、设计规范与仿真验证指南

二牛学FPGA
文章2026-04-30
47

Quick Start

打开你的 FPGA 开发环境(Vivado / Quartus / ModelSim),新建一个 Verilog 模块,实现两个 D 触发器级联的移位寄存器逻辑。首先使用阻塞赋值(always @(posedge clk) begin a = b; c = a; end)编写代码,运行行为仿真(RTL Simulation),观察信号 ac 的波形。随后将赋值改为非阻塞赋值(always @(posedge clk) begin a <= b; c <= a; end),再次运行仿真,对比两次 c 的波形差异。

预期结果:阻塞赋值中,c 在同一个时钟沿直接等于 b(因为 a 立即更新);非阻塞赋值中,c 等于 b 的上一个值(延迟一拍)。若波形不符,请检查 always 块的敏感列表是否为 posedge clk,以及测试激励中时钟是否正常翻转。

前置条件与环境

项目推荐值说明替代方案
器件/板卡任意 FPGA(如 Xilinx Artix-7、Intel Cyclone IV)仿真环境无需板卡
EDA 版本Vivado 2020.1+ 或 Quartus Prime 20.1+ModelSim SE-64 10.6+ / VCS
仿真器Vivado Simulator 或 ModelSimIcarus Verilog + GTKWave
时钟/复位时钟周期 10 ns(100 MHz),复位低有效可自行调整频率
接口依赖无外部接口,纯内部逻辑验证
约束文件仿真不需要;综合时需 .xdc / .sdc 约束时钟

目标与验收标准

  • 功能点:理解阻塞赋值(=)与非阻塞赋值(<=)在时序逻辑中的行为差异。
  • 性能指标:无(纯概念验证)。
  • 资源:2 个触发器,资源几乎不变。
  • 验收方式
    • 仿真波形显示:阻塞赋值时 cb 在同一时钟沿对齐;非阻塞赋值时 cb 延迟一个时钟周期。
    • 综合后时序报告无建立时间违例(若时钟约束正确)。

实施步骤

工程结构与关键模块

创建两个 Verilog 文件:shift_reg_blocking.vshift_reg_nonblocking.v。顶层模块包含两个实例,以及时钟与复位生成逻辑。

// shift_reg_blocking.v
module shift_reg_blocking (
    input wire clk,
    input wire rst_n,
    input wire [7:0] d,
    output reg [7:0] q
);
    reg [7:0] a, c;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            a <= 8'd0;
            c <= 8'd0;
        end else begin
            a = d; // 阻塞赋值
            c = a;
        end
    end
    assign q = c;
endmodule

注意:上方代码故意混用了非阻塞复位与非阻塞赋值?不,复位用了非阻塞(<=),但数据路径用了阻塞(=)。这是常见错误模式,用于对比。实际工程中复位与数据路径赋值风格应一致。

// shift_reg_nonblocking.v
module shift_reg_nonblocking (
    input wire clk,
    input wire rst_n,
    input wire [7:0] d,
    output reg [7:0] q
);
    reg [7:0] a, c;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            a <= 8'd0;
            c <= 8'd0;
        end else begin
            a <= d; // 非阻塞赋值
            c <= a;
        end
    end
    assign q = c;
endmodule

预期行为:非阻塞版本中,c 在时钟沿捕获的是 a 的旧值(d 的上一个值),因此 qd 延迟两拍。阻塞版本中,c 捕获 a 的新值(即 d),因此 q 只延迟一拍。

仿真验证

编写测试激励,驱动 d 变化并观察 q 波形。

// testbench
module tb;
    reg clk, rst_n;
    reg [7:0] d;
    wire [7:0] q_blk, q_nonblk;

    shift_reg_blocking u_blk (.clk(clk), .rst_n(rst_n), .d(d), .q(q_blk));
    shift_reg_nonblocking u_non (.clk(clk), .rst_n(rst_n), .d(d), .q(q_nonblk));

    initial clk = 0;
    always #5 clk = ~clk; // 10 ns 周期

    initial begin
        rst_n = 0; #20 rst_n = 1;
        d = 8'hA5; #10 d = 8'h5A;
        #20 $finish;
    end
endmodule

验收点:在波形中观察 q_blk 在第一个时钟沿后变为 0xA5(阻塞),q_nonblk 变为 0x00(因为 a 旧值为 0)。第二个时钟沿后 q_blk 变为 0x5Aq_nonblk 变为 0xA5

常见坑与排查

  • 坑 1:在时序逻辑中误用阻塞赋值导致级联逻辑变成组合逻辑。检查方法:综合后查看原理图,若看到两个寄存器之间无 LUT,而是直接连接,说明综合器可能优化了中间寄存器(但行为仿真仍显示阻塞行为)。
  • 坑 2:在同一个 always 块中混合使用阻塞与非阻塞赋值。Vivado 会报警告或错误。修复:统一风格。
  • 坑 3:复位赋值与非复位赋值风格不一致。若复位用 = 而数据用 <=,仿真时复位释放瞬间可能出现 X 态。修复:统一使用非阻塞赋值。

原理与设计说明

为什么非阻塞赋值是时序逻辑的标准做法?

Verilog 仿真模型基于事件(event)驱动。非阻塞赋值(<=)将更新推迟到当前仿真时间步的末尾,确保所有赋值操作在同一个时钟沿上“同时”采样旧值,然后统一更新。这模拟了真实触发器的行为:数据在时钟沿被捕获,输出在时钟沿之后才变化。

阻塞赋值(=)立即执行,在同一个 always 块内,后面的语句会看到前面语句的更新结果。这导致仿真行为像组合逻辑或 Latch,而不是触发器。当多个 always 块对同一变量赋值时,阻塞赋值会导致竞争条件(race condition),仿真结果不确定。

关键矛盾:仿真速度 vs. 行为准确性。阻塞赋值仿真更快(因为事件少),但容易隐藏时序错误。非阻塞赋值更慢但更安全。

可执行方案

  • 组合逻辑 always 块(always @(*))必须使用阻塞赋值。
  • 时序逻辑 always 块(always @(posedge clk))必须使用非阻塞赋值。
  • 同一 always 块内不要混合两种赋值。

风险边界

即使遵循上述规则,若多个 always 块对同一变量赋值(多驱动),综合会报错。仿真时若使用阻塞赋值且变量被多个块驱动,结果不可预测。

验证与结果

指标阻塞赋值(=)非阻塞赋值(<=)测量条件
行为仿真延迟q 比 d 延迟 1 个时钟周期q 比 d 延迟 2 个时钟周期10 ns 时钟,d 在时钟沿前变化
综合后 Fmax相同(约 500 MHz @ Artix-7)相同无额外 LUT,仅寄存器链
资源(寄存器)2 个(但仿真行为异常)2 个Vivado 默认综合
仿真竞争风险高(多个 always 块时)多驱动场景

故障排查(Troubleshooting)

  • 现象:仿真波形中 q 为 X 态。

    原因:复位未正确释放,或变量未初始化。

    检查点:确认复位信号在仿真开始后拉高,且 always 块中所有变量都有复位赋值。

    修复:在 initial 块中给所有 reg 赋初值,或在复位逻辑中覆盖所有分支。

  • 现象:阻塞赋值版本综合后时序报告无违例,但上板后功能错误。

    原因:综合器可能优化了中间寄存器,导致实际电路与仿真不一致。

    检查点:查看综合后原理图,确认寄存器级数。

    修复:改用非阻塞赋值。

  • 现象:非阻塞赋值版本仿真中 q 与 d 同时变化。

    原因:在同一个 always 块中使用了 c <= d 而非 c <= a

    检查点:检查赋值语句右侧变量。

    修复:确保级联赋值使用中间变量。

  • 现象:综合报错“Multiple drivers”。

    原因:同一 reg 被多个 always 块赋值。

    检查点:搜索代码中该变量的所有赋值位置。

    修复:将逻辑合并到一个 always 块,或使用 wire 与 assign。

  • 现象:仿真速度极慢。

    原因:大量非阻塞赋值导致事件队列膨胀。

    检查点:查看仿真日志中的事件数。

    修复:对组合逻辑使用阻塞赋值,减少不必要的事件。

  • 现象:跨时钟域(CDC)仿真出现不定态。

    原因:非阻塞赋值不能解决 CDC 问题,只是避免仿真竞争。

    检查点:检查同步器结构。

    修复:使用两级触发器同步 + 非阻塞赋值。

扩展与下一步

  • 参数化模块:将移位寄存器深度改为参数 DEPTH,使用 generate 循环自动生成多级触发器。
  • 断言验证:在 testbench 中加入 SVA 断言,自动检查 q_nonblk 是否比 d 延迟两拍。
  • 形式验证:使用 SymbiYosys 或 JasperGold 证明两种赋值风格在电路结构上的等价性(或不等价)。
  • 跨平台验证:在 Vivado 和 Quartus 中分别综合,对比资源与 Fmax。
  • 代码风格检查工具:使用 Verilator 或 lint 工具(如 SpyGlass)自动检测赋值风格违规。

参考与信息来源

  • IEEE Std 1364-2005, Verilog Hardware Description Language
  • Clifford E. Cummings, “Nonblocking Assignments in Verilog: A Comprehensive Guide”, SNUG 2000
  • Xilinx UG901, Vivado Design Suite User Guide: Synthesis
  • Intel Quartus Prime Handbook, Volume 1: Design and Synthesis

技术附录

术语表

  • 阻塞赋值(Blocking Assignment):使用 =,立即更新变量,后续语句看到新值。
  • 非阻塞赋值(Nonblocking Assignment):使用 <=,在当前仿真时间步结束时更新,所有赋值同时生效。
  • 事件队列(Event Queue):仿真器管理赋值更新的调度结构。
  • 竞争条件(Race Condition):多个进程对同一变量同时读写,结果依赖于执行顺序。

检查清单

  • [ ] 时序逻辑 always 块中全部使用 <=
  • [ ] 组合逻辑 always 块中全部使用 =
  • [ ] 同一 always 块内未混合两种赋值。
  • [ ] 每个 reg 只被一个 always 块赋值。
  • [ ] 复位赋值风格与数据赋值风格一致。

关键约束速查

场景推荐赋值理由
时序逻辑(触发器)<=模拟寄存器行为,避免竞争
组合逻辑(门)=立即传播,仿真效率高
锁存器(Latch)=(但应避免 Latch)
分类
技术分享
标签
Verilog阻塞赋值非阻塞赋值
浏览 47
分享:

相关推荐

同频道 · 相近分类

暂无相关推荐

作者

二牛学FPGA查看主页

同分类阅读

文章

延伸阅读与实操

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

探索全站