usbd: hold OUT endpoints with RX_PENDING until the buffer is consumed (#3937)
XFER_COMPLETE cleared BUSY|CLAIMED before the class copied ep_buf, so another
task could claim and re-arm the endpoint into an unconsumed buffer (#1292).
Completion now sets RX_PENDING in the same store; claim() refuses it until the
class calls usbd_edpt_rx_consume() or re-arms with usbd_edpt_xfer(). Applies to
every build, not only OSAL_MUTEX_REQUIRED.
Also: mtp example supports FreeRTOS on ESP-IDF; test_ci_select reads sources as UTF-8.
diff --git a/examples/device/mtp/src/CMakeLists.txt b/examples/device/mtp/src/CMakeLists.txt
new file mode 100644
index 0000000..5d01524
--- /dev/null
+++ b/examples/device/mtp/src/CMakeLists.txt
@@ -0,0 +1,6 @@
+# This file is for ESP-IDF only
+# WHOLE_ARCHIVE: mtp_fs_example.c only overrides weak tud_mtp_*_cb, so the linker would never pull it in
+idf_component_register(SRCS "main.c" "mtp_fs_example.c" "usb_descriptors.c"
+ INCLUDE_DIRS "."
+ REQUIRES boards tinyusb_src
+ WHOLE_ARCHIVE)
diff --git a/examples/device/mtp/src/main.c b/examples/device/mtp/src/main.c
index 6ffa435..71a3890 100644
--- a/examples/device/mtp/src/main.c
+++ b/examples/device/mtp/src/main.c
@@ -47,12 +47,20 @@
static uint32_t blink_interval_ms = BLINK_NOT_MOUNTED;
-void led_blinking_task(void);
+void led_blinking_task(void* param);
+
+#if CFG_TUSB_OS == OPT_OS_FREERTOS
+void usb_device_task(void* param);
+void freertos_init(void);
+#endif
/*------------- MAIN -------------*/
int main(void) {
board_init();
+#if CFG_TUSB_OS == OPT_OS_FREERTOS
+ freertos_init();
+#else
// init device stack on configured roothub port
tusb_rhport_init_t dev_init = {
.role = TUSB_ROLE_DEVICE,
@@ -63,8 +71,9 @@
while (1) {
tud_task(); // tinyusb device task
- led_blinking_task();
+ led_blinking_task(NULL);
}
+#endif
}
//--------------------------------------------------------------------+
@@ -97,14 +106,85 @@
//--------------------------------------------------------------------+
// BLINKING TASK
//--------------------------------------------------------------------+
-void led_blinking_task(void) {
- static uint32_t start_ms = 0;
+void led_blinking_task(void* param) {
+ (void) param;
static bool led_state = false;
+#if CFG_TUSB_OS == OPT_OS_FREERTOS
+ while (1) {
+ vTaskDelay(blink_interval_ms / portTICK_PERIOD_MS);
+ board_led_write(led_state);
+ led_state = 1 - led_state; // toggle
+ }
+#else
+ static uint32_t start_ms = 0;
+
// Blink every interval ms
if (tusb_time_millis_api() - start_ms < blink_interval_ms) return; // not enough time
start_ms += blink_interval_ms;
board_led_write(led_state);
led_state = 1 - led_state; // toggle
+#endif
}
+
+//--------------------------------------------------------------------+
+// FreeRTOS
+//--------------------------------------------------------------------+
+#if CFG_TUSB_OS == OPT_OS_FREERTOS
+
+#define BLINKY_STACK_SIZE configMINIMAL_STACK_SIZE
+
+#ifdef ESP_PLATFORM
+ #define USBD_STACK_SIZE 4096
+ void app_main(void) {
+ main();
+ }
+#else
+ // Increase stack size when debug log is enabled
+ #define USBD_STACK_SIZE ((3*configMINIMAL_STACK_SIZE/2) * (CFG_TUSB_DEBUG ? 2 : 1))
+#endif
+
+#if configSUPPORT_STATIC_ALLOCATION
+StackType_t blinky_stack[BLINKY_STACK_SIZE];
+StaticTask_t blinky_taskdef;
+
+StackType_t usb_device_stack[USBD_STACK_SIZE];
+StaticTask_t usb_device_taskdef;
+#endif
+
+// USB Device Driver task: processes all usb events and invokes callbacks
+void usb_device_task(void* param) {
+ (void) param;
+
+ // init device stack on configured roothub port. Must be called after the
+ // scheduler starts: the USB IRQ handler uses RTOS queue APIs.
+ tusb_rhport_init_t dev_init = {
+ .role = TUSB_ROLE_DEVICE,
+ .speed = TUSB_SPEED_AUTO
+ };
+ tusb_init(BOARD_TUD_RHPORT, &dev_init);
+ board_init_after_tusb();
+
+ while (1) {
+ tud_task(); // put thread to waiting state until there is a new event
+ }
+}
+
+void freertos_init(void) {
+ #if configSUPPORT_STATIC_ALLOCATION
+ TU_ASSERT(xTaskCreateStatic(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1,
+ blinky_stack, &blinky_taskdef) != NULL, );
+ TU_ASSERT(xTaskCreateStatic(usb_device_task, "usbd", USBD_STACK_SIZE, NULL, configMAX_PRIORITIES - 1,
+ usb_device_stack, &usb_device_taskdef) != NULL, );
+ #else
+ TU_ASSERT(xTaskCreate(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, NULL) == pdPASS, );
+ TU_ASSERT(xTaskCreate(usb_device_task, "usbd", USBD_STACK_SIZE, NULL, configMAX_PRIORITIES - 1, NULL) == pdPASS, );
+ #endif
+
+ // espressif starts the scheduler before app_main
+ #ifndef ESP_PLATFORM
+ vTaskStartScheduler();
+ #endif
+}
+#endif
diff --git a/examples/device/mtp/src/tusb_config.h b/examples/device/mtp/src/tusb_config.h
index 5224cd7..2bf64ff 100644
--- a/examples/device/mtp/src/tusb_config.h
+++ b/examples/device/mtp/src/tusb_config.h
@@ -57,6 +57,11 @@
#define CFG_TUSB_OS OPT_OS_NONE
#endif
+// Espressif IDF requires "freertos/" prefix in include path
+#ifdef ESP_PLATFORM
+#define CFG_TUSB_OS_INC_PATH freertos/
+#endif
+
#ifndef CFG_TUSB_DEBUG
#define CFG_TUSB_DEBUG 0
#endif
diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c
index b0a9e25..1b06331 100644
--- a/src/class/midi/midi2_device.c
+++ b/src/class/midi/midi2_device.c
@@ -989,6 +989,8 @@
_nego_process_rx(p_midi);
}
tud_midi2_rx_cb(idx);
+ } else {
+ tu_edpt_stream_read_xfer_complete(ep_rx, 0); // release ep_buf for the re-arm below
}
tu_edpt_stream_read_xfer(ep_rx);
} else if (ep_addr == ep_tx->ep_addr && result == XFER_RESULT_SUCCESS) {
diff --git a/src/class/midi/midi_device.c b/src/class/midi/midi_device.c
index 8a12221..2be56a9 100644
--- a/src/class/midi/midi_device.c
+++ b/src/class/midi/midi_device.c
@@ -534,6 +534,8 @@
if (result == XFER_RESULT_SUCCESS) {
tu_edpt_stream_read_xfer_complete(ep_st_rx, xferred_bytes);
tud_midi_rx_cb(idx); // invoke callback
+ } else {
+ tu_edpt_stream_read_xfer_complete(ep_st_rx, 0); // release ep_buf for the re-arm below
}
tu_edpt_stream_read_xfer(ep_st_rx); // prepare for next data
} else if (ep_addr == ep_st_tx->ep_addr && result == XFER_RESULT_SUCCESS) {
diff --git a/src/class/mtp/mtp_device.c b/src/class/mtp/mtp_device.c
index 275c9f8..f867bb3 100644
--- a/src/class/mtp/mtp_device.c
+++ b/src/class/mtp/mtp_device.c
@@ -215,6 +215,7 @@
TU_LOG_DRV(" MTP Data OUT: xferred_len/total_len=%lu/%lu, xact_len=%u\r\n", p_mtp->xferred_len, p_mtp->total_len,
xact_len);
+ usbd_edpt_rx_consume(p_mtp->rhport, p_mtp->ep_out); // arming a new receive relinquishes the previous data
TU_VERIFY(usbd_edpt_claim(p_mtp->rhport, p_mtp->ep_out));
TU_ASSERT(usbd_edpt_xfer(p_mtp->rhport, p_mtp->ep_out, _mtpd_epbuf.buf, xact_len, false));
return true;
@@ -475,6 +476,7 @@
if (need_zlp) {
TU_LOG_DRV(" queue ZLP\r\n");
+ usbd_edpt_rx_consume(p_mtp->rhport, ep_addr); // arming a new receive relinquishes the previous data
TU_VERIFY(usbd_edpt_claim(p_mtp->rhport, ep_addr));
TU_ASSERT(usbd_edpt_xfer(p_mtp->rhport, ep_addr, NULL, 0, false));
return true;
diff --git a/src/class/vendor/vendor_device.c b/src/class/vendor/vendor_device.c
index c4a550e..595513c 100644
--- a/src/class/vendor/vendor_device.c
+++ b/src/class/vendor/vendor_device.c
@@ -224,6 +224,7 @@
TU_ATTR_UNUSED static inline bool vendord_ep_read_xfer(vendord_interface_t *p_itf, uint8_t ep, uint8_t *epbuf, uint16_t xfer_len) {
TU_VERIFY(ep > 0); // must be opened
+ usbd_edpt_rx_consume(p_itf->rhport, ep); // arming a new receive relinquishes the previous data
TU_VERIFY(usbd_edpt_claim(p_itf->rhport, ep));
return usbd_edpt_xfer(p_itf->rhport, ep, epbuf, xfer_len, false);
}
diff --git a/src/common/tusb_private.h b/src/common/tusb_private.h
index b91fc06..f45273f 100644
--- a/src/common/tusb_private.h
+++ b/src/common/tusb_private.h
@@ -31,6 +31,7 @@
#define TU_EDPT_STATE_BUSY 0x01u
#define TU_EDPT_STATE_STALLED 0x02u
#define TU_EDPT_STATE_CLAIMED 0x04u
+#define TU_EDPT_STATE_RX_PENDING 0x08u // OUT xfer completed, ep buffer not yet consumed by the class (#1292)
typedef struct {
uint8_t hwid; // device: rhport, host: daddr
@@ -144,12 +145,22 @@
// Start an usb transfer if endpoint is not busy
uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s);
+#if CFG_TUD_ENABLED
+// Release an OUT endpoint's RX_PENDING hold once its buffer is consumed, allowing it to be claimed
+void usbd_edpt_rx_consume(uint8_t rhport, uint8_t ep_addr);
+#else
+ #define usbd_edpt_rx_consume(_rhport, _ep_addr)
+#endif
+
// Complete read transfer by writing EP -> FIFO. Must be called in the transfer complete callback
TU_ATTR_ALWAYS_INLINE static inline
void tu_edpt_stream_read_xfer_complete(tu_edpt_stream_t* s, uint32_t xferred_bytes) {
if (s->ep_buf != NULL) {
tu_fifo_write_n(&s->ff, s->ep_buf, (uint16_t)xferred_bytes);
}
+ if (!s->is_host) {
+ usbd_edpt_rx_consume(s->hwid, s->ep_addr);
+ }
}
// Complete read transfer with provided buffer
diff --git a/src/device/usbd.c b/src/device/usbd.c
index 21d865c..f702bce 100644
--- a/src/device/usbd.c
+++ b/src/device/usbd.c
@@ -753,10 +753,19 @@
TU_LOG_USBD("on EP %02X with %u bytes\r\n", ep_addr, (unsigned int) event.xfer_complete.len);
- // Clear busy + claimed
- _usbd_dev.ep_status[epnum][ep_dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED);
+ usbd_class_driver_t const* driver = NULL; // EP0 has no class driver
+ if (0 != epnum) {
+ driver = get_driver(_usbd_dev.ep2drv[epnum][ep_dir]);
+ TU_ASSERT(driver,);
+ }
- if (0 == epnum) {
+ // Clear busy + claimed. An OUT endpoint changed to RX_PENDING, so no other task can claim and re-arm it before
+ // its buffer is consumed
+ uint8_t const rx_pending = (0 != epnum && ep_dir == TUSB_DIR_OUT) ? TU_EDPT_STATE_RX_PENDING : 0u;
+ _usbd_dev.ep_status[epnum][ep_dir] = (uint8_t) (
+ (_usbd_dev.ep_status[epnum][ep_dir] & ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED)) | rx_pending);
+
+ if (driver == NULL) {
// Not stalled on failure: a DCD refuses an EP0 prime when a newer setup is already
// latched, and EP0 stalls are cleared by hardware when that setup arrives - so a stall
// issued here lands after the auto-clear and would stall the transfer that superseded
@@ -766,9 +775,6 @@
TU_LOG_USBD(" Control stage not continued\r\n");
}
} else {
- usbd_class_driver_t const* driver = get_driver(_usbd_dev.ep2drv[epnum][ep_dir]);
- TU_ASSERT(driver,);
-
TU_LOG_USBD(" %s xfer callback\r\n", driver->name);
driver->xfer_cb(event.rhport, ep_addr, (xfer_result_t) event.xfer_complete.result, event.xfer_complete.len);
}
@@ -1589,6 +1595,21 @@
return tu_edpt_release(&_usbd_dev.ep_status[epnum][dir], _usbd_mutex);
}
+void usbd_edpt_rx_consume(uint8_t rhport, uint8_t ep_addr) {
+ (void) rhport;
+
+ uint8_t const epnum = tu_edpt_number(ep_addr);
+ uint8_t const dir = tu_edpt_dir(ep_addr);
+ volatile uint8_t *ep_state = &_usbd_dev.ep_status[epnum][dir];
+
+ TU_VERIFY((*ep_state & TU_EDPT_STATE_RX_PENDING) != 0,);
+ (void) osal_mutex_lock(_usbd_mutex, OSAL_TIMEOUT_WAIT_FOREVER);
+ if ((*ep_state & TU_EDPT_STATE_RX_PENDING) != 0) {
+ *ep_state &= (uint8_t) ~TU_EDPT_STATE_RX_PENDING;
+ }
+ (void) osal_mutex_unlock(_usbd_mutex);
+}
+
bool usbd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t* buffer, uint16_t total_bytes, bool is_isr) {
rhport = _usbd_rhport;
@@ -1610,7 +1631,8 @@
// Set busy first since the actual transfer can be complete before dcd_edpt_xfer()
// could return and USBD task can preempt and clear the busy
- _usbd_dev.ep_status[epnum][dir] |= TU_EDPT_STATE_BUSY;
+ _usbd_dev.ep_status[epnum][dir] =
+ (uint8_t) ((_usbd_dev.ep_status[epnum][dir] & ~TU_EDPT_STATE_RX_PENDING) | TU_EDPT_STATE_BUSY);
if (dcd_edpt_xfer(rhport, ep_addr, buffer, total_bytes, is_isr)) {
return true;
@@ -1643,7 +1665,8 @@
// Set busy first since the actual transfer can be complete before dcd_edpt_xfer() could return
// and usbd task can preempt and clear the busy
- _usbd_dev.ep_status[epnum][dir] |= TU_EDPT_STATE_BUSY;
+ _usbd_dev.ep_status[epnum][dir] =
+ (uint8_t) ((_usbd_dev.ep_status[epnum][dir] & ~TU_EDPT_STATE_RX_PENDING) | TU_EDPT_STATE_BUSY);
if (dcd_edpt_xfer_fifo(rhport, ep_addr, ff, total_bytes, is_isr)) {
TU_LOG_USBD("OK\r\n");
diff --git a/src/device/usbd_pvt.h b/src/device/usbd_pvt.h
index abf136e..a68e7f4 100644
--- a/src/device/usbd_pvt.h
+++ b/src/device/usbd_pvt.h
@@ -74,6 +74,8 @@
// Claim an endpoint before submitting a transfer.
// If caller does not make any transfer, it must release endpoint for others.
+// From an OUT xfer_cb, call usbd_edpt_rx_consume() (common/tusb_private.h) before claiming, or re-arm with
+// usbd_edpt_xfer() directly: the claim is refused until the buffer is consumed.
bool usbd_edpt_claim(uint8_t rhport, uint8_t ep_addr);
// Release claimed endpoint without submitting a transfer
diff --git a/src/tusb.c b/src/tusb.c
index e1548e8..ca298cd 100644
--- a/src/tusb.c
+++ b/src/tusb.c
@@ -209,11 +209,11 @@
(void) mutex;
// pre-check to help reducing mutex lock
- TU_VERIFY((*ep_state & (TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED)) == 0);
+ TU_VERIFY((*ep_state & (TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED | TU_EDPT_STATE_RX_PENDING)) == 0);
(void) osal_mutex_lock(mutex, OSAL_TIMEOUT_WAIT_FOREVER);
- // can only claim the endpoint if it is not busy and not claimed yet.
- bool const available = (*ep_state & (TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED)) == 0;
+ // can only claim the endpoint if it is not busy, not claimed yet and its received data is consumed.
+ bool const available = (*ep_state & (TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED | TU_EDPT_STATE_RX_PENDING)) == 0;
if (available) {
*ep_state |= TU_EDPT_STATE_CLAIMED;
}
diff --git a/test/hil/test/test_ci_select.py b/test/hil/test/test_ci_select.py
index 6f8be2f..8028e5a 100644
--- a/test/hil/test/test_ci_select.py
+++ b/test/hil/test/test_ci_select.py
@@ -31,7 +31,8 @@
def _read(path):
- with open(path) as f:
+ # Fixed encoding, like ci_select._read: a non-UTF-8 locale would fail on tracked non-ASCII bytes.
+ with open(path, encoding='utf-8', errors='replace') as f:
return f.read()
@@ -1680,8 +1681,8 @@
def test_espressif_prunes_to_what_its_build_path_can_build(self):
# build.py's espressif branch builds get_examples('espressif') only (the
# *_freertos examples plus a short extra list), so keeping espressif for a
- # device/mtp diff spins CircleCI's most expensive leg up to skip everything
- s = ci_select.classify_build(['examples/device/mtp/src/main.c'], REPO)
+ # device/cdc_msc diff spins CircleCI's most expensive leg up to skip everything
+ s = ci_select.classify_build(['examples/device/cdc_msc/src/main.c'], REPO)
self.assertFalse(s['full'])
self.assertNotIn('espressif', s['families'])
diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json
index 4f8283e..250bd2b 100644
--- a/test/hil/tinyusb.json
+++ b/test/hil/tinyusb.json
@@ -57,6 +57,7 @@
"device/cdc_msc_throughput",
"device/hid_composite_freertos",
"device/audio_test_freertos",
+ "device/mtp",
"device/usbtest",
"host/device_info",
"host/msc_file_explorer_freertos"
@@ -98,6 +99,7 @@
"device/cdc_msc_throughput",
"device/hid_composite_freertos",
"device/audio_test_freertos",
+ "device/mtp",
"device/usbtest",
"host/device_info",
"host/msc_file_explorer_freertos"
diff --git a/test/unit-test/test/device/usbd/test_usbd.c b/test/unit-test/test/device/usbd/test_usbd.c
index 8490973..a221f86 100644
--- a/test/unit-test/test/device/usbd/test_usbd.c
+++ b/test/unit-test/test/device/usbd/test_usbd.c
@@ -381,3 +381,146 @@
tud_task();
}
+
+//--------------------------------------------------------------------+
+// OUT endpoint held by RX_PENDING until its buffer is consumed (#1292)
+//--------------------------------------------------------------------+
+enum {
+ EDPT_MSC_OUT = 0x01,
+ EDPT_MSC_IN = 0x81
+};
+
+uint8_t const msc_desc_configuration[] = {
+ TUD_CONFIG_DESCRIPTOR(1, 1, 0, TUD_CONFIG_DESC_LEN + TUD_MSC_DESC_LEN, 0, 100),
+ TUD_MSC_DESCRIPTOR(0, 0, EDPT_MSC_OUT, EDPT_MSC_IN, 64),
+};
+
+tusb_control_request_t const req_set_configuration = {
+ .bmRequestType = 0x00,
+ .bRequest = TUSB_REQ_SET_CONFIGURATION,
+ .wValue = 1,
+ .wIndex = 0,
+ .wLength = 0
+};
+
+static uint8_t msc_out_buf[64];
+
+// Bind the bulk endpoints to the (mocked) MSC driver, then claim and arm the OUT endpoint
+static void msc_out_armed(void) {
+ mscd_reset_Ignore();
+ dcd_event_bus_reset(rhport, TUSB_SPEED_FULL, false);
+ tud_task();
+
+ desc_configuration = msc_desc_configuration;
+ dcd_event_setup_received(rhport, (uint8_t*) &req_set_configuration, false);
+ mscd_open_ExpectAndReturn(rhport, (tusb_desc_interface_t const*) (msc_desc_configuration + TUD_CONFIG_DESC_LEN),
+ TUD_MSC_DESC_LEN, TUD_MSC_DESC_LEN);
+ dcd_edpt_xfer_ExpectAndReturn(rhport, EDPT_CTRL_IN, NULL, 0, false, true);
+ dcd_event_xfer_complete(rhport, EDPT_CTRL_IN, 0, 0, false);
+ dcd_edpt0_status_complete_ExpectWithArray(rhport, &req_set_configuration, 1);
+ tud_task();
+
+ TEST_ASSERT_TRUE(usbd_edpt_claim(rhport, EDPT_MSC_OUT));
+ dcd_edpt_xfer_ExpectAndReturn(rhport, EDPT_MSC_OUT, msc_out_buf, sizeof(msc_out_buf), false, true);
+ TEST_ASSERT_TRUE(usbd_edpt_xfer(rhport, EDPT_MSC_OUT, msc_out_buf, sizeof(msc_out_buf), false));
+}
+
+static void msc_out_complete(CMOCK_mscd_xfer_cb_CALLBACK xfer_cb) {
+ mscd_xfer_cb_Stub(xfer_cb);
+ dcd_event_xfer_complete(rhport, EDPT_MSC_OUT, sizeof(msc_out_buf), XFER_RESULT_SUCCESS, false);
+ tud_task();
+}
+
+static bool xfer_cb_claim_after_consume(uint8_t rhport_, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes,
+ int num_calls) {
+ (void) result; (void) xferred_bytes; (void) num_calls;
+ TEST_ASSERT_FALSE(usbd_edpt_busy(rhport_, ep_addr));
+ TEST_ASSERT_FALSE(usbd_edpt_claim(rhport_, ep_addr)); // buffer not yet consumed
+
+ usbd_edpt_rx_consume(rhport_, ep_addr);
+ TEST_ASSERT_TRUE(usbd_edpt_claim(rhport_, ep_addr));
+ TEST_ASSERT_TRUE(usbd_edpt_release(rhport_, ep_addr));
+ return true;
+}
+
+void test_usbd_out_complete_refuses_claim_until_consumed(void) {
+ msc_out_armed();
+ msc_out_complete(xfer_cb_claim_after_consume);
+ TEST_ASSERT_TRUE(usbd_edpt_claim(rhport, EDPT_MSC_OUT));
+}
+
+static bool xfer_cb_no_consume(uint8_t rhport_, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes,
+ int num_calls) {
+ (void) result; (void) xferred_bytes; (void) num_calls;
+ TEST_ASSERT_FALSE(usbd_edpt_claim(rhport_, ep_addr));
+ return true;
+}
+
+// xfer_cb that neither consumes nor re-arms: the hold outlives xfer_cb until the class consumes
+void test_usbd_out_complete_held_after_xfer_cb_until_consumed(void) {
+ msc_out_armed();
+ msc_out_complete(xfer_cb_no_consume);
+ TEST_ASSERT_FALSE(usbd_edpt_claim(rhport, EDPT_MSC_OUT));
+ usbd_edpt_rx_consume(rhport, EDPT_MSC_OUT);
+ TEST_ASSERT_TRUE(usbd_edpt_claim(rhport, EDPT_MSC_OUT));
+}
+
+static bool xfer_cb_consume_rearm(uint8_t rhport_, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes,
+ int num_calls) {
+ (void) result; (void) xferred_bytes; (void) num_calls;
+ usbd_edpt_rx_consume(rhport_, ep_addr);
+ TEST_ASSERT_TRUE(usbd_edpt_claim(rhport_, ep_addr));
+ dcd_edpt_xfer_ExpectAndReturn(rhport_, ep_addr, msc_out_buf, sizeof(msc_out_buf), false, true);
+ TEST_ASSERT_TRUE(usbd_edpt_xfer(rhport_, ep_addr, msc_out_buf, sizeof(msc_out_buf), false));
+ return true;
+}
+
+// a transfer re-armed inside xfer_cb stays BUSY and is not held by RX_PENDING
+void test_usbd_out_rearmed_in_xfer_cb_stays_busy(void) {
+ msc_out_armed();
+ msc_out_complete(xfer_cb_consume_rearm);
+ TEST_ASSERT_TRUE(usbd_edpt_busy(rhport, EDPT_MSC_OUT));
+ TEST_ASSERT_FALSE(usbd_edpt_claim(rhport, EDPT_MSC_OUT));
+}
+
+static bool xfer_cb_rearm_refused(uint8_t rhport_, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes,
+ int num_calls) {
+ (void) result; (void) xferred_bytes; (void) num_calls;
+ // direct re-arm without claim, as MSC does; the DCD refuses it
+ dcd_edpt_xfer_ExpectAndReturn(rhport_, ep_addr, msc_out_buf, sizeof(msc_out_buf), false, false);
+ TEST_ASSERT_FALSE(usbd_edpt_xfer(rhport_, ep_addr, msc_out_buf, sizeof(msc_out_buf), false));
+ return true;
+}
+
+// a failed re-arm leaves the endpoint idle: neither BUSY nor still held by RX_PENDING
+void test_usbd_out_failed_rearm_in_xfer_cb_stays_idle(void) {
+ msc_out_armed();
+ msc_out_complete(xfer_cb_rearm_refused);
+ TEST_ASSERT_FALSE(usbd_edpt_busy(rhport, EDPT_MSC_OUT));
+ TEST_ASSERT_TRUE(usbd_edpt_claim(rhport, EDPT_MSC_OUT));
+}
+
+static bool xfer_cb_rearm_dropped(uint8_t rhport_, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes,
+ int num_calls) {
+ (void) result; (void) xferred_bytes; (void) num_calls;
+ dcd_edpt_xfer_ExpectAndReturn(rhport_, ep_addr, msc_out_buf, sizeof(msc_out_buf), false, true);
+ TEST_ASSERT_TRUE(usbd_edpt_xfer(rhport_, ep_addr, msc_out_buf, sizeof(msc_out_buf), false));
+
+ // fill the queue with events that leave endpoints alone, so the re-armed completion is dropped
+ for (unsigned i = 0; i < CFG_TUD_TASK_QUEUE_SZ; i++) {
+ dcd_event_bus_signal(rhport_, DCD_EVENT_SUSPEND, false);
+ }
+ dcd_event_xfer_complete(rhport_, ep_addr, sizeof(msc_out_buf), XFER_RESULT_SUCCESS, false);
+ return true;
+}
+
+// a re-armed transfer whose completion is dropped leaves the endpoint idle
+void test_usbd_out_dropped_rearm_in_xfer_cb_stays_idle(void) {
+ msc_out_armed();
+ msc_out_complete(xfer_cb_rearm_dropped);
+ for (unsigned i = 0; i < (CFG_TUD_TASK_QUEUE_SZ / CFG_TUD_TASK_EVENTS_PER_RUN) + 1; i++) {
+ tud_task();
+ }
+ TEST_ASSERT_FALSE(usbd_edpt_busy(rhport, EDPT_MSC_OUT));
+ TEST_ASSERT_TRUE(usbd_edpt_claim(rhport, EDPT_MSC_OUT));
+}
diff --git a/tools/build.py b/tools/build.py
index 473ecc4..4a63536 100755
--- a/tools/build.py
+++ b/tools/build.py
@@ -83,6 +83,7 @@
if family == 'espressif':
all_examples.append('device/board_test')
all_examples.append('device/cdc_msc_throughput')
+ all_examples.append('device/mtp')
all_examples.append('device/usbtest')
all_examples.append('device/video_capture')
all_examples.append('host/device_info')