ARTICLE DETAIL

资讯详情

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

RISC-V上跑通Zephyr和Linux:从QEMU到开发板的完整指南

RISC-V上跑通Zephyr和Linux:从QEMU到开发板的完整指南 如果你和我一样手头放着一块还带着静电袋的RISC-V开发板脑子里却同时在纠结两件事——先用Zephyr跑个实时任务还是直接上Linux感受一下桌面级系统那么这篇东西就是给你写的。我第一次在QEMU里把RISC-V上的Zephyr跑通是在一个加班到深夜的晚上串口蹦出Hello World的那一刻疲惫感直接没了后来花了一个周末又把Linux在同一个QEMU里带起来才真正理解了什么叫“同一套指令集两种完全不同的软件栈”。这篇文章不聊虚的就记录我在RISC-V上运行Zephyr和Linux的完整方法、原理和踩坑过程全部精确到命令行你可以照着一步步复现。1. 为什么要在RISC-V上同时折腾Zephyr和Linux两个系统的定位差异很多人拿到RISC-V开发板后的第一个问题不是“怎么跑”而是“跑什么”。Zephyr还是Linux这个选择题困扰了我很久后来想明白了真正的问题不是二选一而是你得知道这颗CPU上到底能同时承载什么。RISC-V的本质是一个开放的指令集架构ISA它不规定具体硬件实现只规定指令怎么编码、怎么执行。这就带来了一个嵌入式领域里很罕见的特性同一颗处理器核心既可以运行一个几KB的实时内核也可以运行一个完整的多进程操作系统。在ARM世界里虽然也有这种能力但Cortex-M和Cortex-A分了家工具链、调试器、软件生态都各玩各的。RISC-V从设计之初就预留了这种组合拳的可能。Zephyr和Linux正是这条光谱上的两个典型代表。Zephyr是一个轻量级实时操作系统面向MCU场景内核可以压缩到几十KB在只有几百KB内存的芯片上也能活蹦乱跳。Linux则是一个通用操作系统它默认你有一颗带MMU的处理器几十MB内存起步跑文件系统、跑网络协议栈、跑容器都行。我整理了一个表格方便你直观感受两者的区别维度ZephyrLinux定位实时操作系统RTOS通用操作系统内核镜像几十KB到几百KB几MB起步内存需求几十KB即可运行建议至少64MB以上实时性硬实时调度策略可配置软实时更侧重吞吐量MMU不需要可选PMP/MPU必须依赖MMU做进程隔离文件系统可选通常用Flash分区标配且支持多种文件系统驱动模型devicetree 子系统devicetree 驱动框架典型场景MCU、传感器节点、PLC、电机控制单板电脑、网关、边缘计算从这张表能看出Zephyr和Linux不是替代关系而是互补关系。实际项目中完全可以把一个双核RISC-V SoC分成两个世界小核跑Zephyr处理实时中断大核跑Linux做业务逻辑。这也是很多边缘计算SoC的常见玩法。不过这种混合架构有个前提——你得先把两个系统在同一个平台上单独跑通理解它们的启动边界和资源开销然后再谈核间通信。这篇文章的路线就是这样先跑通Zephyr再跑通Linux最后一章讲清楚它们在RISC-V架构下的运行边界。对刚接触RISC-V的嵌入式工程师来说这是最快建立全局观的方式。2. 起步阶段的环境准备工具链、QEMU与SDK的选择2.1 为什么先选QEMU而不是直接买板子我见过不少朋友的路径拿到一块真实开发板先被刷机流程劝退再被串口驱动折磨最后连个点灯程序都没跑起来板子就吃灰了。所以我强烈建议你先在QEMU上跑而不是一上来就买板子。理由有三条。第一成本为零你在自己电脑上就能完成所有实验。第二可复现性是模拟器的天然优势环境坏了直接删除重来别人向你请教问题的时候你给他一串命令就够了。第三QEMU是Zephyr和Linux社区持续验证的平台它的virt机器和真实硬件相比已经很接近官方文档里的测试用例绝大多数跑在QEMU上这意味着你踩到的坑答案大概率已经在网上出现了。等你在QEMU上把Zephyr和Linux的软件栈玩明白了再买一块社区长期维护的开发板速度会快很多。2.2 两条工具链千万别混着用RISC-V的工具链和ARM不太一样一上来就有两条路riscv64-unknown-elf-gcc裸机工具链针对bare-metal环境C库用的是newlib适合编译固件、RTOS、裸机程序。Zephyr SDK自带的就是这种。riscv64-linux-gnu-gcc面向Linux用户态C库使用glibc适合编译Linux内核、BusyBox、各种Linux应用程序。很多人第一次在RISC-V上折腾会把这两者搞混。比如用riscv64-unknown-elf工具链去编译BusyBox链接阶段会报一堆找不到libc的错。当然编译Linux内核本身不用libc裸机工具链也能编内核但如果你要制作initramfs里面的用户态程序就必须要Linux工具链了。我的建议很简单Zephyr的源码和工程统一交给Zephyr SDK处理Linux相关的内核、BusyBox、根文件系统统一用riscv64-linux-gnu-gcc两边各管各的省心。2.3 一次性装齐基础环境我的实验环境是Ubuntu 22.04其他发行版大同小异。先更新系统并安装编译依赖sudo apt update sudo apt upgrade -y sudo apt install -y --no-install-recommends \ git cmake ninja-build gperf ccache dfu-util \ device-tree-compiler wget python3-dev python3-pip \ python3-setuptools python3-tk python3-wheel xz-utils \ file make gcc gcc-multilib g-multilib \ libsdl2-dev libmagic1 qemu-system-miscqemu-system-misc这个包在Ubuntu里包含了qemu-system-riscv32和qemu-system-riscv64。装完之后确认一下版本qemu-system-riscv64 --version建议版本不低于7.0新版本对RISC-V虚拟机的支持更完善特别是virt机器上的PCIe和中断控制器仿真更接近真实硬件。另外还需要用pip安装west这是Zephyr官方推荐的工程管理工具pip3 install --user west export PATH$HOME/.local/bin:$PATH west --version2.4 顺手验证工具链是否可用如果你按上面的命令装完已经有了一部分环境。现在还需要安装Linux交叉编译工具链趁早验证一下避免后面Linux编译到一半才发现缺东西sudo apt install -y gcc-riscv64-linux-gnu riscv64-linux-gnu-gcc -v看到gcc version信息输出就说明OK。这里再提醒一次Zephyr的工具链和Linux的工具链是分开的Zephyr SDK我们下一节再装因为它的安装方式和路径会影响west build的调用方式。3. 跑通Zephyr从工程管理到QEMU板卡启动3.1 westZephyr的工程管理入口Zephyr不是一个单一仓库它由zephyr主仓库、hal板级支持包、第三方库等一大堆子仓库组成。直接用git clone一个仓库是拉不完整的west就是用来管理这个多仓库结构的工具。初始化一个Zephyr工程的完整命令如下mkdir -p ~/zephyrproject cd ~/zephyrproject west init -m https://github.com/zephyrproject-rtos/zephyr --mr v3.7.0 west updatewest init会根据manifest文件把顶层仓库拉下来west update则会把manifest里声明的所有子仓库全部同步到本地。这一步会耗一些时间取决于你的网络情况不过是一次性的。初始化完成之后还要把ZEPHYR_BASE环境变量指过去这样CMake才能通过find_package找到Zephyr构建系统export ZEPHYR_BASE$HOME/zephyrproject/zephyr建议把这一行写到你的~/.bashrc里后面所有Zephyr工程构建都依赖它。3.2 安装Zephyr SDKZephyr SDK是Zephyr官方发布的交叉编译工具链合集里面包含了多个架构的gcc、gdb、OpenOCD等工具。对于RISC-V而言我们需要riscv64-zephyr-elf系列。去Zephyr官方的GitHub Release页面下载对应版本我用的是0.16.8cd ~ wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.8/zephyr-sdk-0.16.8_linux-x86_64.tar.xz tar -xf zephyr-sdk-0.16.8_linux-x86_64.tar.xz cd zephyr-sdk-0.16.8 ./setup.sh -t riscv64-zephyr-elf -csetup.sh会把工具链安装到默认位置并创建必要的CMake配置文件。如果你不确定这里做了什么可以后面把环境变量也加上export ZEPHYR_TOOLCHAIN_VARIANTzephyr export ZEPHYR_SDK_INSTALL_DIR$HOME/zephyr-sdk-0.16.8这样west build的时候就会自动选择Zephyr SDK里的riscv64工具链而不是系统自带的gcc。3.3 编译并启动hello_worldZephyr官方示例很多hello_world是最简单的一个。在QEMU上跑qemu_riscv32板卡cd ~/zephyrproject/zephyr west build -b qemu_riscv32 samples/hello_world west build -t run如果一切正常编译完成后终端会直接进入QEMU并弹出类似下面的输出*** Booting Zephyr OS build v3.7.0-rc1 *** Hello World! qemu_riscv32这一步跑通说明你的Zephyr工具链、构建系统、QEMU模拟环境全部已经正常工作了。想换成64位体验把-b参数改成qemu_riscv64即可west build -b qemu_riscv64 samples/hello_world west build -t run3.4 从hello_world到可交互的shell如果你觉得hello_world只是打印一句话没看出Zephyr的系统能力那我建议你编译一个带shell的示例。Zephyr里有个经典的shell示例west build -b qemu_riscv32 samples/shell/shell_module west build -t run启动后你会进入一个交互式shell可以输入命令查看系统状态。比如输入kernel version可以看内核版本号输入devices能看到当前已经初始化的设备驱动列表。通过这个shell你能比较直观地感受到Zephyr虽然小但它有完整的设备模型、Kconfig配置体系和子系统框架并不是一个玩具级别的调度循环。3.5 写一个自己的应用并在QEMU上验证跑通了官方示例接下来就得试试自己写应用了。创建一个工程目录mkdir -p ~/hello_app/src cd ~/hello_appCMakeLists.txt内容如下cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(hello_app) target_sources(app PRIVATE src/main.c)src/main.c#include zephyr/kernel.h void main(void) { printk(Hello from my own RISC-V app!\n); printk(Running on %s, %d MHz\n, CONFIG_BOARD, CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC / 1000000); }然后编译并运行cd ~/zephyrproject/zephyr west build -b qemu_riscv32 ~/hello_app west build -t run你会看到自己的工程在QEMU上跑起来。这一步很重要标志着你已经脱离了“只会看示例”的阶段开始能自己组织Zephyr工程了。Zephyr的构建系统由CMake、Kconfig和devicetree三部分协同后面你每加一个驱动或模块基本都要和这三者打交道。4. 跑通Linux内核编译、根文件系统与QEMU启动Zephyr跑通后Linux这条线的难度会上一个台阶但思路更清晰编译内核、制作根文件系统、启动QEMU。这里我建议不要用任何发行版镜像直接手工从源码构建这样你对整个系统从零到一的流程才有真正的掌控感。4.1 准备RISC-V Linux交叉工具链前面已经装过riscv64-linux-gnu-gcc这里再确认一次riscv64-linux-gnu-gcc -v这套工具链默认使用glibc生成的程序依赖Linux syscall接口可以编译内核、BusyBox和用户态应用。4.2 编译Linux内核defconfig与Image我用的是6.12 LTS版本内核LTS的好处是长期维护后面遇到问题社区资料多。cd ~ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.tar.xz tar -Jxf linux-6.12.tar.xz cd linux-6.12配置内核时RISC-V架构只需要指定ARCH和CROSS_COMPILE然后选一个默认配置make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- defconfig make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- -j$(nproc)这个过程会编一会儿。结束后内核镜像在arch/riscv/boot/Imagels -l arch/riscv/boot/Image这里有个RISC-V特有的细节新的RISC-V内核一般没有像ARM那样使用zImage而是直接用Image这个平铺镜像QEMU或者U-Boot加载它非常方便。我第一次编译完还在到处找zImage浪费了不少时间特此提一句。4.3 用BusyBox制作最小initramfsLinux内核本身只是一个“空壳”启动后需要挂载根文件系统找到init进程。最方便的方案是做initramfs也就是把根文件系统打包成一个cpio归档由内核在启动时解压到内存里。这样做不需要磁盘驱动也不需要文件系统驱动最适合学习和验证。先下载BusyBoxcd ~ wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 tar -jxf busybox-1.36.1.tar.bz2 cd busybox-1.36.1配置并编译make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- defconfig make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- menuconfig注意在menuconfig里一定要进入BusyBox Settings - Build Options勾选Build static binary (no shared libs)。这一步原因很直接如果BusyBox动态链接glibcinitramfs里还得额外带上完整的libc库否则启动后一堆命令报错找不到共享库。静态链接可以把所有依赖打进去一个busybox二进制就是整根系统。然后编译安装到指定目录make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- -j$(nproc) make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- CONFIG_PREFIX$HOME/rootfs install接下来补上proc、sys、dev目录和init脚本cd ~/rootfs mkdir -p proc sys dev etc sudo mknod -m 622 dev/console c 5 1init脚本内容如下#!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys echo Hello from RISC-V Linux init! echo Running with PID $$ exec /bin/sh最后把整个rootfs目录打包成initramfscd ~/rootfs find . -print0 | cpio --null -ov --formatnewc | gzip -9 ~/rootfs.cpio.gz这个打包方式的原理比较简单用cpio把目录结构变成归档再用gzip压缩。内核收到cpio格式的initramfs后会自动在内存中解压把它作为临时根文件系统。4.4 QEMU启动参数的拆解现在内核和根文件系统都齐了用一条命令把RISC-V Linux跑起来qemu-system-riscv64 \ -M virt \ -m 1G \ -smp 2 \ -kernel ~/linux-6.12/arch/riscv/boot/Image \ -initrd ~/rootfs.cpio.gz \ -nographic \ -append consolettyS0 rdinit/init如果你启动后什么都看不到先在内核命令行加earlycon看看早期日志-append consolettyS0 rdinit/init earlyconsbi把参数拆开解释一下-M virt选择QEMU的virt虚拟机平台这是QEMU为RISC-V设计的通用虚拟平台包含了UART、PLIC中断控制器、CLINT定时器、PCIe控制器等外设。-m 1G给虚拟机分配1GB内存。Linux在RISC-V上对内存大小比较敏感太小了会频繁swap1G足够学习。-smp 2给虚拟机两个CPU核心。-kernel指定编译好的内核镜像。-initrd指定initramfs归档。-nographic把虚拟机的串口输出重定向到当前终端方便调试。-append向内核传递命令行参数。consolettyS0告诉内核把日志输出到串口0rdinit/init告诉内核initramfs解压后执行/init脚本。QEMU virt机器默认会自动生成设备树DTB并传给内核所以大部分情况下你不需要手动指定-dtb。如果你遇到外设初始化异常或者想看看DTB里的节点结构可以手动导出qemu-system-riscv64 -machine virt,dumpdtbriscv64-virt.dtb然后在启动命令里加上-dtb riscv64-virt.dtb这样内核使用的设备树就完全由你掌控了。4.5 启动后能观察到什么启动成功后你应该会看到OpenSBI的banner、内核早期的初始化日志然后进入init脚本最后停在BusyBox的sh提示符下。此时可以执行uname -a cat /proc/cpuinfouname会显示内核版本和CPU架构cat /proc/cpuinfo能看到当前核的ISA扩展信息。这就意味着你已经有了一套可以在QEMU里“折腾”的RISC-V Linux环境。接下来无论是写内核模块、调试设备驱动还是研究系统调用都有了落脚点。5. 两套系统在RISC-V上的运行边界从启动流程到内存管理跑通只是第一步真正让你和别人拉开差距的是理解这两个系统在RISC-V上各自的运行模式。5.1 M模式、S模式与OpenSBIRISC-V特有的固件层RISC-V定义了三种特权模式机器模式M-mode、监管者模式S-mode和用户模式U-mode。类比ARM世界的话M-mode相当于EL3或Secure MonitorS-mode相当于EL1内核态U-mode相当于EL0用户态。Zephyr在RISC-V上默认跑在M-mode它可以直接访问所有硬件资源不需要任何固件支持上电之后代码直接执行这是它启动极快的根本原因。Linux则必须跑在S-mode因为Linux要利用MMU做进程隔离而MMU的配置和异常处理需要OS在S-mode完成。那么S-mode下的内核想操作硬件怎么办RISC-V规定了一条标准通道ecall指令。S-mode调用ecall陷入M-mode由M-mode固件代为完成硬件访问。这个M-mode固件最主流的就是OpenSBI。你在QEMU启动日志里看到的OpenSBI信息就是这段固件的功劳。所以RISC-V跑Linux的完整启动链路是OpenSBIM-mode先接管硬件然后跳转到Linux内核S-mode。如果没有OpenSBILinux起不来。5.2 Zephyr的启动链路从reset向量到mainZephyr在RISC-V上的启动流程很直接。处理器复位后从开始地址执行arch/riscv/core/start.S里的代码做的事情包括初始化栈指针sp清零bss段设置gp全局指针准备C运行环境跳转到C语言入口z_cstart。z_cstart会完成内核对象初始化、调度器初始化、设备驱动初始化最后调用main函数。整个过程是线性的没有虚拟地址转换没有页表所有指针都是物理地址。在嵌入式实时场景里这种直接、确定性的启动方式是巨大优势。5.3 Linux的启动链路从OpenSBI到initLinux在RISC-V上的启动链路要复杂得多。OpenSBI把CPU带到S-mode后跳转到内核入口_ start_kernel在汇编阶段完成早期页表建立和CPU特性检测然后进入start_kernel完成大批子系统初始化最终通过kernel_init线程创建第一个用户态进程。这个过程中有一个关键动作Linux需要先解析设备树确认内存大小、中断控制器、串口等外设信息然后建立完整的页表映射打开MMU。之后所有内核代码和数据结构都运行在虚拟地址上。这也是为什么RISC-V Linux对内存大小有硬性要求的原因之一——页表本身、vmemmap、各种内核数据结构都需要物理内存支撑。5.4 有没有MMU决定了这两种系统的天花板有没有MMU是Zephyr和Linux之间最本质的分水岭。Zephyr面向的是“直接操作硬件”的场景不需要虚拟内存带来的隔离和保护因此上下文切换的开销极小只是保存和恢复寄存器。实时任务的确定性很强。Linux面向的是“多进程共享CPU和内存”的场景每个进程有自己的独立地址空间一个进程崩溃不会拖垮整个系统。这种隔离靠的是MMU的页表映射。但代价是上下文切换时要切换页表TLB也要失效性能开销远高于RTOS。用一个生活化的类比Zephyr像一个小作坊所有材料和工具都摆在桌面上师傅拿起来就能干活速度快但大家共享同一张桌子Linux像一个大企业每个部门有独立办公室文件和权限分得很细工作安全、互不干扰但进出办公室都得刷卡自然慢一些。理解了这点你在选择技术路线时就有了理论依据如果项目需要硬实时、低功耗、秒级启动Zephyr合适如果需要复杂应用、网络协议栈、文件系统Linux更合适。6. 换到真实开发板之前先排掉这些雷QEMU跑得很顺不代表真实板子上也能一键成功。从模拟器到真实硬件有几个坑我几乎是必踩的。6.1 板卡选型官方支持列表比任何宣传都靠谱买板子前先查两件事。一是Zephyr的boards/riscv目录下有没有这块板子二是Linux内核arch/riscv/boot/dts下有没有对应设备树。官方支持列表就是最权威的兼容性清单。我见过几个典型例子SiFive HiFive1 Rev BFE310芯片RV32IMAC没有MMU只能跑Zephyr或裸机程序。SiFive HiFive UnmatchedU740多核RV64可以跑完整的Linux开箱支持很好。StarFive VisionFive V2JH7110四核RV64Linux支持较完善Zephyr也有社区移植。如果你看到某块板子芯片很新、性能很强但Linux和Zephyr都没有官方支持那就要做好自己移植驱动的心理准备这种工作量对一个新手来说非常劝退。6.2 Zephyr的devicetree和Linux的DTB不能互拷Zephyr和Linux都用设备树来描述硬件但这是两套不完全兼容的“语言”。Zephyr的设备树绑定有很多Zephyr特有属性比如console设备标记、flash分区定义、GPIO中断标志的Zephyr扩展而Linux的DTB遵循Linux内核的binding规范。有次我把Linux里一块板子的dts文件直接拷贝到Zephyr工程下编译虽然通过但串口驱动怎么都起不来日志里提示console设备找不到。排查到最后才发现Zephyr的UART节点里需要statusokay和相应的clocks属性而Linux的dts里这些字段可能是空着的或者由bootloader在运行时修补。正确做法是在Zephyr工程中以Zephyr提供的SoC级dtsi为基准只覆盖自己板卡相关的差异在Linux侧则以上游内核的dts为基准修改。两边不要互相搬运。6.3 串口和时钟频率最常见的板级翻车现场QEMU里面UART时钟频率是理想值你随便设置波特率都能输出。真实板卡可没这么宽容UART外设的时钟源可能来自SoC内部的PLL频率不匹配最典型的表现就是串口输出乱码。排查乱码问题的思路很固定查看板卡的原理图确认调试串口的电平标准和引脚复用。查SoC手册确认UART模块的输入时钟频率。在Zephyr的dts中检查clocks字段在Linux的dts中检查clock-frequency字段两边必须和硬件实际值一致。如果乱码还是存在用逻辑分析仪抓TX引脚的波形数一下位宽能估算出实际波特率。这套排查链路我走了一遍之后基本不会再被串口问题卡住。6.4 用QEMU调试内核早期崩溃的小技巧真实板卡上调试Linux早期崩溃比较麻烦但QEMU里有一个非常方便的调试手段。启动QEMU时加上-s -S参数qemu-system-riscv64 \ -M virt -m 1G -smp 2 \ -kernel ~/linux-6.12/arch/riscv/boot/Image \ -initrd ~/rootfs.cpio.gz \ -nographic \ -append consolettyS0 rdinit/init \ -s -S-s表示在TCP 1234端口打开GDB调试服务-S表示QEMU启动后先暂停等待调试器连接。然后在另一个终端gdb ~/linux-6.12/vmlinux (gdb) target remote :1234 (gdb) hbreak start_kernel (gdb) continue这样你就可以在内核入口处断下来逐步跟踪早期启动过程。很多内核早期崩溃比如页表配置错误、设备树解析失败都可以用这种方式快速定位。7. 一点实操后的个人体会这套流程完整走下来之后我对RISC-V、Zephyr和Linux的理解比过去几年看文档都深。在这里分享一些个人体会。先学哪个我建议先Zephyr后Linux。Zephyr的构建系统虽然初看很绕west、CMake、Kconfig、devicetree但它规模小你能在一天内看到完整的“从源码到运行”闭环。有了这个基础再去碰Linux你会更容易理解内核在干什么而不是被编译日志淹没。学的时候尽量别一股脑上高性能开发板。QEMU的好处是你敢随便改配置改坏了重来就是一行命令的事。真实板子刷坏了可能得用JTAG救砖心态完全不一样。我自己的经验是QEMU上的Zephyr和Linux各跑通一遍之后再拿真实板子来验证整个过程会顺畅很多。再分享一个小技巧遇到问题时先去看上游代码和官方文档而不是零散搜博客。Zephyr的Documentation目录、Linux的Documentation/riscv目录都是很好的起点。网络上很多教程是别人基于老版本写的RISC-V生态迭代太快版本不匹配会让你莫名其妙的踩坑。最后想说的是RISC-V学习的最大价值不在于你用哪个OS而在于你能同时看到RTOS和通用OS在同一个ISA上是怎么协作、怎么分工的。这份全局视角在传统ARM生态里需要花不少钱和时间才能摸到边在RISC-V上你只要一个免费模拟器。把这份折腾精神保持住你的嵌入式功底一定会比同龄人厚实不少。
返回列表