summaryrefslogtreecommitdiff
path: root/src/portable/wch/dcd_ch32_usbfs.c
blob: ec521224e5127d56b25a2b88ef0fabcd1cab97ec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
/*
 * SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Tran
 * SPDX-FileCopyrightText: Copyright (c) 2024 Ha Thach
 * SPDX-License-Identifier: MIT
 *
 * This file is part of the TinyUSB stack.
 */

#include "tusb_option.h"

#if CFG_TUD_ENABLED && defined(TUP_USBIP_WCH_USBFS) && CFG_TUD_WCH_USBIP_USBFS

  #include "device/dcd.h"
  #include "ch32_usbfs_reg.h"

  /* private defines */
  #define EP_MAX         (8)

  // EP3 IN buffer size. CH32V20x/V30x/F20x USBFS support full-speed iso packets up to 1023 B on
  // endpoint 3 (every other endpoint is 64 B); those parts set CH32_USBFS_EP3_1023_BUFSIZE in
  // ch32_usbfs_reg.h. V103/X035/CH58x cap every endpoint at 64 B. Overridable per project.
  #ifndef CFG_TUD_WCH_USBFS_EP3_BUFSIZE
    #ifdef CH32_USBFS_EP3_1023_BUFSIZE
      #define CFG_TUD_WCH_USBFS_EP3_BUFSIZE 1023
    #else
      #define CFG_TUD_WCH_USBFS_EP3_BUFSIZE 64
    #endif
  #endif

  // Struct-based EP register access (uniform layout). CH58X has a different register map and
  // defines EP_DMA/EP_TX_LEN/EP_CTRL itself in ch32_usbfs_reg.h.
  #if CFG_TUSB_MCU == OPT_MCU_CH583
    // CH58X EP registers split into a low block (EP0-4) and a high block (EP5-7). Walk from each
    // block's first slot by the 4-byte slot stride (pointer arithmetic off slot 0, so the unused
    // ternary branch's index can't trip -Warray-bounds). EP4 has no DMA register of its own (it
    // shares EP0's, slot 0) and is never written (see ep_shares_ep0_dma()).
    #define EP_TX_LEN(ep) (*((ep) <= 4u ? &USBOTG_FS->EP_CTRL_0_4[0].T_LEN + (ep) * 4u \
                                        : &USBOTG_FS->EP_CTRL_5_7[0].T_LEN + ((ep) - 5u) * 4u))
    #define EP_CTRL(ep)   (*((ep) <= 4u ? &USBOTG_FS->EP_CTRL_0_4[0].CTRL  + (ep) * 4u \
                                        : &USBOTG_FS->EP_CTRL_5_7[0].CTRL  + ((ep) - 5u) * 4u))
    #define EP_DMA(ep)    (*((ep) <= 3u ? &USBOTG_FS->EP_DMA_0_3[0].DMA + (ep) * 2u \
                             : (ep) == 4u ? &USBOTG_FS->EP_DMA_0_3[0].DMA \
                                          : &USBOTG_FS->EP_DMA_5_7[0].DMA + ((ep) - 5u) * 2u))
  #else
    #define EP_DMA(ep)     ((&USBOTG_FS->UEP0_DMA)[ep])
    #define EP_TX_LEN(ep)  ((&USBOTG_FS->UEP0_TX_LEN)[2 * ep])
    #define EP_TX_CTRL(ep) ((&USBOTG_FS->UEP0_TX_CTRL)[4 * ep])
    #define EP_RX_CTRL(ep) ((&USBOTG_FS->UEP0_RX_CTRL)[4 * ep])
  #endif

// Endpoint control register access. The newer USBFS IP (CH32V20x/V307/X035) has separate
// TX_CTRL and RX_CTRL bytes per endpoint; the older IP (CH32V103) has a single combined
// UEPn_CTRL register. These helpers hide the difference so the rest of the driver is shared.
// Values use the newer-IP encoding (USBFS_EP_T_*/USBFS_EP_R_*); the combined path remaps them.
#ifdef CH32_USBFS_EP_CTRL_COMBINED
  #ifndef EP_CTRL // parts with a custom register map (CH58X) define EP_CTRL directly in reg.h
  #define EP_CTRL(ep) EP_TX_CTRL(ep) // UEPn_TX_CTRL field aliases the combined UEPn_CTRL register
  #endif

  static inline uint8_t ep_tx_to_comb(uint8_t v) {
    uint8_t c = v & USBFS_EP_T_RES_MASK; // IN response: bits [1:0] in both encodings
    if (v & USBFS_EP_T_TOG)      { c |= USBFS_EPC_T_TOG; }
    if (v & USBFS_EP_T_AUTO_TOG) { c |= USBFS_EPC_AUTO_TOG; }
    return c;
  }
  static inline uint8_t ep_rx_to_comb(uint8_t v) {
    uint8_t c = (uint8_t) ((v & USBFS_EP_R_RES_MASK) << USBFS_EPC_R_RES_SHIFT); // OUT response -> bits [3:2]
    if (v & USBFS_EP_R_TOG)      { c |= USBFS_EPC_R_TOG; }
    if (v & USBFS_EP_R_AUTO_TOG) { c |= USBFS_EPC_AUTO_TOG; }
    return c;
  }
  // Set IN side (response/toggle/auto-tog), preserving the OUT response + OUT toggle.
  static inline void ep_tx_ctrl_set(uint8_t ep, uint8_t v) {
    EP_CTRL(ep) = (uint8_t) ((EP_CTRL(ep) & (USBFS_EPC_R_RES_MASK | USBFS_EPC_R_TOG)) | ep_tx_to_comb(v));
  }
  // Set OUT side, preserving the IN response + IN toggle.
  static inline void ep_rx_ctrl_set(uint8_t ep, uint8_t v) {
    EP_CTRL(ep) = (uint8_t) ((EP_CTRL(ep) & (USBFS_EPC_T_RES_MASK | USBFS_EPC_T_TOG)) | ep_rx_to_comb(v));
  }
  static inline void ep_tx_set_response(uint8_t ep, uint8_t res) {
    EP_CTRL(ep) = (uint8_t) ((EP_CTRL(ep) & ~USBFS_EPC_T_RES_MASK) | (res & USBFS_EP_T_RES_MASK));
  }
  static inline void ep_rx_set_response(uint8_t ep, uint8_t res) {
    EP_CTRL(ep) = (uint8_t) ((EP_CTRL(ep) & ~USBFS_EPC_R_RES_MASK) | ((res & USBFS_EP_R_RES_MASK) << USBFS_EPC_R_RES_SHIFT));
  }
  #define EP0_SETUP_RX_TOG USBFS_EP_R_TOG // combined IP: data/status stage after SETUP is DATA1
#else
  static inline void ep_tx_ctrl_set(uint8_t ep, uint8_t v) { EP_TX_CTRL(ep) = v; }
  static inline void ep_rx_ctrl_set(uint8_t ep, uint8_t v) { EP_RX_CTRL(ep) = v; }
  static inline void ep_tx_set_response(uint8_t ep, uint8_t res) {
    EP_TX_CTRL(ep) = (uint8_t) ((EP_TX_CTRL(ep) & ~USBFS_EP_T_RES_MASK) | res);
  }
  static inline void ep_rx_set_response(uint8_t ep, uint8_t res) {
    EP_RX_CTRL(ep) = (uint8_t) ((EP_RX_CTRL(ep) & ~USBFS_EP_R_RES_MASK) | res);
  }
  #define EP0_SETUP_RX_TOG 0
#endif

// Hardware auto data-toggle flag. Parts whose AUTO_TOG is reliable OR it into the EP setup so the
// controller flips DATA0/DATA1 itself; CH58x (CH32_USBFS_EP_MANUAL_TOG) leaves it clear and the
// ISR flips the toggle bit after each packet instead.
#ifdef CH32_USBFS_EP_MANUAL_TOG
  #define EP_T_AUTO_TOG 0
  #define EP_R_AUTO_TOG 0
#else
  #define EP_T_AUTO_TOG USBFS_EP_T_AUTO_TOG
  #define EP_R_AUTO_TOG USBFS_EP_R_AUTO_TOG
#endif

/* private data */
struct usb_xfer {
  bool     valid;
  uint8_t *buffer;
  size_t   len;
  size_t   processed_len;
  size_t   max_size;
};

static struct {
  bool            ep0_tog;
  bool            isochronous[EP_MAX][2]; // per [ep][dir]: an ep number may be iso in one direction
  struct usb_xfer xfer[EP_MAX][2];
#ifdef CH32_USBFS_EP4_SHARES_EP0
  // CH58X buffers laid out by hand so EP0/EP4 don't burn two unused buffer[] slots. EP0 and EP4
  // share one contiguous 192-byte DMA region (EP4 has no DMA register of its own):
  // EP0 [0:63] (half-duplex OUT+IN) + EP4 OUT [64:127] + EP4 IN [128:191]. Every other endpoint
  // (incl. EP3, which is bulk-only here — CH58X has no isochronous support) gets a plain 128-byte
  // OUT+IN buffer, so no oversized EP3 buffer is needed.
  TU_ATTR_ALIGNED(4) uint8_t ep0_ep4_buffer[3 * 64];
  TU_ATTR_ALIGNED(4) uint8_t ep1_buffer[2][64];
  TU_ATTR_ALIGNED(4) uint8_t ep2_buffer[2][64];
  TU_ATTR_ALIGNED(4) uint8_t ep3_buffer[2][64];
  TU_ATTR_ALIGNED(4) uint8_t ep5_buffer[2][64];
  TU_ATTR_ALIGNED(4) uint8_t ep6_buffer[2][64];
  TU_ATTR_ALIGNED(4) uint8_t ep7_buffer[2][64];
#else
  // Every endpoint gets a 64-byte OUT + 64-byte IN buffer.
  TU_ATTR_ALIGNED(4) uint8_t buffer[EP_MAX][2][64];
  #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
  // ...except EP3, which supports full-speed iso packets up to 1023 B on CH32V20x/V30x/F20x, so its
  // IN buffer is enlarged (OUT stays 64 B; an OUT transfer >64 B on EP3 would overwrite queued IN).
  TU_ATTR_ALIGNED(4) struct {
    uint8_t out[64];
    uint8_t in[CFG_TUD_WCH_USBFS_EP3_BUFSIZE];
    uint8_t pad;
  } ep3_buffer;
  #endif
#endif
} data;

// DMA / copy buffer pointers per endpoint. The WCH USBFS buffer holds OUT (RX) at offset 0 and
// IN (TX) at +64; EP0 is half-duplex and reuses its OUT chunk for IN. On CH58X, EP0/EP4 share
// ep0_ep4_buffer and the regular endpoints use their own named buffer (see the struct above).
#ifdef CH32_USBFS_EP4_SHARES_EP0
// OUT base of the regular CH58X endpoints (EP1/2/3/5/6/7; EP0/EP4 share ep0_ep4_buffer).
static inline uint8_t* ch58x_ep_buffer(uint8_t ep) {
  switch (ep) {
    case 1:  return data.ep1_buffer[0];
    case 2:  return data.ep2_buffer[0];
    case 3:  return data.ep3_buffer[0];
    case 5:  return data.ep5_buffer[0];
    case 6:  return data.ep6_buffer[0];
    default: return data.ep7_buffer[0]; // ep == 7
  }
}
#endif

static inline uint32_t ep_dma_addr(uint8_t ep) {
#ifdef CH32_USBFS_EP4_SHARES_EP0
  if (ep == 0 || ep == 4) { return (uint32_t) &data.ep0_ep4_buffer[0]; } // EP4 shares EP0's DMA
  return (uint32_t) ch58x_ep_buffer(ep);
#else
  #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
  if (ep == 3) { return (uint32_t) &data.ep3_buffer.out[0]; } // EP3 has an enlarged IN buffer
  #endif
  return (uint32_t) &data.buffer[ep][0];
#endif
}

static inline uint8_t* ep_out_buf(uint8_t ep) {
#ifdef CH32_USBFS_EP4_SHARES_EP0
  if (ep == 0) { return &data.ep0_ep4_buffer[0]; }
  if (ep == 4) { return &data.ep0_ep4_buffer[64]; }
  return ch58x_ep_buffer(ep);
#else
  #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
  if (ep == 3) { return data.ep3_buffer.out; }
  #endif
  return data.buffer[ep][TUSB_DIR_OUT];
#endif
}

static inline uint8_t* ep_in_buf(uint8_t ep) {
#ifdef CH32_USBFS_EP4_SHARES_EP0
  if (ep == 0) { return &data.ep0_ep4_buffer[0]; } // EP0 half-duplex: IN reuses OUT chunk
  if (ep == 4) { return &data.ep0_ep4_buffer[128]; }
  return ch58x_ep_buffer(ep) + 64; // IN at +64 within the endpoint's 128-byte buffer
#else
  if (ep == 0) { return data.buffer[0][TUSB_DIR_OUT]; } // EP0 half-duplex: IN reuses OUT chunk
  #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
  if (ep == 3) { return data.ep3_buffer.in; } // enlarged IN buffer for full-speed iso
  #endif
  return data.buffer[ep][TUSB_DIR_IN];
#endif
}

// EP4 on CH58X has no DMA register (shares EP0's); skip its EP_DMA() write.
static inline bool ep_shares_ep0_dma(uint8_t ep) {
#ifdef CH32_USBFS_EP4_SHARES_EP0
  return ep == 4;
#else
  (void) ep;
  return false;
#endif
}

/* private helpers */
static void update_in(uint8_t rhport, uint8_t ep, bool force) {
  struct usb_xfer *xfer = &data.xfer[ep][TUSB_DIR_IN];
  if (xfer->valid) {
    if (force || xfer->len) {
      size_t len = TU_MIN(xfer->max_size, xfer->len);
#if CFG_TUSB_MCU == OPT_MCU_CH583
      // Every CH58x endpoint buffer is 64 bytes; cap the copy so an iso mps a class mistakenly set
      // larger can't write past the buffer into a neighbouring endpoint's.
      len = TU_MIN(len, 64u);
#endif
      memcpy(ep_in_buf(ep), xfer->buffer, len);
      xfer->buffer += len;
      xfer->len -= len;
      xfer->processed_len += len;

      EP_TX_LEN(ep) = len;
      if (ep == 0) {
        ep_tx_ctrl_set(0, USBFS_EP_T_RES_ACK | (data.ep0_tog ? USBFS_EP_T_TOG : 0));
        data.ep0_tog  = !data.ep0_tog;
      } else if (data.isochronous[ep][TUSB_DIR_IN]) {
        ep_tx_set_response(ep, USBFS_EP_T_RES_NYET);
      } else {
        ep_tx_set_response(ep, USBFS_EP_T_RES_ACK);
      }
    } else {
      xfer->valid = false;
      if (ep == 0) {
        ep_tx_ctrl_set(0, USBFS_EP_T_RES_NAK | (data.ep0_tog ? USBFS_EP_T_TOG : 0));
      } else if (!data.isochronous[ep][TUSB_DIR_IN]) {
        ep_tx_set_response(ep, USBFS_EP_T_RES_NAK);
      }
      dcd_event_xfer_complete(rhport, ep | TUSB_DIR_IN_MASK, xfer->processed_len, XFER_RESULT_SUCCESS, true);
    }
  }
}

static void update_out(uint8_t rhport, uint8_t ep, size_t rx_len) {
  struct usb_xfer *xfer = &data.xfer[ep][TUSB_DIR_OUT];
  if (xfer->valid) {
    size_t len = TU_MIN(xfer->max_size, TU_MIN(xfer->len, rx_len));
#if CFG_TUSB_MCU == OPT_MCU_CH583
    len = TU_MIN(len, 64u); // cap to the 64-byte EP buffer (see update_in)
#endif
    memcpy(xfer->buffer, ep_out_buf(ep), len);
    xfer->buffer += len;
    xfer->len -= len;
    xfer->processed_len += len;

    if (xfer->len == 0 || len < xfer->max_size) {
      xfer->valid = false;
      dcd_event_xfer_complete(rhport, ep, xfer->processed_len, XFER_RESULT_SUCCESS, true);
    }

    if (ep == 0) {
      ep_rx_set_response(0, USBFS_EP_R_RES_NAK);
    } else {
      uint8_t rx_res =
        data.isochronous[ep][TUSB_DIR_OUT] ? USBFS_EP_R_RES_NYET : (xfer->valid ? USBFS_EP_R_RES_ACK : USBFS_EP_R_RES_NAK);
      ep_rx_set_response(ep, rx_res);
    }
  }
}

static void reset_ep_ctrls(void) {
  for (uint8_t ep = 1; ep < EP_MAX; ep++) {
    if (!ep_shares_ep0_dma(ep)) { EP_DMA(ep) = ep_dma_addr(ep); }
    EP_TX_LEN(ep)  = 0;
    ep_tx_ctrl_set(ep, EP_T_AUTO_TOG | USBFS_EP_T_RES_NYET);
    ep_rx_ctrl_set(ep, EP_R_AUTO_TOG | USBFS_EP_R_RES_NYET);
  }
}

/* public functions */
bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) {
  (void)rh_init;
  // init registers
  USBOTG_FS->BASE_CTRL = USBFS_CTRL_SYS_CTRL | USBFS_CTRL_INT_BUSY | USBFS_CTRL_DMA_EN;
  USBOTG_FS->UDEV_CTRL = USBFS_UDEV_CTRL_PD_DIS | USBFS_UDEV_CTRL_PORT_EN;
  USBOTG_FS->DEV_ADDR  = 0x00;

  USBOTG_FS->INT_FG = 0xFF;
  USBOTG_FS->INT_EN = USBFS_INT_EN_BUS_RST | USBFS_INT_EN_TRANSFER | USBFS_INT_EN_SUSPEND;

  // setup endpoint 0 (also backs EP4's buffer on CH58X via the shared DMA region)
  EP_DMA(0)     = ep_dma_addr(0);
  EP_TX_LEN(0)  = 0;
  ep_tx_ctrl_set(0, USBFS_EP_T_RES_NAK);
  ep_rx_ctrl_set(0, USBFS_EP_R_RES_ACK);

  // enable other endpoints but NAK everything
  USBOTG_FS->UEP4_1_MOD = 0xCC;
  USBOTG_FS->UEP2_3_MOD = 0xCC;
#if CFG_TUSB_MCU == OPT_MCU_CH583
  // CH58X: a single mode register enables EP5/6/7 RX+TX (different bit layout than CH32).
  USBOTG_FS->UEP567_MOD = RB_UEP5_RX_EN | RB_UEP5_TX_EN | RB_UEP6_RX_EN | RB_UEP6_TX_EN |
                          RB_UEP7_RX_EN | RB_UEP7_TX_EN;
#else
  USBOTG_FS->UEP5_6_MOD = 0xCC;
  USBOTG_FS->UEP7_MOD   = 0x0C;
#endif

  reset_ep_ctrls();

  dcd_connect(rhport);

  return true;
}

void dcd_int_handler(uint8_t rhport) {
  (void)rhport;
  uint8_t status = USBOTG_FS->INT_FG;
  if (status & USBFS_INT_FG_TRANSFER) {
    uint8_t  int_st   = USBOTG_FS->INT_ST;
    uint8_t  ep       = USBFS_INT_ST_MASK_UIS_ENDP(int_st);
    uint8_t  token    = USBFS_INT_ST_MASK_UIS_TOKEN(int_st);
    uint16_t rx_len   = USBOTG_FS->RX_LEN;

    switch (token) {
      case PID_OUT: {
        // Drop an OUT packet whose data toggle doesn't match what we expect -- a host retransmit
        // after a lost ACK, or a host that doesn't alternate DATA0/DATA1. The hardware auto-toggle
        // does not reject these on its own, so the check is needed on every variant. EP0 keeps its
        // own toggle via the SETUP/status flow and is exempt; isochronous is DATA0-only (no toggle),
        // so its packets must not be toggle-checked.
        if (ep != 0 && !data.isochronous[ep][TUSB_DIR_OUT] && !(int_st & USBFS_INT_ST_TOG_OK)) { break; }
#ifdef CH32_USBFS_EP_MANUAL_TOG
        // CH58x has no hardware auto-toggle: advance the expected RX toggle after each accepted packet
        // (EP0 included -- it also has no auto-toggle and a control-OUT data stage can span packets).
        // Iso endpoints are DATA0-only, so leave them alone (matches the PID_IN path).
        if (!data.isochronous[ep][TUSB_DIR_OUT]) { EP_CTRL(ep) ^= USBFS_EPC_R_TOG; }
#endif
        update_out(rhport, ep, rx_len);
        break;
      }

      case PID_IN:
#ifdef CH32_USBFS_EP_MANUAL_TOG
        // Manual toggle: flip the TX toggle after each ACK'd IN packet (EP0 manages its own).
        // Isochronous transfers are DATA0-only (no toggle), so leave iso endpoints alone.
        if (ep != 0 && !data.isochronous[ep][TUSB_DIR_IN]) { EP_CTRL(ep) ^= USBFS_EPC_T_TOG; }
#endif
        update_in(rhport, ep, false);
        break;

      case PID_SETUP:
        // setup clears stall
        ep_tx_ctrl_set(0, USBFS_EP_T_RES_NAK);
        data.ep0_tog  = true;
        // A new SETUP supersedes any control transfer still in flight; drop its stale EP0 state so a
        // spurious EP0 IN/OUT can't run update_in()/update_out() against the previous request.
        data.xfer[0][TUSB_DIR_OUT].valid = false;
        data.xfer[0][TUSB_DIR_IN].valid  = false;

        uint8_t *ep0_out = ep_out_buf(0);
        const tusb_control_request_t *setup = (const tusb_control_request_t *)ep0_out;
        // EP0_SETUP_RX_TOG arms the data/status stage at DATA1 on the combined-control IP
        ep_rx_ctrl_set(0, ((setup->wLength == 0) ? USBFS_EP_R_RES_ACK : USBFS_EP_R_RES_NAK) | EP0_SETUP_RX_TOG);

        dcd_event_setup_received(rhport, ep0_out, true);
        break;
    }

    USBOTG_FS->INT_FG = USBFS_INT_FG_TRANSFER;
  } else if (status & USBFS_INT_FG_BUS_RST) {
    data.ep0_tog                        = true;
    data.xfer[0][TUSB_DIR_OUT].max_size = 64;
    data.xfer[0][TUSB_DIR_IN].max_size  = 64;

    // dcd_event_bus_reset(rhport, (USBOTG_FS->BASE_CTRL & USBFS_CTRL_LOW_SPEED) ? TUSB_SPEED_LOW : TUSB_SPEED_FULL,
    // true);
    dcd_event_bus_reset(rhport, (USBOTG_FS->UDEV_CTRL & USBFS_UDEV_CTRL_LOW_SPEED) ? TUSB_SPEED_LOW : TUSB_SPEED_FULL,
                        true);

    USBOTG_FS->DEV_ADDR = 0x00;
    ep_rx_ctrl_set(0, USBFS_EP_R_RES_ACK);

    reset_ep_ctrls();

    USBOTG_FS->INT_FG = USBFS_INT_FG_BUS_RST;
  } else if (status & USBFS_INT_FG_SUSPEND) {
#if CFG_TUSB_MCU == OPT_MCU_CH583
    // CH58x raises this single interrupt for both suspend and resume; MIS_ST's suspend bit tells
    // them apart (set while suspended, clear once resumed) so tud_resume_cb() actually fires.
    dcd_event_t event = {.rhport = rhport,
                         .event_id = (USBOTG_FS->MIS_ST & USBFS_MIS_ST_SUSPEND) ? DCD_EVENT_SUSPEND : DCD_EVENT_RESUME};
#else
    dcd_event_t event = {.rhport = rhport, .event_id = DCD_EVENT_SUSPEND};
#endif
    dcd_event_handler(&event, true);
    USBOTG_FS->INT_FG = USBFS_INT_FG_SUSPEND;
  }
}

void dcd_int_enable(uint8_t rhport) {
  (void)rhport;
  NVIC_EnableIRQ(USBHD_IRQn);
}

void dcd_int_disable(uint8_t rhport) {
  (void)rhport;
  NVIC_DisableIRQ(USBHD_IRQn);
}

void dcd_set_address(uint8_t rhport, uint8_t dev_addr) {
  (void)dev_addr;
  dcd_edpt_xfer(rhport, 0x80, NULL, 0, false); // zlp status response
}

void dcd_remote_wakeup(uint8_t rhport) {
  (void)rhport;
  // TODO optional
}

void dcd_connect(uint8_t rhport) {
  (void)rhport;
  USBOTG_FS->BASE_CTRL |= USBFS_CTRL_DEV_PUEN;
}

void dcd_disconnect(uint8_t rhport) {
  (void)rhport;
  USBOTG_FS->BASE_CTRL &= ~USBFS_CTRL_DEV_PUEN;
}

void dcd_sof_enable(uint8_t rhport, bool en) {
  (void)rhport;
  (void)en;

  // TODO implement later
}

void dcd_edpt0_status_complete(uint8_t rhport, const tusb_control_request_t *request) {
  (void)rhport;
  if (request->bmRequestType_bit.recipient == TUSB_REQ_RCPT_DEVICE &&
      request->bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && request->bRequest == TUSB_REQ_SET_ADDRESS) {
#if CFG_TUSB_MCU == OPT_MCU_CH583
    // On CH58x R8_USB_DEV_AD bit 7 is a user general-purpose flag; only bits [6:0] are the address.
    USBOTG_FS->DEV_ADDR = (uint8_t)((USBOTG_FS->DEV_ADDR & 0x80u) | (request->wValue & 0x7Fu));
#else
    USBOTG_FS->DEV_ADDR = (uint8_t)request->wValue;
#endif
  }
}

bool dcd_edpt_open(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) {
  (void)rhport;
  uint8_t ep  = tu_edpt_number(desc_ep->bEndpointAddress);
  uint8_t dir = tu_edpt_dir(desc_ep->bEndpointAddress);
  TU_ASSERT(ep < EP_MAX);

  data.isochronous[ep][dir]   = false; // (re)opening as a non-iso endpoint clears any stale iso flag
  data.xfer[ep][dir].max_size = tu_edpt_packet_size(desc_ep);

  if (ep != 0) {
    // Opening clears the toggle to DATA0 (ep_*_ctrl_set writes the toggle bit clear since v has no
    // R/T_TOG); with manual toggle EP_*_AUTO_TOG is 0 so the ISR owns subsequent toggling.
    if (dir == TUSB_DIR_OUT) {
      ep_rx_ctrl_set(ep, EP_R_AUTO_TOG | USBFS_EP_R_RES_NAK);
    } else {
      ep_tx_ctrl_set(ep, EP_T_AUTO_TOG | USBFS_EP_T_RES_NAK);
    }
  }
  return true;
}

void dcd_edpt_close_all(uint8_t rhport) {
  (void)rhport;
  // TODO optional
}

bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) {
  (void)rhport;
  uint8_t ep  = tu_edpt_number(ep_addr);
  uint8_t dir = tu_edpt_dir(ep_addr);
  TU_ASSERT(ep < EP_MAX);

  // Endpoint buffers are 64 B, except EP3 IN which is enlarged for full-speed iso on the parts that
  // support 1023-byte EP3 packets (CH32V20x/V30x/F20x; CFG_TUD_WCH_USBFS_EP3_BUFSIZE). Reject a
  // larger mps rather than running off the end into the neighbouring endpoint's memory.
  uint16_t max_packet = 64;
#if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
  if (ep == 3 && dir == TUSB_DIR_IN) { max_packet = CFG_TUD_WCH_USBFS_EP3_BUFSIZE; }
#endif
  TU_VERIFY(largest_packet_size <= max_packet);

  data.isochronous[ep][dir]   = true;
  data.xfer[ep][dir].max_size = largest_packet_size;
  return true;
}

bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) {
  (void)rhport;
  const uint8_t ep = tu_edpt_number(desc_ep->bEndpointAddress);
  const uint8_t dir = tu_edpt_dir(desc_ep->bEndpointAddress);

  // a transfer armed before SET_INTERFACE survives to here (no dcd close on this port): drop the
  // stale descriptor and NAK the endpoint so the ISR can't complete it against the old buffer
  data.xfer[ep][dir].valid = false;
  if (dir == TUSB_DIR_IN) {
    ep_tx_set_response(ep, USBFS_EP_T_RES_NAK);
  } else {
    ep_rx_set_response(ep, USBFS_EP_R_RES_NAK);
  }
  return true;
}

bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes, bool is_isr) {
  (void)is_isr;
  (void)rhport;
  uint8_t ep  = tu_edpt_number(ep_addr);
  uint8_t dir = tu_edpt_dir(ep_addr);

  struct usb_xfer *xfer = &data.xfer[ep][dir];
  // Keep the IRQ masked across the whole arming sequence: update_in()/ep_rx_set_response() do a
  // read-modify-write of the (combined) EP control register, which the ISR also RMWs to flip the
  // manual data toggle; re-enabling before they run lets a transfer IRQ clobber that toggle.
  dcd_int_disable(rhport);
  xfer->valid         = true;
  xfer->buffer        = buffer;
  xfer->len           = total_bytes;
  xfer->processed_len = 0;

  if (dir == TUSB_DIR_IN) {
    update_in(rhport, ep, true);
  } else {
    uint8_t rx_res = data.isochronous[ep][TUSB_DIR_OUT] ? USBFS_EP_R_RES_NYET : USBFS_EP_R_RES_ACK;
    ep_rx_set_response(ep, rx_res);
  }
  dcd_int_enable(rhport);
  return true;
}

void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) {
  (void)rhport;
  uint8_t ep  = tu_edpt_number(ep_addr);
  uint8_t dir = tu_edpt_dir(ep_addr);
  if (ep == 0) {
    if (dir == TUSB_DIR_OUT) {
      ep_rx_ctrl_set(0, USBFS_EP_R_RES_STALL);
    } else {
      EP_TX_LEN(0)  = 0;
      ep_tx_ctrl_set(0, USBFS_EP_T_RES_STALL);
    }
  } else {
    if (dir == TUSB_DIR_OUT) {
      ep_rx_set_response(ep, USBFS_EP_R_RES_STALL);
    } else {
      ep_tx_set_response(ep, USBFS_EP_T_RES_STALL);
    }
  }
}

void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) {
  (void)rhport;
  uint8_t ep  = tu_edpt_number(ep_addr);
  uint8_t dir = tu_edpt_dir(ep_addr);
  if (ep == 0) {
    if (dir == TUSB_DIR_OUT) {
      ep_rx_ctrl_set(0, USBFS_EP_R_RES_ACK);
    }
  } else {
    // clear-stall resets the toggle to DATA0 (USB spec); manual-toggle parts then re-sync via ISR.
    // Preserve an in-flight receive: if a read is still armed (the class driver considers it
    // submitted and won't re-arm), fall back to ACK, not NAK, or the endpoint NAKs forever and the
    // host times out (usbtest toggle test 29 clears the halt between bulk writes on an armed EP).
    if (dir == TUSB_DIR_OUT) {
      uint8_t res = data.xfer[ep][TUSB_DIR_OUT].valid
                    ? (data.isochronous[ep][TUSB_DIR_OUT] ? USBFS_EP_R_RES_NYET : USBFS_EP_R_RES_ACK)
                    : USBFS_EP_R_RES_NAK;
      ep_rx_ctrl_set(ep, EP_R_AUTO_TOG | res);
    } else {
      ep_tx_ctrl_set(ep, EP_T_AUTO_TOG | USBFS_EP_T_RES_NAK);
    }
  }
}
#endif