= 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 @(*) / assign | always @(posedge clk) |
| 顺序敏感 | 是——调换语句顺序可能改变结果 | 否——语句顺序不影响结果 |
| 典型应用 | 多路选择器、编码器 | 移位寄存器、计数器、状态机 |
铁律:组合用 =,时序用 <=,永远不要在同一个 always 块中混用。
6. 总结
| 你需要记住的 | 内容 |
|---|---|
阻塞 = | 像写 C 一样,按顺序立即更新 → 用于组合逻辑 |
非阻塞 <= | 先读旧值再统一更新 → 用于时序逻辑 |
| 搞混的后果 | 移位寄存器变成单级寄存器、流水线坍缩、仿真与硬件不一致 |
| 记忆口诀 | 组合用等号,时序用箭头,混用就出错 |
下一步:
- 想了解数据传输中的错误检测?→ 阅读下一篇《奇偶校验》
- 动手练习:写一个 4 级移位寄存器,分别用
=和<=,对比仿真波形的差异
常见问题
💬 如果我在时序逻辑里用了阻塞赋值,综合工具会报错吗?
大多数情况下不会报错——综合工具会尝试”理解”你的意图并生成对应的硬件。但生成的电路可能和你期望的完全不同(比如移位寄存器变成单级寄存器)。有些工具会发出 warning,但很容易被忽略。所以不要依赖工具来替你检查这个问题,养成好习惯最重要。
💬 assign 语句算阻塞赋值吗?
assign是连续赋值,严格来说既不是阻塞也不是非阻塞——它描述的是一个持续的连接关系(类似一根导线)。但从行为上看,它和阻塞赋值类似:输入变了,输出立刻跟着变。assign只能用于组合逻辑。
💬 两种赋值混在同一个 always 块里会怎样?
这是 Verilog 中最危险的写法之一。仿真器的行为会变得不确定——不同仿真器可能给出不同结果,综合结果也可能出乎意料。IEEE 标准明确不推荐这种写法。规则很简单:一个
always块只用一种赋值方式。
参考资料
- Clifford E. Cummings, Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!, SNUG 2000
- IEEE Std 1364-2005: IEEE Standard for Verilog Hardware Description Language
- Stuart Sutherland, Verilog HDL Quick Reference Guide
系列导航:本文是「FPGA 入门系列」第 16 篇。
如果这篇文章对你有帮助,欢迎点赞、收藏,也欢迎在评论区交流你对阻塞赋值和非阻塞赋值的理解——这是 FPGA 面试中的经典考题。