Embedded HandbookApps
All sections › XXVI. From prototype to product
STM32F407 · level intermediate · 25 min read
Open in the interactive handbook

201а. Firmware release: version, metadata and one file for production

Section 201 set up the line: the fixture programs the board, runs the self-test and writes a line to the log — with the version of the production firmware and its CRC32. Where that version and that CRC come from was not covered there: the fixture script received them ready-made. This section covers the path from a tag in git to one file that goes to the line: the version and the build number get into the image by themselves and sit at a known place in Flash memory, the bootloader checks the image CRC before starting it, two builds of the same commit produce the same file, and the bootloader and the application are merged into one HEX that is programmed with a single command.

Everything was built and run on a PC: a temporary git repository with the tag v1.4.2, a bootloader and an application for the F407 — arm-none-eabi-gcc 12.2.1 in the image stm-arm:1, the same application — armclang 6.24, the merged file — in qemu-system-arm -M netduinoplus2 (Cortex-M4, output through semihosting, section 35а). The release script release.py walks the whole path and prints the result; the check compares every number in the section against it.

Foundations: the image header and image_valid — section 48 (48.3, 48.4); the signature — section 48а; a repeatable build in the pipeline — section 110 (110.8); the line log — section 201 (201.7). The F407 vector table is startup_stm32f407xx.s from the STM32F4xx_DFP 2.17.1 pack.

Terms used in the documentation

Term Meaning
semantic versioning (SemVer) a version of the form major.minor.patch
build number / build metadata the build number, build metadata (+3 in 1.4.2+3)
annotated / lightweight tag a git tag with author, date and message / a plain tag
git describe --dirty the nearest tag, the number of commits after it, the hash and a modified-tree flag
linker script / scatter file the GNU ld linker script / the armlink layout file
FIXED execution region a region whose execution address equals its load address
reproducible build the same input gives the same image byte for byte
SOURCE_DATE_EPOCH an environment variable: "now" for the compiler, in seconds since 1970
Intel HEX, extended linear address record a text image format; a record of type 04 holds the upper 16 bits of the address
release notes / release log a description of a release / a log of releases

201а.1 The version from git: tag, build number, modified flag

You do not write the version into the source by hand: someone will forget to bump it. The source of truth is a tag in git. The semantic version 1.4.2 is three numbers: the major one changes when compatibility breaks (the exchange format, the settings layout), the minor one when something new is added without breaking anything, the third one for a fix. A build number is added to it — the number of commits in the history — and the short commit hash.

All of this comes from a single command. gen_version.py parses its output and writes the header 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"))}

In the experiment the repository has three commits with a fixed author and dates, and the tag v1.4.2 on the last one. git describe returned v1.4.2-0-g07234455: zero commits after the tag, hash 07234455. The build number is 3, the resulting version string is 1.4.2+3. The generated header:

#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

Two flags distinguish a release from "a build on my desk". A non-zero number after the tag means the commits after the tag have not been released yet. The suffix -dirty means the tree has uncommitted changes, and the image cannot be reproduced from any commit. The release script refuses in both cases (201а.6).


201а.2 Metadata at a fixed place in Flash memory

The version is not only for people. The line fixture has to read it from the programmed chip and compare it with the log; the bootloader has to check it against the header; the update program has to decide whether the incoming image is newer. None of them knows how the firmware was built, and none will search the whole memory for a string. So the metadata sits at a known address — here at offset 0x200 from the start of the application's vector table.

One file for production: on the left, the path from the v1.4.2 tag through version.h, the build and the merge to merged.hex and a log line; on the right, the F407 Flash map — a 628-byte bootloader from 0x08000000, 0xFF fill, a 512-byte header from 0x08020000 with length 892, CRC 0x6ED38201 and version 0x01040200, the vector table from 0x08020200, 44 bytes of metadata from 0x08020400, and the application code

Why 0x200: the F407 vector table in startup_stm32f407xx.s is 98 words, 392 bytes, so it fits into 512 bytes completely. Metadata placed right after it would move every time the number of vectors changed.

The structure is 44 bytes; its first field is a magic number, the second is the version of the layout itself:

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;

It is filled in by the macros from version.h, and an attribute puts it into a separate section:

__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",
};

The address is set by the linker. In GNU ld it is the linker script app.ld; KEEP prevents sections that nothing references from being discarded, and two ASSERT checks stop the build if the vector table grows or the address moves:

  .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")

In armlink the scatter file does the same: the region ER_META with the attribute FIXED sits exactly at its address. armlink provides the top of the stack through the empty region ARM_LIB_STACK — as the symbol 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 { }
}

The result of both builds is the same address: fw_meta at 0x08020400, the vector table at 0x08020200 for both gcc and armclang (armlink with the --keep option for both sections). All 44 bytes of metadata in the two images matched. In the image they look like this:

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 is "FWMD", 2C 00 is the size 44, 01 04 02 is the version, 03 00 00 00 is the build number, followed by the hash 07234455 and the product name. Section sizes with gcc: the vector table is 64 bytes, the metadata 44, code and constants 336; between the vectors and the metadata there are 448 bytes of empty space, and the application image comes out at 892 bytes. With armclang it is 868.

A rule for the structure itself: fields are only added at the end, and layout and size tell the reader how many there are. An old fixture that reads the metadata of new firmware will take the first 44 bytes and get them right.


201а.3 The image CRC: the release script computes it, the bootloader checks it

The metadata says what is programmed; the CRC says whether it is intact. Computing the CRC inside the build is awkward: it depends on the whole image, including itself. So the release script computes it from the finished image and puts it into the header from section 48 — 512 bytes in front of the application:

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)}

The script takes the version from the metadata of the image itself, not from git: this way the header and the contents cannot diverge. The entry address is the second word of the vector table. The CRC-32 is the same as in zlib: the reflected polynomial 0xEDB88320; the bootloader contains the same bitwise function. In the experiment: length 892, CRC 0x6ED38201, version 0x01040200, entry 0x0802053D.

The bootloader in the experiment is image_valid from 48.4, extended with a check of the header version against the metadata. The merged file was run in QEMU three times:

What is programmed Bootloader output Exit code
the release as is boot: crc ok 0x6ED38201, boot: version 0x01040200, then the application: app: thermo-201a 1.4.2+3 g07234455 0
one bit in the application code changed boot: crc mismatch 0x1AD57894, boot: stay in loader 1
version 1.5.0 instead of 1.4.2 in the header boot: crc ok 0x6ED38201, boot: meta does not match header 1

The third case shows why two sources are compared: the CRC covers only the image body, and a substituted version in the header does not change it. The bootloader catches this because the image itself also stores the version. Against deliberate tampering the CRC gives no protection at all — that is what the signature is for, section 48а: it covers the header as well.

The armclang application with the same gcc bootloader passed the same way: CRC 0x50F54E62, the same version string. The bootloader does not care what built the image — what matters to it are the header, the vectors and the metadata at the address.


201а.4 Reproducible build: two builds — one hash

A release makes sense only if the image can be rebuilt from the tag and you get the same file. Then the hash in the log is proof that the devices contain exactly this commit (section 110.8). The experiment: the same application was built twice in different directories with a 2 s pause — gcc in a container, armclang on the PC.

Experiment gcc: BIN gcc: ELF armclang: BIN armclang: AXF
as is matched different matched different
with __DATE__ " " __TIME__ in the code different different different different
the same, SOURCE_DATE_EPOCH=1790000000 matched different matched different
without the date, -ffile-prefix-map=<dir>=. matched matched matched matched

Conclusions from the table:

So the release build in build.sh has no date and uses the path option:

  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"

And the whole path — from creating the repository to the merged file — was repeated twice: the commit hash 07234455 and the SHA-256 of the merged image ab347b03… matched. The commit is repeatable because the author, the dates and the line endings in the experiment's repository are fixed.


201а.5 One file for the line: merging the bootloader and the application

On the line there is no time to program two files and keep track of the order. You need one image: the bootloader from 0x08000000, the header from 0x08020000, the application from 0x08020200. There is no ready-made srec_cat on the machine, so the merge is done by our own merge_fw.py: it reads two Intel HEX files, puts the header from 201а.3 between them, and writes the result as HEX and BIN.

Parsing Intel HEX is not hard. A line is :, length, address (16 bits), type, data, checksum; the sum of all bytes of the line including the checksum is zero modulo 256. F407 addresses do not fit into 16 bits, so the data is preceded by a record of type 04 — the upper 16 bits of the address. The merged file has two such records: :020000040800F2 before the bootloader and :020000040802F0 before the header. The script also checks what the format does not forbid: two different bytes at the same address, a bootloader that spills over 128 KB, an application not built for 0x08020200.

The result:

File Size Contents
boot.bin 628 bytes bootloader
app.bin 892 bytes gcc application
merged.hex 131 lines, 5644 bytes bootloader, header, application; no empty space
merged.bin 132476 bytes the same, contiguous from 0x08000000, of which 130444 is 0xFF fill

The HEX carries addresses and does not store empty space — that is what goes to the line. The BIN is tied to a start address that you must not forget to give to the programmer, and it is 98.5 % 0xFF. An independent check: arm-none-eabi-objcopy -I ihex -O binary --gap-fill 0xFF from the same merged.hex produced a file that matched merged.bin byte for byte.

The merged image is programmed with a single programmer command (section 35б, 201.3) — programming on the line was not run in this section.


201а.6 When a release is forbidden, and the release log

The release script refuses by itself, without relying on anyone's attention:

Repository state git describe Result
on the tag, clean tree v1.4.2-0-g07234455 release
uncommitted change in app.c v1.4.2-0-g07234455-dirty refused
a commit after the tag v1.4.2-1-g79396a43 refused
an image with dirty = 1 in its metadata arrives for merging — refused: "image built from a tree with uncommitted changes — not a release"

The last row is a safeguard in case the image was built bypassing the script: the modified flag is written into the image itself, and the merge sees it.

Every release is a line in the log 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

This is a different log from the one in 201.7: there it is one line per board, here one per release. The link between them is the version and the CRC: the line fixture reads the metadata at 0x08020400 and the CRC from the header at 0x08020008 straight from the chip and writes them into the board's line. A year later, the board's serial number leads to the release line, that leads to the commit, and from the commit the same image is rebuilt byte for byte.

The release notes for people (what changed) are written alongside, from the commits between tags; the script does not replace them.


201а.7 Pitfalls

A version in the source, bumped by hand. Two releases later the device says 1.4.2, and the log says 1.4.3. The version is taken from the git tag, and a generator puts it into the image.

Metadata "somewhere in .rodata". The linker places it at a new address after every code change; the fixture reads garbage. The address is set by the linker script and checked by ASSERT at build time.

--gc-sections or armlink discarded the metadata. Nothing in the code references it — the section disappears silently. KEEP in GNU ld, --keep in armlink and used in the attribute.

The CRC in the header matched, but the version is someone else's. The CRC covers only the body. The bootloader checks the version against the image metadata, and protection against deliberate tampering needs a signature over the header and the body (48а).

__DATE__ in the firmware. Two builds of the same commit are different files, and the hash in the log proves nothing. Do not put the time into the image, or set SOURCE_DATE_EPOCH.

The BIN matched, so the build is reproducible. The ELF with the build directory path is still different, and crash analysis with the "wrong" ELF gives false line numbers. Use -ffile-prefix-map and compare the hashes of both files.

A HEX line across a 64 KB boundary. The address in a line is 16-bit, the upper half comes from the last record of type 04. A line that crosses the boundary may be interpreted differently by different programs. merge_fw.py breaks the line before the boundary and inserts a new record of type 04 — so there is no ambiguity.

A BIN programmed at the wrong address. A BIN has no addresses: an application written from 0x08000000 will overwrite the bootloader. The line gets a HEX or a merged BIN that starts exactly at 0x08000000.


201а.8 Summary

Full sources

gen_version.py — version.h from 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 — release metadata at a fixed place in the image.

/* Раздел 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 — the application: version and metadata in the image.

/* Раздел 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 — the application's linker script.

/* Раздел 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 — the bootloader's linker script.

/* Раздел 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 — merging the bootloader, the header and the application into one HEX and 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"]))

Next: section 202 "Calibration on the production line" — the fixture already measures numbers; the next step is to turn them into corrections programmed into each individual board.

Open in the interactive handbook