Quick Start
- 准备 Vivado 2025.2(或更高版本)与任意 7 系列 / UltraScale+ 开发板(如 Artix-7)。
- 新建工程,选择目标器件,添加两个 Verilog 源文件:
blocking_example.v与nonblocking_example.v。 - 在两个文件中分别实现一个简单的 2 级移位寄存器:一个完全使用阻塞赋值(
=),一个完全使用非阻塞赋值(<=)。 - 编写顶层模块,例化两个移位寄存器,将输入
din同时送入两个模块,观察输出dout_block与dout_nonblock。 - 运行综合(Synthesis),查看综合后的 RTL 原理图或网表,确认两个模块的硬件结构差异。
- 编写仿真 Testbench,驱动时钟与复位,对比仿真波形中两个输出的时序差异(重点观察
posedge clk采样点的值)。 - 上板下载,用逻辑分析仪(ILA)抓取内部信号,验证硬件行为是否与仿真一致。
- 若发现输出不符合预期,优先检查综合选项(
-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实现可配置的移位寄存器,并对比阻塞与非阻塞在参数化下的综合结果。 - <


