把开源乱序核推到 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 影响:
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。时序报告里最差的路径很集中:
ROB 重定向判定(提交段)
↓ 同拍组合穿透
nextPC 三候选选择
↓
ICache 读地址
也就是说,后端在提交段发现分支误预测,重定向信号同拍穿过前端的候选选择逻辑,直接抵达 ICache 的读地址。功能上这是对的,但在 105 MHz 下,逻辑级数加上布线延迟,这条路径无法收敛。
这不是一个“再加一级流水”就能随便解决的问题。乱序核的重定向路径本来就是关键路径:它决定了误预测后多久能重新取指。随便加寄存器可能改变握手语义,导致漏冲或重复冲刷。我们需要的是只改时序、不改值和次序的重构。
S3v2:把组合穿透改成寄存器切片
基座的重定向接口叫 backendJumpInterface,原来用 s2mPipe() 做流水化。S3v2 把它改成 m2sPipe():
// 改动示意(ProgramCounterPlugin.scala)
backendJumpInterface
.m2sPipe() // 原为 s2mPipe()
在 SpinalHDL 的 Stream 语义里,这两个算子都是协议保持的流水化原语,区别在于寄存器切片放在接口的哪一侧。换成 m2sPipe() 之后,重定向锥到 nextPC/ICache 读地址的组合链路被寄存器化斩断,跨阶段组合桥变成干净的寄存器到寄存器路径。
但只改这一行还不够。m2sPipe() 让重定向晚一拍到达,取指重启和冲刷配对出现了同拍依赖。如果不同步冲刷流水寄存器里的旧路径指令,就会漏冲一条。最终提交版在功能适配基线之上的网表差异只有 42 行,其中另一行关键改动是:
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 加法器之后:各候选地址并行完成全部计算,晚到的选择信号到达后只剩一级独热多选。
等价性依据是重结合恒等式:
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 访存的绝对延迟不随频率下降,它的相对占比随频率上升,最终决定了频率收益的上界窗口。
下一步:四发射与更好的预测器
设计报告里记录了两个已经在实验线完成等价性实证、但未并入最终提交网表的方向:
- 压缩队列的循环队列重构:当前 IntIQ 的压缩位移链(
queue_0 → queue_1的寄存器到寄存器搬移)在 105 MHz 下是四发射扩展的架构级瓶颈;扇出/组合级修复在原理上不可收敛,唯一正解是循环队列。实测收益锚点是 3.875。 - S3v2 + mini-TAGE:把更强的预测器与已经收敛的前端时序结合,待频率余量重建后合入。
这两个方向都没有进入最终提交版。比赛提交的是稳定、可验证的那一版,实验线保留在仓库历史里,供赛后继续。
可迁移的教训
这次工作留下三条比分数更重要的经验:
- 先定位唯一不收敛的锥,再做时序重构。 105 MHz 下最差路径只有一条,改一条比改十条有效。
- 协议保持的重构,等价性由协议担保,但边界条件要自己验证。
m2sPipe()是标准算子,但它带来的 +1 拍延迟会制造新的同拍依赖;flushIt那一行才是功能等价的关键。 - “功能全过”不是等价性证明。 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。如果你也在做 FPGA 上的乱序核,希望这篇能帮你少走一点“功能过了但时序不收敛”的弯路。