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) | din | q0_nb | q1_nb | q2_nb | q3_nb | q0_b | q1_b | q2_b | q3_b |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 30 (时钟沿) | A | 0→A | 0 | 0 | 0 | 0→A | 0→A | 0→A | 0→A |
| 50 (时钟沿) | 5 | A→5 | 0→A | 0 | 0 | 5 | 5 | 5 | 5 |
| 70 (时钟沿) | F | 5→F | A→5 | 0→A | 0 | F | F | F | F |
| 90 (时钟沿) | 3 | F→3 | 5→F | A→5 | 0→A | 3 | 3 | 3 | 3 |
测量条件: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





