From 129b553bbb92a2ec805da061bc0e3139151b3c68 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:55:26 -0600 Subject: [PATCH] build_mp: a GIGA build picks its M4 boot padding from 4 KB, not four sizes On a GIGA whose BCM4 option bit is set, the M4 starts from the bytes at 0x08100000, so a build refuses an image whose bytes there point at memory and pads the code before that address until they don't. It tried 16, 32, 48 and 64 bytes, which move the address only 24 to 72 bytes through the code. An image whose address landed in a literal pool of flash addresses (the end of mp_builtin_help here) was refused at every size, and the build failed: audiodsp 6d08b07 grew the code before the address by 1,456 bytes and put it there. Now the padding function is always compiled, so the build knows where it is, and every padding from 0 to 4,096 bytes in 16-byte steps is read off the image already linked: growing the pad by a multiple of .text's largest alignment (8) moves everything after it by exactly that much. The build relinks once with the smallest padding whose bytes pass, checks them, and tries again (up to four times) only if the relink didn't put there what the image said. Windows 16 bytes apart over 4 KB can't all sit in one literal pool. The GIGA DISPLAY_SHIELD build with audiodsp 6d08b07, which failed with "none of 16, 32, 48, 64 bytes of padding moved it clear", now prints "m4 pad: 96 bytes" after one relink, and the bytes at 0x08100000 are stack 0xf7e84809, reset 0x3501f82c, HardFault 0xbf18429d. Over 4 KB of growth either way from that image, the smallest padding that passes is never more than 224 bytes. tests/test_build_mp.py passes (97 tests). --- build_mp.py | 118 ++++++++++++++++++++++++++++++----------- tests/test_build_mp.py | 100 +++++++++++++++++++++++++++++----- 2 files changed, 174 insertions(+), 44 deletions(-) diff --git a/build_mp.py b/build_mp.py index 464fac7..e6a3a66 100755 --- a/build_mp.py +++ b/build_mp.py @@ -1168,25 +1168,34 @@ def m4_reaches_memory(sp, pc, nmi, hf): return any(lo <= w < hi for w in ((sp - 4) & 0xFFFFFFFF, pc & ~1, hf & ~1) for lo, hi in M4_BUS_REGIONS) -#: Padding tried, in bytes, when an image's M4 vector table reaches memory. -M4_PAD_SIZES = (16, 32, 48, 64) +#: Paddings tried step by this many bytes: a multiple of every alignment in +#: .text (8 at most on the GIGA), so a pad that grows by it moves all the code +#: after it on by just as much, and the bytes it brings to the M4's address can +#: be read off the image already linked. +M4_PAD_STEP = 16 +#: The most padding tried. The windows it can bring to the M4's address lie 16 +#: bytes apart over 4 KB, so no literal pool or table of addresses (a few dozen +#: bytes) can refuse them all, however the image grows around them. +M4_PAD_MAX = 4096 +#: Relinks tried before giving up, when the bytes read off the image weren't +#: what the relink put there. +M4_PAD_TRIES = 4 M4_PAD_SYMBOL = "pydevices_m4_pad" def stm32_m4_pad_module(build, pad): - """A generated C module whose only code is *pad* bytes of nops, kept by - the linker and placed with the other modules, before the M4's address: - it moves the code there. With pad 0 it compiles nothing. Returns its - directory, for USER_C_MODULES; files are rewritten only when they change.""" + """A generated C module whose only code is a function with *pad* bytes of + nops, kept by the linker and placed with the other modules, before the M4's + address: growing it moves the code there. Returns its directory, for + USER_C_MODULES; files are rewritten only when they change.""" d = build / "m4pad-src" / "m4pad" d.mkdir(parents=True, exist_ok=True) - mk = "# Generated by build_mp.py: padding that moves the code at the M4's boot address.\n" - c = "// Generated by build_mp.py: padding that moves the code at the M4's boot address.\n" - if pad: - mk += f"SRC_USERMOD_C += $(USERMOD_DIR)/m4pad.c\nLDFLAGS_USERMOD += -Wl,-u,{M4_PAD_SYMBOL}\n" - c += (f"void {M4_PAD_SYMBOL}(void);\n" - f"void {M4_PAD_SYMBOL}(void) {{\n" - f" __asm volatile (\".rept {pad // 2}\\n nop\\n .endr\");\n}}\n") + mk = ("# Generated by build_mp.py: padding that moves the code at the M4's boot address.\n" + f"SRC_USERMOD_C += $(USERMOD_DIR)/m4pad.c\nLDFLAGS_USERMOD += -Wl,-u,{M4_PAD_SYMBOL}\n") + c = ("// Generated by build_mp.py: padding that moves the code at the M4's boot address.\n" + f"void {M4_PAD_SYMBOL}(void);\n" + f"void {M4_PAD_SYMBOL}(void) {{\n" + f" __asm volatile (\".rept {pad // 2}\\n nop\\n .endr\");\n}}\n") for name, text in (("micropython.mk", mk), ("m4pad.c", c)): f = d / name if not f.exists() or f.read_text() != text: @@ -1201,22 +1210,70 @@ def stm32_m4_pad_now(build): return int(m.group(1)) * 2 if m else 0 -def stm32_m4_settle(image, text, pad, relink): +def elf_symbol(elf, name): + """The value of symbol *name* in a 32-bit little-endian ELF file, or None.""" + data = Path(elf).read_bytes() if Path(elf).exists() else b"" + if data[:6] != b"\x7fELF\x01\x01": + return None + shoff, = struct.unpack_from(" len(data): + continue + if not m4_reaches_memory(*struct.unpack_from(" make's exit code for each. Returns (pad, words, - refused words or None), or (None, words, None) when relink failed.""" - first = None - for p in [pad] + [q for q in (0,) + M4_PAD_SIZES if q != pad]: - if p != pad: - rc = relink(p) - if rc != 0: - return None, None, first - words = stm32_m4_boot_words(image, text) + memory. Keeps the current *pad* when its table passes; otherwise picks the + smallest padding whose table would pass, read off the image + (stm32_m4_pad_candidates), and calls relink(pad) -> make's exit code, up to + M4_PAD_TRIES times. *pad_at* () returns the padding function's address in + the image linked now. Returns (pad, words, refused words or None), or + (None, None, refused words) when relink failed.""" + words = stm32_m4_boot_words(image, text) + if not (words and m4_reaches_memory(*words)): + return pad, words, None + first, tried = words, {pad} + for _ in range(M4_PAD_TRIES): + p = next((q for q in stm32_m4_pad_candidates(image, text, pad, pad_at()) or () if q not in tried), None) + if p is None: + break + tried.add(p) + if relink(p) != 0: + return None, None, first + pad, words = p, stm32_m4_boot_words(image, text) if not (words and m4_reaches_memory(*words)): - return p, words, first - first = first or words - pad = p + return pad, words, first return pad, words, first @@ -2296,15 +2353,16 @@ def relink(p): (build / "firmware.elf").unlink(missing_ok=True) return shell(make, log=True)[0] - pad, words, refused = stm32_m4_settle(image, STM32_M4_SLOT[board][1], pad_before, relink) + pad, words, refused = stm32_m4_settle(image, STM32_M4_SLOT[board][1], pad_before, + lambda: elf_symbol(build / "firmware.elf", M4_PAD_SYMBOL), relink) if pad is None: rc = 1 elif words and m4_reaches_memory(*words): die(f"{image.name} hands the M4 core a vector table that reaches memory (stack {hex(words[0])}, " f"reset {hex(words[1])}, HardFault {hex(words[3])}) at {hex(STM32_M4_BOOT_DEFAULT)}, where " "the M4 of a board as shipped starts at reset on its own (BOOT_CM4_ADD0, with BCM4 set), " - f"and none of {', '.join(str(p) for p in M4_PAD_SIZES)} bytes of padding moved it clear. " - "Change the build a little, or turn BCM4 off on your board: the flash guide says how.") + f"and no padding up to {M4_PAD_MAX:,} bytes before it moved it clear. Look at what " + f"{build / 'firmware.map'} puts there, or turn BCM4 off on your board: the flash guide says how.") else: stm32_m4_pad = pad if refused: diff --git a/tests/test_build_mp.py b/tests/test_build_mp.py index 45f8657..8f525e9 100644 --- a/tests/test_build_mp.py +++ b/tests/test_build_mp.py @@ -816,46 +816,118 @@ def test_a_stack_in_memory_is_refused_whatever_the_reset_address(self): def test_the_m7s_tightly_coupled_memory_is_out_of_the_m4s_reach(self): self.assertFalse(build_mp.m4_reaches_memory(0x20010000, 0x00000101, 0, 0x20004791)) + # Where the padding function sits in the fake images below, well before the + # M4's address, as it does in a real GIGA image. + PAD_AT = 0x080405ED + + def _code(self, windows): + """An image whose code before the M4's address is *windows*, 16-byte + vector tables in order, the last one at the address.""" + off = build_mp.STM32_M4_BOOT_DEFAULT - 0x08040000 + body = b"".join(struct.pack("