ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

嵌入式固件启动流程、HardFault定位与OTA升级实战解析

嵌入式固件启动流程、HardFault定位与OTA升级实战解析 上周在客户现场排查一块跑 RT-Thread 的板子现象特别诡异上电后大约 5% 的概率卡死按复位键大概率能起来但偶尔又起不来。用调试器挂上看有时候死在启动文件里有时候死在 main 之前的库初始化还有时候跑进 RTOS 调度之后才崩。折腾了一下午最后定位到是一颗外置电源芯片的使能时序在低温下不满足规格导致内核电压建立缓慢Cortex-M 在复位释放后有一段窗口期非法访问总线。这个案例让我再次确信嵌入式固件的问题十有八九要回到启动流程、异常现场和升级链路这三件事上去找答案。这也是本专栏把这三块内容放在一起的原因。如果你做过一段时间 MCU 或 SoC 开发应该会发现一个规律——很多看起来毫无头绪的 bug本质上都是对芯片上电后究竟按什么顺序执行、出错后现场在哪、固件怎么从旧版本安全切换这三条线索理解得不够深。这篇文章是专栏的第三篇连载接上篇留下的思考题我按实际项目里最常遇到的几个场景来展开先是 Cortex-M 和 RT-Thread 的启动初始化全流程拆解再对比 i.MX6 这类带 uboot 的 SoC 启动链路接着讲 HardFault 发生后的具体定位手段最后把 OTA 从分区规划、签名验签到失败回滚的整体工程做法完整梳理一遍。文末会把上篇布置的思考题逐题解析参考答案都是基于真实工程实践不是教科书套话。如果你是刚转嵌入式、想系统搞懂固件底层逻辑的开发者建议从第一节顺序读如果你已经在负责产品维护可以直接跳到故障定位和 OTA 两节这两部分会以踩坑视角为主尽量帮你少走弯路。1. 从向量表到调度器Cortex-M 启动流程里的每一条路都要心里有数1.1 复位后的前三笔事务SP、PC、VTORCortex-M 内核的启动流程和传统 ARM7/ARM9 不一样它不是上电后直接跳去执行 bootloader 代码而是由硬件自动完成一个取数动作。芯片复位释放后内核会从地址 0x00000000 处读取初始栈指针 SP从地址 0x00000004 处读取复位向量也就是 Reset_Handler 的地址然后跳过去执行。这个机制很多内核文档里一句话就带过了但实际项目中恰恰是这里容易埋雷。如果你把向量表放在片内 Flash 起始地址0x00000000 对应的是 Flash 首地址这没问题。但一旦你启用了 BootLoaderAPP 的向量表往往不在 0x00000000而是偏移到 0x00008000、0x00010000 这类位置。这时如果 APP 的启动代码里没有重新设置 VTOR向量表偏移寄存器地址 0xE000ED08中断一发生内核还是会去 0x00000000 取向量取到的就是 BootLoader 的向量表中断处理必然错乱。这个坑我见过太多次尤其是从标准库工程迁移到 CubeMX 工程时SCB-VTOR FLASH_BASE | VECT_TAB_OFFSET这行代码被优化掉或者写错位置导致中断完全不可用。再看 Reset_Handler 内部它一般做四件事拷贝 RW 段、清零 ZI 段、调用 SystemInit 初始化时钟、跳转 main。MDK 环境下这些动作大多由 __main 库函数自动完成启动文件里只需要调用__main即可。GCC 环境下则是_start配合链接脚本完成同样的工作。这里我想提醒一点不要迷信启动文件是编译器自动生成的不用管。当你用到自定义链接脚本、将代码放在外部 SDRAM、或做 bootloader app 双区运行时启动文件里段拷贝的前提条件、栈指针的初始值、以及是否需要提前初始化外部存储控制器这些都会直接影响系统能不能跑起来。RT-Thread 的 bsp 里已经帮你处理好了大部分情况但理解它为什么这么写比背下来更重要。1.2 RT-Thread 的启动初始化从 main 到调度器的路线图RT-Thread 启动流程相对清晰它把大部分初始化工作封装在了rtthread_startup()函数里。用户代码里的 main 函数通常只做两件事调用rtthread_startup()然后返回。真正的工作从rt_hw_board_init()开始这里完成系统时钟、堆内存初始化紧接着是定时器、调度器、信号量等内核组件初始化最后创建主线程由rt_application_init()调用用户main的线程版本再启动调度器。有几个点需要特别留意。第一rt_hw_board_init()里通常会用rt_system_heap_init()设置系统堆的起始地址和大小。如果外部 RAM 在此时还没有完成初始化而你又把堆放在外部 RAM那系统在第一次分配内存时就会 HardFault。第二调度器启动之前rt_assert里的断言、定时器初始化不能使用会阻塞的线程原语。第三main 线程的栈大小默认值在某些 bsp 里设置得偏小如果你在 main 里定义了较大的局部变量或递归调用较深就会发生栈溢出而 RT-Thread 的栈溢出检测默认情况下只在开启RT_USING_DEBUG时才比较敏感。从 Cortex-M 到 RTOS 的过渡还有一个底层细节值得展开。Cortex-M 的线程模式和处理模式分别对应运行 RTOS 线程和中断/异常。RT-Thread 在启动第一个线程时会触发 SVC 异常借助异常返回机制将内核从处理模式切换到线程模式同时选择使用 PSP进程栈指针。如果你需要做底层调试特别是看线程切换现场时不理解 EXC_RETURN 代表什么会很难判断当前 CPU 正在用哪个栈、处于什么模式。这一块我会在第三节故障定位里继续深入。1.3 启动流程中常见的时序病时钟、引脚与供电启动流程表面上是代码问题实际上很多是硬件时序问题。上面那个客户案例是最典型的例子内核供电没有在复位释放前稳定到工作电压导致 CPU 取第一条指令时总线读到乱码表现就是随机卡死在不同阶段。排查这类问题不要一开始就死磕代码而是先看供电时序、复位信号释放时间、以及时钟稳定时间。逻辑分析仪或者示波器同时抓三路信号电源轨、NRST、主时钟输出能快速确认是否是时序问题。另一个常见的启动期故障是引脚复用冲突。芯片上电瞬间所有 IO 默认状态是浮空或者上拉如果某个引脚正好控制外部设备的关键使能信号在 main 初始化之前被异常拉高或拉低设备可能进入错误状态。比如你用 PA8 做外部 Flash 的片选但 Flash 是低有效片选上电瞬间 PA8 处于浮空电平不确定Flash 可能被误选中并响应了错误的 SPI 指令导致后续通信句柄拿不到正确 ID。解决方法是选用默认电平正确的引脚或者在外部加上下拉电阻或者在 startup 阶段尽快把关键引脚配置为确定状态。2. SoC 级启动链路i.MX6 的 IVT 和 uboot 到底比 MCU 复杂在哪2.1 Boot ROM 的行为i.MX6 的 IVT 与启动设备选择从 MCU 跨越到应用处理器SoC最明显的区别就是Cortex-M 的启动过程由内部 Flash 的向量表直接接管而 i.MX6 这类芯片没有内部 Flash它内置了一段 Boot ROM上电后由 Boot ROM 先运行然后根据 BOOT_CFG 引脚或 FUSE 的配置从 SD/eMMC、NOR Flash、NAND、USB 等设备中加载下一级程序。i.MX6 的 Boot ROM 会先在存储介质中寻找 IVTImage Vector Table。IVT 是一个固定格式的结构体包含自校验信息、跳转地址、DCD 地址、Boot Data 地址等字段。Boot ROM 读到 IVT 后会根据 DCDDevice Configuration Data来完成 DDR 控制器的初始化然后再把外部的 bootloader 镜像拷贝到指定内存地址并跳转执行。这个流程在 i.MX6UL 等型号上尤为典型。千万注意DCD 如果写得不对DDR 训练失败或时序参数错误整个系统就会卡在 Boot ROM 阶段而且多数情况下串口没有任何输出很容易被误判为 芯片没贴上。如果你用的是 NXP 官方 Yocto 或 MCUXpresso SDK这些细节已经被编译工具链封装好了。但在做产品定制时比如修改板级 DDR 容量、更换 SDRAM 颗粒型号你必须同步修改 DCD 参数否则启动到一半就死。实测经验是先把官方评估板的 DCD 跑通再逐项改为自己板子的参数一次只改一项每改完就验证一次内存读写稳定性不要同时动多项参数。2.2 uboot 的启动流程SPL 与 FIT 镜像与 MCU 上的 RT-Thread 启动相比uboot 要分的层更多。i.MX6 上典型的启动链是 Boot ROM - SPL如果启用- uboot - 内核。SPLSecondary Program Loader是一段精简版 uboot主要在 DDR 还未初始化时负责最基础的板级初始化然后把完整 uboot 从启动介质拷贝到 DDR 中。board_init_f 阶段负责时钟、串口、DDR 等最小系统初始化board_init_r 阶段则是完整的设备模型加载环境变量、解析设备树最后通过 bootm 命令启动 Linux 内核。搞 uboot 启动最容易出问题的点往往是设备树和外设驱动。系统启动到 uboot 后如果串口没有输出先查 uboot 的 baudrate 与终端是否一致、调试串口对应的设备树节点是否 disable 状态。uboot 中saveenv的环境变量如果设置了错误的 bootcmd比如内核镜像路径不存在会出现卡死在 Hit any key to stop autoboot之后无响应的假死现象。排查时优先使用printenv检查变量ls mmc 1:2 /boot确认镜像存在再执行boot代替自动启动逐步缩小范围。开个不算偏题的玩笑从 MCU 转到带系统 SoC 开发的工程师最容易犯的错就是把 MCU 的裸机思维搬到 uboot 里总觉得上电后应该直接进入用户 main看到 uboot 界面就以为系统已经启动。实际上 uboot 只是个引导程序它的职责是准备好运行环境、跳进内核真正可靠的标准是打出 bootargs 传递正确、设备树加载成功、内核开始打印。在调试阶段建议打开bootargs里earlyprintk和ignore_loglevel会让崩溃前的最后输出明显清楚很多。2.3 MCU 启动与 SoC 启动的对照表为了帮大家快速建立整体图景我把两种系统的启动关键差异整理成了一张对照表。实际项目里无论是做 RTOS 还是跑 Linux这张表都值得贴在工位上。对比项Cortex-M MCU如 STM32、RT-ThreadSoC如 i.MX6 uboot Linux启动源头芯片内部 Flash 固定地址向量表芯片内 Boot ROM 根据引脚/FUSE 选择介质第一阶段代码Reset_Handler启动文件Boot ROM 加载 SPL 或直接加载 uboot存储介质内部 Flash可选外部 SPI FlashSD/eMMC、NAND、NOR、USB 等是否需要 DDR可选取决于应用必须DRAM 初始化是启动前置条件二级加载器可无或用户自写 bootloaderuboot SPL / ubootOS 加载RTOS 镜像直接烧录无动态加载支持 FIT/设备树/ramdisk架构更复杂常用调试手段调试器断点、HardFault 现场、串口uboot 命令、设备树 overlay、内核 printk理解这张表之后你会发现一个规律无论是 MCU 还是 SoC启动流程的核心从来不是把代码跑起来而是在正确的时间点把硬件资源初始化到可用状态。启动期崩溃的排查思路是同一套逻辑差别只在排查工具的手段上。3. 故障定位方法论HardFault 现场比起猜更重要的是还原3.1 HardFault 处理函数不打印等于白搭很多工程师在 Cortex-M 上做故障定位时第一反应是用调试器看当前 PC 在哪。这个做法在开发调试阶段有效但到了产线故障、客户现场、设备偶发死机时根本没有调试器可挂。真正的工程化做法是从项目初期就写好一个足够好用的 HardFault 处理函数把异常现场保存到内存或 Flash再通过串口上报。只有把定位手段前置到这个程度遇到偶发问题时才有东西可查。HardFault_Handler 里最核心的任务是抓取压栈的寄存器。Cortex-M3/M4 在进入异常时硬件会自动把 xPSR、PC、LR、R12、R3~R0 这八个寄存器压栈如果使能了 FPU还会一并压入浮点寄存器。关键在于你需要判断当时用的是 MSP 还是 PSP。判断依据是 LR 里的 EXC_RETURN 值0xFFFFFFF9 表示使用 MSP0xFFFFFFFD 表示使用 PSP0xFFFFFFF1 则表示处于 Handler 模式。拿到正确的栈指针后从栈帧里解析出 PC 和 LR就能反推故障发生的位置。3.2 一次真实的 HardFault 定位过程野指针调用与栈回溯分享一个实际例子。某产品在持续运行 5~8 小时后偶发死机串口在死机前最后一条日志是某条传感器数据更新。按照先抓现场的思路我们在 HardFault_Handler 里打印了 MSP/PSP、EXC_RETURN、栈帧里的 PC 和 LR以及栈顶附近 32 个字的原始数据。重启后看到打印故障 PC 指向了一个不可能的地址——0xFFFFFFFF 附近明显是函数指针被踩坏。通过 CMBacktrace 的自动栈回溯功能我们拿到了更完整的调用链发现 PC 是从一个任务回调入口跳过去的而该回调的注册应该发生在初始化阶段。顺着调用链检查最终定位到一块内存越界写某个结构体数组的索引在极端时序下超界正好把回调函数指针区域覆盖了。这个案例说明一个方法论HardFault 的 PC 不一定直接指向 bug 所在行但它一定是第一现场。拿到 PC/LR 后优先在反汇编窗口里看这条指令附近访问了哪个地址、调用了哪个函数然后沿着 LR 回溯调用链比漫无目的地搜索日志有效得多。3.3 栈顶标记与内存边界检查把故障消灭在可检测范围HardFault 定位得再准确也属于事后补救。更高级的做法是在系统设计时就让故障更早暴露。常见的两个手段是栈填充标记和内存保护单元MPU。栈填充标记的思路很简单在系统初始化时把任务栈区域全部填充为固定值比如 0xDEADBEEF 或 0xCD。定期或在任务切换时扫描栈空间末尾一段区域如果发现填充值被改写说明栈已经溢出到了一定深度。RT-Thread 的rt_thread_delay等调度点会检查栈顶标记但这个机制只能检测溢出后的情况。很多团队会再加一个水位线统计实时确认每个任务实际使用了多少栈这能帮你在开发阶段就选对栈大小而不是上线后靠偶发死机来教训。MPU 的用法更硬核把关键数据区域、外设寄存器区域、栈区配置成不同的访问权限和内存属性。比如把任务栈所在区域设置为写入后立即产生 MemManage 异常的权限越界访问会在发生时直接被捕获而不是等几小时后系统慢慢崩坏。STM32 的某些系列支持 MPU 的 privilege 属性和不可执行属性对于防止数据区被当代码执行这类问题非常有效。不过 MPU 配置要特别小心区域重叠和总线操作类型我在项目中见过因为 MPU 配置不当导致 CPU 正常工作也被异常打断的情况所以做 MPU 裁剪前一定要先读懂 ARM 内存模型。3.4 中断中崩溃的额外陷阱双错误和总线错误如果 HardFault 发生在中断处理函数内问题会更棘手一些。这种情况下栈帧结构、压栈的寄存器来源、以及对嵌套异常的处理都和处理线程模式下的异常不同。一个特别常见的坑是在中断处理函数里触发了总线错误比如访问外部外设地址时总线未使能引发 HardFault如果 HardFault 处理函数本身又访问了非法外设就会触发双错误Double Fault最终进入 Lockup 状态。Lockup 是芯片彻底死掉的状态调试器只能通过复位恢复。避免 Lockup 的一个实用措施是在 HardFault_Handler 里不要做复杂操作只做最必要的现场保存然后死循环等待外部看门狗复位或人为介入。后续通过非易失存储的上报数据来定位。另外如果你用到了外部存储控制器建议把对应外设的时钟开关状态也保存到现场里很多总线错误其实是因为时钟被关闭了还去访问寄存器。4. OTA 升级工程化从全量包、差分包到加签验签与失败回滚4.1 先搞清分区表OTA 的根不在于下载而在摆放OTA 升级最核心的设计决策不是用哪颗芯片、跑什么协议而是 Flash 分区表怎么分。很多项目第一次做 OTA 时只划分了 bootloader 区和 app 区然后把下载的固件直接覆盖 app 区。这种方式最大的问题是升级过程中一旦断电或写入失败app 区既不是完整的新固件也不是完整的旧固件设备变砖。工程上至少要有三块区域Bootloader 区、APP 区、Download下载缓存区。升级流程是先把新固件下载到 Download 区做完整性和签名校验校验通过后再把新固件搬运或切换到 APP 区。更进一步的方案是 A/B 分区或者说双槽方案设备维护两份可运行的 APP分别叫 slot A 和 slot BBootloader 根据标志位决定从哪个 slot 启动。升级时往当前不用的 slot 写入新固件写完后更新标志位重启后 Bootloader 从新 slot 启动如果新固件运行失败可以由 Bootloader 自动回退到旧 slot。这种方案的代价是 Flash 占用翻倍但对于要求高可靠性的产品这笔账是值得的。分区表大小要根据具体 Flash 容量精打细算。以 16MB SPI Flash 为例我建议这样分配Bootloader 区 64KBAPP A 区和 APP B 区各 4MB或者不启用双槽时留一个 download 区 4MB标志/参数区 64KB剩余部分做文件系统或数据存储。标志区建议单独放在一个扇区并使用多份备份和三态标志待升级、可启动、启动失败防止标志区写入一半时断电导致状态混乱。4.2 升级流程设计下载、校验、切换三步之间必须有的保护OTA 的完整流程可以抽象成三句话先下载到安全区域再校验完整性最后做切换。但每一句背后都有大量细节。下载阶段我推荐用断点续传和分段校验。常见做法是固件包按 256 字节或 1024 字节分块每一块带 CRC32 或基于块号的校验值接收方逐块校验后写入 Flash。如果中间断网或断电重新启动后可以从最后一块有效位置继续而不是从头下载。在资源受限的 MCU 上这个分块写入还能和 Flash 的扇区大小对齐减少读改写操作延长 Flash 寿命。校验阶段完整性校验比如 SHA-256只解决数据有没有损坏的问题不解决数据是不是可信的问题。如果你面对的是可以物理接触设备的恶意攻击者或者升级包可能被替换的场景必须加上签名验签。常用的做法是用 ECDSA P-256 或 RSA-2048 对固件摘要做签名Bootloader 里烧录对应的公钥升级时先验签再安装。工程上要注意公钥一旦被替换整个信任链就崩溃了所以公钥区最好由 Bootloader 写保护并提供防回滚机制——版本号只能递增禁止从高版本降回低版本否则攻击者可以把设备降级到带漏洞的旧固件上。切换阶段细节更多。如果采用 Single Slot Download 区设计搬运固件时要考虑搬一半断电的情况因此搬运前要把标志区写成升级中搬运完成后再写成升级完成可启动。Bootloader 在每次启动时读到升级中标志就知道上一次升级没有结束可以用 Download 区残留的数据再试一次或者直接选择回退到旧版本。如果采用 A/B 分区切换就是改一个 slot index然后复位。之后新固件启动后应用层要主动上报运行正常Bootloader 才会把新 slot 标记为 confirmed否则下一次重启会回退。4.3 ESP32 与 STM32两种平台 OTA 落地的典型差异ESP32 的 OTA 机制在 MCU 里算做得比较完善的。它内部有专门的 ota_data 分区来记录当前启动的是哪个 app slotesp_ota_ops和esp_https_ota库封装好了下载、校验、切换、回滚的完整流程。使用 ESP32 做产品时我建议优先用官方例程改而不是自己从头写 OTA。官方库处理了很多边缘情况包括分片写入 NVS、标记 app 为 invalid 实现回滚等。STM32 没有统一官方 OTA 方案多数项目要靠自己落地。关键差异在于 STM32 的向量表偏移必须正确设置并且 Bootloader 跳转 App 时要关闭全局中断、恢复默认栈顶、重新设置 VTOR。这些步骤顺序如果错了App 启动后中断异常无法解释。STM32 的 IAP 通常通过 UART/USB/CAN 接收固件下载到片内 Flash 或外部 SPI Flash。我更推荐把新固件先放到外部 SPI Flash校验通过后再写入内部 Flash因为内部 Flash 的擦写次数有限直接边下载边写内部 Flash 风险太高。4.4 汽车嵌入式场景的额外要求加签验签只是起点汽车嵌入式软件的 OTA 对安全性和一致性的要求更高。除了签名验签还要考虑固件加密传输、安全启动、刷新会话管理、以及和诊断协议比如 UDS的配合。汽车 ECU 做刷写时不太可能像 MCU 那样直接操作一个 UART 下载固件而是先建立诊断会话做安全访问认证再通过 34/36/37 服务完成请求下载、数据传输、退出传输三个步骤。这一层我虽然没有在纯 MCU 产品里全部落地过但和 Tier1 合作时学到的一个重要思路是加签验签只是起点真正难的是如何在被测设备上建立一个认证-授权-传输-存储-执行-回滚的完整链。对应到普通嵌入式产品至少要做到公钥不可被应用层篡改、固件包有独立版本号、升级状态掉电可恢复、回滚后有日志可查。这些要求不是汽车独有的只是汽车行业把标准量化得更明确其他行业可以拿来当参考。4.5 OTA 实测中的三个反直觉教训第一下载协议用 HTTPS 比自定义 TCP 校验更省心因为 TLS 本身提供了完整性保护还可以通过 CA 证书防止中间人替换固件包。第二升级过程中的看门狗要处理好很多方案在写入 Flash 时因为耗时过长触发看门狗复位导致升级永远失败。解决方式是升级期间暂停喂狗或延长看门狗超时但暂停喂狗时要有配套的死循环保护比如超时强制失败回滚。第三稳定的 OTA 版本切过去不代表万事大吉建议新固件在运行 24 小时后才允许被 confirmed否则某些时序问题或内存泄漏会导致设备在刚升级后表现正常、运行半天后崩溃此时再回滚已经晚了。5. 上篇思考题完整解析四道题覆盖启动、崩溃与升级的真实场景上篇专栏留了四道思考题不少读者在评论区给了自己的答案有些思路很好有些则踩了典型的坑。下面逐题展开我的分析和参考答案不保证是唯一正确答案但每道题的思考路径都来自实际项目验证。5.1 为什么 Cortex-M 复位后要从 0x00000000 取栈指针而不是直接跳 C 语言的 main标准答案Cortex-M 的硬件设计基于中断向量表在地址 0的模型。地址 0x00000000 存放初始栈顶地址 0x00000004 存放复位向量这是架构约定。硬件复位后先把栈顶加载进 MSP是为了让后续的 C 函数调用有栈可用因为 C 语言运行的前提是栈已经初始化。如果直接跳 mainmain 函数第一条指令使用栈时比如保存局部变量MSP 还是未知状态必然异常。这道题更深层的意义在于提醒你启动文件不是可有可无的模板代码。GCC 环境下如果你做了-nostartfiles并且自己实现了_start一定要记得设置栈指针。很多嵌入式开发者第一次做 freestanding 环境时在这里翻车。5.2 当 BootLoader 跳转到 APP 时为什么要先关中断、再设置 VTOR标准答案如果不关闭全局中断跳转过程中一旦产生中断CPU 会拿着 BootLoader 的向量表去处理中断而中断服务函数可能已经在 App 中被重新实现了地址不匹配轻则中断丢失重则跳飞到非法地址 HardFault。设置 VTOR 是为了让内核知道新向量表的位置只有向量表切过去后中断才能按 App 的设计运行。实际操作中有一点经常被忽略跳转前要把 SysTick、外设中断等所有在 BootLoader 阶段打开的中断源全部关闭。我曾经遇到过 BootLoader 里开了 UART 接收中断跳转时没有关App 启动后不断收到残留字符产生莫名其妙的中断行为。真正稳妥的跳转流程是关全局中断 - 关闭外设时钟可选 - 设置主栈指针 - 设置 VTOR - 重新使能中断。5.3 中断里的 HardFault 和普通线程里的 HardFault现场还原有什么不同标准答案区别在栈指针和栈帧来源。普通线程模式下发生异常压栈使用的是 PSP通过 LR 的 EXC_RETURN0xFFFFFFFD可以判断中断/异常处理过程中再发生异常压栈使用的是 MSPEXC_RETURN 为 0xFFFFFFF9。如果故障发生在更高优先级异常抢占后栈中可能嵌套多层栈帧解析时要从当前 SP 逐层向外找不能只取最近一层。另外一个容易忽略的问题是如果使能了 FPU压栈的寄存器还包含浮点寄存器组S0~S15 等此时栈帧长度和普通异常不同解析 PC 的偏移量要对应调整。否则打出来的 PC 是错的定位结论自然也不对。5.4 为什么不建议在升级时直接覆盖 App 区标准答案覆盖写是一个不可逆的破坏过程。写入过程中一旦掉电或擦写异常App 区就变成半成品Bootloader 既无法跳转新版本也无法回退旧版本。而使用 download 缓存区加标志位的方式整个过程可中断、可恢复、可回滚。Bootloader 每次启动时先查标志如果发现上次升级未完成可以重新尝试从缓存区恢复或者直接启动旧版本。这比任何断电保护都更可靠。如果还想更稳妥可以把标志区设计成三个独立扇区轮换写入避免频繁擦写同一扇区导致磨损不均。实测下来批量产线上即使偶发升级断电只要标志区设计对了返修率几乎可以降到零。这也是我每次做 OTA 时最先检查的一项。从启动流程到故障定位再到 OTA 工程化这三块内容表面上看是三个独立主题实际是一条完整的工程逻辑链启动代码管的是系统怎么活着异常定位管的是活着出问题时怎么查账OTA 管的是怎么安全地换一种活法。我自己在带团队时会要求每个固件工程师拿到一块新板子的第一周必须做三件事画启动流程图、写一个 HardFault 现场打印函数、给板子规划一个合理的 Flash 分区表。这三件事做完后面大部分疑难杂症都会好查很多。下一期打算聊聊嵌入式设备的内存管理方法包括内存池设计、内存泄漏检测和 RTOS 下的内存调优如果你们有特别想看的主题也可以直接评论区告诉我我按照实际项目经验来安排。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进