201а. Выпуск прошивки: версия, метаданные и один файл на производство
Раздел 201 поставил линию: стенд прошивает плату, гоняет самопроверку и пишет строку в журнал — с версией боевой прошивки и её CRC32. Откуда берутся эта версия и эта CRC, там не разбиралось: скрипт стенда получал их готовыми. Здесь — путь от метки в git до одного файла, который уходит на линию: версия и номер сборки попадают в образ сами, лежат в известном месте флеш-памяти, загрузчик проверяет CRC образа до запуска, две сборки одного коммита дают один и тот же файл, а загрузчик и приложение склеены в один HEX, который прошивается одной командой.
Всё собрано и прогнано на ПК: временный репозиторий git с меткой v1.4.2,
загрузчик и приложение для F407 — arm-none-eabi-gcc 12.2.1 в образе
stm-arm:1, то же приложение — armclang 6.24, склеенный файл — в
qemu-system-arm -M netduinoplus2 (Cortex-M4, вывод через semihosting,
раздел 35а). Скрипт выпуска release.py проходит весь путь и печатает
итог; проверка сверяет с ним каждое число раздела.
Опоры: заголовок образа и image_valid — раздел 48 (48.3, 48.4); подпись —
раздел 48а; повторяемая сборка в конвейере — раздел 110 (110.8); журнал
линии — раздел 201 (201.7). Таблица векторов F407 — startup_stm32f407xx.s
из пака STM32F4xx_DFP 2.17.1.
Как это называется в документации
Оригинал Перевод semantic versioning (SemVer) семантическая версия: старшая.младшая.исправление build number / build metadata номер сборки, метаданные сборки ( +3в1.4.2+3)annotated / lightweight tag метка git с автором, датой и сообщением / простая метка git describe --dirtyближайшая метка, число коммитов после неё, хеш и признак правок linker script / scatter file сценарий компоновщика GNU ld / файл раскладки armlink FIXED execution region область с адресом исполнения, равным адресу загрузки reproducible build воспроизводимая сборка: тот же вход — тот же образ байт в байт SOURCE_DATE_EPOCHпеременная окружения: «сейчас» для компилятора в секундах от 1970 Intel HEX, extended linear address record текстовый формат образа; запись типа 04 — старшие 16 бит адреса release notes / release log описание выпуска / журнал выпусков
201а.1 Версия из git: метка, номер сборки, признак правок
Версию не пишут руками в исходник: её забудут поднять. Источник правды —
метка в git. Семантическая версия 1.4.2 — это три числа: старшее меняют,
когда ломается совместимость (формат обмена, раскладка настроек), младшее —
когда добавлено новое без поломки, третье — исправление. К ней добавляют
номер сборки — число коммитов в истории — и короткий хеш коммита.
Всё это отдаёт одна команда. gen_version.py разбирает её вывод и пишет
заголовок version.h:
def describe(repo):
d = git(repo, "describe", "--tags", "--long", "--dirty", "--match", "v[0-9]*", "--abbrev=8")
m = re.fullmatch(r"v(\d+)\.(\d+)\.(\d+)-(\d+)-g([0-9a-f]{8})(-dirty)?", d)
if not m:
raise SystemExit("не разобрал git describe: " + d)
ma, mi, pa, ahead, h, dirty = m.groups()
return {"describe": d, "major": int(ma), "minor": int(mi), "patch": int(pa), "ahead": int(ahead),
"git": h, "dirty": int(bool(dirty)), "build": int(git(repo, "rev-list", "--count", "HEAD"))}
В опыте репозиторий из трёх коммитов с закреплёнными автором и датами,
метка v1.4.2 на последнем. git describe вернул v1.4.2-0-g07234455:
ноль коммитов после метки, хеш 07234455. Номер сборки — 3, итоговая
строка версии — 1.4.2+3. Сгенерированный заголовок:
#define FW_VER_MAJOR 1
#define FW_VER_MINOR 4
#define FW_VER_PATCH 2
#define FW_BUILD 3u
#define FW_GIT "07234455"
#define FW_DIRTY 0
Два признака отличают выпуск от «сборки у меня на столе». Число после
метки не ноль — коммиты после метки ещё не выпущены. Суффикс -dirty —
в дереве есть незакоммиченные правки, и образ нельзя воспроизвести ни из
какого коммита. Скрипт выпуска на обоих отказывает (201а.6).
201а.2 Метаданные в фиксированном месте флеш-памяти
Версия нужна не только человеку. Стенд линии должен прочитать её из
прошитого камня и сравнить с журналом; загрузчик — сверить с заголовком;
программа обновления — решить, новее ли пришедший образ. Никто из них не
знает, как собиралась прошивка, и искать строку по всей памяти не будет.
Значит, метаданные лежат по известному адресу — здесь со смещением
0x200 от начала таблицы векторов приложения.
Почему 0x200: таблица векторов F407 в startup_stm32f407xx.s — 98 слов,
392 байта, она помещается в 512 байт целиком. А метаданные сразу за ней
сдвигались бы при каждом изменении числа векторов.
Структура — 44 байта, первым полем магическое число, вторым — версия самой раскладки:
typedef struct {
uint32_t magic; /* FW_META_MAGIC — метаданные на месте */
uint16_t layout; /* FW_META_LAYOUT: новые поля — только в конец */
uint16_t size; /* sizeof(fw_meta_t) этой сборки */
uint8_t major, minor, patch; /* версия SemVer */
uint8_t dirty; /* 1 — собрано из дерева с незакоммиченными правками */
uint32_t build; /* номер сборки: число коммитов */
char git[12]; /* короткий хеш коммита, с нулём в конце */
char product[16]; /* что это за прошивка */
} fw_meta_t;
Заполняют её макросы из version.h, а в отдельную секцию кладёт атрибут:
__attribute__((section(".fw_meta"), used))
const fw_meta_t fw_meta = {
FW_META_MAGIC, FW_META_LAYOUT, (uint16_t)sizeof(fw_meta_t),
FW_VER_MAJOR, FW_VER_MINOR, FW_VER_PATCH, FW_DIRTY,
FW_BUILD, FW_GIT, "thermo-201a",
};
Адрес задаёт компоновщик. У GNU ld — сценарий app.ld; KEEP не даёт
выбросить секции, на которые никто не ссылается, а две проверки
ASSERT остановят сборку, если таблица векторов разрастётся или адрес
уедет:
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.fw_meta ORIGIN(FLASH) + 0x200 : { KEEP(*(.fw_meta)) } > FLASH
.text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH
/DISCARD/ : { *(.ARM.exidx*) *(.comment) }
ASSERT(SIZEOF(.isr_vector) <= 0x200, "vector table overlaps metadata")
ASSERT(ADDR(.fw_meta) == 0x08020400, "metadata is not at 0x08020400")
У armlink то же делает файл раскладки: область ER_META с атрибутом
FIXED стоит ровно по своему адресу. Вершину стека armlink даёт через
пустую область ARM_LIB_STACK — символом Image$$ARM_LIB_STACK$$ZI$$Limit:
LR_APP 0x08020200 0x0005FE00 {
ER_VEC 0x08020200 0x200 { *(.isr_vector, +FIRST) }
ER_META 0x08020400 FIXED { *(.fw_meta) }
ER_CODE +0 { *(+RO) }
ARM_LIB_STACK 0x20020000 EMPTY -0x1000 { }
}
Итог обеих сборок — одинаковый адрес: fw_meta по 0x08020400, таблица
векторов по 0x08020200 и у gcc, и у armclang (armlink — с ключами
--keep для обеих секций). Все 44 байта метаданных в двух образах
совпали. В образе они выглядят так:
46 57 4D 44 01 00 2C 00 01 04 02 00 03 00 00 00
30 37 32 33 34 34 35 35 00 00 00 00 74 68 65 72
6D 6F 2D 32 30 31 61 00 00 00 00 00
46 57 4D 44 — «FWMD», 2C 00 — размер 44, 01 04 02 — версия,
03 00 00 00 — номер сборки, дальше хеш 07234455 и имя изделия.
Размеры секций у gcc: таблица векторов 64 байта, метаданные 44, код и
константы 336; между векторами и метаданными — 448 байт пустоты,
и образ приложения выходит 892 байта. У armclang — 868.
Правило для самой структуры: поля только добавляются в конец, а
layout и size говорят читателю, сколько их. Старый стенд, прочитав
метаданные новой прошивки, возьмёт первые 44 байта и не ошибётся.
201а.3 CRC образа: считает скрипт выпуска, проверяет загрузчик
Метаданные говорят, что прошито; CRC говорит, цело ли оно. Считать CRC внутри сборки неудобно: она зависит от всего образа, включая саму себя. Поэтому её считает скрипт выпуска уже по готовому образу и кладёт в заголовок раздела 48 — 512 байт перед приложением:
def header_for(app):
"""Заголовок раздела 48 для образа приложения app (bytes, начало — таблица векторов)."""
magic, layout, size, ma, mi, pa, dirty, build = struct.unpack_from("<IHHBBBBI", app, META_OFF)
if magic != META_MAGIC:
raise ValueError("нет метаданных по смещению 0x200")
if dirty:
raise ValueError("образ собран из дерева с незакоммиченными правками — не выпуск")
version = ma << 24 | mi << 16 | pa << 8
entry = struct.unpack_from("<I", app, 4)[0]
crc = zlib.crc32(app) & 0xFFFFFFFF
hdr = struct.pack("<8I", IMG_MAGIC, len(app), crc, version, entry, 0, 0, 0)
return hdr + b"\xFF" * (HDR - len(hdr)), {"size": len(app), "crc": crc, "version": version, "entry": entry,
"semver": "%d.%d.%d+%d" % (ma, mi, pa, build)}
Скрипт берёт версию из метаданных самого образа, а не из git: так
заголовок и содержимое не разойдутся. Адрес входа — второе слово таблицы
векторов. CRC-32 — как у zlib: отражённый многочлен 0xEDB88320; ту же
побитовую функцию содержит загрузчик. В опыте: длина 892, CRC
0x6ED38201, версия 0x01040200, вход 0x0802053D.
Загрузчик в опыте — image_valid из 48.4, дополненный сверкой версии
заголовка с метаданными. Склеенный файл прогнан в QEMU трижды:
| Что прошито | Вывод загрузчика | Код выхода |
|---|---|---|
| выпуск как есть | boot: crc ok 0x6ED38201, boot: version 0x01040200, дальше приложение: app: thermo-201a 1.4.2+3 g07234455 |
0 |
| один бит в коде приложения изменён | boot: crc mismatch 0x1AD57894, boot: stay in loader |
1 |
| в заголовке версия 1.5.0 вместо 1.4.2 | boot: crc ok 0x6ED38201, boot: meta does not match header |
1 |
Третий случай показывает, зачем сверять два источника: CRC покрывает только тело образа, и подменённая версия в заголовке её не меняет. Загрузчик ловит это, потому что версию хранит и сам образ. Защиту от намеренной подмены CRC не даёт вовсе — для этого подпись, раздел 48а: она покрывает и заголовок.
Приложение armclang с тем же загрузчиком gcc прошло так же: CRC
0x50F54E62, та же строка версии. Загрузчику всё равно, чем собран образ, —
ему важны заголовок, векторы и метаданные по адресу.
201а.4 Воспроизводимая сборка: два билда — один хеш
Выпуск имеет смысл, только если образ можно пересобрать из метки и получить тот же файл. Тогда хеш в журнале — доказательство, что в приборах ровно этот коммит (раздел 110.8). Опыт: одно и то же приложение собрано дважды в разных каталогах с паузой 2 с — gcc в контейнере, armclang на ПК.
| Опыт | gcc: BIN | gcc: ELF | armclang: BIN | armclang: AXF |
|---|---|---|---|---|
| как есть | совпал | разный | совпал | разный |
с __DATE__ " " __TIME__ в коде |
разный | разный | разный | разный |
то же, SOURCE_DATE_EPOCH=1790000000 |
совпал | разный | совпал | разный |
без даты, -ffile-prefix-map=<каталог>=. |
совпал | совпал | совпал | совпал |
Выводы из таблицы:
- Время сборки ломает сам образ.
__DATE__и__TIME__попадают в константы, и две сборки с разницей в 2 с дают разные BIN. ПеременнаяSOURCE_DATE_EPOCHфиксирует «сейчас» — и gcc 12.2.1, и armclang 6.24 её соблюдают: BIN совпадают. Проще вовсе не класть время в прошивку: номер сборки и хеш коммита говорят больше. - Путь к каталогу сборки ломает только ELF. Он записан в отладочную информацию: в ELF без ключа путь каталога встретился 1 раз, с ключом — 0. В BIN отладочной информации нет, поэтому BIN совпал и без ключа. Но ELF нужен для разбора падений (раздел 35в) — его тоже хранят рядом с выпуском, и он должен совпадать.
- Идентификатор сборки здесь не мешал: ELF с
-ffile-prefix-mapсовпали и без--build-id=none.
Выпускная сборка в build.sh поэтому без даты и с ключом путей:
arm-none-eabi-gcc $F -ffile-prefix-map=$R=. -I/gen $p.c -T $p.ld $L -Wl,-Map=$p.map -o $p.elf 2>&1 || echo "build_failed=$p"
И весь путь целиком — от создания репозитория до склеенного файла —
повторён дважды: хеш коммита 07234455 и SHA-256 склеенного образа
ab347b03… совпали. Коммит повторяется, потому что автор, даты и
концы строк в репозитории опыта закреплены.
201а.5 Один файл на линию: склейка загрузчика и приложения
На линии нет времени прошивать два файла и следить за порядком. Нужен
один образ: загрузчик с 0x08000000, заголовок с 0x08020000,
приложение с 0x08020200. Готового srec_cat на машине нет, поэтому
склейку делает свой merge_fw.py: читает два Intel HEX, ставит между
ними заголовок из 201а.3 и пишет итог в HEX и BIN.
Разбор Intel HEX несложен. Строка — :, длина, адрес (16 бит), тип,
данные, контрольная сумма; сумма всех байтов строки вместе с контрольной
равна нулю по модулю 256. Адреса F407 не помещаются в 16 бит, поэтому
перед данными стоит запись типа 04 — старшие 16 бит адреса. В склеенном
файле таких записей две: :020000040800F2 перед загрузчиком и
:020000040802F0 перед заголовком. Скрипт проверяет и то, чего формат
не запрещает: два разных байта по одному адресу, загрузчик, вылезший за
128 КБ, приложение, собранное не под 0x08020200.
Что получилось:
| Файл | Размер | Что внутри |
|---|---|---|
boot.bin |
628 байт | загрузчик |
app.bin |
892 байта | приложение gcc |
merged.hex |
131 строка, 5644 байта | загрузчик, заголовок, приложение; пустоты нет |
merged.bin |
132476 байт | то же сплошь от 0x08000000, из них 130444 — заполнение 0xFF |
HEX несёт адреса и не хранит пустоту — его и отдают на линию. BIN
привязан к начальному адресу, который надо не забыть указать
программатору, и на 98,5 % состоит из 0xFF. Независимая сверка:
arm-none-eabi-objcopy -I ihex -O binary --gap-fill 0xFF из того же
merged.hex дал файл, совпавший с merged.bin байт в байт.
Склеенный образ прошивается одной командой программатора (раздел 35б, 201.3) — прошивка на линии в этом разделе не запускалась.
201а.6 Когда выпуск запрещён и журнал выпусков
Скрипт выпуска отказывает сам, не полагаясь на внимательность:
| Состояние репозитория | git describe |
Итог |
|---|---|---|
| на метке, дерево чистое | v1.4.2-0-g07234455 |
выпуск |
правка в app.c без коммита |
v1.4.2-0-g07234455-dirty |
отказ |
| коммит после метки | v1.4.2-1-g79396a43 |
отказ |
образ с dirty = 1 в метаданных пришёл на склейку |
— | отказ: «образ собран из дерева с незакоммиченными правками — не выпуск» |
Последняя строка — страховка на случай, если образ собрали в обход скрипта: признак правок записан в самом образе, и склейка его видит.
Каждый выпуск — строка в журнале releases.csv:
version,git,file,date,app_size,app_crc32,merged_bin,merged_sha256
1.4.2+3,07234455,merged.hex,2026-09-28,892,0x6ED38201,132476,ab347b03f7d3c236e136ae4c724b0c22a7938edadb7961fe2cb4956ce6278555
Это другой журнал, не тот, что в 201.7: там строка на плату, здесь —
на выпуск. Связь между ними — версия и CRC: стенд линии читает
метаданные по 0x08020400 и CRC из заголовка по 0x08020008 прямо из
камня и пишет их в строку платы. Через год по серийнику платы находится
строка выпуска, по ней — коммит, а по коммиту пересобирается тот же образ
байт в байт.
Описание выпуска для людей (что изменилось) пишут рядом, по коммитам между метками; скрипт его не заменяет.
201а.7 Грабли
Версия в исходнике, поднятая руками. Через два выпуска в приборе
1.4.2, а в журнале 1.4.3. Версию берут из метки git, в образ она
попадает генератором.
Метаданные «где-то в .rodata». Компоновщик кладёт их по новому
адресу после каждой правки кода; стенд читает мусор. Адрес задаётся
сценарием компоновщика и проверяется ASSERT при сборке.
--gc-sections или armlink выбросили метаданные. На них никто не
ссылается из кода — секция пропадает молча. KEEP в GNU ld, --keep в
armlink и used в атрибуте.
CRC в заголовке совпала, а версия — чужая. CRC покрывает только тело. Версию загрузчик сверяет с метаданными образа, а от намеренной подмены нужна подпись по заголовку и телу (48а).
__DATE__ в прошивке. Две сборки одного коммита — разные файлы, хеш в
журнале ничего не доказывает. Не класть время в образ или задавать
SOURCE_DATE_EPOCH.
BIN совпал — значит, сборка воспроизводима. ELF с путём каталога
сборки при этом разный, и разбор падения по «не тому» ELF врёт в
строках. -ffile-prefix-map и сверка хешей обоих файлов.
Строка HEX через границу 64 КБ. Адрес в строке 16-битный, старшая
половина — из последней записи типа 04. Строку, перешедшую границу,
разные программы могут понять по-разному. merge_fw.py рвёт строку перед
границей и ставит новую запись типа 04 — так неоднозначности нет.
BIN прошит не с того адреса. В BIN адресов нет: приложение,
записанное с 0x08000000, затрёт загрузчик. На линию отдают HEX или
склеенный BIN, который начинается ровно с 0x08000000.
201а.8 Итоги
- Версия — из метки git:
git describeдалv1.4.2-0-g07234455, в образе1.4.2+3и хеш07234455; коммит после метки или правка без коммита — отказ выпуска. - Метаданные — 44 байта по
0x08020400, в 0x200 от таблицы векторов (у F407 таблица 392 байта); адрес задаютapp.ldиapp.sct, у gcc и armclang он один. - Заголовок раздела 48 считает скрипт выпуска по готовому образу: длина
892, CRC
0x6ED38201, версия0x01040200; загрузчик в QEMU пропустил выпуск и остановил образ с одним изменённым битом и образ с чужой версией в заголовке. - Две сборки — один BIN у gcc 12.2.1 и armclang 6.24;
__DATE__это ломает,SOURCE_DATE_EPOCHчинит; ELF совпадают только с-ffile-prefix-map. - Один файл на линию:
merged.hex— 131 строка,merged.bin— 132476 байт, из них 130444 — заполнение0xFF;objcopyиз того же HEX дал тот же BIN. - Журнал выпусков — строка на выпуск: версия, хеш, длина, CRC и SHA-256 склеенного образа; связь с журналом линии — через версию и CRC.
Полные исходники
gen_version.py — version.h из git.
# -*- coding: utf-8 -*-
"""Раздел 201а: version.h из git — версия из метки vX.Y.Z, номер сборки — число коммитов, короткий хеш.
python gen_version.py <каталог репозитория> <куда писать version.h>
Печатает строку git describe и итог. Метка должна быть вида v1.4.2."""
import re, subprocess, sys
def git(repo, *args):
return subprocess.run(["git", "-C", repo] + list(args), capture_output=True, text=True, check=True).stdout.strip()
def describe(repo):
d = git(repo, "describe", "--tags", "--long", "--dirty", "--match", "v[0-9]*", "--abbrev=8")
m = re.fullmatch(r"v(\d+)\.(\d+)\.(\d+)-(\d+)-g([0-9a-f]{8})(-dirty)?", d)
if not m:
raise SystemExit("не разобрал git describe: " + d)
ma, mi, pa, ahead, h, dirty = m.groups()
return {"describe": d, "major": int(ma), "minor": int(mi), "patch": int(pa), "ahead": int(ahead),
"git": h, "dirty": int(bool(dirty)), "build": int(git(repo, "rev-list", "--count", "HEAD"))}
def write_header(v, path):
text = ("/* Сгенерировано gen_version.py из «%s» — не править руками. */\n"
"#define FW_VER_MAJOR %d\n#define FW_VER_MINOR %d\n#define FW_VER_PATCH %d\n"
"#define FW_BUILD %du\n#define FW_GIT \"%s\"\n#define FW_DIRTY %d\n"
% (v["describe"], v["major"], v["minor"], v["patch"], v["build"], v["git"], v["dirty"]))
open(path, "w", encoding="utf-8", newline="\n").write(text)
return text
if __name__ == "__main__":
v = describe(sys.argv[1])
write_header(v, sys.argv[2])
print("describe=%s version=%d.%d.%d+%d git=%s dirty=%d" % (v["describe"], v["major"], v["minor"], v["patch"], v["build"], v["git"], v["dirty"]))
meta.h — метаданные выпуска в фиксированном месте образа.
/* Раздел 201а: метаданные выпуска — в фиксированном месте образа, по смещению 0x200 от таблицы векторов.
* Стенд, загрузчик и программа обновления читают их по адресу, ничего не зная о сборке. */
#ifndef META_H
#define META_H
#include <stdint.h>
#define FW_META_MAGIC 0x444D5746u /* в памяти: 46 57 4D 44 — «FWMD» */
#define FW_META_OFFSET 0x200u /* от начала таблицы векторов приложения */
#define FW_META_LAYOUT 1u /* версия раскладки самой структуры */
typedef struct {
uint32_t magic; /* FW_META_MAGIC — метаданные на месте */
uint16_t layout; /* FW_META_LAYOUT: новые поля — только в конец */
uint16_t size; /* sizeof(fw_meta_t) этой сборки */
uint8_t major, minor, patch; /* версия SemVer */
uint8_t dirty; /* 1 — собрано из дерева с незакоммиченными правками */
uint32_t build; /* номер сборки: число коммитов */
char git[12]; /* короткий хеш коммита, с нулём в конце */
char product[16]; /* что это за прошивка */
} fw_meta_t;
_Static_assert(sizeof(fw_meta_t) == 44u, "раскладка метаданных — 44 байта");
_Static_assert(FW_META_OFFSET % 4u == 0u, "метаданные выровнены на слово");
/* Заголовок перед образом — как в разделе 48 (img_hdr_t), 512 байт. */
#define IMG_MAGIC 0x53544D33u /* «STM3» */
#define IMG_HDR_SIZE 512u
typedef struct {
uint32_t magic, size, crc, version, entry, flags, reserved[2];
uint8_t pad[IMG_HDR_SIZE - 8u * 4u];
} img_hdr_t;
_Static_assert(sizeof(img_hdr_t) == IMG_HDR_SIZE, "заголовок ровно 512 байт");
#define SLOT_A 0x08020000u /* F407: секторы 0…4 — загрузчик 128 КБ, дальше область A (раздел 48) */
#define SLOT_SIZE (384u * 1024u)
#define APP_ORIGIN (SLOT_A + IMG_HDR_SIZE)
#define FW_VERSION(ma, mi, pa) (((uint32_t)(ma) << 24) | ((uint32_t)(mi) << 16) | ((uint32_t)(pa) << 8))
#endif
app.c — приложение: версия и метаданные в образе.
/* Раздел 201а: приложение — версия и метаданные в образе. Печатает их через semihosting и выходит. */
#include <stdint.h>
#include "meta.h"
#include "semi.h"
#include "version.h" /* пишет gen_version.py из git describe */
__attribute__((section(".fw_meta"), used))
const fw_meta_t fw_meta = {
FW_META_MAGIC, FW_META_LAYOUT, (uint16_t)sizeof(fw_meta_t),
FW_VER_MAJOR, FW_VER_MINOR, FW_VER_PATCH, FW_DIRTY,
FW_BUILD, FW_GIT, "thermo-201a",
};
#ifdef WITH_DATE
const char build_stamp[] = __DATE__ " " __TIME__; /* так делать не надо: см. 201а.5 */
#endif
static char *put(char *p, const char *s)
{
while (*s)
*p++ = *s++;
return p;
}
static char *num(char *p, unsigned v)
{
char t[12];
int n = 0;
do {
t[n++] = (char)('0' + v % 10u);
v /= 10u;
} while (v);
while (n)
*p++ = t[--n];
return p;
}
int main(void)
{
char line[96], *p = line;
p = put(p, "app: ");
p = put(p, fw_meta.product);
p = put(p, " ");
p = num(p, fw_meta.major);
p = put(p, ".");
p = num(p, fw_meta.minor);
p = put(p, ".");
p = num(p, fw_meta.patch);
p = put(p, "+");
p = num(p, fw_meta.build);
p = put(p, " g");
p = put(p, fw_meta.git);
p = put(p, fw_meta.dirty ? " dirty\n" : "\n");
*p = 0;
semi_puts(line);
#ifdef WITH_DATE
semi_puts(build_stamp);
semi_puts("\n");
#endif
semi_hex("app: meta at 0x", (unsigned)(uintptr_t)&fw_meta);
semi_exit(0);
for (;;) {
}
}
void Reset_Handler(void)
{
main();
}
static void Fault_Handler(void)
{
semi_puts("app: fault\n");
semi_exit(1);
}
#if defined(__ARMCC_VERSION) /* armlink: вершина пустой области ARM_LIB_STACK из app.sct */
extern uint32_t Image$$ARM_LIB_STACK$$ZI$$Limit;
#define STACK_TOP (&Image$$ARM_LIB_STACK$$ZI$$Limit)
#else /* GNU ld: символ из app.ld */
extern uint32_t __stack_top;
#define STACK_TOP (&__stack_top)
#endif
__attribute__((section(".isr_vector"), used))
const void *const app_vectors[16] = {STACK_TOP, (void *)Reset_Handler, (void *)Fault_Handler, (void *)Fault_Handler};
app.ld — скрипт компоновки приложения.
/* Раздел 201а: приложение в области A F407 — за 512-байтовым заголовком раздела 48.
* Метаданные — ровно по смещению 0x200 от таблицы векторов, что бы ни было в коде. */
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08020200, LENGTH = 384K - 512
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
__stack_top = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.fw_meta ORIGIN(FLASH) + 0x200 : { KEEP(*(.fw_meta)) } > FLASH
.text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH
/DISCARD/ : { *(.ARM.exidx*) *(.comment) }
ASSERT(SIZEOF(.isr_vector) <= 0x200, "vector table overlaps metadata")
ASSERT(ADDR(.fw_meta) == 0x08020400, "metadata is not at 0x08020400")
}
boot.ld — скрипт компоновки загрузчика.
/* Раздел 201а: загрузчик — секторы 0…4 F407, 128 КБ. */
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
__stack_top = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH
/DISCARD/ : { *(.ARM.exidx*) *(.comment) }
}
merge_fw.py — склейка загрузчика, заголовка и приложения в один HEX и BIN.
# -*- coding: utf-8 -*-
"""Раздел 201а: один файл на производство — загрузчик + заголовок раздела 48 + приложение в одном HEX и BIN.
python merge_fw.py boot.hex app.hex merged.hex merged.bin
Заголовок (512 байт перед приложением) считается здесь: длина, CRC-32 приложения, версия из метаданных
по смещению 0x200, адрес входа из таблицы векторов. Разбор Intel HEX — все шесть типов записей."""
import struct, sys, zlib
IMG_MAGIC, HDR, SLOT_A = 0x53544D33, 512, 0x08020000
META_OFF, META_MAGIC = 0x200, 0x444D5746
def read_hex(path):
"""Intel HEX → {адрес: байт}. Сумма всех байт строки вместе с контрольной — ноль."""
mem, base = {}, 0
for n, line in enumerate(open(path, encoding="ascii").read().split(), 1):
if not line.startswith(":"):
raise ValueError("%s:%d: нет двоеточия" % (path, n))
raw = bytes.fromhex(line[1:])
if len(raw) < 5 or len(raw) != raw[0] + 5 or sum(raw) & 0xFF:
raise ValueError("%s:%d: длина или контрольная сумма" % (path, n))
cnt, addr, typ, data = raw[0], raw[1] << 8 | raw[2], raw[3], raw[4:-1]
if typ == 0:
for i, b in enumerate(data):
a = base + addr + i
if a in mem and mem[a] != b:
raise ValueError("%s: два разных байта по адресу 0x%08X" % (path, a))
mem[a] = b
elif typ == 1:
break
elif typ == 2:
base = (data[0] << 8 | data[1]) << 4 # сегментный адрес × 16
elif typ == 4:
base = (data[0] << 8 | data[1]) << 16 # старшая половина линейного адреса
elif typ in (3, 5):
pass # адрес запуска: он и так во втором слове векторов
else:
raise ValueError("%s:%d: тип записи %d" % (path, n, typ))
return mem
def segments(mem):
out, start, buf = [], None, bytearray()
for a in sorted(mem):
if start is not None and a == start + len(buf):
buf.append(mem[a])
else:
if start is not None:
out.append((start, bytes(buf)))
start, buf = a, bytearray([mem[a]])
if start is not None:
out.append((start, bytes(buf)))
return out
def rec(addr, typ, data):
raw = bytes([len(data), addr >> 8 & 0xFF, addr & 0xFF, typ]) + data
return ":" + raw.hex().upper() + "%02X" % (-sum(raw) & 0xFF)
def write_hex(mem, path):
lines, hi = [], None
for start, data in segments(mem):
for off in range(0, len(data), 16):
a = start + off
chunk = data[off:off + 16]
if (a & 0xFFFF) + len(chunk) > 0x10000: # строка не должна пересекать границу 64 КБ
chunk = chunk[:0x10000 - (a & 0xFFFF)]
if a >> 16 != hi:
hi = a >> 16
lines.append(rec(0, 4, struct.pack(">H", hi)))
lines.append(rec(a & 0xFFFF, 0, chunk))
if len(chunk) < len(data[off:off + 16]): # хвост строки — в следующую 64-КБ страницу
rest = data[off + len(chunk):off + 16]
hi = (a + len(chunk)) >> 16
lines.append(rec(0, 4, struct.pack(">H", hi)))
lines.append(rec(0, 0, rest))
lines.append(rec(0, 1, b""))
open(path, "w", encoding="ascii", newline="\n").write("\n".join(lines) + "\n")
return len(lines)
def header_for(app):
"""Заголовок раздела 48 для образа приложения app (bytes, начало — таблица векторов)."""
magic, layout, size, ma, mi, pa, dirty, build = struct.unpack_from("<IHHBBBBI", app, META_OFF)
if magic != META_MAGIC:
raise ValueError("нет метаданных по смещению 0x200")
if dirty:
raise ValueError("образ собран из дерева с незакоммиченными правками — не выпуск")
version = ma << 24 | mi << 16 | pa << 8
entry = struct.unpack_from("<I", app, 4)[0]
crc = zlib.crc32(app) & 0xFFFFFFFF
hdr = struct.pack("<8I", IMG_MAGIC, len(app), crc, version, entry, 0, 0, 0)
return hdr + b"\xFF" * (HDR - len(hdr)), {"size": len(app), "crc": crc, "version": version, "entry": entry,
"semver": "%d.%d.%d+%d" % (ma, mi, pa, build)}
def merge(boot_hex, app_hex):
boot, app = read_hex(boot_hex), read_hex(app_hex)
a0 = min(app)
if a0 != SLOT_A + HDR:
raise ValueError("приложение собрано не под 0x%08X" % (SLOT_A + HDR))
blob = bytes(app.get(a, 0xFF) for a in range(a0, max(app) + 1))
hdr, info = header_for(blob)
if max(boot) >= SLOT_A:
raise ValueError("загрузчик вылез за 128 КБ")
mem = dict(boot)
for i, b in enumerate(hdr + blob):
mem[SLOT_A + i] = b
return mem, info
def to_bin(mem, base=0x08000000, fill=0xFF):
return bytes(mem.get(a, fill) for a in range(base, max(mem) + 1))
if __name__ == "__main__":
mem, info = merge(sys.argv[1], sys.argv[2])
n = write_hex(mem, sys.argv[3])
b = to_bin(mem)
open(sys.argv[4], "wb").write(b)
print("merged_hex_lines=%d merged_bin=%d size=%d crc=0x%08X version=0x%08X entry=0x%08X semver=%s"
% (n, len(b), info["size"], info["crc"], info["version"], info["entry"], info["semver"]))
Дальше: раздел 202 «Калибровка на линии» — стенд уже меряет числа; следующий шаг — превращать их в поправки, зашиваемые в каждую конкретную плату.