Skip to content

Commit 5ce3aaa

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 c7d0366 commit 5ce3aaa

13 files changed

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

‎shared-bindings/async_spi/SPI.h‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
// Each start returns the context for transfer_end() and transfer_cancel(); both act only on the
16+
// transfer started with done.
17+
void *common_hal_async_spi_spi_write_start(async_spi_spi_obj_t *self, const uint8_t *data,
18+
size_t len, circuitpy_async_flag_t *done);
19+
void *common_hal_async_spi_spi_readinto_start(async_spi_spi_obj_t *self, uint8_t *data,
20+
size_t len, uint8_t write_value, circuitpy_async_flag_t *done);
21+
void *common_hal_async_spi_spi_write_readinto_start(async_spi_spi_obj_t *self,
22+
const uint8_t *data_out, uint8_t *data_in, size_t len, circuitpy_async_flag_t *done);
23+
mp_obj_t common_hal_async_spi_spi_transfer_end(void *context, circuitpy_async_flag_t *done);
24+
void common_hal_async_spi_spi_transfer_cancel(void *context, circuitpy_async_flag_t *done);
25+
// lock(): the context is the SPI object; poll tries to take the lock.
26+
mp_obj_t common_hal_async_spi_spi_lock_end(void *context, circuitpy_async_flag_t *done);
27+
void common_hal_async_spi_spi_lock_cancel(void *context, circuitpy_async_flag_t *done);
28+
bool common_hal_async_spi_spi_lock_poll(void *context);
29+
30+
// The names CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW looks up; all three transfers end the same way.
31+
#define common_hal_async_spi_spi_write_end common_hal_async_spi_spi_transfer_end
32+
#define common_hal_async_spi_spi_write_cancel common_hal_async_spi_spi_transfer_cancel
33+
#define common_hal_async_spi_spi_readinto_end common_hal_async_spi_spi_transfer_end
34+
#define common_hal_async_spi_spi_readinto_cancel common_hal_async_spi_spi_transfer_cancel
35+
#define common_hal_async_spi_spi_write_readinto_end common_hal_async_spi_spi_transfer_end
36+
#define common_hal_async_spi_spi_write_readinto_cancel common_hal_async_spi_spi_transfer_cancel

0 commit comments

Comments
 (0)