mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
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:
1
Kconfig
1
Kconfig
@@ -1031,3 +1031,4 @@ mainmenu "Espressif IoT Development Framework Configuration"
|
||||
- CONFIG_USB_HOST_EXT_PORT_RESET_ATTEMPTS
|
||||
- CONFIG_GDMA_ENABLE_WEIGHTED_ARBITRATION
|
||||
- CONFIG_COMPILER_KASAN
|
||||
- CONFIG_ETH_SUBLAYER_SUPPORT
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
360
components/esp_eth/include/esp_eth_sublayer.h
Normal file
360
components/esp_eth/include/esp_eth_sublayer.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
327
components/esp_eth/src/sublayer/README.md
Normal file
327
components/esp_eth/src/sublayer/README.md
Normal 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).
|
||||
|
||||
651
components/esp_eth/src/sublayer/esp_eth_sublayer.c
Normal file
651
components/esp_eth/src/sublayer/esp_eth_sublayer.c
Normal 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;
|
||||
}
|
||||
110
components/esp_eth/src/sublayer/esp_eth_sublayer_core.h
Normal file
110
components/esp_eth/src/sublayer/esp_eth_sublayer_core.h
Normal 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
|
||||
28
components/esp_eth/src/sublayer/esp_eth_sublayer_frame.h
Normal file
28
components/esp_eth/src/sublayer/esp_eth_sublayer_frame.h
Normal 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;
|
||||
186
components/esp_eth/src/sublayer/esp_eth_sublayer_switch.c
Normal file
186
components/esp_eth/src/sublayer/esp_eth_sublayer_switch.c
Normal 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
|
||||
72
components/esp_eth/src/sublayer/esp_eth_sublayer_switch.h
Normal file
72
components/esp_eth/src/sublayer/esp_eth_sublayer_switch.h
Normal 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
|
||||
54
components/esp_eth/src/sublayer/esp_eth_sublayer_vlan.h
Normal file
54
components/esp_eth/src/sublayer/esp_eth_sublayer_vlan.h
Normal 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);
|
||||
269
components/esp_eth/src/sublayer/esp_eth_sublayer_vlan_child.c
Normal file
269
components/esp_eth/src/sublayer/esp_eth_sublayer_vlan_child.c
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
2
components/esp_eth/test_apps/test_app_sublayer/README.md
Normal file
2
components/esp_eth/test_apps/test_app_sublayer/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
| Supported Targets | ESP32 | ESP32-P4 |
|
||||
| ----------------- | ----- | -------- |
|
||||
@@ -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)
|
||||
@@ -0,0 +1,4 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
espressif/ethernet_init:
|
||||
version: "^1.3.0"
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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, ðertype_filter),
|
||||
"failed to set L2TAP ethertype filter");
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int l2tap_open_tx(const char *if_key)
|
||||
{
|
||||
int fd = open(L2TAP_VFS_DEFAULT_PATH, O_NONBLOCK);
|
||||
TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, fd, "failed to open L2TAP device");
|
||||
TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, ioctl(fd, L2TAP_S_INTF_DEVICE, if_key),
|
||||
"failed to bind L2TAP fd to interface");
|
||||
return fd;
|
||||
}
|
||||
|
||||
static void test_build_frame(hook_test_frame_t *frame, esp_eth_handle_t eth,
|
||||
uint16_t ethertype, uint8_t marker)
|
||||
{
|
||||
memset(frame, 0, sizeof(*frame));
|
||||
esp_eth_ioctl(eth, ETH_CMD_G_MAC_ADDR, frame->header.src.addr);
|
||||
memcpy(frame->header.dest.addr, frame->header.src.addr, ETH_ADDR_LEN);
|
||||
frame->header.type = htons(ethertype);
|
||||
frame->marker = marker;
|
||||
}
|
||||
|
||||
static void test_setup(hooks_test_ctx_t *ctx)
|
||||
{
|
||||
memset(ctx, 0, sizeof(*ctx));
|
||||
memset(&s_hooks_ctx, 0, sizeof(s_hooks_ctx));
|
||||
ctx->hook_ctx = &s_hooks_ctx;
|
||||
|
||||
sublayer_test_create_event_loop(&ctx->base);
|
||||
sublayer_test_init_ethernet(&ctx->base);
|
||||
sublayer_test_register_l2tap(&ctx->base);
|
||||
sublayer_test_register_ctx(&ctx->base);
|
||||
|
||||
esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT();
|
||||
sub_cfg.eth_handle = ctx->base.eth_handles[0];
|
||||
sub_cfg.tx_hook = test_tx_hook;
|
||||
sub_cfg.post_tx_hook = test_post_tx_hook;
|
||||
sub_cfg.rx_hook = test_rx_hook;
|
||||
sub_cfg.hook_ctx = ctx->hook_ctx;
|
||||
TEST_ESP_OK(esp_eth_sublayer_new(&sub_cfg, &ctx->base.sub));
|
||||
|
||||
TEST_ESP_OK(esp_eth_sublayer_vlan_add(ctx->base.sub, ESP_ETH_SUBLAYER_UNTAGGED_VID, &ctx->vlan_untagged));
|
||||
TEST_ESP_OK(esp_eth_sublayer_vlan_add(ctx->base.sub, TEST_VLAN_ID, &ctx->vlan_tagged));
|
||||
|
||||
esp_netif_t *netif_untagged = sublayer_test_create_eth_netif(&ctx->base, IFKEY_UNTAGGED, "eth");
|
||||
esp_netif_t *netif_vlan = sublayer_test_create_eth_netif(&ctx->base, IFKEY_VLAN, "eth_vlan");
|
||||
|
||||
TEST_ESP_OK(esp_netif_attach(netif_untagged, ctx->vlan_untagged));
|
||||
TEST_ESP_OK(esp_netif_attach(netif_vlan, ctx->vlan_tagged));
|
||||
|
||||
sublayer_test_start_and_wait_connect(&ctx->base);
|
||||
}
|
||||
|
||||
static void test_teardown(hooks_test_ctx_t *ctx)
|
||||
{
|
||||
sublayer_test_teardown(&ctx->base);
|
||||
sublayer_test_unregister_ctx();
|
||||
}
|
||||
|
||||
static bool verify_loopback_frame(const uint8_t *rx_buf, size_t rx_len,
|
||||
const hook_test_frame_t *tx_frame)
|
||||
{
|
||||
return rx_len >= sizeof(*tx_frame) &&
|
||||
memcmp(rx_buf, tx_frame, sizeof(*tx_frame)) == 0;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *if_key;
|
||||
const hook_test_frame_t *frame;
|
||||
} l2tap_send_arg_t;
|
||||
|
||||
static void l2tap_send_task(void *arg)
|
||||
{
|
||||
l2tap_send_arg_t *send_arg = (l2tap_send_arg_t *)arg;
|
||||
vTaskDelay(pdMS_TO_TICKS(L2TAP_SEND_DELAY_MS));
|
||||
int tx_fd = l2tap_open_tx(send_arg->if_key);
|
||||
ssize_t n = write(tx_fd, send_arg->frame, sizeof(*send_arg->frame));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, n);
|
||||
close(tx_fd);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static bool l2tap_wait_for_frame_timeout(int fd, uint8_t *rx_buf, size_t rx_buf_size,
|
||||
size_t *rx_len, int timeout_ms)
|
||||
{
|
||||
struct timeval tv = {
|
||||
.tv_sec = timeout_ms / 1000,
|
||||
.tv_usec = (timeout_ms % 1000) * 1000,
|
||||
};
|
||||
fd_set rfds;
|
||||
|
||||
FD_ZERO(&rfds);
|
||||
FD_SET(fd, &rfds);
|
||||
|
||||
int ret = select(fd + 1, &rfds, NULL, NULL, &tv);
|
||||
if (ret <= 0 || !FD_ISSET(fd, &rfds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t n = read(fd, rx_buf, rx_buf_size);
|
||||
if (n <= 0) {
|
||||
return false;
|
||||
}
|
||||
*rx_len = (size_t)n;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool l2tap_wait_for_frame(int fd, uint8_t *rx_buf, size_t rx_buf_size, size_t *rx_len)
|
||||
{
|
||||
return l2tap_wait_for_frame_timeout(fd, rx_buf, rx_buf_size, rx_len, L2TAP_IO_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
static bool verify_frame_prefix(const uint8_t *rx_buf, size_t rx_len,
|
||||
const hook_test_frame_t *tx_frame)
|
||||
{
|
||||
size_t expected_len = sizeof(*tx_frame) + TX_PREFIX_LEN;
|
||||
if (rx_len < expected_len) {
|
||||
return false;
|
||||
}
|
||||
if (memcmp(rx_buf, tx_frame, ETH_HEADER_LEN) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (!test_prefix_magics_match(rx_buf + ETH_HEADER_LEN)) {
|
||||
return false;
|
||||
}
|
||||
return memcmp(rx_buf + ETH_HEADER_LEN + TX_PREFIX_LEN,
|
||||
((const uint8_t *)tx_frame) + ETH_HEADER_LEN,
|
||||
sizeof(*tx_frame) - ETH_HEADER_LEN) == 0;
|
||||
}
|
||||
|
||||
/* Send a frame and verify the TX PREFIX_HEAD rewrite (new leading buf) is
|
||||
* delivered intact to L2TAP via EMAC loopback. */
|
||||
static void test_tx_prefix_head_on_interface(hooks_test_ctx_t *ctx, const char *if_key,
|
||||
uint16_t ethertype)
|
||||
{
|
||||
hook_test_frame_t tx_frame;
|
||||
test_build_frame(&tx_frame, ctx->base.eth_handles[0], ethertype, FRAME_MARKER_TX);
|
||||
|
||||
int rx_fd = l2tap_open_filtered(if_key, ethertype);
|
||||
l2tap_send_arg_t send_arg = { .if_key = if_key, .frame = &tx_frame };
|
||||
xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL);
|
||||
|
||||
uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN];
|
||||
size_t rx_len = 0;
|
||||
bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len);
|
||||
bool frame_ok = got_frame && verify_loopback_frame(rx_buf, rx_len, &tx_frame);
|
||||
close(rx_fd);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
TEST_ASSERT_TRUE(got_frame);
|
||||
TEST_ASSERT_TRUE(frame_ok);
|
||||
}
|
||||
|
||||
/* Send a frame and verify the TX PREFIX rewrite (extra segment + ethertype
|
||||
* tweak) is delivered to L2TAP via EMAC loopback. */
|
||||
static void test_tx_prefix_on_interface(hooks_test_ctx_t *ctx, const char *if_key,
|
||||
uint16_t ethertype)
|
||||
{
|
||||
hook_test_frame_t tx_frame;
|
||||
test_build_frame(&tx_frame, ctx->base.eth_handles[0], ethertype, FRAME_MARKER_TX);
|
||||
|
||||
int rx_fd = l2tap_open_filtered(if_key, ethertype);
|
||||
l2tap_send_arg_t send_arg = { .if_key = if_key, .frame = &tx_frame };
|
||||
xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL);
|
||||
|
||||
uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN];
|
||||
size_t rx_len = 0;
|
||||
bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len);
|
||||
bool frame_ok = got_frame && verify_frame_prefix(rx_buf, rx_len, &tx_frame);
|
||||
close(rx_fd);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
TEST_ASSERT_TRUE(got_frame);
|
||||
TEST_ASSERT_TRUE(frame_ok);
|
||||
}
|
||||
|
||||
/* Send a VLAN frame and verify the TX MERGE rewrite (contiguous buffer) is
|
||||
* delivered to L2TAP via EMAC loopback. */
|
||||
static void test_tx_merge_on_vlan(hooks_test_ctx_t *ctx)
|
||||
{
|
||||
hook_test_frame_t tx_frame;
|
||||
test_build_frame(&tx_frame, ctx->base.eth_handles[0], ETH_TYPE_HOOK_VLAN, FRAME_MARKER_TX);
|
||||
|
||||
int rx_fd = l2tap_open_filtered(IFKEY_VLAN, ETH_TYPE_HOOK_VLAN);
|
||||
l2tap_send_arg_t send_arg = { .if_key = IFKEY_VLAN, .frame = &tx_frame };
|
||||
xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL);
|
||||
|
||||
uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN];
|
||||
size_t rx_len = 0;
|
||||
bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len);
|
||||
bool frame_ok = got_frame && verify_loopback_frame(rx_buf, rx_len, &tx_frame);
|
||||
close(rx_fd);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
TEST_ASSERT_TRUE(got_frame);
|
||||
TEST_ASSERT_TRUE(frame_ok);
|
||||
}
|
||||
|
||||
/* Send a frame on if_key and assert L2TAP never receives it.
|
||||
* Used when a TX hook skips/aborts transmit, or an RX hook drops/aborts the
|
||||
* frame before delivery. expect_write_error selects whether write() itself
|
||||
* must fail (TX abort) or succeed (skip / RX drop-or-abort). */
|
||||
static void test_expect_no_frame_on_interface(hooks_test_ctx_t *ctx, const char *if_key,
|
||||
uint16_t ethertype, bool expect_write_error)
|
||||
{
|
||||
hook_test_frame_t tx_frame;
|
||||
test_build_frame(&tx_frame, ctx->base.eth_handles[0], ethertype, FRAME_MARKER_TX);
|
||||
|
||||
int rx_fd = l2tap_open_filtered(if_key, ethertype);
|
||||
int tx_fd = l2tap_open_tx(if_key);
|
||||
ssize_t wr = write(tx_fd, &tx_frame, sizeof(tx_frame));
|
||||
close(tx_fd);
|
||||
if (expect_write_error) {
|
||||
TEST_ASSERT_EQUAL(-1, wr);
|
||||
} else {
|
||||
TEST_ASSERT_NOT_EQUAL(-1, wr);
|
||||
}
|
||||
|
||||
uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN];
|
||||
size_t rx_len = 0;
|
||||
bool got_frame = l2tap_wait_for_frame_timeout(rx_fd, rx_buf, sizeof(rx_buf), &rx_len, 1500);
|
||||
close(rx_fd);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
TEST_ASSERT_FALSE(got_frame);
|
||||
(void)ctx;
|
||||
}
|
||||
|
||||
/* Send a frame and verify the RX hook rewrote the payload marker
|
||||
* (FRAME_MARKER_TX -> FRAME_MARKER_RX) before L2TAP delivery. */
|
||||
static void test_rx_modify_on_interface(hooks_test_ctx_t *ctx, const char *if_key,
|
||||
uint16_t ethertype)
|
||||
{
|
||||
hook_test_frame_t tx_frame;
|
||||
test_build_frame(&tx_frame, ctx->base.eth_handles[0], ethertype, FRAME_MARKER_TX);
|
||||
|
||||
int rx_fd = l2tap_open_filtered(if_key, ethertype);
|
||||
l2tap_send_arg_t send_arg = { .if_key = if_key, .frame = &tx_frame };
|
||||
xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL);
|
||||
|
||||
uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN];
|
||||
size_t rx_len = 0;
|
||||
bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len);
|
||||
bool marker_ok = got_frame && rx_len >= ETH_HEADER_LEN + 1 &&
|
||||
rx_buf[ETH_HEADER_LEN] == FRAME_MARKER_RX;
|
||||
close(rx_fd);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
TEST_ASSERT_TRUE(got_frame);
|
||||
TEST_ASSERT_TRUE(marker_ok);
|
||||
}
|
||||
|
||||
static void test_set_tx_op(hooks_test_ctx_t *ctx, hook_tx_op_t tx_op)
|
||||
{
|
||||
memset(&s_hooks_ctx, 0, sizeof(s_hooks_ctx));
|
||||
s_hooks_ctx.dir = HOOK_DIR_TX;
|
||||
s_hooks_ctx.tx.op = tx_op;
|
||||
ctx->hook_ctx = &s_hooks_ctx;
|
||||
}
|
||||
|
||||
static void test_set_rx_op(hooks_test_ctx_t *ctx, hook_rx_op_t rx_op)
|
||||
{
|
||||
memset(&s_hooks_ctx, 0, sizeof(s_hooks_ctx));
|
||||
s_hooks_ctx.dir = HOOK_DIR_RX;
|
||||
s_hooks_ctx.rx.op = rx_op;
|
||||
ctx->hook_ctx = &s_hooks_ctx;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test cases */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
TEST_CASE("sublayer tx and post tx hooks", "[sublayer_hooks]")
|
||||
{
|
||||
hooks_test_ctx_t ctx;
|
||||
test_setup(&ctx);
|
||||
test_set_tx_op(&ctx, HOOK_TX_OP_PREFIX);
|
||||
|
||||
ESP_LOGI(TAG, "TX hook: insert prefix, move frame to bufs[1]");
|
||||
test_tx_prefix_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED);
|
||||
test_tx_prefix_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.post_hook_calls);
|
||||
|
||||
ESP_LOGI(TAG, "TX hook: insert custom leading frame + reorder; RX hook strips on loopback");
|
||||
test_set_tx_op(&ctx, HOOK_TX_OP_PREFIX_HEAD);
|
||||
test_tx_prefix_head_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED);
|
||||
test_tx_prefix_head_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.rx_strip_calls);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.post_hook_calls);
|
||||
|
||||
ESP_LOGI(TAG, "TX hook: merge tagged header and payload into one buffer");
|
||||
test_set_tx_op(&ctx, HOOK_TX_OP_MERGE);
|
||||
test_tx_merge_on_vlan(&ctx);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.post_hook_calls);
|
||||
|
||||
ESP_LOGI(TAG, "TX hook: buf_count=0 takes ownership, no loopback frame");
|
||||
test_set_tx_op(&ctx, HOOK_TX_OP_SKIP);
|
||||
test_expect_no_frame_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED, false);
|
||||
test_expect_no_frame_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN, false);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls);
|
||||
TEST_ASSERT_EQUAL(0, ctx.hook_ctx->tx.post_hook_calls);
|
||||
|
||||
ESP_LOGI(TAG, "TX hook: non-ESP_OK aborts transmit");
|
||||
test_set_tx_op(&ctx, HOOK_TX_OP_ABORT);
|
||||
test_expect_no_frame_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED, true);
|
||||
test_expect_no_frame_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN, true);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls);
|
||||
TEST_ASSERT_EQUAL(0, ctx.hook_ctx->tx.post_hook_calls);
|
||||
|
||||
ESP_LOGI(TAG, "post_tx_hook frees buffer allocated in tx hook");
|
||||
test_set_tx_op(&ctx, HOOK_TX_OP_POST_TX_FREE);
|
||||
hook_test_frame_t tx_frame;
|
||||
test_build_frame(&tx_frame, ctx.base.eth_handles[0], ETH_TYPE_HOOK_UNTAGGED, FRAME_MARKER_TX);
|
||||
int rx_fd = l2tap_open_filtered(IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED);
|
||||
l2tap_send_arg_t send_arg = { .if_key = IFKEY_UNTAGGED, .frame = &tx_frame };
|
||||
xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL);
|
||||
uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN];
|
||||
size_t rx_len = 0;
|
||||
bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len);
|
||||
close(rx_fd);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
TEST_ASSERT_TRUE(got_frame);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.post_hook_calls);
|
||||
TEST_ASSERT_NOT_NULL(ctx.hook_ctx->tx.freed_ptr);
|
||||
|
||||
test_teardown(&ctx);
|
||||
}
|
||||
|
||||
TEST_CASE("sublayer rx hooks", "[sublayer_hooks]")
|
||||
{
|
||||
hooks_test_ctx_t ctx;
|
||||
test_setup(&ctx);
|
||||
test_set_rx_op(&ctx, HOOK_RX_OP_MODIFY);
|
||||
|
||||
ESP_LOGI(TAG, "RX hook: modify payload before VLAN demux / L2TAP delivery");
|
||||
test_rx_modify_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED);
|
||||
test_rx_modify_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->rx.hook_calls);
|
||||
|
||||
ESP_LOGI(TAG, "RX hook: len=0 drops frame on untagged and tagged");
|
||||
test_set_rx_op(&ctx, HOOK_RX_OP_DROP);
|
||||
test_expect_no_frame_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED, false);
|
||||
test_expect_no_frame_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN, false);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->rx.hook_calls);
|
||||
|
||||
ESP_LOGI(TAG, "RX hook: non-ESP_OK aborts processing");
|
||||
test_set_rx_op(&ctx, HOOK_RX_OP_ABORT);
|
||||
test_expect_no_frame_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED, false);
|
||||
test_expect_no_frame_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN, false);
|
||||
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->rx.hook_calls);
|
||||
|
||||
test_teardown(&ctx);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
CONFIG_IDF_TARGET="esp32"
|
||||
@@ -0,0 +1 @@
|
||||
CONFIG_IDF_TARGET="esp32p4"
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) 上下文中调用,因此应尽量简洁。
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user