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 Разделы: где живут две прошивки
Таблица разделов для обновлений — готовая в 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 и может сообщить
на сервер, что обновление не прижилось.
Откат срабатывает на любой перезапуск до подтверждения: программный, от сторожевого таймера,
от паники, от пропадания питания. Поэтому прошивка, которая падает при старте, сама вернёт
прибор на прошлую версию. Зависшая — только если зависание кончается сбросом: сторож задач
по умолчанию лишь печатает виновника (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 — значит отключить защиту. Разумный порядок:
- поднять сеть и дождаться адреса;
- связаться с сервером обновлений — прошивка, которая не выходит в сеть, сама себя уже не заменит;
- пройти самопроверку: датчики отвечают, настройки из NVS читаются;
- только потом
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 Итоги
- Разделы:
factoryпо 0x10000,ota_0по 0x110000,ota_1по 0x210000, по 1 МБ;otadata— 8 КБ по 0xd000. - Обновление пишется в свободный раздел;
esp_https_ota_finishпереключает загрузку только после проверки образа. - В опыте: образ 564 208 байт, свободно 46 % раздела; версия из
version.txtвидна в заголовке до загрузки. - Новый образ стартует в
PENDING_VERIFY;esp_ota_mark_app_valid_cancel_rollback()делает егоVALID. - Перезапуск до подтверждения — откат: образ
ABORTED, работает прежнее приложение. - В изделии — HTTPS с сертификатом сервера, подпись образа и защита от отката на старую версию.
Полные исходники
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 — длинные линии и антенны.