Skip to content

Commit 12842f7

Browse files
lynt-smitkakamocat
andcommitted
async_spi: SPI transfers for asyncio
async_spi.SPI is configured and locked like busio.SPI, and lock(), write(), readinto() and write_readinto() are awaited: other tasks run during a transfer. Enabled on raspberrypi. Manual tests are in tests/circuitpython-manual/async_spi. Co-authored-by: Marshal Horn <175229+kamocat@users.noreply.github.com>
1 parent 4ba8f02 commit 12842f7

11 files changed

Lines changed: 476 additions & 2 deletions

File tree

‎ports/raspberrypi/mpconfigport.mk‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CIRCUITPY_WIFI_RADIO_SETTABLE_MAC_ADDRESS = 0
77

88
CIRCUITPY_RP2PIO ?= 1
99
CIRCUITPY_BUSIO_SPI_ASYNC ?= 1
10+
CIRCUITPY_ASYNC_SPI ?= $(call enable-if-all,$(MICROPY_PY_ASYNC_AWAIT) $(CIRCUITPY_BUSIO_SPI) $(CIRCUITPY_BUSIO_SPI_ASYNC))
1011
# The 24 KB stack has room for a larger display refresh buffer.
1112
CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048
1213
CIRCUITPY_NEOPIXEL_WRITE ?= $(CIRCUITPY_RP2PIO)

‎py/circuitpy_defns.mk‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ endif
116116
ifeq ($(CIRCUITPY_ANALOGIO),1)
117117
SRC_PATTERNS += analogio/%
118118
endif
119+
ifeq ($(CIRCUITPY_ASYNC_SPI),1)
120+
SRC_PATTERNS += async_spi/%
121+
endif
119122
ifeq ($(CIRCUITPY_ATEXIT),1)
120123
SRC_PATTERNS += atexit/%
121124
endif
@@ -735,6 +738,8 @@ SRC_SHARED_MODULE_ALL = \
735738
picogame/Canvas.c \
736739
aesio/__init__.c \
737740
aesio/aes.c \
741+
async_spi/SPI.c \
742+
async_spi/__init__.c \
738743
atexit/__init__.c \
739744
audiocore/RawSample.c \
740745
audiocore/WaveFile.c \

‎shared-bindings/async_spi/SPI.c‎

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#include <stdint.h>
8+
9+
#include "supervisor/shared/awaitable.h"
10+
#include "py/binary.h"
11+
#include "py/objproperty.h"
12+
#include "py/runtime.h"
13+
14+
#include "shared/runtime/buffer_helper.h"
15+
#include "shared/runtime/context_manager_helpers.h"
16+
#include "shared-bindings/async_spi/SPI.h"
17+
#include "shared-bindings/busio/SPI.h"
18+
#include "shared-bindings/util.h"
19+
20+
//| class SPI:
21+
//| """An SPI bus whose transfers are awaited
22+
//|
23+
//| Configuration and locking work as in `busio.SPI`. ``write``, ``readinto`` and
24+
//| ``write_readinto`` return awaitables: the transfer runs while other asyncio tasks run.
25+
//| Short transfers, and transfers that cannot use DMA (for example from a buffer in flash),
26+
//| block other tasks until they finish. A transfer's arguments are checked when the result is
27+
//| awaited."""
28+
//|
29+
//| def __init__(
30+
//| self,
31+
//| clock: microcontroller.Pin,
32+
//| MOSI: Optional[microcontroller.Pin] = None,
33+
//| MISO: Optional[microcontroller.Pin] = None,
34+
//| ) -> None:
35+
//| """Construct an SPI object on the given pins.
36+
//|
37+
//| :param ~microcontroller.Pin clock: the pin to use for the clock.
38+
//| :param ~microcontroller.Pin MOSI: the Main Out Selected In pin.
39+
//| :param ~microcontroller.Pin MISO: the Main In Selected Out pin."""
40+
//| ...
41+
//|
42+
// Provided by busio.SPI.
43+
44+
static async_spi_spi_obj_t *native_spi(mp_obj_t self_in) {
45+
return MP_OBJ_TO_PTR(mp_arg_validate_type(self_in, &async_spi_spi_type, MP_QSTR_self));
46+
}
47+
48+
static async_spi_spi_obj_t *locked_spi(mp_obj_t self_in) {
49+
async_spi_spi_obj_t *self = native_spi(self_in);
50+
if (common_hal_busio_spi_deinited(&self->spi)) {
51+
raise_deinited_error();
52+
}
53+
if (!common_hal_busio_spi_has_lock(&self->spi)) {
54+
mp_raise_RuntimeError(MP_ERROR_TEXT("Function requires lock"));
55+
}
56+
return self;
57+
}
58+
59+
// buffer[start:end], with start and end in elements as in busio.SPI. Returns the slice's address
60+
// and sets *len to its length in bytes.
61+
static uint8_t *buffer_slice(mp_obj_t buffer, int32_t start, int32_t end, mp_uint_t flags, size_t *len) {
62+
mp_buffer_info_t bufinfo;
63+
mp_get_buffer_raise(buffer, &bufinfo, flags);
64+
int stride_in_bytes = mp_binary_get_size('@', bufinfo.typecode, NULL);
65+
size_t length = bufinfo.len / stride_in_bytes;
66+
normalize_buffer_bounds(&start, end, &length);
67+
*len = length * stride_in_bytes;
68+
return (uint8_t *)bufinfo.buf + start * stride_in_bytes;
69+
}
70+
71+
//| def deinit(self) -> None:
72+
//| """Turn off the SPI bus. A transfer still running is finished first."""
73+
//| ...
74+
//|
75+
// Provided by busio.SPI.
76+
77+
//| def __enter__(self) -> SPI:
78+
//| """No-op used by Context Managers."""
79+
//| ...
80+
//|
81+
// Provided by context manager helper.
82+
83+
//| def __exit__(self) -> None:
84+
//| """Automatically deinitializes the hardware when exiting a context. See
85+
//| :ref:`lifetime-and-contextmanagers` for more info."""
86+
//| ...
87+
//|
88+
// Provided by context manager helper.
89+
90+
//| def configure(
91+
//| self, *, baudrate: int = 100000, polarity: int = 0, phase: int = 0, bits: int = 8
92+
//| ) -> None:
93+
//| """Configure the SPI bus, as `busio.SPI.configure`. The bus must be locked."""
94+
//| ...
95+
//|
96+
// Provided by busio.SPI.
97+
98+
//| def try_lock(self) -> bool:
99+
//| """Attempts to grab the SPI lock. Returns True on success."""
100+
//| ...
101+
//|
102+
// Provided by busio.SPI.
103+
104+
//| async def lock(self) -> None:
105+
//| """Waits until the SPI lock is free and takes it. Other tasks run while it waits."""
106+
//| ...
107+
//|
108+
static void *async_spi_spi_lock(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
109+
return native_spi(pos_args[0]);
110+
}
111+
static CIRCUITPY_DEFINE_ASYNC_POLL_FUN_OBJ_1(async_spi_spi_lock_obj, async_spi_spi_lock);
112+
113+
//| def unlock(self) -> None:
114+
//| """Releases the SPI lock."""
115+
//| ...
116+
//|
117+
// Provided by busio.SPI.
118+
119+
//| import sys
120+
//|
121+
//| async def write(
122+
//| self, buffer: ReadableBuffer, *, start: int = 0, end: int = sys.maxsize
123+
//| ) -> None:
124+
//| """Write the data in ``buffer[start:end]``. The bus must be locked.
125+
//|
126+
//| :param ~circuitpython_typing.ReadableBuffer buffer: write out the data in this buffer
127+
//| :param int start: beginning of buffer slice
128+
//| :param int end: end of buffer slice; if not specified, use ``len(buffer)``"""
129+
//| ...
130+
//|
131+
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) {
132+
enum { ARG_buffer, ARG_start, ARG_end };
133+
static const mp_arg_t allowed_args[] = {
134+
{ MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ },
135+
{ MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
136+
{ MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} },
137+
};
138+
async_spi_spi_obj_t *self = locked_spi(pos_args[0]);
139+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
140+
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
141+
142+
size_t len;
143+
uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int,
144+
MP_BUFFER_READ, &len);
145+
common_hal_busio_spi_write_start(&self->spi, data, len, done);
146+
return self;
147+
}
148+
static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_obj, 1, async_spi_spi_write);
149+
150+
//| async def readinto(
151+
//| self,
152+
//| buffer: WriteableBuffer,
153+
//| *,
154+
//| start: int = 0,
155+
//| end: int = sys.maxsize,
156+
//| write_value: int = 0,
157+
//| ) -> None:
158+
//| """Read into ``buffer[start:end]`` while writing ``write_value`` for each byte read.
159+
//| The bus must be locked.
160+
//|
161+
//| :param ~circuitpython_typing.WriteableBuffer buffer: read data into this buffer
162+
//| :param int start: beginning of buffer slice
163+
//| :param int end: end of buffer slice; if not specified, use ``len(buffer)``
164+
//| :param int write_value: value to write while reading"""
165+
//| ...
166+
//|
167+
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) {
168+
enum { ARG_buffer, ARG_start, ARG_end, ARG_write_value };
169+
static const mp_arg_t allowed_args[] = {
170+
{ MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ },
171+
{ MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
172+
{ MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} },
173+
{ MP_QSTR_write_value, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
174+
};
175+
async_spi_spi_obj_t *self = locked_spi(pos_args[0]);
176+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
177+
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
178+
179+
size_t len;
180+
uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int,
181+
MP_BUFFER_WRITE, &len);
182+
common_hal_busio_spi_read_start(&self->spi, data, len, (uint8_t)args[ARG_write_value].u_int, done);
183+
return self;
184+
}
185+
static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_readinto_obj, 1, async_spi_spi_readinto);
186+
187+
//| async def write_readinto(
188+
//| self,
189+
//| out_buffer: ReadableBuffer,
190+
//| in_buffer: WriteableBuffer,
191+
//| *,
192+
//| out_start: int = 0,
193+
//| out_end: int = sys.maxsize,
194+
//| in_start: int = 0,
195+
//| in_end: int = sys.maxsize,
196+
//| ) -> None:
197+
//| """Write out the data in ``out_buffer`` while simultaneously reading data into ``in_buffer``.
198+
//| The two slices must have the same length. The bus must be locked.
199+
//|
200+
//| :param ~circuitpython_typing.ReadableBuffer out_buffer: write out the data in this buffer
201+
//| :param ~circuitpython_typing.WriteableBuffer in_buffer: read data into this buffer
202+
//| :param int out_start: beginning of ``out_buffer`` slice
203+
//| :param int out_end: end of ``out_buffer`` slice; if not specified, use ``len(out_buffer)``
204+
//| :param int in_start: beginning of ``in_buffer`` slice
205+
//| :param int in_end: end of ``in_buffer`` slice; if not specified, use ``len(in_buffer)``"""
206+
//| ...
207+
//|
208+
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) {
209+
enum { ARG_out_buffer, ARG_in_buffer, ARG_out_start, ARG_out_end, ARG_in_start, ARG_in_end };
210+
static const mp_arg_t allowed_args[] = {
211+
{ MP_QSTR_out_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ },
212+
{ MP_QSTR_in_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ },
213+
{ MP_QSTR_out_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
214+
{ MP_QSTR_out_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} },
215+
{ MP_QSTR_in_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
216+
{ MP_QSTR_in_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} },
217+
};
218+
async_spi_spi_obj_t *self = locked_spi(pos_args[0]);
219+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
220+
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
221+
222+
size_t out_len, in_len;
223+
const uint8_t *data_out = buffer_slice(args[ARG_out_buffer].u_obj, args[ARG_out_start].u_int,
224+
args[ARG_out_end].u_int, MP_BUFFER_READ, &out_len);
225+
uint8_t *data_in = buffer_slice(args[ARG_in_buffer].u_obj, args[ARG_in_start].u_int,
226+
args[ARG_in_end].u_int, MP_BUFFER_WRITE, &in_len);
227+
if (out_len != in_len) {
228+
mp_raise_ValueError(MP_ERROR_TEXT("buffer slices must be of equal length"));
229+
}
230+
common_hal_busio_spi_transfer_start(&self->spi, data_out, data_in, out_len, done);
231+
return self;
232+
}
233+
static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_readinto_obj, 2, async_spi_spi_write_readinto);
234+
235+
//| frequency: int
236+
//| """The actual SPI bus frequency. This may not match the frequency requested
237+
//| due to internal limitations."""
238+
//|
239+
//|
240+
MP_PROPERTY_GETTER(async_spi_spi_frequency_obj,
241+
(mp_obj_t)&busio_spi_get_frequency_obj);
242+
243+
static const mp_rom_map_elem_t async_spi_spi_locals_dict_table[] = {
244+
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&busio_spi_deinit_obj) },
245+
{ MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&busio_spi_deinit_obj) },
246+
{ MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
247+
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) },
248+
249+
{ MP_ROM_QSTR(MP_QSTR_configure), MP_ROM_PTR(&busio_spi_configure_obj) },
250+
{ MP_ROM_QSTR(MP_QSTR_try_lock), MP_ROM_PTR(&busio_spi_try_lock_obj) },
251+
{ MP_ROM_QSTR(MP_QSTR_lock), MP_ROM_PTR(&async_spi_spi_lock_obj) },
252+
{ MP_ROM_QSTR(MP_QSTR_unlock), MP_ROM_PTR(&busio_spi_unlock_obj) },
253+
254+
{ MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&async_spi_spi_write_obj) },
255+
{ MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&async_spi_spi_readinto_obj) },
256+
{ MP_ROM_QSTR(MP_QSTR_write_readinto), MP_ROM_PTR(&async_spi_spi_write_readinto_obj) },
257+
258+
{ MP_ROM_QSTR(MP_QSTR_frequency), MP_ROM_PTR(&async_spi_spi_frequency_obj) },
259+
};
260+
static MP_DEFINE_CONST_DICT(async_spi_spi_locals_dict, async_spi_spi_locals_dict_table);
261+
262+
MP_DEFINE_CONST_OBJ_TYPE(
263+
async_spi_spi_type,
264+
MP_QSTR_SPI,
265+
MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS,
266+
make_new, busio_spi_make_new,
267+
locals_dict, &async_spi_spi_locals_dict
268+
);

‎shared-bindings/async_spi/SPI.h‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#pragma once
8+
9+
#include "py/obj.h"
10+
#include "supervisor/shared/async_flag.h"
11+
#include "shared-module/async_spi/SPI.h"
12+
13+
extern const mp_obj_type_t async_spi_spi_type;
14+
15+
// The transfers are busio's async ones, with the SPI object as the context. transfer_end() and
16+
// transfer_cancel() act only on the transfer started with done.
17+
mp_obj_t common_hal_async_spi_spi_transfer_end(void *context, circuitpy_async_flag_t *done);
18+
void common_hal_async_spi_spi_transfer_cancel(void *context, circuitpy_async_flag_t *done);
19+
// lock(): the context is the SPI object; poll tries to take the lock.
20+
mp_obj_t common_hal_async_spi_spi_lock_end(void *context, circuitpy_async_flag_t *done);
21+
void common_hal_async_spi_spi_lock_cancel(void *context, circuitpy_async_flag_t *done);
22+
bool common_hal_async_spi_spi_lock_poll(void *context);
23+
24+
// The names CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW looks up; all three transfers end the same way.
25+
#define common_hal_async_spi_spi_write_end common_hal_async_spi_spi_transfer_end
26+
#define common_hal_async_spi_spi_write_cancel common_hal_async_spi_spi_transfer_cancel
27+
#define common_hal_async_spi_spi_readinto_end common_hal_async_spi_spi_transfer_end
28+
#define common_hal_async_spi_spi_readinto_cancel common_hal_async_spi_spi_transfer_cancel
29+
#define common_hal_async_spi_spi_write_readinto_end common_hal_async_spi_spi_transfer_end
30+
#define common_hal_async_spi_spi_write_readinto_cancel common_hal_async_spi_spi_transfer_cancel
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#include "py/obj.h"
8+
#include "py/runtime.h"
9+
10+
#include "shared-bindings/async_spi/SPI.h"
11+
12+
//| """SPI transfers for asyncio
13+
//|
14+
//| `async_spi.SPI` is an SPI bus whose transfers are awaited. While a transfer runs, other
15+
//| asyncio tasks run too.
16+
//|
17+
//| .. code-block:: python
18+
//|
19+
//| import asyncio
20+
//| import board
21+
//| import async_spi
22+
//|
23+
//| async def main():
24+
//| spi = async_spi.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
25+
//| await spi.lock()
26+
//| spi.configure(baudrate=8_000_000)
27+
//| data = bytearray(512)
28+
//| await spi.readinto(data)
29+
//| spi.unlock()
30+
//|
31+
//| asyncio.run(main())
32+
//| """
33+
34+
static const mp_rom_map_elem_t async_spi_module_globals_table[] = {
35+
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_async_spi) },
36+
{ MP_ROM_QSTR(MP_QSTR_SPI), MP_ROM_PTR(&async_spi_spi_type) },
37+
};
38+
39+
static MP_DEFINE_CONST_DICT(async_spi_module_globals, async_spi_module_globals_table);
40+
41+
const mp_obj_module_t async_spi_module = {
42+
.base = { &mp_type_module },
43+
.globals = (mp_obj_dict_t *)&async_spi_module_globals,
44+
};
45+
46+
MP_REGISTER_MODULE(MP_QSTR_async_spi, async_spi_module);

‎shared-bindings/busio/SPI.c‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686

8787

8888
// TODO(tannewt): Support LSB SPI.
89-
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) {
89+
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) {
9090
#if CIRCUITPY_BUSIO_SPI
9191
enum { ARG_clock, ARG_MOSI, ARG_MISO, ARG_half_duplex };
9292
static const mp_arg_t allowed_args[] = {
@@ -106,7 +106,7 @@ static mp_obj_t busio_spi_make_new(const mp_obj_type_t *type, size_t n_args, siz
106106
mp_raise_ValueError(MP_ERROR_TEXT("Must provide MISO or MOSI pin"));
107107
}
108108

109-
busio_spi_obj_t *self = mp_obj_malloc_with_finaliser(busio_spi_obj_t, &busio_spi_type);
109+
busio_spi_obj_t *self = mp_obj_malloc_with_finaliser(busio_spi_obj_t, type);
110110
common_hal_busio_spi_construct(self, clock, mosi, miso, args[ARG_half_duplex].u_bool);
111111
return MP_OBJ_FROM_PTR(self);
112112
#else

0 commit comments

Comments
 (0)