ESPHome 2026.4.0
Loading...
Searching...
No Matches
ethernet_component_rp2040.cpp
Go to the documentation of this file.
2
3#if defined(USE_ETHERNET) && defined(USE_RP2040)
4
7#include "esphome/core/log.h"
8
10
11#include <SPI.h>
12#include <lwip/dns.h>
13#include <lwip/netif.h>
14
15namespace esphome::ethernet {
16
17static const char *const TAG = "ethernet";
18
20 // Configure SPI pins
21#if !defined(USE_ETHERNET_W6300)
22 SPI.setRX(this->miso_pin_);
23 SPI.setTX(this->mosi_pin_);
24 SPI.setSCK(this->clk_pin_);
25#endif
26 // W6300 uses PIO QSPI with hardcoded pins, not Arduino SPI.
27 // SPI pin config is skipped; Wiznet6300lwIPFixed (needsSPI()=false)
28 // prevents LwipIntfDev::begin() from calling SPI.begin().
29
30 // Toggle reset pin if configured
31 if (this->reset_pin_ >= 0) {
32 rp2040::RP2040GPIOPin reset_pin;
33 reset_pin.set_pin(this->reset_pin_);
34 reset_pin.set_flags(gpio::FLAG_OUTPUT);
35 reset_pin.setup();
36 reset_pin.digital_write(false);
37 delay(1); // NOLINT
38 reset_pin.digital_write(true);
39 // W5100S needs 150ms for PLL lock; W5500/ENC28J60 need ~10ms
40 delay(RESET_DELAY_MS); // NOLINT
41 }
42
43 // Create the SPI Ethernet device instance
44#if defined(USE_ETHERNET_W5500)
45 this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
46#elif defined(USE_ETHERNET_W5100)
47 this->eth_ = new Wiznet5100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
48#elif defined(USE_ETHERNET_W6100)
49 this->eth_ = new Wiznet6100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
50#elif defined(USE_ETHERNET_W6300)
51 this->eth_ = new Wiznet6300lwIPFixed(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
52#elif defined(USE_ETHERNET_ENC28J60)
53 this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
54#endif
55
56 // Set hostname before begin() so the LWIP netif gets it
57 this->eth_->hostname(App.get_name().c_str());
58
59 // Configure static IP if set (must be done before begin())
60#ifdef USE_ETHERNET_MANUAL_IP
61 if (this->manual_ip_.has_value()) {
62 IPAddress ip(this->manual_ip_->static_ip);
63 IPAddress gateway(this->manual_ip_->gateway);
64 IPAddress subnet(this->manual_ip_->subnet);
65 IPAddress dns1(this->manual_ip_->dns1);
66 IPAddress dns2(this->manual_ip_->dns2);
67 this->eth_->config(ip, gateway, subnet, dns1, dns2);
68 }
69#endif
70
71 // Begin with fixed MAC or auto-generated
72 bool success;
73 if (this->fixed_mac_.has_value()) {
74 success = this->eth_->begin(this->fixed_mac_->data());
75 } else {
76 success = this->eth_->begin();
77 }
78
79 if (!success) {
80 ESP_LOGE(TAG, "Failed to initialize Ethernet");
81 delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory)
82 this->eth_ = nullptr;
83 this->mark_failed();
84 return;
85 }
86
87 // Make this the default interface for routing
88 this->eth_->setDefault(true);
89
90 // The arduino-pico LwipIntfDev automatically handles packet processing
91 // via __addEthernetPacketHandler when no interrupt pin is used,
92 // or via GPIO interrupt when one is provided.
93
94 // Don't set started_ here — let the link polling in loop() set it
95 // when the link is actually up. Setting it prematurely causes
96 // a "Starting → Stopped → Starting" log sequence because the chip
97 // needs time after begin() before the PHY link is ready.
98}
99
101 // On RP2040, we need to poll connection state since there are no events.
103
104 // Throttle link/IP polling to avoid excessive SPI transactions.
105 // W5500/ENC28J60 read PHY register via SPI on every linkStatus() call.
106 // W5100 can't detect link state, so we skip the SPI read and assume link-up.
107 // connected() reads netif->ip_addr without LwIPLock, but this is a single
108 // 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale
109 // value, which is benign for polling.
110 if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) {
111 this->last_link_check_ = now;
112#if defined(USE_ETHERNET_W5100)
113 // W5100 can't detect link (isLinkDetectable() returns false), so linkStatus()
114 // returns Unknown — assume link is up after successful begin()
115 bool link_up = true;
116#else
117 bool link_up = this->eth_->linkStatus() == LinkON;
118#endif
119 bool has_ip = this->eth_->connected();
120
121 if (!link_up) {
122 if (this->started_) {
123 this->started_ = false;
124 this->connected_ = false;
125 }
126 } else {
127 if (!this->started_) {
128 this->started_ = true;
129 }
130 bool was_connected = this->connected_;
131 this->connected_ = has_ip;
132 if (this->connected_ && !was_connected) {
133#ifdef USE_ETHERNET_IP_STATE_LISTENERS
135#endif
136 }
137 }
138 }
139
140 // State machine
141 switch (this->state_) {
143 if (this->started_) {
144 ESP_LOGI(TAG, "Starting connection");
146 this->start_connect_();
147 }
148 break;
150 if (!this->started_) {
151 ESP_LOGI(TAG, "Stopped connection");
153 } else if (this->connected_) {
154 // connection established
155 ESP_LOGI(TAG, "Connected");
157
158 this->dump_connect_params_();
159 this->status_clear_warning();
160#ifdef USE_ETHERNET_CONNECT_TRIGGER
162#endif
163 } else if (now - this->connect_begin_ > 15000) {
164 ESP_LOGW(TAG, "Connecting failed; reconnecting");
165 this->start_connect_();
166 }
167 break;
169 if (!this->started_) {
170 ESP_LOGI(TAG, "Stopped connection");
172#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
174#endif
175 } else if (!this->connected_) {
176 ESP_LOGW(TAG, "Connection lost; reconnecting");
178 this->start_connect_();
179#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
181#endif
182 } else {
183 this->finish_connect_();
184 }
185 break;
186 }
187}
188
190 const char *type_str = "Unknown";
191#if defined(USE_ETHERNET_W5500)
192 type_str = "W5500";
193#elif defined(USE_ETHERNET_W5100)
194 type_str = "W5100";
195#elif defined(USE_ETHERNET_W6100)
196 type_str = "W6100";
197#elif defined(USE_ETHERNET_W6300)
198 type_str = "W6300";
199#elif defined(USE_ETHERNET_ENC28J60)
200 type_str = "ENC28J60";
201#endif
202#if defined(USE_ETHERNET_W6300)
203 // W6300 uses PIO QSPI with hardcoded pins — SPI pin fields are not used
204 ESP_LOGCONFIG(TAG,
205 "Ethernet:\n"
206 " Type: %s (PIO QSPI)\n"
207 " Connected: %s\n"
208 " IRQ Pin: %d\n"
209 " Reset Pin: %d",
210 type_str, YESNO(this->is_connected()), this->interrupt_pin_, this->reset_pin_);
211#else
212 ESP_LOGCONFIG(TAG,
213 "Ethernet:\n"
214 " Type: %s\n"
215 " Connected: %s\n"
216 " CLK Pin: %u\n"
217 " MISO Pin: %u\n"
218 " MOSI Pin: %u\n"
219 " CS Pin: %u\n"
220 " IRQ Pin: %d\n"
221 " Reset Pin: %d",
222 type_str, YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
223 this->interrupt_pin_, this->reset_pin_);
224#endif
225 this->dump_connect_params_();
226}
227
229 network::IPAddresses addresses;
230 if (this->eth_ != nullptr) {
231 LwIPLock lock;
232 addresses[0] = network::IPAddress(this->eth_->localIP());
233 }
234 return addresses;
235}
236
237network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
238 LwIPLock lock;
239 const ip_addr_t *dns_ip = dns_getserver(num);
240 return dns_ip;
241}
242
244 if (this->eth_ != nullptr) {
245 this->eth_->macAddress(mac);
246 } else {
247 memset(mac, 0, 6);
248 }
249}
250
251std::string EthernetComponent::get_eth_mac_address_pretty() {
252 char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
253 return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
254}
255
257 std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
258 uint8_t mac[6];
260 format_mac_addr_upper(mac, buf.data());
261 return buf.data();
262}
263
265 // W5100, W5500, and ENC28J60 are full-duplex on RP2040
266 return ETH_DUPLEX_FULL;
267}
268
270#ifdef USE_ETHERNET_ENC28J60
271 // ENC28J60 is 10Mbps only
272 return ETH_SPEED_10M;
273#else
274 // W5100 and W5500 are 100Mbps
275 return ETH_SPEED_100M;
276#endif
277}
278
280 ESP_LOGI(TAG, "Powering down ethernet");
281 if (this->eth_ != nullptr) {
282 this->eth_->end();
283 }
284 this->connected_ = false;
285 this->started_ = false;
286 return true;
287}
288
290 this->got_ipv4_address_ = false;
291 this->connect_begin_ = millis();
292 this->status_set_warning(LOG_STR("waiting for IP configuration"));
293
294 // Hostname is already set in setup() via LwipIntf::setHostname()
295
296#ifdef USE_ETHERNET_MANUAL_IP
297 if (this->manual_ip_.has_value()) {
298 // Static IP was already configured before begin() in setup()
299 // Set DNS servers
300 LwIPLock lock;
301 if (this->manual_ip_->dns1.is_set()) {
302 ip_addr_t d;
303 d = this->manual_ip_->dns1;
304 dns_setserver(0, &d);
305 }
306 if (this->manual_ip_->dns2.is_set()) {
307 ip_addr_t d;
308 d = this->manual_ip_->dns2;
309 dns_setserver(1, &d);
310 }
311 }
312#endif
313}
314
316 // No additional work needed on RP2040 for now
317 // IPv6 link-local could be added here in the future
318}
319
321 if (this->eth_ == nullptr) {
322 return;
323 }
324
325 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
326 char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
327 char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
328 char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
329 char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
330 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
331
332 // Copy all lwIP state under the lock to avoid races with IRQ callbacks
333 ip_addr_t ip_addr, netmask, gw, dns1_addr, dns2_addr;
334 {
335 LwIPLock lock;
336 auto *netif = this->eth_->getNetIf();
337 ip_addr = netif->ip_addr;
338 netmask = netif->netmask;
339 gw = netif->gw;
340 dns1_addr = *dns_getserver(0);
341 dns2_addr = *dns_getserver(1);
342 }
343 ESP_LOGCONFIG(TAG,
344 " IP Address: %s\n"
345 " Hostname: '%s'\n"
346 " Subnet: %s\n"
347 " Gateway: %s\n"
348 " DNS1: %s\n"
349 " DNS2: %s\n"
350 " MAC Address: %s",
351 network::IPAddress(&ip_addr).str_to(ip_buf), App.get_name().c_str(),
352 network::IPAddress(&netmask).str_to(subnet_buf), network::IPAddress(&gw).str_to(gateway_buf),
353 network::IPAddress(&dns1_addr).str_to(dns1_buf), network::IPAddress(&dns2_addr).str_to(dns2_buf),
354 this->get_eth_mac_address_pretty_into_buffer(mac_buf));
355}
356
357void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
358void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
359void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
360void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
361void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
362void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
363
364} // namespace esphome::ethernet
365
366#endif // USE_ETHERNET && USE_RP2040
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
void status_clear_warning()
Definition component.h:293
constexpr const char * c_str() const
Definition string_ref.h:73
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:470
static constexpr uint32_t LINK_CHECK_INTERVAL
network::IPAddress get_dns_address(uint8_t num)
static constexpr uint32_t RESET_DELAY_MS
optional< std::array< uint8_t, 6 > > fixed_mac_
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") std const char * get_eth_mac_address_pretty_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
uint32_t ip_addr
LwipIntfDev< Wiznet6300NoSPI > Wiznet6300lwIPFixed
in_addr ip_addr_t
Definition ip_address.h:22
@ FLAG_OUTPUT
Definition gpio.h:28
std::array< IPAddress, 5 > IPAddresses
Definition ip_address.h:187
void HOT delay(uint32_t ms)
Definition core.cpp:28
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:26
Application App
Global storage of Application pointer - only one Application can exist.
char * format_mac_addr_upper(const uint8_t *mac, char *output)
Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators)
Definition helpers.h:1420
static void uint32_t