Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32F405, STM32F407 · уровень средний · 60 мин чтения
Открыть в интерактивном справочнике

198щ. Мини-проект: свой загрузчик серийного прибора на STM32F405 — раскладка по секторам, буферный слот, обновление по UART-Bluetooth и защита от обрыва питания

Раздел 48 разобрал загрузчик в общем виде, 48а добавил подпись, 48б показал готовый MCUboot с обменом слотов, раздел 195 — схему A/B с подтверждением. Здесь всё это сходится на одном настоящем случае: загрузчике серийного прибора на STM32F405, который обновляется у покупателей через Bluetooth-модуль. Схема у прибора рабочая и простая — и в ней есть места, где обрыв питания или связи оставляет прибор без прошивки или, хуже, с недописанной.

Мы разбираем эту схему словами, без её кода и констант, и пишем свой загрузчик с нуля на той же раскладке секторов F405: заголовок образа вместо голых меток, приём только в буферный слот, копирование с журналом, CRC-32 по всему образу, проверка происхождения и номера версии. Весь код раздела — свой: прошивка — 1133 строки в 13 файлах (загрузчик, проверочное приложение, заголовки, скрипты компоновщика), модель и сборщик образов на ПК — ещё 930 строк. Загрузчик занимает 4528 байт флеш-памяти.

Платы под рукой не было. Логика загрузчика — тот же C-код — собрана gcc на компьютере и прогнана против модели флеш-памяти F405 и модели канала: в модели пропадает питание на каждой операции стирания и записи, теряются и портятся байты в линии. Второй, независимый расчёт на Python собирает образы, считает CRC-32, HMAC и кадры. Загрузчик и проверочное приложение собраны arm-none-eabi-gcc под Cortex-M4 и запущены в QEMU на машине netduinoplus2 (это STM32F405): там проверены выбор образа, переход и обмен кадрами по UART. Запись во флеш-память QEMU не моделирует — это видно по опыту в 198щ.2, и всё, что касается записи, проверено только на модели.

Опоры: RM0090 Rev 19 (таблица 5 — секторы; 3.6 — стирание и запись, ключи FLASH_KEYR, таблица 12 — разрядность PSIZE; 4 — блок CRC; 5.1.2 — резервный домен; 26 — резервные регистры RTC; 39.1 — номер кристалла); даташит DS8626 Rev 9 (таблица 40 — время записи слова и стирания секторов, таблица 42 — ресурс); заголовок stm32f405xx.h из пакета STM32F4xx_DFP 2.17.1 и core_cm4.h из CMSIS 5.9.0 — с ними сверены все адреса и биты регистров. Лицензия: свой код раздела — свободно.

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

Оригинал Перевод
sector erase (SER, SNB) стирание сектора: бит SER и номер сектора SNB
program/erase parallelism (PSIZE) разрядность записи и стирания: x8, x16, x32
program sequence error (PGSERR) ошибка порядка записи
backup domain write protection (DBP) запрет записи в резервный домен
RTC backup registers (RTC_BKPxR) резервные регистры RTC: 20 слов на батарейке
backup SRAM (BKPSRAM) резервное ОЗУ, 4 КБ
program parallelism error (PGPERR) ошибка разрядности: доступ не того размера, что задан в PSIZE
write-ahead journal журнал: сначала записать намерение, потом делать
staging slot (download slot) буферный слот: сюда принимают, отсюда копируют
stop-and-wait ARQ «послал — жди подтверждения»: повтор по тайм-ауту
anti-rollback защита от отката на старую версию
transparent mode (UART bridge) прозрачный режим модуля: что пришло по радио, то и в UART

198щ.1 Исходная схема, её слабые места и новая раскладка

Свой загрузчик на STM32F405: слева флеш-память 1 МБ по секторам — сектор 0 загрузчик 4528 байт, секторы 1 и 2 журнал по 512 записей, секторы 3 и 4 настройки приложения, секторы 5 и 6 рабочий слот 256 КБ с заголовком 512 байт, секторы 7 и 8 буферный слот 256 КБ, секторы 9–11 свободны; справа хост, Bluetooth-модуль в прозрачном режиме и USART3, кадры идут в proto.c и словами пишутся в буферный слот, загрузчик boot.c по журналу READY, COPY k, IDLE копирует буфер в рабочий слот, проверяет CRC-32 и переходит в приложение; исходная схема с приёмом прямо в рабочие секторы оставляет недописанную прошивку в 6156 из 6660 точек обрыва

Как устроен загрузчик прибора. Он лежит с начала флеш-памяти и занимает первые секторы; по фиксированным адресам в нём записаны его идентификатор и версия. Приложение начинается с сектора 5. Признак «приложение есть» — несколько констант-меток на фиксированных адресах внутри образа приложения; загрузчику достаточно увидеть любую из них. Сектор 7 — буферный слот. Обновление идёт двумя путями:

Переход: проверить метки, взять указатель стека из первого слова образа, прыгнуть по второму; таблицу векторов переставляет само приложение. Номер кристалла — 96 бит — сворачивается аппаратным блоком CRC в одно слово, из которого делается имя Bluetooth-модуля. Ещё одна деталь: обработчик HardFault приложения стирает собственные секторы и сбрасывает прибор, чтобы тот ушёл в загрузчик.

Что в этом плохо и что мы делаем вместо.

Слабое место Чем грозит В нашем загрузчике
метки присутствия вместо проверки целостности оборванный приём с меткой в начале образа выглядит готовой прошивкой заголовок: длина, CRC-32 тела, CRC-32 самого заголовка
аддитивная 16-битная сумма пакета перестановку байт не видит совсем (198щ.3) CRC-16 кадра и CRC-32 образа
запись по байту вчетверо больше операций записи запись словами, PSIZE = x32
приём прямо в рабочие секторы обрыв связи или питания — прибор без прошивки или с недописанной приём только в буфер; рабочий слот трогает загрузчик после проверки
копирование без сверки ошибка записи останется незамеченной сверка каждого слова, отметки в журнале по секторам, CRC после копии
нет проверки происхождения прошивку может залить любой, кто знает формат HMAC-SHA-256 по заголовку и телу
нет номера версии можно вернуть старую прошивку с известной ошибкой версия в заголовке, старшая версия в журнале
HardFault стирает приложение один сбой — прибор без прошивки до перепрошивки приложение себя не стирает; запуск останавливает окно ожидания загрузчика (198щ.2)

Раскладка. Секторы F405 разного размера (RM0090, таблица 5): четыре по 16 КБ, один 64 КБ и семь по 128 КБ. Модель печатает ту же карту:

  сектор  0: 0x08000000,  16 КБ
  сектор  1: 0x08004000,  16 КБ
  сектор  2: 0x08008000,  16 КБ
  сектор  3: 0x0800c000,  16 КБ
  сектор  4: 0x08010000,  64 КБ
  сектор  5: 0x08020000, 128 КБ
  сектор  6: 0x08040000, 128 КБ
  сектор  7: 0x08060000, 128 КБ
  сектор  8: 0x08080000, 128 КБ
  сектор  9: 0x080a0000, 128 КБ
  сектор 10: 0x080c0000, 128 КБ
  сектор 11: 0x080e0000, 128 КБ
Секторы Что лежит Почему так
0 (16 КБ) загрузчик помещается с запасом: 4528 байт; сектор можно закрыть от записи (WRP)
1–2 (по 16 КБ) журнал обновления, две копии маленькие секторы стираются за 250 мс, а не за секунду
3–4 (16 + 64 КБ) настройки приложения загрузчик их не трогает
5–6 (256 КБ) рабочий слот: заголовок 512 байт и приложение как у прибора — приложение с сектора 5
7–8 (256 КБ) буферный слот как у прибора — буфер с сектора 7
9–11 (384 КБ) свободно данные, журналы приложения

Скрипт компоновщика загрузчика ограничивает его сектором 0: вырастет больше 16 КБ — сборка остановится, а не затрёт журнал. Последние 16 байт ОЗУ оба скрипта отдают под ячейку запроса (198щ.4):

/* boot.ld — загрузчик: сектор 0 (16 КБ). Больше — ошибка сборки, а не порча журнала в секторе 1. */
ENTRY(Reset_Handler)

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 16K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K - 16   /* последние 16 байт — ячейка запроса */
}

_estack = ORIGIN(RAM) + LENGTH(RAM);

SECTIONS
{
  .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
  .text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH
  .data : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > RAM AT > FLASH
  _sidata = LOADADDR(.data);
  .bss (NOLOAD) : { _sbss = .; *(.bss .bss.*) *(COMMON) . = ALIGN(4); _ebss = .; } > RAM
}

Приложение собрано под адрес после заголовка — 0x08020200. Этот адрес кратен 512, как требует регистр VTOR (48.3):

/* app.ld — приложение: рабочий слот 0x08020000, первые 512 байт — заголовок образа,
 * таблица векторов с 0x08020200 (кратно 512 — VTOR это требует). */
ENTRY(Reset_Handler)

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08020200, LENGTH = 256K - 512
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K - 16   /* та же ячейка запроса, что у загрузчика */
}

_estack = ORIGIN(RAM) + LENGTH(RAM);

SECTIONS
{
  .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
  .text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH
  .data : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > RAM AT > FLASH
  _sidata = LOADADDR(.data);
  .bss (NOLOAD) : { _sbss = .; *(.bss .bss.*) *(COMMON) . = ALIGN(4); _ebss = .; } > RAM
}

Заголовок образа — 60 байт полей, дополненных байтами 0xFF до 512, и запись журнала — 32 байта (bl.h):

#define IMG_MAGIC         0x31474D49u   /* 'I','M','G','1' */
typedef struct {
    uint32_t magic;
    uint32_t hdr_size;      /* 512 */
    uint32_t body_size;     /* байт тела за заголовком, кратно 4 */
    uint32_t body_crc;      /* CRC-32 тела */
    uint32_t version;       /* растёт с каждым выпуском */
    uint32_t load_addr;     /* под какой адрес собрано тело: APP_ENTRY_BASE */
    uint8_t  tag[32];       /* HMAC-SHA-256 байт 0…23 заголовка и тела */
    uint32_t hdr_crc;       /* CRC-32 байт 0…55 */
} img_hdr_t;                /* 60 байт; до 512 — 0xFF */
_Static_assert(sizeof(img_hdr_t) == 60u, "img_hdr_t");
_Static_assert(offsetof(img_hdr_t, hdr_crc) == 56u, "hdr_crc");
#define JRN_MAGIC         0x314E524Au   /* 'J','R','N','1' */
enum { ST_IDLE = 0, ST_RX = 1, ST_READY = 2, ST_COPY = 3 };
typedef struct {
    uint32_t magic;
    uint32_t seq;           /* номер записи: больший — новее */
    uint32_t state;         /* ST_* */
    uint32_t arg;           /* ST_COPY: сколько секторов уже скопировано */
    uint32_t max_version;   /* старшая версия, когда-либо установленная */
    uint32_t img_size;      /* размер образа в буфере вместе с заголовком */
    uint32_t body_crc;      /* CRC-32 тела образа в буфере */
    uint32_t crc;           /* CRC-32 слов 0…6 */
} jrn_rec_t;
_Static_assert(sizeof(jrn_rec_t) == 32u, "jrn_rec_t");

Образ собирает mkimage.py — на Python и независимо от C: CRC-32 там из zlib, HMAC из hmac и hashlib. Загрузчик (тот же C-код в модели) принимает все три тестовых образа и отвергает поддельный по подлинности:

  c v1: 4608 байт, image_check 0, версия 1, body_crc 92b98d9f, hdr_crc 0af7c299, подлинность да
  c v2: 6656 байт, image_check 0, версия 2, body_crc 29ef4820, hdr_crc 6af9272a, подлинность да
  c v3: 160512 байт, image_check 0, версия 3, body_crc d7fa2e93, hdr_crc b0c2ec00, подлинность да
  c v2forged: 6656 байт, image_check 0, версия 2, body_crc 29ef4820, hdr_crc 52c6f27d, подлинность НЕТ

Те же CRC Python напечатал до запуска модели — числа совпали.


198щ.2 Решение при старте и переход

Проверка образа идёт от дешёвого к дорогому, как в 48.4: магия, размер заголовка, его CRC, длина, адрес сборки, указатель стека и точка входа — и только потом CRC-32 всего тела. Буферный образ проверяется той же функцией: он собран под рабочий слот, а не под буфер, и адрес входа должен смотреть в рабочий слот.

int image_check(uint32_t slot, img_hdr_t *out)
{
    const uint8_t *p = flash_ptr(slot);
    img_hdr_t h;
    uint32_t sp, pc;

    h.magic = rd32(p + 0);
    h.hdr_size = rd32(p + 4);
    h.body_size = rd32(p + 8);
    h.body_crc = rd32(p + 12);
    h.version = rd32(p + 16);
    h.load_addr = rd32(p + 20);
    for (int i = 0; i < 32; i++)
        h.tag[i] = p[24 + i];
    h.hdr_crc = rd32(p + 56);

    if (h.magic != IMG_MAGIC)
        return IMG_ENOMAGIC;
    if (h.hdr_size != HDR_SIZE)
        return IMG_EHDR;
    if (crc32(p, offsetof(img_hdr_t, hdr_crc)) != h.hdr_crc)
        return IMG_EHDRCRC;
    if (h.body_size < 8u || (h.body_size & 3u) != 0u || h.body_size > SLOT_SIZE - HDR_SIZE)
        return IMG_ESIZE;
    if (h.load_addr != APP_ENTRY_BASE)       /* собран под рабочий слот, а не под буфер */
        return IMG_EADDR;
    sp = rd32(p + HDR_SIZE);
    pc = rd32(p + HDR_SIZE + 4u);
    if (sp <= RAM_START || sp > RAM_END || (sp & 3u) != 0u)
        return IMG_EVEC;
    if ((pc & 1u) == 0u || pc < APP_ENTRY_BASE || pc >= APP_ENTRY_BASE + h.body_size)
        return IMG_EVEC;
    if (crc32(p + HDR_SIZE, h.body_size) != h.body_crc)
        return IMG_EBODYCRC;
    if (out)
        *out = h;
    return IMG_OK;
}

CRC-32 считается по полубайтам: таблица в 16 слов занимает 64 байта вместо 1 КБ у побайтной. Результат тот же, что у zlib.crc32:

uint32_t crc32_update(uint32_t crc, const uint8_t *p, uint32_t n)
{
    crc = ~crc;
    while (n--) {
        crc ^= *p++;
        crc = (crc >> 4) ^ crc_nib[crc & 15u];
        crc = (crc >> 4) ^ crc_nib[crc & 15u];
    }
    return ~crc;
}

Журнал. Всё, что загрузчик должен помнить между стартами, — одна запись: состояние, сколько секторов уже скопировано, длина и CRC образа в буфере, старшая установленная версия. Записи только дописываются; каждая закрыта своим CRC-32. Оборвалась запись на середине — CRC не сойдётся, и чтение её пропустит, взяв предыдущую. Заполнилась копия — стирается другая, и запись идёт туда; номер seq показывает, какая новее.

/* Самая новая целая запись из двух копий; ни одной — «покой», версия 0. */
void jrn_open(jrn_rec_t *cur)
{
    static const uint32_t base[2] = { JRN_A_ADDR, JRN_B_ADDR };
    const jrn_rec_t *best = 0;
    j_last = 0u;
    for (int s = 0; s < 2; s++) {
        for (uint32_t a = base[s]; a < base[s] + JRN_SECTOR_SIZE; a += sizeof(jrn_rec_t)) {
            const jrn_rec_t *r = (const jrn_rec_t *)flash_ptr(a);
            if (rec_ok(r) && (best == 0 || r->seq > best->seq)) {
                best = r;
                j_last = a;
                j_active = base[s];
            }
        }
    }
    if (best) {
        *cur = *best;
        j_seq = best->seq;
    } else {
        cur->magic = JRN_MAGIC;
        cur->seq = 0u;
        cur->state = ST_IDLE;
        cur->arg = cur->max_version = cur->img_size = cur->body_crc = 0u;
        j_active = JRN_A_ADDR;
        j_seq = 0u;
    }
}
/* Дописать запись. Оборванная на середине запись не сходится по CRC и просто пропускается. */
int jrn_append(jrn_rec_t *r)
{
    uint32_t a = free_slot(j_last ? j_last + sizeof(jrn_rec_t) : j_active, j_active + JRN_SECTOR_SIZE);
    if (a == 0u) {                       /* копия заполнена: стираем другую и переходим туда */
        uint32_t other = (j_active == JRN_A_ADDR) ? JRN_B_ADDR : JRN_A_ADDR;
        if (flash_erase_sector(other == JRN_A_ADDR ? JRN_A_SECTOR : JRN_B_SECTOR) != 0)
            return -1;
        j_active = other;
        a = other;
    }
    r->magic = JRN_MAGIC;
    r->seq = j_seq + 1u;
    r->crc = crc32(r, 28u);
    const uint32_t *w = (const uint32_t *)r;
    for (int i = 0; i < 8; i++)
        if (flash_program_word(a + 4u * (uint32_t)i, w[i]) != 0)
            return -1;
    j_seq = r->seq;
    j_last = a;
    return 0;
}

Копирование. Каждый сектор рабочего слота — стереть, записать словами из буфера (запись сверяется в flash_program_word), отметить в журнале COPY k. Пропало питание — при следующем старте копирование продолжится с первого неотмеченного сектора: буфер цел, а сектор, попавший под обрыв, стирается заново.

/* Каждый сектор: стереть, записать словами, сверить (в flash_program_word), отметить в журнале.
 * После обрыва копирование продолжается с первого неотмеченного сектора: буфер цел. */
static int copy_buffer(jrn_rec_t *j)
{
    uint32_t nsec = (j->img_size + SLOT_SECTOR_SIZE - 1u) / SLOT_SECTOR_SIZE;
    uint32_t k = (j->state == ST_COPY) ? j->arg : 0u;

    for (; k < nsec; k++) {
        uint32_t off = k * SLOT_SECTOR_SIZE;
        uint32_t end = off + SLOT_SECTOR_SIZE;
        if (end > j->img_size)
            end = j->img_size;
        if (flash_erase_sector(APP_FIRST_SECTOR + k) != 0)
            return -1;
        for (; off < end; off += 4u)
            if (flash_program_word(APP_ADDR + off, flash_read32(BUF_ADDR + off)) != 0)
                return -1;
        j->state = ST_COPY;
        j->arg = k + 1u;
        if (jrn_append(j) != 0)
            return -1;
    }
    return 0;
}
int boot_decide(void)
{
    jrn_rec_t j;
    img_hdr_t h;
    int e;

    jrn_open(&j);
    if (j.state == ST_READY || j.state == ST_COPY) {
        if (image_check(BUF_ADDR, &h) != IMG_OK || h.body_crc != j.body_crc) {
            bl_log("buffer damaged, update dropped", j.state);
            j.state = ST_IDLE;
            j.arg = 0u;
            (void)jrn_append(&j);
        } else if (copy_buffer(&j) != 0) {
            bl_log("copy failed, sectors done", j.arg);
        } else if (image_check(APP_ADDR, &h) == IMG_OK && h.body_crc == j.body_crc) {
            j.state = ST_IDLE;
            j.arg = 0u;
            j.max_version = h.version;
            (void)jrn_append(&j);
            bl_log("update installed, version", h.version);
        } else {
            bl_log("copy verify failed", 0u);
        }
    }
    if (upd_request_take()) {
        bl_log("update requested by app", 0u);
        return BOOT_STAY;
    }
    e = image_check(APP_ADDR, &h);
    if (e != IMG_OK) {
        bl_log("no valid app, error", (uint32_t)e);
        return BOOT_STAY;
    }
    bl_log("app ok, version", h.version);
    return BOOT_RUN;
}

Порядок в boot_decide такой: сначала незаконченное обновление (оно важнее всего), потом запрос от приложения, потом проверка рабочего слота. Нет годного приложения — загрузчик остаётся и ждёт образ по UART: прибор не бывает «пустым», загрузчик живёт всегда.

Переход. Загрузчик работает от HSI 16 МГц и такт не трогает — приложению нечего возвращать. Включённое им — порты, два UART, блок CRC, SysTick — сбрасывается через RCC_xxxRSTR и возвращается к значениям после сброса (RCC_AHB1ENR = 0x00100000 по RM0090); потом VTOR, барьеры, стек и прыжок.

/* Вернуть всё, что включил загрузчик, переставить таблицу векторов, стек — и прыжок. */
static void jump_to_app(void)
{
    uint32_t sp = REG(APP_ENTRY_BASE);
    uint32_t pc = REG(APP_ENTRY_BASE + 4u);

    while (!(USART1_SR & (1u << 6))) {   /* TC: журнал допечатан */
    }
    __asm volatile("cpsid i" ::: "memory");
    SYST_CSR = 0u;
    SYST_RVR = 0u;
    SYST_CVR = 0u;
    for (uint32_t i = 0; i < 8u; i++) {
        NVIC_ICER(i) = 0xFFFFFFFFu;
        NVIC_ICPR(i) = 0xFFFFFFFFu;
    }
    RCC_AHB1RSTR = RCC_AHB1_GPIOA | RCC_AHB1_GPIOB | RCC_AHB1_CRC;
    RCC_AHB1RSTR = 0u;
    RCC_APB1RSTR = RCC_APB1_USART3;
    RCC_APB1RSTR = 0u;
    RCC_APB2RSTR = RCC_APB2_USART1;
    RCC_APB2RSTR = 0u;
    RCC_AHB1ENR = RCC_AHB1ENR_RESET;
    RCC_APB1ENR = 0u;
    RCC_APB2ENR = 0u;
    SCB_VTOR = APP_ENTRY_BASE;
    __asm volatile("dsb\n\tisb" ::: "memory");
    __asm volatile("msr msp, %0\n\tcpsie i\n\tbx %1" : : "r"(sp), "r"(pc) : "memory");
    for (;;) {
    }
}

Главный цикл: старт, решение, окно ожидания, приём. Окно — 300 мс после решения «запускать»: если в нём от хоста пришёл байт начала кадра, загрузчик остаётся. Это заменяет стирание приложения из HardFault: прибор с ошибкой в новой прошивке, которая падает сразу при старте, всё равно можно перешить.

int main(void)
{
    proto_t p;
    uint32_t wait = BOOT_WAIT_MS, quiet = 0u;
    uint8_t b;
    int stay;

    uart_init();
    bl_log("start, loader version", BOOT_VERSION);
    proto_init(&p);
    stay = boot_decide();
    while (stay == BOOT_RUN && wait) {   /* окно: SOF от хоста — остаёмся в загрузчике */
        if (link_byte(&b) && b == PROTO_SOF) {
            (void)proto_feed(&p, b);
            stay = BOOT_STAY;
            bl_log("host is here, staying", 0u);
        }
        if (SYST_CSR & (1u << 16))
            wait--;
    }
    if (stay == BOOT_RUN)
        jump_to_app();
    for (;;) {
        if (link_byte(&b)) {
            quiet = 0u;
            if (proto_feed(&p, b) == PROTO_RESET) {
                bl_log("image accepted, reset", 0u);
                system_reset();
            }
        } else if ((SYST_CSR & (1u << 16)) && ++quiet == IDLE_MS) {
            proto_idle(&p);
        }
    }
}

В QEMU. Образ флеш-памяти на 1 МБ собирает mkimage.py: загрузчик (сборка с -DQEMU_NO_UID, потому что области номера кристалла у netduinoplus2 в QEMU нет — чтение даёт ошибку шины), приложение v1 с заголовком, журнал. USART1 — журнал загрузчика, USART3 — линия к модулю:

== q1. Приложение v1 цело
boot: start, loader version 00010000
boot: app ok, version 00000001
app: running, header version 0x00000001
app: VTOR 0x08020200

Загрузчик нашёл приложение по заголовку, проверил CRC-32 и перешёл; приложение видит VTOR = 0x08020200. Один испорченный бит тела — ошибка 7 (IMG_EBODYCRC), загрузчик остаётся и отвечает на запрос QUERY; в ответе версия загрузчика 0x00010000 и версия приложения 0:

== q2. Один бит тела испорчен, хост спрашивает QUERY
boot: start, loader version 00010000
boot: no valid app, error 00000007
USART3 ответ:  a5 82 07 00 14 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 b6 bc

Окно ожидания тоже работает: поток кадров QUERY при целом приложении останавливает запуск, и в ответе уже версия 1:

== q5. Хост в окне ожидания: QUERY (первый ответ)
boot: start, loader version 00010000
boot: app ok, version 00000001
boot: host is here, staying 00000000
USART3 ответ:  a5 82 07 00 14 00 00 00 01 00 00 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00 10 f8

А вот запись флеш-памяти QEMU не делает: регистры FLASH у этой машины — заглушка (читаются нулями, запись пропадает), сама память только для чтения. Журнал с состоянием READY и образом v2 в буфере даёт честный отказ — стирание «прошло», но сверка увидела старые данные, — и прибор работает на прежней v1:

== q4. В журнале READY, в буфере v2
boot: start, loader version 00010000
boot: copy failed, sectors done 00000000
boot: app ok, version 00000001

Поэтому всё, что касается записи, — в модели (198щ.5).


198щ.3 Приём по UART: Bluetooth-модуль — это просто UART

Модуль в прозрачном режиме передаёт байты из радиоканала в UART и обратно; для загрузчика это USART3 на 115200. Поверх — свой протокол «послал — жди подтверждения»:

Поле Байт Что
SOF 1 0xA5 — начало кадра
тип 1 QUERY 0x01, START 0x02, DATA 0x03, END 0x04; ответы ACK 0x80, NAK 0x81, QUERY_R 0x82
номер 2 номер кадра DATA с нуля; в ответе — на какой кадр ответ
длина 2 0…256 байт данных
данные 0…256 START: длина образа; DATA: кусок образа, кратный 4
CRC-16 2 CCITT, начальное 0xFFFF, от типа до конца данных

Кадр START для образа v2 (6656 = 0x1A00 байт) — 12 байт:

  START: a5 02 00 00 04 00 00 1a 00 00 16 5c

Python собирает тот же кадр через binascii.crc_hqx — байты совпадают. Хост шлёт кадр и ждёт ответа. ACK с тем же номером — следующий кадр; NAK или тишина — повтор. Прибор пишет кадр DATA только если его номер — ожидаемый. Если номер на единицу меньше, значит, наш ACK потерялся, и кадр уже записан: прибор подтверждает его ещё раз, но не пишет. Пауза в линии 20 мс выбрасывает недособранный кадр — так приём восстанавливается после потерянного байта.

/* Байт из UART. Кадр: SOF, тип, номер (2), длина (2), данные, CRC-16 от типа до конца данных. */
int proto_feed(proto_t *p, uint8_t b)
{
    uint16_t len, crc;
    if (p->n == 0u && b != PROTO_SOF)
        return PROTO_NONE;               /* ждём начало кадра */
    p->buf[p->n++] = b;
    if (p->n < 6u)
        return PROTO_NONE;
    len = (uint16_t)(p->buf[4] | p->buf[5] << 8);
    if (len > PROTO_MAX) {               /* длина испорчена: это не кадр */
        p->n = 0u;
        return PROTO_NONE;
    }
    if (p->n < 8u + len)
        return PROTO_NONE;
    p->n = 0u;
    crc = (uint16_t)(p->buf[6 + len] | p->buf[7 + len] << 8);
    if (crc16(p->buf + 1, 5u + len) != crc) {
        nak((uint16_t)(p->buf[2] | p->buf[3] << 8), E_CRC);
        return PROTO_NONE;
    }
    return on_frame(p);
}
static void on_start(proto_t *p, uint16_t seq, const uint8_t *d, uint16_t len)
{
    jrn_rec_t j;
    uint32_t size = (len == 4u) ? rd32(d) : 0u;
    if (size <= HDR_SIZE || size > SLOT_SIZE || (size & 3u) != 0u) {
        nak(seq, E_LEN);
        return;
    }
    jrn_open(&j);
    j.state = ST_RX;                     /* буфер больше не годен — до проверки в END */
    j.arg = 0u;
    j.img_size = size;
    j.body_crc = 0u;
    p->img_size = 0u;
    if (jrn_append(&j) != 0) {
        nak(seq, E_FLASH);
        return;
    }
    for (uint32_t k = 0; k * SLOT_SECTOR_SIZE < size; k++)
        if (flash_erase_sector(BUF_FIRST_SECTOR + k) != 0) {
            nak(seq, E_FLASH);
            return;
        }
    p->img_size = size;
    p->written = 0u;
    p->expect = 0u;
    ack(seq);
}
static void on_data(proto_t *p, uint16_t seq, const uint8_t *d, uint16_t len)
{
    if (p->img_size == 0u) {
        nak(seq, E_STATE);
    } else if (p->expect > 0u && seq == p->expect - 1u) {
        ack(seq);                        /* повтор: наш ACK потерялся, второй раз не пишем */
    } else if (seq != p->expect) {
        nak(p->expect, E_SEQ);
    } else if (len == 0u || (len & 3u) != 0u || p->written + len > p->img_size) {
        nak(seq, E_LEN);
    } else {
        for (uint16_t i = 0; i < len; i += 4u)
            if (flash_program_word(BUF_ADDR + p->written + i, rd32(d + i)) != 0) {
                nak(seq, E_FLASH);
                return;
            }
        p->written += len;
        p->expect++;
        ack(seq);
    }
}
static int on_end(proto_t *p, uint16_t seq)
{
    jrn_rec_t j;
    img_hdr_t h;
    if (p->img_size == 0u || p->written != p->img_size) {
        nak(seq, E_LEN);
        return PROTO_NONE;
    }
    if (image_check(BUF_ADDR, &h) != IMG_OK || HDR_SIZE + h.body_size != p->img_size) {
        nak(seq, E_IMAGE);
        return PROTO_NONE;
    }
    if (image_auth(BUF_ADDR, &h) != IMG_OK) {
        nak(seq, E_AUTH);
        return PROTO_NONE;
    }
    if (h.version <= boot_floor_version()) {
        nak(seq, E_OLD);
        return PROTO_NONE;
    }
    jrn_open(&j);
    j.state = ST_READY;                  /* с этой записи копирование неизбежно */
    j.arg = 0u;
    j.img_size = p->img_size;
    j.body_crc = h.body_crc;
    if (jrn_append(&j) != 0) {
        nak(seq, E_FLASH);
        return PROTO_NONE;
    }
    ack(seq);
    p->img_size = 0u;
    return PROTO_RESET;
}

Главное здесь — куда идёт запись: только в буферный слот. Рабочий слот трогает загрузчик и только после записи READY в журнал, а READY ставится, когда образ целиком прошёл длину, CRC-32, HMAC и проверку версии. Оборвалась связь посреди приёма — прибор при следующем старте просто запускает прежнюю прошивку. Отказы в модели:

  чужой ключ: ответ -107 (NAK 7), после старта работает v1
  та же версия 1: ответ -108 (NAK 8), после старта работает v1
  бит тела испорчен: ответ -106 (NAK 6), после старта работает v1
  оборван: послано 4096 байт: ответ -104 (NAK 4), после старта работает v1

Коды: 4 — длина, 6 — образ, 7 — подлинность, 8 — версия не новее.

CRC-16 кадра — сама функция:

uint16_t crc16(const uint8_t *p, uint32_t n)
{
    uint16_t crc = 0xFFFFu;
    while (n--) {
        crc ^= (uint16_t)(*p++ << 8);
        for (int i = 0; i < 8; i++)
            crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u) : (uint16_t)(crc << 1);
    }
    return crc;
}

Почему не сумма. Модель берёт кадр 256 случайных байт и портит его четырьмя способами, по 20 000 испытаний на каждый (испытание, где порча ничего не меняет, пропускается). 16-битная аддитивная сумма байтов, как у прибора, против CRC-16:

  один бит: испытаний 20000, сумма не заметила 0, CRC-16 не заметила 0
  соседние байты поменялись местами: испытаний 19929, сумма не заметила 19929, CRC-16 не заметила 0
  +d в одном байте и -d в другом: испытаний 19257, сумма не заметила 19257, CRC-16 не заметила 0
  два соседних байта испорчены: испытаний 20000, сумма не заметила 54, CRC-16 не заметила 0

Сумма не зависит от порядка байт — перестановку она не видит ни разу. Python повторяет опыт тем же генератором и получает те же числа.

Канал с потерями. Двадцать обновлений v1 → v3 (160 512 байт, два сектора) по линии, где 0,05 % байт пропадает и 0,05 % искажается в обе стороны:

  обновлений v1 -> v3: 20, установлено целым 20, без подтверждения END 0
  кадров 16382, повторов 3799 (без ответа 2119, NAK по CRC 1680), байт к прибору 4313928, записей слов данных 1605120 = 20 x 2 x 40128

Все 20 образов установлены целыми. Записей данных ровно вдвое больше слов образа (буфер плюс рабочий слот) — значит, ни один повторный кадр не записан второй раз.


198щ.4 Как приложение просит перепрошивку; номер кристалла

Приложению нужно сказать загрузчику «останься после сброса». У F405 для этого четыре места:

Где Что нужно (RM0090) Переживает Цена
ячейка в конце ОЗУ общая область в обоих скриптах (48.6) программный сброс ничего; при снятии питания пропадает
RTC_BKPxR PWREN, бит DBP, выбор такта RTC и RTCEN (5.1.2) сброс и снятие питания при батарейке на VBAT 20 слов; сбрасываются по вмешательству (tamper)
резервное ОЗУ BKPSRAM PWREN, DBP, BKPSRAMEN (5.1.2) то же, если включён резервный стабилизатор 4 КБ
запись в журнале во флеш-памяти ничего нового всё цикл стирания; сектор 128 КБ — 1 с

Прибор ставит признак во флеш-память — в отдельный 128-килобайтный сектор, то есть стирает целый сектор на каждый запрос. Нам нужно пережить только программный сброс: обновление всегда начинается с работающего приложения. Поэтому — ячейка ОЗУ: два слова, магия и её дополнение, чтобы случайный мусор после включения питания не сошёл за запрос.

/* Признак от приложения: два слова в общей ячейке ОЗУ; прочитали — стёрли. */
int upd_request_take(void)
{
    volatile uint32_t *f = (volatile uint32_t *)UPD_FLAG_ADDR;
    int yes = (f[0] == UPD_FLAG_MAGIC && f[1] == ~UPD_FLAG_MAGIC);
    f[0] = 0u;
    f[1] = 0u;
    return yes;
}

В приложении:

#ifdef APP_ASK_UPDATE
    volatile uint32_t *f = (volatile uint32_t *)UPD_FLAG_ADDR;
    say("app: asking loader for update 0x", 0u);
    f[0] = UPD_FLAG_MAGIC;
    f[1] = ~UPD_FLAG_MAGIC;
    while (!(USART1_SR & (1u << 6))) {
    }
    SCB_AIRCR = 0x05FA0004u;
#endif

В QEMU приложение просит, сбрасывает прибор, загрузчик видит запрос и остаётся — ячейка пережила сброс:

== q3. Приложение просит обновление
boot: start, loader version 00010000
boot: app ok, version 00000001
app: running, header version 0x00000001
app: VTOR 0x08020200
app: asking loader for update 0x00000000
boot: start, loader version 00010000
boot: update requested by app 00000000

Что ОЗУ на кристалле переживает программный сброс, проверено только в QEMU. Запрос, потерянный при снятии питания, приложение просто повторит.

Номер кристалла. 96 бит с адреса 0x1FFF7A10 (RM0090, 39.1) удобно свернуть в одно слово блоком CRC: многочлен 0x4C11DB7, слово за 4 такта (RM0090, 4.2). Из этого слова получается имя модуля, у каждого прибора своё, и оно же уходит в ответ QUERY:

/* 96 бит номера кристалла сворачиваются аппаратным CRC в одно слово: из него имя модуля. */
uint32_t uid_word(void)
{
#ifdef QEMU_NO_UID
    return 0u;                           /* у netduinoplus2 в QEMU этой области нет */
#else
    RCC_AHB1ENR |= RCC_AHB1_CRC;
    CRC_CR = 1u;                         /* RESET: начальное значение 0xFFFFFFFF */
    CRC_DR = REG(UID_ADDR);
    CRC_DR = REG(UID_ADDR + 4u);
    CRC_DR = REG(UID_ADDR + 8u);
    return CRC_DR;
#endif
}

Блок CRC считает не так, как zlib: старший бит первым, без отражения и без конечного «исключающего или». Модель на ПК повторяет его побитно; для примера номера 00340047 3235510b 31383830 слово — ca979f91, и Python получает его же как CRC-32/MPEG-2 по байтам каждого слова, старшим вперёд. В QEMU блока CRC нет — это место проверено только моделью.


198щ.5 Обрыв питания: модель флеш-памяти и счёт точек

Модель (flash_model.c) — 12 секторов F405 с их размерами. Стирание ставит 0xFF, запись слова только сбрасывает биты — поднять 0 в 1 без стирания нельзя (RM0090, 3.6.4). Про сброс посреди операции RM0090 (3.6) говорит одно: содержимое не гарантировано. Модель делает так: на выбранной операции питание пропадает; при стирании часть бит сектора уже стала единицами, при записи слова — часть бит уже нулями. Потом «включение» — тот же boot_decide, и проверка: что запустилось? Старый образ байт в байт, новый байт в байт, ничего или «что-то»?

Настоящий драйвер — регистры по RM0090: ключи, PSIZE = x32, SER и номер сектора, ожидание BSY, сверка:

int flash_erase_sector(uint32_t sector)
{
    uint32_t a, end = sector_base(sector) + sector_size(sector);
    int e;
    flash_unlock();
    (void)flash_wait();
    FLASH_CR = FCR_PSIZE_X32 | FCR_SER | (sector << FCR_SNB_POS);
    FLASH_CR |= FCR_STRT;
    e = flash_wait();                    /* 16 КБ — до 0,5 с, 128 КБ — до 2 с (DS8626) */
    FLASH_CR = FCR_LOCK;
    if (e)
        return -1;
    for (a = sector_base(sector); a < end; a += 4u)
        if (REG(a) != 0xFFFFFFFFu)
            return -1;
    return 0;
}
int flash_program_word(uint32_t addr, uint32_t value)
{
    int e;
    flash_unlock();
    (void)flash_wait();
    FLASH_CR = FCR_PSIZE_X32 | FCR_PG;
    REG(addr) = value;                   /* одно слово за одну запись, а не четыре байта */
    e = flash_wait();
    FLASH_CR = FCR_LOCK;
    return (e || REG(addr) != value) ? -1 : 0;
}

Обновление v1 → v2 (6656 байт) от просьбы приложения до запуска новой версии — 3362 операции стирания и записи. Питание пропадает на каждой:

v1 -> v2 (6656 байт): операций без обрыва 3362 (приём 1681, копирование 1681), стираний 2, записей слов 3360, из них в журнал 32
  обрыв при приёме: точек 1681 — старая целая 1681, новая целая 0, без прошивки 0, НЕДОПИСАННАЯ 0
  обрыв при копировании: точек 1681 — старая целая 0, новая целая 1681, без прошивки 0, НЕДОПИСАННАЯ 0
  второй обрыв во время восстановления (первая, средняя, последняя операция): точек 5043 — старая целая 0, новая целая 5043, без прошивки 0, НЕДОПИСАННАЯ 0

Граница проходит ровно по записи READY: до неё прибор всегда запускает старую целую прошивку, после — новую целую. Второй обрыв, уже во время продолжения копирования, ничего не меняет. Журнал почти полон (510 записей из 512) — переход на другую копию происходит посреди обновления, добавляется стирание сектора журнала:

v1 -> v2, журнал почти полон (510 записей): операций без обрыва 3363 (приём 1681, копирование 1682), стираний 3, записей слов 3360, из них в журнал 32
  обрыв при приёме: точек 1681 — старая целая 1681, новая целая 0, без прошивки 0, НЕДОПИСАННАЯ 0
  обрыв при копировании: точек 1682 — старая целая 0, новая целая 1682, без прошивки 0, НЕДОПИСАННАЯ 0

Образ v3 на два сектора — 80 300 операций; здесь обрыв пробуется на каждом стирании, каждой записи журнала и каждой 97-й записи данных:

v1 -> v3 (160512 байт), запись данных — каждая 97-я: операций без обрыва 80300 (приём 40146, копирование 40154), стираний 4, записей слов 80296, из них в журнал 40
  обрыв при приёме: точек 431 — старая целая 431, новая целая 0, без прошивки 0, НЕДОПИСАННАЯ 0
  обрыв при копировании: точек 440 — старая целая 0, новая целая 440, без прошивки 0, НЕДОПИСАННАЯ 0

Исходная схема на той же модели (naive.c, свои константы: метка присутствия в начале образа, запись по байту, буфер копируется целиком). Тот же образ v2:

исходная схема, приём загрузчиком в рабочие секторы: операций 6660 (приём 6660, копирование 0)
  обрыв при приёме: точек 6660 — старая целая 0, новая целая 0, без прошивки 504, НЕДОПИСАННАЯ 6156
исходная схема, приём в буфер приложением: операций 268806 (приём 6658, копирование 262148)
  обрыв при приёме: точек 6658 — старая целая 502, новая целая 0, без прошивки 0, НЕДОПИСАННАЯ 6156
  обрыв при копировании: точек 261 — старая целая 0, новая целая 261, без прошивки 0, НЕДОПИСАННАЯ 0

Приём прямо в рабочие секторы: в 504 точках из 6660 прибор остаётся без прошивки (метка ещё не записана), в 6156 — запускает недописанную: метка в начале образа уже на месте. Приём в буфер через приложение не лучше — 6156 недописанных: метка в буфере есть, и загрузчик копирует обрывок поверх рабочей прошивки. Зато копирование из буфера у исходной схемы обрыв переживает (здесь пробовалась каждая 1024-я из 262 148 операций): буфер стирается последним, и копия начинается заново. Чинить, значит, надо не копирование, а признак готовности и место приёма.


198щ.6 Цена: размер, время, ресурс

Размер. gcc 12.2 с -Os, без библиотек (arm_host.txt):

   text    data     bss     dec     hex filename
   4524       4       8    4536    11b8 boot.elf
    368       0       0     368     170 app1.elf

Загрузчик — 4528 байт флеш-памяти (код и начальные данные), 28 % сектора 0. Самая крупная часть — SHA-256 и HMAC: 1044 байта (sha256.o), из них 256 байт — таблица констант. Разбор кадров — 714 байт, проверка образа — 428, журнал — 400, решение при старте — 510. Сборка с -Wall -Wextra прошла без предупреждений; все адреса и биты f4regs.h сверены с stm32f405xx.h и core_cm4.h — 58 проверок _Static_assert при компиляции (selfcheck.c).

Время — только оценка по DS8626 (таблица 40), на плате не измерялось. Запись слова — 16 мкс типично и 100 мкс наибольшее при любой разрядности; стирание сектора 16 КБ при x32 — 250 и 500 мс, 128 КБ — 1 и 2 с; при x8 сектор 128 КБ стирается 2 и 4 с.

Что Типично Наибольшее
запись 256 КБ словами: 65 536 операций 1,05 с 6,6 с
запись 256 КБ по байту: 262 144 операции 4,2 с 26,2 с
копирование v3 (160 512 байт): 2 стирания + 40 128 слов 2,64 с 8,01 с
передача v3 по UART 115200: 165 548 байт к прибору, 5032 обратно 14,8 с —

Передача — без задержек радиоканала и без времени стирания буфера перед приёмом; кадр добавляет к 256 байтам данных 8 байт и ещё 8 байт ответа. Копирование по байту, как у прибора, вчетверо дольше записи словами — время записи на операцию у F4 одинаковое при любой разрядности.

Ресурс. Каждое обновление стирает по одному разу задействованные секторы буфера и рабочего слота. Модель прогнала 300 обновлений подряд:

  установлено 300 из 300, запись журнала №1200, старшая версия 301, всего стираний 602

Четыре записи журнала на обновление (RX, READY, COPY 1, IDLE) — сектор журнала на 512 записей стирается раз в 128 обновлений; 602 стирания = 2 на обновление плюс 2 перехода журнала. При ресурсе 10 тысяч циклов (DS8626, таблица 42) первыми износятся секторы слотов — через 10 000 обновлений.


198щ.7 Грабли

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

Приём прямо в рабочие секторы. Первое же стирание оставляет прибор без прошивки до конца приёма; обрыв связи на середине — недописанная прошивка. Принимать только в буфер, а копировать — после проверки.

Сумма вместо CRC. Аддитивная сумма не видит перестановку байт и компенсирующие ошибки: 19 929 и 19 257 пропусков из 19 929 и 19 257 в 198щ.3. CRC-16 на кадр и CRC-32 на образ — обе дешевле, чем кажется: 64 байта таблицы на CRC-32.

Запись по байту. Вчетверо больше операций и вчетверо дольше; к тому же разрядность записи обязана совпадать с PSIZE (RM0090, 3.6.4), иначе PGPERR. Писать словами при питании 2,7–3,6 В.

Стирание сектора останавливает ядро. Пока идёт стирание, чтение флеш-памяти — в том числе выборка команд — стоит (RM0090, 3.6), а сектор 128 КБ стирается до 2 с. Байты, пришедшие за это время в UART, пропадут. Поэтому прибор стирает буфер, получив START, и отвечает ACK только после стирания: хост ждёт и ничего не шлёт.

Сторожевой таймер. Те же 2 с на сектор: сторож с коротким интервалом перезапустит прибор посреди копирования. Журнал это переживёт, но копирование не закончится никогда. Интервал — больше наибольшего времени стирания.

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

Переход без возврата периферии. Загрузчик прибора при каждом старте настраивает UART с прерыванием и Bluetooth-модуль — ещё до того, как решил, что запускать, — и прыгает, не вернув их к сбросу. Приложение получает камень не в том состоянии, на которое рассчитана его инициализация (48.5), а каждый старт удлиняется обменом с модулем. Решать сначала, с модулем говорить — только если остаёмся, а перед прыжком сбрасывать всё, что включил загрузчик (198щ.2).

Номер кристалла в QEMU. У netduinoplus2 нет области 0x1FFF7A10 — чтение даёт ошибку шины. Сборка для QEMU подставляет ноль (QEMU_NO_UID).

«В QEMU всё записалось». Нет: флеш-память QEMU только для чтения, регистры FLASH — заглушка, BSY всегда ноль. Драйвер без сверки записанного рапортовал бы об успехе. Сверка каждого слова и стирания — обязательна и на кристалле.


198щ.8 Итоги

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

bl.h — раскладка, заголовок образа, журнал и протокол — один заголовок на всё.

/* bl.h — свой загрузчик для STM32F405/F407: раскладка, заголовок образа, журнал, протокол.
 * Один заголовок на всё: его включают и загрузчик, и модель на ПК. */
#ifndef BL_H
#define BL_H

#include <stddef.h>
#include <stdint.h>

/* ── Раскладка флеш-памяти F405 (RM0090, таблица 5) ───────────────────── */
#define BOOT_ADDR         0x08000000u   /* сектор 0, 16 КБ — загрузчик            */
#define JRN_A_ADDR        0x08004000u   /* сектор 1, 16 КБ — журнал, копия A      */
#define JRN_B_ADDR        0x08008000u   /* сектор 2, 16 КБ — журнал, копия B      */
#define JRN_A_SECTOR      1u
#define JRN_B_SECTOR      2u
#define JRN_SECTOR_SIZE   0x4000u
#define APP_ADDR          0x08020000u   /* секторы 5–6, 256 КБ — рабочий слот     */
#define APP_FIRST_SECTOR  5u
#define BUF_ADDR          0x08060000u   /* секторы 7–8, 256 КБ — буферный слот    */
#define BUF_FIRST_SECTOR  7u
#define SLOT_SECTORS      2u
#define SLOT_SECTOR_SIZE  0x20000u
#define SLOT_SIZE         (SLOT_SECTORS * SLOT_SECTOR_SIZE)
#define HDR_SIZE          512u
#define APP_ENTRY_BASE    (APP_ADDR + HDR_SIZE)  /* таблица векторов приложения */
#define RAM_START         0x20000000u
#define RAM_END           0x20020000u   /* SRAM1 + SRAM2 = 128 КБ                 */
#define UPD_FLAG_ADDR     0x2001FFF0u   /* общая ячейка запроса: 16 байт в конце ОЗУ */
#define UPD_FLAG_MAGIC    0x52515055u   /* 'U','P','Q','R'                        */

#define BOOT_VERSION      0x00010000u   /* 1.0.0 */

/* Секторы F405: 0–3 по 16 КБ, 4 — 64 КБ, 5–11 по 128 КБ. */
static inline uint32_t sector_size(uint32_t s)
{
    return s < 4u ? 0x4000u : (s == 4u ? 0x10000u : 0x20000u);
}

static inline uint32_t sector_base(uint32_t s)
{
    return s < 4u ? 0x08000000u + s * 0x4000u : (s == 4u ? 0x08010000u : 0x08020000u + (s - 5u) * 0x20000u);
}

/* ── Заголовок образа: первые 512 байт слота ─────────────────────────── */
#define IMG_MAGIC         0x31474D49u   /* 'I','M','G','1' */
typedef struct {
    uint32_t magic;
    uint32_t hdr_size;      /* 512 */
    uint32_t body_size;     /* байт тела за заголовком, кратно 4 */
    uint32_t body_crc;      /* CRC-32 тела */
    uint32_t version;       /* растёт с каждым выпуском */
    uint32_t load_addr;     /* под какой адрес собрано тело: APP_ENTRY_BASE */
    uint8_t  tag[32];       /* HMAC-SHA-256 байт 0…23 заголовка и тела */
    uint32_t hdr_crc;       /* CRC-32 байт 0…55 */
} img_hdr_t;                /* 60 байт; до 512 — 0xFF */
_Static_assert(sizeof(img_hdr_t) == 60u, "img_hdr_t");
_Static_assert(offsetof(img_hdr_t, hdr_crc) == 56u, "hdr_crc");

enum { IMG_OK = 0, IMG_ENOMAGIC, IMG_EHDR, IMG_EHDRCRC, IMG_ESIZE, IMG_EADDR, IMG_EVEC, IMG_EBODYCRC, IMG_EAUTH };

/* ── Журнал обновления: записи по 32 байта в секторах 1 и 2 ─────────── */
#define JRN_MAGIC         0x314E524Au   /* 'J','R','N','1' */
enum { ST_IDLE = 0, ST_RX = 1, ST_READY = 2, ST_COPY = 3 };
typedef struct {
    uint32_t magic;
    uint32_t seq;           /* номер записи: больший — новее */
    uint32_t state;         /* ST_* */
    uint32_t arg;           /* ST_COPY: сколько секторов уже скопировано */
    uint32_t max_version;   /* старшая версия, когда-либо установленная */
    uint32_t img_size;      /* размер образа в буфере вместе с заголовком */
    uint32_t body_crc;      /* CRC-32 тела образа в буфере */
    uint32_t crc;           /* CRC-32 слов 0…6 */
} jrn_rec_t;
_Static_assert(sizeof(jrn_rec_t) == 32u, "jrn_rec_t");
_Static_assert(SLOT_SIZE % SLOT_SECTOR_SIZE == 0u, "slot");
_Static_assert(JRN_SECTOR_SIZE % sizeof(jrn_rec_t) == 0u, "jrn");

/* ── Кадры протокола обновления ──────────────────────────────────────── */
#define PROTO_SOF         0xA5u
#define PROTO_MAX         256u          /* полезная нагрузка кадра, байт */
enum { F_QUERY = 0x01, F_START = 0x02, F_DATA = 0x03, F_END = 0x04,
       F_ACK = 0x80, F_NAK = 0x81, F_QUERY_R = 0x82 };
enum { E_CRC = 1, E_STATE, E_SEQ, E_LEN, E_FLASH, E_IMAGE, E_AUTH, E_OLD, E_TYPE };
enum { PROTO_NONE = 0, PROTO_RESET = 1 };
typedef struct {
    uint8_t  buf[6u + PROTO_MAX + 2u];  /* SOF, тип, номер (2), длина (2), данные, CRC-16 */
    uint32_t n;             /* принято байт текущего кадра */
    uint32_t img_size;      /* объявлен в START; 0 — приёма нет */
    uint32_t written;       /* записано байт в буфер */
    uint16_t expect;        /* номер следующего кадра DATA */
} proto_t;

/* ── Модули ──────────────────────────────────────────────────────────── */
uint32_t crc32_update(uint32_t crc, const uint8_t *p, uint32_t n);
uint32_t crc32(const void *p, uint32_t n);
uint16_t crc16(const uint8_t *p, uint32_t n);
void     hmac_sha256(const uint8_t key[32], const uint8_t *a, uint32_t na,
                     const uint8_t *b, uint32_t nb, uint8_t out[32]);

const uint8_t *flash_ptr(uint32_t addr);         /* чтение флеш-памяти */
uint32_t flash_read32(uint32_t addr);
int      flash_erase_sector(uint32_t sector);     /* 0 — стёрт и проверен */
int      flash_program_word(uint32_t addr, uint32_t value); /* 0 — записано и сверено */

int      image_check(uint32_t slot, img_hdr_t *out);
int      image_auth(uint32_t slot, const img_hdr_t *h);
extern const uint8_t bl_key[32];

void     jrn_open(jrn_rec_t *cur);
int      jrn_append(jrn_rec_t *r);

enum { BOOT_RUN = 0, BOOT_STAY = 1 };
int      boot_decide(void);
uint32_t boot_floor_version(void);

void     proto_init(proto_t *p);
void     proto_idle(proto_t *p);
int      proto_feed(proto_t *p, uint8_t b);
uint32_t proto_frame(uint8_t *out, uint8_t type, uint16_t seq, const uint8_t *data, uint16_t len);

/* То, что у загрузчика и у модели на ПК своё. */
void     bl_log(const char *msg, uint32_t val);
void     link_put(const uint8_t *p, uint32_t n);
uint32_t uid_word(void);
int      upd_request_take(void);

#endif

boot_main.c — загрузчик на STM32F405: старт, UART, окно ожидания, приём, переход в приложение.

/* boot_main.c — загрузчик на STM32F405: старт, UART, окно ожидания, приём, переход в приложение.
 * Такт не меняется: загрузчик работает от HSI 16 МГц, и приложению нечего возвращать. */
#include "bl.h"
#include "f4regs.h"

#define BOOT_WAIT_MS  300u               /* окно, в которое хост может остановить запуск */
#define IDLE_MS       20u                /* пауза, после которой недособранный кадр выбрасывается */

extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
int main(void);

void Reset_Handler(void)
{
    uint32_t *s = &_sidata, *d = &_sdata;
    while (d < &_edata)
        *d++ = *s++;
    for (d = &_sbss; d < &_ebss;)
        *d++ = 0u;
    main();
    for (;;) {
    }
}

static void Fault_Handler(void)
{
    for (;;) {
    }
}

__attribute__((section(".isr_vector"), used))
static void (*const vectors[16])(void) = {
    (void (*)(void))&_estack, Reset_Handler, Fault_Handler, Fault_Handler,
    Fault_Handler, Fault_Handler, Fault_Handler, 0, 0, 0, 0, Fault_Handler, 0, 0, Fault_Handler, Fault_Handler,
};

/* gcc вправе вызвать их сам — для копирования структур. */
void *memcpy(void *d, const void *s, size_t n)
{
    uint8_t *a = d;
    const uint8_t *b = s;
    while (n--)
        *a++ = *b++;
    return d;
}

void *memset(void *d, int c, size_t n)
{
    uint8_t *a = d;
    while (n--)
        *a++ = (uint8_t)c;
    return d;
}

static void uart_init(void)
{
    RCC_AHB1ENR |= RCC_AHB1_GPIOA | RCC_AHB1_GPIOB;
    RCC_APB2ENR |= RCC_APB2_USART1;
    RCC_APB1ENR |= RCC_APB1_USART3;
    GPIOA_MODER = (GPIOA_MODER & ~(0xFu << 18)) | (0xAu << 18);   /* PA9, PA10 — AF */
    GPIOA_AFRH = (GPIOA_AFRH & ~(0xFFu << 4)) | (0x77u << 4);     /* AF7 = USART1 */
    GPIOB_MODER = (GPIOB_MODER & ~(0xFu << 20)) | (0xAu << 20);   /* PB10, PB11 — AF */
    GPIOB_AFRH = (GPIOB_AFRH & ~(0xFFu << 8)) | (0x77u << 8);     /* AF7 = USART3 */
    USART1_BRR = 0x8Bu;                  /* 115200 при 16 МГц */
    USART1_CR1 = U_CR1_UE | U_CR1_TE;
    USART3_BRR = 0x8Bu;
    USART3_CR1 = U_CR1_UE | U_CR1_TE | U_CR1_RE;
    SYST_RVR = 16000u - 1u;              /* тик 1 мс */
    SYST_CVR = 0u;
    SYST_CSR = 5u;                       /* такт ядра, счёт без прерывания */
}

static void put1(char c)
{
    while (!(USART1_SR & U_SR_TXE)) {
    }
    USART1_DR = (uint8_t)c;
}

void bl_log(const char *msg, uint32_t val)
{
    const char *p = "boot: ";
    while (*p)
        put1(*p++);
    while (*msg)
        put1(*msg++);
    put1(' ');
    for (int i = 28; i >= 0; i -= 4) {
        uint32_t d = (val >> i) & 15u;
        put1((char)(d < 10u ? '0' + d : 'a' + d - 10u));
    }
    put1('\n');
}

void link_put(const uint8_t *p, uint32_t n)
{
    while (n--) {
        while (!(USART3_SR & U_SR_TXE)) {
        }
        USART3_DR = *p++;
    }
}

/* 96 бит номера кристалла сворачиваются аппаратным CRC в одно слово: из него имя модуля. */
uint32_t uid_word(void)
{
#ifdef QEMU_NO_UID
    return 0u;                           /* у netduinoplus2 в QEMU этой области нет */
#else
    RCC_AHB1ENR |= RCC_AHB1_CRC;
    CRC_CR = 1u;                         /* RESET: начальное значение 0xFFFFFFFF */
    CRC_DR = REG(UID_ADDR);
    CRC_DR = REG(UID_ADDR + 4u);
    CRC_DR = REG(UID_ADDR + 8u);
    return CRC_DR;
#endif
}

/* Признак от приложения: два слова в общей ячейке ОЗУ; прочитали — стёрли. */
int upd_request_take(void)
{
    volatile uint32_t *f = (volatile uint32_t *)UPD_FLAG_ADDR;
    int yes = (f[0] == UPD_FLAG_MAGIC && f[1] == ~UPD_FLAG_MAGIC);
    f[0] = 0u;
    f[1] = 0u;
    return yes;
}

static int link_byte(uint8_t *b)
{
    uint32_t sr = USART3_SR;
    if (!(sr & (U_SR_RXNE | U_SR_ORE)))
        return 0;
    *b = (uint8_t)USART3_DR;             /* чтение DR снимает и RXNE, и ORE */
    return 1;
}

static void system_reset(void)
{
    while (!(USART3_SR & (1u << 6))) {   /* TC: ответ ушёл целиком */
    }
    SCB_AIRCR = 0x05FA0004u;             /* VECTKEY + SYSRESETREQ */
    for (;;) {
    }
}

/* Вернуть всё, что включил загрузчик, переставить таблицу векторов, стек — и прыжок. */
static void jump_to_app(void)
{
    uint32_t sp = REG(APP_ENTRY_BASE);
    uint32_t pc = REG(APP_ENTRY_BASE + 4u);

    while (!(USART1_SR & (1u << 6))) {   /* TC: журнал допечатан */
    }
    __asm volatile("cpsid i" ::: "memory");
    SYST_CSR = 0u;
    SYST_RVR = 0u;
    SYST_CVR = 0u;
    for (uint32_t i = 0; i < 8u; i++) {
        NVIC_ICER(i) = 0xFFFFFFFFu;
        NVIC_ICPR(i) = 0xFFFFFFFFu;
    }
    RCC_AHB1RSTR = RCC_AHB1_GPIOA | RCC_AHB1_GPIOB | RCC_AHB1_CRC;
    RCC_AHB1RSTR = 0u;
    RCC_APB1RSTR = RCC_APB1_USART3;
    RCC_APB1RSTR = 0u;
    RCC_APB2RSTR = RCC_APB2_USART1;
    RCC_APB2RSTR = 0u;
    RCC_AHB1ENR = RCC_AHB1ENR_RESET;
    RCC_APB1ENR = 0u;
    RCC_APB2ENR = 0u;
    SCB_VTOR = APP_ENTRY_BASE;
    __asm volatile("dsb\n\tisb" ::: "memory");
    __asm volatile("msr msp, %0\n\tcpsie i\n\tbx %1" : : "r"(sp), "r"(pc) : "memory");
    for (;;) {
    }
}

int main(void)
{
    proto_t p;
    uint32_t wait = BOOT_WAIT_MS, quiet = 0u;
    uint8_t b;
    int stay;

    uart_init();
    bl_log("start, loader version", BOOT_VERSION);
    proto_init(&p);
    stay = boot_decide();
    while (stay == BOOT_RUN && wait) {   /* окно: SOF от хоста — остаёмся в загрузчике */
        if (link_byte(&b) && b == PROTO_SOF) {
            (void)proto_feed(&p, b);
            stay = BOOT_STAY;
            bl_log("host is here, staying", 0u);
        }
        if (SYST_CSR & (1u << 16))
            wait--;
    }
    if (stay == BOOT_RUN)
        jump_to_app();
    for (;;) {
        if (link_byte(&b)) {
            quiet = 0u;
            if (proto_feed(&p, b) == PROTO_RESET) {
                bl_log("image accepted, reset", 0u);
                system_reset();
            }
        } else if ((SYST_CSR & (1u << 16)) && ++quiet == IDLE_MS) {
            proto_idle(&p);
        }
    }
}

boot.c — решение при старте и копирование буфера в рабочий слот по журналу.

/* boot.c — решение при старте и копирование буфера в рабочий слот по журналу. */
#include "bl.h"

/* Каждый сектор: стереть, записать словами, сверить (в flash_program_word), отметить в журнале.
 * После обрыва копирование продолжается с первого неотмеченного сектора: буфер цел. */
static int copy_buffer(jrn_rec_t *j)
{
    uint32_t nsec = (j->img_size + SLOT_SECTOR_SIZE - 1u) / SLOT_SECTOR_SIZE;
    uint32_t k = (j->state == ST_COPY) ? j->arg : 0u;

    for (; k < nsec; k++) {
        uint32_t off = k * SLOT_SECTOR_SIZE;
        uint32_t end = off + SLOT_SECTOR_SIZE;
        if (end > j->img_size)
            end = j->img_size;
        if (flash_erase_sector(APP_FIRST_SECTOR + k) != 0)
            return -1;
        for (; off < end; off += 4u)
            if (flash_program_word(APP_ADDR + off, flash_read32(BUF_ADDR + off)) != 0)
                return -1;
        j->state = ST_COPY;
        j->arg = k + 1u;
        if (jrn_append(j) != 0)
            return -1;
    }
    return 0;
}

/* Нижняя граница версии: старше и записанной в журнале, и той, что сейчас в рабочем слоте. */
uint32_t boot_floor_version(void)
{
    jrn_rec_t j;
    img_hdr_t h;
    jrn_open(&j);
    if (image_check(APP_ADDR, &h) == IMG_OK && h.version > j.max_version)
        return h.version;
    return j.max_version;
}

int boot_decide(void)
{
    jrn_rec_t j;
    img_hdr_t h;
    int e;

    jrn_open(&j);
    if (j.state == ST_READY || j.state == ST_COPY) {
        if (image_check(BUF_ADDR, &h) != IMG_OK || h.body_crc != j.body_crc) {
            bl_log("buffer damaged, update dropped", j.state);
            j.state = ST_IDLE;
            j.arg = 0u;
            (void)jrn_append(&j);
        } else if (copy_buffer(&j) != 0) {
            bl_log("copy failed, sectors done", j.arg);
        } else if (image_check(APP_ADDR, &h) == IMG_OK && h.body_crc == j.body_crc) {
            j.state = ST_IDLE;
            j.arg = 0u;
            j.max_version = h.version;
            (void)jrn_append(&j);
            bl_log("update installed, version", h.version);
        } else {
            bl_log("copy verify failed", 0u);
        }
    }
    if (upd_request_take()) {
        bl_log("update requested by app", 0u);
        return BOOT_STAY;
    }
    e = image_check(APP_ADDR, &h);
    if (e != IMG_OK) {
        bl_log("no valid app, error", (uint32_t)e);
        return BOOT_STAY;
    }
    bl_log("app ok, version", h.version);
    return BOOT_RUN;
}

proto.c — приём образа по UART кадрами с CRC-16 и ответом ACK или NAK.

/* proto.c — приём образа по UART кадрами: номер, длина, данные, CRC-16; ответ ACK или NAK.
 * Образ пишется в БУФЕРНЫЙ слот; рабочий слот трогает только загрузчик после проверки. */
#include "bl.h"

static uint32_t rd32(const uint8_t *p)
{
    return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
}

static void wr32(uint8_t *p, uint32_t v)
{
    p[0] = (uint8_t)v;
    p[1] = (uint8_t)(v >> 8);
    p[2] = (uint8_t)(v >> 16);
    p[3] = (uint8_t)(v >> 24);
}

uint32_t proto_frame(uint8_t *out, uint8_t type, uint16_t seq, const uint8_t *data, uint16_t len)
{
    out[0] = PROTO_SOF;
    out[1] = type;
    out[2] = (uint8_t)seq;
    out[3] = (uint8_t)(seq >> 8);
    out[4] = (uint8_t)len;
    out[5] = (uint8_t)(len >> 8);
    for (uint16_t i = 0; i < len; i++)
        out[6 + i] = data[i];
    uint16_t c = crc16(out + 1, 5u + len);
    out[6 + len] = (uint8_t)c;
    out[7 + len] = (uint8_t)(c >> 8);
    return 8u + len;
}

static void reply(uint8_t type, uint16_t seq, const uint8_t *d, uint16_t n)
{
    uint8_t f[8 + 20];
    link_put(f, proto_frame(f, type, seq, d, n));
}

static void ack(uint16_t seq)
{
    reply(F_ACK, seq, 0, 0u);
}

static void nak(uint16_t seq, uint8_t why)
{
    reply(F_NAK, seq, &why, 1u);
}

void proto_init(proto_t *p)
{
    p->n = 0u;
    p->img_size = 0u;
    p->written = 0u;
    p->expect = 0u;
}

/* Пауза в линии дольше межбайтового тайм-аута: недособранный кадр выбрасываем. */
void proto_idle(proto_t *p)
{
    p->n = 0u;
}

static void on_start(proto_t *p, uint16_t seq, const uint8_t *d, uint16_t len)
{
    jrn_rec_t j;
    uint32_t size = (len == 4u) ? rd32(d) : 0u;
    if (size <= HDR_SIZE || size > SLOT_SIZE || (size & 3u) != 0u) {
        nak(seq, E_LEN);
        return;
    }
    jrn_open(&j);
    j.state = ST_RX;                     /* буфер больше не годен — до проверки в END */
    j.arg = 0u;
    j.img_size = size;
    j.body_crc = 0u;
    p->img_size = 0u;
    if (jrn_append(&j) != 0) {
        nak(seq, E_FLASH);
        return;
    }
    for (uint32_t k = 0; k * SLOT_SECTOR_SIZE < size; k++)
        if (flash_erase_sector(BUF_FIRST_SECTOR + k) != 0) {
            nak(seq, E_FLASH);
            return;
        }
    p->img_size = size;
    p->written = 0u;
    p->expect = 0u;
    ack(seq);
}

static void on_data(proto_t *p, uint16_t seq, const uint8_t *d, uint16_t len)
{
    if (p->img_size == 0u) {
        nak(seq, E_STATE);
    } else if (p->expect > 0u && seq == p->expect - 1u) {
        ack(seq);                        /* повтор: наш ACK потерялся, второй раз не пишем */
    } else if (seq != p->expect) {
        nak(p->expect, E_SEQ);
    } else if (len == 0u || (len & 3u) != 0u || p->written + len > p->img_size) {
        nak(seq, E_LEN);
    } else {
        for (uint16_t i = 0; i < len; i += 4u)
            if (flash_program_word(BUF_ADDR + p->written + i, rd32(d + i)) != 0) {
                nak(seq, E_FLASH);
                return;
            }
        p->written += len;
        p->expect++;
        ack(seq);
    }
}

static int on_end(proto_t *p, uint16_t seq)
{
    jrn_rec_t j;
    img_hdr_t h;
    if (p->img_size == 0u || p->written != p->img_size) {
        nak(seq, E_LEN);
        return PROTO_NONE;
    }
    if (image_check(BUF_ADDR, &h) != IMG_OK || HDR_SIZE + h.body_size != p->img_size) {
        nak(seq, E_IMAGE);
        return PROTO_NONE;
    }
    if (image_auth(BUF_ADDR, &h) != IMG_OK) {
        nak(seq, E_AUTH);
        return PROTO_NONE;
    }
    if (h.version <= boot_floor_version()) {
        nak(seq, E_OLD);
        return PROTO_NONE;
    }
    jrn_open(&j);
    j.state = ST_READY;                  /* с этой записи копирование неизбежно */
    j.arg = 0u;
    j.img_size = p->img_size;
    j.body_crc = h.body_crc;
    if (jrn_append(&j) != 0) {
        nak(seq, E_FLASH);
        return PROTO_NONE;
    }
    ack(seq);
    p->img_size = 0u;
    return PROTO_RESET;
}

static int on_frame(proto_t *p)
{
    uint8_t type = p->buf[1];
    uint16_t seq = (uint16_t)(p->buf[2] | p->buf[3] << 8);
    uint16_t len = (uint16_t)(p->buf[4] | p->buf[5] << 8);
    const uint8_t *d = p->buf + 6;
    uint8_t ans[20];
    img_hdr_t h;
    jrn_rec_t j;

    switch (type) {
    case F_QUERY:
        jrn_open(&j);
        wr32(ans + 0, BOOT_VERSION);
        wr32(ans + 4, uid_word());
        wr32(ans + 8, image_check(APP_ADDR, &h) == IMG_OK ? h.version : 0u);
        wr32(ans + 12, boot_floor_version());
        wr32(ans + 16, j.state);
        reply(F_QUERY_R, seq, ans, 20u);
        return PROTO_NONE;
    case F_START:
        on_start(p, seq, d, len);
        return PROTO_NONE;
    case F_DATA:
        on_data(p, seq, d, len);
        return PROTO_NONE;
    case F_END:
        return on_end(p, seq);
    default:
        nak(seq, E_TYPE);
        return PROTO_NONE;
    }
}

/* Байт из UART. Кадр: SOF, тип, номер (2), длина (2), данные, CRC-16 от типа до конца данных. */
int proto_feed(proto_t *p, uint8_t b)
{
    uint16_t len, crc;
    if (p->n == 0u && b != PROTO_SOF)
        return PROTO_NONE;               /* ждём начало кадра */
    p->buf[p->n++] = b;
    if (p->n < 6u)
        return PROTO_NONE;
    len = (uint16_t)(p->buf[4] | p->buf[5] << 8);
    if (len > PROTO_MAX) {               /* длина испорчена: это не кадр */
        p->n = 0u;
        return PROTO_NONE;
    }
    if (p->n < 8u + len)
        return PROTO_NONE;
    p->n = 0u;
    crc = (uint16_t)(p->buf[6 + len] | p->buf[7 + len] << 8);
    if (crc16(p->buf + 1, 5u + len) != crc) {
        nak((uint16_t)(p->buf[2] | p->buf[3] << 8), E_CRC);
        return PROTO_NONE;
    }
    return on_frame(p);
}

journal.c — журнал обновления: записи только дописываются, две копии по очереди.

/* journal.c — журнал обновления: записи только дописываются, две копии по очереди. */
#include "bl.h"

static uint32_t j_active = JRN_A_ADDR;   /* сектор, куда пишем */
static uint32_t j_last;                  /* адрес последней целой записи; 0 — нет */
static uint32_t j_seq;                   /* её номер */

static int rec_ok(const jrn_rec_t *r)
{
    return r->magic == JRN_MAGIC && crc32(r, 28u) == r->crc;
}

/* Самая новая целая запись из двух копий; ни одной — «покой», версия 0. */
void jrn_open(jrn_rec_t *cur)
{
    static const uint32_t base[2] = { JRN_A_ADDR, JRN_B_ADDR };
    const jrn_rec_t *best = 0;
    j_last = 0u;
    for (int s = 0; s < 2; s++) {
        for (uint32_t a = base[s]; a < base[s] + JRN_SECTOR_SIZE; a += sizeof(jrn_rec_t)) {
            const jrn_rec_t *r = (const jrn_rec_t *)flash_ptr(a);
            if (rec_ok(r) && (best == 0 || r->seq > best->seq)) {
                best = r;
                j_last = a;
                j_active = base[s];
            }
        }
    }
    if (best) {
        *cur = *best;
        j_seq = best->seq;
    } else {
        cur->magic = JRN_MAGIC;
        cur->seq = 0u;
        cur->state = ST_IDLE;
        cur->arg = cur->max_version = cur->img_size = cur->body_crc = 0u;
        j_active = JRN_A_ADDR;
        j_seq = 0u;
    }
}

static uint32_t free_slot(uint32_t from, uint32_t end)
{
    for (uint32_t a = from; a < end; a += sizeof(jrn_rec_t)) {
        const uint32_t *w = (const uint32_t *)flash_ptr(a);
        int clean = 1;
        for (int i = 0; i < 8; i++)
            if (w[i] != 0xFFFFFFFFu)
                clean = 0;
        if (clean)
            return a;
    }
    return 0u;
}

/* Дописать запись. Оборванная на середине запись не сходится по CRC и просто пропускается. */
int jrn_append(jrn_rec_t *r)
{
    uint32_t a = free_slot(j_last ? j_last + sizeof(jrn_rec_t) : j_active, j_active + JRN_SECTOR_SIZE);
    if (a == 0u) {                       /* копия заполнена: стираем другую и переходим туда */
        uint32_t other = (j_active == JRN_A_ADDR) ? JRN_B_ADDR : JRN_A_ADDR;
        if (flash_erase_sector(other == JRN_A_ADDR ? JRN_A_SECTOR : JRN_B_SECTOR) != 0)
            return -1;
        j_active = other;
        a = other;
    }
    r->magic = JRN_MAGIC;
    r->seq = j_seq + 1u;
    r->crc = crc32(r, 28u);
    const uint32_t *w = (const uint32_t *)r;
    for (int i = 0; i < 8; i++)
        if (flash_program_word(a + 4u * (uint32_t)i, w[i]) != 0)
            return -1;
    j_seq = r->seq;
    j_last = a;
    return 0;
}

image.c — проверка образа в слоте — от дешёвых проверок к дорогим.

/* image.c — проверка образа в слоте: от дешёвых проверок к дорогим. */
#include "bl.h"

/* Учебный ключ HMAC. В изделии — свой, в секторе загрузчика под защитой от чтения (RDP). */
const uint8_t bl_key[32] = "198shch-demo-key-not-for-devices";

static uint32_t rd32(const uint8_t *p)
{
    return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
}

int image_check(uint32_t slot, img_hdr_t *out)
{
    const uint8_t *p = flash_ptr(slot);
    img_hdr_t h;
    uint32_t sp, pc;

    h.magic = rd32(p + 0);
    h.hdr_size = rd32(p + 4);
    h.body_size = rd32(p + 8);
    h.body_crc = rd32(p + 12);
    h.version = rd32(p + 16);
    h.load_addr = rd32(p + 20);
    for (int i = 0; i < 32; i++)
        h.tag[i] = p[24 + i];
    h.hdr_crc = rd32(p + 56);

    if (h.magic != IMG_MAGIC)
        return IMG_ENOMAGIC;
    if (h.hdr_size != HDR_SIZE)
        return IMG_EHDR;
    if (crc32(p, offsetof(img_hdr_t, hdr_crc)) != h.hdr_crc)
        return IMG_EHDRCRC;
    if (h.body_size < 8u || (h.body_size & 3u) != 0u || h.body_size > SLOT_SIZE - HDR_SIZE)
        return IMG_ESIZE;
    if (h.load_addr != APP_ENTRY_BASE)       /* собран под рабочий слот, а не под буфер */
        return IMG_EADDR;
    sp = rd32(p + HDR_SIZE);
    pc = rd32(p + HDR_SIZE + 4u);
    if (sp <= RAM_START || sp > RAM_END || (sp & 3u) != 0u)
        return IMG_EVEC;
    if ((pc & 1u) == 0u || pc < APP_ENTRY_BASE || pc >= APP_ENTRY_BASE + h.body_size)
        return IMG_EVEC;
    if (crc32(p + HDR_SIZE, h.body_size) != h.body_crc)
        return IMG_EBODYCRC;
    if (out)
        *out = h;
    return IMG_OK;
}

/* Происхождение: HMAC от первых 24 байт заголовка и тела; сравнение без раннего выхода. */
int image_auth(uint32_t slot, const img_hdr_t *h)
{
    const uint8_t *p = flash_ptr(slot);
    uint8_t mac[32], diff = 0u;
    hmac_sha256(bl_key, p, 24u, p + HDR_SIZE, h->body_size, mac);
    for (int i = 0; i < 32; i++)
        diff |= (uint8_t)(mac[i] ^ h->tag[i]);
    return diff == 0u ? IMG_OK : IMG_EAUTH;
}

crc.c — CRC-32 образа и CRC-16 кадра.

/* crc.c — CRC-32 образа (как zlib) и CRC-16 кадра (CCITT, начальное 0xFFFF). */
#include "bl.h"

/* CRC-32 по полубайтам: таблица 16 слов = 64 байта вместо 1 КБ. */
static const uint32_t crc_nib[16] = {
    0x00000000u, 0x1DB71064u, 0x3B6E20C8u, 0x26D930ACu, 0x76DC4190u, 0x6B6B51F4u, 0x4DB26158u, 0x5005713Cu,
    0xEDB88320u, 0xF00F9344u, 0xD6D6A3E8u, 0xCB61B38Cu, 0x9B64C2B0u, 0x86D3D2D4u, 0xA00AE278u, 0xBDBDF21Cu,
};

uint32_t crc32_update(uint32_t crc, const uint8_t *p, uint32_t n)
{
    crc = ~crc;
    while (n--) {
        crc ^= *p++;
        crc = (crc >> 4) ^ crc_nib[crc & 15u];
        crc = (crc >> 4) ^ crc_nib[crc & 15u];
    }
    return ~crc;
}

uint32_t crc32(const void *p, uint32_t n)
{
    return crc32_update(0u, (const uint8_t *)p, n);
}

uint16_t crc16(const uint8_t *p, uint32_t n)
{
    uint16_t crc = 0xFFFFu;
    while (n--) {
        crc ^= (uint16_t)(*p++ << 8);
        for (int i = 0; i < 8; i++)
            crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u) : (uint16_t)(crc << 1);
    }
    return crc;
}

sha256.c — SHA-256 и HMAC-SHA-256.

/* sha256.c — SHA-256 и HMAC-SHA-256 для проверки происхождения образа. */
#include "bl.h"

typedef struct {
    uint32_t h[8];
    uint8_t  blk[64];
    uint32_t nblk;          /* байт в blk */
    uint32_t total;         /* всего байт */
} sha_t;

static const uint32_t K[64] = {
    0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u,
    0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u,
    0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau,
    0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u,
    0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u,
    0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u,
    0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u,
    0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u,
};

#define ROR(x, n) (((x) >> (n)) | ((x) << (32 - (n))))

static void sha_block(sha_t *s, const uint8_t *p)
{
    uint32_t w[64], a, b, c, d, e, f, g, h, t1, t2;
    for (int i = 0; i < 16; i++)
        w[i] = (uint32_t)p[4 * i] << 24 | (uint32_t)p[4 * i + 1] << 16 | (uint32_t)p[4 * i + 2] << 8 | p[4 * i + 3];
    for (int i = 16; i < 64; i++)
        w[i] = (ROR(w[i - 2], 17) ^ ROR(w[i - 2], 19) ^ (w[i - 2] >> 10)) + w[i - 7]
             + (ROR(w[i - 15], 7) ^ ROR(w[i - 15], 18) ^ (w[i - 15] >> 3)) + w[i - 16];
    a = s->h[0]; b = s->h[1]; c = s->h[2]; d = s->h[3];
    e = s->h[4]; f = s->h[5]; g = s->h[6]; h = s->h[7];
    for (int i = 0; i < 64; i++) {
        t1 = h + (ROR(e, 6) ^ ROR(e, 11) ^ ROR(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i];
        t2 = (ROR(a, 2) ^ ROR(a, 13) ^ ROR(a, 22)) + ((a & b) ^ (a & c) ^ (b & c));
        h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2;
    }
    s->h[0] += a; s->h[1] += b; s->h[2] += c; s->h[3] += d;
    s->h[4] += e; s->h[5] += f; s->h[6] += g; s->h[7] += h;
}

static void sha_init(sha_t *s)
{
    static const uint32_t h0[8] = { 0x6a09e667u, 0xbb67ae85u, 0x3c6ef372u, 0xa54ff53au,
                                    0x510e527fu, 0x9b05688cu, 0x1f83d9abu, 0x5be0cd19u };
    for (int i = 0; i < 8; i++)
        s->h[i] = h0[i];
    s->nblk = 0u;
    s->total = 0u;
}

static void sha_update(sha_t *s, const uint8_t *p, uint32_t n)
{
    s->total += n;
    while (n--) {
        s->blk[s->nblk++] = *p++;
        if (s->nblk == 64u) {
            sha_block(s, s->blk);
            s->nblk = 0u;
        }
    }
}

static void sha_final(sha_t *s, uint8_t out[32])
{
    uint64_t bits = (uint64_t)s->total * 8u;
    uint8_t pad = 0x80u;
    sha_update(s, &pad, 1u);
    pad = 0u;
    while (s->nblk != 56u)
        sha_update(s, &pad, 1u);
    for (int i = 7; i >= 0; i--) {
        uint8_t b = (uint8_t)(bits >> (8 * i));
        sha_update(s, &b, 1u);
    }
    for (int i = 0; i < 32; i++)
        out[i] = (uint8_t)(s->h[i / 4] >> (24 - 8 * (i % 4)));
}

/* HMAC-SHA-256 от склейки двух кусков a и b — заголовок и тело читаются прямо из флеш-памяти. */
void hmac_sha256(const uint8_t key[32], const uint8_t *a, uint32_t na,
                 const uint8_t *b, uint32_t nb, uint8_t out[32])
{
    sha_t s;
    uint8_t pad[64], inner[32];
    for (int i = 0; i < 64; i++)
        pad[i] = (uint8_t)((i < 32 ? key[i] : 0u) ^ 0x36u);
    sha_init(&s);
    sha_update(&s, pad, 64u);
    sha_update(&s, a, na);
    sha_update(&s, b, nb);
    sha_final(&s, inner);
    for (int i = 0; i < 64; i++)
        pad[i] = (uint8_t)((i < 32 ? key[i] : 0u) ^ 0x5Cu);
    sha_init(&s);
    sha_update(&s, pad, 64u);
    sha_update(&s, inner, 32u);
    sha_final(&s, out);
}

flash_f4.c — стирание сектора и запись слова во флеш-память F405.

/* flash_f4.c — стирание сектора и запись слова во флеш-память F405 регистрами (RM0090, 3.6). */
#include "bl.h"
#include "f4regs.h"

const uint8_t *flash_ptr(uint32_t addr)
{
    return (const uint8_t *)addr;
}

uint32_t flash_read32(uint32_t addr)
{
    return REG(addr);
}

static void flash_unlock(void)
{
    if (FLASH_CR & FCR_LOCK) {
        FLASH_KEYR = 0x45670123u;        /* KEY1 и KEY2 из RM0090, 3.6.1 */
        FLASH_KEYR = 0xCDEF89ABu;
    }
}

static int flash_wait(void)
{
    uint32_t err;
    while (FLASH_SR & FSR_BSY) {
    }
    err = FLASH_SR & FSR_ERRS;
    FLASH_SR = err | FSR_EOP;            /* флаги сбрасываются записью единицы */
    return err ? -1 : 0;
}

int flash_erase_sector(uint32_t sector)
{
    uint32_t a, end = sector_base(sector) + sector_size(sector);
    int e;
    flash_unlock();
    (void)flash_wait();
    FLASH_CR = FCR_PSIZE_X32 | FCR_SER | (sector << FCR_SNB_POS);
    FLASH_CR |= FCR_STRT;
    e = flash_wait();                    /* 16 КБ — до 0,5 с, 128 КБ — до 2 с (DS8626) */
    FLASH_CR = FCR_LOCK;
    if (e)
        return -1;
    for (a = sector_base(sector); a < end; a += 4u)
        if (REG(a) != 0xFFFFFFFFu)
            return -1;
    return 0;
}

int flash_program_word(uint32_t addr, uint32_t value)
{
    int e;
    flash_unlock();
    (void)flash_wait();
    FLASH_CR = FCR_PSIZE_X32 | FCR_PG;
    REG(addr) = value;                   /* одно слово за одну запись, а не четыре байта */
    e = flash_wait();
    FLASH_CR = FCR_LOCK;
    return (e || REG(addr) != value) ? -1 : 0;
}

app_main.c — проверочное приложение: печатает свою версию и может попросить перепрошивку.

/* app_main.c — проверочное приложение: печатает свою версию из заголовка и, если собрано
 * с APP_ASK_UPDATE, просит загрузчик о перепрошивке. */
#include "bl.h"
#include "f4regs.h"

extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
int main(void);

void Reset_Handler(void)
{
    uint32_t *s = &_sidata, *d = &_sdata;
    while (d < &_edata)
        *d++ = *s++;
    for (d = &_sbss; d < &_ebss;)
        *d++ = 0u;
    main();
    for (;;) {
    }
}

static void Fault_Handler(void)
{
    for (;;) {
    }
}

__attribute__((section(".isr_vector"), used))
static void (*const vectors[16])(void) = {
    (void (*)(void))&_estack, Reset_Handler, Fault_Handler, Fault_Handler,
    Fault_Handler, Fault_Handler, Fault_Handler, 0, 0, 0, 0, Fault_Handler, 0, 0, Fault_Handler, Fault_Handler,
};

static void put1(char c)
{
    while (!(USART1_SR & U_SR_TXE)) {
    }
    USART1_DR = (uint8_t)c;
}

static void say(const char *s, uint32_t v)
{
    while (*s)
        put1(*s++);
    for (int i = 28; i >= 0; i -= 4) {
        uint32_t d = (v >> i) & 15u;
        put1((char)(d < 10u ? '0' + d : 'a' + d - 10u));
    }
    put1('\n');
}

int main(void)
{
    RCC_AHB1ENR |= RCC_AHB1_GPIOA;       /* загрузчик всё вернул к сбросу — включаем заново */
    RCC_APB2ENR |= RCC_APB2_USART1;
    GPIOA_MODER = (GPIOA_MODER & ~(0xFu << 18)) | (0xAu << 18);
    GPIOA_AFRH = (GPIOA_AFRH & ~(0xFFu << 4)) | (0x77u << 4);
    USART1_BRR = 0x8Bu;
    USART1_CR1 = U_CR1_UE | U_CR1_TE;

    say("app: running, header version 0x", REG(APP_ADDR + 16u));
    say("app: VTOR 0x", SCB_VTOR);
#ifdef APP_ASK_UPDATE
    volatile uint32_t *f = (volatile uint32_t *)UPD_FLAG_ADDR;
    say("app: asking loader for update 0x", 0u);
    f[0] = UPD_FLAG_MAGIC;
    f[1] = ~UPD_FLAG_MAGIC;
    while (!(USART1_SR & (1u << 6))) {
    }
    SCB_AIRCR = 0x05FA0004u;
#endif
    for (;;) {
    }
}

Дальше: раздел 198э «Мини-проект: датчик температуры по Bluetooth LE на P-NUCLEO-WB55 — два ядра, IPCC и свой сервис GATT» — второе ядро с радио, IPCC и свой сервис для телефона.

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