From 1e2e1eccb4f04e311d4473a7b7147b73c5fac26e Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Mon, 12 Oct 2026 18:39:16 -0400 Subject: [PATCH 1/9] zephyr-cp: make the Feather UART console a build option The Feather nRF52840 and Feather Bluefruit Sense put the Zephyr console on UART0, which fixed TX/RX and kept them from busio.UART. UART0 is now a dynamic iobroker instance like UART1, and the console comes back only in CONSOLE=1 builds through the board's console.overlay and console.conf. DEBUG=1 implies CONSOLE=1. Co-Authored-By: Claude Opus 5.5 --- ports/zephyr-cp/Makefile | 20 +++++---- ports/zephyr-cp/README.md | 5 +++ .../feather_nrf52840_sense_zephyr/board.conf | 5 +++ .../board.overlay | 42 ++++++++++++++----- .../console.conf | 4 ++ .../console.overlay | 18 ++++++++ .../feather_nrf52840_zephyr/board.conf | 5 +++ .../feather_nrf52840_zephyr/board.overlay | 37 ++++++++++++---- .../feather_nrf52840_zephyr/console.conf | 4 ++ .../feather_nrf52840_zephyr/console.overlay | 18 ++++++++ 10 files changed, 132 insertions(+), 26 deletions(-) create mode 100644 ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.conf create mode 100644 ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.overlay create mode 100644 ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/console.conf create mode 100644 ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/console.overlay diff --git a/ports/zephyr-cp/Makefile b/ports/zephyr-cp/Makefile index 8e134ca66fa..f889f5eea90 100644 --- a/ports/zephyr-cp/Makefile +++ b/ports/zephyr-cp/Makefile @@ -61,16 +61,22 @@ endif # When DEBUG=1, apply additional Kconfig fragments for debug-friendly settings. DEBUG_CONF_FILE ?= $(CURDIR)/debug.conf ifeq ($(DEBUG),1) -ifneq ($(CP_BOARD_CONF),) -CP_BOARD_CONF := $(CP_BOARD_CONF);$(DEBUG_CONF_FILE) -else -CP_BOARD_CONF := $(DEBUG_CONF_FILE) +CP_BOARD_CONF := $(strip $(CP_BOARD_CONF) $(DEBUG_CONF_FILE)) endif +# When CONSOLE=1 (implied by DEBUG=1), apply the board's console.conf and console.overlay if it +# has them. Boards whose console pins are also user pins enable the Zephyr UART console only then. +CONSOLE ?= $(DEBUG) +CP_BOARD_CONSOLE_OVERLAY := +ifeq ($(CONSOLE),1) +CP_BOARD_CONF := $(strip $(CP_BOARD_CONF) $(wildcard $(CP_BOARD_DIR)/console.conf)) +CP_BOARD_CONSOLE_OVERLAY := $(wildcard $(CP_BOARD_DIR)/console.overlay) endif -# Always pass the conf file, even if empty. This undoes DEBUG=1 when it's omitted on a later run. -# Quoted because with DEBUG=1 the value is a semicolon-separated list, and an unquoted semicolon -# would end the shell command. +CP_BOARD_CONF := $(subst $(eval) ,;,$(CP_BOARD_CONF)) +# Always pass the conf and console overlay files, even if empty. This undoes DEBUG=1 or CONSOLE=1 +# when it's omitted on a later run. Quoted because the conf value can be a semicolon-separated +# list, and an unquoted semicolon would end the shell command. WEST_CMAKE_ARGS += -Dzephyr-cp_EXTRA_CONF_FILE="$(CP_BOARD_CONF)" +WEST_CMAKE_ARGS += -Dzephyr-cp_EXTRA_DTC_OVERLAY_FILE="$(CP_BOARD_CONSOLE_OVERLAY)" # Boards can opt into building the MCUboot bootloader (and thus BLE OTA # updates) by shipping a sysbuild.conf fragment. It sets sysbuild-level diff --git a/ports/zephyr-cp/README.md b/ports/zephyr-cp/README.md index 4e895936787..f3ba5e63adb 100644 --- a/ports/zephyr-cp/README.md +++ b/ports/zephyr-cp/README.md @@ -28,6 +28,11 @@ make BOARD=nordic_nrf7002dk This uses Zephyr's cmake to generate Makefiles that then delegate to `tools/cpbuild/build_circuitpython.py` to build the CircuitPython bits in parallel. +`DEBUG=1` adds the debug Kconfig settings in `debug.conf`. `CONSOLE=1` (implied +by `DEBUG=1`) applies a board's `console.conf` and `console.overlay`, if it has +them. Boards whose console UART pins are also user pins, such as the Feather +nRF52840, have the Zephyr UART console only in those builds. + ## Native simulator build container Building the native sim requires `libsdl2-dev:i386` and other 32bit dependencies that 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 eae90680937..414256fc88f 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,5 +1,10 @@ CONFIG_BOARD_SERIAL_BACKEND_CDC_ACM=n +# No Zephyr UART console by default (see board.overlay); a CONSOLE=1 build turns +# it back on in console.conf. +CONFIG_UART_CONSOLE=n +CONFIG_LOG_BACKEND_UART=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 148011e6fab..dd5cbaecef1 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 @@ -1,13 +1,15 @@ /* Partition layout is owned by the mcuboot fork * (dts/adafruit/adafruit_feather_nrf52840.dtsi) and applied to this image by * sysbuild. This overlay only adds board/app config. */ +/* No Zephyr console by default, so TX/RX are free for busio.UART. A CONSOLE=1 + * build puts the console back on UART0 (console.overlay). */ / { 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; }; }; @@ -18,13 +20,26 @@ // 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.) +// constructed, and de-initializes it on release: I2C1, SPI2, SPI3, UART0 +// and UART1. (The fixed I2C0 is the Stemma QT bus with the SHT31 sensor. 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 { + uart0_dyn_default: uart0_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart0_dyn_sleep: uart0_dyn_sleep { + group1 { + psels = , + ; + }; + }; + i2c1_dyn_default: i2c1_dyn_default { group1 { psels = , @@ -197,7 +212,12 @@ }; &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"; }; // PWM instances, same scheme: all four outputs disconnected and deferred diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.conf b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.conf new file mode 100644 index 00000000000..83db0acb170 --- /dev/null +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.conf @@ -0,0 +1,4 @@ +# CONSOLE=1 and DEBUG=1 builds: the Zephyr UART console on TX/RX +# (console.overlay). +CONFIG_UART_CONSOLE=y +CONFIG_LOG_BACKEND_UART=y diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.overlay b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.overlay new file mode 100644 index 00000000000..095c4aa1d2d --- /dev/null +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.overlay @@ -0,0 +1,18 @@ +/* Applied after board.overlay in CONSOLE=1 and DEBUG=1 builds: the Zephyr + * console on UART0, fixed to the TX/RX pins. TX/RX are then not available to + * busio.UART. */ +/ { + chosen { + zephyr,console = &uart0; + zephyr,shell-uart = &uart0; + zephyr,uart-mcumgr = &uart0; + zephyr,bt-mon-uart = &uart0; + zephyr,bt-c2h-uart = &uart0; + }; +}; + +&uart0 { + /delete-property/ zephyr,deferred-init; + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; +}; 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 eae90680937..414256fc88f 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.conf +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.conf @@ -1,5 +1,10 @@ CONFIG_BOARD_SERIAL_BACKEND_CDC_ACM=n +# No Zephyr UART console by default (see board.overlay); a CONSOLE=1 build turns +# it back on in console.conf. +CONFIG_UART_CONSOLE=n +CONFIG_LOG_BACKEND_UART=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 935a5ade10d..4004ce0522a 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay @@ -1,13 +1,15 @@ /* Partition layout is owned by the mcuboot fork * (dts/adafruit/adafruit_feather_nrf52840.dtsi) and applied to this image by * sysbuild. This overlay only adds board/app config. */ +/* No Zephyr console by default, so TX/RX are free for busio.UART. A CONSOLE=1 + * build puts the console back on UART0 (console.overlay). */ / { 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; }; }; @@ -18,12 +20,26 @@ // 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 +// constructed, and de-initializes it on release: I2C1, SPI2, SPI3, UART0 +// and UART1. (The fixed I2C0 is the Stemma QT bus. 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 { + uart0_dyn_default: uart0_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart0_dyn_sleep: uart0_dyn_sleep { + group1 { + psels = , + ; + }; + }; + i2c1_dyn_default: i2c1_dyn_default { group1 { psels = , @@ -232,7 +248,12 @@ }; &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"; }; #include "../../../app.overlay" diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/console.conf b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/console.conf new file mode 100644 index 00000000000..83db0acb170 --- /dev/null +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/console.conf @@ -0,0 +1,4 @@ +# CONSOLE=1 and DEBUG=1 builds: the Zephyr UART console on TX/RX +# (console.overlay). +CONFIG_UART_CONSOLE=y +CONFIG_LOG_BACKEND_UART=y diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/console.overlay b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/console.overlay new file mode 100644 index 00000000000..095c4aa1d2d --- /dev/null +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/console.overlay @@ -0,0 +1,18 @@ +/* Applied after board.overlay in CONSOLE=1 and DEBUG=1 builds: the Zephyr + * console on UART0, fixed to the TX/RX pins. TX/RX are then not available to + * busio.UART. */ +/ { + chosen { + zephyr,console = &uart0; + zephyr,shell-uart = &uart0; + zephyr,uart-mcumgr = &uart0; + zephyr,bt-mon-uart = &uart0; + zephyr,bt-c2h-uart = &uart0; + }; +}; + +&uart0 { + /delete-property/ zephyr,deferred-init; + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; +}; From 3fe840296532b15822d13f8bf5ff0469e7573480 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Mon, 12 Oct 2026 20:44:40 -0400 Subject: [PATCH 2/9] zephyr-cp: make the Feather nRF52840 I2C0 dynamic The upstream Feather nRF52840 and Feather Bluefruit Sense boards fix I2C0 to SCL/SDA, so busio.I2C() on those pins failed with "SCL in use" and the pins could not be used as GPIO. I2C0 is now a dynamic TWIM instance like I2C1. The Sense drops its SHT3XD node, which sat on the fixed bus. The pinctrl entries and device nodes in both overlays are now in alphabetical order; the compiled devicetree is otherwise unchanged. Co-Authored-By: Claude Opus 5.5 --- .../board.overlay | 205 ++++++++++-------- .../feather_nrf52840_zephyr/board.overlay | 185 +++++++++------- 2 files changed, 220 insertions(+), 170 deletions(-) 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 dd5cbaecef1..ed906edb858 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 @@ -20,23 +20,21 @@ // 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, UART0 -// and UART1. (The fixed I2C0 is the Stemma QT bus with the SHT31 sensor. 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.) +// constructed, and de-initializes it on release: I2C0, I2C1, SPI2, SPI3, +// UART0 and UART1. &pinctrl { - uart0_dyn_default: uart0_dyn_default { + i2c0_dyn_default: i2c0_dyn_default { group1 { - psels = , - ; + psels = , + ; }; }; - uart0_dyn_sleep: uart0_dyn_sleep { + i2c0_dyn_sleep: i2c0_dyn_sleep { group1 { - psels = , - ; + psels = , + ; }; }; @@ -54,52 +52,6 @@ }; }; - 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 = , - ; - }; - }; - pwm0_dyn_default: pwm0_dyn_default { group1 { psels = , @@ -171,52 +123,88 @@ ; }; }; -}; -&i2c1 { - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&i2c1_dyn_default>; - pinctrl-1 = <&i2c1_dyn_sleep>; - pinctrl-names = "default", "sleep"; -}; + spi2_dyn_default: spi2_dyn_default { + group1 { + psels = , + , + ; + }; + }; -// SPI1 is the same peripheral instance as I2C1, so leave it disabled. -&spi1 { - status = "disabled"; -}; + spi2_dyn_sleep: spi2_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; -&spi2 { - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&spi2_dyn_default>; - pinctrl-1 = <&spi2_dyn_sleep>; - pinctrl-names = "default", "sleep"; -}; + spi3_dyn_default: spi3_dyn_default { + group1 { + psels = , + , + ; + }; + }; -&spi3 { - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&spi3_dyn_default>; - pinctrl-1 = <&spi3_dyn_sleep>; - pinctrl-names = "default", "sleep"; + spi3_dyn_sleep: spi3_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; + + uart0_dyn_default: uart0_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart0_dyn_sleep: uart0_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + uart1_dyn_default: uart1_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart1_dyn_sleep: uart1_dyn_sleep { + group1 { + psels = , + ; + }; + }; }; -&uart1 { +// The upstream board fixes I2C0 to SCL/SDA with the legacy TWI driver. Use +// TWIM like I2C1 so iobroker can route it. +&i2c0 { + compatible = "nordic,nrf-twim"; status = "okay"; zephyr,deferred-init; - current-speed = <115200>; - pinctrl-0 = <&uart1_dyn_default>; - pinctrl-1 = <&uart1_dyn_sleep>; + pinctrl-0 = <&i2c0_dyn_default>; + pinctrl-1 = <&i2c0_dyn_sleep>; pinctrl-names = "default", "sleep"; }; -&uart0 { +// CircuitPython drives the SHT31 from Python, so drop the Zephyr sensor node +// on the now dynamic I2C0. +/delete-node/ &SHT3XD; + +&i2c1 { status = "okay"; zephyr,deferred-init; - current-speed = <115200>; - pinctrl-0 = <&uart0_dyn_default>; - pinctrl-1 = <&uart0_dyn_sleep>; + pinctrl-0 = <&i2c1_dyn_default>; + pinctrl-1 = <&i2c1_dyn_sleep>; pinctrl-names = "default", "sleep"; }; @@ -256,6 +244,45 @@ 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; + 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"; +}; + +&uart0 { + status = "okay"; + zephyr,deferred-init; + current-speed = <115200>; + pinctrl-0 = <&uart0_dyn_default>; + pinctrl-1 = <&uart0_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"; +}; + #include "../../../app.overlay" /* Enable the SAADC for analogio (AIN0..AIN7 are on P0.02..P0.05 and P0.28..P0.31). */ 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 4004ce0522a..dfdf0a4f184 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay @@ -20,23 +20,21 @@ // 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, UART0 -// and UART1. (The fixed I2C0 is the Stemma QT bus. 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.) +// constructed, and de-initializes it on release: I2C0, I2C1, SPI2, SPI3, +// UART0 and UART1. &pinctrl { - uart0_dyn_default: uart0_dyn_default { + i2c0_dyn_default: i2c0_dyn_default { group1 { - psels = , - ; + psels = , + ; }; }; - uart0_dyn_sleep: uart0_dyn_sleep { + i2c0_dyn_sleep: i2c0_dyn_sleep { group1 { - psels = , - ; + psels = , + ; }; }; @@ -54,52 +52,6 @@ }; }; - 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 = , - ; - }; - }; - pwm0_dyn_default: pwm0_dyn_default { group1 { psels = , @@ -171,43 +123,84 @@ ; }; }; -}; -&i2c1 { - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&i2c1_dyn_default>; - pinctrl-1 = <&i2c1_dyn_sleep>; - pinctrl-names = "default", "sleep"; -}; + spi2_dyn_default: spi2_dyn_default { + group1 { + psels = , + , + ; + }; + }; -// SPI1 is the same peripheral instance as I2C1, so leave it disabled. -&spi1 { - status = "disabled"; -}; + spi2_dyn_sleep: spi2_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; -&spi2 { - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&spi2_dyn_default>; - pinctrl-1 = <&spi2_dyn_sleep>; - pinctrl-names = "default", "sleep"; + spi3_dyn_default: spi3_dyn_default { + group1 { + psels = , + , + ; + }; + }; + + spi3_dyn_sleep: spi3_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; + + uart0_dyn_default: uart0_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart0_dyn_sleep: uart0_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + uart1_dyn_default: uart1_dyn_default { + group1 { + psels = , + ; + }; + }; + + uart1_dyn_sleep: uart1_dyn_sleep { + group1 { + psels = , + ; + }; + }; }; -&spi3 { +// The upstream board fixes I2C0 to SCL/SDA with the legacy TWI driver. Use +// TWIM like I2C1 so iobroker can route it. +&i2c0 { + compatible = "nordic,nrf-twim"; status = "okay"; zephyr,deferred-init; - pinctrl-0 = <&spi3_dyn_default>; - pinctrl-1 = <&spi3_dyn_sleep>; + pinctrl-0 = <&i2c0_dyn_default>; + pinctrl-1 = <&i2c0_dyn_sleep>; pinctrl-names = "default", "sleep"; }; -&uart1 { +&i2c1 { status = "okay"; zephyr,deferred-init; - current-speed = <115200>; - pinctrl-0 = <&uart1_dyn_default>; - pinctrl-1 = <&uart1_dyn_sleep>; + pinctrl-0 = <&i2c1_dyn_default>; + pinctrl-1 = <&i2c1_dyn_sleep>; pinctrl-names = "default", "sleep"; }; @@ -247,6 +240,27 @@ 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; + 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"; +}; + &uart0 { status = "okay"; zephyr,deferred-init; @@ -256,6 +270,15 @@ 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"; +}; + #include "../../../app.overlay" /* Enable the SAADC for analogio (AIN0..AIN7 are on P0.02..P0.05 and P0.28..P0.31). */ From 7454a0698a8d25f7a29d870039d0c4e333a1bc79 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Mon, 12 Oct 2026 20:44:58 -0400 Subject: [PATCH 3/9] zephyr-cp: define board.I2C(), SPI() and UART() in circuitpython.toml zephyr2cp.py named a board's main bus after the single enabled devicetree instance of each kind, which doesn't work once the instances are dynamic iobroker pools. A board now lists its buses as [[board_i2c]], [[board_spi]] and [[board_uart]] tables in circuitpython.toml, each with its board names and pins, and zephyr2cp.py generates the CIRCUITPY_BOARD_* pin tables for shared-module/board. As in the other ports, each bus is constructed on its pins on first use. The Feather nRF52840 and Feather Bluefruit Sense define I2C, SPI and UART this way. Co-Authored-By: Claude Opus 5.5 --- .../circuitpython.toml | 18 ++++ .../circuitpython.toml | 18 ++++ .../zephyr-cp/cptools/tests/test_zephyr2cp.py | 69 +++++++++++++ ports/zephyr-cp/cptools/zephyr2cp.py | 99 ++++++++++++++++++- 4 files changed, 202 insertions(+), 2 deletions(-) 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 985f53ca662..ecea8e87174 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 @@ -54,3 +54,21 @@ MICROPHONE_CLOCK = 18 MICROPHONE_DATA = 17 PROXIMITY_LIGHT_INTERRUPT = 47 ACCELEROMETER_GYRO_INTERRUPT = 4 + +# Board buses, as on the ports/ counterpart: board.I2C(), board.SPI() and +# board.UART(), constructed on these pins when first used. +[[board_i2c]] +names = ["I2C"] +scl = "SCL" +sda = "SDA" + +[[board_spi]] +names = ["SPI"] +clock = "SCK" +mosi = "MOSI" +miso = "MISO" + +[[board_uart]] +names = ["UART"] +tx = "TX" +rx = "RX" 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 ef4dc115a83..db6d04ed545 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/circuitpython.toml +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/circuitpython.toml @@ -52,3 +52,21 @@ LED = 8 RED_LED = 8 D3 = 8 BLUE_LED = 3 + +# Board buses, as on the ports/ counterpart: board.I2C(), board.SPI() and +# board.UART(), constructed on these pins when first used. +[[board_i2c]] +names = ["I2C"] +scl = "SCL" +sda = "SDA" + +[[board_spi]] +names = ["SPI"] +clock = "SCK" +mosi = "MOSI" +miso = "MISO" + +[[board_uart]] +names = ["UART"] +tx = "TX" +rx = "RX" diff --git a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py index 556aec0cccb..d69abc4f2a9 100644 --- a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py +++ b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py @@ -23,6 +23,7 @@ BLOCKED_FLASH_COMPAT, MINIMUM_RAM_SIZE, add_toml_pin_names, + _toml_board_buses, _toml_status_pin, ) @@ -796,3 +797,71 @@ def test_absent_key_is_none(self): def test_unknown_name_raises(self): with pytest.raises(RuntimeError, match="STATUS_NEOPIXEL = 'NOPE' is not a board pin name"): _toml_status_pin({"STATUS_NEOPIXEL": "NOPE"}, self.BOARD_NAMES, "STATUS_NEOPIXEL") + + +class TestTomlBoardBuses: + """Resolution of circuitpython.toml [[board_i2c]]/[[board_spi]]/[[board_uart]].""" + + BOARD_NAMES = { + ("gpio0", 11): ["SCL"], + ("gpio0", 12): ["SDA"], + ("gpio0", 14): ["SCK"], + ("gpio0", 13): ["MOSI"], + ("gpio0", 25): ["TX"], + ("gpio0", 24): ["RX"], + ("gpio1", 1): ["SCL1"], + ("gpio1", 2): ["SDA1"], + } + + def test_resolves_buses_in_order(self): + buses = _toml_board_buses( + { + "board_i2c": [ + {"names": ["I2C", "stemma_i2c"], "scl": "SCL", "sda": "SDA"}, + {"names": ["I2C1"], "scl": "SCL1", "sda": "SDA1"}, + ], + "board_uart": [{"names": ["UART"], "tx": "TX", "rx": "RX"}], + }, + self.BOARD_NAMES, + ) + assert buses == { + "i2c": [ + (["I2C", "STEMMA_I2C"], {"scl": ("gpio0", 11), "sda": ("gpio0", 12)}), + (["I2C1"], {"scl": ("gpio1", 1), "sda": ("gpio1", 2)}), + ], + "uart": [(["UART"], {"tx": ("gpio0", 25), "rx": ("gpio0", 24)})], + } + + def test_optional_pins_may_be_absent(self): + buses = _toml_board_buses( + {"board_spi": [{"names": ["SPI"], "clock": "SCK", "mosi": "MOSI"}]}, + self.BOARD_NAMES, + ) + assert buses == {"spi": [(["SPI"], {"clock": ("gpio0", 14), "mosi": ("gpio0", 13)})]} + + def test_absent_tables_are_empty(self): + assert _toml_board_buses({}, self.BOARD_NAMES) == {} + assert _toml_board_buses(None, self.BOARD_NAMES) == {} + + def test_missing_names_raises(self): + with pytest.raises(RuntimeError, match=r"\[\[board_i2c\]\] needs names"): + _toml_board_buses({"board_i2c": [{"scl": "SCL", "sda": "SDA"}]}, self.BOARD_NAMES) + + def test_missing_required_pin_raises(self): + with pytest.raises(RuntimeError, match=r"\[\[board_i2c\]\] needs sda"): + _toml_board_buses({"board_i2c": [{"names": ["I2C"], "scl": "SCL"}]}, self.BOARD_NAMES) + + def test_no_pins_raises(self): + with pytest.raises(RuntimeError, match="needs at least one pin"): + _toml_board_buses({"board_uart": [{"names": ["UART"]}]}, self.BOARD_NAMES) + + def test_unknown_key_raises(self): + with pytest.raises(RuntimeError, match=r"unknown keys \['sck'\]"): + _toml_board_buses({"board_spi": [{"names": ["SPI"], "sck": "SCK"}]}, self.BOARD_NAMES) + + def test_unknown_pin_raises(self): + with pytest.raises(RuntimeError, match="'NOPE' is not a board pin name"): + _toml_board_buses( + {"board_i2c": [{"names": ["I2C"], "scl": "NOPE", "sda": "SDA"}]}, + self.BOARD_NAMES, + ) diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index 660b263262f..f55b0582959 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -745,6 +745,52 @@ def _toml_status_pin(mpconfigboard, board_names, key): raise RuntimeError(f"circuitpython.toml {key} = {name!r} is not a board pin name") +# Pin fields of the circuitpython.toml [[board_i2c]], [[board_spi]] and +# [[board_uart]] tables, as (field, required). The field names match the +# CIRCUITPY_BOARD_*_PIN structs in shared-module/board. +BOARD_BUS_PINS = { + "i2c": (("scl", True), ("sda", True)), + "spi": (("clock", True), ("mosi", False), ("miso", False)), + "uart": (("tx", False), ("rx", False)), +} +# The board bus class each busio-capable devicetree driver provides. +BOARD_BUS_OF_DRIVER = {"i2c": "i2c", "spi": "spi", "serial": "uart"} + + +def _toml_board_buses(mpconfigboard, board_names): + """Resolve the circuitpython.toml board bus tables to pins. + + Each ``[[board_i2c]]``, ``[[board_spi]]`` or ``[[board_uart]]`` table is + one board bus, in instance order: ``names`` lists its board names and the + other keys name its pins. Returns ``{bus: [(names, {field: pin}), ...]}`` + for the bus classes the toml defines. + """ + buses = {} + for bus, fields in BOARD_BUS_PINS.items(): + key = f"board_{bus}" + resolved = [] + for table in (mpconfigboard or {}).get(key, []): + field_names = {field for field, _ in fields} + unknown = sorted(set(table) - field_names - {"names"}) + if unknown: + raise RuntimeError(f"circuitpython.toml [[{key}]] has unknown keys {unknown}") + names = table.get("names") + if not names: + raise RuntimeError(f"circuitpython.toml [[{key}]] needs names") + pins = {} + for field, required in fields: + if field in table: + pins[field] = _toml_status_pin(table, board_names, field) + elif required: + raise RuntimeError(f"circuitpython.toml [[{key}]] needs {field}") + if not pins: + raise RuntimeError(f"circuitpython.toml [[{key}]] needs at least one pin") + resolved.append(([str(name).upper() for name in names], pins)) + if resolved: + buses[bus] = resolved + return buses + + @cpbuild.run_in_thread def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfigboard=None): # noqa: C901 board_dir = builddir / "board" @@ -1190,6 +1236,13 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig # the [pins] table, that the status LED code drives with neopixel_write. status_neopixel = _toml_status_pin(mpconfigboard, board_names, "STATUS_NEOPIXEL") status_neopixel_power = _toml_status_pin(mpconfigboard, board_names, "STATUS_NEOPIXEL_POWER") + # board.I2C(), board.SPI(), board.UART() and other named board buses from + # circuitpython.toml, constructed on demand on their pins. + board_buses = _toml_board_buses(mpconfigboard, board_names) + board_bus_names = { + name for tables in board_buses.values() for names, _ in tables for name in names + } + pin_object_of = {} # GPIO pad number -> pad: every (label, num) name and alias the # devicetree walk produced keys into one pin object per pad. pad_of_gpio = {} @@ -1249,6 +1302,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig status_neopixel = pin_object_name if status_neopixel_power and (ioport, num) == status_neopixel_power: status_neopixel_power = pin_object_name + pin_object_of[(ioport, num)] = pin_object_name pin_defs.append( f"const mcu_pin_obj_t pin_{pin_object_name} = {{ .base.type = &mcu_pin_type, .package_pin = {package_pin_init}}};" ) @@ -1324,8 +1378,11 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig zephyr_binding_headers.append(f'#include "shared-bindings/{module}/{driverclass}.h"') - # Designate a main device such as board.I2C or board.I2S. - if len(instances) == 1: + # Designate a main device such as board.I2C or board.I2S, unless + # circuitpython.toml defines the board buses of this class. + if BOARD_BUS_OF_DRIVER.get(driver) in board_buses: + pass + elif len(instances) == 1: instances[0].append(driverclass) else: # Check to see if a main device has already been designated @@ -1381,6 +1438,9 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig static MP_DEFINE_CONST_FUN_OBJ_0({function_object}, {c_function_name});""".lstrip() ) for label in labels: + if label.upper() in board_bus_names: + # A circuitpython.toml board bus has this name. + continue zephyr_binding_labels.append( f"{{ MP_ROM_QSTR(MP_QSTR_{label.upper()}), MP_ROM_PTR(&{function_object}) }}," ) @@ -1388,6 +1448,38 @@ 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) + # Board buses from circuitpython.toml: the CIRCUITPY_BOARD_* pin tables for + # shared-module/board, and board names for each bus. Bus 0 of a class is + # board__obj; later ones get their own singleton. + board_bus_defines = [] + board_bus_objects = [] + board_bus_labels = [] + for bus, tables in board_buses.items(): + entries = [] + for index, (names, pins) in enumerate(tables): + fields = ", ".join( + f".{field} = &pin_{pin_object_of[pin]}" for field, pin in pins.items() + ) + entries.append(f"{{{fields}}}") + if index == 0: + function_object = f"board_{bus}_obj" + else: + board_bus_objects.append( + f"CIRCUITPY_BOARD_BUS_SINGLETON({names[0].lower()}, {bus}, {index})" + ) + function_object = f"board_{names[0].lower()}_obj" + for name in names: + board_bus_labels.append( + f"{{ MP_ROM_QSTR(MP_QSTR_{name}), MP_ROM_PTR(&{function_object}) }}," + ) + board_bus_defines.append(f"#define CIRCUITPY_BOARD_{bus.upper()} ({len(tables)})") + board_bus_defines.append( + f"#define CIRCUITPY_BOARD_{bus.upper()}_PIN {{{', '.join(entries)}}}" + ) + board_bus_defines = "\n".join(board_bus_defines) + board_bus_objects = "\n".join(board_bus_objects) + board_bus_labels = "\n".join(board_bus_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; @@ -1682,6 +1774,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig {boot_button_active_high} {status_neopixel} {status_neopixel_power} +{board_bus_defines} """ if not header.exists() or header.read_text() != new_header_content: header.write_text(new_header_content) @@ -1731,6 +1824,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig {pin_defs} {zephyr_binding_objects} +{board_bus_objects} {iobroker_tables} {zephyr_display_object} {i2sout_reset_func} @@ -1748,6 +1842,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig {board_pin_mapping} {zephyr_binding_labels} +{board_bus_labels} }}; From 11d8f1d4cb4cf4736b4c87857f4e98c2185e2ec4 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Mon, 12 Oct 2026 20:45:02 -0400 Subject: [PATCH 4/9] zephyr-cp: allocate the fastest bus instance first iobroker hands out the first free instance in a pool, and the pools were in devicetree order, so busio.SPI() on the nRF52840 got the 8 MHz SPIM2 before the 32 MHz SPIM3. zephyr2cp.py now orders each pool by the instances' max-frequency, highest first, keeping devicetree order among equals. Co-Authored-By: Claude Opus 5.5 --- .../zephyr-cp/cptools/tests/test_zephyr2cp.py | 33 +++++++++++++++++++ ports/zephyr-cp/cptools/zephyr2cp.py | 24 +++++++++++--- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py index d69abc4f2a9..538f6923764 100644 --- a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py +++ b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py @@ -1,6 +1,7 @@ import sys import pathlib import tempfile +from types import SimpleNamespace # Add devicetree library to path portdir = pathlib.Path(__file__).parent.parent.parent @@ -23,6 +24,7 @@ BLOCKED_FLASH_COMPAT, MINIMUM_RAM_SIZE, add_toml_pin_names, + _fastest_first, _toml_board_buses, _toml_status_pin, ) @@ -865,3 +867,34 @@ def test_unknown_pin_raises(self): {"board_i2c": [{"names": ["I2C"], "scl": "NOPE", "sda": "SDA"}]}, self.BOARD_NAMES, ) + + +class TestFastestFirst: + """Ordering of bus instance pools by devicetree max-frequency.""" + + @staticmethod + def _node(name, max_frequency=None): + props = {} + if max_frequency is not None: + props["max-frequency"] = SimpleNamespace(to_num=lambda: max_frequency) + return SimpleNamespace(name=name, props=props) + + def test_highest_first(self): + nodes = [self._node("spi2", 8_000_000), self._node("spi3", 32_000_000)] + assert [n.name for n in _fastest_first(nodes)] == ["spi3", "spi2"] + + def test_stable_for_equal_and_missing(self): + nodes = [ + self._node("i2c1"), + self._node("spi22", 8_000_000), + self._node("i2c0"), + self._node("spi21", 8_000_000), + self._node("spi00", 32_000_000), + ] + assert [n.name for n in _fastest_first(nodes)] == [ + "spi00", + "spi22", + "spi21", + "i2c1", + "i2c0", + ] diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index f55b0582959..b9d481b7b88 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -525,6 +525,21 @@ def _reg_addr_expr(device_tree, label): return f"DT_REG_ADDR(DT_NODELABEL({label}))" +def _fastest_first(nodes): + """Order bus instance nodes by their ``max-frequency``, highest first. + + iobroker hands out the first free instance in a pool, so this gives + busio the fastest one first, such as the nRF52840's 32 MHz SPIM3. The + sort is stable: nodes with equal or no ``max-frequency`` keep their order. + """ + + def max_frequency(node): + prop = node.props.get("max-frequency") + return prop.to_num() if prop is not None else 0 + + return sorted(nodes, key=max_frequency, reverse=True) + + def _pinctrl_default_psels(node): """Return the raw nRF psel entries of a node's "default" pinctrl state. @@ -1552,8 +1567,9 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig instances = ( active_zephyr_devices.get(driver, []) if driver != "pwm" or pwm_nrfx else [] ) - for labels in instances: - node = device_tree.label2node[labels[0]] + nodes = _fastest_first(device_tree.label2node[labels[0]] for labels in instances) + for node in nodes: + label = node.labels[0] if node in path2chosen: # Console and other system devices are not allocatable. continue @@ -1561,9 +1577,9 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig 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)) + dynamic_entries.append((label, None, 0)) else: - fixed_entries.append((labels[0], psels, len(psels))) + fixed_entries.append((label, psels, len(psels))) entries = dynamic_entries + fixed_entries From fcb193973e582c5ea9dc91328f396ea24520313d Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Mon, 12 Oct 2026 20:45:07 -0400 Subject: [PATCH 5/9] zephyr-cp: document how nRF serial instances are shared and allocated Add an "nRF serial instances" section to the iobroker README, the counterpart of the comments in ports/nordic's nrfx_config.h: I2C, SPI and UART modes share an instance, the board overlay picks each instance's mode, the Feather split matches ports/nordic, and pools are allocated fastest first. Each nRF board overlay points to it. Co-Authored-By: Claude Opus 5.5 --- .../clue_nrf52840_zephyr/board.overlay | 3 ++ .../board.overlay | 3 ++ .../feather_nrf52840_zephyr/board.overlay | 3 ++ .../boards/nordic/nrf5340dk/board.overlay | 3 ++ .../boards/nordic/nrf54h20dk/board.overlay | 3 ++ .../boards/nordic/nrf54l15dk/board.overlay | 3 ++ .../boards/nordic/nrf54l15tag/board.overlay | 3 ++ .../boards/nordic/nrf54lm20dk/board.overlay | 3 ++ .../boards/nordic/nrf7002dk/board.overlay | 3 ++ .../internal-modules/iobroker/README.md | 31 +++++++++++++++++++ 10 files changed, 58 insertions(+) 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 d7a16eacb50..08e919bfcce 100644 --- a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay @@ -21,6 +21,9 @@ // constructed, and de-initializes it on release. (I2C0 is the fixed sensor // bus, SPI2 the fixed display bus, and QSPI the fixed external flash.) // +// See "nRF serial instances" in internal-modules/iobroker/README.md for how +// the instances are shared and allocated. +// // 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 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 ed906edb858..ac3edc35c66 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 @@ -22,6 +22,9 @@ // initializes and routes a device to arbitrary pins when a busio object is // constructed, and de-initializes it on release: I2C0, I2C1, SPI2, SPI3, // UART0 and UART1. +// +// See "nRF serial instances" in internal-modules/iobroker/README.md for how +// the instances are shared and allocated. &pinctrl { i2c0_dyn_default: i2c0_dyn_default { 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 dfdf0a4f184..b2bcdd5b827 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay @@ -22,6 +22,9 @@ // initializes and routes a device to arbitrary pins when a busio object is // constructed, and de-initializes it on release: I2C0, I2C1, SPI2, SPI3, // UART0 and UART1. +// +// See "nRF serial instances" in internal-modules/iobroker/README.md for how +// the instances are shared and allocated. &pinctrl { i2c0_dyn_default: i2c0_dyn_default { diff --git a/ports/zephyr-cp/boards/nordic/nrf5340dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf5340dk/board.overlay index 996a9e5aa42..50b2583f827 100644 --- a/ports/zephyr-cp/boards/nordic/nrf5340dk/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf5340dk/board.overlay @@ -29,6 +29,9 @@ i2s_rxtx: &i2s0 { // claimed at boot; iobroker initializes and routes a device when a busio // object is constructed and de-initializes it on release. // +// See "nRF serial instances" in internal-modules/iobroker/README.md for how +// the instances are shared and allocated. +// // 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. diff --git a/ports/zephyr-cp/boards/nordic/nrf54h20dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf54h20dk/board.overlay index 35d0af63aeb..f4c8dbcdacf 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54h20dk/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf54h20dk/board.overlay @@ -44,6 +44,9 @@ // nothing is claimed at boot; iobroker initializes and routes a device when a // busio object is constructed and de-initializes it on release. // +// See "nRF serial instances" in internal-modules/iobroker/README.md for how +// the instances are shared and allocated. +// // 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. diff --git a/ports/zephyr-cp/boards/nordic/nrf54l15dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf54l15dk/board.overlay index cf0ff6756cc..03d399f05b3 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54l15dk/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf54l15dk/board.overlay @@ -8,6 +8,9 @@ // claimed at boot; iobroker initializes and routes a device when a busio // object is constructed and de-initializes it on release. // +// See "nRF serial instances" in internal-modules/iobroker/README.md for how +// the instances are shared and allocated. +// // 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). diff --git a/ports/zephyr-cp/boards/nordic/nrf54l15tag/board.overlay b/ports/zephyr-cp/boards/nordic/nrf54l15tag/board.overlay index 6dd08bfce59..627a28177ce 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54l15tag/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf54l15tag/board.overlay @@ -46,6 +46,9 @@ // claimed at boot; iobroker initializes and routes a device when a busio // object is constructed and de-initializes it on release. // +// See "nRF serial instances" in internal-modules/iobroker/README.md for how +// the instances are shared and allocated. +// // 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 diff --git a/ports/zephyr-cp/boards/nordic/nrf54lm20dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf54lm20dk/board.overlay index e8ffce9a459..e07eac751f5 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54lm20dk/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf54lm20dk/board.overlay @@ -14,6 +14,9 @@ // claimed at boot; iobroker initializes and routes a device when a busio // object is constructed and de-initializes it on release. // +// See "nRF serial instances" in internal-modules/iobroker/README.md for how +// the instances are shared and allocated. +// // 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. diff --git a/ports/zephyr-cp/boards/nordic/nrf7002dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf7002dk/board.overlay index 0103b74e9e6..eeb4e248ecf 100644 --- a/ports/zephyr-cp/boards/nordic/nrf7002dk/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf7002dk/board.overlay @@ -6,6 +6,9 @@ // claimed at boot; iobroker initializes and routes a device when a busio // object is constructed and de-initializes it on release. // +// See "nRF serial instances" in internal-modules/iobroker/README.md for how +// the instances are shared and allocated. +// // 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 diff --git a/ports/zephyr-cp/internal-modules/iobroker/README.md b/ports/zephyr-cp/internal-modules/iobroker/README.md index 6ddacaaee7e..6435dc43373 100644 --- a/ports/zephyr-cp/internal-modules/iobroker/README.md +++ b/ports/zephyr-cp/internal-modules/iobroker/README.md @@ -136,6 +136,37 @@ 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. +## nRF serial instances + +On nRF SoCs, the I2C and SPI nodes with the same instance number are one +peripheral, and on nRF53, nRF54L and nRF91 so is the UART node: on the +nRF52840, `i2c1` and `spi1` are both the peripheral at 0x40004000. Only one +mode of an instance can be enabled, which Zephyr checks at build time +(`zephyr/soc/nordic/validate_enabled_instances.c`). + +So the board overlay decides how many instances of each kind there are. It +enables each free instance in one mode, with all signals disconnected and +`zephyr,deferred-init`, and leaves alone the instances the board uses for a +fixed purpose, such as the console or external flash. The overlay's +comment says what it chose and why. + +The Feather nRF52840 boards use the same split as `ports/nordic`, whose +`nrfx_config.h` defaults to `CIRCUITPY_NRF_NUM_I2C` = 2: + +| Kind | Instances | +|---|---| +| I2C | `i2c0`, `i2c1` (TWIM) | +| SPI | `spi2`, `spi3` (SPIM) | +| UART | `uart0`, `uart1` (UARTE, separate peripherals on nRF52) | + +`spi0` and `spi1` stay disabled because I2C has those instances. + +`zephyr2cp.py` lists each pool's instances by devicetree `max-frequency`, +highest first, and iobroker allocates the first free instance that can +reach the requested pins. So busio gets the nRF52840's 32 MHz SPIM3 before +the 8 MHz SPIM2, and on the nRF54L the 32 MHz `spi00` before the 8 MHz +instances, when a board leaves `spi00` dynamic. + ## Public API See `include/iobroker/iobroker.h`. Signals are requested with From 13cd72c881f2a5401d48c717b888ef296fa4a67d Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Tue, 13 Oct 2026 19:20:52 -0400 Subject: [PATCH 6/9] zephyr-cp: share the Feather nRF52840 boards' overlays and console files The Feather nRF52840 and Feather Bluefruit Sense had identical console.conf and console.overlay files, and board.overlay files that differed only in deleting the Sense's SHT3XD node. console.conf is now one port-level file, which the Makefile applies in CONSOLE=1 builds of any board that has a console.overlay. The Sense's console.overlay and board.overlay include the Feather's; its board.overlay then only deletes SHT3XD. The compiled devicetree is unchanged. Co-Authored-By: Claude Opus 5.5 --- ports/zephyr-cp/Makefile | 9 +- ports/zephyr-cp/README.md | 7 +- .../board.overlay | 293 +----------------- .../console.conf | 4 - .../console.overlay | 20 +- .../feather_nrf52840_zephyr/console.conf | 4 - ports/zephyr-cp/console.conf | 5 + 7 files changed, 20 insertions(+), 322 deletions(-) delete mode 100644 ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.conf delete mode 100644 ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/console.conf create mode 100644 ports/zephyr-cp/console.conf diff --git a/ports/zephyr-cp/Makefile b/ports/zephyr-cp/Makefile index f889f5eea90..adb1d658ee6 100644 --- a/ports/zephyr-cp/Makefile +++ b/ports/zephyr-cp/Makefile @@ -63,13 +63,16 @@ DEBUG_CONF_FILE ?= $(CURDIR)/debug.conf ifeq ($(DEBUG),1) CP_BOARD_CONF := $(strip $(CP_BOARD_CONF) $(DEBUG_CONF_FILE)) endif -# When CONSOLE=1 (implied by DEBUG=1), apply the board's console.conf and console.overlay if it -# has them. Boards whose console pins are also user pins enable the Zephyr UART console only then. +# When CONSOLE=1 (implied by DEBUG=1), apply the board's console.overlay, if it has one, and the +# port's console.conf. Boards whose console pins are also user pins enable the Zephyr UART console +# only then. CONSOLE ?= $(DEBUG) CP_BOARD_CONSOLE_OVERLAY := ifeq ($(CONSOLE),1) -CP_BOARD_CONF := $(strip $(CP_BOARD_CONF) $(wildcard $(CP_BOARD_DIR)/console.conf)) CP_BOARD_CONSOLE_OVERLAY := $(wildcard $(CP_BOARD_DIR)/console.overlay) +ifneq ($(CP_BOARD_CONSOLE_OVERLAY),) +CP_BOARD_CONF := $(strip $(CP_BOARD_CONF) $(CURDIR)/console.conf) +endif endif CP_BOARD_CONF := $(subst $(eval) ,;,$(CP_BOARD_CONF)) # Always pass the conf and console overlay files, even if empty. This undoes DEBUG=1 or CONSOLE=1 diff --git a/ports/zephyr-cp/README.md b/ports/zephyr-cp/README.md index f3ba5e63adb..4285bf3c98c 100644 --- a/ports/zephyr-cp/README.md +++ b/ports/zephyr-cp/README.md @@ -29,9 +29,10 @@ This uses Zephyr's cmake to generate Makefiles that then delegate to `tools/cpbuild/build_circuitpython.py` to build the CircuitPython bits in parallel. `DEBUG=1` adds the debug Kconfig settings in `debug.conf`. `CONSOLE=1` (implied -by `DEBUG=1`) applies a board's `console.conf` and `console.overlay`, if it has -them. Boards whose console UART pins are also user pins, such as the Feather -nRF52840, have the Zephyr UART console only in those builds. +by `DEBUG=1`) applies a board's `console.overlay`, if it has one, together with +the port's `console.conf`, which turns the Zephyr UART console back on. Boards +whose console UART pins are also user pins, such as the Feather nRF52840, have +the Zephyr UART console only in those builds. ## Native simulator build container 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 ac3edc35c66..72163bd5a2c 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 @@ -1,294 +1,7 @@ -/* Partition layout is owned by the mcuboot fork - * (dts/adafruit/adafruit_feather_nrf52840.dtsi) and applied to this image by - * sysbuild. This overlay only adds board/app config. */ -/* No Zephyr console by default, so TX/RX are free for busio.UART. A CONSOLE=1 - * build puts the console back on UART0 (console.overlay). */ -/ { - chosen { - /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; - }; -}; - -&zephyr_udc0 { - /delete-node/ board_cdc_acm_uart; -}; - -// 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: I2C0, I2C1, SPI2, SPI3, -// UART0 and UART1. -// -// See "nRF serial instances" in internal-modules/iobroker/README.md for how -// the instances are shared and allocated. - -&pinctrl { - i2c0_dyn_default: i2c0_dyn_default { - group1 { - psels = , - ; - }; - }; - - i2c0_dyn_sleep: i2c0_dyn_sleep { - group1 { - psels = , - ; - }; - }; - - i2c1_dyn_default: i2c1_dyn_default { - group1 { - psels = , - ; - }; - }; - - i2c1_dyn_sleep: i2c1_dyn_sleep { - group1 { - psels = , - ; - }; - }; - - pwm0_dyn_default: pwm0_dyn_default { - group1 { - psels = , - , - , - ; - }; - }; - - pwm0_dyn_sleep: pwm0_dyn_sleep { - group1 { - psels = , - , - , - ; - }; - }; - - pwm1_dyn_default: pwm1_dyn_default { - group1 { - psels = , - , - , - ; - }; - }; - - pwm1_dyn_sleep: pwm1_dyn_sleep { - group1 { - psels = , - , - , - ; - }; - }; - - pwm2_dyn_default: pwm2_dyn_default { - group1 { - psels = , - , - , - ; - }; - }; - - pwm2_dyn_sleep: pwm2_dyn_sleep { - group1 { - psels = , - , - , - ; - }; - }; - - pwm3_dyn_default: pwm3_dyn_default { - group1 { - psels = , - , - , - ; - }; - }; - - pwm3_dyn_sleep: pwm3_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 = , - , - ; - }; - }; - - uart0_dyn_default: uart0_dyn_default { - group1 { - psels = , - ; - }; - }; - - uart0_dyn_sleep: uart0_dyn_sleep { - group1 { - psels = , - ; - }; - }; - - uart1_dyn_default: uart1_dyn_default { - group1 { - psels = , - ; - }; - }; - - uart1_dyn_sleep: uart1_dyn_sleep { - group1 { - psels = , - ; - }; - }; -}; - -// The upstream board fixes I2C0 to SCL/SDA with the legacy TWI driver. Use -// TWIM like I2C1 so iobroker can route it. -&i2c0 { - compatible = "nordic,nrf-twim"; - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&i2c0_dyn_default>; - pinctrl-1 = <&i2c0_dyn_sleep>; - pinctrl-names = "default", "sleep"; -}; +/* Same as the Feather nRF52840 Express, whose upstream board is the same + * apart from the LED pin and the SHT3XD node below. */ +#include "../feather_nrf52840_zephyr/board.overlay" // CircuitPython drives the SHT31 from Python, so drop the Zephyr sensor node // on the now dynamic I2C0. /delete-node/ &SHT3XD; - -&i2c1 { - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&i2c1_dyn_default>; - pinctrl-1 = <&i2c1_dyn_sleep>; - pinctrl-names = "default", "sleep"; -}; - -// PWM instances, same scheme: all four outputs disconnected and deferred -// init, so iobroker can hand an instance to neopixel_write and pwmio and -// route its outputs at runtime. - -&pwm0 { - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&pwm0_dyn_default>; - pinctrl-1 = <&pwm0_dyn_sleep>; - pinctrl-names = "default", "sleep"; -}; - -&pwm1 { - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&pwm1_dyn_default>; - pinctrl-1 = <&pwm1_dyn_sleep>; - pinctrl-names = "default", "sleep"; -}; - -&pwm2 { - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&pwm2_dyn_default>; - pinctrl-1 = <&pwm2_dyn_sleep>; - pinctrl-names = "default", "sleep"; -}; - -&pwm3 { - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&pwm3_dyn_default>; - pinctrl-1 = <&pwm3_dyn_sleep>; - 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; - 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"; -}; - -&uart0 { - status = "okay"; - zephyr,deferred-init; - current-speed = <115200>; - pinctrl-0 = <&uart0_dyn_default>; - pinctrl-1 = <&uart0_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"; -}; - -#include "../../../app.overlay" - -/* Enable the SAADC for analogio (AIN0..AIN7 are on P0.02..P0.05 and P0.28..P0.31). */ -&adc { - status = "okay"; -}; diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.conf b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.conf deleted file mode 100644 index 83db0acb170..00000000000 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.conf +++ /dev/null @@ -1,4 +0,0 @@ -# CONSOLE=1 and DEBUG=1 builds: the Zephyr UART console on TX/RX -# (console.overlay). -CONFIG_UART_CONSOLE=y -CONFIG_LOG_BACKEND_UART=y diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.overlay b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.overlay index 095c4aa1d2d..03f4f3d1c3b 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.overlay +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.overlay @@ -1,18 +1,2 @@ -/* Applied after board.overlay in CONSOLE=1 and DEBUG=1 builds: the Zephyr - * console on UART0, fixed to the TX/RX pins. TX/RX are then not available to - * busio.UART. */ -/ { - chosen { - zephyr,console = &uart0; - zephyr,shell-uart = &uart0; - zephyr,uart-mcumgr = &uart0; - zephyr,bt-mon-uart = &uart0; - zephyr,bt-c2h-uart = &uart0; - }; -}; - -&uart0 { - /delete-property/ zephyr,deferred-init; - pinctrl-0 = <&uart0_default>; - pinctrl-1 = <&uart0_sleep>; -}; +/* Same console setup as the Feather nRF52840 Express. */ +#include "../feather_nrf52840_zephyr/console.overlay" diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/console.conf b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/console.conf deleted file mode 100644 index 83db0acb170..00000000000 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/console.conf +++ /dev/null @@ -1,4 +0,0 @@ -# CONSOLE=1 and DEBUG=1 builds: the Zephyr UART console on TX/RX -# (console.overlay). -CONFIG_UART_CONSOLE=y -CONFIG_LOG_BACKEND_UART=y diff --git a/ports/zephyr-cp/console.conf b/ports/zephyr-cp/console.conf new file mode 100644 index 00000000000..c2f90e7e25b --- /dev/null +++ b/ports/zephyr-cp/console.conf @@ -0,0 +1,5 @@ +# CONSOLE=1 and DEBUG=1 builds of boards that have a console.overlay: turn the +# Zephyr UART console back on, on the UART that the board's console.overlay +# sets up. +CONFIG_UART_CONSOLE=y +CONFIG_LOG_BACKEND_UART=y From 18a2b4817440188556d7598507495b356988fa13 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 14 Oct 2026 14:36:55 -0400 Subject: [PATCH 7/9] zephyr-cp: find board buses from devicetree labels Replace the [[board_i2c]], [[board_spi]] and [[board_uart]] tables in circuitpython.toml with the devicetree as the source of truth. A board whose devicetree has a feather_i2c, feather_spi or feather_serial label gets board.I2C(), board.SPI() or board.UART() on the header pins named SCL/SDA, SCK/MOSI/MISO or TX/RX. The labeled node only marks that the board has the bus; iobroker picks the instance when the bus is constructed. The Feather nRF52840 boards' circuitpython.toml files are back to their main versions, and the generated board buses are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../circuitpython.toml | 18 --- .../circuitpython.toml | 18 --- .../zephyr-cp/cptools/tests/test_zephyr2cp.py | 78 ++++------- ports/zephyr-cp/cptools/zephyr2cp.py | 123 ++++++++---------- 4 files changed, 80 insertions(+), 157 deletions(-) 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 ecea8e87174..985f53ca662 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 @@ -54,21 +54,3 @@ MICROPHONE_CLOCK = 18 MICROPHONE_DATA = 17 PROXIMITY_LIGHT_INTERRUPT = 47 ACCELEROMETER_GYRO_INTERRUPT = 4 - -# Board buses, as on the ports/ counterpart: board.I2C(), board.SPI() and -# board.UART(), constructed on these pins when first used. -[[board_i2c]] -names = ["I2C"] -scl = "SCL" -sda = "SDA" - -[[board_spi]] -names = ["SPI"] -clock = "SCK" -mosi = "MOSI" -miso = "MISO" - -[[board_uart]] -names = ["UART"] -tx = "TX" -rx = "RX" 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 db6d04ed545..ef4dc115a83 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/circuitpython.toml +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/circuitpython.toml @@ -52,21 +52,3 @@ LED = 8 RED_LED = 8 D3 = 8 BLUE_LED = 3 - -# Board buses, as on the ports/ counterpart: board.I2C(), board.SPI() and -# board.UART(), constructed on these pins when first used. -[[board_i2c]] -names = ["I2C"] -scl = "SCL" -sda = "SDA" - -[[board_spi]] -names = ["SPI"] -clock = "SCK" -mosi = "MOSI" -miso = "MISO" - -[[board_uart]] -names = ["UART"] -tx = "TX" -rx = "RX" diff --git a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py index 538f6923764..76dacee0a3f 100644 --- a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py +++ b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py @@ -25,7 +25,7 @@ MINIMUM_RAM_SIZE, add_toml_pin_names, _fastest_first, - _toml_board_buses, + _dt_board_buses, _toml_status_pin, ) @@ -801,72 +801,50 @@ def test_unknown_name_raises(self): _toml_status_pin({"STATUS_NEOPIXEL": "NOPE"}, self.BOARD_NAMES, "STATUS_NEOPIXEL") -class TestTomlBoardBuses: - """Resolution of circuitpython.toml [[board_i2c]]/[[board_spi]]/[[board_uart]].""" +class TestDtBoardBuses: + """board.I2C(), board.SPI() and board.UART() from devicetree bus labels.""" BOARD_NAMES = { ("gpio0", 11): ["SCL"], ("gpio0", 12): ["SDA"], ("gpio0", 14): ["SCK"], ("gpio0", 13): ["MOSI"], + ("gpio0", 15): ["MISO"], ("gpio0", 25): ["TX"], ("gpio0", 24): ["RX"], - ("gpio1", 1): ["SCL1"], - ("gpio1", 2): ["SDA1"], } - def test_resolves_buses_in_order(self): - buses = _toml_board_buses( - { - "board_i2c": [ - {"names": ["I2C", "stemma_i2c"], "scl": "SCL", "sda": "SDA"}, - {"names": ["I2C1"], "scl": "SCL1", "sda": "SDA1"}, - ], - "board_uart": [{"names": ["UART"], "tx": "TX", "rx": "RX"}], - }, - self.BOARD_NAMES, + @staticmethod + def _device_tree(*labels): + return SimpleNamespace(label2node={label: object() for label in labels}) + + def test_feather_labels(self): + buses = _dt_board_buses( + self._device_tree("feather_i2c", "feather_spi", "feather_serial"), self.BOARD_NAMES ) assert buses == { - "i2c": [ - (["I2C", "STEMMA_I2C"], {"scl": ("gpio0", 11), "sda": ("gpio0", 12)}), - (["I2C1"], {"scl": ("gpio1", 1), "sda": ("gpio1", 2)}), - ], - "uart": [(["UART"], {"tx": ("gpio0", 25), "rx": ("gpio0", 24)})], + "i2c": {"scl": ("gpio0", 11), "sda": ("gpio0", 12)}, + "spi": {"clock": ("gpio0", 14), "mosi": ("gpio0", 13), "miso": ("gpio0", 15)}, + "uart": {"tx": ("gpio0", 25), "rx": ("gpio0", 24)}, } - def test_optional_pins_may_be_absent(self): - buses = _toml_board_buses( - {"board_spi": [{"names": ["SPI"], "clock": "SCK", "mosi": "MOSI"}]}, - self.BOARD_NAMES, - ) - assert buses == {"spi": [(["SPI"], {"clock": ("gpio0", 14), "mosi": ("gpio0", 13)})]} + def test_only_labeled_buses(self): + buses = _dt_board_buses(self._device_tree("feather_i2c", "i2c0"), self.BOARD_NAMES) + assert list(buses) == ["i2c"] - def test_absent_tables_are_empty(self): - assert _toml_board_buses({}, self.BOARD_NAMES) == {} - assert _toml_board_buses(None, self.BOARD_NAMES) == {} + def test_no_labels(self): + assert _dt_board_buses(self._device_tree("i2c0", "spi1"), self.BOARD_NAMES) == {} - def test_missing_names_raises(self): - with pytest.raises(RuntimeError, match=r"\[\[board_i2c\]\] needs names"): - _toml_board_buses({"board_i2c": [{"scl": "SCL", "sda": "SDA"}]}, self.BOARD_NAMES) + def test_pin_names_ignore_case(self): + names = dict(self.BOARD_NAMES) + names[("gpio0", 11)] = ["scl"] + buses = _dt_board_buses(self._device_tree("feather_i2c"), names) + assert buses["i2c"]["scl"] == ("gpio0", 11) - def test_missing_required_pin_raises(self): - with pytest.raises(RuntimeError, match=r"\[\[board_i2c\]\] needs sda"): - _toml_board_buses({"board_i2c": [{"names": ["I2C"], "scl": "SCL"}]}, self.BOARD_NAMES) - - def test_no_pins_raises(self): - with pytest.raises(RuntimeError, match="needs at least one pin"): - _toml_board_buses({"board_uart": [{"names": ["UART"]}]}, self.BOARD_NAMES) - - def test_unknown_key_raises(self): - with pytest.raises(RuntimeError, match=r"unknown keys \['sck'\]"): - _toml_board_buses({"board_spi": [{"names": ["SPI"], "sck": "SCK"}]}, self.BOARD_NAMES) - - def test_unknown_pin_raises(self): - with pytest.raises(RuntimeError, match="'NOPE' is not a board pin name"): - _toml_board_buses( - {"board_i2c": [{"names": ["I2C"], "scl": "NOPE", "sda": "SDA"}]}, - self.BOARD_NAMES, - ) + def test_missing_pin_name_raises(self): + names = {key: value for key, value in self.BOARD_NAMES.items() if value != ["MISO"]} + with pytest.raises(RuntimeError, match="feather_spi needs a board pin named MISO"): + _dt_board_buses(self._device_tree("feather_spi"), names) class TestFastestFirst: diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index ef9bafeeb2c..630293053f2 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -760,49 +760,51 @@ def _toml_status_pin(mpconfigboard, board_names, key): raise RuntimeError(f"circuitpython.toml {key} = {name!r} is not a board pin name") -# Pin fields of the circuitpython.toml [[board_i2c]], [[board_spi]] and -# [[board_uart]] tables, as (field, required). The field names match the -# CIRCUITPY_BOARD_*_PIN structs in shared-module/board. -BOARD_BUS_PINS = { - "i2c": (("scl", True), ("sda", True)), - "spi": (("clock", True), ("mosi", False), ("miso", False)), - "uart": (("tx", False), ("rx", False)), +# Devicetree labels that mark a board's standard header buses, mapped to the +# bus class and the board pin names its signals use, as (field, pin name). +# The field names match the CIRCUITPY_BOARD_*_PIN structs in +# shared-module/board. The pin names come from the board's connector (see +# CONNECTORS) or its [pins] table. +BOARD_BUS_LABELS = { + "feather_i2c": ("i2c", (("scl", "SCL"), ("sda", "SDA"))), + "feather_spi": ("spi", (("clock", "SCK"), ("mosi", "MOSI"), ("miso", "MISO"))), + "feather_serial": ("uart", (("tx", "TX"), ("rx", "RX"))), } # The board bus class each busio-capable devicetree driver provides. BOARD_BUS_OF_DRIVER = {"i2c": "i2c", "spi": "spi", "serial": "uart"} -def _toml_board_buses(mpconfigboard, board_names): - """Resolve the circuitpython.toml board bus tables to pins. +def _board_pin_named(board_names, name): + """Return the ``(ioport, num)`` of the board pin called ``name``, or None.""" + for pin_key, names in board_names.items(): + if any(existing.upper() == name for existing in names): + return pin_key + return None + - Each ``[[board_i2c]]``, ``[[board_spi]]`` or ``[[board_uart]]`` table is - one board bus, in instance order: ``names`` lists its board names and the - other keys name its pins. Returns ``{bus: [(names, {field: pin}), ...]}`` - for the bus classes the toml defines. +def _dt_board_buses(device_tree, board_names): + """Find the board buses that devicetree labels mark. + + Each BOARD_BUS_LABELS label in the devicetree gives one board bus, named + after its class (``board.I2C``, ``board.SPI`` or ``board.UART``), on the + board pins with the label's pin names. The labeled node only says that + the board has the bus. It may be dynamic or disabled, because iobroker + picks the instance when the bus is constructed. Returns + ``{bus: {field: pin}}``. """ buses = {} - for bus, fields in BOARD_BUS_PINS.items(): - key = f"board_{bus}" - resolved = [] - for table in (mpconfigboard or {}).get(key, []): - field_names = {field for field, _ in fields} - unknown = sorted(set(table) - field_names - {"names"}) - if unknown: - raise RuntimeError(f"circuitpython.toml [[{key}]] has unknown keys {unknown}") - names = table.get("names") - if not names: - raise RuntimeError(f"circuitpython.toml [[{key}]] needs names") - pins = {} - for field, required in fields: - if field in table: - pins[field] = _toml_status_pin(table, board_names, field) - elif required: - raise RuntimeError(f"circuitpython.toml [[{key}]] needs {field}") - if not pins: - raise RuntimeError(f"circuitpython.toml [[{key}]] needs at least one pin") - resolved.append(([str(name).upper() for name in names], pins)) - if resolved: - buses[bus] = resolved + for label, (bus, fields) in BOARD_BUS_LABELS.items(): + if label not in device_tree.label2node: + continue + if bus in buses: + raise RuntimeError(f"devicetree label {label} is a second {bus} board bus") + pins = {} + for field, name in fields: + pin = _board_pin_named(board_names, name) + if pin is None: + raise RuntimeError(f"devicetree label {label} needs a board pin named {name}") + pins[field] = pin + buses[bus] = pins return buses @@ -1258,12 +1260,10 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig # the [pins] table, that the status LED code drives with neopixel_write. status_neopixel = _toml_status_pin(mpconfigboard, board_names, "STATUS_NEOPIXEL") status_neopixel_power = _toml_status_pin(mpconfigboard, board_names, "STATUS_NEOPIXEL_POWER") - # board.I2C(), board.SPI(), board.UART() and other named board buses from - # circuitpython.toml, constructed on demand on their pins. - board_buses = _toml_board_buses(mpconfigboard, board_names) - board_bus_names = { - name for tables in board_buses.values() for names, _ in tables for name in names - } + # board.I2C(), board.SPI() and board.UART() for the buses that devicetree + # labels mark, constructed on demand on their pins. + board_buses = _dt_board_buses(device_tree, board_names) + board_bus_names = {bus.upper() for bus in board_buses} pin_object_of = {} # GPIO pad number -> pad: every (label, num) name and alias the # devicetree walk produced keys into one pin object per pad. @@ -1400,8 +1400,8 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig zephyr_binding_headers.append(f'#include "shared-bindings/{module}/{driverclass}.h"') - # Designate a main device such as board.I2C or board.I2S, unless - # circuitpython.toml defines the board buses of this class. + # Designate a main device such as board.I2C or board.I2S, unless a + # devicetree label marks a board bus of this class. if BOARD_BUS_OF_DRIVER.get(driver) in board_buses: pass elif len(instances) == 1: @@ -1461,7 +1461,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig ) for label in labels: if label.upper() in board_bus_names: - # A circuitpython.toml board bus has this name. + # A labeled board bus has this name. continue zephyr_binding_labels.append( f"{{ MP_ROM_QSTR(MP_QSTR_{label.upper()}), MP_ROM_PTR(&{function_object}) }}," @@ -1470,36 +1470,18 @@ 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) - # Board buses from circuitpython.toml: the CIRCUITPY_BOARD_* pin tables for - # shared-module/board, and board names for each bus. Bus 0 of a class is - # board__obj; later ones get their own singleton. + # Board buses from devicetree labels: the CIRCUITPY_BOARD_* pin tables for + # shared-module/board, and the board name for each bus. board_bus_defines = [] - board_bus_objects = [] board_bus_labels = [] - for bus, tables in board_buses.items(): - entries = [] - for index, (names, pins) in enumerate(tables): - fields = ", ".join( - f".{field} = &pin_{pin_object_of[pin]}" for field, pin in pins.items() - ) - entries.append(f"{{{fields}}}") - if index == 0: - function_object = f"board_{bus}_obj" - else: - board_bus_objects.append( - f"CIRCUITPY_BOARD_BUS_SINGLETON({names[0].lower()}, {bus}, {index})" - ) - function_object = f"board_{names[0].lower()}_obj" - for name in names: - board_bus_labels.append( - f"{{ MP_ROM_QSTR(MP_QSTR_{name}), MP_ROM_PTR(&{function_object}) }}," - ) - board_bus_defines.append(f"#define CIRCUITPY_BOARD_{bus.upper()} ({len(tables)})") - board_bus_defines.append( - f"#define CIRCUITPY_BOARD_{bus.upper()}_PIN {{{', '.join(entries)}}}" + for bus, pins in board_buses.items(): + fields = ", ".join(f".{field} = &pin_{pin_object_of[pin]}" for field, pin in pins.items()) + board_bus_defines.append(f"#define CIRCUITPY_BOARD_{bus.upper()} (1)") + board_bus_defines.append(f"#define CIRCUITPY_BOARD_{bus.upper()}_PIN {{{{{fields}}}}}") + board_bus_labels.append( + f"{{ MP_ROM_QSTR(MP_QSTR_{bus.upper()}), MP_ROM_PTR(&board_{bus}_obj) }}," ) board_bus_defines = "\n".join(board_bus_defines) - board_bus_objects = "\n".join(board_bus_objects) board_bus_labels = "\n".join(board_bus_labels) # Generate tables of allocatable bus instances for the iobroker @@ -1870,7 +1852,6 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig {pin_defs} {zephyr_binding_objects} -{board_bus_objects} {iobroker_tables} void board_init(void) {{ From 09f6a81f85799c714e5264b0f8758e39b012d0e4 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 14 Oct 2026 14:58:21 -0400 Subject: [PATCH 8/9] zephyr-cp: use bus labels for board buses only with nRF routing Constructing a board bus on its pins needs iobroker's runtime routing, which only nRF SoCs have. On other SoCs, such as the Feather RP2040, the labeled buses would raise NotImplementedError. Find board buses from devicetree labels only when CONFIG_PINCTRL_NRF is on, and leave other boards as before. The .config scan moves earlier so the board bus code can use it. Co-Authored-By: Claude Opus 5.5 --- ports/zephyr-cp/cptools/zephyr2cp.py | 47 ++++++++++++++-------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index 630293053f2..981bfd5ffa1 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -1260,9 +1260,31 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig # the [pins] table, that the status LED code drives with neopixel_write. status_neopixel = _toml_status_pin(mpconfigboard, board_names, "STATUS_NEOPIXEL") status_neopixel_power = _toml_status_pin(mpconfigboard, board_names, "STATUS_NEOPIXEL_POWER") + pinctrl_nrf = False + pwm_nrfx = False + pwm_emul = False + if config_present: + for line in config.read_text().splitlines(): + if line.startswith("CONFIG_PINCTRL_NRF="): + pinctrl_nrf = line.strip().endswith("=y") + elif line.startswith("CONFIG_PWM_NRFX="): + # Zephyr's nRF PWM driver defines the PWM instance devices. + # Without it, PWM nodes have no device to reference, and + # neopixel_write (the neopixel module's nRF implementation, + # which allocates an instance through iobroker) cannot transmit, so + # both the pwm pool and the module follow this symbol. pwmio + # allocates its channels through iobroker's nRF implementation + # too. + pwm_nrfx = line.strip().endswith("=y") + elif line.startswith("CONFIG_PWM_ADAFRUIT_EMUL="): + # The emulated PWM controllers on native_sim (pwm_emul + # module), which iobroker allocates pwmio channels on too. + pwm_emul = line.strip().endswith("=y") + board_info["neopixel_write"] = pwm_nrfx # board.I2C(), board.SPI() and board.UART() for the buses that devicetree - # labels mark, constructed on demand on their pins. - board_buses = _dt_board_buses(device_tree, board_names) + # labels mark, constructed on demand on their pins. That needs iobroker + # routing (nRF); other SoCs keep their fixed devicetree buses. + board_buses = _dt_board_buses(device_tree, board_names) if pinctrl_nrf else {} board_bus_names = {bus.upper() for bus in board_buses} pin_object_of = {} # GPIO pad number -> pad: every (label, num) name and alias the @@ -1489,27 +1511,6 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig # 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 - pwm_nrfx = False - pwm_emul = False - if config_present: - for line in config.read_text().splitlines(): - if line.startswith("CONFIG_PINCTRL_NRF="): - pinctrl_nrf = line.strip().endswith("=y") - elif line.startswith("CONFIG_PWM_NRFX="): - # Zephyr's nRF PWM driver defines the PWM instance devices. - # Without it, PWM nodes have no device to reference, and - # neopixel_write (the neopixel module's nRF implementation, - # which allocates an instance through iobroker) cannot transmit, so - # both the pwm pool and the module follow this symbol. pwmio - # allocates its channels through iobroker's nRF implementation - # too. - pwm_nrfx = line.strip().endswith("=y") - elif line.startswith("CONFIG_PWM_ADAFRUIT_EMUL="): - # The emulated PWM controllers on native_sim (pwm_emul - # module), which iobroker allocates pwmio channels on too. - pwm_emul = line.strip().endswith("=y") - board_info["neopixel_write"] = pwm_nrfx board_info["pwmio"] = pwm_nrfx or pwm_emul iobroker_includes = """ From 6e572934dbc58d57138a21265c562d028cf88595 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 14 Oct 2026 18:03:46 -0400 Subject: [PATCH 9/9] zephyr-cp: mark board buses with generic board_* labels BOARD_BUS_LABELS now knows only board_i2c, board_spi and board_serial. Boards add them in their board.overlay: the Feather nRF52840 overlay aliases the upstream feather_* connector labels, and boards without upstream labels (ItsyBitsy, CLUE) can label the bus nodes directly. Co-Authored-By: Claude Opus 5.5 --- .../adafruit/feather_nrf52840_zephyr/board.overlay | 6 ++++++ ports/zephyr-cp/cptools/tests/test_zephyr2cp.py | 14 ++++++++------ ports/zephyr-cp/cptools/zephyr2cp.py | 10 +++++++--- 3 files changed, 21 insertions(+), 9 deletions(-) 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 b2bcdd5b827..517074cdb7b 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/board.overlay @@ -17,6 +17,12 @@ /delete-node/ board_cdc_acm_uart; }; +// board.I2C(), board.SPI() and board.UART() are on the Feather header buses. +// See BOARD_BUS_LABELS in cptools/zephyr2cp.py. +board_i2c: &feather_i2c {}; +board_spi: &feather_spi {}; +board_serial: &feather_serial {}; + // 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 diff --git a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py index 76dacee0a3f..136a1b75443 100644 --- a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py +++ b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py @@ -818,9 +818,9 @@ class TestDtBoardBuses: def _device_tree(*labels): return SimpleNamespace(label2node={label: object() for label in labels}) - def test_feather_labels(self): + def test_board_labels(self): buses = _dt_board_buses( - self._device_tree("feather_i2c", "feather_spi", "feather_serial"), self.BOARD_NAMES + self._device_tree("board_i2c", "board_spi", "board_serial"), self.BOARD_NAMES ) assert buses == { "i2c": {"scl": ("gpio0", 11), "sda": ("gpio0", 12)}, @@ -829,7 +829,9 @@ def test_feather_labels(self): } def test_only_labeled_buses(self): - buses = _dt_board_buses(self._device_tree("feather_i2c", "i2c0"), self.BOARD_NAMES) + buses = _dt_board_buses( + self._device_tree("board_i2c", "feather_spi", "i2c0"), self.BOARD_NAMES + ) assert list(buses) == ["i2c"] def test_no_labels(self): @@ -838,13 +840,13 @@ def test_no_labels(self): def test_pin_names_ignore_case(self): names = dict(self.BOARD_NAMES) names[("gpio0", 11)] = ["scl"] - buses = _dt_board_buses(self._device_tree("feather_i2c"), names) + buses = _dt_board_buses(self._device_tree("board_i2c"), names) assert buses["i2c"]["scl"] == ("gpio0", 11) def test_missing_pin_name_raises(self): names = {key: value for key, value in self.BOARD_NAMES.items() if value != ["MISO"]} - with pytest.raises(RuntimeError, match="feather_spi needs a board pin named MISO"): - _dt_board_buses(self._device_tree("feather_spi"), names) + with pytest.raises(RuntimeError, match="board_spi needs a board pin named MISO"): + _dt_board_buses(self._device_tree("board_spi"), names) class TestFastestFirst: diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index 981bfd5ffa1..c9fa84c4307 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -765,10 +765,14 @@ def _toml_status_pin(mpconfigboard, board_names, key): # The field names match the CIRCUITPY_BOARD_*_PIN structs in # shared-module/board. The pin names come from the board's connector (see # CONNECTORS) or its [pins] table. +# +# These labels are ours, not upstream's. Each board adds them in its +# board.overlay, as an alias of an upstream connector label when there is +# one (board_i2c: &feather_i2c {};) or on the bus node otherwise. BOARD_BUS_LABELS = { - "feather_i2c": ("i2c", (("scl", "SCL"), ("sda", "SDA"))), - "feather_spi": ("spi", (("clock", "SCK"), ("mosi", "MOSI"), ("miso", "MISO"))), - "feather_serial": ("uart", (("tx", "TX"), ("rx", "RX"))), + "board_i2c": ("i2c", (("scl", "SCL"), ("sda", "SDA"))), + "board_spi": ("spi", (("clock", "SCK"), ("mosi", "MOSI"), ("miso", "MISO"))), + "board_serial": ("uart", (("tx", "TX"), ("rx", "RX"))), } # The board bus class each busio-capable devicetree driver provides. BOARD_BUS_OF_DRIVER = {"i2c": "i2c", "spi": "spi", "serial": "uart"}