Skip to content

zephyr-cp: dynamic buses on the Feather nRF52840 boards - #11576

Open
dhalbert wants to merge 9 commits into
adafruit:mainfrom
dhalbert:zephyr-cp-optional-console
Open

dhalbert wants to merge 9 commits into
adafruit:mainfrom
dhalbert:zephyr-cp-optional-console

Conversation

@dhalbert

@dhalbert dhalbert commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Code and tests by Claude.

UART console on TX/RX

Problem: zephyr-cp's overlays for the Feather nRF52840 and Feather Bluefruit Sense put the Zephyr console on UART0, fixed to TX/RX, so busio.UART() could not use those pins. (The upstream uf2 variants of these boards use USB CDC for the console, but CircuitPython takes over USB itself.)

Fix: UART0 is now a dynamic iobroker instance. The console is now a build option: CONSOLE=1 (implied by DEBUG=1) applies the board's console.overlay and the port's console.conf, which put it back on TX/RX. It carries Zephyr's boot banner, printk() and warning/error logs; the REPL stays on USB.

Fixed I2C0

Problem: The upstream boards also fix I2C0 to SCL/SDA with the legacy TWI driver, so busio.I2C(board.SCL, board.SDA) failed with "SCL in use" and the pins could not be used for anything else.

Fix: I2C0 is now a dynamic TWIM instance like I2C1. The Sense drops its SHT3XD node, which was on that bus; the sensor is still at 0x44. That node is the only difference between the two upstream boards that matters here, so the Sense's board.overlay now includes the Feather's and only deletes it.

board.I2C(), board.SPI(), board.UART()

Problem: zephyr2cp.py defines board.I2C(), board.SPI() and board.UART() only when a board's devicetree has exactly one device of that kind. In 10.3.x the Feather and Sense had one each, so the names existed. Since #11420, which isn't released yet, these boards also enable dynamic I2C, SPI and UART instances, so main lost the three names; board only has I2C0, FEATHER_I2C, UART0 and FEATHER_SERIAL.

Fix: The devicetree is the source of truth. The upstream Feather board labels its header buses feather_i2c, feather_spi and feather_serial. On nRF boards, where iobroker can route buses at runtime, zephyr2cp.py defines board.I2C(), board.SPI() or board.UART() for each of these labels, on the header pins named SCL/SDA, SCK/MOSI/MISO or TX/RX, which come from the board's Feather connector. Other SoCs keep their fixed devicetree buses. The labeled node only marks that the board has the bus; iobroker picks the instance when the bus is constructed. zephyr2cp.py generates the CIRCUITPY_BOARD_* pin tables for shared-module/board, so the buses are constructed on first use, as in the other ports. This restores the three names on the Feather and Sense. Other boards can get the same by adding labels, such as the Arduino header labels on the Nordic DKs (#11602).

Bus instance order

Problem: iobroker hands out the first free instance in a pool, in devicetree order, so busio.SPI() on the nRF52840 got the 8 MHz SPIM2 before the 32 MHz SPIM3.

Fix: zephyr2cp.py orders each pool by max-frequency, highest first, so SPIM3 is allocated first, as in ports/nordic.

Documentation

The iobroker README has a new "nRF serial instances" section: I2C, SPI and UART share an instance, the board overlay chooses each instance's mode, and pools are allocated fastest first. The nRF board overlays point to it.

Testing

  • Feather nRF52840: board.I2C() with an MSA301, busio.I2C() on SCL/SDA and A2/A3 at once, SCL as GPIO after deinit(), board.SPI() loopback at 1 to 16 MHz, board.UART() loopback on TX/RX, CONSOLE=1 output on TX.
  • Feather Bluefruit Sense: board.I2C() and busio.I2C() find all five onboard sensors.
  • cptools/tests/test_zephyr2cp.py passes.

The SPI loopback fails at 32 MHz on both this port and ports/nordic 10.3.1. A logic analyzer shows that SPIM3's default MISO sample delay (rx-delay = <2>) lands on the bit boundary when MISO has no output delay, as in a loopback, so this is likely a test artifact rather than a problem with real devices.

dhalbert and others added 5 commits October 12, 2026 18:39
The Feather nRF52840 and Feather Bluefruit Sense put the Zephyr console
on UART0, which fixed TX/RX and kept them from busio.UART. UART0 is now
a dynamic iobroker instance like UART1, and the console comes back only
in CONSOLE=1 builds through the board's console.overlay and
console.conf. DEBUG=1 implies CONSOLE=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The upstream Feather nRF52840 and Feather Bluefruit Sense boards fix I2C0
to SCL/SDA, so busio.I2C() on those pins failed with "SCL in use" and
the pins could not be used as GPIO. I2C0 is now a dynamic TWIM instance
like I2C1. The Sense drops its SHT3XD node, which sat on the fixed bus.

The pinctrl entries and device nodes in both overlays are now in
alphabetical order; the compiled devicetree is otherwise unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zephyr2cp.py named a board's main bus after the single enabled devicetree
instance of each kind, which doesn't work once the instances are dynamic
iobroker pools. A board now lists its buses as [[board_i2c]],
[[board_spi]] and [[board_uart]] tables in circuitpython.toml, each with
its board names and pins, and zephyr2cp.py generates the
CIRCUITPY_BOARD_* pin tables for shared-module/board. As in the other
ports, each bus is constructed on its pins on first use.

The Feather nRF52840 and Feather Bluefruit Sense define I2C, SPI and UART
this way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iobroker hands out the first free instance in a pool, and the pools were
in devicetree order, so busio.SPI() on the nRF52840 got the 8 MHz SPIM2
before the 32 MHz SPIM3. zephyr2cp.py now orders each pool by the
instances' max-frequency, highest first, keeping devicetree order among
equals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add an "nRF serial instances" section to the iobroker README, the
counterpart of the comments in ports/nordic's nrfx_config.h: I2C, SPI and
UART modes share an instance, the board overlay picks each instance's
mode, the Feather split matches ports/nordic, and pools are allocated
fastest first. Each nRF board overlay points to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@tannewt tannewt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this. A couple Qs.

Comment thread ports/zephyr-cp/boards/adafruit/feather_nrf52840_sense_zephyr/board.overlay Outdated
Comment on lines +58 to +64
# Board buses, as on the ports/ counterpart: board.I2C(), board.SPI() and
# board.UART(), constructed on these pins when first used.
[[board_i2c]]
names = ["I2C"]
scl = "SCL"
sda = "SDA"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we auto-detect this?

I feel like this should be something we can gather from DTS. I guess the challenge is that we change the state of the DTS in order to make it dynamic.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears we can only do this partly, because some of the board definitions don't have the labels we would need to do this automatically. I can look at this further in a later PR.

@tannewt tannewt Oct 14, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we add them where they are missing? I think it's a question of what is the source of truth for these buses. I know the arduino connector in zephyr should imply one set of these.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may be able to. Can I do that in a later PR? I have some other things backed up behind this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hesitant to because code that's in the repo tends to be copied. What is blocked by this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try doing it here. I was going to get the Feathers done and have their regular board. buses restored to normal and then do this as the next PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the tables from the toml files and added some lookups in zephyr2cp.py. For example, for the Feathers, the feather_i2c bus label turns into board.I2C(). For boards with Arduino header labels and pin mappings, we will translate those. For boards that don't have such labels, we can add them in the .overlay file.

The Feather nRF52840 and Feather Bluefruit Sense had identical
console.conf and console.overlay files, and board.overlay files that
differed only in deleting the Sense's SHT3XD node. console.conf is now
one port-level file, which the Makefile applies in CONSOLE=1 builds of
any board that has a console.overlay. The Sense's console.overlay and
board.overlay include the Feather's; its board.overlay then only deletes
SHT3XD. The compiled devicetree is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dhalbert
dhalbert force-pushed the zephyr-cp-optional-console branch from 262d0e1 to 13cd72c Compare October 13, 2026 23:20
…console

# Conflicts:
#	ports/zephyr-cp/boards/nordic/nrf7002dk/board.overlay

@tannewt tannewt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we still need to settle on where the buses are defined.

CONSOLE=1 is fine with me. The intent is to not change the log level though right? I know changing the log level can require stack size increases too.

@dhalbert

Copy link
Copy Markdown
Collaborator Author

Right, no log level changes for CONSOLE=1.

dhalbert and others added 2 commits October 14, 2026 14:36
Replace the [[board_i2c]], [[board_spi]] and [[board_uart]] tables in
circuitpython.toml with the devicetree as the source of truth. A board
whose devicetree has a feather_i2c, feather_spi or feather_serial label
gets board.I2C(), board.SPI() or board.UART() on the header pins named
SCL/SDA, SCK/MOSI/MISO or TX/RX. The labeled node only marks that the
board has the bus; iobroker picks the instance when the bus is
constructed. The Feather nRF52840 boards' circuitpython.toml files are
back to their main versions, and the generated board buses are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Constructing a board bus on its pins needs iobroker's runtime routing,
which only nRF SoCs have. On other SoCs, such as the Feather RP2040,
the labeled buses would raise NotImplementedError. Find board buses
from devicetree labels only when CONFIG_PINCTRL_NRF is on, and leave
other boards as before. The .config scan moves earlier so the board
bus code can use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dhalbert
dhalbert requested a review from tannewt October 14, 2026 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants