From 1f6b2e71601ab9fadc69cf993df8ef985f718db0 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:50:15 -0500 Subject: [PATCH] Add the Pimoroni Pico Plus 2 W as a variant of RPI_PICO2_W rp2 variants of ours now work: build_mp.py generates a board directory that links upstream's board unchanged and adds the variant's mpconfigvariant.cmake, mpconfigboard.h and pins.csv, the way it already does for esp32. PIMORONI_PICO_PLUS2_W builds the Pico 2 W for the board's 16 MB flash (13 MB filesystem), 8 MB PSRAM on GP47, 48 GPIOs and its own pin names, with the RM2's PIO clock divider written out. It has an optional release cell beside the Pico 2's. --- .github/workflows/release-firmware.yml | 2 +- README.md | 7 +- build_mp.py | 55 ++++++++++-- docs/newcomers.md | 20 +++++ scripts/release_firmware.py | 5 +- test_build_mp.py | 54 ++++++++++++ test_release_firmware.py | 4 +- .../PIMORONI_PICO_PLUS2_W/mpconfigboard.h | 9 ++ .../mpconfigvariant.cmake | 42 ++++++++++ .../PIMORONI_PICO_PLUS2_W/pins.csv | 84 +++++++++++++++++++ 10 files changed, 270 insertions(+), 12 deletions(-) create mode 100644 variants/rp2/RPI_PICO2_W/PIMORONI_PICO_PLUS2_W/mpconfigboard.h create mode 100644 variants/rp2/RPI_PICO2_W/PIMORONI_PICO_PLUS2_W/mpconfigvariant.cmake create mode 100644 variants/rp2/RPI_PICO2_W/PIMORONI_PICO_PLUS2_W/pins.csv diff --git a/.github/workflows/release-firmware.yml b/.github/workflows/release-firmware.yml index 8779e12c..0a02fbeb 100644 --- a/.github/workflows/release-firmware.yml +++ b/.github/workflows/release-firmware.yml @@ -33,7 +33,7 @@ on: default: false type: boolean pico2: - description: Build the Raspberry Pi Pico 2 too (off until a board is on the bench) + description: Build the Raspberry Pi Pico 2 and the Pimoroni Pico Plus 2 W too (off until a board is on the bench) default: false type: boolean lvgl-circuitpython-ref: diff --git a/README.md b/README.md index 5f4f62e1..18fd7a1a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ one command: --flash 16MB --modules displayif,pygraphics,/home/you/earful ``` -Boards are upstream's own generic ones; the +Boards are upstream's own, with a variant of ours where a board needs one; the [newcomer's guide](docs/newcomers.md#boards-we-build-for) has the command for each board we use. You name the modules you want: ours by short name, anything else by its path, or `all`. Leave out `--port` and it @@ -95,7 +95,10 @@ New here? The [newcomer's guide](docs/newcomers.md) explains the model; the module and a Fetch-backed `requests`), and `esp32/`: the sdkconfig fragments and partition table every esp32 or every chip gets, and the few board variants of ours (`ESP32_GENERIC_S3` `LCD_4_3`, `LCD_7` and `T_EMBED`, - `ESP32_GENERIC_P4` `WIFI6_DEV_KIT` and `WIFI6_DEV_KIT_H2`). + `ESP32_GENERIC_P4` `WIFI6_DEV_KIT` and `WIFI6_DEV_KIT_H2`); and `rp2/` + (`RPI_PICO2_W` `PIMORONI_PICO_PLUS2_W`, the Pimoroni Pico Plus 2 W with its + 16 MB flash, 8 MB PSRAM and 48 GPIOs: + `--port rp2 --board RPI_PICO2_W --variant PIMORONI_PICO_PLUS2_W`). - [`scripts/exercise_build.sh`](scripts/exercise_build.sh) — builds every desktop port and two esp32 boards from a bare clone, checks the failure paths, and writes a report of what should match on any machine. Needs no board. For a report on builds you diff --git a/build_mp.py b/build_mp.py index 7735973e..476529a1 100755 --- a/build_mp.py +++ b/build_mp.py @@ -740,19 +740,23 @@ def has_coredump_partition(table): return any(re.match(r"\s*[^#,\s][^,]*,\s*data\s*,\s*coredump\s*,", line) for line in table.read_text().splitlines()) +def link_board(gen, base, generated): + """Link everything upstream's board holds into gen, except the files the + caller generates there: ${MICROPY_BOARD_DIR} is an include directory and + board cmake files name files in it.""" + for entry in sorted(base.iterdir()): + if entry.name not in generated: + (gen / entry.name).symlink_to(entry.resolve()) + + def esp32_board_dir(gen, base, board, variant, fragment_path, module_dirs, components=()): """Upstream's board, unchanged, plus our fragment, as BOARD_DIR= (the way - cmods' build_mp.sh did it). Everything else the board holds is linked - through, because ${MICROPY_BOARD_DIR} is an include directory and board - cmake files name files in it.""" + cmods' build_mp.sh did it).""" if gen.exists(): shutil.rmtree(gen) gen.mkdir(parents=True) vfile = f"mpconfigvariant_{variant}.cmake" if variant else "mpconfigvariant.cmake" - generated = {"mpconfigboard.cmake", "mpconfigboard.h", vfile} - for entry in sorted(base.iterdir()): - if entry.name not in generated: - (gen / entry.name).symlink_to(entry.resolve()) + link_board(gen, base, {"mpconfigboard.cmake", "mpconfigboard.h", vfile}) lines = ["# Generated by build_mp.py: the board below, unchanged.", f"include({base.as_posix()}/mpconfigboard.cmake)"] for d in module_dirs: comps = d / "components" @@ -787,6 +791,39 @@ def esp32_board_dir(gen, base, board, variant, fragment_path, module_dirs, compo (gen / vfile).write_text("\n".join(vlines) + "\n") +def rp2_board_dir(gen, base, board, variant): + """Upstream's board, unchanged, plus our variant's files, as BOARD_DIR=. + The port takes BOARD_VARIANT= only when the board holds an + mpconfigvariant_.cmake, and builds the pin names from the board's + pins.csv, so the generated directory holds ours: a variant file that + includes upstream's default variant and then ours, our pins.csv when we + have one (it replaces upstream's whole), and the board's header followed by + ours (upstream's defines its name and hostname without #ifndef, so ours + has to come after to #undef them).""" + ours = board_variant_dir("rp2", board, variant) + if not ours: + die(f"{board} has no variant {variant}") + vfile = f"mpconfigvariant_{variant}.cmake" + if (base / vfile).exists(): + die(f"{board} has a variant {variant} of its own") + if gen.exists(): + shutil.rmtree(gen) + gen.mkdir(parents=True) + own = {n for n in ("mpconfigboard.h", "pins.csv") if (ours / n).exists()} + link_board(gen, base, {vfile} | own) + if "pins.csv" in own: + (gen / "pins.csv").symlink_to((ours / "pins.csv").resolve()) + if "mpconfigboard.h" in own: + (gen / "mpconfigboard.h").write_text( + "// Generated by build_mp.py: the board's own header, then the variant's.\n" + f'#include "{(base / "mpconfigboard.h").as_posix()}"\n' + f'#include "{(ours / "mpconfigboard.h").as_posix()}"\n') + (gen / vfile).write_text( + "# Generated by build_mp.py: the board's default variant, then ours.\n" + f"include({base.as_posix()}/mpconfigvariant.cmake OPTIONAL)\n" + f"include({(ours / 'mpconfigvariant.cmake').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 @@ -1744,6 +1781,10 @@ def shell(cmd, log=None): if args.coredump: say("This is a troubleshooting build: a crash writes a core dump to the 'coredump' partition.") elif port == "rp2": + if board_variant_dir(port, board, variant): + gen = build / board + rp2_board_dir(gen, board_dir(port, board), board, variant) + make += [f"BOARD_DIR={gen}"] shell(make + ["submodules"]) storage = None for attempt in (1, 2): diff --git a/docs/newcomers.md b/docs/newcomers.md index 0bf684df..e00e905f 100644 --- a/docs/newcomers.md +++ b/docs/newcomers.md @@ -181,6 +181,7 @@ on top. Add `--modules` to any of these: | WT32-SC01 Plus | `--port esp32 --board ESP32_GENERIC_S3 --flash 16MB` | | Raspberry Pi Pico (RP2040) | `--port rp2 --board RPI_PICO` (not `--modules all`: see [The model](#the-model)) | | 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` | A new board usually needs no file at all: pick the upstream board and variant that match its chip and PSRAM, and give `--flash` its flash size. Make @@ -317,6 +318,25 @@ The partition table is found the same way, the most specific coredump partition on the S3, which a standard build leaves unused); without one, the board's own is used. Either way autosize grows the app partition when the image doesn't fit. +## rp2 settings + +An rp2 board of ours is a variant of one of upstream's, in +`variants/rp2///`. The port only knows variants that sit in +the board's own folder, so `build_mp.py` makes a folder of its own for the +build, `builds/rp2////`, links upstream's board into it +unchanged and adds what the variant holds: + +- `mpconfigvariant.cmake` (required): cmake settings, included after the + board's own default variant. The board's `PICO_BOARD`, the number of GPIOs, + PSRAM and the size of the filesystem go here. +- `mpconfigboard.h`: C defines, included after the board's own header, so it + can `#undef` what the board sets, such as its name. +- `pins.csv`: the pin names, in place of the board's. + +`PIMORONI_PICO_PLUS2_W` is the one so far. It builds the Pico 2 W for the +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. + ## Where things are | Path | What it is | diff --git a/scripts/release_firmware.py b/scripts/release_firmware.py index 1226045a..2c8f3824 100755 --- a/scripts/release_firmware.py +++ b/scripts/release_firmware.py @@ -6,7 +6,7 @@ release_firmware.py matrix [--pico2] Print the build matrix as JSON for the workflow: one row per cell. - The Pico 2's row is left out unless --pico2 is given. + The Pico 2's and the Pico Plus 2 W's rows are left out unless --pico2 is given. release_firmware.py build [--ws DIR] Build one cell with build_mp.py and put its files, a sha256 of each and @@ -96,6 +96,9 @@ dict(id="mp-pico2", label="Raspberry Pi Pico 2", interpreter="micropython", port="rp2", board="RPI_PICO2", variant=None, flash="4MB", modules="all", chip="rp2350", offset=None, kind="rp2", deps="none", optional=True), + dict(id="mp-pico-plus2-w", label="Pimoroni Pico Plus 2 W", interpreter="micropython", port="rp2", + board="RPI_PICO2_W", variant="PIMORONI_PICO_PLUS2_W", flash="16MB", modules="all", + chip="rp2350", offset=None, kind="rp2", deps="none", optional=True), ] # What each kind of cell must hand over, as (role, file in the build dir, required). diff --git a/test_build_mp.py b/test_build_mp.py index be591206..ed11b3c5 100644 --- a/test_build_mp.py +++ b/test_build_mp.py @@ -434,6 +434,60 @@ def test_variant_manifests(self): build_mp.VARIANTS_DIR, build_mp.MODULES_DIR = saved +class Rp2BoardDir(unittest.TestCase): + """A variant of ours on rp2 gets a board directory: upstream's, linked + through unchanged, with our files added where the port looks for them.""" + + 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.cmake", "set(A 1)"), ("mpconfigboard.h", "#define NAME 1"), + ("mpconfigvariant.cmake", "set(P 1)"), ("pins.csv", "GP0,GPIO0"), ("board.json", "{}")): + (self.base / name).write_text(text) + self.ours = root / "variants" / "rp2" / "BOARD" / "OURS" + self.ours.mkdir(parents=True) + (self.ours / "mpconfigvariant.cmake").write_text("set(Q 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.rp2_board_dir(self.gen, self.base, "BOARD", "OURS") + for name in ("mpconfigboard.cmake", "mpconfigboard.h", "mpconfigvariant.cmake", "pins.csv", "board.json"): + 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.cmake").read_text() + self.assertIn(f"include({self.base.as_posix()}/mpconfigvariant.cmake OPTIONAL)", text) + self.assertLess(text.index("mpconfigvariant.cmake OPTIONAL"), text.index(self.ours.as_posix())) + self.assertIn(f"include({self.ours.as_posix()}/mpconfigvariant.cmake)", text) + + def test_our_pins_and_header_replace_and_follow_upstreams(self): + (self.ours / "pins.csv").write_text("GP47,GPIO47") + (self.ours / "mpconfigboard.h").write_text("#undef NAME") + build_mp.rp2_board_dir(self.gen, self.base, "BOARD", "OURS") + self.assertEqual((self.gen / "pins.csv").resolve(), (self.ours / "pins.csv").resolve()) + header = (self.gen / "mpconfigboard.h").read_text() + self.assertFalse((self.gen / "mpconfigboard.h").is_symlink()) + self.assertLess(header.index(self.base.as_posix()), header.index(self.ours.as_posix())) + self.assertEqual((self.base / "pins.csv").read_text(), "GP0,GPIO0") # upstream untouched + + def test_regenerates_over_an_old_directory(self): + build_mp.rp2_board_dir(self.gen, self.base, "BOARD", "OURS") + (self.ours / "pins.csv").write_text("GP47,GPIO47") + build_mp.rp2_board_dir(self.gen, self.base, "BOARD", "OURS") + self.assertEqual((self.gen / "pins.csv").resolve(), (self.ours / "pins.csv").resolve()) + + def test_a_variant_that_is_not_ours_is_refused(self): + with self.assertRaises(SystemExit): + build_mp.rp2_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/test_release_firmware.py b/test_release_firmware.py index 180a9d5b..c2cf969d 100644 --- a/test_release_firmware.py +++ b/test_release_firmware.py @@ -62,9 +62,11 @@ class Table(unittest.TestCase): def test_ten_cells_by_default_and_the_pico2_only_when_asked(self): self.assertEqual(len(rf.matrix(False)["include"]), 10) ids = [c["id"] for c in rf.matrix(True)["include"]] - self.assertEqual(len(ids), 11) + self.assertEqual(len(ids), 12) self.assertIn("mp-pico2", ids) + self.assertIn("mp-pico-plus2-w", ids) self.assertNotIn("mp-pico2", [c["id"] for c in rf.matrix(False)["include"]]) + self.assertNotIn("mp-pico-plus2-w", [c["id"] for c in rf.matrix(False)["include"]]) self.assertEqual(len(set(ids)), len(ids)) def test_five_micropython_and_five_circuitpython(self): diff --git a/variants/rp2/RPI_PICO2_W/PIMORONI_PICO_PLUS2_W/mpconfigboard.h b/variants/rp2/RPI_PICO2_W/PIMORONI_PICO_PLUS2_W/mpconfigboard.h new file mode 100644 index 00000000..b85c8d11 --- /dev/null +++ b/variants/rp2/RPI_PICO2_W/PIMORONI_PICO_PLUS2_W/mpconfigboard.h @@ -0,0 +1,9 @@ +// Pimoroni Pico Plus 2 W, after upstream's RPI_PICO2_W header (which defines +// these without #ifndef, hence the #undef). + +#undef MICROPY_HW_BOARD_NAME +#define MICROPY_HW_BOARD_NAME "Pimoroni Pico Plus 2 W" + +// What the board calls itself on a network, as the stock Pico 2 W does with its own name. +#undef MICROPY_PY_NETWORK_HOSTNAME_DEFAULT +#define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "PicoPlus2W" diff --git a/variants/rp2/RPI_PICO2_W/PIMORONI_PICO_PLUS2_W/mpconfigvariant.cmake b/variants/rp2/RPI_PICO2_W/PIMORONI_PICO_PLUS2_W/mpconfigvariant.cmake new file mode 100644 index 00000000..4c0295eb --- /dev/null +++ b/variants/rp2/RPI_PICO2_W/PIMORONI_PICO_PLUS2_W/mpconfigvariant.cmake @@ -0,0 +1,42 @@ +# Pimoroni Pico Plus 2 W (PIM726) on top of upstream's RPI_PICO2_W: an RP2350B +# (QFN-80, 48 GPIOs) with 16 MB of flash, 8 MB of PSRAM and an RM2 radio. +# build_mp.py includes this after upstream's own default variant, which sets +# PICO_PLATFORM for the RP2350. + +# The SDK's own header for this board. It is what puts the 16 MB flash size, +# the 8 MB PSRAM and its chip select (GP47), the user switch (GP45), the +# Qw/ST I2C bus (GP4/GP5) and the RM2's pins (REG_ON GP23, data and host wake +# GP24, CS GP25, clock GP29, LED on the radio's GPIO 0) into the build. These +# are the same pins CircuitPython's pimoroni_pico_plus2w sets by hand. +set(PICO_BOARD "pimoroni_pico_plus2_w_rp2350") + +# The QFN-80 package has 48 GPIOs where the Pico 2 W's RP2350A has 30. This also +# picks the RP2350B alternate-function table for the pin names. +set(PICO_NUM_GPIOS 48) + +# 8 MB of PSRAM on GP47. The port brings it up at boot and adds it to the +# heap beside the on-chip RAM. The SDK header gives the size; the chip select +# must agree with its PICO_PSRAM_CS_PIN or main.c refuses to build. +set(MICROPY_HW_ENABLE_PSRAM 1) +set(MICROPY_HW_PSRAM_CS_PIN 47) + +# 13 MB of the 16 MB flash is the filesystem, and the firmware gets the other +# 3 MB: the image with every module in it is 2.9 MB on the Pico 2 W. A +# smaller build can ask for more with MICROPY_HW_FLASH_STORAGE_BYTES; one +# that doesn't fit is shrunk by build_mp.py. Upstream's board sets this +# for 4 MB of flash before this file runs, so only a value given on the +# command line (a cache entry) may keep it. +if(NOT DEFINED CACHE{MICROPY_HW_FLASH_STORAGE_BYTES}) + set(MICROPY_HW_FLASH_STORAGE_BYTES 13631488) # 13 * 1024 * 1024 +endif() + +# The RM2 talks to the RP2350 over PIO at the system clock divided by 2, the +# divider CircuitPython uses on this board (CYW43_PIO_CLOCK_DIV_INT=2) and the +# SDK's default. It is written out so a change in the SDK can't move it; raise +# it if the radio turns unreliable at a higher system clock. The pins and the +# radio's three GPIOs (CYW43_WL_GPIO_COUNT, the LED on 0) come from the header +# above, so CircuitPython's -DCYW43_DEFAULT_PIN_WL_* flags have nothing to add. +list(APPEND MICROPY_DEF_BOARD + CYW43_PIO_CLOCK_DIV_INT=2 + CYW43_PIO_CLOCK_DIV_FRAC8=0 +) diff --git a/variants/rp2/RPI_PICO2_W/PIMORONI_PICO_PLUS2_W/pins.csv b/variants/rp2/RPI_PICO2_W/PIMORONI_PICO_PLUS2_W/pins.csv new file mode 100644 index 00000000..aa483647 --- /dev/null +++ b/variants/rp2/RPI_PICO2_W/PIMORONI_PICO_PLUS2_W/pins.csv @@ -0,0 +1,84 @@ +# Pin names for the Pimoroni Pico Plus 2 W. This file replaces the Pico 2 W's, +# which names 26 of the 30 pins of an RP2350A, so every GPIO of the RP2350B is here. +# machine.Pin(n) takes any of them by number whether or not it has a name. +# GP23, GP24, GP25 and GP29 are wired to the RM2 radio (below): leave them to it. +# GP30 to GP39, GP44 and GP46 are not connected to anything on the board. +# GP47 is the PSRAM chip select. +GP0,GPIO0 +GP1,GPIO1 +GP2,GPIO2 +GP3,GPIO3 +GP4,GPIO4 +GP5,GPIO5 +GP6,GPIO6 +GP7,GPIO7 +GP8,GPIO8 +GP9,GPIO9 +GP10,GPIO10 +GP11,GPIO11 +GP12,GPIO12 +GP13,GPIO13 +GP14,GPIO14 +GP15,GPIO15 +GP16,GPIO16 +GP17,GPIO17 +GP18,GPIO18 +GP19,GPIO19 +GP20,GPIO20 +GP21,GPIO21 +GP22,GPIO22 +GP23,GPIO23 +GP24,GPIO24 +GP25,GPIO25 +GP26,GPIO26 +GP27,GPIO27 +GP28,GPIO28 +GP29,GPIO29 +GP30,GPIO30 +GP31,GPIO31 +GP32,GPIO32 +GP33,GPIO33 +GP34,GPIO34 +GP35,GPIO35 +GP36,GPIO36 +GP37,GPIO37 +GP38,GPIO38 +GP39,GPIO39 +GP40,GPIO40 +GP41,GPIO41 +GP42,GPIO42 +GP43,GPIO43 +GP44,GPIO44 +GP45,GPIO45 +GP46,GPIO46 +GP47,GPIO47 + +# The user switch, named as CircuitPython names it. It is on GP45. +USER_SW,GPIO45 +BUTTON,GPIO45 + +# GP40 to GP43 have analog input; GP43 also reads VSYS through a divider (CircuitPython's names). +A0,GPIO40 +A1,GPIO41 +A2,GPIO42 +A3,GPIO43 +VOLTAGE_MONITOR,GPIO43 + +# The Qw/ST connector's I2C bus, I2C0. CircuitPython names the bus itself (I2C, +# STEMMA_I2C) rather than its pins, which a pin table can't do: these two name the pins, +# and machine.I2C(0) already defaults to them. +I2C_SDA,GPIO4 +I2C_SCL,GPIO5 + +# The RM2's gSPI lines. GP24 is both data and host wake. +WL_REG_ON,GPIO23 +WL_DATA,GPIO24 +WL_CS,GPIO25 +WL_CLK,GPIO29 + +# The RM2's own GPIOs. The user LED is on the first; the third senses VBUS (CircuitPython's VBUS_SENSE). +WL_GPIO0,EXT_GPIO0 +WL_GPIO1,EXT_GPIO1 +WL_GPIO2,EXT_GPIO2 +LED,EXT_GPIO0 +VBUS_SENSE,EXT_GPIO2