feat(examples): add Ethernet sublayer example and update PTP

Add a dedicated sublayer example under examples/ethernet and enable
sublayer usage in the PTP example.
This commit is contained in:
Ondrej Kosta
2026-08-12 12:39:21 +02:00
parent ddaf9c2b79
commit 05993b5090
16 changed files with 337 additions and 1 deletions

View File

@@ -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 <https://components.espressif.com/>`_.
.. _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

View File

@@ -683,6 +683,20 @@ ESP-IDF 以太网驱动程序所需的大部分 PHY 管理功能都已涵盖在
实现新的自定义 PHY 驱动程序后,你可以通过 `乐鑫组件注册表 <https://components.espressif.com/>`_ 将驱动分享给其他用户。
.. _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 参考

View File

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

View File

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

View File

@@ -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, &eth_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, &eth_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));

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "ethernet_sublayer_example_main.c"
PRIV_REQUIRES esp_netif esp_eth
INCLUDE_DIRS ".")

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
dependencies:
espressif/ethernet_init:
version: "~1.4.1"

View File

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

View File

@@ -0,0 +1,4 @@
CONFIG_IDF_TARGET="esp32"
CONFIG_ETHERNET_INTERNAL_SUPPORT=y
CONFIG_ETHERNET_PHY_IP101=y

View File

@@ -0,0 +1,4 @@
CONFIG_IDF_TARGET="esp32p4"
CONFIG_ETHERNET_INTERNAL_SUPPORT=y
CONFIG_ETHERNET_PHY_IP101=y

View File

@@ -0,0 +1,4 @@
CONFIG_IDF_TARGET="esp32s31"
CONFIG_ETHERNET_INTERNAL_SUPPORT=y
CONFIG_ETHERNET_PHY_YT8531=y

View File

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