diff --git a/docs/en/api-reference/network/esp_eth.rst b/docs/en/api-reference/network/esp_eth.rst
index f19183a92f0..b89b807a21c 100644
--- a/docs/en/api-reference/network/esp_eth.rst
+++ b/docs/en/api-reference/network/esp_eth.rst
@@ -683,6 +683,20 @@ The majority of PHY management functionality required by the ESP-IDF Ethernet dr
Once you finish the new custom PHY driver implementation, consider sharing it among other users via `ESP Component Registry `_.
+.. _ethernet-sublayer:
+
+Ethernet Sublayer
+^^^^^^^^^^^^^^^^^
+
+The Ethernet sublayer is an optional layer between one physical Ethernet driver (``esp_eth_handle_t``) and one or more ``esp_netif`` instances. It owns the driver RX input path, distributes Ethernet events to attached interfaces, and can perform mid-path frame processing such as 802.1Q VLAN multiplexing and demultiplexing.
+
+.. note::
+ The Ethernet sublayer is an **experimental** feature. Enable :menuitem:`CONFIG_IDF_EXPERIMENTAL_FEATURES` and :menuitem:`CONFIG_ETH_SUBLAYER_SUPPORT` to use it. The API is under active development and may change in future ESP-IDF releases without a deprecation period.
+
+For architecture details, configuration, and usage guidance, see :component_file:`esp_eth/src/sublayer/README.md`.
+
+A working demonstration is available in :example:`ethernet/sublayer`, which shares one Ethernet driver between an untagged ``esp_netif`` and a tagged VLAN ``esp_netif``.
+
.. ---------------------------- API Reference ----------------------------------
API Reference
diff --git a/docs/zh_CN/api-reference/network/esp_eth.rst b/docs/zh_CN/api-reference/network/esp_eth.rst
index af51e757c87..aa48dcb9c8e 100644
--- a/docs/zh_CN/api-reference/network/esp_eth.rst
+++ b/docs/zh_CN/api-reference/network/esp_eth.rst
@@ -683,6 +683,20 @@ ESP-IDF 以太网驱动程序所需的大部分 PHY 管理功能都已涵盖在
实现新的自定义 PHY 驱动程序后,你可以通过 `乐鑫组件注册表 `_ 将驱动分享给其他用户。
+.. _ethernet-sublayer:
+
+以太网子层
+^^^^^^^^^^
+
+以太网子层是位于一个物理以太网驱动程序(``esp_eth_handle_t``)与一个或多个 ``esp_netif`` 实例之间的可选层。它负责拥有驱动程序的 RX 输入路径,将以太网事件分发给所连接的接口,并可执行路径中的帧处理,例如 802.1Q VLAN 复用与解复用。
+
+.. note::
+ 以太网子层是一项**实验性**功能。需启用 :menuitem:`CONFIG_IDF_EXPERIMENTAL_FEATURES` 和 :menuitem:`CONFIG_ETH_SUBLAYER_SUPPORT` 后方可使用。该 API 仍在积极开发中,未来 ESP-IDF 版本中可能会在不经过弃用周期的情况下发生变更。
+
+有关架构细节、配置和使用指南,请参阅 :component_file:`esp_eth/src/sublayer/README.md`。
+
+可参考 :example:`ethernet/sublayer` 中的示例,该示例演示了如何在一个未打标签的 ``esp_netif`` 和一个带标签的 VLAN ``esp_netif`` 之间共享同一个以太网驱动程序。
+
.. ---------------------------- API Reference ----------------------------------
API 参考
diff --git a/examples/ethernet/.build-test-rules.yml b/examples/ethernet/.build-test-rules.yml
index 324366ca33d..1e27da54276 100644
--- a/examples/ethernet/.build-test-rules.yml
+++ b/examples/ethernet/.build-test-rules.yml
@@ -36,3 +36,14 @@ examples/ethernet/ptp:
depends_components:
- esp_eth
- esp_netif
+
+examples/ethernet/sublayer:
+ enable:
+ - if: INCLUDE_DEFAULT == 1
+ disable:
+ - if: IDF_TARGET not in ["esp32", "esp32p4", "esp32s31"]
+ depends_components:
+ - esp_eth
+ - esp_netif
+ - lwip
+ - esp_driver_gpio
diff --git a/examples/ethernet/README.md b/examples/ethernet/README.md
index 8a73c60c532..91e5e102d45 100644
--- a/examples/ethernet/README.md
+++ b/examples/ethernet/README.md
@@ -9,6 +9,8 @@ Ethernet examples demonstrate basic features related to Ethernet layer, such as:
* basic Ethernet initialization and binding to IP stack via ESP-NETIF
+* experimental Ethernet netif sublayer (untagged + VLAN interfaces on one driver)
+
* tools for performance measurement, i.e. `iperf`
* hardware time synchronization
diff --git a/examples/ethernet/ptp/main/ptp_main.c b/examples/ethernet/ptp/main/ptp_main.c
index 581891e9563..c397100780d 100644
--- a/examples/ethernet/ptp/main/ptp_main.c
+++ b/examples/ethernet/ptp/main/ptp_main.c
@@ -59,8 +59,21 @@ void init_ethernet_and_netif(void)
esp_netif_base_config.route_prio -= i*5;
esp_netif_t *eth_netif = esp_netif_new(&esp_netif_config);
+#if CONFIG_ETH_SUBLAYER_SUPPORT
+ ESP_LOGI(TAG, "Using Ethernet sublayer");
+ esp_eth_sublayer_config_t sub_config = {
+ .eth_handle = eth_handles[i],
+ };
+ esp_eth_sublayer_handle_t eth_sublayer = NULL;
+ ESP_ERROR_CHECK(esp_eth_sublayer_new(&sub_config, ð_sublayer));
+ esp_eth_sublayer_vlan_handle_t eth_netif_untag = NULL;
+ ESP_ERROR_CHECK(esp_eth_sublayer_vlan_add(eth_sublayer, ESP_ETH_SUBLAYER_UNTAGGED_VID, ð_netif_untag));
+ // attach Ethernet driver to TCP/IP stack
+ ESP_ERROR_CHECK(esp_netif_attach(eth_netif, eth_netif_untag));
+#else
// attach Ethernet driver to TCP/IP stack
ESP_ERROR_CHECK(esp_netif_attach(eth_netif, esp_eth_new_netif_glue(eth_handles[i])));
+#endif
}
for (int i = 0; i < eth_port_cnt; i++) {
@@ -127,7 +140,12 @@ void app_main(void)
ptp_deamon_start();
#if CONFIG_EXAMPLE_PTP_PULSE_EMAC_PPS
- esp_eth_handle_t eth_handle = esp_netif_get_io_driver(esp_netif_get_handle_from_ifkey(ETH_IF_KEY));
+ esp_netif_iodriver_handle netif_iodriver = esp_netif_get_io_driver(esp_netif_get_handle_from_ifkey(ETH_IF_KEY));
+#if CONFIG_ETH_SUBLAYER_SUPPORT
+ esp_eth_handle_t eth_handle = esp_eth_sublayer_vlan_get_eth_handle(netif_iodriver);
+#else
+ esp_eth_handle_t eth_handle = (esp_eth_handle_t)netif_iodriver;
+#endif
esp_eth_mac_t *mac;
ESP_ERROR_CHECK(esp_eth_get_mac_instance(eth_handle, &mac));
ESP_ERROR_CHECK(esp_eth_mac_set_pps_out_gpio(mac, CONFIG_EXAMPLE_PTP_PULSE_GPIO));
diff --git a/examples/ethernet/sublayer/CMakeLists.txt b/examples/ethernet/sublayer/CMakeLists.txt
new file mode 100644
index 00000000000..4058da1ed16
--- /dev/null
+++ b/examples/ethernet/sublayer/CMakeLists.txt
@@ -0,0 +1,8 @@
+# The following lines of boilerplate have to be in your project's CMakeLists
+# in this exact order for cmake to work correctly
+cmake_minimum_required(VERSION 3.22)
+
+include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
+idf_build_set_property(MINIMAL_BUILD ON)
+project(ethernet_sublayer)
diff --git a/examples/ethernet/sublayer/README.md b/examples/ethernet/sublayer/README.md
new file mode 100644
index 00000000000..c2b59635caf
--- /dev/null
+++ b/examples/ethernet/sublayer/README.md
@@ -0,0 +1,55 @@
+| Supported Targets | ESP32 | ESP32-P4 | ESP32-S31 |
+| ----------------- | ----- | -------- | --------- |
+
+# Ethernet Sublayer Example
+
+Demonstrates the experimental Ethernet netif sublayer (`esp_eth_sublayer`): one physical Ethernet driver shared by an untagged `esp_netif` (DHCP) and a tagged 802.1Q VLAN `esp_netif` (static IP).
+
+Uses the [ethernet_init](https://components.espressif.com/components/espressif/ethernet_init/) component for driver setup.
+
+## How to use example
+
+### Configure the project
+
+```
+idf.py menuconfig
+```
+
+See common configurations for Ethernet examples from [upper level](../README.md#common-configurations).
+
+This example requires enabling `CONFIG_IDF_EXPERIMENTAL_FEATURES` and `CONFIG_ETH_SUBLAYER_SUPPORT`.
+
+### Build, Flash, and Run
+
+Build the project and flash it to the board, then run monitor tool to view serial output:
+
+```
+idf.py -p PORT build flash monitor
+```
+
+(Replace PORT with the name of the serial port to use.)
+
+(To exit the serial monitor, type ``Ctrl-]``.)
+
+### Setup VLAN interface on Linux
+
+Create a tagged 802.1Q interface on the host so you can reach the ESP VLAN netif (defaults: VID `20`, IP `192.168.20.10`):
+
+```bash
+# Replace eth0 with your host Ethernet interface (e.g. enp3s0)
+sudo ip link add link eth0 name eth0.20 type vlan id 20
+sudo ip addr add 192.168.20.1/24 dev eth0.20
+sudo ip link set eth0.20 up
+
+ping 192.168.20.10
+```
+
+To remove it later:
+
+```bash
+sudo ip link delete eth0.20
+```
+
+Match `id` / address to **Example Configuration** if you changed the defaults. The physical link between the host and the board must carry tagged frames (direct cable, or a switch port in trunk/hybrid mode).
+
+See [upper-level Ethernet README](../README.md) for hardware notes and common troubleshooting.
diff --git a/examples/ethernet/sublayer/main/CMakeLists.txt b/examples/ethernet/sublayer/main/CMakeLists.txt
new file mode 100644
index 00000000000..01357c103ab
--- /dev/null
+++ b/examples/ethernet/sublayer/main/CMakeLists.txt
@@ -0,0 +1,3 @@
+idf_component_register(SRCS "ethernet_sublayer_example_main.c"
+ PRIV_REQUIRES esp_netif esp_eth
+ INCLUDE_DIRS ".")
diff --git a/examples/ethernet/sublayer/main/Kconfig.projbuild b/examples/ethernet/sublayer/main/Kconfig.projbuild
new file mode 100644
index 00000000000..fab04dbb0c3
--- /dev/null
+++ b/examples/ethernet/sublayer/main/Kconfig.projbuild
@@ -0,0 +1,22 @@
+menu "Example Configuration"
+
+ config EXAMPLE_ETHERNET_VLAN_ID
+ int "VLAN identifier"
+ range 1 4094
+ default 20
+ help
+ 802.1Q VLAN ID for the tagged virtual interface created via the Ethernet sublayer.
+
+ config EXAMPLE_VLAN_STATIC_IPV4_ADDR
+ string "VLAN IPV4 Address"
+ default "192.168.20.10"
+
+ config EXAMPLE_VLAN_STATIC_ADDR_MASK
+ string "VLAN Subnet Mask"
+ default "255.255.255.0"
+
+ config EXAMPLE_VLAN_STATIC_ADDR_DEF_GW
+ string "VLAN IPV4 Default Gateway"
+ default "192.168.20.1"
+
+endmenu
diff --git a/examples/ethernet/sublayer/main/ethernet_sublayer_example_main.c b/examples/ethernet/sublayer/main/ethernet_sublayer_example_main.c
new file mode 100644
index 00000000000..958072a7b70
--- /dev/null
+++ b/examples/ethernet/sublayer/main/ethernet_sublayer_example_main.c
@@ -0,0 +1,143 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Unlicense OR CC0-1.0
+ */
+#include
+#include
+#include
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "esp_event.h"
+#include "esp_netif.h"
+#include "esp_eth.h"
+#include "ethernet_init.h"
+#include "esp_log.h"
+#include "sdkconfig.h"
+
+#ifndef CONFIG_ETH_SUBLAYER_SUPPORT
+#error "This example requires CONFIG_ETH_SUBLAYER_SUPPORT (enable IDF experimental features)"
+#endif
+
+static const char *TAG = "eth_sublayer_example";
+
+/** Event handler for Ethernet events */
+static void eth_event_handler(void *arg, esp_event_base_t event_base,
+ int32_t event_id, void *event_data)
+{
+ uint8_t mac_addr[6] = {0};
+ esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data;
+
+ switch (event_id) {
+ case ETHERNET_EVENT_CONNECTED:
+ esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, mac_addr);
+ ESP_LOGI(TAG, "Ethernet Link Up");
+ ESP_LOGI(TAG, "Ethernet HW Addr %02x:%02x:%02x:%02x:%02x:%02x",
+ mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
+ break;
+ case ETHERNET_EVENT_DISCONNECTED:
+ ESP_LOGI(TAG, "Ethernet Link Down");
+ break;
+ case ETHERNET_EVENT_START:
+ ESP_LOGI(TAG, "Ethernet Started");
+ break;
+ case ETHERNET_EVENT_STOP:
+ ESP_LOGI(TAG, "Ethernet Stopped");
+ break;
+ default:
+ break;
+ }
+}
+
+/** Event handler for IP_EVENT_ETH_GOT_IP */
+static void got_ip_event_handler(void *arg, esp_event_base_t event_base,
+ int32_t event_id, void *event_data)
+{
+ ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
+ const esp_netif_ip_info_t *ip_info = &event->ip_info;
+
+ ESP_LOGI(TAG, "Ethernet Got IP Address (%s)", esp_netif_get_ifkey(event->esp_netif));
+ ESP_LOGI(TAG, "~~~~~~~~~~~");
+ ESP_LOGI(TAG, "ETHIP:" IPSTR, IP2STR(&ip_info->ip));
+ ESP_LOGI(TAG, "ETHMASK:" IPSTR, IP2STR(&ip_info->netmask));
+ ESP_LOGI(TAG, "ETHGW:" IPSTR, IP2STR(&ip_info->gw));
+ ESP_LOGI(TAG, "~~~~~~~~~~~");
+}
+
+static esp_err_t create_vlan_netif_config(uint16_t vlan_id, esp_netif_config_t *cfg)
+{
+ char *if_key;
+ if (asprintf(&if_key, "ETH_VLAN%d", vlan_id) < 0) {
+ return ESP_ERR_NO_MEM;
+ }
+
+ esp_netif_inherent_config_t *base = malloc(sizeof(*base));
+ if (base == NULL) {
+ free(if_key);
+ return ESP_ERR_NO_MEM;
+ }
+ *base = (esp_netif_inherent_config_t)ESP_NETIF_INHERENT_DEFAULT_ETH();
+ base->if_key = if_key;
+ base->if_desc = "eth_vlan";
+ base->route_prio = 30;
+
+ cfg->base = base;
+ cfg->driver = NULL;
+ cfg->stack = ESP_NETIF_NETSTACK_DEFAULT_ETH;
+ return ESP_OK;
+}
+
+static void free_vlan_netif_config(esp_netif_config_t *cfg)
+{
+ if (cfg && cfg->base) {
+ free((void *)cfg->base->if_key);
+ free((void *)cfg->base);
+ }
+}
+
+void app_main(void)
+{
+ uint8_t eth_port_cnt = 0;
+ esp_eth_handle_t *eth_handles = NULL;
+
+ ESP_ERROR_CHECK(esp_netif_init());
+ ESP_ERROR_CHECK(esp_event_loop_create_default());
+ ESP_ERROR_CHECK(ethernet_init_all(ð_handles, ð_port_cnt));
+ if (eth_port_cnt > 1) {
+ ESP_LOGW(TAG, "Multiple Ethernet interfaces detected, using the first one");
+ }
+
+ ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, ð_event_handler, NULL));
+ ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &got_ip_event_handler, NULL));
+
+ // Create Ethernet sublayer bound to the physical driver
+ esp_eth_sublayer_config_t sub_config = ESP_ETH_SUBLAYER_CONFIG_DEFAULT();
+ sub_config.eth_handle = eth_handles[0];
+ esp_eth_sublayer_handle_t sublayer = NULL;
+ ESP_ERROR_CHECK(esp_eth_sublayer_new(&sub_config, &sublayer));
+
+ // Untagged interface (DHCP)
+ esp_eth_sublayer_vlan_handle_t untagged = NULL;
+ ESP_ERROR_CHECK(esp_eth_sublayer_vlan_add(sublayer, ESP_ETH_SUBLAYER_UNTAGGED_VID, &untagged));
+ esp_netif_config_t eth_cfg = ESP_NETIF_DEFAULT_ETH();
+ esp_netif_t *eth_netif = esp_netif_new(ð_cfg);
+ ESP_ERROR_CHECK(esp_netif_attach(eth_netif, untagged));
+
+ // Tagged VLAN interface (static IP)
+ esp_eth_sublayer_vlan_handle_t vlan = NULL;
+ ESP_ERROR_CHECK(esp_eth_sublayer_vlan_add(sublayer, CONFIG_EXAMPLE_ETHERNET_VLAN_ID, &vlan));
+ esp_netif_config_t vlan_cfg;
+ ESP_ERROR_CHECK(create_vlan_netif_config(CONFIG_EXAMPLE_ETHERNET_VLAN_ID, &vlan_cfg));
+ esp_netif_t *vlan_netif = esp_netif_new(&vlan_cfg);
+ free_vlan_netif_config(&vlan_cfg);
+ ESP_ERROR_CHECK(esp_netif_attach(vlan_netif, vlan));
+
+ esp_netif_dhcpc_stop(vlan_netif);
+ esp_netif_ip_info_t vlan_ip = {0};
+ inet_aton(CONFIG_EXAMPLE_VLAN_STATIC_IPV4_ADDR, &vlan_ip.ip.addr);
+ inet_aton(CONFIG_EXAMPLE_VLAN_STATIC_ADDR_DEF_GW, &vlan_ip.gw.addr);
+ inet_aton(CONFIG_EXAMPLE_VLAN_STATIC_ADDR_MASK, &vlan_ip.netmask.addr);
+ ESP_ERROR_CHECK(esp_netif_set_ip_info(vlan_netif, &vlan_ip));
+
+ ESP_ERROR_CHECK(esp_eth_start(eth_handles[0]));
+}
diff --git a/examples/ethernet/sublayer/main/idf_component.yml b/examples/ethernet/sublayer/main/idf_component.yml
new file mode 100644
index 00000000000..cc244f74d1f
--- /dev/null
+++ b/examples/ethernet/sublayer/main/idf_component.yml
@@ -0,0 +1,3 @@
+dependencies:
+ espressif/ethernet_init:
+ version: "~1.4.1"
diff --git a/examples/ethernet/sublayer/pytest_eth_sublayer.py b/examples/ethernet/sublayer/pytest_eth_sublayer.py
new file mode 100644
index 00000000000..a58a3cbb45b
--- /dev/null
+++ b/examples/ethernet/sublayer/pytest_eth_sublayer.py
@@ -0,0 +1,26 @@
+# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
+# SPDX-License-Identifier: Unlicense OR CC0-1.0
+import platform
+import subprocess
+
+import pytest
+from pytest_embedded import Dut
+
+
+@pytest.mark.parametrize(
+ 'config, target',
+ [
+ pytest.param('defaults', 'esp32', marks=[pytest.mark.eth_ip101]),
+ pytest.param('defaults_esp32p4', 'esp32p4', marks=[pytest.mark.eth_ip101]),
+ pytest.param('defaults_esp32s31', 'esp32s31', marks=[pytest.mark.eth_yt8531]),
+ ],
+ indirect=['target'],
+)
+def test_esp_eth_sublayer(dut: Dut) -> None:
+ # wait for IP on the untagged interface
+ dut_ip = dut.expect(r'esp_netif_handlers: .+ ip: (\d+\.\d+\.\d+\.\d+),').group(1)
+ param = '-n' if platform.system().lower() == 'windows' else '-c'
+ command = ['ping', param, '1', dut_ip]
+ output = subprocess.run(command, capture_output=True)
+ if 'unreachable' in str(output.stdout):
+ raise RuntimeError('Host unreachable')
diff --git a/examples/ethernet/sublayer/sdkconfig.ci.defaults b/examples/ethernet/sublayer/sdkconfig.ci.defaults
new file mode 100644
index 00000000000..4514247a716
--- /dev/null
+++ b/examples/ethernet/sublayer/sdkconfig.ci.defaults
@@ -0,0 +1,4 @@
+CONFIG_IDF_TARGET="esp32"
+
+CONFIG_ETHERNET_INTERNAL_SUPPORT=y
+CONFIG_ETHERNET_PHY_IP101=y
diff --git a/examples/ethernet/sublayer/sdkconfig.ci.defaults_esp32p4 b/examples/ethernet/sublayer/sdkconfig.ci.defaults_esp32p4
new file mode 100644
index 00000000000..987db90b976
--- /dev/null
+++ b/examples/ethernet/sublayer/sdkconfig.ci.defaults_esp32p4
@@ -0,0 +1,4 @@
+CONFIG_IDF_TARGET="esp32p4"
+
+CONFIG_ETHERNET_INTERNAL_SUPPORT=y
+CONFIG_ETHERNET_PHY_IP101=y
diff --git a/examples/ethernet/sublayer/sdkconfig.ci.defaults_esp32s31 b/examples/ethernet/sublayer/sdkconfig.ci.defaults_esp32s31
new file mode 100644
index 00000000000..0e057be5f18
--- /dev/null
+++ b/examples/ethernet/sublayer/sdkconfig.ci.defaults_esp32s31
@@ -0,0 +1,4 @@
+CONFIG_IDF_TARGET="esp32s31"
+
+CONFIG_ETHERNET_INTERNAL_SUPPORT=y
+CONFIG_ETHERNET_PHY_YT8531=y
diff --git a/examples/ethernet/sublayer/sdkconfig.defaults b/examples/ethernet/sublayer/sdkconfig.defaults
new file mode 100644
index 00000000000..009d650eae2
--- /dev/null
+++ b/examples/ethernet/sublayer/sdkconfig.defaults
@@ -0,0 +1,5 @@
+CONFIG_IDF_EXPERIMENTAL_FEATURES=y
+CONFIG_ETH_SUBLAYER_SUPPORT=y
+CONFIG_ETH_SUBLAYER_VLAN_SUPPORT=y
+CONFIG_ETH_TRANSMIT_MUTEX=n
+CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX=y