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..950ed867c --- /dev/null +++ b/components/esp32-ethernet-kit/README.md @@ -0,0 +1,103 @@ +# 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 + +## DHCP modes + +`initialize_ethernet` accepts a `DhcpMode` parameter (default `CLIENT`). + +### DHCP client (default) + +The ESP32 requests an IP address from an upstream router/switch. +The `on_link_up` callback fires once the DHCP lease is granted. + +```cpp +auto &board = espp::Esp32EthernetKit::get(); + +board.initialize_ethernet( + [](esp_ip4_addr_t ip) { + // called when lease is acquired + }, + espp::Esp32EthernetKit::DhcpMode::CLIENT); // default — can be omitted +``` + +### DHCP server + +The ESP32 assigns IP addresses to connected hosts using a static IP. +The `on_link_up` callback fires when the cable is connected (IP is static, so +it is known immediately). An optional `on_client_assigned` callback fires each +time a client receives a lease. + +```cpp +using DhcpMode = espp::Esp32EthernetKit::DhcpMode; +using ServerConfig = espp::Esp32EthernetKit::ServerConfig; + +auto &board = espp::Esp32EthernetKit::get(); + +ServerConfig cfg; +// Leave cfg.ip_info zero to use the built-in default: 192.168.4.1 / 255.255.255.0 +// Or set a custom address: +// IP4_ADDR(&cfg.ip_info.ip, 10, 0, 0, 1); +// IP4_ADDR(&cfg.ip_info.netmask, 255, 255, 255, 0); +// IP4_ADDR(&cfg.ip_info.gw, 10, 0, 0, 1); + +cfg.on_client_assigned = [](esp_ip4_addr_t ip, std::array mac) { + // fired each time a client gets a DHCP lease +}; + +board.initialize_ethernet( + [](esp_ip4_addr_t ip) { + // called when link is up (static IP is already known) + }, + DhcpMode::SERVER, + 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..c3bbd1284 --- /dev/null +++ b/components/esp32-ethernet-kit/example/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "." + INCLUDE_DIRS "." + REQUIRES "esp32-ethernet-kit" + ) 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..0be3536d1 --- /dev/null +++ b/components/esp32-ethernet-kit/example/main/esp32_ethernet_kit_example.cpp @@ -0,0 +1,97 @@ +#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; + +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( + [&](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)); + }, + DhcpMode::SERVER, srv_cfg); + //! [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( + [&](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)); + }, + DhcpMode::CLIENT); + //! [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"); + } + } +} + 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..b14f9b292 --- /dev/null +++ b/components/esp32-ethernet-kit/include/esp32-ethernet-kit.hpp @@ -0,0 +1,152 @@ +#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 when the Ethernet link comes up and an IP is assigned + using ethernet_link_callback_t = std::function; + + /// Callback invoked (SERVER mode only) each time the DHCP server assigns an + /// IP address to a connected client. + /// \param ip IPv4 address that was assigned. + /// \param mac MAC address of the client. + using client_ip_callback_t = + std::function mac)>; + + /// 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 / netmask / gateway; zero → 192.168.4.1/24 + client_ip_callback_t on_client_assigned; ///< Called each time a client is assigned an IP + }; + + /// @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 on_link_up Optional callback invoked once a usable IP is available. + /// CLIENT mode: called when DHCP assigns an IP. + /// SERVER mode: called immediately when the link comes up + /// (the static IP is already known). + /// \param mode DHCP operating mode (CLIENT or SERVER, default CLIENT). + /// \param server_config Static IP configuration for SERVER mode. + /// Ignored in CLIENT mode. + /// \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 ethernet_link_callback_t &on_link_up = nullptr, + DhcpMode mode = DhcpMode::CLIENT, + const ServerConfig &server_config = ServerConfig{}); + + /// Check whether the Ethernet link is up (cable connected + negotiated) + /// \return True if the link is up + 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}; ///< Per-client lease callback (server mode) + std::atomic ethernet_initialized_{false}; + std::atomic ethernet_connected_{false}; + esp_ip4_addr_t ethernet_ip_{}; + ethernet_link_callback_t ethernet_link_callback_{nullptr}; + esp_eth_handle_t eth_handle_{nullptr}; + void *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_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..0785fcab0 --- /dev/null +++ b/components/esp32-ethernet-kit/src/esp32-ethernet-kit.cpp @@ -0,0 +1,248 @@ +#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); + 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"); + // In server mode the IP is static — fire the callback immediately + if (self->dhcp_mode_ == DhcpMode::SERVER) { + self->ethernet_ip_ = self->server_ip_info_.ip; + self->ethernet_connected_ = true; + if (self->ethernet_link_callback_) { + self->ethernet_link_callback_(self->server_ip_info_.ip); + } + } + break; + } + case ETHERNET_EVENT_DISCONNECTED: + self->logger_.info("Ethernet link down"); + self->ethernet_connected_ = false; + self->ethernet_ip_ = {}; + 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); + 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->ethernet_link_callback_) { + self->ethernet_link_callback_(event->ip_info.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); + 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(const ethernet_link_callback_t &on_link_up, + DhcpMode mode, + const ServerConfig &server_config) { + if (ethernet_initialized_) { + logger_.warn("Ethernet already initialized"); + return true; + } + + logger_.info("Initializing Ethernet (EMAC + IP101GRI RMII, DHCP {})", + mode == DhcpMode::SERVER ? "server" : "client"); + ethernet_link_callback_ = on_link_up; + dhcp_mode_ = mode; + client_ip_callback_ = server_config.on_client_assigned; + +#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"); + esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); + if (!mac) { + logger_.error("Failed to create EMAC"); + return false; + } + + logger_.info("Creating IP101GRI PHY"); + esp_eth_phy_t *phy = esp_eth_phy_new_ip101(&phy_config); + if (!phy) { + logger_.error("Failed to create PHY"); + mac->del(mac); + return false; + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + esp_eth_config_t 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) { + logger_.error("esp_eth_driver_install failed: {}", esp_err_to_name(ret)); + mac->del(mac); + phy->del(phy); + return false; + } + + ret = esp_netif_init(); + if (ret != ESP_OK) { + logger_.error("esp_netif_init failed: {}", esp_err_to_name(ret)); + return false; + } + + ret = esp_event_loop_create_default(); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + logger_.error("esp_event_loop_create_default failed: {}", esp_err_to_name(ret)); + return false; + } + + // 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 = 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_) { + logger_.error("Failed to create Ethernet netif"); + return false; + } + + eth_glue_ = esp_eth_new_netif_glue(eth_handle_); + ret = esp_netif_attach(eth_netif_, static_cast(eth_glue_)); + if (ret != ESP_OK) { + logger_.error("esp_netif_attach failed: {}", esp_err_to_name(ret)); + return false; + } + + esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, ðernet_event_handler, this); + esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, ðernet_got_ip_handler, this); + if (mode == DhcpMode::SERVER) { + // IP_EVENT_AP_STAIPASSIGNED fires for every client the DHCP server serves + esp_event_handler_register(IP_EVENT, IP_EVENT_AP_STAIPASSIGNED, + ðernet_client_ip_handler, this); + } + + ret = esp_eth_start(eth_handle_); + if (ret != ESP_OK) { + logger_.error("esp_eth_start failed: {}", esp_err_to_name(ret)); + return false; + } + + 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..f0baad9b1 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -104,6 +104,7 @@ EXAMPLE_PATH = \ $(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/esp-box/example/main/esp_box_example.cpp \ $(PROJECT_PATH)/components/event_manager/example/main/event_manager_example.cpp \ @@ -255,6 +256,7 @@ INPUT = \ $(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/esp-box/include/esp-box.hpp \ $(PROJECT_PATH)/components/event_manager/include/event_manager.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