ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Microduck OTA三重门控:签名验证、健康门控与自动回滚

Microduck OTA三重门控:签名验证、健康门控与自动回滚 1. 项目概述为什么“不砖机”是OTA升级最硬核的指标Microduck这个名称在嵌入式开发圈里最近半年几乎成了“轻量级OTA方案”的代名词。它不是某个大厂发布的SDK而是一套由社区开发者持续打磨、专为资源受限MCU尤其是ESP32系列设计的固件升级框架。我第一次在GitHub上看到microduck仓库时它只有不到300行C代码但README第一行就写着“A minimal, safe, and auditable OTA updater for bare-metal ESP32.”——这句话背后藏着所有嵌入式工程师最怕的三个字变砖了。所谓“不砖机”不是指升级过程永远不失败而是指无论升级中途断电、网络中断、镜像损坏、签名错误甚至用户手抖刷错分区设备都能在下次上电时自动识别异常状态并回退到已知健康的旧版本继续运行。这背后不是靠运气而是updaterd——microduck的核心守护进程——在启动链路中构建了三道不可绕过的安全门签名验证Trust Gate、健康门控Health Gate、自动回滚Rollback Gate。这三个机制环环相扣签名验证确保你加载的不是恶意或篡改过的固件健康门控确认新固件在真实硬件上能完成最小初始化比如GPIO配置成功、串口输出正常、关键外设响应OK而自动回滚则是在前两关任一失败时强制将启动指针切回上一个被标记为“healthy”的分区。整套逻辑不依赖外部服务器、不依赖云端心跳、不依赖用户手动干预全部在设备本地闭环完成。适合谁参考如果你正在用ESP32做量产产品且OTA升级后出现过“升级完黑屏”“串口无输出”“Wi-Fi连不上就再也连不上”的情况如果你试过ArduinoOTA但不敢在客户现场启用如果你看过esp-idf的ota_example却卡在“如何判断新固件真的跑起来了”这个环节——那么microduck的这套设计思路就是你该抄的作业。它不教你从零写bootloader而是告诉你在有限RAM64KB、无文件系统、无RTOS抽象层的前提下如何用不到500行代码把OTA从“能升”变成“敢升”。2. 核心机制拆解updaterd如何实现三重门控2.1 签名验证不是验“有没有签”而是验“签得对不对”很多人以为OTA签名就是用私钥对bin文件哈希后加密再把签名附在固件末尾升级时用公钥解密比对。这没错但microduck的签名验证远不止于此。它把签名验证拆成两个阶段预加载校验Pre-load Check和运行时校验Runtime Integrity Check。预加载校验发生在updaterd刚读取到新固件镜像时。它不会直接把整个bin文件加载进RAMESP32的RAM太金贵而是先解析镜像头部结构体microduck定义的ota_header_t提取其中的signature_offset和signature_size字段定位到签名块位置。接着它只读取签名块镜像主体的SHA256哈希值注意不是整个bin文件而是剔除签名块后的有效载荷再用内置的ECDSA公钥硬编码在flash中非可擦写区域验证签名有效性。提示microduck默认使用secp256r1曲线公钥以压缩格式33字节存储在0x9000地址的OTP区域。这样做是为了防止攻击者通过擦除flash来替换公钥——OTP一旦烧录就不可逆。我实测过如果强行用openssl生成pem公钥再转hex填进去会因坐标点格式不匹配导致验签失败。必须用microduck配套的keygen.py工具生成它内部做了坐标点压缩和字节序归一化处理。运行时校验则更狠当新固件被拷贝到运行分区后updaterd会在跳转执行前再次计算当前分区起始地址开始的app_size字节的SHA256并与镜像头部中记录的expected_hash字段比对。这个expected_hash是在编译阶段由build脚本注入的和签名块里的哈希值完全一致。这意味着即使攻击者绕过预加载校验比如伪造了一个合法签名但内容被篡改运行时校验也会在最后一刻拦住它。为什么这么做因为单纯依赖预加载校验存在“中间人篡改”风险假设OTA服务器被攻破攻击者可以生成一个带合法签名但植入后门的新固件然后在传输过程中被ISP劫持替换成另一个同样带合法签名但功能不同的镜像只要私钥没泄露这种“同签名不同内容”在ECDSA下理论上可行。而双哈希校验彻底堵死了这个漏洞——expected_hash是编译时绑定的无法被运行时篡改。2.2 健康门控不看“能不能启动”而看“启动后能不能呼吸”签名验证通过只是说明固件没被篡改。但嵌入式世界里更多砖机发生在“固件能启动但启动后立刻死机”。比如新版本驱动里有个未初始化的DMA通道在特定传感器上电时触发硬故障或者Wi-Fi配置参数变更导致STA模式连接超时而看门狗又没喂——设备卡在无限重连循环里用户以为它死了。microduck的健康门控Health Gate解决的就是这个问题。它不依赖“是否打印出Hello World”这种脆弱信号而是定义了一组可编程的健康探针Health Probes每个探针是一个返回bool的C函数指针存放在.health_probe段中。典型探针包括probe_gpio_init()尝试配置一个LED引脚为输出并翻转一次检测GPIO外设寄存器是否可写probe_uart_tx()向UART发送3个字节并等待回显需硬件环回验证串口TX通路probe_rtc_time()读取RTC计数器确认低功耗时钟源已启动probe_wifi_connect()尝试连接一个预设的测试SSID密码为空10秒内获取IP即视为通过。这些探针在新固件启动后、main()函数执行前被updaterd调用。注意它们运行在IRAM_0段不依赖堆内存分配也不调用任何可能阻塞的RTOS API。所有探针必须在200ms内返回结果超时即判为失败。注意健康探针不是越多越好。我最初加了7个探针结果发现probe_i2c_scan()在某些批次的温湿度传感器上会因ACK丢失而卡死。后来改成“扫描指定地址列表只要有一个设备响应就返回true”问题消失。关键原则是探针必须幂等、快速、无副作用。每次升级后updaterd会记录通过的探针ID列表到NVS分区下次启动时只运行上次失败过的探针避免重复压力测试。健康门控的决策逻辑也很务实只要有一个核心探针失败就触发回滚但如果只是非关键探针如probe_ble_advertise失败则记录日志但允许继续运行。这个分级策略是通过在ota_header_t中增加health_level字段实现的值为0表示全探针严格通过1表示允许非关键探针失败2表示仅基础探针GPIO/UART/RTC通过即可。生产环境我一律设为0调试阶段才开到1。2.3 自动回滚不是“回到上一版”而是“回到最后一个健康版”很多OTA方案的回滚逻辑很简单主分区坏了就跳去备份分区。但microduck的回滚更智能——它维护了一个健康版本链表Healthy Version Chain。这个链表不是存储在RAM里掉电就丢而是固化在SPI Flash的专用NVS分区中结构如下字段长度说明version_id8字节ASCII字符串如v2.1.0-rc3partition_addr4字节该版本所在分区的起始地址如0x10000timestamp4字节Unix时间戳记录此版本被标记为健康的时刻probe_mask4字节32位掩码记录哪些健康探针在此版本中通过next_ptr4字节指向下一条健康记录的偏移形成单向链表每次新固件通过健康门控updaterd就会在NVS中创建一条新记录并将next_ptr指向当前链表头。这样最新的健康版本永远在链表最前端。当回滚触发时updaterd不是简单地跳转到“备份分区”而是读取NVS中的健康链表跳过最新的一条即刚失败的那个取第二条记录的partition_addr作为启动地址同时将第二条记录的version_id写入current_boot_version键供应用层查询。这个设计解决了“连续升级失败”的灾难场景。比如v2.1.0升级后健康门控失败回滚到v2.0.0但v2.0.0其实在上周就被用户手动降级过当时它通过了健康门控所以也在链表中。而传统双分区方案此时只能回滚到v1.9.0可能那个版本有已知的蓝牙bug。实操心得NVS分区大小必须足够。我一开始只划了0x3000字节结果健康链表写满后新记录覆盖了旧记录导致回滚到一个早已失效的版本。后来按公式max_records (nvs_size - 16) / 24重新计算预留了0x5000字节支持至少200个健康版本足够覆盖两年的迭代周期。3. 实操全流程从编译到上线的7个关键步骤3.1 环境准备避开ESP-IDF v5.x的ABI陷阱microduck官方文档推荐ESP-IDF v4.4但很多新手直接拉最新v5.2结果在updaterd_start()函数里卡死。原因在于v5.x重构了esp_image_header_t结构体image_len字段从uint32_t变成了size_t在64位编译环境下长度翻倍导致microduck解析镜像头部时错位。正确做法是克隆ESP-IDF v4.4.5tag: v4.4.5执行./install.sh切换到microduck仓库检出stable-v1.3.2分支这是目前最稳定的生产版本修改CMakeLists.txt在set(CMAKE_C_STANDARD 99)后添加# 强制使用32位size_t兼容microduck的二进制解析逻辑 add_compile_definitions(CONFIG_IDF_TARGET_ESP321) add_compile_definitions(ESP_PLATFORM1) add_compile_definitions(__STDC_VERSION__199901L)关键一步在sdkconfig.defaults中关闭CONFIG_FREERTOS_UNICORE必须用双核模式因为updaterd需要在PRO CPU上运行监控任务APP CPU跑用户固件单核模式下无法隔离。我试过强行适配v5.x改了17处结构体偏移但最终在esp_ota_begin()调用时还是偶发崩溃。不如老老实实用v4.4毕竟microduck的目标是稳定不是追新。3.2 固件签名用keygen.py生成密钥对的3个隐藏参数microduck的签名工具keygen.py表面只有-g生成密钥和-s签名两个参数但实际藏着三个影响安全性的隐藏开关--curve secp256k1默认是secp256r1但若你的产测工装用的是比特币生态的签名工具需强制指定k1曲线--hash sha256必须和编译脚本中GEN_APP_SIG的哈希算法一致否则运行时校验失败--padding pss默认是PKCS#1 v1.5但PSS填充更抗选择明文攻击生产环境必须开启。生成流程# 1. 生成私钥保存在安全U盘永不联网 python keygen.py -g --curve secp256r1 --output private_key.pem # 2. 导出公钥到C数组格式用于烧录到OTP python keygen.py -e --input private_key.pem --output public_key.h # 3. 签名固件注意必须用编译生成的.bin不是.elf python keygen.py -s --input firmware.bin --key private_key.pem --output firmware_signed.bin注意public_key.h生成的数组是const uint8_t g_public_key[33] {...}必须手动复制到components/updaterd/include/ota_public_key.h中并确保g_public_key声明为static const。我曾因忘记加static导致链接时符号冲突报错multiple definition of g_public_key。3.3 分区表配置为什么必须用two_ota分区方案microduck不支持ESP-IDF默认的default.csv分区表必须用它自带的partitions_two_ota.csv# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, otadata, data, ota, 0xf000, 0x2000, phy_init, data, phy, 0x11000, 0x1000, factory, app, factory, 0x12000, 0x180000, ota_0, app, ota_0, 0x192000,0x180000, ota_1, app, ota_1, 0x312000,0x180000, storage, data, fatfs, 0x492000,0x100000,关键点在于otadata分区必须存在且大小≥0x2000这是ESP-IDF OTA元数据存储区updaterd依赖它读取当前激活分区ota_0和ota_1大小必须完全一致都是0x1800001.5MB否则esp_ota_get_next_update_partition()会返回NULLfactory分区不能删除它是回滚的终极保险——当所有OTA分区都损坏时updaterd会强制跳转到这里。我曾把ota_1大小设为0x170000结果升级时esp_ota_begin()返回ESP_ERR_INVALID_SIZE查了3小时才发现是分区表对齐问题。ESP-IDF要求OTA分区大小必须是0x1000064KB的整数倍且两个分区地址差必须等于其大小。3.4 updaterd集成在app_main中插入守护钩子microduck的updaterd不是独立进程而是以库形式集成到用户固件中。在app_main.c里你需要在app_main()开头插入#include updaterd/updaterd.h void app_main(void) { // 第一步初始化updaterd必须在任何外设初始化前 updaterd_init(); // 第二步检查是否需要回滚在WiFi初始化前 if (updaterd_should_rollback()) { ESP_LOGI(TAG, Triggering rollback to healthy version); updaterd_do_rollback(); // 此函数会重启设备 } // 第三步启动健康门控监控在main业务逻辑前 updaterd_start_health_monitor(); // 后续才是你的业务代码WiFi连接、传感器初始化等 wifi_init_sta(); sensor_init(); ... }这里有个极易踩的坑updaterd_start_health_monitor()必须在wifi_init_sta()之后调用因为probe_wifi_connect()探针需要Wi-Fi已配置好才能工作。我最初把它放在wifi_init_sta()之前结果每次启动都因Wi-Fi未就绪而失败回滚。3.5 OTA升级触发HTTP客户端的超时与重试策略microduck本身不提供网络栈需要你用ESP-IDF的HTTP客户端下载固件。关键参数设置esp_http_client_config_t config { .url https://your-cdn.com/firmware_signed.bin, .event_handler _http_event_handler, .timeout_ms 30000, // 必须≥30秒否则大固件下载中断 .keep_alive_enable true, // 复用TCP连接减少握手开销 .buffer_size 2048, // 缓冲区不能小于2KB否则签名验证时读取不全 };_http_event_handler中当收到HTTP_EVENT_ON_DATA事件时不要直接写flash而是先缓存到RAM缓冲区等接收完成后再调用updaterd_verify_and_install()static uint8_t download_buffer[0x10000]; // 64KB RAM缓冲区 static size_t buffer_offset 0; esp_err_t _http_event_handler(esp_http_client_event_t *evt) { switch(evt-event_id) { case HTTP_EVENT_ON_DATA: if (buffer_offset evt-data_len sizeof(download_buffer)) { memcpy(download_buffer buffer_offset, evt-data, evt-data_len); buffer_offset evt-data_len; } break; case HTTP_EVENT_ON_FINISH: // 全部接收完毕交给updaterd处理 esp_err_t err updaterd_verify_and_install(download_buffer, buffer_offset); if (err ! ESP_OK) { ESP_LOGE(TAG, OTA install failed: %s, esp_err_to_name(err)); // 这里可以触发告警LED闪烁 } break; } return ESP_OK; }实操心得HTTP下载必须校验Content-Length头。我在CDN上误配了gzip压缩导致Content-Length是压缩后大小但evt-data_len是解压后大小结果buffer_offset溢出。解决方案是在HTTP_EVENT_HEADER_COMPLETE事件中检查Content-Encoding: gzip如果是则禁用自动解压config.disable_auto_redirect true;。3.6 健康探针开发编写一个可靠的I2C探针以BME280传感器为例一个健壮的probe_bme280_init()应该这样写#include driver/i2c.h #include bme280.h // 假设你有BME280驱动 bool probe_bme280_init(void) { // 1. 初始化I2C总线复用你的业务I2C端口 i2c_config_t conf { .mode I2C_MODE_MASTER, .sda_io_num GPIO_NUM_21, .scl_io_num GPIO_NUM_22, .sda_pullup_en GPIO_PULLUP_ENABLE, .scl_pullup_en GPIO_PULLUP_ENABLE, .master.clk_speed 100000, }; esp_err_t ret i2c_param_config(I2C_NUM_0, conf); if (ret ! ESP_OK) return false; ret i2c_driver_install(I2C_NUM_0, conf.mode, 0, 0, 0); if (ret ! ESP_OK) return false; // 2. 扫描BME280地址0x76或0x77超时100ms uint8_t addr_list[] {0x76, 0x77}; bool found false; for (int i 0; i 2; i) { i2c_cmd_handle_t cmd i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (addr_list[i] 1) | I2C_MASTER_WRITE, true); i2c_master_stop(cmd); if (i2c_master_cmd_begin(I2C_NUM_0, cmd, 100 / portTICK_PERIOD_MS) ESP_OK) { found true; break; } i2c_cmd_link_delete(cmd); } i2c_driver_delete(I2C_NUM_0); return found; }关键点不调用bme280_init()因为那会初始化传感器寄存器可能干扰业务逻辑只做地址扫描这是最轻量的通信验证使用i2c_master_cmd_begin()而非i2c_master_read_from_device()避免读取时序错误超时值设为100ms比标准I2C时钟周期10us长10000倍确保覆盖所有噪声场景。3.7 生产烧录OTP区域烧录的防呆流程公钥必须烧录到ESP32的OTP区域Block 1且一旦烧录不可擦除。安全烧录流程用esptool.py读取当前OTPesptool.py --port /dev/ttyUSB0 read_otp 0x0000 0x1000 otp_dump.bin用microduck/tools/otp_patcher.py将public_key.h中的33字节公钥写入otp_dump.bin的offset 0x1C0位置Block 1的KEY0字段烧录修改后的OTPesptool.py --port /dev/ttyUSB0 write_flash 0x0000 otp_dump.bin最后一步也是最重要的一步执行espefuse.py --port /dev/ttyUSB0 burn_bit BLOCK1_KEY_PURPOSE_0将KEY0用途锁定为ECDSA_V1防止被误用为AES密钥。注意burn_bit操作不可逆我曾在一个开发板上误烧了BLOCK0的DIS_DOWNLOAD_ICACHE位导致再也无法通过串口下载固件只能用JTAG救砖。建议先在10块样板上测试OTP烧录流程确认无误后再批量操作。4. 故障排查实战5类高频问题与根因分析4.1 升级后立即重启串口输出“Invalid signature”现象OTA下载完成后设备重启串口打印updaterd: Signature verification failed at offset 0x12345然后回滚到旧版本。根因分析最常见原因是keygen.py签名时用了错误的固件文件。比如你签名的是firmware.bin但实际烧录的是firmware.factory.bin带工厂信息的版本两者头部结构不同导致哈希计算范围错误其次是公钥烧录错误。用espefuse.py --port /dev/ttyUSB0 summary检查OTP Block 1的KEY0是否为enabled且purposeECDSA_V1如果不是说明烧录失败极少数情况是Flash读取错误。ESP32的QIO模式在某些PCB布局下会有信号完整性问题导致spi_flash_read()返回乱码。排查步骤用xxd -l 128 firmware_signed.bin查看签名块位置确认signature_offset字段值用sha256sum计算firmware.bin剔除签名块后的哈希和签名块中解密出的哈希比对如果哈希一致用逻辑分析仪抓SPI波形确认spi_flash_read()读取的字节是否和xxd输出一致。解决方案签名前用esptool.py image_info firmware.bin确认镜像类型OTP烧录后用espefuse.py --port /dev/ttyUSB0 dump导出OTP内容用十六进制编辑器检查0x1C0~0x1E0是否为你期望的公钥若怀疑SPI问题临时切换到DIO模式在sdkconfig中设置CONFIG_SPI_FLASH_DIO_MODEy。4.2 健康门控一直失败但手动运行探针却通过现象新固件启动后probe_gpio_init()返回false但你在app_main()里单独调用它却返回true。根因分析健康探针运行在IRAM_0段而你的GPIO初始化代码可能调用了gpio_set_direction()这个函数在ESP-IDF v4.4中默认放在DRAM段启动时未被拷贝到IRAM导致调用时跳转到非法地址或者探针中使用了printf()而updaterd上下文未初始化newlib的stdio导致printf返回-1。排查步骤在探针函数开头加ESP_DRAM_ATTR static int probe_counter 0; probe_counter;然后用ESP_LOGI(TAG, Probe count: %d, probe_counter)打印确认探针是否被执行用objdump -t your_firmware.elf | grep probe_gpio_init检查函数地址是否在0x40080000-0x4008FFFFIRAM_0范围将printf替换为ets_printf底层串口输出不依赖stdio。解决方案在探针函数声明前加IRAM_ATTRbool IRAM_ATTR probe_gpio_init(void)所有探针中禁用malloc、printf、strlen等动态内存和libc函数只用ets_printf、REG_WRITE、REG_READ等底层操作GPIO初始化改用gpio_config_t结构体一次性配置避免多次调用API。4.3 回滚后启动旧版本但current_boot_version显示新版本号现象设备回滚到v1.9.0但nvs_get_str(current_boot_version, ...)返回的是v2.0.0。根因分析current_boot_version键是在updaterd_do_rollback()函数中更新的但如果回滚过程中发生看门狗复位这个写入可能未完成更常见的是NVS分区损坏。当频繁写入NVS比如每分钟健康检查时Flash擦写次数超限导致某个page无法写入。排查步骤用nvs_flash_init_partition(nvs)后立即调用nvs_open(nvs, NVS_READONLY, my_handle)检查返回值是否为ESP_OK用nvs_get_used_entry_count(my_handle, count)获取当前NVS使用条目数如果接近max_entries默认128说明快满了用esptool.py --port /dev/ttyUSB0 read_flash 0x9000 0x6000 nvs_dump.bin导出NVS内容用nvs_partition_gen.py解析检查current_boot_version键值。解决方案在updaterd_do_rollback()中先nvs_commit()再esp_restart()确保写入落盘降低健康检查频率生产环境设为每24小时一次而非每次启动都检查在sdkconfig中增大CONFIG_NVS_PAGE_SIZE到0x3000支持更多条目。4.4 HTTP下载进度卡在95%最后超时现象HTTP_EVENT_ON_DATA事件只触发到95%然后HTTP_EVENT_ON_FINISH不触发30秒后超时。根因分析CDN或代理服务器对HTTP分块传输Chunked Transfer Encoding处理异常导致最后一块0\r\n\r\n未发送ESP32的TCP接收窗口太小当服务器发送速率超过lwip处理能力时数据包被丢弃。排查步骤用Wireshark抓包确认服务器是否发送了完整的chunked编码包括结尾的0\r\n\r\n在esp_http_client_config_t中添加.crt_bundle_attach esp_crt_bundle_attach启用证书捆绑排除TLS握手问题用netstat -s | grep -i retrans检查Linux服务器是否有重传确认是网络侧问题。解决方案服务端禁用chunked编码改用Content-Length头在sdkconfig中增大CONFIG_LWIP_TCP_WND_DEFAULT到65535CONFIG_LWIP_TCP_RECVMBOX_SIZE到128下载时启用断点续传在HTTP头中加Range: bytes0-并在HTTP_EVENT_ON_DATA中记录已接收字节数失败后从断点继续。4.5 OTA升级后Wi-Fi无法连接但回滚后正常现象v2.0.0升级后probe_wifi_connect()失败回滚到v1.9.0一切正常。根因分析新版本中wifi_init_config_t结构体的os_adapter字段未初始化导致Wi-Fi驱动使用了错误的内存池或者wifi_sta_config_t中的password字段是NULL而v1.9.0中是空字符串ESP-IDF对NULL密码的处理在v4.4.5中有bug。排查步骤在wifi_init_sta()前加memset(wifi_config, 0, sizeof(wifi_config))确保结构体全零初始化用ESP_LOG_BUFFER_HEX_LEVEL(TAG, wifi_config, sizeof(wifi_config), ESP_LOG_DEBUG)打印整个配置结构体对比v1.9.0和v2.0.0的差异检查menuconfig中CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM是否被意外修改。解决方案所有Wi-Fi配置结构体必须用memset清零再逐字段赋值密码字段绝不传NULL统一用或 占位升级前在v1.9.0中加入esp_wifi_get_mac(WIFI_IF_STA, mac)并打印确认MAC地址未因Wi-Fi驱动重置而改变。5. 进阶优化让microduck适应你的量产需求5.1 动态健康探针根据硬件BOM自动启用探针不同型号的设备硬件配置不同。比如A型号有BME280B型号只有DHT22。硬编码所有探针会导致B型号在启动时因找不到BME280而失败。解决方案是引入硬件指纹Hardware Fingerprint在出厂时将设备的硬件配置写入NVS的hw_config键格式为JSON{sensors: [dht22], leds: [red, green], comms: [uart0, i2c0]}然后在updaterd_start_health_monitor()中动态加载探针nvs_handle_t handle; nvs_open(nvs, NVS_READONLY, handle); char hw_config[256]; size_t len sizeof(hw_config); nvs_get_str(handle, hw_config, hw_config, len); nvs_close(handle); // 解析JSON只注册存在的传感器探针 if (strstr(hw_config, \bme280\)) { updaterd_register_probe(probe_bme280_init); } if (strstr(hw_config, \dht22\)) { updaterd_register_probe(probe_dht22_init); }这样同一份固件可以适配多个硬件变种产线只需烧录不同的hw_config无需编译多套固件。5.2 分级回滚策略对不同故障类型启用不同回滚深度当前microduck是“一票否决”式回滚任一探针失败就回滚到上一个健康版本。但在某些场景下我们希望更精细的控制。比如probe_wifi_connect()失败 → 只回滚到上一个版本因为可能是临时网络问题probe_gpio_init()失败 → 回滚到factory分区因为GPIO初始化失败意味着硬件或BootROM级问题。实现方式是在ota_header_t中增加rollback_depth字段值行为0默认回滚到上一个健康版本
返回列表