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

48в. Разностное обновление: переслать только изменения прошивки

В разделах 48, 48а и 48б новая прошивка всегда приходит целиком; так же обновляются ESP32 (раздел 159) и Linux с двумя разделами (раздел 195). По проводу это нестрашно. По радио с малой скоростью образ в десятки килобайт идёт часами, а отличается от прежнего часто несколькими десятками байт. Здесь прибору пересылают только разницу: короткий файл, из которого он вместе со старым образом сам собирает новый.

Раздел: счёт кадров и времени для LoRa, свой формат разницы, генератор на Python, наложение на приборе на C без malloc, обрыв питания на каждом шаге, три сборки одной прошивки, у которых разница отличается в пятнадцать раз, и наложение «на месте», без второго слота.

Опоры: формула времени в эфире из раздела 89 и предел 1 % из 93.4; страницы флеш-памяти F1 — RM0008, секторы F4 — RM0090; время стирания и записи и ресурс страницы — даташит STM32F103x8/xB (DocID13587 Rev 17, таблицы 28 и 29); запись во флеш-память — раздел 11.5, передача управления — 48.5. Опыт: восемь сборок прошивки для STM32F103 и загрузчик собраны arm-none-eabi-gcc 12.2.1 в образе stm-arm:1; код прибора без изменений собран gcc 12.2.0 в stm-sys:1 и прогнан на модели флеш-памяти; образы, собранные наложением, запущены в QEMU 7.2.22. На плате не запускалось. Готовые программы этого рода — bsdiff, xdelta, JojoDiff с библиотекой janpatch для микроконтроллеров (github.com/janjongboom/janpatch); с ними раздел не сверялся, формат и код здесь свои.

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

Оригинал Перевод
delta update, differential update разностное обновление
delta, patch разница — файл изменений
diff, patch (программы) построить разницу, наложить разницу
copy / add (insert) команды «взять из старого» и «вставить новое»
variable-length integer, varint число переменной длины
zigzag encoding запись числа со знаком: 0, −1, 1, −2 → 0, 1, 2, 3
in-place update наложение на месте — в тот же слот
power-fail safe переживает обрыв питания
time on air, duty cycle время в эфире, доля времени в эфире
erase unit: page, sector единица стирания: страница, сектор

48в.1 Зачем: счёт кадров

Возьмём прошивку из опыта этого раздела: 24 516 байт для STM32F103. Канал — LoRaWAN на 125 кГц (разделы 93 и 94). В кадр кладём 48 байт образа и 2 байта номера куска; с 13 служебными байтами LoRaWAN (раздел 89) это 63 байта. По формуле lora_airtime_us из раздела 89 такой кадр занимает эфир 118,0 мс на SF7 и 2793,5 мс на SF12. В основных полосах 868 МГц передатчик вправе занимать 1 % времени (93.4), значит, за каждой секундой передачи следуют 99 секунд молчания.

Что передаём Байт Кадров SF7: эфир, не быстрее SF12: эфир, не быстрее
целый образ 24 516 511 60,3 с, 100,5 мин 1427,1 с, 2378,6 мин
разница (б) — новая функция в начале 706 15 1,7 с, 2,9 мин 41,4 с, 69,0 мин
разница (в) — та же функция в конце 77 2 0,2 с, 0,4 мин 4,9 с, 8,2 мин
разница (а) — одна константа 46 1 0,1 с, 0,2 мин 2,8 с, 4,7 мин

Целый образ на SF12 — 2378,6 минуты, больше полутора суток, и это нижняя граница: без повторов потерянных кадров и без передач самого прибора. Прибор класса A слушает только после собственной передачи (94.6), так что каждый кадр вниз стоит ещё и кадра вверх. Во втором окне приёма на 869,525 МГц разрешено 10 % (93.4) — вдесятеро быстрее, но это всё равно почти четыре часа. Разница (б) в 35 раз короче образа, разница (в) — в 318 раз. Откуда берутся строки (а), (б) и (в) и почему они так различаются — в 48в.5.

Разностное обновление на STM32F103 со 128 КБ флеш-памяти: загрузчик 8 КБ, рабочий слот 0 и слот 1 по 52 КБ, область разницы 16 КБ; шаг 1 — команды COPY берут байты старого образа из слота 0, команды ADD — новые байты из разницы, результат пишется в слот 1; шаг 2 — проверка CRC-32; шаг 3 — копирование изменившихся страниц в слот 0; шаг 4 — стирание заголовка разницы; внизу разница пары (а) по байтам: заголовок 32 байта и пять команд

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


48в.2 Формат разницы и генератор

Новый образ почти весь состоит из кусков старого. Разница описывает его двумя командами: COPY — «взять n байт из старого образа» и ADD — «вставить n новых байт». Перед потоком команд — заголовок в 32 байта, восемь слов младшим байтом вперёд:

Смещение Поле Что в нём
0 magic 0x31564C44 — в файле байты DLV1
4 old_size длина старого образа
8 old_crc CRC-32 старого образа: к другой сборке разница не подойдёт
12 new_size длина нового образа
16 new_crc CRC-32 нового образа — сверка результата
20 body_size длина потока команд
24 body_crc CRC-32 потока команд
28 hdr_crc CRC-32 первых 28 байт заголовка

CRC-32 везде одна — как у zlib, полином 0xEDB88320. Поток команд:

Команда Первое число Дальше Что делает прибор
COPY n × 2 сдвиг указателя — число со знаком сдвигает указатель, берёт n байт старого образа, указатель += n
ADD n × 2 + 1 n байт вставляет эти байты, указатель += n

Числа записаны кодом переменной длины: по 7 бит в байте, младшие вперёд, старший бит байта — «есть продолжение»; не длиннее 5 байт. Число со знаком сначала превращается в число без знака чередованием: 0, −1, 1, −2 → 0, 1, 2, 3.

Указатель в старом образе прибор ведёт сам, и COPY несёт не адрес, а сдвиг относительно него. Тонкость: указатель идёт вперёд и на ADD. Тогда самый частый случай — несколько байт заменены на месте — даёт сдвиг 0, и команда COPY занимает от двух до четырёх байт.

Вот разница пары (а) целиком, 46 байт. В прошивке изменили одну константу, в образе изменились 2 байта из 24 484:

  заголовок: 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

Заголовок: DLV1, длины 0x5FA4 = 24 484, поток — 0x0e = 14 байт. Поток: bc 03 — это 0x3C + 3 × 128 = 444 = 222 × 2, то есть COPY 222 байт, за ним сдвиг 00. 03 — ADD одного байта, сам байт 7a. 8a 0a — 1290 = 645 × 2, COPY 645 байт, сдвиг 00: указатель после ADD уже стоит на 0xdf. Ещё один ADD и COPY остатка — fe f0 02 = 47 230 = 23 615 × 2. В сумме 222 + 1 + 645 + 1 + 23 615 = 24 484.

Генератор работает на компьютере. Числа и поиск самого длинного совпадения:

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

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

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)

Первым проверяется место guess — туда указатель придёт сам, если новые байты заменили столько же старых. Для него хватает совпадения в 4 байта (MIN_COPY): короче — дешевле передать байты как новые. Остальные места даёт таблица блоков; из равных по длине берётся ближайшее к guess, чтобы сдвиг занял меньше байт. Запуск:

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

Это вывод python mkdelta48v.py v1.bin v2b.bin b.dlt для пары (б). Длина блока на результат почти не влияет: для той же пары блоки в 4, 8, 16 и 32 байта дали 700, 706, 723 и 723 байта. Генератор не лучший из возможных: он не сжимает новые байты и не ищет, какое из совпадений выгоднее на несколько шагов вперёд. Зато разбор такого потока на приборе — одна функция.


48в.3 Наложение на приборе: два слота, фиксированные буферы

Раскладка опыта — STM32F103 со 128 КБ флеш-памяти, страницы по 1 КБ: загрузчик 8 КБ, слот 0 — рабочий, 52 КБ с 0x2000, слот 1 — 52 КБ с 0xF000, область разницы — 16 КБ с 0x1C000. Приложение всегда работает из слота 0 и собрано под его адрес, как у MCUboot (48б.1). Оно принимает разницу в свою область и перезагружается; накладывает разницу загрузчик. Заголовок, раскладка и порт флеш-памяти:

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);         /* размер единицы стирания по этому адресу */

Весь код наложения знает о камне только эти четыре функции: тот же файл работает со страницами F1 по 1 и 2 КБ и с секторами F4. Память — одна статическая структура: блок записи 256 байт, окно чтения потока 64 байта и семь слов состояния.

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

Запись идёт блоками. Единица стирания стирается тогда, когда в неё входит первый блок, — так один и тот же код пишет и в страницы, и в секторы. wr_open начинает запись с адреса, wr_flush сбрасывает блок во флеш-память:

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

Поток команд читается из области разницы через окно, по байту; rd_open ставит окно на начало потока:

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

Проверка разницы и само наложение. Разнице прибор не верит: каждая команда проверяется на выход за старый образ и за заявленную длину нового.

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

Сдвиг указателя считается в беззнаковых числах: отрицательный сдвиг — это прибавление 0u - …, и указатель, ушедший «ниже нуля», становится огромным и не проходит проверку cur > h->old_size.

Сколько это стоит. Сборка -Os для Cortex-M3:

Байт
код наложения (delta48v.o, секция .text) 1076
его ОЗУ (.bss): буферы 256 + 64 и семь слов 348
стек: main 8 + delta_boot 80 + delta_apply 56 + запись блока 8 + 24 176
загрузчик целиком: старт, порт F103, наложение, прыжок 2068

Кадры стека — из -fstack-usage; самая глубокая цепочка — main, delta_boot, delta_apply, затем либо wr_flush и fl_write (8 + 24), либо rd_number и rd_byte (16 + 16). Загрузчик занял 2068 байт из 8 КБ. Тот же код без предупреждений собирает armclang.

Порт для F103 — те же действия, что в 11.5: ключи, PER и STRT для стирания, PG и запись полусловами. Читать можно прямо по адресу: флеш-память видна в адресном пространстве. Блок нечётной длины дополняется 0xFF. Весь порт:

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

Код прибора проверен не на камне, а на модели флеш-памяти: тот же delta48v.c собран на ПК, а четыре функции порта работают с массивом. Модель строга: писать можно только в стёртые ячейки, стирать — только с начала единицы, а любая запись вне двух слотов и области разницы считается нарушением. Нарушений в прогонах — 0. Результат каждого наложения сравнивается с новым образом побайтно, а саму разницу независимо разбирает второй декодер на Python (undelta48v.py), написанный отдельно от генератора:

Что не так Ответ прибора
разница от другой прошивки DB_FOREIGN
испорчен бит в потоке или в заголовке, поток оборван, нет DLV1 DB_NONE
новый образ больше слота, поток длиннее области DB_NONE
COPY до начала или за концом старого образа DB_FAILED
результат длиннее или короче заявленного, команда нулевой длины DB_FAILED
число длиннее 5 байт или больше 32 бит, ADD обещает больше байт, чем есть DB_FAILED
поток верный, но CRC результата в заголовке другая DB_FAILED

Последнее — свойство схемы: «уже обновлён» прибор определяет по CRC первых new_size байт слота 0.


48в.4 Обрыв питания: решение при старте

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

Что видит загрузчик Что делает
разницы нет, или не сошлись CRC заголовка или потока запускает слот 0 (DB_NONE)
CRC слота 0 равна CRC нового образа стирает заголовок разницы (DB_DONE)
CRC слота 1 равна CRC нового образа копирует слот 1 в слот 0, проверяет, стирает заголовок
CRC слота 0 равна CRC старого образа накладывает разницу в слот 1, проверяет, затем копирует
ничего не совпало стирает заголовок, слот 0 не трогает (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 сравнивает два участка через окно чтения и буфер записи — отдельной памяти ему не нужно.

Обрыв во время наложения оставляет в слоте 1 мусор, но слот 0 и разница целы: следующий старт попадает в четвёртую строку таблицы и накладывает заново. Обрыв во время копирования оставляет слот 0 наполовину новым — зато слот 1 цел, и следующий старт попадает в третью строку. Страницы, которые уже совпадают, slot_copy пропускает, так что копирование продолжается с места обрыва. При DB_RETRY запускать слот 0 нельзя: в нём может лежать половина образа, загрузчик ждёт сброса и повторяет.

Так это выглядит в загрузчике F103. Перед прыжком он проверяет вершину стека и точку входа слота 0, передача управления — как в 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 (;;) {                           /* запускать нечего: ждать сторожа или программатора */
    }
}

Модель обрыва. Питание «пропадает» на заданной по счёту операции стирания или записи, и сама операция выполняется наполовину: в стираемой странице остаётся мусор, записываемый блок недописан, а следующий за ним байт испорчен. Сразу после обрыва модель проверяет, что целая копия старого или нового образа где-то есть, затем «перезагружается» — зовёт delta_boot() заново — и сравнивает слот 0 с ожидаемым.

Набор Точек обрыва Итог
шесть пар прошивок, три раскладки: обрыв на каждой операции 3376 слот 0 собран верно во всех
те же пары: два обрыва подряд в случайных местах 3600 во всех
3000 случайных пар: обрыв на каждой операции 80 059 во всех
3000 случайных пар: два обрыва подряд 9000 во всех
2000 испорченных потоков: обрыв на каждой операции 18 748 слот 0 — старый или новый, как без обрыва

Случаев, когда после обрыва не осталось ни одной целой копии, — 0. Пара (б) на F103 — это 241 операция: 24 стирания и 96 записей блоками до 256 байт на каждый слот и стирание заголовка разницы.

Сколько страниц затронуто. Слот 1 переписывается весь, слот 0 — только там, где образ изменился:

Пара F103, страницы 1 КБ: слот 1 / слот 0 F103, страницы 2 КБ F407, секторы
(а) одна константа 24 / 1 12 / 1 1 / 1
(б) функция в начале 24 / 24 12 / 12 1 / 2
(в) функция в конце 24 / 2 12 / 2 1 / 2

Страницы по 1 КБ — у F103 малой и средней плотности, по 2 КБ — у большой и сверхбольшой (RM0008). У F407 единица стирания — сектор (RM0090): в модели слот 0 — секторы 2 и 3 по 16 КБ, слот 1 — сектор 4 в 64 КБ, разница — сектор 1. На секторах по 128 КБ изменение одного байта стоит стирания всего сектора — времени и ресурса, о которых шла речь в 48б.5.

Время. По даташиту F103 страница стирается от 20 до 40 мс, полуслово пишется от 40 до 70 мкс. Пара (б): 49 стираний и 24 516 полуслов — от 1,96 до 3,68 с. Пара (а): 26 стираний и 12 754 полуслова — от 1,03 до 1,93 с. Время счёта CRC сюда не вошло. Против часов в эфире это ничто, но сторож загрузчика должен эти секунды пережить (48.12).

Что в опыте не сделано. Приём разницы по радио: в модели она уже лежит в области. В приборе приложение пишет её кусками по мере приёма, а действовать разница начинает, только когда на месте и заголовок, и весь поток: иначе не сойдётся body_crc. Заголовка образа из 48.3 у опытных прошивок нет; в изделии он остаётся и для разницы — просто первые байты образа. И подписи здесь нет: CRC ловит порчу, а не подмену (48.4).

Наконец, собранные образы запущены. В QEMU загрузчик стоит по 0x08000000, а в слот 0 положено то, что получилось после наложения:

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»; светодиод: выкл вкл выкл выкл выкл выкл выкл вкл выкл вкл 

Загрузчик не нашёл разницы, проверил вершину стека и точку входа и передал управление. Состояние светодиода — записи в GPIOC_BSRR, которые QEMU отмечает в журнале.

Само наложение в QEMU не идёт: контроллера флеш-памяти в этой модели F1 нет, записи в его регистры эмулятор только отмечает. Но по этим отметкам видно, что делает порт. Со старым образом в слоте 0 и разницей (б) в её области загрузчик записал в FLASH_CR бит PER 25 раз и бит PG 96 раз, а в FLASH_AR — адреса страниц с 0x0800f000 по 0x08014c00 и напоследок 0x0801c000: 24 страницы слота 1, 96 блоков и заголовок разницы. Память при этом не изменилась, CRC слота 1 не сошлась, и загрузчик запустил старый образ — светодиод мигает ровно.


48в.5 Почему разница бывает большой: три сборки одной прошивки

Опытная прошивка — обычное приложение для F103: таблица векторов, прерывание SysTick, UART, семь команд с таблицей указателей на функции и строки, vsnprintf с плавающей точкой из newlib-nano. Версия 1 — 24 484 байта. От неё собраны шесть вторых версий:

Пара Что изменено Новый образ Отличается байт Разница COPY / ADD
(а) константа 500 → 1000 24 484 2 46 3 / 2
(а′) константа 500 → 250 24 476 22 178 1034 141 / 138
(б) новая функция в начале образа 24 516 22 867 706 62 / 56
(в) та же функция в конце образа 24 516 36 77 2 / 2
(в′) функция в конце, но вызов добавлен, а не заменён 24 516 6140 476 91 / 90
(г) ничего; образ собран под адрес слота 1 24 484 284 727 54 / 53

Столбец «отличается байт» — сравнение образов позиция в позицию: так видит изменение тот, кто смотрит на файлы. В парах (а′) и (б) «изменился» почти весь образ, хотя исходник отличается одной строкой.

(а) Одна константа. Период светодиода BLINK_MS стоит в двух командах, в каждой меняется один байт. Разница — 14 байт потока (48в.2).

(б) Функция в начале. Добавлена функция led_pattern в 32 байта, и вызов led_toggle() в main заменён вызовом новой функции:

#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

Функция легла сразу за таблицей векторов, по 0x08002040, и всё остальное сдвинулось на 32 байта: main переехал с 0x080022fc на 0x0800231c. Сдвиг сам по себе разнице не страшен: COPY просто берёт те же байты с другого места. Вызовы между сдвинутыми функциями тоже не меняются — в команде bl записано расстояние, а не адрес. Меняются абсолютные адреса. В образе нашлось 152 изменившихся слова; 151 из них — адрес внутри образа, выросший ровно на 32:

Где лежат адреса Слов
таблица векторов 9
пулы констант между функциями 105
таблица команд: 7 строк по 3 указателя 21
начальные значения .data 16

Последнее, 152-е, слово — сама команда bl в main. Адреса разбросаны по всему образу, и каждый их островок — это отдельные ADD и COPY: отсюда 56 и 62 команды и 706 байт вместо 46.

(в) Та же функция в конце. Исходник тот же, изменилось имя секции. Скрипт компоновщика знает два места для новой функции:

  .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

Секция .tail лежит после всего, даже после начальных значений .data: функция оказалась по 0x08007fa4, и ни один прежний байт не сдвинулся. В образе изменились 4 байта команды bl и добавились 32 байта функции — разница 77 байт, в девять раз меньше, чем в (б). Работают обе сборки одинаково: в QEMU у них один и тот же узор светодиода (48в.4).

Опыт чистый потому, что main не вырос: один вызов заменён другим. В паре (в′) вызов новой функции добавлен рядом с прежним. main вырос с 280 до 284 байт, код за ним сдвинулся на 4 байта, и позиция в позицию отличаются уже 6140 байт. Сдвинулся не весь образ: размер его остался 24 516 байт — выравнивание одной из следующих секций эти 4 байта поглотило. Разница — 476 байт вместо 77: новая функция в конце образа не спасает, если растёт код в его начале. Отсюда правило раскладки: то, что меняется часто, держат в конце образа, а редко меняемое — библиотеки, драйверы — в начале.

(а′) Константа, изменившая длину команды. 250 вместо 500 — и разница 1034 байта вместо 46. Число 500 в команде сравнения требует длинной кодировки, 250 помещается в короткую:

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;

Две команды стали короче, вторая из них уже стоит на 4 байта раньше, а образ укоротился на 8 байт — и сдвинулось всё, что лежит дальше, как в (б).

(г) Тот же исходник под другой адрес. Схема из 48.2 «два образа на выпуск» — образ под слот 0 и образ под слот 1 — для разностного обновления плоха. Версия 1, собранная под 0x0800F000, отличается от самой себя под 0x08002000 теми же 151 словом: все абсолютные адреса выросли на 0xD000. Разница — 727 байт при нулевых изменениях в исходнике. Поэтому здесь образ всегда собран под слот 0, а слот 1 — только место сборки.


48в.6 Наложение на месте: чем опасно и что нужно

Второй слот — половина флеш-памяти. Заманчиво писать новый образ прямо поверх старого, страница за страницей. Опасностей три.

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

Пара Два слота На месте, снизу вверх На месте, сверху вниз Изменилось страниц
(а) 46 46 46 1 из 24
(а′) 1034 1118 1312 24 из 24
(б) 706 1498 824 24 из 24
(в) 77 83 85 2 из 24

В паре (б) всё сдвинулось вверх на 32 байта. При обходе снизу вверх первые 32 байта каждой страницы надо брать из предыдущей, уже затёртой, — разница удвоилась. При обходе сверху вниз те же байты ещё целы.

Страница, стёртая и не записанная, потеряна. В слоте её уже нет, а в ОЗУ она была до обрыва питания. Нужна запасная страница и журнал: новая страница сначала пишется в запасную, в журнал ложится отметка, и только потом стирается страница слота. После обрыва журнал говорит, какую страницу дописать из запасной и с какой продолжить. Это тот же приём, что обмен через scratch у MCUboot (48б.5), и та же цена: запасная страница стирается столько раз, сколько страниц изменилось. Ресурс страницы F103 — 10 тысяч циклов; при 24 изменившихся страницах одной запасной хватит на 416 обновлений, если не чередовать её с другими.

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

Модель на Python (inplace48v.py, кода для прибора в разделе нет) собирает образ на месте с запасной страницей и журналом и обрывает питание на каждой операции, а затем ещё раз — на случайной:

Пара Операций Стираний: слот + запасная Собран верно
(а) 6 1 + 1 6 из 6
(а′) 144 24 + 24 144 из 144
(б) 144 24 + 24 144 из 144
(в) 12 2 + 2 12 из 12

Выигрыш — флеш-память: вместо второго слота одна страница и журнал. И страниц стирается меньше: в паре (а) — одна в слоте вместо 24 в слоте 1. Проигрыш — сложность, износ запасной страницы и то, что прибор обязан довести обновление до конца: при двух слотах он в любой момент до копирования остаётся на старой версии.


48в.7 Грабли

Разница выросла больше чем в двадцать раз от правки одной константы. Изменилась длина команды или размер функции, и всё, что лежит дальше, сдвинулось: пара (а′), 1034 байта вместо 46. Размер разницы смотрят при каждом выпуске, до отправки.

Мелкая правка в начале образа — разница в сотни байт. Сдвиг меняет все абсолютные адреса: 151 слово в паре (б). И 4 лишних байта в main стоят 476 байт разницы — пара (в′). Часто меняемый код — в конец образа (секция вроде .tail), стабильный — в начало.

Прибор отвечает DB_FOREIGN. Разница посчитана от другой сборки. Того же исходника мало: версия 1, собранная с -O2 вместо -Os, — это 24 604 байта вместо 24 484, другой образ с другой CRC. Двоичный образ каждого выпуска хранят, как ELF в 35в.5.

Разницу считают между образами под разные слоты. 727 байт на ровном месте — пара (г). Образ всегда под один адрес.

CRC сошлась — и больше ничего не проверено. Разница несёт только CRC нового образа, а она ловит порчу, не подмену. Подпись из 48а и проверки из 48.4 загрузчик делает по результату — в слоте 1, до копирования в слот 0. В коде раздела подписи нет, а вершина стека и точка входа проверяются перед прыжком.

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

Сторож обрывает наложение. Пара (б) на F103 — до 3,68 с стирания и записи, без счёта CRC. Сторож перезапускать между блоками или дать ему больший срок (48.12).

Разница не влезает в область. Прибор такую отвергнет: body_size больше области — DB_NONE. Генератор обязан знать размер области и в этом случае отправлять целый образ обычным путём.

Блок записи не кратен единице стирания. wr_flush стирает единицу, когда в неё входит блок. Границы слотов должны лежать на границах единиц, а 256 байт блока — укладываться в страницу целое число раз.

На месте — и питание пропало. Без запасной страницы и журнала прибор остаётся с образом, где одна страница не старая и не новая. Нет места под запасную страницу — не делать наложение на месте.


48в.8 Итоги

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

delta48v.h — формат разницы, раскладка и порт флеш-памяти.

/* Раздел 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 — наложение разницы на приборе без 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 — загрузчик STM32F103 с наложением разницы.

/* Раздел 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 — генератор разницы двух образов на компьютере.

# -*- 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 — наложение «на месте» — модель на Python.

# -*- 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 — опытная прошивка для STM32F103 с вариантами сборки.

/* Раздел 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 — скрипт компоновщика опытной прошивки и загрузчика.

/* Раздел 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*) }
}

Дальше: раздел 48г «Шифрование образа прошивки: ключ, режим AES и где ключ живёт в приборе» — образ, который в канале не прочитать.

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