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

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 от начала таблицы векторов приложения.

Один файл на производство: слева путь от метки v1.4.2 через version.h, сборку и склейку к merged.hex и строке журнала, справа карта флеш-памяти F407 — загрузчик 628 байт с 0x08000000, заполнение 0xFF, заголовок 512 байт с 0x08020000 с длиной 892, CRC 0x6ED38201 и версией 0x01040200, таблица векторов с 0x08020200, метаданные 44 байта с 0x08020400 и код приложения

Почему 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=<каталог>=. совпал совпал совпал совпал

Выводы из таблицы:

Выпускная сборка в 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 Итоги

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

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 «Калибровка на линии» — стенд уже меряет числа; следующий шаг — превращать их в поправки, зашиваемые в каждую конкретную плату.

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