diff --git a/.github/workflows/build-board-custom.yml b/.github/workflows/build-board-custom.yml index caeb16cfddf..2f8c0f68d8c 100644 --- a/.github/workflows/build-board-custom.yml +++ b/.github/workflows/build-board-custom.yml @@ -72,7 +72,7 @@ jobs: - name: Set up python uses: actions/setup-python@v6 with: - python-version: 3.x + python-version: "3.14" # minify_html has no Python 3.15 wheel yet - name: Board to port id: board-to-port run: | diff --git a/.github/workflows/build-boards.yml b/.github/workflows/build-boards.yml index 016a2f2f32e..0610103faa4 100644 --- a/.github/workflows/build-boards.yml +++ b/.github/workflows/build-boards.yml @@ -39,7 +39,7 @@ jobs: - name: Set up python uses: actions/setup-python@v6 with: - python-version: ${{ inputs.port == 'zephyr-cp' && '3.13' || '3.x' }} + python-version: ${{ inputs.port == 'zephyr-cp' && '3.13' || '3.14' }} # minify_html has no Python 3.15 wheel yet - name: Set up port id: set-up-port diff --git a/.github/workflows/build-mpy-cross.yml b/.github/workflows/build-mpy-cross.yml index dedf9154b8d..8a09d5b0897 100644 --- a/.github/workflows/build-mpy-cross.yml +++ b/.github/workflows/build-mpy-cross.yml @@ -37,7 +37,7 @@ jobs: - name: Set up python uses: actions/setup-python@v6 with: - python-version: 3.x + python-version: "3.14" # minify_html has no Python 3.15 wheel yet - name: Set up submodules uses: ./.github/actions/deps/submodules with: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0f57a74243..4f53ed308b4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,7 +42,7 @@ jobs: - name: Set up python uses: actions/setup-python@v6 with: - python-version: 3.x + python-version: "3.14" # minify_html has no Python 3.15 wheel yet - name: Duplicate USB VID/PID check run: python3 -u -m tools.ci_check_duplicate_usb_vid_pid - name: Set up submodules @@ -135,7 +135,7 @@ jobs: - name: Set up python uses: actions/setup-python@v6 with: - python-version: 3.x + python-version: "3.14" # minify_html has no Python 3.15 wheel yet - name: Set up submodules uses: ./.github/actions/deps/submodules - name: Versions @@ -177,7 +177,7 @@ jobs: - name: Set up python uses: actions/setup-python@v6 with: - python-version: 3.x + python-version: "3.14" # minify_html has no Python 3.15 wheel yet - name: Set up submodules uses: ./.github/actions/deps/submodules with: diff --git a/.github/workflows/create-website-pr.yml b/.github/workflows/create-website-pr.yml index 559a41e67e7..25e4943d549 100644 --- a/.github/workflows/create-website-pr.yml +++ b/.github/workflows/create-website-pr.yml @@ -26,7 +26,7 @@ jobs: - name: Set up python uses: actions/setup-python@v6 with: - python-version: 3.x + python-version: "3.14" # minify_html has no Python 3.15 wheel yet - name: Set up submodules uses: ./.github/actions/deps/submodules with: diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 46961aca41f..b0dd8798774 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -30,7 +30,7 @@ jobs: - name: Set up python uses: actions/setup-python@v6 with: - python-version: 3.x + python-version: "3.14" # minify_html has no Python 3.15 wheel yet - name: Set up submodules uses: ./.github/actions/deps/submodules - name: Set up external diff --git a/ports/zephyr-cp/Makefile b/ports/zephyr-cp/Makefile index 8e134ca66fa..adb1d658ee6 100644 --- a/ports/zephyr-cp/Makefile +++ b/ports/zephyr-cp/Makefile @@ -61,16 +61,25 @@ 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.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_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 -# 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 42b9e63885e..ca7d045358a 100644 --- a/ports/zephyr-cp/README.md +++ b/ports/zephyr-cp/README.md @@ -28,6 +28,12 @@ make BOARD=nordic_nrf5340dk 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.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 Building the native sim requires `libsdl2-dev:i386` and other 32bit dependencies that 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..e8e0189ce2b 100644 --- a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay @@ -1,6 +1,8 @@ /* Partition layout is owned by the mcuboot fork * (dts/adafruit/adafruit_clue_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; @@ -15,11 +17,15 @@ /delete-node/ board_cdc_acm_uart; }; -// UART0, I2C1 and SPI3 are enabled with all pins disconnected and marked -// zephyr,deferred-init so nothing is claimed at boot: the iobroker +// UART0, I2C0, 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.) +// constructed, and de-initializes it on release. (SPI2 is the fixed display +// bus and QSPI the fixed external flash. The onboard sensors are on SCL/SDA +// but have no devicetree nodes, so I2C0 does not need to be fixed.) +// +// 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. @@ -41,6 +47,20 @@ }; }; + i2c0_dyn_default: i2c0_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c0_dyn_sleep: i2c0_dyn_sleep { + group1 { + psels = , + ; + }; + }; + i2c1_dyn_default: i2c1_dyn_default { group1 { psels = , @@ -144,7 +164,11 @@ }; }; +// The upstream board uses the legacy UART driver for UART0, which can't be +// de-initialized, so iobroker could not route it again once it was released. +// Instead, use the UARTE driver, which can be deinited. &uart0 { + compatible = "nordic,nrf-uarte"; status = "okay"; zephyr,deferred-init; current-speed = <115200>; @@ -153,6 +177,17 @@ pinctrl-names = "default", "sleep"; }; +// 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"; +}; + &i2c1 { status = "okay"; zephyr,deferred-init; diff --git a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/console.overlay b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/console.overlay new file mode 100644 index 00000000000..095c4aa1d2d --- /dev/null +++ b/ports/zephyr-cp/boards/adafruit/clue_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>; +}; 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..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,244 +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. */ -/ { - chosen { - zephyr,console = &uart0; - zephyr,shell-uart = &uart0; - zephyr,uart-mcumgr = &uart0; - zephyr,bt-mon-uart = &uart0; - zephyr,bt-c2h-uart = &uart0; - }; -}; +/* 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" -&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: 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 = , - ; - }; - }; - - 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 = , - , - , - ; - }; - }; -}; - -&i2c1 { - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&i2c1_dyn_default>; - pinctrl-1 = <&i2c1_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"; -}; - -&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"; -}; - -// 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"; -}; - -#include "../../../app.overlay" - -/* Enable the SAADC for analogio (AIN0..AIN7 are on P0.02..P0.05 and P0.28..P0.31). */ -&adc { - status = "okay"; -}; +// CircuitPython drives the SHT31 from Python, so drop the Zephyr sensor node +// on the now dynamic I2C0. +/delete-node/ &SHT3XD; 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..03f4f3d1c3b --- /dev/null +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/console.overlay @@ -0,0 +1,2 @@ +/* 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/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..a26e02917e2 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,69 +20,38 @@ // 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.) +// 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 { - i2c1_dyn_default: i2c1_dyn_default { + i2c0_dyn_default: i2c0_dyn_default { group1 { psels = , ; }; }; - i2c1_dyn_sleep: i2c1_dyn_sleep { + i2c0_dyn_sleep: i2c0_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 { + i2c1_dyn_default: i2c1_dyn_default { group1 { - psels = , - ; + psels = , + ; }; }; - uart1_dyn_sleep: uart1_dyn_sleep { + i2c1_dyn_sleep: i2c1_dyn_sleep { group1 { - psels = , - ; + psels = , + ; }; }; @@ -155,43 +126,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"; }; @@ -231,8 +243,47 @@ 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"; +}; + +// The upstream board uses the legacy UART driver for UART0, which can't be +// de-initialized, so iobroker could not route it again once it was released. +// Instead, use the UARTE driver, which can be deinited, like UART1. &uart0 { - status = "okay"; + compatible = "nordic,nrf-uarte"; + 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" 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>; +}; diff --git a/ports/zephyr-cp/boards/native/native_sim/board.overlay b/ports/zephyr-cp/boards/native/native_sim/board.overlay index 2985a004670..3d4776350dd 100644 --- a/ports/zephyr-cp/boards/native/native_sim/board.overlay +++ b/ports/zephyr-cp/boards/native/native_sim/board.overlay @@ -36,6 +36,12 @@ }; }; +/* A second PTY UART, so there is a UART that isn't the console (uart0): + * board.UART() and the busio.UART tests use it. */ +&uart1 { + status = "okay"; +}; + /* Expand ADC channels for analogio tests. */ &adc0 { nchannels = <4>; diff --git a/ports/zephyr-cp/boards/native/native_sim_lfs/board.overlay b/ports/zephyr-cp/boards/native/native_sim_lfs/board.overlay index 66b9de603ff..a40d65d84f3 100644 --- a/ports/zephyr-cp/boards/native/native_sim_lfs/board.overlay +++ b/ports/zephyr-cp/boards/native/native_sim_lfs/board.overlay @@ -67,6 +67,12 @@ }; }; +/* A second PTY UART, so there is a UART that isn't the console (uart0): + * board.UART() and the busio.UART tests use it. */ +&uart1 { + status = "okay"; +}; + /* Expand ADC channels for analogio tests. */ &adc0 { nchannels = <4>; diff --git a/ports/zephyr-cp/boards/nordic/nrf5340dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf5340dk/board.overlay index 996a9e5aa42..775763d7523 100644 --- a/ports/zephyr-cp/boards/nordic/nrf5340dk/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf5340dk/board.overlay @@ -1,25 +1,6 @@ -&pinctrl { - i2s0_default_alt: i2s0_default_alt { - group1 { - psels = , - , - , - , - ; - }; - }; -}; - -&clock { - hfclkaudio-frequency = <11289600>; -}; - -i2s_rxtx: &i2s0 { - status = "okay"; - pinctrl-0 = <&i2s0_default_alt>; - pinctrl-names = "default"; - clock-source = "ACLK"; -}; +// TODO: restore the I2S for the Open Music Labs Audio Codec Shield. The fixed +// i2s0 that was here used the Arduino SPI pins (D10-D13), so board.SPI() could +// not use them, and audiobusio.I2SOut can't be routed dynamically yet. // Enable free serial peripheral instances so CircuitPython can mux them to // arbitrary pins at runtime. Each instance is enabled in one mode because the @@ -29,11 +10,46 @@ 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. +// same peripheral, so they must not be enabled. Instance 1 is i2c1, the +// Arduino header I2C. spi4 is the upstream Arduino header SPI and the only +// SPI instance. uart1 is the upstream Arduino serial, but it is the same +// instance as i2c1, so busio.UART uses uart3. &pinctrl { + i2c1_dyn_default: i2c1_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c1_dyn_sleep: i2c1_dyn_sleep { + group1 { + psels = , + ; + }; + }; + + spi4_dyn_default: spi4_dyn_default { + group1 { + psels = , + , + ; + }; + }; + + spi4_dyn_sleep: spi4_dyn_sleep { + group1 { + psels = , + , + ; + }; + }; + i2c2_dyn_default: i2c2_dyn_default { group1 { psels = , @@ -63,6 +79,14 @@ i2s_rxtx: &i2s0 { }; }; +&i2c1 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&i2c1_dyn_default>; + pinctrl-1 = <&i2c1_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + &i2c2 { status = "okay"; zephyr,deferred-init; @@ -71,6 +95,14 @@ i2s_rxtx: &i2s0 { pinctrl-names = "default", "sleep"; }; +&spi4 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&spi4_dyn_default>; + pinctrl-1 = <&spi4_dyn_sleep>; + pinctrl-names = "default", "sleep"; +}; + &uart3 { status = "okay"; zephyr,deferred-init; @@ -80,6 +112,14 @@ i2s_rxtx: &i2s0 { pinctrl-names = "default", "sleep"; }; +// The upstream board forwards the Arduino D0/D1 pins (P1.00/P1.01) and +// P0.10/P0.11 to the network core for its UART console, so the application +// core could not use them. Keep them on the application core, for +// board.UART() on TX/RX (D1/D0). +&gpio_fwd { + status = "disabled"; +}; + // PWM instances 1-3, 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. Instance 0 is left as the board defines it: diff --git a/ports/zephyr-cp/boards/nordic/nrf54h20dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf54h20dk/board.overlay index 35d0af63aeb..6a2395bfc7b 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54h20dk/board.overlay +++ b/ports/zephyr-cp/boards/nordic/nrf54h20dk/board.overlay @@ -1,32 +1,10 @@ -/* - * SDA = P2.11 - * SCL = P2.10 - */ - -&pinctrl { - i2c130_default: i2c130_default { - group1 { - psels = , - ; - }; - }; - - i2c130_sleep: i2c130_sleep { - group1 { - psels = , - ; - low-power-enable; - }; - }; -}; +// board.I2C() is on SCL/SDA (P2.10/P2.11), the pins that circuitpython.toml +// names SCL and SDA. -&i2c130 { - clock-frequency = ; - pinctrl-0 = <&i2c130_default>; - pinctrl-1 = <&i2c130_sleep>; - pinctrl-names = "default", "sleep"; - zephyr,concat-buf-size = <256>; - memory-regions = <&cpuapp_dma_region>; +// Enable P2's GPIO controller so its pads get pin objects. The SoC dtsi +// leaves every port disabled and the board enables only the LED and button +// ports. +&gpio2 { status = "okay"; }; @@ -44,13 +22,31 @@ // 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, +// See "nRF serial instances" in internal-modules/iobroker/README.md for how +// the instances are shared and allocated. +// +// Instance 130 is i2c130, which the upstream board fixes to P2.10/P2.11. +// 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 { + i2c130_dyn_default: i2c130_dyn_default { + group1 { + psels = , + ; + }; + }; + + i2c130_dyn_sleep: i2c130_dyn_sleep { + group1 { + psels = , + ; + }; + }; + i2c131_dyn_default: i2c131_dyn_default { group1 { psels = , @@ -96,6 +92,16 @@ }; }; +&i2c130 { + status = "okay"; + zephyr,deferred-init; + pinctrl-0 = <&i2c130_dyn_default>; + pinctrl-1 = <&i2c130_dyn_sleep>; + pinctrl-names = "default", "sleep"; + zephyr,concat-buf-size = <256>; + memory-regions = <&cpuapp_dma_region>; +}; + &i2c131 { status = "okay"; zephyr,deferred-init; diff --git a/ports/zephyr-cp/boards/nordic/nrf54h20dk/circuitpython.toml b/ports/zephyr-cp/boards/nordic/nrf54h20dk/circuitpython.toml index 15d36f2576f..7b8d0483c9c 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54h20dk/circuitpython.toml +++ b/ports/zephyr-cp/boards/nordic/nrf54h20dk/circuitpython.toml @@ -1,2 +1,9 @@ CIRCUITPY_BUILD_EXTENSIONS = ["hex", "uf2"] DISABLED_MODULES=["jpegio", "gifio", "tilepalettemapper"] + +# No package map for this SoC yet, so pins are global GPIO numbers +# (port * 32 + pin). +[pins] +# board.I2C() pins: P2.10/P2.11. +SCL = 74 +SDA = 75 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/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 diff --git a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py index 556aec0cccb..eeb0d3f998e 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,7 +24,10 @@ BLOCKED_FLASH_COMPAT, MINIMUM_RAM_SIZE, add_toml_pin_names, + _fastest_first, + _standard_bus_pins, _toml_status_pin, + _used_by_zephyr, ) @@ -796,3 +800,132 @@ 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 TestStandardBusPins: + """The standard pins of board.I2C(), board.SPI() and board.UART().""" + + BOARD_NAMES = { + ("gpio0", 11): ["SCL"], + ("gpio0", 12): ["SDA"], + ("gpio0", 14): ["SCK"], + ("gpio0", 13): ["MOSI"], + ("gpio0", 15): ["MISO"], + ("gpio0", 25): ["TX"], + ("gpio0", 24): ["RX"], + } + + def test_all_standard_names(self): + assert _standard_bus_pins(self.BOARD_NAMES) == { + "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_missing_name_leaves_out_bus(self): + names = {key: value for key, value in self.BOARD_NAMES.items() if value != ["MISO"]} + assert list(_standard_bus_pins(names)) == ["i2c", "uart"] + + def test_no_standard_names(self): + assert _standard_bus_pins({("gpio0", 2): ["D2"]}) == {} + + def test_pin_names_ignore_case(self): + names = dict(self.BOARD_NAMES) + names[("gpio0", 11)] = ["scl"] + assert _standard_bus_pins(names)["i2c"]["scl"] == ("gpio0", 11) + + def test_connector_aliases(self): + # Arduino header pins carry both their header name and the standard name. + names = { + ("gpio1", 0): ["D0", "RX"], + ("gpio1", 1): ["D1", "TX"], + ("gpio1", 2): ["D14", "SDA"], + ("gpio1", 3): ["D15", "SCL"], + } + assert _standard_bus_pins(names) == { + "i2c": {"scl": ("gpio1", 3), "sda": ("gpio1", 2)}, + "uart": {"tx": ("gpio1", 1), "rx": ("gpio1", 0)}, + } + + +class TestUsedByZephyr: + """Fixed bus instances that Zephyr itself uses.""" + + DTS = """ +/dts-v1/; +/ { + chosen { + zephyr,console = &uart0; + }; + uart0: uart@0 { + }; + uart1: uart@1 { + }; + spi0: spi@0 { + flash@0 { + reg = <0>; + }; + }; + spi1: spi@1 { + }; + spi2: spi@2 { + }; + display { + spi-dev = <&spi1>; + }; + disabled_display { + status = "disabled"; + spi-dev = <&spi2>; + }; +}; +""" + + def _used(self, label): + dt = parse_dts_string(self.DTS) + return _used_by_zephyr(dt, dt.label2node[label]) + + def test_chosen_console(self): + assert self._used("uart0") + + def test_child_device(self): + assert self._used("spi0") + + def test_referenced_by_phandle(self): + assert self._used("spi1") + + def test_referenced_only_by_disabled_node(self): + assert not self._used("spi2") + + def test_unused(self): + assert not self._used("uart1") + + +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 891c5f47284..a9289342986 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -73,6 +73,9 @@ "SCL", "SDA", ], + # The standard bus pin names are those on Arduino Uno R3 boards: TX/RX on + # D1/D0, SPI on D11-D13 (and the 2x3 SPI header), and SDA/SCL on the two + # pins above AREF, which this binding calls D14 and D15. "arduino-header-r3": [ "A0", "A1", @@ -80,8 +83,8 @@ "A3", "A4", "A5", - "D0", - "D1", + ["D0", "RX"], + ["D1", "TX"], "D2", "D3", "D4", @@ -91,11 +94,11 @@ "D8", "D9", "D10", - "D11", - "D12", - "D13", - "D14", - "D15", + ["D11", "MOSI"], + ["D12", "MISO"], + ["D13", "SCK"], + ["D14", "SDA"], + ["D15", "SCL"], ], "adafruit-feather-header": [ "A0", @@ -525,6 +528,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. @@ -745,6 +763,94 @@ def _toml_status_pin(mpconfigboard, board_names, key): raise RuntimeError(f"circuitpython.toml {key} = {name!r} is not a board pin name") +# The standard pins of each board bus. For example: +# +# "i2c [bus kind]": {"scl [pin field]": "SCL [pin name]", "sda [pin field]": "SDA [pin name]"}, +# +# - bus kind: board.I2C(), board.SPI() or board.UART(). The generated pin table +# for shared-module/board is named after it (CIRCUITPY_BOARD_I2C_PIN). +# - pin field: The corresponding name of the field in the struct holding the +# pins for the bus in shared-module/board/__init__.c. For instance, the `scl` +# field in the `board_i2c_pin_t` struct. +# - pin name: the standard board pin name whose pin goes in that field. A +# board's connector (see CONNECTORS) or its circuitpython.toml [pins] table +# gives its header pins these names. +BOARD_BUS_PINS = { + "i2c": {"scl": "SCL", "sda": "SDA"}, + "spi": {"clock": "SCK", "mosi": "MOSI", "miso": "MISO"}, + "uart": {"tx": "TX", "rx": "RX"}, +} +# The board bus kind for each busio driver kind. For example: +# +# "serial [driver kind]": "uart [bus kind]", +# +# - driver kind: a BUSIO_CLASSES key, the kind of devicetree device that a +# busio class uses ("serial" for busio.UART, after Zephyr's serial drivers). +# - bus kind: a BOARD_BUS_PINS key, the board bus that such devices provide +# ("uart" for board.UART()). +BOARD_BUS_OF_DRIVER = {"i2c": "i2c", "spi": "spi", "serial": "uart"} + + +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 + + +def _standard_bus_pins(board_names): + """Find the standard pins of each board bus class. + + A class has standard pins when the board names every pin it needs with + the standard names in BOARD_BUS_PINS. Returns ``{bus: {field: pin}}``, + with ``pin`` an ``(ioport, num)`` key of ``board_names``. + """ + standard_pins = {} + for bus, fields in BOARD_BUS_PINS.items(): + pins = {field: _board_pin_named(board_names, name) for field, name in fields.items()} + if None not in pins.values(): + standard_pins[bus] = pins + return standard_pins + + +def _node_enabled(node): + """Return True if a devicetree node's status is "okay" (or unset).""" + status = node.props.get("status") + return status is None or status.to_string() == "okay" + + +def _used_by_zephyr(device_tree, node): + """Return True if Zephyr itself uses a bus node. + + A bus is Zephyr's when a /chosen property points at it (such as the + zephyr,console UART), it has child device nodes (such as a flash chip), + or another enabled node refers to it by phandle (such as a display's + mipi-dbi node). + """ + for prop in device_tree.root.nodes["chosen"].props.values(): + try: + if prop.to_path() is node: + return True + except dtlib.DTError: + pass + if node.nodes: + return True + for other in device_tree.node_iter(): + if other is node or not _node_enabled(other): + continue + for prop in other.props.values(): + if prop.type == dtlib.Type.PHANDLE: + referenced = [prop.to_node()] + elif prop.type == dtlib.Type.PHANDLES: + referenced = prop.to_nodes() + else: + continue + if node in referenced: + return True + return False + + @cpbuild.run_in_thread def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfigboard=None): # noqa: C901 board_dir = builddir / "board" @@ -1197,6 +1303,29 @@ 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 + standard_bus_pins = _standard_bus_pins(board_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 = {} @@ -1256,6 +1385,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}}};" ) @@ -1314,6 +1444,50 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig ): iobroker_labels.add(labels[0]) + # The zephyr,console and zephyr,shell-uart UARTs belong to Zephyr, so they + # are never board objects. + chosen_props = device_tree.root.nodes["chosen"].props + console_nodes = [ + chosen_props[name].to_path() + for name in ("zephyr,console", "zephyr,shell-uart") + if name in chosen_props + ] + + # A board either routes its I2C, SPI and UART buses with iobroker (nRF + # boards with routable instances) or has only fixed buses from the + # devicetree. + # + # - With iobroker, board.I2C(), board.SPI() and board.UART() are buses on + # the board's standard pins, and iobroker picks the instance when the + # bus is constructed. Fixed bus instances are never board objects, and + # only Zephyr may use them (see _used_by_zephyr()). + # - With only fixed buses, the board objects are fixed instances (below). + uses_iobroker = pinctrl_nrf and any( + labels[0] in iobroker_labels + for driver in BOARD_BUS_OF_DRIVER + for labels in active_zephyr_devices.get(driver, []) + ) + board_buses = {} + if uses_iobroker: + for driver, bus in BOARD_BUS_OF_DRIVER.items(): + instances = active_zephyr_devices.get(driver, []) + for labels in instances: + node = device_tree.label2node[labels[0]] + if labels[0] not in iobroker_labels and not _used_by_zephyr(device_tree, node): + message = ( + f"devicetree node {labels[0]} is a fixed {driver} instance, but the " + "board routes its buses with iobroker: make it dynamic (all pins " + "disconnected, zephyr,deferred-init) or disable it" + ) + # The build's TaskGroup reports only that a task failed, + # so log the reason too. + logger.error(message) + raise RuntimeError(message) + if bus in standard_bus_pins and any( + labels[0] in iobroker_labels for labels in instances + ): + board_buses[bus] = standard_bus_pins[bus] + zephyr_binding_headers = [] zephyr_binding_objects = [] zephyr_binding_labels = [] @@ -1331,51 +1505,52 @@ 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: - instances[0].append(driverclass) + # A board has at most one object per class, under the class's + # standard name (board.I2C, board.I2SOUT, ...). Other fixed instances + # are not board objects. + bus = BOARD_BUS_OF_DRIVER.get(driver) + candidates = [ + labels + for labels in instances + if labels[0] not in iobroker_labels + and device_tree.label2node[labels[0]] not in console_nodes + ] + main = None + if uses_iobroker and bus is not None: + # The board bus, if any, has the standard name. + pass + elif len(candidates) == 1: + main = candidates[0] else: - # Check to see if a main device has already been designated - found_main = False - for labels in instances: - for label in labels: - if label == driverclass: - found_main = True - if not found_main: - for priority_label in (f"zephyr_{driver}", f"arduino_{driver}"): - for labels in instances: - if priority_label in labels: - labels.append(driverclass) - found_main = True - break - 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" - function_object = f"{c_function_name}_obj" - obj_type = f"{module}_{driverclass.lower()}" - - # Handle special cases for different drivers - if driver == "serial": - # UART needs a receiver buffer - buffer_decl = f"static byte {instance_name}_buffer[128];" - construct_call = f"common_hal_busio_uart_construct_from_device(&{instance_name}_obj, DEVICE_DT_GET(DT_NODELABEL({labels[0]})), 128, {instance_name}_buffer, false)" - else: - # Default case (I2C, SPI, I2S) - buffer_decl = "" - construct_call = f"common_hal_{module}_{driverclass.lower()}_construct_from_device(&{instance_name}_obj, DEVICE_DT_GET(DT_NODELABEL({labels[0]})))" + # A label picks the main instance. + for priority_label in (driverclass, f"zephyr_{driver}", f"arduino_{driver}"): + main = next((labels for labels in candidates if priority_label in labels), None) + if main is not None: + break + if main is None: + continue + + instance_name = f"{driver.replace('/', '_')}_{main[0]}" + c_function_name = f"_{instance_name}" + singleton_ptr = f"{c_function_name}_singleton" + function_object = f"{c_function_name}_obj" + obj_type = f"{module}_{driverclass.lower()}" + + # Handle special cases for different drivers + if driver == "serial": + # UART needs a receiver buffer + buffer_decl = f"static byte {instance_name}_buffer[128];" + construct_call = f"common_hal_busio_uart_construct_from_device(&{instance_name}_obj, DEVICE_DT_GET(DT_NODELABEL({main[0]})), 128, {instance_name}_buffer, false)" + else: + # Default case (I2C, SPI, I2S) + buffer_decl = "" + construct_call = f"common_hal_{module}_{driverclass.lower()}_construct_from_device(&{instance_name}_obj, DEVICE_DT_GET(DT_NODELABEL({main[0]})))" - if driver == "i2s": - i2sout_instance_names.append(instance_name) + if driver == "i2s": + i2sout_instance_names.append(instance_name) - zephyr_binding_objects.append( - f"""{buffer_decl} + zephyr_binding_objects.append( + f"""{buffer_decl} static {obj_type}_obj_t {instance_name}_obj; static mp_obj_t {singleton_ptr} = mp_const_none; static mp_obj_t {c_function_name}(void) {{ @@ -1386,41 +1561,33 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig return {singleton_ptr}; }} static MP_DEFINE_CONST_FUN_OBJ_0({function_object}, {c_function_name});""".lstrip() - ) - for label in labels: - zephyr_binding_labels.append( - f"{{ MP_ROM_QSTR(MP_QSTR_{label.upper()}), MP_ROM_PTR(&{function_object}) }}," - ) + ) + zephyr_binding_labels.append( + f"{{ MP_ROM_QSTR(MP_QSTR_{driverclass.upper()}), MP_ROM_PTR(&{function_object}) }}," + ) zephyr_binding_headers = "\n".join(zephyr_binding_headers) zephyr_binding_objects = "\n".join(zephyr_binding_objects) zephyr_binding_labels = "\n".join(zephyr_binding_labels) + # Board buses on the standard pins: the CIRCUITPY_BOARD_* pin tables for + # shared-module/board, and the board name for each bus. + board_bus_defines = [] + board_bus_labels = [] + 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_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; # 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 = """ @@ -1467,8 +1634,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 @@ -1476,9 +1644,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 @@ -1578,17 +1746,16 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig 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( - f"common_hal_audiobusio_i2sout_stop(&{name}_obj);" for name in i2sout_instance_names - ) - i2sout_reset_func = f""" + # Generate i2sout_reset() that stops the board's I2SOut objects. It is + # generated even when there are none, because reset_port() calls it on + # every board with audiobusio.I2SOut. + stop_calls = "\n ".join( + f"common_hal_audiobusio_i2sout_stop(&{name}_obj);" for name in i2sout_instance_names + ) + i2sout_reset_func = f""" void i2sout_reset(void) {{ {stop_calls} }}""" - else: - i2sout_reset_func = "" zephyr_display_header = "" zephyr_display_init = "" @@ -1700,6 +1867,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) @@ -1784,6 +1952,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig {board_pin_mapping} {zephyr_binding_labels} +{board_bus_labels} }}; 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 diff --git a/ports/zephyr-cp/tests/__init__.py b/ports/zephyr-cp/tests/__init__.py index 78b9a17a676..5357f20467d 100644 --- a/ports/zephyr-cp/tests/__init__.py +++ b/ports/zephyr-cp/tests/__init__.py @@ -148,6 +148,12 @@ def __init__(self, cmd, timeout=5, trace_file=None, env=None, flash_file=None): if "connected to pseudotty:" not in uart_pty_line: raise RuntimeError("Failed to connect to UART") pty_path = uart_pty_line.strip().rsplit(":", maxsplit=1)[1].strip() + # The console UART's device name ("uart"). Other PTY UARTs announce + # their pseudotty in the same way, so reconnect_serial() looks for this + # name's line only. + self._console_pty_marker = ( + uart_pty_line.split("connected to pseudotty:")[0].strip() + " connected to pseudotty:" + ) self.serial = SerialSaver( serial.Serial(pty_path, baudrate=115200, timeout=0.05, write_timeout=0), name="uart0", @@ -169,13 +175,17 @@ def reconnect_serial(self, timeout=30.0): """ import time - marker = "connected to pseudotty:" + marker = self._console_pty_marker deadline = time.monotonic() + timeout while time.monotonic() < deadline: if self._proc.poll() is not None: return False out = self.debug_serial.all_output + # Only a line that starts with the console's name: the marker must + # not match the end of another UART's line. idx = out.find(marker, self._pty_search_offset) + while idx > 0 and out[idx - 1] not in "\r\n": + idx = out.find(marker, idx + 1) newline = out.find("\n", idx) if idx >= 0 else -1 if idx >= 0 and newline >= 0: line = out[idx:newline] diff --git a/ports/zephyr-cp/tests/test_audiobusio.py b/ports/zephyr-cp/tests/test_audiobusio.py index 9d03e976bc9..67bfd61f69d 100644 --- a/ports/zephyr-cp/tests/test_audiobusio.py +++ b/ports/zephyr-cp/tests/test_audiobusio.py @@ -31,7 +31,7 @@ channel_count=2, ) -dac = board.I2S0() +dac = board.I2SOUT() print("playing") dac.play(sample, loop=True) time.sleep(0.5) @@ -134,7 +134,7 @@ def test_i2s_play_and_stop(circuitpython): channel_count=2, ) -dac = board.I2S0() +dac = board.I2SOUT() dac.play(sample, loop=True) print("playing") time.sleep(0.2) @@ -183,7 +183,7 @@ def test_i2s_stops_on_code_exit(circuitpython): channel_count=2, ) -dac = board.I2S0() +dac = board.I2SOUT() dac.play(sample, loop=True) print("playing") time.sleep(0.2) @@ -243,7 +243,7 @@ def test_i2s_pause_resume(circuitpython): channel_count=2, ) -dac = board.I2S0() +dac = board.I2SOUT() print("playing") dac.play(sample, loop=False) time.sleep(0.5) diff --git a/ports/zephyr-cp/tests/test_busio_uart.py b/ports/zephyr-cp/tests/test_busio_uart.py index 9d5a044e3ba..b8a9cc32756 100644 --- a/ports/zephyr-cp/tests/test_busio_uart.py +++ b/ports/zephyr-cp/tests/test_busio_uart.py @@ -1,14 +1,14 @@ # SPDX-FileCopyrightText: 2026 Mikey Sklar for Adafruit Industries # SPDX-License-Identifier: MIT -"""busio.UART on native_sim, through the console UART.""" +"""busio.UART on native_sim, through board.UART() on the second PTY UART.""" import pytest TIMEOUT_CODE = """\ import board -uart = board.UART0() +uart = board.UART() for t in (0.5, 2, 0): uart.timeout = t print("timeout", uart.timeout)