[ethernet] Add ENC28J60 SPI Ethernet support (#14945)

This commit is contained in:
J. Nick Koston
2026-03-23 09:18:58 -10:00
committed by GitHub
parent 4c1363b104
commit 1e16b30380
8 changed files with 134 additions and 30 deletions

View File

@@ -119,6 +119,7 @@ ETHERNET_TYPES = {
"OPENETH": EthernetType.ETHERNET_TYPE_OPENETH,
"DM9051": EthernetType.ETHERNET_TYPE_DM9051,
"LAN8670": EthernetType.ETHERNET_TYPE_LAN8670,
"ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60,
}
# PHY types that need compile-time defines for conditional compilation
@@ -134,6 +135,7 @@ _PHY_TYPE_TO_DEFINE = {
"W5500": "USE_ETHERNET_W5500",
"DM9051": "USE_ETHERNET_DM9051",
"LAN8670": "USE_ETHERNET_LAN8670",
"ENC28J60": "USE_ETHERNET_ENC28J60",
}
@@ -155,11 +157,16 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = {
"KSZ8081RNA": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"),
"W5500": IDFRegistryComponent("espressif/w5500", "1.0.1"),
"DM9051": IDFRegistryComponent("espressif/dm9051", "1.0.0"),
"ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"),
"LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"),
}
SPI_ETHERNET_TYPES = ["W5500", "DM9051"]
# These types are always external IDF components (never built-in to ESP-IDF)
_ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"}
SPI_ETHERNET_TYPES = ["W5500", "DM9051", "ENC28J60"]
# RP2040-supported SPI ethernet types
RP2040_SPI_ETHERNET_TYPES = ["W5500"]
RP2040_SPI_ETHERNET_TYPES = ["W5500", "ENC28J60"]
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10)
emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
@@ -220,7 +227,18 @@ def _validate(config):
if CORE.is_esp32:
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
if _is_framework_spi_polling_mode_supported():
# ENC28J60 driver does not support polling mode - interrupt is required
if config[CONF_TYPE] == "ENC28J60":
if CONF_POLLING_INTERVAL in config:
raise cv.Invalid(
f"'{CONF_POLLING_INTERVAL}' is not supported for ENC28J60. "
f"'{CONF_INTERRUPT_PIN}' is required."
)
if CONF_INTERRUPT_PIN not in config:
raise cv.Invalid(
f"'{CONF_INTERRUPT_PIN}' is a required option for ENC28J60."
)
elif _is_framework_spi_polling_mode_supported():
if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config:
raise cv.Invalid(
f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}"
@@ -367,6 +385,7 @@ CONFIG_SCHEMA = cv.All(
"W5500": SPI_SCHEMA,
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
"DM9051": SPI_SCHEMA,
"ENC28J60": SPI_SCHEMA,
"LAN8670": RMII_SCHEMA,
},
upper=True,
@@ -502,7 +521,8 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
cg.add_define("USE_ETHERNET_SPI")
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
# CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0
if idf_version() < cv.Version(6, 0, 0):
# ENC28J60 was never built-in to IDF, so it has no Kconfig option
if idf_version() < cv.Version(6, 0, 0) and config[CONF_TYPE] != "ENC28J60":
add_idf_sdkconfig_option(
f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True
)
@@ -533,12 +553,11 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
# Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time)
include_builtin_idf_component("esp_eth")
if config[CONF_TYPE] == "LAN8670":
# Add LAN867x 10BASE-T1S PHY support component
add_idf_component(name="espressif/lan867x", ref="2.0.0")
# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry
if idf_version() >= cv.Version(6, 0, 0) and (
if config[CONF_TYPE] in _ALWAYS_EXTERNAL_IDF_COMPONENTS:
component = _IDF6_ETHERNET_COMPONENTS[config[CONF_TYPE]]
add_idf_component(name=component.name, ref=component.version)
elif idf_version() >= cv.Version(6, 0, 0) and (
# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry
component := _IDF6_ETHERNET_COMPONENTS.get(config[CONF_TYPE])
):
add_idf_component(name=component.name, ref=component.version)
@@ -555,7 +574,10 @@ async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None:
cg.add(var.set_reset_pin(config[CONF_RESET_PIN]))
cg.add_define("USE_ETHERNET_SPI")
cg.add_library("lwIP_w5500", None)
if config[CONF_TYPE] == "ENC28J60":
cg.add_library("lwIP_enc28j60", None)
else:
cg.add_library("lwIP_w5500", None)
def _final_validate_rmii_pins(config: ConfigType) -> None:

View File

@@ -23,7 +23,13 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void);
#endif // USE_ESP32
#ifdef USE_RP2040
#if defined(USE_ETHERNET_W5500)
#include <W5500lwIP.h>
#elif defined(USE_ETHERNET_ENC28J60)
#include <ENC28J60lwIP.h>
#else
#error "Unsupported RP2040 SPI Ethernet type"
#endif
#endif
namespace esphome::ethernet {
@@ -57,6 +63,7 @@ enum EthernetType : uint8_t {
ETHERNET_TYPE_OPENETH,
ETHERNET_TYPE_DM9051,
ETHERNET_TYPE_LAN8670,
ETHERNET_TYPE_ENC28J60,
};
struct ManualIP {
@@ -215,7 +222,13 @@ class EthernetComponent final : public Component {
#ifdef USE_RP2040
static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls
#if defined(USE_ETHERNET_W5500)
Wiznet5500lwIP *eth_{nullptr};
#elif defined(USE_ETHERNET_ENC28J60)
ENC28J60lwIP *eth_{nullptr};
#else
#error "Unsupported RP2040 SPI Ethernet type"
#endif
uint32_t last_link_check_{0};
uint8_t clk_pin_;
uint8_t miso_pin_;

View File

@@ -44,6 +44,11 @@
#include "esp_eth_phy_lan867x.h"
#endif
// ENC28J60 header exists on all IDF versions (always an external component)
#ifdef USE_ETHERNET_ENC28J60
#include "esp_eth_enc28j60.h"
#endif
#ifdef USE_ETHERNET_SPI
#include <driver/gpio.h>
#include <driver/spi_master.h>
@@ -194,25 +199,27 @@ void EthernetComponent::setup() {
.post_cb = nullptr,
};
#ifdef USE_ETHERNET_W5500
#if defined(USE_ETHERNET_W5500)
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg);
#endif
#ifdef USE_ETHERNET_DM9051
#elif defined(USE_ETHERNET_DM9051)
eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg);
#elif defined(USE_ETHERNET_ENC28J60)
eth_enc28j60_config_t enc28j60_config = ETH_ENC28J60_DEFAULT_CONFIG(host, &devcfg);
#endif
#ifdef USE_ETHERNET_W5500
#if defined(USE_ETHERNET_W5500)
w5500_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
w5500_config.poll_period_ms = this->polling_interval_;
#endif
#endif
#ifdef USE_ETHERNET_DM9051
#elif defined(USE_ETHERNET_DM9051)
dm9051_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
dm9051_config.poll_period_ms = this->polling_interval_;
#endif
#elif defined(USE_ETHERNET_ENC28J60)
enc28j60_config.int_gpio_num = this->interrupt_pin_;
// ENC28J60 does not support poll_period_ms
#endif
phy_config.phy_addr = this->phy_addr_spi_;
@@ -300,19 +307,24 @@ void EthernetComponent::setup() {
#endif
#endif
#ifdef USE_ETHERNET_SPI
#ifdef USE_ETHERNET_W5500
#if defined(USE_ETHERNET_W5500)
case ETHERNET_TYPE_W5500: {
mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
this->phy_ = esp_eth_phy_new_w5500(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_DM9051
#elif defined(USE_ETHERNET_DM9051)
case ETHERNET_TYPE_DM9051: {
mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config);
this->phy_ = esp_eth_phy_new_dm9051(&phy_config);
break;
}
#elif defined(USE_ETHERNET_ENC28J60)
case ETHERNET_TYPE_ENC28J60: {
mac = esp_eth_mac_new_enc28j60(&enc28j60_config, &mac_config);
this->phy_ = esp_eth_phy_new_enc28j60(&phy_config);
break;
}
#endif
#endif
default: {
@@ -405,15 +417,18 @@ void EthernetComponent::dump_config() {
eth_type = "KSZ8081RNA";
break;
#endif
#ifdef USE_ETHERNET_W5500
#if defined(USE_ETHERNET_W5500)
case ETHERNET_TYPE_W5500:
eth_type = "W5500";
break;
#endif
#ifdef USE_ETHERNET_DM9051
#elif defined(USE_ETHERNET_DM9051)
case ETHERNET_TYPE_DM9051:
eth_type = "DM9051";
break;
#elif defined(USE_ETHERNET_ENC28J60)
case ETHERNET_TYPE_ENC28J60:
eth_type = "ENC28J60";
break;
#endif
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH:

View File

@@ -31,11 +31,15 @@ void EthernetComponent::setup() {
reset_pin.digital_write(false);
delay(1); // NOLINT
reset_pin.digital_write(true);
delay(10); // NOLINT - wait for W5500 to initialize after reset
delay(10); // NOLINT - wait for chip to initialize after reset
}
// Create the W5500 device instance
// 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_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());
@@ -61,7 +65,7 @@ void EthernetComponent::setup() {
}
if (!success) {
ESP_LOGE(TAG, "Failed to initialize W5500 Ethernet");
ESP_LOGE(TAG, "Failed to initialize Ethernet");
delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory)
this->eth_ = nullptr;
this->mark_failed();
@@ -164,9 +168,15 @@ void EthernetComponent::loop() {
}
void EthernetComponent::dump_config() {
const char *type_str = "Unknown";
#if defined(USE_ETHERNET_W5500)
type_str = "W5500";
#elif defined(USE_ETHERNET_ENC28J60)
type_str = "ENC28J60";
#endif
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Type: W5500\n"
" Type: %s\n"
" Connected: %s\n"
" CLK Pin: %u\n"
" MISO Pin: %u\n"
@@ -174,7 +184,7 @@ void EthernetComponent::dump_config() {
" CS Pin: %u\n"
" IRQ Pin: %d\n"
" Reset Pin: %d",
YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
type_str, YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
this->interrupt_pin_, this->reset_pin_);
this->dump_connect_params_();
}
@@ -216,13 +226,18 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
}
eth_duplex_t EthernetComponent::get_duplex_mode() {
// W5500 is always full duplex
// Both 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
// W5500 is always 100Mbps
return ETH_SPEED_100M;
#endif
}
bool EthernetComponent::powerdown() {

View File

@@ -0,0 +1,18 @@
ethernet:
type: ENC28J60
clk_pin: 18
mosi_pin: 19
miso_pin: 16
cs_pin: 17
interrupt_pin: 21
reset_pin: 20
manual_ip:
static_ip: 192.168.178.56
gateway: 192.168.178.1
subnet: 255.255.255.0
domain: .local
mac_address: "02:AA:BB:CC:DD:01"
on_connect:
- logger.log: "Ethernet connected!"
on_disconnect:
- logger.log: "Ethernet disconnected!"

View File

@@ -0,0 +1,19 @@
ethernet:
type: ENC28J60
clk_pin: 19
mosi_pin: 21
miso_pin: 23
cs_pin: 18
interrupt_pin: 36
reset_pin: 22
clock_speed: 10Mhz
manual_ip:
static_ip: 192.168.178.56
gateway: 192.168.178.1
subnet: 255.255.255.0
domain: .local
mac_address: "02:AA:BB:CC:DD:01"
on_connect:
- logger.log: "Ethernet connected!"
on_disconnect:
- logger.log: "Ethernet disconnected!"

View File

@@ -0,0 +1 @@
<<: !include common-enc28j60.yaml

View File

@@ -0,0 +1 @@
<<: !include common-enc28j60-rp2040.yaml