ARTICLE DETAIL

资讯详情

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

QEMU虚拟开发板定制实战:从设备树到自定义machine

QEMU虚拟开发板定制实战:从设备树到自定义machine 上篇讲完QEMU装好、能拉起一个最小系统之后好几个朋友跑来问同一件事我手上明明有真开发板为什么非要在QEMU里再搞一块假的这个问题乍看很简单但真正回答清楚恰恰是理解定制开发板这件事的关键。嵌入式开发到了中后期真正卡脖子的不是不会写驱动而是板子资源不够用、环境不可复现、CI没法自动化。QEMU干的不是替代你的开发板而是把开发板这个概念本身变成一段可配置、可复制、可提交到Git仓库里的代码。这篇接着上篇往下走从机器模型选型、内核与根文件系统构建到设备树改写、自定义machine完整梳理一遍在QEMU里定制开发板的实战路径。适合正在做嵌入式、内核、驱动开发或者准备搭自动化测试环境的人参考。1. 为什么非得在QEMU里定制一块开发板先说一个反直觉的事实越是规模大的嵌入式团队越舍得在QEMU上投入精力做一块虚拟板卡而不是多买几块真板子摊到每个人桌上。刚入行的人经常不理解觉得QEMU慢、模拟不了外设细节、跟真板差别大。这些评价都对但它们全都建立在拿QEMU去替代真板卡的错误预设上。QEMU真正解决的不是替代,而是腾挪。1.1 真实板卡的三个痛点第一个痛点是板卡资源分配。做过量产项目的人都懂一个项目组里四五个人共用两三块开发板是常态。硬件工程师要调电源时序驱动工程师要验一个网口驱动应用工程师要跑性能压测大家全挤在同一块板卡上谁都不敢乱动别人的环境。而QEMU不存在这个问题起一个实例就是一块独立板子想起几个起几个。第二个痛点是故障注入和边界场景。真实板卡出问题往往是被动的比如某个引脚接触不良、电压跌落、硬件看门狗复位。你没法用一块健康的好板子去主动测试内核在异常路径下的行为更不敢在量产阶段把板子故意搞坏。但虚拟板可以可以烧一个故意跑飞的内核镜像可以断掉某个虚拟外设的中断线可以让内存读写返回错误。这些在QEMU里都有对应的调试接口。第三个痛点是CI和回归测试。持续集成需要的是确定性的、可重复执行的环境。真板卡放到CI里会面临上电时序不确定、网络环境依赖、串口线被占用、半夜断电等各种幺蛾子。QEMU虚拟板则天然适合跑CI启动一次几分钟跑完销毁下个任务再拉起来干净利落。1.2 QEMU定制开发板的两条路线看QEMU的文档或者各家博客会看到定制开发板这个词被用得很宽泛。如果拆开看其实有两条完全不同的路线。路线A是用QEMU已经提供的machine模型配合参数、设备树DTB和动态设备插入拼出一台很像自己目标板的虚拟机器。比如用-machine virt做基础通过自定义设备树给自己的虚拟驱动挂节点再用-device virtio-net-pci接网卡用-device pci-testdev模拟测试外设。这条路不碰QEMU源码适合绝大多数内核、驱动、应用开发者。路线B是直接改QEMU源码在hw/arm/目录下新增一个machine类型注册一个-M myboard选项从内存布局、CPU类型、中断控制器、串口地址、总线拓扑全部自己定义。这条路适合芯片厂商、BSP团队和平台级开发者他们需要精确模拟一款还不存在或者不方便公开的硬件平台。这两条路没有高低之分只有阶段之分。我的建议是先熟练路线A把设备树、virtio、中断、串口这些基础概念吃透再决定要不要下探到源码级。直接从源码级入门很容易被QEMU内部的QOM对象模型劝退。2. 从装环境到挑板子机器模型选型与安装细节既然要定制板卡第一步是确保QEMU环境完整并且知道这台QEMU到底能模拟哪些开发板。很多人装了一个qemu-system-x86_64就以为万事大吉结果发现根本没法模拟ARM板卡。2.1 安装QEMU的正确姿势在Debian/Ubuntu系系统上不要只装qemu元包要明确安装面向ARM系统的模拟器sudo apt install qemu-system-arm qemu-system-aarch64 qemu-user qemu-user-staticqemu-system-aarch64是完整系统模拟器它能跑完整的ARM64 Linux内核qemu-user和qemu-user-static则是用户态模拟器用于直接在x86宿主机上跑一个ARM二进制程序更快但无法模拟整个系统。这两类工具用途不同建议一起装上。macOS上用Homebrew就简单很多brew install qemuWindows用户可以直接去QEMU官网下载安装包但注意把安装目录加入PATH后续命令行操作会顺手很多。如果你打算深入学习自定义machine强烈建议源码编译一次wget https://download.qemu.org/qemu-8.2.0.tar.xz tar xf qemu-8.2.0.tar.xz cd qemu-8.2.0 ./configure --target-listaarch64-softmmu,arm-softmmu --enable-debug make -j$(nproc)--target-list只编译你需要的目标架构大幅缩短编译时间。--enable-debug会带上调试符号后面自己加断点排查问题会舒服很多。只模拟ARM开发板的话aarch64-softmmu和arm-softmmu两个目标足够。2.2 摸清QEMU支持哪些开发板装好之后先用一条命令摸底qemu-system-aarch64 -machine help这条命令会列出所有可用的机器类型。因为QEMU版本和编译选项不同不同机器上的列表会有些差异但下面这些常见的模型值得认识一下machine名架构CPU默认典型用途virtARM64cortex-a53通用虚拟平台外设丰富推荐首选vexpress-a9ARM32cortex-a9经典教学板文档资料最丰富vexpress-a15ARM32cortex-a15vexpress的A15版本sabreliteARM32cortex-a9飞思卡尔i.MX6Q开发板mcimx6ul-evkARM32cortex-a7恩智浦i.MX6UL评估板raspi3b / raspi4bARM64cortex-a53 / a72树莓派3B/4B的板级模拟highbankARM32cortex-a9Calxeda服务器板xlnx-zynq-a9ARM32cortex-a9Xilinx Zynq-7000系列microbitARM32cortex-m0BBC micro:bit单片机板mps2-an385ARM32cortex-m3ARM MPS2单片机评估板很多人以为QEMU只能模拟通用虚拟机看到这个列表才会意识到它是真的在做板级模拟。不同machine之间不只是名字不同底层的内存映射、中断控制器、串口地址、外设接口全都不一样。比如vexpress-a9上PL011串口的地址和virt板上就不同直接导致内核启动时的earlycon参数不同。2.3 选型逻辑不是越像越好选machine最忌讳的是哪个板子跟我的目标硬件名字像就选哪个。实际选型要看你的目标是什么。如果你是为了跑Linux内核、调试驱动、做CI验证virt几乎是最优选择。它是QEMU为虚拟化专门设计的机器内置了PCIe总线、virtio系列设备、通用中断控制器GIC内核标准发行版基本都能启动外设通过PCIe动态添加非常灵活。如果你想学习板级BSP移植、折腾U-Boot、理解platform device是如何通过设备树匹配驱动的vexpress-a9是经典中的经典网上能找到大量针对它进行内核移植的教程踩坑成本低。如果你想模拟特定厂家的量产板卡比如需要跑i.MX6的官方BSP或者树莓派官方镜像那就要选对应型号的machine。但要提醒一句这类板级模拟往往需要配套固件文件配置复杂新手容易卡在启动早期阶段。我见过太多人一上来就挑战raspi4b结果卡在固件加载上白白消耗热情。选型的核心逻辑是虚拟板不需要看起来像你的目标板只需要行为上能覆盖你关心的那些设备和接口。想通这一点很多纠结自然就解开了。3. 完整跑通一块ARM64虚拟开发板virt板全流程实测理论知识说再多不如亲手起一块板子。这一节我们从零开始在QEMU的virt板上跑通一个完整的ARM64 Linux系统。整个过程分四步交叉编译内核、制作根文件系统、拼启动命令、验证启动结果。3.1 交叉编译内核工具链与配置选择在x86宿主机上编译ARM64内核必须用交叉编译工具链sudo apt install gcc-aarch64-linux-gnu然后下载内核源码并开始编译export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- make defconfig make -j$(nproc)export ARCHarm64是告诉编译系统目标架构export CROSS_COMPILEaarch64-linux-gnu-是让编译系统知道去调用交叉工具链而不是宿主机的gcc。这两行环境变量写错任何一个都会在编译过程中出现看不懂的报错最常见的就是没有找到-lgcc一类的问题。make defconfig会生成arm64架构的默认内核配置。这个配置对QEMUvirt板非常友好因为它已经开启了串口、virtio、PCIe、GIC等驱动选项。如果你想进一步缩小内核可以之后再执行make menuconfig裁剪但第一次跑通完整系统不建议过度裁剪否则会遇到某个驱动没编进去导致外设不工作这种恼火的问题。编译完成后产物在arch/arm64/boot/Image。注意是Image不是zImageARM64不使用ARM32那套压缩镜像格式。3.2 制作最小根文件系统busybox手工流内核跑起来之后还需要一个根文件系统。最轻量的方案是用busybox手工搭建一个initramfs。先下载并编译busyboxwget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 tar xf busybox-1.36.1.tar.bz2 cd busybox-1.36.1 export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- make defconfig make menuconfig进入menuconfig后找到Settings-Build static binary (no shared libs)把它选上。这一步非常关键因为initramfs里只有一个最小根文件系统动态链接busybox会导致它找不到动态链接器而无法运行。选完保存退出然后make -j$(nproc) make installmake install会把编译好的busybox和一堆软链接放到_install目录。接下来手动组装rootfsmkdir -p rootfs/{bin,sbin,etc,proc,sys,dev,lib,lib64,usr,root,tmp,var,mnt} cp -a _install/* rootfs/创建/init脚本它是initramfs的入口cat rootfs/init EOF #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs devtmpfs /dev echo Hello QEMU Board exec /bin/sh EOF chmod x rootfs/init最后用cpio打包cd rootfs find . | cpio -H newc -o | gzip ../initramfs.cpio.gz这个initramfs.cpio.gz就是我们的文件系统盘。3.3 启动参数逐项拆解和验证内核和文件系统都准备好了接下来是启动命令。我在本地反复验证过的稳定启动命令如下qemu-system-aarch64 \ -machine virt \ -cpu cortex-a72 \ -smp 2 \ -m 1G \ -kernel Image \ -initrd initramfs.cpio.gz \ -append consolettyAMA0 rdinit/init panic-1 \ -nographic每个参数都值得说道说道-machine virt指定机器类型是virt。-cpu cortex-a72指定CPU型号。x86宿主机上QEMU通过TCG做二进制翻译cortex-a72是一个比较平衡的选择。也可以改用max让QEMU把TCG支持的所有ARM64特性全部打开适合测试新指令集特性。-smp 2两个CPU核。-m 1G1GB内存。-kernel Image加载编译出的内核镜像。-initrd initramfs.cpio.gz加载initramfs。-append consolettyAMA0 rdinit/init panic-1内核启动参数。consolettyAMA0对应ARM PrimeCell PL011串口驱动rdinit/init告诉内核从initramfs中执行/initpanic-1让内核在panic后自动重启排查时方便一些。-nographic把串口重定向到当前终端不用额外开一个图形窗口。执行之后预期会看到内核启动日志最后出现Hello QEMU Board然后进入一个/bin/sh交互shell。到这一步一块由你亲手构建的ARM64虚拟开发板就算正式跑起来了。提示如果你的-append里用了consolettyAMA0但一直看不到输出多半是内核里没有开启CONFIG_SERIAL_AMBA_PL011。先回内核目录检查一下配置再重新编译。3.4 用buildroot一步到位如果觉得手动编busybox太繁琐日常做实验可以直接用buildroot。buildroot已经内置了针对QEMU virt板的默认配置git clone https://git.builder.org/buildroot.git cd buildroot make qemu_aarch64_virt_defconfig make -j$(nproc)编译完成后output/images/目录下会生成内核镜像、根文件系统镜像和启动脚本。buildroot会帮你处理内核编译、busybox配置、rootfs打包的所有环节。它适合快速验证但不适合深入学习因为它把太多细节封装掉了。我的建议是第一次手动走一遍全流程之后用buildroot提高效率。4. 开始定制设备树改写与自定义外设接入跑通一个通用Linux系统还不够真正有意思的是定制。这一节进入核心部分怎么让这块虚拟板长成你想要的样子。4.1 设备树从哪来dumpdtb和dtc反编译ARM64内核依赖设备树DTB来描述硬件拓扑。QEMU在启动时会根据machine类型和-cpu、-m、-device等参数动态生成一份设备树。我们可以把它导出来看看qemu-system-aarch64 \ -machine virt,dumpdtb/tmp/virt.dtb \ -cpu cortex-a72 -smp 2 -m 1G执行后QEMU会把生成的设备树写到/tmp/virt.dtb。接着用设备树编译工具反编译成可读的dts源码dtc -I dtb -O dts /tmp/virt.dtb -o /tmp/virt.dts打开virt.dts能看到完整的设备树结构CPU节点、GIC中断控制器、PL011串口、PCIe控制器、virtio-mmio节点等。这是QEMU自己生成的主板原理图所有硬件资源都在这里登记。4.2 设备树定制实战给虚拟板加一个自定义节点假设你现在要开发一个PCIe外设驱动但是虚拟板上还没有这个设备节点。可以通过修改设备树来自定义。举一个实际例子。比如我想在0x10000000地址放一个自定义的platform设备给内核传一个compatible属性让我的驱动可以匹配它/ { mydev10000000 { compatible mycompany,mydev; reg 0x0 0x10000000 0x0 0x1000; interrupts 0x0 0x50 0x4; interrupt-parent gic; }; };操作流程是反编译virt.dtb得到dts源码添加这段节点再编译回dtbdtc -I dts -O dtb /tmp/virt.dts -o /tmp/myboard.dtb启动命令里加上-dtb /tmp/myboard.dtbQEMU就会使用这份自定义设备树qemu-system-aarch64 \ -machine virt \ -cpu cortex-a72 \ -m 1G \ -kernel Image \ -initrd initramfs.cpio.gz \ -dtb /tmp/myboard.dtb \ -append consolettyAMA0 rdinit/init panic-1 \ -nographic我在实际测试中发现如果QEMU指定了外部dtb它不会再自动把PCIe热插拔、CPU停车等动态信息写进设备树所以改动越少越好尽量保持原设备树结构只新增不删改。内核启动后如果驱动支持OF匹配会看到驱动probe被调用。提示设备树节点里的interrupts字段、interrupt-parent引用必须跟QEMU实际生成的中断控制器匹配。virt板使用GICSPI中断号通常从32开始这里0x0 0x50 0x4中的0x50表示SPI中断号800x4代表高电平触发。写错中断号驱动不会收到任何中断。4.3 用-device动态组合外设近似搭出目标板形态QEMU的-device参数可以在启动时动态挂接设备这是快速搭建定制板的利器。常见组合如下qemu-system-aarch64 \ -machine virt \ -cpu cortex-a72 -smp 4 -m 2G \ -kernel Image \ -initrd initramfs.cpio.gz \ -append consolettyAMA0 rdinit/init panic-1 \ -netdev user,idnet0,hostfwdtcp::2222-:22 \ -device virtio-net-pci,netdevnet0 \ -drive filedisk.img,formatraw,ifnone,iddisk0 \ -device virtio-blk-pci,drivedisk0 \ -device pci-testdev \ -nographic这里用-device virtio-net-pci给虚拟板加了一块virtio网卡用-device virtio-blk-pci挂了一块磁盘用-device pci-testdev挂了一个调试用的PCI测试设备。pci-testdev是QEMU原生提供的测试外设支持内存映射IO、端口IO、中断触发、DMA等多种测试写驱动之前先拿它练手非常合适。这种方式的妙处在于你不用改QEMU源码也不用改内核只通过命令行就能把一块只存在于命令行里的定制开发板搭出来而且每一条-device都对应内核里一个真实的驱动绑定。4.4 源码级自定义machine属于平台开发者的玩法当现有machine无法满足需求比如内存布局完全不同、需要自定义复位逻辑、希望-M myboard这样直接注册一块新板卡时就得改QEMU源码了。以ARM64平台为例新增machine的代码放在hw/arm/目录下。一个最小骨架长这样#include qemu/osdep.h #include hw/boards.h #include qapi/error.h #include qemu/error-report.h static void myboard_init(MachineState *machine) { // 创建内存 MemoryRegion *ram g_new(MemoryRegion, 1); memory_region_init_ram(ram, NULL, myboard.ram, machine-ram_size, error_fatal); memory_region_add_subregion(get_system_memory(), 0x40000000, ram); // 创建CPU if (!machine-cpu_type) { machine-cpu_type ARM_CPU_TYPE_NAME(cortex-a9); } // 实际项目里要遍历 machine-smp.cpus逐CPU创建并初始化中断控制器 // 再把PL011串口、定时器、GPIO等外设接到对应地址空间 } static void myboard_machine_init(MachineClass *mc) { mc-desc My Custom Development Board; mc-init myboard_init; mc-default_cpu_type ARM_CPU_TYPE_NAME(cortex-a9); mc-min_ram_size 64 * MiB; mc-default_ram_size 256 * MiB; } DEFINE_MACHINE(myboard, myboard_machine_init)这里面省略了中断控制器初始化、CPU核间通信、串口注册等大量细节但它表达了核心思想一个machine就是往系统总线上挂CPU、内存、外设并把这些设备的寄存器地址告诉内核设备树。QEMU源码hw/arm/里有很多现成参考比如vexpress.c、sabrelite.c代码风格都很清晰。修改完源码重新编译QEMU./configure --target-listaarch64-softmmu --enable-debug make -j$(nproc)编译后执行./build/qemu-system-aarch64 -M help | grep myboard如果看到myboard出现恭喜你的第一块源码级定制开发板已经注册成功了。5. 开发与调试工作流让虚拟板成为日常生产力工具定制板卡不是最终目的最终目的是让这块虚拟板成为高效开发工具。这一节讲几个我在实际项目中高频使用的工作流技巧。5.1 网络打通user模式端口转发与tap桥接QEMU的user模式网络是最简单的方案它让guest中的网卡通过宿主机的用户态协议栈访问外网。配合hostfwd做端口转发可以方便地从宿主机SSH到虚拟板里qemu-system-aarch64 ... \ -netdev user,idnet0,hostfwdtcp::2222-:22 \ -device virtio-net-pci,netdevnet0启动后在宿主机执行ssh -p 2222 rootlocalhost就能直接连到guest的22端口。这个方案配置简单适合日常调试。但user模式网络有几个固有缺陷延迟较高不支持ICMPguest内部无法主动访问宿主机上其他容器。如果你需要更接近真实网络环境比如让虚拟板出现在局域网里做设备发现测试可以用tap桥接sudo ip tuntap add dev tap0 mode tap sudo ip addr add 192.168.100.1/24 dev tap0 sudo ip link set tap0 up然后启动QEMUqemu-system-aarch64 ... \ -netdev tap,idnet0,ifnametap0,scriptno,downscriptno \ -device virtio-net-pci,netdevnet0tap模式让虚拟板直接接到宿主的网络栈上性能和真实性都更接近实机。代价是需要root权限且网络拓扑配置相对复杂。5.2 内核调试GDB连接QEMU的几种姿势驱动开发中遇到crash最有效的办法是让QEMU挂在GDB上。QEMU内置了一个GDB server只要在启动命令中加两个参数qemu-system-aarch64 \ -machine virt -cpu cortex-a72 -smp 2 -m 1G \ -kernel Image -initrd initramfs.cpio.gz \ -append consolettyAMA0 rdinit/init panic-1 \ -nographic \ -s -S-s缩写等价于-gdb tcp::1234表示在1234端口开启GDB server-S表示启动时先暂停CPU等待调试器连接。另一个终端启动调试器aarch64-linux-gnu-gdb vmlinux (gdb) target remote :1234 (gdb) hb start_kernel (gdb) cvmlinux是编译内核时生成的带符号的可执行文件位于内核源码根目录。用GDB可以在start_kernel上打断点也可以给某个驱动函数的地址下断点然后c继续运行等断点命中后查寄存器、看调用栈。提示TCG软件模拟模式下GDB调试会有明显的性能开销但用于定位驱动初始化阶段的逻辑错误完全够用。如果要调试性能相关的问题建议换回物理板。5.3 一键化启动脚本模板启动命令越来越长之后手动敲命令容易出错。我习惯在项目仓库里维护一个run.sh把内核、设备树、磁盘镜像、网络配置全部参数化#!/usr/bin/env bash set -e IMAGE${IMAGE:-linux/arch/arm64/boot/Image} INITRD${INITRD:-rootfs/initramfs.cpio.gz} DTB${DTB:-/tmp/myboard.dtb} DISK${DISK:-disk.img} qemu-system-aarch64 \ -machine virt \ -cpu cortex-a72 \ -smp 4 \ -m 2G \ -kernel $IMAGE \ -initrd $INITRD \ -dtb $DTB \ -append consolettyAMA0 rdinit/init panic-1 \ -netdev user,idnet0,hostfwdtcp::2222-:22 \ -device virtio-net-pci,netdevnet0 \ -drive file$DISK,formatraw,ifnone,iddisk0 \ -device virtio-blk-pci,drivedisk0 \ -nographic有了这样一个脚本任何时候clone下来只要把内核和rootfs编译好执行./run.sh就是一台虚拟开发板。这种一条命令拉起一块板子的体验是真板卡永远给不了的。5.4 我踩过的几个坑和排查思路QEMU虚拟板调试中最费时间的问题往往是最基础的环境问题。我把高频问题整理成了表现象常见原因排查/解法启动后没有任何串口输出console参数与内核配置不匹配确认内核开启CONFIG_SERIAL_AMBA_PL011改用consolettyAMA0或开启8250后用consolettyS0卡在Starting kernel ...不动CPU模型太新/太旧或内核早期汇编不兼容尝试-cpu max确认QEMU版本较新shell执行/bin/sh报No such file or directorybusybox是动态链接initramfs里缺动态链接器静态编译busybox重新打包init脚本执行后panicrdinit路径不对或根文件系统挂载失败确认rdinit/init路径/init有执行权限脚本内先mount proc/sys/devvirtio网卡识别不到内核缺少virtio驱动打开CONFIG_VIRTIO_NET、CONFIG_VIRTIO_PCI后重编内核-dtb自定义设备树启动失败设备树改动破坏了QEMU内置信息尽量只新增节点不删除QEMU原有节点确认中断号和地址正确x86宿主上用了-accel kvm报错KVM是架构相关的x86宿主不能加速ARM64 guestARM64 guest在x86宿主上使用-accel tcg这里特别想说一个相互成就的细节QEMU版本和内核版本之间是有连带关系的。新内核可能依赖新的GIC版本、新的虚拟化扩展如果你的QEMU太老就会在启动早期莫名其妙crash。遇到这类问题优先检查QEMU版本别一上来就怀疑自己的代码。根据我个人经验如果严格按照上面这套流程走卡住你的通常不是QEMU本身而是内核配置。遇到问题别急着换参数先打开make menuconfig检查对应驱动和子系统有没有编进去这是排查虚拟板启动问题最高效的切入点。最后再分享一个习惯我几乎从不把启动命令揉在一行里手动敲。一个带参数化的启动脚本、一份记录内核配置的config文件、一份说明设备树改动的commit记录这三样东西的价值比任何硬件调试工具都大。当团队新同事五分钟内就能用一条命令拉起一块开发板的时候你就知道当初在QEMU上投入的时间有多值了。
返回列表