Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

Due native USB stops answering after 4096 control requests since the port was last opened. Arduino IDE 2's serial-discovery triggers it in ~17 minutes

Abierto
#154 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
3/5
Tiempo estimado
Medio día
Aptitud para principiantes
62/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
c, cpp, python
Área
embedded-iot

Línea de trabajo

Start in cores/arduino/USB/USBCore.cpp at USB_ISR(), where the setup packet is read with UDD_Recv(EP0, ...), then read UDD_Recv and UDD_ClearOUT in system/libsam/source/uotghs_device.c to see how ul_recv_fifo_ptr[EP0] advances. The fix belongs in the core file, since libsam ships precompiled. Done means the reproducer (due_setup_overflow_repro.py 20000) reports no failures on a Due running Blink, and the serial port still opens afterwards. This needs a Due board on Windows to verify.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Title: Due native USB stops answering after 4096 control requests since the port was last opened (ul_recv_fifo_ptr[EP0] never reset); Arduino IDE 2's serial-discovery triggers it in ~17 minutes

Summary

On the Arduino Due's native USB port, every USB control request moves the offset that USB_ISR() reads its setup packet from 8 bytes further into endpoint 0's FIFO window. Only a request with an OUT data stage resets it, in practice the SET_LINE_CODING sent when a program opens the serial port. After 4096 control requests without such a reset, the offset passes the end of endpoint 0's 32 KB window. From then on the core reads every setup packet from the wrong memory, and the Due stops answering control requests until RESET is pressed or the board is power-cycled:

  • opening the serial port fails (on Windows: "A device attached to the system is not functioning", or an open that takes 60 s and then fails);
  • uploads fail at the 1200 baud touch ("No device found on COMx");
  • the IDE's Serial Monitor cannot connect.

Holding the port open does not prevent it, because only an open resets the offset. Bulk data keeps flowing on a port that is already open, so nothing looks wrong until the next open or upload.

How it is reached in practice: with Arduino IDE 2 running, serial-discovery.exe reads the Due's string descriptors about 4 times a second (measured on this PC). The limit is reached about 17 minutes after the port was last opened. We confirmed this with the IDE alone: reset the Due, leave the IDE open with the Serial Monitor closed, and after 20 minutes the Serial Monitor cannot connect. With serial-discovery.exe ended, no requests arrive and the Due keeps working. Any program that reads USB descriptors (USB viewers, device-listing tools) has the same effect.

It affects any sketch, Blink included, since the core always runs the native USB device. We found it while testing a Due-based instrument (Sanworks Pulse Pal 2), whose released firmware has it too.

Environment

  • Arduino Due (native USB port), Blink sketch
  • Arduino SAM Boards (arduino:sam) 1.6.12
  • Arduino IDE 2.3.10 (arduino-cli 1.5.1), its bundled serial-discovery
  • Windows 11 Pro 10.0.26200

Steps to reproduce

With the Arduino IDE, about 20 minutes:

  1. Upload Blink to a Due through its native USB port.
  2. Press RESET. Keep the Arduino IDE open, with the Serial Monitor closed.
  3. Wait 20 minutes, then open the Serial Monitor (or upload). It cannot connect. Press RESET and it works again.

In under a second: run the reproducer below (Windows, Python 3, standard library only) with the Due on its native port, after a RESET:

python due_setup_overflow_repro.py

It sends GET_DESCRIPTOR(string) requests to the Due through its USB hub, as device-listing tools do. Typical output on a stock Due running Blink (the first failure falls between about 4086 and 4094, depending on how many requests Windows sent since the reset):

Arduino Due on port 6 of USB\ROOT_HUB30\4&bba4c3c&0&0
First reply: 'Arduino Due'
5000 requests: 2 failed; first failure at request 4086
Bug reproduced: the Due no longer answers USB control requests. Opening its serial port or uploading now fails until RESET is pressed.

After that, the serial port cannot be opened until RESET. With python due_setup_overflow_repro.py 3000, below the limit, every request succeeds and the port still opens.

Expected: the Due answers control requests indefinitely.
Actual: it stops after 4096 setup packets since the port was last opened.

Cause

cores/arduino/USB/USBCore.cpp, USB_ISR() (1.6.12, line 648):

USBSetup setup;
UDD_Recv(EP0, (uint8_t*)&setup, 8);
UDD_ClearSetupInt();

system/libsam/source/uotghs_device.c:

void UDD_Recv(uint32_t ep, uint8_t* data, uint32_t len)
{
	uint8_t *ptr_src = (uint8_t *) &udd_get_endpoint_fifo_access8(ep);
	...
	for (i = 0, ptr_src += ul_recv_fifo_ptr[ep]; i < len; ++i)
		*ptr_dest++ = *ptr_src++;
	ul_recv_fifo_ptr[ep] += i;       // line 282: +8 for every setup packet
}

void UDD_ClearOUT(void)              // the only reset of ul_recv_fifo_ptr[EP0]
{
	UOTGHS->UOTGHS_DEVEPTICR[EP0] = UOTGHS_DEVEPTICR_RXOUTIC;
	ul_recv_fifo_ptr[EP0] = 0;
}

UDD_ClearSetupInt() does not reset the offset. UDD_ClearOUT() is only reached through USBD_RecvControl(), for a request that carries data from the host (SET_LINE_CODING). Each endpoint's FIFO window is 0x8000 bytes (udd_get_endpoint_fifo_access8), so after 0x8000 / 8 = 4096 setup packets UDD_Recv() reads past endpoint 0's window. Every request from then on is misread and mostly stalled. That includes the SET_LINE_CODING that would have reset the offset, so the state persists until reset.

Proposed fix

Reset the offset before reading each setup packet (USBCore.cpp, which is compiled with every sketch):

 		USBSetup setup;
+		UDD_ClearOUT(); // read the setup packet from the start of EP0's FIFO
 		UDD_Recv(EP0, (uint8_t*)&setup, 8);
 		UDD_ClearSetupInt();

UDD_ClearOUT() also acknowledges RXOUTI on endpoint 0. That is harmless here, because a new SETUP ends any control transfer in progress. Alternatively, UDD_ClearSetupInt() in libsam could reset ul_recv_fifo_ptr[EP0], but libsam ships precompiled.

Tested: Blink built against 1.6.12 with this change answers 20,000 requests from the reproducer with no failures, and its port still opens. Stock 1.6.12 fails at about request 4086.

Workaround for sketches (until a fixed core is released)

Hook the core's USB interrupt through gpf_isr (libsam's USB interrupt pointer), and reset the offset whenever a setup packet is waiting. Pulse Pal 2's firmware does this. It passed 45,000 descriptor requests and a 25-minute idle test:

extern "C" { extern void (*gpf_isr)(void); }
static void (*coreUSBInterrupt)(void) = nullptr;

static void usbInterruptHook(void) {
  if (UOTGHS->UOTGHS_DEVEPTISR[0] & UOTGHS_DEVEPTISR_RXSTPI) {
    UDD_ClearOUT(); // EP0's read offset back to the start of its FIFO, before the core reads the setup packet
  }
  coreUSBInterrupt();
}

void setup() {
  noInterrupts();
  coreUSBInterrupt = gpf_isr;
  gpf_isr = usbInterruptHook;
  interrupts();
  // ...
}
Reproducer: due_setup_overflow_repro.py
"""Reproducer: an Arduino Due's native USB port stops answering after 4096 USB control requests since its serial port
was last opened (holding the port open does not prevent it: only an open resets the count).

Windows only, Python 3, standard library only. Run it with the Due plugged into its native USB port, running any sketch
(Blink is enough), after pressing RESET or after opening and closing its serial port:

    python due_setup_overflow_repro.py            # sends 5000 requests
    python due_setup_overflow_repro.py 3000       # stays below the limit: the Due keeps working

It reads the Due's product string (GET_DESCRIPTOR, string index 2) through the USB hub, as device-listing tools do
(IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION, no administrator rights needed), and reports the first request that
fails. With ArduinoCore-sam 1.6.12 the first failure comes at about request 4090: 4096 setup packets since the last
reset or port open, less the few sent at enumeration or by the open. After that the Due's serial port cannot be opened
(Windows: "A device attached to the system is not functioning", or an open that takes 60 s and then fails), and uploads
fail at the 1200 baud touch, until RESET is pressed.
"""
import ctypes
import subprocess
import sys
from ctypes import wintypes

DUE_HARDWARE_ID = r"USB\VID_2341&PID_003E"  # Arduino Due native USB port (composite device)
IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION = 0x220410
GUID_DEVINTERFACE_USB_HUB = "{f18a0e88-c30c-11d0-8815-00a0c906bed8}"


def find_due():
    """Returns (hub instance ID, port number) of the first Arduino Due native port found."""
    script = (
        "$d = Get-PnpDevice -PresentOnly | Where-Object { $_.InstanceId -like '%s\\*' -and $_.InstanceId -notlike '*&MI_*' }"
        " | Select-Object -First 1;"
        "if ($d) { (Get-PnpDeviceProperty -InstanceId $d.InstanceId -KeyName DEVPKEY_Device_Parent).Data;"
        " (Get-PnpDeviceProperty -InstanceId $d.InstanceId -KeyName DEVPKEY_Device_Address).Data }"
    ) % DUE_HARDWARE_ID
    out = subprocess.run(["powershell", "-NoProfile", "-Command", script], capture_output=True, text=True).stdout.split()
    if len(out) < 2:
        sys.exit("No Arduino Due native USB port found (VID 2341, PID 003E).")
    return out[0], int(out[1])


def open_hub(hub_instance_id):
    kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
    kernel32.CreateFileW.restype = wintypes.HANDLE
    kernel32.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, wintypes.LPVOID,
                                     wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE]
    kernel32.DeviceIoControl.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.LPVOID, wintypes.DWORD,
                                         wintypes.LPVOID, wintypes.DWORD, ctypes.POINTER(wintypes.DWORD),
                                         wintypes.LPVOID]
    path = "\\\\?\\" + hub_instance_id.replace("\\", "#") + "#" + GUID_DEVINTERFACE_USB_HUB
    handle = kernel32.CreateFileW(path, 0x40000000, 0x2, None, 3, 0, None)  # GENERIC_WRITE, FILE_SHARE_WRITE, OPEN_EXISTING
    if handle in (None, wintypes.HANDLE(-1).value):
        sys.exit(f"Could not open the hub {path} (error {ctypes.get_last_error()}).")
    return kernel32, handle


def get_string_descriptor(kernel32, hub, port, index):
    """One GET_DESCRIPTOR(string) control request to the device on a hub port. Returns the string, or None on failure."""
    setup = bytes([0x80, 0x06, index, 0x03, 0x09, 0x04, 0xFF, 0x00])  # bmRequestType, bRequest, wValue, wIndex, wLength
    buffer = (ctypes.c_ubyte * (12 + 255))()
    ctypes.memmove(buffer, port.to_bytes(4, "little") + setup, 12)  # USB_DESCRIPTOR_REQUEST: ConnectionIndex, SetupPacket
    returned = wintypes.DWORD(0)
    if not kernel32.DeviceIoControl(hub, IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION, buffer, len(buffer),
                                    buffer, len(buffer), ctypes.byref(returned), None):
        return None
    return bytes(buffer[14:returned.value]).decode("utf-16-le", "replace")


def main():
    count = int(sys.argv[1]) if len(sys.argv) > 1 else 5000
    hub_id, port = find_due()
    kernel32, hub = open_hub(hub_id)
    print(f"Arduino Due on port {port} of {hub_id}")
    print(f"First reply: {get_string_descriptor(kernel32, hub, port, 2)!r}")
    first_failure = None
    failures = 0
    for i in range(2, count + 1):
        if get_string_descriptor(kernel32, hub, port, 2) is None:
            failures += 1
            if first_failure is None:
                first_failure = i
    print(f"{count} requests: {failures} failed; first failure at request {first_failure}")
    if first_failure is None:
        print("The Due answered every request.")
        return 0
    print("Bug reproduced: the Due no longer answers USB control requests. Opening its serial port or uploading now "
          "fails until RESET is pressed.")
    return 1


if __name__ == "__main__":
    sys.exit(main())
Lenguaje dominante
HTML
Estrellas
91
Forks
112
Métricas de merge de PR
Sin PR fusionados en 30 d

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de arduino/ArduinoCore-sam

Todos los issues de arduino/ArduinoCore-sam

Issues similares

Más issues de Embedded & IoT

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.