在Verilog硬件描述语言中,赋值语句是构建数字逻辑行为模型的核心。其中,阻塞赋值(=)与非阻塞赋值(<=)的差异,是初学者乃至有经验的设计者都必须深刻理解的关键概念。选择不当不仅会导致仿真与综合结果不一致,更会引入难以调试的竞争冒险和功能错误。本指南旨在通过清晰的步骤与机制分析,帮助你建立正确的赋值模型思维,并规避常见的设计陷阱。
快速概览
阻塞赋值类似于软件中的顺序执行,语句按书写顺序依次计算并立即更新左值;而非阻塞赋值则将所有右值表达式在当前时刻同时采样,并在当前仿真时间步结束时统一更新左值。简单来说,阻塞赋值是“立即生效”,而非阻塞赋值是“稍后同时生效”。
前置条件与目标
在开始实践前,请确保你已具备以下基础:
- 了解Verilog模块(
module)和过程块(always、initial)的基本结构。 - 熟悉寄存器(
reg)与线网(wire)数据类型。 - 拥有可运行的仿真环境(如ModelSim、VCS或Icarus Verilog)。
本指南的目标与验收标准:完成学习后,你将能够:
- 准确解释两种赋值在仿真时间模型中的执行机制。
- 在组合逻辑和时序逻辑设计中正确选用赋值类型。
- 识别并修复因赋值使用不当导致的典型电路功能错误。
- 编写仿真与综合结果一致的稳健RTL代码。
核心机制与实施步骤
步骤一:理解仿真事件队列
Verilog仿真器维护一个事件队列,这是理解赋值差异的基石。队列主要分为:
- 活跃事件:当前仿真时间需要执行的操作,如计算阻塞赋值的右值并立即更新。
- 非阻塞赋值更新事件:被安排在当前时间步结束时,统一更新所有非阻塞赋值的左值。
你可以将仿真过程想象成一场会议:阻塞赋值是“想到就说,立刻修改白板”,而非阻塞赋值是“各自把想法写在纸条上,等主持人说‘现在统一贴到白板上’时再同时张贴”。
步骤二:组合逻辑设计——使用阻塞赋值
在描述组合逻辑的always @(*)块中,应使用阻塞赋值(=)。其行为模拟了信号通过组合逻辑门的瞬时传播(忽略延迟)。
// 示例:一个简单的与或逻辑链
always @(*) begin
temp = a & b; // 阻塞赋值,temp立即更新
y = temp | c; // 使用更新后的temp值计算y
end原因与风险边界:这里使用阻塞赋值确保了语句的顺序依赖性,正确建模了信号从a、b到y的级联路径。若误用非阻塞赋值,由于temp和y的更新被推迟且同时发生,y将使用temp的旧值,导致逻辑错误。风险在于,如果组合逻辑块中存在反馈(例如a = a + 1),使用阻塞赋值会产生仿真死锁,这已不属于纯组合逻辑范畴。
步骤三:时序逻辑设计——使用非阻塞赋值
在描述时序逻辑(如触发器)的always @(posedge clk)块中,必须使用非阻塞赋值(<=)。这精确建模了所有寄存器在同一时钟沿后同时更新的硬件行为。
// 示例:带同步复位的移位寄存器
always @(posedge clk) begin
if (rst) begin
q1 <= 1'b0;
q2 <= 1'b0;
end else begin
q1 <= d; // 时钟沿时刻采样d,结束后更新q1
q2 <= q1; // 采样的是q1的旧值(上一周期的值)
end
end原因与机制分析:在时钟上升沿,仿真器同时采样所有右值(d和q1的旧值),然后在时间步结束时同时更新q1和q2。这保证了q2得到的是q1在上一时钟周期的值,从而实现正确的移位功能。若误用阻塞赋值(q1 = d; q2 = q1;),则q1会立即更新,导致q2在同一时钟沿就获得了新的q1值,这相当于一个触发器,而非两个级联的触发器,综合结果将与预期严重不符。
步骤四:验证设计——编写测试用例
通过仿真对比正确与错误赋值方式的结果,是巩固理解的最佳途径。
// 测试片段:激励生成
initial begin
clk = 0; rst = 1; d = 1;
#10 rst = 0;
// 观察多个时钟周期...
end
always #5 clk = ~clk; // 生成时钟在仿真波形中,重点观察在rst释放后的第一个时钟上升沿:
1. 正确情况(非阻塞):q1变为1(d的值),q2变为0(复位值)。
2. 错误情况(阻塞):q1变为1,q2也立即变为1(拿到了新的q1值)。
常见陷阱与排障指南
- 陷阱1:在同一个always块中混合使用
现象:仿真行为难以预测,综合可能报错或产生锁存器。
解决:严格遵循单一准则——组合逻辑块全用阻塞,时序逻辑块全用非阻塞。绝不要混合。 - 陷阱2:使用非阻塞赋值描述组合逻辑
现象:仿真结果看似正常,但综合后电路面积可能增大,且功能在特定路径延迟下可能出错(仿真与综合失配)。
解决:检查所有always @(*)块,确保其中没有<=。 - 陷阱3:在时序逻辑中使用阻塞赋值实现“临时计算”
现象:意图是先计算一个中间值,再用其更新寄存器,但破坏了并行性。
解决:将中间计算提取到独立的组合逻辑always块中,或使用连续的阻塞赋值在always @(posedge clk)块开始处先计算好,再用非阻塞赋值将最终结果赋给寄存器。但后者需格外小心,推荐前者。
扩展与最佳实践
1. 统一编码风格:团队应强制执行“时序逻辑非阻塞,组合逻辑阻塞”的规则,并在代码审查中重点检查。
2. 仿真与综合一致性:牢记,非阻塞赋值是确保仿真模型与综合后网表行为一致的关键。综合工具会将非阻塞赋值解读为并行寄存器更新。
3. 处理复杂顺序:对于依赖于多个前序状态的复杂时序更新,可考虑使用“计算-更新”两段式模型,或使用generate块和数组来保持代码的清晰与正确性。
总结与参考
掌握阻塞与非阻塞赋值的本质,是写出可靠、可综合Verilog代码的里程碑。其核心在于深刻理解Verilog的仿真调度机制,并以此映射到真实的硬件并行行为。始终问自己:我描述的是一组同时变化的寄存器(用<=),还是一条信号传播的组合路径(用=)?
进一步参考:
• IEEE Standard for Verilog Hardware Description Language (IEEE Std 1364). 重点关注“Scheduling semantics”章节。
• Cummings, Clifford E. 的经典论文《Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!》对陷阱有深入论述。
附录:快速自查清单
- 我的
always @(posedge clk)块中是否只使用了<=? - 我的
always @(*)块中是否只使用了=? - 我是否避免了在同一个always块内混合两种赋值?
- 我的测试波形是否显示了寄存器在时钟沿后同时更新?




