Skip to content

USB Host: Fast polling + write to CIRCUITPY crashes board #10562

@samblenny

Description

@samblenny

CircuitPython version and board name

Adafruit CircuitPython 10.0.0-beta.2 on 2025-07-30; Adafruit Fruit Jam with rp2350b

Code/REPL

import displayio
import gc
import time
import usb
from usb.core import USBError, USBTimeoutError


def test_descriptor_parsing_read_gamepad():
    # Check USB device descriptors, then read from gamepad if found.
    # This one is designed to trigger bugs where an in progress USB transaction
    # fights with other CircuitPython tasks. For me, this reliably triggers a
    # board reset with 10.0.0-beta.2, Fruit Jam rev D, and an 8BitDo SN30 Pro
    # wired gamepad (XInput style) or the Adafruit generic SNES style gamepad.
    # To trigger bug, attempt to write a file to CIRCUITPY once you see the
    # "Reading gamepad input..." message.
    gamepad_vid_pid_map = {
        # ( vid,    pid): (max_packet, sleep_before_read, description)
        (0x081f, 0xe401): (8, True, "Adafruit generic SNES style (HID)"),
        (0x045e, 0x028e): (32, False, "Xbox 360 compatible (XInput)"),
    }
    print("Finding USB devices...")
    try:
        for device in usb.core.find(find_all=True):
            # 1. Print descriptor properties
            vid = device.idVendor
            pid = device.idProduct
            print(f"idVendor      {vid:04x}")
            print(f"idProduct     {pid:04x}")
            print(f"serial_number {device.serial_number}")
            print(f"product       {device.product}")
            print(f"manufacturer  {device.manufacturer}")
            print(f"speed         {device.speed}")
            # 2. Configure interface for known gamepads (skip other devices)
            try:
                if not ((vid, pid) in gamepad_vid_pid_map):
                    print(" skipping this one (not a known gamepad)")
                    continue
                max_packet = gamepad_vid_pid_map[(vid,pid)][0]
                sleep_before_read = gamepad_vid_pid_map[(vid,pid)][1]
                buf = bytearray(max_packet)
                interface = 0
                if device.is_kernel_driver_active(interface):
                    device.detach_kernel_driver(interface)
                device.set_configuration(interface)
            except USBError as e:
                print(f"conf USBError: .errno={e.errno} '{str(e)}'")
            except USBTimeoutError as e:
                print(f"conf USBError: .errno={e.errno} '{str(e)}'")
            # 3. Rapidly poll gamepad for input. Goal of this is to trigger
            #    bugs where an in progress USB transaction fights with other
            #    CircuitPython tasks.
            print("Reading gamepad input...")
            print("Writing a file to CIRCUITPY now will probably reset board")
            while True:
                try:
                    if sleep_before_read:
                        # Adafruit generic SNES gamepad (low speed device) gets
                        # mad and raises USBError if you poll it too quickly
                        time.sleep(0.003)
                    n = device.read(0x81, buf, timeout=10)
                except USBError as e:
                    print(f"read USBError: .errno={e.errno} '{str(e)}'")
                except USBTimeoutError as e:
                    print(f"read USBError: .errno={e.errno} '{str(e)}'")
    except USBError as e:
        print(f"find USBError: .errno={e.errno} '{str(e)}'")
    except USBTimeoutError as e:
        print(f"find USBError: .errno={e.errno} '{str(e)}'")


displayio.release_displays()
gc.collect()

while True:
    # pause with a prompt to avoid rapidly scrolling serial console output
    input("press Enter to begin running test: ")
    print()
    test_descriptor_parsing_read_gamepad()
    print()

Behavior

For me, the code above reliably triggers a bug where the board locks up when using an 8BitDo SN30 Pro wired gamepad (XInput style) or the Adafruit generic SNES style gamepad (HID style).

I've seen no indications that the bug depends on the model of gamepad, or even that it's specific to gamepads. I mention those two because they are easy to obtain. My impression is that the bug is related to the frequency of calls to usb.core.Device.read().

To trigger bug:

  1. Run the code and connect the gamepad
  2. Connect to the serial console and press Enter at the prompt to begin finding gamepad devices
  3. Once you see the "Reading gamepad input..." message, attempt to write a file to CIRCUIPY

When I do this, the cp code.py /Volumes/CIRCUIPY shell command I'm using will get stuck (does not return to shell prompt). After a while, macOS gives me a "Disk Not Ejected Properly" notification and the serial console device disappears. The board LEDs do not indicate any error conditions. It seems like a pretty low level crash. To get things working again, I have to power cycle the board or press the reset button.

Description

No response

Additional information

It's possible this might be related to #10555 . That could be the case if the crash described here happens because input polling is starving other essential tasks for CPU time. (e.g. interrupt priority, blocking IO, or some other scheduling thing)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions