From 73689f8d8b422ec5cf3361b3feb2c8587b876b6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Aug 2026 08:55:24 -0500 Subject: [PATCH] [rp2] Limit pin validation to GPIO 0-29 on RP2350A boards (#18102) --- esphome/components/rp2/boards.py | 86 +++++--------- esphome/components/rp2/generate_boards.py | 54 ++++++++- .../components/test_rp2_generate_boards.py | 107 +++++++++++++++++- 3 files changed, 183 insertions(+), 64 deletions(-) diff --git a/esphome/components/rp2/boards.py b/esphome/components/rp2/boards.py index 149a719121..d2502b8fb8 100644 --- a/esphome/components/rp2/boards.py +++ b/esphome/components/rp2/boards.py @@ -133,9 +133,7 @@ RP2_BOARD_PINS = { "RX": 1, "SCK": 22, "SCL": 3, - "SCL1": 31, "SDA": 2, - "SDA1": 31, "SS": 13, "TX": 0, }, @@ -146,9 +144,7 @@ RP2_BOARD_PINS = { "RX": 1, "SCK": 22, "SCL": 3, - "SCL1": 31, "SDA": 2, - "SDA1": 31, "SS": 21, "TX": 0, }, @@ -464,9 +460,7 @@ RP2_BOARD_PINS = { "RX": 13, "SCK": 18, "SCL": 21, - "SCL1": 31, "SDA": 20, - "SDA1": 31, "SS": 17, "TX": 12, }, @@ -477,9 +471,7 @@ RP2_BOARD_PINS = { "RX": 13, "SCK": 18, "SCL": 21, - "SCL1": 31, "SDA": 20, - "SDA1": 31, "SS": 17, "TX": 12, }, @@ -509,14 +501,10 @@ RP2_BOARD_PINS = { "LED": 29, "MISO": 20, "MOSI": 19, - "RX": 31, "SCK": 22, "SCL": 17, - "SCL1": 31, "SDA": 16, - "SDA1": 31, "SS": 21, - "TX": 31, }, "cytron_maker_nano_rp2040": { "LED": 2, @@ -954,31 +942,15 @@ RP2_BOARD_PINS = { "TX": 0, }, "pimoroni_plasma2040": {"LED": 16, "SCL": 21, "SDA": 20}, - "pimoroni_plasma2350": { - "LED": 16, - "MISO": 31, - "MOSI": 31, - "RX": 31, - "SCK": 31, - "SCL": 21, - "SCL1": 31, - "SDA": 20, - "SDA1": 31, - "SS": 31, - "TX": 31, - }, + "pimoroni_plasma2350": {"LED": 16, "SCL": 21, "SDA": 20}, "pimoroni_plasma2350w": { "LED": 16, "MISO": 24, "MOSI": 24, - "RX": 31, "SCK": 29, "SCL": 21, - "SCL1": 31, "SDA": 20, - "SDA1": 31, "SS": 25, - "TX": 31, }, "pimoroni_servo2040": {"LED": 18, "SCL": 21, "SDA": 20}, "pimoroni_tiny2040": { @@ -1221,9 +1193,7 @@ RP2_BOARD_PINS = { "RX": 19, "SCK": 14, "SCL": 21, - "SCL1": 31, "SDA": 20, - "SDA1": 31, "SS": 13, "TX": 18, }, @@ -1269,9 +1239,7 @@ RP2_BOARD_PINS = { "RX": 1, "SCK": 22, "SCL": 17, - "SCL1": 31, "SDA": 16, - "SDA1": 31, "SS": 21, "TX": 0, }, @@ -1294,9 +1262,7 @@ RP2_BOARD_PINS = { "RX": 1, "SCK": 2, "SCL": 7, - "SCL1": 31, "SDA": 6, - "SDA1": 31, "SS": 9, "TX": 0, }, @@ -1621,12 +1587,12 @@ BOARDS = { "adafruit_feather_rp2350_adalogger": { "name": "Adafruit Feather RP2350 Adalogger", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "adafruit_feather_rp2350_hstx": { "name": "Adafruit Feather RP2350 HSTX", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "adafruit_feather_scorpio": { "name": "Adafruit Feather RP2040 SCORPIO", @@ -1796,17 +1762,17 @@ BOARDS = { "challenger_2350_bconnect": { "name": "iLabs Challenger 2350 BConnect", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "challenger_2350_nbiot": { "name": "iLabs Challenger 2350 NB-IoT", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "challenger_2350_wifi6_ble5": { "name": "iLabs Challenger 2350 WiFi/BLE", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "challenger_nb_2040_wifi": { "name": "iLabs Challenger NB 2040 WiFi", @@ -1821,7 +1787,7 @@ BOARDS = { "cytron_iriv_io_controller": { "name": "Cytron IRIV IO Controller", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "cytron_maker_nano_rp2040": { "name": "Cytron Maker Nano RP2040", @@ -1841,7 +1807,7 @@ BOARDS = { "cytron_motion_2350_pro": { "name": "Cytron Motion 2350 Pro", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "datanoisetv_picoadk": { "name": "DatanoiseTV PicoADK", @@ -1851,7 +1817,7 @@ BOARDS = { "datanoisetv_picoadk_v2": { "name": "DatanoiseTV PicoADK v2", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "degz_suibo": { "name": "Degz Robotics Suibo RP2040", @@ -1906,7 +1872,7 @@ BOARDS = { "ilabs_cpico_2350": { "name": "iLabs CPico 2350", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "ilabs_rpico32": { "name": "iLabs RPICO32", @@ -2028,12 +1994,12 @@ BOARDS = { "pimoroni_plasma2350": { "name": "Pimoroni Plasma2350", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "pimoroni_plasma2350w": { "name": "Pimoroni Plasma2350W", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, "wifi": True, }, "pimoroni_servo2040": { @@ -2049,7 +2015,7 @@ BOARDS = { "pimoroni_tiny2350": { "name": "Pimoroni Tiny2350", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "pintronix_pinmax": { "name": "Pintronix PinMax", @@ -2079,12 +2045,12 @@ BOARDS = { "rpipico2": { "name": "Raspberry Pi Pico 2", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "rpipico2w": { "name": "Raspberry Pi Pico 2W", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, "wifi": True, "max_virtual_pin": 64, }, @@ -2118,7 +2084,7 @@ BOARDS = { "seeed_xiao_rp2350": { "name": "Seeed XIAO RP2350", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "silicognition_rp2040_shim": { "name": "Silicognition RP2040-Shim", @@ -2144,7 +2110,7 @@ BOARDS = { "solderparty_rp2350_stamp": { "name": "Solder Party RP2350 Stamp", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "solderparty_rp2350_stamp_xl": { "name": "Solder Party RP2350 Stamp XL", @@ -2154,7 +2120,7 @@ BOARDS = { "sparkfun_iotnode_lorawanrp2350": { "name": "SparkFun IoT Node LoRaWAN", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "sparkfun_iotredboard_rp2350": { "name": "SparkFun IoT RedBoard RP2350", @@ -2175,7 +2141,7 @@ BOARDS = { "sparkfun_promicrorp2350": { "name": "SparkFun ProMicro RP2350", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "sparkfun_thingplusrp2040": { "name": "SparkFun Thing Plus RP2040", @@ -2185,7 +2151,7 @@ BOARDS = { "sparkfun_thingplusrp2350": { "name": "SparkFun Thing Plus RP2350", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, "wifi": True, "max_virtual_pin": 64, }, @@ -2266,7 +2232,7 @@ BOARDS = { "waveshare_rp2350_lcd_0_96": { "name": "Waveshare RP2350 LCD 0.96", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "waveshare_rp2350_pizero": { "name": "Waveshare RP2350 PiZero", @@ -2276,12 +2242,12 @@ BOARDS = { "waveshare_rp2350_plus": { "name": "Waveshare RP2350 Plus", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "waveshare_rp2350_zero": { "name": "Waveshare RP2350 Zero", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "waveshare_rp2350b_plus_w": { "name": "Waveshare RP2350B Plus W", @@ -2302,7 +2268,7 @@ BOARDS = { "wiznet_5100s_evb_pico2": { "name": "WIZnet W5100S-EVB-Pico2", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "wiznet_5500_evb_pico": { "name": "WIZnet W5500-EVB-Pico", @@ -2312,7 +2278,7 @@ BOARDS = { "wiznet_5500_evb_pico2": { "name": "WIZnet W5500-EVB-Pico2", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "wiznet_55rp20_evb_pico": { "name": "WIZnet W55RP20-EVB-Pico", @@ -2327,7 +2293,7 @@ BOARDS = { "wiznet_6300_evb_pico2": { "name": "WIZnet W6300-EVB-Pico2", "mcu": "rp2350", - "max_pin": 47, + "max_pin": 29, }, "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 33eb1b3058..5618287cce 100644 --- a/esphome/components/rp2/generate_boards.py +++ b/esphome/components/rp2/generate_boards.py @@ -34,11 +34,17 @@ CYW43_GPIO_COUNT = 3 # Max GPIO pin per MCU (hardware specs from datasheets) MCU_MAX_PIN = { "rp2040": 29, # GPIO 0-29 - "rp2350": 47, # GPIO 0-47 (RP2350A) + "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). +RP2350A_MAX_PIN = 29 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+)") +RP2350A_MENU_PLACEHOLDER = "__PICO_RP2350A" def parse_variant_pins(variant_dir: Path) -> dict[str, int]: @@ -56,6 +62,40 @@ 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). + + 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. + + 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. + """ + header = variant_dir / "pins_arduino.h" + match = ( + RP2350A_DEFINE_RE.search(header.read_text(encoding="utf-8")) + if header.exists() + else None + ) + if match is None: + raise ValueError( + f"{header}: no PICO_RP2350A define found; cannot classify the " + "RP2350 die (A exposes GPIO 0-29, B exposes GPIO 0-47)" + ) + value = match.group(1) + if value == RP2350A_MENU_PLACEHOLDER: + return False + 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 + + def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]: """Load all board definitions and return (board_pins, boards) dicts.""" json_dir = arduino_pico_path / "tools" / "json" @@ -64,6 +104,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] = {} for json_file in sorted(json_dir.glob("*.json")): board_name = json_file.stem @@ -81,10 +122,19 @@ def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]: extra_flags = build.get("extra_flags", "") has_wifi = "PICO_CYW43_SUPPORTED=1" in extra_flags + max_pin = MCU_MAX_PIN.get(mcu, DEFAULT_MAX_PIN) + if mcu == "rp2350": + if variant not in variant_rp2350a_cache: + variant_rp2350a_cache[variant] = parse_variant_is_rp2350a( + variants_dir / variant + ) + if variant_rp2350a_cache[variant]: + max_pin = RP2350A_MAX_PIN + board_entry: dict = { "name": display_name, "mcu": mcu, - "max_pin": MCU_MAX_PIN.get(mcu, DEFAULT_MAX_PIN), + "max_pin": max_pin, } if has_wifi: board_entry["wifi"] = True diff --git a/tests/unit_tests/components/test_rp2_generate_boards.py b/tests/unit_tests/components/test_rp2_generate_boards.py index 68bbada59b..c5d2214695 100644 --- a/tests/unit_tests/components/test_rp2_generate_boards.py +++ b/tests/unit_tests/components/test_rp2_generate_boards.py @@ -158,19 +158,122 @@ def test_load_basic_board(arduino_pico: Path) -> None: def test_load_rp2350_board(arduino_pico: Path) -> None: + """The Pico 2 uses the RP2350A die, which only exposes GPIO 0-29.""" _add_board( arduino_pico, "rpipico2", mcu="rp2350", vendor="Raspberry Pi", name="Pico 2", - pins_header=PICO_PINS_HEADER, + pins_header="#define PICO_RP2350A 1\n" + PICO_PINS_HEADER, ) _, boards = load_boards(arduino_pico) assert boards["rpipico2"]["mcu"] == "rp2350" - assert boards["rpipico2"]["max_pin"] == 47 + assert boards["rpipico2"]["max_pin"] == 29 + + +def test_rp2350_missing_die_define_raises(arduino_pico: Path) -> None: + """A variant without PICO_RP2350A cannot be classified; fail loudly.""" + _add_board( + arduino_pico, + "no_die_define", + mcu="rp2350", + pins_header=PICO_PINS_HEADER, + ) + + with pytest.raises(ValueError, match="no PICO_RP2350A define"): + load_boards(arduino_pico) + + +def test_rp2350_unrecognized_die_define_raises(arduino_pico: Path) -> None: + """An unparseable PICO_RP2350A value must not silently widen to B-die.""" + _add_board( + arduino_pico, + "hex_die_define", + mcu="rp2350", + pins_header="#define PICO_RP2350A 0x1\n" + PICO_PINS_HEADER, + ) + + with pytest.raises(ValueError, match="unrecognized PICO_RP2350A value"): + load_boards(arduino_pico) + + +def test_rp2350a_parenthesized_die_define(arduino_pico: Path) -> None: + """Literal forms like (1u) classify the same as bare 1.""" + _add_board( + arduino_pico, + "paren_die", + mcu="rp2350", + pins_header="#define PICO_RP2350A (1u)\n" + PICO_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert boards["paren_die"]["max_pin"] == 29 + + +def test_rp2350b_board_keeps_max_pin_47(arduino_pico: Path) -> None: + """A variant declaring the RP2350B die keeps the full GPIO 0-47 range. + + The define uses extra whitespace, matching real variant headers. + """ + _add_board( + arduino_pico, + "weact_rp2350b", + mcu="rp2350", + pins_header="#define PICO_RP2350A 0 // RP2350B\n" + PICO_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert boards["weact_rp2350b"]["max_pin"] == 47 + + +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.""" + _add_board( + arduino_pico, + "generic_rp2350", + mcu="rp2350", + pins_header="#define PICO_RP2350A __PICO_RP2350A\n" + PICO_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert boards["generic_rp2350"]["max_pin"] == 47 + + +def test_rp2350a_pins_above_29_filtered(arduino_pico: Path) -> None: + """Pin defines beyond the A-die range are dropped from the pin map.""" + header = textwrap.dedent("""\ + #define PICO_RP2350A 1 + #define PIN_LED (25u) + #define PIN_SPI0_MISO (40u) + """) + _add_board(arduino_pico, "a_die", mcu="rp2350", pins_header=header) + + board_pins, _ = load_boards(arduino_pico) + + assert board_pins["a_die"]["LED"] == 25 + assert "MISO" not in board_pins["a_die"] + + +def test_rp2350a_board_keeps_cyw43_virtual_pins(arduino_pico: Path) -> None: + """A-die narrowing must not filter CYW43 virtual pins (64-66).""" + _add_board( + arduino_pico, + "rpipico2w", + mcu="rp2350", + pins_header="#define PICO_RP2350A 1\n" + PICOW_PINS_HEADER, + ) + + board_pins, boards = load_boards(arduino_pico) + + assert boards["rpipico2w"]["max_pin"] == 29 + assert boards["rpipico2w"]["max_virtual_pin"] == 64 + assert board_pins["rpipico2w"]["LED"] == 64 def test_cyw43_board_has_max_virtual_pin(arduino_pico: Path) -> None: