Add USB HCD

This commit adds the USB HCD (Host Controller Driver) and accompanying unit tests.
This commit is contained in:
Darian Leung 2021-02-09 10:29:01 +08:00
parent bf0c05064c
commit 424e1e1886
11 changed files with 3580 additions and 52 deletions

View File

@ -44,6 +44,7 @@ NOTE: Thread safety is the responsibility fo the HAL user. All USB Host HAL
/**
* @brief Channel states
*
*/
typedef enum {
USBH_HAL_CHAN_STATE_HALTED = 0, /**< The channel is halted. No transfer descriptor list is being executed */
@ -57,6 +58,7 @@ typedef enum {
* @brief Host port HAL events
*/
typedef enum {
USBH_HAL_PORT_EVENT_NONE, /**< No event occurred, or could not decode interrupt */
USBH_HAL_PORT_EVENT_CHAN, /**< A channel event has occurred. Call the the channel event handler instead */
USBH_HAL_PORT_EVENT_CONN, /**< The host port has detected a connection */
USBH_HAL_PORT_EVENT_DISCONN, /**< The host port has been disconnected */
@ -74,7 +76,6 @@ typedef enum {
USBH_HAL_CHAN_EVENT_SLOT_HALT, /**< The channel as completed execution of a single transfer descriptor in a list. Channel is now halted */
USBH_HAL_CHAN_EVENT_ERROR, /**< The channel has encountered an error. Channel is now halted. */
USBH_HAL_CHAN_EVENT_HALT_REQ, /**< The channel has been successfully halted as requested */
USBH_HAL_CHAN_EVENT_SUDDEN_HLT, /**< The channel was suddenly halted (e.g. due to a disconnect). */
} usbh_hal_chan_event_t;
// ------------------------------- HAL Errors ----------------------------------
@ -87,7 +88,6 @@ typedef enum {
USBH_HAL_CHAN_ERROR_BNA, /**< Buffer Not Available error (i.e., transfer slot is unfilled */
USBH_HAL_CHAN_ERROR_PKT_BBL, /**< Packet babbler error (packet exceeded MPS) */
USBH_HAL_CHAN_ERROR_STALL, /**< STALL response received */
USBH_HAL_CHAN_ERROR_AHB, /**< AHB error */
} usbh_hal_chan_error_t;
// ----------------------- Transfer Descriptor Related -------------------------
@ -102,6 +102,11 @@ typedef enum {
/**
* @brief Status value of a transfer descriptor
*
* A transfer descriptor's status remains unexecuted until the entire transfer
* descriptor completes (either successfully or an error). Therefore, if a
* channel halt is requested before a transfer descriptor completes, the
* transfer descriptoor remains unexecuted.
*/
#define USBH_HAL_XFER_DESC_STS_SUCCESS USBH_LL_QTD_STATUS_SUCCESS
#define USBH_HAL_XFER_DESC_STS_PKTERR USBH_LL_QTD_STATUS_PKTERR
@ -120,7 +125,9 @@ typedef struct {
uint32_t bEndpointAddress: 8; /**< Endpoint address (containing endpoint number and direction) */
uint32_t mps: 11; /**< Maximum Packet Size */
uint32_t dev_addr: 8; /**< Device Address */
uint32_t reserved3: 3;
uint32_t ls_via_fs_hub: 1; /**< The endpoint is on a LS device that is routed through an FS hub.
Setting this bit will lead to the addition of the PREamble packet */
uint32_t reserved2: 2;
};
uint32_t val;
};
@ -148,7 +155,7 @@ typedef struct {
struct {
union {
struct {
bool slot_acquired: 1; /**< The transfer descriptor list slot has been acquired */
uint32_t slot_acquired: 1; /**< The transfer descriptor list slot has been acquired */
uint32_t reserved7: 7;
uint32_t cur_qtd_idx: 8; /**< Index of the first QTD in chain of QTDs being executed */
uint32_t qtd_list_len: 8; /**< Length of QTD list in number of QTDs */
@ -178,8 +185,8 @@ typedef struct {
} flags;
//Channel related
struct {
int num_allocd; /**< Number of channels currently allocated */
int chan_pend_intrs_msk; /**< Bit mask of channels with pending interrupts */
int num_allocd; /**< Number of channels currently allocated */
uint32_t chan_pend_intrs_msk; /**< Bit mask of channels with pending interrupts */
usbh_hal_chan_t *hdls[USBH_HAL_NUM_CHAN]; /**< Handles of each channel. Set to NULL if channel has not been allocated */
} channels;
} usbh_hal_context_t;
@ -417,13 +424,17 @@ static inline usb_speed_t usbh_hal_port_get_conn_speed(usbh_hal_context_t *hal)
* @brief Disable the debounce lock
*
* This function should be called after calling usbh_hal_port_check_if_connected()
* and will allow connection/disconnection events to occur again.
* and will allow connection/disconnection events to occur again. Any pending
* connection or disconenction interrupts are cleared.
*
* @param hal Context of the HAL layer
*/
static inline void usbh_hal_disable_debounce_lock(usbh_hal_context_t *hal)
{
hal->flags.dbnc_lock_enabled = 0;
//Clear Conenction and disconenction interrupt in case it triggered again
usb_ll_intr_clear(hal->dev, USB_LL_INTR_CORE_DISCONNINT);
usbh_ll_hprt_intr_clear(hal->dev, USBH_LL_INTR_HPRT_PRTENCHNG);
//Reenable the hprt (connection) and disconnection interrupts
usb_ll_en_intrs(hal->dev, USB_LL_INTR_CORE_PRTINT | USB_LL_INTR_CORE_DISCONNINT);
}
@ -659,7 +670,7 @@ static inline void usbh_hal_chan_slot_acquire(usbh_hal_chan_t *chan_obj, void *x
chan_obj->slot.owner_ctx = owner_ctx;
chan_obj->slot.flags.cur_qtd_idx = 0; //Start from the first descriptor
chan_obj->slot.flags.qtd_list_len = desc_list_len;
chan_obj->slot.flags.slot_acquired = true;
chan_obj->slot.flags.slot_acquired = 1;
//Store the descriptor list length in the HCTSIZ register. Address of desc list is set when channel is activated
usbh_ll_chan_set_qtd_list_len(chan_obj->regs, desc_list_len);
}
@ -696,7 +707,7 @@ static inline void usbh_hal_chan_slot_release(usbh_hal_chan_t *chan_obj, void **
assert(chan_obj->slot.flags.slot_acquired);
*xfer_desc_list = (void *)chan_obj->slot.xfer_desc_list;
*desc_list_len = chan_obj->slot.flags.qtd_list_len;
chan_obj->slot.flags.slot_acquired = false;
chan_obj->slot.flags.slot_acquired = 0;
}
/**
@ -734,11 +745,16 @@ static inline int usbh_hal_chan_get_next_desc_index(usbh_hal_chan_t *chan_obj)
* active, this function will return false and users must wait for the
* USBH_HAL_CHAN_EVENT_HALT_REQ event before treating the channel as halted.
*
* @note When a transfer is in progress (i.e., the channel is active) and a halt
* is requested, the channel will halt after the next USB packet is completed.
* If the transfer has more pending packets, the transfer will just be
* marked as USBH_HAL_XFER_DESC_STS_NOT_EXECUTED.
*
* @param chan_obj Channel object
* @return true The channel is already halted
* @return false The halt was requested, wait for USBH_HAL_CHAN_EVENT_HALT_REQ
*/
bool usbh_hal_chan_request_halt(usbh_hal_chan_t *chan_obj);
bool usbh_hal_chan_slot_request_halt(usbh_hal_chan_t *chan_obj);
/* -----------------------------------------------------------------------------
-------------------------------- Event Handling --------------------------------

View File

@ -283,8 +283,8 @@ static inline bool usb_ll_check_core_soft_reset(usbh_dev_t *hw)
/**
* @brief Reads and clears the global interrupt register
*
* @param hw
* @return uint32_t
* @param hw Start address of the DWC_OTG registers
* @return uint32_t Mask of interrupts
*/
static inline uint32_t usb_ll_intr_read_and_clear(usbh_dev_t *hw)
{
@ -294,6 +294,18 @@ static inline uint32_t usb_ll_intr_read_and_clear(usbh_dev_t *hw)
return gintsts.val;
}
/**
* @brief Clear specific interrupts
*
* @param hw Start address of the DWC_OTG registers
* @param intr_msk Mask of interrupts to clear
*/
static inline void usb_ll_intr_clear(usbh_dev_t *hw, uint32_t intr_msk)
{
//All GINTSTS fields are either W1C or read only. So safe to write directly
hw->gintsts_reg.val = intr_msk;
}
// --------------------------- GINTMSK Register --------------------------------
static inline void usb_ll_en_intrs(usbh_dev_t *hw, uint32_t intr_mask)
@ -403,26 +415,36 @@ static inline void usbh_ll_hcfg_set_fsls_pclk_sel(usbh_dev_t *hw)
}
/**
* @brief Sets some default values to HCFG to operate in Host mode wiht scatter/gather DMA
* @brief Sets some default values to HCFG to operate in Host mode with scatter/gather DMA
*
* @param hw
*/
static inline void usbh_ll_hcfg_set_defaults(usbh_dev_t *hw)
static inline void usbh_ll_hcfg_set_defaults(usbh_dev_t *hw, usb_speed_t speed)
{
hw->hcfg_reg.descdma = 1; //Enable scatt/gatt
hw->hcfg_reg.fslssupp = 1; //FS/LS supp only
hw->hcfg_reg.fslspclksel = 1; //48MHz PHY clock
/*
Indicate to the OTG core what speed the PHY clock is at
Note: It seems like our PHY has an implicit 8 divider applied when in LS mode,
so the values of FSLSPclkSel and FrInt have to be adjusted accordingly.
*/
hw->hcfg_reg.fslspclksel = (speed == USB_SPEED_FULL) ? 1 : 2;
hw->hcfg_reg.perschedena = 0; //Disable perio sched
}
// ----------------------------- HFIR Register ---------------------------------
static inline void usbh_ll_hfir_set_defaults(usbh_dev_t *hw)
static inline void usbh_ll_hfir_set_defaults(usbh_dev_t *hw, usb_speed_t speed)
{
usb_hfir_reg_t hfir;
hfir.val = hw->hfir_reg.val;
hfir.hfirrldctrl = 0; //Disable dynamic loading
hfir.frint = 48000; //Set frame interval to 48000 cycles of 48MHz clock (i.e. equals to 1ms)
/*
Set frame interval to be equal to 1ms
Note: It seems like our PHY has an implicit 8 divider applied when in LS mode,
so the values of FSLSPclkSel and FrInt have to be adjusted accordingly.
*/
hfir.frint = (speed == USB_SPEED_FULL) ? 48000 : 6000;
hw->hfir_reg.val = hfir.val;
}
@ -611,6 +633,13 @@ static inline uint32_t usbh_ll_hprt_intr_read_and_clear(usbh_dev_t *hw)
return (hprt.val & (USBH_LL_HPRT_W1C_MSK & ~(USBH_LL_HPRT_ENA_MSK)));
}
static inline void usbh_ll_hprt_intr_clear(usbh_dev_t *hw, uint32_t intr_mask)
{
usb_hprt_reg_t hprt;
hprt.val = hw->hprt_reg.val;
hw->hprt_reg.val = ((hprt.val & ~USBH_LL_HPRT_ENA_MSK) & ~USBH_LL_HPRT_W1C_MSK) | intr_mask;
}
//Per Channel registers
// --------------------------- HCCHARi Register --------------------------------
@ -665,9 +694,11 @@ static inline void usbh_ll_chan_set_ep_type(volatile usb_host_chan_regs_t *chan,
}
}
static inline void usbh_ll_chan_set_ls(volatile usb_host_chan_regs_t *chan)
//Indicates whether channel is commuunicating with a LS device connected via a FS hub. Setting this bit to 1 will cause
//each packet to be preceded by a PREamble packet
static inline void usbh_ll_chan_set_lspddev(volatile usb_host_chan_regs_t *chan, bool is_ls)
{
chan->hcchar_reg.lspddev = 1;
chan->hcchar_reg.lspddev = is_ls;
}
static inline void usbh_ll_chan_set_dir(volatile usb_host_chan_regs_t *chan, bool is_in)
@ -685,11 +716,12 @@ static inline void usbh_ll_chan_set_mps(volatile usb_host_chan_regs_t *chan, uin
chan->hcchar_reg.mps = mps;
}
static inline void usbh_ll_chan_hcchar_init(volatile usb_host_chan_regs_t *chan, int dev_addr, int ep_num, int mps, usb_xfer_type_t type, bool is_in)
static inline void usbh_ll_chan_hcchar_init(volatile usb_host_chan_regs_t *chan, int dev_addr, int ep_num, int mps, usb_xfer_type_t type, bool is_in, bool is_ls)
{
//Sets all persistent fields of the channel over its lifetime
usbh_ll_chan_set_dev_addr(chan, dev_addr);
usbh_ll_chan_set_ep_type(chan, type);
usbh_ll_chan_set_lspddev(chan, is_ls);
usbh_ll_chan_set_dir(chan, is_in);
usbh_ll_chan_set_ep_num(chan, ep_num);
usbh_ll_chan_set_mps(chan, mps);

View File

@ -84,22 +84,29 @@ _Static_assert((RX_FIFO_LEN + NPTX_FIFO_LEN + PTX_FIFO_LEN + REG_FIFO_LEN) <= HW
* The following channel interrupt bits are currently checked (in order LSB to MSB)
* - USBH_LL_INTR_CHAN_XFERCOMPL
* - USBH_LL_INTR_CHAN_CHHLTD
* - USBH_LL_INTR_CHAN_AHBERR
* - USBH_LL_INTR_CHAN_STALL
* - USBH_LL_INTR_CHAN_BBLEER
* - USBH_LL_INTR_CHAN_BNAINTR
* - USBH_LL_INTR_CHAN_XCS_XACT_ERR
*
* Note the following points about channel interrupts:
* - Not all bits are unmaskable under scatter/gather
* - Those bits proxy their interrupt through the USBH_LL_INTR_CHAN_CHHLTD bit
* - USBH_LL_INTR_CHAN_XCS_XACT_ERR is always unmasked
* - When USBH_LL_INTR_CHAN_BNAINTR occurs, USBH_LL_INTR_CHAN_CHHLTD will NOT.
* - USBH_LL_INTR_CHAN_AHBERR doesn't actually ever happen on our system )i.e., ESP32S2 and later):
* - If the QTD list's starting address is an invalid address (e.g., NULL), the core will attempt to fetch that
* address for a transfer descriptor and probably gets all zeroes. It will interpret the zero as a bad QTD and
* return a USBH_LL_INTR_CHAN_BNAINTR instead.
* - If the QTD's buffer pointer is an invalid address, the core will attempt to read/write data to/from that
* invalid buffer address with NO INDICATION OF ERROR. The transfer will be acknowledged and treated as
* successful. Bad buffer pointers MUST BE CHECKED FROM HIGHER LAYERS INSTEAD.
*/
#define CHAN_INTRS_EN_MSK (USBH_LL_INTR_CHAN_XFERCOMPL | \
USBH_LL_INTR_CHAN_CHHLTD | \
USBH_LL_INTR_CHAN_BNAINTR)
#define CHAN_INTRS_ERROR_MSK (USBH_LL_INTR_CHAN_AHBERR | \
USBH_LL_INTR_CHAN_STALL | \
#define CHAN_INTRS_ERROR_MSK (USBH_LL_INTR_CHAN_STALL | \
USBH_LL_INTR_CHAN_BBLEER | \
USBH_LL_INTR_CHAN_BNAINTR | \
USBH_LL_INTR_CHAN_XCS_XACT_ERR)
@ -164,8 +171,11 @@ void usbh_hal_core_soft_reset(usbh_hal_context_t *hal)
}
//Set the default bits
set_defaults(hal);
//Clear all the flags
//Clear all the flags and channels
hal->flags.val = 0;
hal->channels.num_allocd = 0;
hal->channels.chan_pend_intrs_msk = 0;
memset(hal->channels.hdls, 0, sizeof(usbh_hal_chan_t *) * USBH_HAL_NUM_CHAN);
}
/* -----------------------------------------------------------------------------
@ -181,11 +191,12 @@ static inline void debounce_lock_enable(usbh_hal_context_t *hal)
void usbh_hal_port_enable(usbh_hal_context_t *hal)
{
usb_speed_t speed = usbh_ll_hprt_get_speed(hal->dev);
//Host Configuration
usbh_ll_hcfg_set_defaults(hal->dev);
usbh_ll_hcfg_set_defaults(hal->dev, speed);
//Todo: Set frame list entries and ena per sched
//Configure HFIR
usbh_ll_hfir_set_defaults(hal->dev);
usbh_ll_hfir_set_defaults(hal->dev, speed);
//Config FIFO sizes
usb_ll_set_rx_fifo_size(hal->dev, RX_FIFO_LEN);
usb_ll_set_nptx_fifo_size(hal->dev, RX_FIFO_LEN, NPTX_FIFO_LEN);
@ -225,7 +236,7 @@ bool usbh_hal_chan_alloc(usbh_hal_context_t *hal, usbh_hal_chan_t *chan_obj, voi
usbh_ll_chan_intr_read_and_clear(chan_obj->regs); //Clear the interrupt bits for that channel
usbh_ll_haintmsk_en_chan_intr(hal->dev, 1 << chan_obj->flags.chan_idx);
usbh_ll_chan_set_intr_mask(chan_obj->regs, CHAN_INTRS_EN_MSK); //Unmask interrupts for this channel
usbh_ll_chan_set_pid(chan_obj->regs, 0); //Set the initial PID to zero
usbh_ll_chan_set_pid(chan_obj->regs, 0); //Set the initial PID to zero
usbh_ll_chan_hctsiz_init(chan_obj->regs); //Set the non changing parts of the HCTSIZ registers (e.g., do_ping and sched info)
return true;
}
@ -235,8 +246,8 @@ void usbh_hal_chan_free(usbh_hal_context_t *hal, usbh_hal_chan_t *chan_obj)
{
//Can only free a channel when in the disabled state and descriptor list released
assert(!chan_obj->slot.flags.slot_acquired
&& !chan_obj->flags.active
&& !chan_obj->flags.error_pending);
&& !chan_obj->flags.active
&& !chan_obj->flags.error_pending);
//Deallocate channel
hal->channels.hdls[chan_obj->flags.chan_idx] = NULL;
hal->channels.num_allocd--;
@ -255,7 +266,8 @@ void usbh_hal_chan_set_ep_char(usbh_hal_chan_t *chan_obj, usbh_hal_ep_char_t *ep
ep_char->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK,
ep_char->mps,
ep_char->type,
ep_char->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK);
ep_char->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK,
ep_char->ls_via_fs_hub);
}
/* -----------------------------------------------------------------------------
@ -278,11 +290,11 @@ void usbh_hal_chan_activate(usbh_hal_chan_t *chan_obj, int num_to_skip)
usbh_ll_chan_start(chan_obj->regs);
}
bool usbh_hal_chan_request_halt(usbh_hal_chan_t *chan_obj)
bool usbh_hal_chan_slot_request_halt(usbh_hal_chan_t *chan_obj)
{
//Cannot request halt on a channel that is pending error handling
assert(!chan_obj->flags.error_pending);
if (usbh_ll_chan_is_active(chan_obj->regs)) {
if (usbh_ll_chan_is_active(chan_obj->regs) || chan_obj->flags.active) {
usbh_ll_chan_halt(chan_obj->regs);
chan_obj->flags.halt_requested = 1;
return false;
@ -294,6 +306,16 @@ bool usbh_hal_chan_request_halt(usbh_hal_chan_t *chan_obj)
-------------------------------- Event Handling --------------------------------
----------------------------------------------------------------------------- */
//When a device on the port is no longer valid (e.g., disconnect, port error). All channels are no longer valid
static void chan_all_halt(usbh_hal_context_t *hal)
{
for (int i = 0; i < USBH_HAL_NUM_CHAN; i++) {
if (hal->channels.hdls[i] != NULL) {
hal->channels.hdls[i]->flags.active = 0;
}
}
}
usbh_hal_port_event_t usbh_hal_decode_intr(usbh_hal_context_t *hal)
{
uint32_t intrs_core = usb_ll_intr_read_and_clear(hal->dev); //Read and clear core interrupts
@ -304,7 +326,7 @@ usbh_hal_port_event_t usbh_hal_decode_intr(usbh_hal_context_t *hal)
}
//Note: Do not change order of checks. Regressing events (e.g. enable -> disabled, connected -> connected)
//always take precendance. ENABLED < DISABLED < CONN < DISCONN < OVRCUR
usbh_hal_port_event_t event = -1;
usbh_hal_port_event_t event = USBH_HAL_PORT_EVENT_NONE;
//Check if this is a core or port event
if ((intrs_core & CORE_EVENTS_INTRS_MSK) || (intrs_port & PORT_EVENTS_INTRS_MSK)) {
@ -312,11 +334,13 @@ usbh_hal_port_event_t usbh_hal_decode_intr(usbh_hal_context_t *hal)
if (intrs_core & USB_LL_INTR_CORE_DISCONNINT) {
event = USBH_HAL_PORT_EVENT_DISCONN;
debounce_lock_enable(hal);
chan_all_halt(hal); //All channels are halted on a disconnect
//Mask the port connection and disconnection interrupts to prevent repeated triggering
} else if (intrs_port & USBH_LL_INTR_HPRT_PRTOVRCURRCHNG) {
//Check if this is an overcurrent or an overcurrent cleared
if (usbh_ll_hprt_get_port_overcur(hal->dev)) {
event = USBH_HAL_PORT_EVENT_OVRCUR;
chan_all_halt(hal); //All channels are halted on an overcurrent
} else {
event = USBH_HAL_PORT_EVENT_OVRCUR_CLR;
}
@ -325,13 +349,15 @@ usbh_hal_port_event_t usbh_hal_decode_intr(usbh_hal_context_t *hal)
event = USBH_HAL_PORT_EVENT_ENABLED;
} else { //Host port has been disabled
event = USBH_HAL_PORT_EVENT_DISABLED;
chan_all_halt(hal); //All channels are halted when the port is disabled
}
} else if (intrs_port & USBH_LL_INTR_HPRT_PRTCONNDET && !hal->flags.dbnc_lock_enabled) {
event = USBH_HAL_PORT_EVENT_CONN;
debounce_lock_enable(hal);
}
}
if (intrs_core & USB_LL_INTR_CORE_HCHINT) {
//Port events always take precendance over channel events
if (event == USBH_HAL_PORT_EVENT_NONE && (intrs_core & USB_LL_INTR_CORE_HCHINT)) {
//One or more channels have pending interrupts. Store the mask of those channels
hal->channels.chan_pend_intrs_msk = usbh_ll_get_chan_intrs_msk(hal->dev);
event = USBH_HAL_PORT_EVENT_CHAN;
@ -355,17 +381,15 @@ usbh_hal_chan_event_t usbh_hal_chan_decode_intr(usbh_hal_chan_t *chan_obj)
{
uint32_t chan_intrs = usbh_ll_chan_intr_read_and_clear(chan_obj->regs);
usbh_hal_chan_event_t chan_event;
//Currently, all cases where channel interrupts occur will also halt the channel
assert(chan_intrs & USBH_LL_INTR_CHAN_CHHLTD);
//Currently, all cases where channel interrupts occur will also halt the channel, except for BNA
assert(chan_intrs & (USBH_LL_INTR_CHAN_CHHLTD | USBH_LL_INTR_CHAN_BNAINTR));
chan_obj->flags.active = 0;
//Note: Do not change the current checking order of checks. Certain interrupts (e.g., errors) have precedence over others
if (chan_intrs & CHAN_INTRS_ERROR_MSK) { //One of the error interrupts has occurred.
//Note: Errors are uncommon, so we check against the entire interrupt mask to reduce frequency of entering this call path
//Store the error in hal context
usbh_hal_chan_error_t error;
if (chan_intrs & USBH_LL_INTR_CHAN_AHBERR) {
error = USBH_HAL_CHAN_ERROR_AHB;
} else if (chan_intrs & USBH_LL_INTR_CHAN_STALL) {
if (chan_intrs & USBH_LL_INTR_CHAN_STALL) {
error = USBH_HAL_CHAN_ERROR_STALL;
} else if (chan_intrs & USBH_LL_INTR_CHAN_BBLEER) {
error = USBH_HAL_CHAN_ERROR_PKT_BBL;
@ -393,8 +417,8 @@ usbh_hal_chan_event_t usbh_hal_chan_decode_intr(usbh_hal_chan_t *chan_obj)
chan_event = USBH_HAL_CHAN_EVENT_SLOT_HALT;
}
} else {
//Channel halted suddenly (i.e,, a disconnect)
chan_event = USBH_HAL_CHAN_EVENT_SUDDEN_HLT;
//Should never reach this point
abort();
}
return chan_event;
}

View File

@ -25,9 +25,11 @@ extern "C"
#define USB_DESC_ATTR __attribute__((packed))
/* -----------------------------------------------------------------------------
------------------------------ USB Protocol Enums ------------------------------
------------------------------- Common USB Types -------------------------------
----------------------------------------------------------------------------- */
// ----------------------------- Device Related --------------------------------
/**
* @brief Enumeration of USB PHY type
*/
@ -36,6 +38,18 @@ typedef enum {
USB_PHY_EXTERNAL, /**< Use an external USB PHY */
} usb_phy_t;
// ------------------------------ Bus Related ----------------------------------
/**
* @brief USB Standard Speeds
*/
typedef enum {
USB_SPEED_LOW = 0, /**< USB Low Speed (1.5 Mbit/s) */
USB_SPEED_FULL, /**< USB Full Speed (12 Mbit/s) */
} usb_speed_t;
// ---------------------------- Transfer Related -------------------------------
/**
* @brief The type of USB transfer
*
@ -49,17 +63,65 @@ typedef enum {
} usb_xfer_type_t;
/**
* @brief USB Standard Speeds
* @brief The status of a particular transfer
*/
typedef enum {
USB_SPEED_LOW = 0, /**< USB Low Speed (1.5 Mbit/s) */
USB_SPEED_FULL, /**< USB Full Speed (12 Mbit/s) */
} usb_speed_t;
USB_TRANSFER_STATUS_COMPLETED, /**< The transfer was successful (but may be short) */
USB_TRANSFER_STATUS_ERROR, /**< The transfer failed because due to excessive errors (e.g. no response or CRC error) */
USB_TRANSFER_STATUS_TIMED_OUT, /**< The transfer failed due to to a time out */
USB_TRANSFER_STATUS_CANCELLED, /**< The transfer was cancelled */
USB_TRANSFER_STATUS_STALL, /**< The transfer was stalled */
USB_TRANSFER_STATUS_NO_DEVICE, /**< The transfer failed because the device is no longer valid (e.g., disconencted */
USB_TRANSFER_STATUS_OVERFLOW, /**< The transfer as more data was sent than was requested */
} usb_transfer_status_t;
/**
* @brief Isochronous packet descriptor
*
* If the number of bytes in an IRP transfer is larger than the MPS of the
* endpoint, the IRP is split over multiple packets (one packet per bInterval
* of the endpoint). An array of Isochronous packet descriptos describes how
* an IRP should be split over multiple packets.
*/
typedef struct {
int length; /**< Number of bytes to transmit/receive in the packet */
int actual_length; /**< Actual number of bytes transmitted/received in the packet */
usb_transfer_status_t status; /**< Status of the packet */
} usb_iso_packet_desc_t;
/**
* @brief USB IRP (I/O Request Packet). See USB2.0 Spec
*
* An identifiable request by a software client to move data between itself (on the
* host) and an endpoint of a device in an appropriate direction.
*
* This structure represents the barebones of the request. Different layers of
* USB drivers will wrap their own objects around this.
*
* See 10.5.3.1 os USB2.0 specification
* Bulk: Represnts a single bulk transfer which a pipe will transparently split
* into multiple MPS transactions (until the last)
* Control: Represents a single contorl transfer with the setup packet at the
* first 8 bytes of the buffer.
* Interrupt: Represnts a single interrupt transaction
* Isochronous: Represnts a buffer of a stream of bytes which the pipe will transparently
* transfer the stream of bytes one or more service periods
*/
typedef struct {
int num_bytes; /**< Number of bytes in IRP. Control should exclude size of setup. IN should be integer multiple of MPS */
int actual_num_bytes; /**< Actual number of bytes transmitted/receives in the IRP */
uint8_t *data_buffer; /**< Pointer to data buffer. Must be DMA capable memory */
usb_transfer_status_t status; /**< Status of the transfer */
int num_iso_packets; /**< Only relevant to isochronous. Number of service periods to transfer data buffer over. Set to 0 for non-iso transfers */
usb_iso_packet_desc_t iso_packet_desc[0]; /**< Descriptors for each ISO packet */
} usb_irp_t;
/* -----------------------------------------------------------------------------
-------------------------------- Control Request -------------------------------
----------------------------------- Chapter 9 ----------------------------------
----------------------------------------------------------------------------- */
// ------------------------------ Control Request ------------------------------
/**
* @brief Size of a USB control transfer setup packet in bytes
*/
@ -183,11 +245,7 @@ _Static_assert(sizeof(usb_ctrl_req_t) == USB_CTRL_REQ_SIZE, "Size of usb_ctrl_re
(ctrl_req_ptr)->wLength = 0; \
})
/* -----------------------------------------------------------------------------
---------------------------------- Descriptors ---------------------------------
----------------------------------------------------------------------------- */
// -------------------------- Device Descriptor --------------------------------
// ---------------------------- Device Descriptor ------------------------------
/**
* @brief Size of a USB device descriptor in bytes

View File

@ -0,0 +1,12 @@
idf_build_get_property(target IDF_TARGET)
#USB Host is currently only supported on ESP32-S2
if(NOT "${target}" STREQUAL "esp32s2")
return()
endif()
idf_component_register(SRCS "hcd.c"
INCLUDE_DIRS ""
PRIV_INCLUDE_DIRS "private_include"
PRIV_REQUIRES "hal"
REQUIRES "")

1907
components/usb/hcd.c Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
# USB Host Stack Maintainers Notes
This document is intended future maintainers of the ESP-IDF USB Host stack.
Note: The underlying DWC_OTG controller will sometimes be referred to as the Host Controller in this document.
The host driver is currently split into the following layers, ordered from the lowest (furthest away from the user) to the highest layer (closest to the user).
* USB Host lower layer in `usbh_ll.h`
* USB HAL in `usbh_hal.h` and `usbh_hal.c`
* Host Controller Driver in `hcd.c` and `hcd.h`
# USB Host Lower Layer
* `usbh_ll.h` abstracts away the basic register operation of the **DWC** OTG controller
* The LL provides register operations of the DWC OTG controller operating under Host Mode using scatter/gather internal DMA.
* For non-host mode specific register operations (i.e. global registers), the functions are prefixed with `usb_ll_...()`
* For host mode specific register operations, the functions are prefixed with `usbh_ll_...()`
# USB Host HAL
The HAL layer abstracts the DWC_OTG operating in Host Mode using Internal Scatter/Gather DMA. The HAL presents an abstraction of a single Host Port and multiple channels.
## HAL Host Port
- Models a single USB port where a single device can be attached
- Actions: Port can be powered ON/OFF, reset, suspended
- Events: When an interrupt occurs, call `usbh_hal_decode_intr()` to decoded the port interrupt.
- Port can detect various events such as connection, disconnection, enabled (i.e., connected device successfully reset), overcurrent etc.
## HAL Channels
- Channels are essentially the controllers abstraction of USB pipes. At any one point in time, a channel can be configured to map to a particular endpoint on a particular connected device (i.e., a particular device address).
- Channels have to be allocated and freed. It's possible to change a channel's endpoint characteristics (i.e., EP number, device address, direction, transfer type etc) so long as the channel is in the Halted state whilst doing so.
- Channels transfer data using transfer descriptor lists (i.e., a list of DMA descriptors). Each channel has one slot for a single list. Use `usbh_hal_chan_slot_acquire()` to acquire a channel's list slot, and `usbh_hal_chan_activate()` to start the transfer.
- Once a transfer is completed, an channel event should be generated. Use `usbh_hal_chan_slot_release()` to free the slot, allowing for another transfer list to acquire the slot.
- To fill and parse a transfer descriptor list, use the `usbh_hal_xfer_desc_fill()` and `usbh_hal_xfer_desc_parse()` functions.
- Each channel and each channel slot will allow the callers to set a context variable. This allows client to associate a particular channel or an acquired slot with client objects (e.g., associate a channel to a HCD pipe object).
# Host Controller Driver (HCD)
The HCD (Host Controller Driver) abstracts the DWC_OTG as N number of ports and an arbitrary number of pipes that can be routed through one of the ports to a device. However note that the underlying hardware controller only has one port, so technically only one port can ever be enabled.
- In other words, the HCD essentially implements a root hub (not fully behavioral compliant) that contains a single port.
- Pipes are "an association between an endpoint on a device and software on the host". Transfer requests (where each transfer request represents an entire USB transfer) can be enqueued into a pipe for transmission, and dequeued from a pipe when completed.
The HCD currently has the following limitations:
- HCD **does not** "present the root hub and its behavior according to the hub class definition". We currently don't have a hub driver yet, so the port commands in the driver do not fully represent an interface of a USB hub as described in 10.4 of the USB2.0 spec.
- No more than 8 pipes can be allocated at any one time due to underlying Host Controllers 8 channel limit. In the future, we could make particular pipes share a single Host Controller channel.
- The HCD currently only supports Control and Bulk transfer types.
## HCD Port
- An HCD port can be as a simplified version of a port on the Root Hub of the host controller. However, the complexity of parsing Hub Requests is discarded in favor of port commands (`hcd_port_cmd_t`) as the current USB Host Stack does not support hubs yet.
- A port must first initialized before it can be used. A port is identified by its handled of type `hcd_port_handle_t`
- The port can be manipulated using commands such as
- Powering the port ON/OFF
- Issuing reset/resume signals
- The various host port events are represented in the `hcd_port_event_t` enumeration
- When a fatal error (such as a sudden disconnection or a port over current), the port will be put into the HCD_PORT_STATE_RECOVERY state. The port can be deinitialized from there, or recovered using `hcd_port_recover()`. All the pipes routed through the port will be made invalid.
## HCD Pipes
- Pipes can be opened to a particular endpoint based on a descriptor provided on allocation. If opening a default pipe, a `NULL` descriptor can be provided.
- Transfer requests can be enqueued into a pipe. Pipes use a linked list internally, so there is in-theory no limit to the number of transfer requests that can be enqueued.
- Transfer requests need to be dequeued once they are completed.
- Transfer requests are essentially wrappers for USB IRPs (I/O Request Packets). Once allocated, transfer request need to have their target IRP and pipe set before being enqueued.
- Since the IRP is a `typedef` used throughout the entire Host stack, each layer simply needs to pass the pointer of the IRP to the next layer thus minimizing the amount of copying required.
## HCD SW Arch
The software arch for the HCD has the following properties and intentions:
- Some static functions need to be called in critical sections whilst others need not. Therefore, static functions prefixed by an underscore ((e.g., `_some_static_func()`) will need to be called in critical sections.
- Some static functions may be blocking. Those functions will have a note in their descriptions.
- The HCD communicates events entirely through callbacks and polling/handling functions. The client can choose what type of data structure they want to use in the callbacks to keep track of port and pipe events. Callbacks don't even need to be used, and the HCD should be able to operate entirely on a polling basis.
- The port and each pipe have to be treated as completely separate entities (wither their own handles and events). This allows client to group these entities however it sees fit. For example, the client can:
- Let a hub driver manage the port, and manipulate the port via its port handle
- Group pipes into interfaces and expose the interface as a single entity
- The HCD will not internally allocate any tasks. It is up to the client to decide how distribute workload (e.g., a single while loop polling a port and all its pipes vs or each pipe having its own task).
- The HCD uses an interrupt process things that require low latency such as processing pipe transfer requests. Where possible, processing is deferred to the `hcd_port_handle_event()` to reduce ISR workload.
### Events
In order to communicate events to the client of the HCD, the HCD does not attempt to allocate any queues to store events that have occurred. Callbacks are used in order to achieve maximum flexibility. Within these callbacks, the client of the HCD is free too use whatever OS primitives they want to store/forward these events.
There are two types callbacks that the HCD offers:
- Port callback will run whenever an event a port occurs. `hcd_port_handle_event()` should be called after a port event occurs.
- A pipe callback on each pipe, that will run when a event occurs on a pipe.
The client of the HCD can also forego callbacks entirely and simply poll for port and pipe events using the `hcd_port_handle_event()` and `hcd_pipe_get_event()` respectively.
- Some of the port and pipe commands may need to block for a certain condition before the command is executed. For example when suspending the port, the port command must block until all pipes to finish their current transfers. The blocking and unblocking is handled by an internal event mechanism.
### Thread Safety
The HCD API is thread safe however the following limitations should be noted:
- It is the client's responsibility to ensure that `hcd_install()` is called before any other HCD function is called
- Likewise, it is the client's responsibility to ensure that events and pipes are cleared before calling `hcd_port_deinit()`.
- `hcd_port_command()` is thread safe, but only one port command can be executed at any one time. Therefore HCD internally used a mutex to protect against concurrent commands.
- If multiple threads attempt to execute a command on the sample one, all but one of those threads will return with an invalid state error.
- HCD functions should not be called from critical sections and interrupts, as some functions (e.g., `hcd_port_command()` and `hcd_pipe_command()`) may be blocking.

View File

@ -0,0 +1,526 @@
// Copyright 2015-2020 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#include <sys/queue.h>
#include "hal/usb_types.h"
#include "hal/usbh_hal.h"
#include "esp_err.h"
// ------------------------------------------------- Macros & Types ----------------------------------------------------
// ----------------------- States --------------------------
/**
* @brief States of the HCD port
*
* @note The port can be thought of as an abstraction of the Root Hub that contains
* a single port.
* @note These states roughly match the port states outlined in 11.5.1 of the
* USB2.0 specification.
*/
typedef enum {
HCD_PORT_STATE_NOT_POWERED, /**< The port is not powered */
HCD_PORT_STATE_DISCONNECTED, /**< The port is powered but no device is conencted */
HCD_PORT_STATE_DISABLED, /**< A device has connected to the port but has not been reset. SOF/keep alive are not being sent */
HCD_PORT_STATE_RESETTING, /**< The port is issuing a reset condition */
HCD_PORT_STATE_SUSPENDED, /**< The port has been suspended. */
HCD_PORT_STATE_RESUMING, /**< The port is issuing a resume condition */
HCD_PORT_STATE_ENABLED, /**< The port has been enabled. SOF/keep alive are being sent */
HCD_PORT_STATE_RECOVERY, /**< Port needs to be recovered from a fatal error (port error, overcurrent, or sudden disconnection) */
} hcd_port_state_t;
/**
* @brief States of an HCD pipe
*
* Active:
* - Pipe is able to transmit data. Transfer request can be enqueued.
* - Event if pipe has no transfer requests enqueued, it can still be in the active state.
* Halted:
* - An error has occurred on the pipe. Transfer request will no longer be executed.
* - Halt should be cleared using the clear command
* Invalid:
* - The underlying device that the pipe connects is not longer valid, thus making the pipe invalid.
* - Pending transfer requests should be dequeued and the pipe should be freed.
*/
typedef enum {
HCD_PIPE_STATE_ACTIVE, /**< The pipe is active */
HCD_PIPE_STATE_HALTED, /**< The pipe is halted */
HCD_PIPE_STATE_INVALID, /**< The pipe no longer exists and should be freed */
} hcd_pipe_state_t;
// ----------------------- Events --------------------------
/**
* @brief HCD port events
*
* On receiving a port event, hcd_port_handle_event() should be called to handle that event
*/
typedef enum {
HCD_PORT_EVENT_NONE, /**< No event has ocurred. Or the previous event is no longer valid */
HCD_PORT_EVENT_CONNECTION, /**< A device has been connected to the port */
HCD_PORT_EVENT_DISCONNECTION, /**< A device disconnection has been detected */
HCD_PORT_EVENT_ERROR, /**< A port error has been detected. Port is now HCD_PORT_STATE_RECOVERY */
HCD_PORT_EVENT_OVERCURRENT, /**< Overcurrent detected on the port. Port is now HCD_PORT_STATE_RECOVERY */
HCD_PORT_EVENT_SUDDEN_DISCONN, /**< The port has suddenly disconencted (i.e., there was an enabled device connected
to the port when the disconnection occurred. Port is now HCD_PORT_STATE_RECOVERY. */
} hcd_port_event_t;
/**
* @brief HCD pipe events
*
* @note Pipe error events will put the pipe into the HCD_PIPE_STATE_HALTED state
* @note The HCD_PIPE_EVENT_INVALID will put the pipe in the HCD_PIPE_STATE_INVALID state
*/
typedef enum {
HCD_PIPE_EVENT_NONE, /**< The pipe has no events (used to indicate no events when polling) */
HCD_PIPE_EVENT_XFER_REQ_DONE, /**< The pipe has completed a transfer request and can be dequeued */
HCD_PIPE_EVENT_INVALID, /**< The pipe is invalid because */
HCD_PIPE_EVENT_ERROR_XFER, /**< Excessive (three consecutive) transaction errors (e.g., no ACK, bad CRC etc) */
HCD_PIPE_EVENT_ERROR_XFER_NOT_AVAIL, /**< Transfer request was not available */
HCD_PIPE_EVENT_ERROR_OVERFLOW, /**< Received more data than requested. Usually a Packet babble error
(i.e., an IN packet has exceeded the endpoint's MPS) */
HCD_PIPE_EVENT_ERROR_STALL, /**< Pipe received a STALL response received */
} hcd_pipe_event_t;
// ---------------------- Commands -------------------------
/**
* @brief HCD port commands
*/
typedef enum {
HCD_PORT_CMD_POWER_ON, /**< Power ON the port */
HCD_PORT_CMD_POWER_OFF, /**< Power OFF the port */
HCD_PORT_CMD_RESET, /**< Issue a reset on the port */
HCD_PORT_CMD_SUSPEND, /**< Suspend the port */
HCD_PORT_CMD_RESUME, /**< Resume the port */
HCD_PORT_CMD_DISABLE, /**< Disable the port (stops the SOFs or keep alive) */
} hcd_port_cmd_t;
/**
* @brief HCD pipe commands
*
* The pipe commands represent the list of pipe manipulations outlined in 10.5.2.2. of USB2.0 specification.
*/
typedef enum {
HCD_PIPE_CMD_ABORT, /**< Retire all scheduled transfer requests. Pipe's state remains unchanged */
HCD_PIPE_CMD_RESET, /**< Retire all scheduled transfer requests. Pipe's state moves to active */
HCD_PIPE_CMD_CLEAR, /**< Pipe's state moves from halted to active */
HCD_PIPE_CMD_HALT /**< Pipe's state moves to halted */
} hcd_pipe_cmd_t;
// -------------------- Object Types -----------------------
/**
* @brief Port handle type
*/
typedef void * hcd_port_handle_t;
/**
* @brief Pipe handle type
*/
typedef void * hcd_pipe_handle_t;
/**
* @brief HCD transfer request handle type
*/
typedef void * hcd_xfer_req_handle_t;
/**
* @brief Port event callback type
*
* This callback is run when a port event occurs
*/
typedef bool (*hcd_port_isr_callback_t)(hcd_port_handle_t port_hdl, hcd_port_event_t port_event, void *user_arg, bool in_isr);
/**
* @brief Pipe event callback
*
* This callback is run when a pipe event occurs
*/
typedef bool (*hcd_pipe_isr_callback_t)(hcd_pipe_handle_t pipe_hdl, hcd_pipe_event_t pipe_event, void *user_arg, bool in_isr);
/**
* @brief HCD configuration structure
*/
typedef struct {
int intr_flags; /**< Interrupt flags for HCD interrupt */
} hcd_config_t;
/**
* @brief Port configuration structure
*/
typedef struct {
hcd_port_isr_callback_t callback; /**< HCD port event callback */
void *callback_arg; /**< User argument for HCD port callback */
void *context;
} hcd_port_config_t;
/**
* @brief Pipe configuration structure
*
* @note The callback can be set to NULL if no callback is required (e.g., using HCD in a polling manner).
*/
typedef struct {
hcd_pipe_isr_callback_t callback; /**< HCD pipe event ISR callback */
void *callback_arg; /**< User argument for HCD pipe callback */
void *context; /**< Context variable used to associate the pipe with upper layer object */
usb_desc_ep_t *ep_desc; /**< Pointer to endpoint descriptor of the pipe */
uint8_t dev_addr; /**< Device address of the pipe */
usb_speed_t dev_speed; /**< Speed of the device */
} hcd_pipe_config_t;
// --------------------------------------------- Host Controller Driver ------------------------------------------------
/**
* @brief Installs the Host Controller Driver
*
* - Allocates memory and interrupt resources for the HCD and underlying ports
* - Setups up HCD to use internal PHY
*
* @note This function must be called before any other HCD function is called
*
* @param config HCD configuration
* @retval ESP_OK: HCD successfully installed
* @retval ESP_ERR_NO_MEM: Insufficient memory
* @retval ESP_ERR_INVALID_STATE: HCD is already installed
* @retval ESP_ERR_NOT_FOUND: HCD could not allocate interrupt
* @retval ESP_ERR_INVALID_ARG: Arguments are invalid
*/
esp_err_t hcd_install(const hcd_config_t *config);
/**
* @brief Uninstalls the HCD
*
* Before uninstalling the HCD, the following conditions should be met:
* - All ports must be uninitialized, all pipes freed
*
* @retval ESP_OK: HCD successfully uninstalled
* @retval ESP_ERR_INVALID_STATE: HCD is not in the right condition to be uninstalled
*/
esp_err_t hcd_uninstall(void);
// ---------------------------------------------------- HCD Port -------------------------------------------------------
/**
* @brief Initialize a particular port of the HCD
*
* After a port is initialized, it will be put into the HCD_PORT_STATE_NOT_POWERED state.
*
* @note The host controller only has one port, thus the only valid port_number is 1
*
* @param[in] port_number Port number
* @param[in] port_config Port configuration
* @param[out] port_hdl Port handle
* @retval ESP_OK: Port enabled
* @retval ESP_ERR_NO_MEM: Insufficient memory
* @retval ESP_ERR_INVALID_STATE: The port is already enabled
* @retval ESP_ERR_NOT_FOUND: Port number not found
* @retval ESP_ERR_INVALID_ARG: Arguments are invalid
*/
esp_err_t hcd_port_init(int port_number, hcd_port_config_t *port_config, hcd_port_handle_t *port_hdl);
/**
* @brief Deinitialize a particular port
*
* The port must be placed in the HCD_PORT_STATE_NOT_POWERED or HCD_PORT_STATE_RECOVERY state before it can be
* deinitialized.
*
* @param port_hdl Port handle
* @retval ESP_OK: Port disabled
* @retval ESP_ERR_INVALID_STATE: The port is not in a condition to be disabled (not unpowered)
*/
esp_err_t hcd_port_deinit(hcd_port_handle_t port_hdl);
/**
* @brief Execute a port command
*
* Call this function to manipulate a port (e.g., powering it ON, sending a reset etc). The following conditions
* must be met when calling this function:
* - The port is in the correct state for the command (e.g., port must be suspend in order to use the resume command)
* - The port does not have any pending events
*
* @note This function is internally protected by a mutex. If multiple threads call this function, this function will
* can block.
* @note For some of the commands that involve a blocking delay (e.g., reset and resume), if the port's state changes
* unexpectedly (e.g., a disconnect during a resume), this function will return ESP_ERR_INVALID_RESPONSE.
*
* @param port_hdl Port handle
* @param command Command for the HCD port
* @retval ESP_OK: Command executed successfully
* @retval ESP_ERR_INVALID_STATE: Conditions have not been met to call this function
* @retval ESP_ERR_INVALID_RESPONSE: The command is no longer valid due to a change in the port's state
*/
esp_err_t hcd_port_command(hcd_port_handle_t port_hdl, hcd_port_cmd_t command);
/**
* @brief Get the port's current state
*
* @param port_hdl Port handle
* @return hcd_port_state_t Current port state
*/
hcd_port_state_t hcd_port_get_state(hcd_port_handle_t port_hdl);
/**
* @brief Get the speed of the port
*
* The speed of the port is determined by the speed of the device connected to it.
*
* @note This function is only valid after a device directly to the port and has been reset
*
* @param[in port_hdl Port handle
* @param[out] speed Speed of the port
* @retval ESP_OK Device speed obtained
* @retval ESP_ERR_INVALID_STATE: No valid device connected to the port
* @retval ESP_ERR_INVALID_ARG: Invalid arguments
*/
esp_err_t hcd_port_get_speed(hcd_port_handle_t port_hdl, usb_speed_t *speed);
/**
* @brief Handle a ports event
*
* When an port event occurs (as indicated by a callback), this function should be called the handle this event. A
* port's event should always be handled before attempting to execute a port command. Note that is actually handled
* may be different than the event reflected in the callback.
*
* If the port has no events, this function will return HCD_PORT_EVENT_NONE.
*
* @note If callbacks are not used, this function can also be used in a polling manner to repeatedely check for and
* handle a port's events.
* @note This function is internally protected by a mutex. If multiple threads call this function, this function will
* can block.
*
* @param port_hdl Port handle
* @return hcd_port_event_t The port event that was handled
*/
hcd_port_event_t hcd_port_handle_event(hcd_port_handle_t port_hdl);
/**
* @brief Recover a port after a fatal error has occurred on it
*
* The port must be in the HCD_PORT_STATE_RECOVERY state to be called. Recovering the port will involve issuing a soft
* reset on the underlying USB controller. The port will be returned to the HCD_PORT_STATE_NOT_POWERED state.
*
* @param port_hdl Port handle
* @retval ESP_OK Port recovered successfully
* @retval ESP_ERR_INVALID_STATE Port is not in the HCD_PORT_STATE_RECOVERY state
*/
esp_err_t hcd_port_recover(hcd_port_handle_t port_hdl);
/**
* @brief Get the context variable of a port
*
* @param port_hdl Port handle
* @return void* Context variable
*/
void *hcd_port_get_ctx(hcd_port_handle_t port_hdl);
// --------------------------------------------------- HCD Pipes -------------------------------------------------------
/**
* @brief Allocate a pipe
*
* When allocating a pipe, the HCD will assess whether there are sufficient resources (i.e., bus time, and controller
* channels). If sufficient, the pipe will be allocated.
*
* @note Currently, Interrupt and Isochronous pipes are not supported yet
* @note The host port must be in the enabled state before a pipe can be allcoated
*
* @param[in] port_hdl Handle of the port this pipe will be routed through
* @param[in] pipe_config Pipe configuration
* @param[out] pipe_hdl Pipe handle
*
* @retval ESP_OK: Pipe successfully allocated
* @retval ESP_ERR_NO_MEM: Insufficient memory
* @retval ESP_ERR_INVALID_ARG: Arguments are invalid
* @retval ESP_ERR_INVALID_STATE: Host port is not in the correct state to allocate a pipe
* @retval ESP_ERR_NOT_SUPPORTED: The pipe cannot be supported
*/
esp_err_t hcd_pipe_alloc(hcd_port_handle_t port_hdl, const hcd_pipe_config_t *pipe_config, hcd_pipe_handle_t *pipe_hdl);
/**
* @brief Free a pipe
*
* Frees the resources used by an HCD pipe. The pipe's handle should be discarded after calling this function. The pipe
* must be in following condition before it can be freed:
* - All transfers have been dequeued
*
* @param pipe_hdl Pipe handle
*
* @retval ESP_OK: Pipe successfully freed
* @retval ESP_ERR_INVALID_STATE: Pipe is not in a condition to be freed
*/
esp_err_t hcd_pipe_free(hcd_pipe_handle_t pipe_hdl);
/**
* @brief Update a pipe's device address and maximum packet size
*
* This function is intended to be called on default pipes during enumeration in order to update the pipe's device
* address and maximum packet size. This function can only be called on a pipe that has met the following conditions:
* - Pipe is still valid (i.e., not in the HCD_PIPE_STATE_INVALID state)
* - Pipe is not currently processing a command
* - All transfer request have been dequeued from the pipe
*
* @param pipe_hdl Pipe handle
* @param dev_addr New device address
* @param mps New Maximum Packet Size
*
* @retval ESP_OK: Pipe successfully updated
* @retval ESP_ERR_INVALID_STATE: Pipe is no in a condition to be updated
*/
esp_err_t hcd_pipe_update(hcd_pipe_handle_t pipe_hdl, uint8_t dev_addr, int mps);
/**
* @brief Get the context variable of a pipe from its handle
*
* @param pipe_hdl Pipe handle
* @return void* Context variable
*/
void *hcd_pipe_get_ctx(hcd_pipe_handle_t pipe_hdl);
/**
* @brief Get the current sate of the pipe
*
* @param pipe_hdl Pipe handle
* @return hcd_pipe_state_t Current state of the pipe
*/
hcd_pipe_state_t hcd_pipe_get_state(hcd_pipe_handle_t pipe_hdl);
/**
* @brief Execute a command on a particular pipe
*
* Pipe commands allow a pipe to be manipulated (such as clearing a halt, retiring all transfer requests etc). The
* following conditions must for a pipe command to be issued:
* - Pipe is still valid (i.e., not in the HCD_PIPE_STATE_INVALID)
* - No other thread/task processing a command on the pipe concurrently (will return)
*
* @note Some pipe commands will block until the pipe's current inflight transfer is completed. If the pipe's state
* changes unexpectedley, this function will return ESP_ERR_INVALID_RESPONSE
*
* @param pipe_hdl Pipe handle
* @param command Pipe command
* @retval ESP_OK: Command executed successfully
* @retval ESP_ERR_INVALID_STATE: The pipe is not in the correct state/condition too execute the command
* @retval ESP_ERR_INVALID_RESPONSE: The pipe's state changed unexpectedley
*/
esp_err_t hcd_pipe_command(hcd_pipe_handle_t pipe_hdl, hcd_pipe_cmd_t command);
/**
* @brief Get the last event that occurred on a pipe
*
* This function allows a pipe to be polled for events (i.e., when callbacks are not used). Once an event has been
* obtained, this function reset the last event of the pipe to HCD_PIPE_EVENT_NONE.
*
* @param pipe_hdl Pipe handle
* @return hcd_pipe_event_t Last pipe event to occur
*/
hcd_pipe_event_t hcd_pipe_get_event(hcd_pipe_handle_t pipe_hdl);
// ----------------------------------------------- HCD Transfer Requests -----------------------------------------------
/**
* @brief Allocate a transfer request
*
* @note The allocate transfer request will not have its target set (i.e., no target pipe and associated IRP). Call
* hcd_xfer_req_set_target() before enqueueing the transfer request
*
* @return hcd_xfer_req_handle_t Transfer request handle or NULL if failed.
*/
hcd_xfer_req_handle_t hcd_xfer_req_alloc(void);
/**
* @brief Free a transfer request
*
* @note The transfer request must be dequeued before it can be freed
*
* @param req_hdl Transfer request handle
*/
void hcd_xfer_req_free(hcd_xfer_req_handle_t req_hdl);
/**
* @brief Set a transfer request's target
*
* Setting a transfer request's target will associate a transfer request with a pipe and a USB IRP (i.e., the data). A
* transfer request's target must be set before it can be enqueued.
*
* @note This should only be called when a transfer requests that are not currently enqueued
*
* @param req_hdl Transfer request handle
* @param pipe_hdl Target pipe's handle
* @param irp Target IRP handle
* @param context Context variable to associate transfer request with upper layer object
*/
void hcd_xfer_req_set_target(hcd_xfer_req_handle_t req_hdl, hcd_pipe_handle_t pipe_hdl, usb_irp_t *irp, void *context);
/**
* @brief Get the target of a transfer request
*
* @note This should only be called when a transfer requests that are not currently enqueued
*
* @param[in] req_hdl Transfer request handle
* @param[out] pipe_hdl Target pipe's handle
* @param[out] irp Target IRP's handle
* @param[out] context Context variable
*/
void hcd_xfer_req_get_target(hcd_xfer_req_handle_t req_hdl, hcd_pipe_handle_t *pipe_hdl, usb_irp_t **irp, void **context);
/**
* @brief Enqueue a transfer request
*
* The following conditions must be met for a transfer request to be enqueued:
* - The transfer request's target must be set
* - Transfer request must not already be enqueued
* - The target pipe must be in the HCD_PIPE_STATE_ACTIVE state
*
* @param req_hdl Transfer request handle
* @retval ESP_OK: Transfer request enqueued successfully
* @retval ESP_ERR_INVALID_STATE: Conditions not met to enqueue transfer request
*/
esp_err_t hcd_xfer_req_enqueue(hcd_xfer_req_handle_t req_hdl);
/**
* @brief Dequeue a completed transfer request from a pipe
*
* This function should be called on a pipe after it receives an pipe event. If a pipe has multiple transfer requests
* that can be dequeued, this function must be called repeatedely until all transfer requests are dequeued. If a pipe
* has no more transfer requests to dequeue, this function will return NULL.
*
* @param pipe_hdl Pipe handle
* @return hcd_xfer_req_handle_t Transfer request handle or NULL if no more transfer requests to dequeue.
*/
hcd_xfer_req_handle_t hcd_xfer_req_dequeue(hcd_pipe_handle_t pipe_hdl);
/**
* @brief Abort an ongoing transfer request
*
* This function will attempt to abort an enqueued transfer request. If the transfer request has not yet been executed,
* it will be marked as "cancelled" and can be dequeued. If a transfer request is already in progress or has completed,
* it will not be affected by this function.
*
* @param req_hdl Transfer request handle
* @retval ESP_OK: Transfer request successfully aborted, or did not need to be aborted
* @retval ESP_ERR_INVALID_STATE: Transfer request was never enqueued
*/
esp_err_t hcd_xfer_req_abort(hcd_xfer_req_handle_t req_hdl);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,11 @@
idf_build_get_property(target IDF_TARGET)
#USB Host is currently only supported on ESP32-S2
if(NOT "${target}" STREQUAL "esp32s2")
return()
endif()
idf_component_register(SRC_DIRS "."
PRIV_INCLUDE_DIRS "." "../private_include"
PRIV_REQUIRES cmock usb test_utils
)

View File

@ -0,0 +1,4 @@
#
# Component Makefile (not used for tests, but CI checks test parity between GNU Make & CMake)
#
COMPONENT_CONFIG_ONLY := 1

View File

@ -0,0 +1,833 @@
// Copyright 2015-2020 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "unity.h"
#include "test_utils.h"
#include "esp_intr_alloc.h"
#include "esp_err.h"
#include "esp_attr.h"
#include "esp_rom_gpio.h"
#include "soc/gpio_pins.h"
#include "soc/gpio_sig_map.h"
#include "hal/usbh_ll.h"
#include "hcd.h"
// -------------------------------------------------- PHY Control ------------------------------------------------------
static void phy_force_conn_state(bool connected, TickType_t delay_ticks)
{
vTaskDelay(delay_ticks);
usb_wrap_dev_t *wrap = &USB_WRAP;
if (connected) {
//Swap back to internal PHY that is connected to a devicee
wrap->otg_conf.phy_sel = 0;
} else {
//Set externa PHY input signals to fixed voltage levels mimicing a disconnected state
esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, USB_EXTPHY_VP_IDX, false);
esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, USB_EXTPHY_VM_IDX, false);
esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, USB_EXTPHY_RCV_IDX, false);
//Swap to the external PHY
wrap->otg_conf.phy_sel = 1;
}
}
// ------------------------------------------------ Helper Functions ---------------------------------------------------
#define EVENT_QUEUE_LEN 5
#define NUM_XFER_REQS 3
#define XFER_DATA_MAX_LEN 256 //Just assume that will only IN/OUT 256 bytes for now
#define PORT_NUM 1
typedef struct {
hcd_port_handle_t port_hdl;
hcd_port_event_t port_event;
} port_event_msg_t;
typedef struct {
hcd_pipe_handle_t pipe_hdl;
hcd_pipe_event_t pipe_event;
} pipe_event_msg_t;
static bool port_callback(hcd_port_handle_t port_hdl, hcd_port_event_t port_event, void *user_arg, bool in_isr)
{
QueueHandle_t port_evt_queue = (QueueHandle_t)user_arg;
TEST_ASSERT(in_isr); //Current HCD implementation should never call a port callback in a task context
port_event_msg_t msg = {
.port_hdl = port_hdl,
.port_event = port_event,
};
BaseType_t xTaskWoken = pdFALSE;
xQueueSendFromISR(port_evt_queue, &msg, &xTaskWoken);
return (xTaskWoken == pdTRUE);
}
static bool pipe_callback(hcd_pipe_handle_t pipe_hdl, hcd_pipe_event_t pipe_event, void *user_arg, bool in_isr)
{
QueueHandle_t pipe_evt_queue = (QueueHandle_t)user_arg;
pipe_event_msg_t msg = {
.pipe_hdl = pipe_hdl,
.pipe_event = pipe_event,
};
if (in_isr) {
BaseType_t xTaskWoken = pdFALSE;
xQueueSendFromISR(pipe_evt_queue, &msg, &xTaskWoken);
return (xTaskWoken == pdTRUE);
} else {
xQueueSend(pipe_evt_queue, &msg, portMAX_DELAY);
return false;
}
}
static void expect_port_event(QueueHandle_t port_evt_queue, hcd_port_handle_t expected_hdl, hcd_port_event_t expected_event)
{
port_event_msg_t msg;
xQueueReceive(port_evt_queue, &msg, portMAX_DELAY);
TEST_ASSERT_EQUAL(expected_hdl, msg.port_hdl);
TEST_ASSERT_EQUAL(expected_event, msg.port_event);
printf("\t-> Port event\n");
}
static void expect_pipe_event(QueueHandle_t pipe_evt_queue, hcd_pipe_handle_t expected_hdl, hcd_pipe_event_t expected_event)
{
pipe_event_msg_t msg;
xQueueReceive(pipe_evt_queue, &msg, portMAX_DELAY);
TEST_ASSERT_EQUAL(expected_hdl, msg.pipe_hdl);
TEST_ASSERT_EQUAL(expected_event, msg.pipe_event);
}
/**
* @brief Creates port and pipe event queues. Sets up the HCD, and initializes a port.
*
* @param[out] port_evt_queue Port event queue
* @param[out] pipe_evt_queue Pipe event queue
* @param[out] port_hdl Port handle
*/
static void setup(QueueHandle_t *port_evt_queue, QueueHandle_t *pipe_evt_queue, hcd_port_handle_t *port_hdl)
{
*port_evt_queue = xQueueCreate(EVENT_QUEUE_LEN, sizeof(port_event_msg_t));
*pipe_evt_queue = xQueueCreate(EVENT_QUEUE_LEN, sizeof(pipe_event_msg_t));
TEST_ASSERT_NOT_EQUAL(NULL, *port_evt_queue);
TEST_ASSERT_NOT_EQUAL(NULL, *pipe_evt_queue);
//Install HCD
hcd_config_t config = {
.intr_flags = ESP_INTR_FLAG_LEVEL1,
};
TEST_ASSERT_EQUAL(ESP_OK, hcd_install(&config));
//Initialize a port
hcd_port_config_t port_config = {
.callback = port_callback,
.callback_arg = (void *)*port_evt_queue,
.context = NULL,
};
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_init(PORT_NUM, &port_config, port_hdl));
TEST_ASSERT_NOT_EQUAL(NULL, *port_hdl);
TEST_ASSERT_EQUAL(HCD_PORT_STATE_NOT_POWERED, hcd_port_get_state(*port_hdl));
phy_force_conn_state(false, 0); //Force disconnected state on PHY
}
/**
* @brief Deinitializes the port, uninstalls HCD, and frees port and pipe event queues
*
* @param[in] port_evt_queue Port event queue
* @param[in] pipe_evt_queue Pipe event semaphore
* @param[in] port_hdl Port handle
*/
static void teardown(QueueHandle_t port_evt_queue, QueueHandle_t pipe_evt_queue, hcd_port_handle_t port_hdl)
{
//Deinitialize a port
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_deinit(port_hdl));
//Uninstall the HCD
TEST_ASSERT_EQUAL(ESP_OK, hcd_uninstall());
vQueueDelete(port_evt_queue);
vQueueDelete(pipe_evt_queue);
}
/**
* @brief Powers ON a port and waits for a connection, then resets the connected device
*
* @param port_hdl Port handle
* @param port_evt_queue Port event queue
*/
static void wait_for_connection(hcd_port_handle_t port_hdl, QueueHandle_t port_evt_queue)
{
//Power ON the port
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_POWER_ON));
TEST_ASSERT_EQUAL(HCD_PORT_STATE_DISCONNECTED, hcd_port_get_state(port_hdl));
//Wait for connection event
printf("Waiting for conenction\n");
phy_force_conn_state(true, pdMS_TO_TICKS(100)); //Allow for connected state on PHY
expect_port_event(port_evt_queue, port_hdl, HCD_PORT_EVENT_CONNECTION);
TEST_ASSERT_EQUAL(HCD_PORT_EVENT_CONNECTION, hcd_port_handle_event(port_hdl));
TEST_ASSERT_EQUAL(HCD_PORT_STATE_DISABLED, hcd_port_get_state(port_hdl));
//Reset newly connected device
printf("Resetting\n");
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_RESET));
TEST_ASSERT_EQUAL(HCD_PORT_STATE_ENABLED, hcd_port_get_state(port_hdl));
//Get speed of conencted
usb_speed_t port_speed;
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_get_speed(port_hdl, &port_speed));
if (port_speed == USB_SPEED_FULL) {
printf("Full speed enabled\n");
} else {
printf("Low speed enabled\n");
}
}
/**
* @brief Disables the port, waits for a disconnection, then powers OFF the port
*
* @param port_hdl Port handle
* @param port_evt_queue Port event queue
* @param already_disabled If the port is already disabled, it will skip disabling the port
*/
static void wait_for_disconnection(hcd_port_handle_t port_hdl, QueueHandle_t port_evt_queue, bool already_disabled)
{
if (!already_disabled) {
//Disable the device
printf("Disabling\n");
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_DISABLE));
TEST_ASSERT_EQUAL(HCD_PORT_STATE_DISABLED, hcd_port_get_state(port_hdl));
}
//Wait for a safe disconnect
printf("Waiting for disconnection\n");
phy_force_conn_state(false, pdMS_TO_TICKS(100)); //Force disconnected state on PHY
expect_port_event(port_evt_queue, port_hdl, HCD_PORT_EVENT_DISCONNECTION);
TEST_ASSERT_EQUAL(HCD_PORT_EVENT_DISCONNECTION, hcd_port_handle_event(port_hdl));
TEST_ASSERT_EQUAL(HCD_PORT_STATE_DISCONNECTED, hcd_port_get_state(port_hdl));
//Power down the port
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_POWER_OFF));
TEST_ASSERT_EQUAL(HCD_PORT_STATE_NOT_POWERED, hcd_port_get_state(port_hdl));
}
static void alloc_pipe_and_xfer_reqs(hcd_port_handle_t port_hdl,
QueueHandle_t pipe_evt_queue,
hcd_pipe_handle_t *pipe_hdl,
hcd_xfer_req_handle_t *req_hdls,
uint8_t **data_buffers,
usb_irp_t **irps,
int num_xfers)
{
//We don't support hubs yet. Just get the speed of the port to determine the speed of the device
usb_speed_t port_speed;
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_get_speed(port_hdl, &port_speed));
//Create default pipe
printf("Creating default pipe\n");
hcd_pipe_config_t config = {
.callback = pipe_callback,
.callback_arg = (void *)pipe_evt_queue,
.context = NULL,
.ep_desc = NULL, //NULL EP descriptor to create a default pipe
.dev_addr = 0,
.dev_speed = port_speed,
};
TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_alloc(port_hdl, &config, pipe_hdl));
TEST_ASSERT_NOT_EQUAL(NULL, *pipe_hdl);
//Create transfer requests (and other required objects such as IRPs and data buffers)
printf("Creating transfer requests\n");
for (int i = 0; i < num_xfers; i++) {
//Allocate transfer request object
req_hdls[i] = hcd_xfer_req_alloc();
TEST_ASSERT_NOT_EQUAL(NULL, req_hdls[i]);
//Allocate data buffers
data_buffers[i] = heap_caps_malloc(sizeof(usb_ctrl_req_t) + XFER_DATA_MAX_LEN, MALLOC_CAP_DMA);
TEST_ASSERT_NOT_EQUAL(NULL, data_buffers[i]);
//Allocate IRP object
irps[i] = heap_caps_malloc(sizeof(usb_irp_t), MALLOC_CAP_DEFAULT);
TEST_ASSERT_NOT_EQUAL(NULL, irps[i]);
//Set the transfer request's target
hcd_xfer_req_set_target(req_hdls[i], *pipe_hdl, irps[i], NULL);
}
}
static void free_pipe_and_xfer_reqs(hcd_pipe_handle_t pipe_hdl,
hcd_xfer_req_handle_t *req_hdls,
uint8_t **data_buffers,
usb_irp_t **irps,
int num_xfers)
{
printf("Freeing transfer requets\n");
//Free transfer requests (and their associated objects such as IRPs and data buffers)
for (int i = 0; i < num_xfers; i++) {
heap_caps_free(irps[i]);
heap_caps_free(data_buffers[i]);
hcd_xfer_req_free(req_hdls[i]);
}
printf("Freeing default pipe\n");
//Delete the pipe
TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_free(pipe_hdl));
}
// ------------------------------------------------ Host Port Tests ----------------------------------------------------
/*
Test a port sudden disconnect and port recovery
Purpose: Test that when sudden disconnection happens on an HCD port, the port will
- Generate the HCD_PORT_EVENT_SUDDEN_DISCONN and be put into the HCD_PORT_STATE_RECOVERY state
- Ongoing transfers requests and pipes are handled correctly
Procedure:
- Setup HCD, a default pipe, and multiple transfer requests
- Start transfers but immediately trigger a disconnect
- Check that HCD_PORT_EVENT_SUDDEN_DISCONN event is generated
- Check that default pipe is invalid and transfer requests can be dequeued
- Recover the port and try to connect then disconnect again (to make sure the port works port recovery)
- Teardown HCD
*/
TEST_CASE("Test HCD port sudden disconnect", "[hcd][ignore]")
{
QueueHandle_t port_evt_queue;
QueueHandle_t pipe_evt_queue;
hcd_port_handle_t port_hdl;
setup(&port_evt_queue, &pipe_evt_queue, &port_hdl);
wait_for_connection(port_hdl, port_evt_queue);
vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS)
//Allocate transfer requests
hcd_pipe_handle_t default_pipe;
hcd_xfer_req_handle_t req_hdls[NUM_XFER_REQS];
uint8_t *data_buffers[NUM_XFER_REQS];
usb_irp_t *irps[NUM_XFER_REQS];
alloc_pipe_and_xfer_reqs(port_hdl, pipe_evt_queue, &default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS);
//Initialize transfer requests to send a "Get Device Descriptor" request
for (int i = 0; i < NUM_XFER_REQS; i++) {
irps[i]->num_bytes = 64; //1 worst case MPS
USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) data_buffers[i], 0, XFER_DATA_MAX_LEN);
irps[i]->data_buffer = data_buffers[i];
irps[i]->num_iso_packets = 0;
}
//Enqueue those transfer requests
for (int i = 0; i < NUM_XFER_REQS; i++) {
TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i]));
}
phy_force_conn_state(false, 0); //Force disconnected state on PHY
expect_port_event(port_evt_queue, port_hdl, HCD_PORT_EVENT_SUDDEN_DISCONN);
TEST_ASSERT_EQUAL(HCD_PORT_EVENT_SUDDEN_DISCONN, hcd_port_handle_event(port_hdl));
TEST_ASSERT_EQUAL(HCD_PORT_STATE_RECOVERY, hcd_port_get_state(port_hdl));
printf("Sudden disconnect\n");
//Handling the disconenction event should have invalidated all pipes.
//Pipe should have received (zero or more HCD_PIPE_EVENT_XFER_REQ_DONE) followed by a HCD_PIPE_EVENT_INVALID (MUST OCCUR)
int num_pipe_events = EVENT_QUEUE_LEN - uxQueueSpacesAvailable(pipe_evt_queue);
for (int i = 0; i < num_pipe_events - 1; i++) {
expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE);
}
expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_INVALID);
TEST_ASSERT_EQUAL(hcd_pipe_get_state(default_pipe), HCD_PIPE_STATE_INVALID);
//Dequeue transfer requests
for (int i = 0; i < NUM_XFER_REQS; i++) {
hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe);
hcd_pipe_handle_t pipe_hdl;
usb_irp_t *irp;
void *context;
hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context);
TEST_ASSERT_EQUAL(default_pipe, pipe_hdl);
TEST_ASSERT_EQUAL(irps[i], irp);
TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED || irp->status == USB_TRANSFER_STATUS_NO_DEVICE);
TEST_ASSERT_EQUAL(NULL, context);
}
//Free transfer requests
free_pipe_and_xfer_reqs(default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS);
//Recover the port should return to the to NOT POWERED state
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_recover(port_hdl));
TEST_ASSERT_EQUAL(HCD_PORT_STATE_NOT_POWERED, hcd_port_get_state(port_hdl));
//Recovered port should be able to connect and disconenct again
wait_for_connection(port_hdl, port_evt_queue);
wait_for_disconnection(port_hdl, port_evt_queue, false);
teardown(port_evt_queue, pipe_evt_queue, port_hdl);
}
/*
Test port suspend and resume with active pipes
Purpose:
- Test p[ort suspend and resume commands work correctly whilst there are active pipes with ongoing transfers
- When suspending, the pipes should be allowed to finish their current ongoing transfer before the bus is suspended.
- When resuming, pipes with pending transfer should be started after the bus is resumed.
Procedure:
- Setup HCD, a port, a default pipe, and multiple transfer requests
- Start transfers but immediately suspend the port
- Resume the port
- Check all transfer requests have also be resumed and completed on port resume
- Teardown
*/
TEST_CASE("Test HCD port suspend and resume", "[hcd][ignore]")
{
QueueHandle_t port_evt_queue;
QueueHandle_t pipe_evt_queue;
hcd_port_handle_t port_hdl;
setup(&port_evt_queue, &pipe_evt_queue, &port_hdl);
wait_for_connection(port_hdl, port_evt_queue);
vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS)
//Allocate transfer requests
hcd_pipe_handle_t default_pipe;
hcd_xfer_req_handle_t req_hdls[NUM_XFER_REQS];
uint8_t *data_buffers[NUM_XFER_REQS];
usb_irp_t *irps[NUM_XFER_REQS];
alloc_pipe_and_xfer_reqs(port_hdl, pipe_evt_queue, &default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS);
//Initialize transfer requests to send a "Get Device Descriptor" request
for (int i = 0; i < NUM_XFER_REQS; i++) {
irps[i]->num_bytes = 64; //1 worst case MPS
USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) data_buffers[i], 0, XFER_DATA_MAX_LEN);
irps[i]->data_buffer = data_buffers[i];
irps[i]->num_iso_packets = 0;
}
//Enqueue those transfer requests
for (int i = 0; i < NUM_XFER_REQS; i++) {
TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i]));
}
//Immediately suspend the bus whilst pies are active
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_SUSPEND));
TEST_ASSERT_EQUAL(HCD_PORT_STATE_SUSPENDED, hcd_port_get_state(port_hdl));
printf("Suspended\n");
vTaskDelay(pdMS_TO_TICKS(100));
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_RESUME));
TEST_ASSERT_EQUAL(HCD_PORT_STATE_ENABLED, hcd_port_get_state(port_hdl));
vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for resumed transfers to complete
expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE);
//Dequeue transfer requests
for (int i = 0; i < NUM_XFER_REQS; i++) {
hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe);
hcd_pipe_handle_t pipe_hdl;
usb_irp_t *irp;
void *context;
hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context);
TEST_ASSERT_EQUAL(default_pipe, pipe_hdl);
TEST_ASSERT_EQUAL(irps[i], irp);
TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED);
TEST_ASSERT_EQUAL(NULL, context);
}
//Free transfer requests
free_pipe_and_xfer_reqs(default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS);
vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS)
wait_for_disconnection(port_hdl, port_evt_queue, false);
teardown(port_evt_queue, pipe_evt_queue, port_hdl);
}
/*
Test HCD port disable with active pipes
Purpose:
- Test that the port disable command works correctly with active pipes
- Pipes should be to finish their current ongoing transfer before port is disabled
- After disabling the port, all pipes should become invalid.
Procedure:
- Setup HCD, a default pipe, and multiple transfer requests
- Start transfers but immediately disable the port
- Check pipe received invalid event
- Check that transfer are either done or not executed
- Teardown
*/
TEST_CASE("Test HCD port disable", "[hcd][ignore]")
{
QueueHandle_t port_evt_queue;
QueueHandle_t pipe_evt_queue;
hcd_port_handle_t port_hdl;
setup(&port_evt_queue, &pipe_evt_queue, &port_hdl);
wait_for_connection(port_hdl, port_evt_queue);
vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS)
//Allocate transfer requests
hcd_pipe_handle_t default_pipe;
hcd_xfer_req_handle_t req_hdls[NUM_XFER_REQS];
uint8_t *data_buffers[NUM_XFER_REQS];
usb_irp_t *irps[NUM_XFER_REQS];
alloc_pipe_and_xfer_reqs(port_hdl, pipe_evt_queue, &default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS);
//Initialize transfer requests to send a "Get Device Descriptor" request
for (int i = 0; i < NUM_XFER_REQS; i++) {
irps[i]->num_bytes = 64; //1 worst case MPS
USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) data_buffers[i], 0, XFER_DATA_MAX_LEN);
irps[i]->data_buffer = data_buffers[i];
irps[i]->num_iso_packets = 0;
}
//Enqueue those transfer requests
for (int i = 0; i < NUM_XFER_REQS; i++) {
TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i]));
}
//Immediately disable port
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_DISABLE));
TEST_ASSERT_EQUAL(HCD_PORT_STATE_DISABLED, hcd_port_get_state(port_hdl));
printf("Disabled\n");
//Pipe should have received (zero or more HCD_PIPE_EVENT_XFER_REQ_DONE) followed by a HCD_PIPE_EVENT_INVALID (MUST OCCUR)
int num_pipe_events = EVENT_QUEUE_LEN - uxQueueSpacesAvailable(pipe_evt_queue);
for (int i = 0; i < num_pipe_events - 1; i++) {
expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE);
}
expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_INVALID);
for (int i = 0; i < NUM_XFER_REQS; i++) {
hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe);
hcd_pipe_handle_t pipe_hdl;
usb_irp_t *irp;
void *context;
hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context);
TEST_ASSERT_EQUAL(default_pipe, pipe_hdl);
TEST_ASSERT_EQUAL(irps[i], irp);
TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED || irp->status == USB_TRANSFER_STATUS_NO_DEVICE);
TEST_ASSERT_EQUAL(NULL, context);
}
//Free transfer requests
free_pipe_and_xfer_reqs(default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS);
//Already disabled. Disconnect and teardown
wait_for_disconnection(port_hdl, port_evt_queue, true);
teardown(port_evt_queue, pipe_evt_queue, port_hdl);
}
/*
Test HCD port command bailout
Purpose:
- Test that if the a port's state changes whilst a command is being executed, the port command should return
ESP_ERR_INVALID_RESPONSE
Procedure:
- Setup HCD and wait for connection
- Suspend the port
- Resume the port but trigger a disconnect from another thread during the resume command
- Check that port command returns ESP_ERR_INVALID_RESPONSE
*/
static void concurrent_task(void *arg)
{
SemaphoreHandle_t sync_sem = (SemaphoreHandle_t) arg;
xSemaphoreTake(sync_sem, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(10)); //Give a short delay let reset command start in main thread
//Forcibly a disconenction
phy_force_conn_state(false, 0);
vTaskDelay(portMAX_DELAY); //Block forever and wait to be deleted
}
TEST_CASE("Test HCD port command bailout", "[hcd][ignore]")
{
QueueHandle_t port_evt_queue;
QueueHandle_t pipe_evt_queue;
hcd_port_handle_t port_hdl;
setup(&port_evt_queue, &pipe_evt_queue, &port_hdl);
wait_for_connection(port_hdl, port_evt_queue);
vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS)
//Create task to run commands concurrently
SemaphoreHandle_t sync_sem = xSemaphoreCreateBinary();
TaskHandle_t task_handle;
TEST_ASSERT_NOT_EQUAL(NULL, sync_sem);
TEST_ASSERT_EQUAL(pdTRUE, xTaskCreatePinnedToCore(concurrent_task, "tsk", 4096, (void *) sync_sem, UNITY_FREERTOS_PRIORITY + 1, &task_handle, 0));
//Suspend the device
printf("Suspending\n");
TEST_ASSERT_EQUAL(ESP_OK, hcd_port_command(port_hdl, HCD_PORT_CMD_SUSPEND));
vTaskDelay(pdMS_TO_TICKS(20)); //Short delay for device to enter suspend state
printf("Attempting to resume\n");
xSemaphoreGive(sync_sem); //Trigger concurrent task
//Attempt to resume the port. But the concurrent task should override this with a disconnection event
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_RESPONSE, hcd_port_command(port_hdl, HCD_PORT_CMD_RESUME));
//Check that concurrent task triggered a sudden disconnection
expect_port_event(port_evt_queue, port_hdl, HCD_PORT_EVENT_SUDDEN_DISCONN);
TEST_ASSERT_EQUAL(HCD_PORT_EVENT_SUDDEN_DISCONN, hcd_port_handle_event(port_hdl));
TEST_ASSERT_EQUAL(HCD_PORT_STATE_RECOVERY, hcd_port_get_state(port_hdl));
vTaskDelay(pdMS_TO_TICKS(10)); //Short delay for concurrent task finish running
vTaskDelete(task_handle);
vSemaphoreDelete(sync_sem);
//Directly teardown the port without recovery
teardown(port_evt_queue, pipe_evt_queue, port_hdl);
}
// --------------------------------------------------- Pipe Tests ------------------------------------------------------
/*
Test HCD Transfer Requests (normal completion and early abort)
Purpose:
- Test that pipes can be created
- Transfer requests can be created and enqueued
- Pipe returns HCD_PIPE_EVENT_XFER_REQ_DONE
- Test that transfer requests can be aborted when enqueued
Procedure:
- Setup
- Allocate transfer requests. Initialize as Get Device Descriptor request
- Enqueue transfer requests
- Expect HCD_PIPE_EVENT_XFER_REQ_DONE. Deallocate transfer requests
- Requeue transfer requests, but abort them immediately
- Teardown
*/
TEST_CASE("Test HCD pipe transfer request", "[hcd][ignore]")
{
QueueHandle_t port_evt_queue;
QueueHandle_t pipe_evt_queue;
hcd_port_handle_t port_hdl;
setup(&port_evt_queue, &pipe_evt_queue, &port_hdl);
wait_for_connection(port_hdl, port_evt_queue);
vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS)
//Allocate transfer requests
hcd_pipe_handle_t default_pipe;
hcd_xfer_req_handle_t req_hdls[NUM_XFER_REQS];
uint8_t *data_buffers[NUM_XFER_REQS];
usb_irp_t *irps[NUM_XFER_REQS];
alloc_pipe_and_xfer_reqs(port_hdl, pipe_evt_queue, &default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS);
//Initialize transfer requests to send a "Get Device Descriptor" request
for (int i = 0; i < NUM_XFER_REQS; i++) {
irps[i]->num_bytes = 64; //1 worst case MPS
USB_CTRL_REQ_INIT_GET_DEVC_DESC((usb_ctrl_req_t *) data_buffers[i]);
irps[i]->data_buffer = data_buffers[i];
irps[i]->num_iso_packets = 0;
}
//Enqueue those transfer requests
for (int i = 0; i < NUM_XFER_REQS; i++) {
TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i]));
}
//Wait for each done event of each transfer request
for (int i = 0; i < NUM_XFER_REQS; i++) {
expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE);
}
//Dequeue transfer requests and check results.
for (int i = 0; i < NUM_XFER_REQS; i++) {
hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe);
TEST_ASSERT_EQUAL(req_hdls[i], req_hdl);
hcd_pipe_handle_t pipe_hdl;
usb_irp_t *irp;
void *context;
hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context);
TEST_ASSERT_EQUAL(default_pipe, pipe_hdl);
TEST_ASSERT_EQUAL(irp, irps[i]);
TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->status);
TEST_ASSERT_EQUAL(NULL, context);
}
//Enqueue them again but abort them short after
for (int i = 0; i < NUM_XFER_REQS; i++) {
TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i]));
}
for (int i = 0; i < NUM_XFER_REQS; i++) {
hcd_xfer_req_abort(req_hdls[i]);
}
vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for any inflight transfers to complete
expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE);
//Dequeue transfer rqeuests and check results of aborted transfer request
for (int i = 0; i < NUM_XFER_REQS; i++) {
hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe);
hcd_pipe_handle_t pipe_hdl;
usb_irp_t *irp;
void *context;
hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context);
TEST_ASSERT_EQUAL(default_pipe, pipe_hdl);
//No need to check req_hdl or IRP order as abort will cause them to dequeu out of order
TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED || irp->status == USB_TRANSFER_STATUS_CANCELLED);
TEST_ASSERT_EQUAL(NULL, context);
}
//Free transfer requests
free_pipe_and_xfer_reqs(default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS);
vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS)
wait_for_disconnection(port_hdl, port_evt_queue, false);
teardown(port_evt_queue, pipe_evt_queue, port_hdl);
}
/*
Test HCD pipe STALL condition, abort, and clear
Purpose:
- Test that a pipe can react to a STALL (i.e., a HCD_PIPE_EVENT_HALTED event)
- The HCD_PIPE_CMD_ABORT can retire all transfer requests
- Pipe clear command can return the pipe to being active
Procedure:
- Setup HCD and a port, a default pipe, and multiple transfer requests
- Corrupt the first transfer request, then enqueue all of them.
- The corrupted transfer request should trigger a STALL response from the endpoint
- Check that the correct pipe event, error, and state is returned from the pipe
- Check that the other transfers can be retired using the abort command
- Check that the halt can be cleared by using the clear command
- Requeue correct transfers to check that pipe still works after being cleared
- Teardown
*/
TEST_CASE("Test HCD pipe STALL", "[hcd][ignore]")
{
QueueHandle_t port_evt_queue;
QueueHandle_t pipe_evt_queue;
hcd_port_handle_t port_hdl;
setup(&port_evt_queue, &pipe_evt_queue, &port_hdl);
wait_for_connection(port_hdl, port_evt_queue);
vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS)
//Allocate transfer requests
hcd_pipe_handle_t default_pipe;
hcd_xfer_req_handle_t req_hdls[NUM_XFER_REQS];
uint8_t *data_buffers[NUM_XFER_REQS];
usb_irp_t *irps[NUM_XFER_REQS];
alloc_pipe_and_xfer_reqs(port_hdl, pipe_evt_queue, &default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS);
//Initialize transfer requests to send a "Get Device Descriptor" request
for (int i = 0; i < NUM_XFER_REQS; i++) {
irps[i]->num_bytes = 64; //1 worst case MPS
USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) data_buffers[i], 0, XFER_DATA_MAX_LEN);
irps[i]->data_buffer = data_buffers[i];
irps[i]->num_iso_packets = 0;
}
//Corrupt first transfer so that it triggers a STALL
((usb_ctrl_req_t *) data_buffers[0])->bRequest = 0xAA;
//Enqueue those transfer requests
for (int i = 0; i < NUM_XFER_REQS; i++) {
TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i]));
}
vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for transfers to complete
//Check that pipe has been stalled
printf("Expecting STALL\n");
expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_ERROR_STALL);
TEST_ASSERT_EQUAL(HCD_PIPE_STATE_HALTED, hcd_pipe_get_state(default_pipe));
//Call the pipe abort command to retire all transfers then dequeue all transfers
TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_command(default_pipe, HCD_PIPE_CMD_ABORT));
for (int i = 0; i < NUM_XFER_REQS; i++) {
hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe);
hcd_pipe_handle_t pipe_hdl;
usb_irp_t *irp;
void *context;
hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context);
TEST_ASSERT_EQUAL(default_pipe, pipe_hdl);
TEST_ASSERT_EQUAL(irp, irps[i]);
TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_STALL || irp->status == USB_TRANSFER_STATUS_CANCELLED);
TEST_ASSERT_EQUAL(NULL, context);
}
//Call the clear command to un-stall the pipe
TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_command(default_pipe, HCD_PIPE_CMD_CLEAR));
TEST_ASSERT_EQUAL(HCD_PIPE_STATE_ACTIVE, hcd_pipe_get_state(default_pipe));
//Correct first transfer then requeue
USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) data_buffers[0], 0, XFER_DATA_MAX_LEN);
for (int i = 0; i < NUM_XFER_REQS; i++) {
TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i]));
}
vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for transfers to complete
expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE);
//Dequeue transfer requests and check results.
for (int i = 0; i < NUM_XFER_REQS; i++) {
hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe);
TEST_ASSERT_EQUAL(req_hdls[i], req_hdl);
hcd_pipe_handle_t pipe_hdl;
usb_irp_t *irp;
void *context;
hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context);
TEST_ASSERT_EQUAL(default_pipe, pipe_hdl);
TEST_ASSERT_EQUAL(irp, irps[i]);
TEST_ASSERT_EQUAL(USB_TRANSFER_STATUS_COMPLETED, irp->status);
TEST_ASSERT_EQUAL(NULL, context);
}
//Free transfer requests
free_pipe_and_xfer_reqs(default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS);
vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS)
wait_for_disconnection(port_hdl, port_evt_queue, false);
teardown(port_evt_queue, pipe_evt_queue, port_hdl);
}
/*
Test Pipe runtime halt and clear
Purpose:
- Test that a pipe can be halted with a command whilst there are ongoing transfer requests
- Test that a pipe can be un-halted with a HCD_PIPE_CMD_CLEAR
- Test that enqueued transfer requests are resumed when pipe is cleared
Procedure:
- Setup HCD, a default pipe, and multiple transfer requests
- Enqueue transfer requests but execute a HCD_PIPE_CMD_HALT command immediately after. Halt command should let on
the current going transfer request finish before actually halting the pipe.
- Clear the pipe halt using a HCD_PIPE_CMD_HALT command. Enqueued transfer requests will be resumed
- Check that all transfer requests have completed successfully.
- Teardown
*/
TEST_CASE("Test HCD pipe runtime halt and clear", "[hcd][ignore]")
{
QueueHandle_t port_evt_queue;
QueueHandle_t pipe_evt_queue;
hcd_port_handle_t port_hdl;
setup(&port_evt_queue, &pipe_evt_queue, &port_hdl);
wait_for_connection(port_hdl, port_evt_queue);
vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS)
//Allocate transfer requests
hcd_pipe_handle_t default_pipe;
hcd_xfer_req_handle_t req_hdls[NUM_XFER_REQS];
uint8_t *data_buffers[NUM_XFER_REQS];
usb_irp_t *irps[NUM_XFER_REQS];
alloc_pipe_and_xfer_reqs(port_hdl, pipe_evt_queue, &default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS);
//Initialize transfer requests to send a "Get Device Descriptor" request
for (int i = 0; i < NUM_XFER_REQS; i++) {
irps[i]->num_bytes = 64; //1 worst case MPS
USB_CTRL_REQ_INIT_GET_CFG_DESC((usb_ctrl_req_t *) data_buffers[i], 0, XFER_DATA_MAX_LEN);
irps[i]->data_buffer = data_buffers[i];
irps[i]->num_iso_packets = 0;
}
printf("Enqueuing transfer requests\n");
//Enqueue those transfer requests
for (int i = 0; i < NUM_XFER_REQS; i++) {
TEST_ASSERT_EQUAL(ESP_OK, hcd_xfer_req_enqueue(req_hdls[i]));
}
//Halt the pipe immediately
TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_command(default_pipe, HCD_PIPE_CMD_HALT));
TEST_ASSERT_EQUAL(HCD_PIPE_STATE_HALTED, hcd_pipe_get_state(default_pipe));
printf("Pipe halted\n");
vTaskDelay(pdMS_TO_TICKS(100)); //Give some time for current inflight transfer to complete
//Clear command to un-halt the pipe
TEST_ASSERT_EQUAL(ESP_OK, hcd_pipe_command(default_pipe, HCD_PIPE_CMD_CLEAR));
TEST_ASSERT_EQUAL(HCD_PIPE_STATE_ACTIVE, hcd_pipe_get_state(default_pipe));
printf("Pipe cleared\n");
vTaskDelay(pdMS_TO_TICKS(100)); //Give some time pending for transfers to restart and complete
expect_pipe_event(pipe_evt_queue, default_pipe, HCD_PIPE_EVENT_XFER_REQ_DONE);
for (int i = 0; i < NUM_XFER_REQS; i++) {
hcd_xfer_req_handle_t req_hdl = hcd_xfer_req_dequeue(default_pipe);
TEST_ASSERT_EQUAL(req_hdls[i], req_hdl);
hcd_pipe_handle_t pipe_hdl;
usb_irp_t *irp;
void *context;
hcd_xfer_req_get_target(req_hdl, &pipe_hdl, &irp, &context);
TEST_ASSERT_EQUAL(default_pipe, pipe_hdl);
TEST_ASSERT_EQUAL(irp, irps[i]);
TEST_ASSERT(irp->status == USB_TRANSFER_STATUS_COMPLETED);
TEST_ASSERT_EQUAL(NULL, context);
}
//Free transfer requests
free_pipe_and_xfer_reqs(default_pipe, req_hdls, data_buffers, irps, NUM_XFER_REQS);
vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS)
wait_for_disconnection(port_hdl, port_evt_queue, false);
teardown(port_evt_queue, pipe_evt_queue, port_hdl);
}