LA 挑战赛:BRAM 的引入
BRAM
理想的效果是,IFU 在当前周期上升沿给 inst_sram 发射地址后,在当前周期内就可以拿到 inst_data,但是这是理性情况下。
异步 RAM 很方便,但是实际情况是,CPU 的频率非常快,比 RAM 快得多,因此如果为了保持异步访存,就得将 CPU 的频率降低,这很明显会损失性能(暂不考虑 inst cache)。
这里就引入了 BRAM,它在当前周期给 BRAM 发射地址,下一个周期才能拿到数据。
pre_IFU 的提出
为了解决这样的事,首先可以将 pre_IFU 单独拿出来,设计成一个辅助模块。
需要注意的是,这个模块并不是一个流水单元,因为它没有流水向下一个单元 IFU 传递缓存的数据。它实际上是一个外挂在 IFU 的组合电路。
1 |
|
这里的难点是,在 IDU 阻塞的时候,要保持 inst_sram 输出的数据。笔者仔细观察波形图后,发现当 IDU 阻塞的时候,可以让 inst_addr 变成上一个周期的值也就是 fs_pc,这样,被阻塞在 IDU 的指令一旦阻塞停止,就可以拿到 fs_pc 位置的指令重新执行。
访问 data_sram 信号的提前
和访问 inst_sram 同理,需要将 MEM 阶段的访存信号提前到 EXU 中,这样,当访存指令进行到 MEM,就可以完成 LOAD、STORE,解决了load_use、store_load 相关。
好的,这里给出 load_use
和 store_load
指令冲突的例子(使用类似汇编的指令格式):
1. Load-Use 指令冲突
场景: 一条 LOAD
指令从内存中读取数据到寄存器,紧随其后的指令需要立即使用该寄存器中的数据。由于 LOAD
指令通常需要多个时钟周期才能完成内存访问并将数据写回寄存器,因此后续指令在数据准备好之前尝试读取该寄存器时会发生冲突。
示例指令序列:
1 |
|
流水线执行过程(假设一个简单的五级流水线:取指 (IF)、译码 (ID)、执行 (EX)、访存 (MEM)、写回 (WB)):
时钟周期 | IFU (取指) | IDU (译码) | EXU (执行) | MEM (访存) | WB (写回) |
---|---|---|---|---|---|
1 | LOAD R1, 0(R2) | ||||
2 | ADD R3, R1, R4 | LOAD R1, 0(R2) | |||
3 | ADD R3, R1, R4 | LOAD R1, 0(R2) (计算地址) | |||
4 | ADD R3, R1, R4 (尝试读取 R1) | LOAD R1, 0(R2) (访问内存) | |||
5 | ADD R3, R1, R4 | LOAD R1, 0(R2) (写入 R1) |
冲突分析:
在第 4 个时钟周期,ADD
指令进入执行阶段,需要读取寄存器 R1 的值。然而,LOAD
指令在第 5 个时钟周期才能完成写回操作,将数据写入 R1。因此,ADD
指令在执行时会读取到 R1 的旧值(或者一个无效的值),导致计算结果错误。这就是 load_use
指令冲突。
解决办法(部分):
- 流水线停顿 (Pipeline Stall): 在
ADD
指令的执行阶段暂停一个或多个时钟周期,直到LOAD
指令完成写回。 - 数据前递 (Data Forwarding/Bypassing): 将
LOAD
指令在 MEM 阶段获得的数据直接转发给ADD
指令的执行阶段,而无需等待其写回寄存器。
遗憾的是,如果访问 BRAM,上面的方法无效。笔者前一篇博客用这两种方法结合,解决的是 load_use 冲突,但那是基于异步 RAM。这里如果是 BRAM,那么 IDU 阶段的 ADD 指令需要等待两个周期,这样控制难度提升了一大截。
因此,这里的解决办法就是将访存信号提前到 EXU 中,这样原来的 load_use 控制依然有效,就像访问异步 RAM 那样。
2. Store-Load 指令冲突
场景: 一条 STORE
指令将数据写入内存的某个地址,紧随其后的 LOAD
指令尝试从相同的内存地址读取数据。如果 LOAD
指令在 STORE
指令完成写入之前就访问了内存,可能会读取到旧的数据。
示例指令序列:
1 |
|
流水线执行过程:
时钟周期 | IFU (取指) | IDU (译码) | EXU (执行) | MEM (访存) | WB (写回) |
---|---|---|---|---|---|
1 | STORE R5, 0(R6) | ||||
2 | LOAD R7, 0(R6) | STORE R5, 0(R6) | |||
3 | LOAD R7, 0(R6) | STORE R5, 0(R6) (计算地址) | |||
4 | LOAD R7, 0(R6) (计算地址) | STORE R5, 0(R6) (写入内存) | |||
5 | LOAD R7, 0(R6) (读取内存) | STORE R5, 0(R6) |
冲突分析:
在第 5 个时钟周期,LOAD
指令进入访存阶段,尝试从内存地址 R6 + 0 读取数据。然而,STORE
指令在第 4 个时钟周期才完成写入内存的操作。如果 LOAD
指令在第 5 个时钟周期读取内存时,STORE
指令的写入操作尚未完成或者数据还未完全更新,那么 LOAD
指令可能会读取到 STORE
指令写入之前的数据,导致数据不一致。这就是 store_load
指令冲突。
解决办法(部分):
- 内存访问顺序保证: 确保
LOAD
指令在STORE
指令完成写入操作之后再进行读取。这可能需要流水线停顿。 - 存储转发 (Store Forwarding): 如果
LOAD
指令尝试读取的地址与之前未完成的STORE
指令的目标地址相同,可以将STORE
指令要写入的数据直接转发给LOAD
指令,而无需等待数据真正写入内存后再读出。这正是博文中提到的“访问 data_sram 信号的提前”所要解决的关键问题之一。通过将访存阶段提前到 EXU,可以在 EXU 阶段就进行地址比较和数据转发,避免流水线停顿。
这些例子清晰地说明了 load_use
和 store_load
指令冲突在流水线 CPU 中是如何发生的,以及为什么需要相应的机制来检测和解决这些冲突,以保证程序的正确执行并提高流水线的效率。
到目前为止,笔者实现的所有方法都不用解决这种情况,因为这种情况非常少见,遇到了再说。
通过了所有测试:
1 |
|