From 3349046c5d0d4963e20f37e6ac0181f869557315 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:56:05 +1200 Subject: [PATCH] [rp2] Record the RP2350 die on generated board entries (#18305) --- esphome/components/rp2/boards.jinja2 | 4 + esphome/components/rp2/boards.py | 48 +++++++++++ esphome/components/rp2/generate_boards.py | 60 ++++++++++---- .../components/test_rp2_generate_boards.py | 83 ++++++++++++++++++- 4 files changed, 175 insertions(+), 20 deletions(-) diff --git a/esphome/components/rp2/boards.jinja2 b/esphome/components/rp2/boards.jinja2 index 9223009c267..6e5e55d7719 100644 --- a/esphome/components/rp2/boards.jinja2 +++ b/esphome/components/rp2/boards.jinja2 @@ -14,6 +14,10 @@ RP2_BOARD_PINS = { {%- endfor %} } +# RP2350 boards carry a {{ rp2350_die_key | repr }} key holding the die letter: +# 'A' for the RP2350A (GPIO 0-29, 5 ADC channels), 'B' for the RP2350B +# (GPIO 0-47, 9 ADC channels), and None when the die is a build-time menu +# choice and so is not known here. The key is absent on non-RP2350 boards. BOARDS = { {%- for name, info in boards %} {{ name | repr }}: { diff --git a/esphome/components/rp2/boards.py b/esphome/components/rp2/boards.py index d2502b8fb84..4b2f9769b01 100644 --- a/esphome/components/rp2/boards.py +++ b/esphome/components/rp2/boards.py @@ -1533,6 +1533,10 @@ RP2_BOARD_PINS = { }, } +# RP2350 boards carry a 'die' key holding the die letter: +# 'A' for the RP2350A (GPIO 0-29, 5 ADC channels), 'B' for the RP2350B +# (GPIO 0-47, 9 ADC channels), and None when the die is a build-time menu +# choice and so is not known here. The key is absent on non-RP2350 boards. BOARDS = { "0xcb_helios": { "name": "0xCB Helios", @@ -1548,6 +1552,7 @@ BOARDS = { "name": "MyMakers RP2350B", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "MyRP_bot": { "name": "MyMakers RP2040", @@ -1588,11 +1593,13 @@ BOARDS = { "name": "Adafruit Feather RP2350 Adalogger", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "adafruit_feather_rp2350_hstx": { "name": "Adafruit Feather RP2350 HSTX", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "adafruit_feather_scorpio": { "name": "Adafruit Feather RP2040 SCORPIO", @@ -1618,6 +1625,7 @@ BOARDS = { "name": "Adafruit Fruit Jam RP2350", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "adafruit_itsybitsy": { "name": "Adafruit ItsyBitsy RP2040", @@ -1643,6 +1651,7 @@ BOARDS = { "name": "Adafruit Metro RP2350", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "adafruit_qtpy": { "name": "Adafruit QT Py RP2040", @@ -1763,16 +1772,19 @@ BOARDS = { "name": "iLabs Challenger 2350 BConnect", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "challenger_2350_nbiot": { "name": "iLabs Challenger 2350 NB-IoT", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "challenger_2350_wifi6_ble5": { "name": "iLabs Challenger 2350 WiFi/BLE", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "challenger_nb_2040_wifi": { "name": "iLabs Challenger NB 2040 WiFi", @@ -1788,6 +1800,7 @@ BOARDS = { "name": "Cytron IRIV IO Controller", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "cytron_maker_nano_rp2040": { "name": "Cytron Maker Nano RP2040", @@ -1808,6 +1821,7 @@ BOARDS = { "name": "Cytron Motion 2350 Pro", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "datanoisetv_picoadk": { "name": "DatanoiseTV PicoADK", @@ -1818,6 +1832,7 @@ BOARDS = { "name": "DatanoiseTV PicoADK v2", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "degz_suibo": { "name": "Degz Robotics Suibo RP2040", @@ -1863,6 +1878,7 @@ BOARDS = { "name": "Generic RP2350", "mcu": "rp2350", "max_pin": 47, + "die": None, }, "groundstudio_marble_pico": { "name": "GroundStudio Marble Pico", @@ -1873,6 +1889,7 @@ BOARDS = { "name": "iLabs CPico 2350", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "ilabs_rpico32": { "name": "iLabs RPICO32", @@ -1888,6 +1905,7 @@ BOARDS = { "name": "Architeuthis Flux Jumperless V5", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "melopero_cookie_rp2040": { "name": "Melopero Cookie RP2040", @@ -1928,16 +1946,19 @@ BOARDS = { "name": "Olimex Pico2BB48", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "olimex_pico2xl": { "name": "Olimex Pico2XL", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "olimex_pico2xxl": { "name": "Olimex Pico2XXL", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "olimex_rp2040pico30": { "name": "Olimex RP2040-Pico30", @@ -1963,6 +1984,7 @@ BOARDS = { "name": "Pimoroni Explorer", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "pimoroni_pga2040": { "name": "Pimoroni PGA2040", @@ -1973,16 +1995,19 @@ BOARDS = { "name": "Pimoroni PGA2350", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "pimoroni_pico_plus_2": { "name": "Pimoroni PicoPlus2", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "pimoroni_pico_plus_2w": { "name": "Pimoroni PicoPlus2W", "mcu": "rp2350", "max_pin": 47, + "die": "B", "wifi": True, "max_virtual_pin": 64, }, @@ -1995,11 +2020,13 @@ BOARDS = { "name": "Pimoroni Plasma2350", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "pimoroni_plasma2350w": { "name": "Pimoroni Plasma2350W", "mcu": "rp2350", "max_pin": 29, + "die": "A", "wifi": True, }, "pimoroni_servo2040": { @@ -2016,6 +2043,7 @@ BOARDS = { "name": "Pimoroni Tiny2350", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "pintronix_pinmax": { "name": "Pintronix PinMax", @@ -2046,11 +2074,13 @@ BOARDS = { "name": "Raspberry Pi Pico 2", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "rpipico2w": { "name": "Raspberry Pi Pico 2W", "mcu": "rp2350", "max_pin": 29, + "die": "A", "wifi": True, "max_virtual_pin": 64, }, @@ -2085,6 +2115,7 @@ BOARDS = { "name": "Seeed XIAO RP2350", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "silicognition_rp2040_shim": { "name": "Silicognition RP2040-Shim", @@ -2100,6 +2131,7 @@ BOARDS = { "name": "Soldered Electronics NULA RP2350", "mcu": "rp2350", "max_pin": 47, + "die": "B", "wifi": True, }, "solderparty_rp2040_stamp": { @@ -2111,21 +2143,25 @@ BOARDS = { "name": "Solder Party RP2350 Stamp", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "solderparty_rp2350_stamp_xl": { "name": "Solder Party RP2350 Stamp XL", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "sparkfun_iotnode_lorawanrp2350": { "name": "SparkFun IoT Node LoRaWAN", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "sparkfun_iotredboard_rp2350": { "name": "SparkFun IoT RedBoard RP2350", "mcu": "rp2350", "max_pin": 47, + "die": "B", "wifi": True, }, "sparkfun_micromodrp2040": { @@ -2142,6 +2178,7 @@ BOARDS = { "name": "SparkFun ProMicro RP2350", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "sparkfun_thingplusrp2040": { "name": "SparkFun Thing Plus RP2040", @@ -2152,6 +2189,7 @@ BOARDS = { "name": "SparkFun Thing Plus RP2350", "mcu": "rp2350", "max_pin": 29, + "die": "A", "wifi": True, "max_virtual_pin": 64, }, @@ -2159,6 +2197,7 @@ BOARDS = { "name": "SparkFun XRP Controller", "mcu": "rp2350", "max_pin": 47, + "die": "B", "wifi": True, "max_virtual_pin": 64, }, @@ -2233,32 +2272,38 @@ BOARDS = { "name": "Waveshare RP2350 LCD 0.96", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "waveshare_rp2350_pizero": { "name": "Waveshare RP2350 PiZero", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "waveshare_rp2350_plus": { "name": "Waveshare RP2350 Plus", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "waveshare_rp2350_zero": { "name": "Waveshare RP2350 Zero", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "waveshare_rp2350b_plus_w": { "name": "Waveshare RP2350B Plus W", "mcu": "rp2350", "max_pin": 47, + "die": "B", "wifi": True, }, "weact_rp2350b": { "name": "WeAct Studio RP2350B Core Board", "mcu": "rp2350", "max_pin": 47, + "die": "B", }, "wiznet_5100s_evb_pico": { "name": "WIZnet W5100S-EVB-Pico", @@ -2269,6 +2314,7 @@ BOARDS = { "name": "WIZnet W5100S-EVB-Pico2", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "wiznet_5500_evb_pico": { "name": "WIZnet W5500-EVB-Pico", @@ -2279,6 +2325,7 @@ BOARDS = { "name": "WIZnet W5500-EVB-Pico2", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "wiznet_55rp20_evb_pico": { "name": "WIZnet W55RP20-EVB-Pico", @@ -2294,6 +2341,7 @@ BOARDS = { "name": "WIZnet W6300-EVB-Pico2", "mcu": "rp2350", "max_pin": 29, + "die": "A", }, "wiznet_wizfi360_evb_pico": { "name": "WIZnet WizFi360-EVB-Pico", diff --git a/esphome/components/rp2/generate_boards.py b/esphome/components/rp2/generate_boards.py index 5618287ccef..cd3f50182cf 100644 --- a/esphome/components/rp2/generate_boards.py +++ b/esphome/components/rp2/generate_boards.py @@ -37,13 +37,23 @@ MCU_MAX_PIN = { "rp2350": 47, # GPIO 0-47 (RP2350B; A-die boards are narrowed to 29 below) } DEFAULT_MAX_PIN = 29 -# The RP2350 comes in two die variants: RP2350A exposes GPIO 0-29, RP2350B -# GPIO 0-47. Variant headers declare the die via PICO_RP2350A (1 = A, 0 = B). +# The RP2350 currently comes in two die variants: RP2350A exposes GPIO 0-29, +# RP2350B GPIO 0-47. Variant headers declare the die via PICO_RP2350A +# (1 = A, 0 = B). +RP2350_DIE_A = "A" +RP2350_DIE_B = "B" RP2350A_MAX_PIN = 29 +# Key recording the die letter on RP2350 board entries. Holds a letter rather +# than a bool so a future die can be named instead of forced into "not A". +RP2350_DIE_KEY = "die" PIN_DEFINE_RE = re.compile(r"#define\s+PIN_(\w+)\s+\((\d+)u\)") # Accepts the literal forms seen in these headers: 1, (1), 1u, (1u) RP2350A_DEFINE_RE = re.compile(r"#define\s+PICO_RP2350A\s+(\S+)") +# Only PICO_RP2350A exists today. A define for any other die letter means the +# A/B assumption below no longer holds. The trailing \b keeps this from +# matching unrelated names such as PICO_RP2350_A2_SUPPORTED. +OTHER_DIE_DEFINE_RE = re.compile(r"#define\s+PICO_RP2350(?!A\b)([B-Z])\b") RP2350A_MENU_PLACEHOLDER = "__PICO_RP2350A" @@ -62,23 +72,30 @@ def parse_variant_pins(variant_dir: Path) -> dict[str, int]: return pins -def parse_variant_is_rp2350a(variant_dir: Path) -> bool: - """Return True if the variant declares an RP2350A die (GPIO 0-29 only). +def parse_variant_rp2350_die(variant_dir: Path) -> str | None: + """Return the RP2350 die letter the variant declares, or None if unknown. Generic boards leave the die a build-time menu choice (PICO_RP2350A is set - to a __PICO_RP2350A placeholder rather than a literal); those return False - so they keep the permissive B-die pin range. + to a __PICO_RP2350A placeholder rather than a literal); those return None, + meaning the die is genuinely unknown at code generation time. They keep the + permissive B-die pin range, but that is a fallback and must not be recorded + as a known die. A missing or unrecognized define raises: silently treating it as B-die would widen pin validation back to GPIO 47 on A-die boards, so a framework - bump that changes the header format must fail loudly here instead. + bump that changes the header format must fail loudly here instead. The same + goes for a die beyond A and B: PICO_RP2350A is a yes/no answer about the A + die, so "not A" can only be read as B while A and B are the whole family. """ header = variant_dir / "pins_arduino.h" - match = ( - RP2350A_DEFINE_RE.search(header.read_text(encoding="utf-8")) - if header.exists() - else None - ) + text = header.read_text(encoding="utf-8") if header.exists() else "" + if other_die := OTHER_DIE_DEFINE_RE.search(text): + raise ValueError( + f"{header}: found a PICO_RP2350{other_die.group(1)} define; the " + "RP2350 gained a die beyond A and B, so PICO_RP2350A being 0 no " + "longer means the B die" + ) + match = RP2350A_DEFINE_RE.search(text) if match is None: raise ValueError( f"{header}: no PICO_RP2350A define found; cannot classify the " @@ -86,14 +103,14 @@ def parse_variant_is_rp2350a(variant_dir: Path) -> bool: ) value = match.group(1) if value == RP2350A_MENU_PLACEHOLDER: - return False + return None literal = value.strip("()u") if not literal.isdigit(): raise ValueError( f"{header}: unrecognized PICO_RP2350A value {value!r}; cannot " "classify the RP2350 die (A exposes GPIO 0-29, B exposes GPIO 0-47)" ) - return int(literal) == 1 + return RP2350_DIE_A if int(literal) == 1 else RP2350_DIE_B def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]: @@ -104,7 +121,7 @@ def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]: board_pins = {} boards = {} variant_pins_cache: dict[str, dict[str, int]] = {} - variant_rp2350a_cache: dict[str, bool] = {} + variant_die_cache: dict[str, str | None] = {} for json_file in sorted(json_dir.glob("*.json")): board_name = json_file.stem @@ -123,12 +140,14 @@ def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]: has_wifi = "PICO_CYW43_SUPPORTED=1" in extra_flags max_pin = MCU_MAX_PIN.get(mcu, DEFAULT_MAX_PIN) + die: str | None = None if mcu == "rp2350": - if variant not in variant_rp2350a_cache: - variant_rp2350a_cache[variant] = parse_variant_is_rp2350a( + if variant not in variant_die_cache: + variant_die_cache[variant] = parse_variant_rp2350_die( variants_dir / variant ) - if variant_rp2350a_cache[variant]: + die = variant_die_cache[variant] + if die == RP2350_DIE_A: max_pin = RP2350A_MAX_PIN board_entry: dict = { @@ -136,6 +155,10 @@ def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]: "mcu": mcu, "max_pin": max_pin, } + if mcu == "rp2350": + # Recorded explicitly because max_pin cannot express the die: + # 29 also means RP2040, and 47 also means "die not known yet". + board_entry[RP2350_DIE_KEY] = die if has_wifi: board_entry["wifi"] = True boards[board_name] = board_entry @@ -218,6 +241,7 @@ def generate(arduino_pico_path: Path) -> str: cyw43_gpio_offset=CYW43_GPIO_OFFSET, cyw43_max_gpio=CYW43_GPIO_OFFSET + CYW43_GPIO_COUNT - 1, default_max_pin=DEFAULT_MAX_PIN, + rp2350_die_key=RP2350_DIE_KEY, board_pins=sorted(board_pins.items()), boards=sorted(boards.items()), ) diff --git a/tests/unit_tests/components/test_rp2_generate_boards.py b/tests/unit_tests/components/test_rp2_generate_boards.py index c5d22146950..329248488c6 100644 --- a/tests/unit_tests/components/test_rp2_generate_boards.py +++ b/tests/unit_tests/components/test_rp2_generate_boards.py @@ -8,7 +8,11 @@ import textwrap import pytest -from esphome.components.rp2.generate_boards import load_boards, parse_variant_pins +from esphome.components.rp2.generate_boards import ( + generate, + load_boards, + parse_variant_pins, +) PICO_PINS_HEADER = textwrap.dedent("""\ #pragma once @@ -151,6 +155,8 @@ def test_load_basic_board(arduino_pico: Path) -> None: assert boards["rpipico"]["name"] == "Raspberry Pi Pico" assert boards["rpipico"]["mcu"] == "rp2040" assert boards["rpipico"]["max_pin"] == 29 + # The die key only applies to the RP2350, which ships as more than one die + assert "die" not in boards["rpipico"] assert "rpipico" in board_pins assert board_pins["rpipico"]["LED"] == 25 @@ -172,6 +178,7 @@ def test_load_rp2350_board(arduino_pico: Path) -> None: assert boards["rpipico2"]["mcu"] == "rp2350" assert boards["rpipico2"]["max_pin"] == 29 + assert boards["rpipico2"]["die"] == "A" def test_rp2350_missing_die_define_raises(arduino_pico: Path) -> None: @@ -200,6 +207,35 @@ def test_rp2350_unrecognized_die_define_raises(arduino_pico: Path) -> None: load_boards(arduino_pico) +def test_rp2350_unknown_die_define_raises(arduino_pico: Path) -> None: + """A third die breaks the "not A means B" reading, so stop rather than guess.""" + _add_board( + arduino_pico, + "future_die", + mcu="rp2350", + pins_header="#define PICO_RP2350A 0\n#define PICO_RP2350C 1\n" + + PICO_PINS_HEADER, + ) + + with pytest.raises(ValueError, match="found a PICO_RP2350C define"): + load_boards(arduino_pico) + + +def test_rp2350_silicon_revision_define_ignored(arduino_pico: Path) -> None: + """PICO_RP2350_A2_SUPPORTED is a silicon revision, not a die letter.""" + _add_board( + arduino_pico, + "revision_define", + mcu="rp2350", + pins_header="#define PICO_RP2350A 1\n#define PICO_RP2350_A2_SUPPORTED 1\n" + + PICO_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert boards["revision_define"]["die"] == "A" + + def test_rp2350a_parenthesized_die_define(arduino_pico: Path) -> None: """Literal forms like (1u) classify the same as bare 1.""" _add_board( @@ -212,6 +248,7 @@ def test_rp2350a_parenthesized_die_define(arduino_pico: Path) -> None: _, boards = load_boards(arduino_pico) assert boards["paren_die"]["max_pin"] == 29 + assert boards["paren_die"]["die"] == "A" def test_rp2350b_board_keeps_max_pin_47(arduino_pico: Path) -> None: @@ -229,10 +266,15 @@ def test_rp2350b_board_keeps_max_pin_47(arduino_pico: Path) -> None: _, boards = load_boards(arduino_pico) assert boards["weact_rp2350b"]["max_pin"] == 47 + assert boards["weact_rp2350b"]["die"] == "B" def test_rp2350_menu_selectable_die_keeps_max_pin_47(arduino_pico: Path) -> None: - """Generic boards leave the die a build-time choice; stay permissive.""" + """Generic boards leave the die a build-time choice; stay permissive. + + The permissive range is a fallback, so the die must be recorded as unknown + rather than as the B die. + """ _add_board( arduino_pico, "generic_rp2350", @@ -243,6 +285,43 @@ def test_rp2350_menu_selectable_die_keeps_max_pin_47(arduino_pico: Path) -> None _, boards = load_boards(arduino_pico) assert boards["generic_rp2350"]["max_pin"] == 47 + assert boards["generic_rp2350"]["die"] is None + + +def test_generated_output_records_die(arduino_pico: Path) -> None: + """The rendered boards.py carries the die on every RP2350 entry.""" + _add_board( + arduino_pico, + "rpipico", + pins_header=PICO_PINS_HEADER, + ) + _add_board( + arduino_pico, + "a_die", + mcu="rp2350", + pins_header="#define PICO_RP2350A 1\n" + PICO_PINS_HEADER, + ) + _add_board( + arduino_pico, + "b_die", + mcu="rp2350", + pins_header="#define PICO_RP2350A 0\n" + PICO_PINS_HEADER, + ) + _add_board( + arduino_pico, + "menu_die", + mcu="rp2350", + pins_header="#define PICO_RP2350A __PICO_RP2350A\n" + PICO_PINS_HEADER, + ) + + namespace: dict = {} + exec(compile(generate(arduino_pico), "boards.py", "exec"), namespace) + + boards = namespace["BOARDS"] + assert boards["a_die"]["die"] == "A" + assert boards["b_die"]["die"] == "B" + assert boards["menu_die"]["die"] is None + assert "die" not in boards["rpipico"] def test_rp2350a_pins_above_29_filtered(arduino_pico: Path) -> None: