简介:芯片从上电复位到操作系统启动的完整boot过程,以Word文档形式整理为一份底层原理笔记,面向嵌入式开发、Linux驱动调试及芯片底层原理学习者,帮助理解多级引导程序的分工与协作。文档围绕ROM Code、SPL、U-Boot、Kernel四个阶段展开,详细说明ROM Code如何完成CPU与系统初始化、SPL如何将Bootloader分步搬运至SRAM与DDR、U-Boot如何加载内核并提供UART/USB/SD/EMMC等多种启动模式,以及Kernel最终接管系统管理的过程。资源为1个docx文档,包体大小1.41MB,纯文字笔记形式便于阅读与检索,目前已有921人学习下载。内容结合实际SD卡启动场景展开,涵盖启动模式判断、搬运地址、运行阶段划分等底层细节,并涉及UART/USB下载模式、SD/EMMC启动模式及ptset测试模式,可作为芯片启动流程梳理、嵌入式面试准备或驱动开发参考笔记,适合需要系统性理解完整引导链路的开发者。
1. 芯片启动Boot流程:从上电到Kernel的完整链路
芯片的boot过程,说白了就是一段芯片启动接力:ROM Code、SPL、U-Boot、Kernel。芯片启动时最容易遇到的问题是上电后没任何反应,连打印都没有,这时候如果你不理解这条链路的每一级在哪切换,就只能对着原理图发愣。这篇笔记把芯片从上电复位到最终运行系统代码的完整过程拆开讲,覆盖SD卡、eMMC启动和UART/USB下载模式,也讲清楚为什么SPL经常只搬前70KB就够用,以及ptset测试模式怎么绕开正常启动。适合做嵌入式驱动、系统移植和芯片验证的开发者先通读再上板。
2. ROM Code阶段:芯片上电后的第一段固化代码
2.1 Reset中断与handler模式:CPU第一跳为什么不是main
芯片上电复位后,CPU进入handler模式并触发reset中断,这是芯片架构层面的硬约束。CPU从复位向量取到第一条指令,入口就是ROM Code的代码地址。也就是说,CPU真正执行的第一段指令不是来自SD卡、Flash或DDR,而是芯片内部固化的ROM。这块ROM在芯片出厂时就写好了,用户改不了,所以它的代码风格通常非常保守,依赖的外部条件被压到最少。
ROM Code的入口代码先做CPU init,包含几个关键动作:设置CPU工作模式、关闭或配置中断控制器、配置PLL和时钟树、初始化片内SRAM访问属性。CPU init完成不代表系统已经可用,它只是把CPU和最小存储路径准备好。接下来,ROM Code会读取启动模式判定引脚或OTP标志,决定是进ptset测试模式,还是走正常启动流程。
我在实际调板时,习惯先确认ROM Code是否在跑,方法很简单:把芯片复位脚和UART_TX引脚同时挂示波器,上电后抓一段波形。如果ROM Code阶段已经初始化过UART,会在UART_TX上看到波特率对应的电平翻转。如果完全没有,优先怀疑CPU基础电源、复位时序和时钟源。ROM Code固化的代码虽然不会打印很丰富,但只要有串口输出,就说明这条链路基本是通的。
2.2 System Init:UART与键盘模块在早期被初始化的真实原因
文档里明确说,如果不进入ptset模式,ROM Code会开始进行system init操作。这里的system init不是把芯片所有外设都配一遍,它的目标很窄:只把后续判断启动介质、下载程序或输出状态信息所必需的最小外设集合配好。典型对象是UART和键盘控制器。
键盘控制器在这个场合更多是扫描一组GPIO状态,用于检测启动选择或状态配置,不是接外接键盘。它和UART一样,属于启动阶段的基础模块。UART是boot过程中最重要的调试通道,ROM Code往往在早期就把UART的波特率、引脚复用和数据帧格式配好,这样SPL和U-Boot阶段才能直接复用打印日志。我拿到新板子做的第一件事就是把串口终端接上,波特率从115200开始试,多数平台第一版固件用的是这个值。
有个容易忽略的点:system init阶段不会去初始化DDR。原因很简单,DDR初始化和电源稳定、时钟稳定强相关,而这一阶段要保持最小依赖,只做启动介质判断和镜像搬运准备。有的开发者尝试把DDR初始化提前到这个阶段来,这完全没有必要,ROM Code的职责就是尽量少做事,把复杂工作交给后面的SPL。
验证system init是否正常,可以这样操作:先确认UART引脚复用状态,再看波特率寄存器写入的实际分频值,最后观察启动判别引脚在复位释放后的保持时间。如果ROM Code阶段打印了特定字符串,说明system init里UART初始化已经完成。如果打印出来是乱码,先查波特率配置,不要急着怀疑ROM Code跑飞。
表:ROM Code阶段关键检查点
| 检查项 | 关键参数 | 判断方法 |
|---|---|---|
| CPU init状态 | 复位时序、PLL锁定时间 | 示波器抓复位脚与UART_TX波形 |
| UART配置 | 波特率、引脚复用、8N1格式 | 串口终端能否收到ROM Code打印 |
| ptset判定 | 判定引脚电平组合、OTP标志 | 对照芯片手册的ptset进入条件 |
| 介质判定 | SD/eMMC优先级、UART/USB使能 | 断开不同介质观察ROM Code行为变化 |
| SRAM访问属性 | AON SRAM起始地址与可用容量 | 从链接脚本和map文件反查 |
2.3 介质判断前的准备:ptset测试模式与普通启动的分岔
ptset测试模式是文档单独列出来的一种运行状态。ROM Code完成CPU基础初始化后,会先检查ptset条件,判断依据一般是芯片特定引脚的电平组合或OTP区间的标志位。如果条件成立,ROM Code直接跳进专用测试逻辑,不执行常规system init,也不做正常的介质搬运。这个模式面向量产测试和硬件验证,软件开发阶段基本用不到。
如果ptset不成立,ROM Code走正常分支,执行system init之后开始判断boot方式。文档列出了SD卡、eMMC、UART下载、USB下载四条通道。芯片一般通过配置引脚或OTP里的启动顺序决定优先级。以SD卡启动为例,ROM Code会先初始化SD控制器,再在卡里查找合法镜像。合法镜像通常必须在特定偏移处有魔数或头部结构,不是随便放一个bin文件就能被识别。
我在项目中见过一种误判情况:开发者的启动判别引脚同时配置成SD优先,但卡槽里实际插的是一张eMMC转SD的转接卡。ROM Code按SD流程初始化后发现卡响应异常,一直卡在重试逻辑里,看起来就像死机。这不是芯片bug,而是介质优先级和实际硬件不匹配。建议拿到新板子后,先把boot模式对应的引脚表格和优先级关系摘出来,对照实际接线画一张状态表,再开始调试。
注意:查ptset和介质判定问题时,先把原理图上相关引脚的上下拉电阻检查一遍,引脚悬空导致的启动不稳定案例比想象中多。
3. SD卡启动与SPL搬运:AON SRAM容量和70KB分段策略
3.1 启动介质是怎么判定的:SD、eMMC、UART/USB的优先级关系
ROM Code完成system init之后,要决定这次从哪里取程序。文档里的SD卡、eMMC、UART下载、USB下载四条通道,对应不同使用场景。SD卡和eMMC属于量产启动通道,UART和USB更多用于开发调试和产线烧录。
介质判定过程可以拆成三步:第一步看配置位选的是哪个介质,第二步看介质控制器初始化是否成功,第三步看介质头部镜像描述是否合法。三步里任何一步不满足,ROM Code都会回到重试逻辑。实际表现就是同一个镜像在A卡上能启动,换到B卡完全无反应。原因常常不是容量或速度等级差异,而是B卡的启动分区布局和A卡不一样,ROM Code在预设偏移处找不到头部魔数。
开发调试阶段,UART/USB下载模式比SD卡更常用。进入这两种模式后,ROM Code等待上位机发送镜像数据,然后把数据按协议写入SRAM,写完再跳转执行。下载模式的握手阶段通常有固定的命令序列,比如先发同步字符、再发长度和校验值。有的平台下载前还需要先擦除目标区域,有的则直接覆盖写。这些细节在不同芯片间差异很大,不要假设所有平台都一样。
3.2 70KB分段加载为什么存在:AON SRAM容量与DDR初始化顺序
文档中的70KB是一个非常关键的数字。ROM Code通过SD控制器读取SD卡内容到AON RAM运行,但AON RAM是片内常供电SRAM,容量很小,一般只有几十KB到一两百KB。SD卡上的bootloader镜像只要超过这个容量,就没法一次性载入。
所以设计上采用分段策略:先搬运SD卡前70KB内容到AON SRAM,让这段程序先跑起来。这段程序的任务很明确,一是初始化DDR控制器,二是等DDR可用后,再把SD卡剩余内容搬运到DDR里执行。注意这里的"剩余内容"在文档里被称作kernel,实际包含U-Boot镜像和内核镜像,统称kernel是因为内核在整体镜像中占大头。
在使用这个策略时,我一般会把SPL镜像的链接脚本单独做一遍检查。SPL在SRAM中的加载地址和运行地址,必须严格落在AON SRAM有效范围内。如果SPL镜像的text段或数据段跨越了SRAM边界,链接过程大概率能过,但运行时会出现总线错误。还有一类错误是DMA搬运时目标地址没按对齐要求设置,比如SD读卡按512字节块读,目标地址却是非对齐的,搬运结果就会错位。
3.3 SPL的加载参数:SD控制器、起始块、镜像大小怎么定
SPL阶段涉及的关键参数集中在SD控制器和搬运配置里:
- 起始块地址:指向SD卡上bootloader镜像所在的首块,一般不是0;
- 块数量:对应前70KB镜像所需块数,按512字节块向上取整;
- 目标地址:AON SRAM内预留给SPL运行的内存基址;
- 总线模式:SD 1-bit还是4-bit,直接决定搬运速度和引脚占用;
- 镜像长度字段:SPL头部里记录的镜像大小,用于校验实际搬运长度。
我在调试SPL加载时遇到过一种很隐蔽的情况:前70KB能正常跑,但把剩余镜像搬到DDR后一执行就异常。定位了很长时间,最后发现是DDR控制器初始化时,把CAS延迟和tRCD参数配错了。这个案例说明一个道理:SPL阶段最核心的代码不是搬运本身,而是搬运之前对DDR控制器的初始化配置。DMA搬运只是把数据从一个地方挪到另一个地方,如果DDR参数不对,搬运是否成功没有意义。
还有一点需要注意:如果镜像头部带校验,SPL搬运完成后会做一次头部解析和CRC校验。如果工具生成的镜像长度字段与实际大小对不上,SPL会在校验环节直接停下,表现就是打印停在"loading"之类的提示后不再继续。遇到这种问题,不要先怀疑代码逻辑,先检查生成镜像时有没有人为修改过头部长度字段。
表:SD卡SPL阶段常用参数参考
| 参数 | 典型值 | 作用 |
|---|---|---|
| SD起始块地址 | 按镜像偏移计算 | 定位bootloader起始位置 |
| 单次读取块数 | 按70KB/512B向上取整 | 控制单次DMA传输长度 |
| SPL在AON SRAM运行地址 | 低于SRAM容量上限 | 决定链接脚本的加载地址 |
| DDR目标地址 | 由U-Boot链接地址决定 | 剩余镜像的最终运行位置 |
| 总线模式 | SD 1-bit或4-bit | 影响搬运速度和引脚占用 |
4. 从SPL到Kernel:DDR初始化、U-Boot接力与内存布局
4.1 SPL的运行边界:它只负责DDR初始化和搬运
SPL全称Secondary Program Loader,是ROM Code和完整Bootloader之间的桥梁。ROM Code本身不理解文件系统、分区布局,它只是把前70KB读进SRAM,然后交给SPL。SPL带着这段bootloader内容在SRAM里运行,核心任务就两件:初始化DDR、把剩余镜像从SD卡搬运到DDR。
这两件事做完,SPL的使命就结束了。在代码组织上,我会把SPL的入口函数写得很短,主流程就是时钟、DDR控制器、串口打印、SD读卡、跳转。不要试图在SPL里堆业务逻辑,AON SRAM空间有限,SPL阶段的中断处理也弱,一旦出现异常几乎没有调试信息。开发时建议在SPL入口和跳转前各放一个打印点,如果入口有打印而跳转前没有,说明卡在DDR初始化或搬运环节。
SPL跳转到U-Boot的方式因平台而异,有的是直接跳绝对地址,有的是通过函数指针跳转。跳转前要确保栈指针指向一个有效区域,并且关闭中断。有的平台要求SPL在跳转前把D-Cache清干净,否则U-Boot在DDR里读到的是cache里残留的旧数据,这种问题表现出来就是随机性死机,非常难受。
4.2 U-Boot阶段的参数:bootargs、loadaddr和常见启动命令
镜像搬进DDR后,U-Boot开始执行。U-Boot是一个功能完整的引导程序,它可以从DDR里已经放好的镜像继续加载,也可以重新从SD或eMMC读取内核和设备树。U-Boot阶段的启动参数集中在环境变量里,我调试时一般先执行几条命令确认状态:
# 查看当前环境变量 printenv bootargs bootcmd loadaddr fdt_addr # 从MMC设备0分区0读取内核镜像到内存 mmc dev 0 mmc read ${loadaddr} 0x800 0x4000 # 启动内核,-表示跳过ramdisk bootm ${loadaddr} - ${fdt_addr}这里loadaddr常见值是0x40008000一类地址,fdt_addr指向设备树在DDR中的位置。如果镜像不是U-Boot格式的uImage,bootm会报错,这时改用booti命令加载原始内核镜像。还有一个细节:mmc read的起始块和块数,必须和SD卡上实际写入的位置一致。之前SPL阶段搬运剩余镜像时,目标地址要和这里U-Boot读取的地址错开,不能重叠。
U-Boot阶段最容易踩的坑有三个:一是bootargs里root=参数和根文件系统分区对不上;二是设备树地址和内核加载地址重叠;三是malloc区域配置过小导致环境变量加载失败。我一般会先把loadaddr、fdt_addr、bootargs三个变量打印出来,在纸上排一遍地址关系,再决定怎么调。
4.3 Kernel接管前的最后几步:在DDR中跳转和内存约束
U-Boot通过bootm或booti启动内核时,会做一次带参数跳转。跳转前要确保中断关闭、栈指针设置正确、设备树物理地址传给内核参数。在这个时刻,CPU已经运行在DDR里,之前SRAM中的SPL数据区和栈已经不重要。如果U-Boot在跳转前还访问SRAM里的全局变量,内核启动后那部分地址被新数据覆盖,就会出现随机死机。
实践上,我建议在跳转前做一份检查清单:设备树地址在DDR有效范围内;内核加载地址和U-Boot自身镜像不重叠;bootargs里mem=大小不超过实际DDR容量;内核DTS里的reg属性和当前DDR控制器配置一致。这些检查项每一条都可以写脚本验证,但最直接的方式还是先printenv把所有地址打印出来,人工确认一遍。U-Boot阶段还有安全启动的概念,如果芯片使能了安全启动链,SPL和U-Boot的镜像头部会带签名和证书,没有正确签名的镜像会在校验环节被拒绝。这个机制对开发阶段是个障碍,但对量产产品是必要的保护,做方案选型时要把这条链路提前考虑进去。
表:DDR启动阶段常用地址约束
| 项目 | 检查点 |
|---|---|
| 内核加载地址 | 必须与DDR起始地址加偏移匹配 |
| 设备树地址 | 与内核镜像不重叠,建议错开4MB以上 |
| U-Boot自身位置 | 不能被内核镜像覆盖 |
| bootargs内存大小 | 与DDR控制器实际配置一致 |
| 栈和堆区域 | 跳转前由U-Boot接管,确认无越界 |
5. 避坑:Boot流程中最常踩的五个现场问题
5.1 现象:上电完全无打印
这是最让人头疼的一种情况。串口终端什么都收不到,示波器抓UART引脚也看不到电平翻转。原因可能出在几个方面:芯片根本没有跑起来,复位引脚一直被拉低;电源时序不满足要求;ROM Code的介质判定卡在死循环里等待某个引脚;UART引脚复用错了方向,打印发到了别的引脚上。
解决思路是逐层确认。先查复位脚波形是否符合手册要求的上电时序,再确认晶振或时钟源是否起振,然后把示波器挂在UART_TX引脚看有没有电平翻转。如果确实没有任何打印,优先检查boot模式选择引脚。有些芯片在这些引脚悬空时会持续等待介质,表现就和无启动完全一样。检查原理图时,要把相关引脚上的上下拉电阻画清楚,特别是状态保持到ROM Code采样完成之前,否则会出现上电能启动、下一次又不能的间歇性故障。
5.2 现象:SD卡启动卡在SPL阶段,串口无输出或极短乱码
SD卡上的镜像格式不对,或者镜像头部校验失败时,ROM Code可能根本不会进入SPL代码,表现就是完全没有输出。输出极短乱码多半是UART波特率不匹配,ROM Code配的和SPL配的不一致,两段代码各自用了不同的打印配置。
调试办法是先确认ROM Code打印能正常收到,再确认SPL入口打印是否出现。如果ROM Code有打印但SPL没有,用读卡器把SD卡镜像倒出来,检查前70KB位置的头部魔数和校验字段。另一个常见原因是用烧录工具写卡时,起始块地址被工具默认设置偏移了,镜像没有写在ROM Code预期的位置。
5.3 现象:SPL正常但跳到DDR后死机
SPL在SRAM里跑得好好的,搬运完成跳出后一进DDR就死。这种问题最早我在项目里排查了很久,原因是DDR初始化参数与实际硬件不匹配,尤其是内存频率、时序参数或驱动强度配置不对。也有可能是SPL跳转时没有切换栈指针,或者跳转地址和链接脚本不一致。
解决方式分两步:先把DDR控制器的初始化参数和内存颗粒数据手册逐项核对,重点检查CAS延迟、tRCD、tRP这些时序参数。然后写一个简单的DDR读写测试函数,在SPL跳转前对目标地址做写读比较。如果读写测试都不出错,再回头查跳转地址。这类问题优先用内存读写脚本定位,不要一开始就怀疑跳转代码写错。
5.4 现象:UART/USB下载模式能握手但下载完起不来
下载模式是开发期最常用的调试手段,但有时握手正常、传输也显示成功,跳转后却起不来。原因通常在于下载工具传给ROM Code的镜像长度、校验值和实际文件不一致,或者下载目标地址和运行地址不一致。还有一种情况是协议里带校验头,长度字段写错,ROM Code把有效镜像截断了。
解决办法是核对下载工具输出日志里的下载长度、起始地址和CRC,与本地生成镜像的工具链配置保持一致。如果用了加载地址重定向,要确保链接脚本里的链接地址和下载地址一致。我把镜像生成命令和下载命令写到一个脚本里,自动计算长度和CRC,不手工填值,从根上避免这类错误。
5.5 现象:eMMC与SD卡启动行为不一致
eMMC和SD在电气协议上相近,但细节差异很大。eMMC支持boot分区,SD没有这个概念。eMMC初始化通常需要CMD1获取OCR寄存器,总线宽度切换的方式也和SD不同。直接拿SD版本的SPL放到eMMC上,经常出现能识别容量但读取超时的问题。
解决方式是在SPL或ROM Code的eMMC分支里单独确认boot分区的使能状态和总线宽度配置。有的芯片需要在初始化阶段显式写boot bus width寄存器,没设置的话后续读取全部失败。另外,eMMC如果上一次写入的是非boot分区,ROM Code按boot分区查镜像查不到,表现同样为启动失败。重新烧写时要用烧录工具把镜像写到boot分区而不是普通用户分区,这一步经常被忽略。
注意:遇到启动问题先按介质、地址、参数三条线排查,不要先怀疑是芯片本身有问题。绝大多数boot失败是配置问题或镜像布局问题。
6. 用GPIO翻转和串口时间戳给Boot各阶段做性能画像
Boot过程不只是功能问题,很多产品对启动时间有硬指标。拿到新板子后,我习惯先给boot流程做一次性能画像,看每个阶段花了多少时间,再决定优化方向。
最简单的打点方案是:在ROM Code、SPL、U-Boot、Kernel每段的入口和完成点各翻转一次GPIO。选一个空闲GPIO做标志位,进入阶段时拉高,离开阶段时拉低。逻辑分析仪挂在这个GPIO上,上电后抓一段波形,每一段的持续时间一目了然。需要提醒的是,GPIO在这个阶段能不能用,取决于芯片是否允许ROM Code阶段操作GPIO控制器。有些芯片要在system init之后GPIO才可用,这种情况把第一个打点放在SPL入口更现实。
串口时间戳可以辅助精度要求不高的测量:在启动代码里用系统定时器打印当前计数,换算成毫秒。比如SPL入口打印t=10ms,U-Boot入口打印t=35ms,Kernel入口打印t=120ms,这样各阶段耗时清晰可见。我在几个平台测下来,最耗时的通常是DDR初始化和SD/eMMC的大块镜像搬运。优化方向一般是减少DDR初始化时的稳定等待时间,以及调整总线速度和DMA burst大小。
从那以后,我每次拿到新芯片或者改过启动配置,都会强制自己先做一遍GPIO打点,把各阶段时长记录存档。后续改任何启动代码,都能立刻看出哪一段变慢了、哪一段时序变了,不用等整机功能异常才回头排查。串口日志加GPIO波形,这两个工具配合起来,比仿真器打断点更接近启动过程的真实状态。希望帮到你。
本文还有配套的精品资源,点击获取