summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHa Thach <[email protected]>2026-07-19 00:33:14 +0700
committerGitHub <[email protected]>2026-07-19 00:33:14 +0700
commit50f30773522d21bc599f00c18980ae651f780ff8 (patch)
tree315b497109a480beafe5df10608aae06814cf704 /src
parent3b4544b974054ae95394401adc663a6bd17c7d33 (diff)
parent03f764e5914a96573eee4dd288182deccc60a35a (diff)
Merge pull request #3775 from hathach/claude/add-usb-debug-sniff
usb-target-debug/usb-sniffer skills
Diffstat (limited to 'src')
-rw-r--r--src/portable/nxp/lpc17_40/dcd_lpc17_40.c281
1 files changed, 245 insertions, 36 deletions
diff --git a/src/portable/nxp/lpc17_40/dcd_lpc17_40.c b/src/portable/nxp/lpc17_40/dcd_lpc17_40.c
index 182710016..b577d0e9f 100644
--- a/src/portable/nxp/lpc17_40/dcd_lpc17_40.c
+++ b/src/portable/nxp/lpc17_40/dcd_lpc17_40.c
@@ -19,6 +19,12 @@
//--------------------------------------------------------------------+
#define DCD_ENDPOINT_MAX 32
+// The iso machinery (5th DD word + packet-size memory) costs USB RAM on every build;
+// compile it only when a class that can open an iso endpoint is enabled. Keep this in
+// sync with the classes that actually arm an iso endpoint: audio, video, BTH (voice),
+// and vendor (its optional CFG_TUD_VENDOR_EP_ISO_* endpoints, exercised by usbtest).
+#define DCD_ISO_ENABLED (CFG_TUD_AUDIO || CFG_TUD_VIDEO || CFG_TUD_VENDOR || CFG_TUD_BTH)
+
typedef struct TU_ATTR_ALIGNED(4)
{
//------------- Word 0 -------------//
@@ -48,11 +54,37 @@ typedef struct TU_ATTR_ALIGNED(4)
volatile uint16_t present_count; // For non-iso : The number of bytes transferred by the DMA engine
// For iso : number of packets
+#if DCD_ISO_ENABLED
//------------- Word 4 -------------//
- // uint32_t iso_packet_size_addr; // iso only, can be omitted for non-iso
+ volatile uint32_t iso_packet_size_addr; // iso only: pointer into iso packet-size memory,
+ // advanced by hardware after each packet
+#endif
}dma_desc_t;
-TU_VERIFY_STATIC( sizeof(dma_desc_t) == 16, "size is not correct"); // TODO not support ISO for now
+TU_VERIFY_STATIC( sizeof(dma_desc_t) == (DCD_ISO_ENABLED ? 20 : 16), "size is not correct");
+
+// Hardware fixes endpoint type by number: 3, 6, 9, 12 are the iso-capable ones.
+// Constant per ep_id (= 2*epnum + dir) — unlike dd->isochronous, which dcd_edpt_xfer
+// transiently zeroes while rebuilding the DD, this is safe to dispatch on from the ISR.
+// TU_ATTR_UNUSED: every caller is under #if DCD_ISO_ENABLED, so non-iso builds don't
+// reference it and clang -Wunused-function (fatal) would otherwise reject the build.
+TU_ATTR_UNUSED TU_ATTR_ALWAYS_INLINE static inline bool ep_id_is_iso(uint8_t ep_id) {
+ uint8_t const epnum = (uint8_t)(ep_id >> 1);
+ return (epnum % 3) == 0 && (epnum != 0) && (epnum != 15);
+}
+
+#if DCD_ISO_ENABLED
+// Isochronous packet-size memory (UM10562 12.15.6.3): one word per packet.
+// IN : software fills Packet_length (bits 15:0), 0 = ZLP
+// OUT: hardware writes Frame_number (31:17) | Packet_valid (16) | Packet_length (15:0)
+// Iso-capable endpoint numbers are 3, 6, 9, 12 -> 8 slots (x2 directions).
+// One packet moves per FRAME, so a deep queue only adds latency: 8 frames is plenty.
+#define ISO_MAX_PACKETS 8
+#define ISO_SLOT_COUNT 8
+TU_ATTR_ALWAYS_INLINE static inline uint8_t iso_slot(uint8_t ep_id) {
+ return (uint8_t)(((ep_id / 6) - 1) * 2 + (ep_id & 1)); // ep_id = 2*epnum + dir, epnum in {3,6,9,12}
+}
+#endif
typedef struct
{
@@ -66,11 +98,17 @@ typedef struct
{
uint8_t* out_buffer;
uint8_t out_bytes;
+ volatile bool out_queued; // an OUT xfer is queued; out_buffer may legitimately be NULL (status ZLP)
volatile bool out_received; // indicate if data is already received in endpoint
uint8_t in_bytes;
} control;
+#if DCD_ISO_ENABLED
+ // iso packet-size memory, must be DMA-reachable like the DDs
+ volatile uint32_t iso_psize[ISO_SLOT_COUNT][ISO_MAX_PACKETS];
+#endif
+
} dcd_data_t;
CFG_TUD_MEM_SECTION TU_ATTR_ALIGNED(128) static dcd_data_t _dcd;
@@ -79,6 +117,29 @@ CFG_TUD_MEM_SECTION TU_ATTR_ALIGNED(128) static dcd_data_t _dcd;
//--------------------------------------------------------------------+
// SIE Command
//--------------------------------------------------------------------+
+
+// The SIE command protocol (CmdCode + CCEMPTY/CDFULL handshake) and the
+// slave-mode Ctrl/RxData/TxData registers are shared between thread-mode API
+// calls and dcd_int_handler, and are not reentrant: an ISR preempting a
+// thread-mode SIE sequence consumes its handshake flags and overwrites
+// CmdCode (symptom: EP0 wedges/answers stale data right after SET_INTERFACE
+// stall/clear-stall bursts overlapping bulk EOT interrupts). Mask only the
+// USB interrupt around those sequences; safe to nest, including from the ISR.
+static inline bool usb_irq_lock(void)
+{
+ bool const enabled = NVIC_GetEnableIRQ(USB_IRQn) != 0;
+ if (enabled)
+ {
+ NVIC_DisableIRQ(USB_IRQn); // CMSIS already ends this with DSB+ISB
+ }
+ return enabled;
+}
+
+static inline void usb_irq_unlock(bool enabled)
+{
+ if (enabled) NVIC_EnableIRQ(USB_IRQn);
+}
+
static void sie_cmd_code (sie_cmdphase_t phase, uint8_t code_data)
{
LPC_USB->DevIntClr = (DEV_INT_COMMAND_CODE_EMPTY_MASK | DEV_INT_COMMAND_DATA_FULL_MASK);
@@ -92,19 +153,28 @@ static void sie_cmd_code (sie_cmdphase_t phase, uint8_t code_data)
static void sie_write (uint8_t cmd_code, uint8_t data_len, uint8_t data)
{
+ bool const lock = usb_irq_lock();
+
sie_cmd_code(SIE_CMDPHASE_COMMAND, cmd_code);
if (data_len)
{
sie_cmd_code(SIE_CMDPHASE_WRITE, data);
}
+
+ usb_irq_unlock(lock);
}
static uint8_t sie_read (uint8_t cmd_code)
{
+ bool const lock = usb_irq_lock();
+
sie_cmd_code(SIE_CMDPHASE_COMMAND , cmd_code);
sie_cmd_code(SIE_CMDPHASE_READ , cmd_code);
- return (uint8_t) LPC_USB->CmdData;
+ uint8_t const data = (uint8_t) LPC_USB->CmdData;
+
+ usb_irq_unlock(lock);
+ return data;
}
//--------------------------------------------------------------------+
@@ -117,6 +187,11 @@ static inline uint8_t ep_addr2idx(uint8_t ep_addr)
static void set_ep_size(uint8_t ep_id, uint16_t max_packet_size)
{
+ // ReEp RMW + the EP_RLZED handshake share DevIntSt with the ISR: a bus reset
+ // from dcd_int_handler writes DevIntClr = 0xFFFFFFFF and would consume the
+ // flag this spin waits on, hanging it forever -> same lock as the SIE paths.
+ bool const lock = usb_irq_lock();
+
// follows example in 11.10.4.2
LPC_USB->ReEp |= TU_BIT(ep_id);
LPC_USB->EpInd = ep_id; // select index before setting packet size
@@ -124,6 +199,8 @@ static void set_ep_size(uint8_t ep_id, uint16_t max_packet_size)
while ((LPC_USB->DevIntSt & DEV_INT_ENDPOINT_REALIZED_MASK) == 0) {}
LPC_USB->DevIntClr = DEV_INT_ENDPOINT_REALIZED_MASK;
+
+ usb_irq_unlock(lock);
}
@@ -230,6 +307,7 @@ static inline uint8_t byte2dword(uint8_t bytes)
static void control_ep_write(void const * buffer, uint8_t len)
{
uint32_t const * buf32 = (uint32_t const *) buffer;
+ bool const lock = usb_irq_lock(); // Ctrl/TxData + SIE sequence must not interleave with the ISR
LPC_USB->Ctrl = USBCTRL_WRITE_ENABLE_MASK; // logical endpoint = 0
LPC_USB->TxPLen = (uint32_t) len;
@@ -245,10 +323,14 @@ static void control_ep_write(void const * buffer, uint8_t len)
// select control IN & validate the endpoint
sie_write(SIE_CMDCODE_ENDPOINT_SELECT+1, 0, 0);
sie_write(SIE_CMDCODE_BUFFER_VALIDATE , 0, 0);
+
+ usb_irq_unlock(lock);
}
static uint8_t control_ep_read(void * buffer, uint8_t len)
{
+ bool const lock = usb_irq_lock(); // Ctrl/RxData + SIE sequence must not interleave with the ISR
+
LPC_USB->Ctrl = USBCTRL_READ_ENABLE_MASK; // logical endpoint = 0
while ((LPC_USB->RxPLen & USBRXPLEN_PACKET_READY_MASK) == 0) {} // TODO blocking, should have timeout
@@ -267,6 +349,7 @@ static uint8_t control_ep_read(void * buffer, uint8_t len)
sie_write(SIE_CMDCODE_ENDPOINT_SELECT+0, 0, 0);
sie_write(SIE_CMDCODE_BUFFER_CLEAR , 0, 0);
+ usb_irq_unlock(lock);
return len;
}
@@ -281,8 +364,9 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc)
uint8_t const epnum = tu_edpt_number(p_endpoint_desc->bEndpointAddress);
uint8_t const ep_id = ep_addr2idx(p_endpoint_desc->bEndpointAddress);
- // Endpoint type is fixed to endpoint number
- // 1: interrupt, 2: Bulk, 3: Iso and so on
+ // Endpoint type is fixed to endpoint number (1 interrupt, 2 bulk, 3 iso, ...).
+ // Iso endpoints are armed via dcd_edpt_iso_alloc/activate, never through here
+ // (TUP_DCD_EDPT_ISO_ALLOC is defined for this IP), so only bulk/interrupt land here.
switch ( p_endpoint_desc->bmAttributes.xfer )
{
case TUSB_XFER_INTERRUPT:
@@ -293,10 +377,6 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc)
TU_ASSERT((epnum % 3) == 2 || (epnum == 15));
break;
- case TUSB_XFER_ISOCHRONOUS:
- TU_ASSERT((epnum % 3) == 0 && (epnum != 0) && (epnum != 15));
- break;
-
default:
break;
}
@@ -307,9 +387,7 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc)
//------------- first DD prepare -------------//
dma_desc_t* const dd = &_dcd.dd[ep_id];
- tu_memclr(dd, sizeof(dma_desc_t));
-
- dd->isochronous = (p_endpoint_desc->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) ? 1 : 0;
+ tu_memclr(dd, sizeof(dma_desc_t)); // non-iso: isochronous stays 0
dd->max_packet_size = ep_size;
dd->retired = 1; // invalid at first
@@ -319,16 +397,54 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc)
}
bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) {
+#if DCD_ISO_ENABLED
(void)rhport;
- (void)ep_addr;
- (void)largest_packet_size;
+ uint8_t const ep_id = ep_addr2idx(ep_addr);
+
+ // hardware fixes iso to endpoint numbers 3, 6, 9, 12
+ TU_ASSERT(ep_id_is_iso(ep_id));
+ TU_ASSERT(largest_packet_size > 0);
+
+ set_ep_size(ep_id, largest_packet_size);
+
+ dma_desc_t* const dd = &_dcd.dd[ep_id];
+ tu_memclr(dd, sizeof(dma_desc_t));
+ dd->isochronous = 1;
+ dd->max_packet_size = largest_packet_size;
+ dd->retired = 1; // invalid at first
+
+ sie_write(SIE_CMDCODE_ENDPOINT_SET_STATUS + ep_id, 1, 0);
+ return true;
+#else
+ (void)rhport; (void)ep_addr; (void)largest_packet_size;
return false;
+#endif
}
bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) {
+#if DCD_ISO_ENABLED
(void)rhport;
- (void)desc_ep;
+ uint8_t const ep_id = ep_addr2idx(desc_ep->bEndpointAddress);
+ dma_desc_t* const dd = &_dcd.dd[ep_id];
+
+ // same fixed-number rule as alloc: without it a rejected-but-ignored alloc (classes
+ // discard that return) would set isochronous on a non-iso ep_id and underflow iso_slot()
+ TU_ASSERT(ep_id_is_iso(ep_id));
+
+ // kill any armed transfer from a previous alternate setting
+ LPC_USB->EpDMADis = TU_BIT(ep_id);
+ _dcd.udca[ep_id] = NULL;
+
+ dd->isochronous = 1;
+ dd->max_packet_size = tu_edpt_packet_size(desc_ep);
+ dd->retired = 1;
+
+ sie_write(SIE_CMDCODE_ENDPOINT_SET_STATUS + ep_id, 1, 0);
+ return true;
+#else
+ (void)rhport; (void)desc_ep;
return false;
+#endif
}
void dcd_edpt_close_all (uint8_t rhport)
@@ -369,19 +485,28 @@ static bool control_xact(uint8_t rhport, uint8_t dir, uint8_t * buffer, uint8_t
control_ep_write(buffer, len);
}else
{
+ // guard the out_received/out_buffer handshake against the EP0 OUT ISR
+ bool const lock = usb_irq_lock();
+
if ( _dcd.control.out_received )
{
// Already received the DATA OUT packet
_dcd.control.out_received = false;
- _dcd.control.out_buffer = NULL;
- _dcd.control.out_bytes = 0;
uint8_t received = control_ep_read(buffer, len);
+ // event queued with in_isr=true, which skips the queue's own locking: keep the
+ // USB IRQ masked across it, or a real ISR completion could interleave the write
dcd_event_xfer_complete(0, 0, received, XFER_RESULT_SUCCESS, true);
+ usb_irq_unlock(lock);
}else
{
+ // buffer is NULL for a status-stage ZLP: signal the pending xfer explicitly,
+ // NOT via out_buffer != NULL — a NULL-buffer queue mistaken for "nothing queued"
+ // leaves out_received stale and poisons the next control OUT data stage.
_dcd.control.out_buffer = buffer;
_dcd.control.out_bytes = len;
+ _dcd.control.out_queued = true;
+ usb_irq_unlock(lock);
}
}
@@ -406,26 +531,68 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t t
uint16_t const ep_size = dd->max_packet_size;
uint8_t is_iso = dd->isochronous;
- tu_memclr(dd, sizeof(dma_desc_t));
- dd->isochronous = is_iso;
- dd->max_packet_size = ep_size;
- dd->buffer = (uint32_t) buffer;
- dd->buflen = total_bytes;
+#if DCD_ISO_ENABLED
+ if ( is_iso )
+ {
+ // iso: buflen counts packets; per-packet sizes live in the packet-size memory.
+ // One packet moves per frame (UM10562 12.15.6: DMA request is raised for
+ // DMA-enabled iso endpoints on every FRAME interrupt, both directions).
+ // Validate BEFORE touching the DD: bailing out mid-rebuild would leave a
+ // zeroed (retired=0 -> serviceable) descriptor armed for the frame engine.
+ TU_ASSERT(ep_size > 0);
+ uint16_t const packets = (total_bytes > 0) ? (uint16_t) tu_div_ceil(total_bytes, ep_size) : 1;
+ TU_ASSERT(packets <= ISO_MAX_PACKETS);
+
+ uint8_t const slot = iso_slot(ep_id);
+ uint16_t remain = total_bytes;
+ for ( uint16_t i = 0; i < packets; i++ )
+ {
+ uint16_t const pkt_len = tu_min16(remain, ep_size);
+ // IN: length to send (0 = ZLP). OUT: hardware writes back
+ // Frame_number|Packet_valid|Packet_length -- prefill 0 so a frame the
+ // hardware never wrote (missed/invalid) cannot read back as data.
+ _dcd.iso_psize[slot][i] = (ep_id & 1) ? pkt_len : 0;
+ remain = (uint16_t)(remain - pkt_len);
+ }
- _dcd.udca[ep_id] = dd;
+ tu_memclr(dd, sizeof(dma_desc_t));
+ dd->isochronous = 1;
+ dd->max_packet_size = ep_size;
+ dd->buffer = (uint32_t) buffer;
+ dd->buflen = packets;
+ dd->iso_packet_size_addr = (uint32_t) &_dcd.iso_psize[slot][0];
- if ( ep_id % 2 )
+ _dcd.udca[ep_id] = dd;
+ LPC_USB->EpDMAEn = TU_BIT(ep_id); // frame-triggered: no DMARSet, no EpIntEn
+ }
+ else
+#else
+ (void) is_iso;
+#endif
{
- // Clear EP interrupt before Enable DMA
- LPC_USB->EpIntEn &= ~TU_BIT(ep_id);
- LPC_USB->EpDMAEn = TU_BIT(ep_id);
+ tu_memclr(dd, sizeof(dma_desc_t));
+ dd->max_packet_size = ep_size;
+ dd->buffer = (uint32_t) buffer;
+ dd->buflen = total_bytes;
- // endpoint IN need to actively raise DMA request
- LPC_USB->DMARSet = TU_BIT(ep_id);
- }else
- {
- // Enable DMA
- LPC_USB->EpDMAEn = TU_BIT(ep_id);
+ _dcd.udca[ep_id] = dd;
+
+ if ( ep_id % 2 )
+ {
+ // Clear EP interrupt before Enable DMA
+ // EpIntEn read-modify-write races the ISR's own RMWs -> lock
+ bool const lock = usb_irq_lock();
+ LPC_USB->EpIntEn &= ~TU_BIT(ep_id);
+ LPC_USB->EpDMAEn = TU_BIT(ep_id);
+ usb_irq_unlock(lock);
+
+ // endpoint IN need to actively raise DMA request
+ LPC_USB->DMARSet = TU_BIT(ep_id);
+ }else
+ {
+ // Enable DMA
+ LPC_USB->EpDMAEn = TU_BIT(ep_id);
+ }
}
return true;
@@ -451,13 +618,20 @@ static void control_xfer_isr(uint8_t rhport, uint32_t ep_int_status)
uint8_t setup_packet[8];
control_ep_read(setup_packet, 8); // TODO read before clear setup above
+ // a new SETUP voids any half-finished control state
+ _dcd.control.out_queued = false;
+ _dcd.control.out_received = false;
+ _dcd.control.out_buffer = NULL;
+ _dcd.control.out_bytes = 0;
+
dcd_event_setup_received(rhport, setup_packet, true);
}
- else if ( _dcd.control.out_buffer )
+ else if ( _dcd.control.out_queued )
{
- // software queued transfer previously
+ // software queued transfer previously (out_buffer NULL = status ZLP)
uint8_t received = control_ep_read(_dcd.control.out_buffer, _dcd.control.out_bytes);
+ _dcd.control.out_queued = false;
_dcd.control.out_buffer = NULL;
_dcd.control.out_bytes = 0;
@@ -513,7 +687,32 @@ static void dd_complete_isr(uint8_t rhport, uint8_t ep_id)
uint8_t result = (dd->status == DD_STATUS_NORMAL || dd->status == DD_STATUS_DATA_UNDERUN) ? XFER_RESULT_SUCCESS : XFER_RESULT_FAILED;
uint8_t const ep_addr = (ep_id / 2) | ((ep_id & 0x01) ? TUSB_DIR_IN_MASK : 0);
- dcd_event_xfer_complete(rhport, ep_addr, dd->present_count, result, true);
+ uint32_t xferred_bytes;
+#if DCD_ISO_ENABLED
+ if ( ep_id_is_iso(ep_id) )
+ {
+ // present_count is in packets; actual byte counts are in the packet-size memory
+ // (IN: as programmed by us, OUT: Packet_length written back by hardware,
+ // guarded by Packet_valid -- a frame with no packet must count as 0)
+ uint8_t const slot = iso_slot(ep_id);
+ uint16_t const packets = tu_min16(dd->present_count, ISO_MAX_PACKETS);
+ xferred_bytes = 0;
+ for (uint16_t i = 0; i < packets; i++)
+ {
+ uint32_t const psize = _dcd.iso_psize[slot][i];
+ if ( (ep_id & 1) || (psize & TU_BIT(16)) )
+ {
+ xferred_bytes += (psize & 0xFFFFu);
+ }
+ }
+ }
+ else
+#endif
+ {
+ xferred_bytes = dd->present_count;
+ }
+
+ dcd_event_xfer_complete(rhport, ep_addr, (uint16_t) xferred_bytes, result, true);
}
// main USB IRQ handler
@@ -569,6 +768,16 @@ void dcd_int_handler(uint8_t rhport)
{
if ( tu_bit_test(eot, ep_id) )
{
+ // dispatch on the hardware's fixed ep-number/type map, NOT dd->isochronous:
+ // thread-mode dcd_edpt_xfer transiently zeroes the DD while rebuilding it
+#if DCD_ISO_ENABLED
+ if ( ep_id_is_iso(ep_id) )
+ {
+ // iso: last packet already left with its frame; complete both directions here
+ dd_complete_isr(rhport, ep_id);
+ }
+ else
+#endif
if ( ep_id & 0x01 )
{
// IN enable EpInt for end of usb transfer