[rp2040] Fix Pico W LED pin and auto-generate board definitions for arduino-pico 5.5.x (#14528)

This commit is contained in:
J. Nick Koston
2026-03-06 07:00:31 -10:00
committed by GitHub
parent 74e4b69654
commit a16b8fc0ac
5 changed files with 2688 additions and 17 deletions
+25
View File
@@ -0,0 +1,25 @@
# Auto-generated by generate_boards.py — do not edit manually
# To regenerate: python esphome/components/rp2040/generate_boards.py <arduino-pico-path>
# arduino-pico maps pins >= {{ cyw43_gpio_offset }} to CYW43 wireless chip GPIOs
CYW43_GPIO_OFFSET = {{ cyw43_gpio_offset }}
CYW43_MAX_GPIO = {{ cyw43_max_gpio }}
DEFAULT_MAX_PIN = {{ default_max_pin }}
RP2040_BASE_PINS = {}
RP2040_BOARD_PINS = {
{%- for name, pins in board_pins %}
{{ name | repr }}: {{ pins | format_pins }},
{%- endfor %}
}
BOARDS = {
{%- for name, info in boards %}
{{ name | repr }}: {
{%- for key, value in info.items() %}
{{ key | repr }}: {{ value | repr }},
{%- endfor %}
},
{%- endfor %}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,186 @@
"""Generate boards.py from arduino-pico board definitions.
Usage: python esphome/components/rp2040/generate_boards.py <arduino-pico-path>
"""
import json
from pathlib import Path
import re
import sys
from jinja2 import Environment, FileSystemLoader
# Map arduino-pico pin defines to ESPHome-friendly names
PIN_NAME_MAP = {
"LED": "LED",
"WIRE0_SDA": "SDA",
"WIRE0_SCL": "SCL",
"WIRE1_SDA": "SDA1",
"WIRE1_SCL": "SCL1",
"SPI0_MISO": "MISO",
"SPI0_MOSI": "MOSI",
"SPI0_SCK": "SCK",
"SPI0_SS": "SS",
"SERIAL1_TX": "TX",
"SERIAL1_RX": "RX",
}
# arduino-pico maps pins >= 64 to CYW43 wireless chip GPIOs (pin - 64)
CYW43_GPIO_OFFSET = 64
# CYW43 has 3 GPIOs: 0=LED, 1=VBUS_SENSE, 2=REG_ON
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)
}
DEFAULT_MAX_PIN = 29
PIN_DEFINE_RE = re.compile(r"#define\s+PIN_(\w+)\s+\((\d+)u\)")
def parse_variant_pins(variant_dir: Path) -> dict[str, int]:
"""Parse pins_arduino.h and return mapped pin names."""
header = variant_dir / "pins_arduino.h"
if not header.exists():
return {}
pins = {}
for match in PIN_DEFINE_RE.finditer(header.read_text(encoding="utf-8")):
raw_name = match.group(1)
value = int(match.group(2))
if raw_name in PIN_NAME_MAP:
pins[PIN_NAME_MAP[raw_name]] = value
return pins
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"
variants_dir = arduino_pico_path / "variants"
board_pins = {}
boards = {}
variant_pins_cache: dict[str, dict[str, int]] = {}
for json_file in sorted(json_dir.glob("*.json")):
board_name = json_file.stem
with open(json_file, encoding="utf-8") as f:
data = json.load(f)
build = data.get("build", {})
mcu = build.get("mcu", "rp2040")
variant = build.get("variant", board_name)
name = data.get("name", board_name)
vendor = data.get("vendor", "")
display_name = f"{vendor} {name}".strip() if vendor else name
boards[board_name] = {
"name": display_name,
"mcu": mcu,
"max_pin": MCU_MAX_PIN.get(mcu, DEFAULT_MAX_PIN),
}
# Get pins for this variant
if variant not in variant_pins_cache:
variant_dir = variants_dir / variant
variant_pins_cache[variant] = parse_variant_pins(variant_dir)
pins = variant_pins_cache[variant]
if pins:
max_pin = boards[board_name]["max_pin"]
cyw43_max = CYW43_GPIO_OFFSET + CYW43_GPIO_COUNT - 1
# Filter out placeholder values (e.g. 99 = "not connected")
filtered = {
name: value
for name, value in pins.items()
if value <= max_pin or CYW43_GPIO_OFFSET <= value <= cyw43_max
}
if filtered:
board_pins[board_name] = filtered
# Compute max_virtual_pin per board from pin maps
for board_name, pins in board_pins.items():
if isinstance(pins, str):
continue
virtual_pins = [v for v in pins.values() if v >= CYW43_GPIO_OFFSET]
if virtual_pins and board_name in boards:
boards[board_name]["max_virtual_pin"] = max(virtual_pins)
# Deduplicate: if board pins match its variant's pins, use string alias
for board_name in list(board_pins.keys()):
if board_name not in boards:
continue
build_variant = _get_variant(json_dir / f"{board_name}.json")
if (
build_variant
and build_variant != board_name
and build_variant in board_pins
and board_pins[board_name] == board_pins[build_variant]
):
board_pins[board_name] = build_variant
return board_pins, boards
def _get_variant(json_file: Path) -> str | None:
"""Get variant name from a board JSON file."""
if not json_file.exists():
return None
with open(json_file, encoding="utf-8") as f:
data = json.load(f)
return data.get("build", {}).get("variant")
_TEMPLATE_DIR = Path(__file__).parent
def _format_pins(pins: dict[str, int] | str) -> str:
"""Jinja2 filter to format a pin dict or alias as Python source."""
if isinstance(pins, str):
return repr(pins)
items = ", ".join(f"{k!r}: {v}" for k, v in sorted(pins.items()))
return f"{{{items}}}"
_jinja_env = Environment(
loader=FileSystemLoader(_TEMPLATE_DIR), keep_trailing_newline=True
)
_jinja_env.filters["format_pins"] = _format_pins
_jinja_env.filters["repr"] = repr
def generate(arduino_pico_path: Path) -> str:
"""Generate boards.py content."""
board_pins, boards = load_boards(arduino_pico_path)
template = _jinja_env.get_template("boards.jinja2")
return template.render(
cyw43_gpio_offset=CYW43_GPIO_OFFSET,
cyw43_max_gpio=CYW43_GPIO_OFFSET + CYW43_GPIO_COUNT - 1,
default_max_pin=DEFAULT_MAX_PIN,
board_pins=sorted(board_pins.items()),
boards=sorted(boards.items()),
)
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <arduino-pico-path>", file=sys.stderr)
sys.exit(1)
arduino_pico_path = Path(sys.argv[1])
if not (arduino_pico_path / "tools" / "json").exists():
print(f"Error: {arduino_pico_path}/tools/json not found", file=sys.stderr)
sys.exit(1)
output = generate(arduino_pico_path)
output_file = Path(__file__).parent / "boards.py"
output_file.write_text(output, encoding="utf-8")
print(f"Generated {output_file}")
if __name__ == "__main__":
main()
+16 -6
View File
@@ -54,19 +54,29 @@ def _translate_pin(value):
return _lookup_pin(value)
def _board_max_virtual_pin(board):
"""Get the max CYW43 virtual pin for this board, or None if no virtual pins."""
return boards.BOARDS.get(board, {}).get("max_virtual_pin")
def validate_gpio_pin(value):
value = _translate_pin(value)
board = CORE.data[KEY_RP2040][KEY_BOARD]
if board == "rpipicow" and value == 32:
return value # Special case for Pico-w LED pin
if value < 0 or value > 29:
raise cv.Invalid(f"RP2040: Invalid pin number: {value}")
max_virtual = _board_max_virtual_pin(board)
if max_virtual is not None and boards.CYW43_GPIO_OFFSET <= value <= max_virtual:
return value
max_pin = boards.BOARDS.get(board, {}).get("max_pin", boards.DEFAULT_MAX_PIN)
if value < 0 or value > max_pin:
raise cv.Invalid(f"Invalid pin number: {value} (max {max_pin} for this board)")
return value
def validate_supports(value):
board = CORE.data[KEY_RP2040][KEY_BOARD]
if board != "rpipicow" or value[CONF_NUMBER] != 32:
if (
_board_max_virtual_pin(board) is None
or value[CONF_NUMBER] < boards.CYW43_GPIO_OFFSET
):
return value
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
@@ -75,7 +85,7 @@ def validate_supports(value):
is_pullup = mode[CONF_PULLUP]
is_pulldown = mode[CONF_PULLDOWN]
if not is_output or is_input or is_open_drain or is_pullup or is_pulldown:
raise cv.Invalid("Only output mode is supported for Pico-w LED pin")
raise cv.Invalid("Only output mode is supported for CYW43 virtual pins")
return value