From 7f421048936c6007186db00ed382cd989c4b905e Mon Sep 17 00:00:00 2001 From: Darian Leung Date: Thu, 11 Mar 2021 20:50:05 +0800 Subject: [PATCH] HCD: Add support for interrupt and isochronous pipes This commit adds support for interrupt and isochronous pipes to the HCD: - HCD now internally uses double buffering - Added test cases for interrupt and isochronous transfers - Reorganized test cases for each transfer type - Updated API comments and maintainer's notes Some minor bugs were also fixed --- .../esp32s2/include/hal/usb_types_private.h | 23 + components/hal/esp32s2/include/hal/usbh_hal.h | 493 ++--- components/hal/esp32s2/include/hal/usbh_ll.h | 111 +- components/hal/esp32s2/usbh_hal.c | 182 +- components/usb/CMakeLists.txt | 4 +- components/usb/hcd.c | 1702 ++++++++++++----- components/usb/maintainers.md | 22 +- components/usb/private_include/hcd.h | 75 +- components/usb/private_include/usb.h | 42 +- components/usb/test/CMakeLists.txt | 4 +- components/usb/test/hcd/test_hcd_bulk.c | 262 +++ components/usb/test/hcd/test_hcd_common.c | 350 ++++ components/usb/test/hcd/test_hcd_common.h | 151 ++ components/usb/test/hcd/test_hcd_ctrl.c | 271 +++ components/usb/test/hcd/test_hcd_intr.c | 159 ++ components/usb/test/hcd/test_hcd_isoc.c | 114 ++ components/usb/test/hcd/test_hcd_port.c | 309 +++ components/usb/test/test_hcd.c | 790 -------- 18 files changed, 3377 insertions(+), 1687 deletions(-) create mode 100644 components/usb/test/hcd/test_hcd_bulk.c create mode 100644 components/usb/test/hcd/test_hcd_common.c create mode 100644 components/usb/test/hcd/test_hcd_common.h create mode 100644 components/usb/test/hcd/test_hcd_ctrl.c create mode 100644 components/usb/test/hcd/test_hcd_intr.c create mode 100644 components/usb/test/hcd/test_hcd_isoc.c create mode 100644 components/usb/test/hcd/test_hcd_port.c delete mode 100644 components/usb/test/test_hcd.c diff --git a/components/hal/esp32s2/include/hal/usb_types_private.h b/components/hal/esp32s2/include/hal/usb_types_private.h index 0a50160526..868fdee589 100644 --- a/components/hal/esp32s2/include/hal/usb_types_private.h +++ b/components/hal/esp32s2/include/hal/usb_types_private.h @@ -44,6 +44,29 @@ typedef enum { USB_PRIV_XFER_TYPE_INTR, } usb_priv_xfer_type_t; +/** + * @brief Enumeration of different possible lengths of the periodic frame list + */ +typedef enum { + USB_HAL_FRAME_LIST_LEN_8 = 8, + USB_HAL_FRAME_LIST_LEN_16 = 16, + USB_HAL_FRAME_LIST_LEN_32 = 32, + USB_HAL_FRAME_LIST_LEN_64 = 64, +} usb_hal_frame_list_len_t; + +/** + * @brief Support intervals in number of USB frames (i.e., 1ms) + */ +typedef enum { + USB_HAL_INTERVAL_1 = 1, + USB_HAL_INTERVAL_2 = 2, + USB_HAL_INTERVAL_4 = 4, + USB_HAL_INTERVAL_8 = 8, + USB_HAL_INTERVAL_16 = 16, + USB_HAL_INTERVAL_32 = 32, + USB_HAL_INTERVAL_64 = 64, +} usb_hal_interval_t; + #ifdef __cplusplus } #endif diff --git a/components/hal/esp32s2/include/hal/usbh_hal.h b/components/hal/esp32s2/include/hal/usbh_hal.h index 17afacd7d2..49ae954c43 100644 --- a/components/hal/esp32s2/include/hal/usbh_hal.h +++ b/components/hal/esp32s2/include/hal/usbh_hal.h @@ -20,7 +20,7 @@ extern "C" { /* NOTE: Thread safety is the responsibility fo the HAL user. All USB Host HAL - functions should be called from critical sections unless specified otherwise + functions must be called from critical sections unless specified otherwise */ #include @@ -30,21 +30,29 @@ NOTE: Thread safety is the responsibility fo the HAL user. All USB Host HAL #include "hal/usbh_ll.h" #include "hal/usb_types_private.h" -/* ----------------------------------------------------------------------------- -------------------------------- Macros and Types ------------------------------- ------------------------------------------------------------------------------ */ +// ------------------------------------------------ Macros and Types --------------------------------------------------- -// ---------------------------- Constants/Configs ------------------------------ +// ------------------ Constants/Configs -------------------- #define USBH_HAL_DMA_MEM_ALIGN 512 +#define USBH_HAL_FRAME_LIST_MEM_ALIGN 512 //The frame list needs to be 512 bytes aligned (contrary to the databook) #define USBH_HAL_NUM_CHAN 8 #define USBH_HAL_XFER_DESC_SIZE (sizeof(usbh_ll_dma_qtd_t)) +#define USBH_HAL_FIFO_TOTAL_USABLE_LINES 200 //Although we have a 256 lines, only 200 lines are usuable due to EPINFO_CTL -// ------------------------------- HAL States ---------------------------------- +/** + * @brief FIFO size configuration structure + */ +typedef struct { + uint32_t rx_fifo_lines; /**< Size of the RX FIFO in terms the number of FIFO lines */ + uint32_t nptx_fifo_lines; /**< Size of the Non-periodic FIFO in terms the number of FIFO lines */ + uint32_t ptx_fifo_lines; /**< Size of the Periodic FIFO in terms the number of FIFO lines */ +} usbh_hal_fifo_config_t; + +// --------------------- HAL States ------------------------ /** * @brief Channel states - * */ typedef enum { USBH_HAL_CHAN_STATE_HALTED = 0, /**< The channel is halted. No transfer descriptor list is being executed */ @@ -52,7 +60,7 @@ typedef enum { USBH_HAL_CHAN_STATE_ERROR, /**< The channel is in the error state */ } usbh_hal_chan_state_t; -// ------------------------------- HAL Events ---------------------------------- +// --------------------- HAL Events ------------------------ /** * @brief Host port HAL events @@ -72,48 +80,46 @@ typedef enum { * @brief Channel events */ typedef enum { - USBH_HAL_CHAN_EVENT_SLOT_DONE, /**< The channel has completed execution of an entire transfer descriptor list. Channel is now halted */ - 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_CPLT, /**< The channel has completed execution of a transfer descriptor that had the USBH_HAL_XFER_DESC_FLAG_HOC flag set. 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_NONE, /**< No event (interrupt ran for internal processing) */ } usbh_hal_chan_event_t; -// ------------------------------- HAL Errors ---------------------------------- +// --------------------- HAL Errors ------------------------ /** * @brief Channel errors */ typedef enum { USBH_HAL_CHAN_ERROR_XCS_XACT = 0, /**< Excessive (three consecutive) transaction errors (e.g., no response, bad CRC etc */ - USBH_HAL_CHAN_ERROR_BNA, /**< Buffer Not Available error (i.e., transfer slot is unfilled */ + USBH_HAL_CHAN_ERROR_BNA, /**< Buffer Not Available error (i.e., An inactive transfer descriptor was fetched by the channel) */ USBH_HAL_CHAN_ERROR_PKT_BBL, /**< Packet babbler error (packet exceeded MPS) */ USBH_HAL_CHAN_ERROR_STALL, /**< STALL response received */ } usbh_hal_chan_error_t; -// ----------------------- Transfer Descriptor Related ------------------------- +// ------------- Transfer Descriptor Related --------------- /** * @brief Flags used to describe the type of transfer descriptor to fill */ -#define USBH_HAL_XFER_DESC_FLAG_IN 0x01 -#define USBH_HAL_XFER_DESC_FLAG_SETUP 0x02 -#define USBH_HAL_XFER_DESC_FLAG_NULL 0x04 -#define USBH_HAL_XFER_DESC_FLAG_HALT 0x08 +#define USBH_HAL_XFER_DESC_FLAG_IN 0x01 /**< Indicates this transfer descriptor is of the IN direction */ +#define USBH_HAL_XFER_DESC_FLAG_SETUP 0x02 /**< Indicates this transfer descriptor is an OUT setup */ +#define USBH_HAL_XFER_DESC_FLAG_HOC 0x04 /**< Indicates that the channel will be halted after this transfer descriptor completes */ /** * @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. + * 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 + * descriptor 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 #define USBH_HAL_XFER_DESC_STS_BUFFER_ERR USBH_LL_QTD_STATUS_BUFFER #define USBH_HAL_XFER_DESC_STS_NOT_EXECUTED USBH_LL_QTD_STATUS_NOT_EXECUTED -// ------------------------------ Object Types --------------------------------- +// -------------------- Object Types ----------------------- /** * @brief Endpoint characteristics structure @@ -131,6 +137,10 @@ typedef struct { }; uint32_t val; }; + struct { + usb_hal_interval_t interval; /**< The interval of the endpoint */ + uint32_t phase_offset_frames; /**< Phase offset in number of frames */ + } periodic; /**< Characteristic for periodic (interrupt/isochronous) endpoints only */ } usbh_hal_ep_char_t; /** @@ -143,29 +153,16 @@ typedef struct { uint32_t active: 1; /**< The channel is enabled */ uint32_t halt_requested: 1; /**< A halt has been requested */ uint32_t error_pending: 1; /**< The channel is waiting for the error to be handled */ + uint32_t reserved: 1; uint32_t chan_idx: 4; /**< The index number of the channel */ - uint32_t reserved25: 25; + uint32_t reserved24: 24; }; uint32_t val; } flags; /**< Flags regarding channel's status and information */ usb_host_chan_regs_t *regs; /**< Pointer to the channel's register set */ usbh_hal_chan_error_t error; /**< The last error that occurred on the channel */ + usb_priv_xfer_type_t type; /**< The transfer type of the channel */ void *chan_ctx; /**< Context variable for the owner of the channel */ - //Transfer Descriptor List Slot - struct { - union { - struct { - 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 */ - uint32_t reserved8: 8; - }; - uint32_t val; - } flags; - void *owner_ctx; /**< Context variable for the owner of the slot */ - usbh_ll_dma_qtd_t *xfer_desc_list; /**< Pointer to transfer descriptor list */ - } slot; } usbh_hal_chan_t; /** @@ -176,10 +173,15 @@ typedef struct { usbh_dev_t *dev; /**< Pointer to base address of DWC_OTG registers */ usb_wrap_dev_t *wrap_dev; /**< Pointer to base address of USB Wrapper registers */ //Host Port related + uint32_t *periodic_frame_list; /**< Pointer to scheduling frame list */ + usb_hal_frame_list_len_t frame_list_len; /**< Length of the periodic scheduling frame list */ union { struct { - uint32_t dbnc_lock_enabled: 1; /**< Debounce lock enabled */ - uint32_t reserved31: 31; + uint32_t dbnc_lock_enabled: 1; /**< Debounce lock enabled */ + uint32_t fifo_sizes_set: 1; /**< Whether the FIFO sizes have been set or not */ + uint32_t periodic_sched_enabled: 1; /**< Periodic scheduling (for interrupt and isochronous transfers) is enabled */ + uint32_t reserved: 5; + uint32_t reserved24: 24; }; uint32_t val; } flags; @@ -191,9 +193,7 @@ typedef struct { } channels; } usbh_hal_context_t; -/* ----------------------------------------------------------------------------- ---------------------------------- Core (Global) -------------------------------- ------------------------------------------------------------------------------ */ +// -------------------------------------------------- Core (Global) ---------------------------------------------------- /** * @brief Initialize the HAL context and check if DWC_OTG is alive @@ -219,7 +219,7 @@ void usbh_hal_init(usbh_hal_context_t *hal); * @brief Deinitialize the HAL context * * Entry: - * - All channels should be properly disabled, and any pending events handled + * - All channels must be properly disabled, and any pending events handled * Exit: * - DWC_OTG global interrupt disabled * - HAL context deinitialized @@ -231,13 +231,9 @@ void usbh_hal_deinit(usbh_hal_context_t *hal); /** * @brief Issue a soft reset to the controller * - * This should be called when the host port encounters an error event or has - * been disconnected. Before calling this, users are responsible for safely - * freeing all channels as a soft reset will wipe all host port nd channel - * registers. - * - * This function will result in the host port being put back into same state as - * after calling usbh_hal_init(). + * This should be called when the host port encounters an error event or has been disconnected. Before calling this, + * users are responsible for safely freeing all channels as a soft reset will wipe all host port and channel registers. + * This function will result in the host port being put back into same state as after calling usbh_hal_init(). * * @note This has nothing to do with a USB bus reset. It simply resets the peripheral * @@ -245,18 +241,35 @@ void usbh_hal_deinit(usbh_hal_context_t *hal); */ void usbh_hal_core_soft_reset(usbh_hal_context_t *hal); -/* ----------------------------------------------------------------------------- ----------------------------------- Host Port ---------------------------------- ------------------------------------------------------------------------------ */ +/** + * @brief Set FIFO sizes + * + * This function will set the sizes of each of the FIFOs (RX FIFO, Non-periodic TX FIFO, Periodic TX FIFO) and must be + * called at least once before allocating the channel. Based on the type of endpoints (and the endpionts' MPS), there + * may be situations where this function may need to be called again to resize the FIFOs. If resizing FIFOs dynamically, + * it is the user's responsibility to ensure there are no active channels when this function is called. + * + * @note The totol size of all the FIFOs must be less than or equal to USBH_HAL_FIFO_TOTAL_USABLE_LINES + * @note After a port reset, the FIFO size registers will reset to their default values, so this function must be called + * again post reset. + * + * @param hal Context of the HAL layer + * @param fifo_config FIFO configuration + */ +void usbh_hal_set_fifo_size(usbh_hal_context_t *hal, const usbh_hal_fifo_config_t *fifo_config); -// ---------------------------- Host Port Control ------------------------------ +// ---------------------------------------------------- Host Port ------------------------------------------------------ + +// ------------------ Host Port Control -------------------- /** - * @brief Enable the host port's interrupt allowing port and channel events to occur + * @brief Initialize the host port + * + * - Will enable the host port's interrupts allowing port and channel events to occur * * @param hal Context of the HAL layer */ -static inline void usbh_hal_port_start(usbh_hal_context_t *hal) +static inline void usbh_hal_port_init(usbh_hal_context_t *hal) { //Configure Host related interrupts usbh_ll_haintmsk_dis_chan_intr(hal->dev, 0xFFFFFFFF); //Disable interrupts for all channels @@ -264,11 +277,13 @@ static inline void usbh_hal_port_start(usbh_hal_context_t *hal) } /** - * @brief Disable the host port's interrupt preventing any further port or channel events + * @brief Deinitialize the host port + * + * - Will disable the host port's interrupts preventing further port aand channel events from ocurring * * @param hal Context of the HAL layer */ -static inline void usbh_hal_port_stop(usbh_hal_context_t *hal) +static inline void usbh_hal_port_deinit(usbh_hal_context_t *hal) { //Disable Host port and channel interrupts usb_ll_dis_intrs(hal->dev, USB_LL_INTR_CORE_PRTINT | USB_LL_INTR_CORE_HCHINT); @@ -298,9 +313,8 @@ static inline void usbh_hal_port_toggle_power(usbh_hal_context_t *hal, bool powe * Exit: * - On release of the reset signal, a USBH_HAL_PORT_EVENT_ENABLED will be generated * - * @note If the host port is already enabled, then issuing a reset will cause - * it be disabled and generate a USBH_HAL_PORT_EVENT_DISABLED event. The - * host port will not be enabled until the reset signal is released (thus + * @note If the host port is already enabled, then issuing a reset will cause it be disabled and generate a + * USBH_HAL_PORT_EVENT_DISABLED event. The host port will not be enabled until the reset signal is released (thus * generating the USBH_HAL_PORT_EVENT_ENABLED event) * * @param hal Context of the HAL layer @@ -353,8 +367,7 @@ static inline void usbh_hal_port_suspend(usbh_hal_context_t *hal) * * Hosts should hold the resume signal for at least 20ms * - * @note If a remote wakeup event occurs, the resume signal is driven - * and cleared automatically. + * @note If a remote wakeup event occurs, the resume signal is driven and cleared automatically. * * @param hal Context of the HAL layer * @param enable Enable/disable resume signal @@ -371,9 +384,8 @@ static inline void usbh_hal_port_toggle_resume(usbh_hal_context_t *hal, bool ena /** * @brief Check whether the resume signal is being driven * - * If a remote wakeup event occurs, the core will automatically drive and clear - * the resume signal for the required amount of time. Call this function to - * check whether the resume signal has completed. + * If a remote wakeup event occurs, the core will automatically drive and clear the resume signal for the required + * amount of time. Call this function to check whether the resume signal has completed. * * @param hal Context of the HAL layer * @return true Resume signal is still being driven @@ -384,18 +396,89 @@ static inline bool usbh_hal_port_check_resume(usbh_hal_context_t *hal) return usbh_ll_hprt_get_port_resume(hal->dev); } -// -------------------------- Host Port Status/State --------------------------- +// ---------------- Host Port Scheduling ------------------- + +/** + * @brief Sets the periodic scheduling frame list + * + * @note This function must be called before attempting configuring any channels to be period via + * usbh_hal_chan_set_ep_char() + * + * @param hal Context of the HAL layer + * @param frame_list Base address of the frame list + * @param frame_list_len Number of entries in the frame list (can only be 8, 16, 32, 64) + */ +static inline void usbh_hal_port_set_frame_list(usbh_hal_context_t *hal, uint32_t *frame_list, usb_hal_frame_list_len_t len) +{ + assert(!hal->flags.periodic_sched_enabled); + //Clear and save frame list + hal->periodic_frame_list = frame_list; + hal->frame_list_len = len; +} + +/** + * @brief Get the pointer to the periodic scheduling frame list + * + * @param hal Context of the HAL layer + * @return uint32_t* Base address of the periodic scheduling frame list + */ +static inline uint32_t *usbh_hal_port_get_frame_list(usbh_hal_context_t *hal) +{ + return hal->periodic_frame_list; +} + +/** + * @brief Enable periodic scheduling + * + * @note The periodic frame list must be set via usbh_hal_port_set_frame_list() should be set before calling this + * function + * @note This function must be called before activating any periodic channels + * + * @param hal Context of the HAL layer + */ +static inline void usbh_hal_port_periodic_enable(usbh_hal_context_t *hal) +{ + assert(hal->periodic_frame_list != NULL && !hal->flags.periodic_sched_enabled); + usbh_ll_set_frame_list_base_addr(hal->dev, (uint32_t)hal->periodic_frame_list); + usbh_ll_hcfg_set_num_frame_list_entries(hal->dev, hal->frame_list_len); + usbh_ll_hcfg_en_perio_sched(hal->dev); + hal->flags.periodic_sched_enabled = 1; +} + +/** + * @brief Disable periodic scheduling + * + * Disabling periodic scheduling will save a bit of DMA bandwith (as the controller will no longer fetch the schedule + * from the frame list). + * + * @note Before disabling periodic scheduling, it is the user's responsibility to ensure that all periodic channels have + * halted safely. + * + * @param hal Context of the HAL layer + */ +static inline void usbh_hal_port_periodic_disable(usbh_hal_context_t *hal) +{ + assert(hal->flags.periodic_sched_enabled); + usbh_ll_hcfg_dis_perio_sched(hal->dev); + hal->flags.periodic_sched_enabled = 0; +} + +static inline uint32_t usbh_hal_port_get_cur_frame_num(usbh_hal_context_t *hal) +{ + return usbh_ll_get_frm_num(hal->dev); +} + +// --------------- Host Port Status/State ------------------ /** * @brief Check if a device is currently connected to the host port * - * This function is intended to be called after one of the following events - * followed by an adequate debounce delay + * This function is intended to be called after one of the following events followed by an adequate debounce delay * - USBH_HAL_PORT_EVENT_CONN * - USBH_HAL_PORT_EVENT_DISCONN * - * @note No other connection/disconnection event will occur again until the - * debounce lock is disabled via usbh_hal_disable_debounce_lock() + * @note No other connection/disconnection event will occur again until the debounce lock is disabled via + * usbh_hal_disable_debounce_lock() * * @param hal Context of the HAL layer * @return true A device is connected to the host port @@ -409,8 +492,7 @@ static inline bool usbh_hal_port_check_if_connected(usbh_hal_context_t *hal) /** * @brief Check the speed (LS/FS) of the device connected to the host port * - * @note This function should only be called after confirming that a device is - * connected to the host port + * @note This function should only be called after confirming that a device is connected to the host port * * @param hal Context of the HAL layer * @return usb_priv_speed_t Speed of the connected device (FS or LS only on the esp32-s2) @@ -423,9 +505,8 @@ static inline usb_priv_speed_t usbh_hal_port_get_conn_speed(usbh_hal_context_t * /** * @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. Any pending - * connection or disconenction interrupts are cleared. + * This function must be called after calling usbh_hal_port_check_if_connected() and will allow connection/disconnection + * events to occur again. Any pending connection or disconenction interrupts are cleared. * * @param hal Context of the HAL layer */ @@ -439,11 +520,9 @@ static inline void usbh_hal_disable_debounce_lock(usbh_hal_context_t *hal) usb_ll_en_intrs(hal->dev, USB_LL_INTR_CORE_PRTINT | USB_LL_INTR_CORE_DISCONNINT); } -/* ----------------------------------------------------------------------------- ------------------------------------ Channel ------------------------------------ -------------------------------------------------------------------------------*/ +// ----------------------------------------------------- Channel ------------------------------------------------------- -// --------------------------- Channel Allocation ------------------------------ +// ----------------- Channel Allocation -------------------- /** * @brief Allocate a channel @@ -464,6 +543,8 @@ bool usbh_hal_chan_alloc(usbh_hal_context_t *hal, usbh_hal_chan_t *chan_obj, voi */ void usbh_hal_chan_free(usbh_hal_context_t *hal, usbh_hal_chan_t *chan_obj); +// ---------------- Channel Configuration ------------------ + /** * @brief Get the context variable of the channel * @@ -475,8 +556,6 @@ static inline void *usbh_hal_chan_get_context(usbh_hal_chan_t *chan_obj) return chan_obj->chan_ctx; } -// ---------------------------- Channel Control -------------------------------- - /** * @brief Get the current state of a channel * @@ -502,10 +581,11 @@ static inline usbh_hal_chan_state_t usbh_hal_chan_get_state(usbh_hal_chan_t *cha * @note the channel must be in the disabled state in order to change its EP * information * + * @param hal Context of the HAL layer * @param chan_obj Channel object * @param ep_char Endpoint characteristics */ -void usbh_hal_chan_set_ep_char(usbh_hal_chan_t *chan_obj, usbh_hal_ep_char_t *ep_char); +void usbh_hal_chan_set_ep_char(usbh_hal_context_t *hal, usbh_hal_chan_t *chan_obj, usbh_hal_ep_char_t *ep_char); /** * @brief Set the direction of the channel @@ -514,8 +594,7 @@ void usbh_hal_chan_set_ep_char(usbh_hal_chan_t *chan_obj, usbh_hal_ep_char_t *ep * needing to reconfigure all of the channel's EP info. This is used primarily * for control transfers. * - * @note This function should only be called when the channel is in the disabled - * state or is halted from a USBH_HAL_CHAN_EVENT_SLOT_HALT event + * @note This function should only be called when the channel is halted * * @param chan_obj Channel object * @param is_in Whether the direction is IN @@ -563,6 +642,51 @@ static inline uint32_t usbh_hal_chan_get_pid(usbh_hal_chan_t *chan_obj) return usbh_ll_chan_get_pid(chan_obj->regs); } +// ------------------- Channel Control --------------------- + +/** + * @brief Activate a channel + * + * Activating a channel will cause the channel to start executing transfer descriptors. + * + * @note This function should only be called on channels that were previously halted + * @note An event will be generated when the channel is halted + * + * @param chan_obj Channel object + * @param xfer_desc_list A filled transfer descriptor list + * @param desc_list_len Transfer descriptor list length + * @param start_idx Index of the starting transfer descriptor in the list + */ +void usbh_hal_chan_activate(usbh_hal_chan_t *chan_obj, void *xfer_desc_list, int desc_list_len, int start_idx); + +/** + * @brief Get the index of the current transfer descriptor + * + * @param chan_obj Channel object + * @return int Descriptor index + */ +static inline int usbh_hal_chan_get_qtd_idx(usbh_hal_chan_t *chan_obj) +{ + return usbh_ll_chan_get_ctd(chan_obj->regs); +} + +/** + * @brief Request to halt a channel + * + * This function should be called in order to halt a channel. If the channel is already halted, this function will + * return true. If the channel is still 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); + /** * @brief Get a channel's error * @@ -587,178 +711,74 @@ static inline void usbh_hal_chan_clear_error(usbh_hal_chan_t *chan_obj) chan_obj->flags.error_pending = 0; } -/* ----------------------------------------------------------------------------- --------------------------- Transfer Descriptor List ---------------------------- -------------------------------------------------------------------------------*/ +// -------------------------------------------- Transfer Descriptor List ----------------------------------------------- /** * @brief Fill a single entry in a transfer descriptor list * - * - A single entry corresponds to a USB transfer in a particular direction - * (e.g., a BULK OUT). - * - The channel will automatically split the transfer into multiple MPS sized - * packets of the endpoint. - * - For multi direction transfers (such as the various stages of a control transfer), - * the direction and PID of channel must be managed manually. Set the - * USBH_HAL_XFER_DESC_FLAG_HALT flag to halt on each entry to flip the direction - * and PID of the channel. - * - For IN transfer entries, set the USBH_HAL_XFER_DESC_FLAG_IN. The transfer - * size must also be an integer multiple of the endpoint's MPS + * - Depending on the transfer type, a single transfer descriptor may corresponds + * - A stage of a transfer (for control transfers) + * - A frame of a transfer interval (for interrupt and isoc) + * - An entire transfer (for bulk transfers) + * - Check the various USBH_HAL_XFER_DESC_FLAG_ flags for filling a specific type of descriptor + * - For IN transfer entries, set the USBH_HAL_XFER_DESC_FLAG_IN. The transfer size must also be an integer multiple of + * the endpoint's MPS * - * @note The USBH_HAL_XFER_DESC_FLAG_HALT must be set on the last descriptor of - * the list so that an interrupt is generated at the end of the list - * @note The USBH_HAL_XFER_DESC_FLAG_HALT can be set on every descriptor if users - * prefer to manually step through the list (such as change EP directions in between) * @note Critical section is not required for this function * - * @param xfer_desc_list Transfer descriptor list - * @param xfer_desc_idx Transfer descriptor index + * @param desc_list Transfer descriptor list + * @param desc_idx Transfer descriptor index * @param xfer_data_buff Transfer data buffer * @param xfer_len Transfer length * @param flags Transfer flags */ -static inline void usbh_hal_xfer_desc_fill(void *xfer_desc_list, int xfer_desc_idx, uint8_t *xfer_data_buff, int xfer_len, uint32_t flags) +static inline void usbh_hal_xfer_desc_fill(void *desc_list, uint32_t desc_idx, uint8_t *xfer_data_buff, int xfer_len, uint32_t flags) { - //Check if the channel should be halted on completion of this xfer descriptor - bool halt_on_xfer_cplt = flags & USBH_HAL_XFER_DESC_FLAG_HALT; - usbh_ll_dma_qtd_t *qtd_list = (usbh_ll_dma_qtd_t *)xfer_desc_list; - if (flags & USBH_HAL_XFER_DESC_FLAG_NULL) { - usbh_ll_set_qtd_null(&qtd_list[xfer_desc_idx]); - } else if (flags & USBH_HAL_XFER_DESC_FLAG_IN) { - usbh_ll_set_qtd_in(&qtd_list[xfer_desc_idx], xfer_data_buff, xfer_len, halt_on_xfer_cplt); + usbh_ll_dma_qtd_t *qtd_list = (usbh_ll_dma_qtd_t *)desc_list; + if (flags & USBH_HAL_XFER_DESC_FLAG_IN) { + usbh_ll_set_qtd_in(&qtd_list[desc_idx], + xfer_data_buff, xfer_len, + flags & USBH_HAL_XFER_DESC_FLAG_HOC); } else { - usbh_ll_set_qtd_out(&qtd_list[xfer_desc_idx], xfer_data_buff, xfer_len, halt_on_xfer_cplt, (flags & USBH_HAL_XFER_DESC_FLAG_SETUP)); + usbh_ll_set_qtd_out(&qtd_list[desc_idx], + xfer_data_buff, + xfer_len, + flags & USBH_HAL_XFER_DESC_FLAG_HOC, + flags & USBH_HAL_XFER_DESC_FLAG_SETUP); } } /** - * @brief Parse a transfer decriptors results + * @brief Clear a transfer descriptor (sets all its fields to NULL) * - * @param xfer_desc_list Transfer descriptor list - * @param xfer_desc_idx Transfer descriptor index + * @param desc_list Transfer descriptor list + * @param desc_idx Transfer descriptor index + */ +static inline void usbh_hal_xfer_desc_clear(void *desc_list, uint32_t desc_idx) +{ + usbh_ll_dma_qtd_t *qtd_list = (usbh_ll_dma_qtd_t *)desc_list; + usbh_ll_set_qtd_null(&qtd_list[desc_idx]); +} + +/** + * @brief Parse a transfer decriptor's results + * + * @param desc_list Transfer descriptor list + * @param desc_idx Transfer descriptor index * @param[out] xfer_rem_len Remaining length of the transfer in bytes * @param[out] xfer_status Status of the transfer * * @note Critical section is not required for this function */ -static inline void usbh_hal_xfer_desc_parse(void *xfer_desc_list, int xfer_desc_idx, int *xfer_rem_len, int *xfer_status) +static inline void usbh_hal_xfer_desc_parse(void *desc_list, uint32_t desc_idx, int *xfer_rem_len, int *xfer_status) { - usbh_ll_dma_qtd_t *qtd_list = (usbh_ll_dma_qtd_t *)xfer_desc_list; - usbh_ll_get_qtd_status(&qtd_list[xfer_desc_idx], xfer_rem_len, xfer_status); + usbh_ll_dma_qtd_t *qtd_list = (usbh_ll_dma_qtd_t *)desc_list; + usbh_ll_get_qtd_status(&qtd_list[desc_idx], xfer_rem_len, xfer_status); + //Clear the QTD to prevent it from being read again + usbh_ll_set_qtd_null(&qtd_list[desc_idx]); } -/* ----------------------------------------------------------------------------- --------------------------------- Channel Slot ---------------------------------- -------------------------------------------------------------------------------*/ - -/** - * @brief Acquire a slot - * - * Acquiring a channel's transfer descriptor list slot will cause a give ownership - * of the channel to the acquirer. The transfer descriptor list to be executed - * when the channel is activated. - * - * @param chan_obj Channel object - * @param xfer_desc_list A filled transfer descriptor list - * @param desc_list_len Length of the descriptor list - * @param owner_ctx Context variable of the owner - */ -static inline void usbh_hal_chan_slot_acquire(usbh_hal_chan_t *chan_obj, void *xfer_desc_list, int desc_list_len, void *owner_ctx) -{ - assert(!chan_obj->slot.flags.slot_acquired); - chan_obj->slot.xfer_desc_list = (usbh_ll_dma_qtd_t *)xfer_desc_list; - 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 = 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); -} - -/** - * @brief Get current owner of a slot - * - * This function reqturns a slot's context variable that was set when the slot - * was acquired - * - * @param chan_obj Channel object - * @return void* Context variable of the owner of the slot - */ -static inline void *usbh_hal_chan_slot_get_owner(usbh_hal_chan_t *chan_obj) -{ - assert(chan_obj->slot.flags.slot_acquired); - return chan_obj->slot.owner_ctx; -} - -/** - * @brief Release a slot - * - * @note This should only be called after confirming that the transfer descriptor - * list has completed execution. - * @note Users should parse the completed transfer descriptor list to check the - * results of each transfer. - * - * @param[in] chan_obj Channel object - * @param[out] xfer_desc_list A completed transfer descriptor list - * @param[out] desc_list_len Length of the descriptor list - */ -static inline void usbh_hal_chan_slot_release(usbh_hal_chan_t *chan_obj, void **xfer_desc_list, int *desc_list_len) -{ - 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 = 0; -} - -/** - * @brief Activate a channel - * - * Activating a channel will cause it to start executing the transfer descriptor - * list in its slot starting from its next descriptor index. When a transfer - * descriptor completes execution and has the HALT flag set, an event will be - * generated. - * - * @param chan_obj Channel object - * @param num_to_skip Number of transfer descriptors to skip over - */ -void usbh_hal_chan_activate(usbh_hal_chan_t *chan_obj, int num_to_skip); - -/** - * @brief Get next transfer descriptor index - * - * This function returns the index of the next descriptor that will be executed - * in the transfer descriptor list. - * - * @param chan_obj Channel object - * @return int Descriptor index - */ -static inline int usbh_hal_chan_get_next_desc_index(usbh_hal_chan_t *chan_obj) -{ - return chan_obj->slot.flags.cur_qtd_idx; -} - -/** - * @brief Request to halt a channel - * - * This function should be called in order to halt a channel. If the channel is - * already halted, this function will return true. If the channel is still - * 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_slot_request_halt(usbh_hal_chan_t *chan_obj); - -/* ----------------------------------------------------------------------------- --------------------------------- Event Handling -------------------------------- ------------------------------------------------------------------------------ */ +// ------------------------------------------------- Event Handling ---------------------------------------------------- /** * @brief Decode global and host port interrupts @@ -776,13 +796,8 @@ usbh_hal_port_event_t usbh_hal_decode_intr(usbh_hal_context_t *hal); /** * @brief Gets the next channel with a pending interrupt * - * If no channel is pending an interrupt, this function will return NULL. If one - * or more channels are pending an interrupt, this function returns one of the - * channel's objects. Call this function repeatedly until it returns NULL. - * - * @note If a channel error event occurs, or a Slot halt/done event occurs, the - * channel is immediately halted and no further channel interrupt or errors - * can occur until it is reactivated. + * If no channel is pending an interrupt, this function will return NULL. If one or more channels are pending an + * interrupt, this function returns one of the channel's objects. Call this function repeatedly until it returns NULL. * * @param hal Context of the HAL layer * @return usbh_hal_chan_t* Channel object. NULL if no channel are pending an interrupt. diff --git a/components/hal/esp32s2/include/hal/usbh_ll.h b/components/hal/esp32s2/include/hal/usbh_ll.h index 6e7a9a0695..dcab42c0a4 100644 --- a/components/hal/esp32s2/include/hal/usbh_ll.h +++ b/components/hal/esp32s2/include/hal/usbh_ll.h @@ -159,16 +159,6 @@ typedef struct { uint8_t *buffer; } usbh_ll_dma_qtd_t; -/* - * Enumeration of different possible lengths of the periodic frame list - */ -typedef enum { - USBH_LL_FRAME_LIST_LEN_8 = 0, - USBH_LL_FRAME_LIST_LEN_16, - USBH_LL_FRAME_LIST_LEN_32, - USBH_LL_FRAME_LIST_LEN_64, -} usbh_ll_frame_list_len_t; - /* ----------------------------------------------------------------------------- ------------------------------ USB Wrap Registers ------------------------------ ----------------------------------------------------------------------------- */ @@ -249,18 +239,33 @@ static inline bool usb_ll_check_dma_req_in_progress(usbh_dev_t *hw) return hw->grstctl_reg.dmareq; } -static inline void usb_ll_flush_tx_fifo(usbh_dev_t *hw, uint32_t chan_num) +static inline void usb_ll_flush_nptx_fifo(usbh_dev_t *hw) { - usb_grstctl_reg_t grstctl; - grstctl.val = hw->grstctl_reg.val; - grstctl.txfnum = chan_num; //Set channel number to flush - grstctl.txfflsh = 1; //Flush that channel's TX FIFO - hw->grstctl_reg.val = grstctl.val; + hw->grstctl_reg.txfnum = 0; //Set the TX FIFO number to 0 to select the non-periodic TX FIFO + hw->grstctl_reg.txfflsh = 1; //Flush the selected TX FIFO + //Wait for the flushing to complete + while (hw->grstctl_reg.txfflsh) { + ; + } +} + +static inline void usb_ll_flush_ptx_fifo(usbh_dev_t *hw) +{ + hw->grstctl_reg.txfnum = 1; //Set the TX FIFO number to 1 to select the periodic TX FIFO + hw->grstctl_reg.txfflsh = 1; //FLush the select TX FIFO + //Wait for the flushing to complete + while (hw->grstctl_reg.txfflsh) { + ; + } } static inline void usb_ll_flush_rx_fifo(usbh_dev_t *hw) { hw->grstctl_reg.rxfflsh = 1; + //Wait for the flushing to complete + while (hw->grstctl_reg.rxfflsh) { + ; + } } static inline void usb_ll_reset_frame_counter(usbh_dev_t *hw) @@ -320,20 +325,20 @@ static inline void usb_ll_dis_intrs(usbh_dev_t *hw, uint32_t intr_mask) // --------------------------- GRXFSIZ Register -------------------------------- -static inline void usb_ll_set_rx_fifo_size(usbh_dev_t *hw, uint32_t size) +static inline void usb_ll_set_rx_fifo_size(usbh_dev_t *hw, uint32_t num_lines) { //Set size in words - hw->grxfsiz_reg.rxfdep = size; + hw->grxfsiz_reg.rxfdep = num_lines; } // -------------------------- GNPTXFSIZ Register ------------------------------- -static inline void usb_ll_set_nptx_fifo_size(usbh_dev_t *hw, uint32_t addr, uint32_t size) +static inline void usb_ll_set_nptx_fifo_size(usbh_dev_t *hw, uint32_t addr, uint32_t num_lines) { usb_gnptxfsiz_reg_t gnptxfsiz; gnptxfsiz.val = hw->gnptxfsiz_reg.val; gnptxfsiz.nptxfstaddr = addr; - gnptxfsiz.nptxfdep = size; + gnptxfsiz.nptxfdep = num_lines; hw->gnptxfsiz_reg.val = gnptxfsiz.val; } @@ -364,12 +369,12 @@ static inline void usb_ll_get_hardware_config(usbh_dev_t *hw, uint32_t *ghwcfg1, // --------------------------- HPTXFSIZ Register ------------------------------- -static inline void usbh_ll_set_ptx_fifo_size(usbh_dev_t *hw, uint32_t addr, uint32_t size) +static inline void usbh_ll_set_ptx_fifo_size(usbh_dev_t *hw, uint32_t addr, uint32_t num_lines) { usb_hptxfsiz_reg_t hptxfsiz; hptxfsiz.val = hw->hptxfsiz_reg.val; hptxfsiz.ptxfstaddr = addr; - hptxfsiz.ptxfsize = size; + hptxfsiz.ptxfsize = num_lines; hw->hptxfsiz_reg.val = hptxfsiz.val; } @@ -394,9 +399,24 @@ static inline void usbh_ll_hcfg_dis_perio_sched(usbh_dev_t *hw) * * @param num_entires Number of entires in the frame list */ -static inline void usbh_ll_hcfg_set_num_frame_list_entries(usbh_dev_t *hw, usbh_ll_frame_list_len_t num_entries) +static inline void usbh_ll_hcfg_set_num_frame_list_entries(usbh_dev_t *hw, usb_hal_frame_list_len_t num_entries) { - hw->hcfg_reg.frlisten = num_entries; + uint32_t frlisten; + switch (num_entries) { + case USB_HAL_FRAME_LIST_LEN_8: + frlisten = 0; + break; + case USB_HAL_FRAME_LIST_LEN_16: + frlisten = 1; + break; + case USB_HAL_FRAME_LIST_LEN_32: + frlisten = 2; + break; + default: //USB_HAL_FRAME_LIST_LEN_64 + frlisten = 3; + break; + } + hw->hcfg_reg.frlisten = frlisten; } static inline void usbh_ll_hcfg_en_scatt_gatt_dma(usbh_dev_t *hw) @@ -417,7 +437,8 @@ 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 with scatter/gather DMA * - * @param hw + * @param hw Start address of the USB Wrap registers + * @param speed Speed to initialize the host port at */ static inline void usbh_ll_hcfg_set_defaults(usbh_dev_t *hw, usb_priv_speed_t speed) { @@ -498,11 +519,27 @@ static inline void usbh_ll_haintmsk_dis_chan_intr(usbh_dev_t *hw, uint32_t mask) // --------------------------- HFLBAddr Register ------------------------------- +/** + * @brief Set the base address of the scheduling frame list + * + * @note For some reason, this address must be 512 bytes aligned or else a bunch of frames will not be scheduled when + * the frame list rolls over. However, according to the databook, there is no mention of the HFLBAddr needing to + * be aligned. + * + * @param hw Start address of the DWC_OTG registers + * @param addr Base address of the scheduling frame list + */ static inline void usbh_ll_set_frame_list_base_addr(usbh_dev_t *hw, uint32_t addr) { hw->hflbaddr_reg.hflbaddr = addr; } +/** + * @brief Get the base address of the scheduling frame list + * + * @param hw Start address of the DWC_OTG registers + * @return uint32_t Base address of the scheduling frame list + */ static inline uint32_t usbh_ll_get_frame_list_base_addr(usbh_dev_t *hw) { return hw->hflbaddr_reg.hflbaddr; @@ -529,6 +566,7 @@ static inline uint32_t usbh_ll_hprt_get_test_ctl(usbh_dev_t *hw) { return hw->hprt_reg.prttstctl; } + static inline void usbh_ll_hprt_set_test_ctl(usbh_dev_t *hw, uint32_t test_mode) { usb_hprt_reg_t hprt; @@ -604,6 +642,7 @@ static inline bool usbh_ll_hprt_get_port_resume(usbh_dev_t *hw) { return hw->hprt_reg.prtres; } + static inline bool usbh_ll_hprt_get_port_overcur(usbh_dev_t *hw) { return hw->hprt_reg.prtovrcurract; @@ -780,9 +819,7 @@ static inline void usbh_ll_chan_set_dma_addr_non_iso(volatile usb_host_chan_regs static inline void usbh_ll_chan_set_dma_addr_iso(volatile usb_host_chan_regs_t *chan, void *dmaaddr, - uint32_t ntd, - uint32_t pktcnt, - uint32_t ctd) + uint32_t ntd) { int n; if (ntd == 2) { @@ -861,15 +898,15 @@ static inline usb_host_chan_regs_t *usbh_ll_get_chan_regs(usbh_dev_t *dev, int c * @param data_buff Pointer to buffer containing the data to transfer * @param xfer_len Number of bytes in transfer. Setting 0 will do a zero length IN transfer. * Non zero length must be mulitple of the endpoint's MPS. - * @param halt_on_cplt Generate a channel halted interrupt on completion of QTD + * @param hoc Halt on complete (will generate an interrupt and halt the channel) */ -static inline void usbh_ll_set_qtd_in(usbh_ll_dma_qtd_t *qtd, uint8_t *data_buff, int xfer_len, bool halt_on_cplt) +static inline void usbh_ll_set_qtd_in(usbh_ll_dma_qtd_t *qtd, uint8_t *data_buff, int xfer_len, bool hoc) { qtd->buffer = data_buff; //Set pointer to data buffer qtd->buffer_status_val = 0; //Reset all flags to zero qtd->in_non_iso.xfer_size = xfer_len; - if (halt_on_cplt) { - qtd->in_non_iso.intr_cplt = 1; //Used to indicate successful completion + if (hoc) { + qtd->in_non_iso.intr_cplt = 1; //We need to set this to distinguish between a halt due to a QTD qtd->in_non_iso.eol = 1; //Used to halt the channel at this qtd } qtd->in_non_iso.active = 1; @@ -882,11 +919,11 @@ static inline void usbh_ll_set_qtd_in(usbh_ll_dma_qtd_t *qtd, uint8_t *data_buff * @param data_buff Pointer to buffer containing the data to transfer * @param xfer_len Number of bytes to transfer. Setting 0 will do a zero length transfer. * For ctrl setup packets, this should be set to 8. - * @param halt_on_cplt Generate a channel halted interrupt on completion of QTD. + * @param hoc Halt on complete (will generate an interrupt) * @param is_setup Indicates whether this is a control transfer setup packet or a normal OUT Data transfer. * (As per the USB protocol, setup packets cannot be STALLd or NAKd by the device) */ -static inline void usbh_ll_set_qtd_out(usbh_ll_dma_qtd_t *qtd, uint8_t *data_buff, int xfer_len, bool halt_on_cplt, bool is_setup) +static inline void usbh_ll_set_qtd_out(usbh_ll_dma_qtd_t *qtd, uint8_t *data_buff, int xfer_len, bool hoc, bool is_setup) { qtd->buffer = data_buff; //Set pointer to data buffer qtd->buffer_status_val = 0; //Reset all flags to zero @@ -894,9 +931,9 @@ static inline void usbh_ll_set_qtd_out(usbh_ll_dma_qtd_t *qtd, uint8_t *data_buf if (is_setup) { qtd->out_non_iso.is_setup = 1; } - if (halt_on_cplt) { - qtd->out_non_iso.intr_cplt = 1; //Used to indicate successful completion - qtd->out_non_iso.eol = 1; //Used to halt the channel at this qtd + if (hoc) { + qtd->in_non_iso.intr_cplt = 1; //We need to set this to distinguish between a halt due to a QTD + qtd->in_non_iso.eol = 1; //Used to halt the channel at this qtd } qtd->out_non_iso.active = 1; } diff --git a/components/hal/esp32s2/usbh_hal.c b/components/hal/esp32s2/usbh_hal.c index a24f965ef4..8093a7b09a 100644 --- a/components/hal/esp32s2/usbh_hal.c +++ b/components/hal/esp32s2/usbh_hal.c @@ -19,11 +19,9 @@ #include "hal/usbh_hal.h" #include "hal/usbh_ll.h" -/* ----------------------------------------------------------------------------- -------------------------------- Macros and Types ------------------------------- ------------------------------------------------------------------------------ */ +// ------------------------------------------------ Macros and Types --------------------------------------------------- -// -------------------------------- Constants ---------------------------------- +// ---------------------- Constants ------------------------ #define BENDPOINTADDRESS_NUM_MSK 0x0F //Endpoint number mask of the bEndpointAddress field of an endpoint descriptor #define BENDPOINTADDRESS_DIR_MSK 0x80 //Endpoint direction mask of the bEndpointAddress field of an endpoint descriptor @@ -34,32 +32,7 @@ #define CORE_REG_GHWCFG3 0x00C804B5 #define CORE_REG_GHWCFG4 0xD3F0A030 -// ------------------------------ Configurable --------------------------------- - -#define CHAN_MAX_SLOTS 16 - -/* -FIFO lengths configured as follows: - -RXFIFO (Receive FIFO) - - Recommended: (((LPS/4) + 2) * NUM_PACKETS) + (NUM_CHAN * 2) + (NUM_BULK_CTRL * 1) - - Actual: Assume (LPS = 64), (NUM_CHAN = 8), (NUM_BULK_CTRL = 8): -NPTXFIFO (Non-periodic TX FIFO) - - Recommended: (((LPS/4) + 2) * 2) Fit two largest packet sizes (and each packets overhead info) - - Actual: Assume LPS is 64 (is the MPS for CTRL/BULK/INTR in FS) -PTXFIFO (Periodic TX FIFO) - - Recommended: ((LPS/4) + 2) * NUM_PACKETS - - Actual: Assume a single LPS of 64 (quarter of ISO MPS), then 2 packets worth of overhead -REGFIFO (Register storage) - - Recommended: 4 * NUM_CHAN - - Actual: Assume NUM_CHAN is 8 -*/ -#define HW_FIFO_LEN 256 -#define RX_FIFO_LEN 92 -#define NPTX_FIFO_LEN 36 -#define PTX_FIFO_LEN 72 -#define REG_FIFO_LEN 32 -_Static_assert((RX_FIFO_LEN + NPTX_FIFO_LEN + PTX_FIFO_LEN + REG_FIFO_LEN) <= HW_FIFO_LEN, "Sum of FIFO lengths not equal to HW_FIFO_LEN"); +// -------------------- Configurable ----------------------- /** * The following core interrupts will be enabled (listed LSB to MSB). Some of these @@ -114,18 +87,14 @@ _Static_assert((RX_FIFO_LEN + NPTX_FIFO_LEN + PTX_FIFO_LEN + REG_FIFO_LEN) <= HW USBH_LL_INTR_CHAN_BNAINTR | \ USBH_LL_INTR_CHAN_XCS_XACT_ERR) -/* ----------------------------------------------------------------------------- ---------------------------------- Core (Global) -------------------------------- ------------------------------------------------------------------------------ */ - -// ---------------------------- Private Functions ------------------------------ +// -------------------------------------------------- Core (Global) ---------------------------------------------------- static void set_defaults(usbh_hal_context_t *hal) { usbh_ll_internal_phy_conf(hal->wrap_dev); //Enable and configure internal PHY //GAHBCFG register usb_ll_en_dma_mode(hal->dev); - usb_ll_set_hbstlen(hal->dev, 0); //INCR16 AHB burst length + usb_ll_set_hbstlen(hal->dev, 1); //Use INCR AHB burst. MUST DO SO IN ESP32-S2 DUE TO ARBITER ERRATA. //GUSBCFG register usb_ll_dis_hnp_cap(hal->dev); //Disable HNP usb_ll_dis_srp_cap(hal->dev); //Disable SRP @@ -138,16 +107,13 @@ static void set_defaults(usbh_hal_context_t *hal) usb_ll_set_host_mode(hal->dev); } -// ---------------------------- Public Functions ------------------------------- - void usbh_hal_init(usbh_hal_context_t *hal) { //Check if a peripheral is alive by reading the core ID registers usbh_dev_t *dev = &USBH; -#ifndef NDEBUG uint32_t core_id = usb_ll_get_controller_core_id(dev); assert(core_id == CORE_REG_GSNPSID); -#endif + (void) core_id; //Suppress unused variable warning if asserts are disabled //Initialize HAL context memset(hal, 0, sizeof(usbh_hal_context_t)); hal->dev = dev; @@ -177,15 +143,34 @@ void usbh_hal_core_soft_reset(usbh_hal_context_t *hal) //Set the default bits set_defaults(hal); //Clear all the flags and channels + hal->periodic_frame_list = NULL; 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); } -/* ----------------------------------------------------------------------------- ----------------------------------- Host Port ---------------------------------- ------------------------------------------------------------------------------ */ +void usbh_hal_set_fifo_size(usbh_hal_context_t *hal, const usbh_hal_fifo_config_t *fifo_config) +{ + assert((fifo_config->rx_fifo_lines + fifo_config->nptx_fifo_lines + fifo_config->ptx_fifo_lines) <= USBH_HAL_FIFO_TOTAL_USABLE_LINES); + //Check that none of the channels are active + for (int i = 0; i < USBH_HAL_NUM_CHAN; i++) { + if (hal->channels.hdls[i] != NULL) { + assert(!hal->channels.hdls[i]->flags.active); + } + } + //Set the new FIFO lengths + usb_ll_set_rx_fifo_size(hal->dev, fifo_config->rx_fifo_lines); + usb_ll_set_nptx_fifo_size(hal->dev, fifo_config->rx_fifo_lines, fifo_config->nptx_fifo_lines); + usbh_ll_set_ptx_fifo_size(hal->dev, fifo_config->rx_fifo_lines + fifo_config->nptx_fifo_lines, fifo_config->ptx_fifo_lines); + //Flush the FIFOs + usb_ll_flush_nptx_fifo(hal->dev); + usb_ll_flush_ptx_fifo(hal->dev); + usb_ll_flush_rx_fifo(hal->dev); + hal->flags.fifo_sizes_set = 1; +} + +// ---------------------------------------------------- Host Port ------------------------------------------------------ static inline void debounce_lock_enable(usbh_hal_context_t *hal) { @@ -199,24 +184,17 @@ void usbh_hal_port_enable(usbh_hal_context_t *hal) usb_priv_speed_t speed = usbh_ll_hprt_get_speed(hal->dev); //Host Configuration 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, 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); - usbh_ll_set_ptx_fifo_size(hal->dev, RX_FIFO_LEN + NPTX_FIFO_LEN, PTX_FIFO_LEN); } -/* ----------------------------------------------------------------------------- ------------------------------------ Channel ------------------------------------ -------------------------------------------------------------------------------*/ +// ----------------------------------------------------- Channel ------------------------------------------------------- -// --------------------------- Channel Allocation ------------------------------ +// ----------------- Channel Allocation -------------------- -//Allocate a channel bool usbh_hal_chan_alloc(usbh_hal_context_t *hal, usbh_hal_chan_t *chan_obj, void *chan_ctx) { + assert(hal->flags.fifo_sizes_set); //FIFO sizes should be set befor attempting to allocate a channel //Attempt to allocate channel if (hal->channels.num_allocd == USBH_HAL_NUM_CHAN) { return false; //Out of free channels @@ -246,22 +224,25 @@ bool usbh_hal_chan_alloc(usbh_hal_context_t *hal, usbh_hal_chan_t *chan_obj, voi return true; } -//Returns object memory void usbh_hal_chan_free(usbh_hal_context_t *hal, usbh_hal_chan_t *chan_obj) { + if (chan_obj->type == USB_PRIV_XFER_TYPE_INTR || chan_obj->type == USB_PRIV_XFER_TYPE_ISOCHRONOUS) { + //Unschedule this channel + for (int i = 0; i < hal->frame_list_len; i++) { + hal->periodic_frame_list[i] &= ~(1 << chan_obj->flags.chan_idx); + } + } //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); + assert(!chan_obj->flags.active && !chan_obj->flags.error_pending); //Deallocate channel hal->channels.hdls[chan_obj->flags.chan_idx] = NULL; hal->channels.num_allocd--; assert(hal->channels.num_allocd >= 0); } -// ---------------------------- Channel Control -------------------------------- +// ---------------- Channel Configuration ------------------ -void usbh_hal_chan_set_ep_char(usbh_hal_chan_t *chan_obj, usbh_hal_ep_char_t *ep_char) +void usbh_hal_chan_set_ep_char(usbh_hal_context_t *hal, usbh_hal_chan_t *chan_obj, usbh_hal_ep_char_t *ep_char) { //Cannot change ep_char whilst channel is still active or in error assert(!chan_obj->flags.active && !chan_obj->flags.error_pending); @@ -273,29 +254,34 @@ void usbh_hal_chan_set_ep_char(usbh_hal_chan_t *chan_obj, usbh_hal_ep_char_t *ep ep_char->type, ep_char->bEndpointAddress & BENDPOINTADDRESS_DIR_MSK, ep_char->ls_via_fs_hub); + //Save channel type + chan_obj->type = ep_char->type; + //If this is a periodic endpoint/channel, set its schedule in the frame list + if (ep_char->type == USB_PRIV_XFER_TYPE_ISOCHRONOUS || ep_char->type == USB_PRIV_XFER_TYPE_INTR) { + assert((int)ep_char->periodic.interval <= (int)hal->frame_list_len); //Interval cannot exceed the length of the frame list + //Find the effective offset in the frame list (in case the phase_offset_frames > interval) + int offset = ep_char->periodic.phase_offset_frames % ep_char->periodic.interval; + //Schedule the channel in the frame list + for (int i = offset; i < hal->frame_list_len; i+= ep_char->periodic.interval) { + hal->periodic_frame_list[i] |= 1 << chan_obj->flags.chan_idx; + } + } } -/* ----------------------------------------------------------------------------- -------------------------------- Transfers Slots -------------------------------- -------------------------------------------------------------------------------*/ +// ------------------- Channel Control --------------------- -void usbh_hal_chan_activate(usbh_hal_chan_t *chan_obj, int num_to_skip) +void usbh_hal_chan_activate(usbh_hal_chan_t *chan_obj, void *xfer_desc_list, int desc_list_len, int start_idx) { - //Cannot enable a channel that has already been enabled or is pending error handling + //Cannot activate a channel that has already been enabled or is pending error handling assert(!chan_obj->flags.active && !chan_obj->flags.error_pending); - assert(chan_obj->slot.flags.slot_acquired); - //Update the descriptor list index and check if it's within bounds - chan_obj->slot.flags.cur_qtd_idx += num_to_skip; - assert(chan_obj->slot.flags.cur_qtd_idx < chan_obj->slot.flags.qtd_list_len); - chan_obj->flags.active = 1; - //Set start address of the QTD list and starting QTD index - usbh_ll_chan_set_dma_addr_non_iso(chan_obj->regs, chan_obj->slot.xfer_desc_list, chan_obj->slot.flags.cur_qtd_idx); - //Start the channel - usbh_ll_chan_start(chan_obj->regs); + usbh_ll_chan_set_dma_addr_non_iso(chan_obj->regs, xfer_desc_list, start_idx); + usbh_ll_chan_set_qtd_list_len(chan_obj->regs, desc_list_len); + usbh_ll_chan_start(chan_obj->regs); //Start the channel + chan_obj->flags.active = 1; } -bool usbh_hal_chan_slot_request_halt(usbh_hal_chan_t *chan_obj) +bool usbh_hal_chan_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); @@ -307,9 +293,7 @@ bool usbh_hal_chan_slot_request_halt(usbh_hal_chan_t *chan_obj) return true; } -/* ----------------------------------------------------------------------------- --------------------------------- Event Handling -------------------------------- ------------------------------------------------------------------------------ */ +// ------------------------------------------------- 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) @@ -386,12 +370,9 @@ 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, 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 + + if (chan_intrs & CHAN_INTRS_ERROR_MSK) { //Note: Errors are uncommon, so we check against the entire interrupt mask to reduce frequency of entering this call path + assert(chan_intrs & USBH_LL_INTR_CHAN_CHHLTD); //An error should have halted the channel //Store the error in hal context usbh_hal_chan_error_t error; if (chan_intrs & USBH_LL_INTR_CHAN_STALL) { @@ -405,25 +386,34 @@ usbh_hal_chan_event_t usbh_hal_chan_decode_intr(usbh_hal_chan_t *chan_obj) } //Update flags chan_obj->error = error; + chan_obj->flags.active = 0; chan_obj->flags.error_pending = 1; //Save the error to be handled later chan_event = USBH_HAL_CHAN_EVENT_ERROR; - } else if (chan_obj->flags.halt_requested) { //A halt was previously requested and has not been fulfilled - chan_obj->flags.halt_requested = 0; - chan_event = USBH_HAL_CHAN_EVENT_HALT_REQ; - } else if (chan_intrs & USBH_LL_INTR_CHAN_XFERCOMPL) { - int cur_qtd_idx = usbh_ll_chan_get_ctd(chan_obj->regs); - //Store current qtd index - chan_obj->slot.flags.cur_qtd_idx = cur_qtd_idx; - if (cur_qtd_idx == 0) { - //If the transfer descriptor list has completed, the CTD index should be 0 (wrapped around) - chan_event = USBH_HAL_CHAN_EVENT_SLOT_DONE; + } else if (chan_intrs & USBH_LL_INTR_CHAN_CHHLTD) { + if (chan_obj->flags.halt_requested) { + chan_obj->flags.halt_requested = 0; + chan_event = USBH_HAL_CHAN_EVENT_HALT_REQ; } else { - chan_event = USBH_HAL_CHAN_EVENT_SLOT_HALT; + //Must have been halted due to QTD HOC + chan_event = USBH_HAL_CHAN_EVENT_CPLT; } + chan_obj->flags.active = 0; + } else if (chan_intrs & USBH_LL_INTR_CHAN_XFERCOMPL) { + /* + A transfer complete interrupt WITHOUT the channel halting only occurs when receiving a short interrupt IN packet + and the underlying QTD does not have the HOC bit set. This signifies the last packet of the Interrupt transfer + as all interrupt packets must MPS sized except the last. + */ + //The channel isn't halted yet, so we need to halt it manually to stop the execution of the next QTD/packet + usbh_ll_chan_halt(chan_obj->regs); + /* + After setting the halt bit, this will generate another channel halted interrupt. We treat this interrupt as + a NONE event, then cycle back with the channel halted interrupt to handle the CPLT event. + */ + chan_event = USBH_HAL_CHAN_EVENT_NONE; } else { - //Should never reach this point - abort(); + abort(); //Should never reach this point } return chan_event; } diff --git a/components/usb/CMakeLists.txt b/components/usb/CMakeLists.txt index cfaf84d4e1..8d22022222 100644 --- a/components/usb/CMakeLists.txt +++ b/components/usb/CMakeLists.txt @@ -7,6 +7,4 @@ endif() idf_component_register(SRCS "hcd.c" INCLUDE_DIRS "" - PRIV_INCLUDE_DIRS "private_include" - PRIV_REQUIRES "hal" - REQUIRES "") + PRIV_INCLUDE_DIRS "private_include") diff --git a/components/usb/hcd.c b/components/usb/hcd.c index 4a2541f842..8e2f17c5f7 100644 --- a/components/usb/hcd.c +++ b/components/usb/hcd.c @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include #include #include #include "freertos/FreeRTOS.h" @@ -34,29 +35,117 @@ // --------------------- Constants ------------------------- +#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. + +// ----------------------- Configs ------------------------- + +typedef struct { + int in_mps; + int non_periodic_out_mps; + int periodic_out_mps; +} fifo_mps_limits_t; + /** - * @brief Number of transfer descriptors per transfer for various transfer types + * @brief Default FIFO sizes (see 2.1.2.4 for programming guide) * - * 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 + * RXFIFO + * - Recommended: ((LPS/4) * 2) + 2 + * - Actual: Whatever leftover size: USBH_HAL_FIFO_TOTAL_USABLE_LINES(200) - 48 - 48 = 104 + * - Worst case can accommodate two packets of 204 bytes, or one packet of 408 + * NPTXFIFO + * - Recommended: (LPS/4) * 2 + * - Actual: Assume LPS is 64, and 3 packets: (64/4) * 3 = 48 + * - Worst case can accommodate three packets of 64 bytes or one packet of 192 + * PTXFIFO + * - Recommended: (LPS/4) * 2 + * - Actual: Assume LPS is 64, and 3 packets: (64/4) * 3 = 48 + * - Worst case can accommodate three packets of 64 bytes or one packet of 192 */ -#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 +const usbh_hal_fifo_config_t fifo_config_default = { + .rx_fifo_lines = 104, + .nptx_fifo_lines = 48, + .ptx_fifo_lines = 48, +}; -#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 +const fifo_mps_limits_t mps_limits_default = { + .in_mps = 408, + .non_periodic_out_mps = 192, + .periodic_out_mps = 192, +}; -#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 +/** + * @brief FIFO sizes that bias to giving RX FIFO more capacity + * + * RXFIFO + * - Recommended: ((LPS/4) * 2) + 2 + * - Actual: Whatever leftover size: USBH_HAL_FIFO_TOTAL_USABLE_LINES(200) - 32 - 16 = 152 + * - Worst case can accommodate two packets of 300 bytes or one packet of 600 bytes + * NPTXFIFO + * - Recommended: (LPS/4) * 2 + * - Actual: Assume LPS is 64, and 1 packets: (64/4) * 1 = 16 + * - Worst case can accommodate one packet of 64 bytes + * PTXFIFO + * - Recommended: (LPS/4) * 2 + * - Actual: Assume LPS is 64, and 3 packets: (64/4) * 2 = 32 + * - Worst case can accommodate two packets of 64 bytes or one packet of 128 + */ +const usbh_hal_fifo_config_t fifo_config_bias_rx = { + .rx_fifo_lines = 152, + .nptx_fifo_lines = 16, + .ptx_fifo_lines = 32, +}; -#define NUM_PORTS 1 //The controller only has one port. +const fifo_mps_limits_t mps_limits_bias_rx = { + .in_mps = 600, + .non_periodic_out_mps = 64, + .periodic_out_mps = 128, +}; + +/** + * @brief FIFO sizes that bias to giving Periodic TX FIFO more capacity (i.e., ISOC OUT) + * + * RXFIFO + * - Recommended: ((LPS/4) * 2) + 2 + * - Actual: Assume LPS is 64, and 2 packets: ((64/4) * 2) + 2 = 34 + * - Worst case can accommodate two packets of 64 bytes or one packet of 128 + * NPTXFIFO + * - Recommended: (LPS/4) * 2 + * - Actual: Assume LPS is 64, and 1 packets: (64/4) * 1 = 16 + * - Worst case can accommodate one packet of 64 bytes + * PTXFIFO + * - Recommended: (LPS/4) * 2 + * - Actual: Whatever leftover size: USBH_HAL_FIFO_TOTAL_USABLE_LINES(200) - 34 - 16 = 150 + * - Worst case can accommodate two packets of 300 bytes or one packet of 600 bytes + */ +const usbh_hal_fifo_config_t fifo_config_bias_ptx = { + .rx_fifo_lines = 34, + .nptx_fifo_lines = 16, + .ptx_fifo_lines = 150, +}; + +const fifo_mps_limits_t mps_limits_bias_ptx = { + .in_mps = 128, + .non_periodic_out_mps = 64, + .periodic_out_mps = 600, +}; + +#define FRAME_LIST_LEN USB_HAL_FRAME_LIST_LEN_32 +#define NUM_BUFFERS 2 + +#define XFER_LIST_LEN_CTRL 3 //One descriptor for each stage +#define XFER_LIST_LEN_BULK 2 //One descriptor for transfer, one to support an extra zero length packet +#define XFER_LIST_LEN_INTR 32 +#define XFER_LIST_LEN_ISOC FRAME_LIST_LEN //Same length as the frame list makes it easier to schedule. Must be power of 2 // ------------------------ Flags -------------------------- @@ -66,21 +155,20 @@ * The IRP object has a reserved_flags member for host stack's internal use. The following flags will be set in * reserved_flags in order to keep track of state of an IRP within the HCD. */ -#define IRP_STATE_IDLE 0x0 //The IRP is not enqueued in an HCD pipe -#define IRP_STATE_PENDING 0x1 //The IRP is enqueued and pending execution -#define IRP_STATE_INFLIGHT 0x2 //The IRP is currently in flight -#define IRP_STATE_DONE 0x3 //The IRP has completed execution or is retired, and is waiting to be dequeued -#define IRP_STATE_MASK 0x3 //Bit mask of all the IRP state flags +#define IRP_STATE_IDLE 0x0 //The IRP is not enqueued in an HCD pipe +#define IRP_STATE_PENDING 0x1 //The IRP is enqueued and pending execution +#define IRP_STATE_INFLIGHT 0x2 //The IRP is currently in flight +#define IRP_STATE_DONE 0x3 //The IRP has completed execution or is retired, and is waiting to be dequeued +#define IRP_STATE_MASK 0x3 //Bit mask of all the IRP state flags #define IRP_STATE_SET(reserved_flags, state) (reserved_flags = (reserved_flags & ~IRP_STATE_MASK) | state) #define IRP_STATE_GET(reserved_flags) (reserved_flags & IRP_STATE_MASK) - // -------------------- 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_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)) { \ @@ -99,6 +187,49 @@ typedef struct pipe_obj pipe_t; typedef struct port_obj port_t; +/** + * @brief Object representing a single buffer of a pipe's multi buffer implementation + */ +typedef struct { + void *xfer_desc_list; + usb_irp_t *irp; + union { + struct { + uint32_t data_stg_in: 1; //Data stage of the control transfer is IN + uint32_t data_stg_skip: 1; //Control transfer has no data stage + uint32_t cur_stg: 2; //Index of the current stage (e.g., 0 is setup stage, 2 is status stage) + uint32_t reserved28: 28; + } ctrl; //Control transfer related + struct { + uint32_t zero_len_packet: 1; //Bulk transfer should add a zero length packet at the end regardless + uint32_t reserved31: 31; + } bulk; //Bulk transfer related + struct { + uint32_t num_qtds: 8; //Number of transfer descriptors filled + uint32_t reserved24: 24; + } intr; //Interrupt transfer related + struct { + uint32_t num_qtds: 8; //Number of transfer descriptors filled (including NULL descriptors) + uint32_t interval: 8; //Interval (in number of SOF i.e., ms) + uint32_t irp_start_idx: 8; //Index of the first transfer descriptor in the list + uint32_t next_irp_start_idx: 8; //Index for the first descriptor of the next buffer + } isoc; + uint32_t val; + } flags; + union { + struct { + uint32_t stop_idx: 8; //The descriptor index when the channel was halted + uint32_t executing: 1; //The buffer is currently executing + uint32_t error_occurred: 1; //An error occurred + uint32_t cancelled: 1; //The buffer was actively cancelled + uint32_t reserved5: 5; + hcd_pipe_state_t pipe_state: 8; //The pipe's state when the error occurred + hcd_pipe_event_t pipe_event: 8; //The pipe event when the error occurred + }; + uint32_t val; + } status_flags; //Status flags for the buffer +} dma_buffer_block_t; + /** * @brief Object representing a pipe in the HCD layer */ @@ -108,15 +239,29 @@ struct pipe_obj { TAILQ_HEAD(tailhead_irp_done, usb_irp_obj) done_irp_tailq; int num_irp_pending; int num_irp_done; - usb_irp_t *inflight_irp; //Pointer to the in-flight IRP (i.e., the IRP currently being executed). 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; + //Multi-buffer control + dma_buffer_block_t *buffers[NUM_BUFFERS]; //Double buffering scheme + union { + struct { + uint32_t buffer_num_to_fill: 2; //Number of buffers that can be filled + uint32_t buffer_num_to_exec: 2; //Number of buffers that are filled and need to be executed + uint32_t buffer_num_to_parse: 2;//Number of buffers completed execution and waiting to be parsed + uint32_t reserved2: 2; + uint32_t wr_idx: 1; //Index of the next buffer to fill. Bit width must allow NUM_BUFFERS to wrap automatically + uint32_t rd_idx: 1; //Index of the current buffer in-flight. Bit width must allow NUM_BUFFERS to wrap automatically + uint32_t fr_idx: 1; //Index of the next buffer to parse. Bit width must allow NUM_BUFFERS to wrap automatically + uint32_t buffer_is_executing: 1;//One of the buffers is in flight + uint32_t reserved20: 20; + }; + uint32_t val; + } multi_buffer_control; + //HAL related usbh_hal_chan_t *chan_obj; usbh_hal_ep_char_t ep_char; - //Pipe status, state, and events + //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 + //Pipe status/state/events related hcd_pipe_state_t state; hcd_pipe_event_t last_event; TaskHandle_t task_waiting_pipe_notif; //Task handle used for internal pipe events @@ -125,15 +270,11 @@ struct pipe_obj { 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 is_active: 1; + uint32_t reserved28: 28; }; uint32_t val; - } flags; + } cs_flags; //Pipe callback and context hcd_pipe_isr_callback_t callback; void *callback_arg; @@ -145,6 +286,7 @@ struct pipe_obj { */ struct port_obj { usbh_hal_context_t *hal; + void *frame_list; //Pipes routed through this port TAILQ_HEAD(tailhead_pipes_idle, pipe_obj) pipes_idle_tailq; TAILQ_HEAD(tailhead_pipes_queued, pipe_obj) pipes_active_tailq; @@ -154,21 +296,23 @@ struct port_obj { 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 + 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 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 conn_devc_ena: 1; //Used to indicate the port is connected to a device that has been reset + uint32_t periodic_scheduling_enabled: 1; + uint32_t reserved9: 9; uint32_t num_pipes_waiting_pause: 16; }; uint32_t val; } flags; bool initialized; + hcd_port_fifo_bias_t fifo_bias; //Port callback and context hcd_port_isr_callback_t callback; void *callback_arg; @@ -190,46 +334,213 @@ static hcd_obj_t *s_hcd_obj = NULL; //Note: "s_" is for the static pointer // ------------------------------------------------- Forward Declare --------------------------------------------------- -// ----------------------- Events -------------------------- +// ------------------- Buffer Control ---------------------- /** - * @brief Wait for an internal event from a port + * @brief Check if an inactive buffer can be filled with a pending IRP * - * @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 + * @param pipe Pipe object + * @return true There are one or more pending IRPs, and the inactive buffer is yet to be filled + * @return false Otherwise */ -static void _internal_port_event_wait(port_t *port); +static inline bool _buffer_can_fill(pipe_t *pipe) +{ + //We can only fill if there are pending IRPs and at least one unfilled buffer + if (pipe->num_irp_pending > 0 && pipe->multi_buffer_control.buffer_num_to_fill > 0) { + return true; + } else { + return false; + } +} /** - * @brief Notify (from an ISR context) the thread/task waiting for the internal port event + * @brief Fill an empty buffer with * - * @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 + * This function will: + * - Remove an IRP from the pending tailq + * - Fill that IRP into the inactive buffer * - * @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) + * @note _buffer_can_fill() must return true before calling this function * * @param pipe Pipe object */ -static void _internal_pipe_event_wait(pipe_t *pipe); +static void _buffer_fill(pipe_t *pipe); /** - * @brief Notify (from an ISR context) the thread/task waiting for an internal pipe event + * @brief Check if there are more filled buffers than can be executed * * @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 + * @return true There are more filled buffers to be executed + * @return false No more buffers to execute */ -static bool _internal_pipe_event_notify(pipe_t *pipe, bool from_isr); +static inline bool _buffer_can_exec(pipe_t *pipe) +{ + //We can only execute if there is not already a buffer executing and if there are filled buffers awaiting execution + if (!pipe->multi_buffer_control.buffer_is_executing && pipe->multi_buffer_control.buffer_num_to_exec > 0) { + return true; + } else { + return false; + } +} + +/** + * @brief Execute the next filled buffer + * + * - Must have called _buffer_can_exec() before calling this function + * - Will start the execution of the buffer + * + * @param pipe Pipe object + */ +static void _buffer_exec(pipe_t *pipe); + +/** + * @brief Check if a buffer as completed execution + * + * This should only be called after receiving a USBH_HAL_CHAN_EVENT_CPLT event to check if a buffer is actually + * done. Buffers that aren't complete (such as Control transfers) will be continued automatically. + * + * @param pipe Pipe object + * @return true Buffer complete + * @return false Buffer not complete + */ +static bool _buffer_check_done(pipe_t *pipe); + +/** + * @brief Marks the last executed buffer as complete + * + * This should be called on a pipe that has confirmed that a buffer is completed via _buffer_check_done() + * + * @param pipe Pipe object + * @param stop_idx Descriptor index when the buffer stopped execution + */ +static inline void _buffer_done(pipe_t *pipe, int stop_idx) +{ + //Store the stop_idx for later parsing + dma_buffer_block_t *buffer_done = pipe->buffers[pipe->multi_buffer_control.rd_idx]; + buffer_done->status_flags.executing = 0; + buffer_done->status_flags.error_occurred = 0; + buffer_done->status_flags.stop_idx = stop_idx; + pipe->multi_buffer_control.rd_idx++; + pipe->multi_buffer_control.buffer_num_to_exec--; + pipe->multi_buffer_control.buffer_num_to_parse++; + pipe->multi_buffer_control.buffer_is_executing = 0; +} + +/** + * @brief Marks the last executed buffer as complete due to an error + * + * This should be called on a pipe that has received a USBH_HAL_CHAN_EVENT_ERROR event + * + * @param pipe Pipe object + * @param stop_idx Descriptor index when the buffer stopped execution + * @param pipe_state State of the pipe after the error + * @param pipe_event Error event + * @param cancelled Whether the pipe stopped due to cancellation + */ +static inline void _buffer_done_error(pipe_t *pipe, int stop_idx, hcd_pipe_state_t pipe_state, hcd_pipe_event_t pipe_event, bool cancelled) +{ + //Mark the buffer as erroneous for later parsing + dma_buffer_block_t *buffer_done = pipe->buffers[pipe->multi_buffer_control.rd_idx]; + buffer_done->status_flags.executing = 0; + buffer_done->status_flags.error_occurred = 1; + buffer_done->status_flags.cancelled = cancelled; + buffer_done->status_flags.stop_idx = stop_idx; + buffer_done->status_flags.pipe_state = pipe_state; + buffer_done->status_flags.pipe_event = pipe_event; + pipe->multi_buffer_control.rd_idx++; + pipe->multi_buffer_control.buffer_num_to_exec--; + pipe->multi_buffer_control.buffer_num_to_parse++; + pipe->multi_buffer_control.buffer_is_executing = 0; +} + +/** + * @brief Checks if a pipe has one or more completed buffers to parse + * + * @param pipe Pipe object + * @return true There are one or more buffers to parse + * @return false There are no more buffers to parse + */ +static inline bool _buffer_can_parse(pipe_t *pipe) +{ + if (pipe->multi_buffer_control.buffer_num_to_parse > 0) { + return true; + } else { + return false; + } +} + +/** + * @brief Parse a completed buffer + * + * This function will: + * - Parse the results of an IRP from a completed buffer + * - Put the IRP into the done tailq + * + * @note This function should only be called on the completion of a buffer + * + * @param pipe Pipe object + * @param stop_idx (For INTR pipes only) The index of the descriptor that follows the last descriptor of the IRP. Set to 0 otherwise + */ +static void _buffer_parse(pipe_t *pipe); + +/** + * @brief Marks all buffers pending execution as completed, then parses those buffers + * + * @note This should only be called on pipes do not have any currently executing buffers. + * + * @param pipe Pipe object + * @param cancelled Whether this flush is due to cancellation + */ +static void _buffer_flush_all(pipe_t *pipe, bool cancelled); + +// ------------------------ Pipe --------------------------- + +/** + * @brief Wait until a pipe's in-flight IRP is done + * + * If the pipe has an in-flight IRP, this function will block until it is done (via a internal pipe event). + * If the pipe has no in-flight IRP, this function do nothing and return immediately. + * If the pipe's state changes unexpectedly, this function will return false. + * + * Also parses all buffers on exit + * + * @note This function is blocking (will exit and re-enter the critical section to do so) + * + * @param pipe Pipe object + * @return true Pipes in-flight IRP is done + * @return false Pipes state unexpectedly changed + */ +static bool _pipe_wait_done(pipe_t *pipe); + +/** + * @brief Retires all IRPs (those that were previously in-flight or pending) + * + * Retiring all IRPs will result in any pending IRP being moved to the done tailq. This function will update the IPR + * status of each IRP. + * - If the retiring is self-initiated (i.e., due to a pipe command), the IRP status will be set to USB_TRANSFER_STATUS_CANCELED. + * - 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 in-flight IRP (must already be parsed and returned to done queue) + * - All buffers must be parsed + * Exit: + * - If there was an in-flight IRP, it is parsed and returned to the done queue + * - If there are any pending IRPs: + * - 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 IRP 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); // ------------------------ Port --------------------------- @@ -364,119 +675,46 @@ static bool _port_disable(port_t *port); */ static bool _port_debounce(port_t *port); -// ------------------------ Pipe --------------------------- +// ----------------------- Events -------------------------- /** - * @brief Get the next pending IRP from the pending tailq + * @brief Wait for an internal event from a port * - * Entry: - * - The in-flight IRP must be set to NULL (indicating the pipe currently has no in-flight IRP) - * Exit: - * - If (num_irp_pending > 0), the first IRP is removed from pending_irp_tailq and and - * inflight_irp is set to that IRP. - * - If there are no more queued IRPs, inflight_irp is left as NULL + * @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 pipe Pipe object - * @return true A pending IRP is now set as the in-flight IRP - * @return false No more pending IRPs + * @param port Port object */ -static bool _pipe_get_next_irp(pipe_t *pipe); +static void _internal_port_event_wait(port_t *port); /** - * @brief Return the pipe's current IRP (inflight_irp) to the done tailq + * @brief Notify (from an ISR context) the thread/task waiting for the internal port event * - * Entry: - * - The inflight_irp must already have been parsed (i.e., results have been checked) - * Exit: - * - The IRP is returned to the done tailq and inflight_irp is set to NULL - * - * @param pipe Pipe object + * @param port Port object + * @return true A yield is required + * @return false Whether a yield is required or not */ -static void _pipe_return_cur_irp(pipe_t *pipe); +static bool _internal_port_event_notify_from_isr(port_t *port); /** - * @brief Wait until a pipe's in-flight IRP is done - * - * If the pipe has an in-flight IRP, this function will block until it is done (via a internal pipe event). - * If the pipe has no in-flight IRP, this function do nothing and return immediately. - * If the pipe's state changes unexpectedely, this function will return false. + * @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 - * @return true Pipes in-flight IRP is done - * @return false Pipes state unexpectedly changed */ -static bool _pipe_wait_done(pipe_t *pipe); +static void _internal_pipe_event_wait(pipe_t *pipe); /** - * @brief Retires all IRPs (those that were previously in-flight or pending) - * - * Retiring all IRPs will result in any pending IRP being moved to the done tailq. This - * function will update the IPR status of each IRP. - * - 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 in-flight IRP (must already be parsed and returned to done queue) - * Exit: - * - If there was an in-flight IRP, it is parsed and returned to the done queue - * - If there are any pending IRPs: - * - They are moved to the done tailq + * @brief Notify (from an ISR context) the thread/task waiting for an internal pipe event * * @param pipe Pipe object - * @param cancelled Are we actively Pipe retire is initialized by the user due to a command, thus IRP are - * actively cancelled. + * @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 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 Descriptors ------------------ - -/** - * @brief Fill the inflight_irp into the pipe's transfer descriptor list - * - * Entry: - * - The pipe's inflight_irp must be set to the next IRP - * Exit: - * - inflight_irp 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_irp is already set to the next IRP - */ -static void _xfer_desc_list_fill(pipe_t *pipe); - -/** - * @brief Continue the execution of the transfer descriptor list - * - * @note This is currently only used for control transfers - * - * @param pipe Pipe object - */ -static void _xfer_desc_list_continue(pipe_t *pipe); - -/** - * @brief Parse the pipe's transfer descriptor list to fill the result of the transfers into the pipe's IRP - * - * 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 IRP as they will used to determine the result - * of the IRP - * Exit: - * - The pipe's inflight_irp is filled with result of the IRP (i.e., the underlying IRP has its status set) - * - * @param pipe Pipe object - * @param error_occurred Are we parsing after the pipe had an error (or has become invalid) - */ -static void _xfer_desc_list_parse(pipe_t *pipe, bool error_occurred); +static bool _internal_pipe_event_notify(pipe_t *pipe, bool from_isr); // ----------------------------------------------- Interrupt Handling -------------------------------------------------- @@ -632,42 +870,30 @@ static hcd_pipe_event_t _intr_hdlr_chan(pipe_t *pipe, usbh_hal_chan_t *chan_obj, if (!pipe->port->flags.conn_devc_ena) { return event; //Treat as a no event. } - + bool handle_waiting_xfer_done = false; switch (chan_event) { - case USBH_HAL_CHAN_EVENT_SLOT_DONE: { - //An entire transfer descriptor list has completed execution - pipe->last_event = HCD_PIPE_EVENT_IRP_DONE; - event = HCD_PIPE_EVENT_IRP_DONE; - _xfer_desc_list_parse(pipe, false); //Parse results of IRP - _pipe_return_cur_irp(pipe); //Return the IRP 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_irp(pipe)) { - //Fill the descriptor list with the IRP and start the transfer - _xfer_desc_list_fill(pipe); - usbh_hal_chan_activate(chan_obj, 0); //Start with the first descriptor + case USBH_HAL_CHAN_EVENT_CPLT: { + if (!_buffer_check_done(pipe)) { + break; + } + pipe->last_event = HCD_PIPE_EVENT_IRP_DONE; + event = pipe->last_event; + //Mark the buffer as done + int stop_idx = usbh_hal_chan_get_qtd_idx(chan_obj); + _buffer_done(pipe, stop_idx); + //First check if there is another buffer we can execute + if (_buffer_can_exec(pipe) && !pipe->cs_flags.waiting_xfer_done) { + //If the next buffer is filled and ready to execute, execute it + _buffer_exec(pipe); + } + //Handle the previously done buffer + _buffer_parse(pipe); + if (pipe->cs_flags.waiting_xfer_done) { + handle_waiting_xfer_done = true; + } else if (_buffer_can_fill(pipe)) { + //Now that we've parsed a buffer, see if another IRP can be filled in its place + _buffer_fill(pipe); } - 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_PRIV_XFER_TYPE_CTRL); - _xfer_desc_list_continue(pipe); //Continue the transfer request. - //We are continuing a transfer, so no event has occurred break; } case USBH_HAL_CHAN_EVENT_ERROR: { @@ -677,17 +903,41 @@ static hcd_pipe_event_t _intr_hdlr_chan(pipe_t *pipe, usbh_hal_chan_t *chan_obj, pipe->last_event = pipe_decode_error_event(chan_error); event = pipe->last_event; pipe->state = HCD_PIPE_STATE_HALTED; - //Parse the failed IRP and update it's IRP status - _xfer_desc_list_parse(pipe, true); - _pipe_return_cur_irp(pipe); //Return the IRP to the pipe's done tailq + //Mark the buffer as done with an error + int stop_idx = usbh_hal_chan_get_qtd_idx(chan_obj); + _buffer_done_error(pipe, stop_idx, pipe->state, pipe->last_event, false); + //Parse the buffer + _buffer_parse(pipe); + if (pipe->cs_flags.waiting_xfer_done) { + handle_waiting_xfer_done = true; + } break; } + case USBH_HAL_CHAN_EVENT_NONE: { + break; //Nothing to do + } case USBH_HAL_CHAN_EVENT_HALT_REQ: //We currently don't halt request so this event should never occur default: abort(); break; } - + if (handle_waiting_xfer_done) { + //A port/pipe command is waiting for this pipe to complete its transfer. So don't load the next transfer + pipe->cs_flags.waiting_xfer_done = 0; + if (pipe->port->flags.waiting_all_pipes_pause) { + //Port command is waiting for all pipes to be paused + pipe->cs_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); + } + } return event; } @@ -699,7 +949,8 @@ static hcd_pipe_event_t _intr_hdlr_chan(pipe_t *pipe, usbh_hal_chan_t *chan_obj, * - 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 + * + * @param arg Interrupt handler argument */ static void intr_hdlr_main(void *arg) { @@ -748,16 +999,19 @@ 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)); + void *frame_list = heap_caps_aligned_calloc(USBH_HAL_FRAME_LIST_MEM_ALIGN, FRAME_LIST_LEN,sizeof(uint32_t), MALLOC_CAP_DMA); SemaphoreHandle_t port_mux = xSemaphoreCreateMutex(); - if (port == NULL || hal == NULL || port_mux == NULL) { + if (port == NULL || hal == NULL || frame_list == NULL || port_mux == NULL) { free(port); free(hal); + free(frame_list); if (port_mux != NULL) { vSemaphoreDelete(port_mux); } return NULL; } port->hal = hal; + port->frame_list = frame_list; port->port_mux = port_mux; return port; } @@ -768,6 +1022,7 @@ static void port_obj_free(port_t *port) return; } vSemaphoreDelete(port->port_mux); + free(port->frame_list); free(port->hal); free(port); } @@ -817,7 +1072,7 @@ esp_err_t hcd_install(const hcd_config_t *config) - 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 + - Forces 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); @@ -825,6 +1080,7 @@ esp_err_t hcd_install(const hcd_config_t *config) 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); @@ -868,12 +1124,9 @@ static void _port_invalidate_all_pipes(port_t *port) //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 in-flight transfer, parse and return it - if (pipe->inflight_irp != NULL) { - _xfer_desc_list_parse(pipe, true); - _pipe_return_cur_irp(pipe); - } - //Retire any remaining IRPs + //Flush all buffers that are still awaiting exec + _buffer_flush_all(pipe, false); + //Retire any remaining IRPs in the pending tailq _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. @@ -905,18 +1158,19 @@ static bool _port_pause_all_pipes(port_t *port) int num_pipes_waiting_done = 0; //Process all pipes that have queued IRPs TAILQ_FOREACH(pipe, &port->pipes_active_tailq, tailq_entry) { - if (pipe->inflight_irp != NULL) { - //Pipe has an in-flight transfer. Indicate to the pipe we are waiting the transfer to complete - pipe->flags.waiting_xfer_done = 1; + //Check if pipe is currently executing + if (pipe->multi_buffer_control.buffer_is_executing) { + //Pipe is executing a buffer. Indicate to the pipe we are waiting the buffer's transfer to complete + pipe->cs_flags.waiting_xfer_done = 1; num_pipes_waiting_done++; } else { - //No in-flight transfer so no need to wait - pipe->flags.paused = 1; + //No buffer is being executed so need to wait + pipe->cs_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; + pipe->cs_flags.paused = 1; } if (num_pipes_waiting_done > 0) { //Indicate we need to wait for one or more pipes to complete their transfers @@ -933,15 +1187,16 @@ static void _port_unpause_all_pipes(port_t *port) 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; + pipe->cs_flags.paused = 0; } //Process all pipes that have queued IRPs TAILQ_FOREACH(pipe, &port->pipes_active_tailq, tailq_entry) { - pipe->flags.paused = 0; - //If the pipe has more pending IRP, start them. - if (_pipe_get_next_irp(pipe)) { - _xfer_desc_list_fill(pipe); - usbh_hal_chan_activate(pipe->chan_obj, 0); + pipe->cs_flags.paused = 0; + if (_buffer_can_fill(pipe)) { + _buffer_fill(pipe); + } + if (_buffer_can_exec(pipe)) { + _buffer_exec(pipe); } } } @@ -1005,14 +1260,14 @@ static bool _port_bus_resume(port_t *port) //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 + //Port state unexpectedly 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 + //Port state unexpectedly changed goto bailout; } port->state = HCD_PORT_STATE_ENABLED; @@ -1031,7 +1286,7 @@ static bool _port_disable(port_t *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 + //Port state unexpectedly changed goto bailout; } } @@ -1041,7 +1296,7 @@ static bool _port_disable(port_t *port) usbh_hal_port_disable(port->hal); _internal_port_event_wait(port); if (port->state != HCD_PORT_STATE_DISABLED) { - //Port state unexpectedley changed + //Port state unexpectedly changed goto bailout; } _port_invalidate_all_pipes(port); @@ -1130,7 +1385,7 @@ esp_err_t hcd_port_command(hcd_port_handle_t port_hdl, hcd_port_cmd_t command) //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_init(port->hal); usbh_hal_port_toggle_power(port->hal, true); ret = ESP_OK; } @@ -1140,7 +1395,7 @@ esp_err_t hcd_port_command(hcd_port_handle_t port_hdl, hcd_port_cmd_t command) //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_deinit(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; @@ -1150,7 +1405,18 @@ esp_err_t hcd_port_command(hcd_port_handle_t port_hdl, hcd_port_cmd_t command) 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; + if (_port_bus_reset(port)) { + //Set FIFO sizes to default + usbh_hal_set_fifo_size(port->hal, &fifo_config_default); + port->fifo_bias = HCD_PORT_FIFO_BIAS_BALANCED; + //Reset frame list and enable periodic scheduling + memset(port->frame_list, 0, FRAME_LIST_LEN * sizeof(uint32_t)); + usbh_hal_port_set_frame_list(port->hal, port->frame_list, FRAME_LIST_LEN); + usbh_hal_port_periodic_enable(port->hal); + ret = ESP_OK; + } else { + ret = ESP_ERR_INVALID_RESPONSE; + } } break; } @@ -1198,7 +1464,7 @@ 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 + //Device speed is only valid if there is device connected to the port that has been reset HCD_CHECK_FROM_CRIT(port->flags.conn_devc_ena, ESP_ERR_INVALID_STATE); usb_priv_speed_t hal_speed = usbh_hal_port_get_conn_speed(port->hal); if (hal_speed == USB_PRIV_SPEED_FULL) { @@ -1233,7 +1499,7 @@ hcd_port_event_t hcd_port_handle_event(hcd_port_handle_t port_hdl) port->state = HCD_PORT_STATE_DISABLED; ret = HCD_PORT_EVENT_NONE; } else { - //No device conencted after debounce delay. This is an actual disconenction + //No device connected after debounce delay. This is an actual disconnection port->state = HCD_PORT_STATE_DISCONNECTED; ret = HCD_PORT_EVENT_DISCONNECTION; } @@ -1276,7 +1542,7 @@ esp_err_t hcd_port_recover(hcd_port_handle_t port_hdl) return ESP_OK; } -void *hcd_port_get_ctx(hcd_port_handle_t port_hdl) +void *hcd_port_get_context(hcd_port_handle_t port_hdl) { port_t *port = (port_t *)port_hdl; void *ret; @@ -1286,53 +1552,57 @@ void *hcd_port_get_ctx(hcd_port_handle_t port_hdl) return ret; } +esp_err_t hcd_port_set_fifo_bias(hcd_port_handle_t port_hdl, hcd_port_fifo_bias_t bias) +{ + esp_err_t ret; + port_t *port = (port_t *)port_hdl; + xSemaphoreTake(port->port_mux, portMAX_DELAY); + HCD_ENTER_CRITICAL(); + //Check that port is in the correct state to update FIFO sizes + if (port->initialized && !port->flags.event_pending && port->num_pipes_idle == 0 && port->num_pipes_queued == 0) { + const usbh_hal_fifo_config_t *fifo_config; + switch (bias) { + case HCD_PORT_FIFO_BIAS_BALANCED: + fifo_config = &fifo_config_default; + break; + case HCD_PORT_FIFO_BIAS_RX: + fifo_config = &fifo_config_bias_rx; + break; + case HCD_PORT_FIFO_BIAS_PTX: + fifo_config = &fifo_config_bias_ptx; + break; + default: + fifo_config = NULL; + abort(); + } + usbh_hal_set_fifo_size(port->hal, fifo_config); + port->fifo_bias = bias; + ret = ESP_OK; + } else { + ret = ESP_ERR_INVALID_STATE; + } + HCD_EXIT_CRITICAL(); + xSemaphoreGive(port->port_mux); + return ret; +} + // --------------------------------------------------- HCD Pipes ------------------------------------------------------- // ----------------------- Private ------------------------- -static bool _pipe_get_next_irp(pipe_t *pipe) -{ - assert(pipe->inflight_irp == NULL); - bool ret; - //This function assigns the next pending IRP to the inflight_irp - if (pipe->num_irp_pending > 0) { - //Set inflight_irp to the next pending IRP - pipe->inflight_irp = TAILQ_FIRST(&pipe->pending_irp_tailq); - TAILQ_REMOVE(&pipe->pending_irp_tailq, pipe->inflight_irp, tailq_entry); - pipe->num_irp_pending--; - //Update the IRP's current state - IRP_STATE_SET(pipe->inflight_irp->reserved_flags, IRP_STATE_INFLIGHT); - ret = true; - } else { - ret = false; - } - return ret; -} - -static void _pipe_return_cur_irp(pipe_t *pipe) -{ - assert(pipe->inflight_irp != NULL); - //Add the IRP to the pipe's done tailq - TAILQ_INSERT_TAIL(&pipe->done_irp_tailq, pipe->inflight_irp, tailq_entry); - //Update the IRP's current state - IRP_STATE_SET(pipe->inflight_irp->reserved_flags, IRP_STATE_DONE); - pipe->inflight_irp = NULL; - pipe->num_irp_done++; -} - static bool _pipe_wait_done(pipe_t *pipe) { - //Check if there is a currently in-flight IRP - if (pipe->inflight_irp != NULL) { + //Check if the pipe has a currently executing buffer + if (pipe->multi_buffer_control.buffer_is_executing) { //Wait for pipe to complete its transfer - pipe->flags.waiting_xfer_done = 1; + pipe->cs_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 + pipe->cs_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); + bool chan_halted = usbh_hal_chan_request_halt(pipe->chan_obj); assert(chan_halted); (void) chan_halted; } @@ -1341,16 +1611,16 @@ static bool _pipe_wait_done(pipe_t *pipe) static void _pipe_retire(pipe_t *pipe, bool self_initiated) { - //Cannot have any in-flight IRP - assert(pipe->inflight_irp == NULL); + //Cannot have a currently executing buffer + assert(!pipe->multi_buffer_control.buffer_is_executing); if (pipe->num_irp_pending > 0) { //Process all remaining pending IRPs usb_irp_t *irp; TAILQ_FOREACH(irp, &pipe->pending_irp_tailq, tailq_entry) { //Update the IRP's current state IRP_STATE_SET(irp->reserved_flags, IRP_STATE_DONE); - //If we are initiating the retire, mark the IRP as cancelled - irp->status = (self_initiated) ? USB_TRANSFER_STATUS_CANCELLED : USB_TRANSFER_STATUS_NO_DEVICE; + //If we are initiating the retire, mark the IRP as canceled + irp->status = (self_initiated) ? USB_TRANSFER_STATUS_CANCELED : USB_TRANSFER_STATUS_NO_DEVICE; } //Concatenated pending tailq to the done tailq TAILQ_CONCAT(&pipe->done_irp_tailq, &pipe->pending_irp_tailq, tailq_entry); @@ -1379,6 +1649,155 @@ static inline hcd_pipe_event_t pipe_decode_error_event(usbh_hal_chan_error_t cha return event; } +static dma_buffer_block_t *buffer_block_alloc(usb_transfer_type_t type) +{ + int desc_list_len; + switch (type) { + case USB_TRANSFER_TYPE_CTRL: + desc_list_len = XFER_LIST_LEN_CTRL; + break; + case USB_TRANSFER_TYPE_ISOCHRONOUS: + desc_list_len = XFER_LIST_LEN_ISOC; + break; + case USB_TRANSFER_TYPE_BULK: + desc_list_len = XFER_LIST_LEN_BULK; + break; + default: //USB_TRANSFER_TYPE_INTR: + desc_list_len = XFER_LIST_LEN_INTR; + break; + } + dma_buffer_block_t *buffer = calloc(1, sizeof(dma_buffer_block_t)); + void *xfer_desc_list = heap_caps_aligned_calloc(USBH_HAL_DMA_MEM_ALIGN, desc_list_len, sizeof(usbh_ll_dma_qtd_t), MALLOC_CAP_DMA); + if (buffer == NULL || xfer_desc_list == NULL) { + free(buffer); + heap_caps_free(xfer_desc_list); + return NULL; + } + buffer->xfer_desc_list = xfer_desc_list; + return buffer; +} + +static void buffer_block_free(dma_buffer_block_t *buffer) +{ + if (buffer == NULL) { + return; + } + heap_caps_free(buffer->xfer_desc_list); + free(buffer); +} + +static bool pipe_alloc_check_args(const hcd_pipe_config_t *pipe_config, usb_speed_t port_speed, hcd_port_fifo_bias_t fifo_bias, usb_transfer_type_t type, bool is_default_pipe) +{ + //Check if pipe can be supported + if (port_speed == USB_SPEED_LOW && pipe_config->dev_speed == USB_SPEED_FULL) { + //Low speed port does not supported full speed pipe + return false; + } + if (pipe_config->dev_speed == USB_SPEED_LOW && (type == USB_TRANSFER_TYPE_BULK || type == USB_TRANSFER_TYPE_ISOCHRONOUS)) { + //Low speed does not support Bulk or Isochronous pipes + return false; + } + //Check interval of pipe + if (type == USB_TRANSFER_TYPE_INTR && + (pipe_config->ep_desc->bInterval > 0 && pipe_config->ep_desc->bInterval > 32)) { + //Interval not supported for interrupt pipe + return false; + } + if (type == USB_TRANSFER_TYPE_ISOCHRONOUS && + (pipe_config->ep_desc->bInterval > 0 && pipe_config->ep_desc->bInterval > 6)) { + //Interval not supported for isochronous pipe (where 0 < 2^(bInterval - 1) <= 32) + return false; + } + + if (is_default_pipe) { + return true; + } + //Check if MPS is within FIFO limits + const fifo_mps_limits_t *mps_limits; + switch (fifo_bias) { + case HCD_PORT_FIFO_BIAS_BALANCED: + mps_limits = &mps_limits_default; + break; + case HCD_PORT_FIFO_BIAS_RX: + mps_limits = &mps_limits_bias_rx; + break; + default: //HCD_PORT_FIFO_BIAS_PTX + mps_limits = &mps_limits_bias_ptx; + break; + } + int limit; + if (USB_DESC_EP_GET_EP_DIR(pipe_config->ep_desc)) { //IN + limit = mps_limits->in_mps; + } else { //OUT + if (type == USB_TRANSFER_TYPE_CTRL || type == USB_TRANSFER_TYPE_BULK) { + limit = mps_limits->non_periodic_out_mps; + } else { + limit = mps_limits->periodic_out_mps; + } + } + return (pipe_config->ep_desc->wMaxPacketSize <= limit); +} + +static void pipe_set_ep_char(const hcd_pipe_config_t *pipe_config, usb_transfer_type_t type, bool is_default_pipe, int pipe_idx, usb_speed_t port_speed, usbh_hal_ep_char_t *ep_char) +{ + //Initialize EP characteristics + usb_priv_xfer_type_t hal_xfer_type; + switch (type) { + case USB_TRANSFER_TYPE_CTRL: + hal_xfer_type = USB_PRIV_XFER_TYPE_CTRL; + break; + case USB_TRANSFER_TYPE_ISOCHRONOUS: + hal_xfer_type = USB_PRIV_XFER_TYPE_ISOCHRONOUS; + break; + case USB_TRANSFER_TYPE_BULK: + hal_xfer_type = USB_PRIV_XFER_TYPE_BULK; + break; + default: //USB_TRANSFER_TYPE_INTR + hal_xfer_type = USB_PRIV_XFER_TYPE_INTR; + break; + } + ep_char->type = hal_xfer_type; + if (is_default_pipe) { + ep_char->bEndpointAddress = 0; + //Set the default pipe's MPS to the worst case MPS for the device's speed + ep_char->mps = (pipe_config->dev_speed == USB_SPEED_FULL) ? CTRL_EP_MAX_MPS_FS : CTRL_EP_MAX_MPS_LS; + } else { + ep_char->bEndpointAddress = pipe_config->ep_desc->bEndpointAddress; + ep_char->mps = pipe_config->ep_desc->wMaxPacketSize; + } + ep_char->dev_addr = pipe_config->dev_addr; + ep_char->ls_via_fs_hub = (port_speed == USB_SPEED_FULL && pipe_config->dev_speed == USB_SPEED_LOW); + //Calculate the pipe's interval in terms of USB frames + if (type == USB_TRANSFER_TYPE_INTR || type == USB_TRANSFER_TYPE_ISOCHRONOUS) { + int interval_frames; + if (type == USB_TRANSFER_TYPE_INTR) { + interval_frames = pipe_config->ep_desc->bInterval; + } else { + interval_frames = (1 << (pipe_config->ep_desc->bInterval - 1)); + } + //Round down interval to nearest power of 2 + if (interval_frames >= 32) { + interval_frames = 32; + } else if (interval_frames >= 16) { + interval_frames = 16; + } else if (interval_frames >= 8) { + interval_frames = 8; + } else if (interval_frames >= 4) { + interval_frames = 4; + } else if (interval_frames >= 2) { + interval_frames = 2; + } else if (interval_frames >= 1) { + interval_frames = 1; + } + ep_char->periodic.interval = interval_frames; + //We are the Nth pipe to be allocated. Use N as a phase offset + ep_char->periodic.phase_offset_frames = pipe_idx & (XFER_LIST_LEN_ISOC - 1); + }else { + ep_char->periodic.interval = 0; + ep_char->periodic.phase_offset_frames = 0; + } +} + // ----------------------- 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) @@ -1386,85 +1805,56 @@ esp_err_t hcd_pipe_alloc(hcd_port_handle_t port_hdl, const hcd_pipe_config_t *pi 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 targeted port is initialized and connected to an enabled device + //Can only allocate a pipe if the target port is initialized and connected 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_port_fifo_bias_t port_fifo_bias = port->fifo_bias; + int pipe_idx = port->num_pipes_idle + port->num_pipes_queued; 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_transfer_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; + bool is_default; + if (pipe_config->ep_desc == NULL) { + type = USB_TRANSFER_TYPE_CTRL; + is_default = true; } else { type = USB_DESC_EP_GET_XFERTYPE(pipe_config->ep_desc); - is_default_pipe = false; + is_default = 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; - } + //Check if pipe configuration can be supported + if (!pipe_alloc_check_args(pipe_config, port_speed, port_fifo_bias, type, is_default)) { + return ESP_ERR_NOT_SUPPORTED; } + esp_err_t ret; //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) { + usbh_hal_chan_t *chan_obj = calloc(1, sizeof(usbh_hal_chan_t)); + dma_buffer_block_t *buffers[NUM_BUFFERS] = {0}; + if (pipe == NULL|| chan_obj == NULL) { ret = ESP_ERR_NO_MEM; goto err; } + for (int i = 0; i < NUM_BUFFERS; i++) { + buffers[i] = buffer_block_alloc(type); + if (buffers[i] == NULL) { + ret = ESP_ERR_NO_MEM; + goto err; + } + } //Initialize pipe object TAILQ_INIT(&pipe->pending_irp_tailq); TAILQ_INIT(&pipe->done_irp_tailq); + for (int i = 0; i < NUM_BUFFERS; i++) { + pipe->buffers[i] = buffers[i]; + } + pipe->multi_buffer_control.buffer_num_to_fill = NUM_BUFFERS; pipe->port = port; - pipe->xfer_desc_list = xfer_desc_list; - pipe->flags.xfer_desc_list_len = num_xfer_desc; pipe->chan_obj = chan_obj; - usb_priv_xfer_type_t hal_type; - switch (type) { - case USB_XFER_TYPE_CTRL: - hal_type = USB_PRIV_XFER_TYPE_CTRL; - break; - case USB_XFER_TYPE_ISOCHRONOUS: - hal_type = USB_PRIV_XFER_TYPE_ISOCHRONOUS; - break; - case USB_XFER_TYPE_BULK: - hal_type = USB_PRIV_XFER_TYPE_ISOCHRONOUS; - break; - default: //USB_XFER_TYPE_INTR - hal_type = USB_PRIV_XFER_TYPE_INTR; - break; - } - pipe->ep_char.type = hal_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); + usbh_hal_ep_char_t ep_char; + pipe_set_ep_char(pipe_config, type, is_default, pipe_idx, port_speed, &ep_char); + memcpy(&pipe->ep_char, &ep_char, sizeof(usbh_hal_ep_char_t)); pipe->state = HCD_PIPE_STATE_ACTIVE; pipe->callback = pipe_config->callback; pipe->callback_arg = pipe_config->callback_arg; @@ -1483,17 +1873,19 @@ esp_err_t hcd_pipe_alloc(hcd_port_handle_t port_hdl, const hcd_pipe_config_t *pi ret = ESP_ERR_NOT_SUPPORTED; goto err; } - usbh_hal_chan_set_ep_char(pipe->chan_obj, &pipe->ep_char); - + usbh_hal_chan_set_ep_char(port->hal, 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; + return ESP_OK; err: - free(xfer_desc_list); + for (int i = 0; i < NUM_BUFFERS; i++) { + buffer_block_free(buffers[i]); + } free(chan_obj); free(pipe); return ret; @@ -1504,7 +1896,9 @@ 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 IRPs have been removed and pipe has no pending events - HCD_CHECK_FROM_CRIT(pipe->inflight_irp == NULL + HCD_CHECK_FROM_CRIT(!pipe->multi_buffer_control.buffer_is_executing + && pipe->multi_buffer_control.buffer_num_to_parse == 0 + && pipe->multi_buffer_control.buffer_num_to_exec == 0 && pipe->num_irp_pending == 0 && pipe->num_irp_done == 0, ESP_ERR_INVALID_STATE); @@ -1515,31 +1909,49 @@ esp_err_t hcd_pipe_free(hcd_pipe_handle_t pipe_hdl) HCD_EXIT_CRITICAL(); //Free pipe resources - free(pipe->xfer_desc_list); + for (int i = 0; i < NUM_BUFFERS; i++) { + buffer_block_free(pipe->buffers[i]); + } 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) +esp_err_t hcd_pipe_update_mps(hcd_pipe_handle_t pipe_hdl, 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->cs_flags.pipe_cmd_processing && pipe->num_irp_pending == 0 && pipe->num_irp_done == 0, ESP_ERR_INVALID_STATE); - //Check that all IRPs 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); + //Update the underlying channel's registers + usbh_hal_chan_set_ep_char(pipe->port->hal, pipe->chan_obj, &pipe->ep_char); HCD_EXIT_CRITICAL(); return ESP_OK; } -void *hcd_pipe_get_ctx(hcd_pipe_handle_t pipe_hdl) +esp_err_t hcd_pipe_update_dev_addr(hcd_pipe_handle_t pipe_hdl, uint8_t dev_addr) +{ + 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->cs_flags.pipe_cmd_processing + && pipe->num_irp_pending == 0 + && pipe->num_irp_done == 0, + ESP_ERR_INVALID_STATE); + pipe->ep_char.dev_addr = dev_addr; + //Update the underlying channel's registers + usbh_hal_chan_set_ep_char(pipe->port->hal, pipe->chan_obj, &pipe->ep_char); + HCD_EXIT_CRITICAL(); + return ESP_OK; +} + +void *hcd_pipe_get_context(hcd_pipe_handle_t pipe_hdl) { pipe_t *pipe = (pipe_t *)pipe_hdl; void *ret; @@ -1573,17 +1985,17 @@ esp_err_t hcd_pipe_command(hcd_pipe_handle_t pipe_hdl, hcd_pipe_cmd_t command) 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) { + if (pipe->cs_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; + pipe->cs_flags.pipe_cmd_processing = 1; switch (command) { case HCD_PIPE_CMD_ABORT: { //Retire all scheduled IRPs. Pipe's state remains unchanged if (!_pipe_wait_done(pipe)) { //Stop any on going transfers ret = ESP_ERR_INVALID_RESPONSE; - break; } + _buffer_flush_all(pipe, true); //Some buffers might still be filled. Flush them _pipe_retire(pipe, true); //Retire any pending transfers break; } @@ -1593,6 +2005,7 @@ esp_err_t hcd_pipe_command(hcd_pipe_handle_t pipe_hdl, hcd_pipe_cmd_t command) ret = ESP_ERR_INVALID_RESPONSE; break; } + _buffer_flush_all(pipe, true); //Some buffers might still be filled. Flush them _pipe_retire(pipe, true); //Retire any pending transfers pipe->state = HCD_PIPE_STATE_ACTIVE; break; @@ -1602,10 +2015,11 @@ esp_err_t hcd_pipe_command(hcd_pipe_handle_t pipe_hdl, hcd_pipe_cmd_t command) 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_irp(pipe)) { - //Fill the descriptor list with the IRP and start the transfer - _xfer_desc_list_fill(pipe); - usbh_hal_chan_activate(pipe->chan_obj, 0); //Start with the first descriptor + if (_buffer_can_fill(pipe)) { + _buffer_fill(pipe); + } + if (_buffer_can_exec(pipe)) { + _buffer_exec(pipe); } } break; @@ -1620,7 +2034,7 @@ esp_err_t hcd_pipe_command(hcd_pipe_handle_t pipe_hdl, hcd_pipe_cmd_t command) break; } } - pipe->flags.pipe_cmd_processing = 0; + pipe->cs_flags.pipe_cmd_processing = 0; } HCD_EXIT_CRITICAL(); return ret; @@ -1637,160 +2051,471 @@ hcd_pipe_event_t hcd_pipe_get_event(hcd_pipe_handle_t pipe_hdl) return ret; } -// ---------------------------------------------- HCD Transfer Descriptors --------------------------------------------- +// ------------------------------------------------- Buffer Control ---------------------------------------------------- -// ----------------------- Private ------------------------- - -static void _xfer_desc_list_fill(pipe_t *pipe) +static inline void _buffer_fill_ctrl(dma_buffer_block_t *buffer, usb_irp_t *irp) { - //inflight_irp of the pipe must already set to the target IRP - assert(pipe->inflight_irp != NULL); - //Fill transfer descriptor list with a single IRP - usb_irp_t *usb_irp = pipe->inflight_irp; - switch (pipe->ep_char.type) { - case USB_XFER_TYPE_CTRL: { - //Get information about the control 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); + //Get information about the control transfer by analyzing the setup packet (the first 8 bytes of the IRP's data) + usb_ctrl_req_t *ctrl_req = (usb_ctrl_req_t *)irp->data_buffer; + bool data_stg_in = (ctrl_req->bRequestType & USB_B_REQUEST_TYPE_DIR_IN); + bool data_stg_skip = (irp->num_bytes == 0); + //Fill setup stage + usbh_hal_xfer_desc_fill(buffer->xfer_desc_list, 0, irp->data_buffer, sizeof(usb_ctrl_req_t), + USBH_HAL_XFER_DESC_FLAG_SETUP | USBH_HAL_XFER_DESC_FLAG_HOC); + //Fill data stage + if (data_stg_skip) { + //Not data stage. Fill with an empty descriptor + usbh_hal_xfer_desc_clear(buffer->xfer_desc_list, 1); + } else { + //Fill data stage + usbh_hal_xfer_desc_fill(buffer->xfer_desc_list, 1, irp->data_buffer + sizeof(usb_ctrl_req_t), irp->num_bytes, + ((data_stg_in) ? USBH_HAL_XFER_DESC_FLAG_IN : 0) | USBH_HAL_XFER_DESC_FLAG_HOC); + } + //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(buffer->xfer_desc_list, 2, NULL, 0, + ((data_stg_in && !data_stg_skip) ? 0 : USBH_HAL_XFER_DESC_FLAG_IN) | USBH_HAL_XFER_DESC_FLAG_HOC); + //Update buffer flags + buffer->flags.ctrl.data_stg_in = data_stg_in; + buffer->flags.ctrl.data_stg_skip = data_stg_skip; + buffer->flags.ctrl.cur_stg = 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); +static inline void _buffer_fill_bulk(dma_buffer_block_t *buffer, usb_irp_t *irp, bool is_in) +{ + if (is_in) { + usbh_hal_xfer_desc_fill(buffer->xfer_desc_list, 0, irp->data_buffer, irp->num_bytes, + USBH_HAL_XFER_DESC_FLAG_IN | USBH_HAL_XFER_DESC_FLAG_HOC); + } else if (irp->flags & USB_IRP_FLAG_ZERO_PACK) { + //We need to add an extra zero length packet, so two descriptors are used + usbh_hal_xfer_desc_fill(buffer->xfer_desc_list, 0, irp->data_buffer, irp->num_bytes, 0); + usbh_hal_xfer_desc_fill(buffer->xfer_desc_list, 1, NULL, 0, USBH_HAL_XFER_DESC_FLAG_HOC); + } else { + usbh_hal_xfer_desc_fill(buffer->xfer_desc_list, 0, irp->data_buffer, irp->num_bytes, USBH_HAL_XFER_DESC_FLAG_HOC); + } + //Update buffer flags + buffer->flags.bulk.zero_len_packet = (is_in && (irp->flags & USB_IRP_FLAG_ZERO_PACK)) ? 1 : 0; +} + +static inline void _buffer_fill_intr(dma_buffer_block_t *buffer, usb_irp_t *irp, bool is_in, int mps) +{ + int num_qtds; + if (is_in) { + assert(irp->num_bytes % mps == 0); //IN transfers MUST be integer multiple of MPS + num_qtds = irp->num_bytes / mps; + } else { + num_qtds = irp->num_bytes / mps; //Floor division for number of MPS packets + if (irp->num_bytes % irp->num_bytes > 0) { + num_qtds++; //For the last shot packet + } + } + assert(num_qtds <= XFER_LIST_LEN_INTR); + //Fill all but last descriptor + int bytes_filled = 0; + for (int i = 0; i < num_qtds - 1; i++) { + usbh_hal_xfer_desc_fill(buffer->xfer_desc_list, i, &irp->data_buffer[bytes_filled], mps, (is_in) ? USBH_HAL_XFER_DESC_FLAG_IN : 0); + bytes_filled += mps; + } + //Fill in the last descriptor with HOC flag + usbh_hal_xfer_desc_fill(buffer->xfer_desc_list, num_qtds - 1, &irp->data_buffer[bytes_filled], irp->num_bytes - bytes_filled, + ((is_in) ? USBH_HAL_XFER_DESC_FLAG_IN : 0) | USBH_HAL_XFER_DESC_FLAG_HOC); + //Update buffer members and flags + buffer->flags.intr.num_qtds = num_qtds; +} + +static inline void _buffer_fill_isoc(dma_buffer_block_t *buffer, usb_irp_t *irp, bool is_in, int mps, int interval, int start_idx) +{ + assert(interval > 0); + int total_num_desc = irp->num_iso_packets * interval; + assert(total_num_desc <= XFER_LIST_LEN_ISOC); + int desc_idx = start_idx; + int bytes_filled = 0; + //For each packet, fill in a descriptor and a interval-1 blank descriptor after it + for (int pkt_idx = 0; pkt_idx < irp->num_iso_packets; pkt_idx++) { + int xfer_len = irp->iso_packet_desc[pkt_idx].length; + uint32_t flags = (is_in) ? USBH_HAL_XFER_DESC_FLAG_IN : 0; + if (pkt_idx == irp->num_iso_packets - 1) { + //Last packet, set the the HOC flag + flags |= USBH_HAL_XFER_DESC_FLAG_HOC; + } + usbh_hal_xfer_desc_fill(buffer->xfer_desc_list, desc_idx, &irp->data_buffer[bytes_filled], xfer_len, flags); + bytes_filled += xfer_len; + if (++desc_idx >= XFER_LIST_LEN_ISOC) { + desc_idx = 0; + } + //Clear descriptors for unscheduled frames + for (int i = 0; i < interval - 1; i++) { + usbh_hal_xfer_desc_clear(buffer->xfer_desc_list, desc_idx); + if (++desc_idx >= XFER_LIST_LEN_ISOC) { + desc_idx = 0; } - //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); + } + } + //Update buffer members and flags + buffer->flags.isoc.num_qtds = total_num_desc; + buffer->flags.isoc.interval = interval; + buffer->flags.isoc.irp_start_idx = start_idx; + buffer->flags.isoc.next_irp_start_idx = desc_idx; +} + +static void _buffer_fill(pipe_t *pipe) +{ + //Get an IRP from the pending tailq + usb_irp_t *irp = TAILQ_FIRST(&pipe->pending_irp_tailq); + assert(pipe->num_irp_pending > 0 && irp != NULL); + TAILQ_REMOVE(&pipe->pending_irp_tailq, irp, tailq_entry); + pipe->num_irp_pending--; + + //Select the inactive buffer + assert(pipe->multi_buffer_control.buffer_num_to_exec <= NUM_BUFFERS); + dma_buffer_block_t *buffer_to_fill = pipe->buffers[pipe->multi_buffer_control.wr_idx]; + assert(buffer_to_fill->irp == NULL); + bool is_in = pipe->ep_char.bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK; + int mps = pipe->ep_char.mps; + switch (pipe->ep_char.type) { + case USB_PRIV_XFER_TYPE_CTRL: { + _buffer_fill_ctrl(buffer_to_fill, irp); + break; + } + case USB_PRIV_XFER_TYPE_ISOCHRONOUS: { + uint32_t start_idx; + if (pipe->multi_buffer_control.buffer_num_to_exec == 0) { + //There are no more previously filled buffers to execute. We need to calculate a new start index based on HFNUM and the pipe's schedule + uint32_t cur_frame_num = usbh_hal_port_get_cur_frame_num(pipe->port->hal); + uint32_t cur_mod_idx_no_offset = (cur_frame_num - pipe->ep_char.periodic.phase_offset_frames) & (XFER_LIST_LEN_ISOC - 1); //Get the modulated index (i.e., the Nth desc in the descriptor list) + //This is the non-offset modulated QTD index of the last scheduled interval + uint32_t last_interval_mod_idx_no_offset = (cur_mod_idx_no_offset / pipe->ep_char.periodic.interval) * pipe->ep_char.periodic.interval; //Floor divide and the multiply again + uint32_t next_interval_idx_no_offset = (last_interval_mod_idx_no_offset + pipe->ep_char.periodic.interval); + //We want at least a half interval or 2 frames of buffer space + if (next_interval_idx_no_offset - cur_mod_idx_no_offset > (pipe->ep_char.periodic.interval / 2) + && next_interval_idx_no_offset - cur_mod_idx_no_offset >= 2) { + start_idx = (next_interval_idx_no_offset + pipe->ep_char.periodic.phase_offset_frames) & (XFER_LIST_LEN_ISOC - 1); + } else { + //Not enough time until the next schedule, add another interval to it. + start_idx = (next_interval_idx_no_offset + pipe->ep_char.periodic.interval + pipe->ep_char.periodic.phase_offset_frames) & (XFER_LIST_LEN_ISOC - 1); + } + } else { + //Start index is based on previously filled buffer + uint32_t prev_buffer_idx = (pipe->multi_buffer_control.wr_idx - 1) & (NUM_BUFFERS - 1); + dma_buffer_block_t *prev_filled_buffer = pipe->buffers[prev_buffer_idx]; + start_idx = prev_filled_buffer->flags.isoc.next_irp_start_idx; + } + _buffer_fill_isoc(buffer_to_fill, irp, is_in, mps, (int)pipe->ep_char.periodic.interval, start_idx); + break; + } + case USB_PRIV_XFER_TYPE_BULK: { + _buffer_fill_bulk(buffer_to_fill, irp, is_in); + break; + } + case USB_PRIV_XFER_TYPE_INTR: { + _buffer_fill_intr(buffer_to_fill, irp, is_in, mps); + break; + } + default: { + abort(); + break; + } + } + buffer_to_fill->irp = irp; + IRP_STATE_SET(irp->reserved_flags, IRP_STATE_INFLIGHT); + //Update multi buffer flags + pipe->multi_buffer_control.wr_idx++; + pipe->multi_buffer_control.buffer_num_to_fill--; + pipe->multi_buffer_control.buffer_num_to_exec++; +} + +static void _buffer_exec(pipe_t *pipe) +{ + assert(pipe->multi_buffer_control.rd_idx != pipe->multi_buffer_control.wr_idx || pipe->multi_buffer_control.buffer_num_to_exec > 0); + dma_buffer_block_t *buffer_to_exec = pipe->buffers[pipe->multi_buffer_control.rd_idx]; + assert(buffer_to_exec->irp != NULL); + + uint32_t start_idx; + int desc_list_len; + switch (pipe->ep_char.type) { + case USB_PRIV_XFER_TYPE_CTRL: { + start_idx = 0; + desc_list_len = XFER_LIST_LEN_CTRL; //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); + case USB_PRIV_XFER_TYPE_ISOCHRONOUS: { + start_idx = buffer_to_exec->flags.isoc.irp_start_idx; + desc_list_len = XFER_LIST_LEN_ISOC; + break; + } + case USB_PRIV_XFER_TYPE_BULK: { + start_idx = 0; + desc_list_len = (buffer_to_exec->flags.bulk.zero_len_packet) ? XFER_LIST_LEN_BULK : 1; + break; + } + case USB_PRIV_XFER_TYPE_INTR: { + start_idx = 0; + desc_list_len = buffer_to_exec->flags.intr.num_qtds; break; } default: { - break; //Isoc and Interrupt transfers not supported yet + start_idx = 0; + desc_list_len = 0; + abort(); + break; } } - //Claim slot - usbh_hal_chan_slot_acquire(pipe->chan_obj, pipe->xfer_desc_list, pipe->flags.xfer_desc_list_len, (void *)pipe); + //Update buffer and multi buffer flags + buffer_to_exec->status_flags.executing = 1; + pipe->multi_buffer_control.buffer_is_executing = 1; + usbh_hal_chan_activate(pipe->chan_obj, buffer_to_exec->xfer_desc_list, desc_list_len, start_idx); } -static void _xfer_desc_list_continue(pipe_t *pipe) +static bool _buffer_check_done(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) { + if (pipe->ep_char.type != USB_PRIV_XFER_TYPE_CTRL) { + return true; + } + //Only control transfers need to be continued + dma_buffer_block_t *buffer_inflight = pipe->buffers[pipe->multi_buffer_control.rd_idx]; + bool next_dir_is_in; + int next_pid; + if (buffer_inflight->flags.ctrl.cur_stg == 0) { //Just finished control stage + if (buffer_inflight->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 + buffer_inflight->flags.ctrl.cur_stg = 2; //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_dir_is_in = buffer_inflight->flags.ctrl.data_stg_in; next_pid = 1; //Data stage always starts with a PID of DATA1 - num_to_skip = 0; + buffer_inflight->flags.ctrl.cur_stg = 1; } - } 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 + } else if (buffer_inflight->flags.ctrl.cur_stg == 1) { //Just finished data stage. Go to status stage + next_dir_is_in = !buffer_inflight->flags.ctrl.data_stg_in; //Status stage is always the opposite direction of data stage next_pid = 1; //Status stage always has a PID of DATA1 - num_to_skip = 0; + buffer_inflight->flags.ctrl.cur_stg = 2; + } else { //Just finished status stage. Transfer is complete + return true; } - + //Continue the control transfer 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 + usbh_hal_chan_activate(pipe->chan_obj, buffer_inflight->xfer_desc_list, XFER_LIST_LEN_CTRL, buffer_inflight->flags.ctrl.cur_stg); + return false; } -static void _xfer_desc_list_parse(pipe_t *pipe, bool error_occurred) +static inline void _buffer_parse_ctrl(dma_buffer_block_t *buffer) { - assert(pipe->inflight_irp != 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 *irp = pipe->inflight_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_IRP_NOT_AVAIL should never occur - abort(); - break; - } - } - //We assume no bytes transmitted because of an error. - xfer_rem_len = irp->num_bytes; + usb_irp_t *irp = buffer->irp; + //Update IRP's actual number of bytes + if (buffer->flags.ctrl.data_stg_skip) { + //There was no data stage. Just set the actual length to zero + irp->actual_num_bytes = 0; } else { + //Parse the data stage for the remaining length + int rem_len; int desc_status; + usbh_hal_xfer_desc_parse(buffer->xfer_desc_list, 1, &rem_len, &desc_status); + assert(desc_status == USBH_HAL_XFER_DESC_STS_SUCCESS); + assert(rem_len <= irp->num_bytes); + irp->actual_num_bytes = irp->num_bytes - rem_len; + } + //Update IRP status + irp->status = USB_TRANSFER_STATUS_COMPLETED; + //Clear the descriptor list + memset(buffer->xfer_desc_list, XFER_LIST_LEN_CTRL, sizeof(usbh_ll_dma_qtd_t)); +} + +static inline void _buffer_parse_bulk(dma_buffer_block_t *buffer) +{ + usb_irp_t *irp = buffer->irp; + //Update IRP's actual number of bytes + int rem_len; + int desc_status; + usbh_hal_xfer_desc_parse(buffer->xfer_desc_list, 0, &rem_len, &desc_status); + assert(desc_status == USBH_HAL_XFER_DESC_STS_SUCCESS); + assert(rem_len <= irp->num_bytes); + irp->actual_num_bytes = irp->num_bytes - rem_len; + //Update IRP's status + irp->status = USB_TRANSFER_STATUS_COMPLETED; + //Clear the descriptor list + memset(buffer->xfer_desc_list, XFER_LIST_LEN_BULK, sizeof(usbh_ll_dma_qtd_t)); +} + +static inline void _buffer_parse_intr(dma_buffer_block_t *buffer, bool is_in, int mps) +{ + usb_irp_t *irp = buffer->irp; + int intr_stop_idx = buffer->status_flags.stop_idx; + if (is_in) { + if (intr_stop_idx > 0) { //This is an early stop (short packet) + assert(intr_stop_idx <= buffer->flags.intr.num_qtds); + int rem_len; + int desc_status; + for (int i = 0; i < intr_stop_idx - 1; i++) { //Check all packets before the short + usbh_hal_xfer_desc_parse(buffer->xfer_desc_list, i, &rem_len, &desc_status); + assert(rem_len == 0 && desc_status == USBH_HAL_XFER_DESC_STS_SUCCESS); + } + //Check the short packet + usbh_hal_xfer_desc_parse(buffer->xfer_desc_list, intr_stop_idx - 1, &rem_len, &desc_status); + assert(rem_len > 0 && desc_status == USBH_HAL_XFER_DESC_STS_SUCCESS); + //Update actual bytes + irp->actual_num_bytes = (mps * intr_stop_idx - 2) + (mps - rem_len); + } else { + //Check that all but the last packet transmitted MPS + for (int i = 0; i < buffer->flags.intr.num_qtds - 1; i++) { + int rem_len; + int desc_status; + usbh_hal_xfer_desc_parse(buffer->xfer_desc_list, i, &rem_len, &desc_status); + assert(rem_len == 0 && desc_status == USBH_HAL_XFER_DESC_STS_SUCCESS); + } + //Check the last packet + int last_packet_rem_len; + int last_packet_desc_status; + usbh_hal_xfer_desc_parse(buffer->xfer_desc_list, buffer->flags.intr.num_qtds - 1, &last_packet_rem_len, &last_packet_desc_status); + assert(last_packet_desc_status == USBH_HAL_XFER_DESC_STS_SUCCESS); + //All packets except last MUST be MPS. So just deduct the remaining length of the last packet to get actual number of bytes + irp->actual_num_bytes = irp->num_bytes - last_packet_rem_len; + } + } else { + //OUT INTR transfers can only complete successfully if all MPS packets have been transmitted. Double check + for (int i = 0 ; i < buffer->flags.intr.num_qtds; i++) { + int rem_len; + int desc_status; + usbh_hal_xfer_desc_parse(buffer->xfer_desc_list, i, &rem_len, &desc_status); + assert(rem_len == 0 && desc_status == USBH_HAL_XFER_DESC_STS_SUCCESS); + } + irp->actual_num_bytes = irp->num_bytes; + } + //Update IRP's status + irp->status = USB_TRANSFER_STATUS_COMPLETED; + //Clear the descriptor list + memset(buffer->xfer_desc_list, XFER_LIST_LEN_INTR, sizeof(usbh_ll_dma_qtd_t)); +} + +static inline void _buffer_parse_isoc(dma_buffer_block_t *buffer, bool is_in) +{ + usb_irp_t *irp = buffer->irp; + int desc_idx = buffer->flags.isoc.irp_start_idx; //Descriptor index tracks which descriptor in the QTD list + for (int pkt_idx = 0; pkt_idx < irp->num_iso_packets; pkt_idx++) { + //Clear the filled descriptor + int rem_len; + int desc_status; + usbh_hal_xfer_desc_parse(buffer->xfer_desc_list, desc_idx, &rem_len, &desc_status); + usbh_hal_xfer_desc_clear(buffer->xfer_desc_list, desc_idx); + assert(rem_len == 0 || is_in); + assert(desc_status == USBH_HAL_XFER_DESC_STS_SUCCESS || USBH_HAL_XFER_DESC_STS_NOT_EXECUTED); + assert(rem_len <= irp->iso_packet_desc[pkt_idx].length); //Check for DMA errata + //Update ISO packet actual length and status + irp->iso_packet_desc[pkt_idx].actual_length = irp->iso_packet_desc[pkt_idx].length - rem_len; + irp->iso_packet_desc[pkt_idx].status = (desc_status == USBH_HAL_XFER_DESC_STS_NOT_EXECUTED) ? USB_TRANSFER_STATUS_SKIPPED : USB_TRANSFER_STATUS_COMPLETED; + //A descriptor is also allocated for unscheduled frames. We need to skip over them + desc_idx += buffer->flags.isoc.interval; + if (desc_idx >= XFER_LIST_LEN_INTR) { + desc_idx -= XFER_LIST_LEN_INTR; + } + } +} + +static inline void _buffer_parse_error(dma_buffer_block_t *buffer) +{ + //The IRP had an error, so we consider that NO bytes were transferred + usb_irp_t *irp = buffer->irp; + irp->actual_num_bytes = 0; + for (int i = 0; i < irp->num_iso_packets; i++) { + irp->iso_packet_desc[i].actual_length = 0; + } + //Update status of IRP + if (buffer->status_flags.cancelled) { + irp->status = USB_TRANSFER_STATUS_CANCELED; + } else if (buffer->status_flags.pipe_state == HCD_PIPE_STATE_INVALID) { + irp->status = USB_TRANSFER_STATUS_NO_DEVICE; + } else { + switch (buffer->status_flags.pipe_event) { + case HCD_PIPE_EVENT_ERROR_XFER: //Excessive transaction error + irp->status = USB_TRANSFER_STATUS_ERROR; + break; + case HCD_PIPE_EVENT_ERROR_OVERFLOW: + irp->status = USB_TRANSFER_STATUS_OVERFLOW; + break; + case HCD_PIPE_EVENT_ERROR_STALL: + irp->status = USB_TRANSFER_STATUS_STALL; + break; + case HCD_PIPE_EVENT_IRP_DONE: //Special case where we are cancelling an IRP due to pipe_retire + irp->status = USB_TRANSFER_STATUS_CANCELED; + break; + default: + //HCD_PIPE_EVENT_ERROR_IRP_NOT_AVAIL should never occur + abort(); + break; + } + } + //Clear error flags + buffer->status_flags.val = 0; +} + +static void _buffer_parse(pipe_t *pipe) +{ + assert(pipe->multi_buffer_control.buffer_num_to_parse > 0); + dma_buffer_block_t *buffer_to_parse = pipe->buffers[pipe->multi_buffer_control.fr_idx]; + assert(buffer_to_parse->irp != NULL); + bool is_in = pipe->ep_char.bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK; + int mps = pipe->ep_char.mps; + + //Parsing the buffer will update the buffer's corresponding IRP + if (buffer_to_parse->status_flags.error_occurred) { + _buffer_parse_error(buffer_to_parse); + } else { 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); - } + case USB_PRIV_XFER_TYPE_CTRL: { + _buffer_parse_ctrl(buffer_to_parse); break; } - case USB_XFER_TYPE_BULK: { - usbh_hal_xfer_desc_parse(pipe->xfer_desc_list, 0, &xfer_rem_len, &desc_status); + case USB_PRIV_XFER_TYPE_ISOCHRONOUS: { + _buffer_parse_isoc(buffer_to_parse, is_in); + break; + } + case USB_PRIV_XFER_TYPE_BULK: { + _buffer_parse_bulk(buffer_to_parse); + break; + } + case USB_PRIV_XFER_TYPE_INTR: { + _buffer_parse_intr(buffer_to_parse, is_in, mps); 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 - irp->actual_num_bytes = irp->num_bytes - xfer_rem_len; - irp->status = xfer_status; + usb_irp_t *irp = buffer_to_parse->irp; + IRP_STATE_SET(irp->reserved_flags, IRP_STATE_DONE); + buffer_to_parse->irp = NULL; + buffer_to_parse->flags.val = 0; //Clear flags + //Move the IRP to the done tailq + TAILQ_INSERT_TAIL(&pipe->done_irp_tailq, irp, tailq_entry); + pipe->num_irp_done++; + //Update multi buffer flags + pipe->multi_buffer_control.fr_idx++; + pipe->multi_buffer_control.buffer_num_to_parse--; + pipe->multi_buffer_control.buffer_num_to_fill++; } +static void _buffer_flush_all(pipe_t *pipe, bool cancelled) +{ + int cur_num_to_mark_done = pipe->multi_buffer_control.buffer_num_to_exec; + for (int i = 0; i < cur_num_to_mark_done; i++) { + //Mark any filled buffers as done + _buffer_done_error(pipe, 0, pipe->state, pipe->last_event, cancelled); + } + int cur_num_to_parse = pipe->multi_buffer_control.buffer_num_to_parse; + for (int i = 0; i < cur_num_to_parse; i++) { + _buffer_parse(pipe); + } + //At this point, there should be no more filled buffers. Only IRPs in the pending or done tailq +} + +// ---------------------------------------------- HCD Transfer Descriptors --------------------------------------------- + // ----------------------- Public -------------------------- esp_err_t hcd_irp_enqueue(hcd_pipe_handle_t pipe_hdl, usb_irp_t *irp) @@ -1802,35 +2527,31 @@ esp_err_t hcd_irp_enqueue(hcd_pipe_handle_t pipe_hdl, usb_irp_t *irp) pipe_t *pipe = (pipe_t *)pipe_hdl; HCD_ENTER_CRITICAL(); - //Check that pipe and port are in the corrrect state to receive IRPs - 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 + //Check that pipe and port are in the correct state to receive IRPs + 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->cs_flags.pipe_cmd_processing, //Pipe cannot currently be processing a pipe command ESP_ERR_INVALID_STATE); //Use the IRP's reserved_ptr to store the pipe's irp->reserved_ptr = (void *)pipe; - - //Check if we can start execution on the pipe immediately - if (!pipe->flags.paused && pipe->num_irp_pending == 0 && pipe->inflight_irp == NULL) { - //Pipe isn't executing any transfers. Start immediately - pipe->inflight_irp = irp; - _xfer_desc_list_fill(pipe); - usbh_hal_chan_activate(pipe->chan_obj, 0); //Start with the first descriptor - //use the IRP's reserved_flags to store the IRP's current state - IRP_STATE_SET(irp->reserved_flags, IRP_STATE_INFLIGHT); - if (pipe->num_irp_done == 0) { - //This is the first IRP to be enqueued into the pipe. Move the pipe to the list of active pipes - TAILQ_REMOVE(&pipe->port->pipes_idle_tailq, pipe, tailq_entry); - TAILQ_INSERT_TAIL(&pipe->port->pipes_active_tailq, pipe, tailq_entry); - pipe->port->num_pipes_idle--; - pipe->port->num_pipes_queued++; - } - } else { - //Add the IRP to the pipe's pending tailq - TAILQ_INSERT_TAIL(&pipe->pending_irp_tailq, irp, tailq_entry); - pipe->num_irp_pending++; - //use the IRP's reserved_flags to store the IRP's current state - IRP_STATE_SET(irp->reserved_flags, IRP_STATE_PENDING); + //Add the IRP to the pipe's pending tailq + IRP_STATE_SET(irp->reserved_flags, IRP_STATE_PENDING); + TAILQ_INSERT_TAIL(&pipe->pending_irp_tailq, irp, tailq_entry); + pipe->num_irp_pending++; + //use the IRP's reserved_flags to store the IRP's current state + if (_buffer_can_fill(pipe)) { + _buffer_fill(pipe); + } + if (_buffer_can_exec(pipe)) { + _buffer_exec(pipe); + } + if (!pipe->cs_flags.is_active) { + //This is the first IRP to be enqueued into the pipe. Move the pipe to the list of active pipes + TAILQ_REMOVE(&pipe->port->pipes_idle_tailq, pipe, tailq_entry); + TAILQ_INSERT_TAIL(&pipe->port->pipes_active_tailq, pipe, tailq_entry); + pipe->port->num_pipes_idle--; + pipe->port->num_pipes_queued++; + pipe->cs_flags.is_active = 1; } HCD_EXIT_CRITICAL(); return ESP_OK; @@ -1850,12 +2571,15 @@ usb_irp_t *hcd_irp_dequeue(hcd_pipe_handle_t pipe_hdl) assert(irp->reserved_ptr == (void *)pipe && IRP_STATE_GET(irp->reserved_flags) == IRP_STATE_DONE); //The IRP's reserved field should have been set to this pipe irp->reserved_ptr = NULL; IRP_STATE_SET(irp->reserved_flags, IRP_STATE_IDLE); - if (pipe->num_irp_done == 0 && pipe->num_irp_pending == 0) { + if (pipe->cs_flags.is_active + && pipe->num_irp_pending == 0 && pipe->num_irp_done == 0 + && pipe->multi_buffer_control.buffer_num_to_exec == 0 && pipe->multi_buffer_control.buffer_num_to_parse == 0) { //This pipe has no more enqueued IRPs. Move the pipe to the list of idle pipes TAILQ_REMOVE(&pipe->port->pipes_active_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--; + pipe->cs_flags.is_active = 0; } } else { //No more IRPs to dequeue from this pipe @@ -1881,9 +2605,15 @@ esp_err_t hcd_irp_abort(usb_irp_t *irp) //Add it to the done queue TAILQ_INSERT_TAIL(&pipe->done_irp_tailq, irp, tailq_entry); pipe->num_irp_done++; - //Update the IRP's current state and status + //Update the IRP's current state, status, and actual length IRP_STATE_SET(irp->reserved_flags, IRP_STATE_DONE); - irp->status = USB_TRANSFER_STATUS_CANCELLED; + irp->actual_num_bytes = 0; + irp->status = USB_TRANSFER_STATUS_CANCELED; + //If this is an ISOC IRP, update the ISO packet descriptors as well + for (int i = 0; i < irp->num_iso_packets; i++) { + irp->iso_packet_desc[i].actual_length = 0; + irp->iso_packet_desc[i].status = USB_TRANSFER_STATUS_CANCELED; + } }// Otherwise, the IRP is in-flight or already done thus cannot be aborted HCD_EXIT_CRITICAL(); return ESP_OK; diff --git a/components/usb/maintainers.md b/components/usb/maintainers.md index 849fae716d..ab243489b4 100644 --- a/components/usb/maintainers.md +++ b/components/usb/maintainers.md @@ -31,11 +31,19 @@ The HAL layer abstracts the DWC_OTG operating in Host Mode using Internal Scatte ## 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). +- 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 halted (i.e., not actively executing transfer descriptors). + - Use `usbh_hal_chan_alloc()` to allocate a channel + - Once allocated, use `usbh_hal_chan_set_ep_char()` to set the Endpoint characteristics of the channel (i.e., the information of the endpoint that the channel is communicating with). There are also some `usbh_hal_chan_set...()` functions to change a particular characteristic. + - Once the channel is no longer needed, call `usbh_hal_chan_free()` to free the channel +- Channels use a list of Queue Transfer Descriptors (QTDs) to executed USB transfers. + - A transfer descriptor list must be filled using `usbh_hal_xfer_desc_fill()` + - Once filled, a channel can be activated using `usbh_hal_chan_activate()` + - Once the channel is done (i.e., a descriptor with the `USBH_HAL_XFER_DESC_FLAG_HOC` is executed), a `USBH_HAL_CHAN_EVENT_CPLT` event is generated. The channel is now halted + - Call `usbh_hal_xfer_desc_parse()` to parse the results of the descriptor list + - If you need to halt the channel early (such as aborting a transfer), call `usbh_hal_chan_request_halt()` +- In case of a channel error event: + - Call `usbh_hal_chan_get_error()` to get the specific channel error that occurred + - You must call `usbh_hal_chan_clear_error()` after an error to clear the error and allow the channel to continue to be used. # Host Controller Driver (HCD) @@ -49,16 +57,18 @@ 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. +- If you are connecting to a device with a large MPS requirements (e.g., Isochronous transfers), you may need to call `hcd_port_set_fifo_bias()` to adjust the size of the internal FIFO ## 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 +- 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. +- The FIFO bias of a port can be set using `hcd_port_set_fifo_bias()`. Biasing the FIFO will affect the permissible MPS sizes of pipes. For example, if the connected device has an IN endpoint with large MPS (e.g., 512 bytes), the FIFO should be biased as `HCD_PORT_FIFO_BIAS_RX`. ## HCD Pipes diff --git a/components/usb/private_include/hcd.h b/components/usb/private_include/hcd.h index 48d4547cb2..9e692f1d9d 100644 --- a/components/usb/private_include/hcd.h +++ b/components/usb/private_include/hcd.h @@ -38,7 +38,7 @@ extern "C" { */ 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_DISCONNECTED, /**< The port is powered but no device is connected */ 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. */ @@ -74,12 +74,12 @@ typedef enum { * 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_NONE, /**< No event has occurred. 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 + HCD_PORT_EVENT_SUDDEN_DISCONN, /**< The port has suddenly disconnected (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; @@ -152,6 +152,12 @@ typedef bool (*hcd_port_isr_callback_t)(hcd_port_handle_t port_hdl, hcd_port_eve */ 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); +typedef enum { + HCD_PORT_FIFO_BIAS_BALANCED, /**< Balanced FIFO sizing for RX, Non-periodic TX, and periodic TX */ + HCD_PORT_FIFO_BIAS_RX, /**< Bias towards a large RX FIFO */ + HCD_PORT_FIFO_BIAS_PTX, /**< Bias towards periodic TX FIFO */ +} hcd_port_fifo_bias_t; + /** * @brief HCD configuration structure */ @@ -165,7 +171,7 @@ typedef struct { typedef struct { hcd_port_isr_callback_t callback; /**< HCD port event callback */ void *callback_arg; /**< User argument for HCD port callback */ - void *context; + void *context; /**< Context variable used to associate the port with upper layer object */ } hcd_port_config_t; /** @@ -177,9 +183,9 @@ 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 */ + const usb_desc_ep_t *ep_desc; /**< Pointer to endpoint descriptor of the pipe */ usb_speed_t dev_speed; /**< Speed of the device */ + uint8_t dev_addr; /**< Device address of the pipe */ } hcd_pipe_config_t; // --------------------------------------------- Host Controller Driver ------------------------------------------------ @@ -297,7 +303,7 @@ esp_err_t hcd_port_get_speed(hcd_port_handle_t port_hdl, usb_speed_t *speed); * * 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 + * @note If callbacks are not used, this function can also be used in a polling manner to repeatedly 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. @@ -325,7 +331,22 @@ esp_err_t hcd_port_recover(hcd_port_handle_t port_hdl); * @param port_hdl Port handle * @return void* Context variable */ -void *hcd_port_get_ctx(hcd_port_handle_t port_hdl); +void *hcd_port_get_context(hcd_port_handle_t port_hdl); + +/** + * @brief Set the bias of the HCD port's internal FIFO + * + * @note This function can only be called when the following conditions are met: + * - Port is initialized + * - Port does not have any pending events + * - Port does not have any allocated pipes + * + * @param port_hdl Port handle + * @param bias Fifo bias + * @retval ESP_OK FIFO sizing successfully set + * @retval ESP_ERR_INVALID_STATE Incorrect state for FIFO sizes to be set + */ +esp_err_t hcd_port_set_fifo_bias(hcd_port_handle_t port_hdl, hcd_port_fifo_bias_t bias); // --------------------------------------------------- HCD Pipes ------------------------------------------------------- @@ -346,7 +367,7 @@ void *hcd_port_get_ctx(hcd_port_handle_t port_hdl); * @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 + * @retval ESP_ERR_NOT_SUPPORTED: The pipe's configuration 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); @@ -365,22 +386,38 @@ esp_err_t hcd_pipe_alloc(hcd_port_handle_t port_hdl, const hcd_pipe_config_t *pi esp_err_t hcd_pipe_free(hcd_pipe_handle_t pipe_hdl); /** - * @brief Update a pipe's device address and maximum packet size + * @brief Update a pipe's maximum packet size + * + * This function is intended to be called on default pipes during enumeration in order to update the pipe's 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 IRPs have been dequeued from the pipe + * + * @param pipe_hdl Pipe handle + * @param mps New Maximum Packet Size + * + * @retval ESP_OK: Pipe successfully updated + * @retval ESP_ERR_INVALID_STATE: Pipe is not in a condition to be updated + */ +esp_err_t hcd_pipe_update_mps(hcd_pipe_handle_t pipe_hdl, int mps); + +/** + * @brief Update a pipe's device address * * 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: + * address. 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 IRPs 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 + * @retval ESP_ERR_INVALID_STATE: Pipe is not in a condition to be updated */ -esp_err_t hcd_pipe_update(hcd_pipe_handle_t pipe_hdl, uint8_t dev_addr, int mps); +esp_err_t hcd_pipe_update_dev_addr(hcd_pipe_handle_t pipe_hdl, uint8_t dev_addr); /** * @brief Get the context variable of a pipe from its handle @@ -388,7 +425,7 @@ esp_err_t hcd_pipe_update(hcd_pipe_handle_t pipe_hdl, uint8_t dev_addr, int mps) * @param pipe_hdl Pipe handle * @return void* Context variable */ -void *hcd_pipe_get_ctx(hcd_pipe_handle_t pipe_hdl); +void *hcd_pipe_get_context(hcd_pipe_handle_t pipe_hdl); /** * @brief Get the current sate of the pipe @@ -406,8 +443,8 @@ hcd_pipe_state_t hcd_pipe_get_state(hcd_pipe_handle_t pipe_hdl); * - 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 IRP is complete. If the pipe's state - * changes unexpectedley, this function will return ESP_ERR_INVALID_RESPONSE + * @note Some pipe commands will block until the pipe's current in-flight IRP is complete. If the pipe's state + * changes unexpectedly, this function will return ESP_ERR_INVALID_RESPONSE * * @param pipe_hdl Pipe handle * @param command Pipe command @@ -449,7 +486,7 @@ esp_err_t hcd_irp_enqueue(hcd_pipe_handle_t pipe_hdl, usb_irp_t *irp); * @brief Dequeue an IRP from a particular pipe * * This function should be called on a pipe after a pipe receives a HCD_PIPE_EVENT_IRP_DONE event. If a pipe has - * multiple IRPs that can be dequeued, this function should be called repeatedely until all IRPs are dequeued. If a pipe + * multiple IRPs that can be dequeued, this function should be called repeatedly until all IRPs are dequeued. If a pipe * has no more IRPs to dequeue, this function will return NULL. * * @param pipe_hdl Pipe handle @@ -461,7 +498,7 @@ usb_irp_t *hcd_irp_dequeue(hcd_pipe_handle_t pipe_hdl); * @brief Abort an enqueued IRP * * This function will attempt to abort an IRP that is already enqueued. If the IRP has yet to be executed, it will be - * "cancelled" and can then be dequeued. If the IRP is currenty inflight or has already completed, the IRP will not be + * "cancelled" and can then be dequeued. If the IRP is currenty in-flight or has already completed, the IRP will not be * affected by this function. * * @param irp I/O Request Packet to abort diff --git a/components/usb/private_include/usb.h b/components/usb/private_include/usb.h index fe26f1c8b0..3abe2801f6 100644 --- a/components/usb/private_include/usb.h +++ b/components/usb/private_include/usb.h @@ -54,10 +54,10 @@ typedef enum { * @note The enum values need to match the bmAttributes field of an EP descriptor */ typedef enum { - USB_XFER_TYPE_CTRL = 0, - USB_XFER_TYPE_ISOCHRONOUS, - USB_XFER_TYPE_BULK, - USB_XFER_TYPE_INTR, + USB_TRANSFER_TYPE_CTRL = 0, + USB_TRANSFER_TYPE_ISOCHRONOUS, + USB_TRANSFER_TYPE_BULK, + USB_TRANSFER_TYPE_INTR, } usb_transfer_type_t; /** @@ -67,10 +67,11 @@ typedef enum { 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 a time out */ - USB_TRANSFER_STATUS_CANCELLED, /**< The transfer was canceled */ + USB_TRANSFER_STATUS_CANCELED, /**< The transfer was canceled */ 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., disconnected */ USB_TRANSFER_STATUS_OVERFLOW, /**< The transfer as more data was sent than was requested */ + USB_TRANSFER_STATUS_SKIPPED, /**< ISOC only. The packet was skipped due to system latency */ } usb_transfer_status_t; /** @@ -87,6 +88,8 @@ typedef struct { usb_transfer_status_t status; /**< Status of the packet */ } usb_iso_packet_desc_t; +#define USB_IRP_FLAG_ZERO_PACK 0x01 /**< (For bulk OUT only). Indicates that a bulk OUT transfers should always terminate with a short packet, even if it means adding an extra zero length packet */ + /** * @brief USB IRP (I/O Request Packet). See USB2.0 Spec * @@ -114,8 +117,9 @@ struct usb_irp_obj { uint8_t *data_buffer; /**< Pointer to data buffer. Must be DMA capable memory */ 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 */ + uint32_t flags; /**< IRP flags */ usb_transfer_status_t status; /**< Status of the transfer */ - uint32_t timeout; /**< Timeout (in milliseconds) of the packet */ + uint32_t timeout; /**< Timeout (in milliseconds) of the packet (currently not supported yet) */ void *context; /**< Context variable used to associate the IRP object with another object */ 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 */ @@ -125,6 +129,15 @@ typedef struct usb_irp_obj usb_irp_t; // ---------------------------------------------------- Chapter 9 ------------------------------------------------------ +#define USB_B_DESCRIPTOR_TYPE_DEVICE 1 +#define USB_B_DESCRIPTOR_TYPE_CONFIGURATION 2 +#define USB_B_DESCRIPTOR_TYPE_STRING 3 +#define USB_B_DESCRIPTOR_TYPE_INTERFACE 4 +#define USB_B_DESCRIPTOR_TYPE_ENDPOINT 5 +#define USB_B_DESCRIPTOR_TYPE_DEVICE_QUALIFIER 6 +#define USB_B_DESCRIPTOR_TYPE_OTHER_SPEED_CONFIGURATION 7 +#define USB_B_DESCRIPTOR_TYPE_INTERFACE_POWER 8 + // ------------------- Control Request --------------------- /** @@ -250,12 +263,23 @@ _Static_assert(sizeof(usb_ctrl_req_t) == USB_CTRL_REQ_SIZE, "Size of usb_ctrl_re (ctrl_req_ptr)->wLength = 0; \ }) +/** + * @brief Initializer for a request to set an interface's alternate setting + */ +#define USB_CTRL_REQ_INIT_SET_INTERFACE(ctrl_req_ptr, intf_num, alt_setting_num) ({ \ + (ctrl_req_ptr)->bRequestType = USB_B_REQUEST_TYPE_DIR_OUT | USB_B_REQUEST_TYPE_TYPE_STANDARD | USB_B_REQUEST_TYPE_RECIP_INTERFACE; \ + (ctrl_req_ptr)->bRequest = USB_B_REQUEST_SET_INTERFACE; \ + (ctrl_req_ptr)->wValue = (alt_setting_num); \ + (ctrl_req_ptr)->wIndex = (intf_num); \ + (ctrl_req_ptr)->wLength = 0; \ +}) + // ------------------ Device Descriptor -------------------- /** * @brief Size of a USB device descriptor in bytes */ -#define USB_DESC_DEV_SIZE 18 +#define USB_DESC_DEVC_SIZE 18 /** * @brief Structure representing a USB device descriptor @@ -277,9 +301,9 @@ typedef union { uint8_t iSerialNumber; uint8_t bNumConfigurations; } USB_DESC_ATTR; - uint8_t val[USB_DESC_DEV_SIZE]; + uint8_t val[USB_DESC_DEVC_SIZE]; } usb_desc_devc_t; -_Static_assert(sizeof(usb_desc_devc_t) == USB_DESC_DEV_SIZE, "Size of usb_desc_devc_t incorrect"); +_Static_assert(sizeof(usb_desc_devc_t) == USB_DESC_DEVC_SIZE, "Size of usb_desc_devc_t incorrect"); /** * @brief Possible base class values of the bDeviceClass field of a USB device descriptor diff --git a/components/usb/test/CMakeLists.txt b/components/usb/test/CMakeLists.txt index d02b3aef31..6a7f671f06 100644 --- a/components/usb/test/CMakeLists.txt +++ b/components/usb/test/CMakeLists.txt @@ -5,7 +5,7 @@ if(NOT "${target}" STREQUAL "esp32s2") return() endif() -idf_component_register(SRC_DIRS "." - PRIV_INCLUDE_DIRS "." "../private_include" +idf_component_register(SRC_DIRS "hcd" + PRIV_INCLUDE_DIRS "../private_include" "." "hcd" PRIV_REQUIRES cmock usb test_utils ) diff --git a/components/usb/test/hcd/test_hcd_bulk.c b/components/usb/test/hcd/test_hcd_bulk.c new file mode 100644 index 0000000000..f3cbda5df7 --- /dev/null +++ b/components/usb/test/hcd/test_hcd_bulk.c @@ -0,0 +1,262 @@ +// 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 +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "unity.h" +#include "test_utils.h" +#include "test_hcd_common.h" + +// ------------------------------------------------- Mock MSC SCSI ----------------------------------------------------- + +/* +Note: The following test requires that USB flash drive be connected. The flash drive should... + +- Be implement the Mass Storage class supporting BULK only transfers using SCSI commands +- It's configuration 1 should have the following endpoints + +Endpoint Descriptor: + bLength 7 + bDescriptorType 5 + bEndpointAddress 0x01 EP 1 OUT + bmAttributes 2 + Transfer Type Bulk + Synch Type None + Usage Type Data + wMaxPacketSize 0x0040 1x 64 bytes + bInterval 1 +Endpoint Descriptor: + bLength 7 + bDescriptorType 5 + bEndpointAddress 0x82 EP 2 IN + bmAttributes 2 + Transfer Type Bulk + Synch Type None + Usage Type Data + wMaxPacketSize 0x0040 1x 64 bytes + bInterval 1 + +If you're using a flash driver with different endpoints, modify the endpoint descriptors below. +*/ + +static const usb_desc_ep_t bulk_out_ep_desc = { + .bLength = sizeof(usb_desc_ep_t), + .bDescriptorType = USB_B_DESCRIPTOR_TYPE_ENDPOINT, + .bEndpointAddress = 0x01, //EP 1 OUT + .bmAttributes = USB_BM_ATTRIBUTES_XFER_BULK, + .wMaxPacketSize = 64, //MPS of 64 bytes + .bInterval = 1, +}; + +static const usb_desc_ep_t bulk_in_ep_desc = { + .bLength = sizeof(usb_desc_ep_t), + .bDescriptorType = USB_B_DESCRIPTOR_TYPE_ENDPOINT, + .bEndpointAddress = 0x82, //EP 2 IN + .bmAttributes = USB_BM_ATTRIBUTES_XFER_BULK, + .wMaxPacketSize = 64, //MPS of 64 bytes + .bInterval = 1, +}; + +#define MOCK_MSC_SCSI_SECTOR_SIZE 512 +#define MOCK_MSC_SCSI_LUN 0 +#define MSC_SCSI_INTR_NUMBER 0 + +#define MOCK_MSC_SCSI_REQ_INIT_RESET(ctrl_req_ptr, intf_num) ({ \ + (ctrl_req_ptr)->bRequestType = USB_B_REQUEST_TYPE_DIR_OUT | USB_B_REQUEST_TYPE_TYPE_CLASS | USB_B_REQUEST_TYPE_RECIP_INTERFACE; \ + (ctrl_req_ptr)->bRequest = 0xFF; \ + (ctrl_req_ptr)->wValue = 0; \ + (ctrl_req_ptr)->wIndex = (intf_num); \ + (ctrl_req_ptr)->wLength = 0; \ +}) + +typedef struct __attribute__((packed)) { + uint8_t opcode; //0x28 = read(10), 0x2A=write(10) + uint8_t flags; + uint8_t lba_3; + uint8_t lba_2; + uint8_t lba_1; + uint8_t lba_0; + uint8_t group; + uint8_t len_1; + uint8_t len_0; + uint8_t control; +} mock_scsi_cmd10_t; + +typedef struct __attribute__((packed)) { + uint32_t dCBWSignature; + uint32_t dCBWTag; + uint32_t dCBWDataTransferLength; + uint8_t bmCBWFlags; + uint8_t bCBWLUN; + uint8_t bCBWCBLength; + mock_scsi_cmd10_t CBWCB; + uint8_t padding[6]; +} mock_msc_bulk_cbw_t; + +// USB Bulk Transfer Command Status Wrapper data +typedef struct __attribute__((packed)) { + uint32_t dCSWSignature; + uint32_t dCSWTag; + uint32_t dCSWDataResidue; + uint8_t bCSWStatus; +} mock_msc_bulk_csw_t; + +static void mock_msc_reset_req(hcd_pipe_handle_t default_pipe) +{ + //Create IRP + usb_irp_t *irp = heap_caps_calloc(1, sizeof(usb_irp_t), MALLOC_CAP_DEFAULT); + TEST_ASSERT_NOT_EQUAL(NULL, irp); + irp->data_buffer = heap_caps_malloc(sizeof(usb_ctrl_req_t), MALLOC_CAP_DMA); + TEST_ASSERT_NOT_EQUAL(NULL, irp->data_buffer); + usb_ctrl_req_t *ctrl_req = (usb_ctrl_req_t *)irp->data_buffer; + MOCK_MSC_SCSI_REQ_INIT_RESET(ctrl_req, MSC_SCSI_INTR_NUMBER); + irp->num_bytes = 0; + //Enqueue, wait, dequeue, and check IRP + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp)); + test_hcd_expect_pipe_event(default_pipe, HCD_PIPE_EVENT_IRP_DONE); + TEST_ASSERT_EQUAL(irp, hcd_irp_dequeue(default_pipe)); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->status); + //Free IRP + heap_caps_free(irp->data_buffer); + heap_caps_free(irp); +} + +static void mock_msc_scsi_init_cbw(mock_msc_bulk_cbw_t *cbw, bool is_read, int offset, int num_sectors, uint32_t tag) +{ + cbw->dCBWSignature = 0x43425355; //Fixed value + cbw->dCBWTag = tag; //Random value that is echoed back + cbw->dCBWDataTransferLength = num_sectors * MOCK_MSC_SCSI_SECTOR_SIZE; + cbw->bmCBWFlags = (is_read) ? (1 << 7) : 0; //If this is a read, set the direction flag + cbw->bCBWLUN = MOCK_MSC_SCSI_LUN; + cbw->bCBWCBLength = 10; //The length of the SCSI command + //Initialize SCSI CMD as READ10 or WRITE 10 + cbw->CBWCB.opcode = (is_read) ? 0x28 : 0x2A; //SCSI CMD READ10 or WRITE10 + cbw->CBWCB.flags = 0; + cbw->CBWCB.lba_3 = (offset >> 24); + cbw->CBWCB.lba_2 = (offset >> 16); + cbw->CBWCB.lba_1 = (offset >> 8); + cbw->CBWCB.lba_0 = (offset >> 0); + cbw->CBWCB.group = 0; + cbw->CBWCB.len_1 = (num_sectors >> 8); + cbw->CBWCB.len_0 = (num_sectors >> 0); + cbw->CBWCB.control = 0; +} + +static bool mock_msc_scsi_check_csw(mock_msc_bulk_csw_t *csw, uint32_t tag_expect) +{ + bool no_issues = true; + if (csw->dCSWSignature != 0x53425355) { + no_issues = false; + printf("Warning: csw signature corrupt (0x%X)\n", csw->dCSWSignature); + } + if (csw->dCSWTag != tag_expect) { + no_issues = false; + printf("Warning: csw tag unexpected! Expected %d got %d\n", tag_expect, csw->dCSWTag); + } + if (csw->dCSWDataResidue) { + no_issues = false; + printf("Warning: csw indicates data residue of %d bytes!\n", csw->dCSWDataResidue); + } + if (csw->bCSWStatus) { + no_issues = false; + printf("Warning: csw indicates non-good status %d!\n", csw->bCSWStatus); + } + return no_issues; +} + +// --------------------------------------------------- Test Cases ------------------------------------------------------ + +/* +Test HCD bulk pipe IRPs + +Purpose: + - Test that a bulk pipe can be created + - IRPs can be created and enqueued to the bulk pipe pipe + - Bulk pipe returns HCD_PIPE_EVENT_IRP_DONE for completed IRPs + - Test utilizes a bare bones (i.e., mock) MSC class using SCSI commands + +Procedure: + - Setup HCD and wait for connection + - Allocate default pipe and enumerate the device + - Allocate separate IRPS for CBW, Data, and CSW transfers of the MSC class + - Read TEST_NUM_SECTORS number of sectors for the mass storage device + - Expect HCD_PIPE_EVENT_IRP_DONE for each IRP + - Deallocate IRPs + - Teardown +*/ + +#define TEST_NUM_SECTORS 10 +#define TEST_NUM_SECTORS_PER_ITER 2 + +TEST_CASE("Test HCD bulk pipe IRPs", "[hcd][ignore]") +{ + hcd_port_handle_t port_hdl = test_hcd_setup(); //Setup the HCD and port + usb_speed_t port_speed = test_hcd_wait_for_conn(port_hdl); //Trigger a connection + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Enumerate and reset MSC SCSI device + hcd_pipe_handle_t default_pipe = test_hcd_pipe_alloc(port_hdl, NULL, 0, port_speed); //Create a default pipe (using a NULL EP descriptor) + uint8_t dev_addr = test_hcd_enum_devc(default_pipe); + mock_msc_reset_req(default_pipe); + + //Create BULK IN and BULK OUT pipes for SCSI + hcd_pipe_handle_t bulk_out_pipe = test_hcd_pipe_alloc(port_hdl, &bulk_out_ep_desc, dev_addr, port_speed); + hcd_pipe_handle_t bulk_in_pipe = test_hcd_pipe_alloc(port_hdl, &bulk_in_ep_desc, dev_addr, port_speed); + //Create IRPs for CBW, Data, and CSW transport. IN Buffer sizes are rounded up to nearest MPS + usb_irp_t *irp_cbw = test_hcd_alloc_irp(0, sizeof(mock_msc_bulk_cbw_t)); + usb_irp_t *irp_data = test_hcd_alloc_irp(0, TEST_NUM_SECTORS_PER_ITER * MOCK_MSC_SCSI_SECTOR_SIZE); + usb_irp_t *irp_csw = test_hcd_alloc_irp(0, sizeof(mock_msc_bulk_csw_t) + (bulk_in_ep_desc.wMaxPacketSize - (sizeof(mock_msc_bulk_csw_t) % bulk_in_ep_desc.wMaxPacketSize))); + irp_cbw->num_bytes = sizeof(mock_msc_bulk_cbw_t); + irp_data->num_bytes = TEST_NUM_SECTORS_PER_ITER * MOCK_MSC_SCSI_SECTOR_SIZE; + irp_csw->num_bytes = sizeof(mock_msc_bulk_csw_t) + (bulk_in_ep_desc.wMaxPacketSize - (sizeof(mock_msc_bulk_csw_t) % bulk_in_ep_desc.wMaxPacketSize)); + + for (int block_num = 0; block_num < TEST_NUM_SECTORS; block_num += TEST_NUM_SECTORS_PER_ITER) { + //Initialize CBW IRP, then send it on the BULK OUT pipe + mock_msc_scsi_init_cbw((mock_msc_bulk_cbw_t *)irp_cbw->data_buffer, true, block_num, TEST_NUM_SECTORS_PER_ITER, 0xAAAAAAAA); + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(bulk_out_pipe, irp_cbw)); + test_hcd_expect_pipe_event(bulk_out_pipe, HCD_PIPE_EVENT_IRP_DONE); + TEST_ASSERT_EQUAL(irp_cbw, hcd_irp_dequeue(bulk_out_pipe)); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp_cbw->status); + //Read data through BULK IN pipe + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(bulk_in_pipe, irp_data)); + test_hcd_expect_pipe_event(bulk_in_pipe, HCD_PIPE_EVENT_IRP_DONE); + TEST_ASSERT_EQUAL(irp_data, hcd_irp_dequeue(bulk_in_pipe)); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp_data->status); + //Read the CSW through BULK IN pipe + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(bulk_in_pipe, irp_csw)); + test_hcd_expect_pipe_event(bulk_in_pipe, HCD_PIPE_EVENT_IRP_DONE); + TEST_ASSERT_EQUAL(irp_csw, hcd_irp_dequeue(bulk_in_pipe)); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp_data->status); + TEST_ASSERT_EQUAL(sizeof(mock_msc_bulk_csw_t), irp_csw->actual_num_bytes); + TEST_ASSERT_EQUAL(true, mock_msc_scsi_check_csw((mock_msc_bulk_csw_t *)irp_csw->data_buffer, 0xAAAAAAAA)); + //Print the read data + printf("Block %d to %d:\n", block_num, block_num + TEST_NUM_SECTORS_PER_ITER); + for (int i = 0; i < irp_data->actual_num_bytes; i++) { + printf("0x%02x,", ((char *)irp_data->data_buffer)[i]); + } + printf("\n\n"); + } + + test_hcd_free_irp(irp_cbw); + test_hcd_free_irp(irp_data); + test_hcd_free_irp(irp_csw); + test_hcd_pipe_free(bulk_out_pipe); + test_hcd_pipe_free(bulk_in_pipe); + test_hcd_pipe_free(default_pipe); + //Cleanup + test_hcd_wait_for_disconn(port_hdl, false); + test_hcd_teardown(port_hdl); +} diff --git a/components/usb/test/hcd/test_hcd_common.c b/components/usb/test/hcd/test_hcd_common.c new file mode 100644 index 0000000000..3f1a8464ca --- /dev/null +++ b/components/usb/test/hcd/test_hcd_common.c @@ -0,0 +1,350 @@ +// 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. + + + + +//Todo: Move all the port and PHY to here +//Have a separate test file for INTR (HID), ISOC (UVC), and BULK (SCSI) +//Each test case has a fixed HW device +//Implements bare minimum for a MOCK protocol + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "test_utils.h" +#include "soc/gpio_pins.h" +#include "soc/gpio_sig_map.h" +#include "esp_intr_alloc.h" +#include "esp_err.h" +#include "esp_attr.h" +#include "esp_rom_gpio.h" +#include "hal/usbh_ll.h" +#include "usb.h" +#include "hcd.h" + +#define PORT_NUM 1 +#define EVENT_QUEUE_LEN 5 +#define ENUM_ADDR 1 //Device address to use for tests that enumerate the device +#define ENUM_CONFIG 1 //Device configuration number to use for tests that enumerate the device + +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; + +// ---------------------------------------------------- Private -------------------------------------------------------- + +/** + * @brief HCD port callback. Registered when initializing an HCD port + * + * @param port_hdl Port handle + * @param port_event Port event that triggered the callback + * @param user_arg User argument + * @param in_isr Whether callback was called in an ISR context + * @return true ISR should yield after this callback returns + * @return false No yield required (non-ISR context calls should always return false) + */ +static bool port_callback(hcd_port_handle_t port_hdl, hcd_port_event_t port_event, void *user_arg, bool in_isr) +{ + //We store the port's queue handle in the port's context variable + void *port_ctx = hcd_port_get_context(port_hdl); + QueueHandle_t port_evt_queue = (QueueHandle_t)port_ctx; + 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); +} + +/** + * @brief HCD pipe callback. Registered when allocating a HCD pipe + * + * @param pipe_hdl Pipe handle + * @param pipe_event Pipe event that triggered the callback + * @param user_arg User argument + * @param in_isr Whether the callback was called in an ISR context + * @return true ISR should yield after this callback returns + * @return false No yield required (non-ISR context calls should always return false) + */ +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; + } +} + +// ------------------------------------------------- HCD Event Test ---------------------------------------------------- + +void test_hcd_expect_port_event(hcd_port_handle_t port_hdl, hcd_port_event_t expected_event) +{ + //Get the port event queue from the port's context variable + QueueHandle_t port_evt_queue = (QueueHandle_t)hcd_port_get_context(port_hdl); + TEST_ASSERT_NOT_EQUAL(NULL, port_evt_queue); + //Wait for port callback to send an event message + port_event_msg_t msg; + xQueueReceive(port_evt_queue, &msg, portMAX_DELAY); + //Check the contents of that event message + TEST_ASSERT_EQUAL(port_hdl, msg.port_hdl); + TEST_ASSERT_EQUAL(expected_event, msg.port_event); + printf("\t-> Port event\n"); +} + +void test_hcd_expect_pipe_event(hcd_pipe_handle_t pipe_hdl, hcd_pipe_event_t expected_event) +{ + //Get the pipe's event queue from the pipe's context variable + QueueHandle_t pipe_evt_queue = (QueueHandle_t)hcd_pipe_get_context(pipe_hdl); + TEST_ASSERT_NOT_EQUAL(NULL, pipe_evt_queue); + //Wait for pipe callback to send an event message + pipe_event_msg_t msg; + xQueueReceive(pipe_evt_queue, &msg, portMAX_DELAY); + //Check the contents of that event message + TEST_ASSERT_EQUAL(pipe_hdl, msg.pipe_hdl); + TEST_ASSERT_EQUAL(expected_event, msg.pipe_event); +} + +int test_hcd_get_num_port_events(hcd_port_handle_t port_hdl) +{ + //Get the port event queue from the port's context variable + QueueHandle_t port_evt_queue = (QueueHandle_t)hcd_port_get_context(port_hdl); + TEST_ASSERT_NOT_EQUAL(NULL, port_evt_queue); + return EVENT_QUEUE_LEN - uxQueueSpacesAvailable(port_evt_queue); +} + +int test_hcd_get_num_pipe_events(hcd_pipe_handle_t pipe_hdl) +{ + //Get the pipe's event queue from the pipe's context variable + QueueHandle_t pipe_evt_queue = (QueueHandle_t)hcd_pipe_get_context(pipe_hdl); + TEST_ASSERT_NOT_EQUAL(NULL, pipe_evt_queue); + return EVENT_QUEUE_LEN - uxQueueSpacesAvailable(pipe_evt_queue); +} + +// ----------------------------------------------- Driver/Port Related ------------------------------------------------- + +void test_hcd_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 device + wrap->otg_conf.phy_sel = 0; + } else { + //Set external PHY input signals to fixed voltage levels mimicking 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; + } +} + +hcd_port_handle_t test_hcd_setup(void) +{ + //Create a queue for port callback to queue up port events + QueueHandle_t port_evt_queue = xQueueCreate(EVENT_QUEUE_LEN, sizeof(port_event_msg_t)); + TEST_ASSERT_NOT_EQUAL(NULL, port_evt_queue); + //Install HCD + hcd_config_t hcd_config = { + .intr_flags = ESP_INTR_FLAG_LEVEL1, + }; + TEST_ASSERT_EQUAL(ESP_OK, hcd_install(&hcd_config)); + //Initialize a port + hcd_port_config_t port_config = { + .callback = port_callback, + .callback_arg = (void *)port_evt_queue, + .context = (void *)port_evt_queue, + }; + hcd_port_handle_t port_hdl; + 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)); + test_hcd_force_conn_state(false, 0); //Force disconnected state on PHY + return port_hdl; +} + +void test_hcd_teardown(hcd_port_handle_t port_hdl) +{ + //Get the queue handle from the port's context variable + QueueHandle_t port_evt_queue = (QueueHandle_t)hcd_port_get_context(port_hdl); + TEST_ASSERT_NOT_EQUAL(NULL, port_evt_queue); + //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); +} + +usb_speed_t test_hcd_wait_for_conn(hcd_port_handle_t port_hdl) +{ + //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 connection\n"); + test_hcd_force_conn_state(true, pdMS_TO_TICKS(100)); //Allow for connected state on PHY + test_hcd_expect_port_event(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 connected + 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"); + } + return port_speed; +} + +void test_hcd_wait_for_disconn(hcd_port_handle_t port_hdl, 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"); + test_hcd_force_conn_state(false, pdMS_TO_TICKS(100)); //Force disconnected state on PHY + test_hcd_expect_port_event(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)); +} + +// ---------------------------------------------- Pipe Setup/Tear-down ------------------------------------------------- + +hcd_pipe_handle_t test_hcd_pipe_alloc(hcd_port_handle_t port_hdl, const usb_desc_ep_t *ep_desc, uint8_t dev_addr, usb_speed_t dev_speed) +{ + //Create a queue for pipe callback to queue up pipe events + QueueHandle_t pipe_evt_queue = xQueueCreate(EVENT_QUEUE_LEN, sizeof(pipe_event_msg_t)); + TEST_ASSERT_NOT_EQUAL(NULL, pipe_evt_queue); + printf("Creating pipe\n"); + hcd_pipe_config_t pipe_config = { + .callback = pipe_callback, + .callback_arg = (void *)pipe_evt_queue, + .context = (void *)pipe_evt_queue, + .ep_desc = ep_desc, + .dev_addr = dev_addr, + .dev_speed = dev_speed, + }; + hcd_pipe_handle_t pipe_hdl; + TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_alloc(port_hdl, &pipe_config, &pipe_hdl)); + TEST_ASSERT_NOT_EQUAL(NULL, pipe_hdl); + return pipe_hdl; +} + +void test_hcd_pipe_free(hcd_pipe_handle_t pipe_hdl) +{ + //Get the pipe's event queue from its context variable + QueueHandle_t pipe_evt_queue = (QueueHandle_t)hcd_pipe_get_context(pipe_hdl); + TEST_ASSERT_NOT_EQUAL(NULL, pipe_evt_queue); + //Free the pipe and queue + TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_free(pipe_hdl)); + vQueueDelete(pipe_evt_queue); +} + +usb_irp_t *test_hcd_alloc_irp(int num_iso_packets, size_t data_buffer_size) +{ + //Allocate list of IRPs + usb_irp_t *irp = heap_caps_calloc(1, sizeof(usb_irp_t) + (num_iso_packets * sizeof(usb_iso_packet_desc_t)), MALLOC_CAP_DEFAULT); + TEST_ASSERT_NOT_EQUAL(NULL, irp); + //Allocate data buffer for each IRP and assign them + uint8_t *data_buffer = heap_caps_malloc(data_buffer_size, MALLOC_CAP_DMA); + TEST_ASSERT_NOT_EQUAL(NULL, data_buffer); + irp->data_buffer = data_buffer; + irp->num_iso_packets = num_iso_packets; + return irp; +} + +void test_hcd_free_irp(usb_irp_t *irp) +{ + //Free data buffers of each IRP + heap_caps_free(irp->data_buffer); + //Free the IRP list + heap_caps_free(irp); +} + +uint8_t test_hcd_enum_devc(hcd_pipe_handle_t default_pipe) +{ + //We need to create an IRP for the enumeration control transfers + usb_irp_t *irp = heap_caps_calloc(1, sizeof(usb_irp_t), MALLOC_CAP_DEFAULT); + TEST_ASSERT_NOT_EQUAL(NULL, irp); + //We use a single data buffer for all control transfers during enumerations. 256 bytes should be large enough for most descriptors + irp->data_buffer = heap_caps_malloc(sizeof(usb_ctrl_req_t) + 256, MALLOC_CAP_DMA); + TEST_ASSERT_NOT_EQUAL(NULL, irp->data_buffer); + usb_ctrl_req_t *ctrl_req = (usb_ctrl_req_t *)irp->data_buffer; + + //Get the device descriptor (note that device might only return 8 bytes) + USB_CTRL_REQ_INIT_GET_DEVC_DESC(ctrl_req); + irp->num_bytes = sizeof(usb_desc_devc_t); + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp)); + test_hcd_expect_pipe_event(default_pipe, HCD_PIPE_EVENT_IRP_DONE); + TEST_ASSERT_EQUAL(irp, hcd_irp_dequeue(default_pipe)); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->status); + + //Update the MPS of the default pipe + usb_desc_devc_t *devc_desc = (usb_desc_devc_t *)(irp->data_buffer + sizeof(usb_ctrl_req_t)); + TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_update_mps(default_pipe, devc_desc->bMaxPacketSize0)); + + //Send a set address request + USB_CTRL_REQ_INIT_SET_ADDR(ctrl_req, ENUM_ADDR); //We only support one device for now so use address 1 + irp->num_bytes = 0; + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp)); + test_hcd_expect_pipe_event(default_pipe, HCD_PIPE_EVENT_IRP_DONE); + TEST_ASSERT_EQUAL(irp, hcd_irp_dequeue(default_pipe)); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->status); + + //Update address of default pipe + TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_update_dev_addr(default_pipe, ENUM_ADDR)); + + //Send a set configuration request + USB_CTRL_REQ_INIT_SET_CONFIG(ctrl_req, ENUM_CONFIG); + irp->num_bytes = 0; + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp)); + test_hcd_expect_pipe_event(default_pipe, HCD_PIPE_EVENT_IRP_DONE); + TEST_ASSERT_EQUAL(irp, hcd_irp_dequeue(default_pipe)); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->status); + + //Free IRP + heap_caps_free(irp->data_buffer); + heap_caps_free(irp); + return ENUM_ADDR; +} diff --git a/components/usb/test/hcd/test_hcd_common.h b/components/usb/test/hcd/test_hcd_common.h new file mode 100644 index 0000000000..71a06ce057 --- /dev/null +++ b/components/usb/test/hcd/test_hcd_common.h @@ -0,0 +1,151 @@ +// 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 "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "usb.h" +#include "hcd.h" + +#define IRP_CONTEXT_VAL ((void *)0xDEADBEEF) + +// ------------------------------------------------- HCD Event Test ---------------------------------------------------- + +/** + * @brief Expect (wait) for an HCD port event + * + * @param port_hdl Port handle to expect event from + * @param expected_event Port event to expect + */ +void test_hcd_expect_port_event(hcd_port_handle_t port_hdl, hcd_port_event_t expected_event); + +/** + * @brief Expect (wait) for an HCD pipe event + * + * @param pipe_hdl Pipe handle to expect event from + * @param expected_event Pipe event to expect + */ +void test_hcd_expect_pipe_event(hcd_pipe_handle_t pipe_hdl, hcd_pipe_event_t expected_event); + +/** + * @brief Get the current number of queued port events (dequeued using test_hcd_expect_port_event()) + * + * @param port_hdl Port handle + * @return int Number of port events currently queued + */ +int test_hcd_get_num_port_events(hcd_port_handle_t port_hdl); + +/** + * @brief Get the current number of queued pipe events (dequeued using test_hcd_expect_pipe_event()) + * + * @param pipe_hdl Pipe handle + * @return int Number of pipe events currently queued + */ +int test_hcd_get_num_pipe_events(hcd_pipe_handle_t pipe_hdl); + +// ----------------------------------------------- Driver/Port Related ------------------------------------------------- + +/** + * @brief For the USB PHY into the connected or disconnected state + * + * @param connected For into connected state if true, disconnected if false + * @param delay_ticks Delay in ticks before forcing state + */ +void test_hcd_force_conn_state(bool connected, TickType_t delay_ticks); + +/** + * @brief Sets up the HCD and initializes an HCD port. + * + * @return hcd_port_handle_t Port handle + */ +hcd_port_handle_t test_hcd_setup(void); + +/** + * @brief Frees and HCD port and uninstalls the HCD + * + * @param port_hdl Port handle + */ +void test_hcd_teardown(hcd_port_handle_t port_hdl); + +/** + * @brief Wait for a connection on an HCD port + * + * @note This function will internally call test_hcd_force_conn_state() to allow for a connection + * + * @param port_hdl Port handle + * @return usb_speed_t Speed of the connected device + */ +usb_speed_t test_hcd_wait_for_conn(hcd_port_handle_t port_hdl); + +/** + * @brief Wait for a disconnection on an HCD port + * + * @note This fucntion will internally call test_hcd_force_conn_state() to force a disconnection + * + * @param port_hdl Port handle + * @param already_disabled Whether the HCD port is already in the disabled state + */ +void test_hcd_wait_for_disconn(hcd_port_handle_t port_hdl, bool already_disabled); + +// ------------------------------------------------- Pipe alloc/free --------------------------------------------------- + +/** + * @brief Test the allocation of a pipe + * + * @param port_hdl Port handle + * @param ep_desc Endpoint descriptor + * @param dev_addr Device address of the pipe + * @param dev_speed Device speed of the pipe + * @return hcd_pipe_handle_t Pipe handle + */ +hcd_pipe_handle_t test_hcd_pipe_alloc(hcd_port_handle_t port_hdl, const usb_desc_ep_t *ep_desc, uint8_t dev_addr, usb_speed_t dev_speed); + +/** + * @brief Test the freeing of a pipe + * + * @param pipe_hdl Pipe handle + */ +void test_hcd_pipe_free(hcd_pipe_handle_t pipe_hdl); + +/** + * @brief Allocate an IRP + * + * @param num_iso_packets Number of isochronous packets + * @param data_buffer_size Size of the data buffer of the IRP + * @return usb_irp_t* IRP + */ +usb_irp_t *test_hcd_alloc_irp(int num_iso_packets, size_t data_buffer_size); + +/** + * @brief Free an IRP + * + * @param irp IRP + */ +void test_hcd_free_irp(usb_irp_t *irp); + +// --------------------------------------------------- Enumeration ----------------------------------------------------- + +/** + * @brief Do some basic enumeration of the device + * + * For tests that need a device to have been enumerated (such as bulk tests). This function will enumerate that device + * using the device's default pipe. The minimal enumeration will include + * + * - Getting the device's descriptor and updating the default pipe's MPS + * - Setting the device's address and updating the default pipe to use that address + * - Setting the device to configuration 1 (i.e., the first configuration found + * + * @param default_pipe The connected device's default pipe + * @return uint8_t The address of the device after enumeration + */ +uint8_t test_hcd_enum_devc(hcd_pipe_handle_t default_pipe); diff --git a/components/usb/test/hcd/test_hcd_ctrl.c b/components/usb/test/hcd/test_hcd_ctrl.c new file mode 100644 index 0000000000..6e8f7a1915 --- /dev/null +++ b/components/usb/test/hcd/test_hcd_ctrl.c @@ -0,0 +1,271 @@ +// 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 "test_hcd_common.h" + +#define TEST_DEV_ADDR 0 +#define NUM_IRPS 3 +#define TRANSFER_MAX_BYTES 256 +#define IRP_DATA_BUFF_SIZE (sizeof(usb_ctrl_req_t) + TRANSFER_MAX_BYTES) //256 is worst case size for configuration descriptors + +/* +Test HCD control pipe IRPs (normal completion and early abort) + +Purpose: + - Test that a control pipe can be created + - IRPs can be created and enqueued to the control pipe + - Control pipe returns HCD_PIPE_EVENT_IRP_DONE + - Test that IRPs can be aborted when enqueued + +Procedure: + - Setup HCD and wait for connection + - Setup default pipe and allocate IRPs + - Enqueue IRPs + - Expect HCD_PIPE_EVENT_IRP_DONE + - Requeue IRPs, but abort them immediately + - Expect IRP to be USB_TRANSFER_STATUS_CANCELED or USB_TRANSFER_STATUS_COMPLETED + - Teardown +*/ +TEST_CASE("Test HCD control pipe IRPs", "[hcd][ignore]") +{ + hcd_port_handle_t port_hdl = test_hcd_setup(); //Setup the HCD and port + usb_speed_t port_speed = test_hcd_wait_for_conn(port_hdl); //Trigger a connection + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Allocate some IRPs and initialize their data buffers with control transfers + hcd_pipe_handle_t default_pipe = test_hcd_pipe_alloc(port_hdl, NULL, TEST_DEV_ADDR, port_speed); //Create a default pipe (using a NULL EP descriptor) + usb_irp_t *irp_list[NUM_IRPS]; + for (int i = 0; i < NUM_IRPS; i++) { + irp_list[i] = test_hcd_alloc_irp(0, IRP_DATA_BUFF_SIZE); + //Initialize with a "Get Config Descriptor request" + irp_list[i]->num_bytes = TRANSFER_MAX_BYTES; + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *)irp_list[i]->data_buffer, 0, TRANSFER_MAX_BYTES); + irp_list[i]->context = IRP_CONTEXT_VAL; + } + + //Enqueue IRPs but immediately suspend the port + printf("Enqueuing IRPs\n"); + for (int i = 0; i < NUM_IRPS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[i])); + } + //Wait for each done event of each IRP + for (int i = 0; i < NUM_IRPS; i++) { + test_hcd_expect_pipe_event(default_pipe, HCD_PIPE_EVENT_IRP_DONE); + } + //Dequeue IRPs + for (int i = 0; i < NUM_IRPS; i++) { + usb_irp_t *irp = hcd_irp_dequeue(default_pipe); + TEST_ASSERT_EQUAL(irp_list[i], irp); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->status); + TEST_ASSERT_EQUAL(IRP_CONTEXT_VAL, irp->context); + } + + //Enqueue IRPs again but abort them short after + for (int i = 0; i < NUM_IRPS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[i])); + } + for (int i = 0; i < NUM_IRPS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_abort(irp_list[i])); + } + vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for any inflight transfers to complete + + //Wait for the IRPs to complete and dequeue them, then check results + //Dequeue IRPs + for (int i = 0; i < NUM_IRPS; i++) { + usb_irp_t *irp = hcd_irp_dequeue(default_pipe); + //No need to check for IRP pointer address as they may be out of order + TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED || irp->status == USB_TRANSFER_STATUS_CANCELED); + if (irp->status == USB_TRANSFER_STATUS_COMPLETED) { + TEST_ASSERT_GREATER_THAN(0, irp->actual_num_bytes); + } else { + TEST_ASSERT_EQUAL(0, irp->actual_num_bytes); + } + TEST_ASSERT_EQUAL(irp->context, IRP_CONTEXT_VAL); + } + + //Free IRP list and pipe + for (int i = 0; i < NUM_IRPS; i++) { + test_hcd_free_irp(irp_list[i]); + } + test_hcd_pipe_free(default_pipe); + //Cleanup + test_hcd_wait_for_disconn(port_hdl, false); + test_hcd_teardown(port_hdl); +} + +/* +Test HCD control pipe STALL condition, abort, and clear + +Purpose: + - Test that a control pipe can react to a STALL (i.e., a HCD_PIPE_EVENT_HALTED event) + - The HCD_PIPE_CMD_ABORT can retire all IRPs + - Pipe clear command can return the pipe to being active + +Procedure: + - Setup HCD and wait for connection + - Setup default pipe and allocate IRPs + - Corrupt the first IRP so that it will trigger a STALL, then enqueue all the IRPs + - Check that a HCD_PIPE_EVENT_ERROR_STALL event is triggered + - Check that all IRPs can be retired using HCD_PIPE_CMD_ABORT + - Check that the STALL can be cleared by using HCD_PIPE_CMD_CLEAR + - Fix the corrupt first IRP and retry the IRPs + - Dequeue IRPs + - Teardown +*/ +TEST_CASE("Test HCD control pipe STALL", "[hcd][ignore]") +{ + hcd_port_handle_t port_hdl = test_hcd_setup(); //Setup the HCD and port + usb_speed_t port_speed = test_hcd_wait_for_conn(port_hdl); //Trigger a connection + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Allocate some IRPs and initialize their data buffers with control transfers + hcd_pipe_handle_t default_pipe = test_hcd_pipe_alloc(port_hdl, NULL, TEST_DEV_ADDR, port_speed); //Create a default pipe (using a NULL EP descriptor) + usb_irp_t *irp_list[NUM_IRPS]; + for (int i = 0; i < NUM_IRPS; i++) { + irp_list[i] = test_hcd_alloc_irp(0, IRP_DATA_BUFF_SIZE); + //Initialize with a "Get Config Descriptor request" + irp_list[i]->num_bytes = TRANSFER_MAX_BYTES; + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *)irp_list[i]->data_buffer, 0, TRANSFER_MAX_BYTES); + irp_list[i]->context = IRP_CONTEXT_VAL; + } + //Corrupt the first IRP so that it triggers a STALL + ((usb_ctrl_req_t *)irp_list[0]->data_buffer)->bRequest = 0xAA; + + //Enqueue IRPs. A STALL should occur + for (int i = 0; i < NUM_IRPS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[i])); + } + printf("Expecting STALL\n"); + test_hcd_expect_pipe_event(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 IRPs then dequeue them all + TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_command(default_pipe, HCD_PIPE_CMD_ABORT)); + for (int i = 0; i < NUM_IRPS; i++) { + usb_irp_t *irp = hcd_irp_dequeue(default_pipe); + TEST_ASSERT_EQUAL(irp_list[i], irp); + TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_STALL || irp->status == USB_TRANSFER_STATUS_CANCELED); + if (irp->status == USB_TRANSFER_STATUS_COMPLETED) { + TEST_ASSERT_GREATER_THAN(0, irp->actual_num_bytes); + } else { + TEST_ASSERT_EQUAL(0, irp->actual_num_bytes); + } + TEST_ASSERT_EQUAL(IRP_CONTEXT_VAL, irp->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)); + + printf("Retrying\n"); + //Correct first IRP then requeue + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *)irp_list[0]->data_buffer, 0, TRANSFER_MAX_BYTES); + for (int i = 0; i < NUM_IRPS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[i])); + } + + //Wait for each IRP to be done, deequeue, and check results + for (int i = 0; i < NUM_IRPS; i++) { + test_hcd_expect_pipe_event(default_pipe, HCD_PIPE_EVENT_IRP_DONE); + //expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_IRP_DONE); + usb_irp_t *irp = hcd_irp_dequeue(default_pipe); + TEST_ASSERT_EQUAL(irp_list[i], irp); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->status); + TEST_ASSERT_GREATER_THAN(0, irp->actual_num_bytes); + TEST_ASSERT_EQUAL(IRP_CONTEXT_VAL, irp->context); + } + + //Free IRP list and pipe + for (int i = 0; i < NUM_IRPS; i++) { + test_hcd_free_irp(irp_list[i]); + } + test_hcd_pipe_free(default_pipe); + //Cleanup + test_hcd_wait_for_disconn(port_hdl, false); + test_hcd_teardown(port_hdl); +} + +/* +Test control pipe run-time halt and clear + +Purpose: + - Test that a control pipe can be halted with HCD_PIPE_CMD_HALT whilst there are ongoing IRPs + - Test that a control pipe can be un-halted with a HCD_PIPE_CMD_CLEAR + - Test that enqueued IRPs are resumed when pipe is un-halted + +Procedure: + - Setup HCD and wait for connection + - Setup default pipe and allocate IRPs + - Enqqueue IRPs but execute a HCD_PIPE_CMD_HALT command immediately after. Halt command should let on + the current going IRP finish before actually halting the pipe. + - Un-halt the pipe a HCD_PIPE_CMD_HALT command. Enqueued IRPs will be resumed + - Check that all IRPs have completed successfully + - Dequeue IRPs and teardown +*/ +TEST_CASE("Test HCD control pipe runtime halt and clear", "[hcd][ignore]") +{ + hcd_port_handle_t port_hdl = test_hcd_setup(); //Setup the HCD and port + usb_speed_t port_speed = test_hcd_wait_for_conn(port_hdl); //Trigger a connection + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Allocate some IRPs and initialize their data buffers with control transfers + hcd_pipe_handle_t default_pipe = test_hcd_pipe_alloc(port_hdl, NULL, TEST_DEV_ADDR, port_speed); //Create a default pipe (using a NULL EP descriptor) + usb_irp_t *irp_list[NUM_IRPS]; + for (int i = 0; i < NUM_IRPS; i++) { + irp_list[i] = test_hcd_alloc_irp(0, IRP_DATA_BUFF_SIZE); + //Initialize with a "Get Config Descriptor request" + irp_list[i]->num_bytes = TRANSFER_MAX_BYTES; + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *)irp_list[i]->data_buffer, 0, TRANSFER_MAX_BYTES); + irp_list[i]->context = IRP_CONTEXT_VAL; + } + + //Enqueue IRPs but immediately halt the pipe + printf("Enqueuing IRPs\n"); + for (int i = 0; i < NUM_IRPS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[i])); + } + 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"); + + //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 + + //Wait for each IRP to be done, dequeue, and check results + for (int i = 0; i < NUM_IRPS; i++) { + test_hcd_expect_pipe_event(default_pipe, HCD_PIPE_EVENT_IRP_DONE); + usb_irp_t *irp = hcd_irp_dequeue(default_pipe); + TEST_ASSERT_EQUAL(irp_list[i], irp); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->status); + TEST_ASSERT_GREATER_THAN(0, irp->actual_num_bytes); + TEST_ASSERT_EQUAL(IRP_CONTEXT_VAL, irp->context); + } + + //Free IRP list and pipe + for (int i = 0; i < NUM_IRPS; i++) { + test_hcd_free_irp(irp_list[i]); + } + test_hcd_pipe_free(default_pipe); + //Cleanup + test_hcd_wait_for_disconn(port_hdl, false); + test_hcd_teardown(port_hdl); +} diff --git a/components/usb/test/hcd/test_hcd_intr.c b/components/usb/test/hcd/test_hcd_intr.c new file mode 100644 index 0000000000..8495fd72eb --- /dev/null +++ b/components/usb/test/hcd/test_hcd_intr.c @@ -0,0 +1,159 @@ +// 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 "test_hcd_common.h" + +// ------------------------------------------------- Mock HID Mice ----------------------------------------------------- + +/* +Note: The following test requires that USB low speed mouse be connected. The mouse should... + +- Be implement the HID with standard report format used by mice +- It's configuration 1 should have the following endpoint + +Endpoint Descriptor: + bLength 7 + bDescriptorType 5 + bEndpointAddress 0x81 EP 1 IN + bmAttributes 3 + Transfer Type Interrupt + Synch Type None + Usage Type Data + wMaxPacketSize 0x0004 1x 4 bytes + bInterval 10 + +If you're using another mice with different endpoints, modify the endpoint descriptor below +*/ +static const usb_desc_ep_t in_ep_desc = { + .bLength = sizeof(usb_desc_ep_t), + .bDescriptorType = USB_B_DESCRIPTOR_TYPE_ENDPOINT, + .bEndpointAddress = 0x81, //EP 1 IN + .bmAttributes = USB_BM_ATTRIBUTES_XFER_INT, + .wMaxPacketSize = 4, //MPS of 4 bytes + .bInterval = 10, //Interval of 10ms +}; + +typedef union { + struct { + uint32_t left_button: 1; + uint32_t right_button: 1; + uint32_t middle_button: 1; + uint32_t reserved5: 5; + uint8_t x_movement; + uint8_t y_movement; + } __attribute__((packed)); + uint8_t val[3]; +} mock_hid_mouse_report_t; +_Static_assert(sizeof(mock_hid_mouse_report_t) == 3, "Size of HID mouse report incorrect"); + +static void mock_hid_process_report(mock_hid_mouse_report_t *report, int iter) +{ + static int x_pos = 0; + static int y_pos = 0; + //Update X position + if (report->x_movement & 0x80) { //Positive movement + x_pos += report->x_movement & 0x7F; + } else { //Negative movement + x_pos -= report->x_movement & 0x7F; + } + //Update Y position + if (report->y_movement & 0x80) { //Positive movement + y_pos += report->y_movement & 0x7F; + } else { //Negative movement + y_pos -= report->y_movement & 0x7F; + } + printf("\rX:%d\tY:%d\tIter: %d\n", x_pos, y_pos, iter); +} + +// --------------------------------------------------- Test Cases ------------------------------------------------------ + +/* +Test HCD interrupt pipe IRPs +Purpose: + - Test that an interrupt pipe can be created + - IRPs can be created and enqueued to the interrupt pipe + - Interrupt pipe returns HCD_PIPE_EVENT_IRP_DONE + - Test that IRPs can be aborted when enqueued + +Procedure: + - Setup HCD and wait for connection + - Allocate default pipe and enumerate the device + - Setup interrupt pipe and allocate IRPs + - Enqueue IRPs, expect HCD_PIPE_EVENT_IRP_DONE, and requeue + - Stop after fixed number of iterations + - Deallocate IRPs + - Teardown + +Note: Some mice will NAK until it is moved, so try moving the mouse around if this test case gets stuck. +*/ + +#define TEST_HID_DEV_SPEED USB_SPEED_LOW +#define NUM_IRPS 3 +#define IRP_DATA_BUFF_SIZE 4 //MPS is 4 +#define MOCK_HID_NUM_REPORT_PER_IRP 2 +#define NUM_IRP_ITERS (NUM_IRPS * 100) + +TEST_CASE("Test HCD interrupt pipe IRPs", "[hcd][ignore]") +{ + hcd_port_handle_t port_hdl = test_hcd_setup(); //Setup the HCD and port + usb_speed_t port_speed = test_hcd_wait_for_conn(port_hdl); //Trigger a connection + TEST_ASSERT_EQUAL(TEST_HID_DEV_SPEED, TEST_HID_DEV_SPEED); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + hcd_pipe_handle_t default_pipe = test_hcd_pipe_alloc(port_hdl, NULL, 0, port_speed); //Create a default pipe (using a NULL EP descriptor) + uint8_t dev_addr = test_hcd_enum_devc(default_pipe); + + //Allocate interrupt pipe and IRPS + hcd_pipe_handle_t intr_pipe = test_hcd_pipe_alloc(port_hdl, &in_ep_desc, dev_addr, port_speed); + usb_irp_t *irp_list[NUM_IRPS]; + for (int i = 0; i < NUM_IRPS; i++) { + irp_list[i] = test_hcd_alloc_irp(0, IRP_DATA_BUFF_SIZE); + irp_list[i]->num_bytes = IRP_DATA_BUFF_SIZE; + irp_list[i]->context = IRP_CONTEXT_VAL; + } + + //Enqueue IRPs + for (int i = 0; i < NUM_IRPS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(intr_pipe, irp_list[i])); + } + int iter_count = NUM_IRP_ITERS; + for (iter_count = NUM_IRP_ITERS; iter_count > 0; iter_count--) { + //Wait for an IRP to be done + test_hcd_expect_pipe_event(intr_pipe, HCD_PIPE_EVENT_IRP_DONE); + //Dequeue the IRP and check results + usb_irp_t *irp = hcd_irp_dequeue(intr_pipe); + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->status); + TEST_ASSERT_EQUAL(IRP_CONTEXT_VAL, irp->context); + mock_hid_process_report((mock_hid_mouse_report_t *)irp->data_buffer, iter_count); + //Requeue IRP + if (iter_count > NUM_IRPS) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(intr_pipe, irp)); + } + } + + //Free IRP list and pipe + for (int i = 0; i < NUM_IRPS; i++) { + test_hcd_free_irp(irp_list[i]); + } + test_hcd_pipe_free(intr_pipe); + test_hcd_pipe_free(default_pipe); + //Clearnup + test_hcd_wait_for_disconn(port_hdl, false); + test_hcd_teardown(port_hdl); +} diff --git a/components/usb/test/hcd/test_hcd_isoc.c b/components/usb/test/hcd/test_hcd_isoc.c new file mode 100644 index 0000000000..4379a7b0d2 --- /dev/null +++ b/components/usb/test/hcd/test_hcd_isoc.c @@ -0,0 +1,114 @@ +// 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 +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "unity.h" +#include "test_utils.h" +#include "test_hcd_common.h" + +//We talk to a non-existent device. Since ISOC out requires no ACK, there should be no errors. +#define MOCK_ISOC_EP_NUM 2 +#define MOCK_ISOC_EP_MPS 512 + +#define NUM_IRPS 3 +#define NUM_PACKETS_PER_IRP 3 +#define ISOC_PACKET_SIZE MOCK_ISOC_EP_MPS +#define IRP_DATA_BUFF_SIZE (NUM_PACKETS_PER_IRP * ISOC_PACKET_SIZE) + +static const usb_desc_ep_t isoc_out_ep_desc = { + .bLength = sizeof(usb_desc_ep_t), + .bDescriptorType = USB_B_DESCRIPTOR_TYPE_ENDPOINT, + .bEndpointAddress = MOCK_ISOC_EP_NUM, + .bmAttributes = USB_BM_ATTRIBUTES_XFER_ISOC, + .wMaxPacketSize = MOCK_ISOC_EP_MPS, //MPS of 512 bytes + .bInterval = 1, //Isoc interval is (2 ^ (bInterval - 1)) which means an interval of 1ms +}; + +/* +Test HCD ISOC pipe IRPs + +Purpose: + - Test that an isochronous pipe can be created + - IRPs can be created and enqueued to the isoc pipe pipe + - isoc pipe returns HCD_PIPE_EVENT_IRP_DONE for completed IRPs + - Test utilizes ISOC OUT transfers and do not require ACKs. So the isoc pipe will target a non existing endpoint + +Procedure: + - Setup HCD and wait for connection + - Allocate default pipe and enumerate the device + - Allocate an isochronous pipe and multiple IRPs. Each IRP should contain multiple packets to test HCD's ability to + schedule an IRP across multiple intervals. + - Enqueue those IRPs + - Expect HCD_PIPE_EVENT_IRP_DONE for each IRP. Verify that data is correct using logic analyzer + - Deallocate IRPs + - Teardown +*/ + +TEST_CASE("Test HCD isochronous pipe IRPs", "[hcd][ignore]") +{ + hcd_port_handle_t port_hdl = test_hcd_setup(); //Setup the HCD and port + usb_speed_t port_speed = test_hcd_wait_for_conn(port_hdl); //Trigger a connection + //The MPS of the ISOC OUT pipe is quite large, so we need to bias the FIFO sizing + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_set_fifo_bias(port_hdl, HCD_PORT_FIFO_BIAS_PTX)); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Enumerate and reset device + hcd_pipe_handle_t default_pipe = test_hcd_pipe_alloc(port_hdl, NULL, 0, port_speed); //Create a default pipe (using a NULL EP descriptor) + uint8_t dev_addr = test_hcd_enum_devc(default_pipe); + + //Create ISOC OUT pipe to non-existent device + hcd_pipe_handle_t isoc_out_pipe = test_hcd_pipe_alloc(port_hdl, &isoc_out_ep_desc, dev_addr + 1, port_speed); + //Create IRPs + usb_irp_t *irp_list[NUM_IRPS]; + //Initialize IRPs + for (int irp_idx = 0; irp_idx < NUM_IRPS; irp_idx++) { + irp_list[irp_idx] = test_hcd_alloc_irp(NUM_PACKETS_PER_IRP, IRP_DATA_BUFF_SIZE); + irp_list[irp_idx]->num_bytes = 0; //num_bytes is not used for ISOC + irp_list[irp_idx]->context = IRP_CONTEXT_VAL; + for (int pkt_idx = 0; pkt_idx < NUM_PACKETS_PER_IRP; pkt_idx++) { + irp_list[irp_idx]->iso_packet_desc[pkt_idx].length = ISOC_PACKET_SIZE; + //Each packet will consist of the same byte, but each subsequent packet's byte will increment (i.e., packet 0 transmits all 0x0, packet 1 transmits all 0x1) + memset(&irp_list[irp_idx]->data_buffer[pkt_idx * ISOC_PACKET_SIZE], (irp_idx * NUM_IRPS) + pkt_idx, ISOC_PACKET_SIZE); + } + } + //Enqueue IRPs + for (int i = 0; i < NUM_IRPS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(isoc_out_pipe, irp_list[i])); + } + //Wait for each done event from each IRP + for (int i = 0; i < NUM_IRPS; i++) { + test_hcd_expect_pipe_event(isoc_out_pipe, HCD_PIPE_EVENT_IRP_DONE); + } + //Dequeue IRPs + for (int irp_idx = 0; irp_idx < NUM_IRPS; irp_idx++) { + usb_irp_t *irp = hcd_irp_dequeue(isoc_out_pipe); + TEST_ASSERT_EQUAL(irp_list[irp_idx], irp); + TEST_ASSERT_EQUAL(IRP_CONTEXT_VAL, irp->context); + for (int pkt_idx = 0; pkt_idx < NUM_PACKETS_PER_IRP; pkt_idx++) { + TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->iso_packet_desc[pkt_idx].status); + } + } + //Free IRP list and pipe + for (int i = 0; i < NUM_IRPS; i++) { + test_hcd_free_irp(irp_list[i]); + } + test_hcd_pipe_free(isoc_out_pipe); + test_hcd_pipe_free(default_pipe); + //Cleanup + test_hcd_wait_for_disconn(port_hdl, false); + test_hcd_teardown(port_hdl); +} diff --git a/components/usb/test/hcd/test_hcd_port.c b/components/usb/test/hcd/test_hcd_port.c new file mode 100644 index 0000000000..e3f5bab9be --- /dev/null +++ b/components/usb/test/hcd/test_hcd_port.c @@ -0,0 +1,309 @@ +// 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 "test_hcd_common.h" + +#define TEST_DEV_ADDR 0 +#define NUM_IRPS 3 +#define TRANSFER_MAX_BYTES 256 +#define IRP_DATA_BUFF_SIZE (sizeof(usb_ctrl_req_t) + TRANSFER_MAX_BYTES) //256 is worst case size for configuration descriptors + +/* +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 IRPs and pipes are handled correctly + +Procedure: + - Setup the HCD and a port + - Trigger a port connection + - Create a default pipe + - Start transfers but immediately trigger a disconnect + - Check that HCD_PORT_EVENT_SUDDEN_DISCONN event is generated. Handle the event. + - Check that default pipe received a HCD_PIPE_EVENT_INVALID event. Pipe state should be invalid. Dequeue IRPs + - Free default pipe + - Recover the port + - Trigger connection and disconnection again (to make sure the port works post recovery) + - Teardown port and HCD +*/ + +TEST_CASE("Test HCD port sudden disconnect", "[hcd][ignore]") +{ + hcd_port_handle_t port_hdl = test_hcd_setup(); //Setup the HCD and port + usb_speed_t port_speed = test_hcd_wait_for_conn(port_hdl); //Trigger a connection + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Allocate some IRPs and initialize their data buffers with control transfers + hcd_pipe_handle_t default_pipe = test_hcd_pipe_alloc(port_hdl, NULL, TEST_DEV_ADDR, port_speed); //Create a default pipe (using a NULL EP descriptor) + usb_irp_t *irp_list[NUM_IRPS]; + for (int i = 0; i < NUM_IRPS; i++) { + irp_list[i] = test_hcd_alloc_irp(0, IRP_DATA_BUFF_SIZE); + //Initialize with a "Get Config Descriptor request" + irp_list[i]->num_bytes = TRANSFER_MAX_BYTES; + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *)irp_list[i]->data_buffer, 0, TRANSFER_MAX_BYTES); + irp_list[i]->context = (void *)0xDEADBEEF; + } + + //Enqueue IRPs but immediately trigger a disconnect + printf("Enqueuing IRPs\n"); + for (int i = 0; i < NUM_IRPS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[i])); + } + test_hcd_force_conn_state(false, 0); + //Disconnect event should have occurred. Handle the event + test_hcd_expect_port_event(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"); + + //Pipe should have received (zero or more HCD_PIPE_EVENT_IRP_DONE) followed by a HCD_PIPE_EVENT_INVALID (MUST OCCUR) + int num_pipe_events = test_hcd_get_num_pipe_events(default_pipe); + for (int i = 0; i < num_pipe_events - 1; i++) { + test_hcd_expect_pipe_event(default_pipe, HCD_PIPE_EVENT_IRP_DONE); + } + test_hcd_expect_pipe_event(default_pipe, HCD_PIPE_EVENT_INVALID); + TEST_ASSERT_EQUAL(hcd_pipe_get_state(default_pipe), HCD_PIPE_STATE_INVALID); + + //Dequeue IRPs + for (int i = 0; i < NUM_IRPS; i++) { + usb_irp_t *irp = hcd_irp_dequeue(default_pipe); + TEST_ASSERT_EQUAL(irp_list[i], irp); + TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED || irp->status == USB_TRANSFER_STATUS_NO_DEVICE); + if (irp->status == USB_TRANSFER_STATUS_COMPLETED) { + TEST_ASSERT_GREATER_THAN(0, irp->actual_num_bytes); + } else { + TEST_ASSERT_EQUAL(0, irp->actual_num_bytes); + } + TEST_ASSERT_EQUAL(0xDEADBEEF, irp->context); + } + //Free IRP list and pipe + for (int i = 0; i < NUM_IRPS; i++) { + test_hcd_free_irp(irp_list[i]); + } + test_hcd_pipe_free(default_pipe); + + //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 disconnect again + test_hcd_wait_for_conn(port_hdl); + test_hcd_wait_for_disconn(port_hdl, false); + test_hcd_teardown(port_hdl); +} + +/* +Test port suspend and resume with active pipes + +Purpose: + - Test port 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 the HCD and a port + - Trigger a port connection + - Create a default pipe + - Start transfers but suspend the port immediately + - Resume the port + - Check that all the IRPs have completed successfully + - Cleanup IRPs and default pipe + - Trigger disconnection and teardown +*/ +TEST_CASE("Test HCD port suspend and resume", "[hcd][ignore]") +{ + hcd_port_handle_t port_hdl = test_hcd_setup(); //Setup the HCD and port + usb_speed_t port_speed = test_hcd_wait_for_conn(port_hdl); //Trigger a connection + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Allocate some IRPs and initialize their data buffers with control transfers + hcd_pipe_handle_t default_pipe = test_hcd_pipe_alloc(port_hdl, NULL, TEST_DEV_ADDR, port_speed); //Create a default pipe (using a NULL EP descriptor) + usb_irp_t *irp_list[NUM_IRPS]; + for (int i = 0; i < NUM_IRPS; i++) { + irp_list[i] = test_hcd_alloc_irp(0, IRP_DATA_BUFF_SIZE); + //Initialize with a "Get Config Descriptor request" + irp_list[i]->num_bytes = TRANSFER_MAX_BYTES; + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *)irp_list[i]->data_buffer, 0, TRANSFER_MAX_BYTES); + irp_list[i]->context = (void *)0xDEADBEEF; + } + + //Enqueue IRPs but immediately suspend the port + printf("Enqueuing IRPs\n"); + for (int i = 0; i < NUM_IRPS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[i])); + } + 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)); //Give some time for bus to remain suspended + + //Resume the port + 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)); + printf("Resumed\n"); + vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for resumed IRPs to complete + //Dequeue IRPs + for (int i = 0; i < NUM_IRPS; i++) { + usb_irp_t *irp = hcd_irp_dequeue(default_pipe); + TEST_ASSERT_EQUAL(irp_list[i], irp); + TEST_ASSERT_EQUAL(irp->status, USB_TRANSFER_STATUS_COMPLETED); + TEST_ASSERT_GREATER_THAN(0, irp->actual_num_bytes); + TEST_ASSERT_EQUAL(0xDEADBEEF, irp->context); + } + + //Free IRP list and pipe + for (int i = 0; i < NUM_IRPS; i++) { + test_hcd_free_irp(irp_list[i]); + } + test_hcd_pipe_free(default_pipe); + //Cleanup + test_hcd_wait_for_disconn(port_hdl, false); + test_hcd_teardown(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 IRPs + - 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]") +{ + hcd_port_handle_t port_hdl = test_hcd_setup(); //Setup the HCD and port + usb_speed_t port_speed = test_hcd_wait_for_conn(port_hdl); //Trigger a connection + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Allocate some IRPs and initialize their data buffers with control transfers + hcd_pipe_handle_t default_pipe = test_hcd_pipe_alloc(port_hdl, NULL, TEST_DEV_ADDR, port_speed); //Create a default pipe (using a NULL EP descriptor) + usb_irp_t *irp_list[NUM_IRPS]; + for (int i = 0; i < NUM_IRPS; i++) { + irp_list[i] = test_hcd_alloc_irp(0, IRP_DATA_BUFF_SIZE); + //Initialize with a "Get Config Descriptor request" + irp_list[i]->num_bytes = TRANSFER_MAX_BYTES; + USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *)irp_list[i]->data_buffer, 0, TRANSFER_MAX_BYTES); + irp_list[i]->context = (void *)0xDEADBEEF; + } + + //Enqueue IRPs but immediately disable the port + printf("Enqueuing IRPs\n"); + for (int i = 0; i < NUM_IRPS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[i])); + } + 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_IRP_DONE) followed by a HCD_PIPE_EVENT_INVALID (MUST OCCUR) + int num_pipe_events = test_hcd_get_num_pipe_events(default_pipe); + for (int i = 0; i < num_pipe_events - 1; i++) { + test_hcd_expect_pipe_event(default_pipe, HCD_PIPE_EVENT_IRP_DONE); + } + test_hcd_expect_pipe_event(default_pipe, HCD_PIPE_EVENT_INVALID); + + //Dequeue IRPs + for (int i = 0; i < NUM_IRPS; i++) { + usb_irp_t *irp = hcd_irp_dequeue(default_pipe); + TEST_ASSERT_EQUAL(irp_list[i], irp); + TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED || irp->status == USB_TRANSFER_STATUS_NO_DEVICE); + if (irp->status == USB_TRANSFER_STATUS_COMPLETED) { + TEST_ASSERT_GREATER_THAN(0, irp->actual_num_bytes); + } else { + TEST_ASSERT_EQUAL(0, irp->actual_num_bytes); + } + TEST_ASSERT_EQUAL(0xDEADBEEF, irp->context); + } + + //Free IRP list and pipe + for (int i = 0; i < NUM_IRPS; i++) { + test_hcd_free_irp(irp_list[i]); + } + test_hcd_pipe_free(default_pipe); + //Cleanup + test_hcd_wait_for_disconn(port_hdl, true); + test_hcd_teardown(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 + //Force a disconnection + test_hcd_force_conn_state(false, 0); + vTaskDelay(portMAX_DELAY); //Block forever and wait to be deleted +} + +TEST_CASE("Test HCD port command bailout", "[hcd][ignore]") +{ + hcd_port_handle_t port_hdl = test_hcd_setup(); //Setup the HCD and port + test_hcd_wait_for_conn(port_hdl); //Trigger a connection + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Create task to run port 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 + + //Attempt to resume the port. But the concurrent task should override this with a disconnection event + printf("Attempting to resume\n"); + xSemaphoreGive(sync_sem); //Trigger concurrent task + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_RESPONSE, hcd_port_command(port_hdl, HCD_PORT_CMD_RESUME)); + + //Check that concurrent task triggered a sudden disconnection + test_hcd_expect_port_event(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)); + + //Cleanup task and semaphore + vTaskDelay(pdMS_TO_TICKS(10)); //Short delay for concurrent task finish running + vTaskDelete(task_handle); + vSemaphoreDelete(sync_sem); + + test_hcd_teardown(port_hdl); +} diff --git a/components/usb/test/test_hcd.c b/components/usb/test/test_hcd.c deleted file mode 100644 index 0b915a2fb0..0000000000 --- a/components/usb/test/test_hcd.c +++ /dev/null @@ -1,790 +0,0 @@ -// 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 "soc/gpio_pins.h" -#include "soc/gpio_sig_map.h" -#include "esp_intr_alloc.h" -#include "esp_err.h" -#include "esp_attr.h" -#include "esp_rom_gpio.h" -#include "hal/usbh_ll.h" -#include "usb.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 device - wrap->otg_conf.phy_sel = 0; - } else { - //Set external PHY input signals to fixed voltage levels mimicking 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_IRPS 3 -#define TRANSFER_DATA_MAX_BYTES 256 //Just assume that will only IN/OUT 256 bytes for now -#define PORT_NUM 1 -#define IRP_CONTEXT_VAL ((void *)0xDEADBEEF) //Conext value for created IRPs - -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_irp_list(hcd_port_handle_t port_hdl, - QueueHandle_t pipe_evt_queue, - int num_irps, - hcd_pipe_handle_t *pipe_hdl, - usb_irp_t ***irp_list) -{ - //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 IRPs and their data buffers - printf("Creating IRPs and IRP list\n"); - *irp_list = heap_caps_malloc(sizeof(usb_irp_t *) * num_irps, MALLOC_CAP_DEFAULT); - TEST_ASSERT_NOT_EQUAL(NULL, *irp_list); - for (int i = 0; i < num_irps; i++) { - //Allocate IRP - usb_irp_t *irp = heap_caps_calloc(1, sizeof(usb_irp_t), MALLOC_CAP_DEFAULT); - TEST_ASSERT_NOT_EQUAL(NULL, irp); - //Allocate data buffer - uint8_t *data_buffer = heap_caps_malloc(sizeof(usb_ctrl_req_t) + TRANSFER_DATA_MAX_BYTES, MALLOC_CAP_DMA); - TEST_ASSERT_NOT_EQUAL(NULL, data_buffer); - //Initialize IRP and IRP list - irp->data_buffer = data_buffer; - irp->num_iso_packets = 0; - (*irp_list)[i] = irp; - } -} - -static void free_pipe_and_irp_list(hcd_pipe_handle_t pipe_hdl, - int num_irps, - usb_irp_t **irp_list) -{ - printf("Freeing IRPs and IRP list\n"); - for (int i = 0; i < num_irps; i++) { - usb_irp_t *irp = irp_list[i] ; - //Free data buffer - heap_caps_free(irp->data_buffer); - heap_caps_free(irp); - } - heap_caps_free(irp_list); - printf("Freeing default pipe\n"); - 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 IRPs and pipes are handled correctly - -Procedure: - - Setup HCD, a default pipe, and multiple IRPs - - Start transfers but immediately trigger a disconnect - - Check that HCD_PORT_EVENT_SUDDEN_DISCONN event is generated - - Check that default pipe is invalid and IRPs 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 default pipe and IRPs - hcd_pipe_handle_t default_pipe; - usb_irp_t **irp_list; - alloc_pipe_and_irp_list(port_hdl, pipe_evt_queue, NUM_IRPS, &default_pipe, &irp_list); - - //Initialize IRPs to send a "Get Device Descriptor" request - for (int i = 0; i < NUM_IRPS; i++) { - irp_list[i]->num_bytes = 64; //1 worst case MPS - USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) irp_list[i]->data_buffer, 0, TRANSFER_DATA_MAX_BYTES); - irp_list[i]->context = IRP_CONTEXT_VAL; - } - - //Enqueue those IRPs - for (int i = 0; i < NUM_IRPS; i++) { - TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[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 disconnection event should have invalidated all pipes. - //Pipe should have received (zero or more HCD_PIPE_EVENT_IRP_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_IRP_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 IRPs - for (int i = 0; i < NUM_IRPS; i++) { - usb_irp_t *irp = hcd_irp_dequeue(default_pipe); - TEST_ASSERT_NOT_EQUAL(NULL, irp); - TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED || irp->status == USB_TRANSFER_STATUS_NO_DEVICE); - TEST_ASSERT(irp->context == IRP_CONTEXT_VAL); - } - //Free IRPs - free_pipe_and_irp_list(default_pipe, NUM_IRPS, irp_list); - - //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 disconnect 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 port 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 IRPS - - Start transfers but immediately suspend the port - - Resume the port - - Check all IRPs 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 default pipe and IRPs - hcd_pipe_handle_t default_pipe; - usb_irp_t **irp_list; - alloc_pipe_and_irp_list(port_hdl, pipe_evt_queue, NUM_IRPS, &default_pipe, &irp_list); - - //Initialize IRPs to send a "Get Device Descriptor" request - for (int i = 0; i < NUM_IRPS; i++) { - irp_list[i]->num_bytes = 64; //1 worst case MPS - USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *)irp_list[i]->data_buffer, 0, TRANSFER_DATA_MAX_BYTES); - irp_list[i]->context = IRP_CONTEXT_VAL; - } - - //Enqueue those IRPs - for (int i = 0; i < NUM_IRPS; i++) { - TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[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 - - //Dequeue IRPs - for (int i = 0; i < NUM_IRPS; i++) { - expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_IRP_DONE); - usb_irp_t *irp = hcd_irp_dequeue(default_pipe); - TEST_ASSERT_NOT_EQUAL(NULL, irp); - TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED); - TEST_ASSERT(irp->context == IRP_CONTEXT_VAL); - } - //Free IRPs - free_pipe_and_irp_list(default_pipe, NUM_IRPS, irp_list); - - //Cleanup - 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 IRPs - - 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 default pipe and IRPs - hcd_pipe_handle_t default_pipe; - usb_irp_t **irp_list; - alloc_pipe_and_irp_list(port_hdl, pipe_evt_queue, NUM_IRPS, &default_pipe, &irp_list); - - //Initialize IRPs to send a "Get Device Descriptor" request - for (int i = 0; i < NUM_IRPS; i++) { - irp_list[i]->num_bytes = 64; //1 worst case MPS - USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) irp_list[i]->data_buffer, 0, TRANSFER_DATA_MAX_BYTES); - irp_list[i]->context = IRP_CONTEXT_VAL; - } - - //Enqueue those IRPs - for (int i = 0; i < NUM_IRPS; i++) { - TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[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_IRP_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_IRP_DONE); - } - expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_INVALID); - - //Dequeue IRPs - for (int i = 0; i < NUM_IRPS; i++) { - usb_irp_t *irp = hcd_irp_dequeue(default_pipe); - TEST_ASSERT_NOT_EQUAL(NULL, irp); - TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED || irp->status == USB_TRANSFER_STATUS_NO_DEVICE); - TEST_ASSERT(irp->context == IRP_CONTEXT_VAL); - } - //Free IRPs - free_pipe_and_irp_list(default_pipe, NUM_IRPS, irp_list); - - //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 disconnection - 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 IRPs (normal completion and early abort) - -Purpose: - - Test that pipes can be created - - IRPs can be created and enqueued - - Pipe returns HCD_PIPE_EVENT_IRP_DONE - - Test that IRPs can be aborted when enqueued - -Procedure: - - Setup - - Allocate IRPs. Initialize as Get Device Descriptor request - - Enqueue IRPs - - Expect HCD_PIPE_EVENT_IRP_DONE. Deallocate IRPs - - Requeue IRPs, but abort them immediately - - Teardown -*/ -TEST_CASE("Test HCD IRP enqueue", "[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 default pipe and IRPs - hcd_pipe_handle_t default_pipe; - usb_irp_t **irp_list; - alloc_pipe_and_irp_list(port_hdl, pipe_evt_queue, NUM_IRPS, &default_pipe, &irp_list); - - //Initialize IRPs to send a "Get Config Descriptor 0" request - for (int i = 0; i < NUM_IRPS; i++) { - irp_list[i]->num_bytes = 64; //1 worst case MPS - USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) irp_list[i]->data_buffer, 0, TRANSFER_DATA_MAX_BYTES); - irp_list[i]->context = IRP_CONTEXT_VAL; - } - - //Enqueue those IRPs - for (int i = 0; i < NUM_IRPS; i++) { - TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[i])); - } - - //Wait for each done event of each IRP - for (int i = 0; i < NUM_IRPS; i++) { - expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_IRP_DONE); - } - - //Dequeue IRPs - for (int i = 0; i < NUM_IRPS; i++) { - usb_irp_t *irp = hcd_irp_dequeue(default_pipe); - TEST_ASSERT_NOT_EQUAL(NULL, irp); - TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED); - TEST_ASSERT(irp->context == IRP_CONTEXT_VAL); - } - - //Enqueue them again but abort them short after - for (int i = 0; i < NUM_IRPS; i++) { - TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[i])); - } - for (int i = 0; i < NUM_IRPS; i++) { - TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_abort(irp_list[i])); - } - vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for any in-flight transfers to complete - - expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_IRP_DONE); - //Wait for the IRPs to complete and dequeue them, then check results - for (int i = 0; i < NUM_IRPS; i++) { - usb_irp_t *irp = hcd_irp_dequeue(default_pipe); - TEST_ASSERT_NOT_EQUAL(NULL, irp); - TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED || irp->status == USB_TRANSFER_STATUS_CANCELLED); - TEST_ASSERT(irp->context == IRP_CONTEXT_VAL); - } - - //Free IRPs and default pipe - free_pipe_and_irp_list(default_pipe, NUM_IRPS, irp_list); - - 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 IRPs - - Pipe clear command can return the pipe to being active - -Procedure: - - Setup HCD and a port, a default pipe, and multiple IRPs - - Corrupt the first IRP, then enqueue all of them. - - The corrupted IRP 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 default pipe and IRPs - hcd_pipe_handle_t default_pipe; - usb_irp_t **irp_list; - alloc_pipe_and_irp_list(port_hdl, pipe_evt_queue, NUM_IRPS, &default_pipe, &irp_list); - - //Initialize IRPs to send a "Get Device Descriptor" request - for (int i = 0; i < NUM_IRPS; i++) { - irp_list[i]->num_bytes = 64; //1 worst case MPS - USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) irp_list[i]->data_buffer, 0, TRANSFER_DATA_MAX_BYTES); - irp_list[i]->context = IRP_CONTEXT_VAL; - } - //Corrupt first transfer so that it triggers a STALL - ((usb_ctrl_req_t *)irp_list[0]->data_buffer)->bRequest = 0xAA; - - //Enqueue those IRPs - for (int i = 0; i < NUM_IRPS; i++) { - TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[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)); - //Dequeue IRPs - for (int i = 0; i < NUM_IRPS; i++) { - usb_irp_t *irp = hcd_irp_dequeue(default_pipe); - TEST_ASSERT_NOT_EQUAL(NULL, irp); - TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_STALL || irp->status == USB_TRANSFER_STATUS_CANCELLED); - TEST_ASSERT(irp->context == IRP_CONTEXT_VAL); - } - - //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 *) irp_list[0]->data_buffer, 0, TRANSFER_DATA_MAX_BYTES); - for (int i = 0; i < NUM_IRPS; i++) { - TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[i])); - } - vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for transfers to complete - - //Wait for the IRPs to complete and dequeue them, then check results - for (int i = 0; i < NUM_IRPS; i++) { - expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_IRP_DONE); - usb_irp_t *irp = hcd_irp_dequeue(default_pipe); - TEST_ASSERT_NOT_EQUAL(NULL, irp); - TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED); - TEST_ASSERT(irp->context == IRP_CONTEXT_VAL); - } - //Free IRPs - free_pipe_and_irp_list(default_pipe, NUM_IRPS, irp_list); - - 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 IRPs - - Test that a pipe can be un-halted with a HCD_PIPE_CMD_CLEAR - - Test that enqueued IRPs are resumed when pipe is cleared - -Procedure: - - Setup HCD, a default pipe, and multiple IRPs - - Enqueue IRPs but execute a HCD_PIPE_CMD_HALT command immediately after. Halt command should let on - the current going IRP finish before actually halting the pipe. - - Clear the pipe halt using a HCD_PIPE_CMD_HALT command. Enqueued IRPs will be resumed - - Check that all IRPs 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 default pipe and IRPs - hcd_pipe_handle_t default_pipe; - usb_irp_t **irp_list; - alloc_pipe_and_irp_list(port_hdl, pipe_evt_queue, NUM_IRPS, &default_pipe, &irp_list); - - //Initialize IRPs to send a "Get Device Descriptor" request - for (int i = 0; i < NUM_IRPS; i++) { - irp_list[i]->num_bytes = 64; //1 worst case MPS - USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *)irp_list[i]->data_buffer, 0, TRANSFER_DATA_MAX_BYTES); - irp_list[i]->context = IRP_CONTEXT_VAL; - } - - printf("Enqueuing IRPs\n"); - //Enqueue those IRPs - for (int i = 0; i < NUM_IRPS; i++) { - TEST_ASSERT_EQUAL(ESP_OK, hcd_irp_enqueue(default_pipe, irp_list[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 in-flight 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 - - //Dequeue IRPs - for (int i = 0; i < NUM_IRPS; i++) { - expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_IRP_DONE); - usb_irp_t *irp = hcd_irp_dequeue(default_pipe); - TEST_ASSERT_NOT_EQUAL(NULL, irp); - TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED); - TEST_ASSERT(irp->context == IRP_CONTEXT_VAL); - } - //Free IRPs - free_pipe_and_irp_list(default_pipe, NUM_IRPS, irp_list); - - 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); -}