feat(esp_eth): add Ethernet sublayer with optional VLAN and switch

Introduce the experimental Ethernet sublayer (parent/child VLAN, optional
switch path, iodriver provider), restructure eth test apps, and document
the new APIs.
This commit is contained in:
Ondrej Kosta
2026-08-12 12:39:00 +02:00
parent 2dd71fddaa
commit 2e79c3fd69
61 changed files with 3860 additions and 45 deletions

View File

@@ -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

View File

@@ -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 "$<$<TARGET_EXISTS:idf::esp_netif>:src/esp_eth_netif_glue.c>")
target_sources(${COMPONENT_TARGET} PRIVATE
"$<$<TARGET_EXISTS:idf::esp_netif>:src/esp_eth_netif_glue.c>")
if(CONFIG_ETH_SUBLAYER_SUPPORT)
target_sources(${COMPONENT_TARGET} PRIVATE
"$<$<TARGET_EXISTS:idf::esp_netif>:src/sublayer/esp_eth_sublayer.c>"
"$<$<TARGET_EXISTS:idf::esp_netif>:src/sublayer/esp_eth_sublayer_vlan_child.c>")
if(CONFIG_ETH_SUBLAYER_SWITCH_SUPPORT)
target_sources(${COMPONENT_TARGET} PRIVATE
"$<$<TARGET_EXISTS:idf::esp_netif>: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)

View File

@@ -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

View File

@@ -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

View File

@@ -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 <stddef.h>
#include <stdint.h>
#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
*

View File

@@ -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
*

View File

@@ -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

View File

@@ -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 <stddef.h>
#include <stdint.h>
#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

View File

@@ -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 <stddef.h>
#include <stdint.h>
#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

View File

@@ -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)

View File

@@ -6,6 +6,8 @@
#include <sys/cdefs.h>
#include <stdatomic.h>
#include <string.h>
#include <stdarg.h>
#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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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).

View File

@@ -0,0 +1,651 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <sys/queue.h>
#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;
}

View File

@@ -0,0 +1,110 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#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

View File

@@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#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;

View File

@@ -0,0 +1,186 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#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

View File

@@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#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

View File

@@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <sys/queue.h>
#include <stdint.h>
#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);

View File

@@ -0,0 +1,269 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <inttypes.h>
#include <string.h>
#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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,2 @@
| Supported Targets | ESP32 | ESP32-P4 |
| ----------------- | ----- | -------- |

View File

@@ -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)

View File

@@ -0,0 +1,4 @@
## IDF Component Manager Manifest File
dependencies:
espressif/ethernet_init:
version: "^1.3.0"

View File

@@ -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);
}

View File

@@ -0,0 +1,196 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <string.h>
#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;
}

View File

@@ -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

View File

@@ -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 <string.h>
#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);
}

View File

@@ -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 <string.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <unistd.h>
#include <errno.h>
#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, &ethertype_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);
}

View File

@@ -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)

View File

@@ -0,0 +1 @@
CONFIG_IDF_TARGET="esp32"

View File

@@ -0,0 +1 @@
CONFIG_IDF_TARGET="esp32p4"

View File

@@ -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

View File

@@ -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 <esp_netif_l2tap_ext_buff>` 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 <esp_netif_l2tap_ext_buff>` 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.

View File

@@ -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 <components/esp_eth/test_apps/pytest_esp_eth.py>`.
This code example is taken from :idf_file:`pytest_esp_eth.py <components/esp_eth/test_apps/test_app_driver/pytest_esp_eth.py>`.
.. code-block:: python

View File

@@ -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:`扩展缓冲区 <esp_netif_l2tap_ext_buff>` 机制。
接收帧的时间戳可以通过注册的 :cpp:member:`esp_eth_config_t::stack_input_info` 函数的最后一个参数进行访问,传输帧的时间戳可以通过 :cpp:func:`esp_eth_transmit_ctrl_bufs` 函数的 ``ctrl`` 参数进行访问。然而,对于用户获取时间戳信息,更简便的方式是利用 L2 TAP :ref:`扩展缓冲区 <esp_netif_l2tap_ext_buff>` 机制。
您可以通过注册回调函数和设置事件触发的目标时间,在精确的时间点调度事件。请注意,回调函数将在中断服务程序 (ISR) 上下文中调用,因此应尽量简洁。

View File

@@ -715,7 +715,7 @@ Pytest 使用技巧
某些测试用例基于以太网或 Wi-Fi。然而由于网络问题测试可能会不稳定。此时可以将某个测试用例标记为不稳定的测试用例。
以下代码示例来自 :idf_file:`pytest_esp_eth.py <components/esp_eth/test_apps/pytest_esp_eth.py>`
以下代码示例来自 :idf_file:`pytest_esp_eth.py <components/esp_eth/test_apps/test_app_driver/pytest_esp_eth.py>`
.. code-block:: python