资讯动态

RISC-V开发板实战:将Bao Hypervisor移植到RVA23的完整指南

发布时间:2026/9/26 9:08:16 来源:尧图企业网站定制
1. 从一块开发板说起为什么要折腾Bao到RVA23第一次拿到 Banana Pi BPI-SM10 这块板子的时候我盯着它看了很久。RISC-V 架构、RVA23 指令集规范、多核 SMP 设计这些标签堆在一起意味着它和市面上常见的 ARM 开发板完全不是一回事。而 Bao 这个轻量级 Hypervisor原本在 ARM 和 RISC-V 的旧版规范上跑得挺稳现在要把它挪到 RVA23 上中间要填的坑比想象中多得多。先说清楚这件事的价值在哪里。Bao 是一个静态分区 Hypervisor代码量极小通常只有几千行专门为实时和安全关键场景设计。它的核心思路是把物理硬件资源静态划分给不同的虚拟机每个虚拟机独占自己的 CPU 核、内存区域和外设彼此之间零干扰。这种设计在工业控制、汽车电子、航空航天领域非常吃香因为确定性比什么都重要。而 RVA23 是 RISC-V 指令集的一个新 Profile强制要求了向量扩展、Hypervisor 扩展、位操作扩展等一堆特性这意味着硬件能力更强了但软件适配的复杂度也上去了。Banana Pi BPI-SM10 这块板子用的是 SpacemiT K1 芯片8 核 RISC-V支持 RV64GCVB 指令集基本对齐 RVA23 的要求。它板载 8GB LPDDR4 内存、eMMC 存储、千兆网口、USB 3.0 和一堆 GPIO接口丰富程度在 RISC-V 开发板里算得上第一梯队。但问题在于Bao 官方仓库里对 RISC-V 的支持主要停留在 RV64GC 层面RVA23 新增的那些强制扩展比如 Zicbom、Zihintpause、Zawrs在 Bao 的底层代码里根本没有对应的处理逻辑。所以这个项目的本质是把一个为旧规范写的 Hypervisor移植到一个新规范强制要求更多特性的硬件平台上。这不是简单的编译通过就完事涉及到启动流程重写、中断控制器适配、内存管理单元配置、多核启动同步等一系列底层改动。适合谁来参考如果你正在做 RISC-V 底层开发、Hypervisor 移植、或者嵌入式实时系统这篇内容应该能帮你省下不少查手册和调试的时间。如果你只是好奇 RISC-V 开发板能玩什么也可以看看整个流程长什么样心里有个数。2. 动手之前Bao 的架构底子和 RVA23 的硬性门槛2.1 Bao 的分区模型到底怎么工作的Bao 的架构可以用一句话概括它把硬件资源切成若干块每块给一个虚拟机然后自己退居幕后只负责最基础的隔离和调度。具体来说Bao 启动后会先接管所有 CPU 核然后根据配置文件把核分配给不同的 VM。每个 VM 看到的是一个虚拟化的 CPU 视图但实际上它运行的物理核是独占的不需要上下文切换。内存方面Bao 用二级页表做地址翻译每个 VM 有自己的页表物理内存被静态划分VM 之间无法越界访问。这种设计的好处是确定性极强。因为没有动态资源调度VM 的运行时延可以精确预测这对实时系统来说是刚需。但代价是灵活性差资源划分必须在启动前确定运行中不能改。Bao 的代码结构也反映了这个思路核心文件只有几个bao.c负责初始化vm.c管理虚拟机生命周期mmu.c处理页表plic.c或gic.c处理中断。移植工作的重点就是把这些文件里和硬件强相关的部分改成适配 RVA23 和 K1 芯片的实现。2.2 RVA23 到底新增了哪些必须处理的东西RVA23 不是一个具体的芯片而是一个指令集 Profile定义了软件可以依赖的最小特性集。对于 Bao 移植来说以下几个扩展是必须关注的H 扩展Hypervisor 扩展提供两级地址翻译和虚拟中断注入。Bao 本身就是 Hypervisor这个扩展是基础中的基础。RVA23 强制要求 H 扩展意味着 K1 芯片必须支持Bao 也必须用起来。Zicbom缓存块操作指令用于管理非一致性内存的缓存。在多核系统中VM 之间的内存共享需要缓存维护这个扩展提供了标准指令。Zihintpause暂停提示指令用于自旋锁等场景减少忙等带来的功耗和总线压力。Zawrs等待保留集指令用于实现高效的等待-通知机制替代传统的轮询。V 扩展向量扩展RVA23 强制要求。虽然 Bao 本身可能不用向量指令但 VM 里的应用可能会用Bao 需要确保上下文切换时向量寄存器的保存和恢复是正确的。这些扩展在旧版 RISC-V 规范里是可选的Bao 的代码可能没有完整处理。比如 Zicbom 的缓存维护指令如果 Bao 在 VM 切换时没有正确执行就会导致缓存不一致VM 看到脏数据。再比如 Zawrs如果 Bao 的锁实现没有用这个指令在多核高负载下会出现严重的性能下降。2.3 K1 芯片的硬件细节和坑点SpacemiT K1 是一颗 8 核 RISC-V 芯片具体配置是 4 个高性能核加 4 个能效核类似 ARM 的 big.LITTLE 架构。但 RISC-V 的核间差异没有 ARM 那么大主要是频率和缓存配置不同。对于 Bao 来说这意味着核的启动顺序和初始化流程需要区分处理不能一刀切。中断控制器方面K1 用的是 PLIC平台级中断控制器加 CLINT核心本地中断器的组合。PLIC 负责外部中断的分发CLINT 负责定时器和核间中断。Bao 需要正确配置 PLIC 的优先级和阈值确保 VM 的中断不会互相干扰。这里有个容易忽略的点PLIC 的上下文编号和核的对应关系是硬件固定的移植时必须查手册确认不能想当然。内存映射方面K1 的物理地址空间布局和 QEMU 模拟的 virt 机器不同。比如 UART 的基地址、PLIC 的基地址、CLINT 的基地址都需要根据实际硬件修改。Bao 的配置文件里通常有这些地址的定义移植时第一件事就是把这些地址改对否则连串口输出都看不到。3. 移植实战从编译报错到串口出字3.1 工具链选择和第一次编译RISC-V 的工具链选择是个容易踩坑的地方。官方推荐用riscv64-unknown-elf-gcc或者riscv64-linux-gnu-gcc但两者有区别。前者是裸机工具链不带操作系统支持适合编译 Bao 这种裸机 Hypervisor后者是 Linux 工具链默认链接 Linux 的启动代码直接拿来编译 Bao 会出一堆链接错误。我一开始图省事用了 Linux 工具链结果编译出来的二进制文件在板子上根本跑不起来串口一点输出都没有。后来换成riscv64-unknown-elf-gcc配合-marchrv64gcv_zicbom_zihintpause_zawrs和-mabilp64d参数才编译通过。这里要注意-march参数必须包含 RVA23 要求的所有扩展否则编译器生成的代码可能缺少必要的指令运行时直接非法指令异常。编译命令大概长这样make CROSS_COMPILEriscv64-unknown-elf- \ ARCHriscv \ PLATFORMbanana-pi-sm10 \ CONFIG_RVA23y如果 Bao 的 Makefile 里没有现成的平台配置需要自己加一个。主要改的是platforms目录下的配置文件包括内存基地址、串口地址、PLIC 地址、CLINT 地址这些。3.2 启动流程重写从 _start 到 mainBao 的启动入口是汇编写的_start负责设置栈指针、清零 BSS 段、初始化页表然后跳到 C 语言的main。在 RVA23 上这段代码需要改几个地方。首先是栈指针的设置。K1 芯片的核启动后默认的栈指针是未定义的必须手动设置到一块可写内存区域。Bao 通常把栈放在内存的低地址区域但 K1 的内存映射里低地址可能是 ROM 或者保留区域不能随便用。我查了 K1 的手册发现可用 RAM 从0x80000000开始所以栈指针设到0x80000000 0x100000比较安全留出 1MB 给栈。其次是页表的初始化。RVA23 要求支持 Sv39 或 Sv48 分页模式Bao 默认用的是 Sv39三级页表虚拟地址 39 位。K1 支持 Sv48但 Bao 没必要改Sv39 够用。页表初始化的时候要注意RVA23 新增了menvcfg寄存器里的CBZE和CBCFE位控制缓存块操作的使能必须在页表启用前设置好否则后续的缓存维护指令会触发异常。最后是跳到main之前的准备工作。Bao 的main函数会初始化串口、打印版本信息、解析配置文件。如果串口没初始化好后面什么都看不到。K1 的 UART 是 8250 兼容的但时钟频率和寄存器偏移可能和标准 8250 不同。我对着 K1 的手册调了半天才发现它的 UART 时钟是 24MHz分频系数要按这个算否则波特率不对串口输出全是乱码。3.3 多核启动同步别让核跑飞了K1 有 8 个核Bao 启动时只需要一个核跑初始化代码其他核先停住等初始化完成后再按配置分配。这里的关键是核间同步机制。RISC-V 的标准做法是用 CLINT 的msip寄存器发核间中断但 K1 的 CLINT 实现可能和标准有差异。我一开始用标准 CLINT 地址写msip结果其他核根本没反应。后来查手册发现K1 的 CLINT 基地址是0x02000000但msip的偏移是0x0000每个核间隔0x4和标准一致。问题出在核的 hartid 分配上K1 的 hartid 不是连续的高性能核和能效核的编号有跳跃。Bao 的代码里假设 hartid 从 0 到 7 连续实际不是导致发给错误核的中断。修复方法是在启动阶段先读取每个核的mhartid建立一个 hartid 到物理核的映射表然后按映射表发中断。这个映射表可以硬编码也可以在运行时探测。我选择硬编码因为 K1 的 hartid 分配是固定的查手册就能确定。3.4 串口输出调试最原始但最有效的手段在移植初期串口输出是唯一的调试手段。Bao 的串口驱动很简单就是往 UART 的发送寄存器写字符。但 K1 的 UART 有个坑发送寄存器在写入之前必须检查状态寄存器的THRE位确保发送缓冲区为空。如果直接写字符会丢失。我一开始没注意这个串口输出断断续续以为是波特率问题调了半天才发现是没等THRE。加上等待循环后输出就稳定了。这个经验说明底层驱动再简单也要按硬件手册的流程来不能想当然。另外串口输出的格式化函数也要注意。Bao 用的是自己实现的printf不支持浮点和某些格式符。调试时尽量用%x和%d别用%f否则会出奇怪的结果。4. 中断与内存移植中最容易翻车的两块4.1 PLIC 配置优先级和阈值的坑PLIC 是 RISC-V 系统的外部中断控制器负责把外设中断分发给各个核。Bao 需要配置 PLIC 的优先级、使能位和阈值确保 VM 的中断正确路由。K1 的 PLIC 支持 1024 个中断源每个中断源有独立的优先级寄存器每个核有独立的使能位和阈值寄存器。移植时最容易出错的是上下文编号。PLIC 的上下文编号和核的对应关系是硬件定义的K1 的手册里有一张表列出了每个核的 M 模式和 S 模式上下文编号。Bao 运行在 M 模式所以要用 M 模式的上下文编号。我一开始用了 S 模式的编号结果中断根本进不来调试了好久才发现。另一个坑是优先级阈值。PLIC 的阈值寄存器决定哪些优先级的中断会被屏蔽。如果阈值设得太高低优先级中断永远进不来设得太低高优先级中断会频繁打断低优先级中断影响实时性。Bao 的默认配置是阈值 0所有中断都放行。但在 K1 上我建议把阈值设成 1屏蔽掉优先级 0 的中断因为优先级 0 通常保留给特殊情况不应该被普通外设使用。4.2 内存管理页表配置和缓存一致性Bao 用二级页表做地址翻译每个 VM 有自己的页表。在 RVA23 上页表配置需要注意几个新特性。首先是menvcfg寄存器的CBZE和CBCFE位。这两个位控制缓存块操作的使能必须在页表启用前设置。如果没设置当 Bao 或 VM 执行cbo.zero或cbo.flush指令时会触发非法指令异常。我在移植时忘了设这两个位结果 VM 启动到一半就崩了查了半天才定位到。其次是缓存一致性。K1 的 8 个核有自己的 L1 和 L2 缓存L3 是共享的。Bao 在 VM 切换时需要确保共享内存的缓存一致性。RVA23 的 Zicbom 扩展提供了cbo.clean、cbo.flush、cbo.inval指令用于缓存维护。Bao 的代码里原本没有这些指令需要手动加上。具体来说在 VM 之间传递数据时发送方执行cbo.flush把数据刷到内存接收方执行cbo.inval使自己的缓存失效这样才能保证看到最新数据。这里有个性能优化的点不是每次数据传输都需要全缓存刷新可以根据数据大小和访问模式选择性地刷新。比如小于缓存行大小的数据用cbo.clean就够了不需要cbo.flush。这个优化在实时场景下能省不少时间。4.3 定时器和核间中断CLINT 的配置细节CLINT 负责定时器中断和核间中断。Bao 需要配置mtime和mtimecmp寄存器实现定时器功能。K1 的mtime是 64 位的频率是 24MHz和 UART 时钟同源。配置定时器时要注意mtimecmp的写入顺序先写低 32 位再写高 32 位否则在写入过程中可能触发意外中断。核间中断用msip寄存器每个核一个。Bao 在启动其他核时往目标核的msip写 1目标核收到中断后在中断处理程序里清msip。这里要注意msip的写入是原子操作不需要额外同步。但如果多个核同时往同一个msip写可能会有竞争需要用原子指令或者锁保护。我在调试多核启动时遇到过核间中断丢失的问题。原因是msip写入后目标核还没来得及处理发送方又写了第二次导致第一次中断被覆盖。解决方法是在发送中断前先检查目标核的状态确保它已经处理完上一次中断。这个检查可以用共享内存里的标志位实现也可以用原子指令。5. 跑起来之后验证、调优和那些手册上没写的事5.1 最小验证让第一个 VM 跑起来移植完成后第一步是让一个最简单的 VM 跑起来。Bao 的仓库里通常有一个baremetal示例 VM就是一个死循环什么都不做。这个 VM 的作用是验证 Bao 的启动流程、内存分配和核分配是否正确。配置这个 VM 的时候需要指定它用哪个核、多少内存、哪些外设。K1 的 8 个核里我建议先给 VM 分配一个能效核因为能效核的频率低出问题的时候容易调试。内存给 64MB 就够了baremetal VM 不需要太多内存。外设先不分配等基础跑通再加。启动后如果串口能看到 Bao 的版本信息和 VM 的启动日志说明基本流程通了。如果卡住不动大概率是核间同步或者页表配置有问题。这时候可以用 JTAG 调试器连上去看 PC 指针停在哪里逐步排查。5.2 性能调优让实时性真正达标Bao 的卖点是实时性但默认配置不一定能达到最优。在 K1 上有几个调优点核的频率锁定K1 的核支持动态调频但实时场景下需要锁定频率避免调频带来的延迟抖动。可以通过写 PMU 寄存器锁定频率具体寄存器地址查 K1 手册。缓存分区K1 的 L3 缓存是共享的多个 VM 同时访问会互相干扰。如果硬件支持缓存分区比如通过 PMU 配置可以把 L3 切成几块每个 VM 独占一块。K1 是否支持这个特性需要查手册确认。中断亲和性PLIC 的中断可以绑定到特定核把外设中断绑定到处理它的 VM 所在的核减少核间中断转发带来的延迟。这些调优需要结合具体应用场景没有一刀切的方案。我的建议是先跑通基本功能再根据实际负载逐步调优。5.3 那些手册上没写的经验最后分享几个我在移植过程中总结的经验都是手册上不会写的。第一K1 的串口在启动初期可能不稳定尤其是冷启动的时候。如果第一次上电看不到输出别急着改代码先复位几次试试。我遇到过好几次冷启动无输出复位后就正常了应该是硬件初始化时序的问题。第二Bao 的配置文件里内存地址和大小必须按 2MB 对齐否则页表映射会出问题。K1 的物理内存从0x80000000开始但前 2MB 可能被固件占用Bao 可用的内存从0x80200000开始。配置的时候要注意这个偏移。第三多核启动时从核的启动地址必须按 4 字节对齐而且要在启动前确保指令缓存已经失效。K1 的从核启动后会从msip指定的地址取指令如果缓存里有旧数据会执行错误的代码。解决方法是在写启动地址后执行fence.i指令刷新指令缓存。第四调试的时候如果串口输出正常但 VM 跑不起来大概率是页表问题。可以用 Bao 的调试模式打印页表映射关系检查虚拟地址到物理地址的转换是否正确。Bao 的mmu.c里有调试开关打开后会输出详细的页表信息。第五RVA23 的向量扩展在 Bao 里默认是不保存上下文的。如果 VM 里用了向量指令Bao 需要在 VM 切换时保存和恢复向量寄存器。这个功能 Bao 可能没有默认实现需要手动加。具体做法是在vm.c的上下文切换函数里加上向量寄存器的保存和恢复代码。向量寄存器有 32 个每个 128 位起保存起来开销不小但为了正确性必须做。这些经验都是实际调试中积累的希望能帮你少走弯路。移植这件事说到底就是耐心加细心遇到问题多查手册、多试、多记录总能解决。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑