Skip to content

Commit 5e39446

Browse files
lynt-smitkakamocat
andcommitted
async_spi: SPI transfers for asyncio
async_spi.SPI is configured and locked like busio.SPI, and 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. A manual test is in tests/circuitpython-manual/async_spi. Co-authored-by: Marshal Horn <175229+kamocat@users.noreply.github.com>
1 parent 57ba20d commit 5e39446

11 files changed

Lines changed: 441 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: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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. To share the bus between tasks, hold an `asyncio.Lock` around each use."""
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 *locked_spi(mp_obj_t self_in) {
45+
async_spi_spi_obj_t *self = MP_OBJ_TO_PTR(mp_arg_validate_type(self_in, &async_spi_spi_type, MP_QSTR_self));
46+
if (common_hal_busio_spi_deinited(&self->spi)) {
47+
raise_deinited_error();
48+
}
49+
if (!common_hal_busio_spi_has_lock(&self->spi)) {
50+
mp_raise_RuntimeError(MP_ERROR_TEXT("Function requires lock"));
51+
}
52+
return self;
53+
}
54+
55+
// buffer[start:end], with start and end in elements as in busio.SPI. Returns the slice's address
56+
// and sets *len to its length in bytes.
57+
static uint8_t *buffer_slice(mp_obj_t buffer, int32_t start, int32_t end, mp_uint_t flags, size_t *len) {
58+
mp_buffer_info_t bufinfo;
59+
mp_get_buffer_raise(buffer, &bufinfo, flags);
60+
int stride_in_bytes = mp_binary_get_size('@', bufinfo.typecode, NULL);
61+
size_t length = bufinfo.len / stride_in_bytes;
62+
normalize_buffer_bounds(&start, end, &length);
63+
*len = length * stride_in_bytes;
64+
return (uint8_t *)bufinfo.buf + start * stride_in_bytes;
65+
}
66+
67+
//| def deinit(self) -> None:
68+
//| """Turn off the SPI bus. A transfer still running is finished first."""
69+
//| ...
70+
//|
71+
// Provided by busio.SPI.
72+
73+
//| def __enter__(self) -> SPI:
74+
//| """No-op used by Context Managers."""
75+
//| ...
76+
//|
77+
// Provided by context manager helper.
78+
79+
//| def __exit__(self) -> None:
80+
//| """Automatically deinitializes the hardware when exiting a context. See
81+
//| :ref:`lifetime-and-contextmanagers` for more info."""
82+
//| ...
83+
//|
84+
// Provided by context manager helper.
85+
86+
//| def configure(
87+
//| self, *, baudrate: int = 100000, polarity: int = 0, phase: int = 0, bits: int = 8
88+
//| ) -> None:
89+
//| """Configure the SPI bus, as `busio.SPI.configure`. The bus must be locked."""
90+
//| ...
91+
//|
92+
// Provided by busio.SPI.
93+
94+
//| def try_lock(self) -> bool:
95+
//| """Attempts to grab the SPI lock. Returns True on success."""
96+
//| ...
97+
//|
98+
// Provided by busio.SPI.
99+
100+
//| def unlock(self) -> None:
101+
//| """Releases the SPI lock."""
102+
//| ...
103+
//|
104+
// Provided by busio.SPI.
105+
106+
//| import sys
107+
//|
108+
//| async def write(
109+
//| self, buffer: ReadableBuffer, *, start: int = 0, end: int = sys.maxsize
110+
//| ) -> None:
111+
//| """Write the data in ``buffer[start:end]``. The bus must be locked.
112+
//|
113+
//| :param ~circuitpython_typing.ReadableBuffer buffer: write out the data in this buffer
114+
//| :param int start: beginning of buffer slice
115+
//| :param int end: end of buffer slice; if not specified, use ``len(buffer)``"""
116+
//| ...
117+
//|
118+
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) {
119+
enum { ARG_buffer, ARG_start, ARG_end };
120+
static const mp_arg_t allowed_args[] = {
121+
{ MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ },
122+
{ MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
123+
{ MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} },
124+
};
125+
async_spi_spi_obj_t *self = locked_spi(pos_args[0]);
126+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
127+
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
128+
129+
size_t len;
130+
uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int,
131+
MP_BUFFER_READ, &len);
132+
common_hal_busio_spi_write_start(&self->spi, data, len, done);
133+
return self;
134+
}
135+
static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_obj, 1, async_spi_spi_write);
136+
137+
//| async def readinto(
138+
//| self,
139+
//| buffer: WriteableBuffer,
140+
//| *,
141+
//| start: int = 0,
142+
//| end: int = sys.maxsize,
143+
//| write_value: int = 0,
144+
//| ) -> None:
145+
//| """Read into ``buffer[start:end]`` while writing ``write_value`` for each byte read.
146+
//| The bus must be locked.
147+
//|
148+
//| :param ~circuitpython_typing.WriteableBuffer buffer: read data into this buffer
149+
//| :param int start: beginning of buffer slice
150+
//| :param int end: end of buffer slice; if not specified, use ``len(buffer)``
151+
//| :param int write_value: value to write while reading"""
152+
//| ...
153+
//|
154+
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) {
155+
enum { ARG_buffer, ARG_start, ARG_end, ARG_write_value };
156+
static const mp_arg_t allowed_args[] = {
157+
{ MP_QSTR_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ },
158+
{ MP_QSTR_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
159+
{ MP_QSTR_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} },
160+
{ MP_QSTR_write_value, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
161+
};
162+
async_spi_spi_obj_t *self = locked_spi(pos_args[0]);
163+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
164+
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
165+
166+
size_t len;
167+
uint8_t *data = buffer_slice(args[ARG_buffer].u_obj, args[ARG_start].u_int, args[ARG_end].u_int,
168+
MP_BUFFER_WRITE, &len);
169+
common_hal_busio_spi_read_start(&self->spi, data, len, (uint8_t)args[ARG_write_value].u_int, done);
170+
return self;
171+
}
172+
static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_readinto_obj, 1, async_spi_spi_readinto);
173+
174+
//| async def write_readinto(
175+
//| self,
176+
//| out_buffer: ReadableBuffer,
177+
//| in_buffer: WriteableBuffer,
178+
//| *,
179+
//| out_start: int = 0,
180+
//| out_end: int = sys.maxsize,
181+
//| in_start: int = 0,
182+
//| in_end: int = sys.maxsize,
183+
//| ) -> None:
184+
//| """Write out the data in ``out_buffer`` while simultaneously reading data into ``in_buffer``.
185+
//| The two slices must have the same length. The bus must be locked.
186+
//|
187+
//| :param ~circuitpython_typing.ReadableBuffer out_buffer: write out the data in this buffer
188+
//| :param ~circuitpython_typing.WriteableBuffer in_buffer: read data into this buffer
189+
//| :param int out_start: beginning of ``out_buffer`` slice
190+
//| :param int out_end: end of ``out_buffer`` slice; if not specified, use ``len(out_buffer)``
191+
//| :param int in_start: beginning of ``in_buffer`` slice
192+
//| :param int in_end: end of ``in_buffer`` slice; if not specified, use ``len(in_buffer)``"""
193+
//| ...
194+
//|
195+
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) {
196+
enum { ARG_out_buffer, ARG_in_buffer, ARG_out_start, ARG_out_end, ARG_in_start, ARG_in_end };
197+
static const mp_arg_t allowed_args[] = {
198+
{ MP_QSTR_out_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ },
199+
{ MP_QSTR_in_buffer, MP_ARG_REQUIRED | MP_ARG_OBJ },
200+
{ MP_QSTR_out_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
201+
{ MP_QSTR_out_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} },
202+
{ MP_QSTR_in_start, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
203+
{ MP_QSTR_in_end, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MAX} },
204+
};
205+
async_spi_spi_obj_t *self = locked_spi(pos_args[0]);
206+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
207+
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
208+
209+
size_t out_len, in_len;
210+
const uint8_t *data_out = buffer_slice(args[ARG_out_buffer].u_obj, args[ARG_out_start].u_int,
211+
args[ARG_out_end].u_int, MP_BUFFER_READ, &out_len);
212+
uint8_t *data_in = buffer_slice(args[ARG_in_buffer].u_obj, args[ARG_in_start].u_int,
213+
args[ARG_in_end].u_int, MP_BUFFER_WRITE, &in_len);
214+
if (out_len != in_len) {
215+
mp_raise_ValueError(MP_ERROR_TEXT("buffer slices must be of equal length"));
216+
}
217+
common_hal_busio_spi_transfer_start(&self->spi, data_out, data_in, out_len, done);
218+
return self;
219+
}
220+
static CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(async_spi_spi_write_readinto_obj, 2, async_spi_spi_write_readinto);
221+
222+
//| frequency: int
223+
//| """The actual SPI bus frequency. This may not match the frequency requested
224+
//| due to internal limitations."""
225+
//|
226+
//|
227+
MP_PROPERTY_GETTER(async_spi_spi_frequency_obj,
228+
(mp_obj_t)&busio_spi_get_frequency_obj);
229+
230+
static const mp_rom_map_elem_t async_spi_spi_locals_dict_table[] = {
231+
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&busio_spi_deinit_obj) },
232+
{ MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&busio_spi_deinit_obj) },
233+
{ MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
234+
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) },
235+
236+
{ MP_ROM_QSTR(MP_QSTR_configure), MP_ROM_PTR(&busio_spi_configure_obj) },
237+
{ MP_ROM_QSTR(MP_QSTR_try_lock), MP_ROM_PTR(&busio_spi_try_lock_obj) },
238+
{ MP_ROM_QSTR(MP_QSTR_unlock), MP_ROM_PTR(&busio_spi_unlock_obj) },
239+
240+
{ MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&async_spi_spi_write_obj) },
241+
{ MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&async_spi_spi_readinto_obj) },
242+
{ MP_ROM_QSTR(MP_QSTR_write_readinto), MP_ROM_PTR(&async_spi_spi_write_readinto_obj) },
243+
244+
{ MP_ROM_QSTR(MP_QSTR_frequency), MP_ROM_PTR(&async_spi_spi_frequency_obj) },
245+
};
246+
static MP_DEFINE_CONST_DICT(async_spi_spi_locals_dict, async_spi_spi_locals_dict_table);
247+
248+
MP_DEFINE_CONST_OBJ_TYPE(
249+
async_spi_spi_type,
250+
MP_QSTR_SPI,
251+
MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS,
252+
make_new, busio_spi_make_new,
253+
locals_dict, &async_spi_spi_locals_dict
254+
);

‎shared-bindings/async_spi/SPI.h‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
20+
// The names CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW looks up; all three transfers end the same way.
21+
#define common_hal_async_spi_spi_write_end common_hal_async_spi_spi_transfer_end
22+
#define common_hal_async_spi_spi_write_cancel common_hal_async_spi_spi_transfer_cancel
23+
#define common_hal_async_spi_spi_readinto_end common_hal_async_spi_spi_transfer_end
24+
#define common_hal_async_spi_spi_readinto_cancel common_hal_async_spi_spi_transfer_cancel
25+
#define common_hal_async_spi_spi_write_readinto_end common_hal_async_spi_spi_transfer_end
26+
#define common_hal_async_spi_spi_write_readinto_cancel common_hal_async_spi_spi_transfer_cancel
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
//| while not spi.try_lock():
26+
//| pass
27+
//| spi.configure(baudrate=8_000_000)
28+
//| data = bytearray(512)
29+
//| await spi.readinto(data)
30+
//| spi.unlock()
31+
//|
32+
//| asyncio.run(main())
33+
//| """
34+
35+
static const mp_rom_map_elem_t async_spi_module_globals_table[] = {
36+
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_async_spi) },
37+
{ MP_ROM_QSTR(MP_QSTR_SPI), MP_ROM_PTR(&async_spi_spi_type) },
38+
};
39+
40+
static MP_DEFINE_CONST_DICT(async_spi_module_globals, async_spi_module_globals_table);
41+
42+
const mp_obj_module_t async_spi_module = {
43+
.base = { &mp_type_module },
44+
.globals = (mp_obj_dict_t *)&async_spi_module_globals,
45+
};
46+
47+
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

‎shared-bindings/busio/SPI.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
// Type object used in Python. Should be shared between ports.
1616
extern const mp_obj_type_t busio_spi_type;
1717

18+
// Also used by async_spi.SPI.
19+
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);
20+
1821
// Construct an underlying SPI object.
1922
extern void common_hal_busio_spi_construct(busio_spi_obj_t *self,
2023
const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi,
@@ -57,6 +60,13 @@ extern void common_hal_busio_spi_transfer_start(busio_spi_obj_t *self, const uin
5760
// If the transfer started with done is still the bus's, finish it if done is set, and otherwise
5861
// stop it without setting done. Does nothing for an earlier transfer. Does not allocate.
5962
extern void common_hal_busio_spi_stop(busio_spi_obj_t *self, circuitpy_async_flag_t *done);
63+
64+
// busio.SPI methods that async_spi.SPI shares: its objects start with a busio_spi_obj_t.
65+
extern const mp_obj_fun_builtin_fixed_t busio_spi_deinit_obj;
66+
extern const mp_obj_fun_builtin_var_t busio_spi_configure_obj;
67+
extern const mp_obj_fun_builtin_fixed_t busio_spi_try_lock_obj;
68+
extern const mp_obj_fun_builtin_fixed_t busio_spi_unlock_obj;
69+
extern const mp_obj_fun_builtin_fixed_t busio_spi_get_frequency_obj;
6070
#endif
6171

6272
// Reads in len bytes while outputting the byte write_value.

0 commit comments

Comments
 (0)