ESP32 OTA分区表:不是复制固件那么简单先说结论:ESP32 OTA的核心是分区表设计。双Bank+回滚机制,保证升级失败设备不会变砖。直接覆盖当前固件是最蠢的做法。### 默认分区表的问题ESP-IDF默认分区表:# Name, Type, SubType, Offset, Sizenvs, data, nvs, 0x9000, 0x4000otadata, data, ota, 0xd000, 0x2000phy_init, data, phy, 0xf000, 0x1000factory, app, factory, 0x10000, 1M只有一个factory分区。OTA时需要覆写factory分区——写到一半断电,设备变砖。### 双Bank OTA分区表# Name, Type, SubType, Offset, Sizenvs, data, nvs, 0x9000, 0x4000otadata, data, ota, 0xd000, 0x2000phy_init, data, phy, 0xf000, 0x1000otadata, data, ota, 0xd000, 0x2000phy_init, data, phy, 0xf000, 0x1000ota_0, app, ota_0, 0x10000, 0x180000ota_1, app, ota_1, 0x190000, 0x180000nvs_data, data, nvs, 0x310000, 0x40000两个OTA分区交替使用:- 当前运行ota_0,OTA写入ota_1,切换到ota_1- 当前运行ota_1,OTA写入ota_0,切换到ota_0写入过程中不影响当前运行固件,断电也不怕——大不了回到旧分区。### 分区表配置partitions.csvcsv# Name, Type, SubType, Offset, Size, Flagsnvs, data, nvs, 0x9000, 0x4000,otadata, data, ota, 0xd000, 0x2000,phy_init, data, phy, 0xf000, 0x1000,ota_0, app, ota_0, 0x10000, 0x1E0000,ota_1, app, ota_1, 0x1F0000, 0x1E0000,nvs_data, data, nvs, 0x3D0000, 0x20000,````CMakeLists.txt`:cmakeset(PARTITION_TABLE_CUSTOM_FILENAME “partitions.csv”)set(PARTITION_TABLE_CUSTOM_PARTITION_APP_OFFSET 0x10000)### OTA实现代码c#include “esp_ota_ops.h”#include “esp_http_client.h”#include “esp_https_ota.h”#define FIRMWARE_URL “https://your-server.com/firmware.bin”#define CA_CERT embedded_root_ca_cert // 根证书esp_err_t do_ota_upgrade() { esp_http_client_config_t config = { .url = FIRMWARE_URL, .cert_pem = (const char )CA_CERT, .timeout_ms = 10000, .keep_alive_enable = true, }; esp_https_ota_handle_t ota_handle = NULL; esp_err_t ret = esp_https_ota_begin(&config, &ota_handle); if (ret != ESP_OK) { printf(“OTA begin failed: %s\n”, esp_err_to_name(ret)); return ret; } // 循环写入 while (1) { ret = esp_https_ota_perform(ota_handle); if (ret != ESP_ERR_OTA_IN_PROGRESS) { break; } // 进度 int total = esp_https_ota_get_image_size(ota_handle); int written = esp_https_ota_get_image_len_read(ota_handle); printf(“OTA progress: %d/%d (%d%%)\n”, written, total, written100/total); } if (ret != ESP_OK) { printf(“OTA perform failed: %s\n”, esp_err_to_name(ret)); esp_https_ota_abort(ota_handle); return ret; } // 验证固件 ret = esp_https_ota_finish(ota_handle); if (ret != ESP_OK) { printf(“OTA finish failed: %s\n”, esp_err_to_name(ret)); return ret; } printf(“OTA success. Next boot from new partition.\n”); return ESP_OK;}### 回滚机制OTA写入成功不等于固件能运行。如果新固件crash boot loop,需要自动回滚到旧固件。ESP-IDF的`app rollback`机制:c// 新固件启动时必须调用void app_main() { // 检查启动原因 esp_ota_img_states_t ota_state; esp_partition_t running = esp_ota_get_running_partition(); esp_ota_get_state_partition(running, &ota_state); if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) { // 新固件首次启动,还没被确认 printf(“New firmware first boot. Validating…\n”); // 验证固件功能 if (self_test_passed()) { // 确认固件OK,标记为valid esp_ota_mark_app_valid_cancel_rollback(); printf(“Firmware validated.\n”); } else { // 标记为无效,重启会回滚 esp_ota_mark_app_invalid_and_rollback(); printf(“Firmware invalid. Rolling back…\n”); esp_restart(); } } // 正常运行 run_application();}流程:1. OTA写入新分区(ota_1)2. 设置启动分区为ota_1,标记为PENDING_VERIFY3. 重启4. 新固件启动,自检通过 → mark_valid5. 自检失败 → mark_invalid_rollback → 重启回到ota_0### 看门狗配合OTA过程中必须喂狗。如果OTA卡死(HTTP超时),看门狗重启设备:c#include "esp_task_wdt.h"void ota_task(void pvParameter) { // 注册看门狗 esp_task_wdt_add(NULL); while (1) { // OTA过程 esp_err_t ret = do_ota_upgrade(); // 喂狗 esp_task_wdt_reset(); if (ret == ESP_OK) { // 等待重启 vTaskDelay(pdMS_TO_TICKS(1000)); esp_restart(); } else { // 失败,等5分钟重试 vTaskDelay(pdMS_TO_TICKS(300000)); } }}### 版本管理c// 固件版本#define FIRMWARE_VERSION “1.2.3”#define FIRMWARE_VERSION_CODE 10203 // major10000 + minor100 + patch// 上报当前版本void report_version() { char payload[128]; snprintf(payload, sizeof(payload), “{“version”:”%s",“version_code”:%d,“chip_rev”:%d}", FIRMWARE_VERSION, FIRMWARE_VERSION_CODE, esp_chip_get_revision()); mqtt_publish(“device/ota/status”, payload, qos=1);}// 检查更新void check_ota() { // 从服务器获取最新版本号 int latest_version = fetch_latest_version(); if (latest_version > FIRMWARE_VERSION_CODE) { printf(“New version available: %d\n”, latest_version); do_ota_upgrade(); }}### 签名验证裸OTA下载有被中间人替换固件的风险。ESP-IDF支持固件签名验证:bash# 生成密钥对espsecure.py generate_signing_key secure_boot_signing_key.pem# 配置secure boot# menuconfig → Security features → Enable secure boot``````c// 固件下载后验证签名esp_err_t verify_firmware(const esp_partition_t *partition) { // ESP-IDF在esp_https_ota_finish时自动验证签名 // 如果签名不匹配,finish会返回ESP_ERR_OTA_VALIDATE_FAILED return ESP_OK;}### 防回滚防止降级攻击(把固件刷回有漏洞的旧版本):c// 在anti-rollback eFuse中烧入版本号// 每次发布新固件,版本号必须比eFuse中的大// 固件代码中检查uint32_t efuse_version = esp_efuse_read_reg(EFUSE_BLK_OPT_DATA0 + 3);if (FIRMWARE_VERSION_CODE < efuse_version) { printf(“Firmware version too old. Refusing to boot.\n”); esp_ota_mark_app_invalid_and_rollback();}```实测:eFuse是一次性可写的,烧入后不可逆。谨慎操作。### OTA失败场景测试| 场景 | 结果 ||------|------|| 下载中断网 | OTA分区数据不完整,finish验证失败,不切换分区 || 固件写入一半断电 | 重启后分区数据不完整,bootloader选旧分区启动 || 新固件crash loop | 没调用mark_valid,看门狗重启后回滚到旧固件 || 固件版本号被篡改 | 签名验证失败,拒绝启动 || 服务器返回404 | HTTP请求失败,OTA不执行 |### 踩坑记录1. 两个OTA分区大小必须一致:不然刷不进去。分区表设计时预留足夠空间2. otadata分区:存储当前启动分区信息。这个分区损坏,设备不知道该启动哪个分区。加nvs备份3. HTTPS证书过期:OTA服务器SSL证书过期,设备无法下载。用有效期10年的证书,或者提前在固件里更新证书4. 内存不够:OTA缓冲区约32KB。如果FreeRTOS堆不够,HTTP下载会失败。menuconfig里增大堆5. 分区大小和固件大小:固件不能超过分区大小。用idf.py size检查一句话:ESP32 OTA的可靠性不在代码逻辑,在分区表+回滚+看门狗三件套。三者到位,OTA变砖率趋近于零。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐