ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RISC-V RVA23开发板移植Bao Hypervisor实战指南

RISC-V RVA23开发板移植Bao Hypervisor实战指南 1. 为什么要把Bao搬到RVA23开发板上第一次拿到 Banana Pi BPI-SM10 这块板子的时候我盯着它上面那颗支持 RVA23 规范的 RISC-V 芯片看了很久。RVA23 是 RISC-V 应用处理器配置文件里相当新的一个版本向量扩展、Hypervisor 扩展、位操作这些特性都成了标配这意味着它天生就是为跑复杂系统准备的。而 Bao 这个轻量级 Hypervisor之前在 ARM 和 x86 上已经有不少人玩过它的卖点就是极简、静态分区、实时性可控代码量小到可以通读。把这两者凑到一起本质上是在验证一件事RISC-V 的高端应用处理器能不能撑起一个真正意义上的嵌入式虚拟化底座。这个项目适合谁看如果你正在做 RISC-V 平台的底层开发或者手上有 SM10 这类板子想跑点超出裸机的东西又或者你对 Hypervisor 怎么在异构核之间分配资源感兴趣那这篇内容应该能帮你省下不少翻文档的时间。我会把从环境搭建、交叉编译、链接脚本调整到实际烧录、串口调试、踩坑排查的完整过程都摊开讲包括那些官方文档里不会写的细节。先说清楚 Bao 是什么。它不是一个全功能的 Type-1 Hypervisor不像 Xen 那样什么都能虚拟化。Bao 的设计哲学是“静态分区”也就是说在编译阶段就把 CPU 核、内存区域、外设中断分配给不同的 Guest运行起来之后基本不做动态调整。这种做法的好处是确定性极强没有复杂的调度器中断延迟可以做到很低适合工业控制、汽车电子这类对实时性有硬要求的场景。坏处也很明显灵活性差加个 Guest 可能要重新编译整个镜像。但正是这种简单让它在 RISC-V 这种生态还在快速演进的架构上反而更容易落地。Banana Pi BPI-SM10 这块板子用的是支持 RVA23 的芯片具体型号我这里不展开但关键点是它有多核、有 MMU、有 CLINT 和 PLIC 这些标准中断控制器内存也够大。这些硬件条件是跑 Bao 的前提。我实测下来板子的串口输出很稳定JTAG 调试也方便对于底层开发来说这点很重要。接下来我会按实际操作的顺序把整个移植过程拆成几个大块来讲每一块都会说明为什么这么做以及我踩过的坑。2. 环境搭建与工具链选型2.1 交叉编译工具链的选择与验证RISC-V 的工具链选择其实挺多的官方有 riscv-gnu-toolchain还有各种厂商定制的版本。我一开始用的是系统包管理器里自带的gcc-riscv64-unknown-elf版本比较老编译 Bao 的时候遇到了一些内联汇编的语法问题。后来换成了从源码编译的riscv64-unknown-elf-gcc版本 13.2.0问题就消失了。这里的关键是工具链要支持 RVA23 里用到的那些扩展指令尤其是向量指令和位操作指令老版本的工具链可能不认识。验证工具链是否可用的方法很简单写一个最小的汇编文件里面用几条 RVA23 特有的指令比如vsetvli或者orc.b然后编译看看会不会报错。如果报错说“unrecognized instruction”那就说明工具链太老需要升级。我建议直接用官方仓库编译虽然耗点时间但省心。# 克隆工具链源码 git clone https://github.com/riscv-collab/riscv-gnu-toolchain cd riscv-gnu-toolchain ./configure --prefix/opt/riscv --enable-multilib make -j$(nproc)编译完之后把/opt/riscv/bin加到 PATH 里然后用riscv64-unknown-elf-gcc --version确认一下。这里有个小细节--enable-multilib会编译多种 ABI 的库虽然占空间但后面链接的时候不用再折腾。2.2 Bao 源码获取与目录结构梳理Bao 的源码托管在 GitHub 上直接 clone 下来就行。我拿到的是最新的 main 分支代码量不大核心文件就那几个src/下面是 Hypervisor 的主体逻辑include/是头文件platform/下面按板子分目录每个板子有自己的配置文件。RISC-V 相关的代码在src/arch/riscv/里包括启动汇编、中断处理、页表管理这些。目录结构大概是这样bao/ ├── src/ │ ├── arch/riscv/ │ │ ├── boot.S │ │ ├── traps.c │ │ └── mmu.c │ ├── core/ │ │ ├── vm.c │ │ └── vcpu.c │ └── lib/ ├── platform/ │ └── bpi-sm10/ │ ├── config.mk │ └── platform.c └── Makefile我建议先把platform/下面已有的板子配置看一遍比如qemu-riscv64-virt那个理解一下配置文件的格式和需要填哪些字段。SM10 的配置可以基于它改主要改内存基地址、串口地址、CLINT 和 PLIC 的地址这些。2.3 串口与调试环境的准备底层开发离不开串口。SM10 板子上引出了 UART 接口我用的是 CP2102 转 USB 模块接到电脑上之后dmesg能看到ttyUSB0。串口参数是 115200-8-N-1用minicom或者picocom都行。我习惯用picocom命令简单picocom -b 115200 /dev/ttyUSB0JTAG 调试我用的是一块 FT2232 的调试器配合 OpenOCD。OpenOCD 的配置文件需要根据 SM10 的芯片来写主要是设置 JTAG 时钟频率和 target 配置。这部分如果搞不定其实可以先不接 JTAG靠串口打印也能调只是效率低一些。注意串口模块的电平一定要匹配SM10 的 UART 是 3.3V 电平接 5V 的模块可能会烧芯片。我一开始没注意幸好手头的模块支持电平切换。3. RVA23 平台适配的核心细节3.1 启动流程与链接脚本的调整Bao 在 RISC-V 上的启动流程是这样的上电之后BootROM 把固件加载到内存里然后跳转到 Bao 的入口点。Bao 的入口在boot.S里做的事情包括设置栈指针、初始化页表、跳到 C 语言的main函数。这里最关键的是链接脚本它决定了各个段放在内存的什么位置。原来的链接脚本可能是给 QEMU 用的内存基地址是0x80000000。SM10 的实际内存布局不一样需要根据芯片手册来改。我拿到的手册上写着 DRAM 起始地址是0x80000000但有些外设的 MMIO 区域会占用前面的地址空间所以链接脚本里要把代码段和数据段放到安全的位置。/* link.ld 关键片段 */ OUTPUT_ARCH(riscv) ENTRY(_start) MEMORY { RAM (rwx) : ORIGIN 0x80200000, LENGTH 0x10000000 } SECTIONS { .text : { *(.text.entry) *(.text) } RAM .rodata : { *(.rodata) } RAM .data : { *(.data) } RAM .bss : { *(.bss) } RAM }这里我把起始地址设成了0x80200000留出 2MB 给前面的固件和可能的 OpenSBI。如果你用的是 U-Boot 引导那地址还要再往后挪。这个地址一定要和实际加载地址一致否则跳转过去就是一片乱码。3.2 中断控制器与定时器的初始化RISC-V 的中断体系分两部分CLINT 负责软件中断和定时器中断PLIC 负责外部中断。Bao 需要接管这些中断然后根据配置分发给不同的 Guest。在 SM10 上CLINT 的基地址是0x2000000PLIC 的基地址是0xc000000这些都要在platform.c里定义好。定时器中断的初始化比较关键。Bao 用mtime和mtimecmp来做时间管理mtime是一个不断递增的计数器mtimecmp是比较值当mtime超过mtimecmp时就触发中断。代码大概长这样// 设置定时器中断10ms 后触发 uint64_t freq 10000000; // 假设定时器频率 10MHz uint64_t interval freq / 100; // 10ms csr_write(mtimecmp, csr_read(mtime) interval);这里有个坑不同板子的定时器频率可能不一样如果设错了Guest 里的时间会走得飞快或者慢得离谱。我一开始没注意Guest 里的延时函数完全不准后来查手册才发现频率是 10MHz 而不是我以为的 1MHz。3.3 内存管理与页表配置Bao 用 RISC-V 的 Sv39 页表模式来管理内存也就是三级页表虚拟地址 39 位。每个 Guest 有自己的页表Bao 通过satp寄存器切换。页表的配置在mmu.c里主要是建立虚拟地址到物理地址的映射。对于静态分区的 Hypervisor 来说内存映射是固定的。比如 Guest 0 的物理内存从0x80200000开始大小 64MB那它的页表就把虚拟地址0x80200000映射到物理地址0x80200000属性设为可读可写可执行。Guest 1 从0x84200000开始以此类推。// 建立页表映射的简化示例 void map_page(uint64_t *root, uint64_t va, uint64_t pa, uint64_t flags) { uint64_t vpn2 (va 30) 0x1ff; uint64_t vpn1 (va 21) 0x1ff; uint64_t vpn0 (va 12) 0x1ff; // 逐级查找或创建页表项最后设置叶子节点 // ... }注意页表项的对齐要求很严格每一级页表必须 4KB 对齐。我一开始用malloc分配页表内存结果地址没对齐一开 MMU 就崩了。后来改成静态数组加aligned(4096)属性才搞定。4. Guest 镜像的构建与 FreeRTOS 集成4.1 FreeRTOS 在 RVA23 上的编译要点Bao 本身只是个 Hypervisor它需要 Guest 镜像才能跑起来。我选的是 FreeRTOS因为代码量小、移植方便而且社区里 RISC-V 的移植案例很多。FreeRTOS 的 RISC-V 端口在portable/GCC/RISC-V目录下但默认是给 RV32 用的要改成 RV64 并且适配 RVA23 的指令集。主要改两个地方一是portASM.S里的上下文切换代码要把寄存器保存恢复的指令改成 64 位的二是portmacro.h里的栈类型定义从uint32_t改成uint64_t。另外RVA23 支持原子指令configUSE_PORT_OPTIMISED_TASK_SELECTION可以打开用clz指令来加速任务选择。// portmacro.h 关键修改 #define portSTACK_TYPE uint64_t #define portBASE_TYPE long #define portPOINTER_SIZE_TYPE uint64_t编译的时候记得加-marchrv64gcv -mabilp64d这样才能用上向量和双精度浮点。如果工具链不支持gcv至少要有gc否则 FreeRTOS 里的一些数学运算会编译不过。4.2 链接脚本与内存布局的配合Guest 的链接脚本和 Bao 的类似但起始地址要对应 Bao 分配给它的内存区域。比如 Guest 0 分到0x80200000那它的链接脚本里ORIGIN就写0x80200000。这里有个细节Guest 的入口点要和 Bao 的配置一致Bao 在启动 Guest 时会跳转到指定的入口地址通常是0x80200000。/* guest.ld */ ENTRY(_start) SECTIONS { . 0x80200000; .text : { *(.text) } .rodata : { *(.rodata) } .data : { *(.data) } .bss : { *(.bss) } }我建议在 Guest 的启动代码里加一句串口打印这样 Bao 跳转过去之后能立刻看到输出确认跳转成功。如果串口没反应那可能是入口地址错了或者页表映射有问题。4.3 设备树的处理与传递RISC-V 上设备树是标配Bao 需要把设备树传给 Guest。但静态分区的 Hypervisor 通常不会把完整的设备树传下去而是裁剪过的只保留 Guest 能看到的设备。比如 Guest 0 只能用 UART0那设备树里就只留 UART0 的节点其他都删掉。我一开始偷懒直接把完整的设备树传下去了结果 Guest 启动时去初始化不属于它的设备直接卡死。后来老老实实裁剪把 PLIC 的中断号也重新映射才正常起来。设备树的修改可以用dtc工具反编译成dts改完再编译回去。# 反编译设备树 dtc -I dtb -O dts -o sm10.dts sm10.dtb # 修改后重新编译 dtc -I dts -O dtb -o sm10_guest.dtb sm10.dts提示设备树里的chosen节点可以指定bootargs但 Bao 不一定支持命令行参数传递这个要看具体实现。我试过传consolettyS0结果 Bao 直接忽略了后来是在 Guest 代码里硬编码的。5. 烧录、启动与调试实录5.1 镜像打包与烧录方法Bao 和 Guest 编译完之后需要打包成一个镜像烧到板子上。SM10 支持从 SD 卡启动所以我把镜像写到 SD 卡里。打包的时候要注意Bao 的二进制放在最前面Guest 的镜像跟在后面中间可能要加对齐填充。# 合并镜像 cat bao.bin guest0.bin guest1.bin combined.bin # 写入 SD 卡 dd ifcombined.bin of/dev/sdX bs1M烧录之前一定要确认/dev/sdX是 SD 卡而不是你的硬盘我有个同事手滑写到了系统盘后果你懂的。烧完之后插到板子上上电串口应该能看到 Bao 的启动日志。5.2 串口日志分析与问题定位Bao 启动后会打印一些信息比如检测到的 CPU 核数、内存大小、加载的 Guest 数量。如果卡在某个地方不动了就要根据最后一行日志来判断问题。我遇到过一次卡在“Initializing MMU”之后后来发现是页表里有个地址映射错了导致访问越界。调试的时候可以在关键位置加printkBao 有自己的打印函数用法和 Linux 内核的类似。但要注意串口输出本身会消耗时间如果在中断处理里加太多打印可能会影响实时性甚至导致中断丢失。5.3 Guest 启动失败的常见原因Guest 启动失败的原因五花八门我整理了一个排查表现象可能原因排查方法串口无输出入口地址错误检查链接脚本和 Bao 配置输出乱码串口波特率不对确认 Guest 里的 UART 初始化启动后卡死页表映射缺失检查 MMU 配置和内存区域中断不触发PLIC 配置错误核对中断号和优先级定时器不准频率设置错误查手册确认 mtime 频率我踩过最坑的一个问题是 Guest 的栈指针没设置对导致函数调用时压栈压到了非法地址。这个问题的现象是启动后跑几条指令就崩串口只输出一半。后来在启动汇编里显式设置sp寄存器才解决。6. 性能观察与后续优化方向6.1 中断延迟的实测数据Bao 的卖点之一是低中断延迟我实测了一下从外部中断触发到 Guest 的中断处理函数开始执行延迟大概在 2 到 3 微秒之间。这个数据是在 CPU 频率 1GHz 的情况下测的用的是 GPIO 翻转加示波器观察的方法。相比 Linux 上动辄几十微秒的中断延迟Bao 确实快了一个数量级。但要注意这个延迟和 Guest 的配置有关。如果 Guest 里关了中断或者 PLIC 的优先级设得不对延迟会变大。我建议在 Guest 里把中断优先级设成最高并且确保mstatus里的MIE位是打开的。6.2 内存带宽的分配策略静态分区的一个好处是内存带宽可以预测。Bao 本身不限制 Guest 的内存带宽但如果多个 Guest 同时跑内存密集型任务带宽会互相影响。我试过让两个 Guest 同时做内存拷贝结果每个的吞吐量都降到了单独跑时的 60% 左右。如果对带宽有要求可能需要在硬件层面做 QoS 配置或者调整 Guest 的任务调度。6.3 后续可以尝试的扩展目前这个移植只是跑通了基本功能后面还可以做很多事情。比如把 FreeRTOS 换成 Zephyr看看哪个更适合 RVA23或者试试在 Guest 里跑 Linux虽然 Bao 的静态分区对 Linux 来说限制比较多但跑一个精简的 Linux 也不是不可能。另外RVA23 的向量扩展还没用上如果 Guest 里有向量计算任务可以试试把向量单元也分配给特定的 Guest。我个人在实际操作中的体会是RISC-V 的 Hypervisor 移植最大的难点不在代码本身而在文档的缺失。很多硬件细节要靠猜和试比如中断控制器的具体行为、内存映射的边界、时钟频率的准确值。这时候有个 JTAG 调试器会方便很多能直接看寄存器和内存比串口打印高效得多。如果你也在做类似的事情建议先把硬件手册翻烂然后从最简单的 Hello World 开始一步一步加功能别想着一次搞定。
返回列表