198щ. Mini-project: your own bootloader for a production device on the STM32F405 — sector layout, staging slot, updates over UART-Bluetooth and protection against power loss
Section 48 covered the bootloader in general terms, 48а added a signature, 48б showed the ready-made MCUboot with slot swapping, and section 195 showed an A/B scheme with confirmation. Here all of it comes together on one real case: the bootloader of a production device on the STM32F405 that is updated at the customer's site through a Bluetooth module. The device's scheme works and is simple — and it has places where a loss of power or of the link leaves the device with no firmware or, worse, with half-written firmware.
We take this scheme apart in words, without its code and constants, and write our own bootloader from scratch on the same F405 sector layout: an image header instead of bare markers, reception only into the staging slot, copying with a journal, CRC-32 over the whole image, a check of origin and of the version number. All the code in the section is our own: the firmware is 1133 lines in 13 files (bootloader, test application, headers, linker scripts), the model and the image builder on the PC are another 930 lines. The bootloader takes 4528 bytes of flash memory.
There was no board at hand. The bootloader logic — the same C code — was built with gcc on a computer and run against a model of the F405 flash memory and a model of the link: in the model, power is lost on every erase and program operation, and bytes on the line are lost and corrupted. A second, independent calculation in Python builds the images and computes CRC-32, HMAC and frames. The bootloader and the test application were built with arm-none-eabi-gcc for the Cortex-M4 and run in QEMU on the netduinoplus2 machine (it is an STM32F405): image selection, the jump and the exchange of frames over UART were checked there. QEMU does not model writes to flash memory — this can be seen in the experiment in 198щ.2, and everything to do with writing was checked only on the model.
References: RM0090 Rev 19 (table 5 — sectors; 3.6 — erase and program,
the FLASH_KEYR keys, table 12 — the PSIZE parallelism; 4 — the CRC unit;
5.1.2 — the backup domain; 26 — the RTC backup registers; 39.1 — the unique
device ID); datasheet DS8626 Rev 9 (table 40 — word program time and
sector erase time, table 42 — endurance); the stm32f405xx.h header from
the STM32F4xx_DFP 2.17.1 pack and core_cm4.h from CMSIS 5.9.0 — all register
addresses and bits were checked against them. License: the section's own code is
free to use.
Terms used in the documentation
Term Meaning sector erase (SER, SNB) erasing a sector: the SER bit and the sector number SNB program/erase parallelism (PSIZE) program and erase width: x8, x16, x32 program sequence error (PGSERR) wrong order of the program sequence backup domain write protection (DBP) write lock on the backup domain RTC backup registers (RTC_BKPxR) RTC backup registers: 20 words kept by the battery backup SRAM (BKPSRAM) backup RAM, 4 KB program parallelism error (PGPERR) width error: an access of a size other than the one set in PSIZE write-ahead journal journal: first record the intent, then do it staging slot (download slot) staging slot: data is received here and copied from here stop-and-wait ARQ "send, then wait for the acknowledgement": retry on timeout anti-rollback protection against rolling back to an older version transparent mode (UART bridge) the module's transparent mode: what comes over the radio goes out to the UART
198щ.1 The original scheme, its weak points and the new layout
How the device's bootloader is built. It sits at the start of flash memory and takes the first sectors; its ID and version are stored in it at fixed addresses. The application starts at sector 5. The sign "an application is present" is several marker constants at fixed addresses inside the application image; the bootloader only needs to see any one of them. Sector 7 is the staging slot. An update goes one of two ways:
- through the application: it receives the new firmware over Bluetooth into the staging slot and restarts the device; the bootloader, on seeing a marker in the staging slot, erases the application sectors, rewrites the whole staging slot, 256 KB, one byte at a time, and erases the staging slot;
- through the bootloader, if there is no application: packets with a number and a 16-bit additive checksum are written straight into the application sectors; after the last one the bootloader sets a flag in a separate sector and resets the device.
The jump: check the markers, take the stack pointer from the first word of the image,
jump to the second; the application relocates the vector table itself. The unique device
ID — 96 bits — is folded by the hardware CRC unit into one word, from
which the Bluetooth module's name is made. One more detail: the application's
HardFault handler erases its own sectors and resets the device
so that it falls into the bootloader.
What is wrong with this and what we do instead.
| Weak point | What it leads to | In our bootloader |
|---|---|---|
| presence markers instead of an integrity check | an interrupted download with a marker at the start of the image looks like finished firmware | a header: length, CRC-32 of the body, CRC-32 of the header itself |
| a 16-bit additive checksum per packet | does not see swapped bytes at all (198щ.3) | CRC-16 per frame and CRC-32 per image |
| programming one byte at a time | four times as many program operations | programming in words, PSIZE = x32 |
| receiving straight into the active sectors | a loss of link or power leaves the device with no firmware or with half-written firmware | reception only into the staging slot; the bootloader touches the active slot after the check |
| copying without verification | a program error goes unnoticed | every word is verified, per-sector marks in the journal, CRC after copying |
| no check of origin | anyone who knows the format can load firmware | HMAC-SHA-256 over the header and the body |
| no version number | an old firmware with a known bug can be put back | version in the header, the highest version in the journal |
HardFault erases the application |
one fault, and the device has no firmware until it is reflashed | the application does not erase itself; the bootloader's wait window stops the start (198щ.2) |
The layout. F405 sectors differ in size (RM0090, table 5): four of 16 KB, one of 64 KB and seven of 128 KB. The model prints the same map:
сектор 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 КБ
| Sectors | What is there | Why |
|---|---|---|
| 0 (16 KB) | bootloader | fits with room to spare: 4528 bytes; the sector can be write-protected (WRP) |
| 1–2 (16 KB each) | update journal, two copies | small sectors erase in 250 ms, not in a second |
| 3–4 (16 + 64 KB) | application settings | the bootloader does not touch them |
| 5–6 (256 KB) | active slot: a 512-byte header and the application | as on the device — the application from sector 5 |
| 7–8 (256 KB) | staging slot | as on the device — the staging buffer from sector 7 |
| 9–11 (384 KB) | free | data, application logs |
The bootloader's linker script limits it to sector 0: if it grows beyond 16 KB, the build stops instead of overwriting the journal. Both scripts give the last 16 bytes of RAM to the request cell (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
}
The application is linked for the address after the header — 0x08020200. This
address is a multiple of 512, as the VTOR register requires (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
}
The image header is 60 bytes of fields padded with 0xFF bytes up to 512, and
a journal record is 32 bytes (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");
The image is built by mkimage.py — in Python and independently of the C code: CRC-32 there comes from
zlib, HMAC from hmac and hashlib. The bootloader (the same C code in the model)
accepts all three test images and rejects the forged one on authenticity:
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, подлинность НЕТ
Python printed the same CRCs before the model was run — the numbers matched.
198щ.2 The decision at startup and the jump
The image check goes from cheap to expensive, as in 48.4: magic, header size, its CRC, length, link address, stack pointer and entry point — and only then the CRC-32 of the whole body. The staging image is checked by the same function: it is linked for the active slot, not for the staging slot, and the entry address must point into the active slot.
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 is computed a nibble at a time: a table of 16 words takes 64 bytes
instead of 1 KB for the byte-wise one. The result is the same as 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;
}
The journal. Everything the bootloader must remember between startups is one
record: the state, how many sectors have already been copied, the length and CRC of the image
in the staging slot, the highest installed version. Records are only appended;
each one is protected by its own CRC-32. If a write is interrupted halfway, the CRC will not
match, and reading skips the record and takes the previous one. When one copy is full,
the other is erased and records go there; the seq number shows which one
is newer.
/* Самая новая целая запись из двух копий; ни одной — «покой», версия 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;
}
Copying. Each sector of the active slot is erased, programmed
in words from the staging slot (the write is verified in flash_program_word), and marked in
the journal with COPY k. If power is lost, copying continues at the next startup
from the first unmarked sector: the staging slot is intact, and the sector
hit by the power loss is erased again.
/* Каждый сектор: стереть, записать словами, сверить (в 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;
}
The order in boot_decide is: first an unfinished update (it
matters most), then a request from the application, then the check of the active slot.
If there is no valid application, the bootloader stays and waits for an image over UART: the device
is never "empty", the bootloader is always alive.
The jump. The bootloader runs from the 16 MHz HSI and does not touch the clock —
there is nothing to restore for the application. What it enabled — ports, two UARTs, the CRC unit,
SysTick — is reset through RCC_xxxRSTR and returns to the values
after reset (RCC_AHB1ENR = 0x00100000 according to RM0090); then VTOR,
barriers, the stack and the jump.
/* Вернуть всё, что включил загрузчик, переставить таблицу векторов, стек — и прыжок. */
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 (;;) {
}
}
The main loop: start, decision, wait window, reception. The window is 300 ms after
the "run" decision: if a start-of-frame byte arrives from the host during it,
the bootloader stays. This replaces erasing the application from HardFault:
a device with a bug in the new firmware that crashes right at startup
can still be reflashed.
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);
}
}
}
In QEMU. The 1 MB flash image is built by mkimage.py: the bootloader
(built with -DQEMU_NO_UID, because QEMU's netduinoplus2 has no
unique device ID area — reading it gives a bus fault), application v1 with
its header, the journal. USART1 is the bootloader's log, USART3 is the line to the module:
== q1. Приложение v1 цело
boot: start, loader version 00010000
boot: app ok, version 00000001
app: running, header version 0x00000001
app: VTOR 0x08020200
The bootloader found the application by its header, checked the CRC-32 and jumped;
the application sees VTOR = 0x08020200. One corrupted bit in the body gives
error 7 (IMG_EBODYCRC), the bootloader stays and answers the
QUERY request; the reply carries bootloader version 0x00010000 and application version 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
The wait window works too: a stream of QUERY frames with an intact application
stops the start, and the reply already carries version 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
But QEMU does not write flash memory: the FLASH registers on this machine
are a stub (they read as zeros, writes are lost), and the memory itself is read-only.
A journal with the READY state and image v2 in the staging slot gives an honest
failure — the erase "succeeded", but verification saw the old data — and the device
keeps running the previous v1:
== q4. В журнале READY, в буфере v2
boot: start, loader version 00010000
boot: copy failed, sectors done 00000000
boot: app ok, version 00000001
That is why everything to do with writing is in the model (198щ.5).
198щ.3 Reception over UART: a Bluetooth module is just a UART
The module in transparent mode passes bytes from the radio link to the UART and back; for the bootloader it is USART3 at 115200. On top of it runs our own "send, then wait for the acknowledgement" protocol:
| Field | Bytes | What |
|---|---|---|
| SOF | 1 | 0xA5 — start of frame |
| type | 1 | QUERY 0x01, START 0x02, DATA 0x03, END 0x04; replies ACK 0x80, NAK 0x81, QUERY_R 0x82 |
| number | 2 | number of the DATA frame from zero; in a reply, which frame it answers |
| length | 2 | 0…256 bytes of data |
| data | 0…256 | START: image length; DATA: a piece of the image, a multiple of 4 |
| CRC-16 | 2 | CCITT, initial value 0xFFFF, from the type to the end of the data |
The START frame for image v2 (6656 = 0x1A00 bytes) is 12 bytes:
START: a5 02 00 00 04 00 00 1a 00 00 16 5c
Python builds the same frame with binascii.crc_hqx — the bytes match.
The host sends a frame and waits for a reply. An ACK with the same number means the next frame;
a NAK or silence means a retry. The device writes a DATA frame only if its number
is the expected one. If the number is one less, our ACK was lost and
the frame has already been written: the device acknowledges it again but does not write it. A pause on the
line of 20 ms discards a partly assembled frame — this is how reception recovers
after a lost byte.
/* Байт из 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;
}
The main point here is where the data is written: only into the staging slot. The active
slot is touched by the bootloader, and only after READY is written to the journal, and READY
is set when the whole image has passed the length, CRC-32, HMAC and version
checks. If the link drops in the middle of reception, at the next startup the device
simply runs the previous firmware. Rejections in the model:
чужой ключ: ответ -107 (NAK 7), после старта работает v1
та же версия 1: ответ -108 (NAK 8), после старта работает v1
бит тела испорчен: ответ -106 (NAK 6), после старта работает v1
оборван: послано 4096 байт: ответ -104 (NAK 4), после старта работает v1
Codes: 4 — length, 6 — image, 7 — authenticity, 8 — version not newer.
The frame CRC-16 — the function itself:
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;
}
Why not a sum. The model takes a frame of 256 random bytes and corrupts it in four ways, 20000 trials for each (a trial where the corruption changes nothing is skipped). A 16-bit additive sum of bytes, as on the device, against CRC-16:
один бит: испытаний 20000, сумма не заметила 0, CRC-16 не заметила 0
соседние байты поменялись местами: испытаний 19929, сумма не заметила 19929, CRC-16 не заметила 0
+d в одном байте и -d в другом: испытаний 19257, сумма не заметила 19257, CRC-16 не заметила 0
два соседних байта испорчены: испытаний 20000, сумма не заметила 54, CRC-16 не заметила 0
The sum does not depend on the byte order — it never sees a swap. Python repeats the experiment with the same generator and gets the same numbers.
A lossy link. Twenty updates v1 → v3 (160512 bytes, two sectors) over a line where 0.05% of bytes are lost and 0.05% are corrupted in both directions:
обновлений v1 -> v3: 20, установлено целым 20, без подтверждения END 0
кадров 16382, повторов 3799 (без ответа 2119, NAK по CRC 1680), байт к прибору 4313928, записей слов данных 1605120 = 20 x 2 x 40128
All 20 images were installed intact. There are exactly twice as many data writes as words in the image (the staging slot plus the active slot) — so no repeated frame was written a second time.
198щ.4 How the application asks for reflashing; the unique device ID
The application needs to tell the bootloader "stay after the reset". The F405 has four places for this:
| Where | What it needs (RM0090) | Survives | Cost |
|---|---|---|---|
| a cell at the end of RAM | a shared area in both scripts (48.6) | a software reset | nothing; lost when power is removed |
RTC_BKPxR |
PWREN, the DBP bit, the RTC clock selection and RTCEN (5.1.2) |
reset and power removal with a battery on VBAT |
20 words; cleared on tamper |
backup RAM BKPSRAM |
PWREN, DBP, BKPSRAMEN (5.1.2) |
the same, if the backup regulator is on | 4 KB |
| a record in the journal in flash memory | nothing new | everything | an erase cycle; a 128 KB sector takes 1 s |
The device sets a flag in flash memory — in a separate 128-kilobyte sector, that is, it erases a whole sector for every request. We only need to survive a software reset: an update always starts from a running application. Hence the RAM cell: two words, a magic value and its complement, so that random garbage after power-on is not taken for a request.
/* Признак от приложения: два слова в общей ячейке ОЗУ; прочитали — стёрли. */
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;
}
In the application:
#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
In QEMU the application makes the request and resets the device, the bootloader sees the request and stays — the cell survived the reset:
== 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
That on-chip RAM survives a software reset was checked only in QEMU. If the request is lost when power is removed, the application simply repeats it.
The unique device ID. The 96 bits at address 0x1FFF7A10 (RM0090, 39.1) are conveniently
folded into one word by the CRC unit: polynomial 0x4C11DB7, one word in 4 clock cycles
(RM0090, 4.2). This word gives the module name, unique to each
device, and it also goes into the QUERY reply:
/* 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
}
The CRC unit computes differently from zlib: most significant bit first, without reflection and
without a final XOR. The PC model repeats it bit by bit; for
the example ID 00340047 3235510b 31383830 the word is
ca979f91, and Python gets the same value as CRC-32/MPEG-2 over the bytes
of each word, most significant first. QEMU has no CRC unit — this part was checked
only on the model.
198щ.5 Power loss: the flash memory model and counting the points
The model (flash_model.c) is the 12 F405 sectors with their sizes. An erase
sets 0xFF, a word write only clears bits — a 0 cannot be raised to 1 without
an erase (RM0090, 3.6.4). About a reset in the middle of an operation RM0090 (3.6)
says only one thing: the contents are not guaranteed. The model does this: on
the chosen operation power is lost; during an erase some bits of the sector
have already become ones, during a word write some bits have already become zeros. Then
comes "power-on" — the same boot_decide, and the check: what started?
The old image byte for byte, the new one byte for byte, nothing, or "something"?
The real driver uses the registers as in RM0090: the keys, PSIZE = x32, SER and
the sector number, waiting for BSY, verification:
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;
}
The update v1 → v2 (6656 bytes), from the application's request to the start of the new version, is 3362 erase and program operations. Power is lost on each of them:
v1 -> v2 (6656 байт): операций без обрыва 3362 (приём 1681, копирование 1681), стираний 2, записей слов 3360, из них в журнал 32
обрыв при приёме: точек 1681 — старая целая 1681, новая целая 0, без прошивки 0, НЕДОПИСАННАЯ 0
обрыв при копировании: точек 1681 — старая целая 0, новая целая 1681, без прошивки 0, НЕДОПИСАННАЯ 0
второй обрыв во время восстановления (первая, средняя, последняя операция): точек 5043 — старая целая 0, новая целая 5043, без прошивки 0, НЕДОПИСАННАЯ 0
The boundary lies exactly at the READY write: before it, the device always
runs the old intact firmware, after it, the new intact one. A second power loss, already
while copying resumes, changes nothing. With the journal almost full
(510 records of 512), the switch to the other copy happens in the middle of the
update, and a journal sector erase is added:
v1 -> v2, журнал почти полон (510 записей): операций без обрыва 3363 (приём 1681, копирование 1682), стираний 3, записей слов 3360, из них в журнал 32
обрыв при приёме: точек 1681 — старая целая 1681, новая целая 0, без прошивки 0, НЕДОПИСАННАЯ 0
обрыв при копировании: точек 1682 — старая целая 0, новая целая 1682, без прошивки 0, НЕДОПИСАННАЯ 0
Image v3 spans two sectors — 80300 operations; here a power loss is tried on every erase, every journal write and every 97th data write:
v1 -> v3 (160512 байт), запись данных — каждая 97-я: операций без обрыва 80300 (приём 40146, копирование 40154), стираний 4, записей слов 80296, из них в журнал 40
обрыв при приёме: точек 431 — старая целая 431, новая целая 0, без прошивки 0, НЕДОПИСАННАЯ 0
обрыв при копировании: точек 440 — старая целая 0, новая целая 440, без прошивки 0, НЕДОПИСАННАЯ 0
The original scheme on the same model (naive.c, with its own constants: a presence
marker at the start of the image, programming one byte at a time, the staging slot copied as a whole).
The same image v2:
исходная схема, приём загрузчиком в рабочие секторы: операций 6660 (приём 6660, копирование 0)
обрыв при приёме: точек 6660 — старая целая 0, новая целая 0, без прошивки 504, НЕДОПИСАННАЯ 6156
исходная схема, приём в буфер приложением: операций 268806 (приём 6658, копирование 262148)
обрыв при приёме: точек 6658 — старая целая 502, новая целая 0, без прошивки 0, НЕДОПИСАННАЯ 6156
обрыв при копировании: точек 261 — старая целая 0, новая целая 261, без прошивки 0, НЕДОПИСАННАЯ 0
Receiving straight into the active sectors: at 504 points of 6660 the device is left without firmware (the marker is not written yet), at 6156 it runs half-written firmware: the marker at the start of the image is already in place. Receiving into the staging slot through the application is no better — 6156 half-written: the marker is in the staging slot, and the bootloader copies the fragment over the working firmware. On the other hand, the original scheme's copying from the staging slot survives power loss (here every 1024th of 262148 operations was tried): the staging slot is erased last, and the copy starts over. So what needs fixing is not the copying, but the ready flag and the place where data is received.
198щ.6 The cost: size, time, endurance
Size. gcc 12.2 with -Os, no libraries (arm_host.txt):
text data bss dec hex filename
4524 4 8 4536 11b8 boot.elf
368 0 0 368 170 app1.elf
The bootloader is 4528 bytes of flash memory (code and initialized data), 28% of sector
0. The largest part is SHA-256 and HMAC: 1044 bytes (sha256.o), of which
256 bytes are the constant table. Frame parsing is 714 bytes, the image check
428, the journal 400, the startup decision 510. The build with -Wall -Wextra
passed without warnings; all addresses and bits in f4regs.h were checked against
stm32f405xx.h and core_cm4.h — 58 _Static_assert checks at
compile time (selfcheck.c).
Time is only an estimate from DS8626 (table 40); it was not measured on a board. A word write is 16 µs typical and 100 µs maximum at any parallelism; a 16 KB sector erase at x32 is 250 and 500 ms, 128 KB is 1 and 2 s; at x8 a 128 KB sector erases in 2 and 4 s.
| What | Typical | Maximum |
|---|---|---|
| programming 256 KB in words: 65536 operations | 1.05 s | 6.6 s |
| programming 256 KB one byte at a time: 262144 operations | 4.2 s | 26.2 s |
| copying v3 (160512 bytes): 2 erases + 40128 words | 2.64 s | 8.01 s |
| sending v3 over UART at 115200: 165548 bytes to the device, 5032 back | 14.8 s | — |
The transfer time excludes radio link delays and the time to erase the staging slot before reception; a frame adds 8 bytes to the 256 bytes of data, plus another 8 bytes for the reply. Copying one byte at a time, as on the device, takes four times longer than programming in words — on the F4 the program time per operation is the same at any parallelism.
Endurance. Each update erases the used sectors of the staging slot and of the active slot once each. The model ran 300 updates in a row:
установлено 300 из 300, запись журнала №1200, старшая версия 301, всего стираний 602
Four journal records per update (RX, READY, COPY 1, IDLE) —
a journal sector of 512 records is erased once every 128 updates; 602
erases = 2 per update plus 2 journal switches. With an endurance of 10
thousand cycles (DS8626, table 42) the slot sectors wear out first —
after 10000 updates.
198щ.7 Pitfalls
There is a marker, so there is firmware. No: the marker at the start of the image is written in the very first packets. Anything interrupted after it the bootloader will take as finished. The ready flag must be set last and must check the whole: the length and the CRC of the entire image.
Receiving straight into the active sectors. The very first erase leaves the device without firmware until reception ends; a link drop in the middle leaves half-written firmware. Receive only into the staging slot, and copy after the check.
A sum instead of a CRC. An additive sum does not see swapped bytes or compensating errors: 19929 and 19257 misses out of 19929 and 19257 in 198щ.3. CRC-16 per frame and CRC-32 per image are both cheaper than they seem: 64 bytes of table for CRC-32.
Programming one byte at a time. Four times as many operations and four times as long; besides,
the program width must match PSIZE (RM0090, 3.6.4), otherwise
PGPERR. Program in words with a supply of 2.7–3.6 V.
A sector erase stalls the core. While an erase is in progress, reading
flash memory — including instruction fetch — stalls (RM0090, 3.6), and
a 128 KB sector takes up to 2 s to erase. Bytes that arrive at the UART during this time
are lost. That is why the device erases the staging slot when it receives START and replies
ACK only after the erase: the host waits and sends nothing.
The watchdog. The same 2 s per sector: a watchdog with a short timeout will restart the device in the middle of copying. The journal will survive this, but copying will never finish. The timeout must be longer than the maximum erase time.
The application erases itself on HardFault. One fault, and the device has no
firmware until it is reflashed. The bootloader's wait window
solves the same problem without erasing: the host has time to stop the start.
Jumping without resetting the peripherals. The device's bootloader, at every startup, configures the UART with an interrupt and the Bluetooth module — even before it has decided what to run — and jumps without returning them to their reset state. The application gets a chip in a state its initialization does not expect (48.5), and every startup is lengthened by the exchange with the module. Decide first, talk to the module only if you stay, and before the jump reset everything the bootloader enabled (198щ.2).
The unique device ID in QEMU. The netduinoplus2 has no 0x1FFF7A10 area —
reading it gives a bus fault. The QEMU build substitutes zero
(QEMU_NO_UID).
"Everything got written in QEMU". No: QEMU's flash memory is read-only,
the FLASH registers are a stub, BSY is always zero. A driver that did not verify
what it wrote would report success. Verifying every word and every erase
is mandatory on the chip as well.
198щ.8 Summary
- The bootloader of a production device on the STM32F405 was analysed as a scheme: presence markers, a 16-bit sum, programming one byte at a time, reception straight into the active sectors. On the model, such reception runs half-written firmware at 6156 of 6660 power-loss points and leaves the device without firmware at another 504.
- The new layout: sector 0 is the 4528-byte bootloader, sectors 1–2 are the journal with 512 records each, 5–6 the 256 KB active slot, 7–8 the 256 KB staging slot.
- A 512-byte header: magic, length, CRC-32 of the body, version, link address, HMAC-SHA-256, CRC-32 of the header; the application starts at 0x08020200.
- Reception only into the staging slot, in frames of 256 bytes with CRC-16 and acknowledgement; over a link with 0.05% losses and 0.05% corruption all 20 images of 160512 bytes were installed intact, and repeated frames were not written twice.
- Copying sector by sector with a journal: a power loss at each of the 3362 update operations gives the old intact firmware 1681 times, the new one 1681 times, half-written 0; a second power loss (5043 points) also always gives the new one.
- Rejection without consequences: a foreign key, an older version, a corrupted bit, an interrupted image — the device stays on v1.
- The reflashing request is two words at the end of RAM; a 300 ms wait window replaces erasing the application on a fault.
- In QEMU netduinoplus2 the bootloader selects the image, checks the CRC and
jumps (
VTOR= 0x08020200), and answers frames on USART3; QEMU does not model writes to flash memory. - The cost: 4528 bytes of code, of which 1044 are SHA-256; copying 256 KB in words takes 1.05 s of programming versus 4.2 s one byte at a time; endurance is 10000 updates.
Full sources
bl.h — layout, image header, journal and protocol — one header for everything.
/* 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 — the bootloader on the STM32F405: startup, UART, wait window, reception, jump to the application.
/* 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 — the startup decision and copying the staging slot into the active slot by the journal.
/* 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 — receiving the image over UART in frames with CRC-16 and an ACK or NAK reply.
/* 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 — the update journal: records are only appended, two copies in turn.
/* 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 — checking the image in a slot — from cheap checks to expensive ones.
/* 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 of the image and CRC-16 of a frame.
/* 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 and 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 — sector erase and word programming in the F405 flash memory.
/* 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 — the test application: prints its version and can request reflashing.
/* 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 (;;) {
}
}
Next: section 198э "Mini-project: a temperature sensor over Bluetooth LE on the P-NUCLEO-WB55 — two cores, IPCC and a custom GATT service" — the second core with the radio, IPCC and a custom service for the phone.