嵌入式启动介质与启动架构
从 BootROM → XIP 直跑 → bootloader / loader → 操作系统这条链出发,讲清四类启动介质该怎么排 —— 一份跨 MCU 与 Cortex-A 两类平台的通用基础篇 | 编制日期 2026-10-14 | 版本号 Rev 1.0
报告总结
嵌入式项目里,「启动介质选哪一颗」这个问题几乎总是被简化成一道容量题:代码装不下就换大一档的器件。但真正决定方案的从来不是容量,而是四件更靠前的事——启动时刻的行为是否确定、介质本身会不会失效率、成本落在哪一段、出了问题能不能维护。
这份文档要做三件事:
- 把启动链拆成可对齐口径的四段:复位与取指、ROM 初始化、介质初始化、加载与移交。每一段谁做、做什么、占多少时间,都给可套用的模板。
- 把四类介质放进同一套语言里对齐:片内 BootROM、SPI NOR、SPI NAND / SD NAND、eMMC / UFS,分别讲清它们「启动友好」在哪里、代价在哪里。
- 给一个能落到具体场景的排序框架:不用容量排序,用启动确定性、失效率、成本、可维护性四维排序,并给出五类典型产品的推荐级别总表与自查清单。
覆盖 MCU 与 Cortex-A 两类平台:前者关心代码容量与极简确定性,后者关心容量、分区与 OTA 原子性。两者共用同一套分层模型,只是每一层的权重不同。
结论:
启动介质不是按容量选的,是按启动确定性 + 失效率 + 成本 + 可维护性四个维度排的。
容量只决定「装不装得下」,而这四个维度决定「这设备能不能上线」——
SPI NOR 在小容量高确定性场景仍是首选,NAND 类介质必须配套一块
NOR 才敢用来做引导,eMMC / UFS 是大容量与 OTA 原子性的必选项而非可选项。
先把「谁做第一段引导代码」这件事定下来,再谈容量与价格。
读者路径
- 要做选型决策:直接看第 8 章(四个维度 + 场景排序 + 自查 8 问)与附录 B(四介质对照总表)。
- 要理解启动链本身:按 1 → 2 → 3 章顺序读,这是从复位到操作系统接管的完整因果链。
- 做 Cortex-A / Linux 平台:第 4、6、7 章 + 第 9 章(bootloader 与 loader 切分、安全启动信任链)。
- 做 MCU / 裸机或 RTOS 平台:第 2、3、5 章 + 第 10 章(启动加速),重点看 XIP 与省时手段。
- 现场排启动故障:第 11 章(三段定位法 + 12 条以上失败清单 + 案例复盘),可当手册翻。
- 要落地时间预算与验证用例:附录 C 两个模板直接套用。
1. 启动链的四个阶段
启动不是一件事,而是一条被切成四段的链。把这四段的口径统一之后,后面所有介质之间的比较才有意义——否则「启动只要 80 ms」和「启动要 2 s」很可能说的是完全不同的两段。
上图是全文的骨架,后面每一章都在细化其中某一段或某一类介质:第 2、3 章讲第一段与第二段的做法(BootROM 与 XIP / 拷贝),第 4~7 章按四类介质分别展开,第 8 章把它们放进同一套排序框架,第 9~11 章处理移交、加速与排障。
1.1 从复位到第一行指令:复位向量与初始栈指针
设备上电或复位释放之后,处理器做的第一件事不是「运行你的代码」,而是从固定位置取一条指令。这个固定位置就是复位向量,通常指向一段极短的入口代码。紧跟着它的还有一件事:装载初始栈指针,让后续的函数调用有地方可退。
这两步是整条链上唯一「不可协商」的部分,它们决定了三件事:
- 入口位置的确定性。介质上的内容可以被换、被改、被擦坏,但复位向量的位置不能变——所以启动镜像必须放在一个固定偏移处,而不是「随便找个空位」。
- 栈的有效性。栈指针必须落在真实可写的 RAM 里。BootROM 自带的 RAM 通常很小,很多深层调用链必须靠自己尽早把栈搬走。
- 失败的可观测性。如果连这一步都过不去,后面所有日志手段都是空的——这也是「无输出」这类故障最难定位的原因。
| 动作 | 由谁完成 | 关键前提 | 失败时的现象 |
|---|---|---|---|
| 复位向量取指 | 处理器硬件 + BootROM 入口 | 向量表放在固定偏移且内容有效 | 完全无输出,连串口都没有 |
| 初始栈指针装载 | BootROM 入口代码 | 栈指针指向真实可写 RAM | 进入入口后立刻跑飞,行为随机 |
| 最小外设初始化 | BootROM | 时钟源稳定,供电已稳住 | 串口有乱码或时序完全不对 |
| 介质建立读通路 | BootROM 或外部引导代码 | 接口引脚复用正确、时序参数合理 | 识别不到介质,或读到全 FF / 全 00 |
| 载入并移交 | 外部引导代码 | 镜像头可解析、校验通过 | 反复回到入口,或停在某一级 |
1.2 阶段划分:ROM 初始化 → 介质初始化 → 加载 → 移交
把启动链切成四段的理由不是「好看」,而是这四段的责任方不同、可改动程度不同、时间量级不同。分清责任方,才能在出问题时知道该找谁;分清可改动程度,才能判断哪些环节值得投入优化。
| 阶段 | 典型责任方 | 做什么 | 产物 | 可改动程度 |
|---|---|---|---|---|
| 第一段 · ROM 初始化 | 片内 BootROM(固化) | 取向量与栈指针、最小时钟与内存初始化、串口可用 | 一个可跳出的最小运行环境 | 几乎不可改,只能换料号 |
| 第二段 · 介质初始化 | BootROM 或一级引导代码 | 建立与启动介质的通路、识别器件、准备读出通道 | 可按地址随机读取介质内容 | 取决于这段代码固化在哪一级 |
| 第三段 · 加载 | 一级 / 二级引导代码 | 解析镜像头、定位镜像、搬运或直接执行、逐段校验 | 可运行的下一级镜像 | 可改,通过重烧或 OTA 更新 |
| 第四段 · 移交 | 最后一级引导代码 | 选镜像、判版本、做回退、把控制权交给操作系统或应用 | 操作系统 / 应用接管 | 可改,且常有 A/B 或多镜像机制 |
四段之间有两条经常被忽略的隐性契约:一是交接格式——上一段交给下一段的是什么(一段内存镜像,还是「一个可跳的地址」),这段约定决定了后面能不能灵活替换实现;二是失败语义——下一段失败时,上一段是重试、跳过、还是回退。没有这两条,启动链一旦在中间出错就只能从头重来,现场定位成本极高。
1.3 各阶段的时间预算拆解
做启动优化之前,先把时间拆到段。下面的预算表是通用骨架,具体数值必须实测填入——本文给出的是「哪一段大概占多少」的量级判断,用来决定优化投在哪一段最划算。
| 阶段 | 典型量级 | 主要耗时来源 | 优化手段 | 优化天花板 |
|---|---|---|---|---|
| ROM 初始化 | 百微秒 ~ 数毫秒量级 | 时钟稳定等待、串口初始化 | 提前启动时钟、去掉无关外设初始化 | 低:固化代码基本改不动 |
| 介质初始化 | 毫秒 ~ 数十毫秒量级 | 接口时序、器件识别、状态复位 | 跳过完整识别、只做最小读通路 | 中:取决于这段代码在哪一级 |
| 加载 | 数十毫秒 ~ 数百毫秒量级 | 搬运数据量、校验计算、等待就绪 | XIP 直跑、并行校验、按需加载 | 高:这是最大的一块可动空间 |
| 移交 | 毫秒量级 | 镜像选择、版本判定、跳板 | 减少镜像个数、简化判定逻辑 | 低:本身占比很小 |
把预算表当成立项工具而不是验收工具:立项时按上表的量级分配各段预算并写成验收指标,量产后用实测值回填。若某一段实测远超预算,优先检查是不是把「调试打印」和「多余的外设初始化」算进了正式固件——启动时间超标最常见的原因不是代码慢,而是不该在这段做的代码被做了。
1.4 启动时间的定义边界与测量口径
「启动时间」至少有四种不同的口径,不同口径之间可以差一个数量级。对外沟通前必须先声明用的是哪一种,否则很容易被拿去做错误的对比。
| 口径 | 起点 | 终点 | 典型量级 | 适用场景 |
|---|---|---|---|---|
| 口径 A · 到首条有效指令 | 复位释放 | 第一条有效指令执行 | 百微秒 ~ 毫秒量级 | MCU 实时性分析、向量表正确性验证 |
| 口径 B · 到引导代码入口 | 复位释放 | 一级引导代码开始运行 | 毫秒 ~ 数十毫秒量级 | 评估 ROM 与介质初始化是否达标 |
| 口径 C · 到系统可用 | 上电或唤醒 | 操作系统或应用可以响应外部输入 | 数百毫秒 ~ 数秒量级 | 产品规格书里写的「启动时间」多半指这个 |
| 口径 D · 到首帧画面 / 首包数据 | 上电或唤醒 | 界面可见 / 首个业务数据发出 | 秒级量级 | 带屏与带网口的整机体验指标 |
| 必须写清的条件 | 两种取值 | 对结果的影响 |
|---|---|---|
| 冷启动还是休眠唤醒 | 冷启动 / 热启动 | 休眠唤醒通常可跳过大部分初始化,可能快一个数量级 |
| 是否含调试器接入 | 含 / 不含 | 单步与断点会把时间拉长到数倍,且掩盖真实瓶颈 |
| 是否含外设初始化 | 含 / 不含 | 串口、显示、网络若放在启动链里,会主导整段时间 |
| 是否含校验与解密 | 含 / 不含 | 完整性与安全启动校验是启动链里最重的计算段之一 |
| 统计方式 | 平均值 / 最大值 / 分布 | 启动链的耗时分布往往长尾明显,只报均值会掩盖问题 |
| 样本量与温度条件 | 常温 / 高低温 / 多批次 | 低温与高温下的介质读时延差别可能很大 |
最后一行值得单独强调:启动时间的最大值往往比平均值更值得关注。用户能接受「偶尔慢一点」,不能接受「偶尔开不了机」。因此验收指标建议同时给出平均、P99 与最大三个数,并把「连续冷启动若干次全部成功」写成明确的通过准则。
1.5 一张完整启动链流程图
把前面四段合起来看,加上「失败怎么回来」这条横线,就是一张完整的启动链。图中时间占比是示意,用来说明「加载段通常是最大的一块可动空间」;每段的详细做法分别在第 2、3、5~7 章展开,移交与安全在第 9 章,排障在第 11 章。
四段之间的衔接还有一条容易忽略的主线:每一段交给下一段时,都必须已经成立一组前提,也必须事先定义好失败时的动作。复位交给 ROM 初始化时,供电与时钟源必须已稳定,失败则等待或复位;ROM 交给介质初始化时,串口必须可用,失败则打印诊断后停住;介质初始化交给加载时,镜像头必须可解析,失败则重读或转诊断模式;加载交给移交时,校验必须通过,失败则重试若干次后回退;移交交给操作系统时,版本判定必须完成,失败则回退或进恢复模式。没有这条主线,启动链一旦在中间出错就只能从头重来,现场定位成本极高。
2. BootROM:固化在硅片里的那一段代码
BootROM 是整条启动链里唯一「改不了」的一段。它的容量以 KB 计,代码量以最小可用为设计目标,能力以「刚好够用」为准。理解它的边界,就理解了后面三类介质存在的理由。
2.1 BootROM 的职责边界与它必须做的最小集合
BootROM 的设计目标只有一句:用最小的代码,把系统带到「可以读外部介质」的状态,然后把控制权交出去。为了做到这一点,它必须做且只做这几件事:
- 建立最小运行环境:栈、少量 RAM、必要的时钟。ROM 自带的 RAM 通常很小,深调用链要靠自己尽快搬走。
- 留一条能说话的通路:串口或调试口。这是现场唯一能拿到「卡在哪一步」的手段,没有它的 BootROM 在量产后等于黑盒。
- 让启动介质可读:把引脚复用改对、时序参数设对、器件识别出来。这一步的难度决定了 BootROM 能不能支持大容量介质。
- 移交:把一段「位置已知的可执行内容」交给下一段,并且约定好失败时怎么办。
| 职责 | 为什么必须在 ROM 里 | 放不下的后果 | 典型取舍 |
|---|---|---|---|
| 取复位向量与装载栈 | 位置固定、必须最先执行 | 无法启动,现象为完全无输出 | 无取舍,必须做 |
| 最小时钟与内存初始化 | 后续一切代码的前提 | 读时序错乱,表现为偶发失败 | 只做最小集合,完整初始化下移 |
| 调试口初始化 | 唯一能在早期拿到日志的手段 | 量产后无法定位,返修成本极高 | 宁可少做一个外设也要留调试口 |
| 启动介质识别与读通路 | 决定能不能从介质取代码 | 识别失败或读到错误数据 | 大介质往往只能做「部分识别」 |
| 完整镜像校验 | 需要在拿到足够代码之后才能做 | 引导代码被误改或损坏时无法发现 | ROM 内只做轻量校验,重校验下移 |
| 签名验证与密钥管理 | 需要非易失的信任根 | 无法建立安全启动信任链 | 通常只放公钥或公钥哈希 |
| 多镜像选择与回退 | 需要稳定的存储来记录尝试次数 | 升级失败后设备变砖 | 几乎必须放在可更新的一级 |
BootROM 里多放几千字节去初始化一个用不上的外设很容易,但省下这几 KB 去留一个调试口,是整个产品周期里回报最高的决定。BootROM 里的串口输出不依赖操作系统、不依赖外部介质、不依赖任何镜像是否有效——它是唯一能在「什么都还没起来」的时候说话的手段。缺了它,第 11 章里一半的排查建议都会退化成「换一颗料试试」。
2.2 掩膜固化 vs 预烧录 Flash 内的引导代码:取舍
「引导代码放在哪里」实际上有两条路:固化进硅片(掩膜)或预烧进外部 Flash 的固定偏移。两者不互斥,实际产品常常是「ROM 一小段 + Flash 一段」的两级结构。选择哪条,看四个维度:
| 维度 | 固化在 BootROM(掩膜) | 预烧进外部 Flash 固定偏移 | 怎么取舍 |
|---|---|---|---|
| 可改动性 | 不可改,改动要重做掩膜 | 可随时重烧,分钟级 | 有 BUG 修复需求就必须放在 Flash |
| 启动确定性 | 最高,不受器件与外部接口影响 | 依赖接口正确、器件正常、供电正常 | 功能安全与高可靠场景偏向 ROM |
| 能力上限 | 以 KB 计,只能做最小引导 | 可做完整识别、完整校验甚至二级引导 | 需要复杂介质初始化就必须放在 Flash |
| 供应链 | 随晶圆固化,交期长 | 产线后段烧录,可并行 | 量产爬坡期 Flash 方案更灵活 |
| 防篡改 | 天然不可改,可作为信任根 | 可被改写,需额外的签名或写保护机制 | 安全启动的信任根只能是 ROM |
典型做法是:ROM 只负责「把 Flash 里的第一段读出来」这件事本身(可能只有几百字节到几 KB),第一段引导代码放在外部 Flash 的固定偏移上。这样既拿到了 ROM 的确定性,又拿到了 Flash 的可维护性。代价是多了一次交接,交接协议的严谨程度直接决定系统的健壮性——这正是 2.5 节要讲的内容。
2.3 BootROM 的局限:不可变更、容量小、认证能力弱
上一节的表格已经列出了具体表现,这里把三条局限的根因与后果讲透,因为它们直接决定了第 4 章的选型结论。
这三个局限互相强化,最终形成一个很难挣脱的局面:不可变更让现场发现的 BUG 只能等下一颗料、产线不良品无法现场救活;容量小让它无法容纳完整介质初始化、完整校验与复杂人机交互,只能把识别与校验拆到一级去做;认证能力弱则因为存放密钥需要非易失且不可读的存储,导致真正的签名验证难以实现——应对方式是让信任根只保存公钥或公钥哈希,对称材料外置加密存储。此外还有一条常被忽略的局限:调试受限,ROM 内无法用常规手段观测内部状态,失效分析只能靠猜,因此必须在设计时就预留诊断输出与失败标志位。
把这四条放在一起看,结论其实很直接:BootROM 是一个「兜底 + 引导」的角色,不是一个「运行」的角色。任何需要版本迭代、需要复杂协议、需要承载业务逻辑的功能,都不应该放在 ROM 里。这条判断准则比任何具体容量数字都可靠。
2.4 BootROM 常见失败模式与现场排查
BootROM 层面的失败有个共同特点:因为这段代码不可改,所以现场遇到时几乎没有软件手段可以救。因此这一层的工作重心是预防而不是修复。下面按现象归类,给出首查项。
| 现象 | 最可能的原因 | 首查项 | 可否软件补救 |
|---|---|---|---|
| 完全无串口输出 | 时钟未起或串口引脚复用不对 | 供电时序、时钟源、引脚复用配置 | 基本不能,需改料 |
| 反复输出同一段信息后复位 | 找不到合法镜像头,反复回到入口 | 镜像偏移、头部长度、校验字段 | 可以,重新烧镜像 |
| 能读到器件标识但读数据全 FF / 全 00 | 读命令时序或地址宽度不匹配 | 地址字节数、页内偏移、读时序参数 | 可以,重新配置 |
| 大容量器件能识别、小容量不行 | ROM 内做了器件选择分支,漏了目标型号 | 器件支持列表是否包含目标料号 | 看是否被裁剪过 |
| 低温才偶发失败 | 时钟建立时间不够或时序余量偏紧 | 建立时间、时序参数余量、上电斜率 | 较难,需重新规划时序 |
| 串口有输出但内容乱码 | 波特率或时钟源配置不一致 | 时钟源切换点、波特率计算 | 可以,改外部配置 |
| 同一批板子有个别完全无输出 | 焊接或供电个体差异 | 供电轨电压、焊接、上电时序 | 基本不能,属生产问题 |
2.5 BootROM 与外部 Flash 引导代码的衔接协议思路
两级结构的核心是那个「交接」。交接协议不需要多复杂,但必须把三件事定义清楚:交的是什么、失败了怎么办、怎么知道上一段做完了。
- 交的是一段确定位置的镜像:约定镜像头在介质的固定偏移,头里至少有长度、目标加载地址、校验信息。ROM 只负责把头部与正文读到并校验,不解析任何复杂格式。
- 失败语义要先定好:头部无效或校验不通过时,是重试、跳到备用位置、还是进入一个最小诊断模式。这一条决定了现场是「起不来但能说话」还是「彻底黑盒」。
- 要有可观测的交接记录:至少保留「当前停在哪一级、失败原因是哪一类」的状态。这些状态既用于现场的串口输出,也用于第 11 章讲的故障快照。
| 协议要素 | 作用 | 不定义会怎样 | 建议做法 |
|---|---|---|---|
| 镜像头固定偏移与对齐 | 让 ROM 不需要目录就能找到入口 | ROM 要么找不到、要么误把数据当代码 | 固定对齐偏移,偏移值本身也参与校验 |
| 头部长度与字段含义 | 让 ROM 能跳过不认识的新字段 | 固件版本一升就变砖 | 头部预留扩展区,未知字段按约定跳过 |
| 目标加载地址 | 支持搬运到任意 RAM 位置 | 只能原地执行,RAM 布局被锁死 | 在头部显式声明,加载地址需做合法性检查 |
| 失败重试次数与去向 | 避免无限循环 | 现场看到设备反复重启 | 设有限重试次数,之后进入诊断模式 |
| 运行标志与回退计数 | 支持升级失败后回退 | 升级失败即变砖 | 用介质的固定扇区存放,成功后再清除 |
| 诊断输出约定 | 让现场能判断停在哪一级 | 只能靠拆机换料 | 固定格式打印阶段号与失败类别 |
把镜像头里 ROM 不认识的字段全部填成随机数据,系统仍然能正常启动到应用,就说明协议有扩展余量;再把头部校验改坏,系统应当停在一个能说话的地方而不是死循环或彻底无输出。这两个测试做下来,衔接协议的健壮性基本就心里有底了。
3. XIP 直跑与拷贝运行的分野
这是启动链上一个真正的分叉点。同样是「从 Flash 里取代码执行」,可以完全不碰 RAM,也可以先老老实实搬一遍。两者的代价结构完全不同,适用条件也完全不同。
3.1 XIP 的原理与前提:可随机访问的接口
XIP(Execute In Place,原地执行)的做法只有一句话:把一段外部存储映射到处理器地址空间里的一个固定窗口,取指时硬件自动去读那块存储。程序计数器指向这个窗口内的地址,CPU 以为自己在读内存,实际上每次取指都变成了一次外部读。
它成立的前提有三条,缺一条就不成立:
- 接口必须支持随机访问。给一个地址就要能在合理时间内返回对应的数据,而不能要求「必须从头顺序读」。这是 NOR 天然具备、NAND 天然不具备的差别。
- 读时延必须足够确定。取指对时延抖动比数据访问更敏感——一次不确定的等待会让 CPU 的流水线频繁停顿,表现为「代码跑得慢但说不清为什么」。
- 不需要修改内容。XIP 期间介质里的内容是当作指令读的,不能是压缩包、加密包或需要运行时重定位的格式。这条常被忽略:如果镜像需要解压或解密,就不可能纯 XIP。
| 维度 | XIP 直跑 | 拷贝到 RAM 运行 |
|---|---|---|
| 对介质的要求 | 可随机访问、读时延确定、可映射到地址空间 | 顺序读即可,允许较长的块读时间 |
| RAM 需求 | 仅需栈与少量数据,几乎不需要大块代码缓冲 | 需要与待运行镜像等大或更大的 RAM |
| 启动耗时构成 | 无搬运时间,但执行期持续付出读时延 | 一次性搬运时间,之后执行很快 |
| 镜像可压缩 / 可加密 | 不可以 | 可以,搬运前先解密解压 |
| 执行期总线占用 | 高:每次取指都要走外部总线 | 低:执行期只访问 RAM |
| 实时性 | 受介质读时延抖动影响 | 执行期确定性好,但启动点有一段集中延迟 |
| 典型平台 | 带 NOR 且支持映射窗口的 MCU | 几乎所有平台都可用的兜底方案 |
3.2 拷贝运行的流程与代价
拷贝运行是最普适的方式:任何能读出字节的介质都能用,代价是多花一段搬运时间和一块 RAM。
典型流程是四步:
- 确定源地址与长度:从镜像头读出加载地址与长度,并检查目标区域是否可用。
- 搬运:按块或按页把内容读进 RAM 缓冲区。这一步可以边读边校验,把校验开销和搬运重叠掉。
- 校验:整段搬完后确认校验通过,再决定是否跳转。中途失败应回到重试计数,而不是带着半个镜像往下走。
- 重定位与跳转:若代码含绝对地址,需要做重定位;然后跳到 RAM 里的入口。
拷贝的真实代价有三笔:搬运占用的启动时间、被占用的 RAM、以及RAM 布局被镜像大小绑架。第三笔最容易被忽略——一旦代码膨胀到 RAM 放不下,就得回头改架构,这在大内存 Linux 平台上尤其常见(此时解法是把内核分段加载,而不是整块进 RAM)。
| 成本项 | 量级 | 由什么决定 | 能不能省 |
|---|---|---|---|
| 搬运时间 | 与镜像大小和总线宽度成正比 | 总线宽度、等待状态、介质连续读能力 | 能:用 XIP、或只搬下一级需要的部分 |
| RAM 占用 | 与镜像大小成正比 | RAM 总量与系统其他用途的分配 | 能:分级加载、或用 XIP |
| 重定位时间 | 通常很小 | 镜像内绝对地址引用的数量 | 部分能:编译时生成位置无关代码 |
3.3 XIP 对介质的要求:读延迟确定、无需 ECC 重算、可映射到地址空间
把上面的前提展开成下面四条可核查的判据。这四条里任意一条不满足,XIP 就会从「省时间」变成「浪费时间」甚至「跑不起来」。
| 要求 | 具体含义 | 怎么核查 | 不满足的后果 |
|---|---|---|---|
| 读延迟确定 | 连续读与随机读的耗时差异可控,且有明确上界 | 看时序参数与实测分布,不只看典型值 | 取指停顿,实时性不可控 |
| 无需 ECC 重算 | 读取时不需要软件参与纠错,或纠错开销可忽略 | 区分「有硬件 ECC」与「需软算」两种介质 | 每次取指都要算错误,启动慢到不可接受 |
| 可映射到地址空间 | 能把介质地址空间的一个窗口挂到总线地址上 | 看器件是否支持该模式、窗口大小与粒度限制 | 只能退回拷贝运行 |
| 内容可直接执行 | 介质里的字节就是可执行指令,不需先变换 | 确认镜像是否压缩、加密、含重定位信息 | 需先搬运到 RAM 再跳转 |
NAND 的读出粒度是「页」,而且读出之前需要控制器做坏块映射与纠错——这两件事不是纯硬件地址映射能完成的,它需要一段软件参与。因此把 NAND 映射到地址空间直接执行,在多数实现里并不成立;即使硬件上能映射,纠错带来的延迟与不确定性也让 XIP 失去意义。这正是第 6 章要展开的核心障碍。
3.4 混合模式:XIP 启动 + 拷贝到 RAM 运行
实际产品里最常见的不是二选一,而是混合:用 XIP 把「小而关键」的第一段代码跑起来,再由它把「大而不急」的部分拷到 RAM 执行。典型分工是:
- XIP 区:复位向量、中断向量表、时钟与内存初始化、介质初始化、镜像头解析。这些加起来通常只有几 KB,但必须在最快路径上跑完。
- 拷贝区:主应用、图形资源、需要解密的模块、模型权重。这些大块内容在启动后慢慢搬,搬完再启用。
这个混合模式的四个区段可以这样划分:XIP 区放向量表、时钟与内存初始化、介质初始化——体量小、在关键路径上,必须最快完成;校验区做镜像头与首段校验,用直接执行加少量栈空间即可,因为跳转前必须确认正确而成本很低;拷贝区放主应用与大数据段,体量大、执行期需要高速与确定,必须搬进 RAM;延后区放日志、网络、外设驱动,它们不属于「可用」的必要条件,可以放到启动完成之后再初始化。
混合模式的关键设计点是「在哪里切换」。切换点之前不能依赖任何未初始化的东西,包括 RAM 里未清零的区域、外部设备、甚至栈上的返回值。一个实用的做法是把切换点做成一次显式的、带日志与失败回退的跳转,让它成为一个可观测的事件——这也是第 11 章做故障快照时最有价值的一个时间锚点。
3.5 五种启动方式的对比矩阵
把常见的五种方式放进同一张表,就能看清各自的适用边界。矩阵的价值不在于「哪个最好」,而在于每种方式放弃了什么。
| 启动方式 | 对介质的要求 | 对 RAM 的要求 | 启动确定性 | 可维护性 | 典型适用 |
|---|---|---|---|---|---|
| ① 纯 BootROM 直跑 | 无外部介质 | 几乎不需要 | 最高 | 最低 | 极小代码 MCU,一级镜像固定不变 |
| ② BootROM 拷贝到 RAM | 顺序读即可 | 需较大 | 高 | 低 | 代码略大但仍需不可变启动 |
| ③ SPI NOR XIP 直跑 | NOR 支持映射窗口 | 几乎不需要 | 高 | 中 | 外部 NOR 足够大、要求启动快的 MCU |
| ④ SPI NOR 拷贝到 RAM | 顺序读即可 | 需较大 | 高 | 中 | NOR 不支持 XIP 时的主流选择 |
| ⑤ 容量介质分级加载 | 需完整识别与分区解析 | 需中量 | 中 | 最高 | Cortex-A 平台、大容量、需 OTA 原子性 |
如果不需要更新(生命周期极短、成本敏感、产量巨大),纯 BootROM 或 BootROM + 小容量固定镜像最省事;如果需要更新,就必须把可变的部分放到可写的外部介质上,此时「ROM 只做引导、外部介质做全部逻辑」的分层几乎总是对的。这个问题比「要不要 XIP」重要一个数量级。
4. 第一类介质:片内 BootROM 与 eMCP 类内置
第一类介质的共同点是:启动代码和介质都在芯片内部,外部总线上没有任何启动器件。这一类方案启动确定性最好、成本最省,代价是容量极小、几乎不可变更。
4.1 纯 BootROM 方案的启动时序与代码容量约束
纯 BootROM 方案的启动链只有三步:取向量、建最小环境、跳转到固定地址。它的时间短到几乎可以忽略,真正的约束是代码容量。
ROM 容量以 KB 计,且面积直接对应成本。经验上 ROM 里放不下复杂的东西:完整的介质识别、完整的镜像校验、复杂的中断管理、任何需要较大表格的逻辑——这些都会迅速把体积推上去。所以纯 BootROM 方案里的主程序通常是「极小的固定代码 + 尽可能少的逻辑」,真正的功能在别处(可能是不可编程的应用专用逻辑,也可能是固化在另一片 ROM 里)。
时间上,取向量与装载栈在百微秒以内量级,最小环境初始化在百微秒到毫秒量级,校验固定镜像在毫秒量级,跳转本身是微秒量级。真正需要分配预算的不是这些动作,而是「校验」这一段——它随镜像大小与算法复杂度线性增长,是纯 BootROM 方案里唯一可能变慢的地方。
4.2 eMCP / uMCP 这类把控制器与 Flash 合封的价值:省掉外部介质初始化
eMCP、uMCP 这类器件把存储控制器与 Flash 阵列合封装在同一颗芯片里,对主板而言它看起来就像一颗「带控制器的存储器」——不需要外挂控制器,也不需要复杂的裸介质时序驱动。
它对启动链的价值体现在一个具体的地方:第二阶段「介质初始化」被显著压缩。控制器已经在片内完成上电自检与初始化,主机侧只需要按标准协议发命令,不必自己写一套介质驱动。
把合封器件与外挂方案逐项对比:控制器位置上外挂方案需要主机自己驱动,合封器件片内自带;主机侧代码量上外挂 NOR 最小、外挂 NAND 最大、合封器件居中;上电初始化上外挂 NOR 简单,外挂 NAND 需要强制状态读与参数下发,合封器件由控制器内部完成;启动确定性上外挂 NOR 最好、外挂 NAND 最差;现场可修复性上外挂方案可以换芯片,合封器件焊死在板上,通常要整板返修;适用容量段上外挂 NOR 面向小容量、外挂 NAND 面向小到中容量、合封器件面向中容量。
把控制器与 Flash 合封,换来的是启动简化和更好的性能,但同时失去了「通过更换外部芯片来救回一台设备」的能力。当固件有缺陷、需要靠更新覆盖修复时,合封器件内部的 Flash 内容就成了一次性资源。因此选择合封方案前,必须确认两件事:一是内部 Flash 内容有可行的升级路径;二是即使升级失败,也不会让设备永久报废。这两条不成立时,合封带来的好处抵不过它的风险。
4.3 内置控制器带来的启动简化与代价
把「内置控制器」这件事的收益与代价摊开看,收益集中在启动链的第二阶段,代价则集中在灵活性与可修复性两个维度。
| 方面 | 收益 | 代价 | 如何权衡 |
|---|---|---|---|
| 启动代码量 | 主机侧不必实现介质时序与纠错,代码显著变短 | 仍需实现命令交互层,代码量不为零 | 优先保证代码量能放进 ROM |
| 启动时间 | 省掉器件识别与状态恢复,缩短第二阶段 | 控制器内部初始化有固定开销,不可跳过 | 按实测分配预算,不要假设可省略 |
| 介质灵活性 | 接口统一,换料号时主机代码改动小 | 被限制在合封器件支持的容量与形态内 | 容量规划留足余量,避免顶格设计 |
| 可维护性 | 无 | 不可现场更换器件,返修要动板 | 必须有可用的升级路径兜底 |
4.4 适用边界:不可现场升级 Flash 内容本身时怎么办
这是第一类介质最常被问到的问题。答案取决于「不可升级」的到底是哪一部分——这一点必须先分清,否则方案会做错。
| 不可升级的范围 | 典型形态 | 应对策略 | 残余风险 |
|---|---|---|---|
| 只有 BootROM 不可升级 | 掩膜 ROM + 外部可写介质 | 把全部可变逻辑放外部介质,ROM 只做引导 | ROM 自身有 BUG 时只能换料 |
| 内部 Flash 可升级但需专用流程 | 合封器件,OTA 或特殊通道写入 | 设计成双区或外部备份引导路径 | 升级通道本身失效时无退路 |
| 内部 Flash 完全不可升级 | 掩膜 ROM 是唯一存储 | 只能靠一次性正确的固化,功能必须做减法 | 有缺陷即产品缺陷,需极严格验证 |
| 外部有可写介质但容量不足 | 小容量 NOR + 大 ROM | 只把关键数据放外部,其余固化 | 数据容量受限,需明确取舍 |
如果确实只能用不可升级的固化代码,那么验证强度必须同步提高一个等级:固化内容一旦出错,成本是整批返修甚至整机报废。可行的做法包括:固化前对镜像做完整性与功能双重验证、在产线上保留可观测的启动诊断输出以便快速判定问题归属、以及在设计评审时就把「哪些功能绝对不能放进固化代码」写成明确的红线。