ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

RISC-V虚拟化实战:QEMU上同时跑通Zephyr与Linux

RISC-V虚拟化实战:QEMU上同时跑通Zephyr与Linux 先聊个实际的。RISC-V这两年在嵌入式圈子的热度已经不需要我再画什么曲线图了但真正动手跑过系统的人比围观的人少得多。原因很简单资料大多数停留在“介绍架构特性”和“跑个hello world”的阶段一旦想同时接触RTOS和完整Linux就得自己把工具链、模拟器、编译流程、启动链全部捋一遍。这篇文章把我自己从零搭环境、在QEMU上把Zephyr跑起来、再用同一套RISC-V平台引导Linux的完整过程整理出来了适合刚接触RISC-V的嵌入式开发者、正在评估Zephyr的企业工程师以及那些想在虚拟环境里先验证方案、暂时不想买开发板的同学。我不打算讲理论讲到天上去全程以QEMU模拟的RISC-V virt平台为基准先把Zephyr这个实时操作系统的编译和运行打通再切到Linux内核的编译、initramfs制作和启动流程。整个过程用到的都是免费开源工具链只要一台Ubuntu机器就能复现。1. 先搞清楚为什么要让RISC-V同时跑Zephyr和Linux1.1 RISC-V生态的现实情况RISC-V的指令集开源这件事行业里已经讨论得够多了。但从一个做嵌入式的工程师视角看真正的变化是以前评估一个芯片要花很长时间和原厂沟通、申请样片、看datasheet现在RISC-V架构让你可以先在纯软件的模拟环境里把整个软件栈跑通不用等硬件就位。这种“先软件后硬件”的开发模式对于项目前期评估来说非常友好。不过RISC-V也有一个实际问题就是生态碎片化。ARM那边有成熟的Toolchain、调试器、RTOS支持、Linux发行版适配一套工具链吃遍所有芯片。RISC-V因为每家SoC的核数、中断控制器、外设地址都可能不一样导致同样的内核代码在这个板子上能跑、换个板子就要改设备树。解决这个问题的思路就是先在QEMU的virt平台上跑通一套通用流程后面再针对具体芯片做BSP适配。1.2 Zephyr和Linux的定位差异Zephyr和Linux本来是两种不同定位的东西但在评估一块新架构的芯片时它们恰好是“一个管底层、一个管上层”的组合。Zephyr是一个为资源受限设备设计的RTOS最小配置下只需要几十KB的RAM支持多线程、信号量、消息队列、设备驱动模型还引入了设备树来管理硬件描述。它跟FreeRTOS最大的区别在于Zephyr更强调“可裁剪可配置”而且背后的社区维护了一个非常完整的驱动框架很多RISC-V SoC的UART、GPIO、SPI驱动都已经在主线里了。Linux则完全不同它需要MMU需要至少几MB的内存需要完整的启动链但换来的是强大的进程管理、虚拟内存、文件系统和海量现成驱动。一个典型的RISC-V SoC如果跑在10MHz、只有64KB SRAM那Zephyr是唯一选择如果跑在几百MHz、外挂DDR那Linux就能承担更复杂的业务逻辑。这篇文章里我把两个系统放在同一个QEMU环境里跑目的就是让你直观感受这个分工Zephyr快速启动、低延迟响应Linux承担复杂调度和文件系统。实际项目中很多SoC是异构多核小核跑Zephyr做实时控制大核跑Linux做应用处理这套组合在工业控制和边缘计算里越来越常见。2. 起步环境Ubuntu上的RISC-V交叉工具链与模拟器准备2.1 安装QEMU模拟器QEMU是目前支持RISC-V最成熟的模拟器它不仅能模拟完整的virt开发板还能模拟SiFive、StarFive等具体板卡。对于大多数学习场景我建议直接使用QEMU的virt machine因为它不需要特定板卡的全部外设细节只要关注CPU核心和基础内存映射就行。在Ubuntu系统上安装QEMU非常简单sudo apt update sudo apt install qemu-system-misc安装完检查一下版本qemu-system-riscv32 --version qemu-system-riscv64 --version我建议两个架构的模拟器都装上。Zephyr虽然有riscv32和riscv64两个目标但实际使用中riscv32的资源占用更小、启动更快Linux则必须先跑riscv64因为主流的发行版和buildroot默认都是针对64位架构做优化。后面你会看到同一个virt平台两个架构的核心、内存和外设地址略有差异编译时要区分开。2.2 RISC-V GNU工具链apt安装还是源码编译交叉编译是嵌入式的日常RISC-V的工具链通常指gcc、binutils、gdb这一套它们支持riscv64-unknown-elf这类裸机目标也支持riscv64-linux-gnu这种带glibc的Linux目标。最简单的安装方式是用aptsudo apt install gcc-riscv64-unknown-elf gdb-multiarch sudo apt install gcc-riscv64-linux-gnuriscv64-unknown-elf用于编译Zephyr、裸机程序、RTOS固件它不链接libc生成的elf可以直接烧到Flash或加载到QEMU。riscv64-linux-gnu则用于编译Linux内核、busybox、用户态程序它默认链接glibc生成的程序要跑在Linux环境里。这里有个常见的坑apt源里的工具链版本往往不是最新的而Zephyr SDK的版本更新很快两者版本差异过大可能导致链接失败。所以如果只做Zephyr开发我更建议直接使用Zephyr官方SDK它自带匹配的riscv工具链省去很多版本对齐的麻烦。SDK的安装会在下一节说。2.3 west与Zephyr SDK的安装Zephyr现在使用west作为多仓库管理工具zephyr主仓库、hal硬件抽象层、第三方库都是通过west拉取的。安装west需要Python3和pipsudo apt install python3-pip pip3 install west然后初始化一个Zephyr工作目录mkdir zephyr-project cd zephyr-project west init -m https://github.com/zephyrproject-rtos/zephyr --mr v3.7.0 west updatewest update这一步会拉取所有子模块包括modules/hal下的各种厂商HAL代码所以耗时较长网络不好的时候要有心理准备。拉完后目录结构大致是这样zephyr-project/ ├── zephyr/ # 主仓库 ├── modules/ # HAL、加密库、文件系统等 ├── tools/ # 辅助工具 └── .west/ # 配置文件Zephyr SDK则是一个打包好的工具链集合包含riscv、arm、x86等所有架构的编译器。去Zephyr官网下载对应版本的SDK tar包然后解压安装wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.8/zephyr-sdk-0.16.8_linux-x86_64.tar.xz tar xvf zephyr-sdk-0.16.8_linux-x86_64.tar.xz cd zephyr-sdk-0.16.8 ./setup.sh安装时它会把sysroots里的工具链路径加到环境变量里还会安装一个zephyr-sdk-env.sh脚本。在~/.bashrc里加上这样一行每次终端打开就自动加载source ~/zephyr-sdk-0.16.8/zephyr-env.sh这样Zephyr的编译环境就算准备好了。如果不想下载完整SDK也可以用apt装的gcc-riscv64-unknown-elf代替但要注意在zephyr的CMakeLists.txt里指定工具链前缀而且某些Zephyr版本对gcc版本有要求这个后面会讲。3. Zephyr在RISC-V上跑起来从构建系统到串口输出3.1 Zephyr的构建系统到底做了什么Zephyr用CMakeKconfig这套组合来管理工程配置。Kconfig负责配置开关CMake负责生成构建文件和编译。每个board目录下会有一个board_defconfig文件里面存的是这个板子的默认配置项比如芯片型号、时钟频率、外设使能情况。当我执行west build -b qemu_riscv32 samples/hello_world时构建系统会做这几件事根据-b qemu_riscv32找到zephyr/boards/riscv/qemu_riscv32/目录下的qemu_riscv32_defconfig和qemu_riscv32.dts。解析设备树生成zephyr.dts和generated_dts_board.h这个头文件里是每个外设的基地址、中断号等宏定义。根据Kconfig的配置选项裁剪需要编译的源文件。调用riscv工具链编译所有源文件最终链接出zephyr.elf。这里面设备树的作用值得多说一句。Zephyr从2.x版本开始全面引入设备树驱动不再通过硬编码的地址宏来访问硬件而是通过DT_NODE_LABEL这类API从设备树节点获取硬件信息。这跟Linux的设备树思路是一样的只是Zephyr在编译阶段就把设备树的信息编译进了固件运行时不解析dtb文件。3.2 编译hello_world并跑在QEMU上先用默认的qemu_riscv32板子编译一个最简单的hello_worldcd zephyr-project west build -b qemu_riscv32 -p always samples/hello_world-p always表示每次构建前都先clean避免旧配置残留导致莫名其妙的问题。编译完的产品在build/zephyr/zephyr.elf和build/zephyr/zephyr.bin。直接用west运行west build -t run这个命令会自动调用QEMU并且我们能在终端上看到输出*** Booting Zephyr OS build v3.7.0 *** Hello World! qemu_riscv32第一次看到这个输出意味着你的RISC-V交叉工具链、Zephyr构建系统、QEMU模拟器三者已经打通了。这里要强调一个点Zephyr的最小系统是跑在bare metal上的没有bootloader没有操作系统zephyr.elf被QEMU直接加载到内存后从_start开始执行。所以Zephyr的启动速度极快在QEMU上几乎是瞬间就完成了初始化并进入应用代码。3.3 切换riscv64和更多示例如果之前装了riscv64的QEMU可以试一下64位目标west build -b qemu_riscv64 -p always samples/hello_world west build -t runriscv64的板子启动时会进入64位模式寄存器宽度、地址空间都不同但对用户应用来说差别不大底层代码由Zephyr的arch目录封装好了。建议多试几个Zephyr自带的sample特别是samples/shell/shell_modulewest build -b qemu_riscv32 -p always samples/shell/shell_module west build -t run这个sample会启动一个串口shell能在里面执行kernel threads、device list、help等命令对于了解Zephyr的设备模型和线程调度非常有帮助。我在调自己写的驱动时经常开着这个shell直接在运行时查看设备树注册状态和线程栈使用率。3.4 基于实际板卡移植Zephyr的几个关键点QEMU跑通只是第一步很多人最终是要把Zephyr移植到真实的RISC-V芯片上。根据我的经验移植的核心工作集中在三块第一确认SoC的CLINT和PLIC地址。Zephyr RISC-V架构依赖CLINT提供定时器和软件中断依赖PLIC管理外部中断。不同厂家的SoC外设地址都不一样比如一些国产MCU会把CLINT放在0x2000000附近和SiFive的标准布局一致但也有芯片会调整基址。这些信息必须从datasheet的内存映射图里查清楚。第二编写或修改设备树文件。在zephyr/boards/riscv/board/目录下新建board.dts然后把UART、GPIO、SPI等设备节点按实际地址写进去。设备树写得好不好直接决定了你能不能用现成的驱动。第三配置时钟树。Zephyr的设备树里会定义一个clocks属性里面包括时钟源、分频系数等。RISC-V芯片的时钟配置比ARM简单但如果你用的是PLL倍频出来的core clock必须在.dtsi里把频率关系写对否则延时和波特率都会出错。对于不想从零开始的朋友可以先找一块和你的目标芯片相似的板卡比如把SiFive的HiFive1 Rev B作为模板改芯片名、地址、外设比完全凭空写要快很多。4. Linux on RISC-VOpenSBI、内核Image与initramfs的完整启动链4.1 Linux on RISC-V的启动流程和ARM有什么不同ARM64的Linux启动通常经过ROM-u-boot(SPL)-ATF-u-boot-kernel这个过程RISC-V的启动链则更简明ROM加载OpenSBIOpenSBI跳转到Linux内核。OpenSBI的角色类似ARM的ATF运行在M模式提供SBISupervisor Binary Interface调用给内核使用。QEMU的virt machine默认自带OpenSBI固件所以如果只是跑默认配置不需要额外编译RISC-V的bootloader。QEMU在启动时会把OpenSBI放到内存起始地址然后跳转到内核入口。QEMU的完整启动链路是QEMU固件(OpenSBI) - Linux内核Image - initramfs - /init理解这条链对排查启动问题很重要。比如内核打印第一行日志之前如果卡住通常是OpenSBI没起来或者内核入口地址不对如果内核日志出现但挂载不了根文件系统那就是initramfs或者root参数的问题。4.2 编译RISC-V 64位Linux内核先下载内核源码git clone https://github.com/torvalds/linux.git cd linux git checkout v6.6配置的时候直接使用RISC-V的默认配置make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- defconfigdefconfig对riscv架构会生成一份包含基础驱动、串口、块设备、网络支持的配置。如果后续要在真实板卡上跑通常还要用menuconfig勾选对应的网卡驱动和存储控制器驱动。编译make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- -j$(nproc)编译产物在arch/riscv/boot/Image。这个Image是未经压缩的内核镜像QEMU可以直接加载。如果你看到还有一个Image.gz那是压缩版一般给u-boot用QEMU加载Image就行。整个编译过程大概三五分钟取决于机器性能。如果中间报错找不到libelf、openssl头文件之类的执行sudo apt install libelf-dev libssl-dev再重新编译即可。4.3 用busybox制作最小rootfs和initramfsLinux内核起来后必须挂载一个根文件系统里面至少要有/init这个程序。对于QEMU快速验证场景initramfs是最简单的选择它会被内核直接解压到内存中作为临时的根文件系统不需要模拟块设备也不需要SD卡镜像。用busybox来生成用户态工具git clone https://github.com/mirror/busybox.git cd busybox make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- defconfig make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- menuconfig在menuconfig里一定要做一件事进入Settings把Build static binary (no shared libs)勾上。这一步非常关键busybox默认是动态链接的生成的二进制需要glibc的动态库如果把它放到initramfs里但initramfs里没有glibc的.so文件内核启动后就会报/init: not found。编译安装make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- -j$(nproc) make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- install所有的binaries会装到_install目录下。现在给它补一个init脚本cd _install cat init EOF #!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys echo Welcome to RISC-V Linux! exec /bin/sh EOF chmod x init最后打包成cpio格式的initramfsfind . | cpio -o -H newc | gzip ../rootfs.cpio.gz4.4 在QEMU上启动Linux现在内核和initramfs都准备好了用下列命令启动qemu-system-riscv64 \ -machine virt \ -nographic \ -m 1G \ -bios default \ -kernel Image \ -initrd rootfs.cpio.gz \ -append consolettyS0 rdinit/sbin/init参数解释一下-machine virt选择QEMU内置的riscv virt板卡。-nographic把串口重定向到当前终端不需要额外开终端窗口。-bios default使用QEMU默认的OpenSBI固件。-kernel Image指定Linux内核镜像。-initrd rootfs.cpio.gz指定initramfs。-append consolettyS0 rdinit/sbin/init内核命令行参数console指定控制台串口设备rdinit指定initramfs中的init程序路径。启动后你会看到OpenSBI的启动信息然后是一大段Linux内核日志最后进入busybox的shellOpenSBI v1.2 ... Linux version 6.6.0 ... ... Welcome to RISC-V Linux! / #到了/ #提示符说明RISC-V上的Linux已经完全跑起来了。在这里你可以执行uname -a查看内核版本、cat /proc/cpuinfo查看RISC-V硬件信息、free查看内存使用。如果想要完整的磁盘文件系统可以用buildroot一键生成根文件系统镜像或者用debootstrap制作Debian的rootfs但那是后话。这篇文章里的initramfs方案足够你在几分钟内验证内核能不能跑以及后续做驱动开发测试了。5. 双系统协作同一块RISC-V芯片上Zephyr和Linux怎么分工5.1 为什么实际产品需要同时存在两个系统很多人会有疑问Zephyr能跑实时任务Linux能跑复杂应用为什么不能只用一个答案在于二者的资源消耗和实时性差距太大。Zephyr的中断响应延迟可以做到微秒级而且整个内核镜像只有几十KB。但它没有进程隔离一个驱动的野指针就能把整个系统打挂文件系统、网络协议栈虽然也有但性能和功能都比Linux差得远。Linux的优势在于生态各种中间件、容器、调试工具、远程管理框架全都现成。但Linux的调度延迟受内核抢占、缓存命中、DMA竞争等因素影响很难保证硬实时。实际产品里最常见的设计是一颗带多个RISC-V核的SoC核0跑Linux负责业务逻辑、网络通信、用户界面核1跑Zephyr负责电机控制、传感器采集、高速IO。两个核之间通过共享内存或者Mailbox通信各干各的活互不干扰。这种设计在工业控制器、机器人、边缘网关里越来越多也被称为AMP非对称多处理架构。5.2 在QEMU里搭建AMP环境的思路QEMU目前对AMP的模拟是通过-smp参数来指定CPU核心数的比如qemu-system-riscv64 -machine virt -smp 4 -m 2G ...但QEMU默认情况下每个核跑的都是同一个固件要实现Zephyr跑核0、Linux跑核1还需要在软件层面做区分通常做法是用一个最小的bootloader在核0上启动Zephyr。在Zephyr的启动代码里判断当前核ID如果是核0则进入RTOS调度如果是核1则跳转到Linux内核入口。两个系统通过共享一块物理内存区域进行通信。这个方案在真实芯片上已经很成熟了比如Xilinx的Zynq UltraScale里常见的“LinuxFreeRTOS”AMP方案就是这个套路。RISC-V的Hart硬件线程相当于CPU核调度机制天然支持这种模式每个核独立执行不同程序。不过在QEMU上做完整AMP比较折腾因为QEMU对核间中断的模拟和真实芯片有差异。如果你想快速验证一个变通方案是在同一台主机上开两个QEMU进程一个跑Linux、一个跑Zephyr然后用宿主机的socket或者共享文件夹模拟核间通信。这种方式虽然不是严格的AMP但用来验证通信协议和业务逻辑已经足够。5.3 Zephyr引导Linux的过渡方案有些团队会考虑同一个核上先让Zephyr起来做快速硬件初始化再跳转到Linux实现快速启动。这个思路在消费电子里很常见先用RTOS起一个极简界面让用户看到屏幕亮了后台再慢慢引导完整的Linux系统。实现原理不复杂。Zephyr在M模式运行Linux在S模式运行。Zephyr初始化完DDR、PMIC、显示等外设后调用SBI的hart_start接口唤醒Linux核然后把控制权交给OpenSBIOpenSBI再跳转到Linux。这里面的关键是Zephyr在跳转前要把当前模式从M模式切换到S模式通常用mret指令结合medeleg/mideleg委派配置完成。跳转地址必须是Linux内核入口也就是Image的启动地址。要给Linux预留足够的干净内存空间Zephyr使用的内存区域要报告给Linux避免内核把它当普通内存使用。这个方案我在实验板上验证过Zephyr从上电到点亮LCD大约300毫秒比Linux从bootloader开始启动快了将近3秒用户体验提升非常明显。如果你做的是带屏的产品这个思路值得认真研究。6. 实操中绕不开的坑问题排查与调试技巧6.1 编译期的常见错误问题Zephyr编译时提示找不到riscv工具链这通常是因为Zephyr SDK没有正确配置环境变量。处理方式很简单确认~/.bashrc里的source路径写对了然后重新开一个终端窗口让配置生效。如果用的是apt的交叉编译器需要在构建时显式指定west build -b qemu_riscv32 -p always samples/hello_world \ --sysroot /usr/riscv64-linux-gnu \ -DCROSS_COMPILEriscv64-linux-gnu-不过我个人还是建议直接用Zephyr SDK省心很多。问题编译Linux内核时报fatal error: openssl/bio.h: No such fileRISC-V内核的x509证书管理和MODULE_SIG功能依赖OpenSSL头文件。直接安装sudo apt install libssl-dev然后清理再编译。6.2 运行期的排查速查表现象可能原因处理办法QEMU启动后没有任何输出QEMU命令缺少-nographic或console参数不对加上-nographic确认consolettyS0Zephyr在west build -t run后卡死板子选择错误或内存配置过大换qemu_riscv32/qemu_riscv64检查board configLinux启动后停在Starting kernel ...OpenSBI跳转地址错误或Image格式不匹配确认使用-kernel Image而非-kernel vmlinux内核报/init: not foundbusybox不是静态编译或init路径不对检查Busybox Settings - Build static binary确认init脚本可执行initramfs解压失败cpio格式不对确保用-H newc格式打包内核支持CONFIG_BLK_DEV_INITRDQEMU运行很卡内存不足或未启用KVMRISC-V的QEMU在x86上无法用KVM只能靠TCG模拟可降低-smp核数6.3 用GDB单步调试Zephyr和LinuxQEMU内置了GDB Server支持这使得在模拟器里做指令级调试非常顺手比在真板上调方便太多。调试Zephyr时先让QEMU等待GDB连接qemu-system-riscv32 -machine virt -nographic -kernel build/zephyr/zephyr.elf -s -S-s表示在本地1234端口开放GDB Server-S表示启动后暂停CPU等待调试器连接。然后开另一个终端riscv64-unknown-elf-gdb build/zephyr/zephyr.elf (gdb) target remote localhost:1234 (gdb) continue这时Zephyr开始运行你随时可以用CtrlC停下来单步执行查看寄存器状态。调试Linux内核也一样只是加载的镜像换成了vmlinux这个未压缩的内核文件并用riscv64-linux-gnu-gdb来调试。这个调试手段的价值在于当你的驱动在真板上跑死机时用QEMU可以轻松打断点看现场快速定位是寄存器配置错了还是中断处理流程有bug然后带着结论去真板验证效率会高很多。6.4 开发流程的终极建议最后分享一点个人经验。很多初学者喜欢一上来就买开发板然后被交叉工具链、烧录器、调试器一堆问题劝退。我的建议是先用QEMU把Zephyr和Linux的构建、烧录、调试这条完整链路跑通对RISC-V的启动过程、内存映射、设备树有了体感之后再入手硬件板卡。到那时你会发现真板上遇到的问题大多是电源时序、时钟配置、引脚复用这些硬件层面的差异软件流程早就了然于胸了。RISC-V的魅力就在于指令集简单、开源透明从RTOS到完整Linux都能在这一套架构上学习和实践。这篇文章里的所有操作我已经用最新的Zephyr 3.7和Linux 6.6在Ubuntu 22.04上完整跑了一遍你可以放心照着做。真遇到这里没覆盖到的问题多看看docs.zephyrproject.org和内核的Documentation/riscv目录官方文档永远是最好的老师。
返回列表