/* * 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