1. 项目背景与核心目标
最近在折腾一块基于NXP i.MX6ULL芯片的开发板,想把U-Boot从SD卡启动起来。这听起来像是嵌入式开发里最基础的操作,但真上手了才发现,从“知道”到“做到”之间,隔着不少细节。网上教程很多,但要么语焉不详,要么环境对不上,照着做总差那么一点。比如,用dd命令烧写镜像,参数怎么写?SD卡分区怎么处理?启动失败串口一片漆黑怎么办?这些问题不解决,后续的Linux内核移植、根文件系统加载都无从谈起。
这个笔记,就是记录我如何一步步搞定i.MX6ULL从SD卡启动U-Boot的全过程。我会把每个步骤背后的原理、实际操作中的坑以及验证方法都讲清楚。无论你是刚接触这块板子,还是卡在了启动环节,希望这篇内容能帮你把路走通。整个过程会涉及硬件准备、镜像获取与处理、SD卡的正确烧写、启动参数配置以及最终的验证调试。我们不止要看到“绿灯亮、串口有输出”,更要明白为什么这样操作是有效的。
2. i.MX6ULL启动流程深度解析:ROM Code在做什么?
在动手烧写SD卡之前,我们必须先搞清楚i.MX6ULL这颗芯片上电后到底执行了哪些操作。很多启动失败的问题,根源在于对启动流程的理解有偏差。i.MX6ULL的启动是一个多阶段的过程,由芯片内部的只读存储器代码(ROM Code)主导。
2.1 启动媒介的选择与搜索顺序
i.MX6ULL的ROM Code上电后,第一件事就是按照预先设定的顺序去寻找可以启动的代码。这个顺序由芯片的启动配置引脚(BOOT_MODE[1:0])的状态决定。最常见的情况是将板子设置为从外部媒介启动(如SD卡、eMMC、NAND Flash)。当配置为从SD卡启动时,ROM Code会依次检查以下设备:
- SD卡(通过USDHC接口):这是我们本次操作的重点。
- eMMC:如果SD卡启动失败或无卡,则会尝试eMMC。
- 其他接口:如NAND、串行NOR Flash等,具体取决于板级设计。
这里有一个关键点:ROM Code并非直接去执行SD卡里任意位置的代码。它有一套严格的“映像”查找规则。
2.2 启动映像的结构:IVT、DCD与Boot Data
ROM Code在SD卡中寻找的,是一个符合其要求的“启动映像”。这个映像不是简单的二进制堆砌,而是一个有固定格式的容器。其核心结构包括:
- 映像向量表(Image Vector Table, IVT):这是映像的“目录”或“入口点索引”。IVT必须被放置在ROM Code已知的固定偏移地址处。对于从SD卡启动,这个偏移地址通常是SD卡块设备的第1个块(Block 1, 即偏移512字节),有时也可能是第2个块(Block 2),具体取决于芯片型号和Boot ROM版本。IVT里面包含了程序入口点、DCD表的地址、Boot Data的地址等重要指针。
- 设备配置数据(Device Configuration Data, DCD):这是一系列用于初始化芯片外部RAM(通常是DDR3/LPDDR2)的寄存器配置命令。因为U-Boot本身运行需要内存,而内存控制器在上电后处于未初始化状态,所以必须由ROM Code在跳转到U-Boot之前,先根据DCD的指令完成内存初始化。DCD配置错误是导致启动失败(串口无输出或输出乱码)的最常见原因之一。
- Boot Data:包含了映像的起始地址和长度等信息。
- 实际的程序代码:即U-Boot的二进制文件(
u-boot.bin或u-boot.imx)。
注意:我们通常使用的
u-boot.imx文件,其实就是由编译工具(如mkimage)将IVT、DCD和u-boot.bin打包在一起形成的完整启动映像。而u-boot.bin是纯粹的二进制代码,不包含头部信息。
2.3 为什么是dd命令?烧写的本质
理解了启动映像的结构,就明白了为什么我们不能简单地把u-boot.bin复制到SD卡,而必须使用dd命令进行“块设备”级别的烧写。
- 文件系统 vs 块设备:当我们用
cp命令时,操作对象是文件系统(如FAT32、ext4)里的文件。而ROM Code启动时,SD卡驱动处于非常原始的状态,它不会、也不能去解析复杂的文件系统。它只认“块设备”的原始扇区。 - 精确的偏移地址:
dd命令的seek参数允许我们将数据写入到块设备的指定扇区偏移处。我们必须将包含IVT的完整启动映像(u-boot.imx)写入到ROM Code期望的固定偏移(例如1KB或2KB偏移处),ROM Code才能正确找到并加载它。 - 破坏性操作:
dd命令会直接覆盖指定扇区的原始数据。如果烧写位置错误(例如写到了分区表所在的位置),会导致SD卡无法被操作系统识别。因此,操作前确认SD卡设备名和偏移量至关重要。
3. 实战准备:工具、镜像与SD卡处理
理论清楚了,我们开始准备实战环境。工欲善其事,必先利其器。
3.1 硬件与软件工具清单
硬件:
- i.MX6ULL开发板(如正点原子、野火等)。
- Micro SD卡一张(建议容量4GB以上,Class 10或更高速度)。
- SD卡读卡器。
- USB转串口调试线(如CH340、CP2102等),用于查看启动日志。
- 电脑(Linux环境,可以是虚拟机或WSL2,推荐Ubuntu 20.04/22.04)。
软件与镜像:
- U-Boot源码与交叉编译工具链:你需要获取对应你开发板厂商提供的U-Boot源码,或者从NXP官方仓库克隆。交叉编译工具链通常由厂商提供(如
gcc-linaro-arm-linux-gnueabihf)。 - 预编译的U-Boot镜像:如果你是第一次尝试,强烈建议先使用开发板厂商提供的、已验证可用的
u-boot.imx文件。这能排除编译环境带来的问题。 - 串口终端软件:如
minicom,picocom, 或Windows下的MobaXterm、Putty。 - Linux下的磁盘工具:
fdisk或parted用于分区,dd用于烧写。
- U-Boot源码与交叉编译工具链:你需要获取对应你开发板厂商提供的U-Boot源码,或者从NXP官方仓库克隆。交叉编译工具链通常由厂商提供(如
3.2 获取与编译U-Boot镜像(可选但建议了解)
如果你需要自己编译U-Boot,流程大致如下。这能帮助你理解镜像的生成过程。
# 1. 设置交叉编译工具链环境变量 export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- # 请替换为你的工具链前缀 export PATH=$PATH:/your/toolchain/path/bin # 2. 进入U-Boot源码目录,配置为你的开发板 # 以正点原子I.MX6ULL ALPHA开发板为例 make mx6ull_14x14_evk_defconfig # 使用最接近的默认配置 # 3. 启动图形化或命令行配置(可选,用于微调) # make menuconfig # 4. 编译 make -j$(nproc) # 编译成功后,会在当前目录生成关键文件: # u-boot: ELF格式文件,用于调试。 # u-boot.bin: 纯二进制文件,不包含IVT和DCD。 # u-boot.imx: 由u-boot.bin加上IVT、DCD等头部信息生成,是用于烧写的完整镜像。 # u-boot.srec: S-Record格式文件。关键点:u-boot.imx文件是通过tools/mkimage工具,并参考板级配置文件(如include/configs/mx6ullevk.h)中关于CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR的定义(即烧写到SD卡的扇区偏移),将u-boot.bin打包而成的。这个偏移量(例如0x400=1024字节,即2个扇区)必须与后续dd命令的seek参数匹配。
3.3 SD卡的分区与格式化处理
这是一个极易出错的环节。很多教程直接让dd到/dev/sdb,这非常危险,可能会覆盖你的系统盘。同时,一个未经处理的、带有分区表的SD卡也可能导致启动异常。
安全操作流程:
插入SD卡并确认设备号:
sudo fdisk -l仔细查看输出,找到你的SD卡设备。通常它会是
/dev/sdb或/dev/mmcblk0。务必确认容量等信息与你插入的SD卡一致!假设我们的SD卡是/dev/sdb。彻底清空SD卡(清除旧分区表):
sudo dd if=/dev/zero of=/dev/sdb bs=1M count=10这个命令向SD卡的前10MB写入0,这会清除MBR分区表和可能存在的文件系统签名。再次警告:
of=/dev/sdb目标设备绝对不能写错!创建新分区(可选,但推荐): 如果你希望这张SD卡后续还能用于存放内核(
zImage)和设备树(.dtb)文件,可以创建一个FAT32分区。U-Boot本身可以从RAW扇区启动,但加载内核通常需要文件系统支持。sudo fdisk /dev/sdb在
fdisk交互界面中:- 输入
n创建新分区。 - 选择
p主分区。 - 分区号
1。 - 起始扇区,这里非常关键!我们必须为U-Boot镜像预留空间。假设
u-boot.imx大小约400KB,我们预留1MB(2048个扇区)是安全的。所以起始扇区设置为2048。 - 结束扇区,直接回车使用默认(最后一个扇区)。
- 输入
t更改分区类型,选择c(W95 FAT32 (LBA))。 - 输入
w写入并退出。
- 输入
格式化分区:
# 如果SD卡是/dev/sdb,那么第一个分区是/dev/sdb1 sudo mkfs.vfat /dev/sdb1现在,你的SD卡有一个干净的FAT32分区,且前1MB空间是未分配的,专门留给U-Boot。
4. 核心操作:使用dd命令烧写U-Boot镜像
这是最具决定性的步骤。我们将把u-boot.imx写入SD卡的预留区域。
4.1 确定烧写偏移量(Seek值)
偏移量的单位是“块”(Block),在dd命令中通常一块是512字节。我们需要知道两个信息:
- ROM Code查找IVT的偏移:对于i.MX6ULL从SD卡启动,常见的偏移是1KB (0x400) 或 2KB (0x800),即2个扇区或4个扇区。这取决于芯片的Boot ROM版本和配置。最保险的方法是查阅开发板手册或参考厂商提供的烧写脚本。正点原子的板子通常是2个扇区(seek=2)。
- 我们预留的空间起始扇区:在上一步分区中,我们将分区起始设在2048扇区,所以前2048个扇区(1MB)都是空闲的。只要烧写位置在这个范围内,且与ROM Code期望的匹配即可。
假设我们采用seek=2(即从第2个扇区开始写,跳过前1KB)。
4.2 执行dd命令烧写
# 假设u-boot.imx在当前目录,SD卡设备是/dev/sdb sudo dd if=u-boot.imx of=/dev/sdb bs=512 seek=2 conv=fsync逐参数解释:
if=u-boot.imx: 输入文件,即你的U-Boot镜像。of=/dev/sdb:输出设备,必须是SD卡的整体设备(如/dev/sdb),而不是分区(如/dev/sdb1)!bs=512: 设置读写块大小为512字节,与磁盘扇区大小一致。seek=2: 跳过输出文件(即SD卡)开头的2个块(2 * 512 = 1024字节),从这个位置开始写入。这正好对应ROM Code从1KB偏移处查找IVT。conv=fsync: 确保所有数据完全写入物理设备后再返回,避免缓存导致的数据不完整。
烧写成功的提示:命令会输出类似“记录了 x+0 的读入”和“记录了 x+0 的写出”的信息,并显示拷贝的字节数。
4.3 验证烧写结果(可选但推荐)
烧写完成后,可以读回来一部分数据,与原始镜像头部对比,确保写入正确。
# 从SD卡的1KB偏移处读取1KB数据 sudo dd if=/dev/sdb of=read_back.bin bs=512 skip=2 count=2 # 使用hexdump或od命令查看原始镜像和读回数据的头部 hexdump -C u-boot.imx | head -20 hexdump -C read_back.bin | head -20对比两者开头部分(特别是前几十个字节),应该完全一致。你会看到IVT的签名等信息。
5. 上电调试与启动失败问题排查
将烧写好的SD卡插入开发板的SD卡槽,连接串口线到电脑,打开串口终端(波特率通常设置为115200,数据位8,停止位1,无校验)。给开发板上电。
5.1 成功启动的现象
如果一切顺利,你将在串口终端看到类似以下的输出:
U-Boot 2022.04 (Jun 01 2023 - 15:30:00 +0800) CPU: Freescale i.MX6ULL rev1.1 528 MHz (running at 396 MHz) CPU: Commercial temperature grade (0C to 95C) at 48C Reset cause: POR Model: Freescale i.MX6ULL 14x14 EVK Board Board: MX6ULL 14x14 EVK DRAM: 512 MiB MMC: FSL_SDHC: 0, FSL_SDHC: 1 Loading Environment from MMC... *** Warning - bad CRC, using default environment In: serial Out: serial Err: serial Net: No ethernet found. Hit any key to stop autoboot: 3看到“Hit any key to stop autoboot”的提示,就说明U-Boot已经从SD卡成功启动并运行起来了。
5.2 常见启动失败问题与排查思路
如果串口没有任何输出,或者输出乱码、卡住,请按以下步骤排查:
检查硬件连接:
- 串口线是否接对(开发板的UART_TX接调试线的RX,UART_RX接TX,GND接GND)?
- 串口终端软件参数(波特率115200, 8N1)是否正确?
- 开发板启动模式拨码开关(BOOT_MODE)是否设置为从SD卡启动?请查阅你的开发板手册确认。
- SD卡是否插紧?尝试换一张SD卡。
检查镜像与烧写过程:
- 镜像是否正确?首先使用厂商提供的预编译镜像进行测试,排除自己编译产生问题的可能。
dd命令的seek参数是否正确?这是最高频的错误点。如果开发板ROM Code期望从2KB(4个扇区)启动,而你写在了1KB(2个扇区),就无法启动。尝试seek=2和seek=4。有些板子甚至是seek=1。- 是否烧写到了正确的设备?再次用
sudo fdisk -l确认SD卡设备名。 - SD卡是否有残留分区信息干扰?确保执行了
dd if=/dev/zero of=/dev/sdX bs=1M count=10清空操作。
检查DCD配置(内存初始化):
- 如果串口有输出但很快卡死,或输出大量乱码,极有可能是DCD配置不正确,导致DDR内存初始化失败,U-Boot代码无法在内存中运行。
- DCD配置与开发板使用的具体DDR芯片型号、大小、布线密切相关。不同厂商、不同版本的开发板,DCD参数可能不同。
- 解决方案:确保你使用的
u-boot.imx镜像中的DCD配置与你的开发板硬件完全匹配。最可靠的方法是使用开发板厂商提供的U-Boot源码和配置文件进行编译,或者直接使用他们提供的预编译镜像。
使用JTAG调试(终极手段):
- 如果以上步骤都无法解决,并且你有JTAG调试器(如J-Link),可以尝试连接JTAG,单步跟踪ROM Code的执行,查看它在加载镜像时卡在了哪一步。这对于分析DCD初始化失败等问题非常有效,但门槛较高。
一个实用的排查技巧:尝试编译或寻找一个最简单的、只初始化串口并打印“Hello World”的裸机程序,将其制作成符合IVT格式的镜像并烧写到SD卡。如果这个简单程序能运行并打印,说明启动流程和烧写方法基本正确,问题很可能出在U-Boot本身的DDR配置或代码上。如果简单程序也无法运行,那就要重点检查启动模式、烧写偏移和硬件了。
6. 进阶:配置U-Boot环境变量与测试内核启动
U-Boot成功启动后,我们的工作只完成了一半。一个可用的启动加载器还需要能加载并启动Linux内核。
6.1 理解U-Boot环境变量
U-Boot有一组环境变量,类似于BIOS设置,决定了它的行为。例如:
bootcmd: 定义自动启动时执行的命令序列。bootargs: 传递给Linux内核的启动参数。ipaddr,serverip: 网络相关配置,用于tftp下载。
这些变量可以保存在SD卡、eMMC或SPI Flash的某个区域。第一次启动时,由于该区域是空的,U-Boot会使用默认的内置环境。
6.2 为从SD卡加载内核做准备
我们之前为SD卡创建了FAT32分区(/dev/sdb1),现在可以将内核和设备树文件放进去。
挂载SD卡分区并拷贝文件:
# 在Linux电脑上操作 mkdir /mnt/sdcard sudo mount /dev/sdb1 /mnt/sdcard sudo cp zImage /mnt/sdcard/ # 你的Linux内核镜像 sudo cp imx6ull-xxx.dtb /mnt/sdcard/ # 你的设备树文件 sudo umount /mnt/sdcard在U-Boot中配置环境变量: 开发板上电,在U-Boot倒计时结束前按任意键进入命令行。
# 设置服务器IP(你的电脑IP)和开发板IP setenv serverip 192.168.1.100 setenv ipaddr 192.168.1.101 # 设置从SD卡(第0个SDHC设备,第1分区)加载内核和设备树的命令 # ‘mmc dev 0’ 切换到SD卡(mmc0通常是SD卡,mmc1可能是eMMC) # ‘fatload’ 从FAT分区加载文件到内存 setenv loadkernel 'mmc dev 0; fatload mmc 0:1 ${loadaddr} zImage' setenv loadfdt 'fatload mmc 0:1 ${fdt_addr} imx6ull-xxx.dtb' # 设置启动参数,告诉内核根文件系统在哪里(例如在SD卡的第2个分区) setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk0p2 rootwait rw' # 定义完整的启动命令 setenv bootcmd 'run loadkernel; run loadfdt; bootz ${loadaddr} - ${fdt_addr}' # 保存环境变量到存储介质(如SD卡) saveenv输入
printenv可以查看所有环境变量。输入boot或等待倒计时结束,U-Boot就会执行bootcmd,尝试加载并启动内核。
6.3 测试与验证
执行boot命令后,观察串口输出。如果内核开始解压并运行,最终可能会因为找不到根文件系统而卡住(因为我们还没做根文件系统),但这已经证明了U-Boot成功完成了加载内核的使命。你会看到内核的版本信息、设备树解析日志等。
至此,通过SD卡启动U-Boot的核心任务已经圆满完成。这个过程虽然步骤繁多,但每一步都有其明确的意图。从理解ROM Code的启动机制,到谨慎处理SD卡分区,再到精确使用dd命令,最后完成环境配置,这本身就是嵌入式开发中最经典的入门实践。掌握了它,你就拿到了打开i.MX6ULL世界大门的钥匙,后续的内核移植、驱动开发、根文件系统构建都将在此基础上展开。下次当你需要为不同的存储设备(如eMMC、NAND)部署U-Boot时,你会发现其核心逻辑是相通的,只是存储介质的访问方式和烧写工具有所不同而已。