From f0f8068e5b833711acdadc2f4b6940260320fe94 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Tue, 29 Sep 2026 11:14:08 +0000 Subject: [PATCH 1/4] busio: add a non-blocking SPI write for internal use common_hal_busio_spi_write_start() starts a write and returns; common_hal_busio_spi_write_end() waits for it without running background tasks, since the caller still holds the bus. The done flag uses the circuitpy_async_flag_t macros from #11040, so a port can later set it from an interrupt. Ports that have the write set CIRCUITPY_BUSIO_SPI_ASYNC. There is no Python API. raspberrypi: the blocking transfer and the async write share one DMA path. A blocking transfer claims its two channels per call as before; an async write keeps them until deinit. Writes shorter than 32 bytes or from flash/PSRAM stay synchronous. atmel-samd: samd51/same5x only, through shared_dma_transfer_start(). espressif: up to two queued ESP-IDF transactions (8 KB). Writes of four bytes or less, longer writes and word sizes other than 8 bits stay synchronous. --- ports/atmel-samd/common-hal/busio/SPI.c | 40 ++++++ ports/atmel-samd/common-hal/busio/SPI.h | 10 ++ ports/atmel-samd/mpconfigport.mk | 1 + ports/espressif/common-hal/busio/SPI.c | 49 +++++++ ports/espressif/common-hal/busio/SPI.h | 7 + ports/espressif/mpconfigport.mk | 1 + ports/raspberrypi/common-hal/busio/SPI.c | 158 +++++++++++++++-------- ports/raspberrypi/common-hal/busio/SPI.h | 7 + ports/raspberrypi/mpconfigport.mk | 1 + py/circuitpy_async_flag.h | 18 +++ py/circuitpy_mpconfig.h | 5 + py/circuitpy_mpconfig.mk | 5 + shared-bindings/busio/SPI.h | 12 ++ 13 files changed, 259 insertions(+), 55 deletions(-) create mode 100644 py/circuitpy_async_flag.h diff --git a/ports/atmel-samd/common-hal/busio/SPI.c b/ports/atmel-samd/common-hal/busio/SPI.c index 19fd81e425d..9a736411e77 100644 --- a/ports/atmel-samd/common-hal/busio/SPI.c +++ b/ports/atmel-samd/common-hal/busio/SPI.c @@ -154,6 +154,9 @@ void common_hal_busio_spi_construct(busio_spi_obj_t *self, setup_pin(clock, clock_pinmux, GPIO_DIRECTION_OUT); self->clock_pin = clock->number; + #if CIRCUITPY_BUSIO_SPI_ASYNC + self->async_active = false; + #endif if (mosi_none) { self->MOSI_pin = NO_PIN; @@ -192,6 +195,9 @@ void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { if (common_hal_busio_spi_deinited(self)) { return; } + #if CIRCUITPY_BUSIO_SPI_ASYNC + common_hal_busio_spi_write_end(self); + #endif allow_reset_sercom(self->spi_desc.dev.prvt); spi_m_sync_disable(&self->spi_desc); @@ -285,6 +291,40 @@ bool common_hal_busio_spi_write(busio_spi_obj_t *self, return status >= 0; // Status is number of chars read or an error code < 0. } +#if CIRCUITPY_BUSIO_SPI_ASYNC +void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data, size_t len, + circuitpy_async_flag_t *done) { + common_hal_busio_spi_write_end(self); + CIRCUITPY_ASYNC_FLAG_INIT(done); + self->async_done = done; + // One descriptor holds at most 65535 beats; longer writes and short ones go the normal way. + if (len >= 32 && len <= 65535) { + Sercom *sercom = self->spi_desc.dev.prvt; + shared_dma_transfer_start(&self->async_xfer, sercom, data, &sercom->SPI.DATA.reg, + NULL, NULL, len, 0); + if (self->async_xfer.failure == 0) { + self->async_active = true; + return; + } + } + common_hal_busio_spi_write(self, data, len); + CIRCUITPY_ASYNC_FLAG_SET(done); +} + +void common_hal_busio_spi_write_end(busio_spi_obj_t *self) { + if (!self->async_active) { + return; + } + // finished() also clears the RX overflow a TX-only transfer leaves on the SERCOM. + // No background tasks here: the caller holds the bus, and one of them may want it. + while (!shared_dma_transfer_finished(&self->async_xfer)) { + } + shared_dma_transfer_close(&self->async_xfer); + self->async_active = false; + CIRCUITPY_ASYNC_FLAG_SET(self->async_done); +} +#endif + bool common_hal_busio_spi_read(busio_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value) { if (len == 0) { diff --git a/ports/atmel-samd/common-hal/busio/SPI.h b/ports/atmel-samd/common-hal/busio/SPI.h index ed8bc220a34..ee7aa78c3ac 100644 --- a/ports/atmel-samd/common-hal/busio/SPI.h +++ b/ports/atmel-samd/common-hal/busio/SPI.h @@ -6,11 +6,16 @@ #pragma once +#include "py/mpconfig.h" +#if CIRCUITPY_BUSIO_SPI_ASYNC +#include "peripherals/samd/dma.h" +#endif #include "common-hal/microcontroller/Pin.h" #include "hal/include/hal_spi_m_sync.h" #include "py/obj.h" +#include "py/circuitpy_async_flag.h" typedef struct { mp_obj_base_t base; @@ -19,4 +24,9 @@ typedef struct { uint8_t clock_pin; uint8_t MOSI_pin; uint8_t MISO_pin; + #if CIRCUITPY_BUSIO_SPI_ASYNC + bool async_active; // a write_start DMA transfer may still be running + dma_transfer_t async_xfer; + circuitpy_async_flag_t *async_done; + #endif } busio_spi_obj_t; diff --git a/ports/atmel-samd/mpconfigport.mk b/ports/atmel-samd/mpconfigport.mk index efa1f9ebb12..29a881d2d98 100644 --- a/ports/atmel-samd/mpconfigport.mk +++ b/ports/atmel-samd/mpconfigport.mk @@ -98,6 +98,7 @@ endif # samd21 # Put samx5x-only choices here. ifneq ($(filter $(CHIP_FAMILY),samd51 same51 same54),) +CIRCUITPY_BUSIO_SPI_ASYNC ?= 1 # The 24 KB stack has room for a larger display refresh buffer. CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048 diff --git a/ports/espressif/common-hal/busio/SPI.c b/ports/espressif/common-hal/busio/SPI.c index b85f8cddd2a..a279b4aa3fc 100644 --- a/ports/espressif/common-hal/busio/SPI.c +++ b/ports/espressif/common-hal/busio/SPI.c @@ -97,6 +97,9 @@ void common_hal_busio_spi_construct(busio_spi_obj_t *self, // Ensure the object starts in its deinit state. common_hal_busio_spi_mark_deinit(self); + #if CIRCUITPY_BUSIO_SPI_ASYNC + self->async_count = 0; + #endif const spi_bus_config_t bus_config = { .mosi_io_num = mosi != NULL ? mosi->number : -1, @@ -176,6 +179,10 @@ void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { RUN_BACKGROUND_TASKS; } + #if CIRCUITPY_BUSIO_SPI_ASYNC + common_hal_busio_spi_write_end(self); + #endif + // Mark as deinit early in case we are used in an interrupt. common_hal_reset_pin(self->clock); common_hal_busio_spi_mark_deinit(self); @@ -200,6 +207,9 @@ bool common_hal_busio_spi_configure(busio_spi_obj_t *self, bits == self->bits) { return true; } + #if CIRCUITPY_BUSIO_SPI_ASYNC + common_hal_busio_spi_write_end(self); + #endif spi_bus_remove_device(spi_handle[self->host_id]); set_spi_config(self, baudrate, polarity, phase, bits); return true; @@ -324,6 +334,45 @@ bool common_hal_busio_spi_transfer(busio_spi_obj_t *self, return true; } +#if CIRCUITPY_BUSIO_SPI_ASYNC +void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data, size_t len, + circuitpy_async_flag_t *done) { + common_hal_busio_spi_write_end(self); + CIRCUITPY_ASYNC_FLAG_INIT(done); + self->async_done = done; + size_t chunks = (len + SPI_MAX_DMA_LEN - 1) / SPI_MAX_DMA_LEN; + // Short writes, other word sizes and writes needing more transactions than we keep go the + // normal way. + if (len <= 4 || self->bits != 8 || chunks > MP_ARRAY_SIZE(self->async_trans)) { + common_hal_busio_spi_write(self, data, len); + CIRCUITPY_ASYNC_FLAG_SET(done); + return; + } + for (size_t i = 0; i < chunks; i++) { + size_t offset = i * SPI_MAX_DMA_LEN; + spi_transaction_t *trans = &self->async_trans[i]; + memset(trans, 0, sizeof(*trans)); + trans->length = MIN(len - offset, SPI_MAX_DMA_LEN) * 8; + trans->tx_buffer = data + offset; + spi_device_queue_trans(spi_handle[self->host_id], trans, portMAX_DELAY); + } + self->async_count = chunks; +} + +void common_hal_busio_spi_write_end(busio_spi_obj_t *self) { + if (self->async_count == 0) { + return; + } + // No background tasks here: the caller holds the bus, and one of them may want it. + spi_transaction_t *trans; + while (self->async_count > 0) { + spi_device_get_trans_result(spi_handle[self->host_id], &trans, portMAX_DELAY); + self->async_count--; + } + CIRCUITPY_ASYNC_FLAG_SET(self->async_done); +} +#endif + uint32_t common_hal_busio_spi_get_frequency(busio_spi_obj_t *self) { return self->baudrate; } diff --git a/ports/espressif/common-hal/busio/SPI.h b/ports/espressif/common-hal/busio/SPI.h index 4886ab173ab..9337aa12ebd 100644 --- a/ports/espressif/common-hal/busio/SPI.h +++ b/ports/espressif/common-hal/busio/SPI.h @@ -6,6 +6,8 @@ #pragma once +#include "py/mpconfig.h" +#include "py/circuitpy_async_flag.h" #include "driver/spi_master.h" #include "shared-bindings/microcontroller/Pin.h" @@ -25,4 +27,9 @@ typedef struct { uint32_t requested_baudrate; // Value passed to configure(); used for the cache-hit check. SemaphoreHandle_t mutex; + #if CIRCUITPY_BUSIO_SPI_ASYNC + spi_transaction_t async_trans[2]; // queued by write_start, collected by write_end + uint8_t async_count; + circuitpy_async_flag_t *async_done; + #endif } busio_spi_obj_t; diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 80944a7a130..48ab8d04580 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -54,6 +54,7 @@ CIRCUITPY_FULL_BUILD ?= 1 # The 24 KB stack has room for a larger display refresh buffer. CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048 +CIRCUITPY_BUSIO_SPI_ASYNC ?= 1 # If SSL is enabled, it's mbedtls CIRCUITPY_SSL_MBEDTLS = 1 diff --git a/ports/raspberrypi/common-hal/busio/SPI.c b/ports/raspberrypi/common-hal/busio/SPI.c index aeb06d919ae..43107a44482 100644 --- a/ports/raspberrypi/common-hal/busio/SPI.c +++ b/ports/raspberrypi/common-hal/busio/SPI.c @@ -65,6 +65,8 @@ void common_hal_busio_spi_construct(busio_spi_obj_t *self, mp_raise_ValueError(MP_ERROR_TEXT("SPI peripheral in use")); } + self->async_active = false; + self->dma_kept = false; self->target_frequency = 250000; self->real_frequency = spi_init(self->peripheral, self->target_frequency); @@ -103,6 +105,12 @@ void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { if (common_hal_busio_spi_deinited(self)) { return; } + common_hal_busio_spi_write_end(self); + if (self->dma_kept) { + dma_channel_unclaim(self->dma_tx); + dma_channel_unclaim(self->dma_rx); + self->dma_kept = false; + } spi_deinit(self->peripheral); common_hal_reset_pin(self->clock); @@ -161,90 +169,110 @@ void common_hal_busio_spi_unlock(busio_spi_obj_t *self) { self->has_lock = false; } -static bool _transfer(busio_spi_obj_t *self, +// Start a transfer. With DMA it runs in the background and _end() finishes it; otherwise it is +// done in software before this returns. An out or in buffer shorter than the transfer is one +// byte repeated or dropped. keep_dma keeps the DMA channels until deinit, for buses that send +// often. Returns whether DMA is running. +static bool _start(busio_spi_obj_t *self, const uint8_t *data_out, size_t out_len, - uint8_t *data_in, size_t in_len) { - // Use DMA for large transfers if channels are available - const size_t dma_min_size_threshold = 32; - int chan_tx = -1; - int chan_rx = -1; + uint8_t *data_in, size_t in_len, bool keep_dma) { size_t len = MAX(out_len, in_len); - if (len >= dma_min_size_threshold) { - // Use two DMA channels to service the two FIFOs - chan_tx = dma_claim_unused_channel(false); - chan_rx = dma_claim_unused_channel(false); - } - bool has_dma_channels = chan_rx >= 0 && chan_tx >= 0; // Only use DMA if both data buffers are in SRAM. Otherwise, we'll stall the DMA with PSRAM or flash cache misses. - bool data_in_sram = data_in >= (uint8_t *)SRAM_BASE && data_out >= (uint8_t *)SRAM_BASE; - bool use_dma = has_dma_channels && data_in_sram; + bool use_dma = len >= 32 && data_in >= (uint8_t *)SRAM_BASE && data_out >= (uint8_t *)SRAM_BASE; + if (use_dma && !self->dma_kept) { + int chan_tx = dma_claim_unused_channel(false); + int chan_rx = dma_claim_unused_channel(false); + if (chan_tx >= 0 && chan_rx >= 0) { + self->dma_tx = chan_tx; + self->dma_rx = chan_rx; + self->dma_kept = keep_dma; + } else { + // If we have claimed only one channel successfully, release it. + if (chan_tx >= 0) { + dma_channel_unclaim(chan_tx); + } + if (chan_rx >= 0) { + dma_channel_unclaim(chan_rx); + } + use_dma = false; + } + } if (use_dma) { - dma_channel_config c = dma_channel_get_default_config(chan_tx); + dma_channel_config c = dma_channel_get_default_config(self->dma_tx); channel_config_set_transfer_data_size(&c, DMA_SIZE_8); channel_config_set_dreq(&c, spi_get_index(self->peripheral) ? DREQ_SPI1_TX : DREQ_SPI0_TX); channel_config_set_read_increment(&c, out_len == len); channel_config_set_write_increment(&c, false); - dma_channel_configure(chan_tx, &c, + dma_channel_configure(self->dma_tx, &c, &spi_get_hw(self->peripheral)->dr, data_out, len, false); - c = dma_channel_get_default_config(chan_rx); + c = dma_channel_get_default_config(self->dma_rx); channel_config_set_transfer_data_size(&c, DMA_SIZE_8); channel_config_set_dreq(&c, spi_get_index(self->peripheral) ? DREQ_SPI1_RX : DREQ_SPI0_RX); channel_config_set_read_increment(&c, false); channel_config_set_write_increment(&c, in_len == len); - dma_channel_configure(chan_rx, &c, + dma_channel_configure(self->dma_rx, &c, data_in, &spi_get_hw(self->peripheral)->dr, len, false); - dma_start_channel_mask((1u << chan_rx) | (1u << chan_tx)); - while (dma_channel_is_busy(chan_rx) || dma_channel_is_busy(chan_tx)) { - // TODO: We should idle here until we get a DMA interrupt or something else. - RUN_BACKGROUND_TASKS; - } - } - - // If we have claimed only one channel successfully, we should release immediately. This also - // releases the DMA after use_dma has been done. - if (chan_rx >= 0) { - dma_channel_unclaim(chan_rx); - } - if (chan_tx >= 0) { - dma_channel_unclaim(chan_tx); + dma_start_channel_mask((1u << self->dma_rx) | (1u << self->dma_tx)); + return true; } - if (!use_dma) { - // Use software for small transfers, or if couldn't claim two DMA channels - // Never have more transfers in flight than will fit into the RX FIFO, - // else FIFO will overflow if this code is heavily interrupted. - const size_t fifo_depth = 8; - size_t rx_remaining = len; - size_t tx_remaining = len; - - while (rx_remaining || tx_remaining) { - if (tx_remaining && spi_is_writable(self->peripheral) && rx_remaining - tx_remaining < fifo_depth) { - spi_get_hw(self->peripheral)->dr = (uint32_t)*data_out; - // Increment only if the buffer is the transfer length. It's 1 otherwise. - if (out_len == len) { - data_out++; - } - --tx_remaining; + // Use software for small transfers, or if couldn't claim two DMA channels + // Never have more transfers in flight than will fit into the RX FIFO, + // else FIFO will overflow if this code is heavily interrupted. + const size_t fifo_depth = 8; + size_t rx_remaining = len; + size_t tx_remaining = len; + + while (rx_remaining || tx_remaining) { + if (tx_remaining && spi_is_writable(self->peripheral) && rx_remaining - tx_remaining < fifo_depth) { + spi_get_hw(self->peripheral)->dr = (uint32_t)*data_out; + // Increment only if the buffer is the transfer length. It's 1 otherwise. + if (out_len == len) { + data_out++; } - if (rx_remaining && spi_is_readable(self->peripheral)) { - *data_in = (uint8_t)spi_get_hw(self->peripheral)->dr; - // Increment only if the buffer is the transfer length. It's 1 otherwise. - if (in_len == len) { - data_in++; - } - --rx_remaining; + --tx_remaining; + } + if (rx_remaining && spi_is_readable(self->peripheral)) { + *data_in = (uint8_t)spi_get_hw(self->peripheral)->dr; + // Increment only if the buffer is the transfer length. It's 1 otherwise. + if (in_len == len) { + data_in++; } + --rx_remaining; + } + RUN_BACKGROUND_TASKS; + } + return false; +} + +// Wait for a DMA transfer started by _start(). The RX channel finishes last. +static void _end(busio_spi_obj_t *self, bool background_tasks) { + while (dma_channel_is_busy(self->dma_rx)) { + if (background_tasks) { RUN_BACKGROUND_TASKS; } } + if (!self->dma_kept) { + dma_channel_unclaim(self->dma_tx); + dma_channel_unclaim(self->dma_rx); + } +} + +static bool _transfer(busio_spi_obj_t *self, + const uint8_t *data_out, size_t out_len, + uint8_t *data_in, size_t in_len) { + if (_start(self, data_out, out_len, data_in, in_len, false)) { + // TODO: We should idle here until we get a DMA interrupt or something else. + _end(self, true); + } return true; } @@ -254,6 +282,26 @@ bool common_hal_busio_spi_write(busio_spi_obj_t *self, return _transfer(self, data, len, (uint8_t *)&data_in, MIN(len, 4)); } +void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data, size_t len, + circuitpy_async_flag_t *done) { + common_hal_busio_spi_write_end(self); + CIRCUITPY_ASYNC_FLAG_INIT(done); + self->async_done = done; + self->async_active = _start(self, data, len, &self->discard, 1, true); + if (!self->async_active) { + CIRCUITPY_ASYNC_FLAG_SET(done); + } +} + +void common_hal_busio_spi_write_end(busio_spi_obj_t *self) { + if (self->async_active) { + // No background tasks here: the caller holds the bus, and one of them may want it. + _end(self, false); + self->async_active = false; + CIRCUITPY_ASYNC_FLAG_SET(self->async_done); + } +} + bool common_hal_busio_spi_read(busio_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value) { uint32_t data_out = write_value << 24 | write_value << 16 | write_value << 8 | write_value; diff --git a/ports/raspberrypi/common-hal/busio/SPI.h b/ports/raspberrypi/common-hal/busio/SPI.h index 3d43c1eff00..25fc95b00dc 100644 --- a/ports/raspberrypi/common-hal/busio/SPI.h +++ b/ports/raspberrypi/common-hal/busio/SPI.h @@ -9,6 +9,7 @@ #include "common-hal/microcontroller/Pin.h" #include "py/obj.h" +#include "py/circuitpy_async_flag.h" #include "hardware/spi.h" @@ -24,4 +25,10 @@ typedef struct { uint8_t polarity; uint8_t phase; uint8_t bits; + bool async_active; // a write_start DMA transfer may still be running + bool dma_kept; // dma_tx and dma_rx are ours until deinit + uint8_t dma_tx; + uint8_t dma_rx; + uint8_t discard; // RX target of write_start + circuitpy_async_flag_t *async_done; } busio_spi_obj_t; diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index 3d89e5277f5..f5358b5a12e 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -6,6 +6,7 @@ CIRCUITPY_OPTIMIZE_PROPERTY_FLASH_SIZE ?= 1 CIRCUITPY_WIFI_RADIO_SETTABLE_MAC_ADDRESS = 0 CIRCUITPY_RP2PIO ?= 1 +CIRCUITPY_BUSIO_SPI_ASYNC ?= 1 # The 24 KB stack has room for a larger display refresh buffer. CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048 CIRCUITPY_NEOPIXEL_WRITE ?= $(CIRCUITPY_RP2PIO) diff --git a/py/circuitpy_async_flag.h b/py/circuitpy_async_flag.h new file mode 100644 index 00000000000..5dd2c726386 --- /dev/null +++ b/py/circuitpy_async_flag.h @@ -0,0 +1,18 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +// Flag that marks an operation as done, possibly set from an interrupt. Ports may override these +// before this header is included. +#ifndef CIRCUITPY_ASYNC_FLAG_SET +typedef volatile bool circuitpy_async_flag_t; +#define CIRCUITPY_ASYNC_FLAG_INIT(flag) (*(flag) = false) +#define CIRCUITPY_ASYNC_FLAG_SET(flag) (*(flag) = true) +#define CIRCUITPY_ASYNC_FLAG_IS_SET(flag) (*(flag)) +#endif diff --git a/py/circuitpy_mpconfig.h b/py/circuitpy_mpconfig.h index 292ac236777..be660db9b6b 100644 --- a/py/circuitpy_mpconfig.h +++ b/py/circuitpy_mpconfig.h @@ -404,6 +404,11 @@ typedef long mp_off_t; #define CIRCUITPY_CONSOLE_UART_HEXDUMP(...) (void)0 #endif +// Normally set from CIRCUITPY_BUSIO_SPI_ASYNC in the .mk files. +#ifndef CIRCUITPY_BUSIO_SPI_ASYNC +#define CIRCUITPY_BUSIO_SPI_ASYNC (0) +#endif + // These CIRCUITPY_xxx values should all be defined in the *.mk files as being on or off. // So if any are not defined in *.mk, they'll throw an error here. diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index d3de1d32bb6..4c4353e82e1 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -238,6 +238,11 @@ CFLAGS += -DCIRCUITPY_BUSIO_I2C=$(CIRCUITPY_BUSIO_I2C) CIRCUITPY_BUSIO_SPI ?= $(CIRCUITPY_BUSIO) CFLAGS += -DCIRCUITPY_BUSIO_SPI=$(CIRCUITPY_BUSIO_SPI) +# The port implements common_hal_busio_spi_write_start() and common_hal_busio_spi_write_end(), so a +# display on a FourWire bus can send one strip while the next is being composited. +CIRCUITPY_BUSIO_SPI_ASYNC ?= 0 +CFLAGS += -DCIRCUITPY_BUSIO_SPI_ASYNC=$(CIRCUITPY_BUSIO_SPI_ASYNC) + CIRCUITPY_BUSIO_UART ?= $(CIRCUITPY_BUSIO) CFLAGS += -DCIRCUITPY_BUSIO_UART=$(CIRCUITPY_BUSIO_UART) diff --git a/shared-bindings/busio/SPI.h b/shared-bindings/busio/SPI.h index 969191c35a0..1750494ef9e 100644 --- a/shared-bindings/busio/SPI.h +++ b/shared-bindings/busio/SPI.h @@ -7,6 +7,7 @@ #pragma once #include "py/obj.h" +#include "py/circuitpy_async_flag.h" #include "common-hal/microcontroller/Pin.h" #include "common-hal/busio/SPI.h" @@ -35,6 +36,17 @@ extern void common_hal_busio_spi_unlock(busio_spi_obj_t *self); // Writes out the given data. extern bool common_hal_busio_spi_write(busio_spi_obj_t *self, const uint8_t *data, size_t len); +#if CIRCUITPY_BUSIO_SPI_ASYNC +// Start writing data and return, possibly before it has been sent. *done is set once it has been +// sent, at the latest by common_hal_busio_spi_write_end(). data must stay valid and unchanged, and +// the bus must not be used, until then. +extern void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data, size_t len, + circuitpy_async_flag_t *done); +// Finish a write started by common_hal_busio_spi_write_start(), waiting if needed. Returns at +// once if there is none. +extern void common_hal_busio_spi_write_end(busio_spi_obj_t *self); +#endif + // Reads in len bytes while outputting the byte write_value. extern bool common_hal_busio_spi_read(busio_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value); From aac8aab46737b3306e151f6e63ac93819157cfa7 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Tue, 29 Sep 2026 11:14:08 +0000 Subject: [PATCH 2/4] displayio: send one area buffer while composing the next A display bus can now have send_async next to send, and flush waits for it. FourWire provides both when the SPI port has an async write. BusDisplay then uses two area buffers: it composes the next one while the previous one is sent. Background tasks still run only while the bus is released, so another device on the same SPI bus is not locked out. Full-screen repaint: PicoPad (RP2040) 76.7 -> 57.7 ms, PyBadge (SAMD51) 26.5 -> 15.6 ms, Feather ESP32-S3 TFT 29.9 -> 19.5 ms. Ports without the async write build the same code as before. --- shared-bindings/fourwire/FourWire.h | 6 +++ shared-module/busdisplay/BusDisplay.c | 63 ++++++++++++++++++++++----- shared-module/displayio/bus_core.c | 7 +++ shared-module/displayio/bus_core.h | 4 ++ shared-module/fourwire/FourWire.c | 23 ++++++++++ shared-module/fourwire/FourWire.h | 3 ++ 6 files changed, 94 insertions(+), 12 deletions(-) diff --git a/shared-bindings/fourwire/FourWire.h b/shared-bindings/fourwire/FourWire.h index 3e77c25434d..bbb407de027 100644 --- a/shared-bindings/fourwire/FourWire.h +++ b/shared-bindings/fourwire/FourWire.h @@ -32,6 +32,12 @@ void common_hal_fourwire_fourwire_send(mp_obj_t self, display_byte_type_t byte_t void common_hal_fourwire_fourwire_end_transaction(mp_obj_t self); +#if CIRCUITPY_BUSIO_SPI_ASYNC +void common_hal_fourwire_fourwire_send_async(mp_obj_t self, display_byte_type_t byte_type, + display_chip_select_behavior_t chip_select, const uint8_t *data, uint32_t data_length); +void common_hal_fourwire_fourwire_flush(mp_obj_t self); +#endif + // The FourWire object always lives off the MP heap. So, code must collect any pointers // back to the MP heap manually. Otherwise they'll get freed. void common_hal_fourwire_fourwire_collect_ptrs(mp_obj_t obj); diff --git a/shared-module/busdisplay/BusDisplay.c b/shared-module/busdisplay/BusDisplay.c index a2d3cd7bf90..b4eb4c61dad 100644 --- a/shared-module/busdisplay/BusDisplay.c +++ b/shared-module/busdisplay/BusDisplay.c @@ -224,6 +224,30 @@ static void _send_pixels(busdisplay_busdisplay_obj_t *self, uint8_t *pixels, uin self->bus.send(self->bus.bus, DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels, length); } +// Whether the bus sends in the background. Always false without async SPI, so that code is +// left out. Not a local variable: GCC would then compile the refresh loop twice. +#define SEND_ASYNC(self) (CIRCUITPY_BUSIO_SPI_ASYNC && (self)->bus.send_async != NULL) + +// The data half of _send_pixels, handed to the bus without waiting for it to be sent. +static void _send_pixels_async(busdisplay_busdisplay_obj_t *self, uint8_t *pixels, uint32_t length) { + if (!self->bus.data_as_commands) { + self->bus.send(self->bus.bus, DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, &self->write_ram_command, 1); + } + self->bus.send_async(self->bus.bus, DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels, length); +} + +// Call only with the bus transaction closed: a background task may use the same bus. +static void _run_background_tasks(void) { + // Run background tasks so they can run during an explicit refresh. + // Auto-refresh won't run background tasks here because it is a background task itself. + RUN_BACKGROUND_TASKS; + + // Run USB background tasks so they can run during an implicit refresh. + #if CIRCUITPY_TINYUSB + usb_background(); + #endif +} + static bool _refresh_area(busdisplay_busdisplay_obj_t *self, const displayio_area_t *area) { uint16_t buffer_size = CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE / sizeof(uint32_t); // In uint32_ts @@ -272,8 +296,11 @@ static bool _refresh_area(busdisplay_busdisplay_obj_t *self, const displayio_are // Allocated and shared as a uint32_t array so the compiler knows the // alignment everywhere. uint32_t mask_length = (pixels_per_buffer / 32) + 1; - uint32_t buffer[buffer_size]; + // With an asynchronous bus, one buffer is composited while the other is being sent. + uint32_t buffers[SEND_ASYNC(self) ? 2 : 1][buffer_size]; uint32_t mask[mask_length]; + uint8_t fill_index = 0; + bool sending = false; displayio_area_t subrectangle = clipped; subrectangle.next = NULL; @@ -294,33 +321,45 @@ static bool _refresh_area(busdisplay_busdisplay_obj_t *self, const displayio_are subrectangle_size_bytes = displayio_area_size(&subrectangle) / (8 / self->core.colorspace.depth); } + uint32_t *buffer = buffers[fill_index]; memset(mask, 0, mask_length * sizeof(mask[0])); memset(buffer, 0, buffer_size * sizeof(buffer[0])); displayio_display_core_fill_area(&self->core, &subrectangle, mask, buffer); + // The previous strip must be sent before the region commands for this one. + if (sending) { + displayio_display_bus_flush(&self->bus); + displayio_display_bus_end_transaction(&self->bus); + sending = false; + _run_background_tasks(); + } + displayio_display_bus_set_region_to_update(&self->bus, &self->core, &subrectangle); // Can't acquire display bus; skip the rest of the data. if (!displayio_display_bus_begin_transaction(&self->bus)) { return false; } - _send_pixels(self, (uint8_t *)buffer, subrectangle_size_bytes); - displayio_display_bus_end_transaction(&self->bus); + if (SEND_ASYNC(self)) { + _send_pixels_async(self, (uint8_t *)buffer, subrectangle_size_bytes); + sending = true; + fill_index ^= 1; + } else { + _send_pixels(self, (uint8_t *)buffer, subrectangle_size_bytes); + displayio_display_bus_end_transaction(&self->bus); + } subrectangle.y1 = subrectangle.y2; - - // Run background tasks so they can run during an explicit refresh. - // Auto-refresh won't run background tasks here because it is a background task itself. - RUN_BACKGROUND_TASKS; - - // Run USB background tasks so they can run during an implicit refresh. - #if CIRCUITPY_TINYUSB - usb_background(); - #endif + if (!sending) { + _run_background_tasks(); + } } // Drain any remaining asynchronous transfers. displayio_display_bus_flush(&self->bus); + if (sending) { + displayio_display_bus_end_transaction(&self->bus); + } return true; } diff --git a/shared-module/displayio/bus_core.c b/shared-module/displayio/bus_core.c index 9b3fb426217..2a3bcedf610 100644 --- a/shared-module/displayio/bus_core.c +++ b/shared-module/displayio/bus_core.c @@ -53,6 +53,9 @@ void displayio_display_bus_construct(displayio_display_bus_t *self, self->address_little_endian = address_little_endian; self->flush = NULL; + #if CIRCUITPY_BUSIO_SPI_ASYNC + self->send_async = NULL; + #endif #if CIRCUITPY_PARALLELDISPLAYBUS if (mp_obj_is_type(bus, ¶lleldisplaybus_parallelbus_type)) { @@ -72,6 +75,10 @@ void displayio_display_bus_construct(displayio_display_bus_t *self, self->send = common_hal_fourwire_fourwire_send; self->end_transaction = common_hal_fourwire_fourwire_end_transaction; self->collect_ptrs = common_hal_fourwire_fourwire_collect_ptrs; + #if CIRCUITPY_BUSIO_SPI_ASYNC + self->send_async = common_hal_fourwire_fourwire_send_async; + self->flush = common_hal_fourwire_fourwire_flush; + #endif } else #endif #if CIRCUITPY_I2CDISPLAYBUS diff --git a/shared-module/displayio/bus_core.h b/shared-module/displayio/bus_core.h index 75d03d7f284..fa23df97323 100644 --- a/shared-module/displayio/bus_core.h +++ b/shared-module/displayio/bus_core.h @@ -20,6 +20,10 @@ typedef struct { display_bus_bus_free bus_free; display_bus_begin_transaction begin_transaction; display_bus_send send; + // Optional. Like send, but may return before the data is sent: the data must not change and + // the bus must not be used until flush returns. NULL when the bus has no such mode; only set + // and read when CIRCUITPY_BUSIO_SPI_ASYNC. + display_bus_send send_async; display_bus_end_transaction end_transaction; display_bus_flush flush; display_bus_collect_ptrs collect_ptrs; diff --git a/shared-module/fourwire/FourWire.c b/shared-module/fourwire/FourWire.c index 29ca9805c1e..33b83afbafe 100644 --- a/shared-module/fourwire/FourWire.c +++ b/shared-module/fourwire/FourWire.c @@ -186,6 +186,29 @@ void common_hal_fourwire_fourwire_send(mp_obj_t obj, display_byte_type_t data_ty } } +#if CIRCUITPY_BUSIO_SPI_ASYNC +void common_hal_fourwire_fourwire_send_async(mp_obj_t obj, display_byte_type_t data_type, + display_chip_select_behavior_t chip_select, const uint8_t *data, uint32_t data_length) { + fourwire_fourwire_obj_t *self = MP_OBJ_TO_PTR(obj); + // Without a DC pin every byte carries a DC bit, and the toggling mode pulses CS per byte: + // both are sent byte by byte, so they stay synchronous. + if (self->command == mp_const_none || chip_select == CHIP_SELECT_TOGGLE_EVERY_BYTE) { + common_hal_fourwire_fourwire_send(obj, data_type, chip_select, data, data_length); + return; + } + if (data_length == 0) { + return; + } + digitalinout_protocol_set_value(self->command, data_type == DISPLAY_DATA); + common_hal_busio_spi_write_start(self->bus, data, data_length, &self->sent); +} + +void common_hal_fourwire_fourwire_flush(mp_obj_t obj) { + fourwire_fourwire_obj_t *self = MP_OBJ_TO_PTR(obj); + common_hal_busio_spi_write_end(self->bus); +} +#endif + void common_hal_fourwire_fourwire_end_transaction(mp_obj_t obj) { fourwire_fourwire_obj_t *self = MP_OBJ_TO_PTR(obj); if (self->chip_select != mp_const_none) { diff --git a/shared-module/fourwire/FourWire.h b/shared-module/fourwire/FourWire.h index c239a3dfde3..1abf74fcadc 100644 --- a/shared-module/fourwire/FourWire.h +++ b/shared-module/fourwire/FourWire.h @@ -25,4 +25,7 @@ typedef struct { bool own_command; bool own_chip_select; bool own_reset; + #if CIRCUITPY_BUSIO_SPI_ASYNC + circuitpy_async_flag_t sent; // set once the last send_async has been sent + #endif } fourwire_fourwire_obj_t; From c62376db325c0630dfc275a4e120586e52b62dbb Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Thu, 1 Oct 2026 14:34:06 +0000 Subject: [PATCH 3/4] busio: name the async SPI end function for any transfer common_hal_busio_spi_write_end() becomes common_hal_busio_spi_end(). It finishes whatever a common_hal_busio_spi_*_start() call started, so an async read or full-duplex transfer added later can use it too. --- ports/atmel-samd/common-hal/busio/SPI.c | 6 +++--- ports/espressif/common-hal/busio/SPI.c | 8 ++++---- ports/espressif/common-hal/busio/SPI.h | 2 +- ports/raspberrypi/common-hal/busio/SPI.c | 6 +++--- py/circuitpy_mpconfig.mk | 2 +- shared-bindings/busio/SPI.h | 8 ++++---- shared-module/fourwire/FourWire.c | 2 +- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/ports/atmel-samd/common-hal/busio/SPI.c b/ports/atmel-samd/common-hal/busio/SPI.c index 9a736411e77..d9f1ac13561 100644 --- a/ports/atmel-samd/common-hal/busio/SPI.c +++ b/ports/atmel-samd/common-hal/busio/SPI.c @@ -196,7 +196,7 @@ void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { return; } #if CIRCUITPY_BUSIO_SPI_ASYNC - common_hal_busio_spi_write_end(self); + common_hal_busio_spi_end(self); #endif allow_reset_sercom(self->spi_desc.dev.prvt); @@ -294,7 +294,7 @@ bool common_hal_busio_spi_write(busio_spi_obj_t *self, #if CIRCUITPY_BUSIO_SPI_ASYNC void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data, size_t len, circuitpy_async_flag_t *done) { - common_hal_busio_spi_write_end(self); + common_hal_busio_spi_end(self); CIRCUITPY_ASYNC_FLAG_INIT(done); self->async_done = done; // One descriptor holds at most 65535 beats; longer writes and short ones go the normal way. @@ -311,7 +311,7 @@ void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data CIRCUITPY_ASYNC_FLAG_SET(done); } -void common_hal_busio_spi_write_end(busio_spi_obj_t *self) { +void common_hal_busio_spi_end(busio_spi_obj_t *self) { if (!self->async_active) { return; } diff --git a/ports/espressif/common-hal/busio/SPI.c b/ports/espressif/common-hal/busio/SPI.c index a279b4aa3fc..40ba60dab40 100644 --- a/ports/espressif/common-hal/busio/SPI.c +++ b/ports/espressif/common-hal/busio/SPI.c @@ -180,7 +180,7 @@ void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { } #if CIRCUITPY_BUSIO_SPI_ASYNC - common_hal_busio_spi_write_end(self); + common_hal_busio_spi_end(self); #endif // Mark as deinit early in case we are used in an interrupt. @@ -208,7 +208,7 @@ bool common_hal_busio_spi_configure(busio_spi_obj_t *self, return true; } #if CIRCUITPY_BUSIO_SPI_ASYNC - common_hal_busio_spi_write_end(self); + common_hal_busio_spi_end(self); #endif spi_bus_remove_device(spi_handle[self->host_id]); set_spi_config(self, baudrate, polarity, phase, bits); @@ -337,7 +337,7 @@ bool common_hal_busio_spi_transfer(busio_spi_obj_t *self, #if CIRCUITPY_BUSIO_SPI_ASYNC void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data, size_t len, circuitpy_async_flag_t *done) { - common_hal_busio_spi_write_end(self); + common_hal_busio_spi_end(self); CIRCUITPY_ASYNC_FLAG_INIT(done); self->async_done = done; size_t chunks = (len + SPI_MAX_DMA_LEN - 1) / SPI_MAX_DMA_LEN; @@ -359,7 +359,7 @@ void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data self->async_count = chunks; } -void common_hal_busio_spi_write_end(busio_spi_obj_t *self) { +void common_hal_busio_spi_end(busio_spi_obj_t *self) { if (self->async_count == 0) { return; } diff --git a/ports/espressif/common-hal/busio/SPI.h b/ports/espressif/common-hal/busio/SPI.h index 9337aa12ebd..e611f2c1e61 100644 --- a/ports/espressif/common-hal/busio/SPI.h +++ b/ports/espressif/common-hal/busio/SPI.h @@ -28,7 +28,7 @@ typedef struct { SemaphoreHandle_t mutex; #if CIRCUITPY_BUSIO_SPI_ASYNC - spi_transaction_t async_trans[2]; // queued by write_start, collected by write_end + spi_transaction_t async_trans[2]; // queued by write_start, collected by end uint8_t async_count; circuitpy_async_flag_t *async_done; #endif diff --git a/ports/raspberrypi/common-hal/busio/SPI.c b/ports/raspberrypi/common-hal/busio/SPI.c index 43107a44482..f4d86f538d7 100644 --- a/ports/raspberrypi/common-hal/busio/SPI.c +++ b/ports/raspberrypi/common-hal/busio/SPI.c @@ -105,7 +105,7 @@ void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { if (common_hal_busio_spi_deinited(self)) { return; } - common_hal_busio_spi_write_end(self); + common_hal_busio_spi_end(self); if (self->dma_kept) { dma_channel_unclaim(self->dma_tx); dma_channel_unclaim(self->dma_rx); @@ -284,7 +284,7 @@ bool common_hal_busio_spi_write(busio_spi_obj_t *self, void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data, size_t len, circuitpy_async_flag_t *done) { - common_hal_busio_spi_write_end(self); + common_hal_busio_spi_end(self); CIRCUITPY_ASYNC_FLAG_INIT(done); self->async_done = done; self->async_active = _start(self, data, len, &self->discard, 1, true); @@ -293,7 +293,7 @@ void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data } } -void common_hal_busio_spi_write_end(busio_spi_obj_t *self) { +void common_hal_busio_spi_end(busio_spi_obj_t *self) { if (self->async_active) { // No background tasks here: the caller holds the bus, and one of them may want it. _end(self, false); diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 4c4353e82e1..3678625455a 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -238,7 +238,7 @@ CFLAGS += -DCIRCUITPY_BUSIO_I2C=$(CIRCUITPY_BUSIO_I2C) CIRCUITPY_BUSIO_SPI ?= $(CIRCUITPY_BUSIO) CFLAGS += -DCIRCUITPY_BUSIO_SPI=$(CIRCUITPY_BUSIO_SPI) -# The port implements common_hal_busio_spi_write_start() and common_hal_busio_spi_write_end(), so a +# The port implements common_hal_busio_spi_write_start() and common_hal_busio_spi_end(), so a # display on a FourWire bus can send one strip while the next is being composited. CIRCUITPY_BUSIO_SPI_ASYNC ?= 0 CFLAGS += -DCIRCUITPY_BUSIO_SPI_ASYNC=$(CIRCUITPY_BUSIO_SPI_ASYNC) diff --git a/shared-bindings/busio/SPI.h b/shared-bindings/busio/SPI.h index 1750494ef9e..c9a7a09a2cb 100644 --- a/shared-bindings/busio/SPI.h +++ b/shared-bindings/busio/SPI.h @@ -38,13 +38,13 @@ extern bool common_hal_busio_spi_write(busio_spi_obj_t *self, const uint8_t *dat #if CIRCUITPY_BUSIO_SPI_ASYNC // Start writing data and return, possibly before it has been sent. *done is set once it has been -// sent, at the latest by common_hal_busio_spi_write_end(). data must stay valid and unchanged, and +// sent, at the latest by common_hal_busio_spi_end(). data must stay valid and unchanged, and // the bus must not be used, until then. extern void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data, size_t len, circuitpy_async_flag_t *done); -// Finish a write started by common_hal_busio_spi_write_start(), waiting if needed. Returns at -// once if there is none. -extern void common_hal_busio_spi_write_end(busio_spi_obj_t *self); +// Finish the transfer started by a common_hal_busio_spi_*_start(), waiting if needed. Returns +// at once if there is none. +extern void common_hal_busio_spi_end(busio_spi_obj_t *self); #endif // Reads in len bytes while outputting the byte write_value. diff --git a/shared-module/fourwire/FourWire.c b/shared-module/fourwire/FourWire.c index 33b83afbafe..f475b73ac9d 100644 --- a/shared-module/fourwire/FourWire.c +++ b/shared-module/fourwire/FourWire.c @@ -205,7 +205,7 @@ void common_hal_fourwire_fourwire_send_async(mp_obj_t obj, display_byte_type_t d void common_hal_fourwire_fourwire_flush(mp_obj_t obj) { fourwire_fourwire_obj_t *self = MP_OBJ_TO_PTR(obj); - common_hal_busio_spi_write_end(self->bus); + common_hal_busio_spi_end(self->bus); } #endif From 33f89f3331dc00ad7d29e70f8051e6f08474a4cd Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Thu, 1 Oct 2026 16:26:51 +0000 Subject: [PATCH 4/4] Move the async flag header to supervisor/shared py/ is kept for the core; circuitpy_async_flag.h becomes supervisor/shared/async_flag.h. No code changes. --- ports/atmel-samd/common-hal/busio/SPI.h | 2 +- ports/espressif/common-hal/busio/SPI.h | 2 +- ports/raspberrypi/common-hal/busio/SPI.h | 2 +- shared-bindings/busio/SPI.h | 2 +- py/circuitpy_async_flag.h => supervisor/shared/async_flag.h | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename py/circuitpy_async_flag.h => supervisor/shared/async_flag.h (100%) diff --git a/ports/atmel-samd/common-hal/busio/SPI.h b/ports/atmel-samd/common-hal/busio/SPI.h index ee7aa78c3ac..57acd5581df 100644 --- a/ports/atmel-samd/common-hal/busio/SPI.h +++ b/ports/atmel-samd/common-hal/busio/SPI.h @@ -15,7 +15,7 @@ #include "hal/include/hal_spi_m_sync.h" #include "py/obj.h" -#include "py/circuitpy_async_flag.h" +#include "supervisor/shared/async_flag.h" typedef struct { mp_obj_base_t base; diff --git a/ports/espressif/common-hal/busio/SPI.h b/ports/espressif/common-hal/busio/SPI.h index e611f2c1e61..37f1eba7f61 100644 --- a/ports/espressif/common-hal/busio/SPI.h +++ b/ports/espressif/common-hal/busio/SPI.h @@ -7,7 +7,7 @@ #pragma once #include "py/mpconfig.h" -#include "py/circuitpy_async_flag.h" +#include "supervisor/shared/async_flag.h" #include "driver/spi_master.h" #include "shared-bindings/microcontroller/Pin.h" diff --git a/ports/raspberrypi/common-hal/busio/SPI.h b/ports/raspberrypi/common-hal/busio/SPI.h index 25fc95b00dc..4acb15b7b2c 100644 --- a/ports/raspberrypi/common-hal/busio/SPI.h +++ b/ports/raspberrypi/common-hal/busio/SPI.h @@ -9,7 +9,7 @@ #include "common-hal/microcontroller/Pin.h" #include "py/obj.h" -#include "py/circuitpy_async_flag.h" +#include "supervisor/shared/async_flag.h" #include "hardware/spi.h" diff --git a/shared-bindings/busio/SPI.h b/shared-bindings/busio/SPI.h index c9a7a09a2cb..382c91240b2 100644 --- a/shared-bindings/busio/SPI.h +++ b/shared-bindings/busio/SPI.h @@ -7,7 +7,7 @@ #pragma once #include "py/obj.h" -#include "py/circuitpy_async_flag.h" +#include "supervisor/shared/async_flag.h" #include "common-hal/microcontroller/Pin.h" #include "common-hal/busio/SPI.h" diff --git a/py/circuitpy_async_flag.h b/supervisor/shared/async_flag.h similarity index 100% rename from py/circuitpy_async_flag.h rename to supervisor/shared/async_flag.h