From c1d61d248eaa4a32d0969956aeb1f56604502bce Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Fri, 2 Oct 2026 20:58:57 +0000 Subject: [PATCH 01/27] shared/runtime: add buffer_slice() and use it in busio.SPI buffer_slice() turns a buffer object and element bounds into a byte slice, as busio.SPI's write(), readinto() and write_readinto() did inline. busio.SPI gets a little smaller, and async_spi will use it too. --- shared-bindings/busio/SPI.c | 64 ++++++++-------------------------- shared/runtime/buffer_helper.c | 13 +++++++ shared/runtime/buffer_helper.h | 6 ++++ 3 files changed, 33 insertions(+), 50 deletions(-) diff --git a/shared-bindings/busio/SPI.c b/shared-bindings/busio/SPI.c index 6ff74880266..84444c9b67c 100644 --- a/shared-bindings/busio/SPI.c +++ b/shared-bindings/busio/SPI.c @@ -9,7 +9,6 @@ #include -#include "py/binary.h" #include "py/mperrno.h" #include "py/objproperty.h" #include "py/runtime.h" @@ -258,23 +257,14 @@ static mp_obj_t busio_spi_write(size_t n_args, const mp_obj_t *pos_args, mp_map_ mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); - mp_buffer_info_t bufinfo; - mp_get_buffer_raise(args[ARG_buffer].u_obj, &bufinfo, MP_BUFFER_READ); - // Compute bounds in terms of elements, not bytes. - int stride_in_bytes = mp_binary_get_size('@', bufinfo.typecode, NULL); - int32_t start = args[ARG_start].u_int; - size_t length = bufinfo.len / stride_in_bytes; - normalize_buffer_bounds(&start, args[ARG_end].u_int, &length); - - // Treat start and length in terms of bytes from now on. - start *= stride_in_bytes; - length *= stride_in_bytes; - + size_t length; + uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int, + MP_BUFFER_READ, &length); if (length == 0) { return mp_const_none; } - bool ok = common_hal_busio_spi_write(self, ((uint8_t *)bufinfo.buf) + start, length); + bool ok = common_hal_busio_spi_write(self, data, length); if (!ok) { mp_raise_OSError(MP_EIO); } @@ -325,23 +315,14 @@ static mp_obj_t busio_spi_readinto(size_t n_args, const mp_obj_t *pos_args, mp_m mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); - mp_buffer_info_t bufinfo; - mp_get_buffer_raise(args[ARG_buffer].u_obj, &bufinfo, MP_BUFFER_WRITE); - // Compute bounds in terms of elements, not bytes. - int stride_in_bytes = mp_binary_get_size('@', bufinfo.typecode, NULL); - int32_t start = args[ARG_start].u_int; - size_t length = bufinfo.len / stride_in_bytes; - normalize_buffer_bounds(&start, args[ARG_end].u_int, &length); - - // Treat start and length in terms of bytes from now on. - start *= stride_in_bytes; - length *= stride_in_bytes; - + size_t length; + uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int, + MP_BUFFER_WRITE, &length); if (length == 0) { return mp_const_none; } - bool ok = common_hal_busio_spi_read(self, ((uint8_t *)bufinfo.buf) + start, length, args[ARG_write_value].u_int); + bool ok = common_hal_busio_spi_read(self, data, length, args[ARG_write_value].u_int); if (!ok) { mp_raise_OSError(MP_EIO); } @@ -402,25 +383,11 @@ static mp_obj_t busio_spi_write_readinto(size_t n_args, const mp_obj_t *pos_args mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); - mp_buffer_info_t buf_out_info; - mp_get_buffer_raise(args[ARG_out_buffer].u_obj, &buf_out_info, MP_BUFFER_READ); - int out_stride_in_bytes = mp_binary_get_size('@', buf_out_info.typecode, NULL); - int32_t out_start = args[ARG_out_start].u_int; - size_t out_length = buf_out_info.len / out_stride_in_bytes; - normalize_buffer_bounds(&out_start, args[ARG_out_end].u_int, &out_length); - - mp_buffer_info_t buf_in_info; - mp_get_buffer_raise(args[ARG_in_buffer].u_obj, &buf_in_info, MP_BUFFER_WRITE); - int in_stride_in_bytes = mp_binary_get_size('@', buf_in_info.typecode, NULL); - int32_t in_start = args[ARG_in_start].u_int; - size_t in_length = buf_in_info.len / in_stride_in_bytes; - normalize_buffer_bounds(&in_start, args[ARG_in_end].u_int, &in_length); - - // Treat start and length in terms of bytes from now on. - out_start *= out_stride_in_bytes; - out_length *= out_stride_in_bytes; - in_start *= in_stride_in_bytes; - in_length *= in_stride_in_bytes; + size_t out_length, in_length; + const uint8_t *data_out = buffer_slice(args[ARG_out_buffer].u_obj, args[ARG_out_start].u_int, + args[ARG_out_end].u_int, MP_BUFFER_READ, &out_length); + uint8_t *data_in = buffer_slice(args[ARG_in_buffer].u_obj, args[ARG_in_start].u_int, + args[ARG_in_end].u_int, MP_BUFFER_WRITE, &in_length); if (out_length != in_length) { mp_raise_ValueError(MP_ERROR_TEXT("buffer slices must be of equal length")); @@ -430,10 +397,7 @@ static mp_obj_t busio_spi_write_readinto(size_t n_args, const mp_obj_t *pos_args return mp_const_none; } - bool ok = common_hal_busio_spi_transfer(self, - ((uint8_t *)buf_out_info.buf) + out_start, - ((uint8_t *)buf_in_info.buf) + in_start, - out_length); + bool ok = common_hal_busio_spi_transfer(self, data_out, data_in, out_length); if (!ok) { mp_raise_OSError(MP_EIO); } diff --git a/shared/runtime/buffer_helper.c b/shared/runtime/buffer_helper.c index 29e486af2c9..dc743032f9d 100644 --- a/shared/runtime/buffer_helper.c +++ b/shared/runtime/buffer_helper.c @@ -6,6 +6,9 @@ #include "shared/runtime/buffer_helper.h" +#include "py/binary.h" +#include "py/runtime.h" + void normalize_buffer_bounds(int32_t *start, int32_t end, size_t *length) { if (end < 0) { end += *length; @@ -24,3 +27,13 @@ void normalize_buffer_bounds(int32_t *start, int32_t end, size_t *length) { *length = end - *start; } } + +uint8_t *buffer_slice(mp_obj_t buffer, int32_t start, int32_t end, mp_uint_t flags, size_t *len) { + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(buffer, &bufinfo, flags); + int stride_in_bytes = mp_binary_get_size('@', bufinfo.typecode, NULL); + size_t length = bufinfo.len / stride_in_bytes; + normalize_buffer_bounds(&start, end, &length); + *len = length * stride_in_bytes; + return (uint8_t *)bufinfo.buf + start * stride_in_bytes; +} diff --git a/shared/runtime/buffer_helper.h b/shared/runtime/buffer_helper.h index 6d6f43243db..4295354edbc 100644 --- a/shared/runtime/buffer_helper.h +++ b/shared/runtime/buffer_helper.h @@ -9,4 +9,10 @@ #include #include +#include "py/obj.h" + void normalize_buffer_bounds(int32_t *start, int32_t end, size_t *length); + +// buffer[start:end], with start and end in elements. Returns the slice's address and sets *len +// to its length in bytes. +uint8_t *buffer_slice(mp_obj_t buffer, int32_t start, int32_t end, mp_uint_t flags, size_t *len); From 4719ff9139989d21c69b71610e443b976ec9f74b Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Fri, 2 Oct 2026 20:58:59 +0000 Subject: [PATCH 02/27] raspberrypi: move the SPI driver to peripherals/spi.c busio.SPI's port code becomes rp2_spi_t and rp2_spi_*() in peripherals/spi.c, with common-hal/busio/SPI.c as a thin layer over it, so that another module can drive an SPI bus the same way. No change in behaviour. --- ports/raspberrypi/Makefile | 1 + ports/raspberrypi/common-hal/busio/SPI.c | 268 ++--------------- ports/raspberrypi/common-hal/busio/SPI.h | 22 +- .../raspberrypi/common-hal/picogame/Display.c | 2 +- ports/raspberrypi/peripherals/spi.c | 275 ++++++++++++++++++ ports/raspberrypi/peripherals/spi.h | 61 ++++ 6 files changed, 365 insertions(+), 264 deletions(-) create mode 100644 ports/raspberrypi/peripherals/spi.c create mode 100644 ports/raspberrypi/peripherals/spi.h diff --git a/ports/raspberrypi/Makefile b/ports/raspberrypi/Makefile index 7649e199aa8..1c304d1cd4a 100755 --- a/ports/raspberrypi/Makefile +++ b/ports/raspberrypi/Makefile @@ -595,6 +595,7 @@ SRC_C += \ audio_dma.c \ background.c \ peripherals/pins.c \ + peripherals/spi.c \ lib/crypto-algorithms/sha256.c \ lib/tinyusb/src/portable/raspberrypi/rp2040/dcd_rp2040.c \ lib/tinyusb/src/portable/raspberrypi/rp2040/rp2040_usb.c \ diff --git a/ports/raspberrypi/common-hal/busio/SPI.c b/ports/raspberrypi/common-hal/busio/SPI.c index f4d86f538d7..75dc44bebaa 100644 --- a/ports/raspberrypi/common-hal/busio/SPI.c +++ b/ports/raspberrypi/common-hal/busio/SPI.c @@ -6,146 +6,43 @@ #include "shared-bindings/busio/SPI.h" -#include "shared/runtime/interrupt_char.h" -#include "py/mperrno.h" #include "py/runtime.h" - -#include "supervisor/board.h" -#include "common-hal/microcontroller/Pin.h" #include "shared-bindings/microcontroller/Pin.h" -#include "hardware/dma.h" -#include "hardware/gpio.h" - -#define NO_INSTANCE 0xff - void common_hal_busio_spi_construct(busio_spi_obj_t *self, const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso, bool half_duplex) { - size_t instance_index = NO_INSTANCE; - - // Ensure the object starts in its deinit state. - common_hal_busio_spi_mark_deinit(self); - if (half_duplex) { mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_half_duplex); } - - if (clock->number % 4 == 2) { - instance_index = (clock->number / 8) % 2; - } - if (mosi != NULL) { - // Make sure the set MOSI matches the clock settings. - if (mosi->number % 4 != 3 || - (mosi->number / 8) % 2 != instance_index) { - instance_index = NO_INSTANCE; - } - } - if (miso != NULL) { - // Make sure the set MOSI matches the clock settings. - if (miso->number % 4 != 0 || - (miso->number / 8) % 2 != instance_index) { - instance_index = NO_INSTANCE; - } - } - - // TODO: Check to see if we're sharing the SPI with a native APA102. - - if (instance_index > 1) { + rp2_spi_error_t error = rp2_spi_construct(&self->spi, clock, mosi, miso); + if (error == RP2_SPI_INVALID_PINS) { raise_ValueError_invalid_pins(); } - - if (instance_index == 0) { - self->peripheral = spi0; - } else if (instance_index == 1) { - self->peripheral = spi1; - } - - if ((spi_get_hw(self->peripheral)->cr1 & SPI_SSPCR1_SSE_BITS) != 0) { + if (error == RP2_SPI_IN_USE) { 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); - - gpio_set_function(clock->number, GPIO_FUNC_SPI); - claim_pin(clock); - self->clock = clock; - - self->MOSI = mosi; - if (mosi != NULL) { - gpio_set_function(mosi->number, GPIO_FUNC_SPI); - claim_pin(mosi); - } - - self->MISO = miso; - if (miso != NULL) { - gpio_set_function(miso->number, GPIO_FUNC_SPI); - claim_pin(miso); - } } void common_hal_busio_spi_never_reset(busio_spi_obj_t *self) { - common_hal_never_reset_pin(self->clock); - common_hal_never_reset_pin(self->MOSI); - common_hal_never_reset_pin(self->MISO); + rp2_spi_never_reset(&self->spi); } bool common_hal_busio_spi_deinited(busio_spi_obj_t *self) { - return self->clock == NULL; + return rp2_spi_deinited(&self->spi); } void common_hal_busio_spi_mark_deinit(busio_spi_obj_t *self) { - self->clock = NULL; + self->spi.clock = NULL; } 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); - common_hal_reset_pin(self->MOSI); - common_hal_reset_pin(self->MISO); - - common_hal_busio_spi_mark_deinit(self); + rp2_spi_deinit(&self->spi); } bool common_hal_busio_spi_configure(busio_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, uint8_t bits) { - if (baudrate == self->target_frequency && - polarity == self->polarity && - phase == self->phase && - bits == self->bits) { - return true; - } - - spi_set_format(self->peripheral, bits, polarity, phase, SPI_MSB_FIRST); - - // Workaround to start with clock line high if polarity=1. The hw SPI peripheral does not do this - // automatically. See https://github.com/raspberrypi/pico-sdk/issues/868 and - // https://forums.raspberrypi.com/viewtopic.php?t=336142 - // TODO: scheduled to be be fixed in pico-sdk 1.5.0. - if (polarity) { - hw_clear_bits(&spi_get_hw(self->peripheral)->cr1, SPI_SSPCR1_SSE_BITS); // disable the SPI - hw_set_bits(&spi_get_hw(self->peripheral)->cr1, SPI_SSPCR1_SSE_BITS); // re-enable the SPI - } - - self->polarity = polarity; - self->phase = phase; - self->bits = bits; - self->target_frequency = baudrate; - self->real_frequency = spi_set_baudrate(self->peripheral, baudrate); - + rp2_spi_configure(&self->spi, baudrate, polarity, phase, bits); return true; } @@ -169,157 +66,42 @@ void common_hal_busio_spi_unlock(busio_spi_obj_t *self) { self->has_lock = false; } -// 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, bool keep_dma) { - size_t len = MAX(out_len, in_len); - // Only use DMA if both data buffers are in SRAM. Otherwise, we'll stall the DMA with PSRAM or flash cache misses. - 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(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(self->dma_tx, &c, - &spi_get_hw(self->peripheral)->dr, - data_out, - len, - false); - - 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(self->dma_rx, &c, - data_in, - &spi_get_hw(self->peripheral)->dr, - len, - false); - - dma_start_channel_mask((1u << self->dma_rx) | (1u << self->dma_tx)); - return true; - } - - // 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; - } - 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); - } +bool common_hal_busio_spi_write(busio_spi_obj_t *self, + const uint8_t *data, size_t len) { + uint32_t data_in; + rp2_spi_transfer(&self->spi, data, len, (uint8_t *)&data_in, MIN(len, 4)); + return true; } -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); - } +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; + rp2_spi_transfer(&self->spi, (const uint8_t *)&data_out, MIN(4, len), data, len); return true; } -bool common_hal_busio_spi_write(busio_spi_obj_t *self, - const uint8_t *data, size_t len) { - uint32_t data_in; - return _transfer(self, data, len, (uint8_t *)&data_in, MIN(len, 4)); +bool common_hal_busio_spi_transfer(busio_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len) { + rp2_spi_transfer(&self->spi, data_out, len, data_in, len); + return true; } 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); - } + rp2_spi_write_start(&self->spi, data, len, 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; - return _transfer(self, (const uint8_t *)&data_out, MIN(4, len), data, len); -} - -bool common_hal_busio_spi_transfer(busio_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len) { - return _transfer(self, data_out, len, data_in, len); + rp2_spi_end(&self->spi); } uint32_t common_hal_busio_spi_get_frequency(busio_spi_obj_t *self) { - return self->real_frequency; + return self->spi.real_frequency; } uint8_t common_hal_busio_spi_get_phase(busio_spi_obj_t *self) { - return self->phase; + return self->spi.phase; } uint8_t common_hal_busio_spi_get_polarity(busio_spi_obj_t *self) { - return self->polarity; + return self->spi.polarity; } diff --git a/ports/raspberrypi/common-hal/busio/SPI.h b/ports/raspberrypi/common-hal/busio/SPI.h index 4acb15b7b2c..2a3aecc74bf 100644 --- a/ports/raspberrypi/common-hal/busio/SPI.h +++ b/ports/raspberrypi/common-hal/busio/SPI.h @@ -6,29 +6,11 @@ #pragma once -#include "common-hal/microcontroller/Pin.h" - #include "py/obj.h" -#include "supervisor/shared/async_flag.h" - -#include "hardware/spi.h" +#include "peripherals/spi.h" typedef struct { mp_obj_base_t base; - spi_inst_t *peripheral; + rp2_spi_t spi; bool has_lock; - const mcu_pin_obj_t *clock; - const mcu_pin_obj_t *MOSI; - const mcu_pin_obj_t *MISO; - uint32_t target_frequency; - int32_t real_frequency; - 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/common-hal/picogame/Display.c b/ports/raspberrypi/common-hal/picogame/Display.c index 9c0fcd17011..551b2bc4808 100644 --- a/ports/raspberrypi/common-hal/picogame/Display.c +++ b/ports/raspberrypi/common-hal/picogame/Display.c @@ -40,7 +40,7 @@ void common_hal_picogame_display_construct(picogame_display_obj_t *self, MP_QSTR_display, MP_QSTR_FourWire, mp_obj_get_type(display->bus.bus)->name); } fourwire_fourwire_obj_t *fw = MP_OBJ_TO_PTR(display->bus.bus); - self->spi = fw->bus->peripheral; + self->spi = fw->bus->spi.peripheral; #if CIRCUITPY_PICOGAME_RGB444 // Tell the panel which pixel format we'll send (COLMOD). Asserting it here also recovers from diff --git a/ports/raspberrypi/peripherals/spi.c b/ports/raspberrypi/peripherals/spi.c new file mode 100644 index 00000000000..81243910a10 --- /dev/null +++ b/ports/raspberrypi/peripherals/spi.c @@ -0,0 +1,275 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2021 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "peripherals/spi.h" + +#include "py/mpconfig.h" +#include "shared-bindings/microcontroller/Pin.h" +#include "supervisor/background_callback.h" + +#include "hardware/dma.h" +#include "hardware/gpio.h" + +#define NO_INSTANCE 0xff + + +rp2_spi_error_t rp2_spi_construct(rp2_spi_t *self, const mcu_pin_obj_t *clock, + const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso) { + size_t instance_index = NO_INSTANCE; + + // Ensure the object starts in its deinit state. + self->clock = NULL; + + if (clock->number % 4 == 2) { + instance_index = (clock->number / 8) % 2; + } + if (mosi != NULL) { + // Make sure the set MOSI matches the clock settings. + if (mosi->number % 4 != 3 || + (mosi->number / 8) % 2 != instance_index) { + instance_index = NO_INSTANCE; + } + } + if (miso != NULL) { + // Make sure the set MOSI matches the clock settings. + if (miso->number % 4 != 0 || + (miso->number / 8) % 2 != instance_index) { + instance_index = NO_INSTANCE; + } + } + + // TODO: Check to see if we're sharing the SPI with a native APA102. + + if (instance_index > 1) { + return RP2_SPI_INVALID_PINS; + } + + if (instance_index == 0) { + self->peripheral = spi0; + } else if (instance_index == 1) { + self->peripheral = spi1; + } + + if ((spi_get_hw(self->peripheral)->cr1 & SPI_SSPCR1_SSE_BITS) != 0) { + return RP2_SPI_IN_USE; + } + + self->async_active = false; + self->dma_kept = false; + self->target_frequency = 250000; + self->real_frequency = spi_init(self->peripheral, self->target_frequency); + + gpio_set_function(clock->number, GPIO_FUNC_SPI); + claim_pin(clock); + self->clock = clock; + + self->MOSI = mosi; + if (mosi != NULL) { + gpio_set_function(mosi->number, GPIO_FUNC_SPI); + claim_pin(mosi); + } + + self->MISO = miso; + if (miso != NULL) { + gpio_set_function(miso->number, GPIO_FUNC_SPI); + claim_pin(miso); + } + return RP2_SPI_OK; +} + +void rp2_spi_never_reset(rp2_spi_t *self) { + common_hal_never_reset_pin(self->clock); + common_hal_never_reset_pin(self->MOSI); + common_hal_never_reset_pin(self->MISO); +} + + +void rp2_spi_deinit(rp2_spi_t *self) { + if (rp2_spi_deinited(self)) { + return; + } + 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); + common_hal_reset_pin(self->MOSI); + common_hal_reset_pin(self->MISO); + + self->clock = NULL; +} + +static void _apply(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, + uint8_t bits) { + if (baudrate == self->target_frequency && + polarity == self->polarity && + phase == self->phase && + bits == self->bits) { + return; + } + + spi_set_format(self->peripheral, bits, polarity, phase, SPI_MSB_FIRST); + + // Workaround to start with clock line high if polarity=1. The hw SPI peripheral does not do this + // automatically. See https://github.com/raspberrypi/pico-sdk/issues/868 and + // https://forums.raspberrypi.com/viewtopic.php?t=336142 + // TODO: scheduled to be be fixed in pico-sdk 1.5.0. + if (polarity) { + hw_clear_bits(&spi_get_hw(self->peripheral)->cr1, SPI_SSPCR1_SSE_BITS); // disable the SPI + hw_set_bits(&spi_get_hw(self->peripheral)->cr1, SPI_SSPCR1_SSE_BITS); // re-enable the SPI + } + + self->polarity = polarity; + self->phase = phase; + self->bits = bits; + self->target_frequency = baudrate; + self->real_frequency = spi_set_baudrate(self->peripheral, baudrate); +} + +void rp2_spi_configure(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, + uint8_t bits) { + // A running async transfer finishes with the old settings. + rp2_spi_end(self); + _apply(self, baudrate, polarity, phase, bits); +} + + +// 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. An async transfer passes its done flag, and keeps the DMA channels +// until deinit, for buses that send often. Returns whether DMA is running. +static bool _start(rp2_spi_t *self, + const uint8_t *data_out, size_t out_len, + uint8_t *data_in, size_t in_len, circuitpy_async_flag_t *done) { + size_t len = MAX(out_len, in_len); + // Only use DMA if both data buffers are in SRAM. Otherwise, we'll stall the DMA with PSRAM or flash cache misses. + 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 = done != NULL; + } 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(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(self->dma_tx, &c, + &spi_get_hw(self->peripheral)->dr, + data_out, + len, + false); + + 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(self->dma_rx, &c, + data_in, + &spi_get_hw(self->peripheral)->dr, + len, + false); + + dma_start_channel_mask((1u << self->dma_rx) | (1u << self->dma_tx)); + return true; + } + + // 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; + } + 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(rp2_spi_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); + } +} + +void rp2_spi_transfer(rp2_spi_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, NULL)) { + // TODO: We should idle here until we get a DMA interrupt or something else. + _end(self, true); + } +} + +// Start an async transfer; done is set when it has finished, here if it ran in software. +static __attribute__((noinline)) void _async_start(rp2_spi_t *self, const uint8_t *data_out, size_t out_len, + uint8_t *data_in, size_t in_len, circuitpy_async_flag_t *done) { + CIRCUITPY_ASYNC_FLAG_INIT(done); + self->async_done = done; + // An empty data side means there is nothing to send, whatever the one-byte side holds. + self->async_active = out_len != 0 && in_len != 0 && + _start(self, data_out, out_len, data_in, in_len, done); + if (!self->async_active) { + CIRCUITPY_ASYNC_FLAG_SET(done); + } +} + +void rp2_spi_write_start(rp2_spi_t *self, const uint8_t *data, size_t len, + circuitpy_async_flag_t *done) { + rp2_spi_end(self); + _async_start(self, data, len, &self->one_byte, 1, done); +} + + +void rp2_spi_end(rp2_spi_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); + } +} diff --git a/ports/raspberrypi/peripherals/spi.h b/ports/raspberrypi/peripherals/spi.h new file mode 100644 index 00000000000..f46e8d0e208 --- /dev/null +++ b/ports/raspberrypi/peripherals/spi.h @@ -0,0 +1,61 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2021 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +// An SPI bus of this port, shared by busio.SPI and async_spi.SPI. + +#pragma once + +#include "common-hal/microcontroller/Pin.h" +#include "supervisor/shared/async_flag.h" + +#include "hardware/spi.h" + +typedef struct { + spi_inst_t *peripheral; + const mcu_pin_obj_t *clock; + const mcu_pin_obj_t *MOSI; + const mcu_pin_obj_t *MISO; + uint32_t target_frequency; + int32_t real_frequency; + uint8_t polarity; + uint8_t phase; + uint8_t bits; + bool async_active; // an async 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 one_byte; // the one-byte side of an async write (RX) or read (TX) + circuitpy_async_flag_t *async_done; +} rp2_spi_t; + +typedef enum { + RP2_SPI_OK, + RP2_SPI_INVALID_PINS, // the pins do not make an SPI bus + RP2_SPI_IN_USE, // the peripheral is in use +} rp2_spi_error_t; + +rp2_spi_error_t rp2_spi_construct(rp2_spi_t *self, const mcu_pin_obj_t *clock, + const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso); +// Finishes a running async transfer first. +void rp2_spi_deinit(rp2_spi_t *self); +static inline bool rp2_spi_deinited(rp2_spi_t *self) { + return self->clock == NULL; +} +void rp2_spi_never_reset(rp2_spi_t *self); +// Waits for a running async transfer first. +void rp2_spi_configure(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, + uint8_t bits); + +// A blocking transfer. A one-byte side is sent repeatedly or overwritten. +void rp2_spi_transfer(rp2_spi_t *self, const uint8_t *data_out, size_t out_len, + uint8_t *data_in, size_t in_len); + +// An async transfer: done is set by rp2_spi_end() once the transfer has finished. The buffer +// must stay valid until then. +void rp2_spi_write_start(rp2_spi_t *self, const uint8_t *data, size_t len, + circuitpy_async_flag_t *done); +// Waits for the running async transfer, if any, and sets its done flag. +void rp2_spi_end(rp2_spi_t *self); From 36f48d07162a91830fa9476c1fa0e9921274681a Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Mon, 5 Oct 2026 21:36:10 +0000 Subject: [PATCH 03/27] raspberrypi: async SPI reads, full-duplex transfers, stop and configure rp2_spi_read_start() and rp2_spi_transfer_start() start the other two transfers the way rp2_spi_write_start() starts a write. rp2_spi_stop() stops a transfer, or finishes it if it is already done; it does nothing once the bus has started a later transfer. rp2_spi_configure_start() applies new settings once the running transfer has finished, and rp2_spi_deinit() stops a running transfer. With CIRCUITPY_ASYNC_SPI, a DMA_IRQ_0 handler, shared like the audio and rp2pio ones, sets the done flag when the RX channel finishes, so a waiter does not need to call rp2_spi_end() to find out. --- .../common-hal/microcontroller/__init__.c | 5 +- ports/raspberrypi/peripherals/spi.c | 155 ++++++++++++++++++ ports/raspberrypi/peripherals/spi.h | 26 ++- 3 files changed, 181 insertions(+), 5 deletions(-) diff --git a/ports/raspberrypi/common-hal/microcontroller/__init__.c b/ports/raspberrypi/common-hal/microcontroller/__init__.c index 3d8bd381f5e..f1018d0963b 100644 --- a/ports/raspberrypi/common-hal/microcontroller/__init__.c +++ b/ports/raspberrypi/common-hal/microcontroller/__init__.c @@ -27,8 +27,9 @@ // DMA interrupt (DMA_IRQ_n) allocation for this port: // -// DMA_IRQ_0 Shared. audiocore (audio_dma.c) and rp2pio (StateMachine.c) each -// register a shared handler with irq_add_shared_handler() and +// DMA_IRQ_0 Shared. audiocore (audio_dma.c), rp2pio (StateMachine.c) and, +// with async_spi, SPI (peripherals/spi.c) each register a shared +// handler with irq_add_shared_handler() and // service only their own DMA channels. Any new code that needs a // DMA completion interrupt should do the same: add a shared handler // on DMA_IRQ_0, check dma_hw->ints0, and acknowledge only its own diff --git a/ports/raspberrypi/peripherals/spi.c b/ports/raspberrypi/peripherals/spi.c index 81243910a10..2ee29682efd 100644 --- a/ports/raspberrypi/peripherals/spi.c +++ b/ports/raspberrypi/peripherals/spi.c @@ -12,9 +12,50 @@ #include "hardware/dma.h" #include "hardware/gpio.h" +#if CIRCUITPY_ASYNC_SPI +#include "hardware/irq.h" +#endif #define NO_INSTANCE 0xff +#if CIRCUITPY_ASYNC_SPI +// Done flag of the async transfer on each kept RX channel, set when that channel finishes. +static circuitpy_async_flag_t *async_flags[NUM_DMA_CHANNELS]; +// RX channels with the interrupt enabled. +static uint32_t async_irq_mask; + +// Shared DMA_IRQ_0 handler: acknowledges and services only our channels, like audio_dma and rp2pio. +static void __not_in_flash_func(spi_dma_irq_handler)(void) { + uint32_t pending = dma_hw->ints0 & async_irq_mask; + dma_hw->ints0 = pending; + for (uint chan = 0; pending != 0; chan++, pending >>= 1) { + if ((pending & 1) && async_flags[chan] != NULL) { + CIRCUITPY_ASYNC_FLAG_SET(async_flags[chan]); + } + } +} + +static void async_irq_enable(uint chan) { + dma_hw->ints0 = 1u << chan; + if (async_irq_mask == 0) { + irq_add_shared_handler(DMA_IRQ_0, spi_dma_irq_handler, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY); + } + async_irq_mask |= 1u << chan; + dma_irqn_set_channel_enabled(0, chan, true); + irq_set_enabled(DMA_IRQ_0, true); +} + +static void async_irq_disable(uint chan) { + dma_irqn_set_channel_enabled(0, chan, false); + async_irq_mask &= ~(1u << chan); + if (async_irq_mask == 0) { + irq_remove_handler(DMA_IRQ_0, spi_dma_irq_handler); + } + if (dma_hw->inte0 == 0) { + irq_set_enabled(DMA_IRQ_0, false); + } +} +#endif rp2_spi_error_t rp2_spi_construct(rp2_spi_t *self, const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso) { @@ -59,6 +100,9 @@ rp2_spi_error_t rp2_spi_construct(rp2_spi_t *self, const mcu_pin_obj_t *clock, self->async_active = false; self->dma_kept = false; + #if CIRCUITPY_ASYNC_SPI + self->idle_done = NULL; + #endif self->target_frequency = 250000; self->real_frequency = spi_init(self->peripheral, self->target_frequency); @@ -86,12 +130,28 @@ void rp2_spi_never_reset(rp2_spi_t *self) { common_hal_never_reset_pin(self->MISO); } +#if CIRCUITPY_ASYNC_SPI +static void _abort(rp2_spi_t *self); +static void _idle(rp2_spi_t *self); +#endif void rp2_spi_deinit(rp2_spi_t *self) { if (rp2_spi_deinited(self)) { return; } + #if CIRCUITPY_ASYNC_SPI + if (self->async_active) { + _abort(self); + CIRCUITPY_ASYNC_FLAG_SET(self->async_done); + } + _idle(self); + #else + rp2_spi_end(self); + #endif if (self->dma_kept) { + #if CIRCUITPY_ASYNC_SPI + async_irq_disable(self->dma_rx); + #endif dma_channel_unclaim(self->dma_tx); dma_channel_unclaim(self->dma_rx); self->dma_kept = false; @@ -139,6 +199,42 @@ void rp2_spi_configure(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uin _apply(self, baudrate, polarity, phase, bits); } +#if CIRCUITPY_ASYNC_SPI +// The bus is idle: apply the settings of a waiting configure() and wake it. +static void _idle(rp2_spi_t *self) { + if (self->idle_done != NULL) { + _apply(self, self->pending_baudrate, self->pending_polarity, self->pending_phase, + self->pending_bits); + CIRCUITPY_ASYNC_FLAG_SET(self->idle_done); + self->idle_done = NULL; + } +} + +void rp2_spi_configure_start(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, + uint8_t bits, circuitpy_async_flag_t *done) { + CIRCUITPY_ASYNC_FLAG_INIT(done); + if (!self->async_active) { + _apply(self, baudrate, polarity, phase, bits); + CIRCUITPY_ASYNC_FLAG_SET(done); + return; + } + // A later configure() replaces one still waiting. + if (self->idle_done != NULL) { + CIRCUITPY_ASYNC_FLAG_SET(self->idle_done); + } + self->pending_baudrate = baudrate; + self->pending_polarity = polarity; + self->pending_phase = phase; + self->pending_bits = bits; + self->idle_done = done; +} + +void rp2_spi_configure_cancel(rp2_spi_t *self, circuitpy_async_flag_t *done) { + if (self->idle_done == done) { + self->idle_done = NULL; + } +} +#endif // 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 @@ -157,6 +253,11 @@ static bool _start(rp2_spi_t *self, self->dma_tx = chan_tx; self->dma_rx = chan_rx; self->dma_kept = done != NULL; + #if CIRCUITPY_ASYNC_SPI + if (done != NULL) { + async_irq_enable(chan_rx); + } + #endif } else { // If we have claimed only one channel successfully, release it. if (chan_tx >= 0) { @@ -191,6 +292,10 @@ static bool _start(rp2_spi_t *self, len, false); + #if CIRCUITPY_ASYNC_SPI + // Before the start, so that a short transfer cannot finish unseen. + async_flags[self->dma_rx] = done; + #endif dma_start_channel_mask((1u << self->dma_rx) | (1u << self->dma_tx)); return true; } @@ -264,12 +369,62 @@ void rp2_spi_write_start(rp2_spi_t *self, const uint8_t *data, size_t len, _async_start(self, data, len, &self->one_byte, 1, done); } +#if CIRCUITPY_ASYNC_SPI +void rp2_spi_read_start(rp2_spi_t *self, uint8_t *data, size_t len, uint8_t write_value, + circuitpy_async_flag_t *done) { + // End the previous transfer first: an async write may still be receiving into one_byte. + rp2_spi_end(self); + self->one_byte = write_value; + _async_start(self, &self->one_byte, 1, data, len, done); +} + +void rp2_spi_transfer_start(rp2_spi_t *self, const uint8_t *data_out, uint8_t *data_in, + size_t len, circuitpy_async_flag_t *done) { + rp2_spi_end(self); + _async_start(self, data_out, len, data_in, len, done); +} + +// Stop the running DMA transfer without setting its done flag. +static void _abort(rp2_spi_t *self) { + async_flags[self->dma_rx] = NULL; + // Both channels at once; as dma_channel_abort(), the busy bits clear once in-flight + // transfers have retired. + dma_hw->abort = (1u << self->dma_tx) | (1u << self->dma_rx); + while ((dma_hw->ch[self->dma_tx].ctrl_trig | dma_hw->ch[self->dma_rx].ctrl_trig) & DMA_CH0_CTRL_TRIG_BUSY_BITS) { + } + self->async_active = false; + while (spi_is_busy(self->peripheral)) { + } + while (spi_is_readable(self->peripheral)) { + (void)spi_get_hw(self->peripheral)->dr; + } + spi_get_hw(self->peripheral)->icr = SPI_SSPICR_RORIC_BITS; + _idle(self); +} + +void rp2_spi_stop(rp2_spi_t *self, circuitpy_async_flag_t *done) { + if (!self->async_active || self->async_done != done) { + return; + } + if (CIRCUITPY_ASYNC_FLAG_IS_SET(done)) { + rp2_spi_end(self); + return; + } + _abort(self); +} +#endif void rp2_spi_end(rp2_spi_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; + #if CIRCUITPY_ASYNC_SPI + async_flags[self->dma_rx] = NULL; + #endif CIRCUITPY_ASYNC_FLAG_SET(self->async_done); + #if CIRCUITPY_ASYNC_SPI + _idle(self); + #endif } } diff --git a/ports/raspberrypi/peripherals/spi.h b/ports/raspberrypi/peripherals/spi.h index f46e8d0e208..9a0385767f9 100644 --- a/ports/raspberrypi/peripherals/spi.h +++ b/ports/raspberrypi/peripherals/spi.h @@ -29,6 +29,14 @@ typedef struct { uint8_t dma_rx; uint8_t one_byte; // the one-byte side of an async write (RX) or read (TX) circuitpy_async_flag_t *async_done; + #if CIRCUITPY_ASYNC_SPI + // A configure() waiting for the transfer to finish, and its settings. + circuitpy_async_flag_t *idle_done; + uint32_t pending_baudrate; + uint8_t pending_polarity; + uint8_t pending_phase; + uint8_t pending_bits; + #endif } rp2_spi_t; typedef enum { @@ -39,7 +47,7 @@ typedef enum { rp2_spi_error_t rp2_spi_construct(rp2_spi_t *self, const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso); -// Finishes a running async transfer first. +// Stops a running async transfer, and sets its done flag. void rp2_spi_deinit(rp2_spi_t *self); static inline bool rp2_spi_deinited(rp2_spi_t *self) { return self->clock == NULL; @@ -53,9 +61,21 @@ void rp2_spi_configure(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uin void rp2_spi_transfer(rp2_spi_t *self, const uint8_t *data_out, size_t out_len, uint8_t *data_in, size_t in_len); -// An async transfer: done is set by rp2_spi_end() once the transfer has finished. The buffer -// must stay valid until then. +// Async transfers: done is set once the transfer has finished, by the DMA interrupt with +// CIRCUITPY_ASYNC_SPI and in any case by rp2_spi_end(). The buffers must stay valid until then. void rp2_spi_write_start(rp2_spi_t *self, const uint8_t *data, size_t len, circuitpy_async_flag_t *done); +void rp2_spi_read_start(rp2_spi_t *self, uint8_t *data, size_t len, uint8_t write_value, + circuitpy_async_flag_t *done); +void rp2_spi_transfer_start(rp2_spi_t *self, const uint8_t *data_out, uint8_t *data_in, + size_t len, circuitpy_async_flag_t *done); // Waits for the running async transfer, if any, and sets its done flag. void rp2_spi_end(rp2_spi_t *self); +// Stops the transfer started with done, or finishes it if done is set. Does nothing for an +// earlier transfer, and does not allocate. +void rp2_spi_stop(rp2_spi_t *self, circuitpy_async_flag_t *done); +// Configures the bus once the running transfer, if any, has finished, and sets done then. +void rp2_spi_configure_start(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, + uint8_t bits, circuitpy_async_flag_t *done); +// Drops the waiting configure() started with done. +void rp2_spi_configure_cancel(rp2_spi_t *self, circuitpy_async_flag_t *done); From 4775be015564da2285c9e360eb012a27110fd296 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Fri, 2 Oct 2026 20:58:59 +0000 Subject: [PATCH 04/27] Add awaitable C functions CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj, n_args_min, fun) defines a method that returns an awaitable. As with a CPython coroutine, its arguments are stored and only parsed when it is awaited. While an operation runs, the task waits on asyncio's I/O queue: the awaitable is a stream that polls readable once the operation's done flag is set, possibly from an interrupt. Based on the awaitable in #11040. --- supervisor/shared/awaitable.c | 154 ++++++++++++++++++++++++++++++++++ supervisor/shared/awaitable.h | 72 ++++++++++++++++ supervisor/supervisor.mk | 5 ++ 3 files changed, 231 insertions(+) create mode 100644 supervisor/shared/awaitable.c create mode 100644 supervisor/shared/awaitable.h diff --git a/supervisor/shared/awaitable.c b/supervisor/shared/awaitable.c new file mode 100644 index 00000000000..45924ec8cf3 --- /dev/null +++ b/supervisor/shared/awaitable.c @@ -0,0 +1,154 @@ +// 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 + +#include + +#include "supervisor/shared/awaitable.h" +#include "py/mperrno.h" +#include "py/runtime.h" +#include "py/stream.h" + +#if MICROPY_PY_ASYNC_AWAIT + +#if MICROPY_PY_ASYNCIO +// Make the running asyncio task wait on the I/O queue until this awaitable polls readable. +// asyncio.core is found through sys.modules, because mp_asyncio_context in modasyncio.c is left +// pointing at the old heap after a soft reload. +static void asyncio_wait(mp_obj_t awaitable) { + mp_map_elem_t *core = mp_map_lookup(&MP_STATE_VM(mp_loaded_modules_dict).map, + MP_OBJ_NEW_QSTR(MP_QSTR_asyncio_dot_core), MP_MAP_LOOKUP); + if (core == NULL) { + return; + } + mp_obj_t cur_task = mp_load_attr(core->value, MP_QSTR_cur_task); + if (cur_task == mp_const_none) { + return; + } + mp_obj_t dest[4]; + mp_load_method(mp_load_attr(core->value, MP_QSTR__io_queue), MP_QSTR__enqueue, dest); + dest[2] = awaitable; + dest[3] = MP_OBJ_NEW_SMALL_INT(0); // wait to read + mp_call_method_n_kw(2, 0, dest); +} +#endif + +// ---- the async function ------------------------------------------------------ + +// Store the arguments; they are parsed by start() when the awaitable is first resumed. +static mp_obj_t async_fun_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) { + const circuitpy_async_fun_obj_t *fun = MP_OBJ_TO_PTR(self_in); + mp_arg_check_num_sig(n_args, n_kw, fun->sig); + size_t n = n_args + 2 * n_kw; + circuitpy_awaitable_obj_t *aw = mp_obj_malloc_var_with_finaliser(circuitpy_awaitable_obj_t, + args, mp_obj_t, n, &circuitpy_awaitable_type); + aw->fun = fun; + aw->context = NULL; + aw->state = CIRCUITPY_AWAITABLE_NEW; + aw->n_args = n_args; + aw->n_kw = n_kw; + memcpy(aw->args, args, n * sizeof(mp_obj_t)); + return MP_OBJ_FROM_PTR(aw); +} + +MP_DEFINE_CONST_OBJ_TYPE( + circuitpy_async_fun_type, + MP_QSTR_function, + MP_TYPE_FLAG_BINDS_SELF, + call, async_fun_call + ); + +// ---- the awaitable ----------------------------------------------------------- + +static void awaitable_cancel(circuitpy_awaitable_obj_t *self) { + if (self->state == CIRCUITPY_AWAITABLE_RUNNING) { + self->state = CIRCUITPY_AWAITABLE_FINISHED; + self->fun->cancel(self->context, &self->done); + } +} + +static mp_obj_t awaitable_iternext(mp_obj_t self_in) { + circuitpy_awaitable_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (self->state == CIRCUITPY_AWAITABLE_FINISHED) { + // Awaited again: like a finished generator, return None at once. + MP_STATE_THREAD(stop_iteration_arg) = mp_const_none; + return MP_OBJ_STOP_ITERATION; + } + if (self->state == CIRCUITPY_AWAITABLE_NEW) { + mp_map_t kw_args; + mp_map_init_fixed_table(&kw_args, self->n_kw, self->args + self->n_args); + CIRCUITPY_ASYNC_FLAG_INIT(&self->done); + // An exception from start() (bad arguments) propagates out of the await. + self->context = self->fun->start(&self->done, self->n_args, self->args, &kw_args); + self->state = CIRCUITPY_AWAITABLE_RUNNING; + } + if (!CIRCUITPY_ASYNC_FLAG_IS_SET(&self->done)) { + #if MICROPY_PY_ASYNCIO + asyncio_wait(self_in); + #endif + return mp_const_none; + } + self->state = CIRCUITPY_AWAITABLE_FINISHED; + MP_STATE_THREAD(stop_iteration_arg) = self->fun->end(self->context, &self->done); + return MP_OBJ_STOP_ITERATION; +} + +// A value sent in is ignored, as by CPython's awaitables written in C. +static mp_obj_t awaitable_send(mp_obj_t self_in, mp_obj_t value) { + mp_obj_t ret = awaitable_iternext(self_in); + if (ret == MP_OBJ_STOP_ITERATION) { + mp_raise_StopIteration(MP_STATE_THREAD(stop_iteration_arg)); + } + return ret; +} +static MP_DEFINE_CONST_FUN_OBJ_2(awaitable_send_obj, awaitable_send); + +// throw() is how asyncio cancels a task: stop the operation and raise the exception. +static mp_obj_t awaitable_throw(mp_obj_t self_in, mp_obj_t exc) { + awaitable_cancel(MP_OBJ_TO_PTR(self_in)); + nlr_raise(mp_make_raise_obj(exc)); +} +static MP_DEFINE_CONST_FUN_OBJ_2(awaitable_throw_obj, awaitable_throw); + +static mp_obj_t awaitable_close(mp_obj_t self_in) { + awaitable_cancel(MP_OBJ_TO_PTR(self_in)); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(awaitable_close_obj, awaitable_close); + +// Readable once the operation has finished. +static mp_uint_t awaitable_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) { + circuitpy_awaitable_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (request != MP_STREAM_POLL) { + *errcode = MP_EINVAL; + return MP_STREAM_ERROR; + } + return CIRCUITPY_ASYNC_FLAG_IS_SET(&self->done) ? (arg & MP_STREAM_POLL_RD) : 0; +} + +static const mp_stream_p_t awaitable_stream_p = { + .ioctl = awaitable_ioctl, +}; + +static const mp_rom_map_elem_t awaitable_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR___await__), MP_ROM_PTR(&mp_identity_obj) }, + { MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&awaitable_send_obj) }, + { MP_ROM_QSTR(MP_QSTR_throw), MP_ROM_PTR(&awaitable_throw_obj) }, + { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&awaitable_close_obj) }, + // Runs during GC: an operation still in flight must not write into freed memory. + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&awaitable_close_obj) }, +}; +static MP_DEFINE_CONST_DICT(awaitable_locals_dict, awaitable_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + circuitpy_awaitable_type, + MP_QSTR_coroutine, + MP_TYPE_FLAG_ITER_IS_ITERNEXT, + iter, awaitable_iternext, + protocol, &awaitable_stream_p, + locals_dict, &awaitable_locals_dict + ); + +#endif // MICROPY_PY_ASYNC_AWAIT diff --git a/supervisor/shared/awaitable.h b/supervisor/shared/awaitable.h new file mode 100644 index 00000000000..40084338f5b --- /dev/null +++ b/supervisor/shared/awaitable.h @@ -0,0 +1,72 @@ +// 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 + +// Awaitable functions implemented in C. +// +// CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj_name, n_args_min, fun_name) defines obj_name, a callable +// that takes the same arguments as an MP_DEFINE_CONST_FUN_OBJ_KW function but returns an +// awaitable. A call only checks the number of positional arguments; as with a coroutine in +// CPython, nothing runs and the arguments are parsed only when the awaitable is awaited. Then: +// +// void *fun_name(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, +// mp_map_t *kw_args) +// parses the arguments, starts the operation and returns a context. The operation sets +// *done, possibly from an interrupt, once it has finished. +// mp_obj_t common_hal__end(void *context, circuitpy_async_flag_t *done) +// finishes the operation once *done is set and returns the result of the await. +// void common_hal__cancel(void *context, circuitpy_async_flag_t *done) +// stops the operation, or finishes it if *done is already set. It must not allocate: it +// may run during GC. done tells which operation is meant, if the context has started +// another one since. +// +// While an operation runs, the awaiting asyncio task waits on asyncio's I/O queue: the awaitable +// is a stream that polls readable once *done is set. + +#pragma once + +#include "py/obj.h" +#include "supervisor/shared/async_flag.h" + +#if MICROPY_PY_ASYNC_AWAIT + +typedef void *(*circuitpy_awaitable_start_fn)(circuitpy_async_flag_t *done, size_t n_args, + const mp_obj_t *pos_args, mp_map_t *kw_args); +typedef mp_obj_t (*circuitpy_awaitable_end_fn)(void *context, circuitpy_async_flag_t *done); +typedef void (*circuitpy_awaitable_cancel_fn)(void *context, circuitpy_async_flag_t *done); + +// An async function: calling it returns a circuitpy_awaitable_obj_t. +typedef struct { + mp_obj_base_t base; + uint32_t sig; // as made by MP_OBJ_FUN_MAKE_SIG + circuitpy_awaitable_start_fn start; + circuitpy_awaitable_end_fn end; + circuitpy_awaitable_cancel_fn cancel; +} circuitpy_async_fun_obj_t; + +extern const mp_obj_type_t circuitpy_async_fun_type; + +typedef struct { + mp_obj_base_t base; + const circuitpy_async_fun_obj_t *fun; + void *context; // returned by start() + circuitpy_async_flag_t done; + uint8_t state; // CIRCUITPY_AWAITABLE_NEW, _RUNNING or _FINISHED + uint16_t n_args; + uint16_t n_kw; + mp_obj_t args[]; // positional arguments, then keyword name/value pairs +} circuitpy_awaitable_obj_t; + +extern const mp_obj_type_t circuitpy_awaitable_type; + +enum { CIRCUITPY_AWAITABLE_NEW, CIRCUITPY_AWAITABLE_RUNNING, CIRCUITPY_AWAITABLE_FINISHED }; + +#define CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj_name, n_args_min, fun_name) \ + const circuitpy_async_fun_obj_t obj_name = { \ + {&circuitpy_async_fun_type}, MP_OBJ_FUN_MAKE_SIG(n_args_min, MP_OBJ_FUN_ARGS_MAX, true), \ + fun_name, common_hal_##fun_name##_end, common_hal_##fun_name##_cancel \ + } + +#endif // MICROPY_PY_ASYNC_AWAIT diff --git a/supervisor/supervisor.mk b/supervisor/supervisor.mk index 2d881dfe070..80b7a585a78 100644 --- a/supervisor/supervisor.mk +++ b/supervisor/supervisor.mk @@ -27,6 +27,11 @@ ifeq ($(CIRCUITPY_LIB_TLSF),1) SRC_SUPERVISOR += lib/tlsf/tlsf.c endif +# Awaitable C functions, for the async_* modules. +ifeq ($(CIRCUITPY_ASYNC_SPI),1) +SRC_SUPERVISOR += supervisor/shared/awaitable.c +endif + # For tlsf CFLAGS += -D_DEBUG=0 From 457f592b24b882146a8d3ae77881034193eafbcb Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Mon, 5 Oct 2026 21:36:10 +0000 Subject: [PATCH 05/27] async_spi: SPI transfers for asyncio async_spi.SPI is configured like busio.SPI, and configure(), write(), readinto() and write_readinto() are awaited: other tasks run during a transfer. Tasks that share the bus can use an asyncio.Lock. Enabled on raspberrypi, where it drives the bus through peripherals/spi.c. A manual test is in tests/circuitpython-manual/async_spi. Co-authored-by: Marshal Horn <175229+kamocat@users.noreply.github.com> --- ports/raspberrypi/common-hal/async_spi/SPI.c | 73 +++++ ports/raspberrypi/common-hal/async_spi/SPI.h | 15 + .../common-hal/async_spi/__init__.c | 7 + ports/raspberrypi/mpconfigport.mk | 1 + py/circuitpy_defns.mk | 5 + py/circuitpy_mpconfig.mk | 4 + shared-bindings/async_spi/SPI.c | 277 ++++++++++++++++++ shared-bindings/async_spi/SPI.h | 56 ++++ shared-bindings/async_spi/__init__.c | 44 +++ .../async_spi/async_spi_loopback.py | 50 ++++ 10 files changed, 532 insertions(+) create mode 100644 ports/raspberrypi/common-hal/async_spi/SPI.c create mode 100644 ports/raspberrypi/common-hal/async_spi/SPI.h create mode 100644 ports/raspberrypi/common-hal/async_spi/__init__.c create mode 100644 shared-bindings/async_spi/SPI.c create mode 100644 shared-bindings/async_spi/SPI.h create mode 100644 shared-bindings/async_spi/__init__.c create mode 100644 tests/circuitpython-manual/async_spi/async_spi_loopback.py diff --git a/ports/raspberrypi/common-hal/async_spi/SPI.c b/ports/raspberrypi/common-hal/async_spi/SPI.c new file mode 100644 index 00000000000..b8cadeaac1a --- /dev/null +++ b/ports/raspberrypi/common-hal/async_spi/SPI.c @@ -0,0 +1,73 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/async_spi/SPI.h" + +#include "py/runtime.h" +#include "shared-bindings/microcontroller/Pin.h" + +void common_hal_async_spi_spi_construct(async_spi_spi_obj_t *self, const mcu_pin_obj_t *clock, + const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso) { + rp2_spi_error_t error = rp2_spi_construct(&self->spi, clock, mosi, miso); + if (error == RP2_SPI_INVALID_PINS) { + raise_ValueError_invalid_pins(); + } + if (error == RP2_SPI_IN_USE) { + mp_raise_ValueError(MP_ERROR_TEXT("SPI peripheral in use")); + } +} + +bool common_hal_async_spi_spi_deinited(async_spi_spi_obj_t *self) { + return rp2_spi_deinited(&self->spi); +} + +void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self) { + rp2_spi_deinit(&self->spi); +} + +void common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, + uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done) { + rp2_spi_configure_start(&self->spi, baudrate, polarity, phase, bits, done); +} + +mp_obj_t common_hal_async_spi_spi_configure_end(void *context, circuitpy_async_flag_t *done) { + return mp_const_none; +} + +void common_hal_async_spi_spi_configure_cancel(void *context, circuitpy_async_flag_t *done) { + async_spi_spi_obj_t *self = context; + rp2_spi_configure_cancel(&self->spi, done); +} + +uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self) { + return self->spi.real_frequency; +} + +void common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, + size_t len, circuitpy_async_flag_t *done) { + rp2_spi_write_start(&self->spi, data, len, done); +} + +void common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, + size_t len, uint8_t write_value, circuitpy_async_flag_t *done) { + rp2_spi_read_start(&self->spi, data, len, write_value, done); +} + +void common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, + const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done) { + rp2_spi_transfer_start(&self->spi, data_out, data_in, len, done); +} + +mp_obj_t common_hal_async_spi_spi_transfer_end(void *context, circuitpy_async_flag_t *done) { + async_spi_spi_obj_t *self = context; + rp2_spi_stop(&self->spi, done); + return mp_const_none; +} + +void common_hal_async_spi_spi_transfer_cancel(void *context, circuitpy_async_flag_t *done) { + async_spi_spi_obj_t *self = context; + rp2_spi_stop(&self->spi, done); +} diff --git a/ports/raspberrypi/common-hal/async_spi/SPI.h b/ports/raspberrypi/common-hal/async_spi/SPI.h new file mode 100644 index 00000000000..d8a7a05d2e6 --- /dev/null +++ b/ports/raspberrypi/common-hal/async_spi/SPI.h @@ -0,0 +1,15 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" +#include "peripherals/spi.h" + +typedef struct { + mp_obj_base_t base; + rp2_spi_t spi; +} async_spi_spi_obj_t; diff --git a/ports/raspberrypi/common-hal/async_spi/__init__.c b/ports/raspberrypi/common-hal/async_spi/__init__.c new file mode 100644 index 00000000000..59a4cec7fe9 --- /dev/null +++ b/ports/raspberrypi/common-hal/async_spi/__init__.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +// No async_spi module functions. diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index 5b7aa21029b..64ee62421b7 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -7,6 +7,7 @@ CIRCUITPY_WIFI_RADIO_SETTABLE_MAC_ADDRESS = 0 CIRCUITPY_RP2PIO ?= 1 CIRCUITPY_BUSIO_SPI_ASYNC ?= 1 +CIRCUITPY_ASYNC_SPI ?= $(call enable-if-all,$(MICROPY_PY_ASYNC_AWAIT) $(CIRCUITPY_BUSIO_SPI) $(CIRCUITPY_BUSIO_SPI_ASYNC)) # 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_defns.mk b/py/circuitpy_defns.mk index 85322ce61ee..6ddcfddcaa8 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -116,6 +116,9 @@ endif ifeq ($(CIRCUITPY_ANALOGIO),1) SRC_PATTERNS += analogio/% endif +ifeq ($(CIRCUITPY_ASYNC_SPI),1) +SRC_PATTERNS += async_spi/% +endif ifeq ($(CIRCUITPY_ATEXIT),1) SRC_PATTERNS += atexit/% endif @@ -526,6 +529,8 @@ SRC_COMMON_HAL_ALL = \ analogio/AnalogIn.c \ analogio/AnalogOut.c \ analogio/__init__.c \ + async_spi/SPI.c \ + async_spi/__init__.c \ audiobusio/I2SOut.c \ audiobusio/PDMIn.c \ audiobusio/__init__.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 702fceb5794..e99ea370e92 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -125,6 +125,10 @@ CFLAGS += -DCIRCUITPY_ANALOGIO=$(CIRCUITPY_ANALOGIO) CIRCUITPY_ARRAY ?= 1 CFLAGS += -DCIRCUITPY_ARRAY=$(CIRCUITPY_ARRAY) +# async_spi needs asyncio and a port with the async busio SPI reads (raspberrypi). +CIRCUITPY_ASYNC_SPI ?= 0 +CFLAGS += -DCIRCUITPY_ASYNC_SPI=$(CIRCUITPY_ASYNC_SPI) + CIRCUITPY_ATEXIT ?= $(CIRCUITPY_FULL_BUILD) CFLAGS += -DCIRCUITPY_ATEXIT=$(CIRCUITPY_ATEXIT) diff --git a/shared-bindings/async_spi/SPI.c b/shared-bindings/async_spi/SPI.c new file mode 100644 index 00000000000..75eda233189 --- /dev/null +++ b/shared-bindings/async_spi/SPI.c @@ -0,0 +1,277 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +#include + +#include "supervisor/shared/awaitable.h" +#include "py/objproperty.h" +#include "py/runtime.h" + +#include "shared/runtime/buffer_helper.h" +#include "shared/runtime/context_manager_helpers.h" +#include "shared-bindings/async_spi/SPI.h" +#include "shared-bindings/microcontroller/Pin.h" +#include "shared-bindings/util.h" + +//| class SPI: +//| """An SPI bus whose transfers are awaited +//| +//| ``write``, ``readinto`` and ``write_readinto`` return awaitables: the transfer runs while +//| other asyncio tasks run. Short transfers, and transfers that cannot use DMA (for example +//| from a buffer in flash), block other tasks until they finish. A transfer's arguments are +//| checked when the result is awaited. To share the bus between tasks, hold an +//| `asyncio.Lock` around each use.""" +//| +//| def __init__( +//| self, +//| clock: microcontroller.Pin, +//| MOSI: Optional[microcontroller.Pin] = None, +//| MISO: Optional[microcontroller.Pin] = None, +//| ) -> None: +//| """Construct an SPI object on the given pins. +//| +//| :param ~microcontroller.Pin clock: the pin to use for the clock. +//| :param ~microcontroller.Pin MOSI: the Main Out Selected In pin. +//| :param ~microcontroller.Pin MISO: the Main In Selected Out pin.""" +//| ... +//| +// The tables are busio.SPI's too, so they live here, where the arguments are documented. +const mp_arg_t async_spi_spi_make_new_args[] = { + { MP_QSTR_clock, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_MOSI, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_MISO, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_half_duplex, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, // busio.SPI only +}; +const mp_arg_t async_spi_spi_configure_args[] = { + { MP_QSTR_baudrate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 100000} }, + { MP_QSTR_polarity, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_phase, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_bits, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8} }, +}; +const mp_arg_t async_spi_spi_write_args[] = { + { MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, +}; +const mp_arg_t async_spi_spi_readinto_args[] = { + { MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, + { MP_QSTR_write_value, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, +}; +const mp_arg_t async_spi_spi_write_readinto_args[] = { + { MP_QSTR_out_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_in_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_out_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_out_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, + { MP_QSTR_in_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_in_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, +}; + +static mp_obj_t async_spi_spi_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_clock, ARG_MOSI, ARG_MISO }; + mp_arg_val_t args[3]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, 3, async_spi_spi_make_new_args, args); + + const mcu_pin_obj_t *clock = validate_obj_is_free_pin(args[ARG_clock].u_obj, MP_QSTR_clock); + const mcu_pin_obj_t *mosi = validate_obj_is_free_pin_or_none(args[ARG_MOSI].u_obj, MP_QSTR_MOSI); + const mcu_pin_obj_t *miso = validate_obj_is_free_pin_or_none(args[ARG_MISO].u_obj, MP_QSTR_MISO); + if (!miso && !mosi) { + mp_raise_ValueError(MP_ERROR_TEXT("Must provide MISO or MOSI pin")); + } + + async_spi_spi_obj_t *self = mp_obj_malloc_with_finaliser(async_spi_spi_obj_t, &async_spi_spi_type); + common_hal_async_spi_spi_construct(self, clock, mosi, miso); + return MP_OBJ_FROM_PTR(self); +} + +static MP_NOINLINE async_spi_spi_obj_t *checked_spi(mp_obj_t self_in) { + async_spi_spi_obj_t *self = MP_OBJ_TO_PTR(mp_arg_validate_type(self_in, &async_spi_spi_type, MP_QSTR_self)); + if (common_hal_async_spi_spi_deinited(self)) { + raise_deinited_error(); + } + return self; +} + +//| def deinit(self) -> None: +//| """Turn off the SPI bus. A transfer still running is stopped.""" +//| ... +//| +static mp_obj_t async_spi_spi_obj_deinit(mp_obj_t self_in) { + async_spi_spi_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_async_spi_spi_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(async_spi_spi_deinit_obj, async_spi_spi_obj_deinit); + +//| def __enter__(self) -> SPI: +//| """No-op used by Context Managers.""" +//| ... +//| +// Provided by context manager helper. + +//| def __exit__(self) -> None: +//| """Automatically deinitializes the hardware when exiting a context. See +//| :ref:`lifetime-and-contextmanagers` for more info.""" +//| ... +//| +// Provided by context manager helper. + +//| async def configure( +//| self, *, baudrate: int = 100000, polarity: int = 0, phase: int = 0, bits: int = 8 +//| ) -> None: +//| """Configure the SPI bus, as `busio.SPI.configure`. A transfer still running finishes +//| first, with the old settings.""" +//| ... +//| +static void *async_spi_spi_configure(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_baudrate, ARG_polarity, ARG_phase, ARG_bits }; + async_spi_spi_obj_t *self = checked_spi(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(async_spi_spi_configure_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(async_spi_spi_configure_args), async_spi_spi_configure_args, args); + + uint8_t polarity = (uint8_t)mp_arg_validate_int_range(args[ARG_polarity].u_int, 0, 1, MP_QSTR_polarity); + uint8_t phase = (uint8_t)mp_arg_validate_int_range(args[ARG_phase].u_int, 0, 1, MP_QSTR_phase); + uint8_t bits = (uint8_t)mp_arg_validate_int_range(args[ARG_bits].u_int, 8, 9, MP_QSTR_bits); + + common_hal_async_spi_spi_configure_start(self, args[ARG_baudrate].u_int, polarity, phase, bits, done); + return self; +} +static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_configure_obj, 1, async_spi_spi_configure); + +//| import sys +//| +//| async def write( +//| self, buffer: ReadableBuffer, *, start: int = 0, end: int = sys.maxsize +//| ) -> None: +//| """Write the data in ``buffer[start:end]``. +//| +//| :param ~circuitpython_typing.ReadableBuffer buffer: write out the data in this buffer +//| :param int start: beginning of buffer slice +//| :param int end: end of buffer slice; if not specified, use ``len(buffer)``""" +//| ... +//| +static void *async_spi_spi_write(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_buffer, ARG_start, ARG_end }; + async_spi_spi_obj_t *self = checked_spi(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(async_spi_spi_write_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(async_spi_spi_write_args), async_spi_spi_write_args, args); + + size_t len; + uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int, + MP_BUFFER_READ, &len); + common_hal_async_spi_spi_write_start(self, data, len, done); + return self; +} +static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_obj, 1, async_spi_spi_write); + +//| async def readinto( +//| self, +//| buffer: WriteableBuffer, +//| *, +//| start: int = 0, +//| end: int = sys.maxsize, +//| write_value: int = 0, +//| ) -> None: +//| """Read into ``buffer[start:end]`` while writing ``write_value`` for each byte read. +//| +//| :param ~circuitpython_typing.WriteableBuffer buffer: read data into this buffer +//| :param int start: beginning of buffer slice +//| :param int end: end of buffer slice; if not specified, use ``len(buffer)`` +//| :param int write_value: value to write while reading""" +//| ... +//| +static void *async_spi_spi_readinto(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_buffer, ARG_start, ARG_end, ARG_write_value }; + async_spi_spi_obj_t *self = checked_spi(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(async_spi_spi_readinto_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(async_spi_spi_readinto_args), async_spi_spi_readinto_args, args); + + size_t len; + uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int, + MP_BUFFER_WRITE, &len); + common_hal_async_spi_spi_readinto_start(self, data, len, (uint8_t)args[ARG_write_value].u_int, done); + return self; +} +static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_readinto_obj, 1, async_spi_spi_readinto); + +//| async def write_readinto( +//| self, +//| out_buffer: ReadableBuffer, +//| in_buffer: WriteableBuffer, +//| *, +//| out_start: int = 0, +//| out_end: int = sys.maxsize, +//| in_start: int = 0, +//| in_end: int = sys.maxsize, +//| ) -> None: +//| """Write out the data in ``out_buffer`` while simultaneously reading data into ``in_buffer``. +//| The two slices must have the same length. +//| +//| :param ~circuitpython_typing.ReadableBuffer out_buffer: write out the data in this buffer +//| :param ~circuitpython_typing.WriteableBuffer in_buffer: read data into this buffer +//| :param int out_start: beginning of ``out_buffer`` slice +//| :param int out_end: end of ``out_buffer`` slice; if not specified, use ``len(out_buffer)`` +//| :param int in_start: beginning of ``in_buffer`` slice +//| :param int in_end: end of ``in_buffer`` slice; if not specified, use ``len(in_buffer)``""" +//| ... +//| +static void *async_spi_spi_write_readinto(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_out_buffer, ARG_in_buffer, ARG_out_start, ARG_out_end, ARG_in_start, ARG_in_end }; + async_spi_spi_obj_t *self = checked_spi(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(async_spi_spi_write_readinto_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(async_spi_spi_write_readinto_args), async_spi_spi_write_readinto_args, args); + + size_t out_len, in_len; + const uint8_t *data_out = buffer_slice(args[ARG_out_buffer].u_obj, args[ARG_out_start].u_int, + args[ARG_out_end].u_int, MP_BUFFER_READ, &out_len); + uint8_t *data_in = buffer_slice(args[ARG_in_buffer].u_obj, args[ARG_in_start].u_int, + args[ARG_in_end].u_int, MP_BUFFER_WRITE, &in_len); + if (out_len != in_len) { + mp_raise_ValueError(MP_ERROR_TEXT("buffer slices must be of equal length")); + } + common_hal_async_spi_spi_write_readinto_start(self, data_out, data_in, out_len, done); + return self; +} +static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_readinto_obj, 2, async_spi_spi_write_readinto); + +//| frequency: int +//| """The actual SPI bus frequency. This may not match the frequency requested +//| due to internal limitations.""" +//| +//| +static mp_obj_t async_spi_spi_get_frequency(mp_obj_t self_in) { + async_spi_spi_obj_t *self = checked_spi(self_in); + return MP_OBJ_NEW_SMALL_INT(common_hal_async_spi_spi_get_frequency(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(async_spi_spi_get_frequency_obj, async_spi_spi_get_frequency); + +MP_PROPERTY_GETTER(async_spi_spi_frequency_obj, + (mp_obj_t)&async_spi_spi_get_frequency_obj); + +static const mp_rom_map_elem_t async_spi_spi_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&async_spi_spi_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&async_spi_spi_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, + { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) }, + + { MP_ROM_QSTR(MP_QSTR_configure), MP_ROM_PTR(&async_spi_spi_configure_obj) }, + + { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&async_spi_spi_write_obj) }, + { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&async_spi_spi_readinto_obj) }, + { MP_ROM_QSTR(MP_QSTR_write_readinto), MP_ROM_PTR(&async_spi_spi_write_readinto_obj) }, + + { MP_ROM_QSTR(MP_QSTR_frequency), MP_ROM_PTR(&async_spi_spi_frequency_obj) }, +}; +static MP_DEFINE_CONST_DICT(async_spi_spi_locals_dict, async_spi_spi_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + async_spi_spi_type, + MP_QSTR_SPI, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, async_spi_spi_make_new, + locals_dict, &async_spi_spi_locals_dict + ); diff --git a/shared-bindings/async_spi/SPI.h b/shared-bindings/async_spi/SPI.h new file mode 100644 index 00000000000..ee927cecfce --- /dev/null +++ b/shared-bindings/async_spi/SPI.h @@ -0,0 +1,56 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" +#include "py/runtime.h" +#include "common-hal/async_spi/SPI.h" +#include "common-hal/microcontroller/Pin.h" +#include "supervisor/shared/async_flag.h" + +extern const mp_obj_type_t async_spi_spi_type; + +// Argument tables, shared with busio.SPI, which takes the same arguments. +extern const mp_arg_t async_spi_spi_make_new_args[4]; // clock, MOSI, MISO, half_duplex +extern const mp_arg_t async_spi_spi_configure_args[4]; +extern const mp_arg_t async_spi_spi_write_args[3]; +extern const mp_arg_t async_spi_spi_readinto_args[4]; +extern const mp_arg_t async_spi_spi_write_readinto_args[6]; + +void common_hal_async_spi_spi_construct(async_spi_spi_obj_t *self, const mcu_pin_obj_t *clock, + const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso); +bool common_hal_async_spi_spi_deinited(async_spi_spi_obj_t *self); +// Stops a running transfer. +void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self); +uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self); + +// configure(): done is set once the running transfer, if any, has finished and the settings +// are in place. The context is the SPI object. +void common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, + uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done); +mp_obj_t common_hal_async_spi_spi_configure_end(void *context, circuitpy_async_flag_t *done); +void common_hal_async_spi_spi_configure_cancel(void *context, circuitpy_async_flag_t *done); + +// Each transfer start sets done once the transfer has finished, possibly from an interrupt. The +// buffers must stay valid until then. transfer_end() and transfer_cancel() take the SPI object +// as the context and act only on the transfer started with done. +void common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, + size_t len, circuitpy_async_flag_t *done); +void common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, + size_t len, uint8_t write_value, circuitpy_async_flag_t *done); +void common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, + const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done); +mp_obj_t common_hal_async_spi_spi_transfer_end(void *context, circuitpy_async_flag_t *done); +void common_hal_async_spi_spi_transfer_cancel(void *context, circuitpy_async_flag_t *done); + +// The names CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW looks up; all three transfers end the same way. +#define common_hal_async_spi_spi_write_end common_hal_async_spi_spi_transfer_end +#define common_hal_async_spi_spi_write_cancel common_hal_async_spi_spi_transfer_cancel +#define common_hal_async_spi_spi_readinto_end common_hal_async_spi_spi_transfer_end +#define common_hal_async_spi_spi_readinto_cancel common_hal_async_spi_spi_transfer_cancel +#define common_hal_async_spi_spi_write_readinto_end common_hal_async_spi_spi_transfer_end +#define common_hal_async_spi_spi_write_readinto_cancel common_hal_async_spi_spi_transfer_cancel diff --git a/shared-bindings/async_spi/__init__.c b/shared-bindings/async_spi/__init__.c new file mode 100644 index 00000000000..9d6a1d0da53 --- /dev/null +++ b/shared-bindings/async_spi/__init__.c @@ -0,0 +1,44 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +#include "py/obj.h" +#include "py/runtime.h" + +#include "shared-bindings/async_spi/SPI.h" + +//| """SPI transfers for asyncio +//| +//| `async_spi.SPI` is an SPI bus whose transfers are awaited. While a transfer runs, other +//| asyncio tasks run too. +//| +//| .. code-block:: python +//| +//| import asyncio +//| import board +//| import async_spi +//| +//| async def main(): +//| spi = async_spi.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) +//| await spi.configure(baudrate=8_000_000) +//| data = bytearray(512) +//| await spi.readinto(data) +//| +//| asyncio.run(main()) +//| """ + +static const mp_rom_map_elem_t async_spi_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_async_spi) }, + { MP_ROM_QSTR(MP_QSTR_SPI), MP_ROM_PTR(&async_spi_spi_type) }, +}; + +static MP_DEFINE_CONST_DICT(async_spi_module_globals, async_spi_module_globals_table); + +const mp_obj_module_t async_spi_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&async_spi_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_async_spi, async_spi_module); diff --git a/tests/circuitpython-manual/async_spi/async_spi_loopback.py b/tests/circuitpython-manual/async_spi/async_spi_loopback.py new file mode 100644 index 00000000000..fe5379005a6 --- /dev/null +++ b/tests/circuitpython-manual/async_spi/async_spi_loopback.py @@ -0,0 +1,50 @@ +"""async_spi transfers run while other asyncio tasks run, and can be cancelled. + +Wiring (Raspberry Pi Pico / Pico 2): SPI1 on GP10 (SCK), GP11 (MOSI), GP12 (MISO), +with GP11 jumpered to GP12. Needs the asyncio library in CIRCUITPY/lib. +""" + +import asyncio + +import async_spi +import board + + +async def count(counter): + while True: + counter[0] += 1 + await asyncio.sleep(0) + + +async def main(): + with async_spi.SPI(board.GP10, MOSI=board.GP11, MISO=board.GP12) as spi: + await spi.configure(baudrate=1_000_000) + out = bytes(i * 7 & 0xFF for i in range(4096)) + inb = bytearray(len(out)) + + # Another task runs while the transfer is on the wire, and the data comes back. + counter = [0] + counter_task = asyncio.create_task(count(counter)) + await spi.write_readinto(out, inb) + counter_task.cancel() + print("other task ran", counter[0], "times during a 33 ms transfer") + assert counter[0] > 0 + assert inb == out + + await spi.readinto(inb, end=100, write_value=0xA5) + assert inb[:100] == b"\xa5" * 100 + + # A cancelled transfer stops, and the bus works afterwards. + task = asyncio.create_task(spi.write(out)) + await asyncio.sleep(0.005) + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + await spi.write_readinto(out, inb) + assert inb == out + print("PASS") + + +asyncio.run(main()) From 4f18be77181c6ddb3fb887cfdb1f05177a4d8245 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Mon, 5 Oct 2026 21:36:10 +0000 Subject: [PATCH 06/27] busio: reuse async_spi's argument tables busio.SPI takes the same arguments as async_spi.SPI, so where that module exists its tables are used. Saves 160 bytes on RP2040. --- shared-bindings/busio/SPI.c | 106 ++++++++++++++++++++++-------------- shared-bindings/busio/SPI.h | 1 + 2 files changed, 66 insertions(+), 41 deletions(-) diff --git a/shared-bindings/busio/SPI.c b/shared-bindings/busio/SPI.c index 84444c9b67c..331db96df4e 100644 --- a/shared-bindings/busio/SPI.c +++ b/shared-bindings/busio/SPI.c @@ -14,6 +14,15 @@ #include "py/runtime.h" #include "shared-bindings/microcontroller/Pin.h" #include "shared-bindings/busio/SPI.h" +#if CIRCUITPY_ASYNC_SPI +// async_spi.SPI takes the same arguments, so its tables are used. +#include "shared-bindings/async_spi/SPI.h" +#define busio_spi_make_new_args async_spi_spi_make_new_args +#define busio_spi_configure_args async_spi_spi_configure_args +#define busio_spi_write_args async_spi_spi_write_args +#define busio_spi_readinto_args async_spi_spi_readinto_args +#define busio_spi_write_readinto_args async_spi_spi_write_readinto_args +#endif #include "shared-bindings/util.h" #include "shared/runtime/buffer_helper.h" #include "shared/runtime/context_manager_helpers.h" @@ -85,17 +94,20 @@ // TODO(tannewt): Support LSB SPI. +#if !CIRCUITPY_ASYNC_SPI +static const mp_arg_t busio_spi_make_new_args[] = { + { MP_QSTR_clock, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_MOSI, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_MISO, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_half_duplex, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, +}; +#endif + static mp_obj_t busio_spi_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { #if CIRCUITPY_BUSIO_SPI enum { ARG_clock, ARG_MOSI, ARG_MISO, ARG_half_duplex }; - static const mp_arg_t allowed_args[] = { - { MP_QSTR_clock, MP_ARG_REQUIRED | MP_ARG_OBJ }, - { MP_QSTR_MOSI, MP_ARG_OBJ, {.u_obj = mp_const_none} }, - { MP_QSTR_MISO, MP_ARG_OBJ, {.u_obj = mp_const_none} }, - { MP_QSTR_half_duplex, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, - }; - mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; - mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_make_new_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(busio_spi_make_new_args), busio_spi_make_new_args, args); const mcu_pin_obj_t *clock = validate_obj_is_free_pin(args[ARG_clock].u_obj, MP_QSTR_clock); const mcu_pin_obj_t *mosi = validate_obj_is_free_pin_or_none(args[ARG_MOSI].u_obj, MP_QSTR_mosi); @@ -174,19 +186,22 @@ static void check_for_deinit(busio_spi_obj_t *self) { //| ... //| +#if !CIRCUITPY_ASYNC_SPI +static const mp_arg_t busio_spi_configure_args[] = { + { MP_QSTR_baudrate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 100000} }, + { MP_QSTR_polarity, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_phase, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_bits, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8} }, +}; +#endif + static mp_obj_t busio_spi_configure(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_baudrate, ARG_polarity, ARG_phase, ARG_bits }; - static const mp_arg_t allowed_args[] = { - { MP_QSTR_baudrate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 100000} }, - { MP_QSTR_polarity, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_phase, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_bits, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8} }, - }; busio_spi_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); check_for_deinit(self); check_lock(self); - mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; - mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_configure_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_configure_args), busio_spi_configure_args, args); uint8_t polarity = (uint8_t)mp_arg_validate_int_range(args[ARG_polarity].u_int, 0, 1, MP_QSTR_polarity); uint8_t phase = (uint8_t)mp_arg_validate_int_range(args[ARG_phase].u_int, 0, 1, MP_QSTR_phase); @@ -244,18 +259,21 @@ MP_DEFINE_CONST_FUN_OBJ_1(busio_spi_unlock_obj, busio_spi_obj_unlock); //| ... //| +#if !CIRCUITPY_ASYNC_SPI +static const mp_arg_t busio_spi_write_args[] = { + { MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, +}; +#endif + static mp_obj_t busio_spi_write(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_buffer, ARG_start, ARG_end }; - static const mp_arg_t allowed_args[] = { - { MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, - { MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, - }; busio_spi_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); check_for_deinit(self); check_lock(self); - mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; - mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_write_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_write_args), busio_spi_write_args, args); size_t length; uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int, @@ -301,19 +319,22 @@ MP_DEFINE_CONST_FUN_OBJ_KW(busio_spi_write_obj, 1, busio_spi_write); //| ... //| +#if !CIRCUITPY_ASYNC_SPI +static const mp_arg_t busio_spi_readinto_args[] = { + { MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, + { MP_QSTR_write_value, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, +}; +#endif + static mp_obj_t busio_spi_readinto(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_buffer, ARG_start, ARG_end, ARG_write_value }; - static const mp_arg_t allowed_args[] = { - { MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, - { MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, - { MP_QSTR_write_value, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - }; busio_spi_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); check_for_deinit(self); check_lock(self); - mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; - mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_readinto_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_readinto_args), busio_spi_readinto_args, args); size_t length; uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int, @@ -367,21 +388,24 @@ MP_DEFINE_CONST_FUN_OBJ_KW(busio_spi_readinto_obj, 1, busio_spi_readinto); //| ... //| +#if !CIRCUITPY_ASYNC_SPI +static const mp_arg_t busio_spi_write_readinto_args[] = { + { MP_QSTR_out_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_in_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_out_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_out_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, + { MP_QSTR_in_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_in_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, +}; +#endif + static mp_obj_t busio_spi_write_readinto(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_out_buffer, ARG_in_buffer, ARG_out_start, ARG_out_end, ARG_in_start, ARG_in_end }; - static const mp_arg_t allowed_args[] = { - { MP_QSTR_out_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, - { MP_QSTR_in_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, - { MP_QSTR_out_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_out_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, - { MP_QSTR_in_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_in_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, - }; busio_spi_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); check_for_deinit(self); check_lock(self); - mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; - mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_write_readinto_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_write_readinto_args), busio_spi_write_readinto_args, args); size_t out_length, in_length; const uint8_t *data_out = buffer_slice(args[ARG_out_buffer].u_obj, args[ARG_out_start].u_int, diff --git a/shared-bindings/busio/SPI.h b/shared-bindings/busio/SPI.h index 382c91240b2..fa389131ee4 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/runtime.h" #include "supervisor/shared/async_flag.h" #include "common-hal/microcontroller/Pin.h" From 8bbdf9bbfb62a0f3fc19166880c3fc5fe9e0a76b Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Sat, 3 Oct 2026 06:48:10 +0000 Subject: [PATCH 07/27] zephyr-cp: async_spi Transfers use spi_transceive_cb() where the driver has an async path, and otherwise run on the system work queue. Zephyr cannot stop a transfer, so cancel waits for it. Enabled on boards with an SPI bus; tested on native_sim through the loopback device. --- .../native/native_sim/autogen_board_info.toml | 1 + ports/zephyr-cp/common-hal/async_spi/SPI.c | 216 ++++++++++++++++++ ports/zephyr-cp/common-hal/async_spi/SPI.h | 34 +++ .../zephyr-cp/common-hal/async_spi/__init__.c | 7 + ports/zephyr-cp/cptools/zephyr2cp.py | 2 + ports/zephyr-cp/tests/test_async_spi.py | 105 +++++++++ 6 files changed, 365 insertions(+) create mode 100644 ports/zephyr-cp/common-hal/async_spi/SPI.c create mode 100644 ports/zephyr-cp/common-hal/async_spi/SPI.h create mode 100644 ports/zephyr-cp/common-hal/async_spi/__init__.c create mode 100644 ports/zephyr-cp/tests/test_async_spi.py diff --git a/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml b/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml index fb371ac6da4..728daaebc38 100644 --- a/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml @@ -14,6 +14,7 @@ aesio = true alarm = false analogbufio = false analogio = true # Zephyr board has analogio +async_spi = true # Zephyr board has async_spi atexit = false audiobusio = true # Zephyr board has audiobusio audiocore = true # Zephyr board has audiobusio diff --git a/ports/zephyr-cp/common-hal/async_spi/SPI.c b/ports/zephyr-cp/common-hal/async_spi/SPI.c new file mode 100644 index 00000000000..aa094bf123b --- /dev/null +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.c @@ -0,0 +1,216 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/async_spi/SPI.h" + +#include + +#include "py/runtime.h" +#include "supervisor/port.h" +#include "bindings/zephyr_kernel/__init__.h" + +#include +#include + +// Settings take the other config slot: drivers see a change by the pointer. +static void apply_config(async_spi_spi_obj_t *self, uint32_t frequency, uint16_t operation) { + struct spi_config *current = &self->config[self->active_config]; + if (current->frequency == frequency && current->operation == operation) { + return; + } + self->active_config = 1 - self->active_config; + self->config[self->active_config].frequency = frequency; + self->config[self->active_config].operation = operation; +} + +static void transfer_done(const struct device *dev, int result, void *userdata) { + ARG_UNUSED(dev); + ARG_UNUSED(result); + async_spi_spi_obj_t *self = userdata; + CIRCUITPY_ASYNC_FLAG_SET(self->done); + // A configure() that waited for this transfer. + if (self->idle_done != NULL) { + apply_config(self, self->pending_frequency, self->pending_operation); + CIRCUITPY_ASYNC_FLAG_SET(self->idle_done); + self->idle_done = NULL; + } + port_wake_main_task_from_isr(); +} + +// Zephyr cannot stop a transfer, so this waits for it; deinit() waits too. Does not allocate. +static void wait_done(async_spi_spi_obj_t *self) { + if (self->done == NULL) { + return; + } + while (!CIRCUITPY_ASYNC_FLAG_IS_SET(self->done)) { + RUN_BACKGROUND_TASKS; + } + self->done = NULL; +} + +void common_hal_async_spi_spi_construct(async_spi_spi_obj_t *self, const mcu_pin_obj_t *clock, + const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso) { + const struct device *dev = NULL; + int ret = iobroker_spi_allocate(clock->package_pin, + mosi != NULL ? mosi->package_pin : IOBROKER_NO_PIN, + miso != NULL ? miso->package_pin : IOBROKER_NO_PIN, &dev); + if (ret < 0) { + if (ret == -ENODEV) { + mp_raise_ValueError(MP_ERROR_TEXT("All SPI peripherals are in use")); + } + if (ret == -EBUSY) { + mp_raise_ValueError(MP_ERROR_TEXT("Internal resource(s) in use")); + } + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Use device tree to define %q devices"), MP_QSTR_SPI); + } + + self->spi_device = dev; + self->clock = clock; + self->mosi = mosi; + self->miso = miso; + self->active_config = 0; + for (size_t i = 0; i < 2; i++) { + self->config[i].frequency = 100000; + self->config[i].operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_LINES_SINGLE; + } + self->done = NULL; + self->fill = NULL; + self->idle_done = NULL; + + // A routed device is initialized now; a fixed one already was (-EALREADY). + int init_ret = device_init(dev); + if (init_ret < 0 && init_ret != -EALREADY) { + common_hal_async_spi_spi_deinit(self); + raise_zephyr_error(init_ret); + } +} + +bool common_hal_async_spi_spi_deinited(async_spi_spi_obj_t *self) { + return self->spi_device == NULL; +} + +void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self) { + if (common_hal_async_spi_spi_deinited(self)) { + return; + } + wait_done(self); + (void)iobroker_release(self->spi_device); + self->spi_device = NULL; + self->clock = NULL; + self->mosi = NULL; + self->miso = NULL; +} + +static uint16_t operation_of(uint8_t polarity, uint8_t phase, uint8_t bits) { + uint16_t operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(bits) | SPI_LINES_SINGLE; + if (polarity) { + operation |= SPI_MODE_CPOL; + } + if (phase) { + operation |= SPI_MODE_CPHA; + } + return operation; +} + +void common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, + uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done) { + CIRCUITPY_ASYNC_FLAG_INIT(done); + uint16_t operation = operation_of(polarity, phase, bits); + if (self->done == NULL || CIRCUITPY_ASYNC_FLAG_IS_SET(self->done)) { + apply_config(self, baudrate, operation); + CIRCUITPY_ASYNC_FLAG_SET(done); + return; + } + // A later configure() replaces one still waiting. + if (self->idle_done != NULL) { + CIRCUITPY_ASYNC_FLAG_SET(self->idle_done); + } + self->pending_frequency = baudrate; + self->pending_operation = operation; + self->idle_done = done; +} + +mp_obj_t common_hal_async_spi_spi_configure_end(void *context, circuitpy_async_flag_t *done) { + return mp_const_none; +} + +void common_hal_async_spi_spi_configure_cancel(void *context, circuitpy_async_flag_t *done) { + async_spi_spi_obj_t *self = context; + if (self->idle_done == done) { + self->idle_done = NULL; + } +} + +uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self) { + return self->config[self->active_config].frequency; +} + +// Start a transfer; done is set from the driver's callback, or at once for an empty one. +static void start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, + size_t len, circuitpy_async_flag_t *done) { + wait_done(self); + CIRCUITPY_ASYNC_FLAG_INIT(done); + self->done = done; + if (len == 0) { + CIRCUITPY_ASYNC_FLAG_SET(done); + return; + } + self->tx_buf.buf = (void *)data_out; + self->tx_buf.len = len; + self->tx.buffers = &self->tx_buf; + self->tx.count = data_out != NULL ? 1 : 0; + self->rx_buf.buf = data_in; + self->rx_buf.len = len; + self->rx.buffers = &self->rx_buf; + self->rx.count = data_in != NULL ? 1 : 0; + + int ret = -ENOTSUP; + if (DEVICE_API_GET(spi, self->spi_device)->transceive_async != NULL) { + ret = spi_transceive_cb(self->spi_device, &self->config[self->active_config], + self->tx.count ? &self->tx : NULL, self->rx.count ? &self->rx : NULL, + transfer_done, self); + } + if (ret < 0) { + CIRCUITPY_ASYNC_FLAG_SET(done); + self->done = NULL; + raise_zephyr_error(ret); + } +} + +void common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, + size_t len, circuitpy_async_flag_t *done) { + start(self, data, NULL, len, done); +} + +void common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, + size_t len, uint8_t write_value, circuitpy_async_flag_t *done) { + wait_done(self); + // Without a TX buffer Zephyr sends zeros; another value needs a buffer. + uint8_t *fill = NULL; + if (write_value != 0) { + fill = m_malloc(len); + memset(fill, write_value, len); + } + self->fill = fill; + start(self, fill, data, len, done); +} + +void common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, + const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done) { + start(self, data_out, data_in, len, done); +} + +mp_obj_t common_hal_async_spi_spi_transfer_end(void *context, circuitpy_async_flag_t *done) { + async_spi_spi_obj_t *self = context; + if (self->done == done) { + wait_done(self); + } + return mp_const_none; +} + +void common_hal_async_spi_spi_transfer_cancel(void *context, circuitpy_async_flag_t *done) { + common_hal_async_spi_spi_transfer_end(context, done); +} diff --git a/ports/zephyr-cp/common-hal/async_spi/SPI.h b/ports/zephyr-cp/common-hal/async_spi/SPI.h new file mode 100644 index 00000000000..8045ff19056 --- /dev/null +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.h @@ -0,0 +1,34 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" +#include "common-hal/microcontroller/Pin.h" +#include "supervisor/shared/async_flag.h" + +#include +#include +#include + +typedef struct { + mp_obj_base_t base; + const struct device *spi_device; // NULL once deinited + struct spi_config config[2]; // two slots: a driver sees a change by the pointer + uint8_t active_config; + const mcu_pin_obj_t *clock; + const mcu_pin_obj_t *mosi; + const mcu_pin_obj_t *miso; + circuitpy_async_flag_t *done; // of the running transfer, NULL when none + struct spi_buf tx_buf; // the running transfer's buffers + struct spi_buf rx_buf; + struct spi_buf_set tx; + struct spi_buf_set rx; + uint8_t *fill; // the bytes a read sends, when not zero + circuitpy_async_flag_t *idle_done; // a configure() waiting for the transfer to finish + uint32_t pending_frequency; + uint16_t pending_operation; +} async_spi_spi_obj_t; diff --git a/ports/zephyr-cp/common-hal/async_spi/__init__.c b/ports/zephyr-cp/common-hal/async_spi/__init__.c new file mode 100644 index 00000000000..59a4cec7fe9 --- /dev/null +++ b/ports/zephyr-cp/common-hal/async_spi/__init__.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +// No async_spi module functions. diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index 2d8130f427c..f474fd6d082 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -925,6 +925,8 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig elif driver in BUSIO_CLASSES: # busio driver (i2c, spi, uart) board_info["busio"] = True + if driver == "spi": + board_info["async_spi"] = True logger.info(f"Supported busio driver: {driver}") if driver not in active_zephyr_devices: active_zephyr_devices[driver] = [] diff --git a/ports/zephyr-cp/tests/test_async_spi.py b/ports/zephyr-cp/tests/test_async_spi.py new file mode 100644 index 00000000000..e1e889bd2cc --- /dev/null +++ b/ports/zephyr-cp/tests/test_async_spi.py @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: 2026 Vladimir Smitka +# SPDX-License-Identifier: MIT + +"""async_spi.SPI on native_sim, through the SPI loopback device.""" + +from pathlib import Path + +import pytest + +# The asyncio library is not frozen on native_sim, so the test puts it on the drive. +FROZEN = Path(__file__).resolve().parents[3] / "frozen" +ASYNCIO_LIB = { + f"lib/asyncio/{path.name}": path.read_text() + for path in (FROZEN / "Adafruit_CircuitPython_asyncio" / "asyncio").glob("*.py") +} +ASYNCIO_LIB["lib/adafruit_ticks.py"] = ( + FROZEN / "Adafruit_CircuitPython_Ticks" / "adafruit_ticks.py" +).read_text() + +LOOPBACK_CODE = """\ +import asyncio +import async_spi +import microcontroller + + +async def ticker(count): + while True: + count[0] += 1 + await asyncio.sleep(0) + + +async def main(): + spi = async_spi.SPI(microcontroller.pin.P_02, MOSI=microcontroller.pin.P_03, MISO=microcontroller.pin.P_00) + await spi.configure(baudrate=1_000_000) + print("frequency", spi.frequency) + bad = 0 + for n in (1, 32, 4096): + out = bytes(i * 7 & 0xFF for i in range(n)) + inb = bytearray(n) + await spi.write_readinto(out, inb) + if inb != out: + bad += 1 + print("write_readinto", n, "differs") + await spi.readinto(inb, write_value=0xA5) + if inb != b"\\xa5" * n: + bad += 1 + print("readinto", n, "differs") + await spi.write(out) + + # Another task keeps running around the transfers. + count = [0] + task = asyncio.create_task(ticker(count)) + for _ in range(10): + await spi.write_readinto(out, inb) + await asyncio.sleep(0) + task.cancel() + print("ticker ran", count[0], "times") + + # A cancelled transfer leaves the bus usable. + task = asyncio.create_task(spi.write(out)) + await asyncio.sleep(0) + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + await spi.write_readinto(out, inb) + if inb != out: + bad += 1 + print("after cancel differs") + + try: + await spi.write(123) + except TypeError: + print("bad argument raised") + spi.deinit() + try: + await spi.write(out) + except ValueError: + print("deinit raised") + with async_spi.SPI(microcontroller.pin.P_02, MOSI=microcontroller.pin.P_03, MISO=microcontroller.pin.P_00) as spi2: + await spi2.readinto(inb, end=8, write_value=0x5A) + if inb[:8] != b"\\x5a" * 8: + bad += 1 + print("second bus differs") + print("bad", bad) + print("done") + + +asyncio.run(main()) +""" + + +@pytest.mark.circuitpy_drive({"code.py": LOOPBACK_CODE, **ASYNCIO_LIB}) +def test_async_spi_loopback(circuitpython): + """Awaited transfers come back through the loopback, with cancel and deinit.""" + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + assert "differs" not in output + assert "ticker ran" in output + assert "bad argument raised" in output + assert "deinit raised" in output + assert "bad 0" in output + assert "done" in output From 7e32daecd21b885cf3371231d581a5fe9378ebf2 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Thu, 8 Oct 2026 21:10:54 +0000 Subject: [PATCH 08/27] async_spi: report Zephyr transfer errors, wait without background tasks The driver's result reaches the await as an exception. cancel() and deinit() wait for the transfer without running background tasks, since they may run from a finalizer during GC. async_spi needs asyncio, not only async/await, so raspberrypi enables it with MICROPY_PY_ASYNCIO. --- ports/raspberrypi/mpconfigport.mk | 2 +- ports/zephyr-cp/common-hal/async_spi/SPI.c | 19 ++++++++++++++----- ports/zephyr-cp/common-hal/async_spi/SPI.h | 1 + 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index 64ee62421b7..92af16a1c6c 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -7,7 +7,7 @@ CIRCUITPY_WIFI_RADIO_SETTABLE_MAC_ADDRESS = 0 CIRCUITPY_RP2PIO ?= 1 CIRCUITPY_BUSIO_SPI_ASYNC ?= 1 -CIRCUITPY_ASYNC_SPI ?= $(call enable-if-all,$(MICROPY_PY_ASYNC_AWAIT) $(CIRCUITPY_BUSIO_SPI) $(CIRCUITPY_BUSIO_SPI_ASYNC)) +CIRCUITPY_ASYNC_SPI ?= $(call enable-if-all,$(MICROPY_PY_ASYNCIO) $(CIRCUITPY_BUSIO_SPI) $(CIRCUITPY_BUSIO_SPI_ASYNC)) # 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/ports/zephyr-cp/common-hal/async_spi/SPI.c b/ports/zephyr-cp/common-hal/async_spi/SPI.c index aa094bf123b..a54ce896dac 100644 --- a/ports/zephyr-cp/common-hal/async_spi/SPI.c +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.c @@ -28,8 +28,8 @@ static void apply_config(async_spi_spi_obj_t *self, uint32_t frequency, uint16_t static void transfer_done(const struct device *dev, int result, void *userdata) { ARG_UNUSED(dev); - ARG_UNUSED(result); async_spi_spi_obj_t *self = userdata; + self->result = result; CIRCUITPY_ASYNC_FLAG_SET(self->done); // A configure() that waited for this transfer. if (self->idle_done != NULL) { @@ -40,13 +40,14 @@ static void transfer_done(const struct device *dev, int result, void *userdata) port_wake_main_task_from_isr(); } -// Zephyr cannot stop a transfer, so this waits for it; deinit() waits too. Does not allocate. +// Zephyr cannot stop a transfer, so cancel and deinit wait for it. No background tasks: this +// also runs from a finalizer during GC. static void wait_done(async_spi_spi_obj_t *self) { if (self->done == NULL) { return; } while (!CIRCUITPY_ASYNC_FLAG_IS_SET(self->done)) { - RUN_BACKGROUND_TASKS; + port_task_sleep_ms(1); } self->done = NULL; } @@ -154,6 +155,7 @@ static void start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *d wait_done(self); CIRCUITPY_ASYNC_FLAG_INIT(done); self->done = done; + self->result = 0; if (len == 0) { CIRCUITPY_ASYNC_FLAG_SET(done); return; @@ -203,14 +205,21 @@ void common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, start(self, data_out, data_in, len, done); } +// Called once done is set. mp_obj_t common_hal_async_spi_spi_transfer_end(void *context, circuitpy_async_flag_t *done) { async_spi_spi_obj_t *self = context; if (self->done == done) { - wait_done(self); + self->done = NULL; + if (self->result != 0) { + raise_zephyr_error(self->result); + } } return mp_const_none; } void common_hal_async_spi_spi_transfer_cancel(void *context, circuitpy_async_flag_t *done) { - common_hal_async_spi_spi_transfer_end(context, done); + async_spi_spi_obj_t *self = context; + if (self->done == done) { + wait_done(self); + } } diff --git a/ports/zephyr-cp/common-hal/async_spi/SPI.h b/ports/zephyr-cp/common-hal/async_spi/SPI.h index 8045ff19056..0b5db9237fd 100644 --- a/ports/zephyr-cp/common-hal/async_spi/SPI.h +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.h @@ -23,6 +23,7 @@ typedef struct { const mcu_pin_obj_t *mosi; const mcu_pin_obj_t *miso; circuitpy_async_flag_t *done; // of the running transfer, NULL when none + int result; // of the last transfer, from the driver struct spi_buf tx_buf; // the running transfer's buffers struct spi_buf rx_buf; struct spi_buf_set tx; From 9aaf7f548e25c0f318bd8ccb5c7a328841c23e38 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Fri, 9 Oct 2026 05:07:00 +0000 Subject: [PATCH 09/27] busio: keep the SPI constructor's argument table under CIRCUITPY_BUSIO_SPI Unused, so a build error, on boards without busio.SPI. --- shared-bindings/busio/SPI.c | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/shared-bindings/busio/SPI.c b/shared-bindings/busio/SPI.c index 331db96df4e..797c6bd02f9 100644 --- a/shared-bindings/busio/SPI.c +++ b/shared-bindings/busio/SPI.c @@ -94,18 +94,17 @@ // TODO(tannewt): Support LSB SPI. -#if !CIRCUITPY_ASYNC_SPI -static const mp_arg_t busio_spi_make_new_args[] = { - { MP_QSTR_clock, MP_ARG_REQUIRED | MP_ARG_OBJ }, - { MP_QSTR_MOSI, MP_ARG_OBJ, {.u_obj = mp_const_none} }, - { MP_QSTR_MISO, MP_ARG_OBJ, {.u_obj = mp_const_none} }, - { MP_QSTR_half_duplex, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, -}; -#endif - static mp_obj_t busio_spi_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { #if CIRCUITPY_BUSIO_SPI enum { ARG_clock, ARG_MOSI, ARG_MISO, ARG_half_duplex }; + #if !CIRCUITPY_ASYNC_SPI + static const mp_arg_t busio_spi_make_new_args[] = { + { MP_QSTR_clock, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_MOSI, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_MISO, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_half_duplex, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + }; + #endif mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_make_new_args)]; mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(busio_spi_make_new_args), busio_spi_make_new_args, args); From 7fe5f7a814d5054607c1de3aaeb744ba638a438b Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Mon, 12 Oct 2026 18:40:00 +0000 Subject: [PATCH 10/27] busio: own the SPI argument tables, async_spi uses them busio.SPI is always built, async_spi only on some ports, so the shared tables belong to busio. This drops the CIRCUITPY_ASYNC_SPI conditionals from busio's bindings. Firmware size is unchanged. --- shared-bindings/async_spi/SPI.c | 55 +++++++++------------------------ shared-bindings/async_spi/SPI.h | 7 ----- shared-bindings/busio/SPI.c | 43 +++++++++----------------- shared-bindings/busio/SPI.h | 7 +++++ 4 files changed, 35 insertions(+), 77 deletions(-) diff --git a/shared-bindings/async_spi/SPI.c b/shared-bindings/async_spi/SPI.c index 75eda233189..664fb13963b 100644 --- a/shared-bindings/async_spi/SPI.c +++ b/shared-bindings/async_spi/SPI.c @@ -13,6 +13,11 @@ #include "shared/runtime/buffer_helper.h" #include "shared/runtime/context_manager_helpers.h" #include "shared-bindings/async_spi/SPI.h" +#include "shared-bindings/busio/SPI.h" + +#if !CIRCUITPY_BUSIO_SPI +#error "async_spi uses busio.SPI's argument tables" +#endif #include "shared-bindings/microcontroller/Pin.h" #include "shared-bindings/util.h" @@ -38,43 +43,11 @@ //| :param ~microcontroller.Pin MISO: the Main In Selected Out pin.""" //| ... //| -// The tables are busio.SPI's too, so they live here, where the arguments are documented. -const mp_arg_t async_spi_spi_make_new_args[] = { - { MP_QSTR_clock, MP_ARG_REQUIRED | MP_ARG_OBJ }, - { MP_QSTR_MOSI, MP_ARG_OBJ, {.u_obj = mp_const_none} }, - { MP_QSTR_MISO, MP_ARG_OBJ, {.u_obj = mp_const_none} }, - { MP_QSTR_half_duplex, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, // busio.SPI only -}; -const mp_arg_t async_spi_spi_configure_args[] = { - { MP_QSTR_baudrate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 100000} }, - { MP_QSTR_polarity, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_phase, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_bits, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8} }, -}; -const mp_arg_t async_spi_spi_write_args[] = { - { MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ }, - { MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, -}; -const mp_arg_t async_spi_spi_readinto_args[] = { - { MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ }, - { MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, - { MP_QSTR_write_value, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, -}; -const mp_arg_t async_spi_spi_write_readinto_args[] = { - { MP_QSTR_out_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ }, - { MP_QSTR_in_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ }, - { MP_QSTR_out_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_out_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, - { MP_QSTR_in_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_in_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, -}; static mp_obj_t async_spi_spi_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { enum { ARG_clock, ARG_MOSI, ARG_MISO }; mp_arg_val_t args[3]; - mp_arg_parse_all_kw_array(n_args, n_kw, all_args, 3, async_spi_spi_make_new_args, args); + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, 3, busio_spi_make_new_args, args); const mcu_pin_obj_t *clock = validate_obj_is_free_pin(args[ARG_clock].u_obj, MP_QSTR_clock); const mcu_pin_obj_t *mosi = validate_obj_is_free_pin_or_none(args[ARG_MOSI].u_obj, MP_QSTR_MOSI); @@ -130,8 +103,8 @@ static MP_DEFINE_CONST_FUN_OBJ_1(async_spi_spi_deinit_obj, async_spi_spi_obj_dei static void *async_spi_spi_configure(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_baudrate, ARG_polarity, ARG_phase, ARG_bits }; async_spi_spi_obj_t *self = checked_spi(pos_args[0]); - mp_arg_val_t args[MP_ARRAY_SIZE(async_spi_spi_configure_args)]; - mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(async_spi_spi_configure_args), async_spi_spi_configure_args, args); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_configure_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_configure_args), busio_spi_configure_args, args); uint8_t polarity = (uint8_t)mp_arg_validate_int_range(args[ARG_polarity].u_int, 0, 1, MP_QSTR_polarity); uint8_t phase = (uint8_t)mp_arg_validate_int_range(args[ARG_phase].u_int, 0, 1, MP_QSTR_phase); @@ -157,8 +130,8 @@ static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_configure_obj, 1, async_s static void *async_spi_spi_write(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_buffer, ARG_start, ARG_end }; async_spi_spi_obj_t *self = checked_spi(pos_args[0]); - mp_arg_val_t args[MP_ARRAY_SIZE(async_spi_spi_write_args)]; - mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(async_spi_spi_write_args), async_spi_spi_write_args, args); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_write_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_write_args), busio_spi_write_args, args); size_t len; uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int, @@ -187,8 +160,8 @@ static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_obj, 1, async_spi_s static void *async_spi_spi_readinto(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_buffer, ARG_start, ARG_end, ARG_write_value }; async_spi_spi_obj_t *self = checked_spi(pos_args[0]); - mp_arg_val_t args[MP_ARRAY_SIZE(async_spi_spi_readinto_args)]; - mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(async_spi_spi_readinto_args), async_spi_spi_readinto_args, args); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_readinto_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_readinto_args), busio_spi_readinto_args, args); size_t len; uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int, @@ -222,8 +195,8 @@ static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_readinto_obj, 1, async_sp static void *async_spi_spi_write_readinto(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_out_buffer, ARG_in_buffer, ARG_out_start, ARG_out_end, ARG_in_start, ARG_in_end }; async_spi_spi_obj_t *self = checked_spi(pos_args[0]); - mp_arg_val_t args[MP_ARRAY_SIZE(async_spi_spi_write_readinto_args)]; - mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(async_spi_spi_write_readinto_args), async_spi_spi_write_readinto_args, args); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_write_readinto_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_write_readinto_args), busio_spi_write_readinto_args, args); size_t out_len, in_len; const uint8_t *data_out = buffer_slice(args[ARG_out_buffer].u_obj, args[ARG_out_start].u_int, diff --git a/shared-bindings/async_spi/SPI.h b/shared-bindings/async_spi/SPI.h index ee927cecfce..dbc52bf5b6b 100644 --- a/shared-bindings/async_spi/SPI.h +++ b/shared-bindings/async_spi/SPI.h @@ -14,13 +14,6 @@ extern const mp_obj_type_t async_spi_spi_type; -// Argument tables, shared with busio.SPI, which takes the same arguments. -extern const mp_arg_t async_spi_spi_make_new_args[4]; // clock, MOSI, MISO, half_duplex -extern const mp_arg_t async_spi_spi_configure_args[4]; -extern const mp_arg_t async_spi_spi_write_args[3]; -extern const mp_arg_t async_spi_spi_readinto_args[4]; -extern const mp_arg_t async_spi_spi_write_readinto_args[6]; - void common_hal_async_spi_spi_construct(async_spi_spi_obj_t *self, const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso); bool common_hal_async_spi_spi_deinited(async_spi_spi_obj_t *self); diff --git a/shared-bindings/busio/SPI.c b/shared-bindings/busio/SPI.c index 797c6bd02f9..6411fdaa627 100644 --- a/shared-bindings/busio/SPI.c +++ b/shared-bindings/busio/SPI.c @@ -14,15 +14,6 @@ #include "py/runtime.h" #include "shared-bindings/microcontroller/Pin.h" #include "shared-bindings/busio/SPI.h" -#if CIRCUITPY_ASYNC_SPI -// async_spi.SPI takes the same arguments, so its tables are used. -#include "shared-bindings/async_spi/SPI.h" -#define busio_spi_make_new_args async_spi_spi_make_new_args -#define busio_spi_configure_args async_spi_spi_configure_args -#define busio_spi_write_args async_spi_spi_write_args -#define busio_spi_readinto_args async_spi_spi_readinto_args -#define busio_spi_write_readinto_args async_spi_spi_write_readinto_args -#endif #include "shared-bindings/util.h" #include "shared/runtime/buffer_helper.h" #include "shared/runtime/context_manager_helpers.h" @@ -93,18 +84,20 @@ //| +#if CIRCUITPY_BUSIO_SPI +// async_spi.SPI takes the same arguments and uses these tables too. +const mp_arg_t busio_spi_make_new_args[] = { + { MP_QSTR_clock, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_MOSI, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_MISO, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_half_duplex, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, +}; +#endif + // TODO(tannewt): Support LSB SPI. static mp_obj_t busio_spi_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { #if CIRCUITPY_BUSIO_SPI enum { ARG_clock, ARG_MOSI, ARG_MISO, ARG_half_duplex }; - #if !CIRCUITPY_ASYNC_SPI - static const mp_arg_t busio_spi_make_new_args[] = { - { MP_QSTR_clock, MP_ARG_REQUIRED | MP_ARG_OBJ }, - { MP_QSTR_MOSI, MP_ARG_OBJ, {.u_obj = mp_const_none} }, - { MP_QSTR_MISO, MP_ARG_OBJ, {.u_obj = mp_const_none} }, - { MP_QSTR_half_duplex, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, - }; - #endif mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_make_new_args)]; mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(busio_spi_make_new_args), busio_spi_make_new_args, args); @@ -185,14 +178,12 @@ static void check_for_deinit(busio_spi_obj_t *self) { //| ... //| -#if !CIRCUITPY_ASYNC_SPI -static const mp_arg_t busio_spi_configure_args[] = { +const mp_arg_t busio_spi_configure_args[] = { { MP_QSTR_baudrate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 100000} }, { MP_QSTR_polarity, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, { MP_QSTR_phase, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, { MP_QSTR_bits, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8} }, }; -#endif static mp_obj_t busio_spi_configure(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_baudrate, ARG_polarity, ARG_phase, ARG_bits }; @@ -258,13 +249,11 @@ MP_DEFINE_CONST_FUN_OBJ_1(busio_spi_unlock_obj, busio_spi_obj_unlock); //| ... //| -#if !CIRCUITPY_ASYNC_SPI -static const mp_arg_t busio_spi_write_args[] = { +const mp_arg_t busio_spi_write_args[] = { { MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, { MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, { MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, }; -#endif static mp_obj_t busio_spi_write(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_buffer, ARG_start, ARG_end }; @@ -318,14 +307,12 @@ MP_DEFINE_CONST_FUN_OBJ_KW(busio_spi_write_obj, 1, busio_spi_write); //| ... //| -#if !CIRCUITPY_ASYNC_SPI -static const mp_arg_t busio_spi_readinto_args[] = { +const mp_arg_t busio_spi_readinto_args[] = { { MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, { MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, { MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, { MP_QSTR_write_value, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, }; -#endif static mp_obj_t busio_spi_readinto(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_buffer, ARG_start, ARG_end, ARG_write_value }; @@ -387,8 +374,7 @@ MP_DEFINE_CONST_FUN_OBJ_KW(busio_spi_readinto_obj, 1, busio_spi_readinto); //| ... //| -#if !CIRCUITPY_ASYNC_SPI -static const mp_arg_t busio_spi_write_readinto_args[] = { +const mp_arg_t busio_spi_write_readinto_args[] = { { MP_QSTR_out_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, { MP_QSTR_in_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, { MP_QSTR_out_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, @@ -396,7 +382,6 @@ static const mp_arg_t busio_spi_write_readinto_args[] = { { MP_QSTR_in_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, { MP_QSTR_in_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} }, }; -#endif static mp_obj_t busio_spi_write_readinto(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_out_buffer, ARG_in_buffer, ARG_out_start, ARG_out_end, ARG_in_start, ARG_in_end }; diff --git a/shared-bindings/busio/SPI.h b/shared-bindings/busio/SPI.h index fa389131ee4..515cddbb235 100644 --- a/shared-bindings/busio/SPI.h +++ b/shared-bindings/busio/SPI.h @@ -16,6 +16,13 @@ // Type object used in Python. Should be shared between ports. extern const mp_obj_type_t busio_spi_type; +// Argument tables, also used by async_spi.SPI, which takes the same arguments. +extern const mp_arg_t busio_spi_make_new_args[4]; // clock, MOSI, MISO, half_duplex +extern const mp_arg_t busio_spi_configure_args[4]; +extern const mp_arg_t busio_spi_write_args[3]; +extern const mp_arg_t busio_spi_readinto_args[4]; +extern const mp_arg_t busio_spi_write_readinto_args[6]; + // Construct an underlying SPI object. extern void common_hal_busio_spi_construct(busio_spi_obj_t *self, const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, From 4a45d540da62788049af5ef4521ebae299a540f3 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Mon, 12 Oct 2026 20:19:13 +0000 Subject: [PATCH 11/27] raspberrypi: keep CircuitPython's flag type out of peripherals/spi The driver takes a volatile bool * for a transfer's done flag instead of circuitpy_async_flag_t, so peripherals/ does not depend on the supervisor. common-hal/busio/SPI.h includes the flag header again, as before the driver moved. --- ports/raspberrypi/common-hal/busio/SPI.h | 1 + ports/raspberrypi/peripherals/spi.c | 38 ++++++++++++------------ ports/raspberrypi/peripherals/spi.h | 19 ++++++------ 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/ports/raspberrypi/common-hal/busio/SPI.h b/ports/raspberrypi/common-hal/busio/SPI.h index 2a3aecc74bf..dd067d6a5c3 100644 --- a/ports/raspberrypi/common-hal/busio/SPI.h +++ b/ports/raspberrypi/common-hal/busio/SPI.h @@ -7,6 +7,7 @@ #pragma once #include "py/obj.h" +#include "supervisor/shared/async_flag.h" #include "peripherals/spi.h" typedef struct { diff --git a/ports/raspberrypi/peripherals/spi.c b/ports/raspberrypi/peripherals/spi.c index 2ee29682efd..1127e1279cc 100644 --- a/ports/raspberrypi/peripherals/spi.c +++ b/ports/raspberrypi/peripherals/spi.c @@ -20,7 +20,7 @@ #if CIRCUITPY_ASYNC_SPI // Done flag of the async transfer on each kept RX channel, set when that channel finishes. -static circuitpy_async_flag_t *async_flags[NUM_DMA_CHANNELS]; +static volatile bool *async_flags[NUM_DMA_CHANNELS]; // RX channels with the interrupt enabled. static uint32_t async_irq_mask; @@ -30,7 +30,7 @@ static void __not_in_flash_func(spi_dma_irq_handler)(void) { dma_hw->ints0 = pending; for (uint chan = 0; pending != 0; chan++, pending >>= 1) { if ((pending & 1) && async_flags[chan] != NULL) { - CIRCUITPY_ASYNC_FLAG_SET(async_flags[chan]); + *async_flags[chan] = true; } } } @@ -142,7 +142,7 @@ void rp2_spi_deinit(rp2_spi_t *self) { #if CIRCUITPY_ASYNC_SPI if (self->async_active) { _abort(self); - CIRCUITPY_ASYNC_FLAG_SET(self->async_done); + *self->async_done = true; } _idle(self); #else @@ -205,22 +205,22 @@ static void _idle(rp2_spi_t *self) { if (self->idle_done != NULL) { _apply(self, self->pending_baudrate, self->pending_polarity, self->pending_phase, self->pending_bits); - CIRCUITPY_ASYNC_FLAG_SET(self->idle_done); + *self->idle_done = true; self->idle_done = NULL; } } void rp2_spi_configure_start(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, - uint8_t bits, circuitpy_async_flag_t *done) { - CIRCUITPY_ASYNC_FLAG_INIT(done); + uint8_t bits, volatile bool *done) { + *done = false; if (!self->async_active) { _apply(self, baudrate, polarity, phase, bits); - CIRCUITPY_ASYNC_FLAG_SET(done); + *done = true; return; } // A later configure() replaces one still waiting. if (self->idle_done != NULL) { - CIRCUITPY_ASYNC_FLAG_SET(self->idle_done); + *self->idle_done = true; } self->pending_baudrate = baudrate; self->pending_polarity = polarity; @@ -229,7 +229,7 @@ void rp2_spi_configure_start(rp2_spi_t *self, uint32_t baudrate, uint8_t polarit self->idle_done = done; } -void rp2_spi_configure_cancel(rp2_spi_t *self, circuitpy_async_flag_t *done) { +void rp2_spi_configure_cancel(rp2_spi_t *self, volatile bool *done) { if (self->idle_done == done) { self->idle_done = NULL; } @@ -242,7 +242,7 @@ void rp2_spi_configure_cancel(rp2_spi_t *self, circuitpy_async_flag_t *done) { // until deinit, for buses that send often. Returns whether DMA is running. static bool _start(rp2_spi_t *self, const uint8_t *data_out, size_t out_len, - uint8_t *data_in, size_t in_len, circuitpy_async_flag_t *done) { + uint8_t *data_in, size_t in_len, volatile bool *done) { size_t len = MAX(out_len, in_len); // Only use DMA if both data buffers are in SRAM. Otherwise, we'll stall the DMA with PSRAM or flash cache misses. bool use_dma = len >= 32 && data_in >= (uint8_t *)SRAM_BASE && data_out >= (uint8_t *)SRAM_BASE; @@ -352,26 +352,26 @@ void rp2_spi_transfer(rp2_spi_t *self, const uint8_t *data_out, size_t out_len, // Start an async transfer; done is set when it has finished, here if it ran in software. static __attribute__((noinline)) void _async_start(rp2_spi_t *self, const uint8_t *data_out, size_t out_len, - uint8_t *data_in, size_t in_len, circuitpy_async_flag_t *done) { - CIRCUITPY_ASYNC_FLAG_INIT(done); + uint8_t *data_in, size_t in_len, volatile bool *done) { + *done = false; self->async_done = done; // An empty data side means there is nothing to send, whatever the one-byte side holds. self->async_active = out_len != 0 && in_len != 0 && _start(self, data_out, out_len, data_in, in_len, done); if (!self->async_active) { - CIRCUITPY_ASYNC_FLAG_SET(done); + *done = true; } } void rp2_spi_write_start(rp2_spi_t *self, const uint8_t *data, size_t len, - circuitpy_async_flag_t *done) { + volatile bool *done) { rp2_spi_end(self); _async_start(self, data, len, &self->one_byte, 1, done); } #if CIRCUITPY_ASYNC_SPI void rp2_spi_read_start(rp2_spi_t *self, uint8_t *data, size_t len, uint8_t write_value, - circuitpy_async_flag_t *done) { + volatile bool *done) { // End the previous transfer first: an async write may still be receiving into one_byte. rp2_spi_end(self); self->one_byte = write_value; @@ -379,7 +379,7 @@ void rp2_spi_read_start(rp2_spi_t *self, uint8_t *data, size_t len, uint8_t writ } void rp2_spi_transfer_start(rp2_spi_t *self, const uint8_t *data_out, uint8_t *data_in, - size_t len, circuitpy_async_flag_t *done) { + size_t len, volatile bool *done) { rp2_spi_end(self); _async_start(self, data_out, len, data_in, len, done); } @@ -402,11 +402,11 @@ static void _abort(rp2_spi_t *self) { _idle(self); } -void rp2_spi_stop(rp2_spi_t *self, circuitpy_async_flag_t *done) { +void rp2_spi_stop(rp2_spi_t *self, volatile bool *done) { if (!self->async_active || self->async_done != done) { return; } - if (CIRCUITPY_ASYNC_FLAG_IS_SET(done)) { + if (*done) { rp2_spi_end(self); return; } @@ -422,7 +422,7 @@ void rp2_spi_end(rp2_spi_t *self) { #if CIRCUITPY_ASYNC_SPI async_flags[self->dma_rx] = NULL; #endif - CIRCUITPY_ASYNC_FLAG_SET(self->async_done); + *self->async_done = true; #if CIRCUITPY_ASYNC_SPI _idle(self); #endif diff --git a/ports/raspberrypi/peripherals/spi.h b/ports/raspberrypi/peripherals/spi.h index 9a0385767f9..42511ae42a0 100644 --- a/ports/raspberrypi/peripherals/spi.h +++ b/ports/raspberrypi/peripherals/spi.h @@ -8,8 +8,9 @@ #pragma once +#include + #include "common-hal/microcontroller/Pin.h" -#include "supervisor/shared/async_flag.h" #include "hardware/spi.h" @@ -28,10 +29,10 @@ typedef struct { uint8_t dma_tx; uint8_t dma_rx; uint8_t one_byte; // the one-byte side of an async write (RX) or read (TX) - circuitpy_async_flag_t *async_done; + volatile bool *async_done; #if CIRCUITPY_ASYNC_SPI // A configure() waiting for the transfer to finish, and its settings. - circuitpy_async_flag_t *idle_done; + volatile bool *idle_done; uint32_t pending_baudrate; uint8_t pending_polarity; uint8_t pending_phase; @@ -64,18 +65,18 @@ void rp2_spi_transfer(rp2_spi_t *self, const uint8_t *data_out, size_t out_len, // Async transfers: done is set once the transfer has finished, by the DMA interrupt with // CIRCUITPY_ASYNC_SPI and in any case by rp2_spi_end(). The buffers must stay valid until then. void rp2_spi_write_start(rp2_spi_t *self, const uint8_t *data, size_t len, - circuitpy_async_flag_t *done); + volatile bool *done); void rp2_spi_read_start(rp2_spi_t *self, uint8_t *data, size_t len, uint8_t write_value, - circuitpy_async_flag_t *done); + volatile bool *done); void rp2_spi_transfer_start(rp2_spi_t *self, const uint8_t *data_out, uint8_t *data_in, - size_t len, circuitpy_async_flag_t *done); + size_t len, volatile bool *done); // Waits for the running async transfer, if any, and sets its done flag. void rp2_spi_end(rp2_spi_t *self); // Stops the transfer started with done, or finishes it if done is set. Does nothing for an // earlier transfer, and does not allocate. -void rp2_spi_stop(rp2_spi_t *self, circuitpy_async_flag_t *done); +void rp2_spi_stop(rp2_spi_t *self, volatile bool *done); // Configures the bus once the running transfer, if any, has finished, and sets done then. void rp2_spi_configure_start(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, - uint8_t bits, circuitpy_async_flag_t *done); + uint8_t bits, volatile bool *done); // Drops the waiting configure() started with done. -void rp2_spi_configure_cancel(rp2_spi_t *self, circuitpy_async_flag_t *done); +void rp2_spi_configure_cancel(rp2_spi_t *self, volatile bool *done); From b1de0e76b2ec9ea52b00f4d9ea26803996997754 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Mon, 12 Oct 2026 20:21:15 +0000 Subject: [PATCH 12/27] async_spi: start functions return the context common_hal_async_spi_spi_*_start() return the context that end() and cancel() get, so an implementation can keep per-operation state. Both ports return the SPI object, as the bindings did before. --- ports/raspberrypi/common-hal/async_spi/SPI.c | 12 ++++++++---- ports/zephyr-cp/common-hal/async_spi/SPI.c | 14 +++++++++----- shared-bindings/async_spi/SPI.c | 12 ++++-------- shared-bindings/async_spi/SPI.h | 17 +++++++++-------- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/ports/raspberrypi/common-hal/async_spi/SPI.c b/ports/raspberrypi/common-hal/async_spi/SPI.c index b8cadeaac1a..e04daa0439c 100644 --- a/ports/raspberrypi/common-hal/async_spi/SPI.c +++ b/ports/raspberrypi/common-hal/async_spi/SPI.c @@ -28,9 +28,10 @@ void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self) { rp2_spi_deinit(&self->spi); } -void common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, +void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done) { rp2_spi_configure_start(&self->spi, baudrate, polarity, phase, bits, done); + return self; } mp_obj_t common_hal_async_spi_spi_configure_end(void *context, circuitpy_async_flag_t *done) { @@ -46,19 +47,22 @@ uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self) { return self->spi.real_frequency; } -void common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, +void *common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, size_t len, circuitpy_async_flag_t *done) { rp2_spi_write_start(&self->spi, data, len, done); + return self; } -void common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, +void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value, circuitpy_async_flag_t *done) { rp2_spi_read_start(&self->spi, data, len, write_value, done); + return self; } -void common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, +void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done) { rp2_spi_transfer_start(&self->spi, data_out, data_in, len, done); + return self; } mp_obj_t common_hal_async_spi_spi_transfer_end(void *context, circuitpy_async_flag_t *done) { diff --git a/ports/zephyr-cp/common-hal/async_spi/SPI.c b/ports/zephyr-cp/common-hal/async_spi/SPI.c index a54ce896dac..67dd3444485 100644 --- a/ports/zephyr-cp/common-hal/async_spi/SPI.c +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.c @@ -116,14 +116,14 @@ static uint16_t operation_of(uint8_t polarity, uint8_t phase, uint8_t bits) { return operation; } -void common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, +void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done) { CIRCUITPY_ASYNC_FLAG_INIT(done); uint16_t operation = operation_of(polarity, phase, bits); if (self->done == NULL || CIRCUITPY_ASYNC_FLAG_IS_SET(self->done)) { apply_config(self, baudrate, operation); CIRCUITPY_ASYNC_FLAG_SET(done); - return; + return self; } // A later configure() replaces one still waiting. if (self->idle_done != NULL) { @@ -132,6 +132,7 @@ void common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_ self->pending_frequency = baudrate; self->pending_operation = operation; self->idle_done = done; + return self; } mp_obj_t common_hal_async_spi_spi_configure_end(void *context, circuitpy_async_flag_t *done) { @@ -182,12 +183,13 @@ static void start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *d } } -void common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, +void *common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, size_t len, circuitpy_async_flag_t *done) { start(self, data, NULL, len, done); + return self; } -void common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, +void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value, circuitpy_async_flag_t *done) { wait_done(self); // Without a TX buffer Zephyr sends zeros; another value needs a buffer. @@ -198,11 +200,13 @@ void common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t } self->fill = fill; start(self, fill, data, len, done); + return self; } -void common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, +void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done) { start(self, data_out, data_in, len, done); + return self; } // Called once done is set. diff --git a/shared-bindings/async_spi/SPI.c b/shared-bindings/async_spi/SPI.c index 664fb13963b..20b1ad57fe0 100644 --- a/shared-bindings/async_spi/SPI.c +++ b/shared-bindings/async_spi/SPI.c @@ -110,8 +110,7 @@ static void *async_spi_spi_configure(circuitpy_async_flag_t *done, size_t n_args uint8_t phase = (uint8_t)mp_arg_validate_int_range(args[ARG_phase].u_int, 0, 1, MP_QSTR_phase); uint8_t bits = (uint8_t)mp_arg_validate_int_range(args[ARG_bits].u_int, 8, 9, MP_QSTR_bits); - common_hal_async_spi_spi_configure_start(self, args[ARG_baudrate].u_int, polarity, phase, bits, done); - return self; + return common_hal_async_spi_spi_configure_start(self, args[ARG_baudrate].u_int, polarity, phase, bits, done); } static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_configure_obj, 1, async_spi_spi_configure); @@ -136,8 +135,7 @@ static void *async_spi_spi_write(circuitpy_async_flag_t *done, size_t n_args, co size_t len; uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int, MP_BUFFER_READ, &len); - common_hal_async_spi_spi_write_start(self, data, len, done); - return self; + return common_hal_async_spi_spi_write_start(self, data, len, done); } static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_obj, 1, async_spi_spi_write); @@ -166,8 +164,7 @@ static void *async_spi_spi_readinto(circuitpy_async_flag_t *done, size_t n_args, size_t len; uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int, MP_BUFFER_WRITE, &len); - common_hal_async_spi_spi_readinto_start(self, data, len, (uint8_t)args[ARG_write_value].u_int, done); - return self; + return common_hal_async_spi_spi_readinto_start(self, data, len, (uint8_t)args[ARG_write_value].u_int, done); } static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_readinto_obj, 1, async_spi_spi_readinto); @@ -206,8 +203,7 @@ static void *async_spi_spi_write_readinto(circuitpy_async_flag_t *done, size_t n if (out_len != in_len) { mp_raise_ValueError(MP_ERROR_TEXT("buffer slices must be of equal length")); } - common_hal_async_spi_spi_write_readinto_start(self, data_out, data_in, out_len, done); - return self; + return common_hal_async_spi_spi_write_readinto_start(self, data_out, data_in, out_len, done); } static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_readinto_obj, 2, async_spi_spi_write_readinto); diff --git a/shared-bindings/async_spi/SPI.h b/shared-bindings/async_spi/SPI.h index dbc52bf5b6b..cfb7c99e4bc 100644 --- a/shared-bindings/async_spi/SPI.h +++ b/shared-bindings/async_spi/SPI.h @@ -22,20 +22,21 @@ void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self); uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self); // configure(): done is set once the running transfer, if any, has finished and the settings -// are in place. The context is the SPI object. -void common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, +// are in place. Returns the context that configure_end() and configure_cancel() get. +void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done); mp_obj_t common_hal_async_spi_spi_configure_end(void *context, circuitpy_async_flag_t *done); void common_hal_async_spi_spi_configure_cancel(void *context, circuitpy_async_flag_t *done); -// Each transfer start sets done once the transfer has finished, possibly from an interrupt. The -// buffers must stay valid until then. transfer_end() and transfer_cancel() take the SPI object -// as the context and act only on the transfer started with done. -void common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, +// Each transfer start sets done once the transfer has finished, possibly from an interrupt, and +// returns the context that transfer_end() and transfer_cancel() get. The buffers must stay valid +// until done is set. transfer_end() and transfer_cancel() act only on the transfer started with +// done. +void *common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, size_t len, circuitpy_async_flag_t *done); -void common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, +void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value, circuitpy_async_flag_t *done); -void common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, +void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done); mp_obj_t common_hal_async_spi_spi_transfer_end(void *context, circuitpy_async_flag_t *done); void common_hal_async_spi_spi_transfer_cancel(void *context, circuitpy_async_flag_t *done); From 40d772fc0c639ee8b6c4627187461d890aae764b Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Wed, 14 Oct 2026 15:40:05 +0000 Subject: [PATCH 13/27] async_spi: end() and cancel() get the object and a context The context is for state of one operation; start() returns NULL on both ports, which keep that state in the bus object. end() and cancel() take self and the context as separate arguments, and the macro takes the end and cancel functions by name. --- ports/raspberrypi/common-hal/async_spi/SPI.c | 23 +++++++------- ports/zephyr-cp/common-hal/async_spi/SPI.c | 25 ++++++++------- shared-bindings/async_spi/SPI.c | 26 ++++++++++++--- shared-bindings/async_spi/SPI.h | 32 +++++++++---------- supervisor/shared/awaitable.c | 4 +-- supervisor/shared/awaitable.h | 33 +++++++++++--------- 6 files changed, 83 insertions(+), 60 deletions(-) diff --git a/ports/raspberrypi/common-hal/async_spi/SPI.c b/ports/raspberrypi/common-hal/async_spi/SPI.c index e04daa0439c..63c298bd3b3 100644 --- a/ports/raspberrypi/common-hal/async_spi/SPI.c +++ b/ports/raspberrypi/common-hal/async_spi/SPI.c @@ -31,15 +31,16 @@ void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self) { void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done) { rp2_spi_configure_start(&self->spi, baudrate, polarity, phase, bits, done); - return self; + return NULL; } -mp_obj_t common_hal_async_spi_spi_configure_end(void *context, circuitpy_async_flag_t *done) { +mp_obj_t common_hal_async_spi_spi_configure_end(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { return mp_const_none; } -void common_hal_async_spi_spi_configure_cancel(void *context, circuitpy_async_flag_t *done) { - async_spi_spi_obj_t *self = context; +void common_hal_async_spi_spi_configure_cancel(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { rp2_spi_configure_cancel(&self->spi, done); } @@ -50,28 +51,28 @@ uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self) { void *common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, size_t len, circuitpy_async_flag_t *done) { rp2_spi_write_start(&self->spi, data, len, done); - return self; + return NULL; } void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value, circuitpy_async_flag_t *done) { rp2_spi_read_start(&self->spi, data, len, write_value, done); - return self; + return NULL; } void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done) { rp2_spi_transfer_start(&self->spi, data_out, data_in, len, done); - return self; + return NULL; } -mp_obj_t common_hal_async_spi_spi_transfer_end(void *context, circuitpy_async_flag_t *done) { - async_spi_spi_obj_t *self = context; +mp_obj_t common_hal_async_spi_spi_transfer_end(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { rp2_spi_stop(&self->spi, done); return mp_const_none; } -void common_hal_async_spi_spi_transfer_cancel(void *context, circuitpy_async_flag_t *done) { - async_spi_spi_obj_t *self = context; +void common_hal_async_spi_spi_transfer_cancel(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { rp2_spi_stop(&self->spi, done); } diff --git a/ports/zephyr-cp/common-hal/async_spi/SPI.c b/ports/zephyr-cp/common-hal/async_spi/SPI.c index 67dd3444485..bb887e8fc3a 100644 --- a/ports/zephyr-cp/common-hal/async_spi/SPI.c +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.c @@ -123,7 +123,7 @@ void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32 if (self->done == NULL || CIRCUITPY_ASYNC_FLAG_IS_SET(self->done)) { apply_config(self, baudrate, operation); CIRCUITPY_ASYNC_FLAG_SET(done); - return self; + return NULL; } // A later configure() replaces one still waiting. if (self->idle_done != NULL) { @@ -132,15 +132,16 @@ void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32 self->pending_frequency = baudrate; self->pending_operation = operation; self->idle_done = done; - return self; + return NULL; } -mp_obj_t common_hal_async_spi_spi_configure_end(void *context, circuitpy_async_flag_t *done) { +mp_obj_t common_hal_async_spi_spi_configure_end(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { return mp_const_none; } -void common_hal_async_spi_spi_configure_cancel(void *context, circuitpy_async_flag_t *done) { - async_spi_spi_obj_t *self = context; +void common_hal_async_spi_spi_configure_cancel(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { if (self->idle_done == done) { self->idle_done = NULL; } @@ -186,7 +187,7 @@ static void start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *d void *common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, size_t len, circuitpy_async_flag_t *done) { start(self, data, NULL, len, done); - return self; + return NULL; } void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, @@ -200,18 +201,18 @@ void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t } self->fill = fill; start(self, fill, data, len, done); - return self; + return NULL; } void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done) { start(self, data_out, data_in, len, done); - return self; + return NULL; } // Called once done is set. -mp_obj_t common_hal_async_spi_spi_transfer_end(void *context, circuitpy_async_flag_t *done) { - async_spi_spi_obj_t *self = context; +mp_obj_t common_hal_async_spi_spi_transfer_end(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { if (self->done == done) { self->done = NULL; if (self->result != 0) { @@ -221,8 +222,8 @@ mp_obj_t common_hal_async_spi_spi_transfer_end(void *context, circuitpy_async_fl return mp_const_none; } -void common_hal_async_spi_spi_transfer_cancel(void *context, circuitpy_async_flag_t *done) { - async_spi_spi_obj_t *self = context; +void common_hal_async_spi_spi_transfer_cancel(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { if (self->done == done) { wait_done(self); } diff --git a/shared-bindings/async_spi/SPI.c b/shared-bindings/async_spi/SPI.c index 20b1ad57fe0..1231a37d736 100644 --- a/shared-bindings/async_spi/SPI.c +++ b/shared-bindings/async_spi/SPI.c @@ -93,6 +93,20 @@ static MP_DEFINE_CONST_FUN_OBJ_1(async_spi_spi_deinit_obj, async_spi_spi_obj_dei //| // Provided by context manager helper. +// The awaitable passes the object as mp_obj_t; the common-hal functions take it typed. +static mp_obj_t async_spi_spi_configure_end(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { + return common_hal_async_spi_spi_configure_end(MP_OBJ_TO_PTR(self_in), context, done); +} +static void async_spi_spi_configure_cancel(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { + common_hal_async_spi_spi_configure_cancel(MP_OBJ_TO_PTR(self_in), context, done); +} +static mp_obj_t async_spi_spi_transfer_end(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { + return common_hal_async_spi_spi_transfer_end(MP_OBJ_TO_PTR(self_in), context, done); +} +static void async_spi_spi_transfer_cancel(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { + common_hal_async_spi_spi_transfer_cancel(MP_OBJ_TO_PTR(self_in), context, done); +} + //| async def configure( //| self, *, baudrate: int = 100000, polarity: int = 0, phase: int = 0, bits: int = 8 //| ) -> None: @@ -112,7 +126,8 @@ static void *async_spi_spi_configure(circuitpy_async_flag_t *done, size_t n_args return common_hal_async_spi_spi_configure_start(self, args[ARG_baudrate].u_int, polarity, phase, bits, done); } -static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_configure_obj, 1, async_spi_spi_configure); +static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_configure_obj, 1, async_spi_spi_configure, + async_spi_spi_configure_end, async_spi_spi_configure_cancel); //| import sys //| @@ -137,7 +152,8 @@ static void *async_spi_spi_write(circuitpy_async_flag_t *done, size_t n_args, co MP_BUFFER_READ, &len); return common_hal_async_spi_spi_write_start(self, data, len, done); } -static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_obj, 1, async_spi_spi_write); +static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_obj, 1, async_spi_spi_write, + async_spi_spi_transfer_end, async_spi_spi_transfer_cancel); //| async def readinto( //| self, @@ -166,7 +182,8 @@ static void *async_spi_spi_readinto(circuitpy_async_flag_t *done, size_t n_args, MP_BUFFER_WRITE, &len); return common_hal_async_spi_spi_readinto_start(self, data, len, (uint8_t)args[ARG_write_value].u_int, done); } -static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_readinto_obj, 1, async_spi_spi_readinto); +static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_readinto_obj, 1, async_spi_spi_readinto, + async_spi_spi_transfer_end, async_spi_spi_transfer_cancel); //| async def write_readinto( //| self, @@ -205,7 +222,8 @@ static void *async_spi_spi_write_readinto(circuitpy_async_flag_t *done, size_t n } return common_hal_async_spi_spi_write_readinto_start(self, data_out, data_in, out_len, done); } -static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_readinto_obj, 2, async_spi_spi_write_readinto); +static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_readinto_obj, 2, async_spi_spi_write_readinto, + async_spi_spi_transfer_end, async_spi_spi_transfer_cancel); //| frequency: int //| """The actual SPI bus frequency. This may not match the frequency requested diff --git a/shared-bindings/async_spi/SPI.h b/shared-bindings/async_spi/SPI.h index cfb7c99e4bc..3ee481966e3 100644 --- a/shared-bindings/async_spi/SPI.h +++ b/shared-bindings/async_spi/SPI.h @@ -21,30 +21,28 @@ bool common_hal_async_spi_spi_deinited(async_spi_spi_obj_t *self); void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self); uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self); +// Each start returns a context for state of that one operation, which end() and cancel() get +// back; NULL when the object holds the state. + // configure(): done is set once the running transfer, if any, has finished and the settings -// are in place. Returns the context that configure_end() and configure_cancel() get. +// are in place. void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done); -mp_obj_t common_hal_async_spi_spi_configure_end(void *context, circuitpy_async_flag_t *done); -void common_hal_async_spi_spi_configure_cancel(void *context, circuitpy_async_flag_t *done); +mp_obj_t common_hal_async_spi_spi_configure_end(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done); +void common_hal_async_spi_spi_configure_cancel(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done); -// Each transfer start sets done once the transfer has finished, possibly from an interrupt, and -// returns the context that transfer_end() and transfer_cancel() get. The buffers must stay valid -// until done is set. transfer_end() and transfer_cancel() act only on the transfer started with -// done. +// Each transfer start sets done once the transfer has finished, possibly from an interrupt. The +// buffers must stay valid until done is set. transfer_end() and transfer_cancel() act only on +// the transfer started with done. void *common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, size_t len, circuitpy_async_flag_t *done); void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value, circuitpy_async_flag_t *done); void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done); -mp_obj_t common_hal_async_spi_spi_transfer_end(void *context, circuitpy_async_flag_t *done); -void common_hal_async_spi_spi_transfer_cancel(void *context, circuitpy_async_flag_t *done); - -// The names CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW looks up; all three transfers end the same way. -#define common_hal_async_spi_spi_write_end common_hal_async_spi_spi_transfer_end -#define common_hal_async_spi_spi_write_cancel common_hal_async_spi_spi_transfer_cancel -#define common_hal_async_spi_spi_readinto_end common_hal_async_spi_spi_transfer_end -#define common_hal_async_spi_spi_readinto_cancel common_hal_async_spi_spi_transfer_cancel -#define common_hal_async_spi_spi_write_readinto_end common_hal_async_spi_spi_transfer_end -#define common_hal_async_spi_spi_write_readinto_cancel common_hal_async_spi_spi_transfer_cancel +mp_obj_t common_hal_async_spi_spi_transfer_end(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done); +void common_hal_async_spi_spi_transfer_cancel(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done); diff --git a/supervisor/shared/awaitable.c b/supervisor/shared/awaitable.c index 45924ec8cf3..772e40b50d1 100644 --- a/supervisor/shared/awaitable.c +++ b/supervisor/shared/awaitable.c @@ -65,7 +65,7 @@ MP_DEFINE_CONST_OBJ_TYPE( static void awaitable_cancel(circuitpy_awaitable_obj_t *self) { if (self->state == CIRCUITPY_AWAITABLE_RUNNING) { self->state = CIRCUITPY_AWAITABLE_FINISHED; - self->fun->cancel(self->context, &self->done); + self->fun->cancel(self->args[0], self->context, &self->done); } } @@ -91,7 +91,7 @@ static mp_obj_t awaitable_iternext(mp_obj_t self_in) { return mp_const_none; } self->state = CIRCUITPY_AWAITABLE_FINISHED; - MP_STATE_THREAD(stop_iteration_arg) = self->fun->end(self->context, &self->done); + MP_STATE_THREAD(stop_iteration_arg) = self->fun->end(self->args[0], self->context, &self->done); return MP_OBJ_STOP_ITERATION; } diff --git a/supervisor/shared/awaitable.h b/supervisor/shared/awaitable.h index 40084338f5b..35bb62bd633 100644 --- a/supervisor/shared/awaitable.h +++ b/supervisor/shared/awaitable.h @@ -6,21 +6,24 @@ // Awaitable functions implemented in C. // -// CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj_name, n_args_min, fun_name) defines obj_name, a callable -// that takes the same arguments as an MP_DEFINE_CONST_FUN_OBJ_KW function but returns an -// awaitable. A call only checks the number of positional arguments; as with a coroutine in +// CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj_name, n_args_min, start, end, cancel) defines obj_name, +// a callable that takes the same arguments as an MP_DEFINE_CONST_FUN_OBJ_KW function but returns +// an awaitable. A call only checks the number of positional arguments; as with a coroutine in // CPython, nothing runs and the arguments are parsed only when the awaitable is awaited. Then: // -// void *fun_name(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, +// void *start(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, // mp_map_t *kw_args) -// parses the arguments, starts the operation and returns a context. The operation sets -// *done, possibly from an interrupt, once it has finished. -// mp_obj_t common_hal__end(void *context, circuitpy_async_flag_t *done) +// parses the arguments, starts the operation and returns a context: state of this one +// operation, or NULL when the object holds it all. The operation sets *done, possibly +// from an interrupt, once it has finished. +// mp_obj_t end(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) // finishes the operation once *done is set and returns the result of the await. -// void common_hal__cancel(void *context, circuitpy_async_flag_t *done) +// void cancel(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) // stops the operation, or finishes it if *done is already set. It must not allocate: it -// may run during GC. done tells which operation is meant, if the context has started -// another one since. +// may run during GC. +// +// self_in is the first positional argument, the object for a method. done tells which operation +// is meant, if the object has started another one since. // // While an operation runs, the awaiting asyncio task waits on asyncio's I/O queue: the awaitable // is a stream that polls readable once *done is set. @@ -34,8 +37,10 @@ typedef void *(*circuitpy_awaitable_start_fn)(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); -typedef mp_obj_t (*circuitpy_awaitable_end_fn)(void *context, circuitpy_async_flag_t *done); -typedef void (*circuitpy_awaitable_cancel_fn)(void *context, circuitpy_async_flag_t *done); +typedef mp_obj_t (*circuitpy_awaitable_end_fn)(mp_obj_t self_in, void *context, + circuitpy_async_flag_t *done); +typedef void (*circuitpy_awaitable_cancel_fn)(mp_obj_t self_in, void *context, + circuitpy_async_flag_t *done); // An async function: calling it returns a circuitpy_awaitable_obj_t. typedef struct { @@ -63,10 +68,10 @@ extern const mp_obj_type_t circuitpy_awaitable_type; enum { CIRCUITPY_AWAITABLE_NEW, CIRCUITPY_AWAITABLE_RUNNING, CIRCUITPY_AWAITABLE_FINISHED }; -#define CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj_name, n_args_min, fun_name) \ +#define CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj_name, n_args_min, start, end, cancel) \ const circuitpy_async_fun_obj_t obj_name = { \ {&circuitpy_async_fun_type}, MP_OBJ_FUN_MAKE_SIG(n_args_min, MP_OBJ_FUN_ARGS_MAX, true), \ - fun_name, common_hal_##fun_name##_end, common_hal_##fun_name##_cancel \ + start, end, cancel \ } #endif // MICROPY_PY_ASYNC_AWAIT From d78eabfa27c68cf495e9e15f3500417df87f50ee Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Wed, 14 Oct 2026 16:24:21 +0000 Subject: [PATCH 14/27] busio: build busio.SPI on async_spi where a port has it shared-module/busio/SPI.c implements busio.SPI over common_hal_async_spi: each blocking call starts a transfer and waits for its flag, the lock sits on top, and FourWire's write_start/end map onto the async start and end. A port turns it on with CIRCUITPY_BUSIO_SPI_OVER_ASYNC, which also builds its async_spi common-hal without the Python module. A driver error reaches busio as false and an await as an exception, through transfer_finish() and transfer_end(). raspberrypi and zephyr-cp use it, so their own busio.SPI goes away. The RP2 driver is always async now and loses its blocking entry points; Zephyr keeps a fixed-device constructor for board.SPI() and reuses the fill buffer of reads. Pico 898680 -> 898760 bytes, Pico 2 W 1375564 -> 1375500; other ports are unchanged. --- ports/raspberrypi/common-hal/async_spi/SPI.c | 16 +- ports/raspberrypi/common-hal/busio/SPI.c | 107 ------ ports/raspberrypi/common-hal/busio/SPI.h | 13 +- .../raspberrypi/common-hal/picogame/Display.c | 2 +- ports/raspberrypi/mpconfigport.mk | 4 +- ports/raspberrypi/peripherals/spi.c | 66 +--- ports/raspberrypi/peripherals/spi.h | 13 +- ports/zephyr-cp/common-hal/async_spi/SPI.c | 102 ++++-- ports/zephyr-cp/common-hal/async_spi/SPI.h | 6 + ports/zephyr-cp/common-hal/busio/SPI.c | 320 +----------------- ports/zephyr-cp/common-hal/busio/SPI.h | 25 +- .../zephyr-cp/cptools/build_circuitpython.py | 6 + py/circuitpy_defns.mk | 8 + py/circuitpy_mpconfig.mk | 4 + shared-bindings/async_spi/SPI.h | 8 + shared-module/busio/SPI.c | 136 ++++++++ shared-module/busio/SPI.h | 24 ++ 17 files changed, 313 insertions(+), 547 deletions(-) delete mode 100644 ports/raspberrypi/common-hal/busio/SPI.c create mode 100644 shared-module/busio/SPI.c create mode 100644 shared-module/busio/SPI.h diff --git a/ports/raspberrypi/common-hal/async_spi/SPI.c b/ports/raspberrypi/common-hal/async_spi/SPI.c index 63c298bd3b3..09dbb21e2a5 100644 --- a/ports/raspberrypi/common-hal/async_spi/SPI.c +++ b/ports/raspberrypi/common-hal/async_spi/SPI.c @@ -28,6 +28,14 @@ void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self) { rp2_spi_deinit(&self->spi); } +void common_hal_async_spi_spi_never_reset(async_spi_spi_obj_t *self) { + rp2_spi_never_reset(&self->spi); +} + +void common_hal_async_spi_spi_mark_deinit(async_spi_spi_obj_t *self) { + self->spi.clock = NULL; +} + void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done) { rp2_spi_configure_start(&self->spi, baudrate, polarity, phase, bits, done); @@ -66,9 +74,15 @@ void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, return NULL; } -mp_obj_t common_hal_async_spi_spi_transfer_end(async_spi_spi_obj_t *self, void *context, +bool common_hal_async_spi_spi_transfer_finish(async_spi_spi_obj_t *self, void *context, circuitpy_async_flag_t *done) { rp2_spi_stop(&self->spi, done); + return true; +} + +mp_obj_t common_hal_async_spi_spi_transfer_end(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { + common_hal_async_spi_spi_transfer_finish(self, context, done); return mp_const_none; } diff --git a/ports/raspberrypi/common-hal/busio/SPI.c b/ports/raspberrypi/common-hal/busio/SPI.c deleted file mode 100644 index 75dc44bebaa..00000000000 --- a/ports/raspberrypi/common-hal/busio/SPI.c +++ /dev/null @@ -1,107 +0,0 @@ -// This file is part of the CircuitPython project: https://circuitpython.org -// -// SPDX-FileCopyrightText: Copyright (c) 2021 Scott Shawcroft for Adafruit Industries -// -// SPDX-License-Identifier: MIT - -#include "shared-bindings/busio/SPI.h" - -#include "py/runtime.h" -#include "shared-bindings/microcontroller/Pin.h" - -void common_hal_busio_spi_construct(busio_spi_obj_t *self, - const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, - const mcu_pin_obj_t *miso, bool half_duplex) { - if (half_duplex) { - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_half_duplex); - } - rp2_spi_error_t error = rp2_spi_construct(&self->spi, clock, mosi, miso); - if (error == RP2_SPI_INVALID_PINS) { - raise_ValueError_invalid_pins(); - } - if (error == RP2_SPI_IN_USE) { - mp_raise_ValueError(MP_ERROR_TEXT("SPI peripheral in use")); - } -} - -void common_hal_busio_spi_never_reset(busio_spi_obj_t *self) { - rp2_spi_never_reset(&self->spi); -} - -bool common_hal_busio_spi_deinited(busio_spi_obj_t *self) { - return rp2_spi_deinited(&self->spi); -} - -void common_hal_busio_spi_mark_deinit(busio_spi_obj_t *self) { - self->spi.clock = NULL; -} - -void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { - rp2_spi_deinit(&self->spi); -} - -bool common_hal_busio_spi_configure(busio_spi_obj_t *self, - uint32_t baudrate, uint8_t polarity, uint8_t phase, uint8_t bits) { - rp2_spi_configure(&self->spi, baudrate, polarity, phase, bits); - return true; -} - -bool common_hal_busio_spi_try_lock(busio_spi_obj_t *self) { - if (common_hal_busio_spi_deinited(self)) { - return false; - } - bool grabbed_lock = false; - if (!self->has_lock) { - grabbed_lock = true; - self->has_lock = true; - } - return grabbed_lock; -} - -bool common_hal_busio_spi_has_lock(busio_spi_obj_t *self) { - return self->has_lock; -} - -void common_hal_busio_spi_unlock(busio_spi_obj_t *self) { - self->has_lock = false; -} - -bool common_hal_busio_spi_write(busio_spi_obj_t *self, - const uint8_t *data, size_t len) { - uint32_t data_in; - rp2_spi_transfer(&self->spi, data, len, (uint8_t *)&data_in, MIN(len, 4)); - return true; -} - -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; - rp2_spi_transfer(&self->spi, (const uint8_t *)&data_out, MIN(4, len), data, len); - return true; -} - -bool common_hal_busio_spi_transfer(busio_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len) { - rp2_spi_transfer(&self->spi, data_out, len, data_in, len); - return true; -} - -void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data, size_t len, - circuitpy_async_flag_t *done) { - rp2_spi_write_start(&self->spi, data, len, done); -} - -void common_hal_busio_spi_end(busio_spi_obj_t *self) { - rp2_spi_end(&self->spi); -} - -uint32_t common_hal_busio_spi_get_frequency(busio_spi_obj_t *self) { - return self->spi.real_frequency; -} - -uint8_t common_hal_busio_spi_get_phase(busio_spi_obj_t *self) { - return self->spi.phase; -} - -uint8_t common_hal_busio_spi_get_polarity(busio_spi_obj_t *self) { - return self->spi.polarity; -} diff --git a/ports/raspberrypi/common-hal/busio/SPI.h b/ports/raspberrypi/common-hal/busio/SPI.h index dd067d6a5c3..8b5ec5c444c 100644 --- a/ports/raspberrypi/common-hal/busio/SPI.h +++ b/ports/raspberrypi/common-hal/busio/SPI.h @@ -1,17 +1,10 @@ // This file is part of the CircuitPython project: https://circuitpython.org // -// SPDX-FileCopyrightText: Copyright (c) 2021 Scott Shawcroft for Adafruit Industries +// SPDX-FileCopyrightText: Copyright (c) 2016 Scott Shawcroft // // SPDX-License-Identifier: MIT #pragma once -#include "py/obj.h" -#include "supervisor/shared/async_flag.h" -#include "peripherals/spi.h" - -typedef struct { - mp_obj_base_t base; - rp2_spi_t spi; - bool has_lock; -} busio_spi_obj_t; +// busio.SPI is built on common-hal/async_spi. +#include "shared-module/busio/SPI.h" diff --git a/ports/raspberrypi/common-hal/picogame/Display.c b/ports/raspberrypi/common-hal/picogame/Display.c index 551b2bc4808..47ed29147ef 100644 --- a/ports/raspberrypi/common-hal/picogame/Display.c +++ b/ports/raspberrypi/common-hal/picogame/Display.c @@ -40,7 +40,7 @@ void common_hal_picogame_display_construct(picogame_display_obj_t *self, MP_QSTR_display, MP_QSTR_FourWire, mp_obj_get_type(display->bus.bus)->name); } fourwire_fourwire_obj_t *fw = MP_OBJ_TO_PTR(display->bus.bus); - self->spi = fw->bus->spi.peripheral; + self->spi = fw->bus->spi.spi.peripheral; #if CIRCUITPY_PICOGAME_RGB444 // Tell the panel which pixel format we'll send (COLMOD). Asserting it here also recovers from diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index 92af16a1c6c..8895f110d47 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -7,7 +7,9 @@ CIRCUITPY_WIFI_RADIO_SETTABLE_MAC_ADDRESS = 0 CIRCUITPY_RP2PIO ?= 1 CIRCUITPY_BUSIO_SPI_ASYNC ?= 1 -CIRCUITPY_ASYNC_SPI ?= $(call enable-if-all,$(MICROPY_PY_ASYNCIO) $(CIRCUITPY_BUSIO_SPI) $(CIRCUITPY_BUSIO_SPI_ASYNC)) +# busio.SPI is built on the port's async_spi implementation; the async_spi module needs asyncio. +CIRCUITPY_BUSIO_SPI_OVER_ASYNC = 1 +CIRCUITPY_ASYNC_SPI ?= $(call enable-if-all,$(MICROPY_PY_ASYNCIO) $(CIRCUITPY_BUSIO_SPI)) # 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/ports/raspberrypi/peripherals/spi.c b/ports/raspberrypi/peripherals/spi.c index 1127e1279cc..441f5e73107 100644 --- a/ports/raspberrypi/peripherals/spi.c +++ b/ports/raspberrypi/peripherals/spi.c @@ -12,13 +12,10 @@ #include "hardware/dma.h" #include "hardware/gpio.h" -#if CIRCUITPY_ASYNC_SPI #include "hardware/irq.h" -#endif #define NO_INSTANCE 0xff -#if CIRCUITPY_ASYNC_SPI // Done flag of the async transfer on each kept RX channel, set when that channel finishes. static volatile bool *async_flags[NUM_DMA_CHANNELS]; // RX channels with the interrupt enabled. @@ -55,7 +52,6 @@ static void async_irq_disable(uint chan) { irq_set_enabled(DMA_IRQ_0, false); } } -#endif rp2_spi_error_t rp2_spi_construct(rp2_spi_t *self, const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso) { @@ -100,9 +96,7 @@ rp2_spi_error_t rp2_spi_construct(rp2_spi_t *self, const mcu_pin_obj_t *clock, self->async_active = false; self->dma_kept = false; - #if CIRCUITPY_ASYNC_SPI self->idle_done = NULL; - #endif self->target_frequency = 250000; self->real_frequency = spi_init(self->peripheral, self->target_frequency); @@ -130,28 +124,20 @@ void rp2_spi_never_reset(rp2_spi_t *self) { common_hal_never_reset_pin(self->MISO); } -#if CIRCUITPY_ASYNC_SPI static void _abort(rp2_spi_t *self); static void _idle(rp2_spi_t *self); -#endif void rp2_spi_deinit(rp2_spi_t *self) { if (rp2_spi_deinited(self)) { return; } - #if CIRCUITPY_ASYNC_SPI if (self->async_active) { _abort(self); *self->async_done = true; } _idle(self); - #else - rp2_spi_end(self); - #endif if (self->dma_kept) { - #if CIRCUITPY_ASYNC_SPI async_irq_disable(self->dma_rx); - #endif dma_channel_unclaim(self->dma_tx); dma_channel_unclaim(self->dma_rx); self->dma_kept = false; @@ -192,14 +178,6 @@ static void _apply(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t self->real_frequency = spi_set_baudrate(self->peripheral, baudrate); } -void rp2_spi_configure(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, - uint8_t bits) { - // A running async transfer finishes with the old settings. - rp2_spi_end(self); - _apply(self, baudrate, polarity, phase, bits); -} - -#if CIRCUITPY_ASYNC_SPI // The bus is idle: apply the settings of a waiting configure() and wake it. static void _idle(rp2_spi_t *self) { if (self->idle_done != NULL) { @@ -234,12 +212,11 @@ void rp2_spi_configure_cancel(rp2_spi_t *self, volatile bool *done) { self->idle_done = NULL; } } -#endif -// 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. An async transfer passes its done flag, and keeps the DMA channels -// until deinit, for buses that send often. Returns whether DMA is running. +// Start a transfer. With DMA it runs in the background and done is set from the interrupt; +// otherwise it is done in software before this returns. An out or in buffer shorter than the +// transfer is one byte repeated or dropped. The DMA channels are kept until deinit, for buses +// that send often. Returns whether DMA is running. static bool _start(rp2_spi_t *self, const uint8_t *data_out, size_t out_len, uint8_t *data_in, size_t in_len, volatile bool *done) { @@ -252,12 +229,8 @@ static bool _start(rp2_spi_t *self, if (chan_tx >= 0 && chan_rx >= 0) { self->dma_tx = chan_tx; self->dma_rx = chan_rx; - self->dma_kept = done != NULL; - #if CIRCUITPY_ASYNC_SPI - if (done != NULL) { - async_irq_enable(chan_rx); - } - #endif + self->dma_kept = true; + async_irq_enable(chan_rx); } else { // If we have claimed only one channel successfully, release it. if (chan_tx >= 0) { @@ -292,10 +265,8 @@ static bool _start(rp2_spi_t *self, len, false); - #if CIRCUITPY_ASYNC_SPI // Before the start, so that a short transfer cannot finish unseen. async_flags[self->dma_rx] = done; - #endif dma_start_channel_mask((1u << self->dma_rx) | (1u << self->dma_tx)); return true; } @@ -330,23 +301,8 @@ static bool _start(rp2_spi_t *self, } // Wait for a DMA transfer started by _start(). The RX channel finishes last. -static void _end(rp2_spi_t *self, bool background_tasks) { +static void _end(rp2_spi_t *self) { 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); - } -} - -void rp2_spi_transfer(rp2_spi_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, NULL)) { - // TODO: We should idle here until we get a DMA interrupt or something else. - _end(self, true); } } @@ -369,7 +325,6 @@ void rp2_spi_write_start(rp2_spi_t *self, const uint8_t *data, size_t len, _async_start(self, data, len, &self->one_byte, 1, done); } -#if CIRCUITPY_ASYNC_SPI void rp2_spi_read_start(rp2_spi_t *self, uint8_t *data, size_t len, uint8_t write_value, volatile bool *done) { // End the previous transfer first: an async write may still be receiving into one_byte. @@ -412,19 +367,14 @@ void rp2_spi_stop(rp2_spi_t *self, volatile bool *done) { } _abort(self); } -#endif void rp2_spi_end(rp2_spi_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); + _end(self); self->async_active = false; - #if CIRCUITPY_ASYNC_SPI async_flags[self->dma_rx] = NULL; - #endif *self->async_done = true; - #if CIRCUITPY_ASYNC_SPI _idle(self); - #endif } } diff --git a/ports/raspberrypi/peripherals/spi.h b/ports/raspberrypi/peripherals/spi.h index 42511ae42a0..70c848ee15c 100644 --- a/ports/raspberrypi/peripherals/spi.h +++ b/ports/raspberrypi/peripherals/spi.h @@ -30,14 +30,12 @@ typedef struct { uint8_t dma_rx; uint8_t one_byte; // the one-byte side of an async write (RX) or read (TX) volatile bool *async_done; - #if CIRCUITPY_ASYNC_SPI // A configure() waiting for the transfer to finish, and its settings. volatile bool *idle_done; uint32_t pending_baudrate; uint8_t pending_polarity; uint8_t pending_phase; uint8_t pending_bits; - #endif } rp2_spi_t; typedef enum { @@ -54,16 +52,9 @@ static inline bool rp2_spi_deinited(rp2_spi_t *self) { return self->clock == NULL; } void rp2_spi_never_reset(rp2_spi_t *self); -// Waits for a running async transfer first. -void rp2_spi_configure(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, - uint8_t bits); -// A blocking transfer. A one-byte side is sent repeatedly or overwritten. -void rp2_spi_transfer(rp2_spi_t *self, const uint8_t *data_out, size_t out_len, - uint8_t *data_in, size_t in_len); - -// Async transfers: done is set once the transfer has finished, by the DMA interrupt with -// CIRCUITPY_ASYNC_SPI and in any case by rp2_spi_end(). The buffers must stay valid until then. +// Transfers: done is set once the transfer has finished, by the DMA interrupt or by the +// software path before the start returns. The buffers must stay valid until then. void rp2_spi_write_start(rp2_spi_t *self, const uint8_t *data, size_t len, volatile bool *done); void rp2_spi_read_start(rp2_spi_t *self, uint8_t *data, size_t len, uint8_t write_value, diff --git a/ports/zephyr-cp/common-hal/async_spi/SPI.c b/ports/zephyr-cp/common-hal/async_spi/SPI.c index bb887e8fc3a..4d5b9d58222 100644 --- a/ports/zephyr-cp/common-hal/async_spi/SPI.c +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.c @@ -8,6 +8,7 @@ #include +#include "py/gc.h" #include "py/runtime.h" #include "supervisor/port.h" #include "bindings/zephyr_kernel/__init__.h" @@ -52,6 +53,35 @@ static void wait_done(async_spi_spi_obj_t *self) { self->done = NULL; } +// A fixed devicetree device, as for board.SPI(): already initialized, never released. +void common_hal_async_spi_spi_construct_from_device(async_spi_spi_obj_t *self, + const struct device *dev) { + self->spi_device = dev; + self->dynamic = false; + self->clock = NULL; + self->mosi = NULL; + self->miso = NULL; + self->active_config = 0; + for (size_t i = 0; i < 2; i++) { + self->config[i].frequency = 100000; + self->config[i].operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_LINES_SINGLE; + } + self->done = NULL; + self->fill = NULL; + self->fill_len = 0; + self->fill_from_port = false; + self->idle_done = NULL; +} + +static void free_fill(async_spi_spi_obj_t *self) { + if (self->fill_from_port) { + port_free(self->fill); + } + self->fill = NULL; + self->fill_len = 0; + self->fill_from_port = false; +} + void common_hal_async_spi_spi_construct(async_spi_spi_obj_t *self, const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso) { const struct device *dev = NULL; @@ -68,18 +98,11 @@ void common_hal_async_spi_spi_construct(async_spi_spi_obj_t *self, const mcu_pin mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Use device tree to define %q devices"), MP_QSTR_SPI); } - self->spi_device = dev; + common_hal_async_spi_spi_construct_from_device(self, dev); + self->dynamic = true; self->clock = clock; self->mosi = mosi; self->miso = miso; - self->active_config = 0; - for (size_t i = 0; i < 2; i++) { - self->config[i].frequency = 100000; - self->config[i].operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_LINES_SINGLE; - } - self->done = NULL; - self->fill = NULL; - self->idle_done = NULL; // A routed device is initialized now; a fixed one already was (-EALREADY). int init_ret = device_init(dev); @@ -98,6 +121,11 @@ void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self) { return; } wait_done(self); + free_fill(self); + // A fixed devicetree device (board.SPI()) stays usable. + if (!self->dynamic) { + return; + } (void)iobroker_release(self->spi_device); self->spi_device = NULL; self->clock = NULL; @@ -105,6 +133,15 @@ void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self) { self->miso = NULL; } +void common_hal_async_spi_spi_never_reset(async_spi_spi_obj_t *self) { +} + +void common_hal_async_spi_spi_mark_deinit(async_spi_spi_obj_t *self) { + if (self->dynamic) { + self->spi_device = NULL; + } +} + static uint16_t operation_of(uint8_t polarity, uint8_t phase, uint8_t bits) { uint16_t operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(bits) | SPI_LINES_SINGLE; if (polarity) { @@ -178,9 +215,10 @@ static void start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *d transfer_done, self); } if (ret < 0) { + // Reported by transfer_end() or transfer_finish(); nothing raises here, as a + // blocking busio call may run from a background callback. + self->result = ret; CIRCUITPY_ASYNC_FLAG_SET(done); - self->done = NULL; - raise_zephyr_error(ret); } } @@ -192,14 +230,32 @@ void *common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value, circuitpy_async_flag_t *done) { + // Without a TX buffer Zephyr sends zeros; another value needs a buffer, kept for the next + // read. The previous transfer must be over before it is touched. wait_done(self); - // Without a TX buffer Zephyr sends zeros; another value needs a buffer. uint8_t *fill = NULL; - if (write_value != 0) { - fill = m_malloc(len); + if (write_value != 0 && len != 0) { + if (self->fill_len < len) { + free_fill(self); + if (gc_alloc_possible()) { + self->fill = m_malloc(len); + } else { + self->fill = port_malloc(len, false); + self->fill_from_port = true; + } + if (self->fill == NULL) { + self->fill_from_port = false; + self->result = -ENOMEM; + CIRCUITPY_ASYNC_FLAG_INIT(done); + CIRCUITPY_ASYNC_FLAG_SET(done); + self->done = done; + return NULL; + } + self->fill_len = len; + } + fill = self->fill; memset(fill, write_value, len); } - self->fill = fill; start(self, fill, data, len, done); return NULL; } @@ -211,13 +267,19 @@ void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, } // Called once done is set. +bool common_hal_async_spi_spi_transfer_finish(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { + if (self->done != done) { + return true; + } + self->done = NULL; + return self->result == 0; +} + mp_obj_t common_hal_async_spi_spi_transfer_end(async_spi_spi_obj_t *self, void *context, circuitpy_async_flag_t *done) { - if (self->done == done) { - self->done = NULL; - if (self->result != 0) { - raise_zephyr_error(self->result); - } + if (!common_hal_async_spi_spi_transfer_finish(self, context, done)) { + raise_zephyr_error(self->result); } return mp_const_none; } diff --git a/ports/zephyr-cp/common-hal/async_spi/SPI.h b/ports/zephyr-cp/common-hal/async_spi/SPI.h index 0b5db9237fd..319ce3a8254 100644 --- a/ports/zephyr-cp/common-hal/async_spi/SPI.h +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.h @@ -17,6 +17,7 @@ typedef struct { mp_obj_base_t base; const struct device *spi_device; // NULL once deinited + bool dynamic; // routed to pins at construction, released at deinit struct spi_config config[2]; // two slots: a driver sees a change by the pointer uint8_t active_config; const mcu_pin_obj_t *clock; @@ -29,7 +30,12 @@ typedef struct { struct spi_buf_set tx; struct spi_buf_set rx; uint8_t *fill; // the bytes a read sends, when not zero + size_t fill_len; + bool fill_from_port; // fill came from port_malloc, not the heap circuitpy_async_flag_t *idle_done; // a configure() waiting for the transfer to finish uint32_t pending_frequency; uint16_t pending_operation; } async_spi_spi_obj_t; + +void common_hal_async_spi_spi_construct_from_device(async_spi_spi_obj_t *self, + const struct device *dev); diff --git a/ports/zephyr-cp/common-hal/busio/SPI.c b/ports/zephyr-cp/common-hal/busio/SPI.c index 78a0943354c..3071c0eed71 100644 --- a/ports/zephyr-cp/common-hal/busio/SPI.c +++ b/ports/zephyr-cp/common-hal/busio/SPI.c @@ -5,325 +5,13 @@ // SPDX-License-Identifier: MIT #include "shared-bindings/busio/SPI.h" -#include "shared-bindings/microcontroller/Pin.h" -#include "py/mperrno.h" -#include "py/runtime.h" -#include "py/gc.h" -#include "shared/runtime/interrupt_char.h" -#include "supervisor/port.h" - -#include "bindings/zephyr_kernel/__init__.h" - -#include -#include -#include -#include -#include - -// Helper function for Zephyr-specific initialization from device tree mp_obj_t common_hal_busio_spi_construct_from_device(busio_spi_obj_t *self, const struct device *spi_device) { self->base.type = &busio_spi_type; - self->spi_device = spi_device; - k_mutex_init(&self->mutex); + common_hal_async_spi_spi_construct_from_device(&self->spi, spi_device); + self->sent = NULL; self->has_lock = false; - self->active_config = 0; - self->dynamic = false; - self->clock = NULL; - self->mosi = NULL; - self->miso = NULL; - - k_poll_signal_init(&self->signal); - - // Default configuration for both config slots - self->config[0].frequency = 100000; - self->config[0].operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_LINES_SINGLE; - self->config[1].frequency = 100000; - self->config[1].operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_LINES_SINGLE; - + self->polarity = 0; + self->phase = 0; return MP_OBJ_FROM_PTR(self); } - -// Standard busio construct: pick a free peripheral instance and route it to -// the requested pins at runtime (supported on nRF SoCs). -void common_hal_busio_spi_construct(busio_spi_obj_t *self, - const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, - const mcu_pin_obj_t *miso, bool half_duplex) { - if (half_duplex) { - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_half_duplex); - } - - const struct device *dev = NULL; - int ret = iobroker_spi_allocate(clock->package_pin, - mosi != NULL ? mosi->package_pin : IOBROKER_NO_PIN, - miso != NULL ? miso->package_pin : IOBROKER_NO_PIN, &dev); - if (ret < 0) { - if (ret == -ENODEV) { - mp_raise_ValueError(MP_ERROR_TEXT("All SPI peripherals are in use")); - } - if (ret == -EBUSY) { - mp_raise_ValueError(MP_ERROR_TEXT("Internal resource(s) in use")); - } - if (ret == -EINVAL) { - raise_ValueError_invalid_pins(); - } - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Use device tree to define %q devices"), MP_QSTR_SPI); - } - - common_hal_busio_spi_construct_from_device(self, dev); - self->dynamic = true; - self->clock = clock; - self->mosi = mosi; - self->miso = miso; - - // Initialize the deferred device now that it is routed to the requested - // pins. Fixed devicetree instances are already initialized (-EALREADY). - int init_ret = device_init(dev); - if (init_ret < 0 && init_ret != -EALREADY) { - // The failed init may have routed pins and left the device in a - // partial state; deinit gives up the claim and resets the pins. - common_hal_busio_spi_deinit(self); - raise_zephyr_error(init_ret); - } -} - -bool common_hal_busio_spi_deinited(busio_spi_obj_t *self) { - return self->spi_device == NULL; -} - -void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { - if (common_hal_busio_spi_deinited(self)) { - return; - } - if (self->dynamic) { - // The release de-inits the device, which applies its low-power - // pinctrl state and leaves the routed pins disconnected. - (void)iobroker_release(self->spi_device); - self->clock = NULL; - self->mosi = NULL; - self->miso = NULL; - self->spi_device = NULL; - } -} - -void common_hal_busio_spi_mark_deinit(busio_spi_obj_t *self) { - if (self->dynamic) { - self->spi_device = NULL; - } -} - -bool common_hal_busio_spi_try_lock(busio_spi_obj_t *self) { - if (common_hal_busio_spi_deinited(self)) { - return false; - } - - self->has_lock = k_mutex_lock(&self->mutex, K_NO_WAIT) == 0; - return self->has_lock; -} - -bool common_hal_busio_spi_has_lock(busio_spi_obj_t *self) { - return self->has_lock; -} - -void common_hal_busio_spi_unlock(busio_spi_obj_t *self) { - self->has_lock = false; - k_mutex_unlock(&self->mutex); -} - -bool common_hal_busio_spi_configure(busio_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, uint8_t bits) { - if (common_hal_busio_spi_deinited(self)) { - return false; - } - - // Set operation mode based on polarity and phase - uint16_t operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(bits) | SPI_LINES_SINGLE; - - if (polarity) { - operation |= SPI_MODE_CPOL; - } - if (phase) { - operation |= SPI_MODE_CPHA; - } - - // Check if settings have changed. We must switch to the other config slot if they have because - // Zephyr drivers are allowed to use the pointer value to know if it has changed. - struct spi_config *current_config = &self->config[self->active_config]; - if (current_config->frequency != baudrate || current_config->operation != operation) { - // Settings changed, switch to the other config slot - self->active_config = 1 - self->active_config; - - // Update the new active configuration - self->config[self->active_config].frequency = baudrate; - self->config[self->active_config].operation = operation; - } - - return true; -} - -bool common_hal_busio_spi_write(busio_spi_obj_t *self, const uint8_t *data, size_t len) { - if (common_hal_busio_spi_deinited(self)) { - return false; - } - - if (len == 0) { - return true; - } - - const struct spi_buf tx_buf = { - .buf = (void *)data, - .len = len - }; - const struct spi_buf_set tx = { - .buffers = &tx_buf, - .count = 1 - }; - - // Initialize the signal for async operation - k_poll_signal_reset(&self->signal); - - int ret = spi_transceive_signal(self->spi_device, &self->config[self->active_config], &tx, NULL, &self->signal); - if (ret != 0) { - return false; - } - - // Wait for the transfer to complete while running background tasks - int signaled = 0; - int result = 0; - while (!signaled && !mp_hal_is_interrupted()) { - RUN_BACKGROUND_TASKS; - k_poll_signal_check(&self->signal, &signaled, &result); - } - - return signaled && result == 0; -} - -bool common_hal_busio_spi_read(busio_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value) { - if (common_hal_busio_spi_deinited(self)) { - return false; - } - - if (len == 0) { - return true; - } - - // For read, we need to write dummy bytes - // We'll allocate a temporary buffer if write_value is not 0 - uint8_t *tx_data = NULL; - bool need_free = false; - bool used_port_malloc = false; - - if (write_value != 0) { - // Use port_malloc if GC isn't active, otherwise use m_malloc - if (gc_alloc_possible()) { - tx_data = m_malloc(len); - } else { - tx_data = port_malloc(len, false); - used_port_malloc = true; - } - if (tx_data == NULL) { - return false; - } - memset(tx_data, write_value, len); - need_free = true; - } - - const struct spi_buf tx_buf = { - .buf = tx_data, - .len = tx_data ? len : 0 - }; - const struct spi_buf_set tx = { - .buffers = &tx_buf, - .count = tx_data ? 1 : 0 - }; - - const struct spi_buf rx_buf = { - .buf = data, - .len = len - }; - const struct spi_buf_set rx = { - .buffers = &rx_buf, - .count = 1 - }; - - // Initialize the signal for async operation - k_poll_signal_reset(&self->signal); - - int ret = spi_transceive_signal(self->spi_device, &self->config[self->active_config], &tx, &rx, &self->signal); - - // Wait for the transfer to complete while running background tasks. The driver may read the - // fill buffer until then. - int signaled = 0; - int result = 0; - while (ret == 0 && !signaled && !mp_hal_is_interrupted()) { - RUN_BACKGROUND_TASKS; - k_poll_signal_check(&self->signal, &signaled, &result); - } - - if (need_free) { - if (used_port_malloc) { - port_free(tx_data); - } else { - m_free(tx_data); - } - } - - return ret == 0 && signaled && result == 0; -} - -bool common_hal_busio_spi_transfer(busio_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len) { - if (common_hal_busio_spi_deinited(self)) { - return false; - } - - if (len == 0) { - return true; - } - - const struct spi_buf tx_buf = { - .buf = (void *)data_out, - .len = len - }; - const struct spi_buf_set tx = { - .buffers = &tx_buf, - .count = 1 - }; - - const struct spi_buf rx_buf = { - .buf = data_in, - .len = len - }; - const struct spi_buf_set rx = { - .buffers = &rx_buf, - .count = 1 - }; - - // Initialize the signal for async operation - k_poll_signal_reset(&self->signal); - - int ret = spi_transceive_signal(self->spi_device, &self->config[self->active_config], &tx, &rx, &self->signal); - if (ret != 0) { - return false; - } - - // Wait for the transfer to complete while running background tasks - int signaled = 0; - int result = 0; - while (!signaled && !mp_hal_is_interrupted()) { - RUN_BACKGROUND_TASKS; - k_poll_signal_check(&self->signal, &signaled, &result); - } - - return signaled && result == 0; -} - -uint32_t common_hal_busio_spi_get_frequency(busio_spi_obj_t *self) { - return self->config[self->active_config].frequency; -} - -uint8_t common_hal_busio_spi_get_phase(busio_spi_obj_t *self) { - return (self->config[self->active_config].operation & SPI_MODE_CPHA) ? 1 : 0; -} - -uint8_t common_hal_busio_spi_get_polarity(busio_spi_obj_t *self) { - return (self->config[self->active_config].operation & SPI_MODE_CPOL) ? 1 : 0; -} diff --git a/ports/zephyr-cp/common-hal/busio/SPI.h b/ports/zephyr-cp/common-hal/busio/SPI.h index fa5de574116..3d171915dd2 100644 --- a/ports/zephyr-cp/common-hal/busio/SPI.h +++ b/ports/zephyr-cp/common-hal/busio/SPI.h @@ -6,27 +6,8 @@ #pragma once -#include "py/obj.h" +// busio.SPI is built on common-hal/async_spi. +#include "shared-module/busio/SPI.h" -#include "common-hal/microcontroller/Pin.h" -#include -#include - -typedef struct { - mp_obj_base_t base; - const struct device *spi_device; - struct k_mutex mutex; - struct spi_config config[2]; // Two configs for pointer comparison by driver - uint8_t active_config; // Index of currently active config (0 or 1) - struct k_poll_signal signal; - const mcu_pin_obj_t *clock; - const mcu_pin_obj_t *mosi; - const mcu_pin_obj_t *miso; - // True when the underlying Zephyr device was dynamically routed to the - // pins above at construction time. - bool dynamic; - bool has_lock; -} busio_spi_obj_t; - -// Helper function for Zephyr-specific initialization from device tree +// A fixed devicetree device, for the generated board.SPI(). mp_obj_t common_hal_busio_spi_construct_from_device(busio_spi_obj_t *self, const struct device *spi_device); diff --git a/ports/zephyr-cp/cptools/build_circuitpython.py b/ports/zephyr-cp/cptools/build_circuitpython.py index 74d3eed9368..1581a4db8fa 100644 --- a/ports/zephyr-cp/cptools/build_circuitpython.py +++ b/ports/zephyr-cp/cptools/build_circuitpython.py @@ -653,6 +653,10 @@ async def build_circuitpython(): # noqa: C901 # have no common-hal override, so always compile them. if module.name == "usb_hid": hal_source.append(top / "shared-module/usb_hid/report_descriptors.c") + # busio.SPI is built on common-hal/async_spi, with or without the async_spi module. + if module.name == "busio": + hal_source.append(top / "shared-module/busio/SPI.c") + hal_source.append(top / "ports/zephyr-cp/common-hal/async_spi/SPI.c") hal_source.extend(top.glob(f"shared-bindings/{module.name}/**/*.c")) if module.name in LIBRARY_SOURCE: for library_source in LIBRARY_SOURCE[module.name]: @@ -722,6 +726,8 @@ async def build_circuitpython(): # noqa: C901 f"-DCIRCUITPY_WIFI_PING={1 if mpconfigboard.get('CIRCUITPY_WIFI_PING', True) else 0}" ) + # The busio and async_spi modules both name common-hal/async_spi/SPI.c. + hal_source = list(dict.fromkeys(hal_source)) source_files = supervisor_source + hal_source + ["extmod/vfs.c"] if filesystem_littlefs: source_files.extend( diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 6ddcfddcaa8..26b48baccfb 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -905,6 +905,14 @@ SRC_SHARED_MODULE_EXPANDED = $(addprefix shared-bindings/, $(SRC_SHARED_MODULE)) $(addprefix shared-module/, $(SRC_SHARED_MODULE)) \ $(addprefix shared-module/, $(SRC_SHARED_MODULE_INTERNAL)) +ifeq ($(CIRCUITPY_BUSIO_SPI_OVER_ASYNC),1) +# busio.SPI comes from shared-module, on the port's async_spi common-hal, which is then built +# even without the async_spi module. +SRC_COMMON_HAL_EXPANDED := $(filter-out common-hal/busio/SPI.c, $(SRC_COMMON_HAL_EXPANDED)) \ + common-hal/async_spi/SPI.c +SRC_SHARED_MODULE_EXPANDED += shared-module/busio/SPI.c +endif + # There may be duplicates between SRC_COMMON_HAL_EXPANDED and SRC_SHARED_MODULE_EXPANDED, # because a few modules have files both in common-hal/ and shared-module/. # Doing a $(sort ...) removes duplicates as part of sorting. diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index e99ea370e92..9586cbef6b9 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -129,6 +129,10 @@ CFLAGS += -DCIRCUITPY_ARRAY=$(CIRCUITPY_ARRAY) CIRCUITPY_ASYNC_SPI ?= 0 CFLAGS += -DCIRCUITPY_ASYNC_SPI=$(CIRCUITPY_ASYNC_SPI) +# busio.SPI from shared-module/busio/SPI.c, on the port's common-hal/async_spi. Used by the +# build only. +CIRCUITPY_BUSIO_SPI_OVER_ASYNC ?= 0 + CIRCUITPY_ATEXIT ?= $(CIRCUITPY_FULL_BUILD) CFLAGS += -DCIRCUITPY_ATEXIT=$(CIRCUITPY_ATEXIT) diff --git a/shared-bindings/async_spi/SPI.h b/shared-bindings/async_spi/SPI.h index 3ee481966e3..ddabc3f72e5 100644 --- a/shared-bindings/async_spi/SPI.h +++ b/shared-bindings/async_spi/SPI.h @@ -19,6 +19,9 @@ void common_hal_async_spi_spi_construct(async_spi_spi_obj_t *self, const mcu_pin bool common_hal_async_spi_spi_deinited(async_spi_spi_obj_t *self); // Stops a running transfer. void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self); +// For busio.SPI built on this implementation (shared-module/busio/SPI.c). +void common_hal_async_spi_spi_never_reset(async_spi_spi_obj_t *self); +void common_hal_async_spi_spi_mark_deinit(async_spi_spi_obj_t *self); uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self); // Each start returns a context for state of that one operation, which end() and cancel() get @@ -42,6 +45,11 @@ void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t size_t len, uint8_t write_value, circuitpy_async_flag_t *done); void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done); +// Finishes the transfer started with done, once done is set; false if the driver reported an +// error. For busio.SPI, which must not raise from a background callback. +bool common_hal_async_spi_spi_transfer_finish(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done); +// The same for an await: raises on an error. mp_obj_t common_hal_async_spi_spi_transfer_end(async_spi_spi_obj_t *self, void *context, circuitpy_async_flag_t *done); void common_hal_async_spi_spi_transfer_cancel(async_spi_spi_obj_t *self, void *context, diff --git a/shared-module/busio/SPI.c b/shared-module/busio/SPI.c new file mode 100644 index 00000000000..45807f18157 --- /dev/null +++ b/shared-module/busio/SPI.c @@ -0,0 +1,136 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/busio/SPI.h" + +#include "py/runtime.h" +#include "supervisor/background_callback.h" + +static void wait(circuitpy_async_flag_t *done) { + while (!CIRCUITPY_ASYNC_FLAG_IS_SET(done)) { + RUN_BACKGROUND_TASKS; + } +} + +#if CIRCUITPY_BUSIO_SPI_ASYNC +// Finish a write_start(). No background tasks: the caller holds the bus inside a transaction, +// and one of them may want it. +static void end_sent(busio_spi_obj_t *self) { + if (self->sent == NULL) { + return; + } + while (!CIRCUITPY_ASYNC_FLAG_IS_SET(self->sent)) { + } + common_hal_async_spi_spi_transfer_finish(&self->spi, self->sent_context, self->sent); + self->sent = NULL; +} +#endif + +void common_hal_busio_spi_construct(busio_spi_obj_t *self, const mcu_pin_obj_t *clock, + const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso, bool half_duplex) { + if (half_duplex) { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_half_duplex); + } + common_hal_async_spi_spi_construct(&self->spi, clock, mosi, miso); + self->sent = NULL; + self->has_lock = false; + self->polarity = 0; + self->phase = 0; +} + +#if CIRCUITPY_BULK_RESET +void common_hal_busio_spi_never_reset(busio_spi_obj_t *self) { + common_hal_async_spi_spi_never_reset(&self->spi); +} +#endif + +bool common_hal_busio_spi_deinited(busio_spi_obj_t *self) { + return common_hal_async_spi_spi_deinited(&self->spi); +} + +void common_hal_busio_spi_mark_deinit(busio_spi_obj_t *self) { + common_hal_async_spi_spi_mark_deinit(&self->spi); +} + +void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { + common_hal_async_spi_spi_deinit(&self->spi); +} + +bool common_hal_busio_spi_configure(busio_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, + uint8_t phase, uint8_t bits) { + #if CIRCUITPY_BUSIO_SPI_ASYNC + end_sent(self); + #endif + void *context = common_hal_async_spi_spi_configure_start(&self->spi, baudrate, polarity, phase, + bits, &self->done); + wait(&self->done); + common_hal_async_spi_spi_configure_end(&self->spi, context, &self->done); + self->polarity = polarity; + self->phase = phase; + return true; +} + +bool common_hal_busio_spi_try_lock(busio_spi_obj_t *self) { + if (common_hal_busio_spi_deinited(self) || self->has_lock) { + return false; + } + self->has_lock = true; + return true; +} + +bool common_hal_busio_spi_has_lock(busio_spi_obj_t *self) { + return self->has_lock; +} + +void common_hal_busio_spi_unlock(busio_spi_obj_t *self) { + self->has_lock = false; +} + +// Finish the transfer started with self->done. +static bool finish(busio_spi_obj_t *self, void *context) { + wait(&self->done); + return common_hal_async_spi_spi_transfer_finish(&self->spi, context, &self->done); +} + +bool common_hal_busio_spi_write(busio_spi_obj_t *self, const uint8_t *data, size_t len) { + return finish(self, common_hal_async_spi_spi_write_start(&self->spi, data, len, &self->done)); +} + +bool common_hal_busio_spi_read(busio_spi_obj_t *self, uint8_t *data, size_t len, + uint8_t write_value) { + return finish(self, common_hal_async_spi_spi_readinto_start(&self->spi, data, len, write_value, + &self->done)); +} + +bool common_hal_busio_spi_transfer(busio_spi_obj_t *self, const uint8_t *data_out, + uint8_t *data_in, size_t len) { + return finish(self, common_hal_async_spi_spi_write_readinto_start(&self->spi, data_out, data_in, + len, &self->done)); +} + +#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) { + self->sent_context = common_hal_async_spi_spi_write_start(&self->spi, data, len, done); + self->sent = done; +} + +void common_hal_busio_spi_end(busio_spi_obj_t *self) { + end_sent(self); +} +#endif + +uint32_t common_hal_busio_spi_get_frequency(busio_spi_obj_t *self) { + return common_hal_async_spi_spi_get_frequency(&self->spi); +} + +uint8_t common_hal_busio_spi_get_phase(busio_spi_obj_t *self) { + return self->phase; +} + +uint8_t common_hal_busio_spi_get_polarity(busio_spi_obj_t *self) { + return self->polarity; +} diff --git a/shared-module/busio/SPI.h b/shared-module/busio/SPI.h new file mode 100644 index 00000000000..3e0c38a82c9 --- /dev/null +++ b/shared-module/busio/SPI.h @@ -0,0 +1,24 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +// busio.SPI built on a port's async_spi implementation: every blocking call starts an async +// transfer and waits for it. A port that has async_spi includes this header from its +// common-hal/busio/SPI.h and sets CIRCUITPY_BUSIO_SPI_OVER_ASYNC = 1 in its mpconfigport.mk. + +#pragma once + +#include "shared-bindings/async_spi/SPI.h" + +typedef struct { + mp_obj_base_t base; + async_spi_spi_obj_t spi; + circuitpy_async_flag_t done; // of the blocking transfer in progress + circuitpy_async_flag_t *sent; // of a write_start() not yet ended, NULL otherwise + void *sent_context; + bool has_lock; + uint8_t polarity; + uint8_t phase; +} busio_spi_obj_t; From cff9b570abefb6b854cdedf5281f4004cf45917d Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Wed, 14 Oct 2026 17:55:13 +0000 Subject: [PATCH 15/27] busio: name the embedded async_spi object async_spi bus->spi.spi.peripheral read as a typo. --- .../raspberrypi/common-hal/picogame/Display.c | 2 +- ports/zephyr-cp/common-hal/busio/SPI.c | 2 +- shared-module/busio/SPI.c | 28 +++++++++---------- shared-module/busio/SPI.h | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/ports/raspberrypi/common-hal/picogame/Display.c b/ports/raspberrypi/common-hal/picogame/Display.c index 47ed29147ef..d4373db2269 100644 --- a/ports/raspberrypi/common-hal/picogame/Display.c +++ b/ports/raspberrypi/common-hal/picogame/Display.c @@ -40,7 +40,7 @@ void common_hal_picogame_display_construct(picogame_display_obj_t *self, MP_QSTR_display, MP_QSTR_FourWire, mp_obj_get_type(display->bus.bus)->name); } fourwire_fourwire_obj_t *fw = MP_OBJ_TO_PTR(display->bus.bus); - self->spi = fw->bus->spi.spi.peripheral; + self->spi = fw->bus->async_spi.spi.peripheral; #if CIRCUITPY_PICOGAME_RGB444 // Tell the panel which pixel format we'll send (COLMOD). Asserting it here also recovers from diff --git a/ports/zephyr-cp/common-hal/busio/SPI.c b/ports/zephyr-cp/common-hal/busio/SPI.c index 3071c0eed71..bb11d57d2eb 100644 --- a/ports/zephyr-cp/common-hal/busio/SPI.c +++ b/ports/zephyr-cp/common-hal/busio/SPI.c @@ -8,7 +8,7 @@ mp_obj_t common_hal_busio_spi_construct_from_device(busio_spi_obj_t *self, const struct device *spi_device) { self->base.type = &busio_spi_type; - common_hal_async_spi_spi_construct_from_device(&self->spi, spi_device); + common_hal_async_spi_spi_construct_from_device(&self->async_spi, spi_device); self->sent = NULL; self->has_lock = false; self->polarity = 0; diff --git a/shared-module/busio/SPI.c b/shared-module/busio/SPI.c index 45807f18157..8178ecbd5e0 100644 --- a/shared-module/busio/SPI.c +++ b/shared-module/busio/SPI.c @@ -24,7 +24,7 @@ static void end_sent(busio_spi_obj_t *self) { } while (!CIRCUITPY_ASYNC_FLAG_IS_SET(self->sent)) { } - common_hal_async_spi_spi_transfer_finish(&self->spi, self->sent_context, self->sent); + common_hal_async_spi_spi_transfer_finish(&self->async_spi, self->sent_context, self->sent); self->sent = NULL; } #endif @@ -34,7 +34,7 @@ void common_hal_busio_spi_construct(busio_spi_obj_t *self, const mcu_pin_obj_t * if (half_duplex) { mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_half_duplex); } - common_hal_async_spi_spi_construct(&self->spi, clock, mosi, miso); + common_hal_async_spi_spi_construct(&self->async_spi, clock, mosi, miso); self->sent = NULL; self->has_lock = false; self->polarity = 0; @@ -43,20 +43,20 @@ void common_hal_busio_spi_construct(busio_spi_obj_t *self, const mcu_pin_obj_t * #if CIRCUITPY_BULK_RESET void common_hal_busio_spi_never_reset(busio_spi_obj_t *self) { - common_hal_async_spi_spi_never_reset(&self->spi); + common_hal_async_spi_spi_never_reset(&self->async_spi); } #endif bool common_hal_busio_spi_deinited(busio_spi_obj_t *self) { - return common_hal_async_spi_spi_deinited(&self->spi); + return common_hal_async_spi_spi_deinited(&self->async_spi); } void common_hal_busio_spi_mark_deinit(busio_spi_obj_t *self) { - common_hal_async_spi_spi_mark_deinit(&self->spi); + common_hal_async_spi_spi_mark_deinit(&self->async_spi); } void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { - common_hal_async_spi_spi_deinit(&self->spi); + common_hal_async_spi_spi_deinit(&self->async_spi); } bool common_hal_busio_spi_configure(busio_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, @@ -64,10 +64,10 @@ bool common_hal_busio_spi_configure(busio_spi_obj_t *self, uint32_t baudrate, ui #if CIRCUITPY_BUSIO_SPI_ASYNC end_sent(self); #endif - void *context = common_hal_async_spi_spi_configure_start(&self->spi, baudrate, polarity, phase, + void *context = common_hal_async_spi_spi_configure_start(&self->async_spi, baudrate, polarity, phase, bits, &self->done); wait(&self->done); - common_hal_async_spi_spi_configure_end(&self->spi, context, &self->done); + common_hal_async_spi_spi_configure_end(&self->async_spi, context, &self->done); self->polarity = polarity; self->phase = phase; return true; @@ -92,29 +92,29 @@ void common_hal_busio_spi_unlock(busio_spi_obj_t *self) { // Finish the transfer started with self->done. static bool finish(busio_spi_obj_t *self, void *context) { wait(&self->done); - return common_hal_async_spi_spi_transfer_finish(&self->spi, context, &self->done); + return common_hal_async_spi_spi_transfer_finish(&self->async_spi, context, &self->done); } bool common_hal_busio_spi_write(busio_spi_obj_t *self, const uint8_t *data, size_t len) { - return finish(self, common_hal_async_spi_spi_write_start(&self->spi, data, len, &self->done)); + return finish(self, common_hal_async_spi_spi_write_start(&self->async_spi, data, len, &self->done)); } bool common_hal_busio_spi_read(busio_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value) { - return finish(self, common_hal_async_spi_spi_readinto_start(&self->spi, data, len, write_value, + return finish(self, common_hal_async_spi_spi_readinto_start(&self->async_spi, data, len, write_value, &self->done)); } bool common_hal_busio_spi_transfer(busio_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len) { - return finish(self, common_hal_async_spi_spi_write_readinto_start(&self->spi, data_out, data_in, + return finish(self, common_hal_async_spi_spi_write_readinto_start(&self->async_spi, data_out, data_in, len, &self->done)); } #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) { - self->sent_context = common_hal_async_spi_spi_write_start(&self->spi, data, len, done); + self->sent_context = common_hal_async_spi_spi_write_start(&self->async_spi, data, len, done); self->sent = done; } @@ -124,7 +124,7 @@ void common_hal_busio_spi_end(busio_spi_obj_t *self) { #endif uint32_t common_hal_busio_spi_get_frequency(busio_spi_obj_t *self) { - return common_hal_async_spi_spi_get_frequency(&self->spi); + return common_hal_async_spi_spi_get_frequency(&self->async_spi); } uint8_t common_hal_busio_spi_get_phase(busio_spi_obj_t *self) { diff --git a/shared-module/busio/SPI.h b/shared-module/busio/SPI.h index 3e0c38a82c9..5162467e054 100644 --- a/shared-module/busio/SPI.h +++ b/shared-module/busio/SPI.h @@ -14,7 +14,7 @@ typedef struct { mp_obj_base_t base; - async_spi_spi_obj_t spi; + async_spi_spi_obj_t async_spi; circuitpy_async_flag_t done; // of the blocking transfer in progress circuitpy_async_flag_t *sent; // of a write_start() not yet ended, NULL otherwise void *sent_context; From 4546345d6464ac12a1fbdf71a4fd5e6bb061d650 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Wed, 14 Oct 2026 18:08:10 +0000 Subject: [PATCH 16/27] zephyr-cp: send write_value from a buffer also when it is 0 Without a TX buffer the nRF SPIM driver sends its over-read character, 0xFF, so readinto() with the default write_value clocked out 0xFF instead of 0. Seen on a Feather nRF52840; native_sim's loopback sends zeros, which hid it. --- ports/zephyr-cp/common-hal/async_spi/SPI.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ports/zephyr-cp/common-hal/async_spi/SPI.c b/ports/zephyr-cp/common-hal/async_spi/SPI.c index 4d5b9d58222..07410fe359f 100644 --- a/ports/zephyr-cp/common-hal/async_spi/SPI.c +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.c @@ -230,11 +230,12 @@ void *common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value, circuitpy_async_flag_t *done) { - // Without a TX buffer Zephyr sends zeros; another value needs a buffer, kept for the next - // read. The previous transfer must be over before it is touched. + // Without a TX buffer a driver sends its own idle value (0xFF on nRF), so the read sends + // write_value from a buffer, kept for the next read. The previous transfer must be over + // before it is touched. wait_done(self); uint8_t *fill = NULL; - if (write_value != 0 && len != 0) { + if (len != 0) { if (self->fill_len < len) { free_fill(self); if (gc_alloc_possible()) { From 6f5d10aed17cfeb66d0004ea02282527f04fd039 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Wed, 14 Oct 2026 20:50:28 +0000 Subject: [PATCH 17/27] async_spi: close() on an awaitable that was never awaited finishes it close() or throw() before the first await left the state NEW, so a later await started the transfer. It is now finished, as with a closed coroutine. --- supervisor/shared/awaitable.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/supervisor/shared/awaitable.c b/supervisor/shared/awaitable.c index 772e40b50d1..ceddf149b42 100644 --- a/supervisor/shared/awaitable.c +++ b/supervisor/shared/awaitable.c @@ -62,9 +62,12 @@ MP_DEFINE_CONST_OBJ_TYPE( // ---- the awaitable ----------------------------------------------------------- +// Finishes the awaitable; stops the operation if it has started. A closed awaitable that +// was never awaited does not start later, as with a closed coroutine. static void awaitable_cancel(circuitpy_awaitable_obj_t *self) { - if (self->state == CIRCUITPY_AWAITABLE_RUNNING) { - self->state = CIRCUITPY_AWAITABLE_FINISHED; + uint8_t state = self->state; + self->state = CIRCUITPY_AWAITABLE_FINISHED; + if (state == CIRCUITPY_AWAITABLE_RUNNING) { self->fun->cancel(self->args[0], self->context, &self->done); } } From 51783740f3cbb6f16ddabb61fd60c94c2fe5f439 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Wed, 14 Oct 2026 20:50:29 +0000 Subject: [PATCH 18/27] zephyr-cp: register a waiting configure() under irq_lock() configure_start() checked for a running transfer and then stored the waiting configure(); a transfer_done() between the two left it waiting forever. --- ports/zephyr-cp/common-hal/async_spi/SPI.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ports/zephyr-cp/common-hal/async_spi/SPI.c b/ports/zephyr-cp/common-hal/async_spi/SPI.c index 07410fe359f..d000e26da2e 100644 --- a/ports/zephyr-cp/common-hal/async_spi/SPI.c +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.c @@ -157,7 +157,11 @@ void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32 uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done) { CIRCUITPY_ASYNC_FLAG_INIT(done); uint16_t operation = operation_of(polarity, phase, bits); + // transfer_done() applies a waiting configure(), so the check and the registration must + // not be split by it. + unsigned int key = irq_lock(); if (self->done == NULL || CIRCUITPY_ASYNC_FLAG_IS_SET(self->done)) { + irq_unlock(key); apply_config(self, baudrate, operation); CIRCUITPY_ASYNC_FLAG_SET(done); return NULL; @@ -169,6 +173,7 @@ void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32 self->pending_frequency = baudrate; self->pending_operation = operation; self->idle_done = done; + irq_unlock(key); return NULL; } From c96badb15184ac7ac5935d5d46f0b7da1c59d804 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Wed, 14 Oct 2026 20:50:31 +0000 Subject: [PATCH 19/27] raspberrypi: clear the DMA enable bits before an SPI abort on RP2350 Errata RP2350-E5: an enabled channel can retrigger after the abort. --- ports/raspberrypi/peripherals/spi.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ports/raspberrypi/peripherals/spi.c b/ports/raspberrypi/peripherals/spi.c index 441f5e73107..69aefb19890 100644 --- a/ports/raspberrypi/peripherals/spi.c +++ b/ports/raspberrypi/peripherals/spi.c @@ -341,7 +341,13 @@ void rp2_spi_transfer_start(rp2_spi_t *self, const uint8_t *data_out, uint8_t *d // Stop the running DMA transfer without setting its done flag. static void _abort(rp2_spi_t *self) { + // A completion interrupt may still come (RP2040-E13); the handler skips a NULL flag. async_flags[self->dma_rx] = NULL; + #if PICO_RP2350 + // RP2350-E5: an enabled channel can retrigger after the abort. + hw_clear_bits(&dma_hw->ch[self->dma_tx].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); + hw_clear_bits(&dma_hw->ch[self->dma_rx].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); + #endif // Both channels at once; as dma_channel_abort(), the busy bits clear once in-flight // transfers have retired. dma_hw->abort = (1u << self->dma_tx) | (1u << self->dma_rx); From 629e8a4c083680800d469bf96b3280bc17602546 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Wed, 14 Oct 2026 20:50:32 +0000 Subject: [PATCH 20/27] zephyr-cp: the async_spi test requires the ticker to run during the transfers "ticker ran" once also passed with a blocking transfer; now it has to run more than 20 times during 10 transfers of 4 KB at 1 MHz (about 330 ms). --- ports/zephyr-cp/tests/test_async_spi.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ports/zephyr-cp/tests/test_async_spi.py b/ports/zephyr-cp/tests/test_async_spi.py index e1e889bd2cc..88db99d89a8 100644 --- a/ports/zephyr-cp/tests/test_async_spi.py +++ b/ports/zephyr-cp/tests/test_async_spi.py @@ -98,7 +98,9 @@ def test_async_spi_loopback(circuitpython): output = circuitpython.serial.all_output assert "differs" not in output - assert "ticker ran" in output + # 10 transfers of 4 KB at 1 MHz take about 330 ms; the ticker runs many times in that. + ticker = int(output.split("ticker ran ")[1].split()[0]) + assert ticker > 20, ticker assert "bad argument raised" in output assert "deinit raised" in output assert "bad 0" in output From b3d34bd9b87016f242496097a7ccffb636f7741e Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Thu, 15 Oct 2026 07:16:44 +0000 Subject: [PATCH 21/27] build: one flag, CIRCUITPY_ASYNC_SPI, for a port with async SPI A port that has it turns it on in its mpconfigport.mk; busio.SPI is then always built on the same implementation and FourWire's write_start() follows. The separate CIRCUITPY_BUSIO_SPI_OVER_ASYNC is gone. --- ports/raspberrypi/mpconfigport.mk | 6 ++---- py/circuitpy_defns.mk | 5 ++--- py/circuitpy_mpconfig.mk | 8 ++------ shared-module/busio/SPI.h | 2 +- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index 8895f110d47..efb5f77e9ee 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -6,10 +6,8 @@ CIRCUITPY_OPTIMIZE_PROPERTY_FLASH_SIZE ?= 1 CIRCUITPY_WIFI_RADIO_SETTABLE_MAC_ADDRESS = 0 CIRCUITPY_RP2PIO ?= 1 -CIRCUITPY_BUSIO_SPI_ASYNC ?= 1 -# busio.SPI is built on the port's async_spi implementation; the async_spi module needs asyncio. -CIRCUITPY_BUSIO_SPI_OVER_ASYNC = 1 -CIRCUITPY_ASYNC_SPI ?= $(call enable-if-all,$(MICROPY_PY_ASYNCIO) $(CIRCUITPY_BUSIO_SPI)) +# SPI is async on this port; busio.SPI is built on it. +CIRCUITPY_ASYNC_SPI ?= 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_defns.mk b/py/circuitpy_defns.mk index 26b48baccfb..8a08e1de771 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -905,9 +905,8 @@ SRC_SHARED_MODULE_EXPANDED = $(addprefix shared-bindings/, $(SRC_SHARED_MODULE)) $(addprefix shared-module/, $(SRC_SHARED_MODULE)) \ $(addprefix shared-module/, $(SRC_SHARED_MODULE_INTERNAL)) -ifeq ($(CIRCUITPY_BUSIO_SPI_OVER_ASYNC),1) -# busio.SPI comes from shared-module, on the port's async_spi common-hal, which is then built -# even without the async_spi module. +ifeq ($(CIRCUITPY_ASYNC_SPI),1) +# busio.SPI comes from shared-module, on the port's async_spi common-hal. SRC_COMMON_HAL_EXPANDED := $(filter-out common-hal/busio/SPI.c, $(SRC_COMMON_HAL_EXPANDED)) \ common-hal/async_spi/SPI.c SRC_SHARED_MODULE_EXPANDED += shared-module/busio/SPI.c diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 9586cbef6b9..d1525c8aa57 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -125,14 +125,10 @@ CFLAGS += -DCIRCUITPY_ANALOGIO=$(CIRCUITPY_ANALOGIO) CIRCUITPY_ARRAY ?= 1 CFLAGS += -DCIRCUITPY_ARRAY=$(CIRCUITPY_ARRAY) -# async_spi needs asyncio and a port with the async busio SPI reads (raspberrypi). +# A port with async SPI: the async_spi module, and busio.SPI built on the same implementation. CIRCUITPY_ASYNC_SPI ?= 0 CFLAGS += -DCIRCUITPY_ASYNC_SPI=$(CIRCUITPY_ASYNC_SPI) -# busio.SPI from shared-module/busio/SPI.c, on the port's common-hal/async_spi. Used by the -# build only. -CIRCUITPY_BUSIO_SPI_OVER_ASYNC ?= 0 - CIRCUITPY_ATEXIT ?= $(CIRCUITPY_FULL_BUILD) CFLAGS += -DCIRCUITPY_ATEXIT=$(CIRCUITPY_ATEXIT) @@ -248,7 +244,7 @@ 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 +CIRCUITPY_BUSIO_SPI_ASYNC ?= $(CIRCUITPY_ASYNC_SPI) CFLAGS += -DCIRCUITPY_BUSIO_SPI_ASYNC=$(CIRCUITPY_BUSIO_SPI_ASYNC) CIRCUITPY_BUSIO_UART ?= $(CIRCUITPY_BUSIO) diff --git a/shared-module/busio/SPI.h b/shared-module/busio/SPI.h index 5162467e054..2a463c4164d 100644 --- a/shared-module/busio/SPI.h +++ b/shared-module/busio/SPI.h @@ -6,7 +6,7 @@ // busio.SPI built on a port's async_spi implementation: every blocking call starts an async // transfer and waits for it. A port that has async_spi includes this header from its -// common-hal/busio/SPI.h and sets CIRCUITPY_BUSIO_SPI_OVER_ASYNC = 1 in its mpconfigport.mk. +// common-hal/busio/SPI.h and sets CIRCUITPY_ASYNC_SPI = 1 in its mpconfigport.mk. #pragma once From 2e3aa3e355b5b3eac3db1b066918a3c9aaea419d Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Thu, 15 Oct 2026 07:16:46 +0000 Subject: [PATCH 22/27] build: pick busio/SPI.c's home through the source lists Like touchio: busio/SPI.c goes to SRC_SHARED_MODULE_ALL on a port with async SPI and to SRC_COMMON_HAL_ALL otherwise, instead of filtering the expanded list. --- py/circuitpy_defns.mk | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 8a08e1de771..b0d4c549ff0 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -542,7 +542,6 @@ SRC_COMMON_HAL_ALL = \ audiopwmio/__init__.c \ board/__init__.c \ busio/I2C.c \ - busio/SPI.c \ busio/UART.c \ busio/__init__.c \ camera/__init__.c \ @@ -905,13 +904,6 @@ SRC_SHARED_MODULE_EXPANDED = $(addprefix shared-bindings/, $(SRC_SHARED_MODULE)) $(addprefix shared-module/, $(SRC_SHARED_MODULE)) \ $(addprefix shared-module/, $(SRC_SHARED_MODULE_INTERNAL)) -ifeq ($(CIRCUITPY_ASYNC_SPI),1) -# busio.SPI comes from shared-module, on the port's async_spi common-hal. -SRC_COMMON_HAL_EXPANDED := $(filter-out common-hal/busio/SPI.c, $(SRC_COMMON_HAL_EXPANDED)) \ - common-hal/async_spi/SPI.c -SRC_SHARED_MODULE_EXPANDED += shared-module/busio/SPI.c -endif - # There may be duplicates between SRC_COMMON_HAL_EXPANDED and SRC_SHARED_MODULE_EXPANDED, # because a few modules have files both in common-hal/ and shared-module/. # Doing a $(sort ...) removes duplicates as part of sorting. @@ -930,6 +922,15 @@ SRC_SHARED_MODULE_ALL += \ touchio/__init__.c endif +# On a port with async SPI, busio.SPI is shared code on top of common-hal/async_spi. +ifeq ($(CIRCUITPY_ASYNC_SPI),1) +SRC_SHARED_MODULE_ALL += \ + busio/SPI.c +else +SRC_COMMON_HAL_ALL += \ + busio/SPI.c +endif + ifeq ($(CIRCUITPY_SSL_MBEDTLS),0) SRC_COMMON_HAL_ALL += \ ssl/__init__.c \ From 271634bd7597296a2631bb573dd0d943d72b922a Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Thu, 15 Oct 2026 07:16:47 +0000 Subject: [PATCH 23/27] busio: a blocking SPI call stops on Ctrl-C The wait for the transfer or configure checks the interrupt character, cancels the operation and raises the KeyboardInterrupt instead of finishing it. --- shared-module/busio/SPI.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/shared-module/busio/SPI.c b/shared-module/busio/SPI.c index 8178ecbd5e0..8fbbd68fa04 100644 --- a/shared-module/busio/SPI.c +++ b/shared-module/busio/SPI.c @@ -8,11 +8,17 @@ #include "py/runtime.h" #include "supervisor/background_callback.h" +#include "shared/runtime/interrupt_char.h" -static void wait(circuitpy_async_flag_t *done) { +// False when interrupted (Ctrl-C); the operation is then still running. +static bool wait(circuitpy_async_flag_t *done) { while (!CIRCUITPY_ASYNC_FLAG_IS_SET(done)) { RUN_BACKGROUND_TASKS; + if (mp_hal_is_interrupted()) { + return false; + } } + return true; } #if CIRCUITPY_BUSIO_SPI_ASYNC @@ -66,7 +72,11 @@ bool common_hal_busio_spi_configure(busio_spi_obj_t *self, uint32_t baudrate, ui #endif void *context = common_hal_async_spi_spi_configure_start(&self->async_spi, baudrate, polarity, phase, bits, &self->done); - wait(&self->done); + if (!wait(&self->done)) { + common_hal_async_spi_spi_configure_cancel(&self->async_spi, context, &self->done); + mp_handle_pending(true); // raises the KeyboardInterrupt + return false; + } common_hal_async_spi_spi_configure_end(&self->async_spi, context, &self->done); self->polarity = polarity; self->phase = phase; @@ -91,7 +101,11 @@ void common_hal_busio_spi_unlock(busio_spi_obj_t *self) { // Finish the transfer started with self->done. static bool finish(busio_spi_obj_t *self, void *context) { - wait(&self->done); + if (!wait(&self->done)) { + common_hal_async_spi_spi_transfer_cancel(&self->async_spi, context, &self->done); + mp_handle_pending(true); // raises the KeyboardInterrupt + return false; + } return common_hal_async_spi_spi_transfer_finish(&self->async_spi, context, &self->done); } From 5f96f7ba2f7251ade0f577ec07aa4b3f6f2e07a1 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Thu, 15 Oct 2026 16:19:01 +0000 Subject: [PATCH 24/27] raspberrypi: the SPI driver lives in common-hal/async_spi busio.SPI is built on it, so the rp2_spi_* layer in peripherals/ had one user. Its functions are the common_hal_async_spi_spi_* ones now and its fields are in the object. --- ports/raspberrypi/Makefile | 1 - ports/raspberrypi/common-hal/async_spi/SPI.c | 371 ++++++++++++++++- ports/raspberrypi/common-hal/async_spi/SPI.h | 27 +- .../common-hal/microcontroller/__init__.c | 2 +- .../raspberrypi/common-hal/picogame/Display.c | 2 +- ports/raspberrypi/peripherals/spi.c | 386 ------------------ ports/raspberrypi/peripherals/spi.h | 73 ---- 7 files changed, 379 insertions(+), 483 deletions(-) delete mode 100644 ports/raspberrypi/peripherals/spi.c delete mode 100644 ports/raspberrypi/peripherals/spi.h diff --git a/ports/raspberrypi/Makefile b/ports/raspberrypi/Makefile index 1c304d1cd4a..7649e199aa8 100755 --- a/ports/raspberrypi/Makefile +++ b/ports/raspberrypi/Makefile @@ -595,7 +595,6 @@ SRC_C += \ audio_dma.c \ background.c \ peripherals/pins.c \ - peripherals/spi.c \ lib/crypto-algorithms/sha256.c \ lib/tinyusb/src/portable/raspberrypi/rp2040/dcd_rp2040.c \ lib/tinyusb/src/portable/raspberrypi/rp2040/rp2040_usb.c \ diff --git a/ports/raspberrypi/common-hal/async_spi/SPI.c b/ports/raspberrypi/common-hal/async_spi/SPI.c index 09dbb21e2a5..5883dd179f3 100644 --- a/ports/raspberrypi/common-hal/async_spi/SPI.c +++ b/ports/raspberrypi/common-hal/async_spi/SPI.c @@ -1,6 +1,6 @@ // This file is part of the CircuitPython project: https://circuitpython.org // -// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// SPDX-FileCopyrightText: Copyright (c) 2021 Scott Shawcroft for Adafruit Industries // // SPDX-License-Identifier: MIT @@ -8,37 +8,211 @@ #include "py/runtime.h" #include "shared-bindings/microcontroller/Pin.h" +#include "supervisor/background_callback.h" + +#include "hardware/dma.h" +#include "hardware/gpio.h" +#include "hardware/irq.h" + +#define NO_INSTANCE 0xff + +// Done flag of the async transfer on each kept RX channel, set when that channel finishes. +static circuitpy_async_flag_t *async_flags[NUM_DMA_CHANNELS]; +// RX channels with the interrupt enabled. +static uint32_t async_irq_mask; + +// Shared DMA_IRQ_0 handler: acknowledges and services only our channels, like audio_dma and rp2pio. +static void __not_in_flash_func(spi_dma_irq_handler)(void) { + uint32_t pending = dma_hw->ints0 & async_irq_mask; + dma_hw->ints0 = pending; + for (uint chan = 0; pending != 0; chan++, pending >>= 1) { + if ((pending & 1) && async_flags[chan] != NULL) { + CIRCUITPY_ASYNC_FLAG_SET(async_flags[chan]); + } + } +} + +static void async_irq_enable(uint chan) { + dma_hw->ints0 = 1u << chan; + if (async_irq_mask == 0) { + irq_add_shared_handler(DMA_IRQ_0, spi_dma_irq_handler, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY); + } + async_irq_mask |= 1u << chan; + dma_irqn_set_channel_enabled(0, chan, true); + irq_set_enabled(DMA_IRQ_0, true); +} + +static void async_irq_disable(uint chan) { + dma_irqn_set_channel_enabled(0, chan, false); + async_irq_mask &= ~(1u << chan); + if (async_irq_mask == 0) { + irq_remove_handler(DMA_IRQ_0, spi_dma_irq_handler); + } + if (dma_hw->inte0 == 0) { + irq_set_enabled(DMA_IRQ_0, false); + } +} void common_hal_async_spi_spi_construct(async_spi_spi_obj_t *self, const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso) { - rp2_spi_error_t error = rp2_spi_construct(&self->spi, clock, mosi, miso); - if (error == RP2_SPI_INVALID_PINS) { + size_t instance_index = NO_INSTANCE; + + // Ensure the object starts in its deinit state. + self->clock = NULL; + + if (clock->number % 4 == 2) { + instance_index = (clock->number / 8) % 2; + } + if (mosi != NULL) { + // Make sure the set MOSI matches the clock settings. + if (mosi->number % 4 != 3 || + (mosi->number / 8) % 2 != instance_index) { + instance_index = NO_INSTANCE; + } + } + if (miso != NULL) { + // Make sure the set MOSI matches the clock settings. + if (miso->number % 4 != 0 || + (miso->number / 8) % 2 != instance_index) { + instance_index = NO_INSTANCE; + } + } + + // TODO: Check to see if we're sharing the SPI with a native APA102. + + if (instance_index > 1) { raise_ValueError_invalid_pins(); } - if (error == RP2_SPI_IN_USE) { + + if (instance_index == 0) { + self->peripheral = spi0; + } else if (instance_index == 1) { + self->peripheral = spi1; + } + + if ((spi_get_hw(self->peripheral)->cr1 & SPI_SSPCR1_SSE_BITS) != 0) { mp_raise_ValueError(MP_ERROR_TEXT("SPI peripheral in use")); } + + self->async_active = false; + self->dma_kept = false; + self->idle_done = NULL; + self->target_frequency = 250000; + self->real_frequency = spi_init(self->peripheral, self->target_frequency); + + gpio_set_function(clock->number, GPIO_FUNC_SPI); + claim_pin(clock); + self->clock = clock; + + self->mosi = mosi; + if (mosi != NULL) { + gpio_set_function(mosi->number, GPIO_FUNC_SPI); + claim_pin(mosi); + } + + self->miso = miso; + if (miso != NULL) { + gpio_set_function(miso->number, GPIO_FUNC_SPI); + claim_pin(miso); + } } bool common_hal_async_spi_spi_deinited(async_spi_spi_obj_t *self) { - return rp2_spi_deinited(&self->spi); + return self->clock == NULL; } +void common_hal_async_spi_spi_never_reset(async_spi_spi_obj_t *self) { + common_hal_never_reset_pin(self->clock); + common_hal_never_reset_pin(self->mosi); + common_hal_never_reset_pin(self->miso); +} + +void common_hal_async_spi_spi_mark_deinit(async_spi_spi_obj_t *self) { + self->clock = NULL; +} + +static void _abort(async_spi_spi_obj_t *self); +static void _idle(async_spi_spi_obj_t *self); + void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self) { - rp2_spi_deinit(&self->spi); + if (common_hal_async_spi_spi_deinited(self)) { + return; + } + if (self->async_active) { + _abort(self); + CIRCUITPY_ASYNC_FLAG_SET(self->async_done); + } + _idle(self); + if (self->dma_kept) { + async_irq_disable(self->dma_rx); + 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); + common_hal_reset_pin(self->mosi); + common_hal_reset_pin(self->miso); + + self->clock = NULL; } -void common_hal_async_spi_spi_never_reset(async_spi_spi_obj_t *self) { - rp2_spi_never_reset(&self->spi); +static void _apply(async_spi_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, + uint8_t bits) { + if (baudrate == self->target_frequency && + polarity == self->polarity && + phase == self->phase && + bits == self->bits) { + return; + } + + spi_set_format(self->peripheral, bits, polarity, phase, SPI_MSB_FIRST); + + // Workaround to start with clock line high if polarity=1. The hw SPI peripheral does not do this + // automatically. See https://github.com/raspberrypi/pico-sdk/issues/868 and + // https://forums.raspberrypi.com/viewtopic.php?t=336142 + // TODO: scheduled to be be fixed in pico-sdk 1.5.0. + if (polarity) { + hw_clear_bits(&spi_get_hw(self->peripheral)->cr1, SPI_SSPCR1_SSE_BITS); // disable the SPI + hw_set_bits(&spi_get_hw(self->peripheral)->cr1, SPI_SSPCR1_SSE_BITS); // re-enable the SPI + } + + self->polarity = polarity; + self->phase = phase; + self->bits = bits; + self->target_frequency = baudrate; + self->real_frequency = spi_set_baudrate(self->peripheral, baudrate); } -void common_hal_async_spi_spi_mark_deinit(async_spi_spi_obj_t *self) { - self->spi.clock = NULL; +// The bus is idle: apply the settings of a waiting configure() and wake it. +static void _idle(async_spi_spi_obj_t *self) { + if (self->idle_done != NULL) { + _apply(self, self->pending_baudrate, self->pending_polarity, self->pending_phase, + self->pending_bits); + CIRCUITPY_ASYNC_FLAG_SET(self->idle_done); + self->idle_done = NULL; + } } +// Configures the bus once the running transfer, if any, has finished, and sets done then. void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done) { - rp2_spi_configure_start(&self->spi, baudrate, polarity, phase, bits, done); + CIRCUITPY_ASYNC_FLAG_INIT(done); + if (!self->async_active) { + _apply(self, baudrate, polarity, phase, bits); + CIRCUITPY_ASYNC_FLAG_SET(done); + return NULL; + } + // A later configure() replaces one still waiting. + if (self->idle_done != NULL) { + CIRCUITPY_ASYNC_FLAG_SET(self->idle_done); + } + self->pending_baudrate = baudrate; + self->pending_polarity = polarity; + self->pending_phase = phase; + self->pending_bits = bits; + self->idle_done = done; return NULL; } @@ -49,44 +223,203 @@ mp_obj_t common_hal_async_spi_spi_configure_end(async_spi_spi_obj_t *self, void void common_hal_async_spi_spi_configure_cancel(async_spi_spi_obj_t *self, void *context, circuitpy_async_flag_t *done) { - rp2_spi_configure_cancel(&self->spi, done); + if (self->idle_done == done) { + self->idle_done = NULL; + } } uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self) { - return self->spi.real_frequency; + return self->real_frequency; +} + +// Start a transfer. With DMA it runs in the background and done is set from the interrupt; +// otherwise it is done in software before this returns. An out or in buffer shorter than the +// transfer is one byte repeated or dropped. The DMA channels are kept until deinit, for buses +// that send often. Returns whether DMA is running. +static bool _start(async_spi_spi_obj_t *self, + const uint8_t *data_out, size_t out_len, + uint8_t *data_in, size_t in_len, circuitpy_async_flag_t *done) { + size_t len = MAX(out_len, in_len); + // Only use DMA if both data buffers are in SRAM. Otherwise, we'll stall the DMA with PSRAM or flash cache misses. + 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 = true; + async_irq_enable(chan_rx); + } 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(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(self->dma_tx, &c, + &spi_get_hw(self->peripheral)->dr, + data_out, + len, + false); + + 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(self->dma_rx, &c, + data_in, + &spi_get_hw(self->peripheral)->dr, + len, + false); + + // Before the start, so that a short transfer cannot finish unseen. + async_flags[self->dma_rx] = done; + dma_start_channel_mask((1u << self->dma_rx) | (1u << self->dma_tx)); + return true; + } + + // 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; + } + 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; +} + +// Waits for the running async transfer, if any, and sets its done flag. No background tasks: +// the caller holds the bus, and one of them may want it. +static void _end(async_spi_spi_obj_t *self) { + if (!self->async_active) { + return; + } + // The RX channel finishes last. + while (dma_channel_is_busy(self->dma_rx)) { + } + self->async_active = false; + async_flags[self->dma_rx] = NULL; + CIRCUITPY_ASYNC_FLAG_SET(self->async_done); + _idle(self); +} + +// Start an async transfer after the previous one; done is set when it has finished, here if +// it ran in software. The buffers must stay valid until then. +static __attribute__((noinline)) void _async_start(async_spi_spi_obj_t *self, const uint8_t *data_out, + size_t out_len, uint8_t *data_in, size_t in_len, circuitpy_async_flag_t *done) { + _end(self); + CIRCUITPY_ASYNC_FLAG_INIT(done); + self->async_done = done; + // An empty data side means there is nothing to send, whatever the one-byte side holds. + self->async_active = out_len != 0 && in_len != 0 && + _start(self, data_out, out_len, data_in, in_len, done); + if (!self->async_active) { + CIRCUITPY_ASYNC_FLAG_SET(done); + } } void *common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, size_t len, circuitpy_async_flag_t *done) { - rp2_spi_write_start(&self->spi, data, len, done); + _async_start(self, data, len, &self->one_byte, 1, done); return NULL; } void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value, circuitpy_async_flag_t *done) { - rp2_spi_read_start(&self->spi, data, len, write_value, done); + // End the previous transfer first: an async write may still be receiving into one_byte. + _end(self); + self->one_byte = write_value; + _async_start(self, &self->one_byte, 1, data, len, done); return NULL; } void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done) { - rp2_spi_transfer_start(&self->spi, data_out, data_in, len, done); + _async_start(self, data_out, len, data_in, len, done); return NULL; } +// Stop the running DMA transfer without setting its done flag. +static void _abort(async_spi_spi_obj_t *self) { + // A completion interrupt may still come (RP2040-E13); the handler skips a NULL flag. + async_flags[self->dma_rx] = NULL; + #if PICO_RP2350 + // RP2350-E5: an enabled channel can retrigger after the abort. + hw_clear_bits(&dma_hw->ch[self->dma_tx].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); + hw_clear_bits(&dma_hw->ch[self->dma_rx].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); + #endif + // Both channels at once; as dma_channel_abort(), the busy bits clear once in-flight + // transfers have retired. + dma_hw->abort = (1u << self->dma_tx) | (1u << self->dma_rx); + while ((dma_hw->ch[self->dma_tx].ctrl_trig | dma_hw->ch[self->dma_rx].ctrl_trig) & DMA_CH0_CTRL_TRIG_BUSY_BITS) { + } + self->async_active = false; + while (spi_is_busy(self->peripheral)) { + } + while (spi_is_readable(self->peripheral)) { + (void)spi_get_hw(self->peripheral)->dr; + } + spi_get_hw(self->peripheral)->icr = SPI_SSPICR_RORIC_BITS; + _idle(self); +} + +// Stops the transfer started with done, or finishes it if done is set. Does nothing for an +// earlier transfer, and does not allocate. +static void _stop(async_spi_spi_obj_t *self, circuitpy_async_flag_t *done) { + if (!self->async_active || self->async_done != done) { + return; + } + if (CIRCUITPY_ASYNC_FLAG_IS_SET(done)) { + _end(self); + return; + } + _abort(self); +} + bool common_hal_async_spi_spi_transfer_finish(async_spi_spi_obj_t *self, void *context, circuitpy_async_flag_t *done) { - rp2_spi_stop(&self->spi, done); + _stop(self, done); return true; } mp_obj_t common_hal_async_spi_spi_transfer_end(async_spi_spi_obj_t *self, void *context, circuitpy_async_flag_t *done) { - common_hal_async_spi_spi_transfer_finish(self, context, done); + _stop(self, done); return mp_const_none; } void common_hal_async_spi_spi_transfer_cancel(async_spi_spi_obj_t *self, void *context, circuitpy_async_flag_t *done) { - rp2_spi_stop(&self->spi, done); + _stop(self, done); } diff --git a/ports/raspberrypi/common-hal/async_spi/SPI.h b/ports/raspberrypi/common-hal/async_spi/SPI.h index d8a7a05d2e6..7aa4e3f1ed3 100644 --- a/ports/raspberrypi/common-hal/async_spi/SPI.h +++ b/ports/raspberrypi/common-hal/async_spi/SPI.h @@ -7,9 +7,32 @@ #pragma once #include "py/obj.h" -#include "peripherals/spi.h" +#include "common-hal/microcontroller/Pin.h" +#include "supervisor/shared/async_flag.h" + +#include "hardware/spi.h" typedef struct { mp_obj_base_t base; - rp2_spi_t spi; + spi_inst_t *peripheral; + const mcu_pin_obj_t *clock; // NULL once deinited + const mcu_pin_obj_t *mosi; + const mcu_pin_obj_t *miso; + uint32_t target_frequency; + int32_t real_frequency; + uint8_t polarity; + uint8_t phase; + uint8_t bits; + bool async_active; // an async 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 one_byte; // the one-byte side of an async write (RX) or read (TX) + circuitpy_async_flag_t *async_done; + // A configure() waiting for the transfer to finish, and its settings. + circuitpy_async_flag_t *idle_done; + uint32_t pending_baudrate; + uint8_t pending_polarity; + uint8_t pending_phase; + uint8_t pending_bits; } async_spi_spi_obj_t; diff --git a/ports/raspberrypi/common-hal/microcontroller/__init__.c b/ports/raspberrypi/common-hal/microcontroller/__init__.c index f1018d0963b..9021778157b 100644 --- a/ports/raspberrypi/common-hal/microcontroller/__init__.c +++ b/ports/raspberrypi/common-hal/microcontroller/__init__.c @@ -28,7 +28,7 @@ // DMA interrupt (DMA_IRQ_n) allocation for this port: // // DMA_IRQ_0 Shared. audiocore (audio_dma.c), rp2pio (StateMachine.c) and, -// with async_spi, SPI (peripherals/spi.c) each register a shared +// async_spi (common-hal/async_spi/SPI.c) each register a shared // handler with irq_add_shared_handler() and // service only their own DMA channels. Any new code that needs a // DMA completion interrupt should do the same: add a shared handler diff --git a/ports/raspberrypi/common-hal/picogame/Display.c b/ports/raspberrypi/common-hal/picogame/Display.c index d4373db2269..8dda6054346 100644 --- a/ports/raspberrypi/common-hal/picogame/Display.c +++ b/ports/raspberrypi/common-hal/picogame/Display.c @@ -40,7 +40,7 @@ void common_hal_picogame_display_construct(picogame_display_obj_t *self, MP_QSTR_display, MP_QSTR_FourWire, mp_obj_get_type(display->bus.bus)->name); } fourwire_fourwire_obj_t *fw = MP_OBJ_TO_PTR(display->bus.bus); - self->spi = fw->bus->async_spi.spi.peripheral; + self->spi = fw->bus->async_spi.peripheral; #if CIRCUITPY_PICOGAME_RGB444 // Tell the panel which pixel format we'll send (COLMOD). Asserting it here also recovers from diff --git a/ports/raspberrypi/peripherals/spi.c b/ports/raspberrypi/peripherals/spi.c deleted file mode 100644 index 69aefb19890..00000000000 --- a/ports/raspberrypi/peripherals/spi.c +++ /dev/null @@ -1,386 +0,0 @@ -// This file is part of the CircuitPython project: https://circuitpython.org -// -// SPDX-FileCopyrightText: Copyright (c) 2021 Scott Shawcroft for Adafruit Industries -// -// SPDX-License-Identifier: MIT - -#include "peripherals/spi.h" - -#include "py/mpconfig.h" -#include "shared-bindings/microcontroller/Pin.h" -#include "supervisor/background_callback.h" - -#include "hardware/dma.h" -#include "hardware/gpio.h" -#include "hardware/irq.h" - -#define NO_INSTANCE 0xff - -// Done flag of the async transfer on each kept RX channel, set when that channel finishes. -static volatile bool *async_flags[NUM_DMA_CHANNELS]; -// RX channels with the interrupt enabled. -static uint32_t async_irq_mask; - -// Shared DMA_IRQ_0 handler: acknowledges and services only our channels, like audio_dma and rp2pio. -static void __not_in_flash_func(spi_dma_irq_handler)(void) { - uint32_t pending = dma_hw->ints0 & async_irq_mask; - dma_hw->ints0 = pending; - for (uint chan = 0; pending != 0; chan++, pending >>= 1) { - if ((pending & 1) && async_flags[chan] != NULL) { - *async_flags[chan] = true; - } - } -} - -static void async_irq_enable(uint chan) { - dma_hw->ints0 = 1u << chan; - if (async_irq_mask == 0) { - irq_add_shared_handler(DMA_IRQ_0, spi_dma_irq_handler, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY); - } - async_irq_mask |= 1u << chan; - dma_irqn_set_channel_enabled(0, chan, true); - irq_set_enabled(DMA_IRQ_0, true); -} - -static void async_irq_disable(uint chan) { - dma_irqn_set_channel_enabled(0, chan, false); - async_irq_mask &= ~(1u << chan); - if (async_irq_mask == 0) { - irq_remove_handler(DMA_IRQ_0, spi_dma_irq_handler); - } - if (dma_hw->inte0 == 0) { - irq_set_enabled(DMA_IRQ_0, false); - } -} - -rp2_spi_error_t rp2_spi_construct(rp2_spi_t *self, const mcu_pin_obj_t *clock, - const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso) { - size_t instance_index = NO_INSTANCE; - - // Ensure the object starts in its deinit state. - self->clock = NULL; - - if (clock->number % 4 == 2) { - instance_index = (clock->number / 8) % 2; - } - if (mosi != NULL) { - // Make sure the set MOSI matches the clock settings. - if (mosi->number % 4 != 3 || - (mosi->number / 8) % 2 != instance_index) { - instance_index = NO_INSTANCE; - } - } - if (miso != NULL) { - // Make sure the set MOSI matches the clock settings. - if (miso->number % 4 != 0 || - (miso->number / 8) % 2 != instance_index) { - instance_index = NO_INSTANCE; - } - } - - // TODO: Check to see if we're sharing the SPI with a native APA102. - - if (instance_index > 1) { - return RP2_SPI_INVALID_PINS; - } - - if (instance_index == 0) { - self->peripheral = spi0; - } else if (instance_index == 1) { - self->peripheral = spi1; - } - - if ((spi_get_hw(self->peripheral)->cr1 & SPI_SSPCR1_SSE_BITS) != 0) { - return RP2_SPI_IN_USE; - } - - self->async_active = false; - self->dma_kept = false; - self->idle_done = NULL; - self->target_frequency = 250000; - self->real_frequency = spi_init(self->peripheral, self->target_frequency); - - gpio_set_function(clock->number, GPIO_FUNC_SPI); - claim_pin(clock); - self->clock = clock; - - self->MOSI = mosi; - if (mosi != NULL) { - gpio_set_function(mosi->number, GPIO_FUNC_SPI); - claim_pin(mosi); - } - - self->MISO = miso; - if (miso != NULL) { - gpio_set_function(miso->number, GPIO_FUNC_SPI); - claim_pin(miso); - } - return RP2_SPI_OK; -} - -void rp2_spi_never_reset(rp2_spi_t *self) { - common_hal_never_reset_pin(self->clock); - common_hal_never_reset_pin(self->MOSI); - common_hal_never_reset_pin(self->MISO); -} - -static void _abort(rp2_spi_t *self); -static void _idle(rp2_spi_t *self); - -void rp2_spi_deinit(rp2_spi_t *self) { - if (rp2_spi_deinited(self)) { - return; - } - if (self->async_active) { - _abort(self); - *self->async_done = true; - } - _idle(self); - if (self->dma_kept) { - async_irq_disable(self->dma_rx); - 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); - common_hal_reset_pin(self->MOSI); - common_hal_reset_pin(self->MISO); - - self->clock = NULL; -} - -static void _apply(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, - uint8_t bits) { - if (baudrate == self->target_frequency && - polarity == self->polarity && - phase == self->phase && - bits == self->bits) { - return; - } - - spi_set_format(self->peripheral, bits, polarity, phase, SPI_MSB_FIRST); - - // Workaround to start with clock line high if polarity=1. The hw SPI peripheral does not do this - // automatically. See https://github.com/raspberrypi/pico-sdk/issues/868 and - // https://forums.raspberrypi.com/viewtopic.php?t=336142 - // TODO: scheduled to be be fixed in pico-sdk 1.5.0. - if (polarity) { - hw_clear_bits(&spi_get_hw(self->peripheral)->cr1, SPI_SSPCR1_SSE_BITS); // disable the SPI - hw_set_bits(&spi_get_hw(self->peripheral)->cr1, SPI_SSPCR1_SSE_BITS); // re-enable the SPI - } - - self->polarity = polarity; - self->phase = phase; - self->bits = bits; - self->target_frequency = baudrate; - self->real_frequency = spi_set_baudrate(self->peripheral, baudrate); -} - -// The bus is idle: apply the settings of a waiting configure() and wake it. -static void _idle(rp2_spi_t *self) { - if (self->idle_done != NULL) { - _apply(self, self->pending_baudrate, self->pending_polarity, self->pending_phase, - self->pending_bits); - *self->idle_done = true; - self->idle_done = NULL; - } -} - -void rp2_spi_configure_start(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, - uint8_t bits, volatile bool *done) { - *done = false; - if (!self->async_active) { - _apply(self, baudrate, polarity, phase, bits); - *done = true; - return; - } - // A later configure() replaces one still waiting. - if (self->idle_done != NULL) { - *self->idle_done = true; - } - self->pending_baudrate = baudrate; - self->pending_polarity = polarity; - self->pending_phase = phase; - self->pending_bits = bits; - self->idle_done = done; -} - -void rp2_spi_configure_cancel(rp2_spi_t *self, volatile bool *done) { - if (self->idle_done == done) { - self->idle_done = NULL; - } -} - -// Start a transfer. With DMA it runs in the background and done is set from the interrupt; -// otherwise it is done in software before this returns. An out or in buffer shorter than the -// transfer is one byte repeated or dropped. The DMA channels are kept until deinit, for buses -// that send often. Returns whether DMA is running. -static bool _start(rp2_spi_t *self, - const uint8_t *data_out, size_t out_len, - uint8_t *data_in, size_t in_len, volatile bool *done) { - size_t len = MAX(out_len, in_len); - // Only use DMA if both data buffers are in SRAM. Otherwise, we'll stall the DMA with PSRAM or flash cache misses. - 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 = true; - async_irq_enable(chan_rx); - } 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(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(self->dma_tx, &c, - &spi_get_hw(self->peripheral)->dr, - data_out, - len, - false); - - 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(self->dma_rx, &c, - data_in, - &spi_get_hw(self->peripheral)->dr, - len, - false); - - // Before the start, so that a short transfer cannot finish unseen. - async_flags[self->dma_rx] = done; - dma_start_channel_mask((1u << self->dma_rx) | (1u << self->dma_tx)); - return true; - } - - // 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; - } - 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(rp2_spi_t *self) { - while (dma_channel_is_busy(self->dma_rx)) { - } -} - -// Start an async transfer; done is set when it has finished, here if it ran in software. -static __attribute__((noinline)) void _async_start(rp2_spi_t *self, const uint8_t *data_out, size_t out_len, - uint8_t *data_in, size_t in_len, volatile bool *done) { - *done = false; - self->async_done = done; - // An empty data side means there is nothing to send, whatever the one-byte side holds. - self->async_active = out_len != 0 && in_len != 0 && - _start(self, data_out, out_len, data_in, in_len, done); - if (!self->async_active) { - *done = true; - } -} - -void rp2_spi_write_start(rp2_spi_t *self, const uint8_t *data, size_t len, - volatile bool *done) { - rp2_spi_end(self); - _async_start(self, data, len, &self->one_byte, 1, done); -} - -void rp2_spi_read_start(rp2_spi_t *self, uint8_t *data, size_t len, uint8_t write_value, - volatile bool *done) { - // End the previous transfer first: an async write may still be receiving into one_byte. - rp2_spi_end(self); - self->one_byte = write_value; - _async_start(self, &self->one_byte, 1, data, len, done); -} - -void rp2_spi_transfer_start(rp2_spi_t *self, const uint8_t *data_out, uint8_t *data_in, - size_t len, volatile bool *done) { - rp2_spi_end(self); - _async_start(self, data_out, len, data_in, len, done); -} - -// Stop the running DMA transfer without setting its done flag. -static void _abort(rp2_spi_t *self) { - // A completion interrupt may still come (RP2040-E13); the handler skips a NULL flag. - async_flags[self->dma_rx] = NULL; - #if PICO_RP2350 - // RP2350-E5: an enabled channel can retrigger after the abort. - hw_clear_bits(&dma_hw->ch[self->dma_tx].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); - hw_clear_bits(&dma_hw->ch[self->dma_rx].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); - #endif - // Both channels at once; as dma_channel_abort(), the busy bits clear once in-flight - // transfers have retired. - dma_hw->abort = (1u << self->dma_tx) | (1u << self->dma_rx); - while ((dma_hw->ch[self->dma_tx].ctrl_trig | dma_hw->ch[self->dma_rx].ctrl_trig) & DMA_CH0_CTRL_TRIG_BUSY_BITS) { - } - self->async_active = false; - while (spi_is_busy(self->peripheral)) { - } - while (spi_is_readable(self->peripheral)) { - (void)spi_get_hw(self->peripheral)->dr; - } - spi_get_hw(self->peripheral)->icr = SPI_SSPICR_RORIC_BITS; - _idle(self); -} - -void rp2_spi_stop(rp2_spi_t *self, volatile bool *done) { - if (!self->async_active || self->async_done != done) { - return; - } - if (*done) { - rp2_spi_end(self); - return; - } - _abort(self); -} - -void rp2_spi_end(rp2_spi_t *self) { - if (self->async_active) { - // No background tasks here: the caller holds the bus, and one of them may want it. - _end(self); - self->async_active = false; - async_flags[self->dma_rx] = NULL; - *self->async_done = true; - _idle(self); - } -} diff --git a/ports/raspberrypi/peripherals/spi.h b/ports/raspberrypi/peripherals/spi.h deleted file mode 100644 index 70c848ee15c..00000000000 --- a/ports/raspberrypi/peripherals/spi.h +++ /dev/null @@ -1,73 +0,0 @@ -// This file is part of the CircuitPython project: https://circuitpython.org -// -// SPDX-FileCopyrightText: Copyright (c) 2021 Scott Shawcroft for Adafruit Industries -// -// SPDX-License-Identifier: MIT - -// An SPI bus of this port, shared by busio.SPI and async_spi.SPI. - -#pragma once - -#include - -#include "common-hal/microcontroller/Pin.h" - -#include "hardware/spi.h" - -typedef struct { - spi_inst_t *peripheral; - const mcu_pin_obj_t *clock; - const mcu_pin_obj_t *MOSI; - const mcu_pin_obj_t *MISO; - uint32_t target_frequency; - int32_t real_frequency; - uint8_t polarity; - uint8_t phase; - uint8_t bits; - bool async_active; // an async 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 one_byte; // the one-byte side of an async write (RX) or read (TX) - volatile bool *async_done; - // A configure() waiting for the transfer to finish, and its settings. - volatile bool *idle_done; - uint32_t pending_baudrate; - uint8_t pending_polarity; - uint8_t pending_phase; - uint8_t pending_bits; -} rp2_spi_t; - -typedef enum { - RP2_SPI_OK, - RP2_SPI_INVALID_PINS, // the pins do not make an SPI bus - RP2_SPI_IN_USE, // the peripheral is in use -} rp2_spi_error_t; - -rp2_spi_error_t rp2_spi_construct(rp2_spi_t *self, const mcu_pin_obj_t *clock, - const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso); -// Stops a running async transfer, and sets its done flag. -void rp2_spi_deinit(rp2_spi_t *self); -static inline bool rp2_spi_deinited(rp2_spi_t *self) { - return self->clock == NULL; -} -void rp2_spi_never_reset(rp2_spi_t *self); - -// Transfers: done is set once the transfer has finished, by the DMA interrupt or by the -// software path before the start returns. The buffers must stay valid until then. -void rp2_spi_write_start(rp2_spi_t *self, const uint8_t *data, size_t len, - volatile bool *done); -void rp2_spi_read_start(rp2_spi_t *self, uint8_t *data, size_t len, uint8_t write_value, - volatile bool *done); -void rp2_spi_transfer_start(rp2_spi_t *self, const uint8_t *data_out, uint8_t *data_in, - size_t len, volatile bool *done); -// Waits for the running async transfer, if any, and sets its done flag. -void rp2_spi_end(rp2_spi_t *self); -// Stops the transfer started with done, or finishes it if done is set. Does nothing for an -// earlier transfer, and does not allocate. -void rp2_spi_stop(rp2_spi_t *self, volatile bool *done); -// Configures the bus once the running transfer, if any, has finished, and sets done then. -void rp2_spi_configure_start(rp2_spi_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, - uint8_t bits, volatile bool *done); -// Drops the waiting configure() started with done. -void rp2_spi_configure_cancel(rp2_spi_t *self, volatile bool *done); From a89267360a787cef0bdeada1e046dd9f69e1a52b Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Thu, 15 Oct 2026 17:10:09 +0000 Subject: [PATCH 25/27] async_spi: configure() is a plain call It waits for a running transfer, then applies the settings; nothing to await, and a single task needs no lock for configure() followed by a transfer. Tasks sharing a bus serialize their transfers anyway, with an asyncio.Lock around configure() and the transfer. The pending-configure state in both ports is gone. --- ports/raspberrypi/common-hal/async_spi/SPI.c | 50 ++------------------ ports/raspberrypi/common-hal/async_spi/SPI.h | 6 --- ports/zephyr-cp/common-hal/async_spi/SPI.c | 45 ++---------------- ports/zephyr-cp/common-hal/async_spi/SPI.h | 3 -- ports/zephyr-cp/tests/test_async_spi.py | 2 +- shared-bindings/async_spi/SPI.c | 19 +++----- shared-bindings/async_spi/SPI.h | 11 ++--- shared-bindings/async_spi/__init__.c | 2 +- shared-module/busio/SPI.c | 9 +--- 9 files changed, 22 insertions(+), 125 deletions(-) diff --git a/ports/raspberrypi/common-hal/async_spi/SPI.c b/ports/raspberrypi/common-hal/async_spi/SPI.c index 5883dd179f3..02737a7b5f3 100644 --- a/ports/raspberrypi/common-hal/async_spi/SPI.c +++ b/ports/raspberrypi/common-hal/async_spi/SPI.c @@ -96,7 +96,6 @@ void common_hal_async_spi_spi_construct(async_spi_spi_obj_t *self, const mcu_pin self->async_active = false; self->dma_kept = false; - self->idle_done = NULL; self->target_frequency = 250000; self->real_frequency = spi_init(self->peripheral, self->target_frequency); @@ -132,7 +131,6 @@ void common_hal_async_spi_spi_mark_deinit(async_spi_spi_obj_t *self) { } static void _abort(async_spi_spi_obj_t *self); -static void _idle(async_spi_spi_obj_t *self); void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self) { if (common_hal_async_spi_spi_deinited(self)) { @@ -142,7 +140,6 @@ void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self) { _abort(self); CIRCUITPY_ASYNC_FLAG_SET(self->async_done); } - _idle(self); if (self->dma_kept) { async_irq_disable(self->dma_rx); dma_channel_unclaim(self->dma_tx); @@ -185,47 +182,12 @@ static void _apply(async_spi_spi_obj_t *self, uint32_t baudrate, uint8_t polarit self->real_frequency = spi_set_baudrate(self->peripheral, baudrate); } -// The bus is idle: apply the settings of a waiting configure() and wake it. -static void _idle(async_spi_spi_obj_t *self) { - if (self->idle_done != NULL) { - _apply(self, self->pending_baudrate, self->pending_polarity, self->pending_phase, - self->pending_bits); - CIRCUITPY_ASYNC_FLAG_SET(self->idle_done); - self->idle_done = NULL; - } -} +static void _end(async_spi_spi_obj_t *self); -// Configures the bus once the running transfer, if any, has finished, and sets done then. -void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, - uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done) { - CIRCUITPY_ASYNC_FLAG_INIT(done); - if (!self->async_active) { - _apply(self, baudrate, polarity, phase, bits); - CIRCUITPY_ASYNC_FLAG_SET(done); - return NULL; - } - // A later configure() replaces one still waiting. - if (self->idle_done != NULL) { - CIRCUITPY_ASYNC_FLAG_SET(self->idle_done); - } - self->pending_baudrate = baudrate; - self->pending_polarity = polarity; - self->pending_phase = phase; - self->pending_bits = bits; - self->idle_done = done; - return NULL; -} - -mp_obj_t common_hal_async_spi_spi_configure_end(async_spi_spi_obj_t *self, void *context, - circuitpy_async_flag_t *done) { - return mp_const_none; -} - -void common_hal_async_spi_spi_configure_cancel(async_spi_spi_obj_t *self, void *context, - circuitpy_async_flag_t *done) { - if (self->idle_done == done) { - self->idle_done = NULL; - } +void common_hal_async_spi_spi_configure(async_spi_spi_obj_t *self, uint32_t baudrate, + uint8_t polarity, uint8_t phase, uint8_t bits) { + _end(self); + _apply(self, baudrate, polarity, phase, bits); } uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self) { @@ -331,7 +293,6 @@ static void _end(async_spi_spi_obj_t *self) { self->async_active = false; async_flags[self->dma_rx] = NULL; CIRCUITPY_ASYNC_FLAG_SET(self->async_done); - _idle(self); } // Start an async transfer after the previous one; done is set when it has finished, here if @@ -391,7 +352,6 @@ static void _abort(async_spi_spi_obj_t *self) { (void)spi_get_hw(self->peripheral)->dr; } spi_get_hw(self->peripheral)->icr = SPI_SSPICR_RORIC_BITS; - _idle(self); } // Stops the transfer started with done, or finishes it if done is set. Does nothing for an diff --git a/ports/raspberrypi/common-hal/async_spi/SPI.h b/ports/raspberrypi/common-hal/async_spi/SPI.h index 7aa4e3f1ed3..1d39e4972f8 100644 --- a/ports/raspberrypi/common-hal/async_spi/SPI.h +++ b/ports/raspberrypi/common-hal/async_spi/SPI.h @@ -29,10 +29,4 @@ typedef struct { uint8_t dma_rx; uint8_t one_byte; // the one-byte side of an async write (RX) or read (TX) circuitpy_async_flag_t *async_done; - // A configure() waiting for the transfer to finish, and its settings. - circuitpy_async_flag_t *idle_done; - uint32_t pending_baudrate; - uint8_t pending_polarity; - uint8_t pending_phase; - uint8_t pending_bits; } async_spi_spi_obj_t; diff --git a/ports/zephyr-cp/common-hal/async_spi/SPI.c b/ports/zephyr-cp/common-hal/async_spi/SPI.c index d000e26da2e..aeb33c7fe74 100644 --- a/ports/zephyr-cp/common-hal/async_spi/SPI.c +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.c @@ -32,12 +32,6 @@ static void transfer_done(const struct device *dev, int result, void *userdata) async_spi_spi_obj_t *self = userdata; self->result = result; CIRCUITPY_ASYNC_FLAG_SET(self->done); - // A configure() that waited for this transfer. - if (self->idle_done != NULL) { - apply_config(self, self->pending_frequency, self->pending_operation); - CIRCUITPY_ASYNC_FLAG_SET(self->idle_done); - self->idle_done = NULL; - } port_wake_main_task_from_isr(); } @@ -70,7 +64,6 @@ void common_hal_async_spi_spi_construct_from_device(async_spi_spi_obj_t *self, self->fill = NULL; self->fill_len = 0; self->fill_from_port = false; - self->idle_done = NULL; } static void free_fill(async_spi_spi_obj_t *self) { @@ -153,40 +146,10 @@ static uint16_t operation_of(uint8_t polarity, uint8_t phase, uint8_t bits) { return operation; } -void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, - uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done) { - CIRCUITPY_ASYNC_FLAG_INIT(done); - uint16_t operation = operation_of(polarity, phase, bits); - // transfer_done() applies a waiting configure(), so the check and the registration must - // not be split by it. - unsigned int key = irq_lock(); - if (self->done == NULL || CIRCUITPY_ASYNC_FLAG_IS_SET(self->done)) { - irq_unlock(key); - apply_config(self, baudrate, operation); - CIRCUITPY_ASYNC_FLAG_SET(done); - return NULL; - } - // A later configure() replaces one still waiting. - if (self->idle_done != NULL) { - CIRCUITPY_ASYNC_FLAG_SET(self->idle_done); - } - self->pending_frequency = baudrate; - self->pending_operation = operation; - self->idle_done = done; - irq_unlock(key); - return NULL; -} - -mp_obj_t common_hal_async_spi_spi_configure_end(async_spi_spi_obj_t *self, void *context, - circuitpy_async_flag_t *done) { - return mp_const_none; -} - -void common_hal_async_spi_spi_configure_cancel(async_spi_spi_obj_t *self, void *context, - circuitpy_async_flag_t *done) { - if (self->idle_done == done) { - self->idle_done = NULL; - } +void common_hal_async_spi_spi_configure(async_spi_spi_obj_t *self, uint32_t baudrate, + uint8_t polarity, uint8_t phase, uint8_t bits) { + wait_done(self); + apply_config(self, baudrate, operation_of(polarity, phase, bits)); } uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self) { diff --git a/ports/zephyr-cp/common-hal/async_spi/SPI.h b/ports/zephyr-cp/common-hal/async_spi/SPI.h index 319ce3a8254..aea7d2a3708 100644 --- a/ports/zephyr-cp/common-hal/async_spi/SPI.h +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.h @@ -32,9 +32,6 @@ typedef struct { uint8_t *fill; // the bytes a read sends, when not zero size_t fill_len; bool fill_from_port; // fill came from port_malloc, not the heap - circuitpy_async_flag_t *idle_done; // a configure() waiting for the transfer to finish - uint32_t pending_frequency; - uint16_t pending_operation; } async_spi_spi_obj_t; void common_hal_async_spi_spi_construct_from_device(async_spi_spi_obj_t *self, diff --git a/ports/zephyr-cp/tests/test_async_spi.py b/ports/zephyr-cp/tests/test_async_spi.py index 88db99d89a8..11df1da0214 100644 --- a/ports/zephyr-cp/tests/test_async_spi.py +++ b/ports/zephyr-cp/tests/test_async_spi.py @@ -31,7 +31,7 @@ async def ticker(count): async def main(): spi = async_spi.SPI(microcontroller.pin.P_02, MOSI=microcontroller.pin.P_03, MISO=microcontroller.pin.P_00) - await spi.configure(baudrate=1_000_000) + spi.configure(baudrate=1_000_000) print("frequency", spi.frequency) bad = 0 for n in (1, 32, 4096): diff --git a/shared-bindings/async_spi/SPI.c b/shared-bindings/async_spi/SPI.c index 1231a37d736..10594fa937e 100644 --- a/shared-bindings/async_spi/SPI.c +++ b/shared-bindings/async_spi/SPI.c @@ -94,12 +94,6 @@ static MP_DEFINE_CONST_FUN_OBJ_1(async_spi_spi_deinit_obj, async_spi_spi_obj_dei // Provided by context manager helper. // The awaitable passes the object as mp_obj_t; the common-hal functions take it typed. -static mp_obj_t async_spi_spi_configure_end(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { - return common_hal_async_spi_spi_configure_end(MP_OBJ_TO_PTR(self_in), context, done); -} -static void async_spi_spi_configure_cancel(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { - common_hal_async_spi_spi_configure_cancel(MP_OBJ_TO_PTR(self_in), context, done); -} static mp_obj_t async_spi_spi_transfer_end(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { return common_hal_async_spi_spi_transfer_end(MP_OBJ_TO_PTR(self_in), context, done); } @@ -107,14 +101,15 @@ static void async_spi_spi_transfer_cancel(mp_obj_t self_in, void *context, circu common_hal_async_spi_spi_transfer_cancel(MP_OBJ_TO_PTR(self_in), context, done); } -//| async def configure( +//| def configure( //| self, *, baudrate: int = 100000, polarity: int = 0, phase: int = 0, bits: int = 8 //| ) -> None: //| """Configure the SPI bus, as `busio.SPI.configure`. A transfer still running finishes -//| first, with the old settings.""" +//| first, with the old settings. Tasks sharing a bus serialize their transfers, for +//| example with an `asyncio.Lock` around configure() and the transfer.""" //| ... //| -static void *async_spi_spi_configure(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { +static mp_obj_t async_spi_spi_configure(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_baudrate, ARG_polarity, ARG_phase, ARG_bits }; async_spi_spi_obj_t *self = checked_spi(pos_args[0]); mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_configure_args)]; @@ -124,10 +119,10 @@ static void *async_spi_spi_configure(circuitpy_async_flag_t *done, size_t n_args uint8_t phase = (uint8_t)mp_arg_validate_int_range(args[ARG_phase].u_int, 0, 1, MP_QSTR_phase); uint8_t bits = (uint8_t)mp_arg_validate_int_range(args[ARG_bits].u_int, 8, 9, MP_QSTR_bits); - return common_hal_async_spi_spi_configure_start(self, args[ARG_baudrate].u_int, polarity, phase, bits, done); + common_hal_async_spi_spi_configure(self, args[ARG_baudrate].u_int, polarity, phase, bits); + return mp_const_none; } -static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_configure_obj, 1, async_spi_spi_configure, - async_spi_spi_configure_end, async_spi_spi_configure_cancel); +static MP_DEFINE_CONST_FUN_OBJ_KW(async_spi_spi_configure_obj, 1, async_spi_spi_configure); //| import sys //| diff --git a/shared-bindings/async_spi/SPI.h b/shared-bindings/async_spi/SPI.h index ddabc3f72e5..10ede825af8 100644 --- a/shared-bindings/async_spi/SPI.h +++ b/shared-bindings/async_spi/SPI.h @@ -27,14 +27,9 @@ uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self); // Each start returns a context for state of that one operation, which end() and cancel() get // back; NULL when the object holds the state. -// configure(): done is set once the running transfer, if any, has finished and the settings -// are in place. -void *common_hal_async_spi_spi_configure_start(async_spi_spi_obj_t *self, uint32_t baudrate, - uint8_t polarity, uint8_t phase, uint8_t bits, circuitpy_async_flag_t *done); -mp_obj_t common_hal_async_spi_spi_configure_end(async_spi_spi_obj_t *self, void *context, - circuitpy_async_flag_t *done); -void common_hal_async_spi_spi_configure_cancel(async_spi_spi_obj_t *self, void *context, - circuitpy_async_flag_t *done); +// Waits for the running transfer, if any, then applies the settings. +void common_hal_async_spi_spi_configure(async_spi_spi_obj_t *self, uint32_t baudrate, + uint8_t polarity, uint8_t phase, uint8_t bits); // Each transfer start sets done once the transfer has finished, possibly from an interrupt. The // buffers must stay valid until done is set. transfer_end() and transfer_cancel() act only on diff --git a/shared-bindings/async_spi/__init__.c b/shared-bindings/async_spi/__init__.c index 9d6a1d0da53..b02a2ac0c1f 100644 --- a/shared-bindings/async_spi/__init__.c +++ b/shared-bindings/async_spi/__init__.c @@ -22,7 +22,7 @@ //| //| async def main(): //| spi = async_spi.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) -//| await spi.configure(baudrate=8_000_000) +//| spi.configure(baudrate=8_000_000) //| data = bytearray(512) //| await spi.readinto(data) //| diff --git a/shared-module/busio/SPI.c b/shared-module/busio/SPI.c index 8fbbd68fa04..2db2fd7885e 100644 --- a/shared-module/busio/SPI.c +++ b/shared-module/busio/SPI.c @@ -70,14 +70,7 @@ bool common_hal_busio_spi_configure(busio_spi_obj_t *self, uint32_t baudrate, ui #if CIRCUITPY_BUSIO_SPI_ASYNC end_sent(self); #endif - void *context = common_hal_async_spi_spi_configure_start(&self->async_spi, baudrate, polarity, phase, - bits, &self->done); - if (!wait(&self->done)) { - common_hal_async_spi_spi_configure_cancel(&self->async_spi, context, &self->done); - mp_handle_pending(true); // raises the KeyboardInterrupt - return false; - } - common_hal_async_spi_spi_configure_end(&self->async_spi, context, &self->done); + common_hal_async_spi_spi_configure(&self->async_spi, baudrate, polarity, phase, bits); self->polarity = polarity; self->phase = phase; return true; From 4dba4c194c89d1f8e25afbf39e340a07ae7a8625 Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Fri, 16 Oct 2026 21:11:20 +0000 Subject: [PATCH 26/27] async_spi: a lock for the bus, held with `async with spi.lock:` Lock is a type in the awaitable layer, embedded in the SPI object. __aenter__ is an awaitable that takes the lock or waits in line; the awaitable itself is the queue node, linked through a next pointer, so nothing is allocated. __aexit__ hands the lock to the next waiter and returns the lock, which awaits as already finished. A task cancelled while waiting leaves the line; one cancelled right after the hand-off passes the lock on. Transfers and configure() raise "Function requires lock" without it, as busio does; there is no try_lock, has_lock or unlock. --- ports/raspberrypi/common-hal/async_spi/SPI.h | 2 + ports/zephyr-cp/common-hal/async_spi/SPI.h | 2 + ports/zephyr-cp/tests/test_async_spi.py | 8 +- shared-bindings/async_spi/SPI.c | 44 +++++++-- shared-bindings/async_spi/__init__.c | 20 ++++- supervisor/shared/awaitable.c | 93 ++++++++++++++++++++ supervisor/shared/awaitable.h | 29 +++++- 7 files changed, 186 insertions(+), 12 deletions(-) diff --git a/ports/raspberrypi/common-hal/async_spi/SPI.h b/ports/raspberrypi/common-hal/async_spi/SPI.h index 1d39e4972f8..82abbc8ce6e 100644 --- a/ports/raspberrypi/common-hal/async_spi/SPI.h +++ b/ports/raspberrypi/common-hal/async_spi/SPI.h @@ -9,11 +9,13 @@ #include "py/obj.h" #include "common-hal/microcontroller/Pin.h" #include "supervisor/shared/async_flag.h" +#include "supervisor/shared/awaitable.h" #include "hardware/spi.h" typedef struct { mp_obj_base_t base; + circuitpy_async_lock_obj_t lock; spi_inst_t *peripheral; const mcu_pin_obj_t *clock; // NULL once deinited const mcu_pin_obj_t *mosi; diff --git a/ports/zephyr-cp/common-hal/async_spi/SPI.h b/ports/zephyr-cp/common-hal/async_spi/SPI.h index aea7d2a3708..1df2aa9a2a9 100644 --- a/ports/zephyr-cp/common-hal/async_spi/SPI.h +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.h @@ -9,6 +9,7 @@ #include "py/obj.h" #include "common-hal/microcontroller/Pin.h" #include "supervisor/shared/async_flag.h" +#include "supervisor/shared/awaitable.h" #include #include @@ -16,6 +17,7 @@ typedef struct { mp_obj_base_t base; + circuitpy_async_lock_obj_t lock; const struct device *spi_device; // NULL once deinited bool dynamic; // routed to pins at construction, released at deinit struct spi_config config[2]; // two slots: a driver sees a change by the pointer diff --git a/ports/zephyr-cp/tests/test_async_spi.py b/ports/zephyr-cp/tests/test_async_spi.py index 11df1da0214..55f3d5eeb15 100644 --- a/ports/zephyr-cp/tests/test_async_spi.py +++ b/ports/zephyr-cp/tests/test_async_spi.py @@ -31,6 +31,7 @@ async def ticker(count): async def main(): spi = async_spi.SPI(microcontroller.pin.P_02, MOSI=microcontroller.pin.P_03, MISO=microcontroller.pin.P_00) + await spi.lock.__aenter__() # held for the whole test, as busio's try_lock would be spi.configure(baudrate=1_000_000) print("frequency", spi.frequency) bad = 0 @@ -79,7 +80,12 @@ async def main(): except ValueError: print("deinit raised") with async_spi.SPI(microcontroller.pin.P_02, MOSI=microcontroller.pin.P_03, MISO=microcontroller.pin.P_00) as spi2: - await spi2.readinto(inb, end=8, write_value=0x5A) + try: + await spi2.readinto(inb, end=8) + except RuntimeError: + print("no lock raised") + async with spi2.lock: + await spi2.readinto(inb, end=8, write_value=0x5A) if inb[:8] != b"\\x5a" * 8: bad += 1 print("second bus differs") diff --git a/shared-bindings/async_spi/SPI.c b/shared-bindings/async_spi/SPI.c index 10594fa937e..9d8612ed820 100644 --- a/shared-bindings/async_spi/SPI.c +++ b/shared-bindings/async_spi/SPI.c @@ -27,8 +27,8 @@ //| ``write``, ``readinto`` and ``write_readinto`` return awaitables: the transfer runs while //| other asyncio tasks run. Short transfers, and transfers that cannot use DMA (for example //| from a buffer in flash), block other tasks until they finish. A transfer's arguments are -//| checked when the result is awaited. To share the bus between tasks, hold an -//| `asyncio.Lock` around each use.""" +//| checked when the result is awaited. A transfer or ``configure()`` needs the bus's `lock` +//| held, so that a transaction of several transfers is not interleaved with another task's.""" //| //| def __init__( //| self, @@ -58,6 +58,7 @@ static mp_obj_t async_spi_spi_make_new(const mp_obj_type_t *type, size_t n_args, async_spi_spi_obj_t *self = mp_obj_malloc_with_finaliser(async_spi_spi_obj_t, &async_spi_spi_type); common_hal_async_spi_spi_construct(self, clock, mosi, miso); + circuitpy_async_lock_init(&self->lock); return MP_OBJ_FROM_PTR(self); } @@ -69,6 +70,15 @@ static MP_NOINLINE async_spi_spi_obj_t *checked_spi(mp_obj_t self_in) { return self; } +// For the calls that use the bus. +static MP_NOINLINE async_spi_spi_obj_t *locked_spi(mp_obj_t self_in) { + async_spi_spi_obj_t *self = checked_spi(self_in); + if (!circuitpy_async_lock_held(&self->lock)) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Function requires lock")); + } + return self; +} + //| def deinit(self) -> None: //| """Turn off the SPI bus. A transfer still running is stopped.""" //| ... @@ -105,13 +115,12 @@ static void async_spi_spi_transfer_cancel(mp_obj_t self_in, void *context, circu //| self, *, baudrate: int = 100000, polarity: int = 0, phase: int = 0, bits: int = 8 //| ) -> None: //| """Configure the SPI bus, as `busio.SPI.configure`. A transfer still running finishes -//| first, with the old settings. Tasks sharing a bus serialize their transfers, for -//| example with an `asyncio.Lock` around configure() and the transfer.""" +//| first, with the old settings. Needs the `lock`.""" //| ... //| static mp_obj_t async_spi_spi_configure(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_baudrate, ARG_polarity, ARG_phase, ARG_bits }; - async_spi_spi_obj_t *self = checked_spi(pos_args[0]); + async_spi_spi_obj_t *self = locked_spi(pos_args[0]); mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_configure_args)]; mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_configure_args), busio_spi_configure_args, args); @@ -138,7 +147,7 @@ static MP_DEFINE_CONST_FUN_OBJ_KW(async_spi_spi_configure_obj, 1, async_spi_spi_ //| static void *async_spi_spi_write(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_buffer, ARG_start, ARG_end }; - async_spi_spi_obj_t *self = checked_spi(pos_args[0]); + async_spi_spi_obj_t *self = locked_spi(pos_args[0]); mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_write_args)]; mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_write_args), busio_spi_write_args, args); @@ -168,7 +177,7 @@ static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_obj, 1, async_spi_s //| static void *async_spi_spi_readinto(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_buffer, ARG_start, ARG_end, ARG_write_value }; - async_spi_spi_obj_t *self = checked_spi(pos_args[0]); + async_spi_spi_obj_t *self = locked_spi(pos_args[0]); mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_readinto_args)]; mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_readinto_args), busio_spi_readinto_args, args); @@ -203,7 +212,7 @@ static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_readinto_obj, 1, async_sp //| static void *async_spi_spi_write_readinto(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_out_buffer, ARG_in_buffer, ARG_out_start, ARG_out_end, ARG_in_start, ARG_in_end }; - async_spi_spi_obj_t *self = checked_spi(pos_args[0]); + async_spi_spi_obj_t *self = locked_spi(pos_args[0]); mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_write_readinto_args)]; mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_write_readinto_args), busio_spi_write_readinto_args, args); @@ -234,6 +243,24 @@ MP_DEFINE_CONST_FUN_OBJ_1(async_spi_spi_get_frequency_obj, async_spi_spi_get_fre MP_PROPERTY_GETTER(async_spi_spi_frequency_obj, (mp_obj_t)&async_spi_spi_get_frequency_obj); +//| lock: Lock +//| """The bus's lock. Hold it around a transaction:: +//| +//| async with spi.lock: +//| spi.configure(baudrate=8_000_000) +//| await spi.write(command) +//| await spi.readinto(reply) +//| """ +//| +static mp_obj_t async_spi_spi_get_lock(mp_obj_t self_in) { + async_spi_spi_obj_t *self = checked_spi(self_in); + return MP_OBJ_FROM_PTR(&self->lock); +} +MP_DEFINE_CONST_FUN_OBJ_1(async_spi_spi_get_lock_obj, async_spi_spi_get_lock); + +MP_PROPERTY_GETTER(async_spi_spi_lock_obj, + (mp_obj_t)&async_spi_spi_get_lock_obj); + static const mp_rom_map_elem_t async_spi_spi_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&async_spi_spi_deinit_obj) }, { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&async_spi_spi_deinit_obj) }, @@ -247,6 +274,7 @@ static const mp_rom_map_elem_t async_spi_spi_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_write_readinto), MP_ROM_PTR(&async_spi_spi_write_readinto_obj) }, { MP_ROM_QSTR(MP_QSTR_frequency), MP_ROM_PTR(&async_spi_spi_frequency_obj) }, + { MP_ROM_QSTR(MP_QSTR_lock), MP_ROM_PTR(&async_spi_spi_lock_obj) }, }; static MP_DEFINE_CONST_DICT(async_spi_spi_locals_dict, async_spi_spi_locals_dict_table); diff --git a/shared-bindings/async_spi/__init__.c b/shared-bindings/async_spi/__init__.c index b02a2ac0c1f..7580025c05b 100644 --- a/shared-bindings/async_spi/__init__.c +++ b/shared-bindings/async_spi/__init__.c @@ -8,6 +8,7 @@ #include "py/runtime.h" #include "shared-bindings/async_spi/SPI.h" +#include "supervisor/shared/awaitable.h" //| """SPI transfers for asyncio //| @@ -22,16 +23,31 @@ //| //| async def main(): //| spi = async_spi.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) -//| spi.configure(baudrate=8_000_000) //| data = bytearray(512) -//| await spi.readinto(data) +//| async with spi.lock: +//| spi.configure(baudrate=8_000_000) +//| await spi.readinto(data) //| //| asyncio.run(main()) //| """ +//| +//| class Lock: +//| """A bus lock for asyncio code, used as ``async with bus.lock:``. Tasks waiting for it +//| get it in the order they asked; a task cancelled while waiting leaves the line.""" +//| +//| async def __aenter__(self) -> Lock: +//| """Waits for the lock and takes it.""" +//| ... +//| +//| def __aexit__(self) -> None: +//| """Hands the lock to the next task waiting, or releases it.""" +//| ... +//| static const mp_rom_map_elem_t async_spi_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_async_spi) }, { MP_ROM_QSTR(MP_QSTR_SPI), MP_ROM_PTR(&async_spi_spi_type) }, + { MP_ROM_QSTR(MP_QSTR_Lock), MP_ROM_PTR(&circuitpy_async_lock_type) }, }; static MP_DEFINE_CONST_DICT(async_spi_module_globals, async_spi_module_globals_table); diff --git a/supervisor/shared/awaitable.c b/supervisor/shared/awaitable.c index ceddf149b42..1a20687f8df 100644 --- a/supervisor/shared/awaitable.c +++ b/supervisor/shared/awaitable.c @@ -49,6 +49,7 @@ static mp_obj_t async_fun_call(mp_obj_t self_in, size_t n_args, size_t n_kw, con aw->state = CIRCUITPY_AWAITABLE_NEW; aw->n_args = n_args; aw->n_kw = n_kw; + aw->next = NULL; memcpy(aw->args, args, n * sizeof(mp_obj_t)); return MP_OBJ_FROM_PTR(aw); } @@ -154,4 +155,96 @@ MP_DEFINE_CONST_OBJ_TYPE( locals_dict, &awaitable_locals_dict ); +// ---- the lock ---------------------------------------------------------------- + +void circuitpy_async_lock_init(circuitpy_async_lock_obj_t *lock) { + lock->base.type = &circuitpy_async_lock_type; + lock->held = false; + lock->waiting = NULL; +} + +bool circuitpy_async_lock_try(circuitpy_async_lock_obj_t *lock) { + if (lock->held) { + return false; + } + lock->held = true; + return true; +} + +void circuitpy_async_lock_release(circuitpy_async_lock_obj_t *lock) { + circuitpy_awaitable_obj_t *next = lock->waiting; + if (next == NULL) { + lock->held = false; + return; + } + // The lock stays held; it is the next one's now. + lock->waiting = next->next; + CIRCUITPY_ASYNC_FLAG_SET(&next->done); +} + +// __aenter__: take the lock, or join the end of the line. done is set when it is ours. +static void *lock_enter_start(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + circuitpy_async_lock_obj_t *lock = MP_OBJ_TO_PTR(pos_args[0]); + if (circuitpy_async_lock_try(lock)) { + CIRCUITPY_ASYNC_FLAG_SET(done); + return NULL; + } + circuitpy_awaitable_obj_t *aw = CIRCUITPY_AWAITABLE_OF(done); + circuitpy_awaitable_obj_t **p = &lock->waiting; + while (*p != NULL) { + p = &(*p)->next; + } + *p = aw; + return NULL; +} + +static mp_obj_t lock_enter_end(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { + return self_in; +} + +// Cancelled while waiting: leave the line. Cancelled after the lock was handed over but before +// the task ran: pass it on. +static void lock_enter_cancel(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { + circuitpy_async_lock_obj_t *lock = MP_OBJ_TO_PTR(self_in); + if (CIRCUITPY_ASYNC_FLAG_IS_SET(done)) { + circuitpy_async_lock_release(lock); + return; + } + circuitpy_awaitable_obj_t *aw = CIRCUITPY_AWAITABLE_OF(done); + for (circuitpy_awaitable_obj_t **p = &lock->waiting; *p != NULL; p = &(*p)->next) { + if (*p == aw) { + *p = aw->next; + return; + } + } +} +static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(lock_enter_obj, 1, lock_enter_start, lock_enter_end, lock_enter_cancel); + +// __aexit__ returns the lock itself: awaiting it finishes at once with None, so an exception +// in the block is not swallowed. +static mp_obj_t lock_exit(size_t n_args, const mp_obj_t *args) { + circuitpy_async_lock_release(MP_OBJ_TO_PTR(args[0])); + return args[0]; +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(lock_exit_obj, 4, 4, lock_exit); + +static mp_obj_t lock_iternext(mp_obj_t self_in) { + MP_STATE_THREAD(stop_iteration_arg) = mp_const_none; + return MP_OBJ_STOP_ITERATION; +} + +static const mp_rom_map_elem_t lock_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR___aenter__), MP_ROM_PTR(&lock_enter_obj) }, + { MP_ROM_QSTR(MP_QSTR___aexit__), MP_ROM_PTR(&lock_exit_obj) }, +}; +static MP_DEFINE_CONST_DICT(lock_locals_dict, lock_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + circuitpy_async_lock_type, + MP_QSTR_Lock, + MP_TYPE_FLAG_ITER_IS_ITERNEXT, + iter, lock_iternext, + locals_dict, &lock_locals_dict + ); + #endif // MICROPY_PY_ASYNC_AWAIT diff --git a/supervisor/shared/awaitable.h b/supervisor/shared/awaitable.h index 35bb62bd633..6a035a56a07 100644 --- a/supervisor/shared/awaitable.h +++ b/supervisor/shared/awaitable.h @@ -30,6 +30,8 @@ #pragma once +#include + #include "py/obj.h" #include "supervisor/shared/async_flag.h" @@ -53,7 +55,7 @@ typedef struct { extern const mp_obj_type_t circuitpy_async_fun_type; -typedef struct { +typedef struct _circuitpy_awaitable_obj_t { mp_obj_base_t base; const circuitpy_async_fun_obj_t *fun; void *context; // returned by start() @@ -61,11 +63,36 @@ typedef struct { uint8_t state; // CIRCUITPY_AWAITABLE_NEW, _RUNNING or _FINISHED uint16_t n_args; uint16_t n_kw; + struct _circuitpy_awaitable_obj_t *next; // the next one waiting for the same lock mp_obj_t args[]; // positional arguments, then keyword name/value pairs } circuitpy_awaitable_obj_t; extern const mp_obj_type_t circuitpy_awaitable_type; +// The awaitable that owns a done flag passed to start(), end() or cancel(). +#define CIRCUITPY_AWAITABLE_OF(done_ptr) \ + ((circuitpy_awaitable_obj_t *)((char *)(done_ptr) - offsetof(circuitpy_awaitable_obj_t, done))) + +// A lock for asyncio code: `async with lock:`. __aenter__ is an awaitable that takes the lock +// or waits in line for it; __aexit__ hands the lock to the next in line. Embed it in the +// object it guards; it allocates nothing. +typedef struct { + mp_obj_base_t base; + bool held; + circuitpy_awaitable_obj_t *waiting; // first in line, linked through next +} circuitpy_async_lock_obj_t; + +extern const mp_obj_type_t circuitpy_async_lock_type; + +void circuitpy_async_lock_init(circuitpy_async_lock_obj_t *lock); +static inline bool circuitpy_async_lock_held(circuitpy_async_lock_obj_t *lock) { + return lock->held; +} +// For C code: takes the lock if it is free. +bool circuitpy_async_lock_try(circuitpy_async_lock_obj_t *lock); +// Hands the lock to the next in line, or frees it. +void circuitpy_async_lock_release(circuitpy_async_lock_obj_t *lock); + enum { CIRCUITPY_AWAITABLE_NEW, CIRCUITPY_AWAITABLE_RUNNING, CIRCUITPY_AWAITABLE_FINISHED }; #define CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj_name, n_args_min, start, end, cancel) \ From 8dbae290e13c378487e5b0652ccec2d7089e706c Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Sat, 17 Oct 2026 10:19:59 +0000 Subject: [PATCH 27/27] async_spi: fix the lock docstring's literal block --- shared-bindings/async_spi/SPI.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/shared-bindings/async_spi/SPI.c b/shared-bindings/async_spi/SPI.c index 9d8612ed820..4b006de8930 100644 --- a/shared-bindings/async_spi/SPI.c +++ b/shared-bindings/async_spi/SPI.c @@ -244,12 +244,14 @@ MP_PROPERTY_GETTER(async_spi_spi_frequency_obj, (mp_obj_t)&async_spi_spi_get_frequency_obj); //| lock: Lock -//| """The bus's lock. Hold it around a transaction:: +//| """The bus's lock. Hold it around a transaction. //| -//| async with spi.lock: -//| spi.configure(baudrate=8_000_000) -//| await spi.write(command) -//| await spi.readinto(reply) +//| For example:: +//| +//| async with spi.lock: +//| spi.configure(baudrate=8_000_000) +//| await spi.write(command) +//| await spi.readinto(reply) //| """ //| static mp_obj_t async_spi_spi_get_lock(mp_obj_t self_in) {