公司动态
i.MX6ULL启动流程全解析:从ROM Code到Linux内核的嵌入式系统引导
1. 项目概述从按下电源到系统就绪的旅程搞嵌入式开发尤其是像NXP i.MX6ULL这种应用处理器最基础也最绕不开的一个环节就是启动流程。这东西就像你电脑开机时BIOS到Windows加载的过程只不过在资源受限的嵌入式世界里这个过程更精细、更可控也更容易出问题。很多新手拿到开发板烧录完系统镜像看到串口有输出就以为万事大吉但一旦遇到启动卡住、DDR初始化失败、镜像校验不过这些幺蛾子如果对启动流程两眼一抹黑那基本就只能抓瞎了。我这次就以i.MX6ULL这颗经典的Cortex-A7芯片为例把它的启动流程从头到尾、从硬件到软件给你捋清楚。这不仅仅是看一遍官方手册的ROM Code章节那么简单我会结合我这些年调试各种启动失败问题的实际经验告诉你每个阶段芯片在干什么、我们烧录的镜像里对应是什么、常见的坑在哪里以及怎么排查。理解了这个流程你不仅能搞定日常的固件更新更能深入理解系统是如何从一片“死”的Flash里“活”过来的这对于你做系统裁剪、安全启动、量产工具开发都至关重要。2. i.MX6ULL启动流程全景解析2.1 启动模式的硬件选择i.MX6ULL上电后第一件事不是执行代码而是“看引脚脸色”。芯片内部有一个叫BOOT_MODE的硬件模块它通过检测芯片特定引脚BOOT_MODE0和BOOT_MODE1在上电复位时的电平来决定从哪里、以什么方式启动。这通常由开发板上的拨码开关或上下拉电阻来配置。主要的启动模式有两大类内部Boot模式芯片从内部的Boot ROM开始执行。这是我们最常用的模式也是后续所有复杂启动的起点。Boot ROM是芯片出厂时就固化在内部只读存储器里的一段代码不可修改其首要职责是初始化最基础的硬件如时钟、临时存储介质然后根据其他引脚BOOT_CFG的配置去外部存储设备如SD卡、eMMC、NAND Flash、QSPI NOR Flash里寻找并加载用户程序。外部启动模式例如串行下载模式。当芯片检测到处于串行下载模式时Boot ROM会初始化USB OTG或UART接口然后等待主机通常是PC通过NXP提供的工具如mfgtools或uuu发送程序镜像过来并直接执行。这个模式主要用于工厂烧录、板级测试或者在系统完全无法从外部存储启动时的救砖操作。注意BOOT_MODE引脚的电平必须在芯片复位释放上电完成之前保持稳定。如果在运行中改变拨码开关是没用的必须重新上电。很多硬件问题比如启动始终进不了ROM Code首先要查的就是这几个引脚的上下拉电阻和开关状态。2.2 多阶段启动的接力赛选定从内部ROM启动后一个典型的、完整的启动流程就像一场多级火箭发射每一级完成自己的使命后将控制权交给下一级更强大的引擎。对于i.MX6ULL这个过程通常分为以下四个关键阶段ROM Code这是芯片上电后执行的第一段代码固化在芯片内部。它负责最底层的硬件初始化读取BOOT_CFG引脚决定从哪个外部设备启动然后从该设备的固定位置对于SD卡是第1个扇区后的某个偏移量对于NAND是Page 0加载下一阶段引导程序。ROM Code只认识一种特定格式的镜像即包含Image Vector Table (IVT)、Boot Data和Device Configuration Data (DCD)的集合。DCD是一系列用于配置芯片内部寄存器尤其是DDR控制器、时钟等的指令由ROM Code解析并执行这对于在SDRAMDDR可用之前配置好它至关重要。Bootloader Stage 1 (SPL/U-Boot SPL)由于ROM Code空间和能力有限它加载的通常不是一个完整的U-Boot而是一个精简版的、称为SPLSecondary Program Loader或U-Boot SPL的引导程序。SPL的主要任务是在ROM Code已经完成部分初始化如时钟、DDR的基础上进行更完整的硬件初始化然后从存储设备加载体积更大的、功能完整的主Bootloader到DDR中并跳转执行。SPL本身通常运行在芯片内部的RAMOCRAM里因为此时DDR可能还未完全初始化或不可靠。Bootloader Stage 2 (Full U-Boot)这就是我们熟悉的U-Boot了。它运行在已经初始化好的DDR中拥有丰富的功能初始化更多外设网卡、USB等、提供命令行交互、从网络、USB或存储设备加载操作系统内核Linux Kernel、设备树Device Tree Blob和初始内存盘initramfs并最终将控制权交给内核。Linux Kernel内核接管后会进行更高级别的系统初始化解压自身如果是压缩内核解析设备树获取硬件信息初始化进程管理、内存管理、驱动框架最后挂载根文件系统启动第一个用户空间进程通常是/sbin/init至此系统启动完成。整个流程的核心在于每一级都为下一级准备好执行环境特别是内存空间。ROM Code用DCD配好DDRSPL才能把自己或U-Boot搬到DDRU-Boot把内核、设备树、根文件系统地址安排好内核才能顺利接手。3. 核心镜像文件深度拆解要理解启动流程必须搞清楚我们烧写到存储设备里的那个“镜像文件”到底是什么。它不是一个单一的程序而是一个结构化的数据包。3.1 IVT、Boot Data与DCDROM Code的“寻人启事”ROM Code从存储设备的固定位置读取数据时它期望找到一个非常特定的数据结构这个结构由三部分组成Image Vector Table这是入口点。IVT里包含了一系列绝对地址的指针其中最重要的两个是程序入口点指向可执行代码的开始和DCD的地址。ROM Code首先找到IVT然后根据IVT的指引去找到DCD和程序本身。Device Configuration Data这是一系列寄存器配置命令的集合。在i.MX6ULL中DCD的主要使命是在芯片内部的Boot ROM运行阶段去配置那些必须在外部SDRAMDDR可用之前就配置好的控制器尤其是DDR控制器的时序参数如频率、CAS延迟、行列地址宽度等。ROM Code会解析并执行DCD中的命令将DDR初始化好这样后续的SPL或U-Boot才能被加载到这片高速内存中运行。DCD的配置是否正确直接决定了你的板子能否成功“点亮”DDR这是启动过程中最容易出硬件兼容性问题的地方。Boot Data包含了镜像长度、加载到DDR中的目标地址等信息。在U-Boot的编译输出中我们经常看到u-boot.imx这个文件。这个.imx后缀的文件就是U-Boot的ELF可执行文件u-boot经过工具处理在前面加上了IVT、Boot Data和DCD后生成的最终镜像。ROM Code只认这个格式。3.2 SPL与U-Boot的生成与关系在复杂的系统中U-Boot本身可能很大几百KB而ROM Code能加载的大小有限制。因此引入了SPL。SPL的生成在编译U-Boot时通过配置如make imx6ull_xxx_defconfig会同时编译出两个主要产物spl/u-boot-spl.binSPL的二进制和u-boot.bin主U-Boot的二进制。然后构建系统会调用NXP提供的工具mkimage或芯片专用工具将u-boot-spl.bin也打包成ROM Code可识别的格式生成SPL文件注意没有后缀。这个SPL文件就是包含了IVT/DCD的、给ROM Code加载的第一阶段镜像。最终的烧写镜像对于从SD卡启动我们通常需要将SPL和u-boot.img或u-boot.bin按照特定的偏移量烧写到SD卡中。SPL写在靠前的位置如SD卡的第1个扇区之后u-boot.img写在后面。ROM Code加载并运行SPLSPL再去加载u-boot.img。对于eMMC/NAND情况类似但偏移量不同。有时我们会用一个工具将SPL和U-Boot打包成一个单一镜像如u-boot.imx这个文件其实已经包含了SPL。具体要看板级配置。实操心得当你自己定制板子更换了DDR芯片型号后启动失败十有八九是DCD配置不对。你需要根据新DDR芯片的数据手册重新计算时序参数并修改U-Boot源码中对应板级的DCD配置数组通常在arch/arm/mach-imx/imx6ull-ddr3-xxx.c这样的文件中然后重新编译SPL。不要试图在U-Boot阶段再去改DDR配置那时已经晚了。4. 从存储介质到内核启动的实操推演4.1 以SD卡启动为例的完整链条我们以一个最常见的场景——从SD卡启动Linux系统来串联整个流程硬件准备BOOT_MODE引脚设置为内部BootBOOT_CFG引脚设置为从SD/EMMC启动。芯片上电。ROM Code阶段芯片执行内部ROM Code。ROM Code初始化基础时钟、MMC/SD控制器。根据BOOT_CFG它知道要去SD卡寻找启动镜像。它会读取SD卡的第0x400字节即1个扇区后开始的内容寻找IVT头部。找到IVT后解析其中的DCD指针执行DCD命令来配置DDR控制器。如果DDR型号匹配配置正确DDR就被初始化好了。然后ROM Code根据IVT中的入口地址将IVT之后的可执行代码也就是我们的SPL加载到芯片内部的OCRAMOn-Chip RAM约128KB中指定的地址并跳转执行。SPL阶段SPL在OCRAM中开始运行。它可能会进行一些额外的、更细致的硬件初始化。它的核心任务是加载完整的U-Boot。它会再次访问SD卡从预先约定好的另一个偏移量比如第0x8000扇区读取u-boot.img并将其加载到已经被ROM Code初始化好的DDR内存中。加载完成后SPL跳转到DDR中的U-Boot入口点。U-Boot阶段完整的U-Boot在DDR中运行它拥有命令行界面。U-Boot根据环境变量如bootcmd来决定下一步做什么。典型的bootcmd可能是load mmc 0:1 ${loadaddr} zImage从SD卡第一个分区加载内核镜像到内存地址${loadaddr}。load mmc 0:1 ${fdt_addr} imx6ull-xxx.dtb加载设备树文件。bootz ${loadaddr} - ${fdt_addr}启动内核并传递设备树地址。U-Boot将控制权、内核地址、设备树地址等信息传递给Linux内核。Linux内核阶段内核解压如果使用了压缩镜像如zImage。内核解析设备树识别CPU、内存、外设等。初始化虚拟内存、调度器、驱动框架。尝试挂载根文件系统根文件系统可能在SD卡的后续分区也可能被包含在initramfs中随内核一起加载。挂载成功后执行根文件系统中的/sbin/init启动用户空间系统启动完成。4.2 eMMC与NAND启动的特殊性eMMC启动逻辑与SD卡类似因为eMMC也遵循MMC协议。主要区别在于硬件连接通常走8位数据线和寻址偏移量。ROM Code从eMMC的Boot Partition启动分区的固定偏移量读取数据。在量产时我们通常将SPL和U-Boot烧写到eMMC的启动分区这样即使eMMC的用户分区被擦除系统也能从启动分区正常引导这对于产品恢复很有用。NAND Flash启动NAND启动更复杂。因为NAND存在坏块且读写以页为单位。ROM Code支持从NAND启动但它要求镜像必须写在从Block 0, Page 0开始的连续好块中。ROM Code内置了简单的坏块管理会跳过标记为坏块的区域。但正因如此烧写到NAND的镜像需要特殊的处理确保其结构如IVT能被ROM Code在存在坏块的情况下正确找到。通常U-Boot的构建系统会生成一个叫u-boot.nand的镜像它已经考虑了NAND的页布局和坏块问题。5. 启动失败问题排查实战手册理解了流程排查问题就有了路线图。下面是一个从现象倒推问题的排查思路。5.1 串口无任何输出这是最让人头疼的情况说明系统在非常早的阶段就卡住了。检查电源和时钟用万用表和示波器测量核心电压如VDD_SOC_CAP, VDD_ARM_CAP是否稳定晶振是否起振。这是基础中的基础。确认启动模式用万用表测量BOOT_MODE0/1引脚的电平确保与你的预期内部Boot一致。检查拨码开关或上下拉电阻。检查复位信号确保复位引脚在上电后能正常释放。确认调试串口确保你连接的UART引脚通常是UART1_TXD/UART1_RXD是正确的并且串口工具配置波特率115200 8N1无误。ROM Code在初始化后会从UART1打印出一些信息如果连“CCC...”这样的乱码都没有说明ROM Code可能都没开始执行。存储介质接触如果是SD卡重新插拔或更换一张已知好的卡。检查SD卡座的检测引脚。5.2 串口有输出但卡在特定阶段这时串口是你的“黑匣子”信息无比珍贵。卡在“U-Boot SPL”之后无输出问题定位ROM Code成功SPL被加载并运行了但SPL在加载U-Boot或初始化硬件时失败。排查重点DDR初始化失败这是最常见原因。SPL会尝试重新初始化或校准DDR。检查SPL源码中对应你板子的DDR初始化代码确认DDR类型DDR3/LPDDR2、容量、时序参数是否与板上芯片完全匹配。可以尝试在U-Boot配置中降低DDR频率测试。SPL镜像损坏或位置错误确认SPL文件是否正确烧写到了存储介质的指定偏移地址。对于SD卡使用dd命令烧写时偏移量参数seek是否正确。U-Boot镜像损坏或加载地址错误SPL找不到或加载u-boot.img失败。检查U-Boot的编译配置中定义的加载地址CONFIG_SYS_TEXT_BASE是否与SPL中期望的地址一致。卡在“U-Boot”命令行之前问题定位SPL成功U-Boot被加载到DDR并开始运行但在初始化过程中如DRAM检测、外设初始化出错。排查重点串口输出信息仔细阅读U-Boot启动时打印的信息看停在哪一句。例如如果停在“DRAM:”之后说明DDR检测失败如果停在某个外设初始化可能是该外设的引脚复用或驱动有问题。环境变量有时是环境变量损坏导致bootcmd无法执行。在U-Boot命令行下如果还能进的话执行env default -a恢复默认环境然后saveenv。设备树如果U-Boot使用了设备树CONFIG_OF_CONTROL确认编译出的设备树二进制文件.dtb是否正确是否与当前硬件匹配。卡在“Starting kernel ...”问题定位U-Boot成功已将内核镜像和设备树加载到内存并跳转到内核入口点但内核启动失败。排查重点内核镜像问题内核镜像zImage可能损坏或不匹配比如ARM架构错误。尝试更换一个已知可用的内核。设备树问题这是高发区。设备树中描述的内存地址、大小与实际情况不符或者描述了不存在的硬件导致内核驱动探测卡死。检查U-Boot传递给内核的设备树地址是否正确并尝试用一个最简化的、只包含CPU和内存节点的设备树来测试。启动参数检查U-Boot的bootargs环境变量特别是root参数指定的根文件系统位置是否正确对应的设备如/dev/mmcblk0p2是否存在。5.3 利用JTAG进行深度调试当串口信息不足以定位问题时JTAG是终极武器。通过JTAG调试器如J-Link你可以在代码任意位置设置断点单步跟踪ROM Code、SPL的执行。连接JTAG正确连接TCK、TMS、TDI、TDO以及复位信号线。配置调试工具使用OpenOCD或Segger Ozone等工具加载i.MX6ULL的配置文件。中断在ROM起始处上电后让JTAG立即中断CPU此时PC指针应该位于ROM Code的起始地址0x0000_0000附近。如果不能中断检查JTAG连接和芯片是否处于安全模式某些eFuse设置会禁用JTAG。单步跟踪DCD执行你可以一步步跟踪ROM Code如何解析IVT如何执行DCD命令。观察当执行到配置DDR控制器的寄存器时寄存器的值是否被正确写入这能最直接地验证你的DCD配置是否被正确执行。查看内存在SPL或U-Boot应该被加载的地址OCRAM或DDR地址查看内容确认镜像是否被正确加载。排查技巧准备一个“已知好的”参照物极其重要。如果你有一块官方的或稳定工作的开发板将其启动时的完整串口日志保存下来。当你自己的板子出问题时逐行对比两份日志差异点往往就是问题所在。特别是关注DDR初始化相关的日志不同容量的DDR初始化日志中的“size”字段会不同。