From c1d61d248eaa4a32d0969956aeb1f56604502bce Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Fri, 2 Oct 2026 20:58:57 +0000 Subject: [PATCH 01/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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);