Verilog中阻塞与非阻塞赋值综合结果深度对比

二牛学FPGA
文章2026-05-11
37

Quick Start

  • 准备 Vivado 2023.2 或更高版本,新建一个 RTL 工程,目标器件选 xc7a35ticsg324-1L(Artix-7)。
  • 编写两个完全相同的逻辑功能模块:一个全部使用阻塞赋值(=),另一个全部使用非阻塞赋值(<=),仅测试一个简单的 4 位计数器。
  • 对两个模块分别运行综合(Synthesis),打开综合后的原理图(Schematic)。
  • 对比两个原理图中的寄存器(FDRE)数量、连线结构、组合逻辑深度。
  • 对两个模块分别运行实现(Implementation),查看时序报告(Setup Slack 与 Hold Slack)。
  • 预期现象:对于同一个计数器逻辑,阻塞赋值版本可能综合出更少的寄存器(因共享/复用),但时序更差;非阻塞赋值版本会综合出标准流水线结构,时序更容易收敛。若两者结构一致,则说明赋值方式对综合结果无影响(仅仿真行为不同)。

前置条件与环境

项目推荐值说明替代方案
器件/板卡xc7a35ticsg324-1LArtix-7 系列,速度等级 -1L任何 7 系列或 UltraScale 器件
EDA 版本Vivado 2023.2综合与实现工具Vivado 2020.1+ 或 Quartus Prime 20.1+
仿真器Vivado Simulator用于验证功能正确性ModelSim/Questa、VCS
时钟100 MHz 单端时钟主时钟周期 10 ns50–200 MHz 均可
复位同步高有效复位避免异步复位的 CDC 问题异步复位也可,但需约束
接口依赖无外部接口纯 RTL 内部逻辑测试可添加 I/O 观察
约束文件XDC 中定义 create_clock周期 10 ns,波形 {0 5}使用默认时序约束也可

目标与验收标准

  • 功能点:两个模块在仿真中实现完全相同的计数功能(从 0 到 15 循环)。
  • 性能指标:非阻塞赋值版本 Setup Slack ≥ 0.5 ns(在 100 MHz 下),阻塞赋值版本 Setup Slack 可能为负或紧裕量。
  • 资源指标:记录两个版本使用的 FDRE 数量、LUT 数量、CARRY4 数量,差异应 ≤ 10%(若逻辑等价)。
  • 验收方式:运行综合后查看 Report Utilization 与 Report Timing Summary,截图保存对比。

实施步骤

工程结构与 RTL 编写

创建两个顶层模块:counter_blockingcounter_nonblocking。每个模块包含一个 4 位计数器,时钟上升沿触发,同步复位。以下给出两个版本的完整 RTL。

// counter_blocking.v - 全部使用阻塞赋值
module counter_blocking (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [3:0] cnt
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt = 4'd0;
        end else begin
            cnt = cnt + 1'b1;
        end
    end
endmodule

逐行说明

  • 第 1 行:模块声明,端口列表包含时钟、复位和输出计数。
  • 第 2 行:output reg [3:0] cnt 声明 cnt 为 reg 类型,因为要在 always 块中被赋值。
  • 第 3 行:always 块敏感列表为时钟上升沿或复位下降沿(异步复位风格)。
  • 第 4–6 行:复位逻辑,当 rst_n 为低时,cnt 被赋值为 0。注意这里使用阻塞赋值 =
  • 第 7–8 行:正常计数逻辑,cnt = cnt + 1,同样使用阻塞赋值。综合工具会推断出 cnt 是一个寄存器(FDRE),因为它在时钟沿被赋值。
// counter_nonblocking.v - 全部使用非阻塞赋值
module counter_nonblocking (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [3:0] cnt
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt <= 4'd0;
        end else begin
            cnt <= cnt + 1'b1;
        end
    end
endmodule

逐行说明

  • 第 1–2 行:模块声明与端口定义,与阻塞版本完全一致。
  • 第 3 行:output reg [3:0] cnt 声明相同。
  • 第 4 行:always 块敏感列表相同。
  • 第 5–7 行:复位逻辑,使用非阻塞赋值 <=
  • 第 8–9 行:计数逻辑,使用非阻塞赋值。综合工具同样推断出 cnt 为寄存器,但仿真行为不同:非阻塞赋值在时钟沿同时更新,避免竞争。

综合与实现

  • 在 Vivado 中分别将两个模块设为顶层(Top Module),运行 Synthesis。
  • 打开综合后的 Schematic,观察寄存器结构:阻塞版本可能将 cnt 推断为单个 4 位寄存器,非阻塞版本同样如此——对于简单计数器,两者综合结果完全相同。
  • 运行 Implementation,查看时序报告。由于逻辑完全相同,Setup Slack 应一致(例如 8.5 ns 左右)。

常见坑与排查

  • 坑 1:误以为阻塞赋值一定综合出组合逻辑。实际上,只要赋值在时钟沿敏感 always 块内,综合工具都会推断寄存器。检查方式:查看综合后的原理图,确认 cnt 是否连接到 FDRE 的 Q 输出。
  • 坑 2:在同一个 always 块内混用阻塞和非阻塞赋值导致仿真与综合不一致。排查:使用 lint 工具(如 Vivado 的 Report HDL Linting)检查赋值风格一致性。

复杂场景:多级逻辑与流水线

当逻辑包含多个赋值语句时,阻塞与非阻塞的综合结果会出现本质差异。以下是一个两级移位寄存器的例子。

// shift_blocking.v - 阻塞赋值实现移位寄存器
module shift_blocking (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       din,
    output reg        dout
);
    reg d1, d2;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            d1 = 1'b0;
            d2 = 1'b0;
        end else begin
            d1 = din;
            d2 = d1;   // 阻塞赋值:d2 立即得到 din 的当前值,不是上一拍的值
        end
    end
    assign dout = d2;
endmodule

逐行说明

  • 第 1–5 行:模块声明,输入 din,输出 dout。
  • 第 6 行:声明两个内部 reg 变量 d1 和 d2。
  • 第 7 行:always 块敏感列表。
  • 第 8–11 行:复位逻辑,两个寄存器清零。
  • 第 12 行:d1 = din;阻塞赋值,d1 立即更新。
  • 第 13 行:d2 = d1;此时 d1 已经是 din 的当前值,所以 d2 也等于 din 的当前值。综合工具会优化掉 d1 寄存器,只保留一个寄存器(d2),因为 d1 的值没有被独立存储。
  • 第 14 行:assign dout = d2;输出。
// shift_nonblocking.v - 非阻塞赋值实现移位寄存器
module shift_nonblocking (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       din,
    output reg        dout
);
    reg d1, d2;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            d1 <= 1'b0;
            d2 <= 1'b0;
        end else begin
            d1 <= din;
            d2 <= d1;   // 非阻塞赋值:d2 得到的是 d1 上一拍的值
        end
    end
    assign dout = d2;
endmodule

逐行说明

  • 第 1–5 行:模块声明相同。
  • 第 6 行:声明 d1、d2。
  • 第 7–11 行:复位逻辑,使用非阻塞赋值。
  • 第 12 行:d1 <= din;非阻塞赋值,d1 在时钟沿更新为 din 的当前值。
  • 第 13 行:d2 <= d1;非阻塞赋值,d2 得到的是 d1 在时钟沿之前的值(即上一拍的 d1)。综合工具会推断出两个独立的寄存器:d1 和 d2,形成真正的 2 级流水线。
  • 第 14 行:输出赋值。

综合结果对比

  • 阻塞版本:综合后原理图只显示一个 FDRE(d2),d1 被优化掉。资源:1 FDRE,0 LUT。
  • 非阻塞版本:综合后原理图显示两个 FDRE(d1 和 d2)串联。资源:2 FDRE,0 LUT。
  • 功能差异:阻塞版本 dout 在时钟沿后立即输出 din(零延迟),非阻塞版本 dout 延迟两个时钟周期输出 din。

时序差异

  • 阻塞版本:由于 d1 被优化,组合逻辑路径为 din → d2(Q),路径短,时序容易满足。
  • 非阻塞版本:路径为 din → d1 (D) → d1 (Q) → d2 (D),两级寄存器,但每级之间组合逻辑少,时序同样容易满足。对于更复杂的逻辑(如多级加法),阻塞赋值可能导致长组合链,时序恶化。

原理与设计说明

阻塞赋值与非阻塞赋值的核心区别在于仿真执行顺序综合推断的寄存器数量。

  • 阻塞赋值(=):在 always 块内顺序执行,后面的语句可以看到前面语句的更新结果。综合时,如果多个阻塞赋值在同一个时钟沿块内且存在数据依赖,综合工具会尝试优化掉中间寄存器,只保留最终寄存器。这可能导致流水线级数减少,组合逻辑深度增加。
  • 非阻塞赋值(<=):在 always 块内并行执行,所有赋值在时钟沿同时更新,后面的语句看到的是更新前的值。综合时,每个被赋值的变量都会独立推断为一个寄存器,形成标准的流水线结构。这保证了仿真与综合行为的一致性,是 RTL 设计的推荐风格。

关键 Trade-off

  • 资源 vs Fmax:非阻塞赋值通常消耗更多寄存器(资源增加),但组合逻辑深度更浅,Fmax 更高。阻塞赋值可能节省寄存器,但组合逻辑链变长,Fmax 降低。
  • 吞吐 vs 延迟:非阻塞赋值保持流水线级数,吞吐量高(每时钟一个结果),但延迟增加。阻塞赋值可能减少延迟(如移位寄存器例子中延迟为 1 拍而非 2 拍),但吞吐量不变。
  • 易用性 vs 可移植性:非阻塞赋值是业界标准,易于团队协作和 IP 复用。阻塞赋值在某些特定场景(如组合逻辑建模)有用,但容易引入仿真-综合不匹配,降低可移植性。

验证与结果

指标阻塞赋值(简单计数器)非阻塞赋值(简单计数器)测量条件
FDRE 数量44Vivado 2023.2, xc7a35t
LUT 数量00无组合逻辑
Setup Slack8.5 ns8.5 ns100 MHz,10 ns 周期
Hold Slack0.2 ns0.2 ns默认设置
Fmax(估计)> 400 MHz> 400 MHz基于 Setup Slack 反推

对于两级移位寄存器:

指标阻塞赋值(移位)非阻塞赋值(移位)测量条件
FDRE 数量12Vivado 2023.2
LUT 数量00无组合逻辑
Setup Slack9.0 ns8.8 ns100 MHz
功能延迟1 时钟周期2 时钟周期仿真波形测量

注意:以上数值为示例,实际结果以具体工程和器件速度等级为准。建议读者在自己的环境中复现对比。

故障排查(Troubleshooting)

  • 现象 1:仿真结果正确,但上板后功能错误。原因:阻塞赋值导致综合后逻辑与仿真不一致。检查点:对比仿真波形与 ChipScope 捕获的波形。修复:将 always 块内的赋值改为非阻塞。
  • 现象 2:综合报告显示寄存器数量比预期少。原因:阻塞赋值导致寄存器被优化。检查点:查看综合原理图,确认被优化信号。修复:如果确实需要流水线,改用非阻塞赋值。
  • 现象 3:时序收敛困难,Setup Slack 为负。原因:组合逻辑链过长,可能由阻塞赋值引起。检查点:查看 Timing Report 中的最差路径,确认是否包含多个阻塞赋值语句。修复:在长路径中插入寄存器(流水线),使用非阻塞赋值。
  • 现象 4:仿真中出现 X(未知态)。原因:多个驱动源竞争赋值。检查点:检查是否在多个 always 块中对同一变量赋值。修复:确保每个 reg 只在一个 always 块中被赋值。
  • 现象 5:综合警告“Inferring latch”。原因:在组合逻辑 always 块中使用了阻塞赋值但缺少 else 分支。检查点:查看警告详情,定位到未完整赋值的信号。修复:补全所有分支,或使用默认值。
  • 现象 6:仿真速度极慢。原因:阻塞赋值在大型设计中导致大量事件触发。检查点:使用仿真器的性能分析功能。修复:尽量使用非阻塞赋值,减少仿真事件。
  • 现象 7:跨时钟域(CDC)信号出现亚稳态。原因:在 CDC 路径中使用了阻塞赋值。检查点:检查 CDC 同步器的 RTL 是否使用非阻塞赋值。修复:CDC 同步器必须使用非阻塞赋值。
  • 现象 8:综合后功能与 RTL 不符。原因:阻塞赋值在复杂逻辑中导致综合工具优化错误。检查点:使用仿真对比综合后网表功能。修复:重构 RTL,统一使用非阻塞赋值。

扩展与下一步

  • 参数化:将移位寄存器深度参数化,测试不同深度下阻塞与非阻塞的资源差异。
  • 带宽提升:在数据通路中使用非阻塞赋值实现流水线,提高吞吐量。
  • 跨平台:在 Quartus Prime 中重复本实验,对比综合结果是否一致。
  • 加入断言:在仿真中添加 SVA 断言,自动检查赋值风格是否一致。
  • 覆盖分析:使用仿真覆盖率工具,确认阻塞与非阻塞版本的功能覆盖是否等价。
  • 形式验证:使用形式验证工具(如 OneSpin)证明两个版本在功能上等价(或不等价)。

参考与信息来源

  • IEEE Std 1364-2005, Verilog Hardware Description Language.
  • Clifford E. Cummings, “Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!”, SNUG 2000.
  • Xilinx UG901, Vivado Design Suite User Guide: Synthesis.
  • Xilinx UG949, Vivado Design Suite User Guide: Methodology.
  • Altera (Intel) Quartus Prime Handbook, Volume 1: Design and Synthesis.

技术附录

术语表

  • 阻塞赋值(Blocking Assignment):使用 = 符号,顺序执行,立即更新。
  • 非阻塞赋值(Nonblocking Assignment):使用 <= 符号,并行执行,时钟沿更新。
  • FDRE:Xilinx 原语,带时钟使能和同步复位的 D 触发器。
  • Setup Slack:建立时间裕量,数据必须在时钟沿前稳定的时间余量。
  • Hold Slack:保持时间裕量,数据在时钟沿后需稳定的时间余量。

检查清单

  • 时钟沿敏感的 always 块内全部使用非阻塞赋值。
  • 组合逻辑 always 块内全部使用阻塞赋值。
  • 同一 always 块内不混用阻塞和非阻塞赋值。
  • 每个 reg 只在一个 always 块中被赋值。
  • CDC 同步器使用非阻塞赋值。
  • </ul
分类
技术分享
标签
Verilog阻塞赋值非阻塞赋值
浏览 37
分享:

相关推荐

同频道 · 相近分类

暂无相关推荐

作者

二牛学FPGA查看主页

同分类阅读

文章

延伸阅读与实操

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

探索全站