Embedded HandbookApps
All sections › VI. Practice
STM32F103, STM32F407 · level intermediate · 62 min read
Open in the interactive handbook

48в. Delta update: sending only the changes in the firmware

In sections 48, 48а and 48б the new firmware always arrives in full; the same is true for ESP32 (section 159) and for Linux with two partitions (section 195). Over a wire this is no problem. Over a slow radio link an image of tens of kilobytes takes hours, while it often differs from the previous one by a few dozen bytes. Here the device receives only the delta: a short file from which it builds the new image itself, together with the old one.

In this section: counting frames and time for LoRa, our own delta format, a generator in Python, patching on the device in C without malloc, power loss at every step, three builds of the same firmware whose deltas differ fifteen times, and patching "in place", without a second slot.

References: the time-on-air formula from section 89 and the 1 % limit from 93.4; the flash pages of F1 are in RM0008, the sectors of F4 in RM0090; erase and program times and page endurance are in the STM32F103x8/xB datasheet (DocID13587 Rev 17, tables 28 and 29); writing to flash memory is section 11.5, transfer of control is 48.5. Experiment: eight firmware builds for STM32F103 and the bootloader are built with arm-none-eabi-gcc 12.2.1 in the stm-arm:1 image; the device code, unchanged, is built with gcc 12.2.0 in stm-sys:1 and run on a flash memory model; the images produced by patching are run in QEMU 7.2.22. Not run on a board. Ready-made programs of this kind are bsdiff, xdelta, JojoDiff with the janpatch library for microcontrollers (github.com/janjongboom/janpatch); the section was not compared with them, the format and the code here are our own.

Terms used in the documentation

Original Meaning
delta update, differential update update that sends only the differences
delta, patch the delta — the file of changes
diff, patch (programs) build a delta, apply a delta
copy / add (insert) the commands "take from the old image" and "insert new bytes"
variable-length integer, varint a number of variable length
zigzag encoding writing a signed number: 0, −1, 1, −2 → 0, 1, 2, 3
in-place update patching in place — into the same slot
power-fail safe survives a power loss
time on air, duty cycle time on air, the share of time on air
erase unit: page, sector the unit of erasure: a page, a sector

48в.1 Why: counting frames

Take the firmware from this section's experiment: 24516 bytes for STM32F103. The channel is LoRaWAN at 125 kHz (sections 93 and 94). A frame carries 48 bytes of the image and 2 bytes of chunk number; with the 13 overhead bytes of LoRaWAN (section 89) this is 63 bytes. By the lora_airtime_us formula from section 89 such a frame occupies the air for 118.0 ms at SF7 and 2793.5 ms at SF12. In the main 868 MHz bands a transmitter may occupy 1 % of the time (93.4), so every second of transmission is followed by 99 seconds of silence.

What we send Bytes Frames SF7: on air, no faster than SF12: on air, no faster than
full image 24516 511 60.3 s, 100.5 min 1427.1 s, 2378.6 min
delta (b) — new function at the start 706 15 1.7 s, 2.9 min 41.4 s, 69.0 min
delta (c) — the same function at the end 77 2 0.2 s, 0.4 min 4.9 s, 8.2 min
delta (a) — one constant 46 1 0.1 s, 0.2 min 2.8 s, 4.7 min

The full image at SF12 takes 2378.6 minutes, more than a day and a half, and that is a lower bound: no retries of lost frames and no transmissions by the device itself. A class A device listens only after its own transmission (94.6), so every downlink frame also costs an uplink frame. In the second receive window at 869.525 MHz 10 % is allowed (93.4) — ten times faster, but that is still almost four hours. Delta (b) is 35 times shorter than the image, delta (c) 318 times. Where rows (a), (b) and (c) come from and why they differ so much is in 48в.5.

Delta update on an STM32F103 with 128 KB of flash memory: an 8 KB bootloader, working slot 0 and slot 1 of 52 KB each, a 16 KB delta area; step 1 — COPY commands take bytes of the old image from slot 0, ADD commands take new bytes from the delta, the result is written to slot 1; step 2 — CRC-32 check; step 3 — copying the changed pages into slot 0; step 4 — erasing the delta header; at the bottom, the delta of pair (a) byte by byte: a 32-byte header and five commands

You pay for the saved airtime with flash memory and complexity. The device needs room for the delta and a second slot to build the new image in, and the bootloader needs the patching code. One more condition: a delta fits only the build it was computed from. The server must know which version is installed on every device and keep its binary image.


48в.2 The delta format and the generator

The new image consists almost entirely of pieces of the old one. The delta describes it with two commands: COPY — "take n bytes from the old image" and ADD — "insert n new bytes". The command stream is preceded by a 32-byte header, eight words, least significant byte first:

Offset Field What it holds
0 magic 0x31564C44 — the bytes DLV1 in the file
4 old_size length of the old image
8 old_crc CRC-32 of the old image: the delta will not fit another build
12 new_size length of the new image
16 new_crc CRC-32 of the new image — to check the result
20 body_size length of the command stream
24 body_crc CRC-32 of the command stream
28 hdr_crc CRC-32 of the first 28 bytes of the header

The CRC-32 is the same everywhere — as in zlib, polynomial 0xEDB88320. The command stream:

Command First number Then What the device does
COPY n × 2 pointer offset — a signed number moves the pointer, takes n bytes of the old image, pointer += n
ADD n × 2 + 1 n bytes inserts these bytes, pointer += n

Numbers are written in a variable-length code: 7 bits per byte, least significant first, the top bit of a byte means "more follows"; no longer than 5 bytes. A signed number is first turned into an unsigned one by interleaving: 0, −1, 1, −2 → 0, 1, 2, 3.

The device keeps the pointer into the old image itself, and COPY carries not an address but an offset relative to it. A subtlety: the pointer also moves forward on ADD. Then the most common case — a few bytes replaced in place — gives an offset of 0, and a COPY command takes two to four bytes.

Here is the whole delta of pair (a), 46 bytes. One constant was changed in the firmware, and 2 bytes out of 24484 changed in the image:

  заголовок: 44 4c 56 31 a4 5f 00 00 d5 ea 1a c4 a4 5f 00 00 73 79 2a 13 0e 00 00 00 3d 30 ae a0 99 03 3d 64
  поток:     bc 03 00 03 7a 8a 0a 00 03 7a fe f0 02 00
  команды:   COPY 222 с 0x0; ADD 1; COPY 645 с 0xdf; ADD 1; COPY 23615 с 0x365

Header: DLV1, lengths 0x5FA4 = 24484, stream 0x0e = 14 bytes. The stream: bc 03 is 0x3C + 3 × 128 = 444 = 222 × 2, that is COPY of 222 bytes, followed by offset 00. 03 is ADD of one byte, the byte itself is 7a. 8a 0a = 1290 = 645 × 2, COPY of 645 bytes, offset 00: after the ADD the pointer already stands at 0xdf. One more ADD and a COPY of the rest — fe f0 02 = 47230 = 23615 × 2. In total 222 + 1 + 645 + 1 + 23615 = 24484.

The generator runs on the computer. The numbers and the search for the longest match:

def varint(v):
    """Число без знака: по 7 бит, младшие вперёд; старший бит байта — «есть продолжение»."""
    out = bytearray()
    while True:
        b = v & 0x7F
        v >>= 7
        if v:
            out.append(b | 0x80)
        else:
            out.append(b)
            return bytes(out)


def zigzag(d):
    """Число со знаком -> без знака: 0, -1, 1, -2, 2… -> 0, 1, 2, 3, 4…"""
    return d << 1 if d >= 0 else ((-d) << 1) - 1


def match_len(old, i, new, j):
    """Сколько байт подряд совпадает у old[i:] и new[j:]."""
    n = min(len(old) - i, len(new) - j)
    if i < 0 or n <= 0:
        return 0
    if old[i:i + n] == new[j:j + n]:
        return n
    lo, hi = 0, n                        # lo байт совпадает, hi — уже нет
    while hi - lo > 1:
        mid = (lo + hi) // 2
        if old[i:i + mid] == new[j:j + mid]:
            lo = mid
        else:
            hi = mid
    return lo

The parsing itself is greedy. First a table is built: every block of the old image 8 bytes long (BLOCK) is a dictionary key, the value is the places where it occurs (no more than 64 per block). A Python dictionary is itself the hash table of blocks. Then the generator walks the new image and at every position looks for the longest match with the old one:

def find_ops(old, new, block=BLOCK):
    """Жадный разбор нового образа на команды ("copy", src, n) и ("add", bytes)."""
    index = {}
    for i in range(len(old) - block + 1):
        places = index.setdefault(old[i:i + block], [])
        if len(places) < MAX_CAND:
            places.append(i)
    ops = []
    j = 0                                # позиция в новом образе
    lit = 0                              # начало ещё не выданных новых байт
    cur = 0                              # указатель в старом образе — тот же, что ведёт прибор
    while j < len(new):
        guess = cur + (j - lit)          # «новые байты заменили столько же старых»
        best_n, best_src = match_len(old, guess, new, j), guess
        if best_n < MIN_COPY:
            best_n = 0
        for src in index.get(new[j:j + block], ()):
            n = match_len(old, src, new, j)
            if n > best_n or (n == best_n and abs(src - guess) < abs(best_src - guess)):
                best_n, best_src = n, src
        if best_n == 0:
            j += 1
            continue
        if j > lit:
            ops.append(("add", new[lit:j]))
            cur += j - lit
        ops.append(("copy", best_src, best_n))
        cur = best_src + best_n
        j += best_n
        lit = j
    if j > lit:
        ops.append(("add", new[lit:j]))
    return ops


def encode(ops, old, new):
    """Команды -> файл разницы: заголовок и поток."""
    body = bytearray()
    cur = 0
    for op in ops:
        if op[0] == "add":
            data = op[1]
            body += varint(len(data) << 1 | 1) + data
            cur += len(data)
        else:
            _, src, n = op
            body += varint(n << 1) + varint(zigzag(src - cur))
            cur = src + n
    hdr = struct.pack("<7I", MAGIC, len(old), zlib.crc32(old), len(new), zlib.crc32(new),
                      len(body), zlib.crc32(bytes(body)))
    hdr += struct.pack("<I", zlib.crc32(hdr))
    return hdr + bytes(body)

The guess place is checked first — the pointer gets there by itself if the new bytes replaced as many old ones. For it a match of 4 bytes is enough (MIN_COPY): anything shorter is cheaper to send as new bytes. The other places come from the block table; among equal lengths the one closest to guess is taken, so that the offset takes fewer bytes. Running it:

old 24484, new 24516, delta 706 bytes: 62 COPY (24065 bytes), 56 ADD (451 bytes)

This is the output of python mkdelta48v.py v1.bin v2b.bin b.dlt for pair (b). The block length hardly affects the result: for the same pair blocks of 4, 8, 16 and 32 bytes gave 700, 706, 723 and 723 bytes. The generator is not the best possible one: it does not compress the new bytes and does not look for which of the matches pays off several steps ahead. On the other hand, parsing such a stream on the device takes one function.


48в.3 Patching on the device: two slots, fixed buffers

The layout of the experiment is an STM32F103 with 128 KB of flash memory, 1 KB pages: an 8 KB bootloader, slot 0 — the working one, 52 KB from 0x2000, slot 1 — 52 KB from 0xF000, the delta area — 16 KB from 0x1C000. The application always runs from slot 0 and is built for its address, as with MCUboot (48б.1). It receives the delta into its area and reboots; the bootloader applies the delta. The header, the layout and the flash memory port:

typedef struct {                         /* заголовок разницы: 8 слов, младшим байтом вперёд */
    uint32_t magic;
    uint32_t old_size;                   /* длина старого образа */
    uint32_t old_crc;                    /* его CRC-32: не тот исходник — отказ */
    uint32_t new_size;                   /* длина нового образа */
    uint32_t new_crc;                    /* CRC-32 результата */
    uint32_t body_size;                  /* длина потока команд за заголовком */
    uint32_t body_crc;                   /* CRC-32 потока */
    uint32_t hdr_crc;                    /* CRC-32 первых 28 байт заголовка */
} delta_hdr_t;

_Static_assert(sizeof(delta_hdr_t) == DELTA_HDR, "delta header must be 32 bytes");

typedef struct {                         /* раскладка: смещения от начала флеш-памяти */
    uint32_t slot0;                      /* рабочий слот: отсюда запускается приложение */
    uint32_t slot1;                      /* второй слот: здесь собирается новый образ */
    uint32_t slot_size;
    uint32_t delta;                      /* область, куда приложение приняло разницу */
    uint32_t delta_size;
} delta_map_t;

enum {
    DB_NONE = 0,                         /* разницы нет или она повреждена: запускать слот 0 */
    DB_DONE,                             /* слот 0 — новая версия, разница убрана */
    DB_FOREIGN,                          /* разница не к этой прошивке: слот 0 не тронут */
    DB_FAILED,                           /* поток команд неверен или результат не сошёлся: слот 0 не тронут */
    DB_RETRY                             /* сбой записи при копировании: слот 1 цел, повторить при следующем старте */
};

/* Порт: четыре функции над флеш-памятью. Все возвращают 0 при успехе. */
int      fl_read(uint32_t addr, uint8_t *buf, uint32_t n);
int      fl_erase(uint32_t addr);        /* стереть единицу стирания, начинающуюся с addr */
int      fl_write(uint32_t addr, const uint8_t *buf, uint32_t n);
uint32_t fl_unit(uint32_t addr);         /* размер единицы стирания по этому адресу */

The whole patching code knows only these four functions about the chip: the same file works with F1 pages of 1 and 2 KB and with F4 sectors. Memory is one static structure: a 256-byte write block, a 64-byte stream read window and seven words of state.

#define WR_BLOCK 256u                    /* блок записи: страница и сектор кратны ему */
#define RD_BLOCK 64u                     /* окно чтения потока команд */

static struct {
    uint8_t  wr[WR_BLOCK];               /* блок, который ляжет во флеш-память */
    uint8_t  rd[RD_BLOCK];               /* окно потока команд */
    uint32_t wr_addr;                    /* куда ляжет блок */
    uint32_t wr_fill;                    /* сколько в нём байт */
    uint32_t erased_to;                  /* до этого адреса слот уже стёрт */
    uint32_t rd_addr;                    /* откуда читать поток дальше */
    uint32_t rd_left;                    /* сколько байт потока ещё во флеш-памяти */
    uint32_t rd_pos, rd_fill;            /* положение в окне и его заполнение */
} w;

/* CRC-32 как у zlib: полином 0xEDB88320, без таблицы — 8 сдвигов на байт */
uint32_t crc32_update(uint32_t crc, const uint8_t *p, uint32_t n)
{
    crc = ~crc;
    while (n--) {
        crc ^= *p++;
        for (int i = 0; i < 8; i++)
            crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
    }
    return ~crc;
}

/* Совпадает ли CRC-32 участка флеш-памяти с ожидаемым. Читает через буфер записи. */
static int flash_is(uint32_t addr, uint32_t n, uint32_t expect)
{
    uint32_t crc = 0u;
    while (n != 0u) {
        uint32_t k = n < WR_BLOCK ? n : WR_BLOCK;
        if (fl_read(addr, w.wr, k) != 0)
            return 0;
        crc = crc32_update(crc, w.wr, k);
        addr += k;
        n -= k;
    }
    return crc == expect;
}

Writing goes in blocks. An erase unit is erased when the first block enters it — so the same code writes to both pages and sectors. wr_open starts writing at an address, wr_flush flushes the block to flash memory:

static void wr_open(uint32_t addr)
{
    w.wr_addr = addr;
    w.erased_to = addr;
    w.wr_fill = 0u;
}

static int wr_flush(void)
{
    if (w.wr_fill == 0u)
        return 0;
    while (w.erased_to < w.wr_addr + w.wr_fill) {   /* блок входит в нестёртую единицу */
        if (fl_erase(w.erased_to) != 0)
            return -1;
        w.erased_to += fl_unit(w.erased_to);
    }
    if (fl_write(w.wr_addr, w.wr, w.wr_fill) != 0)
        return -1;
    w.wr_addr += w.wr_fill;
    w.wr_fill = 0u;
    return 0;
}

The command stream is read from the delta area through the window, byte by byte; rd_open sets the window to the start of the stream:

static void rd_open(uint32_t addr, uint32_t n)
{
    w.rd_addr = addr;
    w.rd_left = n;
    w.rd_pos = 0u;
    w.rd_fill = 0u;
}

static int rd_byte(void)                 /* -1 — поток кончился или сбой чтения */
{
    if (w.rd_pos == w.rd_fill) {
        uint32_t k = w.rd_left < RD_BLOCK ? w.rd_left : RD_BLOCK;
        if (k == 0u || fl_read(w.rd_addr, w.rd, k) != 0)
            return -1;
        w.rd_addr += k;
        w.rd_left -= k;
        w.rd_pos = 0u;
        w.rd_fill = k;
    }
    return w.rd[w.rd_pos++];
}

static int rd_number(uint32_t *v)        /* число переменной длины: 7 бит в байте, не больше 32 бит */
{
    uint32_t x = 0u;
    for (unsigned shift = 0u; shift < 35u; shift += 7u) {
        int b = rd_byte();
        if (b < 0 || (shift == 28u && (b & 0xF0)))
            return -1;
        x |= (uint32_t)(b & 0x7F) << shift;
        if (!(b & 0x80)) {
            *v = x;
            return 0;
        }
    }
    return -1;
}

Checking the delta and the patching itself. The device does not trust the delta: every command is checked for running past the old image and past the declared length of the new one.

int delta_check(const delta_map_t *m, delta_hdr_t *h)
{
    if (fl_read(m->delta, (uint8_t *)h, DELTA_HDR) != 0)
        return -1;
    if (h->magic != DELTA_MAGIC)
        return -1;
    if (crc32_update(0u, (const uint8_t *)h, DELTA_HDR - 4u) != h->hdr_crc)
        return -1;
    if (h->old_size > m->slot_size || h->new_size == 0u || h->new_size > m->slot_size)
        return -1;
    if (h->body_size > m->delta_size - DELTA_HDR)
        return -1;
    return flash_is(m->delta + DELTA_HDR, h->body_size, h->body_crc) ? 0 : -1;
}

/* ── наложение: слот 0 + разница -> слот 1 ─────────────────────────────── */
int delta_apply(const delta_map_t *m, const delta_hdr_t *h)
{
    uint32_t cur = 0u;                   /* указатель в старом образе */
    uint32_t out = 0u;                   /* сколько байт нового образа уже выдано */

    rd_open(m->delta + DELTA_HDR, h->body_size);
    wr_open(m->slot1);
    while (w.rd_left != 0u || w.rd_pos != w.rd_fill) {
        uint32_t head, n, z;
        if (rd_number(&head) != 0)
            return -1;
        n = head >> 1;
        if (n == 0u || n > h->new_size - out)
            return -1;                   /* пустая команда или результат длиннее заявленного */
        if (!(head & 1u)) {              /* COPY: сначала сдвиг указателя, со знаком */
            if (rd_number(&z) != 0)
                return -1;
            cur += (z & 1u) ? 0u - ((z >> 1) + 1u) : z >> 1;
            if (cur > h->old_size || n > h->old_size - cur)
                return -1;               /* за пределами старого образа */
        }
        out += n;
        while (n != 0u) {
            uint32_t k = WR_BLOCK - w.wr_fill;
            if (k > n)
                k = n;
            if (head & 1u) {             /* ADD: байты из потока */
                for (uint32_t i = 0u; i < k; i++) {
                    int b = rd_byte();
                    if (b < 0)
                        return -1;
                    w.wr[w.wr_fill + i] = (uint8_t)b;
                }
            } else if (fl_read(m->slot0 + cur, w.wr + w.wr_fill, k) != 0) {
                return -1;
            }
            cur += k;                    /* указатель идёт вперёд и на ADD, и на COPY */
            w.wr_fill += k;
            n -= k;
            if (w.wr_fill == WR_BLOCK && wr_flush() != 0)
                return -1;
        }
    }
    if (out != h->new_size)
        return -1;
    return wr_flush();
}

The pointer offset is computed in unsigned numbers: a negative offset is adding 0u - …, and a pointer that has gone "below zero" becomes huge and fails the cur > h->old_size check.

What it costs. An -Os build for Cortex-M3:

Bytes
patching code (delta48v.o, section .text) 1076
its RAM (.bss): buffers 256 + 64 and seven words 348
stack: main 8 + delta_boot 80 + delta_apply 56 + block write 8 + 24 176
the whole bootloader: startup, F103 port, patching, jump 2068

The stack frames come from -fstack-usage; the deepest chain is main, delta_boot, delta_apply, then either wr_flush and fl_write (8 + 24) or rd_number and rd_byte (16 + 16). The bootloader took 2068 bytes out of 8 KB. armclang builds the same code without warnings.

The port for F103 does the same things as in 11.5: the keys, PER and STRT for erasing, PG and writing by half-words. Reading can go straight through the address: flash memory is visible in the address space. A block of odd length is padded with 0xFF. The whole port:

int fl_read(uint32_t addr, uint8_t *buf, uint32_t n)
{
    const uint8_t *p = (const uint8_t *)(FLASH_BASE + addr);   /* флеш-память отображена в адреса */
    while (n--)
        *buf++ = *p++;
    return 0;
}

uint32_t fl_unit(uint32_t addr)
{
    (void)addr;
    return PAGE;
}

static void flash_wait(void)
{
    while (FLASH_SR & FLASH_SR_BSY) {
    }
}

static void flash_unlock(void)
{
    if (FLASH_CR & FLASH_CR_LOCK) {
        FLASH_KEYR = FLASH_KEY1;
        FLASH_KEYR = FLASH_KEY2;
    }
}

int fl_erase(uint32_t addr)
{
    flash_unlock();
    flash_wait();
    FLASH_CR |= FLASH_CR_PER;            /* режим стирания страницы */
    FLASH_AR = FLASH_BASE + addr;
    FLASH_CR |= FLASH_CR_STRT;
    flash_wait();
    FLASH_CR &= ~FLASH_CR_PER;
    FLASH_CR |= FLASH_CR_LOCK;
    return (FLASH_SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) ? -1 : 0;
}

int fl_write(uint32_t addr, const uint8_t *buf, uint32_t n)
{
    int bad = 0;
    flash_unlock();
    flash_wait();
    FLASH_CR |= FLASH_CR_PG;
    for (uint32_t i = 0u; i < n && !bad; i += 2u) {   /* F1 пишет полусловами; нечётный хвост — 0xFF */
        uint16_t hw = (uint16_t)(buf[i] | ((i + 1u < n ? buf[i + 1u] : 0xFFu) << 8));
        *(volatile uint16_t *)(FLASH_BASE + addr + i) = hw;
        flash_wait();
        bad = (FLASH_SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) != 0u;
    }
    FLASH_CR &= ~FLASH_CR_PG;
    FLASH_CR |= FLASH_CR_LOCK;
    return bad ? -1 : 0;
}

The device code was tested not on the chip but on a flash memory model: the same delta48v.c is built on a PC, and the four port functions work with an array. The model is strict: you may write only to erased cells, erase only from the start of a unit, and any write outside the two slots and the delta area counts as a violation. Violations in the runs: 0. The result of every patching is compared with the new image byte by byte, and the delta itself is parsed independently by a second decoder in Python (undelta48v.py), written separately from the generator:

What is wrong Device's answer
delta from another firmware DB_FOREIGN
a bit corrupted in the stream or in the header, stream cut short, no DLV1 DB_NONE
new image larger than the slot, stream longer than the area DB_NONE
COPY before the start or past the end of the old image DB_FAILED
result longer or shorter than declared, a zero-length command DB_FAILED
a number longer than 5 bytes or above 32 bits, ADD promises more bytes than there are DB_FAILED
the stream is valid, but the result CRC in the header is different DB_FAILED

The last case is a property of the scheme: the device decides "already updated" by the CRC of the first new_size bytes of slot 0.


48в.4 Power loss: the decision at startup

Erasing and writing take seconds, and power can fail during any operation. There is one rule: slot 0 is not touched until slot 1 holds a new image with a correct CRC. There is no separate journal — the state is visible from three CRCs, and the bootloader decides anew at every startup:

What the bootloader sees What it does
no delta, or the header or stream CRC does not match runs slot 0 (DB_NONE)
slot 0 CRC equals the new image CRC erases the delta header (DB_DONE)
slot 1 CRC equals the new image CRC copies slot 1 to slot 0, checks, erases the header
slot 0 CRC equals the old image CRC applies the delta into slot 1, checks, then copies
nothing matched erases the header, leaves slot 0 alone (DB_FOREIGN)
static int flash_same(uint32_t a, uint32_t b, uint32_t n)   /* одинаковы ли n байт по двум адресам */
{
    while (n != 0u) {
        uint32_t k = n < RD_BLOCK ? n : RD_BLOCK;
        if (fl_read(a, w.rd, k) != 0 || fl_read(b, w.wr, k) != 0)
            return 0;
        for (uint32_t i = 0u; i < k; i++) {
            if (w.rd[i] != w.wr[i])
                return 0;
        }
        a += k;
        b += k;
        n -= k;
    }
    return 1;
}

/* По единицам стирания. Единица, где уже лежит то же самое, не трогается: меньше стираний,
 * а после обрыва питания копирование само продолжается с того места, где оборвалось. */
static int slot_copy(uint32_t from, uint32_t to, uint32_t n)
{
    while (n != 0u) {
        uint32_t unit = fl_unit(to);
        uint32_t len = n < unit ? n : unit;
        if (!flash_same(from, to, len)) {
            wr_open(to);
            for (uint32_t done = 0u; done < len; done += WR_BLOCK) {
                uint32_t k = len - done < WR_BLOCK ? len - done : WR_BLOCK;
                if (fl_read(from + done, w.wr, k) != 0)
                    return -1;
                w.wr_fill = k;
                if (wr_flush() != 0)
                    return -1;
            }
        }
        from += len;
        to += len;
        n -= len;
    }
    return 0;
}

/* ── решение при старте: вызывает загрузчик до запуска приложения ──────── */
int delta_boot(const delta_map_t *m)
{
    delta_hdr_t h;

    if (delta_check(m, &h) != 0)
        return DB_NONE;
    if (!flash_is(m->slot0, h.new_size, h.new_crc)) {       /* слот 0 ещё не новый */
        if (!flash_is(m->slot1, h.new_size, h.new_crc)) {   /* и слот 1 ещё не готов */
            if (!flash_is(m->slot0, h.old_size, h.old_crc)) {
                fl_erase(m->delta);                         /* разница не к этой прошивке */
                return DB_FOREIGN;
            }
            if (delta_apply(m, &h) != 0 || !flash_is(m->slot1, h.new_size, h.new_crc)) {
                fl_erase(m->delta);
                return DB_FAILED;
            }
        }
        /* В слоте 1 — проверенный новый образ. С этого места назад дороги нет, но и риска
         * нет: оборвётся копирование — следующий старт придёт сюда же и повторит его. */
        if (slot_copy(m->slot1, m->slot0, h.new_size) != 0 || !flash_is(m->slot0, h.new_size, h.new_crc))
            return DB_RETRY;
    }
    fl_erase(m->delta);                  /* разница больше не нужна */
    return DB_DONE;
}

flash_same compares two areas through the read window and the write buffer — it needs no memory of its own.

A power loss during patching leaves garbage in slot 1, but slot 0 and the delta are intact: the next startup hits the fourth row of the table and patches again. A power loss during copying leaves slot 0 half new — but slot 1 is intact, and the next startup hits the third row. Pages that already match are skipped by slot_copy, so copying continues from where it was interrupted. With DB_RETRY slot 0 must not be run: it may hold half an image, so the bootloader waits for a reset and tries again.

This is how it looks in the F103 bootloader. Before the jump it checks the stack top and the entry point of slot 0; transfer of control is as in 48.5:

static void jump_to_app(uint32_t slot)
{
    uint32_t sp = ((const uint32_t *)slot)[0];
    uint32_t pc = ((const uint32_t *)slot)[1];

    __asm volatile("cpsid i");
    for (unsigned i = 0u; i < 8u; i++) {
        NVIC_ICER(i) = 0xFFFFFFFFu;
        NVIC_ICPR(i) = 0xFFFFFFFFu;
    }
    SYST_CSR = 0u;
    SYST_RVR = 0u;
    SYST_CVR = 0u;
    SCB_VTOR = slot;
    __asm volatile("dsb\n isb");
    __asm volatile("msr msp, %0\n cpsie i\n bx %1" : : "r"(sp), "r"(pc));
    for (;;) {
    }
}

int main(void)
{
    int rc = delta_boot(&map);           /* принятая разница накладывается до запуска приложения */
    uint32_t slot = FLASH_BASE + map.slot0;
    uint32_t sp = ((const uint32_t *)slot)[0];
    uint32_t pc = ((const uint32_t *)slot)[1];

    if (rc != DB_RETRY && sp >= RAM_BASE && sp <= RAM_BASE + RAM_SIZE
        && (pc & 1u) && pc >= slot && pc < slot + map.slot_size)
        jump_to_app(slot);
    for (;;) {                           /* запускать нечего: ждать сторожа или программатора */
    }
}

The power-loss model. Power "fails" on a given erase or write operation by count, and that operation is done only halfway: the erased page is left with garbage, the block being written is incomplete, and the byte following it is corrupted. Right after the power loss the model checks that a whole copy of the old or the new image exists somewhere, then "reboots" — calls delta_boot() again — and compares slot 0 with the expected one.

Set Power-loss points Outcome
six firmware pairs, three layouts: power loss at every operation 3376 slot 0 built correctly in all
the same pairs: two power losses in a row at random places 3600 in all
3000 random pairs: power loss at every operation 80059 in all
3000 random pairs: two power losses in a row 9000 in all
2000 corrupted streams: power loss at every operation 18748 slot 0 old or new, as without power loss

Cases where no whole copy was left after a power loss: 0. Pair (b) on F103 is 241 operations: 24 erases and 96 block writes of up to 256 bytes for each slot, plus erasing the delta header.

How many pages are touched. Slot 1 is rewritten entirely, slot 0 only where the image changed:

Pair F103, 1 KB pages: slot 1 / slot 0 F103, 2 KB pages F407, sectors
(a) one constant 24 / 1 12 / 1 1 / 1
(b) function at the start 24 / 24 12 / 12 1 / 2
(c) function at the end 24 / 2 12 / 2 1 / 2

1 KB pages are on low- and medium-density F103, 2 KB pages on high- and XL-density (RM0008). On F407 the erase unit is a sector (RM0090): in the model slot 0 is sectors 2 and 3 of 16 KB, slot 1 is sector 4 of 64 KB, the delta is sector 1. On 128 KB sectors changing one byte costs erasing the whole sector — the time and endurance discussed in 48б.5.

Time. According to the F103 datasheet a page is erased in 20 to 40 ms, a half-word is programmed in 40 to 70 µs. Pair (b): 49 erases and 24516 half-words — from 1.96 to 3.68 s. Pair (a): 26 erases and 12754 half-words — from 1.03 to 1.93 s. The time to compute the CRCs is not included. Compared with hours on air this is nothing, but the bootloader's watchdog must survive these seconds (48.12).

What the experiment does not do. Receiving the delta over the radio: in the model it already lies in the area. In a device the application writes it in chunks as they are received, and the delta takes effect only when both the header and the whole stream are in place: otherwise body_crc will not match. The experimental firmware has no image header from 48.3; in a product it stays, and for the delta it is simply the first bytes of the image. And there is no signature here: a CRC catches corruption, not substitution (48.4).

Finally, the built images were run. In QEMU the bootloader sits at 0x08000000, and slot 0 holds what came out of patching:

v1: UART «fw48v 1.4.0 blink=500»; светодиод: вкл выкл вкл выкл вкл выкл вкл выкл вкл выкл 
a: UART «fw48v 1.4.0 blink=1000»; светодиод: вкл выкл вкл выкл вкл выкл вкл выкл вкл выкл 
a2: UART «fw48v 1.4.0 blink=250»; светодиод: вкл выкл вкл выкл вкл выкл вкл выкл вкл выкл 
b: UART «fw48v 1.4.0 blink=500»; светодиод: выкл вкл выкл выкл выкл выкл выкл вкл выкл вкл 
c: UART «fw48v 1.4.0 blink=500»; светодиод: выкл вкл выкл выкл выкл выкл выкл вкл выкл вкл 

The bootloader found no delta, checked the stack top and the entry point and transferred control. The LED state is the writes to GPIOC_BSRR, which QEMU records in its log.

The patching itself does not run in QEMU: this F1 model has no flash memory controller, and the emulator only logs writes to its registers. But these log entries show what the port does. With the old image in slot 0 and delta (b) in its area the bootloader wrote the PER bit to FLASH_CR 25 times and the PG bit 96 times, and to FLASH_AR the page addresses from 0x0800f000 to 0x08014c00 and finally 0x0801c000: 24 pages of slot 1, 96 blocks and the delta header. The memory did not change in the process, the slot 1 CRC did not match, and the bootloader ran the old image — the LED blinks evenly.


48в.5 Why a delta can be large: three builds of the same firmware

The experimental firmware is an ordinary application for F103: a vector table, a SysTick interrupt, UART, seven commands with a table of pointers to functions and strings, vsnprintf with floating point from newlib-nano. Version 1 is 24484 bytes. Six second versions were built from it:

Pair What was changed New image Bytes that differ Delta COPY / ADD
(a) constant 500 → 1000 24484 2 46 3 / 2
(a′) constant 500 → 250 24476 22178 1034 141 / 138
(b) new function at the start of the image 24516 22867 706 62 / 56
(c) the same function at the end of the image 24516 36 77 2 / 2
(c′) function at the end, but the call is added rather than replaced 24516 6140 476 91 / 90
(d) nothing; the image is built for the slot 1 address 24484 284 727 54 / 53

The "bytes that differ" column compares the images position by position: this is how a person looking at the files sees the change. In pairs (a′) and (b) almost the whole image "changed", although the source differs by one line.

(a) One constant. The LED period BLINK_MS appears in two instructions, and one byte changes in each. The delta is 14 bytes of stream (48в.2).

(b) A function at the start. A 32-byte function led_pattern was added, and the call to led_toggle() in main was replaced with a call to the new function:

#ifdef NEWFN
/* Новая функция версии 2: вместо ровного мигания — две вспышки и пауза. Своих переменных у неё
 * нет: фаза берётся из счётчика миллисекунд. Куда она ляжет, решает имя секции. */
#if NEWFN == 1
__attribute__((noinline, section(".text.first")))
#else
__attribute__((noinline, section(".tail.last")))
#endif
void led_pattern(void)
{
    uint32_t phase = (ticks / BLINK_MS) & 7u;
    led_set(phase == 0u || phase == 2u);
}
#endif

The function landed right after the vector table, at 0x08002040, and everything else moved by 32 bytes: main moved from 0x080022fc to 0x0800231c. The shift by itself does no harm to the delta: COPY simply takes the same bytes from another place. Calls between the shifted functions do not change either — the bl instruction holds a distance, not an address. What changes are the absolute addresses. The image has 152 changed words; 151 of them are an address inside the image that grew by exactly 32:

Where the addresses are Words
vector table 9
literal pools between functions 105
command table: 7 rows of 3 pointers 21
initial values of .data 16

The last, 152nd, word is the bl instruction itself in main. The addresses are scattered across the whole image, and each island of them is a separate ADD and COPY: hence 56 and 62 commands and 706 bytes instead of 46.

(c) The same function at the end. The source is the same, only the section name changed. The linker script knows two places for the new function:

  .text :
  {
    *(.text.first)                       /* вариант (б): новая функция в начале */
    *(.text .text.*)
    _srodata = .;
    *(.rodata .rodata.*)
    . = ALIGN(4);
  } > FLASH

  .data :
  {
    _sdata = .;
    *(.data .data.*)
    . = ALIGN(4);
    _edata = .;
  } > RAM AT > FLASH
  _sidata = LOADADDR(.data);

  .tail :
  {
    *(.tail.last)                        /* вариант (в): новая функция в конце образа */
    . = ALIGN(4);
  } > FLASH

The .tail section lies after everything, even after the initial values of .data: the function ended up at 0x08007fa4, and not a single old byte moved. In the image the 4 bytes of the bl instruction changed and the 32 bytes of the function were added — a delta of 77 bytes, nine times smaller than in (b). Both builds work the same way: in QEMU they show the same LED pattern (48в.4).

The experiment is clean because main did not grow: one call was replaced by another. In pair (c′) the call to the new function is added next to the old one. main grew from 280 to 284 bytes, the code after it moved by 4 bytes, and position by position 6140 bytes now differ. Not the whole image moved: its size stayed 24516 bytes — the alignment of one of the following sections absorbed those 4 bytes. The delta is 476 bytes instead of 77: a new function at the end of the image does not help if the code at its start grows. Hence the layout rule: what changes often is kept at the end of the image, and what rarely changes — libraries, drivers — at the start.

(a′) A constant that changed an instruction length. 250 instead of 500 — and the delta is 1034 bytes instead of 46. The number 500 in a compare instruction needs a long encoding, 250 fits into a short one:

v1: 80020dc: mov.w r1, #500;8002362: cmp.w r3, #500;
v2a: 80020dc: mov.w r1, #1000;8002362: cmp.w r3, #1000;
v2a2: 80020dc: movs r1, #250;800235e: cmp r3, #249;

Two instructions became shorter, the second of them already sits 4 bytes earlier, and the image became 8 bytes shorter — and everything further on moved, as in (b).

(d) The same source for a different address. The scheme from 48.2, "two images per release" — an image for slot 0 and an image for slot 1 — is bad for a delta update. Version 1 built for 0x0800F000 differs from itself built for 0x08002000 by the same 151 words: all absolute addresses grew by 0xD000. The delta is 727 bytes with zero changes in the source. That is why here the image is always built for slot 0, and slot 1 is only the place where it is assembled.


48в.6 Patching in place: the dangers and what it takes

The second slot is half of the flash memory. It is tempting to write the new image straight over the old one, page by page. There are three dangers.

Old bytes disappear along the way. A new page is assembled in RAM, then the slot page is erased and written. By this time part of the old image has already been overwritten by the new one, and COPY may take only what is still intact. When pages are walked from the bottom up — old bytes not below the start of the current page; from the top down — not above its end. Whatever cannot be taken has to be sent as new bytes, and the delta grows. The walk order is chosen by the generator:

Pair Two slots In place, bottom up In place, top down Pages changed
(a) 46 46 46 1 of 24
(a′) 1034 1118 1312 24 of 24
(b) 706 1498 824 24 of 24
(c) 77 83 85 2 of 24

In pair (b) everything moved up by 32 bytes. When walking bottom up, the first 32 bytes of every page have to be taken from the previous one, already overwritten — the delta doubled. When walking top down, the same bytes are still intact.

A page that was erased and not written is lost. It is no longer in the slot, and it was in RAM only until the power loss. You need a spare page and a journal: the new page is first written to the spare one, a mark goes into the journal, and only then is the slot page erased. After a power loss the journal tells which page to finish from the spare one and where to continue. This is the same technique as the swap through scratch in MCUboot (48б.5), with the same price: the spare page is erased as many times as pages changed. The endurance of an F103 page is 10 thousand cycles; with 24 changed pages one spare page is enough for 416 updates if it is not rotated with others.

There is no way back. From the first erased page the old image no longer exists. The result can be checked by CRC only at the end, when there is nothing left to fix it with. Everything that can be checked is checked before starting: the CRC of the delta and the CRC of the old image. And the delta must survive to the end: the area holding it must not be touched.

A Python model (inplace48v.py, the section has no device code for it) assembles the image in place with a spare page and a journal and cuts the power at every operation, and then once more at a random one:

Pair Operations Erases: slot + spare Built correctly
(a) 6 1 + 1 6 of 6
(a′) 144 24 + 24 144 of 144
(b) 144 24 + 24 144 of 144
(c) 12 2 + 2 12 of 12

The gain is flash memory: one page and a journal instead of a second slot. And fewer pages are erased: in pair (a), one in the slot instead of 24 in slot 1. The loss is complexity, wear of the spare page, and the fact that the device must take the update to the end: with two slots it stays on the old version at any moment before the copying.


48в.7 Pitfalls

The delta grew more than twenty times from editing one constant. An instruction length or a function size changed, and everything further on moved: pair (a′), 1034 bytes instead of 46. Check the delta size at every release, before sending it.

A small edit at the start of the image — a delta of hundreds of bytes. A shift changes all absolute addresses: 151 words in pair (b). And 4 extra bytes in main cost 476 bytes of delta — pair (c′). Frequently changed code goes to the end of the image (a section like .tail), stable code to the start.

The device answers DB_FOREIGN. The delta was computed from another build. The same source is not enough: version 1 built with -O2 instead of -Os is 24604 bytes instead of 24484, a different image with a different CRC. Keep the binary image of every release, like the ELF in 35в.5.

The delta is computed between images for different slots. 727 bytes out of nothing — pair (d). Always build the image for one address.

The CRC matched — and nothing else was checked. The delta carries only the CRC of the new image, and it catches corruption, not substitution. The signature from 48а and the checks from 48.4 are done by the bootloader on the result — in slot 1, before copying into slot 0. The section's code has no signature, and the stack top and the entry point are checked before the jump.

Writing to an unerased page. After the previous update slot 1 holds the previous image. Erase every unit before its first block; on F1 pad a block of odd length to a half-word.

The watchdog cuts the patching short. Pair (b) on F103 is up to 3.68 s of erasing and writing, without computing the CRCs. Reload the watchdog between blocks or give it a longer timeout (48.12).

The delta does not fit into the area. The device rejects it: body_size larger than the area gives DB_NONE. The generator must know the area size and in that case send the full image the usual way.

The write block is not a divisor of the erase unit. wr_flush erases a unit when a block enters it. Slot boundaries must lie on unit boundaries, and the 256-byte block must fit into a page a whole number of times.

In place — and the power failed. Without a spare page and a journal the device is left with an image in which one page is neither old nor new. No room for a spare page — do not patch in place.


48в.8 Summary

Full sources

delta48v.h — the delta format, the layout and the flash memory port.

/* Раздел 48в: наложение разницы на приборе — формат, раскладка, порт флеш-памяти. */
#ifndef DELTA48V_H
#define DELTA48V_H
#include <stdint.h>

#define DELTA_MAGIC  0x31564C44u         /* в памяти — байты «DLV1» */
#define DELTA_HDR    32u

typedef struct {                         /* заголовок разницы: 8 слов, младшим байтом вперёд */
    uint32_t magic;
    uint32_t old_size;                   /* длина старого образа */
    uint32_t old_crc;                    /* его CRC-32: не тот исходник — отказ */
    uint32_t new_size;                   /* длина нового образа */
    uint32_t new_crc;                    /* CRC-32 результата */
    uint32_t body_size;                  /* длина потока команд за заголовком */
    uint32_t body_crc;                   /* CRC-32 потока */
    uint32_t hdr_crc;                    /* CRC-32 первых 28 байт заголовка */
} delta_hdr_t;

_Static_assert(sizeof(delta_hdr_t) == DELTA_HDR, "delta header must be 32 bytes");

typedef struct {                         /* раскладка: смещения от начала флеш-памяти */
    uint32_t slot0;                      /* рабочий слот: отсюда запускается приложение */
    uint32_t slot1;                      /* второй слот: здесь собирается новый образ */
    uint32_t slot_size;
    uint32_t delta;                      /* область, куда приложение приняло разницу */
    uint32_t delta_size;
} delta_map_t;

enum {
    DB_NONE = 0,                         /* разницы нет или она повреждена: запускать слот 0 */
    DB_DONE,                             /* слот 0 — новая версия, разница убрана */
    DB_FOREIGN,                          /* разница не к этой прошивке: слот 0 не тронут */
    DB_FAILED,                           /* поток команд неверен или результат не сошёлся: слот 0 не тронут */
    DB_RETRY                             /* сбой записи при копировании: слот 1 цел, повторить при следующем старте */
};

/* Порт: четыре функции над флеш-памятью. Все возвращают 0 при успехе. */
int      fl_read(uint32_t addr, uint8_t *buf, uint32_t n);
int      fl_erase(uint32_t addr);        /* стереть единицу стирания, начинающуюся с addr */
int      fl_write(uint32_t addr, const uint8_t *buf, uint32_t n);
uint32_t fl_unit(uint32_t addr);         /* размер единицы стирания по этому адресу */

uint32_t crc32_update(uint32_t crc, const uint8_t *p, uint32_t n);
int      delta_check(const delta_map_t *m, delta_hdr_t *h);
int      delta_apply(const delta_map_t *m, const delta_hdr_t *h);
int      delta_boot(const delta_map_t *m);

#endif

delta48v.c — applying the delta on the device without malloc.

/* Раздел 48в: наложение разницы на приборе. Без malloc: два буфера и семь слов состояния
 * в одной статической структуре. Старый образ читается из слота 0, новый собирается в слоте 1,
 * проверяется по CRC-32 и только потом копируется в слот 0. Порядок байт — младшим вперёд
 * (Cortex-M); заголовок читается прямо в структуру. */
#include "delta48v.h"

#define WR_BLOCK 256u                    /* блок записи: страница и сектор кратны ему */
#define RD_BLOCK 64u                     /* окно чтения потока команд */

static struct {
    uint8_t  wr[WR_BLOCK];               /* блок, который ляжет во флеш-память */
    uint8_t  rd[RD_BLOCK];               /* окно потока команд */
    uint32_t wr_addr;                    /* куда ляжет блок */
    uint32_t wr_fill;                    /* сколько в нём байт */
    uint32_t erased_to;                  /* до этого адреса слот уже стёрт */
    uint32_t rd_addr;                    /* откуда читать поток дальше */
    uint32_t rd_left;                    /* сколько байт потока ещё во флеш-памяти */
    uint32_t rd_pos, rd_fill;            /* положение в окне и его заполнение */
} w;

/* CRC-32 как у zlib: полином 0xEDB88320, без таблицы — 8 сдвигов на байт */
uint32_t crc32_update(uint32_t crc, const uint8_t *p, uint32_t n)
{
    crc = ~crc;
    while (n--) {
        crc ^= *p++;
        for (int i = 0; i < 8; i++)
            crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
    }
    return ~crc;
}

/* Совпадает ли CRC-32 участка флеш-памяти с ожидаемым. Читает через буфер записи. */
static int flash_is(uint32_t addr, uint32_t n, uint32_t expect)
{
    uint32_t crc = 0u;
    while (n != 0u) {
        uint32_t k = n < WR_BLOCK ? n : WR_BLOCK;
        if (fl_read(addr, w.wr, k) != 0)
            return 0;
        crc = crc32_update(crc, w.wr, k);
        addr += k;
        n -= k;
    }
    return crc == expect;
}

/* ── запись блоками ────────────────────────────────────────────────────── */
static void wr_open(uint32_t addr)
{
    w.wr_addr = addr;
    w.erased_to = addr;
    w.wr_fill = 0u;
}

static int wr_flush(void)
{
    if (w.wr_fill == 0u)
        return 0;
    while (w.erased_to < w.wr_addr + w.wr_fill) {   /* блок входит в нестёртую единицу */
        if (fl_erase(w.erased_to) != 0)
            return -1;
        w.erased_to += fl_unit(w.erased_to);
    }
    if (fl_write(w.wr_addr, w.wr, w.wr_fill) != 0)
        return -1;
    w.wr_addr += w.wr_fill;
    w.wr_fill = 0u;
    return 0;
}

/* ── чтение потока команд ──────────────────────────────────────────────── */
static void rd_open(uint32_t addr, uint32_t n)
{
    w.rd_addr = addr;
    w.rd_left = n;
    w.rd_pos = 0u;
    w.rd_fill = 0u;
}

static int rd_byte(void)                 /* -1 — поток кончился или сбой чтения */
{
    if (w.rd_pos == w.rd_fill) {
        uint32_t k = w.rd_left < RD_BLOCK ? w.rd_left : RD_BLOCK;
        if (k == 0u || fl_read(w.rd_addr, w.rd, k) != 0)
            return -1;
        w.rd_addr += k;
        w.rd_left -= k;
        w.rd_pos = 0u;
        w.rd_fill = k;
    }
    return w.rd[w.rd_pos++];
}

static int rd_number(uint32_t *v)        /* число переменной длины: 7 бит в байте, не больше 32 бит */
{
    uint32_t x = 0u;
    for (unsigned shift = 0u; shift < 35u; shift += 7u) {
        int b = rd_byte();
        if (b < 0 || (shift == 28u && (b & 0xF0)))
            return -1;
        x |= (uint32_t)(b & 0x7F) << shift;
        if (!(b & 0x80)) {
            *v = x;
            return 0;
        }
    }
    return -1;
}

/* ── проверка разницы: заголовок и CRC потока ──────────────────────────── */
int delta_check(const delta_map_t *m, delta_hdr_t *h)
{
    if (fl_read(m->delta, (uint8_t *)h, DELTA_HDR) != 0)
        return -1;
    if (h->magic != DELTA_MAGIC)
        return -1;
    if (crc32_update(0u, (const uint8_t *)h, DELTA_HDR - 4u) != h->hdr_crc)
        return -1;
    if (h->old_size > m->slot_size || h->new_size == 0u || h->new_size > m->slot_size)
        return -1;
    if (h->body_size > m->delta_size - DELTA_HDR)
        return -1;
    return flash_is(m->delta + DELTA_HDR, h->body_size, h->body_crc) ? 0 : -1;
}

/* ── наложение: слот 0 + разница -> слот 1 ─────────────────────────────── */
int delta_apply(const delta_map_t *m, const delta_hdr_t *h)
{
    uint32_t cur = 0u;                   /* указатель в старом образе */
    uint32_t out = 0u;                   /* сколько байт нового образа уже выдано */

    rd_open(m->delta + DELTA_HDR, h->body_size);
    wr_open(m->slot1);
    while (w.rd_left != 0u || w.rd_pos != w.rd_fill) {
        uint32_t head, n, z;
        if (rd_number(&head) != 0)
            return -1;
        n = head >> 1;
        if (n == 0u || n > h->new_size - out)
            return -1;                   /* пустая команда или результат длиннее заявленного */
        if (!(head & 1u)) {              /* COPY: сначала сдвиг указателя, со знаком */
            if (rd_number(&z) != 0)
                return -1;
            cur += (z & 1u) ? 0u - ((z >> 1) + 1u) : z >> 1;
            if (cur > h->old_size || n > h->old_size - cur)
                return -1;               /* за пределами старого образа */
        }
        out += n;
        while (n != 0u) {
            uint32_t k = WR_BLOCK - w.wr_fill;
            if (k > n)
                k = n;
            if (head & 1u) {             /* ADD: байты из потока */
                for (uint32_t i = 0u; i < k; i++) {
                    int b = rd_byte();
                    if (b < 0)
                        return -1;
                    w.wr[w.wr_fill + i] = (uint8_t)b;
                }
            } else if (fl_read(m->slot0 + cur, w.wr + w.wr_fill, k) != 0) {
                return -1;
            }
            cur += k;                    /* указатель идёт вперёд и на ADD, и на COPY */
            w.wr_fill += k;
            n -= k;
            if (w.wr_fill == WR_BLOCK && wr_flush() != 0)
                return -1;
        }
    }
    if (out != h->new_size)
        return -1;
    return wr_flush();
}

/* ── копирование слота 1 в слот 0 ──────────────────────────────────────── */
static int flash_same(uint32_t a, uint32_t b, uint32_t n)   /* одинаковы ли n байт по двум адресам */
{
    while (n != 0u) {
        uint32_t k = n < RD_BLOCK ? n : RD_BLOCK;
        if (fl_read(a, w.rd, k) != 0 || fl_read(b, w.wr, k) != 0)
            return 0;
        for (uint32_t i = 0u; i < k; i++) {
            if (w.rd[i] != w.wr[i])
                return 0;
        }
        a += k;
        b += k;
        n -= k;
    }
    return 1;
}

/* По единицам стирания. Единица, где уже лежит то же самое, не трогается: меньше стираний,
 * а после обрыва питания копирование само продолжается с того места, где оборвалось. */
static int slot_copy(uint32_t from, uint32_t to, uint32_t n)
{
    while (n != 0u) {
        uint32_t unit = fl_unit(to);
        uint32_t len = n < unit ? n : unit;
        if (!flash_same(from, to, len)) {
            wr_open(to);
            for (uint32_t done = 0u; done < len; done += WR_BLOCK) {
                uint32_t k = len - done < WR_BLOCK ? len - done : WR_BLOCK;
                if (fl_read(from + done, w.wr, k) != 0)
                    return -1;
                w.wr_fill = k;
                if (wr_flush() != 0)
                    return -1;
            }
        }
        from += len;
        to += len;
        n -= len;
    }
    return 0;
}

/* ── решение при старте: вызывает загрузчик до запуска приложения ──────── */
int delta_boot(const delta_map_t *m)
{
    delta_hdr_t h;

    if (delta_check(m, &h) != 0)
        return DB_NONE;
    if (!flash_is(m->slot0, h.new_size, h.new_crc)) {       /* слот 0 ещё не новый */
        if (!flash_is(m->slot1, h.new_size, h.new_crc)) {   /* и слот 1 ещё не готов */
            if (!flash_is(m->slot0, h.old_size, h.old_crc)) {
                fl_erase(m->delta);                         /* разница не к этой прошивке */
                return DB_FOREIGN;
            }
            if (delta_apply(m, &h) != 0 || !flash_is(m->slot1, h.new_size, h.new_crc)) {
                fl_erase(m->delta);
                return DB_FAILED;
            }
        }
        /* В слоте 1 — проверенный новый образ. С этого места назад дороги нет, но и риска
         * нет: оборвётся копирование — следующий старт придёт сюда же и повторит его. */
        if (slot_copy(m->slot1, m->slot0, h.new_size) != 0 || !flash_is(m->slot0, h.new_size, h.new_crc))
            return DB_RETRY;
    }
    fl_erase(m->delta);                  /* разница больше не нужна */
    return DB_DONE;
}

boot48v.c — the STM32F103 bootloader with delta patching.

/* Раздел 48в: загрузчик STM32F103 с наложением разницы. Порт флеш-памяти — по разделу 11.5,
 * передача управления — по 48.5. Раскладка 128 КБ: загрузчик 8 КБ, два слота по 52 КБ,
 * 16 КБ под разницу. */
#include "delta48v.h"
#include "regs48v.h"

#define FLASH_BASE 0x08000000u
#define PAGE       1024u                 /* страница F103 средней плотности */
#define RAM_BASE   0x20000000u
#define RAM_SIZE   0x5000u               /* 20 КБ у STM32F103CB */

static const delta_map_t map = {
    .slot0 = 0x2000u, .slot1 = 0xF000u, .slot_size = 0xD000u,
    .delta = 0x1C000u, .delta_size = 0x4000u,
};

/* ── порт флеш-памяти ──────────────────────────────────────────────────── */
int fl_read(uint32_t addr, uint8_t *buf, uint32_t n)
{
    const uint8_t *p = (const uint8_t *)(FLASH_BASE + addr);   /* флеш-память отображена в адреса */
    while (n--)
        *buf++ = *p++;
    return 0;
}

uint32_t fl_unit(uint32_t addr)
{
    (void)addr;
    return PAGE;
}

static void flash_wait(void)
{
    while (FLASH_SR & FLASH_SR_BSY) {
    }
}

static void flash_unlock(void)
{
    if (FLASH_CR & FLASH_CR_LOCK) {
        FLASH_KEYR = FLASH_KEY1;
        FLASH_KEYR = FLASH_KEY2;
    }
}

int fl_erase(uint32_t addr)
{
    flash_unlock();
    flash_wait();
    FLASH_CR |= FLASH_CR_PER;            /* режим стирания страницы */
    FLASH_AR = FLASH_BASE + addr;
    FLASH_CR |= FLASH_CR_STRT;
    flash_wait();
    FLASH_CR &= ~FLASH_CR_PER;
    FLASH_CR |= FLASH_CR_LOCK;
    return (FLASH_SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) ? -1 : 0;
}

int fl_write(uint32_t addr, const uint8_t *buf, uint32_t n)
{
    int bad = 0;
    flash_unlock();
    flash_wait();
    FLASH_CR |= FLASH_CR_PG;
    for (uint32_t i = 0u; i < n && !bad; i += 2u) {   /* F1 пишет полусловами; нечётный хвост — 0xFF */
        uint16_t hw = (uint16_t)(buf[i] | ((i + 1u < n ? buf[i + 1u] : 0xFFu) << 8));
        *(volatile uint16_t *)(FLASH_BASE + addr + i) = hw;
        flash_wait();
        bad = (FLASH_SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) != 0u;
    }
    FLASH_CR &= ~FLASH_CR_PG;
    FLASH_CR |= FLASH_CR_LOCK;
    return bad ? -1 : 0;
}

/* ── передача управления приложению ────────────────────────────────────── */
static void jump_to_app(uint32_t slot)
{
    uint32_t sp = ((const uint32_t *)slot)[0];
    uint32_t pc = ((const uint32_t *)slot)[1];

    __asm volatile("cpsid i");
    for (unsigned i = 0u; i < 8u; i++) {
        NVIC_ICER(i) = 0xFFFFFFFFu;
        NVIC_ICPR(i) = 0xFFFFFFFFu;
    }
    SYST_CSR = 0u;
    SYST_RVR = 0u;
    SYST_CVR = 0u;
    SCB_VTOR = slot;
    __asm volatile("dsb\n isb");
    __asm volatile("msr msp, %0\n cpsie i\n bx %1" : : "r"(sp), "r"(pc));
    for (;;) {
    }
}

int main(void)
{
    int rc = delta_boot(&map);           /* принятая разница накладывается до запуска приложения */
    uint32_t slot = FLASH_BASE + map.slot0;
    uint32_t sp = ((const uint32_t *)slot)[0];
    uint32_t pc = ((const uint32_t *)slot)[1];

    if (rc != DB_RETRY && sp >= RAM_BASE && sp <= RAM_BASE + RAM_SIZE
        && (pc & 1u) && pc >= slot && pc < slot + map.slot_size)
        jump_to_app(slot);
    for (;;) {                           /* запускать нечего: ждать сторожа или программатора */
    }
}

mkdelta48v.py — the generator of the delta between two images, on the computer.

# -*- coding: utf-8 -*-
"""Раздел 48в: генератор разницы двух образов прошивки (работает на компьютере).

python mkdelta48v.py old.bin new.bin out.dlt

Разница — заголовок 32 байта и поток команд двух видов: COPY «взять n байт из старого образа»
и ADD «вставить n новых байт». Совпадения ищутся по таблице всех блоков старого образа длиной
BLOCK байт; числа пишутся кодом переменной длины (7 бит в байте, старший бит — «есть ещё»)."""
import struct
import sys
import zlib

MAGIC = 0x31564C44          # в файле — байты «DLV1»
HDR_SIZE = 32
BLOCK = 8                   # длина блока для поиска совпадений
MIN_COPY = 4                # короче — выгоднее передать байты как новые
MAX_CAND = 64               # сколько мест старого образа помнить на один блок


def varint(v):
    """Число без знака: по 7 бит, младшие вперёд; старший бит байта — «есть продолжение»."""
    out = bytearray()
    while True:
        b = v & 0x7F
        v >>= 7
        if v:
            out.append(b | 0x80)
        else:
            out.append(b)
            return bytes(out)


def zigzag(d):
    """Число со знаком -> без знака: 0, -1, 1, -2, 2… -> 0, 1, 2, 3, 4…"""
    return d << 1 if d >= 0 else ((-d) << 1) - 1


def match_len(old, i, new, j):
    """Сколько байт подряд совпадает у old[i:] и new[j:]."""
    n = min(len(old) - i, len(new) - j)
    if i < 0 or n <= 0:
        return 0
    if old[i:i + n] == new[j:j + n]:
        return n
    lo, hi = 0, n                        # lo байт совпадает, hi — уже нет
    while hi - lo > 1:
        mid = (lo + hi) // 2
        if old[i:i + mid] == new[j:j + mid]:
            lo = mid
        else:
            hi = mid
    return lo


def find_ops(old, new, block=BLOCK):
    """Жадный разбор нового образа на команды ("copy", src, n) и ("add", bytes)."""
    index = {}
    for i in range(len(old) - block + 1):
        places = index.setdefault(old[i:i + block], [])
        if len(places) < MAX_CAND:
            places.append(i)
    ops = []
    j = 0                                # позиция в новом образе
    lit = 0                              # начало ещё не выданных новых байт
    cur = 0                              # указатель в старом образе — тот же, что ведёт прибор
    while j < len(new):
        guess = cur + (j - lit)          # «новые байты заменили столько же старых»
        best_n, best_src = match_len(old, guess, new, j), guess
        if best_n < MIN_COPY:
            best_n = 0
        for src in index.get(new[j:j + block], ()):
            n = match_len(old, src, new, j)
            if n > best_n or (n == best_n and abs(src - guess) < abs(best_src - guess)):
                best_n, best_src = n, src
        if best_n == 0:
            j += 1
            continue
        if j > lit:
            ops.append(("add", new[lit:j]))
            cur += j - lit
        ops.append(("copy", best_src, best_n))
        cur = best_src + best_n
        j += best_n
        lit = j
    if j > lit:
        ops.append(("add", new[lit:j]))
    return ops


def encode(ops, old, new):
    """Команды -> файл разницы: заголовок и поток."""
    body = bytearray()
    cur = 0
    for op in ops:
        if op[0] == "add":
            data = op[1]
            body += varint(len(data) << 1 | 1) + data
            cur += len(data)
        else:
            _, src, n = op
            body += varint(n << 1) + varint(zigzag(src - cur))
            cur = src + n
    hdr = struct.pack("<7I", MAGIC, len(old), zlib.crc32(old), len(new), zlib.crc32(new),
                      len(body), zlib.crc32(bytes(body)))
    hdr += struct.pack("<I", zlib.crc32(hdr))
    return hdr + bytes(body)


def make_delta(old, new, block=BLOCK):
    return encode(find_ops(old, new, block), old, new)


def stats(ops):
    """(число COPY, число ADD, байт под COPY, байт в ADD)."""
    copies = [op for op in ops if op[0] == "copy"]
    adds = [op for op in ops if op[0] == "add"]
    return len(copies), len(adds), sum(op[2] for op in copies), sum(len(op[1]) for op in adds)


if __name__ == "__main__":
    old = open(sys.argv[1], "rb").read()
    new = open(sys.argv[2], "rb").read()
    ops = find_ops(old, new)
    delta = encode(ops, old, new)
    open(sys.argv[3], "wb").write(delta)
    nc, na, bc, ba = stats(ops)
    print("old %d, new %d, delta %d bytes: %d COPY (%d bytes), %d ADD (%d bytes)"
          % (len(old), len(new), len(delta), nc, bc, na, ba))

inplace48v.py — patching "in place" — a Python model.

# -*- coding: utf-8 -*-
"""Раздел 48в: наложение «на месте» — модель на Python (кода для прибора здесь нет).

python3 inplace48v.py <каталог с v1.bin, v2a.bin…>

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

Обрыв питания: страница, стёртая, но не записанная, потеряна — в слоте её уже нет, в ОЗУ ещё
была. Модель держит одну запасную страницу и журнал: новая страница сначала пишется в запасную,
в журнал — отметка, и только потом стирается страница слота. После обрыва журнал говорит, какую
страницу доделать и с какой продолжить."""
import os
import random
import sys
import zlib

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import mkdelta48v as mk

PAGE = 1024


def page_ops(old, new, order, page=PAGE):
    """Команды по изменившимся страницам нового образа, в порядке обхода: [(номер страницы, [команды])]."""
    index = {}
    for i in range(len(old) - mk.BLOCK + 1):
        places = index.setdefault(old[i:i + mk.BLOCK], [])
        if len(places) < mk.MAX_CAND:
            places.append(i)
    n_pages = (len(new) + page - 1) // page
    pages = range(n_pages) if order == "up" else range(n_pages - 1, -1, -1)
    out = []
    cur = 0
    for k in pages:
        j, end = k * page, min((k + 1) * page, len(new))
        if old[j:j + page] == new[j:j + page]:
            continue                     # страница не изменилась: в разнице её нет, во флеш-памяти её не трогают
        lo, hi = (k * page, len(old)) if order == "up" else (0, min((k + 1) * page, len(old)))
        ops, lit = [], j
        while j < end:
            guess = cur + (j - lit)
            best_n, best_src = 0, 0
            for src in [guess] + index.get(new[j:j + mk.BLOCK], []):
                if src < lo:
                    continue
                n = min(mk.match_len(old, src, new, j), end - j, hi - src)
                if n >= mk.MIN_COPY and (n > best_n or (n == best_n and abs(src - guess) < abs(best_src - guess))):
                    best_n, best_src = n, src
            if best_n == 0:
                j += 1
                continue
            if j > lit:
                ops.append(("add", new[lit:j]))
                cur += j - lit
            ops.append(("copy", best_src, best_n))
            cur = best_src + best_n
            j += best_n
            lit = j
        if j > lit:
            ops.append(("add", new[lit:j]))
            cur += j - lit
        out.append((k, ops))
    return out


def size_of(pages, old, new):
    """Длина разницы в том же кодировании: заголовок 32 байта, поток команд и номер перед каждой страницей."""
    return len(mk.encode([op for _, ops in pages for op in ops], old, new)) + sum(len(mk.varint(k)) for k, _ in pages)


def build_page(mem, ops):
    """Собрать страницу в ОЗУ: COPY читает слот в его нынешнем состоянии."""
    buf = bytearray()
    for op in ops:
        buf += op[1] if op[0] == "add" else mem[op[1]:op[1] + op[2]]
    return bytes(buf)


class PowerCut(Exception):
    pass


class Flash:
    """Слот, запасная страница и журнал; обрыв питания на заданной по счёту операции."""

    def __init__(self, old, n_pages):
        self.slot = bytearray(old) + b"\xff" * (n_pages * PAGE - len(old))
        self.spare = bytearray(b"\xff" * PAGE)
        self.journal = []                # записи (вид, страница, crc); None — оборванная запись
        self.ops = 0
        self.cut_at = -1
        self.erases = {"slot": 0, "spare": 0}
        self.rng = random.Random(1)

    def step(self):
        self.ops += 1
        return self.ops - 1 == self.cut_at

    def erase(self, area, k=0):
        buf, off = (self.slot, k * PAGE) if area == "slot" else (self.spare, 0)
        if self.step():                  # недостёртая страница: мусор
            buf[off:off + PAGE] = bytes(b | self.rng.getrandbits(8) for b in buf[off:off + PAGE])
            raise PowerCut
        buf[off:off + PAGE] = b"\xff" * PAGE
        self.erases[area] += 1

    def write(self, area, data, k=0):
        buf, off = (self.slot, k * PAGE) if area == "slot" else (self.spare, 0)
        if self.step():                  # недописанная страница
            n = self.rng.randrange(len(data) + 1)
            buf[off:off + n] = data[:n]
            raise PowerCut
        buf[off:off + len(data)] = data

    def log(self, kind, k, crc=0):
        if self.step():
            self.journal.append(None)    # оборванная запись: контрольная сумма записи не сойдётся
            raise PowerCut
        self.journal.append((kind, k, crc))


def update(fl, pages):
    """Наложение на месте; после обрыва вызывается заново и продолжает по журналу."""
    done = {r[1] for r in fl.journal if r and r[0] == "done"}
    saved = [r for r in fl.journal if r and r[0] == "saved" and r[1] not in done]
    if saved:                            # страница лежит в запасной, в слот не дописана
        _, k, crc = saved[-1]
        data = bytes(fl.spare)
        assert zlib.crc32(data) == crc
        fl.erase("slot", k)
        fl.write("slot", data, k)
        fl.log("done", k)
        done.add(k)
    for k, ops in pages:
        if k in done:
            continue
        data = build_page(fl.slot, ops)
        data += b"\xff" * (PAGE - len(data))
        fl.erase("spare")
        fl.write("spare", data)
        fl.log("saved", k, zlib.crc32(data))
        fl.erase("slot", k)
        fl.write("slot", data, k)
        fl.log("done", k)


if __name__ == "__main__":
    fw = sys.argv[1]
    read = lambda name: open(os.path.join(fw, name), "rb").read()
    v1 = read("v1.bin")
    print("== наложение на месте: модель")
    for key, name in (("a", "v2a.bin"), ("a2", "v2a2.bin"), ("b", "v2b.bin"), ("c", "v2c.bin")):
        new = read(name)
        n_pages = (max(len(new), len(v1)) + PAGE - 1) // PAGE
        row = ["два слота %d" % len(mk.make_delta(v1, new))]
        best = None
        for order, title in (("up", "снизу вверх"), ("down", "сверху вниз")):
            pages = page_ops(v1, new, order)
            fl = Flash(v1, n_pages)
            update(fl, pages)
            good = bytes(fl.slot[:len(new)]) == new
            size = size_of(pages, v1, new)
            row.append("на месте %s %d%s" % (title, size, "" if good else " (НЕ СОШЛОСЬ)"))
            changed = len(pages)
            if best is None or size < best[0]:
                best = (size, order, pages, fl.ops, dict(fl.erases))
        print("пара %s: %s байт; изменившихся страниц %d из %d" % (key, ", ".join(row), changed, n_pages))
        size, order, pages, total, erases = best
        ok = 0
        for cut in range(total):         # обрыв на каждой операции, потом — ещё один на случайной
            fl = Flash(v1, n_pages)
            fl.cut_at = cut
            try:
                update(fl, pages)
            except PowerCut:
                pass
            fl.cut_at = fl.ops + fl.rng.randrange(1, 40)
            try:
                update(fl, pages)
            except PowerCut:
                fl.cut_at = -1
                update(fl, pages)
            ok += bytes(fl.slot[:len(new)]) == new
        print("  обход %s: операций %d, стираний страниц слота %d, запасной страницы %d; "
              "обрыв на каждой операции и второй следом: новый образ собран %d из %d"
              % ("снизу вверх" if order == "up" else "сверху вниз", total, erases["slot"], erases["spare"], ok, total))

app48v.c — the experimental firmware for STM32F103 with build variants.

/* Раздел 48в: опытная прошивка для STM32F103. В ней есть всё, из чего состоит обычный образ:
 * таблица векторов, обработчик прерывания, таблица команд с указателями на функции и строки,
 * библиотечный код (vsnprintf с плавающей точкой из newlib-nano, арифметика из libgcc).
 * Варианты сборки:
 *   без ключей       версия 1
 *   -DBLINK_MS=1000u (а) изменена одна константа; -DBLINK_MS=250u — она же, но команда короче
 *   -DNEWFN=1        (б) добавлена функция led_pattern, секция .text.first — сразу за векторами
 *   -DNEWFN=2        (в) та же функция, секция .tail.last — в самом конце образа
 *   -DNEWFN=3        (в′) как (в), но вызов в main не заменён, а добавлен */
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "regs48v.h"

#ifndef BLINK_MS
#define BLINK_MS 500u                    /* период светодиода, мс */
#endif

volatile uint32_t ticks;                 /* миллисекунды от старта */

void SysTick_Handler(void)
{
    ticks++;
}

/* ── UART ──────────────────────────────────────────────────────────────── */
static void uart_putc(char c)
{
    while (!(USART1_SR & USART_SR_TXE)) {
    }
    USART1_DR = (uint8_t)c;
}

static void uart_puts(const char *s)
{
    while (*s)
        uart_putc(*s++);
}

static void uart_printf(const char *fmt, ...)
{
    char buf[96];
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(buf, sizeof buf, fmt, ap);
    va_end(ap);
    uart_puts(buf);
}

/* ── светодиод на PC13 (горит при нуле) ───────────────────────────────── */
static uint8_t led_on;

__attribute__((noinline)) void led_set(int on)
{
    led_on = (uint8_t)on;
    GPIOC_BSRR = on ? (1u << (13 + 16)) : (1u << 13);
}

__attribute__((noinline)) void led_toggle(void)
{
    led_set(!led_on);
}

#ifdef NEWFN
/* Новая функция версии 2: вместо ровного мигания — две вспышки и пауза. Своих переменных у неё
 * нет: фаза берётся из счётчика миллисекунд. Куда она ляжет, решает имя секции. */
#if NEWFN == 1
__attribute__((noinline, section(".text.first")))
#else
__attribute__((noinline, section(".tail.last")))
#endif
void led_pattern(void)
{
    uint32_t phase = (ticks / BLINK_MS) & 7u;
    led_set(phase == 0u || phase == 2u);
}
#endif

/* ── «датчики» ─────────────────────────────────────────────────────────── */
static uint32_t lcg = 12345u;

static uint16_t adc_read(void)           /* вместо АЦП — псевдослучайный отсчёт около 1,43 В */
{
    lcg = lcg * 1664525u + 1013904223u;
    return (uint16_t)(1750u + ((lcg >> 24) & 31u));
}

/* Температура по встроенному датчику: T = (V25 − Vsense) / Avg_Slope + 25 */
static float temp_celsius(uint16_t code)
{
    float v = (float)code * 3.3f / 4096.0f;
    return (1.43f - v) / 0.0043f + 25.0f;
}

#define AVG_N 8u
static uint16_t avg_buf[AVG_N];
static uint32_t avg_sum, avg_pos;

static uint16_t avg_push(uint16_t x)     /* скользящее среднее по 8 отсчётам */
{
    avg_sum += x;
    avg_sum -= avg_buf[avg_pos];
    avg_buf[avg_pos] = x;
    avg_pos = (avg_pos + 1u) % AVG_N;
    return (uint16_t)(avg_sum / AVG_N);
}

static uint16_t crc16_modbus(const uint8_t *p, size_t n)
{
    uint16_t crc = 0xFFFFu;
    while (n--) {
        crc ^= *p++;
        for (int i = 0; i < 8; i++)
            crc = (crc & 1u) ? (uint16_t)((crc >> 1) ^ 0xA001u) : (uint16_t)(crc >> 1);
    }
    return crc;
}

/* ── команды ───────────────────────────────────────────────────────────── */
typedef struct {
    const char *name;
    void (*fn)(const char *arg);
    const char *help;
} cmd_t;

static void cmd_help(const char *arg);

static void cmd_ver(const char *arg)
{
    (void)arg;
    uart_printf("fw48v 1.4.0 blink=%u\r\n", (unsigned)BLINK_MS);
}

static void cmd_up(const char *arg)
{
    (void)arg;
    uint32_t t = ticks;
    uart_printf("up %lu.%03lu s\r\n", (unsigned long)(t / 1000u), (unsigned long)(t % 1000u));
}

static void cmd_led(const char *arg)
{
    if (strcmp(arg, "on") == 0)
        led_set(1);
    else if (strcmp(arg, "off") == 0)
        led_set(0);
    else
        led_toggle();
    uart_printf("led %s\r\n", led_on ? "on" : "off");
}

static void cmd_temp(const char *arg)
{
    (void)arg;
    uint16_t code = avg_push(adc_read());
    uart_printf("adc %u, t %.1f C\r\n", (unsigned)code, (double)temp_celsius(code));
}

static void cmd_crc(const char *arg)
{
    uart_printf("crc16 %04x\r\n", (unsigned)crc16_modbus((const uint8_t *)arg, strlen(arg)));
}

static void cmd_dump(const char *arg)
{
    uint32_t a = 0;
    for (; *arg; arg++) {                /* адрес — шестнадцатеричный */
        char c = *arg;
        uint32_t d = (c >= '0' && c <= '9') ? (uint32_t)(c - '0')
                   : (c >= 'a' && c <= 'f') ? (uint32_t)(c - 'a' + 10) : 16u;
        if (d > 15u)
            break;
        a = (a << 4) | d;
    }
    const uint8_t *p = (const uint8_t *)a;
    uart_printf("%08lx:", (unsigned long)a);
    for (int i = 0; i < 16; i++)
        uart_printf(" %02x", p[i]);
    uart_puts("\r\n");
}

static const cmd_t cmds[] = {
    {"ver",  cmd_ver,  "firmware version"},
    {"up",   cmd_up,   "uptime"},
    {"led",  cmd_led,  "led on|off|toggle"},
    {"temp", cmd_temp, "temperature, averaged"},
    {"crc",  cmd_crc,  "crc16 of the argument"},
    {"dump", cmd_dump, "16 bytes at hex address"},
    {"help", cmd_help, "this list"},
    {0, 0, 0},
};

static void cmd_help(const char *arg)
{
    (void)arg;
    for (const cmd_t *c = cmds; c->name; c++)
        uart_printf("  %-5s %s\r\n", c->name, c->help);
}

static void run_line(char *line)
{
    char *arg = strchr(line, ' ');
    if (arg)
        *arg++ = 0;
    else
        arg = line + strlen(line);
    for (const cmd_t *c = cmds; c->name; c++) {
        if (strcmp(line, c->name) == 0) {
            c->fn(arg);
            return;
        }
    }
    uart_printf("unknown: %s\r\n", line);
}

static void poll_uart(void)
{
    static char line[40];
    static size_t len;
    if (!(USART1_SR & USART_SR_RXNE))
        return;
    char c = (char)USART1_DR;
    if (c == '\r' || c == '\n') {
        line[len] = 0;
        if (len)
            run_line(line);
        len = 0;
    } else if (len < sizeof line - 1u) {
        line[len++] = c;
    }
}

int main(void)
{
    RCC_APB2ENR |= RCC_IOPAEN | RCC_IOPCEN | RCC_USART1EN;
    GPIOC_CRH = (GPIOC_CRH & ~(0xFu << 20)) | (0x2u << 20);   /* PC13 — выход 2 МГц */
    GPIOA_CRH = (GPIOA_CRH & ~(0xFFu << 4)) | (0x4Bu << 4);   /* PA9 — TX, PA10 — вход */
    USART1_BRR = BRR_115200;
    USART1_CR1 = (1u << 13) | (1u << 3) | (1u << 2);          /* UE, TE, RE */
    SYST_RVR = 8000u - 1u;                                    /* 1 мс при 8 МГц */
    SYST_CVR = 0u;
    SYST_CSR = 7u;                                            /* такт ядра, прерывание, пуск */

    cmd_ver("");
    uint32_t last = 0;
    for (;;) {
        if ((uint32_t)(ticks - last) >= BLINK_MS) {
            last = ticks;
#ifdef NEWFN
#if NEWFN == 3
            led_toggle();                /* (в′): вызов добавлен, main вырос */
#endif
            led_pattern();
#else
            led_toggle();
#endif
        }
        poll_uart();
    }
}

link48v.ld — the linker script of the experimental firmware and the bootloader.

/* Раздел 48в: скрипт компоновщика опытной прошивки и загрузчика. Начало и размер области
 * кода задаёт командная строка: -Wl,--defsym=ROM_ORG=0x08002000,--defsym=ROM_LEN=0xD000.
 * Два места для «новой» функции: .text.first — сразу за таблицей векторов (всё, что дальше,
 * сдвигается) и .tail.last — в самом конце образа (не сдвигается ничего). */
ENTRY(Reset_Handler)

MEMORY
{
  FLASH (rx)  : ORIGIN = ROM_ORG, LENGTH = ROM_LEN
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 8K
}

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

SECTIONS
{
  .isr_vector :
  {
    KEEP(*(.isr_vector))
  } > FLASH

  .text :
  {
    *(.text.first)                       /* вариант (б): новая функция в начале */
    *(.text .text.*)
    _srodata = .;
    *(.rodata .rodata.*)
    . = ALIGN(4);
  } > FLASH

  .data :
  {
    _sdata = .;
    *(.data .data.*)
    . = ALIGN(4);
    _edata = .;
  } > RAM AT > FLASH
  _sidata = LOADADDR(.data);

  .tail :
  {
    *(.tail.last)                        /* вариант (в): новая функция в конце образа */
    . = ALIGN(4);
  } > FLASH

  .bss (NOLOAD) :
  {
    _sbss = .;
    *(.bss .bss.*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
    end = .;                             /* начало кучи для _sbrk из nosys */
  } > RAM

  /DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) }
}

Next: section 48г "Encrypting the firmware image: the key, the AES mode and where the key lives in the device" — an image that cannot be read in the channel.

Open in the interactive handbook