ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RVA23开发板移植Bao hypervisor与FreeRTOS实战

2026/9/26 9:08:10 拓冰建站 浏览量
RVA23开发板移植Bao hypervisor与FreeRTOS实战 1. 为什么要把 Bao 搬到 RVA23 开发板上第一次拿到 Banana Pi BPI-SM10 这块板子的时候我盯着它看了很久。RISC-V 架构、RVA23 指令集规范、多核 SMP、板载 PCIe 和一堆外设接口纸面参数确实漂亮但真正让我兴奋的不是硬件本身而是它给了我一个机会把 Bao hypervisor 这个轻量级虚拟化方案从原来的 ARM 和 RISC-V 旧 profile 上迁移到 RVA23 这个新基准上来。Bao 是什么简单说它是一个面向嵌入式实时场景的静态分区 hypervisor代码量小、启动快、隔离性强最初在 ARMv8 上跑得很成熟后来也支持了 RISC-V 的 RV64GC。它的核心思路是把一块物理芯片硬切成几个独立分区每个分区跑自己的 RTOS 或裸机程序互不干扰。FreeRTOS 就是它最常搭配的 guest 之一因为两者都追求确定性调度和低延迟响应。那为什么非要上 RVA23因为 RISC-V 的指令集一直在演进RVA23 是当前应用处理器 profile 的一个重要里程碑。它强制要求了向量扩展、Hypervisor 扩展、位操作扩展等一堆特性这意味着芯片的虚拟化能力比早期 RV64GC 强了一个档次。BPI-SM10 用的就是符合 RVA23 的 SoC所以把 Bao 移植上去本质上是在验证一个成熟的嵌入式 hypervisor 能不能在新一代 RISC-V 应用级芯片上继续发挥它的实时隔离优势。这件事适合谁看如果你正在做 RISC-V 平台的虚拟化选型或者手头有 FreeRTOS 项目需要多系统隔离又或者你单纯想搞清楚 hypervisor 移植到底要动哪些地方那这篇内容应该能帮你省下不少查文档和试错的时间。我会从整体设计思路讲到具体代码改动再到实际跑起来之后踩过的坑尽量把每个决策背后的原因说清楚。2. 移植前的整体设计与思路拆解2.1 先搞清楚 Bao 的架构分层Bao 的代码结构其实很清晰从上到下大致分四层最上面是 guest 管理负责创建分区、分配内存、配置虚拟中断中间是 hypervisor 核心处理 trap、异常、页表切换和 vCPU 调度下面是平台抽象层包括串口、中断控制器、定时器这些驱动最底下是架构相关代码也就是跟具体指令集和特权级规范打交道的那部分。移植到 RVA23 的主要工作量集中在最底下两层。平台抽象层要适配 BPI-SM10 的 UART 地址、PLIC 或 APLIC 中断控制器、CLINT 定时器架构层则要处理 RVA23 新增的 Hypervisor 扩展指令、向量上下文保存、以及可能变化的 CSR 访问规则。我当时的判断是不要一上来就改架构层先把平台层跑通让 Bao 能在板子上打印出第一行日志再逐步往上加功能。这个顺序很重要因为如果串口都没通后面调试基本靠猜。2.2 为什么选 FreeRTOS 作为第一个 guestBao 支持多种 guest包括裸机程序、Linux、FreeRTOS、Zephyr 等。我选择 FreeRTOS 作为第一个移植目标原因有三个。第一FreeRTOS 的代码量小编译快启动流程简单出问题容易定位。第二FreeRTOS 对硬件的要求低只需要一个定时器中断和一个串口就能跑起来不需要 MMU 和复杂的中断控制器配置。第三FreeRTOS 的调度器行为确定方便我验证 Bao 的 vCPU 调度是否真的做到了低延迟隔离。相比之下如果一上来就移植 Linux光是设备树和内存映射就能耗掉好几天而且一旦跑不起来很难判断是 Bao 的问题还是 Linux 配置的问题。所以先用 FreeRTOS 打通链路再考虑更复杂的 guest这是比较稳妥的策略。2.3 RVA23 带来的关键变化RVA23 不是一个小修订它把很多之前可选的扩展变成了强制项。对 hypervisor 移植影响最大的几个点H 扩展HypervisorRVA23 要求必须支持 H 扩展这意味着芯片有真正的两级地址翻译和虚拟中断注入能力。Bao 在旧 RISC-V 上跑的时候有些版本是靠软件模拟来绕过缺失的 H 扩展现在可以直接用硬件特性代码可以简化不少。V 扩展Vector向量扩展是强制的但 Bao 本身对向量上下文的管理需要额外处理。如果 guest 用了向量指令hypervisor 必须在上下文切换时保存和恢复向量寄存器否则会出数据错乱。Zicbom 和 Zicboz缓存块操作指令影响 DMA 和内存一致性处理。Bao 在分区之间共享内存时需要确保缓存一致性这些指令能帮上忙。Svpbmt 和 Svinval页表属性和 TLB 失效指令对虚拟化场景下的内存管理有直接影响。我在移植时的一个核心决策是尽量利用 RVA23 的硬件特性而不是沿用旧版的软件绕过方案。这样做的好处是性能更好、代码更干净代价是需要仔细阅读最新的特权级规范确保每个 CSR 的访问权限和触发条件都正确。2.4 工具链和构建系统的选择Bao 官方推荐用 RISC-V GNU 工具链但 RVA23 需要较新版本的 GCC 和 binutils 才能正确识别所有扩展。我试过几个版本最后锁定在 GCC 13.2 加上 binutils 2.41这个组合对 RVA23 的支持比较完整编译时不会报未知扩展的错误。构建系统方面Bao 用的是 Makefile 加 Kconfig 的组合。Kconfig 负责配置选项比如选择平台、选择 guest、开关调试信息Makefile 负责实际编译。移植新平台时需要在platform目录下新建一个文件夹里面放链接脚本、平台初始化代码和配置文件。这个结构很清晰照着现有平台的模板改就行。提示不要直接修改现有平台的文件来适配新板子而是新建一个平台目录。这样后续升级 Bao 版本时你的改动不会跟上游冲突。3. 核心细节解析与实操要点3.1 内存布局与链接脚本的调整BPI-SM10 的内存映射跟常见的 QEMU 虚拟平台不一样它的 DRAM 起始地址、外设寄存器基址、以及保留内存区域都需要根据实际芯片手册来定。我拿到板子后第一件事就是确认这几个地址区域起始地址大小用途DRAM0x800000004GB主内存UART00x100000004KB调试串口CLINT0x0200000064KB定时器和 IPIPLIC0x0C00000064MB外部中断控制器PCIe ECAM0x30000000256MBPCIe 配置空间链接脚本的关键是确保 Bao 的代码段和数据段放在 DRAM 的起始位置并且为每个 guest 分区预留独立的内存区域。我当时的分配方案是Bao 自己占 16MBFreeRTOS guest 占 32MB剩下的留给后续可能的 Linux guest。这里有个容易忽略的点RVA23 要求页表对齐到 4KB而且 H 扩展下的两级页表需要额外的对齐要求。如果链接脚本没有正确对齐启动时会直接触发页错误而且错误信息很难看懂。我的做法是在链接脚本里显式加上ALIGN(4096)并且在启动代码里检查页表基址的低 12 位是否为零。3.2 串口驱动的适配串口是移植过程中最重要的调试手段没有之一。BPI-SM10 用的是 8250 兼容 UART但寄存器偏移和时钟频率跟 QEMU 的虚拟串口不同。我需要修改的地方包括波特率除数计算根据实际时钟频率算出正确的分频值FIFO 配置设置触发阈值和清空方式中断使能决定是用轮询还是中断方式输出我一开始用的是轮询方式因为简单可靠不需要配置中断控制器。等系统基本跑通后再改成中断方式以减少 CPU 占用。这个渐进式的方法让我在早期避免了很多中断相关的问题。// 串口初始化核心代码 #define UART_BASE 0x10000000 #define UART_CLK 3686400 #define BAUD_RATE 115200 void uart_init(void) { uint16_t divisor UART_CLK / (16 * BAUD_RATE); mmio_write(UART_BASE UART_LCR, 0x80); // 使能除数锁存 mmio_write(UART_BASE UART_DLL, divisor 0xFF); mmio_write(UART_BASE UART_DLM, (divisor 8) 0xFF); mmio_write(UART_BASE UART_LCR, 0x03); // 8N1 mmio_write(UART_BASE UART_FCR, 0x07); // 使能并清空FIFO }注意除数计算时一定要用实际时钟频率不要照抄其他平台的数值。我见过有人直接把 QEMU 的除数复制过来结果串口输出全是乱码查了半天才发现是时钟频率不同。3.3 中断控制器的配置差异RVA23 平台可能使用 PLIC 或 APLIC 作为外部中断控制器两者在编程模型上有本质区别。PLIC 是传统的寄存器映射方式每个中断源有独立的优先级和使能位APLIC 则是基于 MSI 的机制配置方式更接近 PCIe 的中断模式。BPI-SM10 用的是 PLIC所以我在平台层实现了标准的 PLIC 初始化流程设置优先级阈值、使能特定中断号、配置目标 hart。这里的关键是中断号到 hart 的映射因为 Bao 需要把外部中断正确地路由到运行 guest 的那个物理 hart 上。void plic_init(void) { // 设置全局优先级阈值 mmio_write(PLIC_BASE PLIC_THRESHOLD, 0); // 使能UART中断优先级设为1 mmio_write(PLIC_BASE PLIC_PRIORITY(10), 1); mmio_write(PLIC_BASE PLIC_ENABLE(0), 1 10); // 设置hart 0的阈值 mmio_write(PLIC_BASE PLIC_CONTEXT(0) PLIC_THRESHOLD, 0); }如果芯片用的是 APLIC代码结构会完全不同需要配置 MSI 地址和中断域。我在移植前专门确认了 BPI-SM10 的中断控制器类型避免了走弯路。3.4 定时器与 vCPU 调度Bao 的 vCPU 调度依赖定时器中断来触发上下文切换。RVA23 平台通常有 CLINT 提供机器模式定时器但 hypervisor 运行在 HS 模式需要把定时器中断委托给 HS 模式处理。这里涉及一个关键 CSR 操作mideleg和medeleg寄存器。我需要把定时器中断和页错误等异常委托给 HS 模式这样 Bao 才能捕获并处理 guest 的 trap。// 委托定时器中断和页错误给HS模式 csr_write(mideleg, MIP_MTIP | MIP_MEIP); csr_write(medeleg, MCAUSE_LOAD_PAGE_FAULT | MCAUSE_STORE_PAGE_FAULT);定时器的周期设置直接影响 guest 的调度延迟。我一开始设的是 10ms后来发现 FreeRTOS 的 tick 是 1ms两者不匹配会导致调度抖动。改成 1ms 后FreeRTOS 的任务切换就稳定多了。实操心得定时器周期最好是 guest tick 周期的整数倍否则会出现 tick 丢失或重复计数的问题。这个细节在文档里通常不会写但实际调试时非常关键。4. 实操过程与核心环节实现4.1 搭建编译环境与获取源码第一步是准备工具链。我用的是一台 Ubuntu 22.04 的机器安装了以下组件sudo apt install build-essential git make gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu但系统自带的工具链版本偏旧对 RVA23 支持不完整。所以我从源码编译了 GCC 13.2git clone https://gcc.gnu.org/git/gcc.git cd gcc git checkout releases/gcc-13.2.0 ./contrib/download_prerequisites mkdir build cd build ../configure --targetriscv64-unknown-elf --enable-languagesc --without-headers --with-newlib make -j$(nproc)这个过程大概花了四十分钟编译完成后把riscv64-unknown-elf-gcc加入 PATH。Bao 的源码从官方仓库获取切换到支持 RISC-V 的分支。我用的版本是 v1.0.0 之后的某个提交因为早期版本对 H 扩展的支持不完整。4.2 新建平台目录与配置文件在src/platform下新建bpi-sm10目录里面至少需要这几个文件platform.mk定义编译选项和源文件列表linker.ld链接脚本platform.c平台初始化代码config.mk平台特定的配置宏platform.mk的内容参考现有 RISC-V 平台主要改编译目标和宏定义CROSS_COMPILE ? riscv64-unknown-elf- PLATFORM : bpi-sm10 ARCH : riscv RISCV_ISA : rv64gcvh_zicsr_zifencei这里rv64gcvh表示 64 位、通用扩展、向量扩展、Hypervisor 扩展。zicsr和zifencei是 CSR 访问和指令缓存刷新扩展RVA23 要求必须显式声明。4.3 实现平台初始化流程平台初始化的顺序很重要我按照以下步骤执行关闭所有中断设置初始栈指针初始化串口确保能输出调试信息配置物理内存保护PMP给 HS 模式足够的访问权限设置委托寄存器把 guest 相关异常交给 HS 模式初始化 PLIC配置中断路由初始化定时器设置调度周期创建第一个 guest 分区跳转到 guest 入口每一步都有对应的调试输出这样如果卡在某一步我能立刻知道问题出在哪里。void platform_init(void) { uart_init(); printf([Bao] UART initialized\n); pmp_init(); printf([Bao] PMP configured\n); delegate_traps(); printf([Bao] Traps delegated\n); plic_init(); printf([Bao] PLIC initialized\n); timer_init(1000000); // 1ms周期 printf([Bao] Timer started\n); }4.4 编译 FreeRTOS guest 并打包FreeRTOS 的移植相对独立我把它编译成一个裸机二进制文件然后在 Bao 的配置里指定加载地址和入口点。FreeRTOS 需要配置的硬件相关部分包括configCPU_CLOCK_HZ设为 CPU 实际频率configTICK_RATE_HZ设为 1000即 1ms tickconfigTOTAL_HEAP_SIZE根据分区内存大小设置configKERNEL_INTERRUPT_PRIORITY设置内核中断优先级编译完成后用objcopy生成二进制文件然后在 Bao 的构建脚本里把它嵌入到最终镜像中。riscv64-unknown-elf-objcopy -O binary freertos.elf freertos.binBao 的构建系统支持把 guest 镜像作为二进制数组嵌入这样启动时直接从内存加载不需要文件系统支持。4.5 启动与首次调试第一次上电时串口没有任何输出。我检查了三个地方串口引脚是否接对、波特率是否匹配、时钟频率是否正确。最后发现是时钟频率设错了实际是 24MHz 而不是我假设的 3.6864MHz。改正后串口输出了第一行日志[Bao] UART initialized接下来是 PMP 配置失败原因是 RVA23 的 PMP 寄存器数量比旧版多配置循环没有覆盖所有寄存器。修正后系统顺利启动到 guest 加载阶段。FreeRTOS 第一次运行时卡在调度器启动原因是定时器中断没有正确注入到 guest。我检查了hvip寄存器的设置发现虚拟定时器中断的位没有置位。加上之后FreeRTOS 的任务切换正常了。5. 常见问题与排查技巧实录5.1 串口无输出或乱码这是最常见的问题排查顺序如下现象可能原因解决方法完全无输出引脚接错、时钟未使能检查原理图确认TX/RX交叉连接输出乱码波特率不匹配重新计算除数确认时钟频率输出部分乱码FIFO配置错误清空FIFO调整触发阈值输出后卡死中断冲突检查PLIC配置确认中断号正确我的经验是先用示波器或逻辑分析仪确认TX引脚有波形再看波形宽度是否符合波特率。如果波形正常但内容乱码基本就是时钟频率的问题。5.2 页错误导致启动失败RVA23 的页表格式跟旧版有细微差别特别是Svpbmt扩展引入的页表属性位。如果链接脚本没有正确对齐或者页表项的属性位设置错误启动时会触发页错误。排查方法在 trap 处理函数里打印stval和scause寄存器这两个值能告诉你出错的虚拟地址和错误类型。void trap_handler(void) { uint64_t scause csr_read(scause); uint64_t stval csr_read(stval); printf(Trap: cause%lx, stval%lx\n, scause, stval); // 根据cause判断是页错误还是非法指令 }5.3 定时器中断丢失如果 guest 的 tick 不稳定或者任务切换有明显延迟很可能是定时器中断丢失。原因通常有两个一是定时器周期设置不当二是中断优先级配置错误。我遇到过一次定时器中断被 PLIC 的阈值过滤掉了因为阈值设得比定时器优先级高。把阈值降到 0 之后中断就正常了。避坑技巧PLIC 的阈值是全局的但每个 hart 有独立的上下文。如果多核运行要确保每个 hart 的阈值都正确设置。5.4 向量上下文保存不完整RVA23 强制要求向量扩展如果 guest 使用了向量指令hypervisor 必须在上下文切换时保存和恢复向量寄存器。我一开始忽略了这一点导致 FreeRTOS 任务切换后数据错乱。解决方法是在上下文切换代码里加上向量寄存器的保存和恢复# 保存向量寄存器 csrr t0, vlenb addi sp, sp, -32*t0 vs1r.v v1, (sp) # ... 保存v2到v31向量寄存器的数量取决于vlenb的值所以保存区域的大小是动态的。这个细节在移植时很容易被忽略但一旦 guest 用了向量指令问题就会暴露。5.5 FreeRTOS 堆栈溢出FreeRTOS 在 Bao 分区里运行时堆栈大小受限于分区内存。如果任务堆栈设得太小会出现溢出表现为任务莫名卡死或数据被覆盖。FreeRTOS 提供了堆栈溢出检测机制在FreeRTOSConfig.h里开启#define configCHECK_FOR_STACK_OVERFLOW 2然后在vApplicationStackOverflowHook里打印任务名和堆栈指针方便定位是哪个任务出了问题。我的经验是在 Bao 分区里跑 FreeRTOS 时每个任务的堆栈至少要比裸机运行时多留 20%因为 hypervisor 的 trap 处理会消耗额外的栈空间。6. 移植后的性能验证与调优6.1 中断延迟测量移植完成后我做的第一件事是测量中断延迟。方法是在 FreeRTOS 里用一个 GPIO 翻转来标记中断进入和退出然后用逻辑分析仪测量时间差。实测下来从外部中断触发到 guest 中断处理函数执行延迟大约在 2 到 3 微秒之间。这个数字比裸机运行多了大约 1 微秒主要是 hypervisor 的 trap 处理和中断注入开销。对于大多数实时场景来说这个延迟是可以接受的。6.2 上下文切换开销vCPU 上下文切换的开销直接影响 guest 的调度实时性。我测量了从定时器中断触发到另一个 vCPU 开始执行的时间大约在 5 微秒左右。这个开销主要来自页表切换和寄存器保存恢复。如果对实时性要求极高可以考虑减少上下文切换的频率或者使用 Bao 的静态分区模式让每个 vCPU 固定绑定到一个物理 hart 上避免切换开销。6.3 内存带宽影响Bao 的两级地址翻译会带来额外的内存访问开销。我用内存带宽测试程序对比了裸机和虚拟化环境下的表现发现虚拟化环境下带宽下降了大约 8% 到 12%。这个损耗主要来自 TLB 未命中时的页表遍历。RVA23 的Svinval扩展可以帮助减少 TLB 失效的开销但需要在代码里正确使用sinval.vma和hinval.gvma指令。我在页表更新代码里加上了这些指令带宽损耗降到了 5% 左右。7. 后续扩展方向与个人体会这套移植方案跑通之后我接着做了几件事。一是把第二个 guest 换成了 Zephyr验证多分区并行的稳定性二是尝试在 guest 里跑 lwIP 协议栈通过虚拟网卡实现分区间的网络通信三是测试了 Bao 的缓存着色功能看能不能进一步隔离内存带宽。过程中最大的体会是RVA23 的硬件特性确实让 hypervisor 的实现干净了很多但前提是你得把规范读透。很多问题不是代码写错了而是对规范的理解有偏差。比如hvip寄存器的中断注入时机、hgatp的页表格式、vsstatus的字段含义这些细节在手册里都有但容易看漏。另外调试工具的选择也很重要。我一开始只用串口打印后来发现有些问题比如页表遍历错误用打印根本定位不了必须上 JTAG 调试器。BPI-SM10 支持标准的 RISC-V JTAG配合 OpenOCD 可以单步跟踪 hypervisor 的 trap 处理流程效率比打印高得多。最后分享一个小技巧在移植初期把 Bao 的调试输出级别调到最高虽然日志会很多但能帮你快速定位问题出在哪个阶段。等系统稳定后再把日志级别降下来减少对实时性的影响。这个开关在 Kconfig 里就能配置不需要改代码。