Menu Close

Hazard冲突解决:旁路Forwarding 和 暂停Stall 实现

🚀 Hazard 解决:Forwarding 和 Stall 实现

流水线让 CPU 变快。
但它也带来了一个新问题:

👉 指令之间会互相“撞车”

这就是 Hazard(冒险 / 冲突)

如果不处理,CPU 跑得再快也没用,因为结果会错。


🔍 一、Hazard 到底是什么?

在流水线 CPU 里,多条指令同时运行在不同阶段。

这很快。
但也意味着:

  • 后面的指令可能提前需要前面的结果
  • 分支结果可能还没出来
  • 资源可能被抢占

👉 所以 Hazard 本质上就是:

流水线中的时序冲突


🧩 二、最常见的是 Data Hazard

先看一个例子:


第一条指令:

  • 计算 x1 = x2 + x3

第二条指令:

  • 需要马上读取 x1

问题来了:

👉 第一条指令的结果,通常要到 WB(Write Back)阶段 才真正写入寄存器
👉 但第二条指令在 ID / EX 阶段 就已经要用了


所以第二条读到的,很可能还是旧值。


⚠️ 三、如果不处理,会发生什么?

我们把它放进 5-stage pipeline 看一下:

Cycle Instr 1 Instr 2
1 IF
2 ID IF
3 EX ID
4 MEM EX
5 WB MEM

Cycle 4

  • 第一条 ADD 的结果刚从 ALU 出来
  • 第二条 SUB 已经在 EX 阶段要用 x1

👉 但这时 x1 还没写回寄存器

所以结果错误。


🔥 四、解决方法1:Forwarding(旁路)

Forwarding 是最核心的解决方案。

它的思想非常直接:

既然结果已经算出来了,就不要等写回寄存器,直接送给后面的 ALU


🧠 举个例子


正常情况:

  • ADD 在 EX 阶段算出结果
  • 要等到 WB 才写回 x1

Forwarding:

  • ADD 在 EX 阶段的输出,直接转发给 SUB 的输入

👉 不等写回
👉 直接旁路


🏗️ 五、Forwarding 的硬件逻辑

你需要比较:

  • 当前 EX 阶段指令的源寄存器 rs1 / rs2
  • 前一条或前两条指令的目标寄存器 rd

如果匹配,就说明可以转发。


Forwarding 判断规则

EX hazard

如果上一条指令要写回寄存器,并且:


或者


就从 EX/MEM 转发。


MEM hazard

如果上上一条指令的结果在 MEM/WB 阶段:


或者


就从 MEM/WB 转发。


💻 六、Forwarding Unit Verilog 示例

下面是一个最小 Forwarding Unit:


🔄 七、Forwarding MUX 怎么接?

Forwarding Unit 只告诉你“该转发谁”。

真正的数据路径里,还需要 MUX(多路选择器) 来切换 ALU 输入。

比如 ALU 的 A 输入:

  • 00 → 原始寄存器值
  • 10 → 从 EX/MEM 转发
  • 01 → 从 MEM/WB 转发

示例写法


⚠️ 八、为什么仅有 Forwarding 还不够?

因为有一种情况,Forwarding 也救不了:

Load-Use Hazard

看这个例子:


问题:

  • LW 的数据不是在 EX 阶段算出来
  • 它要等到 MEM 阶段访问内存之后 才真正拿到数据

但下一条 ADD 在 EX 阶段已经想用 x1


👉 这时即使你想转发,也没有数据可转


所以必须:

让流水线暂停一个周期

这就是 Stall


🛑 九、解决方法2:Stall(暂停)

Stall 的本质是:

让后面的指令等等

具体做法:

  • PC 不更新
  • IF/ID 寄存器不更新
  • 往 ID/EX 插入一个空操作(bubble / NOP)

这样就能给 LW 多一个周期,把数据准备好。


💻 十、Hazard Detection Unit Verilog 示例

下面是最经典的 load-use 检测逻辑:


🧠 十一、control_stall 是怎么工作的?

当检测到 load-use hazard:

  • pc_write = 0
    → PC 不前进
  • if_id_write = 0
    → 当前 IF/ID 保持不变
  • control_stall = 1
    → 把控制信号清零,相当于插入 NOP

插入 bubble 的常见写法


👉 这样进入 EX 阶段的就是一条“什么都不做”的假指令


📈 十二、Forwarding 和 Stall 的关系

很多人会误解:

有了 Forwarding,就不需要 Stall

这是错的。


正确关系是:

  • Forwarding:解决大多数算术数据依赖
  • Stall:解决 forwarding 无法覆盖的情况,尤其是 load-use hazard

👉 这两个要一起用


🧩 十三、一个完整例子

看这段代码:


怎么处理?

第1、2条

SUB 依赖 ADD

👉 用 Forwarding


第3条

LWx1

👉 也可以通过 forwarding 拿到 ADD 的结果


第4条

ADD 依赖 LW

👉 必须 Stall 1 个周期


这就是现实 CPU 里最常见的混合场景。


🚨 十四、新手最容易犯的错误

❌ 1. 忘记排除 x0

RISC-V 的 x0 永远是 0,不能被当成正常写回目标


❌ 2. MEM hazard 覆盖 EX hazard

如果同时匹配,应该优先取最近的数据,也就是 EX/MEM


❌ 3. 只做 forwarding,不做 stall

这样一跑 load 指令就错


❌ 4. stall 时只停 PC,不插 bubble

这样流水线状态会乱掉


🔥 十五、最重要的认知

Hazard 处理的本质,不是“修 bug”。

而是:

让流水线在有依赖时,仍然保持正确和高效


也就是说:

  • Forwarding 是“少等”
  • Stall 是“该等就等”

真正的 CPU 设计,不是单纯追求快,而是:

在正确的前提下尽量不浪费周期


🚀 十六、你现在完成了什么?

如果你已经有:

  • 5-stage pipeline
  • forwarding unit
  • hazard detection unit
  • stall / bubble 插入逻辑

那么你就已经做到了:

一个真正像样的流水线 CPU 核心

这已经不是“教学玩具”了。


📌 总结

  • Hazard 是流水线中的时序冲突
  • 最常见的是 data hazard
  • Forwarding 用来直接转发结果,减少等待
  • Stall 用来处理 load-use 这类无法立即转发的情况
  • 两者必须配合使用

👉 一句话:

流水线 CPU 快,不是因为没有依赖,而是因为它知道什么时候该绕开,什么时候该等待


下一篇:

《Branch Hazard:分支为什么会让流水线失速?》

你将看到:

  • 为什么 BEQ 会打断流水线
  • Flush 是怎么实现的
  • 静态预测和动态预测的基本思路

 

除教程外,本网站大部分文章来自互联网,如果有内容冒犯到你,请联系我们删除!
Posted in RISC-V教程