嵌入式固件启动失败与OTA变砖的底层根因分析
2026/9/11 23:15:45 网站建设 项目流程

1. 这不是“讲启动流程”,而是嵌入式固件工程师的生存现场

你有没有经历过凌晨两点,手边摆着三块开发板、两台逻辑分析仪、一份没注释的汇编启动代码,和一个永远不报错但就是卡在0x08000000不动的串口log?我做过——那是在给一款基于Cortex-M4的工业网关做量产固件交付时,客户现场反馈“上电后LED不亮,串口无任何输出”。没有panic,没有assert,连第一个printf都没打出来。它就安静地死在了reset handler入口之前。

这不是理论题,是嵌入式固件工程师每天面对的真实战场。CSDN专栏标题里写的“启动流程深度拆解”,绝不是照着ARM ARM手册逐行翻译Reset Vector Table;“故障定位方法论”,也不是罗列几个GDB命令;“OTA升级工程化实战”,更不是调个esp_http_client_send_get就完事。它是一整套从芯片上电那一刻起,到用户点击“升级完成”按钮之间,所有可能崩塌的环节、所有必须踩实的脚手架、所有被教科书刻意忽略的灰色地带。

我用这个专栏内容,在过去三年里带过17个硬件团队完成固件交付,覆盖STM32F4/F7/H7、NXP i.MX RT系列、ESP32-C3/C6、全志H616、瑞芯微RK3566等十余种平台。最深的体会是:启动流程不是一段代码,而是一条由供电质量、时钟树配置、内存映射、向量表校验、栈指针初始化、C运行时环境构建共同编织的脆弱链条——断掉任意一环,系统就静默死亡,且不给你任何线索。而OTA,从来不是“把新固件写进Flash”,它是对这条链条的二次压力测试:你得确保升级过程本身不会破坏启动链,还要让失败能回滚、能诊断、能被用户感知。

所以这篇博文,不讲概念,不列大纲,不画流程图。我们直接切进三个最硬核的实战切口:

  • 为什么你的bootloader总在跳转到app前崩溃?(不是代码问题,是SP寄存器被悄悄改写了)
  • 为什么用J-Link烧录成功的固件,用UART ISP升级后就跑飞?(BootROM校验逻辑与你的Flash擦除粒度存在隐性冲突)
  • 为什么OTA升级后设备变砖,但用ST-Link读出的Flash数据完全正确?(NVIC中断向量表偏移地址被OTA工具错误重定向)

这些坑,我在专栏上篇课后思考题里埋了伏笔,现在,我把完整的排查链路、底层原理、可复现的验证步骤,全部摊开给你看。你不需要懂ARM汇编,但必须知道SP寄存器在reset后如何被加载;你不需要会写uboot,但必须清楚IVT头在i.MX平台里如何被BootROM解析;你不需要精通密码学,但必须明白AES-ECB模式在固件加密中为何是致命陷阱。

提示:本文所有案例均来自真实量产项目,代码片段可直接用于你的开发板验证。请务必准备一块支持SWD调试的STM32或ESP32开发板,跟着文中的寄存器地址和dump指令动手操作——固件调试,永远是手比眼快。

2. 启动流程的“死亡七分钟”:从POR到main()之间到底发生了什么

很多人以为启动流程就是“复位→跳转到Reset_Handler→初始化→main()”。这就像说“开车就是踩油门→车动了”。但真正让你在高速上失控的,永远是那0.3秒内ABS没介入、ESP延迟响应、轮胎抓地力临界点的微妙变化。嵌入式启动同理——从Power-On Reset(POR)信号生效,到你的main()函数第一行代码执行,中间有7个关键阶段,每个阶段都可能成为静默死亡的起点。我们按时间轴拆解,重点标注那些教科书绝口不提的“魔鬼细节”。

2.1 阶段一:供电与复位信号的物理博弈(0~100ms)

这不是软件问题,是硬件工程师和固件工程师的联合责任区。POR信号的有效性,取决于VDD上升斜率(slew rate)和稳定时间(tVDD)。以STM32F407为例,手册要求VDD必须在10ms内从0V升至1.8V以上,且纹波峰峰值≤100mV。但现实中,你用的DC-DC模块输出纹波可能是150mV,而你的PCB走线又恰好形成LC谐振——结果就是POR信号被抖动触发多次,芯片反复复位。

实操验证法
用示波器探头直接测量MCU VDD引脚,触发方式设为“上升沿+脉宽>5ms”,观察VDD是否平滑上升。若出现阶梯状爬升或高频振荡,立即检查:

  • DC-DC输出电容ESR值(必须<50mΩ,别信标称值,实测!)
  • VDD到GND的去耦电容布局(必须紧贴MCU引脚,走线长度<2mm)
  • 复位芯片(如TPS3823)的RESET引脚是否悬空(必须10kΩ下拉)

我曾在一个车载OBD项目中,因VDD电容ESR实测达120mΩ,导致冷机启动失败率17%。更换为低ESR钽电容后,问题消失。但注意:低ESR电容可能引发DC-DC环路震荡,需同步调整补偿网络——这是硬件与固件的交叉地带,固件工程师必须懂基础电源设计。

2.2 阶段二:时钟树的“信任危机”(复位后第1个周期)

ARM Cortex-M内核复位后,默认使用内部HSI RC振荡器(16MHz),但HSI出厂校准精度仅±1%。这意味着你的SysTick定时器每秒误差可达160ms。更致命的是:HSI频率会随温度漂移,-40℃时可能跌至14.2MHz。而你的Flash编程算法(如STM32的FLASH_ProgramWord)依赖精确的时钟周期计数来控制编程脉冲宽度。一旦HSI漂移,Flash写入可能失败,但错误标志位(BSY/PGERR)却未置位——因为硬件认为“操作已完成”,只是数据错了。

破解方案
必须在启动代码早期(早于任何Flash操作)切换到高精度时钟源。但切换过程本身就有风险:

; 错误示范:直接使能PLL并等待就绪 LDR R0, =RCC_CR LDR R1, [R0] ORR R1, R1, #RCC_CR_PLLON STR R1, [R0] WaitPLL: LDR R1, [R0] TST R1, #RCC_CR_PLLRDY BEQ WaitPLL ; 危险!若PLL未锁相,此处死循环

问题在于:某些低成本晶振(如7ppm精度)在低温下可能无法起振,PLL永远不锁相,系统卡死。正确做法是加超时保护:

MOV R2, #0x100000 ; 1M cycles timeout WaitPLL: SUBS R2, R2, #1 BEQ PLL_Fail ; 超时则降级使用HSI LDR R1, [R0] TST R1, #RCC_CR_PLLRDY BEQ WaitPLL

2.3 阶段三:向量表的“地址幻觉”(复位后第2~3个周期)

这是最常被忽视的致命环节。Cortex-M内核复位后,从地址0x00000000处读取初始SP值,从0x00000004处读取Reset_Handler地址。但这个“地址0x00000000”是虚拟的——它由SCB->VTOR寄存器决定。默认VTOR=0,所以访问0x00000000实际读取的是Flash首地址。但如果你的bootloader将app固件放在0x08008000,且未在跳转前设置VTOR,那么app的中断向量表仍在0x00000000,而那里存放的是bootloader的向量表!结果就是:app运行中触发SysTick,CPU却跳转到bootloader的SysTick_Handler,造成不可预测行为。

验证方法
用J-Link命令行执行:

JLinkExe -Device STM32F407VG -If SWD -Speed 4000 -CommanderScript verify_vtor.jlink

其中verify_vtor.jlink内容:

si 2 mem32 0xE000ED08 1 // 读取VTOR寄存器值(0xE000ED08) r q

若返回值非0x08008000(app起始地址),则向量表未重定向。

工程化补救
在bootloader跳转前,强制重定向VTOR:

// 确保app向量表首地址有效(校验CRC) if (verify_app_vector_table(0x08008000)) { SCB->VTOR = 0x08008000; // 关键!必须在此处设置 __DSB(); __ISB(); // 跳转... }

2.4 阶段四:栈指针的“幽灵篡改”(复位后第4~5个周期)

这是课后思考题第一题的核心陷阱。Reset_Handler执行时,CPU从向量表0x00000000读取初始SP值。但很多开发者会在此处手动修改SP:

Reset_Handler: ldr sp, =_estack ; 加载栈顶地址 bl SystemInit ; 初始化系统 bl main ; 跳转main

问题在于:SystemInit()函数内部可能调用__set_MSP()__set_PSP(),而这些函数会直接修改SP寄存器。如果SystemInit()中某处代码(如配置FPU)意外触发了异常,异常处理程序又使用了错误的栈,就会导致SP被覆盖为非法值。

真实案例
某客户使用STM32H743,SystemInit()中调用HAL_RCC_OscConfig()配置PLL时,因外部晶振未起振,触发HardFault。但HardFault_Handler未正确设置栈,导致SP指向0x00000000,后续所有寄存器压栈操作都写入Flash首地址,彻底破坏向量表。

根治方案
在Reset_Handler中,禁用所有中断,再设置SP,最后启用中断

Reset_Handler: cpsid i ; 关中断 ldr sp, =_estack bl SystemInit cpsie i ; 开中断(此时SystemInit已确保时钟稳定) bl main

2.5 阶段五:C运行时环境的“隐形契约”(复位后第6~10个周期)

main()函数能执行,是因为C库在进入main前做了三件事:

  1. .data段从Flash复制到RAM
  2. .bss段清零
  3. 调用全局对象构造函数(C++)

但这个过程依赖链接脚本定义的内存布局。常见错误是:.data段在链接脚本中定义为*(.data),但实际Flash中该段被压缩存储(如LZ4),而启动代码未解压就直接memcpy——结果RAM中得到一堆乱码。

验证工具链
arm-none-eabi-objdump -h your.elf查看各段地址:

Sections: Idx Name Size VMA LMA File off Algn 1 .isr_vector 00000188 08000000 08000000 00010000 2**0 2 .text 00002a00 08000188 08000188 00010188 2**0 3 .rodata 00000400 08002b88 08002b88 00012b88 2**0 4 .data 00000200 20000000 08002f88 00012f88 2**0 ; 注意VMA=RAM, LMA=Flash!

关键看.data的VMA(Virtual Memory Address)和LMA(Load Memory Address)。VMA是运行时地址(RAM),LMA是加载地址(Flash)。启动代码必须从LMA复制到VMA。

安全复制模板

extern uint32_t _sidata; // data段在Flash中的起始地址(LMA) extern uint32_t _sdata; // data段在RAM中的起始地址(VMA) extern uint32_t _edata; // data段在RAM中的结束地址 uint32_t *dst = &_sdata; uint32_t *src = &_sidata; while (dst < &_edata) { *dst++ = *src++; // 逐字复制,避免memcpy依赖未初始化的堆 }

2.6 阶段六:main()之前的“最后一道门”(复位后第11~15个周期)

当CPU终于执行到main()第一行,你以为安全了?不。GCC编译器会在main前插入__libc_init_array(),它遍历.init_array段调用所有初始化函数。而.init_array中的函数指针,来自链接脚本中*(.init_array)的收集。如果某个模块(如FatFS)的初始化函数被优化掉,或其.init_array条目未被正确链接,__libc_init_array()可能跳转到0x00000000——再次触发HardFault。

排查命令

arm-none-eabi-readelf -S your.elf | grep init # 查看.init_array段是否存在且非空 arm-none-eabi-objdump -s -j .init_array your.elf # 查看该段内容是否为有效函数指针

2.7 阶段七:用户代码的“信任崩塌点”(main()执行后)

这才是真正的战场。main()中第一行HAL_Init()看似简单,但它会:

  • 检查SysTick是否就绪(依赖前面时钟树)
  • 初始化NVIC优先级分组(影响后续中断)
  • 配置SysTick为1ms中断(依赖前面时钟精度)

如果此时SysTick未正确配置,HAL_Delay()将永远阻塞。而HAL_Delay()的实现是:

while (hal_tick_counter < tickstart + delay) { } // 依赖SysTick中断更新hal_tick_counter

但SysTick中断未触发,hal_tick_counter永远为0。

终极验证法
main()开头插入:

// 强制触发一次SysTick中断,验证硬件 SysTick->LOAD = 1000 - 1; // 1ms @ 1MHz SysTick->VAL = 0; SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; __DSB(); while(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk == 0) { } // 等待中断标志

若此循环永不退出,说明SysTick硬件故障或时钟未就绪。

注意:以上7个阶段,任意一个出问题,现象都是“黑屏、无串口、LED不亮”。但根源可能天差地别。我的经验是:先用示波器看VDD和复位信号(排除硬件),再用J-Link读VTOR和SP寄存器(排除启动配置),最后单步跟踪Reset_Handler(排除代码逻辑)。顺序错了,三天都找不到问题。

3. 故障定位的“三维坐标系”:寄存器、时序、内存的联合勘测

嵌入式固件故障,90%以上表现为“现象模糊、原因隐蔽、复现困难”。比如:设备在-20℃环境下启动失败,但在实验室25℃下100%正常;OTA升级后概率性死机,但单独烧录同一固件却稳定运行。这类问题无法靠“重启试试”解决,必须建立一套系统化的定位坐标系——它由三个维度构成:寄存器状态(空间维度)、信号时序(时间维度)、内存数据(逻辑维度)。下面用一个真实案例展开:某款智能门锁固件,在量产测试中发现“低电量(<3.0V)时,指纹识别模块初始化失败,但串口无任何日志”。

3.1 维度一:寄存器状态——冻结故障瞬间的“DNA快照”

故障发生时,CPU可能已进入HardFault或BusFault,但寄存器状态被完整保存在Fault Status Registers中。关键不是看报错类型,而是看触发故障的指令地址和操作数

实操步骤

  1. 在HardFault_Handler中添加寄存器dump:
void HardFault_Handler(void) { __ASM volatile( "mov r0, #0\n\t" // r0 = 0 "mrs r1, psp\n\t" // 获取PSP(进程栈指针) "mrs r2, msp\n\t" // 获取MSP(主栈指针) "mrs r3, control\n\t" // CONTROL寄存器 "mrs r4, xpsr\n\t" // xPSR "mrs r5, ipsr\n\t" // IPSR "mrs r6, fpscr\n\t" // FPSCR "ldr r7, =0xE000ED28\n\t" // CFSR地址 "ldr r7, [r7]\n\t" // 读取CFSR "ldr r8, =0xE000ED2C\n\t" // HFSR地址 "ldr r8, [r8]\n\t" // 读取HFSR "ldr r9, =0xE000ED34\n\t" // BFAR地址(总线故障地址) "ldr r9, [r9]\n\t" // 读取BFAR "ldr r10, =0xE000ED38\n\t" // AFAR地址(辅助故障地址) "ldr r10, [r10]\n\t" // 读取AFAR "bkpt #0\n\t" // 断点,让调试器捕获 ); }
  1. 触发故障后,在调试器中查看寄存器:
  • CFSRIBUSERR位(bit 8)置位,且BFAR=0x40022000,说明访问了APB1外设基地址(RCC寄存器)但该地址未使能时钟。
  • CFSRPRECISERR位(bit 9)置位,且BFAR=0x20001234,说明向RAM地址0x20001234写入了未对齐数据(如向0x20001235写入32位整数)。

本案定位
dump显示CFSR=0x00000800(PRECISERR),BFAR=0x40005804(指纹模块SPI寄存器地址)。进一步检查发现:SPI初始化代码中,向SPI_CR1寄存器写入0x00000040(启用SPI),但此时RCC->APB2ENRSPI1EN位为0——SPI时钟未使能。低电量时,APB2总线电压波动导致时钟门控电路误判,使能位被清零。

3.2 维度二:信号时序——捕捉毫秒级的“电平谋杀”

寄存器dump告诉你“哪里错了”,但时序分析告诉你“为什么错”。比如I2C通信失败,寄存器可能显示ADDR位未置位,但根本原因是SCL时钟频率超标。

必备工具

  • 逻辑分析仪(推荐Saleae Logic Pro 16,采样率≥100MS/s)
  • 探头必须使用短地线(<5cm),否则引入噪声

本案时序分析
将逻辑分析仪接在指纹模块的I2C SDA/SCL线上,触发条件设为“I2C Start Condition”。捕获到:

  • 正常情况:SCL高电平时间=4.8μs,低电平时间=4.9μs(符合标准模式100kHz)
  • 故障情况:SCL高电平时间=2.1μs,低电平时间=7.3μs(严重不对称)

根源是:低电量时,MCU的GPIO驱动能力下降,SCL上拉电阻(4.7kΩ)与MCU输出阻抗形成RC滤波,导致上升沿变缓。解决方案不是换电阻,而是在I2C初始化中降低速率

hi2c1.Init.ClockSpeed = 50000; // 从100kHz降至50kHz hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 保持50%占空比

3.3 维度三:内存数据——解剖固件的“活体切片”

寄存器和时序是“症状”,内存数据是“病灶”。比如OTA升级后功能异常,但寄存器和时序都正常——问题一定在Flash数据被意外修改。

内存勘测三步法

  1. 对比基线:用J-Link命令导出Flash原始镜像:
JLinkExe -Device STM32F407VG -If SWD -Speed 4000 -CommanderScript dump_flash.jlink

dump_flash.jlink内容:

si 2 mem32 0x08000000 0x100000 > flash_before.bin q
  1. 定位可疑区域:用xxd -g1 flash_before.bin | head -20查看前20字节,确认向量表:
0000000: 00 00 00 20 11 00 00 08 00 00 00 00 00 00 00 00 ... ............

首4字节是SP值(0x20001000),次4字节是Reset_Handler地址(0x08000011)。
3.动态追踪:在疑似故障函数中插入内存快照:

void fingerprint_init(void) { uint32_t snapshot[16]; memcpy(snapshot, (void*)0x20000000, 64); // 快照RAM首64字节 // ... 初始化代码 ... if (init_failed) { // 将snapshot通过USB CDC发送到PC分析 send_to_pc(snapshot, 64); } }

本案发现:snapshot[0](即RAM首地址)在故障时为0xDEADBEEF,而正常时为0x00000000。追查发现,指纹驱动中一处数组越界,将0xDEADBEEF写入了RAM首地址,而该地址恰好是_estack定义的位置——导致后续所有栈操作崩溃。

3.4 三维联合定位工作流——一张表定乾坤

故障现象寄存器维度证据时序维度证据内存维度证据根本原因解决方案
OTA后设备变砖,但Flash读取数据正确VTOR=0x00000000(未重定向)Reset后首个指令从0x00000000读取Flash中app向量表在0x08008000,但VTOR未指向它bootloader跳转前未设置SCB->VTOR在跳转前强制SCB->VTOR = APP_VECTOR_TABLE_ADDR
低电量时SPI通信失败CFSR=0x00000004(BUSFAULT),BFAR=0x40005804SCL上升沿时间>3μs(标准要求<1μs)SPI_CR1寄存器值正确,但时钟使能位被硬件清零低电压导致APB2时钟门控电路失效降低SPI速率至50kHz,并增加时钟使能确认循环
指纹识别概率性失败HardFault,CFSR=0x00000800(PRECISERR),BFAR=0x20001235SDA在地址传输阶段出现毛刺RAM首地址被写入0xDEADBEEF驱动中数组越界,覆盖栈顶增加数组边界检查,启用MPU保护RAM首页

经验之谈:寄存器是入口,时序是路径,内存是终点。必须按此顺序排查——先看寄存器确定故障大类,再用示波器/逻辑分析仪验证时序假设,最后用内存dump确认数据完整性。跳过任一维度,都会陷入“试错式调试”的泥潭。

4. OTA升级的“工程化悬崖”:从烧录成功到用户点击升级的生死距离

OTA(Over-The-Air)常被简化为“下载固件→写入Flash→重启”。但真实世界里,OTA是嵌入式系统最危险的升级方式——它要求你在设备运行时,动态修改正在执行的代码所在的存储介质。稍有不慎,设备永久变砖。我见过最惨的案例:某共享单车锁具OTA后,12%的车辆锁死,维修成本远超OTA节省的流量费。问题根源不是代码bug,而是OTA流程设计违背了三个铁律:原子性、可逆性、可观测性。下面用ESP32平台为例,拆解OTA工程化落地的每一处悬崖。

4.1 悬崖一:Flash擦除的“非原子性陷阱”

ESP32的Flash擦除以sector(4KB)为单位。但你的固件可能只有1.2MB,分布在多个sector中。OTA工具若按顺序擦除sector,擦到一半断电,结果就是:前半部分sector已擦成0xFF,后半部分仍是旧固件——重启后,向量表损坏,设备无法启动。

标准解法(App Partition)
ESP-IDF强制使用分区表(partition_table.csv),定义至少两个app分区:

# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, otadata, data, otadata, 0xf000, 0x2000, phy_init, data, phy, 0x11000,0x1000, factory, app, factory, 0x10000,1M, ota_0, app, ota_0, 0x110000,1M, ota_1, app, ota_1, 0x210000,1M,

OTA时,新固件写入空闲分区(如ota_1),写完后更新otadata分区中的active flag,最后重启。即使断电,otadata分区要么全0要么全1,不会出现中间态。

但坑在这里:otadata分区大小仅0x2000(8KB),而ESP32的otadata结构包含两个32字节的slot(每个slot存一个app分区的CRC和seq),共64字节。剩余空间用于存储加密密钥和签名。若你自定义了密钥存储,超出64字节,otadata写入会失败,且无错误提示——设备永远停留在factory分区。

验证脚本

# otadata_validator.py import struct with open('otadata.bin', 'rb') as f: data = f.read() # otadata格式:前32字节slot0,后32字节slot1 slot0 = data[0:32] slot1 = data[32:64] # 检查seq字段(偏移24字节) seq0 = struct.unpack('<I', slot0[24:28])[0] seq1 = struct.unpack('<I', slot1[24:28])[0] print(f"Slot0 seq: {seq0}, Slot1 seq: {seq1}") # 正确状态:一个seq为0xFFFFFFFF(无效),另一个为递增序列号

4.2 悬崖二:签名验证的“信任链断裂”

OTA固件必须签名,否则攻击者可推送恶意固件。但签名验证本身可能成为单点故障。常见错误是:在验证前就擦除旧固件,若签名失败,设备无回退固件。

安全流程

  1. 下载固件到RAM或临时分区
  2. 计算固件SHA256哈希
  3. 用公钥验证签名(RSA-2048)
  4. 仅当验证通过,才擦除目标分区并写入
  5. 写入完成后,更新otadata

致命陷阱:RSA签名验证依赖模幂运算,耗时长。若在中断上下文中调用,可能被高优先级中断打断,导致计算错误。ESP-IDF的esp_secure_boot_verify_signature()必须在task中调用,且需保证足够栈空间(>4KB)。

实测数据
在ESP32-WROVER上,验证1MB固件签名:

  • 使用硬件加速(RSA peripheral):耗时≈850ms
  • 纯软件实现:耗时≈3200ms
    若任务栈仅2KB,软件实现会栈溢出,返回随机错误码。

4.3 悬崖三:升级过程的“黑盒不可观测”

用户点击“升级”,进度条走到50%时卡住。你是等1分钟重启,还是强制断电?运维人员需要知道:是网络超时?Flash写入失败?还是签名验证卡死?

工程化方案

  • 进度上报:每写入一个sector(4KB),通过MQTT上报{"progress": 35, "stage": "writing", "sector": 12}
  • 状态持久化:在NVS中记录当前stage(download/verify/write/reboot),断电后可恢复
  • 看门狗协同:OTA task中喂狗,若10秒无进展,触发看门狗复位,进入安全模式

NVS状态结构

typedef struct { uint32_t magic; // 0xCAFEBABE,标识有效状态 uint8_t stage; // 0=IDLE, 1=DOWNLOAD, 2=VERIFY, 3=WRITE, 4=REBOOT uint32_t progress; // 0~100 uint32_t sector_num; // 当前写入sector编号 } ota_state_t;

每次状态变更,调用nvs_set_blob(handle, "ota_state", &state, sizeof(state)),并nvs_commit(handle)确保写入。

4.4 悬崖四:回滚机制的“伪安全幻觉”

很多方案声称“OTA失败自动回滚”。但回滚的前提是:旧固件必须完好。而Flash有擦写寿命(通常10万次),频繁OTA会提前耗尽sector寿命。更糟的是:若回滚分区本身在上次OTA中被意外擦除,回滚即失败。

真·安全回滚设计

  • 双备份分区:除ota_0/ota_1外,额外保留ota_backup分区(大小=最大固件尺寸)
  • 写入前校验:每次OTA前,用CRC32校验ota_backup分区内容,确保可回滚
  • 磨损均衡:记录每个分区擦写次数,优先选择擦写次数最少的分区作为目标

磨损均衡实现

// 在NVS中存储分区擦写计数 nvs_handle_t handle; nvs_open("ota_stats", NVS_READWRITE, &handle); uint32_t count_0, count_1, count_b; nvs_get_u32(handle, "ota_0_count", &count_0); nvs_get_u32(handle, "ota_1_count", &count_1); nvs_get_u32(handle, "ota_b_count", &count_b); // 选择最小计数的分区 if (count_0 <= count_1 && count_0 <= count_b) { target_partition = "ota_0"; nvs_set_u32(handle, "ota_0_count", count_0 + 1); } else if (count_1 <= count_0 && count_1 <= count_b) { target_partition = "ota_1"; nvs_set_u32(handle, "ota_

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询