---
title: 2026年Q2：Verilog中阻塞与非阻塞赋值在综合后的硬件差异排查
url: "https://www.fpgalearn.com/49513.html"
kind: article
id: 49513
excerpt: Quick Start准备 Vivado 2025.2（或更高版本）与任意 7 系列 / UltraScale+ 开发板（如 Artix-7）。新建工程，选择目…
published_at: 2026-06-07
author: FPGA小白
tags: Verilog, 阻塞赋值, 非阻塞赋值
word_count: 6407
source: "https://www.fpgalearn.com"
---

# 2026年Q2：Verilog中阻塞与非阻塞赋值在综合后的硬件差异排查

> 标签：Verilog、阻塞赋值、非阻塞赋值
> Quick Start准备 Vivado 2025.2（或更高版本）与任意 7 系列 / UltraScale+ 开发板（如 Artix-7）。新建工程，选择目…

> Quick Start准备 Vivado 2025.2（或更高版本）与任意 7 系列 / UltraScale+ 开发板（如 Artix-7）。新建工程，选择目…

## Quick Start
1. 准备 Vivado 2025.2（或更高版本）与任意 7 系列 / UltraScale+ 开发板（如 Artix-7）。
1. 新建工程，选择目标器件，添加两个 Verilog 源文件：`blocking_example.v` 与 `nonblocking_example.v`。
1. 在两个文件中分别实现一个简单的 2 级移位寄存器：一个完全使用阻塞赋值（`=`），一个完全使用非阻塞赋值（`<=`）。
1. 编写顶层模块，例化两个移位寄存器，将输入 `din` 同时送入两个模块，观察输出 `dout_block` 与 `dout_nonblock`。
1. 运行综合（Synthesis），查看综合后的 RTL 原理图或网表，确认两个模块的硬件结构差异。
1. 编写仿真 Testbench，驱动时钟与复位，对比仿真波形中两个输出的时序差异（重点观察 `posedge clk` 采样点的值）。
1. 上板下载，用逻辑分析仪（ILA）抓取内部信号，验证硬件行为是否与仿真一致。
1. 若发现输出不符合预期，优先检查综合选项（`-keep_equivalent_registers`）与约束文件中的时钟定义。
## 前置条件与环境
| 项目 | 推荐值 | 说明 | 替代方案 |
| --- | --- | --- | --- |
| 器件/板卡 | Xilinx Artix-7 XC7A35T | 主流入门级 FPGA，资源充足 | Intel Cyclone V / Lattice ECP5 |
| EDA 版本 | Vivado 2025.2 | 支持最新综合优化与 CDC 检查 | Vivado 2024.x / Quartus Prime 24.x |
| 仿真器 | Vivado Simulator 或 ModelSim SE-64 2025.1 | 支持 VCD 波形与 Tcl 自动化 | Questa / Verilator（仅仿真） |
| 时钟/复位 | 50 MHz 单端时钟，异步高有效复位 | 避免跨时钟域干扰 | 100 MHz / 差分时钟 |
| 接口依赖 | 无外部接口，仅使用板载 LED 或 ILA | 纯内部逻辑验证 | UART / GPIO 输出 |
| 约束文件 | XDC 文件定义时钟周期 20 ns，输入输出延迟 | 保证时序分析准确 | SDC（Quartus） |
| 辅助工具 | Vivado Tcl Console / Python 脚本 | 批量运行综合与仿真 | Makefile / 批处理文件 |
## 目标与验收标准
- **功能点**：两个移位寄存器在综合后应产生相同的逻辑功能（串行移位），但硬件结构（寄存器间连接方式）不同。
- **性能指标**：非阻塞版本 Fmax 不低于 200 MHz（示例值，以实际器件为准）；阻塞版本可能因组合环路导致 Fmax 下降或综合警告。
- **资源占用**：两个版本消耗的寄存器数量相同（每个移位寄存器 2 个 FF），但 LUT 或组合逻辑数量可能不同。
- **关键波形**：仿真波形中，`dout_nonblock` 在时钟上升沿后稳定变化；`dout_block` 可能在同一个时钟沿内出现多次跳变（毛刺）。
- **验收方式**：综合后网表对比、仿真波形截图、ILA 捕获的硬件波形。
## 实施步骤
### 1. 工程结构与代码编写
- 创建 Vivado 工程，添加两个 Verilog 源文件：`shift_block.v` 与 `shift_nonblock.v`。
- 编写顶层模块 `top_shift.v`，例化两个子模块，共用时钟、复位与输入数据。
- 使用 `\`define` 宏或参数控制是否启用阻塞/非阻塞版本，便于快速切换对比。
### 2. 关键模块实现：阻塞赋值移位寄存器
```
module shift_block (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       din,
    output reg        dout
);
reg r1, r2;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        r1  = 1'b0;
        r2  = 1'b0;
        dout = 1'b0;
    end else begin
        r1  = din;
        r2  = r1;
        dout = r2;
    end
end
endmodule
```
### 逐行说明（shift_block.v）
- **第 1–7 行**：模块端口声明。clk 与 rst_n 为全局时钟和异步复位（低有效），din 为串行输入，dout 为串行输出。所有输出定义为 reg 类型，因为需要在 always 块内赋值。
- **第 8 行**：声明两个内部寄存器 r1、r2。注意这里没有指定初始值，综合后默认初始值为 X（不确定）。
- **第 9 行**：敏感列表为时钟上升沿或复位下降沿，这是标准的时序逻辑写法。
- **第 10–14 行**：复位逻辑，将所有寄存器清零。注意阻塞赋值在此处没有问题，因为复位时所有赋值顺序执行，最终结果正确。
- **第 15–18 行**：核心移位逻辑。由于使用阻塞赋值，`r1 = din` 立即更新，接着 `r2 = r1` 读取的是更新后的 r1（即 din），最后 `dout = r2` 读取的是更新后的 r2（即 din）。**关键后果**：综合工具会将 r1、r2、dout 优化成一个寄存器链，但组合逻辑上存在“读后写”依赖，可能插入额外组合路径，导致时序变差。
- **第 19 行**：endmodule。
### 3. 关键模块实现：非阻塞赋值移位寄存器
```
module shift_nonblock (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       din,
    output reg        dout
);
reg r1, r2;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        r1  <= 1'b0;
        r2  <= 1'b0;
        dout <= 1'b0;
    end else begin
        r1  <= din;
        r2  <= r1;
        dout <= r2;
    end
end
endmodule
```
### 逐行说明（shift_nonblock.v）
- **第 1–7 行**：端口声明与 shift_block 完全相同，便于对比。
- **第 8 行**：同样声明内部寄存器 r1、r2。
- **第 9 行**：敏感列表相同。
- **第 10–14 行**：复位逻辑使用非阻塞赋值。在仿真中，所有赋值在 always 块结束时同时生效，复位时所有寄存器被清零。
- **第 15–18 行**：核心移位逻辑。非阻塞赋值在 always 块开始时采样右侧值（`r1` 采样的是上一个时钟周期的值，`r2` 采样的是上一个时钟周期的 `r1`），然后在块结束时统一更新。**关键后果**：综合工具会直接推断出 3 级流水线寄存器，每个寄存器之间只有布线延迟，无组合逻辑插入，时序最优。
- **第 19 行**：endmodule。
### 4. 顶层模块与 Testbench
```
module top_shift (
    input  wire clk,
    input  wire rst_n,
    input  wire din,
    output wire dout_block,
    output wire dout_nonblock
);

shift_block u_block (
    .clk    (clk),
    .rst_n  (rst_n),
    .din    (din),
    .dout   (dout_block)
);

shift_nonblock u_nonblock (
    .clk    (clk),
    .rst_n  (rst_n),
    .din    (din),
    .dout   (dout_nonblock)
);

endmodule
```
### 逐行说明（top_shift.v）
- **第 1–6 行**：顶层模块端口，包含时钟、复位、输入与两个输出。
- **第 8–13 行**：例化 shift_block，输出连接到 dout_block。
- **第 15–20 行**：例化 shift_nonblock，输出连接到 dout_nonblock。
- **第 22 行**：endmodule。
### 5. 综合与网表对比
- 运行综合（`synth_design`），打开综合后的原理图（Schematic）。
- 观察 `u_block` 内部：可能看到 `r1` 与 `r2` 之间插入了组合逻辑（如 LUT1），用于实现“读后写”的依赖。
- 观察 `u_nonblock` 内部：三个寄存器直接级联，无组合逻辑。
- 使用 `report_timing_summary` 对比两个路径的时序裕量。阻塞版本路径延迟通常更大。
- **常见坑**：如果综合工具优化了阻塞版本中的冗余寄存器（例如将 r2 与 dout 合并），可以设置 `-keep_equivalent_registers` 来保留结构以便观察。
### 6. 仿真验证
```
// Testbench 片段
initial begin
    clk = 0;
    forever #10 clk = ~clk;  // 50 MHz
end

initial begin
    rst_n = 0;
    din = 0;
    #25 rst_n = 1;
    #10 din = 1;
    #20 din = 0;
    #20 din = 1;
    #40 $finish;
end
```
### 逐行说明（Testbench 片段）
- **第 1–4 行**：生成 50 MHz 时钟，周期 20 ns。
- **第 6–12 行**：复位 25 ns 后释放，然后输入数据序列：先拉高一个时钟周期，再拉低，再拉高。
- **预期结果**：`dout_nonblock` 在 din 变化后经过 3 个时钟周期输出正确值；`dout_block` 可能提前一个周期输出（因为阻塞赋值导致数据穿透）。
### 7. 上板验证（ILA）
- 添加 ILA IP 核，探测 `clk`、`din`、`dout_block`、`dout_nonblock` 以及内部寄存器 `u_block/r1`、`u_nonblock/r1`。
- 设置触发条件为 `din` 上升沿，捕获深度 1024。
- 下载比特流，运行 ILA，观察波形。确认硬件行为与仿真一致。
- **常见坑**：如果 ILA 采样时钟与逻辑时钟不同，可能导致采样错误。确保 ILA 使用与设计相同的时钟。
## 原理与设计说明
**为什么阻塞赋值在时序逻辑中会导致硬件差异？**
阻塞赋值（`=`）在 Verilog 中是一种“立即更新”的赋值方式。在一个 always 块内，语句按书写顺序执行，前一条赋值的结果会立即影响后续语句。对于组合逻辑（如 `always @(*)`），这符合硬件行为——组合逻辑的传播是连续的。但对于时序逻辑（`always @(posedge clk)`），综合工具必须推断出寄存器（FF）。当在同一个 always 块内使用阻塞赋值时，工具会检测到“读后写”依赖，并可能插入额外的组合逻辑来满足这种依赖，而不是直接使用寄存器。
具体到移位寄存器例子：阻塞版本中，`r2 = r1` 读取的是已经被 `r1 = din` 更新过的值，因此 `r2` 实际上直接获得了 `din`，而不是上一个时钟周期的 `r1`。综合工具为了保持这种“在同一时钟沿内传递数据”的行为，会将 `r1` 和 `r2` 合并，或者插入一个组合路径绕过 `r1`。结果就是硬件结构不再是纯粹的流水线，而可能是一个带有组合反馈的寄存器链。
**非阻塞赋值（<=）** 则不同：它采用“采样-更新”两阶段机制。在 always 块开始时，所有右侧表达式被采样（读取当前值），然后块结束时统一更新左侧变量。这意味着 `r2 <= r1` 采样的是上一个时钟周期的 `r1`，而不是当前周期更新后的值。综合工具可以安全地将每个被赋值的变量映射到一个独立的寄存器，形成标准的流水线结构。
**关键 trade-off**：
- **资源 vs Fmax**：非阻塞版本资源利用率更高（每个赋值对应一个寄存器），但 Fmax 更高（无组合逻辑插入）。阻塞版本可能节省寄存器（通过合并），但 Fmax 降低。
- **吞吐 vs 延迟**：非阻塞版本延迟固定（3 个时钟周期），吞吐稳定。阻塞版本延迟可能更短（2 个周期），但存在组合路径风险。
- **易用性 vs 可移植性**：在时序逻辑中坚持使用非阻塞赋值是业界最佳实践，代码易读且综合结果可预测。阻塞赋值在时序逻辑中容易引入 bug，且不同综合工具的处理方式可能不同。
## 验证与结果
| 指标 | 阻塞版本（shift_block） | 非阻塞版本（shift_nonblock） | 测量条件 |
| --- | --- | --- | --- |
| 寄存器数量 | 2（可能合并为 2） | 3 | Vivado 2025.2 综合报告 |
| LUT 数量 | 1（用于组合旁路） | 0 | 同上 |
| Fmax（示例） | 180 MHz | 250 MHz | Artix-7 -1 speed grade，50 MHz 时钟输入 |
| 延迟（时钟周期数） | 2 | 3 | 仿真波形测量 |
| 输出毛刺 | 有（同一时钟沿内跳变） | 无 | 仿真波形 / ILA 捕获 |
说明：以上数值为示例，以实际综合与仿真结果为准。Fmax 受器件、约束、温度影响，建议读者在自己的工程中复现验证。
## 故障排查（Troubleshooting）
- **现象**：仿真中阻塞版本输出与预期不符（数据提前一个周期）。
**原因**：阻塞赋值导致数据在同一时钟沿穿透。
**检查点**：查看仿真波形中 `r1`、`r2` 的变化时刻。
**修复建议**：将时序逻辑中的阻塞赋值改为非阻塞赋值。
- **现象**：综合后阻塞版本报告时序违规（Setup Violation）。
**原因**：组合逻辑插入导致路径延迟过大。
**检查点**：运行 `report_timing`，查看最差路径是否经过阻塞版本。
**修复建议**：改用非阻塞赋值，或手动插入流水线寄存器。
- **现象**：综合后阻塞版本与非阻塞版本资源占用相同。
**原因**：综合工具优化了冗余逻辑，合并了寄存器。
**检查点**：查看综合报告中的“Register Merging”或“Equivalent Register Removal”。
**修复建议**：使用 `-keep_equivalent_registers` 选项保留结构，或添加 `/* synthesis keep */` 属性。
- **现象**：上板后 ILA 捕获的波形与仿真不一致。
**原因**：时钟抖动、复位同步问题或 ILA 采样触发设置错误。
**检查点**：确认 ILA 时钟与设计时钟同源；检查复位释放是否满足时序。
**修复建议**：使用同步复位或增加复位同步器。
- **现象**：仿真中非阻塞版本输出为 X（未知）。
**原因**：寄存器未初始化，且复位未正确生效。
**检查点**：检查复位信号是否在仿真开始时为低电平。
**修复建议**：在 Testbench 中确保复位持续足够长的时间（至少一个时钟周期）。
- **现象**：综合警告“Inferring latch for signal xxx”。
**原因**：在 always 块中某些分支未覆盖所有赋值，导致综合出锁存器。
**检查点**：检查 always 块是否在所有分支（包括 else）中都对每个 reg 赋值。
**修复建议**：补全赋值，或使用默认值。
- **现象**：两个版本在综合后原理图中完全相同。
**原因**：综合工具优化过于激进，或者代码中阻塞赋值被工具自动纠正。
**检查点**：查看综合日志中是否有“Replacing blocking with non-blocking”或类似信息。
**修复建议**：检查综合选项，或使用更严格的编码风格（如 SystemVerilog 的 `always_ff`）。
- **现象**：使用 Quartus 综合时，阻塞版本与非阻塞版本结果相同。
**原因**：Quartus 可能对阻塞赋值有特殊处理，或默认优化策略不同。
**检查点**：查看 Quartus 的“Analysis & Synthesis”报告。
**修复建议**：以 Vivado 为主要参考，或查阅具体工具文档。
## 扩展与下一步
- <
- **参数化移位寄存器**：使用 `parameter WIDTH` 和 `parameter DEPTH` 实现可配置的移位寄存器，并对比阻塞与非阻塞在参数化下的综合结果。
- <
