summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHiFiPHile <[email protected]>2026-08-25 09:55:24 +0200
committerHiFiPHile <[email protected]>2026-09-02 10:50:02 +0200
commita0d3de76869ce728c7c1d9713085bc970da39671 (patch)
treebd24f3efb60daeda2e0aaf4df08da97504aafbf3 /src
parent278c0531a07e4fe8112ef91d0d2dbba8ef0a40b3 (diff)
parent5c0e31cdabaf37f14e1f5e988a020abfc1000495 (diff)
Merge branch 'master' into agent/fix-dwc2-host-fifo-allocation
Diffstat (limited to 'src')
-rw-r--r--src/class/midi/midi2_device.c204
-rw-r--r--src/class/midi/midi2_device.h28
-rw-r--r--src/class/mtp/mtp_device.c18
-rw-r--r--src/class/usbtmc/usbtmc_device.c21
-rw-r--r--src/class/usbtmc/usbtmc_device.h1
-rw-r--r--src/class/video/video_device.c3
-rw-r--r--src/common/tusb_mcu.h19
-rw-r--r--src/device/dcd.h26
-rw-r--r--src/device/usbd.c51
-rw-r--r--src/portable/chipidea/ci_hs/ci_hs_imxrt.h3
-rw-r--r--src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h5
-rw-r--r--src/portable/chipidea/ci_hs/ci_hs_type.h17
-rw-r--r--src/portable/chipidea/ci_hs/dcd_ci_hs.c247
-rw-r--r--src/portable/chipidea/ci_hs/hcd_ci_hs.c4
-rw-r--r--src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c106
-rw-r--r--src/portable/synopsys/dwc2/dcd_dwc2.c7
16 files changed, 620 insertions, 140 deletions
diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c
index 1d40a2efa..b0a9e2503 100644
--- a/src/class/midi/midi2_device.c
+++ b/src/class/midi/midi2_device.c
@@ -36,6 +36,9 @@ TU_ATTR_WEAK const char* tud_midi2_fb_name_cb(uint8_t itf, uint8_t fb_idx) {
TU_ATTR_WEAK tud_midi2_stream_result_t tud_midi2_stream_msg_cb(uint8_t itf, const uint32_t* ump_words) {
(void) itf; (void) ump_words; return MIDI2_STREAM_PASS;
}
+TU_ATTR_WEAK bool tud_midi2_device_identity_cb(uint8_t itf, tud_midi2_device_identity_t* identity) {
+ (void) itf; (void) identity; return false;
+}
//--------------------------------------------------------------------+
// Byte order note
@@ -59,6 +62,7 @@ enum {
enum {
STREAM_ENDPOINT_DISCOVERY = 0x000,
STREAM_ENDPOINT_INFO = 0x001,
+ STREAM_DEVICE_IDENTITY = 0x002,
STREAM_EP_NAME = 0x003,
STREAM_PROD_INSTANCE_ID = 0x004,
STREAM_CONFIG_REQUEST = 0x005,
@@ -103,6 +107,16 @@ typedef struct {
uint8_t protocol;
bool negotiated;
+ // Discovery reply bits waiting for TX FIFO room, drained on TX complete
+ uint8_t nego_pending_ep_filter;
+ uint8_t nego_pending_fb_filter;
+ uint8_t nego_pending_fb_num; // block requested by the pending discovery, 0xFF = all
+ uint8_t nego_pending_fb_next; // next block index to reply for
+ bool nego_pending_fb_restart; // restart after the active FB name when requests merge
+ uint16_t nego_text_status; // text reply owning nego_text_offset, 0 = none
+ uint16_t nego_text_offset; // progress into the text reply being sent
+ uint8_t nego_text_index; // Function Block index for an active FB name
+
/*------------- From this point, data is not cleared by bus reset -------------*/
struct {
midi2d_tx_t tx;
@@ -327,16 +341,20 @@ static void _nego_send_endpoint_info(midi2d_interface_t* p_midi) {
// index byte (the Function Block number for FB Name) and 13 chars fit per
// packet; otherwise the text starts there and 14 chars fit (Endpoint Name,
// Product Instance Id).
-static void _nego_send_stream_text(midi2d_interface_t* p_midi, uint16_t status,
- bool has_index, uint8_t index, const char* str) {
- if (!str || str[0] == '\0') return;
+// Sends a stream text from `offset` and returns how far it got. Resuming keeps
+// the End packet, which dropping the tail would lose.
+static uint16_t _nego_send_stream_text(midi2d_interface_t* p_midi, uint16_t status,
+ bool has_index, uint8_t index, const char* str,
+ uint16_t offset) {
+ if (!str || str[0] == '\0') return 0;
- uint16_t total_len = (uint16_t) strlen(str);
- uint16_t offset = 0;
+ const uint16_t total_len = (uint16_t) strlen(str);
const uint8_t per_pkt = has_index ? 13 : 14;
const uint8_t head_chars = has_index ? 1 : 2; // chars carried in word0
+ if (offset >= total_len) return total_len;
while (offset < total_len) {
+ if (tu_fifo_remaining(&p_midi->ep_stream.tx.ff) < 16) break;
uint16_t remaining = total_len - offset;
uint8_t n = (uint8_t)((remaining > per_pkt) ? per_pkt : remaining);
bool is_first = (offset == 0);
@@ -370,6 +388,7 @@ static void _nego_send_stream_text(midi2d_interface_t* p_midi, uint16_t status,
_nego_send_ump(p_midi, msg, 4);
offset += n;
}
+ return offset;
}
static void _nego_send_config_notify(midi2d_interface_t* p_midi, uint8_t protocol) {
@@ -380,6 +399,33 @@ static void _nego_send_config_notify(midi2d_interface_t* p_midi, uint8_t protoco
_nego_send_ump(p_midi, msg, 4);
}
+static void _nego_send_device_identity(midi2d_interface_t* p_midi) {
+ tud_midi2_device_identity_t id;
+ tu_memclr(&id, sizeof(id));
+ if (!tud_midi2_device_identity_cb(_itf_idx(p_midi), &id)) return;
+
+ // Every field is a run of bytes, each carrying 7 bits, laid out in the same
+ // order as the MIDI 1.0 Device Inquiry reply this message mirrors. A 1-byte
+ // manufacturer ID occupies the first of the three bytes, the other two stay
+ // zero, so the caller passes it as 0x7D0000 and not 0x00007D.
+ uint32_t msg[4] = {0};
+ msg[0] = ((uint32_t) MT_STREAM << 28)
+ | ((uint32_t) STREAM_DEVICE_IDENTITY << 16);
+ msg[1] = id.manufacturer & UINT32_C(0x7F7F7F);
+ // Family and model are 14-bit numbers sent least significant byte first,
+ // as in the Device Inquiry reply. Manufacturer above is a byte sequence
+ // rather than a number, so it keeps its own order.
+ msg[2] = ((uint32_t) (id.family & 0x7F) << 24)
+ | ((uint32_t) ((id.family >> 7) & 0x7F) << 16)
+ | ((uint32_t) (id.model & 0x7F) << 8)
+ | ((uint32_t) ((id.model >> 7) & 0x7F));
+ msg[3] = ((uint32_t) ((id.sw_revision >> 24) & 0x7F) << 24)
+ | ((uint32_t) ((id.sw_revision >> 16) & 0x7F) << 16)
+ | ((uint32_t) ((id.sw_revision >> 8) & 0x7F) << 8)
+ | ((uint32_t) (id.sw_revision & 0x7F));
+ _nego_send_ump(p_midi, msg, 4);
+}
+
static void _nego_send_fb_info(midi2d_interface_t* p_midi, uint8_t fb_idx) {
// Derive direction and group span for this block from the GTB descriptor.
uint16_t gtb_len = 0;
@@ -395,10 +441,122 @@ static void _nego_send_fb_info(midi2d_interface_t* p_midi, uint8_t fb_idx) {
| ((uint32_t) fb_idx << 8)
| _fb_dir_byte(type); // UI hint + bDirection from the GTB block type
msg[1] = ((uint32_t) first_group << 24)
- | ((uint32_t) num_groups << 16);
+ | ((uint32_t) num_groups << 16)
+ | ((uint32_t) (CFG_TUD_MIDI2_FB_CI_VERSION & 0xFF) << 8)
+ | ((uint32_t) (CFG_TUD_MIDI2_FB_SYSEX8_STREAMS & 0xFF));
_nego_send_ump(p_midi, msg, 4);
}
+static void _nego_clear_pending(midi2d_interface_t* p_midi) {
+ p_midi->nego_pending_ep_filter = 0;
+ p_midi->nego_pending_fb_filter = 0;
+ p_midi->nego_pending_fb_num = 0;
+ p_midi->nego_pending_fb_next = 0;
+ p_midi->nego_pending_fb_restart = false;
+ p_midi->nego_text_status = 0;
+ p_midi->nego_text_offset = 0;
+ p_midi->nego_text_index = 0;
+}
+
+static const char* _nego_text_cb(midi2d_interface_t* p_midi, uint16_t status, uint8_t index) {
+ const uint8_t itf = _itf_idx(p_midi);
+ switch (status) {
+ case STREAM_EP_NAME: return tud_midi2_ep_name_cb(itf);
+ case STREAM_PROD_INSTANCE_ID: return tud_midi2_product_id_cb(itf);
+ case STREAM_FB_NAME: return tud_midi2_fb_name_cb(itf, index);
+ default: return NULL;
+ }
+}
+
+// Send or resume one text reply. While it is incomplete, its status and index
+// identify the sole owner of nego_text_offset so another discovery request
+// cannot resume a different string from the same offset.
+static bool _nego_send_text(midi2d_interface_t* p_midi, uint16_t status, uint8_t index) {
+ const char* text = _nego_text_cb(p_midi, status, index);
+ const uint16_t len = text ? (uint16_t) strlen(text) : 0;
+
+ p_midi->nego_text_status = status;
+ p_midi->nego_text_index = index;
+ p_midi->nego_text_offset = _nego_send_stream_text(p_midi, status, status == STREAM_FB_NAME,
+ index, text, p_midi->nego_text_offset);
+ if (p_midi->nego_text_offset < len) return false;
+
+ p_midi->nego_text_status = 0;
+ p_midi->nego_text_offset = 0;
+ p_midi->nego_text_index = 0;
+ return true;
+}
+
+// Send pending discovery replies, one whole reply at a time and only when the
+// TX FIFO can take it. A full-filter Endpoint Discovery asks for more bytes
+// than the default FIFO holds; replies that do not fit stay pending and are
+// retried from the TX complete path, paced by the transfer flow.
+static void _nego_send_pending(midi2d_interface_t* p_midi) {
+ tu_fifo_t* tx_ff = &p_midi->ep_stream.tx.ff;
+
+ // An incomplete text sequence must finish before any newly arrived request
+ // is serviced; otherwise its Continue/End packets could be attached to a
+ // different Endpoint or Function Block string.
+ if (p_midi->nego_text_status) {
+ const uint16_t status = p_midi->nego_text_status;
+ const uint8_t index = p_midi->nego_text_index;
+ if (!_nego_send_text(p_midi, status, index)) return;
+
+ if (status == STREAM_FB_NAME) {
+ if (p_midi->nego_pending_fb_restart) {
+ p_midi->nego_pending_fb_next = 0;
+ p_midi->nego_pending_fb_restart = false;
+ } else {
+ p_midi->nego_pending_fb_next++;
+ }
+ } else {
+ const uint8_t bit = (status == STREAM_EP_NAME) ? 0x04 : 0x08;
+ p_midi->nego_pending_ep_filter &= (uint8_t) ~bit;
+ }
+ }
+
+ while (p_midi->nego_pending_ep_filter) {
+ const uint8_t bit = (uint8_t)(p_midi->nego_pending_ep_filter & (uint8_t)(-p_midi->nego_pending_ep_filter));
+ uint16_t status = 0;
+ switch (bit) {
+ case 0x04: status = STREAM_EP_NAME; break;
+ case 0x08: status = STREAM_PROD_INSTANCE_ID; break;
+ default: break;
+ }
+
+ if (status != 0) {
+ if (!_nego_send_text(p_midi, status, 0)) return;
+ } else {
+ if (tu_fifo_remaining(tx_ff) < 16) return;
+ switch (bit) {
+ case 0x01: _nego_send_endpoint_info(p_midi); break;
+ case 0x02: _nego_send_device_identity(p_midi); break;
+ case 0x10: _nego_send_config_notify(p_midi, p_midi->protocol); break;
+ default: break;
+ }
+ }
+ p_midi->nego_pending_ep_filter &= (uint8_t) ~bit;
+ }
+
+ const uint8_t fb_count = _gtb_block_count(p_midi);
+ while (p_midi->nego_pending_fb_filter && p_midi->nego_pending_fb_next < fb_count) {
+ const uint8_t f = p_midi->nego_pending_fb_next;
+ if (p_midi->nego_pending_fb_num != 0xFF && p_midi->nego_pending_fb_num != f) {
+ p_midi->nego_pending_fb_next++;
+ continue;
+ }
+ if ((p_midi->nego_pending_fb_filter & 0x01) && p_midi->nego_text_offset == 0) {
+ if (tu_fifo_remaining(tx_ff) < 16) return;
+ _nego_send_fb_info(p_midi, f);
+ }
+ if (p_midi->nego_pending_fb_filter & 0x02) {
+ if (!_nego_send_text(p_midi, STREAM_FB_NAME, f)) return;
+ }
+ p_midi->nego_pending_fb_next++;
+ }
+ if (p_midi->nego_pending_fb_next >= fb_count) p_midi->nego_pending_fb_filter = 0;
+}
+
static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t* words) {
// Let the application override this message before the built-in responder.
switch (tud_midi2_stream_msg_cb(_itf_idx(p_midi), words)) {
@@ -421,9 +579,9 @@ static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t*
switch (status) {
case STREAM_ENDPOINT_DISCOVERY:
- _nego_send_endpoint_info(p_midi);
- _nego_send_stream_text(p_midi, STREAM_EP_NAME, false, 0, tud_midi2_ep_name_cb(_itf_idx(p_midi)));
- _nego_send_stream_text(p_midi, STREAM_PROD_INSTANCE_ID, false, 0, tud_midi2_product_id_cb(_itf_idx(p_midi)));
+ // Filter bitmap: each bit set asks for one individual reply.
+ p_midi->nego_pending_ep_filter |= (uint8_t)(words[1] & 0x1F);
+ _nego_send_pending(p_midi);
break;
case STREAM_CONFIG_REQUEST: {
@@ -437,14 +595,23 @@ static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t*
}
case STREAM_FB_DISCOVERY: {
- uint8_t fb_idx = (words[0] >> 8) & 0xFF;
- uint8_t filter = words[0] & 0xFF; // bit 0: FB Info, bit 1: FB Name
- uint8_t fb_count = _gtb_block_count(p_midi);
- for (uint8_t f = 0; f < fb_count; f++) {
- if (fb_idx != 0xFF && fb_idx != f) continue;
- if (filter & 0x01) _nego_send_fb_info(p_midi, f);
- if (filter & 0x02) _nego_send_stream_text(p_midi, STREAM_FB_NAME, true, f, tud_midi2_fb_name_cb(_itf_idx(p_midi), f));
+ const uint8_t req_num = (uint8_t)((words[0] >> 8) & 0xFF);
+ const uint8_t req_filter = (uint8_t)(words[0] & 0x03);
+ // Merge with a pending request: repeating a Function Block Info is allowed
+ // at any time, losing a requested one is not.
+ if (req_filter && p_midi->nego_pending_fb_filter) {
+ if (p_midi->nego_pending_fb_num != req_num) p_midi->nego_pending_fb_num = 0xFF;
+ if (p_midi->nego_text_status == STREAM_FB_NAME) {
+ p_midi->nego_pending_fb_restart = true;
+ } else {
+ p_midi->nego_pending_fb_next = 0;
+ }
+ } else if (!p_midi->nego_pending_fb_filter) {
+ p_midi->nego_pending_fb_num = req_num;
+ p_midi->nego_pending_fb_next = 0;
}
+ p_midi->nego_pending_fb_filter |= req_filter; // bit 0: FB Info, bit 1: FB Name
+ _nego_send_pending(p_midi);
break;
}
@@ -754,6 +921,7 @@ bool midi2d_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_re
tu_edpt_stream_clear(&p_midi->ep_stream.rx);
tu_fifo_clear(&p_midi->ep_stream.tx.ff);
+ _nego_clear_pending(p_midi);
if (alt == 1) {
p_midi->negotiated = false;
@@ -824,6 +992,10 @@ bool midi2d_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint3
}
tu_edpt_stream_read_xfer(ep_rx);
} else if (ep_addr == ep_tx->ep_addr && result == XFER_RESULT_SUCCESS) {
+ // Completed transfer freed FIFO room: flush discovery replies still pending.
+ if (p_midi->alt_setting == 1) {
+ _nego_send_pending(p_midi);
+ }
uint16_t queued = _tx_start_xfer(p_midi);
// Send ZLP if no more data is queued but the last transfer was exactly mps
if (queued == 0 && tu_fifo_count(&ep_tx->ff) == 0 && xferred_bytes > 0 &&
diff --git a/src/class/midi/midi2_device.h b/src/class/midi/midi2_device.h
index 171b404b7..e3eb084d9 100644
--- a/src/class/midi/midi2_device.h
+++ b/src/class/midi/midi2_device.h
@@ -58,6 +58,17 @@ extern "C" {
#define CFG_TUD_MIDI2_PRODUCT_ID "TinyUSB-MIDI2"
#endif
+// Function Block capabilities reported in Function Block Info Notification.
+// The GTB descriptor carries direction and group span, but not these: they
+// depend on what the application implements, so they default to "none".
+#ifndef CFG_TUD_MIDI2_FB_CI_VERSION
+ #define CFG_TUD_MIDI2_FB_CI_VERSION 0 // 0: none or unknown, 1 or higher: MIDI-CI version
+#endif
+
+#ifndef CFG_TUD_MIDI2_FB_SYSEX8_STREAMS
+ #define CFG_TUD_MIDI2_FB_SYSEX8_STREAMS 0 // 0: unsupported, 1: single, 2-255: simultaneous streams
+#endif
+
// String descriptor index for the Group Terminal Block (iBlockItem, Table 5-6).
// 0 = no string descriptor (default, spec-allowed).
#ifndef CFG_TUD_MIDI2_BLOCK_STRIDX
@@ -118,6 +129,17 @@ typedef enum {
MIDI2_STREAM_NEGOTIATED_MIDI2,
} tud_midi2_stream_result_t;
+// Device identity fields, as defined for the MIDI 1.0 Device Inquiry reply and
+// reused by the Device Identity Notification. Every byte carries 7 bits.
+// A 1-byte System Exclusive ID goes in the first of the three manufacturer
+// bytes, so 0x7D is passed as 0x7D0000.
+typedef struct {
+ uint32_t manufacturer; // 3 bytes, first byte is most significant
+ uint16_t family; // 2 bytes
+ uint16_t model; // 2 bytes
+ uint32_t sw_revision; // 4 bytes
+} tud_midi2_device_identity_t;
+
//--------------------------------------------------------------------+
// Application Callback API (weak, optional)
//--------------------------------------------------------------------+
@@ -138,6 +160,12 @@ const uint8_t* tud_midi2_gtb_desc_cb(uint8_t itf, uint16_t* len);
// discovery. Return NULL or "" for no name.
const char* tud_midi2_fb_name_cb(uint8_t itf, uint8_t fb_idx);
+// Optional device identity, sent as a Device Identity Notification when the
+// host sets the 'd' bit in the Endpoint Discovery filter. Same four fields as
+// the MIDI 1.0 Device Inquiry reply. Return false to skip the notification,
+// which is the default. All values are 7-bit per byte.
+bool tud_midi2_device_identity_cb(uint8_t itf, tud_midi2_device_identity_t* identity);
+
// Optional: intercept an incoming UMP Stream message (MT 0xF). Return PASS to
// let the built-in responder handle it, or HANDLED / NEGOTIATED_* if the app
// answered it (e.g. via tud_midi2_n_ump_write). Lets an app override a single
diff --git a/src/class/mtp/mtp_device.c b/src/class/mtp/mtp_device.c
index 7657899ec..275c9f858 100644
--- a/src/class/mtp/mtp_device.c
+++ b/src/class/mtp/mtp_device.c
@@ -437,8 +437,11 @@ bool mtpd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t
TU_LOG_DRV(" MTP Data %s CB: xferred_bytes=%lu, xferred_len/total_len=%lu/%lu, is_complete=%d\r\n",
is_data_in ? "IN" : "OUT", xferred_bytes, p_mtp->xferred_len, p_mtp->total_len, is_complete ? 1 : 0);
- // Send/queue ZLP if packet is full-sized but transfer is complete
- if (is_complete && xferred_bytes > 0 && !(xferred_bytes & (threshold - 1))) {
+ // Send/queue ZLP if packet is full-sized but transfer is complete.
+ // OUT must deliver this final payload to the application before receiving
+ // its terminating ZLP below.
+ const bool need_zlp = is_complete && xferred_bytes > 0 && !(xferred_bytes & (threshold - 1));
+ if (is_data_in && need_zlp) {
TU_LOG_DRV(" queue ZLP\r\n");
TU_VERIFY(usbd_edpt_claim(p_mtp->rhport, ep_addr));
TU_ASSERT(usbd_edpt_xfer(p_mtp->rhport, ep_addr, NULL, 0, false));
@@ -466,9 +469,16 @@ bool mtpd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t
cb_data.io_container = headerless_packet;
cb_data.io_container.payload_bytes = xferred_bytes;
}
- tud_mtp_data_xfer_cb(&cb_data);
+ if (xferred_bytes > 0) {
+ tud_mtp_data_xfer_cb(&cb_data);
+ }
- if (is_complete) {
+ if (need_zlp) {
+ TU_LOG_DRV(" queue ZLP\r\n");
+ 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;
+ } else if (is_complete) {
// back to header + payload for response
cb_data.io_container = headered_packet;
cb_data.io_container.header->len = sizeof(mtp_container_header_t);
diff --git a/src/class/usbtmc/usbtmc_device.c b/src/class/usbtmc/usbtmc_device.c
index 07190d89f..0e9978a81 100644
--- a/src/class/usbtmc/usbtmc_device.c
+++ b/src/class/usbtmc/usbtmc_device.c
@@ -497,9 +497,24 @@ bool usbtmcd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint
#if (CFG_TUD_USBTMC_ENABLE_488)
case USBTMC_MSGID_USB488_TRIGGER:
- // Spec says we halt the EP if we didn't declare we support it.
- TU_VERIFY(usbtmc_state.capabilities->bmIntfcCapabilities488.supportsTrigger);
- TU_VERIFY(tud_usbtmc_msg_trigger_cb(msg));
+ // Unlike the messages above, TRIGGER is complete on arrival and has no response, so nothing else
+ // will move us out of STATE_IDLE. Do it here, otherwise the tud_usbtmc_start_bus_read() below (and
+ // any call the application makes from its callback) is a no-op and the bulk-OUT endpoint is left
+ // un-armed, silently timing out every subsequent host transfer.
+ TU_VERIFY(atomicChangeState(STATE_IDLE, STATE_NAK));
+
+ // Spec says we halt the EP if we didn't declare we support it; do the same when the application
+ // rejects the trigger. The callback result must not be wrapped in TU_VERIFY() here: returning
+ // early would skip both the stall and the re-arm below.
+ if (!usbtmc_state.capabilities->bmIntfcCapabilities488.supportsTrigger ||
+ !tud_usbtmc_msg_trigger_cb(msg)) {
+ usbd_edpt_stall(rhport, usbtmc_state.ep_bulk_out);
+ return false;
+ }
+ // Result deliberately ignored: false here means the endpoint is already armed - either the
+ // application re-armed it from its callback, or a transfer is still queued - not that arming
+ // failed. Stalling on it would halt a healthy endpoint.
+ tud_usbtmc_start_bus_read();
break;
#endif
diff --git a/src/class/usbtmc/usbtmc_device.h b/src/class/usbtmc/usbtmc_device.h
index 3dc700876..efda84f16 100644
--- a/src/class/usbtmc/usbtmc_device.h
+++ b/src/class/usbtmc/usbtmc_device.h
@@ -25,7 +25,6 @@
// * tud_usbtmc_open_cb
// * tud_usbtmc_msg_data_cb
// * tud_usbtmc_msgBulkIn_complete_cb
-// * tud_usbtmc_msg_trigger_cb
// * (successful) tud_usbtmc_check_abort_bulk_out_cb
// * (successful) tud_usbtmc_check_abort_bulk_in_cb
// * (successful) tud_usmtmc_bulkOut_clearFeature_cb
diff --git a/src/class/video/video_device.c b/src/class/video/video_device.c
index 3797e6b2b..770595178 100644
--- a/src/class/video/video_device.c
+++ b/src/class/video/video_device.c
@@ -1144,6 +1144,9 @@ static int handle_video_stm_cs_req(uint8_t rhport, uint8_t stage,
video_probe_and_commit_control_t *param = &stm->probe_commit_payload;
TU_VERIFY(_update_streaming_parameters(stm, param), VIDEO_ERROR_INVALID_VALUE_WITHIN_RANGE);
/* Set the negotiated value */
+ if (CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE < param->dwMaxPayloadTransferSize) {
+ param->dwMaxPayloadTransferSize = CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE;
+ }
stm->max_payload_transfer_size = param->dwMaxPayloadTransferSize;
int ret = tud_video_commit_cb(stm->index_vc, stm->index_vs, param);
if (VIDEO_ERROR_NONE == ret) {
diff --git a/src/common/tusb_mcu.h b/src/common/tusb_mcu.h
index 93b4a2ee9..af43dfb12 100644
--- a/src/common/tusb_mcu.h
+++ b/src/common/tusb_mcu.h
@@ -126,6 +126,14 @@
#define CFG_TUSB_MEM_DCACHE_LINE_SIZE_DEFAULT 32
#endif
+ // Errata ERR050101, listed for RT1015/RT1020/RT1024/RT1050 (no fix scheduled) and for
+ // RT1060/RT1064 rev A (fixed in rev B); not listed for RT1010 or the RT11xx family.
+ #if defined(MIMXRT1015_SERIES) || defined(MIMXRT1021_SERIES) || defined(MIMXRT1024_SERIES) || \
+ defined(MIMXRT1051_SERIES) || defined(MIMXRT1052_SERIES) || defined(MIMXRT1061_SERIES) || \
+ defined(MIMXRT1062_SERIES) || defined(MIMXRT1064_SERIES)
+ #define CFG_TUSB_MIMXRT1XXX_ERRATA_ERR050101 1
+ #endif
+
#elif TU_CHECK_MCU(OPT_MCU_KINETIS_KL, OPT_MCU_KINETIS_K32L, OPT_MCU_KINETIS_K)
#define TUP_USBIP_CHIPIDEA_FS
#define TUP_USBIP_CHIPIDEA_FS_KINETIS
@@ -768,6 +776,17 @@
#define TUP_DCD_EDPT_ISO_ALLOC
#endif
+// Set by silicon whose isochronous IN endpoint can be unprimed by an IN token sent to that same
+// endpoint number on ANOTHER device sharing the host, taking one of this device's OUT endpoints
+// down with it - undetectable in software. Descriptors must then give an isochronous IN endpoint
+// a number no other device on the bus uses; a number is only safe while it stays unique, so two
+// affected boards on one hub must not pick the same one. Default 0 (no such conflict). Set it to
+// 0 by hand on RT1060/RT1064 rev B, which carry the fix - the revision cannot be told apart at
+// compile time, so the affected parts are assumed to be rev A.
+#ifndef CFG_TUSB_MIMXRT1XXX_ERRATA_ERR050101
+ #define CFG_TUSB_MIMXRT1XXX_ERRATA_ERR050101 0
+#endif
+
// Some USBIPs (SAMG, SAMX7X, PIC32, MAX3266x/MAX78002) cannot assign the same endpoint
// number to both IN and OUT. Default to 0 (same endpoint number may be used for IN and OUT).
#ifndef CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY
diff --git a/src/device/dcd.h b/src/device/dcd.h
index f005e9620..a4006ae0c 100644
--- a/src/device/dcd.h
+++ b/src/device/dcd.h
@@ -20,19 +20,27 @@
// MACRO CONSTANT TYPEDEF PROTYPES
//--------------------------------------------------------------------+
+// Bus reset is reported as two edges. BUS_RESET_START is optional: a controller that
+// cannot tell the edges apart emits only BUS_RESET_END, which stays self-sufficient (it
+// performs the full teardown with or without a preceding START). Emit START when reset
+// signaling is detected - the link is unusable and the speed is not negotiated yet - so
+// the stack stops using endpoints immediately instead of at the end of the reset.
typedef enum {
- DCD_EVENT_INVALID = 0, // 0
- DCD_EVENT_BUS_RESET, // 1
- DCD_EVENT_UNPLUGGED, // 2
- DCD_EVENT_SOF, // 3
- DCD_EVENT_SUSPEND, // 4 TODO LPM Sleep L1 support
- DCD_EVENT_RESUME, // 5
- DCD_EVENT_SETUP_RECEIVED, // 6
- DCD_EVENT_XFER_COMPLETE, // 7
- USBD_EVENT_FUNC_CALL, // 8 Not an DCD event, just a convenient way to defer ISR function
+ DCD_EVENT_INVALID = 0, // 0
+ DCD_EVENT_BUS_RESET_START, // 1
+ DCD_EVENT_BUS_RESET_END, // 2 with negotiated speed
+ DCD_EVENT_UNPLUGGED, // 3
+ DCD_EVENT_SOF, // 4
+ DCD_EVENT_SUSPEND, // 5 TODO LPM Sleep L1 support
+ DCD_EVENT_RESUME, // 6
+ DCD_EVENT_SETUP_RECEIVED, // 7
+ DCD_EVENT_XFER_COMPLETE, // 8
+ USBD_EVENT_FUNC_CALL, // 9 Not an DCD event, just a convenient way to defer ISR function
DCD_EVENT_COUNT
} dcd_eventid_t;
+#define DCD_EVENT_BUS_RESET DCD_EVENT_BUS_RESET_END // backward compatibility
+
typedef struct TU_ATTR_ALIGNED(4) {
uint8_t rhport;
uint8_t event_id;
diff --git a/src/device/usbd.c b/src/device/usbd.c
index 5471e132d..e84d72fa4 100644
--- a/src/device/usbd.c
+++ b/src/device/usbd.c
@@ -456,7 +456,8 @@ TU_ATTR_WEAK bool dcd_configure(uint8_t rhport, uint32_t cfg_id, const void* cfg
#if CFG_TUSB_DEBUG >= CFG_TUD_LOG_LEVEL
static char const *const _usbd_event_str[DCD_EVENT_COUNT] = {
"Invalid",
- "Bus Reset",
+ "Bus Reset Start",
+ "Bus Reset End",
"Unplugged",
"SOF",
"Suspend",
@@ -642,6 +643,8 @@ static void configuration_reset(uint8_t rhport) {
static void usbd_reset(uint8_t rhport) {
configuration_reset(rhport);
+ // discard any pre-reset SETUP still counted: a stale count skips post-reset SETUPs
+ _usbd_queued_setup = 0;
}
bool tud_task_event_ready(void) {
@@ -695,8 +698,15 @@ void tud_task_ext(uint32_t timeout_ms, bool in_isr) {
#endif
switch (event.event_id) {
- case DCD_EVENT_BUS_RESET:
+ case DCD_EVENT_BUS_RESET_START:
+ TU_LOG_USBD("\r\n");
+ usbd_reset(event.rhport);
+ break;
+
+ case DCD_EVENT_BUS_RESET_END:
TU_LOG_USBD(": %s Speed\r\n", tu_str_speed[event.bus_reset.speed]);
+ // TODO a DCD that reports both edges pays for two teardowns: track a per-rhport
+ // "start seen" flag and skip this reset, keeping it for the single-event DCDs.
usbd_reset(event.rhport);
_usbd_dev.speed = event.bus_reset.speed;
break;
@@ -747,7 +757,14 @@ void tud_task_ext(uint32_t timeout_ms, bool in_isr) {
_usbd_dev.ep_status[epnum][ep_dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED);
if (0 == epnum) {
- usbd_control_xfer_cb(event.rhport, ep_addr, (xfer_result_t) event.xfer_complete.result, event.xfer_complete.len);
+ // 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
+ // this one. The pending setup re-drives EP0 by itself.
+ if (!usbd_control_xfer_cb(event.rhport, ep_addr, (xfer_result_t) event.xfer_complete.result,
+ event.xfer_complete.len)) {
+ 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,);
@@ -865,10 +882,10 @@ bool tud_control_xfer(uint8_t rhport, const tusb_control_request_t* request, voi
if (ctrl_xfer->data_len > 0U) {
TU_ASSERT(buffer);
}
- TU_ASSERT(data_stage_xact(rhport));
+ TU_VERIFY(data_stage_xact(rhport));
} else {
// wLength == 0: Status stage is always IN per USB 2.0 ยง9.3.1
- TU_ASSERT(status_stage_xact(rhport, TU_EP0_IN));
+ TU_VERIFY(status_stage_xact(rhport, TU_EP0_IN));
}
return true;
@@ -919,7 +936,7 @@ static bool usbd_control_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t
}
if (is_ok) {
- TU_ASSERT(status_stage_xact(rhport, ep_status));
+ TU_VERIFY(status_stage_xact(rhport, ep_status));
} else {
// Stall both IN and OUT control endpoint
dcd_edpt_stall(rhport, TU_EP0_OUT);
@@ -927,7 +944,7 @@ static bool usbd_control_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t
}
} else {
// More data to transfer
- TU_ASSERT(data_stage_xact(rhport));
+ TU_VERIFY(data_stage_xact(rhport));
}
return true;
@@ -1473,8 +1490,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);
+ 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);
+ }
}
}
@@ -1588,10 +1615,12 @@ bool usbd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t* buffer, uint16_t t
if (dcd_edpt_xfer(rhport, ep_addr, buffer, total_bytes, is_isr)) {
return true;
} else {
- // DCD error, mark endpoint as ready to allow next transfer
+ // Driver refused the transfer, mark endpoint as ready to allow next transfer. This is a
+ // recoverable condition (e.g. a new setup superseding a control response), not a bug, so
+ // do not break into the debugger - TU_BREAKPOINT() halts the CPU whenever a probe is
+ // attached, which on a test rig is always.
_usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED);
TU_LOG_USBD("FAILED\r\n");
- TU_BREAKPOINT();
return false;
}
}
diff --git a/src/portable/chipidea/ci_hs/ci_hs_imxrt.h b/src/portable/chipidea/ci_hs/ci_hs_imxrt.h
index f0f918fe2..8f0d6083e 100644
--- a/src/portable/chipidea/ci_hs/ci_hs_imxrt.h
+++ b/src/portable/chipidea/ci_hs/ci_hs_imxrt.h
@@ -36,6 +36,9 @@ static const ci_hs_controller_t _ci_controller[] =
#define CI_HS_REG(_port) ((ci_hs_regs_t*) _ci_controller[_port].reg_base)
+// NXP recommends AHBBRST = INCR16 (remainder as unspecified-length bursts)
+#define CI_HS_SET_AHB_BURST(_p) (CI_HS_REG(_p)->SBUSCFG = SBUSCFG_AHBBRST_INCR16_UNSPEC)
+
//------------- DCD -------------//
#define CI_DCD_INT_ENABLE(_p) NVIC_EnableIRQ ((IRQn_Type)_ci_controller[_p].irqnum)
#define CI_DCD_INT_DISABLE(_p) NVIC_DisableIRQ((IRQn_Type)_ci_controller[_p].irqnum)
diff --git a/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h b/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h
index f2061bd7a..c7dc7e69f 100644
--- a/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h
+++ b/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h
@@ -34,4 +34,9 @@ static const ci_hs_controller_t _ci_controller[] =
#define CI_HCD_INT_ENABLE(_p) NVIC_EnableIRQ ((IRQn_Type)_ci_controller[_p].irqnum)
#define CI_HCD_INT_DISABLE(_p) NVIC_DisableIRQ((IRQn_Type)_ci_controller[_p].irqnum)
+// USB0 (high-speed) only: NXP recommends AHBBRST = INCR16 (remainder as
+// unspecified-length bursts)
+#define CI_HS_SET_AHB_BURST(_p) \
+ do { if ((_p) == 0) { CI_HS_REG(_p)->SBUSCFG = SBUSCFG_AHBBRST_INCR16_UNSPEC; } } while (0)
+
#endif
diff --git a/src/portable/chipidea/ci_hs/ci_hs_type.h b/src/portable/chipidea/ci_hs/ci_hs_type.h
index 70817a6e3..5baa14821 100644
--- a/src/portable/chipidea/ci_hs/ci_hs_type.h
+++ b/src/portable/chipidea/ci_hs/ci_hs_type.h
@@ -36,10 +36,18 @@ enum {
PORTSC1_CURRENT_CONNECT_STATUS = TU_BIT(0),
PORTSC1_FORCE_PORT_RESUME = TU_BIT(6),
PORTSC1_SUSPEND = TU_BIT(7),
+ PORTSC1_PORT_RESET = TU_BIT(8), // read-only in device mode: a reset is being driven
PORTSC1_FORCE_FULL_SPEED = TU_BIT(24),
PORTSC1_PORT_SPEED = TU_BIT(26) | TU_BIT(27)
};
+// PORTSC1 PSPD field values, once shifted down by PORTSC1_PORT_SPEED_POS. 3 is undefined.
+enum {
+ PORTSC1_PORT_SPEED_FULL = 0,
+ PORTSC1_PORT_SPEED_LOW = 1,
+ PORTSC1_PORT_SPEED_HIGH = 2,
+};
+
// OTGSC
enum {
OTGSC_VBUS_DISCHARGE = TU_BIT(0),
@@ -71,11 +79,18 @@ enum {
USBMODE_VBUS_POWER_SELECT = TU_BIT(5), // Need to be enabled for LPC18XX/43XX in host mode
};
+// SBUSCFG
+enum {
+ SBUSCFG_AHBBRST_INCR16_UNSPEC = 7, // INCR16 burst, remainder as unspecified-length bursts
+};
+
// Device Registers
typedef struct
{
//------------- ID + HW Parameter Registers-------------//
- volatile uint32_t TU_RESERVED[64]; ///< For iMX RT10xx, but not used by LPC18XX/LPC43XX
+ volatile uint32_t TU_RESERVED[36]; ///< ID/HW parameter registers, not used by this driver
+ volatile uint32_t SBUSCFG; ///< System Bus Interface Configuration (not present on every MCU)
+ volatile uint32_t TU_RESERVED[27];
//------------- Capability Registers-------------//
volatile uint8_t CAPLENGTH; ///< Capability Registers Length
diff --git a/src/portable/chipidea/ci_hs/dcd_ci_hs.c b/src/portable/chipidea/ci_hs/dcd_ci_hs.c
index fa98d6882..6ab28e0be 100644
--- a/src/portable/chipidea/ci_hs/dcd_ci_hs.c
+++ b/src/portable/chipidea/ci_hs/dcd_ci_hs.c
@@ -154,6 +154,14 @@ TU_VERIFY_STATIC(sizeof(dcd_qhd_t) == 64, "size is not correct");
#define QTD_NEXT_INVALID 0x01
+// Bounded spin for register waits. The longest legitimate wait is a flush held off by a packet
+// already in progress: ~50 us for a full-speed 64-byte packet, a low thousands of dependent
+// register reads, so healthy hardware never approaches this bound. Exceeding it means the
+// controller has stopped responding, and the spin then only serves to keep an ISR (or an
+// IRQ-masked caller) from hanging outright - the 3 ms reset-cleanup window of IMXRT1060RM 42.5.6.2.1 (p.2394)
+// is already unreachable in that state, and the manual's remedy there is a controller reset.
+#define CI_HS_BUSY_SPIN 10000u
+
typedef struct {
// Must be at 2K alignment
// Each endpoint with direction (IN/OUT) occupies a queue head
@@ -164,6 +172,17 @@ typedef struct {
CFG_TUD_MEM_SECTION TU_ATTR_ALIGNED(2048) static dcd_data_t _dcd_data;
+// What the next Port Change Detect will be. Each one is preceded by the interrupt that causes it:
+// a reset interrupt for the end of a bus reset - where the speed first becomes final - or a
+// suspend interrupt for the resume that ends the suspend. A suspend itself raises no port change,
+// which is why there is no such value here. Indexed by rhport, which is 0 or 1 on every ci_hs
+// variant (NOT the controller count: mcx/rw61x map rhport 1 to controller 0).
+enum {
+ PORT_CHANGE_REASON_RESET = 0,
+ PORT_CHANGE_REASON_RESUME = 1,
+};
+static volatile uint8_t _port_change_reason[2];
+
//--------------------------------------------------------------------+
// Prototypes and Helper Functions
//--------------------------------------------------------------------+
@@ -172,12 +191,37 @@ TU_ATTR_ALWAYS_INLINE static inline uint8_t ci_ep_count(const ci_hs_regs_t *dcd_
return dcd_reg->DCCPARAMS & DCCPARAMS_DEN_MASK;
}
+static bool controller_reset(uint8_t rhport);
+
//--------------------------------------------------------------------+
// Controller API
//--------------------------------------------------------------------+
-/// follows LPC43xx User Manual 23.10.3
-static void bus_reset(uint8_t rhport) {
+// Flush endpoint buffers, following IMXRT1060RM 42.5.6.6.5 Flushing/De-priming an Endpoint
+// (p.2413): write ENDPTFLUSH, wait for the controller
+// to acknowledge, then confirm ENDPTSTAT went to zero. The controller refuses the flush when a
+// packet is in progress, and the manual requires the procedure be repeated until it takes.
+// Callers proceed regardless of the result; the bound only prevents an ISR-context hang on dead
+// hardware.
+static bool flush_endpoints(ci_hs_regs_t *dcd_reg, uint32_t mask) {
+ uint32_t guard = CI_HS_BUSY_SPIN;
+ do {
+ dcd_reg->ENDPTFLUSH = mask;
+ while (dcd_reg->ENDPTFLUSH & mask) {
+ if (!guard--) {
+ return false;
+ }
+ }
+ } while ((dcd_reg->ENDPTSTAT & mask) && guard--);
+
+ return !(dcd_reg->ENDPTSTAT & mask);
+}
+
+/// Everything the manual asks of the DCD when a reset is detected, in its order: clear the setup
+/// and completion semaphores, cancel every prime, check the reset is still being driven, and free
+/// the dTDs. All of it belongs inside the reset window (IMXRT1060RM 42.5.6.2.1, p.2394); nothing
+/// is left for the port change that ends the reset, which only reports the negotiated speed.
+static void bus_reset_begin(uint8_t rhport) {
ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport);
// The reset value for all endpoint types is the control endpoint. If one endpoint
@@ -193,17 +237,24 @@ static void bus_reset(uint8_t rhport) {
//------------- Clear All Registers -------------//
dcd_reg->ENDPTNAK = dcd_reg->ENDPTNAK;
dcd_reg->ENDPTNAKEN = 0;
- dcd_reg->USBSTS = dcd_reg->USBSTS;
dcd_reg->ENDPTSETUPSTAT = dcd_reg->ENDPTSETUPSTAT;
dcd_reg->ENDPTCOMPLETE = dcd_reg->ENDPTCOMPLETE;
- while (dcd_reg->ENDPTPRIME) {}
- dcd_reg->ENDPTFLUSH = 0xFFFFFFFF;
- while (dcd_reg->ENDPTFLUSH) {}
+ uint32_t guard = CI_HS_BUSY_SPIN;
+ while (dcd_reg->ENDPTPRIME && guard--) {}
+ dcd_reg->ENDPTFLUSH = 0xFFFFFFFFUL;
- // read reset bit in portsc
+ // All of the above must land while the reset is still being driven - it lasts at least 3 ms.
+ // Arriving late leaves the controller in an undefined state, and the manual's remedy is to
+ // hardware-reset it. That clears Run/Stop, so the device detaches and the host will drive a
+ // fresh reset and enumeration - which is why nothing below this point is worth doing here.
+ if (!(dcd_reg->PORTSC1 & PORTSC1_PORT_RESET)) {
+ TU_LOG1("ci_hs: reset cleanup ran past the end of the reset, resetting controller\r\n");
+ controller_reset(rhport);
+ return; // the controller detached; the host's next reset redoes everything below
+ }
- //------------- Queue Head & Queue TD -------------//
+ //------------- Free all allocated dTDs: the controller will not execute them again -------------//
tu_memclr(&_dcd_data, sizeof(dcd_data_t));
//------------- Set up Control Endpoints (0 OUT, 1 IN) -------------//
@@ -216,27 +267,29 @@ static void bus_reset(uint8_t rhport) {
dcd_dcache_clean_invalidate(&_dcd_data, sizeof(dcd_data_t));
}
-bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) {
- (void)rh_init;
- tu_memclr(&_dcd_data, sizeof(dcd_data_t));
-
+/// Reset the controller and bring it back up in device mode. Also the manual's remedy when the
+/// reset cleanup misses its window: the controller reset clears Run/Stop and detaches the device,
+/// so it must be re-initialised completely afterwards (IMXRT1060RM 42.5.6.2.1, p.2394).
+static bool controller_reset(uint8_t rhport) {
ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport);
- TU_ASSERT(ci_ep_count(dcd_reg) <= TUP_DCD_ENDPOINT_MAX);
-
- #if TU_CHECK_MCU(OPT_MCU_HPM)
- usb_phy_init((USB_Type *)dcd_reg, false);
- #endif
+ tu_memclr(&_dcd_data, sizeof(dcd_data_t));
// Reset controller
dcd_reg->USBCMD |= USBCMD_RESET;
- while (dcd_reg->USBCMD & USBCMD_RESET) {}
+ uint32_t guard = CI_HS_BUSY_SPIN;
+ while ((dcd_reg->USBCMD & USBCMD_RESET) && guard--) {}
+ TU_VERIFY(!(dcd_reg->USBCMD & USBCMD_RESET)); // reached from the ISR too, so never halt here
// Set mode to device, must be set immediately after reset
uint32_t usbmode = dcd_reg->USBMODE & ~USBMOD_CM_MASK;
usbmode |= USBMODE_CM_DEVICE;
dcd_reg->USBMODE = usbmode;
+ #ifdef CI_HS_SET_AHB_BURST
+ CI_HS_SET_AHB_BURST(rhport);
+ #endif
+
#ifdef CFG_TUD_CI_HS_VBUS_CHARGE
dcd_reg->OTGSC = OTGSC_VBUS_CHARGE | OTGSC_OTG_TERMINATION;
#else
@@ -253,9 +306,11 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) {
dcd_dcache_clean_invalidate(&_dcd_data, sizeof(dcd_data_t));
+ _port_change_reason[rhport] = PORT_CHANGE_REASON_RESET;
+
dcd_reg->ENDPTLISTADDR = (uint32_t)_dcd_data.qhd; // Endpoint List Address has to be 2K alignment
dcd_reg->USBSTS = dcd_reg->USBSTS;
- dcd_reg->USBINTR = INTR_USB | INTR_ERROR | INTR_PORT_CHANGE | INTR_SUSPEND;
+ dcd_reg->USBINTR = INTR_USB | INTR_ERROR | INTR_PORT_CHANGE | INTR_RESET | INTR_SUSPEND;
uint32_t usbcmd = dcd_reg->USBCMD;
usbcmd &= ~USBCMD_INTR_THRESHOLD_MASK; // Interrupt Threshold Interval = 0
@@ -266,8 +321,22 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) {
return true;
}
+bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) {
+ (void)rh_init;
+ ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport);
+
+ TU_ASSERT(ci_ep_count(dcd_reg) <= TUP_DCD_ENDPOINT_MAX);
+
+ #if TU_CHECK_MCU(OPT_MCU_HPM)
+ usb_phy_init((USB_Type *)dcd_reg, false);
+ #endif
+
+ return controller_reset(rhport);
+}
+
bool dcd_deinit(uint8_t rhport) {
ci_hs_regs_t* dcd_reg = CI_HS_REG(rhport);
+ _port_change_reason[rhport] = PORT_CHANGE_REASON_RESET;
// disable all interrupt
dcd_reg->USBINTR = 0;
@@ -276,9 +345,9 @@ bool dcd_deinit(uint8_t rhport) {
dcd_reg->USBCMD &= ~USBCMD_RUN_STOP;
// flush all endpoints
- while (dcd_reg->ENDPTPRIME) {}
- dcd_reg->ENDPTFLUSH = 0xFFFFFFFF;
- while (dcd_reg->ENDPTFLUSH) {}
+ uint32_t guard = CI_HS_BUSY_SPIN;
+ while (dcd_reg->ENDPTPRIME && guard--) {}
+ flush_endpoints(dcd_reg, 0xFFFFFFFF);
return true;
}
@@ -292,11 +361,13 @@ void dcd_int_disable(uint8_t rhport) {
}
void dcd_set_address(uint8_t rhport, uint8_t dev_addr) {
- // Response with status first before changing device address
- dcd_edpt_xfer(rhport, tu_edpt_addr(0, TUSB_DIR_IN), NULL, 0, false);
-
- ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport);
- dcd_reg->DEVICEADDR = (dev_addr << 25) | TU_BIT(24);
+ // Response with status first before changing device address. A refused prime means a new
+ // setup superseded this transfer; staging an address whose ACK will never arrive would
+ // leave the device answering on it, so only arm the address when the status went out.
+ if (dcd_edpt_xfer(rhport, tu_edpt_addr(0, TUSB_DIR_IN), NULL, 0, false)) {
+ ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport);
+ dcd_reg->DEVICEADDR = (dev_addr << 25) | TU_BIT(24);
+ }
}
void dcd_remote_wakeup(uint8_t rhport) {
@@ -464,9 +535,7 @@ bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep)
// dcd_dcache_clean_invalidate(&_dcd_data, sizeof(dcd_data_t));
// Flush EP
- const uint32_t flush_mask = TU_BIT(epnum + (dir ? 16 : 0));
- dcd_reg->ENDPTFLUSH = flush_mask;
- while (dcd_reg->ENDPTFLUSH & flush_mask) {}
+ flush_endpoints(dcd_reg, TU_BIT(epnum + (dir ? 16 : 0)));
// disable to change max packet size
ep_ctrl_clear(endptctrl, dir, ENDPTCTRL_ENABLE);
@@ -492,7 +561,7 @@ void dcd_edpt_close_all(uint8_t rhport) {
}
}
-static void qhd_start_xfer(uint8_t rhport, uint8_t epnum, uint8_t dir) {
+static bool qhd_start_xfer(uint8_t rhport, uint8_t epnum, uint8_t dir) {
ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport);
dcd_qhd_t *p_qhd = &_dcd_data.qhd[epnum][dir];
dcd_qtd_t *p_qtd = &_dcd_data.qtd[epnum][dir];
@@ -505,13 +574,22 @@ static void qhd_start_xfer(uint8_t rhport, uint8_t epnum, uint8_t dir) {
dcd_dcache_clean_invalidate(&_dcd_data, sizeof(dcd_data_t));
if (epnum == 0) {
- // follows UM 24.10.8.1.1 Setup packet handling using setup lockout mechanism
- // wait until ENDPTSETUPSTAT before priming data/status in response TODO add time out
- while (dcd_reg->ENDPTSETUPSTAT & TU_BIT(0)) {}
+ // Setup lockout (IMXRT1060RM 42.5.6.4.2.1 Setup Phase, p.2403): never prime EP0 while a new
+ // SETUP is pending. The ISR
+ // normally consumes ENDPTSETUPSTAT quickly; if the guard trips, fail the transfer so usbd
+ // releases the endpoint (a pending SETUP supersedes this response anyway; without one, usbd
+ // stalls EP0 and the host recovers with a fresh control transfer).
+ uint32_t guard = CI_HS_BUSY_SPIN;
+ while (dcd_reg->ENDPTSETUPSTAT & TU_BIT(0)) {
+ if (!guard--) {
+ return false;
+ }
+ }
}
// start transfer
dcd_reg->ENDPTPRIME = TU_BIT(epnum + (dir ? 16 : 0));
+ return true;
}
bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes, bool is_isr) {
@@ -527,9 +605,7 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t to
// Start qhd transfer
p_qhd->ff = NULL;
- qhd_start_xfer(rhport, epnum, dir);
-
- return true;
+ return qhd_start_xfer(rhport, epnum, dir);
}
#if !CFG_TUD_MEM_DCACHE_ENABLE
@@ -580,9 +656,7 @@ bool dcd_edpt_xfer_fifo(uint8_t rhport, uint8_t ep_addr, tu_fifo_t *ff, uint16_t
// Start qhd transfer
p_qhd->ff = ff;
- qhd_start_xfer(rhport, epnum, dir);
-
- return true;
+ return qhd_start_xfer(rhport, epnum, dir);
}
#endif
@@ -630,43 +704,43 @@ void dcd_int_handler(uint8_t rhport) {
return;
}
- // Set if the port controller enters the full or high-speed operational state.
- // either from Bus Reset or Suspended state
- if (int_status & INTR_PORT_CHANGE) {
- // TU_LOG2("PortChange %08lx\r\n", dcd_reg->PORTSC1);
+ const uint8_t pci_reason = _port_change_reason[rhport]; // save current pci_reason
- // Reset interrupt is not enabled, we manually check if Port Change is due
- // to connection / disconnection
- if (dcd_reg->USBSTS & INTR_RESET) {
- dcd_reg->USBSTS = INTR_RESET;
-
- if (dcd_reg->PORTSC1 & PORTSC1_CURRENT_CONNECT_STATUS) {
- const uint32_t speed = (dcd_reg->PORTSC1 & PORTSC1_PORT_SPEED) >> PORTSC1_PORT_SPEED_POS;
- bus_reset(rhport);
- dcd_event_bus_reset(rhport, (tusb_speed_t)speed, true);
- } else {
- dcd_event_bus_signal(rhport, DCD_EVENT_UNPLUGGED, true);
- }
- } else {
- // Triggered by resuming from suspended state
- if (!(dcd_reg->PORTSC1 & PORTSC1_SUSPEND)) {
- dcd_event_bus_signal(rhport, DCD_EVENT_RESUME, true);
- }
- }
+ if (int_status & INTR_SUSPEND) {
+ _port_change_reason[rhport] = PORT_CHANGE_REASON_RESUME; // next PCI is resume
+ dcd_event_bus_signal(rhport, DCD_EVENT_SUSPEND, true);
}
- if (int_status & INTR_SUSPEND) {
- // TU_LOG2("Suspend %08lx\r\n", dcd_reg->PORTSC1);
+ // USB Reset Received: register cleanup runs here within the reset window (IMXRT1060RM 42.5.6.2.1, p.2394)
+ // and BUS_RESET_START fires now; BUS_RESET_END, with the final speed, is triggered later by PCI.
+ if (int_status & INTR_RESET) {
+ _port_change_reason[rhport] = PORT_CHANGE_REASON_RESET;
+ bus_reset_begin(rhport);
+ dcd_event_bus_signal(rhport, DCD_EVENT_BUS_RESET_START, true);
+ }
- if (dcd_reg->PORTSC1 & PORTSC1_SUSPEND) {
- // Note: Host may delay more than 3 ms before and/or after bus reset before doing enumeration.
- // Skip suspend event if we are not addressed
- if ((dcd_reg->DEVICEADDR >> 25) & 0x0f) {
- dcd_event_bus_signal(rhport, DCD_EVENT_SUSPEND, true);
- }
+ // Port entered the full/high-speed operational state: the end of a bus reset, or a resume.
+ if (int_status & INTR_PORT_CHANGE) {
+ if (pci_reason == PORT_CHANGE_REASON_RESUME) {
+ dcd_event_bus_signal(rhport, DCD_EVENT_RESUME, true);
+ } else {
+ // the undefined encoding falls back to full speed
+ const uint32_t pspd = (dcd_reg->PORTSC1 & PORTSC1_PORT_SPEED) >> PORTSC1_PORT_SPEED_POS;
+ const tusb_speed_t speed = (pspd == PORTSC1_PORT_SPEED_LOW) ? TUSB_SPEED_LOW :
+ (pspd == PORTSC1_PORT_SPEED_HIGH) ? TUSB_SPEED_HIGH : TUSB_SPEED_FULL;
+ dcd_event_bus_reset(rhport, speed, true);
+ // This reset is over, so the next port change is a resume. Leaving it at RESET instead would
+ // dispatch every later resume as another end-of-reset, clearing the queue heads mid-session.
+ _port_change_reason[rhport] = PORT_CHANGE_REASON_RESUME;
}
}
+ // No unplug detection yet, by the manual rather than by omission: IMXRT1060RM 42.7.31 (p.2470) says a zero
+ // Current Connect Status means the device "did not attach successfully or was forcibly
+ // disconnected by the software writing a zero to the Run bit ... It does not state the device
+ // being disconnected or suspended", so a cable pull raises no port change at all. VBUS via
+ // OTGSC BSV is the manual's disconnect indicator, and it is board dependent.
+
if (int_status & INTR_USB) {
// Make sure we read the latest version of _dcd_data.
dcd_dcache_clean_invalidate(&_dcd_data, sizeof(dcd_data_t));
@@ -674,7 +748,7 @@ void dcd_int_handler(uint8_t rhport) {
const uint32_t edpt_complete = dcd_reg->ENDPTCOMPLETE;
dcd_reg->ENDPTCOMPLETE = edpt_complete; // acknowledge
- // 23.10.12.3 Failed QTD also get ENDPTCOMPLETE set
+ // 42.5.6.6.4 Transfer Completion (p.2413): a failed dTD also sets ENDPTCOMPLETE
// nothing to do, we will submit xfer as error to usbd
// if (int_status & INTR_ERROR) { }
@@ -690,12 +764,39 @@ void dcd_int_handler(uint8_t rhport) {
}
// Set up Received
- // 23.10.10.2 Operational model for setup transfers
+ // 42.5.6.4.2 Control Endpoint Operation Model (p.2403)
// Must be after normal transfer complete since it is possible to have both previous control status + new setup
// in the same frame and we should handle previous status first.
if (dcd_reg->ENDPTSETUPSTAT) {
+ // 42.5.6.4.2.1 Setup Phase (p.2403) steps 1-2: duplicate the setup payload BEFORE clearing
+ // ENDPTSETUPSTAT -
+ // the clear releases the setup lockout and a back-to-back SETUP (usbtest case 10) can
+ // overwrite the queue-head buffer immediately after. The copy is read through the volatile
+ // qualifier rather than memcpy'd because C orders volatile accesses only against each
+ // other: a plain copy may legally be sunk past the lockout-releasing store below.
+ union {
+ tusb_control_request_t request;
+ uint8_t byte[8];
+ } setup;
+ const volatile uint8_t *setup_src = (const volatile uint8_t *)&_dcd_data.qhd[0][0].setup_request;
+ for (uint8_t i = 0; i < sizeof(setup.request); i++) {
+ setup.byte[i] = setup_src[i];
+ }
dcd_reg->ENDPTSETUPSTAT = dcd_reg->ENDPTSETUPSTAT;
- dcd_event_setup_received(rhport, (uint8_t *)(uintptr_t)&_dcd_data.qhd[0][0].setup_request, true);
+
+ // Retire a status/handshake phase left primed by the previous control sequence
+ // (IMXRT1060RM 42.5.6.4.2.1, p.2403), which would otherwise retire the response the task is about to
+ // prime for this setup. Skipped when EP0 has nothing primed or priming, since the manual
+ // does not want the flush wait in an interrupt handler when it has nothing to do.
+ // One volatile read per statement: C leaves their order unspecified within a single
+ // expression, which IAR rejects outright (Pa082).
+ const uint32_t ep0_mask = TU_BIT(0) | TU_BIT(16);
+ const uint32_t ep0_stat = dcd_reg->ENDPTSTAT;
+ const uint32_t ep0_prime = dcd_reg->ENDPTPRIME;
+ if ((ep0_stat | ep0_prime) & ep0_mask) {
+ flush_endpoints(dcd_reg, ep0_mask);
+ }
+ dcd_event_setup_received(rhport, setup.byte, true);
}
}
diff --git a/src/portable/chipidea/ci_hs/hcd_ci_hs.c b/src/portable/chipidea/ci_hs/hcd_ci_hs.c
index 3cb69acfa..0f24f5bb6 100644
--- a/src/portable/chipidea/ci_hs/hcd_ci_hs.c
+++ b/src/portable/chipidea/ci_hs/hcd_ci_hs.c
@@ -82,6 +82,10 @@ bool hcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) {
hcd_reg->USBMODE = USBMODE_CM_HOST;
#endif
+ #ifdef CI_HS_SET_AHB_BURST
+ CI_HS_SET_AHB_BURST(rhport);
+ #endif
+
#if !TUH_OPT_HIGH_SPEED
hcd_reg->PORTSC1 |= PORTSC1_FORCE_FULL_SPEED;
#endif
diff --git a/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c b/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c
index d5b03e4b1..42f6750b1 100644
--- a/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c
+++ b/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c
@@ -87,6 +87,10 @@ enum {
DEVCMDSTAT_SUSPEND_CHANGE_MASK = TU_BIT(25),
DEVCMDSTAT_RESET_CHANGE_MASK = TU_BIT(26),
DEVCMDSTAT_VBUS_DEBOUNCED_MASK = TU_BIT(28),
+
+ // write-1-to-clear latches
+ DEVCMDSTAT_W1C_MASK = DEVCMDSTAT_SETUP_RECEIVED_MASK | DEVCMDSTAT_CONNECT_CHANGE_MASK |
+ DEVCMDSTAT_SUSPEND_CHANGE_MASK | DEVCMDSTAT_RESET_CHANGE_MASK,
};
enum {
@@ -171,7 +175,9 @@ typedef struct
ep_cmd_sts_t ep[2*MAX_EP_PAIRS][2];
xfer_dma_t dma[2*MAX_EP_PAIRS];
- TU_ATTR_ALIGNED(64) uint8_t setup_packet[8];
+ // volatile: the controller DMAs a new setup packet into this buffer as soon as the SETUP
+ // latch is cleared, so reads of it must stay ordered against the register accesses around them
+ TU_ATTR_ALIGNED(64) volatile uint8_t setup_packet[8];
}dcd_data_t;
// EP list must be 256-byte aligned
@@ -180,8 +186,12 @@ typedef struct
// Use CFG_TUD_MEM_SECTION to place it accordingly.
CFG_TUD_MEM_SECTION TU_ATTR_ALIGNED(256) static dcd_data_t _dcd;
-// Dummy buffer to fix ZLPs overwriting the buffer (probably an USB/DMA controller bug)
-// TODO find way to save memory
+// Dummy buffer to fix ZLPs overwriting the buffer: Errata LPC55S6x USB.5 / LPC55S2x USB.4 - the
+// HS device controller always DMA-writes OUT data in 8-byte units, so up to 7 bytes land past the
+// received length. This redirects the ZLP case; the general short-OUT case is unhandled here
+// (TinyUSB's own endpoint buffers are sized/aligned so the spill stays inside them, but a tight
+// caller buffer can be overrun by up to 7 bytes - the SDK's documented workaround is a bounce
+// buffer). TODO find way to save memory
CFG_TUD_MEM_SECTION TU_ATTR_ALIGNED(64) static uint8_t dummy[8];
//--------------------------------------------------------------------+
@@ -221,7 +231,7 @@ static const dcd_controller_t _dcd_controller[] = {
// INTERNAL OBJECT & FUNCTION DECLARATION
//--------------------------------------------------------------------+
-TU_ATTR_ALWAYS_INLINE static inline uint16_t get_buf_offset(void const * buffer) {
+TU_ATTR_ALWAYS_INLINE static inline uint16_t get_buf_offset(void const volatile * buffer) {
uint32_t addr = (uint32_t) buffer;
TU_ASSERT( (addr & 0x3f) == 0, 0 );
return ( (addr >> 6) & 0xFFFFUL ) ;
@@ -247,6 +257,16 @@ TU_ATTR_ALWAYS_INLINE static inline bool rhport_is_highspeed(uint8_t rhport) {
return _dcd_controller[rhport].is_highspeed;
}
+
+// DEVCMDSTAT mixes RW fields with write-1-to-clear latches (SETUP + the 3 change bits): a blind
+// RMW writes a pending latch back as 1 and silently clears it (a SETUP eaten this way strands
+// EP0). Mask the latches on every update; pass one in set_mask only to clear it.
+TU_ATTR_ALWAYS_INLINE static inline void devcmdstat_update(dcd_registers_t* dcd_reg,
+ uint32_t clear_mask, uint32_t set_mask) {
+ const uint32_t v = dcd_reg->DEVCMDSTAT & ~(DEVCMDSTAT_W1C_MASK | clear_mask);
+ dcd_reg->DEVCMDSTAT = v | set_mask;
+}
+
//--------------------------------------------------------------------+
// CONTROLLER API
//--------------------------------------------------------------------+
@@ -284,8 +304,10 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) {
dcd_reg->DATABUFSTART = tu_align((uint32_t) &_dcd, TU_BIT(22)); // 22-bit alignment
dcd_reg->INTSTAT = dcd_reg->INTSTAT; // clear all pending interrupt
dcd_reg->INTEN = INT_DEVICE_STATUS_MASK;
- dcd_reg->DEVCMDSTAT |= DEVCMDSTAT_DEVICE_ENABLE_MASK | DEVCMDSTAT_DEVICE_CONNECT_MASK |
- DEVCMDSTAT_RESET_CHANGE_MASK | DEVCMDSTAT_CONNECT_CHANGE_MASK | DEVCMDSTAT_SUSPEND_CHANGE_MASK;
+ // deliberately clear every latch (incl. a SETUP left by a bootloader/warm start) for a
+ // deterministic init state
+ devcmdstat_update(dcd_reg, 0, DEVCMDSTAT_DEVICE_ENABLE_MASK | DEVCMDSTAT_DEVICE_CONNECT_MASK |
+ DEVCMDSTAT_W1C_MASK);
NVIC_ClearPendingIRQ(_dcd_controller[rhport].irqnum);
@@ -309,8 +331,7 @@ void dcd_set_address(uint8_t rhport, uint8_t dev_addr)
// Response with status first before changing device address
dcd_edpt_xfer(rhport, tu_edpt_addr(0, TUSB_DIR_IN), NULL, 0, false);
- dcd_reg->DEVCMDSTAT &= ~DEVCMDSTAT_DEVICE_ADDR_MASK;
- dcd_reg->DEVCMDSTAT |= dev_addr;
+ devcmdstat_update(dcd_reg, DEVCMDSTAT_DEVICE_ADDR_MASK, dev_addr);
}
void dcd_remote_wakeup(uint8_t rhport)
@@ -321,13 +342,13 @@ void dcd_remote_wakeup(uint8_t rhport)
void dcd_connect(uint8_t rhport)
{
dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs;
- dcd_reg->DEVCMDSTAT |= DEVCMDSTAT_DEVICE_CONNECT_MASK;
+ devcmdstat_update(dcd_reg, 0, DEVCMDSTAT_DEVICE_CONNECT_MASK);
}
void dcd_disconnect(uint8_t rhport)
{
dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs;
- dcd_reg->DEVCMDSTAT &= ~DEVCMDSTAT_DEVICE_CONNECT_MASK;
+ devcmdstat_update(dcd_reg, DEVCMDSTAT_DEVICE_CONNECT_MASK, 0);
}
void dcd_sof_enable(uint8_t rhport, bool en)
@@ -380,9 +401,17 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr)
uint8_t const ep_id = ep_addr2id(ep_addr);
+ // Preserve rf_tv: for non-control endpoints it is a TYPE bit, not the toggle value (UM11126:
+ // T=1 + RF 1/0 = interrupt/iso). Zeroing it here turned HS periodic interrupt endpoints into
+ // isochronous - no handshake on OUT, dead IN (usbtest cases 25/26 on lpc55 HS port).
+ // TODO implement the Errata LPC546xx USB.13 work-around (same semantics in UM11126): with RF/TV preserved at 1, TR
+ // loads the toggle from TV, so an HS interrupt endpoint restarts on DATA1 after clear-halt and
+ // the host discards one packet as a retransmission. The documented workaround needs an
+ // interrupt-on-NAK state machine (park as generic TR=1/TV=0, wait for a NAKed token to latch
+ // toggle 0 via EPTOGGLE, restore the type) - deferred; one lost packet beats the fully broken
+ // endpoint the old rf_tv clear caused.
_dcd.ep[ep_id][0].cmd_sts.stall = 0;
_dcd.ep[ep_id][0].cmd_sts.toggle_reset = 1;
- _dcd.ep[ep_id][0].cmd_sts.rf_tv = 0;
}
bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc)
@@ -432,7 +461,7 @@ void dcd_edpt_close_all (uint8_t rhport)
{
for (uint8_t ep_id = 0; ep_id < 2*_dcd_controller[rhport].ep_pairs; ++ep_id)
{
- _dcd.ep[ep_id][0].cmd_sts.active = _dcd.ep[ep_id][0].cmd_sts.active = 0; // TODO proper way is to EPSKIP then wait ep[][].active then write ep[][].disable (see table 778 in LPC55S69 Use Manual)
+ _dcd.ep[ep_id][0].cmd_sts.active = _dcd.ep[ep_id][1].cmd_sts.active = 0; // TODO proper way is to EPSKIP then wait ep[][].active then write ep[][].disable (see table 778 in LPC55S69 Use Manual)
_dcd.ep[ep_id][0].cmd_sts.disable = _dcd.ep[ep_id][1].cmd_sts.disable = 1;
}
}
@@ -538,7 +567,7 @@ static void bus_reset(uint8_t rhport)
dcd_reg->EPSKIP = 0xFFFFFFFF;
dcd_reg->INTSTAT = dcd_reg->INTSTAT; // clear all pending interrupt
- dcd_reg->DEVCMDSTAT |= DEVCMDSTAT_SETUP_RECEIVED_MASK; // clear setup received interrupt
+ devcmdstat_update(dcd_reg, 0, DEVCMDSTAT_SETUP_RECEIVED_MASK); // clear setup received interrupt
dcd_reg->INTEN = INT_DEVICE_STATUS_MASK | TU_BIT(0) | TU_BIT(1); // enable device status & control endpoints
}
@@ -597,18 +626,25 @@ void dcd_int_handler(uint8_t rhport)
{
dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs;
- uint32_t const cmd_stat = dcd_reg->DEVCMDSTAT;
-
uint32_t int_status = dcd_reg->INTSTAT;
- int_status &= dcd_reg->INTEN;
+ int_status &= dcd_reg->INTEN;
dcd_reg->INTSTAT = int_status; // Acknowledge handled interrupt
if (int_status == 0) return;
+ // Snapshot after the INTSTAT ack: latch bits persist (RWC) so nothing is lost, while the reverse
+ // order could consume INTSTAT bit0 for a SETUP not yet visible in the snapshot - stranding the
+ // SETUP (INTSTAT is edge-latched) and feeding bit0 to process_xfer_isr as a bogus completion.
+ uint32_t const cmd_stat = dcd_reg->DEVCMDSTAT;
+
//------------- Device Status -------------//
if ( int_status & INT_DEVICE_STATUS_MASK )
{
- dcd_reg->DEVCMDSTAT |= DEVCMDSTAT_RESET_CHANGE_MASK | DEVCMDSTAT_CONNECT_CHANGE_MASK | DEVCMDSTAT_SUSPEND_CHANGE_MASK;
+ // clear only the change latches observed in the snapshot: one latched by hardware between the
+ // snapshot and this write would be acknowledged unseen (its DEV_INT re-latches and dispatches
+ // next pass instead)
+ devcmdstat_update(dcd_reg, 0, cmd_stat &
+ (DEVCMDSTAT_RESET_CHANGE_MASK | DEVCMDSTAT_CONNECT_CHANGE_MASK | DEVCMDSTAT_SUSPEND_CHANGE_MASK));
if ( cmd_stat & DEVCMDSTAT_RESET_CHANGE_MASK) // bus reset
{
@@ -653,15 +689,43 @@ void dcd_int_handler(uint8_t rhport)
_dcd.ep[0][0].cmd_sts.active = _dcd.ep[1][0].cmd_sts.active = 0;
_dcd.ep[0][0].cmd_sts.stall = _dcd.ep[1][0].cmd_sts.stall = 0;
- dcd_reg->DEVCMDSTAT |= DEVCMDSTAT_SETUP_RECEIVED_MASK;
+ // UM flow: ack the latch FIRST, then read the payload. This IP has no setup lockout, so a
+ // back-to-back SETUP can overwrite _dcd.setup_packet at any time - but with the latch already
+ // released, any such overwrite re-latches SETUP_RECEIVED and is redelivered (worst case a
+ // superseded duplicate, absorbed by usbd's queued-setup counter). The reverse order can
+ // consume the newer SETUP's latch unseen and lose it.
+ devcmdstat_update(dcd_reg, 0, DEVCMDSTAT_SETUP_RECEIVED_MASK);
+
+ // UM11126 Fig 163 (control EP0 flowchart) requires clearing the EP0IN interrupt here: a
+ // control IN completion latched before this SETUP must not reach usbd after it, where it
+ // would be applied to the new request and arm its status stage early. EP0OUT goes with it -
+ // bit0 is set by SETUP reception too, and left set it would replay next pass as a phantom
+ // completion. Neither can discard live work: the SETUP latch NAKs all EP0 traffic until the
+ // update above, and both EP0 Active bits were cleared a few lines up.
+ dcd_reg->INTSTAT = TU_BIT(0) | TU_BIT(1);
- dcd_event_setup_received(rhport, _dcd.setup_packet, true);
+ // Copied a byte at a time rather than with memcpy: C orders volatile accesses only against
+ // each other, so a non-volatile copy of this buffer may be sunk below the guard read that
+ // follows - gcc does exactly that at -O2 and -O3, leaving only -Os correct.
+ uint8_t setup_copy[8];
+ for (uint8_t i = 0; i < sizeof(setup_copy); i++) {
+ setup_copy[i] = _dcd.setup_packet[i];
+ }
+
+ // a SETUP that raced in after the acks (its bit0 consumed above) makes this copy suspect:
+ // its latch is visible again, so re-raise the endpoint interrupt and let the next pass
+ // deliver the newer payload rather than passing up bytes that may be torn between the two
+ if (dcd_reg->DEVCMDSTAT & DEVCMDSTAT_SETUP_RECEIVED_MASK) {
+ dcd_reg->INTSETSTAT = TU_BIT(0);
+ } else {
+ dcd_event_setup_received(rhport, setup_copy, true);
+ }
// keep waiting for next setup
prepare_setup_packet(rhport);
- // clear bit0
- int_status = tu_bit_clear(int_status, 0);
+ // drop both EP0 bits: acked above, and neither belongs to the request this SETUP starts
+ int_status &= ~(TU_BIT(0) | TU_BIT(1));
}
// Endpoint transfer complete interrupt
diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c
index 86aa54510..b2f1a93a4 100644
--- a/src/portable/synopsys/dwc2/dcd_dwc2.c
+++ b/src/portable/synopsys/dwc2/dcd_dwc2.c
@@ -1143,7 +1143,12 @@ static void handle_incomplete_iso_in(uint8_t rhport) {
xfer_ctl_t *xfer = XFER_CTL_BASE(epnum, TUSB_DIR_IN);
if (xfer->iso_retry > 0) {
xfer->iso_retry--;
- // Restart ISO transfe: re-write TSIZ and CTL
+ // Restart ISO transfer: re-write DMA address, TSIZ, and CTL
+ #if CFG_TUD_DWC2_DMA_ENABLE
+ if (dma_device_enabled(dwc2)) {
+ epin->diepdma = (uintptr_t) xfer->buffer;
+ }
+ #endif
dwc2_ep_tsize_t deptsiz = {.value = 0};
deptsiz.xfer_size = xfer->total_len;
deptsiz.packet_count = tu_div_ceil(xfer->total_len, xfer->max_size);