VexRiscv 体系架构分析

VexRiscv 体系架构分析

VexRiscvSpinalHDL 实现,核心设计思想是“极度可参数化 + 插件化”:

  • 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_ADECODE阶段由其它插件插入流水线的,随后Pipline对象会确保OP_A沿着流水线传递到DECODE阶段。

如果插件在EXECUTE阶段产生了一个stageable RESULT,随后需要将它导入WRITEBACK阶段,流水线阶段会再一次确保它能被送过去,无论在WRITEBACKEXECUTE之间有多少中间stage。”

—-CPU设计的新思路 “The VexRiscv CPU - A New Way to Design”

在VexRiscv中,流水线阶段的描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class 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())

/*
object ifGen {
def apply[T](cond: Boolean)(block: => T): T = if (cond) block else null.asInstanceOf[T]
}

https://github.com/SpinalHDL/SpinalHDL/blob/b3623617a34c3c0127589b53cbcd2e743b308c06/core/src/main/scala/spinal/core/Misc.scala#L532
*/
...

VexRiscvComponentSpinalHDL的一个原语,等价于一个Verilogmodule)的一个带有Pipeline字段的子类。CPU中一定有decodeexecute这两个stage,而访存和写回stage是可选的,主要看你期望的配置。stage的顺序由newStage()的调用顺序决定。一旦定义好了CPU的各个stage,就可以通过插件来向流水线添加逻辑了。

关于CPU的文件统一都放在了路径https://github.com/SpinalHDL/VexRiscv/tree/master/src/main/scala/vexriscv/plugin下。

实例化

GenCustomCsr

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
config = VexRiscvConfig(
plugins = List(
new CustomCsrDemoPlugin,
new CsrPlugin(CsrPluginConfig.small),
new CustomCsrDemoGpioPlugin,
new IBusSimplePlugin(
resetVector = 0x00000000l,
cmdForkOnSecondStage = false,
cmdForkPersistence = false,
prediction = NONE,
catchAccessFault = false,
compressedGen = false
),
new DBusSimplePlugin(
catchAddressMisaligned = false,
catchAccessFault = false
),
new DecoderSimplePlugin(
catchIllegalInstruction = false
),
new RegFilePlugin(
regFileReadyKind = plugin.SYNC,
zeroBoot = false
),
new IntAluPlugin,
new SrcPlugin(
separatedAddSub = false,
executeInsertion = false
),
new FullBarrelShifterPlugin,
new HazardSimplePlugin(
bypassExecute = true,
bypassMemory = true,
bypassWriteBack = true,
bypassWriteBackBuffer = true,
pessimisticUseSrc = false,
pessimisticWriteRegFile = false,
pessimisticAddressMatch = false
),
new BranchPlugin(
earlyBranch = false,
catchAddressMisaligned = false
),
new YamlPlugin("cpu0.yaml")
)
)

这是源码demo中最小的CPU配置,所需的插件被添加到configplugins中,必须有这几个基本插件才能构成完整的CPU

然后实现实例化:

1
val cpu = new VexRiscv(config)

如果我们想添加乘法指令拓展,只需要在plugins中new一个乘法插件。

VexRiscv的顶层文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class 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这个特性,就表示你的类里面会有两个默认的列表:stagesplugins。从顶层中可以发现,我们通过newStage例化的阶段,都被放进了stages列表里,配置中的插件列表被追加到了plugins里。

Pipeline特性里还实现了一个build函数,它会在Component被编译的时候自动调用。Pipelinebuild函数先遍历plugins里的插件并调用它们的build函数,这时能统计出一些有用的信息,然后根据这些信息进行流水线的自动构建。”

在插件中并没有创建任何一个寄存器,所有赋值都是使用Stageable完成,所有的寄存器都将在最后由程序自动生成。

1
class Stageable[T <: Data](_dataType : => T) extends HardType[T](_dataType) with Nameable{}

把所有Stageableinsert input output信息汇总以后,才能得到该Stageable代表的寄存器组的始末位置。

1
2
3
4
5
6
7
8
9
10
class Stage() extends Area{
//...
val inputs = mutable.LinkedHashMap[Stageable[Data],Data]()
val outputs = mutable.LinkedHashMap[Stageable[Data],Data]()
val inserts = mutable.LinkedHashMap[Stageable[Data],Data]()
//...
def input [T <: Data](key : Stageable[T]) : T = {...}
def output[T <: Data](key : Stageable[T]) : T = {...}
def insert[T <: Data](key : Stageable[T]) : T = {...}
}

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
7
package 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
2
3
4
5
6
7
8
9
10
11
12
13
// 插件类:继承VexRiscv插件接口 + 实现地址翻译器接口
class MmuPlugin(
var ioRange : UInt => Bool, // 判定IO地址的函数(输入地址→布尔值)
virtualRange : UInt => Bool = address => True, // 默认所有地址都是虚拟地址
enableMmuInMachineMode : Boolean = false, // 机器模式是否启用MMU
exportSatp: Boolean = false // 是否导出satp寄存器
) extends Plugin[VexRiscv] with MemoryTranslator {

// 成员变量:数据总线接口 + MMU端口列表
var dBusAccess : DBusAccess = null
val portsInfo = ArrayBuffer[MmuPort]() // 可变数组,存所有MMU端口

...

实现地址翻译端口创建(接口重写):

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
9
override 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
6
case 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
19
val 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
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
val shared = new Area {
// 状态枚举:IDLE(空闲) → L1_CMD(发一级页表请求) → L1_RSP(等响应) → L0_CMD(发二级页表请求) → L0_RSP(等响应)
val State = new SpinalEnum{
val IDLE, L1_CMD, L1_RSP, L0_CMD, L0_RSP = newElement()
}
val state = RegInit(State.IDLE) // 状态寄存器,初始IDLE

// 状态机逻辑
switch(state){
is(State.IDLE){
// 检测到TLB未命中 → 记录虚拟页号,进入L1_CMD
when(refills.orR){
vpn(1) := addr(31 downto 22) // 保存VPN1
vpn(0) := addr(21 downto 12) // 保存VPN0
state := State.L1_CMD
}
}
is(State.L1_CMD){
// 配置DBus:访问一级页表(地址=satp.ppn + VPN1 + 00)
dBusAccess.cmd.valid := True
dBusAccess.cmd.address := csr.satp.ppn(19 downto 0) @@ vpn(1) @@ U"00"
// 总线就绪后,进入L1_RSP等待响应
when(dBusAccess.cmd.ready){
state := State.L1_RSP
}
}
// ... L1_RSP/L0_CMD/L0_RSP逻辑(读一级页表→读二级页表→填充TLB)
}
}


VexRiscv 体系架构分析
http://ruak.github.io/2026/01/27/VexRiscv-体系架构分析/
作者
HUANGDAN
发布于
2026年1月27日
许可协议