diff --git a/components/usb/.build-test-rules.yml b/components/usb/.build-test-rules.yml new file mode 100644 index 0000000000..d11df13540 --- /dev/null +++ b/components/usb/.build-test-rules.yml @@ -0,0 +1,5 @@ +# Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps + +components/usb/test_apps: + enable: + - if: SOC_USB_OTG_SUPPORTED == 1 diff --git a/components/usb/CMakeLists.txt b/components/usb/CMakeLists.txt new file mode 100644 index 0000000000..8be713727a --- /dev/null +++ b/components/usb/CMakeLists.txt @@ -0,0 +1,25 @@ +set(srcs) +set(include) +set(priv_includes) +# As CONFIG_SOC_USB_OTG_SUPPORTED comes from Kconfig, it is not evaluated yet +# when components are being registered. +# Thus, always add the (private) requirements, regardless of Kconfig +set(priv_requires driver) # usb_phy driver relies on gpio driver API + +if(CONFIG_SOC_USB_OTG_SUPPORTED) + list(APPEND srcs "hcd_dwc.c" + "hub.c" + "usb_helpers.c" + "usb_host.c" + "usb_private.c" + "usbh.c" + "usb_phy.c") + list(APPEND include "include") + list(APPEND priv_includes "private_include") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS ${include} + PRIV_INCLUDE_DIRS ${priv_includes} + PRIV_REQUIRES ${priv_requires} + ) diff --git a/components/usb/Kconfig b/components/usb/Kconfig new file mode 100644 index 0000000000..8c170d17fd --- /dev/null +++ b/components/usb/Kconfig @@ -0,0 +1,124 @@ +menu "USB-OTG" + depends on SOC_USB_OTG_SUPPORTED + + config USB_HOST_CONTROL_TRANSFER_MAX_SIZE + int "Largest size (in bytes) of transfers to/from default endpoints" + default 256 + help + Each USB device attached is allocated a dedicated buffer for its OUT/IN transfers to/from the device's + control endpoint. The maximum size of that buffer is determined by this option. The limited size of the + transfer buffer have the following implications: + - The maximum length of control transfers is limited + - Device's with configuration descriptors larger than this limit cannot be supported + + choice USB_HOST_HW_BUFFER_BIAS + prompt "Hardware FIFO size biasing" + default USB_HOST_HW_BUFFER_BIAS_BALANCED + help + The underlying hardware has size adjustable FIFOs to cache USB packets on reception (IN) or for + transmission (OUT). The size of these FIFOs will affect the largest MPS (maximum packet size) and the + maximum number of packets that can be cached at any one time. The hardware contains the following + FIFOS: RX (for all IN packets), Non-periodic TX (for Bulk and Control OUT packets), and Periodic TX + (for Interrupt and Isochronous OUT packets). This configuration option allows biasing the FIFO sizes + towards a particular use case, which may be necessary for devices that have endpoints with large MPS. + The MPS limits for each biasing are listed below: + + Balanced: + - IN (all transfer types), 408 bytes + - OUT non-periodic (Bulk/Control), 192 bytes (i.e., 3 x 64 byte packets) + - OUT periodic (Interrupt/Isochronous), 192 bytes + + Bias IN: + - IN (all transfer types), 600 bytes + - OUT non-periodic (Bulk/Control), 64 bytes (i.e., 1 x 64 byte packets) + - OUT periodic (Interrupt/Isochronous), 128 bytes + + Bias Periodic OUT: + - IN (all transfer types), 128 bytes + - OUT non-periodic (Bulk/Control), 64 bytes (i.e., 1 x 64 byte packets) + - OUT periodic (Interrupt/Isochronous), 600 bytes + + config USB_HOST_HW_BUFFER_BIAS_BALANCED + bool "Balanced" + config USB_HOST_HW_BUFFER_BIAS_IN + bool "Bias IN" + config USB_HOST_HW_BUFFER_BIAS_PERIODIC_OUT + bool "Periodic OUT" + endchoice + + menu "Root Hub configuration" + + config USB_HOST_DEBOUNCE_DELAY_MS + int "Debounce delay in ms" + default 250 + help + On connection of a USB device, the USB 2.0 specification requires a "debounce interval with a minimum + duration of 100ms" to allow the connection to stabilize (see USB 2.0 chapter 7.1.7.3 for more details). + During the debounce interval, no new connection/disconnection events are registered. + + The default value is set to 250 ms to be safe. + + config USB_HOST_RESET_HOLD_MS + int "Reset hold in ms" + default 30 + help + The reset signaling can be generated on any Hub or Host Controller port by request from the USB System + Software. The USB 2.0 specification requires that "the reset signaling must be driven for a minimum of + 10ms" (see USB 2.0 chapter 7.1.7.5 for more details). After the reset, the hub port will transition to + the Enabled state (refer to Section 11.5). + + The default value is set to 30 ms to be safe. + + config USB_HOST_RESET_RECOVERY_MS + int "Reset recovery delay in ms" + default 30 + help + After a port stops driving the reset signal, the USB 2.0 specification requires that the "USB System + Software guarantees a minimum of 10 ms for reset recovery" before the attached device is expected to + respond to data transfers (see USB 2.0 chapter 7.1.7.3 for more details). The device may ignore any + data transfers during the recovery interval. + + The default value is set to 30 ms to be safe. + + + config USB_HOST_SET_ADDR_RECOVERY_MS + int "SetAddress() recovery time in ms" + default 10 + help + "After successful completion of the Status stage, the device is allowed a SetAddress() recovery + interval of 2 ms. At the end of this interval, the device must be able to accept Setup packets + addressed to the new address. Also, at the end of the recovery interval, the device must not respond to + tokens sent to the old address (unless, of course, the old and new address is the same)." See USB 2.0 + chapter 9.2.6.3 for more details. + + The default value is set to 10 ms to be safe. + + endmenu #Root Hub configuration + + # Hidden or compatibility options + + config USB_OTG_SUPPORTED + # Invisible config kept for compatibility + # Todo: Remove in v6.0 (IDF-8936) + bool + default y + + config USB_HOST_ENABLE_ENUM_FILTER_CALLBACK + bool "Enable enumeration filter callback" + default n + help + The enumeration filter callback is called before enumeration of each newly attached device. This callback + allows users to control whether a device should be enumerated, and what configuration number to use when + enumerating a device. + + If enabled, the enumeration filter callback can be set via 'usb_host_config_t' when calling + 'usb_host_install()'. + + config USB_HOST_EXT_HUB_SUPPORT + depends on IDF_EXPERIMENTAL_FEATURES + bool "Support USB HUB (Experimental)" + default n + help + Feature is under development. + +endmenu #USB-OTG diff --git a/components/usb/hcd_dwc.c b/components/usb/hcd_dwc.c new file mode 100644 index 0000000000..3e5c77e8d1 --- /dev/null +++ b/components/usb/hcd_dwc.c @@ -0,0 +1,2490 @@ +/* + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_heap_caps.h" +#include "esp_intr_alloc.h" +#include "soc/interrupts.h" // For interrupt index +#include "esp_err.h" +#include "esp_log.h" +#include "hal/usb_dwc_hal.h" +#include "hal/usb_dwc_types.h" +#include "hcd.h" +#include "usb_private.h" +#include "usb/usb_types_ch9.h" + +// ----------------------------------------------------- Macros -------------------------------------------------------- + +// --------------------- 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 CONFIG_USB_HOST_DEBOUNCE_DELAY_MS +#define RESET_HOLD_MS CONFIG_USB_HOST_RESET_HOLD_MS +#define RESET_RECOVERY_MS CONFIG_USB_HOST_RESET_RECOVERY_MS +#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_HSFS 64 // Largest Maximum Packet Size for High & Full Speed control endpoints + +#define NUM_PORTS 1 // The controller only has one port. + +// ----------------------- Configs ------------------------- + +#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 +// Periodic transfer descriptor lists: Same length as the frame list makes it easier to schedule. Must be power of 2 +// FS: Must be 2-64. HS: Must be 8-256. See USB-OTG databook Table 5-47 +#define XFER_LIST_LEN_INTR FRAME_LIST_LEN +#define XFER_LIST_LEN_ISOC 64 // Implement longer ISOC transfer list to give us enough space for additional timing margin +#define XFER_LIST_ISOC_MARGIN 2 // The 1st ISOC transfer is scheduled 2 (micro)frames later so we have enough timing margin + +// ------------------------ Flags -------------------------- + +/** + * @brief Bit masks for the HCD to use in the URBs reserved_flags field + * + * The URB 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 URB within the HCD. + */ +#define URB_HCD_STATE_IDLE 0 // The URB is not enqueued in an HCD pipe +#define URB_HCD_STATE_PENDING 1 // The URB is enqueued and pending execution +#define URB_HCD_STATE_INFLIGHT 2 // The URB is currently in flight +#define URB_HCD_STATE_DONE 3 // The URB has completed execution or is retired, and is waiting to be dequeued + +#define URB_HCD_STATE_SET(reserved_flags, state) (reserved_flags = (reserved_flags & ~URB_HCD_STATE_MASK) | state) +#define URB_HCD_STATE_GET(reserved_flags) (reserved_flags & URB_HCD_STATE_MASK) + +// -------------------- Convenience ------------------------ + +const char *HCD_DWC_TAG = "HCD DWC"; + +#define HCD_ENTER_CRITICAL_ISR() portENTER_CRITICAL_ISR(&hcd_lock) +#define HCD_EXIT_CRITICAL_ISR() portEXIT_CRITICAL_ISR(&hcd_lock) +#define HCD_ENTER_CRITICAL() portENTER_CRITICAL(&hcd_lock) +#define HCD_EXIT_CRITICAL() portEXIT_CRITICAL(&hcd_lock) + +#define HCD_CHECK(cond, ret_val) ({ \ + if (!(cond)) { \ + return (ret_val); \ + } \ +}) +#define HCD_CHECK_FROM_CRIT(cond, ret_val) ({ \ + if (!(cond)) { \ + HCD_EXIT_CRITICAL(); \ + return ret_val; \ + } \ +}) + +// ------------------------------------------------------ Types -------------------------------------------------------- + +typedef struct 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; + urb_t *urb; + 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; // Added a zero length packet, so transfer consists of 2 QTDs + uint32_t reserved31: 31; + } bulk; // Bulk transfer related + struct { + uint32_t num_qtds: 8; // Number of transfer descriptors filled (excluding zero length packet) + uint32_t zero_len_packet: 1; // Added a zero length packet, so true number descriptors is num_qtds + 1 + uint32_t reserved23: 23; + } 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 start_idx: 8; // Index of the first transfer descriptor in the list + uint32_t next_start_idx: 8; // Index for the first descriptor of the next buffer + } isoc; + uint32_t val; + } flags; + union { + struct { + uint32_t executing: 1; // The buffer is currently executing + uint32_t was_canceled: 1; // Buffer was done due to a cancellation (i.e., a halt request) + uint32_t reserved6: 6; + uint32_t stop_idx: 8; // The descriptor index when the channel was halted + hcd_pipe_event_t pipe_event: 8; // The pipe event when the buffer was done + uint32_t reserved8: 8; + }; + uint32_t val; + } status_flags; // Status flags for the buffer +} dma_buffer_block_t; + +/** + * @brief Object representing a pipe in the HCD layer + */ +struct pipe_obj { + // URB queuing related + TAILQ_HEAD(tailhead_urb_pending, urb_s) pending_urb_tailq; + TAILQ_HEAD(tailhead_urb_done, urb_s) done_urb_tailq; + int num_urb_pending; + int num_urb_done; + // 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 + usb_dwc_hal_chan_t *chan_obj; + usb_dwc_hal_ep_char_t ep_char; + // 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; + volatile TaskHandle_t task_waiting_pipe_notif; // Task handle used for internal pipe events. Set by waiter, cleared by notifier + union { + struct { + uint32_t waiting_halt: 1; + uint32_t pipe_cmd_processing: 1; + uint32_t has_urb: 1; // Indicates there is at least one URB either pending, in-flight, or done + uint32_t reserved29: 29; + }; + uint32_t val; + } cs_flags; + // Pipe callback and context + hcd_pipe_callback_t callback; + void *callback_arg; + void *context; +}; + +/** + * @brief Object representing a port in the HCD layer + */ +struct port_obj { + usb_dwc_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; + int num_pipes_idle; + int num_pipes_queued; + // Port status, state, and events + hcd_port_state_t state; + usb_speed_t speed; + hcd_port_event_t last_event; + volatile TaskHandle_t task_waiting_port_notif; // Task handle used for internal port events. Set by waiter, cleared by notifier + 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 disable_requested: 1; + uint32_t conn_dev_ena: 1; // Used to indicate the port is connected to a device that has been reset + uint32_t periodic_scheduling_enabled: 1; + uint32_t reserved26: 26; + }; + uint32_t val; + } flags; + bool initialized; + // FIFO biasing related + usb_hal_fifo_bias_t fifo_bias; // Bias is saved so it can be reconfigured upon reset + // Port callback and context + hcd_port_callback_t callback; + void *callback_arg; + SemaphoreHandle_t port_mux; + void *context; +}; + +/** + * @brief Object representing the HCD + */ +typedef struct { + // Ports (Hardware only has one) + port_t *port_obj; + intr_handle_t isr_hdl; +} hcd_obj_t; + +static portMUX_TYPE hcd_lock = portMUX_INITIALIZER_UNLOCKED; +static hcd_obj_t *s_hcd_obj = NULL; // Note: "s_" is for the static pointer + +// ------------------------------------------------- Forward Declare --------------------------------------------------- + +// ------------------- Buffer Control ---------------------- + +/** + * @brief Check if an inactive buffer can be filled with a pending URB + * + * @param pipe Pipe object + * @return true There are one or more pending URBs, and the inactive buffer is yet to be filled + * @return false Otherwise + */ +static inline bool _buffer_can_fill(pipe_t *pipe) +{ + // We can only fill if there are pending URBs and at least one unfilled buffer + if (pipe->num_urb_pending > 0 && pipe->multi_buffer_control.buffer_num_to_fill > 0) { + return true; + } else { + return false; + } +} + +/** + * @brief Fill an empty buffer with + * + * This function will: + * - Remove an URB from the pending tailq + * - Fill that URB into the inactive buffer + * + * @note _buffer_can_fill() must return true before calling this function + * + * @param pipe Pipe object + */ +static void _buffer_fill(pipe_t *pipe); + +/** + * @brief Check if there are more filled buffers than can be executed + * + * @param pipe Pipe object + * @return true There are more filled buffers to be executed + * @return false No more buffers to execute + */ +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 USB_DWC_HAL_CHAN_EVENT_CPLT event to check if a buffer is actually + * done. + * + * @param pipe Pipe object + * @return true Buffer complete + * @return false Buffer not complete + */ +static inline bool _buffer_check_done(pipe_t *pipe) +{ + if (pipe->ep_char.type != USB_DWC_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]; + return (buffer_inflight->flags.ctrl.cur_stg == 2); +} + +/** + * @brief Continue execution of a buffer + * + * This should only be called after checking if a buffer has completed execution using _buffer_check_done() + * + * @param pipe Pipe object + */ +static void _buffer_exec_cont(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 + * @param pipe_event Pipe event that caused the buffer to be complete. Use HCD_PIPE_EVENT_NONE for halt request of disconnections + * @param canceled Whether the buffer was done due to a canceled (i.e., halt request). Must set pipe_event to HCD_PIPE_EVENT_NONE + */ +static inline void _buffer_done(pipe_t *pipe, int stop_idx, hcd_pipe_event_t pipe_event, bool canceled) +{ + // Store the stop_idx and pipe_event 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.was_canceled = canceled; + buffer_done->status_flags.stop_idx = stop_idx; + 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 URB from a completed buffer + * - Put the URB 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 URB. 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 canceled Whether this flush is due to cancellation + * @return true One or more buffers were flushed + * @return false There were no buffers that needed to be flushed + */ +static bool _buffer_flush_all(pipe_t *pipe, bool canceled); + +// ------------------------ Pipe --------------------------- + +/** + * @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(usb_dwc_hal_chan_error_t chan_error); + +/** + * @brief Halt a pipe + * + * - Attempts to halt a pipe. Pipe must be active in order to be halted + * - If the underlying channel has an ongoing transfer, a halt will be requested, then the function will block until the + * channel indicates it is halted + * - If the channel is no on-going transfer, the pipe will simply be marked has halted (thus preventing any further URBs + * from being enqueued) + * + * @note This function can block + * @param pipe Pipe object + * @return esp_err_t + */ +static esp_err_t _pipe_cmd_halt(pipe_t *pipe); + +/** + * @brief Flush a pipe + * + * - Flushing a pipe causes all of its pending URBs to be become done, thus allowing them to be dequeued + * - The pipe must be halted in order to be flushed + * - The pipe callback will be run if one or more URBs become done + * + * @param pipe Pipe object + * @return esp_err_t + */ +static esp_err_t _pipe_cmd_flush(pipe_t *pipe); + +/** + * @brief Clear a pipe from its halt + * + * - Pipe must be halted in order to be cleared + * - Clearing a pipe makes it active again + * - If there are any enqueued URBs, they will executed + * + * @param pipe Pipe object + * @return esp_err_t + */ +static esp_err_t _pipe_cmd_clear(pipe_t *pipe); + +// ------------------------ Port --------------------------- + +/** + * @brief Checks if all pipes are in the halted state + * + * @param port Port object + * @return true All pipes are halted + * @return false Not all pipes are halted + */ +static bool _port_check_all_pipes_halted(port_t *port); + +/** + * @brief Debounce port after a connection or disconnection event + * + * This function should be called after a port connection or disconnect event. This function will execute a debounce + * delay then check the actual connection/disconnections state. + * + * @note This function can block + * @param port Port object + * @return true A device is connected + * @return false No device connected + */ +static bool _port_debounce(port_t *port); + +/** + * @brief Power ON the port + * + * @param port Port object + * @return esp_err_t + */ +static esp_err_t _port_cmd_power_on(port_t *port); + +/** + * @brief Power OFF the port + * + * - If a device is currently connected, this function will cause a disconnect event + * + * @param port Port object + * @return esp_err_t + */ +static esp_err_t _port_cmd_power_off(port_t *port); + +/** + * @brief Reset the port + * + * - This function issues a reset signal using the timings specified by the USB2.0 spec + * + * @note This function can block + * @param port Port object + * @return esp_err_t + */ +static esp_err_t _port_cmd_reset(port_t *port); + +/** + * @brief Suspend the port + * + * - Port must be enabled in order to to be suspended + * - All pipes must be halted for the port to be suspended + * - Suspending the port stops Keep Alive/SOF from being sent to the connected device + * + * @param port Port object + * @return esp_err_t + */ +static esp_err_t _port_cmd_bus_suspend(port_t *port); + +/** + * @brief Resume the port + * + * - Port must be suspended in order to be resumed + * + * @note This function can block + * @param port Port object + * @return esp_err_t + */ +static esp_err_t _port_cmd_bus_resume(port_t *port); + +/** + * @brief Disable the port + * + * - All pipes must be halted for the port to be disabled + * - The port must be enabled or suspended in order to be disabled + * + * @note This function can block + * @param port Port object + * @return esp_err_t + */ +static esp_err_t _port_cmd_disable(port_t *port); + +// ----------------------- Events -------------------------- + +/** + * @brief Wait for an internal event from a port + * + * @note For each port, there can only be one thread/task waiting for an internal port event + * @note This function is blocking (will exit and re-enter the critical section to do so) + * + * @param port Port object + */ +static void _internal_port_event_wait(port_t *port); + +/** + * @brief Notify (from an ISR context) the thread/task waiting for the internal port event + * + * @param port Port object + * @return true A yield is required + * @return false Whether a yield is required or not + */ +static bool _internal_port_event_notify_from_isr(port_t *port); + +/** + * @brief Wait for an internal event from a particular pipe + * + * @note For each pipe, there can only be one thread/task waiting for an internal port event + * @note This function is blocking (will exit and re-enter the critical section to do so) + * + * @param pipe Pipe object + */ +static void _internal_pipe_event_wait(pipe_t *pipe); + +/** + * @brief Notify (from an ISR context) the thread/task waiting for an internal pipe event + * + * @param pipe Pipe object + * @param from_isr Whether this is called from an ISR or not + * @return true A yield is required + * @return false Whether a yield is required or not. Always false when from_isr is also false + */ +static bool _internal_pipe_event_notify(pipe_t *pipe, bool from_isr); + +// ----------------------------------------------- Interrupt Handling -------------------------------------------------- + +// ------------------- Internal Event ---------------------- + +static void _internal_port_event_wait(port_t *port) +{ + // There must NOT be another thread/task already waiting for an internal event + assert(port->task_waiting_port_notif == NULL); + port->task_waiting_port_notif = xTaskGetCurrentTaskHandle(); + /* We need to loop as task notifications can come from anywhere. If we this + was a port event notification, task_waiting_port_notif will have been cleared + by the notifier. */ + while (port->task_waiting_port_notif != NULL) { + HCD_EXIT_CRITICAL(); + // Wait to be notified from ISR + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + HCD_ENTER_CRITICAL(); + } +} + +static bool _internal_port_event_notify_from_isr(port_t *port) +{ + // There must be a thread/task waiting for an internal event + assert(port->task_waiting_port_notif != NULL); + TaskHandle_t task_to_unblock = port->task_waiting_port_notif; + // Clear task_waiting_port_notif to indicate to the waiter that the unblock was indeed an port event notification + port->task_waiting_port_notif = NULL; + // Unblock the thread/task waiting for the notification + BaseType_t xTaskWoken = pdFALSE; + // Note: We don't exit the critical section to be atomic. vTaskNotifyGiveFromISR() doesn't block anyways + vTaskNotifyGiveFromISR(task_to_unblock, &xTaskWoken); + return (xTaskWoken == pdTRUE); +} + +static void _internal_pipe_event_wait(pipe_t *pipe) +{ + // There must NOT be another thread/task already waiting for an internal event + assert(pipe->task_waiting_pipe_notif == NULL); + pipe->task_waiting_pipe_notif = xTaskGetCurrentTaskHandle(); + /* We need to loop as task notifications can come from anywhere. If we this + was a pipe event notification, task_waiting_pipe_notif will have been cleared + by the notifier. */ + while (pipe->task_waiting_pipe_notif != NULL) { + // Wait to be unblocked by notified + HCD_EXIT_CRITICAL(); + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + HCD_ENTER_CRITICAL(); + } +} + +static bool _internal_pipe_event_notify(pipe_t *pipe, bool from_isr) +{ + // There must be a thread/task waiting for an internal event + assert(pipe->task_waiting_pipe_notif != NULL); + TaskHandle_t task_to_unblock = pipe->task_waiting_pipe_notif; + // Clear task_waiting_pipe_notif to indicate to the waiter that the unblock was indeed an pipe event notification + pipe->task_waiting_pipe_notif = NULL; + bool ret; + if (from_isr) { + BaseType_t xTaskWoken = pdFALSE; + // Note: We don't exit the critical section to be atomic. vTaskNotifyGiveFromISR() doesn't block anyways + // Unblock the thread/task waiting for the pipe notification + vTaskNotifyGiveFromISR(task_to_unblock, &xTaskWoken); + ret = (xTaskWoken == pdTRUE); + } else { + HCD_EXIT_CRITICAL(); + xTaskNotifyGive(task_to_unblock); + HCD_ENTER_CRITICAL(); + ret = false; + } + return ret; +} + +// ----------------- HAL <-> USB helpers -------------------- + +static usb_speed_t get_usb_port_speed(usb_dwc_speed_t priv) +{ + switch (priv) { + case USB_DWC_SPEED_LOW: return USB_SPEED_LOW; + case USB_DWC_SPEED_FULL: return USB_SPEED_FULL; + case USB_DWC_SPEED_HIGH: return USB_SPEED_HIGH; + default: abort(); + } +} + +static usb_hal_fifo_bias_t get_hal_fifo_bias(hcd_port_fifo_bias_t public) +{ + switch (public) { + case HCD_PORT_FIFO_BIAS_BALANCED: return USB_HAL_FIFO_BIAS_DEFAULT; + case HCD_PORT_FIFO_BIAS_RX: return USB_HAL_FIFO_BIAS_RX; + case HCD_PORT_FIFO_BIAS_PTX: return USB_HAL_FIFO_BIAS_PTX; + default: abort(); + } +} + +// ----------------- Interrupt Handlers -------------------- + +/** + * @brief Handle a HAL port interrupt and obtain the corresponding port event + * + * @param[in] port Port object + * @param[in] hal_port_event The HAL port event + * @param[out] yield Set to true if a yield is required as a result of handling the interrupt + * @return hcd_port_event_t Returns a port event, or HCD_PORT_EVENT_NONE if no port event occurred + */ +static hcd_port_event_t _intr_hdlr_hprt(port_t *port, usb_dwc_hal_port_event_t hal_port_event, bool *yield) +{ + hcd_port_event_t port_event = HCD_PORT_EVENT_NONE; + switch (hal_port_event) { + case USB_DWC_HAL_PORT_EVENT_CONN: { + // Don't update state immediately, we still need to debounce. + port_event = HCD_PORT_EVENT_CONNECTION; + break; + } + case USB_DWC_HAL_PORT_EVENT_DISCONN: { + port->state = HCD_PORT_STATE_RECOVERY; + port_event = HCD_PORT_EVENT_DISCONNECTION; + port->flags.conn_dev_ena = 0; + break; + } + case USB_DWC_HAL_PORT_EVENT_ENABLED: { + usb_dwc_hal_port_enable(port->hal); // Initialize remaining host port registers + port->speed = get_usb_port_speed(usb_dwc_hal_port_get_conn_speed(port->hal)); + port->state = HCD_PORT_STATE_ENABLED; + port->flags.conn_dev_ena = 1; + // This was triggered by a command, so no event needs to be propagated. + break; + } + case USB_DWC_HAL_PORT_EVENT_DISABLED: { + port->flags.conn_dev_ena = 0; + // Disabled could be due to a disable request or reset request, or due to a port error + if (port->state != HCD_PORT_STATE_RESETTING) { // Ignore the disable event if it's due to a reset request + if (port->flags.disable_requested) { + // Disabled by request (i.e. by port command). Generate an internal event + port->state = HCD_PORT_STATE_DISABLED; + port->flags.disable_requested = 0; + *yield |= _internal_port_event_notify_from_isr(port); + } else { + // Disabled due to a port error + port->state = HCD_PORT_STATE_RECOVERY; + port_event = HCD_PORT_EVENT_ERROR; + } + } + break; + } + case USB_DWC_HAL_PORT_EVENT_OVRCUR: + case USB_DWC_HAL_PORT_EVENT_OVRCUR_CLR: { // Could occur if a quick overcurrent then clear happens + if (port->state != HCD_PORT_STATE_NOT_POWERED) { + // We need to power OFF the port to protect it + usb_dwc_hal_port_toggle_power(port->hal, false); + port->state = HCD_PORT_STATE_RECOVERY; + port_event = HCD_PORT_EVENT_OVERCURRENT; + } + port->flags.conn_dev_ena = 0; + break; + } + default: { + abort(); + break; + } + } + return port_event; +} + +/** + * @brief Handles a HAL channel interrupt + * + * This function should be called on a HAL channel when it has an interrupt. Most HAL channel events will correspond to + * to a pipe event, but not always. This function will store the pipe event and return a pipe object pointer if a pipe + * event occurred, or return NULL otherwise. + * + * @param[in] chan_obj Pointer to HAL channel object with interrupt + * @param[out] yield Set to true if a yield is required as a result of handling the interrupt + * @return hcd_pipe_event_t The pipe event + */ +static hcd_pipe_event_t _intr_hdlr_chan(pipe_t *pipe, usb_dwc_hal_chan_t *chan_obj, bool *yield) +{ + usb_dwc_hal_chan_event_t chan_event = usb_dwc_hal_chan_decode_intr(chan_obj); + hcd_pipe_event_t event = HCD_PIPE_EVENT_NONE; + + switch (chan_event) { + case USB_DWC_HAL_CHAN_EVENT_CPLT: { + if (!_buffer_check_done(pipe)) { + _buffer_exec_cont(pipe); + break; + } + pipe->last_event = HCD_PIPE_EVENT_URB_DONE; + event = pipe->last_event; + // Mark the buffer as done + int stop_idx = usb_dwc_hal_chan_get_qtd_idx(chan_obj); + _buffer_done(pipe, stop_idx, pipe->last_event, false); + // First check if there is another buffer we can execute. But we only want to execute if there's still a valid device + if (_buffer_can_exec(pipe) && pipe->port->flags.conn_dev_ena) { + // If the next buffer is filled and ready to execute, execute it + _buffer_exec(pipe); + } + // Handle the previously done buffer + _buffer_parse(pipe); + // Check to see if we can fill another buffer. But we only want to fill if there is still a valid device + if (_buffer_can_fill(pipe) && pipe->port->flags.conn_dev_ena) { + // Now that we've parsed a buffer, see if another URB can be filled in its place + _buffer_fill(pipe); + } + break; + } + case USB_DWC_HAL_CHAN_EVENT_ERROR: { + // Get and store the pipe error event + usb_dwc_hal_chan_error_t chan_error = usb_dwc_hal_chan_get_error(chan_obj); + pipe->last_event = pipe_decode_error_event(chan_error); + event = pipe->last_event; + pipe->state = HCD_PIPE_STATE_HALTED; + // Mark the buffer as done with an error + int stop_idx = usb_dwc_hal_chan_get_qtd_idx(chan_obj); + _buffer_done(pipe, stop_idx, pipe->last_event, false); + // Parse the buffer + _buffer_parse(pipe); + break; + } + case USB_DWC_HAL_CHAN_EVENT_HALT_REQ: { + assert(pipe->cs_flags.waiting_halt); + // We've halted a transfer, so we need to trigger the pipe callback + pipe->last_event = HCD_PIPE_EVENT_URB_DONE; + event = pipe->last_event; + // Halt request event is triggered when packet is successful completed. But just treat all halted transfers as errors + pipe->state = HCD_PIPE_STATE_HALTED; + int stop_idx = usb_dwc_hal_chan_get_qtd_idx(chan_obj); + _buffer_done(pipe, stop_idx, HCD_PIPE_EVENT_NONE, true); + // Parse the buffer + _buffer_parse(pipe); + // Notify the task waiting for the pipe halt + *yield |= _internal_pipe_event_notify(pipe, true); + break; + } + case USB_DWC_HAL_CHAN_EVENT_NONE: { + break; // Nothing to do + } + default: + abort(); + break; + } + return event; +} + +/** + * @brief Main interrupt handler + * + * - Handle all HPRT (Host Port) related interrupts first as they may change the + * state of the driver (e.g., a disconnect event) + * - If any channels (pipes) have pending interrupts, handle them one by one + * - The HCD has not blocking functions, so the user's ISR callback is run to + * allow the users to send whatever OS primitives they need. + * + * @param arg Interrupt handler argument + */ +static void intr_hdlr_main(void *arg) +{ + port_t *port = (port_t *) arg; + bool yield = false; + + HCD_ENTER_CRITICAL_ISR(); + usb_dwc_hal_port_event_t hal_port_evt = usb_dwc_hal_decode_intr(port->hal); + if (hal_port_evt == USB_DWC_HAL_PORT_EVENT_CHAN) { + // Channel event. Cycle through each pending channel + usb_dwc_hal_chan_t *chan_obj = usb_dwc_hal_get_chan_pending_intr(port->hal); + while (chan_obj != NULL) { + pipe_t *pipe = (pipe_t *)usb_dwc_hal_chan_get_context(chan_obj); + hcd_pipe_event_t event = _intr_hdlr_chan(pipe, chan_obj, &yield); + // Run callback if a pipe event has occurred and the pipe also has a callback + if (event != HCD_PIPE_EVENT_NONE && pipe->callback != NULL) { + HCD_EXIT_CRITICAL_ISR(); + yield |= pipe->callback((hcd_pipe_handle_t)pipe, event, pipe->callback_arg, true); + HCD_ENTER_CRITICAL_ISR(); + } + // Check for more channels with pending interrupts. Returns NULL if there are no more + chan_obj = usb_dwc_hal_get_chan_pending_intr(port->hal); + } + } else if (hal_port_evt != USB_DWC_HAL_PORT_EVENT_NONE) { // Port event + hcd_port_event_t port_event = _intr_hdlr_hprt(port, hal_port_evt, &yield); + if (port_event != HCD_PORT_EVENT_NONE) { + port->last_event = port_event; + port->flags.event_pending = 1; + if (port->callback != NULL) { + HCD_EXIT_CRITICAL_ISR(); + yield |= port->callback((hcd_port_handle_t)port, port_event, port->callback_arg, true); + HCD_ENTER_CRITICAL_ISR(); + } + } + } + HCD_EXIT_CRITICAL_ISR(); + + if (yield) { + portYIELD_FROM_ISR(); + } +} + +// --------------------------------------------- Host Controller Driver ------------------------------------------------ + +static port_t *port_obj_alloc(void) +{ + port_t *port = calloc(1, sizeof(port_t)); + usb_dwc_hal_context_t *hal = malloc(sizeof(usb_dwc_hal_context_t)); + void *frame_list = heap_caps_aligned_calloc(USB_DWC_FRAME_LIST_MEM_ALIGN, FRAME_LIST_LEN, sizeof(uint32_t), MALLOC_CAP_DMA); + SemaphoreHandle_t port_mux = xSemaphoreCreateMutex(); + 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; +} + +static void port_obj_free(port_t *port) +{ + if (port == NULL) { + return; + } + vSemaphoreDelete(port->port_mux); + free(port->frame_list); + free(port->hal); + free(port); +} + +// ----------------------- Public -------------------------- + +esp_err_t hcd_install(const hcd_config_t *config) +{ + HCD_ENTER_CRITICAL(); + HCD_CHECK_FROM_CRIT(s_hcd_obj == NULL, ESP_ERR_INVALID_STATE); + HCD_EXIT_CRITICAL(); + + esp_err_t err_ret; + // Allocate memory for the driver object + hcd_obj_t *p_hcd_obj_dmy = calloc(1, sizeof(hcd_obj_t)); + if (p_hcd_obj_dmy == NULL) { + return ESP_ERR_NO_MEM; + } + // Allocate each port object (the hardware currently only has one port) + p_hcd_obj_dmy->port_obj = port_obj_alloc(); + if (p_hcd_obj_dmy->port_obj == NULL) { + err_ret = ESP_ERR_NO_MEM; + goto port_alloc_err; + } + // Allocate interrupt + err_ret = esp_intr_alloc(ETS_USB_INTR_SOURCE, + config->intr_flags | ESP_INTR_FLAG_INTRDISABLED, // The interrupt must be disabled until the port is initialized + intr_hdlr_main, + (void *)p_hcd_obj_dmy->port_obj, + &p_hcd_obj_dmy->isr_hdl); + if (err_ret != ESP_OK) { + goto intr_alloc_err; + } + // Assign the + HCD_ENTER_CRITICAL(); + if (s_hcd_obj != NULL) { + HCD_EXIT_CRITICAL(); + err_ret = ESP_ERR_INVALID_STATE; + goto assign_err; + } + s_hcd_obj = p_hcd_obj_dmy; + HCD_EXIT_CRITICAL(); + return ESP_OK; + +assign_err: + esp_intr_free(p_hcd_obj_dmy->isr_hdl); +intr_alloc_err: + port_obj_free(p_hcd_obj_dmy->port_obj); +port_alloc_err: + free(p_hcd_obj_dmy); + return err_ret; +} + +esp_err_t hcd_uninstall(void) +{ + HCD_ENTER_CRITICAL(); + // Check that all ports have been disabled (there's only one port) + if (s_hcd_obj == NULL || s_hcd_obj->port_obj->initialized) { + HCD_EXIT_CRITICAL(); + return ESP_ERR_INVALID_STATE; + } + hcd_obj_t *p_hcd_obj_dmy = s_hcd_obj; + s_hcd_obj = NULL; + HCD_EXIT_CRITICAL(); + + // Free resources + port_obj_free(p_hcd_obj_dmy->port_obj); + esp_intr_free(p_hcd_obj_dmy->isr_hdl); + free(p_hcd_obj_dmy); + return ESP_OK; +} + +// ------------------------------------------------------ Port --------------------------------------------------------- + +// ----------------------- Helpers ------------------------- + +static bool _port_check_all_pipes_halted(port_t *port) +{ + bool all_halted = true; + pipe_t *pipe; + TAILQ_FOREACH(pipe, &port->pipes_active_tailq, tailq_entry) { + if (pipe->state != HCD_PIPE_STATE_HALTED) { + all_halted = false; + break; + } + } + TAILQ_FOREACH(pipe, &port->pipes_idle_tailq, tailq_entry) { + if (pipe->state != HCD_PIPE_STATE_HALTED) { + all_halted = false; + break; + } + } + return all_halted; +} + +static bool _port_debounce(port_t *port) +{ + if (port->state == HCD_PORT_STATE_NOT_POWERED) { + // Disconnect event due to power off, no need to debounce or update port state. + return false; + } + HCD_EXIT_CRITICAL(); + vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_DELAY_MS)); + HCD_ENTER_CRITICAL(); + // Check the post-debounce state of the bus (i.e., whether it's actually connected/disconnected) + bool is_connected = usb_dwc_hal_port_check_if_connected(port->hal); + if (is_connected) { + port->state = HCD_PORT_STATE_DISABLED; + } else { + port->state = HCD_PORT_STATE_DISCONNECTED; + } + // Disable debounce lock + usb_dwc_hal_disable_debounce_lock(port->hal); + return is_connected; +} + +// ---------------------- Commands ------------------------- + +static esp_err_t _port_cmd_power_on(port_t *port) +{ + esp_err_t ret; + // Port can only be powered on if it's currently unpowered + if (port->state == HCD_PORT_STATE_NOT_POWERED) { + port->state = HCD_PORT_STATE_DISCONNECTED; + usb_dwc_hal_port_init(port->hal); + usb_dwc_hal_port_toggle_power(port->hal, true); + ret = ESP_OK; + } else { + ret = ESP_ERR_INVALID_STATE; + } + return ret; +} + +static esp_err_t _port_cmd_power_off(port_t *port) +{ + esp_err_t ret; + // Port can only be unpowered if already powered + if (port->state != HCD_PORT_STATE_NOT_POWERED) { + port->state = HCD_PORT_STATE_NOT_POWERED; + usb_dwc_hal_port_deinit(port->hal); + usb_dwc_hal_port_toggle_power(port->hal, false); + // If a device is currently connected, this should trigger a disconnect event + ret = ESP_OK; + } else { + ret = ESP_ERR_INVALID_STATE; + } + return ret; +} + +static esp_err_t _port_cmd_reset(port_t *port) +{ + esp_err_t ret; + + // Port can only a reset when it is in the enabled or disabled (in the case of a new connection)states. + if (port->state != HCD_PORT_STATE_ENABLED && port->state != HCD_PORT_STATE_DISABLED) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + // Port can only be reset if all pipes are idle + if (port->num_pipes_queued > 0) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + /* + Proceed to resetting the bus + - Update the port's state variable + - Hold the bus in the reset state for RESET_HOLD_MS. + - Return the bus to the idle state for RESET_RECOVERY_MS + */ + port->state = HCD_PORT_STATE_RESETTING; + + // Place the bus into the reset state. If the port was previously enabled, a disabled event will occur after this + usb_dwc_hal_port_toggle_reset(port->hal, true); + HCD_EXIT_CRITICAL(); + vTaskDelay(pdMS_TO_TICKS(RESET_HOLD_MS)); + HCD_ENTER_CRITICAL(); + if (port->state != HCD_PORT_STATE_RESETTING) { + // The port state has unexpectedly changed + ret = ESP_ERR_INVALID_RESPONSE; + goto bailout; + } + + // Return the bus to the idle state. Port enabled event should occur + usb_dwc_hal_port_toggle_reset(port->hal, false); + HCD_EXIT_CRITICAL(); + vTaskDelay(pdMS_TO_TICKS(RESET_RECOVERY_MS)); + HCD_ENTER_CRITICAL(); + if (port->state != HCD_PORT_STATE_ENABLED || !port->flags.conn_dev_ena) { + // The port state has unexpectedly changed + ret = ESP_ERR_INVALID_RESPONSE; + goto bailout; + } + + // Reinitialize port registers. + usb_dwc_hal_set_fifo_bias(port->hal, port->fifo_bias); // Set FIFO biases + usb_dwc_hal_port_set_frame_list(port->hal, port->frame_list, FRAME_LIST_LEN); // Set periodic frame list + usb_dwc_hal_port_periodic_enable(port->hal); // Enable periodic scheduling + + ret = ESP_OK; +bailout: + /* + We add a blank statement here to work around a quirk in the C language where a label can only be followed by + statements, thus generating this warning when building with "-Wpedantic": + + "warning: a label can only be part of a statement and a declaration is not a statement" + */ + {}; + // Reinitialize channel registers + pipe_t *pipe; + TAILQ_FOREACH(pipe, &port->pipes_idle_tailq, tailq_entry) { + usb_dwc_hal_chan_set_ep_char(port->hal, pipe->chan_obj, &pipe->ep_char); + } +exit: + return ret; +} + +static esp_err_t _port_cmd_bus_suspend(port_t *port) +{ + esp_err_t ret; + // Port must have been previously enabled, and all pipes must already be halted + if (port->state == HCD_PORT_STATE_ENABLED && !_port_check_all_pipes_halted(port)) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + // All pipes are guaranteed halted at this point. Proceed to suspend the port + usb_dwc_hal_port_suspend(port->hal); + port->state = HCD_PORT_STATE_SUSPENDED; + ret = ESP_OK; +exit: + return ret; +} + +static esp_err_t _port_cmd_bus_resume(port_t *port) +{ + esp_err_t ret; + // Port can only be resumed if it was previously suspended + if (port->state != HCD_PORT_STATE_SUSPENDED) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + // Put and hold the bus in the K state. + usb_dwc_hal_port_toggle_resume(port->hal, true); + port->state = HCD_PORT_STATE_RESUMING; + HCD_EXIT_CRITICAL(); + vTaskDelay(pdMS_TO_TICKS(RESUME_HOLD_MS)); + HCD_ENTER_CRITICAL(); + // Return and hold the bus to the J state (as port of the LS EOP) + usb_dwc_hal_port_toggle_resume(port->hal, false); + if (port->state != HCD_PORT_STATE_RESUMING || !port->flags.conn_dev_ena) { + // Port state unexpectedly changed + ret = ESP_ERR_INVALID_RESPONSE; + goto exit; + } + HCD_EXIT_CRITICAL(); + vTaskDelay(pdMS_TO_TICKS(RESUME_RECOVERY_MS)); + HCD_ENTER_CRITICAL(); + if (port->state != HCD_PORT_STATE_RESUMING || !port->flags.conn_dev_ena) { + // Port state unexpectedly changed + ret = ESP_ERR_INVALID_RESPONSE; + goto exit; + } + port->state = HCD_PORT_STATE_ENABLED; + ret = ESP_OK; +exit: + return ret; +} + +static esp_err_t _port_cmd_disable(port_t *port) +{ + esp_err_t ret; + if (port->state != HCD_PORT_STATE_ENABLED && port->state != HCD_PORT_STATE_SUSPENDED) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + // All pipes must be halted before disabling the port + if (!_port_check_all_pipes_halted(port)) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + // All pipes are guaranteed to be halted or freed at this point. Proceed to disable the port + port->flags.disable_requested = 1; + usb_dwc_hal_port_disable(port->hal); + _internal_port_event_wait(port); + if (port->state != HCD_PORT_STATE_DISABLED) { + // Port state unexpectedly changed + ret = ESP_ERR_INVALID_RESPONSE; + goto exit; + } + ret = ESP_OK; +exit: + return ret; +} + +// ----------------------- Public -------------------------- + +esp_err_t hcd_port_init(int port_number, const hcd_port_config_t *port_config, hcd_port_handle_t *port_hdl) +{ + HCD_CHECK(port_number > 0 && port_config != NULL && port_hdl != NULL, ESP_ERR_INVALID_ARG); + HCD_CHECK(port_number <= NUM_PORTS, ESP_ERR_NOT_FOUND); + + HCD_ENTER_CRITICAL(); + HCD_CHECK_FROM_CRIT(s_hcd_obj != NULL && !s_hcd_obj->port_obj->initialized, ESP_ERR_INVALID_STATE); + // Port object memory and resources (such as the mutex) already be allocated. Just need to initialize necessary fields only + port_t *port_obj = s_hcd_obj->port_obj; + TAILQ_INIT(&port_obj->pipes_idle_tailq); + TAILQ_INIT(&port_obj->pipes_active_tailq); + port_obj->state = HCD_PORT_STATE_NOT_POWERED; + port_obj->last_event = HCD_PORT_EVENT_NONE; + port_obj->fifo_bias = get_hal_fifo_bias(port_config->fifo_bias); + port_obj->callback = port_config->callback; + port_obj->callback_arg = port_config->callback_arg; + port_obj->context = port_config->context; + usb_dwc_hal_init(port_obj->hal); + port_obj->initialized = true; + // Clear the frame list. We set the frame list register and enable periodic scheduling after a successful reset + memset(port_obj->frame_list, 0, FRAME_LIST_LEN * sizeof(uint32_t)); + esp_intr_enable(s_hcd_obj->isr_hdl); + *port_hdl = (hcd_port_handle_t)port_obj; + HCD_EXIT_CRITICAL(); + + vTaskDelay(pdMS_TO_TICKS(INIT_DELAY_MS)); // Need a short delay before host mode takes effect + return ESP_OK; +} + +esp_err_t hcd_port_deinit(hcd_port_handle_t port_hdl) +{ + port_t *port = (port_t *)port_hdl; + + HCD_ENTER_CRITICAL(); + HCD_CHECK_FROM_CRIT(s_hcd_obj != NULL && port->initialized + && port->num_pipes_idle == 0 && port->num_pipes_queued == 0 + && (port->state == HCD_PORT_STATE_NOT_POWERED || port->state == HCD_PORT_STATE_RECOVERY) + && port->task_waiting_port_notif == NULL, + ESP_ERR_INVALID_STATE); + port->initialized = false; + esp_intr_disable(s_hcd_obj->isr_hdl); + usb_dwc_hal_deinit(port->hal); + HCD_EXIT_CRITICAL(); + + return ESP_OK; +} + +esp_err_t hcd_port_command(hcd_port_handle_t port_hdl, hcd_port_cmd_t command) +{ + esp_err_t ret = ESP_ERR_INVALID_STATE; + port_t *port = (port_t *)port_hdl; + xSemaphoreTake(port->port_mux, portMAX_DELAY); + HCD_ENTER_CRITICAL(); + if (port->initialized && !port->flags.event_pending) { // Port events need to be handled first before issuing a command + port->flags.cmd_processing = 1; + switch (command) { + case HCD_PORT_CMD_POWER_ON: { + ret = _port_cmd_power_on(port); + break; + } + case HCD_PORT_CMD_POWER_OFF: { + ret = _port_cmd_power_off(port); + break; + } + case HCD_PORT_CMD_RESET: { + ret = _port_cmd_reset(port); + break; + } + case HCD_PORT_CMD_SUSPEND: { + ret = _port_cmd_bus_suspend(port); + break; + } + case HCD_PORT_CMD_RESUME: { + ret = _port_cmd_bus_resume(port); + break; + } + case HCD_PORT_CMD_DISABLE: { + ret = _port_cmd_disable(port); + break; + } + } + port->flags.cmd_processing = 0; + } + HCD_EXIT_CRITICAL(); + xSemaphoreGive(port->port_mux); + return ret; +} + +hcd_port_state_t hcd_port_get_state(hcd_port_handle_t port_hdl) +{ + port_t *port = (port_t *)port_hdl; + hcd_port_state_t ret; + HCD_ENTER_CRITICAL(); + ret = port->state; + HCD_EXIT_CRITICAL(); + return ret; +} + +esp_err_t hcd_port_get_speed(hcd_port_handle_t port_hdl, usb_speed_t *speed) +{ + port_t *port = (port_t *)port_hdl; + HCD_CHECK(speed != NULL, ESP_ERR_INVALID_ARG); + HCD_ENTER_CRITICAL(); + // Device speed is only valid if there is device connected to the port that has been reset + HCD_CHECK_FROM_CRIT(port->flags.conn_dev_ena, ESP_ERR_INVALID_STATE); + *speed = get_usb_port_speed(usb_dwc_hal_port_get_conn_speed(port->hal)); + HCD_EXIT_CRITICAL(); + return ESP_OK; +} + +hcd_port_event_t hcd_port_handle_event(hcd_port_handle_t port_hdl) +{ + port_t *port = (port_t *)port_hdl; + hcd_port_event_t ret = HCD_PORT_EVENT_NONE; + xSemaphoreTake(port->port_mux, portMAX_DELAY); + HCD_ENTER_CRITICAL(); + if (port->initialized && port->flags.event_pending) { + port->flags.event_pending = 0; + port->flags.event_processing = 1; + ret = port->last_event; + switch (ret) { + case HCD_PORT_EVENT_CONNECTION: { + if (_port_debounce(port)) { + ret = HCD_PORT_EVENT_CONNECTION; + } + break; + } + case HCD_PORT_EVENT_DISCONNECTION: + case HCD_PORT_EVENT_ERROR: + case HCD_PORT_EVENT_OVERCURRENT: { + break; + } + default: { + break; + } + } + port->flags.event_processing = 0; + } else { + ret = HCD_PORT_EVENT_NONE; + } + HCD_EXIT_CRITICAL(); + xSemaphoreGive(port->port_mux); + return ret; +} + +esp_err_t hcd_port_recover(hcd_port_handle_t port_hdl) +{ + port_t *port = (port_t *)port_hdl; + HCD_ENTER_CRITICAL(); + HCD_CHECK_FROM_CRIT(s_hcd_obj != NULL && port->initialized && port->state == HCD_PORT_STATE_RECOVERY + && port->num_pipes_idle == 0 && port->num_pipes_queued == 0 + && port->flags.val == 0 && port->task_waiting_port_notif == NULL, + ESP_ERR_INVALID_STATE); + // We are about to do a soft reset on the peripheral. Disable the peripheral throughout + esp_intr_disable(s_hcd_obj->isr_hdl); + usb_dwc_hal_core_soft_reset(port->hal); + port->state = HCD_PORT_STATE_NOT_POWERED; + port->last_event = HCD_PORT_EVENT_NONE; + port->flags.val = 0; + // Soft reset wipes all registers so we need to reinitialize the HAL + usb_dwc_hal_init(port->hal); + // Clear the frame list. We set the frame list register and enable periodic scheduling after a successful reset + memset(port->frame_list, 0, FRAME_LIST_LEN * sizeof(uint32_t)); + esp_intr_enable(s_hcd_obj->isr_hdl); + HCD_EXIT_CRITICAL(); + return ESP_OK; +} + +void *hcd_port_get_context(hcd_port_handle_t port_hdl) +{ + port_t *port = (port_t *)port_hdl; + void *ret; + HCD_ENTER_CRITICAL(); + ret = port->context; + HCD_EXIT_CRITICAL(); + return ret; +} + +esp_err_t hcd_port_set_fifo_bias(hcd_port_handle_t port_hdl, hcd_port_fifo_bias_t bias) +{ + esp_err_t ret; + usb_hal_fifo_bias_t hal_bias = get_hal_fifo_bias(bias); + + // Configure the new FIFO sizes and store the pointers + 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) { + usb_dwc_hal_set_fifo_bias(port->hal, hal_bias); + port->fifo_bias = hal_bias; + ret = ESP_OK; + } else { + ret = ESP_ERR_INVALID_STATE; + } + HCD_EXIT_CRITICAL(); + xSemaphoreGive(port->port_mux); + return ret; +} + +// --------------------------------------------------- HCD Pipes ------------------------------------------------------- + +// ----------------------- Private ------------------------- + +static inline hcd_pipe_event_t pipe_decode_error_event(usb_dwc_hal_chan_error_t chan_error) +{ + hcd_pipe_event_t event = HCD_PIPE_EVENT_NONE; + switch (chan_error) { + case USB_DWC_HAL_CHAN_ERROR_XCS_XACT: + event = HCD_PIPE_EVENT_ERROR_XFER; + break; + case USB_DWC_HAL_CHAN_ERROR_BNA: + event = HCD_PIPE_EVENT_ERROR_URB_NOT_AVAIL; + break; + case USB_DWC_HAL_CHAN_ERROR_PKT_BBL: + event = HCD_PIPE_EVENT_ERROR_OVERFLOW; + break; + case USB_DWC_HAL_CHAN_ERROR_STALL: + event = HCD_PIPE_EVENT_ERROR_STALL; + break; + } + 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(USB_DWC_QTD_LIST_MEM_ALIGN, desc_list_len, sizeof(usb_dwc_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_args_usb_compliance_verification(const hcd_pipe_config_t *pipe_config, usb_speed_t port_speed, usb_transfer_type_t type) +{ + // Check if pipe can be supported + if (port_speed == USB_SPEED_LOW && pipe_config->dev_speed == USB_SPEED_FULL) { + ESP_LOGE(HCD_DWC_TAG, "Low speed port does not support full speed pipe"); + return false; + } + + if (pipe_config->dev_speed == USB_SPEED_LOW && (type == USB_TRANSFER_TYPE_BULK || type == USB_TRANSFER_TYPE_ISOCHRONOUS)) { + ESP_LOGE(HCD_DWC_TAG, "Low speed does not support Bulk or Isochronous pipes"); + return false; + } + + return true; +} + +static bool pipe_alloc_hcd_support_verification(usb_dwc_hal_context_t *hal, const usb_ep_desc_t * ep_desc) +{ + assert(hal != NULL); + assert(ep_desc != NULL); + + usb_hal_fifo_mps_limits_t mps_limits = {0}; + usb_dwc_hal_get_mps_limits(hal, &mps_limits); + const usb_transfer_type_t type = USB_EP_DESC_GET_XFERTYPE(ep_desc); + + // Check the pipe's interval is not zero + if ((type == USB_TRANSFER_TYPE_INTR || type == USB_TRANSFER_TYPE_ISOCHRONOUS) && + (ep_desc->bInterval == 0)) { + ESP_LOGE(HCD_DWC_TAG, "bInterval value (%d) invalid for pipe type INTR/ISOC", + ep_desc->bInterval); + return false; + } + + // Check if pipe MPS exceeds HCD MPS limits (due to DWC FIFO sizing) + int limit; + if (USB_EP_DESC_GET_EP_DIR(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; + } + } + + if (USB_EP_DESC_GET_MPS(ep_desc) > limit) { + ESP_LOGE(HCD_DWC_TAG, "EP MPS (%d) exceeds supported limit (%d)", + USB_EP_DESC_GET_MPS(ep_desc), + limit); + return false; + } + + return true; +} + +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, usb_dwc_hal_ep_char_t *ep_char) +{ + // Initialize EP characteristics + usb_dwc_xfer_type_t hal_xfer_type; + switch (type) { + case USB_TRANSFER_TYPE_CTRL: + hal_xfer_type = USB_DWC_XFER_TYPE_CTRL; + break; + case USB_TRANSFER_TYPE_ISOCHRONOUS: + hal_xfer_type = USB_DWC_XFER_TYPE_ISOCHRONOUS; + break; + case USB_TRANSFER_TYPE_BULK: + hal_xfer_type = USB_DWC_XFER_TYPE_BULK; + break; + default: // USB_TRANSFER_TYPE_INTR + hal_xfer_type = USB_DWC_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_LOW) ? CTRL_EP_MAX_MPS_LS : CTRL_EP_MAX_MPS_HSFS; + } else { + ep_char->bEndpointAddress = pipe_config->ep_desc->bEndpointAddress; + ep_char->mps = USB_EP_DESC_GET_MPS(pipe_config->ep_desc); + } + 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 + // @see USB-OTG programming guide chapter 6.5 for more information + if (type == USB_TRANSFER_TYPE_INTR || type == USB_TRANSFER_TYPE_ISOCHRONOUS) { + // Convert bInterval field to real value + // @see USB 2.0 specs, Table 9-13 + unsigned int interval_value; + if (type == USB_TRANSFER_TYPE_INTR && pipe_config->dev_speed != USB_SPEED_HIGH) { + interval_value = pipe_config->ep_desc->bInterval; + } else { + interval_value = (1 << (pipe_config->ep_desc->bInterval - 1)); + } + ep_char->periodic.interval = interval_value; + // We are the Nth pipe to be allocated. Use N as a phase offset + unsigned int xfer_list_len = (type == USB_TRANSFER_TYPE_INTR) ? XFER_LIST_LEN_INTR : XFER_LIST_LEN_ISOC; + ep_char->periodic.offset = (pipe_idx % xfer_list_len) % interval_value; + ep_char->periodic.is_hs = (pipe_config->dev_speed == USB_SPEED_HIGH); + } else { + ep_char->periodic.interval = 0; + ep_char->periodic.offset = 0; + } +} + +// ---------------------- Commands ------------------------- + +static esp_err_t _pipe_cmd_halt(pipe_t *pipe) +{ + esp_err_t ret; + + // If pipe is already halted, just return. + if (pipe->state == HCD_PIPE_STATE_HALTED) { + ret = ESP_OK; + goto exit; + } + // If the pipe's port is invalid, we just mark the pipe as halted without needing to halt the underlying channel + if (pipe->port->flags.conn_dev_ena // Skip halting the underlying channel if the port is invalid + && !usb_dwc_hal_chan_request_halt(pipe->chan_obj)) { // Check if the channel is already halted + // Channel is not halted, we need to request and wait for a haltWe need to wait for channel to be halted. + pipe->cs_flags.waiting_halt = 1; + _internal_pipe_event_wait(pipe); + // State should have been updated in the ISR + assert(pipe->state == HCD_PIPE_STATE_HALTED); + } else { + // We are already halted, just need to update the state + usb_dwc_hal_chan_mark_halted(pipe->chan_obj); + pipe->state = HCD_PIPE_STATE_HALTED; + } + ret = ESP_OK; +exit: + return ret; +} + +static esp_err_t _pipe_cmd_flush(pipe_t *pipe) +{ + esp_err_t ret; + // The pipe must be halted in order to be flushed + if (pipe->state != HCD_PIPE_STATE_HALTED) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + // If the port is still valid, we are canceling transfers. Otherwise, we are flushing due to a port error + bool canceled = pipe->port->flags.conn_dev_ena; + bool call_pipe_cb; + // Flush any filled buffers + call_pipe_cb = _buffer_flush_all(pipe, canceled); + // Move all URBs from the pending tailq to the done tailq + if (pipe->num_urb_pending > 0) { + // Process all remaining pending URBs + urb_t *urb; + TAILQ_FOREACH(urb, &pipe->pending_urb_tailq, tailq_entry) { + // Update the URB's current state + urb->hcd_var = URB_HCD_STATE_DONE; + // URBs were never executed, Update the actual_num_bytes and status + urb->transfer.actual_num_bytes = 0; + urb->transfer.status = (canceled) ? USB_TRANSFER_STATUS_CANCELED : USB_TRANSFER_STATUS_NO_DEVICE; + if (pipe->ep_char.type == USB_DWC_XFER_TYPE_ISOCHRONOUS) { + // Update the URB's isoc packet descriptors as well + for (int pkt_idx = 0; pkt_idx < urb->transfer.num_isoc_packets; pkt_idx++) { + urb->transfer.isoc_packet_desc[pkt_idx].actual_num_bytes = 0; + urb->transfer.isoc_packet_desc[pkt_idx].status = (canceled) ? USB_TRANSFER_STATUS_CANCELED : USB_TRANSFER_STATUS_NO_DEVICE; + } + } + } + // Concatenated pending tailq to the done tailq + TAILQ_CONCAT(&pipe->done_urb_tailq, &pipe->pending_urb_tailq, tailq_entry); + pipe->num_urb_done += pipe->num_urb_pending; + pipe->num_urb_pending = 0; + call_pipe_cb = true; + } + if (call_pipe_cb) { + // One or more URBs can be dequeued as a result of the flush. We need to call the callback + HCD_EXIT_CRITICAL(); + pipe->callback((hcd_pipe_handle_t)pipe, HCD_PIPE_EVENT_URB_DONE, pipe->callback_arg, false); + HCD_ENTER_CRITICAL(); + } + ret = ESP_OK; +exit: + return ret; +} + +static esp_err_t _pipe_cmd_clear(pipe_t *pipe) +{ + esp_err_t ret; + // Pipe must be in the halted state in order to be made active, and there must be an enabled device on the port + if (pipe->state != HCD_PIPE_STATE_HALTED || !pipe->port->flags.conn_dev_ena) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + // Update the pipe's state + pipe->state = HCD_PIPE_STATE_ACTIVE; + if (pipe->num_urb_pending > 0) { + // Fill as many buffers as possible + while (_buffer_can_fill(pipe)) { + _buffer_fill(pipe); + } + } + // Execute any filled buffers + if (_buffer_can_exec(pipe)) { + _buffer_exec(pipe); + } + ret = ESP_OK; +exit: + return ret; +} + +// ----------------------- Public -------------------------- + +esp_err_t hcd_pipe_alloc(hcd_port_handle_t port_hdl, const hcd_pipe_config_t *pipe_config, hcd_pipe_handle_t *pipe_hdl) +{ + HCD_CHECK(port_hdl != NULL && pipe_config != NULL && pipe_hdl != NULL, ESP_ERR_INVALID_ARG); + port_t *port = (port_t *)port_hdl; + HCD_ENTER_CRITICAL(); + // Can only allocate a pipe if the target port is initialized and connected to an enabled device + HCD_CHECK_FROM_CRIT(port->initialized && port->flags.conn_dev_ena, ESP_ERR_INVALID_STATE); + usb_speed_t port_speed = port->speed; + int pipe_idx = port->num_pipes_idle + port->num_pipes_queued; + HCD_EXIT_CRITICAL(); + + usb_transfer_type_t type; + bool is_default; + if (pipe_config->ep_desc == NULL) { + // Default CTRL pipe allocation + type = USB_TRANSFER_TYPE_CTRL; + is_default = true; + } else { + type = USB_EP_DESC_GET_XFERTYPE(pipe_config->ep_desc); + is_default = false; + } + + esp_err_t ret; + // Check if pipe configuration can be supported + if (!pipe_args_usb_compliance_verification(pipe_config, port_speed, type)) { + return ESP_ERR_NOT_SUPPORTED; + } + // Default pipes have a NULL ep_desc thus should skip the HCD support verification + if (!is_default && !pipe_alloc_hcd_support_verification(port->hal, pipe_config->ep_desc)) { + return ESP_ERR_NOT_SUPPORTED; + } + // Allocate the pipe resources + pipe_t *pipe = calloc(1, sizeof(pipe_t)); + usb_dwc_hal_chan_t *chan_obj = calloc(1, sizeof(usb_dwc_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_urb_tailq); + TAILQ_INIT(&pipe->done_urb_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->chan_obj = chan_obj; + usb_dwc_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(usb_dwc_hal_ep_char_t)); + pipe->state = HCD_PIPE_STATE_ACTIVE; + pipe->callback = pipe_config->callback; + pipe->callback_arg = pipe_config->callback_arg; + pipe->context = pipe_config->context; + + // Allocate channel + HCD_ENTER_CRITICAL(); + if (!port->initialized || !port->flags.conn_dev_ena) { + HCD_EXIT_CRITICAL(); + ret = ESP_ERR_INVALID_STATE; + goto err; + } + bool chan_allocated = usb_dwc_hal_chan_alloc(port->hal, pipe->chan_obj, (void *) pipe); + if (!chan_allocated) { + HCD_EXIT_CRITICAL(); + ret = ESP_ERR_NOT_SUPPORTED; + goto err; + } + usb_dwc_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 ESP_OK; + +err: + for (int i = 0; i < NUM_BUFFERS; i++) { + buffer_block_free(buffers[i]); + } + free(chan_obj); + free(pipe); + return ret; +} + +int hcd_pipe_get_mps(hcd_pipe_handle_t pipe_hdl) +{ + pipe_t *pipe = (pipe_t *)pipe_hdl; + int mps; + HCD_ENTER_CRITICAL(); + mps = pipe->ep_char.mps; + HCD_EXIT_CRITICAL(); + return mps; +} + +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 URBs have been removed and pipe has no pending events + HCD_CHECK_FROM_CRIT(!pipe->multi_buffer_control.buffer_is_executing + && !pipe->cs_flags.has_urb, + ESP_ERR_INVALID_STATE); + // Remove pipe from the list of idle pipes (it must be in the idle list because it should have no queued URBs) + TAILQ_REMOVE(&pipe->port->pipes_idle_tailq, pipe, tailq_entry); + pipe->port->num_pipes_idle--; + usb_dwc_hal_chan_free(pipe->port->hal, pipe->chan_obj); + HCD_EXIT_CRITICAL(); + + // Free pipe resources + 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_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->cs_flags.pipe_cmd_processing && + !pipe->cs_flags.has_urb, + ESP_ERR_INVALID_STATE); + pipe->ep_char.mps = mps; + // Update the underlying channel's registers + usb_dwc_hal_chan_set_ep_char(pipe->port->hal, pipe->chan_obj, &pipe->ep_char); + HCD_EXIT_CRITICAL(); + return ESP_OK; +} + +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->cs_flags.pipe_cmd_processing && + !pipe->cs_flags.has_urb, + ESP_ERR_INVALID_STATE); + pipe->ep_char.dev_addr = dev_addr; + // Update the underlying channel's registers + usb_dwc_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; + HCD_ENTER_CRITICAL(); + ret = pipe->context; + HCD_EXIT_CRITICAL(); + return ret; +} + +hcd_pipe_state_t hcd_pipe_get_state(hcd_pipe_handle_t pipe_hdl) +{ + hcd_pipe_state_t ret; + pipe_t *pipe = (pipe_t *)pipe_hdl; + HCD_ENTER_CRITICAL(); + ret = pipe->state; + HCD_EXIT_CRITICAL(); + return ret; +} + +unsigned int hcd_pipe_get_num_urbs(hcd_pipe_handle_t pipe_hdl) +{ + unsigned int ret; + pipe_t *pipe = (pipe_t *)pipe_hdl; + HCD_ENTER_CRITICAL(); + ret = pipe->num_urb_pending + pipe->num_urb_done; + HCD_EXIT_CRITICAL(); + return ret; +} + +esp_err_t hcd_pipe_command(hcd_pipe_handle_t pipe_hdl, hcd_pipe_cmd_t command) +{ + pipe_t *pipe = (pipe_t *)pipe_hdl; + esp_err_t ret = ESP_OK; + + HCD_ENTER_CRITICAL(); + pipe->cs_flags.pipe_cmd_processing = 1; + switch (command) { + case HCD_PIPE_CMD_HALT: { + ret = _pipe_cmd_halt(pipe); + break; + } + case HCD_PIPE_CMD_FLUSH: { + ret = _pipe_cmd_flush(pipe); + break; + } + case HCD_PIPE_CMD_CLEAR: { + ret = _pipe_cmd_clear(pipe); + break; + } + } + pipe->cs_flags.pipe_cmd_processing = 0; + HCD_EXIT_CRITICAL(); + return ret; +} + +hcd_pipe_event_t hcd_pipe_get_event(hcd_pipe_handle_t pipe_hdl) +{ + pipe_t *pipe = (pipe_t *)pipe_hdl; + hcd_pipe_event_t ret; + HCD_ENTER_CRITICAL(); + ret = pipe->last_event; + pipe->last_event = HCD_PIPE_EVENT_NONE; + HCD_EXIT_CRITICAL(); + return ret; +} + +// ------------------------------------------------- Buffer Control ---------------------------------------------------- + +static inline void _buffer_fill_ctrl(dma_buffer_block_t *buffer, usb_transfer_t *transfer) +{ + // Get information about the control transfer by analyzing the setup packet (the first 8 bytes of the URB's data) + usb_setup_packet_t *setup_pkt = (usb_setup_packet_t *)transfer->data_buffer; + bool data_stg_in = (setup_pkt->bmRequestType & USB_BM_REQUEST_TYPE_DIR_IN); + bool data_stg_skip = (setup_pkt->wLength == 0); + // Fill setup stage + usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, 0, transfer->data_buffer, sizeof(usb_setup_packet_t), + USB_DWC_HAL_XFER_DESC_FLAG_SETUP | USB_DWC_HAL_XFER_DESC_FLAG_HOC); + // Fill data stage + if (data_stg_skip) { + // Not data stage. Fill with an empty descriptor + usb_dwc_hal_xfer_desc_clear(buffer->xfer_desc_list, 1); + } else { + // Fill data stage. Note that we still fill with transfer->num_bytes instead of setup_pkt->wLength as it's possible to require more bytes than wLength + usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, 1, transfer->data_buffer + sizeof(usb_setup_packet_t), transfer->num_bytes - sizeof(usb_setup_packet_t), + ((data_stg_in) ? USB_DWC_HAL_XFER_DESC_FLAG_IN : 0) | USB_DWC_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. + usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, 2, NULL, 0, + ((data_stg_in && !data_stg_skip) ? 0 : USB_DWC_HAL_XFER_DESC_FLAG_IN) | USB_DWC_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; +} + +static inline void _buffer_fill_bulk(dma_buffer_block_t *buffer, usb_transfer_t *transfer, bool is_in, int mps) +{ + // Only add a zero length packet if OUT, flag is set, and transfer length is multiple of EP's MPS + // Minor optimization: Do the mod operation last + bool zero_len_packet = !is_in && (transfer->flags & USB_TRANSFER_FLAG_ZERO_PACK) && (transfer->num_bytes % mps == 0); + if (is_in) { + usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, 0, transfer->data_buffer, transfer->num_bytes, + USB_DWC_HAL_XFER_DESC_FLAG_IN | USB_DWC_HAL_XFER_DESC_FLAG_HOC); + } else { // OUT + if (zero_len_packet) { + // Adding a zero length packet, so two descriptors are used. + usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, 0, transfer->data_buffer, transfer->num_bytes, 0); + usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, 1, NULL, 0, USB_DWC_HAL_XFER_DESC_FLAG_HOC); + } else { + // Zero length packet not required. One descriptor is enough + usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, 0, transfer->data_buffer, transfer->num_bytes, USB_DWC_HAL_XFER_DESC_FLAG_HOC); + } + } + // Update buffer flags + buffer->flags.bulk.zero_len_packet = zero_len_packet; +} + +static inline void _buffer_fill_intr(dma_buffer_block_t *buffer, usb_transfer_t *transfer, bool is_in, int mps) +{ + int num_qtds; + int mod_mps = transfer->num_bytes % mps; + // Only add a zero length packet if OUT, flag is set, and transfer length is multiple of EP's MPS + bool zero_len_packet = !is_in && (transfer->flags & USB_TRANSFER_FLAG_ZERO_PACK) && (mod_mps == 0); + if (is_in) { + assert(mod_mps == 0); // IN transfers MUST be integer multiple of MPS + num_qtds = transfer->num_bytes / mps; // Can just floor divide as it's already multiple of MPS + } else { + num_qtds = transfer->num_bytes / mps; // Floor division to get the number of MPS sized packets + if (mod_mps > 0) { + num_qtds++; // Add a short packet for the remainder + } + } + assert((zero_len_packet) ? num_qtds + 1 : num_qtds <= XFER_LIST_LEN_INTR); // Check that the number of QTDs doesn't exceed the QTD list's length + + uint32_t xfer_desc_flags = (is_in) ? USB_DWC_HAL_XFER_DESC_FLAG_IN : 0; + int bytes_filled = 0; + // Fill all but last QTD + for (int i = 0; i < num_qtds - 1; i++) { + usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, i, &transfer->data_buffer[bytes_filled], mps, xfer_desc_flags); + bytes_filled += mps; + } + // Fill last QTD and zero length packet + if (zero_len_packet) { + // Fill in last data packet without HOC flag + usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, num_qtds - 1, &transfer->data_buffer[bytes_filled], transfer->num_bytes - bytes_filled, + xfer_desc_flags); + // HOC flag goes to zero length packet instead + usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, num_qtds, NULL, 0, USB_DWC_HAL_XFER_DESC_FLAG_HOC); + } else { + // Zero length packet not required. Fill in last QTD with HOC flag + usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, num_qtds - 1, &transfer->data_buffer[bytes_filled], transfer->num_bytes - bytes_filled, + xfer_desc_flags | USB_DWC_HAL_XFER_DESC_FLAG_HOC); + } + + // Update buffer members and flags + buffer->flags.intr.num_qtds = num_qtds; + buffer->flags.intr.zero_len_packet = zero_len_packet; +} + +static inline void IRAM_ATTR _buffer_fill_isoc(dma_buffer_block_t *buffer, usb_transfer_t *transfer, bool is_in, int mps, int interval, int start_idx) +{ + assert(interval > 0); + assert(__builtin_popcount(interval) == 1); // Isochronous interval must be power of 2 according to USB2.0 specification + int total_num_desc = transfer->num_isoc_packets * interval; + assert(total_num_desc <= XFER_LIST_LEN_ISOC - XFER_LIST_ISOC_MARGIN); // Some space in the qTD list is reserved for timing margin + int desc_idx = start_idx; + int bytes_filled = 0; + // Zeroize the whole QTD, so we can focus only on the active descriptors + memset(buffer->xfer_desc_list, 0, XFER_LIST_LEN_ISOC * sizeof(usb_dwc_ll_dma_qtd_t)); + for (int pkt_idx = 0; pkt_idx < transfer->num_isoc_packets; pkt_idx++) { + int xfer_len = transfer->isoc_packet_desc[pkt_idx].num_bytes; + uint32_t flags = (is_in) ? USB_DWC_HAL_XFER_DESC_FLAG_IN : 0; + if (pkt_idx == transfer->num_isoc_packets - 1) { + // Last packet, set the the HOC flag + flags |= USB_DWC_HAL_XFER_DESC_FLAG_HOC; + } + usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, desc_idx, &transfer->data_buffer[bytes_filled], xfer_len, flags); + bytes_filled += xfer_len; + desc_idx += interval; + desc_idx %= XFER_LIST_LEN_ISOC; + } + // Update buffer members and flags + buffer->flags.isoc.num_qtds = total_num_desc; + buffer->flags.isoc.interval = interval; + buffer->flags.isoc.start_idx = start_idx; + buffer->flags.isoc.next_start_idx = desc_idx; +} + +static void IRAM_ATTR _buffer_fill(pipe_t *pipe) +{ + // Get an URB from the pending tailq + urb_t *urb = TAILQ_FIRST(&pipe->pending_urb_tailq); + assert(pipe->num_urb_pending > 0 && urb != NULL); + TAILQ_REMOVE(&pipe->pending_urb_tailq, urb, tailq_entry); + pipe->num_urb_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]; + buffer_to_fill->status_flags.val = 0; // Clear the buffer's status flags + assert(buffer_to_fill->urb == NULL); + bool is_in = pipe->ep_char.bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK; + int mps = pipe->ep_char.mps; + usb_transfer_t *transfer = &urb->transfer; + switch (pipe->ep_char.type) { + case USB_DWC_XFER_TYPE_CTRL: { + _buffer_fill_ctrl(buffer_to_fill, transfer); + break; + } + case USB_DWC_XFER_TYPE_ISOCHRONOUS: { + uint16_t start_idx; + // Interval in frames (FS) or microframes (HS). But it does not matter here, as each QTD represents one transaction in a frame or microframe + unsigned int interval = pipe->ep_char.periodic.interval; + if (interval > XFER_LIST_LEN_ISOC) { + // Each QTD in the list corresponds to one frame/microframe. Interval > Descriptor_list does not make sense here. + interval = XFER_LIST_LEN_ISOC; + } + 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 + uint16_t cur_frame_num = usb_dwc_hal_port_get_cur_frame_num(pipe->port->hal); + start_idx = cur_frame_num + 1; // This is the next frame that the periodic scheduler will fetch + start_idx += XFER_LIST_ISOC_MARGIN; // Start scheduling with a little delay. This will get us enough timing margin so no transfer is skipped + + // Only every (interval + offset) transfer belongs to this channel + // Following calculation effectively rounds up to nearest (interval + offset) + if (interval > 1) { + uint32_t interval_offset = (start_idx - pipe->ep_char.periodic.offset) % interval; // Can be <0, interval) + if (interval_offset > 0) { + start_idx += interval - interval_offset; + } + } + start_idx %= XFER_LIST_LEN_ISOC; + } 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_start_idx; + } + _buffer_fill_isoc(buffer_to_fill, transfer, is_in, mps, (int)interval, start_idx); + break; + } + case USB_DWC_XFER_TYPE_BULK: { + _buffer_fill_bulk(buffer_to_fill, transfer, is_in, mps); + break; + } + case USB_DWC_XFER_TYPE_INTR: { + _buffer_fill_intr(buffer_to_fill, transfer, is_in, mps); + break; + } + default: { + abort(); + break; + } + } + buffer_to_fill->urb = urb; + urb->hcd_var = URB_HCD_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 IRAM_ATTR _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->urb != NULL); + + uint32_t start_idx; + int desc_list_len; + switch (pipe->ep_char.type) { + case USB_DWC_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 + usb_dwc_hal_chan_set_dir(pipe->chan_obj, false); // Setup stage is always OUT + usb_dwc_hal_chan_set_pid(pipe->chan_obj, 0); // Setup stage always has a PID of DATA0 + break; + } + case USB_DWC_XFER_TYPE_ISOCHRONOUS: { + start_idx = buffer_to_exec->flags.isoc.start_idx; + desc_list_len = XFER_LIST_LEN_ISOC; + break; + } + case USB_DWC_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_DWC_XFER_TYPE_INTR: { + start_idx = 0; + desc_list_len = (buffer_to_exec->flags.intr.zero_len_packet) ? buffer_to_exec->flags.intr.num_qtds + 1 : buffer_to_exec->flags.intr.num_qtds; + break; + } + default: { + start_idx = 0; + desc_list_len = 0; + abort(); + break; + } + } + // Update buffer and multi buffer flags + buffer_to_exec->status_flags.executing = 1; + pipe->multi_buffer_control.buffer_is_executing = 1; + usb_dwc_hal_chan_activate(pipe->chan_obj, buffer_to_exec->xfer_desc_list, desc_list_len, start_idx); +} + +static void _buffer_exec_cont(pipe_t *pipe) +{ + // This should only ever be called on control transfers + assert(pipe->ep_char.type == USB_DWC_XFER_TYPE_CTRL); + dma_buffer_block_t *buffer_inflight = pipe->buffers[pipe->multi_buffer_control.rd_idx]; + bool next_dir_is_in; + int next_pid; + assert(buffer_inflight->flags.ctrl.cur_stg != 2); + 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 + 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 = buffer_inflight->flags.ctrl.data_stg_in; + next_pid = 1; // Data stage always starts with a PID of DATA1 + buffer_inflight->flags.ctrl.cur_stg = 1; + } + } else { // 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 + buffer_inflight->flags.ctrl.cur_stg = 2; + } + // Continue the control transfer + usb_dwc_hal_chan_set_dir(pipe->chan_obj, next_dir_is_in); + usb_dwc_hal_chan_set_pid(pipe->chan_obj, next_pid); + usb_dwc_hal_chan_activate(pipe->chan_obj, buffer_inflight->xfer_desc_list, XFER_LIST_LEN_CTRL, buffer_inflight->flags.ctrl.cur_stg); +} + +static inline void _buffer_parse_ctrl(dma_buffer_block_t *buffer) +{ + usb_transfer_t *transfer = &buffer->urb->transfer; + // Update URB's actual number of bytes + if (buffer->flags.ctrl.data_stg_skip) { + // There was no data stage. Just set the actual length to the size of the setup packet + transfer->actual_num_bytes = sizeof(usb_setup_packet_t); + } else { + // Parse the data stage for the remaining length + int rem_len; + int desc_status; + usb_dwc_hal_xfer_desc_parse(buffer->xfer_desc_list, 1, &rem_len, &desc_status); + assert(desc_status == USB_DWC_HAL_XFER_DESC_STS_SUCCESS); + assert(rem_len <= (transfer->num_bytes - sizeof(usb_setup_packet_t))); + transfer->actual_num_bytes = transfer->num_bytes - rem_len; + } + // Update URB status + transfer->status = USB_TRANSFER_STATUS_COMPLETED; + // Clear the descriptor list + memset(buffer->xfer_desc_list, 0, XFER_LIST_LEN_CTRL * sizeof(usb_dwc_ll_dma_qtd_t)); +} + +static inline void _buffer_parse_bulk(dma_buffer_block_t *buffer) +{ + usb_transfer_t *transfer = &buffer->urb->transfer; + // Update URB's actual number of bytes + int rem_len; + int desc_status; + usb_dwc_hal_xfer_desc_parse(buffer->xfer_desc_list, 0, &rem_len, &desc_status); + assert(desc_status == USB_DWC_HAL_XFER_DESC_STS_SUCCESS); + assert(rem_len <= transfer->num_bytes); + transfer->actual_num_bytes = transfer->num_bytes - rem_len; + // Update URB's status + transfer->status = USB_TRANSFER_STATUS_COMPLETED; + // Clear the descriptor list + memset(buffer->xfer_desc_list, 0, XFER_LIST_LEN_BULK * sizeof(usb_dwc_ll_dma_qtd_t)); +} + +static inline void _buffer_parse_intr(dma_buffer_block_t *buffer, bool is_in, int mps) +{ + usb_transfer_t *transfer = &buffer->urb->transfer; + 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 + usb_dwc_hal_xfer_desc_parse(buffer->xfer_desc_list, i, &rem_len, &desc_status); + assert(rem_len == 0 && desc_status == USB_DWC_HAL_XFER_DESC_STS_SUCCESS); + } + // Check the short packet + usb_dwc_hal_xfer_desc_parse(buffer->xfer_desc_list, intr_stop_idx - 1, &rem_len, &desc_status); + assert(rem_len > 0 && desc_status == USB_DWC_HAL_XFER_DESC_STS_SUCCESS); + // Update actual bytes + transfer->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; + usb_dwc_hal_xfer_desc_parse(buffer->xfer_desc_list, i, &rem_len, &desc_status); + assert(rem_len == 0 && desc_status == USB_DWC_HAL_XFER_DESC_STS_SUCCESS); + } + // Check the last packet + int last_packet_rem_len; + int last_packet_desc_status; + usb_dwc_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 == USB_DWC_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 + transfer->actual_num_bytes = transfer->num_bytes - last_packet_rem_len; + } + } else { + // OUT INTR transfers can only complete successfully if all packets have been transmitted. Double check + for (int i = 0 ; i < buffer->flags.intr.num_qtds; i++) { + int rem_len; + int desc_status; + usb_dwc_hal_xfer_desc_parse(buffer->xfer_desc_list, i, &rem_len, &desc_status); + assert(rem_len == 0 && desc_status == USB_DWC_HAL_XFER_DESC_STS_SUCCESS); + } + transfer->actual_num_bytes = transfer->num_bytes; + } + // Update URB's status + transfer->status = USB_TRANSFER_STATUS_COMPLETED; + // Clear the descriptor list + memset(buffer->xfer_desc_list, 0, XFER_LIST_LEN_INTR * sizeof(usb_dwc_ll_dma_qtd_t)); +} + +static inline void _buffer_parse_isoc(dma_buffer_block_t *buffer, bool is_in) +{ + usb_transfer_t *transfer = &buffer->urb->transfer; + int desc_idx = buffer->flags.isoc.start_idx; // Descriptor index tracks which descriptor in the QTD list + int total_actual_num_bytes = 0; + for (int pkt_idx = 0; pkt_idx < transfer->num_isoc_packets; pkt_idx++) { + // Clear the filled descriptor + int rem_len; + int desc_status; + usb_dwc_hal_xfer_desc_parse(buffer->xfer_desc_list, desc_idx, &rem_len, &desc_status); + usb_dwc_hal_xfer_desc_clear(buffer->xfer_desc_list, desc_idx); + switch (desc_status) { + case USB_DWC_HAL_XFER_DESC_STS_SUCCESS: + transfer->isoc_packet_desc[pkt_idx].status = USB_TRANSFER_STATUS_COMPLETED; + break; + case USB_DWC_HAL_XFER_DESC_STS_NOT_EXECUTED: + transfer->isoc_packet_desc[pkt_idx].status = USB_TRANSFER_STATUS_SKIPPED; + break; + case USB_DWC_HAL_XFER_DESC_STS_PKTERR: + transfer->isoc_packet_desc[pkt_idx].status = USB_TRANSFER_STATUS_ERROR; + break; + case USB_DWC_HAL_XFER_DESC_STS_BUFFER_ERR: + transfer->isoc_packet_desc[pkt_idx].status = USB_TRANSFER_STATUS_ERROR; + break; + default: + assert(false); + break; + } + + assert(rem_len <= transfer->isoc_packet_desc[pkt_idx].num_bytes); // Check for DMA errata + // Update ISO packet actual length and status + transfer->isoc_packet_desc[pkt_idx].actual_num_bytes = transfer->isoc_packet_desc[pkt_idx].num_bytes - rem_len; + total_actual_num_bytes += transfer->isoc_packet_desc[pkt_idx].actual_num_bytes; + // A descriptor is also allocated for unscheduled frames. We need to skip over them + desc_idx += buffer->flags.isoc.interval; + desc_idx %= XFER_LIST_LEN_ISOC; + } + // Write back the actual_num_bytes and statue of entire transfer + assert(total_actual_num_bytes <= transfer->num_bytes); + transfer->actual_num_bytes = total_actual_num_bytes; + transfer->status = USB_TRANSFER_STATUS_COMPLETED; +} + +static inline void _buffer_parse_error(dma_buffer_block_t *buffer) +{ + // The URB had an error in one of its packet, or a port error), so we the entire URB an error. + usb_transfer_t *transfer = &buffer->urb->transfer; + transfer->actual_num_bytes = 0; + // Update the overall status of URB. Status will depend on the pipe_event + switch (buffer->status_flags.pipe_event) { + case HCD_PIPE_EVENT_NONE: + transfer->status = (buffer->status_flags.was_canceled) ? USB_TRANSFER_STATUS_CANCELED : USB_TRANSFER_STATUS_NO_DEVICE; + break; + case HCD_PIPE_EVENT_ERROR_XFER: + transfer->status = USB_TRANSFER_STATUS_ERROR; + break; + case HCD_PIPE_EVENT_ERROR_OVERFLOW: + transfer->status = USB_TRANSFER_STATUS_OVERFLOW; + break; + case HCD_PIPE_EVENT_ERROR_STALL: + transfer->status = USB_TRANSFER_STATUS_STALL; + break; + default: + // HCD_PIPE_EVENT_URB_DONE and HCD_PIPE_EVENT_ERROR_URB_NOT_AVAIL should not occur here + abort(); + break; + } +} + +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->urb != 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 URB + if (buffer_to_parse->status_flags.pipe_event == HCD_PIPE_EVENT_URB_DONE) { + // URB was successful + switch (pipe->ep_char.type) { + case USB_DWC_XFER_TYPE_CTRL: { + _buffer_parse_ctrl(buffer_to_parse); + break; + } + case USB_DWC_XFER_TYPE_ISOCHRONOUS: { + _buffer_parse_isoc(buffer_to_parse, is_in); + break; + } + case USB_DWC_XFER_TYPE_BULK: { + _buffer_parse_bulk(buffer_to_parse); + break; + } + case USB_DWC_XFER_TYPE_INTR: { + _buffer_parse_intr(buffer_to_parse, is_in, mps); + break; + } + default: { + abort(); + break; + } + } + } else { + // URB failed + _buffer_parse_error(buffer_to_parse); + } + urb_t *urb = buffer_to_parse->urb; + urb->hcd_var = URB_HCD_STATE_DONE; + buffer_to_parse->urb = NULL; + buffer_to_parse->flags.val = 0; // Clear flags + // Move the URB to the done tailq + TAILQ_INSERT_TAIL(&pipe->done_urb_tailq, urb, tailq_entry); + pipe->num_urb_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 bool _buffer_flush_all(pipe_t *pipe, bool canceled) +{ + 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(pipe, 0, HCD_PIPE_EVENT_NONE, canceled); + } + 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 URBs in the pending or done tailq + return (cur_num_to_parse > 0); +} + +// ---------------------------------------------- HCD Transfer Descriptors --------------------------------------------- + +// ----------------------- Public -------------------------- + +esp_err_t hcd_urb_enqueue(hcd_pipe_handle_t pipe_hdl, urb_t *urb) +{ + // Check that URB has not already been enqueued + HCD_CHECK(urb->hcd_ptr == NULL && urb->hcd_var == URB_HCD_STATE_IDLE, ESP_ERR_INVALID_STATE); + pipe_t *pipe = (pipe_t *)pipe_hdl; + // Check if the ISOC pipe can handle all packets: + // In case the pipe's interval is too long and there are too many ISOC packets, they might not fit into the transfer descriptor list + HCD_CHECK( + !((pipe->ep_char.type == USB_DWC_XFER_TYPE_ISOCHRONOUS) && (urb->transfer.num_isoc_packets * pipe->ep_char.periodic.interval > XFER_LIST_LEN_ISOC)), + ESP_ERR_INVALID_SIZE + ); + + HCD_ENTER_CRITICAL(); + // Check that pipe and port are in the correct state to receive URBs + 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 URB's reserved_ptr to store the pipe's + urb->hcd_ptr = (void *)pipe; + // Add the URB to the pipe's pending tailq + urb->hcd_var = URB_HCD_STATE_PENDING; + TAILQ_INSERT_TAIL(&pipe->pending_urb_tailq, urb, tailq_entry); + pipe->num_urb_pending++; + // use the URB's reserved_flags to store the URB's current state + if (_buffer_can_fill(pipe)) { + _buffer_fill(pipe); + } + if (_buffer_can_exec(pipe)) { + _buffer_exec(pipe); + } + if (!pipe->cs_flags.has_urb) { + // This is the first URB 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.has_urb = 1; + } + HCD_EXIT_CRITICAL(); + return ESP_OK; +} + +urb_t *hcd_urb_dequeue(hcd_pipe_handle_t pipe_hdl) +{ + pipe_t *pipe = (pipe_t *)pipe_hdl; + urb_t *urb; + + HCD_ENTER_CRITICAL(); + if (pipe->num_urb_done > 0) { + urb = TAILQ_FIRST(&pipe->done_urb_tailq); + TAILQ_REMOVE(&pipe->done_urb_tailq, urb, tailq_entry); + pipe->num_urb_done--; + // Check the URB's reserved fields then reset them + assert(urb->hcd_ptr == (void *)pipe && urb->hcd_var == URB_HCD_STATE_DONE); // The URB's reserved field should have been set to this pipe + urb->hcd_ptr = NULL; + urb->hcd_var = URB_HCD_STATE_IDLE; + if (pipe->cs_flags.has_urb + && pipe->num_urb_pending == 0 && pipe->num_urb_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 URBs. 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.has_urb = 0; + } + } else { + // No more URBs to dequeue from this pipe + urb = NULL; + } + HCD_EXIT_CRITICAL(); + return urb; +} + +esp_err_t hcd_urb_abort(urb_t *urb) +{ + HCD_ENTER_CRITICAL(); + // Check that the URB was enqueued to begin with + HCD_CHECK_FROM_CRIT(urb->hcd_ptr != NULL && urb->hcd_var != URB_HCD_STATE_IDLE, ESP_ERR_INVALID_STATE); + if (urb->hcd_var == URB_HCD_STATE_PENDING) { + // URB has not been executed so it can be aborted + pipe_t *pipe = (pipe_t *)urb->hcd_ptr; + // Remove it form the pending queue + TAILQ_REMOVE(&pipe->pending_urb_tailq, urb, tailq_entry); + pipe->num_urb_pending--; + // Add it to the done queue + TAILQ_INSERT_TAIL(&pipe->done_urb_tailq, urb, tailq_entry); + pipe->num_urb_done++; + // Update the URB's current state, status, and actual length + urb->hcd_var = URB_HCD_STATE_DONE; + if (urb->transfer.num_isoc_packets == 0) { + urb->transfer.actual_num_bytes = 0; + urb->transfer.status = USB_TRANSFER_STATUS_CANCELED; + } else { + // If this is an ISOC URB, update the ISO packet descriptors instead + for (int i = 0; i < urb->transfer.num_isoc_packets; i++) { + urb->transfer.isoc_packet_desc[i].actual_num_bytes = 0; + urb->transfer.isoc_packet_desc[i].status = USB_TRANSFER_STATUS_CANCELED; + } + } + } // Otherwise, the URB is in-flight or already done thus cannot be aborted + HCD_EXIT_CRITICAL(); + return ESP_OK; +} diff --git a/components/usb/hub.c b/components/usb/hub.c new file mode 100644 index 0000000000..2b98a057b4 --- /dev/null +++ b/components/usb/hub.c @@ -0,0 +1,1116 @@ +/* + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "sdkconfig.h" +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/portmacro.h" +#include "esp_err.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "usb_private.h" +#include "hcd.h" +#include "hub.h" +#include "usb/usb_helpers.h" + +/* +Implementation of the HUB driver that only supports the Root Hub with a single port. Therefore, we currently don't +implement the bare minimum to control the root HCD port. +*/ + +#define HUB_ROOT_PORT_NUM 1 // HCD only supports one port +#ifdef CONFIG_USB_HOST_HW_BUFFER_BIAS_IN +#define HUB_ROOT_HCD_PORT_FIFO_BIAS HCD_PORT_FIFO_BIAS_RX +#elif CONFIG_USB_HOST_HW_BUFFER_BIAS_PERIODIC_OUT +#define HUB_ROOT_HCD_PORT_FIFO_BIAS HCD_PORT_FIFO_BIAS_PTX +#else // CONFIG_USB_HOST_HW_BUFFER_BIAS_BALANCED +#define HUB_ROOT_HCD_PORT_FIFO_BIAS HCD_PORT_FIFO_BIAS_BALANCED +#endif + +#ifdef CONFIG_USB_HOST_ENABLE_ENUM_FILTER_CALLBACK +#define ENABLE_ENUM_FILTER_CALLBACK +#endif // CONFIG_USB_HOST_ENABLE_ENUM_FILTER_CALLBACK + +#define SET_ADDR_RECOVERY_INTERVAL_MS CONFIG_USB_HOST_SET_ADDR_RECOVERY_MS + +#define ENUM_CTRL_TRANSFER_MAX_DATA_LEN CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE +#define ENUM_DEV_ADDR 1 // Device address used in enumeration +#define ENUM_DEV_UID 1 // Unique ID for device connected to root port +#define ENUM_CONFIG_INDEX_DEFAULT 0 // Index used to get the first configuration descriptor of the device +#define ENUM_SHORT_DESC_REQ_LEN 8 // Number of bytes to request when getting a short descriptor (just enough to get bMaxPacketSize0 or wTotalLength) +#define ENUM_WORST_CASE_MPS_LS 8 // The worst case MPS of EP0 for a LS device +#define ENUM_WORST_CASE_MPS_FS 64 // The worst case MPS of EP0 for a FS device +#define ENUM_LOW_SPEED_MPS 8 // Worst case MPS for the default endpoint of a low-speed device +#define ENUM_FULL_SPEED_MPS 64 // Worst case MPS for the default endpoint of a full-speed device +#define ENUM_LANGID 0x409 // Current enumeration only supports English (United States) string descriptors + +// Hub driver action flags. LISTED IN THE ORDER THEY SHOULD BE HANDLED IN within hub_process(). Some actions are mutually exclusive +#define HUB_DRIVER_FLAG_ACTION_ROOT_EVENT 0x01 +#define HUB_DRIVER_FLAG_ACTION_PORT_REQ 0x02 +#define HUB_DRIVER_FLAG_ACTION_ENUM_EVENT 0x04 + +#define PORT_REQ_DISABLE 0x01 +#define PORT_REQ_RECOVER 0x02 + +/** + * @brief Root port states + * + */ +typedef enum { + ROOT_PORT_STATE_NOT_POWERED, /**< Root port initialized and/or not powered */ + ROOT_PORT_STATE_POWERED, /**< Root port is powered, device is not connected */ + ROOT_PORT_STATE_DISABLED, /**< A device is connected but is disabled (i.e., not reset, no SOFs are sent) */ + ROOT_PORT_STATE_ENABLED, /**< A device is connected, port has been reset, SOFs are sent */ + ROOT_PORT_STATE_RECOVERY, /**< Root port encountered an error and needs to be recovered */ +} root_port_state_t; + +/** + * @brief Stages of device enumeration listed in their order of execution + * + * - These stages MUST BE LISTED IN THE ORDER OF THEIR EXECUTION as the enumeration will simply increment the current stage + * - If an error occurs at any stage, ENUM_STAGE_CLEANUP_FAILED acts as a common exit stage on failure + * - Must start with 0 as enum is also used as an index + * - The short descriptor stages are used to fetch the start particular descriptors that don't have a fixed length in order to determine the full descriptors length + */ +typedef enum { + ENUM_STAGE_NONE = 0, /**< There is no device awaiting enumeration. Start requires device connection and first reset. */ + ENUM_STAGE_START, /**< A device has connected and has already been reset once. Allocate a device object in USBH */ + // Basic device enumeration + ENUM_STAGE_GET_SHORT_DEV_DESC, /**< Getting short dev desc (wLength is ENUM_SHORT_DESC_REQ_LEN) */ + ENUM_STAGE_CHECK_SHORT_DEV_DESC, /**< Save bMaxPacketSize0 from the short dev desc. Update the MPS of the enum pipe */ + ENUM_STAGE_SECOND_RESET, /**< Reset the device again (Workaround for old USB devices that get confused by the previous short dev desc request). */ + ENUM_STAGE_SET_ADDR, /**< Send SET_ADDRESS request */ + ENUM_STAGE_CHECK_ADDR, /**< Update the enum pipe's target address */ + ENUM_STAGE_SET_ADDR_RECOVERY, /**< Wait SET ADDRESS recovery interval at least for 2ms due to usb_20, chapter 9.2.6.3 */ + ENUM_STAGE_GET_FULL_DEV_DESC, /**< Get the full dev desc */ + ENUM_STAGE_CHECK_FULL_DEV_DESC, /**< Check the full dev desc, fill it into the device object in USBH. Save the string descriptor indexes*/ + ENUM_STAGE_GET_SHORT_CONFIG_DESC, /**< Getting a short config desc (wLength is ENUM_SHORT_DESC_REQ_LEN) */ + ENUM_STAGE_CHECK_SHORT_CONFIG_DESC, /**< Save wTotalLength of the short config desc */ + ENUM_STAGE_GET_FULL_CONFIG_DESC, /**< Get the full config desc (wLength is the saved wTotalLength) */ + ENUM_STAGE_CHECK_FULL_CONFIG_DESC, /**< Check the full config desc, fill it into the device object in USBH */ + ENUM_STAGE_SET_CONFIG, /**< Send SET_CONFIGURATION request */ + ENUM_STAGE_CHECK_CONFIG, /**< Check that SET_CONFIGURATION request was successful */ + // Get string descriptors + ENUM_STAGE_GET_SHORT_LANGID_TABLE, /**< Get the header of the LANGID table string descriptor */ + ENUM_STAGE_CHECK_SHORT_LANGID_TABLE, /**< Save the bLength of the LANGID table string descriptor */ + ENUM_STAGE_GET_FULL_LANGID_TABLE, /**< Get the full LANGID table string descriptor */ + ENUM_STAGE_CHECK_FULL_LANGID_TABLE, /**< Check whether ENUM_LANGID is in the LANGID table */ + ENUM_STAGE_GET_SHORT_MANU_STR_DESC, /**< Get the header of the iManufacturer string descriptor */ + ENUM_STAGE_CHECK_SHORT_MANU_STR_DESC, /**< Save the bLength of the iManufacturer string descriptor */ + ENUM_STAGE_GET_FULL_MANU_STR_DESC, /**< Get the full iManufacturer string descriptor */ + ENUM_STAGE_CHECK_FULL_MANU_STR_DESC, /**< Check and fill the full iManufacturer string descriptor */ + ENUM_STAGE_GET_SHORT_PROD_STR_DESC, /**< Get the header of the string descriptor at index iProduct */ + ENUM_STAGE_CHECK_SHORT_PROD_STR_DESC, /**< Save the bLength of the iProduct string descriptor */ + ENUM_STAGE_GET_FULL_PROD_STR_DESC, /**< Get the full iProduct string descriptor */ + ENUM_STAGE_CHECK_FULL_PROD_STR_DESC, /**< Check and fill the full iProduct string descriptor */ + ENUM_STAGE_GET_SHORT_SER_STR_DESC, /**< Get the header of the string descriptor at index iSerialNumber */ + ENUM_STAGE_CHECK_SHORT_SER_STR_DESC, /**< Save the bLength of the iSerialNumber string descriptor */ + ENUM_STAGE_GET_FULL_SER_STR_DESC, /**< Get the full iSerialNumber string descriptor */ + ENUM_STAGE_CHECK_FULL_SER_STR_DESC, /**< Check and fill the full iSerialNumber string descriptor */ + // Cleanup + ENUM_STAGE_CLEANUP, /**< Clean up after successful enumeration. Adds enumerated device to USBH */ + ENUM_STAGE_CLEANUP_FAILED, /**< Cleanup failed enumeration. Free device resources */ +} enum_stage_t; + +const char *const enum_stage_strings[] = { + "NONE", + "START", + "GET_SHORT_DEV_DESC", + "CHECK_SHORT_DEV_DESC", + "SECOND_RESET", + "SET_ADDR", + "CHECK_ADDR", + "SET_ADDR_RECOVERY", + "GET_FULL_DEV_DESC", + "CHECK_FULL_DEV_DESC", + "GET_SHORT_CONFIG_DESC", + "CHECK_SHORT_CONFIG_DESC", + "GET_FULL_CONFIG_DESC", + "CHECK_FULL_CONFIG_DESC", + "SET_CONFIG", + "CHECK_CONFIG", + "GET_SHORT_LANGID_TABLE", + "CHECK_SHORT_LANGID_TABLE", + "GET_FULL_LANGID_TABLE", + "CHECK_FULL_LANGID_TABLE", + "GET_SHORT_MANU_STR_DESC", + "CHECK_SHORT_MANU_STR_DESC", + "GET_FULL_MANU_STR_DESC", + "CHECK_FULL_MANU_STR_DESC", + "GET_SHORT_PROD_STR_DESC", + "CHECK_SHORT_PROD_STR_DESC", + "GET_FULL_PROD_STR_DESC", + "CHECK_FULL_PROD_STR_DESC", + "GET_SHORT_SER_STR_DESC", + "CHECK_SHORT_SER_STR_DESC", + "GET_FULL_SER_STR_DESC", + "CHECK_FULL_SER_STR_DESC", + "CLEANUP", + "CLEANUP_FAILED", +}; + +typedef struct { + // Constant + urb_t *urb; /**< URB used for enumeration control transfers. Max data length of ENUM_CTRL_TRANSFER_MAX_DATA_LEN */ + // Initialized at start of a particular enumeration + usb_device_handle_t dev_hdl; /**< Handle of device being enumerated */ + // Updated during enumeration + enum_stage_t stage; /**< Current enumeration stage */ + int expect_num_bytes; /**< Expected number of bytes for IN transfers stages. Set to 0 for OUT transfer */ + uint8_t bMaxPacketSize0; /**< Max packet size of the device's EP0. Read from bMaxPacketSize0 field of device descriptor */ + uint16_t wTotalLength; /**< Total length of device's configuration descriptor. Read from wTotalLength field of config descriptor */ + uint8_t iManufacturer; /**< Index of the Manufacturer string descriptor */ + uint8_t iProduct; /**< Index of the Product string descriptor */ + uint8_t iSerialNumber; /**< Index of the Serial Number string descriptor */ + uint8_t str_desc_bLength; /**< Saved bLength from getting a short string descriptor */ + uint8_t bConfigurationValue; /**< Device's current configuration number */ + uint8_t enum_config_index; /**< Configuration index used during enumeration */ +#ifdef ENABLE_ENUM_FILTER_CALLBACK + usb_host_enum_filter_cb_t enum_filter_cb; /**< Set device configuration callback */ + bool graceful_exit; /**< Exit enumeration by user's request from the callback function */ +#endif // ENABLE_ENUM_FILTER_CALLBACK +} enum_ctrl_t; + +typedef struct { + // Dynamic members require a critical section + struct { + union { + struct { + uint32_t actions: 8; + uint32_t reserved24: 24; + }; + uint32_t val; + } flags; + root_port_state_t root_port_state; + unsigned int port_reqs; + } dynamic; + // Single thread members don't require a critical section so long as they are never accessed from multiple threads + struct { + unsigned int root_dev_uid; // UID of the device connected to root port. 0 if no device connected + enum_ctrl_t enum_ctrl; + } single_thread; + // Constant members do no change after installation thus do not require a critical section + struct { + hcd_port_handle_t root_port_hdl; + usb_proc_req_cb_t proc_req_cb; + void *proc_req_cb_arg; + } constant; +} hub_driver_t; + +static hub_driver_t *p_hub_driver_obj = NULL; +static portMUX_TYPE hub_driver_lock = portMUX_INITIALIZER_UNLOCKED; + +const char *HUB_DRIVER_TAG = "HUB"; + +#define HUB_DRIVER_ENTER_CRITICAL_ISR() portENTER_CRITICAL_ISR(&hub_driver_lock) +#define HUB_DRIVER_EXIT_CRITICAL_ISR() portEXIT_CRITICAL_ISR(&hub_driver_lock) +#define HUB_DRIVER_ENTER_CRITICAL() portENTER_CRITICAL(&hub_driver_lock) +#define HUB_DRIVER_EXIT_CRITICAL() portEXIT_CRITICAL(&hub_driver_lock) +#define HUB_DRIVER_ENTER_CRITICAL_SAFE() portENTER_CRITICAL_SAFE(&hub_driver_lock) +#define HUB_DRIVER_EXIT_CRITICAL_SAFE() portEXIT_CRITICAL_SAFE(&hub_driver_lock) + +#define HUB_DRIVER_CHECK(cond, ret_val) ({ \ + if (!(cond)) { \ + return (ret_val); \ + } \ +}) +#define HUB_DRIVER_CHECK_FROM_CRIT(cond, ret_val) ({ \ + if (!(cond)) { \ + HUB_DRIVER_EXIT_CRITICAL(); \ + return ret_val; \ + } \ +}) + +// ------------------------------------------------- Forward Declare --------------------------------------------------- + +/** + * @brief HCD port callback for the root port + * + * - This callback is called from the context of the HCD, so any event handling should be deferred to hub_process() + * - Under the current HCD implementation, this callback should only be ever be called in an ISR + * - This callback needs to call proc_req_cb to ensure that hub_process() gets a chance to run + * + * @param port_hdl HCD port handle + * @param port_event HCD port event + * @param user_arg Callback argument + * @param in_isr Whether callback is in an ISR context + * @return Whether a yield is required + */ +static bool root_port_callback(hcd_port_handle_t port_hdl, hcd_port_event_t port_event, void *user_arg, bool in_isr); + +/** + * @brief Control transfer callback used for enumeration + * + * @param transfer Transfer object + */ +static void enum_transfer_callback(usb_transfer_t *transfer); + +// ------------------------------------------------- Enum Functions ---------------------------------------------------- + +static bool enum_stage_start(enum_ctrl_t *enum_ctrl) +{ + // Open the newly added device (at address 0) + ESP_ERROR_CHECK(usbh_devs_open(0, &p_hub_driver_obj->single_thread.enum_ctrl.dev_hdl)); + + // Get the speed of the device to set the initial MPS of EP0 + usb_device_info_t dev_info; + ESP_ERROR_CHECK(usbh_dev_get_info(p_hub_driver_obj->single_thread.enum_ctrl.dev_hdl, &dev_info)); + enum_ctrl->bMaxPacketSize0 = (dev_info.speed == USB_SPEED_LOW) ? ENUM_WORST_CASE_MPS_LS : ENUM_WORST_CASE_MPS_FS; + + // Lock the device for enumeration. This allows us call usbh_dev_set_...() functions during enumeration + ESP_ERROR_CHECK(usbh_dev_enum_lock(p_hub_driver_obj->single_thread.enum_ctrl.dev_hdl)); + + // Flag to gracefully exit the enumeration process if requested by the user in the enumeration filter cb +#ifdef ENABLE_ENUM_FILTER_CALLBACK + enum_ctrl->graceful_exit = false; +#endif // ENABLE_ENUM_FILTER_CALLBACK + return true; +} + +static bool enum_stage_second_reset(enum_ctrl_t *enum_ctrl) +{ + if (hcd_port_command(p_hub_driver_obj->constant.root_port_hdl, HCD_PORT_CMD_RESET) != ESP_OK) { + ESP_LOGE(HUB_DRIVER_TAG, "Failed to issue second reset"); + return false; + } + return true; +} + +static void get_string_desc_index_and_langid(enum_ctrl_t *enum_ctrl, uint8_t *index, uint16_t *langid) +{ + switch (enum_ctrl->stage) { + case ENUM_STAGE_GET_SHORT_LANGID_TABLE: + case ENUM_STAGE_GET_FULL_LANGID_TABLE: + *index = 0; // The LANGID table uses an index of 0 + *langid = 0; // Getting the LANGID table itself should use a LANGID of 0 + break; + case ENUM_STAGE_GET_SHORT_MANU_STR_DESC: + case ENUM_STAGE_GET_FULL_MANU_STR_DESC: + *index = enum_ctrl->iManufacturer; + *langid = ENUM_LANGID; // Use the default LANGID + break; + case ENUM_STAGE_GET_SHORT_PROD_STR_DESC: + case ENUM_STAGE_GET_FULL_PROD_STR_DESC: + *index = enum_ctrl->iProduct; + *langid = ENUM_LANGID; // Use the default LANGID + break; + case ENUM_STAGE_GET_SHORT_SER_STR_DESC: + case ENUM_STAGE_GET_FULL_SER_STR_DESC: + *index = enum_ctrl->iSerialNumber; + *langid = ENUM_LANGID; // Use the default LANGID + break; + default: + // Should not occur + abort(); + break; + } +} + +static bool set_config_index(enum_ctrl_t *enum_ctrl, const usb_device_desc_t *device_desc) +{ +#ifdef ENABLE_ENUM_FILTER_CALLBACK + // Callback enabled in the menuncofig, but the callback function was not defined + if (enum_ctrl->enum_filter_cb == NULL) { + enum_ctrl->enum_config_index = ENUM_CONFIG_INDEX_DEFAULT; + return true; + } + + uint8_t enum_config_index; + const bool enum_continue = enum_ctrl->enum_filter_cb(device_desc, &enum_config_index); + + // User's request NOT to enumerate the USB device + if (!enum_continue) { + ESP_LOGW(HUB_DRIVER_TAG, "USB device (PID = 0x%x, VID = 0x%x) will not be enumerated", device_desc->idProduct, device_desc->idVendor); + enum_ctrl->graceful_exit = true; + return false; + } + + // Set configuration descriptor + if ((enum_config_index == 0) || (enum_config_index > device_desc->bNumConfigurations)) { + ESP_LOGW(HUB_DRIVER_TAG, "bConfigurationValue %d provided by user, device will be configured with configuration descriptor 1", enum_config_index); + enum_ctrl->enum_config_index = ENUM_CONFIG_INDEX_DEFAULT; + } else { + enum_ctrl->enum_config_index = enum_config_index - 1; + } +#else // ENABLE_ENUM_FILTER_CALLBACK + enum_ctrl->enum_config_index = ENUM_CONFIG_INDEX_DEFAULT; +#endif // ENABLE_ENUM_FILTER_CALLBACK + + return true; +} + +static bool enum_stage_transfer(enum_ctrl_t *enum_ctrl) +{ + usb_transfer_t *transfer = &enum_ctrl->urb->transfer; + switch (enum_ctrl->stage) { + case ENUM_STAGE_GET_SHORT_DEV_DESC: { + // Initialize a short device descriptor request + USB_SETUP_PACKET_INIT_GET_DEVICE_DESC((usb_setup_packet_t *)transfer->data_buffer); + ((usb_setup_packet_t *)transfer->data_buffer)->wLength = ENUM_SHORT_DESC_REQ_LEN; + transfer->num_bytes = sizeof(usb_setup_packet_t) + usb_round_up_to_mps(ENUM_SHORT_DESC_REQ_LEN, enum_ctrl->bMaxPacketSize0); + // IN data stage should return exactly ENUM_SHORT_DESC_REQ_LEN bytes + enum_ctrl->expect_num_bytes = sizeof(usb_setup_packet_t) + ENUM_SHORT_DESC_REQ_LEN; + break; + } + case ENUM_STAGE_SET_ADDR: { + USB_SETUP_PACKET_INIT_SET_ADDR((usb_setup_packet_t *)transfer->data_buffer, ENUM_DEV_ADDR); + transfer->num_bytes = sizeof(usb_setup_packet_t); // No data stage + enum_ctrl->expect_num_bytes = 0; // OUT transfer. No need to check number of bytes returned + break; + } + case ENUM_STAGE_GET_FULL_DEV_DESC: { + USB_SETUP_PACKET_INIT_GET_DEVICE_DESC((usb_setup_packet_t *)transfer->data_buffer); + transfer->num_bytes = sizeof(usb_setup_packet_t) + usb_round_up_to_mps(sizeof(usb_device_desc_t), enum_ctrl->bMaxPacketSize0); + // IN data stage should return exactly sizeof(usb_device_desc_t) bytes + enum_ctrl->expect_num_bytes = sizeof(usb_setup_packet_t) + sizeof(usb_device_desc_t); + break; + } + case ENUM_STAGE_GET_SHORT_CONFIG_DESC: { + // Get a short config descriptor at index 0 + USB_SETUP_PACKET_INIT_GET_CONFIG_DESC((usb_setup_packet_t *)transfer->data_buffer, enum_ctrl->enum_config_index, ENUM_SHORT_DESC_REQ_LEN); + transfer->num_bytes = sizeof(usb_setup_packet_t) + usb_round_up_to_mps(ENUM_SHORT_DESC_REQ_LEN, enum_ctrl->bMaxPacketSize0); + // IN data stage should return exactly ENUM_SHORT_DESC_REQ_LEN bytes + enum_ctrl->expect_num_bytes = sizeof(usb_setup_packet_t) + ENUM_SHORT_DESC_REQ_LEN; + break; + } + case ENUM_STAGE_GET_FULL_CONFIG_DESC: { + // Get the full configuration descriptor at index 0, requesting its exact length. + USB_SETUP_PACKET_INIT_GET_CONFIG_DESC((usb_setup_packet_t *)transfer->data_buffer, enum_ctrl->enum_config_index, enum_ctrl->wTotalLength); + transfer->num_bytes = sizeof(usb_setup_packet_t) + usb_round_up_to_mps(enum_ctrl->wTotalLength, enum_ctrl->bMaxPacketSize0); + // IN data stage should return exactly wTotalLength bytes + enum_ctrl->expect_num_bytes = sizeof(usb_setup_packet_t) + enum_ctrl->wTotalLength; + break; + } + case ENUM_STAGE_SET_CONFIG: { + USB_SETUP_PACKET_INIT_SET_CONFIG((usb_setup_packet_t *)transfer->data_buffer, enum_ctrl->bConfigurationValue); + transfer->num_bytes = sizeof(usb_setup_packet_t); // No data stage + enum_ctrl->expect_num_bytes = 0; // OUT transfer. No need to check number of bytes returned + break; + } + case ENUM_STAGE_GET_SHORT_LANGID_TABLE: + case ENUM_STAGE_GET_SHORT_MANU_STR_DESC: + case ENUM_STAGE_GET_SHORT_PROD_STR_DESC: + case ENUM_STAGE_GET_SHORT_SER_STR_DESC: { + uint8_t index; + uint16_t langid; + get_string_desc_index_and_langid(enum_ctrl, &index, &langid); + // Get only the header of the string descriptor + USB_SETUP_PACKET_INIT_GET_STR_DESC((usb_setup_packet_t *)transfer->data_buffer, + index, + langid, + sizeof(usb_str_desc_t)); + transfer->num_bytes = sizeof(usb_setup_packet_t) + usb_round_up_to_mps(sizeof(usb_str_desc_t), enum_ctrl->bMaxPacketSize0); + // IN data stage should return exactly sizeof(usb_str_desc_t) bytes + enum_ctrl->expect_num_bytes = sizeof(usb_setup_packet_t) + sizeof(usb_str_desc_t); + break; + } + case ENUM_STAGE_GET_FULL_LANGID_TABLE: + case ENUM_STAGE_GET_FULL_MANU_STR_DESC: + case ENUM_STAGE_GET_FULL_PROD_STR_DESC: + case ENUM_STAGE_GET_FULL_SER_STR_DESC: { + uint8_t index; + uint16_t langid; + get_string_desc_index_and_langid(enum_ctrl, &index, &langid); + // Get the full string descriptor at a particular index, requesting the descriptors exact length + USB_SETUP_PACKET_INIT_GET_STR_DESC((usb_setup_packet_t *)transfer->data_buffer, + index, + langid, + enum_ctrl->str_desc_bLength); + transfer->num_bytes = sizeof(usb_setup_packet_t) + usb_round_up_to_mps(enum_ctrl->str_desc_bLength, enum_ctrl->bMaxPacketSize0); + // IN data stage should return exactly str_desc_bLength bytes + enum_ctrl->expect_num_bytes = sizeof(usb_setup_packet_t) + enum_ctrl->str_desc_bLength; + break; + } + default: // Should never occur + abort(); + break; + } + if (usbh_dev_submit_ctrl_urb(enum_ctrl->dev_hdl, enum_ctrl->urb) != ESP_OK) { + ESP_LOGE(HUB_DRIVER_TAG, "Failed to submit: %s", enum_stage_strings[enum_ctrl->stage]); + return false; + } + return true; +} + +static bool enum_stage_wait(enum_ctrl_t *enum_ctrl) +{ + switch (enum_ctrl->stage) { + case ENUM_STAGE_SET_ADDR_RECOVERY: { + vTaskDelay(pdMS_TO_TICKS(SET_ADDR_RECOVERY_INTERVAL_MS)); // Need a short delay before device is ready. Todo: IDF-7007 + return true; + } + + default: // Should never occur + abort(); + break; + } + + return false; +} + +static bool enum_stage_transfer_check(enum_ctrl_t *enum_ctrl) +{ + // Check transfer status + usb_transfer_t *transfer = &enum_ctrl->urb->transfer; + if (transfer->status != USB_TRANSFER_STATUS_COMPLETED) { + ESP_LOGE(HUB_DRIVER_TAG, "Bad transfer status %d: %s", transfer->status, enum_stage_strings[enum_ctrl->stage]); + return false; + } + // Check IN transfer returned the expected correct number of bytes + if (enum_ctrl->expect_num_bytes != 0 && transfer->actual_num_bytes != enum_ctrl->expect_num_bytes) { + if (transfer->actual_num_bytes > enum_ctrl->expect_num_bytes) { + // The device returned more bytes than requested. + // This violates the USB specs chapter 9.3.5, but we can continue + ESP_LOGW(HUB_DRIVER_TAG, "Incorrect number of bytes returned %d: %s", transfer->actual_num_bytes, enum_stage_strings[enum_ctrl->stage]); + } else { + // The device returned less bytes than requested. We cannot continue. + ESP_LOGE(HUB_DRIVER_TAG, "Incorrect number of bytes returned %d: %s", transfer->actual_num_bytes, enum_stage_strings[enum_ctrl->stage]); + return false; + } + } + + // Stage specific checks and updates + bool ret; + switch (enum_ctrl->stage) { + case ENUM_STAGE_CHECK_SHORT_DEV_DESC: { + const usb_device_desc_t *device_desc = (usb_device_desc_t *)(transfer->data_buffer + sizeof(usb_setup_packet_t)); + // Check if the returned descriptor is corrupted + if (device_desc->bDescriptorType != USB_B_DESCRIPTOR_TYPE_DEVICE) { + ESP_LOGE(HUB_DRIVER_TAG, "Short dev desc corrupt"); + ret = false; + break; + } + // Update and save the MPS of the EP0 + if (usbh_dev_set_ep0_mps(enum_ctrl->dev_hdl, device_desc->bMaxPacketSize0) != ESP_OK) { + ESP_LOGE(HUB_DRIVER_TAG, "Failed to update MPS"); + ret = false; + break; + } + // Save the actual MPS of EP0 + enum_ctrl->bMaxPacketSize0 = device_desc->bMaxPacketSize0; + ret = true; + break; + } + case ENUM_STAGE_CHECK_ADDR: { + // Update the device's address + ESP_ERROR_CHECK(usbh_dev_set_addr(enum_ctrl->dev_hdl, ENUM_DEV_ADDR)); + ret = true; + break; + } + case ENUM_STAGE_CHECK_FULL_DEV_DESC: { + // Set the device's descriptor + const usb_device_desc_t *device_desc = (const usb_device_desc_t *)(transfer->data_buffer + sizeof(usb_setup_packet_t)); + ESP_ERROR_CHECK(usbh_dev_set_desc(enum_ctrl->dev_hdl, device_desc)); + enum_ctrl->iManufacturer = device_desc->iManufacturer; + enum_ctrl->iProduct = device_desc->iProduct; + enum_ctrl->iSerialNumber = device_desc->iSerialNumber; + ret = set_config_index(enum_ctrl, device_desc); + break; + } + case ENUM_STAGE_CHECK_SHORT_CONFIG_DESC: { + const usb_config_desc_t *config_desc = (usb_config_desc_t *)(transfer->data_buffer + sizeof(usb_setup_packet_t)); + // Check if the returned descriptor is corrupted + if (config_desc->bDescriptorType != USB_B_DESCRIPTOR_TYPE_CONFIGURATION) { + ESP_LOGE(HUB_DRIVER_TAG, "Short config desc corrupt"); + ret = false; + break; + } +#if (ENUM_CTRL_TRANSFER_MAX_DATA_LEN < UINT16_MAX) // Suppress -Wtype-limits warning due to uint16_t wTotalLength + // Check if the descriptor is too long to be supported + if (config_desc->wTotalLength > ENUM_CTRL_TRANSFER_MAX_DATA_LEN) { + ESP_LOGE(HUB_DRIVER_TAG, "Configuration descriptor larger than control transfer max length"); + ret = false; + break; + } +#endif + // Save the configuration descriptors full length + enum_ctrl->wTotalLength = config_desc->wTotalLength; + ret = true; + break; + } + case ENUM_STAGE_CHECK_FULL_CONFIG_DESC: { + // Set the device's configuration descriptor + const usb_config_desc_t *config_desc = (usb_config_desc_t *)(transfer->data_buffer + sizeof(usb_setup_packet_t)); + enum_ctrl->bConfigurationValue = config_desc->bConfigurationValue; + ESP_ERROR_CHECK(usbh_dev_set_config_desc(enum_ctrl->dev_hdl, config_desc)); + ret = true; + break; + } + case ENUM_STAGE_CHECK_CONFIG: { + ret = true; + // Nothing to do + break; + } + case ENUM_STAGE_CHECK_SHORT_LANGID_TABLE: + case ENUM_STAGE_CHECK_SHORT_MANU_STR_DESC: + case ENUM_STAGE_CHECK_SHORT_PROD_STR_DESC: + case ENUM_STAGE_CHECK_SHORT_SER_STR_DESC: { + const usb_str_desc_t *str_desc = (usb_str_desc_t *)(transfer->data_buffer + sizeof(usb_setup_packet_t)); + // Check if the returned descriptor is supported or corrupted + if (str_desc->bDescriptorType == 0) { + ESP_LOGW(HUB_DRIVER_TAG, "String desc not supported"); + ret = false; + break; + } else if (str_desc->bDescriptorType != USB_B_DESCRIPTOR_TYPE_STRING) { + ESP_LOGE(HUB_DRIVER_TAG, "Full string desc corrupt"); + ret = false; + break; + } +#if (ENUM_CTRL_TRANSFER_MAX_DATA_LEN < UINT8_MAX) // Suppress -Wtype-limits warning due to uint8_t bLength + // Check if the descriptor is too long to be supported + if (str_desc->bLength > (uint32_t)ENUM_CTRL_TRANSFER_MAX_DATA_LEN) { + ESP_LOGE(HUB_DRIVER_TAG, "String descriptor larger than control transfer max length"); + ret = false; + break; + } +#endif + // Save the descriptors full length + enum_ctrl->str_desc_bLength = str_desc->bLength; + ret = true; + break; + } + case ENUM_STAGE_CHECK_FULL_LANGID_TABLE: + case ENUM_STAGE_CHECK_FULL_MANU_STR_DESC: + case ENUM_STAGE_CHECK_FULL_PROD_STR_DESC: + case ENUM_STAGE_CHECK_FULL_SER_STR_DESC: { + const usb_str_desc_t *str_desc = (usb_str_desc_t *)(transfer->data_buffer + sizeof(usb_setup_packet_t)); + // Check if the returned descriptor is supported or corrupted + if (str_desc->bDescriptorType == 0) { + ESP_LOGW(HUB_DRIVER_TAG, "String desc not supported"); + ret = false; + break; + } else if (str_desc->bDescriptorType != USB_B_DESCRIPTOR_TYPE_STRING) { + ESP_LOGE(HUB_DRIVER_TAG, "Full string desc corrupt"); + ret = false; + break; + } + if (enum_ctrl->stage == ENUM_STAGE_CHECK_FULL_LANGID_TABLE) { + // Scan the LANGID table for our target LANGID + bool target_langid_found = false; + int langid_table_num_entries = (str_desc->bLength - sizeof(usb_str_desc_t)) / 2; // Each LANGID is 2 bytes + for (int i = 0; i < langid_table_num_entries; i++) { // Each LANGID is 2 bytes + if (str_desc->wData[i] == ENUM_LANGID) { + target_langid_found = true; + break; + } + } + if (!target_langid_found) { + ESP_LOGE(HUB_DRIVER_TAG, "LANGID 0x%x not found", ENUM_LANGID); + } + ret = target_langid_found; + break; + } else { + // Fill the string descriptor into the device object + int select; + if (enum_ctrl->stage == ENUM_STAGE_CHECK_FULL_MANU_STR_DESC) { + select = 0; + } else if (enum_ctrl->stage == ENUM_STAGE_CHECK_FULL_PROD_STR_DESC) { + select = 1; + } else { // ENUM_STAGE_CHECK_FULL_PROD_STR_DESC + select = 2; + } + ESP_ERROR_CHECK(usbh_dev_set_str_desc(enum_ctrl->dev_hdl, str_desc, select)); + ret = true; + break; + } + } + default: // Should never occur + ret = false; + abort(); + break; + } + return ret; +} + +static void enum_stage_cleanup(enum_ctrl_t *enum_ctrl) +{ + // Unlock the device as we are done with the enumeration + ESP_ERROR_CHECK(usbh_dev_enum_unlock(enum_ctrl->dev_hdl)); + // Propagate a new device event + ESP_ERROR_CHECK(usbh_devs_new_dev_event(enum_ctrl->dev_hdl)); + // We are done with using the device. Close it. + ESP_ERROR_CHECK(usbh_devs_close(enum_ctrl->dev_hdl)); + // Clear values in enum_ctrl + enum_ctrl->dev_hdl = NULL; +} + +static void enum_stage_cleanup_failed(enum_ctrl_t *enum_ctrl) +{ + if (enum_ctrl->dev_hdl) { + // Close the device and unlock it as we done with enumeration + ESP_ERROR_CHECK(usbh_dev_enum_unlock(enum_ctrl->dev_hdl)); + ESP_ERROR_CHECK(usbh_devs_close(enum_ctrl->dev_hdl)); + // We allow this to fail in case the device object was already freed + usbh_devs_remove(ENUM_DEV_UID); + } + // Clear values in enum_ctrl + enum_ctrl->dev_hdl = NULL; +} + +static enum_stage_t get_next_stage(enum_stage_t old_stage, enum_ctrl_t *enum_ctrl) +{ + enum_stage_t new_stage = old_stage + 1; + // Skip the GET_DESCRIPTOR string type corresponding stages if a particular index is 0. + while (((new_stage == ENUM_STAGE_GET_SHORT_MANU_STR_DESC || + new_stage == ENUM_STAGE_CHECK_SHORT_MANU_STR_DESC || + new_stage == ENUM_STAGE_GET_FULL_MANU_STR_DESC || + new_stage == ENUM_STAGE_CHECK_FULL_MANU_STR_DESC) && enum_ctrl->iManufacturer == 0) || + ((new_stage == ENUM_STAGE_GET_SHORT_PROD_STR_DESC || + new_stage == ENUM_STAGE_CHECK_SHORT_PROD_STR_DESC || + new_stage == ENUM_STAGE_GET_FULL_PROD_STR_DESC || + new_stage == ENUM_STAGE_CHECK_FULL_PROD_STR_DESC) && enum_ctrl->iProduct == 0) || + ((new_stage == ENUM_STAGE_GET_SHORT_SER_STR_DESC || + new_stage == ENUM_STAGE_CHECK_SHORT_SER_STR_DESC || + new_stage == ENUM_STAGE_GET_FULL_SER_STR_DESC || + new_stage == ENUM_STAGE_CHECK_FULL_SER_STR_DESC) && enum_ctrl->iSerialNumber == 0)) { + new_stage++; + } + return new_stage; +} + +static void enum_set_next_stage(enum_ctrl_t *enum_ctrl, bool last_stage_pass) +{ + // Set next stage + if (last_stage_pass) { + if (enum_ctrl->stage != ENUM_STAGE_NONE && + enum_ctrl->stage != ENUM_STAGE_CLEANUP && + enum_ctrl->stage != ENUM_STAGE_CLEANUP_FAILED) { + enum_ctrl->stage = get_next_stage(enum_ctrl->stage, enum_ctrl); + } else { + enum_ctrl->stage = ENUM_STAGE_NONE; + } + } else { + switch (enum_ctrl->stage) { + case ENUM_STAGE_START: + // Stage failed but clean up not required + enum_ctrl->stage = ENUM_STAGE_NONE; + break; + case ENUM_STAGE_GET_SHORT_LANGID_TABLE: + case ENUM_STAGE_CHECK_SHORT_LANGID_TABLE: + case ENUM_STAGE_GET_FULL_LANGID_TABLE: + case ENUM_STAGE_CHECK_FULL_LANGID_TABLE: + case ENUM_STAGE_GET_SHORT_MANU_STR_DESC: + case ENUM_STAGE_CHECK_SHORT_MANU_STR_DESC: + case ENUM_STAGE_GET_FULL_MANU_STR_DESC: + case ENUM_STAGE_CHECK_FULL_MANU_STR_DESC: + case ENUM_STAGE_GET_SHORT_PROD_STR_DESC: + case ENUM_STAGE_CHECK_SHORT_PROD_STR_DESC: + case ENUM_STAGE_GET_FULL_PROD_STR_DESC: + case ENUM_STAGE_CHECK_FULL_PROD_STR_DESC: + case ENUM_STAGE_GET_SHORT_SER_STR_DESC: + case ENUM_STAGE_CHECK_SHORT_SER_STR_DESC: + case ENUM_STAGE_GET_FULL_SER_STR_DESC: + case ENUM_STAGE_CHECK_FULL_SER_STR_DESC: + // String descriptor stages are allow to fail. We just don't fetch them and treat enumeration as successful + enum_ctrl->stage = ENUM_STAGE_CLEANUP; + break; + default: + // Enumeration failed. Go to failure clean up + enum_ctrl->stage = ENUM_STAGE_CLEANUP_FAILED; + break; + } + } + + // These stages are not waiting for a callback, so we need to re-trigger the enum event + bool re_trigger; + switch (enum_ctrl->stage) { + case ENUM_STAGE_GET_SHORT_DEV_DESC: + case ENUM_STAGE_SECOND_RESET: + case ENUM_STAGE_SET_ADDR: + case ENUM_STAGE_SET_ADDR_RECOVERY: + case ENUM_STAGE_GET_FULL_DEV_DESC: + case ENUM_STAGE_GET_SHORT_CONFIG_DESC: + case ENUM_STAGE_GET_FULL_CONFIG_DESC: + case ENUM_STAGE_SET_CONFIG: + case ENUM_STAGE_GET_SHORT_LANGID_TABLE: + case ENUM_STAGE_GET_FULL_LANGID_TABLE: + case ENUM_STAGE_GET_SHORT_MANU_STR_DESC: + case ENUM_STAGE_GET_FULL_MANU_STR_DESC: + case ENUM_STAGE_GET_SHORT_PROD_STR_DESC: + case ENUM_STAGE_GET_FULL_PROD_STR_DESC: + case ENUM_STAGE_GET_SHORT_SER_STR_DESC: + case ENUM_STAGE_GET_FULL_SER_STR_DESC: + case ENUM_STAGE_CLEANUP: + case ENUM_STAGE_CLEANUP_FAILED: + re_trigger = true; + break; + default: + re_trigger = false; + break; + } + if (re_trigger) { + HUB_DRIVER_ENTER_CRITICAL(); + p_hub_driver_obj->dynamic.flags.actions |= HUB_DRIVER_FLAG_ACTION_ENUM_EVENT; + HUB_DRIVER_EXIT_CRITICAL(); + } +} + +// ------------------------------------------------- Event Handling ---------------------------------------------------- + +// ---------------------- Callbacks ------------------------ + +static bool root_port_callback(hcd_port_handle_t port_hdl, hcd_port_event_t port_event, void *user_arg, bool in_isr) +{ + HUB_DRIVER_ENTER_CRITICAL_SAFE(); + p_hub_driver_obj->dynamic.flags.actions |= HUB_DRIVER_FLAG_ACTION_ROOT_EVENT; + HUB_DRIVER_EXIT_CRITICAL_SAFE(); + assert(in_isr); // Currently, this callback should only ever be called from an ISR context + return p_hub_driver_obj->constant.proc_req_cb(USB_PROC_REQ_SOURCE_HUB, in_isr, p_hub_driver_obj->constant.proc_req_cb_arg); +} + +static void enum_transfer_callback(usb_transfer_t *transfer) +{ + // We simply trigger a processing request to handle the completed enumeration control transfer + HUB_DRIVER_ENTER_CRITICAL_SAFE(); + p_hub_driver_obj->dynamic.flags.actions |= HUB_DRIVER_FLAG_ACTION_ENUM_EVENT; + HUB_DRIVER_EXIT_CRITICAL_SAFE(); + p_hub_driver_obj->constant.proc_req_cb(USB_PROC_REQ_SOURCE_HUB, false, p_hub_driver_obj->constant.proc_req_cb_arg); +} + +// ---------------------- Handlers ------------------------- + +static void root_port_handle_events(hcd_port_handle_t root_port_hdl) +{ + hcd_port_event_t port_event = hcd_port_handle_event(root_port_hdl); + switch (port_event) { + case HCD_PORT_EVENT_NONE: + // Nothing to do + break; + case HCD_PORT_EVENT_CONNECTION: { + if (hcd_port_command(root_port_hdl, HCD_PORT_CMD_RESET) != ESP_OK) { + ESP_LOGE(HUB_DRIVER_TAG, "Root port reset failed"); + goto reset_err; + } + ESP_LOGD(HUB_DRIVER_TAG, "Root port reset"); + usb_speed_t speed; + if (hcd_port_get_speed(p_hub_driver_obj->constant.root_port_hdl, &speed) != ESP_OK) { + goto new_dev_err; + } + // Allocate a new device. We use a fixed ENUM_DEV_UID for now since we only support a single device + if (usbh_devs_add(ENUM_DEV_UID, speed, p_hub_driver_obj->constant.root_port_hdl) != ESP_OK) { + ESP_LOGE(HUB_DRIVER_TAG, "Failed to add device"); + goto new_dev_err; + } + p_hub_driver_obj->single_thread.root_dev_uid = ENUM_DEV_UID; + // Start enumeration + HUB_DRIVER_ENTER_CRITICAL(); + p_hub_driver_obj->dynamic.flags.actions |= HUB_DRIVER_FLAG_ACTION_ENUM_EVENT; + p_hub_driver_obj->dynamic.root_port_state = ROOT_PORT_STATE_ENABLED; + HUB_DRIVER_EXIT_CRITICAL(); + p_hub_driver_obj->single_thread.enum_ctrl.stage = ENUM_STAGE_START; + break; +new_dev_err: + // We allow this to fail in case a disconnect/port error happens while disabling. + hcd_port_command(p_hub_driver_obj->constant.root_port_hdl, HCD_PORT_CMD_DISABLE); +reset_err: + break; + } + case HCD_PORT_EVENT_DISCONNECTION: + case HCD_PORT_EVENT_ERROR: + case HCD_PORT_EVENT_OVERCURRENT: { + bool pass_event_to_usbh = false; + HUB_DRIVER_ENTER_CRITICAL(); + switch (p_hub_driver_obj->dynamic.root_port_state) { + case ROOT_PORT_STATE_POWERED: // This occurred before enumeration + case ROOT_PORT_STATE_DISABLED: // This occurred after the device has already been disabled + // Therefore, there's no device object to clean up, and we can go straight to port recovery + p_hub_driver_obj->dynamic.port_reqs |= PORT_REQ_RECOVER; + p_hub_driver_obj->dynamic.flags.actions |= HUB_DRIVER_FLAG_ACTION_PORT_REQ; + break; + case ROOT_PORT_STATE_ENABLED: + // There is an enabled (active) device. We need to indicate to USBH that the device is gone + pass_event_to_usbh = true; + break; + default: + abort(); // Should never occur + break; + } + p_hub_driver_obj->dynamic.root_port_state = ROOT_PORT_STATE_RECOVERY; + HUB_DRIVER_EXIT_CRITICAL(); + if (pass_event_to_usbh) { + // The port must have a device object + assert(p_hub_driver_obj->single_thread.root_dev_uid != 0); + // We allow this to fail in case the device object was already freed + usbh_devs_remove(p_hub_driver_obj->single_thread.root_dev_uid); + } + break; + } + default: + abort(); // Should never occur + break; + } +} + +static void root_port_req(hcd_port_handle_t root_port_hdl) +{ + unsigned int port_reqs; + + HUB_DRIVER_ENTER_CRITICAL(); + port_reqs = p_hub_driver_obj->dynamic.port_reqs; + p_hub_driver_obj->dynamic.port_reqs = 0; + HUB_DRIVER_EXIT_CRITICAL(); + + if (port_reqs & PORT_REQ_DISABLE) { + ESP_LOGD(HUB_DRIVER_TAG, "Disabling root port"); + // We allow this to fail in case a disconnect/port error happens while disabling. + hcd_port_command(p_hub_driver_obj->constant.root_port_hdl, HCD_PORT_CMD_DISABLE); + } + if (port_reqs & PORT_REQ_RECOVER) { + ESP_LOGD(HUB_DRIVER_TAG, "Recovering root port"); + ESP_ERROR_CHECK(hcd_port_recover(p_hub_driver_obj->constant.root_port_hdl)); + ESP_ERROR_CHECK(hcd_port_command(p_hub_driver_obj->constant.root_port_hdl, HCD_PORT_CMD_POWER_ON)); + HUB_DRIVER_ENTER_CRITICAL(); + p_hub_driver_obj->dynamic.root_port_state = ROOT_PORT_STATE_POWERED; + HUB_DRIVER_EXIT_CRITICAL(); + } +} + +static void enum_handle_events(void) +{ + bool stage_pass; + enum_ctrl_t *enum_ctrl = &p_hub_driver_obj->single_thread.enum_ctrl; + switch (enum_ctrl->stage) { + case ENUM_STAGE_START: + stage_pass = enum_stage_start(enum_ctrl); + break; + case ENUM_STAGE_SECOND_RESET: + stage_pass = enum_stage_second_reset(enum_ctrl); + break; + // Transfer submission stages + case ENUM_STAGE_GET_SHORT_DEV_DESC: + case ENUM_STAGE_SET_ADDR: + case ENUM_STAGE_GET_FULL_DEV_DESC: + case ENUM_STAGE_GET_SHORT_CONFIG_DESC: + case ENUM_STAGE_GET_FULL_CONFIG_DESC: + case ENUM_STAGE_SET_CONFIG: + case ENUM_STAGE_GET_SHORT_LANGID_TABLE: + case ENUM_STAGE_GET_FULL_LANGID_TABLE: + case ENUM_STAGE_GET_SHORT_MANU_STR_DESC: + case ENUM_STAGE_GET_FULL_MANU_STR_DESC: + case ENUM_STAGE_GET_SHORT_PROD_STR_DESC: + case ENUM_STAGE_GET_FULL_PROD_STR_DESC: + case ENUM_STAGE_GET_SHORT_SER_STR_DESC: + case ENUM_STAGE_GET_FULL_SER_STR_DESC: + stage_pass = enum_stage_transfer(enum_ctrl); + break; + // Recovery interval + case ENUM_STAGE_SET_ADDR_RECOVERY: + stage_pass = enum_stage_wait(enum_ctrl); + break; + // Transfer check stages + case ENUM_STAGE_CHECK_SHORT_DEV_DESC: + case ENUM_STAGE_CHECK_ADDR: + case ENUM_STAGE_CHECK_FULL_DEV_DESC: + case ENUM_STAGE_CHECK_SHORT_CONFIG_DESC: + case ENUM_STAGE_CHECK_FULL_CONFIG_DESC: + case ENUM_STAGE_CHECK_CONFIG: + case ENUM_STAGE_CHECK_SHORT_LANGID_TABLE: + case ENUM_STAGE_CHECK_FULL_LANGID_TABLE: + case ENUM_STAGE_CHECK_SHORT_MANU_STR_DESC: + case ENUM_STAGE_CHECK_FULL_MANU_STR_DESC: + case ENUM_STAGE_CHECK_SHORT_PROD_STR_DESC: + case ENUM_STAGE_CHECK_FULL_PROD_STR_DESC: + case ENUM_STAGE_CHECK_SHORT_SER_STR_DESC: + case ENUM_STAGE_CHECK_FULL_SER_STR_DESC: + stage_pass = enum_stage_transfer_check(enum_ctrl); + break; + case ENUM_STAGE_CLEANUP: + enum_stage_cleanup(enum_ctrl); + stage_pass = true; + break; + case ENUM_STAGE_CLEANUP_FAILED: + enum_stage_cleanup_failed(enum_ctrl); + stage_pass = true; + break; + default: + stage_pass = true; + break; + } + if (stage_pass) { + ESP_LOGD(HUB_DRIVER_TAG, "Stage done: %s", enum_stage_strings[enum_ctrl->stage]); + } else { +#ifdef ENABLE_ENUM_FILTER_CALLBACK + if (!enum_ctrl->graceful_exit) { + ESP_LOGE(HUB_DRIVER_TAG, "Stage failed: %s", enum_stage_strings[enum_ctrl->stage]); + } else { + ESP_LOGD(HUB_DRIVER_TAG, "Stage done: %s", enum_stage_strings[enum_ctrl->stage]); + } +#else // ENABLE_ENUM_FILTER_CALLBACK + ESP_LOGE(HUB_DRIVER_TAG, "Stage failed: %s", enum_stage_strings[enum_ctrl->stage]); +#endif // ENABLE_ENUM_FILTER_CALLBACK + } + enum_set_next_stage(enum_ctrl, stage_pass); +} + +// ---------------------------------------------- Hub Driver Functions ------------------------------------------------- + +esp_err_t hub_install(hub_config_t *hub_config, void **client_ret) +{ + HUB_DRIVER_ENTER_CRITICAL(); + HUB_DRIVER_CHECK_FROM_CRIT(p_hub_driver_obj == NULL, ESP_ERR_INVALID_STATE); + HUB_DRIVER_EXIT_CRITICAL(); + esp_err_t ret; + + // Allocate Hub driver object + hub_driver_t *hub_driver_obj = heap_caps_calloc(1, sizeof(hub_driver_t), MALLOC_CAP_DEFAULT); + urb_t *enum_urb = urb_alloc(sizeof(usb_setup_packet_t) + ENUM_CTRL_TRANSFER_MAX_DATA_LEN, 0); + if (hub_driver_obj == NULL || enum_urb == NULL) { + return ESP_ERR_NO_MEM; + } + enum_urb->usb_host_client = (void *)hub_driver_obj; + enum_urb->transfer.callback = enum_transfer_callback; + + // Install HCD port + hcd_port_config_t port_config = { + .fifo_bias = HUB_ROOT_HCD_PORT_FIFO_BIAS, + .callback = root_port_callback, + .callback_arg = NULL, + .context = NULL, + }; + hcd_port_handle_t port_hdl; + ret = hcd_port_init(HUB_ROOT_PORT_NUM, &port_config, &port_hdl); + if (ret != ESP_OK) { + goto err; + } + + // Initialize Hub driver object + hub_driver_obj->single_thread.enum_ctrl.stage = ENUM_STAGE_NONE; + hub_driver_obj->single_thread.enum_ctrl.urb = enum_urb; +#ifdef ENABLE_ENUM_FILTER_CALLBACK + hub_driver_obj->single_thread.enum_ctrl.enum_filter_cb = hub_config->enum_filter_cb; +#endif // ENABLE_ENUM_FILTER_CALLBACK + hub_driver_obj->constant.root_port_hdl = port_hdl; + hub_driver_obj->constant.proc_req_cb = hub_config->proc_req_cb; + hub_driver_obj->constant.proc_req_cb_arg = hub_config->proc_req_cb_arg; + + HUB_DRIVER_ENTER_CRITICAL(); + hub_driver_obj->dynamic.root_port_state = ROOT_PORT_STATE_NOT_POWERED; + if (p_hub_driver_obj != NULL) { + HUB_DRIVER_EXIT_CRITICAL(); + ret = ESP_ERR_INVALID_STATE; + goto assign_err; + } + p_hub_driver_obj = hub_driver_obj; + HUB_DRIVER_EXIT_CRITICAL(); + + // Write-back client_ret pointer + *client_ret = (void *)hub_driver_obj; + ret = ESP_OK; + + return ret; + +assign_err: + ESP_ERROR_CHECK(hcd_port_deinit(port_hdl)); +err: + urb_free(enum_urb); + heap_caps_free(hub_driver_obj); + return ret; +} + +esp_err_t hub_uninstall(void) +{ + HUB_DRIVER_ENTER_CRITICAL(); + HUB_DRIVER_CHECK_FROM_CRIT(p_hub_driver_obj != NULL, ESP_ERR_INVALID_STATE); + HUB_DRIVER_CHECK_FROM_CRIT(p_hub_driver_obj->dynamic.root_port_state == ROOT_PORT_STATE_NOT_POWERED, ESP_ERR_INVALID_STATE); + hub_driver_t *hub_driver_obj = p_hub_driver_obj; + p_hub_driver_obj = NULL; + HUB_DRIVER_EXIT_CRITICAL(); + + ESP_ERROR_CHECK(hcd_port_deinit(hub_driver_obj->constant.root_port_hdl)); + // Free Hub driver resources + urb_free(hub_driver_obj->single_thread.enum_ctrl.urb); + heap_caps_free(hub_driver_obj); + return ESP_OK; +} + +esp_err_t hub_root_start(void) +{ + HUB_DRIVER_ENTER_CRITICAL(); + HUB_DRIVER_CHECK_FROM_CRIT(p_hub_driver_obj != NULL, ESP_ERR_INVALID_STATE); + HUB_DRIVER_CHECK_FROM_CRIT(p_hub_driver_obj->dynamic.root_port_state == ROOT_PORT_STATE_NOT_POWERED, ESP_ERR_INVALID_STATE); + HUB_DRIVER_EXIT_CRITICAL(); + // Power ON the root port + esp_err_t ret; + ret = hcd_port_command(p_hub_driver_obj->constant.root_port_hdl, HCD_PORT_CMD_POWER_ON); + if (ret == ESP_OK) { + HUB_DRIVER_ENTER_CRITICAL(); + p_hub_driver_obj->dynamic.root_port_state = ROOT_PORT_STATE_POWERED; + HUB_DRIVER_EXIT_CRITICAL(); + } + return ret; +} + +esp_err_t hub_root_stop(void) +{ + HUB_DRIVER_ENTER_CRITICAL(); + HUB_DRIVER_CHECK_FROM_CRIT(p_hub_driver_obj != NULL, ESP_ERR_INVALID_STATE); + HUB_DRIVER_CHECK_FROM_CRIT(p_hub_driver_obj->dynamic.root_port_state != ROOT_PORT_STATE_NOT_POWERED, ESP_ERR_INVALID_STATE); + HUB_DRIVER_EXIT_CRITICAL(); + esp_err_t ret; + ret = hcd_port_command(p_hub_driver_obj->constant.root_port_hdl, HCD_PORT_CMD_POWER_OFF); + if (ret == ESP_OK) { + HUB_DRIVER_ENTER_CRITICAL(); + p_hub_driver_obj->dynamic.root_port_state = ROOT_PORT_STATE_NOT_POWERED; + HUB_DRIVER_EXIT_CRITICAL(); + } + return ret; +} + +esp_err_t hub_port_recycle(unsigned int dev_uid) +{ + if (dev_uid == p_hub_driver_obj->single_thread.root_dev_uid) { + // Device is free, we can now request its port be recycled + hcd_port_state_t port_state = hcd_port_get_state(p_hub_driver_obj->constant.root_port_hdl); + p_hub_driver_obj->single_thread.root_dev_uid = 0; + HUB_DRIVER_ENTER_CRITICAL(); + // How the port is recycled will depend on the port's state + switch (port_state) { + case HCD_PORT_STATE_ENABLED: + p_hub_driver_obj->dynamic.port_reqs |= PORT_REQ_DISABLE; + break; + case HCD_PORT_STATE_RECOVERY: + p_hub_driver_obj->dynamic.port_reqs |= PORT_REQ_RECOVER; + break; + default: + abort(); // Should never occur + break; + } + p_hub_driver_obj->dynamic.flags.actions |= HUB_DRIVER_FLAG_ACTION_PORT_REQ; + HUB_DRIVER_EXIT_CRITICAL(); + + p_hub_driver_obj->constant.proc_req_cb(USB_PROC_REQ_SOURCE_HUB, false, p_hub_driver_obj->constant.proc_req_cb_arg); + } + + return ESP_OK; +} + +esp_err_t hub_process(void) +{ + HUB_DRIVER_ENTER_CRITICAL(); + uint32_t action_flags = p_hub_driver_obj->dynamic.flags.actions; + p_hub_driver_obj->dynamic.flags.actions = 0; + HUB_DRIVER_EXIT_CRITICAL(); + + while (action_flags) { + if (action_flags & HUB_DRIVER_FLAG_ACTION_ROOT_EVENT) { + root_port_handle_events(p_hub_driver_obj->constant.root_port_hdl); + } + if (action_flags & HUB_DRIVER_FLAG_ACTION_PORT_REQ) { + root_port_req(p_hub_driver_obj->constant.root_port_hdl); + } + if (action_flags & HUB_DRIVER_FLAG_ACTION_ENUM_EVENT) { + enum_handle_events(); + } + + HUB_DRIVER_ENTER_CRITICAL(); + action_flags = p_hub_driver_obj->dynamic.flags.actions; + p_hub_driver_obj->dynamic.flags.actions = 0; + HUB_DRIVER_EXIT_CRITICAL(); + } + return ESP_OK; +} diff --git a/components/usb/include/esp_private/usb_phy.h b/components/usb/include/esp_private/usb_phy.h new file mode 100644 index 0000000000..7848e7cdba --- /dev/null +++ b/components/usb/include/esp_private/usb_phy.h @@ -0,0 +1,180 @@ +/* + * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "soc/soc_caps.h" +#include "hal/usb_phy_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialization for usb_phy_otg_io_conf_t: Self-powered device + */ +#define USB_PHY_SELF_POWERED_DEVICE(vbus_monitor_io) \ + { \ + .iddig_io_num = -1, \ + .avalid_io_num = -1, \ + .vbusvalid_io_num = -1, \ + .idpullup_io_num = -1, \ + .dppulldown_io_num = -1, \ + .dmpulldown_io_num = -1, \ + .drvvbus_io_num = -1, \ + .bvalid_io_num = vbus_monitor_io, \ + .sessend_io_num = -1, \ + .chrgvbus_io_num = -1, \ + .dischrgvbus_io_num = -1, \ + }; + +/** + * @brief USB PHY status + */ +typedef enum { + USB_PHY_STATUS_FREE, /**< PHY is not being used */ + USB_PHY_STATUS_IN_USE, /**< PHY is in use */ +} usb_phy_status_t; + +/** + * @brief USB PHY available actions + */ +typedef enum { + USB_PHY_ACTION_HOST_ALLOW_CONN, /**< Enable physical connection when operating as an OTG Host */ + USB_PHY_ACTION_HOST_FORCE_DISCONN, /**< Disable physical connection when operating as an OTG Host */ + USB_PHY_ACTION_MAX, +} usb_phy_action_t; + +/** + * @brief USB external PHY IO pins configuration structure + */ +typedef struct { + int vp_io_num; /**< GPIO pin to USB_EXTPHY_VP_IDX */ + int vm_io_num; /**< GPIO pin to USB_EXTPHY_VM_IDX */ + int rcv_io_num; /**< GPIO pin to USB_EXTPHY_RCV_IDX */ + int oen_io_num; /**< GPIO pin to USB_EXTPHY_OEN_IDX */ + int vpo_io_num; /**< GPIO pin to USB_EXTPHY_VPO_IDX */ + int vmo_io_num; /**< GPIO pin to USB_EXTPHY_VMO_IDX */ +} usb_phy_ext_io_conf_t; + +/** + * @brief USB OTG IO pins configuration structure + */ +typedef struct { + int iddig_io_num; /**< GPIO pin to USB_OTG_IDDIG_IN_IDX */ + int avalid_io_num; /**< GPIO pin to USB_OTG_AVALID_IN_IDX */ + int vbusvalid_io_num; /**< GPIO pin to USB_OTG_VBUSVALID_IN_IDX */ + int idpullup_io_num; /**< GPIO pin to USB_OTG_IDPULLUP_IDX */ + int dppulldown_io_num; /**< GPIO pin to USB_OTG_DPPULLDOWN_IDX */ + int dmpulldown_io_num; /**< GPIO pin to USB_OTG_DMPULLDOWN_IDX */ + int drvvbus_io_num; /**< GPIO pin to USB_OTG_DRVVBUS_IDX */ + int bvalid_io_num; /**< GPIO pin to USB_SRP_BVALID_IN_IDX */ + int sessend_io_num; /**< GPIO pin to USB_SRP_SESSEND_IN_IDX */ + int chrgvbus_io_num; /**< GPIO pin to USB_SRP_CHRGVBUS_IDX */ + int dischrgvbus_io_num; /**< GPIO pin to USB_SRP_DISCHRGVBUS_IDX */ +} usb_phy_otg_io_conf_t; + +/** + * @brief USB PHY configure struct + * + * At minimum the PHY controller and PHY target must be initialized. + */ +typedef struct { + usb_phy_controller_t controller; /**< USB PHY controller */ + usb_phy_target_t target; /**< USB PHY target INT/EXT */ + usb_otg_mode_t otg_mode; /**< USB OTG mode */ + usb_phy_speed_t otg_speed; /**< USB OTG speed */ + const usb_phy_ext_io_conf_t *ext_io_conf; /**< USB external PHY IO pins configuration */ + const usb_phy_otg_io_conf_t *otg_io_conf; /**< USB OTG IO pins configuration */ +} usb_phy_config_t; + +typedef struct phy_context_t *usb_phy_handle_t; /**< USB PHY context handle */ + +/** + * @brief Initialize a new USB PHY + * Configure at least PHY source. + * + * This function will enable the OTG Controller + * + * @param[in] config USB PHY configurtion struct + * @param[out] handle_ret USB PHY context handle + * + * @return + * - ESP_OK Success + * - ESP_FAIL USB PHY init error. + * - ESP_ERR_INVALID_STATE USB PHY not installed. + * - ESP_ERR_NO_MEM USB_OTG installation failed due to no mem. + */ +esp_err_t usb_new_phy(const usb_phy_config_t *config, usb_phy_handle_t *handle_ret); + +/** + * @brief Configure OTG mode for a USB PHY + * + * @param handle Pointer of USB PHY context handle + * @param mode USB OTG mode + * + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_ARG Parameter error. + * - ESP_FAIL OTG set mode fail. + */ +esp_err_t usb_phy_otg_set_mode(usb_phy_handle_t handle, usb_otg_mode_t mode); + +/** + * @brief Configure USB speed for a USB PHY that is operating as an OTG Device + * + * @param handle Pointer of USB PHY context handle + * @param mode USB speed + * + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_ARG Parameter error. + * - ESP_FAIL OTG set speed fail. + */ +esp_err_t usb_phy_otg_dev_set_speed(usb_phy_handle_t handle, usb_phy_speed_t speed); + +/** + * @brief Take a action for a USB PHY + * + * @param handle Pointer of USB PHY context handle + * @param action USB PHY action + * + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_ARG Parameter error. + * - ESP_FAIL Action cannot be performed. + */ +esp_err_t usb_phy_action(usb_phy_handle_t handle, usb_phy_action_t action); + +/** + * @brief Delete a USB PHY + * + * @param handle Pointer of USB PHY context handle + * + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_ARG Parameter error. + */ +esp_err_t usb_del_phy(usb_phy_handle_t handle); + +/** + * @brief Get status of a USB PHY + * + * @param[in] target The specific PHY target to check + * @param[out] status Status of the PHY + * + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_ARG Parameter error. + * - ESP_ERR_INVALID_STATE USB PHY not installed. + */ +esp_err_t usb_phy_get_phy_status(usb_phy_target_t target, usb_phy_status_t *status); + +#ifdef __cplusplus +} +#endif diff --git a/components/usb/include/usb/usb_helpers.h b/components/usb/include/usb/usb_helpers.h new file mode 100644 index 0000000000..faf430de39 --- /dev/null +++ b/components/usb/include/usb/usb_helpers.h @@ -0,0 +1,180 @@ +/* + * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* +Warning: The USB Host Library API is still a beta version and may be subject to change +*/ + +#pragma once + +#include +#include "esp_err.h" +#include "sdkconfig.h" +#include "usb/usb_types_stack.h" +#include "usb/usb_types_ch9.h" +#if (CONFIG_USB_HOST_EXT_HUB_SUPPORT) +#include "usb/usb_types_ch11.h" +#endif // CONFIG_USB_HOST_EXT_HUB_SUPPORT + +#ifdef __cplusplus +extern "C" { +#endif + +// ---------------------------------------- Configuration Descriptor Parsing ------------------------------------------- + +/** + * @brief Get the next descriptor + * + * Given a particular descriptor within a full configuration descriptor, get the next descriptor within the + * configuration descriptor. This is a convenience function that can be used to walk each individual descriptor within + * a full configuration descriptor. + * + * @param[in] cur_desc Current descriptor + * @param[in] wTotalLength Total length of the configuration descriptor + * @param[inout] offset Byte offset relative to the start of the configuration descriptor. On input, it is the offset of + * the current descriptor. On output, it is the offset of the returned descriptor. + * @return usb_standard_desc_t* Next descriptor, NULL if end of configuration descriptor reached + */ +const usb_standard_desc_t *usb_parse_next_descriptor(const usb_standard_desc_t *cur_desc, uint16_t wTotalLength, int *offset); + +/** + * @brief Get the next descriptor of a particular type + * + * Given a particular descriptor within a full configuration descriptor, get the next descriptor of a particular type + * (i.e., using the bDescriptorType value) within the configuration descriptor. + * + * @param[in] cur_desc Current descriptor + * @param[in] wTotalLength Total length of the configuration descriptor + * @param[in] bDescriptorType Type of the next descriptor to get + * @param[inout] offset Byte offset relative to the start of the configuration descriptor. On input, it is the offset of + * the current descriptor. On output, it is the offset of the returned descriptor. + * @return usb_standard_desc_t* Next descriptor, NULL if end descriptor is not found or configuration descriptor reached + */ +const usb_standard_desc_t *usb_parse_next_descriptor_of_type(const usb_standard_desc_t *cur_desc, uint16_t wTotalLength, uint8_t bDescriptorType, int *offset); + +/** + * @brief Get the number of alternate settings for a bInterfaceNumber + * + * Given a particular configuration descriptor, for a particular bInterfaceNumber, get the number of alternate settings + * available for that interface (i.e., the max possible value of bAlternateSetting for that bInterfaceNumber). + * + * @param[in] config_desc Pointer to the start of a full configuration descriptor + * @param[in] bInterfaceNumber Interface number + * @return int The number of alternate settings that the interface has, -1 if bInterfaceNumber not found + */ +int usb_parse_interface_number_of_alternate(const usb_config_desc_t *config_desc, uint8_t bInterfaceNumber); + +/** + * @brief Get a particular interface descriptor (using bInterfaceNumber and bAlternateSetting) + * + * Given a full configuration descriptor, get a particular interface descriptor. + * + * @note To get the number of alternate settings for a particular bInterfaceNumber, call + * usb_parse_interface_number_of_alternate() + * + * @param[in] config_desc Pointer to the start of a full configuration descriptor + * @param[in] bInterfaceNumber Interface number + * @param[in] bAlternateSetting Alternate setting number + * @param[out] offset Byte offset of the interface descriptor relative to the start of the configuration descriptor. Can be NULL. + * @return const usb_intf_desc_t* Pointer to interface descriptor, NULL if not found. + */ +const usb_intf_desc_t *usb_parse_interface_descriptor(const usb_config_desc_t *config_desc, uint8_t bInterfaceNumber, uint8_t bAlternateSetting, int *offset); + +/** + * @brief Get an endpoint descriptor within an interface descriptor + * + * Given an interface descriptor, get the Nth endpoint descriptor of the interface. The number of endpoints in an + * interface is indicated by the bNumEndpoints field of the interface descriptor. + * + * @note If bNumEndpoints is 0, it means the interface uses the default endpoint only + * + * @param[in] intf_desc Pointer to thee start of an interface descriptor + * @param[in] index Endpoint index + * @param[in] wTotalLength Total length of the containing configuration descriptor + * @param[inout] offset Byte offset relative to the start of the configuration descriptor. On input, it is the offset + * of the interface descriptor. On output, it is the offset of the endpoint descriptor. + * @return const usb_ep_desc_t* Pointer to endpoint descriptor, NULL if not found. + */ +const usb_ep_desc_t *usb_parse_endpoint_descriptor_by_index(const usb_intf_desc_t *intf_desc, int index, uint16_t wTotalLength, int *offset); + +/** + * @brief Get an endpoint descriptor based on an endpoint's address + * + * Given a configuration descriptor, get an endpoint descriptor based on it's bEndpointAddress, bAlternateSetting, and + * bInterfaceNumber. + * + * @param[in] config_desc Pointer to the start of a full configuration descriptor + * @param[in] bInterfaceNumber Interface number + * @param[in] bAlternateSetting Alternate setting number + * @param[in] bEndpointAddress Endpoint address + * @param[out] offset Byte offset of the endpoint descriptor relative to the start of the configuration descriptor. Can be NULL + * @return const usb_ep_desc_t* Pointer to endpoint descriptor, NULL if not found. + */ +const usb_ep_desc_t *usb_parse_endpoint_descriptor_by_address(const usb_config_desc_t *config_desc, uint8_t bInterfaceNumber, uint8_t bAlternateSetting, uint8_t bEndpointAddress, int *offset); + +// ----------------------------------------------- Descriptor Printing ------------------------------------------------- + +/** + * @brief Print class specific descriptor callback + * + * Optional callback to be provided to usb_print_config_descriptor() function. + * The callback is called when when a non-standard descriptor is encountered. + * The callback should decode the descriptor as print it. + */ +typedef void (*print_class_descriptor_cb)(const usb_standard_desc_t *); + +/** + * @brief Print device descriptor + * + * @param devc_desc Device descriptor + */ +void usb_print_device_descriptor(const usb_device_desc_t *devc_desc); + +/** + * @brief Print configuration descriptor + * + * - This function prints the full contents of a configuration descriptor (including interface and endpoint descriptors) + * - When a non-standard descriptor is encountered, this function will call the class_specific_cb if it is provided + * + * @param cfg_desc Configuration descriptor + * @param class_specific_cb Class specific descriptor callback. Can be NULL + */ +void usb_print_config_descriptor(const usb_config_desc_t *cfg_desc, print_class_descriptor_cb class_specific_cb); + +/** + * @brief Print a string descriptor + * + * This funciton will only print ASCII characters of the UTF-16 encoded string + * + * @param str_desc String descriptor + */ +void usb_print_string_descriptor(const usb_str_desc_t *str_desc); + +// ------------------------------------------------------ Misc --------------------------------------------------------- + +/** + * @brief Round up to an integer multiple of endpoint's MPS + * + * This is a convenience function to round up a size/length to an endpoint's MPS (Maximum packet size). This is useful + * when calculating transfer or buffer lengths of IN endpoints. + * - If MPS <= 0, this function will return 0 + * - If num_bytes <= 0, this function will return 0 + * + * @param[in] num_bytes Number of bytes + * @param[in] mps MPS + * @return int Round up integer multiple of MPS + */ +static inline int usb_round_up_to_mps(int num_bytes, int mps) +{ + if (num_bytes <= 0 || mps <= 0) { //MPS can never be zero + return 0; + } + return ((num_bytes + mps - 1) / mps) * mps; +} + +#ifdef __cplusplus +} +#endif diff --git a/components/usb/include/usb/usb_host.h b/components/usb/include/usb/usb_host.h new file mode 100644 index 0000000000..13a58caee9 --- /dev/null +++ b/components/usb/include/usb/usb_host.h @@ -0,0 +1,481 @@ +/* + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* +Warning: The USB Host Library API is still a beta version and may be subject to change +*/ + +#pragma once + +#include +#include "freertos/FreeRTOS.h" +#include "esp_err.h" +#include "esp_intr_alloc.h" +// Include the other USB Host Library headers as well +#include "usb/usb_helpers.h" +#include "usb/usb_types_ch9.h" +#include "usb/usb_types_stack.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// ------------------------------------------------- Macros and Types -------------------------------------------------- + +// ----------------------- Handles ------------------------- + +/** + * @brief Handle to a USB Host Library asynchronous client + * + * An asynchronous client can be registered using usb_host_client_register() + * + * @note Asynchronous API + */ +typedef struct usb_host_client_handle_s *usb_host_client_handle_t; + +// ----------------------- Events -------------------------- + +#define USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS 0x01 /**< All clients have been deregistered from the USB Host Library */ +#define USB_HOST_LIB_EVENT_FLAGS_ALL_FREE 0x02 /**< The USB Host Library has freed all devices */ + +/** + * @brief The type event in a client event message + */ +typedef enum { + USB_HOST_CLIENT_EVENT_NEW_DEV, /**< A new device has been enumerated and added to the USB Host Library */ + USB_HOST_CLIENT_EVENT_DEV_GONE, /**< A device opened by the client is now gone */ +} usb_host_client_event_t; + +/** + * @brief Client event message + * + * Client event messages are sent to each client of the USB Host Library in order to notify them of various + * USB Host Library events such as: + * - Addition of new devices + * - Removal of existing devices + * + * @note The event message structure has a union with members corresponding to each particular event. Based on the event + * type, only the relevant member field should be accessed. + */ +typedef struct { + usb_host_client_event_t event; /**< Type of event */ + union { + struct { + uint8_t address; /**< New device's address */ + } new_dev; /**< New device info */ + struct { + usb_device_handle_t dev_hdl; /**< The handle of the device that was gone */ + } dev_gone; /**< Gone device info */ + }; +} usb_host_client_event_msg_t; + +// ------------------------ Info --------------------------- + +/** + * @brief Current information about the USB Host Library obtained via usb_host_lib_info() + */ +typedef struct { + int num_devices; /**< Current number of connected (and enumerated) devices */ + int num_clients; /**< Current number of registered clients */ +} usb_host_lib_info_t; + +// ---------------------- Callbacks ------------------------ + +/** + * @brief Client event callback + * + * - Each client of the USB Host Library must register an event callback to receive event messages from the USB Host + * Library. + * - The client event callback is run from the context of the clients usb_host_client_handle_events() function + */ +typedef void (*usb_host_client_event_cb_t)(const usb_host_client_event_msg_t *event_msg, void *arg); + +// -------------------- Configurations --------------------- + +/** + * @brief USB Host Library configuration + * + * Configuration structure of the USB Host Library. Provided in the usb_host_install() function + */ +typedef struct { + bool skip_phy_setup; /**< If set, the USB Host Library will not configure the USB PHY thus allowing the user + to manually configure the USB PHY before calling usb_host_install(). Users should + set this if they want to use an external USB PHY. Otherwise, the USB Host Library + will automatically configure the internal USB PHY */ + int intr_flags; /**< Interrupt flags for the underlying ISR used by the USB Host stack */ + usb_host_enum_filter_cb_t enum_filter_cb; /**< Enumeration filter callback. Enable CONFIG_USB_HOST_ENABLE_ENUM_FILTER_CALLBACK + to use this feature. Set to NULL otherwise. */ +} usb_host_config_t; + +/** + * @brief USB Host Library Client configuration + * + * Configuration structure for a USB Host Library client. Provided in usb_host_client_register() + */ +typedef struct { + bool is_synchronous; /**< Whether the client is asynchronous or synchronous or not. Set to false for now. */ + int max_num_event_msg; /**< Maximum number of event messages that can be stored (e.g., 3) */ + union { // Note: Made into union or future expansion + struct { + usb_host_client_event_cb_t client_event_callback; /**< Client's event callback function */ + void *callback_arg; /**< Event callback function argument */ + } async; /**< Async callback config */ + }; +} usb_host_client_config_t; + +// ------------------------------------------------ Library Functions -------------------------------------------------- + +/** + * @brief Install the USB Host Library + * + * - This function should only once to install the USB Host Library + * - This function should be called before any other USB Host Library functions are called + * + * @note If skip_phy_setup is set in the install configuration, the user is responsible for ensuring that the underlying + * Host Controller is enabled and the USB PHY (internal or external) is already setup before this function is + * called. + * @param[in] config USB Host Library configuration + * @return esp_err_t + */ +esp_err_t usb_host_install(const usb_host_config_t *config); + +/** + * @brief Uninstall the USB Host Library + * + * - This function should be called to uninstall the USB Host Library, thereby freeing its resources + * - All clients must have been deregistered before calling this function + * - All devices must have been freed by calling usb_host_device_free_all() and receiving the + * USB_HOST_LIB_EVENT_FLAGS_ALL_FREE event flag + * + * @note If skip_phy_setup was set when the Host Library was installed, the user is responsible for disabling the + * underlying Host Controller and USB PHY (internal or external). + * @return esp_err_t + */ +esp_err_t usb_host_uninstall(void); + +/** + * @brief Handle USB Host Library events + * + * - This function handles all of the USB Host Library's processing and should be called repeatedly in a loop + * - Check event_flags_ret to see if an flags are set indicating particular USB Host Library events + * - This function should never be called by multiple threads simultaneously + * + * @note This function can block + * @param[in] timeout_ticks Timeout in ticks to wait for an event to occur + * @param[out] event_flags_ret Event flags that indicate what USB Host Library event occurred. + * @return esp_err_t + */ +esp_err_t usb_host_lib_handle_events(TickType_t timeout_ticks, uint32_t *event_flags_ret); + +/** + * @brief Unblock the USB Host Library handler + * + * - This function simply unblocks the USB Host Library event handling function (usb_host_lib_handle_events()) + * + * @return esp_err_t + */ +esp_err_t usb_host_lib_unblock(void); + +/** + * @brief Get current information about the USB Host Library + * + * @param[out] info_ret USB Host Library Information + * @return esp_err_t + */ +esp_err_t usb_host_lib_info(usb_host_lib_info_t *info_ret); + +// ------------------------------------------------ Client Functions --------------------------------------------------- + +/** + * @brief Register a client of the USB Host Library + * + * - This function registers a client of the USB Host Library + * - Once a client is registered, its processing function usb_host_client_handle_events() should be called repeatedly + * + * @param[in] client_config Client configuration + * @param[out] client_hdl_ret Client handle + * @return esp_err_t + */ +esp_err_t usb_host_client_register(const usb_host_client_config_t *client_config, usb_host_client_handle_t *client_hdl_ret); + +/** + * @brief Deregister a USB Host Library client + * + * - This function deregisters a client of the USB Host Library + * - The client must have closed all previously opened devices before attempting to deregister + * + * @param[in] client_hdl Client handle + * @return esp_err_t + */ +esp_err_t usb_host_client_deregister(usb_host_client_handle_t client_hdl); + +/** + * @brief USB Host Library client processing function + * + * - This function handles all of a client's processing and should be called repeatedly in a loop + * - For a particular client, this function should never be called by multiple threads simultaneously + * + * @note This function can block + * @param[in] client_hdl Client handle + * @param[in] timeout_ticks Timeout in ticks to wait for an event to occur + * @return esp_err_t + */ +esp_err_t usb_host_client_handle_events(usb_host_client_handle_t client_hdl, TickType_t timeout_ticks); + +/** + * @brief Unblock a client + * + * - This function simply unblocks a client if it is blocked on the usb_host_client_handle_events() function. + * - This function is useful when need to unblock a client in order to deregister it. + * + * @param[in] client_hdl Client handle + * @return esp_err_t + */ +esp_err_t usb_host_client_unblock(usb_host_client_handle_t client_hdl); + +// ------------------------------------------------- Device Handling --------------------------------------------------- + +/** + * @brief Open a device + * + * - This function allows a client to open a device + * - A client must open a device first before attempting to use it (e.g., sending transfers, device requests etc.) + * + * @param[in] client_hdl Client handle + * @param[in] dev_addr Device's address + * @param[out] dev_hdl_ret Device's handle + * @return esp_err_t + */ +esp_err_t usb_host_device_open(usb_host_client_handle_t client_hdl, uint8_t dev_addr, usb_device_handle_t *dev_hdl_ret); + +/** + * @brief Close a device + * + * - This function allows a client to close a device + * - A client must close a device after it has finished using the device (claimed interfaces must also be released) + * - A client must close all devices it has opened before deregistering + * + * @note This function can block + * @param[in] client_hdl Client handle + * @param[in] dev_hdl Device handle + * @return esp_err_t + */ +esp_err_t usb_host_device_close(usb_host_client_handle_t client_hdl, usb_device_handle_t dev_hdl); + +/** + * @brief Indicate that all devices can be freed when possible + * + * - This function marks all devices as waiting to be freed + * - If a device is not opened by any clients, it will be freed immediately + * - If a device is opened by at least one client, the device will be free when the last client closes that device. + * - Wait for the USB_HOST_LIB_EVENT_FLAGS_ALL_FREE flag to be set by usb_host_lib_handle_events() in order to know + * when all devices have been freed + * - This function is useful when cleaning up devices before uninstalling the USB Host Library + * + * @return + * - ESP_ERR_NOT_FINISHED: There are one or more devices that still need to be freed. Wait for USB_HOST_LIB_EVENT_FLAGS_ALL_FREE event + * - ESP_OK: All devices already freed (i.e., there were no devices) + * - Other: Error + */ +esp_err_t usb_host_device_free_all(void); + +/** + * @brief Fill a list of device address + * + * - This function fills an empty list with the address of connected devices + * - The Device addresses can then used in usb_host_device_open() + * - If there are more devices than the list_len, this function will only fill up to list_len number of devices. + * + * @param[in] list_len Length of the empty list + * @param[inout] dev_addr_list Empty list to be filled + * @param[out] num_dev_ret Number of devices + * @return esp_err_t + */ +esp_err_t usb_host_device_addr_list_fill(int list_len, uint8_t *dev_addr_list, int *num_dev_ret); + +// ------------------------------------------------- Device Requests --------------------------------------------------- + +// ------------------- Cached Requests --------------------- + +/** + * @brief Get device's information + * + * - This function gets some basic information of a device + * - The device must be opened first before attempting to get its information + * + * @note This function can block + * @param[in] dev_hdl Device handle + * @param[out] dev_info Device information + * @return esp_err_t + */ +esp_err_t usb_host_device_info(usb_device_handle_t dev_hdl, usb_device_info_t *dev_info); + +// ----------------------------------------------- Descriptor Requests ------------------------------------------------- + +// ----------------- Cached Descriptors -------------------- + +/** + * @brief Get device's device descriptor + * + * - A client must call usb_host_device_open() first + * - No control transfer is sent. The device's descriptor is cached on enumeration + * - This function simple returns a pointer to the cached descriptor + * + * @note No control transfer is sent. The device's descriptor is cached on enumeration + * @param[in] dev_hdl Device handle + * @param[out] device_desc Device descriptor + * @return esp_err_t + */ +esp_err_t usb_host_get_device_descriptor(usb_device_handle_t dev_hdl, const usb_device_desc_t **device_desc); + +/** + * @brief Get device's active configuration descriptor + * + * - A client must call usb_host_device_open() first + * - No control transfer is sent. The device's active configuration descriptor is cached on enumeration + * - This function simple returns a pointer to the cached descriptor + * + * @note This function can block + * @note No control transfer is sent. A device's active configuration descriptor is cached on enumeration + * @param[in] dev_hdl Device handle + * @param[out] config_desc Configuration descriptor + * @return esp_err_t + */ +esp_err_t usb_host_get_active_config_descriptor(usb_device_handle_t dev_hdl, const usb_config_desc_t **config_desc); + +// ----------------------------------------------- Interface Functions ------------------------------------------------- + +/** + * @brief Function for a client to claim a device's interface + * + * - A client must claim a device's interface before attempting to communicate with any of its endpoints + * - Once an interface is claimed by a client, it cannot be claimed by any other client. + * + * @note This function can block + * @param[in] client_hdl Client handle + * @param[in] dev_hdl Device handle + * @param[in] bInterfaceNumber Interface number + * @param[in] bAlternateSetting Interface alternate setting number + * @return esp_err_t + */ +esp_err_t usb_host_interface_claim(usb_host_client_handle_t client_hdl, usb_device_handle_t dev_hdl, uint8_t bInterfaceNumber, uint8_t bAlternateSetting); + +/** + * @brief Function for a client to release a previously claimed interface + * + * - A client should release a device's interface after it no longer needs to communicate with the interface + * - A client must release all of its interfaces of a device it has claimed before being able to close the device + * + * @note This function can block + * @param[in] client_hdl Client handle + * @param[in] dev_hdl Device handle + * @param[in] bInterfaceNumber Interface number + * @return esp_err_t + */ +esp_err_t usb_host_interface_release(usb_host_client_handle_t client_hdl, usb_device_handle_t dev_hdl, uint8_t bInterfaceNumber); + +/** + * @brief Halt a particular endpoint + * + * - The device must have been opened by a client + * - The endpoint must be part of an interface claimed by a client + * - Once halted, the endpoint must be cleared using usb_host_endpoint_clear() before it can communicate again + * + * @note This function can block + * @param dev_hdl Device handle + * @param bEndpointAddress Endpoint address + * @return esp_err_t + */ +esp_err_t usb_host_endpoint_halt(usb_device_handle_t dev_hdl, uint8_t bEndpointAddress); + +/** + * @brief Flush a particular endpoint + * + * - The device must have been opened by a client + * - The endpoint must be part of an interface claimed by a client + * - The endpoint must have been halted (either through a transfer error, or usb_host_endpoint_halt()) + * - Flushing an endpoint will caused an queued up transfers to be canceled + * + * @note This function can block + * @param dev_hdl Device handle + * @param bEndpointAddress Endpoint address + * @return esp_err_t + */ +esp_err_t usb_host_endpoint_flush(usb_device_handle_t dev_hdl, uint8_t bEndpointAddress); + +/** + * @brief Clear a halt on a particular endpoint + * + * - The device must have been opened by a client + * - The endpoint must be part of an interface claimed by a client + * - The endpoint must have been halted (either through a transfer error, or usb_host_endpoint_halt()) + * - If the endpoint has any queued up transfers, clearing a halt will resume their execution + * + * @note This function can block + * @param dev_hdl Device handle + * @param bEndpointAddress Endpoint address + * @return esp_err_t + */ +esp_err_t usb_host_endpoint_clear(usb_device_handle_t dev_hdl, uint8_t bEndpointAddress); + +// ------------------------------------------------ Asynchronous I/O --------------------------------------------------- + +/** + * @brief Allocate a transfer object + * + * - This function allocates a transfer object + * - Each transfer object has a fixed sized buffer specified on allocation + * - A transfer object can be re-used indefinitely + * - A transfer can be submitted using usb_host_transfer_submit() or usb_host_transfer_submit_control() + * + * @param[in] data_buffer_size Size of the transfer's data buffer + * @param[in] num_isoc_packets Number of isochronous packets in transfer (set to 0 for non-isochronous transfers) + * @param[out] transfer Transfer object + * @return esp_err_t + */ +esp_err_t usb_host_transfer_alloc(size_t data_buffer_size, int num_isoc_packets, usb_transfer_t **transfer); + +/** + * @brief Free a transfer object + * + * - Free a transfer object previously allocated using usb_host_transfer_alloc() + * - The transfer must not be in-flight when attempting to free it + * - If a NULL pointer is passed, this function will simply return ESP_OK + * + * @param[in] transfer Transfer object + * @return esp_err_t + */ +esp_err_t usb_host_transfer_free(usb_transfer_t *transfer); + +/** + * @brief Submit a non-control transfer + * + * - Submit a transfer to a particular endpoint. The device and endpoint number is specified inside the transfer + * - The transfer must be properly initialized before submitting + * - On completion, the transfer's callback will be called from the client's usb_host_client_handle_events() function. + * + * @param[in] transfer Initialized transfer object + * @return esp_err_t + */ +esp_err_t usb_host_transfer_submit(usb_transfer_t *transfer); + +/** + * @brief Submit a control transfer + * + * - Submit a control transfer to a particular device. The client must have opened the device first + * - The transfer must be properly initialized before submitting. The first 8 bytes of the transfer's data buffer should + * contain the control transfer setup packet + * - On completion, the transfer's callback will be called from the client's usb_host_client_handle_events() function. + * + * @param[in] client_hdl Client handle + * @param[in] transfer Initialized transfer object + * @return esp_err_t + */ +esp_err_t usb_host_transfer_submit_control(usb_host_client_handle_t client_hdl, usb_transfer_t *transfer); + +#ifdef __cplusplus +} +#endif diff --git a/components/usb/include/usb/usb_types_ch11.h b/components/usb/include/usb/usb_types_ch11.h new file mode 100644 index 0000000000..6d13998cc5 --- /dev/null +++ b/components/usb/include/usb/usb_types_ch11.h @@ -0,0 +1,221 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "esp_assert.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief USB Hub request types + */ + +#define USB_BM_REQUEST_TYPE_HUB (USB_BM_REQUEST_TYPE_TYPE_CLASS | USB_BM_REQUEST_TYPE_RECIP_DEVICE) +#define USB_BM_REQUEST_TYPE_PORT (USB_BM_REQUEST_TYPE_TYPE_CLASS | USB_BM_REQUEST_TYPE_RECIP_OTHER) + +/** + * @brief USB Hub descriptor type + */ +#define USB_CLASS_DESCRIPTOR_TYPE_HUB 0x29 + +/** + * @brief USB Hub Class bRequest codes + * + * See USB 2.0 spec Table 11-16 + */ +typedef enum { + USB_B_REQUEST_HUB_GET_PORT_STATUS = 0x00, /**< Get port status. */ + USB_B_REQUEST_HUB_CLEAR_FEATURE = 0x01, /**< Clearing a feature disables that feature or starts a process associated with the feature. */ + USB_B_REQUEST_HUB_GET_STATE = 0x02, /**< Outdated. Used in previous specifications for GET_STATE */ + USB_B_REQUEST_HUB_SET_PORT_FEATURE = 0x03, /**< Set port feature. */ + USB_B_REQUEST_HUB_GET_DESCRIPTOR = 0x06, /**< Get HUB descriptor. */ + USB_B_REQUEST_HUB_SET_DESCRIPTOR = 0x07, /**< Set HUB descriptor. */ + USB_B_REQUEST_HUB_CLEAR_TT_BUFFER = 0x08, /**< This request clears the state of a Transaction Translator(TT) bulk/control buffer after it has been left in a busy state due to high-speed errors. */ + USB_B_REQUEST_HUB_RESET_TT = 0x09, /**< Reset TT. */ + USB_B_REQUEST_HUB_GET_TT_STATE = 0x0A, /**< Get TT state. */ + USB_B_REQUEST_HUB_STOP_TT = 0x0B, /**< Stop TT. */ +} usb_hub_class_request_t ; + +/** + * @brief USB Hub Port feature selector codes + * + * See USB 2.0 spec Table 11-17 + */ +typedef enum { + USB_FEATURE_PORT_CONNECTION = 0x00, + USB_FEATURE_PORT_ENABLE = 0x01, + USB_FEATURE_PORT_SUSPEND = 0x02, + USB_FEATURE_PORT_OVER_CURRENT = 0x03, + USB_FEATURE_PORT_RESET = 0x04, + USB_FEATURE_PORT_POWER = 0x08, + USB_FEATURE_PORT_LOWSPEED = 0x09, + USB_FEATURE_C_PORT_CONNECTION = 0x10, + USB_FEATURE_C_PORT_ENABLE = 0x11, + USB_FEATURE_C_PORT_SUSPEND = 0x12, + USB_FEATURE_C_PORT_OVER_CURRENT = 0x13, + USB_FEATURE_C_PORT_RESET = 0x14, + USB_FEATURE_PORT_TEST = 0x15, + USB_FEATURE_PORT_INDICATOR = 0x16, +} usb_hub_port_feature_t; + +/** + * @brief Size of a USB Hub Port Status and Hub Change results + */ +#define USB_PORT_STATUS_SIZE 4 + +/** + * @brief USB Hub Port Status and Hub Change results + * + * See USB 2.0 spec Table 11-19 and Table 11-20 + */ +typedef struct { + union { + struct { + uint8_t PORT_CONNECTION : 1; /**< 0 = No device is present. 1 = A device is present on this port.*/ + uint8_t PORT_ENABLE : 1; /**< 0 = Port is disabled. 1 = Port is enabled.*/ + uint8_t PORT_SUSPEND : 1; /**< 0 = Not suspended. 1 = Suspended or resuming. */ + uint8_t PORT_OVER_CURRENT : 1; /**< 0 = All no over-current condition exists on this port. 1 = An over-current condition exists on this port. */ + uint8_t PORT_RESET : 1; /**< 0 = Reset signaling not asserted. 1 = Reset signaling asserted. */ + uint8_t RESERVED_1 : 3; /**< Reserved field */ + uint8_t PORT_POWER : 1; /**< 0 = This port is in the Powered-off state. 1 = This port is not in the Powered-off state. */ + uint8_t PORT_LOW_SPEED : 1; /**< 0 = Full-speed or High-speed device attached to this port (determined by bit 10). 1 = Low-speed device attached to this port.*/ + uint8_t PORT_HIGH_SPEED : 1; /**< 0 = Full-speed device attached to this port. 1 = High-speed device attached to this port. */ + uint8_t PORT_TEST : 1; /**< 0 = This port is not in the Port Test Mode. 1 = This port is in Port Test Mode. */ + uint8_t PORT_INDICATOR : 1; /**< 0 = Port indicator displays default colors. 1 = Port indicator displays software controlled color. */ + uint8_t RESERVED_2 : 3; /**< Reserved field */ + }; + uint16_t val; /**< Port status value */ + } wPortStatus; + + union { + struct { + uint8_t C_PORT_CONNECTION : 1; /**< 0 = No change has occurred to Current Connect status. 1 = Current Connect status has changed. */ + uint8_t C_PORT_ENABLE : 1; /**< This field is set to one when a port is disabled because of a Port_Error condition */ + uint8_t C_PORT_SUSPEND : 1; /**< 0 = No change. 1 = Resume complete. */ + uint8_t C_PORT_OVER_CURRENT : 1; /**< 0 = No change has occurred to Over-Current Indicator. 1 = Over-Current Indicator has changed. */ + uint8_t C_PORT_RESET : 1; /**< This field is set when reset processing on this port is complete. 0 = No change. 1 = Reset complete.*/ + uint16_t RESERVED : 11; /**< Reserved field */ + }; + uint16_t val; /**< Port change value */ + } wPortChange; +} __attribute__((packed)) usb_port_status_t; +ESP_STATIC_ASSERT(sizeof(usb_port_status_t) == USB_PORT_STATUS_SIZE, "Size of usb_port_status_t incorrect"); + +/** + * @brief Size of a USB Hub Status + */ +#define USB_HUB_STATUS_SIZE 4 + +/** + * @brief USB Hub Status + */ +typedef struct { + union { + struct { + uint8_t HUB_LOCAL_POWER : 1; /**< 0 = Local power supply good. 1 = Local power supply lost (inactive)*/ + uint8_t HUB_OVER_CURRENT : 1; /**< 0 = No over-current condition currently exists. 1 = A hub over-current condition exists.*/ + uint16_t RESERVED : 14; /**< Reserved fields */ + }; + uint16_t val; /**< Hub status value */ + } wHubStatus; + union { + struct { + uint8_t C_HUB_LOCAL_POWER : 1; /**< 0 = No change has occurred to Local Power Status. 1 = Local Power Status has changed.*/ + uint8_t C_HUB_OVER_CURRENT : 1; /**< 0 = No change has occurred to the Over-Current Status. 1 = Over-Current Status has changed.*/ + uint16_t RESERVED : 14; /**< Reserved fields */ + }; + uint16_t val; /**< Hub change value */ + } wHubChange; +} __attribute__((packed)) usb_hub_status_t; +ESP_STATIC_ASSERT(sizeof(usb_hub_status_t) == USB_HUB_STATUS_SIZE, "Size of usb_hub_status_t incorrect"); + +/** + * @brief Size of a USB Hub Device descriptor + */ +#define USB_HUB_DESCRIPTOR_SIZE (7) + +/** + * @brief USB Hub Device descriptor + */ +typedef struct { + uint8_t bDescLength; /**< Number of bytes in this descriptor, including this byte */ + uint8_t bDescriptorType; /**< Descriptor Type, value: 29H for Hub descriptor */ + uint8_t bNbrPorts; /**< Number of downstream facing ports that this Hub supports */ + uint16_t wHubCharacteristics; /**< Logical Power Switching Mode, Compound Device, Over-current Protection Mode, TT Think Time, Port Indicators Supported */ + uint8_t bPwrOn2PwrGood; /**< Time (in 2 ms intervals) from the time the power-on sequence begins on a port until power is good on that port */ + uint8_t bHubContrCurrent; /**< Maximum current requirements of the Hub Controller electronics in mA. */ +} __attribute__((packed)) usb_hub_descriptor_t; +ESP_STATIC_ASSERT(sizeof(usb_hub_descriptor_t) == USB_HUB_DESCRIPTOR_SIZE, "Size of usb_hub_descriptor_t incorrect"); + +/** + * @brief Initializer for a request to get HUB descriptor + */ +#define USB_SETUP_PACKET_INIT_GET_HUB_DESCRIPTOR(setup_pkt_ptr) ({ \ + (setup_pkt_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_IN | USB_BM_REQUEST_TYPE_HUB; \ + (setup_pkt_ptr)->bRequest = USB_B_REQUEST_HUB_GET_DESCRIPTOR; \ + (setup_pkt_ptr)->wValue = (USB_CLASS_DESCRIPTOR_TYPE_HUB << 8); \ + (setup_pkt_ptr)->wIndex = 0; \ + (setup_pkt_ptr)->wLength = sizeof(usb_hub_descriptor_t); \ +}) + +/** + * @brief Initializer for a request to get HUB status + */ +#define USB_SETUP_PACKET_INIT_GET_HUB_STATUS(setup_pkt_ptr) ({ \ + (setup_pkt_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_IN | USB_BM_REQUEST_TYPE_HUB; \ + (setup_pkt_ptr)->bRequest = USB_B_REQUEST_HUB_GET_PORT_STATUS; \ + (setup_pkt_ptr)->wValue = 0; \ + (setup_pkt_ptr)->wIndex = 0; \ + (setup_pkt_ptr)->wLength = sizeof(usb_hub_status_t); \ +}) + +/** + * @brief Initializer for a request to get port status + */ +#define USB_SETUP_PACKET_INIT_GET_PORT_STATUS(setup_pkt_ptr, port) ({ \ + (setup_pkt_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_IN | USB_BM_REQUEST_TYPE_PORT; \ + (setup_pkt_ptr)->bRequest = USB_B_REQUEST_HUB_GET_PORT_STATUS; \ + (setup_pkt_ptr)->wValue = 0; \ + (setup_pkt_ptr)->wIndex = (port); \ + (setup_pkt_ptr)->wLength = sizeof(usb_port_status_t); \ +}) + +/** + * @brief Initializer for a set port feature + */ +#define USB_SETUP_PACKET_INIT_SET_PORT_FEATURE(setup_pkt_ptr, port, feature) ({ \ + (setup_pkt_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_OUT | USB_BM_REQUEST_TYPE_PORT; \ + (setup_pkt_ptr)->bRequest = USB_B_REQUEST_HUB_SET_PORT_FEATURE; \ + (setup_pkt_ptr)->wValue = (feature); \ + (setup_pkt_ptr)->wIndex = (port); \ + (setup_pkt_ptr)->wLength = 0; \ +}) + +/** + * @brief Initializer for a clear port feature + */ +#define USB_SETUP_PACKET_INIT_CLEAR_PORT_FEATURE(setup_pkt_ptr, port, feature) ({ \ + (setup_pkt_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_OUT | USB_BM_REQUEST_TYPE_PORT; \ + (setup_pkt_ptr)->bRequest = USB_B_REQUEST_HUB_CLEAR_FEATURE; \ + (setup_pkt_ptr)->wValue = (feature); \ + (setup_pkt_ptr)->wIndex = (port); \ + (setup_pkt_ptr)->wLength = 0; \ +}) + +/** + * @brief Get Port Number from a setup packet + */ +#define USB_SETUP_PACKET_GET_PORT(setup_pkt_ptr) ({ \ + (setup_pkt_ptr)->wIndex; \ +}) + +#ifdef __cplusplus +} +#endif //__cplusplus diff --git a/components/usb/include/usb/usb_types_ch9.h b/components/usb/include/usb/usb_types_ch9.h new file mode 100644 index 0000000000..c09440eb00 --- /dev/null +++ b/components/usb/include/usb/usb_types_ch9.h @@ -0,0 +1,495 @@ +/* + * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* +Warning: The USB Host Library API is still a beta version and may be subject to change +*/ + +#pragma once + +#include +#include "esp_assert.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define USB_DESC_ATTR __attribute__((packed)) + +// ---------------------------------------------------- Chapter 9 ------------------------------------------------------ + +/** + * @brief USB2.0 device states + * + * See Table 9-1 of USB2.0 specification for more details + * + * @note The USB_DEVICE_STATE_NOT_ATTACHED is not part of the USB2.0 specification, but is a catch all state for devices + * that need to be cleaned up after a sudden disconnection or port error. + */ +typedef enum { + USB_DEVICE_STATE_NOT_ATTACHED, /**< The device was previously configured or suspended, but is no longer attached (either suddenly disconnected or a port error) */ + USB_DEVICE_STATE_ATTACHED, /**< Device is attached to the USB, but is not powered. */ + USB_DEVICE_STATE_POWERED, /**< Device is attached to the USB and powered, but has not been reset. */ + USB_DEVICE_STATE_DEFAULT, /**< Device is attached to the USB and powered and has been reset, but has not been assigned a unique address. Device responds at the default address. */ + USB_DEVICE_STATE_ADDRESS, /**< Device is attached to the USB, powered, has been reset, and a unique device address has been assigned. Device is not configured. */ + USB_DEVICE_STATE_CONFIGURED, /**< Device is attached to the USB, powered, has been reset, has a unique address, is configured, and is not suspended. The host may now use the function provided by the device. */ + USB_DEVICE_STATE_SUSPENDED, /**< Device is, at minimum, attached to the USB and is powered and has not seen bus activity for 3 ms. It may also have a unique address and be configured for use. However, because the device is suspended, the host may not use the device’s function. */ +} usb_device_state_t; + +/** + * @brief Descriptor types from USB2.0 specification table 9.5 + */ +#define USB_B_DESCRIPTOR_TYPE_DEVICE 0x01 +#define USB_B_DESCRIPTOR_TYPE_CONFIGURATION 0x02 +#define USB_B_DESCRIPTOR_TYPE_STRING 0x03 +#define USB_B_DESCRIPTOR_TYPE_INTERFACE 0x04 +#define USB_B_DESCRIPTOR_TYPE_ENDPOINT 0x05 +#define USB_B_DESCRIPTOR_TYPE_DEVICE_QUALIFIER 0x06 +#define USB_B_DESCRIPTOR_TYPE_OTHER_SPEED_CONFIGURATION 0x07 +#define USB_B_DESCRIPTOR_TYPE_INTERFACE_POWER 0x08 + +/** + * @brief Descriptor types from USB 2.0 ECN + */ +#define USB_B_DESCRIPTOR_TYPE_OTG 0x09 +#define USB_B_DESCRIPTOR_TYPE_DEBUG 0x0a +#define USB_B_DESCRIPTOR_TYPE_INTERFACE_ASSOCIATION 0x0b + +/** + * @brief Descriptor types from Wireless USB spec + */ +#define USB_B_DESCRIPTOR_TYPE_SECURITY 0x0c +#define USB_B_DESCRIPTOR_TYPE_KEY 0x0d +#define USB_B_DESCRIPTOR_TYPE_ENCRYPTION_TYPE 0x0e +#define USB_B_DESCRIPTOR_TYPE_BOS 0x0f +#define USB_B_DESCRIPTOR_TYPE_DEVICE_CAPABILITY 0x10 +#define USB_B_DESCRIPTOR_TYPE_WIRELESS_ENDPOINT_COMP 0x11 +#define USB_B_DESCRIPTOR_TYPE_WIRE_ADAPTER 0x21 +#define USB_B_DESCRIPTOR_TYPE_RPIPE 0x22 +#define USB_B_DESCRIPTOR_TYPE_CS_RADIO_CONTROL 0x23 + +/** + * @brief Descriptor types from UAS specification + */ +#define USB_B_DESCRIPTOR_TYPE_PIPE_USAGE 0x24 + +// -------------------- Setup Packet ----------------------- + +/** + * @brief Size of a USB control transfer setup packet in bytes + */ +#define USB_SETUP_PACKET_SIZE 8 + +/** + * @brief Structure representing a USB control transfer setup packet + * + * See Table 9-2 of USB2.0 specification for more details + */ +typedef union { + struct { + uint8_t bmRequestType; /**< Characteristics of request */ + uint8_t bRequest; /**< Specific request */ + uint16_t wValue; /**< Word-sized field that varies according to request */ + uint16_t wIndex; /**< Word-sized field that varies according to request; typically used to pass an index or offset */ + uint16_t wLength; /**< Number of bytes to transfer if there is a data stage */ + } USB_DESC_ATTR; /**< USB descriptor attributes */ + uint8_t val[USB_SETUP_PACKET_SIZE]; /**< Descriptor value */ +} usb_setup_packet_t; +ESP_STATIC_ASSERT(sizeof(usb_setup_packet_t) == USB_SETUP_PACKET_SIZE, "Size of usb_setup_packet_t incorrect"); + +/** + * @brief Bit masks belonging to the bmRequestType field of a setup packet + */ +#define USB_BM_REQUEST_TYPE_DIR_OUT (0X00 << 7) +#define USB_BM_REQUEST_TYPE_DIR_IN (0x01 << 7) +#define USB_BM_REQUEST_TYPE_TYPE_STANDARD (0x00 << 5) +#define USB_BM_REQUEST_TYPE_TYPE_CLASS (0x01 << 5) +#define USB_BM_REQUEST_TYPE_TYPE_VENDOR (0x02 << 5) +#define USB_BM_REQUEST_TYPE_TYPE_RESERVED (0x03 << 5) +#define USB_BM_REQUEST_TYPE_TYPE_MASK (0x03 << 5) +#define USB_BM_REQUEST_TYPE_RECIP_DEVICE (0x00 << 0) +#define USB_BM_REQUEST_TYPE_RECIP_INTERFACE (0x01 << 0) +#define USB_BM_REQUEST_TYPE_RECIP_ENDPOINT (0x02 << 0) +#define USB_BM_REQUEST_TYPE_RECIP_OTHER (0x03 << 0) +#define USB_BM_REQUEST_TYPE_RECIP_MASK (0x1f << 0) + +/** + * @brief Bit masks belonging to the bRequest field of a setup packet + */ +#define USB_B_REQUEST_GET_STATUS 0x00 +#define USB_B_REQUEST_CLEAR_FEATURE 0x01 +#define USB_B_REQUEST_SET_FEATURE 0x03 +#define USB_B_REQUEST_SET_ADDRESS 0x05 +#define USB_B_REQUEST_GET_DESCRIPTOR 0x06 +#define USB_B_REQUEST_SET_DESCRIPTOR 0x07 +#define USB_B_REQUEST_GET_CONFIGURATION 0x08 +#define USB_B_REQUEST_SET_CONFIGURATION 0x09 +#define USB_B_REQUEST_GET_INTERFACE 0x0A +#define USB_B_REQUEST_SET_INTERFACE 0x0B +#define USB_B_REQUEST_SYNCH_FRAME 0x0C + +/** + * @brief Bit masks belonging to the wValue field of a setup packet + */ +#define USB_W_VALUE_DT_DEVICE 0x01 +#define USB_W_VALUE_DT_CONFIG 0x02 +#define USB_W_VALUE_DT_STRING 0x03 +#define USB_W_VALUE_DT_INTERFACE 0x04 +#define USB_W_VALUE_DT_ENDPOINT 0x05 +#define USB_W_VALUE_DT_DEVICE_QUALIFIER 0x06 +#define USB_W_VALUE_DT_OTHER_SPEED_CONFIG 0x07 +#define USB_W_VALUE_DT_INTERFACE_POWER 0x08 + +/** + * @brief Initializer for a SET_ADDRESS request + * + * Sets the address of a connected device + */ +#define USB_SETUP_PACKET_INIT_SET_ADDR(setup_pkt_ptr, addr) ({ \ + (setup_pkt_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_OUT | USB_BM_REQUEST_TYPE_TYPE_STANDARD |USB_BM_REQUEST_TYPE_RECIP_DEVICE; \ + (setup_pkt_ptr)->bRequest = USB_B_REQUEST_SET_ADDRESS; \ + (setup_pkt_ptr)->wValue = (addr); \ + (setup_pkt_ptr)->wIndex = 0; \ + (setup_pkt_ptr)->wLength = 0; \ +}) + +/** + * @brief Initializer for a request to get a device's device descriptor + */ +#define USB_SETUP_PACKET_INIT_GET_DEVICE_DESC(setup_pkt_ptr) ({ \ + (setup_pkt_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_IN | USB_BM_REQUEST_TYPE_TYPE_STANDARD | USB_BM_REQUEST_TYPE_RECIP_DEVICE; \ + (setup_pkt_ptr)->bRequest = USB_B_REQUEST_GET_DESCRIPTOR; \ + (setup_pkt_ptr)->wValue = (USB_W_VALUE_DT_DEVICE << 8); \ + (setup_pkt_ptr)->wIndex = 0; \ + (setup_pkt_ptr)->wLength = 18; \ +}) + +/** + * @brief Initializer for a request to get a device's current configuration number + */ +#define USB_SETUP_PACKET_INIT_GET_CONFIG(setup_pkt_ptr) ({ \ + (setup_pkt_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_IN | USB_BM_REQUEST_TYPE_TYPE_STANDARD | USB_BM_REQUEST_TYPE_RECIP_DEVICE; \ + (setup_pkt_ptr)->bRequest = USB_B_REQUEST_GET_CONFIGURATION; \ + (setup_pkt_ptr)->wValue = 0; \ + (setup_pkt_ptr)->wIndex = 0; \ + (setup_pkt_ptr)->wLength = 1; \ +}) + +/** + * @brief Initializer for a request to get one of the device's current configuration descriptor + * + * - desc_index indicates the configuration's index number + * - Number of bytes of the configuration descriptor to get + */ +#define USB_SETUP_PACKET_INIT_GET_CONFIG_DESC(setup_pkt_ptr, desc_index, desc_len) ({ \ + (setup_pkt_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_IN | USB_BM_REQUEST_TYPE_TYPE_STANDARD | USB_BM_REQUEST_TYPE_RECIP_DEVICE; \ + (setup_pkt_ptr)->bRequest = USB_B_REQUEST_GET_DESCRIPTOR; \ + (setup_pkt_ptr)->wValue = (USB_W_VALUE_DT_CONFIG << 8) | ((desc_index) & 0xFF); \ + (setup_pkt_ptr)->wIndex = 0; \ + (setup_pkt_ptr)->wLength = (desc_len); \ +}) + +/** + * @brief Initializer for a request to set a device's current configuration number + */ +#define USB_SETUP_PACKET_INIT_SET_CONFIG(setup_pkt_ptr, config_num) ({ \ + (setup_pkt_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_OUT | USB_BM_REQUEST_TYPE_TYPE_STANDARD | USB_BM_REQUEST_TYPE_RECIP_DEVICE; \ + (setup_pkt_ptr)->bRequest = USB_B_REQUEST_SET_CONFIGURATION; \ + (setup_pkt_ptr)->wValue = (config_num); \ + (setup_pkt_ptr)->wIndex = 0; \ + (setup_pkt_ptr)->wLength = 0; \ +}) + +/** + * @brief Initializer for a request to set an interface's alternate setting + */ +#define USB_SETUP_PACKET_INIT_SET_INTERFACE(setup_pkt_ptr, intf_num, alt_setting_num) ({ \ + (setup_pkt_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_OUT | USB_BM_REQUEST_TYPE_TYPE_STANDARD | USB_BM_REQUEST_TYPE_RECIP_INTERFACE; \ + (setup_pkt_ptr)->bRequest = USB_B_REQUEST_SET_INTERFACE; \ + (setup_pkt_ptr)->wValue = (alt_setting_num); \ + (setup_pkt_ptr)->wIndex = (intf_num); \ + (setup_pkt_ptr)->wLength = 0; \ +}) + +/** + * @brief Initializer for a request to get an string descriptor + */ +#define USB_SETUP_PACKET_INIT_GET_STR_DESC(setup_pkt_ptr, string_index, lang_id, desc_len) ({ \ + (setup_pkt_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_IN | USB_BM_REQUEST_TYPE_TYPE_STANDARD | USB_BM_REQUEST_TYPE_RECIP_DEVICE; \ + (setup_pkt_ptr)->bRequest = USB_B_REQUEST_GET_DESCRIPTOR; \ + (setup_pkt_ptr)->wValue = (USB_W_VALUE_DT_STRING << 8) | ((string_index) & 0xFF); \ + (setup_pkt_ptr)->wIndex = (lang_id); \ + (setup_pkt_ptr)->wLength = (desc_len); \ +}) + +// ---------------- Standard Descriptor -------------------- + +/** + * @brief Size of dummy USB standard descriptor + */ +#define USB_STANDARD_DESC_SIZE 2 + +/** + * @brief USB standard descriptor + * + * All USB standard descriptors start with these two bytes. Use this type when traversing over configuration descriptors + */ +typedef union { + struct { + uint8_t bLength; /**< Size of the descriptor in bytes */ + uint8_t bDescriptorType; /**< Descriptor Type */ + } USB_DESC_ATTR; /**< USB descriptor attributes */ + uint8_t val[USB_STANDARD_DESC_SIZE]; /**< Descriptor value */ +} usb_standard_desc_t; +ESP_STATIC_ASSERT(sizeof(usb_standard_desc_t) == USB_STANDARD_DESC_SIZE, "Size of usb_standard_desc_t incorrect"); + +// ------------------ Device Descriptor -------------------- + +/** + * @brief Size of a USB device descriptor in bytes + */ +#define USB_DEVICE_DESC_SIZE 18 + +/** + * @brief Structure representing a USB device descriptor + * + * See Table 9-8 of USB2.0 specification for more details + */ +typedef union { + struct { + uint8_t bLength; /**< Size of the descriptor in bytes */ + uint8_t bDescriptorType; /**< DEVICE Descriptor Type */ + uint16_t bcdUSB; /**< USB Specification Release Number in Binary-Coded Decimal (i.e., 2.10 is 210H) */ + uint8_t bDeviceClass; /**< Class code (assigned by the USB-IF) */ + uint8_t bDeviceSubClass; /**< Subclass code (assigned by the USB-IF) */ + uint8_t bDeviceProtocol; /**< Protocol code (assigned by the USB-IF) */ + uint8_t bMaxPacketSize0; /**< Maximum packet size for endpoint zero (only 8, 16, 32, or 64 are valid) */ + uint16_t idVendor; /**< Vendor ID (assigned by the USB-IF) */ + uint16_t idProduct; /**< Product ID (assigned by the manufacturer) */ + uint16_t bcdDevice; /**< Device release number in binary-coded decimal */ + uint8_t iManufacturer; /**< Index of string descriptor describing manufacturer */ + uint8_t iProduct; /**< Index of string descriptor describing product */ + uint8_t iSerialNumber; /**< Index of string descriptor describing the device’s serial number */ + uint8_t bNumConfigurations; /**< Number of possible configurations */ + } USB_DESC_ATTR; /**< USB descriptor attributes */ + uint8_t val[USB_DEVICE_DESC_SIZE]; /**< Descriptor value */ +} usb_device_desc_t; +ESP_STATIC_ASSERT(sizeof(usb_device_desc_t) == USB_DEVICE_DESC_SIZE, "Size of usb_device_desc_t incorrect"); + +/** + * @brief Possible base class values of the bDeviceClass field of a USB device descriptor + */ +#define USB_CLASS_PER_INTERFACE 0x00 +#define USB_CLASS_AUDIO 0x01 +#define USB_CLASS_COMM 0x02 +#define USB_CLASS_HID 0x03 +#define USB_CLASS_PHYSICAL 0x05 +#define USB_CLASS_STILL_IMAGE 0x06 +#define USB_CLASS_PRINTER 0x07 +#define USB_CLASS_MASS_STORAGE 0x08 +#define USB_CLASS_HUB 0x09 +#define USB_CLASS_CDC_DATA 0x0a +#define USB_CLASS_CSCID 0x0b +#define USB_CLASS_CONTENT_SEC 0x0d +#define USB_CLASS_VIDEO 0x0e +#define USB_CLASS_WIRELESS_CONTROLLER 0xe0 +#define USB_CLASS_PERSONAL_HEALTHCARE 0x0f +#define USB_CLASS_AUDIO_VIDEO 0x10 +#define USB_CLASS_BILLBOARD 0x11 +#define USB_CLASS_USB_TYPE_C_BRIDGE 0x12 +#define USB_CLASS_MISC 0xef +#define USB_CLASS_APP_SPEC 0xfe +#define USB_CLASS_VENDOR_SPEC 0xff + +/** + * @brief Vendor specific subclass code + */ +#define USB_SUBCLASS_VENDOR_SPEC 0xff + +// -------------- Configuration Descriptor ----------------- + +/** + * @brief Size of a short USB configuration descriptor in bytes + * + * @note The size of a full USB configuration includes all the interface and endpoint + * descriptors of that configuration. + */ +#define USB_CONFIG_DESC_SIZE 9 + +/** + * @brief Structure representing a short USB configuration descriptor + * + * See Table 9-10 of USB2.0 specification for more details + * + * @note The full USB configuration includes all the interface and endpoint + * descriptors of that configuration. + */ +typedef union { + struct { + uint8_t bLength; /**< Size of the descriptor in bytes */ + uint8_t bDescriptorType; /**< CONFIGURATION Descriptor Type */ + uint16_t wTotalLength; /**< Total length of data returned for this configuration */ + uint8_t bNumInterfaces; /**< Number of interfaces supported by this configuration */ + uint8_t bConfigurationValue; /**< Value to use as an argument to the SetConfiguration() request to select this configuration */ + uint8_t iConfiguration; /**< Index of string descriptor describing this configuration */ + uint8_t bmAttributes; /**< Configuration characteristics */ + uint8_t bMaxPower; /**< Maximum power consumption of the USB device from the bus in this specific configuration when the device is fully operational. */ + } USB_DESC_ATTR; /**< USB descriptor attributes */ + uint8_t val[USB_CONFIG_DESC_SIZE]; /**< Descriptor value */ +} usb_config_desc_t; +ESP_STATIC_ASSERT(sizeof(usb_config_desc_t) == USB_CONFIG_DESC_SIZE, "Size of usb_config_desc_t incorrect"); + +/** + * @brief Bit masks belonging to the bmAttributes field of a configuration descriptor + */ +#define USB_BM_ATTRIBUTES_ONE (1 << 7) /**< Must be set */ +#define USB_BM_ATTRIBUTES_SELFPOWER (1 << 6) /**< Self powered */ +#define USB_BM_ATTRIBUTES_WAKEUP (1 << 5) /**< Can wake-up */ +#define USB_BM_ATTRIBUTES_BATTERY (1 << 4) /**< Battery powered */ + +// ---------- Interface Association Descriptor ------------- + +/** + * @brief Size of a USB interface association descriptor in bytes + */ +#define USB_IAD_DESC_SIZE 9 + +/** + * @brief Structure representing a USB interface association descriptor + */ +typedef union { + struct { + uint8_t bLength; /**< Size of the descriptor in bytes */ + uint8_t bDescriptorType; /**< INTERFACE ASSOCIATION Descriptor Type */ + uint8_t bFirstInterface; /**< Interface number of the first interface that is associated with this function */ + uint8_t bInterfaceCount; /**< Number of contiguous interfaces that are associated with this function */ + uint8_t bFunctionClass; /**< Class code (assigned by USB-IF) */ + uint8_t bFunctionSubClass; /**< Subclass code (assigned by USB-IF) */ + uint8_t bFunctionProtocol; /**< Protocol code (assigned by USB-IF) */ + uint8_t iFunction; /**< Index of string descriptor describing this function */ + } USB_DESC_ATTR; /**< USB descriptor attributes */ + uint8_t val[USB_IAD_DESC_SIZE]; /**< Descriptor value */ +} usb_iad_desc_t; +ESP_STATIC_ASSERT(sizeof(usb_iad_desc_t) == USB_IAD_DESC_SIZE, "Size of usb_iad_desc_t incorrect"); + +// ---------------- Interface Descriptor ------------------- + +/** + * @brief Size of a USB interface descriptor in bytes + */ +#define USB_INTF_DESC_SIZE 9 + +/** + * @brief Structure representing a USB interface descriptor + * + * See Table 9-12 of USB2.0 specification for more details + */ +typedef union { + struct { + uint8_t bLength; /**< Size of the descriptor in bytes */ + uint8_t bDescriptorType; /**< INTERFACE Descriptor Type */ + uint8_t bInterfaceNumber; /**< Number of this interface. */ + uint8_t bAlternateSetting; /**< Value used to select this alternate setting for the interface identified in the prior field */ + uint8_t bNumEndpoints; /**< Number of endpoints used by this interface (excluding endpoint zero). */ + uint8_t bInterfaceClass; /**< Class code (assigned by the USB-IF) */ + uint8_t bInterfaceSubClass; /**< Subclass code (assigned by the USB-IF) */ + uint8_t bInterfaceProtocol; /**< Protocol code (assigned by the USB) */ + uint8_t iInterface; /**< Index of string descriptor describing this interface */ + } USB_DESC_ATTR; /**< USB descriptor attributes */ + uint8_t val[USB_INTF_DESC_SIZE]; /**< Descriptor value */ +} usb_intf_desc_t; +ESP_STATIC_ASSERT(sizeof(usb_intf_desc_t) == USB_INTF_DESC_SIZE, "Size of usb_intf_desc_t incorrect"); + +// ----------------- Endpoint Descriptor ------------------- + +/** + * @brief Size of a USB endpoint descriptor in bytes + */ +#define USB_EP_DESC_SIZE 7 + +/** + * @brief Structure representing a USB endpoint descriptor + * + * See Table 9-13 of USB2.0 specification for more details + */ +typedef union { + struct { + uint8_t bLength; /**< Size of the descriptor in bytes */ + uint8_t bDescriptorType; /**< ENDPOINT Descriptor Type */ + uint8_t bEndpointAddress; /**< The address of the endpoint on the USB device described by this descriptor */ + uint8_t bmAttributes; /**< This field describes the endpoint’s attributes when it is configured using the bConfigurationValue. */ + uint16_t wMaxPacketSize; /**< Maximum packet size this endpoint is capable of sending or receiving when this configuration is selected. */ + uint8_t bInterval; /**< Interval for polling Isochronous and Interrupt endpoints. Expressed in frames or microframes depending on the device operating speed (1 ms for Low-Speed and Full-Speed or 125 us for USB High-Speed and above). */ + } USB_DESC_ATTR; /**< USB descriptor attributes */ + uint8_t val[USB_EP_DESC_SIZE]; /**< Descriptor value */ +} usb_ep_desc_t; +ESP_STATIC_ASSERT(sizeof(usb_ep_desc_t) == USB_EP_DESC_SIZE, "Size of usb_ep_desc_t incorrect"); + +/** + * @brief Bit masks belonging to the bEndpointAddress field of an endpoint descriptor + */ +#define USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK 0x0f +#define USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK 0x80 + +/** + * @brief Bit masks belonging to the wMaxPacketSize field of endpoint descriptor + */ +#define USB_W_MAX_PACKET_SIZE_MPS_MASK 0x07ff +#define USB_W_MAX_PACKET_SIZE_MULT_MASK 0x1800 + +/** + * @brief Bit masks belonging to the bmAttributes field of an endpoint descriptor + */ +#define USB_BM_ATTRIBUTES_XFERTYPE_MASK 0x03 +#define USB_BM_ATTRIBUTES_XFER_CONTROL (0 << 0) +#define USB_BM_ATTRIBUTES_XFER_ISOC (1 << 0) +#define USB_BM_ATTRIBUTES_XFER_BULK (2 << 0) +#define USB_BM_ATTRIBUTES_XFER_INT (3 << 0) +#define USB_BM_ATTRIBUTES_SYNCTYPE_MASK 0x0C /* in bmAttributes */ +#define USB_BM_ATTRIBUTES_SYNC_NONE (0 << 2) +#define USB_BM_ATTRIBUTES_SYNC_ASYNC (1 << 2) +#define USB_BM_ATTRIBUTES_SYNC_ADAPTIVE (2 << 2) +#define USB_BM_ATTRIBUTES_SYNC_SYNC (3 << 2) +#define USB_BM_ATTRIBUTES_USAGETYPE_MASK 0x30 +#define USB_BM_ATTRIBUTES_USAGE_DATA (0 << 4) +#define USB_BM_ATTRIBUTES_USAGE_FEEDBACK (1 << 4) +#define USB_BM_ATTRIBUTES_USAGE_IMPLICIT_FB (2 << 4) + +/** + * @brief Macro helpers to get information about an endpoint from its descriptor + */ +#define USB_EP_DESC_GET_XFERTYPE(desc_ptr) ((usb_transfer_type_t) ((desc_ptr)->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK)) +#define USB_EP_DESC_GET_EP_NUM(desc_ptr) ((desc_ptr)->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK) +#define USB_EP_DESC_GET_EP_DIR(desc_ptr) (((desc_ptr)->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK) ? 1 : 0) +#define USB_EP_DESC_GET_MPS(desc_ptr) ((desc_ptr)->wMaxPacketSize & USB_W_MAX_PACKET_SIZE_MPS_MASK) +#define USB_EP_DESC_GET_MULT(desc_ptr) (((desc_ptr)->wMaxPacketSize & USB_W_MAX_PACKET_SIZE_MULT_MASK) >> 11) +#define USB_EP_DESC_GET_SYNCTYPE(desc_ptr) (((desc_ptr)->bmAttributes & USB_BM_ATTRIBUTES_SYNCTYPE_MASK) >> 2) +#define USB_EP_DESC_GET_USAGETYPE(desc_ptr) (((desc_ptr)->bmAttributes & USB_BM_ATTRIBUTES_USAGETYPE_MASK) >> 4) + +// ------------------ String Descriptor -------------------- + +/** + * @brief Size of a short USB string descriptor in bytes + */ +#define USB_STR_DESC_SIZE 2 + +/** + * @brief Structure representing a USB string descriptor + */ +typedef union { + struct { + uint8_t bLength; /**< Size of the descriptor in bytes */ + uint8_t bDescriptorType; /**< STRING Descriptor Type */ + uint16_t wData[]; /**< UTF-16LE encoded */ + } USB_DESC_ATTR; /**< USB descriptor attributes */ + uint8_t val[USB_STR_DESC_SIZE]; /**< Descriptor value */ +} usb_str_desc_t; +ESP_STATIC_ASSERT(sizeof(usb_str_desc_t) == USB_STR_DESC_SIZE, "Size of usb_str_desc_t incorrect"); + +#ifdef __cplusplus +} +#endif diff --git a/components/usb/include/usb/usb_types_stack.h b/components/usb/include/usb/usb_types_stack.h new file mode 100644 index 0000000000..8b351063f4 --- /dev/null +++ b/components/usb/include/usb/usb_types_stack.h @@ -0,0 +1,200 @@ +/* + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* +Warning: The USB Host Library API is still a beta version and may be subject to change +*/ + +#pragma once + +#include +#include "usb/usb_types_ch9.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// ------------------------------------------------ Protocol Standard -------------------------------------------------- + +/** + * @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_HIGH, /**< USB High Speed (480 Mbit/s) */ +} usb_speed_t; + +/** + * @brief The type of USB transfer + * + * @note The enum values need to match the bmAttributes field of an EP descriptor + */ +typedef enum { + USB_TRANSFER_TYPE_CTRL = 0, + USB_TRANSFER_TYPE_ISOCHRONOUS, + USB_TRANSFER_TYPE_BULK, + USB_TRANSFER_TYPE_INTR, +} usb_transfer_type_t; + +// ------------------------------------------------- Device Related ---------------------------------------------------- + +/** + * @brief Handle of a USB Device connected to a USB Host + */ +typedef struct usb_device_handle_s *usb_device_handle_t; + +/** + * @brief Enumeration filter callback + * + * This callback is called at the beginning of the enumeration process for a newly attached device. + * Through this callback, users are able to: + * + * - filter which devices should be enumerated + * - select the configuration number to use when enumerating the device + * + * The device descriptor is passed to this callback to allow users to filter devices based on + * Vendor ID, Product ID, and class code. + * + * @attention This callback must be non-blocking + * @attention This callback must not submit any USB transfers + * @param[in] dev_desc Device descriptor of the device to enumerate + * @param[out] bConfigurationValue Configuration number to use when enumerating the device (starts with 1) + * + * @return bool + * - true: USB device will be enumerated + * - false: USB device will not be enumerated + */ +typedef bool (*usb_host_enum_filter_cb_t)(const usb_device_desc_t *dev_desc, uint8_t *bConfigurationValue); + +/** + * @brief Basic information of an enumerated device + */ +typedef struct { + usb_speed_t speed; /**< Device's speed */ + uint8_t dev_addr; /**< Device's address */ + uint8_t bMaxPacketSize0; /**< The maximum packet size of the device's default endpoint */ + uint8_t bConfigurationValue; /**< Device's current configuration number */ + const usb_str_desc_t *str_desc_manufacturer; /**< Pointer to Manufacturer string descriptor (can be NULL) */ + const usb_str_desc_t *str_desc_product; /**< Pointer to Product string descriptor (can be NULL) */ + const usb_str_desc_t *str_desc_serial_num; /**< Pointer to Serial Number string descriptor (can be NULL) */ +} usb_device_info_t; + +// ------------------------------------------------ Transfer Related --------------------------------------------------- + +/** + * @brief The status of a particular transfer + */ +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_CANCELED, /**< The transfer was canceled */ + USB_TRANSFER_STATUS_STALL, /**< The transfer was stalled */ + USB_TRANSFER_STATUS_OVERFLOW, /**< The transfer as more data was sent than was requested */ + USB_TRANSFER_STATUS_SKIPPED, /**< ISOC packets only. The packet was skipped due to system latency or bus overload */ + USB_TRANSFER_STATUS_NO_DEVICE, /**< The transfer failed because the target device is gone */ +} usb_transfer_status_t; + +/** + * @brief Isochronous packet descriptor + * + * If the number of bytes in an Isochronous transfer is larger than the MPS of the endpoint, the transfer is split + * into multiple packets transmitted at the endpoint's specified interval. An array of Isochronous packet descriptors + * describes how an Isochronous transfer should be split into multiple packets. + */ +typedef struct { + int num_bytes; /**< Number of bytes to transmit/receive in the packet. IN packets should be integer multiple of MPS */ + int actual_num_bytes; /**< Actual number of bytes transmitted/received in the packet */ + usb_transfer_status_t status; /**< Status of the packet */ +} usb_isoc_packet_desc_t; + +/** + * @brief USB transfer structure + * + * This structure is used to represent a transfer from a software client to an endpoint over the USB bus. Some of the + * fields are made const on purpose as they are fixed on allocation. Users should call the appropriate USB Host Library + * function to allocate a USB transfer structure instead of allocating this structure themselves. + * + * The transfer type is inferred from the endpoint this transfer is sent to. Depending on the transfer type, users + * should note the following: + * + * - Bulk: This structure represents a single bulk transfer. If the number of bytes exceeds the endpoint's MPS, the + * transfer will be split into multiple MPS sized packets followed by a short packet. + * - Control: This structure represents a single control transfer. This first 8 bytes of the data_buffer must be filled + * with the setup packet (see usb_setup_packet_t). The num_bytes field should be the total size of the + * transfer (i.e., size of setup packet + wLength). + * - Interrupt: Represents an interrupt transfer. If num_bytes exceeds the MPS of the endpoint, the transfer will be + * split into multiple packets, and each packet is transferred at the endpoint's specified interval. + * - Isochronous: Represents a stream of bytes that should be transferred to an endpoint at a fixed rate. The transfer + * is split into packets according to the each isoc_packet_desc. A packet is transferred at each interval + * of the endpoint. If an entire ISOC URB was transferred without error (skipped packets do not count as + * errors), the URB's overall status and the status of each packet descriptor will be updated, and the + * actual_num_bytes reflects the total bytes transferred over all packets. If the ISOC URB encounters an + * error, the entire URB is considered erroneous so only the overall status will updated. + * + * @note For Bulk/Control/Interrupt IN transfers, the num_bytes must be a integer multiple of the endpoint's MPS + * @note This structure should be allocated via usb_host_transfer_alloc() + * @note Once the transfer has be submitted, users should not modify the structure until the transfer has completed + */ +typedef struct usb_transfer_s usb_transfer_t; + +/** + * @brief USB transfer completion callback + */ +typedef void (*usb_transfer_cb_t)(usb_transfer_t *transfer); + +/** + * @brief USB transfer structure + * + */ +struct usb_transfer_s { + uint8_t *const data_buffer; /**< Pointer to data buffer */ + const size_t data_buffer_size; /**< Size of the data buffer in bytes */ + int num_bytes; /**< Number of bytes to transfer. + Control transfers should include the size of the setup packet. + Isochronous transfer should be the total transfer size of all packets. + For non-control IN transfers, num_bytes should be an integer multiple of MPS. */ + int actual_num_bytes; /**< Actual number of bytes transferred */ + uint32_t flags; /**< Transfer flags */ + usb_device_handle_t device_handle; /**< Device handle */ + uint8_t bEndpointAddress; /**< Endpoint Address */ + usb_transfer_status_t status; /**< Status of the transfer */ + uint32_t timeout_ms; /**< Timeout (in milliseconds) of the packet (currently not supported yet) */ + usb_transfer_cb_t callback; /**< Transfer callback */ + void *context; /**< Context variable for transfer to associate transfer with something */ + const int num_isoc_packets; /**< Only relevant to Isochronous. Number of service periods (i.e., intervals) to transfer data buffer over. */ + usb_isoc_packet_desc_t isoc_packet_desc[]; /**< Descriptors for each Isochronous packet */ +}; + +/** + * @brief Terminate Bulk/Interrupt OUT transfer with a zero length packet + * + * OUT transfers normally terminate when the Host has transferred the exact amount of data it needs to the device. + * However, for bulk and interrupt OUT transfers, if the transfer size just happened to be a multiple of MPS, it will be + * impossible to know the boundary between two consecutive transfers to the same endpoint. + * + * Therefore, this flag will cause the transfer to automatically add a zero length packet (ZLP) at the end of the + * transfer if the following conditions are met: + * - The target endpoint is a Bulk/Interrupt OUT endpoint (Host to device) + * - The transfer's length (i.e., transfer.num_bytes) is a multiple of the endpoint's MPS + * + * Otherwise, this flag has no effect. + * + * Users should check whether their target device's class requires a ZLP, as not all Bulk/Interrupt OUT endpoints + * require them. For example: + * - For MSC Bulk Only Transport class, the Host MUST NEVER send a ZLP. Bulk transfer boundaries are determined by the CBW and CSW instead + * - For CDC Ethernet, the Host MUST ALWAYS send a ZLP if a segment (i.e., a transfer) is a multiple of MPS (See 3.3.1 Segment Delineation) + * + * @note See USB2.0 specification 5.7.3 and 5.8.3 for more details + * @note IN transfers normally terminate when the Host as receive the exact amount of data it needs (must be multiple of MPS) + * or the endpoint sends a short packet to the Host + */ +#define USB_TRANSFER_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 */ + +#ifdef __cplusplus +} +#endif diff --git a/components/usb/maintainers.md b/components/usb/maintainers.md new file mode 100644 index 0000000000..8a0da5d23f --- /dev/null +++ b/components/usb/maintainers.md @@ -0,0 +1,143 @@ +# USB Host Stack Maintainers Notes + +This document is intended for 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. + +Note: Some static functions within the stack need to be called within critical sections. Therefore, the USB Host stack will prefix all such function names with an underscore (e.g., `_some_static_func()`) will need to be called in critical sections) + +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 Driver in `usb_host.h` and `usb_host.c` + +# DWC_OTG + +Details regarding the DWC_OTG peripheral are covered in the data book and programming guide. However, this section adds some extra notes regarding the hardware's behavior that are not specified / cannot be easily found in the data book or programming guide. + +## Implicit interrupt on short packet on INTERRUPT transfer. + +- An interrupt channel will trigger an interrupt one of its QTDs is a short packet (event if the QTD did not set the interrupt on complete bit). This implicit interrupt is used to notify the software that it should halt the channel to +stop the remainder of the interrupt transfer. + +## Channel interrupt on port errors + +- If there are one or more channels active, and a port error interrupt occurs (such as disconnection, over-current), the active channels will not have an interrupt. +- The channels will remain active (i.e., `HCCHAR.ChEna` will still be set) +- Normal methods of disabling the channel (via setting `HCCHAR.ChDis` and waiting for an interrupt) will not work for ISOC channels (the interrupt will never be generated). +- Therefore, on port errors, just treat all the channels as halted and treat their in-flight transfers as failed. The soft reset that occurs after will reset all the channel's registers. + +## Reset + +- When resetting, if a disconnect occurs while a reset condition is being held. The disconnect is not detected until the reset condition is released. Once released, a `USBH_HAL_PORT_EVENT_DISCONN` is generated. +- During a second reset (i.e., there is already an enabled device). + - Once the reset condition is asserted, a `USBH_HAL_PORT_EVENT_DISABLED` is generated + - Once released, a `USBH_HAL_PORT_EVENT_ENABLED` is generated again. + +# 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 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 + +# 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 a single 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". URBs (USB Request Block) represent a USB transfer that 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. + +## HCD Port + +- An HCD port can be thought of 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 the following port events occur, 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 must be freed before the port can be recovered. + - `HCD_PORT_EVENT_DISCONNECTION` + - `HCD_PORT_EVENT_ERROR` + - `HCD_PORT_EVENT_OVERCURRENT` +- The port's internal FIFOs (RX, Periodic TX, Non-periodic TX) must be after each port reset (a port reset seems to clear the FIFO sizing registers). The sizing of these FIFOs will affect the largest supported MPS of endpoints using that FIFO. For convenience, the HCD provides the `hcd_port_fifo_bias_t` enum that will set the FIFO sizes for you but biased towards a particular use case. 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`. + - The FIFO sizes will be set on port initialization (supplied in `hcd_port_config_t`) + - FIFOs can be resized after port reset using `hcd_port_set_fifo_bias()` but some restrictions apply (see API description). + +## 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. +- URBs can be enqueued into a pipe. Pipes use a linked list internally, so there is (in theory) no limit to the number of URBs that can be enqueued. +- URBs need to be dequeued once they are completed. +- URBs need to have the transfer information (such as data buffer, transfer length in bytes) filled before they should be enqueued. +- URBs will be owned by the HCD until they are dequeued. Thus, users should not attempt to modify a URB object (and the URB's data buffer) until the URB is dequeued. +- The URB is defined in `usb_private.h` instead of `hcd.h` so that the same structure can shared throughout the entire Host stack. Each layer simply needs to pass the pointer of the URB 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 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 (with their own handles and events). This allows clients 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 to 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 to 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. +- Blocking HCD functions should not be called from critical sections and interrupts (e.g., `hcd_port_command()` and `hcd_pipe_command()`). \ No newline at end of file diff --git a/components/usb/private_include/hcd.h b/components/usb/private_include/hcd.h new file mode 100644 index 0000000000..1fd3617307 --- /dev/null +++ b/components/usb/private_include/hcd.h @@ -0,0 +1,518 @@ +/* + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include "esp_err.h" +#include "usb_private.h" +#include "usb/usb_types_ch9.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 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. */ + 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. URBs can be enqueued. + * - Even if pipe has no URBs enqueued, it can still be in the active state. + * Halted: + * - An error has occurred on the pipe. URBs will no longer be executed. + * - Halt should be cleared using the HCD_PIPE_CMD_CLEAR command + */ +typedef enum { + HCD_PIPE_STATE_ACTIVE, /**< The pipe is active */ + HCD_PIPE_STATE_HALTED, /**< The pipe is halted */ +} 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 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_t; + +/** + * @brief HCD pipe events + * + * @note Pipe error events will put the pipe into the HCD_PIPE_STATE_HALTED state + */ +typedef enum { + HCD_PIPE_EVENT_NONE, /**< The pipe has no events (used to indicate no events when polling) */ + HCD_PIPE_EVENT_URB_DONE, /**< The pipe has completed an URB. The URB can be dequeued */ + HCD_PIPE_EVENT_ERROR_XFER, /**< Excessive (three consecutive) transaction errors (e.g., no ACK, bad CRC etc) */ + HCD_PIPE_EVENT_ERROR_URB_NOT_AVAIL, /**< URB 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. If the port is enabled, this will cause a HCD_PORT_EVENT_SUDDEN_DISCONN event. + If the port is disabled, this will cause a HCD_PORT_EVENT_DISCONNECTION event. */ + HCD_PORT_CMD_RESET, /**< Issue a reset on the port */ + HCD_PORT_CMD_SUSPEND, /**< Suspend the port. All pipes must be halted */ + HCD_PORT_CMD_RESUME, /**< Resume the port */ + HCD_PORT_CMD_DISABLE, /**< Disable the port (stops the SOFs or keep alive). All pipes must be halted. */ +} 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_HALT, /**< Halt an active pipe. The currently executing URB will be canceled. Enqueued URBs are left untouched */ + HCD_PIPE_CMD_FLUSH, /**< Can only be called when halted. Will cause all enqueued URBs to be canceled */ + HCD_PIPE_CMD_CLEAR, /**< Causes a halted pipe to become active again. Any enqueued URBs will being executing.*/ +} 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 Port event callback type + * + * This callback is run when a port event occurs + */ +typedef bool (*hcd_port_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_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 + */ +typedef struct { + int intr_flags; /**< Interrupt flags for HCD interrupt */ +} hcd_config_t; + +/** + * @brief Port configuration structure + */ +typedef struct { + hcd_port_fifo_bias_t fifo_bias; /**< HCD port internal FIFO biasing */ + hcd_port_callback_t callback; /**< HCD port event callback */ + void *callback_arg; /**< User argument for HCD port callback */ + void *context; /**< Context variable used to associate the port with upper layer object */ +} 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_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 */ + const usb_ep_desc_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 ------------------------------------------------ + +/** + * @brief Installs the Host Controller Driver + * + * - Allocates memory and interrupt resources for the HCD and underlying ports + * + * @note This function must be called before any other HCD function is called + * @note Before calling this function, the Host Controller must already be un-clock gated and reset. The USB PHY + * (internal or external, and associated GPIOs) must already be configured. + * + * @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 + * + * @note This function will simply free the resources used by the HCD. The underlying Host Controller and USB PHY will + * not be disabled. + * + * @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, const 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 suspended 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 The function 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 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. + * + * @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_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 ------------------------------------------------------- + +/** + * @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 The host port must be in the enabled state before a pipe can be allocated + * + * @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'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); + +/** + * @brief Get maximum packet size (mps) of HCD pipe + * + * @param[in] port_hdl Pipe handle + * + * @retval HCD pipe mps + */ +int hcd_pipe_get_mps(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 URBs 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 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 not current processing a command + * - Pipe does not have any enqueued URBs + * - Port cannot be resetting + * + * @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. This function can only be called on a pipe that has met the following conditions: + * - Pipe is not current processing a command + * - Pipe does not have any enqueued URBs + * - Port cannot be resetting + * + * @param pipe_hdl Pipe handle + * @param dev_addr New device address + * + * @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_dev_addr(hcd_pipe_handle_t pipe_hdl, uint8_t dev_addr); + +/** + * @brief Get the context variable of a pipe from its handle + * + * @param pipe_hdl Pipe handle + * @return void* Context variable + */ +void *hcd_pipe_get_context(hcd_pipe_handle_t pipe_hdl); + +/** + * @brief Get the current state 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 Get the number of in-flight URBs in the pipe + * + * Returns the current number of URBs that have been enqueued (via hcd_urb_enqueue()) and have yet to be dequeued (via + * hcd_urb_dequeue()). + * + * @param pipe_hdl Pipe handle + * @return Number of in-flight URBs + */ +unsigned int hcd_pipe_get_num_urbs(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 URBs etc) + * + * @note This function can block + * + * @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 + */ +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 URBs ------------------------------------------------------- + +/** + * @brief Enqueue an URB to a particular pipe + * + * The following conditions must be met before an URB can be enqueued: + * - The URB is properly initialized (data buffer and transfer length are set) + * - The URB must not already be enqueued + * - The pipe must be in the HCD_PIPE_STATE_ACTIVE state + * - The pipe cannot be executing a command + * + * @param pipe_hdl Pipe handle + * @param urb URB to enqueue + * @retval ESP_OK: URB enqueued successfully + * @retval ESP_ERR_INVALID_STATE: Conditions not met to enqueue URB + */ +esp_err_t hcd_urb_enqueue(hcd_pipe_handle_t pipe_hdl, urb_t *urb); + +/** + * @brief Dequeue an URB from a particular pipe + * + * This function should be called on a pipe after a pipe receives a HCD_PIPE_EVENT_URB_DONE event. If a pipe has + * multiple URBs that can be dequeued, this function should be called repeatedly until all URBs are dequeued. If a pipe + * has no more URBs to dequeue, this function will return NULL. + * + * @param pipe_hdl Pipe handle + * @return urb_t* Dequeued URB, or NULL if no more URBs to dequeue + */ +urb_t *hcd_urb_dequeue(hcd_pipe_handle_t pipe_hdl); + +/** + * @brief Abort an enqueued URB + * + * This function will attempt to abort an URB that is already enqueued. If the URB has yet to be executed, it will be + * "canceled" and can then be dequeued. If the URB is currently in-flight or has already completed, the URB will not be + * affected by this function. + * + * @param urb URB to abort + * @retval ESP_OK: URB successfully aborted, or was not affected by this function + * @retval ESP_ERR_INVALID_STATE: URB was never enqueued + */ +esp_err_t hcd_urb_abort(urb_t *urb); + +#ifdef __cplusplus +} +#endif diff --git a/components/usb/private_include/hub.h b/components/usb/private_include/hub.h new file mode 100644 index 0000000000..cf85ad861d --- /dev/null +++ b/components/usb/private_include/hub.h @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "sdkconfig.h" +#include "esp_err.h" +#include "usb_private.h" +#include "usbh.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// ------------------------------------------------------ Types -------------------------------------------------------- + +/** + * @brief Hub driver configuration + */ +typedef struct { + usb_proc_req_cb_t proc_req_cb; /**< Processing request callback */ + void *proc_req_cb_arg; /**< Processing request callback argument */ +#ifdef CONFIG_USB_HOST_ENABLE_ENUM_FILTER_CALLBACK + usb_host_enum_filter_cb_t enum_filter_cb; /**< Set device configuration callback */ +#endif // CONFIG_USB_HOST_ENABLE_ENUM_FILTER_CALLBACK +} hub_config_t; + +// ---------------------------------------------- Hub Driver Functions ------------------------------------------------- + +/** + * @brief Install Hub driver + * + * Entry: + * - USBH must already be installed + * Exit: + * - Install Hub driver memory resources + * - Initializes the HCD root port + * + * @param[in] hub_config Hub driver configuration + * @param[out] client_ret Unique pointer to identify the Hub as a USB Host client + * @return esp_err_t + */ +esp_err_t hub_install(hub_config_t *hub_config, void **client_ret); + +/** + * @brief Uninstall Hub driver + * + * This must be called before uninstalling the USBH + * Entry: + * - Must have stopped the root port + * Exit: + * - HCD root port deinitialized + * + * @return esp_err_t + */ +esp_err_t hub_uninstall(void); + +/** + * @brief Start the Hub driver's root port + * + * This will power the root port ON + * + * @return esp_err_t + */ +esp_err_t hub_root_start(void); + +/** + * @brief Stops the Hub driver's root port + * + * This will power OFF the root port + * + * @return esp_err_t + */ +esp_err_t hub_root_stop(void); + +/** + * @brief Indicate to the Hub driver that a device's port can be recycled + * + * The device connected to the port has been freed. The Hub driver can now + * recycled the port. + * + * @param dev_uid Device's unique ID + * @return + * - ESP_OK: Success + */ +esp_err_t hub_port_recycle(unsigned int dev_uid); + +/** + * @brief Hub driver's processing function + * + * Hub driver handling function that must be called repeatdly to process the Hub driver's events. If blocking, the + * caller can block on the notification callback of source USB_PROC_REQ_SOURCE_HUB to run this function. + * + * @return esp_err_t + */ +esp_err_t hub_process(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/usb/private_include/usb_private.h b/components/usb/private_include/usb_private.h new file mode 100644 index 0000000000..4fb25cc4ee --- /dev/null +++ b/components/usb/private_include/usb_private.h @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include "usb/usb_types_ch9.h" +#include "usb/usb_types_stack.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// ------------------------------------------------------ Types -------------------------------------------------------- + +typedef struct { + uint8_t *data_buffer; + size_t data_buffer_size; + int num_bytes; + int actual_num_bytes; + uint32_t flags; + usb_device_handle_t device_handle; + uint8_t bEndpointAddress; + usb_transfer_status_t status; + uint32_t timeout; + usb_transfer_cb_t callback; + void *context; + int num_isoc_packets; + usb_isoc_packet_desc_t isoc_packet_desc[0]; +} usb_transfer_dummy_t; +_Static_assert(sizeof(usb_transfer_dummy_t) == sizeof(usb_transfer_t), "usb_transfer_dummy_t does not match usb_transfer_t"); + +struct urb_s { + TAILQ_ENTRY(urb_s) tailq_entry; + // HCD Layer: Handler pointer and variables. Must be initialized to NULL and 0 respectively + void *hcd_ptr; + uint32_t hcd_var; + // Host Lib Layer: + void *usb_host_client; // Currently only used when submitted to shared pipes (i.e., Device default pipes) + bool usb_host_inflight; // Debugging variable, used to prevent re-submitting URBs already inflight + // Public transfer structure. Must be last due to variable length array + usb_transfer_t transfer; +}; +typedef struct urb_s urb_t; + +/** + * @brief Processing request source + * + * Enum to indicate which layer of the USB Host stack requires processing. The main handling loop should then call that + * layer's processing function (i.e., xxx_process()). + */ +typedef enum { + USB_PROC_REQ_SOURCE_USBH = 0x01, + USB_PROC_REQ_SOURCE_HUB = 0x02, +} usb_proc_req_source_t; + +/** + * @brief Processing request callback + * + * Callback function provided to each layer of the USB Host stack so that each layer can request calls to their + * processing function. + */ +typedef bool (*usb_proc_req_cb_t)(usb_proc_req_source_t source, bool in_isr, void *context); + +// --------------------------------------------------- Allocation ------------------------------------------------------ + +/** + * @brief Allocate a URB + * + * - Data buffer is allocated in DMA capable memory + * - The constant fields of the URB are also set + * - The data_buffer field of the URB is set to point to start of the allocated data buffer. + * + * @param data_buffer_size Size of the URB's data buffer + * @param num_isoc_packets Number of isochronous packet descriptors + * @return urb_t* URB object + */ +urb_t *urb_alloc(size_t data_buffer_size, int num_isoc_packets); + +/** + * @brief Free a URB + * + * @param urb URB object + */ +void urb_free(urb_t *urb); + +#ifdef __cplusplus +} +#endif diff --git a/components/usb/private_include/usbh.h b/components/usb/private_include/usbh.h new file mode 100644 index 0000000000..a1b5b2a845 --- /dev/null +++ b/components/usb/private_include/usbh.h @@ -0,0 +1,521 @@ +/* + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "hcd.h" +#include "usb/usb_types_ch9.h" +#include "usb/usb_types_stack.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// ------------------------------------------------------ Types -------------------------------------------------------- + +// ----------------------- Handles ------------------------- + +/** + * @brief Handle of a allocated endpoint + */ +typedef struct usbh_ep_handle_s *usbh_ep_handle_t; + +// ----------------------- Events -------------------------- + +/** + * @brief Enumerator for various USBH events + */ +typedef enum { + USBH_EVENT_CTRL_XFER, /**< A control transfer has completed */ + USBH_EVENT_NEW_DEV, /**< A new device has been enumerated and added to the device pool */ + USBH_EVENT_DEV_GONE, /**< A device is gone. Clients should close the device */ + USBH_EVENT_DEV_FREE, /**< A device has been freed. Its upstream port can now be recycled */ + USBH_EVENT_ALL_FREE, /**< All devices have been freed */ +} usbh_event_t; + +/** + * @brief Event data object for USBH events + */ +typedef struct { + usbh_event_t event; + union { + struct { + usb_device_handle_t dev_hdl; + urb_t *urb; + } ctrl_xfer_data; + struct { + uint8_t dev_addr; + } new_dev_data; + struct { + uint8_t dev_addr; + usb_device_handle_t dev_hdl; + } dev_gone_data; + struct { + unsigned int dev_uid; + } dev_free_data; + }; +} usbh_event_data_t; + +/** + * @brief Endpoint events + * + * @note Optimization: Keep this identical to hcd_pipe_event_t + */ +typedef enum { + USBH_EP_EVENT_NONE, /**< The EP has no events (used to indicate no events when polling) */ + USBH_EP_EVENT_URB_DONE, /**< The EP has completed a URB. The URB can be dequeued */ + USBH_EP_EVENT_ERROR_XFER, /**< The EP encountered excessive errors when transferring a URB i.e., three three consecutive transaction errors (e.g., no ACK, bad CRC etc) */ + USBH_EP_EVENT_ERROR_URB_NOT_AVAIL, /**< The EP tried to execute a transfer but no URB was available */ + USBH_EP_EVENT_ERROR_OVERFLOW, /**< The EP received more data than requested. Usually a Packet babble error (i.e., an IN packet has exceeded the EP's MPS) */ + USBH_EP_EVENT_ERROR_STALL, /**< EP received a STALL response */ +} usbh_ep_event_t; + +// ------------------ Requests/Commands -------------------- + +/** + * @brief Endpoint commands + * + * @note Optimization: Keep this identical to hcd_pipe_cmd_t + */ +typedef enum { + USBH_EP_CMD_HALT, /**< Halt an active endpoint. Any currently executing URB will be canceled. Enqueued URBs are left untouched */ + USBH_EP_CMD_FLUSH, /**< Can only be called when halted. Will cause all enqueued URBs to be canceled */ + USBH_EP_CMD_CLEAR, /**< Causes a halted endpoint to become active again. Any enqueued URBs will being executing.*/ +} usbh_ep_cmd_t; + +// ---------------------- Callbacks ------------------------ + +/** + * @brief Callback used to indicate that the USBH has an event + * + * @note This callback is called from within usbh_process() + */ +typedef void (*usbh_event_cb_t)(usbh_event_data_t *event_data, void *arg); + +/** + * @brief Callback used to indicate an event on an endpoint + * + * Return whether to yield or not if called from an ISR. Always return false if not called from an ISR + */ +typedef bool (*usbh_ep_cb_t)(usbh_ep_handle_t ep_hdl, usbh_ep_event_t ep_event, void *arg, bool in_isr); + +// ----------------------- Objects ------------------------- + +/** + * @brief Configuration for an endpoint being allocated using usbh_ep_alloc() + */ +typedef struct { + uint8_t bInterfaceNumber; /**< Interface number */ + uint8_t bAlternateSetting; /**< Alternate setting number of the interface */ + uint8_t bEndpointAddress; /**< Endpoint address */ + usbh_ep_cb_t ep_cb; /**< Endpoint event callback */ + void *ep_cb_arg; /**< Endpoint callback argument */ + void *context; /**< Endpoint context */ +} usbh_ep_config_t; + +/** + * @brief USBH configuration used in usbh_install() + */ +typedef struct { + usb_proc_req_cb_t proc_req_cb; /**< Processing request callback */ + void *proc_req_cb_arg; /**< Processing request callback argument */ + usbh_event_cb_t event_cb; /**< USBH event callback */ + void *event_cb_arg; /**< USBH event callback argument */ +} usbh_config_t; + +// -------------------------------------------- USBH Processing Functions ---------------------------------------------- + +/** + * @brief Installs the USBH driver + * + * - This function will internally install the HCD + * - This must be called before calling any Hub driver functions + * + * @note Before calling this function, the Host Controller must already be un-clock gated and reset. The USB PHY + * (internal or external, and associated GPIOs) must already be configured. + * @param usbh_config USBH driver configuration + * @return esp_err_t + */ +esp_err_t usbh_install(const usbh_config_t *usbh_config); + +/** + * @brief Uninstall the USBH driver + * + * - This function will uninstall the HCD + * - The Hub driver must be uninstalled before calling this function + * + * @note This function will simply free the resources used by the USBH. The underlying Host Controller and USB PHY will + * not be disabled. + * @return esp_err_t + */ +esp_err_t usbh_uninstall(void); + +/** + * @brief USBH processing function + * + * - USBH processing function that must be called repeatedly to process USBH events + * - If blocking, the caller can block until the proc_req_cb() is called with USB_PROC_REQ_SOURCE_USBH as the request + * source. The USB_PROC_REQ_SOURCE_USBH source indicates that this function should be called. + * + * @note This function can block + * @return esp_err_t + */ +esp_err_t usbh_process(void); + +// ---------------------------------------------- Device Pool Functions ------------------------------------------------ + +/** + * @brief Get the current number of devices + * + * @note This function can block + * @param[out] num_devs_ret Current number of devices + * @return esp_err_t + */ +esp_err_t usbh_devs_num(int *num_devs_ret); + +/** + * @brief Fill list with address of currently connected devices + * + * - This function fills the provided list with the address of current connected devices + * - Device address can then be used in usbh_devs_open() + * - If there are more devices than the list_len, this function will only fill + * up to list_len number of devices. + * + * @param[in] list_len Length of empty list + * @param[inout] dev_addr_list Empty list to be filled + * @param[out] num_dev_ret Number of devices filled into list + * @return esp_err_t + */ +esp_err_t usbh_devs_addr_list_fill(int list_len, uint8_t *dev_addr_list, int *num_dev_ret); + +/** + * @brief Create a device and add it to the device pool + * + * The created device will not be enumerated where the device's address is 0, + * device and config descriptor are NULL. The device will still have a default + * pipe, thus allowing control transfers to be submitted. + * + * - Call usbh_devs_open() before communicating with the device + * - Call usbh_dev_enum_lock() before enumerating the device via the various + * usbh_dev_set_...() functions. + * + * @param[in] uid Unique ID assigned to the device + * @param[in] dev_speed Device's speed + * @param[in] port_hdl Handle of the port that the device is connected to + * @return esp_err_t + */ +esp_err_t usbh_devs_add(unsigned int uid, usb_speed_t dev_speed, hcd_port_handle_t port_hdl); + +/** + * @brief Indicates to the USBH that a device is gone + * + * @param[in] uid Unique ID assigned to the device on creation (see 'usbh_devs_add()') + * @return esp_err_t + */ +esp_err_t usbh_devs_remove(unsigned int uid); + +/** + * @brief Mark that all devices should be freed at the next possible opportunity + * + * A device marked as free will not be freed until the last client using the device has called usbh_devs_close() + * + * @return + * - ESP_OK: There were no devices to free to begin with. Current state is all free + * - ESP_ERR_NOT_FINISHED: One or more devices still need to be freed (but have been marked "to be freed") + */ +esp_err_t usbh_devs_mark_all_free(void); + +/** + * @brief Open a device by address + * + * A device must be opened before it can be used + * + * @param[in] dev_addr Device address + * @param[out] dev_hdl Device handle + * @return esp_err_t + */ +esp_err_t usbh_devs_open(uint8_t dev_addr, usb_device_handle_t *dev_hdl); + +/** + * @brief CLose a device + * + * Device can be opened by calling usbh_devs_open() + * + * @param[in] dev_hdl Device handle + * @return esp_err_t + */ +esp_err_t usbh_devs_close(usb_device_handle_t dev_hdl); + +/** + * @brief Trigger a USBH_EVENT_NEW_DEV event for the device + * + * This is typically called after a device has been fully enumerated. + * + * @param[in] dev_hdl Device handle + * @return esp_err_t + */ +esp_err_t usbh_devs_new_dev_event(usb_device_handle_t dev_hdl); + +// ------------------------------------------------ Device Functions --------------------------------------------------- + +// ----------------------- Getters ------------------------- + +/** + * @brief Get a device's address + * + * @note Can be called without opening the device + * + * @param[in] dev_hdl Device handle + * @param[out] dev_addr Device's address + * @return esp_err_t + */ +esp_err_t usbh_dev_get_addr(usb_device_handle_t dev_hdl, uint8_t *dev_addr); + +/** + * @brief Get a device's information + * + * @note It is possible that the device has not been enumerated yet, thus some + * fields may be NULL. + * @param[in] dev_hdl Device handle + * @param[out] dev_info Device information + * @return esp_err_t + */ +esp_err_t usbh_dev_get_info(usb_device_handle_t dev_hdl, usb_device_info_t *dev_info); + +/** + * @brief Get a device's device descriptor + * + * - The device descriptor is cached when the device is created by the Hub driver + * + * @note It is possible that the device has not been enumerated yet, thus the + * device descriptor could be NULL. + * @param[in] dev_hdl Device handle + * @param[out] dev_desc_ret Device descriptor + * @return esp_err_t + */ +esp_err_t usbh_dev_get_desc(usb_device_handle_t dev_hdl, const usb_device_desc_t **dev_desc_ret); + +/** + * @brief Get a device's active configuration descriptor + * + * Simply returns a reference to the internally cached configuration descriptor + * + * @note It is possible that the device has not been enumerated yet, thus the + * configuration descriptor could be NULL. + * @param[in] dev_hdl Device handle + * @param config_desc_ret + * @return esp_err_t + */ +esp_err_t usbh_dev_get_config_desc(usb_device_handle_t dev_hdl, const usb_config_desc_t **config_desc_ret); + +// ----------------------- Setters ------------------------- + +/** + * @brief Lock a device for enumeration + * + * - A device's enumeration lock must be set before any of its enumeration fields + * (e.g., address, device/config descriptors) can be set/updated. + * - The caller must be the sole opener of the device (see 'usbh_devs_open()') + * when locking the device for enumeration. + * + * @param[in] dev_hdl Device handle + * @return esp_err_t + */ +esp_err_t usbh_dev_enum_lock(usb_device_handle_t dev_hdl); + +/** + * @brief Release a device's enumeration lock + * + * @param[in] dev_hdl Device handle + * @return esp_err_t + */ +esp_err_t usbh_dev_enum_unlock(usb_device_handle_t dev_hdl); + +/** + * @brief Set the maximum packet size of EP0 for a device + * + * Typically called during enumeration after obtaining the first 8 bytes of the + * device's descriptor. + * + * @note The device's enumeration lock must be set before calling this function + * (see 'usbh_dev_enum_lock()') + * @param[in] dev_hdl Device handle + * @param[in] wMaxPacketSize Maximum packet size + * @return esp_err_t + */ +esp_err_t usbh_dev_set_ep0_mps(usb_device_handle_t dev_hdl, uint16_t wMaxPacketSize); + +/** + * @brief Set a device's address + * + * Typically called during enumeration after a SET_ADDRESSS request has be + * sent to the device. + * + * @note The device's enumeration lock must be set before calling this function + * (see 'usbh_dev_enum_lock()') + * @param[in] dev_hdl Device handle + * @param[in] dev_addr + * @return esp_err_t + */ +esp_err_t usbh_dev_set_addr(usb_device_handle_t dev_hdl, uint8_t dev_addr); + +/** + * @brief Set a device's descriptor + * + * Typically called during enumeration after obtaining the device's descriptor + * via a GET_DESCRIPTOR request. + * + * @note The device's enumeration lock must be set before calling this function + * (see 'usbh_dev_enum_lock()') + * @param[in] dev_hdl Device handle + * @param[in] device_desc Device descriptor to copy + * @return esp_err_t + */ +esp_err_t usbh_dev_set_desc(usb_device_handle_t dev_hdl, const usb_device_desc_t *device_desc); + +/** + * @brief Set a device's configuration descriptor + * + * Typically called during enumeration after obtaining the device's configuration + * descriptor via a GET_DESCRIPTOR request. + * + * @note The device's enumeration lock must be set before calling this function + * (see 'usbh_dev_enum_lock()') + * @param[in] dev_hdl Device handle + * @param[in] config_desc_full Configuration descriptor to copy + * @return esp_err_t + */ +esp_err_t usbh_dev_set_config_desc(usb_device_handle_t dev_hdl, const usb_config_desc_t *config_desc_full); + +/** + * @brief Set a device's string descriptor + * + * Typically called during enumeration after obtaining one of the device's string + * descriptor via a GET_DESCRIPTOR request. + * + * @note The device's enumeration lock must be set before calling this function + * (see 'usbh_dev_enum_lock()') + * @param[in] dev_hdl Device handle + * @param[in] str_desc String descriptor to copy + * @param[in] select Select string descriptor. 0/1/2 for Manufacturer/Product/Serial + * Number string descriptors respectively + * @return esp_err_t + */ +esp_err_t usbh_dev_set_str_desc(usb_device_handle_t dev_hdl, const usb_str_desc_t *str_desc, int select); + +// ----------------------------------------------- Endpoint Functions ------------------------------------------------- + +/** + * @brief Allocate an endpoint on a device + * + * This function allows clients to allocate a non-default endpoint (i.e., not EP0) on a connected device + * + * - A client must have opened the device using usbh_devs_open() before attempting to allocate an endpoint on the device + * - A client should call this function to allocate all endpoints in an interface that the client has claimed. + * - A client must allocate an endpoint using this function before attempting to communicate with it + * - Once the client allocates an endpoint, the client is now owns/manages the endpoint. No other client should use or + * deallocate the endpoint. + * + * @note This function can block + * @note Default endpoints (EP0) are owned by the USBH. For control transfers, use usbh_dev_submit_ctrl_urb() instead + * + * @param[in] dev_hdl Device handle + * @param[in] ep_config Endpoint configuration + * @param[out] ep_hdl_ret Endpoint handle + * @return esp_err_t + */ +esp_err_t usbh_ep_alloc(usb_device_handle_t dev_hdl, usbh_ep_config_t *ep_config, usbh_ep_handle_t *ep_hdl_ret); + +/** + * @brief Free and endpoint on a device + * + * This function frees an endpoint previously allocated by the client using usbh_ep_alloc() + * + * - Only the client that allocated the endpoint should free it + * - The client must have halted and flushed the endpoint using usbh_ep_command() before attempting to free it + * - The client must ensure that there are no more function calls to the endpoint before freeing it + * + * @note This function can block + * @param[in] ep_hdl Endpoint handle + * @return esp_err_t + */ +esp_err_t usbh_ep_free(usbh_ep_handle_t ep_hdl); + +/** + * @brief Get the handle of an endpoint using its address + * + * The endpoint must have been previously allocated using usbh_ep_alloc() + * + * @param[in] dev_hdl Device handle + * @param[in] bEndpointAddress Endpoint address + * @param[out] ep_hdl_ret Endpoint handle + * @return esp_err_t + */ +esp_err_t usbh_ep_get_handle(usb_device_handle_t dev_hdl, uint8_t bEndpointAddress, usbh_ep_handle_t *ep_hdl_ret); + +/** + * @brief Execute a command on a particular endpoint + * + * Endpoint commands allows executing a certain action on an endpoint (e.g., halting, flushing, clearing etc) + * + * @param[in] ep_hdl Endpoint handle + * @param[in] command Endpoint command + * @return esp_err_t + */ +esp_err_t usbh_ep_command(usbh_ep_handle_t ep_hdl, usbh_ep_cmd_t command); + +/** + * @brief Get the context of an endpoint + * + * Get the context variable assigned to and endpoint on allocation. + * + * @note This function can block + * @param[in] ep_hdl Endpoint handle + * @return Endpoint context + */ +void *usbh_ep_get_context(usbh_ep_handle_t ep_hdl); + +// ----------------------------------------------- Transfer Functions -------------------------------------------------- + +/** + * @brief Submit a control transfer (URB) to a device + * + * @param[in] dev_hdl Device handle + * @param[in] urb URB + * @return esp_err_t + */ +esp_err_t usbh_dev_submit_ctrl_urb(usb_device_handle_t dev_hdl, urb_t *urb); + +/** + * @brief Enqueue a URB to an endpoint + * + * The URB will remain enqueued until it completes (successfully or errors out). Use usbh_ep_dequeue_urb() to dequeue + * a completed URB. + * + * @param[in] ep_hdl Endpoint handle + * @param[in] urb URB to enqueue + * @return esp_err_t + */ +esp_err_t usbh_ep_enqueue_urb(usbh_ep_handle_t ep_hdl, urb_t *urb); + +/** + * @brief Dequeue a URB from an endpoint + * + * Dequeue a completed URB from an endpoint. The USBH_EP_EVENT_URB_DONE indicates that URBs can be dequeued + * + * @param[in] ep_hdl Endpoint handle + * @param[out] urb_ret Dequeued URB, or NULL if no more URBs to dequeue + * @return esp_err_t + */ +esp_err_t usbh_ep_dequeue_urb(usbh_ep_handle_t ep_hdl, urb_t **urb_ret); + +#ifdef __cplusplus +} +#endif diff --git a/components/usb/usb_helpers.c b/components/usb/usb_helpers.c new file mode 100644 index 0000000000..08c50e3b87 --- /dev/null +++ b/components/usb/usb_helpers.c @@ -0,0 +1,321 @@ +/* + * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include "usb/usb_helpers.h" +#include "usb/usb_types_ch9.h" +#include "esp_check.h" +#include "usb/usb_host.h" + +// ---------------------------------------- Configuration Descriptor Parsing ------------------------------------------- + +const usb_standard_desc_t *usb_parse_next_descriptor(const usb_standard_desc_t *cur_desc, uint16_t wTotalLength, int *offset) +{ + assert(cur_desc != NULL && offset != NULL); + if (*offset >= wTotalLength) { + return NULL; // We have traversed the entire configuration descriptor + } + if (*offset + cur_desc->bLength >= wTotalLength) { + return NULL; // Next descriptor is out of bounds + } + // Return the next descriptor, update offset + const usb_standard_desc_t *ret_desc = (const usb_standard_desc_t *)(((uint32_t)cur_desc) + cur_desc->bLength); + *offset += cur_desc->bLength; + return ret_desc; +} + +const usb_standard_desc_t *usb_parse_next_descriptor_of_type(const usb_standard_desc_t *cur_desc, uint16_t wTotalLength, uint8_t bDescriptorType, int *offset) +{ + assert(cur_desc != NULL && offset != NULL); + int offset_temp = *offset; // We only want to update offset if we've actually found a descriptor + // Keep stepping over descriptors until we find one of bDescriptorType or until we go out of bounds + const usb_standard_desc_t *ret_desc = usb_parse_next_descriptor(cur_desc, wTotalLength, &offset_temp); + while (ret_desc != NULL) { + if (ret_desc->bDescriptorType == bDescriptorType) { + break; + } + ret_desc = usb_parse_next_descriptor(ret_desc, wTotalLength, &offset_temp); + } + if (ret_desc != NULL) { + // We've found a descriptor. Update the offset + *offset = offset_temp; + } + return ret_desc; +} + +int usb_parse_interface_number_of_alternate(const usb_config_desc_t *config_desc, uint8_t bInterfaceNumber) +{ + assert(config_desc != NULL); + int offset = 0; + // Find the first interface descriptor of bInterfaceNumber + const usb_intf_desc_t *first_intf_desc = usb_parse_interface_descriptor(config_desc, bInterfaceNumber, 0, &offset); + if (first_intf_desc == NULL) { + return -1; // bInterfaceNumber not found + } + + int num_alt_setting = 0; + const usb_intf_desc_t *next_intf_desc = (const usb_intf_desc_t *)usb_parse_next_descriptor_of_type((const usb_standard_desc_t *)first_intf_desc, config_desc->wTotalLength, USB_B_DESCRIPTOR_TYPE_INTERFACE, &offset); + while (next_intf_desc != NULL) { + if (next_intf_desc->bInterfaceNumber != bInterfaceNumber) { + break; + } + num_alt_setting++; + next_intf_desc = (const usb_intf_desc_t *)usb_parse_next_descriptor_of_type((const usb_standard_desc_t *)next_intf_desc, config_desc->wTotalLength, USB_B_DESCRIPTOR_TYPE_INTERFACE, &offset); + } + return num_alt_setting; +} + +const usb_intf_desc_t *usb_parse_interface_descriptor(const usb_config_desc_t *config_desc, uint8_t bInterfaceNumber, uint8_t bAlternateSetting, int *offset) +{ + assert(config_desc != NULL); + + // Walk to first interface descriptor of bInterfaceNumber + int offset_temp = 0; + const usb_intf_desc_t *next_intf_desc = (const usb_intf_desc_t *)usb_parse_next_descriptor_of_type((const usb_standard_desc_t *)config_desc, config_desc->wTotalLength, USB_B_DESCRIPTOR_TYPE_INTERFACE, &offset_temp); + while (next_intf_desc != NULL) { + if (next_intf_desc->bInterfaceNumber == bInterfaceNumber) { + break; // We found the first interface descriptor with matching bInterfaceNumber + } + next_intf_desc = (const usb_intf_desc_t *)usb_parse_next_descriptor_of_type((const usb_standard_desc_t *)next_intf_desc, config_desc->wTotalLength, USB_B_DESCRIPTOR_TYPE_INTERFACE, &offset_temp); + } + if (next_intf_desc == NULL) { + return NULL; // Couldn't find a interface with bInterfaceNumber + } + + // Keep walking until an interface descriptor matching bInterfaceNumber and bAlternateSetting is found + while (next_intf_desc != NULL) { + if (next_intf_desc->bInterfaceNumber == bInterfaceNumber + 1) { + // We've walked past our target bInterfaceNumber + next_intf_desc = NULL; + break; + } + if (next_intf_desc->bAlternateSetting == bAlternateSetting) { + // We've found our target interface descriptor + break; + } + // Get the next interface descriptor + next_intf_desc = (const usb_intf_desc_t *)usb_parse_next_descriptor_of_type((const usb_standard_desc_t *)next_intf_desc, config_desc->wTotalLength, USB_B_DESCRIPTOR_TYPE_INTERFACE, &offset_temp); + } + if (next_intf_desc != NULL && offset != NULL) { + *offset = offset_temp; + } + return next_intf_desc; +} + +const usb_ep_desc_t *usb_parse_endpoint_descriptor_by_index(const usb_intf_desc_t *intf_desc, int index, uint16_t wTotalLength, int *offset) +{ + assert(intf_desc != NULL && offset != NULL); + if (index >= intf_desc->bNumEndpoints) { + return NULL; // Index is out of range + } + // Walk to the Nth endpoint descriptor we find + int offset_temp = *offset; + bool ep_found = true; + const usb_standard_desc_t *next_desc = (const usb_standard_desc_t *)intf_desc; + for (int i = 0; i <= index; i++) { + next_desc = usb_parse_next_descriptor_of_type((const usb_standard_desc_t *)next_desc, wTotalLength, USB_B_DESCRIPTOR_TYPE_ENDPOINT, &offset_temp); + if (next_desc == NULL) { + ep_found = false; + break; + } + } + if (ep_found) { + *offset = offset_temp; + return (const usb_ep_desc_t *)next_desc; + } + return NULL; +} + +const usb_ep_desc_t *usb_parse_endpoint_descriptor_by_address(const usb_config_desc_t *config_desc, uint8_t bInterfaceNumber, uint8_t bAlternateSetting, uint8_t bEndpointAddress, int *offset) +{ + assert(config_desc != NULL); + + // Find the interface descriptor + int offset_intf; + const usb_intf_desc_t *intf_desc = usb_parse_interface_descriptor(config_desc, bInterfaceNumber, bAlternateSetting, &offset_intf); + if (intf_desc == NULL) { + return NULL; + } + + // Walk endpoint descriptors until one matching bEndpointAddress is found + int offset_ep; + bool ep_found = false; + const usb_ep_desc_t *ep_desc = NULL; + for (int index = 0; index < intf_desc->bNumEndpoints; index++) { + offset_ep = offset_intf; + ep_desc = usb_parse_endpoint_descriptor_by_index(intf_desc, index, config_desc->wTotalLength, &offset_ep); + if (ep_desc == NULL) { + break; + } + if (ep_desc->bEndpointAddress == bEndpointAddress) { + ep_found = true; + break; + } + } + if (ep_found && offset != NULL) { + *offset = offset_ep; + } + return ep_desc; +} + +// ----------------------------------------------- Descriptor Printing ------------------------------------------------- + +static void print_ep_desc(const usb_ep_desc_t *ep_desc) +{ + const char *ep_type_str; + int type = ep_desc->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK; + + switch (type) { + case USB_BM_ATTRIBUTES_XFER_CONTROL: + ep_type_str = "CTRL"; + break; + case USB_BM_ATTRIBUTES_XFER_ISOC: + ep_type_str = "ISOC"; + break; + case USB_BM_ATTRIBUTES_XFER_BULK: + ep_type_str = "BULK"; + break; + case USB_BM_ATTRIBUTES_XFER_INT: + ep_type_str = "INT"; + break; + default: + ep_type_str = NULL; + break; + } + + printf("\t\t*** Endpoint descriptor ***\n"); + printf("\t\tbLength %d\n", ep_desc->bLength); + printf("\t\tbDescriptorType %d\n", ep_desc->bDescriptorType); + printf("\t\tbEndpointAddress 0x%x\tEP %d %s\n", ep_desc->bEndpointAddress, + USB_EP_DESC_GET_EP_NUM(ep_desc), + USB_EP_DESC_GET_EP_DIR(ep_desc) ? "IN" : "OUT"); + printf("\t\tbmAttributes 0x%x\t%s\n", ep_desc->bmAttributes, ep_type_str); + printf("\t\twMaxPacketSize %d\n", USB_EP_DESC_GET_MPS(ep_desc)); + printf("\t\tbInterval %d\n", ep_desc->bInterval); +} + +static void usbh_print_intf_desc(const usb_intf_desc_t *intf_desc) +{ + printf("\t*** Interface descriptor ***\n"); + printf("\tbLength %d\n", intf_desc->bLength); + printf("\tbDescriptorType %d\n", intf_desc->bDescriptorType); + printf("\tbInterfaceNumber %d\n", intf_desc->bInterfaceNumber); + printf("\tbAlternateSetting %d\n", intf_desc->bAlternateSetting); + printf("\tbNumEndpoints %d\n", intf_desc->bNumEndpoints); + printf("\tbInterfaceClass 0x%x\n", intf_desc->bInterfaceClass); + printf("\tbInterfaceSubClass 0x%x\n", intf_desc->bInterfaceSubClass); + printf("\tbInterfaceProtocol 0x%x\n", intf_desc->bInterfaceProtocol); + printf("\tiInterface %d\n", intf_desc->iInterface); +} + +static void usbh_print_cfg_desc(const usb_config_desc_t *cfg_desc) +{ + printf("*** Configuration descriptor ***\n"); + printf("bLength %d\n", cfg_desc->bLength); + printf("bDescriptorType %d\n", cfg_desc->bDescriptorType); + printf("wTotalLength %d\n", cfg_desc->wTotalLength); + printf("bNumInterfaces %d\n", cfg_desc->bNumInterfaces); + printf("bConfigurationValue %d\n", cfg_desc->bConfigurationValue); + printf("iConfiguration %d\n", cfg_desc->iConfiguration); + printf("bmAttributes 0x%x\n", cfg_desc->bmAttributes); + printf("bMaxPower %dmA\n", cfg_desc->bMaxPower * 2); +} + +static void print_iad_desc(const usb_iad_desc_t *iad_desc) +{ + printf("*** Interface Association Descriptor ***\n"); + printf("bLength %d\n", iad_desc->bLength); + printf("bDescriptorType %d\n", iad_desc->bDescriptorType); + printf("bFirstInterface %d\n", iad_desc->bFirstInterface); + printf("bInterfaceCount %d\n", iad_desc->bInterfaceCount); + printf("bFunctionClass 0x%x\n", iad_desc->bFunctionClass); + printf("bFunctionSubClass 0x%x\n", iad_desc->bFunctionSubClass); + printf("bFunctionProtocol 0x%x\n", iad_desc->bFunctionProtocol); + printf("iFunction %d\n", iad_desc->iFunction); +} + +void usb_print_device_descriptor(const usb_device_desc_t *devc_desc) +{ + if (devc_desc == NULL) { + return; + } + + printf("*** Device descriptor ***\n"); + printf("bLength %d\n", devc_desc->bLength); + printf("bDescriptorType %d\n", devc_desc->bDescriptorType); + printf("bcdUSB %d.%d0\n", ((devc_desc->bcdUSB >> 8) & 0xF), ((devc_desc->bcdUSB >> 4) & 0xF)); + printf("bDeviceClass 0x%x\n", devc_desc->bDeviceClass); + printf("bDeviceSubClass 0x%x\n", devc_desc->bDeviceSubClass); + printf("bDeviceProtocol 0x%x\n", devc_desc->bDeviceProtocol); + printf("bMaxPacketSize0 %d\n", devc_desc->bMaxPacketSize0); + printf("idVendor 0x%x\n", devc_desc->idVendor); + printf("idProduct 0x%x\n", devc_desc->idProduct); + printf("bcdDevice %d.%d0\n", ((devc_desc->bcdDevice >> 8) & 0xF), ((devc_desc->bcdDevice >> 4) & 0xF)); + printf("iManufacturer %d\n", devc_desc->iManufacturer); + printf("iProduct %d\n", devc_desc->iProduct); + printf("iSerialNumber %d\n", devc_desc->iSerialNumber); + printf("bNumConfigurations %d\n", devc_desc->bNumConfigurations); +} + +void usb_print_config_descriptor(const usb_config_desc_t *cfg_desc, print_class_descriptor_cb class_specific_cb) +{ + if (cfg_desc == NULL) { + return; + } + + int offset = 0; + uint16_t wTotalLength = cfg_desc->wTotalLength; + const usb_standard_desc_t *next_desc = (const usb_standard_desc_t *)cfg_desc; + + do { + switch (next_desc->bDescriptorType) { + case USB_B_DESCRIPTOR_TYPE_CONFIGURATION: + usbh_print_cfg_desc((const usb_config_desc_t *)next_desc); + break; + case USB_B_DESCRIPTOR_TYPE_INTERFACE: + usbh_print_intf_desc((const usb_intf_desc_t *)next_desc); + break; + case USB_B_DESCRIPTOR_TYPE_ENDPOINT: + print_ep_desc((const usb_ep_desc_t *)next_desc); + break; + case USB_B_DESCRIPTOR_TYPE_INTERFACE_ASSOCIATION: + print_iad_desc((const usb_iad_desc_t *)next_desc); + break; + default: + if (class_specific_cb) { + class_specific_cb(next_desc); + } + break; + } + + next_desc = usb_parse_next_descriptor(next_desc, wTotalLength, &offset); + + } while (next_desc != NULL); +} + +void usb_print_string_descriptor(const usb_str_desc_t *str_desc) +{ + if (str_desc == NULL) { + return; + } + + for (int i = 0; i < str_desc->bLength / 2; i++) { + /* + USB String descriptors of UTF-16. + Right now We just skip any character larger than 0xFF to stay in BMP Basic Latin and Latin-1 Supplement range. + */ + if (str_desc->wData[i] > 0xFF) { + continue; + } + printf("%c", (char)str_desc->wData[i]); + } + printf("\n"); +} diff --git a/components/usb/usb_host.c b/components/usb/usb_host.c new file mode 100644 index 0000000000..d1932e8f4d --- /dev/null +++ b/components/usb/usb_host.c @@ -0,0 +1,1329 @@ +/* + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* +Warning: The USB Host Library API is still a beta version and may be subject to change +*/ + +#include +#include +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_heap_caps.h" +#include "hub.h" +#include "usbh.h" +#include "hcd.h" +#include "esp_private/usb_phy.h" +#include "usb/usb_host.h" + +static portMUX_TYPE host_lock = portMUX_INITIALIZER_UNLOCKED; + +#define HOST_ENTER_CRITICAL_ISR() portENTER_CRITICAL_ISR(&host_lock) +#define HOST_EXIT_CRITICAL_ISR() portEXIT_CRITICAL_ISR(&host_lock) +#define HOST_ENTER_CRITICAL() portENTER_CRITICAL(&host_lock) +#define HOST_EXIT_CRITICAL() portEXIT_CRITICAL(&host_lock) +#define HOST_ENTER_CRITICAL_SAFE() portENTER_CRITICAL_SAFE(&host_lock) +#define HOST_EXIT_CRITICAL_SAFE() portEXIT_CRITICAL_SAFE(&host_lock) + +#define HOST_CHECK(cond, ret_val) ({ \ + if (!(cond)) { \ + return (ret_val); \ + } \ +}) +#define HOST_CHECK_FROM_CRIT(cond, ret_val) ({ \ + if (!(cond)) { \ + HOST_EXIT_CRITICAL(); \ + return ret_val; \ + } \ +}) + +#define PROCESS_REQUEST_PENDING_FLAG_USBH 0x01 +#define PROCESS_REQUEST_PENDING_FLAG_HUB 0x02 + +#ifdef CONFIG_USB_HOST_ENABLE_ENUM_FILTER_CALLBACK +#define ENABLE_ENUM_FILTER_CALLBACK +#endif // CONFIG_USB_HOST_ENABLE_ENUM_FILTER_CALLBACK + +typedef struct ep_wrapper_s ep_wrapper_t; +typedef struct interface_s interface_t; +typedef struct client_s client_t; + +struct ep_wrapper_s { + // Dynamic members require a critical section + struct { + TAILQ_ENTRY(ep_wrapper_s) tailq_entry; + union { + struct { + uint32_t pending: 1; + uint32_t reserved31: 31; + }; + } flags; + uint32_t num_urb_inflight; + usbh_ep_event_t last_event; + } dynamic; + // Constant members do no change after claiming the interface thus do not require a critical section + struct { + usbh_ep_handle_t ep_hdl; + interface_t *intf_obj; + } constant; +}; + +struct interface_s { + // Dynamic members require a critical section + struct { + TAILQ_ENTRY(interface_s) tailq_entry; + } mux_protected; + // Constant members do no change after claiming the interface thus do not require a critical section + struct { + const usb_intf_desc_t *intf_desc; + usb_device_handle_t dev_hdl; + client_t *client_obj; + ep_wrapper_t *endpoints[0]; + } constant; +}; + +struct client_s { + // Dynamic members require a critical section + struct { + TAILQ_ENTRY(client_s) tailq_entry; + TAILQ_HEAD(tailhead_pending_ep, ep_wrapper_s) pending_ep_tailq; + TAILQ_HEAD(tailhead_idle_ep, ep_wrapper_s) idle_ep_tailq; + TAILQ_HEAD(tailhead_done_ctrl_xfers, urb_s) done_ctrl_xfer_tailq; + union { + struct { + uint32_t handling_events: 1; + uint32_t taking_mux: 1; + uint32_t reserved6: 6; + uint32_t num_intf_claimed: 8; + uint32_t reserved16: 16; + }; + uint32_t val; + } flags; + uint32_t num_done_ctrl_xfer; + uint32_t opened_dev_addr_map; + } dynamic; + // Mux protected members must be protected by host library the mux_lock when accessed + struct { + TAILQ_HEAD(tailhead_interfaces, interface_s) interface_tailq; + } mux_protected; + // Constant members do no change after registration thus do not require a critical section + struct { + SemaphoreHandle_t event_sem; + usb_host_client_event_cb_t event_callback; + void *callback_arg; + QueueHandle_t event_msg_queue; + } constant; +}; + +typedef struct { + // Dynamic members require a critical section + struct { + // Access to these should be done in a critical section + uint32_t process_pending_flags; + uint32_t lib_event_flags; + union { + struct { + uint32_t handling_events: 1; + uint32_t reserved7: 7; + uint32_t num_clients: 8; + uint32_t reserved16: 16; + }; + uint32_t val; + } flags; + } dynamic; + // Mux protected members must be protected by host library the mux_lock when accessed + struct { + TAILQ_HEAD(tailhead_clients, client_s) client_tailq; // List of all clients registered + } mux_protected; + // Constant members do no change after installation thus do not require a critical section + struct { + SemaphoreHandle_t event_sem; + SemaphoreHandle_t mux_lock; + usb_phy_handle_t phy_handle; // Will be NULL if host library is installed with skip_phy_setup + void *hub_client; // Pointer to Hub driver (acting as a client). Used to reroute completed USBH control transfers + } constant; +} host_lib_t; + +static host_lib_t *p_host_lib_obj = NULL; + +const char *USB_HOST_TAG = "USB HOST"; + +// ----------------------------------------------------- Helpers ------------------------------------------------------- + +static inline void _record_client_opened_device(client_t *client_obj, uint8_t dev_addr) +{ + assert(dev_addr != 0); + client_obj->dynamic.opened_dev_addr_map |= (1 << (dev_addr - 1)); +} + +static inline void _clear_client_opened_device(client_t *client_obj, uint8_t dev_addr) +{ + assert(dev_addr != 0); + client_obj->dynamic.opened_dev_addr_map &= ~(1 << (dev_addr - 1)); +} + +static inline bool _check_client_opened_device(client_t *client_obj, uint8_t dev_addr) +{ + bool ret; + + if (dev_addr != 0) { + ret = client_obj->dynamic.opened_dev_addr_map & (1 << (dev_addr - 1)); + } else { + ret = false; + } + + return ret; +} + +static bool _unblock_client(client_t *client_obj, bool in_isr) +{ + bool yield; + + HOST_EXIT_CRITICAL_SAFE(); + if (in_isr) { + BaseType_t xTaskWoken = pdFALSE; + xSemaphoreGiveFromISR(client_obj->constant.event_sem, &xTaskWoken); + yield = (xTaskWoken == pdTRUE); + } else { + xSemaphoreGive(client_obj->constant.event_sem); + yield = false; + } + HOST_ENTER_CRITICAL_SAFE(); + + return yield; +} + +static bool _unblock_lib(bool in_isr) +{ + bool yield; + + HOST_EXIT_CRITICAL_SAFE(); + if (in_isr) { + BaseType_t xTaskWoken = pdFALSE; + xSemaphoreGiveFromISR(p_host_lib_obj->constant.event_sem, &xTaskWoken); + yield = (xTaskWoken == pdTRUE); + } else { + xSemaphoreGive(p_host_lib_obj->constant.event_sem); + yield = false; + } + HOST_ENTER_CRITICAL_SAFE(); + + return yield; +} + +static void send_event_msg_to_clients(const usb_host_client_event_msg_t *event_msg, bool send_to_all, uint8_t opened_dev_addr) +{ + // Lock client list + xSemaphoreTake(p_host_lib_obj->constant.mux_lock, portMAX_DELAY); + // Send event message to relevant or all clients + client_t *client_obj; + TAILQ_FOREACH(client_obj, &p_host_lib_obj->mux_protected.client_tailq, dynamic.tailq_entry) { + if (!send_to_all) { + // Check if client opened the device + HOST_ENTER_CRITICAL(); + bool send = _check_client_opened_device(client_obj, opened_dev_addr); + HOST_EXIT_CRITICAL(); + if (!send) { + continue; + } + } + // Send the event message + if (xQueueSend(client_obj->constant.event_msg_queue, event_msg, 0) == pdTRUE) { + HOST_ENTER_CRITICAL(); + _unblock_client(client_obj, false); + HOST_EXIT_CRITICAL(); + } else { + ESP_LOGE(USB_HOST_TAG, "Client event message queue full"); + } + } + // Unlock client list + xSemaphoreGive(p_host_lib_obj->constant.mux_lock); +} + +// ---------------------------------------------------- Callbacks ------------------------------------------------------ + +// ------------------- Library Related --------------------- + +static bool proc_req_callback(usb_proc_req_source_t source, bool in_isr, void *arg) +{ + HOST_ENTER_CRITICAL_SAFE(); + // Store the processing request source + switch (source) { + case USB_PROC_REQ_SOURCE_USBH: + p_host_lib_obj->dynamic.process_pending_flags |= PROCESS_REQUEST_PENDING_FLAG_USBH; + break; + case USB_PROC_REQ_SOURCE_HUB: + p_host_lib_obj->dynamic.process_pending_flags |= PROCESS_REQUEST_PENDING_FLAG_HUB; + break; + } + bool yield = _unblock_lib(in_isr); + HOST_EXIT_CRITICAL_SAFE(); + + return yield; +} + +static void usbh_event_callback(usbh_event_data_t *event_data, void *arg) +{ + switch (event_data->event) { + case USBH_EVENT_CTRL_XFER: { + assert(event_data->ctrl_xfer_data.urb != NULL); + assert(event_data->ctrl_xfer_data.urb->usb_host_client != NULL); + // Redistribute completed control transfers to the clients that submitted them + if (event_data->ctrl_xfer_data.urb->usb_host_client == p_host_lib_obj->constant.hub_client) { + // Redistribute to Hub driver. Simply call the transfer callback + event_data->ctrl_xfer_data.urb->transfer.callback(&event_data->ctrl_xfer_data.urb->transfer); + } else { + client_t *client_obj = (client_t *)event_data->ctrl_xfer_data.urb->usb_host_client; + HOST_ENTER_CRITICAL(); + TAILQ_INSERT_TAIL(&client_obj->dynamic.done_ctrl_xfer_tailq, event_data->ctrl_xfer_data.urb, tailq_entry); + client_obj->dynamic.num_done_ctrl_xfer++; + _unblock_client(client_obj, false); + HOST_EXIT_CRITICAL(); + } + break; + } + case USBH_EVENT_NEW_DEV: { + // Prepare a NEW_DEV client event message, the send it to all clients + usb_host_client_event_msg_t event_msg = { + .event = USB_HOST_CLIENT_EVENT_NEW_DEV, + .new_dev.address = event_data->new_dev_data.dev_addr, + }; + send_event_msg_to_clients(&event_msg, true, 0); + break; + } + case USBH_EVENT_DEV_GONE: { + // Prepare event msg, send only to clients that have opened the device + usb_host_client_event_msg_t event_msg = { + .event = USB_HOST_CLIENT_EVENT_DEV_GONE, + .dev_gone.dev_hdl = event_data->dev_gone_data.dev_hdl, + }; + send_event_msg_to_clients(&event_msg, false, event_data->dev_gone_data.dev_addr); + break; + } + case USBH_EVENT_DEV_FREE: { + // Let the Hub driver know that the device is free and its port can be recycled + ESP_ERROR_CHECK(hub_port_recycle(event_data->dev_free_data.dev_uid)); + break; + } + case USBH_EVENT_ALL_FREE: { + // Notify the lib handler that all devices are free + HOST_ENTER_CRITICAL(); + p_host_lib_obj->dynamic.lib_event_flags |= USB_HOST_LIB_EVENT_FLAGS_ALL_FREE; + _unblock_lib(false); + HOST_EXIT_CRITICAL(); + break; + } + default: + abort(); // Should never occur + break; + } +} + +// ------------------- Client Related ---------------------- + +static bool endpoint_callback(usbh_ep_handle_t ep_hdl, usbh_ep_event_t ep_event, void *user_arg, bool in_isr) +{ + ep_wrapper_t *ep_wrap = (ep_wrapper_t *)user_arg; + client_t *client_obj = (client_t *)ep_wrap->constant.intf_obj->constant.client_obj; + + HOST_ENTER_CRITICAL_SAFE(); + // Store the event to be handled later. Note that we allow overwriting of events because more severe will halt the pipe prevent any further events. + ep_wrap->dynamic.last_event = ep_event; + // Add the EP to the client's pending list if it's not in the list already + if (!ep_wrap->dynamic.flags.pending) { + ep_wrap->dynamic.flags.pending = 1; + TAILQ_REMOVE(&client_obj->dynamic.idle_ep_tailq, ep_wrap, dynamic.tailq_entry); + TAILQ_INSERT_TAIL(&client_obj->dynamic.pending_ep_tailq, ep_wrap, dynamic.tailq_entry); + } + bool yield = _unblock_client(client_obj, in_isr); + HOST_EXIT_CRITICAL_SAFE(); + + return yield; +} + +// ------------------------------------------------ Library Functions -------------------------------------------------- + +// ----------------------- Public -------------------------- + +esp_err_t usb_host_install(const usb_host_config_t *config) +{ + HOST_CHECK(config != NULL, ESP_ERR_INVALID_ARG); + HOST_ENTER_CRITICAL(); + HOST_CHECK_FROM_CRIT(p_host_lib_obj == NULL, ESP_ERR_INVALID_STATE); + HOST_EXIT_CRITICAL(); + + esp_err_t ret; + host_lib_t *host_lib_obj = heap_caps_calloc(1, sizeof(host_lib_t), MALLOC_CAP_DEFAULT); + SemaphoreHandle_t event_sem = xSemaphoreCreateBinary(); + SemaphoreHandle_t mux_lock = xSemaphoreCreateMutex(); + if (host_lib_obj == NULL || event_sem == NULL || mux_lock == NULL) { + ret = ESP_ERR_NO_MEM; + goto alloc_err; + } + // Initialize host library object + TAILQ_INIT(&host_lib_obj->mux_protected.client_tailq); + host_lib_obj->constant.event_sem = event_sem; + host_lib_obj->constant.mux_lock = mux_lock; + + /* + Install each layer of the Host stack (listed below) from the lowest layer to the highest + - USB PHY + - HCD + - USBH + - Hub + */ + + // Install USB PHY (if necessary). USB PHY driver will also enable the underlying Host Controller + if (!config->skip_phy_setup) { + // Host Library defaults to internal PHY + usb_phy_config_t phy_config = { + .controller = USB_PHY_CTRL_OTG, + .target = USB_PHY_TARGET_INT, + .otg_mode = USB_OTG_MODE_HOST, + .otg_speed = USB_PHY_SPEED_UNDEFINED, // In Host mode, the speed is determined by the connected device + .ext_io_conf = NULL, + .otg_io_conf = NULL, + }; + ret = usb_new_phy(&phy_config, &host_lib_obj->constant.phy_handle); + if (ret != ESP_OK) { + goto phy_err; + } + } + + // Install HCD + hcd_config_t hcd_config = { + .intr_flags = config->intr_flags + }; + ret = hcd_install(&hcd_config); + if (ret != ESP_OK) { + goto hcd_err; + } + + // Install USBH + usbh_config_t usbh_config = { + .proc_req_cb = proc_req_callback, + .proc_req_cb_arg = NULL, + .event_cb = usbh_event_callback, + .event_cb_arg = NULL, + }; + ret = usbh_install(&usbh_config); + if (ret != ESP_OK) { + goto usbh_err; + } + +#ifdef ENABLE_ENUM_FILTER_CALLBACK + if (config->enum_filter_cb == NULL) { + ESP_LOGW(USB_HOST_TAG, "User callback to set USB device configuration is enabled, but not used"); + } +#endif // ENABLE_ENUM_FILTER_CALLBACK + // Install Hub + hub_config_t hub_config = { + .proc_req_cb = proc_req_callback, + .proc_req_cb_arg = NULL, +#ifdef ENABLE_ENUM_FILTER_CALLBACK + .enum_filter_cb = config->enum_filter_cb, +#endif // ENABLE_ENUM_FILTER_CALLBACK + }; + ret = hub_install(&hub_config, &host_lib_obj->constant.hub_client); + if (ret != ESP_OK) { + goto hub_err; + } + + // Assign host library object + HOST_ENTER_CRITICAL(); + if (p_host_lib_obj != NULL) { + HOST_EXIT_CRITICAL(); + ret = ESP_ERR_INVALID_STATE; + goto assign_err; + } + p_host_lib_obj = host_lib_obj; + HOST_EXIT_CRITICAL(); + + // Start the root hub + ESP_ERROR_CHECK(hub_root_start()); + ret = ESP_OK; + return ret; + +assign_err: + ESP_ERROR_CHECK(hub_uninstall()); +hub_err: + ESP_ERROR_CHECK(usbh_uninstall()); +usbh_err: + ESP_ERROR_CHECK(hcd_uninstall()); +hcd_err: + if (host_lib_obj->constant.phy_handle) { + ESP_ERROR_CHECK(usb_del_phy(host_lib_obj->constant.phy_handle)); + } +phy_err: +alloc_err: + if (mux_lock) { + vSemaphoreDelete(mux_lock); + } + if (event_sem) { + vSemaphoreDelete(event_sem); + } + heap_caps_free(host_lib_obj); + return ret; +} + +esp_err_t usb_host_uninstall(void) +{ + // All devices must have been freed at this point + HOST_ENTER_CRITICAL(); + HOST_CHECK_FROM_CRIT(p_host_lib_obj != NULL, ESP_ERR_INVALID_STATE); + HOST_CHECK_FROM_CRIT(p_host_lib_obj->dynamic.process_pending_flags == 0 && + p_host_lib_obj->dynamic.lib_event_flags == 0 && + p_host_lib_obj->dynamic.flags.val == 0, + ESP_ERR_INVALID_STATE); + HOST_EXIT_CRITICAL(); + + // Stop the root hub + ESP_ERROR_CHECK(hub_root_stop()); + + // Unassign the host library object + HOST_ENTER_CRITICAL(); + host_lib_t *host_lib_obj = p_host_lib_obj; + p_host_lib_obj = NULL; + HOST_EXIT_CRITICAL(); + + /* + Uninstall each layer of the Host stack (listed below) from the highest layer to the lowest + - Hub + - USBH + - HCD + - USB PHY + */ + ESP_ERROR_CHECK(hub_uninstall()); + ESP_ERROR_CHECK(usbh_uninstall()); + ESP_ERROR_CHECK(hcd_uninstall()); + // If the USB PHY was setup, then delete it + if (host_lib_obj->constant.phy_handle) { + ESP_ERROR_CHECK(usb_del_phy(host_lib_obj->constant.phy_handle)); + } + + // Free memory objects + vSemaphoreDelete(host_lib_obj->constant.mux_lock); + vSemaphoreDelete(host_lib_obj->constant.event_sem); + heap_caps_free(host_lib_obj); + return ESP_OK; +} + +esp_err_t usb_host_lib_handle_events(TickType_t timeout_ticks, uint32_t *event_flags_ret) +{ + // Check arguments and state + HOST_CHECK(p_host_lib_obj != NULL, ESP_ERR_INVALID_STATE); + + esp_err_t ret = (timeout_ticks == 0) ? ESP_OK : ESP_ERR_TIMEOUT; // We don't want to return ESP_ERR_TIMEOUT if we aren't blocking + uint32_t event_flags; + + HOST_ENTER_CRITICAL(); + // Set handling_events flag. This prevents the host library from being uninstalled + p_host_lib_obj->dynamic.flags.handling_events = 1; + HOST_EXIT_CRITICAL(); + + while (1) { + // Loop until there are no more events + if (xSemaphoreTake(p_host_lib_obj->constant.event_sem, timeout_ticks) == pdFALSE) { + // Timed out waiting for semaphore or currently no events + break; + } + + // Read and clear process pending flags + HOST_ENTER_CRITICAL(); + uint32_t process_pending_flags = p_host_lib_obj->dynamic.process_pending_flags; + p_host_lib_obj->dynamic.process_pending_flags = 0; + HOST_EXIT_CRITICAL(); + + if (process_pending_flags & PROCESS_REQUEST_PENDING_FLAG_USBH) { + ESP_ERROR_CHECK(usbh_process()); + } + if (process_pending_flags & PROCESS_REQUEST_PENDING_FLAG_HUB) { + ESP_ERROR_CHECK(hub_process()); + } + + ret = ESP_OK; + // Set timeout_ticks to 0 so that we can check for events again without blocking + timeout_ticks = 0; + } + + HOST_ENTER_CRITICAL(); + p_host_lib_obj->dynamic.flags.handling_events = 0; + // Read and clear any event flags + event_flags = p_host_lib_obj->dynamic.lib_event_flags; + p_host_lib_obj->dynamic.lib_event_flags = 0; + HOST_EXIT_CRITICAL(); + + if (event_flags_ret != NULL) { + *event_flags_ret = event_flags; + } + return ret; +} + +esp_err_t usb_host_lib_unblock(void) +{ + // All devices must have been freed at this point + HOST_ENTER_CRITICAL(); + HOST_CHECK_FROM_CRIT(p_host_lib_obj != NULL, ESP_ERR_INVALID_STATE); + _unblock_lib(false); + HOST_EXIT_CRITICAL(); + return ESP_OK; +} + +esp_err_t usb_host_lib_info(usb_host_lib_info_t *info_ret) +{ + HOST_CHECK(info_ret != NULL, ESP_ERR_INVALID_ARG); + int num_devs_temp; + int num_clients_temp; + HOST_ENTER_CRITICAL(); + HOST_CHECK_FROM_CRIT(p_host_lib_obj != NULL, ESP_ERR_INVALID_STATE); + num_clients_temp = p_host_lib_obj->dynamic.flags.num_clients; + HOST_EXIT_CRITICAL(); + usbh_devs_num(&num_devs_temp); + + // Write back return values + info_ret->num_devices = num_devs_temp; + info_ret->num_clients = num_clients_temp; + return ESP_OK; +} + +// ------------------------------------------------ Client Functions --------------------------------------------------- + +// ----------------------- Private ------------------------- + +static void _handle_pending_ep(client_t *client_obj) +{ + // Handle each EP on the pending list + while (!TAILQ_EMPTY(&client_obj->dynamic.pending_ep_tailq)) { + // Get the next pending EP. + ep_wrapper_t *ep_wrap = TAILQ_FIRST(&client_obj->dynamic.pending_ep_tailq); + TAILQ_REMOVE(&client_obj->dynamic.pending_ep_tailq, ep_wrap, dynamic.tailq_entry); + TAILQ_INSERT_TAIL(&client_obj->dynamic.idle_ep_tailq, ep_wrap, dynamic.tailq_entry); + ep_wrap->dynamic.flags.pending = 0; + usbh_ep_event_t last_event = ep_wrap->dynamic.last_event; + uint32_t num_urb_dequeued = 0; + + HOST_EXIT_CRITICAL(); + // Handle pipe event + switch (last_event) { + case USBH_EP_EVENT_ERROR_XFER: + case USBH_EP_EVENT_ERROR_URB_NOT_AVAIL: + case USBH_EP_EVENT_ERROR_OVERFLOW: + case USBH_EP_EVENT_ERROR_STALL: + // The endpoint is now stalled. Flush all pending URBs + ESP_ERROR_CHECK(usbh_ep_command(ep_wrap->constant.ep_hdl, USBH_EP_CMD_FLUSH)); + // All URBs in this pipe are now retired waiting to be dequeued. Fall through to dequeue them + __attribute__((fallthrough)); + case USBH_EP_EVENT_URB_DONE: { + // Dequeue all URBs and run their transfer callback + urb_t *urb; + usbh_ep_dequeue_urb(ep_wrap->constant.ep_hdl, &urb); + while (urb != NULL) { + // Clear the transfer's in-flight flag to indicate the transfer is no longer in-flight + urb->usb_host_inflight = false; + urb->transfer.callback(&urb->transfer); + num_urb_dequeued++; + usbh_ep_dequeue_urb(ep_wrap->constant.ep_hdl, &urb); + } + break; + } + default: + abort(); // Should never occur + break; + } + HOST_ENTER_CRITICAL(); + + // Update the endpoint's number of URB's in-flight + assert(num_urb_dequeued <= ep_wrap->dynamic.num_urb_inflight); + ep_wrap->dynamic.num_urb_inflight -= num_urb_dequeued; + } +} + +// ----------------------- Public -------------------------- + +esp_err_t usb_host_client_register(const usb_host_client_config_t *client_config, usb_host_client_handle_t *client_hdl_ret) +{ + HOST_CHECK(p_host_lib_obj, ESP_ERR_INVALID_STATE); + HOST_CHECK(client_config != NULL && client_hdl_ret != NULL, ESP_ERR_INVALID_ARG); + HOST_CHECK(client_config->max_num_event_msg > 0, ESP_ERR_INVALID_ARG); + if (!client_config->is_synchronous) { + // Asynchronous clients must provide a + HOST_CHECK(client_config->async.client_event_callback != NULL, ESP_ERR_INVALID_ARG); + } + + esp_err_t ret; + // Create client object + client_t *client_obj = heap_caps_calloc(1, sizeof(client_t), MALLOC_CAP_DEFAULT); + SemaphoreHandle_t event_sem = xSemaphoreCreateBinary(); + QueueHandle_t event_msg_queue = xQueueCreate(client_config->max_num_event_msg, sizeof(usb_host_client_event_msg_t)); + if (client_obj == NULL || event_sem == NULL || event_msg_queue == NULL) { + ret = ESP_ERR_NO_MEM; + goto alloc_err; + } + // Initialize client object + TAILQ_INIT(&client_obj->dynamic.pending_ep_tailq); + TAILQ_INIT(&client_obj->dynamic.idle_ep_tailq); + TAILQ_INIT(&client_obj->mux_protected.interface_tailq); + TAILQ_INIT(&client_obj->dynamic.done_ctrl_xfer_tailq); + client_obj->constant.event_sem = event_sem; + client_obj->constant.event_callback = client_config->async.client_event_callback; + client_obj->constant.callback_arg = client_config->async.callback_arg; + client_obj->constant.event_msg_queue = event_msg_queue; + + // Add client to the host library's list of clients + xSemaphoreTake(p_host_lib_obj->constant.mux_lock, portMAX_DELAY); + HOST_ENTER_CRITICAL(); + p_host_lib_obj->dynamic.flags.num_clients++; + HOST_EXIT_CRITICAL(); + TAILQ_INSERT_TAIL(&p_host_lib_obj->mux_protected.client_tailq, client_obj, dynamic.tailq_entry); + xSemaphoreGive(p_host_lib_obj->constant.mux_lock); + + // Write back client handle + *client_hdl_ret = (usb_host_client_handle_t)client_obj; + ret = ESP_OK; + return ret; + +alloc_err: + if (event_msg_queue) { + vQueueDelete(event_msg_queue); + } + if (event_sem) { + vSemaphoreDelete(event_sem); + } + heap_caps_free(client_obj); + return ret; +} + +esp_err_t usb_host_client_deregister(usb_host_client_handle_t client_hdl) +{ + HOST_CHECK(client_hdl != NULL, ESP_ERR_INVALID_ARG); + client_t *client_obj = (client_t *)client_hdl; + esp_err_t ret; + + // We take the mux_lock because we need to access the host library's client_tailq + xSemaphoreTake(p_host_lib_obj->constant.mux_lock, portMAX_DELAY); + HOST_ENTER_CRITICAL(); + // Check that client can currently deregistered + bool can_deregister; + if (!TAILQ_EMPTY(&client_obj->dynamic.pending_ep_tailq) || + !TAILQ_EMPTY(&client_obj->dynamic.idle_ep_tailq) || + !TAILQ_EMPTY(&client_obj->dynamic.done_ctrl_xfer_tailq) || + client_obj->dynamic.flags.handling_events || + client_obj->dynamic.flags.taking_mux || + client_obj->dynamic.flags.num_intf_claimed != 0 || + client_obj->dynamic.num_done_ctrl_xfer != 0 || + client_obj->dynamic.opened_dev_addr_map != 0) { + can_deregister = false; + } else { + can_deregister = true; + } + HOST_EXIT_CRITICAL(); + if (!can_deregister) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + + // Remove client object from the library's list of clients + TAILQ_REMOVE(&p_host_lib_obj->mux_protected.client_tailq, client_obj, dynamic.tailq_entry); + HOST_ENTER_CRITICAL(); + p_host_lib_obj->dynamic.flags.num_clients--; + if (p_host_lib_obj->dynamic.flags.num_clients == 0) { + // This is the last client being deregistered. Notify the lib handler + p_host_lib_obj->dynamic.lib_event_flags |= USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS; + _unblock_lib(false); + } + HOST_EXIT_CRITICAL(); + // Free client object + vQueueDelete(client_obj->constant.event_msg_queue); + vSemaphoreDelete(client_obj->constant.event_sem); + heap_caps_free(client_obj); + ret = ESP_OK; +exit: + xSemaphoreGive(p_host_lib_obj->constant.mux_lock); + return ret; +} + +esp_err_t usb_host_client_handle_events(usb_host_client_handle_t client_hdl, TickType_t timeout_ticks) +{ + // Check arguments and state + HOST_CHECK(client_hdl != NULL, ESP_ERR_INVALID_ARG); + HOST_CHECK(p_host_lib_obj != NULL, ESP_ERR_INVALID_STATE); + + esp_err_t ret = (timeout_ticks == 0) ? ESP_OK : ESP_ERR_TIMEOUT; // We don't want to return ESP_ERR_TIMEOUT if we aren't blocking + client_t *client_obj = (client_t *)client_hdl; + + HOST_ENTER_CRITICAL(); + // Set handling_events flag. This prevents the client from being deregistered + client_obj->dynamic.flags.handling_events = 1; + HOST_EXIT_CRITICAL(); + + while (1) { + // Loop until there are no more events + if (xSemaphoreTake(client_obj->constant.event_sem, timeout_ticks) == pdFALSE) { + // Timed out waiting for semaphore or currently no events + break; + } + + HOST_ENTER_CRITICAL(); + // Handle pending endpoints + if (!TAILQ_EMPTY(&client_obj->dynamic.pending_ep_tailq)) { + _handle_pending_ep(client_obj); + } + // Handle any done control transfers + while (client_obj->dynamic.num_done_ctrl_xfer > 0) { + urb_t *urb = TAILQ_FIRST(&client_obj->dynamic.done_ctrl_xfer_tailq); + TAILQ_REMOVE(&client_obj->dynamic.done_ctrl_xfer_tailq, urb, tailq_entry); + client_obj->dynamic.num_done_ctrl_xfer--; + HOST_EXIT_CRITICAL(); + // Clear the transfer's in-flight flag to indicate the transfer is no longer in-flight + urb->usb_host_inflight = false; + // Call the transfer's callback + urb->transfer.callback(&urb->transfer); + HOST_ENTER_CRITICAL(); + } + HOST_EXIT_CRITICAL(); + + // Handle event messages + while (uxQueueMessagesWaiting(client_obj->constant.event_msg_queue) > 0) { + // Dequeue the event message and call the client event callback + usb_host_client_event_msg_t event_msg; + BaseType_t queue_ret = xQueueReceive(client_obj->constant.event_msg_queue, &event_msg, 0); + assert(queue_ret == pdTRUE); + client_obj->constant.event_callback(&event_msg, client_obj->constant.callback_arg); + } + + ret = ESP_OK; + // Set timeout_ticks to 0 so that we can check for events again without blocking + timeout_ticks = 0; + } + + HOST_ENTER_CRITICAL(); + client_obj->dynamic.flags.handling_events = 0; + HOST_EXIT_CRITICAL(); + + return ret; +} + +esp_err_t usb_host_client_unblock(usb_host_client_handle_t client_hdl) +{ + HOST_CHECK(client_hdl != NULL, ESP_ERR_INVALID_ARG); + client_t *client_obj = (client_t *)client_hdl; + + HOST_ENTER_CRITICAL(); + _unblock_client(client_obj, false); + HOST_EXIT_CRITICAL(); + + return ESP_OK; +} + +// ------------------------------------------------- Device Handling --------------------------------------------------- + +esp_err_t usb_host_device_open(usb_host_client_handle_t client_hdl, uint8_t dev_addr, usb_device_handle_t *dev_hdl_ret) +{ + HOST_CHECK(dev_addr > 0 && client_hdl != NULL && dev_hdl_ret != NULL, ESP_ERR_INVALID_ARG); + client_t *client_obj = (client_t *)client_hdl; + + esp_err_t ret; + usb_device_handle_t dev_hdl; + ret = usbh_devs_open(dev_addr, &dev_hdl); + if (ret != ESP_OK) { + goto exit; + } + + HOST_ENTER_CRITICAL(); + if (_check_client_opened_device(client_obj, dev_addr)) { + // Client has already opened the device. Close it and return an error + ret = ESP_ERR_INVALID_STATE; + HOST_EXIT_CRITICAL(); + goto already_opened; + } + // Record in client object that we have opened the device of this address + _record_client_opened_device(client_obj, dev_addr); + HOST_EXIT_CRITICAL(); + + *dev_hdl_ret = dev_hdl; + ret = ESP_OK; + return ret; + +already_opened: + ESP_ERROR_CHECK(usbh_devs_close(dev_hdl)); +exit: + return ret; +} + +esp_err_t usb_host_device_close(usb_host_client_handle_t client_hdl, usb_device_handle_t dev_hdl) +{ + HOST_CHECK(dev_hdl != NULL && client_hdl != NULL, ESP_ERR_INVALID_ARG); + client_t *client_obj = (client_t *)client_hdl; + + // We take the lock because we need to walk the interface list + xSemaphoreTake(p_host_lib_obj->constant.mux_lock, portMAX_DELAY); + esp_err_t ret; + // Check that all interfaces claimed by this client do not belong to this device + bool all_released = true; + interface_t *intf_obj; + TAILQ_FOREACH(intf_obj, &client_obj->mux_protected.interface_tailq, mux_protected.tailq_entry) { + if (intf_obj->constant.dev_hdl == dev_hdl) { + all_released = false; + break; + } + } + if (!all_released) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + + // Check that client actually opened the device in the first place + HOST_ENTER_CRITICAL(); + uint8_t dev_addr; + ESP_ERROR_CHECK(usbh_dev_get_addr(dev_hdl, &dev_addr)); + HOST_CHECK_FROM_CRIT(_check_client_opened_device(client_obj, dev_addr), ESP_ERR_NOT_FOUND); + if (!_check_client_opened_device(client_obj, dev_addr)) { + // Client never opened this device + ret = ESP_ERR_INVALID_STATE; + HOST_EXIT_CRITICAL(); + goto exit; + } + // Proceed to clear the record of the device form the client + _clear_client_opened_device(client_obj, dev_addr); + HOST_EXIT_CRITICAL(); + + ESP_ERROR_CHECK(usbh_devs_close(dev_hdl)); + ret = ESP_OK; +exit: + xSemaphoreGive(p_host_lib_obj->constant.mux_lock); + return ret; +} + +esp_err_t usb_host_device_free_all(void) +{ + HOST_ENTER_CRITICAL(); + HOST_CHECK_FROM_CRIT(p_host_lib_obj->dynamic.flags.num_clients == 0, ESP_ERR_INVALID_STATE); // All clients must have been deregistered + HOST_EXIT_CRITICAL(); + esp_err_t ret; + ret = usbh_devs_mark_all_free(); + // If ESP_ERR_NOT_FINISHED is returned, caller must wait for USB_HOST_LIB_EVENT_FLAGS_ALL_FREE to confirm all devices are free + return ret; +} + +esp_err_t usb_host_device_addr_list_fill(int list_len, uint8_t *dev_addr_list, int *num_dev_ret) +{ + HOST_CHECK(dev_addr_list != NULL && num_dev_ret != NULL, ESP_ERR_INVALID_ARG); + return usbh_devs_addr_list_fill(list_len, dev_addr_list, num_dev_ret); +} + +// ------------------------------------------------- Device Requests --------------------------------------------------- + +// ------------------- Cached Requests --------------------- + +esp_err_t usb_host_device_info(usb_device_handle_t dev_hdl, usb_device_info_t *dev_info) +{ + HOST_CHECK(dev_hdl != NULL && dev_info != NULL, ESP_ERR_INVALID_ARG); + return usbh_dev_get_info(dev_hdl, dev_info); +} + +// ----------------------------------------------- Descriptor Requests ------------------------------------------------- + +// ----------------- Cached Descriptors -------------------- + +esp_err_t usb_host_get_device_descriptor(usb_device_handle_t dev_hdl, const usb_device_desc_t **device_desc) +{ + HOST_CHECK(dev_hdl != NULL && device_desc != NULL, ESP_ERR_INVALID_ARG); + return usbh_dev_get_desc(dev_hdl, device_desc); +} + +esp_err_t usb_host_get_active_config_descriptor(usb_device_handle_t dev_hdl, const usb_config_desc_t **config_desc) +{ + HOST_CHECK(dev_hdl != NULL && config_desc != NULL, ESP_ERR_INVALID_ARG); + return usbh_dev_get_config_desc(dev_hdl, config_desc); +} + +// ----------------------------------------------- Interface Functions ------------------------------------------------- + +// ----------------------- Private ------------------------- + +static esp_err_t ep_wrapper_alloc(usb_device_handle_t dev_hdl, const usb_ep_desc_t *ep_desc, interface_t *intf_obj, ep_wrapper_t **ep_wrap_ret) +{ + ep_wrapper_t *ep_wrap = heap_caps_calloc(1, sizeof(ep_wrapper_t), MALLOC_CAP_DEFAULT); + if (ep_wrap == NULL) { + return ESP_ERR_NO_MEM; + } + esp_err_t ret; + usbh_ep_handle_t ep_hdl; + usbh_ep_config_t ep_config = { + .bInterfaceNumber = intf_obj->constant.intf_desc->bInterfaceNumber, + .bAlternateSetting = intf_obj->constant.intf_desc->bAlternateSetting, + .bEndpointAddress = ep_desc->bEndpointAddress, + .ep_cb = endpoint_callback, + .ep_cb_arg = (void *)ep_wrap, + .context = (void *)ep_wrap, + }; + ret = usbh_ep_alloc(dev_hdl, &ep_config, &ep_hdl); + if (ret != ESP_OK) { + goto alloc_err; + } + // Initialize endpoint wrapper item + ep_wrap->constant.ep_hdl = ep_hdl; + ep_wrap->constant.intf_obj = intf_obj; + // Write back result + *ep_wrap_ret = ep_wrap; + ret = ESP_OK; + return ret; + +alloc_err: + heap_caps_free(ep_wrap); + return ret; +} + +static void ep_wrapper_free(usb_device_handle_t dev_hdl, ep_wrapper_t *ep_wrap) +{ + if (ep_wrap == NULL) { + return; + } + // Free the underlying endpoint + ESP_ERROR_CHECK(usbh_ep_free(ep_wrap->constant.ep_hdl)); + // Free the endpoint wrapper item + heap_caps_free(ep_wrap); +} + +static interface_t *interface_alloc(client_t *client_obj, usb_device_handle_t dev_hdl, const usb_intf_desc_t *intf_desc) +{ + interface_t *intf_obj = heap_caps_calloc(1, sizeof(interface_t) + (sizeof(ep_wrapper_t *) * intf_desc->bNumEndpoints), MALLOC_CAP_DEFAULT); + if (intf_obj == NULL) { + return NULL; + } + intf_obj->constant.intf_desc = intf_desc; + intf_obj->constant.client_obj = client_obj; + intf_obj->constant.dev_hdl = dev_hdl; + return intf_obj; +} + +static void interface_free(interface_t *intf_obj) +{ + if (intf_obj == NULL) { + return; + } + for (int i = 0; i < intf_obj->constant.intf_desc->bNumEndpoints; i++) { + assert(intf_obj->constant.endpoints[i] == NULL); + } + heap_caps_free(intf_obj); +} + +static esp_err_t interface_claim(client_t *client_obj, usb_device_handle_t dev_hdl, const usb_config_desc_t *config_desc, uint8_t bInterfaceNumber, uint8_t bAlternateSetting, interface_t **intf_obj_ret) +{ + esp_err_t ret; + // We need to walk to configuration descriptor to find the correct interface descriptor, and each of its constituent endpoint descriptors + // Find the interface descriptor and allocate the interface object + int offset_intf; + const usb_intf_desc_t *intf_desc = usb_parse_interface_descriptor(config_desc, bInterfaceNumber, bAlternateSetting, &offset_intf); + if (intf_desc == NULL) { + ret = ESP_ERR_NOT_FOUND; + goto exit; + } + // Allocate interface object + interface_t *intf_obj = interface_alloc(client_obj, dev_hdl, intf_desc); + if (intf_obj == NULL) { + ret = ESP_ERR_NO_MEM; + goto exit; + } + // Find each endpoint descriptor in the interface by index, and allocate those endpoints + for (int i = 0; i < intf_desc->bNumEndpoints; i++) { + int offset_ep = offset_intf; + const usb_ep_desc_t *ep_desc = usb_parse_endpoint_descriptor_by_index(intf_desc, i, config_desc->wTotalLength, &offset_ep); + if (ep_desc == NULL) { + ret = ESP_ERR_NOT_FOUND; + goto ep_alloc_err; + } + // Allocate the endpoint wrapper item + ep_wrapper_t *ep_wrap; + ret = ep_wrapper_alloc(dev_hdl, ep_desc, intf_obj, &ep_wrap); + if (ret != ESP_OK) { + goto ep_alloc_err; + } + // Fill the interface object with the allocated endpoints + intf_obj->constant.endpoints[i] = ep_wrap; + } + // Add interface object to client (safe because we have already taken the mutex) + TAILQ_INSERT_TAIL(&client_obj->mux_protected.interface_tailq, intf_obj, mux_protected.tailq_entry); + // Add each endpoint wrapper item to the client's endpoint list + HOST_ENTER_CRITICAL(); + for (int i = 0; i < intf_desc->bNumEndpoints; i++) { + TAILQ_INSERT_TAIL(&client_obj->dynamic.idle_ep_tailq, intf_obj->constant.endpoints[i], dynamic.tailq_entry); + } + HOST_EXIT_CRITICAL(); + // Write back result + *intf_obj_ret = intf_obj; + ret = ESP_OK; + return ret; + +ep_alloc_err: + for (int i = 0; i < intf_desc->bNumEndpoints; i++) { + ep_wrapper_free(dev_hdl, intf_obj->constant.endpoints[i]); + intf_obj->constant.endpoints[i] = NULL; + } + interface_free(intf_obj); +exit: + return ret; +} + +static esp_err_t interface_release(client_t *client_obj, usb_device_handle_t dev_hdl, uint8_t bInterfaceNumber) +{ + esp_err_t ret; + // Find the interface object + interface_t *intf_obj_iter; + interface_t *intf_obj = NULL; + TAILQ_FOREACH(intf_obj_iter, &client_obj->mux_protected.interface_tailq, mux_protected.tailq_entry) { + if (intf_obj_iter->constant.dev_hdl == dev_hdl && intf_obj_iter->constant.intf_desc->bInterfaceNumber == bInterfaceNumber) { + intf_obj = intf_obj_iter; + break; + } + } + if (intf_obj == NULL) { + ret = ESP_ERR_NOT_FOUND; + goto exit; + } + + // Check that all endpoints in the interface are in a state to be freed + // Todo: Check that each EP is halted before allowing them to be freed (IDF-7273) + HOST_ENTER_CRITICAL(); + bool can_free = true; + for (int i = 0; i < intf_obj->constant.intf_desc->bNumEndpoints; i++) { + ep_wrapper_t *ep_wrap = intf_obj->constant.endpoints[i]; + // Endpoint must not be on the pending list and must not have in-flight URBs + if (ep_wrap->dynamic.num_urb_inflight != 0 || ep_wrap->dynamic.flags.pending) { + can_free = false; + break; + } + } + if (!can_free) { + HOST_EXIT_CRITICAL(); + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + // Proceed to remove all endpoint wrapper items from the list + for (int i = 0; i < intf_obj->constant.intf_desc->bNumEndpoints; i++) { + TAILQ_REMOVE(&client_obj->dynamic.idle_ep_tailq, intf_obj->constant.endpoints[i], dynamic.tailq_entry); + } + HOST_EXIT_CRITICAL(); + + // Remove the interface object from the list (safe because we have already taken the mutex) + TAILQ_REMOVE(&client_obj->mux_protected.interface_tailq, intf_obj, mux_protected.tailq_entry); + + // Free each endpoint in the interface + for (int i = 0; i < intf_obj->constant.intf_desc->bNumEndpoints; i++) { + ep_wrapper_free(dev_hdl, intf_obj->constant.endpoints[i]); + intf_obj->constant.endpoints[i] = NULL; + } + // Free the interface object itself + interface_free(intf_obj); + ret = ESP_OK; +exit: + return ret; +} + +// ----------------------- Public -------------------------- + +esp_err_t usb_host_interface_claim(usb_host_client_handle_t client_hdl, usb_device_handle_t dev_hdl, uint8_t bInterfaceNumber, uint8_t bAlternateSetting) +{ + HOST_CHECK(client_hdl != NULL && dev_hdl != NULL, ESP_ERR_INVALID_ARG); + client_t *client_obj = (client_t *)client_hdl; + + HOST_ENTER_CRITICAL(); + uint8_t dev_addr; + ESP_ERROR_CHECK(usbh_dev_get_addr(dev_hdl, &dev_addr)); + // Check if client actually opened device + HOST_CHECK_FROM_CRIT(_check_client_opened_device(client_obj, dev_addr), ESP_ERR_INVALID_STATE); + client_obj->dynamic.flags.taking_mux = 1; + HOST_EXIT_CRITICAL(); + + // Take mux lock. This protects the client being released or other clients from claiming interfaces + xSemaphoreTake(p_host_lib_obj->constant.mux_lock, portMAX_DELAY); + esp_err_t ret; + const usb_config_desc_t *config_desc; + ESP_ERROR_CHECK(usbh_dev_get_config_desc(dev_hdl, &config_desc)); + interface_t *intf_obj; + // Claim interface + ret = interface_claim(client_obj, dev_hdl, config_desc, bInterfaceNumber, bAlternateSetting, &intf_obj); + if (ret != ESP_OK) { + goto exit; + } + ret = ESP_OK; +exit: + xSemaphoreGive(p_host_lib_obj->constant.mux_lock); + + HOST_ENTER_CRITICAL(); + if (ret == ESP_OK) { + client_obj->dynamic.flags.num_intf_claimed++; + } + client_obj->dynamic.flags.taking_mux = 0; + HOST_EXIT_CRITICAL(); + return ret; +} + +esp_err_t usb_host_interface_release(usb_host_client_handle_t client_hdl, usb_device_handle_t dev_hdl, uint8_t bInterfaceNumber) +{ + HOST_CHECK(client_hdl != NULL && dev_hdl != NULL, ESP_ERR_INVALID_ARG); + client_t *client_obj = (client_t *)client_hdl; + + HOST_ENTER_CRITICAL(); + uint8_t dev_addr; + ESP_ERROR_CHECK(usbh_dev_get_addr(dev_hdl, &dev_addr)); + // Check if client actually opened device + HOST_CHECK_FROM_CRIT(_check_client_opened_device(client_obj, dev_addr), ESP_ERR_INVALID_STATE); + client_obj->dynamic.flags.taking_mux = 1; + HOST_EXIT_CRITICAL(); + + // Take mux lock. This protects the client being released or other clients from claiming interfaces + xSemaphoreTake(p_host_lib_obj->constant.mux_lock, portMAX_DELAY); + esp_err_t ret = interface_release(client_obj, dev_hdl, bInterfaceNumber); + xSemaphoreGive(p_host_lib_obj->constant.mux_lock); + + HOST_ENTER_CRITICAL(); + if (ret == ESP_OK) { + client_obj->dynamic.flags.num_intf_claimed--; + } + client_obj->dynamic.flags.taking_mux = 0; + HOST_EXIT_CRITICAL(); + + return ret; +} + +esp_err_t usb_host_endpoint_halt(usb_device_handle_t dev_hdl, uint8_t bEndpointAddress) +{ + esp_err_t ret; + usbh_ep_handle_t ep_hdl; + + ret = usbh_ep_get_handle(dev_hdl, bEndpointAddress, &ep_hdl); + if (ret != ESP_OK) { + goto exit; + } + ret = usbh_ep_command(ep_hdl, USBH_EP_CMD_HALT); + +exit: + return ret; +} + +esp_err_t usb_host_endpoint_flush(usb_device_handle_t dev_hdl, uint8_t bEndpointAddress) +{ + esp_err_t ret; + usbh_ep_handle_t ep_hdl; + + ret = usbh_ep_get_handle(dev_hdl, bEndpointAddress, &ep_hdl); + if (ret != ESP_OK) { + goto exit; + } + ret = usbh_ep_command(ep_hdl, USBH_EP_CMD_FLUSH); + +exit: + return ret; +} + +esp_err_t usb_host_endpoint_clear(usb_device_handle_t dev_hdl, uint8_t bEndpointAddress) +{ + esp_err_t ret; + usbh_ep_handle_t ep_hdl; + + ret = usbh_ep_get_handle(dev_hdl, bEndpointAddress, &ep_hdl); + if (ret != ESP_OK) { + goto exit; + } + ret = usbh_ep_command(ep_hdl, USBH_EP_CMD_CLEAR); + +exit: + return ret; +} + +// ------------------------------------------------ Asynchronous I/O --------------------------------------------------- + +// ----------------------- Public -------------------------- + +esp_err_t usb_host_transfer_alloc(size_t data_buffer_size, int num_isoc_packets, usb_transfer_t **transfer) +{ + urb_t *urb = urb_alloc(data_buffer_size, num_isoc_packets); + if (urb == NULL) { + return ESP_ERR_NO_MEM; + } + *transfer = &urb->transfer; + return ESP_OK; +} + +esp_err_t usb_host_transfer_free(usb_transfer_t *transfer) +{ + if (transfer == NULL) { + return ESP_OK; + } + urb_t *urb = __containerof(transfer, urb_t, transfer); + urb_free(urb); + return ESP_OK; +} + +esp_err_t usb_host_transfer_submit(usb_transfer_t *transfer) +{ + HOST_CHECK(transfer != NULL, ESP_ERR_INVALID_ARG); + // Check that transfer and target endpoint are valid + HOST_CHECK(transfer->device_handle != NULL, ESP_ERR_INVALID_ARG); // Target device must be set + HOST_CHECK((transfer->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK) != 0, ESP_ERR_INVALID_ARG); + + usbh_ep_handle_t ep_hdl; + ep_wrapper_t *ep_wrap = NULL; + urb_t *urb_obj = __containerof(transfer, urb_t, transfer); + esp_err_t ret; + + ret = usbh_ep_get_handle(transfer->device_handle, transfer->bEndpointAddress, &ep_hdl); + if (ret != ESP_OK) { + goto err; + } + ep_wrap = usbh_ep_get_context(ep_hdl); + assert(ep_wrap != NULL); + // Check that we are not submitting a transfer already in-flight + HOST_CHECK(!urb_obj->usb_host_inflight, ESP_ERR_NOT_FINISHED); + urb_obj->usb_host_inflight = true; + HOST_ENTER_CRITICAL(); + ep_wrap->dynamic.num_urb_inflight++; + HOST_EXIT_CRITICAL(); + + ret = usbh_ep_enqueue_urb(ep_hdl, urb_obj); + if (ret != ESP_OK) { + goto submit_err; + } + return ret; + +submit_err: + HOST_ENTER_CRITICAL(); + ep_wrap->dynamic.num_urb_inflight--; + HOST_EXIT_CRITICAL(); + urb_obj->usb_host_inflight = false; +err: + return ret; +} + +esp_err_t usb_host_transfer_submit_control(usb_host_client_handle_t client_hdl, usb_transfer_t *transfer) +{ + HOST_CHECK(client_hdl != NULL && transfer != NULL, ESP_ERR_INVALID_ARG); + // Check that control transfer is valid + HOST_CHECK(transfer->device_handle != NULL, ESP_ERR_INVALID_ARG); // Target device must be set + // Control transfers must be targeted at EP 0 + HOST_CHECK((transfer->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK) == 0, ESP_ERR_INVALID_ARG); + + usb_device_handle_t dev_hdl = transfer->device_handle; + urb_t *urb_obj = __containerof(transfer, urb_t, transfer); + // Check that we are not submitting a transfer already in-flight + HOST_CHECK(!urb_obj->usb_host_inflight, ESP_ERR_NOT_FINISHED); + urb_obj->usb_host_inflight = true; + // Save client handle into URB + urb_obj->usb_host_client = (void *)client_hdl; + + esp_err_t ret; + ret = usbh_dev_submit_ctrl_urb(dev_hdl, urb_obj); + if (ret != ESP_OK) { + urb_obj->usb_host_inflight = false; + } + return ret; +} diff --git a/components/usb/usb_phy.c b/components/usb/usb_phy.c new file mode 100644 index 0000000000..bd07287a3c --- /dev/null +++ b/components/usb/usb_phy.c @@ -0,0 +1,391 @@ +/* + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "freertos/FreeRTOS.h" +#include "esp_log.h" +#include "esp_check.h" +#include "esp_private/periph_ctrl.h" +#include "esp_private/usb_phy.h" +#include "soc/usb_dwc_periph.h" +#include "hal/usb_wrap_hal.h" +#include "hal/usb_serial_jtag_hal.h" +#include "esp_rom_gpio.h" +#include "driver/gpio.h" +#include "hal/gpio_ll.h" +#include "soc/soc_caps.h" +#include "soc/usb_pins.h" + +static const char *USBPHY_TAG = "usb_phy"; + +#define USBPHY_NOT_INIT_ERR_STR "USB_PHY is not initialized" + +typedef struct phy_context_t phy_context_t; + +struct phy_context_t { + usb_phy_target_t target; /**< PHY target */ + usb_phy_controller_t controller; /**< PHY controller */ + usb_phy_status_t status; /**< PHY status */ + usb_otg_mode_t otg_mode; /**< USB OTG mode */ + usb_phy_speed_t otg_speed; /**< USB speed */ + usb_phy_ext_io_conf_t *iopins; /**< external PHY I/O pins */ + usb_wrap_hal_context_t wrap_hal; /**< USB WRAP HAL context */ +#if SOC_USB_SERIAL_JTAG_SUPPORTED + usb_serial_jtag_hal_context_t usj_hal; /**< USJ HAL context */ +#endif +}; + +typedef struct { + phy_context_t *internal_phy; /**< internal PHY context */ + phy_context_t *external_phy; /**< external PHY context */ + uint32_t ref_count; /**< reference count used to protect p_phy_ctrl_obj */ +} phy_ctrl_obj_t; + +/** + * @brief A pin descriptor for initialize external PHY I/O pins + */ +typedef struct { + int pin; /**< GPIO pin num */ + const int func; /**< GPIO matrix signal */ + const bool is_output; /**< input/output signal */ +} usb_iopin_dsc_t; + +static phy_ctrl_obj_t *p_phy_ctrl_obj = NULL; +static portMUX_TYPE phy_spinlock = portMUX_INITIALIZER_UNLOCKED; + +static esp_err_t phy_iopins_configure(const usb_iopin_dsc_t *usb_periph_iopins, int iopins_num) +{ + for (int i = 0; i < iopins_num; i++) { + const usb_iopin_dsc_t iopin = usb_periph_iopins[i]; + if (iopin.pin != GPIO_NUM_NC) { + ESP_RETURN_ON_FALSE((iopin.is_output && GPIO_IS_VALID_OUTPUT_GPIO(iopin.pin)) || + (!iopin.is_output && GPIO_IS_VALID_GPIO(iopin.pin)), + ESP_ERR_INVALID_ARG, USBPHY_TAG, "io_num argument is invalid"); + esp_rom_gpio_pad_select_gpio(iopin.pin); + if (iopin.is_output) { + esp_rom_gpio_connect_out_signal(iopin.pin, iopin.func, false, false); + } else { + esp_rom_gpio_connect_in_signal(iopin.pin, iopin.func, false); + gpio_ll_input_enable(&GPIO, iopin.pin); + } + esp_rom_gpio_pad_unhold(iopin.pin); + } + } + return ESP_OK; +} + +static esp_err_t phy_external_iopins_configure(const usb_phy_ext_io_conf_t *ext_io_conf) +{ + const usb_iopin_dsc_t usb_periph_iopins[] = { + {ext_io_conf->vp_io_num, usb_otg_periph_signal.extphy_vp_in, false}, + {ext_io_conf->vm_io_num, usb_otg_periph_signal.extphy_vm_in, false}, + {ext_io_conf->rcv_io_num, usb_otg_periph_signal.extphy_rcv_in, false}, + {ext_io_conf->oen_io_num, usb_otg_periph_signal.extphy_oen_out, true}, + {ext_io_conf->vpo_io_num, usb_otg_periph_signal.extphy_vpo_out, true}, + {ext_io_conf->vmo_io_num, usb_otg_periph_signal.extphy_vmo_out, true}, + }; + + return phy_iopins_configure(usb_periph_iopins, sizeof(usb_periph_iopins) / sizeof(usb_iopin_dsc_t)); +} + +static esp_err_t phy_otg_iopins_configure(const usb_phy_otg_io_conf_t *otg_io_conf) +{ + const usb_iopin_dsc_t usb_periph_iopins[] = { + {otg_io_conf->iddig_io_num, usb_otg_periph_signal.otg_iddig_in, false}, + {otg_io_conf->avalid_io_num, usb_otg_periph_signal.otg_avalid_in, false}, + {otg_io_conf->vbusvalid_io_num, usb_otg_periph_signal.otg_vbusvalid_in, false}, + {otg_io_conf->idpullup_io_num, usb_otg_periph_signal.otg_idpullup_out, true}, + {otg_io_conf->dppulldown_io_num, usb_otg_periph_signal.otg_dppulldown_out, true}, + {otg_io_conf->dmpulldown_io_num, usb_otg_periph_signal.otg_dmpulldown_out, true}, + {otg_io_conf->drvvbus_io_num, usb_otg_periph_signal.otg_drvvbus_out, true}, + {otg_io_conf->bvalid_io_num, usb_otg_periph_signal.srp_bvalid_in, false}, + {otg_io_conf->sessend_io_num, usb_otg_periph_signal.srp_sessend_in, false}, + {otg_io_conf->chrgvbus_io_num, usb_otg_periph_signal.srp_chrgvbus_out, true}, + {otg_io_conf->dischrgvbus_io_num, usb_otg_periph_signal.srp_dischrgvbus_out, true}, + }; + return phy_iopins_configure(usb_periph_iopins, sizeof(usb_periph_iopins) / sizeof(usb_iopin_dsc_t)); +} + +esp_err_t usb_phy_otg_set_mode(usb_phy_handle_t handle, usb_otg_mode_t mode) +{ + ESP_RETURN_ON_FALSE(handle, ESP_ERR_INVALID_ARG, USBPHY_TAG, "handle argument is invalid"); + ESP_RETURN_ON_FALSE(mode < USB_OTG_MODE_MAX, ESP_ERR_INVALID_ARG, USBPHY_TAG, "mode argument is invalid"); + ESP_RETURN_ON_FALSE(handle->controller == USB_PHY_CTRL_OTG, ESP_FAIL, USBPHY_TAG, "phy source is not USB_OTG"); + + handle->otg_mode = mode; + if (mode == USB_OTG_MODE_HOST) { + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, USB_OTG_IDDIG_IN_IDX, false); // connected connector is A side + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, USB_SRP_BVALID_IN_IDX, false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, USB_OTG_VBUSVALID_IN_IDX, false); // receiving a valid Vbus from host + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, USB_OTG_AVALID_IN_IDX, false); // HIGH to force USB host mode + if (handle->target == USB_PHY_TARGET_INT) { + // Configure pull resistors for host + usb_wrap_pull_override_vals_t vals = { + .dp_pu = false, + .dm_pu = false, + .dp_pd = true, + .dm_pd = true, + }; + usb_wrap_hal_phy_enable_pull_override(&handle->wrap_hal, &vals); + } + } else if (mode == USB_OTG_MODE_DEVICE) { + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, USB_OTG_IDDIG_IN_IDX, false); // connected connector is mini-B side + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, USB_SRP_BVALID_IN_IDX, false); // HIGH to force USB device mode + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, USB_OTG_VBUSVALID_IN_IDX, false); // receiving a valid Vbus from device + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, USB_OTG_AVALID_IN_IDX, false); + } + + return ESP_OK; +} + +esp_err_t usb_phy_otg_dev_set_speed(usb_phy_handle_t handle, usb_phy_speed_t speed) +{ + ESP_RETURN_ON_FALSE(handle, ESP_ERR_INVALID_ARG, USBPHY_TAG, "handle argument is invalid"); + ESP_RETURN_ON_FALSE(speed < USB_PHY_SPEED_MAX, ESP_ERR_INVALID_ARG, USBPHY_TAG, "speed argument is invalid"); + ESP_RETURN_ON_FALSE(handle->controller == USB_PHY_CTRL_OTG, ESP_FAIL, USBPHY_TAG, "phy source is not USB_OTG"); + ESP_RETURN_ON_FALSE((handle->target == USB_PHY_TARGET_INT && handle->otg_mode == USB_OTG_MODE_DEVICE), ESP_FAIL, + USBPHY_TAG, "set speed not supported"); + + handle->otg_speed = speed; + // Configure pull resistors for device + usb_wrap_pull_override_vals_t vals = { + .dp_pd = false, + .dm_pd = false, + }; + if (speed == USB_PHY_SPEED_LOW) { + vals.dp_pu = false; + vals.dm_pu = true; + } else { + vals.dp_pu = true; + vals.dm_pu = false; + } + usb_wrap_hal_phy_enable_pull_override(&handle->wrap_hal, &vals); + return ESP_OK; +} + +esp_err_t usb_phy_action(usb_phy_handle_t handle, usb_phy_action_t action) +{ + ESP_RETURN_ON_FALSE(handle, ESP_ERR_INVALID_ARG, USBPHY_TAG, "handle argument is invalid"); + ESP_RETURN_ON_FALSE(action < USB_PHY_ACTION_MAX, ESP_ERR_INVALID_ARG, USBPHY_TAG, "action argument is invalid"); + ESP_RETURN_ON_FALSE((action == USB_PHY_ACTION_HOST_ALLOW_CONN && handle->controller == USB_PHY_CTRL_OTG) || + (action == USB_PHY_ACTION_HOST_FORCE_DISCONN && handle->controller == USB_PHY_CTRL_OTG), + ESP_ERR_INVALID_ARG, USBPHY_TAG, "wrong target for the action"); + + esp_err_t ret = ESP_OK; + switch (action) { + case USB_PHY_ACTION_HOST_ALLOW_CONN: + if (handle->target == USB_PHY_TARGET_INT) { + usb_wrap_hal_phy_enable_test_mode(&handle->wrap_hal, false); + } else { + if (!handle->iopins) { + ret = ESP_FAIL; + ESP_LOGE(USBPHY_TAG, "no I/O pins provided for connection"); + break; + } + /* + Allow for connections on the external PHY by connecting the VP and VM signals to the external PHY. + */ + esp_rom_gpio_connect_in_signal(handle->iopins->vp_io_num, USB_EXTPHY_VP_IDX, false); + esp_rom_gpio_connect_in_signal(handle->iopins->vm_io_num, USB_EXTPHY_VM_IDX, false); + } + break; + + case USB_PHY_ACTION_HOST_FORCE_DISCONN: + if (handle->target == USB_PHY_TARGET_INT) { + /* + We mimic a disconnect by enabling the internal PHY's test mode, + then forcing the output_enable to HIGH. This will cause the received + VP and VM to be zero, thus mimicking a disconnection. + */ + const usb_wrap_test_mode_vals_t vals = { + .tx_enable_n = true, + .tx_dp = false, + .tx_dm = false, + .rx_dp = false, + .rx_dm = false, + .rx_rcv = false, + }; + usb_wrap_hal_phy_test_mode_set_signals(&handle->wrap_hal, &vals); + usb_wrap_hal_phy_enable_test_mode(&handle->wrap_hal, true); + } else { + /* + Disable connections on the external PHY by connecting the VP and VM signals to the constant LOW signal. + */ + 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); + } + break; + + default: + break; + } + + return ret; +} + +static esp_err_t usb_phy_install(void) +{ + portENTER_CRITICAL(&phy_spinlock); + if (p_phy_ctrl_obj) { + // p_phy_ctrl_obj already installed, return immediately + portEXIT_CRITICAL(&phy_spinlock); + return ESP_OK; + } + portEXIT_CRITICAL(&phy_spinlock); + + esp_err_t ret = ESP_OK; + phy_ctrl_obj_t *phy_ctrl_obj = (phy_ctrl_obj_t *) calloc(1, sizeof(phy_ctrl_obj_t)); + ESP_GOTO_ON_FALSE(phy_ctrl_obj, ESP_ERR_NO_MEM, cleanup, USBPHY_TAG, "no mem for USB_PHY driver"); + + portENTER_CRITICAL(&phy_spinlock); + if (!p_phy_ctrl_obj) { + p_phy_ctrl_obj = phy_ctrl_obj; + p_phy_ctrl_obj->ref_count = 0; + } else { + // p_phy_ctrl_obj already installed, need to free resource + portEXIT_CRITICAL(&phy_spinlock); + goto cleanup; + } + usb_wrap_ll_enable_bus_clock(true); + usb_wrap_ll_reset_register(); + // Enable USB peripheral and reset the register + portEXIT_CRITICAL(&phy_spinlock); + return ESP_OK; + +cleanup: + free(phy_ctrl_obj); + return ret; +} + +esp_err_t usb_new_phy(const usb_phy_config_t *config, usb_phy_handle_t *handle_ret) +{ + ESP_RETURN_ON_FALSE(config, ESP_ERR_INVALID_ARG, USBPHY_TAG, "config argument is invalid"); + ESP_RETURN_ON_FALSE(config->target < USB_PHY_TARGET_MAX, ESP_ERR_INVALID_ARG, USBPHY_TAG, "specified PHY argument is invalid"); + ESP_RETURN_ON_FALSE(config->controller < USB_PHY_CTRL_MAX, ESP_ERR_INVALID_ARG, USBPHY_TAG, "specified source argument is invalid"); + + ESP_RETURN_ON_ERROR(usb_phy_install(), USBPHY_TAG, "usb_phy driver installation failed"); + esp_err_t ret = ESP_OK; + bool new_phy = false; + phy_context_t *phy_context = (phy_context_t *) calloc(1, sizeof(phy_context_t)); + ESP_GOTO_ON_FALSE(phy_context, ESP_ERR_NO_MEM, cleanup, USBPHY_TAG, "no mem for phy context"); + + portENTER_CRITICAL(&phy_spinlock); + usb_phy_get_phy_status(config->target, &phy_context->status); + if (phy_context->status == USB_PHY_STATUS_FREE) { + new_phy = true; + p_phy_ctrl_obj->ref_count++; + if (config->target == USB_PHY_TARGET_EXT) { + p_phy_ctrl_obj->external_phy = phy_context; + } else { + p_phy_ctrl_obj->internal_phy = phy_context; + } + } + portEXIT_CRITICAL(&phy_spinlock); + ESP_GOTO_ON_FALSE(new_phy, ESP_ERR_INVALID_STATE, cleanup, USBPHY_TAG, "selected PHY is in use"); + + phy_context->target = config->target; + phy_context->controller = config->controller; + phy_context->status = USB_PHY_STATUS_IN_USE; + + usb_wrap_hal_init(&phy_context->wrap_hal); +#if SOC_USB_SERIAL_JTAG_SUPPORTED + usb_serial_jtag_hal_init(&phy_context->usj_hal); +#endif + if (config->controller == USB_PHY_CTRL_OTG) { +#if USB_WRAP_LL_EXT_PHY_SUPPORTED + usb_wrap_hal_phy_set_external(&phy_context->wrap_hal, (config->target == USB_PHY_TARGET_EXT)); +#endif + } +#if SOC_USB_SERIAL_JTAG_SUPPORTED + else if (config->controller == USB_PHY_CTRL_SERIAL_JTAG) { + usb_serial_jtag_hal_phy_set_external(&phy_context->usj_hal, (config->target == USB_PHY_TARGET_EXT)); + phy_context->otg_mode = USB_OTG_MODE_DEVICE; + phy_context->otg_speed = USB_PHY_SPEED_FULL; + } +#endif + + if (config->target == USB_PHY_TARGET_INT) { + gpio_set_drive_capability(USBPHY_DM_NUM, GPIO_DRIVE_CAP_3); + gpio_set_drive_capability(USBPHY_DP_NUM, GPIO_DRIVE_CAP_3); + } + + *handle_ret = (usb_phy_handle_t) phy_context; + if (config->ext_io_conf && config->target == USB_PHY_TARGET_EXT) { + phy_context->iopins = (usb_phy_ext_io_conf_t *) calloc(1, sizeof(usb_phy_ext_io_conf_t)); + ESP_GOTO_ON_FALSE(phy_context->iopins, ESP_ERR_NO_MEM, cleanup, USBPHY_TAG, "no mem for storing I/O pins"); + memcpy(phy_context->iopins, config->ext_io_conf, sizeof(usb_phy_ext_io_conf_t)); + ESP_ERROR_CHECK(phy_external_iopins_configure(phy_context->iopins)); + } + if (config->otg_mode != USB_PHY_MODE_DEFAULT) { + ESP_ERROR_CHECK(usb_phy_otg_set_mode(*handle_ret, config->otg_mode)); + } + if (config->otg_speed != USB_PHY_SPEED_UNDEFINED) { + ESP_ERROR_CHECK(usb_phy_otg_dev_set_speed(*handle_ret, config->otg_speed)); + } + if (config->otg_io_conf && (phy_context->controller == USB_PHY_CTRL_OTG)) { + ESP_ERROR_CHECK(phy_otg_iopins_configure(config->otg_io_conf)); + } + return ESP_OK; + +cleanup: + free(phy_context->iopins); + free(phy_context); + if (p_phy_ctrl_obj->ref_count == 0) { + free(p_phy_ctrl_obj); + p_phy_ctrl_obj = NULL; + } + return ret; +} + +static void phy_uninstall(void) +{ + phy_ctrl_obj_t *p_phy_ctrl_obj_free = NULL; + portENTER_CRITICAL(&phy_spinlock); + if (p_phy_ctrl_obj->ref_count == 0) { + p_phy_ctrl_obj_free = p_phy_ctrl_obj; + p_phy_ctrl_obj = NULL; + // Disable USB peripheral without reset the module + usb_wrap_ll_enable_bus_clock(false); + } + portEXIT_CRITICAL(&phy_spinlock); + free(p_phy_ctrl_obj_free); +} + +esp_err_t usb_del_phy(usb_phy_handle_t handle) +{ + ESP_RETURN_ON_FALSE(handle, ESP_ERR_INVALID_ARG, USBPHY_TAG, "handle argument is invalid"); + + portENTER_CRITICAL(&phy_spinlock); + p_phy_ctrl_obj->ref_count--; + if (handle->target == USB_PHY_TARGET_EXT) { + p_phy_ctrl_obj->external_phy = NULL; + } else { + // Clear pullup and pulldown loads on D+ / D-, and disable the pads + usb_wrap_hal_phy_disable_pull_override(&handle->wrap_hal); + p_phy_ctrl_obj->internal_phy = NULL; + } + portEXIT_CRITICAL(&phy_spinlock); + free(handle->iopins); + free(handle); + phy_uninstall(); + return ESP_OK; +} + +esp_err_t usb_phy_get_phy_status(usb_phy_target_t target, usb_phy_status_t *status) +{ + ESP_RETURN_ON_FALSE(target < USB_PHY_TARGET_MAX, ESP_ERR_INVALID_ARG, USBPHY_TAG, "argument is invalid"); + ESP_RETURN_ON_FALSE(p_phy_ctrl_obj, ESP_ERR_INVALID_STATE, USBPHY_TAG, USBPHY_NOT_INIT_ERR_STR); + + if (target == USB_PHY_TARGET_EXT && p_phy_ctrl_obj->external_phy) { + *status = p_phy_ctrl_obj->external_phy->status; + } else if (target == USB_PHY_TARGET_INT && p_phy_ctrl_obj->internal_phy) { + *status = p_phy_ctrl_obj->internal_phy->status; + } else { + *status = USB_PHY_STATUS_FREE; + } + return ESP_OK; +} diff --git a/components/usb/usb_private.c b/components/usb/usb_private.c new file mode 100644 index 0000000000..5d082c19d0 --- /dev/null +++ b/components/usb/usb_private.c @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_heap_caps.h" +#include "usb_private.h" +#include "usb/usb_types_ch9.h" + +urb_t *urb_alloc(size_t data_buffer_size, int num_isoc_packets) +{ + urb_t *urb = heap_caps_calloc(1, sizeof(urb_t) + (sizeof(usb_isoc_packet_desc_t) * num_isoc_packets), MALLOC_CAP_DEFAULT); + uint8_t *data_buffer = heap_caps_malloc(data_buffer_size, MALLOC_CAP_DMA); + if (urb == NULL || data_buffer == NULL) { + goto err; + } + // Cast as dummy transfer so that we can assign to const fields + usb_transfer_dummy_t *dummy_transfer = (usb_transfer_dummy_t *)&urb->transfer; + dummy_transfer->data_buffer = data_buffer; + dummy_transfer->data_buffer_size = data_buffer_size; + dummy_transfer->num_isoc_packets = num_isoc_packets; + return urb; +err: + heap_caps_free(urb); + heap_caps_free(data_buffer); + return NULL; +} + +void urb_free(urb_t *urb) +{ + if (urb == NULL) { + return; + } + heap_caps_free(urb->transfer.data_buffer); + heap_caps_free(urb); +} diff --git a/components/usb/usbh.c b/components/usb/usbh.c new file mode 100644 index 0000000000..b402e72a99 --- /dev/null +++ b/components/usb/usbh.c @@ -0,0 +1,1527 @@ +/* + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "sdkconfig.h" +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/portmacro.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_heap_caps.h" +#include "hcd.h" +#include "usbh.h" +#include "usb/usb_helpers.h" +#include "usb/usb_types_ch9.h" + +#define EP_NUM_MIN 1 // The smallest possible non-default endpoint number +#define EP_NUM_MAX 16 // The largest possible non-default endpoint number +#define NUM_NON_DEFAULT_EP ((EP_NUM_MAX - 1) * 2) // The total number of non-default endpoints a device can have. + +// Device action flags. LISTED IN THE ORDER THEY SHOULD BE HANDLED IN within usbh_process(). Some actions are mutually exclusive +typedef enum { + DEV_ACTION_EPn_HALT_FLUSH = (1 << 0), // Halt all non-default endpoints then flush them (called after a device gone is gone) + DEV_ACTION_EP0_FLUSH = (1 << 1), // Retire all URBS submitted to EP0 + DEV_ACTION_EP0_DEQUEUE = (1 << 2), // Dequeue all URBs from EP0 + DEV_ACTION_EP0_CLEAR = (1 << 3), // Move EP0 to the the active state + DEV_ACTION_PROP_GONE_EVT = (1 << 4), // Propagate a USBH_EVENT_DEV_GONE event + DEV_ACTION_FREE = (1 << 5), // Free the device object + DEV_ACTION_PROP_NEW_DEV = (1 << 6), // Propagate a USBH_EVENT_NEW_DEV event +} dev_action_t; + +typedef struct device_s device_t; + +typedef struct { + struct { + usbh_ep_cb_t ep_cb; + void *ep_cb_arg; + hcd_pipe_handle_t pipe_hdl; + device_t *dev; // Pointer to the device object that this endpoint is contained in + const usb_ep_desc_t *ep_desc; // This just stores a pointer endpoint descriptor inside the device's "config_desc" + } constant; +} endpoint_t; + +struct device_s { + // Dynamic members require a critical section + struct { + TAILQ_ENTRY(device_s) tailq_entry; + union { + struct { + uint32_t in_pending_list: 1; + uint32_t is_gone: 1; // Device is gone (disconnected or port error) + uint32_t waiting_free: 1; // Device object is awaiting to be freed + uint32_t enum_lock: 1; // Device is locked for enumeration. Enum information (e.g., address, device/config desc etc) may change + uint32_t reserved28: 28; + }; + uint32_t val; + } flags; + uint32_t action_flags; + int num_ctrl_xfers_inflight; + usb_device_state_t state; + uint32_t open_count; + } dynamic; + // Mux protected members must be protected by the USBH mux_lock when accessed + struct { + /* + - Endpoint object pointers for each possible non-default endpoint + - All OUT EPs are listed before IN EPs (i.e., EP_NUM_MIN OUT ... EP_NUM_MAX OUT ... EP_NUM_MIN IN ... EP_NUM_MAX) + */ + endpoint_t *endpoints[NUM_NON_DEFAULT_EP]; + } mux_protected; + // Constant members do not require a critical section + struct { + // Assigned on device allocation and remain constant for the device's lifetime + hcd_pipe_handle_t default_pipe; + hcd_port_handle_t port_hdl; + usb_speed_t speed; + unsigned int uid; + /* + These fields are can only be changed when enum_lock is set, thus can be treated as constant + */ + uint8_t address; + usb_device_desc_t *desc; + usb_config_desc_t *config_desc; + usb_str_desc_t *str_desc_manu; + usb_str_desc_t *str_desc_product; + usb_str_desc_t *str_desc_ser_num; + } constant; +}; + +typedef struct { + // Dynamic members require a critical section + struct { + TAILQ_HEAD(tailhead_devs, device_s) devs_idle_tailq; // Tailq of all enum and configured devices + TAILQ_HEAD(tailhead_devs_cb, device_s) devs_pending_tailq; // Tailq of devices that need to have their cb called + } dynamic; + // Mux protected members must be protected by the USBH mux_lock when accessed + struct { + uint8_t num_device; // Number of enumerated devices + } mux_protected; + // Constant members do no change after installation thus do not require a critical section + struct { + usb_proc_req_cb_t proc_req_cb; + void *proc_req_cb_arg; + usbh_event_cb_t event_cb; + void *event_cb_arg; + SemaphoreHandle_t mux_lock; + } constant; +} usbh_t; + +static usbh_t *p_usbh_obj = NULL; + +static portMUX_TYPE usbh_lock = portMUX_INITIALIZER_UNLOCKED; + +const char *USBH_TAG = "USBH"; + +#define USBH_ENTER_CRITICAL_ISR() portENTER_CRITICAL_ISR(&usbh_lock) +#define USBH_EXIT_CRITICAL_ISR() portEXIT_CRITICAL_ISR(&usbh_lock) +#define USBH_ENTER_CRITICAL() portENTER_CRITICAL(&usbh_lock) +#define USBH_EXIT_CRITICAL() portEXIT_CRITICAL(&usbh_lock) +#define USBH_ENTER_CRITICAL_SAFE() portENTER_CRITICAL_SAFE(&usbh_lock) +#define USBH_EXIT_CRITICAL_SAFE() portEXIT_CRITICAL_SAFE(&usbh_lock) + +#define USBH_CHECK(cond, ret_val) ({ \ + if (!(cond)) { \ + return (ret_val); \ + } \ +}) +#define USBH_CHECK_FROM_CRIT(cond, ret_val) ({ \ + if (!(cond)) { \ + USBH_EXIT_CRITICAL(); \ + return ret_val; \ + } \ +}) + +// ------------------------------------------------- Forward Declare --------------------------------------------------- + +static bool ep0_pipe_callback(hcd_pipe_handle_t pipe_hdl, hcd_pipe_event_t pipe_event, void *user_arg, bool in_isr); + +static bool epN_pipe_callback(hcd_pipe_handle_t pipe_hdl, hcd_pipe_event_t pipe_event, void *user_arg, bool in_isr); + +static bool _dev_set_actions(device_t *dev_obj, uint32_t action_flags); + +// ----------------------------------------------------- Helpers ------------------------------------------------------- + +static device_t *_find_dev_from_uid(unsigned int uid) +{ + /* + THIS FUNCTION MUST BE CALLED FROM A CRITICAL SECTION + */ + device_t *dev_iter; + + // Search the device lists for a device with the specified address + TAILQ_FOREACH(dev_iter, &p_usbh_obj->dynamic.devs_idle_tailq, dynamic.tailq_entry) { + if (dev_iter->constant.uid == uid) { + return dev_iter; + } + } + TAILQ_FOREACH(dev_iter, &p_usbh_obj->dynamic.devs_pending_tailq, dynamic.tailq_entry) { + if (dev_iter->constant.uid == uid) { + return dev_iter; + } + } + + return NULL; +} + +static device_t *_find_dev_from_addr(uint8_t dev_addr) +{ + /* + THIS FUNCTION MUST BE CALLED FROM A CRITICAL SECTION + */ + device_t *dev_iter; + + // Search the device lists for a device with the specified address + TAILQ_FOREACH(dev_iter, &p_usbh_obj->dynamic.devs_idle_tailq, dynamic.tailq_entry) { + if (dev_iter->constant.address == dev_addr) { + return dev_iter; + } + } + TAILQ_FOREACH(dev_iter, &p_usbh_obj->dynamic.devs_pending_tailq, dynamic.tailq_entry) { + if (dev_iter->constant.address == dev_addr) { + return dev_iter; + } + } + + return NULL; +} + +static inline bool check_ep_addr(uint8_t bEndpointAddress) +{ + /* + Check that the bEndpointAddress is valid + - Must be <= EP_NUM_MAX (e.g., 16) + - Must be >= EP_NUM_MIN (e.g., 1). + - EP0 is the owned/managed by USBH, thus must never by directly addressed by users (see USB 2.0 section 10.5.1.2) + */ + uint8_t addr = bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK; + return (addr >= EP_NUM_MIN) && (addr <= EP_NUM_MAX); +} + +static endpoint_t *get_ep_from_addr(device_t *dev_obj, uint8_t bEndpointAddress) +{ + /* + CALLER IS RESPONSIBLE FOR TAKING THE mux_lock + */ + + // Calculate index to the device's endpoint object list + int index; + // EP_NUM_MIN should map to an index of 0 + index = (bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK) - EP_NUM_MIN; + assert(index >= 0); // Endpoint address is not supported + if (bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK) { + // OUT EPs are listed before IN EPs, so add an offset + index += (EP_NUM_MAX - EP_NUM_MIN); + } + return dev_obj->mux_protected.endpoints[index]; +} + +static inline void set_ep_from_addr(device_t *dev_obj, uint8_t bEndpointAddress, endpoint_t *ep_obj) +{ + /* + CALLER IS RESPONSIBLE FOR TAKING THE mux_lock + */ + + // Calculate index to the device's endpoint object list + int index; + // EP_NUM_MIN should map to an index of 0 + index = (bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK) - EP_NUM_MIN; + assert(index >= 0); // Endpoint address is not supported + if (bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK) { + // OUT EPs are listed before IN EPs, so add an offset + index += (EP_NUM_MAX - EP_NUM_MIN); + } + dev_obj->mux_protected.endpoints[index] = ep_obj; +} + +static bool urb_check_args(urb_t *urb) +{ + if (urb->transfer.callback == NULL) { + ESP_LOGE(USBH_TAG, "usb_transfer_t callback is NULL"); + return false; + } + if (urb->transfer.num_bytes > urb->transfer.data_buffer_size) { + ESP_LOGE(USBH_TAG, "usb_transfer_t num_bytes > data_buffer_size"); + return false; + } + return true; +} + +static bool transfer_check_usb_compliance(usb_transfer_t *transfer, usb_transfer_type_t type, unsigned int mps, bool is_in) +{ + if (type == USB_TRANSFER_TYPE_CTRL) { + // Check that num_bytes and wLength are set correctly + usb_setup_packet_t *setup_pkt = (usb_setup_packet_t *)transfer->data_buffer; + bool mismatch = false; + if (is_in) { + // For IN transfers, 'num_bytes >= sizeof(usb_setup_packet_t) + setup_pkt->wLength' due to MPS rounding + mismatch = (transfer->num_bytes < sizeof(usb_setup_packet_t) + setup_pkt->wLength); + } else { + // For OUT transfers, num_bytes must match 'sizeof(usb_setup_packet_t) + setup_pkt->wLength' + mismatch = (transfer->num_bytes != sizeof(usb_setup_packet_t) + setup_pkt->wLength); + } + if (mismatch) { + ESP_LOGE(USBH_TAG, "usb_transfer_t num_bytes %d and usb_setup_packet_t wLength %d mismatch", transfer->num_bytes, setup_pkt->wLength); + return false; + } + } else if (type == USB_TRANSFER_TYPE_ISOCHRONOUS) { + // Check that there is at least one isochronous packet descriptor + if (transfer->num_isoc_packets <= 0) { + ESP_LOGE(USBH_TAG, "usb_transfer_t num_isoc_packets is 0"); + return false; + } + // Check that sum of all packet lengths add up to transfer length + // If IN, check that each packet length is integer multiple of MPS + int total_num_bytes = 0; + bool mod_mps_all_zero = true; + for (int i = 0; i < transfer->num_isoc_packets; i++) { + total_num_bytes += transfer->isoc_packet_desc[i].num_bytes; + if (transfer->isoc_packet_desc[i].num_bytes % mps != 0) { + mod_mps_all_zero = false; + } + } + if (transfer->num_bytes != total_num_bytes) { + ESP_LOGE(USBH_TAG, "ISOC transfer num_bytes != num_bytes of all packets"); + return false; + } + if (is_in && !mod_mps_all_zero) { + ESP_LOGE(USBH_TAG, "ISOC IN num_bytes not integer multiple of MPS"); + return false; + } + } else { + // Check that IN transfers are integer multiple of MPS + if (is_in && (transfer->num_bytes % mps != 0)) { + ESP_LOGE(USBH_TAG, "IN transfer num_bytes not integer multiple of MPS"); + return false; + } + } + return true; +} + +// --------------------------------------------------- Allocation ------------------------------------------------------ + +static esp_err_t endpoint_alloc(device_t *dev_obj, const usb_ep_desc_t *ep_desc, usbh_ep_config_t *ep_config, endpoint_t **ep_obj_ret) +{ + esp_err_t ret; + endpoint_t *ep_obj; + hcd_pipe_handle_t pipe_hdl; + + ep_obj = heap_caps_calloc(1, sizeof(endpoint_t), MALLOC_CAP_DEFAULT); + if (ep_obj == NULL) { + return ESP_ERR_NO_MEM; + } + // Allocate the EP's underlying pipe + hcd_pipe_config_t pipe_config = { + .callback = epN_pipe_callback, + .callback_arg = (void *)ep_obj, + .context = ep_config->context, + .ep_desc = ep_desc, + .dev_speed = dev_obj->constant.speed, + .dev_addr = dev_obj->constant.address, + }; + ret = hcd_pipe_alloc(dev_obj->constant.port_hdl, &pipe_config, &pipe_hdl); + if (ret != ESP_OK) { + goto pipe_err; + } + // Initialize the endpoint object + ep_obj->constant.pipe_hdl = pipe_hdl; + ep_obj->constant.ep_cb = ep_config->ep_cb; + ep_obj->constant.ep_cb_arg = ep_config->ep_cb_arg; + ep_obj->constant.dev = dev_obj; + ep_obj->constant.ep_desc = ep_desc; + // Return the endpoint object + *ep_obj_ret = ep_obj; + + ret = ESP_OK; + return ret; + +pipe_err: + heap_caps_free(ep_obj); + return ret; +} + +static void endpoint_free(endpoint_t *ep_obj) +{ + if (ep_obj == NULL) { + return; + } + // Deallocate the EP's underlying pipe + ESP_ERROR_CHECK(hcd_pipe_free(ep_obj->constant.pipe_hdl)); + // Free the heap object + heap_caps_free(ep_obj); +} + +static esp_err_t device_alloc(unsigned int uid, + usb_speed_t speed, + hcd_port_handle_t port_hdl, + device_t **dev_obj_ret) +{ + device_t *dev_obj = heap_caps_calloc(1, sizeof(device_t), MALLOC_CAP_DEFAULT); + if (dev_obj == NULL) { + return ESP_ERR_NO_MEM; + } + + esp_err_t ret; + // Allocate a pipe for EP0 + hcd_pipe_config_t pipe_config = { + .callback = ep0_pipe_callback, + .callback_arg = (void *)dev_obj, + .context = (void *)dev_obj, + .ep_desc = NULL, // No endpoint descriptor means we're allocating a pipe for EP0 + .dev_speed = speed, + .dev_addr = 0, + }; + hcd_pipe_handle_t default_pipe_hdl; + ret = hcd_pipe_alloc(port_hdl, &pipe_config, &default_pipe_hdl); + if (ret != ESP_OK) { + goto err; + } + // Initialize device object + dev_obj->dynamic.state = USB_DEVICE_STATE_DEFAULT; + dev_obj->constant.default_pipe = default_pipe_hdl; + dev_obj->constant.port_hdl = port_hdl; + dev_obj->constant.speed = speed; + dev_obj->constant.uid = uid; + // Note: Enumeration related dev_obj->constant fields are initialized later using usbh_dev_set_...() functions + + // Write-back device object + *dev_obj_ret = dev_obj; + ret = ESP_OK; + + return ret; + +err: + heap_caps_free(dev_obj); + return ret; +} + +static void device_free(device_t *dev_obj) +{ + if (dev_obj == NULL) { + return; + } + // Device descriptor might not have been set yet + if (dev_obj->constant.desc) { + heap_caps_free(dev_obj->constant.desc); + } + // Configuration descriptor might not have been set yet + if (dev_obj->constant.config_desc) { + heap_caps_free(dev_obj->constant.config_desc); + } + // String descriptors might not have been set yet + if (dev_obj->constant.str_desc_manu) { + heap_caps_free(dev_obj->constant.str_desc_manu); + } + if (dev_obj->constant.str_desc_product) { + heap_caps_free(dev_obj->constant.str_desc_product); + } + if (dev_obj->constant.str_desc_ser_num) { + heap_caps_free(dev_obj->constant.str_desc_ser_num); + } + ESP_ERROR_CHECK(hcd_pipe_free(dev_obj->constant.default_pipe)); + heap_caps_free(dev_obj); +} + +// ---------------------------------------------------- Callbacks ------------------------------------------------------ + +static bool ep0_pipe_callback(hcd_pipe_handle_t pipe_hdl, hcd_pipe_event_t pipe_event, void *user_arg, bool in_isr) +{ + uint32_t action_flags; + device_t *dev_obj = (device_t *)user_arg; + switch (pipe_event) { + case HCD_PIPE_EVENT_URB_DONE: + // A control transfer completed on EP0's pipe . We need to dequeue it + action_flags = DEV_ACTION_EP0_DEQUEUE; + break; + case HCD_PIPE_EVENT_ERROR_XFER: + case HCD_PIPE_EVENT_ERROR_URB_NOT_AVAIL: + case HCD_PIPE_EVENT_ERROR_OVERFLOW: + // EP0's pipe has encountered an error. We need to retire all URBs, dequeue them, then make the pipe active again + action_flags = DEV_ACTION_EP0_FLUSH | + DEV_ACTION_EP0_DEQUEUE | + DEV_ACTION_EP0_CLEAR; + if (in_isr) { + ESP_EARLY_LOGE(USBH_TAG, "Dev %d EP 0 Error", dev_obj->constant.address); + } else { + ESP_LOGE(USBH_TAG, "Dev %d EP 0 Error", dev_obj->constant.address); + } + break; + case HCD_PIPE_EVENT_ERROR_STALL: + // EP0's pipe encountered a "protocol stall". We just need to dequeue URBs then make the pipe active again + action_flags = DEV_ACTION_EP0_DEQUEUE | DEV_ACTION_EP0_CLEAR; + if (in_isr) { + ESP_EARLY_LOGE(USBH_TAG, "Dev %d EP 0 STALL", dev_obj->constant.address); + } else { + ESP_LOGE(USBH_TAG, "Dev %d EP 0 STALL", dev_obj->constant.address); + } + break; + default: + action_flags = 0; + break; + } + + USBH_ENTER_CRITICAL_SAFE(); + bool call_proc_req_cb = _dev_set_actions(dev_obj, action_flags); + USBH_EXIT_CRITICAL_SAFE(); + + bool yield = false; + if (call_proc_req_cb) { + yield = p_usbh_obj->constant.proc_req_cb(USB_PROC_REQ_SOURCE_USBH, in_isr, p_usbh_obj->constant.proc_req_cb_arg); + } + return yield; +} + +static bool epN_pipe_callback(hcd_pipe_handle_t pipe_hdl, hcd_pipe_event_t pipe_event, void *user_arg, bool in_isr) +{ + endpoint_t *ep_obj = (endpoint_t *)user_arg; + return ep_obj->constant.ep_cb((usbh_ep_handle_t)ep_obj, + (usbh_ep_event_t)pipe_event, + ep_obj->constant.ep_cb_arg, + in_isr); +} + +// -------------------------------------------------- Event Related ---------------------------------------------------- + +static bool _dev_set_actions(device_t *dev_obj, uint32_t action_flags) +{ + /* + THIS FUNCTION MUST BE CALLED FROM A CRITICAL SECTION + */ + if (action_flags == 0) { + return false; + } + bool call_proc_req_cb; + // Check if device is already on the callback list + if (!dev_obj->dynamic.flags.in_pending_list) { + // Move device form idle device list to callback device list + TAILQ_REMOVE(&p_usbh_obj->dynamic.devs_idle_tailq, dev_obj, dynamic.tailq_entry); + TAILQ_INSERT_TAIL(&p_usbh_obj->dynamic.devs_pending_tailq, dev_obj, dynamic.tailq_entry); + dev_obj->dynamic.action_flags |= action_flags; + dev_obj->dynamic.flags.in_pending_list = 1; + call_proc_req_cb = true; + } else { + // The device is already on the callback list, thus a processing request is already pending. + dev_obj->dynamic.action_flags |= action_flags; + call_proc_req_cb = false; + } + return call_proc_req_cb; +} + +static inline void handle_epn_halt_flush(device_t *dev_obj) +{ + // We need to take the mux_lock to access mux_protected members + xSemaphoreTake(p_usbh_obj->constant.mux_lock, portMAX_DELAY); + // Halt then flush all non-default EPs + for (int i = 0; i < NUM_NON_DEFAULT_EP; i++) { + if (dev_obj->mux_protected.endpoints[i] != NULL) { + ESP_ERROR_CHECK(hcd_pipe_command(dev_obj->mux_protected.endpoints[i]->constant.pipe_hdl, HCD_PIPE_CMD_HALT)); + ESP_ERROR_CHECK(hcd_pipe_command(dev_obj->mux_protected.endpoints[i]->constant.pipe_hdl, HCD_PIPE_CMD_FLUSH)); + } + } + xSemaphoreGive(p_usbh_obj->constant.mux_lock); +} + +static inline void handle_ep0_flush(device_t *dev_obj) +{ + ESP_ERROR_CHECK(hcd_pipe_command(dev_obj->constant.default_pipe, HCD_PIPE_CMD_HALT)); + ESP_ERROR_CHECK(hcd_pipe_command(dev_obj->constant.default_pipe, HCD_PIPE_CMD_FLUSH)); +} + +static inline void handle_ep0_dequeue(device_t *dev_obj) +{ + // Empty URBs from EP0's pipe and call the control transfer callback + ESP_LOGD(USBH_TAG, "Default pipe device %d", dev_obj->constant.address); + int num_urbs = 0; + urb_t *urb = hcd_urb_dequeue(dev_obj->constant.default_pipe); + while (urb != NULL) { + num_urbs++; + usbh_event_data_t event_data = { + .event = USBH_EVENT_CTRL_XFER, + .ctrl_xfer_data = { + .dev_hdl = (usb_device_handle_t)dev_obj, + .urb = urb, + }, + }; + p_usbh_obj->constant.event_cb(&event_data, p_usbh_obj->constant.event_cb_arg); + urb = hcd_urb_dequeue(dev_obj->constant.default_pipe); + } + USBH_ENTER_CRITICAL(); + dev_obj->dynamic.num_ctrl_xfers_inflight -= num_urbs; + USBH_EXIT_CRITICAL(); +} + +static inline void handle_ep0_clear(device_t *dev_obj) +{ + // We allow the pipe command to fail just in case the pipe becomes invalid mid command + hcd_pipe_command(dev_obj->constant.default_pipe, HCD_PIPE_CMD_CLEAR); +} + +static inline void handle_prop_gone_evt(device_t *dev_obj) +{ + // Flush EP0's pipe. Then propagate a USBH_EVENT_DEV_GONE event + ESP_LOGE(USBH_TAG, "Device %d gone", dev_obj->constant.address); + usbh_event_data_t event_data = { + .event = USBH_EVENT_DEV_GONE, + .dev_gone_data = { + .dev_addr = dev_obj->constant.address, + .dev_hdl = (usb_device_handle_t)dev_obj, + }, + }; + p_usbh_obj->constant.event_cb(&event_data, p_usbh_obj->constant.event_cb_arg); +} + +static inline void handle_free(device_t *dev_obj) +{ + // Cache a copy of the device's address as we are about to free the device object + const unsigned int dev_uid = dev_obj->constant.uid; + bool all_free; + ESP_LOGD(USBH_TAG, "Freeing device %d", dev_obj->constant.address); + + // We need to take the mux_lock to access mux_protected members + xSemaphoreTake(p_usbh_obj->constant.mux_lock, portMAX_DELAY); + USBH_ENTER_CRITICAL(); + // Remove the device object for it's containing list + if (dev_obj->dynamic.flags.in_pending_list) { + dev_obj->dynamic.flags.in_pending_list = 0; + TAILQ_REMOVE(&p_usbh_obj->dynamic.devs_pending_tailq, dev_obj, dynamic.tailq_entry); + } else { + TAILQ_REMOVE(&p_usbh_obj->dynamic.devs_idle_tailq, dev_obj, dynamic.tailq_entry); + } + USBH_EXIT_CRITICAL(); + p_usbh_obj->mux_protected.num_device--; + all_free = (p_usbh_obj->mux_protected.num_device == 0); + xSemaphoreGive(p_usbh_obj->constant.mux_lock); + device_free(dev_obj); + + // Propagate USBH_EVENT_DEV_FREE event + usbh_event_data_t event_data = { + .event = USBH_EVENT_DEV_FREE, + .dev_free_data = { + .dev_uid = dev_uid, + } + }; + p_usbh_obj->constant.event_cb(&event_data, p_usbh_obj->constant.event_cb_arg); + + // If all devices have been freed, propagate a USBH_EVENT_ALL_FREE event + if (all_free) { + ESP_LOGD(USBH_TAG, "Device all free"); + event_data.event = USBH_EVENT_ALL_FREE; + p_usbh_obj->constant.event_cb(&event_data, p_usbh_obj->constant.event_cb_arg); + } +} + +static inline void handle_prop_new_dev(device_t *dev_obj) +{ + ESP_LOGD(USBH_TAG, "New device %d", dev_obj->constant.address); + usbh_event_data_t event_data = { + .event = USBH_EVENT_NEW_DEV, + .new_dev_data = { + .dev_addr = dev_obj->constant.address, + }, + }; + p_usbh_obj->constant.event_cb(&event_data, p_usbh_obj->constant.event_cb_arg); +} + +// -------------------------------------------- USBH Processing Functions ---------------------------------------------- + +esp_err_t usbh_install(const usbh_config_t *usbh_config) +{ + USBH_CHECK(usbh_config != NULL, ESP_ERR_INVALID_ARG); + USBH_ENTER_CRITICAL(); + USBH_CHECK_FROM_CRIT(p_usbh_obj == NULL, ESP_ERR_INVALID_STATE); + USBH_EXIT_CRITICAL(); + + esp_err_t ret; + usbh_t *usbh_obj = heap_caps_calloc(1, sizeof(usbh_t), MALLOC_CAP_DEFAULT); + SemaphoreHandle_t mux_lock = xSemaphoreCreateMutex(); + if (usbh_obj == NULL || mux_lock == NULL) { + ret = ESP_ERR_NO_MEM; + goto err; + } + // Initialize USBH object + TAILQ_INIT(&usbh_obj->dynamic.devs_idle_tailq); + TAILQ_INIT(&usbh_obj->dynamic.devs_pending_tailq); + usbh_obj->constant.proc_req_cb = usbh_config->proc_req_cb; + usbh_obj->constant.proc_req_cb_arg = usbh_config->proc_req_cb_arg; + usbh_obj->constant.event_cb = usbh_config->event_cb; + usbh_obj->constant.event_cb_arg = usbh_config->event_cb_arg; + usbh_obj->constant.mux_lock = mux_lock; + + // Assign USBH object pointer + USBH_ENTER_CRITICAL(); + if (p_usbh_obj != NULL) { + USBH_EXIT_CRITICAL(); + ret = ESP_ERR_INVALID_STATE; + goto err; + } + p_usbh_obj = usbh_obj; + USBH_EXIT_CRITICAL(); + + ret = ESP_OK; + return ret; + +err: + if (mux_lock != NULL) { + vSemaphoreDelete(mux_lock); + } + heap_caps_free(usbh_obj); + return ret; +} + +esp_err_t usbh_uninstall(void) +{ + // Check that USBH is in a state to be uninstalled + USBH_ENTER_CRITICAL(); + USBH_CHECK_FROM_CRIT(p_usbh_obj != NULL, ESP_ERR_INVALID_STATE); + usbh_t *usbh_obj = p_usbh_obj; + USBH_EXIT_CRITICAL(); + + esp_err_t ret; + // We need to take the mux_lock to access mux_protected members + xSemaphoreTake(usbh_obj->constant.mux_lock, portMAX_DELAY); + if (p_usbh_obj->mux_protected.num_device > 0) { + // There are still devices allocated. Can't uninstall right now. + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + // Check again if we can uninstall + USBH_ENTER_CRITICAL(); + assert(p_usbh_obj == usbh_obj); + p_usbh_obj = NULL; + USBH_EXIT_CRITICAL(); + xSemaphoreGive(usbh_obj->constant.mux_lock); + + // Free resources + vSemaphoreDelete(usbh_obj->constant.mux_lock); + heap_caps_free(usbh_obj); + ret = ESP_OK; + return ret; + +exit: + xSemaphoreGive(p_usbh_obj->constant.mux_lock); + return ret; +} + +esp_err_t usbh_process(void) +{ + USBH_ENTER_CRITICAL(); + USBH_CHECK_FROM_CRIT(p_usbh_obj != NULL, ESP_ERR_INVALID_STATE); + // Keep processing until all device's with pending events have been handled + while (!TAILQ_EMPTY(&p_usbh_obj->dynamic.devs_pending_tailq)) { + // Move the device back into the idle device list, + device_t *dev_obj = TAILQ_FIRST(&p_usbh_obj->dynamic.devs_pending_tailq); + TAILQ_REMOVE(&p_usbh_obj->dynamic.devs_pending_tailq, dev_obj, dynamic.tailq_entry); + TAILQ_INSERT_TAIL(&p_usbh_obj->dynamic.devs_idle_tailq, dev_obj, dynamic.tailq_entry); + // Clear the device's flags + uint32_t action_flags = dev_obj->dynamic.action_flags; + dev_obj->dynamic.action_flags = 0; + dev_obj->dynamic.flags.in_pending_list = 0; + + /* --------------------------------------------------------------------- + Exit critical section to handle device action flags in their listed order + --------------------------------------------------------------------- */ + USBH_EXIT_CRITICAL(); + ESP_LOGD(USBH_TAG, "Processing actions 0x%"PRIx32"", action_flags); + + if (action_flags & DEV_ACTION_EPn_HALT_FLUSH) { + handle_epn_halt_flush(dev_obj); + } + if (action_flags & DEV_ACTION_EP0_FLUSH) { + handle_ep0_flush(dev_obj); + } + if (action_flags & DEV_ACTION_EP0_DEQUEUE) { + handle_ep0_dequeue(dev_obj); + } + if (action_flags & DEV_ACTION_EP0_CLEAR) { + handle_ep0_clear(dev_obj); + } + if (action_flags & DEV_ACTION_PROP_GONE_EVT) { + handle_prop_gone_evt(dev_obj); + } + /* + Note: We make these action flags mutually exclusive in case they happen in rapid succession. They are handled + in the order of precedence + For example + - New device event is requested followed immediately by a disconnection + */ + if (action_flags & DEV_ACTION_FREE) { + handle_free(dev_obj); + } else if (action_flags & DEV_ACTION_PROP_NEW_DEV) { + handle_prop_new_dev(dev_obj); + } + USBH_ENTER_CRITICAL(); + /* --------------------------------------------------------------------- + Re-enter critical sections. All device action flags should have been handled. + --------------------------------------------------------------------- */ + + } + USBH_EXIT_CRITICAL(); + return ESP_OK; +} + +// ---------------------------------------------- Device Pool Functions ------------------------------------------------ + +esp_err_t usbh_devs_num(int *num_devs_ret) +{ + USBH_CHECK(num_devs_ret != NULL, ESP_ERR_INVALID_ARG); + xSemaphoreTake(p_usbh_obj->constant.mux_lock, portMAX_DELAY); + *num_devs_ret = p_usbh_obj->mux_protected.num_device; + xSemaphoreGive(p_usbh_obj->constant.mux_lock); + return ESP_OK; +} + +esp_err_t usbh_devs_addr_list_fill(int list_len, uint8_t *dev_addr_list, int *num_dev_ret) +{ + USBH_CHECK(dev_addr_list != NULL && num_dev_ret != NULL, ESP_ERR_INVALID_ARG); + int num_filled = 0; + device_t *dev_obj; + + USBH_ENTER_CRITICAL(); + /* + Fill list with devices from idle tailq and pending tailq. Only devices that + are fully enumerated are added to the list. Thus, the following devices are + not excluded: + - Devices with their enum_lock set + - Devices not in the configured state + - Devices with address 0 + */ + TAILQ_FOREACH(dev_obj, &p_usbh_obj->dynamic.devs_idle_tailq, dynamic.tailq_entry) { + if (num_filled < list_len) { + if (!dev_obj->dynamic.flags.enum_lock && + dev_obj->dynamic.state == USB_DEVICE_STATE_CONFIGURED && + dev_obj->constant.address != 0) { + dev_addr_list[num_filled] = dev_obj->constant.address; + num_filled++; + } + } else { + // Address list is already full + break; + } + } + // Fill list with devices from pending tailq + TAILQ_FOREACH(dev_obj, &p_usbh_obj->dynamic.devs_pending_tailq, dynamic.tailq_entry) { + if (num_filled < list_len) { + if (!dev_obj->dynamic.flags.enum_lock && + dev_obj->dynamic.state == USB_DEVICE_STATE_CONFIGURED && + dev_obj->constant.address != 0) { + dev_addr_list[num_filled] = dev_obj->constant.address; + num_filled++; + } + } else { + // Address list is already full + break; + } + } + USBH_EXIT_CRITICAL(); + // Write back number of devices filled + *num_dev_ret = num_filled; + return ESP_OK; +} + +esp_err_t usbh_devs_add(unsigned int uid, usb_speed_t dev_speed, hcd_port_handle_t port_hdl) +{ + USBH_CHECK(port_hdl != NULL, ESP_ERR_INVALID_ARG); + esp_err_t ret; + device_t *dev_obj; + + // Allocate a device object (initialized to address 0) + ret = device_alloc(uid, dev_speed, port_hdl, &dev_obj); + if (ret != ESP_OK) { + return ret; + } + + // We need to take the mux_lock to access mux_protected members + xSemaphoreTake(p_usbh_obj->constant.mux_lock, portMAX_DELAY); + USBH_ENTER_CRITICAL(); + + // Check that there is not already a device with the same uid + if (_find_dev_from_uid(uid) != NULL) { + ret = ESP_ERR_INVALID_ARG; + goto exit; + } + // Check that there is not already a device currently with address 0 + if (_find_dev_from_addr(0) != NULL) { + ret = ESP_ERR_NOT_FINISHED; + goto exit; + } + // Add the device to the idle device list + TAILQ_INSERT_TAIL(&p_usbh_obj->dynamic.devs_idle_tailq, dev_obj, dynamic.tailq_entry); + p_usbh_obj->mux_protected.num_device++; + ret = ESP_OK; + +exit: + USBH_EXIT_CRITICAL(); + xSemaphoreGive(p_usbh_obj->constant.mux_lock); + + return ret; +} + +esp_err_t usbh_devs_remove(unsigned int uid) +{ + esp_err_t ret; + device_t *dev_obj; + bool call_proc_req_cb = false; + + USBH_ENTER_CRITICAL(); + dev_obj = _find_dev_from_uid(uid); + if (dev_obj == NULL) { + ret = ESP_ERR_NOT_FOUND; + goto exit; + } + // Mark the device as gone + dev_obj->dynamic.flags.is_gone = 1; + // Check if the device can be freed immediately + if (dev_obj->dynamic.open_count == 0) { + // Device is not currently opened at all. Can free immediately. + call_proc_req_cb = _dev_set_actions(dev_obj, DEV_ACTION_FREE); + } else { + // Device is still opened. Flush endpoints and propagate device gone event + call_proc_req_cb = _dev_set_actions(dev_obj, + DEV_ACTION_EPn_HALT_FLUSH | + DEV_ACTION_EP0_FLUSH | + DEV_ACTION_EP0_DEQUEUE | + DEV_ACTION_PROP_GONE_EVT); + } + ret = ESP_OK; +exit: + USBH_EXIT_CRITICAL(); + + // Call the processing request callback + if (call_proc_req_cb) { + p_usbh_obj->constant.proc_req_cb(USB_PROC_REQ_SOURCE_USBH, false, p_usbh_obj->constant.proc_req_cb_arg); + } + + return ret; +} + +esp_err_t usbh_devs_mark_all_free(void) +{ + USBH_ENTER_CRITICAL(); + /* + Go through the device list and mark each device as waiting to be closed. If the device is not opened at all, we can + disable it immediately. + Note: We manually traverse the list because we need to add/remove items while traversing + */ + bool call_proc_req_cb = false; + bool wait_for_free = false; + for (int i = 0; i < 2; i++) { + device_t *dev_obj_cur; + device_t *dev_obj_next; + // Go through pending list first as it's more efficient + if (i == 0) { + dev_obj_cur = TAILQ_FIRST(&p_usbh_obj->dynamic.devs_pending_tailq); + } else { + dev_obj_cur = TAILQ_FIRST(&p_usbh_obj->dynamic.devs_idle_tailq); + } + while (dev_obj_cur != NULL) { + // Keep a copy of the next item first in case we remove the current item + dev_obj_next = TAILQ_NEXT(dev_obj_cur, dynamic.tailq_entry); + if (dev_obj_cur->dynamic.open_count == 0) { + // Device is not opened. Can free immediately. + call_proc_req_cb |= _dev_set_actions(dev_obj_cur, DEV_ACTION_FREE); + } else { + // Device is still opened. Just mark it as waiting to be freed + dev_obj_cur->dynamic.flags.waiting_free = 1; + } + // At least one device needs to be freed. User needs to wait for USBH_EVENT_ALL_FREE event + wait_for_free = true; + dev_obj_cur = dev_obj_next; + } + } + USBH_EXIT_CRITICAL(); + + if (call_proc_req_cb) { + p_usbh_obj->constant.proc_req_cb(USB_PROC_REQ_SOURCE_USBH, false, p_usbh_obj->constant.proc_req_cb_arg); + } + return (wait_for_free) ? ESP_ERR_NOT_FINISHED : ESP_OK; +} + +esp_err_t usbh_devs_open(uint8_t dev_addr, usb_device_handle_t *dev_hdl) +{ + USBH_CHECK(dev_hdl != NULL, ESP_ERR_INVALID_ARG); + esp_err_t ret; + + USBH_ENTER_CRITICAL(); + // Go through the device lists to find the device with the specified address + device_t *dev_obj = _find_dev_from_addr(dev_addr); + if (dev_obj != NULL) { + // Check if the device is in a state to be opened + if (dev_obj->dynamic.flags.is_gone || // Device is already gone (disconnected) + dev_obj->dynamic.flags.waiting_free) { // Device is waiting to be freed + ret = ESP_ERR_INVALID_STATE; + } else if (dev_obj->dynamic.flags.enum_lock) { // Device's enum_lock is set + ret = ESP_ERR_NOT_ALLOWED; + } else { + dev_obj->dynamic.open_count++; + *dev_hdl = (usb_device_handle_t)dev_obj; + ret = ESP_OK; + } + } else { + ret = ESP_ERR_NOT_FOUND; + } + USBH_EXIT_CRITICAL(); + + return ret; +} + +esp_err_t usbh_devs_close(usb_device_handle_t dev_hdl) +{ + USBH_CHECK(dev_hdl != NULL, ESP_ERR_INVALID_ARG); + device_t *dev_obj = (device_t *)dev_hdl; + + USBH_ENTER_CRITICAL(); + // Device should never be closed while its enum_lock is + USBH_CHECK_FROM_CRIT(!dev_obj->dynamic.flags.enum_lock, ESP_ERR_NOT_ALLOWED); + dev_obj->dynamic.open_count--; + bool call_proc_req_cb = false; + if (dev_obj->dynamic.open_count == 0) { + // Sanity check. + assert(dev_obj->dynamic.num_ctrl_xfers_inflight == 0); // There cannot be any control transfer in-flight + assert(!dev_obj->dynamic.flags.waiting_free); // This can only be set when open_count reaches 0 + if (dev_obj->dynamic.flags.is_gone || dev_obj->dynamic.flags.waiting_free) { + // Device is already gone or is awaiting to be freed. Trigger the USBH process to free the device + call_proc_req_cb = _dev_set_actions(dev_obj, DEV_ACTION_FREE); + } + // Else, there's nothing to do. Leave the device allocated + } + USBH_EXIT_CRITICAL(); + + if (call_proc_req_cb) { + p_usbh_obj->constant.proc_req_cb(USB_PROC_REQ_SOURCE_USBH, false, p_usbh_obj->constant.proc_req_cb_arg); + } + + return ESP_OK; +} + +esp_err_t usbh_devs_new_dev_event(usb_device_handle_t dev_hdl) +{ + device_t *dev_obj = (device_t *)dev_hdl; + bool call_proc_req_cb = false; + + USBH_ENTER_CRITICAL(); + // Device must be in the configured state + USBH_CHECK_FROM_CRIT(dev_obj->dynamic.state == USB_DEVICE_STATE_CONFIGURED, ESP_ERR_INVALID_STATE); + call_proc_req_cb = _dev_set_actions(dev_obj, DEV_ACTION_PROP_NEW_DEV); + USBH_EXIT_CRITICAL(); + + // Call the processing request callback + if (call_proc_req_cb) { + p_usbh_obj->constant.proc_req_cb(USB_PROC_REQ_SOURCE_USBH, false, p_usbh_obj->constant.proc_req_cb_arg); + } + + return ESP_OK; +} + +// ------------------------------------------------ Device Functions --------------------------------------------------- + +// ----------------------- Getters ------------------------- + +esp_err_t usbh_dev_get_addr(usb_device_handle_t dev_hdl, uint8_t *dev_addr) +{ + USBH_CHECK(dev_hdl != NULL && dev_addr != NULL, ESP_ERR_INVALID_ARG); + device_t *dev_obj = (device_t *)dev_hdl; + + USBH_ENTER_CRITICAL(); + USBH_CHECK_FROM_CRIT(dev_obj->constant.address > 0, ESP_ERR_INVALID_STATE); + *dev_addr = dev_obj->constant.address; + USBH_EXIT_CRITICAL(); + + return ESP_OK; +} + +esp_err_t usbh_dev_get_info(usb_device_handle_t dev_hdl, usb_device_info_t *dev_info) +{ + USBH_CHECK(dev_hdl != NULL && dev_info != NULL, ESP_ERR_INVALID_ARG); + device_t *dev_obj = (device_t *)dev_hdl; + + dev_info->speed = dev_obj->constant.speed; + dev_info->dev_addr = dev_obj->constant.address; + // Device descriptor might not have been set yet + if (dev_obj->constant.desc) { + dev_info->bMaxPacketSize0 = dev_obj->constant.desc->bMaxPacketSize0; + } else { + // Use the default pipe's MPS instead + dev_info->bMaxPacketSize0 = hcd_pipe_get_mps(dev_obj->constant.default_pipe); + } + // Configuration descriptor might not have been set yet + if (dev_obj->constant.config_desc) { + dev_info->bConfigurationValue = dev_obj->constant.config_desc->bConfigurationValue; + } else { + dev_info->bConfigurationValue = 0; + } + dev_info->str_desc_manufacturer = dev_obj->constant.str_desc_manu; + dev_info->str_desc_product = dev_obj->constant.str_desc_product; + dev_info->str_desc_serial_num = dev_obj->constant.str_desc_ser_num; + + return ESP_OK; +} + +esp_err_t usbh_dev_get_desc(usb_device_handle_t dev_hdl, const usb_device_desc_t **dev_desc_ret) +{ + USBH_CHECK(dev_hdl != NULL && dev_desc_ret != NULL, ESP_ERR_INVALID_ARG); + device_t *dev_obj = (device_t *)dev_hdl; + + *dev_desc_ret = dev_obj->constant.desc; + return ESP_OK; +} + +esp_err_t usbh_dev_get_config_desc(usb_device_handle_t dev_hdl, const usb_config_desc_t **config_desc_ret) +{ + USBH_CHECK(dev_hdl != NULL && config_desc_ret != NULL, ESP_ERR_INVALID_ARG); + device_t *dev_obj = (device_t *)dev_hdl; + + *config_desc_ret = dev_obj->constant.config_desc; + + return ESP_OK; +} + +// ----------------------- Setters ------------------------- + +esp_err_t usbh_dev_enum_lock(usb_device_handle_t dev_hdl) +{ + USBH_CHECK(dev_hdl != NULL, ESP_ERR_INVALID_ARG); + esp_err_t ret; + device_t *dev_obj = (device_t *)dev_hdl; + + // We need to take the mux_lock to access mux_protected members + xSemaphoreTake(p_usbh_obj->constant.mux_lock, portMAX_DELAY); + + /* + The device's enum_lock can only be set when the following conditions are met: + - No other endpoints except EP0 have been allocated + - We are the sole opener + - Device's enum_lock is not already set + */ + // Check that no other endpoints except EP0 have been allocated + bool ep_found = false; + for (int i = 0; i < NUM_NON_DEFAULT_EP; i++) { + if (dev_obj->mux_protected.endpoints[i] != NULL) { + ep_found = true; + break; + } + } + if (ep_found) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + // Check that we are the sole opener and enum_lock is not already set + USBH_ENTER_CRITICAL(); + if (!dev_obj->dynamic.flags.enum_lock && (dev_obj->dynamic.open_count == 1)) { + dev_obj->dynamic.flags.enum_lock = true; + ret = ESP_OK; + } else { + ret = ESP_ERR_INVALID_STATE; + } + USBH_EXIT_CRITICAL(); + +exit: + xSemaphoreGive(p_usbh_obj->constant.mux_lock); + + return ret; +} + +esp_err_t usbh_dev_enum_unlock(usb_device_handle_t dev_hdl) +{ + USBH_CHECK(dev_hdl != NULL, ESP_ERR_INVALID_ARG); + esp_err_t ret; + device_t *dev_obj = (device_t *)dev_hdl; + + USBH_ENTER_CRITICAL(); + // Device's enum_lock must have been previously set + if (dev_obj->dynamic.flags.enum_lock) { + assert(dev_obj->dynamic.open_count == 1); // We must still be the sole opener + dev_obj->dynamic.flags.enum_lock = false; + ret = ESP_OK; + } else { + ret = ESP_ERR_INVALID_STATE; + } + USBH_EXIT_CRITICAL(); + + return ret; +} + +esp_err_t usbh_dev_set_ep0_mps(usb_device_handle_t dev_hdl, uint16_t wMaxPacketSize) +{ + USBH_CHECK(dev_hdl != NULL, ESP_ERR_INVALID_ARG); + esp_err_t ret; + device_t *dev_obj = (device_t *)dev_hdl; + + USBH_ENTER_CRITICAL(); + // Device's EP0 MPS can only be updated when in the default state + if (dev_obj->dynamic.state != USB_DEVICE_STATE_DEFAULT) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + // Device's enum_lock must be set before enumeration related data fields can be set + if (dev_obj->dynamic.flags.enum_lock) { + ret = hcd_pipe_update_mps(dev_obj->constant.default_pipe, wMaxPacketSize); + } else { + ret = ESP_ERR_NOT_ALLOWED; + } +exit: + USBH_EXIT_CRITICAL(); + + return ret; +} + +esp_err_t usbh_dev_set_addr(usb_device_handle_t dev_hdl, uint8_t dev_addr) +{ + USBH_CHECK(dev_hdl != NULL, ESP_ERR_INVALID_ARG); + esp_err_t ret; + device_t *dev_obj = (device_t *)dev_hdl; + + USBH_ENTER_CRITICAL(); + // Device's address can only be set when in the default state + USBH_CHECK_FROM_CRIT(dev_obj->dynamic.state == USB_DEVICE_STATE_DEFAULT, ESP_ERR_INVALID_STATE); + // Device's enum_lock must be set before enumeration related data fields can be set + USBH_CHECK_FROM_CRIT(dev_obj->dynamic.flags.enum_lock, ESP_ERR_NOT_ALLOWED); + // Update the device and default pipe's target address + ret = hcd_pipe_update_dev_addr(dev_obj->constant.default_pipe, dev_addr); + if (ret == ESP_OK) { + dev_obj->constant.address = dev_addr; + dev_obj->dynamic.state = USB_DEVICE_STATE_ADDRESS; + } + USBH_EXIT_CRITICAL(); + + return ret; +} + +esp_err_t usbh_dev_set_desc(usb_device_handle_t dev_hdl, const usb_device_desc_t *device_desc) +{ + USBH_CHECK(dev_hdl != NULL && device_desc != NULL, ESP_ERR_INVALID_ARG); + esp_err_t ret; + device_t *dev_obj = (device_t *)dev_hdl; + usb_device_desc_t *new_desc, *old_desc; + + // Allocate and copy new device descriptor + new_desc = heap_caps_malloc(sizeof(usb_device_desc_t), MALLOC_CAP_DEFAULT); + if (new_desc == NULL) { + return ESP_ERR_NO_MEM; + } + memcpy(new_desc, device_desc, sizeof(usb_device_desc_t)); + + USBH_ENTER_CRITICAL(); + // Device's descriptor can only be set in the default or addressed state + if (!(dev_obj->dynamic.state == USB_DEVICE_STATE_DEFAULT || dev_obj->dynamic.state == USB_DEVICE_STATE_ADDRESS)) { + ret = ESP_ERR_INVALID_STATE; + goto err; + } + // Device's enum_lock must be set before we can set its device descriptor + if (!dev_obj->dynamic.flags.enum_lock) { + ret = ESP_ERR_NOT_ALLOWED; + goto err; + } + old_desc = dev_obj->constant.desc; // Save old descriptor for cleanup + dev_obj->constant.desc = new_desc; // Assign new descriptor + USBH_EXIT_CRITICAL(); + + // Clean up old descriptor or failed assignment + heap_caps_free(old_desc); + ret = ESP_OK; + + return ret; + +err: + USBH_EXIT_CRITICAL(); + heap_caps_free(new_desc); + return ret; +} + +esp_err_t usbh_dev_set_config_desc(usb_device_handle_t dev_hdl, const usb_config_desc_t *config_desc_full) +{ + USBH_CHECK(dev_hdl != NULL && config_desc_full != NULL, ESP_ERR_INVALID_ARG); + esp_err_t ret; + device_t *dev_obj = (device_t *)dev_hdl; + usb_config_desc_t *new_desc, *old_desc; + + // Allocate and copy new config descriptor + new_desc = heap_caps_malloc(config_desc_full->wTotalLength, MALLOC_CAP_DEFAULT); + if (new_desc == NULL) { + return ESP_ERR_NO_MEM; + } + memcpy(new_desc, config_desc_full, config_desc_full->wTotalLength); + + USBH_ENTER_CRITICAL(); + // Device's config descriptor can only be set when in the addressed state + if (dev_obj->dynamic.state != USB_DEVICE_STATE_ADDRESS) { + ret = ESP_ERR_INVALID_STATE; + goto err; + } + // Device's enum_lock must be set before we can set its config descriptor + if (!dev_obj->dynamic.flags.enum_lock) { + ret = ESP_ERR_NOT_ALLOWED; + goto err; + } + old_desc = dev_obj->constant.config_desc; // Save old descriptor for cleanup + dev_obj->constant.config_desc = new_desc; // Assign new descriptor + dev_obj->dynamic.state = USB_DEVICE_STATE_CONFIGURED; + USBH_EXIT_CRITICAL(); + + // Clean up old descriptor or failed assignment + heap_caps_free(old_desc); + ret = ESP_OK; + + return ret; + +err: + USBH_EXIT_CRITICAL(); + heap_caps_free(new_desc); + return ret; +} + +esp_err_t usbh_dev_set_str_desc(usb_device_handle_t dev_hdl, const usb_str_desc_t *str_desc, int select) +{ + USBH_CHECK(dev_hdl != NULL && str_desc != NULL && (select >= 0 && select < 3), ESP_ERR_INVALID_ARG); + esp_err_t ret; + device_t *dev_obj = (device_t *)dev_hdl; + usb_str_desc_t *new_desc, *old_desc; + + // Allocate and copy new string descriptor + new_desc = heap_caps_malloc(str_desc->bLength, MALLOC_CAP_DEFAULT); + if (new_desc == NULL) { + return ESP_ERR_NO_MEM; + } + memcpy(new_desc, str_desc, str_desc->bLength); + + USBH_ENTER_CRITICAL(); + // Device's string descriptors can only be set when in the default state + if (dev_obj->dynamic.state != USB_DEVICE_STATE_CONFIGURED) { + ret = ESP_ERR_INVALID_STATE; + goto err; + } + // Device's enum_lock must be set before we can set its string descriptors + if (!dev_obj->dynamic.flags.enum_lock) { + ret = ESP_ERR_NOT_ALLOWED; + goto err; + } + // Assign to the selected descriptor + switch (select) { + case 0: + old_desc = dev_obj->constant.str_desc_manu; + dev_obj->constant.str_desc_manu = new_desc; + break; + case 1: + old_desc = dev_obj->constant.str_desc_product; + dev_obj->constant.str_desc_product = new_desc; + break; + default: // 2 + old_desc = dev_obj->constant.str_desc_ser_num; + dev_obj->constant.str_desc_ser_num = new_desc; + break; + } + USBH_EXIT_CRITICAL(); + + // Clean up old descriptor or failed assignment + heap_caps_free(old_desc); + ret = ESP_OK; + + return ret; + +err: + USBH_EXIT_CRITICAL(); + heap_caps_free(new_desc); + return ret; +} + +// ----------------------------------------------- Endpoint Functions ------------------------------------------------- + +esp_err_t usbh_ep_alloc(usb_device_handle_t dev_hdl, usbh_ep_config_t *ep_config, usbh_ep_handle_t *ep_hdl_ret) +{ + USBH_CHECK(dev_hdl != NULL && ep_config != NULL && ep_hdl_ret != NULL, ESP_ERR_INVALID_ARG); + uint8_t bEndpointAddress = ep_config->bEndpointAddress; + USBH_CHECK(check_ep_addr(bEndpointAddress), ESP_ERR_INVALID_ARG); + + esp_err_t ret; + device_t *dev_obj = (device_t *)dev_hdl; + endpoint_t *ep_obj; + USBH_CHECK(dev_obj->constant.config_desc, ESP_ERR_INVALID_STATE); // Configuration descriptor must be set + + // Find the endpoint descriptor from the device's current configuration descriptor + const usb_ep_desc_t *ep_desc = usb_parse_endpoint_descriptor_by_address(dev_obj->constant.config_desc, ep_config->bInterfaceNumber, ep_config->bAlternateSetting, ep_config->bEndpointAddress, NULL); + if (ep_desc == NULL) { + return ESP_ERR_NOT_FOUND; + } + // Allocate the endpoint object + ret = endpoint_alloc(dev_obj, ep_desc, ep_config, &ep_obj); + if (ret != ESP_OK) { + goto alloc_err; + } + + // We need to take the mux_lock to access mux_protected members + xSemaphoreTake(p_usbh_obj->constant.mux_lock, portMAX_DELAY); + USBH_ENTER_CRITICAL(); + // Check the device's state before we assign the a pipe to the allocated endpoint + if (dev_obj->dynamic.state != USB_DEVICE_STATE_CONFIGURED) { + USBH_EXIT_CRITICAL(); + ret = ESP_ERR_INVALID_STATE; + goto dev_state_err; + } + USBH_EXIT_CRITICAL(); + // Check if the endpoint has already been allocated + if (get_ep_from_addr(dev_obj, bEndpointAddress) == NULL) { + set_ep_from_addr(dev_obj, bEndpointAddress, ep_obj); + // Write back the endpoint handle + *ep_hdl_ret = (usbh_ep_handle_t)ep_obj; + ret = ESP_OK; + } else { + // Endpoint is already allocated + ret = ESP_ERR_INVALID_STATE; + } +dev_state_err: + xSemaphoreGive(p_usbh_obj->constant.mux_lock); + + // If the endpoint was not assigned, free it + if (ret != ESP_OK) { + endpoint_free(ep_obj); + } +alloc_err: + return ret; +} + +esp_err_t usbh_ep_free(usbh_ep_handle_t ep_hdl) +{ + USBH_CHECK(ep_hdl != NULL, ESP_ERR_INVALID_ARG); + + esp_err_t ret; + endpoint_t *ep_obj = (endpoint_t *)ep_hdl; + device_t *dev_obj = (device_t *)ep_obj->constant.dev; + uint8_t bEndpointAddress = ep_obj->constant.ep_desc->bEndpointAddress; + + // Todo: Check that the EP's underlying pipe is halted before allowing the EP to be freed (IDF-7273) + // Check that the the EP's underlying pipe has no more in-flight URBs + if (hcd_pipe_get_num_urbs(ep_obj->constant.pipe_hdl) != 0) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + + // We need to take the mux_lock to access mux_protected members + xSemaphoreTake(p_usbh_obj->constant.mux_lock, portMAX_DELAY); + // Check if the endpoint was allocated on this device + if (ep_obj == get_ep_from_addr(dev_obj, bEndpointAddress)) { + // Clear the endpoint from the device's endpoint object list + set_ep_from_addr(dev_obj, bEndpointAddress, NULL); + ret = ESP_OK; + } else { + ret = ESP_ERR_NOT_FOUND; + } + xSemaphoreGive(p_usbh_obj->constant.mux_lock); + + // Finally, we free the endpoint object + if (ret == ESP_OK) { + endpoint_free(ep_obj); + } +exit: + return ret; +} + +esp_err_t usbh_ep_get_handle(usb_device_handle_t dev_hdl, uint8_t bEndpointAddress, usbh_ep_handle_t *ep_hdl_ret) +{ + USBH_CHECK(dev_hdl != NULL && ep_hdl_ret != NULL, ESP_ERR_INVALID_ARG); + USBH_CHECK(check_ep_addr(bEndpointAddress), ESP_ERR_INVALID_ARG); + + esp_err_t ret; + device_t *dev_obj = (device_t *)dev_hdl; + endpoint_t *ep_obj; + + // We need to take the mux_lock to access mux_protected members + xSemaphoreTake(p_usbh_obj->constant.mux_lock, portMAX_DELAY); + ep_obj = get_ep_from_addr(dev_obj, bEndpointAddress); + xSemaphoreGive(p_usbh_obj->constant.mux_lock); + if (ep_obj) { + *ep_hdl_ret = (usbh_ep_handle_t)ep_obj; + ret = ESP_OK; + } else { + ret = ESP_ERR_NOT_FOUND; + } + + return ret; +} + +esp_err_t usbh_ep_command(usbh_ep_handle_t ep_hdl, usbh_ep_cmd_t command) +{ + USBH_CHECK(ep_hdl != NULL, ESP_ERR_INVALID_ARG); + + endpoint_t *ep_obj = (endpoint_t *)ep_hdl; + // Send the command to the EP's underlying pipe + return hcd_pipe_command(ep_obj->constant.pipe_hdl, (hcd_pipe_cmd_t)command); +} + +void *usbh_ep_get_context(usbh_ep_handle_t ep_hdl) +{ + assert(ep_hdl); + endpoint_t *ep_obj = (endpoint_t *)ep_hdl; + return hcd_pipe_get_context(ep_obj->constant.pipe_hdl); +} + +// ----------------------------------------------- Transfer Functions -------------------------------------------------- + +esp_err_t usbh_dev_submit_ctrl_urb(usb_device_handle_t dev_hdl, urb_t *urb) +{ + USBH_CHECK(dev_hdl != NULL && urb != NULL, ESP_ERR_INVALID_ARG); + device_t *dev_obj = (device_t *)dev_hdl; + USBH_CHECK(urb_check_args(urb), ESP_ERR_INVALID_ARG); + bool xfer_is_in = ((usb_setup_packet_t *)urb->transfer.data_buffer)->bmRequestType & USB_BM_REQUEST_TYPE_DIR_IN; + // Device descriptor could still be NULL at this point, so we get the MPS from the pipe instead. + unsigned int mps = hcd_pipe_get_mps(dev_obj->constant.default_pipe); + USBH_CHECK(transfer_check_usb_compliance(&(urb->transfer), USB_TRANSFER_TYPE_CTRL, mps, xfer_is_in), ESP_ERR_INVALID_ARG); + + USBH_ENTER_CRITICAL(); + // Increment the control transfer count first + dev_obj->dynamic.num_ctrl_xfers_inflight++; + USBH_EXIT_CRITICAL(); + + esp_err_t ret; + if (hcd_pipe_get_state(dev_obj->constant.default_pipe) != HCD_PIPE_STATE_ACTIVE) { + ret = ESP_ERR_INVALID_STATE; + goto hcd_err; + } + ret = hcd_urb_enqueue(dev_obj->constant.default_pipe, urb); + if (ret != ESP_OK) { + goto hcd_err; + } + ret = ESP_OK; + return ret; + +hcd_err: + USBH_ENTER_CRITICAL(); + dev_obj->dynamic.num_ctrl_xfers_inflight--; + USBH_EXIT_CRITICAL(); + return ret; +} + +esp_err_t usbh_ep_enqueue_urb(usbh_ep_handle_t ep_hdl, urb_t *urb) +{ + USBH_CHECK(ep_hdl != NULL && urb != NULL, ESP_ERR_INVALID_ARG); + USBH_CHECK(urb_check_args(urb), ESP_ERR_INVALID_ARG); + + endpoint_t *ep_obj = (endpoint_t *)ep_hdl; + + USBH_CHECK(transfer_check_usb_compliance(&(urb->transfer), + USB_EP_DESC_GET_XFERTYPE(ep_obj->constant.ep_desc), + USB_EP_DESC_GET_MPS(ep_obj->constant.ep_desc), + USB_EP_DESC_GET_EP_DIR(ep_obj->constant.ep_desc)), + ESP_ERR_INVALID_ARG); + // Check that the EP's underlying pipe is in the active state before submitting the URB + if (hcd_pipe_get_state(ep_obj->constant.pipe_hdl) != HCD_PIPE_STATE_ACTIVE) { + return ESP_ERR_INVALID_STATE; + } + // Enqueue the URB to the EP's underlying pipe + return hcd_urb_enqueue(ep_obj->constant.pipe_hdl, urb); +} + +esp_err_t usbh_ep_dequeue_urb(usbh_ep_handle_t ep_hdl, urb_t **urb_ret) +{ + USBH_CHECK(ep_hdl != NULL && urb_ret != NULL, ESP_ERR_INVALID_ARG); + + endpoint_t *ep_obj = (endpoint_t *)ep_hdl; + // Enqueue the URB to the EP's underlying pipe + *urb_ret = hcd_urb_dequeue(ep_obj->constant.pipe_hdl); + return ESP_OK; +} diff --git a/zephyr/esp32s3/CMakeLists.txt b/zephyr/esp32s3/CMakeLists.txt index 406b742377..e0e4e922cc 100644 --- a/zephyr/esp32s3/CMakeLists.txt +++ b/zephyr/esp32s3/CMakeLists.txt @@ -217,6 +217,18 @@ if(CONFIG_SOC_SERIES_ESP32S3) ) endif() + if (CONFIG_UDC_DWC2) + zephyr_include_directories( + ../../components/usb/include + ) + + zephyr_sources( + ../../components/driver/gpio/gpio.c + ../../components/driver/gpio/rtc_io.c + ../../components/hal/usb_wrap_hal.c + ) + endif() + zephyr_sources_ifdef( CONFIG_DMA_ESP32 ../../components/soc/lldesc.c