Skip to content

usb_host.Port has no deinit() — USB Host pins permanently claimed, cannot be used by PIO or other peripherals #10953

@TheKitty

Description

@TheKitty

CircuitPython version:
Adafruit CircuitPython 10.1.4 on 2026-03-09; Adafruit Metro RP2350 with rp2350b

Board: adafruit_metro_rp2350

Code/REPL

boot.py (attempt to release pins):

import usb_host, board
port = usb_host.Port(board.USB_HOST_DATA_PLUS, board.USB_HOST_DATA_MINUS)
port.deinit()

code.py (attempt to use GPIO32/33 with PIO):

import board, rp2pio, adafruit_pioasm

prog = adafruit_pioasm.assemble("""
.pio_version 1
.program test_q
    nop
    out pins, 2
""")

sm = rp2pio.StateMachine(
    prog,
    frequency=28_636_360,
    first_out_pin=board.USB_HOST_DATA_PLUS,
    out_pin_count=2,
    auto_pull=True,
    pull_threshold=8,
    out_shift_right=True,
    exclusive_pin_use=False,
)

Behavior

boot.py raises immediately:

AttributeError: 'Port' object has no attribute 'deinit'

code.py (even with exclusive_pin_use=False and without the boot.py deinit attempt):

ValueError: USB_HOST_DATA_PLUS in use

dir(usb_host.Port(...)) returns ['__class__'] — the object exposes no methods at all.

Description

On the Adafruit Metro RP2350, board_init() (in boards/adafruit_metro_rp2350/board.c) calls common_hal_usb_host_port_construct() unconditionally before boot.py runs. This claims GPIO32 (USB_HOST_DATA_PLUS) and GPIO33 (USB_HOST_DATA_MINUS) and marks them never-reset. There is no Python API to release them.

usb_host.Port has an intentionally empty locals_dict — no deinit(), no context manager support. The object is designed as a persistent singleton. As a result, once USB Host is auto-initialized at boot, those pins are permanently unavailable for any other use — including PIO — with no workaround available to the user.

exclusive_pin_use=False on rp2pio.StateMachine does not bypass this; the USB Host claim is stronger than the PIO exclusivity check.

Additional information

Root cause: shared-bindings/usb_host/Port.c has an empty locals_dict_table. No deinit() method exists at either the shared-bindings or port level.

Proposed fix (3 files, ~20 lines):

shared-bindings/usb_host/Port.h — add declaration:

void common_hal_usb_host_port_deinit(usb_host_port_obj_t *self);

shared-bindings/usb_host/Port.c — add Python wrapper and register in locals dict:

//| def deinit(self) -> None:
//|     """Release the USB host port's D+ and D- pins back to the GPIO pool.
//|     USB host functionality stops. The pins become available for PIO or
//|     other peripheral use. Can be called in ``boot.py``."""//|
static mp_obj_t usb_host_port_deinit(mp_obj_t self_in) {
    usb_host_port_obj_t *self = MP_OBJ_TO_PTR(self_in);
    common_hal_usb_host_port_deinit(self);
    return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_1(usb_host_port_deinit_obj, usb_host_port_deinit);

static const mp_rom_map_elem_t usb_host_port_locals_dict_table[] = {
    { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&usb_host_port_deinit_obj) },
};

ports/raspberrypi/common-hal/usb_host/Port.c — add implementation:

void common_hal_usb_host_port_deinit(usb_host_port_obj_t *self) {
    if (self->dp == NULL) {
        return;  // already deinitialized
    }
    common_hal_reset_pin(self->dp);
    common_hal_reset_pin(self->dm);
    self->dp = NULL;
    self->dm = NULL;
}

Default behavior is completely unchanged. Boards that never call deinit() continue working exactly as before. The fix follows the standard CircuitPython pattern for hardware object cleanup.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions