Skip to content

nrf: Fix I2C regression introduced by nrfx v3 update.#19220

Open
andrewleech wants to merge 1 commit into
micropython:masterfrom
andrewleech:nrf-i2c-fix
Open

nrf: Fix I2C regression introduced by nrfx v3 update.#19220
andrewleech wants to merge 1 commit into
micropython:masterfrom
andrewleech:nrf-i2c-fix

Conversation

@andrewleech
Copy link
Copy Markdown
Contributor

@andrewleech andrewleech commented May 14, 2026

Summary

Fixes a regression introduced in #19125 where I2C stopped working on nRF52840 when SPI is also enabled (issue #19214).

The root cause is an IRQ handler symbol conflict in nrfx v3: both nrfx_prs_box_0_irq_handler (PRS dispatcher) and nrfx_twim_0_irq_handler map to the same final symbol SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQHandler via the nrfx irq alias headers. With LTO enabled, the PRS dispatcher wins and the TWIM event handler is never called, so I2C transfers stall indefinitely waiting for a xfer_done flag that never gets set.

The fix is to use nrfx blocking mode (NULL event handler) where nrfy_twim_tx_start polls the STOPPED/SUSPENDED peripheral register directly without relying on the NVIC interrupt. This is appropriate for MicroPython's synchronous machine.I2C API.

Also fixes the NOSTOP flag: the old code passed (flags & MP_MACHINE_I2C_FLAG_STOP) == 0 as the xfer flags argument, which evaluates to 0 or 1. The value 1 is NRFX_TWIM_FLAG_TX_POSTINC in nrfx v3, not the no-stop flag. Now uses NRFX_TWIM_FLAG_TX_NO_STOP explicitly.

Testing

Tested on PCA10059 (nRF52840-Dongle) via SWD with probe-rs. machine.I2C(0, scl=Pin(26), sda=Pin(24)).scan() completes without crash and returns an empty list as expected with no devices on the bus, confirming the blocking-mode fix resolves the indefinite stall. Floating-point operations also work correctly after boot.

Build tested for nRF52840 only (the only nRF target with TWIM in nrfx v3).

Generative AI

I used generative AI tools when creating this PR, but a human has checked the code and is responsible for the code and the description above.

In nrfx v3, when both SPIM0 and TWIM0 are enabled (PRS_BOX_0_ENABLED),
nrfx_prs.c and nrfx_twim.c emit the same IRQ handler symbol via macro
aliasing in nrfx_irqs_nrf52840.h. LTO resolves this to the PRS dispatcher,
which never reaches twi_event_handler, causing I2C to stall indefinitely.

Fix by switching to blocking mode (NULL handler), where nrfy_twim_tx_start
polls the STOPPED/SUSPENDED event internally without IRQ dependency.

Also fix the NOSTOP flag: the old code passed (flags & FLAG_STOP) == 0,
evaluating to 0 or 1. In nrfx v3 TWIM, flag 1 is NRFX_TWIM_FLAG_TX_POSTINC
(buffer post-increment), not no-stop. The correct flag is
NRFX_TWIM_FLAG_TX_NO_STOP (1 << 5).

Fixes: micropython#19214

Signed-off-by: Andrew Leech <andrew@alelec.net>
Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
@ricksorensen
Copy link
Copy Markdown
Contributor

I can confirm I2C works with this update on my XIAO nrf52840 with expansion board. Both I2C(1) - the intermal IMU bus and I2C(0) work.
I have not yet checked with SPI.

@robert-hh
Copy link
Copy Markdown
Contributor

robert-hh commented May 15, 2026

Tested with a Arduino Nano 33 BLE. I2C works. No SPI connected or enabled, besides the internal Flash of the UBLOX module. But I do not see if and where the supplied value for timeout is applied.

@dpgeorge
Copy link
Copy Markdown
Member

This fix does not look correct. If non-blocking (interrupt based) I2C worked before, it should still work with nrfx v3.

The PRS (peripheral resource sharing) should be able to route the IRQ to the correct peripheral.

@ricksorensen
Copy link
Copy Markdown
Contributor

ricksorensen commented May 18, 2026

EDIT2: Switching to SPI(1) fixes the interference.

EDIT: Is there a right way for a user to select I2C/SPI IDs? I see there are shared internal drivers for SPI and I2C on the 52840.

I tried a simple experiment using Peter Hinch's micropython-nano-gui ST7789 SPI driver.

With version 1.27 - if SPI is created first, the display tests run okay. An I2C can be invoked and the ssd1306 works. If this is repeated (no reset) then the SPI display no longer displays (but does not hang.)
If I2C bus is created first (not connected to ssd1306 driver) and the the SPI display driver started, the SPI is created but when the driver (SSD) is initiated the code hangs.

With this PR, both the testnrf_xxx programs complete with no hang. But if the I2C is created before the SPI then the SPI display does not refesh. The setup was:

  • SSD1306 I2C display (128x32)

  • 1.14" ST7789 SPI display(135x240)

  • XIAO nrf52840, standard pins. I do have the standard external QSPI Flash also mounted.

  • testitdual.doit(0): create SPI driver first. This works, but if run again the SPI display does not refresh without machine hard reset.

  • testitdual.doit(1): create I2C driver first. The SPI display does not display- but it does not hang.

# testitdual.py
import gc
import machine
from color_setup import ssd  # Create a display instance
import ssd1306

print("ssd imported mem {}".format(gc.mem_free()))
# seems to be slow import for tinys3
from gui.core.colors import RED, BLUE, GREEN

# note seems to work without refresh also
from gui.core.nanogui import refresh


def doit(earlyi2c=False):
    print("refresh")
    i2c = None
    if earlyi2c:
        i2c = machine.I2C(0, scl=machine.Pin("P5"), sda=machine.Pin("P4"))

    refresh(ssd, True)  # Initialise and clear display.
    # Uncomment for ePaper displays
    # ssd.wait_until_ready()
    ssd.fill(0)
    ssd.line(
        0, 0, ssd.width - 1, ssd.height - 1, GREEN
    )  # Green diagonal corner-to-corner
    ssd.rect(0, 0, 15, 15, RED)  # Red square at top left
    ssd.rect(
        ssd.width - 15, ssd.height - 15, 15, 15, BLUE
    )  # Blue square at bottom right
    ssd.show()

    print(gc.mem_free())
    if not i2c:
        i2c = machine.I2C(0, scl=machine.Pin("P5"), sda=machine.Pin("P4"))

    si2c = ssd1306.SSD1306_I2C(128, 32, i2c, addr=60)

    si2c.fill(0)
    si2c.text(" First Line", 0, 10)
    si2c.line(0, 0, 127, 31, 1)
    si2c.show()
    print("All done")
# color_setup.py Customise for your xiao nrf52840

# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch, Ihor Nehrutsa

# Supports:
# TTGO T-Display 1.14" 135*240(Pixel) based on ST7789V

# WIRING (TTGO T-Display pin numbers and names).
# Pinout of TFT Driver
# ST7789     ESP32
# TFT_MISO  N/A
TFT_MOSI = 47  # pin 11 D10 (SDA on schematic pdf) SPI interface output/input pin.
TFT_SCLK = 45  # pin 9  D8 This pin is used to be serial interface clock.
TFT_CS = 29  # pin 4  D3 Chip selection pin, low enable, high disable.
TFT_DC = 28  # pin 3  D3 Display data/command selection pin in 4-line serial interface.
TFT_RST = 3  # pin 2  D1 This signal will reset the device,Signal is active low.
TFT_BL = 2  # pin 1  D0 (LEDK on schematic pdf) Display backlight control pin

from machine import Pin, SPI
import gc

from drivers.st7789.st7789_4bit import *

SSD = ST7789

pdc = Pin(TFT_DC, Pin.OUT, value=0)  # Arbitrary pins
pcs = Pin(TFT_CS, Pin.OUT, value=1)
prst = Pin(TFT_RST, Pin.OUT, value=1)
pbl = Pin(TFT_BL, Pin.OUT, value=1)

gc.collect()  # Precaution before instantiating framebuf
# Conservative low baudrate. Can go to 62.5MHz.
spi = SPI(0, 30_000_000, sck=Pin(TFT_SCLK), mosi=Pin(TFT_MOSI))


"""            TTGO 
     v  +----------------+
 40  |  |                |
     ^  |    +------+    | pin 36
     |  |    |      |    |
     |  |    |      |    |
240  |  |    |      |    |
     |  |    |      |    |
     |  |    |      |    |
     v  |    +------+    |
 40  |  |                | Reset button
     ^  +----------------+
        >----<------>----<        
          52   135    xx
        BUTTON2    BUTTON1
"""
# Right way up landscape: defined as top left adjacent to pin 36
ssd = SSD(
    spi,
    height=135,
    width=240,
    dc=pdc,
    cs=pcs,
    rst=prst,
    disp_mode=LANDSCAPE,
    display=TDISPLAY,
)
# Normal portrait display: consistent with TTGO logo at top
# ssd = SSD(spi, height=240, width=135, dc=pdc, cs=pcs, rst=prst, disp_mode=PORTRAIT, display=TDISPLAY)

testnrf_i2c.py,testnrf_spi.pythe test programs with I2C created first, then SPI created first. I run from the root of micropython-nano-gui:

>pwd
/work/domicropy/micropython-nano-gui
>mpremote mount . run testnrf_i2c.py
Local directory . is mounted at /remote
3.4.0; MicroPython v1.27.0 on 2025-12-09
# hangs here does no return, power off board.
# power reset
>pwd
/work/domicropy/micropython-nano-gui
>mpremote mount . run testnrf_spi.py
Local directory . is mounted at /remote
3.4.0; MicroPython v1.27.0 on 2025-12-09
Completed SSD startup
>
#testnrf_i2c.py
import sys
from machine import Pin, I2C, SPI
from drivers.st7789.st7789_4bit import ST7789, LANDSCAPE, TDISPLAY

print(sys.version)

i2 = I2C(0, scl=Pin("P5"), sda=Pin("P4"))
i2.scan()

SSD = ST7789
spi = SPI(0, 30_000_000, sck=Pin(45), mosi=Pin(47))
pdc = Pin(28, Pin.OUT, value=0)  # Arbitrary pins
pcs = Pin(29, Pin.OUT, value=1)
prst = Pin(3, Pin.OUT, value=1)
pbl = Pin(2, Pin.OUT, value=1)
ssd = SSD(
    spi,
    height=135,
    width=240,
    dc=pdc,
    cs=pcs,
    rst=prst,
    disp_mode=LANDSCAPE,
    display=TDISPLAY,
)
print("Completed SSD startup")

testnrf_spi.py # spi initialized first, then I2C

#testnrf_spi.py
import sys
from machine import Pin, I2C, SPI
from drivers.st7789.st7789_4bit import ST7789, LANDSCAPE, TDISPLAY

print(sys.version)


SSD = ST7789
spi = SPI(0, 30_000_000, sck=Pin(45), mosi=Pin(47))
pdc = Pin(28, Pin.OUT, value=0)  # Arbitrary pins
pcs = Pin(29, Pin.OUT, value=1)
prst = Pin(3, Pin.OUT, value=1)
pbl = Pin(2, Pin.OUT, value=1)
ssd = SSD(
    spi,
    height=135,
    width=240,
    dc=pdc,
    cs=pcs,
    rst=prst,
    disp_mode=LANDSCAPE,
    display=TDISPLAY,
)

i2 = I2C(0, scl=Pin("P5"), sda=Pin("P4"))
i2.scan()

print("Completed SSD startup")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants