Skip to content

Commit c902051

Browse files
committed
usb,rp2: Fix "TinyUSB callback can't recurse" error when using threads.
Looks like there was a long standing underlying bug here when using rp2 threads, where either CPU may poll CDC input and trigger the TinyUSB task. TinyUSB could run on both CPUs concurrently, which may have lead to some incorrect behaviour. The race started triggering an exception when runtime USB support was added, and a check was added for the USB task recursing on itself from a Python handler function. The race is most commonly triggered when working from the interactive REPL, even a minimal running thread can trigger it. This commit adds a test case that triggers it in a different way (polling stdin from a thread). Fix is to add a port-level macro that indicates whether the TinyUSB task can run. Closes #15390. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
1 parent 557d31e commit c902051

File tree

5 files changed

+61
-6
lines changed

5 files changed

+61
-6
lines changed

ports/rp2/mphalport.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
extern int mp_interrupt_char;
6969
extern ringbuf_t stdin_ringbuf;
7070

71+
#if MICROPY_PY_THREAD
72+
// Only run the TinyUSB task on CPU 0 to avoid data races
73+
#define MICROPY_HW_USBD_TASK_CAN_RUN() (get_core_num() == 0)
74+
#endif
75+
7176
// Port-specific function to create a wakeup interrupt after timeout_ms and enter WFE
7277
void mp_wfe_or_timeout(uint32_t timeout_ms);
7378

shared/tinyusb/mp_usbd.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,20 @@
2424
* THE SOFTWARE.
2525
*/
2626

27-
#include "py/mpconfig.h"
27+
#include "mp_usbd.h"
2828

2929
#if MICROPY_HW_ENABLE_USBDEV
3030

31-
#include "mp_usbd.h"
32-
3331
#ifndef NO_QSTR
3432
#include "device/dcd.h"
3533
#endif
3634

3735
#if !MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
3836

3937
void mp_usbd_task(void) {
40-
tud_task_ext(0, false);
38+
if (MICROPY_HW_USBD_TASK_CAN_RUN()) {
39+
tud_task_ext(0, false);
40+
}
4141
}
4242

4343
void mp_usbd_task_callback(mp_sched_node_t *node) {

shared/tinyusb/mp_usbd.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
#ifndef MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_H
2828
#define MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_H
2929

30-
#include "py/mpconfig.h"
30+
#include "py/mphal.h"
3131

3232
#if MICROPY_HW_ENABLE_USBDEV
3333

@@ -67,6 +67,11 @@ extern const uint8_t mp_usbd_builtin_desc_cfg[MP_USBD_BUILTIN_DESC_CFG_LEN];
6767

6868
void mp_usbd_task_callback(mp_sched_node_t *node);
6969

70+
#ifndef MICROPY_HW_USBD_TASK_CAN_RUN
71+
// Redefine this macro on ports where the TinyUSB task can't always execute
72+
#define MICROPY_HW_USBD_TASK_CAN_RUN() (1)
73+
#endif
74+
7075
#if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
7176
void mp_usbd_deinit(void);
7277
void mp_usbd_init(void);

shared/tinyusb/mp_usbd_runtime.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ static void mp_usbd_disconnect(mp_obj_usb_device_t *usbd) {
488488

489489
// Thjs callback is queued by mp_usbd_schedule_task() to process USB later.
490490
void mp_usbd_task_callback(mp_sched_node_t *node) {
491-
if (tud_inited() && !in_usbd_task) {
491+
if (tud_inited() && !in_usbd_task && MICROPY_HW_USBD_TASK_CAN_RUN()) {
492492
mp_usbd_task_inner();
493493
}
494494
// If in_usbd_task is set, it means something else has already manually called
@@ -501,6 +501,9 @@ void mp_usbd_task_callback(mp_sched_node_t *node) {
501501
// Task function can be called manually to force processing of USB events
502502
// (mostly from USB-CDC serial port when blocking.)
503503
void mp_usbd_task(void) {
504+
if (!MICROPY_HW_USBD_TASK_CAN_RUN()) {
505+
return; // The port can't run the USB task from here (i.e. wrong CPU)
506+
}
504507
if (in_usbd_task) {
505508
// If this exception triggers, it means a USB callback tried to do
506509
// something that itself became blocked on TinyUSB (most likely: read or

tests/thread/thread_stdin.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# test that having multiple threads block on stdin doesn't cause any issues.
2+
#
3+
# The test doesn't expect any input on stdin.
4+
#
5+
# This is a regression test for https://github.com/micropython/micropython/issues/15230
6+
# on rp2, but doubles as a general property to test across all ports.
7+
import select
8+
import sys
9+
import time
10+
import _thread
11+
12+
13+
class StdinWaiter:
14+
def __init__(self):
15+
self._done = False
16+
17+
def wait_stdin(self, timeout_ms):
18+
poller = select.poll()
19+
poller.register(sys.stdin, select.POLLIN)
20+
poller.poll(timeout_ms)
21+
# ignoring the poll result as we don't expect any input
22+
self._done = True
23+
24+
def is_done(self):
25+
return self._done
26+
27+
28+
thread_waiter = StdinWaiter()
29+
_thread.start_new_thread(thread_waiter.wait_stdin, (1000,))
30+
StdinWaiter().wait_stdin(1000)
31+
32+
# add a short delay to account for inconsistency waking the two threads (on
33+
# CPython CI runners there seems to be some intermittent difference in thread
34+
# wakeup delays.) On Linux we expect both threads should resume more or less
35+
# concurrently - aka the "thundering herd" effect"
36+
try:
37+
time.sleep_ms(100)
38+
except AttributeError:
39+
time.sleep(0.1)
40+
41+
# the background thread should have completed its wait by now
42+
print(thread_waiter.is_done())

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy