Embedded HandbookПриложения
Все разделы › VI. Практикум
STM32F103, STM32F407, STM32G474 · уровень продвинутый · 19 мин чтения
Открыть в интерактивном справочнике

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 и передача управления

Загрузчик лежит в начале Flash — по адресу сброса стартует именно он. Остальное делят на области под образы. Схема с двумя областями — рабочей и приёмной — стоит вдвое больше памяти, но снимает главный страх: обновление можно прервать в любой момент, работающая программа цела.

Одна тонкость определяет устройство всей схемы: приложение собрано под свой адрес (48.5) и работает только оттуда — в таблице векторов и в коде стоят абсолютные адреса. Образ, собранный под область A, из области B не запустится; проверка из 48.4 его и не пропустит — точка входа окажется вне области. Путей три:

#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));
}

Почему именно так:


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 — точка входа с взведённым младшим битом. То самое, что проверяет загрузчик.

Две строки Intel HEX из примера по полям: длина, адрес, тип, данные, контрольная сумма

Что выбрать. 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 и всё равно не работать: не с тем оборудованием, ошибка в новой ветке. Схема защиты — три шага:

  1. Загрузчик помечает новый образ флагом «на пробу» и запускает его.
  2. Приложение, убедившись, что живо (связь есть, датчики отвечают), ставит флаг «подтверждено».
  3. Если после сброса образ всё ещё «на пробу» — загрузчик считает попытку неудачной и возвращается на прежнюю область.

Как хранить эти флаги. Во Flash их не переписывают, а дописывают: слово пишется только из стёртого состояния, а стереть страницу с заголовком — значит стереть и начало образа. У G4 запись идёт двойными словами с кодом исправления ошибок, и в уже записанное двойное слово можно записать разве что нули. Поэтому у каждого шага своя ячейка в заголовке (у G4 — по двойному слову, место есть в дополнении pad): «запускался на пробу» и «подтверждён». Стёртая ячейка (все единицы) значит «ещё нет», записанная — «да»; при записи образа эти ячейки пропускают. Загрузчик перед пробным запуском пишет первую, приложение — вторую; если после сброса первая записана, а вторая нет — откат.

Сторожевой таймер здесь союзник: приложение, зависшее до подтверждения, перезагрузится само и откатится.

Жизнь образа: загрузчик проверяет CRC и пишет ячейку «на пробу», приложение пишет ячейку «подтверждён»; сброс до подтверждения ведёт к откату на прежнюю область, а образ, недописанный из-за пропавшего питания, отвергается

Что где хранится, чтобы откат был возможен. Прежний образ остаётся на месте — в этом и смысл двух областей. Загрузчик помнит, какая из них рабочая, по флагам в заголовках и по номеру версии; запускает он образ только из той области, под чей адрес образ собран (48.2).


48.9 Защита

Загрузчик отвечает не только за то, чтобы обновление прошло, но и за то, чтобы прошло только правильное.

Мера От чего Где настраивается
CRC-32 образа порча при передаче и записи заголовок образа
подпись образа подмена прошивки заголовок + ключ в загрузчике
шифрование образа чтение прошивки в канале и на диске ключ в загрузчике (раздел 43)
защита от чтения (RDP) считывание Flash отладчиком байты конфигурации (раздел 11.6)
защита страниц от записи (WRP) порча самого загрузчика байты конфигурации
проверка версии откат на старую версию с известной дырой заголовок образа

Про ключи: массив в исходниках — не защита. Ключ должен быть в области, закрытой от чтения, и лучше свой на каждое изделие. Общий ключ на всю партию означает, что вскрытие одного прибора компрометирует все.

⚠️ Защита от чтения снимается только вместе со стиранием всей Flash. Включив её, вы теряете возможность считать прошивку — включая свою собственную при отладке. Второй уровень у части семейств необратим.


48.10 Обновление по воздуху

Канал — беспроводной модуль, сотовый модем, USB, SD-карта, проводной интерфейс; загрузчику всё равно, ему нужен поток байт. Разница в том, где хранится образ до записи:

Что должно быть в протоколе независимо от канала: длина и контрольная сумма всего образа заранее, нумерация кусков, подтверждение каждого, возобновление с места обрыва, тайм-аут на весь сеанс. И журнал: что за версия, когда пришла, чем закончилось — иначе разбирать отказ в поле будет не по чему.


48.11 Готовые пути, если свой не нужен

Прежде чем писать загрузчик, стоит проверить, не хватит ли готового:

Свой загрузчик пишут, когда нужен свой канал, свой формат или свои правила проверки — то есть почти всегда, если обновление идёт по воздуху.


48.12 Грабли

После прыжка сразу сбой. Не переставлен VTOR, либо приложение собрано под адрес 0x08000000, а лежит по другому.

Работает, пока нет прерываний. Тот же VTOR: векторы ищутся в загрузчике.

Образ из второй области не проходит проверку или падает. Он собран под адрес первой: нужен свой образ под каждую область, копирование в рабочую или обмен банков (48.2).

Таблица векторов «не выравнивается». Заголовок образа не кратен требуемому выравниванию. Дополнять до 512 байт.

Устройство навсегда в загрузчике. Признак запроса не сбрасывается после чтения, либо секция без обнуления всё-таки обнуляется стартовым кодом.

Приложение просит перепрошивку, а загрузчик её не видит. Признак в .noinit у двух программ лежит по разным адресам: нужна общая область в обоих скриптах компоновщика.

Прошивка принялась, но не запускается. Проверить порядок: не забыт ли байт при передаче, совпадает ли длина в заголовке с реально записанным, считается ли CRC по тем же границам, что и при сборке.

Стирание не проходит. Граница области не совпадает с границей страницы или сектора; у F4 — попытка применить формулу вместо карты секторов.

Обновление обрывает сторожевой таймер. Стирание страницы — десятки миллисекунд; либо перезапускать сторож внутри цикла записи, либо на время обновления брать больший интервал.

Питание пропало посреди записи. Ради этого и делают две области: рабочая цела, загрузчик увидит незавершённый образ по флагу и запустит прежний.

Новая прошивка «кирпичит» устройство. Нет подтверждения работоспособности и отката.

Ресурс Flash кончился. 10 000 циклов — это 27 лет при обновлении раз в день, но всего неделя, если приложение пишет настройки в ту же страницу раз в минуту. Настройки — в отдельную страницу и с чередованием.

Загрузчик оказался с ошибкой. Его не обновить тем же механизмом. Отсюда правило: минимум кода, никаких экспериментов, тщательная проверка перед выпуском партии.


48.13 Итоги

Дальше: раздел 48а «Подпись прошивки: загрузчик проверяет, кто её сделал».

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