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 9513273dbf7..c6afd95e75a 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..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.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..517074cdb7b 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; }; }; @@ -15,72 +17,47 @@ /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 -// 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 +132,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 +249,43 @@ 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"; + 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/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/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..136a1b75443 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,8 @@ BLOCKED_FLASH_COMPAT, MINIMUM_RAM_SIZE, add_toml_pin_names, + _fastest_first, + _dt_board_buses, _toml_status_pin, ) @@ -796,3 +799,82 @@ 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 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"], + } + + @staticmethod + def _device_tree(*labels): + return SimpleNamespace(label2node={label: object() for label in labels}) + + def test_board_labels(self): + buses = _dt_board_buses( + self._device_tree("board_i2c", "board_spi", "board_serial"), self.BOARD_NAMES + ) + assert buses == { + "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_only_labeled_buses(self): + buses = _dt_board_buses( + self._device_tree("board_i2c", "feather_spi", "i2c0"), self.BOARD_NAMES + ) + assert list(buses) == ["i2c"] + + def test_no_labels(self): + assert _dt_board_buses(self._device_tree("i2c0", "spi1"), 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("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="board_spi needs a board pin named MISO"): + _dt_board_buses(self._device_tree("board_spi"), 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 ca1dceddfba..c9fa84c4307 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. @@ -745,6 +760,58 @@ def _toml_status_pin(mpconfigboard, board_names, key): raise RuntimeError(f"circuitpython.toml {key} = {name!r} is not a board pin name") +# 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. +# +# 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 = { + "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"} + + +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 _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 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 + + @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 +1264,33 @@ 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. 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 # devicetree walk produced keys into one pin object per pad. pad_of_gpio = {} @@ -1256,6 +1350,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}}};" ) @@ -1331,8 +1426,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 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: instances[0].append(driverclass) else: # Check to see if a main device has already been designated @@ -1388,6 +1486,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 labeled board bus has this name. + continue zephyr_binding_labels.append( f"{{ MP_ROM_QSTR(MP_QSTR_{label.upper()}), MP_ROM_PTR(&{function_object}) }}," ) @@ -1395,32 +1496,25 @@ 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 devicetree labels: 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 +1561,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 +1571,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 @@ -1700,6 +1795,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 +1880,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