diff --git a/components/hal/esp32s2/include/hal/usbh_hal.h b/components/hal/esp32s2/include/hal/usbh_hal.h index 5dce919232..6e1f05773b 100644 --- a/components/hal/esp32s2/include/hal/usbh_hal.h +++ b/components/hal/esp32s2/include/hal/usbh_hal.h @@ -44,6 +44,7 @@ NOTE: Thread safety is the responsibility fo the HAL user. All USB Host HAL /** * @brief Channel states + * */ typedef enum { USBH_HAL_CHAN_STATE_HALTED = 0, /**< The channel is halted. No transfer descriptor list is being executed */ @@ -57,6 +58,7 @@ typedef enum { * @brief Host port HAL events */ typedef enum { + USBH_HAL_PORT_EVENT_NONE, /**< No event occurred, or could not decode interrupt */ USBH_HAL_PORT_EVENT_CHAN, /**< A channel event has occurred. Call the the channel event handler instead */ USBH_HAL_PORT_EVENT_CONN, /**< The host port has detected a connection */ USBH_HAL_PORT_EVENT_DISCONN, /**< The host port has been disconnected */ @@ -74,7 +76,6 @@ typedef enum { USBH_HAL_CHAN_EVENT_SLOT_HALT, /**< The channel as completed execution of a single transfer descriptor in a list. Channel is now halted */ USBH_HAL_CHAN_EVENT_ERROR, /**< The channel has encountered an error. Channel is now halted. */ USBH_HAL_CHAN_EVENT_HALT_REQ, /**< The channel has been successfully halted as requested */ - USBH_HAL_CHAN_EVENT_SUDDEN_HLT, /**< The channel was suddenly halted (e.g. due to a disconnect). */ } usbh_hal_chan_event_t; // ------------------------------- HAL Errors ---------------------------------- @@ -87,7 +88,6 @@ typedef enum { USBH_HAL_CHAN_ERROR_BNA, /**< Buffer Not Available error (i.e., transfer slot is unfilled */ USBH_HAL_CHAN_ERROR_PKT_BBL, /**< Packet babbler error (packet exceeded MPS) */ USBH_HAL_CHAN_ERROR_STALL, /**< STALL response received */ - USBH_HAL_CHAN_ERROR_AHB, /**< AHB error */ } usbh_hal_chan_error_t; // ----------------------- Transfer Descriptor Related ------------------------- @@ -102,6 +102,11 @@ typedef enum { /** * @brief Status value of a transfer descriptor + * + * A transfer descriptor's status remains unexecuted until the entire transfer + * descriptor completes (either successfully or an error). Therefore, if a + * channel halt is requested before a transfer descriptor completes, the + * transfer descriptoor remains unexecuted. */ #define USBH_HAL_XFER_DESC_STS_SUCCESS USBH_LL_QTD_STATUS_SUCCESS #define USBH_HAL_XFER_DESC_STS_PKTERR USBH_LL_QTD_STATUS_PKTERR @@ -120,7 +125,9 @@ typedef struct { uint32_t bEndpointAddress: 8; /**< Endpoint address (containing endpoint number and direction) */ uint32_t mps: 11; /**< Maximum Packet Size */ uint32_t dev_addr: 8; /**< Device Address */ - uint32_t reserved3: 3; + uint32_t ls_via_fs_hub: 1; /**< The endpoint is on a LS device that is routed through an FS hub. + Setting this bit will lead to the addition of the PREamble packet */ + uint32_t reserved2: 2; }; uint32_t val; }; @@ -148,7 +155,7 @@ typedef struct { struct { union { struct { - bool slot_acquired: 1; /**< The transfer descriptor list slot has been acquired */ + uint32_t slot_acquired: 1; /**< The transfer descriptor list slot has been acquired */ uint32_t reserved7: 7; uint32_t cur_qtd_idx: 8; /**< Index of the first QTD in chain of QTDs being executed */ uint32_t qtd_list_len: 8; /**< Length of QTD list in number of QTDs */ @@ -178,8 +185,8 @@ typedef struct { } flags; //Channel related struct { - int num_allocd; /**< Number of channels currently allocated */ - int chan_pend_intrs_msk; /**< Bit mask of channels with pending interrupts */ + int num_allocd; /**< Number of channels currently allocated */ + uint32_t chan_pend_intrs_msk; /**< Bit mask of channels with pending interrupts */ usbh_hal_chan_t *hdls[USBH_HAL_NUM_CHAN]; /**< Handles of each channel. Set to NULL if channel has not been allocated */ } channels; } usbh_hal_context_t; @@ -417,13 +424,17 @@ static inline usb_speed_t usbh_hal_port_get_conn_speed(usbh_hal_context_t *hal) * @brief Disable the debounce lock * * This function should be called after calling usbh_hal_port_check_if_connected() - * and will allow connection/disconnection events to occur again. + * and will allow connection/disconnection events to occur again. Any pending + * connection or disconenction interrupts are cleared. * * @param hal Context of the HAL layer */ static inline void usbh_hal_disable_debounce_lock(usbh_hal_context_t *hal) { hal->flags.dbnc_lock_enabled = 0; + //Clear Conenction and disconenction interrupt in case it triggered again + usb_ll_intr_clear(hal->dev, USB_LL_INTR_CORE_DISCONNINT); + usbh_ll_hprt_intr_clear(hal->dev, USBH_LL_INTR_HPRT_PRTENCHNG); //Reenable the hprt (connection) and disconnection interrupts usb_ll_en_intrs(hal->dev, USB_LL_INTR_CORE_PRTINT | USB_LL_INTR_CORE_DISCONNINT); } @@ -659,7 +670,7 @@ static inline void usbh_hal_chan_slot_acquire(usbh_hal_chan_t *chan_obj, void *x chan_obj->slot.owner_ctx = owner_ctx; chan_obj->slot.flags.cur_qtd_idx = 0; //Start from the first descriptor chan_obj->slot.flags.qtd_list_len = desc_list_len; - chan_obj->slot.flags.slot_acquired = true; + chan_obj->slot.flags.slot_acquired = 1; //Store the descriptor list length in the HCTSIZ register. Address of desc list is set when channel is activated usbh_ll_chan_set_qtd_list_len(chan_obj->regs, desc_list_len); } @@ -696,7 +707,7 @@ static inline void usbh_hal_chan_slot_release(usbh_hal_chan_t *chan_obj, void ** assert(chan_obj->slot.flags.slot_acquired); *xfer_desc_list = (void *)chan_obj->slot.xfer_desc_list; *desc_list_len = chan_obj->slot.flags.qtd_list_len; - chan_obj->slot.flags.slot_acquired = false; + chan_obj->slot.flags.slot_acquired = 0; } /** @@ -734,11 +745,16 @@ static inline int usbh_hal_chan_get_next_desc_index(usbh_hal_chan_t *chan_obj) * active, this function will return false and users must wait for the * USBH_HAL_CHAN_EVENT_HALT_REQ event before treating the channel as halted. * + * @note When a transfer is in progress (i.e., the channel is active) and a halt + * is requested, the channel will halt after the next USB packet is completed. + * If the transfer has more pending packets, the transfer will just be + * marked as USBH_HAL_XFER_DESC_STS_NOT_EXECUTED. + * * @param chan_obj Channel object * @return true The channel is already halted * @return false The halt was requested, wait for USBH_HAL_CHAN_EVENT_HALT_REQ */ -bool usbh_hal_chan_request_halt(usbh_hal_chan_t *chan_obj); +bool usbh_hal_chan_slot_request_halt(usbh_hal_chan_t *chan_obj); /* ----------------------------------------------------------------------------- -------------------------------- Event Handling -------------------------------- diff --git a/components/hal/esp32s2/include/hal/usbh_ll.h b/components/hal/esp32s2/include/hal/usbh_ll.h index aa26cb6fba..af82e25b82 100644 --- a/components/hal/esp32s2/include/hal/usbh_ll.h +++ b/components/hal/esp32s2/include/hal/usbh_ll.h @@ -283,8 +283,8 @@ static inline bool usb_ll_check_core_soft_reset(usbh_dev_t *hw) /** * @brief Reads and clears the global interrupt register * - * @param hw - * @return uint32_t + * @param hw Start address of the DWC_OTG registers + * @return uint32_t Mask of interrupts */ static inline uint32_t usb_ll_intr_read_and_clear(usbh_dev_t *hw) { @@ -294,6 +294,18 @@ static inline uint32_t usb_ll_intr_read_and_clear(usbh_dev_t *hw) return gintsts.val; } +/** + * @brief Clear specific interrupts + * + * @param hw Start address of the DWC_OTG registers + * @param intr_msk Mask of interrupts to clear + */ +static inline void usb_ll_intr_clear(usbh_dev_t *hw, uint32_t intr_msk) +{ + //All GINTSTS fields are either W1C or read only. So safe to write directly + hw->gintsts_reg.val = intr_msk; +} + // --------------------------- GINTMSK Register -------------------------------- static inline void usb_ll_en_intrs(usbh_dev_t *hw, uint32_t intr_mask) @@ -403,26 +415,36 @@ static inline void usbh_ll_hcfg_set_fsls_pclk_sel(usbh_dev_t *hw) } /** - * @brief Sets some default values to HCFG to operate in Host mode wiht scatter/gather DMA + * @brief Sets some default values to HCFG to operate in Host mode with scatter/gather DMA * * @param hw */ -static inline void usbh_ll_hcfg_set_defaults(usbh_dev_t *hw) +static inline void usbh_ll_hcfg_set_defaults(usbh_dev_t *hw, usb_speed_t speed) { hw->hcfg_reg.descdma = 1; //Enable scatt/gatt hw->hcfg_reg.fslssupp = 1; //FS/LS supp only - hw->hcfg_reg.fslspclksel = 1; //48MHz PHY clock + /* + Indicate to the OTG core what speed the PHY clock is at + Note: It seems like our PHY has an implicit 8 divider applied when in LS mode, + so the values of FSLSPclkSel and FrInt have to be adjusted accordingly. + */ + hw->hcfg_reg.fslspclksel = (speed == USB_SPEED_FULL) ? 1 : 2; hw->hcfg_reg.perschedena = 0; //Disable perio sched } // ----------------------------- HFIR Register --------------------------------- -static inline void usbh_ll_hfir_set_defaults(usbh_dev_t *hw) +static inline void usbh_ll_hfir_set_defaults(usbh_dev_t *hw, usb_speed_t speed) { usb_hfir_reg_t hfir; hfir.val = hw->hfir_reg.val; hfir.hfirrldctrl = 0; //Disable dynamic loading - hfir.frint = 48000; //Set frame interval to 48000 cycles of 48MHz clock (i.e. equals to 1ms) + /* + Set frame interval to be equal to 1ms + Note: It seems like our PHY has an implicit 8 divider applied when in LS mode, + so the values of FSLSPclkSel and FrInt have to be adjusted accordingly. + */ + hfir.frint = (speed == USB_SPEED_FULL) ? 48000 : 6000; hw->hfir_reg.val = hfir.val; } @@ -611,6 +633,13 @@ static inline uint32_t usbh_ll_hprt_intr_read_and_clear(usbh_dev_t *hw) return (hprt.val & (USBH_LL_HPRT_W1C_MSK & ~(USBH_LL_HPRT_ENA_MSK))); } +static inline void usbh_ll_hprt_intr_clear(usbh_dev_t *hw, uint32_t intr_mask) +{ + usb_hprt_reg_t hprt; + hprt.val = hw->hprt_reg.val; + hw->hprt_reg.val = ((hprt.val & ~USBH_LL_HPRT_ENA_MSK) & ~USBH_LL_HPRT_W1C_MSK) | intr_mask; +} + //Per Channel registers // --------------------------- HCCHARi Register -------------------------------- @@ -665,9 +694,11 @@ static inline void usbh_ll_chan_set_ep_type(volatile usb_host_chan_regs_t *chan, } } -static inline void usbh_ll_chan_set_ls(volatile usb_host_chan_regs_t *chan) +//Indicates whether channel is commuunicating with a LS device connected via a FS hub. Setting this bit to 1 will cause +//each packet to be preceded by a PREamble packet +static inline void usbh_ll_chan_set_lspddev(volatile usb_host_chan_regs_t *chan, bool is_ls) { - chan->hcchar_reg.lspddev = 1; + chan->hcchar_reg.lspddev = is_ls; } static inline void usbh_ll_chan_set_dir(volatile usb_host_chan_regs_t *chan, bool is_in) @@ -685,11 +716,12 @@ static inline void usbh_ll_chan_set_mps(volatile usb_host_chan_regs_t *chan, uin chan->hcchar_reg.mps = mps; } -static inline void usbh_ll_chan_hcchar_init(volatile usb_host_chan_regs_t *chan, int dev_addr, int ep_num, int mps, usb_xfer_type_t type, bool is_in) +static inline void usbh_ll_chan_hcchar_init(volatile usb_host_chan_regs_t *chan, int dev_addr, int ep_num, int mps, usb_xfer_type_t type, bool is_in, bool is_ls) { //Sets all persistent fields of the channel over its lifetime usbh_ll_chan_set_dev_addr(chan, dev_addr); usbh_ll_chan_set_ep_type(chan, type); + usbh_ll_chan_set_lspddev(chan, is_ls); usbh_ll_chan_set_dir(chan, is_in); usbh_ll_chan_set_ep_num(chan, ep_num); usbh_ll_chan_set_mps(chan, mps); diff --git a/components/hal/esp32s2/usbh_hal.c b/components/hal/esp32s2/usbh_hal.c index 98f1ee92c8..b00a08ae8e 100644 --- a/components/hal/esp32s2/usbh_hal.c +++ b/components/hal/esp32s2/usbh_hal.c @@ -84,22 +84,29 @@ _Static_assert((RX_FIFO_LEN + NPTX_FIFO_LEN + PTX_FIFO_LEN + REG_FIFO_LEN) <= HW * The following channel interrupt bits are currently checked (in order LSB to MSB) * - USBH_LL_INTR_CHAN_XFERCOMPL * - USBH_LL_INTR_CHAN_CHHLTD - * - USBH_LL_INTR_CHAN_AHBERR * - USBH_LL_INTR_CHAN_STALL * - USBH_LL_INTR_CHAN_BBLEER * - USBH_LL_INTR_CHAN_BNAINTR * - USBH_LL_INTR_CHAN_XCS_XACT_ERR * + * Note the following points about channel interrupts: * - Not all bits are unmaskable under scatter/gather * - Those bits proxy their interrupt through the USBH_LL_INTR_CHAN_CHHLTD bit * - USBH_LL_INTR_CHAN_XCS_XACT_ERR is always unmasked + * - When USBH_LL_INTR_CHAN_BNAINTR occurs, USBH_LL_INTR_CHAN_CHHLTD will NOT. + * - USBH_LL_INTR_CHAN_AHBERR doesn't actually ever happen on our system )i.e., ESP32S2 and later): + * - If the QTD list's starting address is an invalid address (e.g., NULL), the core will attempt to fetch that + * address for a transfer descriptor and probably gets all zeroes. It will interpret the zero as a bad QTD and + * return a USBH_LL_INTR_CHAN_BNAINTR instead. + * - If the QTD's buffer pointer is an invalid address, the core will attempt to read/write data to/from that + * invalid buffer address with NO INDICATION OF ERROR. The transfer will be acknowledged and treated as + * successful. Bad buffer pointers MUST BE CHECKED FROM HIGHER LAYERS INSTEAD. */ #define CHAN_INTRS_EN_MSK (USBH_LL_INTR_CHAN_XFERCOMPL | \ USBH_LL_INTR_CHAN_CHHLTD | \ USBH_LL_INTR_CHAN_BNAINTR) -#define CHAN_INTRS_ERROR_MSK (USBH_LL_INTR_CHAN_AHBERR | \ - USBH_LL_INTR_CHAN_STALL | \ +#define CHAN_INTRS_ERROR_MSK (USBH_LL_INTR_CHAN_STALL | \ USBH_LL_INTR_CHAN_BBLEER | \ USBH_LL_INTR_CHAN_BNAINTR | \ USBH_LL_INTR_CHAN_XCS_XACT_ERR) @@ -164,8 +171,11 @@ void usbh_hal_core_soft_reset(usbh_hal_context_t *hal) } //Set the default bits set_defaults(hal); - //Clear all the flags + //Clear all the flags and channels hal->flags.val = 0; + hal->channels.num_allocd = 0; + hal->channels.chan_pend_intrs_msk = 0; + memset(hal->channels.hdls, 0, sizeof(usbh_hal_chan_t *) * USBH_HAL_NUM_CHAN); } /* ----------------------------------------------------------------------------- @@ -181,11 +191,12 @@ static inline void debounce_lock_enable(usbh_hal_context_t *hal) void usbh_hal_port_enable(usbh_hal_context_t *hal) { + usb_speed_t speed = usbh_ll_hprt_get_speed(hal->dev); //Host Configuration - usbh_ll_hcfg_set_defaults(hal->dev); + usbh_ll_hcfg_set_defaults(hal->dev, speed); //Todo: Set frame list entries and ena per sched //Configure HFIR - usbh_ll_hfir_set_defaults(hal->dev); + usbh_ll_hfir_set_defaults(hal->dev, speed); //Config FIFO sizes usb_ll_set_rx_fifo_size(hal->dev, RX_FIFO_LEN); usb_ll_set_nptx_fifo_size(hal->dev, RX_FIFO_LEN, NPTX_FIFO_LEN); @@ -225,7 +236,7 @@ bool usbh_hal_chan_alloc(usbh_hal_context_t *hal, usbh_hal_chan_t *chan_obj, voi usbh_ll_chan_intr_read_and_clear(chan_obj->regs); //Clear the interrupt bits for that channel usbh_ll_haintmsk_en_chan_intr(hal->dev, 1 << chan_obj->flags.chan_idx); usbh_ll_chan_set_intr_mask(chan_obj->regs, CHAN_INTRS_EN_MSK); //Unmask interrupts for this channel - usbh_ll_chan_set_pid(chan_obj->regs, 0); //Set the initial PID to zero + usbh_ll_chan_set_pid(chan_obj->regs, 0); //Set the initial PID to zero usbh_ll_chan_hctsiz_init(chan_obj->regs); //Set the non changing parts of the HCTSIZ registers (e.g., do_ping and sched info) return true; } @@ -235,8 +246,8 @@ void usbh_hal_chan_free(usbh_hal_context_t *hal, usbh_hal_chan_t *chan_obj) { //Can only free a channel when in the disabled state and descriptor list released assert(!chan_obj->slot.flags.slot_acquired - && !chan_obj->flags.active - && !chan_obj->flags.error_pending); + && !chan_obj->flags.active + && !chan_obj->flags.error_pending); //Deallocate channel hal->channels.hdls[chan_obj->flags.chan_idx] = NULL; hal->channels.num_allocd--; @@ -255,7 +266,8 @@ void usbh_hal_chan_set_ep_char(usbh_hal_chan_t *chan_obj, usbh_hal_ep_char_t *ep ep_char->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK, ep_char->mps, ep_char->type, - ep_char->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK); + ep_char->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK, + ep_char->ls_via_fs_hub); } /* ----------------------------------------------------------------------------- @@ -278,11 +290,11 @@ void usbh_hal_chan_activate(usbh_hal_chan_t *chan_obj, int num_to_skip) usbh_ll_chan_start(chan_obj->regs); } -bool usbh_hal_chan_request_halt(usbh_hal_chan_t *chan_obj) +bool usbh_hal_chan_slot_request_halt(usbh_hal_chan_t *chan_obj) { //Cannot request halt on a channel that is pending error handling assert(!chan_obj->flags.error_pending); - if (usbh_ll_chan_is_active(chan_obj->regs)) { + if (usbh_ll_chan_is_active(chan_obj->regs) || chan_obj->flags.active) { usbh_ll_chan_halt(chan_obj->regs); chan_obj->flags.halt_requested = 1; return false; @@ -294,6 +306,16 @@ bool usbh_hal_chan_request_halt(usbh_hal_chan_t *chan_obj) -------------------------------- Event Handling -------------------------------- ----------------------------------------------------------------------------- */ +//When a device on the port is no longer valid (e.g., disconnect, port error). All channels are no longer valid +static void chan_all_halt(usbh_hal_context_t *hal) +{ + for (int i = 0; i < USBH_HAL_NUM_CHAN; i++) { + if (hal->channels.hdls[i] != NULL) { + hal->channels.hdls[i]->flags.active = 0; + } + } +} + usbh_hal_port_event_t usbh_hal_decode_intr(usbh_hal_context_t *hal) { uint32_t intrs_core = usb_ll_intr_read_and_clear(hal->dev); //Read and clear core interrupts @@ -304,7 +326,7 @@ usbh_hal_port_event_t usbh_hal_decode_intr(usbh_hal_context_t *hal) } //Note: Do not change order of checks. Regressing events (e.g. enable -> disabled, connected -> connected) //always take precendance. ENABLED < DISABLED < CONN < DISCONN < OVRCUR - usbh_hal_port_event_t event = -1; + usbh_hal_port_event_t event = USBH_HAL_PORT_EVENT_NONE; //Check if this is a core or port event if ((intrs_core & CORE_EVENTS_INTRS_MSK) || (intrs_port & PORT_EVENTS_INTRS_MSK)) { @@ -312,11 +334,13 @@ usbh_hal_port_event_t usbh_hal_decode_intr(usbh_hal_context_t *hal) if (intrs_core & USB_LL_INTR_CORE_DISCONNINT) { event = USBH_HAL_PORT_EVENT_DISCONN; debounce_lock_enable(hal); + chan_all_halt(hal); //All channels are halted on a disconnect //Mask the port connection and disconnection interrupts to prevent repeated triggering } else if (intrs_port & USBH_LL_INTR_HPRT_PRTOVRCURRCHNG) { //Check if this is an overcurrent or an overcurrent cleared if (usbh_ll_hprt_get_port_overcur(hal->dev)) { event = USBH_HAL_PORT_EVENT_OVRCUR; + chan_all_halt(hal); //All channels are halted on an overcurrent } else { event = USBH_HAL_PORT_EVENT_OVRCUR_CLR; } @@ -325,13 +349,15 @@ usbh_hal_port_event_t usbh_hal_decode_intr(usbh_hal_context_t *hal) event = USBH_HAL_PORT_EVENT_ENABLED; } else { //Host port has been disabled event = USBH_HAL_PORT_EVENT_DISABLED; + chan_all_halt(hal); //All channels are halted when the port is disabled } } else if (intrs_port & USBH_LL_INTR_HPRT_PRTCONNDET && !hal->flags.dbnc_lock_enabled) { event = USBH_HAL_PORT_EVENT_CONN; debounce_lock_enable(hal); } } - if (intrs_core & USB_LL_INTR_CORE_HCHINT) { + //Port events always take precendance over channel events + if (event == USBH_HAL_PORT_EVENT_NONE && (intrs_core & USB_LL_INTR_CORE_HCHINT)) { //One or more channels have pending interrupts. Store the mask of those channels hal->channels.chan_pend_intrs_msk = usbh_ll_get_chan_intrs_msk(hal->dev); event = USBH_HAL_PORT_EVENT_CHAN; @@ -355,17 +381,15 @@ usbh_hal_chan_event_t usbh_hal_chan_decode_intr(usbh_hal_chan_t *chan_obj) { uint32_t chan_intrs = usbh_ll_chan_intr_read_and_clear(chan_obj->regs); usbh_hal_chan_event_t chan_event; - //Currently, all cases where channel interrupts occur will also halt the channel - assert(chan_intrs & USBH_LL_INTR_CHAN_CHHLTD); + //Currently, all cases where channel interrupts occur will also halt the channel, except for BNA + assert(chan_intrs & (USBH_LL_INTR_CHAN_CHHLTD | USBH_LL_INTR_CHAN_BNAINTR)); chan_obj->flags.active = 0; //Note: Do not change the current checking order of checks. Certain interrupts (e.g., errors) have precedence over others if (chan_intrs & CHAN_INTRS_ERROR_MSK) { //One of the error interrupts has occurred. //Note: Errors are uncommon, so we check against the entire interrupt mask to reduce frequency of entering this call path //Store the error in hal context usbh_hal_chan_error_t error; - if (chan_intrs & USBH_LL_INTR_CHAN_AHBERR) { - error = USBH_HAL_CHAN_ERROR_AHB; - } else if (chan_intrs & USBH_LL_INTR_CHAN_STALL) { + if (chan_intrs & USBH_LL_INTR_CHAN_STALL) { error = USBH_HAL_CHAN_ERROR_STALL; } else if (chan_intrs & USBH_LL_INTR_CHAN_BBLEER) { error = USBH_HAL_CHAN_ERROR_PKT_BBL; @@ -393,8 +417,8 @@ usbh_hal_chan_event_t usbh_hal_chan_decode_intr(usbh_hal_chan_t *chan_obj) chan_event = USBH_HAL_CHAN_EVENT_SLOT_HALT; } } else { - //Channel halted suddenly (i.e,, a disconnect) - chan_event = USBH_HAL_CHAN_EVENT_SUDDEN_HLT; + //Should never reach this point + abort(); } return chan_event; } diff --git a/components/hal/include/hal/usb_types.h b/components/hal/include/hal/usb_types.h index ef93fed6b2..00f8b8c2c0 100644 --- a/components/hal/include/hal/usb_types.h +++ b/components/hal/include/hal/usb_types.h @@ -25,9 +25,11 @@ extern "C" #define USB_DESC_ATTR __attribute__((packed)) /* ----------------------------------------------------------------------------- ------------------------------- USB Protocol Enums ------------------------------ +------------------------------- Common USB Types ------------------------------- ----------------------------------------------------------------------------- */ +// ----------------------------- Device Related -------------------------------- + /** * @brief Enumeration of USB PHY type */ @@ -36,6 +38,18 @@ typedef enum { USB_PHY_EXTERNAL, /**< Use an external USB PHY */ } usb_phy_t; +// ------------------------------ Bus Related ---------------------------------- + +/** + * @brief USB Standard Speeds + */ +typedef enum { + USB_SPEED_LOW = 0, /**< USB Low Speed (1.5 Mbit/s) */ + USB_SPEED_FULL, /**< USB Full Speed (12 Mbit/s) */ +} usb_speed_t; + +// ---------------------------- Transfer Related ------------------------------- + /** * @brief The type of USB transfer * @@ -49,17 +63,65 @@ typedef enum { } usb_xfer_type_t; /** - * @brief USB Standard Speeds + * @brief The status of a particular transfer */ typedef enum { - USB_SPEED_LOW = 0, /**< USB Low Speed (1.5 Mbit/s) */ - USB_SPEED_FULL, /**< USB Full Speed (12 Mbit/s) */ -} usb_speed_t; + USB_TRANSFER_STATUS_COMPLETED, /**< The transfer was successful (but may be short) */ + USB_TRANSFER_STATUS_ERROR, /**< The transfer failed because due to excessive errors (e.g. no response or CRC error) */ + USB_TRANSFER_STATUS_TIMED_OUT, /**< The transfer failed due to to a time out */ + USB_TRANSFER_STATUS_CANCELLED, /**< The transfer was cancelled */ + USB_TRANSFER_STATUS_STALL, /**< The transfer was stalled */ + USB_TRANSFER_STATUS_NO_DEVICE, /**< The transfer failed because the device is no longer valid (e.g., disconencted */ + USB_TRANSFER_STATUS_OVERFLOW, /**< The transfer as more data was sent than was requested */ +} usb_transfer_status_t; + +/** + * @brief Isochronous packet descriptor + * + * If the number of bytes in an IRP transfer is larger than the MPS of the + * endpoint, the IRP is split over multiple packets (one packet per bInterval + * of the endpoint). An array of Isochronous packet descriptos describes how + * an IRP should be split over multiple packets. + */ +typedef struct { + int length; /**< Number of bytes to transmit/receive in the packet */ + int actual_length; /**< Actual number of bytes transmitted/received in the packet */ + usb_transfer_status_t status; /**< Status of the packet */ +} usb_iso_packet_desc_t; + +/** + * @brief USB IRP (I/O Request Packet). See USB2.0 Spec + * + * An identifiable request by a software client to move data between itself (on the + * host) and an endpoint of a device in an appropriate direction. + * + * This structure represents the barebones of the request. Different layers of + * USB drivers will wrap their own objects around this. + * + * See 10.5.3.1 os USB2.0 specification + * Bulk: Represnts a single bulk transfer which a pipe will transparently split + * into multiple MPS transactions (until the last) + * Control: Represents a single contorl transfer with the setup packet at the + * first 8 bytes of the buffer. + * Interrupt: Represnts a single interrupt transaction + * Isochronous: Represnts a buffer of a stream of bytes which the pipe will transparently + * transfer the stream of bytes one or more service periods + */ +typedef struct { + int num_bytes; /**< Number of bytes in IRP. Control should exclude size of setup. IN should be integer multiple of MPS */ + int actual_num_bytes; /**< Actual number of bytes transmitted/receives in the IRP */ + uint8_t *data_buffer; /**< Pointer to data buffer. Must be DMA capable memory */ + usb_transfer_status_t status; /**< Status of the transfer */ + int num_iso_packets; /**< Only relevant to isochronous. Number of service periods to transfer data buffer over. Set to 0 for non-iso transfers */ + usb_iso_packet_desc_t iso_packet_desc[0]; /**< Descriptors for each ISO packet */ +} usb_irp_t; /* ----------------------------------------------------------------------------- --------------------------------- Control Request ------------------------------- +----------------------------------- Chapter 9 ---------------------------------- ----------------------------------------------------------------------------- */ +// ------------------------------ Control Request ------------------------------ + /** * @brief Size of a USB control transfer setup packet in bytes */ @@ -183,11 +245,7 @@ _Static_assert(sizeof(usb_ctrl_req_t) == USB_CTRL_REQ_SIZE, "Size of usb_ctrl_re (ctrl_req_ptr)->wLength = 0; \ }) -/* ----------------------------------------------------------------------------- ----------------------------------- Descriptors --------------------------------- ------------------------------------------------------------------------------ */ - -// -------------------------- Device Descriptor -------------------------------- +// ---------------------------- Device Descriptor ------------------------------ /** * @brief Size of a USB device descriptor in bytes diff --git a/components/usb/CMakeLists.txt b/components/usb/CMakeLists.txt new file mode 100644 index 0000000000..cfaf84d4e1 --- /dev/null +++ b/components/usb/CMakeLists.txt @@ -0,0 +1,12 @@ +idf_build_get_property(target IDF_TARGET) + +#USB Host is currently only supported on ESP32-S2 +if(NOT "${target}" STREQUAL "esp32s2") + return() +endif() + +idf_component_register(SRCS "hcd.c" + INCLUDE_DIRS "" + PRIV_INCLUDE_DIRS "private_include" + PRIV_REQUIRES "hal" + REQUIRES "") diff --git a/components/usb/hcd.c b/components/usb/hcd.c new file mode 100644 index 0000000000..99b9bc8f57 --- /dev/null +++ b/components/usb/hcd.c @@ -0,0 +1,1907 @@ +// Copyright 2015-2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "sys/queue.h" +#include "esp_heap_caps.h" +#include "esp_intr_alloc.h" +#include "esp_timer.h" +#include "esp_err.h" +#include "esp_rom_gpio.h" +#include "hal/usbh_hal.h" +#include "soc/gpio_pins.h" +#include "soc/gpio_sig_map.h" +#include "driver/periph_ctrl.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "hcd.h" + +// ----------------------------------------------------- Macros -------------------------------------------------------- + +// --------------------- Constants ------------------------- + +/** + * @brief Number of transfer descriptors per transfer for various transfer types + * + * Control: Requires 3 transfer descriptors for a single transfer + * corresponding to each stage of a control transfer + * Bulk: Requires 1 transfer descriptor for each transfer + */ +#define NUM_DESC_PER_XFER_CTRL 3 +#define NUM_DESC_PER_XFER_BULK 1 +#define XFER_LIST_LEN_CTRL 1 +#define XFER_LIST_LEN_BULK 1 + +#define INIT_DELAY_MS 30 //A delay of at least 25ms to enter Host mode. Make it 30ms to be safe +#define DEBOUNCE_DELAY_MS 250 //A debounce delay of 250ms +#define RESET_HOLD_MS 30 //Spec requires at least 10ms. Make it 30ms to be safe +#define RESET_RECOVERY_MS 30 //Reset recovery delay of 10ms (make it 30 ms to be safe) to allow for connected device to recover (and for port enabled interrupt to occur) +#define RESUME_HOLD_MS 30 //Spec requires at least 20ms, Make it 30ms to be safe +#define RESUME_RECOVERY_MS 20 //Resume recovery of at least 10ms. Make it 20 ms to be safe. This will include the 3 LS bit times of the EOP + +#define CTRL_EP_MAX_MPS_LS 8 //Largest Maximum Packet Size for Low Speed control endpoints +#define CTRL_EP_MAX_MPS_FS 64 //Largest Maximum Packet Size for Full Speed control endpoints + +#define NUM_PORTS 1 //The controller only has one port. + +typedef enum { + XFER_REQ_STATE_IDLE, //The transfer request is not enqueued + XFER_REQ_STATE_PENDING, //The transfer request is enqueued and pending execution + XFER_REQ_STATE_INFLIGHT, //The transfer request is currently being executed + XFER_REQ_STATE_DONE, //The transfer request has completed executed or is retired, and is waiting to be dequeued +} xfer_req_state_t; + +// -------------------- Convenience ------------------------ + +#define HCD_ENTER_CRITICAL_ISR() portENTER_CRITICAL_ISR(&hcd_lock) +#define HCD_EXIT_CRITICAL_ISR() portEXIT_CRITICAL_ISR(&hcd_lock) +#define HCD_ENTER_CRITICAL() portENTER_CRITICAL(&hcd_lock) +#define HCD_EXIT_CRITICAL() portEXIT_CRITICAL(&hcd_lock) + +#define HCD_CHECK(cond, ret_val) ({ \ + if (!(cond)) { \ + return (ret_val); \ + } \ +}) +#define HCD_CHECK_FROM_CRIT(cond, ret_val) ({ \ + if (!(cond)) { \ + HCD_EXIT_CRITICAL(); \ + return ret_val; \ + } \ +}) + +// ------------------------------------------------------ Types -------------------------------------------------------- + +typedef struct xfer_req_obj xfer_req_t; +typedef struct pipe_obj pipe_t; +typedef struct port_obj port_t; + +/** + * @brief Object representing an HCD transfer request + */ +struct xfer_req_obj { + TAILQ_ENTRY(xfer_req_obj) tailq_entry; //TailQ entry for pending or done tailq in pipe object + pipe_t *pipe; //Target pipe of transfer request + usb_irp_t *irp; //Target IRP + void *context; //Context variable of transfer request + xfer_req_state_t state; //Current state of the transfer request +}; + +/** + * @brief Object representing a pipe in the HCD layer + */ +struct pipe_obj { + //Transfer requests related + TAILQ_HEAD(tailhead_xfer_req_pend, xfer_req_obj) pend_xfer_req_tailq; + TAILQ_HEAD(tailhead_xfer_req_done, xfer_req_obj) done_xfer_req_tailq; + int num_xfer_req_pending; + int num_xfer_req_done; + xfer_req_t *inflight_xfer_req; //Pointer to the current transfer request being executed by the pipe. NULL if none. + //Port related + port_t *port; //The port to which this pipe is routed through + TAILQ_ENTRY(pipe_obj) tailq_entry; //TailQ entry for port's list of pipes + //HAl channel related + void *xfer_desc_list; + usbh_hal_chan_t *chan_obj; + usbh_hal_ep_char_t ep_char; + //Pipe status, state, and events + hcd_pipe_state_t state; + hcd_pipe_event_t last_event; + TaskHandle_t task_waiting_pipe_notif; //Task handle used for internal pipe events + union { + struct { + uint32_t waiting_xfer_done: 1; + uint32_t paused: 1; + uint32_t pipe_cmd_processing: 1; + //Flags only used by control transfers + uint32_t ctrl_data_stg_in: 1; + uint32_t ctrl_data_stg_skip: 1; + uint32_t reserved3: 3; + uint32_t xfer_desc_list_len: 8; + uint32_t reserved16: 16; + }; + uint32_t val; + } flags; + //Pipe callback and context + hcd_pipe_isr_callback_t callback; + void *callback_arg; + void *context; +}; + +/** + * @brief Object representing a port in the HCD layer + */ +struct port_obj { + usbh_hal_context_t *hal; + //Pipes routed through this port + TAILQ_HEAD(tailhead_pipes_idle, pipe_obj) pipes_idle_tailq; + TAILQ_HEAD(tailhead_pipes_queued, pipe_obj) pipes_queued_tailq; + int num_pipes_idle; + int num_pipes_queued; + //Port status, state, and events + hcd_port_state_t state; + usb_speed_t speed; + hcd_port_event_t last_event; + TaskHandle_t task_waiting_port_notif; //Task handle used for internal port events + union { + struct { + uint32_t event_pending: 1; //The port has an event that needs to be handled + uint32_t event_processing: 1; //The port is current processing (handling) an event + uint32_t cmd_processing: 1; //Used to indicate command handling is ongoing + uint32_t waiting_all_pipes_pause: 1; //Waiting for all pipes routed through this port to be paused + uint32_t disable_requested: 1; + uint32_t conn_devc_ena: 1; //Used to indicate the port is connected to a device that has been reset + uint32_t reserved10: 10; + uint32_t num_pipes_waiting_pause: 16; + }; + uint32_t val; + } flags; + bool initialized; + //Port callback and context + hcd_port_isr_callback_t callback; + void *callback_arg; + SemaphoreHandle_t port_mux; + void *context; +}; + +/** + * @brief Object representing the HCD + */ +typedef struct { + //Ports (Hardware only has one) + port_t *port_obj; + intr_handle_t isr_hdl; +} hcd_obj_t; + +static portMUX_TYPE hcd_lock = portMUX_INITIALIZER_UNLOCKED; +static hcd_obj_t *s_hcd_obj = NULL; //Note: "s_" is for the static pointer + +// ------------------------------------------------- Forward Declare --------------------------------------------------- + +// ----------------------- Events -------------------------- + +/** + * @brief Wait for an internal event from a port + * + * @note For each port, there can only be one thread/task waiting for an internal port event + * @note This function is blocking (will exit and re-enter the critical section to do so) + * + * @param port Port object + */ +static void _internal_port_event_wait(port_t *port); + +/** + * @brief Notify (from an ISR context) the thread/task waiting for the internal port event + * + * @param port Port object + * @return true A yield is required + * @return false Whether a yield is required or not + */ +static bool _internal_port_event_notify_from_isr(port_t *port); + +/** + * @brief Wait for an internal event from a particular pipe + * + * @note For each pipe, there can only be one thread/task waiting for an internal port event + * @note This function is blocking (will exit and re-enter the critical section to do so) + * + * @param pipe Pipe object + */ +static void _internal_pipe_event_wait(pipe_t *pipe); + +/** + * @brief Notify (from an ISR context) the thread/task waiting for an internal pipe event + * + * @param pipe Pipe object + * @param from_isr Whether this is called from an ISR or not + * @return true A yield is required + * @return false Whether a yield is required or not. Always false when from_isr is also false + */ +static bool _internal_pipe_event_notify(pipe_t *pipe, bool from_isr); + +// ------------------------ Port --------------------------- + +/** + * @brief Invalidates all the pipes routed through a port + * + * This should be called when port or its connected device is no longer valid (e.g., the port is suddenly reset/disabled + * or the device suddenly disconnects) + * + * @note This function may run one or more callbacks, and will exit and enter the critical section to do so + * + * Entry: + * - The port or its connected device is no longer valid. This guarantees that none of the pipes will be transferring + * Exit: + * - Each pipe will have any pending transfer request moved to their respective done tailq + * - Each pipe will be put into the invalid state + * - Generate a HCD_PIPE_EVENT_INVALID event on each pipe and run their respective callbacks + * + * @param port Port object + */ +static void _port_invalidate_all_pipes(port_t *port); + +/** + * @brief Pause all pipes routed through a port + * + * Call this before attempting to reset or suspend a port + * + * Entry: + * - The port is in the HCD_PORT_STATE_ENABLED state (i.e., there is a connected device which has been reset) + * Exit: + * - All pipes of the port have either paused, or are waiting to complete their inflight transfer request to pause + * - If waiting for one or more pipes, _internal_port_event_wait() must be called after this function returns + * + * @param port Port object + * @return true All pipes have been paused + * @return false Need to wait for one or more pipes to pause. Call _internal_port_event_wait() afterwards + */ +static bool _port_pause_all_pipes(port_t *port); + +/** + * @brief Un-pause all pipes routed through a port + * + * Call this before after coming out of a port reset or resume. + * + * Entry: + * - The port is in the HCD_PORT_STATE_ENABLED state + * - All pipes are paused + * Exit: + * - All pipes un-paused. If those pipes have pending transfer requests, they will be started. + * + * @param port Port object + */ +static void _port_unpause_all_pipes(port_t *port); + +/** + * @brief Send a reset condition on a port's bus + * + * Entry: + * - The port must be in the HCD_PORT_STATE_ENABLED or HCD_PORT_STATE_DISABLED state + * Exit: + * - Reset condition sent on the port's bus + * + * @note This function is blocking (will exit and re-enter the critical section to do so) + * + * @param port Port object + * @return true Reset condition successfully sent + * @return false Failed to send reset condition due to unexpected port state + */ +static bool _port_bus_reset(port_t *port); + +/** + * @brief Send a suspend condition on a port's bus + * + * This function will first pause pipes routed through a port, and then send a suspend condition. + * + * Entry: + * - The port must be in the HCD_PORT_STATE_ENABLED state + * Exit: + * - All pipes paused and the port is put into the suspended state + * + * @note This function is blocking (will exit and re-enter the critical section to do so) + * + * @param port Port object + * @return true Suspend condition successfully sent. Port is now in the HCD_PORT_STATE_SUSPENDED state + * @return false Failed to send a suspend condition due to unexpected port state + */ +static bool _port_bus_suspend(port_t *port); + +/** + * @brief Send a resume condition on a port's bus + * + * This function will send a resume condition, and then un-pause all the pipes routed through a port + * + * Entry: + * - The port must be in the HCD_PORT_STATE_SUSPENDED state + * Exit: + * - The port is put into the enabled state and all pipes un-paused + * + * @note This function is blocking (will exit and re-enter the critical section to do so) + * + * @param port Port object + * @return true Resume condition successfully sent. Port is now in the HCD_PORT_STATE_ENABLED state + * @return false Failed to send a resume condition due to unexpected port state. + */ +static bool _port_bus_resume(port_t *port); + +/** + * @brief Disable a port + * + * Entry: + * - The port must be in the HCD_PORT_STATE_ENABLED or HCD_PORT_STATE_SUSPENDED state + * Exit: + * - All pipes paused (should already be paused if port was suspended), and the port is put into the disabled state. + * + * @note This function is blocking (will exit and re-enter the critical section to do so) + * + * @param port Port object + * @return true Port successfully disabled + * @return false Port to disable port due to unexpected port state + */ +static bool _port_disable(port_t *port); + +/** + * @brief Debounce port after a connection or disconnection event + * + * This function should be called after a port connection or disconnect event. This function will execute a debounce + * delay then check the actual connection/disconnections state. + * + * @param port Port object + * @return true A device is connected + * @return false No device connected + */ +static bool _port_debounce(port_t *port); + +// ------------------------ Pipe --------------------------- + +/** + * @brief Get the next pending transfer request from the pending tailq + * + * Entry: + * - The inflight transfer request must be set to NULL (indicating the pipe currently has no inflight transfer request) + * Exit: + * - If (num_xfer_req_pending > 0), the first transfer request is removed from pend_xfer_req_tailq and and + * inflight_xfer_req is set to that transfer request. + * - If there are no more queued transfer requests, inflight_xfer_req is left as NULL + * + * @param pipe Pipe object + * @return true A pending transfer request is now set as the inflight transfer request + * @return false No more pending transfer requests + */ +static bool _pipe_get_next_xfer_req(pipe_t *pipe); + +/** + * @brief Return the inflight transfer request to the done tailq + * + * Entry: + * - The inflight transfer request must already have been parsed (i.e., results have been checked) + * Exit: + * - The inflight transfer request is returned to the done tailq and inflight_xfer_req is set to NULL + * + * @param pipe Pipe object + */ +static void _pipe_ret_cur_xfer_req(pipe_t *pipe); + +/** + * @brief Wait until a pipe's inflight transfer request is done + * + * If the pipe has an inflight transfer request, this function will block until it is done (via a internal pipe event). + * If the pipe has no inflight transfer request, this function do nothing and return immediately. + * If the pipe's state changes unexpectedely, this function will return false. + * + * @note This function is blocking (will exit and re-enter the critical section to do so) + * + * @param pipe Pipe object + * @return true Pipes inflight transfer request is done + * @return false Pipes state unexpectedly changed + */ +static bool _pipe_wait_done(pipe_t *pipe); + +/** + * @brief Retires all transfer requests (those that were previously inflight or pending) + * + * Retiring all transfer requests will result in any pending transfer request being moved to the done tailq. This + * function will update the IPR status of each transfer request. + * - If the retiring is self-initiated (i.e., due to a pipe command), the IRP status will be set to USB_TRANSFER_STATUS_CANCELLED. + * - If the retiring is NOT self-initiated (i.e., the pipe is no longer valid), the IRP status will be set to USB_TRANSFER_STATUS_NO_DEVICE + * + * Entry: + * - There can be no inflight transfer request (must already be parsed and returned to done queue) + * Exit: + * - If there was an inflight transfer request, it is parsed and returned to the done queue + * - If there are any pending transfer requests: + * - They are moved to the done tailq + * + * @param pipe Pipe object + * @param cancelled Are we actively Pipe retire is initialized by the user due to a command, thus transfer request are actively + * cancelled + */ +static void _pipe_retire(pipe_t *pipe, bool self_initiated); + +/** + * @brief Decode a HAL channel error to the corresponding pipe event + * + * @param chan_error The HAL channel error + * @return hcd_pipe_event_t The corresponding pipe error event + */ +static inline hcd_pipe_event_t pipe_decode_error_event(usbh_hal_chan_error_t chan_error); + +// ------------------ Transfer Requests -------------------- + +/** + * @brief Fill a transfer request into the pipe's transfer descriptor list + * + * Entry: + * - The pipe's inflight_xfer_req must be set to the next transfer request + * Exit: + * - inflight_xfer_req filled into the pipe's transfer descriptor list + * - Starting PIDs and directions set + * - Channel slot acquired. Will need to call usbh_hal_chan_activate() to actually start execution + * + * @param pipe Pipe where inflight_xfer_req is already set to the next transfer request + */ +static void _xfer_req_fill(pipe_t *pipe); + +/** + * @brief Continue a transfer request + * + * @note This is currently only used for control transfers + * + * @param pipe Pipe where inflight_xfer_req contains the transfer request to continue + */ +static void _xfer_req_continue(pipe_t *pipe); + +/** + * @brief Parse the results of a pipe's transfer descriptor list into a transfer request + * + * Entry: + * - The pipe must have stop transferring either due a channel event or a port disconnection. + * - The pipe's state and last_event must be updated before parsing the transfer request as + * they will used to determine the resuult of the transfer request + * Exit: + * - The pipe's inflight_xfer_req is filled with result of the transfer request (i.e., the underlying IRP has its status set) + * + * @param pipe Pipe where inflight_xfer_req contains the completed transfer request + * @param error_occurred Are we parsing after the pipe had an error (or has become invalid) + */ +static void _xfer_req_parse(pipe_t *pipe, bool error_occurred); + +// ----------------------------------------------- Interrupt Handling -------------------------------------------------- + +// ------------------- Internal Event ---------------------- + +static void _internal_port_event_wait(port_t *port) +{ + //There must NOT be another thread/task already waiting for an internal event + assert(port->task_waiting_port_notif == NULL); + port->task_waiting_port_notif = xTaskGetCurrentTaskHandle(); + HCD_EXIT_CRITICAL(); + //Wait to be notified from ISR + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + HCD_ENTER_CRITICAL(); + port->task_waiting_port_notif = NULL; +} + +static bool _internal_port_event_notify_from_isr(port_t *port) +{ + //There must be a thread/task waiting for an internal event + assert(port->task_waiting_port_notif != NULL); + BaseType_t xTaskWoken = pdFALSE; + //Unblock the thread/task waiting for the notification + HCD_EXIT_CRITICAL_ISR(); + vTaskNotifyGiveFromISR(port->task_waiting_port_notif, &xTaskWoken); + HCD_ENTER_CRITICAL_ISR(); + return (xTaskWoken == pdTRUE); +} + +static void _internal_pipe_event_wait(pipe_t *pipe) +{ + //There must NOT be another thread/task already waiting for an internal event + assert(pipe->task_waiting_pipe_notif == NULL); + pipe->task_waiting_pipe_notif = xTaskGetCurrentTaskHandle(); + HCD_EXIT_CRITICAL(); + //Wait to be notified from ISR + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + HCD_ENTER_CRITICAL(); + pipe->task_waiting_pipe_notif = NULL; +} + +static bool _internal_pipe_event_notify(pipe_t *pipe, bool from_isr) +{ + //There must be a thread/task waiting for an internal event + assert(pipe->task_waiting_pipe_notif != NULL); + bool ret; + if (from_isr) { + BaseType_t xTaskWoken = pdFALSE; + HCD_EXIT_CRITICAL_ISR(); + //Unblock the thread/task waiting for the pipe notification + vTaskNotifyGiveFromISR(pipe->task_waiting_pipe_notif, &xTaskWoken); + HCD_ENTER_CRITICAL_ISR(); + ret = (xTaskWoken == pdTRUE); + } else { + HCD_EXIT_CRITICAL(); + xTaskNotifyGive(pipe->task_waiting_pipe_notif); + HCD_ENTER_CRITICAL(); + ret = false; + } + return ret; +} + +// ----------------- Interrupt Handlers -------------------- + +/** + * @brief Handle a HAL port interrupt and obtain the corresponding port event + * + * @param[in] port Port object + * @param[in] hal_port_event The HAL port event + * @param[out] yield Set to true if a yield is required as a result of handling the interrupt + * @return hcd_port_event_t Returns a port event, or HCD_PORT_EVENT_NONE if no port event occurred + */ +static hcd_port_event_t _intr_hdlr_hprt(port_t *port, usbh_hal_port_event_t hal_port_event, bool *yield) +{ + hcd_port_event_t port_event = HCD_PORT_EVENT_NONE; + switch (hal_port_event) { + case USBH_HAL_PORT_EVENT_CONN: { + //Don't update state immediately, we still need to debounce. + port_event = HCD_PORT_EVENT_CONNECTION; + break; + } + case USBH_HAL_PORT_EVENT_DISCONN: { + if (port->flags.conn_devc_ena) { + //The port was previously enabled, so this is a sudden disconenction + port->state = HCD_PORT_STATE_RECOVERY; + port_event = HCD_PORT_EVENT_SUDDEN_DISCONN; + } else { + //For normal disconnections, don't update state immediately as we still need to debounce. + port_event = HCD_PORT_EVENT_DISCONNECTION; + } + port->flags.conn_devc_ena = 0; + break; + } + case USBH_HAL_PORT_EVENT_ENABLED: { + usbh_hal_port_enable(port->hal); //Initialize remaining host port registers + port->speed = usbh_hal_port_get_conn_speed(port->hal); + port->state = HCD_PORT_STATE_ENABLED; + port->flags.conn_devc_ena = 1; + //This was triggered by a command, so no event needs to be propagated. + break; + } + case USBH_HAL_PORT_EVENT_DISABLED: { + port->flags.conn_devc_ena = 0; + //Disabled could be due to a disable request or reset request, or due to a port error + if (port->state != HCD_PORT_STATE_RESETTING) { //Ignore the disable event if it's due to a reset request + port->state = HCD_PORT_STATE_DISABLED; + if (port->flags.disable_requested) { + //Disabled by request (i.e. by port command). Generate an internal event + port->flags.disable_requested = 0; + *yield |= _internal_port_event_notify_from_isr(port); + } else { + //Disabled due to a port error + port_event = HCD_PORT_EVENT_ERROR; + } + } + break; + } + case USBH_HAL_PORT_EVENT_OVRCUR: + case USBH_HAL_PORT_EVENT_OVRCUR_CLR: { //Could occur if a quick overcurrent then clear happens + if (port->state != HCD_PORT_STATE_NOT_POWERED) { + //We need to power OFF the port to protect it + usbh_hal_port_toggle_power(port->hal, false); + port->state = HCD_PORT_STATE_NOT_POWERED; + port_event = HCD_PORT_EVENT_OVERCURRENT; + } + port->flags.conn_devc_ena = 0; + break; + } + default: { + abort(); + break; + } + } + return port_event; +} + +/** + * @brief Handles a HAL channel interrupt + * + * This function should be called on a HAL channel when it has an interrupt. Most HAL channel events will correspond to + * to a pipe event, but not always. This function will store the pipe event and return a pipe object pointer if a pipe + * event occurred, or return NULL otherwise. + * + * @param[in] chan_obj Pointer to HAL channel object with interrupt + * @param[out] yield Set to true if a yield is required as a result of handling the interrupt + * @return hcd_pipe_event_t The pipe event + */ +static hcd_pipe_event_t _intr_hdlr_chan(pipe_t *pipe, usbh_hal_chan_t *chan_obj, bool *yield) +{ + usbh_hal_chan_event_t chan_event = usbh_hal_chan_decode_intr(chan_obj); + hcd_pipe_event_t event = HCD_PIPE_EVENT_NONE; + //Check the the pipe's port still has a connected and enabled device before processing the interrupt + if (!pipe->port->flags.conn_devc_ena) { + return event; //Treat as a no event. + } + + switch (chan_event) { + case USBH_HAL_CHAN_EVENT_SLOT_DONE: { + //An entire transfer descriptor list has completed execution + pipe->last_event = HCD_PIPE_EVENT_XFER_REQ_DONE; + event = HCD_PIPE_EVENT_XFER_REQ_DONE; + _xfer_req_parse(pipe, false); //Parse results of transfer request + _pipe_ret_cur_xfer_req(pipe); //Return the transfer request to the pipe's done tailq + if (pipe->flags.waiting_xfer_done) { + //A port/pipe command is waiting for this pipe to complete its transfer. So don't load the next transfer + pipe->flags.waiting_xfer_done = 0; + if (pipe->port->flags.waiting_all_pipes_pause) { + //Port command is waiting for all pipes to be paused + pipe->flags.paused = 1; + pipe->port->flags.num_pipes_waiting_pause--; + if (pipe->port->flags.num_pipes_waiting_pause == 0) { + //All pipes have finished pausing, Notify the blocked port command + pipe->port->flags.waiting_all_pipes_pause = 0; + *yield |= _internal_port_event_notify_from_isr(pipe->port); + } + } else { + //Pipe command is waiting for transfer to complete + *yield |= _internal_pipe_event_notify(pipe, true); + } + } else if (_pipe_get_next_xfer_req(pipe)) { + //Fill the descriptor list with the transfer request and start the transfer + _xfer_req_fill(pipe); + usbh_hal_chan_activate(chan_obj, 0); //Start with the first descriptor + } + break; + } + case USBH_HAL_CHAN_EVENT_SLOT_HALT: { + //A transfer descriptor list has partially completed. This currently only happens on control pipes + assert(pipe->ep_char.type == USB_XFER_TYPE_CTRL); + _xfer_req_continue(pipe); //Continue the transfer request. + //We are continuing a transfer, so no event has occurred + break; + } + case USBH_HAL_CHAN_EVENT_ERROR: { + //Get and store the pipe error event + usbh_hal_chan_error_t chan_error = usbh_hal_chan_get_error(chan_obj); + usbh_hal_chan_clear_error(chan_obj); + pipe->last_event = pipe_decode_error_event(chan_error); + event = pipe->last_event; + pipe->state = HCD_PIPE_STATE_HALTED; + //Parse the failed transfer request and update it's IRP status + _xfer_req_parse(pipe, true); + _pipe_ret_cur_xfer_req(pipe); //Return the transfer request to the pipe's done tailq + break; + } + case USBH_HAL_CHAN_EVENT_HALT_REQ: //We currently don't halt request so this event should never occur + default: + abort(); + break; + } + + return event; +} + +/** + * @brief Main interrupt handler + * + * - Handle all HPRT (Host Port) related interrupts first as they may change the + * state of the driver (e.g., a disconnect event) + * - If any channels (pipes) have pending interrupts, handle them one by one + * - The HCD has not blocking functions, so the user's ISR callback is run to + * allow the users to send whatever OS primitives they need. + * @param arg + */ +static void intr_hdlr_main(void *arg) +{ + port_t *port = (port_t *)arg; + bool yield = false; + + HCD_ENTER_CRITICAL_ISR(); + usbh_hal_port_event_t hal_port_evt = usbh_hal_decode_intr(port->hal); + if (hal_port_evt == USBH_HAL_PORT_EVENT_CHAN) { + //Channel event. Cycle through each pending channel + usbh_hal_chan_t *chan_obj = usbh_hal_get_chan_pending_intr(port->hal); + while (chan_obj != NULL) { + pipe_t *pipe = (pipe_t *)usbh_hal_chan_get_context(chan_obj); + hcd_pipe_event_t event = _intr_hdlr_chan(pipe, chan_obj, &yield); + //Run callback if a pipe event has occurred and the pipe also has a callback + if (event != HCD_PIPE_EVENT_NONE && pipe->callback != NULL) { + HCD_EXIT_CRITICAL_ISR(); + yield |= pipe->callback((hcd_pipe_handle_t)pipe, event, pipe->callback_arg, true); + HCD_ENTER_CRITICAL_ISR(); + } + //Check for more channels with pending interrupts. Returns NULL if there are no more + chan_obj = usbh_hal_get_chan_pending_intr(port->hal); + } + } else if (hal_port_evt != USBH_HAL_PORT_EVENT_NONE) { //Port event + hcd_port_event_t port_event = _intr_hdlr_hprt(port, hal_port_evt, &yield); + if (port_event != HCD_PORT_EVENT_NONE) { + port->last_event = port_event; + port->flags.event_pending = 1; + if (port->callback != NULL) { + HCD_EXIT_CRITICAL_ISR(); + yield |= port->callback((hcd_port_handle_t)port, port_event, port->callback_arg, true); + HCD_ENTER_CRITICAL_ISR(); + } + } + } + HCD_EXIT_CRITICAL_ISR(); + + if (yield) { + portYIELD_FROM_ISR(); + } +} + +// --------------------------------------------- Host Controller Driver ------------------------------------------------ + +static port_t *port_obj_alloc(void) +{ + port_t *port = calloc(1, sizeof(port_t)); + usbh_hal_context_t *hal = malloc(sizeof(usbh_hal_context_t)); + SemaphoreHandle_t port_mux = xSemaphoreCreateMutex(); + if (port == NULL || hal == NULL || port_mux == NULL) { + free(port); + free(hal); + if (port_mux != NULL) { + vSemaphoreDelete(port_mux); + } + return NULL; + } + port->hal = hal; + port->port_mux = port_mux; + return port; +} + +static void port_obj_free(port_t *port) +{ + if (port == NULL) { + return; + } + vSemaphoreDelete(port->port_mux); + free(port->hal); + free(port); +} + +// ----------------------- Public -------------------------- + +esp_err_t hcd_install(const hcd_config_t *config) +{ + HCD_ENTER_CRITICAL(); + HCD_CHECK_FROM_CRIT(s_hcd_obj == NULL, ESP_ERR_INVALID_STATE); + HCD_EXIT_CRITICAL(); + + esp_err_t err_ret; + //Allocate memory and resources for driver object and all port objects + hcd_obj_t *p_hcd_obj_dmy = calloc(1, sizeof(hcd_obj_t)); + if (p_hcd_obj_dmy == NULL) { + return ESP_ERR_NO_MEM; + } + + //Allocate resources for each port (there's only one) + p_hcd_obj_dmy->port_obj = port_obj_alloc(); + esp_err_t intr_alloc_ret = esp_intr_alloc(ETS_USB_INTR_SOURCE, + config->intr_flags | ESP_INTR_FLAG_INTRDISABLED, //The interruupt must be disabled until the port is initialized + intr_hdlr_main, + (void *)p_hcd_obj_dmy->port_obj, + &p_hcd_obj_dmy->isr_hdl); + if (p_hcd_obj_dmy->port_obj == NULL) { + err_ret = ESP_ERR_NO_MEM; + } + if (intr_alloc_ret != ESP_OK) { + err_ret = intr_alloc_ret; + goto err; + } + + HCD_ENTER_CRITICAL(); + if (s_hcd_obj != NULL) { + HCD_EXIT_CRITICAL(); + err_ret = ESP_ERR_INVALID_STATE; + goto err; + } + s_hcd_obj = p_hcd_obj_dmy; + //Set HW prereqs for each port (there's only one) + periph_module_enable(PERIPH_USB_MODULE); + periph_module_reset(PERIPH_USB_MODULE); + /* + Configure GPIOS for Host mode operation using internal PHY + - Forces ID to GND for A side + - Forces B Valid to GND as we are A side host + - Forces VBUS Valid to HIGH + - Froces A Valid to HIGH + */ + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, USB_OTG_IDDIG_IN_IDX, false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, USB_SRP_BVALID_IN_IDX, false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, USB_OTG_VBUSVALID_IN_IDX, false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, USB_OTG_AVALID_IN_IDX, false); + HCD_EXIT_CRITICAL(); + return ESP_OK; +err: + if (intr_alloc_ret == ESP_OK) { + esp_intr_free(p_hcd_obj_dmy->isr_hdl); + } + port_obj_free(p_hcd_obj_dmy->port_obj); + free(p_hcd_obj_dmy); + return err_ret; +} + +esp_err_t hcd_uninstall(void) +{ + HCD_ENTER_CRITICAL(); + //Check that all ports have been disabled (theres only one) + if (s_hcd_obj == NULL || s_hcd_obj->port_obj->initialized) { + HCD_EXIT_CRITICAL(); + return ESP_ERR_INVALID_STATE; + } + periph_module_disable(PERIPH_USB_MODULE); + hcd_obj_t *p_hcd_obj_dmy = s_hcd_obj; + s_hcd_obj = NULL; + HCD_EXIT_CRITICAL(); + + //Free resources + port_obj_free(p_hcd_obj_dmy->port_obj); + esp_intr_free(p_hcd_obj_dmy->isr_hdl); + free(p_hcd_obj_dmy); + return ESP_OK; +} + +// ------------------------------------------------------ Port --------------------------------------------------------- + +// ----------------------- Private ------------------------- + +static void _port_invalidate_all_pipes(port_t *port) +{ + //This function should only be called when the port is invalid + assert(!port->flags.conn_devc_ena); + pipe_t *pipe; + //Process all pipes that have queued transfer requests + TAILQ_FOREACH(pipe, &port->pipes_queued_tailq, tailq_entry) { + //Mark the pipe as invalid and set an invalid event + pipe->state = HCD_PIPE_STATE_INVALID; + pipe->last_event = HCD_PIPE_EVENT_INVALID; + //If the pipe had an inflight transfer, parse and return it + if (pipe->inflight_xfer_req != NULL) { + _xfer_req_parse(pipe, true); + _pipe_ret_cur_xfer_req(pipe); + } + //Retire any remaining transfer requests + _pipe_retire(pipe, false); + if (pipe->task_waiting_pipe_notif != NULL) { + //Unblock the thread/task waiting for a notification from the pipe as the pipe is no longer valid. + _internal_pipe_event_notify(pipe, false); + } + if (pipe->callback != NULL) { + HCD_EXIT_CRITICAL(); + (void) pipe->callback((hcd_pipe_handle_t)pipe, HCD_PIPE_EVENT_INVALID, pipe->callback_arg, false); + HCD_ENTER_CRITICAL(); + } + } + //Process all idle pipes + TAILQ_FOREACH(pipe, &port->pipes_idle_tailq, tailq_entry) { + //Mark pipe as invalid and call its callback + pipe->state = HCD_PIPE_STATE_INVALID; + pipe->last_event = HCD_PIPE_EVENT_INVALID; + if (pipe->callback != NULL) { + HCD_EXIT_CRITICAL(); + (void) pipe->callback((hcd_pipe_handle_t)pipe, HCD_PIPE_EVENT_INVALID, pipe->callback_arg, false); + HCD_ENTER_CRITICAL(); + } + } +} + +static bool _port_pause_all_pipes(port_t *port) +{ + assert(port->state == HCD_PORT_STATE_ENABLED); + pipe_t *pipe; + int num_pipes_waiting_done = 0; + //Process all pipes that have queued transfer requests + TAILQ_FOREACH(pipe, &port->pipes_queued_tailq, tailq_entry) { + if (pipe->inflight_xfer_req != NULL) { + //Pipe has an inflight transfer. Indicate to the pipe we are waiting the transfer to complete + pipe->flags.waiting_xfer_done = 1; + num_pipes_waiting_done++; + } else { + //No inflight transfer so no need to wait + pipe->flags.paused = 1; + } + } + //Process all idle pipes. They don't have queue transfer so just mark them as paused + TAILQ_FOREACH(pipe, &port->pipes_idle_tailq, tailq_entry) { + pipe->flags.paused = 1; + } + if (num_pipes_waiting_done > 0) { + //Indicate we need to wait for one or more pipes to complete their transfers + port->flags.num_pipes_waiting_pause = num_pipes_waiting_done; + port->flags.waiting_all_pipes_pause = 1; + return false; + } + return true; +} + +static void _port_unpause_all_pipes(port_t *port) +{ + assert(port->state == HCD_PORT_STATE_ENABLED); + pipe_t *pipe; + //Process all idle pipes. They don't have queue transfer so just mark them as un-paused + TAILQ_FOREACH(pipe, &port->pipes_idle_tailq, tailq_entry) { + pipe->flags.paused = 0; + } + //Process all pipes that have queued transfer requests + TAILQ_FOREACH(pipe, &port->pipes_queued_tailq, tailq_entry) { + pipe->flags.paused = 0; + //If the pipe has more pending transfer request, start them. + if (_pipe_get_next_xfer_req(pipe)) { + _xfer_req_fill(pipe); + usbh_hal_chan_activate(pipe->chan_obj, 0); + } + } +} + +static bool _port_bus_reset(port_t *port) +{ + assert(port->state == HCD_PORT_STATE_ENABLED || port->state == HCD_PORT_STATE_DISABLED); + //Put and hold the bus in the reset state. If the port was previously enabled, a disabled event will occur after this + port->state = HCD_PORT_STATE_RESETTING; + usbh_hal_port_toggle_reset(port->hal, true); + HCD_EXIT_CRITICAL(); + vTaskDelay(pdMS_TO_TICKS(RESET_HOLD_MS)); + HCD_ENTER_CRITICAL(); + if (port->state != HCD_PORT_STATE_RESETTING) { + //The port state has unexpectedly changed + goto bailout; + } + //Return the bus to the idle state and hold it for the required reset recovery time. Port enabled event should occur + usbh_hal_port_toggle_reset(port->hal, false); + HCD_EXIT_CRITICAL(); + vTaskDelay(pdMS_TO_TICKS(RESET_RECOVERY_MS)); + HCD_ENTER_CRITICAL(); + if (port->state != HCD_PORT_STATE_ENABLED || !port->flags.conn_devc_ena) { + //The port state has unexpectedly changed + goto bailout; + } + return true; +bailout: + return false; +} + +static bool _port_bus_suspend(port_t *port) +{ + assert(port->state == HCD_PORT_STATE_ENABLED); + //Pause all pipes before suspending the bus + if (!_port_pause_all_pipes(port)) { + //Need to wait for some pipes to pause. Wait for notification from ISR + _internal_port_event_wait(port); + if (port->state != HCD_PORT_STATE_ENABLED || !port->flags.conn_devc_ena) { + //Port state unexpectedley changed + goto bailout; + } + } + //All pipes are guaranteed paused at this point. Proceed to suspend the port + usbh_hal_port_suspend(port->hal); + port->state = HCD_PORT_STATE_SUSPENDED; + return true; +bailout: + return false; +} + +static bool _port_bus_resume(port_t *port) +{ + assert(port->state == HCD_PORT_STATE_SUSPENDED); + //Put and hold the bus in the K state. + usbh_hal_port_toggle_resume(port->hal, true); + port->state = HCD_PORT_STATE_RESUMING; + HCD_EXIT_CRITICAL(); + vTaskDelay(pdMS_TO_TICKS(RESUME_HOLD_MS)); + HCD_ENTER_CRITICAL(); + //Return and hold the bus to the J state (as port of the LS EOP) + usbh_hal_port_toggle_resume(port->hal, false); + if (port->state != HCD_PORT_STATE_RESUMING || !port->flags.conn_devc_ena) { + //Port state unexpectedley changed + goto bailout; + } + HCD_EXIT_CRITICAL(); + vTaskDelay(pdMS_TO_TICKS(RESUME_RECOVERY_MS)); + HCD_ENTER_CRITICAL(); + if (port->state != HCD_PORT_STATE_RESUMING || !port->flags.conn_devc_ena) { + //Port state unexpectedley changed + goto bailout; + } + port->state = HCD_PORT_STATE_ENABLED; + _port_unpause_all_pipes(port); + return true; +bailout: + return false; +} + +static bool _port_disable(port_t *port) +{ + assert(port->state == HCD_PORT_STATE_ENABLED || port->state == HCD_PORT_STATE_SUSPENDED); + if (port->state == HCD_PORT_STATE_ENABLED) { + //There may be pipes that are still transferring, so pause them. + if (!_port_pause_all_pipes(port)) { + //Need to wait for some pipes to pause. Wait for notification from ISR + _internal_port_event_wait(port); + if (port->state != HCD_PORT_STATE_ENABLED || !port->flags.conn_devc_ena) { + //Port state unexpectedley changed + goto bailout; + } + } + } + //All pipes are guaranteed paused at this point. Proceed to suspend the port. This should trigger an internal event + port->flags.disable_requested = 1; + usbh_hal_port_disable(port->hal); + _internal_port_event_wait(port); + if (port->state != HCD_PORT_STATE_DISABLED) { + //Port state unexpectedley changed + goto bailout; + } + _port_invalidate_all_pipes(port); + return true; +bailout: + return false; +} + +static bool _port_debounce(port_t *port) +{ + if (port->state == HCD_PORT_STATE_NOT_POWERED) { + //Disconnect event due to power off, no need to debounce or update port state. + return false; + } + HCD_EXIT_CRITICAL(); + vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_DELAY_MS)); + HCD_ENTER_CRITICAL(); + //Check the post-debounce state of the bus (i.e., whether it's actually connected/disconnected) + bool is_connected = usbh_hal_port_check_if_connected(port->hal); + if (is_connected) { + port->state = HCD_PORT_STATE_DISABLED; + } else { + port->state = HCD_PORT_STATE_DISCONNECTED; + } + //Disable debounce lock + usbh_hal_disable_debounce_lock(port->hal); + return is_connected; +} + +// ----------------------- Public -------------------------- + +esp_err_t hcd_port_init(int port_number, hcd_port_config_t *port_config, hcd_port_handle_t *port_hdl) +{ + HCD_CHECK(port_number > 0 && port_config != NULL && port_hdl != NULL, ESP_ERR_INVALID_ARG); + HCD_CHECK(port_number <= NUM_PORTS, ESP_ERR_NOT_FOUND); + + HCD_ENTER_CRITICAL(); + HCD_CHECK_FROM_CRIT(s_hcd_obj != NULL && !s_hcd_obj->port_obj->initialized, ESP_ERR_INVALID_STATE); + //Port object memory and resources (such as mutex) already be allocated. Just need to initialize necessary fields only + port_t *port_obj = s_hcd_obj->port_obj; + TAILQ_INIT(&port_obj->pipes_idle_tailq); + TAILQ_INIT(&port_obj->pipes_queued_tailq); + port_obj->state = HCD_PORT_STATE_NOT_POWERED; + port_obj->last_event = HCD_PORT_EVENT_NONE; + port_obj->callback = port_config->callback; + port_obj->callback_arg = port_config->callback_arg; + port_obj->context = port_config->context; + usbh_hal_init(port_obj->hal); + port_obj->initialized = true; + esp_intr_enable(s_hcd_obj->isr_hdl); + *port_hdl = (hcd_port_handle_t)port_obj; + HCD_EXIT_CRITICAL(); + + vTaskDelay(pdMS_TO_TICKS(INIT_DELAY_MS)); //Need a short delay before host mode takes effect + return ESP_OK; +} + +esp_err_t hcd_port_deinit(hcd_port_handle_t port_hdl) +{ + port_t *port = (port_t *)port_hdl; + + HCD_ENTER_CRITICAL(); + HCD_CHECK_FROM_CRIT(s_hcd_obj != NULL && port->initialized + && port->num_pipes_idle == 0 && port->num_pipes_queued == 0 + && (port->state == HCD_PORT_STATE_NOT_POWERED || port->state == HCD_PORT_STATE_RECOVERY) + && port->flags.val == 0 && port->task_waiting_port_notif == NULL, + ESP_ERR_INVALID_STATE); + port->initialized = false; + esp_intr_disable(s_hcd_obj->isr_hdl); + usbh_hal_deinit(port->hal); + HCD_EXIT_CRITICAL(); + + return ESP_OK; +} + +esp_err_t hcd_port_command(hcd_port_handle_t port_hdl, hcd_port_cmd_t command) +{ + esp_err_t ret = ESP_ERR_INVALID_STATE; + port_t *port = (port_t *)port_hdl; + xSemaphoreTake(port->port_mux, portMAX_DELAY); + HCD_ENTER_CRITICAL(); + if (port->initialized && !port->flags.event_pending) { //Port events need to be handled first before issuing a command + port->flags.cmd_processing = 1; + switch (command) { + case HCD_PORT_CMD_POWER_ON: { + //Port can only be powered on if currently unpowered + if (port->state == HCD_PORT_STATE_NOT_POWERED) { + port->state = HCD_PORT_STATE_DISCONNECTED; + usbh_hal_port_start(port->hal); + usbh_hal_port_toggle_power(port->hal, true); + ret = ESP_OK; + } + break; + } + case HCD_PORT_CMD_POWER_OFF: { + //Port can only be unpowered if already powered + if (port->state != HCD_PORT_STATE_NOT_POWERED) { + port->state = HCD_PORT_STATE_NOT_POWERED; + usbh_hal_port_stop(port->hal); + usbh_hal_port_toggle_power(port->hal, false); + //If a device is currently connected, this should trigger a disconnect event + ret = ESP_OK; + } + break; + } + case HCD_PORT_CMD_RESET: { + //Port can only a reset when it is in the enabled or disabled states (in case of new connection) + if (port->state == HCD_PORT_STATE_ENABLED || port->state == HCD_PORT_STATE_DISABLED) { + ret = (_port_bus_reset(port)) ? ESP_OK : ESP_ERR_INVALID_RESPONSE; + } + break; + } + case HCD_PORT_CMD_SUSPEND: { + //Port can only be suspended if already in the enabled state + if (port->state == HCD_PORT_STATE_ENABLED) { + ret = (_port_bus_suspend(port)) ? ESP_OK : ESP_ERR_INVALID_RESPONSE; + } + break; + } + case HCD_PORT_CMD_RESUME: { + //Port can only be resumed if already suspended + if (port->state == HCD_PORT_STATE_SUSPENDED) { + ret = (_port_bus_resume(port)) ? ESP_OK : ESP_ERR_INVALID_RESPONSE; + } + break; + } + case HCD_PORT_CMD_DISABLE: { + //Can only disable the port when already enabled or suspended + if (port->state == HCD_PORT_STATE_ENABLED || port->state == HCD_PORT_STATE_SUSPENDED) { + ret = (_port_disable(port)) ? ESP_OK : ESP_ERR_INVALID_RESPONSE; + } + break; + } + } + port->flags.cmd_processing = 0; + } + HCD_EXIT_CRITICAL(); + xSemaphoreGive(port->port_mux); + return ret; +} + +hcd_port_state_t hcd_port_get_state(hcd_port_handle_t port_hdl) +{ + port_t *port = (port_t *)port_hdl; + hcd_port_state_t ret; + HCD_ENTER_CRITICAL(); + ret = port->state; + HCD_EXIT_CRITICAL(); + return ret; +} + +esp_err_t hcd_port_get_speed(hcd_port_handle_t port_hdl, usb_speed_t *speed) +{ + port_t *port = (port_t *)port_hdl; + HCD_CHECK(speed != NULL, ESP_ERR_INVALID_ARG); + HCD_ENTER_CRITICAL(); + //Device speed is only valid if there is a resetted device connected to the port + HCD_CHECK_FROM_CRIT(port->flags.conn_devc_ena, ESP_ERR_INVALID_STATE); + *speed = usbh_hal_port_get_conn_speed(port->hal); + HCD_EXIT_CRITICAL(); + return ESP_OK; +} + +hcd_port_event_t hcd_port_handle_event(hcd_port_handle_t port_hdl) +{ + port_t *port = (port_t *)port_hdl; + hcd_port_event_t ret = HCD_PORT_EVENT_NONE; + xSemaphoreTake(port->port_mux, portMAX_DELAY); + HCD_ENTER_CRITICAL(); + if (port->initialized && port->flags.event_pending) { + port->flags.event_pending = 0; + port->flags.event_processing = 1; + ret = port->last_event; + switch (ret) { + case HCD_PORT_EVENT_CONNECTION: { + if (_port_debounce(port)) { + ret = HCD_PORT_EVENT_CONNECTION; + } + break; + } + case HCD_PORT_EVENT_DISCONNECTION: + if (_port_debounce(port)) { + //A device is still connected, so it was just a debounce + port->state = HCD_PORT_STATE_DISABLED; + ret = HCD_PORT_EVENT_NONE; + } else { + //No device conencted after debounce delay. This is an actual disconenction + port->state = HCD_PORT_STATE_DISCONNECTED; + ret = HCD_PORT_EVENT_DISCONNECTION; + } + break; + case HCD_PORT_EVENT_ERROR: + case HCD_PORT_EVENT_OVERCURRENT: + case HCD_PORT_EVENT_SUDDEN_DISCONN: { + _port_invalidate_all_pipes(port); + break; + } + default: { + break; + } + } + port->flags.event_processing = 0; + } else { + ret = HCD_PORT_EVENT_NONE; + } + HCD_EXIT_CRITICAL(); + xSemaphoreGive(port->port_mux); + return ret; +} + +esp_err_t hcd_port_recover(hcd_port_handle_t port_hdl) +{ + port_t *port = (port_t *)port_hdl; + HCD_ENTER_CRITICAL(); + HCD_CHECK_FROM_CRIT(s_hcd_obj != NULL && port->initialized && port->state == HCD_PORT_STATE_RECOVERY + && port->num_pipes_idle == 0 && port->num_pipes_queued == 0 + && port->flags.val == 0 && port->task_waiting_port_notif == NULL, + ESP_ERR_INVALID_STATE); + //We are about to do a soft reset on the peripheral. Disable the peripheral throughout + esp_intr_disable(s_hcd_obj->isr_hdl); + usbh_hal_core_soft_reset(port->hal); + port->state = HCD_PORT_STATE_NOT_POWERED; + port->last_event = HCD_PORT_EVENT_NONE; + port->flags.val = 0; + esp_intr_enable(s_hcd_obj->isr_hdl); + HCD_EXIT_CRITICAL(); + return ESP_OK; +} + +void *hcd_port_get_ctx(hcd_port_handle_t port_hdl) +{ + port_t *port = (port_t *)port_hdl; + void *ret; + HCD_ENTER_CRITICAL(); + ret = port->context; + HCD_EXIT_CRITICAL(); + return ret; +} + +// --------------------------------------------------- HCD Pipes ------------------------------------------------------- + +// ----------------------- Private ------------------------- + +static bool _pipe_get_next_xfer_req(pipe_t *pipe) +{ + assert(pipe->inflight_xfer_req == NULL); + bool ret; + //This function assigns the next pending transfer request to the inflight_xfer_req + if (pipe->num_xfer_req_pending > 0) { + //Set inflight_xfer_req to the next pending transfer request + pipe->inflight_xfer_req = TAILQ_FIRST(&pipe->pend_xfer_req_tailq); + TAILQ_REMOVE(&pipe->pend_xfer_req_tailq, pipe->inflight_xfer_req, tailq_entry); + pipe->inflight_xfer_req->state = XFER_REQ_STATE_INFLIGHT; + pipe->num_xfer_req_pending--; + ret = true; + } else { + ret = false; + } + return ret; +} + +static void _pipe_ret_cur_xfer_req(pipe_t *pipe) +{ + assert(pipe->inflight_xfer_req != NULL); + //Add the transfer request to the pipe's done tailq + TAILQ_INSERT_TAIL(&pipe->done_xfer_req_tailq, pipe->inflight_xfer_req, tailq_entry); + pipe->inflight_xfer_req->state = XFER_REQ_STATE_DONE; + pipe->inflight_xfer_req = NULL; + pipe->num_xfer_req_done++; +} + +static bool _pipe_wait_done(pipe_t *pipe) +{ + //Check if there is a currently inflight transfer request + if (pipe->inflight_xfer_req != NULL) { + //Wait for pipe to complete its transfer + pipe->flags.waiting_xfer_done = 1; + _internal_pipe_event_wait(pipe); + if (pipe->state == HCD_PIPE_STATE_INVALID) { + //The pipe become invalid whilst waiting for its internal event + pipe->flags.waiting_xfer_done = 0; //Need to manually reset this bit in this case + return false; + } + bool chan_halted = usbh_hal_chan_slot_request_halt(pipe->chan_obj); + assert(chan_halted); + (void) chan_halted; + } + return true; +} + +static void _pipe_retire(pipe_t *pipe, bool self_initiated) +{ + //Cannot have any inflight transfer request + assert(pipe->inflight_xfer_req == NULL); + if (pipe->num_xfer_req_pending > 0) { + //Process all remaining pending transfer requests + xfer_req_t *xfer_req; + TAILQ_FOREACH(xfer_req, &pipe->pend_xfer_req_tailq, tailq_entry) { + xfer_req->state = XFER_REQ_STATE_DONE; + //If we are initiating the retire, mark the transfer request as cancelled + xfer_req->irp->status = (self_initiated) ? USB_TRANSFER_STATUS_CANCELLED : USB_TRANSFER_STATUS_NO_DEVICE; + } + //Concatenated pending tailq to the done tailq + TAILQ_CONCAT(&pipe->done_xfer_req_tailq, &pipe->pend_xfer_req_tailq, tailq_entry); + pipe->num_xfer_req_done += pipe->num_xfer_req_pending; + pipe->num_xfer_req_pending = 0; + } +} + +static inline hcd_pipe_event_t pipe_decode_error_event(usbh_hal_chan_error_t chan_error) +{ + hcd_pipe_event_t event = HCD_PIPE_EVENT_NONE; + switch (chan_error) { + case USBH_HAL_CHAN_ERROR_XCS_XACT: + event = HCD_PIPE_EVENT_ERROR_XFER; + break; + case USBH_HAL_CHAN_ERROR_BNA: + event = HCD_PIPE_EVENT_ERROR_XFER_NOT_AVAIL; + break; + case USBH_HAL_CHAN_ERROR_PKT_BBL: + event = HCD_PIPE_EVENT_ERROR_OVERFLOW; + break; + case USBH_HAL_CHAN_ERROR_STALL: + event = HCD_PIPE_EVENT_ERROR_STALL; + break; + } + return event; +} + +// ----------------------- Public -------------------------- + +esp_err_t hcd_pipe_alloc(hcd_port_handle_t port_hdl, const hcd_pipe_config_t *pipe_config, hcd_pipe_handle_t *pipe_hdl) +{ + HCD_CHECK(port_hdl != NULL && pipe_config != NULL && pipe_hdl != NULL, ESP_ERR_INVALID_ARG); + port_t *port = (port_t *)port_hdl; + HCD_ENTER_CRITICAL(); + //Can only allocate a pipe if the targetted port is initialized and conencted to an enabled device + HCD_CHECK_FROM_CRIT(port->initialized && port->flags.conn_devc_ena, ESP_ERR_INVALID_STATE); + usb_speed_t port_speed = port->speed; + HCD_EXIT_CRITICAL(); + //Cannot connect to a FS device if the port is LS + HCD_CHECK(port_speed == USB_SPEED_FULL || (port_speed == USB_SPEED_LOW && pipe_config->dev_speed == USB_SPEED_LOW), ESP_ERR_NOT_SUPPORTED); + + esp_err_t ret = ESP_OK; + //Get the type of pipe to allocate + usb_xfer_type_t type; + bool is_default_pipe; + if (pipe_config->ep_desc == NULL) { //A NULL ep_desc indicates we are allocating a default pipe + type = USB_XFER_TYPE_CTRL; + is_default_pipe = true; + } else { + type = USB_DESC_EP_GET_XFERTYPE(pipe_config->ep_desc); + is_default_pipe = false; + } + size_t num_xfer_desc = 0; + switch (type) { + case USB_XFER_TYPE_CTRL: { + num_xfer_desc = XFER_LIST_LEN_CTRL * NUM_DESC_PER_XFER_CTRL; + break; + } + case USB_XFER_TYPE_BULK: { + if (pipe_config->dev_speed == USB_SPEED_LOW) { + return ESP_ERR_NOT_SUPPORTED; //Low speed devices do not support bulk transfers + } + num_xfer_desc = XFER_LIST_LEN_BULK * NUM_DESC_PER_XFER_BULK; + break; + } + default: { + //Isochronous and Interrupt pipes currently not supported + return ESP_ERR_NOT_SUPPORTED; + } + } + + //Allocate the pipe resources + pipe_t *pipe = calloc(1, sizeof(pipe_t)); + usbh_hal_chan_t *chan_obj = malloc(sizeof(usbh_hal_chan_t)); + void *xfer_desc_list = heap_caps_aligned_calloc(USBH_HAL_DMA_MEM_ALIGN, num_xfer_desc, USBH_HAL_XFER_DESC_SIZE, MALLOC_CAP_DMA); + if (pipe == NULL|| chan_obj == NULL || xfer_desc_list == NULL) { + ret = ESP_ERR_NO_MEM; + goto err; + } + + //Initialize pipe object + TAILQ_INIT(&pipe->pend_xfer_req_tailq); + TAILQ_INIT(&pipe->done_xfer_req_tailq); + pipe->port = port; + pipe->xfer_desc_list = xfer_desc_list; + pipe->flags.xfer_desc_list_len = num_xfer_desc; + pipe->chan_obj = chan_obj; + pipe->ep_char.type = type; + if (is_default_pipe) { + pipe->ep_char.bEndpointAddress = 0; + //Set the default pipe's MPS to the worst case MPS for the device's speed + pipe->ep_char.mps = (pipe_config->dev_speed == USB_SPEED_FULL) ? CTRL_EP_MAX_MPS_FS : CTRL_EP_MAX_MPS_LS; + } else { + pipe->ep_char.bEndpointAddress = pipe_config->ep_desc->bEndpointAddress; + pipe->ep_char.mps = pipe_config->ep_desc->wMaxPacketSize; + } + pipe->ep_char.dev_addr = pipe_config->dev_addr; + pipe->ep_char.ls_via_fs_hub = (port_speed == USB_SPEED_FULL && pipe_config->dev_speed == USB_SPEED_LOW); + pipe->state = HCD_PIPE_STATE_ACTIVE; + pipe->callback = pipe_config->callback; + pipe->callback_arg = pipe_config->callback_arg; + pipe->context = pipe_config->context; + + //Allocate channel + HCD_ENTER_CRITICAL(); + if (!port->initialized || !port->flags.conn_devc_ena) { + HCD_EXIT_CRITICAL(); + ret = ESP_ERR_INVALID_STATE; + goto err; + } + bool chan_allocated = usbh_hal_chan_alloc(port->hal, pipe->chan_obj, (void *) pipe); + if (!chan_allocated) { + HCD_EXIT_CRITICAL(); + ret = ESP_ERR_NOT_SUPPORTED; + goto err; + } + usbh_hal_chan_set_ep_char(pipe->chan_obj, &pipe->ep_char); + + //Add the pipe to the list of idle pipes in the port object + TAILQ_INSERT_TAIL(&port->pipes_idle_tailq, pipe, tailq_entry); + port->num_pipes_idle++; + HCD_EXIT_CRITICAL(); + *pipe_hdl = (hcd_pipe_handle_t)pipe; + return ret; + +err: + free(xfer_desc_list); + free(chan_obj); + free(pipe); + return ret; +} + +esp_err_t hcd_pipe_free(hcd_pipe_handle_t pipe_hdl) +{ + pipe_t *pipe = (pipe_t *)pipe_hdl; + HCD_ENTER_CRITICAL(); + //Check that all transfer requests have been removed and pipe has no pending events + HCD_CHECK_FROM_CRIT(pipe->inflight_xfer_req == NULL + && pipe->num_xfer_req_pending == 0 + && pipe->num_xfer_req_done == 0, + ESP_ERR_INVALID_STATE); + //Remove pipe from the list of idle pipes (it must be in the idle list because it should have no queued transfer requests) + TAILQ_REMOVE(&pipe->port->pipes_idle_tailq, pipe, tailq_entry); + pipe->port->num_pipes_idle--; + usbh_hal_chan_free(pipe->port->hal, pipe->chan_obj); + HCD_EXIT_CRITICAL(); + + //Free pipe resources + free(pipe->xfer_desc_list); + free(pipe->chan_obj); + free(pipe); + return ESP_OK; +} + +esp_err_t hcd_pipe_update(hcd_pipe_handle_t pipe_hdl, uint8_t dev_addr, int mps) +{ + pipe_t *pipe = (pipe_t *)pipe_hdl; + HCD_ENTER_CRITICAL(); + //Check if pipe is in the correct state to be updated + HCD_CHECK_FROM_CRIT(pipe->state != HCD_PIPE_STATE_INVALID + && !pipe->flags.pipe_cmd_processing + && pipe->num_xfer_req_pending == 0 + && pipe->num_xfer_req_done == 0, + ESP_ERR_INVALID_STATE); + //Check that all transfer requests have been removed and pipe has no pending events + pipe->ep_char.dev_addr = dev_addr; + pipe->ep_char.mps = mps; + usbh_hal_chan_set_ep_char(pipe->chan_obj, &pipe->ep_char); + HCD_EXIT_CRITICAL(); + return ESP_OK; +} + +void *hcd_pipe_get_ctx(hcd_pipe_handle_t pipe_hdl) +{ + pipe_t *pipe = (pipe_t *) pipe_hdl; + void *ret; + HCD_ENTER_CRITICAL(); + ret = pipe->context; + HCD_EXIT_CRITICAL(); + return ret; +} + +hcd_pipe_state_t hcd_pipe_get_state(hcd_pipe_handle_t pipe_hdl) +{ + hcd_pipe_state_t ret; + pipe_t *pipe = (pipe_t *) pipe_hdl; + HCD_ENTER_CRITICAL(); + //If there is no enabled device, all existing pipes are invalid. + if (pipe->port->state != HCD_PORT_STATE_ENABLED + && pipe->port->state != HCD_PORT_STATE_SUSPENDED + && pipe->port->state != HCD_PORT_STATE_RESUMING) { + ret = HCD_PIPE_STATE_INVALID; + } else { + ret = pipe->state; + } + HCD_EXIT_CRITICAL(); + return ret; +} + +esp_err_t hcd_pipe_command(hcd_pipe_handle_t pipe_hdl, hcd_pipe_cmd_t command) +{ + pipe_t *pipe = (pipe_t *) pipe_hdl; + bool ret = ESP_OK; + + HCD_ENTER_CRITICAL(); + //Cannot execute pipe commands the pipe is already executing a command, or if the pipe or its port are no longer valid + if (pipe->flags.pipe_cmd_processing || !pipe->port->flags.conn_devc_ena || pipe->state == HCD_PIPE_STATE_INVALID) { + ret = ESP_ERR_INVALID_STATE; + } else { + pipe->flags.pipe_cmd_processing = 1; + switch (command) { + case HCD_PIPE_CMD_ABORT: { + //Retire all scheduled transfer requests. Pipe's state remains unchanged + if (!_pipe_wait_done(pipe)) { //Stop any on going transfers + ret = ESP_ERR_INVALID_RESPONSE; + break; + } + _pipe_retire(pipe, true); //Retire any pending transfers + break; + } + case HCD_PIPE_CMD_RESET: { + //Retire all scheduled transfer requests. Pipe's state moves to active + if (!_pipe_wait_done(pipe)) { //Stop any on going transfers + ret = ESP_ERR_INVALID_RESPONSE; + break; + } + _pipe_retire(pipe, true); //Retire any pending transfers + pipe->state = HCD_PIPE_STATE_ACTIVE; + break; + } + case HCD_PIPE_CMD_CLEAR: { //Can only do this if port is still active + //Pipe's state moves from halted to active + if (pipe->state == HCD_PIPE_STATE_HALTED) { + pipe->state = HCD_PIPE_STATE_ACTIVE; + //Start the next pending transfer if it exists + if (_pipe_get_next_xfer_req(pipe)) { + //Fill the descriptor list with the transfer request and start the transfer + _xfer_req_fill(pipe); + usbh_hal_chan_activate(pipe->chan_obj, 0); //Start with the first descriptor + } + } + break; + } + case HCD_PIPE_CMD_HALT: { + //Pipe's state moves to halted + if (!_pipe_wait_done(pipe)) { //Stop any on going transfers + ret = ESP_ERR_INVALID_RESPONSE; + break; + } + pipe->state = HCD_PIPE_STATE_HALTED; + break; + } + } + pipe->flags.pipe_cmd_processing = 0; + } + HCD_EXIT_CRITICAL(); + return ret; +} + +hcd_pipe_event_t hcd_pipe_get_event(hcd_pipe_handle_t pipe_hdl) +{ + pipe_t *pipe = (pipe_t *) pipe_hdl; + hcd_pipe_event_t ret; + HCD_ENTER_CRITICAL(); + ret = pipe->last_event; + pipe->last_event = HCD_PIPE_EVENT_NONE; + HCD_EXIT_CRITICAL(); + return ret; +} + +// ----------------------------------------------- HCD Transfer Requests ----------------------------------------------- + +// ----------------------- Private ------------------------- + +static void _xfer_req_fill(pipe_t *pipe) +{ + //inflight_xfer_req of the pipe must already set to the target transfer request + assert(pipe->inflight_xfer_req != NULL); + //Fill transfer descriptor list with a single transfer request + usb_irp_t *usb_irp = pipe->inflight_xfer_req->irp; + switch (pipe->ep_char.type) { + case USB_XFER_TYPE_CTRL: { + //Get information about the contorl transfer by analyzing the setup packet (the first 8 bytes) + usb_ctrl_req_t *ctrl_req = (usb_ctrl_req_t *)usb_irp->data_buffer; + pipe->flags.ctrl_data_stg_in = ((ctrl_req->bRequestType & USB_B_REQUEST_TYPE_DIR_IN) != 0); + pipe->flags.ctrl_data_stg_skip = (usb_irp->num_bytes == 0); + + //Fill setup stage + usbh_hal_xfer_desc_fill(pipe->xfer_desc_list, 0, usb_irp->data_buffer, sizeof(usb_ctrl_req_t), + USBH_HAL_XFER_DESC_FLAG_SETUP | USBH_HAL_XFER_DESC_FLAG_HALT); + if (pipe->flags.ctrl_data_stg_skip) { + //Fill a NULL packet if there is no data stage + usbh_hal_xfer_desc_fill(pipe->xfer_desc_list, 1, NULL, 0, USBH_HAL_XFER_DESC_FLAG_NULL); + } else { + //Fill data stage + usbh_hal_xfer_desc_fill(pipe->xfer_desc_list, 1, usb_irp->data_buffer + sizeof(usb_ctrl_req_t), usb_irp->num_bytes, + ((pipe->flags.ctrl_data_stg_in) ? USBH_HAL_XFER_DESC_FLAG_IN : 0) | USBH_HAL_XFER_DESC_FLAG_HALT); + } + //Fill status stage (i.e., a zero length packet). If data stage is skipped, the status stage is always IN. + usbh_hal_xfer_desc_fill(pipe->xfer_desc_list, 2, NULL, 0, + ((pipe->flags.ctrl_data_stg_in && !pipe->flags.ctrl_data_stg_skip) ? 0 : USBH_HAL_XFER_DESC_FLAG_IN) | USBH_HAL_XFER_DESC_FLAG_HALT); + //Set the channel's direction to OUT and PID to 0 respectively for the the setup stage + usbh_hal_chan_set_dir(pipe->chan_obj, false); //Setup stage is always OUT + usbh_hal_chan_set_pid(pipe->chan_obj, 0); //Setup stage always has a PID of DATA0 + break; + } + case USB_XFER_TYPE_BULK: { + bool is_in = pipe->ep_char.bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK; + usbh_hal_xfer_desc_fill(pipe->xfer_desc_list, 0, usb_irp->data_buffer, usb_irp->num_bytes, + ((is_in) ? USBH_HAL_XFER_DESC_FLAG_IN : 0) | USBH_HAL_XFER_DESC_FLAG_HALT); + break; + } + default: { + break; //Isoc and Interrupt transfers not supported yet + } + } + //Claim slot + usbh_hal_chan_slot_acquire(pipe->chan_obj, pipe->xfer_desc_list, pipe->flags.xfer_desc_list_len, (void *)pipe); +} + +static void _xfer_req_continue(pipe_t *pipe) +{ + int next_idx = usbh_hal_chan_get_next_desc_index(pipe->chan_obj); + bool next_dir_is_in; //Next descriptor direction is IN + int next_pid; //Next PID (DATA0 or DATA 1) + int num_to_skip; //Number of descriptors to skip + if (next_idx == 1) { + //Just finished setup stage + if (pipe->flags.ctrl_data_stg_skip) { + //Skipping data stage. Go straight to status stage + next_dir_is_in = true; //With no data stage, status stage must be IN + next_pid = 1; //Status stage always has a PID of DATA1 + num_to_skip = 1; //Skip over the null descriptor representing the skipped data stage + } else { + //Go to data stage + next_dir_is_in = pipe->flags.ctrl_data_stg_in; + next_pid = 1; //Data stage always starts with a PID of DATA1 + num_to_skip = 0; + } + } else { //next_idx == 2 + //Going to status stage from data stage + next_dir_is_in = !pipe->flags.ctrl_data_stg_in; //Status stage is opposite direction of data stage + next_pid = 1; //Status stage always has a PID of DATA1 + num_to_skip = 0; + } + + usbh_hal_chan_set_dir(pipe->chan_obj, next_dir_is_in); + usbh_hal_chan_set_pid(pipe->chan_obj, next_pid); + usbh_hal_chan_activate(pipe->chan_obj, num_to_skip); //Start the next stage +} + +static void _xfer_req_parse(pipe_t *pipe, bool error_occurred) +{ + assert(pipe->inflight_xfer_req != NULL); + //Release the slot + void *xfer_desc_list; + int xfer_desc_len; + usbh_hal_chan_slot_release(pipe->chan_obj, &xfer_desc_list, &xfer_desc_len); + assert(xfer_desc_list == pipe->xfer_desc_list); + (void) xfer_desc_len; + + //Parse the transfer descriptor list for the result of the transfer + usb_irp_t *usb_irp = pipe->inflight_xfer_req->irp; + usb_transfer_status_t xfer_status; + int xfer_rem_len; + if (error_occurred) { + //Either a pipe error has occurred or the pipe is no longer valid + if (pipe->state == HCD_PIPE_STATE_INVALID) { + xfer_status = USB_TRANSFER_STATUS_NO_DEVICE; + } else { + //Must have been a pipe error event + switch (pipe->last_event) { + case HCD_PIPE_EVENT_ERROR_XFER: //Excessive transaction error + xfer_status = USB_TRANSFER_STATUS_ERROR; + break; + case HCD_PIPE_EVENT_ERROR_OVERFLOW: + xfer_status = USB_TRANSFER_STATUS_OVERFLOW; + break; + case HCD_PIPE_EVENT_ERROR_STALL: + xfer_status = USB_TRANSFER_STATUS_STALL; + break; + default: + //HCD_PIPE_EVENT_ERROR_XFER_NOT_AVAIL should never occur + abort(); + break; + } + } + //We assume no bytes transmitted because of an error. + xfer_rem_len = usb_irp->num_bytes; + } else { + int desc_status; + switch (pipe->ep_char.type) { + case USB_XFER_TYPE_CTRL: { + if (pipe->flags.ctrl_data_stg_skip) { + //There was no data stage. Just set it as successful + desc_status = USBH_HAL_XFER_DESC_STS_SUCCESS; + xfer_rem_len = 0; + } else { + //Check the data stage (index 1) + usbh_hal_xfer_desc_parse(pipe->xfer_desc_list, 1, &xfer_rem_len, &desc_status); + } + break; + } + case USB_XFER_TYPE_BULK: { + usbh_hal_xfer_desc_parse(pipe->xfer_desc_list, 0, &xfer_rem_len, &desc_status); + break; + } + default: { + //We don't supportISOC and INTR pipes yet + desc_status = USBH_HAL_XFER_DESC_STS_NOT_EXECUTED; + xfer_rem_len = 0; + xfer_status = USB_TRANSFER_STATUS_ERROR; + abort(); + break; + } + } + xfer_status = USB_TRANSFER_STATUS_COMPLETED; + assert(desc_status == USBH_HAL_XFER_DESC_STS_SUCCESS); + } + //Write back results to IRP + usb_irp->actual_num_bytes = usb_irp->num_bytes - xfer_rem_len; + usb_irp->status = xfer_status; +} + +// ----------------------- Public -------------------------- + +hcd_xfer_req_handle_t hcd_xfer_req_alloc() +{ + xfer_req_t *xfer_req = calloc(1, sizeof(xfer_req_t)); + xfer_req->state = XFER_REQ_STATE_IDLE; + return (hcd_xfer_req_handle_t) xfer_req; +} + +void hcd_xfer_req_free(hcd_xfer_req_handle_t req_hdl) +{ + if (req_hdl == NULL) { + return; + } + xfer_req_t *xfer_req = (xfer_req_t *) req_hdl; + //Cannot free a transfer request that is still being used + assert(xfer_req->state == XFER_REQ_STATE_IDLE); + free(xfer_req); +} + +void hcd_xfer_req_set_target(hcd_xfer_req_handle_t req_hdl, hcd_pipe_handle_t pipe_hdl, usb_irp_t *irp, void *context) +{ + xfer_req_t *xfer_req = (xfer_req_t *) req_hdl; + //Can only set an transfer request's target when the transfer request is idl + assert(xfer_req->state == XFER_REQ_STATE_IDLE); + xfer_req->pipe = (pipe_t *) pipe_hdl; + xfer_req->irp = irp; + xfer_req->context = context; +} + +void hcd_xfer_req_get_target(hcd_xfer_req_handle_t req_hdl, hcd_pipe_handle_t *pipe_hdl, usb_irp_t **irp, void **context) +{ + xfer_req_t *xfer_req = (xfer_req_t *) req_hdl; + *pipe_hdl = (hcd_pipe_handle_t) xfer_req->pipe; + *irp = xfer_req->irp; + *context = xfer_req->context; +} + +esp_err_t hcd_xfer_req_enqueue(hcd_xfer_req_handle_t req_hdl) +{ + xfer_req_t *xfer_req = (xfer_req_t *) req_hdl; + HCD_CHECK(xfer_req->pipe != NULL && xfer_req->irp != NULL //The transfer request's target must be set + && xfer_req->state == XFER_REQ_STATE_IDLE, //The transfer request cannot be already enqueued + ESP_ERR_INVALID_STATE); + pipe_t *pipe = xfer_req->pipe; + HCD_ENTER_CRITICAL(); + HCD_CHECK_FROM_CRIT(pipe->port->state == HCD_PORT_STATE_ENABLED //The pipe's port must be in the correct state + && pipe->state == HCD_PIPE_STATE_ACTIVE //The pipe must be in the correct state + && !pipe->flags.pipe_cmd_processing, //Pipe cannot currently be processing a pipe command + ESP_ERR_INVALID_STATE); + //Check if we can start execution on the pipe immediately + if (!pipe->flags.paused && pipe->num_xfer_req_pending == 0 && pipe->inflight_xfer_req == NULL) { + //Pipe isn't executing any transfers. Start immediately + pipe->inflight_xfer_req = xfer_req; + _xfer_req_fill(pipe); + usbh_hal_chan_activate(pipe->chan_obj, 0); //Start with the first descriptor + xfer_req->state = XFER_REQ_STATE_INFLIGHT; + if (pipe->num_xfer_req_done == 0) { + //This is the first transfer request to be enqueued into the pipe. Move the pipe to the list of queued pipes + TAILQ_REMOVE(&pipe->port->pipes_idle_tailq, pipe, tailq_entry); + TAILQ_INSERT_TAIL(&pipe->port->pipes_queued_tailq, pipe, tailq_entry); + pipe->port->num_pipes_idle--; + pipe->port->num_pipes_queued++; + } + } else { + //Add the transfer request to the pipe's pending tailq + TAILQ_INSERT_TAIL(&pipe->pend_xfer_req_tailq, xfer_req, tailq_entry); + pipe->num_xfer_req_pending++; + xfer_req->state = XFER_REQ_STATE_PENDING; + } + HCD_EXIT_CRITICAL(); + return ESP_OK; +} + +hcd_xfer_req_handle_t hcd_xfer_req_dequeue(hcd_pipe_handle_t pipe_hdl) +{ + pipe_t *pipe = (pipe_t *)pipe_hdl; + hcd_xfer_req_handle_t ret; + + HCD_ENTER_CRITICAL(); + if (pipe->num_xfer_req_done > 0) { + xfer_req_t *xfer_req = TAILQ_FIRST(&pipe->done_xfer_req_tailq); + TAILQ_REMOVE(&pipe->done_xfer_req_tailq, xfer_req, tailq_entry); + pipe->num_xfer_req_done--; + assert(xfer_req->state == XFER_REQ_STATE_DONE); + xfer_req->state = XFER_REQ_STATE_IDLE; + ret = (hcd_xfer_req_handle_t) xfer_req; + if (pipe->num_xfer_req_done == 0 && pipe->num_xfer_req_pending == 0) { + //This pipe has no more enqueued transfers. Move the pipe to the list of idle pipes + TAILQ_REMOVE(&pipe->port->pipes_queued_tailq, pipe, tailq_entry); + TAILQ_INSERT_TAIL(&pipe->port->pipes_idle_tailq, pipe, tailq_entry); + pipe->port->num_pipes_idle++; + pipe->port->num_pipes_queued--; + } + } else { + ret = NULL; + } + HCD_EXIT_CRITICAL(); + return ret; +} + +esp_err_t hcd_xfer_req_abort(hcd_xfer_req_handle_t req_hdl) +{ + xfer_req_t *xfer_req = (xfer_req_t *) req_hdl; + esp_err_t ret; + + HCD_ENTER_CRITICAL(); + switch (xfer_req->state) { + case XFER_REQ_STATE_PENDING: { + //Transfer request has not been executed so it can be aborted + pipe_t *pipe = xfer_req->pipe; + //Remove it form the pending queue + TAILQ_REMOVE(&pipe->pend_xfer_req_tailq, xfer_req, tailq_entry); + pipe->num_xfer_req_pending--; + //Add it to the done queue + TAILQ_INSERT_TAIL(&pipe->done_xfer_req_tailq, xfer_req, tailq_entry); + pipe->num_xfer_req_done++; + //Update the transfer request and associated IRP's status + xfer_req->state = XFER_REQ_STATE_DONE; + xfer_req->irp->status = USB_TRANSFER_STATUS_CANCELLED; + ret = ESP_OK; + break; + } + case XFER_REQ_STATE_IDLE: { + //Cannot abort a transfer request that was never enqueued + ret = ESP_ERR_INVALID_STATE; + break; + } + default :{ + //Transfer request is currently or has already been executed. Nothing to do. + ret = ESP_OK; + break; + } + } + HCD_EXIT_CRITICAL(); + return ret; +} diff --git a/components/usb/maintainers.md b/components/usb/maintainers.md new file mode 100644 index 0000000000..e9d6af7a9b --- /dev/null +++ b/components/usb/maintainers.md @@ -0,0 +1,105 @@ +# USB Host Stack Maintainers Notes + +This document is intended future maintainers of the ESP-IDF USB Host stack. + +Note: The underlying DWC_OTG controller will sometimes be referred to as the Host Controller in this document. + +The host driver is currently split into the following layers, ordered from the lowest (furthest away from the user) to the highest layer (closest to the user). + +* USB Host lower layer in `usbh_ll.h` +* USB HAL in `usbh_hal.h` and `usbh_hal.c` +* Host Controller Driver in `hcd.c` and `hcd.h` + +# USB Host Lower Layer + +* `usbh_ll.h` abstracts away the basic register operation of the **DWC** OTG controller +* The LL provides register operations of the DWC OTG controller operating under Host Mode using scatter/gather internal DMA. +* For non-host mode specific register operations (i.e. global registers), the functions are prefixed with `usb_ll_...()` +* For host mode specific register operations, the functions are prefixed with `usbh_ll_...()` + +# USB Host HAL + +The HAL layer abstracts the DWC_OTG operating in Host Mode using Internal Scatter/Gather DMA. The HAL presents an abstraction of a single Host Port and multiple channels. + +## HAL Host Port + +- Models a single USB port where a single device can be attached + - Actions: Port can be powered ON/OFF, reset, suspended + - Events: When an interrupt occurs, call `usbh_hal_decode_intr()` to decoded the port interrupt. + - Port can detect various events such as connection, disconnection, enabled (i.e., connected device successfully reset), overcurrent etc. + +## HAL Channels + +- Channels are essentially the controllers abstraction of USB pipes. At any one point in time, a channel can be configured to map to a particular endpoint on a particular connected device (i.e., a particular device address). +- Channels have to be allocated and freed. It's possible to change a channel's endpoint characteristics (i.e., EP number, device address, direction, transfer type etc) so long as the channel is in the Halted state whilst doing so. +- Channels transfer data using transfer descriptor lists (i.e., a list of DMA descriptors). Each channel has one slot for a single list. Use `usbh_hal_chan_slot_acquire()` to acquire a channel's list slot, and `usbh_hal_chan_activate()` to start the transfer. +- Once a transfer is completed, an channel event should be generated. Use `usbh_hal_chan_slot_release()` to free the slot, allowing for another transfer list to acquire the slot. +- To fill and parse a transfer descriptor list, use the `usbh_hal_xfer_desc_fill()` and `usbh_hal_xfer_desc_parse()` functions. +- Each channel and each channel slot will allow the callers to set a context variable. This allows client to associate a particular channel or an acquired slot with client objects (e.g., associate a channel to a HCD pipe object). + +# Host Controller Driver (HCD) + +The HCD (Host Controller Driver) abstracts the DWC_OTG as N number of ports and an arbitrary number of pipes that can be routed through one of the ports to a device. However note that the underlying hardware controller only has one port, so technically only one port can ever be enabled. + +- In other words, the HCD essentially implements a root hub (not fully behavioral compliant) that contains a single port. +- Pipes are "an association between an endpoint on a device and software on the host". Transfer requests (where each transfer request represents an entire USB transfer) can be enqueued into a pipe for transmission, and dequeued from a pipe when completed. + +The HCD currently has the following limitations: + +- HCD **does not** "present the root hub and its behavior according to the hub class definition". We currently don't have a hub driver yet, so the port commands in the driver do not fully represent an interface of a USB hub as described in 10.4 of the USB2.0 spec. +- No more than 8 pipes can be allocated at any one time due to underlying Host Controllers 8 channel limit. In the future, we could make particular pipes share a single Host Controller channel. +- The HCD currently only supports Control and Bulk transfer types. + +## HCD Port + +- An HCD port can be as a simplified version of a port on the Root Hub of the host controller. However, the complexity of parsing Hub Requests is discarded in favor of port commands (`hcd_port_cmd_t`) as the current USB Host Stack does not support hubs yet. +- A port must first initialized before it can be used. A port is identified by its handled of type `hcd_port_handle_t` +- The port can be manipulated using commands such as + - Powering the port ON/OFF + - Issuing reset/resume signals +- The various host port events are represented in the `hcd_port_event_t` enumeration +- When a fatal error (such as a sudden disconnection or a port over current), the port will be put into the HCD_PORT_STATE_RECOVERY state. The port can be deinitialized from there, or recovered using `hcd_port_recover()`. All the pipes routed through the port will be made invalid. + +## HCD Pipes + +- Pipes can be opened to a particular endpoint based on a descriptor provided on allocation. If opening a default pipe, a `NULL` descriptor can be provided. +- Transfer requests can be enqueued into a pipe. Pipes use a linked list internally, so there is in-theory no limit to the number of transfer requests that can be enqueued. +- Transfer requests need to be dequeued once they are completed. +- Transfer requests are essentially wrappers for USB IRPs (I/O Request Packets). Once allocated, transfer request need to have their target IRP and pipe set before being enqueued. +- Since the IRP is a `typedef` used throughout the entire Host stack, each layer simply needs to pass the pointer of the IRP to the next layer thus minimizing the amount of copying required. + +## HCD SW Arch + +The software arch for the HCD has the following properties and intentions: + +- Some static functions need to be called in critical sections whilst others need not. Therefore, static functions prefixed by an underscore ((e.g., `_some_static_func()`) will need to be called in critical sections. +- Some static functions may be blocking. Those functions will have a note in their descriptions. +- The HCD communicates events entirely through callbacks and polling/handling functions. The client can choose what type of data structure they want to use in the callbacks to keep track of port and pipe events. Callbacks don't even need to be used, and the HCD should be able to operate entirely on a polling basis. +- The port and each pipe have to be treated as completely separate entities (wither their own handles and events). This allows client to group these entities however it sees fit. For example, the client can: + - Let a hub driver manage the port, and manipulate the port via its port handle + - Group pipes into interfaces and expose the interface as a single entity +- The HCD will not internally allocate any tasks. It is up to the client to decide how distribute workload (e.g., a single while loop polling a port and all its pipes vs or each pipe having its own task). +- The HCD uses an interrupt process things that require low latency such as processing pipe transfer requests. Where possible, processing is deferred to the `hcd_port_handle_event()` to reduce ISR workload. + +### Events + +In order to communicate events to the client of the HCD, the HCD does not attempt to allocate any queues to store events that have occurred. Callbacks are used in order to achieve maximum flexibility. Within these callbacks, the client of the HCD is free too use whatever OS primitives they want to store/forward these events. + +There are two types callbacks that the HCD offers: + +- Port callback will run whenever an event a port occurs. `hcd_port_handle_event()` should be called after a port event occurs. +- A pipe callback on each pipe, that will run when a event occurs on a pipe. + +The client of the HCD can also forego callbacks entirely and simply poll for port and pipe events using the `hcd_port_handle_event()` and `hcd_pipe_get_event()` respectively. + +- Some of the port and pipe commands may need to block for a certain condition before the command is executed. For example when suspending the port, the port command must block until all pipes to finish their current transfers. The blocking and unblocking is handled by an internal event mechanism. + +### Thread Safety + +The HCD API is thread safe however the following limitations should be noted: + +- It is the client's responsibility to ensure that `hcd_install()` is called before any other HCD function is called +- Likewise, it is the client's responsibility to ensure that events and pipes are cleared before calling `hcd_port_deinit()`. +- `hcd_port_command()` is thread safe, but only one port command can be executed at any one time. Therefore HCD internally used a mutex to protect against concurrent commands. +- If multiple threads attempt to execute a command on the sample one, all but one of those threads will return with an invalid state error. +- HCD functions should not be called from critical sections and interrupts, as some functions (e.g., `hcd_port_command()` and `hcd_pipe_command()`) may be blocking. \ No newline at end of file diff --git a/components/usb/private_include/hcd.h b/components/usb/private_include/hcd.h new file mode 100644 index 0000000000..50b75e2454 --- /dev/null +++ b/components/usb/private_include/hcd.h @@ -0,0 +1,526 @@ +// Copyright 2015-2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include "hal/usb_types.h" +#include "hal/usbh_hal.h" +#include "esp_err.h" + +// ------------------------------------------------- Macros & Types ---------------------------------------------------- + +// ----------------------- States -------------------------- + +/** + * @brief States of the HCD port + * + * @note The port can be thought of as an abstraction of the Root Hub that contains + * a single port. + * @note These states roughly match the port states outlined in 11.5.1 of the + * USB2.0 specification. + */ +typedef enum { + HCD_PORT_STATE_NOT_POWERED, /**< The port is not powered */ + HCD_PORT_STATE_DISCONNECTED, /**< The port is powered but no device is conencted */ + HCD_PORT_STATE_DISABLED, /**< A device has connected to the port but has not been reset. SOF/keep alive are not being sent */ + HCD_PORT_STATE_RESETTING, /**< The port is issuing a reset condition */ + HCD_PORT_STATE_SUSPENDED, /**< The port has been suspended. */ + HCD_PORT_STATE_RESUMING, /**< The port is issuing a resume condition */ + HCD_PORT_STATE_ENABLED, /**< The port has been enabled. SOF/keep alive are being sent */ + HCD_PORT_STATE_RECOVERY, /**< Port needs to be recovered from a fatal error (port error, overcurrent, or sudden disconnection) */ +} hcd_port_state_t; + +/** + * @brief States of an HCD pipe + * + * Active: + * - Pipe is able to transmit data. Transfer request can be enqueued. + * - Event if pipe has no transfer requests enqueued, it can still be in the active state. + * Halted: + * - An error has occurred on the pipe. Transfer request will no longer be executed. + * - Halt should be cleared using the clear command + * Invalid: + * - The underlying device that the pipe connects is not longer valid, thus making the pipe invalid. + * - Pending transfer requests should be dequeued and the pipe should be freed. + */ +typedef enum { + HCD_PIPE_STATE_ACTIVE, /**< The pipe is active */ + HCD_PIPE_STATE_HALTED, /**< The pipe is halted */ + HCD_PIPE_STATE_INVALID, /**< The pipe no longer exists and should be freed */ +} hcd_pipe_state_t; + +// ----------------------- Events -------------------------- + +/** + * @brief HCD port events + * + * On receiving a port event, hcd_port_handle_event() should be called to handle that event + */ +typedef enum { + HCD_PORT_EVENT_NONE, /**< No event has ocurred. Or the previous event is no longer valid */ + HCD_PORT_EVENT_CONNECTION, /**< A device has been connected to the port */ + HCD_PORT_EVENT_DISCONNECTION, /**< A device disconnection has been detected */ + HCD_PORT_EVENT_ERROR, /**< A port error has been detected. Port is now HCD_PORT_STATE_RECOVERY */ + HCD_PORT_EVENT_OVERCURRENT, /**< Overcurrent detected on the port. Port is now HCD_PORT_STATE_RECOVERY */ + HCD_PORT_EVENT_SUDDEN_DISCONN, /**< The port has suddenly disconencted (i.e., there was an enabled device connected + to the port when the disconnection occurred. Port is now HCD_PORT_STATE_RECOVERY. */ +} hcd_port_event_t; + +/** + * @brief HCD pipe events + * + * @note Pipe error events will put the pipe into the HCD_PIPE_STATE_HALTED state + * @note The HCD_PIPE_EVENT_INVALID will put the pipe in the HCD_PIPE_STATE_INVALID state + */ +typedef enum { + HCD_PIPE_EVENT_NONE, /**< The pipe has no events (used to indicate no events when polling) */ + HCD_PIPE_EVENT_XFER_REQ_DONE, /**< The pipe has completed a transfer request and can be dequeued */ + HCD_PIPE_EVENT_INVALID, /**< The pipe is invalid because */ + HCD_PIPE_EVENT_ERROR_XFER, /**< Excessive (three consecutive) transaction errors (e.g., no ACK, bad CRC etc) */ + HCD_PIPE_EVENT_ERROR_XFER_NOT_AVAIL, /**< Transfer request was not available */ + HCD_PIPE_EVENT_ERROR_OVERFLOW, /**< Received more data than requested. Usually a Packet babble error + (i.e., an IN packet has exceeded the endpoint's MPS) */ + HCD_PIPE_EVENT_ERROR_STALL, /**< Pipe received a STALL response received */ +} hcd_pipe_event_t; + +// ---------------------- Commands ------------------------- + +/** + * @brief HCD port commands + */ +typedef enum { + HCD_PORT_CMD_POWER_ON, /**< Power ON the port */ + HCD_PORT_CMD_POWER_OFF, /**< Power OFF the port */ + HCD_PORT_CMD_RESET, /**< Issue a reset on the port */ + HCD_PORT_CMD_SUSPEND, /**< Suspend the port */ + HCD_PORT_CMD_RESUME, /**< Resume the port */ + HCD_PORT_CMD_DISABLE, /**< Disable the port (stops the SOFs or keep alive) */ +} hcd_port_cmd_t; + +/** + * @brief HCD pipe commands + * + * The pipe commands represent the list of pipe manipulations outlined in 10.5.2.2. of USB2.0 specification. + */ +typedef enum { + HCD_PIPE_CMD_ABORT, /**< Retire all scheduled transfer requests. Pipe's state remains unchanged */ + HCD_PIPE_CMD_RESET, /**< Retire all scheduled transfer requests. Pipe's state moves to active */ + HCD_PIPE_CMD_CLEAR, /**< Pipe's state moves from halted to active */ + HCD_PIPE_CMD_HALT /**< Pipe's state moves to halted */ +} hcd_pipe_cmd_t; + +// -------------------- Object Types ----------------------- + +/** + * @brief Port handle type + */ +typedef void * hcd_port_handle_t; + +/** + * @brief Pipe handle type + */ +typedef void * hcd_pipe_handle_t; + +/** + * @brief HCD transfer request handle type + */ +typedef void * hcd_xfer_req_handle_t; + +/** + * @brief Port event callback type + * + * This callback is run when a port event occurs + */ +typedef bool (*hcd_port_isr_callback_t)(hcd_port_handle_t port_hdl, hcd_port_event_t port_event, void *user_arg, bool in_isr); + +/** + * @brief Pipe event callback + * + * This callback is run when a pipe event occurs + */ +typedef bool (*hcd_pipe_isr_callback_t)(hcd_pipe_handle_t pipe_hdl, hcd_pipe_event_t pipe_event, void *user_arg, bool in_isr); + +/** + * @brief HCD configuration structure + */ +typedef struct { + int intr_flags; /**< Interrupt flags for HCD interrupt */ +} hcd_config_t; + +/** + * @brief Port configuration structure + */ +typedef struct { + hcd_port_isr_callback_t callback; /**< HCD port event callback */ + void *callback_arg; /**< User argument for HCD port callback */ + void *context; +} hcd_port_config_t; + +/** + * @brief Pipe configuration structure + * + * @note The callback can be set to NULL if no callback is required (e.g., using HCD in a polling manner). + */ +typedef struct { + hcd_pipe_isr_callback_t callback; /**< HCD pipe event ISR callback */ + void *callback_arg; /**< User argument for HCD pipe callback */ + void *context; /**< Context variable used to associate the pipe with upper layer object */ + usb_desc_ep_t *ep_desc; /**< Pointer to endpoint descriptor of the pipe */ + uint8_t dev_addr; /**< Device address of the pipe */ + usb_speed_t dev_speed; /**< Speed of the device */ +} hcd_pipe_config_t; + +// --------------------------------------------- Host Controller Driver ------------------------------------------------ + +/** + * @brief Installs the Host Controller Driver + * + * - Allocates memory and interrupt resources for the HCD and underlying ports + * - Setups up HCD to use internal PHY + * + * @note This function must be called before any other HCD function is called + * + * @param config HCD configuration + * @retval ESP_OK: HCD successfully installed + * @retval ESP_ERR_NO_MEM: Insufficient memory + * @retval ESP_ERR_INVALID_STATE: HCD is already installed + * @retval ESP_ERR_NOT_FOUND: HCD could not allocate interrupt + * @retval ESP_ERR_INVALID_ARG: Arguments are invalid + */ +esp_err_t hcd_install(const hcd_config_t *config); + +/** + * @brief Uninstalls the HCD + * + * Before uninstalling the HCD, the following conditions should be met: + * - All ports must be uninitialized, all pipes freed + * + * @retval ESP_OK: HCD successfully uninstalled + * @retval ESP_ERR_INVALID_STATE: HCD is not in the right condition to be uninstalled + */ +esp_err_t hcd_uninstall(void); + +// ---------------------------------------------------- HCD Port ------------------------------------------------------- + +/** + * @brief Initialize a particular port of the HCD + * + * After a port is initialized, it will be put into the HCD_PORT_STATE_NOT_POWERED state. + * + * @note The host controller only has one port, thus the only valid port_number is 1 + * + * @param[in] port_number Port number + * @param[in] port_config Port configuration + * @param[out] port_hdl Port handle + * @retval ESP_OK: Port enabled + * @retval ESP_ERR_NO_MEM: Insufficient memory + * @retval ESP_ERR_INVALID_STATE: The port is already enabled + * @retval ESP_ERR_NOT_FOUND: Port number not found + * @retval ESP_ERR_INVALID_ARG: Arguments are invalid + */ +esp_err_t hcd_port_init(int port_number, hcd_port_config_t *port_config, hcd_port_handle_t *port_hdl); + +/** + * @brief Deinitialize a particular port + * + * The port must be placed in the HCD_PORT_STATE_NOT_POWERED or HCD_PORT_STATE_RECOVERY state before it can be + * deinitialized. + * + * @param port_hdl Port handle + * @retval ESP_OK: Port disabled + * @retval ESP_ERR_INVALID_STATE: The port is not in a condition to be disabled (not unpowered) + */ +esp_err_t hcd_port_deinit(hcd_port_handle_t port_hdl); + +/** + * @brief Execute a port command + * + * Call this function to manipulate a port (e.g., powering it ON, sending a reset etc). The following conditions + * must be met when calling this function: + * - The port is in the correct state for the command (e.g., port must be suspend in order to use the resume command) + * - The port does not have any pending events + * + * @note This function is internally protected by a mutex. If multiple threads call this function, this function will + * can block. + * @note For some of the commands that involve a blocking delay (e.g., reset and resume), if the port's state changes + * unexpectedly (e.g., a disconnect during a resume), this function will return ESP_ERR_INVALID_RESPONSE. + * + * @param port_hdl Port handle + * @param command Command for the HCD port + * @retval ESP_OK: Command executed successfully + * @retval ESP_ERR_INVALID_STATE: Conditions have not been met to call this function + * @retval ESP_ERR_INVALID_RESPONSE: The command is no longer valid due to a change in the port's state + */ +esp_err_t hcd_port_command(hcd_port_handle_t port_hdl, hcd_port_cmd_t command); + +/** + * @brief Get the port's current state + * + * @param port_hdl Port handle + * @return hcd_port_state_t Current port state + */ +hcd_port_state_t hcd_port_get_state(hcd_port_handle_t port_hdl); + +/** + * @brief Get the speed of the port + * + * The speed of the port is determined by the speed of the device connected to it. + * + * @note This function is only valid after a device directly to the port and has been reset + * + * @param[in port_hdl Port handle + * @param[out] speed Speed of the port + * @retval ESP_OK Device speed obtained + * @retval ESP_ERR_INVALID_STATE: No valid device connected to the port + * @retval ESP_ERR_INVALID_ARG: Invalid arguments + */ +esp_err_t hcd_port_get_speed(hcd_port_handle_t port_hdl, usb_speed_t *speed); + +/** + * @brief Handle a ports event + * + * When an port event occurs (as indicated by a callback), this function should be called the handle this event. A + * port's event should always be handled before attempting to execute a port command. Note that is actually handled + * may be different than the event reflected in the callback. + * + * If the port has no events, this function will return HCD_PORT_EVENT_NONE. + * + * @note If callbacks are not used, this function can also be used in a polling manner to repeatedely check for and + * handle a port's events. + * @note This function is internally protected by a mutex. If multiple threads call this function, this function will + * can block. + * + * @param port_hdl Port handle + * @return hcd_port_event_t The port event that was handled + */ +hcd_port_event_t hcd_port_handle_event(hcd_port_handle_t port_hdl); + +/** + * @brief Recover a port after a fatal error has occurred on it + * + * The port must be in the HCD_PORT_STATE_RECOVERY state to be called. Recovering the port will involve issuing a soft + * reset on the underlying USB controller. The port will be returned to the HCD_PORT_STATE_NOT_POWERED state. + * + * @param port_hdl Port handle + * @retval ESP_OK Port recovered successfully + * @retval ESP_ERR_INVALID_STATE Port is not in the HCD_PORT_STATE_RECOVERY state + */ +esp_err_t hcd_port_recover(hcd_port_handle_t port_hdl); + +/** + * @brief Get the context variable of a port + * + * @param port_hdl Port handle + * @return void* Context variable + */ +void *hcd_port_get_ctx(hcd_port_handle_t port_hdl); + +// --------------------------------------------------- HCD Pipes ------------------------------------------------------- + +/** + * @brief Allocate a pipe + * + * When allocating a pipe, the HCD will assess whether there are sufficient resources (i.e., bus time, and controller + * channels). If sufficient, the pipe will be allocated. + * + * @note Currently, Interrupt and Isochronous pipes are not supported yet + * @note The host port must be in the enabled state before a pipe can be allcoated + * + * @param[in] port_hdl Handle of the port this pipe will be routed through + * @param[in] pipe_config Pipe configuration + * @param[out] pipe_hdl Pipe handle + * + * @retval ESP_OK: Pipe successfully allocated + * @retval ESP_ERR_NO_MEM: Insufficient memory + * @retval ESP_ERR_INVALID_ARG: Arguments are invalid + * @retval ESP_ERR_INVALID_STATE: Host port is not in the correct state to allocate a pipe + * @retval ESP_ERR_NOT_SUPPORTED: The pipe cannot be supported + */ +esp_err_t hcd_pipe_alloc(hcd_port_handle_t port_hdl, const hcd_pipe_config_t *pipe_config, hcd_pipe_handle_t *pipe_hdl); + +/** + * @brief Free a pipe + * + * Frees the resources used by an HCD pipe. The pipe's handle should be discarded after calling this function. The pipe + * must be in following condition before it can be freed: + * - All transfers have been dequeued + * + * @param pipe_hdl Pipe handle + * + * @retval ESP_OK: Pipe successfully freed + * @retval ESP_ERR_INVALID_STATE: Pipe is not in a condition to be freed + */ +esp_err_t hcd_pipe_free(hcd_pipe_handle_t pipe_hdl); + +/** + * @brief Update a pipe's device address and maximum packet size + * + * This function is intended to be called on default pipes during enumeration in order to update the pipe's device + * address and maximum packet size. This function can only be called on a pipe that has met the following conditions: + * - Pipe is still valid (i.e., not in the HCD_PIPE_STATE_INVALID state) + * - Pipe is not currently processing a command + * - All transfer request have been dequeued from the pipe + * + * @param pipe_hdl Pipe handle + * @param dev_addr New device address + * @param mps New Maximum Packet Size + * + * @retval ESP_OK: Pipe successfully updated + * @retval ESP_ERR_INVALID_STATE: Pipe is no in a condition to be updated + */ +esp_err_t hcd_pipe_update(hcd_pipe_handle_t pipe_hdl, uint8_t dev_addr, int mps); + +/** + * @brief Get the context variable of a pipe from its handle + * + * @param pipe_hdl Pipe handle + * @return void* Context variable + */ +void *hcd_pipe_get_ctx(hcd_pipe_handle_t pipe_hdl); + +/** + * @brief Get the current sate of the pipe + * + * @param pipe_hdl Pipe handle + * @return hcd_pipe_state_t Current state of the pipe + */ +hcd_pipe_state_t hcd_pipe_get_state(hcd_pipe_handle_t pipe_hdl); + +/** + * @brief Execute a command on a particular pipe + * + * Pipe commands allow a pipe to be manipulated (such as clearing a halt, retiring all transfer requests etc). The + * following conditions must for a pipe command to be issued: + * - Pipe is still valid (i.e., not in the HCD_PIPE_STATE_INVALID) + * - No other thread/task processing a command on the pipe concurrently (will return) + * + * @note Some pipe commands will block until the pipe's current inflight transfer is completed. If the pipe's state + * changes unexpectedley, this function will return ESP_ERR_INVALID_RESPONSE + * + * @param pipe_hdl Pipe handle + * @param command Pipe command + * @retval ESP_OK: Command executed successfully + * @retval ESP_ERR_INVALID_STATE: The pipe is not in the correct state/condition too execute the command + * @retval ESP_ERR_INVALID_RESPONSE: The pipe's state changed unexpectedley + */ +esp_err_t hcd_pipe_command(hcd_pipe_handle_t pipe_hdl, hcd_pipe_cmd_t command); + +/** + * @brief Get the last event that occurred on a pipe + * + * This function allows a pipe to be polled for events (i.e., when callbacks are not used). Once an event has been + * obtained, this function reset the last event of the pipe to HCD_PIPE_EVENT_NONE. + * + * @param pipe_hdl Pipe handle + * @return hcd_pipe_event_t Last pipe event to occur + */ +hcd_pipe_event_t hcd_pipe_get_event(hcd_pipe_handle_t pipe_hdl); + +// ----------------------------------------------- HCD Transfer Requests ----------------------------------------------- + +/** + * @brief Allocate a transfer request + * + * @note The allocate transfer request will not have its target set (i.e., no target pipe and associated IRP). Call + * hcd_xfer_req_set_target() before enqueueing the transfer request + * + * @return hcd_xfer_req_handle_t Transfer request handle or NULL if failed. + */ +hcd_xfer_req_handle_t hcd_xfer_req_alloc(void); + +/** + * @brief Free a transfer request + * + * @note The transfer request must be dequeued before it can be freed + * + * @param req_hdl Transfer request handle + */ +void hcd_xfer_req_free(hcd_xfer_req_handle_t req_hdl); + +/** + * @brief Set a transfer request's target + * + * Setting a transfer request's target will associate a transfer request with a pipe and a USB IRP (i.e., the data). A + * transfer request's target must be set before it can be enqueued. + * + * @note This should only be called when a transfer requests that are not currently enqueued + * + * @param req_hdl Transfer request handle + * @param pipe_hdl Target pipe's handle + * @param irp Target IRP handle + * @param context Context variable to associate transfer request with upper layer object + */ +void hcd_xfer_req_set_target(hcd_xfer_req_handle_t req_hdl, hcd_pipe_handle_t pipe_hdl, usb_irp_t *irp, void *context); + +/** + * @brief Get the target of a transfer request + * + * @note This should only be called when a transfer requests that are not currently enqueued + * + * @param[in] req_hdl Transfer request handle + * @param[out] pipe_hdl Target pipe's handle + * @param[out] irp Target IRP's handle + * @param[out] context Context variable + */ +void hcd_xfer_req_get_target(hcd_xfer_req_handle_t req_hdl, hcd_pipe_handle_t *pipe_hdl, usb_irp_t **irp, void **context); + +/** + * @brief Enqueue a transfer request + * + * The following conditions must be met for a transfer request to be enqueued: + * - The transfer request's target must be set + * - Transfer request must not already be enqueued + * - The target pipe must be in the HCD_PIPE_STATE_ACTIVE state + * + * @param req_hdl Transfer request handle + * @retval ESP_OK: Transfer request enqueued successfully + * @retval ESP_ERR_INVALID_STATE: Conditions not met to enqueue transfer request + */ +esp_err_t hcd_xfer_req_enqueue(hcd_xfer_req_handle_t req_hdl); + +/** + * @brief Dequeue a completed transfer request from a pipe + * + * This function should be called on a pipe after it receives an pipe event. If a pipe has multiple transfer requests + * that can be dequeued, this function must be called repeatedely until all transfer requests are dequeued. If a pipe + * has no more transfer requests to dequeue, this function will return NULL. + * + * @param pipe_hdl Pipe handle + * @return hcd_xfer_req_handle_t Transfer request handle or NULL if no more transfer requests to dequeue. + */ +hcd_xfer_req_handle_t hcd_xfer_req_dequeue(hcd_pipe_handle_t pipe_hdl); + +/** + * @brief Abort an ongoing transfer request + * + * This function will attempt to abort an enqueued transfer request. If the transfer request has not yet been executed, + * it will be marked as "cancelled" and can be dequeued. If a transfer request is already in progress or has completed, + * it will not be affected by this function. + * + * @param req_hdl Transfer request handle + * @retval ESP_OK: Transfer request successfully aborted, or did not need to be aborted + * @retval ESP_ERR_INVALID_STATE: Transfer request was never enqueued + */ +esp_err_t hcd_xfer_req_abort(hcd_xfer_req_handle_t req_hdl); + +#ifdef __cplusplus +} +#endif diff --git a/components/usb/test/CMakeLists.txt b/components/usb/test/CMakeLists.txt new file mode 100644 index 0000000000..d02b3aef31 --- /dev/null +++ b/components/usb/test/CMakeLists.txt @@ -0,0 +1,11 @@ +idf_build_get_property(target IDF_TARGET) + +#USB Host is currently only supported on ESP32-S2 +if(NOT "${target}" STREQUAL "esp32s2") + return() +endif() + +idf_component_register(SRC_DIRS "." + PRIV_INCLUDE_DIRS "." "../private_include" + PRIV_REQUIRES cmock usb test_utils + ) diff --git a/components/usb/test/component.mk b/components/usb/test/component.mk new file mode 100644 index 0000000000..d2183390cc --- /dev/null +++ b/components/usb/test/component.mk @@ -0,0 +1,4 @@ +# +# Component Makefile (not used for tests, but CI checks test parity between GNU Make & CMake) +# +COMPONENT_CONFIG_ONLY := 1 diff --git a/components/usb/test/test_hcd.c b/components/usb/test/test_hcd.c new file mode 100644 index 0000000000..e5919176b0 --- /dev/null +++ b/components/usb/test/test_hcd.c @@ -0,0 +1,833 @@ +// Copyright 2015-2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "unity.h" +#include "test_utils.h" +#include "esp_intr_alloc.h" +#include "esp_err.h" +#include "esp_attr.h" +#include "esp_rom_gpio.h" +#include "soc/gpio_pins.h" +#include "soc/gpio_sig_map.h" +#include "hal/usbh_ll.h" +#include "hcd.h" + +// -------------------------------------------------- PHY Control ------------------------------------------------------ + +static void phy_force_conn_state(bool connected, TickType_t delay_ticks) +{ + vTaskDelay(delay_ticks); + usb_wrap_dev_t *wrap = &USB_WRAP; + if (connected) { + //Swap back to internal PHY that is connected to a devicee + wrap->otg_conf.phy_sel = 0; + } else { + //Set externa PHY input signals to fixed voltage levels mimicing a disconnected state + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, USB_EXTPHY_VP_IDX, false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, USB_EXTPHY_VM_IDX, false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, USB_EXTPHY_RCV_IDX, false); + //Swap to the external PHY + wrap->otg_conf.phy_sel = 1; + } +} + +// ------------------------------------------------ Helper Functions --------------------------------------------------- + +#define EVENT_QUEUE_LEN 5 +#define NUM_XFER_REQS 3 +#define XFER_DATA_MAX_LEN 256 //Just assume that will only IN/OUT 256 bytes for now +#define PORT_NUM 1 + +typedef struct { + hcd_port_handle_t port_hdl; + hcd_port_event_t port_event; +} port_event_msg_t; + +typedef struct { + hcd_pipe_handle_t pipe_hdl; + hcd_pipe_event_t pipe_event; +} pipe_event_msg_t; + +static bool port_callback(hcd_port_handle_t port_hdl, hcd_port_event_t port_event, void *user_arg, bool in_isr) +{ + QueueHandle_t port_evt_queue = (QueueHandle_t)user_arg; + TEST_ASSERT(in_isr); //Current HCD implementation should never call a port callback in a task context + port_event_msg_t msg = { + .port_hdl = port_hdl, + .port_event = port_event, + }; + BaseType_t xTaskWoken = pdFALSE; + xQueueSendFromISR(port_evt_queue, &msg, &xTaskWoken); + return (xTaskWoken == pdTRUE); +} + +static bool pipe_callback(hcd_pipe_handle_t pipe_hdl, hcd_pipe_event_t pipe_event, void *user_arg, bool in_isr) +{ + QueueHandle_t pipe_evt_queue = (QueueHandle_t)user_arg; + pipe_event_msg_t msg = { + .pipe_hdl = pipe_hdl, + .pipe_event = pipe_event, + }; + if (in_isr) { + BaseType_t xTaskWoken = pdFALSE; + xQueueSendFromISR(pipe_evt_queue, &msg, &xTaskWoken); + return (xTaskWoken == pdTRUE); + } else { + xQueueSend(pipe_evt_queue, &msg, portMAX_DELAY); + return false; + } +} + +static void expect_port_event(QueueHandle_t port_evt_queue, hcd_port_handle_t expected_hdl, hcd_port_event_t expected_event) +{ + port_event_msg_t msg; + xQueueReceive(port_evt_queue, &msg, portMAX_DELAY); + TEST_ASSERT_EQUAL(expected_hdl, msg.port_hdl); + TEST_ASSERT_EQUAL(expected_event, msg.port_event); + printf("\t-> Port event\n"); +} + +static void expect_pipe_event(QueueHandle_t pipe_evt_queue, hcd_pipe_handle_t expected_hdl, hcd_pipe_event_t expected_event) +{ + pipe_event_msg_t msg; + xQueueReceive(pipe_evt_queue, &msg, portMAX_DELAY); + TEST_ASSERT_EQUAL(expected_hdl, msg.pipe_hdl); + TEST_ASSERT_EQUAL(expected_event, msg.pipe_event); +} + +/** + * @brief Creates port and pipe event queues. Sets up the HCD, and initializes a port. + * + * @param[out] port_evt_queue Port event queue + * @param[out] pipe_evt_queue Pipe event queue + * @param[out] port_hdl Port handle + */ +static void setup(QueueHandle_t *port_evt_queue, QueueHandle_t *pipe_evt_queue, hcd_port_handle_t *port_hdl) +{ + *port_evt_queue = xQueueCreate(EVENT_QUEUE_LEN, sizeof(port_event_msg_t)); + *pipe_evt_queue = xQueueCreate(EVENT_QUEUE_LEN, sizeof(pipe_event_msg_t)); + TEST_ASSERT_NOT_EQUAL(NULL, *port_evt_queue); + TEST_ASSERT_NOT_EQUAL(NULL, *pipe_evt_queue); + + //Install HCD + hcd_config_t config = { + .intr_flags = ESP_INTR_FLAG_LEVEL1, + }; + TEST_ASSERT_EQUAL(ESP_OK, hcd_install(&config)); + //Initialize a port + hcd_port_config_t port_config = { + .callback = port_callback, + .callback_arg = (void *)*port_evt_queue, + .context = NULL, + }; + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_init(PORT_NUM, &port_config, port_hdl)); + TEST_ASSERT_NOT_EQUAL(NULL, *port_hdl); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_NOT_POWERED, hcd_port_get_state(*port_hdl)); + phy_force_conn_state(false, 0); //Force disconnected state on PHY +} + +/** + * @brief Deinitializes the port, uninstalls HCD, and frees port and pipe event queues + * + * @param[in] port_evt_queue Port event queue + * @param[in] pipe_evt_queue Pipe event semaphore + * @param[in] port_hdl Port handle + */ +static void teardown(QueueHandle_t port_evt_queue, QueueHandle_t pipe_evt_queue, hcd_port_handle_t port_hdl) +{ + //Deinitialize a port + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_deinit(port_hdl)); + //Uninstall the HCD + TEST_ASSERT_EQUAL(ESP_OK, hcd_uninstall()); + vQueueDelete(port_evt_queue); + vQueueDelete(pipe_evt_queue); +} + +/** + * @brief Powers ON a port and waits for a connection, then resets the connected device + * + * @param port_hdl Port handle + * @param port_evt_queue Port event queue + */ +static void wait_for_connection(hcd_port_handle_t port_hdl, QueueHandle_t port_evt_queue) +{ + //Power ON the port + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_POWER_ON)); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_DISCONNECTED, hcd_port_get_state(port_hdl)); + //Wait for connection event + printf("Waiting for conenction\n"); + phy_force_conn_state(true, pdMS_TO_TICKS(100)); //Allow for connected state on PHY + expect_port_event(port_evt_queue, port_hdl, HCD_PORT_EVENT_CONNECTION); + TEST_ASSERT_EQUAL(HCD_PORT_EVENT_CONNECTION, hcd_port_handle_event(port_hdl)); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_DISABLED, hcd_port_get_state(port_hdl)); + //Reset newly connected device + printf("Resetting\n"); + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_RESET)); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_ENABLED, hcd_port_get_state(port_hdl)); + //Get speed of conencted + usb_speed_t port_speed; + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_get_speed(port_hdl, &port_speed)); + if (port_speed == USB_SPEED_FULL) { + printf("Full speed enabled\n"); + } else { + printf("Low speed enabled\n"); + } +} + +/** + * @brief Disables the port, waits for a disconnection, then powers OFF the port + * + * @param port_hdl Port handle + * @param port_evt_queue Port event queue + * @param already_disabled If the port is already disabled, it will skip disabling the port + */ +static void wait_for_disconnection(hcd_port_handle_t port_hdl, QueueHandle_t port_evt_queue, bool already_disabled) +{ + if (!already_disabled) { + //Disable the device + printf("Disabling\n"); + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_DISABLE)); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_DISABLED, hcd_port_get_state(port_hdl)); + } + //Wait for a safe disconnect + printf("Waiting for disconnection\n"); + phy_force_conn_state(false, pdMS_TO_TICKS(100)); //Force disconnected state on PHY + expect_port_event(port_evt_queue, port_hdl, HCD_PORT_EVENT_DISCONNECTION); + TEST_ASSERT_EQUAL(HCD_PORT_EVENT_DISCONNECTION, hcd_port_handle_event(port_hdl)); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_DISCONNECTED, hcd_port_get_state(port_hdl)); + //Power down the port + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_POWER_OFF)); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_NOT_POWERED, hcd_port_get_state(port_hdl)); +} + +static void alloc_pipe_and_xfer_reqs(hcd_port_handle_t port_hdl, + QueueHandle_t pipe_evt_queue, + hcd_pipe_handle_t *pipe_hdl, + hcd_xfer_req_handle_t *req_hdls, + uint8_t **data_buffers, + usb_irp_t **irps, + int num_xfers) +{ + //We don't support hubs yet. Just get the speed of the port to determine the speed of the device + usb_speed_t port_speed; + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_get_speed(port_hdl, &port_speed)); + + //Create default pipe + printf("Creating default pipe\n"); + hcd_pipe_config_t config = { + .callback = pipe_callback, + .callback_arg = (void *)pipe_evt_queue, + .context = NULL, + .ep_desc = NULL, //NULL EP descriptor to create a default pipe + .dev_addr = 0, + .dev_speed = port_speed, + }; + TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_alloc(port_hdl, &config, pipe_hdl)); + TEST_ASSERT_NOT_EQUAL(NULL, *pipe_hdl); + //Create transfer requests (and other required objects such as IRPs and data buffers) + printf("Creating transfer requests\n"); + for (int i = 0; i < num_xfers; i++) { + //Allocate transfer request object + req_hdls[i] = hcd_xfer_req_alloc(); + TEST_ASSERT_NOT_EQUAL(NULL, req_hdls[i]); + //Allocate data buffers + data_buffers[i] = heap_caps_malloc(sizeof(usb_ctrl_req_t) + XFER_DATA_MAX_LEN, MALLOC_CAP_DMA); + TEST_ASSERT_NOT_EQUAL(NULL, data_buffers[i]); + //Allocate IRP object + irps[i] = heap_caps_malloc(sizeof(usb_irp_t), MALLOC_CAP_DEFAULT); + TEST_ASSERT_NOT_EQUAL(NULL, irps[i]); + //Set the transfer request's target + hcd_xfer_req_set_target(req_hdls[i], *pipe_hdl, irps[i], NULL); + } +} + +static void free_pipe_and_xfer_reqs(hcd_pipe_handle_t pipe_hdl, + hcd_xfer_req_handle_t *req_hdls, + uint8_t **data_buffers, + usb_irp_t **irps, + int num_xfers) +{ + printf("Freeing transfer requets\n"); + //Free transfer requests (and their associated objects such as IRPs and data buffers) + for (int i = 0; i < num_xfers; i++) { + heap_caps_free(irps[i]); + heap_caps_free(data_buffers[i]); + hcd_xfer_req_free(req_hdls[i]); + } + printf("Freeing default pipe\n"); + //Delete the pipe + TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_free(pipe_hdl)); +} + +// ------------------------------------------------ Host Port Tests ---------------------------------------------------- + +/* +Test a port sudden disconnect and port recovery + +Purpose: Test that when sudden disconnection happens on an HCD port, the port will + - Generate the HCD_PORT_EVENT_SUDDEN_DISCONN and be put into the HCD_PORT_STATE_RECOVERY state + - Ongoing transfers requests and pipes are handled correctly + +Procedure: + - Setup HCD, a default pipe, and multiple transfer requests + - Start transfers but immediately trigger a disconnect + - Check that HCD_PORT_EVENT_SUDDEN_DISCONN event is generated + - Check that default pipe is invalid and transfer requests can be dequeued + - Recover the port and try to connect then disconnect again (to make sure the port works port recovery) + - Teardown HCD +*/ +TEST_CASE("Test HCD port sudden disconnect", "[hcd][ignore]") +{ + QueueHandle_t port_evt_queue; + QueueHandle_t pipe_evt_queue; + hcd_port_handle_t port_hdl; + setup(&port_evt_queue, &pipe_evt_queue, &port_hdl); + wait_for_connection(port_hdl, port_evt_queue); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Allocate transfer requests + hcd_pipe_handle_t default_pipe; + hcd_xfer_req_handle_t req_hdls[NUM_XFER_REQS]; + uint8_t *data_buffers[NUM_XFER_REQS]; + usb_irp_t *irps[NUM_XFER_REQS]; + alloc_pipe_and_xfer_reqs(port_hdl, pipe_evt_queue, &default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS); + + //Initialize transfer requests to send a "Get Device Descriptor" request + for (int i = 0; i < NUM_XFER_REQS; i++) { + irps[i]->num_bytes = 64; //1 worst case MPS + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) data_buffers[i], 0, XFER_DATA_MAX_LEN); + irps[i]->data_buffer = data_buffers[i]; + irps[i]->num_iso_packets = 0; + } + + //Enqueue those transfer requests + for (int i = 0; i < NUM_XFER_REQS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i])); + } + phy_force_conn_state(false, 0); //Force disconnected state on PHY + + expect_port_event(port_evt_queue, port_hdl, HCD_PORT_EVENT_SUDDEN_DISCONN); + TEST_ASSERT_EQUAL(HCD_PORT_EVENT_SUDDEN_DISCONN, hcd_port_handle_event(port_hdl)); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_RECOVERY, hcd_port_get_state(port_hdl)); + printf("Sudden disconnect\n"); + + //Handling the disconenction event should have invalidated all pipes. + //Pipe should have received (zero or more HCD_PIPE_EVENT_XFER_REQ_DONE) followed by a HCD_PIPE_EVENT_INVALID (MUST OCCUR) + int num_pipe_events = EVENT_QUEUE_LEN - uxQueueSpacesAvailable(pipe_evt_queue); + for (int i = 0; i < num_pipe_events - 1; i++) { + expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE); + } + expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_INVALID); + TEST_ASSERT_EQUAL(hcd_pipe_get_state(default_pipe), HCD_PIPE_STATE_INVALID); + + //Dequeue transfer requests + for (int i = 0; i < NUM_XFER_REQS; i++) { + hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe); + hcd_pipe_handle_t pipe_hdl; + usb_irp_t *irp; + void *context; + hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context); + TEST_ASSERT_EQUAL(default_pipe, pipe_hdl); + TEST_ASSERT_EQUAL(irps[i], irp); + TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED || irp->status == USB_TRANSFER_STATUS_NO_DEVICE); + TEST_ASSERT_EQUAL(NULL, context); + } + //Free transfer requests + free_pipe_and_xfer_reqs(default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS); + + //Recover the port should return to the to NOT POWERED state + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_recover(port_hdl)); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_NOT_POWERED, hcd_port_get_state(port_hdl)); + + //Recovered port should be able to connect and disconenct again + wait_for_connection(port_hdl, port_evt_queue); + wait_for_disconnection(port_hdl, port_evt_queue, false); + teardown(port_evt_queue, pipe_evt_queue, port_hdl); +} + +/* +Test port suspend and resume with active pipes + +Purpose: + - Test p[ort suspend and resume commands work correctly whilst there are active pipes with ongoing transfers + - When suspending, the pipes should be allowed to finish their current ongoing transfer before the bus is suspended. + - When resuming, pipes with pending transfer should be started after the bus is resumed. + +Procedure: + - Setup HCD, a port, a default pipe, and multiple transfer requests + - Start transfers but immediately suspend the port + - Resume the port + - Check all transfer requests have also be resumed and completed on port resume + - Teardown +*/ +TEST_CASE("Test HCD port suspend and resume", "[hcd][ignore]") +{ + QueueHandle_t port_evt_queue; + QueueHandle_t pipe_evt_queue; + hcd_port_handle_t port_hdl; + setup(&port_evt_queue, &pipe_evt_queue, &port_hdl); + wait_for_connection(port_hdl, port_evt_queue); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Allocate transfer requests + hcd_pipe_handle_t default_pipe; + hcd_xfer_req_handle_t req_hdls[NUM_XFER_REQS]; + uint8_t *data_buffers[NUM_XFER_REQS]; + usb_irp_t *irps[NUM_XFER_REQS]; + alloc_pipe_and_xfer_reqs(port_hdl, pipe_evt_queue, &default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS); + //Initialize transfer requests to send a "Get Device Descriptor" request + for (int i = 0; i < NUM_XFER_REQS; i++) { + irps[i]->num_bytes = 64; //1 worst case MPS + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) data_buffers[i], 0, XFER_DATA_MAX_LEN); + irps[i]->data_buffer = data_buffers[i]; + irps[i]->num_iso_packets = 0; + } + //Enqueue those transfer requests + for (int i = 0; i < NUM_XFER_REQS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i])); + } + //Immediately suspend the bus whilst pies are active + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_SUSPEND)); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_SUSPENDED, hcd_port_get_state(port_hdl)); + printf("Suspended\n"); + vTaskDelay(pdMS_TO_TICKS(100)); + + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_RESUME)); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_ENABLED, hcd_port_get_state(port_hdl)); + vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for resumed transfers to complete + + expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE); + //Dequeue transfer requests + for (int i = 0; i < NUM_XFER_REQS; i++) { + hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe); + hcd_pipe_handle_t pipe_hdl; + usb_irp_t *irp; + void *context; + hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context); + TEST_ASSERT_EQUAL(default_pipe, pipe_hdl); + TEST_ASSERT_EQUAL(irps[i], irp); + TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED); + TEST_ASSERT_EQUAL(NULL, context); + } + + //Free transfer requests + free_pipe_and_xfer_reqs(default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + wait_for_disconnection(port_hdl, port_evt_queue, false); + teardown(port_evt_queue, pipe_evt_queue, port_hdl); +} + +/* +Test HCD port disable with active pipes + +Purpose: + - Test that the port disable command works correctly with active pipes + - Pipes should be to finish their current ongoing transfer before port is disabled + - After disabling the port, all pipes should become invalid. + +Procedure: + - Setup HCD, a default pipe, and multiple transfer requests + - Start transfers but immediately disable the port + - Check pipe received invalid event + - Check that transfer are either done or not executed + - Teardown +*/ +TEST_CASE("Test HCD port disable", "[hcd][ignore]") +{ + QueueHandle_t port_evt_queue; + QueueHandle_t pipe_evt_queue; + hcd_port_handle_t port_hdl; + setup(&port_evt_queue, &pipe_evt_queue, &port_hdl); + wait_for_connection(port_hdl, port_evt_queue); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Allocate transfer requests + hcd_pipe_handle_t default_pipe; + hcd_xfer_req_handle_t req_hdls[NUM_XFER_REQS]; + uint8_t *data_buffers[NUM_XFER_REQS]; + usb_irp_t *irps[NUM_XFER_REQS]; + alloc_pipe_and_xfer_reqs(port_hdl, pipe_evt_queue, &default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS); + //Initialize transfer requests to send a "Get Device Descriptor" request + for (int i = 0; i < NUM_XFER_REQS; i++) { + irps[i]->num_bytes = 64; //1 worst case MPS + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) data_buffers[i], 0, XFER_DATA_MAX_LEN); + irps[i]->data_buffer = data_buffers[i]; + irps[i]->num_iso_packets = 0; + } + //Enqueue those transfer requests + for (int i = 0; i < NUM_XFER_REQS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i])); + } + //Immediately disable port + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_DISABLE)); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_DISABLED, hcd_port_get_state(port_hdl)); + printf("Disabled\n"); + //Pipe should have received (zero or more HCD_PIPE_EVENT_XFER_REQ_DONE) followed by a HCD_PIPE_EVENT_INVALID (MUST OCCUR) + int num_pipe_events = EVENT_QUEUE_LEN - uxQueueSpacesAvailable(pipe_evt_queue); + for (int i = 0; i < num_pipe_events - 1; i++) { + expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE); + } + expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_INVALID); + for (int i = 0; i < NUM_XFER_REQS; i++) { + hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe); + hcd_pipe_handle_t pipe_hdl; + usb_irp_t *irp; + void *context; + hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context); + TEST_ASSERT_EQUAL(default_pipe, pipe_hdl); + TEST_ASSERT_EQUAL(irps[i], irp); + TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED || irp->status == USB_TRANSFER_STATUS_NO_DEVICE); + TEST_ASSERT_EQUAL(NULL, context); + } + + //Free transfer requests + free_pipe_and_xfer_reqs(default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS); + //Already disabled. Disconnect and teardown + wait_for_disconnection(port_hdl, port_evt_queue, true); + teardown(port_evt_queue, pipe_evt_queue, port_hdl); +} + +/* +Test HCD port command bailout + +Purpose: + - Test that if the a port's state changes whilst a command is being executed, the port command should return + ESP_ERR_INVALID_RESPONSE + +Procedure: + - Setup HCD and wait for connection + - Suspend the port + - Resume the port but trigger a disconnect from another thread during the resume command + - Check that port command returns ESP_ERR_INVALID_RESPONSE +*/ +static void concurrent_task(void *arg) +{ + SemaphoreHandle_t sync_sem = (SemaphoreHandle_t) arg; + xSemaphoreTake(sync_sem, portMAX_DELAY); + vTaskDelay(pdMS_TO_TICKS(10)); //Give a short delay let reset command start in main thread + //Forcibly a disconenction + phy_force_conn_state(false, 0); + vTaskDelay(portMAX_DELAY); //Block forever and wait to be deleted +} + +TEST_CASE("Test HCD port command bailout", "[hcd][ignore]") +{ + QueueHandle_t port_evt_queue; + QueueHandle_t pipe_evt_queue; + hcd_port_handle_t port_hdl; + setup(&port_evt_queue, &pipe_evt_queue, &port_hdl); + wait_for_connection(port_hdl, port_evt_queue); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Create task to run commands concurrently + SemaphoreHandle_t sync_sem = xSemaphoreCreateBinary(); + TaskHandle_t task_handle; + TEST_ASSERT_NOT_EQUAL(NULL, sync_sem); + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreatePinnedToCore(concurrent_task, "tsk", 4096, (void *) sync_sem, UNITY_FREERTOS_PRIORITY + 1, &task_handle, 0)); + + //Suspend the device + printf("Suspending\n"); + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_SUSPEND)); + vTaskDelay(pdMS_TO_TICKS(20)); //Short delay for device to enter suspend state + + printf("Attempting to resume\n"); + xSemaphoreGive(sync_sem); //Trigger concurrent task + //Attempt to resume the port. But the concurrent task should override this with a disconnection event + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_RESPONSE, hcd_port_command(port_hdl, HCD_PORT_CMD_RESUME)); + + //Check that concurrent task triggered a sudden disconnection + expect_port_event(port_evt_queue, port_hdl, HCD_PORT_EVENT_SUDDEN_DISCONN); + TEST_ASSERT_EQUAL(HCD_PORT_EVENT_SUDDEN_DISCONN, hcd_port_handle_event(port_hdl)); + TEST_ASSERT_EQUAL(HCD_PORT_STATE_RECOVERY, hcd_port_get_state(port_hdl)); + + vTaskDelay(pdMS_TO_TICKS(10)); //Short delay for concurrent task finish running + vTaskDelete(task_handle); + vSemaphoreDelete(sync_sem); + //Directly teardown the port without recovery + teardown(port_evt_queue, pipe_evt_queue, port_hdl); +} + +// --------------------------------------------------- Pipe Tests ------------------------------------------------------ +/* +Test HCD Transfer Requests (normal completion and early abort) + +Purpose: + - Test that pipes can be created + - Transfer requests can be created and enqueued + - Pipe returns HCD_PIPE_EVENT_XFER_REQ_DONE + - Test that transfer requests can be aborted when enqueued + +Procedure: + - Setup + - Allocate transfer requests. Initialize as Get Device Descriptor request + - Enqueue transfer requests + - Expect HCD_PIPE_EVENT_XFER_REQ_DONE. Deallocate transfer requests + - Requeue transfer requests, but abort them immediately + - Teardown +*/ +TEST_CASE("Test HCD pipe transfer request", "[hcd][ignore]") +{ + QueueHandle_t port_evt_queue; + QueueHandle_t pipe_evt_queue; + hcd_port_handle_t port_hdl; + setup(&port_evt_queue, &pipe_evt_queue, &port_hdl); + wait_for_connection(port_hdl, port_evt_queue); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Allocate transfer requests + hcd_pipe_handle_t default_pipe; + hcd_xfer_req_handle_t req_hdls[NUM_XFER_REQS]; + uint8_t *data_buffers[NUM_XFER_REQS]; + usb_irp_t *irps[NUM_XFER_REQS]; + alloc_pipe_and_xfer_reqs(port_hdl, pipe_evt_queue, &default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS); + + //Initialize transfer requests to send a "Get Device Descriptor" request + for (int i = 0; i < NUM_XFER_REQS; i++) { + irps[i]->num_bytes = 64; //1 worst case MPS + USB_CTRL_REQ_INIT_GET_DEVC_DESC((usb_ctrl_req_t *) data_buffers[i]); + irps[i]->data_buffer = data_buffers[i]; + irps[i]->num_iso_packets = 0; + } + + //Enqueue those transfer requests + for (int i = 0; i < NUM_XFER_REQS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i])); + } + //Wait for each done event of each transfer request + for (int i = 0; i < NUM_XFER_REQS; i++) { + expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE); + } + + //Dequeue transfer requests and check results. + for (int i = 0; i < NUM_XFER_REQS; i++) { + hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe); + TEST_ASSERT_EQUAL(req_hdls[i], req_hdl); + hcd_pipe_handle_t pipe_hdl; + usb_irp_t *irp; + void *context; + hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context); + TEST_ASSERT_EQUAL(default_pipe, pipe_hdl); + TEST_ASSERT_EQUAL(irp, irps[i]); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->status); + TEST_ASSERT_EQUAL(NULL, context); + } + + //Enqueue them again but abort them short after + for (int i = 0; i < NUM_XFER_REQS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i])); + } + for (int i = 0; i < NUM_XFER_REQS; i++) { + hcd_xfer_req_abort(req_hdls[i]); + } + vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for any inflight transfers to complete + + expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE); + //Dequeue transfer rqeuests and check results of aborted transfer request + for (int i = 0; i < NUM_XFER_REQS; i++) { + hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe); + hcd_pipe_handle_t pipe_hdl; + usb_irp_t *irp; + void *context; + hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context); + TEST_ASSERT_EQUAL(default_pipe, pipe_hdl); + //No need to check req_hdl or IRP order as abort will cause them to dequeu out of order + TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED || irp->status == USB_TRANSFER_STATUS_CANCELLED); + TEST_ASSERT_EQUAL(NULL, context); + } + + //Free transfer requests + free_pipe_and_xfer_reqs(default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS); + + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + wait_for_disconnection(port_hdl, port_evt_queue, false); + teardown(port_evt_queue, pipe_evt_queue, port_hdl); +} + +/* +Test HCD pipe STALL condition, abort, and clear + +Purpose: + - Test that a pipe can react to a STALL (i.e., a HCD_PIPE_EVENT_HALTED event) + - The HCD_PIPE_CMD_ABORT can retire all transfer requests + - Pipe clear command can return the pipe to being active + +Procedure: + - Setup HCD and a port, a default pipe, and multiple transfer requests + - Corrupt the first transfer request, then enqueue all of them. + - The corrupted transfer request should trigger a STALL response from the endpoint + - Check that the correct pipe event, error, and state is returned from the pipe + - Check that the other transfers can be retired using the abort command + - Check that the halt can be cleared by using the clear command + - Requeue correct transfers to check that pipe still works after being cleared + - Teardown +*/ +TEST_CASE("Test HCD pipe STALL", "[hcd][ignore]") +{ + QueueHandle_t port_evt_queue; + QueueHandle_t pipe_evt_queue; + hcd_port_handle_t port_hdl; + setup(&port_evt_queue, &pipe_evt_queue, &port_hdl); + wait_for_connection(port_hdl, port_evt_queue); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Allocate transfer requests + hcd_pipe_handle_t default_pipe; + hcd_xfer_req_handle_t req_hdls[NUM_XFER_REQS]; + uint8_t *data_buffers[NUM_XFER_REQS]; + usb_irp_t *irps[NUM_XFER_REQS]; + alloc_pipe_and_xfer_reqs(port_hdl, pipe_evt_queue, &default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS); + //Initialize transfer requests to send a "Get Device Descriptor" request + for (int i = 0; i < NUM_XFER_REQS; i++) { + irps[i]->num_bytes = 64; //1 worst case MPS + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) data_buffers[i], 0, XFER_DATA_MAX_LEN); + irps[i]->data_buffer = data_buffers[i]; + irps[i]->num_iso_packets = 0; + } + //Corrupt first transfer so that it triggers a STALL + ((usb_ctrl_req_t *) data_buffers[0])->bRequest = 0xAA; + //Enqueue those transfer requests + for (int i = 0; i < NUM_XFER_REQS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i])); + } + vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for transfers to complete + + //Check that pipe has been stalled + printf("Expecting STALL\n"); + expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_ERROR_STALL); + TEST_ASSERT_EQUAL(HCD_PIPE_STATE_HALTED, hcd_pipe_get_state(default_pipe)); + //Call the pipe abort command to retire all transfers then dequeue all transfers + TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_command(default_pipe, HCD_PIPE_CMD_ABORT)); + for (int i = 0; i < NUM_XFER_REQS; i++) { + hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe); + hcd_pipe_handle_t pipe_hdl; + usb_irp_t *irp; + void *context; + hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context); + TEST_ASSERT_EQUAL(default_pipe, pipe_hdl); + TEST_ASSERT_EQUAL(irp, irps[i]); + TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_STALL || irp->status == USB_TRANSFER_STATUS_CANCELLED); + TEST_ASSERT_EQUAL(NULL, context); + } + + //Call the clear command to un-stall the pipe + TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_command(default_pipe, HCD_PIPE_CMD_CLEAR)); + TEST_ASSERT_EQUAL(HCD_PIPE_STATE_ACTIVE, hcd_pipe_get_state(default_pipe)); + //Correct first transfer then requeue + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) data_buffers[0], 0, XFER_DATA_MAX_LEN); + for (int i = 0; i < NUM_XFER_REQS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i])); + } + vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for transfers to complete + + expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE); + //Dequeue transfer requests and check results. + for (int i = 0; i < NUM_XFER_REQS; i++) { + hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe); + TEST_ASSERT_EQUAL(req_hdls[i], req_hdl); + hcd_pipe_handle_t pipe_hdl; + usb_irp_t *irp; + void *context; + hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context); + TEST_ASSERT_EQUAL(default_pipe, pipe_hdl); + TEST_ASSERT_EQUAL(irp, irps[i]); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->status); + TEST_ASSERT_EQUAL(NULL, context); + } + + //Free transfer requests + free_pipe_and_xfer_reqs(default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + wait_for_disconnection(port_hdl, port_evt_queue, false); + teardown(port_evt_queue, pipe_evt_queue, port_hdl); +} + +/* +Test Pipe runtime halt and clear + +Purpose: + - Test that a pipe can be halted with a command whilst there are ongoing transfer requests + - Test that a pipe can be un-halted with a HCD_PIPE_CMD_CLEAR + - Test that enqueued transfer requests are resumed when pipe is cleared + +Procedure: + - Setup HCD, a default pipe, and multiple transfer requests + - Enqueue transfer requests but execute a HCD_PIPE_CMD_HALT command immediately after. Halt command should let on + the current going transfer request finish before actually halting the pipe. + - Clear the pipe halt using a HCD_PIPE_CMD_HALT command. Enqueued transfer requests will be resumed + - Check that all transfer requests have completed successfully. + - Teardown +*/ +TEST_CASE("Test HCD pipe runtime halt and clear", "[hcd][ignore]") +{ + QueueHandle_t port_evt_queue; + QueueHandle_t pipe_evt_queue; + hcd_port_handle_t port_hdl; + setup(&port_evt_queue, &pipe_evt_queue, &port_hdl); + wait_for_connection(port_hdl, port_evt_queue); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Allocate transfer requests + hcd_pipe_handle_t default_pipe; + hcd_xfer_req_handle_t req_hdls[NUM_XFER_REQS]; + uint8_t *data_buffers[NUM_XFER_REQS]; + usb_irp_t *irps[NUM_XFER_REQS]; + alloc_pipe_and_xfer_reqs(port_hdl, pipe_evt_queue, &default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS); + + //Initialize transfer requests to send a "Get Device Descriptor" request + for (int i = 0; i < NUM_XFER_REQS; i++) { + irps[i]->num_bytes = 64; //1 worst case MPS + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) data_buffers[i], 0, XFER_DATA_MAX_LEN); + irps[i]->data_buffer = data_buffers[i]; + irps[i]->num_iso_packets = 0; + } + printf("Enqueuing transfer requests\n"); + //Enqueue those transfer requests + for (int i = 0; i < NUM_XFER_REQS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i])); + } + + //Halt the pipe immediately + TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_command(default_pipe, HCD_PIPE_CMD_HALT)); + TEST_ASSERT_EQUAL(HCD_PIPE_STATE_HALTED, hcd_pipe_get_state(default_pipe)); + printf("Pipe halted\n"); + vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for current inflight transfer to complete + //Clear command to un-halt the pipe + TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_command(default_pipe, HCD_PIPE_CMD_CLEAR)); + TEST_ASSERT_EQUAL(HCD_PIPE_STATE_ACTIVE, hcd_pipe_get_state(default_pipe)); + printf("Pipe cleared\n"); + vTaskDelay(pdMS_TO_TICKS(100)); //Give some time pending for transfers to restart and complete + + expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE); + for (int i = 0; i < NUM_XFER_REQS; i++) { + hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe); + TEST_ASSERT_EQUAL(req_hdls[i], req_hdl); + hcd_pipe_handle_t pipe_hdl; + usb_irp_t *irp; + void *context; + hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context); + TEST_ASSERT_EQUAL(default_pipe, pipe_hdl); + TEST_ASSERT_EQUAL(irp, irps[i]); + TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED); + TEST_ASSERT_EQUAL(NULL, context); + } + + //Free transfer requests + free_pipe_and_xfer_reqs(default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + wait_for_disconnection(port_hdl, port_evt_queue, false); + teardown(port_evt_queue, pipe_evt_queue, port_hdl); +}