Quick Start
- 准备 Vivado 2023.2 或更高版本,新建一个 RTL 工程,目标器件选 xc7a35ticsg324-1L(Artix-7)。
- 编写两个完全相同的逻辑功能模块:一个全部使用阻塞赋值(=),另一个全部使用非阻塞赋值(<=),仅测试一个简单的 4 位计数器。
- 对两个模块分别运行综合(Synthesis),打开综合后的原理图(Schematic)。
- 对比两个原理图中的寄存器(FDRE)数量、连线结构、组合逻辑深度。
- 对两个模块分别运行实现(Implementation),查看时序报告(Setup Slack 与 Hold Slack)。
- 预期现象:对于同一个计数器逻辑,阻塞赋值版本可能综合出更少的寄存器(因共享/复用),但时序更差;非阻塞赋值版本会综合出标准流水线结构,时序更容易收敛。若两者结构一致,则说明赋值方式对综合结果无影响(仅仿真行为不同)。
前置条件与环境
| 项目 | 推荐值 | 说明 | 替代方案 |
|---|---|---|---|
| 器件/板卡 | xc7a35ticsg324-1L | Artix-7 系列,速度等级 -1L | 任何 7 系列或 UltraScale 器件 |
| EDA 版本 | Vivado 2023.2 | 综合与实现工具 | Vivado 2020.1+ 或 Quartus Prime 20.1+ |
| 仿真器 | Vivado Simulator | 用于验证功能正确性 | ModelSim/Questa、VCS |
| 时钟 | 100 MHz 单端时钟 | 主时钟周期 10 ns | 50–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_blocking 和 counter_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 级流水线。




