diff --git a/components/esp32-ethernet-kit/CMakeLists.txt b/components/esp32-ethernet-kit/CMakeLists.txt new file mode 100644 index 000000000..f3befb4dc --- /dev/null +++ b/components/esp32-ethernet-kit/CMakeLists.txt @@ -0,0 +1,10 @@ +idf_component_register( + INCLUDE_DIRS "include" + SRC_DIRS "src" + REQUIRES + "base_component" + "esp_eth" + "esp_netif" + "esp_event" + REQUIRED_IDF_TARGETS "esp32" + ) diff --git a/components/esp32-ethernet-kit/README.md b/components/esp32-ethernet-kit/README.md new file mode 100644 index 000000000..a662a5f48 --- /dev/null +++ b/components/esp32-ethernet-kit/README.md @@ -0,0 +1,130 @@ +# ESP32-Ethernet-Kit A V1.2 + +[![Badge](https://components.espressif.com/components/espp/esp32-ethernet-kit/badge.svg)](https://components.espressif.com/components/espp/esp32-ethernet-kit) + +Board Support Package (BSP) for the Espressif **ESP32-Ethernet-Kit A V1.2**. + +## Official board documentation + +- [ESP32-Ethernet-Kit overview](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32/esp32-ethernet-kit/index.html) +- [ESP32-Ethernet-Kit V1.2 User Guide](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32/esp32-ethernet-kit/user_guide_v1.2.html) +- [Board schematic V1.2](https://dl.espressif.com/dl/schematics/SCH_ESP32-ETHERNET-KIT_A_V1.2_20200528.pdf) + +## What this BSP provides + +The `espp::Esp32EthernetKit` class is a singleton hardware abstraction for: + +- **10/100 Ethernet** — internal ESP32 EMAC + IP101GRI RMII PHY, selectable + DHCP client **or** DHCP server mode + +## Initialization API + +The BSP exposes two initialization entry points: + +- `initialize_ethernet()` +- `initialize_ethernet(const EthernetConfig &config)` + +The no-argument overload is equivalent to `EthernetConfig{}` (DHCP client mode, +no callbacks). + +```cpp +auto &board = espp::Esp32EthernetKit::get(); +bool ok = board.initialize_ethernet(); +``` + +## DHCP modes and callbacks + +Mode selection is done through `EthernetConfig::mode`: + +- `DhcpMode::CLIENT` (default): obtains an address from an upstream DHCP server. +- `DhcpMode::SERVER`: serves leases to connected hosts from a static interface IP. + +Callback behavior: + +- `on_link_up`: cable/link negotiated. +- `on_link_down`: link lost. +- `on_got_ip`: IPv4 becomes usable. + - Client mode: after DHCP lease is obtained. + - Server mode: when link comes up (static IP is already known). +- `on_lost_ip`: IPv4 no longer usable. +- `server_config.on_client_assigned` (server mode only): fires for each lease assignment. + +### DHCP client (default) + +```cpp +using Kit = espp::Esp32EthernetKit; + +Kit::EthernetConfig cfg; +cfg.mode = Kit::DhcpMode::CLIENT; +cfg.on_link_up = []() { + // physical link is up +}; +cfg.on_got_ip = [](esp_ip4_addr_t ip) { + // DHCP lease acquired +}; +cfg.on_lost_ip = []() { + // lease lost or interface disconnected +}; + +auto &board = Kit::get(); +bool ok = board.initialize_ethernet(cfg); +``` + +### DHCP server + +```cpp +using Kit = espp::Esp32EthernetKit; + +Kit::EthernetConfig cfg; +cfg.mode = Kit::DhcpMode::SERVER; + +// Leave ip_info all-zero to use default 192.168.4.1/24. +// Or set custom static address information: +// IP4_ADDR(&cfg.server_config.ip_info.ip, 10, 0, 0, 1); +// IP4_ADDR(&cfg.server_config.ip_info.netmask, 255, 255, 255, 0); +// IP4_ADDR(&cfg.server_config.ip_info.gw, 10, 0, 0, 1); + +cfg.on_got_ip = [](esp_ip4_addr_t ip) { + // server interface IP became active +}; +cfg.server_config.on_client_assigned = [](esp_ip4_addr_t ip, std::array mac) { + // a client received a lease +}; + +auto &board = Kit::get(); +bool ok = board.initialize_ethernet(cfg); +``` + +## RMII pin mapping + +The ESP32 RMII data-plane signals are fixed to specific GPIOs via IO_MUX and +**cannot be changed**. The control-plane signals (MDC/MDIO/PHY_RST) can be +routed via the GPIO matrix. + +| Signal | GPIO | Notes | +|--------------|------|--------------------------------------------| +| REF_CLK (in) | 0 | External 50 MHz oscillator on V1.2 | +| TX_EN | 21 | IO_MUX — fixed | +| TXD0 | 19 | IO_MUX — fixed | +| TXD1 | 22 | IO_MUX — fixed | +| CRS_DV | 27 | IO_MUX — fixed | +| RXD0 | 25 | IO_MUX — fixed | +| RXD1 | 26 | IO_MUX — fixed | +| MDC | 23 | GPIO matrix — reconfigurable | +| MDIO | 18 | GPIO matrix — reconfigurable | +| PHY_RST | 5 | Active-low; set `eth_phy_reset_gpio = -1` to skip | + +> [!WARNING] +> **GPIO0 / REF_CLK conflict.** GPIO0 is both the RMII REF_CLK input (driven by +> the on-board 50 MHz oscillator) and the BOOT strapping pin. Pressing BOOT while +> Ethernet is running briefly pulls the clock line to GND, disrupting the 50 MHz +> clock and corrupting active traffic. Do **not** use GPIO0 as a runtime input +> while Ethernet is active. + +## sdkconfig requirements + +``` +CONFIG_ETH_ENABLED=y +CONFIG_ETH_USE_ESP32_EMAC=y +CONFIG_ETH_PHY_ENABLE_IP101=y +``` diff --git a/components/esp32-ethernet-kit/example/CMakeLists.txt b/components/esp32-ethernet-kit/example/CMakeLists.txt new file mode 100644 index 000000000..67f753320 --- /dev/null +++ b/components/esp32-ethernet-kit/example/CMakeLists.txt @@ -0,0 +1,22 @@ +# 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.20) + +set(ENV{IDF_COMPONENT_MANAGER} "0") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# add the component directories that we want to use +set(EXTRA_COMPONENT_DIRS + "../../../components/" +) + +set( + COMPONENTS + "main esptool_py esp32-ethernet-kit" + CACHE STRING + "List of components to include" + ) + +project(esp32_ethernet_kit_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/esp32-ethernet-kit/example/README.md b/components/esp32-ethernet-kit/example/README.md new file mode 100644 index 000000000..6e300fa4d --- /dev/null +++ b/components/esp32-ethernet-kit/example/README.md @@ -0,0 +1,15 @@ +# ESP32-Ethernet-Kit Example + +This example shows how to use the `espp::Esp32EthernetKit` BSP to bring up the +Ethernet interface and print the assigned IP address. + +## Hardware + +[ESP32-Ethernet-Kit A V1.2](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32/esp32-ethernet-kit/index.html) + +## How to build and flash + +```bash +idf.py set-target esp32 +idf.py -p PORT flash monitor +``` diff --git a/components/esp32-ethernet-kit/example/main/CMakeLists.txt b/components/esp32-ethernet-kit/example/main/CMakeLists.txt new file mode 100644 index 000000000..7840fb139 --- /dev/null +++ b/components/esp32-ethernet-kit/example/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + SRC_DIRS "." + INCLUDE_DIRS "." + ) diff --git a/components/esp32-ethernet-kit/example/main/Kconfig.projbuild b/components/esp32-ethernet-kit/example/main/Kconfig.projbuild new file mode 100644 index 000000000..74d44ef43 --- /dev/null +++ b/components/esp32-ethernet-kit/example/main/Kconfig.projbuild @@ -0,0 +1,45 @@ +menu "ESP32-Ethernet-Kit Example Configuration" + + choice EXAMPLE_ETH_DHCP_MODE + prompt "DHCP mode" + default EXAMPLE_ETH_DHCP_CLIENT + help + Select whether the board acts as a DHCP client (acquires an IP from + an upstream router) or as a DHCP server (assigns IPs to connected hosts). + + config EXAMPLE_ETH_DHCP_CLIENT + bool "DHCP client" + help + The ESP32 requests an IP address from the network's DHCP server. + + config EXAMPLE_ETH_DHCP_SERVER + bool "DHCP server" + help + The ESP32 acts as a DHCP server. It gets a static IP and hands out + addresses to connected hosts. Configure the static IP below. + endchoice + + if EXAMPLE_ETH_DHCP_SERVER + + config EXAMPLE_ETH_SERVER_IP + string "Server static IP address" + default "192.168.4.1" + help + Static IPv4 address assigned to the ESP32's Ethernet interface when + operating as a DHCP server. + + config EXAMPLE_ETH_SERVER_NETMASK + string "Server netmask" + default "255.255.255.0" + help + Netmask for the DHCP server subnet. + + config EXAMPLE_ETH_SERVER_GW + string "Server gateway" + default "192.168.4.1" + help + Gateway address advertised to DHCP clients (usually the server's own IP). + + endif # EXAMPLE_ETH_DHCP_SERVER + +endmenu diff --git a/components/esp32-ethernet-kit/example/main/esp32_ethernet_kit_example.cpp b/components/esp32-ethernet-kit/example/main/esp32_ethernet_kit_example.cpp new file mode 100644 index 000000000..79472af10 --- /dev/null +++ b/components/esp32-ethernet-kit/example/main/esp32_ethernet_kit_example.cpp @@ -0,0 +1,101 @@ +#include +#include + +#include + +#include "esp32-ethernet-kit.hpp" +#include "logger.hpp" + +#if CONFIG_EXAMPLE_ETH_DHCP_SERVER +#include +#endif + +using namespace std::chrono_literals; +using DhcpMode = espp::Esp32EthernetKit::DhcpMode; +using EthConfig = espp::Esp32EthernetKit::EthernetConfig; + +extern "C" void app_main(void) { + espp::Logger logger({.tag = "EthKitExample", .level = espp::Logger::Verbosity::INFO}); + logger.info("ESP32-Ethernet-Kit A V1.2 example starting"); + + // Example 1: get the singleton board instance + //! [esp32 ethernet kit get instance] + auto &board = espp::Esp32EthernetKit::get(); + //! [esp32 ethernet kit get instance] + +#if CONFIG_EXAMPLE_ETH_DHCP_SERVER + // Example 2: init as DHCP server -- ESP32 assigns IPs to connected hosts. + // ServerConfig::ip_info zero -> default 192.168.4.1/24. Supply Kconfig + // values (EXAMPLE_ETH_SERVER_IP / _NETMASK / _GW) for a custom static IP. + //! [esp32 ethernet kit dhcp server] + espp::Esp32EthernetKit::ServerConfig srv_cfg; + ip4addr_aton(CONFIG_EXAMPLE_ETH_SERVER_IP, reinterpret_cast(&srv_cfg.ip_info.ip)); + ip4addr_aton(CONFIG_EXAMPLE_ETH_SERVER_NETMASK, + reinterpret_cast(&srv_cfg.ip_info.netmask)); + ip4addr_aton(CONFIG_EXAMPLE_ETH_SERVER_GW, reinterpret_cast(&srv_cfg.ip_info.gw)); + srv_cfg.on_client_assigned = [&](esp_ip4_addr_t ip, std::array mac) { + logger.info("Client assigned {}.{}.{}.{} (mac {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x})", + esp_ip4_addr1_16(&ip), esp_ip4_addr2_16(&ip), esp_ip4_addr3_16(&ip), + esp_ip4_addr4_16(&ip), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + }; + bool eth_ok = board.initialize_ethernet({ + .mode = DhcpMode::SERVER, + .server_config = srv_cfg, + .on_link_up = [&]() { logger.info("Ethernet link up"); }, + .on_link_down = [&]() { logger.warn("Ethernet link down"); }, + .on_got_ip = + [&](esp_ip4_addr_t ip) { + logger.info("DHCP server up at {}.{}.{}.{}", esp_ip4_addr1_16(&ip), + esp_ip4_addr2_16(&ip), esp_ip4_addr3_16(&ip), esp_ip4_addr4_16(&ip)); + }, + .on_lost_ip = [&]() { logger.warn("Ethernet lost IP"); }, + }); + //! [esp32 ethernet kit dhcp server] +#else + // Example 3: init as DHCP client -- ESP32 acquires an IP from the network. + //! [esp32 ethernet kit dhcp client] + bool eth_ok = board.initialize_ethernet({ + .mode = DhcpMode::CLIENT, + .on_link_up = [&]() { logger.info("Ethernet link up"); }, + .on_link_down = [&]() { logger.warn("Ethernet link down"); }, + .on_got_ip = + [&](esp_ip4_addr_t ip) { + logger.info("DHCP lease acquired: {}.{}.{}.{}", esp_ip4_addr1_16(&ip), + esp_ip4_addr2_16(&ip), esp_ip4_addr3_16(&ip), esp_ip4_addr4_16(&ip)); + }, + .on_lost_ip = [&]() { logger.warn("Ethernet lost IP"); }, + }); + //! [esp32 ethernet kit dhcp client] +#endif + + if (!eth_ok) { + logger.error("Ethernet initialization failed"); + return; + } + + // SERVER mode: connected as soon as the cable is plugged in (static IP). + // CLIENT mode: connected once the DHCP lease is granted (up to ~30 s). + logger.info("Waiting for Ethernet..."); + for (int i = 0; i < 60 && !board.is_ethernet_connected(); ++i) { + std::this_thread::sleep_for(500ms); + } + + if (board.is_ethernet_connected()) { + auto ip = board.ethernet_ip(); + logger.info("Connected. IP: {}.{}.{}.{}", esp_ip4_addr1_16(&ip), esp_ip4_addr2_16(&ip), + esp_ip4_addr3_16(&ip), esp_ip4_addr4_16(&ip)); + } else { + logger.warn("Ethernet not ready within 30 s"); + } + + while (true) { + std::this_thread::sleep_for(5s); + if (board.is_ethernet_connected()) { + auto ip = board.ethernet_ip(); + logger.info("Ethernet up, IP: {}.{}.{}.{}", esp_ip4_addr1_16(&ip), esp_ip4_addr2_16(&ip), + esp_ip4_addr3_16(&ip), esp_ip4_addr4_16(&ip)); + } else { + logger.warn("Ethernet not connected"); + } + } +} \ No newline at end of file diff --git a/components/esp32-ethernet-kit/example/sdkconfig.defaults b/components/esp32-ethernet-kit/example/sdkconfig.defaults new file mode 100644 index 000000000..35b87569c --- /dev/null +++ b/components/esp32-ethernet-kit/example/sdkconfig.defaults @@ -0,0 +1,13 @@ +CONFIG_IDF_TARGET="esp32" + +# Ethernet +CONFIG_ETH_ENABLED=y +CONFIG_ETH_USE_ESP32_EMAC=y +CONFIG_ETH_PHY_ENABLE_IP101=y + +# FreeRTOS tick rate +CONFIG_FREERTOS_HZ=1000 + +# Allow RMII clock on GPIO0 (strapping pin warning suppressed by IDF when used +# as EMAC_CLK_EXT_IN input — GPIO0 is configured as input by the EMAC driver). +CONFIG_ESP_SYSTEM_GPIO_MATRIX_BYPASS=n diff --git a/components/esp32-ethernet-kit/idf_component.yml b/components/esp32-ethernet-kit/idf_component.yml new file mode 100644 index 000000000..8ee569f00 --- /dev/null +++ b/components/esp32-ethernet-kit/idf_component.yml @@ -0,0 +1,22 @@ +## IDF Component Manager Manifest File +license: "MIT" +description: "ESP32-Ethernet-Kit A V1.2 Board Support Package (BSP) component in C++" +url: "https://github.com/esp-cpp/espp/tree/main/components/esp32-ethernet-kit" +repository: "git://github.com/esp-cpp/espp.git" +maintainers: + - William Emfinger +documentation: "https://esp-cpp.github.io/espp/dev_boards/espressif/esp32_ethernet_kit.html" +examples: + - path: example +tags: + - cpp + - Component + - BSP + - ESP32 + - Ethernet + - EthernetKit +dependencies: + idf: ">=5.0" + espp/base_component: ">=1.0" +targets: + - esp32 diff --git a/components/esp32-ethernet-kit/include/esp32-ethernet-kit.hpp b/components/esp32-ethernet-kit/include/esp32-ethernet-kit.hpp new file mode 100644 index 000000000..3f8a50f81 --- /dev/null +++ b/components/esp32-ethernet-kit/include/esp32-ethernet-kit.hpp @@ -0,0 +1,186 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include + +#include "base_component.hpp" + +namespace espp { +/// @brief Board Support Package (BSP) for the Espressif ESP32-Ethernet-Kit A V1.2. +/// +/// This class provides a singleton interface to the board's Ethernet peripheral: +/// - 10/100 Ethernet via the internal ESP32 EMAC and an IP101GRI RMII PHY +/// +/// RMII pin mapping (fixed in the ESP32 IO_MUX; cannot be changed): +/// | Signal | GPIO | +/// |----------|------| +/// | REF_CLK | 0 | ← external 50 MHz oscillator (EMAC_CLK_EXT_IN) +/// | TX_EN | 21 | +/// | TXD0 | 19 | +/// | TXD1 | 22 | +/// | CRS_DV | 27 | +/// | RXD0 | 25 | +/// | RXD1 | 26 | +/// | MDC | 23 | +/// | MDIO | 18 | +/// | PHY_RST | 5 | +/// +/// The class is a singleton and can be accessed via get(). +/// +/// \section esp32_ethernet_kit_example Example +/// \subsection esp32_ethernet_kit_get_instance Get Instance +/// \snippet esp32_ethernet_kit_example.cpp esp32 ethernet kit get instance +/// \subsection esp32_ethernet_kit_dhcp_server DHCP Server +/// \snippet esp32_ethernet_kit_example.cpp esp32 ethernet kit dhcp server +/// \subsection esp32_ethernet_kit_dhcp_client DHCP Client +/// \snippet esp32_ethernet_kit_example.cpp esp32 ethernet kit dhcp client +class Esp32EthernetKit : public BaseComponent { +public: + /// Callback invoked (SERVER mode only) each time the DHCP server assigns an + /// IP address to a connected client. + using client_ip_callback_t = std::function mac)>; + + /// Callback invoked when the Ethernet link state changes (comes up or goes + /// down) or when the IP address is lost. + /// \note Runs in the ESP-IDF event-loop task context — return quickly, do not block. + using EthernetLinkCallback = std::function; + + /// Callback invoked when the interface obtains an IPv4 address. + /// \param ip The assigned IPv4 address. + /// \note Runs in the ESP-IDF event-loop task context — return quickly, do not block. + using EthernetIpCallback = std::function; + + /// DHCP operating mode for the Ethernet interface + enum class DhcpMode { + CLIENT, ///< DHCP client — acquire an IP from an upstream server (default) + SERVER, ///< DHCP server — assign IPs to hosts connected to this interface + }; + + /// Static IP configuration used when operating as a DHCP server. + /// Leave \c ip_info zero-initialised to use the built-in defaults + /// (192.168.4.1 / 255.255.255.0 / gw 192.168.4.1). + struct ServerConfig { + esp_netif_ip_info_t ip_info{ + .ip = 0, .netmask = 0, .gw = 0}; ///< IP / netmask / gateway; zero → 192.168.4.1/24 + client_ip_callback_t on_client_assigned{ + nullptr}; ///< Called each time a client is assigned an IP + }; + + /// Configuration for the Ethernet interface. + struct EthernetConfig { + /// DHCP operating mode (CLIENT or SERVER). + DhcpMode mode{DhcpMode::CLIENT}; + + /// Static IP / DHCP server settings — only used when mode == SERVER. + ServerConfig server_config{}; + + /// Called when the physical link comes up (cable connected + negotiated). + EthernetLinkCallback on_link_up{nullptr}; + + /// Called when the physical link goes down (cable disconnected). + EthernetLinkCallback on_link_down{nullptr}; + + /// Called when the interface is assigned an IPv4 address. + /// CLIENT mode: fired by the DHCP lease. + /// SERVER mode: fired immediately when the link comes up (static IP). + EthernetIpCallback on_got_ip{nullptr}; + + /// Called when the interface loses its IPv4 address + /// (DHCP lease loss in CLIENT mode, or link-down in SERVER mode). + EthernetLinkCallback on_lost_ip{nullptr}; + }; + + /// @brief Access the singleton instance + /// @return Reference to the singleton instance + static Esp32EthernetKit &get() { + static Esp32EthernetKit instance; + return instance; + } + + Esp32EthernetKit(const Esp32EthernetKit &) = delete; + Esp32EthernetKit &operator=(const Esp32EthernetKit &) = delete; + Esp32EthernetKit(Esp32EthernetKit &&) = delete; + Esp32EthernetKit &operator=(Esp32EthernetKit &&) = delete; + + ///////////////////////////////////////////////////////////////////////////// + // Ethernet (EMAC + IP101GRI RMII PHY) + ///////////////////////////////////////////////////////////////////////////// + + /// Initialize the Ethernet interface (EMAC + IP101GRI RMII PHY). + /// \param config Ethernet configuration (DHCP mode, callbacks). + /// All fields have defaults so \c EthernetConfig{} gives a + /// plain DHCP-client interface with no callbacks. + /// \return True if Ethernet was successfully initialized and started. + /// \note Requires the ESP-IDF default event loop. The BSP creates it if needed. + bool initialize_ethernet(const EthernetConfig &config); + + /// Initialize Ethernet with default configuration (DHCP client mode). + /// \return True if Ethernet was successfully initialized and started. + bool initialize_ethernet(); + + /// Check whether the interface has a usable IP address + /// (DHCP lease granted in CLIENT mode, or link is up in SERVER mode). + /// \return True if the interface is connected with a valid IP. + bool is_ethernet_connected() const { return ethernet_connected_; } + + /// Get the most recently acquired IPv4 address (0 if none). + /// \return The IPv4 address. + esp_ip4_addr_t ethernet_ip() const { return ethernet_ip_; } + +protected: + Esp32EthernetKit(); + + ///////////////////////////////////////////////////////////////////////////// + // RMII / EMAC pin constants + ///////////////////////////////////////////////////////////////////////////// + // The ESP32 RMII data-plane signals are fixed via IO_MUX and cannot be + // reassigned (TX_EN=21, TXD0=19, TXD1=22, CRS_DV=27, RXD0=25, RXD1=26). + // They are listed here for documentation only; the EMAC driver selects them + // automatically when EMAC_DATA_INTERFACE_RMII is chosen. + + // RMII REF_CLK input: external 50 MHz oscillator on V1.2 drives GPIO0 (EMAC_CLK_IN_GPIO) + static constexpr int rmii_clk_gpio = 0; // EMAC_CLK_IN_GPIO — fixed by ESP32 IO_MUX + + // SMI (management interface) — routable via GPIO matrix + static constexpr int eth_mdc_io = 23; + static constexpr int eth_mdio_io = 18; + + // IP101GRI PHY + static constexpr int eth_phy_reset_gpio = 5; ///< Active-low; set to -1 to disable + static constexpr int eth_phy_addr = 1; + + ///////////////////////////////////////////////////////////////////////////// + // Member variables + ///////////////////////////////////////////////////////////////////////////// + + // Ethernet + DhcpMode dhcp_mode_{DhcpMode::CLIENT}; + esp_netif_ip_info_t server_ip_info_{}; ///< Resolved static IP (server mode only) + client_ip_callback_t client_ip_callback_{nullptr}; + EthernetLinkCallback on_link_up_{}; + EthernetLinkCallback on_link_down_{}; + EthernetIpCallback on_got_ip_{}; + EthernetLinkCallback on_lost_ip_{}; + std::atomic ethernet_initialized_{false}; + std::atomic ethernet_connected_{false}; + esp_ip4_addr_t ethernet_ip_{}; + esp_eth_handle_t eth_handle_{nullptr}; + esp_eth_netif_glue_handle_t eth_glue_{nullptr}; // esp_eth_netif_glue_handle_t + esp_netif_t *eth_netif_{nullptr}; + + static void ethernet_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data); + static void ethernet_got_ip_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data); + static void ethernet_lost_ip_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data); + static void ethernet_client_ip_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data); +}; // class Esp32EthernetKit +} // namespace espp diff --git a/components/esp32-ethernet-kit/src/esp32-ethernet-kit.cpp b/components/esp32-ethernet-kit/src/esp32-ethernet-kit.cpp new file mode 100644 index 000000000..6456a0aa3 --- /dev/null +++ b/components/esp32-ethernet-kit/src/esp32-ethernet-kit.cpp @@ -0,0 +1,364 @@ +#include "esp32-ethernet-kit.hpp" + +#include "esp_idf_version.h" +#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 + +#include +#include +#include +#include +#include +#include + +#include + +namespace espp { + +Esp32EthernetKit::Esp32EthernetKit() + : BaseComponent("Esp32EthernetKit") {} + +void Esp32EthernetKit::ethernet_event_handler(void *arg, esp_event_base_t /*event_base*/, + int32_t event_id, void * /*event_data*/) { + auto *self = static_cast(arg); + if (!self) { + return; + } + switch (event_id) { + case ETHERNET_EVENT_CONNECTED: { + eth_speed_t speed = ETH_SPEED_10M; + eth_duplex_t duplex = ETH_DUPLEX_HALF; + if (self->eth_handle_) { + esp_eth_ioctl(self->eth_handle_, ETH_CMD_G_SPEED, &speed); + esp_eth_ioctl(self->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex); + } + self->logger_.info("Ethernet link up: {} Mbps, {} duplex", speed == ETH_SPEED_100M ? 100 : 10, + duplex == ETH_DUPLEX_FULL ? "full" : "half"); + if (self->on_link_up_) { + self->on_link_up_(); + } + // In server mode the IP is static — fire got_ip immediately + if (self->dhcp_mode_ == DhcpMode::SERVER) { + self->ethernet_ip_ = self->server_ip_info_.ip; + self->ethernet_connected_ = true; + if (self->on_got_ip_) { + self->on_got_ip_(self->server_ip_info_.ip); + } + } + break; + } + case ETHERNET_EVENT_DISCONNECTED: + self->logger_.info("Ethernet link down"); + if (self->ethernet_connected_) { + self->ethernet_connected_ = false; + if (self->on_lost_ip_) { + self->on_lost_ip_(); + } + } + self->ethernet_ip_ = {}; + if (self->on_link_down_) { + self->on_link_down_(); + } + break; + case ETHERNET_EVENT_START: + self->logger_.info("Ethernet started"); + break; + case ETHERNET_EVENT_STOP: + self->logger_.info("Ethernet stopped"); + break; + default: + break; + } +} + +void Esp32EthernetKit::ethernet_got_ip_handler(void *arg, esp_event_base_t /*event_base*/, + int32_t /*event_id*/, void *event_data) { + auto *self = static_cast(arg); + if (!self) { + return; + } + auto *event = static_cast(event_data); + self->ethernet_ip_ = event->ip_info.ip; + self->ethernet_connected_ = true; + self->logger_.info("Ethernet 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->on_got_ip_) { + self->on_got_ip_(event->ip_info.ip); + } +} + +void Esp32EthernetKit::ethernet_lost_ip_handler(void *arg, esp_event_base_t /*event_base*/, + int32_t /*event_id*/, void * /*event_data*/) { + auto *self = static_cast(arg); + if (!self) { + return; + } + self->logger_.info("Ethernet lost IP"); + self->ethernet_connected_ = false; + self->ethernet_ip_ = {}; + if (self->on_lost_ip_) { + self->on_lost_ip_(); + } +} + +void Esp32EthernetKit::ethernet_client_ip_handler(void *arg, esp_event_base_t /*event_base*/, + int32_t /*event_id*/, void *event_data) { + auto *self = static_cast(arg); + if (!self) { + return; + } + auto *event = static_cast(event_data); + // Filter: only handle leases from our Ethernet DHCP server netif + if (event->esp_netif != self->eth_netif_) { + return; + } + std::array mac; + std::copy(std::begin(event->mac), std::end(event->mac), mac.begin()); + self->logger_.info( + "DHCP server assigned {}.{}.{}.{} to {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + esp_ip4_addr1_16(&event->ip), esp_ip4_addr2_16(&event->ip), esp_ip4_addr3_16(&event->ip), + esp_ip4_addr4_16(&event->ip), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + if (self->client_ip_callback_) { + self->client_ip_callback_(event->ip, mac); + } +} + +bool Esp32EthernetKit::initialize_ethernet() { return initialize_ethernet(EthernetConfig{}); } + +bool Esp32EthernetKit::initialize_ethernet(const EthernetConfig &config) { + if (ethernet_initialized_) { + logger_.warn("Ethernet already initialized"); + return true; + } + + const DhcpMode mode = config.mode; + logger_.info("Initializing Ethernet (EMAC + IP101GRI RMII, DHCP {})", + mode == DhcpMode::SERVER ? "server" : "client"); + dhcp_mode_ = mode; + on_link_up_ = config.on_link_up; + on_link_down_ = config.on_link_down; + on_got_ip_ = config.on_got_ip; + on_lost_ip_ = config.on_lost_ip; + client_ip_callback_ = config.server_config.on_client_assigned; + + esp_eth_mac_t *mac = nullptr; + esp_eth_phy_t *phy = nullptr; + bool eth_handler_registered = false; + bool got_ip_handler_registered = false; + bool lost_ip_handler_registered = false; + bool client_ip_handler_registered = false; + + auto fail = [&](const char *message, esp_err_t err) { + logger_.error("{}: {}", message, esp_err_to_name(err)); + + if (client_ip_handler_registered) { + esp_event_handler_unregister(IP_EVENT, IP_EVENT_AP_STAIPASSIGNED, + &Esp32EthernetKit::ethernet_client_ip_handler); + } + if (lost_ip_handler_registered) { + esp_event_handler_unregister(IP_EVENT, IP_EVENT_ETH_LOST_IP, + &Esp32EthernetKit::ethernet_lost_ip_handler); + } + if (got_ip_handler_registered) { + esp_event_handler_unregister(IP_EVENT, IP_EVENT_ETH_GOT_IP, + &Esp32EthernetKit::ethernet_got_ip_handler); + } + if (eth_handler_registered) { + esp_event_handler_unregister(ETH_EVENT, ESP_EVENT_ANY_ID, + &Esp32EthernetKit::ethernet_event_handler); + } + + if (eth_handle_) { + // Best-effort stop in case the driver was already started. + 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_); + eth_handle_ = nullptr; + } + if (phy) { + phy->del(phy); + phy = nullptr; + } + if (mac) { + mac->del(mac); + mac = nullptr; + } + if (eth_netif_) { + esp_netif_destroy(eth_netif_); + eth_netif_ = nullptr; + } + + // Reset observable runtime state so retries start from a clean slate. + ethernet_initialized_ = false; + ethernet_connected_ = false; + ethernet_ip_ = {}; + server_ip_info_ = {}; + dhcp_mode_ = DhcpMode::CLIENT; + on_link_up_ = nullptr; + on_link_down_ = nullptr; + on_got_ip_ = nullptr; + on_lost_ip_ = nullptr; + client_ip_callback_ = nullptr; + + return false; + }; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + + // ESP32-Ethernet-Kit A V1.2 uses an external 50 MHz oscillator connected to + // GPIO0 as the RMII reference clock (EMAC_CLK_EXT_IN). GPIO0 is also the BOOT + // strapping pin; see the class-level warning about the conflict. + // + // Note: on ESP32 the RMII data-plane pins are fixed via IO_MUX (TX_EN=21, + // TXD0=19, TXD1=22, CRS_DV=27, RXD0=25, RXD1=26) so SOC_EMAC_USE_MULTI_IO_MUX + // is not defined and emac_dataif_gpio does not exist in the struct. + // SOC_EMAC_RMII_CLK_OUT_INTERNAL_LOOPBACK=1 on ESP32, so clock_config_out_in + // is also not present in the struct. + eth_esp32_emac_config_t esp32_emac_config = { + .smi_gpio = {.mdc_num = eth_mdc_io, .mdio_num = eth_mdio_io}, + .interface = EMAC_DATA_INTERFACE_RMII, + .clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, + .clock_gpio = static_cast(rmii_clk_gpio)}}, + .dma_burst_len = ETH_DMA_BURST_LEN_32, + .intr_priority = 0, +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + .mdc_freq_hz = 0, +#endif + }; +#pragma GCC diagnostic pop + + phy_config.phy_addr = eth_phy_addr; + phy_config.reset_gpio_num = eth_phy_reset_gpio; + + logger_.info("Creating ESP32 EMAC"); + mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); + if (!mac) { + return fail("Failed to create EMAC", ESP_FAIL); + } + + logger_.info("Creating IP101GRI PHY"); + phy = esp_eth_phy_new_ip101(&phy_config); + if (!phy) { + return fail("Failed to create PHY", ESP_FAIL); + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy); +#pragma GCC diagnostic pop + + eth_handle_ = nullptr; + logger_.info("Installing Ethernet driver"); + esp_err_t ret = esp_eth_driver_install(ð_config, ð_handle_); + if (ret != ESP_OK) { + return fail("esp_eth_driver_install failed", ret); + } + // Ownership of MAC/PHY objects transfers to the driver after install. + mac = nullptr; + phy = nullptr; + + ret = esp_netif_init(); + if (ret != ESP_OK) { + return fail("esp_netif_init failed", ret); + } + + ret = esp_event_loop_create_default(); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + return fail("esp_event_loop_create_default failed", ret); + } + + // Create Ethernet netif — server mode uses a proper DHCP-server-flagged netif so that + // esp_netif allocates dhcps_t and wires up dhcps_set_new_lease_cb, which then fires + // IP_EVENT_AP_STAIPASSIGNED for every lease. Client mode uses the standard ETH default. + if (mode == DhcpMode::SERVER) { + esp_netif_ip_info_t ip_info = config.server_config.ip_info; + if (ip_info.ip.addr == 0) { + IP4_ADDR(&ip_info.ip, 192, 168, 4, 1); + IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0); + IP4_ADDR(&ip_info.gw, 192, 168, 4, 1); + } + server_ip_info_ = ip_info; + + // ESP_NETIF_FLAG_AUTOUP causes the DHCP server to start at ETHERNET_EVENT_START + // (before the cable is connected), so dhcps_set_new_lease_cb is properly registered + // and will fire IP_EVENT_AP_STAIPASSIGNED on each lease. + esp_netif_inherent_config_t dhcps_cfg = {}; + dhcps_cfg.flags = (esp_netif_flags_t)(ESP_NETIF_DHCP_SERVER | ESP_NETIF_FLAG_AUTOUP); + dhcps_cfg.ip_info = &ip_info; + dhcps_cfg.if_key = "ETH_DHCPS"; + dhcps_cfg.if_desc = "eth"; + dhcps_cfg.route_prio = 50; + esp_netif_config_t netif_cfg = {}; + netif_cfg.base = &dhcps_cfg; + netif_cfg.stack = ESP_NETIF_NETSTACK_DEFAULT_ETH; + eth_netif_ = esp_netif_new(&netif_cfg); + } else { + esp_netif_config_t netif_cfg = ESP_NETIF_DEFAULT_ETH(); + eth_netif_ = esp_netif_new(&netif_cfg); + } + if (!eth_netif_) { + return fail("Failed to create Ethernet netif", ESP_FAIL); + } + + eth_glue_ = esp_eth_new_netif_glue(eth_handle_); + if (!eth_glue_) { + return fail("Failed to create Ethernet netif glue", ESP_FAIL); + } + ret = esp_netif_attach(eth_netif_, eth_glue_); + if (ret != ESP_OK) { + return fail("esp_netif_attach failed", ret); + } + + ret = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, ðernet_event_handler, this); + if (ret != ESP_OK) { + return fail("Failed to register Ethernet event handler", ret); + } + eth_handler_registered = true; + + ret = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, ðernet_got_ip_handler, this); + if (ret != ESP_OK) { + return fail("Failed to register Ethernet got-IP handler", ret); + } + got_ip_handler_registered = true; + + ret = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_LOST_IP, ðernet_lost_ip_handler, this); + if (ret != ESP_OK) { + return fail("Failed to register Ethernet lost-IP handler", ret); + } + lost_ip_handler_registered = true; + + if (mode == DhcpMode::SERVER) { + // IP_EVENT_AP_STAIPASSIGNED fires for every client the DHCP server serves + ret = esp_event_handler_register(IP_EVENT, IP_EVENT_AP_STAIPASSIGNED, + ðernet_client_ip_handler, this); + if (ret != ESP_OK) { + return fail("Failed to register DHCP-server client-IP handler", ret); + } + client_ip_handler_registered = true; + } + + ret = esp_eth_start(eth_handle_); + if (ret != ESP_OK) { + return fail("esp_eth_start failed", ret); + } + + ethernet_initialized_ = true; + logger_.info("Ethernet initialized (DHCP {})", + mode == DhcpMode::SERVER ? "server" : "client — waiting for link/DHCP"); + return true; +} + +} // namespace espp diff --git a/doc/Doxyfile b/doc/Doxyfile index e5b3f886e..990e7b412 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -103,8 +103,9 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/dns_server/example/main/dns_server_example.cpp \ $(PROJECT_PATH)/components/drv2605/example/main/drv2605_example.cpp \ $(PROJECT_PATH)/components/encoder/example/main/encoder_example.cpp \ - $(PROJECT_PATH)/components/esp32-timer-cam/example/main/esp_timer_cam_example.cpp \ + $(PROJECT_PATH)/components/esp32-ethernet-kit/example/main/esp32_ethernet_kit_example.cpp \ $(PROJECT_PATH)/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp \ + $(PROJECT_PATH)/components/esp32-timer-cam/example/main/esp_timer_cam_example.cpp \ $(PROJECT_PATH)/components/esp-box/example/main/esp_box_example.cpp \ $(PROJECT_PATH)/components/event_manager/example/main/event_manager_example.cpp \ $(PROJECT_PATH)/components/expressive_eyes/example/main/expressive_eyes_example.cpp \ @@ -254,8 +255,9 @@ INPUT = \ $(PROJECT_PATH)/components/drv2605/include/drv2605_menu.hpp \ $(PROJECT_PATH)/components/encoder/include/abi_encoder.hpp \ $(PROJECT_PATH)/components/encoder/include/encoder_types.hpp \ - $(PROJECT_PATH)/components/esp32-timer-cam/include/esp32-timer-cam.hpp \ + $(PROJECT_PATH)/components/esp32-ethernet-kit/include/esp32-ethernet-kit.hpp \ $(PROJECT_PATH)/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp \ + $(PROJECT_PATH)/components/esp32-timer-cam/include/esp32-timer-cam.hpp \ $(PROJECT_PATH)/components/esp-box/include/esp-box.hpp \ $(PROJECT_PATH)/components/event_manager/include/event_manager.hpp \ $(PROJECT_PATH)/components/expressive_eyes/include/expressive_eyes.hpp \ diff --git a/doc/en/dev_boards/espressif/esp32_ethernet_kit.rst b/doc/en/dev_boards/espressif/esp32_ethernet_kit.rst new file mode 100644 index 000000000..2785c5b0a --- /dev/null +++ b/doc/en/dev_boards/espressif/esp32_ethernet_kit.rst @@ -0,0 +1,77 @@ +ESP32-Ethernet-Kit A V1.2 +************************* + +Esp32-Ethernet-Kit +------------------ + +The ESP32-Ethernet-Kit A V1.2 is an Espressif development board built around +the ESP32, featuring a wired 10/100 Ethernet port via the on-board IP101GRI +PHY connected to the ESP32's internal EMAC over RMII. + +The `espp::Esp32EthernetKit` component provides a singleton hardware abstraction +for initializing the Ethernet interface in either **DHCP client** or **DHCP +server** mode. + +**DHCP client** (default): the ESP32 requests an IP address from an upstream +router or switch. The ``on_link_up`` callback fires once the DHCP lease is +granted. + +**DHCP server**: the ESP32 assigns IP addresses to connected hosts. The interface +uses a static IP (default ``192.168.4.1/24``, configurable via +:cpp:class:`espp::Esp32EthernetKit::ServerConfig`). The ``on_link_up`` callback +fires immediately when the cable is connected. An optional +``on_client_assigned`` callback fires each time a DHCP lease is issued to a +client. + +.. warning:: + + **GPIO0 / REF_CLK conflict.** GPIO0 is both the RMII REF_CLK input (driven + by the on-board 50 MHz oscillator) and the ESP32 BOOT strapping pin. + Pressing BOOT while Ethernet is active briefly pulls the clock line to GND, + disrupting the 50 MHz clock and corrupting active traffic. Do **not** use + GPIO0 as a runtime input while Ethernet is active. + +RMII pin mapping (fixed via ESP32 IO_MUX; cannot be reassigned): + ++-------------+------+------------------------------------------+ +| Signal | GPIO | Notes | ++=============+======+==========================================+ +| REF_CLK in | 0 | External 50 MHz oscillator (V1.2) | ++-------------+------+------------------------------------------+ +| TX_EN | 21 | IO_MUX — fixed | ++-------------+------+------------------------------------------+ +| TXD0 | 19 | IO_MUX — fixed | ++-------------+------+------------------------------------------+ +| TXD1 | 22 | IO_MUX — fixed | ++-------------+------+------------------------------------------+ +| CRS_DV | 27 | IO_MUX — fixed | ++-------------+------+------------------------------------------+ +| RXD0 | 25 | IO_MUX — fixed | ++-------------+------+------------------------------------------+ +| RXD1 | 26 | IO_MUX — fixed | ++-------------+------+------------------------------------------+ +| MDC | 23 | GPIO matrix — reconfigurable | ++-------------+------+------------------------------------------+ +| MDIO | 18 | GPIO matrix — reconfigurable | ++-------------+------+------------------------------------------+ +| PHY_RST | 5 | Active-low | ++-------------+------+------------------------------------------+ + +Official board documentation: + +- `ESP32-Ethernet-Kit overview `_ +- `ESP32-Ethernet-Kit V1.2 User Guide `_ +- `Board schematic V1.2 (PDF) `_ + +.. ------------------------------- Example ------------------------------------- + +.. toctree:: + + esp32_ethernet_kit_example + +.. ---------------------------- API Reference ---------------------------------- + +API Reference +------------- + +.. include-build-file:: inc/esp32-ethernet-kit.inc diff --git a/doc/en/dev_boards/espressif/esp32_ethernet_kit_example.md b/doc/en/dev_boards/espressif/esp32_ethernet_kit_example.md new file mode 100644 index 000000000..5abc0790f --- /dev/null +++ b/doc/en/dev_boards/espressif/esp32_ethernet_kit_example.md @@ -0,0 +1,2 @@ +```{include} ../../components/esp32-ethernet-kit/example/README.md +``` diff --git a/doc/en/dev_boards/espressif/index.rst b/doc/en/dev_boards/espressif/index.rst index 443e55843..91f7b0b87 100644 --- a/doc/en/dev_boards/espressif/index.rst +++ b/doc/en/dev_boards/espressif/index.rst @@ -5,6 +5,7 @@ Espressif Boards :maxdepth: 1 esp_box + esp32_ethernet_kit esp32_p4_function_ev_board esp32_timer_cam wrover_kit