Quick Start
打开你的 FPGA 开发环境(Vivado / Quartus / ModelSim),新建一个 Verilog 模块,实现两个 D 触发器级联的移位寄存器逻辑。首先使用阻塞赋值(always @(posedge clk) begin a = b; c = a; end)编写代码,运行行为仿真(RTL Simulation),观察信号 a 和 c 的波形。随后将赋值改为非阻塞赋值(always @(posedge clk) begin a <= b; c <= a; end),再次运行仿真,对比两次 c 的波形差异。
预期结果:阻塞赋值中,c 在同一个时钟沿直接等于 b(因为 a 立即更新);非阻塞赋值中,c 等于 b 的上一个值(延迟一拍)。若波形不符,请检查 always 块的敏感列表是否为 posedge clk,以及测试激励中时钟是否正常翻转。
前置条件与环境
| 项目 | 推荐值 | 说明 | 替代方案 |
|---|---|---|---|
| 器件/板卡 | 任意 FPGA(如 Xilinx Artix-7、Intel Cyclone IV) | 仿真环境无需板卡 | — |
| EDA 版本 | Vivado 2020.1+ 或 Quartus Prime 20.1+ | — | ModelSim SE-64 10.6+ / VCS |
| 仿真器 | Vivado Simulator 或 ModelSim | — | Icarus Verilog + GTKWave |
| 时钟/复位 | 时钟周期 10 ns(100 MHz),复位低有效 | — | 可自行调整频率 |
| 接口依赖 | 无外部接口,纯内部逻辑验证 | — | — |
| 约束文件 | 仿真不需要;综合时需 .xdc / .sdc 约束时钟 | — | — |
目标与验收标准
- 功能点:理解阻塞赋值(
=)与非阻塞赋值(<=)在时序逻辑中的行为差异。 - 性能指标:无(纯概念验证)。
- 资源:2 个触发器,资源几乎不变。
- 验收方式:
实施步骤
工程结构与关键模块
创建两个 Verilog 文件:shift_reg_blocking.v 和 shift_reg_nonblocking.v。顶层模块包含两个实例,以及时钟与复位生成逻辑。
// shift_reg_blocking.v
module shift_reg_blocking (
input wire clk,
input wire rst_n,
input wire [7:0] d,
output reg [7:0] q
);
reg [7:0] a, c;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
a <= 8'd0;
c <= 8'd0;
end else begin
a = d; // 阻塞赋值
c = a;
end
end
assign q = c;
endmodule注意:上方代码故意混用了非阻塞复位与非阻塞赋值?不,复位用了非阻塞(<=),但数据路径用了阻塞(=)。这是常见错误模式,用于对比。实际工程中复位与数据路径赋值风格应一致。
// shift_reg_nonblocking.v
module shift_reg_nonblocking (
input wire clk,
input wire rst_n,
input wire [7:0] d,
output reg [7:0] q
);
reg [7:0] a, c;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
a <= 8'd0;
c <= 8'd0;
end else begin
a <= d; // 非阻塞赋值
c <= a;
end
end
assign q = c;
endmodule预期行为:非阻塞版本中,c 在时钟沿捕获的是 a 的旧值(d 的上一个值),因此 q 比 d 延迟两拍。阻塞版本中,c 捕获 a 的新值(即 d),因此 q 只延迟一拍。
仿真验证
编写测试激励,驱动 d 变化并观察 q 波形。
// testbench
module tb;
reg clk, rst_n;
reg [7:0] d;
wire [7:0] q_blk, q_nonblk;
shift_reg_blocking u_blk (.clk(clk), .rst_n(rst_n), .d(d), .q(q_blk));
shift_reg_nonblocking u_non (.clk(clk), .rst_n(rst_n), .d(d), .q(q_nonblk));
initial clk = 0;
always #5 clk = ~clk; // 10 ns 周期
initial begin
rst_n = 0; #20 rst_n = 1;
d = 8'hA5; #10 d = 8'h5A;
#20 $finish;
end
endmodule验收点:在波形中观察 q_blk 在第一个时钟沿后变为 0xA5(阻塞),q_nonblk 变为 0x00(因为 a 旧值为 0)。第二个时钟沿后 q_blk 变为 0x5A,q_nonblk 变为 0xA5。
常见坑与排查
- 坑 1:在时序逻辑中误用阻塞赋值导致级联逻辑变成组合逻辑。检查方法:综合后查看原理图,若看到两个寄存器之间无 LUT,而是直接连接,说明综合器可能优化了中间寄存器(但行为仿真仍显示阻塞行为)。
- 坑 2:在同一个
always块中混合使用阻塞与非阻塞赋值。Vivado 会报警告或错误。修复:统一风格。 - 坑 3:复位赋值与非复位赋值风格不一致。若复位用
=而数据用<=,仿真时复位释放瞬间可能出现 X 态。修复:统一使用非阻塞赋值。
原理与设计说明
为什么非阻塞赋值是时序逻辑的标准做法?
Verilog 仿真模型基于事件(event)驱动。非阻塞赋值(<=)将更新推迟到当前仿真时间步的末尾,确保所有赋值操作在同一个时钟沿上“同时”采样旧值,然后统一更新。这模拟了真实触发器的行为:数据在时钟沿被捕获,输出在时钟沿之后才变化。
阻塞赋值(=)立即执行,在同一个 always 块内,后面的语句会看到前面语句的更新结果。这导致仿真行为像组合逻辑或 Latch,而不是触发器。当多个 always 块对同一变量赋值时,阻塞赋值会导致竞争条件(race condition),仿真结果不确定。
关键矛盾:仿真速度 vs. 行为准确性。阻塞赋值仿真更快(因为事件少),但容易隐藏时序错误。非阻塞赋值更慢但更安全。
可执行方案
- 组合逻辑
always块(always @(*))必须使用阻塞赋值。 - 时序逻辑
always块(always @(posedge clk))必须使用非阻塞赋值。 - 同一
always块内不要混合两种赋值。
风险边界
即使遵循上述规则,若多个 always 块对同一变量赋值(多驱动),综合会报错。仿真时若使用阻塞赋值且变量被多个块驱动,结果不可预测。
验证与结果
| 指标 | 阻塞赋值(=) | 非阻塞赋值(<=) | 测量条件 |
|---|---|---|---|
| 行为仿真延迟 | q 比 d 延迟 1 个时钟周期 | q 比 d 延迟 2 个时钟周期 | 10 ns 时钟,d 在时钟沿前变化 |
| 综合后 Fmax | 相同(约 500 MHz @ Artix-7) | 相同 | 无额外 LUT,仅寄存器链 |
| 资源(寄存器) | 2 个(但仿真行为异常) | 2 个 | Vivado 默认综合 |
| 仿真竞争风险 | 高(多个 always 块时) | 低 | 多驱动场景 |
故障排查(Troubleshooting)
- 现象:仿真波形中 q 为 X 态。
原因:复位未正确释放,或变量未初始化。
检查点:确认复位信号在仿真开始后拉高,且 always 块中所有变量都有复位赋值。
修复:在 initial 块中给所有 reg 赋初值,或在复位逻辑中覆盖所有分支。 - 现象:阻塞赋值版本综合后时序报告无违例,但上板后功能错误。
原因:综合器可能优化了中间寄存器,导致实际电路与仿真不一致。
检查点:查看综合后原理图,确认寄存器级数。
修复:改用非阻塞赋值。 - 现象:非阻塞赋值版本仿真中 q 与 d 同时变化。
原因:在同一个 always 块中使用了c <= d而非c <= a。
检查点:检查赋值语句右侧变量。
修复:确保级联赋值使用中间变量。 - 现象:综合报错“Multiple drivers”。
原因:同一 reg 被多个 always 块赋值。
检查点:搜索代码中该变量的所有赋值位置。
修复:将逻辑合并到一个 always 块,或使用 wire 与 assign。 - 现象:仿真速度极慢。
原因:大量非阻塞赋值导致事件队列膨胀。
检查点:查看仿真日志中的事件数。
修复:对组合逻辑使用阻塞赋值,减少不必要的事件。 - 现象:跨时钟域(CDC)仿真出现不定态。
原因:非阻塞赋值不能解决 CDC 问题,只是避免仿真竞争。
检查点:检查同步器结构。
修复:使用两级触发器同步 + 非阻塞赋值。
扩展与下一步
- 参数化模块:将移位寄存器深度改为参数
DEPTH,使用generate循环自动生成多级触发器。 - 断言验证:在 testbench 中加入 SVA 断言,自动检查
q_nonblk是否比d延迟两拍。 - 形式验证:使用 SymbiYosys 或 JasperGold 证明两种赋值风格在电路结构上的等价性(或不等价)。
- 跨平台验证:在 Vivado 和 Quartus 中分别综合,对比资源与 Fmax。
- 代码风格检查工具:使用 Verilator 或 lint 工具(如 SpyGlass)自动检测赋值风格违规。
参考与信息来源
- IEEE Std 1364-2005, Verilog Hardware Description Language
- Clifford E. Cummings, “Nonblocking Assignments in Verilog: A Comprehensive Guide”, SNUG 2000
- Xilinx UG901, Vivado Design Suite User Guide: Synthesis
- Intel Quartus Prime Handbook, Volume 1: Design and Synthesis
技术附录
术语表
- 阻塞赋值(Blocking Assignment):使用
=,立即更新变量,后续语句看到新值。 - 非阻塞赋值(Nonblocking Assignment):使用
<=,在当前仿真时间步结束时更新,所有赋值同时生效。 - 事件队列(Event Queue):仿真器管理赋值更新的调度结构。
- 竞争条件(Race Condition):多个进程对同一变量同时读写,结果依赖于执行顺序。
检查清单
- [ ] 时序逻辑 always 块中全部使用
<=。 - [ ] 组合逻辑 always 块中全部使用
=。 - [ ] 同一 always 块内未混合两种赋值。
- [ ] 每个 reg 只被一个 always 块赋值。
- [ ] 复位赋值风格与数据赋值风格一致。
关键约束速查
| 场景 | 推荐赋值 | 理由 |
|---|---|---|
| 时序逻辑(触发器) | <= | 模拟寄存器行为,避免竞争 |
| 组合逻辑(门) | = | 立即传播,仿真效率高 |
| 锁存器(Latch) | =(但应避免 Latch) | — |




