尧图精选

嵌入式启动流程全解析:从Cortex-M向量表到OTA升级与故障定位

🕒 发布时间:2026/9/9 4:09:42 📁 来源:尧图网络
1. 为什么会点灯的工程师面对启动流程仍然心里发虚我见过太多这样的场景同事把编译好的嵌入式固件烧进板子LED没亮第一反应是我再改改代码可改来改去发现跟代码没关系于是开始怀疑芯片坏了、焊接虚了、下载器老化。反复折腾一上午最后发现是boot引脚电平被飞线带偏了芯片压根没从内部Flash启动。这种挫败感我太熟悉了。嵌入式固件开发做到一定阶段程序能跑和知道它为什么能跑之间会拉开一条巨大的鸿沟而这条鸿沟的起点几乎都压在启动流程上。你如果不理解芯片从上电复位到main函数之间替你做了什么遇到问题就只能靠试错、靠重新烧录、靠玄学。而故障定位方法论和OTA升级工程化这两件事本质上都是建立在你对固件从上电到运行这段路径有多熟悉这个前提上的——启动流程就是整套嵌入式基本功里的水线水线以下的东西看不清水面以上的技巧全是空中楼阁。作为一个在固件开发里摸爬滚打了十几年的工程师我在CSDN开付费专栏连载就是想把这块水线以下的硬骨头啃下来。这篇是系列里的进阶篇核心就三件事第一把Cortex-M内核和典型SoC以IMX6为例的启动链路逐段拆开回答复位后第一条指令到底在哪第二基于对启动链路的理解讲一套可以复用的故障定位排查方法论解决板子没反应第一步该查什么第三落到工程层面用ESP32的OTA实践讲清楚双分区、回滚校验和坚决不能变砖的底线设计。文章最后把上篇连载的课后思考题完整解析补上。1.1 一个让我彻底重视启动流程的现场2017年我调试一块量产返修的板子。现象很统一用户反映设备偶发死机返厂后我们测试又一切正常。后来我用示波器抓了上电时序发现复位脚释放的瞬间电源电压还在爬升——硬件上电时序不满足芯片要求导致芯片偶发复位失败。那一次之后我意识到一个问题固件工程师不能只看软件启动这件事是软硬件交界的第一道关卡越早建立完整的启动全景图越少在低级问题上浪费时间。很多刚入行的朋友喜欢直接跳到应用层写业务逻辑觉得启动代码是厂家给的、是成熟的、不需要看。但只要你做过几款不同芯片的项目就会发现不同平台之间复位向量→时钟配置→存储初始化→系统堆栈建立→进入main这几步思路高度一致但细节千差万别。你把Cortex-M的启动摸透了再看IMX6的IVT、再看uboot的启动流程、再看RT-Thread组件自动初始化会发现它们只是在同一张底图上不断叠加复杂度。1.2 三个主题为什么要放在同一篇里讲启动流程、故障定位、OTA升级表面上是三个方向实际上是一条逻辑链。启动流程解决的是固件是怎么活过来的故障定位解决的是固件是怎么死掉的OTA解决的是固件怎么安全地换血。你只有先知道固件正常的生命轨迹才能在它异常时快速定位偏离点只有熟悉启动的每一步才能设计出断电不砖、失败能回滚的OTA方案。我见过不少团队OTA方案设计得很豪华但遇到一次升级后设备变砖就束手无策因为压根没搞懂bootloader在升级流程里扮演什么角色。下面我从复位向量开始一点一点拆。2. Cortex-M启动全链路拆解从复位向量到RT-Thread调度器2.1 复位后的第一步向量表里那八个字节Cortex-M内核M0/M3/M4/M7复位后硬件会做一件非常固定的事从地址0x00000000读取初始栈指针MSP从地址0x00000004读取复位向量并跳转过去。这两个地址是所有Cortex-M固件的生命起点。很多新手会问为什么不能直接从0x00000000开始执行代码这里有两个层面的原因。第一Cortex-M是向量表驱动的中断架构异常和中断入口统一由向量表管理向量表的第一项固定是栈顶地址第二项固定是复位向量这是ARM架构的约定硬件就这么设计你要遵循它第二任何函数调用、中断响应、压栈出栈都依赖栈指针CPU复位后SP是未知的所以必须先加载初始MSP再执行第一条指令否则一压栈就崩。关于向量表还要注意几个关键点向量表并不一定非得在0地址Cortex-M3/M4内核可以通过VTOR寄存器0xE000ED08把向量表重定位到其他地址。这在BootloaderApp架构里非常重要——App要运行时bootloader先把App的向量表地址写入VTOR再跳转过去。M0与M3/M4有差异。M0的向量表位置相对固定且异常数量较少M3/M4有VTOR可重定位但要注意向量表必须按地址对齐通常是2^N对齐大小等于向量表字节数。Flash起始地址和0地址的关系如果芯片支持内存映射内部Flash通常映射在0x08000000以STM32为例但复位后CPU依然先从0x00000000取向量。厂商会在芯片内部做别名映射实际访问的还是Flash区域。这也是为什么你用调试器看0地址和0x08000000的内容是一样的。2.2 startup文件、分散加载与C库初始化工程里那个startup_stm32fxxx.s汇编文件干的事情比大多数人想象的要多。它的核心结构是定义栈空间Stack_Size、堆空间Heap_Size、建立中断向量表、编写Reset_Handler。Reset_Handler的典型流程是Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT SystemInit IMPORT __main LDR R0, SystemInit BLX R0 LDR R0, __main BX R0 ENDPSystemInit负责把系统时钟配好PLL、Flash等待周期、总线分频然后跳转到C库的__main。注意这里__main不是我们C语言里写的main()它是C库的初始化入口会完成三类事把RW段从Flash拷贝到RAM、把ZI段清零、建立堆栈调用环境最后才调用我们写的main()。在这一步里我踩过最深的坑是分散加载文件写错导致的神秘跑飞。有一次我把某段只读数据放进了RW段结果上电后变量初值全是乱的排查了两天才定位到是scatter文件里加载域和执行域地址不匹配。后来我总结出一个经验调试启动阶段的问题先别急着怀疑编译器先把Map文件打开看每个段的加载地址Load Region和执行地址Execution Region是否符合预期。2.3 RT-Thread初始化链rtthread_startup里究竟做了什么如果你的项目用了RT-Thread那么main函数通常只做一件事——调用rtthread_startup()。这个函数是系统所有组件的总开关它的执行顺序有严格要求顺序错了系统就起不来。我这里给出一份典型的执行序列rt_hw_interrupt_disable()关闭全局中断。启动阶段不允许被打断这是所有RTOS的共识。rt_hw_board_init()板级初始化。这里会调用rt_components_board_init()把INIT_BOARD_EXPORT导出的函数全部执行一遍同时完成系统堆初始化。显示RT-Thread版本信息。rt_system_timer_init()初始化系统节拍定时器相关数据结构。rt_system_scheduler_init()初始化调度器。rt_application_init()创建main线程。注意到这里之前系统还没有开始调度。rt_system_timer_thread_init()创建定时器线程。rt_thread_idle_init()创建空闲线程。rt_system_scheduler_start()启动调度器系统开始多线程运行。这个顺序不是随便排的。比如定时器线程必须在调度器启动前创建好否则一旦开始调度定时器线程还没就绪系统节拍就无法推进。理解这条链的关键在于RT-Thread的启动是一个先搭骨架、再填血肉的过程。中断、板级、堆、调度器是骨架线程和组件是血肉骨架没搭好之前血肉不能先跑。2.4 自动初始化宏INIT_BOARD_EXPORT的魔法RT-Thread的自动初始化机制很多人用得很熟但不一定知道它底层是怎么实现的。它的核心是编译器段section机制。#define INIT_EXPORT(fn, level) \ const init_fn_t __rt_init_##fn SECTION(.rti_fn. level) fn这条宏把函数指针放到指定的段里比如.rti_fn.0、.rti_fn.1……然后RT-Thread在链接脚本里把这些段按序排列启动时统一遍历调用。这就是为什么你写的初始化函数不需要任何显式注册表只要用宏导出了系统会自动帮你按优先级执行。各等级的调用时机和典型用途如下初始化宏段名调用时机典型用途INIT_BOARD_EXPORT.rti_fn.0rt_hw_board_init内板级外设、引脚、时钟INIT_PREV_EXPORT.rti_fn.1rt_components_init纯软件模块前置初始化INIT_DEVICE_EXPORT.rti_fn.2rt_components_init设备驱动注册INIT_COMPONENT_EXPORT.rti_fn.3rt_components_init组件如shell、lwipINIT_ENV_EXPORT.rti_fn.4rt_components_init环境变量等INIT_APP_EXPORT.rti_fn.5rt_components_init应用层初始化理解了这套机制你就明白为什么驱动注册函数不能写在INIT_BOARD_EXPORT里做应用逻辑——它执行太早了调度器和信号量都还没就绪。很多新手在这里踩坑在INIT_BOARD_EXPORT里调用rt_mutex_create结果系统直接断言崩溃。原则是板级阶段只做硬件相关的初始化任何依赖内核对象的操作必须放到组件或应用阶段。3. SoC启动与MCU的差异IMX6 IVT、DCD与uboot的三级接力3.1 为什么MCU那套思维搬到SoC上会失灵做过STM32的人对上电就从Flash执行这件事习以为常。但到了IMX6这类应用处理器上这套思维直接失灵。原因很简单IMX6通常是外部接DDR、eMMC/SD卡、NOR Flash芯片内部没有你放固件的大容量非易失存储CPU也不可能直接从DDR启动因为DDR本身需要初始化之后才能用——这就成了一个鸡生蛋的问题代码要运行得先初始化DDR可DDR没初始化代码怎么运行SoC的方案是引入一个芯片出厂固化的Boot ROM。Boot ROM是芯片内部的一块只读存储器出厂时就写好了启动代码上电后CPU从Boot ROM开始执行由它来负责从外部存储介质搬运代码、初始化内存、最终跳转到用户程序。这就是MCU和SoC启动流程最本质的差异MCU是硬件直接映射启动SoC是软件引导式启动。3.2 IMX6的IVT与DCD藏在启动头里的硬件初始化IMX6的Boot ROM从SD卡、eMMC或NAND加载用户程序的依据是一张叫做IVTImage Vector Table的结构。这张表通常存放在启动介质开头的固定偏移处比如SD卡的1KB偏移处。IVT的核心字段包括字段作用Header标识和版本如0x402000D1Entry用户程序入口地址DCD指针指向DCDDevice Configuration Data数据Boot Data包含程序在存储介质上的起始地址和长度Self指针指向IVT自身其中最关键也最容易出问题的是DCD。DCD是一组寄存器配置数据Boot ROM会逐条解析它用来初始化DDR控制器MMDC、IOMUX引脚、时钟等。换句话说你的uboot还没有运行芯片先用DCD把DDR点亮了。DCD里一个寄存器地址写错、Timing参数不对效果不是慢一点而是板子彻底无输出因为代码压根没地方跑。我调试IMX6平台时最痛苦的就是DCD的DDR初始化参数。不同DDR颗粒的时序参数tRCD、tRP、tRFC千差万别DCD配置错了现象往往一模一样串口静默、没有任何输出。这时候你有两种手段一是用仿真器连接查看Boot ROM的执行状态和报错信息二是对照内存控制器手册逐条核对DCD里的寄存器值。后者极其考验耐心。3.3 uboot的接力赛从Boot ROM到kernelDCD执行完毕Boot ROM把uboot镜像搬运到DDR里跳转到uboot入口。之后的启动流程大致是这样如果使用SPL方案uboot会先跑一个轻量级的SPLSecondary Program Loader负责更复杂的板级初始化再加载完整的uboot。完整uboot启动后经历board_init_f初始化DRAM、串口等基础外设和board_init_r完整的设备模型、文件系统、命令系统两个阶段。进入命令行或执行bootcmd环境变量里的启动命令读取kernel镜像和设备树到内存最终通过booti或bootz启动内核。uboot阶段故障定位的方法论和裸机完全不是一个量级。我常用的排查顺序是串口有没有输出→看到哪一行停住→md命令读内存内容验证DDR访问→mmc read验证存储介质读取→printenv检查bootcmd和bootargs。始终记住一个原则uboot是软硬件的中转站它自己打印出来的每一条信息都是珍贵的故障锚点。3.4 冷启动、热启动与看门狗复位被忽视的启动路径差异启动流程并不是只有上电启动这一条路。看门狗复位、软复位、掉电重启看起来都叫复位但芯片走的路径有微妙差异有些SoC的软复位不会重新执行Boot ROM而是直接跳转到固定地址有些外设在上电复位和软复位后的默认状态不同。这个差异在实际联调中会坑人。我之前遇到过一个问题断电再上电系统正常看门狗复位后系统就卡死。排查到最后发现某个外设需要在RTC域保持寄存器里做一个标志位来判断是否跳过一段初始化代码而软复位没有清掉这个标志导致外设进入了错误状态。所以设计固件时我建议复位处理统一进入同一个入口函数并且用寄存器判断复位源分情况做差异化处理。4. 固件跑飞与启动卡死一套可复用的故障定位方法论4.1 先定边界硬件问题还是软件问题板子上电没反应最忌讳的就是直接打开代码开始看。我给自己定了一个铁律先花十分钟排除硬件再打开IDE。具体动作是电源万用表量各路电源轨是否存在、电压是否正常、纹波是否过大。尤其要检查上电时序——很多SoC对电源轨的上电顺序有严格要求。时钟示波器看外部晶振/时钟芯片输出是否起振频率对不对。晶振没起振一切免谈。复位确认复位脚电平是否符合预期。如果在低电平上卡住芯片就是一直处于复位态。烧录状态确认芯片里到底烧进去的是不是你最新编译的固件。我见过太多次排查两小时最后发现烧的是上一个版本的乌龙。这几步做完才能判断进入软件排查阶段。这个顺序反过来就是典型的浪费时间先看代码、再量电压、最后发现是电源没焊好。4.2 HardFault现场还原EXC_RETURN、PC与调用栈三板斧固件跑飞最常见的是触发HardFault。新手打开调试器看到PC指针飞到一个奇怪地址直接懵了。其实Cortex-M把故障现场保存得很完整关键是你要知道去哪里看。第一步看LR寄存器的值判断异常返回模式。LR在异常入口处被压栈时会变成EXC_RETURN格式EXC_RETURN值含义0xFFFFFFF1返回后进入Handler模式使用MSP0xFFFFFFF9返回后进入Thread模式使用MSP0xFFFFFFFD返回后进入Thread模式使用PSPRT-Thread这类RTOS里线程运行在Thread模式并使用PSP所以LR通常是0xFFFFFFFD如果是中断嵌套导致的故障LR可能是0xFFFFFFF1。第二步找到正确的栈指针。LR0xFFFFFFFD时用PSP去取栈内容LR0xFFFFFFF9时用MSP去取。这步错了一步后面全白费。第三步解析压栈的8个字。Cortex-M在异常入口自动保存的顺序是R0、R1、R2、R3、R12、LR被中断前的返回地址、PC断点地址、xPSR。栈顶往上第7个字就是故障发生时的PC第6个字是被中断函数的返回地址LR。这两个值加上SCB-CFSR可配置故障状态寄存器里具体的错误类型基本就能定位到出错的代码。4.3 启动阶段的典型故障模式与快速判别基于对启动流程的拆解我整理了启动阶段最常见的几种故障模式和它们的判别方法故障现象大概率原因快速验证手段上电后完全没反应电源/时钟/复位硬件问题或芯片未进固件示波器、调试器连接调试器能连上但PC停在0xFFFFFFFE向量表被破坏或Flash内容异常查看Flash起始8字节数据卡死在SystemInit里时钟配置锁死PLL无法锁定调试器单步、检查HSE/HSImain没执行卡在C库初始化分散加载文件错误或堆栈设置过小查看Map文件、调大栈空间调度器启动后第一、二个线程就崩溃线程栈溢出或对象初始化顺序错误查看线程栈水位、检查自动初始化等级这里特别要注意堆栈溢出问题。RT-Thread的每个线程栈是在创建线程时从系统堆里分配的如果配置的栈大小不够运行到深层调用时就会踩到栈边界表现为随机性很强的跑飞或HardFault。排查时用list_thread命令看栈峰值或者打开栈溢出检测往往一抓一个准。4.4 用日志锚点二分法缩小启动故障范围如果启动卡死的问题无法用调试器直接定位比如量产现场、无调试接口我的做法是布日志锚点。做法很简单在启动流程的每个关键节点时钟初始化完、外设初始化完、进入main、调度器启动分别给一个不同的LED闪烁模式或串口字符输出。设备出问题时看它停在哪个锚点之后就能把故障范围缩小到两个锚点之间。再配合二分法在可疑区间中间再加锚点最多两三轮就能锁定故障函数。这套方法听起来简单但它的前提是你对启动流程足够熟悉知道在哪几个位置埋锚点才有区分度。这也回到这篇的核心没有启动流程的完整认知你连锚点都不知道该埋在哪。5. OTA升级工程化落地双分区、回滚与永远变砖不了的底线设计5.1 OTA不是把新固件写到Flash这么简单OTA升级的第一反应往往是用新固件覆盖旧固件。但仔细想想这里有个致命问题如果你的程序直接在Flash里执行XIP而你正在擦写自己所在的区域指令都没法取了系统瞬间崩掉。退一步说就算程序运行在RAM里、闪存也支持原地擦写只要升级过程中断比如用户突然断电、网络掉线、写入错误Flash里就是一个残缺的镜像设备就变砖了。所以OTA工程化的本质不是写Flash这一个动作而是一套保证升级过程在任何异常情况下都不会让设备变砖的状态机。核心手段就是分区方案、引导选择、完整性校验和回滚机制。5.2 A/B双分区用一份硬件成本换一份安全感业界最常见的方案是A/B双分区。具体做法是在Flash里规划两个应用分区App A和App B外加一个控制信息分区OTA Data。当前运行在A分区升级时把新固件写入B分区写完后在OTA Data里记录下次从B启动重启后bootloader按记录选择B启动。这个方案的关键收益有两点运行分区和写入分区分离写坏B分区不影响A分区继续跑。天然支持回滚新固件启动后需要在规定时间内上报我起来了、我很健康bootloader才会把B分区标记为有效如果新固件起不来或没上报bootloader自动回退到A分区。代价是Flash空间翻倍。但在量产产品上这点硬件成本通常远低于变砖返修的成本。5.3 基于ESP32的OTA工程实践ESP32的OTA架构非常典型很适合用来理解这套机制。它默认支持两个App分区app0、app1和一个otadata分区。核心API调用链是这样的const esp_partition_t *partition esp_ota_get_next_update_partition(NULL); esp_ota_handle_t handle; esp_ota_begin(partition, OTA_SIZE_UNKNOWN, handle); // 循环接收数据 esp_ota_write(handle, data, len); esp_ota_end(handle); esp_ota_set_boot_partition(partition); esp_restart();每一行的背后都有讲究esp_ota_get_next_update_partition会帮你在两个App分区里选出当前没在运行的那个作为写入目标。esp_ota_begin传入OTA_SIZE_UNKNOWN表示需要芯片在写入过程中维护版本信息实际生产中建议传整个镜像大小便于内部做空间判断。esp_ota_write按块写入注意这里要按Flash扇区对齐最好做好4KB对齐处理。esp_ota_end会对整个镜像做校验校验失败返回错误不会设置启动分区。esp_ota_set_boot_partition只是把otadata里的启动指向改了并没有真正让新固件生效。最关键的一步在重启之后新固件起来必须尽快调用esp_ota_mark_app_valid_cancel_rollback()。如果你使能了CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE而新固件迟迟不标记有效bootloader在下次复位时会判定新固件不可用自动回滚到旧分区。这个机制就是OTA防变砖的最后一道保险。我建议在量产代码里这样处理main函数早期完成基本硬件初始化串口、Flash、网络后立即调用mark valid再继续完成剩余启动。太早调用有风险——如果后续初始化必崩但你已经标记有效了太晚调用又可能触发超时回滚。一般把系统能稳定运行、日志能输出作为标记有效的最低标准。5.4 生产环境必须考虑的五个升级细节第一镜像完整性校验。不只是OTA结束后校验一次bootloader在每次启动时都应该对要启动的镜像做签名或CRC校验。一旦发现镜像被截断或损坏直接回滚到另一个分区。第二断电保护。写入过程中断电是最常见的事故。A/B双分区天然防护写入中断电但要注意控制信息分区otadata的写入原子性。ESP32的otadata分区有两个独立的入口就是为了避免只写了一半的状态。第三升级过程中的看门狗。如果你在升级流程里喂了独立看门狗要确保擦写Flash这种长耗时操作不会导致看门狗超时复位。一个经验做法升级期间暂停喂狗但给升级流程设置一个更长的整体超时超时则放弃升级并重启。第四版本管理与回滚策略。回滚不是永远回滚到上一个版本要考虑到旧版本可能存在已知安全问题。生产环境通常会做版本下限检查新版本写入了防回滚计数器旧版本无法覆盖新版本。但这套机制引入后要非常小心一旦启用防回滚调试器写旧镜像也会失败会给你日常开发调试带来额外障碍。第五升级过程中的日志与状态上报。升级进度、失败原因、当前分区状态这些信息一定要能远程拿到。我见过很多设备OTA失败但设备端连失败在哪一步的日志都没有只能靠猜。6. 上篇课后思考题完整解析六道题背后的知识点图谱上篇发出后不少读者在评论区提交了课后思考题答案。这里我把六道题完整解析一遍——不只是给答案更要把每道题背后的知识点重新串一次因为这几道题本质上是我给整个启动流程拆解埋的六颗钉子。6.1 题目一为什么复位后PC取的是0x00000004位置的值而不是直接从0地址执行这道题考的是Cortex-M的向量表机制。0地址存放的是初始栈指针MSP0x00000004存放的是复位向量。CPU复位后的第一条指令不是从0地址取的而是先读0地址的值装入SP再读0x00000004的值装入PC。这背后的设计逻辑是CPU需要先建立栈环境才能响应中断、执行函数调用而栈顶地址和复位入口必须在CPU上电的第一时间就确定下来。进一步延伸这道题还隐含了一个考点向量表是可以重定位的通过VTOR寄存器修改这也是所有Bootloader跳转App前必须做的一步。如果跳转前忘了重设VTORApp里第一个中断进来就会跑飞。6.2 题目二INIT_BOARD_EXPORT和INIT_APP_EXPORT导出的函数到底谁先执行这道题考的是RT-Thread自动初始化的等级机制。答案很明确INIT_BOARD_EXPORT先执行而且是在rt_hw_board_init()内部通过rt_components_board_init()调用的INIT_APP_EXPORT要等到rt_components_init()执行时才被调用此时系统堆、调度器都还没完全就绪。两者的段位置不同.rti_fn.0与.rti_fn.5执行时机也不同。这个顺序决定了你的驱动注册函数里能不能调用内核对象函数在BOARD阶段不能在APP阶段可以。很多RT-Thread新手在这里踩坑报assertion failed多半就是等级用错了。6.3 题目三IMX6板子烧了uboot但串口无输出排查顺序怎么排这道题没有唯一答案但有一条标准排查链路我给出我的顺序。第一步确认Boot模式引脚拨码正确确保Boot ROM确实从你烧录的介质加载第二步确认镜像放置位置和IVT偏移正确比如SD卡从1KB偏移放IVT放错位置Boot ROM根本找不到第三步检查DCD尤其是DDR初始化参数DCD错最典型的现象就是uboot的二进制明明在介质里却没有任何输出——因为代码还没跑到uboot入口DDR都没起来。第四步才轮到怀疑uboot本身。这个顺序背后是一个判断串口无输出意味着uboot代码大概率没有开始执行或者执行环境DDR压根不存在。6.4 题目四HardFault时LR0xFFFFFFF9与LR0xFFFFFFFD有什么区别这道题考的是异常现场还原。0xFFFFFFF9表示故障前运行在Thread模式且使用的是主栈指针MSP0xFFFFFFFD表示故障前运行在Thread模式但使用的是进程栈指针PSP。在RT-Thread中线程运行一定用的是PSP所以看到0xFFFFFFFD你需要从PSP指向的栈区去读取压栈的8个字R0-R3、R12、LR、PC、xPSR才能拿到真正的故障PC。如果误用了MSP读出来的是中断嵌套现场或主栈内容定位方向整个就错了。这也是我反复强调先看LR再取栈的原因。6.5 题目五为什么OTA不能直接擦除正在运行的应用分区这道题的核心是XIP片上执行机制。Cortex-M和大多数MCU都是直接从Flash取指执行的如果你把当前正在运行的分区擦除了CPU下一条指令就取不到了系统直接崩溃。即便有些平台支持代码在RAM里跑擦除正在运行的分区也破坏了异常处理和中断向量表所在的区域任何一次中断都会导致跳转到空地址。所以OTA必须采用运行A、写B、再切换的分区策略让写入目标和执行目标物理隔离。这道题延伸出来就是A/B分区的设计动机理解了它你就理解了为什么OTA升级要额外占一份Flash空间。6.6 题目六OTA升级完成后立刻断电设备下次启动会发生什么这是我最喜欢的一道题因为它直接关系生产环境的可靠性。答案是设备大概率会回滚到旧版本。原因是新固件还没有机会调用esp_ota_mark_app_valid_cancel_rollback()来标记自己有效otadata里的状态仍然是待确认。bootloader在下一次启动时发现新固件未被确认或者启动次数计数器超限就会判定升级失败自动从另一个分区启动旧固件。这个过程对用户是透明的设备表现为升级了但版本号没变。这个机制在绝大多数场景里是好事但你在设计产品时要考虑一个体验问题如果升级内容包含不可变不可逆的数据迁移旧固件回滚后面对新数据格式可能出问题。所以在回滚策略上要把固件版本和数据格式版本设计成能协同配合的状态机而不是各管各的。我自己做OTA方案时最后总会做一个终极测试清单升级一半拔电、升级完成立刻拔电、下载失败重试、反复升降级、连续升级十次每个场景都要有明确的预期结果和日志输出。等你跑完这份清单才算真正理解了OTA升级工程化这几个字的重量。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →