VexRiscv 体系架构分析
VexRiscv 体系架构分析
VexRiscv 用 SpinalHDL 实现,核心设计思想是“极度可参数化 + 插件化”:
CPU核心本身只是一个 5 级(可裁剪)的in‑order流水线骨架;绝大多数功能(PC 管理、寄存器文件、取指/访存接口、hazard 逻辑、算术单元、CSR/异常、MMU/PMP、FPU、调试等)都以插件(Plugin)实现并在CPU实例化时组合。- 如果不装任何
plugin,生成的就是仅含流水线阶段与基本仲裁的空壳CPU;所有功能均通过插件插入pipeline的不同stage来实现。
流水线与数据流
流水线
通常的 5 个阶段:
- Prefetch / Fetch
- Decode
- Execute
- Memory
- WriteBack
流水线是 in‑order,插件可以在任意 stage 插入/读取信号,工具链自动为跨阶段信号做必要的寄存/转发(automatic pipelining)。
数据流
指令流典型路径:
i.IBus 插件负责取指(简单接口或带 cache 的接口,通过不同 IBus*Plugin 提供)。
ii.Decoder 插件在 Decode 阶段解码指令并产生控制信号、指示寄存器使用/写回、bypassable 标志等。
iii.SrcPlugin 负责生成常用的 SRC1/SRC2/SRC_ADD/SRC_SUB/SRC_LESS 等运算数(可以选择在 Decode 或 Execute 生成,以折中 bypass 网络/时序)。
iv.Execute 阶段由 ALU/Branch/Shifter/Mult/Div/FPU 等插件计算,结果按插件配置注入到后续 stage(Execute/Memory/WriteBack)。
v.DBus 插件处理负载/存储访问(simple 或 cached)。
vi.RegFilePlugin 在 WriteBack(或配置的阶段)写回结果。
插件系统(Plugin)
VexRiscv围绕流水线的各阶段来构建,每个功能对象可以以插件的形式随意添加。
Pipeline
“一个Pipeline对象有很多个stage。叫做Stageable的元素可以被一个stage传递到下一个stage。插件会被整合进整个流水线,它们可以给每个stage添加逻辑,可以使用其中一个Stageable作为流水线上某个特定stage的输入,可以插入新的Stageable到流水线的下一个stage。
Pipeline对象会自动负责管理这些stageable。如果一个插件在EXECUTE阶段需要stagealbe OP_A,而OP_A是DECODE阶段由其它插件插入流水线的,随后Pipline对象会确保OP_A沿着流水线传递到DECODE阶段。
如果插件在EXECUTE阶段产生了一个stageable RESULT,随后需要将它导入WRITEBACK阶段,流水线阶段会再一次确保它能被送过去,无论在WRITEBACK和EXECUTE之间有多少中间stage。”
—-CPU设计的新思路 “The VexRiscv CPU - A New Way to Design”
在VexRiscv中,流水线阶段的描述如下:
1 | |
VexRiscv是Component(SpinalHDL的一个原语,等价于一个Verilog的module)的一个带有Pipeline字段的子类。CPU中一定有decode和execute这两个stage,而访存和写回stage是可选的,主要看你期望的配置。stage的顺序由newStage()的调用顺序决定。一旦定义好了CPU的各个stage,就可以通过插件来向流水线添加逻辑了。
关于CPU的文件统一都放在了路径https://github.com/SpinalHDL/VexRiscv/tree/master/src/main/scala/vexriscv/plugin下。
实例化
1 | |
这是源码demo中最小的CPU配置,所需的插件被添加到config的plugins中,必须有这几个基本插件才能构成完整的CPU。
然后实现实例化:
1 | |
如果我们想添加乘法指令拓展,只需要在plugins中new一个乘法插件。
VexRiscv的顶层文件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class VexRiscv(val config : VexRiscvConfig) extends Component with Pipeline{
type T = VexRiscv
import config._
//Define stages
def newStage(): Stage = { val s = new Stage; stages += s; s }
val decode = newStage()
val execute = newStage()
val memory = ifGen(config.withMemoryStage) (newStage())
val writeBack = ifGen(config.withWriteBackStage) (newStage())
def stagesFromExecute = stages.dropWhile(_ != execute)
plugins ++= config.plugins
...
}
“代码中可以看到,顶层是带有Pipeline特性的Component。带上Pipeline这个特性,就表示你的类里面会有两个默认的列表:stages和plugins。从顶层中可以发现,我们通过newStage例化的阶段,都被放进了stages列表里,配置中的插件列表被追加到了plugins里。
Pipeline特性里还实现了一个build函数,它会在Component被编译的时候自动调用。Pipeline的build函数先遍历plugins里的插件并调用它们的build函数,这时能统计出一些有用的信息,然后根据这些信息进行流水线的自动构建。”
在插件中并没有创建任何一个寄存器,所有赋值都是使用Stageable完成,所有的寄存器都将在最后由程序自动生成。
1 | |
把所有Stageable的insert input output信息汇总以后,才能得到该Stageable代表的寄存器组的始末位置。
1 | |
在Stage类中,insert input output这三个方法做了两件事:
- 1.把输入的Stageable 解包,返回它包着的硬件原语,然后就可以对该原语赋值或取值。这就是我们在插件中对Stageable的操作都需要调用这三个方法的原因。
- 2.它会把该Stageable 的信息存到对应的三个表里面,为后续的自动构建做准备。
TLB
MemoryTranslatorPlugin
在 VexRiscv 的源码中,MemoryTranslator 并不是一个独立的 Plugin 类,而是一个 Service (服务/接口) 定义。
- 代码定位:位于
vexriscv.plugin包中 - 作用:它定义了“地址翻译”这一行为的标准协议。
- 实现逻辑:
- 它不包含具体的翻译逻辑(如页表查找),而是定义了输入(虚拟地址、访问类型)和输出(物理地址、允许访问、异常信号)。
- 解耦:其他的插件(如取指单元
IBus、加载存储单元DBus/LSU)只需要依赖MemoryTranslator接口,而不需要知道底层是否有 MMU。如果有 MMU,MmuPlugin会提供这个服务;如果没有,可能是直连(Identity Map)。
MmuPlugin
这是 MMU 的核心硬件实现,它实现了 MemoryTranslator 接口。
- 代码定位:
vexriscv.plugin.MmuPlugin - 实现 RISC-V 标准的32位系统分页机制。
- 主要部件:
- TLB (Translation Lookaside Buffer):快表,缓存页表项。
- CSR 管理:管理 satp (Supervisor Address Translation and Protection) 寄存器。
- 权限检查:根据当前特权级 (Machine/Supervisor/User) 和页表项的 R/W/X 位检查权限。
总览:
i.虚拟地址:VPN1(31-22位) + VPN0(21-12位) + Offset(11-0位)
ii.两组翻译端口:
1. Instruction Port:服务于 IBus(取指)
2. Data Port:服务于 LSU/DBus(数据读写)
每个端口包含:
- virtualAddress: 输入的虚拟地址
- physicalAddress: 输出的物理地址
- allowRead/Write/Execute: 权限允许信号
- exception: 是否发生 Page Fault 或 Access Fault
- refill: 用于写入 TLB 条目的接口
iii.TLB 查找:
1. Tag 比较:将虚拟地址的高位(VPN, Virtual Page Number) VPN1(31-22) / VPN0(21-12) 与 TLB CacheLine 中存储的 virtualAddress 进行比较
2. Hit 判断:line.valid(条目有效) + VPN 匹配
3. 物理地址拼接:physicalAddress = PPN1 @@ (superPage ? addr(21-12) : PPN0) @@ addr(11-0) 物理页号(PPN)+ 页内偏移(Offset)
iv.权限检查
v.异常生成
代码
1.包/导入1
2
3
4
5
6
7package vexriscv.plugin
import vexriscv.{VexRiscv, _}
import spinal.core._
import spinal.lib._
import scala.collection.mutable.ArrayBuffer
2.硬件接口定义
数据总线访问接口:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 抽象接口:定义获取DBus访问接口的方法
trait DBusAccessService{
def newDBusAccess() : DBusAccess
}
// 数据总线访问命令(写/读请求)
case class DBusAccessCmd() extends Bundle {
val address = UInt(32 bits) // 32位地址
val size = UInt(2 bits) // 访问大小
val write = Bool // 读写标识 True=写 False=读
val data = Bits(32 bits) // 写数据 32位比特流
val writeMask = Bits(4 bits) // 写掩码(1 → 写入)
}
// 数据总线响应(读结果/错误)
case class DBusAccessRsp() extends Bundle {
val data = Bits(32 bits) // 读返回数据
val error = Bool() // 错误标志
val redo = Bool() // 重发请求标志
}
// 整合命令+响应的总线接口
case class DBusAccess() extends Bundle {
val cmd = Stream(DBusAccessCmd()) // 带握手的命令流(valid/ready)
val rsp = Flow(DBusAccessRsp()) // 单向响应流(仅valid)
}
MMU 端口配置:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 定义端口优先级
object MmuPort{
val PRIORITY_DATA = 1 // 数据端口优先级
val PRIORITY_INSTRUCTION = 0 // 指令端口优先级
}
// MMU端口配置
case class MmuPortConfig(
portTlbSize : Int, // 该端口TLB大小
latency : Int = 0, // 延迟(默认0)
earlyRequireMmuLockup : Boolean = false, // 提前检查MMU锁定
earlyCacheHits : Boolean = false // 提前检查TLB命中
)
// MMU端口结构体(总线/优先级/配置/ID)
case class MmuPort(bus : MemoryTranslatorBus, priority : Int, args : MmuPortConfig, id : Int)
3.MmuPlugin 主类
1 | |
实现地址翻译端口创建(接口重写):1
2
3
4
5
6
7// 重写地址翻译器接口:创建新的MMU端口
override def newTranslationPort(priority : Int,args : Any): MemoryTranslatorBus = {
val config = args.asInstanceOf[MmuPortConfig] // 类型转换
val port = MmuPort(MemoryTranslatorBus(...), priority, config, portsInfo.length)
portsInfo += port // 加入端口列表
port.bus
}
插件初始化(setup):1
2
3
4
5
6
7
8
9override def setup(pipeline: VexRiscv): Unit = {
// 1. 配置SFENCE_VMA指令(TLB刷新):解码器识别该指令时标记IS_SFENCE_VMA2=True
val decoderService = pipeline.service(classOf[DecoderService])
decoderService.addDefault(IS_SFENCE_VMA2, False)
decoderService.add(SFENCE_VMA, List(IS_SFENCE_VMA2 -> True))
// 2. 初始化数据总线访问接口
dBusAccess = pipeline.service(classOf[DBusAccessService]).newDBusAccess()
}
硬件逻辑构建(build)
生成实际的硬件电路,分多个子模块:
①TLB 缓存行定义1
2
3
4
5
6case class CacheLine() extends Bundle {
val valid, exception, superPage = Bool // 有效/异常/大页标志
val virtualAddress = Vec(UInt(10 bits), UInt(10 bits)) // 虚拟页号(VPN1/VPN0)
val physicalAddress = Vec(UInt(10 bits), UInt(10 bits)) // 物理页号(PPN1/PPN0)
val allowRead, allowWrite, allowExecute, allowUser = Bool // 访问权限
}
- 对应 RISC-V Sv32 的页表项(PTE),是 TLB 缓存的最小单元;
- Vec(UInt(10 bits), 2):硬件数组,存储 10 位的 VPN1/VPN0。
②CSR 寄存器管理1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19val csr = pipeline plug new Area{
// MSTATUS寄存器字段(sum=用户访问监督页,mxr=读执行页,mprv=修改特权级)
val status = new Area{
val sum, mxr, mprv = RegInit(False) // 寄存器,初始值False
mprv clearWhen(csrService.xretAwayFromMachine) // 退出机器模式时清零
}
// SATP寄存器(地址转换配置)
val satp = new Area {
val mode = RegInit(False) // 地址转换模式(False=禁用,True=启用Sv32)
val asid = Reg(Bits(9 bits)) // 地址空间ID
val ppn = Reg(UInt(22 bits)) // 页目录基址
}
// 注册CSR读写映射(MSTATUS的19位对应mxr,18位对应sum...)
for(offset <- List(CSR.MSTATUS, CSR.SSTATUS))
csrService.rw(offset, 19 -> status.mxr, 18 -> status.sum, 17 -> status.mprv)
csrService.rw(CSR.SATP, 31 -> satp.mode, 22 -> satp.asid, 0 -> satp.ppn)
}
- RegInit(False):硬件寄存器(掉电消失);
- csrService.rw:把 MMU 相关的 CSR 字段(sum/mxr/satp)映射到 RISC-V 标准 CSR 地址,实现寄存器读写。
③TLB 命中检查 + 地址翻译1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// 遍历所有MMU端口,为每个端口生成TLB逻辑
val ports = for (port <- sortedPortsInfo) yield new Area {
val cache = Vec(Reg(CacheLine()) init, port.args.portTlbSize) // 该端口TLB缓存(数组)
// 判定是否需要MMU翻译(组合逻辑:输入变,输出立刻变)
val requireMmuLockupCalc = virtualRange(addr) && !bypass && csr.satp.mode
// 机器模式下禁用MMU(如果配置了enableMmuInMachineMode=false)
if(!enableMmuInMachineMode) {
requireMmuLockupCalc clearWhen(!csr.status.mprv && privilegeService.isMachine())
}
// TLB命中检查:遍历所有TLB行,检查“有效 + VPN匹配”
val cacheHitsCalc = B(cache.map(line =>
line.valid &&
line.virtualAddress(1) === addr(31 downto 22) && // VPN1匹配(31-22位)
(line.superPage || line.virtualAddress(0) === addr(21 downto 12)) // 大页跳过VPN0,小页匹配VPN0
))
val cacheHit = cacheHits.asBits.orR // 任意一行命中则为True
val cacheLine = MuxOH(cacheHits, cache) // 选择命中的TLB行(硬件多路器)
// 地址翻译:物理地址 = PPN1 + (大页/VPN0 | 小页/PPN0) + 页内偏移
port.bus.rsp.physicalAddress := cacheLine.physicalAddress(1) @@
(cacheLine.superPage ? addr(21 downto 12) | cacheLine.physicalAddress(0)) @@
addr(11 downto 0)
// 权限检查:例如port.bus.rsp.allowRead = 缓存行允许读 or mxr=1且允许执行
port.bus.rsp.allowRead := cacheLine.allowRead || csr.status.mxr && cacheLine.allowExecute
// 异常检查:比如用户模式访问监督页且sum=0 → 触发异常
port.bus.rsp.exception := ...
}
- ===:硬件相等比较
- @@:位拼接
- Vec(Reg(CacheLine()), N):N 个 CacheLine 寄存器组成的 TLB 缓存(硬件数组)
④TLB 填充状态机(TLB miss 时的页表遍历)
当 TLB 未命中时,需要从内存中读页表(二级页表)并填充 TLB,这部分用状态机实现:
1 | |