Quick Start:快速上手实验
本指南通过一个简单的移位寄存器实例,帮助你直观理解阻塞赋值(=)与非阻塞赋值(<=)在行为级仿真中的差异。你只需准备一个支持 Verilog 的仿真工具(如 Vivado 2020.1+、ModelSim 或 QuestaSim),并按照以下步骤操作,即可在 10 分钟内复现典型误区并验证正确做法。
前置条件
- 已安装任一主流 Verilog 仿真工具(Vivado、ModelSim、QuestaSim 等)。
- 具备基本 Verilog 语法知识,了解 always 块与敏感列表。
- 建议熟悉波形查看工具(如 Vivado Simulator 或 GTKWave)。
目标与验收标准
- 目标:通过对比阻塞与非阻塞赋值实现的移位寄存器,理解二者在时序逻辑中的行为差异,并掌握正确使用场景。
- 验收标准:
实施步骤
步骤 1:创建工程与顶层模块
在仿真工具中新建一个工程,并创建一个 Verilog 模块,命名为 shift_register_tb(测试模块)。
步骤 2:编写非阻塞赋值版移位寄存器
module shift_nonblocking (
input clk,
input rst_n,
input din,
output reg dout
);
reg [2:0] shift_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
shift_reg <= 3'b0;
else begin
shift_reg[0] <= din;
shift_reg[1] <= shift_reg[0];
shift_reg[2] <= shift_reg[1];
dout <= shift_reg[2];
end
end
endmodule步骤 3:编写阻塞赋值版移位寄存器(常见误区)
module shift_blocking (
input clk,
input rst_n,
input din,
output reg dout
);
reg [2:0] shift_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
shift_reg = 3'b0;
else begin
shift_reg[0] = din;
shift_reg[1] = shift_reg[0];
shift_reg[2] = shift_reg[1];
dout = shift_reg[2];
end
end
endmodule步骤 4:编写测试激励(testbench)
module tb;
reg clk, rst_n, din;
wire dout_nb, dout_b;
shift_nonblocking u_nb (.clk(clk), .rst_n(rst_n), .din(din), .dout(dout_nb));
shift_blocking u_b (.clk(clk), .rst_n(rst_n), .din(din), .dout(dout_b));
initial begin
clk = 0;
forever #5 clk = ~clk;
end
initial begin
rst_n = 0; din = 0;
#20 rst_n = 1;
#10 din = 1;
#10 din = 0;
#100 $finish;
end
endmodule步骤 5:运行仿真并观察波形
运行仿真至少 200 ns,添加 dout_nb 和 dout_b 到波形窗口。你会看到:
- 非阻塞赋值版:数据在每个时钟上升沿逐位右移,延迟 3 个时钟周期后出现在
dout_nb。 - 阻塞赋值版:
dout_b在同一个时钟周期内直接跟随din变化,没有移位效果——因为阻塞赋值在 always 块内立即更新,导致所有赋值“同时”完成,相当于组合逻辑。
验证结果
观察波形后,应确认以下关键差异:
- 非阻塞赋值:
shift_reg[0]在时钟上升沿采样din,shift_reg[1]采样上一拍shift_reg[0],以此类推。输出dout_nb在 3 个时钟周期后复制din的初始值。 - 阻塞赋值:由于
shift_reg[0] = din立即更新,随后shift_reg[1] = shift_reg[0]读取的是刚更新的值,导致所有寄存器在同一拍内完成数据传递,失去时序存储特性。
如果波形符合上述描述,则实验验证通过。
排障指南
- 问题:波形中非阻塞版本也没有移位效果——检查敏感列表是否遗漏了
negedge rst_n,或复位逻辑中使用了阻塞赋值。确保 always 块内所有时序赋值均使用<=。 - 问题:仿真结果与预期完全相反——确认测试激励中
din的变化时刻是否在时钟沿附近。建议将din变化安排在时钟上升沿之后(如#6或#7),避免建立/保持时间冲突。 - 问题:工具报错“multiple drivers”——检查是否在多个 always 块中对同一 reg 赋值。移位寄存器应只在一个 always 块中描述。
扩展:深入理解赋值机制
为什么阻塞赋值不适合时序逻辑?
阻塞赋值(=)在 always 块内按顺序立即执行,每个赋值语句都会更新目标寄存器的值,并影响后续语句的读取。在时钟沿触发的 always 块中,这会导致同一时钟周期内多个寄存器同时更新,破坏时序逻辑的“锁存-延迟”特性。而非阻塞赋值(<=)将赋值操作推迟到 always 块结束时统一执行,所有右值在块开始时被采样(即上一时钟周期的值),从而正确模拟了触发器的行为。
风险边界:
- 在组合逻辑 always 块中(敏感列表为电平触发),应使用阻塞赋值,否则会导致仿真与综合不一致。
- 在同一个 always 块中混合使用阻塞与非阻塞赋值是常见的错误来源,应严格避免。
- 对于多时钟域或异步复位设计,非阻塞赋值也不能自动解决亚稳态问题,仍需同步器电路。
参考资源
- IEEE Std 1364-2001 Verilog 硬件描述语言标准,第 9.2 节“阻塞与非阻塞赋值”。
- Clifford E. Cummings, “Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!” (SNUG 2000).
- Vivado Design Suite 用户指南:仿真 (UG900)。
附录:完整测试代码
以下为可直接复制到仿真工具中的完整 testbench 代码(包含两个模块):
// 非阻塞赋值版
module shift_nonblocking (
input clk, rst_n, din,
output reg dout
);
reg [2:0] shift_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) shift_reg <= 3'b0;
else begin
shift_reg[0] <= din;
shift_reg[1] <= shift_reg[0];
shift_reg[2] <= shift_reg[1];
dout <= shift_reg[2];
end
end
endmodule
// 阻塞赋值版(常见错误)
module shift_blocking (
input clk, rst_n, din,
output reg dout
);
reg [2:0] shift_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) shift_reg = 3'b0;
else begin
shift_reg[0] = din;
shift_reg[1] = shift_reg[0];
shift_reg[2] = shift_reg[1];
dout = shift_reg[2];
end
end
endmodule
// 测试激励
module tb;
reg clk, rst_n, din;
wire dout_nb, dout_b;
shift_nonblocking u_nb (.*);
shift_blocking u_b (.*);
initial begin
clk = 0;
forever #5 clk = ~clk;
end
initial begin
rst_n = 0; din = 0;
#20 rst_n = 1;
#10 din = 1;
#10 din = 0;
#100 $finish;
end
initial begin
$monitor("Time=%0t din=%b dout_nb=%b dout_b=%b", $time, din, dout_nb, dout_b);
end
endmodule


