拓冰建站拓冰建站
首页 / 资讯中心 / 正文

QEMU模拟IMX6ULL:嵌入式Linux驱动开发实战环境搭建

做了大半年 Linux 驱动开发最让我头疼的不是驱动本身而是手里没有 IMX6ULL 开发板。年初想写一个平台驱动手边只有一台 PC于是我研究出一套完全靠 qemu 模拟 IMX6ULL 开发板的环境用 QEMU 跑内核、挂 rootfs、看串口日志、写字符设备驱动再把驱动编译成模块加载进去。这套方案实测下来覆盖了真实开发板上九成的驱动开发流程内核编译、设备树、模块加载、串口控制台、GDB 远程调试全都能在命令行里闭环。如果你也是刚入行做嵌入式 Linux 驱动或者公司板子还没到位想提前把环境摸熟这篇文章会把从零到一的过程完整走一遍。我会给出能直接复制运行的命令也会解释每条命令背后的理由。那些网上教程不会告诉你的坑我单独拉了一章出来保证都是我自己踩过的。1. 为什么要在 QEMU 里搭 IMX6ULL 驱动环境1.1 实体板子的痛点用实体开发板做驱动开发首先要面对三个问题硬件成本、环境破坏、反复烧录。IMX6ULL 这块板子入门不贵但正点原子、野火这类全套资料加屏幕加各种模块加起来也是一笔开销。更麻烦的是调试驱动时经常要把内核、设备树、根文件系统来回烧写。SD 卡烧一次要几分钟U-Boot 不小心改坏板子直接变砖又要想办法用 OTG 或者仿真器救回来。如果是团队里几个人共用一块板子那冲突更严重——一个人在做内核实验另一个人想测自己的驱动只能排队。QEMU 恰恰能把这三个痛点全消掉。环境是一个普通文件毁了大不了重新解压启动时间从按下电源键到进入 shell 只需要几秒每个人都能在 PC 上独立开一个“板子”而且完全不占实体硬件资源。1.2 QEMU 能模拟到什么程度不要把它想成一个普通的虚拟机。QEMU 在 ARM 模拟方面并不是简单地把 CPU 指令翻译执行而是把 IMX6ULL 这颗 SoC 内部的很多外设控制器也建模了。对驱动开发来说这意味着CPU 层面Cortex-A7 内核的指令能正常跑32 位 ARM 代码、中断、异常、MMU、Cache 都有对应模拟外设层面GPIO、UART、SD 控制器、定时器、看门狗这类常用外设都有寄存器模型内核里的对应驱动能正常 probe、正常读写寄存器板级层面QEMU 提供了类似真实开发板的 machine 模型内核启动时会加载匹配的设备树总线枚举、platform 设备注册、驱动匹配这一整套流程和真实硬件完全一致。换句话说驱动本身的逻辑、注册流程、中断处理、文件操作接口在 QEMU 里验证和真实板子几乎没区别。我在这套环境里调过的字符设备驱动移植回真实板子后几乎没有改动就能跑。1.3 模拟不了的边界我必须把话说清楚QEMU 不是万能的。IMX6ULL 上那些复杂外设比如 LCD 控制器、GPU、视频编解码单元、MIPI CSI 摄像头接口QEMU 的模拟程度很浅甚至完全没有模拟。你在这些外设上做驱动开发还是得靠实体板。另外模拟器的 GPIO 引脚并不会真的产生电平变化。你可以操作寄存器、可以请求中断、可以用它验证驱动框架的流程但不要指望能在 QEMU 里看到 LED 真的亮起来。部分版本的板级模型会把板载 LED 接到 GPIO 上能通过设备模型观察状态变化但这类细节每个 QEMU 版本不一样不能当作可靠依据。所以我的建议是基础驱动、字符设备、平台驱动、中断请求、设备树匹配这类“流程类”开发放心交给 QEMU涉及视频、显示、模拟信号这类强硬件相关的还是得老老实实用板子。2. 环境准备QEMU、交叉编译器与 rootfs2.1 QEMU 安装与版本确认我这里以 Ubuntu 系的发行版为例。用系统包管理器安装是最快的sudo apt install qemu-system-arm装完以后第一件事不是急着启动而是确认你拿到的 QEMU 机器模型。IMX6ULL 的 machine 是后来才加入 QEMU 的旧版本里没有。我自己用的是 QEMU 8.2可以这样查qemu-system-arm -machine help | grep -i imx6输出里应该能看到类似这样的条目mcimx6ul-evk Freescale i.MX6UL Evaluation Kit (Cortex-A7) mcimx6ull-evk Freescale i.MX6ULL Evaluation Kit (Cortex-A7)如果你的系统包版本太老比如 Ubuntu 20.04 自带的 QEMU 4.x很可能只有mcimx6ul-evk而没有mcimx6ull-evk。这时候有两种选择一是直接用mcimx6ul-evkIMX6UL 和 IMX6ULL 在寄存器和设备树层面高度兼容对大多数驱动开发影响不大二是自己编译新版本 QEMU在官网下载源码后按标准流程 configure、make、make install。后者不复杂但会花一些时间我建议先用系统包把环境跑通再回头考虑升级。2.2 交叉工具链选择IMX6ULL 是 32 位 ARM Cortex-A7 处理器所以必须用 ARM 32 位交叉编译器。不少人会在这里犯迷糊顺手装了 aarch64 的交叉编译器编译出来的内核根本跑不起来。Ubuntu 下直接安装sudo apt install gcc-arm-linux-gnueabihf libc6-dev-armhf-cross安装后检查一下arm-linux-gnueabihf-gcc -v正常情况下会显示 GCC 版本号并且 target 是 arm-linux-gnueabihf。注意这个hf后缀表示硬浮点IMX6ULL 的 Cortex-A7 支持 VFPv4 硬浮点选它是没问题的。后面的内核、busybox、驱动模块全部要用这套工具链编译所以先把环境变量固定好export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf-2.3 用 BusyBox 做最小 rootfs内核能启动还不够你得有一个能进去操作的根文件系统。BusyBox 是最经典的方案它把sh、ls、cat、mount这些常用命令集合成一个二进制编译快、体积小非常适合做嵌入式最小环境。先下载 BusyBox 源码版本选 1.36 左右的稳定版。然后tar xjf busybox-1.36.1.tar.bz2 cd busybox-1.36.1 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- defconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j$(nproc)编译完以后把生成的 BusyBox 和命令链接安装到一个临时目录里这个目录就是我们 rootfs 的雏形mkdir -p ../rootfs make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- install CONFIG_PREFIX../rootfs光有 BusyBox 还不够得手工把目录结构和必要设备节点搭好mkdir -p rootfs/{proc,sys,dev,etc/init.d,lib,tmp,var} sudo mknod -m 666 rootfs/dev/console c 5 1 sudo mknod -m 666 rootfs/dev/null c 1 3没有/dev/console是个大坑后面启动时会话直接起不来这个节点必须提前创建。然后写启动脚本。etc/inittab负责告诉 init 进程起来以后干什么::sysinit:/etc/init.d/rcS ::respawn:-/bin/sh ::restart:/sbin/initetc/init.d/rcS的内容#!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs devtmpfs /dev echo echo IMX6ULL QEMU rootfs OK echo 记得给 rcS 加执行权限chmod x rootfs/etc/init.d/rcS2.4 制作 ext4 根文件系统镜像QEMU 里通常把 rootfs 做成一个 ext4 镜像然后模拟成 SD 卡接进系统。大小给 256MB 到 512MB一般 256MB 足够。dd if/dev/zero ofrootfs.img bs1M count256 mkfs.ext4 rootfs.img如果你用的是新版本的 e2fsprogs1.47 及以上可以直接用-d参数把目录内容写进镜像一步到位mkfs.ext4 -d rootfs rootfs.img如果报不支持-d那就挂载后复制mkdir -p mnt sudo mount rootfs.img mnt sudo cp -a rootfs/* mnt/ sudo umount mnt这一步做完rootfs 准备完毕。后面内核里挂载的就是这个文件。3. 内核编译从 defconfig 到 imx6ull 设备树3.1 内核源码选取做 QEMU 环境我强烈建议用主线内核不要用开发板厂商提供的 BSP 内核。原因有二第一厂商 BSP 里的内核往往改了很多板级代码还夹杂着大量针对实体板卡的 NAND 分区、LCD 初始化、WiFi 模组支持这些配置在 QEMU 里不仅没用反而可能引发启动异常。第二主线内核里的 imx6ull 设备树和 QEMU 的 machine 模型配合得最干净你编译完就能启动不用做任何 hack。我用的内核版本是 6.1.y LTS这个版本足够稳定而且对 imx_v6_v7_defconfig 的支持非常完善。内核源码直接从 kernel.org 下载tar xjf linux-6.1.x.tar.xz cd linux-6.1.x3.2 defconfig 的取舍IMX6ULL 属于 i.MX 系列里 armv7 架构的 SoC主线内核里对应的默认配置是imx_v6_v7_defconfig。这个配置会把 i.MX 系列大部分 SoC 的支持都编进去好处是你不用纠结具体选了哪些选项坏处是编译时间稍微长一点但完全在可接受范围内。配置命令make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- imx_v6_v7_defconfig接下来有一个容易被忽略的环节——检查关键配置项。执行grep -E CONFIG_SOC_IMX6ULL|CONFIG_GPIO_IMX|CONFIG_SERIAL_IMX .config这三个配置分别对应 IMX6ULL SoC 支持、GPIO 驱动、串口驱动任何一个缺失都会让后续工作卡住。正常 defconfig 下这三个都是开启的。如果你想用 GDB 调试内核记得额外开启调试信息make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig在 Kernel hacking → Compile-time checks and compiler options 里勾选Compile the kernel with debug info对应配置项是CONFIG_DEBUG_INFO。有了这个后面才能对内核下断点调试。配置完成后开始编译内核镜像make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j$(nproc) zImage编译产物在arch/arm/boot/zImage。这一步顺利的话说明基础工具链没问题。3.3 编译设备树设备树是 IMX6ULL 驱动开发里绕不开的一环。QEMU 启动时会把 dtb 加载到内存内核根据它来枚举平台设备、匹配驱动所以 dtb 必须和 SoC 型号一致。在内核源码目录下执行make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- dtbs完成以后找imx6ull-14x14-evk.dtbfind arch/arm/boot/dts -name imx6ull*.dtb注意路径在不同内核版本里有差异。6.1 及更早版本通常在arch/arm/boot/dts/imx6ull-14x14-evk.dtb6.7 之后内核把设备树源码结构调整过文件挪到了arch/arm/boot/dts/nxp/imx/imx6ull-14x14-evk.dtb。找不到就用 find 全盘搜一下这个最保险。设备树里有一项需要注意内存大小要和 QEMU 的-m参数匹配。imx6ull-14x14-evk.dtb默认声明 512MB 内存我后面启动时用的就是-m 512M两者能对应上。如果你要改内存大小记得同步去改 dtb 或者用 QEMU 的-m参数对齐不然后果可能是内核启动一半直接 panic。3.4 验证产物完整性编译完以后先做一个简单验证避免后面启动时手忙脚乱。核对三个文件是否齐全arch/arm/boot/zImagearch/arm/boot/dts/.../imx6ull-14x14-evk.dtb上一章做的rootfs.img这三个文件是后面 QEMU 启动命令的全部输入。4. QEMU 启动 Linux 的完整命令4.1 启动参数逐项拆解万事俱备现在该上 QEMU 了。我最终使用的启动命令长这样qemu-system-arm \ -M mcimx6ull-evk \ -m 512M \ -kernel zImage \ -dtb imx6ull-14x14-evk.dtb \ -drive filerootfs.img,formatraw,ifsd \ -append root/dev/mmcblk0 rw rootfstypeext4 consolettymxc0,115200 panic5 \ -nographic每个参数都解释一下知道为什么这么写遇到问题才能自己排查-M mcimx6ull-evk指定模拟的板卡模型。如果你的 QEMU 版本没有这个机器名就用mcimx6ul-evk。-m 512M模拟内存大小要和 dtb 里声明的大小匹配。-kernel zImage直接把内核镜像加载到内存启动不需要 U-Boot。这条路径最简单QEMU 会自行处理 zImage 的加载地址。-dtb imx6ull-14x14-evk.dtb把设备树二进制传给内核。-drive filerootfs.img,formatraw,ifsd把 rootfs 镜像模拟成 SD 卡系统里会出现 mmcblk0 设备。-append内核启动参数。root/dev/mmcblk0指定根文件系统所在设备consolettymxc0,115200指定内核串口控制台IMX 系列的第一路 UART 在 Linux 里的设备名是 ttymxc0panic5指定内核 panic 后 5 秒自动重启调试时很有用。-nographic把串口输出重定向到当前终端不使用图形窗口。跑嵌入式模拟基本都是这个用法。4.2 第一次启动成功的输出长什么样输入启动命令后你会看到一串内核启动日志最终停在类似这样的界面... VFS: Mounted root (ext4 filesystem) on device 179:0. devtmpfs: mounted Freeing unused kernel image (initmem) memory: 1024K Run /sbin/init as init process IMX6ULL QEMU rootfs OK / #看到这个/ #提示符恭喜你的 QEMU 环境已经全部跑通了。进去先敲几个命令验证环境完整性/ # uname -a Linux (none) 6.1.xx #1 SMP ... armv7l GNU/Linux / # cat /proc/cmdline root/dev/mmcblk0 rw rootfstypeext4 consolettymxc0,115200 panic5 / # ls /sys/bus/platform/devices//sys/bus/platform/devices/下能看到大量由设备树生成的 platform 设备这证明设备树已经被内核正确解析了。到了这一步你已经拥有了一个和真实开发板高度相似的 Linux 运行环境。4.3 网络与外部访问可选如果你要给 QEMU 加网络接法比较直接qemu-system-arm \ -M mcimx6ull-evk \ -m 512M \ -kernel zImage \ -dtb imx6ull-14x14-evk.dtb \ -drive filerootfs.img,formatraw,ifsd \ -append root/dev/mmcblk0 rw rootfstypeext4 consolettymxc0,115200 panic5 \ -netdev user,ideth0 \ -device imx_fec,netdeveth0 \ -nographic进系统后用ifconfig -a或者ip link show看有没有eth0。需要提醒的是QEMU 不同版本对 i.MX 网卡模型的名字和绑定方式有差异如果你用的版本里imx_fec设备名不对可以用qemu-system-arm -device help | grep -i imx查一下支持列表。说句实话调试驱动阶段网络不是必需品。文件要传进系统更省事的方式是在 rootfs 镜像制作时就拷贝进去或者用 9p 共享目录我建议新手先把网络这层放一放别让它干扰主线。5. 驱动调试从写代码到断点5.1 最小字符设备驱动环境跑通了接下来进入正题写驱动。我用 misc 设备框架做例子它比传统的 register_chrdev 写起来简单得多不用手动分配主设备号适合用来验证整个工具链。文件叫hello.c#include linux/module.h #include linux/fs.h #include linux/miscdevice.h #include linux/uaccess.h #define DEV_NAME hello_imx6ull static ssize_t hello_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { const char *msg hello from IMX6ULL QEMU!\n; size_t len strlen(msg); if (*ppos len) return 0; if (copy_to_user(buf, msg, len)) return -EFAULT; *ppos len; return len; } static const struct file_operations hello_fops { .owner THIS_MODULE, .read hello_read, }; static struct miscdevice hello_dev { .minor MISC_DYNAMIC_MINOR, .name DEV_NAME, .fops hello_fops, }; static int __init hello_init(void) { int ret; ret misc_register(hello_dev); if (ret) return ret; printk(KERN_INFO hello_imx6ull: module loaded\n); return 0; } static void __exit hello_exit(void) { misc_deregister(hello_dev); printk(KERN_INFO hello_imx6ull: module unloaded\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL);配套的 Makefileobj-m : hello.o KDIR ? /path/to/linux-6.1.x all: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules clean: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- clean注意KDIR必须指向你编译内核的那个源码目录而且模块必须和内核使用同一个源码树、同一个交叉编译器否则 insmod 时会报 version magic 不匹配。5.2 交叉编译与加载编译模块make正常会生成hello.ko。这里看不出什么特别关键在后面的加载环节。把hello.ko放进 rootfs 的/root目录里sudo cp hello.ko rootfs/root/ sudo umount mnt # 如果用挂载方式做的镜像记得重新打包这里有个经验如果你用的是mkfs.ext4 -d rootfs rootfs.img这种一次性生成方式那就在生成镜像前把 hello.ko 放进 rootfs 目录。如果已经是镜像文件就挂载后复制再卸载。重新启动 QEMU进入系统后加载模块/ # insmod /root/hello.ko / # cat /proc/misc 127 hello_imx6ull / # cat /dev/hello_imx6ull hello from IMX6ULL QEMU! / # rmmod hello_imx6ull / # dmesg | tail [ xx.xxxxxx] hello_imx6ull: module loaded [ yy.yyyyyy] hello_imx6ull: module unloaded这一整套流程走通说明你已经具备在 QEMU 里做驱动开发的基本能力了。/proc/misc能看到设备注册成功/dev/hello_imx6ull能正常读写这不只是“能编译”而是驱动从注册到文件操作接口的全链路验证。5.3 GDB 远程调试内核这是 QEMU 环境比实体板子强太多的地方。实体板子上你想看内核某个函数有没有被调用得靠 printk 一点一点打QEMU 里可以直接挂 GDB 断点。先重新编译内核确认开启CONFIG_DEBUG_INFO然后保留编译生成的vmlinux文件注意不是 zImage。之后启动 QEMU 时加两个参数qemu-system-arm \ -M mcimx6ull-evk \ -m 512M \ -kernel zImage \ -dtb imx6ull-14x14-evk.dtb \ -drive filerootfs.img,formatraw,ifsd \ -append root/dev/mmcblk0 rw rootfstypeext4 consolettymxc0,115200 panic5 \ -nographic \ -s -S-s表示在 TCP 1234 端口开放 GDB 服务-S表示暂停 CPU等待调试器连接后再开始执行。打开另一个终端gdb-multiarch vmlinux在 GDB 里(gdb) target remote :1234 (gdb) break do_init_module (gdb) continue这时如果回到 QEMU 终端执行insmod /root/hello.koGDB 会立刻命中do_init_module断点——这是内核加载任何模块都会经过的入口函数。你可以从这里一步步往下看模块初始化是怎么被调用的。如果你想直接对hello_init下断点需要先等模块加载后让 GDB 读取模块符号方法是在 insmod 前先break do_init_module断下后再用(gdb) add-symbol-file /path/to/hello.ko 0x地址模块加载的基地址可以从/proc/modules里读到。这块稍微进阶但掌握了以后你调试驱动时的视野会完全不一样。VS Code 里配好 cortex-debug 或者 Native Debug 插件也能把这套 GDB 流程图形化适合不习惯命令行的人。6. 踩过的坑和常见问题解决6.1 启动卡死在 “Uncompressing Linux... done, booting the kernel”这是最常见的一种翻车现场。内核其实已经解压完成但在切换控制台时输出中断了。绝大多数原因是控制台参数不对——很多人习惯性地写consolettyS0,115200但 IMX6ULL 的串口在 Linux 里叫ttymxc0。把启动参数改成consolettymxc0,115200问题立刻消失。如果还不行检查 dtb 文件名是不是imx6ull-14x14-evk.dtb以及-M指定的机器名是否支持当前 dtb。6.2 机器名选错导致的行为差异如果你的 QEMU 版本比较老没有mcimx6ull-evk用mcimx6ul-evk启动大概率也能跑但设备树最好换成imx6ul-14x14-evk.dtb。IMX6UL 和 IMX6ULL 差异不大可设备树里外设的细节还是有区别。我的经验是查一下 QEMU 支持哪些机器名然后让 dtb 跟着机器名走别混搭。要是系统自带的 QEMU 实在太老连mcimx6ul-evk都没有那就直接去官网下载新版源码编译./configure --target-listarm-softmmu make -j$(nproc) sudo make install编译新版 QEMU 本身不复杂二十分钟以内能搞定。6.3 insmod 报 version magic 不匹配这个报错信息类似hello: version magic 6.1.0 ... should be 5.15.0 ...原因只有一个模块和内核不是同一个源码树编出来的。很多人为了方便模块用板子厂商给的 SDK 编译内核却自己下了主线源码两边版本对不上。解决方案是模块的KDIR必须指向你实际编译内核的那个目录且CROSS_COMPILE一致。模块和内核用同一套配置、同一个编译器version magic 自然就对了。6.4 rootfs 起来以后没有 shell 交互有时候内核能挂载 rootfs但输出停在一行就没有动静或者直接 kernel panic。排查顺序第一确认/dev/console节点存在这是我在 2.3 节特意强调过的点。没有这个节点init 进程无法打开标准输入输出shell 起不来。如果你初始化脚本里有mount -t devtmpfs devtmpfs /dev理论上 devtmpfs 会自动创建 console但保险起见手动创建一次没坏处。第二确认etc/inittab里的::respawn:-/bin/sh写对了。BusyBox 的 init 和传统 SysV init 语法类似但细节不同少一个冒号都会导致行为异常。第三rcS脚本有没有可执行权限。我犯过最蠢的错误就是忘了chmod x结果每次启动都静默失败查了半天才发现。6.5 镜像反复修改太麻烦如果像我一样频繁修改 rootfs每次重新生成 ext4 镜像会非常浪费时间。我的建议是做一次 NFS 根文件系统把 rootfs 放在主机目录里通过网络挂载QEMU 里改文件即时生效。虽然前面说网络属于进阶内容但 NFS rootfs 在开发阶段的价值极大值得专门花时间配一次。命令大致长这样guest 内核启动参数里把root改成 NFS 路径配合-netdev user的 hostfwd 做端口转发root/dev/nfs nfsroot10.0.2.2:/path/to/rootfs,vers3 rwQEMU 的 user 网络模式下guest 的 10.0.2.2 就是宿主机这个地址组合是固定的。如果你频繁迭代驱动模块这套方案能帮你省下大量重复打包的时间。这套 QEMU 环境我到现在还在用哪怕后来拿到了实体开发板很多基础验证工作我还是会先在 QEMU 里跑一遍。驱动框架、设备树匹配、模块加载这些逻辑性问题在模拟环境里排查效率远高于实体板子毕竟重启快、可断点、不怕搞坏。等你把 QEMU 这套链路跑熟了再回到真实板子你会发现自己省下来最多的就是反复烧写和串口日志翻找的时间。如果你也打算搭一套按照上面的顺序一步步来中间卡住的地方多半都能在第六章里找到答案。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门