Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions build_mp.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,9 +554,11 @@ def board_dir(port, board):


def board_variant_dir(port, board, variant):
"""Ours, variants/<port>/<BOARD>/<VARIANT>/, when it holds a variant file."""
"""Ours, variants/<port>/<BOARD>/<VARIANT>/, 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):
Expand Down Expand Up @@ -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_<V>.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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}")

Expand Down
18 changes: 18 additions & 0 deletions docs/guides/add-a-board-variant.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<BOARD>/<VARIANT>/`, with `make` files instead of cmake ones.
`build_mp.py` makes `builds/stm32/<BOARD>/<VARIANT>/<BOARD>/` 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)/<name>`.

`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
Expand Down
36 changes: 36 additions & 0 deletions docs/guides/flash-a-board.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/boards.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion modules.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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

3 changes: 3 additions & 0 deletions provenance.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
55 changes: 55 additions & 0 deletions tests/test_build_mp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
18 changes: 18 additions & 0 deletions variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/cyw43_qspi.ld
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 9 additions & 0 deletions variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/mpconfigboard.h
Original file line number Diff line number Diff line change
@@ -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)))
25 changes: 25 additions & 0 deletions variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/mpconfigvariant.mk
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 12 additions & 0 deletions variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/stm32h7xx_hal_conf.h
Original file line number Diff line number Diff line change
@@ -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
Loading