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..02737a7b5f3 --- /dev/null +++ b/ports/raspberrypi/common-hal/async_spi/SPI.c @@ -0,0 +1,385 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2021 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/async_spi/SPI.h" + +#include "py/runtime.h" +#include "shared-bindings/microcontroller/Pin.h" +#include "supervisor/background_callback.h" + +#include "hardware/dma.h" +#include "hardware/gpio.h" +#include "hardware/irq.h" + +#define NO_INSTANCE 0xff + +// Done flag of the async transfer on each kept RX channel, set when that channel finishes. +static circuitpy_async_flag_t *async_flags[NUM_DMA_CHANNELS]; +// RX channels with the interrupt enabled. +static uint32_t async_irq_mask; + +// Shared DMA_IRQ_0 handler: acknowledges and services only our channels, like audio_dma and rp2pio. +static void __not_in_flash_func(spi_dma_irq_handler)(void) { + uint32_t pending = dma_hw->ints0 & async_irq_mask; + dma_hw->ints0 = pending; + for (uint chan = 0; pending != 0; chan++, pending >>= 1) { + if ((pending & 1) && async_flags[chan] != NULL) { + CIRCUITPY_ASYNC_FLAG_SET(async_flags[chan]); + } + } +} + +static void async_irq_enable(uint chan) { + dma_hw->ints0 = 1u << chan; + if (async_irq_mask == 0) { + irq_add_shared_handler(DMA_IRQ_0, spi_dma_irq_handler, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY); + } + async_irq_mask |= 1u << chan; + dma_irqn_set_channel_enabled(0, chan, true); + irq_set_enabled(DMA_IRQ_0, true); +} + +static void async_irq_disable(uint chan) { + dma_irqn_set_channel_enabled(0, chan, false); + async_irq_mask &= ~(1u << chan); + if (async_irq_mask == 0) { + irq_remove_handler(DMA_IRQ_0, spi_dma_irq_handler); + } + if (dma_hw->inte0 == 0) { + irq_set_enabled(DMA_IRQ_0, false); + } +} + +void common_hal_async_spi_spi_construct(async_spi_spi_obj_t *self, const mcu_pin_obj_t *clock, + const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso) { + size_t instance_index = NO_INSTANCE; + + // Ensure the object starts in its deinit state. + self->clock = NULL; + + if (clock->number % 4 == 2) { + instance_index = (clock->number / 8) % 2; + } + if (mosi != NULL) { + // Make sure the set MOSI matches the clock settings. + if (mosi->number % 4 != 3 || + (mosi->number / 8) % 2 != instance_index) { + instance_index = NO_INSTANCE; + } + } + if (miso != NULL) { + // Make sure the set MOSI matches the clock settings. + if (miso->number % 4 != 0 || + (miso->number / 8) % 2 != instance_index) { + instance_index = NO_INSTANCE; + } + } + + // TODO: Check to see if we're sharing the SPI with a native APA102. + + if (instance_index > 1) { + raise_ValueError_invalid_pins(); + } + + if (instance_index == 0) { + self->peripheral = spi0; + } else if (instance_index == 1) { + self->peripheral = spi1; + } + + if ((spi_get_hw(self->peripheral)->cr1 & SPI_SSPCR1_SSE_BITS) != 0) { + mp_raise_ValueError(MP_ERROR_TEXT("SPI peripheral in use")); + } + + self->async_active = false; + self->dma_kept = false; + self->target_frequency = 250000; + self->real_frequency = spi_init(self->peripheral, self->target_frequency); + + gpio_set_function(clock->number, GPIO_FUNC_SPI); + claim_pin(clock); + self->clock = clock; + + self->mosi = mosi; + if (mosi != NULL) { + gpio_set_function(mosi->number, GPIO_FUNC_SPI); + claim_pin(mosi); + } + + self->miso = miso; + if (miso != NULL) { + gpio_set_function(miso->number, GPIO_FUNC_SPI); + claim_pin(miso); + } +} + +bool common_hal_async_spi_spi_deinited(async_spi_spi_obj_t *self) { + return self->clock == NULL; +} + +void common_hal_async_spi_spi_never_reset(async_spi_spi_obj_t *self) { + common_hal_never_reset_pin(self->clock); + common_hal_never_reset_pin(self->mosi); + common_hal_never_reset_pin(self->miso); +} + +void common_hal_async_spi_spi_mark_deinit(async_spi_spi_obj_t *self) { + self->clock = NULL; +} + +static void _abort(async_spi_spi_obj_t *self); + +void common_hal_async_spi_spi_deinit(async_spi_spi_obj_t *self) { + if (common_hal_async_spi_spi_deinited(self)) { + return; + } + if (self->async_active) { + _abort(self); + CIRCUITPY_ASYNC_FLAG_SET(self->async_done); + } + if (self->dma_kept) { + async_irq_disable(self->dma_rx); + dma_channel_unclaim(self->dma_tx); + dma_channel_unclaim(self->dma_rx); + self->dma_kept = false; + } + spi_deinit(self->peripheral); + + common_hal_reset_pin(self->clock); + common_hal_reset_pin(self->mosi); + common_hal_reset_pin(self->miso); + + self->clock = NULL; +} + +static void _apply(async_spi_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, + uint8_t bits) { + if (baudrate == self->target_frequency && + polarity == self->polarity && + phase == self->phase && + bits == self->bits) { + return; + } + + spi_set_format(self->peripheral, bits, polarity, phase, SPI_MSB_FIRST); + + // Workaround to start with clock line high if polarity=1. The hw SPI peripheral does not do this + // automatically. See https://github.com/raspberrypi/pico-sdk/issues/868 and + // https://forums.raspberrypi.com/viewtopic.php?t=336142 + // TODO: scheduled to be be fixed in pico-sdk 1.5.0. + if (polarity) { + hw_clear_bits(&spi_get_hw(self->peripheral)->cr1, SPI_SSPCR1_SSE_BITS); // disable the SPI + hw_set_bits(&spi_get_hw(self->peripheral)->cr1, SPI_SSPCR1_SSE_BITS); // re-enable the SPI + } + + self->polarity = polarity; + self->phase = phase; + self->bits = bits; + self->target_frequency = baudrate; + self->real_frequency = spi_set_baudrate(self->peripheral, baudrate); +} + +static void _end(async_spi_spi_obj_t *self); + +void common_hal_async_spi_spi_configure(async_spi_spi_obj_t *self, uint32_t baudrate, + uint8_t polarity, uint8_t phase, uint8_t bits) { + _end(self); + _apply(self, baudrate, polarity, phase, bits); +} + +uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self) { + return self->real_frequency; +} + +// Start a transfer. With DMA it runs in the background and done is set from the interrupt; +// otherwise it is done in software before this returns. An out or in buffer shorter than the +// transfer is one byte repeated or dropped. The DMA channels are kept until deinit, for buses +// that send often. Returns whether DMA is running. +static bool _start(async_spi_spi_obj_t *self, + const uint8_t *data_out, size_t out_len, + uint8_t *data_in, size_t in_len, circuitpy_async_flag_t *done) { + size_t len = MAX(out_len, in_len); + // Only use DMA if both data buffers are in SRAM. Otherwise, we'll stall the DMA with PSRAM or flash cache misses. + bool use_dma = len >= 32 && data_in >= (uint8_t *)SRAM_BASE && data_out >= (uint8_t *)SRAM_BASE; + if (use_dma && !self->dma_kept) { + int chan_tx = dma_claim_unused_channel(false); + int chan_rx = dma_claim_unused_channel(false); + if (chan_tx >= 0 && chan_rx >= 0) { + self->dma_tx = chan_tx; + self->dma_rx = chan_rx; + self->dma_kept = true; + async_irq_enable(chan_rx); + } else { + // If we have claimed only one channel successfully, release it. + if (chan_tx >= 0) { + dma_channel_unclaim(chan_tx); + } + if (chan_rx >= 0) { + dma_channel_unclaim(chan_rx); + } + use_dma = false; + } + } + if (use_dma) { + dma_channel_config c = dma_channel_get_default_config(self->dma_tx); + channel_config_set_transfer_data_size(&c, DMA_SIZE_8); + channel_config_set_dreq(&c, spi_get_index(self->peripheral) ? DREQ_SPI1_TX : DREQ_SPI0_TX); + channel_config_set_read_increment(&c, out_len == len); + channel_config_set_write_increment(&c, false); + dma_channel_configure(self->dma_tx, &c, + &spi_get_hw(self->peripheral)->dr, + data_out, + len, + false); + + c = dma_channel_get_default_config(self->dma_rx); + channel_config_set_transfer_data_size(&c, DMA_SIZE_8); + channel_config_set_dreq(&c, spi_get_index(self->peripheral) ? DREQ_SPI1_RX : DREQ_SPI0_RX); + channel_config_set_read_increment(&c, false); + channel_config_set_write_increment(&c, in_len == len); + dma_channel_configure(self->dma_rx, &c, + data_in, + &spi_get_hw(self->peripheral)->dr, + len, + false); + + // Before the start, so that a short transfer cannot finish unseen. + async_flags[self->dma_rx] = done; + dma_start_channel_mask((1u << self->dma_rx) | (1u << self->dma_tx)); + return true; + } + + // Use software for small transfers, or if couldn't claim two DMA channels + // Never have more transfers in flight than will fit into the RX FIFO, + // else FIFO will overflow if this code is heavily interrupted. + const size_t fifo_depth = 8; + size_t rx_remaining = len; + size_t tx_remaining = len; + + while (rx_remaining || tx_remaining) { + if (tx_remaining && spi_is_writable(self->peripheral) && rx_remaining - tx_remaining < fifo_depth) { + spi_get_hw(self->peripheral)->dr = (uint32_t)*data_out; + // Increment only if the buffer is the transfer length. It's 1 otherwise. + if (out_len == len) { + data_out++; + } + --tx_remaining; + } + if (rx_remaining && spi_is_readable(self->peripheral)) { + *data_in = (uint8_t)spi_get_hw(self->peripheral)->dr; + // Increment only if the buffer is the transfer length. It's 1 otherwise. + if (in_len == len) { + data_in++; + } + --rx_remaining; + } + RUN_BACKGROUND_TASKS; + } + return false; +} + +// Waits for the running async transfer, if any, and sets its done flag. No background tasks: +// the caller holds the bus, and one of them may want it. +static void _end(async_spi_spi_obj_t *self) { + if (!self->async_active) { + return; + } + // The RX channel finishes last. + while (dma_channel_is_busy(self->dma_rx)) { + } + self->async_active = false; + async_flags[self->dma_rx] = NULL; + CIRCUITPY_ASYNC_FLAG_SET(self->async_done); +} + +// Start an async transfer after the previous one; done is set when it has finished, here if +// it ran in software. The buffers must stay valid until then. +static __attribute__((noinline)) void _async_start(async_spi_spi_obj_t *self, const uint8_t *data_out, + size_t out_len, uint8_t *data_in, size_t in_len, circuitpy_async_flag_t *done) { + _end(self); + CIRCUITPY_ASYNC_FLAG_INIT(done); + self->async_done = done; + // An empty data side means there is nothing to send, whatever the one-byte side holds. + self->async_active = out_len != 0 && in_len != 0 && + _start(self, data_out, out_len, data_in, in_len, done); + if (!self->async_active) { + CIRCUITPY_ASYNC_FLAG_SET(done); + } +} + +void *common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data, + size_t len, circuitpy_async_flag_t *done) { + _async_start(self, data, len, &self->one_byte, 1, done); + return NULL; +} + +void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, + size_t len, uint8_t write_value, circuitpy_async_flag_t *done) { + // End the previous transfer first: an async write may still be receiving into one_byte. + _end(self); + self->one_byte = write_value; + _async_start(self, &self->one_byte, 1, data, len, done); + return NULL; +} + +void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, + const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done) { + _async_start(self, data_out, len, data_in, len, done); + return NULL; +} + +// Stop the running DMA transfer without setting its done flag. +static void _abort(async_spi_spi_obj_t *self) { + // A completion interrupt may still come (RP2040-E13); the handler skips a NULL flag. + async_flags[self->dma_rx] = NULL; + #if PICO_RP2350 + // RP2350-E5: an enabled channel can retrigger after the abort. + hw_clear_bits(&dma_hw->ch[self->dma_tx].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); + hw_clear_bits(&dma_hw->ch[self->dma_rx].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); + #endif + // Both channels at once; as dma_channel_abort(), the busy bits clear once in-flight + // transfers have retired. + dma_hw->abort = (1u << self->dma_tx) | (1u << self->dma_rx); + while ((dma_hw->ch[self->dma_tx].ctrl_trig | dma_hw->ch[self->dma_rx].ctrl_trig) & DMA_CH0_CTRL_TRIG_BUSY_BITS) { + } + self->async_active = false; + while (spi_is_busy(self->peripheral)) { + } + while (spi_is_readable(self->peripheral)) { + (void)spi_get_hw(self->peripheral)->dr; + } + spi_get_hw(self->peripheral)->icr = SPI_SSPICR_RORIC_BITS; +} + +// Stops the transfer started with done, or finishes it if done is set. Does nothing for an +// earlier transfer, and does not allocate. +static void _stop(async_spi_spi_obj_t *self, circuitpy_async_flag_t *done) { + if (!self->async_active || self->async_done != done) { + return; + } + if (CIRCUITPY_ASYNC_FLAG_IS_SET(done)) { + _end(self); + return; + } + _abort(self); +} + +bool common_hal_async_spi_spi_transfer_finish(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { + _stop(self, done); + return true; +} + +mp_obj_t common_hal_async_spi_spi_transfer_end(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { + _stop(self, done); + return mp_const_none; +} + +void common_hal_async_spi_spi_transfer_cancel(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { + _stop(self, 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..82abbc8ce6e --- /dev/null +++ b/ports/raspberrypi/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 "supervisor/shared/awaitable.h" + +#include "hardware/spi.h" + +typedef struct { + mp_obj_base_t base; + circuitpy_async_lock_obj_t lock; + spi_inst_t *peripheral; + const mcu_pin_obj_t *clock; // NULL once deinited + const mcu_pin_obj_t *mosi; + 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; +} 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/common-hal/busio/SPI.c b/ports/raspberrypi/common-hal/busio/SPI.c deleted file mode 100644 index f4d86f538d7..00000000000 --- a/ports/raspberrypi/common-hal/busio/SPI.c +++ /dev/null @@ -1,325 +0,0 @@ -// This file is part of the CircuitPython project: https://circuitpython.org -// -// SPDX-FileCopyrightText: Copyright (c) 2021 Scott Shawcroft for Adafruit Industries -// -// SPDX-License-Identifier: MIT - -#include "shared-bindings/busio/SPI.h" - -#include "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) { - 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) { - 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); -} - -bool common_hal_busio_spi_deinited(busio_spi_obj_t *self) { - return self->clock == NULL; -} - -void common_hal_busio_spi_mark_deinit(busio_spi_obj_t *self) { - self->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); -} - -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); - - return true; -} - -bool common_hal_busio_spi_try_lock(busio_spi_obj_t *self) { - if (common_hal_busio_spi_deinited(self)) { - return false; - } - bool grabbed_lock = false; - if (!self->has_lock) { - grabbed_lock = true; - self->has_lock = true; - } - return grabbed_lock; -} - -bool common_hal_busio_spi_has_lock(busio_spi_obj_t *self) { - return self->has_lock; -} - -void common_hal_busio_spi_unlock(busio_spi_obj_t *self) { - self->has_lock = false; -} - -// 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); - } -} - -static bool _transfer(busio_spi_obj_t *self, - const uint8_t *data_out, size_t out_len, - uint8_t *data_in, size_t in_len) { - if (_start(self, data_out, out_len, data_in, in_len, false)) { - // TODO: We should idle here until we get a DMA interrupt or something else. - _end(self, true); - } - return true; -} - -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)); -} - -void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data, size_t len, - circuitpy_async_flag_t *done) { - common_hal_busio_spi_end(self); - CIRCUITPY_ASYNC_FLAG_INIT(done); - self->async_done = done; - self->async_active = _start(self, data, len, &self->discard, 1, true); - if (!self->async_active) { - CIRCUITPY_ASYNC_FLAG_SET(done); - } -} - -void common_hal_busio_spi_end(busio_spi_obj_t *self) { - if (self->async_active) { - // No background tasks here: the caller holds the bus, and one of them may want it. - _end(self, false); - self->async_active = false; - CIRCUITPY_ASYNC_FLAG_SET(self->async_done); - } -} - -bool common_hal_busio_spi_read(busio_spi_obj_t *self, - uint8_t *data, size_t len, uint8_t write_value) { - uint32_t data_out = write_value << 24 | write_value << 16 | write_value << 8 | write_value; - 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); -} - -uint32_t common_hal_busio_spi_get_frequency(busio_spi_obj_t *self) { - return self->real_frequency; -} - -uint8_t common_hal_busio_spi_get_phase(busio_spi_obj_t *self) { - return self->phase; -} - -uint8_t common_hal_busio_spi_get_polarity(busio_spi_obj_t *self) { - return self->polarity; -} diff --git a/ports/raspberrypi/common-hal/busio/SPI.h b/ports/raspberrypi/common-hal/busio/SPI.h index 4acb15b7b2c..8b5ec5c444c 100644 --- a/ports/raspberrypi/common-hal/busio/SPI.h +++ b/ports/raspberrypi/common-hal/busio/SPI.h @@ -1,34 +1,10 @@ // This file is part of the CircuitPython project: https://circuitpython.org // -// SPDX-FileCopyrightText: Copyright (c) 2021 Scott Shawcroft for Adafruit Industries +// SPDX-FileCopyrightText: Copyright (c) 2016 Scott Shawcroft // // SPDX-License-Identifier: MIT #pragma once -#include "common-hal/microcontroller/Pin.h" - -#include "py/obj.h" -#include "supervisor/shared/async_flag.h" - -#include "hardware/spi.h" - -typedef struct { - mp_obj_base_t base; - spi_inst_t *peripheral; - 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; +// busio.SPI is built on common-hal/async_spi. +#include "shared-module/busio/SPI.h" diff --git a/ports/raspberrypi/common-hal/microcontroller/__init__.c b/ports/raspberrypi/common-hal/microcontroller/__init__.c index 3d8bd381f5e..9021778157b 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, +// async_spi (common-hal/async_spi/SPI.c) each register a shared +// handler with irq_add_shared_handler() and // service only their own DMA channels. Any new code that needs a // DMA completion interrupt should do the same: add a shared handler // on DMA_IRQ_0, check dma_hw->ints0, and acknowledge only its own diff --git a/ports/raspberrypi/common-hal/picogame/Display.c b/ports/raspberrypi/common-hal/picogame/Display.c index 9c0fcd17011..8dda6054346 100644 --- a/ports/raspberrypi/common-hal/picogame/Display.c +++ b/ports/raspberrypi/common-hal/picogame/Display.c @@ -40,7 +40,7 @@ void common_hal_picogame_display_construct(picogame_display_obj_t *self, MP_QSTR_display, MP_QSTR_FourWire, mp_obj_get_type(display->bus.bus)->name); } fourwire_fourwire_obj_t *fw = MP_OBJ_TO_PTR(display->bus.bus); - self->spi = fw->bus->peripheral; + self->spi = fw->bus->async_spi.peripheral; #if CIRCUITPY_PICOGAME_RGB444 // Tell the panel which pixel format we'll send (COLMOD). Asserting it here also recovers from diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index 5b7aa21029b..efb5f77e9ee 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -6,7 +6,8 @@ CIRCUITPY_OPTIMIZE_PROPERTY_FLASH_SIZE ?= 1 CIRCUITPY_WIFI_RADIO_SETTABLE_MAC_ADDRESS = 0 CIRCUITPY_RP2PIO ?= 1 -CIRCUITPY_BUSIO_SPI_ASYNC ?= 1 +# SPI is async on this port; busio.SPI is built on it. +CIRCUITPY_ASYNC_SPI ?= 1 # The 24 KB stack has room for a larger display refresh buffer. CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048 CIRCUITPY_NEOPIXEL_WRITE ?= $(CIRCUITPY_RP2PIO) diff --git a/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml b/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml index 45fd76cfce9..663daf40b2b 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..aeb33c7fe74 --- /dev/null +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.c @@ -0,0 +1,261 @@ +// 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/gc.h" +#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); + async_spi_spi_obj_t *self = userdata; + self->result = result; + CIRCUITPY_ASYNC_FLAG_SET(self->done); + port_wake_main_task_from_isr(); +} + +// 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)) { + port_task_sleep_ms(1); + } + self->done = NULL; +} + +// A fixed devicetree device, as for board.SPI(): already initialized, never released. +void common_hal_async_spi_spi_construct_from_device(async_spi_spi_obj_t *self, + const struct device *dev) { + self->spi_device = dev; + self->dynamic = false; + self->clock = NULL; + self->mosi = NULL; + self->miso = NULL; + self->active_config = 0; + for (size_t i = 0; i < 2; i++) { + self->config[i].frequency = 100000; + self->config[i].operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_LINES_SINGLE; + } + self->done = NULL; + self->fill = NULL; + self->fill_len = 0; + self->fill_from_port = false; +} + +static void free_fill(async_spi_spi_obj_t *self) { + if (self->fill_from_port) { + port_free(self->fill); + } + self->fill = NULL; + self->fill_len = 0; + self->fill_from_port = false; +} + +void common_hal_async_spi_spi_construct(async_spi_spi_obj_t *self, const mcu_pin_obj_t *clock, + const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso) { + const struct device *dev = NULL; + 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); + } + + common_hal_async_spi_spi_construct_from_device(self, dev); + self->dynamic = true; + self->clock = clock; + self->mosi = mosi; + self->miso = miso; + + // 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); + free_fill(self); + // A fixed devicetree device (board.SPI()) stays usable. + if (!self->dynamic) { + return; + } + (void)iobroker_release(self->spi_device); + self->spi_device = NULL; + self->clock = NULL; + self->mosi = NULL; + self->miso = NULL; +} + +void common_hal_async_spi_spi_never_reset(async_spi_spi_obj_t *self) { +} + +void common_hal_async_spi_spi_mark_deinit(async_spi_spi_obj_t *self) { + if (self->dynamic) { + self->spi_device = NULL; + } +} + +static uint16_t operation_of(uint8_t polarity, uint8_t phase, uint8_t bits) { + uint16_t operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(bits) | SPI_LINES_SINGLE; + if (polarity) { + operation |= SPI_MODE_CPOL; + } + if (phase) { + operation |= SPI_MODE_CPHA; + } + return operation; +} + +void common_hal_async_spi_spi_configure(async_spi_spi_obj_t *self, uint32_t baudrate, + uint8_t polarity, uint8_t phase, uint8_t bits) { + wait_done(self); + apply_config(self, baudrate, operation_of(polarity, phase, bits)); +} + +uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self) { + 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; + self->result = 0; + 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) { + // Reported by transfer_end() or transfer_finish(); nothing raises here, as a + // blocking busio call may run from a background callback. + self->result = ret; + CIRCUITPY_ASYNC_FLAG_SET(done); + } +} + +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 NULL; +} + +void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data, + size_t len, uint8_t write_value, circuitpy_async_flag_t *done) { + // Without a TX buffer a driver sends its own idle value (0xFF on nRF), so the read sends + // write_value from a buffer, kept for the next read. The previous transfer must be over + // before it is touched. + wait_done(self); + uint8_t *fill = NULL; + if (len != 0) { + if (self->fill_len < len) { + free_fill(self); + if (gc_alloc_possible()) { + self->fill = m_malloc(len); + } else { + self->fill = port_malloc(len, false); + self->fill_from_port = true; + } + if (self->fill == NULL) { + self->fill_from_port = false; + self->result = -ENOMEM; + CIRCUITPY_ASYNC_FLAG_INIT(done); + CIRCUITPY_ASYNC_FLAG_SET(done); + self->done = done; + return NULL; + } + self->fill_len = len; + } + fill = self->fill; + memset(fill, write_value, len); + } + start(self, fill, data, len, done); + return NULL; +} + +void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self, + const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done) { + start(self, data_out, data_in, len, done); + return NULL; +} + +// Called once done is set. +bool common_hal_async_spi_spi_transfer_finish(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { + if (self->done != done) { + return true; + } + self->done = NULL; + return self->result == 0; +} + +mp_obj_t common_hal_async_spi_spi_transfer_end(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { + if (!common_hal_async_spi_spi_transfer_finish(self, context, done)) { + raise_zephyr_error(self->result); + } + return mp_const_none; +} + +void common_hal_async_spi_spi_transfer_cancel(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done) { + 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 new file mode 100644 index 00000000000..1df2aa9a2a9 --- /dev/null +++ b/ports/zephyr-cp/common-hal/async_spi/SPI.h @@ -0,0 +1,40 @@ +// 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 "supervisor/shared/awaitable.h" + +#include +#include +#include + +typedef struct { + mp_obj_base_t base; + circuitpy_async_lock_obj_t lock; + const struct device *spi_device; // NULL once deinited + bool dynamic; // routed to pins at construction, released at deinit + struct spi_config config[2]; // two slots: a driver sees a change by the pointer + 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 + 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; + struct spi_buf_set rx; + uint8_t *fill; // the bytes a read sends, when not zero + size_t fill_len; + bool fill_from_port; // fill came from port_malloc, not the heap +} async_spi_spi_obj_t; + +void common_hal_async_spi_spi_construct_from_device(async_spi_spi_obj_t *self, + const struct device *dev); diff --git a/ports/zephyr-cp/common-hal/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/common-hal/busio/SPI.c b/ports/zephyr-cp/common-hal/busio/SPI.c index 78a0943354c..bb11d57d2eb 100644 --- a/ports/zephyr-cp/common-hal/busio/SPI.c +++ b/ports/zephyr-cp/common-hal/busio/SPI.c @@ -5,325 +5,13 @@ // SPDX-License-Identifier: MIT #include "shared-bindings/busio/SPI.h" -#include "shared-bindings/microcontroller/Pin.h" -#include "py/mperrno.h" -#include "py/runtime.h" -#include "py/gc.h" -#include "shared/runtime/interrupt_char.h" -#include "supervisor/port.h" - -#include "bindings/zephyr_kernel/__init__.h" - -#include -#include -#include -#include -#include - -// Helper function for Zephyr-specific initialization from device tree mp_obj_t common_hal_busio_spi_construct_from_device(busio_spi_obj_t *self, const struct device *spi_device) { self->base.type = &busio_spi_type; - self->spi_device = spi_device; - k_mutex_init(&self->mutex); + common_hal_async_spi_spi_construct_from_device(&self->async_spi, spi_device); + self->sent = NULL; self->has_lock = false; - self->active_config = 0; - self->dynamic = false; - self->clock = NULL; - self->mosi = NULL; - self->miso = NULL; - - k_poll_signal_init(&self->signal); - - // Default configuration for both config slots - self->config[0].frequency = 100000; - self->config[0].operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_LINES_SINGLE; - self->config[1].frequency = 100000; - self->config[1].operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_LINES_SINGLE; - + self->polarity = 0; + self->phase = 0; return MP_OBJ_FROM_PTR(self); } - -// Standard busio construct: pick a free peripheral instance and route it to -// the requested pins at runtime (supported on nRF SoCs). -void common_hal_busio_spi_construct(busio_spi_obj_t *self, - const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, - const mcu_pin_obj_t *miso, bool half_duplex) { - if (half_duplex) { - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_half_duplex); - } - - const struct device *dev = NULL; - int ret = iobroker_spi_allocate(clock->package_pin, - mosi != NULL ? mosi->package_pin : IOBROKER_NO_PIN, - miso != NULL ? miso->package_pin : IOBROKER_NO_PIN, &dev); - if (ret < 0) { - if (ret == -ENODEV) { - mp_raise_ValueError(MP_ERROR_TEXT("All SPI peripherals are in use")); - } - if (ret == -EBUSY) { - mp_raise_ValueError(MP_ERROR_TEXT("Internal resource(s) in use")); - } - if (ret == -EINVAL) { - raise_ValueError_invalid_pins(); - } - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Use device tree to define %q devices"), MP_QSTR_SPI); - } - - common_hal_busio_spi_construct_from_device(self, dev); - self->dynamic = true; - self->clock = clock; - self->mosi = mosi; - self->miso = miso; - - // Initialize the deferred device now that it is routed to the requested - // pins. Fixed devicetree instances are already initialized (-EALREADY). - int init_ret = device_init(dev); - if (init_ret < 0 && init_ret != -EALREADY) { - // The failed init may have routed pins and left the device in a - // partial state; deinit gives up the claim and resets the pins. - common_hal_busio_spi_deinit(self); - raise_zephyr_error(init_ret); - } -} - -bool common_hal_busio_spi_deinited(busio_spi_obj_t *self) { - return self->spi_device == NULL; -} - -void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { - if (common_hal_busio_spi_deinited(self)) { - return; - } - if (self->dynamic) { - // The release de-inits the device, which applies its low-power - // pinctrl state and leaves the routed pins disconnected. - (void)iobroker_release(self->spi_device); - self->clock = NULL; - self->mosi = NULL; - self->miso = NULL; - self->spi_device = NULL; - } -} - -void common_hal_busio_spi_mark_deinit(busio_spi_obj_t *self) { - if (self->dynamic) { - self->spi_device = NULL; - } -} - -bool common_hal_busio_spi_try_lock(busio_spi_obj_t *self) { - if (common_hal_busio_spi_deinited(self)) { - return false; - } - - self->has_lock = k_mutex_lock(&self->mutex, K_NO_WAIT) == 0; - return self->has_lock; -} - -bool common_hal_busio_spi_has_lock(busio_spi_obj_t *self) { - return self->has_lock; -} - -void common_hal_busio_spi_unlock(busio_spi_obj_t *self) { - self->has_lock = false; - k_mutex_unlock(&self->mutex); -} - -bool common_hal_busio_spi_configure(busio_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, uint8_t phase, uint8_t bits) { - if (common_hal_busio_spi_deinited(self)) { - return false; - } - - // Set operation mode based on polarity and phase - uint16_t operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(bits) | SPI_LINES_SINGLE; - - if (polarity) { - operation |= SPI_MODE_CPOL; - } - if (phase) { - operation |= SPI_MODE_CPHA; - } - - // Check if settings have changed. We must switch to the other config slot if they have because - // Zephyr drivers are allowed to use the pointer value to know if it has changed. - struct spi_config *current_config = &self->config[self->active_config]; - if (current_config->frequency != baudrate || current_config->operation != operation) { - // Settings changed, switch to the other config slot - self->active_config = 1 - self->active_config; - - // Update the new active configuration - self->config[self->active_config].frequency = baudrate; - self->config[self->active_config].operation = operation; - } - - return true; -} - -bool common_hal_busio_spi_write(busio_spi_obj_t *self, const uint8_t *data, size_t len) { - if (common_hal_busio_spi_deinited(self)) { - return false; - } - - if (len == 0) { - return true; - } - - const struct spi_buf tx_buf = { - .buf = (void *)data, - .len = len - }; - const struct spi_buf_set tx = { - .buffers = &tx_buf, - .count = 1 - }; - - // Initialize the signal for async operation - k_poll_signal_reset(&self->signal); - - int ret = spi_transceive_signal(self->spi_device, &self->config[self->active_config], &tx, NULL, &self->signal); - if (ret != 0) { - return false; - } - - // Wait for the transfer to complete while running background tasks - int signaled = 0; - int result = 0; - while (!signaled && !mp_hal_is_interrupted()) { - RUN_BACKGROUND_TASKS; - k_poll_signal_check(&self->signal, &signaled, &result); - } - - return signaled && result == 0; -} - -bool common_hal_busio_spi_read(busio_spi_obj_t *self, uint8_t *data, size_t len, uint8_t write_value) { - if (common_hal_busio_spi_deinited(self)) { - return false; - } - - if (len == 0) { - return true; - } - - // For read, we need to write dummy bytes - // We'll allocate a temporary buffer if write_value is not 0 - uint8_t *tx_data = NULL; - bool need_free = false; - bool used_port_malloc = false; - - if (write_value != 0) { - // Use port_malloc if GC isn't active, otherwise use m_malloc - if (gc_alloc_possible()) { - tx_data = m_malloc(len); - } else { - tx_data = port_malloc(len, false); - used_port_malloc = true; - } - if (tx_data == NULL) { - return false; - } - memset(tx_data, write_value, len); - need_free = true; - } - - const struct spi_buf tx_buf = { - .buf = tx_data, - .len = tx_data ? len : 0 - }; - const struct spi_buf_set tx = { - .buffers = &tx_buf, - .count = tx_data ? 1 : 0 - }; - - const struct spi_buf rx_buf = { - .buf = data, - .len = len - }; - const struct spi_buf_set rx = { - .buffers = &rx_buf, - .count = 1 - }; - - // Initialize the signal for async operation - k_poll_signal_reset(&self->signal); - - int ret = spi_transceive_signal(self->spi_device, &self->config[self->active_config], &tx, &rx, &self->signal); - - // Wait for the transfer to complete while running background tasks. The driver may read the - // fill buffer until then. - int signaled = 0; - int result = 0; - while (ret == 0 && !signaled && !mp_hal_is_interrupted()) { - RUN_BACKGROUND_TASKS; - k_poll_signal_check(&self->signal, &signaled, &result); - } - - if (need_free) { - if (used_port_malloc) { - port_free(tx_data); - } else { - m_free(tx_data); - } - } - - return ret == 0 && signaled && result == 0; -} - -bool common_hal_busio_spi_transfer(busio_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len) { - if (common_hal_busio_spi_deinited(self)) { - return false; - } - - if (len == 0) { - return true; - } - - const struct spi_buf tx_buf = { - .buf = (void *)data_out, - .len = len - }; - const struct spi_buf_set tx = { - .buffers = &tx_buf, - .count = 1 - }; - - const struct spi_buf rx_buf = { - .buf = data_in, - .len = len - }; - const struct spi_buf_set rx = { - .buffers = &rx_buf, - .count = 1 - }; - - // Initialize the signal for async operation - k_poll_signal_reset(&self->signal); - - int ret = spi_transceive_signal(self->spi_device, &self->config[self->active_config], &tx, &rx, &self->signal); - if (ret != 0) { - return false; - } - - // Wait for the transfer to complete while running background tasks - int signaled = 0; - int result = 0; - while (!signaled && !mp_hal_is_interrupted()) { - RUN_BACKGROUND_TASKS; - k_poll_signal_check(&self->signal, &signaled, &result); - } - - return signaled && result == 0; -} - -uint32_t common_hal_busio_spi_get_frequency(busio_spi_obj_t *self) { - return self->config[self->active_config].frequency; -} - -uint8_t common_hal_busio_spi_get_phase(busio_spi_obj_t *self) { - return (self->config[self->active_config].operation & SPI_MODE_CPHA) ? 1 : 0; -} - -uint8_t common_hal_busio_spi_get_polarity(busio_spi_obj_t *self) { - return (self->config[self->active_config].operation & SPI_MODE_CPOL) ? 1 : 0; -} diff --git a/ports/zephyr-cp/common-hal/busio/SPI.h b/ports/zephyr-cp/common-hal/busio/SPI.h index fa5de574116..3d171915dd2 100644 --- a/ports/zephyr-cp/common-hal/busio/SPI.h +++ b/ports/zephyr-cp/common-hal/busio/SPI.h @@ -6,27 +6,8 @@ #pragma once -#include "py/obj.h" +// busio.SPI is built on common-hal/async_spi. +#include "shared-module/busio/SPI.h" -#include "common-hal/microcontroller/Pin.h" -#include -#include - -typedef struct { - mp_obj_base_t base; - const struct device *spi_device; - struct k_mutex mutex; - struct spi_config config[2]; // Two configs for pointer comparison by driver - uint8_t active_config; // Index of currently active config (0 or 1) - struct k_poll_signal signal; - const mcu_pin_obj_t *clock; - const mcu_pin_obj_t *mosi; - const mcu_pin_obj_t *miso; - // True when the underlying Zephyr device was dynamically routed to the - // pins above at construction time. - bool dynamic; - bool has_lock; -} busio_spi_obj_t; - -// Helper function for Zephyr-specific initialization from device tree +// A fixed devicetree device, for the generated board.SPI(). mp_obj_t common_hal_busio_spi_construct_from_device(busio_spi_obj_t *self, const struct device *spi_device); diff --git a/ports/zephyr-cp/cptools/build_circuitpython.py b/ports/zephyr-cp/cptools/build_circuitpython.py index 74d3eed9368..1581a4db8fa 100644 --- a/ports/zephyr-cp/cptools/build_circuitpython.py +++ b/ports/zephyr-cp/cptools/build_circuitpython.py @@ -653,6 +653,10 @@ async def build_circuitpython(): # noqa: C901 # have no common-hal override, so always compile them. if module.name == "usb_hid": hal_source.append(top / "shared-module/usb_hid/report_descriptors.c") + # busio.SPI is built on common-hal/async_spi, with or without the async_spi module. + if module.name == "busio": + hal_source.append(top / "shared-module/busio/SPI.c") + hal_source.append(top / "ports/zephyr-cp/common-hal/async_spi/SPI.c") hal_source.extend(top.glob(f"shared-bindings/{module.name}/**/*.c")) if module.name in LIBRARY_SOURCE: for library_source in LIBRARY_SOURCE[module.name]: @@ -722,6 +726,8 @@ async def build_circuitpython(): # noqa: C901 f"-DCIRCUITPY_WIFI_PING={1 if mpconfigboard.get('CIRCUITPY_WIFI_PING', True) else 0}" ) + # The busio and async_spi modules both name common-hal/async_spi/SPI.c. + hal_source = list(dict.fromkeys(hal_source)) source_files = supervisor_source + hal_source + ["extmod/vfs.c"] if filesystem_littlefs: source_files.extend( diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index 660b263262f..2cf5a88ca5d 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..55f3d5eeb15 --- /dev/null +++ b/ports/zephyr-cp/tests/test_async_spi.py @@ -0,0 +1,113 @@ +# 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.lock.__aenter__() # held for the whole test, as busio's try_lock would be + spi.configure(baudrate=1_000_000) + print("frequency", spi.frequency) + bad = 0 + 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: + try: + await spi2.readinto(inb, end=8) + except RuntimeError: + print("no lock raised") + async with spi2.lock: + await spi2.readinto(inb, end=8, write_value=0x5A) + if inb[:8] != b"\\x5a" * 8: + bad += 1 + print("second bus differs") + 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 + # 10 transfers of 4 KB at 1 MHz take about 330 ms; the ticker runs many times in that. + ticker = int(output.split("ticker ran ")[1].split()[0]) + assert ticker > 20, ticker + assert "bad argument raised" in output + assert "deinit raised" in output + assert "bad 0" in output + assert "done" in output diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 85322ce61ee..b0d4c549ff0 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 \ @@ -537,7 +542,6 @@ SRC_COMMON_HAL_ALL = \ audiopwmio/__init__.c \ board/__init__.c \ busio/I2C.c \ - busio/SPI.c \ busio/UART.c \ busio/__init__.c \ camera/__init__.c \ @@ -918,6 +922,15 @@ SRC_SHARED_MODULE_ALL += \ touchio/__init__.c endif +# On a port with async SPI, busio.SPI is shared code on top of common-hal/async_spi. +ifeq ($(CIRCUITPY_ASYNC_SPI),1) +SRC_SHARED_MODULE_ALL += \ + busio/SPI.c +else +SRC_COMMON_HAL_ALL += \ + busio/SPI.c +endif + ifeq ($(CIRCUITPY_SSL_MBEDTLS),0) SRC_COMMON_HAL_ALL += \ ssl/__init__.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 702fceb5794..d1525c8aa57 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) +# A port with async SPI: the async_spi module, and busio.SPI built on the same implementation. +CIRCUITPY_ASYNC_SPI ?= 0 +CFLAGS += -DCIRCUITPY_ASYNC_SPI=$(CIRCUITPY_ASYNC_SPI) + CIRCUITPY_ATEXIT ?= $(CIRCUITPY_FULL_BUILD) CFLAGS += -DCIRCUITPY_ATEXIT=$(CIRCUITPY_ATEXIT) @@ -240,7 +244,7 @@ CFLAGS += -DCIRCUITPY_BUSIO_SPI=$(CIRCUITPY_BUSIO_SPI) # The port implements common_hal_busio_spi_write_start() and common_hal_busio_spi_end(), so a # display on a FourWire bus can send one strip while the next is being composited. -CIRCUITPY_BUSIO_SPI_ASYNC ?= 0 +CIRCUITPY_BUSIO_SPI_ASYNC ?= $(CIRCUITPY_ASYNC_SPI) CFLAGS += -DCIRCUITPY_BUSIO_SPI_ASYNC=$(CIRCUITPY_BUSIO_SPI_ASYNC) CIRCUITPY_BUSIO_UART ?= $(CIRCUITPY_BUSIO) diff --git a/shared-bindings/async_spi/SPI.c b/shared-bindings/async_spi/SPI.c new file mode 100644 index 00000000000..9d8612ed820 --- /dev/null +++ b/shared-bindings/async_spi/SPI.c @@ -0,0 +1,287 @@ +// 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/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" + +//| 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. A transfer or ``configure()`` needs the bus's `lock` +//| held, so that a transaction of several transfers is not interleaved with another task's.""" +//| +//| def __init__( +//| self, +//| 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.""" +//| ... +//| + +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, 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); + 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); + circuitpy_async_lock_init(&self->lock); + 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; +} + +// For the calls that use the bus. +static MP_NOINLINE async_spi_spi_obj_t *locked_spi(mp_obj_t self_in) { + async_spi_spi_obj_t *self = checked_spi(self_in); + if (!circuitpy_async_lock_held(&self->lock)) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Function requires lock")); + } + return self; +} + +//| def deinit(self) -> None: +//| """Turn off the SPI bus. A transfer still running is stopped.""" +//| ... +//| +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. + +// The awaitable passes the object as mp_obj_t; the common-hal functions take it typed. +static mp_obj_t async_spi_spi_transfer_end(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { + return common_hal_async_spi_spi_transfer_end(MP_OBJ_TO_PTR(self_in), context, done); +} +static void async_spi_spi_transfer_cancel(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { + common_hal_async_spi_spi_transfer_cancel(MP_OBJ_TO_PTR(self_in), context, done); +} + +//| 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. Needs the `lock`.""" +//| ... +//| +static mp_obj_t async_spi_spi_configure(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_baudrate, ARG_polarity, ARG_phase, ARG_bits }; + async_spi_spi_obj_t *self = locked_spi(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_configure_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_configure_args), busio_spi_configure_args, args); + + 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(self, args[ARG_baudrate].u_int, polarity, phase, bits); + return mp_const_none; +} +static MP_DEFINE_CONST_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 = locked_spi(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_write_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_write_args), busio_spi_write_args, args); + + 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); + 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, + async_spi_spi_transfer_end, async_spi_spi_transfer_cancel); + +//| 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 = locked_spi(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_readinto_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_readinto_args), busio_spi_readinto_args, args); + + 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); + 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, + async_spi_spi_transfer_end, async_spi_spi_transfer_cancel); + +//| 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 = locked_spi(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(busio_spi_write_readinto_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(busio_spi_write_readinto_args), busio_spi_write_readinto_args, args); + + 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")); + } + 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, + async_spi_spi_transfer_end, async_spi_spi_transfer_cancel); + +//| 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); + +//| lock: Lock +//| """The bus's lock. Hold it around a transaction:: +//| +//| async with spi.lock: +//| spi.configure(baudrate=8_000_000) +//| await spi.write(command) +//| await spi.readinto(reply) +//| """ +//| +static mp_obj_t async_spi_spi_get_lock(mp_obj_t self_in) { + async_spi_spi_obj_t *self = checked_spi(self_in); + return MP_OBJ_FROM_PTR(&self->lock); +} +MP_DEFINE_CONST_FUN_OBJ_1(async_spi_spi_get_lock_obj, async_spi_spi_get_lock); + +MP_PROPERTY_GETTER(async_spi_spi_lock_obj, + (mp_obj_t)&async_spi_spi_get_lock_obj); + +static const mp_rom_map_elem_t async_spi_spi_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&async_spi_spi_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&async_spi_spi_deinit_obj) }, + { 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) }, + { MP_ROM_QSTR(MP_QSTR_lock), MP_ROM_PTR(&async_spi_spi_lock_obj) }, +}; +static MP_DEFINE_CONST_DICT(async_spi_spi_locals_dict, async_spi_spi_locals_dict_table); + +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..10ede825af8 --- /dev/null +++ b/shared-bindings/async_spi/SPI.h @@ -0,0 +1,51 @@ +// 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; + +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); +// For busio.SPI built on this implementation (shared-module/busio/SPI.c). +void common_hal_async_spi_spi_never_reset(async_spi_spi_obj_t *self); +void common_hal_async_spi_spi_mark_deinit(async_spi_spi_obj_t *self); +uint32_t common_hal_async_spi_spi_get_frequency(async_spi_spi_obj_t *self); + +// Each start returns a context for state of that one operation, which end() and cancel() get +// back; NULL when the object holds the state. + +// Waits for the running transfer, if any, then applies the settings. +void common_hal_async_spi_spi_configure(async_spi_spi_obj_t *self, uint32_t baudrate, + uint8_t polarity, uint8_t phase, uint8_t bits); + +// Each transfer start sets done once the transfer has finished, possibly from an interrupt. The +// buffers must stay valid until done is set. transfer_end() and transfer_cancel() act only on +// 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); +// Finishes the transfer started with done, once done is set; false if the driver reported an +// error. For busio.SPI, which must not raise from a background callback. +bool common_hal_async_spi_spi_transfer_finish(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done); +// The same for an await: raises on an error. +mp_obj_t common_hal_async_spi_spi_transfer_end(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done); +void common_hal_async_spi_spi_transfer_cancel(async_spi_spi_obj_t *self, void *context, + circuitpy_async_flag_t *done); diff --git a/shared-bindings/async_spi/__init__.c b/shared-bindings/async_spi/__init__.c new file mode 100644 index 00000000000..7580025c05b --- /dev/null +++ b/shared-bindings/async_spi/__init__.c @@ -0,0 +1,60 @@ +// 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" +#include "supervisor/shared/awaitable.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) +//| data = bytearray(512) +//| async with spi.lock: +//| spi.configure(baudrate=8_000_000) +//| await spi.readinto(data) +//| +//| asyncio.run(main()) +//| """ +//| +//| class Lock: +//| """A bus lock for asyncio code, used as ``async with bus.lock:``. Tasks waiting for it +//| get it in the order they asked; a task cancelled while waiting leaves the line.""" +//| +//| async def __aenter__(self) -> Lock: +//| """Waits for the lock and takes it.""" +//| ... +//| +//| def __aexit__(self) -> None: +//| """Hands the lock to the next task waiting, or releases it.""" +//| ... +//| + +static const mp_rom_map_elem_t async_spi_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_async_spi) }, + { MP_ROM_QSTR(MP_QSTR_SPI), MP_ROM_PTR(&async_spi_spi_type) }, + { MP_ROM_QSTR(MP_QSTR_Lock), MP_ROM_PTR(&circuitpy_async_lock_type) }, +}; + +static MP_DEFINE_CONST_DICT(async_spi_module_globals, async_spi_module_globals_table); + +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/shared-bindings/busio/SPI.c b/shared-bindings/busio/SPI.c index 6ff74880266..6411fdaa627 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" @@ -85,18 +84,22 @@ //| +#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 }; - 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); @@ -175,19 +178,20 @@ static void check_for_deinit(busio_spi_obj_t *self) { //| ... //| +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} }, +}; + 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); @@ -245,36 +249,28 @@ MP_DEFINE_CONST_FUN_OBJ_1(busio_spi_unlock_obj, busio_spi_obj_unlock); //| ... //| +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} }, +}; + 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_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; + 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, + 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); } @@ -311,37 +307,29 @@ MP_DEFINE_CONST_FUN_OBJ_KW(busio_spi_write_obj, 1, busio_spi_write); //| ... //| +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} }, +}; + 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_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; + 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, + 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); } @@ -386,41 +374,28 @@ MP_DEFINE_CONST_FUN_OBJ_KW(busio_spi_readinto_obj, 1, busio_spi_readinto); //| ... //| +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} }, +}; + 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_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; + 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, + 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 +405,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-bindings/busio/SPI.h b/shared-bindings/busio/SPI.h index 382c91240b2..515cddbb235 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" @@ -15,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, diff --git a/shared-module/busio/SPI.c b/shared-module/busio/SPI.c new file mode 100644 index 00000000000..2db2fd7885e --- /dev/null +++ b/shared-module/busio/SPI.c @@ -0,0 +1,143 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/busio/SPI.h" + +#include "py/runtime.h" +#include "supervisor/background_callback.h" +#include "shared/runtime/interrupt_char.h" + +// False when interrupted (Ctrl-C); the operation is then still running. +static bool wait(circuitpy_async_flag_t *done) { + while (!CIRCUITPY_ASYNC_FLAG_IS_SET(done)) { + RUN_BACKGROUND_TASKS; + if (mp_hal_is_interrupted()) { + return false; + } + } + return true; +} + +#if CIRCUITPY_BUSIO_SPI_ASYNC +// Finish a write_start(). No background tasks: the caller holds the bus inside a transaction, +// and one of them may want it. +static void end_sent(busio_spi_obj_t *self) { + if (self->sent == NULL) { + return; + } + while (!CIRCUITPY_ASYNC_FLAG_IS_SET(self->sent)) { + } + common_hal_async_spi_spi_transfer_finish(&self->async_spi, self->sent_context, self->sent); + self->sent = NULL; +} +#endif + +void common_hal_busio_spi_construct(busio_spi_obj_t *self, const mcu_pin_obj_t *clock, + const mcu_pin_obj_t *mosi, const mcu_pin_obj_t *miso, bool half_duplex) { + if (half_duplex) { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_half_duplex); + } + common_hal_async_spi_spi_construct(&self->async_spi, clock, mosi, miso); + self->sent = NULL; + self->has_lock = false; + self->polarity = 0; + self->phase = 0; +} + +#if CIRCUITPY_BULK_RESET +void common_hal_busio_spi_never_reset(busio_spi_obj_t *self) { + common_hal_async_spi_spi_never_reset(&self->async_spi); +} +#endif + +bool common_hal_busio_spi_deinited(busio_spi_obj_t *self) { + return common_hal_async_spi_spi_deinited(&self->async_spi); +} + +void common_hal_busio_spi_mark_deinit(busio_spi_obj_t *self) { + common_hal_async_spi_spi_mark_deinit(&self->async_spi); +} + +void common_hal_busio_spi_deinit(busio_spi_obj_t *self) { + common_hal_async_spi_spi_deinit(&self->async_spi); +} + +bool common_hal_busio_spi_configure(busio_spi_obj_t *self, uint32_t baudrate, uint8_t polarity, + uint8_t phase, uint8_t bits) { + #if CIRCUITPY_BUSIO_SPI_ASYNC + end_sent(self); + #endif + common_hal_async_spi_spi_configure(&self->async_spi, baudrate, polarity, phase, bits); + self->polarity = polarity; + self->phase = phase; + return true; +} + +bool common_hal_busio_spi_try_lock(busio_spi_obj_t *self) { + if (common_hal_busio_spi_deinited(self) || self->has_lock) { + return false; + } + self->has_lock = true; + return true; +} + +bool common_hal_busio_spi_has_lock(busio_spi_obj_t *self) { + return self->has_lock; +} + +void common_hal_busio_spi_unlock(busio_spi_obj_t *self) { + self->has_lock = false; +} + +// Finish the transfer started with self->done. +static bool finish(busio_spi_obj_t *self, void *context) { + if (!wait(&self->done)) { + common_hal_async_spi_spi_transfer_cancel(&self->async_spi, context, &self->done); + mp_handle_pending(true); // raises the KeyboardInterrupt + return false; + } + return common_hal_async_spi_spi_transfer_finish(&self->async_spi, context, &self->done); +} + +bool common_hal_busio_spi_write(busio_spi_obj_t *self, const uint8_t *data, size_t len) { + return finish(self, common_hal_async_spi_spi_write_start(&self->async_spi, data, len, &self->done)); +} + +bool common_hal_busio_spi_read(busio_spi_obj_t *self, uint8_t *data, size_t len, + uint8_t write_value) { + return finish(self, common_hal_async_spi_spi_readinto_start(&self->async_spi, data, len, write_value, + &self->done)); +} + +bool common_hal_busio_spi_transfer(busio_spi_obj_t *self, const uint8_t *data_out, + uint8_t *data_in, size_t len) { + return finish(self, common_hal_async_spi_spi_write_readinto_start(&self->async_spi, data_out, data_in, + len, &self->done)); +} + +#if CIRCUITPY_BUSIO_SPI_ASYNC +void common_hal_busio_spi_write_start(busio_spi_obj_t *self, const uint8_t *data, size_t len, + circuitpy_async_flag_t *done) { + self->sent_context = common_hal_async_spi_spi_write_start(&self->async_spi, data, len, done); + self->sent = done; +} + +void common_hal_busio_spi_end(busio_spi_obj_t *self) { + end_sent(self); +} +#endif + +uint32_t common_hal_busio_spi_get_frequency(busio_spi_obj_t *self) { + return common_hal_async_spi_spi_get_frequency(&self->async_spi); +} + +uint8_t common_hal_busio_spi_get_phase(busio_spi_obj_t *self) { + return self->phase; +} + +uint8_t common_hal_busio_spi_get_polarity(busio_spi_obj_t *self) { + return self->polarity; +} diff --git a/shared-module/busio/SPI.h b/shared-module/busio/SPI.h new file mode 100644 index 00000000000..2a463c4164d --- /dev/null +++ b/shared-module/busio/SPI.h @@ -0,0 +1,24 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +// busio.SPI built on a port's async_spi implementation: every blocking call starts an async +// transfer and waits for it. A port that has async_spi includes this header from its +// common-hal/busio/SPI.h and sets CIRCUITPY_ASYNC_SPI = 1 in its mpconfigport.mk. + +#pragma once + +#include "shared-bindings/async_spi/SPI.h" + +typedef struct { + mp_obj_base_t base; + async_spi_spi_obj_t async_spi; + circuitpy_async_flag_t done; // of the blocking transfer in progress + circuitpy_async_flag_t *sent; // of a write_start() not yet ended, NULL otherwise + void *sent_context; + bool has_lock; + uint8_t polarity; + uint8_t phase; +} busio_spi_obj_t; 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); diff --git a/supervisor/shared/awaitable.c b/supervisor/shared/awaitable.c new file mode 100644 index 00000000000..1a20687f8df --- /dev/null +++ b/supervisor/shared/awaitable.c @@ -0,0 +1,250 @@ +// 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; + aw->next = NULL; + 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 ----------------------------------------------------------- + +// Finishes the awaitable; stops the operation if it has started. A closed awaitable that +// was never awaited does not start later, as with a closed coroutine. +static void awaitable_cancel(circuitpy_awaitable_obj_t *self) { + uint8_t state = self->state; + self->state = CIRCUITPY_AWAITABLE_FINISHED; + if (state == CIRCUITPY_AWAITABLE_RUNNING) { + self->fun->cancel(self->args[0], self->context, &self->done); + } +} + +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->args[0], 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 + ); + +// ---- the lock ---------------------------------------------------------------- + +void circuitpy_async_lock_init(circuitpy_async_lock_obj_t *lock) { + lock->base.type = &circuitpy_async_lock_type; + lock->held = false; + lock->waiting = NULL; +} + +bool circuitpy_async_lock_try(circuitpy_async_lock_obj_t *lock) { + if (lock->held) { + return false; + } + lock->held = true; + return true; +} + +void circuitpy_async_lock_release(circuitpy_async_lock_obj_t *lock) { + circuitpy_awaitable_obj_t *next = lock->waiting; + if (next == NULL) { + lock->held = false; + return; + } + // The lock stays held; it is the next one's now. + lock->waiting = next->next; + CIRCUITPY_ASYNC_FLAG_SET(&next->done); +} + +// __aenter__: take the lock, or join the end of the line. done is set when it is ours. +static void *lock_enter_start(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + circuitpy_async_lock_obj_t *lock = MP_OBJ_TO_PTR(pos_args[0]); + if (circuitpy_async_lock_try(lock)) { + CIRCUITPY_ASYNC_FLAG_SET(done); + return NULL; + } + circuitpy_awaitable_obj_t *aw = CIRCUITPY_AWAITABLE_OF(done); + circuitpy_awaitable_obj_t **p = &lock->waiting; + while (*p != NULL) { + p = &(*p)->next; + } + *p = aw; + return NULL; +} + +static mp_obj_t lock_enter_end(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { + return self_in; +} + +// Cancelled while waiting: leave the line. Cancelled after the lock was handed over but before +// the task ran: pass it on. +static void lock_enter_cancel(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) { + circuitpy_async_lock_obj_t *lock = MP_OBJ_TO_PTR(self_in); + if (CIRCUITPY_ASYNC_FLAG_IS_SET(done)) { + circuitpy_async_lock_release(lock); + return; + } + circuitpy_awaitable_obj_t *aw = CIRCUITPY_AWAITABLE_OF(done); + for (circuitpy_awaitable_obj_t **p = &lock->waiting; *p != NULL; p = &(*p)->next) { + if (*p == aw) { + *p = aw->next; + return; + } + } +} +static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(lock_enter_obj, 1, lock_enter_start, lock_enter_end, lock_enter_cancel); + +// __aexit__ returns the lock itself: awaiting it finishes at once with None, so an exception +// in the block is not swallowed. +static mp_obj_t lock_exit(size_t n_args, const mp_obj_t *args) { + circuitpy_async_lock_release(MP_OBJ_TO_PTR(args[0])); + return args[0]; +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(lock_exit_obj, 4, 4, lock_exit); + +static mp_obj_t lock_iternext(mp_obj_t self_in) { + MP_STATE_THREAD(stop_iteration_arg) = mp_const_none; + return MP_OBJ_STOP_ITERATION; +} + +static const mp_rom_map_elem_t lock_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR___aenter__), MP_ROM_PTR(&lock_enter_obj) }, + { MP_ROM_QSTR(MP_QSTR___aexit__), MP_ROM_PTR(&lock_exit_obj) }, +}; +static MP_DEFINE_CONST_DICT(lock_locals_dict, lock_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + circuitpy_async_lock_type, + MP_QSTR_Lock, + MP_TYPE_FLAG_ITER_IS_ITERNEXT, + iter, lock_iternext, + locals_dict, &lock_locals_dict + ); + +#endif // MICROPY_PY_ASYNC_AWAIT diff --git a/supervisor/shared/awaitable.h b/supervisor/shared/awaitable.h new file mode 100644 index 00000000000..6a035a56a07 --- /dev/null +++ b/supervisor/shared/awaitable.h @@ -0,0 +1,104 @@ +// 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, start, end, cancel) defines obj_name, +// a callable that takes the same arguments as an MP_DEFINE_CONST_FUN_OBJ_KW function but returns +// an awaitable. A call only checks the number of positional arguments; as with a coroutine in +// CPython, nothing runs and the arguments are parsed only when the awaitable is awaited. Then: +// +// void *start(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, +// mp_map_t *kw_args) +// parses the arguments, starts the operation and returns a context: state of this one +// operation, or NULL when the object holds it all. The operation sets *done, possibly +// from an interrupt, once it has finished. +// mp_obj_t end(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) +// finishes the operation once *done is set and returns the result of the await. +// void cancel(mp_obj_t self_in, void *context, circuitpy_async_flag_t *done) +// stops the operation, or finishes it if *done is already set. It must not allocate: it +// may run during GC. +// +// self_in is the first positional argument, the object for a method. done tells which operation +// is meant, if the object has started another one since. +// +// While an operation runs, the awaiting asyncio task waits on asyncio's I/O queue: the awaitable +// is a stream that polls readable once *done is set. + +#pragma once + +#include + +#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)(mp_obj_t self_in, void *context, + circuitpy_async_flag_t *done); +typedef void (*circuitpy_awaitable_cancel_fn)(mp_obj_t self_in, void *context, + circuitpy_async_flag_t *done); + +// An async function: calling it returns a circuitpy_awaitable_obj_t. +typedef struct { + 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 _circuitpy_awaitable_obj_t { + 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; + struct _circuitpy_awaitable_obj_t *next; // the next one waiting for the same lock + mp_obj_t args[]; // positional arguments, then keyword name/value pairs +} circuitpy_awaitable_obj_t; + +extern const mp_obj_type_t circuitpy_awaitable_type; + +// The awaitable that owns a done flag passed to start(), end() or cancel(). +#define CIRCUITPY_AWAITABLE_OF(done_ptr) \ + ((circuitpy_awaitable_obj_t *)((char *)(done_ptr) - offsetof(circuitpy_awaitable_obj_t, done))) + +// A lock for asyncio code: `async with lock:`. __aenter__ is an awaitable that takes the lock +// or waits in line for it; __aexit__ hands the lock to the next in line. Embed it in the +// object it guards; it allocates nothing. +typedef struct { + mp_obj_base_t base; + bool held; + circuitpy_awaitable_obj_t *waiting; // first in line, linked through next +} circuitpy_async_lock_obj_t; + +extern const mp_obj_type_t circuitpy_async_lock_type; + +void circuitpy_async_lock_init(circuitpy_async_lock_obj_t *lock); +static inline bool circuitpy_async_lock_held(circuitpy_async_lock_obj_t *lock) { + return lock->held; +} +// For C code: takes the lock if it is free. +bool circuitpy_async_lock_try(circuitpy_async_lock_obj_t *lock); +// Hands the lock to the next in line, or frees it. +void circuitpy_async_lock_release(circuitpy_async_lock_obj_t *lock); + +enum { CIRCUITPY_AWAITABLE_NEW, CIRCUITPY_AWAITABLE_RUNNING, CIRCUITPY_AWAITABLE_FINISHED }; + +#define CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj_name, n_args_min, start, end, cancel) \ + 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), \ + start, end, 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 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())