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 Исходная схема, её слабые места и новая раскладка
Как устроен загрузчик прибора. Он лежит с начала флеш-памяти и занимает первые секторы; по фиксированным адресам в нём записаны его идентификатор и версия. Приложение начинается с сектора 5. Признак «приложение есть» — несколько констант-меток на фиксированных адресах внутри образа приложения; загрузчику достаточно увидеть любую из них. Сектор 7 — буферный слот. Обновление идёт двумя путями:
- через приложение: оно принимает новую прошивку по Bluetooth в буфер и перезапускает прибор; загрузчик, увидев в буфере метку, стирает секторы приложения, переписывает буфер целиком, 256 КБ, по байту, и стирает буфер;
- через загрузчик, если приложения нет: пакеты с номером и 16-битной аддитивной суммой пишутся прямо в секторы приложения; после последнего загрузчик ставит признак в отдельном секторе и сбрасывает прибор.
Переход: проверить метки, взять указатель стека из первого слова образа,
прыгнуть по второму; таблицу векторов переставляет само приложение. Номер
кристалла — 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 Итоги
- Загрузчик прибора на STM32F405 разобран как схема: метки присутствия, 16-битная сумма, запись по байту, приём прямо в рабочие секторы. На модели такой приём в 6156 из 6660 точек обрыва запускает недописанную прошивку и ещё в 504 оставляет прибор без прошивки.
- Новая раскладка: сектор 0 — загрузчик 4528 байт, секторы 1–2 — журнал по 512 записей, 5–6 — рабочий слот 256 КБ, 7–8 — буфер 256 КБ.
- Заголовок 512 байт: магия, длина, CRC-32 тела, версия, адрес сборки, HMAC-SHA-256, CRC-32 заголовка; приложение начинается с 0x08020200.
- Приём только в буфер, кадрами по 256 байт с CRC-16 и подтверждением; по каналу с 0,05 % потерь и 0,05 % искажений все 20 образов на 160 512 байт установлены целыми, повторные кадры не записаны дважды.
- Копирование по секторам с журналом: обрыв питания в каждой из 3362 операций обновления — 1681 раз старая целая прошивка, 1681 раз новая, недописанных 0; второй обрыв (5043 точки) — тоже всегда новая.
- Отказ без последствий: чужой ключ, старая версия, испорченный бит, оборванный образ — прибор остаётся на v1.
- Запрос перепрошивки — два слова в конце ОЗУ; окно ожидания 300 мс заменяет стирание приложения при сбое.
- В QEMU netduinoplus2 загрузчик выбирает образ, проверяет CRC и
переходит (
VTOR= 0x08020200), отвечает на кадры по USART3; запись во флеш-память QEMU не моделирует. - Цена: 4528 байт кода, из них 1044 — SHA-256; копирование 256 КБ словами — 1,05 с записи против 4,2 с по байту; ресурс — 10 000 обновлений.
Полные исходники
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 и свой сервис для телефона.