Hi,
I have an application using the MCUXpresso SDK USB stack (v1.6.3) as a high-speed device on a K26FN2M0VMD18 custom board. All of the USB examples have some example application code to update the USB descriptors based on negotiated speed in the USB_DeviceCallback() function (I have pasted some code here from the usb_device_hid_mouse example):
case kUSB_DeviceEventBusReset:
{
/* USB bus reset signal detected */
g_UsbDeviceHidMouse.attach = 0U;
error = kStatus_USB_Success;
#if (defined(USB_DEVICE_CONFIG_EHCI) && (USB_DEVICE_CONFIG_EHCI > 0U)) || \
(defined(USB_DEVICE_CONFIG_LPCIP3511HS) && (USB_DEVICE_CONFIG_LPCIP3511HS > 0U))
/* Get USB speed to configure the device, including max packet size and interval of the endpoints. */
if (kStatus_USB_Success == USB_DeviceClassGetSpeed(CONTROLLER_ID, &g_UsbDeviceHidMouse.speed))
{
USB_DeviceSetSpeed(handle, g_UsbDeviceHidMouse.speed);
}
#endif
}
The problem is that when I connect my board to a macOS machine, USB_DeviceClassGetSpeed() returns the wrong value. It returns USB_SPEED_FULL when a USB protocol analyzer and apps on the USB host both confirm that the bus has negotiated to high speed. When connected to Windows, the device is able to properly recognize that it is connected at high speed.
This is a big problem because when using USB bulk transfers, the descriptors NEED to be different for full or high speed to avoid breaking the USB spec (the set of allowed values for wMaxPacketSize in the Configuration Descriptor is mutually exclusive for full and high speed operation).
I dug down into the EHCI code to try to find the cause of the issue. I found a few interesting things. There are two interrupts that happen when a USB host issues a bus reset: USB Reset (USBHS_USBSTS[URI]) and Port Change Detect (USBHS_USBSTS[PCI]). Here is the code in usb_device_ehci.c for the USB Reset handler:
/*!
* @brief Handle the reset interrupt.
*
* The function is used to handle the reset interrupt.
*
* @param ehciState Pointer of the device EHCI state structure.
*
*/
static void USB_DeviceEhciInterruptReset(usb_device_ehci_state_struct_t *ehciState)
{
uint32_t status = 0U;
uint32_t timeout = 0U;
/* Clear the setup flag */
status = ehciState->registerBase->EPSETUPSR;
ehciState->registerBase->EPSETUPSR = status;
/* Clear the endpoint complete flag */
status = ehciState->registerBase->EPCOMPLETE;
ehciState->registerBase->EPCOMPLETE = status;
do
{
/* Flush the pending transfers */
ehciState->registerBase->EPFLUSH = USBHS_EPFLUSH_FERB_MASK | USBHS_EPFLUSH_FETB_MASK;
} while (ehciState->registerBase->EPPRIME & (USBHS_EPPRIME_PERB_MASK | USBHS_EPPRIME_PETB_MASK));
while (ehciState->registerBase->PORTSC1 & USBHS_PORTSC1_PR_MASK)
{
timeout++;
if (timeout > 10000000)
break;
}
/* Whether is the port reset. If yes, set the isResetting flag. Or, notify the up layer. */
if (ehciState->registerBase->PORTSC1 & USBHS_PORTSC1_PR_MASK)
{
ehciState->isResetting = 1U;
}
else
{
usb_device_callback_message_struct_t message;
message.buffer = (uint8_t *)NULL;
message.code = kUSB_DeviceNotifyBusReset;
message.length = 0U;
message.isSetup = 0U;
USB_DeviceNotificationTrigger(ehciState->deviceHandle, &message);
}
}
Two things I noticed:
After that callback executes, the Port Change interrupt comes in:
/*!
* @brief Handle the port status change interrupt.
*
* The function is used to handle the port status change interrupt.
*
* @param ehciState Pointer of the device EHCI state structure.
*
*/
static void USB_DeviceEhciInterruptPortChange(usb_device_ehci_state_struct_t *ehciState)
{
usb_device_callback_message_struct_t message;
message.buffer = (uint8_t *)NULL;
message.length = 0U;
message.isSetup = 0U;
/* Whether the port is doing reset. */
if (!(ehciState->registerBase->PORTSC1 & USBHS_PORTSC1_PR_MASK))
{
/* If not, update the USB speed. */
if (ehciState->registerBase->PORTSC1 & USBHS_PORTSC1_HSP_MASK)
{
ehciState->speed = USB_SPEED_HIGH;
}
else
{
ehciState->speed = USB_SPEED_FULL;
}
/* If the device reset flag is non-zero, notify the up layer the device reset finished. */
if (ehciState->isResetting)
{
message.code = kUSB_DeviceNotifyBusReset;
USB_DeviceNotificationTrigger(ehciState->deviceHandle, &message);
ehciState->isResetting = 0U;
}
}
#if (defined(USB_DEVICE_CONFIG_LOW_POWER_MODE) && (USB_DEVICE_CONFIG_LOW_POWER_MODE > 0U))
if ((ehciState->isSuspending) && (!(ehciState->registerBase->PORTSC1 & USBHS_PORTSC1_SUSP_MASK)))
{
/* Set the resume flag */
ehciState->isSuspending = 0U;
message.code = kUSB_DeviceNotifyResume;
USB_DeviceNotificationTrigger(ehciState->deviceHandle, &message);
}
#endif /* USB_DEVICE_CONFIG_LOW_POWER_MODE */
}
In my application this interrupt executes directly after the reset interrupt, and it correctly updates the speed to USB_SPEED_HIGH, but there is no subsequent application callback (because the ehciState->isResetting flag is set to 0) and thus the descriptors are not updated.
The only reason that my device has worked with Windows thus far is that Windows issues an extra bus reset (not required by USB 2.0) as part of its enumeration process, and macOS does not. Therefore on the second reset from Windows, USB_SPEED_HIGH is stored as part of the ehciState variable from the first reset, and the application code updates its descriptors for high speed. (Technically, this is acting on outdated information; if after the second reset the bus was to somehow negotiate to full speed, the descriptors would still be updated for high speed!)
The K26 Sub-Family Reference Manual, section 53.5.3.1.1 lists software steps for a USB Device upon receiving a bus reset. The last two steps reference the Port Change interrupt:
6. At this time, the DCD may release control back to the OS because no further changes
to the device controller are permitted until a port change detect is indicated.7. After a port change detect, the device has reached the default state and the DCD can
read the PORTSCn register to determine if the device operates in FS or HS mode. At
this time, the device controller has reached normal operating mode and DCD can
begin enumeration according to the chapter 9 Device Framework of the USB
specification.
It seems like the code should probably be waiting until the Port Change interrupt is received, and determining the bus speed before sending the kUSB_DeviceEventBusReset callback. This would also make the spin wait in the Reset interrupt handler unnecessary. The minimum time for a bus reset is 10ms, but it could last quite a bit longer, especially if multiple devices are being enumerated at the same time.
I put a quick patch in my copy of the stack which will allow my project to release on time, but I am interested to know what the USB stack developers have to say about this.
Thanks,
Sam