diff --git a/Kconfig b/Kconfig index 1c2a78144c0..6dd1ae87fae 100644 --- a/Kconfig +++ b/Kconfig @@ -1031,3 +1031,4 @@ mainmenu "Espressif IoT Development Framework Configuration" - CONFIG_USB_HOST_EXT_PORT_RESET_ATTEMPTS - CONFIG_GDMA_ENABLE_WEIGHTED_ARBITRATION - CONFIG_COMPILER_KASAN + - CONFIG_ETH_SUBLAYER_SUPPORT diff --git a/components/esp_eth/CMakeLists.txt b/components/esp_eth/CMakeLists.txt index 4eb4d730baf..cffb31fa72f 100644 --- a/components/esp_eth/CMakeLists.txt +++ b/components/esp_eth/CMakeLists.txt @@ -6,6 +6,7 @@ endif() set(srcs) set(include) +set(priv_include_dirs) set(ld_fragments linker.lf) # As CONFIG_ETH_ENABLED comes from Kconfig, it is not evaluated yet # when components are being registered. @@ -22,6 +23,14 @@ if(CONFIG_ETH_ENABLED) idf_build_get_property(components_to_build BUILD_COMPONENTS) if(esp_netif IN_LIST components_to_build) list(APPEND srcs "src/esp_eth_netif_glue.c") + if(CONFIG_ETH_SUBLAYER_SUPPORT) + list(APPEND srcs "src/sublayer/esp_eth_sublayer.c" + "src/sublayer/esp_eth_sublayer_vlan_child.c") + if(CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT) + list(APPEND srcs "src/sublayer/esp_eth_sublayer_switch.c") + endif() + list(APPEND priv_include_dirs "src/sublayer") + endif() endif() endif() @@ -30,6 +39,7 @@ if(CONFIG_ETH_ENABLED) "src/mac/esp_eth_mac_esp_dma.c" "src/mac/esp_eth_mac_esp_gpio.c" "src/phy/esp_eth_phy_generic.c") + list(APPEND priv_include_dirs "src/mac") if(CONFIG_SOC_PAU_SUPPORTED) list(APPEND srcs "src/mac/${target}/emac_retention.c") endif() @@ -47,7 +57,7 @@ endif() idf_component_register(SRCS "${srcs}" INCLUDE_DIRS ${include} - PRIV_INCLUDE_DIRS "src/mac" + PRIV_INCLUDE_DIRS ${priv_include_dirs} LDFRAGMENTS ${ld_fragments} REQUIRES esp_event # For using "ESP_EVENT_DECLARE_BASE" in header file esp_hal_emac @@ -55,7 +65,18 @@ idf_component_register(SRCS "${srcs}" if(CONFIG_ETH_ENABLED) if(IDF_BUILD_V2) - target_sources(${COMPONENT_TARGET} PRIVATE "$<$:src/esp_eth_netif_glue.c>") + target_sources(${COMPONENT_TARGET} PRIVATE + "$<$:src/esp_eth_netif_glue.c>") + if(CONFIG_ETH_SUBLAYER_SUPPORT) + target_sources(${COMPONENT_TARGET} PRIVATE + "$<$:src/sublayer/esp_eth_sublayer.c>" + "$<$:src/sublayer/esp_eth_sublayer_vlan_child.c>") + if(CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT) + target_sources(${COMPONENT_TARGET} PRIVATE + "$<$:src/sublayer/esp_eth_sublayer_switch.c>") + endif() + target_include_directories(${COMPONENT_TARGET} PRIVATE "src/sublayer") + endif() endif() if(CONFIG_ETH_USE_SPI_ETHERNET) idf_component_optional_requires(PUBLIC esp_driver_spi) diff --git a/components/esp_eth/Kconfig b/components/esp_eth/Kconfig index 4a000d0512a..e4e064a197e 100644 --- a/components/esp_eth/Kconfig +++ b/components/esp_eth/Kconfig @@ -174,8 +174,103 @@ menu "Ethernet" config ETH_TRANSMIT_MUTEX depends on ETH_ENABLED bool "Enable Transmit Mutex" + default y if ESP_NETIF_L2_TAP default n help Prevents multiple accesses when Ethernet interface is used as shared resource and multiple functionalities might try to access it at a time. + + config ETH_SUBLAYER_SUPPORT + bool "Support Ethernet netif sublayer (EXPERIMENTAL)" + depends on ETH_ENABLED && IDF_EXPERIMENTAL_FEATURES + default n + help + Enable the experimental Ethernet sublayer API (esp_eth_sublayer.h). + + The sublayer is the coupling point between one physical Ethernet driver and one or more + esp_netif instances, with optional 802.1Q VLAN demux/mux, integrated switch, and L2TAP integration. + + This API is experimental and may change in future ESP-IDF releases. + + if ETH_SUBLAYER_SUPPORT + + config ETH_SUBLAYER_IODRIVER_PROVIDER + bool "Expose the sublayer as an IO driver provider" + default y if ESP_NETIF_L2_TAP + default n + help + Build the sublayer's IO driver provider implementation : a generic virtual table + (transmit / transmit_wrap / free_rx_buffer / get_ll_driver) that resolves a + base, VLAN child, or integrated switch port into its transmit/free functions. + + This is required for L2 TAP integration and is enabled automatically when + ESP_NETIF_L2_TAP is selected. It can also be enabled independently so that other upper + layers can obtain IO functions for a specific sublayer endpoint. + + config ETH_SUBLAYER_TRANSMIT_MUTEX + bool "Enable Ethernet sublayer Transmit Mutex" + default y if ETH_SUBLAYER_IODRIVER_PROVIDER + default n + help + Serializes the whole Ethernet sublayer TX path (TX hook, integrated switch mux, and the + actual driver transmit call) per sublayer instance with a dedicated mutex. + + This is needed whenever more than one task can end up transmitting through the same + sublayer concurrently (e.g. multiple L2TAP sockets bound to different switch/VLAN ports, + each potentially owned by a different task), since TX hooks and integrated switch drivers + may keep small amounts of mutable state (e.g. tag/padding scratch buffers) that are only + safe to access from one task at a time. + + This option is independent of ETH_TRANSMIT_MUTEX: enabling one does not disable the other. + If every esp_eth_handle_t used by the application is exclusively driven through a + sublayer, ETH_TRANSMIT_MUTEX can usually be left disabled to avoid the extra locking, since + this option already serializes the complete TX path leading up to the driver transmit + call. However, if any Ethernet handle is also transmitted to directly (bypassing the + sublayer), ETH_TRANSMIT_MUTEX is still required for that handle - this option only + protects traffic that goes through eth_sublayer_transmit(). + + config ETH_SUBLAYER_VLAN_SUPPORT + bool "Enable 802.1Q VLAN support in the sublayer" + default n + help + Allow tagged 802.1Q VLAN netifs to be created on top of the sublayer, in addition + to the plain untagged one. + + Disabling this option restricts the sublayer to a single, untagged netif per + Ethernet driver (or per integrated switch port), reducing code size and RAM usage. + + config ETH_SUBLAYER_SWITCH_SUPPORT + bool "Enable integrated Ethernet switch support in the sublayer" + default n + help + Enable support for attaching an integrated Ethernet switch to the sublayer, + including switch-specific frame mux/demux and per-port IO driver handling. + + This is intended for Ethernet switch drivers that require host-interface frame + tagging. Leave this option disabled to reduce code size and RAM usage when the + sublayer is used without a switch. + + config ETH_SUBLAYER_TX_BUF_DESC_CAP_RANGE_MIN + int + default 3 if ETH_SUBLAYER_VLAN_SUPPORT && ETH_SUBLAYER_SWITCH_SUPPORT + default 2 if ETH_SUBLAYER_VLAN_SUPPORT || ETH_SUBLAYER_SWITCH_SUPPORT + default 1 + + config ETH_SUBLAYER_TX_BUF_DESC_CAPACITY + int "Sublayer TX buffer descriptor capacity" + range ETH_SUBLAYER_TX_BUF_DESC_CAP_RANGE_MIN 16 + default ETH_SUBLAYER_TX_BUF_DESC_CAP_RANGE_MIN + help + Maximum number of esp_eth_buf_desc_t entries available to the + Ethernet sublayer TX path (including TX hook edits). + + If TX hook needs to add more frame segments without memcpying the entire frame, + it can do so by adding more entries to the descriptor array. + + The descriptor array is stack-allocated on hot TX paths, so larger + values increase stack usage but allow hooks to add more frame + segments without allocating a new descriptor array. + + endif # ETH_SUBLAYER_SUPPORT + endmenu diff --git a/components/esp_eth/include/esp_eth.h b/components/esp_eth/include/esp_eth.h index d37353550b5..d67ea078194 100644 --- a/components/esp_eth/include/esp_eth.h +++ b/components/esp_eth/include/esp_eth.h @@ -1,16 +1,21 @@ /* - * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #pragma once +#include "sdkconfig.h" + /** * Purpose of this file is to create a common header for the typical ethernet usecase, - * so using the ethernet driver and the default glue layer to its network interface. + * so using the ethernet driver and the default glue or sublayer layer to its network interface. * * If you prefer to create a custom network interface or use the Ethernet as a driver only, * then it is recommended to include the "esp_eth_driver.h" only. */ #include "esp_eth_driver.h" #include "esp_eth_netif_glue.h" +#if CONFIG_ETH_SUBLAYER_SUPPORT +#include "esp_eth_sublayer.h" +#endif diff --git a/components/esp_eth/include/esp_eth_com.h b/components/esp_eth/include/esp_eth_com.h index 5435fddb32b..d9785eb1af7 100644 --- a/components/esp_eth/include/esp_eth_com.h +++ b/components/esp_eth/include/esp_eth_com.h @@ -1,10 +1,12 @@ /* - * SPDX-FileCopyrightText: 2019-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2019-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #pragma once +#include +#include #include "esp_err.h" #include "esp_event_base.h" #include "hal/eth_types.h" @@ -14,6 +16,14 @@ extern "C" { #endif +/** + * @brief Scatter-gather buffer descriptor for extended Ethernet transmit. + */ +typedef struct { + uint8_t *buf; /*!< Pointer to the buffer segment */ + size_t len; /*!< Length of the buffer segment in bytes */ +} esp_eth_buf_desc_t; + /** * @brief Offset for start of MAC custom ioctl commands * diff --git a/components/esp_eth/include/esp_eth_driver.h b/components/esp_eth/include/esp_eth_driver.h index b5d51bf3b66..a51bf1e1da9 100644 --- a/components/esp_eth/include/esp_eth_driver.h +++ b/components/esp_eth/include/esp_eth_driver.h @@ -324,6 +324,8 @@ esp_err_t esp_eth_transmit(esp_eth_handle_t hdl, void *buf, size_t length); /** * @brief Extended Transmit with variable number of arguments * + * @deprecated Use esp_eth_transmit_ctrl_bufs() instead. + * * @note Typical intended use case of this function is to assemble Ethernet frame from multiple input buffers * at lower layer of the driver (MAC layer) to avoid unnecessary buffer reallocation and copy. * @@ -337,12 +339,13 @@ esp_err_t esp_eth_transmit(esp_eth_handle_t hdl, void *buf, size_t length); * - ESP_ERR_TIMEOUT: transmit frame buffer failed because HW was not get available in predefined period * - ESP_FAIL: transmit frame buffer failed because some other error occurred */ -esp_err_t esp_eth_transmit_ctrl_vargs(esp_eth_handle_t hdl, void *ctrl, uint32_t argc, ...); +esp_err_t esp_eth_transmit_ctrl_vargs(esp_eth_handle_t hdl, void *ctrl, uint32_t argc, ...) +__attribute__((deprecated("Use esp_eth_transmit_ctrl_bufs() instead"))); /** * @brief Wrapper over Extended Transmit function to ensure backward compatibility. * -* @note For new implementations, it is recommended to use `esp_eth_transmit_ctrl_vargs()` directly. +* @note For new implementations, it is recommended to use `esp_eth_transmit_ctrl_bufs()` directly. * * @param[in] eth_hdl handle of Ethernet driver * @param[in] argc number variable arguments @@ -355,6 +358,25 @@ esp_err_t esp_eth_transmit_ctrl_vargs(esp_eth_handle_t hdl, void *ctrl, uint32_t */ #define esp_eth_transmit_vargs(eth_hdl, argc, ...) esp_eth_transmit_ctrl_vargs(eth_hdl, NULL, (argc) * 2, ##__VA_ARGS__) +/** + * @brief Extended transmit from scatter-gather buffer descriptors. + * + * @note Typical intended use case is to assemble an Ethernet frame from multiple + * input buffers at the MAC layer to avoid unnecessary buffer reallocation + * and copy. + * + * @param hdl handle of Ethernet driver + * @param ctrl optional transmit control structure (MAC specific), set to NULL when not required + * @param bufs array of buffer descriptors comprising the frame + * @param buf_count number of valid entries in @a bufs + * @return + * - ESP_OK: transmit successful + * - ESP_ERR_INVALID_STATE: invalid driver state (e.i., driver is not started) + * - ESP_ERR_TIMEOUT: transmit frame buffer failed because HW was not get available in predefined period + * - ESP_FAIL: transmit frame buffer failed because some other error occurred + */ +esp_err_t esp_eth_transmit_ctrl_bufs(esp_eth_handle_t hdl, void *ctrl, const esp_eth_buf_desc_t *bufs, size_t buf_count); + /** * @brief Misc IO function of Ethernet driver * diff --git a/components/esp_eth/include/esp_eth_mac.h b/components/esp_eth/include/esp_eth_mac.h index 85084cfaf60..2b3acfd3ec8 100644 --- a/components/esp_eth/include/esp_eth_mac.h +++ b/components/esp_eth/include/esp_eth_mac.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2019-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2019-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -104,9 +104,32 @@ struct esp_eth_mac_s { */ esp_err_t (*transmit)(esp_eth_mac_t *mac, uint8_t *buf, uint32_t length); + /** + * @brief Transmit packet with extended control from scatter-gather buffer descriptors. + * + * @param[in] mac: Ethernet MAC instance + * @param[in] ctrl: optional transmit control structure (chip specific), set to NULL when not required + * @param[in] bufs: array of buffer descriptors comprising the frame + * @param[in] buf_count: number of valid entries in @a bufs + * + * @note Typical intended use case is to make possible to construct a frame from multiple higher layer + * buffers without a need of buffer reallocations. + * + * @return + * - ESP_OK: transmit packet successfully + * - ESP_ERR_INVALID_SIZE: number of actually sent bytes differs to expected + * - ESP_FAIL: transmit packet failed because some other error occurred + * + * @note Returned error codes may differ for each specific MAC chip. + * + */ + esp_err_t (*transmit_ctrl_bufs)(esp_eth_mac_t *mac, void *ctrl, const esp_eth_buf_desc_t *bufs, size_t buf_count); + /** * @brief Transmit packet with extended control from Ethernet MAC and constructed with special parameters at Layer2. * + * @warning Deprecated, use `transmit_ctrl_bufs()` instead. + * * @param[in] mac: Ethernet MAC instance * @param[in] ctrl: optional transmit control structure (chip specific), set to NULL when not required * @param[in] argc: number variable arguments @@ -123,7 +146,7 @@ struct esp_eth_mac_s { * @note Returned error codes may differ for each specific MAC chip. * */ - esp_err_t (*transmit_ctrl_vargs)(esp_eth_mac_t *mac, void *ctrl, uint32_t argc, va_list args); + esp_err_t (*transmit_ctrl_vargs)(esp_eth_mac_t *mac, void *ctrl, uint32_t argc, va_list args) __attribute__((deprecated("Use transmit_ctrl_bufs instead"))); /** * @brief Transmit packet from Ethernet MAC constructed with special parameters at Layer2. @@ -145,7 +168,7 @@ struct esp_eth_mac_s { * @note Returned error codes may differ for each specific MAC chip. * */ - esp_err_t (*transmit_vargs)(esp_eth_mac_t *mac, uint32_t argc, va_list args) __attribute__((deprecated("Use transmit_ctrl_vargs instead"))); + esp_err_t (*transmit_vargs)(esp_eth_mac_t *mac, uint32_t argc, va_list args) __attribute__((deprecated("Use transmit_ctrl_bufs instead"))); /** * @brief Receive packet from Ethernet MAC diff --git a/components/esp_eth/include/esp_eth_sublayer.h b/components/esp_eth/include/esp_eth_sublayer.h new file mode 100644 index 00000000000..a53688912fe --- /dev/null +++ b/components/esp_eth/include/esp_eth_sublayer.h @@ -0,0 +1,360 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "sdkconfig.h" + +#if CONFIG_ETH_SUBLAYER_SUPPORT + +#include +#include +#include "esp_eth_driver.h" +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct esp_eth_sublayer_s *esp_eth_sublayer_handle_t; +typedef struct esp_eth_sublayer_vlan_s *esp_eth_sublayer_vlan_handle_t; +#if CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT +typedef struct esp_eth_sublayer_switch_s *esp_eth_sublayer_switch_handle_t; +#endif + +#define ESP_ETH_SUBLAYER_UNTAGGED_VID UINT16_MAX +#define ESP_ETH_SUBLAYER_TX_BUF_DESC_CAPACITY CONFIG_ETH_SUBLAYER_TX_BUF_DESC_CAPACITY + +/** + * @brief Mutable TX buffer descriptor set passed through the sublayer TX path. + */ +typedef struct { + esp_eth_buf_desc_t *bufs; /*!< Descriptor array */ + size_t *buf_count; /*!< In/out: number of valid entries in @c bufs */ + const size_t buf_capacity; /*!< Maximum number of entries in @c bufs */ +} esp_eth_sublayer_tx_bufs_t; + +/** + * @brief Event info structure for sublayer optional events. + */ +typedef struct +{ + esp_event_base_t base; + int32_t event_id; + void *event_data; +} esp_eth_sublayer_event_t; + +/** + * @brief Extra information for RX hook callbacks. + */ +typedef struct { + void *l2_buffer; /*!< Starting address of the original Ethernet driverRX allocation */ + void *driver_info; /*!< Opaque per-frame metadata from MAC / stack_input_info, may be NULL */ +} esp_eth_sublayer_rx_info_t; + +/** + * @brief Optional TX hook on frames passed to the Ethernet driver (after VLAN tag insert for VLAN netifs). + * + * When a VLAN tag is inserted the sublayer splits the frame into two + * esp_eth_buf_desc_t entries (tagged header + payload) so that no payload + * memcpy is needed. For untagged frames a single descriptor is passed. + * + * The hook may modify the descriptor array in place: + * - Change @c buf pointers, @c len values, and descriptor order (e.g. insert a + * prefix in @c bufs[0] and move the original frame to @c bufs[1], encrypt into + * a new buffer, or merge scattered segments into one contiguous @c bufs[0]). + * Use @c post_tx_hook to free any buffers allocated for these edits after the + * driver finishes transmitting. + * - Adjust *@a tx_bufs->buf_count up or down, but never exceed @a tx_bufs->buf_capacity. + * - Must NOT free the input buffers. + * - May set *@a tx_bufs->buf_count to 0 to skip driver transmit and @c post_tx_hook + * (e.g. the hook handled the frame elsewhere or chose to drop it). The @a tx_bufs + * descriptor set remains caller-owned; only transmission is suppressed. + * + * @param eth Underlying Ethernet driver handle + * @param tx_bufs Mutable buffer descriptor set comprising the frame + * @param ctx User context from esp_eth_sublayer_config_t::hook_ctx + * @return ESP_OK to transmit; other values abort transmit for this packet + */ +typedef esp_err_t (*esp_eth_sublayer_tx_hook_t)(esp_eth_handle_t eth, + esp_eth_sublayer_tx_bufs_t *tx_bufs, + void *ctx); + +/** + * @brief Optional post-TX hook called after a successful TX hook, to release resources it allocated. + * + * @warning The Tx buffer descriptors may have different order than on the output of the TX hook. + * + * Intended for freeing buffers that were allocated by the TX hook. + * Called whenever the TX hook completed with @a *tx_bufs->buf_count > 0, regardless of the transmit result + * (or of any failure that occurs after the TX hook and before the actual transmit). This guarantees that + * buffers allocated by the TX hook are always released. + * + * Not called when the TX hook sets @a *tx_bufs->buf_count to 0 (transmit suppressed) or returns non-ESP_OK. + * + * @param eth Underlying Ethernet driver handle + * @param tx_bufs Buffer descriptor set that was passed to (or prepared for) the driver + * @param ctx User context from esp_eth_sublayer_config_t::hook_ctx + */ +typedef void (*esp_eth_sublayer_post_tx_hook_t)(esp_eth_handle_t eth, + const esp_eth_sublayer_tx_bufs_t *tx_bufs, + void *ctx); + +/** + * @brief Optional RX hook on frames from the Ethernet driver (before VLAN demux / input path). + * + * The hook may strip a leading protocol header (e.g. MACsec SecTAG) by advancing @p buf forward and + * decrementing @p len, or remove a trailing trailer by decrementing @p len only. + * @p buf must remain within the original allocation. + * + * Set @c *len to 0 to take ownership of the original allocation and stop further sublayer processing. + * The hook must then release or forward @c info->alloc_base itself. + * + * @param eth Underlying Ethernet driver handle (when integrated switch is used, this is per port handle) + * @param[in,out] buf in: frame start; out: frame start with hook-specific header/trailer removed + * @param[in,out] len frame length; set to 0 to stop further processing + * @param info Per-frame RX metadata; never NULL + * @param ctx User context from esp_eth_sublayer_config_t::hook_ctx + * @return ESP_OK to continue processing; other values cause the sublayer to free the allocation and stop + */ +typedef esp_err_t (*esp_eth_sublayer_rx_hook_t)(esp_eth_handle_t eth, uint8_t **buf, uint32_t *len, esp_eth_sublayer_rx_info_t *info, void *ctx); + +/** + * @brief Sublayer configuration structure. + * + */ +typedef struct +{ + esp_eth_handle_t eth_handle; /*!< Underlying Ethernet driver handle the sublayer is bound to (required) */ + + /** + * Optional event that must be observed (in addition to the physical link being up) before the sublayer + * reports child netifs as connected. Leave @c base NULL (the default) to bring netifs up as soon as + * ETHERNET_EVENT_CONNECTED fires, with no extra confirmation needed. + * + * The order in which this event and ETHERNET_EVENT_CONNECTED are received is not significant, the child + * netifs are brought up once both conditions are met. Any subsequent disconnect (ETHERNET_EVENT_DISCONNECTED, + * ETHERNET_EVENT_STOP or @c disconnect_trigger_event) invalidates the confirmation, i.e. the event needs to + * be posted again to bring the child netifs up. + */ + esp_eth_sublayer_event_t connect_confirm_event; + + /** + * Optional event that, in addition to ETHERNET_EVENT_DISCONNECTED, forces all child netifs down + * (e.g. an upper-layer authentication/link-partner event). Leave @c base NULL (the default) to only + * react to ETHERNET_EVENT_DISCONNECTED. + */ + esp_eth_sublayer_event_t disconnect_trigger_event; + + esp_eth_sublayer_tx_hook_t tx_hook; /*!< Optional TX hook, see esp_eth_sublayer_tx_hook_t (NULL to disable) */ + esp_eth_sublayer_post_tx_hook_t post_tx_hook; /*!< Optional post-TX hook, see esp_eth_sublayer_post_tx_hook_t (NULL to disable) */ + esp_eth_sublayer_rx_hook_t rx_hook; /*!< Optional RX hook, see esp_eth_sublayer_rx_hook_t (NULL to disable) */ + void *hook_ctx; /*!< User context passed to tx_hook, post_tx_hook, and rx_hook */ + + /** + * Timeout (ms) to acquire the CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX before failing with ESP_ERR_TIMEOUT. + * 0 uses the built-in default. Ignored when CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX is disabled. + */ + uint32_t transmit_mutex_timeout_ms; +} esp_eth_sublayer_config_t; + +#if CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT +/** + * @brief Integrated switch "tag process init" callback. + * + * Invoked once by the sublayer when it is created. The driver allocates and returns (through @p ctx) all the + * state it needs to mux/demux Tail Tagged traffic between the single Host Ethernet interface and the per-port + * Ethernet interfaces. The sublayer owns the returned @p ctx and passes it back to the demux and deinit callbacks. + * + * @param[in] host_eth_handle Host Ethernet driver handle (the one the sublayer is bound to) + * @param[in] port_eth_handles array of per-port Ethernet driver handles + * @param[in] ports_count number of entries in @p port_eth_handles + * @param[out] ctx location to store the driver's integrated switch context + * @return ESP_OK on success, error code otherwise + */ + typedef esp_err_t (*esp_eth_sublayer_switch_tag_init_t)(esp_eth_handle_t host_eth_handle, + esp_eth_handle_t *port_eth_handles, + uint32_t ports_count, + void **ctx); + +/** +* @brief Integrated switch "tag process deinit" callback. Releases the context allocated by the init callback. +* +* @param[in] ctx driver integrated switch context returned by the init callback +* @return ESP_OK on success, error code otherwise +*/ +typedef esp_err_t (*esp_eth_sublayer_switch_tag_deinit_t)(void *ctx); + +/** +* @brief Integrated switch RX demux callback. +* +* Called by the sublayer at the very beginning of the RX path (before RX hook / L2 TAP / VLAN demux). The driver +* strips its switch tag and reports the ingress port number through @p src_port. The frame is NOT re-routed by +* this callback; the sublayer continues its normal RX pipeline on the now tag-free frame. +* +* Tag stripping strategies: +* - Trailing tag (e.g. KSZ8863 Tail Tag): decrement @p length. +* - Leading tag after the MAC header (e.g. DSA, 802.1Q port-VLAN): advance @p buffer forward and decrement +* @p length (typically after memmove of the 12-byte MAC header over the tag). +* +* The callback must NOT free or realloc the buffer; on error the sublayer frees the original allocation. +* @p buffer must remain within the original allocation. +* +* @param[in] ctx driver integrated switch context returned by the init callback +* @param[in,out] buffer in: frame start including the switch tag; out: frame start with tag removed +* @param[in,out] length frame length including the tag on input, with the tag removed on output +* @param[out] src_port ingress port number (callback may ignore if NULL) +* @return ESP_OK on success, error code otherwise (frame will be dropped by the sublayer) +*/ +typedef esp_err_t (*esp_eth_sublayer_switch_demux_t)(void *ctx, uint8_t **buffer, uint32_t *length, int32_t *src_port); + +/** +* @brief Integrated switch TX mux callback. +* +* Called by the sublayer during transmit to append switch-specific tagging (e.g. KSZ8863 Tail Tag) to +* @a tx_bufs before the frame is sent on the Host Ethernet interface. +* +* @param[in] ctx driver integrated switch context returned by the init callback +* @param[in,out] tx_bufs buffer descriptor set comprising the frame +* @param port destination port number, or negative for default switch lookup behaviour +* @return ESP_OK on success, error code otherwise +*/ +typedef esp_err_t (*esp_eth_sublayer_switch_mux_t)(void *ctx, esp_eth_sublayer_tx_bufs_t *tx_bufs, int32_t port); + +/** + * @brief Integrated switch configuration. + * + */ +typedef struct +{ + esp_eth_sublayer_switch_tag_init_t tag_process_init; /*!< Called when the switch is added; returns @c ctx */ + esp_eth_sublayer_switch_tag_deinit_t tag_process_deinit; /*!< Called when the switch is deleted */ + esp_eth_sublayer_switch_demux_t demux; /*!< Called per RX frame to strip the tag / resolve the ingress port */ + esp_eth_sublayer_switch_mux_t mux; /*!< Called per TX frame to append the switch tag */ + esp_eth_handle_t host_eth_handle; + esp_eth_handle_t *port_eth_handles; + uint32_t ports_count; +} esp_eth_sublayer_switch_config_t; +#endif // CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT + +/** + * @brief Default sublayer configuration. + * + */ +#define ESP_ETH_SUBLAYER_CONFIG_DEFAULT() \ + { \ + .eth_handle = NULL, \ + .connect_confirm_event = { NULL, 0, NULL }, \ + .disconnect_trigger_event = { NULL, 0, NULL }, \ + .tx_hook = NULL, \ + .post_tx_hook = NULL, \ + .rx_hook = NULL, \ + .hook_ctx = NULL, \ + .transmit_mutex_timeout_ms = 0, \ + } + +/** + * @brief Create a Ethernet sublayer for an Ethernet driver. + * + * @note The interface base sublayer is required to be created. Derived child + * interfaces can be used and attached to netifs (e.g. VLAN interfaces). + * + * @warning Management of the Ethernet-Netif sublayer is not thread-safe for performance reasons. + * Configure it only from a single task/context (for example during initialization). + * + * @param config Sublayer configuration + * @param[out] sublayer Location to store the created sublayer handle + * @return ESP_OK on success + */ +esp_err_t esp_eth_sublayer_new(const esp_eth_sublayer_config_t *config, esp_eth_sublayer_handle_t *sublayer); + +/** + * @brief Delete Ethernet sublayer and all child entries (VLANs). + * + * Caller must detach and destroy esp_netif instances before calling this. + * + * @param sublayer Sublayer handle + * @return ESP_OK on success + */ +esp_err_t esp_eth_sublayer_del(esp_eth_sublayer_handle_t sublayer); + +/** + * @brief Add a VLAN entry to the sublayer (802.1Q VID 0..4095 excluding duplicates). + * + * Attach the VLAN netif with esp_netif_attach(esp_netif, returned handle). + * + * @warning Management of the Ethernet-Netif sublayer is not thread-safe for performance reasons. + * Configure it only from a single task/context (for example during initialization). + * + * @param sublayer Sublayer handle + * @param vlan_id VLAN ID (12-bit), or ESP_ETH_SUBLAYER_UNTAGGED_VID for the untagged child + * @param[out] vlan Location to store the created VLAN handle + * @return ESP_OK on success; ESP_ERR_INVALID_STATE if the VLAN ID is already present; + * ESP_ERR_NOT_SUPPORTED if @p vlan_id is not ESP_ETH_SUBLAYER_UNTAGGED_VID and + * CONFIG_ETH_SUBLAYER_VLAN_SUPPORT is disabled + */ +esp_err_t esp_eth_sublayer_vlan_add(esp_eth_sublayer_handle_t sublayer, uint16_t vlan_id, + esp_eth_sublayer_vlan_handle_t *vlan); + +/** + * @brief Remove a VLAN entry from the sublayer. + * + * @param sublayer Sublayer handle + * @param vlan VLAN handle returned by esp_eth_sublayer_vlan_add + * @return ESP_ERR_NOT_FOUND if not on this sublayer; ESP_OK otherwise + */ +esp_err_t esp_eth_sublayer_vlan_del(esp_eth_sublayer_handle_t sublayer, esp_eth_sublayer_vlan_handle_t vlan); + +#if CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT +/** + * @brief Add the integrated switch (Tail Tag) processing to the sublayer. + * + * A sublayer can hold at most one switch instance. The switch is attached to the sublayer similarly to a VLAN + * child but it is a single instance rather than a list. The provided @p config callbacks (`tag_process_init`, + * `demux`, `tag_process_deinit`) are implemented by the integrated switch driver (e.g. KSZ8863). `tag_process_init` + * is invoked here to create the driver's switch context. + * + * @warning Management of the Ethernet-Netif sublayer is not thread-safe for performance reasons. + * Configure it only from a single task/context (for example during initialization). + * + * @param sublayer Sublayer handle + * @param config Integrated switch configuration (its own configuration, not part of the sublayer config) + * @param[out] sw Location to store the created switch handle + * @return ESP_OK on success; ESP_ERR_INVALID_STATE if a switch is already added + */ +esp_err_t esp_eth_sublayer_switch_add(esp_eth_sublayer_handle_t sublayer, + const esp_eth_sublayer_switch_config_t *config, + esp_eth_sublayer_switch_handle_t *sw); + +/** + * @brief Remove the integrated switch from the sublayer. + * + * @param sw Switch handle returned by esp_eth_sublayer_switch_add + * @return ESP_OK on success + */ +esp_err_t esp_eth_sublayer_switch_del(esp_eth_sublayer_switch_handle_t sw); +#endif // CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT + +/** + * @brief Get the underlying Ethernet driver handle from the sublayer. + * + * @param sublayer Sublayer handle + * @return Ethernet driver handle + */ +esp_eth_handle_t esp_eth_sublayer_get_eth_handle(esp_eth_sublayer_handle_t sublayer); + +/** + * @brief Get the underlying Ethernet driver handle from the VLAN child of the sublayer. + * + * @param vlan VLAN handle + * @return Ethernet driver handle + */ +esp_eth_handle_t esp_eth_sublayer_vlan_get_eth_handle(esp_eth_sublayer_vlan_handle_t vlan); + +#ifdef __cplusplus +} +#endif + +#endif // CONFIG_ETH_SUBLAYER_SUPPORT diff --git a/components/esp_eth/include/esp_private/esp_eth_sublayer_iodriver.h b/components/esp_eth/include/esp_private/esp_eth_sublayer_iodriver.h new file mode 100644 index 00000000000..efa33a1e405 --- /dev/null +++ b/components/esp_eth/include/esp_private/esp_eth_sublayer_iodriver.h @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +/* + * @file esp_private/esp_eth_sublayer_iodriver.h + * + * @brief Internal (inter-component) IO driver access contract. + * + * This header is NOT part of the stable public application API. It defines a generic virtual table used + * between an IO driver provider (e.g. the Ethernet sublayer) and a consumer (e.g. L2 TAP, or any other + * upper layer). + */ + +#include +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle identifying a single logical IO driver endpoint. + * + * An IO driver endpoint is a point at which L2 frames can be transmitted and received (e.g. an Ethernet + * driver, a VLAN child, or an integrated switch port of the Ethernet sublayer). The concrete meaning of the + * handle is defined by the provider that resolves it (see @ref esp_eth_iodriver_provider_base_t). + */ +typedef void *esp_eth_iodriver_handle; + +/** + * @brief Opaque handle identifying an IO driver provider. + * + * A provider knows how to resolve one or more @ref esp_eth_iodriver_handle values into a set of IO functions + * (@ref esp_eth_iodriver_io_fns_t). The Ethernet sublayer is one such provider. + */ +typedef void *esp_eth_iodriver_provider_handle; + +/** + * @brief Set of IO functions for a single IO driver endpoint. + * + * This is a lightweight virtual table that decouples a consumer (e.g. L2 TAP, or any other upper layer) from + * the concrete implementation of an IO driver endpoint. A provider fills this structure for a given + * @ref esp_eth_iodriver_handle; the consumer then calls the function pointers passing back @c io_handle. + * + * @note This concept is intentionally independent of any particular consumer so it can be reused by layers + * other than L2 TAP. + */ +typedef struct { + esp_eth_iodriver_handle io_handle; /*!< Handle passed back to the function pointers below */ + esp_err_t (*iodriver_transmit)(esp_eth_iodriver_handle io_handle, void *buf, size_t len); /*!< Transmit a single buffer */ + esp_err_t (*iodriver_transmit_wrap)(esp_eth_iodriver_handle io_handle, void *buf, size_t len, void *eb); /*!< Transmit with a driver control/extended buffer (e.g. metadata) */ + void (*iodriver_free_rx_buffer)(esp_eth_iodriver_handle io_handle, void *buffer); /*!< Free an RX buffer previously received on this endpoint */ + esp_err_t (*iodriver_get_ll_driver)(esp_eth_iodriver_handle io_handle, void **ll_driver); /*!< Get the underlying low-level driver handle */ +} esp_eth_iodriver_io_fns_t; + +/** + * @brief Base interface implemented by an IO driver provider. + * + * A provider is registered with a consumer (e.g. L2 TAP) by embedding this structure as the first member of + * the provider object, so the provider handle can be cast to @c esp_eth_iodriver_provider_base_t *. + */ +typedef struct { + /** + * @brief Resolve an IO driver handle into its set of IO functions. + * + * @warning The consumer may call this callback with its internal lock held. The implementation + * must therefore be a simple lookup only: it must not log, allocate, block, or take any lock. + * + * @param[in] provider_handle Provider handle (the object embedding this base). + * @param[in] io_handle IO driver handle to resolve. + * @param[out] io_fns Filled with the IO functions for @p io_handle on success. + * @return + * - ESP_OK if @p io_handle is served by this provider and @p io_fns was filled + * - ESP_ERR_NOT_FOUND if this provider does not serve @p io_handle + * - ESP_ERR_INVALID_ARG on invalid arguments + */ + esp_err_t (*get_io_fns)(esp_eth_iodriver_provider_handle provider_handle, void *io_handle, esp_eth_iodriver_io_fns_t *io_fns); +} esp_eth_iodriver_provider_base_t; + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_eth/linker.lf b/components/esp_eth/linker.lf index 8dcdca709f0..f339962f4c3 100644 --- a/components/esp_eth/linker.lf +++ b/components/esp_eth/linker.lf @@ -3,8 +3,10 @@ archive: libesp_eth.a entries: if ETH_IRAM_OPTIMIZATION = y: esp_eth:esp_eth_transmit (noflash_text) + esp_eth:esp_eth_transmit_ctrl_bufs (noflash_text) esp_eth:esp_eth_transmit_ctrl_vargs (noflash_text) esp_eth_mac_esp:emac_esp32_transmit (noflash_text) + esp_eth_mac_esp:emac_esp32_transmit_ctrl_bufs (noflash_text) esp_eth_mac_esp:emac_esp32_transmit_ctrl_vargs (noflash_text) esp_eth_mac_esp:emac_esp32_receive (noflash_text) esp_eth_mac_esp:emac_esp32_rx_task (noflash_text) diff --git a/components/esp_eth/src/esp_eth.c b/components/esp_eth/src/esp_eth.c index 112a15109bc..a0ebd096329 100644 --- a/components/esp_eth/src/esp_eth.c +++ b/components/esp_eth/src/esp_eth.c @@ -6,6 +6,8 @@ #include #include +#include +#include #include "esp_log.h" #include "esp_check.h" #include "esp_eth_driver.h" @@ -403,7 +405,30 @@ err: return ret; } -esp_err_t esp_eth_transmit_ctrl_vargs(esp_eth_handle_t hdl, void *ctrl, uint32_t argc, ...) +static esp_err_t esp_eth_transmit_ctrl_bufs_fallback(esp_eth_mac_t *mac, const esp_eth_buf_desc_t *bufs, size_t buf_count) +{ + size_t total_len = 0; + for (size_t i = 0; i < buf_count; i++) { + total_len += bufs[i].len; + } + + uint8_t *single_buff = (uint8_t *)malloc(total_len); + if (single_buff == NULL) { + return ESP_ERR_NO_MEM; + } + + size_t offset = 0; + for (size_t i = 0; i < buf_count; i++) { + memcpy(single_buff + offset, bufs[i].buf, bufs[i].len); + offset += bufs[i].len; + } + + esp_err_t ret = mac->transmit(mac, single_buff, (uint32_t)total_len); + free(single_buff); + return ret; +} + +esp_err_t esp_eth_transmit_ctrl_bufs(esp_eth_handle_t hdl, void *ctrl, const esp_eth_buf_desc_t *bufs, size_t buf_count) { esp_err_t ret = ESP_OK; esp_eth_driver_t *eth_driver = (esp_eth_driver_t *)hdl; @@ -415,22 +440,46 @@ esp_err_t esp_eth_transmit_ctrl_vargs(esp_eth_handle_t hdl, void *ctrl, uint32_t } esp_eth_mac_t *mac = eth_driver->mac; + #if CONFIG_ETH_TRANSMIT_MUTEX if (xSemaphoreTake(eth_driver->transmit_mutex, pdMS_TO_TICKS(ESP_ETH_TX_TIMEOUT_MS)) == pdFALSE) { return ESP_ERR_TIMEOUT; } #endif // CONFIG_ETH_TRANSMIT_MUTEX - va_list args = {0}; - va_start(args, argc); - ret = mac->transmit_ctrl_vargs(mac, ctrl, argc, args); + + // if the underlying MAC does not support transmit_ctrl_bufs (multiple buffer zero-copy transmit), use the fallback + if (mac->transmit_ctrl_bufs) { + ret = mac->transmit_ctrl_bufs(mac, ctrl, bufs, buf_count); + } else { + ESP_LOGD(TAG, "Using fallback transmit function (transmit control and multiple buffer zero-copy not supported)"); + ret = esp_eth_transmit_ctrl_bufs_fallback(mac, bufs, buf_count); + } + #if CONFIG_ETH_TRANSMIT_MUTEX xSemaphoreGive(eth_driver->transmit_mutex); #endif // CONFIG_ETH_TRANSMIT_MUTEX - va_end(args); err: return ret; } +esp_err_t esp_eth_transmit_ctrl_vargs(esp_eth_handle_t hdl, void *ctrl, uint32_t argc, ...) +{ + esp_err_t ret = ESP_OK; + uint32_t buf_num = argc / 2; + esp_eth_buf_desc_t stack_bufs[buf_num]; + va_list args = {0}; + + va_start(args, argc); + for (uint32_t i = 0; i < buf_num; i++) { + stack_bufs[i].buf = va_arg(args, uint8_t *); + stack_bufs[i].len = va_arg(args, uint32_t); + } + va_end(args); + + ret = esp_eth_transmit_ctrl_bufs(hdl, ctrl, stack_bufs, buf_num); + return ret; +} + esp_err_t esp_eth_ioctl(esp_eth_handle_t hdl, esp_eth_io_cmd_t cmd, void *data) { esp_err_t ret = ESP_OK; diff --git a/components/esp_eth/src/esp_eth_netif_glue.c b/components/esp_eth/src/esp_eth_netif_glue.c index 9dec04bc104..8f5039203fe 100644 --- a/components/esp_eth/src/esp_eth_netif_glue.c +++ b/components/esp_eth/src/esp_eth_netif_glue.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2019-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2019-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -33,7 +33,11 @@ static esp_err_t eth_input_to_netif(esp_eth_handle_t eth_handle, uint8_t *buffer { #if CONFIG_ESP_NETIF_L2_TAP esp_err_t ret = ESP_OK; - ret = esp_vfs_l2tap_eth_filter_frame(eth_handle, buffer, (size_t *)&length, info); + l2tap_eth_filter_info_t l2tap_info = { + .l2_buffer = NULL, + .hw_ts = (l2tap_timestamp_t *)info, // Memory layout matches the Ethernet MAC driver type (eth_mac_time_t) + }; + ret = esp_vfs_l2tap_eth_filter_frame(eth_handle, buffer, (size_t *)&length, &l2tap_info); if (length == 0) { return ret; } diff --git a/components/esp_eth/src/mac/esp_eth_mac_esp.c b/components/esp_eth/src/mac/esp_eth_mac_esp.c index 6448fcd6de4..e84f8ff5977 100644 --- a/components/esp_eth/src/mac/esp_eth_mac_esp.c +++ b/components/esp_eth/src/mac/esp_eth_mac_esp.c @@ -556,30 +556,41 @@ static esp_err_t emac_esp32_transmit(esp_eth_mac_t *mac, uint8_t *buf, uint32_t return ESP_OK; } -static esp_err_t emac_esp32_transmit_ctrl_vargs(esp_eth_mac_t *mac, void *ctrl, uint32_t argc, va_list args) +static esp_err_t emac_esp32_transmit_ctrl_bufs(esp_eth_mac_t *mac, void *ctrl, const esp_eth_buf_desc_t *bufs, size_t buf_count) { emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent); - uint32_t buf_num = argc / 2; - - emac_esp_dma_transmit_buff_t buff_array[buf_num]; + emac_esp_dma_transmit_buff_t buff_array[buf_count]; uint32_t exp_len = 0; - for (int i = 0; i < buf_num; i++) { - buff_array[i].buf = va_arg(args, uint8_t *); - buff_array[i].size = va_arg(args, uint32_t); + for (size_t i = 0; i < buf_count; i++) { + buff_array[i].buf = bufs[i].buf; + buff_array[i].size = bufs[i].len; exp_len += buff_array[i].size; } eth_mac_time_t *ts = (eth_mac_time_t *)ctrl; - uint32_t sent_len = emac_esp_dma_transmit_frame_ext(emac->emac_dma_hndl, buff_array, buf_num, ts); + uint32_t sent_len = emac_esp_dma_transmit_frame_ext(emac->emac_dma_hndl, buff_array, (uint32_t)buf_count, ts); - if(sent_len != exp_len) { + if (sent_len != exp_len) { ESP_LOGD(TAG, "insufficient TX buffer size"); return ESP_ERR_NO_MEM; } return ESP_OK; } +static esp_err_t emac_esp32_transmit_ctrl_vargs(esp_eth_mac_t *mac, void *ctrl, uint32_t argc, va_list args) +{ + uint32_t buf_num = argc / 2; + esp_eth_buf_desc_t bufs[buf_num]; + + for (uint32_t i = 0; i < buf_num; i++) { + bufs[i].buf = va_arg(args, uint8_t *); + bufs[i].len = va_arg(args, uint32_t); + } + + return emac_esp32_transmit_ctrl_bufs(mac, ctrl, bufs, buf_num); +} + static esp_err_t emac_esp32_receive(esp_eth_mac_t *mac, uint8_t *buf, uint32_t *length) { esp_err_t ret = ESP_OK; @@ -1239,7 +1250,11 @@ esp_eth_mac_t *esp_eth_mac_new_esp32(const eth_esp32_emac_config_t *esp32_config emac->parent.set_peer_pause_ability = emac_esp32_set_peer_pause_ability; emac->parent.enable_flow_ctrl = emac_esp32_enable_flow_ctrl; emac->parent.transmit = emac_esp32_transmit; + emac->parent.transmit_ctrl_bufs = emac_esp32_transmit_ctrl_bufs; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" emac->parent.transmit_ctrl_vargs = emac_esp32_transmit_ctrl_vargs; +#pragma GCC diagnostic pop emac->parent.receive = emac_esp32_receive; emac->parent.custom_ioctl = emac_esp_custom_ioctl; #ifdef SOC_EMAC_IEEE1588V2_SUPPORTED diff --git a/components/esp_eth/src/sublayer/README.md b/components/esp_eth/src/sublayer/README.md new file mode 100644 index 00000000000..15b2947813f --- /dev/null +++ b/components/esp_eth/src/sublayer/README.md @@ -0,0 +1,327 @@ +# ESP-ETH Netif Sublayer + +> **Experimental feature** — enable `CONFIG_ETH_SUBLAYER_SUPPORT` in menuconfig +> (requires `CONFIG_IDF_EXPERIMENTAL_FEATURES`). The sublayer API is under active +> development and **may change** in future ESP-IDF releases without a deprecation +> period. +> +> Integrated Ethernet switch support is a separate, disabled-by-default feature. +> Enable `CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT` to build its API and frame-processing paths. + +The **sublayer** (`esp_eth_sublayer_t`) is the single coupling point between one physical Ethernet driver (`esp_eth_handle_t`) and one or more `esp_netif` instances. +It owns the RX/TX paths registered with the driver, distributes ETH events to every attached `esp_netif`, and exposes optional hooks for mid-path frame processing (e.g. MACsec). + +--- + +## Architecture + +```mermaid +flowchart TB + Driver["`**ETH Driver** + esp_eth_handle_t`"] + + subgraph SUB["Sublayer — esp_eth_sublayer_t"] + ICswitch["`**Integrated Switch (opt)** + MUX / DEMUX + (e.g. tail tagging)`"] + Hook["`**RX / TX Hook** (optional) + e.g. MACsec`"] + TapLL["`**ETH TAP LL** + L2TAP at driver level + sees raw 802.1Q frames`"] + Demux["`**VLAN DEMUX / MUX** + 802.1Q inspect, route + strip RX, insert TX`"] + end + + subgraph CU["VLAN Child — untagged (VID = 0xFFFF)"] + TapU["`**L2TAP** + per-VLAN filter`"] + NetifU["esp_netif (untagged)"] + end + + subgraph CN["VLAN Child — tagged (VID = N)"] + TapN["`**L2TAP** + per-VLAN filter`"] + NetifN["esp_netif (VLAN N)"] + end + + Driver <-->|"RX / TX"| ICswitch + ICswitch <--> Hook + Hook <--> TapLL + TapLL <--> Demux + Demux <-->|"untagged"| TapU <--> NetifU + Demux <-->|"VID=N"| TapN <--> NetifN +``` + +### Structural rules + +* The **sublayer must not be attached to a netif directly** — only VLAN children are attached via `esp_netif_attach()`. The `post_attach` callback lives in the VLAN child, not in the sublayer. +* The sublayer is the **sole owner** of the driver input path (`esp_eth_update_input_path_info`) and the ETH event handlers; children derive their events from it. +* VLAN children are stored in a singly-linked list (`SLIST`) managed exclusively by the sublayer. + +--- + +## RX Data Path + +``` +ETH Driver + │ raw frame (may carry 802.1Q VLAN tag) + optional hardware timestamp + ▼ +eth_sublayer_input() [esp_eth_sublayer.c] + │ + │ alloc_base = buffer (original malloc pointer, used for all frees) + │ frame = buffer (movable frame start, may advance as tags are stripped) + │ + ├─ ⓪ Integrated switch demux (optional; CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT) + │ eth_switch_demux(sw, &frame, &length, &src_port) + │ Strips switch-specific tagging before any further processing. + │ • Trailing tag (e.g. KSZ8863 Tail Tag): decrement length. + │ • Leading tag (e.g. DSA, 802.1Q port-VLAN): advance frame forward + │ (typically memmove MAC header over the tag) and decrement length. + │ eth_switch_resolve_ingress_port() then maps src_port to the per-port + │ Ethernet handle (passed to the RX hook) and L2TAP iodriver handle. + │ + ├─ ① RX Hook (optional) + │ rx_info = { .l2_buffer = alloc_base, .driver_info = info } + │ rx_hook(eth, &frame, &length, &rx_info, hook_ctx) + │ eth is the host driver, or the ingress port handle when a switch is attached. + │ Set via esp_eth_sublayer_config_t at creation (immutable thereafter). + │ May advance frame or shrink length to strip a leading/trailing header + │ (e.g. MACsec SecTAG). Must NOT realloc the buffer. + │ Set length=0 to take ownership and stop sublayer RX processing; + │ the hook must release/forward rx_info.l2_buffer itself. + │ Example use-case: MACsec decryption, filtering. + │ + ├─ ② ETH TAP LL ← closest to driver + │ esp_vfs_l2tap_eth_filter_frame(io_handle, frame, &length, &l2tap_info) + │ l2tap_info.l2_buffer = alloc_base (original allocation for L2TAP free path). + │ io_handle is the sublayer (sub) when no switch is attached, or the + │ resolved ingress-port iodriver when a switch is attached. + │ Frames captured here still carry the original 802.1Q tag. + │ length=0 after the call means the frame was consumed by L2TAP. + │ + ├─ ③ VLAN DEMUX + │ eth_vlan_get_ether_type(frame) reads EtherType at bytes [12:13]. + │ • 0x8100 (802.1Q): extracts VID from TCI bytes [14:15]. + │ • anything else: VID = ESP_ETH_SUBLAYER_UNTAGGED_VID (0xFFFF). + │ eth_sublayer_find_vlan_by_vid() looks up the matching VLAN child. + │ Frame is dropped (freed via alloc_base) when no child is registered. + │ + └─ ④ Per-VLAN input eth_vlan_input() [esp_eth_sublayer_vlan_child.c] + │ + ├─ L2TAP (per-VLAN) + │ esp_vfs_l2tap_eth_filter_frame(vlan_netif_driver, ...) + │ The vlan_netif_driver pointer is the iodriver handle here. + │ For tagged frames: the VLAN tag is stripped first (memmove of + │ the Ethernet header over the 4-byte TCI/TPID fields) so the + │ netif and L2TAP at this level see an untagged frame. + │ l2tap_info.l2_buffer = alloc_base so L2TAP can free correctly. + │ + └─ esp_netif_receive(netif, data, len, alloc_base) + Delivers the untagged payload to the network stack (lwIP). + alloc_base is passed as the L2 buffer base for zero-copy pbuf free. +``` + +### RX frame pointer contract + +Integrated switch demux and RX hook callbacks receive the frame start as an in/out +`uint8_t **` pointer. They may advance `*buffer` forward (leading tag strip) and/or +decrement `*length` (trailing tag strip), but must stay within the original allocation +and must not realloc. RX hook also receives `esp_eth_sublayer_rx_info_t`, which includes +`l2_buffer`. The sublayer uses `alloc_base` for every +`eth_sublayer_buf_free()` call and as the `eb` / `l2_buffer` argument to +`esp_netif_receive()` / L2TAP unless the hook takes ownership by setting length to 0. + +--- + +## TX Data Path + +``` +esp_netif + │ standard Ethernet frame (no VLAN tag) + ▼ +eth_vlan_transmit_wrap() [esp_eth_sublayer_vlan_child.c] + │ + ├─ Tagged VLAN child (VID ≠ 0xFFFF): + │ Allocates a new Ethernet header buffer (ETH_HEADER_LEN + 4 bytes). + │ Copies DA/SA, inserts TPID=0x8100, TCI (pre-computed tci_be), and + │ original EtherType. Frame is split into two scatter-gather descriptors: + │ bufs[0] = new tagged header (14 + 4 = 18 bytes) + │ bufs[1] = original payload (frame − 14 bytes) + │ No memcpy of payload. + │ + └─ Untagged child (VID = 0xFFFF): + Single descriptor, frame passed through as-is. + + ▼ +eth_sublayer_transmit() [esp_eth_sublayer.c] + │ + ├─ TX Hook (optional) + │ tx_hook(eth_driver, tx_bufs, hook_ctx) + │ May modify buf pointers / lengths in place, merge bufs, or set + │ *tx_bufs->buf_count = 0 to skip driver transmit (descriptor set + │ stays caller-owned). + │ Example use-case: MACsec encryption. + │ + ├─ Integrated switch mux (optional; CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT) + │ eth_switch_mux(sw, tx_bufs, port) + │ Appends switch-specific tagging (e.g. Tail Tag) after the TX hook. + │ + └─ esp_eth_transmit_ctrl_bufs() + Passes the descriptor array to the MAC driver. +``` + +--- + +## L2TAP Integration + +L2TAP provides POSIX-like file-descriptor access to raw Ethernet frames +(`/dev/net/tap`). The sublayer integrates these L2TAP access points: + +| Access point | Bind / iodriver handle | Frame content | Typical use | +|---|---|---|---| +| **ETH TAP LL** (no switch) | host `esp_eth_handle_t` / `esp_eth_sublayer_t *sub` | Raw, still 802.1Q-tagged | Capture/inject frames before VLAN demux; TSN / PTP tooling | +| **Switch port** | per-port `esp_eth_handle_t` | Switch tag already stripped; 802.1Q still present | Per-port capture/inject | +| **Per-VLAN L2TAP** | `esp_eth_sublayer_vlan_t *vlan_netif_driver` | VLAN tag already stripped | Per-VLAN capture; tagged-VLAN sockets | + +The sublayer registers itself with L2TAP on creation (`esp_vfs_l2tap_iodriver_provider_register`) and unregisters on deletion. The `get_io_fns` callback (`eth_sublayer_get_io_fns`) is called by L2TAP when an fd binds to a specific iodriver handle: + +* if `io_handle` matches a registered VLAN child → returns VLAN child TX functions (includes VLAN tag insertion on transmit). +* if a switch is attached and `io_handle` matches a port Ethernet handle → returns that port's TX functions (switch mux tags the frame for the port). Direct bind to the host driver (`sub->eth_driver`) is not served in switch mode. +* if no switch is attached and `io_handle` matches the host driver (`sub->eth_driver`) → returns the ETH TAP LL TX functions that bypass VLAN tag insertion. + +The IO driver function table (`esp_eth_iodriver_io_fns_t`) and the provider interface +(`esp_eth_iodriver_provider_base_t`) are a **generic esp_eth concept** defined in +`esp_private/esp_eth_sublayer_iodriver.h` (an internal, inter-component header — not stable application API), +independent of L2TAP. L2TAP is just the first consumer; any upper layer can +obtain the transmit/free/get-ll-driver functions for a base, VLAN child, or switch port through the +same `get_io_fns` resolver. The provider implementation is built when +`CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER` is enabled (selected automatically by `CONFIG_ESP_NETIF_L2_TAP`), +while the L2TAP-specific registration is additionally guarded by `CONFIG_ESP_NETIF_L2_TAP`. + +--- + +## VLAN Support (802.1Q) + +``` +Registered child VIDs Frame arriving at sublayer input +──────────────────────── ────────────────────────────────────────────── +ESP_ETH_SUBLAYER_UNTAGGED_VID ← EtherType ≠ 0x8100 (untagged) +VID = 10 ← EtherType == 0x8100, TCI & 0x0FFF == 10 +VID = 20 ← EtherType == 0x8100, TCI & 0x0FFF == 20 +(no match) ← frame is freed / dropped +``` + +* **RX**: the 4-byte VLAN tag (TPID + TCI) is removed from tagged frames using `memmove` before delivery to `esp_netif_receive`. The `l2_buffer` pointer in `l2tap_eth_filter_info_t` is set to the original allocation start so that the L2TAP layer can free it. +* **TX**: a new 18-byte Ethernet header is allocated and the payload is referenced via a second scatter-gather descriptor. The original frame buffer is never copied. +* **ETH events**: `ETHERNET_EVENT_START/STOP/CONNECTED/DISCONNECTED` are applied to every VLAN child whose `base.netif` is non-NULL. `IP_EVENT_ETH_GOT_IP` is matched against `ip_event->esp_netif` and forwarded to exactly one child. + + Optional `esp_eth_sublayer_config_t` events (leave event `base` NULL to disable): + + * `connect_confirm_event` — child netifs go up only after both `ETHERNET_EVENT_CONNECTED` and this event. Order does not matter. A later disconnect (`ETHERNET_EVENT_DISCONNECTED`, `ETHERNET_EVENT_STOP`, or `disconnect_trigger_event`) invalidates the confirmation. + * `disconnect_trigger_event` — additional event that forces all child netifs down, besides `ETHERNET_EVENT_DISCONNECTED`. + +`CONFIG_ETH_SUBLAYER_VLAN_SUPPORT` (disabled by default) gates 802.1Q tagging support. When disabled: + +* `esp_eth_sublayer_vlan_add()` returns `ESP_ERR_NOT_SUPPORTED` for any `vlan_id` other than `ESP_ETH_SUBLAYER_UNTAGGED_VID` — only a single, untagged netif per sublayer is possible. Switch ports are L2TAP / iodriver endpoints, not extra untagged netifs. +* The TX tag-insertion and RX tag-removal code paths are compiled out. + +--- + +## Optional TX / RX Hooks + +```c +esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT(); +sub_cfg.eth_handle = eth_handle; +sub_cfg.tx_hook = tx_hook; +sub_cfg.post_tx_hook = post_tx_hook; +sub_cfg.rx_hook = rx_hook; +sub_cfg.hook_ctx = ctx; +esp_eth_sublayer_handle_t sub = NULL; +ESP_ERROR_CHECK(esp_eth_sublayer_new(&sub_cfg, &sub)); +``` + +Hooks are called on every frame passing through the sublayer before VLAN demux (RX) or after VLAN tag insertion (TX). Pass `NULL` for a hook to leave that callback unhooked. Hooks are fixed at sublayer creation and cannot be changed at runtime. + +`post_tx_hook` is an optional companion to `tx_hook`: it is invoked after the TX hook returns `ESP_OK` with `*tx_bufs->buf_count > 0`, so the hook can free any buffers it allocated. It is not called when the TX hook sets `*tx_bufs->buf_count` to 0 or returns an error. + +**RX hook signature**: +```c +esp_err_t rx_hook(esp_eth_handle_t eth, uint8_t **buf, uint32_t *len, + esp_eth_sublayer_rx_info_t *info, void *ctx); +// Set *len = 0 to stop sublayer processing for the frame. +// This takes ownership by the hook function; release/forward info->l2_buffer. +// Return non-ESP_OK to abort processing (buffer is freed). +``` + +**TX hook signature**: +```c +esp_err_t tx_hook(esp_eth_handle_t eth, esp_eth_sublayer_tx_bufs_t *tx_bufs, void *ctx); +// May reorder descriptors (e.g. move original frame from bufs[0] to bufs[1] +// and place prefix/header data into bufs[0]). +// May increase/decrease *tx_bufs->buf_count as needed, but never exceed tx_bufs->buf_capacity. +// Set *tx_bufs->buf_count = 0 to skip driver transmit (and post_tx_hook). +``` + +--- + +## Minimal Usage + +```c +// 1. Create sublayer for an existing Ethernet driver handle +esp_eth_sublayer_config_t sub_cfg = { .eth_handle = eth_handle }; +esp_eth_sublayer_handle_t sub = NULL; +ESP_ERROR_CHECK(esp_eth_sublayer_new(&sub_cfg, &sub)); + +// 2. Add VLAN children (VID or UNTAGGED sentinel) +esp_eth_sublayer_vlan_handle_t untagged = NULL; +ESP_ERROR_CHECK(esp_eth_sublayer_vlan_add(sub, ESP_ETH_SUBLAYER_UNTAGGED_VID, &untagged)); +esp_eth_sublayer_vlan_handle_t vlan10 = NULL; +ESP_ERROR_CHECK(esp_eth_sublayer_vlan_add(sub, 10, &vlan10)); + +// 3. Create esp_netif instances and attach VLAN child handles +esp_netif_t *netif_plain = esp_netif_new(&ESP_NETIF_DEFAULT_ETH()); +esp_netif_attach(netif_plain, untagged); + +esp_netif_t *netif_vlan10 = esp_netif_new(&vlan_cfg); +esp_netif_attach(netif_vlan10, vlan10); + +// 4. Start driver — sublayer handles ETH events from here +esp_eth_start(eth_handle); +``` + +See `examples/ethernet/sublayer` for a complete working example including static IP configuration. + +--- + +## Integrated Switch + +Requires `CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT`. A sublayer holds at most one switch. The mux/demux/init/deinit callbacks are implemented by the switch driver (e.g. KSZ8863 Tail Tag); the sublayer only invokes them. + +```c +esp_eth_sublayer_switch_config_t sw_cfg = { + .tag_process_init = driver_tag_init, // once, when the switch is added; returns ctx + .tag_process_deinit = driver_tag_deinit, // when the switch is deleted + .demux = driver_demux, // RX: strip switch tag, report ingress port + .mux = driver_mux, // TX: append switch tag (port < 0 = default lookup) + .host_eth_handle = host_eth, + .port_eth_handles = port_eths, + .ports_count = port_count, +}; +esp_eth_sublayer_switch_handle_t sw = NULL; +ESP_ERROR_CHECK(esp_eth_sublayer_switch_add(sub, &sw_cfg, &sw)); +// ... +ESP_ERROR_CHECK(esp_eth_sublayer_switch_del(sw)); +``` + +`esp_eth_sublayer_del()` also deletes an attached switch if one is still present. + +--- + +## Implementation Notes + +* The sublayer's `vlan_children` list is not protected by a mutex; configure it only from a single task context (during initialization), as documented in the header. +* The sublayer increases the driver's reference count on creation and decreases it on deletion to prevent the driver from being destroyed while still in use. +* `ESP_ETH_SUBLAYER_UNTAGGED_VID` is `UINT16_MAX` (0xFFFF); it is never a valid 802.1Q VID (VIDs are 12-bit, 0..4094, with 4095 reserved). + diff --git a/components/esp_eth/src/sublayer/esp_eth_sublayer.c b/components/esp_eth/src/sublayer/esp_eth_sublayer.c new file mode 100644 index 00000000000..7d382baa8ac --- /dev/null +++ b/components/esp_eth/src/sublayer/esp_eth_sublayer.c @@ -0,0 +1,651 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include "esp_err.h" +#include "esp_eth_driver.h" +#include "esp_netif.h" +#include "esp_netif_net_stack.h" +#include "esp_eth_sublayer_core.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_check.h" +#include "esp_eth_spec.h" +#include "hal/eth_types.h" +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER +#include "esp_private/esp_eth_sublayer_iodriver.h" +#endif +#if CONFIG_ESP_NETIF_L2_TAP +#include "esp_vfs_l2tap.h" +#endif +#if CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +/** + * @brief Transmit timeout when multiple tasks may transmit through the same sublayer concurrently + */ +#define ESP_ETH_SUBLAYER_TX_TIMEOUT_MS 250 +#endif // CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX + +typedef struct esp_eth_sublayer_s esp_eth_sublayer_t; + +static const char *TAG = "esp_eth.sublayer"; + +struct esp_eth_sublayer_s { +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER + esp_eth_iodriver_provider_base_t base; /*!< IO driver provider base; must stay first for provider handle downcast */ +#endif + SLIST_HEAD(, esp_eth_sublayer_vlan_s) vlan_child; +#if CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT + esp_eth_sublayer_switch_t *switch_child; +#endif + + esp_eth_handle_t eth_driver; + eth_speed_t link_speed; + bool connect_confirmed; + esp_event_handler_instance_t start_ctx_handler; + esp_event_handler_instance_t stop_ctx_handler; + esp_event_handler_instance_t connect_ctx_handler; + esp_event_handler_instance_t connect_confirm_ctx_handler; + esp_event_handler_instance_t disconnect_ctx_handler; + esp_event_handler_instance_t disconnect_trigger_ctx_handler; + esp_event_handler_instance_t get_ip_ctx_handler; + + esp_eth_sublayer_event_t connect_confirm; + esp_eth_sublayer_event_t disconnect_trigger; + + esp_eth_sublayer_tx_hook_t tx_hook; + esp_eth_sublayer_post_tx_hook_t post_tx_hook; + esp_eth_sublayer_rx_hook_t rx_hook; + void *hook_ctx; + +#if CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX + SemaphoreHandle_t transmit_mutex; + TickType_t transmit_mutex_timeout_ticks; +#endif // CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX +}; + +void eth_sublayer_buf_free(esp_eth_sublayer_handle_t sub, void *buffer) +{ + (void)sub; + free(buffer); +} + +esp_err_t eth_sublayer_set_mac_filter(esp_eth_sublayer_handle_t sub, const uint8_t *eth_mac, size_t mac_len, bool add) +{ + esp_eth_sublayer_t *sub_impl = (esp_eth_sublayer_t *)sub; + + ESP_RETURN_ON_FALSE(mac_len == ETH_ADDR_LEN, ESP_ERR_INVALID_ARG, TAG, "invalid MAC length"); + ESP_LOGD(TAG, "%s filter MAC: %02x:%02x:%02x:%02x:%02x:%02x", add ? "Add" : "Del", eth_mac[0], eth_mac[1], + eth_mac[2], eth_mac[3], eth_mac[4], eth_mac[5]); + if (add) { + ESP_RETURN_ON_ERROR(esp_eth_ioctl(sub_impl->eth_driver, ETH_CMD_ADD_MAC_FILTER, (void *)eth_mac), TAG, + "failed to add mac filter"); + } else { + ESP_RETURN_ON_ERROR(esp_eth_ioctl(sub_impl->eth_driver, ETH_CMD_DEL_MAC_FILTER, (void *)eth_mac), TAG, + "failed to delete mac filter"); + } + return ESP_OK; +} + +esp_err_t eth_sublayer_transmit(esp_eth_sublayer_handle_t sub, esp_eth_sublayer_tx_bufs_t *tx_bufs, void *eb, int32_t port) +{ + esp_err_t ret = ESP_OK; +#if !CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT + (void)port; +#endif + +#if CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX + // Serializes the complete TX path below (TX hook, integrated switch mux, driver transmit and post-TX + // hook) since different tasks may call this concurrently (e.g. multiple L2TAP sockets on different + // switch/VLAN ports), and TX hooks / integrated switch drivers may keep mutable state that is only + // safe to touch from one task at a time. + if (xSemaphoreTake(sub->transmit_mutex, sub->transmit_mutex_timeout_ticks) == pdFALSE) { + return ESP_ERR_TIMEOUT; + } +#endif // CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX + + if (sub->tx_hook) { + ret = sub->tx_hook(sub->eth_driver, tx_bufs, sub->hook_ctx); + if (ret != ESP_OK) { + goto out_no_post_tx; + } + } + // No buffers to transmit, skip the rest of the function + if (*tx_bufs->buf_count == 0) { + goto out_no_post_tx; + } + assert(*tx_bufs->buf_count <= tx_bufs->buf_capacity && "buffer count exceeds capacity"); + +#if CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT + if (sub->switch_child) { + ret = eth_switch_mux(sub->switch_child, tx_bufs, port); + if (ret != ESP_OK && ret != ESP_ERR_NOT_SUPPORTED) { + goto out; + } + } +#endif + + ret = esp_eth_transmit_ctrl_bufs(sub->eth_driver, eb, tx_bufs->bufs, *tx_bufs->buf_count); +#if CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT +out: +#endif + if (sub->post_tx_hook) { + sub->post_tx_hook(sub->eth_driver, tx_bufs, sub->hook_ctx); + } +out_no_post_tx: +#if CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX + xSemaphoreGive(sub->transmit_mutex); +#endif // CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX + return ret; +} + +static esp_err_t eth_sublayer_input(esp_eth_handle_t eth_handle, uint8_t *buffer, uint32_t length, void *priv, void *info) +{ + esp_eth_sublayer_t *sub = (esp_eth_sublayer_t *)priv; + esp_err_t ret = ESP_OK; + (void)eth_handle; + + uint8_t *alloc_base = buffer; + uint8_t *frame = buffer; + + esp_eth_handle_t src_eth_handle = sub->eth_driver; +#if CONFIG_ESP_NETIF_L2_TAP + void *l2tap_io_handle = (void *)sub; +#endif +#if CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT + // Integrated switch demux: strip the switch tag and resolve the ingress port before + // any further processing, so RX hook / L2 TAP / VLAN demux all operate on a clean frame. + if (sub->switch_child) { + int32_t src_port = -1; + ret = eth_switch_demux(sub->switch_child, &frame, &length, &src_port); + if (ret == ESP_OK) { +#if CONFIG_ESP_NETIF_L2_TAP + ret = eth_switch_resolve_ingress_port(sub->switch_child, src_port, &src_eth_handle, &l2tap_io_handle); +#else + ret = eth_switch_resolve_ingress_port(sub->switch_child, src_port, &src_eth_handle, NULL); +#endif + if (ret != ESP_OK) { + ESP_LOGE(TAG, "eth_switch_resolve_ingress_port failed: %d", ret); + eth_sublayer_buf_free(sub, alloc_base); + return ret; + } + } else if (ret != ESP_ERR_NOT_SUPPORTED) { + ESP_LOGE(TAG, "eth_switch_demux failed: %d", ret); + eth_sublayer_buf_free(sub, alloc_base); + return ret; + } + } +#endif + + esp_eth_sublayer_rx_info_t rx_info = { + .l2_buffer = alloc_base, + .driver_info = info, + }; + if (sub->rx_hook) { + ret = sub->rx_hook(src_eth_handle, &frame, &length, &rx_info, sub->hook_ctx); + if (ret == ESP_OK) { + if (length == 0) { + return ret; + } + } else { + eth_sublayer_buf_free(sub, alloc_base); + return ret; + } + } + + // low level L2TAP +#if CONFIG_ESP_NETIF_L2_TAP + l2tap_eth_filter_info_t l2tap_info = { + .l2_buffer = alloc_base, + .hw_ts = (l2tap_timestamp_t *)info, // Memory layout matches the Ethernet MAC driver type (eth_mac_time_t) + }; + ret = esp_vfs_l2tap_eth_filter_frame(l2tap_io_handle, frame, (size_t *)&length, &l2tap_info); + if (ret == ESP_OK) { + if (length == 0) { + return ret; + } + } else { + eth_sublayer_buf_free(sub, alloc_base); + return ret; + } +#endif + + uint16_t vlan_id = eth_vlan_get_ether_type(frame); + esp_eth_sublayer_vlan_t *vlan_netif_driver = eth_sublayer_find_vlan_by_vid(sub, vlan_id); + if (vlan_netif_driver == NULL) { + ESP_LOGD(TAG, "vlan child not found for VLAN ID %" PRIu16, vlan_id); + eth_sublayer_buf_free(sub, alloc_base); + return ESP_ERR_NOT_FOUND; + } + return eth_vlan_input(vlan_netif_driver, frame, length, alloc_base, info); +} + +esp_eth_sublayer_vlan_t *eth_sublayer_find_vlan_by_vid(esp_eth_sublayer_handle_t sub, uint16_t vlan_id) +{ + esp_eth_sublayer_vlan_t *netif_iodriver; + esp_eth_sublayer_t *sub_impl = (esp_eth_sublayer_t *)sub; + SLIST_FOREACH(netif_iodriver, &sub_impl->vlan_child, next) { + if (netif_iodriver->vlan_id == vlan_id) { + return netif_iodriver; + } + } + return NULL; +} + +esp_err_t eth_sublayer_insert_vlan(esp_eth_sublayer_handle_t sub, esp_eth_sublayer_vlan_t *vlan) +{ + ESP_RETURN_ON_FALSE(sub && vlan, ESP_ERR_INVALID_ARG, TAG, "invalid arg"); + + esp_eth_sublayer_t *sub_impl = (esp_eth_sublayer_t *)sub; + SLIST_INSERT_HEAD(&sub_impl->vlan_child, vlan, next); + return ESP_OK; +} + +esp_err_t eth_sublayer_remove_vlan(esp_eth_sublayer_handle_t sub, esp_eth_sublayer_vlan_t *vlan) +{ + ESP_RETURN_ON_FALSE(sub && vlan, ESP_ERR_INVALID_ARG, TAG, "invalid arg"); + + esp_eth_sublayer_t *sub_impl = (esp_eth_sublayer_t *)sub; + esp_eth_sublayer_vlan_t *v; + SLIST_FOREACH(v, &sub_impl->vlan_child, next) { + if (v == vlan) { + SLIST_REMOVE(&sub_impl->vlan_child, vlan, esp_eth_sublayer_vlan_s, next); + return ESP_OK; + } + } + return ESP_ERR_NOT_FOUND; +} + +#if CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT +esp_err_t eth_sublayer_set_switch(esp_eth_sublayer_handle_t sub, esp_eth_sublayer_switch_t *sw) +{ + ESP_RETURN_ON_FALSE(sub && sw, ESP_ERR_INVALID_ARG, TAG, "invalid arg"); + ESP_RETURN_ON_FALSE(sub->switch_child == NULL, ESP_ERR_INVALID_STATE, TAG, "switch already attached"); + sub->switch_child = sw; + return ESP_OK; +} + +esp_eth_sublayer_switch_t *eth_sublayer_get_switch(esp_eth_sublayer_handle_t sub) +{ + if (sub == NULL) { + return NULL; + } + return sub->switch_child; +} + +esp_err_t eth_sublayer_remove_switch(esp_eth_sublayer_handle_t sub, esp_eth_sublayer_switch_t *sw) +{ + ESP_RETURN_ON_FALSE(sub && sw, ESP_ERR_INVALID_ARG, TAG, "invalid arg"); + ESP_RETURN_ON_FALSE(sub->switch_child == sw, ESP_ERR_NOT_FOUND, TAG, "switch is not attached to this sublayer"); + sub->switch_child = NULL; + return ESP_OK; +} +#endif // CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT + +/** One handler per ETH_EVENT: dispatch to base netif and every VLAN netif bound to this sublayer. */ +static void eth_action_start(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data; + esp_eth_sublayer_t *sub = handler_args; + if (sub->eth_driver != eth_handle) { + return; + } + esp_eth_sublayer_vlan_t *netif_iodriver; + SLIST_FOREACH(netif_iodriver, &sub->vlan_child, next) { + if (netif_iodriver->base.netif) { + esp_netif_action_start(netif_iodriver->base.netif, base, event_id, event_data); + } + } +} + +static void eth_action_stop(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data; + esp_eth_sublayer_t *sub = handler_args; + if (sub->eth_driver != eth_handle) { + return; + } + sub->link_speed = ETH_SPEED_MAX; // set link speed to invalid value to indicate that the physical link is not up + sub->connect_confirmed = false; // a new confirmation is required once the interface is started again + esp_eth_sublayer_vlan_t *netif_iodriver; + SLIST_FOREACH(netif_iodriver, &sub->vlan_child, next) { + if (netif_iodriver->base.netif) { + esp_netif_action_stop(netif_iodriver->base.netif, base, event_id, event_data); + } + } +} + +static void eth_action_connected(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + esp_eth_sublayer_t *sub = handler_args; + + if (base == ETH_EVENT) { + esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data; + if (sub->eth_driver != eth_handle) { + return; + } + if (event_id == ETHERNET_EVENT_CONNECTED) { + eth_speed_t speed; + esp_eth_ioctl(eth_handle, ETH_CMD_G_SPEED, &speed); + sub->link_speed = speed; + } + } + + bool set_connected = false; + // if no confirm connect handler is registered, immediately set the link, otherwise wait for confirmation event + if (!sub->connect_confirm_ctx_handler) { + set_connected = true; + } else if (base == sub->connect_confirm.base && event_id == sub->connect_confirm.event_id) { + // the confirmation is remembered so the order in which the confirmation and the physical link up + // are reported does not matter (the confirmation source does not need to be driven by ETH_EVENT) + sub->connect_confirmed = true; + if (sub->link_speed < ETH_SPEED_MAX) { + set_connected = true; + } else { + ESP_LOGD(TAG, "physical link is not up, skipping confirm connected event"); + } + } else if (sub->connect_confirmed && sub->link_speed < ETH_SPEED_MAX) { + // the confirmation was already received before the physical link came up + set_connected = true; + } + + if (set_connected) { + uint32_t link_speed = sub->link_speed == ETH_SPEED_1000M ? 1000'000'000 : sub->link_speed == ETH_SPEED_100M ? 100'000'000 : 10'000'000; + + esp_eth_sublayer_vlan_t *netif_iodriver; + SLIST_FOREACH(netif_iodriver, &sub->vlan_child, next) { + if (netif_iodriver->base.netif) { + esp_netif_set_link_speed(netif_iodriver->base.netif, link_speed); + esp_netif_action_connected(netif_iodriver->base.netif, base, event_id, event_data); + } + } + } +} + +static void eth_action_disconnected(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + esp_eth_sublayer_t *sub = handler_args; + if (base == ETH_EVENT) { + esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data; + if (sub->eth_driver != eth_handle) { + return; + } + sub->link_speed = ETH_SPEED_MAX; // set link speed to invalid value to indicate that the physical link is not up + } + sub->connect_confirmed = false; // a new confirmation is required to bring the child netifs up again + + // note: disconnect event always results in all child netifs being disconnected (no matter of event id) + esp_eth_sublayer_vlan_t *netif_iodriver; + SLIST_FOREACH(netif_iodriver, &sub->vlan_child, next) { + if (netif_iodriver->base.netif) { + esp_netif_action_disconnected(netif_iodriver->base.netif, base, event_id, event_data); + } + } +} + +static void eth_action_got_ip(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + ip_event_got_ip_t *ip_event = (ip_event_got_ip_t *)event_data; + esp_eth_sublayer_t *sub = handler_args; + + esp_eth_sublayer_vlan_t *netif_iodriver; + SLIST_FOREACH(netif_iodriver, &sub->vlan_child, next) { + if (netif_iodriver->base.netif == ip_event->esp_netif) { + esp_netif_action_got_ip(ip_event->esp_netif, base, event_id, event_data); + return; + } + } +} + +static esp_err_t sub_clear_handlers(esp_eth_sublayer_handle_t sub) +{ + ESP_RETURN_ON_FALSE(sub, ESP_ERR_INVALID_ARG, TAG, "sublayer handle can't be null"); + + if (sub->start_ctx_handler) { + esp_event_handler_instance_unregister(ETH_EVENT, ETHERNET_EVENT_START, sub->start_ctx_handler); + sub->start_ctx_handler = NULL; + } + if (sub->stop_ctx_handler) { + esp_event_handler_instance_unregister(ETH_EVENT, ETHERNET_EVENT_STOP, sub->stop_ctx_handler); + sub->stop_ctx_handler = NULL; + } + if (sub->connect_ctx_handler) { + esp_event_handler_instance_unregister(ETH_EVENT, ETHERNET_EVENT_CONNECTED, sub->connect_ctx_handler); + sub->connect_ctx_handler = NULL; + } + if (sub->connect_confirm_ctx_handler) { + esp_event_handler_instance_unregister(sub->connect_confirm.base, sub->connect_confirm.event_id, + sub->connect_confirm_ctx_handler); + sub->connect_confirm_ctx_handler = NULL; + } + if (sub->disconnect_ctx_handler) { + esp_event_handler_instance_unregister(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED, sub->disconnect_ctx_handler); + sub->disconnect_ctx_handler = NULL; + } + if (sub->disconnect_trigger_ctx_handler) { + esp_event_handler_instance_unregister(sub->disconnect_trigger.base, sub->disconnect_trigger.event_id, + sub->disconnect_trigger_ctx_handler); + sub->disconnect_trigger_ctx_handler = NULL; + } + if (sub->get_ip_ctx_handler) { + esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_ETH_GOT_IP, sub->get_ip_ctx_handler); + sub->get_ip_ctx_handler = NULL; + } + return ESP_OK; +} + +static esp_err_t sub_set_handlers(esp_eth_sublayer_handle_t sub) +{ + ESP_RETURN_ON_FALSE(sub, ESP_ERR_INVALID_ARG, TAG, "sublayer handle can't be null"); + + esp_err_t ret = esp_event_handler_instance_register(ETH_EVENT, ETHERNET_EVENT_START, eth_action_start, sub, + &sub->start_ctx_handler); + if (ret != ESP_OK) { + goto fail; + } + ret = esp_event_handler_instance_register(ETH_EVENT, ETHERNET_EVENT_STOP, eth_action_stop, sub, &sub->stop_ctx_handler); + if (ret != ESP_OK) { + goto fail; + } + ret = esp_event_handler_instance_register(ETH_EVENT, ETHERNET_EVENT_CONNECTED, eth_action_connected, sub, + &sub->connect_ctx_handler); + if (ret != ESP_OK) { + goto fail; + } + ret = esp_event_handler_instance_register(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED, eth_action_disconnected, sub, + &sub->disconnect_ctx_handler); + if (ret != ESP_OK) { + goto fail; + } + ret = esp_event_handler_instance_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, eth_action_got_ip, sub, &sub->get_ip_ctx_handler); + if (ret != ESP_OK) { + goto fail; + } + + if (sub->connect_confirm.base) { + ret = esp_event_handler_instance_register(sub->connect_confirm.base, sub->connect_confirm.event_id, + eth_action_connected, sub, + &sub->connect_confirm_ctx_handler); + if (ret != ESP_OK) { + goto fail; + } + } + if (sub->disconnect_trigger.base) { + ret = esp_event_handler_instance_register(sub->disconnect_trigger.base, sub->disconnect_trigger.event_id, + eth_action_disconnected, sub, + &sub->disconnect_trigger_ctx_handler); + if (ret != ESP_OK) { + goto fail; + } + } + return ESP_OK; +fail: + sub_clear_handlers(sub); + return ret; +} + +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER +static esp_err_t eth_sublayer_ll_transmit_wrap(void *h, void *buf, size_t len, void *eb) +{ + esp_eth_sublayer_t *sub = (esp_eth_sublayer_t *)h; + esp_eth_buf_desc_t bufs[ESP_ETH_SUBLAYER_TX_BUF_DESC_CAPACITY]; + bufs[0].buf = buf; + bufs[0].len = len; + size_t buf_count = 1; + esp_eth_sublayer_tx_bufs_t tx_bufs = { + .bufs = bufs, + .buf_count = &buf_count, + .buf_capacity = ESP_ETH_SUBLAYER_TX_BUF_DESC_CAPACITY, + }; + // port is not defined for direct lower layer transmit (integrated switch internal lookup is used when needed) + return eth_sublayer_transmit(sub, &tx_bufs, eb, -1); +} + +static esp_err_t eth_sublayer_ll_transmit(void *h, void *buf, size_t len) +{ + return eth_sublayer_ll_transmit_wrap(h, buf, len, NULL); +} + +static esp_err_t eth_sublayer_get_ll_driver(void *h, void **ll_driver) +{ + esp_eth_sublayer_t *sub = (esp_eth_sublayer_t *)h; + *ll_driver = sub->eth_driver; + return ESP_OK; +} + +static void eth_sublayer_ll_free(void *h, void *buffer) +{ + esp_eth_sublayer_t *sub = (esp_eth_sublayer_t *)h; + eth_sublayer_buf_free(sub, buffer); +} + +// Note: The consumer may call this from a critical section, hence no logging, allocation nor blocking is +// allowed here. See esp_eth_iodriver_provider_base_t::get_io_fns. +static esp_err_t eth_sublayer_get_io_fns(esp_eth_iodriver_provider_handle sublayer, void *io_handle, esp_eth_iodriver_io_fns_t *io_fns) +{ + if (sublayer == NULL || io_handle == NULL || io_fns == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_eth_sublayer_t *sub = (esp_eth_sublayer_t *)sublayer; + + // Based on the io_handle, get the io_fns - it can be either higher layer transmit functions with additional + // processing like VLAN tagged, or lower layer sublayer transmit functions (closer to the underlying Ethernet driver). + // At first check if the io_handle is a VLAN tagged netif iodriver + esp_eth_sublayer_vlan_t *netif_iodriver; + SLIST_FOREACH(netif_iodriver, &sub->vlan_child, next) { + if (netif_iodriver == io_handle) { + return eth_vlan_get_iodriver_io_fns(netif_iodriver, io_fns); + } + } + // If no VLAN tagged netif iodriver is found, check if switch port is requested + // Note: It's not allowed to request direct host Ethernet driver access when in switch mode. +#if CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT + if (sub->switch_child) { + return eth_switch_get_iodriver_io_fns(sub->switch_child, io_handle, io_fns); + } +#endif + // If no switch, check if direct lower layer is requested + if (io_handle == sub->eth_driver) { + io_fns->io_handle = sub; + io_fns->iodriver_transmit = eth_sublayer_ll_transmit; + io_fns->iodriver_transmit_wrap = eth_sublayer_ll_transmit_wrap; + io_fns->iodriver_free_rx_buffer = eth_sublayer_ll_free; + io_fns->iodriver_get_ll_driver = eth_sublayer_get_ll_driver; + return ESP_OK; + } + return ESP_ERR_NOT_FOUND; +} +#endif + +esp_err_t esp_eth_sublayer_del(esp_eth_sublayer_handle_t sublayer) +{ + ESP_RETURN_ON_FALSE(sublayer, ESP_ERR_INVALID_ARG, TAG, "sublayer can't be null"); + + while (!SLIST_EMPTY(&sublayer->vlan_child)) { + esp_eth_sublayer_vlan_del(sublayer, SLIST_FIRST(&sublayer->vlan_child)); + } + +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER && CONFIG_ESP_NETIF_L2_TAP + esp_vfs_l2tap_iodriver_provider_unregister(sublayer); +#endif // CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER && CONFIG_ESP_NETIF_L2_TAP + +#if CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT + if (sublayer->switch_child) { + esp_eth_sublayer_switch_del(sublayer->switch_child); + } +#endif + + sub_clear_handlers(sublayer); + esp_eth_decrease_reference(sublayer->eth_driver); +#if CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX + if (sublayer->transmit_mutex) { + vSemaphoreDelete(sublayer->transmit_mutex); + } +#endif // CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX + free(sublayer); + return ESP_OK; +} + +esp_err_t esp_eth_sublayer_new(const esp_eth_sublayer_config_t *config, esp_eth_sublayer_handle_t *sublayer) +{ + ESP_RETURN_ON_FALSE(config && sublayer, ESP_ERR_INVALID_ARG, TAG, "invalid arg"); + ESP_RETURN_ON_ERROR(esp_eth_increase_reference(config->eth_handle), TAG, "failed to increase reference"); + + esp_err_t ret = ESP_OK; + esp_eth_sublayer_t *sub = calloc(1, sizeof(esp_eth_sublayer_t)); + ESP_RETURN_ON_FALSE(sub, ESP_ERR_NO_MEM, TAG, "create netif sublayer failed"); + sub->eth_driver = config->eth_handle; + sub->link_speed = ETH_SPEED_MAX; // invalid speed indicates that the speed is not yet known (link is not up) + + sub->connect_confirm = config->connect_confirm_event; + sub->disconnect_trigger = config->disconnect_trigger_event; + + sub->tx_hook = config->tx_hook; + sub->post_tx_hook = config->post_tx_hook; + sub->rx_hook = config->rx_hook; + sub->hook_ctx = config->hook_ctx; + +#if CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX + sub->transmit_mutex = xSemaphoreCreateMutex(); + ESP_GOTO_ON_FALSE(sub->transmit_mutex, ESP_ERR_NO_MEM, err, TAG, "failed to create sublayer transmit mutex"); + uint32_t transmit_mutex_timeout_ms = config->transmit_mutex_timeout_ms ? config->transmit_mutex_timeout_ms + : ESP_ETH_SUBLAYER_TX_TIMEOUT_MS; + sub->transmit_mutex_timeout_ticks = pdMS_TO_TICKS(transmit_mutex_timeout_ms); +#endif // CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX + + SLIST_INIT(&sub->vlan_child); + +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER + sub->base.get_io_fns = eth_sublayer_get_io_fns; +#if CONFIG_ESP_NETIF_L2_TAP + ESP_GOTO_ON_ERROR(esp_vfs_l2tap_iodriver_provider_register(sub), err, TAG, "failed to register sublayer with L2 TAP"); +#endif // CONFIG_ESP_NETIF_L2_TAP +#endif // CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER + ESP_GOTO_ON_ERROR(esp_eth_update_input_path_info(config->eth_handle, eth_sublayer_input, sub), err, TAG, "failed to update input path info"); + ESP_GOTO_ON_ERROR(sub_set_handlers(sub), err, TAG, "failed to set handlers"); + + *sublayer = sub; + return ESP_OK; +err: + esp_eth_sublayer_del(sub); + return ret; +} + +esp_err_t eth_sublayer_get_mac_addr(esp_eth_sublayer_handle_t sublayer, uint8_t *mac_addr) +{ + ESP_RETURN_ON_FALSE(sublayer && mac_addr, ESP_ERR_INVALID_ARG, TAG, "invalid arg"); + return esp_eth_ioctl(sublayer->eth_driver, ETH_CMD_G_MAC_ADDR, mac_addr); +} + +esp_eth_handle_t esp_eth_sublayer_get_eth_handle(esp_eth_sublayer_handle_t sublayer) +{ + ESP_RETURN_ON_FALSE(sublayer, NULL, TAG, "sublayer can't be null"); + return sublayer->eth_driver; +} diff --git a/components/esp_eth/src/sublayer/esp_eth_sublayer_core.h b/components/esp_eth/src/sublayer/esp_eth_sublayer_core.h new file mode 100644 index 00000000000..8307e8cd8c1 --- /dev/null +++ b/components/esp_eth/src/sublayer/esp_eth_sublayer_core.h @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include "esp_err.h" +#include "esp_eth_driver.h" +#include "esp_eth_sublayer.h" +#include "esp_eth_sublayer_vlan.h" +#if CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT +#include "esp_eth_sublayer_switch.h" +#endif + +/** + * @brief Find a VLAN entry by VID in the sublayer's child list. + * + * @param sub The sublayer handle. + * @param vlan_id The VLAN ID to find. + * @return The VLAN child if found, NULL otherwise. + */ +esp_eth_sublayer_vlan_t *eth_sublayer_find_vlan_by_vid(esp_eth_sublayer_handle_t sub, uint16_t vlan_id); + +/** + * @brief Insert a VLAN child into the sublayer's child list. + */ +esp_err_t eth_sublayer_insert_vlan(esp_eth_sublayer_handle_t sub, esp_eth_sublayer_vlan_t *vlan); + +/** + * @brief Remove a VLAN child from the sublayer's child list. + * + * @param sub The sublayer handle. + * @param vlan The VLAN child to remove. + * @return ESP_OK if found and removed, ESP_ERR_NOT_FOUND otherwise. + */ +esp_err_t eth_sublayer_remove_vlan(esp_eth_sublayer_handle_t sub, esp_eth_sublayer_vlan_t *vlan); + +/** + * @brief Get the MAC address from the underlying Ethernet driver. + * + * @param sublayer The sublayer handle. + * @param mac_addr The MAC address to get. + * @return ESP_OK if the MAC address is got, ESP_ERR_INVALID_ARG if the sublayer is NULL, ESP_ERR_INVALID_ARG if the MAC address is NULL. + */ + esp_err_t eth_sublayer_get_mac_addr(esp_eth_sublayer_handle_t sublayer, uint8_t *mac_addr); + +/** + * @brief Set the MAC filter through the sublayer. + * + * @param sub The sublayer handle. + * @param eth_mac The MAC address. + * @param mac_len The length of the MAC address (must be 6). + * @param add Whether to add or remove the MAC address. + * @return ESP_OK if the MAC filter is set, ESP_ERR_INVALID_ARG if the sublayer is NULL, ESP_ERR_INVALID_ARG if the MAC address is NULL, ESP_ERR_INVALID_ARG if the MAC address length is not 6, ESP_ERR_INVALID_ARG if the add flag is not true or false. + */ + esp_err_t eth_sublayer_set_mac_filter(esp_eth_sublayer_handle_t sub, const uint8_t *eth_mac, size_t mac_len, bool add); + +/** + * @brief Transmit a frame through the sublayer. + * + * @param sub The sublayer handle. + * @param tx_bufs The buffer descriptor set. + * @param eb The extended buffer (may hold additional metadata like time stamp). + * @param port The port number. + * @return ESP_OK if the frame is transmitted, ESP_ERR_INVALID_ARG if the sublayer is NULL, ESP_ERR_INVALID_ARG if the buffer descriptors are NULL, ESP_ERR_INVALID_ARG if the event base is NULL. + */ +esp_err_t eth_sublayer_transmit(esp_eth_sublayer_handle_t sub, esp_eth_sublayer_tx_bufs_t *tx_bufs, void *eb, int32_t port); + + +/** + * @brief Free a buffer from the sublayer. + * + * @param sub The sublayer handle. + * @param buffer The buffer to free. + * @return void + */ +void eth_sublayer_buf_free(esp_eth_sublayer_handle_t sub, void *buffer); + +#if CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT +/** + * @brief Attach the single integrated switch instance to the sublayer. + * + * @param sub The sublayer handle. + * @param sw The switch instance. + * @return ESP_OK on success, ESP_ERR_INVALID_STATE if a switch is already attached, ESP_ERR_INVALID_ARG on bad args. + */ +esp_err_t eth_sublayer_set_switch(esp_eth_sublayer_handle_t sub, esp_eth_sublayer_switch_t *sw); + +/** + * @brief Get the single integrated switch instance attached to the sublayer. + * + * @param sub The sublayer handle. + * @return The switch instance if found, NULL otherwise. + */ +esp_eth_sublayer_switch_t *eth_sublayer_get_switch(esp_eth_sublayer_handle_t sub); + +/** + * @brief Detach the integrated switch instance from the sublayer. + * + * @param sub The sublayer handle. + * @param sw The switch instance. + * @return ESP_OK if detached, ESP_ERR_NOT_FOUND if not attached to this sublayer, ESP_ERR_INVALID_ARG on bad args. + */ +esp_err_t eth_sublayer_remove_switch(esp_eth_sublayer_handle_t sub, esp_eth_sublayer_switch_t *sw); +#endif // CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT diff --git a/components/esp_eth/src/sublayer/esp_eth_sublayer_frame.h b/components/esp_eth/src/sublayer/esp_eth_sublayer_frame.h new file mode 100644 index 00000000000..0f846b6a7ba --- /dev/null +++ b/components/esp_eth/src/sublayer/esp_eth_sublayer_frame.h @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "esp_eth_spec.h" + +typedef struct { + uint8_t da[ETH_ADDR_LEN]; + uint8_t sa[ETH_ADDR_LEN]; + uint16_t ether_type; +} __attribute__((packed)) eth_hdr_t; + +typedef struct { + uint16_t tpid; + uint16_t tci; +} __attribute__((packed)) eth_vlan_tag_t; + +typedef struct { + uint8_t da[ETH_ADDR_LEN]; + uint8_t sa[ETH_ADDR_LEN]; + eth_vlan_tag_t vlan_tag; + uint16_t ether_type; +} __attribute__((packed)) eth_vlan_hdr_t; diff --git a/components/esp_eth/src/sublayer/esp_eth_sublayer_switch.c b/components/esp_eth/src/sublayer/esp_eth_sublayer_switch.c new file mode 100644 index 00000000000..f292190c5a5 --- /dev/null +++ b/components/esp_eth/src/sublayer/esp_eth_sublayer_switch.c @@ -0,0 +1,186 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "esp_eth_sublayer_switch.h" +#include "esp_eth_sublayer_core.h" +#include "esp_log.h" +#include "esp_check.h" +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER +#include "esp_private/esp_eth_sublayer_iodriver.h" +#endif + +typedef struct esp_eth_sublayer_ic_switch_port_s { + esp_eth_sublayer_switch_t *sw_parent; + int32_t port_num; + esp_eth_handle_t eth_handle; +} esp_eth_sublayer_ic_switch_port_t; + +/* Integrated switch (Tail Tag) child. */ +struct esp_eth_sublayer_switch_s { + esp_eth_sublayer_handle_t sub_parent; + esp_eth_sublayer_switch_demux_t demux; /*!< Per RX frame tag strip / ingress port resolve */ + esp_eth_sublayer_switch_mux_t mux; /*!< Per TX frame tag append */ + esp_eth_sublayer_switch_tag_deinit_t tag_process_deinit; /*!< Releases @c ctx on delete */ + void *ctx; /*!< Driver switch context from tag_process_init */ + uint32_t ports_count; /*!< Number of per-port Ethernet driver handles */ + esp_eth_sublayer_ic_switch_port_t port_io_handles[]; /*!< Per-port driver handles */ +}; + +static const char *TAG = "esp_eth.sublayer.ic_switch"; + +esp_err_t eth_switch_demux(esp_eth_sublayer_switch_t *sw, uint8_t **buffer, uint32_t *length, int32_t *src_port) +{ + ESP_RETURN_ON_FALSE(sw, ESP_ERR_INVALID_ARG, TAG, "switch can't be null"); + + // No demux callback means nothing to strip and no ingress port to report, so the frame is left + // unchanged and the caller must keep using the sublayer level handles + if (sw->demux == NULL) { + return ESP_ERR_NOT_SUPPORTED; + } + return sw->demux(sw->ctx, buffer, length, src_port); +} + +esp_err_t eth_switch_resolve_ingress_port(esp_eth_sublayer_switch_t *sw, int32_t port_num, + esp_eth_handle_t *eth_handle, void **io_handle) +{ + ESP_RETURN_ON_FALSE(sw, ESP_ERR_INVALID_ARG, TAG, "switch can't be null"); + ESP_RETURN_ON_FALSE(port_num >= 1 && port_num <= (int32_t)sw->ports_count, ESP_ERR_INVALID_ARG, TAG, "invalid port"); + + if (eth_handle) { + *eth_handle = sw->port_io_handles[port_num - 1].eth_handle; + } + if (io_handle) { + *io_handle = &sw->port_io_handles[port_num - 1]; + } + return ESP_OK; +} + +esp_err_t eth_switch_mux(esp_eth_sublayer_switch_t *sw, esp_eth_sublayer_tx_bufs_t *tx_bufs, int32_t port) +{ + ESP_RETURN_ON_FALSE(sw, ESP_ERR_INVALID_ARG, TAG, "switch can't be null"); + ESP_RETURN_ON_FALSE(tx_bufs && tx_bufs->bufs && tx_bufs->buf_count, ESP_ERR_INVALID_ARG, TAG, "invalid buffer"); + ESP_RETURN_ON_FALSE(port >= -1 && port <= (int32_t)sw->ports_count, ESP_ERR_INVALID_ARG, TAG, "invalid port"); + + if (sw->mux == NULL) { + return ESP_ERR_NOT_SUPPORTED; + } + return sw->mux(sw->ctx, tx_bufs, port); +} + +esp_err_t esp_eth_sublayer_switch_add(esp_eth_sublayer_handle_t sublayer, + const esp_eth_sublayer_switch_config_t *config, + esp_eth_sublayer_switch_handle_t *sw) +{ + ESP_RETURN_ON_FALSE(sublayer && config && sw, ESP_ERR_INVALID_ARG, TAG, "invalid arg"); + ESP_RETURN_ON_FALSE(config->port_eth_handles && config->ports_count > 0, ESP_ERR_INVALID_ARG, TAG, "invalid port config"); + + // A sublayer can hold at most one switch instance + if (eth_sublayer_get_switch(sublayer) != NULL) { + ESP_LOGE(TAG, "switch already added to sublayer"); + return ESP_ERR_INVALID_STATE; + } + + esp_eth_sublayer_switch_t *sw_entry = calloc(1, sizeof(esp_eth_sublayer_switch_t) + + config->ports_count * sizeof(esp_eth_sublayer_ic_switch_port_t)); + ESP_RETURN_ON_FALSE(sw_entry, ESP_ERR_NO_MEM, TAG, "alloc switch entry failed"); + for (uint32_t i = 0; i < config->ports_count; i++) { + sw_entry->port_io_handles[i].eth_handle = config->port_eth_handles[i]; + sw_entry->port_io_handles[i].port_num = i + 1; + sw_entry->port_io_handles[i].sw_parent = sw_entry; + } + sw_entry->ports_count = config->ports_count; + sw_entry->sub_parent = sublayer; + sw_entry->demux = config->demux; + sw_entry->mux = config->mux; + sw_entry->tag_process_deinit = config->tag_process_deinit; + + // Let the driver allocate its switch context (e.g. enable Tail Tagging, build the port table) + if (config->tag_process_init) { + esp_err_t ret = config->tag_process_init(config->host_eth_handle, config->port_eth_handles, + config->ports_count, &sw_entry->ctx); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "switch tag process init failed (0x%x)", ret); + free(sw_entry); + return ret; + } + } + + esp_err_t ret = eth_sublayer_set_switch(sublayer, sw_entry); + if (ret != ESP_OK) { + if (sw_entry->tag_process_deinit) { + sw_entry->tag_process_deinit(sw_entry->ctx); + } + free(sw_entry); + return ret; + } + *sw = sw_entry; + return ESP_OK; +} + +esp_err_t esp_eth_sublayer_switch_del(esp_eth_sublayer_switch_handle_t sw) +{ + ESP_RETURN_ON_FALSE(sw, ESP_ERR_INVALID_ARG, TAG, "switch handle can't be null"); + + esp_err_t ret = eth_sublayer_remove_switch(sw->sub_parent, sw); + if (ret == ESP_OK) { + if (sw->tag_process_deinit) { + sw->tag_process_deinit(sw->ctx); + } + free(sw); + } + return ret; +} + +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER +static esp_err_t eth_switch_transmit_wrap(void *h, void *buf, size_t len, void *eb) +{ + esp_eth_sublayer_ic_switch_port_t *port = (esp_eth_sublayer_ic_switch_port_t *)h; + esp_eth_sublayer_handle_t sub = port->sw_parent->sub_parent; + esp_eth_buf_desc_t bufs[ESP_ETH_SUBLAYER_TX_BUF_DESC_CAPACITY]; + size_t buf_count = 1; + + bufs[0] = (esp_eth_buf_desc_t){ .buf = (uint8_t *)buf, .len = len }; + esp_eth_sublayer_tx_bufs_t tx_bufs = { + .bufs = bufs, + .buf_count = &buf_count, + .buf_capacity = ESP_ETH_SUBLAYER_TX_BUF_DESC_CAPACITY, + }; + return eth_sublayer_transmit(sub, &tx_bufs, eb, port->port_num); +} + +static esp_err_t eth_switch_transmit(void *h, void *buf, size_t len) +{ + return eth_switch_transmit_wrap(h, buf, len, NULL); +} + +static void eth_switch_free_rx_buffer(void *h, void *buffer) +{ + esp_eth_sublayer_ic_switch_port_t *port = (esp_eth_sublayer_ic_switch_port_t *)h; + eth_sublayer_buf_free(port->sw_parent->sub_parent, buffer); +} + +static esp_err_t eth_switch_get_ll_driver(void *h, void **ll_driver) +{ + esp_eth_sublayer_ic_switch_port_t *port = (esp_eth_sublayer_ic_switch_port_t *)h; + *ll_driver = port->eth_handle; + return ESP_OK; +} + +esp_err_t eth_switch_get_iodriver_io_fns(esp_eth_sublayer_switch_t *sw, void *io_handle, esp_eth_iodriver_io_fns_t *io_fns) +{ + for(int i = 0; i < sw->ports_count; i++) { + if (sw->port_io_handles[i].eth_handle == io_handle) { + io_fns->io_handle = &sw->port_io_handles[i]; + io_fns->iodriver_transmit = eth_switch_transmit; + io_fns->iodriver_transmit_wrap = eth_switch_transmit_wrap; + io_fns->iodriver_free_rx_buffer = eth_switch_free_rx_buffer; + io_fns->iodriver_get_ll_driver = eth_switch_get_ll_driver; + return ESP_OK; + } + } + return ESP_ERR_NOT_FOUND; +} +#endif diff --git a/components/esp_eth/src/sublayer/esp_eth_sublayer_switch.h b/components/esp_eth/src/sublayer/esp_eth_sublayer_switch.h new file mode 100644 index 00000000000..61cb74e80dc --- /dev/null +++ b/components/esp_eth/src/sublayer/esp_eth_sublayer_switch.h @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "esp_err.h" +#include "esp_eth_driver.h" +#include "esp_eth_sublayer.h" + +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER +#include "esp_private/esp_eth_sublayer_iodriver.h" +#endif + +typedef struct esp_eth_sublayer_switch_s esp_eth_sublayer_switch_t; + +/** + * @brief RX demultiplexing of an integrated switch frame. + * + * Strips the switch tag (e.g. Tail Tag) and resolves the ingress port by invoking the driver demux + * callback. The frame is not re-routed; the sublayer continues its normal RX pipeline afterwards. + * + * @param[in] sw The switch instance. + * @param[in,out] buffer In: frame start including the switch tag; out: frame start with tag removed. + * @param[in,out] length Frame length including the tag on input, tag removed on output. + * @param[out] src_port Ingress port number (may be NULL). Only written when ESP_OK is returned. + * @return ESP_OK on success, ESP_ERR_NOT_SUPPORTED when the switch has no demux callback (frame left + * unchanged, @p src_port not resolved), error code otherwise (the sublayer drops the frame). + */ +esp_err_t eth_switch_demux(esp_eth_sublayer_switch_t *sw, uint8_t **buffer, uint32_t *length, int32_t *src_port); + +/** + * @brief Resolves a switch port number to its Ethernet driver handle and L2 TAP iodriver handle. + * + * @param[in] sw The switch instance. + * @param[in] port_num Ingress port number reported by the demux callback. + * @param[out] eth_handle Port Ethernet driver handle (may be NULL). + * @param[out] io_handle Port L2 TAP iodriver handle (may be NULL). + * @return ESP_OK on success, ESP_ERR_INVALID_ARG if @p port_num is not registered. + */ +esp_err_t eth_switch_resolve_ingress_port(esp_eth_sublayer_switch_t *sw, int32_t port_num, + esp_eth_handle_t *eth_handle, void **io_handle); + +/** + * @brief TX multiplexing of an integrated switch frame. + * + * Appends the switch tag (e.g. Tail Tag) by invoking the driver mux callback. + * + * @param[in] sw The switch instance. + * @param[in,out] tx_bufs Buffer descriptor set comprising the frame. + * @param port Destination port number. + * @return ESP_OK on success, ESP_ERR_NOT_SUPPORTED when the switch has no mux callback (frame left + * unchanged), error code otherwise. + */ +esp_err_t eth_switch_mux(esp_eth_sublayer_switch_t *sw, esp_eth_sublayer_tx_bufs_t *tx_bufs, int32_t port); + +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER +/** + * @brief Fill IO driver function pointers for a switch. + * + * Allows transmit via switch specific port. + * + * @param[in] sw The switch instance. + * @param[in] io_handle The IO handle. + * @param[out] io_fns Output; switch specific port iodriver handle. + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t eth_switch_get_iodriver_io_fns(esp_eth_sublayer_switch_t *sw, void *io_handle, esp_eth_iodriver_io_fns_t *io_fns); +#endif diff --git a/components/esp_eth/src/sublayer/esp_eth_sublayer_vlan.h b/components/esp_eth/src/sublayer/esp_eth_sublayer_vlan.h new file mode 100644 index 00000000000..c93d5ce0fdd --- /dev/null +++ b/components/esp_eth/src/sublayer/esp_eth_sublayer_vlan.h @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "esp_err.h" +#include "esp_netif_types.h" +#include "esp_eth_sublayer.h" + +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER +#include "esp_private/esp_eth_sublayer_iodriver.h" +#endif + +typedef struct esp_eth_sublayer_vlan_s { + esp_netif_driver_base_t base; + uint16_t vlan_id; + uint16_t tci_be; /*!< Pre-computed big-endian TCI for TX path */ + esp_eth_sublayer_handle_t parent; + SLIST_ENTRY(esp_eth_sublayer_vlan_s) next; /*!< Managed exclusively by sublayer.c */ +} esp_eth_sublayer_vlan_t; + +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER +/** + * @brief Fill IO driver function pointers for a VLAN + * + * @param vlan_netif_driver VLAN netif driver handle (same pointer as esp_netif_attach(..., handle) / esp_netif_get_io_driver()). + * @param io_fns Output; iodriver handle transmit functions. + */ +esp_err_t eth_vlan_get_iodriver_io_fns(esp_eth_sublayer_vlan_t *vlan_netif_driver, esp_eth_iodriver_io_fns_t *io_fns); +#endif + +/** + * @brief Input a frame through the VLAN netif. + * + * @param vlan_netif_driver VLAN netif driver handle. + * @param frame Input; Ethernet frame start (may be offset from the allocation base). + * @param length Input; Ethernet frame length. + * @param alloc_base Original RX buffer allocation base (passed to esp_netif_receive for freeing). + * @param info Input; Additional information. + * @return ESP_OK on success, error code otherwise. + + */ +esp_err_t eth_vlan_input(esp_eth_sublayer_vlan_t *vlan_netif_driver, uint8_t *frame, uint32_t length, + void *alloc_base, void *info); + +/** + * @brief Extract VLAN ID from an Ethernet frame (or ESP_ETH_SUBLAYER_UNTAGGED_VID if untagged). + */ +uint16_t eth_vlan_get_ether_type(uint8_t *buffer); diff --git a/components/esp_eth/src/sublayer/esp_eth_sublayer_vlan_child.c b/components/esp_eth/src/sublayer/esp_eth_sublayer_vlan_child.c new file mode 100644 index 00000000000..d4df001d735 --- /dev/null +++ b/components/esp_eth/src/sublayer/esp_eth_sublayer_vlan_child.c @@ -0,0 +1,269 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include "esp_eth_sublayer_frame.h" +#include "esp_eth_sublayer_vlan.h" +#include "esp_eth_sublayer_core.h" +#include "esp_netif.h" +#include "esp_log.h" +#include "esp_check.h" +#include "esp_eth_spec.h" +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER +#include "esp_private/esp_eth_sublayer_iodriver.h" +#endif +#if CONFIG_ESP_NETIF_L2_TAP +#include "esp_vfs_l2tap.h" +#endif + +static const char *TAG = "esp_eth.sublayer.vlan_child"; + + +uint16_t eth_vlan_get_ether_type(uint8_t *buffer) +{ + const eth_hdr_t *hdr = (const eth_hdr_t *)buffer; + if (__builtin_bswap16(hdr->ether_type) != ETH_T_8021Q) { + return ESP_ETH_SUBLAYER_UNTAGGED_VID; + } + const eth_vlan_hdr_t *vlan_hdr = (const eth_vlan_hdr_t *)buffer; + uint16_t vid = __builtin_bswap16(vlan_hdr->vlan_tag.tci) & 0x0FFF; + + return vid; +} + +esp_err_t eth_vlan_input(esp_eth_sublayer_vlan_t *vlan_netif_driver, uint8_t *frame, uint32_t length, + void *alloc_base, void *info) +{ +#if CONFIG_ESP_NETIF_L2_TAP + esp_err_t ret = ESP_OK; +#endif + + // frame is not VLAN tagged + if (vlan_netif_driver->vlan_id == ESP_ETH_SUBLAYER_UNTAGGED_VID) { + if (unlikely(vlan_netif_driver->base.netif == NULL)) { + ESP_LOGD(TAG, "untagged netif not found"); + eth_sublayer_buf_free(vlan_netif_driver->parent, alloc_base); + return ESP_ERR_NOT_FOUND; + } +#if CONFIG_ESP_NETIF_L2_TAP + l2tap_eth_filter_info_t l2tap_info = { + .l2_buffer = alloc_base, + .hw_ts = (l2tap_timestamp_t *)info, // Memory layout matches the Ethernet MAC driver type (eth_mac_time_t) + }; + ret = esp_vfs_l2tap_eth_filter_frame(vlan_netif_driver, frame, (size_t *)&length, &l2tap_info); + if (ret == ESP_OK) { + if (length == 0) { + return ret; + } + } else { + eth_sublayer_buf_free(vlan_netif_driver->parent, alloc_base); + return ret; + } +#endif + return esp_netif_receive(vlan_netif_driver->base.netif, frame, length, alloc_base); + } + +#if !CONFIG_ETH_SUBLAYER_VLAN_SUPPORT + // esp_eth_sublayer_vlan_add() only allows the untagged VID when VLAN support is disabled, so no + // tagged VLAN child can exist here - this path is unreachable, kept only to satisfy the compiler. + eth_sublayer_buf_free(vlan_netif_driver->parent, alloc_base); + return ESP_ERR_NOT_SUPPORTED; +#else + // check if VLAN netif is attached + if (vlan_netif_driver->base.netif == NULL) { + ESP_LOGD(TAG, "netif not found for VLAN ID %" PRIu16, vlan_netif_driver->vlan_id); + eth_sublayer_buf_free(vlan_netif_driver->parent, alloc_base); + return ESP_ERR_NOT_FOUND; + } + + // remove VLAN tag - just shift start of the frame over VLAN tag to avoid long memcpy of payload + memmove(frame + ETH_VLAN_TAG_LEN, frame, ETH_HEADER_LEN - 2); + length = length - ETH_VLAN_TAG_LEN; +#if CONFIG_ESP_NETIF_L2_TAP + // Process VLAN tagged frames through L2 TAP + l2tap_eth_filter_info_t l2tap_info = { + .l2_buffer = alloc_base, + .hw_ts = (l2tap_timestamp_t *)info, // Memory layout matches the Ethernet MAC driver type (eth_mac_time_t) + }; + ret = esp_vfs_l2tap_eth_filter_frame(vlan_netif_driver, frame + ETH_VLAN_TAG_LEN, (size_t *)&length, &l2tap_info); + if (ret == ESP_OK) { + if (length == 0) { + return ret; + } + } else { + eth_sublayer_buf_free(vlan_netif_driver->parent, alloc_base); + return ret; + } +#endif + return esp_netif_receive(vlan_netif_driver->base.netif, frame + ETH_VLAN_TAG_LEN, length, alloc_base); +#endif // !CONFIG_ETH_SUBLAYER_VLAN_SUPPORT +} + +static esp_err_t eth_vlan_transmit_wrap(void *h, void *buf, size_t len, void *eb) +{ + esp_eth_sublayer_vlan_t *vlan_netif_driver = (esp_eth_sublayer_vlan_t *)h; + esp_eth_sublayer_handle_t sub = vlan_netif_driver->parent; + esp_err_t ret = ESP_OK; + esp_eth_buf_desc_t bufs[ESP_ETH_SUBLAYER_TX_BUF_DESC_CAPACITY]; + size_t buf_count; + +#if CONFIG_ETH_SUBLAYER_VLAN_SUPPORT + eth_vlan_hdr_t *eth_hdr_buf = NULL; + if (vlan_netif_driver->vlan_id != ESP_ETH_SUBLAYER_UNTAGGED_VID) { + eth_hdr_buf = (eth_vlan_hdr_t *)malloc(sizeof(eth_vlan_hdr_t)); + if (eth_hdr_buf == NULL) { + return ESP_ERR_NO_MEM; + } + + // Copy Ethernet header to special buffer and add VLAN tag, manipulate only with header to avoid memcpy of payload + memcpy(eth_hdr_buf->da, ((eth_hdr_t *)buf)->da, sizeof(eth_hdr_buf->da) + sizeof(eth_hdr_buf->sa)); + eth_hdr_buf->vlan_tag.tpid = __builtin_bswap16(ETH_T_8021Q); + eth_hdr_buf->vlan_tag.tci = vlan_netif_driver->tci_be; + eth_hdr_buf->ether_type = ((eth_hdr_t *)buf)->ether_type; + bufs[0] = (esp_eth_buf_desc_t){ .buf = (uint8_t *)eth_hdr_buf, .len = ETH_HEADER_LEN + ETH_VLAN_TAG_LEN }; + + // Set offset of payload to skip Ethernet header in the second buffer + bufs[1] = (esp_eth_buf_desc_t){ .buf = (uint8_t *)buf + ETH_HEADER_LEN, .len = len - ETH_HEADER_LEN }; + buf_count = 2; + } else +#endif // CONFIG_ETH_SUBLAYER_VLAN_SUPPORT + { + // vlan_id is always ESP_ETH_SUBLAYER_UNTAGGED_VID when CONFIG_ETH_SUBLAYER_VLAN_SUPPORT is disabled, + // esp_eth_sublayer_vlan_add() rejects any other VID in that case. + bufs[0] = (esp_eth_buf_desc_t){ .buf = (uint8_t *)buf, .len = len }; + buf_count = 1; + } + esp_eth_sublayer_tx_bufs_t tx_bufs = { + .bufs = bufs, + .buf_count = &buf_count, + .buf_capacity = ESP_ETH_SUBLAYER_TX_BUF_DESC_CAPACITY, + }; + // eb holds additional metadata like time stamp, + // specific port is not defined for VLAN (use integrated switch internal lookup) + // Note, this keeps door open to force port netif instance if needed in the future + ret = eth_sublayer_transmit(sub, &tx_bufs, eb, -1); +#if CONFIG_ETH_SUBLAYER_VLAN_SUPPORT + free(eth_hdr_buf); +#endif // CONFIG_ETH_SUBLAYER_VLAN_SUPPORT + return ret; +} + +static esp_err_t eth_vlan_transmit(void *h, void *buf, size_t len) +{ + return eth_vlan_transmit_wrap(h, buf, len, NULL); +} + +static esp_err_t eth_vlan_set_mac_filter(void *h, const uint8_t *eth_mac, size_t mac_len, bool add) +{ + esp_eth_sublayer_vlan_t *vlan_netif_driver = (esp_eth_sublayer_vlan_t *)h; + return eth_sublayer_set_mac_filter(vlan_netif_driver->parent, eth_mac, mac_len, add); +} + +static void eth_vlan_free_rx_buffer(void *h, void *buffer) +{ + esp_eth_sublayer_vlan_t *vlan_netif_driver = (esp_eth_sublayer_vlan_t *)h; + eth_sublayer_buf_free(vlan_netif_driver->parent, buffer); +} + +esp_err_t netif_post_attach(esp_netif_t *esp_netif, void *args) +{ + uint8_t eth_mac[ETH_ADDR_LEN]; + esp_eth_sublayer_vlan_t *vlan_netif_driver = (esp_eth_sublayer_vlan_t *)args; + esp_eth_sublayer_handle_t parent_sub = vlan_netif_driver->parent; + + esp_netif_driver_ifconfig_t driver_ifconfig = { + .handle = vlan_netif_driver, + .transmit = eth_vlan_transmit, + .transmit_wrap = eth_vlan_transmit_wrap, + .driver_free_rx_buffer = eth_vlan_free_rx_buffer, + .driver_set_mac_filter = eth_vlan_set_mac_filter, + }; + + ESP_RETURN_ON_ERROR(esp_netif_set_driver_config(esp_netif, &driver_ifconfig), TAG, "failed to set driver config"); + ESP_RETURN_ON_ERROR(eth_sublayer_get_mac_addr(parent_sub, eth_mac), TAG, "failed to get MAC addr"); + ESP_RETURN_ON_ERROR(esp_netif_set_mac(esp_netif, eth_mac), TAG, "failed to set MAC"); + // assigned only once the netif is fully configured, the Rx path uses it to decide whether the frame + // can be passed to the netif + vlan_netif_driver->base.netif = esp_netif; + + if (vlan_netif_driver->vlan_id == ESP_ETH_SUBLAYER_UNTAGGED_VID) { + ESP_LOGI(TAG, "%02x:%02x:%02x:%02x:%02x:%02x", eth_mac[0], eth_mac[1], + eth_mac[2], eth_mac[3], eth_mac[4], eth_mac[5]); + ESP_LOGI(TAG, "untagged Ethernet attached to netif"); + } else { + ESP_LOGI(TAG, "VLAN %" PRIu16 " attached to netif", vlan_netif_driver->vlan_id); + } + return ESP_OK; +} + +esp_eth_handle_t esp_eth_sublayer_vlan_get_eth_handle(esp_eth_sublayer_vlan_handle_t vlan) +{ + ESP_RETURN_ON_FALSE(vlan, NULL, TAG, "vlan can't be null"); + return esp_eth_sublayer_get_eth_handle(vlan->parent); +} + +esp_err_t esp_eth_sublayer_vlan_add(esp_eth_sublayer_handle_t sublayer, uint16_t vlan_id, + esp_eth_sublayer_vlan_handle_t *vlan) +{ + ESP_RETURN_ON_FALSE(sublayer && vlan, ESP_ERR_INVALID_ARG, TAG, "invalid arg"); + +#if !CONFIG_ETH_SUBLAYER_VLAN_SUPPORT + ESP_RETURN_ON_FALSE(vlan_id == ESP_ETH_SUBLAYER_UNTAGGED_VID, ESP_ERR_NOT_SUPPORTED, TAG, + "tagged VLAN children require CONFIG_ETH_SUBLAYER_VLAN_SUPPORT"); +#endif // !CONFIG_ETH_SUBLAYER_VLAN_SUPPORT + + if (eth_sublayer_find_vlan_by_vid(sublayer, vlan_id) != NULL) { + ESP_LOGE(TAG, "duplicate VLAN id %" PRIu16, vlan_id); + return ESP_ERR_INVALID_STATE; + } + + esp_eth_sublayer_vlan_t *vlan_entry = calloc(1, sizeof(esp_eth_sublayer_vlan_t)); + ESP_RETURN_ON_FALSE(vlan_entry, ESP_ERR_NO_MEM, TAG, "alloc vlan entry failed"); + vlan_entry->vlan_id = vlan_id; + vlan_entry->tci_be = __builtin_bswap16((uint16_t)(vlan_id & 0x0FFF)); + vlan_entry->parent = sublayer; + vlan_entry->base.post_attach = netif_post_attach; + + eth_sublayer_insert_vlan(sublayer, vlan_entry); + *vlan = vlan_entry; + return ESP_OK; +} + +esp_err_t esp_eth_sublayer_vlan_del(esp_eth_sublayer_handle_t sublayer, esp_eth_sublayer_vlan_handle_t vlan) +{ + ESP_RETURN_ON_FALSE(sublayer && vlan, ESP_ERR_INVALID_ARG, TAG, "invalid arg"); + + esp_err_t ret = eth_sublayer_remove_vlan(sublayer, vlan); + if (ret == ESP_OK) { + free(vlan); + } + return ret; +} + +#if CONFIG_ETH_SUBLAYER_IODRIVER_PROVIDER +static esp_err_t eth_vlan_get_ll_driver(void *h, void **ll_driver) +{ + esp_eth_sublayer_vlan_t *vlan_netif_driver = (esp_eth_sublayer_vlan_t *)h; + *ll_driver = esp_eth_sublayer_get_eth_handle(vlan_netif_driver->parent); + return ESP_OK; +} + +esp_err_t eth_vlan_get_iodriver_io_fns(esp_eth_sublayer_vlan_t *vlan_netif_driver, esp_eth_iodriver_io_fns_t *io_fns) +{ + // Note: May be called from a critical section, hence no logging, allocation nor blocking is allowed here. + if (vlan_netif_driver == NULL || io_fns == NULL) { + return ESP_ERR_INVALID_ARG; + } + + io_fns->io_handle = vlan_netif_driver; + io_fns->iodriver_transmit = eth_vlan_transmit; + io_fns->iodriver_transmit_wrap = eth_vlan_transmit_wrap; + io_fns->iodriver_free_rx_buffer = eth_vlan_free_rx_buffer; + io_fns->iodriver_get_ll_driver = eth_vlan_get_ll_driver; + return ESP_OK; +} +#endif diff --git a/components/esp_eth/test_apps/.build-test-rules.yml b/components/esp_eth/test_apps/.build-test-rules.yml index 823edca42f2..41004aad179 100644 --- a/components/esp_eth/test_apps/.build-test-rules.yml +++ b/components/esp_eth/test_apps/.build-test-rules.yml @@ -1,6 +1,6 @@ # Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps -components/esp_eth/test_apps: +components/esp_eth/test_apps/test_app_driver: enable: - if: IDF_TARGET in ["esp32", "esp32p4", "esp32s31"] reason: ESP32, ESP32P4 and ESP32S31 have internal EMAC. @@ -13,3 +13,12 @@ components/esp_eth/test_apps: - hal - lwip - soc + +components/esp_eth/test_apps/test_app_sublayer: + enable: + - if: IDF_TARGET in ["esp32", "esp32p4"] + reason: ESP32 and ESP32P4 have internal EMAC. + depends_components: + - esp_eth + - esp_netif + - lwip diff --git a/components/esp_eth/test_apps/.gitignore b/components/esp_eth/test_apps/test_app_driver/.gitignore similarity index 100% rename from components/esp_eth/test_apps/.gitignore rename to components/esp_eth/test_apps/test_app_driver/.gitignore diff --git a/components/esp_eth/test_apps/CMakeLists.txt b/components/esp_eth/test_apps/test_app_driver/CMakeLists.txt similarity index 100% rename from components/esp_eth/test_apps/CMakeLists.txt rename to components/esp_eth/test_apps/test_app_driver/CMakeLists.txt diff --git a/components/esp_eth/test_apps/README.md b/components/esp_eth/test_apps/test_app_driver/README.md similarity index 100% rename from components/esp_eth/test_apps/README.md rename to components/esp_eth/test_apps/test_app_driver/README.md diff --git a/components/esp_eth/test_apps/conftest.py b/components/esp_eth/test_apps/test_app_driver/conftest.py similarity index 100% rename from components/esp_eth/test_apps/conftest.py rename to components/esp_eth/test_apps/test_app_driver/conftest.py diff --git a/components/esp_eth/test_apps/main/CMakeLists.txt b/components/esp_eth/test_apps/test_app_driver/main/CMakeLists.txt similarity index 100% rename from components/esp_eth/test_apps/main/CMakeLists.txt rename to components/esp_eth/test_apps/test_app_driver/main/CMakeLists.txt diff --git a/components/esp_eth/test_apps/main/esp_eth_test_emac_init.cpp b/components/esp_eth/test_apps/test_app_driver/main/esp_eth_test_emac_init.cpp similarity index 100% rename from components/esp_eth/test_apps/main/esp_eth_test_emac_init.cpp rename to components/esp_eth/test_apps/test_app_driver/main/esp_eth_test_emac_init.cpp diff --git a/components/esp_eth/test_apps/main/esp_eth_test_emac_sleep_retention.c b/components/esp_eth/test_apps/test_app_driver/main/esp_eth_test_emac_sleep_retention.c similarity index 100% rename from components/esp_eth/test_apps/main/esp_eth_test_emac_sleep_retention.c rename to components/esp_eth/test_apps/test_app_driver/main/esp_eth_test_emac_sleep_retention.c diff --git a/components/esp_eth/test_apps/main/esp_eth_test_esp_emac.c b/components/esp_eth/test_apps/test_app_driver/main/esp_eth_test_esp_emac.c similarity index 94% rename from components/esp_eth/test_apps/main/esp_eth_test_esp_emac.c rename to components/esp_eth/test_apps/test_app_driver/main/esp_eth_test_esp_emac.c index 62caaac5328..4a2c53e28a1 100644 --- a/components/esp_eth/test_apps/main/esp_eth_test_esp_emac.c +++ b/components/esp_eth/test_apps/test_app_driver/main/esp_eth_test_esp_emac.c @@ -191,48 +191,58 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]") TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); ESP_LOGI(TAG, "-- Verify transmission using extended Tx fnc using one buffer--"); + esp_eth_buf_desc_t tx_bufs[3] = { + { .buf = (uint8_t *)test_pkt }, + }; transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE; ESP_LOGI(TAG, "transmit frame size: %" PRIu16, transmit_size); recv_info.expected_size = transmit_size; eth_mac_time_t ts; - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, &ts, 2, test_pkt, transmit_size)); + tx_bufs[0].len = transmit_size; + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, &ts, tx_bufs, 1)); printf("test %lu.%lu sec\n", ts.seconds, ts.nanoseconds); // TODO finish the test TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE - 1; ESP_LOGI(TAG, "transmit frame size: %" PRIu16, transmit_size); recv_info.expected_size = transmit_size; - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 2, test_pkt, transmit_size)); + tx_bufs[0].len = transmit_size; + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 1)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE + 1; ESP_LOGI(TAG, "transmit frame size: %" PRIu16, transmit_size); recv_info.expected_size = transmit_size; - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 2, test_pkt, transmit_size)); + tx_bufs[0].len = transmit_size; + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 1)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); transmit_size = 2 * CONFIG_ETH_DMA_BUFFER_SIZE; ESP_LOGI(TAG, "transmit frame size: %" PRIu16, transmit_size); recv_info.expected_size = transmit_size; - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 2, test_pkt, transmit_size)); + tx_bufs[0].len = transmit_size; + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 1)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); transmit_size = 2 * CONFIG_ETH_DMA_BUFFER_SIZE - 1; ESP_LOGI(TAG, "transmit frame size: %" PRIu16, transmit_size); recv_info.expected_size = transmit_size; - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 2, test_pkt, transmit_size)); + tx_bufs[0].len = transmit_size; + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 1)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); transmit_size = 2 * CONFIG_ETH_DMA_BUFFER_SIZE + 1; ESP_LOGI(TAG, "transmit frame size: %" PRIu16, transmit_size); recv_info.expected_size = transmit_size; - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 2, test_pkt, transmit_size)); + tx_bufs[0].len = transmit_size; + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 1)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); ESP_LOGI(TAG, "-- Verify transmission using extended Tx func with multiple buffers --"); uint16_t transmit_size_2; // allocated the second buffer uint8_t *pkt_data_2 = (uint8_t *)eth_test_alloc(ETH_MAX_PAYLOAD_LEN); + tx_bufs[1].buf = pkt_data_2; // fill with data (reverse order to differentiate the buffers) int j = ETH_MAX_PAYLOAD_LEN; for (int i = 0; i < ETH_MAX_PAYLOAD_LEN; i++) { @@ -247,9 +257,11 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]") transmit_size_2 = CONFIG_ETH_DMA_BUFFER_SIZE; recv_info.expected_size = transmit_size; recv_info.expected_size_2 = transmit_size_2; + tx_bufs[0].len = transmit_size; + tx_bufs[1].len = transmit_size_2; for (int32_t i = 0; i < config_eth_dma_max_buffer_num*2; i++) { ESP_LOGI(TAG, "transmit joint frame size: %" PRIu16 ", i = %" PRIi32, transmit_size + transmit_size_2, i); - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 4, test_pkt, transmit_size, pkt_data_2, transmit_size_2)); + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 2)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); } @@ -258,8 +270,10 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]") transmit_size_2 = CONFIG_ETH_DMA_BUFFER_SIZE; recv_info.expected_size = transmit_size; recv_info.expected_size_2 = transmit_size_2; + tx_bufs[0].len = transmit_size; + tx_bufs[1].len = transmit_size_2; ESP_LOGI(TAG, "transmit joint frame size: %" PRIu16, transmit_size + transmit_size_2); - TEST_ESP_OK(esp_eth_transmit_vargs(eth_handle, 2, test_pkt, transmit_size, pkt_data_2, transmit_size_2)); + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 2)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); ESP_LOGI(TAG, "Verify boundary conditions"); @@ -267,29 +281,36 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]") transmit_size_2 = CONFIG_ETH_DMA_BUFFER_SIZE; recv_info.expected_size = transmit_size; recv_info.expected_size_2 = transmit_size_2; + tx_bufs[0].len = transmit_size; + tx_bufs[1].len = transmit_size_2; ESP_LOGI(TAG, "transmit joint frame size: %" PRIu16, transmit_size + transmit_size_2); - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 4, test_pkt, transmit_size, pkt_data_2, transmit_size_2)); + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 2)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE - 1; transmit_size_2 = CONFIG_ETH_DMA_BUFFER_SIZE; recv_info.expected_size = transmit_size; recv_info.expected_size_2 = transmit_size_2; + tx_bufs[0].len = transmit_size; + tx_bufs[1].len = transmit_size_2; ESP_LOGI(TAG, "transmit joint frame size: %" PRIu16, transmit_size + transmit_size_2); - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 4, test_pkt, transmit_size, pkt_data_2, transmit_size_2)); + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 2)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE + 1; transmit_size_2 = CONFIG_ETH_DMA_BUFFER_SIZE; recv_info.expected_size = transmit_size; recv_info.expected_size_2 = transmit_size_2; + tx_bufs[0].len = transmit_size; + tx_bufs[1].len = transmit_size_2; ESP_LOGI(TAG, "transmit joint frame size: %" PRIu16, transmit_size + transmit_size_2); - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 4, test_pkt, transmit_size, pkt_data_2, transmit_size_2)); + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 2)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); uint16_t transmit_size_3 = 256; // allocated the third buffer uint8_t *pkt_data_3 = (uint8_t *)eth_test_alloc(256); + tx_bufs[2].buf = pkt_data_3; // fill with data for (int i = 0; i < 256; i++) { pkt_data_3[i] = i & 0xFF; @@ -302,8 +323,11 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]") recv_info.expected_size = transmit_size; recv_info.expected_size_2 = transmit_size_2; recv_info.expected_size_3 = transmit_size_3; + tx_bufs[0].len = transmit_size; + tx_bufs[1].len = transmit_size_2; + tx_bufs[2].len = transmit_size_3; ESP_LOGI(TAG, "transmit joint frame size (3 buffs): %" PRIu16, transmit_size + transmit_size_2 + transmit_size_3); - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 6, test_pkt, transmit_size, pkt_data_2, transmit_size_2, pkt_data_3, transmit_size_3)); + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 3)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE - 1; @@ -312,8 +336,11 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]") recv_info.expected_size = transmit_size; recv_info.expected_size_2 = transmit_size_2; recv_info.expected_size_3 = transmit_size_3; + tx_bufs[0].len = transmit_size; + tx_bufs[1].len = transmit_size_2; + tx_bufs[2].len = transmit_size_3; ESP_LOGI(TAG, "transmit joint frame size (3 buffs): %" PRIu16, transmit_size + transmit_size_2 + transmit_size_3); - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 6, test_pkt, transmit_size, pkt_data_2, transmit_size_2, pkt_data_3, transmit_size_3)); + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 3)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE + 1; @@ -322,8 +349,11 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]") recv_info.expected_size = transmit_size; recv_info.expected_size_2 = transmit_size_2; recv_info.expected_size_3 = transmit_size_3; + tx_bufs[0].len = transmit_size; + tx_bufs[1].len = transmit_size_2; + tx_bufs[2].len = transmit_size_3; ESP_LOGI(TAG, "transmit joint frame size (3 buffs): %" PRIu16, transmit_size + transmit_size_2 + transmit_size_3); - TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 6, test_pkt, transmit_size, pkt_data_2, transmit_size_2, pkt_data_3, transmit_size_3)); + TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 3)); TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500))); // stop Ethernet driver diff --git a/components/esp_eth/test_apps/main/esp_eth_test_main.c b/components/esp_eth/test_apps/test_app_driver/main/esp_eth_test_main.c similarity index 100% rename from components/esp_eth/test_apps/main/esp_eth_test_main.c rename to components/esp_eth/test_apps/test_app_driver/main/esp_eth_test_main.c diff --git a/components/esp_eth/test_apps/main/idf_component.yml b/components/esp_eth/test_apps/test_app_driver/main/idf_component.yml similarity index 100% rename from components/esp_eth/test_apps/main/idf_component.yml rename to components/esp_eth/test_apps/test_app_driver/main/idf_component.yml diff --git a/components/esp_eth/test_apps/pytest_esp_eth.py b/components/esp_eth/test_apps/test_app_driver/pytest_esp_eth.py similarity index 100% rename from components/esp_eth/test_apps/pytest_esp_eth.py rename to components/esp_eth/test_apps/test_app_driver/pytest_esp_eth.py diff --git a/components/esp_eth/test_apps/sdkconfig.ci.default_generic b/components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.default_generic similarity index 100% rename from components/esp_eth/test_apps/sdkconfig.ci.default_generic rename to components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.default_generic diff --git a/components/esp_eth/test_apps/sdkconfig.ci.default_generic_esp32p4 b/components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.default_generic_esp32p4 similarity index 100% rename from components/esp_eth/test_apps/sdkconfig.ci.default_generic_esp32p4 rename to components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.default_generic_esp32p4 diff --git a/components/esp_eth/test_apps/sdkconfig.ci.default_generic_esp32p4v1 b/components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.default_generic_esp32p4v1 similarity index 100% rename from components/esp_eth/test_apps/sdkconfig.ci.default_generic_esp32p4v1 rename to components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.default_generic_esp32p4v1 diff --git a/components/esp_eth/test_apps/sdkconfig.ci.default_yt8531_esp32s31 b/components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.default_yt8531_esp32s31 similarity index 100% rename from components/esp_eth/test_apps/sdkconfig.ci.default_yt8531_esp32s31 rename to components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.default_yt8531_esp32s31 diff --git a/components/esp_eth/test_apps/sdkconfig.ci.emac_sleep_retention b/components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.emac_sleep_retention similarity index 100% rename from components/esp_eth/test_apps/sdkconfig.ci.emac_sleep_retention rename to components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.emac_sleep_retention diff --git a/components/esp_eth/test_apps/sdkconfig.ci.release_generic b/components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.release_generic similarity index 100% rename from components/esp_eth/test_apps/sdkconfig.ci.release_generic rename to components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.release_generic diff --git a/components/esp_eth/test_apps/sdkconfig.ci.rmii_clko_esp32 b/components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.rmii_clko_esp32 similarity index 100% rename from components/esp_eth/test_apps/sdkconfig.ci.rmii_clko_esp32 rename to components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.rmii_clko_esp32 diff --git a/components/esp_eth/test_apps/sdkconfig.ci.rmii_clko_esp32p4 b/components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.rmii_clko_esp32p4 similarity index 100% rename from components/esp_eth/test_apps/sdkconfig.ci.rmii_clko_esp32p4 rename to components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.rmii_clko_esp32p4 diff --git a/components/esp_eth/test_apps/sdkconfig.ci.rmii_clko_esp32p4v1 b/components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.rmii_clko_esp32p4v1 similarity index 100% rename from components/esp_eth/test_apps/sdkconfig.ci.rmii_clko_esp32p4v1 rename to components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.rmii_clko_esp32p4v1 diff --git a/components/esp_eth/test_apps/sdkconfig.ci.single_core_generic b/components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.single_core_generic similarity index 100% rename from components/esp_eth/test_apps/sdkconfig.ci.single_core_generic rename to components/esp_eth/test_apps/test_app_driver/sdkconfig.ci.single_core_generic diff --git a/components/esp_eth/test_apps/sdkconfig.defaults b/components/esp_eth/test_apps/test_app_driver/sdkconfig.defaults similarity index 100% rename from components/esp_eth/test_apps/sdkconfig.defaults rename to components/esp_eth/test_apps/test_app_driver/sdkconfig.defaults diff --git a/components/esp_eth/test_apps/test_app_sublayer/CMakeLists.txt b/components/esp_eth/test_apps/test_app_sublayer/CMakeLists.txt new file mode 100644 index 00000000000..01950fac901 --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/CMakeLists.txt @@ -0,0 +1,5 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(esp_eth_sublayer_test) diff --git a/components/esp_eth/test_apps/test_app_sublayer/README.md b/components/esp_eth/test_apps/test_app_sublayer/README.md new file mode 100644 index 00000000000..4873c15b15b --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/README.md @@ -0,0 +1,2 @@ +| Supported Targets | ESP32 | ESP32-P4 | +| ----------------- | ----- | -------- | diff --git a/components/esp_eth/test_apps/test_app_sublayer/main/CMakeLists.txt b/components/esp_eth/test_apps/test_app_sublayer/main/CMakeLists.txt new file mode 100644 index 00000000000..13a70a234cf --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/main/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register(SRCS "test_main.c" + "test_sublayer_common.c" + "test_sublayer_events.c" + "test_sublayer_hooks.c" + INCLUDE_DIRS "." + PRIV_REQUIRES unity esp_eth esp_netif esp_event lwip + WHOLE_ARCHIVE) diff --git a/components/esp_eth/test_apps/test_app_sublayer/main/idf_component.yml b/components/esp_eth/test_apps/test_app_sublayer/main/idf_component.yml new file mode 100644 index 00000000000..b187568955c --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/main/idf_component.yml @@ -0,0 +1,4 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/ethernet_init: + version: "^1.3.0" diff --git a/components/esp_eth/test_apps/test_app_sublayer/main/test_main.c b/components/esp_eth/test_apps/test_app_sublayer/main/test_main.c new file mode 100644 index 00000000000..687abcdfbf5 --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/main/test_main.c @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "unity.h" +#include "unity_test_utils_memory.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "sdkconfig.h" +#include "test_sublayer_common.h" + +#ifndef CONFIG_ETH_SUBLAYER_SUPPORT +#error "This test app requires CONFIG_ETH_SUBLAYER_SUPPORT" +#endif + +#define TEST_TASK_STACK_SIZE 8192 +#define TEST_TASK_PRIORITY 5 +/* Some resources are lazy allocated in lwIP which we don't have under control */ +#define TEST_MEMORY_LEAK_THRESHOLD 400 + +void setUp(void) +{ + unity_utils_record_free_mem(); +} + +void tearDown(void) +{ + sublayer_test_force_teardown(); + unity_utils_evaluate_leaks_direct(TEST_MEMORY_LEAK_THRESHOLD); +} + +static void test_task(void *pvParameters) +{ + unity_run_menu(); +} + +void app_main(void) +{ + xTaskCreatePinnedToCore(test_task, "testTask", TEST_TASK_STACK_SIZE, + NULL, TEST_TASK_PRIORITY, NULL, tskNO_AFFINITY); +} diff --git a/components/esp_eth/test_apps/test_app_sublayer/main/test_sublayer_common.c b/components/esp_eth/test_apps/test_app_sublayer/main/test_sublayer_common.c new file mode 100644 index 00000000000..9be88fcaa08 --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/main/test_sublayer_common.c @@ -0,0 +1,196 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include "test_sublayer_common.h" +#include "esp_vfs_l2tap.h" +#include "esp_log.h" +#include "unity.h" +#include "ethernet_init.h" + +static const char *TAG = "sublayer_test_common"; + +static sublayer_test_ctx_t *s_active_ctx; + +/* ------------------------------------------------------------------ */ +/* ETH event handler */ +/* ------------------------------------------------------------------ */ + +static void eth_event_handler(void *arg, esp_event_base_t base, + int32_t event_id, void *event_data) +{ + EventGroupHandle_t eg = (EventGroupHandle_t)arg; + (void)base; + (void)event_data; + switch (event_id) { + case ETHERNET_EVENT_START: + xEventGroupSetBits(eg, SUBLAYER_TEST_ETH_START_BIT); + break; + case ETHERNET_EVENT_CONNECTED: + xEventGroupSetBits(eg, SUBLAYER_TEST_ETH_CONNECT_BIT); + break; + case ETHERNET_EVENT_STOP: + xEventGroupSetBits(eg, SUBLAYER_TEST_ETH_STOP_BIT); + break; + default: + break; + } +} + +/* ------------------------------------------------------------------ */ +/* Granular init helpers */ +/* ------------------------------------------------------------------ */ + +void sublayer_test_create_event_loop(sublayer_test_ctx_t *ctx) +{ + TEST_ESP_OK(esp_netif_init()); + + if (esp_event_loop_create_default() == ESP_ERR_INVALID_STATE) { + esp_event_loop_delete_default(); + TEST_ESP_OK(esp_event_loop_create_default()); + } + + ctx->eg = xEventGroupCreate(); + TEST_ASSERT_NOT_NULL(ctx->eg); + + TEST_ESP_OK(esp_event_handler_instance_register(ETH_EVENT, ESP_EVENT_ANY_ID, + eth_event_handler, ctx->eg, + &ctx->eth_evt_inst)); +} + +void sublayer_test_init_ethernet(sublayer_test_ctx_t *ctx) +{ + TEST_ESP_OK(ethernet_init_all(&ctx->eth_handles, &ctx->eth_cnt)); + TEST_ASSERT_GREATER_THAN(0, ctx->eth_cnt); +} + +void sublayer_test_register_l2tap(sublayer_test_ctx_t *ctx) +{ + esp_err_t ret = esp_vfs_l2tap_intf_register(NULL); + if (ret == ESP_ERR_INVALID_STATE) { + esp_vfs_l2tap_intf_unregister(NULL); + TEST_ESP_OK(esp_vfs_l2tap_intf_register(NULL)); + } else { + TEST_ESP_OK(ret); + } + ctx->l2tap_registered = true; +} + +esp_netif_t *sublayer_test_create_eth_netif(sublayer_test_ctx_t *ctx, + const char *if_key, + const char *if_desc) +{ + esp_netif_inherent_config_t base_cfg = ESP_NETIF_INHERENT_DEFAULT_ETH(); + base_cfg.if_key = if_key; + base_cfg.if_desc = if_desc; + esp_netif_config_t netif_cfg = { + .base = &base_cfg, + .driver = NULL, + .stack = ESP_NETIF_NETSTACK_DEFAULT_ETH, + }; + esp_netif_t *netif = esp_netif_new(&netif_cfg); + TEST_ASSERT_NOT_NULL(netif); + + TEST_ASSERT_LESS_THAN(SUBLAYER_TEST_MAX_NETIFS, ctx->netif_count); + ctx->netifs[ctx->netif_count++] = netif; + return netif; +} + +void sublayer_test_start_and_wait_connect(sublayer_test_ctx_t *ctx) +{ + bool loopback_en = true; + esp_eth_ioctl(ctx->eth_handles[0], ETH_CMD_S_PHY_LOOPBACK, &loopback_en); + + TEST_ESP_OK(esp_eth_start(ctx->eth_handles[0])); + + EventBits_t bits; + bits = xEventGroupWaitBits(ctx->eg, SUBLAYER_TEST_ETH_START_BIT, pdTRUE, pdTRUE, + pdMS_TO_TICKS(SUBLAYER_TEST_ETH_START_TIMEOUT_MS)); + TEST_ASSERT_BITS(SUBLAYER_TEST_ETH_START_BIT, SUBLAYER_TEST_ETH_START_BIT, bits); + + bits = xEventGroupWaitBits(ctx->eg, SUBLAYER_TEST_ETH_CONNECT_BIT, pdTRUE, pdTRUE, + pdMS_TO_TICKS(SUBLAYER_TEST_ETH_CONNECT_TIMEOUT_MS)); + TEST_ASSERT_BITS(SUBLAYER_TEST_ETH_CONNECT_BIT, SUBLAYER_TEST_ETH_CONNECT_BIT, bits); + + vTaskDelay(pdMS_TO_TICKS(SUBLAYER_TEST_EVENT_PROPAGATION_MS)); +} + +/* ------------------------------------------------------------------ */ +/* Teardown */ +/* ------------------------------------------------------------------ */ + +void sublayer_test_teardown(sublayer_test_ctx_t *ctx) +{ + if (ctx->eth_handles != NULL && ctx->eth_cnt > 0) { + esp_eth_stop(ctx->eth_handles[0]); + if (ctx->eg != NULL) { + xEventGroupWaitBits(ctx->eg, SUBLAYER_TEST_ETH_STOP_BIT, pdTRUE, pdTRUE, + pdMS_TO_TICKS(SUBLAYER_TEST_ETH_STOP_TIMEOUT_MS)); + } + vTaskDelay(pdMS_TO_TICKS(100)); + } + + for (int i = 0; i < ctx->netif_count; i++) { + if (ctx->netifs[i] != NULL) { + esp_netif_destroy(ctx->netifs[i]); + ctx->netifs[i] = NULL; + } + } + ctx->netif_count = 0; + + if (ctx->sub != NULL) { + esp_eth_sublayer_del(ctx->sub); + ctx->sub = NULL; + } + + if (ctx->eth_evt_inst != NULL) { + esp_event_handler_instance_unregister(ETH_EVENT, ESP_EVENT_ANY_ID, ctx->eth_evt_inst); + ctx->eth_evt_inst = NULL; + } + + if (ctx->eth_handles != NULL) { + ethernet_deinit_all(ctx->eth_handles); + ctx->eth_handles = NULL; + ctx->eth_cnt = 0; + } + + if (ctx->l2tap_registered) { + esp_vfs_l2tap_intf_unregister(NULL); + ctx->l2tap_registered = false; + } + + if (ctx->eg != NULL) { + vEventGroupDelete(ctx->eg); + ctx->eg = NULL; + } + + esp_event_loop_delete_default(); +} + +/* ------------------------------------------------------------------ */ +/* Force-teardown safety net */ +/* ------------------------------------------------------------------ */ + +void sublayer_test_register_ctx(sublayer_test_ctx_t *ctx) +{ + s_active_ctx = ctx; +} + +void sublayer_test_unregister_ctx(void) +{ + s_active_ctx = NULL; +} + +void sublayer_test_force_teardown(void) +{ + if (s_active_ctx == NULL) { + return; + } + + ESP_LOGW(TAG, "Force teardown triggered — cleaning up after test failure"); + sublayer_test_teardown(s_active_ctx); + s_active_ctx = NULL; +} diff --git a/components/esp_eth/test_apps/test_app_sublayer/main/test_sublayer_common.h b/components/esp_eth/test_apps/test_app_sublayer/main/test_sublayer_common.h new file mode 100644 index 00000000000..f2f9ecf4491 --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/main/test_sublayer_common.h @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#pragma once + +#include "sdkconfig.h" + +#ifndef CONFIG_ETH_SUBLAYER_SUPPORT +#error "This test app requires CONFIG_ETH_SUBLAYER_SUPPORT" +#endif + +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_eth.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define SUBLAYER_TEST_ETH_START_BIT BIT0 +#define SUBLAYER_TEST_ETH_CONNECT_BIT BIT1 +#define SUBLAYER_TEST_ETH_STOP_BIT BIT2 + +#define SUBLAYER_TEST_ETH_START_TIMEOUT_MS 10000 +#define SUBLAYER_TEST_ETH_CONNECT_TIMEOUT_MS 30000 +#define SUBLAYER_TEST_ETH_STOP_TIMEOUT_MS 10000 +#define SUBLAYER_TEST_EVENT_PROPAGATION_MS 500 + +#define SUBLAYER_TEST_MAX_NETIFS 4 + +/** + * Base test context shared by all sublayer test modules. + * Embed as the FIRST member of test-specific context structs. + */ +typedef struct { + esp_eth_handle_t *eth_handles; + uint8_t eth_cnt; + EventGroupHandle_t eg; + esp_eth_sublayer_handle_t sub; + esp_event_handler_instance_t eth_evt_inst; + esp_netif_t *netifs[SUBLAYER_TEST_MAX_NETIFS]; + uint8_t netif_count; + bool l2tap_registered; +} sublayer_test_ctx_t; + +/** + * Create the default event loop and register an ETH event handler + * that signals START/CONNECT/STOP bits on ctx->eg. + */ +void sublayer_test_create_event_loop(sublayer_test_ctx_t *ctx); + +/** + * Initialize all available Ethernet drivers (PHY + MAC). + * Populates ctx->eth_handles and ctx->eth_cnt. + */ +void sublayer_test_init_ethernet(sublayer_test_ctx_t *ctx); + +/** + * Register L2TAP VFS interface (/dev/net/tap). + * Sets ctx->l2tap_registered so teardown knows to unregister. + */ +void sublayer_test_register_l2tap(sublayer_test_ctx_t *ctx); + +/** + * Create an esp_netif with custom if_key/if_desc and track it in + * ctx->netifs[] for automatic cleanup during teardown. + */ +esp_netif_t *sublayer_test_create_eth_netif(sublayer_test_ctx_t *ctx, + const char *if_key, + const char *if_desc); + +/** + * Enable PHY loopback, start ETH driver, and wait for START + CONNECT events. + */ +void sublayer_test_start_and_wait_connect(sublayer_test_ctx_t *ctx); + +/** + * Graceful teardown: stop ETH, destroy tracked netifs, delete sublayer, + * unregister events, deinit ETH drivers, unregister L2TAP, delete event loop. + */ +void sublayer_test_teardown(sublayer_test_ctx_t *ctx); + +/** + * Register the active test context for the force-teardown safety net. + */ +void sublayer_test_register_ctx(sublayer_test_ctx_t *ctx); + +/** + * Clear the registered active context (call at end of normal teardown). + */ +void sublayer_test_unregister_ctx(void); + +/** + * Force-teardown safety net called from Unity tearDown() — cleans up + * after a test failure regardless of which test module was active. + */ +void sublayer_test_force_teardown(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_eth/test_apps/test_app_sublayer/main/test_sublayer_events.c b/components/esp_eth/test_apps/test_app_sublayer/main/test_sublayer_events.c new file mode 100644 index 00000000000..8a0a20bf07c --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/main/test_sublayer_events.c @@ -0,0 +1,210 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/* + * Target tests for the Ethernet sublayer event forwarding logic. + * + * Tested behaviour: + * - Without a connect-confirm event the sublayer forwards + * ETHERNET_EVENT_CONNECTED straight to esp_netif_action_connected(), + * bringing the netif up immediately. + * - With a connect-confirm event the netif stays down after + * ETHERNET_EVENT_CONNECTED and only comes up when the custom + * confirm event is posted. + * - ETHERNET_EVENT_DISCONNECTED brings the netif down. + * - A custom disconnect-trigger event also brings the netif down. + * + * PHY loopback mode is enabled so the tests are self-contained and do + * not require a physical cable. + */ + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_eth.h" +#include "esp_log.h" +#include "unity.h" +#include "ethernet_init.h" +#include "test_sublayer_common.h" + +static const char *TAG = "sublayer_events_test"; + +#define EVENT_PROPAGATION_MS 200 + +ESP_EVENT_DEFINE_BASE(TEST_SUBLAYER_EVENT); +enum { + TEST_CONFIRM_EVENT_ID = 0, + TEST_DISCONNECT_EVENT_ID = 1, +}; + +/* ------------------------------------------------------------------ */ +/* Test context and helpers */ +/* ------------------------------------------------------------------ */ + +typedef struct { + sublayer_test_ctx_t base; + esp_netif_t *netif; + esp_eth_sublayer_vlan_handle_t vlan_hdl; +} events_test_ctx_t; + +static void test_setup(events_test_ctx_t *ctx, const esp_eth_sublayer_config_t *sub_cfg_template) +{ + memset(ctx, 0, sizeof(*ctx)); + + sublayer_test_create_event_loop(&ctx->base); + sublayer_test_init_ethernet(&ctx->base); + sublayer_test_register_ctx(&ctx->base); + + esp_eth_sublayer_config_t cfg = *sub_cfg_template; + cfg.eth_handle = ctx->base.eth_handles[0]; + TEST_ESP_OK(esp_eth_sublayer_new(&cfg, &ctx->base.sub)); + + TEST_ESP_OK(esp_eth_sublayer_vlan_add(ctx->base.sub, ESP_ETH_SUBLAYER_UNTAGGED_VID, &ctx->vlan_hdl)); + + ctx->netif = sublayer_test_create_eth_netif(&ctx->base, "ETH_DEF", "eth"); + TEST_ESP_OK(esp_netif_attach(ctx->netif, ctx->vlan_hdl)); +} + +static void test_start_and_wait_connect(events_test_ctx_t *ctx) +{ + sublayer_test_start_and_wait_connect(&ctx->base); +} + +static void test_teardown(events_test_ctx_t *ctx) +{ + sublayer_test_teardown(&ctx->base); + sublayer_test_unregister_ctx(); +} + +/* ------------------------------------------------------------------ */ +/* Test cases */ +/* ------------------------------------------------------------------ */ + +TEST_CASE("connect without confirm event sets link up", "[sublayer_events]") +{ + ESP_LOGI(TAG, "--- no confirm event: CONNECTED should bring netif up immediately ---"); + + esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT(); + + events_test_ctx_t ctx; + test_setup(&ctx, &sub_cfg); + test_start_and_wait_connect(&ctx); + + TEST_ASSERT_TRUE_MESSAGE(esp_netif_is_netif_up(ctx.netif), + "netif must be up after CONNECTED (no confirm event)"); + + test_teardown(&ctx); +} + +TEST_CASE("connect with confirm event defers link up", "[sublayer_events]") +{ + ESP_LOGI(TAG, "--- with confirm event: CONNECTED alone must NOT bring netif up ---"); + + esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT(); + sub_cfg.connect_confirm_event = (esp_eth_sublayer_event_t){ + .base = TEST_SUBLAYER_EVENT, + .event_id = TEST_CONFIRM_EVENT_ID, + }; + + events_test_ctx_t ctx; + test_setup(&ctx, &sub_cfg); + test_start_and_wait_connect(&ctx); + + TEST_ASSERT_FALSE_MESSAGE(esp_netif_is_netif_up(ctx.netif), + "netif must stay down after CONNECTED when confirm event is configured"); + + ESP_LOGI(TAG, "--- posting confirm event ---"); + TEST_ESP_OK(esp_event_post(TEST_SUBLAYER_EVENT, TEST_CONFIRM_EVENT_ID, + NULL, 0, portMAX_DELAY)); + vTaskDelay(pdMS_TO_TICKS(EVENT_PROPAGATION_MS)); + + TEST_ASSERT_TRUE_MESSAGE(esp_netif_is_netif_up(ctx.netif), + "netif must be up after confirm event"); + + test_teardown(&ctx); +} + +TEST_CASE("confirm event prior connect sets link up", "[sublayer_events]") +{ + ESP_LOGI(TAG, "--- confirm event posted before CONNECTED: netif must come up on CONNECTED ---"); + + esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT(); + sub_cfg.connect_confirm_event = (esp_eth_sublayer_event_t){ + .base = TEST_SUBLAYER_EVENT, + .event_id = TEST_CONFIRM_EVENT_ID, + }; + + events_test_ctx_t ctx; + test_setup(&ctx, &sub_cfg); + + ESP_LOGI(TAG, "--- posting confirm event while the physical link is down ---"); + TEST_ESP_OK(esp_event_post(TEST_SUBLAYER_EVENT, TEST_CONFIRM_EVENT_ID, + NULL, 0, portMAX_DELAY)); + vTaskDelay(pdMS_TO_TICKS(EVENT_PROPAGATION_MS)); + + TEST_ASSERT_FALSE_MESSAGE(esp_netif_is_netif_up(ctx.netif), + "netif must stay down when only the confirm event was received"); + + test_start_and_wait_connect(&ctx); + + TEST_ASSERT_TRUE_MESSAGE(esp_netif_is_netif_up(ctx.netif), + "netif must be up after CONNECTED when the confirm event was received before"); + + test_teardown(&ctx); +} + +TEST_CASE("eth disconnect event sets link down", "[sublayer_events]") +{ + ESP_LOGI(TAG, "--- ETHERNET_EVENT_DISCONNECTED should bring netif down ---"); + + esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT(); + + events_test_ctx_t ctx; + test_setup(&ctx, &sub_cfg); + test_start_and_wait_connect(&ctx); + + TEST_ASSERT_TRUE_MESSAGE(esp_netif_is_netif_up(ctx.netif), + "precondition: netif must be up before disconnect test"); + + esp_eth_handle_t hdl = ctx.base.eth_handles[0]; + TEST_ESP_OK(esp_event_post(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED, + &hdl, sizeof(hdl), portMAX_DELAY)); + vTaskDelay(pdMS_TO_TICKS(EVENT_PROPAGATION_MS)); + + TEST_ASSERT_FALSE_MESSAGE(esp_netif_is_netif_up(ctx.netif), + "netif must be down after ETHERNET_EVENT_DISCONNECTED"); + + test_teardown(&ctx); +} + +TEST_CASE("custom disconnect trigger sets link down", "[sublayer_events]") +{ + ESP_LOGI(TAG, "--- custom disconnect trigger should bring netif down ---"); + + esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT(); + sub_cfg.disconnect_trigger_event = (esp_eth_sublayer_event_t){ + .base = TEST_SUBLAYER_EVENT, + .event_id = TEST_DISCONNECT_EVENT_ID, + }; + + events_test_ctx_t ctx; + test_setup(&ctx, &sub_cfg); + test_start_and_wait_connect(&ctx); + + TEST_ASSERT_TRUE_MESSAGE(esp_netif_is_netif_up(ctx.netif), + "precondition: netif must be up before disconnect trigger test"); + + TEST_ESP_OK(esp_event_post(TEST_SUBLAYER_EVENT, TEST_DISCONNECT_EVENT_ID, + NULL, 0, portMAX_DELAY)); + vTaskDelay(pdMS_TO_TICKS(EVENT_PROPAGATION_MS)); + + TEST_ASSERT_FALSE_MESSAGE(esp_netif_is_netif_up(ctx.netif), + "netif must be down after custom disconnect trigger"); + + test_teardown(&ctx); +} diff --git a/components/esp_eth/test_apps/test_app_sublayer/main/test_sublayer_hooks.c b/components/esp_eth/test_apps/test_app_sublayer/main/test_sublayer_hooks.c new file mode 100644 index 00000000000..4ccfa88ed8a --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/main/test_sublayer_hooks.c @@ -0,0 +1,757 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/* + * Target tests for Ethernet sublayer TX / RX / post-TX hooks. + * + * PHY loopback mode and L2TAP read/write are used so the tests are + * self-contained. Both untagged and tagged (802.1Q) netifs are + * exercised in the same cases where applicable. + */ + +#include +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_eth.h" +#include "esp_eth_spec.h" +#include "esp_log.h" +#include "esp_vfs_l2tap.h" +#include "unity.h" +#include "ethernet_init.h" +#include "lwip/prot/ethernet.h" +#include "arpa/inet.h" +#include "test_sublayer_common.h" + +static const char *TAG = "sublayer_hooks_test"; + +#define L2TAP_SEND_DELAY_MS 1000 +#define L2TAP_IO_TIMEOUT_MS 5000 + +#define IFKEY_UNTAGGED "ETH_DEF" +#define IFKEY_VLAN "ETH_VLAN" +#define TEST_VLAN_ID 100 + +#define ETH_TYPE_HOOK_UNTAGGED 0x88B5 +#define ETH_TYPE_HOOK_VLAN 0x88B6 +#define ETH_TYPE_HOOK_HEAD 0x88B7 /* Ethertype of the TX hook inserted leading segment */ + +#define TX_PREFIX_HEAD_SEG_LEN (ETH_HEADER_LEN + TX_PREFIX_LEN) + +#define TX_PREFIX_MAGIC_0 0xDE +#define TX_PREFIX_MAGIC_1 0xAD +#define TX_PREFIX_MAGIC_2 0xBE +#define TX_PREFIX_MAGIC_3 0xEF +#define TX_PREFIX_LEN 4 + +#define FRAME_MARKER_TX 0xA5 +#define FRAME_MARKER_RX 0x5A + +/* + * Use large frames so a missed free in a sublayer/hook path exceeds the + * non-zero leak threshold (see TEST_MEMORY_LEAK_THRESHOLD in test_main.c; + * some lwIP resources are lazy-allocated and not under our control). + */ +#define HOOK_TEST_FRAME_LEN 1000 +#define HOOK_TEST_RX_BUF_LEN (HOOK_TEST_FRAME_LEN + TX_PREFIX_LEN) + +typedef enum { + HOOK_TX_OP_NONE = 0, + HOOK_TX_OP_PREFIX, + HOOK_TX_OP_PREFIX_HEAD, + HOOK_TX_OP_MERGE, + HOOK_TX_OP_SKIP, + HOOK_TX_OP_ABORT, + HOOK_TX_OP_POST_TX_FREE, +} hook_tx_op_t; + +typedef enum { + HOOK_RX_OP_NONE = 0, + HOOK_RX_OP_MODIFY, + HOOK_RX_OP_DROP, + HOOK_RX_OP_ABORT, +} hook_rx_op_t; + +typedef enum { + HOOK_DIR_NONE = 0, + HOOK_DIR_TX, + HOOK_DIR_RX, +} hook_dir_t; + +typedef struct { + hook_tx_op_t op; + uint32_t hook_calls; + uint32_t post_hook_calls; + uint32_t rx_strip_calls; /*!< PREFIX_HEAD companion strips on loopback */ + void *freed_ptr; +} hooks_tx_ctx_t; + +typedef struct { + hook_rx_op_t op; + uint32_t hook_calls; +} hooks_rx_ctx_t; + +typedef struct { + hook_dir_t dir; + union { + hooks_tx_ctx_t tx; + hooks_rx_ctx_t rx; + }; +} hooks_ctx_t; + +typedef struct { + struct eth_hdr header; + uint8_t marker; + uint8_t pad[HOOK_TEST_FRAME_LEN - ETH_HEADER_LEN - 1]; +} __attribute__((packed)) hook_test_frame_t; + +_Static_assert(sizeof(hook_test_frame_t) == HOOK_TEST_FRAME_LEN, + "hook_test_frame_t must be HOOK_TEST_FRAME_LEN bytes"); + +typedef struct { + sublayer_test_ctx_t base; + esp_eth_sublayer_vlan_handle_t vlan_untagged; + esp_eth_sublayer_vlan_handle_t vlan_tagged; + hooks_ctx_t *hook_ctx; +} hooks_test_ctx_t; + +static hooks_ctx_t s_hooks_ctx; + +/* ------------------------------------------------------------------ */ +/* Hook implementations */ +/* ------------------------------------------------------------------ */ + +/** + * TX hook used by the sublayer hook tests. The active scenario is selected + * via hooks_ctx_t::tx.op when dir == HOOK_DIR_TX. + * + * Scenarios exercised (see esp_eth_sublayer.h TX hook contract): + * - HOOK_TX_OP_PREFIX: Modify buf pointers/len and increase buf_count — + * insert a 4-byte prefix before payload; on untagged + * frames split one descriptor into header + prefixed + * payload (bufs[0]/bufs[1]); on VLAN-tagged frames + * the sublayer already provides two descriptors. + * - HOOK_TX_OP_PREFIX_HEAD: Insert a new custom leading bufs[0] (broadcast DA, + * device SA, ETH_TYPE_HOOK_HEAD, magic payload), shift + * original descriptors down and increase buf_count. + * The RX hook strips that segment on loopback by + * recognizing ETH_TYPE_HOOK_HEAD. + * - HOOK_TX_OP_MERGE: Merge scattered segments into one contiguous bufs[0] + * (synthetic split for untagged, native VLAN split + * for tagged); buf_count reduced to 1. + * - HOOK_TX_OP_SKIP: Set buf_count to 0 — driver transmit and post_tx_hook + * are skipped; tx_bufs remains caller-owned. + * - HOOK_TX_OP_ABORT: Return non-ESP_OK — transmit aborted, post_tx_hook + * not called. + * - HOOK_TX_OP_POST_TX_FREE: Replace bufs[0] with a malloc'd copy so that + * post_tx_hook can free hook-owned memory after TX. + */ +static void test_fill_prefix_magics(uint8_t *dst) +{ + dst[0] = TX_PREFIX_MAGIC_0; + dst[1] = TX_PREFIX_MAGIC_1; + dst[2] = TX_PREFIX_MAGIC_2; + dst[3] = TX_PREFIX_MAGIC_3; +} + +static bool test_prefix_magics_match(const uint8_t *src) +{ + uint8_t expected[TX_PREFIX_LEN]; + + test_fill_prefix_magics(expected); + return memcmp(src, expected, TX_PREFIX_LEN) == 0; +} + +static esp_err_t test_tx_hook(esp_eth_handle_t eth, esp_eth_sublayer_tx_bufs_t *tx_bufs, void *ctx) +{ + hooks_ctx_t *hctx = (hooks_ctx_t *)ctx; + + if (hctx->dir != HOOK_DIR_TX) { + return ESP_OK; + } + hctx->tx.hook_calls++; + + esp_eth_buf_desc_t *bufs = tx_bufs->bufs; + size_t *buf_count = tx_bufs->buf_count; + size_t buf_capacity = tx_bufs->buf_capacity; + + switch (hctx->tx.op) { + case HOOK_TX_OP_PREFIX: { + /* Change buf pointers/len and bump buf_count (untagged) or edit bufs[1] (tagged). */ + if (*buf_count < 1 || buf_capacity < 2) { + return ESP_ERR_INVALID_SIZE; + } + if (*buf_count >= 2 && bufs[0].len == ETH_HEADER_LEN + ETH_VLAN_TAG_LEN) { + /* Tagged VLAN path: header already in bufs[0], prefix payload in bufs[1]. */ + size_t payload_len = bufs[1].len; + uint8_t *prefixed = malloc(TX_PREFIX_LEN + payload_len); + if (prefixed == NULL) { + return ESP_ERR_NO_MEM; + } + test_fill_prefix_magics(prefixed); + memcpy(prefixed + TX_PREFIX_LEN, bufs[1].buf, payload_len); + bufs[1] = (esp_eth_buf_desc_t){ .buf = prefixed, .len = TX_PREFIX_LEN + payload_len }; + return ESP_OK; + } + /* Untagged path: single descriptor — shrink bufs[0] to header, add prefixed bufs[1]. */ + size_t payload_len = bufs[0].len - ETH_HEADER_LEN; + uint8_t *prefixed = malloc(TX_PREFIX_LEN + payload_len); + if (prefixed == NULL) { + return ESP_ERR_NO_MEM; + } + test_fill_prefix_magics(prefixed); + memcpy(prefixed + TX_PREFIX_LEN, bufs[0].buf + ETH_HEADER_LEN, payload_len); + bufs[0].len = ETH_HEADER_LEN; + bufs[1] = (esp_eth_buf_desc_t){ .buf = prefixed, .len = TX_PREFIX_LEN + payload_len }; + *buf_count = 2; + return ESP_OK; + } + case HOOK_TX_OP_PREFIX_HEAD: { + /* + * Insert a standalone leading Ethernet frame at bufs[0] (any ethertype + * is fine on TX). Shift the original frame descriptors down and bump + * buf_count. RX hook strips this segment before VLAN demux / L2TAP. + */ + if (*buf_count < 1 || buf_capacity < *buf_count + 1) { + return ESP_ERR_INVALID_SIZE; + } + uint8_t *head = malloc(TX_PREFIX_HEAD_SEG_LEN); + if (head == NULL) { + return ESP_ERR_NO_MEM; + } + memset(head, 0xFF, ETH_ADDR_LEN); + esp_eth_ioctl(eth, ETH_CMD_G_MAC_ADDR, head + ETH_ADDR_LEN); + head[ETH_HEADER_LEN - sizeof(uint16_t)] = (uint8_t)(ETH_TYPE_HOOK_HEAD >> 8); + head[ETH_HEADER_LEN - sizeof(uint16_t) + 1] = (uint8_t)ETH_TYPE_HOOK_HEAD; + test_fill_prefix_magics(head + ETH_HEADER_LEN); + for (size_t i = *buf_count; i > 0; i--) { + bufs[i] = bufs[i - 1]; + } + bufs[0] = (esp_eth_buf_desc_t){ .buf = head, .len = TX_PREFIX_HEAD_SEG_LEN }; + *buf_count += 1; + return ESP_OK; + } + case HOOK_TX_OP_MERGE: { + /* Merge scattered segments into one malloc'd bufs[0]; reduce buf_count to 1. */ + if (*buf_count == 1 && bufs[0].len > ETH_HEADER_LEN) { + bufs[1] = (esp_eth_buf_desc_t){ + .buf = bufs[0].buf + ETH_HEADER_LEN, + .len = bufs[0].len - ETH_HEADER_LEN, + }; + bufs[0].len = ETH_HEADER_LEN; + *buf_count = 2; + } + if (*buf_count < 2) { + return ESP_ERR_INVALID_SIZE; + } + size_t total = 0; + for (size_t i = 0; i < *buf_count; i++) { + total += bufs[i].len; + } + uint8_t *merged = malloc(total); + if (merged == NULL) { + return ESP_ERR_NO_MEM; + } + size_t off = 0; + for (size_t i = 0; i < *buf_count; i++) { + memcpy(merged + off, bufs[i].buf, bufs[i].len); + off += bufs[i].len; + } + bufs[0] = (esp_eth_buf_desc_t){ .buf = merged, .len = total }; + *buf_count = 1; + return ESP_OK; + } + case HOOK_TX_OP_SKIP: + /* buf_count = 0: skip driver TX and post_tx_hook; tx_bufs stays caller-owned. */ + *buf_count = 0; + return ESP_OK; + case HOOK_TX_OP_ABORT: + /* Non-ESP_OK: abort transmit for this packet; post_tx_hook not called. */ + return ESP_ERR_INVALID_STATE; + case HOOK_TX_OP_POST_TX_FREE: + /* Allocate replacement buffer; post_tx_hook must free bufs[0] after TX. */ + if (*buf_count >= 1) { + uint8_t *copy = malloc(bufs[0].len); + if (copy == NULL) { + return ESP_ERR_NO_MEM; + } + memcpy(copy, bufs[0].buf, bufs[0].len); + copy[ETH_HEADER_LEN + 1] ^= 0x01; + bufs[0].buf = copy; + } + return ESP_OK; + default: + return ESP_OK; + } + (void)eth; +} + +static void test_post_tx_hook(esp_eth_handle_t eth, const esp_eth_sublayer_tx_bufs_t *tx_bufs, void *ctx) +{ + hooks_ctx_t *hctx = (hooks_ctx_t *)ctx; + + if (hctx->dir != HOOK_DIR_TX) { + return; + } + hctx->tx.post_hook_calls++; + + esp_eth_buf_desc_t *bufs = tx_bufs->bufs; + size_t buf_count = *tx_bufs->buf_count; + + switch (hctx->tx.op) { + case HOOK_TX_OP_PREFIX: + if (buf_count >= 2) { + hctx->tx.freed_ptr = bufs[1].buf; + free(bufs[1].buf); + } + break; + case HOOK_TX_OP_PREFIX_HEAD: + if (buf_count >= 1) { + hctx->tx.freed_ptr = bufs[0].buf; + free(bufs[0].buf); + } + break; + case HOOK_TX_OP_MERGE: + case HOOK_TX_OP_POST_TX_FREE: + if (buf_count >= 1) { + hctx->tx.freed_ptr = bufs[0].buf; + free(bufs[0].buf); + } + break; + default: + break; + } + (void)eth; +} + +static esp_err_t test_rx_hook(esp_eth_handle_t eth, uint8_t **buf, uint32_t *len, + esp_eth_sublayer_rx_info_t *info, void *ctx) +{ + hooks_ctx_t *hctx = (hooks_ctx_t *)ctx; + + if (hctx->dir == HOOK_DIR_TX) { + /* + * TX PREFIX_HEAD companion: strip the custom leading segment on loopback + * before VLAN demux / L2TAP. Counted on the TX context because the op + * union holds tx while dir == HOOK_DIR_TX. + */ + if (*len >= TX_PREFIX_HEAD_SEG_LEN) { + uint16_t etype = (uint16_t)(((*buf)[12] << 8) | (*buf)[13]); + if (etype == ETH_TYPE_HOOK_HEAD) { + if (!test_prefix_magics_match(*buf + ETH_HEADER_LEN)) { + return ESP_ERR_INVALID_ARG; + } + hctx->tx.rx_strip_calls++; + *buf += TX_PREFIX_HEAD_SEG_LEN; + *len -= TX_PREFIX_HEAD_SEG_LEN; + return ESP_OK; + } + } + return ESP_OK; + } + + if (hctx->dir != HOOK_DIR_RX) { + return ESP_OK; + } + hctx->rx.hook_calls++; + + switch (hctx->rx.op) { + case HOOK_RX_OP_MODIFY: + if (*len > ETH_HEADER_LEN + 1) { + size_t marker_off = ETH_HEADER_LEN; + if (*len > ETH_HEADER_LEN + ETH_VLAN_TAG_LEN + 1 && + ((uint16_t)(((*buf)[12] << 8) | (*buf)[13]) == ETH_T_8021Q)) { + marker_off = ETH_HEADER_LEN + ETH_VLAN_TAG_LEN; + } + if (marker_off + 1 < *len && (*buf)[marker_off] == FRAME_MARKER_TX) { + (*buf)[marker_off] = FRAME_MARKER_RX; + } + } + return ESP_OK; + case HOOK_RX_OP_DROP: + *len = 0; + // rx hook took the buffer, so we need to free it + free(info->l2_buffer); + info->l2_buffer = NULL; + return ESP_OK; + case HOOK_RX_OP_ABORT: + return ESP_ERR_INVALID_STATE; + default: + return ESP_OK; + } + (void)eth; + (void)info; +} + +/* ------------------------------------------------------------------ */ +/* Test helpers */ +/* ------------------------------------------------------------------ */ + +static int l2tap_open_filtered(const char *if_key, uint16_t ethertype_filter) +{ + int fd = open(L2TAP_VFS_DEFAULT_PATH, O_NONBLOCK); + TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, fd, "failed to open L2TAP device"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, ioctl(fd, L2TAP_S_INTF_DEVICE, if_key), + "failed to bind L2TAP fd to interface"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, ioctl(fd, L2TAP_S_RCV_FILTER, ðertype_filter), + "failed to set L2TAP ethertype filter"); + return fd; +} + +static int l2tap_open_tx(const char *if_key) +{ + int fd = open(L2TAP_VFS_DEFAULT_PATH, O_NONBLOCK); + TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, fd, "failed to open L2TAP device"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, ioctl(fd, L2TAP_S_INTF_DEVICE, if_key), + "failed to bind L2TAP fd to interface"); + return fd; +} + +static void test_build_frame(hook_test_frame_t *frame, esp_eth_handle_t eth, + uint16_t ethertype, uint8_t marker) +{ + memset(frame, 0, sizeof(*frame)); + esp_eth_ioctl(eth, ETH_CMD_G_MAC_ADDR, frame->header.src.addr); + memcpy(frame->header.dest.addr, frame->header.src.addr, ETH_ADDR_LEN); + frame->header.type = htons(ethertype); + frame->marker = marker; +} + +static void test_setup(hooks_test_ctx_t *ctx) +{ + memset(ctx, 0, sizeof(*ctx)); + memset(&s_hooks_ctx, 0, sizeof(s_hooks_ctx)); + ctx->hook_ctx = &s_hooks_ctx; + + sublayer_test_create_event_loop(&ctx->base); + sublayer_test_init_ethernet(&ctx->base); + sublayer_test_register_l2tap(&ctx->base); + sublayer_test_register_ctx(&ctx->base); + + esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT(); + sub_cfg.eth_handle = ctx->base.eth_handles[0]; + sub_cfg.tx_hook = test_tx_hook; + sub_cfg.post_tx_hook = test_post_tx_hook; + sub_cfg.rx_hook = test_rx_hook; + sub_cfg.hook_ctx = ctx->hook_ctx; + TEST_ESP_OK(esp_eth_sublayer_new(&sub_cfg, &ctx->base.sub)); + + TEST_ESP_OK(esp_eth_sublayer_vlan_add(ctx->base.sub, ESP_ETH_SUBLAYER_UNTAGGED_VID, &ctx->vlan_untagged)); + TEST_ESP_OK(esp_eth_sublayer_vlan_add(ctx->base.sub, TEST_VLAN_ID, &ctx->vlan_tagged)); + + esp_netif_t *netif_untagged = sublayer_test_create_eth_netif(&ctx->base, IFKEY_UNTAGGED, "eth"); + esp_netif_t *netif_vlan = sublayer_test_create_eth_netif(&ctx->base, IFKEY_VLAN, "eth_vlan"); + + TEST_ESP_OK(esp_netif_attach(netif_untagged, ctx->vlan_untagged)); + TEST_ESP_OK(esp_netif_attach(netif_vlan, ctx->vlan_tagged)); + + sublayer_test_start_and_wait_connect(&ctx->base); +} + +static void test_teardown(hooks_test_ctx_t *ctx) +{ + sublayer_test_teardown(&ctx->base); + sublayer_test_unregister_ctx(); +} + +static bool verify_loopback_frame(const uint8_t *rx_buf, size_t rx_len, + const hook_test_frame_t *tx_frame) +{ + return rx_len >= sizeof(*tx_frame) && + memcmp(rx_buf, tx_frame, sizeof(*tx_frame)) == 0; +} + +typedef struct { + const char *if_key; + const hook_test_frame_t *frame; +} l2tap_send_arg_t; + +static void l2tap_send_task(void *arg) +{ + l2tap_send_arg_t *send_arg = (l2tap_send_arg_t *)arg; + vTaskDelay(pdMS_TO_TICKS(L2TAP_SEND_DELAY_MS)); + int tx_fd = l2tap_open_tx(send_arg->if_key); + ssize_t n = write(tx_fd, send_arg->frame, sizeof(*send_arg->frame)); + TEST_ASSERT_NOT_EQUAL(-1, n); + close(tx_fd); + vTaskDelete(NULL); +} + +static bool l2tap_wait_for_frame_timeout(int fd, uint8_t *rx_buf, size_t rx_buf_size, + size_t *rx_len, int timeout_ms) +{ + struct timeval tv = { + .tv_sec = timeout_ms / 1000, + .tv_usec = (timeout_ms % 1000) * 1000, + }; + fd_set rfds; + + FD_ZERO(&rfds); + FD_SET(fd, &rfds); + + int ret = select(fd + 1, &rfds, NULL, NULL, &tv); + if (ret <= 0 || !FD_ISSET(fd, &rfds)) { + return false; + } + + ssize_t n = read(fd, rx_buf, rx_buf_size); + if (n <= 0) { + return false; + } + *rx_len = (size_t)n; + return true; +} + +static bool l2tap_wait_for_frame(int fd, uint8_t *rx_buf, size_t rx_buf_size, size_t *rx_len) +{ + return l2tap_wait_for_frame_timeout(fd, rx_buf, rx_buf_size, rx_len, L2TAP_IO_TIMEOUT_MS); +} + +static bool verify_frame_prefix(const uint8_t *rx_buf, size_t rx_len, + const hook_test_frame_t *tx_frame) +{ + size_t expected_len = sizeof(*tx_frame) + TX_PREFIX_LEN; + if (rx_len < expected_len) { + return false; + } + if (memcmp(rx_buf, tx_frame, ETH_HEADER_LEN) != 0) { + return false; + } + if (!test_prefix_magics_match(rx_buf + ETH_HEADER_LEN)) { + return false; + } + return memcmp(rx_buf + ETH_HEADER_LEN + TX_PREFIX_LEN, + ((const uint8_t *)tx_frame) + ETH_HEADER_LEN, + sizeof(*tx_frame) - ETH_HEADER_LEN) == 0; +} + +/* Send a frame and verify the TX PREFIX_HEAD rewrite (new leading buf) is + * delivered intact to L2TAP via EMAC loopback. */ +static void test_tx_prefix_head_on_interface(hooks_test_ctx_t *ctx, const char *if_key, + uint16_t ethertype) +{ + hook_test_frame_t tx_frame; + test_build_frame(&tx_frame, ctx->base.eth_handles[0], ethertype, FRAME_MARKER_TX); + + int rx_fd = l2tap_open_filtered(if_key, ethertype); + l2tap_send_arg_t send_arg = { .if_key = if_key, .frame = &tx_frame }; + xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL); + + uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN]; + size_t rx_len = 0; + bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len); + bool frame_ok = got_frame && verify_loopback_frame(rx_buf, rx_len, &tx_frame); + close(rx_fd); + vTaskDelay(pdMS_TO_TICKS(50)); + TEST_ASSERT_TRUE(got_frame); + TEST_ASSERT_TRUE(frame_ok); +} + +/* Send a frame and verify the TX PREFIX rewrite (extra segment + ethertype + * tweak) is delivered to L2TAP via EMAC loopback. */ +static void test_tx_prefix_on_interface(hooks_test_ctx_t *ctx, const char *if_key, + uint16_t ethertype) +{ + hook_test_frame_t tx_frame; + test_build_frame(&tx_frame, ctx->base.eth_handles[0], ethertype, FRAME_MARKER_TX); + + int rx_fd = l2tap_open_filtered(if_key, ethertype); + l2tap_send_arg_t send_arg = { .if_key = if_key, .frame = &tx_frame }; + xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL); + + uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN]; + size_t rx_len = 0; + bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len); + bool frame_ok = got_frame && verify_frame_prefix(rx_buf, rx_len, &tx_frame); + close(rx_fd); + vTaskDelay(pdMS_TO_TICKS(50)); + TEST_ASSERT_TRUE(got_frame); + TEST_ASSERT_TRUE(frame_ok); +} + +/* Send a VLAN frame and verify the TX MERGE rewrite (contiguous buffer) is + * delivered to L2TAP via EMAC loopback. */ +static void test_tx_merge_on_vlan(hooks_test_ctx_t *ctx) +{ + hook_test_frame_t tx_frame; + test_build_frame(&tx_frame, ctx->base.eth_handles[0], ETH_TYPE_HOOK_VLAN, FRAME_MARKER_TX); + + int rx_fd = l2tap_open_filtered(IFKEY_VLAN, ETH_TYPE_HOOK_VLAN); + l2tap_send_arg_t send_arg = { .if_key = IFKEY_VLAN, .frame = &tx_frame }; + xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL); + + uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN]; + size_t rx_len = 0; + bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len); + bool frame_ok = got_frame && verify_loopback_frame(rx_buf, rx_len, &tx_frame); + close(rx_fd); + vTaskDelay(pdMS_TO_TICKS(50)); + TEST_ASSERT_TRUE(got_frame); + TEST_ASSERT_TRUE(frame_ok); +} + +/* Send a frame on if_key and assert L2TAP never receives it. + * Used when a TX hook skips/aborts transmit, or an RX hook drops/aborts the + * frame before delivery. expect_write_error selects whether write() itself + * must fail (TX abort) or succeed (skip / RX drop-or-abort). */ +static void test_expect_no_frame_on_interface(hooks_test_ctx_t *ctx, const char *if_key, + uint16_t ethertype, bool expect_write_error) +{ + hook_test_frame_t tx_frame; + test_build_frame(&tx_frame, ctx->base.eth_handles[0], ethertype, FRAME_MARKER_TX); + + int rx_fd = l2tap_open_filtered(if_key, ethertype); + int tx_fd = l2tap_open_tx(if_key); + ssize_t wr = write(tx_fd, &tx_frame, sizeof(tx_frame)); + close(tx_fd); + if (expect_write_error) { + TEST_ASSERT_EQUAL(-1, wr); + } else { + TEST_ASSERT_NOT_EQUAL(-1, wr); + } + + uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN]; + size_t rx_len = 0; + bool got_frame = l2tap_wait_for_frame_timeout(rx_fd, rx_buf, sizeof(rx_buf), &rx_len, 1500); + close(rx_fd); + vTaskDelay(pdMS_TO_TICKS(50)); + TEST_ASSERT_FALSE(got_frame); + (void)ctx; +} + +/* Send a frame and verify the RX hook rewrote the payload marker + * (FRAME_MARKER_TX -> FRAME_MARKER_RX) before L2TAP delivery. */ +static void test_rx_modify_on_interface(hooks_test_ctx_t *ctx, const char *if_key, + uint16_t ethertype) +{ + hook_test_frame_t tx_frame; + test_build_frame(&tx_frame, ctx->base.eth_handles[0], ethertype, FRAME_MARKER_TX); + + int rx_fd = l2tap_open_filtered(if_key, ethertype); + l2tap_send_arg_t send_arg = { .if_key = if_key, .frame = &tx_frame }; + xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL); + + uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN]; + size_t rx_len = 0; + bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len); + bool marker_ok = got_frame && rx_len >= ETH_HEADER_LEN + 1 && + rx_buf[ETH_HEADER_LEN] == FRAME_MARKER_RX; + close(rx_fd); + vTaskDelay(pdMS_TO_TICKS(50)); + TEST_ASSERT_TRUE(got_frame); + TEST_ASSERT_TRUE(marker_ok); +} + +static void test_set_tx_op(hooks_test_ctx_t *ctx, hook_tx_op_t tx_op) +{ + memset(&s_hooks_ctx, 0, sizeof(s_hooks_ctx)); + s_hooks_ctx.dir = HOOK_DIR_TX; + s_hooks_ctx.tx.op = tx_op; + ctx->hook_ctx = &s_hooks_ctx; +} + +static void test_set_rx_op(hooks_test_ctx_t *ctx, hook_rx_op_t rx_op) +{ + memset(&s_hooks_ctx, 0, sizeof(s_hooks_ctx)); + s_hooks_ctx.dir = HOOK_DIR_RX; + s_hooks_ctx.rx.op = rx_op; + ctx->hook_ctx = &s_hooks_ctx; +} + +/* ------------------------------------------------------------------ */ +/* Test cases */ +/* ------------------------------------------------------------------ */ + +TEST_CASE("sublayer tx and post tx hooks", "[sublayer_hooks]") +{ + hooks_test_ctx_t ctx; + test_setup(&ctx); + test_set_tx_op(&ctx, HOOK_TX_OP_PREFIX); + + ESP_LOGI(TAG, "TX hook: insert prefix, move frame to bufs[1]"); + test_tx_prefix_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED); + test_tx_prefix_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.post_hook_calls); + + ESP_LOGI(TAG, "TX hook: insert custom leading frame + reorder; RX hook strips on loopback"); + test_set_tx_op(&ctx, HOOK_TX_OP_PREFIX_HEAD); + test_tx_prefix_head_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED); + test_tx_prefix_head_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.rx_strip_calls); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.post_hook_calls); + + ESP_LOGI(TAG, "TX hook: merge tagged header and payload into one buffer"); + test_set_tx_op(&ctx, HOOK_TX_OP_MERGE); + test_tx_merge_on_vlan(&ctx); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.post_hook_calls); + + ESP_LOGI(TAG, "TX hook: buf_count=0 takes ownership, no loopback frame"); + test_set_tx_op(&ctx, HOOK_TX_OP_SKIP); + test_expect_no_frame_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED, false); + test_expect_no_frame_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN, false); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls); + TEST_ASSERT_EQUAL(0, ctx.hook_ctx->tx.post_hook_calls); + + ESP_LOGI(TAG, "TX hook: non-ESP_OK aborts transmit"); + test_set_tx_op(&ctx, HOOK_TX_OP_ABORT); + test_expect_no_frame_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED, true); + test_expect_no_frame_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN, true); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls); + TEST_ASSERT_EQUAL(0, ctx.hook_ctx->tx.post_hook_calls); + + ESP_LOGI(TAG, "post_tx_hook frees buffer allocated in tx hook"); + test_set_tx_op(&ctx, HOOK_TX_OP_POST_TX_FREE); + hook_test_frame_t tx_frame; + test_build_frame(&tx_frame, ctx.base.eth_handles[0], ETH_TYPE_HOOK_UNTAGGED, FRAME_MARKER_TX); + int rx_fd = l2tap_open_filtered(IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED); + l2tap_send_arg_t send_arg = { .if_key = IFKEY_UNTAGGED, .frame = &tx_frame }; + xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL); + uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN]; + size_t rx_len = 0; + bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len); + close(rx_fd); + vTaskDelay(pdMS_TO_TICKS(50)); + TEST_ASSERT_TRUE(got_frame); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.post_hook_calls); + TEST_ASSERT_NOT_NULL(ctx.hook_ctx->tx.freed_ptr); + + test_teardown(&ctx); +} + +TEST_CASE("sublayer rx hooks", "[sublayer_hooks]") +{ + hooks_test_ctx_t ctx; + test_setup(&ctx); + test_set_rx_op(&ctx, HOOK_RX_OP_MODIFY); + + ESP_LOGI(TAG, "RX hook: modify payload before VLAN demux / L2TAP delivery"); + test_rx_modify_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED); + test_rx_modify_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->rx.hook_calls); + + ESP_LOGI(TAG, "RX hook: len=0 drops frame on untagged and tagged"); + test_set_rx_op(&ctx, HOOK_RX_OP_DROP); + test_expect_no_frame_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED, false); + test_expect_no_frame_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN, false); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->rx.hook_calls); + + ESP_LOGI(TAG, "RX hook: non-ESP_OK aborts processing"); + test_set_rx_op(&ctx, HOOK_RX_OP_ABORT); + test_expect_no_frame_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED, false); + test_expect_no_frame_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN, false); + TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->rx.hook_calls); + + test_teardown(&ctx); +} diff --git a/components/esp_eth/test_apps/test_app_sublayer/pytest_sublayer.py b/components/esp_eth/test_apps/test_app_sublayer/pytest_sublayer.py new file mode 100644 index 00000000000..7dd9af5e4bb --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/pytest_sublayer.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded_idf import IdfDut +from pytest_embedded_idf.utils import idf_parametrize + + +@pytest.mark.eth_ip101 +@pytest.mark.parametrize('config', ['default_generic'], indirect=True) +@idf_parametrize('target', ['esp32'], indirect=['target']) +def test_sublayer_events(dut: IdfDut) -> None: + dut.run_all_single_board_cases(group='sublayer_events', timeout=120) + + +@pytest.mark.eth_ip101 +@pytest.mark.parametrize('config', ['default_generic'], indirect=True) +@idf_parametrize('target', ['esp32'], indirect=['target']) +def test_sublayer_hooks(dut: IdfDut) -> None: + dut.run_all_single_board_cases(group='sublayer_hooks', timeout=120) + + +@pytest.mark.eth_ip101 +@pytest.mark.parametrize('config', ['default_generic_esp32p4'], indirect=True) +@idf_parametrize('target', ['esp32p4'], indirect=['target']) +def test_sublayer_events_esp32p4(dut: IdfDut) -> None: + dut.run_all_single_board_cases(group='sublayer_events', timeout=120) + + +@pytest.mark.eth_ip101 +@pytest.mark.parametrize('config', ['default_generic_esp32p4'], indirect=True) +@idf_parametrize('target', ['esp32p4'], indirect=['target']) +def test_sublayer_hooks_esp32p4(dut: IdfDut) -> None: + dut.run_all_single_board_cases(group='sublayer_hooks', timeout=120) diff --git a/components/esp_eth/test_apps/test_app_sublayer/sdkconfig.ci.default_generic b/components/esp_eth/test_apps/test_app_sublayer/sdkconfig.ci.default_generic new file mode 100644 index 00000000000..2a43df1a395 --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/sdkconfig.ci.default_generic @@ -0,0 +1 @@ +CONFIG_IDF_TARGET="esp32" diff --git a/components/esp_eth/test_apps/test_app_sublayer/sdkconfig.ci.default_generic_esp32p4 b/components/esp_eth/test_apps/test_app_sublayer/sdkconfig.ci.default_generic_esp32p4 new file mode 100644 index 00000000000..bd81009ac8f --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/sdkconfig.ci.default_generic_esp32p4 @@ -0,0 +1 @@ +CONFIG_IDF_TARGET="esp32p4" diff --git a/components/esp_eth/test_apps/test_app_sublayer/sdkconfig.defaults b/components/esp_eth/test_apps/test_app_sublayer/sdkconfig.defaults new file mode 100644 index 00000000000..adcfe11f9f9 --- /dev/null +++ b/components/esp_eth/test_apps/test_app_sublayer/sdkconfig.defaults @@ -0,0 +1,20 @@ +CONFIG_ESP_MAIN_TASK_STACK_SIZE=4096 + +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y +CONFIG_ETH_USE_ESP32_EMAC=y +CONFIG_ESP_TASK_WDT_EN=n + +# interface configuration +CONFIG_ETHERNET_INTERNAL_SUPPORT=y +CONFIG_ETHERNET_PHY_GENERIC=y +CONFIG_ETHERNET_DEFAULT_EVENT_HANDLER=n + +CONFIG_ESP_NETIF_L2_TAP=y +CONFIG_ETH_TRANSMIT_MUTEX=n + +CONFIG_IDF_EXPERIMENTAL_FEATURES=y +CONFIG_ETH_SUBLAYER_SUPPORT=y +CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX=y +CONFIG_ETH_SUBLAYER_TX_BUF_DESC_CAPACITY=3 +CONFIG_ETH_SUBLAYER_VLAN_SUPPORT=y diff --git a/docs/en/api-reference/network/esp_eth.rst b/docs/en/api-reference/network/esp_eth.rst index 83cebcbe294..f19183a92f0 100644 --- a/docs/en/api-reference/network/esp_eth.rst +++ b/docs/en/api-reference/network/esp_eth.rst @@ -577,7 +577,7 @@ The following functions should only be invoked after the Ethernet driver has bee * :cpp:member:`eth_mac_ptp_config_t::roll_type`: Rollover mode (digital or binary) for subseconds register. The binary rollover mode is recommended as it provides a more precise time synchronization. - Time stamps for transmitted and received frames can be accessed via the last argument of the registered :cpp:member:`esp_eth_config_t::stack_input_info` function for the receive path, and via the ``ctrl`` argument of the :cpp:func:`esp_eth_transmit_ctrl_vargs` function for the transmit path. However, a more user-friendly approach to retrieve time stamp information in user space is by utilizing the L2 TAP :ref:`Extended Buffer ` mechanism. + Time stamps for transmitted and received frames can be accessed via the last argument of the registered :cpp:member:`esp_eth_config_t::stack_input_info` function for the receive path, and via the ``ctrl`` argument of the :cpp:func:`esp_eth_transmit_ctrl_bufs` function for the transmit path. However, a more user-friendly approach to retrieve time stamp information in user space is by utilizing the L2 TAP :ref:`Extended Buffer ` mechanism. You have an option to schedule event at precise point in time by registering callback function and configuring a target time when the event is supposed to be fired. Note that the callback function is then called from ISR context so it should be as brief as possible. diff --git a/docs/en/contribute/esp-idf-tests-with-pytest.rst b/docs/en/contribute/esp-idf-tests-with-pytest.rst index 562fae9df58..d9f37218894 100644 --- a/docs/en/contribute/esp-idf-tests-with-pytest.rst +++ b/docs/en/contribute/esp-idf-tests-with-pytest.rst @@ -715,7 +715,7 @@ Mark Flaky Tests Certain test cases are based on Ethernet or Wi-Fi. However, the test may be flaky due to networking issues. Thus, it is possible to mark a particular test case as flaky. -This code example is taken from :idf_file:`pytest_esp_eth.py `. +This code example is taken from :idf_file:`pytest_esp_eth.py `. .. code-block:: python diff --git a/docs/zh_CN/api-reference/network/esp_eth.rst b/docs/zh_CN/api-reference/network/esp_eth.rst index 1296898cc9a..af51e757c87 100644 --- a/docs/zh_CN/api-reference/network/esp_eth.rst +++ b/docs/zh_CN/api-reference/network/esp_eth.rst @@ -577,7 +577,7 @@ ESP-IDF 在宏 :c:macro:`ETH_DEFAULT_CONFIG` 中为安装驱动程序提供了 * :cpp:member:`eth_mac_ptp_config_t::roll_type`:亚秒寄存器的翻转模式(数字或二进制)。推荐使用二进制翻转模式,因为它能提供更精确的时间同步。 - 接收帧的时间戳可以通过注册的 :cpp:member:`esp_eth_config_t::stack_input_info` 函数的最后一个参数进行访问,传输帧的时间戳可以通过注册的 :cpp:func:`esp_eth_transmit_ctrl_vargs` 函数的 ``ctrl`` 参数进行访问。然而,对于用户获取时间戳信息,更简便的方式是利用 L2 TAP :ref:`扩展缓冲区 ` 机制。 + 接收帧的时间戳可以通过注册的 :cpp:member:`esp_eth_config_t::stack_input_info` 函数的最后一个参数进行访问,传输帧的时间戳可以通过 :cpp:func:`esp_eth_transmit_ctrl_bufs` 函数的 ``ctrl`` 参数进行访问。然而,对于用户获取时间戳信息,更简便的方式是利用 L2 TAP :ref:`扩展缓冲区 ` 机制。 您可以通过注册回调函数和设置事件触发的目标时间,在精确的时间点调度事件。请注意,回调函数将在中断服务程序 (ISR) 上下文中调用,因此应尽量简洁。 diff --git a/docs/zh_CN/contribute/esp-idf-tests-with-pytest.rst b/docs/zh_CN/contribute/esp-idf-tests-with-pytest.rst index 9b5a455c635..55df9231072 100644 --- a/docs/zh_CN/contribute/esp-idf-tests-with-pytest.rst +++ b/docs/zh_CN/contribute/esp-idf-tests-with-pytest.rst @@ -715,7 +715,7 @@ Pytest 使用技巧 某些测试用例基于以太网或 Wi-Fi。然而由于网络问题,测试可能会不稳定。此时,可以将某个测试用例标记为不稳定的测试用例。 -以下代码示例来自 :idf_file:`pytest_esp_eth.py `。 +以下代码示例来自 :idf_file:`pytest_esp_eth.py `。 .. code-block:: python