# 把开源乱序核推到 105 MHz：一次跨阶段组合桥的流水化重构

> 作者: Lazybones | 日期: 2026-10-22

# 把开源乱序核推到 105 MHz：一次跨阶段组合桥的流水化重构

> 这是一篇关于“时序收敛”的文章，不是关于“从零设计 CPU”的文章。基座是清华大学 NOP 队开源的 NOP-Core（MIT License），本队的工作集中在 S3v2 前端时序重构、CPUCFG 功能适配、AXI 壳适配，以及全部时序收敛与等价性验证。把边界说清楚，后面的数字才有意义。

## 基座与边界

2026 年龙芯杯团体赛的初赛要求实现一套支持 LA32R 的裸机微系统，功能测试 58 项、性能测试 20 项，评分同时看功能分和系统计数器比值。我们选择在 NOP-Core 上二次开发：它已经是一个三发射、乱序执行、带分支预测的处理器核，基座提交为 `1a5986d`，用 SpinalHDL 1.8.1 / Scala 2.12 构建。

基座提供的结构包括：

| 结构 | 参数 |
|---|---|
| 发射/提交宽度 | 3 发射：整数 ALU ×3 + 乘除 ×1 + 访存 ×1 |
| 分支预测 | BTB 1024 项、PHT 8192 项（gshare）、GHR 5 位、RAS 8 项 |
| 乱序窗口 | ROB 32 项，按程序序提交 |
| 发射队列 | IntIQ 压缩队列（CompressedQueue） |
| 取指缓冲 | FetchBuffer，4 路入队 / 3 路出队 |
| 流水深度 | 宏观三级流水段，最深路径约 13 级 |

这些不是本队重新设计的部分。本文要讲的是：在这样一个基座上，怎样让它在 105 MHz 下收敛，并且证明时序重构没有改变功能。

## 目标不是“跑得快”，而是“计数器比值”

比赛的性能分不是直接测秒数，而是对 20 项 benchmark 分别统计 CPU 域计数和 SoC 域计数，与官方参考核求比，再取几何平均。这个比值同时受频率和 IPC 影响：

```text
system counter ratio ∝ 实测频率 × IPC
```

最终提交版的数据是：

| 指标 | 结果 |
|---|---|
| System counter ratio（20 项几何平均） | 3.817781304 |
| IPC ratio（20 项几何平均） | 1.180267034 |
| 实测频率 | 105.887336 MHz |
| 功能测试 | 58/58 × 3 随机种子全 PASS |
| 性能测试 | 20/20 全 PASS |
| 时序 | setup WNS +0.035 ns，hold +0.057 ns |

从比赛基线 0.287 到最终 3.8178，是团队整个迭代过程的结果，包含基座能力、前端预测、后端调度和大量板上调试。本文只拆解其中与 S3v2 相关的一段：**为什么 105 MHz 下唯一不收敛的是一条跨阶段组合桥，以及怎样用协议保持的方式把它斩断。**

## 问题：提交段到取指段的组合桥

100 MHz 版本在板上全绿之后，第一次冲 105 MHz，setup WNS 是 −0.098 ns。时序报告里最差的路径很集中：

```text
ROB 重定向判定（提交段）
        ↓ 同拍组合穿透
nextPC 三候选选择
        ↓
ICache 读地址
```

也就是说，后端在提交段发现分支误预测，重定向信号同拍穿过前端的候选选择逻辑，直接抵达 ICache 的读地址。功能上这是对的，但在 105 MHz 下，逻辑级数加上布线延迟，这条路径无法收敛。

这不是一个“再加一级流水”就能随便解决的问题。乱序核的重定向路径本来就是关键路径：它决定了误预测后多久能重新取指。随便加寄存器可能改变握手语义，导致漏冲或重复冲刷。我们需要的是**只改时序、不改值和次序**的重构。

## S3v2：把组合穿透改成寄存器切片

基座的重定向接口叫 `backendJumpInterface`，原来用 `s2mPipe()` 做流水化。S3v2 把它改成 `m2sPipe()`：

```scala
// 改动示意（ProgramCounterPlugin.scala）
backendJumpInterface
  .m2sPipe()   // 原为 s2mPipe()
```

在 SpinalHDL 的 Stream 语义里，这两个算子都是协议保持的流水化原语，区别在于寄存器切片放在接口的哪一侧。换成 `m2sPipe()` 之后，重定向锥到 nextPC/ICache 读地址的组合链路被寄存器化斩断，跨阶段组合桥变成干净的寄存器到寄存器路径。

但只改这一行还不够。`m2sPipe()` 让重定向晚一拍到达，取指重启和冲刷配对出现了同拍依赖。如果不同步冲刷流水寄存器里的旧路径指令，就会漏冲一条。最终提交版在功能适配基线之上的网表差异只有 **42 行**，其中另一行关键改动是：

```scala
arbitration.flushIt setWhen jumpPipe.valid
```

它保证重定向流水寄存器里的旧路径指令在同拍被冲刷。收益和代价都很明确：

- setup WNS 从 **−0.098 ns** 收敛到 **+0.035 ns**，hold +0.057 ns；
- 实测频率 **105.887336 MHz**；
- 代价是重定向延迟 **+1 拍**，本地实测 CoreMark 代价 **<1%**；
- 实验 pipeline 独立跑出 3.818592600，与官验 3.817781304 的偏差只有 **0.02%**，属于测量涨落量级。

## 高扇出治理三法

S3v2 是主刀，但 105 MHz 不是靠一刀就收敛的。过程中形成了三条可复用的连线级重构方法，共同纪律是：不改值、不改协议、零新增状态元。

### 1. 级间寄存器切片 + 独立使能副本

对扇出数百的宽级间寄存器广播网，按位切片（例如 128 bit），并为每片生成独立的使能副本和 `dont_touch` 驱动副本。这样每片负载群就近驱动，布线局部化；使能信号同步复制，保证各片更新语义一致。

等价性是构造性的：同一个布尔表达式生成 k 个副本，负载按位切片恰好覆盖原负载全集，每个输出 bit 的次态函数字面不变。这个方法把 ROB push 占用度广播锥（fanout 621/687）消除，100 MHz 下 WNS 从 −0.057 ns 收敛到 +0.018 ns。

### 2. 优先级 mux 重结合

对“选择信号 → 地址函数（index 抽取/hash/递增）→ 存储读地址”的串行链，把选择操作从地址函数之前搬到每个 bank 加法器之后：各候选地址并行完成全部计算，晚到的选择信号到达后只剩一级独热多选。

等价性依据是重结合恒等式：

```text
sel · f(x) ≡ f(sel · x)
```

先选后算与先算后选，在同一个选择语义下结果一致，真值表只有 3 行，可以穷尽验证。这个改动让 BTB/PHT 读地址锥从时序报告头部彻底消失，而且寄存器计数 21553 = 21553，逐位相等。

### 3. 并行前缀求和

压缩队列的 `popCounts` 原本是串行 `Mux(b, pc+1, pc)` 前缀链，级数随队列深度线性增长，是发射通路的固有长链。把它重构成并行前缀平衡求和树，级数从 6 级降到约 3 级；再辅以发射槽独热选择的 64 bit 切片副本和唤醒比较器副本。

等价性对全部 2^7 × 2 = 256 个输入组合（含不可达 grant 数）做数值穷尽验证，0 失配。

## 等价性不是“跑一遍功能测试”

时序重构最危险的地方是“功能测试过了，但边界条件下漏冲一条指令”。所以这套工作采用真值表级/逐拍级的验证标准，而不是仿真容差：

- 组合逻辑重写：要求真值表穷尽，或构造性恒等论证；
- 网表级改动：与基线在双 harness 下逐拍 A/B 对照；
- 功能 harness：3 个随机种子，覆盖固定延迟和 AXI 随机延迟两种剖面，逐拍 bit 一致（PASS 点、每点周期戳、冻结点、commits/cycles 终值 diff 为空）；
- 性能 harness：CoreMark + 5 项分支密集负载，周期逐位相同；
- 零新增状态元：`clocked<=` 计数逐位相等。

这套标准把“等价”从纸面主张变成了可重复的实验事实。也正因为如此，42 行的网表改动才敢直接上板。

## 结果与瓶颈

20 项性能测试里，几何平均是 3.817781304，但分布并不均匀：

| benchmark | IPC ratio | system counter ratio |
|---|---|---|
| bitcount | 1.847140 | 5.964467 |
| coremark | 1.035607 | 3.350227 |
| stream_copy | 1.188413 | 3.823799 |
| quick_sort | 0.869319 | 2.812430 |
| fireye_C0 | 0.725695 | 2.347451 |

最低的几项都是分支密集负载：`fireye_C0` 0.7257、`quick_sort` 0.8693、`bubble_sort` 1.0226；计算密集的 `bitcount` 则达到 1.8471。**分支误预测冲刷是 IPC 与三发射理论上限之间的最大缺口。** 访存密集项的 CPU/SoC 计数比偏差最大，对应 DDR 延迟占比随频率上升的衰减机制。

这也解释了为什么频率不是越高越好：基座 IPC 从 25 MHz 的 1.527 衰减到 105 MHz 的 1.180。DDR 访存的绝对延迟不随频率下降，它的相对占比随频率上升，最终决定了频率收益的上界窗口。

## 下一步：四发射与更好的预测器

设计报告里记录了两个已经在实验线完成等价性实证、但未并入最终提交网表的方向：

1. **压缩队列的循环队列重构**：当前 IntIQ 的压缩位移链（`queue_0 → queue_1` 的寄存器到寄存器搬移）在 105 MHz 下是四发射扩展的架构级瓶颈；扇出/组合级修复在原理上不可收敛，唯一正解是循环队列。实测收益锚点是 3.875。
2. **S3v2 + mini-TAGE**：把更强的预测器与已经收敛的前端时序结合，待频率余量重建后合入。

这两个方向都没有进入最终提交版。比赛提交的是稳定、可验证的那一版，实验线保留在仓库历史里，供赛后继续。

## 可迁移的教训

这次工作留下三条比分数更重要的经验：

1. **先定位唯一不收敛的锥，再做时序重构。** 105 MHz 下最差路径只有一条，改一条比改十条有效。
2. **协议保持的重构，等价性由协议担保，但边界条件要自己验证。** `m2sPipe()` 是标准算子，但它带来的 +1 拍延迟会制造新的同拍依赖；`flushIt` 那一行才是功能等价的关键。
3. **“功能全过”不是等价性证明。** 3 个随机种子、逐拍 bit 一致、零新增状态元，才足以支撑一次上板。

## 小结

这不是一个“自研 CPU”的故事，而是一个更常见、也更有借鉴意义的工程故事：**在一个成熟的开源乱序核上，怎样把最后一条不收敛的路径解决掉，并且证明你没有改坏它。**

最终结果是 105.887 MHz、setup WNS +0.035 ns、功能 58/58、性能 20/20、System counter ratio 3.817781304。数字属于整个团队和基座；本文拆解的是其中 S3v2 那 42 行。

基座与许可证声明、设计报告、20 项明细和实验线历史都在 [nscscc2026-loongarch-cpu](https://github.com/LiaoZiqi-GZFLS/nscscc2026-loongarch-cpu)。如果你也在做 FPGA 上的乱序核，希望这篇能帮你少走一点“功能过了但时序不收敛”的弯路。