Skip to content

Commit d0e7554

Browse files
mikeysklarclaude
andcommitted
usb.core: buffer bulk IN endpoints in the background
read() only queued an IN transfer while it was waiting, so nothing polled the device between calls. A USB MIDI keyboard fills its small buffer while Python is busy and drops the rest, which shows up as stuck notes (#10554). After the first read of a bulk IN endpoint, keep a transfer queued and hold up to 8 finished packets for read(). When the queue is full nothing is queued and the device holds its data, as before. Interrupt endpoints are unchanged so HID reads don't return stale reports. Only enabled where tuh_task() runs from background tasks, so the completion callback can't interrupt read(). Each buffer belongs to the Device object that claimed it, and a released buffer is freed only after an aborted transfer has had time to finish. Found and measured by John Park. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent d65f6f2 commit d0e7554

2 files changed

Lines changed: 265 additions & 19 deletions

File tree

‎shared-bindings/usb/core/Device.c‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ MP_DEFINE_CONST_FUN_OBJ_KW(usb_core_device_write_obj, 2, usb_core_device_write);
235235
//| ) -> int:
236236
//| """Read data from the endpoint.
237237
//|
238+
//| After the first read of a bulk IN endpoint, packets from it are buffered in
239+
//| the background, so data isn't lost while your code is busy. A read returns
240+
//| packets that have already arrived. Buffered data is discarded by `deinit`.
241+
//|
238242
//| :param int endpoint: the bEndpointAddress you want to communicate with.
239243
//| :param array.array size_or_buffer: the array to read data into. PyUSB also allows size but CircuitPython only support array to force deliberate memory use.
240244
//| :param int timeout: Time to wait specified in milliseconds. (Different from most CircuitPython!)

‎shared-module/usb/core/Device.c‎

Lines changed: 261 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,19 @@ void tuh_mount_cb(uint8_t dev_addr) {
2929
_mounted_devices |= 1 << dev_addr;
3030
}
3131

32+
// Bulk IN buffering needs the completion callback to run from CircuitPython's
33+
// background tasks, where it can't interrupt read() partway through.
34+
#define IN_BUFFER_ENABLED (CFG_TUSB_OS == OPT_OS_NONE || CFG_TUSB_OS == OPT_OS_PICO)
35+
36+
#if IN_BUFFER_ENABLED
37+
static void _in_buffer_release_device(uint8_t daddr);
38+
#endif
39+
3240
void tuh_umount_cb(uint8_t dev_addr) {
3341
_mounted_devices &= ~(1 << dev_addr);
42+
#if IN_BUFFER_ENABLED
43+
_in_buffer_release_device(dev_addr);
44+
#endif
3445
}
3546

3647
static xfer_result_t _xfer_result;
@@ -57,6 +68,122 @@ static uint8_t *_ensure_dma_buffer(usb_core_device_obj_t *self, const uint8_t *b
5768
}
5869

5970
#endif
71+
72+
#if IN_BUFFER_ENABLED
73+
// Once Python reads a bulk IN endpoint, keep a transfer queued on it so the device
74+
// is polled while Python is busy. Completed transfers wait here for read(). When
75+
// the queue is full no transfer is queued, and the device holds its data.
76+
#define IN_BUFFER_SLOTS 4
77+
#define IN_BUFFER_DEPTH 8
78+
// How long a released buffer is kept for an aborted transfer to finish writing.
79+
#define IN_BUFFER_STALE_MS 10
80+
81+
typedef struct {
82+
usb_core_device_obj_t *owner; // NULL when the slot is not in use
83+
uint8_t *data; // IN_BUFFER_DEPTH transfers of mps bytes each
84+
uint8_t *stale_data; // released while a transfer was queued, freed by _in_buffer_reap()
85+
uint32_t stale_time;
86+
uint16_t len[IN_BUFFER_DEPTH];
87+
uint16_t mps;
88+
uint16_t offset; // bytes of the head transfer already read
89+
uint8_t daddr;
90+
uint8_t ep_addr;
91+
uint8_t head;
92+
uint8_t count;
93+
uint8_t gen; // changed on release so an old completion is ignored
94+
bool busy;
95+
xfer_result_t error;
96+
} in_buffer_t;
97+
98+
static in_buffer_t _in_buffers[IN_BUFFER_SLOTS];
99+
100+
static void _in_buffer_queue(in_buffer_t *buf);
101+
102+
static void _in_buffer_cb(tuh_xfer_t *xfer) {
103+
in_buffer_t *buf = &_in_buffers[xfer->user_data & 0xff];
104+
if (buf->owner == NULL || !buf->busy || buf->gen != (uint8_t)(xfer->user_data >> 8)) {
105+
return;
106+
}
107+
buf->busy = false;
108+
if (xfer->result != XFER_RESULT_SUCCESS) {
109+
buf->error = xfer->result;
110+
return;
111+
}
112+
buf->len[(buf->head + buf->count) % IN_BUFFER_DEPTH] = xfer->actual_len;
113+
buf->count++;
114+
_in_buffer_queue(buf);
115+
}
116+
117+
static void _in_buffer_queue(in_buffer_t *buf) {
118+
if (buf->busy || buf->error != XFER_RESULT_SUCCESS || buf->count == IN_BUFFER_DEPTH) {
119+
return;
120+
}
121+
tuh_xfer_t xfer = {
122+
.daddr = buf->daddr,
123+
.ep_addr = buf->ep_addr,
124+
.buflen = buf->mps,
125+
.buffer = buf->data + ((buf->head + buf->count) % IN_BUFFER_DEPTH) * buf->mps,
126+
.complete_cb = _in_buffer_cb,
127+
.user_data = (buf - _in_buffers) | (buf->gen << 8),
128+
};
129+
buf->busy = tuh_edpt_xfer(&xfer);
130+
}
131+
132+
static void _in_buffer_release(in_buffer_t *buf) {
133+
if (buf->busy) {
134+
// Some host controllers can still write into the buffer after an abort, and
135+
// TinyUSB may have its completion queued, so free it later.
136+
tuh_edpt_abort_xfer(buf->daddr, buf->ep_addr);
137+
buf->stale_data = buf->data;
138+
buf->stale_time = supervisor_ticks_ms32();
139+
} else {
140+
port_free(buf->data);
141+
}
142+
buf->data = NULL;
143+
buf->owner = NULL;
144+
buf->busy = false;
145+
buf->head = 0;
146+
buf->count = 0;
147+
buf->offset = 0;
148+
buf->gen++;
149+
}
150+
151+
static bool _in_buffer_in_use(uint8_t daddr, uint8_t ep_addr) {
152+
for (size_t i = 0; i < IN_BUFFER_SLOTS; i++) {
153+
in_buffer_t *buf = &_in_buffers[i];
154+
if (buf->owner != NULL && buf->daddr == daddr && buf->ep_addr == ep_addr) {
155+
return true;
156+
}
157+
}
158+
return false;
159+
}
160+
161+
static void _in_buffer_release_device(uint8_t daddr) {
162+
for (size_t i = 0; i < IN_BUFFER_SLOTS; i++) {
163+
in_buffer_t *buf = &_in_buffers[i];
164+
if (buf->owner != NULL && buf->daddr == daddr) {
165+
_in_buffer_release(buf);
166+
}
167+
}
168+
}
169+
170+
// Runs TinyUSB until released buffers can't be written to or completed into, then
171+
// frees them. Called from Python context, never from a finaliser or callback.
172+
static void _in_buffer_reap(void) {
173+
for (size_t i = 0; i < IN_BUFFER_SLOTS; i++) {
174+
in_buffer_t *buf = &_in_buffers[i];
175+
if (buf->stale_data == NULL) {
176+
continue;
177+
}
178+
while (supervisor_ticks_ms32() - buf->stale_time < IN_BUFFER_STALE_MS) {
179+
RUN_BACKGROUND_TASKS;
180+
}
181+
port_free(buf->stale_data);
182+
buf->stale_data = NULL;
183+
}
184+
}
185+
#endif
186+
60187
bool common_hal_usb_core_device_construct(usb_core_device_obj_t *self, uint8_t device_address) {
61188
if (!tuh_inited()) {
62189
mp_raise_RuntimeError(MP_ERROR_TEXT("No usb host port initialized"));
@@ -82,9 +209,23 @@ void common_hal_usb_core_device_deinit(usb_core_device_obj_t *self) {
82209
if (common_hal_usb_core_device_deinited(self)) {
83210
return;
84211
}
212+
#if IN_BUFFER_ENABLED
213+
for (size_t i = 0; i < IN_BUFFER_SLOTS; i++) {
214+
if (_in_buffers[i].owner == self) {
215+
_in_buffer_release(&_in_buffers[i]);
216+
}
217+
}
218+
#endif
85219
size_t open_size = sizeof(self->open_endpoints);
86220
for (size_t i = 0; i < open_size; i++) {
87221
if (self->open_endpoints[i] != 0) {
222+
#if IN_BUFFER_ENABLED
223+
// Another Device object for the same device is still buffering it.
224+
if (_in_buffer_in_use(self->device_address, self->open_endpoints[i])) {
225+
self->open_endpoints[i] = 0;
226+
continue;
227+
}
228+
#endif
88229
tuh_edpt_close(self->device_address, self->open_endpoints[i]);
89230
self->open_endpoints[i] = 0;
90231
}
@@ -411,6 +552,27 @@ static size_t _xfer(tuh_xfer_t *xfer, mp_int_t timeout, bool our_buffer, bool ra
411552
return _handle_timed_transfer_callback(xfer, timeout, our_buffer, raise_on_timeout);
412553
}
413554

555+
static tusb_desc_endpoint_t const *_find_endpoint_descriptor(usb_core_device_obj_t *self, mp_int_t endpoint) {
556+
tusb_desc_configuration_t *desc_cfg = (tusb_desc_configuration_t *)self->configuration_descriptor;
557+
558+
uint32_t total_length = tu_le16toh(desc_cfg->wTotalLength);
559+
uint8_t const *desc_end = ((uint8_t const *)desc_cfg) + total_length;
560+
uint8_t const *p_desc = tu_desc_next(desc_cfg);
561+
562+
// parse each interfaces
563+
while (p_desc < desc_end) {
564+
if (TUSB_DESC_ENDPOINT == tu_desc_type(p_desc)) {
565+
tusb_desc_endpoint_t const *desc_ep = (tusb_desc_endpoint_t const *)p_desc;
566+
if (desc_ep->bEndpointAddress == endpoint) {
567+
return desc_ep;
568+
}
569+
}
570+
571+
p_desc = tu_desc_next(p_desc);
572+
}
573+
return NULL;
574+
}
575+
414576
static bool _open_endpoint(usb_core_device_obj_t *self, mp_int_t endpoint) {
415577
bool endpoint_open = false;
416578
size_t open_size = sizeof(self->open_endpoints);
@@ -431,27 +593,10 @@ static bool _open_endpoint(usb_core_device_obj_t *self, mp_int_t endpoint) {
431593
return false;
432594
}
433595

434-
tusb_desc_configuration_t *desc_cfg = (tusb_desc_configuration_t *)self->configuration_descriptor;
435-
436-
uint32_t total_length = tu_le16toh(desc_cfg->wTotalLength);
437-
uint8_t const *desc_end = ((uint8_t const *)desc_cfg) + total_length;
438-
uint8_t const *p_desc = tu_desc_next(desc_cfg);
439-
440-
// parse each interfaces
441-
while (p_desc < desc_end) {
442-
if (TUSB_DESC_ENDPOINT == tu_desc_type(p_desc)) {
443-
tusb_desc_endpoint_t const *desc_ep = (tusb_desc_endpoint_t const *)p_desc;
444-
if (desc_ep->bEndpointAddress == endpoint) {
445-
break;
446-
}
447-
}
448-
449-
p_desc = tu_desc_next(p_desc);
450-
}
451-
if (p_desc >= desc_end) {
596+
tusb_desc_endpoint_t const *desc_ep = _find_endpoint_descriptor(self, endpoint);
597+
if (desc_ep == NULL) {
452598
return false;
453599
}
454-
tusb_desc_endpoint_t const *desc_ep = (tusb_desc_endpoint_t const *)p_desc;
455600

456601
bool open = tuh_edpt_open(self->device_address, desc_ep);
457602
if (open) {
@@ -460,6 +605,95 @@ static bool _open_endpoint(usb_core_device_obj_t *self, mp_int_t endpoint) {
460605
return open;
461606
}
462607

608+
#if IN_BUFFER_ENABLED
609+
// Returns the buffer for a bulk IN endpoint, claiming a slot on first use. Returns
610+
// NULL for other endpoints, or when no slot or memory is free, and read() then
611+
// reads directly as before.
612+
static in_buffer_t *_in_buffer_get(usb_core_device_obj_t *self, mp_int_t endpoint) {
613+
in_buffer_t *free_buf = NULL;
614+
for (size_t i = 0; i < IN_BUFFER_SLOTS; i++) {
615+
in_buffer_t *buf = &_in_buffers[i];
616+
if (buf->owner != NULL && buf->daddr == self->device_address && buf->ep_addr == endpoint) {
617+
return buf;
618+
}
619+
if (free_buf == NULL && buf->owner == NULL && buf->stale_data == NULL) {
620+
free_buf = buf;
621+
}
622+
}
623+
if (free_buf == NULL || tu_edpt_dir(endpoint) != TUSB_DIR_IN ||
624+
(_mounted_devices & (1 << self->device_address)) == 0) {
625+
return NULL;
626+
}
627+
tusb_desc_endpoint_t const *desc_ep = _find_endpoint_descriptor(self, endpoint);
628+
if (desc_ep == NULL || desc_ep->bmAttributes.xfer != TUSB_XFER_BULK) {
629+
return NULL;
630+
}
631+
uint16_t mps = tu_edpt_packet_size(desc_ep);
632+
if (mps == 0) {
633+
return NULL;
634+
}
635+
free_buf->data = port_malloc(IN_BUFFER_DEPTH * mps, true);
636+
if (free_buf->data == NULL) {
637+
return NULL;
638+
}
639+
free_buf->owner = self;
640+
free_buf->mps = mps;
641+
free_buf->daddr = self->device_address;
642+
free_buf->ep_addr = endpoint;
643+
free_buf->error = XFER_RESULT_SUCCESS;
644+
return free_buf;
645+
}
646+
647+
// Copies queued packets into buffer until it is full or a short packet ends the
648+
// transfer, like a direct read.
649+
static size_t _in_buffer_read(in_buffer_t *buf, uint8_t *buffer, size_t len, mp_int_t timeout, bool raise_on_timeout) {
650+
_in_buffer_queue(buf);
651+
uint32_t start_time = supervisor_ticks_ms32();
652+
while ((timeout == 0 || supervisor_ticks_ms32() - start_time < (uint32_t)timeout) &&
653+
!mp_hal_is_interrupted() &&
654+
buf->count == 0 && buf->busy) {
655+
RUN_BACKGROUND_TASKS;
656+
}
657+
if (mp_hal_is_interrupted()) {
658+
return 0;
659+
}
660+
if (buf->count == 0) {
661+
xfer_result_t error = buf->error;
662+
buf->error = XFER_RESULT_SUCCESS;
663+
if (error == XFER_RESULT_STALLED) {
664+
mp_raise_usb_core_USBError(MP_ERROR_TEXT("Pipe error"));
665+
}
666+
if ((error == XFER_RESULT_SUCCESS && !buf->busy) ||
667+
(error != XFER_RESULT_SUCCESS && error != XFER_RESULT_TIMEOUT)) {
668+
mp_raise_usb_core_USBError(NULL);
669+
}
670+
if (raise_on_timeout) {
671+
mp_raise_usb_core_USBTimeoutError();
672+
}
673+
return 0;
674+
}
675+
size_t total = 0;
676+
while (buf->count > 0 && total < len) {
677+
uint16_t packet_len = buf->len[buf->head];
678+
size_t n = MIN(len - total, (size_t)(packet_len - buf->offset));
679+
memcpy(buffer + total, buf->data + buf->head * buf->mps + buf->offset, n);
680+
total += n;
681+
buf->offset += n;
682+
if (buf->offset < packet_len) {
683+
break;
684+
}
685+
buf->offset = 0;
686+
buf->head = (buf->head + 1) % IN_BUFFER_DEPTH;
687+
buf->count--;
688+
if (packet_len < buf->mps) {
689+
break;
690+
}
691+
}
692+
_in_buffer_queue(buf);
693+
return total;
694+
}
695+
#endif
696+
463697
mp_int_t common_hal_usb_core_device_write(usb_core_device_obj_t *self, mp_int_t endpoint, const uint8_t *buffer, mp_int_t len, mp_int_t timeout) {
464698
if (!_open_endpoint(self, endpoint)) {
465699
mp_raise_usb_core_USBError(NULL);
@@ -497,6 +731,14 @@ mp_int_t common_hal_usb_core_device_read(usb_core_device_obj_t *self, mp_int_t e
497731
return 0;
498732
}
499733

734+
#if IN_BUFFER_ENABLED
735+
_in_buffer_reap();
736+
in_buffer_t *in_buf = _in_buffer_get(self, endpoint);
737+
if (in_buf != NULL) {
738+
return _in_buffer_read(in_buf, buffer, len, timeout, raise_on_timeout);
739+
}
740+
#endif
741+
500742
#if !CIRCUITPY_ALL_MEMORY_DMA_CAPABLE
501743
// Ensure buffer is in DMA-capable memory
502744
uint8_t *dma_buffer = _ensure_dma_buffer(self, buffer, len, false); // false = for read

0 commit comments

Comments
 (0)