Veloris.
返回索引
概念基础 2026-02-23

= vs <=:一个字符的差别,决定你的电路对不对

4 分钟
1.2k words

= vs <=:一个字符的差别,决定你的电路对不对

💡 刚学 Verilog 的时候,你一定困惑过:<= 不是”小于等于”吗?为什么在 always 块里它变成了赋值?更让人困惑的是——什么时候用 =,什么时候用 <=,搞混了代码居然还能仿真通过,但硬件行为完全不对。

这篇文章带你彻底搞清楚阻塞赋值和非阻塞赋值的本质区别,以及为什么搞混它们会导致Bug


1. 阻塞赋值(=):像写 C 语言一样

阻塞赋值使用 = 号,行为和你熟悉的 C/Python 赋值完全一样:立即执行,按顺序来

always @(*) begin
    a = 1;      // 第1步:a 立即变为 1
    b = a;      // 第2步:b 拿到 a 的新值(1)
    c = b + 1;  // 第3步:c = 1 + 1 = 2
end

核心特征:每条语句执行完毕后,变量立即更新,下一条语句看到的是更新后的值。

用在哪里? 组合逻辑(always @(*)assign)。因为组合逻辑的本质就是”输入变了,输出立刻跟着变”,阻塞赋值的”立即更新”正好匹配这个行为。


2. 非阻塞赋值(<=):像硬件一样并行

非阻塞赋值使用 <= 号,行为和软件编程完全不同:先全部读取旧值,最后统一更新

always @(posedge clk) begin
    a <= 1;      // 计划:a 将变为 1
    b <= a;      // 计划:b 将变为 a 的旧值(不是 1!)
    c <= b + 1;  // 计划:c 将变为 b 的旧值 + 1
end

核心特征:所有右侧表达式(RHS)先用旧值同时计算,然后所有左侧变量(LHS)在时间步结束时同时更新。

用在哪里? 时序逻辑(always @(posedge clk))。因为硬件中所有触发器是在同一个时钟沿同时翻转的——非阻塞赋值正好模拟了这个物理行为。


3. ★ 为什么搞混会出 Bug?——移位寄存器的例子

这是全文最重要的部分。看一个 3 级移位寄存器的例子:

// ❌ 错误:时序逻辑中用阻塞赋值
always @(posedge clk) begin
    q1 = d;     // q1 立即变为 d
    q2 = q1;    // q2 拿到的是"刚更新的 q1"(也就是 d)
    q3 = q2;    // q3 拿到的是"刚更新的 q2"(也就是 d)
end
// 结果:q1 = q2 = q3 = d,三级变一级!
// ✅ 正确:时序逻辑中用非阻塞赋值
always @(posedge clk) begin
    q1 <= d;    // 计划:q1 将变为 d
    q2 <= q1;   // 计划:q2 将变为 q1 的旧值
    q3 <= q2;   // 计划:q3 将变为 q2 的旧值
end
// 结果:数据逐级传递,d → q1 → q2 → q3,正确的移位行为!

这个 Bug 最阴险的地方是:阻塞赋值的版本在仿真中可能”看起来”正常(取决于仿真器的调度策略),但综合出来的硬件行为一定是错的。

💡 工程师手记我曾经在一个 SPI 从机模块里用阻塞赋值写了一个数据移位寄存器。仿真的时候数据完美地一位一位移入,但上板后收到的数据全是最后一位的重复。查了两个小时才发现是 =<= 搞混了。从那以后我养成了一个习惯:写完 always @(posedge clk) 后,第一件事就是检查所有赋值是不是都用了 <=

(建议替换为你自己的真实经历,读者会更有共鸣)


4. 仿真器的视角:为什么非阻塞赋值能”并行”

理解了仿真器的事件调度模型,你就彻底理解了非阻塞赋值的原理。

Verilog 仿真器在每个时间步(time step)中,分两个阶段处理非阻塞赋值:

阶段 1:评估(Evaluate)
  ┌─────────────────────────────────────┐
  │ 同时读取所有 RHS 的当前值           │
  │ a_new = 1                           │
  │ b_new = a_current (旧值,假设为 3)│
  │ c_new = b_current + 1 (旧值 + 1)  │
  └─────────────────────────────────────┘

阶段 2:更新(Update)
  ┌─────────────────────────────────────┐
  │ 同时将所有 LHS 更新为计算结果       │
  │ a = 1                               │
  │ b = 3                               │
  │ c = b_old + 1                       │
  └─────────────────────────────────────┘

这就是为什么非阻塞赋值不受代码书写顺序的影响——无论你怎么排列语句,RHS 都是在同一时刻用旧值计算的,LHS 都是在同一时刻更新的。

而阻塞赋值是边算边更新的——每执行一行,变量就立刻变了,下一行看到的是新值。所以代码书写顺序会影响结果。

💬 你可能会问:这和硬件有什么关系?

在真实硬件中,所有由同一个时钟驱动的触发器确实是在同一个时钟沿同时采样、同时翻转的。非阻塞赋值的”先全部读旧值,再统一更新”正好模拟了这个物理过程。如果你用阻塞赋值,就相当于让触发器”排队”一个一个翻转——这不是硬件的真实行为。


5. 核心对比

维度阻塞赋值 =非阻塞赋值 <=
执行方式立即执行,按顺序更新先全部读旧值,最后统一更新
模拟什么组合逻辑的即时计算触发器的同时翻转
用在哪里always @(*) / assignalways @(posedge clk)
顺序敏感是——调换语句顺序可能改变结果否——语句顺序不影响结果
典型应用多路选择器、编码器移位寄存器、计数器、状态机

铁律:组合用 =,时序用 <=,永远不要在同一个 always 块中混用。


6. 总结

你需要记住的内容
阻塞 =像写 C 一样,按顺序立即更新 → 用于组合逻辑
非阻塞 <=先读旧值再统一更新 → 用于时序逻辑
搞混的后果移位寄存器变成单级寄存器、流水线坍缩、仿真与硬件不一致
记忆口诀组合用等号,时序用箭头,混用就出错

下一步:

  • 想了解数据传输中的错误检测?→ 阅读下一篇《奇偶校验》
  • 动手练习:写一个 4 级移位寄存器,分别用 =<=,对比仿真波形的差异

常见问题

💬 如果我在时序逻辑里用了阻塞赋值,综合工具会报错吗?

大多数情况下不会报错——综合工具会尝试”理解”你的意图并生成对应的硬件。但生成的电路可能和你期望的完全不同(比如移位寄存器变成单级寄存器)。有些工具会发出 warning,但很容易被忽略。所以不要依赖工具来替你检查这个问题,养成好习惯最重要。

💬 assign 语句算阻塞赋值吗?

assign连续赋值,严格来说既不是阻塞也不是非阻塞——它描述的是一个持续的连接关系(类似一根导线)。但从行为上看,它和阻塞赋值类似:输入变了,输出立刻跟着变。assign 只能用于组合逻辑。

💬 两种赋值混在同一个 always 块里会怎样?

这是 Verilog 中最危险的写法之一。仿真器的行为会变得不确定——不同仿真器可能给出不同结果,综合结果也可能出乎意料。IEEE 标准明确不推荐这种写法。规则很简单:一个 always 块只用一种赋值方式


参考资料

  1. Clifford E. Cummings, Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!, SNUG 2000
  2. IEEE Std 1364-2005: IEEE Standard for Verilog Hardware Description Language
  3. Stuart Sutherland, Verilog HDL Quick Reference Guide

系列导航:本文是「FPGA 入门系列」第 16 篇。

如果这篇文章对你有帮助,欢迎点赞、收藏,也欢迎在评论区交流你对阻塞赋值和非阻塞赋值的理解——这是 FPGA 面试中的经典考题。

End of file.