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.
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:
- six pairs of real firmware (48в.5) on three layouts — after patching slot 0 matched the new image byte for byte;
- 3000 random pairs of images up to 6000 bytes — replacements, insertions, cuts, swaps and repeats of pieces: 7926582 bytes of new images fitted into 706138 bytes of deltas, the second decoder never disagreed;
- 16 bad deltas — all rejected, slot 0 intact:
| 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 |
- 2000 streams corrupted on purpose, but with recomputed CRCs: the device and the second decoder decided the same way. 1899 were rejected; in 2 the corruption did not change the result; in 99 more the new image matched the start of the old one — the device sees that the required CRC is already in slot 0 and did not patch anything.
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
- An image of 24516 bytes is 511 LoRaWAN frames of 48 bytes: 100.5 min at SF7 and 2378.6 min at SF12 at 1 % of airtime. A delta is 1 to 15 frames.
- Format: a 32-byte header (lengths and CRC-32 of the old and new image, CRC of the stream and of the header itself) and COPY and ADD commands with variable-length numbers. One changed constant is 46 bytes.
- Generator: 8-byte blocks of the old image in a dictionary and a greedy search for the longest match; on 3000 random pairs the second decoder never disagreed.
- Patching on the device: 1076 bytes of code, 348 bytes of RAM, 176 bytes of stack; the whole F103 bootloader is 2068 bytes. Four port functions — and the same code works with F1 pages and F4 sectors.
- Slot 0 is not touched until the slot 1 CRC matches. Power loss at every operation: 3376 points on real firmware and 80059 on random pairs — slot 0 is always built correctly.
- The layout decides the delta size: a constant is 46 bytes, a function at the end of the image 77, the same function at the start 706, an image for another address 727. A shift changes 151 absolute addresses.
- Patching in place saves a slot, but needs a spare page and a journal, makes the delta larger (706 → 824 bytes in pair (b)) and leaves no way back.
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.