/* * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include "sdkconfig.h" #include "esp_err.h" #include "esp_log.h" #include "esp_heap_caps.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "usb_private.h" #include "usb/usb_types_ch9.h" #include "usb/usb_types_ch11.h" #include "usbh.h" #include "ext_port.h" #include "usb/usb_helpers.h" // Amount of attempts to reset the port with a device in case of a failure #if (CONFIG_USB_HOST_EXT_PORT_RESET_ATTEMPTS) #define EXT_PORT_RESET_ATTEMPTS CONFIG_USB_HOST_EXT_PORT_RESET_ATTEMPTS #else #define EXT_PORT_RESET_ATTEMPTS 1 #endif // Delay in ms after sending the SetFeature() class specific request #define EXT_PORT_RESET_CUSTOM_DELAY CONFIG_USB_HOST_EXT_PORT_CUSTOM_RESET_ENABLE #define EXT_PORT_RESET_CUSTOM_DELAY_MS CONFIG_USB_HOST_EXT_PORT_CUSTOM_RESET_MS #define EXT_PORT_RESET_DEFAULT_DELAY_MS 100 /** * @brief External Port driver action flags */ typedef enum { PORT_ACTION_HANDLE = (1 << 0), /**< Port requires handling */ PORT_ACTION_DISABLE = (1 << 1), /**< Disable port */ PORT_ACTION_RECYCLE = (1 << 2), /**< Recycle port */ PORT_ACTION_RESET = (1 << 3), /**< Reset port */ PORT_ACTION_GET_STATUS = (1 << 4), /**< Get status request */ } port_action_t; /** * @brief State of the device, attached to the port */ typedef enum { PORT_DEV_NOT_PRESENT = 0, PORT_DEV_PRESENT } port_dev_state_t; /** * @brief Object representing a single External Port */ struct ext_port_s { /**< Single thread members don't require a critical section so long as they are never accessed from multiple threads */ TAILQ_ENTRY(ext_port_s) tailq_entry; union { struct { uint32_t in_pending_list: 1; /**< Port in pending list */ uint32_t status_lock: 1; /**< Ports' status lock. Parent Hub processes receiving new status */ uint32_t status_outdated: 1; /**< Ports' has changed and status is not valid. Get port status is required */ uint32_t is_gone: 1; /**< Ports' parent Hub is gone */ uint32_t has_enum_device: 1; /**< Port has an enumerated device */ uint32_t waiting_recycle: 1; /**< Port is waiting to be recycled */ uint32_t waiting_free: 1; /**< Port is waiting to be freed */ uint32_t reserved25: 25; /**< Reserved */ }; uint32_t val; /**< Ports' flags value */ } flags; /**< Ports' flags */ uint32_t action_flags; /**< Ports' action flags */ usb_hub_port_state_t state; /**< Ports' state */ usb_port_status_t status; /**< Ports' status data */ port_dev_state_t dev_state; /**< Ports' device state */ uint8_t dev_reset_attempts; /**< Ports' device reset failure */ struct { // Ports' parent info for optimisation and better debug output usb_device_handle_t parent_dev_hdl; /**< Ports' parent device handle */ uint8_t parent_dev_addr; /**< Ports' parent device bus address */ // Port related constant members ext_hub_handle_t ext_hub_hdl; /**< Ports' parent External Hub handle */ uint8_t port_num; /**< Ports' parent External Hub Port number */ int reset_delay_ms; /**< Ports' Power on time to Power Good, ms */ } constant; /**< Constant members. Do not change after installation thus do not require a critical section or mutex */ }; /** * @brief Definition of the type for the struct, representing a single External Port */ typedef struct ext_port_s ext_port_t; /** * @brief Object representing the External Port Driver */ typedef struct { struct { TAILQ_HEAD(ext_ports, ext_port_s) pending_tailq; /**< External Ports require handling */ } single_thread; /**< Single thread members don't require a critical section so long as they are never accessed from multiple threads */ struct { ext_port_cb_t proc_req_cb; /**< External Port process callback */ void *proc_req_cb_arg; /**< External Port process callback argument */ ext_port_event_cb_t event_cb; /**< External Port event callback */ void *event_cb_arg; /**< External Port event callback argument */ } constant; /**< Constant members. Do not change after installation thus do not require a critical section or mutex */ } ext_port_driver_t; static ext_port_driver_t *p_ext_port_driver = NULL; const char *EXT_PORT_TAG = "EXT_PORT"; // ----------------------------------------------------------------------------- // --------------------------- Helpers & Macros -------------------------------- // ----------------------------------------------------------------------------- #define EXT_PORT_CHECK(cond, ret_val) ({ \ if (!(cond)) { \ return (ret_val); \ } \ }) /** * @brief Port connection status * * @param[in] ext_port Port object * @return * - true Port has a connection * - false Port doesn't have a connection */ static inline bool port_has_connection(ext_port_t *ext_port) { return ext_port->status.wPortStatus.PORT_CONNECTION ? true : false; } /** * @brief Port power status * * @param[in] ext_port Port object * @return * - true Port is powered * - false Port in not powered */ static inline bool port_is_powered(ext_port_t *ext_port) { return ext_port->status.wPortStatus.PORT_POWER ? true : false; } /** * @brief Port reset status * * @param[in] ext_port Port object * @return * - true Port is in reset * - false Port is not in reset */ static inline bool port_is_in_reset(ext_port_t *ext_port) { return ext_port->status.wPortStatus.PORT_RESET ? true : false; } /** * @brief Port enable status * * @param[in] ext_port Port object * @return * - true Port is enabled * - false Port is disabled */ static inline bool port_is_enabled(ext_port_t *ext_port) { return ext_port->status.wPortStatus.PORT_ENABLE ? true : false; } /** * @brief Port connection status changed * * @param[in] ext_port Port object * @return * - true Port connection changed * - false Port connection not changed */ static inline bool port_has_changed_connection(ext_port_t * ext_port) { return ext_port->status.wPortChange.C_PORT_CONNECTION ? true : false; } /** * @brief Port enabled status changed * * @param[in] ext_port Port object * @return * - true Port has error state and was disabled * - false Port operates normally */ static inline bool port_has_changed_from_enable(ext_port_t *ext_port) { return ext_port->status.wPortChange.C_PORT_ENABLE ? true : false; } /** * @brief Port reset status changed * * @param[in] ext_port Port object * @return * - true Port has finished the reset * - false Port hasn't finished the reset */ static inline bool port_has_finished_reset(ext_port_t *ext_port) { return ext_port->status.wPortChange.C_PORT_RESET ? true : false; } // ----------------------------------------------------------------------------- // ------------------------ Parent Hub related logic --------------------------- // ----------------------------------------------------------------------------- /** * @brief Request the port status for the port object * * @note This call uses the External Hub Driver API * * @param[in] ext_port Port object * @return * - ESP_ERR_NOT_ALLOWED: The External Hub Driver has not been installed * - ESP_ERR_INVALID_ARG: The parent hub handle couldn't be NULL * - ESP_ERR_INVALID_SIZE: The port number should be in a range: [1, .. , bNbrPort] * - ESP_ERR_INVALID_STATE: The parent hub device wasn't configured * - ESP_OK: Status has been requested */ static esp_err_t port_request_status(ext_port_t* ext_port) { esp_err_t ret = ext_hub_get_port_status(ext_port->constant.ext_hub_hdl, ext_port->constant.port_num); if (ret != ESP_OK) { return ret; } // Port is requesting status, lock the status ext_port->flags.status_lock = 1; return ret; } /** * @brief Sets the feature to the port * * @note This call uses the External Hub Driver API * * @param[in] ext_port Port object * @param[in] feature Port feature to set * @return * - ESP_ERR_NOT_ALLOWED: The External Hub Driver has not been installed * - ESP_ERR_INVALID_ARG: The parent hub handle couldn't be NULL * - ESP_ERR_INVALID_SIZE: The port number should be in a range: [1, .. , bNbrPort] * - ESP_ERR_INVALID_STATE: The parent hub device wasn't configured * - ESP_OK: SetPortFeature() has been requested */ static esp_err_t port_set_feature(ext_port_t *ext_port, const usb_hub_port_feature_t feature) { esp_err_t ret = ext_hub_set_port_feature(ext_port->constant.ext_hub_hdl, ext_port->constant.port_num, feature); if (ret != ESP_OK) { return ret; } // Every set feature requires status update ext_port->flags.status_outdated = 1; // PowerOn to PowerGood delay for port if (feature == USB_FEATURE_PORT_RESET) { vTaskDelay(pdMS_TO_TICKS(ext_port->constant.reset_delay_ms)); } return ret; } /** * @brief Clears the feature to the port * * @note This call uses the External Hub Driver API * * @param[in] ext_port Port object * @param[in] feature Port feature to set * @return * - ESP_ERR_NOT_ALLOWED: The External Hub Driver has not been installed * - ESP_ERR_INVALID_ARG: The parent hub handle couldn't be NULL * - ESP_ERR_INVALID_SIZE: The port number should be in a range: [1, .. , bNbrPort] * - ESP_ERR_INVALID_STATE: The parent hub device wasn't configured * - ESP_OK: ClearPortFeature() has been requested */ static esp_err_t port_clear_feature(ext_port_t *ext_port, const usb_hub_port_feature_t feature) { esp_err_t ret = ext_hub_clear_port_feature(ext_port->constant.ext_hub_hdl, ext_port->constant.port_num, feature); if (ret != ESP_OK) { return ret; } // Every clearing feature requires status update ext_port->flags.status_outdated = 1; return ret; } // ----------------------------------------------------------------------------- // ---------------------------- Internal logic --------------------------------- // ----------------------------------------------------------------------------- /** * @brief Port set actions * * @note * - The External Port Driver is a single threaded driver * - All calls are made from USB Host related task * - Does not need critical section * - Always requires processing * * @param[in] ext_port Port object * @param[in] action_flags Action flags (could not be 0) */ static void port_set_actions(ext_port_t *ext_port, uint32_t action_flags) { assert(action_flags != 0); // Sanity check // Check if port is not in the pending list if (!ext_port->flags.in_pending_list) { // Add port to pending list ext_port->flags.in_pending_list = 1; TAILQ_INSERT_TAIL(&p_ext_port_driver->single_thread.pending_tailq, ext_port, tailq_entry); } ext_port->action_flags |= action_flags; p_ext_port_driver->constant.proc_req_cb(p_ext_port_driver->constant.proc_req_cb_arg); } /** * @brief Stops the handling and remove the port from the pending list * * When the ports parent hub device has been removed, the port could be freed immediately * if it doesn't have an inserted device. * For this purpose port object should be removed from the pending * list and the action flags should be dropped. * * @note * - The External Port Driver is a single threaded driver * - All calls are made from USB Host related task * - Does not need critical section * - Does not require processing * * @param[in] ext_port Port object */ static void port_stop_handling(ext_port_t *ext_port) { // Check if port is in the pending list if (ext_port->flags.in_pending_list) { // Add port to pending list ext_port->flags.in_pending_list = 0; TAILQ_REMOVE(&p_ext_port_driver->single_thread.pending_tailq, ext_port, tailq_entry); } if (ext_port->action_flags) { // Port should be freed but has actions ESP_LOGD(EXT_PORT_TAG, "[%d:%d] Port stop handling. Dropped actions 0x%"PRIx32"", ext_port->constant.parent_dev_addr, ext_port->constant.port_num, ext_port->action_flags); } ext_port->action_flags = 0; } /** * @brief Port event propagation * * Propagating the following events to the Hub Driver: * - EXT_PORT_CONNECTED * - EXT_PORT_RESET_COMPLETED * - EXT_PORT_DISCONNECTED * * Filling all the event data parameters, according to the event. * * @param[in] ext_port Port object * @param[in] event Port event */ static void port_event(ext_port_t *ext_port, ext_port_event_t event) { ext_port_event_data_t event_data = { .event = event, }; switch (event) { case EXT_PORT_CONNECTED: event_data.connected.ext_hub_hdl = ext_port->constant.ext_hub_hdl; event_data.connected.parent_dev_hdl = ext_port->constant.parent_dev_hdl; event_data.connected.parent_port_num = ext_port->constant.port_num; break; case EXT_PORT_RESET_COMPLETED: event_data.reset_completed.parent_dev_hdl = ext_port->constant.parent_dev_hdl; event_data.reset_completed.parent_port_num = ext_port->constant.port_num; break; case EXT_PORT_DISCONNECTED: event_data.disconnected.parent_dev_hdl = ext_port->constant.parent_dev_hdl; event_data.disconnected.parent_port_num = ext_port->constant.port_num; break; default: // Should never occur abort(); break; } p_ext_port_driver->constant.event_cb(&event_data, p_ext_port_driver->constant.event_cb_arg); } /** * @brief Returns first port from the pending list * * @return * - Port object pointer. NULL if no ports in pending list. */ static ext_port_t *get_port_from_pending_list(void) { ext_port_t *ext_port = NULL; if (!TAILQ_EMPTY(&p_ext_port_driver->single_thread.pending_tailq)) { ext_port = TAILQ_FIRST(&p_ext_port_driver->single_thread.pending_tailq); } return ext_port; } /** * @brief Allocates port object * * Allocates new por object with following parameters: * - Port state: USB_PORT_STATE_NOT_CONFIGURED * - Port device status: PORT_DEV_NOT_PRESENT * * @param[in] ext_hub_hdl Ports' parent hub handle * @param[in] parent_dev_hdl Ports' parent device handle * @param[in] parent_port_num Ports' parent port number * @param[in] port_delay_ms Ports' Power on time to Power Good, ms * @param[out] port_obj Port objects' pointer * @return * - ESP_ERR_INVALID_ARG: Unable to allocate the port object: parent hub handle and parent device handle must be not NULL * - ESP_ERR_NO_MEM: Unable to allocate the port object: no memory * - ESP_ERR_NOT_FINISHED: Unable to allocate the port object: parent device is not available * - ESP_OK: Port object created successfully */ static esp_err_t port_alloc(ext_hub_handle_t ext_hub_hdl, usb_device_handle_t parent_dev_hdl, uint8_t parent_port_num, uint16_t port_delay_ms, ext_port_t **port_obj) { uint8_t parent_dev_addr = 0; EXT_PORT_CHECK(ext_hub_hdl != NULL && parent_dev_hdl != NULL, ESP_ERR_INVALID_ARG); // This is the one exception from the requirement to use only the Ext Hub Driver API. // TODO: IDF-10023 Move responsibility of parent-child tree building to Hub Driver instead of USBH EXT_PORT_CHECK(usbh_dev_get_addr(parent_dev_hdl, &parent_dev_addr) == ESP_OK, ESP_ERR_NOT_FINISHED); ext_port_t *ext_port = heap_caps_calloc(1, sizeof(ext_port_t), MALLOC_CAP_DEFAULT); if (ext_port == NULL) { return ESP_ERR_NO_MEM; } ext_port->constant.parent_dev_hdl = parent_dev_hdl; ext_port->constant.parent_dev_addr = parent_dev_addr; ext_port->constant.ext_hub_hdl = ext_hub_hdl; ext_port->constant.port_num = parent_port_num; #if (EXT_PORT_RESET_CUSTOM_DELAY) ext_port->constant.reset_delay_ms = EXT_PORT_RESET_CUSTOM_DELAY_MS; #else ext_port->constant.reset_delay_ms = (port_delay_ms == 0) ? EXT_PORT_RESET_DEFAULT_DELAY_MS : port_delay_ms; #endif // EXT_PORT_POWER_ON_CUSTOM ext_port->state = USB_PORT_STATE_NOT_CONFIGURED; ext_port->dev_state = PORT_DEV_NOT_PRESENT; ESP_LOGD(EXT_PORT_TAG, "[%d:%d] Port has been added (PwrOn2PwrGood=%d ms)", ext_port->constant.parent_dev_addr, ext_port->constant.port_num, ext_port->constant.reset_delay_ms); *port_obj = ext_port; return ESP_OK; } /** * @brief Port object handling complete * * @note This is the final stage of port object handling. * After port being handled, the port object: * - is removed from the pending list * - is freed, is the port gone or port object waiting to be freed * * The final stage is parent notification about port object handling completion to enable the EP IN * * @param[in] ext_port Port object * @return * - ESP_OK: Port object handle completed */ static esp_err_t handle_complete(ext_port_t *ext_port) { bool all_ports_were_handled = true; bool has_pending_ports = false; ESP_LOGD(EXT_PORT_TAG, "[%d:%d] Port is %s", ext_port->constant.parent_dev_addr, ext_port->constant.port_num, ext_port->flags.is_gone ? "gone" : (ext_port->dev_state == PORT_DEV_PRESENT) ? "active" : "idle"); assert(ext_port->flags.is_gone == 0); // Handle complete could be called only on port that is still available assert(ext_port->flags.waiting_free == 0); // Handle completion could be called only on port that should be freed assert(ext_port->action_flags == 0); // Port should not have any further actions assert(ext_port->flags.waiting_recycle == 0); // Port should not await to be recycled assert(ext_port->flags.in_pending_list == 1); // Port should be in pending list // Remove port from it port_stop_handling(ext_port); // Verify, that list has more ports in pending list if (!TAILQ_EMPTY(&p_ext_port_driver->single_thread.pending_tailq)) { has_pending_ports = true; } // Port handling complete #if ENABLE_MULTIPLE_HUBS // When multiply Hubs are attached, we can have ports in pending list, but for another parent ext_port_t *port = NULL; TAILQ_FOREACH(port, &p_ext_port_driver->single_thread.pending_tailq, tailq_entry) { if (port->constant.ext_hub_hdl == ext_port->constant.ext_hub_hdl) { // Port with same parent has been found all_ports_were_handled = false; break; } } #else // When only one hub is supported - all ports have the same parent // all_port_were_handled should be triggered if no more ports in pending list all_ports_were_handled = !has_pending_ports; #endif // if (all_ports_were_handled) { // Notify parent to enable Interrupt EP ext_hub_status_handle_complete(ext_port->constant.ext_hub_hdl); } if (has_pending_ports) { // Re-trigger the processing if there are more ports in pending list p_ext_port_driver->constant.proc_req_cb(p_ext_port_driver->constant.proc_req_cb_arg); } return ESP_OK; } /** * @brief Handles port status * * @param[in] ext_port Port object * @return * - true: Port has a status, that requires processing * - false: Port has a status, that doesn't require processing */ static bool handle_port_status(ext_port_t *ext_port) { bool need_processing = false; if (port_is_in_reset(ext_port)) { ESP_LOGD(EXT_PORT_TAG, "[%d:%d] Port still in reset, wait and repeat get status...", ext_port->constant.parent_dev_addr, ext_port->constant.port_num); // PowerOn to PowerGood delay for port vTaskDelay(pdMS_TO_TICKS(ext_port->constant.reset_delay_ms)); port_request_status(ext_port); need_processing = true; } return need_processing; } /** * @brief Handles port connection * * @param[in] ext_port Port object */ static void handle_port_connection(ext_port_t *ext_port) { bool has_device = false; switch (ext_port->state) { case USB_PORT_STATE_POWERED_OFF: if (!port_is_powered(ext_port)) { ext_port->state = USB_PORT_STATE_DISCONNECTED; port_set_feature(ext_port, USB_FEATURE_PORT_POWER); } break; case USB_PORT_STATE_DISCONNECTED: case USB_PORT_STATE_DISABLED: if (port_has_connection(ext_port)) { if (ext_port->dev_state == PORT_DEV_PRESENT) { ESP_LOGE(EXT_PORT_TAG, "[%d:%d] Mismatch port state and device status", ext_port->constant.parent_dev_addr, ext_port->constant.port_num); has_device = true; } else { // New device connected, flush reset attempts ext_port->dev_reset_attempts = 0; ext_port->state = USB_PORT_STATE_RESETTING; // New device has not been enumerated yet, reset the flag ext_port->flags.has_enum_device = 0; } } break; case USB_PORT_STATE_RESETTING: if (!port_has_connection(ext_port)) { if (ext_port->dev_state == PORT_DEV_PRESENT) { ESP_LOGE(EXT_PORT_TAG, "[%d:%d] Failed to issue downstream port reset", ext_port->constant.parent_dev_addr, ext_port->constant.port_num); has_device = true; } else { ext_port->state = USB_PORT_STATE_DISCONNECTED; } } break; case USB_PORT_STATE_ENABLED: // TODO: IDF-10071 Port debounce mechanism if (ext_port->dev_state == PORT_DEV_PRESENT) { ext_port->flags.waiting_recycle = 1; } break; default: // Should never occur abort(); break; } if (has_device) { ext_port->flags.waiting_recycle = 1; ext_port->dev_state = PORT_DEV_NOT_PRESENT; port_event(ext_port, EXT_PORT_DISCONNECTED); } } /** * @brief Handles port changes * * @param[in] ext_port Port object * @return * - true: Port has a change, that requires processing * - false: Port has no changes and doesn't require processing */ static bool handle_port_changes(ext_port_t *ext_port) { bool need_processing = false; if (port_has_changed_connection(ext_port)) { handle_port_connection(ext_port); port_clear_feature(ext_port, USB_FEATURE_C_PORT_CONNECTION); need_processing = true; } else if (port_has_changed_from_enable(ext_port)) { // For more information, refer to section 11.8.1 Port Error of usb_2.0 specification ESP_LOGE(EXT_PORT_TAG, "[%d:%d] Error on port (state=%d, dev=%d)", ext_port->constant.parent_dev_addr, ext_port->constant.port_num, ext_port->state, ext_port->dev_state == PORT_DEV_PRESENT); port_clear_feature(ext_port, USB_FEATURE_C_PORT_ENABLE); need_processing = true; } else if (port_has_finished_reset(ext_port)) { if (port_has_connection(ext_port)) { ext_port->state = USB_PORT_STATE_ENABLED; } port_clear_feature(ext_port, USB_FEATURE_C_PORT_RESET); need_processing = true; } return need_processing; } /** * @brief Handles port state * * @param[in] ext_port Port object */ static void handle_port_state(ext_port_t *ext_port) { bool need_handling = false; usb_hub_port_state_t curr_state = ext_port->state; usb_hub_port_state_t new_state = ext_port->state; switch (curr_state) { case USB_PORT_STATE_NOT_CONFIGURED: new_state = USB_PORT_STATE_POWERED_OFF; port_request_status(ext_port); need_handling = true; break; case USB_PORT_STATE_POWERED_OFF: // Port power state depends on the wHubCharacteristics.power_switching new_state = USB_PORT_STATE_DISCONNECTED; port_set_feature(ext_port, USB_FEATURE_PORT_POWER); need_handling = true; break; case USB_PORT_STATE_DISCONNECTED: if (port_has_connection(ext_port)) { if (ext_port->dev_reset_attempts < EXT_PORT_RESET_ATTEMPTS) { // Available, it EXT_PORT_RESET_ATTEMPTS > 1 ext_port->dev_reset_attempts++; port_set_feature(ext_port, USB_FEATURE_PORT_RESET); need_handling = true; } else { ESP_LOGE(EXT_PORT_TAG, "[%d:%d] Unable to reset the device", ext_port->constant.parent_dev_addr, ext_port->constant.port_num); } } break; case USB_PORT_STATE_DISABLED: if (port_has_connection(ext_port)) { // This logic does depend on the moment, when we propagate the EXT_PORT_DISCONNECTED event // during the port disable. if (!ext_port->flags.has_enum_device && ext_port->flags.waiting_recycle) { // Port was disabled before enumeration, so the USBH device object was not created. // Clean the recycle flag and complete port handling with device attached. ext_port->flags.waiting_recycle = 0; } } break; case USB_PORT_STATE_RESETTING: if (port_has_connection(ext_port)) { // Port in resetting state and has connection if (ext_port->dev_state == PORT_DEV_NOT_PRESENT) { assert(ext_port->dev_reset_attempts == 0); // First reset, attempts always should be 0 ext_port->dev_reset_attempts++; port_set_feature(ext_port, USB_FEATURE_PORT_RESET); } else { assert(port_is_enabled(ext_port)); // Port should be enabled port_event(ext_port, EXT_PORT_RESET_COMPLETED); new_state = USB_PORT_STATE_ENABLED; } need_handling = true; } else { // Port in resetting state and doesn't have connection // Error case, could be, when device was removed during port reset ESP_LOGE(EXT_PORT_TAG, "[%d:%d] Device gone during port reset, recover port", ext_port->constant.parent_dev_addr, ext_port->constant.port_num); new_state = USB_PORT_STATE_DISCONNECTED; } break; case USB_PORT_STATE_ENABLED: if (port_is_enabled(ext_port)) { if (port_has_connection(ext_port)) { if (ext_port->dev_state == PORT_DEV_NOT_PRESENT) { usb_speed_t dev_speed = USB_SPEED_LOW; if (ext_port->status.wPortStatus.PORT_LOW_SPEED == 0) { dev_speed = ext_port->status.wPortStatus.PORT_HIGH_SPEED ? USB_SPEED_HIGH : USB_SPEED_FULL; } ESP_LOGD(EXT_PORT_TAG, "Device speed %s", (char *[]) { "Low", "Full", "High" }[dev_speed]); ext_port->dev_state = PORT_DEV_PRESENT; port_event(ext_port, EXT_PORT_CONNECTED); } else { // Port enabled, device present, reset completed ext_port->dev_reset_attempts = 0; port_event(ext_port, EXT_PORT_RESET_COMPLETED); } need_handling = true; } else { // TODO: IDF-10071 Port debounce mechanism ESP_LOGE(EXT_PORT_TAG, "[%d:%d] Enabled, but doesn't have connection", ext_port->constant.parent_dev_addr, ext_port->constant.port_num); } } else { // If port was enabled, there should be an active device if (ext_port->dev_state == PORT_DEV_PRESENT) { ext_port->dev_state = PORT_DEV_NOT_PRESENT; port_event(ext_port, EXT_PORT_DISCONNECTED); ext_port->flags.waiting_recycle = 1; } else { // Error state ESP_LOGE(EXT_PORT_TAG, "[%d:%d] Enabled, but doesn't have a device", ext_port->constant.parent_dev_addr, ext_port->constant.port_num); new_state = USB_PORT_STATE_DISCONNECTED; } } break; default: // Should never occur abort(); break; } if (curr_state != new_state) { ESP_LOGD(EXT_PORT_TAG, "New state: %d", new_state); ext_port->state = new_state; } if (!ext_port->flags.waiting_recycle && !need_handling) { handle_complete(ext_port); } } /** * @brief Port object handling action * * @note Handles the port in the following order: * - Port changes (C_CONNECTION, C_ENABLE, C_RESET) * - Port status (Port still in RESET state) * - Port state * * When ports' handling has been completed, removes the port object from the pending list. * * @param[in] ext_port Port object */ static void handle_port(ext_port_t *ext_port) { ESP_LOGD(EXT_PORT_TAG, "[%d:%d] change=0x%04x, status=0x%04x, state=%d", ext_port->constant.parent_dev_addr, ext_port->constant.port_num, ext_port->status.wPortChange.val, ext_port->status.wPortStatus.val, ext_port->state); if (handle_port_changes(ext_port)) { return; } else if (handle_port_status(ext_port)) { return; } handle_port_state(ext_port); } /** * @brief Port object handling recycle action * * @note Port should undergo a recycle procedure when: * - Device has been detached and freed by all clients (port is present) * - Parent device been detached (port is gone) * - Port has run an error during the handling * * @param[in] ext_port Port object */ static void handle_recycle(ext_port_t *ext_port) { assert(ext_port->flags.waiting_recycle == 1); ESP_LOGD(EXT_PORT_TAG, "[%d:%d] Port recycle (state=%d, dev=%d)", ext_port->constant.parent_dev_addr, ext_port->constant.port_num, ext_port->state, ext_port->dev_state); ext_port->flags.waiting_recycle = 0; if (ext_port->flags.status_lock) { // Port is awaiting the status return; } // Port should not have any changes assert(ext_port->status.wPortChange.val == 0); switch (ext_port->state) { case USB_PORT_STATE_DISABLED: // We don't need to do anything, as port will be handled after completing USB_FEATURE_PORT_ENABLE if (ext_port->flags.is_gone) { handle_complete(ext_port); } break; default: ext_port->state = USB_PORT_STATE_DISCONNECTED; if (ext_port->flags.is_gone) { handle_complete(ext_port); } else { handle_port(ext_port); } break; } } /** * @brief Port object handling disable action * * @param[in] ext_port Port object */ static void handle_disable(ext_port_t *ext_port) { ESP_LOGD(EXT_PORT_TAG, "[%d:%d] Disable (state=%d, dev=%d)", ext_port->constant.parent_dev_addr, ext_port->constant.port_num, ext_port->state, ext_port->dev_state); assert(ext_port->state != USB_PORT_STATE_POWERED_OFF); assert(ext_port->state != USB_PORT_STATE_DISCONNECTED); assert(ext_port->state != USB_PORT_STATE_NOT_CONFIGURED); if (ext_port->state == USB_PORT_STATE_ENABLED) { if (port_has_connection(ext_port)) { ESP_LOGE(EXT_PORT_TAG, "[%d:%d] Port disabled, reset attempts=%d", ext_port->constant.parent_dev_addr, ext_port->constant.port_num, ext_port->dev_reset_attempts); // Do not try to reset port anymore ext_port->dev_reset_attempts = EXT_PORT_RESET_ATTEMPTS; if (ext_port->dev_state == PORT_DEV_PRESENT) { ext_port->dev_state = PORT_DEV_NOT_PRESENT; // Propagate device disconnection if device present port_event(ext_port, EXT_PORT_DISCONNECTED); ext_port->flags.waiting_recycle = 1; } } } ext_port->state = USB_PORT_STATE_DISABLED; if (!ext_port->flags.is_gone) { // Port not gone, disable port port_clear_feature(ext_port, USB_FEATURE_PORT_ENABLE); } } // ----------------------------------------------------------------------------- // ------------------------ External Port API ---------------------------------- // ----------------------------------------------------------------------------- /** * @brief Creates new port object * * @note This function should only be called from the External Hub Driver * * @param[in] port_cfg Port configuration * @param[out] port_hdl Port object handle * @return * - ESP_ERR_INVALID_STATE: The External Port Driver has not been installed * - ESP_ERR_INVALID_ARG: Unable to create the port, arguments couldn't be NULL * - ESP_ERR_NO_MEM: Unable to allocate the port object: no memory * - ESP_ERR_NOT_FINISHED: Unable to allocate the port object: parent device is not available * - ESP_OK: Port has been created and added to the pending list */ static esp_err_t port_new(void *port_cfg, void **port_hdl) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_INVALID_STATE); EXT_PORT_CHECK(port_cfg != NULL && port_hdl != NULL, ESP_ERR_INVALID_ARG); ext_port_t *port = NULL; ext_port_config_t *config = (ext_port_config_t *)port_cfg; esp_err_t ret = port_alloc(config->ext_hub_hdl, config->parent_dev_hdl, config->parent_port_num, config->port_power_delay_ms, &port); if (ret != ESP_OK) { *port_hdl = NULL; goto exit; } port_set_actions(port, PORT_ACTION_HANDLE); *port_hdl = (ext_port_hdl_t) port; exit: return ret; } /** * @brief Indicates to the External Port driver that a device's port can be recycled * * The device connected to the port has been freed. The External Port driver can now * recycle the port. * * @note This function should only be called from the External Hub Driver * * @param[in] port_hdl Port object handle * @return * - ESP_ERR_NOT_ALLOWED: The External Port Driver has not been installed * - ESP_ERR_INVALID_ARG: Unable to recycle the port, port handle couldn't be NULL * - ESP_OK: */ static esp_err_t port_recycle(void *port_hdl) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_NOT_ALLOWED); EXT_PORT_CHECK(port_hdl != NULL, ESP_ERR_INVALID_ARG); ext_port_t *ext_port = (ext_port_t *) port_hdl; ESP_LOGD(EXT_PORT_TAG, "Port %d request recycle, state=%d", ext_port->constant.port_num, ext_port->state); port_set_actions(ext_port, PORT_ACTION_RECYCLE); return ESP_OK; } /** * @brief Indicate to the External Port driver that reset of a device's port is required * * @note This function should only be called from the External Hub Driver * * @param[in] port_hdl Port object handle * @return * - ESP_ERR_NOT_ALLOWED: The External Port Driver has not been installed * - ESP_ERR_INVALID_ARG: Port handle couldn't be NULL * - ESP_ERR_INVALID_STATE: Port doesn't have a connection or not enabled * - ESP_OK: Port reset requested */ static esp_err_t port_reset(void *port_hdl) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_NOT_ALLOWED); EXT_PORT_CHECK(port_hdl != NULL, ESP_ERR_INVALID_ARG); ext_port_t *ext_port = (ext_port_t *) port_hdl; EXT_PORT_CHECK(port_has_connection(ext_port), ESP_ERR_INVALID_STATE); ESP_LOGD(EXT_PORT_TAG, "[%d:%d] Port reset request", ext_port->constant.parent_dev_addr, ext_port->constant.port_num); // Reset can be triggered only when port is enabled EXT_PORT_CHECK(ext_port->state == USB_PORT_STATE_ENABLED, ESP_ERR_INVALID_STATE); port_set_actions(ext_port, PORT_ACTION_RESET); return ESP_OK; } /** * @brief Get the speed of the External Port * * The speed of the port is determined by the speed of the device connected to it. * * @note This function is only when a device connected to the port and has been reset * @note This function should only be called from the External Hub Driver * * @param[in] port_hdl Port object handle * @param[out] speed Speed of the port * @return * - ESP_ERR_NOT_ALLOWED: The External Port Driver has not been installed * - ESP_ERR_INVALID_ARG: The port handle can't be NULL, speed pointer can't be NULL * - ESP_ERR_INVALID_STATE: No valid device connected to the port * - ESP_OK: Device speed obtained */ esp_err_t port_get_speed(void *port_hdl, usb_speed_t *speed) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_NOT_ALLOWED); EXT_PORT_CHECK(port_hdl != NULL && speed != NULL, ESP_ERR_INVALID_ARG); ext_port_t *ext_port = (ext_port_t *) port_hdl; usb_speed_t dev_speed = USB_SPEED_LOW; EXT_PORT_CHECK(port_has_connection(ext_port), ESP_ERR_INVALID_STATE); // Full-speed or High-speed device attached to this port (determined by bit PORT_HIGH_SPEED). if (ext_port->status.wPortStatus.PORT_LOW_SPEED == 0) { dev_speed = ext_port->status.wPortStatus.PORT_HIGH_SPEED ? USB_SPEED_HIGH : USB_SPEED_FULL; } // Otherwise, Low-speed *speed = dev_speed; return ESP_OK; } /** * @brief Indicate to the External Port Driver that port has an active device * * The device: * - has been enumerated * - still present * - is in configured state * * @note This function should only be called from the External Hub Driver * * @param[in] port_hdl Port object handle * @return * - ESP_ERR_NOT_ALLOWED: The External Port Driver has not been installed * - ESP_ERR_INVALID_ARG: The port handle can't be NULL * - ESP_OK: Port has a device and the device completed enumeration process */ static esp_err_t port_active(void *port_hdl) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_NOT_ALLOWED); EXT_PORT_CHECK(port_hdl != NULL, ESP_ERR_INVALID_ARG); ext_port_t *ext_port = (ext_port_t *) port_hdl; ESP_LOGD(EXT_PORT_TAG, "[%d:%d] Port has an enumerated device", ext_port->constant.parent_dev_addr, ext_port->constant.port_num); ext_port->flags.has_enum_device = 1; return handle_complete(ext_port); } /** * @brief Indicate to the External Port Driver that port should be disabled * * @note This function should only be called from the External Hub Driver * * @param[in] port_hdl Port object handle * @return * - ESP_ERR_NOT_ALLOWED: The External Port Driver has not been installed * - ESP_ERR_INVALID_ARG: The port handle can't be NULL * - ESP_ERR_INVALID_STATE: The port is not enabled * - ESP_OK: Port disable requested */ static esp_err_t port_disable(void *port_hdl) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_NOT_ALLOWED); EXT_PORT_CHECK(port_hdl != NULL, ESP_ERR_INVALID_ARG); ext_port_t *ext_port = (ext_port_t *) port_hdl; ESP_LOGD(EXT_PORT_TAG, "[%d:%d] Disable", ext_port->constant.parent_dev_addr, ext_port->constant.port_num); EXT_PORT_CHECK(ext_port->state == USB_PORT_STATE_ENABLED, ESP_ERR_INVALID_STATE); port_set_actions(ext_port, PORT_ACTION_DISABLE); return ESP_OK; } /** * @brief Deletes the port object * * @note This function should only be called from the External Hub Driver * * @param[in] port_hdl Port object handle * @return * - ESP_ERR_NOT_ALLOWED: The External Port Driver has not been installed * - ESP_ERR_INVALID_ARG: The port handle can't be NULL * - ESP_OK: Port object has been freed and deleted */ static esp_err_t port_delete(void *port_hdl) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_NOT_ALLOWED); EXT_PORT_CHECK(port_hdl != NULL, ESP_ERR_INVALID_ARG); ext_port_t *ext_port = (ext_port_t *) port_hdl; // Sanity checks assert(ext_port->dev_state == PORT_DEV_NOT_PRESENT); // Port should not have a device assert(ext_port->flags.in_pending_list == 0); // Port should not be in pending list ESP_LOGD(EXT_PORT_TAG, "[%d:%d] Freeing", ext_port->constant.parent_dev_addr, ext_port->constant.port_num); heap_caps_free(ext_port); return ESP_OK; } /** * @brief Marks the Port as gone and initialize process of freeing the Port * * @note This function should only be called from the External Hub Driver * * @param[in] port_hdl Port object handle * @return * - ESP_ERR_NOT_ALLOWED: The External Port Driver has not been installed * - ESP_ERR_INVALID_ARG: The port handle can't be NULL * - ESP_ERR_NOT_FINISHED: The port has a device and should be recycled * - ESP_OK: Port object has been marked as gone and could be freed */ static esp_err_t port_gone(void *port_hdl) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_NOT_ALLOWED); EXT_PORT_CHECK(port_hdl != NULL, ESP_ERR_INVALID_ARG); ext_port_t *ext_port = (ext_port_t *)port_hdl; ESP_LOGD(EXT_PORT_TAG, "[%d:%d] Port is gone (state=%d, dev=%d)", ext_port->constant.parent_dev_addr, ext_port->constant.port_num, ext_port->state, ext_port->dev_state); bool has_device = false; ext_port->flags.is_gone = 1; ext_port->flags.waiting_free = 1; switch (ext_port->state) { case USB_PORT_STATE_ENABLED: case USB_PORT_STATE_DISABLED: case USB_PORT_STATE_RESETTING: // Port could have a device if (ext_port->dev_state == PORT_DEV_PRESENT) { ext_port->dev_state = PORT_DEV_NOT_PRESENT; ext_port->flags.waiting_recycle = 1; port_event(ext_port, EXT_PORT_DISCONNECTED); has_device = true; } break; case USB_PORT_STATE_NOT_CONFIGURED: // Port has been added, but the Driver has not started the handling yet case USB_PORT_STATE_POWERED_OFF: case USB_PORT_STATE_DISCONNECTED: // Port has been added, but doesn't have a device // We can remove the port from pending list and could be freed by the External Hub Driver break; default: // Should never occur abort(); break; } // If the port is in handling list, stop it if (ext_port->flags.in_pending_list) { port_stop_handling(ext_port); } return (has_device) ? ESP_ERR_NOT_FINISHED : ESP_OK; } /** * @brief Indicate to the External Port Driver that Port request has been finished and the Port requires handling * * @note This function should only be called from the External Hub Driver * * @param[in] port_hdl Port object handle * @return * - ESP_ERR_NOT_ALLOWED: The External Port Driver has not been installed * - ESP_ERR_INVALID_ARG: The port handle can't be NULL * - ESP_OK: Port action was requested */ static esp_err_t port_get_status(void* port_hdl) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_NOT_ALLOWED); EXT_PORT_CHECK(port_hdl != NULL, ESP_ERR_INVALID_ARG); ext_port_t *ext_port = (ext_port_t *)port_hdl; port_set_actions(ext_port, PORT_ACTION_GET_STATUS); return ESP_OK; } /** * @brief Indicate to the External Port Driver that the Port has new status * * @note This function should only be called from the External Hub Driver * * @param[in] port_hdl Port object handle * @param[in] port_status New status data * @return * - ESP_ERR_NOT_ALLOWED: The External Port Driver has not been installed * - ESP_ERR_INVALID_ARG: The port handle can't be NULL, the status data can't be NULL * - ESP_OK: Port action was requested */ static esp_err_t port_set_status(void* port_hdl, const usb_port_status_t *port_status) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_NOT_ALLOWED); EXT_PORT_CHECK(port_hdl != NULL && port_status != NULL, ESP_ERR_INVALID_ARG); ext_port_t *ext_port = (ext_port_t *)port_hdl; // Update status ext_port->status.wPortChange.val = port_status->wPortChange.val; ext_port->status.wPortStatus.val = port_status->wPortStatus.val; // Status valid ext_port->flags.status_outdated = 0; // Remove status lock ext_port->flags.status_lock = 0; // Request port handling port_set_actions(ext_port, PORT_ACTION_HANDLE); return ESP_OK; } /** * @brief Indicate to the External Port Driver that the Port requires processing * * @note This function should only be called from the External Hub Driver * * @param[in] port_hdl Port object handle * @return * - ESP_ERR_NOT_ALLOWED: The External Port Driver has not been installed * - ESP_ERR_INVALID_ARG: The port handle can't be NULL * - ESP_OK: Port action was requested */ static esp_err_t port_req_process(void* port_hdl) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_NOT_ALLOWED); EXT_PORT_CHECK(port_hdl != NULL, ESP_ERR_INVALID_ARG); ext_port_t *ext_port = (ext_port_t *)port_hdl; if (ext_port->flags.status_outdated) { port_set_actions(ext_port, PORT_ACTION_GET_STATUS); } else { port_set_actions(ext_port, PORT_ACTION_HANDLE); } return ESP_OK; } // ----------------------------------------------------------------------------- // ------------------ External Port Processing Functions ----------------------- // ----------------------------------------------------------------------------- /** * @brief External Port Driver API */ const ext_hub_port_driver_t ext_port_driver = { .new = port_new, .reset = port_reset, .recycle = port_recycle, .active = port_active, .disable = port_disable, .gone = port_gone, .del = port_delete, .get_speed = port_get_speed, .get_status = port_get_status, .set_status = port_set_status, .req_process = port_req_process, }; esp_err_t ext_port_install(const ext_port_driver_config_t *config) { EXT_PORT_CHECK(p_ext_port_driver == NULL, ESP_ERR_NOT_ALLOWED); ext_port_driver_t *ext_port_drv = heap_caps_calloc(1, sizeof(ext_port_driver_t), MALLOC_CAP_DEFAULT); EXT_PORT_CHECK(ext_port_drv != NULL, ESP_ERR_NO_MEM); // Save callbacks ext_port_drv->constant.proc_req_cb = config->proc_req_cb; ext_port_drv->constant.proc_req_cb_arg = config->proc_req_cb_arg; ext_port_drv->constant.event_cb = config->event_cb; ext_port_drv->constant.event_cb_arg = config->event_cb_arg; TAILQ_INIT(&ext_port_drv->single_thread.pending_tailq); p_ext_port_driver = ext_port_drv; ESP_LOGD(EXT_PORT_TAG, "Driver installed"); return ESP_OK; } esp_err_t ext_port_uninstall(void) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_NOT_ALLOWED); EXT_PORT_CHECK(TAILQ_EMPTY(&p_ext_port_driver->single_thread.pending_tailq), ESP_ERR_INVALID_STATE); ext_port_driver_t *ext_port_drv = p_ext_port_driver; p_ext_port_driver = NULL; heap_caps_free(ext_port_drv); ESP_LOGD(EXT_PORT_TAG, "Driver uninstalled"); return ESP_OK; } esp_err_t ext_port_process(void) { EXT_PORT_CHECK(p_ext_port_driver != NULL, ESP_ERR_NOT_ALLOWED); ext_port_t *ext_port = get_port_from_pending_list(); if (ext_port == NULL) { // No more ports in list to handle // NOTE: // This is possible, when an external Hub detached sooner than being // configured and handled by the Driver. This is not an error case, // because the processing requests by the HUB_DRIVER_ACTION_EXT_PORT // flag in the Hub Driver and there is no way to clean it. ESP_LOGD(EXT_PORT_TAG, "No more ports to handle"); return ESP_OK; } uint32_t action_flags = ext_port->action_flags; ext_port->action_flags = 0; while (action_flags) { // Keep processing until all port's action have been handled ESP_LOGD(EXT_PORT_TAG, "[%d:%d] Processing actions 0x%"PRIx32"", ext_port->constant.parent_dev_addr, ext_port->constant.port_num, action_flags); if (action_flags & PORT_ACTION_HANDLE) { handle_port(ext_port); } if (action_flags & PORT_ACTION_DISABLE) { handle_disable(ext_port); } if (action_flags & PORT_ACTION_RECYCLE) { handle_recycle(ext_port); } /* * Feature related actions are mutual exclusive and require: * - transfer completion callback * - further request of new port status via get_status(), except PORT_ACTION_GET_STATUS itself */ if (action_flags & PORT_ACTION_GET_STATUS) { port_request_status(ext_port); } else if (action_flags & PORT_ACTION_RESET) { if (ext_port->state != USB_PORT_STATE_RESETTING) { /* * IMPORTANT NOTE * This is possible, when the reset is requested via port_reset() * Port reset is possible only in two states: * - USB_PORT_STATE_DISCONNECTED (mainly, first reset) * - USB_PORT_STATE_ENABLED (mainly, second reset) */ assert(ext_port->state == USB_PORT_STATE_DISCONNECTED || ext_port->state == USB_PORT_STATE_ENABLED); ext_port->state = USB_PORT_STATE_RESETTING; } port_set_feature(ext_port, USB_FEATURE_PORT_RESET); } action_flags = ext_port->action_flags; ext_port->action_flags = 0; } return ESP_OK; } const ext_hub_port_driver_t *ext_port_get_driver(void) { EXT_PORT_CHECK(p_ext_port_driver != NULL, NULL); return &ext_port_driver; }