Repository navigation
async_spi: SPI transfers for asyncio - #11489
lynt-smitka wants to merge 9 commits into
Conversation
tannewt
left a comment
There was a problem hiding this comment.
Thanks for picking this up! I'd prefer it to be more like a new module than changing the implementation in busio.SPI().
|
|
||
| #define NO_INSTANCE 0xff | ||
|
|
||
| #if CIRCUITPY_ASYNC_SPI |
There was a problem hiding this comment.
I'd prefer an new common-hal subfolder for this. If this implementation wants to use the async version under the hood that's ok.
| #include "py/misc.h" | ||
| #include "common-hal/busio/SPI.h" | ||
|
|
||
| // busio_spi_obj_t first, so that busio.SPI's methods work on these objects too. busio.SPI's |
There was a problem hiding this comment.
I don't expect it to work like a busio.SPI. The async version should only be async.
There was a problem hiding this comment.
It'd be cool to test this on zephyr's native_sim.
|
I would like to clarify why the PR looks the way it does:
What I'm working on:
Questions before I push
|
This is what I was thinking. Build busio on top of async under the hood.
The constructor can be shared by building on the async portion. I don't think we want busio's locking. Instead, we likely want something async friendly. (Like an async lock)
Yup, that's fine. Code in peripherals shouldn't have any python specific code though.
The reverse but yes. Have it in async and reuse it in busio.
Yes, but I image they too should be async then. |
buffer_slice() turns a buffer object and element bounds into a byte slice, as busio.SPI's write(), readinto() and write_readinto() did inline. busio.SPI gets a little smaller, and async_spi will use it too.
busio.SPI's port code becomes rp2_spi_t and rp2_spi_*() in peripherals/spi.c, with common-hal/busio/SPI.c as a thin layer over it, so that another module can drive an SPI bus the same way. No change in behaviour.
rp2_spi_read_start() and rp2_spi_transfer_start() start the other two transfers the way rp2_spi_write_start() starts a write. rp2_spi_stop() stops a transfer, or finishes it if it is already done; it does nothing once the bus has started a later transfer. rp2_spi_configure_start() applies new settings once the running transfer has finished, and rp2_spi_deinit() stops a running transfer. With CIRCUITPY_ASYNC_SPI, a DMA_IRQ_0 handler, shared like the audio and rp2pio ones, sets the done flag when the RX channel finishes, so a waiter does not need to call rp2_spi_end() to find out.
CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj, n_args_min, fun) defines a method that returns an awaitable. As with a CPython coroutine, its arguments are stored and only parsed when it is awaited. While an operation runs, the task waits on asyncio's I/O queue: the awaitable is a stream that polls readable once the operation's done flag is set, possibly from an interrupt. Based on the awaitable in adafruit#11040.
async_spi.SPI is configured like busio.SPI, and configure(), write(), readinto() and write_readinto() are awaited: other tasks run during a transfer. Tasks that share the bus can use an asyncio.Lock. Enabled on raspberrypi, where it drives the bus through peripherals/spi.c. A manual test is in tests/circuitpython-manual/async_spi. Co-authored-by: Marshal Horn <175229+kamocat@users.noreply.github.com>
busio.SPI takes the same arguments as async_spi.SPI, so where that module exists its tables are used. Saves 160 bytes on RP2040.
Transfers use spi_transceive_cb() where the driver has an async path, and otherwise run on the system work queue. Zephyr cannot stop a transfer, so cancel waits for it. Enabled on boards with an SPI bus; tested on native_sim through the loopback device.
The driver's result reaches the await as an exception. cancel() and deinit() wait for the transfer without running background tasks, since they may run from a finalizer during GC. async_spi needs asyncio, not only async/await, so raspberrypi enables it with MICROPY_PY_ASYNCIO.
…O_SPI Unused, so a build error, on boards without busio.SPI.
Adds the async_spi module, based on @kamocat's work in #11040. Part of #10856.
async_spi.SPIis configured and locked likebusio.SPI, butwrite(),readinto()andwrite_readinto()are awaited, so other asyncio tasks run during a transfer. Cancelling a task stops its transfer. Tasks that share the bus can use anasyncio.Lock.supervisor/shared/awaitable.clets C functions return awaitables, so other async modules (I2C, UART) can use it later.Tested on a Pico W with MOSI jumpered to MISO: data reads back correctly at up to 62.5 MHz, other tasks keep running during transfers, and cancelling transfers works while audio plays.