From 40147ec86184e1e85ca7f6bffb330bcb75d842dd Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Sat, 29 Aug 2026 13:50:11 -0700 Subject: [PATCH 1/6] Support pin muxing through IOBroker IOBroker is a new zephyr module that maps package pins to peripherals and uses Zephyr's dynamic pinctrl to prep a device for use. It does resource use tracking for pads and peripherals/devices as well. --- .codespell/ignore-words.txt | 1 + ports/zephyr-cp/.gitignore | 11 +- ports/zephyr-cp/CMakeLists.txt | 6 + ports/zephyr-cp/Kconfig | 11 + ports/zephyr-cp/Makefile | 2 +- ports/zephyr-cp/README.md | 43 ++ .../adafruit/clue_nrf52840_zephyr/board.conf | 4 + .../clue_nrf52840_zephyr/board.overlay | 94 +++- .../clue_nrf52840_zephyr/circuitpython.toml | 83 ++++ .../feather_nrf52840_sense_zephyr/board.conf | 4 + .../board.overlay | 104 ++++ .../circuitpython.toml | 46 ++ .../feather_nrf52840_zephyr/board.conf | 4 + .../feather_nrf52840_zephyr/board.overlay | 103 ++++ .../circuitpython.toml | 43 ++ .../adafruit/feather_rp2040_zephyr/board.conf | 3 + .../feather_rp2040_zephyr/circuitpython.toml | 32 ++ .../boards/nordic/nrf5340dk/board.overlay | 59 +++ .../boards/nordic/nrf54h20dk/board.overlay | 95 +++- .../boards/nordic/nrf54l15dk/board.overlay | 86 +++- .../boards/nordic/nrf54l15tag/board.conf | 6 + .../boards/nordic/nrf54l15tag/board.overlay | 62 +++ .../boards/nordic/nrf54lm20dk/board.overlay | 126 ++++- .../nordic/nrf54lm20dk/circuitpython.toml | 114 +++++ .../boards/nordic/nrf7002dk/board.overlay | 65 +++ .../boards/renesas/ek_ra6m5/board.conf | 3 + .../boards/renesas/ek_ra6m5/board.overlay | 31 ++ .../boards/st/nucleo_n657x0_q/board.conf | 3 + .../boards/st/nucleo_n657x0_q/board.overlay | 28 ++ ports/zephyr-cp/common-hal/busio/I2C.c | 74 ++- ports/zephyr-cp/common-hal/busio/I2C.h | 8 + ports/zephyr-cp/common-hal/busio/SPI.c | 64 ++- ports/zephyr-cp/common-hal/busio/SPI.h | 8 + ports/zephyr-cp/common-hal/busio/UART.c | 107 +++- ports/zephyr-cp/common-hal/busio/UART.h | 12 + .../common-hal/digitalio/DigitalInOut.c | 28 +- .../common-hal/digitalio/DigitalInOut.h | 4 + .../common-hal/microcontroller/Pin.c | 80 ++- .../common-hal/microcontroller/Pin.h | 13 +- .../common-hal/rotaryio/IncrementalEncoder.c | 72 ++- .../common-hal/rotaryio/IncrementalEncoder.h | 6 + .../zephyr-cp/cptools/build_circuitpython.py | 2 +- .../zephyr-cp/cptools/tests/test_zephyr2cp.py | 116 ++++- ports/zephyr-cp/cptools/zephyr2cp.py | 392 ++++++++++++++- .../zephyr-cp/modules/iobroker/CMakeLists.txt | 84 ++++ ports/zephyr-cp/modules/iobroker/Kconfig | 36 ++ .../modules/iobroker/Kconfig.packages | 71 +++ ports/zephyr-cp/modules/iobroker/README.md | 132 +++++ .../modules/iobroker/datasheets/README.md | 29 ++ .../iobroker/include/iobroker/iobroker.h | 203 ++++++++ .../iobroker/packages/mdbt50q_1mv2.toml | 302 ++++++++++++ .../iobroker/packages/nrf52840_aqfn73.toml | 301 ++++++++++++ .../iobroker/packages/nrf5340_qkaa.toml | 301 ++++++++++++ .../iobroker/packages/nrf54l15_qfn48.toml | 165 +++++++ .../iobroker/packages/nrf54lm20_csp98.toml | 409 ++++++++++++++++ .../iobroker/packages/rp2040_qfn56.toml | 159 ++++++ .../zephyr-cp/modules/iobroker/src/iobroker.c | 250 ++++++++++ .../modules/iobroker/src/iobroker_internal.h | 19 + .../iobroker/src/nordic/nrf/iobroker_route.c | 459 ++++++++++++++++++ .../modules/iobroker/tools/gen_package.py | 280 +++++++++++ .../modules/iobroker/tools/gen_package_c.py | 52 ++ ports/zephyr-cp/prj.conf | 11 + ports/zephyr-cp/socs/nrf52840.conf | 3 +- 63 files changed, 5383 insertions(+), 141 deletions(-) create mode 100644 ports/zephyr-cp/boards/renesas/ek_ra6m5/board.conf create mode 100644 ports/zephyr-cp/boards/renesas/ek_ra6m5/board.overlay create mode 100644 ports/zephyr-cp/boards/st/nucleo_n657x0_q/board.conf create mode 100644 ports/zephyr-cp/boards/st/nucleo_n657x0_q/board.overlay create mode 100644 ports/zephyr-cp/modules/iobroker/CMakeLists.txt create mode 100644 ports/zephyr-cp/modules/iobroker/Kconfig create mode 100644 ports/zephyr-cp/modules/iobroker/Kconfig.packages create mode 100644 ports/zephyr-cp/modules/iobroker/README.md create mode 100644 ports/zephyr-cp/modules/iobroker/datasheets/README.md create mode 100644 ports/zephyr-cp/modules/iobroker/include/iobroker/iobroker.h create mode 100644 ports/zephyr-cp/modules/iobroker/packages/mdbt50q_1mv2.toml create mode 100644 ports/zephyr-cp/modules/iobroker/packages/nrf52840_aqfn73.toml create mode 100644 ports/zephyr-cp/modules/iobroker/packages/nrf5340_qkaa.toml create mode 100644 ports/zephyr-cp/modules/iobroker/packages/nrf54l15_qfn48.toml create mode 100644 ports/zephyr-cp/modules/iobroker/packages/nrf54lm20_csp98.toml create mode 100644 ports/zephyr-cp/modules/iobroker/packages/rp2040_qfn56.toml create mode 100644 ports/zephyr-cp/modules/iobroker/src/iobroker.c create mode 100644 ports/zephyr-cp/modules/iobroker/src/iobroker_internal.h create mode 100644 ports/zephyr-cp/modules/iobroker/src/nordic/nrf/iobroker_route.c create mode 100644 ports/zephyr-cp/modules/iobroker/tools/gen_package.py create mode 100644 ports/zephyr-cp/modules/iobroker/tools/gen_package_c.py diff --git a/.codespell/ignore-words.txt b/.codespell/ignore-words.txt index d435ed01dac..2ae7d88d6ba 100644 --- a/.codespell/ignore-words.txt +++ b/.codespell/ignore-words.txt @@ -28,3 +28,4 @@ ftbs ftb curren mabey +rsource diff --git a/ports/zephyr-cp/.gitignore b/ports/zephyr-cp/.gitignore index 65d8deaa722..7cfe421e6b4 100644 --- a/ports/zephyr-cp/.gitignore +++ b/ports/zephyr-cp/.gitignore @@ -1,7 +1,12 @@ -# West manages these folders. +# West manages these folders. modules/* are west clones, except iobroker, +# which is an in-tree Zephyr module (see modules/iobroker/README.md); its +# datasheets/ PDFs are licensed by Nordic and not redistributed here. bootloader build -modules -tools +modules/* +!modules/iobroker +modules/iobroker/datasheets/* +!modules/iobroker/datasheets/README.md +/tools zephyr .west diff --git a/ports/zephyr-cp/CMakeLists.txt b/ports/zephyr-cp/CMakeLists.txt index 9d115f1e176..e16e258a861 100644 --- a/ports/zephyr-cp/CMakeLists.txt +++ b/ports/zephyr-cp/CMakeLists.txt @@ -1,5 +1,11 @@ cmake_minimum_required(VERSION 3.20.0) +# The dynamic peripheral allocation / runtime pin routing code lives in a +# Zephyr module kept in-tree for now (modules/iobroker, see its README.md). +# It must be set before find_package(Zephyr). Repoint this at the module's new +# home to consume an externalized copy. +set(ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/modules/iobroker) + find_package(Zephyr REQUIRED HINTS lib/zephyr) project(circuitpython) diff --git a/ports/zephyr-cp/Kconfig b/ports/zephyr-cp/Kconfig index 015864100ec..1b9b76e1159 100644 --- a/ports/zephyr-cp/Kconfig +++ b/ports/zephyr-cp/Kconfig @@ -25,6 +25,17 @@ config UART_LINE_CTRL config ENTROPY_GENERATOR default y +# ===== Dynamic pin routing (nRF) ===== +# CircuitPython routes peripherals to pins at runtime on nRF SoCs. That needs +# pinctrl states to be swappable (PINCTRL_DYNAMIC) and devices to be +# de-initializable/re-initializable (DEVICE_DEINIT_SUPPORT). Both cost a little +# RAM (pinctrl configs move out of flash) and one function pointer per device. +config PINCTRL_DYNAMIC + default y if SOC_FAMILY_NORDIC_NRF + +config DEVICE_DEINIT_SUPPORT + default y if SOC_FAMILY_NORDIC_NRF + # ===== Bluetooth defaults ===== # Use a variable for the chosen name so the comma isn't parsed as an argument separator diff --git a/ports/zephyr-cp/Makefile b/ports/zephyr-cp/Makefile index 1fc5040fefa..2b328825b8f 100644 --- a/ports/zephyr-cp/Makefile +++ b/ports/zephyr-cp/Makefile @@ -41,7 +41,7 @@ CP_BOARD_CONF := $(DEBUG_CONF_FILE) endif endif ifneq ($(CP_BOARD_CONF),) -WEST_CMAKE_ARGS += -Dzephyr-cp_EXTRA_CONF_FILE=$(CP_BOARD_CONF) +WEST_CMAKE_ARGS += -Dzephyr-cp_EXTRA_CONF_FILE="$(CP_BOARD_CONF)" endif .PHONY: $(BUILD)/zephyr-cp/zephyr/zephyr.elf flash recover debug debug-jlink debugserver attach run run-sim clean menuconfig all clean-all sim clean-sim test fetch-port-submodules diff --git a/ports/zephyr-cp/README.md b/ports/zephyr-cp/README.md index d051097c83d..d7e5d169121 100644 --- a/ports/zephyr-cp/README.md +++ b/ports/zephyr-cp/README.md @@ -99,6 +99,49 @@ Behavior and precedence: - If neither is provided, defaults from `circuitpython.toml` are used. - Use `SHIELD=` (empty) to disable a board default shield for one build. +## Pin names + +Human readable pin names (the `board` module) come from the devicetree by +default: `gpio-leds` and `gpio-keys` labels, node aliases, and connector +`gpio-map`s. Boards can add names without any devicetree involvement by +listing them in `boards///circuitpython.toml` under `[pins]`. +Each entry maps a board module name to the pin number exposed by the board's +hardware: the SoC package pin (or, for ball grid array packages, the +datasheet's ball id, e.g. `"B2"`), or the castellated module pin when the +board uses a module like the Raytac MDBT50Q: + +```toml +[pins] +LED = 17 # QFN package pin number +SDA = "B2" # ball id for BGA/CSP packages +D13 = 8 # MDBT50Q-1MV2 module pin number +``` + +The build resolves each package pin to a SoC pad using the iobroker package +pin map selected by `CONFIG_IOBROKER_PACKAGE_` +(`modules/iobroker/packages/.toml`) and exposes the name on the +matching pad in the `board` module. A name that already maps to the same pin +(from the devicetree or an earlier entry) is deduplicated; a name that maps +to two different pins is a build error. The package map must not be `CUSTOM` +or missing, and the pad must be on an enabled GPIO controller; otherwise the +build fails with an error naming the offending entry. + +## Connector names + +Devicetree connector nodes (`gpio-map`) get their names from a generic +per-compatible list in `cptools/zephyr2cp.py`. A board whose silkscreen +differs can override them per position in `circuitpython.toml` under +`[connectors.]`, keying the gpio-map position (the header pin +number, as a string) to a name: + +```toml +[connectors.nordic_expansion_header] +0 = "EXP_00" # header GPIO 00 +21 = "EXP_21" # header GPIO 21 (QSPI CS) +``` + +Positions left out of the table get no name. + ## Testing other boards [Any Zephyr board](https://docs.zephyrproject.org/latest/boards/index.html#) can diff --git a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.conf b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.conf index cfa31e2fdcf..6b4d778740d 100644 --- a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.conf +++ b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.conf @@ -4,3 +4,7 @@ CONFIG_BOARD_SERIAL_BACKEND_CDC_ACM=n # Enable the ST7789V TFT via the Zephyr display subsystem CONFIG_DISPLAY=y + +# Pin numbers are module pins (MDBT50Q-1MV2 carries the aQFN73 inside). +# The CLUE uses the MDBT50Q-1MV2 module, so pin numbers are module pins. +CONFIG_IOBROKER_PACKAGE_MDBT50Q_1MV2=y diff --git a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay index 586165dfe06..f39df2ad94e 100644 --- a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay @@ -1,10 +1,14 @@ +// The UF2 board definition points the Zephyr console/shell/mcumgr/BT chosen +// nodes at board_cdc_acm_uart, which is deleted above: CircuitPython talks USB +// CDC ACM through its own usb_cdc bindings instead, and no serial pins are +// claimed at boot. / { chosen { - zephyr,console = &uart0; - zephyr,shell-uart = &uart0; - zephyr,uart-mcumgr = &uart0; - zephyr,bt-mon-uart = &uart0; - zephyr,bt-c2h-uart = &uart0; + /delete-property/ zephyr,console; + /delete-property/ zephyr,shell-uart; + /delete-property/ zephyr,uart-mcumgr; + /delete-property/ zephyr,bt-mon-uart; + /delete-property/ zephyr,bt-c2h-uart; }; }; @@ -42,8 +46,86 @@ }; }; +// UART0, I2C1 and SPI3 are enabled with all pins disconnected and marked +// zephyr,deferred-init so nothing is claimed at boot: the iobroker +// initializes and routes a device to arbitrary pins when a busio object is +// constructed, and de-initializes it on release. (I2C0 is the fixed sensor +// bus, SPI2 the fixed display bus, and QSPI the fixed external flash.) +// +// I2C1 and SPI1 are the same peripheral instance (0x40004000), so only one +// mode can be enabled: I2C is the more useful dynamic bus on the CLUE. +// SPI1 is left disabled; UART1 is enabled by neither overlay because UART0 +// already provides a dynamic serial instance. + +&pinctrl { + uart0_dyn_default: uart0_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart0_dyn_sleep: uart0_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + i2c1_dyn_default: i2c1_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c1_dyn_sleep: i2c1_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + spi3_dyn_default: spi3_dyn_default { + group1 { + psels = , + , + ; + }; + }; + + spi3_dyn_sleep: spi3_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; +}; + &uart0 { - status = "okay"; + status = "okay"; + zephyr,deferred-init; + current-speed = <115200>; + pinctrl-0 = <&uart0_dyn_default>; + pinctrl-1 = <&uart0_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&i2c1 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&i2c1_dyn_default>; + pinctrl-1 = <&i2c1_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi3 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&spi3_dyn_default>; + pinctrl-1 = <&spi3_dyn_sleep>; + pinctrl-names = "default", "sleep"; }; #include "../../../app.overlay" diff --git a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/circuitpython.toml b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/circuitpython.toml index 4af0c99d878..01eeb66eed0 100644 --- a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/circuitpython.toml +++ b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/circuitpython.toml @@ -5,3 +5,86 @@ NAME="CLUE nRF52840 Express" # Non-Zephyr build of the same board; nvm and CIRCUITPY must sit where it puts them. counterpart = "nordic/clue_nrf52840_express" + + +# Pin names copied from the ports/ counterpart board's pins.c and resolved +# through the package pin map selected in board.conf. + +[pins] +# Pin names copied from the ports/ counterpart board's pins.c and resolved +# to MDBT50Q-1MV2 module pin numbers through the package pin map selected +# in board.conf. +P0 = 20 +D0 = 20 +A2 = 20 +RX = 20 +P1 = 21 +D1 = 21 +A3 = 21 +TX = 21 +P2 = 9 +D2 = 9 +A4 = 9 +P3 = 13 +D3 = 13 +A5 = 13 +P4 = 11 +D4 = 11 +A6 = 11 +P5 = 50 +D5 = 50 +BUTTON_A = 50 +P6 = 26 +D6 = 26 +P7 = 23 +D7 = 23 +P8 = 58 +D8 = 58 +P9 = 16 +D9 = 16 +P10 = 14 +D10 = 14 +A7 = 14 +P11 = 3 +D11 = 3 +BUTTON_B = 3 +P12 = 12 +D12 = 12 +A0 = 12 +P13 = 24 +D13 = 24 +SCK = 24 +P14 = 22 +D14 = 22 +MISO = 22 +P15 = 19 +D15 = 19 +MOSI = 19 +P16 = 10 +D16 = 10 +A1 = 10 +P17 = 61 +D17 = 61 +L = 61 +LED = 61 +P18 = 38 +D18 = 38 +NEOPIXEL = 38 +P19 = 49 +D19 = 49 +SCL = 49 +P20 = 48 +D20 = 48 +SDA = 48 +MICROPHONE_CLOCK = 18 +MICROPHONE_DATA = 17 +SPEAKER = 47 +PROXIMITY_LIGHT_INTERRUPT = 52 +ACCELEROMETER_GYRO_INTERRUPT = 57 +WHITE_LEDS = 54 +TFT_RESET = 60 +TFT_BACKLIGHT = 59 +TFT_CS = 29 +TFT_DC = 37 +TFT_SCK = 36 +TFT_MOSI = 39 diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.conf b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.conf index 6d7299ec6e6..799f4a727ba 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.conf +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.conf @@ -1,3 +1,7 @@ CONFIG_USE_DT_CODE_PARTITION=y CONFIG_BOARD_SERIAL_BACKEND_CDC_ACM=n + +# Pin numbers are module pins (MDBT50Q-1MV2 carries the aQFN73 inside). +# The feathers use the MDBT50Q-1MV2 module, so pin numbers are module pins. +CONFIG_IOBROKER_PACKAGE_MDBT50Q_1MV2=y diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.overlay b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.overlay index 586165dfe06..2065ba5e433 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.overlay @@ -42,6 +42,110 @@ }; }; +// Bus instances enabled with all pins disconnected and marked +// zephyr,deferred-init so nothing is claimed at boot: the iobroker +// initializes and routes a device to arbitrary pins when a busio object is +// constructed, and de-initializes it on release: I2C1, SPI2, SPI3 and UART1. +// (The fixed I2C0 is the Stemma QT bus with the SHT31 sensor, and UART0 the +// console. The formerly fixed SPI1 is dropped: it had no device attached and +// its instance is the same peripheral as I2C1, so only one mode can be +// enabled.) + +&pinctrl { + i2c1_dyn_default: i2c1_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c1_dyn_sleep: i2c1_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + spi2_dyn_default: spi2_dyn_default { + group1 { + psels = , + , + ; + }; + }; + + spi2_dyn_sleep: spi2_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; + + spi3_dyn_default: spi3_dyn_default { + group1 { + psels = , + , + ; + }; + }; + + spi3_dyn_sleep: spi3_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; + + uart1_dyn_default: uart1_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart1_dyn_sleep: uart1_dyn_sleep { + group1 { + psels = , + ; + }; + }; +}; + +&i2c1 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&i2c1_dyn_default>; + pinctrl-1 = <&i2c1_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi2 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&spi2_dyn_default>; + pinctrl-1 = <&spi2_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi3 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&spi3_dyn_default>; + pinctrl-1 = <&spi3_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&uart1 { + status = "okay"; + zephyr,deferred-init; + current-speed = <115200>; + pinctrl-0 = <&uart1_dyn_default>; + pinctrl-1 = <&uart1_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + &uart0 { status = "okay"; }; diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/circuitpython.toml b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/circuitpython.toml index 5dfa12f79df..78a48bc36c0 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/circuitpython.toml +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/circuitpython.toml @@ -5,3 +5,49 @@ NAME="Feather Bluefruit Sense" # Non-Zephyr build of the same board; nvm and CIRCUITPY must sit where it puts them. counterpart = "nordic/feather_bluefruit_sense" + + +# Pin names copied from the ports/ counterpart board's pins.c and resolved +# through the package pin map selected in board.conf. + +[pins] +# Pin names copied from the ports/ counterpart board's pins.c and resolved +# to MDBT50Q-1MV2 module pin numbers through the package pin map selected +# in board.conf. +A0 = 20 +A1 = 21 +A2 = 14 +A3 = 13 +A4 = 11 +A5 = 9 +AREF = 12 +VOLTAGE_MONITOR = 10 +BATTERY = 10 +SWITCH = 50 +NFC1 = 52 +NFC2 = 54 +D2 = 54 +D3 = 4 +D5 = 25 +D6 = 23 +D9 = 19 +D10 = 16 +D11 = 22 +D12 = 24 +D13 = 26 +NEOPIXEL = 38 +SCK = 36 +MOSI = 37 +MISO = 39 +TX = 49 +RX = 48 +SCL = 27 +SDA = 29 +L = 26 +LED = 26 +RED_LED = 26 +BLUE_LED = 3 +MICROPHONE_CLOCK = 18 +MICROPHONE_DATA = 17 +PROXIMITY_LIGHT_INTERRUPT = 47 +ACCELEROMETER_GYRO_INTERRUPT = 4 diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.conf b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.conf index 6d7299ec6e6..799f4a727ba 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.conf +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.conf @@ -1,3 +1,7 @@ CONFIG_USE_DT_CODE_PARTITION=y CONFIG_BOARD_SERIAL_BACKEND_CDC_ACM=n + +# Pin numbers are module pins (MDBT50Q-1MV2 carries the aQFN73 inside). +# The feathers use the MDBT50Q-1MV2 module, so pin numbers are module pins. +CONFIG_IOBROKER_PACKAGE_MDBT50Q_1MV2=y diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay index 586165dfe06..ea1eeca9843 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay @@ -42,6 +42,109 @@ }; }; +// Bus instances enabled with all pins disconnected and marked +// zephyr,deferred-init so nothing is claimed at boot: the iobroker +// initializes and routes a device to arbitrary pins when a busio object is +// constructed, and de-initializes it on release: I2C1, SPI2, SPI3 and UART1. +// (The fixed I2C0 is the Stemma QT bus and UART0 the console. The formerly +// fixed SPI1 is dropped: it had no device attached and its instance is the +// same peripheral as I2C1, so only one mode can be enabled.) + +&pinctrl { + i2c1_dyn_default: i2c1_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c1_dyn_sleep: i2c1_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + spi2_dyn_default: spi2_dyn_default { + group1 { + psels = , + , + ; + }; + }; + + spi2_dyn_sleep: spi2_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; + + spi3_dyn_default: spi3_dyn_default { + group1 { + psels = , + , + ; + }; + }; + + spi3_dyn_sleep: spi3_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; + + uart1_dyn_default: uart1_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart1_dyn_sleep: uart1_dyn_sleep { + group1 { + psels = , + ; + }; + }; +}; + +&i2c1 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&i2c1_dyn_default>; + pinctrl-1 = <&i2c1_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi2 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&spi2_dyn_default>; + pinctrl-1 = <&spi2_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi3 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&spi3_dyn_default>; + pinctrl-1 = <&spi3_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&uart1 { + status = "okay"; + zephyr,deferred-init; + current-speed = <115200>; + pinctrl-0 = <&uart1_dyn_default>; + pinctrl-1 = <&uart1_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + &uart0 { status = "okay"; }; diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/circuitpython.toml b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/circuitpython.toml index 8f97aca1d3d..58a36de1eba 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/circuitpython.toml +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/circuitpython.toml @@ -5,3 +5,46 @@ NAME="Feather nRF52840 Express" # Non-Zephyr build of the same board; nvm and CIRCUITPY must sit where it puts them. counterpart = "nordic/feather_nrf52840_express" + + +# Pin names copied from the ports/ counterpart board's pins.c and resolved +# through the package pin map selected in board.conf. + +[pins] +# Pin names copied from the ports/ counterpart board's pins.c and resolved +# to MDBT50Q-1MV2 module pin numbers through the package pin map selected +# in board.conf. +A0 = 20 +A1 = 21 +A2 = 14 +A3 = 13 +A4 = 11 +A5 = 9 +AREF = 12 +VOLTAGE_MONITOR = 10 +BATTERY = 10 +SWITCH = 50 +NFC1 = 52 +NFC2 = 54 +D2 = 54 +D5 = 25 +D6 = 23 +D9 = 19 +D10 = 16 +D11 = 22 +D12 = 24 +D13 = 26 +NEOPIXEL = 38 +NEOPIXEL_POWER = 7 +SCK = 36 +MOSI = 37 +MISO = 39 +TX = 49 +RX = 48 +SCL = 27 +SDA = 29 +L = 8 +LED = 8 +RED_LED = 8 +D3 = 8 +BLUE_LED = 3 diff --git a/ports/zephyr-cp/boards/adafruit/feather_rp2040_zephyr/board.conf b/ports/zephyr-cp/boards/adafruit/feather_rp2040_zephyr/board.conf index 91c3c15b37d..be49c1e7940 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_rp2040_zephyr/board.conf +++ b/ports/zephyr-cp/boards/adafruit/feather_rp2040_zephyr/board.conf @@ -1 +1,4 @@ CONFIG_GPIO=y + +# Package pin map for the RP2040 QFN-56 used by this board. +CONFIG_IOBROKER_PACKAGE_RP2040_QFN56=y diff --git a/ports/zephyr-cp/boards/adafruit/feather_rp2040_zephyr/circuitpython.toml b/ports/zephyr-cp/boards/adafruit/feather_rp2040_zephyr/circuitpython.toml index 4e992a212f4..c6f089958da 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_rp2040_zephyr/circuitpython.toml +++ b/ports/zephyr-cp/boards/adafruit/feather_rp2040_zephyr/circuitpython.toml @@ -2,3 +2,35 @@ CIRCUITPY_BUILD_EXTENSIONS = ["elf", "uf2"] # Non-Zephyr build of the same board; nvm and CIRCUITPY must sit where it puts them. counterpart = "raspberrypi/adafruit_feather_rp2040" + + +# Pin names copied from the ports/ counterpart board's pins.c and resolved +# through the package pin map selected in board.conf. +[pins] +A0 = 38 +A1 = 39 +A2 = 40 +A3 = 41 +D24 = 36 +D25 = 37 +SCK = 29 +MOSI = 30 +MISO = 31 +D0 = 3 +RX = 3 +D1 = 2 +TX = 2 +D4 = 8 +SDA = 4 +SCL = 5 +D5 = 9 +D6 = 11 +D9 = 12 +D10 = 13 +D11 = 14 +D12 = 15 +LED = 16 +D13 = 16 +BUTTON = 6 +BOOT = 6 +NEOPIXEL = 27 diff --git a/ports/zephyr-cp/boards/nordic/nrf5340dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf5340dk/board.overlay index 6b7ee85de7c..2842e7a3f98 100644 --- a/ports/zephyr-cp/boards/nordic/nrf5340dk/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf5340dk/board.overlay @@ -29,4 +29,63 @@ i2s_rxtx: &i2s0 { /delete-node/ &slot1_partition; +// Enable free serial peripheral instances so CircuitPython can mux them to +// arbitrary pins at runtime. Each instance is enabled in one mode because the +// i2c/spi/uart nodes of an instance are the same peripheral (enforced by +// zephyr/soc/nordic/validate_enabled_instances.c). All signals are +// disconnected and the devices are marked zephyr,deferred-init so nothing is +// claimed at boot; iobroker initializes and routes a device when a busio +// object is constructed and de-initializes it on release. +// +// Instance 0 is left alone: uart0 is the console and i2c0/spi0 share that +// same peripheral, so they must not be enabled. Instance 1 stays with the +// fixed arduino header i2c1, and spi4 stays the fixed arduino header SPI. + +&pinctrl { + i2c2_dyn_default: i2c2_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c2_dyn_sleep: i2c2_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + uart3_dyn_default: uart3_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart3_dyn_sleep: uart3_dyn_sleep { + group1 { + psels = , + ; + }; + }; +}; + +&i2c2 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&i2c2_dyn_default>; + pinctrl-1 = <&i2c2_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&uart3 { + status = "okay"; + zephyr,deferred-init; + current-speed = <115200>; + pinctrl-0 = <&uart3_dyn_default>; + pinctrl-1 = <&uart3_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + #include "../../../app.overlay" diff --git a/ports/zephyr-cp/boards/nordic/nrf54h20dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf54h20dk/board.overlay index e557179992b..9461fa34569 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54h20dk/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf54h20dk/board.overlay @@ -45,11 +45,104 @@ }; /* Remove slot1 (OTA), expand slot0 to use the space. - * CircuitPython doesn't use OTA updates. */ + * CircuitPython doesn't use OTA updates. The expansion absorbs the + * cpurad slot0 as well: no cpurad image is built (CONFIG_BT=n), so its + * slot would otherwise be dead space between cpuapp slot0 and the + * cpuppr code partition at 0xe4000. */ &slot0_partition { reg = <0x40000 DT_SIZE_K(656)>; }; /delete-node/ &slot1_partition; +/delete-node/ &cpurad_slot0_partition; + +// Enable free serial peripheral instances so CircuitPython can mux them to +// arbitrary pins at runtime. On this SoC the i2c/spi/uart nodes with the same +// instance number are the same peripheral block (e.g. i2c130/spi130/uart130 +// all sit at 0x9a5000), so each instance is enabled in exactly one mode. All +// signals are disconnected and the devices are marked zephyr,deferred-init so +// nothing is claimed at boot; iobroker initializes and routes a device when a +// busio object is constructed and de-initializes it on release. +// +// Instance 130 stays with the fixed arduino header i2c130. Instances 120, +// 135 and 136 are left alone: uart120 and uart135 are the console UARTs of +// the (not built here) FLPR and PPR cores, and uart136 is the app console. +// The DMA region is required on this SoC: the serial drivers only access +// memory the peripheral's EasyDMA is allowed to reach. + +&pinctrl { + i2c131_dyn_default: i2c131_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c131_dyn_sleep: i2c131_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + spi132_dyn_default: spi132_dyn_default { + group1 { + psels = , + , + ; + }; + }; + + spi132_dyn_sleep: spi132_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; + + uart137_dyn_default: uart137_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart137_dyn_sleep: uart137_dyn_sleep { + group1 { + psels = , + ; + }; + }; +}; + +&i2c131 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&i2c131_dyn_default>; + pinctrl-1 = <&i2c131_dyn_sleep>; + pinctrl-names = "default", "sleep"; + zephyr,concat-buf-size = <256>; + memory-regions = <&cpuapp_dma_region>; +}; + +&spi132 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&spi132_dyn_default>; + pinctrl-1 = <&spi132_dyn_sleep>; + pinctrl-names = "default", "sleep"; + memory-regions = <&cpuapp_dma_region>; +}; + +&uart137 { + status = "okay"; + zephyr,deferred-init; + current-speed = <115200>; + pinctrl-0 = <&uart137_dyn_default>; + pinctrl-1 = <&uart137_dyn_sleep>; + pinctrl-names = "default", "sleep"; + memory-regions = <&cpuapp_dma_region>; +}; #include "../../../app.overlay" diff --git a/ports/zephyr-cp/boards/nordic/nrf54l15dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf54l15dk/board.overlay index 7ffd2dec4d6..e4dc1bc7802 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54l15dk/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf54l15dk/board.overlay @@ -1,28 +1,90 @@ // nRF54L15 DK doesn't have USB, so no app.overlay for CDC ACM. -// I2C bus on P1.8 (SDA) and P1.10 (SCL). +// Enable every free serial peripheral instance so CircuitPython can mux it to +// arbitrary pins at runtime. Each instance is enabled in one mode because the +// i2c/spi/uart nodes of an instance are the same peripheral (enforced by +// zephyr/soc/nordic/validate_enabled_instances.c). All signals are +// disconnected and the devices are marked zephyr,deferred-init so nothing is +// claimed at boot; iobroker initializes and routes a device when a busio +// object is constructed and de-initializes it on release. +// +// Instance 20 is left alone: uart20 is the console (main UART) and i2c20/spi20 +// share that same peripheral, so they must not be enabled. Instance 00 is +// owned by spi00 and the MX25R64 SPI flash (no TWI00 on this SoC). + &pinctrl { - i2c21_default: i2c21_default { + i2c21_dyn_default: i2c21_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c21_dyn_sleep: i2c21_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + spi22_dyn_default: spi22_dyn_default { + group1 { + psels = , + , + ; + }; + }; + + spi22_dyn_sleep: spi22_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; + + uart30_dyn_default: uart30_dyn_default { group1 { - psels = , - ; - bias-pull-up; + psels = , + ; }; }; - i2c21_sleep: i2c21_sleep { + uart30_dyn_sleep: uart30_dyn_sleep { group1 { - psels = , - ; - low-power-enable; + psels = , + ; }; }; }; &i2c21 { - clock-frequency = ; - pinctrl-0 = <&i2c21_default>; - pinctrl-1 = <&i2c21_sleep>; + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&i2c21_dyn_default>; + pinctrl-1 = <&i2c21_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi22 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&spi22_dyn_default>; + pinctrl-1 = <&spi22_dyn_sleep>; pinctrl-names = "default", "sleep"; +}; + +&uart30 { status = "okay"; + zephyr,deferred-init; + current-speed = <115200>; + pinctrl-0 = <&uart30_dyn_default>; + pinctrl-1 = <&uart30_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +/ { + chosen { + zephyr,code-partition = &slot0_partition; + }; }; diff --git a/ports/zephyr-cp/boards/nordic/nrf54l15tag/board.conf b/ports/zephyr-cp/boards/nordic/nrf54l15tag/board.conf index 2627ebe2e5f..b679a257de5 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54l15tag/board.conf +++ b/ports/zephyr-cp/boards/nordic/nrf54l15tag/board.conf @@ -2,3 +2,9 @@ CONFIG_SERIAL=y CONFIG_CONSOLE=y CONFIG_UART_CONSOLE=y + +# Enable the controller's Zephyr VS HCI set/get TX power commands, which +# _bleio's start_advertising(tx_power=...) uses. Without this the controller +# rejects the commands and tx power is fixed at the BT_CTLR_TX_PWR default (0 dBm). +# The nRF54L15 radio supports -46 to +8 dBm. +CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL=y diff --git a/ports/zephyr-cp/boards/nordic/nrf54l15tag/board.overlay b/ports/zephyr-cp/boards/nordic/nrf54l15tag/board.overlay index ddf5c8f7e8f..6dd08bfce59 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54l15tag/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf54l15tag/board.overlay @@ -37,3 +37,65 @@ zephyr,uart-mcumgr = &uart30; }; }; + +// Enable free serial peripheral instances so CircuitPython can mux them to +// arbitrary pins at runtime. Each instance is enabled in one mode because the +// i2c/spi/uart nodes of an instance are the same peripheral (enforced by +// zephyr/soc/nordic/validate_enabled_instances.c). All signals are +// disconnected and the devices are marked zephyr,deferred-init so nothing is +// claimed at boot; iobroker initializes and routes a device when a busio +// object is constructed and de-initializes it on release. +// +// Instance 21 stays with the fixed i2c21 (bme688 and adxl367 hang off it) and +// instance 22 with the fixed spi22 (bmi270). Instance 30 is left alone: +// uart30 is the console above and i2c30/spi30 share that same peripheral, so +// they must not be enabled. There is no i2c00 on this SoC, so instance 00 can +// only carry spi00 (or uart00). + +&pinctrl { + i2c20_dyn_default: i2c20_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c20_dyn_sleep: i2c20_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + spi00_dyn_default: spi00_dyn_default { + group1 { + psels = , + , + ; + }; + }; + + spi00_dyn_sleep: spi00_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; +}; + +&i2c20 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&i2c20_dyn_default>; + pinctrl-1 = <&i2c20_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi00 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&spi00_dyn_default>; + pinctrl-1 = <&spi00_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; diff --git a/ports/zephyr-cp/boards/nordic/nrf54lm20dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf54lm20dk/board.overlay index 0f48a5a5d46..636f481718e 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54lm20dk/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf54lm20dk/board.overlay @@ -24,29 +24,131 @@ }; }; -// I2C bus on P1.13 (SDA) and P1.23 (SCL). +// Enable every free serial peripheral instance so CircuitPython can mux it to +// arbitrary pins at runtime. Each instance is enabled in one mode because the +// i2c/spi/uart nodes of an instance are the same peripheral (enforced by +// zephyr/soc/nordic/validate_enabled_instances.c). All signals are +// disconnected and the devices are marked zephyr,deferred-init so nothing is +// claimed at boot; iobroker initializes and routes a device when a busio +// object is constructed and de-initializes it on release. +// +// Instance 20 is left alone: uart20 is the console (main UART) and i2c20/spi20 +// share that same peripheral, so they must not be enabled. Instance 00 is +// owned by spi00 and the MX25R64 SPI flash, so uart00 must not be enabled. + &pinctrl { - i2c21_default: i2c21_default { + i2c21_dyn_default: i2c21_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c21_dyn_sleep: i2c21_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + i2c23_dyn_default: i2c23_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c23_dyn_sleep: i2c23_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + spi22_dyn_default: spi22_dyn_default { + group1 { + psels = , + , + ; + }; + }; + + spi22_dyn_sleep: spi22_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; + + spi24_dyn_default: spi24_dyn_default { group1 { - psels = , - ; - bias-pull-up; + psels = , + , + ; }; }; - i2c21_sleep: i2c21_sleep { + spi24_dyn_sleep: spi24_dyn_sleep { group1 { - psels = , - ; - low-power-enable; + psels = , + , + ; + }; + }; + + uart30_dyn_default: uart30_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart30_dyn_sleep: uart30_dyn_sleep { + group1 { + psels = , + ; }; }; }; &i2c21 { - clock-frequency = ; - pinctrl-0 = <&i2c21_default>; - pinctrl-1 = <&i2c21_sleep>; + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&i2c21_dyn_default>; + pinctrl-1 = <&i2c21_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&i2c23 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&i2c23_dyn_default>; + pinctrl-1 = <&i2c23_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi22 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&spi22_dyn_default>; + pinctrl-1 = <&spi22_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi24 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&spi24_dyn_default>; + pinctrl-1 = <&spi24_dyn_sleep>; pinctrl-names = "default", "sleep"; +}; + +&uart30 { status = "okay"; + zephyr,deferred-init; + current-speed = <115200>; + pinctrl-0 = <&uart30_dyn_default>; + pinctrl-1 = <&uart30_dyn_sleep>; + pinctrl-names = "default", "sleep"; }; diff --git a/ports/zephyr-cp/boards/nordic/nrf54lm20dk/circuitpython.toml b/ports/zephyr-cp/boards/nordic/nrf54lm20dk/circuitpython.toml index 83e6bcd39c4..460f0f32cfc 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54lm20dk/circuitpython.toml +++ b/ports/zephyr-cp/boards/nordic/nrf54lm20dk/circuitpython.toml @@ -1 +1,115 @@ CIRCUITPY_BUILD_EXTENSIONS = ["hex"] + +# Headers silkscreened PORT0 (P0.00-P0.09), PORT1:00-15 (P1.00-P1.15), +# PORT1:16-31 (P1.16-P1.31), PORT2 (P2.00-P2.10) and PORT3 (P3.00-P3.12), +# resolved to CSP98 ball names through the package map selected in board.conf. + +[pins] +PORT0_00 = "K3" +PORT0_01 = "K2" +PORT0_02 = "J3" +PORT0_03 = "J2" +PORT0_04 = "J1" +PORT0_05 = "H3" +PORT0_06 = "H2" +PORT0_07 = "G3" +PORT0_08 = "F3" +PORT0_09 = "E3" + +# P1.01 (NFC1) and P1.02 (NFC2) are routed to the NFC antenna by default; +# move R33/R34 to R3/R4 and disable the NFCT peripheral to use them as GPIO. +PORT1_00 = "K8" +PORT1_01 = "J6" +PORT1_02 = "J7" +PORT1_03 = "H9" +PORT1_04 = "J9" +PORT1_05 = "H8" +PORT1_06 = "J8" +PORT1_07 = "G8" +PORT1_08 = "F8" +PORT1_09 = "E8" +PORT1_10 = "D5" +PORT1_11 = "C5" +PORT1_12 = "C4" +PORT1_13 = "C3" +PORT1_14 = "C2" +PORT1_15 = "B2" +PORT1_16 = "B3" +PORT1_17 = "B4" +PORT1_18 = "B5" +PORT1_19 = "B6" +# P1.20/P1.21 feed the 32.768 kHz crystal by default; short SB3/SB4 (and cut +# SB1/SB2) to use them as GPIO. +PORT1_22 = "D6" +PORT1_23 = "B7" +PORT1_24 = "D7" +PORT1_25 = "E7" +PORT1_26 = "B8" +PORT1_27 = "C8" +PORT1_28 = "D8" +PORT1_29 = "H10" +PORT1_30 = "J10" +PORT1_31 = "K9" + +# P2.00-P2.05 are shared with the MX25R64 external flash. +PORT2_00 = "B10" +PORT2_01 = "C10" +PORT2_02 = "D10" +PORT2_03 = "E10" +PORT2_04 = "F10" +PORT2_05 = "G9" +PORT2_06 = "B9" +PORT2_07 = "C9" +PORT2_08 = "D9" +PORT2_09 = "E9" +PORT2_10 = "F9" + +PORT3_00 = "H7" +PORT3_01 = "H6" +PORT3_02 = "H5" +PORT3_03 = "H4" +PORT3_04 = "J4" +PORT3_05 = "G7" +PORT3_06 = "G6" +PORT3_07 = "G5" +PORT3_08 = "G4" +PORT3_09 = "F7" +PORT3_10 = "F4" +PORT3_11 = "E4" +PORT3_12 = "D4" + +# Silkscreen button names. LED0-LED3, SW0-SW3 and PUSH_BUTTON_0-3 already come +# from the devicetree. +BUTTON0 = "B8" +BUTTON1 = "E8" +BUTTON2 = "F8" +BUTTON3 = "H3" + +# Nordic expansion board header (P17), named per the board silkscreen. +# Positions key the gpio-map order (header GPIO 00-21, odd/even columns from +# the A0/B0 end); I0 sits opposite the header's mid GND and has no gpio-map +# entry. Function groups check out: B0 = PWM (P3.04), D0-D3 = SPI +# (P3.00-P3.03), H0-H5 = QSPI (P2.00-P2.05). +[connectors.nordic_expansion_header] +0 = "A0" +1 = "B0" +2 = "C0" +3 = "C1" +4 = "C2" +5 = "C3" +6 = "D0" +7 = "D1" +8 = "E2" +9 = "E1" +10 = "D2" +11 = "D3" +12 = "F0" +13 = "F1" +14 = "C4" +15 = "G0" +16 = "H0" +17 = "H1" +18 = "H2" +19 = "H3" +20 = "H4" +21 = "H5" diff --git a/ports/zephyr-cp/boards/nordic/nrf7002dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf7002dk/board.overlay index cec4fbb2b98..70f39cd19e3 100644 --- a/ports/zephyr-cp/boards/nordic/nrf7002dk/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf7002dk/board.overlay @@ -10,7 +10,72 @@ reg = <0x00000000 0x0000C000>; }; +&storage_partition { + reg = <0x000FC000 0x00004000>; +}; + /delete-node/ &slot1_partition; /delete-node/ &storage_partition; +// Enable free serial peripheral instances so CircuitPython can mux them to +// arbitrary pins at runtime. Each instance is enabled in one mode because the +// i2c/spi/uart nodes of an instance are the same peripheral (enforced by +// zephyr/soc/nordic/validate_enabled_instances.c). All signals are +// disconnected and the devices are marked zephyr,deferred-init so nothing is +// claimed at boot; iobroker initializes and routes a device when a busio +// object is constructed and de-initializes it on release. +// +// Instance 0 is left alone: uart0 is the console and i2c0/spi0 share that +// same peripheral, so they must not be enabled. Instance 1 stays with the +// fixed arduino header i2c1, and spi4 stays fixed because it owns the +// MX25R64 flash. The disabled arduino spi3/uart1 nodes keep their devicetree +// pinout in case a shield wants them. + +&pinctrl { + i2c2_dyn_default: i2c2_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c2_dyn_sleep: i2c2_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + uart3_dyn_default: uart3_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart3_dyn_sleep: uart3_dyn_sleep { + group1 { + psels = , + ; + }; + }; +}; + +&i2c2 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&i2c2_dyn_default>; + pinctrl-1 = <&i2c2_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&uart3 { + status = "okay"; + zephyr,deferred-init; + current-speed = <115200>; + pinctrl-0 = <&uart3_dyn_default>; + pinctrl-1 = <&uart3_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + #include "../../../app.overlay" diff --git a/ports/zephyr-cp/boards/renesas/ek_ra6m5/board.conf b/ports/zephyr-cp/boards/renesas/ek_ra6m5/board.conf new file mode 100644 index 00000000000..57dea741be8 --- /dev/null +++ b/ports/zephyr-cp/boards/renesas/ek_ra6m5/board.conf @@ -0,0 +1,3 @@ +# Disable BIN output; Renesas RA OFS registers at high addresses create +# ~16 MB binaries. HEX output (already the SoC default) is sparse. +CONFIG_BUILD_OUTPUT_BIN=n diff --git a/ports/zephyr-cp/boards/renesas/ek_ra6m5/board.overlay b/ports/zephyr-cp/boards/renesas/ek_ra6m5/board.overlay new file mode 100644 index 00000000000..42663b3acf4 --- /dev/null +++ b/ports/zephyr-cp/boards/renesas/ek_ra6m5/board.overlay @@ -0,0 +1,31 @@ +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* The app links at offset 0 (no zephyr,code-partition is set, + * so FLASH_LOAD_OFFSET stays 0) and CIRCUITPY gets the second + * half of the 2 MB internal code flash. + */ + circuitpy_partition: partition@100000 { + label = "circuitpy"; + reg = <0x100000 DT_SIZE_M(1)>; + }; + }; +}; + +/* Disable OFS (Option Function Select) nodes so their high-address + * sections don't end up in the app HEX. + */ +&option_setting_ofs0 { status = "disabled"; }; +&option_setting_dualsel { status = "disabled"; }; +&option_setting_ofs1_sec { status = "disabled"; }; +&option_setting_banksel_sec { status = "disabled"; }; +&option_setting_bps_sec { status = "disabled"; }; +&option_setting_pbps_sec { status = "disabled"; }; +&option_setting_ofs1_sel { status = "disabled"; }; +&option_setting_banksel_sel { status = "disabled"; }; +&option_setting_bps_sel { status = "disabled"; }; + +#include "../../../app.overlay" diff --git a/ports/zephyr-cp/boards/st/nucleo_n657x0_q/board.conf b/ports/zephyr-cp/boards/st/nucleo_n657x0_q/board.conf new file mode 100644 index 00000000000..b0948df7a13 --- /dev/null +++ b/ports/zephyr-cp/boards/st/nucleo_n657x0_q/board.conf @@ -0,0 +1,3 @@ +# No USB, button, or retention device available. The stale UF2/MCUboot +# options are not set here because they only exist in the MCUboot image's +# Kconfig, which this board does not build. diff --git a/ports/zephyr-cp/boards/st/nucleo_n657x0_q/board.overlay b/ports/zephyr-cp/boards/st/nucleo_n657x0_q/board.overlay new file mode 100644 index 00000000000..37011475f9a --- /dev/null +++ b/ports/zephyr-cp/boards/st/nucleo_n657x0_q/board.overlay @@ -0,0 +1,28 @@ +/ { + chosen { + zephyr,flash-controller = &xspi2; + zephyr,flash = &mx25um51245g; + zephyr,code-partition = &slot0_partition; + }; +}; + +/* Code and CIRCUITPY live on the 64 MB external XSPI NOR flash. The board + * dts only declares a storage partition (last sector, shrunk for an errata + * workaround); add the code slot and the filesystem here. The image runs + * from RAM, so slot0 is only a load address. + */ +&mx25um51245g { + partitions { + slot0_partition: partition@0 { + label = "image-0"; + reg = <0x0 DT_SIZE_M(2)>; + }; + + circuitpy_partition: partition@200000 { + label = "circuitpy"; + reg = <0x200000 (DT_SIZE_M(64) - DT_SIZE_M(2) - DT_SIZE_K(64))>; + }; + }; +}; + +#include "../../../app.overlay" diff --git a/ports/zephyr-cp/common-hal/busio/I2C.c b/ports/zephyr-cp/common-hal/busio/I2C.c index 84e95721b27..d77fe960c7e 100644 --- a/ports/zephyr-cp/common-hal/busio/I2C.c +++ b/ports/zephyr-cp/common-hal/busio/I2C.c @@ -5,9 +5,14 @@ // SPDX-License-Identifier: MIT #include "shared-bindings/busio/I2C.h" +#include "shared-bindings/microcontroller/Pin.h" + +#include "bindings/zephyr_kernel/__init__.h" #include "py/mperrno.h" #include "py/runtime.h" +#include + #include #include #include @@ -18,30 +23,87 @@ mp_obj_t common_hal_busio_i2c_construct_from_device(busio_i2c_obj_t *self, const self->i2c_device = i2c_device; k_mutex_init(&self->mutex); self->has_lock = false; + self->dynamic = false; + self->sda = NULL; + self->scl = NULL; return MP_OBJ_FROM_PTR(self); } -// Standard busio construct - not used in Zephyr port (devices come from device tree) +static void raise_no_i2c_peripheral(int err) { + if (err == -ENODEV) { + mp_raise_ValueError(MP_ERROR_TEXT("All I2C peripherals are in use")); + } + if (err == -EBUSY) { + mp_raise_ValueError(MP_ERROR_TEXT("Internal resource(s) in use")); + } + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Use device tree to define %q devices"), MP_QSTR_I2C); +} + +// Standard busio construct: pick a free peripheral instance and route it to +// the requested pins at runtime (supported on nRF SoCs). void common_hal_busio_i2c_construct(busio_i2c_obj_t *self, const mcu_pin_obj_t *scl, const mcu_pin_obj_t *sda, uint32_t frequency, uint32_t timeout_ms) { - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Use device tree to define %q devices"), MP_QSTR_I2C); + const struct device *dev = NULL; + int ret = iobroker_i2c_allocate(sda->package_pin, scl->package_pin, &dev); + if (ret < 0) { + raise_no_i2c_peripheral(ret); + } + + common_hal_busio_i2c_construct_from_device(self, dev); + self->dynamic = true; + self->sda = sda; + self->scl = scl; + + // Initialize the deferred device now that it is routed to the requested + // pins. Fixed devicetree instances are already initialized (-EALREADY). + int init_ret = device_init(dev); + if (init_ret < 0 && init_ret != -EALREADY) { + // The failed init may have routed pins and left the device in a + // partial state; deinit gives up the claim and resets the pins. + common_hal_busio_i2c_deinit(self); + raise_zephyr_error(init_ret); + } + + // Apply the requested bus frequency. Zephyr nRF drivers support 100k, + // 400k and (on TWIM) 1M. + uint8_t speed; + if (frequency <= 100000) { + speed = I2C_SPEED_STANDARD; + } else if (frequency <= 400000) { + speed = I2C_SPEED_FAST; + } else { + speed = I2C_SPEED_FAST_PLUS; + } + int config_ret = i2c_configure(self->i2c_device, I2C_SPEED_SET(speed)); + if (config_ret < 0) { + common_hal_busio_i2c_deinit(self); + raise_zephyr_error(config_ret); + } } bool common_hal_busio_i2c_deinited(busio_i2c_obj_t *self) { - // Always leave it active (managed by Zephyr) - return false; + return self->i2c_device == NULL; } void common_hal_busio_i2c_deinit(busio_i2c_obj_t *self) { if (common_hal_busio_i2c_deinited(self)) { return; } - // Always leave it active (managed by Zephyr) + if (self->dynamic) { + // The release de-inits the device, which applies its low-power + // pinctrl state and leaves the routed pins disconnected. + (void)iobroker_release(self->i2c_device); + self->sda = NULL; + self->scl = NULL; + self->i2c_device = NULL; + } } void common_hal_busio_i2c_mark_deinit(busio_i2c_obj_t *self) { - // Not needed for Zephyr port + if (self->dynamic) { + self->i2c_device = NULL; + } } bool common_hal_busio_i2c_probe(busio_i2c_obj_t *self, uint8_t addr) { diff --git a/ports/zephyr-cp/common-hal/busio/I2C.h b/ports/zephyr-cp/common-hal/busio/I2C.h index 4fa877739b7..fcaa0298a8a 100644 --- a/ports/zephyr-cp/common-hal/busio/I2C.h +++ b/ports/zephyr-cp/common-hal/busio/I2C.h @@ -7,6 +7,8 @@ #pragma once #include "py/obj.h" + +#include "common-hal/microcontroller/Pin.h" #include typedef struct { @@ -14,6 +16,12 @@ typedef struct { const struct device *i2c_device; struct k_mutex mutex; bool has_lock; + // True when the underlying Zephyr device was dynamically routed to the + // pins below at construction time. Such objects deinitialize the device + // and release their pins. + bool dynamic; + const mcu_pin_obj_t *sda; + const mcu_pin_obj_t *scl; } busio_i2c_obj_t; // Helper function to construct from Zephyr device tree device diff --git a/ports/zephyr-cp/common-hal/busio/SPI.c b/ports/zephyr-cp/common-hal/busio/SPI.c index 2864c90b490..bb2342ee113 100644 --- a/ports/zephyr-cp/common-hal/busio/SPI.c +++ b/ports/zephyr-cp/common-hal/busio/SPI.c @@ -5,12 +5,18 @@ // SPDX-License-Identifier: MIT #include "shared-bindings/busio/SPI.h" +#include "shared-bindings/microcontroller/Pin.h" + #include "py/mperrno.h" #include "py/runtime.h" #include "py/gc.h" #include "shared/runtime/interrupt_char.h" #include "supervisor/port.h" +#include "bindings/zephyr_kernel/__init__.h" + +#include +#include #include #include #include @@ -22,6 +28,10 @@ mp_obj_t common_hal_busio_spi_construct_from_device(busio_spi_obj_t *self, const k_mutex_init(&self->mutex); self->has_lock = false; self->active_config = 0; + self->dynamic = false; + self->clock = NULL; + self->mosi = NULL; + self->miso = NULL; k_poll_signal_init(&self->signal); @@ -34,27 +44,69 @@ mp_obj_t common_hal_busio_spi_construct_from_device(busio_spi_obj_t *self, const return MP_OBJ_FROM_PTR(self); } -// Standard busio construct - not used in Zephyr port (devices come from device tree) +// Standard busio construct: pick a free peripheral instance and route it to +// the requested pins at runtime (supported on nRF SoCs). void common_hal_busio_spi_construct(busio_spi_obj_t *self, const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso, bool half_duplex) { - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Use device tree to define %q devices"), MP_QSTR_SPI); + if (half_duplex) { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_half_duplex); + } + + const struct device *dev = NULL; + int ret = iobroker_spi_allocate(clock->package_pin, + mosi != NULL ? mosi->package_pin : IOBROKER_NO_PIN, + miso != NULL ? miso->package_pin : IOBROKER_NO_PIN, &dev); + if (ret < 0) { + if (ret == -ENODEV) { + mp_raise_ValueError(MP_ERROR_TEXT("All SPI peripherals are in use")); + } + if (ret == -EBUSY) { + mp_raise_ValueError(MP_ERROR_TEXT("Internal resource(s) in use")); + } + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Use device tree to define %q devices"), MP_QSTR_SPI); + } + + common_hal_busio_spi_construct_from_device(self, dev); + self->dynamic = true; + self->clock = clock; + self->mosi = mosi; + self->miso = miso; + + // Initialize the deferred device now that it is routed to the requested + // pins. Fixed devicetree instances are already initialized (-EALREADY). + int init_ret = device_init(dev); + if (init_ret < 0 && init_ret != -EALREADY) { + // The failed init may have routed pins and left the device in a + // partial state; deinit gives up the claim and resets the pins. + common_hal_busio_spi_deinit(self); + raise_zephyr_error(init_ret); + } } bool common_hal_busio_spi_deinited(busio_spi_obj_t *self) { - // Always leave it active - return false; + return self->spi_device == NULL; } void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { if (common_hal_busio_spi_deinited(self)) { return; } - // Always leave it active + if (self->dynamic) { + // The release de-inits the device, which applies its low-power + // pinctrl state and leaves the routed pins disconnected. + (void)iobroker_release(self->spi_device); + self->clock = NULL; + self->mosi = NULL; + self->miso = NULL; + self->spi_device = NULL; + } } void common_hal_busio_spi_mark_deinit(busio_spi_obj_t *self) { - // Not needed for Zephyr port + if (self->dynamic) { + self->spi_device = NULL; + } } bool common_hal_busio_spi_try_lock(busio_spi_obj_t *self) { diff --git a/ports/zephyr-cp/common-hal/busio/SPI.h b/ports/zephyr-cp/common-hal/busio/SPI.h index 87411c9825c..57641c173ac 100644 --- a/ports/zephyr-cp/common-hal/busio/SPI.h +++ b/ports/zephyr-cp/common-hal/busio/SPI.h @@ -7,6 +7,8 @@ #pragma once #include "py/obj.h" + +#include "common-hal/microcontroller/Pin.h" #include #include @@ -18,6 +20,12 @@ typedef struct { struct spi_config config[2]; // Two configs for pointer comparison by driver uint8_t active_config; // Index of currently active config (0 or 1) struct k_poll_signal signal; + // True when the underlying Zephyr device was dynamically routed to the + // pins below at construction time. + bool dynamic; + const mcu_pin_obj_t *clock; + const mcu_pin_obj_t *mosi; + const mcu_pin_obj_t *miso; } busio_spi_obj_t; // Helper function for Zephyr-specific initialization from device tree diff --git a/ports/zephyr-cp/common-hal/busio/UART.c b/ports/zephyr-cp/common-hal/busio/UART.c index af1de0e9023..b230cc7180e 100644 --- a/ports/zephyr-cp/common-hal/busio/UART.c +++ b/ports/zephyr-cp/common-hal/busio/UART.c @@ -6,17 +6,22 @@ #include "shared-bindings/microcontroller/__init__.h" #include "shared-bindings/busio/UART.h" +#include "shared-bindings/microcontroller/Pin.h" #include "shared/runtime/interrupt_char.h" #include "py/mpconfig.h" #include "py/gc.h" +#include "py/mphal.h" #include "py/mperrno.h" #include "py/runtime.h" #include "py/stream.h" +#include "bindings/zephyr_kernel/__init__.h" + #include #include +#include #include #include LOG_MODULE_REGISTER(busio_uart); @@ -57,6 +62,12 @@ void common_hal_busio_uart_never_reset(busio_uart_obj_t *self) { mp_obj_t common_hal_busio_uart_construct_from_device(busio_uart_obj_t *self, const struct device *uart_device, uint16_t receiver_buffer_size, byte *receiver_buffer) { self->base.type = &busio_uart_type; self->uart_device = uart_device; + self->dynamic = false; + self->receiver_buffer = NULL; + self->tx = NULL; + self->rx = NULL; + self->rts = NULL; + self->cts = NULL; int ret = uart_irq_callback_user_data_set(uart_device, serial_cb, self); if (ret < 0) { @@ -73,7 +84,8 @@ mp_obj_t common_hal_busio_uart_construct_from_device(busio_uart_obj_t *self, con return MP_OBJ_FROM_PTR(self); } -// Standard busio construct - not used in Zephyr port (devices come from device tree) +// Standard busio construct: pick a free peripheral instance and route it to +// the requested pins at runtime (supported on nRF SoCs). void common_hal_busio_uart_construct(busio_uart_obj_t *self, const mcu_pin_obj_t *tx, const mcu_pin_obj_t *rx, const mcu_pin_obj_t *rts, const mcu_pin_obj_t *cts, @@ -81,15 +93,102 @@ void common_hal_busio_uart_construct(busio_uart_obj_t *self, uint32_t baudrate, uint8_t bits, busio_uart_parity_t parity, uint8_t stop, mp_float_t timeout, uint16_t receiver_buffer_size, byte *receiver_buffer, bool sigint_enabled) { - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Use device tree to define %q devices"), MP_QSTR_UART); + if (rs485_dir != NULL) { + mp_raise_NotImplementedError(MP_ERROR_TEXT("RS485")); + } + // nRF UARTE only supports 8 data bits. + mp_arg_validate_int(bits, 8, MP_QSTR_bits); + + const struct device *dev = NULL; + int ret = iobroker_uart_allocate(tx != NULL ? tx->package_pin : IOBROKER_NO_PIN, + rx != NULL ? rx->package_pin : IOBROKER_NO_PIN, + rts != NULL ? rts->package_pin : IOBROKER_NO_PIN, + cts != NULL ? cts->package_pin : IOBROKER_NO_PIN, &dev); + if (ret < 0) { + if (ret == -ENODEV) { + mp_raise_ValueError(MP_ERROR_TEXT("All UART peripherals are in use")); + } + if (ret == -EBUSY) { + mp_raise_ValueError(MP_ERROR_TEXT("Internal resource(s) in use")); + } + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Use device tree to define %q devices"), MP_QSTR_UART); + } + + bool allocated_buffer = false; + if (receiver_buffer == NULL) { + receiver_buffer = m_malloc(receiver_buffer_size); + allocated_buffer = true; + } + + common_hal_busio_uart_construct_from_device(self, dev, receiver_buffer_size, receiver_buffer); + self->dynamic = true; + self->receiver_buffer = allocated_buffer ? receiver_buffer : NULL; + self->tx = tx; + self->rx = rx; + self->rts = rts; + self->cts = cts; + + // Initialize the deferred device now that it is routed to the requested + // pins. Fixed devicetree instances are already initialized (-EALREADY). + int init_ret = device_init(dev); + if (init_ret < 0 && init_ret != -EALREADY) { + // The failed init may have routed pins and left the device in a + // partial state; deinit gives up the claim and resets the pins. + common_hal_busio_uart_deinit(self); + raise_zephyr_error(init_ret); + } + + // Apply line configuration. + struct uart_config config = { + .baudrate = baudrate, + .data_bits = UART_CFG_DATA_BITS_8, + .parity = (parity == BUSIO_UART_PARITY_NONE) ? UART_CFG_PARITY_NONE : + ((parity == BUSIO_UART_PARITY_EVEN) ? UART_CFG_PARITY_EVEN : UART_CFG_PARITY_ODD), + .stop_bits = (stop == 1) ? UART_CFG_STOP_BITS_1 : UART_CFG_STOP_BITS_2, + .flow_ctrl = (rts != NULL && cts != NULL) ? UART_CFG_FLOW_CTRL_RTS_CTS : UART_CFG_FLOW_CTRL_NONE, + }; + int config_ret = uart_configure(self->uart_device, &config); + if (config_ret < 0) { + LOG_ERR("uart_configure failed: %d (baudrate=%u stop=%u flow=%u)", + config_ret, baudrate, stop, (rts != NULL && cts != NULL)); + common_hal_busio_uart_deinit(self); + raise_zephyr_error(config_ret); + } + + self->timeout = K_USEC((uint64_t)(timeout * 1000000)); } bool common_hal_busio_uart_deinited(busio_uart_obj_t *self) { - return !device_is_ready(self->uart_device); + return self->uart_device == NULL; } void common_hal_busio_uart_deinit(busio_uart_obj_t *self) { - // Leave it active (managed by Zephyr) + if (common_hal_busio_uart_deinited(self)) { + return; + } + if (self->dynamic) { + // The device may not be fully initialized: construct de-inits this + // object when device_init() fails partway through. Zephyr then + // reports it not ready (init_res != 0), so only poke the driver + // when it is really up and running. The iobroker claim and any + // routed pins are still given up below. + if (device_is_ready(self->uart_device)) { + uart_irq_rx_disable(self->uart_device); + uart_irq_callback_user_data_set(self->uart_device, NULL, NULL); + } + // The release de-inits the device, which applies its low-power + // pinctrl state and leaves the routed pins disconnected. + (void)iobroker_release(self->uart_device); + self->tx = NULL; + self->rx = NULL; + self->rts = NULL; + self->cts = NULL; + if (self->receiver_buffer != NULL) { + m_free(self->receiver_buffer); + self->receiver_buffer = NULL; + } + self->uart_device = NULL; + } } // Read characters. diff --git a/ports/zephyr-cp/common-hal/busio/UART.h b/ports/zephyr-cp/common-hal/busio/UART.h index be0b7ff83a9..369e2e2bf93 100644 --- a/ports/zephyr-cp/common-hal/busio/UART.h +++ b/ports/zephyr-cp/common-hal/busio/UART.h @@ -8,6 +8,8 @@ #include "py/obj.h" +#include "common-hal/microcontroller/Pin.h" + #include typedef struct { @@ -20,6 +22,16 @@ typedef struct { k_timeout_t write_timeout; bool rx_paused; // set by irq if no space in rbuf + + // True when the underlying Zephyr device was dynamically routed to the + // pins below at construction time. Such objects own their receiver + // buffer and deinitialize the device and release their pins. + bool dynamic; + byte *receiver_buffer; + const mcu_pin_obj_t *tx; + const mcu_pin_obj_t *rx; + const mcu_pin_obj_t *rts; + const mcu_pin_obj_t *cts; } busio_uart_obj_t; // Helper function for Zephyr-specific initialization from device tree diff --git a/ports/zephyr-cp/common-hal/digitalio/DigitalInOut.c b/ports/zephyr-cp/common-hal/digitalio/DigitalInOut.c index 0da16a9b720..cc2da9f5120 100644 --- a/ports/zephyr-cp/common-hal/digitalio/DigitalInOut.c +++ b/ports/zephyr-cp/common-hal/digitalio/DigitalInOut.c @@ -6,6 +6,8 @@ #include "shared-bindings/digitalio/DigitalInOut.h" +#include + #include #include @@ -15,15 +17,25 @@ void common_hal_digitalio_digitalinout_never_reset( digitalinout_result_t common_hal_digitalio_digitalinout_construct( digitalio_digitalinout_obj_t *self, const mcu_pin_obj_t *pin) { - claim_pin(pin); + // Claim the pin in the iobroker module so that bus allocations refuse + // it while this object holds it. The call also resolves the GPIO + // controller device and pin number from the pin's global number; they are + // kept in the object for every later pad operation. + int ret = iobroker_gpio_allocate(pin->package_pin, &self->port, &self->number); + if (ret < 0) { + return DIGITALINOUT_PIN_BUSY; + } + self->pin = pin; - if (!device_is_ready(pin->port)) { + if (!device_is_ready(self->port)) { printk("Port device not ready\n"); + common_hal_digitalio_digitalinout_deinit(self); return DIGITALINOUT_PIN_BUSY; } - if (gpio_pin_configure(pin->port, pin->number, GPIO_INPUT) != 0) { + if (gpio_pin_configure(self->port, self->number, GPIO_INPUT) != 0) { + common_hal_digitalio_digitalinout_deinit(self); return DIGITALINOUT_PIN_BUSY; } self->direction = DIRECTION_INPUT; @@ -40,7 +52,7 @@ void common_hal_digitalio_digitalinout_deinit(digitalio_digitalinout_obj_t *self return; } - reset_pin(self->pin); + (void)iobroker_gpio_release(self->port, self->number); self->pin = NULL; } @@ -68,7 +80,7 @@ digitalio_direction_t common_hal_digitalio_digitalinout_get_direction( void common_hal_digitalio_digitalinout_set_value( digitalio_digitalinout_obj_t *self, bool value) { - int res = gpio_pin_set(self->pin->port, self->pin->number, value); + int res = gpio_pin_set(self->port, self->number, value); if (res != 0) { printk("Failed to set value %d\n", res); } @@ -81,7 +93,7 @@ bool common_hal_digitalio_digitalinout_get_value( if (self->direction == DIRECTION_OUTPUT) { return self->value; } - return gpio_pin_get(self->pin->port, self->pin->number) == 1; + return gpio_pin_get(self->port, self->number) == 1; } digitalinout_result_t common_hal_digitalio_digitalinout_set_drive_mode( @@ -92,7 +104,7 @@ digitalinout_result_t common_hal_digitalio_digitalinout_set_drive_mode( if (drive_mode == DRIVE_MODE_OPEN_DRAIN) { flags |= GPIO_OPEN_DRAIN; } - int res = gpio_pin_configure(self->pin->port, self->pin->number, flags); + int res = gpio_pin_configure(self->port, self->number, flags); if (res != 0) { // TODO: Fake open drain. printk("Failed to set drive mode %d\n", res); @@ -115,7 +127,7 @@ digitalinout_result_t common_hal_digitalio_digitalinout_set_pull( } else if (pull == PULL_DOWN) { pull_flags = GPIO_PULL_DOWN; } - if (gpio_pin_configure(self->pin->port, self->pin->number, GPIO_INPUT | pull_flags) != 0) { + if (gpio_pin_configure(self->port, self->number, GPIO_INPUT | pull_flags) != 0) { return DIGITALINOUT_INVALID_PULL; } self->pull = pull; diff --git a/ports/zephyr-cp/common-hal/digitalio/DigitalInOut.h b/ports/zephyr-cp/common-hal/digitalio/DigitalInOut.h index 9e0c3265268..4d21bb46a10 100644 --- a/ports/zephyr-cp/common-hal/digitalio/DigitalInOut.h +++ b/ports/zephyr-cp/common-hal/digitalio/DigitalInOut.h @@ -14,6 +14,10 @@ typedef struct { mp_obj_base_t base; const mcu_pin_obj_t *pin; + // GPIO controller device and pin number within it, resolved from the + // pin's global number by the gpio allocate call at construct time. + const struct device *port; + gpio_pin_t number; digitalio_direction_t direction; bool value; digitalio_drive_mode_t drive_mode; diff --git a/ports/zephyr-cp/common-hal/microcontroller/Pin.c b/ports/zephyr-cp/common-hal/microcontroller/Pin.c index 66882b6b5f0..c5832c13505 100644 --- a/ports/zephyr-cp/common-hal/microcontroller/Pin.c +++ b/ports/zephyr-cp/common-hal/microcontroller/Pin.c @@ -5,69 +5,67 @@ // SPDX-License-Identifier: MIT #include "shared-bindings/microcontroller/Pin.h" -#include "shared-bindings/digitalio/DigitalInOut.h" #include "py/mphal.h" -// Bit mask of claimed pins on each of up to two ports. nrf52832 has one port; nrf52840 has two. -// static uint32_t claimed_pins[GPIO_COUNT]; -// static uint32_t never_reset_pins[GPIO_COUNT]; +#include -void reset_all_pins(void) { - // for (size_t i = 0; i < GPIO_COUNT; i++) { - // claimed_pins[i] = never_reset_pins[i]; - // } - - // for (uint32_t pin = 0; pin < NUMBER_OF_PINS; ++pin) { - // if ((never_reset_pins[nrf_pin_port(pin)] & (1 << nrf_relative_pin_number(pin))) != 0) { - // continue; - // } - // nrf_gpio_cfg_default(pin); - // } - - // // After configuring SWD because it may be shared. - // reset_speaker_enable_pin(); -} - -// Mark pin as free and return it to a quiescent state. -void reset_pin(const mcu_pin_obj_t *pin) { +// Pin claims and pad reset live in the iobroker module: the objects that use +// a pin (busio buses through iobroker_*_allocate(), digitalio and rotaryio +// through iobroker_gpio_allocate()) own the claim and give it up again when +// they deinit, so the port keeps no claim table of its own. +// iobroker_pin_in_use() answers whether a pin is taken. - // Clear claimed bit. - // claimed_pins[nrf_pin_port(pin_number)] &= ~(1 << nrf_relative_pin_number(pin_number)); - // never_reset_pins[nrf_pin_port(pin_number)] &= ~(1 << nrf_relative_pin_number(pin_number)); +void reset_all_pins(void) { + // Nothing to do: pins belong to the objects that allocated them through + // iobroker, and those release their claims (and reset the pads) when they + // deinit. } - void never_reset_pin_number(uint8_t pin_number) { - // never_reset_pins[nrf_pin_port(pin_number)] |= 1 << nrf_relative_pin_number(pin_number); + // Deprecated single-byte pin number API; not used by this port. + (void)pin_number; } void common_hal_never_reset_pin(const mcu_pin_obj_t *pin) { - never_reset_pin_number(pin->number); + // Nothing to mark: reset_all_pins() leaves pins alone, so there is no + // reset to opt out of. + (void)pin; } void common_hal_reset_pin(const mcu_pin_obj_t *pin) { - if (pin == NULL) { - return; - } - reset_pin(pin); + // iobroker resets the pads when the object holding them releases its + // claim; a pin on its own has nothing to reset here. + (void)pin; } -void claim_pin(const mcu_pin_obj_t *pin) { - // Set bit in claimed_pins bitmask. - // claimed_pins[nrf_pin_port(pin->number)] |= 1 << nrf_relative_pin_number(pin->number); +bool pin_number_is_free(uint8_t pin_number) { + // Deprecated single-byte pin number API; not used by this port. + (void)pin_number; + return true; } +bool common_hal_mcu_pin_is_free(const mcu_pin_obj_t *pin) { + if (pin == NULL) { + return true; + } + return !iobroker_pin_in_use(pin->package_pin); +} -bool pin_number_is_free(uint8_t pin_number) { - return false; // !(claimed_pins[nrf_pin_port(pin_number)] & (1 << nrf_relative_pin_number(pin_number))); +void common_hal_mcu_pin_claim(const mcu_pin_obj_t *pin) { + // iobroker records the claim when the object using the pin allocates it; + // a bare claim has nothing to record and nobody to release it. + (void)pin; } -bool common_hal_mcu_pin_is_free(const mcu_pin_obj_t *pin) { - return true; +uint8_t common_hal_mcu_pin_number(const mcu_pin_obj_t *pin) { + return (uint8_t)pin->number; +} +void common_hal_mcu_pin_claim_number(uint8_t pin_no) { + (void)pin_no; } -void common_hal_mcu_pin_claim(const mcu_pin_obj_t *pin) { - claim_pin(pin); +void common_hal_mcu_pin_reset_number(uint8_t pin_no) { + (void)pin_no; } diff --git a/ports/zephyr-cp/common-hal/microcontroller/Pin.h b/ports/zephyr-cp/common-hal/microcontroller/Pin.h index d38ab9bd200..86c2eb5a6af 100644 --- a/ports/zephyr-cp/common-hal/microcontroller/Pin.h +++ b/ports/zephyr-cp/common-hal/microcontroller/Pin.h @@ -9,16 +9,21 @@ #include "py/mphal.h" #include "py/obj.h" +#include #include typedef struct { mp_obj_base_t base; - const struct device *port; - gpio_pin_t number; + // Global pin number: gpio port index * 32 + pin within the port. The + // GPIO controller device and pin number within it are resolved from it + // when needed (iobroker_gpio_split()). + uint16_t number; + // Package pin of the SoC package the pad is bonded to, resolved at + // build time from the board's package pin map. IOBROKER_NO_PIN when the + // pad has no entry in the map (or the SoC has no package pin map). + package_pin_t package_pin; } mcu_pin_obj_t; #include "autogen-pins.h" void reset_all_pins(void); -void reset_pin(const mcu_pin_obj_t *pin); -void claim_pin(const mcu_pin_obj_t *pin); diff --git a/ports/zephyr-cp/common-hal/rotaryio/IncrementalEncoder.c b/ports/zephyr-cp/common-hal/rotaryio/IncrementalEncoder.c index d36b571535a..4901bbbe43b 100644 --- a/ports/zephyr-cp/common-hal/rotaryio/IncrementalEncoder.c +++ b/ports/zephyr-cp/common-hal/rotaryio/IncrementalEncoder.c @@ -12,6 +12,7 @@ #include "py/runtime.h" #include +#include #include #include #include @@ -27,8 +28,8 @@ static void incrementalencoder_gpio_callback(const struct device *port, return; } - int a = gpio_pin_get(self->pin_a->port, self->pin_a->number); - int b = gpio_pin_get(self->pin_b->port, self->pin_b->number); + int a = gpio_pin_get(self->port_a, self->number_a); + int b = gpio_pin_get(self->port_b, self->number_b); if (a < 0 || b < 0) { return; } @@ -45,18 +46,34 @@ void common_hal_rotaryio_incrementalencoder_construct(rotaryio_incrementalencode self->pin_b = pin_b; self->divisor = 4; - if (!device_is_ready(pin_a->port) || !device_is_ready(pin_b->port)) { + // Claim both pins in the iobroker module so that bus allocations + // refuse them while this object holds them. The calls also resolve the + // GPIO controller devices and pin numbers from the pins' global numbers; + // they are kept in the object for every later pad operation. + int ret = iobroker_gpio_allocate(pin_a->package_pin, &self->port_a, &self->number_a); + if (ret < 0) { + common_hal_rotaryio_incrementalencoder_deinit(self); + raise_zephyr_error(ret); + } + + ret = iobroker_gpio_allocate(pin_b->package_pin, &self->port_b, &self->number_b); + if (ret < 0) { + common_hal_rotaryio_incrementalencoder_deinit(self); + raise_zephyr_error(ret); + } + + if (!device_is_ready(self->port_a) || !device_is_ready(self->port_b)) { common_hal_rotaryio_incrementalencoder_deinit(self); raise_zephyr_error(-ENODEV); } - int result = gpio_pin_configure(pin_a->port, pin_a->number, GPIO_INPUT | GPIO_PULL_UP); + int result = gpio_pin_configure(self->port_a, self->number_a, GPIO_INPUT | GPIO_PULL_UP); if (result != 0) { common_hal_rotaryio_incrementalencoder_deinit(self); raise_zephyr_error(result); } - result = gpio_pin_configure(pin_b->port, pin_b->number, GPIO_INPUT | GPIO_PULL_UP); + result = gpio_pin_configure(self->port_b, self->number_b, GPIO_INPUT | GPIO_PULL_UP); if (result != 0) { common_hal_rotaryio_incrementalencoder_deinit(self); raise_zephyr_error(result); @@ -64,8 +81,8 @@ void common_hal_rotaryio_incrementalencoder_construct(rotaryio_incrementalencode self->callback_a.encoder = self; gpio_init_callback(&self->callback_a.callback, incrementalencoder_gpio_callback, - BIT(pin_a->number)); - result = gpio_add_callback(pin_a->port, &self->callback_a.callback); + BIT(self->number_a)); + result = gpio_add_callback(self->port_a, &self->callback_a.callback); if (result != 0) { common_hal_rotaryio_incrementalencoder_deinit(self); raise_zephyr_error(result); @@ -73,32 +90,29 @@ void common_hal_rotaryio_incrementalencoder_construct(rotaryio_incrementalencode self->callback_b.encoder = self; gpio_init_callback(&self->callback_b.callback, incrementalencoder_gpio_callback, - BIT(pin_b->number)); - result = gpio_add_callback(pin_b->port, &self->callback_b.callback); + BIT(self->number_b)); + result = gpio_add_callback(self->port_b, &self->callback_b.callback); if (result != 0) { common_hal_rotaryio_incrementalencoder_deinit(self); raise_zephyr_error(result); } - result = gpio_pin_interrupt_configure(pin_a->port, pin_a->number, GPIO_INT_EDGE_BOTH); + result = gpio_pin_interrupt_configure(self->port_a, self->number_a, GPIO_INT_EDGE_BOTH); if (result != 0) { common_hal_rotaryio_incrementalencoder_deinit(self); raise_zephyr_error(result); } - result = gpio_pin_interrupt_configure(pin_b->port, pin_b->number, GPIO_INT_EDGE_BOTH); + result = gpio_pin_interrupt_configure(self->port_b, self->number_b, GPIO_INT_EDGE_BOTH); if (result != 0) { common_hal_rotaryio_incrementalencoder_deinit(self); raise_zephyr_error(result); } - int a = gpio_pin_get(pin_a->port, pin_a->number); - int b = gpio_pin_get(pin_b->port, pin_b->number); + int a = gpio_pin_get(self->port_a, self->number_a); + int b = gpio_pin_get(self->port_b, self->number_b); uint8_t quiescent_state = ((uint8_t)(a > 0) << 1) | (uint8_t)(b > 0); shared_module_softencoder_state_init(self, quiescent_state); - - claim_pin(pin_a); - claim_pin(pin_b); } bool common_hal_rotaryio_incrementalencoder_deinited(rotaryio_incrementalencoder_obj_t *self) { @@ -111,14 +125,22 @@ void common_hal_rotaryio_incrementalencoder_deinit(rotaryio_incrementalencoder_o } // Best-effort cleanup. During failed construct(), some of these may not be - // initialized yet. Ignore cleanup errors. - gpio_pin_interrupt_configure(self->pin_a->port, self->pin_a->number, GPIO_INT_DISABLE); - gpio_pin_interrupt_configure(self->pin_b->port, self->pin_b->number, GPIO_INT_DISABLE); - gpio_remove_callback(self->pin_a->port, &self->callback_a.callback); - gpio_remove_callback(self->pin_b->port, &self->callback_b.callback); - - reset_pin(self->pin_a); - reset_pin(self->pin_b); + // initialized yet. Ignore cleanup errors. The pad operations are only run + // when both claims were taken, so that the devices are valid. + if (self->port_a != NULL && self->port_b != NULL) { + gpio_pin_interrupt_configure(self->port_a, self->number_a, GPIO_INT_DISABLE); + gpio_pin_interrupt_configure(self->port_b, self->number_b, GPIO_INT_DISABLE); + gpio_remove_callback(self->port_a, &self->callback_a.callback); + gpio_remove_callback(self->port_b, &self->callback_b.callback); + } + if (self->port_a != NULL) { + (void)iobroker_gpio_release(self->port_a, self->number_a); + self->port_a = NULL; + } + if (self->port_b != NULL) { + (void)iobroker_gpio_release(self->port_b, self->number_b); + self->port_b = NULL; + } common_hal_rotaryio_incrementalencoder_mark_deinit(self); } @@ -126,4 +148,6 @@ void common_hal_rotaryio_incrementalencoder_deinit(rotaryio_incrementalencoder_o void common_hal_rotaryio_incrementalencoder_mark_deinit(rotaryio_incrementalencoder_obj_t *self) { self->pin_a = NULL; self->pin_b = NULL; + self->port_a = NULL; + self->port_b = NULL; } diff --git a/ports/zephyr-cp/common-hal/rotaryio/IncrementalEncoder.h b/ports/zephyr-cp/common-hal/rotaryio/IncrementalEncoder.h index a0d2bb392e2..55ab26c65d4 100644 --- a/ports/zephyr-cp/common-hal/rotaryio/IncrementalEncoder.h +++ b/ports/zephyr-cp/common-hal/rotaryio/IncrementalEncoder.h @@ -22,6 +22,12 @@ struct rotaryio_incrementalencoder_obj { mp_obj_base_t base; const mcu_pin_obj_t *pin_a; const mcu_pin_obj_t *pin_b; + // GPIO controller devices and pin numbers within them, resolved from the + // pins' global numbers by the gpio allocate calls at construct time. + const struct device *port_a; + gpio_pin_t number_a; + const struct device *port_b; + gpio_pin_t number_b; rotaryio_incrementalencoder_gpio_callback_t callback_a; rotaryio_incrementalencoder_gpio_callback_t callback_b; uint8_t state; // diff --git a/ports/zephyr-cp/cptools/build_circuitpython.py b/ports/zephyr-cp/cptools/build_circuitpython.py index 0a2a6ed0ae9..b2694cbb07a 100644 --- a/ports/zephyr-cp/cptools/build_circuitpython.py +++ b/ports/zephyr-cp/cptools/build_circuitpython.py @@ -114,7 +114,7 @@ # Other flags to set when a module is enabled EXTRA_FLAGS = { "audiobusio": {"AUDIOBUSIO_I2SOUT": 1, "AUDIOBUSIO_PDMIN": 0}, - "busio": {"BUSIO_SPI": 1, "BUSIO_I2C": 1}, + "busio": {"BUSIO_SPI": 1, "BUSIO_I2C": 1, "BUSIO_UART": 1}, "rotaryio": {"ROTARYIO_SOFTENCODER": 1}, "synthio": {"SYNTHIO_MAX_CHANNELS": 12}, } diff --git a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py index b147ae0605e..988e55abff7 100644 --- a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py +++ b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py @@ -15,7 +15,15 @@ sys.modules["cpbuild"] = type(sys)("cpbuild") sys.modules["cpbuild"].run_in_thread = lambda x: x -from zephyr2cp import find_flash_devices, find_ram_regions, BLOCKED_FLASH_COMPAT, MINIMUM_RAM_SIZE +import pytest + +from zephyr2cp import ( + find_flash_devices, + find_ram_regions, + BLOCKED_FLASH_COMPAT, + MINIMUM_RAM_SIZE, + add_toml_pin_names, +) def parse_dts_string(dts_content): @@ -587,3 +595,109 @@ def test_board_with_chosen_memory_region(self): assert len(rams) == 2 assert rams[0][0] == "axisram2" assert rams[1][0] == "axisram1" + + +class TestAddTomlPinNames: + """Test suite for add_toml_pin_names.""" + + def _add(self, toml_pins, package_pins, port_indexes, ioports): + board_names = {} + add_toml_pin_names( + board_names, {"pins": toml_pins}, package_pins, "nrf54l15_qfn48", port_indexes, ioports + ) + return board_names + + def test_package_pin_number(self): + package_pins = [ + {"pin": 1, "pad": 32, "pad_name": "P1.00"}, + {"pin": 5, "pad": 7, "pad_name": "P0.07"}, + ] + board_names = self._add({"LED": 5}, package_pins, {"gpio0": 0, "gpio1": 1}, {"gpio0": {7}}) + assert board_names == {("gpio0", 7): ["LED"]} + + def test_ball_id(self): + package_pins = [{"pin": 12, "pad": 47, "pad_name": "P1.15", "ball": "B2"}] + board_names = self._add( + {"LED": "b2"}, package_pins, {"gpio0": 0, "gpio1": 1}, {"gpio1": {15}} + ) + assert board_names == {("gpio1", 15): ["LED"]} + + def test_name_sanitized(self): + package_pins = [{"pin": 3, "pad": 0, "pad_name": "P0.00"}] + board_names = self._add({"boot button": 3}, package_pins, {"gpio0": 0}, {"gpio0": {0}}) + assert board_names == {("gpio0", 0): ["BOOT_BUTTON"]} + + def test_missing_package_map_raises(self): + with pytest.raises(RuntimeError, match="package pin map"): + add_toml_pin_names( + {}, {"pins": {"LED": 1}}, None, "custom", {"gpio0": 0}, {"gpio0": {0}} + ) + + def test_unknown_package_pin_raises(self): + package_pins = [{"pin": 1, "pad": 32, "pad_name": "P1.00"}] + with pytest.raises(RuntimeError, match="package pin 9 is not in"): + self._add({"LED": 9}, package_pins, {"gpio0": 0, "gpio1": 1}, {"gpio0": {0}}) + + def test_unknown_ball_raises(self): + package_pins = [{"pin": 1, "pad": 32, "pad_name": "P1.00"}] + with pytest.raises(RuntimeError, match="ball Z9"): + self._add({"LED": "Z9"}, package_pins, {"gpio0": 0, "gpio1": 1}, {"gpio0": {0}}) + + def test_bad_value_type_raises(self): + package_pins = [{"pin": 1, "pad": 32, "pad_name": "P1.00"}] + with pytest.raises(RuntimeError, match="package pin number"): + self._add({"LED": True}, package_pins, {"gpio0": 0, "gpio1": 1}, {"gpio0": {0}}) + + def test_bad_name_raises(self): + package_pins = [{"pin": 1, "pad": 32, "pad_name": "P1.00"}] + with pytest.raises(RuntimeError, match="not usable"): + self._add({"1 LED": 1}, package_pins, {"gpio0": 0, "gpio1": 1}, {"gpio0": {0}}) + + def test_pad_not_on_gpio_controller_raises(self): + package_pins = [{"pin": 1, "pad": 32, "pad_name": "P1.00"}] + with pytest.raises(RuntimeError, match="not on an enabled GPIO controller"): + self._add({"LED": 1}, package_pins, {"gpio0": 0, "gpio1": 1}, {"gpio0": {0}}) + + def test_no_pins_is_noop(self): + board_names = {} + add_toml_pin_names(board_names, {}, None, None, {"gpio0": 0}, {"gpio0": {0}}) + assert board_names == {} + + +class TestAddTomlPinNamesDuplicates: + """Deduplication and conflict behavior of add_toml_pin_names.""" + + PACKAGE_PINS = [ + {"pin": 5, "pad": 36, "pad_name": "P1.04"}, + {"pin": 27, "pad": 2, "pad_name": "P0.02"}, + ] + PORT_INDEXES = {"gpio0": 0, "gpio1": 1} + IOPORTS = {"gpio0": {2}, "gpio1": {4}} + + def _add(self, toml_pins, board_names=None): + board_names = dict(board_names or {}) + add_toml_pin_names( + board_names, + {"pins": toml_pins}, + self.PACKAGE_PINS, + "nrf54l15_qfn48", + self.PORT_INDEXES, + self.IOPORTS, + ) + return board_names + + def test_dedupes_two_keys_same_name_same_pin(self): + board_names = self._add({"MY PIN": 5, "my-pin": 5}) + assert board_names == {("gpio1", 4): ["MY_PIN"]} + + def test_conflict_two_keys_same_name_different_pins(self): + with pytest.raises(RuntimeError, match="already maps to gpio1 pin 4"): + self._add({"MY PIN": 5, "my-pin": 27}) + + def test_dedupes_existing_devicetree_name_same_pin(self): + board_names = self._add({"green led": 5}, {("gpio1", 4): ["Green LED"]}) + assert board_names == {("gpio1", 4): ["Green LED"]} + + def test_conflict_existing_devicetree_name_different_pin(self): + with pytest.raises(RuntimeError, match="already maps to gpio0 pin 2"): + self._add({"STATUS": 5}, {("gpio0", 2): ["STATUS"]}) diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index cc40ccfda85..f58f4ce75b2 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -1,6 +1,7 @@ import logging import pathlib import re +import tomllib import cpbuild import yaml @@ -487,6 +488,37 @@ def find_ram_regions(device_tree): INPUT_KEY_NAMES = {} +# Mask selecting the nRF pin number field (absolute pin, port*32+pin) of +# a pinctrl psel entry. The pin control entry uses all-ones in this field to +# mark a disconnected signal (NRF_PIN_DISCONNECTED). +NRF_PIN_FIELD_MASK = 0x1FF + + +def _pinctrl_default_psels(node): + """Return the raw nRF psel entries of a node's "default" pinctrl state. + + The state node (referenced by pinctrl-0) groups its configuration in + child nodes (typically named group1, group2, ...) that each carry a + psels property. + + Returns None when the node does not use pinctrl. + """ + prop = node.props.get("pinctrl-0") + if prop is None: + return None + psels = [] + try: + for state in prop.to_nodes(): + for group in state.nodes.values(): + if "psels" not in group.props: + continue + for value in group.props["psels"].to_nums(): + psels.append(value) + except (dtlib.DTError, KeyError): + return None + return psels + + def _populate_input_key_names(): header = ( pathlib.Path(__file__).parent.parent @@ -509,6 +541,107 @@ def _populate_input_key_names(): _populate_input_key_names() +def add_toml_pin_names( + board_names, mpconfigboard, package_pins, package_choice, port_indexes, ioports +): + """Add board pin names from circuitpython.toml's ``[pins]`` table. + + Each entry maps a board module name to a package pin number or, for ball + grid array packages, the datasheet's ball id (e.g. ``"A1"``). Entries are + resolved to SoC pads with the iobroker package pin map selected by the + ``IOBROKER_PACKAGE`` choice, and appended to ``board_names`` like + devicetree-derived names are. + + A name that already maps to the same pin (from the devicetree walk or an + earlier entry) is deduplicated; a name that would map to a different pin + than an existing entry is a build error. + """ + toml_pins = (mpconfigboard or {}).get("pins") + if not toml_pins: + return + if package_pins is None: + reason = "NONE" if package_choice == "none" else "unset" + raise RuntimeError( + f"circuitpython.toml [pins] needs an iobroker package pin map but " + f"CONFIG_IOBROKER_PACKAGE is {reason}" + ) + pad_of_package_pin = {} + pad_of_ball = {} + for package_pin_entry in package_pins: + pin = package_pin_entry["pin"] + if "pad" not in package_pin_entry: + # Unbonded or non-GPIO ball; not usable for pin names. + continue + pad = package_pin_entry["pad"] + if pin in pad_of_package_pin and pad_of_package_pin[pin] != pad: + raise RuntimeError(f"package pin {pin} maps to multiple pads in the package pin map") + pad_of_package_pin[pin] = pad + if "ball" in package_pin_entry: + ball = package_pin_entry["ball"] + if ball in pad_of_ball and pad_of_ball[ball] != pad: + raise RuntimeError(f"ball {ball} maps to multiple pads in the package pin map") + pad_of_ball[ball] = pad + port_label_of_index = {index: label for label, index in port_indexes.items()} + + def sanitize(name): + return name.upper().replace(" ", "_").replace("-", "_").replace("(", "").replace(")", "") + + # Names already in use from the devicetree walk, as sanitized name -> + # list of pins it is attached to. + pins_of_name = {} + for pin_key, names in board_names.items(): + for existing_name in names: + pins = pins_of_name.setdefault(sanitize(existing_name), []) + if pin_key not in pins: + pins.append(pin_key) + for name, package_pin in toml_pins.items(): + board_name = sanitize(name) + if not re.match(r"^[A-Z][A-Z0-9_]*$", board_name): + raise RuntimeError( + f"circuitpython.toml [pins] name {name!r} is not usable as a board module name" + ) + if isinstance(package_pin, str): + ball = package_pin.strip().upper() + if ball not in pad_of_ball: + raise RuntimeError( + f"circuitpython.toml [pins] name {name}: ball {package_pin} is not in " + f"the package pin map" + ) + pad = pad_of_ball[ball] + elif isinstance(package_pin, int) and not isinstance(package_pin, bool): + if package_pin not in pad_of_package_pin: + raise RuntimeError( + f"circuitpython.toml [pins] name {name}: package pin {package_pin} is " + f"not in the package pin map" + ) + pad = pad_of_package_pin[package_pin] + else: + raise RuntimeError( + f"circuitpython.toml [pins] name {name}: value must be a package pin number " + f"or ball id" + ) + label = port_label_of_index.get(pad // 32) + if label not in ioports: + raise RuntimeError( + f"circuitpython.toml [pins] name {name}: package pin {package_pin} is on " + f"pad {pad}, which is not on an enabled GPIO controller" + ) + pin_key = (label, pad % 32) + previous_pins = pins_of_name.get(board_name, []) + if previous_pins and pin_key not in previous_pins: + previous = ", ".join(f"{label} pin {num}" for label, num in previous_pins) + raise RuntimeError( + f"circuitpython.toml [pins] name {name}: already maps to {previous}, " + f"but [pins] assigns it to {label} pin {pad % 32}" + ) + if previous_pins: + # Same name already attached to this pin (devicetree or an + # earlier entry); nothing to add. + continue + pins_of_name[board_name] = [pin_key] + board_names.setdefault(pin_key, []).append(board_name) + + @cpbuild.run_in_thread def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfigboard=None): # noqa: C901 board_dir = builddir / "board" @@ -693,7 +826,21 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig if status == "okay": ioports[node.labels[0]] = set(range(0, ngpios)) if gpio_map and compatible and compatible[0] != "gpio-nexus": - connector_pins = CONNECTORS.get(compatible[0], None) + # Per-board connector names from circuitpython.toml's + # ``[connectors.]`` table take precedence. They key the + # gpio-map position (the header pin number as a string) to a name + # or a list of names, so boards whose silkscreen differs from the + # generic per-compatible list can supply their own. + connector_override = None + if node.labels: + connector_override = ( + (mpconfigboard or {}).get("connectors", {}).get(node.labels[0]) + ) + connector_pins = ( + connector_override + if connector_override is not None + else CONNECTORS.get(compatible[0], None) + ) if connector_pins is None: logger.warning(f"Unsupported connector mapping compatible: {compatible[0]}") else: @@ -701,16 +848,25 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig for offset, t, label in gpio_map._markers: if not label: continue - if i >= len(connector_pins): - logger.warning( - f"Connector mapping for {compatible[0]} has more pins than names; " - f"stopping at {len(connector_pins)}" - ) - break num = int.from_bytes(gpio_map.value[offset + 4 : offset + 8], "big") + if isinstance(connector_pins, dict): + pin_entry = connector_pins.get(str(i)) + if pin_entry is None: + logger.debug( + f"Connector {node.labels[0]} position {i} has no name; skipping" + ) + i += 1 + continue + else: + if i >= len(connector_pins): + logger.warning( + f"Connector mapping for {compatible[0]} has more pins than names; " + f"stopping at {len(connector_pins)}" + ) + break + pin_entry = connector_pins[i] if (label, num) not in board_names: board_names[(label, num)] = [] - pin_entry = connector_pins[i] if isinstance(pin_entry, list): board_names[(label, num)].extend(pin_entry) else: @@ -787,6 +943,51 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig pin_declarations = ["#pragma once"] mcu_pin_mapping = [] board_pin_mapping = [] + # Hardware port index of each GPIO controller; it defines the global pin + # numbering (index * 32 + pin) that pin objects and the iobroker module + # both use. On nRF SoCs the index comes from the label digits (gpio0 -> + # port 0, gpio6 -> port 6). + port_indexes = {} + for label in sorted(ioports.keys()): + match = re.match(r"^gpio(\d+)$", label) + port_indexes[label] = int(match.group(1)) if match else len(port_indexes) + # Package pin map selected through the IOBROKER_PACKAGE choice: map each + # SoC pad to the package pin it is bonded to so that the pin objects can + # hand package pins straight to the iobroker module. When no package + # applies to the SoC (IOBROKER_PACKAGE_NONE) there is no map, so the pin + # objects get IOBROKER_NO_PIN instead. + package_pin_of_pad = {} + package_pins = None + package_choice = None + if config_present: + for line in config.read_text().splitlines(): + stripped = line.strip() + if not stripped.startswith("CONFIG_IOBROKER_PACKAGE_") or not stripped.endswith("=y"): + continue + package_choice = stripped[len("CONFIG_IOBROKER_PACKAGE_") : -len("=y")].lower() + if package_choice == "none": + continue + package_toml = ( + pathlib.Path(__file__).resolve().parent.parent + / "modules" + / "iobroker" + / "packages" + / f"{package_choice}.toml" + ) + with package_toml.open("rb") as f: + package = tomllib.load(f) + package_pins = package["pins"] + for package_pin_entry in package_pins: + if "pad" in package_pin_entry: + package_pin_of_pad[package_pin_entry["pad"]] = package_pin_entry["pin"] + break + # Board pin names from circuitpython.toml: ``[pins]`` maps a board module + # name to a package pin number or ball id, resolved to a SoC pad with the + # package pin map above. This is independent of Zephyr's devicetree + # labels and aliases. + add_toml_pin_names( + board_names, mpconfigboard, package_pins, package_choice, port_indexes, ioports + ) for ioport in sorted(ioports.keys()): for num in ioports[ioport]: pin_object_name = f"P{ioport[len(shared_prefix) :].upper()}_{num:02d}" @@ -794,8 +995,11 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig status_led = pin_object_name if boot_button and (ioport, num) == boot_button: boot_button = pin_object_name + global_number = port_indexes[ioport] * 32 + num + package_pin = package_pin_of_pad.get(global_number) + package_pin_init = str(package_pin) if package_pin is not None else "IOBROKER_NO_PIN" pin_defs.append( - f"const mcu_pin_obj_t pin_{pin_object_name} = {{ .base.type = &mcu_pin_type, .port = DEVICE_DT_GET(DT_NODELABEL({ioport})), .number = {num}}};" + f"const mcu_pin_obj_t pin_{pin_object_name} = {{ .base.type = &mcu_pin_type, .number = {global_number}, .package_pin = {package_pin_init}}};" ) pin_declarations.append(f"extern const mcu_pin_obj_t pin_{pin_object_name};") mcu_pin_mapping.append( @@ -820,6 +1024,19 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig board_pin_mapping = "\n ".join(board_pin_mapping) mcu_pin_mapping = "\n ".join(mcu_pin_mapping) + # Bus instances the board enabled with all-disconnected pins are routed to + # arbitrary pins at runtime by busio objects instead of being exposed as + # fixed board.X() singletons. + iobroker_labels = set() + for driver in BUSIO_CLASSES: + for labels in active_zephyr_devices.get(driver, []): + node = device_tree.label2node[labels[0]] + psels = _pinctrl_default_psels(node) + if psels is not None and all( + (value & NRF_PIN_FIELD_MASK) == NRF_PIN_FIELD_MASK for value in psels + ): + iobroker_labels.add(labels[0]) + zephyr_binding_headers = [] zephyr_binding_objects = [] zephyr_binding_labels = [] @@ -857,6 +1074,10 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig if found_main: break for labels in instances: + if labels[0] in iobroker_labels: + # Dynamically routable instances are not exposed as board + # singletons; construct a busio object with pins instead. + continue instance_name = f"{driver.replace('/', '_')}_{labels[0]}" c_function_name = f"_{instance_name}" singleton_ptr = f"{c_function_name}_singleton" @@ -897,6 +1118,157 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig zephyr_binding_objects = "\n".join(zephyr_binding_objects) zephyr_binding_labels = "\n".join(zephyr_binding_labels) + # Generate tables of allocatable bus instances for the iobroker + # Zephyr module (dynamic pin routing; nRF SoCs). Instances enabled with + # all-disconnected pinctrl can be routed to arbitrary pins at runtime; + # instances with fixed devicetree pins are only usable when the requested + # pins match their state. + pinctrl_nrf = False + if config_present: + for line in config.read_text().splitlines(): + if line.startswith("CONFIG_PINCTRL_NRF="): + pinctrl_nrf = line.strip().endswith("=y") + break + + iobroker_includes = """ +#include +#include +""" + + iobroker_tables = "" + table_parts = [] + + # Map GPIO controller devices to their hardware port index. The indexes + # define the global pin numbering shared by the pin objects and the + # iobroker module, which resolves a global number back to the + # controller device and pin within it. Boards without GPIO controllers + # generate an empty table so that gpio_split() returns -EINVAL. + if ioports: + devices = ", ".join( + f"DEVICE_DT_GET(DT_NODELABEL({label}))" for label in sorted(ioports.keys()) + ) + indexes = ", ".join(str(port_indexes[label]) for label in sorted(ioports.keys())) + count = len(port_indexes) + else: + devices = "NULL" + indexes = "0" + count = 0 + table_parts.append( + f""" +const struct device * const iobroker_gpio_port_devices[] = {{ {devices} }}; +const uint8_t iobroker_gpio_port_indexes[] = {{ {indexes} }}; +const size_t iobroker_gpio_port_count = {count}; +""" + ) + + if pinctrl_nrf: + pool_kinds = (("i2c", "i2c"), ("spi", "spi"), ("serial", "uart")) + bus_table_parts = [] + for driver, pool in pool_kinds: + dynamic_entries = [] + fixed_entries = [] + for labels in active_zephyr_devices.get(driver, []): + node = device_tree.label2node[labels[0]] + if node in path2chosen: + # Console and other system devices are not allocatable. + continue + psels = _pinctrl_default_psels(node) + if psels is None or len(psels) > 4: + continue + if all((value & NRF_PIN_FIELD_MASK) == NRF_PIN_FIELD_MASK for value in psels): + dynamic_entries.append((labels[0], None, 0)) + else: + fixed_entries.append((labels[0], psels, len(psels))) + + entries = dynamic_entries + fixed_entries + + # Always define all three pools, even when empty: iobroker.c and + # the nRF routing code reference every pool's tables whenever + # CONFIG_PINCTRL_NRF is on, so an empty pool is still an empty + # array plus a zero count. + if not entries: + bus_table_parts.append( + "const iobroker_instance_t" + f" iobroker_{pool}_buses[] = {{}};\n" + "iobroker_state_t" + f" iobroker_{pool}_bus_states[ARRAY_SIZE(iobroker_{pool}_buses)];\n" + "const size_t" + f" iobroker_{pool}_bus_count = ARRAY_SIZE(iobroker_{pool}_buses);" + ) + continue + + entry_lines = [] + psel_arrays = [] + declares = [] + for label, psels, count in entries: + declares.append(f"PINCTRL_DT_DEV_CONFIG_DECLARE(DT_NODELABEL({label}));") + entry = ( + f" {{ .dev = DEVICE_DT_GET(DT_NODELABEL({label})), " + f".pcfg = PINCTRL_DT_DEV_CONFIG_GET(DT_NODELABEL({label}))" + ) + if psels is not None: + values = ", ".join(hex(value) for value in psels) + psel_arrays.append( + f"static const pinctrl_soc_pin_t cp_{label}_dt_psels[] = {{ {values} }};" + ) + entry += f", .dt_psels = cp_{label}_dt_psels, .dt_psel_count = {count}" + entry += " }," + entry_lines.append(entry) + + bus_table_parts.append( + "\n".join(declares) + + "\n\n" + + "\n".join(psel_arrays) + + f"\nconst iobroker_instance_t iobroker_{pool}_buses[] = {{\n" + + "\n".join(entry_lines) + + "\n};\n" + + f"iobroker_state_t iobroker_{pool}_bus_states[ARRAY_SIZE(iobroker_{pool}_buses)];\n" + + f"const size_t iobroker_{pool}_bus_count = ARRAY_SIZE(iobroker_{pool}_buses);" + ) + + iobroker_tables = ( + "#if defined(CONFIG_PINCTRL)\n" + "#include \n" + + "\n\n".join(bus_table_parts) + + "\n#endif // CONFIG_PINCTRL" + ) + + if pinctrl_nrf: + # Pads claimed at boot by fixed peripherals (console UART, flash + # instance, I2S, ...): their devicetree pinctrl "default" state points + # at real pads. iobroker reports these pads as always in use so that + # allocate() rejects requests for them with -EBUSY instead of + # re-routing pads that something else is already driving. Dynamically + # routable instances have all-disconnected default states and + # contribute nothing here. + reserved_pads = set() + for instances in active_zephyr_devices.values(): + for labels in instances: + node = device_tree.label2node[labels[0]] + psels = _pinctrl_default_psels(node) + if psels is None: + continue + for value in psels: + pad = value & NRF_PIN_FIELD_MASK + if pad != NRF_PIN_FIELD_MASK: + reserved_pads.add(pad) + if reserved_pads: + pads = ", ".join(str(pad) for pad in sorted(reserved_pads)) + reserved_table = ( + "const uint16_t iobroker_reserved_pads[] = { " + pads + " };\n" + "const size_t iobroker_reserved_pads_count = " + f"{len(reserved_pads)};" + ) + iobroker_tables = ( + "#if defined(CONFIG_PINCTRL)\n" + "#include \n" + + "\n\n".join(bus_table_parts + [reserved_table]) + + "\n#endif // CONFIG_PINCTRL" + ) + + if table_parts: + iobroker_tables = iobroker_tables + "\n\n" + "\n\n".join(table_parts) + # Generate i2sout_reset() that stops all board I2SOut instances if i2sout_instance_names: stop_calls = "\n ".join( @@ -1024,6 +1396,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig #include "py/mphal.h" {zephyr_binding_headers} +{iobroker_includes} {zephyr_display_header} const struct device* const flashes[] = {{ {", ".join(flashes)} }}; @@ -1038,6 +1411,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig {pin_defs} {zephyr_binding_objects} +{iobroker_tables} {zephyr_display_object} {i2sout_reset_func} diff --git a/ports/zephyr-cp/modules/iobroker/CMakeLists.txt b/ports/zephyr-cp/modules/iobroker/CMakeLists.txt new file mode 100644 index 00000000000..2e4f64a567d --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/CMakeLists.txt @@ -0,0 +1,84 @@ +# iobroker Zephyr module build. +# +# The core is always compiled so that callers (CircuitPython's common-hal +# code) can include and call the module directly on +# every SoC. SoC-specific routing implementations are added under +# src///; without one the allocate functions report -ENOSYS. + +zephyr_library() +# SoC-agnostic core: package pin resolution, GPIO claims, pin-in-use. +zephyr_library_sources(src/iobroker.c) + +# SoC implementations live in src///. The nRF implementation +# covers every nRF SoC (they share one pinctrl encoding), so the +# level is the nRF family. Other vendors add their own directories and a +# corresponding zephyr_library_sources_ifdef() line here. +zephyr_library_sources_ifdef(CONFIG_PINCTRL_NRF + src/nordic/nrf/iobroker_route.c +) +# Private headers shared by the core and the vendor implementations. +zephyr_library_include_directories(src) + +# Package pin map: render the TOML selected through the IOBROKER_PACKAGE +# choice into a build-directory translation unit. Kconfig has already run by +# the time module CMakeLists are included, so the CONFIG_ symbols are final +# here. When no reference map applies to the SoC, an empty map is generated +# instead, so package pin lookups fail cleanly with -EINVAL rather than the +# build failing to link. +set(IOBROKER_PACKAGE_CONFIGS + IOBROKER_PACKAGE_NRF54L15_QFN48 + IOBROKER_PACKAGE_NRF54LM20_CSP98 + IOBROKER_PACKAGE_NRF5340_QKAA + IOBROKER_PACKAGE_MDBT50Q_1MV2 + IOBROKER_PACKAGE_NRF52840_AQFN73 + IOBROKER_PACKAGE_RP2040_QFN56 +) +set(IOBROKER_PACKAGE_TOMLS + nrf54l15_qfn48 + nrf54lm20_csp98 + nrf5340_qkaa + mdbt50q_1mv2 + nrf52840_aqfn73 + rp2040_qfn56 +) +set(pkg_selected) +set(pkg_name) +foreach(pkg toml IN ZIP_LISTS IOBROKER_PACKAGE_CONFIGS IOBROKER_PACKAGE_TOMLS) + if(CONFIG_${pkg}) + set(pkg_selected ${pkg}) + set(pkg_name ${toml}) + endif() +endforeach() + +if(pkg_selected) + set(pkg_toml_file ${CMAKE_CURRENT_LIST_DIR}/packages/${pkg_name}.toml) + set(pkg_c_file ${CMAKE_CURRENT_BINARY_DIR}/package_pins_${pkg_name}.c) + add_custom_command(OUTPUT ${pkg_c_file} + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/tools/gen_package_c.py + ${pkg_toml_file} ${pkg_c_file} + DEPENDS ${pkg_toml_file} ${CMAKE_CURRENT_LIST_DIR}/tools/gen_package_c.py + COMMENT "iobroker: generating package pin map ${pkg_name}" + ) + set_source_files_properties(${pkg_c_file} PROPERTIES GENERATED TRUE) +else() + # No package map applies to this SoC (yet): an empty map keeps the symbols + # defined so the core can report -EINVAL for every package pin lookup. + set(pkg_c_file ${CMAKE_CURRENT_BINARY_DIR}/package_pins_empty.c) + file(WRITE ${pkg_c_file} + "// Generated by the iobroker module build -- no package pin map for this SoC.\n" + "#include \n" + "const iobroker_package_pin_t iobroker_package_pins[1] = {{0}};\n" + "const size_t iobroker_package_pin_count = 0;\n" + ) +endif() + +# The generated file is consumed by the zephyr target from the same +# directory, so order the compilation explicitly through a custom target. +add_custom_target(iobroker_package_pins DEPENDS ${pkg_c_file}) +add_dependencies(zephyr iobroker_package_pins) +target_sources(zephyr PRIVATE ${pkg_c_file}) + +# Register the public headers app-wide (zephyr_interface) so that external +# builds that consume Zephyr's exported compile flags, such as the +# CircuitPython static library build, can include them too. +zephyr_include_directories(include) diff --git a/ports/zephyr-cp/modules/iobroker/Kconfig b/ports/zephyr-cp/modules/iobroker/Kconfig new file mode 100644 index 00000000000..07e9bae8b07 --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/Kconfig @@ -0,0 +1,36 @@ +# iobroker: dynamic peripheral allocation and runtime pin routing. +# +# The module's core is always compiled (see CMakeLists.txt); callers call it +# directly. Runtime routing needs SoC support: today it is implemented for +# nRF SoCs, whose pin control encoding can be computed at runtime and whose +# peripherals can be routed to (almost) any pin via PSEL. On other SoCs the +# allocate functions always report -ENOSYS. + +config IOBROKER_GPIO_MAX_PINS + int "Maximum number of pins allocatable for GPIO use" + default 32 + help + Size of the registry of pins claimed for plain GPIO use via + iobroker_gpio_allocate(). + +choice IOBROKER_PACKAGE + prompt "SoC package pin map" + help + The module resolves requested package pins to SoC pads through the + package pin map selected here. Reference maps shipped with the module + live in packages/; when none applies to the SoC, the NONE option keeps + the map empty and every package pin lookup fails with -EINVAL. + +config IOBROKER_PACKAGE_NONE + bool "None (package pin lookups fail)" + help + No package pin map is available for this SoC yet. Package pin + lookups (and therefore pin claims and bus pin routing) fail with + -EINVAL until a reference map is added for the SoC. + +# Reference maps (one option per package transcribed from a SoC datasheet) +# live in Kconfig.packages; each depends on the SOC_* symbols of the SoCs the +# map applies to, so only the options valid for the selected SoC are visible. +rsource "Kconfig.packages" + +endchoice diff --git a/ports/zephyr-cp/modules/iobroker/Kconfig.packages b/ports/zephyr-cp/modules/iobroker/Kconfig.packages new file mode 100644 index 00000000000..a4b55bd4f8c --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/Kconfig.packages @@ -0,0 +1,71 @@ +# Reference package pin maps for the IOBROKER_PACKAGE choice. +# +# Generated by hand from the transcriptions in packages/*.toml (one option per +# TOML, depends on the SOC_* symbols of the SoCs the map applies to). When a +# package option is invisible the SoC does not come in that package, so the +# option cannot be selected or set from conf fragments either. +# +# Choice defaults live here too, most-specific first: the development kits' +# packages preselect the reference map for their SoC; boards with more than +# one matching package (e.g. nRF52840 modules) set their choice explicitly in +# board.conf; everything else falls back to NONE so package pin lookups fail +# cleanly. + +config IOBROKER_PACKAGE_NRF54L15_QFN48 + bool "nRF54L15/10/05 QFN48 (QFAA)" + depends on SOC_NRF54L15 || SOC_NRF54L10 || SOC_NRF54L05 + help + QFN48 package pin map for the nRF54L15, nRF54L10 and nRF54L05, which + are pin-compatible. 48 pins, 31 routable GPIOs. Used by the + nRF54L15 DK. + +config IOBROKER_PACKAGE_NRF54LM20_CSP98 + bool "nRF54LM20 CSP98 (PAAA)" + depends on SOC_NRF54LM20A || SOC_NRF54LM20B + help + CSP98 (flip-chip) package pin map for the nRF54LM20A and nRF54LM20B. + 98 balls, 66 routable GPIOs, numbered sequentially in row-major order + (the datasheet labels them A1..K10). Used by the nRF54LM20 DK. + +config IOBROKER_PACKAGE_NRF5340_QKAA + bool "nRF5340 aQFN94 (QKAA)" + depends on SOC_NRF5340_CPUAPP + help + aQFN94 package pin map for the nRF5340 application core. 94 balls + (plus four corner pads and a die pad, not in the map), all 48 GPIOs + bonded, numbered sequentially in row-major order (the datasheet only + labels the balls, e.g. A17). Used by the nRF5340 DK and the nRF7002 DK. + +config IOBROKER_PACKAGE_MDBT50Q_1MV2 + bool "Raytac MDBT50Q-1MV2 module" + depends on SOC_NRF52840_QIAA + help + Pin map for the Raytac MDBT50Q-1MV2 module, which carries the + nRF52840 aQFN73 inside. 'pin' numbers the module's 61 castellated + pins as the datasheet numbers them. + +config IOBROKER_PACKAGE_NRF52840_AQFN73 + bool "nRF52840 aQFN73 (NQI73)" + depends on SOC_NRF52840_QIAA + help + aQFN73 package pin map for the nRF52840. 73 balls, all 48 GPIOs + bonded, numbered sequentially in row-major order (the datasheet only + labels the balls, e.g. A8). + +config IOBROKER_PACKAGE_RP2040_QFN56 + bool "RP2040 QFN-56" + depends on SOC_RP2040 + help + QFN-56 package pin map for the RP2040. 30 user GPIOs (the package + pin map covers GPIO0..GPIO29; QSPI and power pins are omitted), + numbered as the datasheet numbers the pins. Used by the Feather + RP2040 board. + +choice IOBROKER_PACKAGE + default IOBROKER_PACKAGE_NRF54L15_QFN48 if SOC_NRF54L15 || SOC_NRF54L10 || SOC_NRF54L05 + default IOBROKER_PACKAGE_NRF54LM20_CSP98 if SOC_NRF54LM20A || SOC_NRF54LM20B + default IOBROKER_PACKAGE_NRF5340_QKAA if SOC_NRF5340_CPUAPP_QKAA + default IOBROKER_PACKAGE_NRF52840_AQFN73 if SOC_NRF52840_QIAA + default IOBROKER_PACKAGE_RP2040_QFN56 if SOC_RP2040 + default IOBROKER_PACKAGE_NONE +endchoice diff --git a/ports/zephyr-cp/modules/iobroker/README.md b/ports/zephyr-cp/modules/iobroker/README.md new file mode 100644 index 00000000000..0e360bb2ee0 --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/README.md @@ -0,0 +1,132 @@ +# iobroker + +A Zephyr module for **dynamic peripheral allocation and runtime pin routing**: +pick a free bus instance (I2C, SPI, UART) enabled in the devicetree, re-route +it to requested pins at runtime and hand the Zephyr device to the caller. + +Currently, runtime routing is implemented for nRF SoCs, whose pin control +encoding can be computed at runtime and whose peripherals can be routed to +(almost) any pin via PSEL. On other SoCs the module compiles but the allocate +functions always return `-ENOSYS`. + +## Source layout + +The source is organized by vendor and SoC family under `src/`: + +``` +src/ + iobroker.c # SoC-agnostic core: package pin map + # lookup, GPIO controller split, claim + # registry, -ENOSYS stubs when routing + # is unavailable + iobroker_internal.h # helpers shared between core and + # vendor implementations (private) + nordic/ + nrf/ # every nRF SoC shares one pinctrl + iobroker_route.c # encoding, so the family is one dir +``` + +The core compiles on every SoC. Each vendor adds a `src///` +directory with an implementation of the bus allocate/release functions and a +corresponding `zephyr_library_sources_ifdef()` line in `CMakeLists.txt`; +without one the core's `#if !IOBROKER_ROUTING` stubs report `-ENOSYS`. When +an implementation covers a whole SoC family (as the nRF one does, keyed on +`CONFIG_PINCTRL_NRF`), the directory is named for the family. + +## Enabling + +The module is registered by the CircuitPython Zephyr application +(`ports/zephyr-cp/CMakeLists.txt`) via `ZEPHYR_EXTRA_MODULES`, so it works +without west manifest changes. Its core is always compiled and its callers +include `` directly; runtime routing additionally +requires `CONFIG_PINCTRL_DYNAMIC` and `CONFIG_DEVICE_DEINIT_SUPPORT`, which +the port enables by default on nRF SoCs. + +## Board tables + +The core does not know which instances are allocatable; the application must +provide per-board tables (the generated `board.c` always emits them): + +```c +const iobroker_instance_t iobroker_i2c_buses[]; // + _states[] and _bus_count +const iobroker_instance_t iobroker_spi_buses[]; // ... +const iobroker_instance_t iobroker_uart_buses[]; // ... +const struct device * const iobroker_gpio_port_devices[]; // + _indexes[] and _count +const iobroker_package_pin_t iobroker_package_pins[]; // + _pin_count +const uint16_t iobroker_reserved_pads[]; // + _pin_count +``` + +The package pin map is selected from the module's reference maps: +`Kconfig.packages` offers one option per transcribed package +(`packages/*.toml`), each visible only for the SoCs it applies to and +preselected for the development kits. When no map applies to a SoC the +`IOBROKER_PACKAGE_NONE` choice is used and an empty map is generated, so +package pin lookups fail with `-EINVAL`. The selected TOML (or the empty +map) is rendered into a build-directory translation unit at build time. +New maps are transcribed from a SoC datasheet with `tools/gen_package.py` +(see the script's docstring; the datasheets live in `datasheets/`). + +Each instance entry contains the Zephyr device, its `struct +pinctrl_dev_config` (via `PINCTRL_DT_DEV_CONFIG_DECLARE`/`_GET`) and, when the +devicetree state has fixed pins, the raw `pinctrl_soc_pin_t` values of the +"default" state. Instances whose devicetree "default" state leaves every +signal disconnected (`NRF_PIN_DISCONNECTED`) set `.dt_psels = NULL` and can be +routed to any pin at runtime. Instances with fixed devicetree pins are only +allocatable when a request matches their existing state. + +In the CircuitPython tree these tables are generated into the board's +`board.c` by `cptools/zephyr2cp.py` at build time. + +`iobroker_reserved_pads` lists the SoC pads that fixed peripherals (console +UART, flash instance, I2S, ...) drive at boot through their devicetree +pinctrl default state. `iobroker_pin_in_use()` reports them as busy so that +allocate() rejects requests for those pads with `-EBUSY` instead of +re-routing pads something else is already driving. The table only contains +connected pins; dynamically routable instances (all signals disconnected in +the devicetree) contribute nothing. + +The GPIO controller table pairs each controller device with its hardware +port index. The indexes define the global pin numbering (index * 32 + pin +within the port) shared with the pin objects, and let the module resolve a +global pin number back to the controller device and pin number that +Zephyr's GPIO API takes. + +## Public API + +See `include/iobroker/iobroker.h`. Signals are requested with +`package_pin_t` values (`PACKAGE_PIN(n)`): the physical package pin numbered +as the SoC datasheet numbers it (QFN pins 1..N; CSP/BGA balls numbered +sequentially in row-major order, with the datasheet's ball label noted in the +map and the generated C). The module resolves package pins to SoC pads +through the package pin map, `iobroker_package_pins[]`, selected via +`CONFIG_IOBROKER_PACKAGE_*` (see above); internally the lookup then +proceeds package pin -> SoC pad -> peripheral routing. `IOBROKER_NO_PIN` leaves a +signal disconnected. Pull resistors are chosen by the module per bus signal +(I2C SDA/SCL and UART RX idle high). Every allocate call must be paired with +`iobroker_release()`, so the application owns the lifecycle. Releasing also +leaves the pins quiescent: `iobroker_release()` de-initializes the device, +which applies its low-power pinctrl state to the routed pins. +`iobroker_pin_in_use()` reports whether a package pin is claimed by a +currently allocated instance, and `allocate()` refuses (-EBUSY) requests that +would double-use a pin. Package pins can also be claimed for plain GPIO use +with `iobroker_gpio_allocate()` / `iobroker_gpio_release()`: the caller +configures the pad while the claim is held and the module returns it to a +quiescent state (disconnected) on release, and GPIO claims conflict with bus +allocations the same way bus allocations conflict with each other. +`iobroker_gpio_allocate()` resolves the package pin through the map +and returns both the GPIO controller device and the pin number within it. +`iobroker_gpio_package_pin()` maps a GPIO controller's hardware port index +and pin number (the two halves of the global pin numbering) back to the +package pin the pad is bonded to. + +## Externalizing + +This module is application-agnostic: it depends only on Zephyr. To move it +into its own repository: + +1. Copy this directory (minus this README) to the new repo root, keeping + `zephyr/module.yml`, `CMakeLists.txt`, `Kconfig`, `include/` and `src/`. +2. Point `ZEPHYR_EXTRA_MODULES` in `ports/zephyr-cp/CMakeLists.txt` at the new + checkout, or register it as a project in `zephyr-config/west.yml`. +3. Keep `iobroker.h`'s struct/function names stable — the generated + board tables are part of the module's de-facto API. diff --git a/ports/zephyr-cp/modules/iobroker/datasheets/README.md b/ports/zephyr-cp/modules/iobroker/datasheets/README.md new file mode 100644 index 00000000000..7bcec733a58 --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/datasheets/README.md @@ -0,0 +1,29 @@ +# Datasheets + +The package pin maps in `../packages/` are transcribed from Nordic's SoC +datasheets. The PDFs are not redistributed in this repository; download them +from Nordic (https://docs.nordicsemi.com) into this directory and verify them +against the SHA-256 hashes below before regenerating a map. + +| File | SHA-256 | +|------|---------| +| `nRF54L15_nRF54L10_nRF54L05_Datasheet_v1.0.pdf` | `ade0d340ba95e31f8299e6721b08d53a702962335d9b87fa202f8d2767603554` | +| `nRF54LM20A_nRF54LM20B_Datasheet_v1.0.pdf` | `03a834fb52be8cf6248df6f67737f055c035cfb83492d44d58bdc2f0bfa3a3af` | +| `nRF5340_PS_v1.6.pdf` | `fe5c7e8908c94080548a1ddd4ec8b96aa09a4b97239fe9b32afb49eb8cf2fc87` | +| `nRF52840_PS_v1.1.pdf` | `c619e336b9c0610663273041f057f2537a65fd408ce0c5b8214a26de2aa88422` | +| `[nRF52840] MDBT50Q-1MV2 & MDBT50Q-P1MV2_Ver.L spec.pdf` | `61fec8c0c9f8c33175be2237a8ebba73c6cfc0a3572fe3835fd341079c103d03` | +| `rp2040-datasheet.pdf` | `be56fbb75ba0ae9e26558a73c93ac3e75c2ad4e6878d3b6703de2a76d886ea8c` | + +The nRF52840 Product Specification is Nordic's, downloadable from +https://docs.nordicsemi.com; the copy here came from +https://cdn-learn.adafruit.com/assets/assets/000/092/427/original/nRF52840_PS_v1.1.pdf. +The MDBT50Q module datasheet is Raytac's, from +https://www.raytac.com. The RP2040 datasheet is Raspberry Pi's, from +https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf. + +The transcription command lines are recorded in the header of each +`packages/*.toml` (section number, package name, pin count). After running +`pdftotext -layout` on a verified PDF, the same command reproduces the map. + +This directory is ignored by git; this README is force-added so the hashes +stay with the code. diff --git a/ports/zephyr-cp/modules/iobroker/include/iobroker/iobroker.h b/ports/zephyr-cp/modules/iobroker/include/iobroker/iobroker.h new file mode 100644 index 00000000000..cdb268625bf --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/include/iobroker/iobroker.h @@ -0,0 +1,203 @@ +// iobroker: dynamic peripheral allocation and runtime pin routing. +// +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +// Allocate a free bus instance (I2C, SPI, UART) enabled in the devicetree, +// route it to the requested pins at runtime and hand the Zephyr device to the +// caller. Per-board tables of allocatable instances are provided by the +// application (the CircuitPython build generates them from the board's +// devicetree; see modules/iobroker/README.md for the contract). +// +// Runtime routing works on nRF SoCs because their pin control encoding is a +// simple bit-packed value (see zephyr/dt-bindings/pinctrl/nrf-pinctrl.h and +// soc/nordic/common/pinctrl_soc.h) that can be computed at runtime, and any +// peripheral function can be routed to (almost) any pin via PSEL. + +#pragma once + +#include +#include +#include + +#include +#include + +// Runtime bus routing lets the module re-route a bus instance's pins at +// runtime. It is available only on nRF SoCs, and only when the nRF pinctrl +// driver is built with dynamic pinctrl states and device de-init support. +// Without it the bus allocate/release functions report -ENOSYS and the bus +// instance tables are not used. +#if defined(CONFIG_PINCTRL_NRF) && defined(CONFIG_PINCTRL_DYNAMIC) && \ + defined(CONFIG_DEVICE_DEINIT_SUPPORT) +#define IOBROKER_ROUTING 1 +#else +#define IOBROKER_ROUTING 0 +#endif + +// Signals needed by the widest bus (UART with tx/rx/rts/cts). +#define IOBROKER_MAX_PINS 4 + +// Package pin that leaves a signal disconnected (only valid for optional +// signals such as UART rts/cts). +#define IOBROKER_NO_PIN ((package_pin_t)0xffff) + +// A physical pin of the SoC's package, numbered the way the SoC datasheet +// numbers its package pins (1..N for QFN-style packages; BGA-style packages +// use the datasheet's sequential numbering of the balls). Translating a pin +// name from a schematic or board layout to a package pin is a board concern; +// the module only knows the package pin -> SoC pad mapping below. +typedef uint16_t package_pin_t; + +// Wrap a datasheet package pin number into a package_pin_t. +#define PACKAGE_PIN(number) ((package_pin_t)(number)) + +// One entry of the package pin map: a physical package pin and the SoC pad +// it is bonded to (gpio port index * 32 + pin within the port; the same +// numbering that CircuitPython's Pin objects and the GPIO controller table +// below use). Package pins that are not bonded to a GPIO (power, analog +// only, ...) are omitted; requesting one fails with -EINVAL. +// +// The map is generated by the module build from the selected IOBROKER_PACKAGE +// choice (see packages/), or as an empty map when no package applies to the +// SoC. With an empty map every package pin lookup fails with -EINVAL. +typedef struct { + package_pin_t package_pin; + uint16_t soc_pad; +} iobroker_package_pin_t; + +extern const iobroker_package_pin_t iobroker_package_pins[]; +extern const size_t iobroker_package_pin_count; + +// GPIO controller table: the board lists every GPIO controller together with +// its hardware port index, which defines the global pin numbering +// (index * 32 + pin). Generated into board.c; boards without GPIO +// controllers generate an empty table, so lookups return -EINVAL. +extern const struct device *const iobroker_gpio_port_devices[]; +extern const uint8_t iobroker_gpio_port_indexes[]; +extern const size_t iobroker_gpio_port_count; + +// Resolve a global pin number to its GPIO controller device and the pin +// number within that controller (the two values Zephyr's GPIO API takes). +// Returns 0, or -EINVAL when no controller covers the number. +int iobroker_gpio_split(uint16_t number, const struct device **port_out, + gpio_pin_t *pin_out); + +// Map a GPIO controller's hardware port index and the pin number within it +// (the two halves of the global pin numbering: index * 32 + pin) to the +// package pin the pad is bonded to. The inverse of the package pin map's +// soc_pad column. Returns 0, or -EINVAL when the pad has no entry in the +// package pin map. +int iobroker_gpio_package_pin(uint8_t port, gpio_pin_t pin, + package_pin_t *package_pin_out); + +#if defined(CONFIG_PINCTRL_NRF) + +#include + +// Description of one allocatable bus instance. Filled in by the board tables. +typedef struct { + const struct device *dev; + // Pin control configuration of the device. Mutable because + // CONFIG_PINCTRL_DYNAMIC moves these to RAM so that states can be + // swapped at runtime. + struct pinctrl_dev_config *pcfg; + // Pin control entries of the devicetree "default" state, or NULL when + // the instance was enabled with disconnected pins so that it can be + // routed dynamically to any pin. + const pinctrl_soc_pin_t *dt_psels; + uint8_t dt_psel_count; +} iobroker_instance_t; + +// Runtime bookkeeping for one allocatable bus instance. The arrays live for +// the life of the firmware because Zephyr's pinctrl API keeps pointers to +// them inside the device's pinctrl_dev_config. +typedef struct { + pinctrl_soc_pin_t default_pins[IOBROKER_MAX_PINS]; + pinctrl_soc_pin_t sleep_pins[IOBROKER_MAX_PINS]; + struct pinctrl_state states[2]; + bool in_use; + // True when the instance's states were swapped for runtime-built entries. + bool routed; + // Package pins the current allocation claims (copies of the request). + // Used by iobroker_pin_in_use(); empty while the instance is free. + package_pin_t pins[IOBROKER_MAX_PINS]; + uint8_t pin_count; +} iobroker_state_t; + +// Generated board tables, emitted by the board's generated board.c whenever +// the nRF pinctrl driver is built. They are used only when runtime routing is +// available (IOBROKER_ROUTING); boards without it still define them. +extern const iobroker_instance_t iobroker_i2c_buses[]; +extern const size_t iobroker_i2c_bus_count; +extern iobroker_state_t iobroker_i2c_bus_states[]; + +extern const iobroker_instance_t iobroker_spi_buses[]; +extern const size_t iobroker_spi_bus_count; +extern iobroker_state_t iobroker_spi_bus_states[]; + +extern const iobroker_instance_t iobroker_uart_buses[]; +extern const size_t iobroker_uart_bus_count; +extern iobroker_state_t iobroker_uart_bus_states[]; + +// SoC pads owned by fixed peripherals (console UART, flash instance, I2S, +// ...): the pads their devicetree pinctrl default state drives at boot. +// iobroker_pin_in_use() reports these as always busy so that allocate() +// rejects requests for them with -EBUSY. +extern const uint16_t iobroker_reserved_pads[]; +extern const size_t iobroker_reserved_pads_count; + +#endif // CONFIG_PINCTRL_NRF + +// The functions below return 0 on success and set *dev_out to the Zephyr +// device of an allocated instance. A negative errno is returned on failure: +// -ENODEV: no compatible instance is free +// -ENOSYS: dynamic pin routing is unsupported on this SoC +// -EBUSY: a requested pin is already claimed by an allocated instance +// -EINVAL/-EIO: a pin or routing operation failed +// Optional signals may be disconnected (IOBROKER_NO_PIN). Every allocate +// call must be paired with iobroker_release(). +int iobroker_i2c_allocate(package_pin_t sda, package_pin_t scl, + const struct device **dev_out); +int iobroker_spi_allocate(package_pin_t clock, package_pin_t mosi, + package_pin_t miso, const struct device **dev_out); +int iobroker_uart_allocate(package_pin_t tx, package_pin_t rx, + package_pin_t rts, package_pin_t cts, const struct device **dev_out); + +// Returns true when the package pin is currently claimed by an allocated bus +// instance, a GPIO allocation, or a fixed peripheral (console UART, flash +// instance, ... whose pads are listed in iobroker_reserved_pads). Call this +// before allocate() to find out which requested pin is already in use. +// Disconnected signals (IOBROKER_NO_PIN) and package pins with no SoC pad are +// never in use. +bool iobroker_pin_in_use(package_pin_t pin); + +// Allocate a package pin for plain GPIO use. The module only claims it: while +// held, bus allocate() calls and further GPIO allocations fail with -EBUSY. +// The pin is resolved through the package pin map, and both values Zephyr's +// GPIO API needs are returned: *port_out receives the GPIO controller device +// and *pin_out the pin number within it, so that the caller can configure the +// pad itself (gpio_pin_configure()). Returns 0, or a negative errno: +// -EINVAL: pin is disconnected (IOBROKER_NO_PIN) or has no entry in the +// package pin map +// -EBUSY: the pin is already claimed +// -ENOMEM: the GPIO claim registry is full +int iobroker_gpio_allocate(package_pin_t pin, + const struct device **port_out, gpio_pin_t *pin_out); + +// Release a GPIO claim made with iobroker_gpio_allocate() and return the pad +// to a quiescent state: disconnected from any peripheral routing, input +// buffer and driver off, no pulls (a plain input on SoCs without +// GPIO_DISCONNECTED support). Pass the device and pin number that the +// allocate call returned. Returns true when a claim was held. +bool iobroker_gpio_release(const struct device *port, gpio_pin_t number); + +// Release an instance previously returned by one of the allocate functions. +// Returns true when the instance had been dynamically routed, in which case +// the caller is responsible for resetting the pins it had routed to it. The +// device is de-initialized on release, so it is left uninitialized; the +// caller initializes it again when it allocates the instance next. +bool iobroker_release(const struct device *dev); diff --git a/ports/zephyr-cp/modules/iobroker/packages/mdbt50q_1mv2.toml b/ports/zephyr-cp/modules/iobroker/packages/mdbt50q_1mv2.toml new file mode 100644 index 00000000000..a7ea7b89ae7 --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/packages/mdbt50q_1mv2.toml @@ -0,0 +1,302 @@ +# Package pin map for the Raytac MDBT50Q-1MV2 module (nRF52840 aQFN73 +# inside), transcribed from "[nRF52840] MDBT50Q-1MV2 & MDBT50Q-P1MV2_Ver.L +# spec.pdf", section 2.5 Pin Assignment, with a one-off pdftotext -layout +# parse. Review against the datasheet figure before editing by hand. +# +# 'pin' numbers the module's castellated pins as the datasheet numbers them +# (1..61); non-GPIO pins carry no pad. + +name = "mdbt50q_1mv2" +socs = ["SOC_NRF52840_QIAA"] + +[[pins]] +pin = 1 +name = "GND" # non-GPIO + +[[pins]] +pin = 2 +name = "GND" # non-GPIO + +[[pins]] +pin = 3 +pad = 42 +pad_name = "P1.10" + +[[pins]] +pin = 4 +pad = 43 +pad_name = "P1.11" + +[[pins]] +pin = 5 +pad = 44 +pad_name = "P1.12" + +[[pins]] +pin = 6 +pad = 45 +pad_name = "P1.13" + +[[pins]] +pin = 7 +pad = 46 +pad_name = "P1.14" + +[[pins]] +pin = 8 +pad = 47 +pad_name = "P1.15" + +[[pins]] +pin = 9 +pad = 3 +pad_name = "P0.03" + +[[pins]] +pin = 10 +pad = 29 +pad_name = "P0.29" + +[[pins]] +pin = 11 +pad = 2 +pad_name = "P0.02" + +[[pins]] +pin = 12 +pad = 31 +pad_name = "P0.31" + +[[pins]] +pin = 13 +pad = 28 +pad_name = "P0.28" + +[[pins]] +pin = 14 +pad = 30 +pad_name = "P0.30" + +[[pins]] +pin = 15 +name = "GND" # non-GPIO + +[[pins]] +pin = 16 +pad = 27 +pad_name = "P0.27" + +[[pins]] +pin = 17 +pad = 0 +pad_name = "P0.00" + +[[pins]] +pin = 18 +pad = 1 +pad_name = "P0.01" + +[[pins]] +pin = 19 +pad = 26 +pad_name = "P0.26" + +[[pins]] +pin = 20 +pad = 4 +pad_name = "P0.04" + +[[pins]] +pin = 21 +pad = 5 +pad_name = "P0.05" + +[[pins]] +pin = 22 +pad = 6 +pad_name = "P0.06" + +[[pins]] +pin = 23 +pad = 7 +pad_name = "P0.07" + +[[pins]] +pin = 24 +pad = 8 +pad_name = "P0.08" + +[[pins]] +pin = 25 +pad = 40 +pad_name = "P1.08" + +[[pins]] +pin = 26 +pad = 41 +pad_name = "P1.09" + +[[pins]] +pin = 27 +pad = 11 +pad_name = "P0.11" + +[[pins]] +pin = 28 +name = "VDD" # non-GPIO + +[[pins]] +pin = 29 +pad = 12 +pad_name = "P0.12" + +[[pins]] +pin = 30 +name = "VDD" # non-GPIO + +[[pins]] +pin = 31 +name = "DCCH" # non-GPIO + +[[pins]] +pin = 32 +name = "VBUS" # non-GPIO + +[[pins]] +pin = 33 +name = "GND" # non-GPIO + +[[pins]] +pin = 34 +name = "D-" # non-GPIO + +[[pins]] +pin = 35 +name = "D+" # non-GPIO + +[[pins]] +pin = 36 +pad = 14 +pad_name = "P0.14" + +[[pins]] +pin = 37 +pad = 13 +pad_name = "P0.13" + +[[pins]] +pin = 38 +pad = 16 +pad_name = "P0.16" + +[[pins]] +pin = 39 +pad = 15 +pad_name = "P0.15" + +[[pins]] +pin = 40 +pad = 18 +pad_name = "P0.18" + +[[pins]] +pin = 41 +pad = 17 +pad_name = "P0.17" + +[[pins]] +pin = 42 +pad = 19 +pad_name = "P0.19" + +[[pins]] +pin = 43 +pad = 21 +pad_name = "P0.21" + +[[pins]] +pin = 44 +pad = 20 +pad_name = "P0.20" + +[[pins]] +pin = 45 +pad = 23 +pad_name = "P0.23" + +[[pins]] +pin = 46 +pad = 22 +pad_name = "P0.22" + +[[pins]] +pin = 47 +pad = 32 +pad_name = "P1.00" + +[[pins]] +pin = 48 +pad = 24 +pad_name = "P0.24" + +[[pins]] +pin = 49 +pad = 25 +pad_name = "P0.25" + +[[pins]] +pin = 50 +pad = 34 +pad_name = "P1.02" + +[[pins]] +pin = 51 +name = "SWDIO" # non-GPIO + +[[pins]] +pin = 52 +pad = 9 +pad_name = "P0.09" + +[[pins]] +pin = 53 +name = "SWDCLK" # non-GPIO + +[[pins]] +pin = 54 +pad = 10 +pad_name = "P0.10" + +[[pins]] +pin = 55 +name = "GND" # non-GPIO + +[[pins]] +pin = 56 +pad = 36 +pad_name = "P1.04" + +[[pins]] +pin = 57 +pad = 38 +pad_name = "P1.06" + +[[pins]] +pin = 58 +pad = 39 +pad_name = "P1.07" + +[[pins]] +pin = 59 +pad = 37 +pad_name = "P1.05" + +[[pins]] +pin = 60 +pad = 35 +pad_name = "P1.03" + +[[pins]] +pin = 61 +pad = 33 +pad_name = "P1.01" diff --git a/ports/zephyr-cp/modules/iobroker/packages/nrf52840_aqfn73.toml b/ports/zephyr-cp/modules/iobroker/packages/nrf52840_aqfn73.toml new file mode 100644 index 00000000000..00334ea58a1 --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/packages/nrf52840_aqfn73.toml @@ -0,0 +1,301 @@ +# Package pin map for nrf52840_aqfn73, transcribed from nRF52840_PS_v1.1.pdf, +# section 7.1.1 (aQFN73 ball assignments), +# by tools/gen_package.py. Review against the datasheet figure before +# editing by hand; regenerate instead when the datasheet changes. + +# Ball packages: 'pin' numbers the balls sequentially in row-major +# order (the datasheet only labels them, e.g. B2). + +name = "nrf52840_aqfn73" +socs = ["SOC_NRF52840_QIAA"] + +[[pins]] +pin = 1 +ball = "A8" +pad = 31 +pad_name = "P0.31" + +[[pins]] +pin = 2 +ball = "A10" +pad = 29 +pad_name = "P0.29" + +[[pins]] +pin = 3 +ball = "A12" +pad = 2 +pad_name = "P0.02" + +[[pins]] +pin = 4 +ball = "A14" +pad = 47 +pad_name = "P1.15" + +[[pins]] +pin = 5 +ball = "A16" +pad = 45 +pad_name = "P1.13" + +[[pins]] +pin = 7 +ball = "A20" +pad = 42 +pad_name = "P1.10" + +[[pins]] +pin = 14 +ball = "B9" +pad = 30 +pad_name = "P0.30" + +[[pins]] +pin = 15 +ball = "B11" +pad = 28 +pad_name = "P0.28" + +[[pins]] +pin = 16 +ball = "B13" +pad = 3 +pad_name = "P0.03" + +[[pins]] +pin = 17 +ball = "B15" +pad = 46 +pad_name = "P1.14" + +[[pins]] +pin = 18 +ball = "B17" +pad = 44 +pad_name = "P1.12" + +[[pins]] +pin = 19 +ball = "B19" +pad = 43 +pad_name = "P1.11" + +[[pins]] +pin = 22 +ball = "D2" +pad = 0 +pad_name = "P0.00" + +[[pins]] +pin = 25 +ball = "F2" +pad = 1 +pad_name = "P0.01" + +[[pins]] +pin = 27 +ball = "G1" +pad = 26 +pad_name = "P0.26" + +[[pins]] +pin = 28 +ball = "H2" +pad = 27 +pad_name = "P0.27" + +[[pins]] +pin = 30 +ball = "J1" +pad = 4 +pad_name = "P0.04" + +[[pins]] +pin = 31 +ball = "J24" +pad = 10 +pad_name = "P0.10" + +[[pins]] +pin = 32 +ball = "K2" +pad = 5 +pad_name = "P0.05" + +[[pins]] +pin = 33 +ball = "L1" +pad = 6 +pad_name = "P0.06" + +[[pins]] +pin = 34 +ball = "L24" +pad = 9 +pad_name = "P0.09" + +[[pins]] +pin = 35 +ball = "M2" +pad = 7 +pad_name = "P0.07" + +[[pins]] +pin = 36 +ball = "N1" +pad = 8 +pad_name = "P0.08" + +[[pins]] +pin = 38 +ball = "P2" +pad = 40 +pad_name = "P1.08" + +[[pins]] +pin = 39 +ball = "P23" +pad = 39 +pad_name = "P1.07" + +[[pins]] +pin = 40 +ball = "R1" +pad = 41 +pad_name = "P1.09" + +[[pins]] +pin = 41 +ball = "R24" +pad = 38 +pad_name = "P1.06" + +[[pins]] +pin = 42 +ball = "T2" +pad = 11 +pad_name = "P0.11" + +[[pins]] +pin = 43 +ball = "T23" +pad = 37 +pad_name = "P1.05" + +[[pins]] +pin = 44 +ball = "U1" +pad = 12 +pad_name = "P0.12" + +[[pins]] +pin = 45 +ball = "U24" +pad = 36 +pad_name = "P1.04" + +[[pins]] +pin = 46 +ball = "V23" +pad = 35 +pad_name = "P1.03" + +[[pins]] +pin = 48 +ball = "W24" +pad = 34 +pad_name = "P1.02" + +[[pins]] +pin = 50 +ball = "Y23" +pad = 33 +pad_name = "P1.01" + +[[pins]] +pin = 54 +ball = "AC9" +pad = 14 +pad_name = "P0.14" + +[[pins]] +pin = 55 +ball = "AC11" +pad = 16 +pad_name = "P0.16" + +[[pins]] +pin = 56 +ball = "AC13" +pad = 18 +pad_name = "P0.18" + +[[pins]] +pin = 57 +ball = "AC15" +pad = 19 +pad_name = "P0.19" + +[[pins]] +pin = 58 +ball = "AC17" +pad = 21 +pad_name = "P0.21" + +[[pins]] +pin = 59 +ball = "AC19" +pad = 23 +pad_name = "P0.23" + +[[pins]] +pin = 60 +ball = "AC21" +pad = 25 +pad_name = "P0.25" + +[[pins]] +pin = 65 +ball = "AD8" +pad = 13 +pad_name = "P0.13" + +[[pins]] +pin = 66 +ball = "AD10" +pad = 15 +pad_name = "P0.15" + +[[pins]] +pin = 67 +ball = "AD12" +pad = 17 +pad_name = "P0.17" + +[[pins]] +pin = 69 +ball = "AD16" +pad = 20 +pad_name = "P0.20" + +[[pins]] +pin = 70 +ball = "AD18" +pad = 22 +pad_name = "P0.22" + +[[pins]] +pin = 71 +ball = "AD20" +pad = 24 +pad_name = "P0.24" + +[[pins]] +pin = 72 +ball = "AD22" +pad = 32 +pad_name = "P1.00" + +# Package pins without a GPIO pad (not routable): +# A18, A22, A23, B1, B3, B5, B7, B24, C1, D23, E24, F23, H23, N24, W1, Y2, AA24, AB2, AC5, AC24, AD2, AD4, AD6, AD14, AD23 diff --git a/ports/zephyr-cp/modules/iobroker/packages/nrf5340_qkaa.toml b/ports/zephyr-cp/modules/iobroker/packages/nrf5340_qkaa.toml new file mode 100644 index 00000000000..3d278be5fb2 --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/packages/nrf5340_qkaa.toml @@ -0,0 +1,301 @@ +# Package pin map for nrf5340_qkaa, transcribed from nRF5340_PS_v1.6.pdf, +# section 9.1.1 (aQFN94 pin assignments), +# by tools/gen_package.py. Review against the datasheet figure before +# editing by hand; regenerate instead when the datasheet changes. + +# Ball packages: 'pin' numbers the balls sequentially in row-major +# order (the datasheet only labels them, e.g. B2). + +name = "nrf5340_qkaa" +socs = ["SOC_NRF5340_CPUAPP"] + +[[pins]] +pin = 4 +ball = "A17" +pad = 45 +pad_name = "P1.13" + +[[pins]] +pin = 16 +ball = "B14" +pad = 47 +pad_name = "P1.15" + +[[pins]] +pin = 17 +ball = "B16" +pad = 46 +pad_name = "P1.14" + +[[pins]] +pin = 18 +ball = "B18" +pad = 44 +pad_name = "P1.12" + +[[pins]] +pin = 19 +ball = "B20" +pad = 43 +pad_name = "P1.11" + +[[pins]] +pin = 20 +ball = "B22" +pad = 31 +pad_name = "P0.31" + +[[pins]] +pin = 21 +ball = "B24" +pad = 30 +pad_name = "P0.30" + +[[pins]] +pin = 39 +ball = "M2" +pad = 32 +pad_name = "P1.00" + +[[pins]] +pin = 40 +ball = "N1" +pad = 0 +pad_name = "P0.00" + +[[pins]] +pin = 42 +ball = "P2" +pad = 33 +pad_name = "P1.01" + +[[pins]] +pin = 43 +ball = "R1" +pad = 1 +pad_name = "P0.01" + +[[pins]] +pin = 44 +ball = "R31" +pad = 42 +pad_name = "P1.10" + +[[pins]] +pin = 47 +ball = "U31" +pad = 29 +pad_name = "P0.29" + +[[pins]] +pin = 48 +ball = "V2" +pad = 4 +pad_name = "P0.04" + +[[pins]] +pin = 49 +ball = "W1" +pad = 2 +pad_name = "P0.02" + +[[pins]] +pin = 51 +ball = "Y2" +pad = 5 +pad_name = "P0.05" + +[[pins]] +pin = 52 +ball = "AA1" +pad = 3 +pad_name = "P0.03" + +[[pins]] +pin = 54 +ball = "AB2" +pad = 6 +pad_name = "P0.06" + +[[pins]] +pin = 57 +ball = "AD2" +pad = 7 +pad_name = "P0.07" + +[[pins]] +pin = 58 +ball = "AE1" +pad = 34 +pad_name = "P1.02" + +[[pins]] +pin = 59 +ball = "AE31" +pad = 28 +pad_name = "P0.28" + +[[pins]] +pin = 60 +ball = "AF2" +pad = 35 +pad_name = "P1.03" + +[[pins]] +pin = 63 +ball = "AH2" +pad = 8 +pad_name = "P0.08" + +[[pins]] +pin = 64 +ball = "AJ1" +pad = 9 +pad_name = "P0.09" + +[[pins]] +pin = 66 +ball = "AK2" +pad = 10 +pad_name = "P0.10" + +[[pins]] +pin = 67 +ball = "AK4" +pad = 11 +pad_name = "P0.11" + +[[pins]] +pin = 68 +ball = "AK6" +pad = 12 +pad_name = "P0.12" + +[[pins]] +pin = 69 +ball = "AK8" +pad = 14 +pad_name = "P0.14" + +[[pins]] +pin = 70 +ball = "AK10" +pad = 15 +pad_name = "P0.15" + +[[pins]] +pin = 71 +ball = "AK12" +pad = 17 +pad_name = "P0.17" + +[[pins]] +pin = 72 +ball = "AK14" +pad = 18 +pad_name = "P0.18" + +[[pins]] +pin = 73 +ball = "AK16" +pad = 20 +pad_name = "P0.20" + +[[pins]] +pin = 74 +ball = "AK18" +pad = 22 +pad_name = "P0.22" + +[[pins]] +pin = 75 +ball = "AK20" +pad = 23 +pad_name = "P0.23" + +[[pins]] +pin = 76 +ball = "AK22" +pad = 37 +pad_name = "P1.05" + +[[pins]] +pin = 77 +ball = "AK24" +pad = 39 +pad_name = "P1.07" + +[[pins]] +pin = 78 +ball = "AK26" +pad = 41 +pad_name = "P1.09" + +[[pins]] +pin = 79 +ball = "AK28" +pad = 25 +pad_name = "P0.25" + +[[pins]] +pin = 80 +ball = "AK30" +pad = 27 +pad_name = "P0.27" + +[[pins]] +pin = 82 +ball = "AL5" +pad = 13 +pad_name = "P0.13" + +[[pins]] +pin = 84 +ball = "AL9" +pad = 16 +pad_name = "P0.16" + +[[pins]] +pin = 86 +ball = "AL13" +pad = 19 +pad_name = "P0.19" + +[[pins]] +pin = 87 +ball = "AL15" +pad = 21 +pad_name = "P0.21" + +[[pins]] +pin = 89 +ball = "AL19" +pad = 36 +pad_name = "P1.04" + +[[pins]] +pin = 90 +ball = "AL21" +pad = 38 +pad_name = "P1.06" + +[[pins]] +pin = 91 +ball = "AL23" +pad = 40 +pad_name = "P1.08" + +[[pins]] +pin = 93 +ball = "AL27" +pad = 24 +pad_name = "P0.24" + +[[pins]] +pin = 94 +ball = "AL29" +pad = 26 +pad_name = "P0.26" + +# Package pins without a GPIO pad (not routable): +# A5, A13, A15, A19, A21, A23, A25, A27, B2, B4, B6, B8, B10, B12, B26, B28, B30, C1, C31, D2, E1, E31, F2, G1, G31, H2, J1, J31, K2, L1, L31, N31, T2, U1, W31, AA31, AC1, AC31, AG1, AG31, AJ31, AL3, AL7, AL11, AL17, AL25 diff --git a/ports/zephyr-cp/modules/iobroker/packages/nrf54l15_qfn48.toml b/ports/zephyr-cp/modules/iobroker/packages/nrf54l15_qfn48.toml new file mode 100644 index 00000000000..63cddcf27b0 --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/packages/nrf54l15_qfn48.toml @@ -0,0 +1,165 @@ +# Package pin map for nrf54l15_qfn48, transcribed from nRF54L15_nRF54L10_nRF54L05_Datasheet_v1.0.pdf, +# section 10.1.4 (QFN48, QFAA package pin assignments), +# by tools/gen_package.py. Review against the datasheet figure before +# editing by hand; regenerate instead when the datasheet changes. + +name = "nrf54l15_qfn48" +socs = ["SOC_NRF54L15", "SOC_NRF54L10", "SOC_NRF54L05"] + +[[pins]] +pin = 1 +pad = 32 +pad_name = "P1.00" + +[[pins]] +pin = 2 +pad = 33 +pad_name = "P1.01" + +[[pins]] +pin = 3 +pad = 34 +pad_name = "P1.02" + +[[pins]] +pin = 4 +pad = 35 +pad_name = "P1.03" + +[[pins]] +pin = 5 +pad = 36 +pad_name = "P1.04" + +[[pins]] +pin = 6 +pad = 37 +pad_name = "P1.05" + +[[pins]] +pin = 7 +pad = 38 +pad_name = "P1.06" + +[[pins]] +pin = 8 +pad = 39 +pad_name = "P1.07" + +[[pins]] +pin = 9 +pad = 40 +pad_name = "P1.08" + +[[pins]] +pin = 11 +pad = 64 +pad_name = "P2.00" + +[[pins]] +pin = 12 +pad = 65 +pad_name = "P2.01" + +[[pins]] +pin = 13 +pad = 66 +pad_name = "P2.02" + +[[pins]] +pin = 14 +pad = 67 +pad_name = "P2.03" + +[[pins]] +pin = 15 +pad = 68 +pad_name = "P2.04" + +[[pins]] +pin = 16 +pad = 69 +pad_name = "P2.05" + +[[pins]] +pin = 17 +pad = 70 +pad_name = "P2.06" + +[[pins]] +pin = 18 +pad = 71 +pad_name = "P2.07" + +[[pins]] +pin = 19 +pad = 72 +pad_name = "P2.08" + +[[pins]] +pin = 20 +pad = 73 +pad_name = "P2.09" + +[[pins]] +pin = 21 +pad = 74 +pad_name = "P2.10" + +[[pins]] +pin = 23 +pad = 0 +pad_name = "P0.00" + +[[pins]] +pin = 24 +pad = 1 +pad_name = "P0.01" + +[[pins]] +pin = 27 +pad = 2 +pad_name = "P0.02" + +[[pins]] +pin = 28 +pad = 3 +pad_name = "P0.03" + +[[pins]] +pin = 29 +pad = 4 +pad_name = "P0.04" + +[[pins]] +pin = 37 +pad = 41 +pad_name = "P1.09" + +[[pins]] +pin = 38 +pad = 42 +pad_name = "P1.10" + +[[pins]] +pin = 39 +pad = 43 +pad_name = "P1.11" + +[[pins]] +pin = 40 +pad = 44 +pad_name = "P1.12" + +[[pins]] +pin = 41 +pad = 45 +pad_name = "P1.13" + +[[pins]] +pin = 42 +pad = 46 +pad_name = "P1.14" + +# Package pins without a GPIO pad (not routable): +# 10, 22, 25, 26, 30, 31, 32, 33, 34, 35, 36, 43, 44, 45, 46, 47, 48 diff --git a/ports/zephyr-cp/modules/iobroker/packages/nrf54lm20_csp98.toml b/ports/zephyr-cp/modules/iobroker/packages/nrf54lm20_csp98.toml new file mode 100644 index 00000000000..526d6e09619 --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/packages/nrf54lm20_csp98.toml @@ -0,0 +1,409 @@ +# Package pin map for nrf54lm20_csp98, transcribed from nRF54LM20A_nRF54LM20B_Datasheet_v1.0.pdf, +# section 10.1.4 (CSP98, PAAA package pin assignments), +# by tools/gen_package.py. Review against the datasheet figure before +# editing by hand; regenerate instead when the datasheet changes. + +# Ball packages: 'pin' numbers the balls sequentially in row-major +# order (the datasheet only labels them, e.g. B2). + +name = "nrf54lm20_csp98" +socs = ["SOC_NRF54LM20A", "SOC_NRF54LM20B"] + +[[pins]] +pin = 12 +ball = "B2" +pad = 47 +pad_name = "P1.15" + +[[pins]] +pin = 13 +ball = "B3" +pad = 48 +pad_name = "P1.16" + +[[pins]] +pin = 14 +ball = "B4" +pad = 49 +pad_name = "P1.17" + +[[pins]] +pin = 15 +ball = "B5" +pad = 50 +pad_name = "P1.18" + +[[pins]] +pin = 16 +ball = "B6" +pad = 51 +pad_name = "P1.19" + +[[pins]] +pin = 17 +ball = "B7" +pad = 55 +pad_name = "P1.23" + +[[pins]] +pin = 18 +ball = "B8" +pad = 58 +pad_name = "P1.26" + +[[pins]] +pin = 19 +ball = "B9" +pad = 70 +pad_name = "P2.06" + +[[pins]] +pin = 20 +ball = "B10" +pad = 64 +pad_name = "P2.00" + +[[pins]] +pin = 22 +ball = "C2" +pad = 46 +pad_name = "P1.14" + +[[pins]] +pin = 23 +ball = "C3" +pad = 45 +pad_name = "P1.13" + +[[pins]] +pin = 24 +ball = "C4" +pad = 44 +pad_name = "P1.12" + +[[pins]] +pin = 25 +ball = "C5" +pad = 43 +pad_name = "P1.11" + +[[pins]] +pin = 26 +ball = "C6" +pad = 52 +pad_name = "P1.20" + +[[pins]] +pin = 27 +ball = "C7" +pad = 53 +pad_name = "P1.21" + +[[pins]] +pin = 28 +ball = "C8" +pad = 59 +pad_name = "P1.27" + +[[pins]] +pin = 29 +ball = "C9" +pad = 71 +pad_name = "P2.07" + +[[pins]] +pin = 30 +ball = "C10" +pad = 65 +pad_name = "P2.01" + +[[pins]] +pin = 33 +ball = "D4" +pad = 108 +pad_name = "P3.12" + +[[pins]] +pin = 34 +ball = "D5" +pad = 42 +pad_name = "P1.10" + +[[pins]] +pin = 35 +ball = "D6" +pad = 54 +pad_name = "P1.22" + +[[pins]] +pin = 36 +ball = "D7" +pad = 56 +pad_name = "P1.24" + +[[pins]] +pin = 37 +ball = "D8" +pad = 60 +pad_name = "P1.28" + +[[pins]] +pin = 38 +ball = "D9" +pad = 72 +pad_name = "P2.08" + +[[pins]] +pin = 39 +ball = "D10" +pad = 66 +pad_name = "P2.02" + +[[pins]] +pin = 41 +ball = "E3" +pad = 9 +pad_name = "P0.09" + +[[pins]] +pin = 42 +ball = "E4" +pad = 107 +pad_name = "P3.11" + +[[pins]] +pin = 45 +ball = "E7" +pad = 57 +pad_name = "P1.25" + +[[pins]] +pin = 46 +ball = "E8" +pad = 41 +pad_name = "P1.09" + +[[pins]] +pin = 47 +ball = "E9" +pad = 73 +pad_name = "P2.09" + +[[pins]] +pin = 48 +ball = "E10" +pad = 67 +pad_name = "P2.03" + +[[pins]] +pin = 51 +ball = "F3" +pad = 8 +pad_name = "P0.08" + +[[pins]] +pin = 52 +ball = "F4" +pad = 106 +pad_name = "P3.10" + +[[pins]] +pin = 55 +ball = "F7" +pad = 105 +pad_name = "P3.09" + +[[pins]] +pin = 56 +ball = "F8" +pad = 40 +pad_name = "P1.08" + +[[pins]] +pin = 57 +ball = "F9" +pad = 74 +pad_name = "P2.10" + +[[pins]] +pin = 58 +ball = "F10" +pad = 68 +pad_name = "P2.04" + +[[pins]] +pin = 61 +ball = "G3" +pad = 7 +pad_name = "P0.07" + +[[pins]] +pin = 62 +ball = "G4" +pad = 104 +pad_name = "P3.08" + +[[pins]] +pin = 63 +ball = "G5" +pad = 103 +pad_name = "P3.07" + +[[pins]] +pin = 64 +ball = "G6" +pad = 102 +pad_name = "P3.06" + +[[pins]] +pin = 65 +ball = "G7" +pad = 101 +pad_name = "P3.05" + +[[pins]] +pin = 66 +ball = "G8" +pad = 39 +pad_name = "P1.07" + +[[pins]] +pin = 67 +ball = "G9" +pad = 69 +pad_name = "P2.05" + +[[pins]] +pin = 70 +ball = "H2" +pad = 6 +pad_name = "P0.06" + +[[pins]] +pin = 71 +ball = "H3" +pad = 5 +pad_name = "P0.05" + +[[pins]] +pin = 72 +ball = "H4" +pad = 99 +pad_name = "P3.03" + +[[pins]] +pin = 73 +ball = "H5" +pad = 98 +pad_name = "P3.02" + +[[pins]] +pin = 74 +ball = "H6" +pad = 97 +pad_name = "P3.01" + +[[pins]] +pin = 75 +ball = "H7" +pad = 96 +pad_name = "P3.00" + +[[pins]] +pin = 76 +ball = "H8" +pad = 37 +pad_name = "P1.05" + +[[pins]] +pin = 77 +ball = "H9" +pad = 35 +pad_name = "P1.03" + +[[pins]] +pin = 78 +ball = "H10" +pad = 61 +pad_name = "P1.29" + +[[pins]] +pin = 79 +ball = "J1" +pad = 4 +pad_name = "P0.04" + +[[pins]] +pin = 80 +ball = "J2" +pad = 3 +pad_name = "P0.03" + +[[pins]] +pin = 81 +ball = "J3" +pad = 2 +pad_name = "P0.02" + +[[pins]] +pin = 82 +ball = "J4" +pad = 100 +pad_name = "P3.04" + +[[pins]] +pin = 84 +ball = "J6" +pad = 33 +pad_name = "P1.01" + +[[pins]] +pin = 85 +ball = "J7" +pad = 34 +pad_name = "P1.02" + +[[pins]] +pin = 86 +ball = "J8" +pad = 38 +pad_name = "P1.06" + +[[pins]] +pin = 87 +ball = "J9" +pad = 36 +pad_name = "P1.04" + +[[pins]] +pin = 88 +ball = "J10" +pad = 62 +pad_name = "P1.30" + +[[pins]] +pin = 90 +ball = "K2" +pad = 1 +pad_name = "P0.01" + +[[pins]] +pin = 91 +ball = "K3" +pad = 0 +pad_name = "P0.00" + +[[pins]] +pin = 96 +ball = "K8" +pad = 32 +pad_name = "P1.00" + +[[pins]] +pin = 97 +ball = "K9" +pad = 63 +pad_name = "P1.31" + +# Package pins without a GPIO pad (not routable): +# A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, B1, C1, D1, D2, E1, E5, E6, F1, F2, F5, F6, G1, G2, G10, H1, J5, K1, K4, K5, K6, K7, K10 diff --git a/ports/zephyr-cp/modules/iobroker/packages/rp2040_qfn56.toml b/ports/zephyr-cp/modules/iobroker/packages/rp2040_qfn56.toml new file mode 100644 index 00000000000..d90b5ac8ea7 --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/packages/rp2040_qfn56.toml @@ -0,0 +1,159 @@ +# Package pin map for rp2040_qfn56, transcribed from rp2040-datasheet.pdf, +# Table 615 (GPIO pins), with a one-off pdftotext -layout parse. Review +# against the datasheet figure before editing by hand. +# +# RP2040 has a single GPIO port; pads are numbered 0-29 and pad_name uses +# the datasheet's GPIOx naming. + +name = "rp2040_qfn56" +socs = ["SOC_RP2040"] + +[[pins]] +pin = 2 +pad = 0 +pad_name = "GPIO0" + +[[pins]] +pin = 3 +pad = 1 +pad_name = "GPIO1" + +[[pins]] +pin = 4 +pad = 2 +pad_name = "GPIO2" + +[[pins]] +pin = 5 +pad = 3 +pad_name = "GPIO3" + +[[pins]] +pin = 6 +pad = 4 +pad_name = "GPIO4" + +[[pins]] +pin = 7 +pad = 5 +pad_name = "GPIO5" + +[[pins]] +pin = 8 +pad = 6 +pad_name = "GPIO6" + +[[pins]] +pin = 9 +pad = 7 +pad_name = "GPIO7" + +[[pins]] +pin = 11 +pad = 8 +pad_name = "GPIO8" + +[[pins]] +pin = 12 +pad = 9 +pad_name = "GPIO9" + +[[pins]] +pin = 13 +pad = 10 +pad_name = "GPIO10" + +[[pins]] +pin = 14 +pad = 11 +pad_name = "GPIO11" + +[[pins]] +pin = 15 +pad = 12 +pad_name = "GPIO12" + +[[pins]] +pin = 16 +pad = 13 +pad_name = "GPIO13" + +[[pins]] +pin = 17 +pad = 14 +pad_name = "GPIO14" + +[[pins]] +pin = 18 +pad = 15 +pad_name = "GPIO15" + +[[pins]] +pin = 27 +pad = 16 +pad_name = "GPIO16" + +[[pins]] +pin = 28 +pad = 17 +pad_name = "GPIO17" + +[[pins]] +pin = 29 +pad = 18 +pad_name = "GPIO18" + +[[pins]] +pin = 30 +pad = 19 +pad_name = "GPIO19" + +[[pins]] +pin = 31 +pad = 20 +pad_name = "GPIO20" + +[[pins]] +pin = 32 +pad = 21 +pad_name = "GPIO21" + +[[pins]] +pin = 34 +pad = 22 +pad_name = "GPIO22" + +[[pins]] +pin = 35 +pad = 23 +pad_name = "GPIO23" + +[[pins]] +pin = 36 +pad = 24 +pad_name = "GPIO24" + +[[pins]] +pin = 37 +pad = 25 +pad_name = "GPIO25" + +[[pins]] +pin = 38 +pad = 26 +pad_name = "GPIO26" + +[[pins]] +pin = 39 +pad = 27 +pad_name = "GPIO27" + +[[pins]] +pin = 40 +pad = 28 +pad_name = "GPIO28" + +[[pins]] +pin = 41 +pad = 29 +pad_name = "GPIO29" diff --git a/ports/zephyr-cp/modules/iobroker/src/iobroker.c b/ports/zephyr-cp/modules/iobroker/src/iobroker.c new file mode 100644 index 00000000000..965a27682f6 --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/src/iobroker.c @@ -0,0 +1,250 @@ +// iobroker: dynamic peripheral allocation and runtime pin routing. +// +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +// SoC-agnostic core: package pin resolution, GPIO controller lookup and the +// claim registry. The bus allocate/release functions are SoC-specific and +// live in src///; the -ENOSYS stubs below stand in for SoCs +// with no implementation. + +#include +#include + +#include +#include + +#include "iobroker_internal.h" + +// One log instance for the whole module; iobroker_route.c declares it. +// Compiles away when CONFIG_LOG is off. +LOG_MODULE_REGISTER(iobroker, CONFIG_LOG_DEFAULT_LEVEL); + +int iobroker_gpio_split(uint16_t number, const struct device **port_out, + gpio_pin_t *pin_out) { + for (size_t i = 0; i < iobroker_gpio_port_count; i++) { + uint32_t base = (uint32_t)iobroker_gpio_port_indexes[i] * 32U; + if (number >= base && number < base + 32U) { + *port_out = iobroker_gpio_port_devices[i]; + *pin_out = (gpio_pin_t)(number - base); + return 0; + } + } + return -EINVAL; +} + +int iobroker_gpio_package_pin(uint8_t port, gpio_pin_t pin, + package_pin_t *package_pin_out) { + uint16_t soc_pad = (uint16_t)((uint32_t)port * 32U + pin); + for (size_t i = 0; i < iobroker_package_pin_count; i++) { + if (iobroker_package_pins[i].soc_pad == soc_pad) { + *package_pin_out = iobroker_package_pins[i].package_pin; + return 0; + } + } + return -EINVAL; +} + +int iobroker_package_pin_soc_pad(package_pin_t pin, uint16_t *soc_pad_out) { + if (pin == IOBROKER_NO_PIN) { + *soc_pad_out = IOBROKER_NO_PIN; + return 0; + } + for (size_t i = 0; i < iobroker_package_pin_count; i++) { + if (iobroker_package_pins[i].package_pin == pin) { + *soc_pad_out = iobroker_package_pins[i].soc_pad; + return 0; + } + } + return -EINVAL; +} + +#if !IOBROKER_ROUTING + +// SoCs without runtime routing: the bus allocate/release API still exists so +// busio can call it, but every allocate reports -ENOSYS. The implementations +// for routing SoCs live in src///. +int iobroker_i2c_allocate(package_pin_t sda, package_pin_t scl, + const struct device **dev_out) { + (void)sda; + (void)scl; + (void)dev_out; + return -ENOSYS; +} + +int iobroker_spi_allocate(package_pin_t clock, package_pin_t mosi, + package_pin_t miso, const struct device **dev_out) { + (void)clock; + (void)mosi; + (void)miso; + (void)dev_out; + return -ENOSYS; +} + +int iobroker_uart_allocate(package_pin_t tx, package_pin_t rx, + package_pin_t rts, package_pin_t cts, const struct device **dev_out) { + (void)tx; + (void)rx; + (void)rts; + (void)cts; + (void)dev_out; + return -ENOSYS; +} + +bool iobroker_release(const struct device *dev) { + (void)dev; + LOG_DBG("release: no routing support on this SoC, nothing to release"); + return false; +} + +#endif // !IOBROKER_ROUTING + +// Pins currently claimed for plain GPIO use. The module leaves the pad alone +// on allocate (the caller configures it) but returns it to a quiescent state +// on release. Claims store the GPIO controller device and pin number that the +// allocate call resolved and returned. +typedef struct { + const struct device *port; + gpio_pin_t number; + bool in_use; +} gpio_claim_t; + +static gpio_claim_t gpio_claims[CONFIG_IOBROKER_GPIO_MAX_PINS]; + +// Returns true when the package pin is claimed by a currently allocated +// instance or a GPIO allocation. Disconnected signals (IOBROKER_NO_PIN) +// and package pins with no entry in the map claim nothing. The bus loops +// below exist whenever the nRF pinctrl driver is built (the board emits the +// instance tables); without routing the tables are never marked in use. +bool iobroker_pin_in_use(package_pin_t pin) { + if (pin == IOBROKER_NO_PIN) { + return false; + } + #if defined(CONFIG_PINCTRL_NRF) + for (size_t i = 0; i < iobroker_i2c_bus_count; i++) { + if (!iobroker_i2c_bus_states[i].in_use) { + continue; + } + for (uint8_t j = 0; j < iobroker_i2c_bus_states[i].pin_count; j++) { + if (iobroker_i2c_bus_states[i].pins[j] == pin) { + return true; + } + } + } + for (size_t i = 0; i < iobroker_spi_bus_count; i++) { + if (!iobroker_spi_bus_states[i].in_use) { + continue; + } + for (uint8_t j = 0; j < iobroker_spi_bus_states[i].pin_count; j++) { + if (iobroker_spi_bus_states[i].pins[j] == pin) { + return true; + } + } + } + for (size_t i = 0; i < iobroker_uart_bus_count; i++) { + if (!iobroker_uart_bus_states[i].in_use) { + continue; + } + for (uint8_t j = 0; j < iobroker_uart_bus_states[i].pin_count; j++) { + if (iobroker_uart_bus_states[i].pins[j] == pin) { + return true; + } + } + } + #endif // CONFIG_PINCTRL_NRF + uint16_t soc_pad; + if (iobroker_package_pin_soc_pad(pin, &soc_pad) < 0) { + return false; + } + #if defined(CONFIG_PINCTRL_NRF) + // Pads owned by fixed peripherals (console UART, flash instance, ...) are + // always in use: their pinctrl state drives them from boot. + for (size_t i = 0; i < iobroker_reserved_pads_count; i++) { + if (iobroker_reserved_pads[i] == soc_pad) { + return true; + } + } + #endif + const struct device *port; + gpio_pin_t number; + if (iobroker_gpio_split(soc_pad, &port, &number) < 0) { + return false; + } + for (size_t i = 0; i < ARRAY_SIZE(gpio_claims); i++) { + if (gpio_claims[i].in_use && gpio_claims[i].port == port && + gpio_claims[i].number == number) { + return true; + } + } + return false; +} + +// Claim a package pin for GPIO use. The caller configures the pad itself +// while the claim is held; release returns it to a quiescent state. The pin +// is resolved through the package pin map, and the GPIO controller device and +// pin number within it are returned. +int iobroker_gpio_allocate(package_pin_t pin, + const struct device **port_out, gpio_pin_t *pin_out) { + if (pin == IOBROKER_NO_PIN) { + LOG_WRN("gpio allocate: pin is disconnected"); + return -EINVAL; + } + if (iobroker_pin_in_use(pin)) { + LOG_WRN("gpio allocate: package pin %u already claimed", (unsigned)pin); + return -EBUSY; + } + uint16_t soc_pad; + int ret = iobroker_package_pin_soc_pad(pin, &soc_pad); + if (ret < 0) { + LOG_WRN("gpio allocate: package pin %u not in package map", (unsigned)pin); + return ret; + } + ret = iobroker_gpio_split(soc_pad, port_out, pin_out); + if (ret < 0) { + LOG_WRN("gpio allocate: no GPIO controller for pad %u", (unsigned)soc_pad); + return ret; + } + for (size_t i = 0; i < ARRAY_SIZE(gpio_claims); i++) { + if (gpio_claims[i].in_use) { + continue; + } + gpio_claims[i].port = *port_out; + gpio_claims[i].number = *pin_out; + gpio_claims[i].in_use = true; + LOG_DBG("gpio allocate: package pin %u -> %s pin %u", + (unsigned)pin, (*port_out)->name, (unsigned)*pin_out); + return 0; + } + LOG_WRN("gpio allocate: claim registry full (%u pins)", + (unsigned)CONFIG_IOBROKER_GPIO_MAX_PINS); + return -ENOMEM; +} + +// Return a pad to a quiescent state: disconnected from any peripheral +// routing, input buffer and driver off, no pulls. +static void gpio_deconfigure(const struct device *port, gpio_pin_t number) { + if (gpio_pin_configure(port, number, GPIO_DISCONNECTED) == -ENOTSUP) { + // SoCs without GPIO_DISCONNECTED support settle for a plain input. + gpio_pin_configure(port, number, GPIO_INPUT); + } +} + +// Release a GPIO claim and return the pad to a quiescent state. Pass the +// device and pin number that the allocate call returned. Returns true when a +// claim was held. +bool iobroker_gpio_release(const struct device *port, gpio_pin_t number) { + for (size_t i = 0; i < ARRAY_SIZE(gpio_claims); i++) { + if (gpio_claims[i].in_use && gpio_claims[i].port == port && + gpio_claims[i].number == number) { + gpio_claims[i].in_use = false; + gpio_deconfigure(port, number); + LOG_DBG("gpio release: %s pin %u back to quiescent state", + port->name, (unsigned)number); + return true; + } + } + return false; +} diff --git a/ports/zephyr-cp/modules/iobroker/src/iobroker_internal.h b/ports/zephyr-cp/modules/iobroker/src/iobroker_internal.h new file mode 100644 index 00000000000..fa8e1afe6a3 --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/src/iobroker_internal.h @@ -0,0 +1,19 @@ +// iobroker internal helpers shared between the SoC-agnostic core and the +// vendor/SoC implementations in src///. Not part of the public +// API; see include/iobroker/iobroker.h for that. +// +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +// Resolve a package pin to the SoC pad it is bonded to (defined by the core). +// IOBROKER_NO_PIN passes through unchanged so that disconnected optional +// signals stay disconnected. Returns 0, or -EINVAL when the pin is not in +// the map. +int iobroker_package_pin_soc_pad(package_pin_t pin, uint16_t *soc_pad_out); diff --git a/ports/zephyr-cp/modules/iobroker/src/nordic/nrf/iobroker_route.c b/ports/zephyr-cp/modules/iobroker/src/nordic/nrf/iobroker_route.c new file mode 100644 index 00000000000..330b9aae1ea --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/src/nordic/nrf/iobroker_route.c @@ -0,0 +1,459 @@ +// iobroker: nRF runtime pin routing. +// +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +// / = nordic/nrf: one implementation covers every nRF SoC, +// because their pin control encoding is a simple bit-packed value (see +// zephyr/dt-bindings/pinctrl/nrf-pinctrl.h and soc/nordic/common/pinctrl_soc.h) +// that can be computed at runtime, and any peripheral function can be routed +// to (almost) any pin via PSEL. +// +// Compiled whenever CONFIG_PINCTRL_NRF is on. The allocate/release +// definitions here only exist with the full feature set +// (CONFIG_PINCTRL_DYNAMIC and CONFIG_DEVICE_DEINIT_SUPPORT); otherwise the +// stubs in the core report -ENOSYS. + +#include +#include +#include + +#include +#include +#include +#include + +#include "iobroker_internal.h" + +LOG_MODULE_DECLARE(iobroker, CONFIG_LOG_DEFAULT_LEVEL); + +#if IOBROKER_ROUTING + +// Bit positions/fields replicated from nrf-pinctrl.h so that entries can be +// encoded at runtime instead of by the DT macros. +#define NRF_PSEL_FUN(fun) (((uint32_t)(fun) & NRF_FUN_MSK) << NRF_FUN_POS) +#define NRF_PSEL_PIN(pin) (((uint32_t)(pin) & NRF_PIN_MSK) << NRF_PIN_POS) +#define NRF_PSEL_DISCONNECT(fun) \ + ((uint32_t)NRF_PIN_DISCONNECTED | NRF_PSEL_FUN(fun)) +#define NRF_PSEL_PULL_UP ((uint32_t)NRF_PULL_UP << NRF_PULL_POS) +#define NRF_PSEL_LP ((uint32_t)NRF_LP_ENABLE << NRF_LP_POS) +#define NRF_PSEL_CLOCKPIN BIT(NRF_CLOCKPIN_ENABLE_POS) +// Pin + function bits, used to compare a request against a devicetree state. +#define NRF_PSEL_PINFUN_MASK \ + (((uint32_t)NRF_PIN_MSK << NRF_PIN_POS) | ((uint32_t)NRF_FUN_MSK << NRF_FUN_POS)) + +// Log decode helpers: turn a fun code and a SoC pad into readable names, so +// that the logs say which peripheral function goes to which GPIO. +static const char *nrf_fun_name(uint32_t fun) { + static const char *const names[] = { + "UART_TX", "UART_RX", "UART_RTS", "UART_CTS", + "SPIM_SCK", "SPIM_MOSI", "SPIM_MISO", + "SPIS_SCK", "SPIS_MOSI", "SPIS_MISO", "SPIS_CSN", + "TWIM_SCL", "TWIM_SDA", + }; + if (fun < ARRAY_SIZE(names)) { + return names[fun]; + } + return "(other)"; +} + +// Render a SoC pad as a GPIO name ("P0.09"), or "disconnected" for +// IOBROKER_NO_PIN. Writes into buf; returns buf for chaining. +static const char *nrf_pad_name(uint32_t soc_pad, char *buf, size_t size) { + if (soc_pad == IOBROKER_NO_PIN) { + return "disconnected"; + } + snprintf(buf, size, "P%u.%02u", (unsigned)(soc_pad / 32U), + (unsigned)(soc_pad % 32U)); + return buf; +} + +// Decode one pinctrl entry into flags text, e.g. "pull-up clockpin". buf +// receives an empty string when neither flag is set. +static const char *nrf_pin_flags(uint32_t psel, char *buf, size_t size) { + buf[0] = '\0'; + size_t used = 0; + uint32_t pull = NRF_GET_PULL(psel); + if (pull == NRF_PULL_UP) { + used += (size_t)snprintf(buf + used, size - used, "pull-up"); + } else if (pull == NRF_PULL_DOWN) { + used += (size_t)snprintf(buf + used, size - used, "pull-down"); + } + if (NRF_GET_CLOCKPIN_ENABLE(psel)) { + if (used > 0) { + used += (size_t)snprintf(buf + used, size - used, " "); + } + snprintf(buf + used, size - used, "clockpin"); + } + return buf; +} + +// Check that a SoC pad can be encoded (it sits on a known GPIO controller, +// so its number is a valid nRF pin). +static bool nrf_pad_ok(uint16_t soc_pad) { + if (soc_pad == IOBROKER_NO_PIN) { + return true; + } + const struct device *port; + gpio_pin_t number; + return iobroker_gpio_split(soc_pad, &port, &number) == 0; +} + +// Encode one nRF pin control entry. soc_pad may be IOBROKER_NO_PIN to +// leave the signal disconnected. pull_up enables the internal pull resistor; +// the caller picks it per bus signal (I2C SDA/SCL and UART RX idle high). +static pinctrl_soc_pin_t nrf_psel_encode(uint32_t fun, uint16_t soc_pad, + bool pull_up) { + uint32_t psel; + + if (soc_pad != IOBROKER_NO_PIN) { + psel = NRF_PSEL_PIN(soc_pad) | NRF_PSEL_FUN(fun); + // On nRF54 the GPIO pin clock must be enabled for signals that drive + // the pad. The pinctrl driver ignores this bit where unsupported + // (nRF52/nRF53). + switch (fun) { + case NRF_FUN_TWIM_SDA: + case NRF_FUN_TWIM_SCL: + case NRF_FUN_SPIM_SCK: + case NRF_FUN_SPIM_MOSI: + case NRF_FUN_UART_TX: + psel |= NRF_PSEL_CLOCKPIN; + break; + default: + break; + } + } else { + psel = NRF_PSEL_DISCONNECT(fun); + } + + if (pull_up) { + psel |= NRF_PSEL_PULL_UP; + } + + return psel; +} + +// Check whether a set of requested entries matches the devicetree default +// state of an instance (pin + function only; configuration bits ignored). +static bool nrf_psels_match(const pinctrl_soc_pin_t *psels, uint8_t count, + const pinctrl_soc_pin_t *requested, uint8_t requested_count) { + uint32_t mask = NRF_PSEL_PINFUN_MASK; + + for (uint8_t i = 0; i < requested_count; i++) { + bool found = false; + for (uint8_t j = 0; j < count; j++) { + if ((psels[j] & mask) == (requested[i] & mask)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + // The DT state must not use extra pins either. + for (uint8_t j = 0; j < count; j++) { + bool found = false; + for (uint8_t i = 0; i < requested_count; i++) { + if ((psels[j] & mask) == (requested[i] & mask)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; +} + +// Re-point an instance at the requested pins: ensure the device is +// de-initialized (initialization is the caller's job), then swap its pinctrl +// states for entries built at runtime. The caller initializes the device +// afterwards, which applies the new "default" state. +static int iobroker_route(const iobroker_instance_t *inst, iobroker_state_t *state, + const pinctrl_soc_pin_t *pins, uint8_t pin_count) { + if (pin_count > IOBROKER_MAX_PINS) { + return -EINVAL; + } + + LOG_INF("routing %s: de-initializing then swapping in %u runtime psels", + inst->dev->name, (unsigned)pin_count); + int ret = device_deinit(inst->dev); + if (ret < 0 && ret != -ENOSYS && ret != -EPERM) { + LOG_WRN("routing %s: device_deinit failed: %d", inst->dev->name, ret); + return ret; + } + + // The Zephyr pinctrl core requires the same set of state ids to be + // provided, so mirror the ids the device currently has. + uint8_t state_cnt = MIN(inst->pcfg->state_cnt, ARRAY_SIZE(state->states)); + if (state_cnt == 0) { + return -ENOENT; + } + + for (uint8_t i = 0; i < pin_count; i++) { + state->default_pins[i] = pins[i]; + // Sleep state: same pins, low power (input buffer disconnected). + state->sleep_pins[i] = pins[i] | NRF_PSEL_LP; + } + for (uint8_t s = 0; s < state_cnt; s++) { + uint8_t id = inst->pcfg->states[s].id; + state->states[s].id = id; + if (id == PINCTRL_STATE_SLEEP) { + state->states[s].pins = state->sleep_pins; + } else { + state->states[s].pins = state->default_pins; + } + state->states[s].pin_cnt = pin_count; + } + + ret = pinctrl_update_states(inst->pcfg, state->states, state_cnt); + if (ret < 0) { + LOG_WRN("routing %s: pinctrl_update_states failed: %d", + inst->dev->name, ret); + } else { + for (uint8_t i = 0; i < pin_count; i++) { + uint32_t psel = pins[i]; + char pad[12]; + char flags[24]; + LOG_INF("routing %s: psel[%u] = 0x%08x -> %s %s %s", + inst->dev->name, (unsigned)i, (unsigned)psel, + nrf_fun_name(NRF_GET_FUN(psel)), + nrf_pad_name(NRF_GET_PIN(psel), pad, sizeof(pad)), + nrf_pin_flags(psel, flags, sizeof(flags))); + } + } + return ret; +} + +// Allocate an instance from a pool. Instances that the board enabled with +// disconnected pins are routed dynamically to the requested pins. Instances +// with fixed devicetree pins are only used when the request matches their +// existing state exactly. The requested pins are recorded in the instance's +// state so that iobroker_pin_in_use() can report them. +static int iobroker_allocate(const char *kind, const iobroker_instance_t *buses, + size_t count, iobroker_state_t *states, const package_pin_t *requested, + const pinctrl_soc_pin_t *pins, uint8_t pin_count, const struct device **dev_out) { + for (size_t i = 0; i < count; i++) { + iobroker_state_t *state = &states[i]; + if (state->in_use) { + continue; + } + + if (buses[i].dt_psels == NULL) { + int ret = iobroker_route(&buses[i], state, pins, pin_count); + if (ret < 0) { + continue; + } + state->in_use = true; + state->routed = true; + state->pin_count = pin_count; + for (uint8_t j = 0; j < pin_count; j++) { + state->pins[j] = requested[j]; + } + *dev_out = buses[i].dev; + LOG_INF("%s: allocated %s, routed to %u package pins", + kind, buses[i].dev->name, (unsigned)pin_count); + return 0; + } + + if (nrf_psels_match(buses[i].dt_psels, buses[i].dt_psel_count, pins, pin_count)) { + // Already routed to these pins by the devicetree; just claim it. + state->in_use = true; + state->routed = false; + state->pin_count = pin_count; + for (uint8_t j = 0; j < pin_count; j++) { + state->pins[j] = requested[j]; + } + *dev_out = buses[i].dev; + LOG_INF("%s: allocated %s, devicetree pins match request", + kind, buses[i].dev->name); + return 0; + } + } + LOG_WRN("%s: no free instance for the requested pins", kind); + return -ENODEV; +} + +// Returns -EBUSY when any requested pin is already claimed by an allocated +// instance, so that a pin is only ever used by one allocate() call at a time. +static int iobroker_check_request(const char *kind, const package_pin_t *pins, + size_t count) { + for (size_t i = 0; i < count; i++) { + if (iobroker_pin_in_use(pins[i])) { + LOG_WRN("%s: package pin %u already in use", kind, + (unsigned)pins[i]); + return -EBUSY; + } + } + return 0; +} + +static bool iobroker_state_find(const struct device *dev, iobroker_state_t **state_out) { + for (size_t i = 0; i < iobroker_i2c_bus_count; i++) { + if (iobroker_i2c_buses[i].dev == dev) { + *state_out = &iobroker_i2c_bus_states[i]; + return true; + } + } + for (size_t i = 0; i < iobroker_spi_bus_count; i++) { + if (iobroker_spi_buses[i].dev == dev) { + *state_out = &iobroker_spi_bus_states[i]; + return true; + } + } + for (size_t i = 0; i < iobroker_uart_bus_count; i++) { + if (iobroker_uart_buses[i].dev == dev) { + *state_out = &iobroker_uart_bus_states[i]; + return true; + } + } + return false; +} + +bool iobroker_release(const struct device *dev) { + iobroker_state_t *state = NULL; + if (!iobroker_state_find(dev, &state)) { + LOG_WRN("release: %s is not an iobroker-managed instance", + dev == NULL ? "(null)" : dev->name); + return false; + } + bool routed = state->routed; + // De-init so that the device ends up de-initialized (like deferred-init + // devices are after boot); the caller initializes it again when it + // allocates the instance next. + (void)device_deinit(dev); + state->in_use = false; + state->routed = false; + state->pin_count = 0; + LOG_INF("released %s (was dynamically routed: %u)", dev->name, + (unsigned)routed); + return routed; +} + +int iobroker_i2c_allocate(package_pin_t sda, package_pin_t scl, + const struct device **dev_out) { + LOG_INF("i2c allocate: sda=%u scl=%u", (unsigned)sda, (unsigned)scl); + const package_pin_t requested[] = { sda, scl }; + int ret = iobroker_check_request("i2c", requested, 2); + if (ret < 0) { + return ret; + } + uint16_t sda_pad; + uint16_t scl_pad; + if (iobroker_package_pin_soc_pad(sda, &sda_pad) < 0 || + iobroker_package_pin_soc_pad(scl, &scl_pad) < 0) { + LOG_WRN("i2c allocate: package pin %u or %u not in package map", + (unsigned)sda, (unsigned)scl); + return -EINVAL; + } + if (!nrf_pad_ok(sda_pad) || !nrf_pad_ok(scl_pad)) { + LOG_WRN("i2c allocate: pad %u or %u is not on a GPIO controller", + (unsigned)sda_pad, (unsigned)scl_pad); + return -EINVAL; + } + char sda_name[12]; + char scl_name[12]; + LOG_INF("i2c allocate: SDA package pin %u -> %s, SCL package pin %u -> %s", + (unsigned)sda, nrf_pad_name(sda_pad, sda_name, sizeof(sda_name)), + (unsigned)scl, nrf_pad_name(scl_pad, scl_name, sizeof(scl_name))); + pinctrl_soc_pin_t pins[2]; + // Open-drain bus: both lines idle high via the internal pull-up. + pins[0] = nrf_psel_encode(NRF_FUN_TWIM_SDA, sda_pad, true); + pins[1] = nrf_psel_encode(NRF_FUN_TWIM_SCL, scl_pad, true); + return iobroker_allocate("i2c", iobroker_i2c_buses, iobroker_i2c_bus_count, + iobroker_i2c_bus_states, requested, pins, 2, dev_out); +} + +int iobroker_spi_allocate(package_pin_t clock, package_pin_t mosi, + package_pin_t miso, const struct device **dev_out) { + LOG_INF("spi allocate: clock=%u mosi=%u miso=%u", (unsigned)clock, + (unsigned)mosi, (unsigned)miso); + const package_pin_t requested[] = { clock, mosi, miso }; + int ret = iobroker_check_request("spi", requested, 3); + if (ret < 0) { + return ret; + } + uint16_t clock_pad; + uint16_t mosi_pad; + uint16_t miso_pad; + if (iobroker_package_pin_soc_pad(clock, &clock_pad) < 0 || + iobroker_package_pin_soc_pad(mosi, &mosi_pad) < 0 || + iobroker_package_pin_soc_pad(miso, &miso_pad) < 0) { + LOG_WRN("spi allocate: a package pin (%u/%u/%u) is not in the map", + (unsigned)clock, (unsigned)mosi, (unsigned)miso); + return -EINVAL; + } + if (!nrf_pad_ok(clock_pad) || !nrf_pad_ok(mosi_pad) || !nrf_pad_ok(miso_pad)) { + LOG_WRN("spi allocate: pad %u/%u/%u is not on a GPIO controller", + (unsigned)clock_pad, (unsigned)mosi_pad, (unsigned)miso_pad); + return -EINVAL; + } + char clock_name[12]; + char mosi_name[12]; + char miso_name[12]; + LOG_INF("spi allocate: SCK package pin %u -> %s, MOSI %u -> %s, MISO %u -> %s", + (unsigned)clock, nrf_pad_name(clock_pad, clock_name, sizeof(clock_name)), + (unsigned)mosi, nrf_pad_name(mosi_pad, mosi_name, sizeof(mosi_name)), + (unsigned)miso, nrf_pad_name(miso_pad, miso_name, sizeof(miso_name))); + pinctrl_soc_pin_t pins[3]; + // All signals are push-pull outputs (MISO from the peripheral's view). + pins[0] = nrf_psel_encode(NRF_FUN_SPIM_SCK, clock_pad, false); + pins[1] = nrf_psel_encode(NRF_FUN_SPIM_MOSI, mosi_pad, false); + pins[2] = nrf_psel_encode(NRF_FUN_SPIM_MISO, miso_pad, false); + return iobroker_allocate("spi", iobroker_spi_buses, iobroker_spi_bus_count, + iobroker_spi_bus_states, requested, pins, 3, dev_out); +} + +int iobroker_uart_allocate(package_pin_t tx, package_pin_t rx, + package_pin_t rts, package_pin_t cts, const struct device **dev_out) { + LOG_INF("uart allocate: tx=%u rx=%u rts=%u cts=%u", (unsigned)tx, + (unsigned)rx, (unsigned)rts, (unsigned)cts); + const package_pin_t requested[] = { tx, rx, rts, cts }; + int ret = iobroker_check_request("uart", requested, 4); + if (ret < 0) { + return ret; + } + uint16_t tx_pad; + uint16_t rx_pad; + uint16_t rts_pad; + uint16_t cts_pad; + if (iobroker_package_pin_soc_pad(tx, &tx_pad) < 0 || + iobroker_package_pin_soc_pad(rx, &rx_pad) < 0 || + iobroker_package_pin_soc_pad(rts, &rts_pad) < 0 || + iobroker_package_pin_soc_pad(cts, &cts_pad) < 0) { + LOG_WRN("uart allocate: a package pin (%u/%u/%u/%u) is not in the map", + (unsigned)tx, (unsigned)rx, (unsigned)rts, (unsigned)cts); + return -EINVAL; + } + if (!nrf_pad_ok(tx_pad) || !nrf_pad_ok(rx_pad) || + !nrf_pad_ok(rts_pad) || !nrf_pad_ok(cts_pad)) { + LOG_WRN("uart allocate: pad %u/%u/%u/%u is not on a GPIO controller", + (unsigned)tx_pad, (unsigned)rx_pad, (unsigned)rts_pad, + (unsigned)cts_pad); + return -EINVAL; + } + char tx_name[12]; + char rx_name[12]; + char rts_name[12]; + char cts_name[12]; + LOG_INF("uart allocate: TX package pin %u -> %s, RX %u -> %s, RTS %u -> %s, CTS %u -> %s", + (unsigned)tx, nrf_pad_name(tx_pad, tx_name, sizeof(tx_name)), + (unsigned)rx, nrf_pad_name(rx_pad, rx_name, sizeof(rx_name)), + (unsigned)rts, nrf_pad_name(rts_pad, rts_name, sizeof(rts_name)), + (unsigned)cts, nrf_pad_name(cts_pad, cts_name, sizeof(cts_name))); + pinctrl_soc_pin_t pins[4]; + // RX floats until the peer drives it, so pull it up internally. + pins[0] = nrf_psel_encode(NRF_FUN_UART_TX, tx_pad, false); + pins[1] = nrf_psel_encode(NRF_FUN_UART_RX, rx_pad, true); + pins[2] = nrf_psel_encode(NRF_FUN_UART_RTS, rts_pad, false); + pins[3] = nrf_psel_encode(NRF_FUN_UART_CTS, cts_pad, false); + return iobroker_allocate("uart", iobroker_uart_buses, iobroker_uart_bus_count, + iobroker_uart_bus_states, requested, pins, 4, dev_out); +} + +#endif // IOBROKER_ROUTING diff --git a/ports/zephyr-cp/modules/iobroker/tools/gen_package.py b/ports/zephyr-cp/modules/iobroker/tools/gen_package.py new file mode 100644 index 00000000000..0240cc3601c --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/tools/gen_package.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +"""Transcribe a package pin assignment table into a iobroker package TOML. + +Input is a `pdftotext -layout` dump of a Nordic SoC datasheet (the module's +datasheets/ directory keeps the PDFs; run pdftotext -layout yourself). The +script locates one "X.Y.Z () package pin assignments" section, +parses its pin table and cross-checks the GPIO pads it finds against the pads +named in the section's pin assignment figure. + +The pin tables are laid out with the pin number vertically centered in its +row, so a pin's GPIO pad name can appear on the line before its number. The +parser therefore assigns every GPIO pad (P.) to the next pin +number at or after it in reading order; sequential pin numbering makes that +unambiguous. Every mapped pin is printed for manual review -- the datasheet +figure remains the source of truth, so diff the output against it. + +Usage (nRF54 datasheets, " () package pin assignments" with +numeric QFN pin numbers or a ball grid): + + gen_package.py DATASHEET.txt --section 10.1.4 --name nrf54l15_qfn48 \ + --socs SOC_NRF54L15,SOC_NRF54L10,SOC_NRF54L05 --pins 48 \ + --source nRF54L15_nRF54L10_nRF54L05_Datasheet_v1.0.pdf \ + --out ../packages/nrf54l15_qfn48.toml + +Ball grids label their pins "A5"-style instead of numbering them; pass the +grid's row letters with --rows (comma-separated when the grid has two-letter +rows, as the aQFN grids do: --rows A,B,...,Y,AA,...,AL) and --cols. + +nRF52/nRF53 product specifications name their sections " pin/ball +assignments" without a package code and lay the table out the same way (the +"Pin" column holds the ball label, there is no Clock column, and the aQFN +tables follow the main table with a "Corner pads" subsection); the same +options transcribe them: + + gen_package.py DATASHEET.txt --section 9.1.1 --name nrf5340_qkaa \ + --socs SOC_NRF5340_CPUAPP --pins 94 \ + --rows A,B,C,D,E,F,G,H,J,K,L,M,N,P,R,T,U,V,W,Y,AA,AB,AC,AD,AE,AF,AG,AH,AJ,AK,AL \ + --cols 31 --source nRF5340_PS_v1.6.pdf --out ../packages/nrf5340_qkaa.toml +""" + +import argparse +import re +import sys +from pathlib import Path + +PAD_RE = re.compile(r"P(\d+)\.(\d+)") +# Section headings: "10.1.4 QFN48 (QFAA) package pin assignments" (nRF54 +# datasheets; title plus package code) or "9.1.1 aQFN94 pin assignments" / +# "7.1.1 aQFN73 ball assignments" (nRF52840/nRF5340 product specifications; no +# package code). The heading must end in the assignments phrase, which keeps +# table-of-contents lines (trailing dot leaders and page numbers) out. +SECTION_RE = re.compile( + r"^\s*(\d+\.\d+\.\d+)\s+(.+?)\s+((?:package\s+)?(?:pin|ball)\s+assignments)$" +) +# Optional "()" at the end of a section title, as in the nRF54 headings. +TITLE_CODE_RE = re.compile(r"^(.*?)\s*\((\S+)\)$") +FIGURE_END_RE = re.compile(r"^\s*Figure \d+:") +# A pin number line: optionally preceded by the "Yes" clock-pin marker, either +# alone or with the row's first name/function entry on the same line. +PIN_LINE_RE = re.compile(r"^\s*(?:Yes\s+)?(\d+)(?:\s+(\S.*))?$") +# A ball name line (aQFN/CSP/BGA packages): row letter(s) + column at column 0. +BALL_LINE_RE = re.compile(r"^([A-Z]+)(\d+)(?:\s+|$)") +TABLE_HEADER_RE = re.compile(r"^\s*Pin\s+(?:Clock\s+)?Name\s+Function\s+Description") +# The pin table ends at its "Table N:" caption, or -- for aQFN packages, +# which list their corner pads right after the main table -- at the +# "Corner pads" heading. Neither belongs to the table. +TABLE_END_RE = re.compile(r"^\s*(?:Table\s+\d+:|Corner pads\b)") + + +def find_section(lines, section): + """Return (title, code, kind, section lines) for a numbered heading. + + code is the package code in the nRF54 "QFN48 (QFAA)" headings, or None + when the heading carries none ("aQFN94 pin assignments"). kind is the + heading's "package pin assignments"/"pin assignments"/"ball assignments" + phrase, used in the output header comment. + """ + starts = [(i, SECTION_RE.match(line)) for i, line in enumerate(lines)] + starts = [(i, m) for i, m in starts if m] + for index, (i, m) in enumerate(starts): + if m.group(1) == section: + end = starts[index + 1][0] if index + 1 < len(starts) else len(lines) + title, code = m.group(2), None + code_match = TITLE_CODE_RE.match(title) + if code_match: + title, code = code_match.group(1), code_match.group(2) + return title, code, m.group(3), lines[i + 1 : end] + raise SystemExit(f"section {section!r} not found") + + +def parse_table(section_lines, rows=None, cols=10): + """Return ({pin_id: [(port, pin), ...]}, ordered pin ids) from the table. + + Numeric packages (QFN) use 1-based pin numbers validated against + sequential numbering. Ball packages pass the grid's row letters (a list, + since aQFN grids have two-letter rows); pins are (row, column) pairs, + ordered row-major. + """ + header = next((i for i, line in enumerate(section_lines) if TABLE_HEADER_RE.match(line)), None) + if header is None: + raise SystemExit("pin table header ('Pin Clock Name Function ...') not found") + body = section_lines[header + 1 :] + # The table ends at its caption line or at the "Corner pads" subsection + # some packages list after it; neither is part of the table. + end = next((i for i, line in enumerate(body) if TABLE_END_RE.match(line)), None) + if end is not None: + body = body[:end] + + # Pass 1: pin lines. Numeric mode validates against sequential numbering + # so page numbers and stray digits cannot be mistaken for pins; ball mode + # validates against the grid instead. + number_lines = [] + if rows: + valid = {(row, column) for row in rows for column in range(1, cols + 1)} + for i, line in enumerate(body): + m = BALL_LINE_RE.match(line) + if m and (m.group(1), int(m.group(2))) in valid: + ball = (m.group(1), int(m.group(2))) + if ball in (b for _, b in number_lines): + raise SystemExit(f"duplicate ball {ball[0]}{ball[1]}") + number_lines.append((i, ball)) + else: + for i, line in enumerate(body): + m = PIN_LINE_RE.match(line) + if m and int(m.group(1)) == len(number_lines) + 1: + number_lines.append((i, int(m.group(1)))) + if not number_lines: + raise SystemExit("no pin numbers found; table layout mismatch?") + + # Pass 2: assign each GPIO pad to the next pin at or after it. The pin + # number is vertically centered in its table row, so the row's GPIO pad + # name can sit on the line before the number. + pins = {pin: [] for _, pin in number_lines} + for i, line in enumerate(body): + for m in PAD_RE.finditer(line): + owner = next((pin for j, pin in number_lines if j >= i), None) + if owner is None: + raise SystemExit(f"GPIO pad {m.group(0)!r} after the last pin") + pins[owner].append((int(m.group(1)), int(m.group(2)))) + order = [pin for _, pin in number_lines] + return pins, order + + +def parse_figure(section_lines): + """GPIO pads named anywhere in the section's pin assignment figure.""" + pads = set() + for line in section_lines: + if FIGURE_END_RE.match(line): + break + pads.update(PAD_RE.finditer(line)) + return {(int(m.group(1)), int(m.group(2))) for m in pads} + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument("datasheet", type=Path, help="pdftotext -layout dump") + parser.add_argument("--section", required=True, help="e.g. 10.1.4") + parser.add_argument("--name", required=True, help="package map name, e.g. nrf54l15_qfn48") + parser.add_argument( + "--socs", required=True, help="comma-separated Kconfig SoC symbols the map applies to" + ) + parser.add_argument( + "--pins", type=int, required=True, help="expected number of package pins (or balls)" + ) + parser.add_argument( + "--rows", + help="ball row letters, e.g. ABCDEFGHJK (comma-separated A,B,...,AA " + "when the grid has two-letter rows), for ball grid packages; " + "pins are numbered sequentially in row-major order", + ) + parser.add_argument("--cols", type=int, default=10, help="ball grid columns (with --rows)") + parser.add_argument( + "--die-pad", + action="store_true", + help="the table lists the exposed die pad as one pin past --pins; " + "it is bonded to VSS and never routable", + ) + parser.add_argument( + "--source", required=True, help="datasheet file name for the header comment" + ) + parser.add_argument("--out", type=Path, required=True, help="output TOML path") + args = parser.parse_args() + + # Ball row letters: comma-separated when the grid has two-letter rows. + rows = None + if args.rows: + rows = args.rows.split(",") if "," in args.rows else list(args.rows) + + lines = args.datasheet.read_text().splitlines() + title, code, kind, section_lines = find_section(lines, args.section) + + pins, order = parse_table(section_lines, rows=rows, cols=args.cols) + expected_max = args.pins + (1 if args.die_pad else 0) + if len(order) != expected_max: + raise SystemExit(f"parsed {len(order)} pins, expected {expected_max}") + if args.die_pad: + die_pad = order[-1] + if pins[die_pad]: + raise SystemExit(f"die pad {die_pad} unexpectedly bonded to GPIO {pins[die_pad]}") + del pins[die_pad] + order = order[:-1] + if not args.rows: + missing = [n for n in range(1, args.pins + 1) if n not in pins] + if missing: + raise SystemExit(f"pin numbers not found: {missing}") + + # Every pin must bond at most one GPIO pad, and the set of bonded pads + # must match the pads the figure shows for this package. + multi = {n: p for n, p in pins.items() if len(p) > 1} + if multi: + raise SystemExit(f"pins with more than one GPIO pad: {multi}") + table_pads = {p[0] for p in pins.values() if p} + figure_pads = parse_figure(section_lines) + if not figure_pads: + # CSP/BGA figures are unlabeled ball grids; there is nothing to + # cross-check against. The pad-count sanity check below still applies. + print("note: figure names no GPIO pads; skipping table/figure cross-check") + elif table_pads != figure_pads: + raise SystemExit( + "pad mismatch table vs figure:\n" + f" only in table: {sorted(table_pads - figure_pads)}\n" + f" only in figure: {sorted(figure_pads - table_pads)}" + ) + + skipped = [pin for pin in order if not pins[pin]] + + def pin_label(pin): + return f"{pin[0]}{pin[1]}" if isinstance(pin, tuple) else str(pin) + + header_notes = [] + if args.rows: + header_notes += [ + "# Ball packages: 'pin' numbers the balls sequentially in row-major", + "# order (the datasheet only labels them, e.g. B2).", + "", + ] + out_lines = [ + f"# Package pin map for {args.name}, transcribed from {args.source},", + f"# section {args.section} ({title}{f', {code}' if code else ''} {kind}),", + "# by tools/gen_package.py. Review against the datasheet figure before", + "# editing by hand; regenerate instead when the datasheet changes.", + "", + *header_notes, + f'name = "{args.name}"', + f"socs = [{', '.join(f'"{s}"' for s in args.socs.split(','))}]", + "", + ] + for number, pin in enumerate(order, start=1): + pad = pins[pin][0] if pins[pin] else None + if not pad: + continue + soc_pad = pad[0] * 32 + pad[1] + out_lines.append("[[pins]]") + out_lines.append(f"pin = {number}") + if isinstance(pin, tuple): + out_lines.append(f'ball = "{pin[0]}{pin[1]}"') + out_lines.append(f"pad = {soc_pad}") + out_lines.append(f'pad_name = "P{pad[0]}.{pad[1]:02d}"') + out_lines.append("") + if skipped: + out_lines.append("# Package pins without a GPIO pad (not routable):") + out_lines.append("# " + ", ".join(pin_label(pin) for pin in skipped)) + out_lines.append("") + + args.out.write_text("\n".join(out_lines)) + + print( + f"{args.out}: {sum(1 for p in pins.values() if p)} GPIO pads " + f"on {len(order)} package pins, {len(skipped)} skipped" + ) + for number, pin in enumerate(order, start=1): + pad = pins[pin][0] if pins[pin] else None + if pad: + print(f" pin {pin_label(pin):>4} -> P{pad[0]}.{pad[1]:02d}") + + +if __name__ == "__main__": + main() diff --git a/ports/zephyr-cp/modules/iobroker/tools/gen_package_c.py b/ports/zephyr-cp/modules/iobroker/tools/gen_package_c.py new file mode 100644 index 00000000000..1ac0feab7d3 --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/tools/gen_package_c.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Render a iobroker package TOML as the C pin map translation unit. + +Run at build time by the module's CMakeLists.txt for the package selected via +CONFIG_IOBROKER_PACKAGE_*; the output lands in the build directory, so +nothing generated is committed. The declarations live in iobroker.h. + +Usage: + gen_package_c.py packages/nrf54l15_qfn48.toml package_pins.c +""" + +import sys +import tomllib +from pathlib import Path + + +def main(): + if len(sys.argv) != 3: + raise SystemExit(__doc__) + toml_path, out_path = Path(sys.argv[1]), Path(sys.argv[2]) + with toml_path.open("rb") as f: + data = tomllib.load(f) + + lines = [ + f"// Generated from {toml_path.name} by tools/gen_package_c.py -- do not edit.", + f"// Package pin map '{data['name']}' for {', '.join(data['socs'])}.", + "", + "#include ", + "#include ", + "", + "#include ", + "", + "const iobroker_package_pin_t iobroker_package_pins[] = {", + ] + for entry in data["pins"]: + if "pad" not in entry: + # Non-GPIO pins (mounting, power, etc.) carry no SoC pad. + continue + ball = f" ({entry['ball']})" if "ball" in entry else "" + pad_name = f" {entry['pad_name']}" if "pad_name" in entry else "" + comment = f" //{ball}{pad_name}" + lines.append(f" {{ PACKAGE_PIN({entry['pin']}), {entry['pad']} }},{comment}") + lines += [ + "};", + f"const size_t iobroker_package_pin_count = {len(data['pins'])};", + "", + ] + out_path.write_text("\n".join(lines)) + + +if __name__ == "__main__": + main() diff --git a/ports/zephyr-cp/prj.conf b/ports/zephyr-cp/prj.conf index 80a28a9c272..6b480c373a0 100644 --- a/ports/zephyr-cp/prj.conf +++ b/ports/zephyr-cp/prj.conf @@ -38,6 +38,17 @@ CONFIG_EVENTS=y CONFIG_SERIAL=y +# Fail quickly on hung I2C devices (probe/scan also uses this timeout) +CONFIG_I2C_TRANSFER_TIMEOUT_MS=50 + +# Zephyr's Nordic SoC defconfig sets UART_USE_RUNTIME_CONFIGURE default n +# (soc/nordic/Kconfig.defconfig) for footprint. Without it the UARTE driver +# does not register api.configure and uart_configure() returns -ENOSYS +# unconditionally, which makes busio.UART fail with "Unsupported UART +# configuration" for every config. Runtime configure is required for the +# iobroker-routed UARTs to get their baudrate/parity/stop bits applied. +CONFIG_UART_USE_RUNTIME_CONFIGURE=y + CONFIG_LOG=y CONFIG_LOG_MAX_LEVEL=2 CONFIG_FRAME_POINTER=n diff --git a/ports/zephyr-cp/socs/nrf52840.conf b/ports/zephyr-cp/socs/nrf52840.conf index bf70997d83f..734d26db8e9 100644 --- a/ports/zephyr-cp/socs/nrf52840.conf +++ b/ports/zephyr-cp/socs/nrf52840.conf @@ -1,4 +1,3 @@ -CONFIG_NRFX_UARTE0=y -CONFIG_NRFX_UARTE1=y +CONFIG_NRFX_UARTE=y CONFIG_NRFX_POWER=y From bc6c07c33630f49e611f65fdf498719a717efe9c Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Fri, 18 Sep 2026 16:44:09 -0700 Subject: [PATCH 2/6] Pick up zephyr UART fix --- ports/zephyr-cp/zephyr-config/west.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/zephyr-cp/zephyr-config/west.yml b/ports/zephyr-cp/zephyr-config/west.yml index 73b87efd85e..5830910c27d 100644 --- a/ports/zephyr-cp/zephyr-config/west.yml +++ b/ports/zephyr-cp/zephyr-config/west.yml @@ -10,7 +10,7 @@ manifest: path: modules/bsim_hw_models/nrf_hw_models - name: zephyr url: https://github.com/adafruit/zephyr - revision: 52dc937c7cda06a1c18ff6adec281bbeb096b3d4 + revision: db2dd2aa0914b9b427baa6a3df3e6460699e4c80 clone-depth: 100 import: # Skip what no CircuitPython board can use, so west update fetches 37 From 2f444da2110f462b8632ec115e49f7f6015798d8 Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Mon, 21 Sep 2026 11:21:34 -0700 Subject: [PATCH 3/6] Include ignored module file --- ports/zephyr-cp/modules/iobroker/zephyr/module.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 ports/zephyr-cp/modules/iobroker/zephyr/module.yml diff --git a/ports/zephyr-cp/modules/iobroker/zephyr/module.yml b/ports/zephyr-cp/modules/iobroker/zephyr/module.yml new file mode 100644 index 00000000000..6d5d548c16e --- /dev/null +++ b/ports/zephyr-cp/modules/iobroker/zephyr/module.yml @@ -0,0 +1,10 @@ +# Zephyr module descriptor for the iobroker module. +# +# The module is loaded from the CircuitPython tree via ZEPHYR_EXTRA_MODULES in +# ports/zephyr-cp/CMakeLists.txt. To externalize it, move this directory into +# its own repository and either point ZEPHYR_EXTRA_MODULES at the new checkout +# or add it to the west manifest instead. +name: iobroker +build: + cmake: . + kconfig: Kconfig From cbcbafb1837be4ebf6c77d720b04d8c02915fada Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Mon, 21 Sep 2026 12:17:28 -0700 Subject: [PATCH 4/6] Fix nrf52840s with duplicate uses --- .../boards/adafruit/clue_nrf52840_zephyr/board.conf | 7 +++++++ .../boards/adafruit/clue_nrf52840_zephyr/board.overlay | 6 ++++++ .../adafruit/feather_nrf52840_sense_zephyr/board.overlay | 5 +++++ .../boards/adafruit/feather_nrf52840_zephyr/board.overlay | 5 +++++ 4 files changed, 23 insertions(+) diff --git a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.conf b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.conf index 6b4d778740d..75dfd33d908 100644 --- a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.conf +++ b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.conf @@ -2,6 +2,13 @@ CONFIG_USE_DT_CODE_PARTITION=y CONFIG_BOARD_SERIAL_BACKEND_CDC_ACM=n +# The board overlay deletes the Zephyr chosen console/shell/mcumgr nodes: +# CircuitPython drives USB CDC ACM through its own usb_cdc bindings. Turn off +# Zephyr's UART console so nothing references the deleted +# DT_CHOSEN(zephyr_console). SERIAL stays on for the dynamically routed UART. +CONFIG_UART_CONSOLE=n +CONFIG_LOG_BACKEND_UART=n + # Enable the ST7789V TFT via the Zephyr display subsystem CONFIG_DISPLAY=y diff --git a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay index f39df2ad94e..7b502df9bd4 100644 --- a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay @@ -120,6 +120,12 @@ pinctrl-names = "default", "sleep"; }; +// SPI1 shares the 0x40004000 instance with I2C1, which is enabled above, so +// leave SPI1 disabled. +&spi1 { + status = "disabled"; +}; + &spi3 { status = "okay"; zephyr,deferred-init; diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.overlay b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.overlay index 2065ba5e433..5c24d0f50f3 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.overlay @@ -121,6 +121,11 @@ pinctrl-names = "default", "sleep"; }; +// SPI1 is the same peripheral instance as I2C1, so leave it disabled. +&spi1 { + status = "disabled"; +}; + &spi2 { status = "okay"; zephyr,deferred-init; diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay index ea1eeca9843..9a3bd0051c2 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay @@ -120,6 +120,11 @@ pinctrl-names = "default", "sleep"; }; +// SPI1 is the same peripheral instance as I2C1, so leave it disabled. +&spi1 { + status = "disabled"; +}; + &spi2 { status = "okay"; zephyr,deferred-init; From ad53ec5736fe20cddb8d1139bb6754d8237eb075 Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Mon, 21 Sep 2026 14:47:49 -0700 Subject: [PATCH 5/6] Fix native_sim by adding 1:1 pin mapping --- .../boards/native/native_sim/board.conf | 4 ++++ .../boards/native/native_sim_asan/board.conf | 4 ++++ ports/zephyr-cp/cptools/zephyr2cp.py | 19 +++++++++++++------ ports/zephyr-cp/modules/iobroker/Kconfig | 14 ++++++++++++-- .../modules/iobroker/Kconfig.packages | 6 +++--- ports/zephyr-cp/modules/iobroker/README.md | 10 +++++++--- .../zephyr-cp/modules/iobroker/src/iobroker.c | 12 ++++++++++++ 7 files changed, 55 insertions(+), 14 deletions(-) diff --git a/ports/zephyr-cp/boards/native/native_sim/board.conf b/ports/zephyr-cp/boards/native/native_sim/board.conf index a550d70dacc..08c386bf959 100644 --- a/ports/zephyr-cp/boards/native/native_sim/board.conf +++ b/ports/zephyr-cp/boards/native/native_sim/board.conf @@ -1,6 +1,10 @@ # No Bluetooth hardware on native_sim CONFIG_BT=n +# native_sim has no physical package: use the identity package pin map so +# pin objects (LED, P_00..P_31) resolve to their GPIO controller. +CONFIG_IOBROKER_PACKAGE_ONE_TO_ONE=y + CONFIG_EMUL=y CONFIG_GPIO=y CONFIG_NATIVE_SIM_SLOWDOWN_TO_REAL_TIME=n diff --git a/ports/zephyr-cp/boards/native/native_sim_asan/board.conf b/ports/zephyr-cp/boards/native/native_sim_asan/board.conf index 62ebc476a8e..66362a9aab7 100644 --- a/ports/zephyr-cp/boards/native/native_sim_asan/board.conf +++ b/ports/zephyr-cp/boards/native/native_sim_asan/board.conf @@ -4,6 +4,10 @@ CONFIG_ASAN=y # No Bluetooth hardware on native_sim CONFIG_BT=n +# native_sim has no physical package: use the identity package pin map so +# pin objects (LED, P_00..P_31) resolve to their GPIO controller. +CONFIG_IOBROKER_PACKAGE_ONE_TO_ONE=y + CONFIG_EMUL=y CONFIG_GPIO=y CONFIG_NATIVE_SIM_SLOWDOWN_TO_REAL_TIME=n diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index 071ad6a1baf..e7e30c24735 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -956,9 +956,9 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig port_indexes[label] = int(match.group(1)) if match else len(port_indexes) # Package pin map selected through the IOBROKER_PACKAGE choice: map each # SoC pad to the package pin it is bonded to so that the pin objects can - # hand package pins straight to the iobroker module. When no package - # applies to the SoC (IOBROKER_PACKAGE_NONE) there is no map, so the pin - # objects get IOBROKER_NO_PIN instead. + # hand package pins straight to the iobroker module. The 1:1 choice is an + # identity map (package pin number == global pin number); IOBROKER_PACKAGE_NONE + # has no map, so the pin objects get IOBROKER_NO_PIN instead. package_pin_of_pad = {} package_pins = None package_choice = None @@ -968,8 +968,16 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig if not stripped.startswith("CONFIG_IOBROKER_PACKAGE_") or not stripped.endswith("=y"): continue package_choice = stripped[len("CONFIG_IOBROKER_PACKAGE_") : -len("=y")].lower() - if package_choice == "none": - continue + break + if package_choice == "one_to_one": + # Identity map over the enabled GPIO controllers. + package_pins = [] + for ioport in sorted(ioports.keys()): + for num in ioports[ioport]: + global_number = port_indexes[ioport] * 32 + num + package_pins.append({"pin": global_number, "pad": global_number}) + package_pin_of_pad[global_number] = global_number + elif package_choice not in (None, "none"): package_toml = ( pathlib.Path(__file__).resolve().parent.parent / "modules" @@ -983,7 +991,6 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig for package_pin_entry in package_pins: if "pad" in package_pin_entry: package_pin_of_pad[package_pin_entry["pad"]] = package_pin_entry["pin"] - break # Board pin names from circuitpython.toml: ``[pins]`` maps a board module # name to a package pin number or ball id, resolved to a SoC pad with the # package pin map above. This is independent of Zephyr's devicetree diff --git a/ports/zephyr-cp/modules/iobroker/Kconfig b/ports/zephyr-cp/modules/iobroker/Kconfig index 07e9bae8b07..178bd42c832 100644 --- a/ports/zephyr-cp/modules/iobroker/Kconfig +++ b/ports/zephyr-cp/modules/iobroker/Kconfig @@ -18,8 +18,18 @@ choice IOBROKER_PACKAGE help The module resolves requested package pins to SoC pads through the package pin map selected here. Reference maps shipped with the module - live in packages/; when none applies to the SoC, the NONE option keeps - the map empty and every package pin lookup fails with -EINVAL. + live in packages/; the ONE_TO_ONE option is an identity map for boards + without a transcribed physical package, and NONE keeps the map empty so + every package pin lookup fails with -EINVAL. + +config IOBROKER_PACKAGE_ONE_TO_ONE + bool "1:1 (package pin number is the global pin number)" + help + Identity package pin map: package pin N maps to SoC pad N, where the + SoC pad is the global pin numbering (gpio port index * 32 + pin within + the port). Boards without a transcribed physical package, such as the + native simulator, use this so that pin objects can hand the iobroker + module a package pin that resolves to their pad. config IOBROKER_PACKAGE_NONE bool "None (package pin lookups fail)" diff --git a/ports/zephyr-cp/modules/iobroker/Kconfig.packages b/ports/zephyr-cp/modules/iobroker/Kconfig.packages index a4b55bd4f8c..864b635e08b 100644 --- a/ports/zephyr-cp/modules/iobroker/Kconfig.packages +++ b/ports/zephyr-cp/modules/iobroker/Kconfig.packages @@ -8,8 +8,8 @@ # Choice defaults live here too, most-specific first: the development kits' # packages preselect the reference map for their SoC; boards with more than # one matching package (e.g. nRF52840 modules) set their choice explicitly in -# board.conf; everything else falls back to NONE so package pin lookups fail -# cleanly. +# board.conf; SoCs with no reference map fall back to the 1:1 identity map +# (package pin number == global pin number), which lets their pins resolve. config IOBROKER_PACKAGE_NRF54L15_QFN48 bool "nRF54L15/10/05 QFN48 (QFAA)" @@ -67,5 +67,5 @@ choice IOBROKER_PACKAGE default IOBROKER_PACKAGE_NRF5340_QKAA if SOC_NRF5340_CPUAPP_QKAA default IOBROKER_PACKAGE_NRF52840_AQFN73 if SOC_NRF52840_QIAA default IOBROKER_PACKAGE_RP2040_QFN56 if SOC_RP2040 - default IOBROKER_PACKAGE_NONE + default IOBROKER_PACKAGE_ONE_TO_ONE endchoice diff --git a/ports/zephyr-cp/modules/iobroker/README.md b/ports/zephyr-cp/modules/iobroker/README.md index 0e360bb2ee0..7a8a68c4949 100644 --- a/ports/zephyr-cp/modules/iobroker/README.md +++ b/ports/zephyr-cp/modules/iobroker/README.md @@ -59,10 +59,14 @@ const uint16_t iobroker_reserved_pads[]; // + _pin_count The package pin map is selected from the module's reference maps: `Kconfig.packages` offers one option per transcribed package (`packages/*.toml`), each visible only for the SoCs it applies to and -preselected for the development kits. When no map applies to a SoC the -`IOBROKER_PACKAGE_NONE` choice is used and an empty map is generated, so +preselected for the development kits. SoCs with no reference map fall back to +`IOBROKER_PACKAGE_ONE_TO_ONE`, an identity map where the package pin number +is the global pin number (gpio port index * 32 + pin within the port), so +boards without a transcribed physical package can still resolve their pins. +`IOBROKER_PACKAGE_NONE` is also available and generates an empty map, making package pin lookups fail with `-EINVAL`. The selected TOML (or the empty -map) is rendered into a build-directory translation unit at build time. +map) is rendered into a build-directory translation unit at build time. The +identity map needs no rendered table: the core applies it directly. New maps are transcribed from a SoC datasheet with `tools/gen_package.py` (see the script's docstring; the datasheets live in `datasheets/`). diff --git a/ports/zephyr-cp/modules/iobroker/src/iobroker.c b/ports/zephyr-cp/modules/iobroker/src/iobroker.c index 965a27682f6..64ef475dce2 100644 --- a/ports/zephyr-cp/modules/iobroker/src/iobroker.c +++ b/ports/zephyr-cp/modules/iobroker/src/iobroker.c @@ -39,6 +39,11 @@ int iobroker_gpio_split(uint16_t number, const struct device **port_out, int iobroker_gpio_package_pin(uint8_t port, gpio_pin_t pin, package_pin_t *package_pin_out) { uint16_t soc_pad = (uint16_t)((uint32_t)port * 32U + pin); + #if defined(CONFIG_IOBROKER_PACKAGE_ONE_TO_ONE) + // Identity map: the package pin is the global pin number. + *package_pin_out = soc_pad; + return 0; + #else for (size_t i = 0; i < iobroker_package_pin_count; i++) { if (iobroker_package_pins[i].soc_pad == soc_pad) { *package_pin_out = iobroker_package_pins[i].package_pin; @@ -46,6 +51,7 @@ int iobroker_gpio_package_pin(uint8_t port, gpio_pin_t pin, } } return -EINVAL; + #endif } int iobroker_package_pin_soc_pad(package_pin_t pin, uint16_t *soc_pad_out) { @@ -53,6 +59,11 @@ int iobroker_package_pin_soc_pad(package_pin_t pin, uint16_t *soc_pad_out) { *soc_pad_out = IOBROKER_NO_PIN; return 0; } + #if defined(CONFIG_IOBROKER_PACKAGE_ONE_TO_ONE) + // Identity map: the package pin is the global pin number. + *soc_pad_out = pin; + return 0; + #else for (size_t i = 0; i < iobroker_package_pin_count; i++) { if (iobroker_package_pins[i].package_pin == pin) { *soc_pad_out = iobroker_package_pins[i].soc_pad; @@ -60,6 +71,7 @@ int iobroker_package_pin_soc_pad(package_pin_t pin, uint16_t *soc_pad_out) { } } return -EINVAL; + #endif } #if !IOBROKER_ROUTING From cfd2f749263740a9b1907c4404d48cd50cfec8d7 Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Tue, 22 Sep 2026 11:16:24 -0700 Subject: [PATCH 6/6] Tweaks based on review feedback --- ports/zephyr-cp/common-hal/busio/I2C.h | 4 +- ports/zephyr-cp/common-hal/busio/SPI.h | 8 +-- ports/zephyr-cp/common-hal/busio/UART.h | 12 ++-- .../common-hal/rotaryio/IncrementalEncoder.c | 67 ++++++------------- ports/zephyr-cp/modules/iobroker/README.md | 19 ++++++ .../iobroker/include/iobroker/iobroker.h | 7 +- 6 files changed, 54 insertions(+), 63 deletions(-) diff --git a/ports/zephyr-cp/common-hal/busio/I2C.h b/ports/zephyr-cp/common-hal/busio/I2C.h index fcaa0298a8a..607a0422d9c 100644 --- a/ports/zephyr-cp/common-hal/busio/I2C.h +++ b/ports/zephyr-cp/common-hal/busio/I2C.h @@ -14,14 +14,14 @@ typedef struct { mp_obj_base_t base; const struct device *i2c_device; + const mcu_pin_obj_t *sda; + const mcu_pin_obj_t *scl; struct k_mutex mutex; bool has_lock; // True when the underlying Zephyr device was dynamically routed to the // pins below at construction time. Such objects deinitialize the device // and release their pins. bool dynamic; - const mcu_pin_obj_t *sda; - const mcu_pin_obj_t *scl; } busio_i2c_obj_t; // Helper function to construct from Zephyr device tree device diff --git a/ports/zephyr-cp/common-hal/busio/SPI.h b/ports/zephyr-cp/common-hal/busio/SPI.h index 57641c173ac..fa5de574116 100644 --- a/ports/zephyr-cp/common-hal/busio/SPI.h +++ b/ports/zephyr-cp/common-hal/busio/SPI.h @@ -16,16 +16,16 @@ typedef struct { mp_obj_base_t base; const struct device *spi_device; struct k_mutex mutex; - bool has_lock; struct spi_config config[2]; // Two configs for pointer comparison by driver uint8_t active_config; // Index of currently active config (0 or 1) struct k_poll_signal signal; - // True when the underlying Zephyr device was dynamically routed to the - // pins below at construction time. - bool dynamic; const mcu_pin_obj_t *clock; const mcu_pin_obj_t *mosi; const mcu_pin_obj_t *miso; + // True when the underlying Zephyr device was dynamically routed to the + // pins above at construction time. + bool dynamic; + bool has_lock; } busio_spi_obj_t; // Helper function for Zephyr-specific initialization from device tree diff --git a/ports/zephyr-cp/common-hal/busio/UART.h b/ports/zephyr-cp/common-hal/busio/UART.h index 369e2e2bf93..3e8614a8dd2 100644 --- a/ports/zephyr-cp/common-hal/busio/UART.h +++ b/ports/zephyr-cp/common-hal/busio/UART.h @@ -20,18 +20,18 @@ typedef struct { k_timeout_t timeout; k_timeout_t write_timeout; + byte *receiver_buffer; + const mcu_pin_obj_t *tx; + const mcu_pin_obj_t *rx; + const mcu_pin_obj_t *rts; + const mcu_pin_obj_t *cts; bool rx_paused; // set by irq if no space in rbuf // True when the underlying Zephyr device was dynamically routed to the - // pins below at construction time. Such objects own their receiver + // pins above at construction time. Such objects own their receiver // buffer and deinitialize the device and release their pins. bool dynamic; - byte *receiver_buffer; - const mcu_pin_obj_t *tx; - const mcu_pin_obj_t *rx; - const mcu_pin_obj_t *rts; - const mcu_pin_obj_t *cts; } busio_uart_obj_t; // Helper function for Zephyr-specific initialization from device tree diff --git a/ports/zephyr-cp/common-hal/rotaryio/IncrementalEncoder.c b/ports/zephyr-cp/common-hal/rotaryio/IncrementalEncoder.c index 4901bbbe43b..07ac4fa1634 100644 --- a/ports/zephyr-cp/common-hal/rotaryio/IncrementalEncoder.c +++ b/ports/zephyr-cp/common-hal/rotaryio/IncrementalEncoder.c @@ -37,6 +37,17 @@ static void incrementalencoder_gpio_callback(const struct device *port, shared_module_softencoder_state_update(self, new_state); } +// Runs an iobroker/GPIO call and, on failure, releases any partial setup +// before raising a Python exception with the Zephyr errno. +#define CHECK_RESULT_OR_DEINIT(x) \ + do { \ + int _res = (x); \ + if (_res < 0) { \ + common_hal_rotaryio_incrementalencoder_deinit(self); \ + raise_zephyr_error(_res); \ + } \ + } while (0) + void common_hal_rotaryio_incrementalencoder_construct(rotaryio_incrementalencoder_obj_t *self, const mcu_pin_obj_t *pin_a, const mcu_pin_obj_t *pin_b) { // Ensure object starts in its deinit state. @@ -50,64 +61,26 @@ void common_hal_rotaryio_incrementalencoder_construct(rotaryio_incrementalencode // refuse them while this object holds them. The calls also resolve the // GPIO controller devices and pin numbers from the pins' global numbers; // they are kept in the object for every later pad operation. - int ret = iobroker_gpio_allocate(pin_a->package_pin, &self->port_a, &self->number_a); - if (ret < 0) { - common_hal_rotaryio_incrementalencoder_deinit(self); - raise_zephyr_error(ret); - } + CHECK_RESULT_OR_DEINIT(iobroker_gpio_allocate(pin_a->package_pin, &self->port_a, &self->number_a)); + CHECK_RESULT_OR_DEINIT(iobroker_gpio_allocate(pin_b->package_pin, &self->port_b, &self->number_b)); - ret = iobroker_gpio_allocate(pin_b->package_pin, &self->port_b, &self->number_b); - if (ret < 0) { - common_hal_rotaryio_incrementalencoder_deinit(self); - raise_zephyr_error(ret); - } + CHECK_RESULT_OR_DEINIT(device_is_ready(self->port_a) && device_is_ready(self->port_b) ? 0 : -ENODEV); - if (!device_is_ready(self->port_a) || !device_is_ready(self->port_b)) { - common_hal_rotaryio_incrementalencoder_deinit(self); - raise_zephyr_error(-ENODEV); - } - - int result = gpio_pin_configure(self->port_a, self->number_a, GPIO_INPUT | GPIO_PULL_UP); - if (result != 0) { - common_hal_rotaryio_incrementalencoder_deinit(self); - raise_zephyr_error(result); - } - - result = gpio_pin_configure(self->port_b, self->number_b, GPIO_INPUT | GPIO_PULL_UP); - if (result != 0) { - common_hal_rotaryio_incrementalencoder_deinit(self); - raise_zephyr_error(result); - } + CHECK_RESULT_OR_DEINIT(gpio_pin_configure(self->port_a, self->number_a, GPIO_INPUT | GPIO_PULL_UP)); + CHECK_RESULT_OR_DEINIT(gpio_pin_configure(self->port_b, self->number_b, GPIO_INPUT | GPIO_PULL_UP)); self->callback_a.encoder = self; gpio_init_callback(&self->callback_a.callback, incrementalencoder_gpio_callback, BIT(self->number_a)); - result = gpio_add_callback(self->port_a, &self->callback_a.callback); - if (result != 0) { - common_hal_rotaryio_incrementalencoder_deinit(self); - raise_zephyr_error(result); - } + CHECK_RESULT_OR_DEINIT(gpio_add_callback(self->port_a, &self->callback_a.callback)); self->callback_b.encoder = self; gpio_init_callback(&self->callback_b.callback, incrementalencoder_gpio_callback, BIT(self->number_b)); - result = gpio_add_callback(self->port_b, &self->callback_b.callback); - if (result != 0) { - common_hal_rotaryio_incrementalencoder_deinit(self); - raise_zephyr_error(result); - } + CHECK_RESULT_OR_DEINIT(gpio_add_callback(self->port_b, &self->callback_b.callback)); - result = gpio_pin_interrupt_configure(self->port_a, self->number_a, GPIO_INT_EDGE_BOTH); - if (result != 0) { - common_hal_rotaryio_incrementalencoder_deinit(self); - raise_zephyr_error(result); - } - - result = gpio_pin_interrupt_configure(self->port_b, self->number_b, GPIO_INT_EDGE_BOTH); - if (result != 0) { - common_hal_rotaryio_incrementalencoder_deinit(self); - raise_zephyr_error(result); - } + CHECK_RESULT_OR_DEINIT(gpio_pin_interrupt_configure(self->port_a, self->number_a, GPIO_INT_EDGE_BOTH)); + CHECK_RESULT_OR_DEINIT(gpio_pin_interrupt_configure(self->port_b, self->number_b, GPIO_INT_EDGE_BOTH)); int a = gpio_pin_get(self->port_a, self->number_a); int b = gpio_pin_get(self->port_b, self->number_b); diff --git a/ports/zephyr-cp/modules/iobroker/README.md b/ports/zephyr-cp/modules/iobroker/README.md index 7a8a68c4949..c6e13065b39 100644 --- a/ports/zephyr-cp/modules/iobroker/README.md +++ b/ports/zephyr-cp/modules/iobroker/README.md @@ -4,6 +4,25 @@ A Zephyr module for **dynamic peripheral allocation and runtime pin routing**: pick a free bus instance (I2C, SPI, UART) enabled in the devicetree, re-route it to requested pins at runtime and hand the Zephyr device to the caller. +Pins are specified using `package_pin_t` and represent a single pin on a package +or module containing a system-on-a-chip (SoC). This is the most common boundary +between an SoC and printed circuit board (PCB). Packages and modules may choose +to map more than one SoC pin to a package pin and this way IOBroker ensures that +each package pin is only used for one thing at a time. GPIO port and pin numbers +are often used as names for these pins but using package pins allow us to +accommodate pins without GPIO and those with multiple GPIO. + +IOBroker takes in a number of package pins and a device type. Device types are +usually `drivers/` in the zephyr source tree. For example, +`int iobroker_i2c_allocate(package_pin_t sda, package_pin_t scl, const struct device **dev_out);` +will find a Zephyr I2C device that pins sda and scl can be connected to, connect +them using pinctrl, claim these resources and return it. It will return +`-ENODEV` if no such device can be found. The board DTS must enable these +devices with `status = "okay";`, mark them as `zephyr,deferred-init;` and +provide default pinctrl settings that will be overridden. + +## Status + Currently, runtime routing is implemented for nRF SoCs, whose pin control encoding can be computed at runtime and whose peripherals can be routed to (almost) any pin via PSEL. On other SoCs the module compiles but the allocate diff --git a/ports/zephyr-cp/modules/iobroker/include/iobroker/iobroker.h b/ports/zephyr-cp/modules/iobroker/include/iobroker/iobroker.h index cdb268625bf..7e28b1ed78c 100644 --- a/ports/zephyr-cp/modules/iobroker/include/iobroker/iobroker.h +++ b/ports/zephyr-cp/modules/iobroker/include/iobroker/iobroker.h @@ -27,10 +27,9 @@ #include // Runtime bus routing lets the module re-route a bus instance's pins at -// runtime. It is available only on nRF SoCs, and only when the nRF pinctrl -// driver is built with dynamic pinctrl states and device de-init support. -// Without it the bus allocate/release functions report -ENOSYS and the bus -// instance tables are not used. +// runtime. It is available when Zephyr is built with dynamic pinctrl states and +// device de-init support. Without it the bus allocate/release functions report +// -ENOSYS and the bus instance tables are not used. #if defined(CONFIG_PINCTRL_NRF) && defined(CONFIG_PINCTRL_DYNAMIC) && \ defined(CONFIG_DEVICE_DEINIT_SUPPORT) #define IOBROKER_ROUTING 1