diff --git a/ports/atmel-samd/common-hal/busio/SPI.c b/ports/atmel-samd/common-hal/busio/SPI.c index 19fd81e425d..d9f1ac13561 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_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_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_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..57acd5581df 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 "supervisor/shared/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..40ba60dab40 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_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_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_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_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..37f1eba7f61 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 "supervisor/shared/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 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..f4d86f538d7 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_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_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_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..4acb15b7b2c 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 "supervisor/shared/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_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..3678625455a 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_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..382c91240b2 100644 --- a/shared-bindings/busio/SPI.h +++ b/shared-bindings/busio/SPI.h @@ -7,6 +7,7 @@ #pragma once #include "py/obj.h" +#include "supervisor/shared/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_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 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. extern bool common_hal_busio_spi_read(busio_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value); 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..f475b73ac9d 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_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; diff --git a/supervisor/shared/async_flag.h b/supervisor/shared/async_flag.h new file mode 100644 index 00000000000..5dd2c726386 --- /dev/null +++ b/supervisor/shared/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