diff --git a/.build-test-rules.yml b/.build-test-rules.yml index b5b338a1..1d32d8d0 100644 --- a/.build-test-rules.yml +++ b/.build-test-rules.yml @@ -28,6 +28,14 @@ host/class/msc: enable: - if: SOC_USB_OTG_SUPPORTED == 1 +type_c: + depends_filepatterns: + - 'type_c/**' + disable: + - if: IDF_TARGET in ["linux"] + - if: SOC_I2C_SUPPORTED == 0 + reason: Type-C TCPM backend requires I2C support (FUSB302) + host/class/uac: depends_filepatterns: - 'host/class/uac/**' diff --git a/.github/workflows/upload_component.yml b/.github/workflows/upload_component.yml index f7667193..b69a7f5c 100644 --- a/.github/workflows/upload_component.yml +++ b/.github/workflows/upload_component.yml @@ -44,6 +44,7 @@ jobs: host/class/uac/usb_host_uac host/class/uvc/usb_host_uvc host/usb + type_c/usb_tcpm namespace: "espressif" # API token will only be available in the master branch in the main repository. # However, dry-run doesn't require a valid token. diff --git a/type_c/usb_tcpm/CHANGELOG.md b/type_c/usb_tcpm/CHANGELOG.md new file mode 100644 index 00000000..6be248ec --- /dev/null +++ b/type_c/usb_tcpm/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog for USB Type-C library + +All notable changes to this component will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Initial pre-release scope of the USB Type-C (`usb_tcpm`) component. +- Core APIs for install/uninstall, port creation/destruction, and status snapshot. +- Shared worker-task runtime model and Type-C attach/detach event flow. +- FUSB302 backend, example app (`examples/pd_fusb302`), and target test app (`test_apps/pd_fusb302`). diff --git a/type_c/usb_tcpm/CMakeLists.txt b/type_c/usb_tcpm/CMakeLists.txt new file mode 100644 index 00000000..21516daf --- /dev/null +++ b/type_c/usb_tcpm/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register( + SRCS "src/usb_tcpm.c" "src/fusb302_ctrl.c" + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS "include_private" + PRIV_REQUIRES esp_driver_gpio esp_driver_i2c + REQUIRES esp_event +) diff --git a/type_c/usb_tcpm/Kconfig.pd_fusb302 b/type_c/usb_tcpm/Kconfig.pd_fusb302 new file mode 100644 index 00000000..7ae29699 --- /dev/null +++ b/type_c/usb_tcpm/Kconfig.pd_fusb302 @@ -0,0 +1,48 @@ +menu "FUSB302 TCPM Configuration" + + orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps" + + config USB_TCPM_PD_FUSB302_I2C_SDA_GPIO + int "I2C SDA GPIO number" + range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX + default 7 + help + GPIO number for the I2C SDA line. + + config USB_TCPM_PD_FUSB302_I2C_SCL_GPIO + int "I2C SCL GPIO number" + range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX + default 8 + help + GPIO number for the I2C SCL line. + + config USB_TCPM_PD_FUSB302_FUSB_INT_GPIO + int "FUSB302 INT GPIO number" + range ENV_GPIO_RANGE_MIN ENV_GPIO_IN_RANGE_MAX + default 9 + help + GPIO number for the FUSB302 interrupt line. + + config USB_TCPM_PD_FUSB302_VBUS_GPIO_PH + int "VBUS enable GPIO (active high)" + range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX + default 28 if IDF_TARGET_ESP32 + default 28 if IDF_TARGET_ESP32S2 + default 28 if IDF_TARGET_ESP32S3 + default 28 if IDF_TARGET_ESP32P4 + default 28 if IDF_TARGET_ESP32H4 + default 28 if IDF_TARGET_ESP32C5 + default 28 if IDF_TARGET_ESP32C6 + default 28 if IDF_TARGET_ESP32C61 + default 4 + help + Active-high VBUS enable GPIO. + + config USB_TCPM_PD_FUSB302_VBUS_GPIO_N + int "VBUS enable GPIO (active low)" + range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX + default 15 + help + Active-low VBUS enable GPIO. + +endmenu diff --git a/type_c/usb_tcpm/LICENSE b/type_c/usb_tcpm/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/type_c/usb_tcpm/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/type_c/usb_tcpm/README.md b/type_c/usb_tcpm/README.md new file mode 100644 index 00000000..f6903aba --- /dev/null +++ b/type_c/usb_tcpm/README.md @@ -0,0 +1,46 @@ +# ESP Type-C (usb_tcpm) + +[![Component Registry](https://components.espressif.com/components/espressif/usb_tcpm/badge.svg)](https://components.espressif.com/components/espressif/usb_tcpm) ![maintenance-status](https://img.shields.io/badge/maintenance-experimental-blue.svg) ![changelog](https://img.shields.io/badge/Keep_a_Changelog-blue?logo=keepachangelog&logoColor=E05735) + +Minimal USB Type-C CC library for attach/detach, cable orientation, power-role control, and status snapshot. The current implementation uses the FUSB302 TCPC for CC detection and interrupt handling. + +## Supported Controllers + +- FUSB302 (CC attach/detach and role control) + +## Usage + +1. Install the TCPM library with `usb_tcpm_install()` +2. Create a port using `usb_tcpm_port_create_fusb302()` +3. Register for `USB_TCPM_EVENT` events via `esp_event_handler_register()` +4. (Optional) Read a snapshot with `usb_tcpm_get_status()` +5. Destroy the port with `usb_tcpm_port_destroy()` +6. Uninstall the library with `usb_tcpm_uninstall()` + +### Install Configuration + +`usb_tcpm_install_config_t` configures the shared worker task: + +- `task_stack`: shared task stack size in bytes (`0` = default) +- `task_prio`: shared task priority (`0` = default) + +Example: + +```c +const usb_tcpm_install_config_t install_cfg = { + .task_stack = 4096, + .task_prio = 5, +}; +ESP_ERROR_CHECK(usb_tcpm_install(&install_cfg)); +``` + +### Runtime Model + +- One shared worker task is created during `usb_tcpm_install()`. +- Each created port only registers its backend and IRQ. +- Port IRQs notify the shared worker, which services pending ports one by one. +- `usb_tcpm_uninstall()` succeeds only when no ports are active. + +## Examples + +- `examples/pd_fusb302` (logs attach/detach events and prints a one-shot status snapshot after startup) diff --git a/type_c/usb_tcpm/examples/pd_fusb302/CMakeLists.txt b/type_c/usb_tcpm/examples/pd_fusb302/CMakeLists.txt new file mode 100644 index 00000000..3311e9ea --- /dev/null +++ b/type_c/usb_tcpm/examples/pd_fusb302/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(typec_fusb302) diff --git a/type_c/usb_tcpm/examples/pd_fusb302/main/CMakeLists.txt b/type_c/usb_tcpm/examples/pd_fusb302/main/CMakeLists.txt new file mode 100644 index 00000000..378be2c2 --- /dev/null +++ b/type_c/usb_tcpm/examples/pd_fusb302/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + SRCS "main.c" + PRIV_REQUIRES esp_driver_i2c esp_driver_gpio +) diff --git a/type_c/usb_tcpm/examples/pd_fusb302/main/Kconfig.projbuild b/type_c/usb_tcpm/examples/pd_fusb302/main/Kconfig.projbuild new file mode 100644 index 00000000..703e8ed1 --- /dev/null +++ b/type_c/usb_tcpm/examples/pd_fusb302/main/Kconfig.projbuild @@ -0,0 +1 @@ +orsource "../../../Kconfig.pd_fusb302" diff --git a/type_c/usb_tcpm/examples/pd_fusb302/main/idf_component.yml b/type_c/usb_tcpm/examples/pd_fusb302/main/idf_component.yml new file mode 100644 index 00000000..062babff --- /dev/null +++ b/type_c/usb_tcpm/examples/pd_fusb302/main/idf_component.yml @@ -0,0 +1,5 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/usb_tcpm: + version: "*" + override_path: "../../../" diff --git a/type_c/usb_tcpm/examples/pd_fusb302/main/main.c b/type_c/usb_tcpm/examples/pd_fusb302/main/main.c new file mode 100644 index 00000000..258089f4 --- /dev/null +++ b/type_c/usb_tcpm/examples/pd_fusb302/main/main.c @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "driver/i2c_master.h" +#include "esp_event.h" +#include "esp_log.h" +#include "sdkconfig.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "usb/fusb302.h" +#include "usb/usb_tcpm.h" + +static const char *TAG = "example_usb_tcpm_fusb302"; +#define VBUS_GPIO_PH CONFIG_USB_TCPM_PD_FUSB302_VBUS_GPIO_PH // active-high enable +#define VBUS_GPIO_N CONFIG_USB_TCPM_PD_FUSB302_VBUS_GPIO_N // active-low enable (e.g., PW2609A) + +/* --- I2C master bus --- */ +#define I2C_SDA CONFIG_USB_TCPM_PD_FUSB302_I2C_SDA_GPIO +#define I2C_SCL CONFIG_USB_TCPM_PD_FUSB302_I2C_SCL_GPIO +#define INT_GPIO CONFIG_USB_TCPM_PD_FUSB302_FUSB_INT_GPIO + +/* ===== Event callback ===== */ +static void on_usb_tcpm_event(void *handler_arg, esp_event_base_t base, int32_t id, void *event_data) +{ + (void)handler_arg; + (void)base; + + switch (id) { + case USB_TCPM_EVENT_SINK_ATTACHED: + case USB_TCPM_EVENT_SOURCE_ATTACHED: { + const usb_tcpm_evt_attached_t *const attached_event = (const usb_tcpm_evt_attached_t *)event_data; + const char *const role = (id == USB_TCPM_EVENT_SOURCE_ATTACHED) ? "SRC" : "SNK"; + const uint32_t rp_current_ma = attached_event ? attached_event->rp_current_ma : 0; + const bool cc2_active = attached_event ? attached_event->cc2_active : false; + const void *port_handle = attached_event ? (void *)attached_event->port : NULL; + ESP_LOGI(TAG, "%s ATTACHED: port=%p, cc2=%d, rp=%u mA", role, port_handle, cc2_active, rp_current_ma); + break; + } + case USB_TCPM_EVENT_SINK_DETACHED: + case USB_TCPM_EVENT_SOURCE_DETACHED: { + const usb_tcpm_evt_detached_t *const detached_event = (const usb_tcpm_evt_detached_t *)event_data; + const char *const role = (id == USB_TCPM_EVENT_SOURCE_DETACHED) ? "SRC" : "SNK"; + ESP_LOGI(TAG, "%s DETACHED: port=%p", role, detached_event ? (void *)detached_event->port : NULL); + break; + } + case USB_TCPM_EVENT_ERROR: + default: + ESP_LOGW(TAG, "EVENT %d", (int)id); + break; + } +} + +/* ===== App entry ===== */ +void app_main(void) +{ + /* --- Install TCPM --- */ + const usb_tcpm_install_config_t install_cfg = { + .task_stack = 4096, + .task_prio = 5, + }; + ESP_ERROR_CHECK(usb_tcpm_install(&install_cfg)); + + i2c_master_bus_handle_t bus = NULL; + const i2c_master_bus_config_t bus_cfg = { + .i2c_port = 0, /* use I2C0 */ + .sda_io_num = I2C_SDA, + .scl_io_num = I2C_SCL, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags = { .enable_internal_pullup = 1 }, + }; + ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &bus)); + + /* --- Create typec port (FUSB302 backend) --- */ + usb_tcpm_port_handle_t port = NULL; + + const usb_tcpm_port_config_t port_cfg = { + .default_power_role = USB_TCPM_PWR_DRP, + .rp_current = USB_TCPM_RP_1A5, + .src_vbus_gpio = VBUS_GPIO_PH, + .src_vbus_gpio_n = VBUS_GPIO_N, + }; + + const usb_tcpm_fusb302_config_t hw = { + .i2c_bus = bus, + .i2c_addr_7b = USB_TCPM_FUSB302_I2C_ADDR_7B_010, + .gpio_int = INT_GPIO, + }; + + ESP_ERROR_CHECK(esp_event_handler_register( + USB_TCPM_EVENT, + ESP_EVENT_ANY_ID, + &on_usb_tcpm_event, NULL)); + ESP_ERROR_CHECK(usb_tcpm_port_create_fusb302(&port_cfg, &hw, &port)); + ESP_LOGI(TAG, "Waiting for attach/detach events..."); + + /* One-shot status snapshot after startup */ + vTaskDelay(pdMS_TO_TICKS(5000)); + usb_tcpm_port_status_t status = {0}; + if (usb_tcpm_get_status(port, &status) == ESP_OK) { + ESP_LOGI(TAG, "status: attached=%d cc2=%d rp=%u mA role=%d", + status.attached, status.cc2_active, (unsigned)status.rp_current_ma, (int)status.role); + } + + /* App can idle; the Type-C task & ISR do the work */ + while (true) { + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} diff --git a/type_c/usb_tcpm/examples/pd_fusb302/sdkconfig.defaults b/type_c/usb_tcpm/examples/pd_fusb302/sdkconfig.defaults new file mode 100644 index 00000000..feec006e --- /dev/null +++ b/type_c/usb_tcpm/examples/pd_fusb302/sdkconfig.defaults @@ -0,0 +1,10 @@ +# Keep mbedTLS crypto features aligned with current Kconfig defaults. +# CONFIG_MBEDTLS_ARIA_C is not set +# CONFIG_MBEDTLS_SHA3_C is not set + +# Keep target/flash defaults aligned with current Kconfig defaults. +CONFIG_ESPTOOLPY_FLASHFREQ_40M=y + +# Keep libc console line-ending behavior explicit. +CONFIG_LIBC_STDOUT_LINE_ENDING_CRLF=y +CONFIG_LIBC_STDIN_LINE_ENDING_CR=y diff --git a/type_c/usb_tcpm/examples/pd_fusb302/sdkconfig.defaults.esp32p4 b/type_c/usb_tcpm/examples/pd_fusb302/sdkconfig.defaults.esp32p4 new file mode 100644 index 00000000..2dbcf836 --- /dev/null +++ b/type_c/usb_tcpm/examples/pd_fusb302/sdkconfig.defaults.esp32p4 @@ -0,0 +1 @@ +CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y diff --git a/type_c/usb_tcpm/idf_component.yml b/type_c/usb_tcpm/idf_component.yml new file mode 100644 index 00000000..3b5de9c2 --- /dev/null +++ b/type_c/usb_tcpm/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File + +description: ESP-IDF USB Type-C stack (beta) +version: "0.1.0-beta1" +url: https://github.com/espressif/esp-usb/tree/master/type_c/usb_tcpm +issues: "https://github.com/espressif/esp-usb/issues" +repository: "https://github.com/espressif/esp-usb.git" +repository_info: + path: "type_c/usb_tcpm" +dependencies: + idf: ">=5.4" +files: + exclude: + - "test_apps/**/*" +tags: + - usb + - usb_typec diff --git a/type_c/usb_tcpm/include/usb/fusb302.h b/type_c/usb_tcpm/include/usb/fusb302.h new file mode 100644 index 00000000..de940457 --- /dev/null +++ b/type_c/usb_tcpm/include/usb/fusb302.h @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "driver/gpio.h" +#include "driver/i2c_master.h" +#include "usb/usb_tcpm.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief FUSB302 7-bit I2C slave addresses. + * + * Use these values for @ref usb_tcpm_fusb302_config_t.i2c_addr_7b. + * Suffix `XYZ` corresponds to address bits `[2:0]`. + */ +#define USB_TCPM_FUSB302_I2C_ADDR_7B_010 (0x22) /**< 8-bit bus address: 0x44 (W) / 0x45 (R). */ +#define USB_TCPM_FUSB302_I2C_ADDR_7B_011 (0x23) /**< 8-bit bus address: 0x46 (W) / 0x47 (R). */ +#define USB_TCPM_FUSB302_I2C_ADDR_7B_100 (0x24) /**< 8-bit bus address: 0x48 (W) / 0x49 (R). */ +#define USB_TCPM_FUSB302_I2C_ADDR_7B_101 (0x25) /**< 8-bit bus address: 0x4A (W) / 0x4B (R). */ + +/** + * @brief Hardware configuration for FUSB302 backend. + */ +typedef struct { + i2c_master_bus_handle_t i2c_bus; /**< I2C bus handle */ + uint8_t i2c_addr_7b; /**< 7-bit I2C address of FUSB302 */ + gpio_num_t gpio_int; /**< Active-low INT GPIO from the chip */ +} usb_tcpm_fusb302_config_t; + +/** + * @brief Create a Type-C port backed by FUSB302 (I2C + INT). + * + * @note Events are posted on the USB_TCPM_EVENT base. + * + * @param[in] port_cfg Port configuration. + * @param[in] hw_cfg Hardware configuration for FUSB302. + * @param[out] port_hdl_ret Pointer to port handle output. + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG if any argument is NULL or invalid + * - ESP_ERR_INVALID_STATE if usb_tcpm_install() has not been called + * - ESP_ERR_NO_MEM if allocation fails + * - Other error codes from GPIO/I2C/backend initialization + */ +esp_err_t usb_tcpm_port_create_fusb302(const usb_tcpm_port_config_t *port_cfg, + const usb_tcpm_fusb302_config_t *hw_cfg, + usb_tcpm_port_handle_t *port_hdl_ret); + +#ifdef __cplusplus +} +#endif diff --git a/type_c/usb_tcpm/include/usb/usb_tcpm.h b/type_c/usb_tcpm/include/usb/usb_tcpm.h new file mode 100644 index 00000000..3dabfa69 --- /dev/null +++ b/type_c/usb_tcpm/include/usb/usb_tcpm.h @@ -0,0 +1,154 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include +#include "esp_err.h" +#include "driver/gpio.h" +#include "esp_event.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct typec_port; + +/** + * @brief Event base for Type-C events. + * + * Events are posted with: + * - base: USB_TCPM_EVENT + * - id: usb_tcpm_event_id_t + * + * Events are posted by the internal USB TCPM worker task using `esp_event_post()` + * (not from ISR context). Handlers run in the context of the default event loop task. + */ +ESP_EVENT_DECLARE_BASE(USB_TCPM_EVENT); + +/** + * @brief Type-C power role. + */ +typedef enum { + USB_TCPM_PWR_SINK = 0, /**< Sink role */ + USB_TCPM_PWR_SOURCE, /**< Source role */ + USB_TCPM_PWR_DRP, /**< Dual-role power */ +} usb_tcpm_power_role_t; + +/** + * @brief Type-C event IDs. + */ +typedef enum { + USB_TCPM_EVENT_SINK_ATTACHED = 0, /**< payload: usb_tcpm_evt_attached_t */ + USB_TCPM_EVENT_SINK_DETACHED, /**< payload: usb_tcpm_evt_detached_t */ + USB_TCPM_EVENT_SINK_RP_CURRENT_CHANGED, /**< Reserved (not emitted yet), payload: usb_tcpm_evt_attached_t */ + USB_TCPM_EVENT_SOURCE_ATTACHED, /**< payload: usb_tcpm_evt_attached_t */ + USB_TCPM_EVENT_SOURCE_DETACHED, /**< payload: usb_tcpm_evt_detached_t */ + USB_TCPM_EVENT_ERROR, /**< payload: usb_tcpm_error_t (TBD) */ +} usb_tcpm_event_id_t; + +/** + * @brief Advertised Rp current levels when acting as Source/DFP. + */ +typedef enum { + USB_TCPM_RP_DEFAULT = 0, /**< Default USB current (~500 mA) */ + USB_TCPM_RP_1A5, /**< 1.5 A advertised current */ + USB_TCPM_RP_3A0, /**< 3.0 A advertised current */ +} usb_tcpm_rp_current_t; + +/** + * @brief Type-C library install configuration. + */ +typedef struct { + uint32_t task_stack; /**< Shared worker task stack size in bytes (0 = default). */ + uint32_t task_prio; /**< Shared worker task priority (0 = default). */ +} usb_tcpm_install_config_t; + +/** + * @brief Type-C port configuration structure. + */ +typedef struct { + usb_tcpm_power_role_t default_power_role; /**< Default role at bring-up */ + usb_tcpm_rp_current_t rp_current; /**< Advertised Rp current when acting as Source/DFP */ + gpio_num_t src_vbus_gpio; /**< Active-high VBUS control (GPIO_NUM_NC if not used)*/ + gpio_num_t src_vbus_gpio_n; /**< Optional active-low VBUS control (GPIO_NUM_NC if unused) */ +} usb_tcpm_port_config_t; + +/** + * @brief Opaque handle for a Type-C port instance. + */ +typedef struct typec_port *usb_tcpm_port_handle_t; + +/** + * @brief Event payload for Type-C ATTACHED event. + */ +typedef struct { + usb_tcpm_port_handle_t port; /**< Port that generated the event */ + bool cc2_active; /**< true if CC2 is active, false = CC1 */ + uint32_t rp_current_ma; /**< Advertised/detected Rp current in mA (0 if unknown). */ +} usb_tcpm_evt_attached_t; + +/** + * @brief Event payload for Type-C DETACHED event. + */ +typedef struct { + usb_tcpm_port_handle_t port; /**< Port that generated the event */ +} usb_tcpm_evt_detached_t; + +/** + * @brief Type-C port status snapshot. + */ +typedef struct { + bool attached; /**< True if attached. */ + bool cc2_active; /**< True if CC2 is active, false = CC1. */ + uint32_t rp_current_ma; /**< Advertised/detected Rp current in mA (0 if unknown). */ + usb_tcpm_power_role_t role; /**< Current power role. */ +} usb_tcpm_port_status_t; + +/** + * @brief Install and initialize the Type-C library. + * + * @param[in] config Optional install configuration. + * @return + * - ESP_OK on success + * - ESP_ERR_NO_MEM if shared worker task creation fails + * - Other error codes from esp_event_loop_create_default() + */ +esp_err_t usb_tcpm_install(const usb_tcpm_install_config_t *config); + +/** + * @brief Uninstall the Type-C library. + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_STATE if one or more ports are still active + */ +esp_err_t usb_tcpm_uninstall(void); + +/** + * @brief Destroy a Type-C port instance and free resources. + * + * @param[in] port_hdl Port handle to destroy. + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG if port_hdl is NULL + */ +esp_err_t usb_tcpm_port_destroy(usb_tcpm_port_handle_t port_hdl); + +/** + * @brief Get Type-C port status. + * + * @param[in] port_hdl Port handle. + * @param[out] status Pointer to status output. + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG if port_hdl or status is NULL + */ +esp_err_t usb_tcpm_get_status(usb_tcpm_port_handle_t port_hdl, usb_tcpm_port_status_t *status); + +#ifdef __cplusplus +} +#endif diff --git a/type_c/usb_tcpm/include_private/husb320_ctrl.h b/type_c/usb_tcpm/include_private/husb320_ctrl.h new file mode 100644 index 00000000..f892360e --- /dev/null +++ b/type_c/usb_tcpm/include_private/husb320_ctrl.h @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +/* Experimental stub: API/behavior may change or be incomplete. */ +#include +#include +#include "esp_err.h" +#include "usb/usb_tcpm.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief CC status snapshot for HUSB320. + */ +typedef struct { + bool attached; /**< Rd/Ra detected */ + bool cc2_active; /**< true if CC2 is active line */ + uint32_t rp_current_ma; /**< advertised current (0 if unknown) */ +} husb320_cc_status_t; + +/** + * @brief Hardware configuration for HUSB320. + */ +typedef struct { + int i2c_port; /**< I2C controller port */ + uint8_t i2c_addr; /**< 7-bit I2C address */ + int gpio_int; /**< INT/ATTACH GPIO */ + bool use_intr; /**< If false, backend may poll (debug) */ +} husb320_hw_cfg_t; + +/** + * @brief Opaque device handle for HUSB320. + */ +typedef struct husb320_dev husb320_dev_t; + +#ifdef __cplusplus +} +#endif diff --git a/type_c/usb_tcpm/include_private/usb_tcpm_backend.h b/type_c/usb_tcpm/include_private/usb_tcpm_backend.h new file mode 100644 index 00000000..41450fc4 --- /dev/null +++ b/type_c/usb_tcpm/include_private/usb_tcpm_backend.h @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include +#include "esp_err.h" +#include "usb/usb_tcpm.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle for a Type-C port (core-owned). + */ +typedef struct typec_port *typec_port_handle_t; + +typedef enum { + TYPEC_TOG_RESULT_NONE = 0, /**< Toggle engine still running / no result */ + TYPEC_TOG_RESULT_SRC, /**< Partner is a sink; we should act as source */ + TYPEC_TOG_RESULT_SNK, /**< Partner is a source; we should act as sink */ +} typec_tog_result_t; + +/** + * @brief Simple CC status snapshot for a Type-C controller. + */ +typedef struct { + bool attached; /**< True when exactly one CC detects Rp */ + bool cc2_active; /**< True if CC2 is the active/connected CC */ + uint32_t rp_current_ma; /**< Advertised/detected Rp current in mA (0 if unknown). */ + bool vbus_ok; /**< True if VBUSOK is asserted */ + typec_tog_result_t tog_result; /**< Result from toggle engine (if any) */ +} typec_cc_status_t; + +/** + * @brief Backend event mask bits returned by the Type-C controller. + */ +typedef enum { + TYPEC_EVT_CC = (1u << 0), /**< CC comparator / BC_LVL change */ + TYPEC_EVT_VBUS = (1u << 1), /**< VBUSOK change */ + TYPEC_EVT_RX = (1u << 2), /**< RX message available (reserved) */ + TYPEC_EVT_HRST = (1u << 3), /**< Hard reset detected */ + TYPEC_EVT_FAULT = (1u << 4), /**< OCP/OTP/etc. */ + TYPEC_EVT_TOG = (1u << 5), /**< Toggle state change (TOG_DONE) */ +} typec_evt_mask_t; + +/** + * @brief Backend operations for a Type-C port controller. + */ +typedef struct typec_port_backend { + esp_err_t (*init)(void *device, const usb_tcpm_port_config_t *cfg); /**< Initialize backend. */ + esp_err_t (*deinit)(void *device); /**< Deinitialize backend (free device). */ + esp_err_t (*set_role)(void *device, usb_tcpm_power_role_t role); /**< Set power role. */ + esp_err_t (*service_irq)(void *device, typec_evt_mask_t *events); /**< Service IRQ and return events. */ + esp_err_t (*get_status)(void *device, typec_cc_status_t *status); /**< Read CC/VBUS status snapshot. */ + esp_err_t (*commit_attach)(void *device, bool cc2_active, bool is_source); /**< Commit orientation/role after attach. */ + esp_err_t (*get_irq_gpio)(void *device, gpio_num_t *gpio_num); /**< Return IRQ GPIO number. */ +} typec_port_backend_t; + +/** + * @brief Create a Type-C port instance using a backend. + * + * The core takes ownership of backend_ctx only when this function returns ESP_OK. + * If this function returns an error, ownership remains with the caller and + * the caller is responsible for cleanup (for example by calling backend->deinit()). + * + * @param[in] backend Backend operations. + * @param[in] backend_ctx Backend context (opaque, backend-owned). + * @param[in] port_cfg Port configuration. + * @param[out] out Port handle output. + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG if any argument is NULL + * - ESP_ERR_INVALID_STATE if usb_tcpm_install() has not been called + * - ESP_ERR_NO_MEM if allocation fails + * - Other error codes from backend/core initialization + */ +esp_err_t typec_port_new(const typec_port_backend_t *backend, + void *backend_ctx, + const usb_tcpm_port_config_t *port_cfg, + typec_port_handle_t *out); + +#ifdef __cplusplus +} +#endif diff --git a/type_c/usb_tcpm/src/fusb302_ctrl.c b/type_c/usb_tcpm/src/fusb302_ctrl.c new file mode 100644 index 00000000..bc1aa6d5 --- /dev/null +++ b/type_c/usb_tcpm/src/fusb302_ctrl.c @@ -0,0 +1,859 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "driver/gpio.h" +#include "driver/i2c_master.h" +#include "esp_check.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_rom_sys.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "usb/fusb302.h" +#include "usb/usb_tcpm.h" + +#include "usb_tcpm_backend.h" + +static const char *TAG = "fusb302"; + +#define MAX_INT_READS 8 /* Maximum number of interrupt register reads to avoid infinite loops */ +#define SRC_ATTACH_DEBOUNCE_MS 10 + +typedef struct fusb302_dev fusb302_dev_t; + +typedef struct { + i2c_master_dev_handle_t i2c_dev; /**< REQUIRED: device handle from esp_driver_i2c */ + gpio_num_t gpio_int; /**< REQUIRED: GPIO number for FUSB302 INT (active-low) */ + usb_tcpm_rp_current_t rp_current;/**< Advertised Rp current when sourcing */ +} fusb302_hw_cfg_t; + +/** + * @brief FUSB302 device context structure. + */ +struct fusb302_dev { + fusb302_hw_cfg_t hw; /**< Hardware configuration */ + usb_tcpm_power_role_t role; /**< Current power role */ + usb_tcpm_rp_current_t rp_current;/**< Advertised Rp current when sourcing */ +}; + +/** + * @brief Backend context for a Type-C port using FUSB302. + */ +typedef struct { + usb_tcpm_fusb302_config_t hw_cfg; /**< Hardware config from public API */ + i2c_master_dev_handle_t i2c_dev; /**< I2C device handle */ + fusb302_dev_t *device; /**< FUSB302 device handle */ +} fusb302_port_ctx_t; + +/* Internal device functions */ +static esp_err_t fusb302_commit_attach(const fusb302_dev_t *device, bool cc2_active, bool is_source); +static esp_err_t fusb302_init(const fusb302_hw_cfg_t *hw, fusb302_dev_t **out); +static esp_err_t fusb302_deinit(fusb302_dev_t *device); +static esp_err_t fusb302_set_role(fusb302_dev_t *device, usb_tcpm_power_role_t role); +static esp_err_t fusb302_service_irq(const fusb302_dev_t *device, typec_evt_mask_t *events); +static esp_err_t fusb302_enable_irq(const fusb302_dev_t *device, bool enable); +static esp_err_t fusb302_get_status(const fusb302_dev_t *device, typec_cc_status_t *status); + +/* FUSB302 registers */ +enum { + REG_DEVICE_ID = 0x01, + REG_SWITCHES0 = 0x02, + REG_SWITCHES1 = 0x03, + REG_MEASURE = 0x04, + REG_CONTROL0 = 0x06, + REG_CONTROL1 = 0x07, + REG_CONTROL2 = 0x08, + REG_CONTROL3 = 0x09, + REG_MASK = 0x0A, + REG_POWER = 0x0B, + REG_RESET = 0x0C, + REG_MASKA = 0x0E, + REG_MASKB = 0x0F, + + REG_STATUS0A = 0x3C, + REG_STATUS1A = 0x3D, + REG_INTERRUPTA = 0x3E, + REG_INTERRUPTB = 0x3F, + REG_STATUS0 = 0x40, + REG_STATUS1 = 0x41, + REG_INTERRUPT = 0x42, + REG_FIFOS = 0x43, +}; + +/* SWITCHES0 */ +#define SW0_CC2_PU_EN (1u << 7) +#define SW0_CC1_PU_EN (1u << 6) +#define SW0_VCONN_CC2 (1u << 5) +#define SW0_VCONN_CC1 (1u << 4) +#define SW0_MEAS_CC2 (1u << 3) +#define SW0_MEAS_CC1 (1u << 2) +#define SW0_CC2_PD_EN (1u << 1) +#define SW0_CC1_PD_EN (1u << 0) + +/* SWITCHES1 */ +#define SW1_POWERROLE (1u << 7) +#define SW1_SPECREV1 (1u << 6) +#define SW1_SPECREV0 (1u << 5) +#define SW1_DATAROLE (1u << 4) +#define SW1_TXCC2_EN (1u << 1) +#define SW1_TXCC1_EN (1u << 0) + +/* CONTROL0 */ +#define CTL0_INT_MASK (1u << 5) /* 0 = INT pin enabled, 1 = masked */ +#define CTL0_HOST_CUR_MASK (0x3 << 2) +#define CTL0_HOST_CUR_DEFAULT (0x1u << 2) /* 01b -> Default */ +#define CTL0_HOST_CUR_MEDIUM (0x2u << 2) /* 10b -> 1.5A */ +#define CTL0_HOST_CUR_HIGH (0x3u << 2) /* 11b -> 3A */ + +/* CONTROL2 MODE bits */ +#define CTL2_MODE_MASK (0x3u << 1) +#define CTL2_MODE_DFP (0x6) +#define CTL2_MODE_UFP (0x4) +#define CTL2_MODE_DRP (0x2) +#define CTL2_MODE_NONE (0x0) +#define CTL2_TOGGLE (1u << 0) + +/* POWER levels */ +#define PWR_PWR_ALL 0x0F /* full on */ +#define PWR_PWR_HIGH 0x07 +#define PWR_PWR_MEDIUM 0x03 +#define PWR_PWR_LOW 0x01 + +/* RESET */ +#define RST_PD_RESET (1u << 1) +#define RST_SW_RESET (1u << 0) + +/* STATUS0 */ +#define ST0_VBUSOK (1u << 7) +#define ST0_ACTIVITY (1u << 6) +#define ST0_COMP (1u << 5) +#define ST0_CRC_CHK (1u << 4) +#define ST0_ALERT (1u << 3) +#define ST0_WAKE (1u << 2) +#define ST0_BC_LVL_MASK 0x03 +#define ST0_BC_LVL_0_200 0x0 +#define ST0_BC_LVL_200_600 0x1 +#define ST0_BC_LVL_600_1230 0x2 +#define ST0_BC_LVL_1230_MAX 0x3 + +/* STATUS1A (0x3D): [5:0] = TOGSS3 TOGSS2 TOGSS1 RXSOP2DB RXSOP1DB RXSOP */ +#define ST1A_TOGSS_SHIFT 3 +#define ST1A_TOGSS_MASK (0x7u << ST1A_TOGSS_SHIFT) +#define ST1A_RXSOP_MASK 0x7u + +/* Decoded 3-bit TOGSS values after shifting */ +#define ST1A_TOGSS_TOGGLE 0u /* toggle logic running */ +#define ST1A_TOGSS_SRC1 1u /* STOP_SRC1 (CC1) */ +#define ST1A_TOGSS_SRC2 2u /* STOP_SRC2 (CC2) */ +#define ST1A_TOGSS_SNK1 5u /* STOP_SNK1 (CC1) */ +#define ST1A_TOGSS_SNK2 6u /* STOP_SNK2 (CC2) */ +#define ST1A_TOGSS_AUDIO 7u /* Audio accessory (settles to SRC1) */ + +/* REG_INTERRUPT (0x42) — top-level CC/VBUS causes */ +#define INT_BC_LVL (1u << 0) +#define INT_COLLISION (1u << 1) +#define INT_WAKE (1u << 2) +#define INT_ALERT (1u << 3) +#define INT_CRC_CHK (1u << 4) +#define INT_COMP_CHNG (1u << 5) +#define INT_ACTIVITY (1u << 6) +#define INT_VBUSOK (1u << 7) + +/* REG_INTERRUPTA (0x3E) — RX/Toggle outcomes (mapping varies by rev) */ +#define INTA_TX_SUCC (1u << 1) +#define INTA_RETRY_FAIL (1u << 2) +#define INTA_SOFT_FAIL (1u << 3) +#define INTA_HARD_SENT (1u << 5) +#define INTA_TOG_DONE (1u << 6) + +/* REG_INTERRUPTB (0x3F) — RX, resets, FIFO (mapping varies by rev) */ +#define INTB_GCRCSENT (1u << 0) +#define INTB_RX_SOP (1u << 1) +#define INTB_RX_ANY_MASK 0x1F + +/* --- helpers for Rp current mapping --- */ +static inline uint8_t rp_current_to_host_cur_bits(usb_tcpm_rp_current_t rp) +{ + switch (rp) { + case USB_TCPM_RP_1A5: + return CTL0_HOST_CUR_MEDIUM; + case USB_TCPM_RP_3A0: + return CTL0_HOST_CUR_HIGH; + case USB_TCPM_RP_DEFAULT: + default: + return CTL0_HOST_CUR_DEFAULT; + } +} + +static inline uint32_t rp_current_to_ma(usb_tcpm_rp_current_t rp) +{ + switch (rp) { + case USB_TCPM_RP_1A5: + return 1500; + case USB_TCPM_RP_3A0: + return 3000; + case USB_TCPM_RP_DEFAULT: + default: + return 500; + } +} + +static inline uint8_t rp_current_to_mdac(usb_tcpm_rp_current_t rp) +{ + switch (rp) { + case USB_TCPM_RP_3A0: + return 0x3E; + case USB_TCPM_RP_1A5: + case USB_TCPM_RP_DEFAULT: + default: + return 0x26; + } +} + +static inline uint32_t bclvl_to_ma(uint8_t bclvl) +{ + switch (bclvl) { + case ST0_BC_LVL_200_600: + return 500; + case ST0_BC_LVL_600_1230: + return 1500; + case ST0_BC_LVL_1230_MAX: + return 3000; + default: + return 0; + } +} + +// Write 8-bit value to a FUSB302 register via I2C. +static inline esp_err_t i2c_wr8(i2c_master_dev_handle_t device, uint8_t reg, uint8_t val) +{ + const uint8_t buf[2] = { reg, val }; + return i2c_master_transmit(device, buf, sizeof(buf), 100 /*ms*/); +} + +// Read 8-bit value from a FUSB302 register via I2C. +static inline esp_err_t i2c_rd8(i2c_master_dev_handle_t device, uint8_t reg, uint8_t *val) +{ + if (!val) { + return ESP_ERR_INVALID_ARG; + } + return i2c_master_transmit_receive(device, ®, 1, val, 1, 100); +} + +static esp_err_t fusb302_set_host_cur(const fusb302_dev_t *device, uint8_t host_cur_bits) +{ + if (!device) { + return ESP_ERR_INVALID_ARG; + } + + const i2c_master_dev_handle_t i2c_dev = device->hw.i2c_dev; + uint8_t ctl0 = 0; + + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_CONTROL0, &ctl0), TAG, "read CONTROL0"); + ctl0 = (ctl0 & ~CTL0_HOST_CUR_MASK) | host_cur_bits; + return i2c_wr8(i2c_dev, REG_CONTROL0, ctl0); +} + +static esp_err_t fusb302_toggle_disable(const fusb302_dev_t *device) +{ + if (!device) { + return ESP_ERR_INVALID_ARG; + } + + const i2c_master_dev_handle_t i2c_dev = device->hw.i2c_dev; + uint8_t ctl2 = 0; + + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_CONTROL2, &ctl2), TAG, "read CONTROL2"); + if (ctl2 & CTL2_TOGGLE) { + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_CONTROL2, ctl2 & ~CTL2_TOGGLE), TAG, "disable toggle"); + } + return ESP_OK; +} + +static esp_err_t fusb302_toggle_enable(const fusb302_dev_t *device) +{ + const i2c_master_dev_handle_t i2c_dev = device->hw.i2c_dev; + uint8_t ctl2; + + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_CONTROL2, &ctl2), TAG, "read CONTROL2"); + + const uint8_t base = ctl2 & ~CTL2_TOGGLE; + + // Clear any latched toggle-related causes + uint8_t tmp; + (void)i2c_rd8(i2c_dev, REG_INTERRUPTA, &tmp); + (void)i2c_rd8(i2c_dev, REG_INTERRUPT, &tmp); + (void)i2c_rd8(i2c_dev, REG_INTERRUPTB, &tmp); + + // Force a clean 0 -> 1 edge + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_CONTROL2, base), TAG, "disable toggle"); + esp_rom_delay_us(50); + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_CONTROL2, base | CTL2_TOGGLE), TAG, "enable toggle"); + + return ESP_OK; +} + +static esp_err_t fusb302_apply_polarity(const fusb302_dev_t *device, bool cc2_active, bool is_source) +{ + if (!device) { + return ESP_ERR_INVALID_ARG; + } + + const i2c_master_dev_handle_t i2c_dev = device->hw.i2c_dev; + + if (is_source) { + uint8_t sw1 = 0; + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_SWITCHES1, &sw1), TAG, "read SWITCHES1"); + sw1 = (sw1 & ~(SW1_TXCC1_EN | SW1_TXCC2_EN)) | SW1_POWERROLE | + (cc2_active ? SW1_TXCC2_EN : SW1_TXCC1_EN); + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_SWITCHES1, sw1), TAG, "write SWITCHES1 TXCC"); + } + + uint8_t sw0 = 0; + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_SWITCHES0, &sw0), TAG, "read SWITCHES0"); + sw0 = (sw0 & ~(SW0_MEAS_CC1 | SW0_MEAS_CC2)) | + (cc2_active ? SW0_MEAS_CC2 : SW0_MEAS_CC1); + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_SWITCHES0, sw0), TAG, "write SWITCHES0 MEAS"); + esp_rom_delay_us(50); + + return ESP_OK; +} + +/* ---------------- Internal device functions ---------------- */ + +static esp_err_t fusb302_commit_attach(const fusb302_dev_t *device, bool cc2_active, bool is_source) +{ + if (!device) { + return ESP_ERR_INVALID_ARG; + } + + /* Stop autonomous toggle before committing orientation/role. */ + ESP_RETURN_ON_ERROR(fusb302_toggle_disable(device), TAG, "disable toggle"); + ESP_RETURN_ON_ERROR(fusb302_apply_polarity(device, cc2_active, is_source), TAG, "apply polarity"); + + if (is_source) { + /* Advertise the configured Rp current after attach. */ + ESP_RETURN_ON_ERROR(fusb302_set_host_cur(device, rp_current_to_host_cur_bits(device->rp_current)), + TAG, "set HOST_CUR attach"); + ESP_RETURN_ON_ERROR(i2c_wr8(device->hw.i2c_dev, REG_MEASURE, + rp_current_to_mdac(device->rp_current)), + TAG, "set MDAC attach"); + /* Debounce to avoid false detach during the first few ms after attach. */ + vTaskDelay(pdMS_TO_TICKS(SRC_ATTACH_DEBOUNCE_MS)); + } + return ESP_OK; +} + +static esp_err_t fusb302_init(const fusb302_hw_cfg_t *hw, fusb302_dev_t **out) +{ + if (!hw || !out) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = ESP_OK; + fusb302_dev_t *device = calloc(1, sizeof(*device)); + if (!device) { + return ESP_ERR_NO_MEM; + } + device->hw = *hw; + device->rp_current = (hw->rp_current <= USB_TCPM_RP_3A0) ? + hw->rp_current : USB_TCPM_RP_DEFAULT; + *out = NULL; + + uint8_t device_id; + + // Probe + ESP_GOTO_ON_ERROR(i2c_rd8(hw->i2c_dev, REG_DEVICE_ID, &device_id), fail, TAG, "probe"); + ESP_LOGI(TAG, "FUSB302 DEVICE_ID=0x%02x", device_id); + + // Soft reset + ESP_GOTO_ON_ERROR(i2c_wr8(hw->i2c_dev, REG_RESET, RST_SW_RESET), fail, TAG, "reset"); + vTaskDelay(pdMS_TO_TICKS(10)); + + // Power all relevant blocks + ESP_GOTO_ON_ERROR(i2c_wr8(hw->i2c_dev, REG_POWER, PWR_PWR_ALL), fail, TAG, "power"); + + ESP_GOTO_ON_ERROR(fusb302_enable_irq(device, true), fail, TAG, "enable irq"); + ESP_LOGI(TAG, "init done"); + *out = device; + return ESP_OK; + +fail: + free(device); + return ret; +} + +static esp_err_t fusb302_deinit(fusb302_dev_t *device) +{ + esp_err_t ret = ESP_OK; + if (!device) { + return ESP_ERR_INVALID_ARG; + } + const i2c_master_dev_handle_t i2c_dev = device->hw.i2c_dev; + + ESP_GOTO_ON_ERROR(fusb302_enable_irq(device, false), fail, TAG, "disable irq"); + ESP_GOTO_ON_ERROR(i2c_wr8(i2c_dev, REG_SWITCHES0, 0x00), fail, TAG, "SWITCHES0 Hi-Z"); + ESP_GOTO_ON_ERROR(i2c_wr8(i2c_dev, REG_CONTROL2, CTL2_MODE_NONE), fail, TAG, "CONTROL2 mode none"); + ESP_GOTO_ON_ERROR(i2c_wr8(i2c_dev, REG_POWER, PWR_PWR_LOW), fail, TAG, "power down"); + +fail: + free(device); + return ret; +} + +static esp_err_t fusb302_set_role(fusb302_dev_t *device, usb_tcpm_power_role_t role) +{ + if (!device) { + return ESP_ERR_INVALID_ARG; + } + + const i2c_master_dev_handle_t i2c_dev = device->hw.i2c_dev; + uint8_t value; + + switch (role) { + case USB_TCPM_PWR_SINK: { + uint8_t prev_status1a = 0; + uint8_t prev_togss = 0; + const bool from_drp = (device->role == USB_TCPM_PWR_DRP); + + if (from_drp) { + (void)i2c_rd8(i2c_dev, REG_STATUS1A, &prev_status1a); + prev_togss = (prev_status1a >> ST1A_TOGSS_SHIFT) & 0x07; + + /* + * In DRP this path runs after TOG_DONE in the common attach flow. + * Preload Rd so manual pull state is correct as soon as control + * returns to SWITCHES0 bits while leaving autonomous toggle mode. + */ + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_SWITCHES0, SW0_CC1_PD_EN | SW0_CC2_PD_EN), + TAG, "SWITCHES0 Rd preload"); + } + + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_CONTROL2, CTL2_MODE_UFP), TAG, "set UFP mode"); + + if (!from_drp) { + /* Rd on both CC pins */ + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_SWITCHES0, SW0_CC1_PD_EN | SW0_CC2_PD_EN), + TAG, "SWITCHES0 Rd"); + } + + const bool already_snk = from_drp && + (prev_togss == ST1A_TOGSS_SNK1 || prev_togss == ST1A_TOGSS_SNK2); + if (!already_snk) { + /* Start/ensure toggle engine running in UFP mode */ + ESP_RETURN_ON_ERROR(fusb302_toggle_enable(device), TAG, "enable toggle (UFP)"); + } + + ESP_LOGI(TAG, "FUSB302 set as Sink (UFP+TOGGLE)"); + break; + } + + case USB_TCPM_PWR_SOURCE: { + uint8_t prev_status1a = 0; + uint8_t prev_togss = 0; + const bool from_drp = (device->role == USB_TCPM_PWR_DRP); + + if (from_drp) { + (void)i2c_rd8(i2c_dev, REG_STATUS1A, &prev_status1a); + prev_togss = (prev_status1a >> 3) & 0x07; + } + const bool already_src = from_drp && + (prev_togss == ST1A_TOGSS_SRC1 || prev_togss == ST1A_TOGSS_SRC2); + + // Set DFP mode WITHOUT toggle first + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_CONTROL2, CTL2_MODE_DFP), TAG, "set DFP mode"); + + /* Rp on both CC pins, clear any Rd leftover from sink */ + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_SWITCHES0, SW0_CC1_PU_EN | SW0_CC2_PU_EN), + TAG, "SWITCHES0 Rp"); + + if (!already_src) { + /* Use Default Rp while toggling for reliable TOG_DONE */ + ESP_RETURN_ON_ERROR(fusb302_set_host_cur(device, CTL0_HOST_CUR_DEFAULT), TAG, "set HOST_CUR toggle"); + ESP_RETURN_ON_ERROR(i2c_wr8(device->hw.i2c_dev, REG_MEASURE, + rp_current_to_mdac(USB_TCPM_RP_DEFAULT)), + TAG, "set MDAC toggle"); + } + + /* POWERROLE=1, TXCC disabled until polarity is applied */ + uint8_t sw1 = 0; + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_SWITCHES1, &sw1), TAG, "read SWITCHES1"); + sw1 = (sw1 & ~(SW1_TXCC1_EN | SW1_TXCC2_EN)) | SW1_POWERROLE; + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_SWITCHES1, sw1), TAG, "write SWITCHES1 role"); + + if (!already_src) { + // Not already attached: arm toggle normally + ESP_RETURN_ON_ERROR(fusb302_toggle_enable(device), TAG, "enable toggle"); + } + + ESP_LOGI(TAG, "FUSB302 set as Source (DFP+TOGGLE)"); + break; + } + + case USB_TCPM_PWR_DRP: { // Dual-Role (DRP toggle) + /* + * DRP + TOGGLE: + * - FUSB302 alternates between UFP/DFP, reports result in TOGSS. + * - Policy layer (later) can look at TOGSS to decide final role. + */ + /* Set DRP mode first; then force a clean 0->1 toggle edge. */ + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_CONTROL2, CTL2_MODE_DRP), + TAG, "set DRP"); + + /* Enable Rp when acting as DFP */ + value = SW0_CC1_PU_EN | SW0_CC2_PU_EN; + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_SWITCHES0, value), + TAG, "SWITCHES0 DRP Rp"); + + /* HOST_CUR for DFP phases */ + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_CONTROL0, &value), + TAG, "read CONTROL0"); + value &= ~CTL0_HOST_CUR_MASK; + value |= CTL0_HOST_CUR_DEFAULT; + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_CONTROL0, value), + TAG, "set HOST_CUR DRP"); + + /* Use Default Rp MDAC while toggling (datasheet toggle example). */ + ESP_RETURN_ON_ERROR(i2c_wr8(device->hw.i2c_dev, REG_MEASURE, + rp_current_to_mdac(USB_TCPM_RP_DEFAULT)), + TAG, "set MDAC DRP (default)"); + + /* Ensure toggle restarts even if it was already set in a prior role. */ + ESP_RETURN_ON_ERROR(fusb302_toggle_enable(device), + TAG, "toggle arm (DRP)"); + + ESP_LOGI(TAG, "FUSB302 set as DRP (TOGGLE), Rp advert=%umA", + (unsigned)rp_current_to_ma(device->rp_current)); + break; + } + + default: + return ESP_ERR_INVALID_ARG; + } + + device->role = role; + return ESP_OK; +} + +static esp_err_t fusb302_service_irq(const fusb302_dev_t *device, + typec_evt_mask_t *events) +{ + if (!device || !events) { + return ESP_ERR_INVALID_ARG; + } + + const i2c_master_dev_handle_t i2c_dev = device->hw.i2c_dev; + *events = 0; + + // Drain latched causes; exit when no causes AND pin is high + for (int read_idx = 0; read_idx < MAX_INT_READS; ++read_idx) { + uint8_t intr_reg = 0; + uint8_t intr_a_reg = 0; + uint8_t intr_b_reg = 0; + + // Read-to-clear + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_INTERRUPT, &intr_reg), TAG, "read INTERRUPT"); + (void)i2c_rd8(i2c_dev, REG_INTERRUPTA, &intr_a_reg); + (void)i2c_rd8(i2c_dev, REG_INTERRUPTB, &intr_b_reg); + + if (intr_reg & INT_COMP_CHNG) { + *events |= TYPEC_EVT_CC; + } + if (intr_a_reg & INTA_TOG_DONE) { + *events |= TYPEC_EVT_TOG; + } + if (intr_reg & INT_VBUSOK) { + *events |= TYPEC_EVT_VBUS; + } + if (intr_b_reg & INTB_RX_SOP) { + *events |= TYPEC_EVT_RX; + } + + ESP_LOGD(TAG, "IRQ: INT=0x%02x INTA=0x%02x INTB=0x%02x -> events=0x%02x", + intr_reg, intr_a_reg, intr_b_reg, *events); + // Done if nothing latched AND INT pin is high (deasserted) + if ((intr_reg | intr_a_reg | intr_b_reg) == 0 && gpio_get_level(device->hw.gpio_int) == 1) { + break; + } + + // Small breath to let the device clear internal latches + esp_rom_delay_us(200); + } + return ESP_OK; +} + +static esp_err_t fusb302_enable_irq(const fusb302_dev_t *device, bool enable) +{ + if (!device) { + return ESP_ERR_INVALID_ARG; + } + + const i2c_master_dev_handle_t i2c_dev = device->hw.i2c_dev; + uint8_t control0 = 0; + + if (!enable) { + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_MASK, 0xFF), TAG, "mask INTERRUPT"); + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_MASKA, 0xFF), TAG, "mask INTERRUPTA"); + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_MASKB, 0xFF), TAG, "mask INTERRUPTB"); + + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_CONTROL0, &control0), TAG, "read CONTROL0"); + control0 |= CTL0_INT_MASK; + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_CONTROL0, control0), TAG, "write CONTROL0"); + return ESP_OK; + } + + // INT pin enabled: CONTROL0.INT_MASK = 0 + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_CONTROL0, &control0), TAG, "read CONTROL0"); + control0 &= ~CTL0_INT_MASK; + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_CONTROL0, control0), TAG, "write CONTROL0"); + + // Clear any latched interrupts (read-to-clear) + uint8_t tmp; + (void)i2c_rd8(i2c_dev, REG_INTERRUPT, &tmp); + (void)i2c_rd8(i2c_dev, REG_INTERRUPTA, &tmp); + (void)i2c_rd8(i2c_dev, REG_INTERRUPTB, &tmp); + + // REG_MASK: only what we actually use + uint8_t mask = 0xFF; + mask &= ~INT_VBUSOK; // sink attach/detach, useful globally + mask &= ~INT_COMP_CHNG; // source detach (after MEAS points to active CC) + + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_MASK, mask), TAG, "write MASK"); + + // REG_MASKA: TOG_DONE for toggle attach/orientation + uint8_t maska = 0xFF; + maska &= ~INTA_TOG_DONE; + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_MASKA, maska), TAG, "write MASKA"); + + // REG_MASKB: RX_SOP when RX handling is enabled + uint8_t maskb = 0xFF; + maskb &= ~INTB_RX_SOP; + ESP_RETURN_ON_ERROR(i2c_wr8(i2c_dev, REG_MASKB, maskb), TAG, "write MASKB"); + + return ESP_OK; +} + +static esp_err_t fusb302_get_status(const fusb302_dev_t *device, typec_cc_status_t *status) +{ + if (!device || !status) { + return ESP_ERR_INVALID_ARG; + } + + const i2c_master_dev_handle_t i2c_dev = device->hw.i2c_dev; + memset(status, 0, sizeof(*status)); + + status->tog_result = TYPEC_TOG_RESULT_NONE; + + uint8_t status0 = 0; + uint8_t status1a = 0; + + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_STATUS1A, &status1a), TAG, "read STATUS1A"); + const uint8_t togss = (status1a >> ST1A_TOGSS_SHIFT) & 0x07; + + const bool src_att = (togss == ST1A_TOGSS_SRC1) || (togss == ST1A_TOGSS_SRC2); + const bool snk_att = (togss == ST1A_TOGSS_SNK1) || (togss == ST1A_TOGSS_SNK2); + + if (src_att) { + status->tog_result = TYPEC_TOG_RESULT_SRC; + } else if (snk_att) { + status->tog_result = TYPEC_TOG_RESULT_SNK; + } + + status->attached = src_att || snk_att; + status->cc2_active = (togss == ST1A_TOGSS_SRC2) || (togss == ST1A_TOGSS_SNK2); + + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_STATUS0, &status0), TAG, "read STATUS0"); + status->vbus_ok = (status0 & ST0_VBUSOK) != 0; + + if (device->role == USB_TCPM_PWR_SINK) { + uint8_t bclvl = (status0 & ST0_BC_LVL_MASK); + if (status->attached && status->vbus_ok) { + esp_rom_delay_us(1000); + ESP_RETURN_ON_ERROR(i2c_rd8(i2c_dev, REG_STATUS0, &status0), TAG, "read STATUS0 retry"); + status->vbus_ok = (status0 & ST0_VBUSOK) != 0; + bclvl = (status0 & ST0_BC_LVL_MASK); + } + status->rp_current_ma = bclvl_to_ma(bclvl); + return ESP_OK; + } + + status->rp_current_ma = rp_current_to_ma(device->rp_current); + if (snk_att) { + status->rp_current_ma = 0; + } + + if (device->role == USB_TCPM_PWR_SOURCE) { + /* COMP=0 => below MDAC => Rd present */ + const bool rd_present = ((status0 & ST0_COMP) == 0); + status->attached = rd_present; + } + + return ESP_OK; +} + +/* ---------------- Backend glue & factory ---------------- */ + +static esp_err_t fusb302_backend_init(void *ctx, const usb_tcpm_port_config_t *cfg) +{ + if (!ctx || !cfg) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = ESP_OK; + fusb302_port_ctx_t *port = (fusb302_port_ctx_t *)ctx; + + const i2c_device_config_t dev_cfg = { + .device_address = port->hw_cfg.i2c_addr_7b, + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .scl_speed_hz = 400000, // default to 400 kHz + }; + ESP_RETURN_ON_ERROR(i2c_master_bus_add_device(port->hw_cfg.i2c_bus, &dev_cfg, &port->i2c_dev), + TAG, "fusb add failed"); + + const fusb302_hw_cfg_t hw = { + .i2c_dev = port->i2c_dev, + .gpio_int = port->hw_cfg.gpio_int, + .rp_current = cfg->rp_current, + }; + + ESP_GOTO_ON_ERROR(fusb302_init(&hw, &port->device), fail, TAG, "fusb302_init failed"); + return ESP_OK; + +fail: + if (port->i2c_dev) { + i2c_master_bus_rm_device(port->i2c_dev); + port->i2c_dev = NULL; + } + return ret; +} + +static esp_err_t fusb302_backend_deinit(void *ctx) +{ + if (!ctx) { + return ESP_ERR_INVALID_ARG; + } + + fusb302_port_ctx_t *port = (fusb302_port_ctx_t *)ctx; + esp_err_t ret = ESP_OK; + + if (port->device) { + ESP_GOTO_ON_ERROR(fusb302_deinit(port->device), clear_device, TAG, "fusb302_deinit failed"); +clear_device: + port->device = NULL; + } + + if (port->i2c_dev) { + if (ret == ESP_OK) { + ESP_GOTO_ON_ERROR(i2c_master_bus_rm_device(port->i2c_dev), clear_i2c_dev, TAG, + "fusb302 remove i2c device failed"); + } else { + (void)i2c_master_bus_rm_device(port->i2c_dev); + } +clear_i2c_dev: + port->i2c_dev = NULL; + } + + free(port); + return ret; +} + +static esp_err_t fusb302_backend_set_role(void *ctx, usb_tcpm_power_role_t role) +{ + if (!ctx) { + return ESP_ERR_INVALID_ARG; + } + const fusb302_port_ctx_t *port = (const fusb302_port_ctx_t *)ctx; + if (!port->device) { + return ESP_ERR_INVALID_STATE; + } + return fusb302_set_role(port->device, role); +} + +static esp_err_t fusb302_backend_service_irq(void *ctx, typec_evt_mask_t *events) +{ + if (!ctx) { + return ESP_ERR_INVALID_ARG; + } + const fusb302_port_ctx_t *port = (const fusb302_port_ctx_t *)ctx; + if (!port->device) { + return ESP_ERR_INVALID_STATE; + } + return fusb302_service_irq(port->device, events); +} + +static esp_err_t fusb302_backend_get_status(void *ctx, typec_cc_status_t *status) +{ + if (!ctx) { + return ESP_ERR_INVALID_ARG; + } + const fusb302_port_ctx_t *port = (const fusb302_port_ctx_t *)ctx; + if (!port->device) { + return ESP_ERR_INVALID_STATE; + } + return fusb302_get_status(port->device, status); +} + +static esp_err_t fusb302_backend_commit_attach(void *ctx, bool cc2_active, bool is_source) +{ + if (!ctx) { + return ESP_ERR_INVALID_ARG; + } + const fusb302_port_ctx_t *port = (const fusb302_port_ctx_t *)ctx; + if (!port->device) { + return ESP_ERR_INVALID_STATE; + } + return fusb302_commit_attach(port->device, cc2_active, is_source); +} + +static esp_err_t fusb302_backend_get_irq_gpio(void *ctx, gpio_num_t *gpio_num) +{ + if (!ctx || !gpio_num) { + return ESP_ERR_INVALID_ARG; + } + const fusb302_port_ctx_t *port = (const fusb302_port_ctx_t *)ctx; + *gpio_num = port->hw_cfg.gpio_int; + return ESP_OK; +} + +static const typec_port_backend_t s_fusb302_backend = { + .init = fusb302_backend_init, + .deinit = fusb302_backend_deinit, + .set_role = fusb302_backend_set_role, + .service_irq = fusb302_backend_service_irq, + .get_status = fusb302_backend_get_status, + .commit_attach = fusb302_backend_commit_attach, + .get_irq_gpio = fusb302_backend_get_irq_gpio, +}; + +esp_err_t usb_tcpm_port_create_fusb302(const usb_tcpm_port_config_t *port_cfg, + const usb_tcpm_fusb302_config_t *hw_cfg, + usb_tcpm_port_handle_t *port_hdl_ret) +{ + ESP_RETURN_ON_FALSE(port_cfg && hw_cfg && port_hdl_ret, + ESP_ERR_INVALID_ARG, TAG, "bad args"); + + fusb302_port_ctx_t *ctx = (fusb302_port_ctx_t *)calloc(1, sizeof(*ctx)); + ESP_RETURN_ON_FALSE(ctx, ESP_ERR_NO_MEM, TAG, "no mem"); + + ctx->hw_cfg = *hw_cfg; + + esp_err_t ret = ESP_OK; + typec_port_handle_t port = NULL; + ESP_GOTO_ON_ERROR(typec_port_new(&s_fusb302_backend, ctx, port_cfg, &port), + fail, TAG, "typec_port_new failed"); + + *port_hdl_ret = (usb_tcpm_port_handle_t)port; + return ESP_OK; + +fail: + (void)fusb302_backend_deinit(ctx); + return ret; +} diff --git a/type_c/usb_tcpm/src/husb320_ctrl.c b/type_c/usb_tcpm/src/husb320_ctrl.c new file mode 100644 index 00000000..a892afc5 --- /dev/null +++ b/type_c/usb_tcpm/src/husb320_ctrl.c @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "esp_err.h" +#include "esp_log.h" +#include "husb320_ctrl.h" + +// Experimental stub; replace with real register map and logic. +static const char *TAG = "husb320"; + +/** + * @brief HUSB320 device context. + */ +struct husb320_dev { + husb320_hw_cfg_t hw; + usb_tcpm_power_role_t role; +}; + +static esp_err_t husb320_init(const husb320_hw_cfg_t *hw, husb320_dev_t **out) +{ + if (!hw || !out) { + return ESP_ERR_INVALID_ARG; + } + husb320_dev_t *device = calloc(1, sizeof(*device)); + if (!device) { + return ESP_ERR_NO_MEM; + } + device->hw = *hw; + device->role = USB_TCPM_PWR_SINK; + + // TODO: I2C probe, optional soft reset, clear INTs, enable CC measure + ESP_LOGI(TAG, "init i2c=%d addr=0x%02x (polling)", hw->i2c_port, hw->i2c_addr); + + *out = device; + return ESP_OK; +} + +static void husb320_deinit(husb320_dev_t *device) +{ + if (!device) { + return; + } + // TODO: put device into safe state if needed + free(device); +} + +static esp_err_t husb320_set_role(husb320_dev_t *device, usb_tcpm_power_role_t role) +{ + if (!device) { + return ESP_ERR_INVALID_ARG; + } + device->role = role; + // TODO: write Rp/Rd/DRP selection to device + ESP_LOGI(TAG, "set role=%d", (int)role); + return ESP_OK; +} + +static esp_err_t husb320_read_cc_status(const husb320_dev_t *device, husb320_cc_status_t *status) +{ + if (!device || !status) { + return ESP_ERR_INVALID_ARG; + } + // TODO: read CC comparators / status regs and fill fields. + // For now, report detached so the task runs harmlessly. + memset(status, 0, sizeof(*status)); + return ESP_OK; +} diff --git a/type_c/usb_tcpm/src/usb_tcpm.c b/type_c/usb_tcpm/src/usb_tcpm.c new file mode 100644 index 00000000..86530100 --- /dev/null +++ b/type_c/usb_tcpm/src/usb_tcpm.c @@ -0,0 +1,798 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_check.h" +#include "esp_log.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" + +#include "usb/usb_tcpm.h" +#include "usb_tcpm_backend.h" + +static const char *TAG = "usb_tcpm"; + +/* Define the event base */ +ESP_EVENT_DEFINE_BASE(USB_TCPM_EVENT); + +/* ---------- Per-port context ---------- */ +/** + * @brief Per-port runtime context. + */ +typedef struct typec_port { + struct { + struct typec_port *next; /**< Next port in global list. */ + bool irq_pending; /**< IRQ pending for shared worker. */ + bool irq_processing; /**< Shared worker is processing this port. */ + bool deleting; /**< Port is being deleted. */ + } dynamic; /**< Fields protected by s_lock. */ + + struct { + bool attached; /**< True if attached. */ + bool cc2_active; /**< True if CC2 is active. */ + uint32_t rp_current_ma; /**< Advertised/observed Rp current in mA. */ + usb_tcpm_power_role_t role; /**< Current power role. */ + } mux_protected; /**< Fields protected by status_lock. */ + + struct { + bool pending_attach; /**< True while waiting for VBUS after commit. */ + bool source_detach_grace_active; /**< True while source detach detection is in startup grace period. */ + TickType_t source_detach_grace_start_tick; /**< Tick when source detach grace was armed. */ + } single_thread; /**< Accessed by shared worker task. */ + + struct { + usb_tcpm_power_role_t policy_role; /**< Policy role for fallback decisions. */ + void *backend_ctx; /**< Backend device handle. */ + const typec_port_backend_t *backend; /**< Backend operations. */ + gpio_num_t gpio_int; /**< INT GPIO (active-low). */ + usb_tcpm_port_config_t cfg; /**< Configuration snapshot. */ + } constant; /**< Initialized during create, then treated as read-only. */ + + struct { + StaticSemaphore_t status_lock_storage; /**< Storage for status lock. */ + SemaphoreHandle_t status_lock; /**< Protects status snapshot fields. */ + } locks; +} typec_port_t; + +#define USB_TCPM_TASK_STACK_DEFAULT (4096) +#define USB_TCPM_TASK_PRIO_DEFAULT (tskIDLE_PRIORITY + 2) +/* + * Bus-powered sinks can briefly disturb CC while local rails and the sink + * controller are coming up after VBUS is enabled. Mask source detach checks + * for a short period after source attach to avoid false detach. + */ +#define USB_TCPM_SRC_DETACH_GRACE_MS (200) + +static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED; +static typec_port_t *s_ports; +static TaskHandle_t s_task; + +static void usb_tcpm_task(void *arg); + +static void usb_tcpm_port_list_add(typec_port_t *ctx) +{ + portENTER_CRITICAL(&s_lock); + ctx->dynamic.next = s_ports; + s_ports = ctx; + portEXIT_CRITICAL(&s_lock); +} + +static void usb_tcpm_port_list_remove(typec_port_t *ctx) +{ + portENTER_CRITICAL(&s_lock); + typec_port_t **port_cursor = &s_ports; + while (*port_cursor && *port_cursor != ctx) { + port_cursor = &(*port_cursor)->dynamic.next; + } + if (*port_cursor == ctx) { + *port_cursor = ctx->dynamic.next; + } + ctx->dynamic.next = NULL; + portEXIT_CRITICAL(&s_lock); +} + +#define USB_TCPM_STATUS_LOCK(ctx) do { \ + (void)xSemaphoreTake((ctx)->locks.status_lock, portMAX_DELAY); \ +} while (0) +#define USB_TCPM_STATUS_UNLOCK(ctx) do { \ + (void)xSemaphoreGive((ctx)->locks.status_lock); \ +} while (0) + +/* ---------- Tiny helpers ---------- */ +static inline void usb_tcpm_emit_attached(const typec_port_t *ctx) +{ + usb_tcpm_evt_attached_t evt = { + .port = (usb_tcpm_port_handle_t)ctx, + .cc2_active = ctx->mux_protected.cc2_active, + .rp_current_ma = ctx->mux_protected.rp_current_ma, + }; + usb_tcpm_event_id_t event_id; + if (ctx->mux_protected.role == USB_TCPM_PWR_SOURCE) { + event_id = USB_TCPM_EVENT_SOURCE_ATTACHED; + } else if (ctx->mux_protected.role == USB_TCPM_PWR_SINK) { + event_id = USB_TCPM_EVENT_SINK_ATTACHED; + } else { + ESP_LOGW(TAG, "Attached event with non-source/sink role=%d", (int)ctx->mux_protected.role); + return; + } + + const esp_err_t err = esp_event_post(USB_TCPM_EVENT, + event_id, + &evt, + sizeof(evt), + portMAX_DELAY); + ESP_RETURN_VOID_ON_ERROR(err, TAG, "Failed to post typec ATTACHED event: %s", esp_err_to_name(err)); +} + +static inline void usb_tcpm_emit_detached(const typec_port_t *ctx) +{ + usb_tcpm_evt_detached_t evt = { + .port = (usb_tcpm_port_handle_t)ctx, + }; + usb_tcpm_event_id_t event_id; + if (ctx->mux_protected.role == USB_TCPM_PWR_SOURCE) { + event_id = USB_TCPM_EVENT_SOURCE_DETACHED; + } else if (ctx->mux_protected.role == USB_TCPM_PWR_SINK) { + event_id = USB_TCPM_EVENT_SINK_DETACHED; + } else { + ESP_LOGW(TAG, "Detached event with non-source/sink role=%d", (int)ctx->mux_protected.role); + return; + } + + const esp_err_t err = esp_event_post(USB_TCPM_EVENT, + event_id, + &evt, + sizeof(evt), + portMAX_DELAY); + ESP_RETURN_VOID_ON_ERROR(err, TAG, "Failed to post typec DETACHED event: %s", esp_err_to_name(err)); +} + +static inline void usb_tcpm_set_vbus_source(const typec_port_t *ctx, bool enable) +{ + if (ctx->constant.cfg.src_vbus_gpio != GPIO_NUM_NC) { + gpio_set_level(ctx->constant.cfg.src_vbus_gpio, enable); // active-high + } + if (ctx->constant.cfg.src_vbus_gpio_n != GPIO_NUM_NC) { + gpio_set_level(ctx->constant.cfg.src_vbus_gpio_n, !enable); // active-low + } + ESP_LOGD(TAG, "VBUS=%d", enable); +} + +static inline void usb_tcpm_set_attach_snapshot(typec_port_t *ctx, bool attached, bool cc2_active, uint32_t rp_current_ma) +{ + USB_TCPM_STATUS_LOCK(ctx); + ctx->mux_protected.attached = attached; + ctx->mux_protected.cc2_active = cc2_active; + ctx->mux_protected.rp_current_ma = rp_current_ma; + USB_TCPM_STATUS_UNLOCK(ctx); +} + +static inline void usb_tcpm_clear_pending_attach_state(typec_port_t *ctx) +{ + ctx->single_thread.pending_attach = false; +} + +static inline void usb_tcpm_arm_source_detach_grace(typec_port_t *ctx) +{ + if (USB_TCPM_SRC_DETACH_GRACE_MS <= 0) { + ctx->single_thread.source_detach_grace_active = false; + return; + } + + ctx->single_thread.source_detach_grace_active = true; + ctx->single_thread.source_detach_grace_start_tick = xTaskGetTickCount(); +} + +static inline bool usb_tcpm_source_detach_grace_active(typec_port_t *ctx) +{ + if (!ctx->single_thread.source_detach_grace_active) { + return false; + } + + const TickType_t grace_ticks = pdMS_TO_TICKS(USB_TCPM_SRC_DETACH_GRACE_MS); + const TickType_t elapsed = xTaskGetTickCount() - ctx->single_thread.source_detach_grace_start_tick; + if (elapsed >= grace_ticks) { + ctx->single_thread.source_detach_grace_active = false; + return false; + } + + return true; +} + +static inline void usb_tcpm_set_sink_pending_attach(typec_port_t *ctx, + const typec_cc_status_t *status) +{ + ctx->single_thread.pending_attach = true; + + USB_TCPM_STATUS_LOCK(ctx); + ctx->mux_protected.cc2_active = status->cc2_active; + ctx->mux_protected.rp_current_ma = status->rp_current_ma; + USB_TCPM_STATUS_UNLOCK(ctx); +} + +static inline void usb_tcpm_sink_attach_or_wait_vbus(typec_port_t *ctx, + const typec_cc_status_t *status) +{ + if (status->vbus_ok) { + bool cc2_active = status->cc2_active; + uint32_t rp_current_ma = status->rp_current_ma; + if (rp_current_ma == 0) { + typec_cc_status_t sink_status = {0}; + if (ctx->constant.backend->get_status(ctx->constant.backend_ctx, &sink_status) == ESP_OK && + sink_status.vbus_ok) { + rp_current_ma = sink_status.rp_current_ma; + if (sink_status.tog_result == TYPEC_TOG_RESULT_SNK) { + cc2_active = sink_status.cc2_active; + } + } + } + usb_tcpm_set_attach_snapshot(ctx, true, cc2_active, rp_current_ma); + usb_tcpm_clear_pending_attach_state(ctx); + usb_tcpm_emit_attached(ctx); + } else { + usb_tcpm_set_sink_pending_attach(ctx, status); + } +} + +static inline void usb_tcpm_rearm_after_detach(typec_port_t *ctx, usb_tcpm_power_role_t rearm_role) +{ + if (ctx->constant.policy_role == USB_TCPM_PWR_DRP) { + const esp_err_t err = ctx->constant.backend->set_role(ctx->constant.backend_ctx, USB_TCPM_PWR_DRP); + ESP_RETURN_VOID_ON_ERROR(err, TAG, "set_role(DRP) failed: %s", esp_err_to_name(err)); + + USB_TCPM_STATUS_LOCK(ctx); + ctx->mux_protected.role = USB_TCPM_PWR_DRP; + USB_TCPM_STATUS_UNLOCK(ctx); + return; + } + + const esp_err_t err = ctx->constant.backend->set_role(ctx->constant.backend_ctx, rearm_role); + ESP_RETURN_VOID_ON_ERROR(err, TAG, "set_role(%d rearm) failed: %s", (int)rearm_role, esp_err_to_name(err)); +} + +/* Locking note: + * usb_tcpm_handle_events() runs in the shared worker task context and once + * during typec_port_new() before the port is visible to the worker. + * Lock-free reads of mux_protected fields are intentional in this function. + * status_lock is still used for updates that must stay coherent for cross-task + * readers (e.g. usb_tcpm_get_status()). + */ +static void usb_tcpm_handle_events(typec_port_t *ctx, typec_evt_mask_t events) +{ + if (!(events & (TYPEC_EVT_TOG | TYPEC_EVT_CC | TYPEC_EVT_VBUS))) { + return; + } + + if (ctx->mux_protected.role == USB_TCPM_PWR_DRP) { + if (!(events & TYPEC_EVT_TOG)) { + return; + } + + typec_cc_status_t status = {0}; + ESP_RETURN_VOID_ON_ERROR(ctx->constant.backend->get_status(ctx->constant.backend_ctx, &status), + TAG, "get_status (DRP) failed"); + + usb_tcpm_power_role_t next_role = USB_TCPM_PWR_DRP; + if (status.tog_result == TYPEC_TOG_RESULT_SRC) { + next_role = USB_TCPM_PWR_SOURCE; + } else if (status.tog_result == TYPEC_TOG_RESULT_SNK) { + next_role = USB_TCPM_PWR_SINK; + } else { + return; + } + + if (next_role == USB_TCPM_PWR_SINK) { + usb_tcpm_set_vbus_source(ctx, false); + } + + ESP_RETURN_VOID_ON_ERROR(ctx->constant.backend->set_role(ctx->constant.backend_ctx, next_role), + TAG, "set_role(%d) failed", (int)next_role); + + USB_TCPM_STATUS_LOCK(ctx); + ctx->mux_protected.role = next_role; + ctx->mux_protected.attached = false; + ctx->mux_protected.cc2_active = false; + ctx->mux_protected.rp_current_ma = 0; + USB_TCPM_STATUS_UNLOCK(ctx); + usb_tcpm_clear_pending_attach_state(ctx); + + if (next_role == USB_TCPM_PWR_SOURCE) { + if (ctx->constant.backend->commit_attach) { + ESP_RETURN_VOID_ON_ERROR(ctx->constant.backend->commit_attach(ctx->constant.backend_ctx, + status.cc2_active, true), + TAG, "commit_attach failed"); + } + usb_tcpm_set_attach_snapshot(ctx, true, status.cc2_active, status.rp_current_ma); + usb_tcpm_arm_source_detach_grace(ctx); + + usb_tcpm_set_vbus_source(ctx, true); + usb_tcpm_emit_attached(ctx); + return; + } + + ctx->single_thread.source_detach_grace_active = false; + if (ctx->constant.backend->commit_attach) { + ESP_RETURN_VOID_ON_ERROR(ctx->constant.backend->commit_attach(ctx->constant.backend_ctx, + status.cc2_active, false), + TAG, "commit_attach failed"); + } + usb_tcpm_sink_attach_or_wait_vbus(ctx, &status); + return; + } + + if (ctx->mux_protected.role == USB_TCPM_PWR_SOURCE) { + const bool attached = ctx->mux_protected.attached; + const typec_evt_mask_t required_ev = attached ? TYPEC_EVT_CC : TYPEC_EVT_TOG; + if (!(events & required_ev)) { + return; + } + + typec_cc_status_t status = {0}; + ESP_RETURN_VOID_ON_ERROR(ctx->constant.backend->get_status(ctx->constant.backend_ctx, &status), + TAG, "get_status (SRC) failed"); + + if (!attached) { + if (status.tog_result != TYPEC_TOG_RESULT_SRC) { + return; + } + + if (ctx->constant.backend->commit_attach) { + ESP_RETURN_VOID_ON_ERROR(ctx->constant.backend->commit_attach(ctx->constant.backend_ctx, + status.cc2_active, true), + TAG, "commit_attach failed"); + } + usb_tcpm_set_attach_snapshot(ctx, true, status.cc2_active, status.rp_current_ma); + usb_tcpm_clear_pending_attach_state(ctx); + usb_tcpm_arm_source_detach_grace(ctx); + + usb_tcpm_set_vbus_source(ctx, true); + usb_tcpm_emit_attached(ctx); + return; + } + + if (status.attached) { + return; + } + + if (usb_tcpm_source_detach_grace_active(ctx)) { + return; + } + + usb_tcpm_set_attach_snapshot(ctx, false, false, 0); + usb_tcpm_clear_pending_attach_state(ctx); + ctx->single_thread.source_detach_grace_active = false; + + usb_tcpm_set_vbus_source(ctx, false); + usb_tcpm_emit_detached(ctx); + + usb_tcpm_rearm_after_detach(ctx, USB_TCPM_PWR_SOURCE); + return; + } + + if (ctx->mux_protected.role == USB_TCPM_PWR_SINK) { + if (ctx->single_thread.pending_attach) { + /* After commit_attach() we disable TOGGLE, so TOGSS is no longer meaningful here. + * While pending_attach, only VBUS (and optionally CC) matters. + */ + if (!(events & (TYPEC_EVT_VBUS | TYPEC_EVT_CC))) { + return; + } + + typec_cc_status_t status = {0}; + ESP_RETURN_VOID_ON_ERROR(ctx->constant.backend->get_status(ctx->constant.backend_ctx, &status), + TAG, "get_status (SNK pending) failed"); + + /* Optional cancel: if CC clearly disappeared while waiting for VBUS. */ + if ((events & TYPEC_EVT_CC) && (status.rp_current_ma == 0)) { + usb_tcpm_clear_pending_attach_state(ctx); + usb_tcpm_rearm_after_detach(ctx, USB_TCPM_PWR_SINK); + return; + } + + if (!status.vbus_ok) { + return; + } + + /* VBUS is present => now we are really attached as Sink. */ + usb_tcpm_set_attach_snapshot(ctx, true, ctx->mux_protected.cc2_active, status.rp_current_ma); + usb_tcpm_clear_pending_attach_state(ctx); + usb_tcpm_emit_attached(ctx); + return; + } + + const bool attached = ctx->mux_protected.attached; + /* While attached, process VBUS for detach; CC can be used to track rp_current_ma updates. */ + const typec_evt_mask_t required_ev = attached ? (TYPEC_EVT_VBUS | TYPEC_EVT_CC) : TYPEC_EVT_TOG; + if (!(events & required_ev)) { + return; + } + + typec_cc_status_t status = {0}; + ESP_RETURN_VOID_ON_ERROR(ctx->constant.backend->get_status(ctx->constant.backend_ctx, &status), + TAG, "get_status (SNK) failed"); + + if (!attached) { + if (status.tog_result != TYPEC_TOG_RESULT_SNK) { + return; + } + + /* Commit orientation now (disable toggle + route measurement to active CC). */ + if (ctx->constant.backend->commit_attach) { + ESP_RETURN_VOID_ON_ERROR(ctx->constant.backend->commit_attach(ctx->constant.backend_ctx, + status.cc2_active, false), + TAG, "commit_attach (SNK) failed"); + } + usb_tcpm_sink_attach_or_wait_vbus(ctx, &status); + return; + } + + /* Optional: keep updating allowed current while attached. */ + if (events & TYPEC_EVT_CC) { + USB_TCPM_STATUS_LOCK(ctx); + ctx->mux_protected.rp_current_ma = status.rp_current_ma; + USB_TCPM_STATUS_UNLOCK(ctx); + } + + if (status.vbus_ok) { + return; + } + + usb_tcpm_set_attach_snapshot(ctx, false, false, 0); + usb_tcpm_clear_pending_attach_state(ctx); + usb_tcpm_emit_detached(ctx); + + usb_tcpm_rearm_after_detach(ctx, USB_TCPM_PWR_SINK); + } +} + +/* ---------- ISR & Task ---------- */ +static void IRAM_ATTR usb_tcpm_gpio_isr(void *arg) +{ + typec_port_t *ctx = (typec_port_t *)arg; + // Gate further edges until the task drains the device + gpio_intr_disable(ctx->constant.gpio_int); + + BaseType_t higher_priority_task_woken = pdFALSE; + TaskHandle_t task = NULL; + + portENTER_CRITICAL_ISR(&s_lock); + if (!ctx->dynamic.deleting) { + ctx->dynamic.irq_pending = true; + task = s_task; + } + portEXIT_CRITICAL_ISR(&s_lock); + + if (task) { + vTaskNotifyGiveFromISR(task, &higher_priority_task_woken); + } + + if (higher_priority_task_woken) { + portYIELD_FROM_ISR(); + } +} + +static void usb_tcpm_task(void *arg) +{ + (void)arg; + + for (;;) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + for (;;) { + typec_port_t *ctx = NULL; + + portENTER_CRITICAL(&s_lock); + for (typec_port_t *it = s_ports; it; it = it->dynamic.next) { + if (it->dynamic.irq_pending && !it->dynamic.irq_processing && !it->dynamic.deleting) { + it->dynamic.irq_pending = false; + it->dynamic.irq_processing = true; + ctx = it; + break; + } + } + portEXIT_CRITICAL(&s_lock); + + if (!ctx) { + break; + } + + typec_evt_mask_t events = 0; + + const esp_err_t irq_err = ctx->constant.backend->service_irq(ctx->constant.backend_ctx, &events); + if (irq_err != ESP_OK) { + ESP_LOGE(TAG, "service_irq failed: %s", esp_err_to_name(irq_err)); + } else { + ESP_LOGD(TAG, "usb_tcpm_task: policy=%d role=%d events=0x%02x attached=%d", + ctx->constant.policy_role, ctx->mux_protected.role, events, ctx->mux_protected.attached); + usb_tcpm_handle_events(ctx, events); + + /* Handle rare stuck-low INT: drain and process any new events */ + if (gpio_get_level(ctx->constant.gpio_int) == 0) { + vTaskDelay(pdMS_TO_TICKS(10)); + typec_evt_mask_t extra_events = 0; + if (ctx->constant.backend->service_irq(ctx->constant.backend_ctx, &extra_events) == ESP_OK && extra_events) { + ESP_LOGD(TAG, "usb_tcpm_task: extra events=0x%02x attached=%d", + extra_events, ctx->mux_protected.attached); + usb_tcpm_handle_events(ctx, extra_events); + } + } + } + + bool deleting = false; + gpio_num_t gpio_int = GPIO_NUM_NC; + portENTER_CRITICAL(&s_lock); + deleting = ctx->dynamic.deleting; + gpio_int = ctx->constant.gpio_int; + ctx->dynamic.irq_processing = false; + portEXIT_CRITICAL(&s_lock); + + if (!deleting) { + gpio_intr_enable(gpio_int); + } + } + } +} + +/* ---------- Public API ---------- */ + +esp_err_t usb_tcpm_install(const usb_tcpm_install_config_t *config) +{ + // Make sure the default event loop exists. + const esp_err_t err = esp_event_loop_create_default(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_RETURN_ON_ERROR(err, TAG, "Failed to create default event loop: %s", esp_err_to_name(err)); + } + + portENTER_CRITICAL(&s_lock); + const bool already_installed = (s_task != NULL); + portEXIT_CRITICAL(&s_lock); + if (already_installed) { + return ESP_OK; + } + + uint32_t stack = USB_TCPM_TASK_STACK_DEFAULT; + UBaseType_t prio = USB_TCPM_TASK_PRIO_DEFAULT; + if (config) { + if (config->task_stack) { + stack = config->task_stack; + } + if (config->task_prio) { + prio = (UBaseType_t)config->task_prio; + } + } + + TaskHandle_t task = NULL; + if (xTaskCreate(usb_tcpm_task, "typec_port", stack, NULL, prio, &task) != pdPASS) { + return ESP_ERR_NO_MEM; + } + + portENTER_CRITICAL(&s_lock); + if (!s_task) { + s_task = task; + task = NULL; + } + portEXIT_CRITICAL(&s_lock); + if (task) { + vTaskDelete(task); + } + + return ESP_OK; +} + +esp_err_t usb_tcpm_uninstall(void) +{ + portENTER_CRITICAL(&s_lock); + if (s_ports != NULL) { + portEXIT_CRITICAL(&s_lock); + ESP_LOGE(TAG, "Cannot uninstall while ports are active"); + return ESP_ERR_INVALID_STATE; + } + const TaskHandle_t task = s_task; + s_task = NULL; + portEXIT_CRITICAL(&s_lock); + + if (task) { + vTaskDelete(task); + } + return ESP_OK; +} + +esp_err_t typec_port_new(const typec_port_backend_t *backend, + void *backend_ctx, + const usb_tcpm_port_config_t *port_cfg, + typec_port_handle_t *out) +{ + esp_err_t ret = ESP_OK; + typec_port_t *ctx = NULL; + ESP_RETURN_ON_FALSE(backend && backend_ctx && port_cfg && out, + ESP_ERR_INVALID_ARG, TAG, "bad args"); + portENTER_CRITICAL(&s_lock); + const bool installed = (s_task != NULL); + portEXIT_CRITICAL(&s_lock); + if (!installed) { + ret = ESP_ERR_INVALID_STATE; + goto fail; + } + + usb_tcpm_rp_current_t rp = port_cfg->rp_current; + if (rp > USB_TCPM_RP_3A0) { + ESP_LOGW(TAG, "Invalid rp_current=%d; using default", (int)rp); + rp = USB_TCPM_RP_DEFAULT; + } + + ctx = (typec_port_t *)calloc(1, sizeof(*ctx)); + if (!ctx) { + ret = ESP_ERR_NO_MEM; + goto fail; + } + ctx->constant.backend = backend; + ctx->constant.backend_ctx = backend_ctx; + + ctx->locks.status_lock = xSemaphoreCreateMutexStatic(&ctx->locks.status_lock_storage); + if (!ctx->locks.status_lock) { + ret = ESP_ERR_NO_MEM; + goto fail; + } + + ctx->constant.cfg = *port_cfg; + ctx->constant.cfg.rp_current = rp; + ctx->constant.policy_role = ctx->constant.cfg.default_power_role; + ctx->mux_protected.role = ctx->constant.cfg.default_power_role; + + // Configure VBUS MOSFET GPIOs if present + if (ctx->constant.cfg.src_vbus_gpio != GPIO_NUM_NC) { + const gpio_config_t io = { + .pin_bit_mask = 1ULL << ctx->constant.cfg.src_vbus_gpio, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + ESP_GOTO_ON_ERROR(gpio_config(&io), fail, TAG, "src_vbus gpio_config failed"); + + // Ensure VBUS is OFF at startup (active-high) + gpio_set_level(ctx->constant.cfg.src_vbus_gpio, 0); + } + if (ctx->constant.cfg.src_vbus_gpio_n != GPIO_NUM_NC) { + const gpio_config_t io2 = { + .pin_bit_mask = 1ULL << ctx->constant.cfg.src_vbus_gpio_n, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + ESP_GOTO_ON_ERROR(gpio_config(&io2), fail, TAG, "src_vbus gpio_n config failed"); + + // Ensure VBUS is OFF at startup (active-low) + gpio_set_level(ctx->constant.cfg.src_vbus_gpio_n, 1); + } + + if ((ctx->constant.cfg.default_power_role == USB_TCPM_PWR_SOURCE || + ctx->constant.cfg.default_power_role == USB_TCPM_PWR_DRP) && + ctx->constant.cfg.src_vbus_gpio == GPIO_NUM_NC && + ctx->constant.cfg.src_vbus_gpio_n == GPIO_NUM_NC) { + ESP_LOGW(TAG, "Source/DRP role requested but no VBUS GPIOs are set; VBUS will not be driven"); + } + + if (ctx->constant.backend->init) { + ESP_GOTO_ON_ERROR(ctx->constant.backend->init(ctx->constant.backend_ctx, &ctx->constant.cfg), + fail, TAG, "backend init failed"); + } + + /* Apply initial role from config for all default roles. */ + ESP_GOTO_ON_ERROR(ctx->constant.backend->set_role(ctx->constant.backend_ctx, ctx->constant.cfg.default_power_role), + fail, TAG, "set role failed"); + + if (!ctx->constant.backend->get_irq_gpio) { + ret = ESP_ERR_NOT_SUPPORTED; + goto fail; + } + ESP_GOTO_ON_ERROR(ctx->constant.backend->get_irq_gpio(ctx->constant.backend_ctx, &ctx->constant.gpio_int), + fail, TAG, "get irq gpio failed"); + + // Configure INT GPIO as input with pull-up; enable interrupt later + const gpio_config_t gc = { + .pin_bit_mask = 1ULL << ctx->constant.gpio_int, + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + ESP_GOTO_ON_ERROR(gpio_config(&gc), fail, TAG, "gpio_config"); + + const esp_err_t isr_ret = gpio_install_isr_service(0); + if (isr_ret != ESP_ERR_INVALID_STATE) { + ESP_GOTO_ON_ERROR(isr_ret, fail, TAG, "gpio_install_isr_service failed"); + } + ESP_GOTO_ON_ERROR(gpio_isr_handler_add(ctx->constant.gpio_int, usb_tcpm_gpio_isr, ctx), fail, TAG, "gpio_isr_add"); + + /* Drain and process any pre-existing latched causes before arming GPIO */ + typec_evt_mask_t events = 0; + + ESP_GOTO_ON_ERROR(ctx->constant.backend->service_irq(ctx->constant.backend_ctx, &events), fail_isr, TAG, "prime irq"); + if (events) { + usb_tcpm_handle_events(ctx, events); + } + + /* Now arm the GPIO */ + ESP_GOTO_ON_ERROR(gpio_set_intr_type(ctx->constant.gpio_int, GPIO_INTR_LOW_LEVEL), + fail_isr, TAG, "gpio_set_intr_type"); + usb_tcpm_port_list_add(ctx); + gpio_intr_enable(ctx->constant.gpio_int); + + *out = (typec_port_handle_t)ctx; + ESP_LOGI(TAG, "Type-C port started"); + return ESP_OK; +fail_isr: + gpio_intr_disable(ctx->constant.gpio_int); + gpio_isr_handler_remove(ctx->constant.gpio_int); +fail: + ESP_LOGE(TAG, "Init failed, err=%d", ret); + if (ctx) { + free(ctx); + } + return ret; +} + +esp_err_t usb_tcpm_port_destroy(usb_tcpm_port_handle_t port_hdl) +{ + typec_port_t *ctx = (typec_port_t *)port_hdl; + if (!ctx) { + return ESP_ERR_INVALID_ARG; + } + + portENTER_CRITICAL(&s_lock); + ctx->dynamic.deleting = true; + ctx->dynamic.irq_pending = false; + portEXIT_CRITICAL(&s_lock); + + gpio_intr_disable(ctx->constant.gpio_int); + gpio_isr_handler_remove(ctx->constant.gpio_int); + + usb_tcpm_port_list_remove(ctx); + + for (;;) { + bool busy = false; + portENTER_CRITICAL(&s_lock); + busy = ctx->dynamic.irq_processing; + portEXIT_CRITICAL(&s_lock); + if (!busy) { + break; + } + vTaskDelay(pdMS_TO_TICKS(10)); + } + + /* Fail-safe: always stop driving local source VBUS on port teardown. */ + usb_tcpm_set_vbus_source(ctx, false); + + if (ctx->constant.backend_ctx && ctx->constant.backend && ctx->constant.backend->deinit) { + ctx->constant.backend->deinit(ctx->constant.backend_ctx); + } + + free(ctx); + return ESP_OK; +} + +esp_err_t usb_tcpm_get_status(usb_tcpm_port_handle_t port_hdl, usb_tcpm_port_status_t *status) +{ + typec_port_t *ctx = (typec_port_t *)port_hdl; + if (!ctx || !status) { + return ESP_ERR_INVALID_ARG; + } + USB_TCPM_STATUS_LOCK(ctx); + *status = (usb_tcpm_port_status_t) { + .attached = ctx->mux_protected.attached, + .cc2_active = ctx->mux_protected.cc2_active, + .rp_current_ma = ctx->mux_protected.rp_current_ma, + .role = ctx->mux_protected.role, + }; + USB_TCPM_STATUS_UNLOCK(ctx); + return ESP_OK; +} diff --git a/type_c/usb_tcpm/test_apps/pd_fusb302/CMakeLists.txt b/type_c/usb_tcpm/test_apps/pd_fusb302/CMakeLists.txt new file mode 100644 index 00000000..0ab801b4 --- /dev/null +++ b/type_c/usb_tcpm/test_apps/pd_fusb302/CMakeLists.txt @@ -0,0 +1,12 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# Register usb_tcpm component from this repo +list(APPEND EXTRA_COMPONENT_DIRS "../..") + +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +idf_build_set_property(MINIMAL_BUILD ON) + +project(test_app_typec_fusb302) diff --git a/type_c/usb_tcpm/test_apps/pd_fusb302/main/CMakeLists.txt b/type_c/usb_tcpm/test_apps/pd_fusb302/main/CMakeLists.txt new file mode 100644 index 00000000..54af1196 --- /dev/null +++ b/type_c/usb_tcpm/test_apps/pd_fusb302/main/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register( + SRC_DIRS . + INCLUDE_DIRS . + REQUIRES unity + PRIV_REQUIRES usb_tcpm esp_driver_i2c esp_driver_gpio esp_event + WHOLE_ARCHIVE +) diff --git a/type_c/usb_tcpm/test_apps/pd_fusb302/main/Kconfig.projbuild b/type_c/usb_tcpm/test_apps/pd_fusb302/main/Kconfig.projbuild new file mode 100644 index 00000000..703e8ed1 --- /dev/null +++ b/type_c/usb_tcpm/test_apps/pd_fusb302/main/Kconfig.projbuild @@ -0,0 +1 @@ +orsource "../../../Kconfig.pd_fusb302" diff --git a/type_c/usb_tcpm/test_apps/pd_fusb302/main/test_app_main.c b/type_c/usb_tcpm/test_apps/pd_fusb302/main/test_app_main.c new file mode 100644 index 00000000..041651b3 --- /dev/null +++ b/type_c/usb_tcpm/test_apps/pd_fusb302/main/test_app_main.c @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_test_utils_memory.h" + +extern void test_pd_fusb302_setup(void); +extern void test_pd_fusb302_teardown(void); + +void app_main(void) +{ + unity_utils_setup_heap_record(80); + unity_utils_set_leak_level(530); + unity_run_menu(); +} + +/* setUp runs before every test */ +void setUp(void) +{ + unity_utils_record_free_mem(); + test_pd_fusb302_setup(); +} + +/* tearDown runs after every test */ +void tearDown(void) +{ + test_pd_fusb302_teardown(); + unity_utils_evaluate_leaks(); +} diff --git a/type_c/usb_tcpm/test_apps/pd_fusb302/main/test_pd_fusb302.c b/type_c/usb_tcpm/test_apps/pd_fusb302/main/test_pd_fusb302.c new file mode 100644 index 00000000..9329e595 --- /dev/null +++ b/type_c/usb_tcpm/test_apps/pd_fusb302/main/test_pd_fusb302.c @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "driver/gpio.h" +#include "driver/i2c_master.h" +#include "esp_err.h" +#include "esp_event.h" +#include "esp_log.h" +#include "sdkconfig.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "unity.h" + +#include "usb/fusb302.h" +#include "usb/usb_tcpm.h" + +static const char *TAG = "test_typec_fusb302"; +#define VBUS_GPIO_PH CONFIG_USB_TCPM_PD_FUSB302_VBUS_GPIO_PH // active-high enable +#define VBUS_GPIO_N CONFIG_USB_TCPM_PD_FUSB302_VBUS_GPIO_N // active-low enable (e.g., PW2609A) + +/* --- I2C master bus --- */ +#define I2C_SDA CONFIG_USB_TCPM_PD_FUSB302_I2C_SDA_GPIO +#define I2C_SCL CONFIG_USB_TCPM_PD_FUSB302_I2C_SCL_GPIO +#define INT_GPIO CONFIG_USB_TCPM_PD_FUSB302_FUSB_INT_GPIO + +static i2c_master_bus_handle_t s_i2c_bus; +static usb_tcpm_port_handle_t s_port; +static bool s_event_handler_registered; +static bool s_default_loop_created; +static bool s_usb_tcpm_installed; + +/* ===== Event callback ===== */ +static void on_usb_tcpm_event(void *handler_arg, esp_event_base_t base, int32_t id, void *event_data) +{ + (void)handler_arg; + (void)base; + + switch (id) { + case USB_TCPM_EVENT_SINK_ATTACHED: + case USB_TCPM_EVENT_SOURCE_ATTACHED: { + const usb_tcpm_evt_attached_t *const attached_event = (const usb_tcpm_evt_attached_t *)event_data; + const char *const role = (id == USB_TCPM_EVENT_SOURCE_ATTACHED) ? "SRC" : "SNK"; + const uint32_t rp_current_ma = attached_event ? attached_event->rp_current_ma : 0; + const bool cc2_active = attached_event ? attached_event->cc2_active : false; + ESP_LOGI(TAG, "%s ATTACHED: cc2=%d, rp=%u mA", role, cc2_active, (unsigned)rp_current_ma); + break; + } + case USB_TCPM_EVENT_SINK_DETACHED: + case USB_TCPM_EVENT_SOURCE_DETACHED: { + const usb_tcpm_evt_detached_t *const detached_event = (const usb_tcpm_evt_detached_t *)event_data; + const char *const role = (id == USB_TCPM_EVENT_SOURCE_DETACHED) ? "SRC" : "SNK"; + ESP_LOGI(TAG, "%s DETACHED: port=%p", role, detached_event ? (void *)detached_event->port : NULL); + break; + } + case USB_TCPM_EVENT_ERROR: + default: + ESP_LOGW(TAG, "EVENT %d", (int)id); + break; + } +} + +void test_pd_fusb302_setup(void) +{ + const esp_err_t loop_err = esp_event_loop_create_default(); + TEST_ASSERT_TRUE_MESSAGE(loop_err == ESP_OK || loop_err == ESP_ERR_INVALID_STATE, + "Failed to create default event loop"); + s_default_loop_created = (loop_err == ESP_OK); + + const usb_tcpm_install_config_t install_cfg = { + .task_stack = 4096, + .task_prio = 5, + }; + TEST_ASSERT_EQUAL(ESP_OK, usb_tcpm_install(&install_cfg)); + s_usb_tcpm_installed = true; + + const i2c_master_bus_config_t bus_cfg = { + .i2c_port = 0, /* use I2C0 */ + .sda_io_num = I2C_SDA, + .scl_io_num = I2C_SCL, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags = { .enable_internal_pullup = 1 }, + }; + TEST_ASSERT_EQUAL(ESP_OK, i2c_new_master_bus(&bus_cfg, &s_i2c_bus)); + + const usb_tcpm_port_config_t port_cfg = { + .default_power_role = USB_TCPM_PWR_SINK, + .rp_current = USB_TCPM_RP_1A5, + .src_vbus_gpio = VBUS_GPIO_PH, + .src_vbus_gpio_n = VBUS_GPIO_N, + }; + + const usb_tcpm_fusb302_config_t hw = { + .i2c_bus = s_i2c_bus, + .i2c_addr_7b = USB_TCPM_FUSB302_I2C_ADDR_7B_010, + .gpio_int = INT_GPIO, + }; + + TEST_ASSERT_EQUAL(ESP_OK, esp_event_handler_register(USB_TCPM_EVENT, + ESP_EVENT_ANY_ID, + &on_usb_tcpm_event, + NULL)); + s_event_handler_registered = true; + + TEST_ASSERT_EQUAL(ESP_OK, usb_tcpm_port_create_fusb302(&port_cfg, &hw, &s_port)); +} + +void test_pd_fusb302_teardown(void) +{ + if (s_port != NULL) { + TEST_ASSERT_EQUAL(ESP_OK, usb_tcpm_port_destroy(s_port)); + s_port = NULL; + } + + if (s_event_handler_registered) { + TEST_ASSERT_EQUAL(ESP_OK, esp_event_handler_unregister(USB_TCPM_EVENT, + ESP_EVENT_ANY_ID, + &on_usb_tcpm_event)); + s_event_handler_registered = false; + } + + if (s_i2c_bus != NULL) { + TEST_ASSERT_EQUAL(ESP_OK, i2c_del_master_bus(s_i2c_bus)); + s_i2c_bus = NULL; + } + + if (s_usb_tcpm_installed) { + TEST_ASSERT_EQUAL(ESP_OK, usb_tcpm_uninstall()); + s_usb_tcpm_installed = false; + } + + TEST_ASSERT_EQUAL(ESP_OK, gpio_uninstall_isr_service()); + + if (s_default_loop_created) { + TEST_ASSERT_EQUAL(ESP_OK, esp_event_loop_delete_default()); + s_default_loop_created = false; + } + + vTaskDelay(pdMS_TO_TICKS(200)); +} + +TEST_CASE("memory_leakage", "[type_c]") +{ + ESP_LOGI(TAG, "Waiting for attach/detach events..."); + + vTaskDelay(pdMS_TO_TICKS(200)); +} diff --git a/type_c/usb_tcpm/test_apps/pd_fusb302/sdkconfig.defaults b/type_c/usb_tcpm/test_apps/pd_fusb302/sdkconfig.defaults new file mode 100644 index 00000000..3e56f21a --- /dev/null +++ b/type_c/usb_tcpm/test_apps/pd_fusb302/sdkconfig.defaults @@ -0,0 +1,22 @@ +# Disable watchdogs, they'd get triggered during unity interactive menu +# CONFIG_ESP_TASK_WDT_INIT is not set + +# Run-time checks of Heap and Stack +CONFIG_HEAP_POISONING_COMPREHENSIVE=y +CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y + +CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL=y + +# Use USB Serial/JTAG for console I/O (Unity menu input/output) +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +CONFIG_ESP_CONSOLE_SECONDARY_NONE=y +# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set +# CONFIG_ESP_CONSOLE_UART_CUSTOM is not set +# CONFIG_ESP_CONSOLE_NONE is not set + +# Keep mbedTLS crypto features aligned with current Kconfig defaults. +# CONFIG_MBEDTLS_ARIA_C is not set +# CONFIG_MBEDTLS_SHA3_C is not set + +# Keep target/flash defaults aligned with current Kconfig defaults. +CONFIG_ESPTOOLPY_FLASHFREQ_40M=y diff --git a/type_c/usb_tcpm/test_apps/pd_fusb302/sdkconfig.defaults.esp32p4 b/type_c/usb_tcpm/test_apps/pd_fusb302/sdkconfig.defaults.esp32p4 new file mode 100644 index 00000000..2dbcf836 --- /dev/null +++ b/type_c/usb_tcpm/test_apps/pd_fusb302/sdkconfig.defaults.esp32p4 @@ -0,0 +1 @@ +CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y