189а. Обновление прошивки из мобильного приложения: от кнопки в Flutter до загрузчика STM32
В разделе 48 загрузчик принимал «поток байт» от кого-то снаружи, в 189 приложение научилось говорить с прибором по BLE и кабелю. Здесь эти половины сходятся: кнопка «Обновить» на телефоне, файл прошивки, кадры через радио, загрузчик, который пишет Flash, и новая прошивка, которая должна сама подтвердить, что работает. Если не подтвердит — прибор возвращается на прежнюю.
Весь путь собран своим кодом с обеих сторон: клиент на Dart, экран на Flutter, загрузчик на C. Проверен он на компьютере: клиент говорит с моделью прибора, где работает тот же C-код загрузчика, а Flash и канал BLE заменены моделями с честно названными параметрами. Модель роняет связь посреди передачи, теряет посылки и выключает питание прибора на каждой из 2069 операций Flash по очереди.
Опоры: раздел 48 (раскладка, заголовок образа, 48.6 — как приложение просит перепрошивку, 48.8 — подтверждение), 48а (подпись), 195 (A/B, подтверждение и откат), 194 (кадры COBS и CRC-16), 185–189 (Dart, асинхронность, экран, транспорт).
Как это называется в документации
Оригинал Перевод device firmware update (DFU) обновление прошивки прибора image, slot образ, область под образ chunk, block, offset кусок, блок, смещение acknowledge (ACK) подтверждение приёма stop-and-wait «послал — жди ответа»: один блок в пути resume продолжение с места обрыва trial boot, confirm, rollback пробный запуск, подтверждение, откат write with response запись с подтверждением connection interval интервал связи BLE app lifecycle (paused, resumed) жизненный цикл приложения: ушло в фон, вернулось
189а.1 Весь путь одной картинкой
Четыре участника и одно правило на каждого:
| Кто | Что делает | Чего не делает никогда |
|---|---|---|
| приложение на телефоне | проверяет файл до отправки, ведёт передачу, показывает состояние | не шлёт образ, который прибору не подходит |
| транспорт (раздел 189) | режет кадры по MTU, держит одну запись за раз | не знает, что везёт прошивку |
| приложение прибора | отвечает «кто я», по просьбе уходит в загрузчик, подтверждает себя | не пишет чужую область Flash |
| загрузчик | стирает свободную область, принимает, проверяет, выбирает, что запускать | не стирает рабочую область и себя |
Почему принимает загрузчик, а не приложение. Вариантов два: приложение прибора пишет вторую область само, на ходу (так в разделе 195 делает обновитель Linux), или приложение только ставит признак 48.6 и перезапускается в загрузчик, а принимает он. Здесь выбран второй. Приём один на все версии приложения и работает даже тогда, когда годного приложения нет вовсе: прибор, потерявший оба образа, остаётся в загрузчике и ждёт файл. Приложение от этого проще — ему не нужен ни драйвер Flash, ни разбор блоков. Цена — прибор на время обновления не делает свою работу, а загрузчик вырастает на приём и разбор кадров (размеры — 189а.6). Если остановка недопустима, берут первый путь, и приём из этого раздела переезжает в приложение.
Две области и два образа на выпуск. Образ собран под свой адрес и из чужой области не запустится (48.2). Здесь выбран путь «два образа на выпуск»: сборка даёт файл под область A и файл под B, прибор говорит, какая свободна, телефон шлёт подходящий. Прежняя прошивка остаётся на месте до самого подтверждения новой — откатываться есть куда, и ничего не копируется.
Готовые решения, если свой протокол не нужен: у MCUboot есть протокол управления SMP (MCUmgr), у Nordic — свой DFU для их радиокристаллов, у ST — заводской загрузчик в системной памяти (48.11). Большой образ по медленному каналу можно передавать разностью — раздел 48в. Целиком свой загрузчик серийного прибора на F405 собран в мини-проекте 198щ.
189а.2 Протокол поверх кадров 194
Кадр — из раздела 194: сообщение, к нему CRC-16/XMODEM старшим байтом вперёд, всё в COBS, в конце ноль. Кадр раздела 185 сюда не годится: в нём не больше 32 байт и CRC-8 — для команд хватает, для прошивки мало. Одно добавление: ведущий ноль перед каждым кадром с обеих сторон (189а.6 — зачем).
Числа в сообщениях — младшим байтом вперёд. Ответ — та же команда с поднятым старшим битом и байт состояния.
| Команда | Код | Поля запроса | Ответ | Кто отвечает |
|---|---|---|---|---|
| INFO | 0x01 |
— | режим, плата, версия, состояние, свободная область, её размер, наибольший блок | приложение и загрузчик |
| ENTER | 0x02 |
— | состояние; затем признак .noinit и сброс |
приложение |
| BEGIN | 0x10 |
область, размер тела, CRC-32 тела, версия | состояние, с какого смещения слать | загрузчик |
| BLOCK | 0x11 |
смещение, данные (кратно 4, до 240 байт) | состояние, следующее ожидаемое смещение | загрузчик |
| END | 0x12 |
— | состояние: CRC сошлась, метка записана | загрузчик |
| REBOOT | 0x13 |
— | состояние; затем сброс | загрузчик |
Состояния: OK, BAD_CMD, BAD_STATE (не тот режим — сеанса приёма нет),
BAD_SLOT, BAD_SIZE, OLD_VERSION, BAD_OFFSET, BAD_CRC, FLASH,
BAD_HEADER. На каждое у телефона своя фраза для экрана.
Смещение, а не номер блока. Блок адресован смещением в файле, а прибор в каждом ответе говорит, какое смещение ждёт следующим. Из этого сразу выходят три свойства:
- повтор уже записанного блока (ответ потерялся) прибор не пишет второй раз, а просто отвечает «жду N»;
- после переподключения телефон шлёт BEGIN с теми же размером, CRC и версией — прибор узнаёт сеанс и отвечает, с какого места продолжать;
- размер блока можно менять на ходу: после переподключения MTU бывает другим, а номер блока при другом размере значил бы другое место.
Размер блока. Кадр блока длиннее данных ровно на 10 байт: ведущий ноль, команда, четыре байта смещения, два байта CRC, байт COBS и завершающий ноль. Наибольший блок, который входит в одну посылку, — полезная часть посылки минус 10, округлённая вниз до кратного четырём: 8 байт при MTU 23, 172 при MTU 185, 232 при MTU 247. Странице Flash блок кратным быть не обязан: стирание делает BEGIN заранее, а запись идёт словами. Обязательна только кратность четырём — у F407 запись словом, у F103 слово пишется двумя полусловами.
Сроки ответа. Блок — 1 с, INFO и прочие короткие — 0,5 с, END — 2 с, а BEGIN — 8 с: он стирает область. У F103 это 33 страницы по 20…40 мс (DocID13587, табл. 28), у F407 сектор 128 КБ стирается 1 с типично и до 2 с (DS8626, табл. 40, запись словом), а область из трёх секторов — до 6 с. Срок ожидания BEGIN, взятый как у блока, означает, что телефон сдастся посреди стирания и начнёт всё сначала.
189а.3 Телефон: файл, проверки до отправки, передача
Образ — тот же, что в 48.3: заголовок 512 байт и тело. Два слова reserved из
48.3 стали ячейками «запускался на пробу» и «подтверждён» (48.8), в первом слове
дополнения лежит код платы. В файле со сборки все три ячейки стёрты — 0xFFFFFFFF.
Проверка до отправки — всё, что телефон может узнать сам. Неподходящий файл не должен даже перевести прибор в загрузчик:
/// Годится ли файл этому прибору. null — годится, иначе причина для экрана.
String? checkImage(Uint8List f, DeviceInfo dev) {
if (f.length < imgHdrSize + 8) return 'файл короче заголовка образа';
final ImageHeader h = ImageHeader(f);
if (h.magic != imgMagic) return 'это не образ прошивки: нет метки STM3';
if (h.size != f.length - imgHdrSize) return 'длина в заголовке ${h.size}, а в файле ${f.length - imgHdrSize}';
if (crc32(f, imgHdrSize) != h.crc) return 'файл повреждён: CRC не сходится';
if (h.tried != cellErased || h.ok != cellErased) return 'в файле стоят отметки запуска: это снимок с прибора, а не сборка';
if (h.board != dev.board) return 'образ для платы ${hex(h.board)}, а прибор — ${hex(dev.board)}';
if (h.size > dev.slotSize - imgHdrSize) return 'образ ${h.size} байт не помещается в область';
if (h.entry != h.pc || (h.pc & 1) == 0 || h.pc < dev.freeSlot + imgHdrSize || h.pc >= dev.freeSlot + dev.slotSize) {
return 'образ собран под другой адрес: свободна область ${hex(dev.freeSlot)}';
}
if (h.version <= dev.version) return 'версия ${h.version} не новее версии ${dev.version} на приборе';
return null;
}
Выпуск — два файла; телефон берёт тот, чья точка входа попадает в свободную область, о которой сказал прибор:
/// Выпуск — два образа, под область A и под B (48.2); берём тот, что под свободную.
Uint8List? pickImage(List<Uint8List> release, DeviceInfo dev) {
for (final Uint8List f in release) {
if (f.length < imgHdrSize + 8) continue;
final int pc = get32(f, imgHdrSize + 4);
if (pc >= dev.freeSlot + imgHdrSize && pc < dev.freeSlot + dev.slotSize) return f;
}
return null;
}
Кадр на отправку и приёмник ответов — повторение 194 на Dart, с ведущим нулём:
/// Кадр на отправку. Ведущий ноль закрывает обрывок прошлого кадра,
/// если его посылка потерялась: иначе прибор склеит обрывок с этим кадром.
Uint8List encodeMessage(List<int> msg, {bool leadingZero = true}) {
final int c = crc16(msg);
final List<int> body = cobsEncode(<int>[...msg, c >> 8, c & 0xFF]);
return Uint8List.fromList(<int>[if (leadingZero) 0, ...body, 0]);
}
Запрос с повтором: после четырёх таймаутов подряд — переподключение и снова; три круга без ответа — ошибка «прибор перестал отвечать».
/// Запрос с повторами по таймауту; после [tries] неудач — переподключение и снова.
Future<Uint8List> _ask(List<int> msg, Duration timeout, {int payload = 0}) async {
for (int round = 0; round < 3; round++) {
for (int n = 0; n < tries; n++) {
stats.requests++;
stats.payload += payload;
try {
final Uint8List? r = await port.request(msg, timeout);
if (r != null) return r;
stats.timeouts++;
} on LinkLost {
break; // писать некуда — сразу переподключаться
}
}
await _reconnect();
}
throw const _Fatal('прибор перестал отвечать');
}
Сама процедура. Если прибор ответил BAD_STATE, значит, он перезапускался и сеанс
пропал — всё начинается заново, не больше трёх раз:
Future<void> _attempt() async {
_set(const UpdateStatus(Phase.connecting));
DeviceInfo info = await _info();
_set(const UpdateStatus(Phase.checking));
Uint8List f = _choose(info); // до перезапуска: не тот файл — прибор не трогаем
if (info.mode == DeviceInfo.app) {
_set(const UpdateStatus(Phase.entering));
await _ask(<int>[Cmd.enter], timeouts.short);
info = await _info();
if (info.mode != DeviceInfo.loader) throw const _Fatal('прибор не перешёл в загрузчик');
f = _choose(info);
}
final ImageHeader h = ImageHeader(f);
final Ack? begin = parseAck(await _ask(msgBegin(info.freeSlot, h.size, h.crc, h.version), timeouts.begin), Cmd.begin);
if (begin == null) throw const _Fatal('прибор ответил не по протоколу');
if (begin.status == Status.badState) throw const _Restart();
if (begin.status != Status.ok) throw _Fatal(statusText[begin.status]!);
int next = begin.next; // не ноль — прибор помнит, докуда дошли
final int from = next;
final Duration t0 = port.now;
_set(UpdateStatus(Phase.sending, sent: next, total: f.length));
while (next < f.length) {
final int end = next + block < f.length ? next + block : f.length;
final List<int> data = f.sublist(next, end);
while (data.length % 4 != 0) {
data.add(0xFF); // хвост до слова: стёртое прибор не пишет
}
final Ack? ack = parseAck(await _ask(msgBlock(next, data), timeouts.block, payload: data.length), Cmd.block);
if (ack == null) throw const _Fatal('прибор ответил не по протоколу');
if (ack.status == Status.badState) throw const _Restart(); // прибор перезапускался: сеанса нет
if (ack.status != Status.ok) throw _Fatal(statusText[ack.status]!);
next = ack.next;
final double s = (port.now - t0).inMicroseconds / 1e6;
_set(UpdateStatus(Phase.sending, sent: next, total: f.length, bytesPerSecond: s > 0 ? (next - from) / s : 0));
}
_set(UpdateStatus(Phase.verifying, sent: f.length, total: f.length));
final Status? st = parseStatus(await _ask(<int>[Cmd.end], timeouts.end), Cmd.end);
if (st == Status.badState) throw const _Restart();
if (st != Status.ok) throw _Fatal(st == null ? 'прибор ответил не по протоколу' : statusText[st]!);
_set(const UpdateStatus(Phase.restarting));
try {
await port.request(<int>[Cmd.reboot], timeouts.short); // ответ может и не прийти: прибор уже в сбросе
} on LinkLost {
// переподключится _ask ниже
}
_set(const UpdateStatus(Phase.confirming));
for (int n = 0; n < 5; n++) {
info = await _info();
if (info.mode == DeviceInfo.app && info.version == h.version && info.state == DeviceInfo.confirmed) {
_set(UpdateStatus(Phase.done, sent: f.length, total: f.length));
return;
}
if (info.mode == DeviceInfo.app && info.version != h.version) {
throw _Fatal('прибор вернулся на версию ${info.version}: новая не подтвердила себя или пропадало питание');
}
await Future<void>.delayed(timeouts.poll);
}
throw const _Fatal('прибор не подтвердил новую прошивку');
}
Под этим кодом — FwPort с двумя методами: «запрос — ответ за срок» и
«переподключиться». Настоящая реализация стоит на интерфейсе Transport из 189:
кадр режет по MTU BleTransport, и очередь записей та же. В проверке этот путь
пройден целиком: BleTransport с MTU 20 и настоящими таймаутами поверх модели
прибора довёл обновление до конца, самая длинная запись — 20 байт.
189а.4 Прибор: приём, метка последней, выбор и откат
Загрузчик — fwup.c. Начало сеанса: проверить, что область та, что свободна,
размер помещается, версия новее подтверждённой, — и стереть.
static uint8_t on_begin(const uint8_t *p, uint32_t n)
{
if (n != 16u)
return ST_BAD_CMD;
uint32_t origin = rd32(p), size = rd32(p + 4), crc = rd32(p + 8), ver = rd32(p + 12);
if ((s.active || s.done) && origin == s.origin && size == s.size && crc == s.crc && ver == s.version)
return ST_OK; /* тот же образ после обрыва: продолжаем с s.next */
uint32_t best = best_confirmed();
if (origin != free_slot())
return ST_BAD_SLOT;
if (size == 0u || size > SLOT_SIZE - IMG_HDR_SIZE)
return ST_BAD_SIZE;
if (best != 0u && ver <= HDR(best)->version)
return ST_OLD_VERSION;
s.active = 0;
s.done = 0;
if (flash_erase_range(origin, IMG_HDR_SIZE + size) != 0)
return ST_FLASH;
s.origin = origin; s.size = size; s.crc = crc; s.version = ver;
s.next = 0;
s.active = 1;
return ST_OK;
}
Блок. Метка magic и стёртые слова не пишутся: стёртое и так стёрто, а метка
появится последней, когда всё сошлось.
static uint8_t on_block(const uint8_t *p, uint32_t n)
{
if (!s.active)
return ST_BAD_STATE;
if (n < 8u || ((n - 4u) & 3u) != 0u || n - 4u > FW_MAX_BLOCK)
return ST_BAD_CMD;
uint32_t off = rd32(p), len = n - 4u;
if (off != s.next)
return off < s.next ? ST_OK : ST_BAD_OFFSET; /* повтор уже записанного — просто ответить */
if (off + len > ((IMG_HDR_SIZE + s.size + 3u) & ~3u))
return ST_BAD_SIZE;
for (uint32_t i = 0; i < len; i += 4u) {
uint32_t w = rd32(p + 4 + i);
if (off + i == 0u || w == CELL_ERASED)
continue; /* магия — в самом конце; стёртое не пишем */
if (flash_prog_word(s.origin + off + i, w) != 0) {
s.active = 0;
return ST_FLASH;
}
}
s.next = off + len;
return ST_OK;
}
Конец: заголовок сверяется с BEGIN, CRC тела считается по тому, что реально
записано во Flash, и только потом пишется метка. До этой записи загрузчик область
не видит вовсе — пропало питание на любом байте, и image_valid её отвергнет на
первой же проверке.
static uint8_t on_end(void)
{
const img_hdr_t *h = HDR(s.origin);
if (s.done)
return ST_OK; /* ответ на первый END потерялся */
if (!s.active || s.next < IMG_HDR_SIZE + s.size)
return ST_BAD_STATE;
if (h->size != s.size || h->crc != s.crc || h->version != s.version || h->board != BOARD_ID ||
h->entry < s.origin + IMG_HDR_SIZE || h->entry >= s.origin + SLOT_SIZE)
return ST_BAD_HEADER;
if (crc32((const void *)(uintptr_t)(s.origin + IMG_HDR_SIZE), s.size) != s.crc)
return ST_BAD_CRC;
s.active = 0;
if (flash_prog_word(s.origin, IMG_MAGIC) != 0) /* последним: теперь образ виден */
return ST_FLASH;
if (!image_valid(s.origin))
return ST_BAD_HEADER;
s.done = 1;
return ST_OK;
}
Выбор, что запускать, — при каждом старте. Свежий образ (не запускался) новее подтверждённого — сначала отметка «на пробу», потом запуск. Образ с отметкой, но без подтверждения, — проба не удалась: загрузчик пишет нули в его метку и запускает подтверждённый.
/* ── Что запускать: подтверждённый новейший или свежий на пробу ─────── */
static uint32_t best_confirmed(void)
{
uint32_t best = 0;
for (uint32_t s = SLOT_A; s <= SLOT_B; s += SLOT_SIZE)
if (image_valid(s) && HDR(s)->ok != CELL_ERASED &&
(best == 0u || HDR(s)->version > HDR(best)->version))
best = s;
return best;
}
uint32_t fw_select(void)
{
uint32_t fresh = 0;
for (uint32_t s = SLOT_A; s <= SLOT_B; s += SLOT_SIZE) {
if (!image_valid(s) || HDR(s)->ok != CELL_ERASED)
continue;
if (HDR(s)->tried != CELL_ERASED)
flash_prog_word(s, 0u); /* проба была и не подтвердилась: образ списан */
else
fresh = s;
}
uint32_t best = best_confirmed();
if (fresh != 0u && (best == 0u || HDR(fresh)->version > HDR(best)->version)) {
flash_prog_word(fresh + offsetof(img_hdr_t, tried), 0u); /* сначала отметка, потом запуск */
return fresh;
}
return best;
}
Все ячейки только дописываются: из стёртого состояния в записанное, а «списать» образ — записать нули в метку: ноль можно записать и поверх записанного — у F103 это единственное исключение из запрета писать в нестёртое, у F407 запись и так даёт побитовое «И» (раздел 4). Ни одна отметка не требует стирания — стирание страницы с заголовком стёрло бы и начало образа (48.8).
Сторона приложения прибора — короткая: ответить на INFO, по ENTER поставить признак 48.6 и сброситься, подтвердить себя.
void fw_confirm(uint32_t slot)
{
if (HDR(slot)->ok == CELL_ERASED)
flash_prog_word(slot + offsetof(img_hdr_t, ok), 0u);
}
void app_handle(const uint8_t *msg, uint32_t n, uint32_t my_slot)
{
uint8_t out[24], frame[32];
uint32_t len = 2;
if (n == 0u)
return;
fw_confirm(my_slot); /* телефон до нас достучался — живы */
out[0] = REPLY(msg[0]);
switch (msg[0]) {
case CMD_INFO: len = fw_info(MODE_APP, my_slot, out); break;
case CMD_ENTER: out[1] = ST_OK; break;
default: out[1] = ST_BAD_STATE; break; /* приём образа — только в загрузчике */
}
frame[0] = 0; /* ведущий ноль: закрыть обрывок чужого кадра */
fw_tx(frame, 1u + frame_encode(out, len, frame + 1));
if (msg[0] == CMD_ENTER) {
reboot_flag = REBOOT_MAGIC; /* 48.6: признак в .noinit и сброс */
fw_reset();
}
}
Здесь приложение подтверждает себя на первом целом кадре от телефона — это и есть проверка «связь работает». В настоящем приборе к ней добавляют свои: датчики отвечают, нагрузка управляется (195.4). Подтверждение раньше проверки здоровья делает откат бесполезным.
Драйвер Flash для F103 — регистры из RM0008, имена — из CMSIS:
static int prog_half(uint32_t addr, uint16_t v)
{
FLASH->CR |= FLASH_CR_PG;
*(volatile uint16_t *)addr = v;
int rc = wait_ready();
FLASH->CR &= ~FLASH_CR_PG;
return (rc != 0 || *(volatile uint16_t *)addr != v) ? -1 : 0;
}
int flash_prog_word(uint32_t addr, uint32_t value)
{
int rc;
if (addr < SLOT_A || (addr & 3u))
return -1;
unlock();
rc = prog_half(addr, (uint16_t)value);
if (rc == 0)
rc = prog_half(addr + 2u, (uint16_t)(value >> 16));
FLASH->CR |= FLASH_CR_LOCK;
return rc;
}
У F407 то же самое словом при PSIZE = x32, а стирание — по номеру сектора
(FLASH_CR_SNB), который считается по карте 16/64/128 КБ из 48.2.
Подпись. CRC защищает от порчи, не от подмены. Проверка подписи из 48а ложится
в on_end перед записью метки: образ без верной подписи метку не получит и
запущен не будет. Здесь её нет, чтобы размеры и числа остались про сам протокол.
189а.5 Экран обновления во Flutter
Экран подписан на поток состояний Updater и показывает одну фразу на каждое —
без технических слов:
const Map<Phase, String> phaseText = <Phase, String>{
Phase.connecting: 'Подключение к прибору',
Phase.checking: 'Проверка файла прошивки',
Phase.entering: 'Прибор переходит в загрузчик',
Phase.sending: 'Передача прошивки',
Phase.verifying: 'Прибор проверяет прошивку',
Phase.restarting: 'Перезапуск прибора',
Phase.confirming: 'Ждём подтверждения новой прошивки',
Phase.done: 'Готово: новая прошивка работает',
Phase.failed: 'Обновление не удалось',
};
Уход приложения в фон экран узнаёт от WidgetsBindingObserver:
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() => background = state != AppLifecycleState.resumed);
}
Построение: процент и скорость при передаче, причина при ошибке. Кнопка заблокирована, пока идёт обновление, «Назад» не уводит с экрана, а уход в фон даёт предупреждение:
Widget build(BuildContext context) {
final UpdateStatus? v = s;
return PopScope(
canPop: !running, // «Назад» посреди передачи не уводит с экрана
child: Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(v == null ? 'Прибор подключён' : phaseText[v.phase]!, key: const Key('phase')),
if (v != null && v.phase == Phase.sending) ...<Widget>[
LinearProgressIndicator(value: v.percent / 100),
Text('${v.percent.round()} % · ${ru1(v.bytesPerSecond / 1024)} КБ/с', key: const Key('progress')),
],
if (v != null && v.phase == Phase.failed)
Text(v.reason, key: const Key('reason'), style: const TextStyle(color: Colors.red)),
if (running && background)
const Text('Не сворачивайте приложение, пока идёт обновление', key: Key('background')),
ElevatedButton(
key: const Key('start'),
onPressed: running ? null : widget.onStart,
child: const Text('Обновить'),
),
],
),
),
),
);
}
Четыре виджет-теста без телефона: «38 % · 3,9 КБ/с» при передаче и запрет
повторного нажатия, причина ошибки на экране и кнопка снова доступна, PopScope
не отпускает посреди проверки на приборе, предупреждение при уходе в фон и его
исчезновение при возврате. Не давать экрану гаснуть во время передачи — задача
платформенного плагина; в проверку это не входит. Разрешения BLE — 189.5.
189а.6 Опыт на компьютере: время, потери, обрывы, откат
Клиент на Dart говорит с моделью прибора через dart:ffi: в библиотеке, собранной
gcc, работает тот же fwup.c и тот же fw_app.c, а Flash модели лежит по
настоящему адресу 0x08000000 — загрузчик читает образ обычными указателями.
Правила записи у модели — как у камня: у F103 полуслово пишется только в стёртое,
кроме нуля; у F407 бит можно только сбросить. Времена — из документации:
| F103 (DocID13587) | F407 (DS8626, x32) | |
|---|---|---|
| единица стирания | страница 1 КБ, 40 мс (наибольшее из 20…40) | сектор 128 КБ, 1 с (типично) |
| запись | полуслово 52,5 мкс | слово 16 мкс |
Модель канала — не замер радио, а договорённость, и её параметры названы: каждая посылка (запись с подтверждением или уведомление) занимает один интервал связи 15 мс; модуль BLE отдаёт байты микроконтроллеру по UART на 115 200 бод; переподключение — 2 с. Абсолютные секунды зависят от этих чисел; соотношения между строками — от протокола.
Время обновления. Тело образа 32 КБ, файл 33 280 байт, F103, без потерь. Время — от первого INFO до «Готово», включая стирание 1,32 с; скорость — на передаче блоков.
| MTU | Блок, байт | Посылок на блок | Время, с | Скорость, КБ/с |
|---|---|---|---|---|
| 23 | 8 — в одну посылку | 1 | 137,73 | 0,24 |
| 23 | 16 | 2 | 102,74 | 0,32 |
| 23 | 64 | 4 | 45,30 | 0,74 |
| 23 | 128 | 7 | 37,03 | 0,92 |
| 23 | 232 | 13 | 35,75 | 0,95 |
| 185 | 16 | 1 | 71,48 | 0,46 |
| 185 | 64 | 1 | 21,84 | 1,60 |
| 185 | 128 | 1 | 13,57 | 2,70 |
| 185 | 172 — в одну посылку | 1 | 11,47 | 3,28 |
| 185 | 232 | 2 | 12,02 | 3,10 |
| 247 | 16 | 1 | 71,48 | 0,46 |
| 247 | 64 | 1 | 21,84 | 1,60 |
| 247 | 128 | 1 | 13,57 | 2,70 |
| 247 | 232 — в одну посылку | 1 | 9,88 | 3,90 |
Из таблицы три вывода:
- при большом MTU выгоден блок, который ровно входит в одну посылку: 232 байта при MTU 247 — 9,88 с, а блок 16 байт при том же MTU — 71,48 с, в семь с лишним раз дольше;
- при MTU 23 блок «в одну посылку» (8 байт) — худший: 137,73 с. Каждый блок стоит ответа и интервала на него, и при малом MTU выгоднее длинный блок из многих посылок — 35,75 с при 232 байтах;
- договориться о большом MTU важнее всего остального: 232-байтовый блок идёт 9,88 с при MTU 247 и 35,75 с при MTU 23.
Тот же образ на F407: 8,83 с — одно стирание сектора за 1 с вместо 33 страниц по 40 мс, а четыре байта пишутся одним словом за 16 мкс вместо двух полуслов по 52,5 мкс.
Потери. Модель теряет посылку целиком с заданной вероятностью, в обе стороны. Потеря стоит таймаута — секунды на блок.
| MTU, блок | Потери | Ведущий ноль | Время, с | Потеряно посылок | Таймаутов | Переподключений |
|---|---|---|---|---|---|---|
| 247, 232 | 1 % | да | 12,91 | 3 | 3 | 0 |
| 247, 232 | 5 % | да | 26,06 | 16 | 16 | 0 |
| 23, 64 | 5 % | да | 204,86 | 155 | 150 | 0 |
| 23, 64 | 5 % | нет | 249,82 | 164 | 189 | 2 |
При MTU 247 кадр блока — одна посылка, и потерянная посылка уносит ровно один кадр. При MTU 23 кадр из четырёх посылок теряет середину, и прибор получает обрывок без завершающего нуля. Без ведущего нуля обрывок склеивается со следующим кадром — с повтором того же блока — и CRC губит и его: таймаутов 189 вместо 150, два переподключения, на 44,96 с дольше. Ведущий ноль закрывает обрывок до начала нового кадра — одна потеря стоит одного кадра.
Обрыв связи посреди передачи. На пятой секунде связь рвётся. Телефон переподключается (2 с модели), повторяет тот же блок, прибор отвечает «жду N» — и передача идёт дальше. Итог: 11,89 с вместо 9,88, одно переподключение, повторно ушло 232 байта — один блок.
Проверки до отправки. Прибор с версией 2; во всех четырёх случаях телефон отказал после единственного запроса INFO, прибор не перезапускался:
| Что в файле | Что на экране |
|---|---|
| код платы 0x104, прибор — 0x103 | «образ для платы 0x00000104, а прибор — 0x00000103» |
| в выпуске только образ под область A | «в выпуске нет образа под свободную область 0x08012000» |
| версия 2 — та же, что на приборе | «версия 2 не новее версии 2 на приборе» |
| испорчен один байт тела | «файл повреждён: CRC не сходится» |
Если телефон проверку пропустит, прибор отвергнет сам: BEGIN с версией 1 поверх
подтверждённой 2 получил ответ OLD_VERSION.
Обрыв питания. Сценарий: в области A — подтверждённая версия 2, в B — версия 1 от прошлого обновления, телефон ставит версию 3 в B. Чистый прогон с образом 4 КБ делает у F103 2069 операций Flash: стирание пяти страниц, запись тела и заголовка полусловами, метку, отметку «на пробу» и подтверждение. Опыт повторяет обновление 2069 раз и в каждом выключает питание на очередной операции — посреди неё: у стирания успевает половина страницы, у записи — младший байт. Через 3 с питание возвращается, телефон переподключается, а если сеанс пропал — пользователь жмёт «Обновить» ещё раз.
| F103 | F407 | |
|---|---|---|
| точек обрыва | 2069 | 1033 |
| после включения запустился целый образ (CRC сошлась) | 2069 | 1033 |
| первым запустилась прежняя версия 2 | 2067 | 1032 |
| первым запустилась новая версия 3 | 2 | 1 |
| обновление в итоге закончено версией 3 | 2069 | 1033 |
| понадобилось второе нажатие «Обновить» | 2 | 1 |
Ни в одной точке прибор не остался без загружаемой прошивки и ни разу не запустил половину образа. Новая версия запускалась сразу после обрыва только тогда, когда питание пропало на записи подтверждения: частично записанная ячейка уже не стёрта и считается записанной — приложение к этому моменту живо. Второе нажатие нужно, когда питание пропало на записи отметки «на пробу»: отметка недописана, загрузчик считает пробу неудачной, списывает образ и запускает версию 2. Это безопасно, но телефон видит возврат на прежнюю версию — поэтому фраза на экране честно называет обе причины: «прибор вернулся на версию 2: новая не подтвердила себя или пропадало питание».
Откат. Версия 3 зависает до подтверждения, сторож перезапускает прибор (в модели — через 1 с). Итог: три перезапуска — в загрузчик, в версию 3 на пробу, по сторожу; загрузчик списал версию 3 (её область больше не проходит проверку) и запустил версию 2; телефон показал «Обновление не удалось» и причину.
Сборка для камня. arm-none-eabi-gcc -Os -Wall -Wextra -Werror, загрузчик целиком —
fwup.c, драйвер Flash, старт и приём по USART1:
| F103, Cortex-M3 | F407, Cortex-M4 | |
|---|---|---|
| Flash, байт | 2532 | 2628 |
| ОЗУ, байт | 552 | 552 |
признак reboot_flag |
0x20004FF0 |
0x2001FFF0 |
У F103 загрузчик занимает меньше шестой части отведённых ему 16 КБ. Подпись так просто не войдёт: в сборке 48а под Cortex-M4 проверка подписи добавила 17 248 байт — больше всех 16 КБ, отведённых загрузчику F103 в 48.2. Признак лежит по адресу общей области из 48.6, одинаковому для загрузчика и приложения.
189а.7 Грабли
Обновление «зависает» на первом же шаге у F407. Срок ответа BEGIN взят как у блока. Стирание трёх секторов длится до 6 с — срок на BEGIN считают от размера области.
При малом MTU потери обходятся вдвое дороже, чем должны. Обрывок потерянного кадра склеивается со следующим, и CRC губит оба. Ведущий ноль перед каждым кадром — с обеих сторон.
После переподключения передача начинается с нуля. Блоки пронумерованы, а прибор не говорит, что ждёт. Смещение в запросе и «жду N» в ответе, BEGIN узнаёт тот же образ.
Повтор блока портит Flash. Прибор пишет блок, ответ на который потерялся,
второй раз; у F103 это PGERR, а у F407 — тихое «И» старого с новым (раздел 4). Блок раньше
ожидаемого — только ответить.
Пропало питание — прибор не загружается. Метку записали первой, вместе с заголовком. Метка — последней, после проверки CRC по записанному.
Новая прошивка зависла — и так навсегда. Нет отметки «на пробу» перед запуском или нет сторожа: зависший образ не перезапускается, попытка не засчитывается (195.4).
Откат не срабатывает. Приложение подтверждает себя первой строкой main. Сначала
проверка здоровья, потом подтверждение.
Телефон шлёт файл, а прибор отвечает BAD_SLOT. Файл собран под область,
где лежит работающая прошивка. Выпуск — два образа, телефон берёт тот, что под
свободную область.
Пользователь свернул приложение посреди передачи. Что станет со связью в фоне, решает платформа. Экран предупреждает, а после обрыва телефон переподключается и продолжает с того смещения, которое назовёт прибор.
189а.8 Итоги
- Путь: телефон проверяет файл → INFO → ENTER (признак 48.6) → BEGIN со стиранием → блоки со смещением и ответом «жду N» → END: CRC по записанному и метка последней → REBOOT → пробный запуск → подтверждение приложением или откат.
- Кадр — из 194 плюс ведущий ноль; кадр блока длиннее данных на 10 байт, блок в одну посылку — 232 байта при MTU 247, 172 при MTU 185, 8 при MTU 23.
- Модель канала (15 мс на посылку, UART 115 200): образ 33 280 байт у F103 — 9,88 с при MTU 247 и блоке 232, 35,75 с при MTU 23; у F407 — 8,83 с.
- Потери 5 % при MTU 247: 16 таймаутов и 26,06 с; при MTU 23 без ведущего нуля таймаутов 189 вместо 150.
- Обрыв связи на пятой секунде: одно переподключение, повторно ушёл один блок — 232 байта.
- Питание выключали на каждой операции Flash: 2069 точек у F103 и 1033 у F407 — прибор всегда запускал целый образ, обновление всегда заканчивалось версией 3, второе нажатие понадобилось 2 и 1 раз.
- Неподтвердившая себя версия откатывается сама: три перезапуска, образ списан, работает прежняя.
- Загрузчик с приёмом, проверкой и выбором — 2532 байт Flash у F103 и 2628 у F407.
Полные исходники
fwup.c — загрузчик: приём образа по кадрам, проверка, подтверждение и откат.
/* Загрузчик с приёмом образа по кадрам: выбор области, приём, проверка,
подтверждение и откат. Раздел 189а; раскладка и заголовок — раздел 48,
кадры COBS + CRC-16 — раздел 194, порядок подтверждения — раздел 195. */
#include <stddef.h>
#include <string.h>
#include "fwup.h"
#define HDR(slot) ((const img_hdr_t *)(uintptr_t)(slot))
static uint32_t rd32(const uint8_t *p)
{
return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
}
static void wr32(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24);
}
/* ── Контрольные суммы ───────────────────────────────────────────────── */
uint32_t crc32(const void *data, uint32_t len) /* как zlib: 0xEDB88320 */
{
const uint8_t *p = data;
uint32_t c = 0xFFFFFFFFu;
while (len--) {
c ^= *p++;
for (int i = 0; i < 8; i++)
c = (c & 1u) ? (c >> 1) ^ 0xEDB88320u : c >> 1;
}
return ~c;
}
uint16_t crc16(const uint8_t *p, uint32_t n) /* CRC-16/XMODEM, как в 194 */
{
uint16_t c = 0;
while (n--) {
c ^= (uint16_t)(*p++) << 8;
for (int i = 0; i < 8; i++)
c = (c & 0x8000u) ? (uint16_t)((c << 1) ^ 0x1021u) : (uint16_t)(c << 1);
}
return c;
}
/* ── Кадр 194: COBS(сообщение + CRC-16 старшим байтом вперёд) + 0x00 ── */
static uint32_t cobs_encode(const uint8_t *in, uint32_t n, uint8_t *out)
{
uint32_t w = 1, code_at = 0;
uint8_t code = 1;
for (uint32_t i = 0; i < n; i++) {
if (in[i] == 0) {
out[code_at] = code;
code_at = w++;
code = 1;
} else {
out[w++] = in[i];
if (++code == 0xFF) {
out[code_at] = code;
code_at = w++;
code = 1;
}
}
}
out[code_at] = code;
return w;
}
static int32_t cobs_decode(const uint8_t *in, uint32_t n, uint8_t *out)
{
uint32_t r = 0, w = 0;
while (r < n) {
uint8_t code = in[r++];
if (code == 0)
return -1;
for (uint8_t i = 1; i < code; i++) {
if (r >= n)
return -1;
out[w++] = in[r++];
}
if (code != 0xFF && r < n)
out[w++] = 0;
}
return (int32_t)w;
}
uint32_t frame_encode(const uint8_t *msg, uint32_t n, uint8_t *out)
{
uint8_t buf[FW_RX_MAX];
if (n > sizeof buf - 2u)
return 0;
memcpy(buf, msg, n);
uint16_t c = crc16(msg, n);
buf[n] = (uint8_t)(c >> 8);
buf[n + 1u] = (uint8_t)c;
uint32_t m = cobs_encode(buf, n + 2u, out);
out[m++] = 0;
return m;
}
/* ── Проверка образа: раздел 48.4, плюс плата ────────────────────────── */
int image_valid(uint32_t slot)
{
const img_hdr_t *h = HDR(slot);
const uint32_t *vec = (const uint32_t *)(uintptr_t)(slot + IMG_HDR_SIZE);
if (h->magic != IMG_MAGIC || h->board != BOARD_ID)
return 0;
if (h->size == 0u || h->size > SLOT_SIZE - IMG_HDR_SIZE)
return 0;
if (vec[0] < RAM_ORIGIN || vec[0] > RAM_ORIGIN + RAM_BYTES)
return 0;
if ((vec[1] & 1u) == 0u || vec[1] < slot + IMG_HDR_SIZE || vec[1] >= slot + SLOT_SIZE)
return 0;
return crc32((const void *)(uintptr_t)(slot + IMG_HDR_SIZE), h->size) == h->crc;
}
/* ── Что запускать: подтверждённый новейший или свежий на пробу ─────── */
static uint32_t best_confirmed(void)
{
uint32_t best = 0;
for (uint32_t s = SLOT_A; s <= SLOT_B; s += SLOT_SIZE)
if (image_valid(s) && HDR(s)->ok != CELL_ERASED &&
(best == 0u || HDR(s)->version > HDR(best)->version))
best = s;
return best;
}
uint32_t fw_select(void)
{
uint32_t fresh = 0;
for (uint32_t s = SLOT_A; s <= SLOT_B; s += SLOT_SIZE) {
if (!image_valid(s) || HDR(s)->ok != CELL_ERASED)
continue;
if (HDR(s)->tried != CELL_ERASED)
flash_prog_word(s, 0u); /* проба была и не подтвердилась: образ списан */
else
fresh = s;
}
uint32_t best = best_confirmed();
if (fresh != 0u && (best == 0u || HDR(fresh)->version > HDR(best)->version)) {
flash_prog_word(fresh + offsetof(img_hdr_t, tried), 0u); /* сначала отметка, потом запуск */
return fresh;
}
return best;
}
/* Свободная область — та, где не лежит подтверждённый образ. */
static uint32_t free_slot(void)
{
return best_confirmed() == SLOT_A ? SLOT_B : SLOT_A;
}
uint32_t fw_info(uint8_t mode, uint32_t run_slot, uint8_t *out)
{
const img_hdr_t *h = HDR(run_slot);
int have = run_slot != 0u && image_valid(run_slot);
out[0] = REPLY(CMD_INFO);
out[1] = ST_OK;
out[2] = mode;
wr32(out + 3, BOARD_ID);
wr32(out + 7, have ? h->version : 0u);
out[11] = !have ? RUN_NONE : h->ok != CELL_ERASED ? RUN_CONFIRMED : RUN_TRIAL;
wr32(out + 12, free_slot());
wr32(out + 16, SLOT_SIZE);
out[20] = (uint8_t)FW_MAX_BLOCK;
out[21] = (uint8_t)(FW_MAX_BLOCK >> 8);
return 22;
}
/* ── Сеанс приёма ────────────────────────────────────────────────────── */
static struct {
uint32_t origin, size, crc, version;
uint32_t next; /* следующее ожидаемое смещение в файле */
uint8_t active; /* идёт приём */
uint8_t done; /* образ принят и проверен: повтор END и BEGIN — не ошибка */
} s;
static struct {
uint8_t acc[FW_RX_MAX + 2u], dec[FW_RX_MAX + 2u];
uint32_t al;
uint8_t overflow;
} rx;
void fw_init(void) /* после сброса: ОЗУ чистое */
{
memset(&s, 0, sizeof s);
memset(&rx, 0, sizeof rx);
}
static uint8_t on_begin(const uint8_t *p, uint32_t n)
{
if (n != 16u)
return ST_BAD_CMD;
uint32_t origin = rd32(p), size = rd32(p + 4), crc = rd32(p + 8), ver = rd32(p + 12);
if ((s.active || s.done) && origin == s.origin && size == s.size && crc == s.crc && ver == s.version)
return ST_OK; /* тот же образ после обрыва: продолжаем с s.next */
uint32_t best = best_confirmed();
if (origin != free_slot())
return ST_BAD_SLOT;
if (size == 0u || size > SLOT_SIZE - IMG_HDR_SIZE)
return ST_BAD_SIZE;
if (best != 0u && ver <= HDR(best)->version)
return ST_OLD_VERSION;
s.active = 0;
s.done = 0;
if (flash_erase_range(origin, IMG_HDR_SIZE + size) != 0)
return ST_FLASH;
s.origin = origin; s.size = size; s.crc = crc; s.version = ver;
s.next = 0;
s.active = 1;
return ST_OK;
}
static uint8_t on_block(const uint8_t *p, uint32_t n)
{
if (!s.active)
return ST_BAD_STATE;
if (n < 8u || ((n - 4u) & 3u) != 0u || n - 4u > FW_MAX_BLOCK)
return ST_BAD_CMD;
uint32_t off = rd32(p), len = n - 4u;
if (off != s.next)
return off < s.next ? ST_OK : ST_BAD_OFFSET; /* повтор уже записанного — просто ответить */
if (off + len > ((IMG_HDR_SIZE + s.size + 3u) & ~3u))
return ST_BAD_SIZE;
for (uint32_t i = 0; i < len; i += 4u) {
uint32_t w = rd32(p + 4 + i);
if (off + i == 0u || w == CELL_ERASED)
continue; /* магия — в самом конце; стёртое не пишем */
if (flash_prog_word(s.origin + off + i, w) != 0) {
s.active = 0;
return ST_FLASH;
}
}
s.next = off + len;
return ST_OK;
}
static uint8_t on_end(void)
{
const img_hdr_t *h = HDR(s.origin);
if (s.done)
return ST_OK; /* ответ на первый END потерялся */
if (!s.active || s.next < IMG_HDR_SIZE + s.size)
return ST_BAD_STATE;
if (h->size != s.size || h->crc != s.crc || h->version != s.version || h->board != BOARD_ID ||
h->entry < s.origin + IMG_HDR_SIZE || h->entry >= s.origin + SLOT_SIZE)
return ST_BAD_HEADER;
if (crc32((const void *)(uintptr_t)(s.origin + IMG_HDR_SIZE), s.size) != s.crc)
return ST_BAD_CRC;
s.active = 0;
if (flash_prog_word(s.origin, IMG_MAGIC) != 0) /* последним: теперь образ виден */
return ST_FLASH;
if (!image_valid(s.origin))
return ST_BAD_HEADER;
s.done = 1;
return ST_OK;
}
void fw_handle(const uint8_t *msg, uint32_t n)
{
uint8_t out[24], frame[32];
uint32_t len = 2;
if (n == 0u)
return;
out[0] = REPLY(msg[0]);
switch (msg[0]) {
case CMD_INFO: len = fw_info(MODE_LOADER, best_confirmed(), out); break;
case CMD_BEGIN: out[1] = on_begin(msg + 1, n - 1u); wr32(out + 2, s.next); len = 6; break;
case CMD_BLOCK: out[1] = on_block(msg + 1, n - 1u); wr32(out + 2, s.next); len = 6; break;
case CMD_END: out[1] = on_end(); break;
case CMD_REBOOT: out[1] = ST_OK; break;
default: out[1] = ST_BAD_CMD; break;
}
frame[0] = 0; /* ведущий ноль: закрыть обрывок чужого кадра */
fw_tx(frame, 1u + frame_encode(out, len, frame + 1));
if (msg[0] == CMD_REBOOT)
fw_reset();
}
/* ── Приём байтов: до нуля копим, потом COBS, CRC и разбор ───────────── */
void fw_rx_byte(uint8_t b)
{
if (b != 0u) {
if (rx.al < sizeof rx.acc) rx.acc[rx.al++] = b;
else rx.overflow = 1; /* длиннее допустимого — кадр испорчен */
return;
}
int32_t dl = (rx.al == 0u || rx.overflow) ? -1 : cobs_decode(rx.acc, rx.al, rx.dec);
rx.al = 0;
rx.overflow = 0;
if (dl >= 3 && crc16(rx.dec, (uint32_t)dl - 2u) == (uint16_t)(rx.dec[dl - 2] << 8 | rx.dec[dl - 1]))
fw_dispatch(rx.dec, (uint32_t)dl - 2u); /* целый кадр; пустой и битый — молча мимо */
}
flash_f1.c — драйвер Flash STM32F103 для загрузчика.
/* Драйвер Flash STM32F103 для загрузчика: страница 1 КБ, запись полусловом (RM0008). Раздел 189а. */
#include "stm32f10x.h"
#include "fwup.h"
static int wait_ready(void)
{
while (FLASH->SR & FLASH_SR_BSY) {
}
uint32_t sr = FLASH->SR;
FLASH->SR = FLASH_SR_EOP | FLASH_SR_PGERR | FLASH_SR_WRPRTERR; /* сбрасываются записью единицы */
return (sr & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) ? -1 : 0;
}
static void unlock(void)
{
if (FLASH->CR & FLASH_CR_LOCK) {
FLASH->KEYR = FLASH_KEY1;
FLASH->KEYR = FLASH_KEY2;
}
}
int flash_erase_range(uint32_t addr, uint32_t len)
{
int rc = 0;
if (addr < SLOT_A)
return -1; /* себя не стираем никогда */
unlock();
for (uint32_t a = addr & ~1023u; a < addr + len && rc == 0; a += 1024u) {
FLASH->CR |= FLASH_CR_PER;
FLASH->AR = a;
FLASH->CR |= FLASH_CR_STRT;
rc = wait_ready();
FLASH->CR &= ~FLASH_CR_PER;
}
FLASH->CR |= FLASH_CR_LOCK;
return rc;
}
static int prog_half(uint32_t addr, uint16_t v)
{
FLASH->CR |= FLASH_CR_PG;
*(volatile uint16_t *)addr = v;
int rc = wait_ready();
FLASH->CR &= ~FLASH_CR_PG;
return (rc != 0 || *(volatile uint16_t *)addr != v) ? -1 : 0;
}
int flash_prog_word(uint32_t addr, uint32_t value)
{
int rc;
if (addr < SLOT_A || (addr & 3u))
return -1;
unlock();
rc = prog_half(addr, (uint16_t)value);
if (rc == 0)
rc = prog_half(addr + 2u, (uint16_t)(value >> 16));
FLASH->CR |= FLASH_CR_LOCK;
return rc;
}
fw_app.c — сторона приложения прибора: «кто я», уход в загрузчик и подтверждение себя.
/* Сторона приложения прибора: ответить «кто я», уйти в загрузчик по просьбе
телефона (48.6) и подтвердить себя (48.8, 195). Приложение собирается
с тем же fwup.c: из него нужны кадры, fw_info и проверка образа. */
#include <stddef.h>
#include "fwup.h"
#define HDR(slot) ((const img_hdr_t *)(uintptr_t)(slot))
void fw_confirm(uint32_t slot)
{
if (HDR(slot)->ok == CELL_ERASED)
flash_prog_word(slot + offsetof(img_hdr_t, ok), 0u);
}
void app_handle(const uint8_t *msg, uint32_t n, uint32_t my_slot)
{
uint8_t out[24], frame[32];
uint32_t len = 2;
if (n == 0u)
return;
fw_confirm(my_slot); /* телефон до нас достучался — живы */
out[0] = REPLY(msg[0]);
switch (msg[0]) {
case CMD_INFO: len = fw_info(MODE_APP, my_slot, out); break;
case CMD_ENTER: out[1] = ST_OK; break;
default: out[1] = ST_BAD_STATE; break; /* приём образа — только в загрузчике */
}
frame[0] = 0; /* ведущий ноль: закрыть обрывок чужого кадра */
fw_tx(frame, 1u + frame_encode(out, len, frame + 1));
if (msg[0] == CMD_ENTER) {
reboot_flag = REBOOT_MAGIC; /* 48.6: признак в .noinit и сброс */
fw_reset();
}
}
Дальше: раздел 190 «Телеметрия и выпуск: кольцевой буфер, прореживание, хранение, сборка».