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.
Платить за экономию эфира приходится флеш-памятью и сложностью. Прибору нужны место под разницу и второй слот под сборку нового образа, а загрузчику — код наложения. И ещё одно условие: разница годится только к той сборке, от которой её считали. Сервер обязан знать, какая версия стоит на каждом приборе, и хранить её двоичный образ.
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), написанный отдельно от генератора:
- шесть пар настоящих прошивок (48в.5) на трёх раскладках — слот 0 после наложения совпал с новым образом байт в байт;
- 3000 случайных пар образов до 6000 байт — замены, вставки, вырезки, перестановки и повторы кусков: 7 926 582 байта новых образов уложились в 706 138 байт разниц, второй декодер не разошёлся ни разу;
- 16 негодных разниц — все отвергнуты, слот 0 цел:
| Что не так | Ответ прибора |
|---|---|
| разница от другой прошивки | DB_FOREIGN |
испорчен бит в потоке или в заголовке, поток оборван, нет DLV1 |
DB_NONE |
| новый образ больше слота, поток длиннее области | DB_NONE |
| COPY до начала или за концом старого образа | DB_FAILED |
| результат длиннее или короче заявленного, команда нулевой длины | DB_FAILED |
| число длиннее 5 байт или больше 32 бит, ADD обещает больше байт, чем есть | DB_FAILED |
| поток верный, но CRC результата в заголовке другая | DB_FAILED |
- 2000 потоков, испорченных нарочно, но с пересчитанными CRC: прибор и второй декодер решили одинаково. 1899 отвергнуты; в 2 порча не изменила результат; ещё в 99 новый образ совпадал с началом старого — прибор видит, что нужная CRC уже лежит в слоте 0, и накладывать ничего не стал.
Последнее — свойство схемы: «уже обновлён» прибор определяет по 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 Итоги
- Образ 24 516 байт — это 511 кадров LoRaWAN по 48 байт: 100,5 мин на SF7 и 2378,6 мин на SF12 при 1 % эфира. Разница — от 1 до 15 кадров.
- Формат: заголовок 32 байта (длины и CRC-32 старого и нового образа, CRC потока и самого заголовка) и команды COPY и ADD с числами переменной длины. Одна изменённая константа — 46 байт.
- Генератор — 8-байтовые блоки старого образа в словаре и жадный поиск самого длинного совпадения; на 3000 случайных парах второй декодер не разошёлся ни разу.
- Наложение на приборе: 1076 байт кода, 348 байт ОЗУ, 176 байт стека; загрузчик F103 целиком — 2068 байт. Четыре функции порта — и тот же код работает со страницами F1 и секторами F4.
- Слот 0 не трогают, пока CRC слота 1 не сошлась. Обрыв питания на каждой операции: 3376 точек на настоящих прошивках и 80 059 на случайных парах — слот 0 всегда собран верно.
- Размер разницы решает раскладка: константа — 46 байт, функция в конце образа — 77, та же функция в начале — 706, образ под другой адрес — 727. Сдвиг меняет 151 абсолютный адрес.
- Наложение на месте экономит слот, но требует запасной страницы и журнала, увеличивает разницу (706 → 824 байта в паре (б)) и не оставляет пути назад.
Полные исходники
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 и где ключ живёт в приборе» — образ, который в канале не прочитать.