From 8d554e7c6fa3ad54dc2d0745c98838a2c47a7a39 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:12:42 -0600 Subject: [PATCH 1/2] modules.lock: audiodsp 6d08b07, the reverb tank at -O2 in -Os firmware Moves audiodsp to audiodsp#399. In size-optimised firmware, the classic ESP32's for one, the reverb tank now compiles for speed: 4.7 to 4.0 ms per 48 kHz block at 160 MHz, with output identical to the bit (400 random configurations against the old code). With the AUDIO_KIT variant at 240 MHz (#374) the seven-note soundmodule run from apps#44 plays with its Reverb and no dropouts. --- modules.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules.lock b/modules.lock index 77d07b3..ea3b014 100644 --- a/modules.lock +++ b/modules.lock @@ -19,7 +19,7 @@ # sibling checkout is linked there instead) and compiles the port for its # interpreter. Move the commit here to move every LVGL build, MicroPython's and # CircuitPython's. -audiodsp https://github.com/PyDevices/audiodsp.git 1a387e5abc19923d5dcad864e072c1799a1a8253 +audiodsp https://github.com/PyDevices/audiodsp.git 6d08b0784939fa122ccb0dabeda5816bd8f70597 omnipython-lvgl https://github.com/PyDevices/omnipython-lvgl.git c34fbeeb0440e71acb62c92ddee1c3c65d089622 pygraphics https://github.com/PyDevices/pygraphics.git f97fd2e20e73f23c2138ab52a630cf04b0a7aea1 ulab https://github.com/v923z/micropython-ulab.git 1d3ddd8f5228e2c7335dac71f4846e6abb0ec9f9 From 8021afb9cb9a320c962ef4967e8b08d9ebf38c69 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 2/2] 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 be0d87c..5384900 100755 --- a/build_mp.py +++ b/build_mp.py @@ -1112,25 +1112,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: @@ -1145,22 +1154,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 @@ -2240,15 +2297,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 63adab0..79c21c8 100644 --- a/tests/test_build_mp.py +++ b/tests/test_build_mp.py @@ -781,46 +781,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("