1. 项目概述
在基于NXP i.MX 8M系列处理器的嵌入式Linux产品开发中,无论是快速原型验证还是最终产品部署,存储介质(通常是eMMC或SD卡)的分区与镜像烧录都是绕不开的关键一步。这不仅仅是把几个文件拷贝到存储设备那么简单,它直接关系到系统能否正常启动、运行,以及后续的维护与升级。很多刚接触这个平台的工程师,常常会卡在这一步:从NXP官网下载的预构建镜像(Pre-built Image)烧进去就能用,但一旦需要调整分区大小、更换内核或者添加自定义应用,就不知从何下手了。而通过Yocto项目自己构建镜像,虽然灵活性大增,但其中涉及的分区配置又显得颇为复杂。
我自己在多个i.MX 8M项目上踩过不少坑,从最初对着官方文档照猫画虎,到后来能根据产品需求灵活定制分区方案,这个过程积累了不少实战经验。今天,我就以i.MX 8M平台为例,抛开那些晦涩的理论,直接上干货,把两种主流的分区与部署方法——使用预构建镜像和基于Yocto构建——的完整流程、核心原理以及避坑要点,系统地梳理一遍。无论你是想快速上手评估板,还是为量产产品定制固件,这篇文章都能给你提供一份可直接“抄作业”的指南。
2. 核心概念与部署流程总览
在动手操作之前,我们必须先理清几个核心概念和整体的工作流。嵌入式Linux系统的启动,依赖于存储介质上特定位置的特定数据,这个布局是硬件(SoC的ROM代码)和软件(U-Boot)约定好的。
2.1 i.MX 8M启动流程与存储布局
i.MX 8M系列处理器上电后,内部的ROM代码会首先运行。它会根据启动模式引脚(Boot Mode)的配置,去指定的存储设备(如eMMC、SD卡、QSPI NOR)的固定位置寻找并加载第一阶段的引导程序。对于我们常用的eMMC/SD卡启动,这个固定位置通常是从存储设备起始偏移33KB(即0x8400字节)或1KB开始,具体取决于芯片型号和启动类型(普通启动或快速启动)。这个被加载的程序,就是我们常说的imx-boot(包含了SPL和U-Boot)。
imx-boot运行起来后,它的任务就是初始化更丰富的硬件环境,然后去加载Linux内核(Image文件)和设备树(.dtb文件)。内核和设备树放在哪里呢?它们通常位于一个FAT32格式的“Boot分区”里。U-Boot知道去这个分区的根目录下寻找特定名称的文件(比如Image和imx8mq-evk.dtb)。内核启动后,会挂载“根文件系统分区”(Rootfs),这是一个EXT4(或EXT3)格式的分区,里面包含了完整的Linux系统目录和我们的应用程序。
所以,一个典型的i.MX 8M存储布局包含三个关键部分:
- Bootloader区:非分区形式,是写在存储设备绝对起始偏移地址(如33KB)的一段裸数据。
- Boot分区:一个FAT32格式的、可启动的主分区,存放内核镜像和设备树。
- Rootfs分区:一个EXT4格式的主分区,存放根文件系统。
注意:Boot分区和Rootfs分区的位置和大小是可以调整的,但有一个铁律:它们绝对不能覆盖Bootloader的存放区域。否则系统将无法启动。同时,分区的大小也不能小于你要放入的镜像文件本身。
2.2 两种部署路径的选择
根据镜像来源的不同,分区和部署的操作方法也分为两条主要路径:
路径一:使用预构建镜像这是最快捷的上手方式。NXP会为每一版BSP(Board Support Package)提供针对评估板编译好的完整镜像包。你下载后,可以直接使用UUU工具,配合一个脚本,将整个系统(Bootloader、Boot分区、Rootfs分区)一键烧录到板子的存储介质上。这个脚本已经内置了默认的分区方案。这种方法的优点是开箱即用,适合功能验证和早期开发。缺点是分区布局固定,难以定制;镜像内容也无法灵活增减。
路径二:使用Yocto构建的镜像这是产品开发的必经之路。通过Yocto项目,你可以从源码开始,完全自定义你的Linux系统:选择需要的软件包、配置内核、调整根文件系统大小,当然也包括定义分区的布局。Yocto最终会生成一个包含完整分区信息的.wic镜像文件,或者分离的各个分区镜像。部署时,你可以选择直接烧录.wic文件,也可以像处理预构建镜像那样,用脚本分别烧录各组件。这种方法灵活性极高,但需要搭建Yocto构建环境,学习曲线较陡。
无论选择哪条路,UUU(Universal Update Utility)都是我们与板子通信、完成烧录的核心工具。它是一个跨平台的命令行工具,通过USB OTG接口与处于下载模式(Serial Downloader Mode)的i.MX板卡连接,可以执行U-Boot命令或Linux命令,从而实现复杂的部署操作。
3. 基于预构建镜像的分区与部署实战
当你拿到一块i.MX 8M EVK板,想最快速度看到系统跑起来,用预构建镜像是最佳选择。我们以i.MX 8MQ EVK和Linux BSP 5.15.32_2.0.0为例,详细走一遍流程。
3.1 准备工作与环境搭建
首先,你需要从NXP官网下载对应版本的Linux BSP发布包。解压后,在samples文件夹里,你能找到一个名为example_kernel_emmc的脚本,这是我们工作的起点。同时,确保你的开发主机(Linux或Windows)上已经安装了UUU工具(版本1.4.193或更高)。
将板子设置为串行下载模式(Serial Downloader Mode)。对于i.MX 8MQ EVK,这通常涉及调整板上的拨码开关(SW1101),具体请参考你的板子手册。用USB线连接板子的USB OTG口(通常是J1301)到主机。
3.2 部署脚本深度解析与定制
官方提供的脚本是一个强大的模板,但直接使用前,理解每一行命令的作用至关重要。下面我结合一个修改后的、更清晰的脚本来逐段解析:
uuu_version 1.2.39 # 阶段1:将Bootloader加载到RAM并运行 SDP: boot -f imx-boot-imx8mqevk-sd.bin-flash_evk # 注意:`-sd.bin-flash_evk`后缀的镜像用于从SD/eMMC启动,并包含刷写指令。 # 阶段2:通过Fastboot协议加载内核、设备树和临时根文件系统到板子内存 FB: ucmd setenv fastboot_buffer ${loadaddr} FB: download -f Image-imx8mqevk.bin FB: ucmd setenv fastboot_buffer ${fdt_addr} FB: download -f imx8mq-evk.dtb FB: ucmd setenv fastboot_buffer ${initrd_addr} FB: download -f fsl-image-mfgtool-initramfs-imx_mfgtools.cpio.zst.u-boot FB: acmd ${kboot} ${loadaddr} ${initrd_addr} ${fdt_addr} # 执行到这里,一个最小的Linux系统已经在板子的RAM中运行了,为我们后续操作提供了环境。 # 阶段3:在板载Linux系统中对eMMC进行分区 # 等待eMMC设备节点出现 FBK: ucmd while [ ! -e /dev/mmcblk*boot0 ]; do sleep 1; echo \"wait for /dev/mmcblk*boot* appear\"; done; # 识别eMMC设备号,例如可能是mmcblk2 FBK: ucmd dev=`ls /dev/mmcblk*boot*`; dev=($dev); dev=${dev[0]}; dev=${dev#/dev/mmcblk}; dev=${dev%boot*}; echo $dev > /tmp/mmcdev; # 清除可能存在的旧MBR(分区表) FBK: ucmd mmc=`cat /tmp/mmcdev`; dd if=/dev/zero of=/dev/mmcblk${mmc} bs=512 count=1 # **核心步骤:创建新分区表** FBK: ucmd mmc=`cat /tmp/mmcdev`; PARTSTR=$'10M,500M,0c\\n600M,,83\\n'; echo \"$PARTSTR\" | sfdisk --force /dev/mmcblk${mmc}这里就是定义分区布局的关键。sfdisk工具通过标准输入接收分区指令。PARTSTR变量定义了两个分区:
10M,500M,0c:第一个分区从10MB偏移开始,大小为500MB,类型为0c(FAT32 LBA),并且是启动分区。600M,,83:第二个分区从600MB偏移开始,大小未指定(,留空),表示占用剩余所有空间,类型为83(Linux)。
重要提示:第一个分区的起始地址(10M)必须大于Bootloader的结束地址。对于i.MX 8MQ,Bootloader(
imx-boot)通常写在偏移33KB(约0.03MB)开始的位置,大小约几百KB,所以10MB是安全的。务必根据你的芯片型号查阅《参考手册》确认。
# 阶段4:写入Bootloader到eMMC的指定位置 # 解锁eMMC boot0分区属性,允许写入 FBK: ucmd mmc=`cat /tmp/mmcdev`; echo 0 > /sys/block/mmcblk${mmc}boot0/force_ro # 将Bootloader镜像写入boot0区域,偏移33KB(即seek=33,块大小1K) FBK: ucp imx-boot-imx8mqevk-sd.bin-flash_evk t:/tmp FBK: ucmd mmc=`cat /tmp/mmcdev`; dd if=/tmp/imx-boot-imx8mqevk-sd.bin-flash_evk of=/dev/mmc${mmc}boot0 bs=1K seek=33 # 重新锁定,防止误操作 FBK: ucmd mmc=`cat /tmp/mmcdev`; echo 1 > /sys/block/mmcblk${mmc}boot0/force_ro # 阶段5:格式化Boot分区并放入内核、设备树 FBK: ucmd mmc=`cat /tmp/mmcdev`; while [ ! -e /dev/mmcblk${mmc}p1 ]; do sleep 1; done FBK: ucmd mmc=`cat /tmp/mmcdev`; mkfs.vfat /dev/mmcblk${mmc}p1 FBK: ucmd mmc=`cat /tmp/mmcdev`; mkdir -p /mnt/fat FBK: ucmd mmc=`cat /tmp/mmcdev`; mount -t vfat /dev/mmcblk${mmc}p1 /mnt/fat FBK: ucp Image-imx8mqevk.bin t:/mnt/fat # **关键重命名**:U-Boot默认寻找名为`Image`的内核文件 FBK: ucmd mmc=`cat /tmp/mmcdev`; mv /mnt/fat/Image-imx8mqevk.bin /mnt/fat/Image FBK: ucp imx8mq-evk.dtb t:/mnt/fat FBK: ucmd umount /mnt/fat # 阶段6:格式化Rootfs分区并解压根文件系统 FBK: ucmd mmc=`cat /tmp/mmcdev`; mkfs.ext3 -F -E nodiscard /dev/mmcblk${mmc}p2 FBK: ucmd mkdir -p /mnt/ext3 FBK: ucmd mmc=`cat /tmp/mmcdev`; mount /dev/mmcblk${mmc}p2 /mnt/ext3 FBK: acmd export EXTRACT_UNSAFE_SYMLINKS=1; tar -jx -C /mnt/ext3 FBK: ucp imx-image-full-imx8mqevk.tar.bz2 t:- FBK: Sync FBK: ucmd umount /mnt/ext3 FBK: DONE3.3 实操心得与避坑指南
- 镜像文件命名一致性:这是最常见的启动失败原因。脚本中从主机下载到板子Boot分区的内核文件是
Image-imx8mqevk.bin,但随后被重命名为Image。这是因为U-Boot的环境变量(如bootcmd)里默认指定了从mmc设备加载名为Image的文件。如果你的内核文件名不同,要么在脚本里正确重命名,要么去修改U-Boot的环境变量。设备树文件(.dtb)同理。 - 分区起始地址计算:在修改
PARTSTR时,一定要留足安全空间。Bootloader的大小会随版本和配置变化。一个稳妥的做法是,第一个分区(Boot分区)的起始地址至少设为Bootloader起始地址 + Bootloader最大预估大小 + 1MB的余量。例如,Bootloader在33KB,大小约1MB,那么从10MB开始是安全的。 - UUU脚本的版本与平台适配:脚本开头的
uuu_version指明了脚本语法版本。不同版本的UUU工具可能支持的特性不同。如果你的UUU工具版本较新,而脚本较旧,可能某些命令不兼容。建议使用BSP包内自带的UUU工具版本,或查阅UUU文档进行适配。 - 存储设备号识别:脚本中通过
/dev/mmcblk*boot0来识别eMMC。如果你的板子上有多个eMMC或SD卡槽,这个通配符可能会匹配到错误的设备。在复杂硬件环境下,更可靠的方法是在Linux启动后,通过lsblk或dmesg | grep mmc命令手动确认设备号(如mmcblk2),然后硬编码到脚本中替换mmc=cat /tmp/mmcdev``部分。
4. 基于Yocto构建镜像的分区与部署
当你需要定制系统,比如增加软件包、修改内核配置、或者调整分区大小时,就必须走Yocto构建这条路。Yocto不仅生成镜像内容,还能定义镜像的布局。
4.1 在镜像构建阶段定义分区
这是最“Yocto”的方式,通过修改配方(Recipe)和配置文件,让构建系统直接产出符合你分区要求的镜像。
调整根文件系统(Rootfs)分区大小根文件系统的大小主要由三个变量控制,你可以在conf/local.conf文件中进行设置:
# 定义根文件系统镜像的初始大小(单位:KB) IMAGE_ROOTFS_SIZE = \"524288\" # 例如,设置为512MB # 定义系统开销的乘数因子(默认约为1.3倍) IMAGE_OVERHEAD_FACTOR = \"1.3\" # 定义额外的空闲空间(单位:KB) IMAGE_ROOTFS_EXTRA_SPACE = \"65536\" # 例如,额外增加64MB最终Rootfs镜像的大小大致为:(IMAGE_ROOTFS_SIZE * IMAGE_OVERHEAD_FACTOR) + IMAGE_ROOTFS_EXTRA_SPACE。设置好后,重新构建镜像即可。
定义整体分区布局(Wic Image)Yocto可以使用Wic(Windows Imaging Creator)工具创建包含分区表的完整磁盘镜像(.wic文件)。分区布局由.wks(kickstart)文件定义。对于i.MX平台,默认的.wks文件通常在meta-freescale层中定义。
例如,查看或自定义imx-imx-boot-bootpart.wks.in文件(或其继承者),你会看到类似内容:
part u-boot --source rawcopy --sourceparams=\"file=imx-boot\" --ondisk mmcblk --no-table --align ${IMX_BOOT_SEEK} part /boot --source bootimg-partition --ondisk mmcblk --fstype=vfat --label boot --active --align 8192 --size 64 part / --source rootfs --ondisk mmcblk --fstype=ext4 --label root --align 8192 bootloader --ptable msdospart u-boot: 指定imx-boot写入的位置,--align ${IMX_BOOT_SEEK}确保了它与硬件要求的偏移对齐。part /boot: 定义Boot分区。--size 64指定分区大小为64MB。你可以根据内核和设备树的总大小来调整,预留一些余量。part /: 定义Rootfs分区,大小由前面提到的IMAGE_ROOTFS_*变量决定。bootloader --ptable msdos: 指定生成MBR格式的分区表。
你可以在你的机器配置(.conf)文件中通过WKS_FILE变量指定自定义的.wks文件。构建完成后,在tmp/deploy/images/<machine>/目录下,除了单独的Image、dtb、rootfs.tar.bz2,还会生成一个<image-name>-<machine>.wic文件。这个.wic文件就是一个包含了完整分区表和所有数据的磁盘镜像,可以直接用dd命令或UUU烧录到存储设备,无需额外的分区脚本。
注意:使用UUU烧录
.wic镜像需要版本1.4.165或更高。命令非常简单:uuu <your_board_specific_script>.uuu,在脚本中对应位置使用FB: flash -raw2sparse all <image-name>.wic即可。
4.2 在镜像部署阶段进行分区(U-Boot环境)
这种方法类似于处理预构建镜像,但使用的是从Yocto构建产出中提取的、更纯净的组件。它适合需要在U-Boot环境下进行动态分区,或者你的部署流程已经固化在U-Boot脚本中的场景。
首先,你需要确保U-Boot配置中启用了CONFIG_CMD_MBR支持,以便使用mbr write命令。可以通过在U-Boot源码目录执行make menuconfig,然后在Command line interface -> MMC utilities下找到并启用。
构建出支持MBR的U-Boot后,Yocto构建产物中会包含分离的分区镜像。在deploy/images/<machine>/目录下,.wic文件旁边通常会有<image-name>-<machine>.wic.bmap和分解出的分区镜像,如<image-name>-<machine>.direct(完整镜像)以及<image-name>-<machine>.direct.0.fat(Boot分区)、<image-name>-<machine>.direct.1.img(Rootfs分区)。
部署脚本的核心部分从使用Linux下的sfdisk,转变为在U-Boot中使用mbr write命令:
# 在UUU脚本的FB(Fastboot)阶段,向U-Boot传递命令 FB: ucmd setenv mbr_parts 'name=boot,start=8M,size=128M,bootable,id=0x0e;name=rootfs,start=140M,size=4096M,id=0x83' FB: ucmd mbr write mmc ${emmc_dev}mbr_parts环境变量的语法定义了分区:名称、起始偏移、大小、是否可启动、分区类型ID。定义好后,mbr write命令会将其写入指定MMC设备。
后续的烧录命令也变为使用flash -raw2sparse直接写入对应的分区句柄:
FB: flash -raw2sparse mmcsda1 0.fat FB: flash -raw2sparse mmcsda2 1.img FB: flash bootloader imx-boot-imx8mqevk-sd.bin-flash_evk这里mmcsda1和mmcsda2是U-Boot中对eMMC第一个和第二个分区的命名。这个命名需要根据你的平台和存储设备类型来确认。最准确的方法是在成功烧录一次预构建镜像后,在U-Boot命令行下输入mmc part或gpt read mmc 2(假设eMMC是设备2)来查看分区列表和对应的句柄。
4.3 Yocto部署的注意事项
- 分区扩展问题:无论是通过
.wks文件定义分区,还是通过U-Boot的mbr write命令创建分区,如果Rootfs分区的大小被设置得大于实际根文件系统镜像的大小,那么在首次启动Linux后,你需要手动使用resize2fs命令来扩展文件系统以填满整个分区空间。否则,多出来的空间无法被使用。命令通常是resize2fs /dev/mmcblk2p2(请替换为你的实际Rootfs分区设备)。 - Wic镜像的通用性:直接烧录
.wic镜像虽然方便,但它的分区布局是固定的。如果你的产品线有多种存储容量(如8GB和16GB的eMMC),使用固定大小的.wic镜像可能不适合小容量设备,或者会浪费大容量设备的空间。此时,使用部署阶段分区的动态脚本可能更灵活。 - 构建产物管理:Yocto构建一次会产生大量中间文件和最终镜像,占用大量磁盘空间。定期清理
tmp和cache目录是必要的。同时,建议对重要的构建配置(local.conf,bblayers.conf)和自定义的.wks文件进行版本管理。
5. 常见问题排查与高级技巧
在实际操作中,你肯定会遇到各种问题。这里我总结了一些典型故障和排查思路。
5.1 系统无法启动:Bootloader阶段失败
- 现象:上电后,串口无输出,或输出少量信息后停止。
- 排查步骤:
- 检查启动模式:确认板子的启动模式拨码开关是否正确设置为从eMMC/SD卡启动,并且处于正常启动(Normal Boot)模式,而非下载模式。
- 确认Bootloader镜像:确保烧录的
imx-boot镜像与你的芯片型号(如8MQ, 8MM)和启动设备(-sd表示SD/eMMC)完全匹配。烧录到了正确的偏移地址(如33KB)。 - 检查存储介质:尝试更换一张新的或已知良好的SD卡/eMMC。有时存储介质损坏或兼容性问题会导致Bootloader无法被正确读取。
- 测量电压与时钟:使用示波器检查核心电压、DDR电压和时钟是否正常。不稳定的电源是Bootloader运行失败的常见硬件原因。
5.2 系统无法启动:内核加载阶段失败
- 现象:U-Boot启动成功,打印出版本信息,但在尝试加载内核(
Image)或设备树(dtb)时失败,提示“File not found”、“Bad Linux ARM64 Image magic!”或直接复位。 - 排查步骤:
- 检查文件名:在U-Boot中使用
fatls mmc <dev>:<part>命令(例如fatls mmc 2:1)列出Boot分区内容,确认Image和.dtb文件是否存在且名称与U-Boot环境变量bootcmd或bootargs中指定的一致。 - 检查文件完整性:在U-Boot中尝试使用
iminfo命令检查内核镜像头信息,或使用fatload加载一小段数据看看是否成功。也可以在主机的Linux环境下使用file命令检查Image文件是否为有效的ARM64内核镜像。 - 检查设备树:确保设备树二进制文件(
.dtb)是针对你当前使用的具体板型(包括内存大小、外设连接)编译的。错误的设备树会导致内核在初始化硬件时崩溃。 - 检查Boot分区格式和大小:确认Boot分区是FAT32格式,并且有足够的剩余空间。损坏的文件系统可能导致读取失败。
- 检查文件名:在U-Boot中使用
5.3 系统无法启动:内核启动后挂载根文件系统失败
- 现象:内核解压成功,开始启动,但最后卡在“Kernel panic - not syncing: VFS: Unable to mount root fs”或类似错误。
- 排查步骤:
- 检查内核命令行参数:在U-Boot中使用
printenv查看bootargs变量。关键参数是root=,它必须正确指向你的Rootfs分区,例如root=/dev/mmcblk2p2 rootwait rw。确认分区号(p2)与实际相符。 - 检查文件系统格式:确认Rootfs分区是用
ext4(或ext3)格式化的,并且与内核中启用的文件系统驱动匹配(通常ext4是默认编译的)。 - 检查根文件系统内容:在U-Boot中,如果支持
ext4ls命令,可以尝试列出Rootfs分区根目录,看是否有/bin,/sbin,/lib等目录。或者,将存储介质连接到电脑,用读卡器检查文件系统是否完整解压。 - 启用更详细的内核日志:在U-Boot的
bootargs中添加earlycon、loglevel=8(或debug)等参数,让内核输出更详细的启动信息,有助于定位挂载失败的具体原因。
- 检查内核命令行参数:在U-Boot中使用
5.4 UUU工具使用问题
- 现象:执行UUU脚本时失败,提示无法识别设备、命令错误或传输失败。
- 排查步骤:
- 设备连接:确保板子处于串行下载模式,USB线连接正确(连接到板子的USB OTG口,而非USB HOST口)。在Linux主机上使用
lsusb命令,应能看到NXP Semiconductor或Freescale的USB下载设备。 - 权限问题(Linux):在Linux下,可能需要将当前用户加入
plugdev组,或为UUU工具设置sudo权限,或创建合适的udev规则,以避免Permission denied错误。 - 脚本语法:确保UUU脚本的语法与你的UUU工具版本兼容。较新的UUU工具可能废弃了某些旧命令(如
SDPU),推荐使用SDPV。 - 镜像文件路径:确保UUU脚本中引用的镜像文件(
-f参数)路径正确,或者这些文件与UUU脚本在同一目录下,UUU会在当前目录和脚本所在目录查找。
- 设备连接:确保板子处于串行下载模式,USB线连接正确(连接到板子的USB OTG口,而非USB HOST口)。在Linux主机上使用
5.5 高级技巧:创建多套分区方案脚本
对于需要频繁切换测试不同配置(如不同内核版本、不同根文件系统)的开发者,可以准备多个部署脚本。例如:
flash_emmc_default.uuu: 烧录默认的预构建镜像。flash_emmc_large_rootfs.uuu: 烧录自定义的、Rootfs分区更大的镜像。flash_emmc_custom_kernel.uuu: 烧录预构建的Rootfs,但使用自己编译的内核和设备树。
通过修改脚本中的PARTSTR、镜像文件名和路径,可以快速切换。将这些脚本和对应的镜像文件归档管理,能极大提升开发和测试效率。
5.6 高级技巧:在U-Boot中动态调整启动参数
如果你不想每次修改内核或设备树都重新烧录Boot分区,可以利用U-Boot的环境变量。将内核和设备树文件放在Boot分区里,然后在U-Boot中设置:
setenv loadimage 'fatload mmc 2:1 ${loadaddr} Image' setenv loadfdt 'fatload mmc 2:1 ${fdt_addr} imx8mq-my-custom.dtb' setenv bootargs 'console=ttymxc0,115200 earlycon=ec_imx6q,0x30860000,115200 root=/dev/mmcblk2p2 rootwait rw' setenv bootcmd 'run loadimage; run loadfdt; booti ${loadaddr} - ${fdt_addr}' saveenv这样,你只需要替换Boot分区里的文件,就可以改变启动内容,而无需修改分区表或重写整个存储介质。这对于内核调试阶段非常有用。