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.
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):
code.py (attempt to use GPIO32/33 with PIO):
Behavior
boot.py raises immediately:
code.py (even with
exclusive_pin_use=Falseand without the boot.py deinit attempt):dir(usb_host.Port(...))returns['__class__']— the object exposes no methods at all.Description
On the Adafruit Metro RP2350,
board_init()(inboards/adafruit_metro_rp2350/board.c) callscommon_hal_usb_host_port_construct()unconditionally beforeboot.pyruns. This claimsGPIO32(USB_HOST_DATA_PLUS) andGPIO33(USB_HOST_DATA_MINUS) and marks them never-reset. There is no Python API to release them.usb_host.Porthas an intentionally emptylocals_dict— nodeinit(), 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=Falseonrp2pio.StateMachinedoes not bypass this; the USB Host claim is stronger than the PIO exclusivity check.Additional information
Root cause:
shared-bindings/usb_host/Port.chas an emptylocals_dict_table. Nodeinit()method exists at either the shared-bindings or port level.Proposed fix (3 files, ~20 lines):
shared-bindings/usb_host/Port.h— add declaration:shared-bindings/usb_host/Port.c— add Python wrapper and register in locals dict:ports/raspberrypi/common-hal/usb_host/Port.c— add implementation: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.