summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhathach <[email protected]>2026-08-12 23:05:59 +0700
committerhathach <[email protected]>2026-08-13 11:45:36 +0700
commit91fbbd192ca9539221d3dc096f00ce77836a5d3d (patch)
tree801ad543cc2608eb5804c3d3d0fd547246d08d83
parenta52562b2be7ea728a176ae94d8a18d9ae0a4423a (diff)
usbd: clear endpoint busy/claimed when a completion event is dropped
An XFER_COMPLETE dropped by a full event queue leaves its endpoint's BUSY|CLAIMED state set forever - the consumer that normally clears it never sees the event, so usbd_edpt_claim()/usbd_edpt_xfer() fail from then on and the class never re-arms the endpoint. Clear both flags when the enqueue fails: the completion is lost either way, but the endpoint stays usable. Unit test: arm a bulk endpoint, drop its completion against a full queue, verify the endpoint can be claimed and re-armed.
-rw-r--r--src/device/usbd.c16
-rw-r--r--test/unit-test/test/device/usbd/test_usbd.c47
2 files changed, 59 insertions, 4 deletions
diff --git a/src/device/usbd.c b/src/device/usbd.c
index 79802e70f..f5c3046d6 100644
--- a/src/device/usbd.c
+++ b/src/device/usbd.c
@@ -1475,10 +1475,18 @@ TU_ATTR_FAST_FUNC void dcd_event_handler(dcd_event_t const* event, bool in_isr)
break;
}
- if (send && !queue_event(event, in_isr) && event->event_id == DCD_EVENT_SETUP_RECEIVED) {
- // dropped by a full queue: undo the increment, else every later SETUP is skipped as
- // "other SETUP in queue" and EP0 is deaf until re-init
- _usbd_queued_setup--;
+ if (send && !queue_event(event, in_isr)) {
+ // event dropped by a full queue: undo state that would otherwise wedge permanently
+ if (event->event_id == DCD_EVENT_SETUP_RECEIVED) {
+ // undo the increment, else every later SETUP is skipped as "other SETUP in queue"
+ // and EP0 is deaf until re-init
+ _usbd_queued_setup--;
+ } else if (event->event_id == DCD_EVENT_XFER_COMPLETE) {
+ // clear busy + claimed, else the endpoint can never be claimed or re-armed again
+ uint8_t const epnum = tu_edpt_number(event->xfer_complete.ep_addr);
+ uint8_t const ep_dir = tu_edpt_dir(event->xfer_complete.ep_addr);
+ _usbd_dev.ep_status[epnum][ep_dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED);
+ }
}
}
diff --git a/test/unit-test/test/device/usbd/test_usbd.c b/test/unit-test/test/device/usbd/test_usbd.c
index 935a20221..849097326 100644
--- a/test/unit-test/test/device/usbd/test_usbd.c
+++ b/test/unit-test/test/device/usbd/test_usbd.c
@@ -29,6 +29,7 @@
#include "tusb_fifo.h"
#include "tusb.h"
#include "usbd.h"
+#include "device/usbd_pvt.h"
TEST_SOURCE_FILE("usbd.c")
// Mock File
@@ -309,6 +310,52 @@ void test_usbd_setup_dropped_by_full_queue_recovers(void)
}
//--------------------------------------------------------------------+
+// Transfer completion dropped by full event queue
+//--------------------------------------------------------------------+
+
+// When the event queue is full, queue_event() drops the XFER_COMPLETE event. The endpoint's
+// busy/claimed state must not survive the dropped completion: a leaked BUSY makes every later
+// usbd_edpt_claim()/usbd_edpt_xfer() on that endpoint fail, so the class never re-arms it.
+void test_usbd_xfer_complete_dropped_by_full_queue_recovers(void)
+{
+ // fillers drain through usbd_reset -> class reset
+ mscd_reset_Ignore();
+
+ // open + claim + arm a bulk OUT endpoint the way a class driver would
+ tusb_desc_endpoint_t desc_ep = {
+ .bLength = sizeof(tusb_desc_endpoint_t),
+ .bDescriptorType = TUSB_DESC_ENDPOINT,
+ .bEndpointAddress = 0x01,
+ .bmAttributes = { .xfer = TUSB_XFER_BULK },
+ .wMaxPacketSize = 64,
+ .bInterval = 0
+ };
+ static uint8_t xfer_buf[64];
+
+ dcd_edpt_open_ExpectAndReturn(rhport, &desc_ep, true);
+ TEST_ASSERT_TRUE(usbd_edpt_open(rhport, &desc_ep));
+ TEST_ASSERT_TRUE(usbd_edpt_claim(rhport, 0x01));
+ dcd_edpt_xfer_ExpectAndReturn(rhport, 0x01, xfer_buf, 64, false, true);
+ TEST_ASSERT_TRUE(usbd_edpt_xfer(rhport, 0x01, xfer_buf, 64, false));
+
+ // fill the queue to the brim, then complete the transfer: queue_event() drops it
+ for (unsigned i = 0; i < CFG_TUD_TASK_QUEUE_SZ; i++) {
+ dcd_event_bus_signal(rhport, DCD_EVENT_UNPLUGGED, false);
+ }
+ dcd_event_xfer_complete(rhport, 0x01, 64, XFER_RESULT_SUCCESS, false);
+
+ // the endpoint must be re-armable: the dropped completion must not leak busy/claimed
+ TEST_ASSERT_TRUE(usbd_edpt_claim(rhport, 0x01));
+ dcd_edpt_xfer_ExpectAndReturn(rhport, 0x01, xfer_buf, 64, false, true);
+ TEST_ASSERT_TRUE(usbd_edpt_xfer(rhport, 0x01, xfer_buf, 64, false));
+
+ // drain the fillers so later tests start from an empty queue
+ for (unsigned i = 0; i < (CFG_TUD_TASK_QUEUE_SZ / CFG_TUD_TASK_EVENTS_PER_RUN) + 1; i++) {
+ tud_task();
+ }
+}
+
+//--------------------------------------------------------------------+
// Control OUT data stage host overrun
//--------------------------------------------------------------------+