feat(ethernet): Add a reusable Ethernet component (RMII + SPI) - #689
feat(ethernet): Add a reusable Ethernet component (RMII + SPI)#689finger563 wants to merge 1 commit into
Conversation
|
✅Static analysis result - no issues found! ✅ |
There was a problem hiding this comment.
Pull request overview
This PR introduces a new reusable espp::Ethernet component that wraps ESP-IDF’s esp_eth APIs behind a single C++ configuration interface supporting RMII, SPI (W5500/optional chips), and a pre-built driver escape hatch, and refactors existing BSP Ethernet implementations to consume it. It also wires the new component into the docs and CI so the example builds in the repo’s build matrix.
Changes:
- Add
components/ethernet(C++ wrapper, Kconfig options, example project, and component manifest). - Refactor
esp32-ethernet-kitandesp32-p4-function-ev-boardBSP Ethernet bring-up to useespp::Ethernet. - Add Sphinx/Doxygen docs wiring and a CI build-matrix entry for the new example.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| doc/en/network/index.rst | Adds ethernet docs pages to the network docs toctree. |
| doc/en/network/ethernet.rst | New Sphinx page describing the Ethernet component + API include. |
| doc/en/network/ethernet_example.md | Includes the component example README into docs. |
| doc/Doxyfile | Adds Ethernet header/example to Doxygen inputs (ordering needs adjustment). |
| components/ethernet/src/ethernet.cpp | Implements espp::Ethernet (event handling, bring-up, teardown). |
| components/ethernet/README.md | Component overview and usage documentation. |
| components/ethernet/Kconfig | Kconfig toggles for managed SPI chip drivers. |
| components/ethernet/include/ethernet.hpp | Public API: config structs, callbacks, lifecycle, getters. |
| components/ethernet/idf_component.yml | Component Manager manifest + conditional managed dependencies. |
| components/ethernet/example/sdkconfig.defaults | Default example config for esp32 RMII build. |
| components/ethernet/example/README.md | Example instructions and build variants (RMII vs W5500). |
| components/ethernet/example/main/ethernet_example.cpp | Example app using callbacks and monitoring loop. |
| components/ethernet/example/main/CMakeLists.txt | Registers the example’s main component. |
| components/ethernet/example/CMakeLists.txt | ESP-IDF example project setup referencing repo components. |
| components/ethernet/CMakeLists.txt | Registers the new Ethernet component and its IDF requirements. |
| components/esp32-p4-function-ev-board/src/ethernet.cpp | Replaces board-specific esp_eth bring-up with espp::Ethernet config. |
| components/esp32-p4-function-ev-board/src/esp32-p4-function-ev-board.cpp | Updates BOOT-button vs Ethernet conflict check to query espp::Ethernet. |
| components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp | Replaces raw ETH state with std::unique_ptr<espp::Ethernet> accessors. |
| components/esp32-p4-function-ev-board/idf_component.yml | Adds dependency on espp/ethernet. |
| components/esp32-p4-function-ev-board/CMakeLists.txt | Switches BSP to require the new ethernet component. |
| components/esp32-ethernet-kit/src/esp32-ethernet-kit.cpp | Replaces inline ETH bring-up with espp::Ethernet. |
| components/esp32-ethernet-kit/include/esp32-ethernet-kit.hpp | Replaces raw ETH state with std::unique_ptr<espp::Ethernet> accessors. |
| components/esp32-ethernet-kit/idf_component.yml | Adds dependency on espp/ethernet. |
| components/esp32-ethernet-kit/CMakeLists.txt | Switches BSP to require the new ethernet component. |
| .github/workflows/build.yml | Adds new ethernet example to CI build matrix (ordering needs adjustment). |
Suppressed comments (1)
doc/Doxyfile:392
- This new
ethernet.hppentry is out of alphabetical order inINPUT(the file explicitly asks to keep this list sorted). It should be moved up near the othere*headers (betweenesp-boxandevent_manager).
$(PROJECT_PATH)/components/seeed-studio-round-display/include/seeed-studio-round-display.hpp \
$(PROJECT_PATH)/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp \
$(PROJECT_PATH)/components/ethernet/include/ethernet.hpp \
$(PROJECT_PATH)/components/socket/include/socket.hpp \
$(PROJECT_PATH)/components/socket/include/udp_socket.hpp \
2c1478d to
9de628e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.
Suppressed comments (3)
components/ethernet/src/ethernet.cpp:512
- In CLIENT mode with a static IP (
config_.ip_info.ip.addr != 0), this code stops DHCP and sets the IP, but does not updateip_addr_/connected_or invokeon_got_ip. If the stack doesn’t emitIP_EVENT_ETH_GOT_IPfor this path, the component can remain 'disconnected' despite a configured static IP. Consider mirroring the SERVER-mode behavior: when static IP is configured and link is (or becomes) up, setip_addr_, setconnected_ = true, and callon_got_ip.
// Static IP in client mode (ip.addr != 0): stop DHCP client, apply the IP.
if (!server_mode && config_.ip_info.ip.addr != 0) {
esp_err_t stop_err = esp_netif_dhcpc_stop(eth_netif_);
if (stop_err != ESP_OK && stop_err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
return fail("esp_netif_dhcpc_stop failed", stop_err, std::errc::io_error);
}
err = esp_netif_set_ip_info(eth_netif_, &config_.ip_info);
if (err != ESP_OK) {
return fail("esp_netif_set_ip_info failed", err, std::errc::io_error);
}
}
components/ethernet/src/ethernet.cpp:570
- The
deinitialize(std::error_code&)API contract says it setsecon failure and returnstrueon success, but the implementation currently ignores return values from ESP-IDF teardown calls and always returnstruewitheccleared. Either (a) check and propagate failures intoec/return value, or (b) change the contract/comments to explicitly state teardown is best-effort and cannot fail.
bool Ethernet::deinitialize(std::error_code &ec) {
ec.clear();
if (!initialized_.load() && !eth_handle_ && !eth_netif_) {
return true;
}
logger_.info("Deinitializing Ethernet");
if (client_ip_handler_registered_) {
esp_event_handler_unregister(IP_EVENT, IP_EVENT_AP_STAIPASSIGNED, &client_ip_handler);
client_ip_handler_registered_ = false;
}
if (handlers_registered_) {
esp_event_handler_unregister(IP_EVENT, IP_EVENT_ETH_LOST_IP, &lost_ip_handler);
esp_event_handler_unregister(IP_EVENT, IP_EVENT_ETH_GOT_IP, &got_ip_handler);
esp_event_handler_unregister(ETH_EVENT, ESP_EVENT_ANY_ID, ð_event_handler);
handlers_registered_ = false;
}
if (eth_handle_) {
esp_eth_stop(eth_handle_);
}
if (eth_glue_) {
esp_eth_del_netif_glue(eth_glue_);
eth_glue_ = nullptr;
}
if (eth_handle_) {
esp_eth_driver_uninstall(eth_handle_); // frees the mac + phy it owns
eth_handle_ = nullptr;
}
if (eth_netif_) {
esp_netif_destroy(eth_netif_);
eth_netif_ = nullptr;
}
initialized_ = false;
link_up_ = false;
connected_ = false;
ip_addr_ = 0;
return true;
}
components/ethernet/example/CMakeLists.txt:5
- The example forcibly disables the IDF Component Manager, but the PR’s SPI path relies on managed components (e.g.,
espressif/w5500) being pulled in whenCONFIG_ESPP_ETHERNET_W5500is enabled. With the component manager disabled, users may be unable to build the W5500 variant unless the dependency is already vendored. Consider removing this line, guarding it behind an option, or documenting explicitly that the W5500 example requires enabling the component manager.
set(ENV{IDF_COMPONENT_MANAGER} "0")
| void Ethernet::eth_event_handler(void *arg, esp_event_base_t /*base*/, int32_t id, | ||
| void * /*data*/) { | ||
| auto *self = static_cast<Ethernet *>(arg); | ||
| if (!self) { | ||
| return; | ||
| } | ||
| switch (id) { | ||
| case ETHERNET_EVENT_CONNECTED: { | ||
| self->link_up_ = true; | ||
| auto sd = self->link_speed_duplex(); |
There was a problem hiding this comment.
Fixed in eb550bc. eth_event_handler now reads the event payload (a pointer to the originating esp_eth_handle_t) and returns early unless it matches self->eth_handle_, so multiple espp::Ethernet instances no longer corrupt each other's link/connection state.
| void Ethernet::got_ip_handler(void *arg, esp_event_base_t /*base*/, int32_t /*id*/, void *data) { | ||
| auto *self = static_cast<Ethernet *>(arg); | ||
| auto *event = static_cast<ip_event_got_ip_t *>(data); | ||
| if (!self || !event) { | ||
| return; | ||
| } | ||
| self->ip_addr_ = event->ip_info.ip.addr; | ||
| self->connected_ = true; | ||
| self->logger_.info("Got IP: {}.{}.{}.{}", esp_ip4_addr1_16(&event->ip_info.ip), | ||
| esp_ip4_addr2_16(&event->ip_info.ip), esp_ip4_addr3_16(&event->ip_info.ip), | ||
| esp_ip4_addr4_16(&event->ip_info.ip)); | ||
| if (self->config_.on_got_ip) { | ||
| self->config_.on_got_ip(event->ip_info.ip); | ||
| } | ||
| } |
There was a problem hiding this comment.
Fixed in eb550bc. got_ip_handler now returns early when event->esp_netif != self->eth_netif_. The same filter is applied to lost_ip_handler — the IP_EVENT_ETH_LOST_IP payload is an ip_event_got_ip_t whose .esp_netif identifies the interface (confirmed in esp_netif_lwip.c), so it filters identically.
| idf: | ||
| version: ">=5.0" |
There was a problem hiding this comment.
Fixed in eb550bc. Bumped the manifest minimum to idf: ">=5.4". That is the real floor: esp_eth_phy_new_generic() (used for the RMII PHY) was added in v5.4, and the split esp_driver_spi/esp_driver_gpio components in REQUIRES are only separately requireable on recent IDF. Added a comment noting why.
Introduce espp::Ethernet, a single C++ class wrapping the ESP-IDF esp_eth APIs
that brings up an Ethernet interface over RMII (internal EMAC, on SoCs with
SOC_EMAC_SUPPORTED) or SPI (external MAC+PHY chip such as the WIZnet W5500), plus
a pre-built-driver escape hatch for any other chip. The interface-specific part
is a single tagged Config::interface (std::variant of RmiiConfig / SpiConfig /
DriverConfig); DHCP mode, static IP, hostname, MAC (explicit or eFuse-derived)
and the link/IP callbacks are shared.
It owns the boilerplate the BSPs otherwise duplicate (netif, event loop, glue,
DHCP client/server, static IP, hostname, MAC assignment, event dispatch) and -
unlike the inline BSP implementations - provides a symmetric teardown
(deinitialize() + destructor). Every fallible method has both a
bool f(std::error_code&) form and a logging convenience overload.
The concrete SPI chip drivers are ESP-IDF managed components pulled in only when
the matching CONFIG_ESPP_ETHERNET_* Kconfig option is enabled (W5500 -> the
espressif/w5500 component, gated via a $CONFIG{} manifest rule), so an RMII-only
project carries no extra dependency; the RMII path uses the generic 802.3 PHY
driver in esp_eth core.
As proof, both existing RMII BSPs are refactored to consume espp::Ethernet:
- esp32-ethernet-kit (fixed esp32 IO_MUX pins): ~365-line ethernet source -> ~45.
- esp32-p4-function-ev-board (routable data pins via SOC_EMAC_USE_MULTI_IO_MUX):
its ethernet source is replaced with a small RmiiConfig{...data_pins...} plus
its BOOT-button/RMII-TXD1 conflict check now queries the component.
Both keep their existing public APIs. Adds an example (RMII by default, W5500
when enabled), component + rst docs, Doxyfile/toctree wiring, and a CI
build-matrix entry.
Verified: the example builds for esp32 (RMII); an isolated build with
CONFIG_ESPP_ETHERNET_W5500 builds the SPI/W5500 path (pulling espressif/w5500);
the migrated esp32-ethernet-kit example builds for esp32 and the
esp32-p4-function-ev-board example builds for esp32p4; cppcheck and a Doxygen
parse are clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9de628e to
eb550bc
Compare
|
Ran down the ESP-IDF v5.5 CI build failure. The root cause was not `mdc_freq_hz` (that field actually exists in 5.5; my version gate just conservatively skips it) — it was `eth_esp32_emac_config_t::clock_config.rmii.clock_gpio`, which is a plain `int` in IDF v6.0 but an `emac_rmii_clock_gpio_t` enum on ESP32 in v5.x. Assigning an `int` GPIO to the enum field is an error in C++. Fixed in eb550bc by casting to the field's actual type via `static_cast<decltype(...)>()` (ESP-IDF's own `ETH_ESP32_EMAC_DEFAULT_CONFIG()` macro does the same `(emac_rmii_clock_gpio_t)` cast), so it compiles on both 5.x and 6.0. Verified against the v5.4/v5.5 esp_eth headers; the esp32 example + migrated BSPs build locally on 6.0 and cppcheck is clean. CI (5.5) will confirm. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (7)
components/ethernet/src/ethernet.cpp:13
- The comment says this compiles even when
esp_idf_version.his unavailable (e.g. under cppcheck), but the file is included unconditionally, which will fail preprocessing if the header isn’t present on the include path. Guard the include with__has_include(or a build-system define) so the fallback macros can actually take effect when the header is missing.
#include "esp_idf_version.h"
// Provide fallbacks so this file's ESP-IDF version checks are well-defined even
// when esp_idf_version.h is not available (e.g. under cppcheck).
#ifndef ESP_IDF_VERSION_VAL
#define ESP_IDF_VERSION_VAL(major, minor, patch) (((major) << 16) | ((minor) << 8) | (patch))
#endif
#ifndef ESP_IDF_VERSION
#define ESP_IDF_VERSION ESP_IDF_VERSION_VAL(0, 0, 0)
#endif
components/ethernet/src/ethernet.cpp:590
deinitialize(std::error_code& ec)always clearsecand always returnstrue, even though several ESP-IDF calls here return error codes. Either (a) check return values and setec/returnfalseon failure, or (b) simplify the API by removing thestd::error_code&overload (or documenting that deinit is best-effort and cannot fail).
bool Ethernet::deinitialize(std::error_code &ec) {
ec.clear();
if (!initialized_.load() && !eth_handle_ && !eth_netif_) {
return true;
}
logger_.info("Deinitializing Ethernet");
if (client_ip_handler_registered_) {
esp_event_handler_unregister(IP_EVENT, IP_EVENT_AP_STAIPASSIGNED, &client_ip_handler);
client_ip_handler_registered_ = false;
}
if (handlers_registered_) {
esp_event_handler_unregister(IP_EVENT, IP_EVENT_ETH_LOST_IP, &lost_ip_handler);
esp_event_handler_unregister(IP_EVENT, IP_EVENT_ETH_GOT_IP, &got_ip_handler);
esp_event_handler_unregister(ETH_EVENT, ESP_EVENT_ANY_ID, ð_event_handler);
handlers_registered_ = false;
}
if (eth_handle_) {
esp_eth_stop(eth_handle_);
}
if (eth_glue_) {
esp_eth_del_netif_glue(eth_glue_);
eth_glue_ = nullptr;
}
if (eth_handle_) {
esp_eth_driver_uninstall(eth_handle_); // frees the mac + phy it owns
eth_handle_ = nullptr;
}
if (eth_netif_) {
esp_netif_destroy(eth_netif_);
eth_netif_ = nullptr;
}
initialized_ = false;
link_up_ = false;
connected_ = false;
ip_addr_ = 0;
return true;
}
components/ethernet/example/CMakeLists.txt:5
- The PR description/documentation says SPI chip drivers are pulled in via ESP-IDF managed components gated by Kconfig, but this example project explicitly disables the component manager. That will prevent fetching
espressif/w5500(and other optional chip components) when enablingCONFIG_ESPP_ETHERNET_W5500, so the documented SPI path won’t be buildable from this example as written.
set(ENV{IDF_COMPONENT_MANAGER} "0")
components/ethernet/Kconfig:25
- The help text says DM9051/ENC28J60 “requires adding the corresponding managed component … to your project,” but this component’s
idf_component.ymlalready declares those managed dependencies with$CONFIG{}rules. Update the help text to reflect the actual behavior (auto-pulled when enabled), or clarify that the requirement only applies when not using the component manager.
config ESPP_ETHERNET_DM9051
bool "Enable Davicom DM9051 SPI Ethernet support"
default n
help
Enable the Davicom DM9051 SPI Ethernet chip in espp::Ethernet.
Requires adding the corresponding managed component (see
idf_component.yml) to your project.
config ESPP_ETHERNET_ENC28J60
bool "Enable Microchip ENC28J60 SPI Ethernet support"
default n
help
Enable the Microchip ENC28J60 SPI Ethernet chip in espp::Ethernet.
Requires adding the corresponding managed component (see
idf_component.yml) to your project.
components/esp32-ethernet-kit/idf_component.yml:21
espp/ethernetdeclares an ESP-IDF floor of>=5.4, but this BSP still advertisesidf: ">=5.0". This can allow incompatible IDF versions to be selected by the component manager and then fail at build time. Bump this BSP’sidfconstraint to>=5.4(or align it to whatever minimumespp/ethernettruly supports).
idf: ">=5.0"
espp/base_component: ">=1.0"
espp/ethernet: ">=1.0"
components/esp32-p4-function-ev-board/idf_component.yml:21
espp/ethernetrequires ESP-IDF>=5.4, but this component’s manifest allowsidf: ">=5.3". Update the constraint to avoid resolving an IDF version that can’t satisfy the transitive dependency requirements.
idf: ">=5.3"
espp/base_component: ">=1.0"
espp/ethernet: ">=1.0"
components/ethernet/example/main/ethernet_example.cpp:58
- “reactor-owned” appears to be a typo in this comment; consider changing it to “caller-owned” (or “component-owned”) to match the surrounding terminology.
// One reactor-owned W5500 over that bus, DHCP client.
Description
Adds
espp::Ethernet, a single C++ class wrapping the ESP-IDFesp_ethAPIsthat brings up an Ethernet interface over either transport with one uniform
configuration:
SOC_EMAC_SUPPORTED:ESP32, ESP32-P4) plus an external RMII PHY, using the generic 802.3 PHY driver
(IP101, LAN87xx, DP83848, RTL8201, KSZ8041, …).
for DM9051 / ENC28J60) on a caller-owned SPI bus. Works on any SoC.
DriverConfigescape hatch taking a caller-createdesp_eth_mac_t*/esp_eth_phy_t*for any other chip.The interface-specific part is a single tagged
Config::interface(
std::variant<RmiiConfig, SpiConfig, DriverConfig>); DHCP mode (client/server),static IP, hostname, MAC (explicit or eFuse-derived), and the
on_link_up/on_link_down/on_got_ip/on_lost_ip/on_client_assignedcallbacks are shared across all interfaces. Unlike the inline BSP implementations
it replaces, it also provides a symmetric teardown (
deinitialize()+destructor), and every fallible method has both a
bool f(std::error_code&)formand a logging convenience overload.
The concrete SPI chip drivers are ESP-IDF managed components pulled in only when
the matching
CONFIG_ESPP_ETHERNET_*Kconfig option is enabled (W5500 → theespressif/w5500component, gated via a$CONFIG{}manifest rule), so anRMII-only project carries no extra dependency.
As proof, both existing RMII BSPs are refactored to consume
espp::Ethernet:SOC_EMAC_USE_MULTI_IO_MUX):replaced with a small
RmiiConfig{ …data_pins… }; its BOOT-button/RMII-TXD1conflict check now queries the component.
Both keep their existing public APIs. Adds a component example (RMII by default,
W5500 when enabled), component + rst docs, Doxyfile/toctree wiring, and CI
build-matrix entries.
Motivation and Context
Every receiving Ethernet BSP previously duplicated the full
esp_ethbring-up(netif, event loop, glue, attach, DHCP client/server, static IP, hostname, MAC
assignment, event dispatch) — the ethernet-kit and P4 BSPs were near-verbatim
copies, and neither had a public teardown. This factors that ~90% of boilerplate
into one reusable, testable component so adding Ethernet to a board (or BSP)
becomes a few lines of pin config, over RMII or SPI.
How has this been tested?
lease, and a valid IP (a full bidirectional DHCP exchange, exercising the RMII
routable-data-pins path end-to-end).
CONFIG_ESPP_ETHERNET_W5500builds the SPI/W5500 path (pullingespressif/w5500); the migrated esp32-ethernet-kit example builds for esp32and the esp32-p4-function-ev-board example builds for esp32p4.
--forceover the#ifbranches) is clean; Doxygen parses
ethernet.hppwith zero warnings.I can only compile, not flash the SPI/W5500 or the ethernet-kit paths, so those
remain build-verified (the W5500 path mirrors a proven implementation and is
checked against the managed-component header API).
Screenshots (if appropriate, e.g. schematic, board, console logs, lab pictures):
Console (ESP32-P4-ETH):
Types of changes
Checklist:
Software
.github/workflows/build.ymlfile to add my new test to the automated cloud build github action.