48. Собственный загрузчик и обновление прошивки
Пока устройство на столе, программу меняет отладчик. Как только оно уехало к заказчику — нужен способ обновиться без отладчика, без вскрытия корпуса и без риска превратить прибор в кирпич посреди процесса. Этим занимается загрузчик: маленькая программа, которая стартует первой, решает, что запускать, и умеет принять новую прошивку.
Раздел — про то, как он устроен: раскладка памяти, передача управления,
формат образа, проверка целостности, откат на предыдущую версию и
обновление по воздуху. Механика записи во Flash разобрана в разделе 11,
заводской загрузчик и BOOT0 — в разделе 4; здесь всё это собирается в
рабочую схему.
Как это называется в документации
Оригинал Перевод bootloader загрузчик application, image приложение, образ прошивки slot, bank область под образ vector table offset (VTOR) смещение таблицы векторов main stack pointer (MSP) основной указатель стека reset handler обработчик сброса, точка входа Thumb bit младший бит адреса: признак набора команд Intel HEX текстовый формат образа с адресами firmware over the air (FOTA, OTA) обновление по беспроводному каналу rollback откат на предыдущую версию confirm / revert подтверждение работоспособности / возврат in-application programming (IAP) запись Flash из своей же программы read-out protection (RDP) защита Flash от чтения write protection (WRP) защита страниц от записи
48.1 Когда он нужен, а когда нет
Честный ответ: чаще не нужен. Датчик, который считает посетителей, согласуют с заказчиком на этапе прототипа и заливают программу на производстве; доработка выходит вместе со следующей партией, а старые приборы просто меняют.
Загрузчик нужен там, где ценность устройства — в программе и где физический доступ дорог:
- распределённая сеть датчиков, до каждого не доехать;
- медицинская и измерительная техника с длинным жизненным циклом;
- изделия, где после продажи появляются новые функции;
- всё, где может понадобиться срочно закрыть ошибку.
И сразу трезво: свой загрузчик — это отдельная программа, которую нельзя обновить тем же способом. Она обязана быть простой, отлаженной и неизменной годами. Всё, что можно вынести в приложение, выносят в приложение.
48.2 Раскладка памяти
Загрузчик лежит в начале Flash — по адресу сброса стартует именно он. Остальное делят на области под образы. Схема с двумя областями — рабочей и приёмной — стоит вдвое больше памяти, но снимает главный страх: обновление можно прервать в любой момент, работающая программа цела.
Одна тонкость определяет устройство всей схемы: приложение собрано под свой адрес (48.5) и работает только оттуда — в таблице векторов и в коде стоят абсолютные адреса. Образ, собранный под область A, из области B не запустится; проверка из 48.4 его и не пропустит — точка входа окажется вне области. Путей три:
- вторая область только приёмная. Проверив принятый образ, загрузчик копирует его в рабочую. Копирование должно уметь продолжиться после сбоя питания, а прежний образ на это время теряется, если его не хранят ещё где-то — например, во внешней памяти;
- два образа на выпуск. Один собран под A, другой под B, и на прибор отправляют тот, что подходит к свободной области;
- два банка Flash. У камней с двумя банками (G474 в двухбанковом
режиме, F42x/F43x) банки меняются местами в адресном пространстве —
битом
FB_MODEу G4 иUFB_MODEу F4 вSYSCFG_MEMRMP, — и оба образа собраны под один адрес; загрузчик тогда держат в начале обоих банков.
#define BOOT_SIZE (16u * 1024u)
#define SLOT_SIZE ((FLASH_BYTES - BOOT_SIZE) / 2u)
#define SLOT_A_ORIGIN (FLASH_ORIGIN + BOOT_SIZE)
#define SLOT_B_ORIGIN (SLOT_A_ORIGIN + SLOT_SIZE)
| Камень | Flash | Единица стирания | Загрузчик | Область |
|---|---|---|---|---|
| F103 | 128 КБ | страница 1 КБ | 16 КБ | 56 КБ |
| G474 | 512 КБ | страница 2 КБ | 16 КБ | 248 КБ |
| F407 | 1 МБ | сектор 16…128 КБ | 128 КБ | 384 КБ |
⚠️ У F4 формула не работает. Секторы разного размера: четыре по 16 КБ, один 64 КБ, дальше по 128 КБ. Деление «пополам после загрузчика» даёт 504 КБ — не кратно ни одному сектору, а стереть половину сектора нельзя. Раскладку задают по карте: секторы 0…4 — загрузчик (128 КБ), 5…7 и 8…10 — две области по 384 КБ, сектор 11 остаётся под настройки.
Это тот случай, когда _Static_assert окупается сразу: границы областей
проверяются на кратность единице стирания при компиляции, а не при
первом обновлении в поле.
48.3 Заголовок образа и выравнивание
Перед прошивкой в области лежит заголовок: всё, что нужно, чтобы решить — запускать или нет.
typedef struct {
uint32_t magic; /* образ вообще есть */
uint32_t size; /* длина прошивки за заголовком, байт */
uint32_t crc; /* CRC-32 этих size байт */
uint32_t version; /* чтобы не откатиться назад молча */
uint32_t entry; /* адрес входа — проверяется на попадание в область */
uint32_t flags; /* признак «принята, но ещё не подтверждена» */
uint32_t reserved[2];
uint8_t pad[IMG_HDR_SIZE - 8u * 4u];
} img_hdr_t;
Заголовку хватило бы 32 байт, но он дополнен до 512. Причина не в
запасе на будущее, а в выравнивании: сразу за заголовком начинается
таблица векторов приложения, а её адрес обязан быть кратен размеру
таблицы, округлённому вверх до степени двойки. Регистр смещения таблицы
векторов просто не хранит младшие семь бит — записать туда
0x08004020 невозможно, и приложение уйдёт в сбой на первом же
прерывании.
Второе требование: у G4 Flash пишется двойными словами, поэтому размер заголовка должен быть кратен восьми. Оба условия проверяются сборкой.
48.4 Проверка образа перед запуском
Первые два слова любой прошивки Cortex-M — начальное значение указателя стека и адрес обработчика сброса. Загрузчик смотрит на них до прыжка: если они не похожи на правду, приложение развалится ещё до первой команды, и понять причину будет трудно.
static int image_valid(uint32_t slot)
{
const img_hdr_t *h = (const img_hdr_t *)slot;
const uint32_t *vec = (const uint32_t *)(slot + IMG_HDR_SIZE);
uint32_t sp, pc;
if (h->magic != IMG_MAGIC)
return 0;
if (h->size == 0u || h->size > SLOT_SIZE - IMG_HDR_SIZE)
return 0;
sp = vec[0];
pc = vec[1];
/* Указатель стека должен смотреть в ОЗУ. */
if (sp < RAM_ORIGIN || sp > RAM_ORIGIN + RAM_BYTES)
return 0;
/* Адрес входа — внутрь этой же области и с признаком Thumb. */
if ((pc & 1u) == 0u)
return 0;
if (pc < slot + IMG_HDR_SIZE || pc >= slot + SLOT_SIZE)
return 0;
return crc32((const void *)(slot + IMG_HDR_SIZE), h->size) == h->crc;
}
Проверки идут от дешёвых к дорогим: магическое число, длина, разумность стека и точки входа — и только потом CRC по всему образу, которая занимает миллисекунды.
Что даёт CRC и чего не даёт. Она ловит порчу: оборванную передачу, сбой записи, деградацию ячейки. Она не защищает от подмены: кто угодно может посчитать CRC своей прошивки. Если важно, чтобы устройство принимало только ваши обновления, нужна подпись — HMAC на общем секрете или, лучше, электронная подпись на паре ключей (раздел 43). Проверка подписи живёт в загрузчике, а ключ проверки — в его же неизменяемой части.
48.5 Передача управления
Порядок здесь важнее кода.
static void jump_to_app(uint32_t slot)
{
uint32_t sp = *(const uint32_t *)(slot + IMG_HDR_SIZE);
uint32_t pc = *(const uint32_t *)(slot + IMG_HDR_SIZE + 4u);
uint32_t i;
__disable_irq();
/* Снять все разрешения и все висящие запросы: приложение начинает с чистого. */
for (i = 0u; i < (sizeof(NVIC->ICER) / sizeof(NVIC->ICER[0])); i++) {
NVIC->ICER[i] = 0xFFFFFFFFu;
NVIC->ICPR[i] = 0xFFFFFFFFu;
}
SysTick->CTRL = 0u;
SysTick->LOAD = 0u;
SysTick->VAL = 0u;
SCB->VTOR = slot + IMG_HDR_SIZE; /* где искать векторы теперь */
__DSB();
__ISB();
/* Стек, разрешение прерываний и переход — одной вставкой: после смены MSP
* компилятору нечего читать из кадра старого стека. Сюда уже не вернёмся. */
__asm volatile ("msr msp, %0\n cpsie i\n bx %1" :: "r"(sp), "r"(pc));
}
Почему именно так:
- Сначала выключить прерывания и периферию. Прерывание, пришедшее посреди перехода, найдёт новый стек и старую таблицу векторов — и уйдёт в никуда. Снимать надо и разрешения, и уже висящие запросы.
- Вернуть камень к состоянию после сброса. Код выше гасит только NVIC
и SysTick, а PLL, DMA и включённые блоки остаются такими, какими их
оставил загрузчик. Приложение со стандартной инициализацией ждёт камень
после сброса: HAL вернёт ошибку, если PLL уже тактует систему с другими
настройками, а незакрытый канал DMA продолжит писать в ОЗУ приложения.
Поэтому каналы DMA останавливают битом
EN(у F103 сбросить DMA через RCC нечем), остальные блоки сбрасывают импульсом в регистрах сбросаRCC->APBxRSTR(у F4 и G4 — иAHBxRSTR), системную частоту возвращают на HSI и выключают PLL. В HAL у F4 и G4 это делаютHAL_RCC_DeInit()иHAL_DeInit(). Надёжнее всего вовсе не включать периферию, пока не решено, что запускать: признак и образ проверяют первыми командами после сброса и сразу прыгают, а периферию загрузчик включает, только если остаётся в нём сам. - Потом переставить таблицу векторов. У Cortex-M0 регистра смещения нет: там таблицу приложения либо копируют в начало ОЗУ и переключают область памяти, либо держат в загрузчике переходники.
- Потом стек. После записи в указатель стека нельзя обращаться к локальным переменным прежней функции — их больше нет. Поэтому смену стека и переход делают одной ассемблерной вставкой: адрес входа уже в регистре.
__DSBи__ISB— чтобы запись смещения успела вступить в силу до первого прерывания.- Приложение обязано быть собрано под свой адрес. В скрипте
компоновщика начало Flash — не
0x08000000, а адрес области плюс размер заголовка (раздел 11.3).
48.6 Как приложение просит перепрошивку
Загрузчику нужно знать: обычный это старт или пользователь запросил обновление. Варианты, от простого к надёжному:
| Где хранить признак | Переживает | Цена |
|---|---|---|
| переменная в ОЗУ в секции без обнуления | программный сброс | ничего; пропадает при снятии питания |
резервные регистры (домен VBAT) |
снятие питания при батарейке | нужен резервный домен |
| страница Flash | всё | цикл стирания на каждый запрос |
| вывод/кнопка при старте | всё | нужен физический доступ |
Самый частый — первый, и его достаточно: обновление всегда начинается с работающего приложения, а оно перезагружается программно.
#define REBOOT_MAGIC 0xB00710ADu
uint32_t reboot_flag __attribute__((section(".noinit")));
static void request_update(void)
{
reboot_flag = REBOOT_MAGIC;
NVIC_SystemReset(); /* сброс без снятия питания */
}
static int update_requested(void)
{
int yes = (reboot_flag == REBOOT_MAGIC);
reboot_flag = 0u; /* прочитали — сбросили, иначе зациклимся */
return yes;
}
Секция должна быть исключена из обнуления в стартовом коде и в
скрипте компоновщика; иначе признак пропадёт до main. И читать его
надо ровно один раз, со сбросом: иначе устройство навсегда останется в
загрузчике.
И главное условие: признак лежит по одному адресу в обеих программах.
Загрузчик и приложение собираются каждый своим скриптом компоновщика, и
секция .noinit у каждого окажется там, где кончились его собственные
переменные: в проверочной сборке для F103 — 0x20000800 у загрузчика и
0x20000258 у приложения. Загрузчик прочитает не ту ячейку, которую
записало приложение. Поэтому в оба скрипта вносят одинаковую отдельную
область в конце ОЗУ — для 20 КБ это
NOINIT (rwx) : ORIGIN = 0x20004FF0, LENGTH = 16 — и кладут секцию туда,
а RAM укорачивают на те же 16 байт, чтобы вершина стека легла ниже.
Можно и обращаться к адресу напрямую:
#define REBOOT_FLAG (*(volatile uint32_t *)0x20004FF0u).
48.7 Формат образа: двоичный или Intel HEX
Двоичный файл — это ровно содержимое Flash: ни адресов, ни разметки. Intel HEX — текстовый формат, где каждая строка несёт кусок данных и адрес, куда его класть.
:020000040800F2
:1000000000000120110A0008190900081F09000852
:00000001FF
Строка: двоеточие, длина данных, адрес (два байта), тип записи, данные,
контрольная сумма. Типы: 00 — данные, 01 — конец файла, 04 —
расширенный линейный адрес (задаёт старшую половину адреса). В файле от
компоновщика обычно есть и 05 — адрес запуска: objcopy ставит его
перед концом файла. Точка входа и так лежит во втором слове таблицы
векторов, поэтому 05 пропускают — как и 03, такой же адрес запуска для
сегментной памяти старых x86. Запись 02, сегментный адрес, встречается у
части инструментов, и её пропускать нельзя: значение, умноженное на 16,
прибавляют к адресам следующих строк. Всего типов шесть, и разборщик
должен знать все: тот, что понимает только 00, 01 и 04 и отвергает
остальное, откажется от настоящего файла прошивки.
Контрольная сумма — дополнение до двух суммы всех остальных байт, поэтому сумма всех байт строки вместе с ней даёт ноль в младшем байте — на этом и строится проверка:
for (i = 0u; i < n; i++)
sum += raw[i];
if ((sum & 0xFFu) != 0u)
return 0; /* сумма всех байт, включая контрольную, — ноль */
Разобрав пример выше, получаем: базовый адрес 0x08000000, первое слово
0x20010000 — вершина ОЗУ, второе 0x08000A11 — точка входа с
взведённым младшим битом. То самое, что проверяет загрузчик.
Что выбрать. HEX удобен тем, что самопроверяем и несёт адреса, — но 16 байт данных превращаются в 45 байт файла: втрое больше. Прошивка 60 КБ по UART на 115200 идёт 5,3 секунды двоичным файлом и 15 секунд в HEX. Для канала, за который платят помегабайтно, разница решающая. Практичный компромисс: HEX для настольного инструмента, двоичный образ с заголовком — для устройства.
48.8 Приём, запись, подтверждение
Сколько это длится. Числа для G474 (типовые характеристики Flash):
| Действие | Время |
|---|---|
| стирание страницы 2 КБ | 22,0 мс |
| запись страницы 2 КБ | 20,9 мс |
| стирание области 248 КБ целиком | 2,73 с |
| запись области целиком | 2,59 с |
| обновление области полностью | 5,3 с |
| стирание и запись только 60 КБ прошивки | 1,29 с |
Отсюда два вывода. Первый: обновление — процесс на секунды, во время которого устройство занято; это надо показывать пользователю и учитывать в сторожевом таймере (раздел 12). Второй: стирать всю область, когда образ вчетверо меньше, — терять четыре секунды впустую.
Как принимать. Кусками по размеру страницы, с подтверждением каждого куска и возможностью продолжить с прерванного места: канал ненадёжен, а начинать заново с середины обидно. Полученный кусок пишут сразу — в ОЗУ микроконтроллера образ целиком не поместится.
Подтверждение работоспособности. Новая прошивка может пройти CRC и всё равно не работать: не с тем оборудованием, ошибка в новой ветке. Схема защиты — три шага:
- Загрузчик помечает новый образ флагом «на пробу» и запускает его.
- Приложение, убедившись, что живо (связь есть, датчики отвечают), ставит флаг «подтверждено».
- Если после сброса образ всё ещё «на пробу» — загрузчик считает попытку неудачной и возвращается на прежнюю область.
Как хранить эти флаги. Во Flash их не переписывают, а дописывают:
слово пишется только из стёртого состояния, а стереть страницу с
заголовком — значит стереть и начало образа. У G4 запись идёт двойными
словами с кодом исправления ошибок, и в уже записанное двойное слово можно
записать разве что нули. Поэтому у каждого шага своя ячейка в заголовке
(у G4 — по двойному слову, место есть в дополнении pad): «запускался на
пробу» и «подтверждён». Стёртая ячейка (все единицы) значит «ещё нет»,
записанная — «да»; при записи образа эти ячейки пропускают. Загрузчик
перед пробным запуском пишет первую, приложение — вторую; если после
сброса первая записана, а вторая нет — откат.
Сторожевой таймер здесь союзник: приложение, зависшее до подтверждения, перезагрузится само и откатится.
Что где хранится, чтобы откат был возможен. Прежний образ остаётся на месте — в этом и смысл двух областей. Загрузчик помнит, какая из них рабочая, по флагам в заголовках и по номеру версии; запускает он образ только из той области, под чей адрес образ собран (48.2).
48.9 Защита
Загрузчик отвечает не только за то, чтобы обновление прошло, но и за то, чтобы прошло только правильное.
| Мера | От чего | Где настраивается |
|---|---|---|
| CRC-32 образа | порча при передаче и записи | заголовок образа |
| подпись образа | подмена прошивки | заголовок + ключ в загрузчике |
| шифрование образа | чтение прошивки в канале и на диске | ключ в загрузчике (раздел 43) |
| защита от чтения (RDP) | считывание Flash отладчиком | байты конфигурации (раздел 11.6) |
| защита страниц от записи (WRP) | порча самого загрузчика | байты конфигурации |
| проверка версии | откат на старую версию с известной дырой | заголовок образа |
Про ключи: массив в исходниках — не защита. Ключ должен быть в области, закрытой от чтения, и лучше свой на каждое изделие. Общий ключ на всю партию означает, что вскрытие одного прибора компрометирует все.
⚠️ Защита от чтения снимается только вместе со стиранием всей Flash. Включив её, вы теряете возможность считать прошивку — включая свою собственную при отладке. Второй уровень у части семейств необратим.
48.10 Обновление по воздуху
Канал — беспроводной модуль, сотовый модем, USB, SD-карта, проводной интерфейс; загрузчику всё равно, ему нужен поток байт. Разница в том, где хранится образ до записи:
- прямо во вторую область Flash — просто, ничего лишнего;
- во внешнюю память (SPI Flash, SD — раздел 41, 42) — если внутренней не хватает или образ надо хранить на будущее;
- у посредника — модуль связи принимает файл целиком и отдаёт микроконтроллеру по своему интерфейсу.
Что должно быть в протоколе независимо от канала: длина и контрольная сумма всего образа заранее, нумерация кусков, подтверждение каждого, возобновление с места обрыва, тайм-аут на весь сеанс. И журнал: что за версия, когда пришла, чем закончилось — иначе разбирать отказ в поле будет не по чему.
48.11 Готовые пути, если свой не нужен
Прежде чем писать загрузчик, стоит проверить, не хватит ли готового:
- Заводской загрузчик в системной памяти. Есть в каждом камне;
принимает прошивку по UART, у старших семейств — по USB (DFU), CAN,
SPI, I²C (раздел 4.9). Запускается выводом
BOOT0либо из самого приложения: оно возвращает камень к состоянию после сброса (48.5) и прыгает в системную память так же, как загрузчик прыгает в приложение (адрес — в описании заводского загрузчика семейства). У G4 вывод можно заменить байтами конфигурации — битамиnSWBOOT0иnBOOT0вFLASH_OPTR. Если UART или USB выведены наружу, это обновление без своего загрузчика и без вскрытия корпуса — но без подписи и отката. - Отладчик по SWD. Две минуты на приборе на столе.
- Готовые библиотеки безопасного обновления. Умеют подпись, две области, откат и журнал; цена — объём и время на изучение.
Свой загрузчик пишут, когда нужен свой канал, свой формат или свои правила проверки — то есть почти всегда, если обновление идёт по воздуху.
48.12 Грабли
После прыжка сразу сбой. Не переставлен VTOR, либо приложение
собрано под адрес 0x08000000, а лежит по другому.
Работает, пока нет прерываний. Тот же VTOR: векторы ищутся в
загрузчике.
Образ из второй области не проходит проверку или падает. Он собран под адрес первой: нужен свой образ под каждую область, копирование в рабочую или обмен банков (48.2).
Таблица векторов «не выравнивается». Заголовок образа не кратен требуемому выравниванию. Дополнять до 512 байт.
Устройство навсегда в загрузчике. Признак запроса не сбрасывается после чтения, либо секция без обнуления всё-таки обнуляется стартовым кодом.
Приложение просит перепрошивку, а загрузчик её не видит. Признак
в .noinit у двух программ лежит по разным адресам: нужна общая область
в обоих скриптах компоновщика.
Прошивка принялась, но не запускается. Проверить порядок: не забыт ли байт при передаче, совпадает ли длина в заголовке с реально записанным, считается ли CRC по тем же границам, что и при сборке.
Стирание не проходит. Граница области не совпадает с границей страницы или сектора; у F4 — попытка применить формулу вместо карты секторов.
Обновление обрывает сторожевой таймер. Стирание страницы — десятки миллисекунд; либо перезапускать сторож внутри цикла записи, либо на время обновления брать больший интервал.
Питание пропало посреди записи. Ради этого и делают две области: рабочая цела, загрузчик увидит незавершённый образ по флагу и запустит прежний.
Новая прошивка «кирпичит» устройство. Нет подтверждения работоспособности и отката.
Ресурс Flash кончился. 10 000 циклов — это 27 лет при обновлении раз в день, но всего неделя, если приложение пишет настройки в ту же страницу раз в минуту. Настройки — в отдельную страницу и с чередованием.
Загрузчик оказался с ошибкой. Его не обновить тем же механизмом. Отсюда правило: минимум кода, никаких экспериментов, тщательная проверка перед выпуском партии.
48.13 Итоги
- Загрузчик нужен там, где ценность в программе, а физический доступ дорог; в остальных случаях хватает отладчика и заводского загрузчика.
- Две области вместо одной стоят вдвое больше Flash и снимают риск обрыва обновления. Образ собран под адрес своей области и из «чужой» не запустится: его копируют в рабочую, собирают два образа или меняют банки местами.
- Признак перепрошивки в
.noinitкладут по одному адресу в обеих программах — общей областью в конце ОЗУ. - Границы областей — строго по границам страниц; у F4 раскладку задают по карте секторов, потому что они разного размера.
- Заголовок образа дополняют до 512 байт: за ним идёт таблица векторов, и её адрес обязан быть выровнен.
- Перед прыжком проверяют магию, длину, указатель стека, точку входа с битом Thumb и CRC; CRC ловит порчу, но не подмену — от подмены подпись.
- Передача управления: выключить прерывания, вернуть периферию и такт
к состоянию после сброса, переставить
VTOR, задатьMSP, прыгнуть по второму слову. - Intel HEX самопроверяем и несёт адреса, но втрое длиннее двоичного образа: 15 секунд против 5,3 на 115200.
- Полное обновление области 248 КБ — около 5,3 секунды: стирание плюс запись; стирать имеет смысл только занятые страницы.
- Подтверждение работоспособности и откат — обязательная часть, иначе одна неудачная прошивка стоит выезда к устройству.
Дальше: раздел 48а «Подпись прошивки: загрузчик проверяет, кто её сделал».