从零开始造一颗 RISC-V CPU(一):项目总览与流水线基础
- Architecture
- 11小时前
- 9 Views
- 0 Comments
- 2675 Words
从零开始造一颗 RISC-V CPU(一):项目总览与流水线基础
系列博客第 1 篇 —— 介绍整个项目的动机、架构全貌、8 级流水线设计以及关键的 hazard 处理。
项目地址:https://github.com/HaibinLai/simple-CPU/tree/main/docs
为什么要自己造 CPU?
每年的计算机组成原理课,都有一个调bug到想死的大作业:用 Verilog 实现一颗 RISC-V CPU。今天我决定试试一个不一样的方式——让 AI agent来主导整个 CPU 的设计与实现,我负责提出需求、审查代码、做关键决策。
从最基础的5级流水线开始,agent 逐步迭代出了 8 级双发射流水线、TAGE 分支预测、2-way set-associative Cache,以及完整的 Tomasulo+ROB 乱序执行引擎。所有 RTL 代码、测试脚本、benchmark 汇编程序,均由 agent 编写,我来指定方向和验收结果。
最终的成果是一颗 4,754 行 RTL 的 dual-issue superscalar CPU:
| 特性 | 实现 |
|---|---|
| ISA | RV32I(40 条指令) |
| 流水线 | 8 级:IF → ID1 → ID2 → EX1 → EX2 → AGU → MEM → WB |
| 发射宽度 | 2-wide(slot0 全功能 + slot1 ALU/load/store/branch) |
| 分支预测 | TAGE-2L + 2-way BTB |
| Cache | I$/D$ 各 2KB,2-way set-associative,4 words/line |
| MMIO | UART TX @ 0x10000000 |
| 验证 | 15 benchmarks + 2000 case 指令测试 + micro tests 全 PASS |
这个系列将记录整个设计过程中的关键决策和知识点。第一篇从全局视角出发,介绍项目的架构全貌和流水线基础。
人机协作模式
整个开发过程采用了一种"人类架构师 + AI 工程师"的协作模式:
| 角色 | 职责 |
|---|---|
| 我(人类) | 提出功能需求、决定架构方向、审查关键设计、跑测试验收 |
| Agent(GitHub Copilot) | 编写全部 Verilog RTL、Python 工具链、测试用例、debug 修复 |
典型的工作流是这样的:我说"把 BPU 升级到 TAGE",agent 就会分析现有代码、设计 TAGE 结构、写出 RTL、连线到 cpu_top、跑回归测试确认 PASS。如果测试失败,agent 自己分析波形和日志定位问题并修复,直到全部通过。
这种模式下,agent 的代码产出速度非常快(几分钟内可以完成一个完整 feature),但架构决策的质量仍然取决于人类的判断——比如选择 Tomasulo 而非 Scoreboard、RS 深度设为 8 而非 4、TAGE 用 2 级而非 3 级,这些 tradeoff 需要对体系结构有理解才能做好。
项目整体结构
cpu20/
├── rtl/
│ ├── core/ # CPU 核心逻辑
│ │ ├── cpu_top.v # 顶层流水线(1,436 行)
│ │ ├── bpu.v # 分支预测单元 TAGE-2L(394 行)
│ │ ├── control.v # 译码与控制信号
│ │ ├── forwarding.v # 前递网络
│ │ ├── hazard.v # 冒险检测
│ │ ├── ifq.v # 指令取指队列(186 行)
│ │ ├── alu.v # 32-bit ALU
│ │ ├── imm_gen.v # 立即数生成
│ │ └── ...
│ └── mem/
│ ├── imem.v # I-Cache 2-way SA
│ └── dmem.v # D-Cache 2-way SA, dual-port
├── tb/
│ ├── tb_cpu.v # Testbench(统计 CPI/分支/Cache 等)
│ └── programs/ # 测试程序 .hex
├── tools/
│ ├── asm.py # 汇编器(支持 label、putchar)
│ ├── gen_benchmarks.py # Benchmark 生成
│ └── run_benchmarks.py # 自动化运行与统计
└── docs/
整个项目使用 Icarus Verilog 仿真,Python 生成测试用例和收集性能数据,开发全程在 macOS 上完成。所有代码由 AI agent 在 VS Code 中通过 GitHub Copilot agent mode 编写和调试。
选择 RV32I
RISC-V 是一个开放的指令集架构。选择 RV32I base integer 子集,是因为它:
- 足够小:只有 40 条指令,decode 逻辑简洁
- 足够完整:支持 ALU、分支、load/store、跳转,能运行任意整数程序
- 正交设计:opcode 字段对齐规整,非常适合硬件实现
RV32I 的指令格式一共 6 种(R/I/S/B/U/J),每条指令固定 32 位宽。这对流水线的 decode 阶段非常友好——不需要像 x86 那样处理变长指令。
R-type: [funct7 | rs2 | rs1 | funct3 | rd | opcode]
I-type: [ imm[11:0] | rs1 | funct3 | rd | opcode]
S-type: [imm[11:5]| rs2 | rs1 | funct3 | imm[4:0] | opcode]
B-type: [imm bits | rs2 | rs1 | funct3 | imm bits | opcode]
U-type: [ imm[31:12] | rd | opcode]
J-type: [ imm bits | rd | opcode]
在 defines.v 中,所有 opcode 都被定义为宏常量:
`define OP_LUI 7'b0110111
`define OP_AUIPC 7'b0010111
`define OP_JAL 7'b1101111
`define OP_JALR 7'b1100111
`define OP_BRANCH 7'b1100011
`define OP_LOAD 7'b0000011
`define OP_STORE 7'b0100011
`define OP_IMM 7'b0010011
`define OP_REG 7'b0110011
`define OP_SYSTEM 7'b1110011
8 级流水线
经典教科书上的 RISC 流水线是 5 级(IF → ID → EX → MEM → WB)。
从 5 级到 8 级的演进
| 阶段 | 5 级 | 8 级 | 拆分原因 |
|---|---|---|---|
| IF | ✅ | ✅ | 取指 |
| ID | 1 级 | ID1 + ID2 | ID1 做指令缓冲和对齐(IFQ),ID2 做译码+读寄存器 |
| EX | 1 级 | EX1 + EX2 | 拆分 ALU 与地址生成,降低关键路径延迟 |
| MEM | 1 级 | AGU + MEM | 地址生成单独一级,MEM 纯做存储器访问 |
| WB | ✅ | ✅ | 写回 |
拆分的核心考虑是:
-
降低关键路径:5 级流水线的 EX 阶段同时做 ALU 计算和分支判断,路径太长。拆成 EX1(ALU + 分支)和 EX2(结果缓冲)后,单级延迟更短,有利于提高时钟频率。
-
为双发射铺路:ID1 引入 IFQ(指令取指队列),可以缓冲多条指令,为 ID2 阶段的双发射配对提供基础。IFQ 深度为 4,每周期最多 push 2 条、pop 2 条。
-
AGU 独立:地址生成(rs1 + offset)与数据存储器读写分离,使 store 数据的前递和 load 的符号扩展逻辑更清晰。
8 级流水线全景
┌─ slot0: EX1 → EX2 → AGU → MEM → WB
IF → ID1 → ID2(decode) ┤
└─ slot1: EX1b → WB (1-cycle shortcut)
- IF:PC 生成 + I-Cache 读取 + 分支预测查询
- ID1:指令推入 IFQ,BPU 结果跟随指令一起入队
- ID2:从 IFQ 弹出 1~2 条指令,译码、读寄存器、冒险检测、配对判定
- EX1:ALU 计算、分支/跳转解析、分支预测验证
- EX2:EX 结果缓冲,为 AGU 提供稳定的输入
- AGU:访存地址生成(load/store 的 base + offset)
- MEM:D-Cache 读写,load 数据符号/零扩展
- WB:结果写回寄存器堆
流水线的核心挑战:Hazard 处理
流水线的性能优势来自指令重叠执行,但这也引入了三类冒险(hazard):
1. 数据冒险(Data Hazard)
最常见的是 RAW(Read After Write) 冒险:后面的指令需要读取前面指令尚未写回的结果。
ADD x1, x2, x3 # 写 x1
SUB x4, x1, x5 # 读 x1 —— 此时 ADD 还在 EX 阶段,x1 尚未更新
前递(Forwarding)
解决方案是前递网络(forwarding.v):直接把 EX/AGU/MEM 阶段计算好的结果旁路回 ID 阶段使用的操作数,无需等到 WB 写回。
我们的前递网络有 4 级优先级,对 rs1 和 rs2 分别独立选择:
| 优先级 | 来源 | 信号前缀 |
|---|---|---|
| 最高 | EX1→EX2 级间 | ex_mem_* |
| 次高 | EX2→AGU 级间 | ex2_agu_* |
| 再次 | AGU→MEM 级间 | agu_mem_* |
| 最低 | MEM→WB 级间 | mem_wb_* |
// forwarding.v 中的优先级逻辑(rs1 为例)
if (ex_mem_reg_write && ex_mem_rd == id_rs1)
fwd_a = 2'b01; // 从 EX 前递
else if (ex2_agu_reg_write && ex2_agu_rd == id_rs1)
fwd_a = 2'b10; // 从 AGU 前递
else if (agu_mem_reg_write && agu_mem_rd == id_rs1)
fwd_a = 2'b11; // 从 MEM 前递
else
fwd_a = 2'b00; // 无前递,使用寄存器值
Load-Use Stall
有一种情况前递也解决不了:load-use 冒险。Load 指令的数据要到 MEM 阶段才能从 D-Cache 读出,但紧随其后的指令在 EX 阶段就需要用到这个值——差了一个周期,无法前递。
LW x1, 0(x2) # MEM 阶段才拿到 x1 的值
ADD x3, x1, x4 # EX 阶段就需要 x1 —— 必须 stall 1 拍
hazard.v 检测这种情况并插入一个 bubble:
// 两级 load-use 检测
wire hazard_id_ex = id_ex_mem_read
&& ((id_use_rs1 && id_ex_rd == id_rs1)
|| (id_use_rs2 && id_ex_rd == id_rs2));
wire hazard_ex_agu = ex_agu_mem_read
&& ((id_use_rs1 && ex_agu_rd == id_rs1)
|| (id_use_rs2 && ex_agu_rd == id_rs2));
assign stall = hazard_id_ex || hazard_ex_agu;
注意 id_use_rs1 / id_use_rs2 信号用于精确过滤——像 LUI 这种不使用源寄存器的指令不会触发虚假 stall。
2. 控制冒险(Control Hazard)
分支/跳转指令需要到 EX1 阶段 才能解析目标地址和方向。如果预测错误,已经进入流水线的后续指令都是无效的,需要 flush。
在 8 级流水线中,一次分支 misprediction 会浪费 3~4 个周期(从 IF 到 EX1 的流水线深度)。这就是为什么分支预测对性能至关重要——后续博客会详细讨论 TAGE 预测器。
3. 结构冒险(Structural Hazard)
当两条指令需要同时使用同一个硬件资源时产生。在我们的设计中:
- 寄存器堆:有足够多的读写端口,不会冲突
- D-Cache:提供双端口(Port A 读写 + Port B 只读),slot0 和 slot1 可以同时访存
- ALU:slot0 和 slot1 各有独立的 ALU 实例
因此结构冒险在本设计中基本不存在。
双发射(Dual-Issue)简介
单发射 CPU 的 IPC 上限是 1.0。要突破这个限制,需要每周期发射多条指令。我们的 CPU 在 ID2 阶段同时从 IFQ 弹出两条指令:
- Slot0(主通道):支持所有指令类型
- Slot1(辅助通道):支持 ALU、load、store、branch
配对条件由 ID2 阶段检查:
- Slot1 指令类型必须是 ALU/load/store/branch(不能是 JAL/JALR/ECALL)
- 两条指令之间无 RAW 依赖(slot0 的 rd ≠ slot1 的 rs1/rs2)
- 两条指令之间无 WAW 冲突(rd 不同)
- 没有 load-use 冒险阻塞
实测数据中,memcpy_64w(连续 LW/SW 对)的双发射率最高达 73%,而分支密集的 sum_1_to_100 只有 34%。详细的双发射设计和 pair-block 分析将在第 2 篇中展开。
Benchmark 一览
为了验证 CPU 的正确性和衡量性能,我们构建了一套完整的 benchmark 套件:
| Benchmark | 特点 | Cycles | CPI |
|---|---|---|---|
fib_20 |
纯寄存器循环 | 157 | 1.81 |
memcpy_64w |
连续 LW/SW | 401 | 1.53 |
bsort_16 |
嵌套循环 + 条件交换 | 1,515 | 1.80 |
crc32_64b |
位操作密集 | 4,348 | 1.86 |
dhrystone_lite |
字符串/函数调用/分支 | 7,546 | 1.84 |
coremark_lite |
链表 + 状态机 + CRC16 | 2,984 | 1.62 |
qsort_32 |
快排 + 显式栈 | 7,515 | 1.93 |
sieve_256 |
Eratosthenes 筛法 | 4,996 | 2.43 |
hello_uart |
MMIO 输出字符串 | 105 | 2.76 |
注:以上数据来自 main 分支(in-order dual-issue)。feature/ooo 分支(out-of-order)的 CPI 显著更低,6/9 benchmarks 达到 CPI < 1.0。
所有 benchmark 的 hex 文件由 Python 汇编器 (asm.py) 生成,支持 label 解析、li 伪指令、putchar/puts MMIO 输出等功能。
小结
这一篇介绍了项目的全貌:
- 8 级流水线:IF → ID1 → ID2 → EX1 → EX2 → AGU → MEM → WB
- 三类冒险处理:前递网络(4 级优先级)、load-use stall、分支 flush
- 双发射基础:slot0 全功能 + slot1 辅助通道
- 验证体系:15 benchmarks + 200 随机回归
后续文章将深入每个子系统:
| 篇目 | 主题 |
|---|---|
| 第 2 篇 | 双发射设计 — IFQ、配对规则、pair-block 分析 |
| 第 3 篇 | 分支预测 — 从 Bimodal 到 TAGE |
| 第 4 篇 | Cache 层次 — 2-way SA I$/D$ |
| 第 5 篇 | 乱序执行 — Tomasulo + ROB |
| 第 6 篇 | 验证与性能 — Benchmark 结果分析 |
