diff --git a/build_mp.py b/build_mp.py index ab79e821..ef4d0afe 100755 --- a/build_mp.py +++ b/build_mp.py @@ -554,9 +554,11 @@ def board_dir(port, board): def board_variant_dir(port, board, variant): - """Ours, variants////, when it holds a variant file.""" + """Ours, variants////, when it holds a variant file + (mpconfigvariant.cmake on a CMake port, mpconfigvariant.mk on stm32).""" d = VARIANTS_DIR / port / board / variant if board and variant else None - return d if d and (d / "mpconfigvariant.cmake").exists() else None + found = d and any((d / f).exists() for f in ("mpconfigvariant.cmake", "mpconfigvariant.mk")) + return d if found else None def variants(port, board): @@ -905,6 +907,42 @@ def rp2_board_dir(gen, base, board, variant): f"include({(ours / 'mpconfigvariant.cmake').as_posix()})\n") +def stm32_board_dir(gen, base, board, variant): + """Upstream's board, unchanged, plus our variant's files, as BOARD_DIR=. + As on rp2, the port takes BOARD_VARIANT= only when the board holds an + mpconfigvariant_.mk, so the generated directory holds one that + includes ours. A header of ours named like one of the board's + (mpconfigboard.h, stm32h7xx_hal_conf.h) comes after the board's, so it + can add to it or #undef what it sets. Any other file of ours (a linker + script fragment, say) is linked in beside the board's, where + $(BOARD_DIR) finds it.""" + ours = board_variant_dir("stm32", board, variant) + if not ours or not (ours / "mpconfigvariant.mk").exists(): + die(f"{board} has no variant {variant}") + vfile = f"mpconfigvariant_{variant}.mk" + if (base / vfile).exists(): + die(f"{board} has a variant {variant} of its own") + extra = sorted(p for p in ours.iterdir() if p.name not in ("mpconfigvariant.mk", "manifest.py")) + for p in extra: + if (base / p.name).exists() and p.suffix != ".h": + die(f"{p.relative_to(VARIANTS_DIR)} would replace the board's own {p.name}") + if gen.exists(): + shutil.rmtree(gen) + gen.mkdir(parents=True) + link_board(gen, base, {vfile} | {p.name for p in extra}) + for p in extra: + if (base / p.name).exists(): + (gen / p.name).write_text( + "// Generated by build_mp.py: the board's own header, then the variant's.\n" + f'#include "{(base / p.name).as_posix()}"\n' + f'#include "{p.resolve().as_posix()}"\n') + else: + (gen / p.name).symlink_to(p.resolve()) + (gen / vfile).write_text( + "# Generated by build_mp.py: the variant's settings, after the board's.\n" + f"include {(ours / 'mpconfigvariant.mk').as_posix()}\n") + + # TinyUSB, for the esp32 targets that build it. The port takes it from the # IDF component manager, which fetches it into managed_components/ at # configure time, so the overlay can't patch it. Instead a copy at the same @@ -1876,6 +1914,10 @@ def shell(cmd, log=None): got = re.search(r"^MICROPY_HW_FLASH_STORAGE_BYTES:\w+=(\d+)$", cache.read_text(), re.M) if cache.exists() else None rp2_storage = int(got.group(1)) if rc == 0 and got else None else: + if port == "stm32" and board_variant_dir(port, board, variant): + gen = build / board + stm32_board_dir(gen, board_dir(port, board), board, variant) + make += [f"BOARD_DIR={gen}"] shell(make + ["submodules"]) if port == "windows" and os.name != "nt": # cross-built: failure_hint() reads the log rc, out = shell(make, log=True) @@ -1907,7 +1949,8 @@ def shell(cmd, log=None): rec["tinyusb"] = git_out(tinyusb, "describe", "--tags", "--always") rec_path.write_text(json.dumps(rec, indent=1) + "\n") say(f"\nBuilt into {build}") - for name in ("firmware.bin", "firmware.uf2", "micropython", "micropython.exe", "micropython.mjs", "micropython.wasm"): + for name in ("firmware.bin", "firmware0.bin", "firmware1.bin", "firmware.dfu", "firmware.uf2", + "micropython", "micropython.exe", "micropython.mjs", "micropython.wasm"): if (build / name).exists(): say(f" {build / name}") diff --git a/docs/guides/add-a-board-variant.md b/docs/guides/add-a-board-variant.md index 95513160..961711c4 100644 --- a/docs/guides/add-a-board-variant.md +++ b/docs/guides/add-a-board-variant.md @@ -61,6 +61,24 @@ unchanged and adds what the variant holds: Pimoroni board's 16 MB flash, 8 MB PSRAM, 48 GPIOs and its own pin names, and its files say why each setting is there. +## An stm32 board + +An stm32 variant of ours works the same way, in +`variants/stm32///`, with `make` files instead of cmake ones. +`build_mp.py` makes `builds/stm32////` from upstream's +board, unchanged, and adds: + +- `mpconfigvariant.mk` (required): make settings, included after the board's + `mpconfigboard.mk`, so it can change what the board sets. +- A header named like one of the board's (`mpconfigboard.h`, + `stm32h7xx_hal_conf.h`): included after the board's own. +- Any other file, such as a linker script fragment: put beside the board's + files, where the variant's settings reach it as `$(BOARD_DIR)/`. + +`ARDUINO_GIGA`'s `DISPLAY_SHIELD` is the one stm32 variant of ours. It moves +the Wi-Fi chip's firmware into QSPI flash with a linker script fragment and +turns on the HAL's DSI driver for displayif's `mipidsi`. + ## Desktop variants The unix and windows ports take our `pydevices` and `vst3-engine` variants, and diff --git a/docs/guides/flash-a-board.md b/docs/guides/flash-a-board.md index 46ed150a..69db3ae9 100644 --- a/docs/guides/flash-a-board.md +++ b/docs/guides/flash-a-board.md @@ -20,6 +20,42 @@ esptool --chip esp32s3 write-flash 0x0 builds/esp32/ESP32_GENERIC_S3/SPIRAM_OCT/ Hold BOOTSEL while you plug the board in, and copy `firmware.uf2` to the drive it shows. +## STM32 (Arduino GIGA R1 WiFi) + +The `DISPLAY_SHIELD` build makes two images: `firmware0.bin` for the +microcontroller's flash and `firmware1.bin`, the Wi-Fi chip's firmware, for +the QSPI flash. Put the board in its bootloader, by running +`machine.bootloader()` at its REPL or double-tapping RESET, and write both with +[dfu-util](https://dfu-util.sourceforge.net/), the Wi-Fi firmware first: + +```bash +cd builds/stm32/ARDUINO_GIGA/DISPLAY_SHIELD +dfu-util -a 1 -d 2341:0366 -s 0x90F00000 -D firmware1.bin +dfu-util -a 0 -d 2341:0366 -s 0x08040000:leave -D firmware0.bin +``` + +`firmware1.bin` only changes when the Wi-Fi driver does, so after the first +time you can write `firmware0.bin` alone. + +The OmniPython libraries don't fit in this firmware beside LVGL, so they go on +the board after it, in its 4 MB ROMFS partition, where MicroPython runs them in +place and finds them before `/flash/lib`. From a clone of +[pydevices](https://github.com/PyDevices/pydevices), deploy its `lib/` folder +with [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html): + +```bash +git clone --depth 1 https://github.com/PyDevices/pydevices.git +mpremote romfs deploy pydevices/lib +``` + +mpremote compiles each `.py` to `.mpy` on the way when the `mpy-cross` Python +package is installed (`pip install mpy-cross`; its 1.29 release writes `.mpy` +files this firmware reads). Without it, mpremote copies the `.py` files and the +board compiles them as they're imported. A deploy replaces the whole partition, +so the board's own drivers go in the same folder: +[the GIGA Display Shield's board page](https://github.com/PyDevices/pydevices/blob/main/docs/boards/display-boards.md#arduino-giga-r1-wifi--giga-display-shield) +has that recipe. + ## With mpftp mpftp writes either file for you, given the build's port and your board's diff --git a/docs/reference/boards.md b/docs/reference/boards.md index 62f8273e..47958767 100644 --- a/docs/reference/boards.md +++ b/docs/reference/boards.md @@ -18,6 +18,7 @@ top. Add `--modules` to any of these: | Raspberry Pi Pico (RP2040) | `--port rp2 --board RPI_PICO` (not `--modules all`: [it doesn't fit](../guides/fit-firmware-on-a-small-flash.md)) | | Raspberry Pi Pico 2 (RP2350) | `--port rp2 --board RPI_PICO2` | | Pimoroni Pico Plus 2 W (16 MB flash, 8 MB PSRAM, 48 GPIOs) | `--port rp2 --board RPI_PICO2_W --variant PIMORONI_PICO_PLUS2_W` | +| Arduino GIGA R1 WiFi with the GIGA Display Shield | `--port stm32 --board ARDUINO_GIGA --variant DISPLAY_SHIELD --modules displayif,lvgl` ([two images to flash](../guides/flash-a-board.md#stm32-arduino-giga-r1-wifi)) | Our ESP32-S3 builds need an S3 with PSRAM: they keep the TLS library in PSRAM, and on an S3 without it, Wi-Fi can't join a WPA2 network. Most S3 boards have @@ -40,6 +41,7 @@ Our variants, in [`variants/`](https://github.com/PyDevices/micropython-pydevice | esp32 | `ESP32_GENERIC_S3` `LCD_4_3`, `LCD_7`, `T_EMBED` | The S3 boards in the table above. | | esp32 | `ESP32_GENERIC_P4` `WIFI6_DEV_KIT`, `WIFI6_DEV_KIT_H2` | The P4 DEV-KIT, with the USB device on the two different connectors in the table above. | | rp2 | `RPI_PICO2_W` `PIMORONI_PICO_PLUS2_W` | The Pimoroni Pico Plus 2 W. | +| stm32 | `ARDUINO_GIGA` `DISPLAY_SHIELD` | The GIGA R1 WiFi with room for LVGL and the shield's MIPI DSI panel: the Wi-Fi firmware moved to QSPI flash as a second image, Bluetooth off, LVGL's 40 px font left out. | The esp32 [`variants/esp32/`](https://github.com/PyDevices/micropython-pydevices/tree/main/variants/esp32) folder also holds the `sdkconfig` fragments and partition table every esp32 or every chip gets: diff --git a/modules.lock b/modules.lock index 5c95cab9..4609d039 100644 --- a/modules.lock +++ b/modules.lock @@ -20,7 +20,7 @@ # interpreter. Move the commit here to move every LVGL build, MicroPython's and # CircuitPython's. audiodsp https://github.com/PyDevices/audiodsp.git 14be12c475f73423cfd422128e26874966984111 -omnipython-lvgl https://github.com/PyDevices/omnipython-lvgl.git e2254f39a8e548be3e9b886dfef3f84d2155aa27 +omnipython-lvgl https://github.com/PyDevices/omnipython-lvgl.git 00bfbab3a7a28515b1da694deac5ccaab28a3c96 pygraphics https://github.com/PyDevices/pygraphics.git f97fd2e20e73f23c2138ab52a630cf04b0a7aea1 ulab https://github.com/v923z/micropython-ulab.git 1d3ddd8f5228e2c7335dac71f4846e6abb0ec9f9 omnipython-lib https://github.com/PyDevices/pydevices.git 10b54173abc8644005f46f1e4b3ba394d4c843f5 diff --git a/patches/micropython/0048-micropython-stm32-link-objects-from-a-response-file.patch b/patches/micropython/0048-micropython-stm32-link-objects-from-a-response-file.patch new file mode 100644 index 00000000..158e57e1 --- /dev/null +++ b/patches/micropython/0048-micropython-stm32-link-objects-from-a-response-file.patch @@ -0,0 +1,41 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> +Date: Fri, 6 Nov 2026 00:00:00 -0500 +Subject: [PATCH] stm32/Makefile: Link the objects from a response file. + +A firmware with LVGL as a user C module has over a thousand objects, and +with the build directory's path in front of each, the link command passes +the 128 KB that Linux allows a single argument. make hands the recipe to +/bin/sh as one argument, and it stops with "Argument list too long". An +Arduino GIGA R1 build with displayif and LVGL failed this way. + +Write the objects to $(BUILD)/firmware.elf.objs and pass that to gcc as +@file, as py/mkrules.mk already does on Windows. $(file) needs GNU make 4.0, +so an older make keeps the command line as it was. + +--- a/ports/stm32/Makefile ++++ b/ports/stm32/Makefile +@@ -572,9 +572,19 @@ + $(Q)$(OPENOCD) -f $(OPENOCD_CONFIG) -c "stm_flash $(1) $(2) $(3) $(4)" + endef + ++# The objects for the link go in a response file, which gcc reads as @file: ++# with large user C modules (LVGL) the command line passes the 128 KB a single ++# argument may hold on Linux, and /bin/sh refuses it. $(file) needs GNU make ++# 4.0; an older make keeps the command line as it was. ++ifeq ($(filter 3.%,$(MAKE_VERSION)),) ++LINK_OBJ = $(file >$(1).objs,$(2))@$(1).objs ++else ++LINK_OBJ = $(2) ++endif ++ + define GENERATE_ELF + $(ECHO) "LINK $(1)" +- $(Q)$(CC) $(LDFLAGS) -o $(1) $(2) $(LDFLAGS_MOD) $(LIBS) ++ $(Q)$(CC) $(LDFLAGS) -o $(1) $(call LINK_OBJ,$(1),$(2)) $(LDFLAGS_MOD) $(LIBS) + $(Q)$(SIZE) $(1) + $(if $(filter-out $(TEXT0_ADDR),0x08000000), \ + $(ECHO) "INFO: this build requires mboot to be installed first") +-- +2.43.0 + diff --git a/provenance.json b/provenance.json index c2f18b2c..df86d9f8 100644 --- a/provenance.json +++ b/provenance.json @@ -139,6 +139,9 @@ }, "0047-micropython-py-scheduler-no-wait-with-a-callback-ready.patch": { "sha256": "b01cd7d260563082e788e0e160e51e3179f127a86fa323755732cfd5a45cffc8" + }, + "0048-micropython-stm32-link-objects-from-a-response-file.patch": { + "sha256": "3c1dd4732af29c9164615dddea2eb12740e503a723be854e83bd65c830a77cbb" } }, "tinyusb_patches": { diff --git a/tests/test_build_mp.py b/tests/test_build_mp.py index da76c7c5..2f715060 100644 --- a/tests/test_build_mp.py +++ b/tests/test_build_mp.py @@ -538,6 +538,61 @@ def test_a_variant_that_is_not_ours_is_refused(self): build_mp.rp2_board_dir(self.gen, self.base, "BOARD", "NOPE") +class Stm32BoardDir(unittest.TestCase): + """A variant of ours on stm32 gets a board directory: upstream's, linked + through unchanged, a variant file that includes ours, our headers after + the board's, and our other files beside the board's.""" + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + root = Path(self.tmp.name) + self.base = root / "mp" / "BOARD" + self.base.mkdir(parents=True) + for name, text in (("mpconfigboard.mk", "A = 1"), ("mpconfigboard.h", "#define NAME 1"), + ("stm32h7xx_hal_conf.h", "// hal"), ("board.ld", "/* ld */"), ("manifest.py", "")): + (self.base / name).write_text(text) + self.ours = root / "variants" / "stm32" / "BOARD" / "OURS" + self.ours.mkdir(parents=True) + (self.ours / "mpconfigvariant.mk").write_text("B = 2") + self.gen = root / "gen" + self.saved = build_mp.VARIANTS_DIR + build_mp.VARIANTS_DIR = root / "variants" + + def tearDown(self): + build_mp.VARIANTS_DIR = self.saved + self.tmp.cleanup() + + def test_variant_file_only(self): + build_mp.stm32_board_dir(self.gen, self.base, "BOARD", "OURS") + for name in ("mpconfigboard.mk", "mpconfigboard.h", "stm32h7xx_hal_conf.h", "board.ld"): + self.assertEqual(self.gen.joinpath(name).resolve(), self.base.joinpath(name).resolve(), name) + self.assertTrue(self.gen.joinpath(name).is_symlink(), name) + text = (self.gen / "mpconfigvariant_OURS.mk").read_text() + self.assertIn(f"include {self.ours.as_posix()}/mpconfigvariant.mk", text) + + def test_our_headers_follow_the_boards_and_other_files_sit_beside_them(self): + (self.ours / "stm32h7xx_hal_conf.h").write_text("#define MORE 1") + (self.ours / "extra.ld").write_text("/* ours */") + (self.ours / "manifest.py").write_text("") + build_mp.stm32_board_dir(self.gen, self.base, "BOARD", "OURS") + header = (self.gen / "stm32h7xx_hal_conf.h").read_text() + self.assertFalse((self.gen / "stm32h7xx_hal_conf.h").is_symlink()) + self.assertLess(header.index(self.base.as_posix()), header.index(self.ours.as_posix())) + self.assertEqual((self.gen / "extra.ld").resolve(), (self.ours / "extra.ld").resolve()) + # The variant's manifest is the build's FROZEN_MANIFEST, not a board file. + self.assertEqual((self.gen / "manifest.py").resolve(), (self.base / "manifest.py").resolve()) + self.assertEqual((self.base / "stm32h7xx_hal_conf.h").read_text(), "// hal") # upstream untouched + + def test_a_file_of_ours_may_not_replace_one_of_the_boards(self): + (self.ours / "board.ld").write_text("/* ours */") + with self.assertRaises(SystemExit): + build_mp.stm32_board_dir(self.gen, self.base, "BOARD", "OURS") + + def test_a_variant_that_is_not_ours_is_refused(self): + with self.assertRaises(SystemExit): + build_mp.stm32_board_dir(self.gen, self.base, "BOARD", "NOPE") + + class CircuitPythonFrozenLibraries(unittest.TestCase): """A CircuitPython build that freezes our Python freezes asyncio and adafruit_ticks too, unless the board already does.""" diff --git a/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/cyw43_qspi.ld b/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/cyw43_qspi.ld new file mode 100644 index 00000000..84ab1011 --- /dev/null +++ b/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/cyw43_qspi.ld @@ -0,0 +1,18 @@ +/* Arduino GIGA R1 WiFi with the GIGA Display Shield: the Wi-Fi chip's firmware + in the QSPI flash the board reserves for it, read in place by the driver. + Linked after the board's own stm32h747.ld. */ + +MEMORY +{ + FLASH_CYW43 (r) : ORIGIN = 0x90F00000, LENGTH = 1024K +} + +SECTIONS +{ + .cyw43_fw : + { + . = ALIGN(4); + KEEP(*(.cyw43_fw*)) + . = ALIGN(4); + } > FLASH_CYW43 +} diff --git a/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/mpconfigboard.h b/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/mpconfigboard.h new file mode 100644 index 00000000..6e36c527 --- /dev/null +++ b/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/mpconfigboard.h @@ -0,0 +1,9 @@ +// Arduino GIGA R1 WiFi with the GIGA Display Shield: included after +// ARDUINO_GIGA's own header. See mpconfigvariant.mk beside this file. + +#undef MICROPY_HW_BOARD_NAME +#define MICROPY_HW_BOARD_NAME "Arduino GIGA R1 WiFi with GIGA Display Shield" + +// The Wi-Fi chip's firmware goes in its own section, which the linker places +// in QSPI flash (cyw43_qspi.ld) and the build writes out as firmware1.bin. +#define CYW43_RESOURCE_ATTRIBUTE __attribute__((section(".cyw43_fw"), aligned(4))) diff --git a/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/mpconfigvariant.mk b/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/mpconfigvariant.mk new file mode 100644 index 00000000..81212dc7 --- /dev/null +++ b/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/mpconfigvariant.mk @@ -0,0 +1,25 @@ +# Arduino GIGA R1 WiFi with the GIGA Display Shield: upstream's ARDUINO_GIGA, +# plus what LVGL and the shield's MIPI DSI panel need, in the board's 1280 KB +# firmware slot. build_mp.py includes this after the board's own +# mpconfigboard.mk. + +# The Wi-Fi chip's firmware (about 420 KB) moves out of internal flash into the +# megabyte of QSPI flash the board reserves for it at 0x90F00000, where the +# driver reads it in place. It is a second image, firmware1.bin, flashed once +# with dfu-util's alt setting 1; firmware0.bin is the rest, for internal flash. +LD_FILES += $(BOARD_DIR)/cyw43_qspi.ld +TEXT1_ADDR = 0x90F00000 +TEXT0_SECTIONS = .isr_vector .isr_extratext .text .gc.blocks.table .data .ARM +TEXT1_SECTIONS = .cyw43_fw + +# Bluetooth off, to make room for LVGL and displayif. +MICROPY_PY_BLUETOOTH = 0 +MICROPY_BLUETOOTH_NIMBLE = 0 + +# LVGL without its 40 px font, to make room as well. A program that wants it +# can load omnipython-lvgl's montserrat_40.bin at run time. +CFLAGS += -DLV_FONT_MONTSERRAT_40=0 + +# The DSI host and LTDC drivers that displayif's mipidsi uses. The +# stm32h7xx_hal_conf.h beside this file enables them. +HAL_SRC_C += $(addprefix $(STM32LIB_HAL_BASE)/Src/stm32$(MCU_SERIES)xx_, hal_dsi.c hal_ltdc.c) diff --git a/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/stm32h7xx_hal_conf.h b/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/stm32h7xx_hal_conf.h new file mode 100644 index 00000000..b6652418 --- /dev/null +++ b/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/stm32h7xx_hal_conf.h @@ -0,0 +1,12 @@ +// Arduino GIGA R1 WiFi with the GIGA Display Shield: included after +// ARDUINO_GIGA's own HAL configuration. displayif's mipidsi drives the +// shield's panel through the DSI host and the LTDC. + +#ifndef PYDEVICES_GIGA_DISPLAY_SHIELD_HAL_CONF_H +#define PYDEVICES_GIGA_DISPLAY_SHIELD_HAL_CONF_H + +#define HAL_DSI_MODULE_ENABLED +#include "stm32h7xx_hal_dsi.h" +#include "stm32h7xx_hal_ltdc.h" + +#endif