Files
esphome/esphome/components/ethernet/ethernet_component_rp2.cpp
T

384 lines
12 KiB
C++

#include "ethernet_component.h"
#if defined(USE_ETHERNET) && defined(USE_RP2)
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/components/rp2/gpio.h"
#include <SPI.h>
#include <lwip/dns.h>
#include <lwip/netif.h>
namespace esphome::ethernet {
static const char *const TAG = "ethernet";
void EthernetComponent::setup() {
// Configure SPI pins
#if !defined(USE_ETHERNET_W6300)
SPI.setRX(this->miso_pin_);
SPI.setTX(this->mosi_pin_);
SPI.setSCK(this->clk_pin_);
#endif
// W6300 uses PIO QSPI with hardcoded pins, not Arduino SPI.
// SPI pin config is skipped; Wiznet6300lwIPFixed (needsSPI()=false)
// prevents LwipIntfDev::begin() from calling SPI.begin().
// Toggle reset pin if configured
if (this->reset_pin_ >= 0) {
rp2::RP2GPIOPin reset_pin;
reset_pin.set_pin(this->reset_pin_);
reset_pin.set_flags(gpio::FLAG_OUTPUT);
reset_pin.setup();
reset_pin.digital_write(false);
delay(1); // NOLINT
reset_pin.digital_write(true);
// W5100S needs 150ms for PLL lock; W5500/ENC28J60 need ~10ms
delay(RESET_DELAY_MS); // NOLINT
}
// Create the SPI Ethernet device instance
#if defined(USE_ETHERNET_W5500)
this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_W5100)
this->eth_ = new Wiznet5100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_W6100)
this->eth_ = new Wiznet6100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_W6300)
this->eth_ = new Wiznet6300lwIPFixed(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_ENC28J60)
this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#endif
// Set hostname before begin() so the LWIP netif gets it
this->eth_->hostname(App.get_name().c_str());
// Configure static IP if set (must be done before begin())
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
IPAddress ip(this->manual_ip_->static_ip);
IPAddress gateway(this->manual_ip_->gateway);
IPAddress subnet(this->manual_ip_->subnet);
IPAddress dns1(this->manual_ip_->dns1);
IPAddress dns2(this->manual_ip_->dns2);
this->eth_->config(ip, gateway, subnet, dns1, dns2);
}
#endif
// Begin with fixed MAC or auto-generated
bool success;
if (this->fixed_mac_.has_value()) {
success = this->eth_->begin(this->fixed_mac_->data());
} else {
success = this->eth_->begin();
}
if (!success) {
ESP_LOGE(TAG, "Failed to initialize Ethernet");
delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory)
this->eth_ = nullptr;
this->mark_failed();
return;
}
// Make this the default interface for routing
this->eth_->setDefault(true);
// The arduino-pico LwipIntfDev automatically handles packet processing
// via __addEthernetPacketHandler when no interrupt pin is used,
// or via GPIO interrupt when one is provided.
// Don't set started_ here — let the link polling in loop() set it
// when the link is actually up. Setting it prematurely causes
// a "Starting → Stopped → Starting" log sequence because the chip
// needs time after begin() before the PHY link is ready.
}
void EthernetComponent::loop() {
// On RP2040, we need to poll connection state since there are no events.
const uint32_t now = App.get_loop_component_start_time();
// Throttle link/IP polling to avoid excessive SPI transactions.
// W5500/ENC28J60 read PHY register via SPI on every linkStatus() call.
// W5100 can't detect link state, so we skip the SPI read and assume link-up.
// connected() reads netif->ip_addr without LwIPLock, but this is a single
// 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale
// value, which is benign for polling.
if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) {
this->last_link_check_ = now;
#if defined(USE_ETHERNET_W5100)
// W5100 can't detect link (isLinkDetectable() returns false), so linkStatus()
// returns Unknown — assume link is up after successful begin()
bool link_up = true;
#else
bool link_up = this->eth_->linkStatus() == LinkON;
#endif
bool has_ip = this->eth_->connected();
if (!link_up) {
if (this->started_) {
this->started_ = false;
this->connected_ = false;
}
} else {
if (!this->started_) {
this->started_ = true;
}
bool was_connected = this->connected_;
this->connected_ = has_ip;
if (this->connected_ && !was_connected) {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
this->notify_ip_state_listeners_();
#endif
}
}
}
// State machine
switch (this->state_) {
case EthernetComponentState::STOPPED:
if (this->started_) {
ESP_LOGI(TAG, "Starting connection");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTING:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
} else if (this->connected_) {
// connection established
ESP_LOGI(TAG, "Connected");
this->state_ = EthernetComponentState::CONNECTED;
this->dump_connect_params_();
this->status_clear_warning();
#ifdef USE_ETHERNET_CONNECT_TRIGGER
this->connect_trigger_.trigger();
#endif
} else if (now - this->connect_begin_ > 15000) {
ESP_LOGW(TAG, "Connecting failed; reconnecting");
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTED:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else if (!this->connected_) {
ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else {
this->finish_connect_();
}
break;
}
}
void EthernetComponent::dump_config() {
const char *type_str = "Unknown";
#if defined(USE_ETHERNET_W5500)
type_str = "W5500";
#elif defined(USE_ETHERNET_W5100)
type_str = "W5100";
#elif defined(USE_ETHERNET_W6100)
type_str = "W6100";
#elif defined(USE_ETHERNET_W6300)
type_str = "W6300";
#elif defined(USE_ETHERNET_ENC28J60)
type_str = "ENC28J60";
#endif
#if defined(USE_ETHERNET_W6300)
// W6300 uses PIO QSPI with hardcoded pins — SPI pin fields are not used
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Type: %s (PIO QSPI)\n"
" Connected: %s\n"
" IRQ Pin: %d\n"
" Reset Pin: %d",
type_str, YESNO(this->is_connected()), this->interrupt_pin_, this->reset_pin_);
#else
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Type: %s\n"
" Connected: %s\n"
" CLK Pin: %u\n"
" MISO Pin: %u\n"
" MOSI Pin: %u\n"
" CS Pin: %u\n"
" IRQ Pin: %d\n"
" Reset Pin: %d",
type_str, YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
this->interrupt_pin_, this->reset_pin_);
#endif
this->dump_connect_params_();
}
network::IPAddresses EthernetComponent::get_ip_addresses() {
network::IPAddresses addresses;
if (this->eth_ != nullptr) {
LwIPLock lock;
addresses[0] = network::IPAddress(this->eth_->localIP());
}
return addresses;
}
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
LwIPLock lock;
const ip_addr_t *dns_ip = dns_getserver(num);
return dns_ip;
}
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
if (this->eth_ != nullptr) {
this->eth_->macAddress(mac);
} else {
memset(mac, 0, 6);
}
}
std::string EthernetComponent::get_eth_mac_address_pretty() {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
}
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
uint8_t mac[6];
get_eth_mac_address_raw(mac);
format_mac_addr_upper(mac, buf.data());
return buf.data();
}
eth_duplex_t EthernetComponent::get_duplex_mode() {
// W5100, W5500, and ENC28J60 are full-duplex on RP2040
return ETH_DUPLEX_FULL;
}
eth_speed_t EthernetComponent::get_link_speed() {
#ifdef USE_ETHERNET_ENC28J60
// ENC28J60 is 10Mbps only
return ETH_SPEED_10M;
#else
// W5100 and W5500 are 100Mbps
return ETH_SPEED_100M;
#endif
}
bool EthernetComponent::powerdown() {
ESP_LOGI(TAG, "Powering down ethernet");
if (this->eth_ != nullptr) {
this->eth_->end();
}
this->connected_ = false;
this->started_ = false;
return true;
}
void EthernetComponent::start_connect_() {
this->got_ipv4_address_ = false;
this->connect_begin_ = millis();
this->status_set_warning(LOG_STR("waiting for IP configuration"));
// Hostname is already set in setup() via LwipIntf::setHostname()
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
// Static IP was already configured before begin() in setup()
// Set DNS servers
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns1;
dns_setserver(0, &d);
}
if (this->manual_ip_->dns2.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
}
#endif
}
void EthernetComponent::finish_connect_() {
// No additional work needed on RP2040 for now
// IPv6 link-local could be added here in the future
}
void EthernetComponent::dump_connect_params_() {
if (this->eth_ == nullptr) {
return;
}
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
// Copy all lwIP state under the lock to avoid races with IRQ callbacks
ip_addr_t ip_addr, netmask, gw, dns1_addr, dns2_addr;
{
LwIPLock lock;
auto *netif = this->eth_->getNetIf();
ip_addr = netif->ip_addr;
netmask = netif->netmask;
gw = netif->gw;
dns1_addr = *dns_getserver(0);
dns2_addr = *dns_getserver(1);
}
ESP_LOGCONFIG(TAG,
" IP Address: %s\n"
" Hostname: '%s'\n"
" Subnet: %s\n"
" Gateway: %s\n"
" DNS1: %s\n"
" DNS2: %s\n"
" MAC Address: %s",
network::IPAddress(&ip_addr).str_to(ip_buf), App.get_name().c_str(),
network::IPAddress(&netmask).str_to(subnet_buf), network::IPAddress(&gw).str_to(gateway_buf),
network::IPAddress(&dns1_addr).str_to(dns1_buf), network::IPAddress(&dns2_addr).str_to(dns2_buf),
this->get_eth_mac_address_pretty_into_buffer(mac_buf));
}
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
void EthernetComponent::enable() {
// RP2040 uses arduino-pico's LwipIntfDev which manages link state internally;
// there is no clean enable/disable hook today. The YAML option is accepted on
// RP2040 for schema parity but has no effect.
if (!this->disabled_)
return;
ESP_LOGW(TAG, "enable_on_boot/disable not supported");
this->disabled_ = false;
}
void EthernetComponent::disable() {
if (this->disabled_)
return;
ESP_LOGW(TAG, "enable_on_boot/disable not supported");
this->disabled_ = true;
}
} // namespace esphome::ethernet
#endif // USE_ETHERNET && USE_RP2