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 ( +3in1.4.2+3)annotated / lightweight tag a git tag with author, date and message / a plain tag git describe --dirtythe 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_EPOCHan 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.
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:
- The build time breaks the image itself.
__DATE__and__TIME__end up in constants, and two builds 2 s apart give different BINs. The variableSOURCE_DATE_EPOCHfixes "now" — both gcc 12.2.1 and armclang 6.24 honor it: the BINs match. It is simpler not to put the time into the firmware at all: the build number and the commit hash say more. - The path to the build directory breaks only the ELF. It is written into the debug information: in the ELF without the option the directory path occurred 1 time, with the option 0. A BIN has no debug information, so the BIN matched even without the option. But the ELF is needed for crash analysis (section 35в) — it is also kept next to the release, and it must match too.
- The build ID did not get in the way here: the ELFs built with
-ffile-prefix-mapmatched even without--build-id=none.
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
- The version comes from the git tag:
git describegavev1.4.2-0-g07234455, the image holds1.4.2+3and the hash07234455; a commit after the tag or a change without a commit means the release is refused. - The metadata is 44 bytes at
0x08020400, 0x200 from the vector table (the F407 table is 392 bytes); the address is set byapp.ldandapp.sct, and it is the same for gcc and armclang. - The release script computes the section 48 header from the finished image: length
892, CRC
0x6ED38201, version0x01040200; in QEMU the bootloader let the release through and stopped the image with one changed bit and the image with a wrong version in the header. - Two builds give one BIN with gcc 12.2.1 and armclang 6.24;
__DATE__breaks this,SOURCE_DATE_EPOCHfixes it; the ELFs match only with-ffile-prefix-map. - One file for the line:
merged.hex— 131 lines,merged.bin— 132476 bytes, of which 130444 is0xFFfill;objcopyfrom the same HEX gave the same BIN. - The release log has one line per release: version, hash, length, CRC and the SHA-256 of the merged image; the link to the line log goes through the version and the CRC.
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.