diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a49abd66..0c7b7e591 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -93,6 +93,8 @@ jobs: target: esp32p4 - path: 'components/esp-box/example' target: esp32s3 + - path: 'components/ethernet/example' + target: esp32 - path: 'components/event_manager/example' target: esp32 - path: 'components/expressive_eyes/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index 5aa3ec492..06aebb8f1 100755 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -64,6 +64,10 @@ jobs: components/drv2605 components/encoder components/esp-box + # ethernet is intentionally listed ahead of the esp32-p4-* board BSPs + # (esp32-p4-eth / esp32-p4-function-ev-board / esp32-p4-nano), which + # depend on espp/ethernet, so it is uploaded to the registry first. + components/ethernet components/esp32-p4-function-ev-board components/esp32-timer-cam components/event_manager diff --git a/components/esp32-ethernet-kit/CMakeLists.txt b/components/esp32-ethernet-kit/CMakeLists.txt index f3befb4dc..2ce3586b2 100644 --- a/components/esp32-ethernet-kit/CMakeLists.txt +++ b/components/esp32-ethernet-kit/CMakeLists.txt @@ -3,8 +3,7 @@ idf_component_register( SRC_DIRS "src" REQUIRES "base_component" - "esp_eth" + "ethernet" "esp_netif" - "esp_event" REQUIRED_IDF_TARGETS "esp32" ) diff --git a/components/esp32-ethernet-kit/idf_component.yml b/components/esp32-ethernet-kit/idf_component.yml index 8ee569f00..f3d8865fc 100644 --- a/components/esp32-ethernet-kit/idf_component.yml +++ b/components/esp32-ethernet-kit/idf_component.yml @@ -18,5 +18,6 @@ tags: dependencies: idf: ">=5.0" espp/base_component: ">=1.0" + espp/ethernet: ">=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 index 3f8a50f81..b5805d455 100644 --- a/components/esp32-ethernet-kit/include/esp32-ethernet-kit.hpp +++ b/components/esp32-ethernet-kit/include/esp32-ethernet-kit.hpp @@ -1,15 +1,15 @@ #pragma once #include -#include #include +#include #include -#include #include #include "base_component.hpp" +#include "ethernet.hpp" namespace espp { /// @brief Board Support Package (BSP) for the Espressif ESP32-Ethernet-Kit A V1.2. @@ -127,11 +127,11 @@ class Esp32EthernetKit : public BaseComponent { /// 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_; } + bool is_ethernet_connected() const { return ethernet_ && ethernet_->is_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_; } + esp_ip4_addr_t ethernet_ip() const { return ethernet_ ? ethernet_->ip() : esp_ip4_addr_t{}; } protected: Esp32EthernetKit(); @@ -159,28 +159,8 @@ class Esp32EthernetKit : public BaseComponent { // 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); + // The board's RMII Ethernet is driven by the reusable espp::Ethernet component + // (this BSP just supplies the board-specific pins / PHY). + std::unique_ptr ethernet_; }; // 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 index 6456a0aa3..b0ab56ecb 100644 --- a/components/esp32-ethernet-kit/src/esp32-ethernet-kit.cpp +++ b/components/esp32-ethernet-kit/src/esp32-ethernet-kit.cpp @@ -1,364 +1,45 @@ #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_) { + if (ethernet_ && ethernet_->is_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 + config.mode == DhcpMode::SERVER ? "server" : "client"); + + // The board-specific part: RMII pins + IP101GRI PHY on the fixed ESP32 IO_MUX + // data-plane pins, with the external 50 MHz ref-clock on GPIO0. + espp::Ethernet::Config eth_config{}; + eth_config.interface = espp::Ethernet::RmiiConfig{ + .mdc_gpio = eth_mdc_io, + .mdio_gpio = eth_mdio_io, + .phy_addr = eth_phy_addr, + .phy_reset_gpio = eth_phy_reset_gpio, + .clock_ext_in = true, + .clock_gpio = rmii_clk_gpio, }; -#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; + eth_config.mode = (config.mode == DhcpMode::SERVER) ? espp::Ethernet::DhcpMode::SERVER + : espp::Ethernet::DhcpMode::CLIENT; + if (config.mode == DhcpMode::SERVER) { + eth_config.ip_info = config.server_config.ip_info; + eth_config.on_client_assigned = config.server_config.on_client_assigned; + } + eth_config.on_link_up = config.on_link_up; + eth_config.on_link_down = config.on_link_down; + eth_config.on_got_ip = config.on_got_ip; + eth_config.on_lost_ip = config.on_lost_ip; + + ethernet_ = std::make_unique(eth_config); + return ethernet_->initialize(); } } // namespace espp diff --git a/components/esp32-p4-function-ev-board/CMakeLists.txt b/components/esp32-p4-function-ev-board/CMakeLists.txt index 5cada4f5b..938d0a267 100644 --- a/components/esp32-p4-function-ev-board/CMakeLists.txt +++ b/components/esp32-p4-function-ev-board/CMakeLists.txt @@ -15,9 +15,8 @@ idf_component_register( "esp_driver_gpio" "esp_lcd" "esp_driver_i2s" - "esp_eth" + "ethernet" "esp_netif" - "esp_event" "fatfs" "esp_driver_sdmmc" "sdmmc" diff --git a/components/esp32-p4-function-ev-board/idf_component.yml b/components/esp32-p4-function-ev-board/idf_component.yml index 6a298cbde..d942987dc 100644 --- a/components/esp32-p4-function-ev-board/idf_component.yml +++ b/components/esp32-p4-function-ev-board/idf_component.yml @@ -18,6 +18,7 @@ tags: dependencies: idf: ">=5.3" espp/base_component: ">=1.0" + espp/ethernet: ">=1.0" espp/codec: ">=1.0" espp/display: ">=1.0" espp/display_drivers: ">=1.0" diff --git a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp index f2aa9aabc..e1d67521a 100644 --- a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp +++ b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp @@ -20,8 +20,9 @@ #include #if CONFIG_ESP_P4_EV_BOARD_ETHERNET -#include #include + +#include "ethernet.hpp" #endif #include @@ -372,11 +373,11 @@ class Esp32P4FunctionEvBoard : public BaseComponent { /// 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_; } + bool is_ethernet_connected() const { return ethernet_ && ethernet_->is_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_; } + esp_ip4_addr_t ethernet_ip() const { return ethernet_ ? ethernet_->ip() : esp_ip4_addr_t{}; } #endif // CONFIG_ESP_P4_EV_BOARD_ETHERNET || defined(_DOXYGEN_) ///////////////////////////////////////////////////////////////////////////// @@ -603,18 +604,9 @@ class Esp32P4FunctionEvBoard : public BaseComponent { void *sd_pwr_ctrl_handle_{nullptr}; #if CONFIG_ESP_P4_EV_BOARD_ETHERNET - // Ethernet - 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); + // The board's RMII Ethernet is driven by the reusable espp::Ethernet component + // (this BSP just supplies the board-specific pins / PHY). + std::unique_ptr ethernet_; #endif // Display state diff --git a/components/esp32-p4-function-ev-board/src/esp32-p4-function-ev-board.cpp b/components/esp32-p4-function-ev-board/src/esp32-p4-function-ev-board.cpp index 7941a9292..67d9bc426 100644 --- a/components/esp32-p4-function-ev-board/src/esp32-p4-function-ev-board.cpp +++ b/components/esp32-p4-function-ev-board/src/esp32-p4-function-ev-board.cpp @@ -26,12 +26,14 @@ bool Esp32P4FunctionEvBoard::initialize_button(const button_callback_t &callback // it as a GPIO input would take down Ethernet TX, so refuse rather than break a // live network: Ethernet (system connectivity) outranks the UI button. Disable // Ethernet if you need the BOOT button. - if (ethernet_initialized_) { +#if CONFIG_ESP_P4_EV_BOARD_ETHERNET + if (ethernet_ && ethernet_->is_initialized()) { logger_.error("BOOT button shares GPIO{} with Ethernet RMII TXD1; refusing to " "initialize it while Ethernet is up (it would kill Ethernet TX).", static_cast(button_io)); return false; } +#endif logger_.info("Initializing BOOT button on GPIO{}", static_cast(button_io)); button_callback_ = callback; diff --git a/components/esp32-p4-function-ev-board/src/ethernet.cpp b/components/esp32-p4-function-ev-board/src/ethernet.cpp index b3a7bf2e0..861b1d668 100644 --- a/components/esp32-p4-function-ev-board/src/ethernet.cpp +++ b/components/esp32-p4-function-ev-board/src/ethernet.cpp @@ -2,193 +2,45 @@ #if CONFIG_ESP_P4_EV_BOARD_ETHERNET -#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 - namespace espp { -// IP101 PHY: reset GPIO and address. The RMII data/clock/MDIO pins for the -// ESP32-P4 Function EV Board are the ESP-IDF ESP32-P4 defaults -// (ETH_ESP32_EMAC_DEFAULT_CONFIG): MDC=31, MDIO=52, REF_CLK in=50, TX_EN=49, -// TXD0=34, TXD1=35, CRS_DV=28, RXD0=29, RXD1=30. -static constexpr int kPhyResetGpio = 51; -static constexpr int kPhyAddr = 1; - -void Esp32P4FunctionEvBoard::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: { - // Log the negotiated speed/duplex. A 10 Mbps or half-duplex result usually - // indicates an autonegotiation/duplex mismatch, which presents as "DHCP works - // but unicast (ping) fails". - 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"); - 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 Esp32P4FunctionEvBoard::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; - // Note: the espp logger uses fmt-style ({}) formatting, not printf-style, so - // format the IPv4 octets explicitly rather than using IPSTR/IP2STR. - 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); - } -} - bool Esp32P4FunctionEvBoard::initialize_ethernet(const ethernet_link_callback_t &on_link_up) { - if (ethernet_initialized_) { + if (ethernet_ && ethernet_->is_initialized()) { logger_.warn("Ethernet already initialized"); return true; } - // warn the user if they are initializing ethernet after initializing the boot - // button, since they share a pin. + // Warn the user if they are initializing ethernet after the BOOT button, since + // the BOOT button is wired to the PHY's RMII_TXD1 pin (GPIO35). if (button_callback_) { logger_.warn( "Initializing Ethernet while BOOT button is initialized. The BOOT button is connected to " "the PHY's RMII_TXD1 pin, so the boot button will not work while Ethernet is enabled!"); } - logger_.info("Initializing Ethernet (EMAC + IP101 EMAC)"); - ethernet_link_callback_ = on_link_up; - -#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(); - // NOTE: we can't use the ETH_ESP32_EMAC_DEFAULT_CONFIG macro because it's out - // of order which is a hard error in c++20 and above. - eth_esp32_emac_config_t esp32_emac_config = { - .smi_gpio = {.mdc_num = 31, .mdio_num = 52}, - .interface = EMAC_DATA_INTERFACE_RMII, - .clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = 50}}, - .dma_burst_len = ETH_DMA_BURST_LEN_32, - .intr_priority = 0, - .emac_dataif_gpio = {.rmii = {.tx_en_num = 49, - .txd0_num = 34, - .txd1_num = 35, - .crs_dv_num = 28, - .rxd0_num = 29, - .rxd1_num = 30}}, - .clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = -1}}, -// The below only exists in esp-idf >= v6.0 -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - .mdc_freq_hz = 0, -#endif + logger_.info("Initializing Ethernet (EMAC + IP101 RMII PHY)"); + + // Board-specific RMII configuration for the ESP32-P4 Function EV Board. Unlike + // the ESP32, the P4's EMAC data-plane pins are routable, so they are supplied + // explicitly (MDC=31, MDIO=52, REF_CLK in=50, TX_EN=49, TXD0=34, TXD1=35, + // CRS_DV=28, RXD0=29, RXD1=30; IP101 PHY at addr 1, reset on GPIO51). + espp::Ethernet::Config eth_config{}; + eth_config.interface = espp::Ethernet::RmiiConfig{ + .mdc_gpio = 31, + .mdio_gpio = 52, + .phy_addr = 1, + .phy_reset_gpio = 51, + .clock_ext_in = true, + .clock_gpio = 50, + .data_pins = + espp::Ethernet::RmiiConfig::DataPins{ + .tx_en = 49, .txd0 = 34, .txd1 = 35, .crs_dv = 28, .rxd0 = 29, .rxd1 = 30}, }; -#pragma GCC diagnostic pop - - // Update PHY config based on board specific configuration - phy_config.phy_addr = 1; - phy_config.reset_gpio_num = 51; - - // Update vendor specific MAC config based on board configuration - esp32_emac_config.smi_gpio.mdc_num = 31; - esp32_emac_config.smi_gpio.mdio_num = 52; - - 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 generic PHY (IP101)"); - esp_eth_phy_t *phy = esp_eth_phy_new_generic(&phy_config); - if (!phy) { - logger_.error("Failed to create generic PHY"); - 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)); - 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; - } - - 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); - - ret = esp_eth_start(eth_handle_); - if (ret != ESP_OK) { - logger_.error("esp_eth_start failed: {}", esp_err_to_name(ret)); - return false; - } + // The board's single link callback fires when an IP is acquired. + eth_config.on_got_ip = on_link_up; - ethernet_initialized_ = true; - logger_.info("Ethernet initialized; waiting for link/DHCP"); - return true; + ethernet_ = std::make_unique(eth_config); + return ethernet_->initialize(); } } // namespace espp diff --git a/components/ethernet/CMakeLists.txt b/components/ethernet/CMakeLists.txt new file mode 100644 index 000000000..7eebb36ef --- /dev/null +++ b/components/ethernet/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + INCLUDE_DIRS "include" + SRC_DIRS "src" + REQUIRES base_component esp_eth esp_netif esp_event esp_driver_spi esp_driver_gpio esp_hw_support) diff --git a/components/ethernet/Kconfig b/components/ethernet/Kconfig new file mode 100644 index 000000000..622ed5546 --- /dev/null +++ b/components/ethernet/Kconfig @@ -0,0 +1,27 @@ +menu "ESPP Ethernet" + + config ESPP_ETHERNET_W5500 + bool "Enable WIZnet W5500 SPI Ethernet support" + default n + help + Enable the WIZnet W5500 SPI Ethernet chip in espp::Ethernet + (SpiConfig with SpiChip::W5500). Pulls in the espressif/w5500 + managed component. + + 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. + +endmenu diff --git a/components/ethernet/README.md b/components/ethernet/README.md new file mode 100644 index 000000000..92e91a576 --- /dev/null +++ b/components/ethernet/README.md @@ -0,0 +1,62 @@ +# Ethernet Component + +[![Badge](https://components.espressif.com/components/espp/ethernet/badge.svg)](https://components.espressif.com/components/espp/ethernet) + +The `ethernet` component provides a single C++ class, `espp::Ethernet`, that +wraps the ESP-IDF `esp_eth` APIs and brings up an Ethernet interface over either +transport with a clean, uniform configuration: + +- **RMII** — the internal EMAC on SoCs that have one (`SOC_EMAC_SUPPORTED`: + ESP32, ESP32-P4) plus an external RMII PHY, using the generic 802.3 PHY driver + (supports IP101, LAN87xx, DP83848, RTL8201, KSZ8041, …). +- **SPI** — an external MAC+PHY chip (WIZnet W5500, and gated support for DM9051 + / ENC28J60) on a caller-owned SPI bus. Works on any SoC. +- **Pre-built driver** — an escape hatch that takes a caller-created + `esp_eth_mac_t*` / `esp_eth_phy_t*` for any other chip. + +It owns all of the boilerplate that BSPs otherwise duplicate — netif, the event +loop, netif glue, DHCP client/server, static IP, hostname, MAC assignment +(explicit or eFuse-derived), link/IP event dispatch to `std::function` +callbacks — and, unlike the inline BSP implementations it replaces, provides a +symmetric teardown (`deinitialize()` + destructor). + +## Interface selection + +The interface-specific configuration is a single tagged `Config::interface` +member (`std::variant`): + +- `Ethernet::RmiiConfig{ .mdc_gpio, .mdio_gpio, .phy_addr, .phy_reset_gpio, ... }` +- `Ethernet::SpiConfig{ .host, .cs_gpio, .int_gpio, .reset_gpio, .chip }` +- `Ethernet::DriverConfig{ .mac, .phy }` + +The rest of `Config` (DHCP `mode`, static `ip_info`, `hostname`, `mac_address`, +and the `on_link_up` / `on_link_down` / `on_got_ip` / `on_lost_ip` / +`on_client_assigned` callbacks) is shared across all interfaces. + +## SPI chip drivers + +The concrete SPI chip drivers are ESP-IDF managed components that are pulled in +only when the matching Kconfig option is enabled, so an RMII-only project does +not carry them: + +- `CONFIG_ESPP_ETHERNET_W5500` → `espressif/w5500` +- `CONFIG_ESPP_ETHERNET_DM9051` +- `CONFIG_ESPP_ETHERNET_ENC28J60` + +## Lifecycle + +```cpp +espp::Ethernet eth(config); // constructs (does not start) +eth.initialize(ec); // create + attach + start (idempotent) +// ... eth.is_connected(), eth.get_ip_address(), eth.link_speed_duplex() ... +eth.deinitialize(); // reverse-order teardown (also called by ~Ethernet) +``` + +Each fallible method has both a `bool f(std::error_code& ec)` form and a +logging convenience overload. + +## Example + +The [example](./example) brings up a DHCP-client interface: RMII by default (on +an EMAC-capable target such as the ESP32-Ethernet-Kit), or a W5500 over SPI when +`CONFIG_ESPP_ETHERNET_W5500` is enabled. diff --git a/components/ethernet/example/CMakeLists.txt b/components/ethernet/example/CMakeLists.txt new file mode 100644 index 000000000..d89ac1291 --- /dev/null +++ b/components/ethernet/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 ethernet" + CACHE STRING + "List of components to include" + ) + +project(ethernet_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/ethernet/example/README.md b/components/ethernet/example/README.md new file mode 100644 index 000000000..6f0453938 --- /dev/null +++ b/components/ethernet/example/README.md @@ -0,0 +1,18 @@ +# Ethernet Example + +This example demonstrates the `espp::Ethernet` component bringing up a +DHCP-client interface and logging link / IP state. + +By default it uses **RMII** (internal EMAC + an RMII PHY) on an EMAC-capable +target such as the ESP32-Ethernet-Kit. Enable `CONFIG_ESPP_ETHERNET_W5500` to +build the **SPI / W5500** variant instead (which also pulls in the +`espressif/w5500` managed component). + +Adjust the pins in `main/ethernet_example.cpp` for your board. + +## How to use + +``` +idf.py set-target esp32 +idf.py build flash monitor +``` diff --git a/components/ethernet/example/main/CMakeLists.txt b/components/ethernet/example/main/CMakeLists.txt new file mode 100644 index 000000000..7840fb139 --- /dev/null +++ b/components/ethernet/example/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + SRC_DIRS "." + INCLUDE_DIRS "." + ) diff --git a/components/ethernet/example/main/ethernet_example.cpp b/components/ethernet/example/main/ethernet_example.cpp new file mode 100644 index 000000000..731a66a8e --- /dev/null +++ b/components/ethernet/example/main/ethernet_example.cpp @@ -0,0 +1,90 @@ +#include +#include + +#include + +#include + +#if CONFIG_ESPP_ETHERNET_W5500 +#include +#include +#endif + +#include "ethernet.hpp" +#include "logger.hpp" + +using namespace std::chrono_literals; + +namespace { +espp::Logger logger({.tag = "Ethernet Example", .level = espp::Logger::Verbosity::INFO}); + +// Log the interface status forever (the callbacks below do the event-driven part). +[[noreturn]] void monitor(espp::Ethernet ð) { + while (true) { + logger.info("link={} connected={} ip={} mac={}", eth.link_up(), eth.is_connected(), + eth.get_ip_address(), eth.get_mac_address()); + std::this_thread::sleep_for(5s); + } +} + +espp::Ethernet::Config with_callbacks(espp::Ethernet::Config config) { + config.hostname = "espp-eth"; + config.log_level = espp::Logger::Verbosity::INFO; + config.on_link_up = []() { logger.info("Link up"); }; + config.on_link_down = []() { logger.warn("Link down"); }; + config.on_got_ip = [](esp_ip4_addr_t ip) { + char buf[16] = {0}; + esp_ip4addr_ntoa(&ip, buf, sizeof(buf)); + logger.info("Got IP: {}", buf); + }; + config.on_lost_ip = []() { logger.warn("Lost IP"); }; + return config; +} +} // namespace + +extern "C" void app_main(void) { +#if CONFIG_ESPP_ETHERNET_W5500 + //! [ethernet spi example] + // Bring up the SPI bus the W5500 lives on (adjust pins for your board). + spi_bus_config_t buscfg = {}; + buscfg.miso_io_num = 13; + buscfg.mosi_io_num = 11; + buscfg.sclk_io_num = 12; + buscfg.quadwp_io_num = -1; + buscfg.quadhd_io_num = -1; + buscfg.max_transfer_sz = 1600; + ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); + + // One reactor-owned W5500 over that bus, DHCP client. + espp::Ethernet eth(with_callbacks({ + .interface = espp::Ethernet::SpiConfig{.host = SPI2_HOST, + .cs_gpio = 10, + .int_gpio = 14, + .reset_gpio = 21, + .chip = espp::Ethernet::SpiChip::W5500}, + })); + eth.initialize(); + //! [ethernet spi example] + monitor(eth); +#elif SOC_EMAC_SUPPORTED + //! [ethernet rmii example] + // RMII on the ESP32-Ethernet-Kit (IP101 PHY on GPIO0 ref-clock). Adjust for your board. + espp::Ethernet eth(with_callbacks({ + .interface = espp::Ethernet::RmiiConfig{.mdc_gpio = 23, + .mdio_gpio = 18, + .phy_addr = 1, + .phy_reset_gpio = 5, + .clock_ext_in = true, + .clock_gpio = 0}, + })); + eth.initialize(); + //! [ethernet rmii example] + monitor(eth); +#else + logger.error("No ethernet interface available: this SoC has no EMAC and " + "CONFIG_ESPP_ETHERNET_W5500 is not enabled."); + while (true) { + std::this_thread::sleep_for(5s); + } +#endif +} diff --git a/components/ethernet/example/sdkconfig.defaults b/components/ethernet/example/sdkconfig.defaults new file mode 100644 index 000000000..a2fe6c3f0 --- /dev/null +++ b/components/ethernet/example/sdkconfig.defaults @@ -0,0 +1,7 @@ +CONFIG_IDF_TARGET="esp32" + +# Ethernet (internal EMAC + RMII) +CONFIG_ETH_ENABLED=y +CONFIG_ETH_USE_ESP32_EMAC=y + +CONFIG_FREERTOS_HZ=1000 diff --git a/components/ethernet/idf_component.yml b/components/ethernet/idf_component.yml new file mode 100644 index 000000000..4fd7a89d1 --- /dev/null +++ b/components/ethernet/idf_component.yml @@ -0,0 +1,39 @@ +## IDF Component Manager Manifest File +license: "MIT" +description: "Ethernet component in C++ wrapping esp_eth for RMII (internal EMAC) and SPI (W5500, etc.) interfaces" +url: "https://github.com/esp-cpp/espp/tree/main/components/ethernet" +repository: "git://github.com/esp-cpp/espp.git" +maintainers: + - William Emfinger +documentation: "https://esp-cpp.github.io/espp/network/ethernet.html" +examples: + - path: example +tags: + - cpp + - Component + - Ethernet + - Network + - RMII + - SPI + - W5500 +dependencies: + idf: + # 5.4 is the floor: esp_eth_phy_new_generic() was added in v5.4 and the split + # esp_driver_spi / esp_driver_gpio components (see CMakeLists REQUIRES) are only + # separately requireable on recent IDF. + version: ">=5.4" + espp/base_component: ">=1.0" + # The concrete SPI ethernet chip drivers are managed components pulled in only + # when the matching CONFIG_ESPP_ETHERNET_* Kconfig option is enabled. + espressif/w5500: + version: "^2.0.0" + rules: + - if: "$CONFIG{ESPP_ETHERNET_W5500} == True" + espressif/eth_dm9051: + version: "*" + rules: + - if: "$CONFIG{ESPP_ETHERNET_DM9051} == True" + espressif/eth_enc28j60: + version: "*" + rules: + - if: "$CONFIG{ESPP_ETHERNET_ENC28J60} == True" diff --git a/components/ethernet/include/ethernet.hpp b/components/ethernet/include/ethernet.hpp new file mode 100644 index 000000000..867dcb4a0 --- /dev/null +++ b/components/ethernet/include/ethernet.hpp @@ -0,0 +1,205 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "base_component.hpp" + +namespace espp { +/// @brief Cross-interface Ethernet wrapper around the ESP-IDF esp_eth APIs. +/// +/// @details One class drives both **RMII** (internal EMAC, on SoCs with +/// `SOC_EMAC_SUPPORTED` - esp32 / esp32-p4) and **SPI** (external +/// MAC+PHY chips such as the WIZnet W5500) interfaces, plus a +/// pre-built-driver escape hatch for any other chip. It owns all of +/// the common boilerplate - netif, event loop, glue, attach, DHCP +/// client/server, static IP, hostname, MAC assignment, link/IP event +/// dispatch and, unlike the inline BSP implementations it replaces, a +/// symmetric teardown (@ref deinitialize + destructor). +/// +/// The interface-specific part is a single tagged @ref Config member: +/// @ref RmiiConfig, @ref SpiConfig, or @ref DriverConfig. +/// +/// @note The concrete SPI chip drivers (W5500, DM9051, ENC28J60) are ESP-IDF +/// managed components pulled in only when the matching +/// `CONFIG_ESPP_ETHERNET_*` option is enabled. The RMII path uses the +/// generic 802.3 PHY driver in `esp_eth` core (no extra dependency), +/// which supports common PHYs (IP101, LAN87xx, DP83848, RTL8201, KSZ8041). +/// +/// \section ethernet_ex1 RMII Example +/// \snippet ethernet_example.cpp ethernet rmii example +/// \section ethernet_ex2 SPI (W5500) Example +/// \snippet ethernet_example.cpp ethernet spi example +class Ethernet : public BaseComponent { +public: + using MacAddress = std::array; + + /// DHCP operating mode. + enum class DhcpMode { + CLIENT, ///< Acquire an IP from an upstream DHCP server (or use a static IP if ip_info is set). + SERVER, ///< Run a DHCP server on this interface and assign IPs to connected hosts. + }; + + /// PHY model hint for the RMII path. All are handled by the generic 802.3 + /// driver in v1; the field is advisory (kept for a future specific-driver + /// option) and does not change behavior today. + enum class PhyModel { GENERIC, IP101, LAN87XX, DP83848, RTL8201, KSZ80XX }; + + /// Supported built-in SPI ethernet chips. + enum class SpiChip { W5500, DM9051, ENC28J60 }; + + /// Callback for link up/down and IP-lost. Runs in the esp event-loop task + /// context - keep it short and non-blocking. + using LinkCallback = std::function; + /// Callback for IP acquisition. Runs in the esp event-loop task context. + using IpCallback = std::function; + /// Callback (SERVER mode) invoked for each DHCP lease the server assigns. + using ClientIpCallback = std::function; + + /// RMII / internal-EMAC interface configuration (esp32 / esp32-p4 only). + struct RmiiConfig { + int mdc_gpio{-1}; ///< SMI management clock (MDC) GPIO (required). + int mdio_gpio{-1}; ///< SMI management data (MDIO) GPIO (required). + int phy_addr{-1}; ///< PHY SMI address; -1 auto-detects. + int phy_reset_gpio{-1}; ///< Active-low PHY reset GPIO; -1 = none. + PhyModel phy{PhyModel::GENERIC}; ///< PHY model hint (advisory; see PhyModel). + bool clock_ext_in{true}; ///< true: external RMII 50 MHz ref-clock in; false: internal out. + int clock_gpio{0}; ///< RMII REF_CLK GPIO (fixed set on esp32; routable on esp32-p4). + /// RMII data-plane pins. Only used on SoCs with routable EMAC pins + /// (esp32-p4); ignored on esp32 where they are fixed via IO_MUX. + struct DataPins { + int tx_en, txd0, txd1, crs_dv, rxd0, rxd1; + }; + std::optional data_pins{}; + }; + + /// SPI interface configuration (external MAC+PHY chip on a caller-owned, + /// already-initialized SPI bus). Works on any SoC. + struct SpiConfig { + spi_host_device_t host{ + SPI2_HOST}; ///< SPI host whose bus the caller already initialized (required). + int cs_gpio{-1}; ///< Chip-select GPIO (required). + int int_gpio{-1}; ///< Interrupt GPIO; -1 to poll instead. + int reset_gpio{-1}; ///< Chip reset GPIO; -1 = none. + int clock_speed_hz{20000000}; ///< SPI clock (W5500 max ~33 MHz; conservative default). + int phy_addr{1}; ///< PHY address (W5500 = 1). + SpiChip chip{SpiChip::W5500}; ///< Which built-in SPI ethernet chip. + }; + + /// Pre-built-driver escape hatch: the caller creates the MAC+PHY for any chip + /// and hands them over. The component takes ownership (they are freed by + /// esp_eth_driver_uninstall on teardown). + struct DriverConfig { + esp_eth_mac_t *mac{nullptr}; ///< Caller-created MAC (ownership transfers on initialize()). + esp_eth_phy_t *phy{nullptr}; ///< Caller-created PHY (ownership transfers on initialize()). + bool needs_isr_service{ + false}; ///< true if the driver uses a GPIO INT line (installs the ISR service). + bool needs_mac_assignment{ + false}; ///< true if the chip has no factory MAC (assign from config/eFuse). + }; + + /// Full configuration. + struct Config { + /// The interface: RMII (internal EMAC), SPI (external chip), or a pre-built driver. + std::variant interface; + DhcpMode mode{DhcpMode::CLIENT}; ///< DHCP client or server. + std::optional + mac_address{}; ///< Explicit MAC; else eFuse (ESP_MAC_ETH) for chips needing one. + std::string hostname{}; ///< netif hostname (empty = ESP-IDF default). + /// CLIENT: if ip.addr != 0, use this as a static IP (DHCP client is stopped); else DHCP. + /// SERVER: the interface/gateway IP for the DHCP server (0 -> 192.168.4.1/24). + esp_netif_ip_info_t ip_info{}; + LinkCallback on_link_up{}; ///< Physical link came up. + LinkCallback on_link_down{}; ///< Physical link went down. + IpCallback on_got_ip{}; ///< Interface obtained an IPv4 address. + LinkCallback on_lost_ip{}; ///< Interface lost its IPv4 address. + ClientIpCallback on_client_assigned{}; ///< SERVER mode: a client was assigned an IP. + espp::Logger::Verbosity log_level{espp::Logger::Verbosity::WARN}; + }; + + /// @brief Construct (does not start the interface; call initialize()). + /// @param config The configuration. + explicit Ethernet(const Config &config); + + /// @brief Destroy - deinitializes the interface if still running. + ~Ethernet(); + + Ethernet(const Ethernet &) = delete; + Ethernet &operator=(const Ethernet &) = delete; + + /// @brief Bring up the interface (create driver, attach netif, register + /// events, apply IP/hostname/MAC, start). Idempotent. + /// @param ec Set to a specific error on failure; cleared on success. + /// @return true on success (interface started). + bool initialize(std::error_code &ec); + + /// @brief Convenience overload that logs on failure. + /// @return true on success. + bool initialize(); + + /// @brief Stop and fully tear down the interface (reverse-order unwind). + /// Idempotent. Does not undo the process-global netif/event-loop init. + /// @param ec Set on failure; cleared on success. + /// @return true on success. + bool deinitialize(std::error_code &ec); + + /// @brief Convenience overload that logs on failure. + void deinitialize(); + + /// @return true if the interface has been initialized/started. + bool is_initialized() const { return initialized_.load(); } + /// @return true if the link is up AND an IP address is held. + bool is_connected() const { return connected_.load(); } + /// @return true if the physical link is up. + bool link_up() const { return link_up_.load(); } + + /// @return The current IPv4 address (0 if none). + esp_ip4_addr_t ip() const; + /// @return The current IPv4 address as a dotted quad ("0.0.0.0" if none). + std::string get_ip_address() const; + /// @return The interface MAC as "aa:bb:cc:dd:ee:ff" (empty if not initialized). + std::string get_mac_address() const; + /// @return {speed_mbps, full_duplex} if the link is up, else nullopt. + std::optional> link_speed_duplex() const; + + /// @return The raw esp_eth driver handle (nullptr if not initialized). + esp_eth_handle_t native_handle() const { return eth_handle_; } + /// @return The raw esp_netif handle (nullptr if not initialized). + esp_netif_t *netif() const { return eth_netif_; } + +protected: + /// Create the interface-specific MAC + PHY per config_.interface. + bool create_mac_phy(esp_eth_mac_t **mac, esp_eth_phy_t **phy, bool &needs_isr, + bool &needs_mac_assign, std::error_code &ec); + + static void eth_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data); + static void got_ip_handler(void *arg, esp_event_base_t base, int32_t id, void *data); + static void lost_ip_handler(void *arg, esp_event_base_t base, int32_t id, void *data); + static void client_ip_handler(void *arg, esp_event_base_t base, int32_t id, void *data); + + Config config_; + std::atomic initialized_{false}; + std::atomic link_up_{false}; + std::atomic connected_{false}; + std::atomic ip_addr_{0}; ///< IPv4 in network byte order. + + esp_eth_handle_t eth_handle_{nullptr}; + esp_eth_netif_glue_handle_t eth_glue_{nullptr}; + esp_netif_t *eth_netif_{nullptr}; + bool handlers_registered_{false}; + bool client_ip_handler_registered_{false}; + esp_netif_ip_info_t server_ip_info_{}; ///< Resolved static/server IP (SERVER mode). +}; +} // namespace espp diff --git a/components/ethernet/src/ethernet.cpp b/components/ethernet/src/ethernet.cpp new file mode 100644 index 000000000..4092272be --- /dev/null +++ b/components/ethernet/src/ethernet.cpp @@ -0,0 +1,640 @@ +#include "ethernet.hpp" + +#include + +#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 + +#include +#include +#include +#include + +#include + +#if SOC_EMAC_SUPPORTED +#include +#include +#endif + +#if CONFIG_ESPP_ETHERNET_W5500 +#include +#include +#endif +#if CONFIG_ESPP_ETHERNET_DM9051 +#include +#include +#endif +#if CONFIG_ESPP_ETHERNET_ENC28J60 +#include +#endif + +namespace espp { + +Ethernet::Ethernet(const Config &config) + : BaseComponent("Ethernet", config.log_level) + , config_(config) {} + +Ethernet::~Ethernet() { deinitialize(); } + +////////////////////////////////////////////////////////////////////////////// +// Event handlers +////////////////////////////////////////////////////////////////////////////// + +void Ethernet::eth_event_handler(void *arg, esp_event_base_t /*base*/, int32_t id, void *data) { + auto *self = static_cast(arg); + if (!self) { + return; + } + // ETH_EVENT is registered with ESP_EVENT_ANY_ID, so when multiple espp::Ethernet + // instances are active each handler sees every driver's events. The payload is a + // pointer to the originating esp_eth_handle_t; ignore events from other drivers so + // we don't corrupt our own link/connection state. + if (!data || *static_cast(data) != self->eth_handle_) { + return; + } + switch (id) { + case ETHERNET_EVENT_CONNECTED: { + self->link_up_ = true; + auto sd = self->link_speed_duplex(); + if (sd) { + self->logger_.info("Link up: {} Mbps, {} duplex", sd->first, sd->second ? "full" : "half"); + } else { + self->logger_.info("Link up"); + } + if (self->config_.on_link_up) { + self->config_.on_link_up(); + } + // In server mode the IP is static, so surface got-IP immediately. + if (self->config_.mode == DhcpMode::SERVER) { + self->ip_addr_ = self->server_ip_info_.ip.addr; + self->connected_ = true; + if (self->config_.on_got_ip) { + self->config_.on_got_ip(self->server_ip_info_.ip); + } + } + break; + } + case ETHERNET_EVENT_DISCONNECTED: + self->logger_.info("Link down"); + self->link_up_ = false; + if (self->connected_.exchange(false)) { + if (self->config_.on_lost_ip) { + self->config_.on_lost_ip(); + } + } + self->ip_addr_ = 0; + if (self->config_.on_link_down) { + self->config_.on_link_down(); + } + break; + case ETHERNET_EVENT_START: + self->logger_.debug("Started"); + break; + case ETHERNET_EVENT_STOP: + self->logger_.debug("Stopped"); + self->link_up_ = false; + self->connected_ = false; + self->ip_addr_ = 0; + break; + default: + break; + } +} + +void Ethernet::got_ip_handler(void *arg, esp_event_base_t /*base*/, int32_t /*id*/, void *data) { + auto *self = static_cast(arg); + auto *event = static_cast(data); + if (!self || !event) { + return; + } + // IP_EVENT is global; only handle got-IP for our own netif. + if (event->esp_netif != self->eth_netif_) { + 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); + } +} + +void Ethernet::lost_ip_handler(void *arg, esp_event_base_t /*base*/, int32_t /*id*/, void *data) { + auto *self = static_cast(arg); + // The lost-IP event carries an ip_event_got_ip_t whose esp_netif identifies the + // interface, so we can filter to our own netif just like got_ip_handler. + auto *event = static_cast(data); + if (!self || !event) { + return; + } + if (event->esp_netif != self->eth_netif_) { + return; + } + self->logger_.info("Lost IP"); + self->connected_ = false; + self->ip_addr_ = 0; + if (self->config_.on_lost_ip) { + self->config_.on_lost_ip(); + } +} + +void Ethernet::client_ip_handler(void *arg, esp_event_base_t /*base*/, int32_t /*id*/, void *data) { + auto *self = static_cast(arg); + auto *event = static_cast(data); + if (!self || !event) { + return; + } + // Only handle leases from our own DHCP-server netif. + if (event->esp_netif != self->eth_netif_) { + return; + } + MacAddress mac; + std::copy(std::begin(event->mac), std::end(event->mac), mac.begin()); + self->logger_.info("DHCP server assigned {}.{}.{}.{}", esp_ip4_addr1_16(&event->ip), + esp_ip4_addr2_16(&event->ip), esp_ip4_addr3_16(&event->ip), + esp_ip4_addr4_16(&event->ip)); + if (self->config_.on_client_assigned) { + self->config_.on_client_assigned(event->ip, mac); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Interface-specific MAC + PHY creation +////////////////////////////////////////////////////////////////////////////// + +bool Ethernet::create_mac_phy(esp_eth_mac_t **mac, esp_eth_phy_t **phy, bool &needs_isr, + bool &needs_mac_assign, std::error_code &ec) { + needs_isr = false; + needs_mac_assign = false; + + // ---- Pre-built driver escape hatch ---- + if (auto *drv = std::get_if(&config_.interface)) { + if (!drv->mac || !drv->phy) { + logger_.error("DriverConfig requires non-null mac and phy"); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + *mac = drv->mac; + *phy = drv->phy; + needs_isr = drv->needs_isr_service; + needs_mac_assign = drv->needs_mac_assignment; + return true; + } + + // ---- RMII (internal EMAC) ---- + if ([[maybe_unused]] auto *rmii = std::get_if(&config_.interface)) { +#if SOC_EMAC_SUPPORTED + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + // Zero-init then assign so we avoid both -Wmissing-field-initializers and the + // out-of-order designated-init hard error under C++20. + eth_esp32_emac_config_t emac_cfg = {}; + emac_cfg.smi_gpio.mdc_num = rmii->mdc_gpio; + emac_cfg.smi_gpio.mdio_num = rmii->mdio_gpio; + emac_cfg.interface = EMAC_DATA_INTERFACE_RMII; + emac_cfg.clock_config.rmii.clock_mode = rmii->clock_ext_in ? EMAC_CLK_EXT_IN : EMAC_CLK_OUT; + // clock_gpio is a plain int in IDF v6.0 but an `emac_rmii_clock_gpio_t` enum on + // ESP32 in v5.x, so cast to the field's actual type to build across both. + emac_cfg.clock_config.rmii.clock_gpio = + static_cast(rmii->clock_gpio); + emac_cfg.dma_burst_len = ETH_DMA_BURST_LEN_32; + emac_cfg.intr_priority = 0; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + // mdc_freq_hz was added to eth_esp32_emac_config_t in ESP-IDF v6.0. + emac_cfg.mdc_freq_hz = 0; +#endif +#if SOC_EMAC_USE_MULTI_IO_MUX || SOC_EMAC_MII_USE_GPIO_MATRIX + if (rmii->data_pins) { + emac_cfg.emac_dataif_gpio.rmii.tx_en_num = rmii->data_pins->tx_en; + emac_cfg.emac_dataif_gpio.rmii.txd0_num = rmii->data_pins->txd0; + emac_cfg.emac_dataif_gpio.rmii.txd1_num = rmii->data_pins->txd1; + emac_cfg.emac_dataif_gpio.rmii.crs_dv_num = rmii->data_pins->crs_dv; + emac_cfg.emac_dataif_gpio.rmii.rxd0_num = rmii->data_pins->rxd0; + emac_cfg.emac_dataif_gpio.rmii.rxd1_num = rmii->data_pins->rxd1; + } +#endif +#if !SOC_EMAC_RMII_CLK_OUT_INTERNAL_LOOPBACK + emac_cfg.clock_config_out_in.rmii.clock_mode = EMAC_CLK_EXT_IN; + emac_cfg.clock_config_out_in.rmii.clock_gpio = + static_cast(-1); +#endif + + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + phy_config.phy_addr = rmii->phy_addr; + phy_config.reset_gpio_num = rmii->phy_reset_gpio; + + *mac = esp_eth_mac_new_esp32(&emac_cfg, &mac_config); + if (!*mac) { + logger_.error("Failed to create ESP32 EMAC"); + ec = std::make_error_code(std::errc::io_error); + return false; + } + // v1 uses the generic 802.3 PHY driver (drives IP101/LAN87xx/DP83848/RTL8201/KSZ8041). + *phy = esp_eth_phy_new_generic(&phy_config); + if (!*phy) { + logger_.error("Failed to create generic PHY"); + (*mac)->del(*mac); + *mac = nullptr; + ec = std::make_error_code(std::errc::io_error); + return false; + } + needs_mac_assign = config_.mac_address.has_value(); // EMAC has a factory MAC otherwise + return true; +#else + logger_.error("RMII requested but this SoC has no internal EMAC (SOC_EMAC_SUPPORTED=0)"); + ec = std::make_error_code(std::errc::function_not_supported); + return false; +#endif + } + + // ---- SPI (external MAC+PHY chip) ---- + auto *spi = std::get_if(&config_.interface); + if (!spi) { + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + spi_device_interface_config_t devcfg = {}; + devcfg.mode = 0; + devcfg.clock_speed_hz = spi->clock_speed_hz; + devcfg.queue_size = 20; + devcfg.spics_io_num = spi->cs_gpio; + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + phy_config.phy_addr = spi->phy_addr; + phy_config.reset_gpio_num = spi->reset_gpio; + needs_isr = (spi->int_gpio >= 0); + needs_mac_assign = true; // SPI chips generally have no factory MAC + + switch (spi->chip) { + case SpiChip::W5500: +#if CONFIG_ESPP_ETHERNET_W5500 + { + eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi->host, &devcfg); + w5500_config.base.int_gpio_num = spi->int_gpio; + *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); + if (*mac) { + *phy = esp_eth_phy_new_w5500(&phy_config); + } + break; + } +#else + logger_.error("W5500 selected but CONFIG_ESPP_ETHERNET_W5500 is not enabled"); + ec = std::make_error_code(std::errc::function_not_supported); + return false; +#endif + case SpiChip::DM9051: +#if CONFIG_ESPP_ETHERNET_DM9051 + { + eth_dm9051_config_t dm_config = ETH_DM9051_DEFAULT_CONFIG(spi->host, &devcfg); + dm_config.int_gpio_num = spi->int_gpio; + *mac = esp_eth_mac_new_dm9051(&dm_config, &mac_config); + if (*mac) { + *phy = esp_eth_phy_new_dm9051(&phy_config); + } + break; + } +#else + logger_.error("DM9051 selected but CONFIG_ESPP_ETHERNET_DM9051 is not enabled"); + ec = std::make_error_code(std::errc::function_not_supported); + return false; +#endif + case SpiChip::ENC28J60: +#if CONFIG_ESPP_ETHERNET_ENC28J60 + { + eth_enc28j60_config_t enc_config = ETH_ENC28J60_DEFAULT_CONFIG(spi->host, &devcfg); + enc_config.int_gpio_num = spi->int_gpio; + *mac = esp_eth_mac_new_enc28j60(&enc_config, &mac_config); + if (*mac) { + *phy = esp_eth_phy_new_enc28j60(&phy_config); + } + break; + } +#else + logger_.error("ENC28J60 selected but CONFIG_ESPP_ETHERNET_ENC28J60 is not enabled"); + ec = std::make_error_code(std::errc::function_not_supported); + return false; +#endif + } + + if (!*mac || !*phy) { + logger_.error("Failed to create SPI MAC/PHY"); + if (*phy) { + (*phy)->del(*phy); + *phy = nullptr; + } + if (*mac) { + (*mac)->del(*mac); + *mac = nullptr; + } + ec = std::make_error_code(std::errc::io_error); + return false; + } + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// initialize / deinitialize +////////////////////////////////////////////////////////////////////////////// + +bool Ethernet::initialize(std::error_code &ec) { + ec.clear(); + if (initialized_.load()) { + return true; + } + + const bool server_mode = (config_.mode == DhcpMode::SERVER); + const bool is_spi = std::holds_alternative(config_.interface); + + esp_eth_mac_t *mac = nullptr; + esp_eth_phy_t *phy = nullptr; + bool needs_isr = false; + bool needs_mac_assign = false; + bool driver_installed = false; + + auto fail = [&](const char *msg, esp_err_t err, std::errc code) { + logger_.error("{}: {}", msg, esp_err_to_name(err)); + 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 (driver_installed && eth_handle_) { + esp_eth_driver_uninstall(eth_handle_); // also frees the mac + phy it owns + eth_handle_ = nullptr; + } else { + // driver not installed yet: free any mac/phy we created ourselves + if (phy) { + phy->del(phy); + } + if (mac) { + mac->del(mac); + } + } + if (eth_netif_) { + esp_netif_destroy(eth_netif_); + eth_netif_ = nullptr; + } + link_up_ = false; + connected_ = false; + ip_addr_ = 0; + ec = std::make_error_code(code); + return false; + }; + + // TCP/IP stack + default event loop (both process-global singletons). + esp_err_t err = esp_netif_init(); + if (err != ESP_OK) { + return fail("esp_netif_init failed", err, std::errc::io_error); + } + err = esp_event_loop_create_default(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + return fail("esp_event_loop_create_default failed", err, std::errc::io_error); + } + + // Create the MAC + PHY for the configured interface. + if (!create_mac_phy(&mac, &phy, needs_isr, needs_mac_assign, ec)) { + return false; // create_mac_phy already logged + freed + } + + // SPI chips with an INT line need the GPIO ISR service (the driver adds a + // handler but not the service). ESP_ERR_INVALID_STATE = already installed. + if (needs_isr) { + esp_err_t isr_err = gpio_install_isr_service(0); + if (isr_err != ESP_OK && isr_err != ESP_ERR_INVALID_STATE) { + return fail("gpio_install_isr_service failed", isr_err, std::errc::io_error); + } + } + + // Install the driver (takes ownership of mac + phy). + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy); + err = esp_eth_driver_install(ð_config, ð_handle_); + if (err != ESP_OK) { + return fail("esp_eth_driver_install failed", err, std::errc::io_error); + } + driver_installed = true; + + // Assign a MAC address if the chip needs one or the user provided one. + if (needs_mac_assign) { + MacAddress mac_addr{}; + if (config_.mac_address) { + mac_addr = *config_.mac_address; + } else { + err = esp_read_mac(mac_addr.data(), ESP_MAC_ETH); + if (err != ESP_OK) { + return fail("esp_read_mac failed", err, std::errc::io_error); + } + } + err = esp_eth_ioctl(eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr.data()); + if (err != ESP_OK) { + return fail("Failed to set MAC address", err, std::errc::io_error); + } + } + + // Create the netif (server mode gets a DHCP-server-flagged netif). + if (server_mode) { + esp_netif_ip_info_t ip_info = 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_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("esp_netif_new failed", ESP_FAIL, std::errc::not_enough_memory); + } + + eth_glue_ = esp_eth_new_netif_glue(eth_handle_); + if (!eth_glue_) { + return fail("esp_eth_new_netif_glue failed", ESP_FAIL, std::errc::not_enough_memory); + } + err = esp_netif_attach(eth_netif_, eth_glue_); + if (err != ESP_OK) { + return fail("esp_netif_attach failed", err, std::errc::io_error); + } + + // Event handlers. Set handlers_registered_ as soon as the first one is + // registered so fail() unregisters whatever got registered (unregistering a + // not-registered handler is a harmless no-op), and validate every return. + err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, ð_event_handler, this); + if (err != ESP_OK) { + return fail("register ETH_EVENT failed", err, std::errc::io_error); + } + handlers_registered_ = true; + err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &got_ip_handler, this); + if (err != ESP_OK) { + return fail("register IP_EVENT_ETH_GOT_IP failed", err, std::errc::io_error); + } + err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_LOST_IP, &lost_ip_handler, this); + if (err != ESP_OK) { + return fail("register IP_EVENT_ETH_LOST_IP failed", err, std::errc::io_error); + } + if (server_mode) { + err = esp_event_handler_register(IP_EVENT, IP_EVENT_AP_STAIPASSIGNED, &client_ip_handler, this); + if (err != ESP_OK) { + return fail("register client-IP handler failed", err, std::errc::io_error); + } + client_ip_handler_registered_ = true; + } + + // Hostname (non-fatal). + if (!config_.hostname.empty()) { + err = esp_netif_set_hostname(eth_netif_, config_.hostname.c_str()); + if (err != ESP_OK) { + logger_.warn("Failed to set hostname '{}': {}", config_.hostname, esp_err_to_name(err)); + } + } + + // 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); + } + } + + err = esp_eth_start(eth_handle_); + if (err != ESP_OK) { + return fail("esp_eth_start failed", err, std::errc::io_error); + } + + initialized_ = true; + logger_.info("Ethernet initialized ({}, DHCP {})", is_spi ? "SPI" : "RMII", + server_mode ? "server" : "client"); + return true; +} + +bool Ethernet::initialize() { + std::error_code ec; + if (!initialize(ec)) { + logger_.error("initialize failed: {}", ec.message()); + return false; + } + return true; +} + +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; +} + +void Ethernet::deinitialize() { + std::error_code ec; + if (!deinitialize(ec)) { + logger_.error("deinitialize failed: {}", ec.message()); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Getters +////////////////////////////////////////////////////////////////////////////// + +esp_ip4_addr_t Ethernet::ip() const { + esp_ip4_addr_t addr{}; + addr.addr = ip_addr_.load(); + return addr; +} + +std::string Ethernet::get_ip_address() const { + esp_ip4_addr_t addr{}; + addr.addr = ip_addr_.load(); + char buf[16] = {0}; + esp_ip4addr_ntoa(&addr, buf, sizeof(buf)); + return std::string(buf); +} + +std::string Ethernet::get_mac_address() const { + if (!eth_handle_) { + return ""; + } + MacAddress mac{}; + if (esp_eth_ioctl(eth_handle_, ETH_CMD_G_MAC_ADDR, mac.data()) != ESP_OK) { + return ""; + } + return fmt::format("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5]); +} + +std::optional> Ethernet::link_speed_duplex() const { + if (!eth_handle_ || !link_up_.load()) { + return std::nullopt; + } + eth_speed_t speed = ETH_SPEED_10M; + eth_duplex_t duplex = ETH_DUPLEX_HALF; + esp_eth_ioctl(eth_handle_, ETH_CMD_G_SPEED, &speed); + esp_eth_ioctl(eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex); + return std::make_pair(speed == ETH_SPEED_100M ? 100 : 10, duplex == ETH_DUPLEX_FULL); +} + +} // namespace espp diff --git a/doc/Doxyfile b/doc/Doxyfile index e43755a04..4d5e2357c 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -108,6 +108,7 @@ EXAMPLE_PATH = \ $(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/ethernet/example/main/ethernet_example.cpp \ $(PROJECT_PATH)/components/event_manager/example/main/event_manager_example.cpp \ $(PROJECT_PATH)/components/expressive_eyes/example/main/expressive_eyes_example.cpp \ $(PROJECT_PATH)/components/file_system/example/main/file_system_example.cpp \ @@ -266,6 +267,7 @@ INPUT = \ $(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/ethernet/include/ethernet.hpp \ $(PROJECT_PATH)/components/event_manager/include/event_manager.hpp \ $(PROJECT_PATH)/components/expressive_eyes/include/expressive_eyes.hpp \ $(PROJECT_PATH)/components/file_system/include/file_system.hpp \ diff --git a/doc/en/network/ethernet.rst b/doc/en/network/ethernet.rst new file mode 100644 index 000000000..d2cce13c1 --- /dev/null +++ b/doc/en/network/ethernet.rst @@ -0,0 +1,37 @@ +Ethernet +******** + +The :cpp:class:`espp::Ethernet` component wraps the ESP-IDF ``esp_eth`` APIs and +brings up an Ethernet interface over either **RMII** (internal EMAC, on SoCs with +``SOC_EMAC_SUPPORTED``) or **SPI** (an external MAC+PHY chip such as the WIZnet +W5500), plus a pre-built-driver escape hatch for any other chip. + +One class owns the boilerplate that BSPs otherwise duplicate - netif, event loop, +netif glue, DHCP client/server, static IP, hostname, MAC assignment (explicit or +eFuse-derived) and link/IP event dispatch to ``std::function`` callbacks - and, +unlike the inline BSP implementations it replaces, provides a symmetric teardown +(``deinitialize()`` + destructor). + +The interface-specific configuration is a single tagged ``Config::interface`` +member (a ``std::variant`` of ``RmiiConfig`` / ``SpiConfig`` / ``DriverConfig``); +the rest of ``Config`` (DHCP mode, static IP, hostname, MAC, callbacks) is shared. + +.. note:: + + The RMII path uses the generic 802.3 PHY driver in ``esp_eth`` core (no extra + dependency). The concrete SPI chip drivers (W5500, DM9051, ENC28J60) are + managed components pulled in only when the matching ``CONFIG_ESPP_ETHERNET_*`` + Kconfig option is enabled. + +.. ------------------------------- Example ------------------------------------- + +.. toctree:: + + ethernet_example + +.. ---------------------------- API Reference ---------------------------------- + +API Reference +------------- + +.. include-build-file:: inc/ethernet.inc diff --git a/doc/en/network/ethernet_example.md b/doc/en/network/ethernet_example.md new file mode 100644 index 000000000..627a66e34 --- /dev/null +++ b/doc/en/network/ethernet_example.md @@ -0,0 +1,2 @@ +```{include} ../../../components/ethernet/example/README.md +``` diff --git a/doc/en/network/index.rst b/doc/en/network/index.rst index 9a0ce2d16..428879b80 100644 --- a/doc/en/network/index.rst +++ b/doc/en/network/index.rst @@ -5,6 +5,8 @@ Network APIs :maxdepth: 1 dns_server + ethernet + ethernet_example ping provisioning socket_example