☰
QEMU模拟IMX6ULL:嵌入式Linux驱动开发实战环境搭建
2026/9/29 20:37:24 网站建设 项目流程

做了大半年 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、交叉编译器与 rootfs

2.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-evk,IMX6UL 和 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 ARCH=arm export CROSS_COMPILE=arm-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 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)

编译完以后,把生成的 BusyBox 和命令链接安装到一个临时目录里,这个目录就是我们 rootfs 的雏形:

mkdir -p ../rootfs make ARCH=arm CROSS_COMPILE=arm-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/init

etc/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/rcS

2.4 制作 ext4 根文件系统镜像

QEMU 里通常把 rootfs 做成一个 ext4 镜像,然后模拟成 SD 卡接进系统。大小给 256MB 到 512MB,一般 256MB 足够。

dd if=/dev/zero of=rootfs.img bs=1M count=256 mkfs.ext4 rootfs.img

如果你用的是新版本的 e2fsprogs(1.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.x

3.2 defconfig 的取舍

IMX6ULL 属于 i.MX 系列里 armv7 架构的 SoC,主线内核里对应的默认配置是imx_v6_v7_defconfig。这个配置会把 i.MX 系列大部分 SoC 的支持都编进去,好处是你不用纠结具体选了哪些选项,坏处是编译时间稍微长一点,但完全在可接受范围内。

配置命令:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig

接下来有一个容易被忽略的环节——检查关键配置项。执行:

grep -E "CONFIG_SOC_IMX6ULL|CONFIG_GPIO_IMX|CONFIG_SERIAL_IMX" .config

这三个配置分别对应 IMX6ULL SoC 支持、GPIO 驱动、串口驱动,任何一个缺失都会让后续工作卡住。正常 defconfig 下这三个都是开启的。

如果你想用 GDB 调试内核,记得额外开启调试信息:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

在 Kernel hacking → Compile-time checks and compiler options 里勾选Compile the kernel with debug info,对应配置项是CONFIG_DEBUG_INFO。有了这个,后面才能对内核下断点调试。

配置完成后开始编译内核镜像:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc) zImage

编译产物在arch/arm/boot/zImage。这一步顺利的话,说明基础工具链没问题。

3.3 编译设备树

设备树是 IMX6ULL 驱动开发里绕不开的一环。QEMU 启动时会把 dtb 加载到内存,内核根据它来枚举平台设备、匹配驱动,所以 dtb 必须和 SoC 型号一致。

在内核源码目录下执行:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

完成以后,找imx6ull-14x14-evk.dtb:

find arch/arm/boot/dts -name "imx6ull*.dtb"

注意路径在不同内核版本里有差异。6.1 及更早版本通常在arch/arm/boot/dts/imx6ull-14x14-evk.dtb;6.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/zImage
  • arch/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 file=rootfs.img,format=raw,if=sd \ -append "root=/dev/mmcblk0 rw rootfstype=ext4 console=ttymxc0,115200 panic=5" \ -nographic

每个参数都解释一下,知道为什么这么写,遇到问题才能自己排查:

  • -M mcimx6ull-evk:指定模拟的板卡模型。如果你的 QEMU 版本没有这个机器名,就用mcimx6ul-evk。
  • -m 512M:模拟内存大小,要和 dtb 里声明的大小匹配。
  • -kernel zImage:直接把内核镜像加载到内存启动,不需要 U-Boot。这条路径最简单,QEMU 会自行处理 zImage 的加载地址。
  • -dtb imx6ull-14x14-evk.dtb:把设备树二进制传给内核。
  • -drive file=rootfs.img,format=raw,if=sd:把 rootfs 镜像模拟成 SD 卡,系统里会出现 mmcblk0 设备。
  • -append:内核启动参数。root=/dev/mmcblk0指定根文件系统所在设备;console=ttymxc0,115200指定内核串口控制台,IMX 系列的第一路 UART 在 Linux 里的设备名是 ttymxc0;panic=5指定内核 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 rootfstype=ext4 console=ttymxc0,115200 panic=5 / # 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 file=rootfs.img,format=raw,if=sd \ -append "root=/dev/mmcblk0 rw rootfstype=ext4 console=ttymxc0,115200 panic=5" \ -netdev user,id=eth0 \ -device imx_fec,netdev=eth0 \ -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");

配套的 Makefile:

obj-m := hello.o KDIR ?= /path/to/linux-6.1.x all: $(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules clean: $(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-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 file=rootfs.img,format=raw,if=sd \ -append "root=/dev/mmcblk0 rw rootfstype=ext4 console=ttymxc0,115200 panic=5" \ -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.ko,GDB 会立刻命中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”

这是最常见的一种翻车现场。内核其实已经解压完成,但在切换控制台时输出中断了。绝大多数原因是控制台参数不对——很多人习惯性地写console=ttyS0,115200,但 IMX6ULL 的串口在 Linux 里叫ttymxc0。把启动参数改成:

console=ttymxc0,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-list=arm-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 nfsroot=10.0.2.2:/path/to/rootfs,vers=3 rw

QEMU 的 user 网络模式下,guest 的 10.0.2.2 就是宿主机,这个地址组合是固定的。如果你频繁迭代驱动模块,这套方案能帮你省下大量重复打包的时间。


这套 QEMU 环境我到现在还在用,哪怕后来拿到了实体开发板,很多基础验证工作我还是会先在 QEMU 里跑一遍。驱动框架、设备树匹配、模块加载这些逻辑性问题,在模拟环境里排查效率远高于实体板子,毕竟重启快、可断点、不怕搞坏。等你把 QEMU 这套链路跑熟了,再回到真实板子,你会发现自己省下来最多的就是反复烧写和串口日志翻找的时间。如果你也打算搭一套,按照上面的顺序一步步来,中间卡住的地方,多半都能在第六章里找到答案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询