最近在折腾一个基于i.MX8M Mini和i.MX8M Nano的模块化项目,把两块核心板都跑起来之后,最大的感受就是:这两颗芯片在Linux生态下的成熟度,确实对得起“Linux-Friendly”这个标签。尤其是当你从STM32MP1或者老平台切换过来,会发现设备树、BSP、Yocto这些链路基本是开箱即用,省掉了大量跟厂商抠文档、对着datasheet硬憋驱动的功夫。
嵌入式Linux项目里,很多人纠结的一个问题是:到底选核心板还是自己画板?我的建议很简单——如果产品迭代快、出货量还没到几十K的量级,直接用第三方SOM(System on Module),也就是核心板。i.MX8M Mini和Nano这个档位,市面上可选的核心板非常多,引脚定义基本兼容,底板自己画,从拿到样品到系统跑起来,快的话一个下午就能完成。这篇文章就把整个过程中的思路、配置方法和踩坑记录整理出来,方便后来人少走弯路。
1. 为什么i.MX8M Mini/Nano是“Linux友好”的模块化平台
1.1 从SoC到SOM:模块化设计到底解决什么问题
先聊清楚一个概念:SOM(System on Module)就是把CPU、DDR、eMMC、PMIC、网络PHY这些“难搞”的部分做成一个邮票孔或板对板连接器的小板,用户只需要设计一张底板,把电源、接口、外设引出来就行。这样做的好处非常直接——DDR布线、阻抗匹配、电源时序这些高风险工作全部由模块厂家搞定,你拿到的核心板,理论上只要供电正常,U-Boot就能起来。
i.MX8M系列之所以适合这种玩法,核心在于NXP把BSP做得极其规范。从Yocto的meta层到U-Boot的板级配置,再到内核里arch/arm64/boot/dts/freescale/下的设备树,全链路都是公开的。你不需要像某些小众IC那样到处求一份NDA才能拿到的SDK,直接去NXP官网下载Linux BSP或者拉L5.x版本的Yocto分支,编译出来的系统就能在官方EVK上跑。这种“官网即源码”的做派,才是Linux友好最实在的体现。
模块化还有一个附加价值:便于做产品系列化。比如你的产品需要高配和低配两个版本,高配用i.MX8M Mini带GPU和VPU,低配用i.MX8M Nano不带PCIe但带NPU,底板尽量复用,核心板换一下,软件上只需要换设备树和内核配置。这里的核心关键词就是“设备树”——同一套内核,通过不同的dts文件适配不同硬件,这正是Linux模块化设计的精髓。
1.2 Mini与Nano核心参数对比
很多人在选型时拿不准这两颗芯片的差别。我整理了一份关键参数对照表,方便你快速判断该选哪个:
| 参数项 | i.MX8M Mini | i.MX8M Nano |
|---|---|---|
| CPU架构 | 4×Cortex-A53 + 1×Cortex-M4 | 4×Cortex-A53 + 1×Cortex-M7 |
| A53主频 | 最高1.8GHz | 最高1.5GHz |
| GPU | GC NanoUltra(支持OpenGL ES 2.0) | GC7000UL(部分型号) |
| VPU | 1080p60 H.265/H.264/VP8解码,1080p30 H.264编码 | 无(部分型号) |
| NPU | 无 | 约0.5 TOPS(INT8) |
| PCIe | Gen2 x1 | 无 |
| 网络 | 1×GbE(支持EEE) | 1×GbE |
| CAN | 2×FlexCAN | 1×FlexCAN + CAN-FD |
| 显示 | MIPI-DSI | MIPI-DSI |
| 内存 | LPDDR4/DDR4/DDR3L | LPDDR4/DDR4/DDR3L |
从这个表能看得很清楚:Mini主打多媒体和通用计算,适合HMI、音视频网关;Nano的优势是低功耗和轻量级AI推理,适合带NPU的工业视觉、边缘计算盒子。如果只是做简单的协议转换或者数据采集,两颗芯片性能都过剩,关键看外设接口和功耗预算。
我实测过两者运行相同版本主线内核(linux 5.15)的差异:在相同负载下,Nano的整板功耗比Mini低大约20%左右,无风扇散热片就压得住。如果你的产品对成本敏感、且不需要PCIe,Nano是更经济的选择。但注意Nano的部分型号没有VPU,如果你要硬解视频,就得选带VPU的版本,或者干脆用Mini。
2. 拿到核心板后的一顿操作:快速启动Linux
2.1 镜像下载、烧录与启动介质选择
拿到一块新的核心板,第一件事不是看原理图,而是想办法把系统跑起来。我推荐的做法是先下载官方或者模块厂商提供的预编译镜像,用工具烧到SD卡里启动,确认硬件基本健康之后,再自己编译内核和Yocto。
这里有两个常见路径:
- 官方方案:去NXP官网下载i.MX8M Mini EVK或Nano EVK的“LF6.x.y”版本BSP镜像,解压后得到
imx-boot、Image、*.dtb、rootfs.ext4等文件。 - 模块厂商方案:大多数核心板厂商会提供自己适配好的镜像和文档,优先使用,因为他们的U-Boot默认参数、DDR初始化、以太网PHY地址可能已经改过,直接用官方EVK镜像大概率会卡在启动早期。
烧录SD卡在Linux主机上非常直接:
lsblk sudo dd if=imx-boot-sd.bin of=/dev/sdX bs=1k seek=1 conv=fsync sudo dd if=Image of=/dev/sdX bs=1M seek=32768 conv=fsync # 创建rootfs分区后用tar或者dd把rootfs解压进去注意几点:第一,imx-boot是写到SD卡偏移1K的位置,不是0扇区;第二,Image通常写在偏移32MB位置,这是U-Boot环境变量里默认的loadaddr和mmcpart约定;第三,如果你用的是模块厂商的底板,SD卡的CD(Card Detect)引脚可能有差异,导致内核识别不到SD卡,这时候优先检查设备树里usdhc1或usdhc2的cd-gpios。
启动介质方面,除了SD卡,还可以从eMMC、U盘甚至网络启动。用U盘启动有个技巧:把imx-boot通过U-Boot烧到eMMC,然后让U-Boot从eMMC读取内核,rootfs放在U盘里,这样调试时只需要往U盘里更新rootfs,不用反复拔插SD卡。
2.2 U-Boot引导流程与常用调试命令
U-Boot是i.MX8M平台启动的第一个软件阶段,负责初始化DDR、时钟、加载ATF、OP-TEE和内核。这个平台用了ARM Trusted Firmware(ATF),启动链路是:
ROM → SPL(或U-Boot) → ATF(BL31) → U-Boot proper → Linux一旦进入U-Boot命令行,常用调试命令要记牢:
printenv # 打印所有环境变量 setenv bootcmd '...' # 修改启动命令 saveenv # 保存环境变量 mmc list / mmc dev 0 # 查看MMC设备 fatls mmc 0:1 # 查看FAT分区文件 tftpboot 0x40480000 Image # 通过TFTP加载内核到内存 booti 0x40480000 - 0x43000000 # 启动64位内核,后面是dtb地址我最常踩的坑是内核启动地址和设备树加载地址。i.MX8M的DDR起始地址是0x40000000,内核通常加载到0x40480000,设备树加载到0x43000000或0x48000000,不同BSP版本会有差异。如果booti执行后立刻复位或者卡住,八成是加载地址和U-Boot里定义的loadaddr不一致。
另一个实用技巧是启用FIT镜像或者把Image+dtb打包成boot.img,减少文件数。但调试阶段我更建议分开加载,因为可以直接用tftpboot单独更新dtb,几秒钟就能看到效果,不用重新烧写整个boot分区。
2.3 根文件系统挂载与网络引导
开发阶段最爽的rootfs方案是NFS挂载。在Ubuntu主机上配好NFS服务,把rootfs目录导出:
sudo apt install nfs-kernel-server # 编辑/etc/exports /home/user/imx8m-rootfs *(rw,sync,no_root_squash,no_subtree_check) sudo exportfs -ra然后在U-Boot里设置:
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.100:/home/user/imx8m-rootfs,v3 ip=dhcp'这样内核起来之后直接从主机加载整个文件系统,你在主机上修改任何应用代码,板子上重启就能生效,省去了反复烧写eMMC或者SD卡的时间。实测下来,千兆网口下NFS启动的体验和本地eMMC启动几乎无差别,唯一的注意点是主机防火墙要放行NFS端口,以及板子的IP需要和主机在同一网段。
如果不想折腾NFS,还有一个折中方案:把rootfs做成rootfs.ext4镜像,用dd直接写入SD卡或者eMMC分区,然后在bootargs里写root=/dev/mmcblk1p2 rootwait。这里的rootwait一定要加,否则内核可能在eMMC设备还没注册完成时就去挂载根,导致“VFS: Unable to mount root fs”的经典报错。
3. 内核与设备树:让外设按你的需求工作
3.1 设备树的核心结构与修改思路
Linux设备树(Device Tree)就是描述硬件配置的文件,内核通过它知道系统里有哪些外设、它们在哪个地址、中断号是多少、引脚怎么复用。对于i.MX8M平台,设备树的目录一般在arch/arm64/boot/dts/freescale/下,比如imx8mm-evk.dts、imx8mn-evk.dts。
设备树的基本结构是节点(node)和属性(property),比如一个串口节点:
&uart3 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_uart3>; status = "okay"; };&uart3表示引用SoC dtsi里定义好的uart3节点,status = "okay"把它使能。pinctrl-0指向一个pinmux配置节点,这个节点通常在imx8mm-evk.dts的iomuxc部分定义:
pinctrl_uart3: uart3grp { fsl,pins = < MX8MM_IOMUXC_UART3_TXD_UART3_TX 0x140 MX8MM_IOMUXC_UART3_RXD_UART3_RX 0x140 >; };这里的0x140是引脚配置寄存器值,控制上下拉、驱动强度、施密特触发等电气属性。刚开始改设备树时,最容易出错的就是复用寄存器值,建议直接参考NXP官方dts或者模块厂商提供的底板dts,别自己凭直觉设置。
3.2 实战:在模块上点亮一颗LED并调通串口
我来分享一个完整的实战案例:在新底板上点亮一颗GPIO LED,并调通一个调试串口。整个过程只要改设备树,不需要写任何驱动代码。
先看LED硬件:GPIO连接在SoC的GPIO1_IO12上,高电平点亮。在设备树里新建一个gpio-leds节点:
leds { compatible = "gpio-leds"; status = "okay"; led0 { label = "user-led"; gpios = <&gpio1 12 GPIO_ACTIVE_HIGH>; linux,default-trigger = "heartbeat"; }; };然后在内核里确认CONFIG_LEDS_GPIO是打开的。重新编译dtb并加载后,在板子上执行:
ls /sys/class/leds/ echo 1 > /sys/class/leds/user-led/brightness看到灯亮了,说明GPIO复用、驱动、设备树这一整条链路都是通的。接下来调串口,假设调试串口是UART3,对应ttymxc2。开机后执行dmesg | grep ttymxc确认注册情况,如果没有输出,大概率是pinctrl配置不对,或者bootargs里的console=参数指定了错误的串口设备。
修改设备树使能UART3后,要确保/etc/inittab或者systemd的serial-getty@ttymxc2.service已经配置好,否则只能看到内核日志,无法登录。这个细节很多人会漏,导致以为串口没通,其实是getty没启动。
3.3 驱动开发接口与编译部署链路
如果外设不在内核自带驱动范围内,就需要自己写驱动。Linux驱动开发的常规接口是platform_driver,配合设备树进行匹配。一个最简单的字符设备驱动大概长这样:
#include <linux/module.h> #include <linux/platform_device.h> static int my_probe(struct platform_device *pdev) { pr_info("my_device probed\n"); return 0; } static const struct of_device_id my_of_match[] = { { .compatible = "mycompany,mydevice" }, { /* sentinel */ } }; static struct platform_driver my_driver = { .probe = my_probe, .driver = { .name = "my_device", .of_match_table = my_of_match, }, }; module_platform_driver(my_driver); MODULE_LICENSE("GPL");在设备树里加上:
mydevice { compatible = "mycompany,mydevice"; reg = <0x0 0x20000000 0x0 0x1000>; };驱动和硬件节点通过compatible字符串匹配。这就是设备树“解耦”的威力:同一份驱动,只需要改dts,就能适配不同的寄存器地址和中断号。
驱动编译可以放在内核源码树内,也可以作为外部模块编译:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- M=$(pwd)/drivers/misc modules把生成的内核模块拷贝到板子上后:
insmod my_device.ko dmesg | tail如果probe没有执行,用ls /sys/bus/platform/devices/查看设备是否注册成功,再用cat /proc/device-tree/mydevice/compatible确认设备树节点是否存在。整个调试链路清晰,问题基本都能定位到具体环节。
4. 遇到的各种坑与排查方法实录
4.1 启动类故障:无法挂载根文件系统/内核panic
这类问题我在调试过程中遇到最多,这里写一个快速排查顺序,建议收藏:
- 先看U-Boot有没有起来:如果串口完全无输出,检查供电、启动拨码、boot设备选择。i.MX8M的启动拨码一般在核心板上,默认应该是SD卡启动,如果拨错会卡在ROM阶段。
- 看内核有没有起来:如果U-Boot正常但内核没启动,执行
booti后没有任何内核日志,优先怀疑dtb地址错误或者内核镜像损坏。用md5sum比对一下加载到内存的Image和原始文件是否一致。 - 看根文件系统挂载:报错
VFS: Unable to mount root fs时,检查bootargs里的root=参数是否正确。SD卡和eMMC的设备节点在不同内核版本里可能变化,有的内核是mmcblk1,有的是mmcblk2,建议在U-Boot里先用mmc list和part list确认设备号。 - 内核panic后自动循环重启:如果系统一直重启,可以在bootargs里加
panic=-1,让内核panic后不重启,方便抓取完整日志。
实测中最气人的一次是:所有配置看起来都对,但内核就是不挂载rootfs。后来发现是U-Boot环境变量里有一段旧的bootcmd,每次启动都走了旧的mmc read命令,加载的内核地址和文件偏移都不对。解决办法很简单,env default -a恢复默认环境变量,重新设置,再saveenv。
4.2 外设类故障:网络、I2C、GPIO调试经验
网络不通是排查外设问题里最典型的。i.MX8M系列用的以太网控制器是FEC(Fast Ethernet Controller)或EQOS(Ethernet QoS),设备树里网络节点常见的有&fec1和&eqos。
遇到网卡启动失败,先看dmesg | grep eth:
- 如果提示
phy not found,多半是PHY地址不对。PHY的地址在设备树里通过phy-handle指定,比如ðphy0,而ethphy0节点的reg = <0>对应PHY的MDIO地址。不同模块厂商用的PHY芯片地址不同,常见的有0、1、4、7。 - 如果提示
Link is down,检查网线、交换机口,或者用ethtool eth0查看链路状态和速率协商情况。 - 如果只有
Link is Up - 10Mbps/Half Duplex,说明PHY的时钟或复位配置有问题,重点检查设备树里reset-gpios和phy-mode,i.MX8M的FEC通常用rmii,EQOS用rgmii。
I2C问题一般表现为i2c_get_adapter失败或者设备不响应。先确认设备树里I2C节点的status = "okay",然后用i2cdetect -y 0扫描总线,看能不能找到设备地址。如果扫描不到,检查引脚复用和上拉电阻,I2C总线必须接上拉,很多定制底板在这一点上忘接电阻。
GPIO调试比较直接,就是/sys/class/gpio/或libgpiod的gpioset/gpioget。但要注意i.MX8M的GPIO bank编号和引脚号换算:例如GPIO1_IO12对应gpiochip0的pin 12,映射关系可以通过cat /sys/kernel/debug/gpio查看。如果GPIO操作报Device or resource busy,多半是引脚被复用到其他功能了,查一下/sys/kernel/debug/pinctrl/下的pinmux状态。
4.3 性能与功耗优化记录
设备跑通只是第一步,真正量产前还要做性能和功耗调优。这里分享几个实测结果。
首先是CPU调频。i.MX8M Mini的A53默认可能有多个OPP(Operating Performance Point)。通过cpufreq的schedutil或ondemandgovernor,可以在性能和功耗间取得平衡。我在项目里用schedutil,配合内核的CPUIdle,轻负载时A53可以进入WFI状态,整板功耗下降明显。
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor echo schedutil > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor其次是DDR频率。i.MX8M的DDR频率由U-Boot里的FSP(Firmware Service Procedure)表控制,默认可能有多个频率档位。如果你的应用不追求极致内存带宽,可以在U-Boot里设置fdt_high和initrd_high相关参数,或者直接在内核cmdline里通过mem=限制不必要的内存映射,都能略微降低功耗。
GPU和NPU的调优则需要针对具体负载。Mini的GPU支持OpenGL ES 2.0,跑Qt的QML界面时,如果发现渲染掉帧,先确认/dev/dri/card0是否存在,以及是否加载了etnaviv驱动。Nano的NPU如果要跑TFLite,官方提供了NXP的eIQ推理框架,使用VX delegate进行加速,实测一个MobileNetV2分类任务,推理时间能压到几十毫秒级别,这个数据对边缘AI应用很有参考价值。
5. 最后说点个人体会:这套方案适合谁、怎么选
折腾完这一整圈,我最大的感受是:“Linux友好”不是嘴上说说,而是整个工具链、文档、社区生态的综合体验。如果你手里正拿着i.MX8M Mini或Nano的核心板,照着我上面这套流程走一遍,大概率能在半天内让系统跑起来,接下来就是在设备树和驱动层面做定制。这个流程本身,放到其他平台也是一样的思路,但i.MX8M的BSP规范和NXP对Yocto/主线内核的支持力度,确实让这条路顺畅不少。
最后分享一个小技巧:在所有调试开始前,先给U-Boot设置一个“快速启动模板”——TFTP加载内核 + NFS挂载rootfs,把常用的bootcmd和bootargs存到环境变量里。这样一来,之后每次改设备树、改驱动,从编译到运行只需要一两分钟,整个开发节奏会快很多。等你把所有外设都调通了,再回头做eMMC烧录、开机自启动、看门狗这些量产优化也不迟。这套“先快速跑通、再逐步加固”的思路,适用于绝大多数嵌入式Linux项目。