Embedded HandbookПриложения
Все разделы › XVIII. ESP32: второй микроконтроллер
ESP32 · уровень средний · 11 мин чтения
Открыть в интерактивном справочнике

159. Обновление по сети на ESP32: разделы OTA, загрузчик, подтверждение и откат

Прибор, который уехал к заказчику, прошивают не программатором, а по сети. Обновление по воздуху (OTA) у ESP32 устроено так: во флеш-памяти два места под приложение, новая прошивка пишется в свободное, а загрузчик по записи в маленьком разделе otadata решает, какое приложение запускать. Главный риск — прошивка, которая скачалась целиком, но не работает: она не должна превратить прибор в кирпич. Для этого есть подтверждение и откат.

Всё показанное — живой опыт в эмуляторе (_verify/esp159/). Одна программа собрана трижды: версии 1.0, 2.0 и 3.0. Первая скачивает обновление по HTTP, вторая подтверждает себя, третья нарочно не подтверждает — и загрузчик возвращает прибор на рабочую версию.

Как это называется в документации

Оригинал Перевод
OTA (over-the-air) update обновление по сети, «по воздуху»
factory app, ota_0, ota_1 заводское приложение и два раздела для обновлений
otadata раздел с записью, какое приложение загружать
boot partition, running partition раздел, выбранный для загрузки, и тот, что работает сейчас
app description (esp_app_desc_t) описание образа: проект, версия, дата сборки
image state: PENDING_VERIFY, VALID, ABORTED состояние образа: ждёт подтверждения, рабочий, отвергнут
rollback откат на предыдущее рабочее приложение
mark app valid подтвердить, что новая прошивка работает
anti-rollback, secure version запрет отката на старую уязвимую версию

159.1 Разделы: где живут две прошивки

Флеш-память ESP32 с двумя разделами под обновления: factory по 0x10000, ota_0 и ota_1 по мегабайту; otadata указывает загрузчику, что запускать; новая прошивка ждёт подтверждения, иначе загрузчик откатывается

Таблица разделов для обновлений — готовая в ESP-IDF (CONFIG_PARTITION_TABLE_TWO_OTA). Разбор собранной таблицы:

nvs,data,nvs,0x9000,16K,
otadata,data,ota,0xd000,8K,
phy_init,data,phy,0xf000,4K,
factory,app,factory,0x10000,1M,
ota_0,app,ota_0,0x110000,1M,
ota_1,app,ota_1,0x210000,1M,

Три места под приложение по мегабайту: заводское factory и два для обновлений. Обновление пишется всегда в тот раздел, который сейчас не работает: из factory — в ota_0, из ota_0 — в ota_1, из ota_1 — снова в ota_0. Раздел otadata на 8 КБ хранит, какой из них загружать и в каком состоянии образ. При первой прошивке его заполняет ota_data_initial.bin:

0x1000 bootloader/bootloader.bin
0x10000 ota159.bin
0x8000 partition_table/partition-table.bin
0xd000 ota_data_initial.bin

Образ этой программы — 564 208 байт, раздел — 1 048 576: сборка сама пишет, что свободно 46 %. Если приложение вырастет больше раздела, сборка остановится — а обновить уже выпущенный прибор другой таблицей разделов по сети нельзя. Размер разделов под приложение выбирают с запасом один раз, до выпуска.


159.2 Скачать и записать: esp_https_ota

Компонент esp_https_ota делает всё сам: соединяется с сервером, проверяет заголовок образа, пишет во флеш-память кусками и в конце меняет раздел загрузки. Если нужно сперва посмотреть версию нового образа, работу разбивают на шаги:

    esp_http_client_config_t http = { .url = CONFIG_LAB_OTA_URL, .timeout_ms = 20000 };
    esp_https_ota_config_t ota = { .http_config = &http };
    esp_https_ota_handle_t h = NULL;
    esp_err_t e = esp_https_ota_begin(&ota, &h);
        e = esp_https_ota_get_img_desc(h, &nd);             /* заголовок нового образа — до загрузки целиком */
        e = esp_https_ota_perform(h);
    esp_err_t fin = (e == ESP_OK) ? esp_https_ota_finish(h) : (h ? esp_https_ota_abort(h) : e);

Прогон версии 1.0:

запуск: раздел factory по 0x010000, версия 1.0, состояние ESP_ERR_NOT_SUPPORTED
обновление: http://10.0.2.2:8070/ota159.bin → раздел ota_0 по 0x110000, 1048576 байт
в образе: проект ota159, версия 2.0
скачано 564208 байт, итог ESP_OK; загрузка теперь из ota_0

У factory состояния нет вовсе — отсюда ESP_ERR_NOT_SUPPORTED: заводское приложение всегда считается рабочим. Версия берётся из файла version.txt проекта и попадает в esp_app_desc_t при сборке; по ней программа решает, нужно ли вообще обновляться, и не качает ту же прошивку по кругу. esp_https_ota_finish проверяет образ целиком и только тогда переключает загрузку; оборванная загрузка раздел загрузки не трогает — прибор остаётся на старой прошивке.


159.3 Первый запуск: PENDING_VERIFY и подтверждение

После перезапуска загрузчик берёт приложение из ota_0:

rst:0xc (SW_CPU_RESET),boot:0x12 (SPI_FAST_FLASH_BOOT)
I (29641) boot: Loaded app from partition at offset 0x110000
запуск: раздел ota_0 по 0x110000, версия 2.0, состояние PENDING_VERIFY
подтверждение: ESP_OK
состояние после подтверждения: VALID

С включённым откатом (CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE) новый образ стартует в состоянии PENDING_VERIFY — «ждёт подтверждения». Программа проверяет, что всё работает, и вызывает одну функцию:

        printf("подтверждение: %s\n", esp_err_to_name(esp_ota_mark_app_valid_cancel_rollback()));

Состояние становится VALID, и дальше прибор загружается из ota_0 всегда.


159.4 Откат: если новая прошивка не подтвердила себя

Версия 3.0 собрана так, что после старта не подтверждает себя, а перезапускается:

        printf("версия %s не подтвердила себя — перезапуск\n", app->version);
        esp_restart();                                      /* загрузчик пометит образ негодным и откатит */
запуск: раздел ota_0 по 0x110000, версия 3.0, состояние PENDING_VERIFY
версия 3.0 не подтвердила себя — перезапуск
rst:0xc (SW_CPU_RESET),boot:0x12 (SPI_FAST_FLASH_BOOT)
I (35236) boot: Loaded app from partition at offset 0x10000
запуск: раздел factory по 0x010000, версия 1.0, состояние ESP_ERR_NOT_SUPPORTED
откат: версия 3.0 в разделе ota_0 — ABORTED, работает factory

Загрузчик увидел, что образ в ota_0 так и остался в PENDING_VERIFY после перезапуска, пометил его ABORTED и загрузил предыдущее рабочее приложение. Старая прошивка узнаёт об откате функцией esp_ota_get_last_invalid_partition и может сообщить на сервер, что обновление не прижилось.

Два запуска после обновления: версия 2.0 стартует в PENDING_VERIFY, подтверждает себя и становится VALID — дальше всегда ota_0; версия 3.0 перезапускается без подтверждения, загрузчик помечает её ABORTED и грузит factory по 0x10000 с версией 1.0

Откат срабатывает на любой перезапуск до подтверждения: программный, от сторожевого таймера, от паники, от пропадания питания. Поэтому прошивка, которая падает при старте, сама вернёт прибор на прошлую версию. Зависшая — только если зависание кончается сбросом: сторож задач по умолчанию лишь печатает виновника (CONFIG_ESP_TASK_WDT_PANIC выключен, раздел 152.5). Так что в изделии CONFIG_ESP_TASK_WDT_PANIC включают или заводят свой таймер, который через разумное время без подтверждения вызывает esp_ota_mark_app_invalid_rollback_and_reboot() (159.5).


159.5 Когда подтверждать

Подтверждение — обещание «эта прошивка умеет обновляться дальше». Подтвердить в первой строке app_main — значит отключить защиту. Разумный порядок:

  1. поднять сеть и дождаться адреса;
  2. связаться с сервером обновлений — прошивка, которая не выходит в сеть, сама себя уже не заменит;
  3. пройти самопроверку: датчики отвечают, настройки из NVS читаются;
  4. только потом esp_ota_mark_app_valid_cancel_rollback().

Если за разумное время проверка не прошла — esp_ota_mark_app_invalid_rollback_and_reboot() откатит прибор, не дожидаясь сторожевого таймера.


159.6 Безопасность обновления

В опыте образ идёт по открытому HTTP (CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP), и esp_https_ota честно предупреждает об этом при каждом обновлении:

W (2865) esp_https_ota: Continuing with insecure option because CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP is set.

В изделии так нельзя: любой в той же сети подменит прошивку. Минимум для прибора — HTTPS с сертификатом своего сервера, вшитым в прошивку (.cert_pem в esp_http_client_config_t). Дальше — подпись образа (Secure Boot): загрузчик не запустит образ без подписи вашим ключом, даже если его удалось записать. И защита от отката на старую версию с известной уязвимостью: номер secure_version в образе и счётчик в eFuse.


159.7 Грабли

Симптом Причина
обновление скачалось, а после перезапуска снова старая версия перезапуск до подтверждения: сработал откат
откат не срабатывает — зависшая прошивка остаётся не включён CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE, либо сторож задач не перезагружает: CONFIG_ESP_TASK_WDT_PANIC выключен
новая версия ни разу не подтвердила себя, но работает подтверждение не вызывается, а откат выключен — защиты нет
esp_https_ota_begin отказывает на http:// без CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP принимается только HTTPS
образ не влезает в раздел приложение выросло больше ota_0; таблицу разделов по сети не поменять
прибор качает ту же прошивку по кругу не сравнивается версия из esp_https_ota_get_img_desc с текущей
после idf.py app-flash или прошивки одного образа прибор грузит старую версию otadata указывает на ota_0, а новый образ лёг в factory; idf.py flash пишет и ota_data_initial.bin, отдельно otadata стирает idf.py erase-otadata
esp_ota_get_state_partition для factory — ошибка у заводского приложения состояния нет: ESP_ERR_NOT_SUPPORTED

159.8 Итоги

Полные исходники

main.c — обновление по сети: скачивание, подтверждение и откат, три сборки одной программы.

/* Раздел 159: обновление прошивки по сети (OTA) в QEMU.
 * Одна программа, три сборки: 1.0 в разделе factory скачивает новую прошивку по HTTP в раздел ota_0
 * и перезагружается; 2.0 стартует в состоянии «ждёт подтверждения» и подтверждает себя;
 * 3.0 (CONFIG_LAB_OTA_BREAK) перезагружается без подтверждения — загрузчик откатывает её. */
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include "esp_app_desc.h"
#include "esp_event.h"
#include "esp_http_client.h"
#include "esp_https_ota.h"
#include "esp_netif.h"
#include "esp_ota_ops.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "protocol_examples_common.h"
#include "sdkconfig.h"

static const char *state_name(esp_ota_img_states_t s)
{
    switch (s) {
    case ESP_OTA_IMG_NEW: return "NEW";
    case ESP_OTA_IMG_PENDING_VERIFY: return "PENDING_VERIFY";
    case ESP_OTA_IMG_VALID: return "VALID";
    case ESP_OTA_IMG_INVALID: return "INVALID";
    case ESP_OTA_IMG_ABORTED: return "ABORTED";
    default: return "UNDEFINED";
    }
}

void app_main(void)
{
    const esp_partition_t *run = esp_ota_get_running_partition();
    const esp_app_desc_t *app = esp_app_get_description();
    esp_ota_img_states_t st;
    esp_err_t se = esp_ota_get_state_partition(run, &st);
    printf("запуск: раздел %s по 0x%06" PRIx32 ", версия %s, состояние %s\n", run->label, run->address, app->version,
           se == ESP_OK ? state_name(st) : esp_err_to_name(se));

    if (se == ESP_OK && st == ESP_OTA_IMG_PENDING_VERIFY) {    /* первый запуск после обновления */
#if CONFIG_LAB_OTA_BREAK
        printf("версия %s не подтвердила себя — перезапуск\n", app->version);
        fflush(stdout);
        esp_restart();                                      /* загрузчик пометит образ негодным и откатит */
#else
        printf("подтверждение: %s\n", esp_err_to_name(esp_ota_mark_app_valid_cancel_rollback()));
        esp_ota_get_state_partition(run, &st);
        printf("состояние после подтверждения: %s\n", state_name(st));
        printf("done\n");
        return;
#endif
    }

    const esp_partition_t *bad = esp_ota_get_last_invalid_partition();
    if (bad != NULL) {                                      /* откатились: новый образ не подтвердил себя */
        esp_app_desc_t d;
        esp_ota_get_partition_description(bad, &d);
        esp_ota_get_state_partition(bad, &st);
        printf("откат: версия %s в разделе %s — %s, работает %s\n", d.version, bad->label, state_name(st), run->label);
        printf("done\n");
        return;
    }
    if (strcmp(run->label, "factory") != 0) {
        printf("done\n");
        return;
    }

    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    ESP_ERROR_CHECK(example_connect());

    const esp_partition_t *next = esp_ota_get_next_update_partition(NULL);
    printf("обновление: %s → раздел %s по 0x%06" PRIx32 ", %" PRIu32 " байт\n", CONFIG_LAB_OTA_URL, next->label,
           next->address, next->size);
    esp_http_client_config_t http = { .url = CONFIG_LAB_OTA_URL, .timeout_ms = 20000 };
    esp_https_ota_config_t ota = { .http_config = &http };
    esp_https_ota_handle_t h = NULL;
    esp_err_t e = esp_https_ota_begin(&ota, &h);
    esp_app_desc_t nd;
    if (e == ESP_OK)
        e = esp_https_ota_get_img_desc(h, &nd);             /* заголовок нового образа — до загрузки целиком */
    if (e == ESP_OK)
        printf("в образе: проект %s, версия %s\n", nd.project_name, nd.version);
    while (e == ESP_OK) {
        e = esp_https_ota_perform(h);
        if (e != ESP_ERR_HTTPS_OTA_IN_PROGRESS)
            break;
        e = ESP_OK;
    }
    int got = h ? esp_https_ota_get_image_len_read(h) : 0;
    esp_err_t fin = (e == ESP_OK) ? esp_https_ota_finish(h) : (h ? esp_https_ota_abort(h) : e);
    printf("скачано %d байт, итог %s; загрузка теперь из %s\n", got, esp_err_to_name(e == ESP_OK ? fin : e),
           esp_ota_get_boot_partition()->label);
    fflush(stdout);
    if (e == ESP_OK && fin == ESP_OK)
        esp_restart();
    printf("done\n");
}

Дальше: раздел 160 — длинные линии и антенны.

Открыть в интерактивном справочнике