君正Zeratul平台U-Boot启动全解析:从SPL到内核加载的实战指南
1. 从一块“砖头”到跑起来的系统为什么我们要深挖uboot启动拿到一块君正Zeratul开发板很多人第一反应是赶紧跑个Demo看看摄像头效果或者跑个AI模型。这没错但如果你只停留在应用层一旦系统启动不起来或者需要深度定制引导流程就会立刻抓瞎。这时候uboot就成了横在你面前的第一道也是最重要的一道坎。它就像火箭发射的倒计时和点火系统负责把一块冰冷的、上电后只会执行固化指令的“砖头”芯片一步步唤醒最终把控制权平稳地交给Linux内核。网上关于uboot的教程很多但大多集中在通用的ARM平台比如树莓派或者全志、瑞芯微的芯片。君正的平台尤其是其自研的T系列如T31、T32和X系列芯片在启动流程上有其独特的设计特别是引入了SPLSecondary Program Loader的概念。如果你用通用的uboot思维去套很可能会在编译、烧录、调试的各个环节碰壁。我最初接触君正平台时就曾因为对SPL和uboot proper的关系理解不清导致系统反复重启浪费了大量时间。所以这篇内容我们不谈空洞的理论就结合君正Zeratul通常基于T31/T32等芯片的实际代码和启动日志把uboot从芯片上电到跳出命令行或者自动引导内核的整个过程掰开揉碎了讲清楚。你会明白SPL到底干了什么uboot本体又承担了哪些职责以及那些关键的初始化动作是如何环环相扣的。无论你是要做快速启动优化、裁剪uboot体积还是调试启动失败的问题这个分析过程都是你必须要掌握的“内功”。2. 君正平台启动全景图SPL与U-Boot的分工与协作在标准的嵌入式Linux启动链中我们通常熟悉的是ROM Code - Bootloader如U-Boot - Linux Kernel。但在君正以及很多现代SoC平台上这个链条变得更精细了多了一个关键角色SPL。你可以把启动过程想象成一场接力赛第一棒 ROM Code这是芯片出厂时就固化在硅片里的代码无法修改。它非常“笨”只做最基础的事情初始化最必要的CPU核心和时钟然后从指定的外部存储设备如SPI NOR Flash、SD卡、NAND Flash的固定位置加载一小段代码到芯片内部SRAM中执行。这段被加载的代码就是SPL。第二棒 SPL (Secondary Program Loader)它的核心使命只有一个初始化DDR内存。为什么需要它因为芯片内部的SRAM很小可能只有几十KB到几百KB根本放不下完整的U-Boot通常几百KB甚至上MB。而初始化DDR是一个高度硬件相关、时序要求严格的操作必须在C语言环境或更复杂的代码下完成。SPL就是一段极其精简的“迷你U-Boot”它用SRAM作为运行空间完成DDR初始化后就可以把“大部队”——完整的U-Boot从外部存储加载到容量巨大的DDR内存中然后跳转过去。第三棒 U-Boot Proper (完整的U-Boot)这就是我们通常所说的uboot。它在宽敞的DDR中运行功能强大进一步初始化更复杂的硬件如网络、USB、显示接口、解析环境变量、加载设备树DTS、最终从Flash或网络加载Linux内核和根文件系统镜像并跳转到内核执行。对于君正ZeratulT31/T32平台这个流程是明确的。我们需要编译出两个独立的镜像spl/u-boot-spl.bin或u-boot-spl和u-boot.img。在烧录时它们需要被放置到存储设备的特定偏移地址。例如在SPI NOR Flash中SPL通常放在起始位置offset 0而U-Boot proper放在稍后的位置如offset 0x40000。注意很多启动失败问题根源就在于SPL和U-Boot的烧录位置不对或者SPL没有正确初始化DDR。查看串口日志时如果卡在U-Boot SPL ...之后没有任何输出大概率就是SPL阶段出了问题。3. 代码跟读从_start到board_init_f要理解启动流程光看框图不够必须跟着代码走一遍。我们以君正平台常见的代码结构为例。首先找到SPL的入口。在arch/mips/cpu/start.S君正芯片多为MIPS架构或arch/arm/cpu/armv7/start.S对于ARM核的君正芯片中存在整个U-Boot包括SPL的入口点_start。对于SPL编译时通常会定义CONFIG_SPL_BUILD宏因此代码会走SPL的初始化路径。/* arch/mips/cpu/start.S 示例 */ .globl _start _start: /* 1. 设置异常向量表 */ b reset ... reset: /* 2. 进入核心初始化设置CPU状态、关闭中断、初始化缓存 */ ... /* 3. 初始化栈指针为运行C代码做准备 */ la t0, _end move sp, t0 /* 4. 调用 lowlevel_init (板级早期初始化) */ jal lowlevel_init /* 5. 跳转到C语言入口board_init_f */ la t9, board_init_f jr t9lowlevel_init这个函数很关键它通常位于arch/mips/cpu/xxx/lowlevel_init.S或板级目录下用汇编实现负责在进入C环境前完成那些时序要求最苛刻的初始化比如设置内存控制器DDR的时序参数。这是SPL阶段最核心、最易出错的地方。君正通常会提供一个ddr_param.h或类似的配置文件里面定义了DDR类型、频率、时序参数tRCD, tRP, tRAS, tRFC等这些参数必须与板上使用的DDR颗粒严格匹配。board_init_f是第一个C函数。在SPL中它的主要任务可以概括为清空BSS段将未初始化的全局变量区域清零。初始化全局数据gd结构体这个结构体记录了U-Boot运行时的各种全局信息。执行一系列初始化函数这些函数通过INIT_FUNC_SPL宏被链接到一个特殊的段如.init_spl中按顺序执行。它们可能包括initf_dm: 初始化驱动模型Driver Model。initf_bootstage: 初始化启动阶段记录用于性能分析。board_early_init_f: 板级早期初始化如GPIO、时钟的进一步配置。spl_init: SPL框架自身的初始化。最关键的一步spl_board_init。这个函数是板级代码的核心对于君正平台它会调用ddr_init()函数。这个函数会使用之前在lowlevel_init中配置的参数真正“激活”DDR内存。如果这一步成功串口会输出类似DDR init success的日志。加载U-Boot ProperDDR可用后SPL会从存储设备如SPI Flash的CONFIG_SYS_UBOOT_BASE偏移处将u-boot.img读取到DDR的指定地址如CONFIG_SYS_TEXT_BASE。跳转最后通过jump_to_image_no_args(uboot_image_info)或类似的函数跳转到DDR中U-Boot Proper的入口点SPL的使命就此完成。4. U-Boot Proper的初始化舞台设备树与驱动模型当控制权从SPL交接到U-Boot Proper后又是一次从_start开始的旅程。但这次因为DDR已经可用舞台变得无比广阔。U-Boot Proper的board_init_f阶段注意同名但内容不同会更复杂。这一阶段的核心目标是为后续的board_init_r阶段建立一个完整的、可用的C运行环境并初始化那些不依赖于复杂外设和内存重定位的基础设施。一个关键变化是引入了设备树Device Tree Blob, DTB。在SPL阶段为了极简可能直接硬编码或使用非常简单的设备配置。而在U-Boot Proper中它会加载并解析DTB。DTB以结构化的方式描述了硬件信息CPU、内存地址空间、外设、总线、中断号等。U-Boot的驱动模型Driver Model会基于DTB来自动匹配和初始化设备驱动。board_init_f阶段的主要序列如下initf_dm(again)再次初始化驱动模型这次会基于DTB。initf_bootstage(again)。board_early_init_f板级早期初始化可能配置更多时钟、引脚复用Pinctrl。timer_init初始化系统定时器如OST为get_timer()等函数提供支持这是实现延时、超时判断的基础。initf_malloc初始化堆heap管理器为动态内存分配malloc做准备。initf_console/serial_init初始化控制台通常就是串口驱动。这样后续的打印信息才能输出。initf_bootdev初始化启动设备Boot Device确定是从哪里SPI Flash, SD, eMMC, USB加载内核和文件系统。board_late_init_f板级晚期初始化一些依赖于基础设备如串口的初始化放在这里。这个阶段结束后U-Boot会执行一次内存重定位。简单说U-Boot一开始是运行在它被加载到DDR的地址加载地址但它的链接地址运行时代码期望的地址可能是另一个。重定位就是把自己复制到链接地址并更新所有相关的地址指针。这之后才会进入board_init_r阶段。5.board_init_r与引导决策环境变量、bootcmd与内核加载board_init_r是U-Boot Proper初始化的大结局也是功能最丰富的阶段。它初始化所有剩下的子系统并最终执行引导命令。这个阶段通过init_sequence_r数组来组织一系列初始化函数包括但不限于initr_caches: 使能指令/数据缓存大幅提升运行速度。initr_malloc(finalize): 最终确定堆内存区域。initr_dm(finalize): 完成驱动模型的初始化所有在DTB中匹配到的设备驱动都会被探测probe和初始化。initr_env:初始化环境变量。这是非常关键的一步环境变量存储在Flash的特定区域如CONFIG_ENV_OFFSET包含了所有用户可配置的启动参数。最著名的就是bootcmd。initr_net(可选): 初始化网络设备如MAC、PHY如果你使用了网络引导TFTP或Ping命令。initr_mmc/initr_spi/initr_nand: 初始化具体的存储设备驱动如SD/MMC控制器、SPI Flash控制器、NAND控制器。board_late_init: 板级最后的初始化例如检查启动模式、设置MAC地址、根据按键设置环境变量等。当所有初始化完成后U-Boot会进入主循环。它会先执行bootdelay延迟通常为2秒或更短在延迟期间检查是否有按键如空格键被按下以中断自动启动。如果没有中断则执行bootcmd环境变量中定义的命令序列。一个典型的君正平台bootcmd可能是这样的setenv bootargs consolettyS1,115200n8 mem64M0x0 rmem64M0x4000000 root/dev/mtdblock2 rootfstypesquashfs ro init/linuxrc mtdpartsjz_sfc:512k(boot),2M(kernel),13M(rootfs),1M(config) sf probe 0 sf read 0x80600000 0x80000 0x200000 bootm 0x80600000让我们拆解这个命令setenv bootargs ...: 设置要传递给Linux内核的命令行参数。这里指定了控制台设备、内存布局、根文件系统位置和类型、MTD分区表等。这些参数必须与你的硬件和系统设计完全一致否则内核无法启动或找不到根文件系统。sf probe 0: 探测并初始化SPI Flash设备0。sf read 0x80600000 0x80000 0x200000: 从SPI Flash的偏移0x80000即512KB处读取0x2000002MB大小的数据到DDR地址0x80600000。这个数据就是Linux内核镜像可能包含压缩的uImage和附加的DTB。bootm 0x80600000: 启动位于0x80600000的镜像。bootm命令会解压内核如果需要、在内存中准备DTB、设置启动参数bootargs最后跳转到内核入口点。至此U-Boot的所有工作完成Linux内核开始执行系统启动进入下一个阶段。6. 实战中的调试技巧与常见问题排查理解了流程我们来看看实际操作中如何调试和解决问题。串口控制台是你的眼睛一定要用好。6.1 获取并解读启动日志连接串口上电你会看到类似如下的信息流U-Boot SPL 2020.10 (May 01 2024 - 15:30:00 0800) Trying to boot from SPI DDR init success ... U-Boot 2020.10 (May 01 2024 - 15:30:00 0800) (编译时间) CPU: Ingenic Xburst T31 1.2 GHz Model: Zeratul Development Board DRAM: 64 MiB MMC: ... In: serial Out: serial Err: serial Net: No ethernet found. Hit any key to stop autoboot: 2卡在U-Boot SPL后无输出SPL失败。检查DDR参数是否正确SPL编译配置make menuconfig或defconfig是否正确SPL烧录位置是否正确卡在DRAM:后无输出或显示错误容量U-Boot Proper的DDR初始化或检测失败。虽然SPL初始化了DDR但U-Boot Proper可能再次检测或配置。检查U-Boot中的内存配置参数CONFIG_SYS_SDRAM_BASE,CONFIG_SYS_SDRAM_SIZE。Net: No ethernet found这不一定是个错误只是说明网络驱动未初始化或未找到PHY。如果你的板子有网络且需要检查设备树中以太网节点的配置和PHY的复位引脚、MDIO总线配置。6.2 使用U-Boot命令进行手动引导如果在自动引导bootcmd时失败可以在倒计时时按任意键进入U-Boot命令行。这里是你强大的调试工具站printenv打印所有环境变量检查bootargs、bootcmd是否正确。setenv/saveenv修改并保存环境变量。例如可以临时修改bootcmd来尝试从不同位置加载内核。md/mw内存显示/写入。可以用来检查从Flash读取的数据是否正确。sf probe/sf read手动操作SPI Flash验证读取功能。mmc list/mmc read操作MMC/SD卡。bootm手动启动内核。你可以先通过sf read将内核镜像读到内存然后用bootm [addr]启动这样可以绕过bootcmd直接测试内核镜像本身是否正确。fdt操作设备树。可以用fdt addr [addr]设置DTB地址然后用fdt print查看节点检查硬件描述是否正确。6.3 常见问题与解决思路问题现象可能原因排查思路上电后完全无串口输出1. 串口线/波特率错误2. SPL未运行3. 芯片未正常复位或供电1. 确认TX/RX交叉连接波特率通常为115200。2. 用示波器或逻辑分析仪测量SPI Flash的CS和CLK引脚看SPL阶段是否有读取动作。3. 检查电源、复位电路、晶振。串口输出乱码1. 波特率不匹配2. 时钟源配置错误1. 尝试常见波特率115200, 57600, 9600等。2. 检查U-Boot中串口时钟源配置是否与硬件设计外部晶振频率一致。卡在Starting kernel ...1. 内核镜像损坏或格式不对2.bootargs参数错误3. 设备树DTB错误或地址不对4. 内存地址冲突1. 用bootm手动指定地址启动或用工具检查uImage头。2. 仔细核对bootargs特别是console,root,mem参数。3. 确认传递给内核的DTB是正确的且未被内核覆盖。有时需要将DTB放在内核镜像之后。4. 确保内核加载地址如0x80600000不会覆盖U-Boot、DTB或其它正在使用的内存区域。内核启动后卡住或panic1. 内核驱动与设备树不匹配2. 根文件系统挂载失败1. 查看内核panic信息通常指向某个驱动初始化失败。对比内核配置与设备树中的节点。2. 检查bootargs中的root参数确认设备节点如/dev/mtdblock2存在且包含有效的文件系统。7. 进阶为Zeratul优化启动速度对于像智能摄像头这类产品快速启动是刚需。君正T31/T32本身支持快速启动特性在uboot层面我们可以做很多优化SPL阶段优化精简代码检查make menuconfig中SPL的配置关闭所有非必要的功能如文件系统支持、命令集。优化DDR初始化使用君正提供的ddr_param工具根据板子实际使用的DDR颗粒生成最优化的时序参数替换掉默认的保守参数可以节省几十毫秒。关闭串口输出在最终产品中如果不需要串口调试可以在SPL编译时关闭CONFIG_SPL_SERIAL_SUPPORT减少初始化时间。U-Boot Proper阶段优化裁剪功能产品中可能不需要网络、USB、HDMI等功能在配置中全部关闭。使用size命令对比裁剪前后u-boot.bin的大小。减少延迟将bootdelay设置为0实现上电即启动。预置环境变量将固定的bootargs和bootcmd编译进U-Boot镜像使用CONFIG_EXTRA_ENV_SETTINGS避免从Flash读取环境变量区的时间开销。内核压缩与加载优化使用lz4等更快的压缩算法替代gzip。如果内核较小甚至可以尝试不压缩。确保内核加载地址是对齐的并且使用sf read的块模式进行快速读取。利用硬件特性休眠唤醒对于电池类产品研究芯片的休眠Suspend to RAM和快速唤醒流程这比冷启动快得多。XIP (Execute In Place)对于SPI NOR Flash可以研究是否能让内核的初始化代码直接在Flash中执行减少加载到DDR的时间。但这需要芯片和内核的支持实现较复杂。优化是一个权衡的过程需要在启动速度、系统功能、稳定性之间找到平衡点。最好的方法是使用高精度计时器或GPIO翻转配合示波器实际测量每个阶段的耗时找到瓶颈所在进行针对性优化。整个uboot的启动分析就像在解构一个精密的机械钟表每一个齿轮函数都有其明确的作用和咬合顺序。当你对这套流程了然于胸无论是移植到新板子、调试启动故障还是进行深度定制和优化都会变得有章可循游刃有余。下次你的Zeratul板子启动失败时别急着重新烧录打开串口跟着日志和代码一步步走下去你一定能找到问题的钥匙。