[ethernet] Add KSZ8851SNL SPI ethernet support (#19883)

This commit is contained in:
Jesse Hills
2026-09-30 13:48:10 +13:00
committed by GitHub
parent ed88002c37
commit ac1190094a
9 changed files with 195 additions and 1 deletions
+11 -1
View File
@@ -138,6 +138,7 @@ ETHERNET_TYPES = {
"GENERIC": EthernetType.ETHERNET_TYPE_GENERIC,
"YT8531": EthernetType.ETHERNET_TYPE_YT8531,
"CH390": EthernetType.ETHERNET_TYPE_CH390,
"KSZ8851SNL": EthernetType.ETHERNET_TYPE_KSZ8851SNL,
}
# PHY types that need compile-time defines for conditional compilation
@@ -160,6 +161,7 @@ _PHY_TYPE_TO_DEFINE = {
"GENERIC": "USE_ETHERNET_GENERIC",
"YT8531": "USE_ETHERNET_YT8531",
"CH390": "USE_ETHERNET_CH390",
"KSZ8851SNL": "USE_ETHERNET_KSZ8851SNL",
}
@@ -184,13 +186,14 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = {
"ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"),
"LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"),
"CH390": IDFRegistryComponent("espressif/ch390", "0.3.0"),
"KSZ8851SNL": IDFRegistryComponent("espressif/ksz8851snl", "1.2.0"),
}
# These types are always external IDF components (never built-in to ESP-IDF)
_ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60", "CH390"}
# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver)
SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60", "CH390"}
SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60", "CH390", "KSZ8851SNL"}
# RP2-supported ethernet types (SPI and PIO QSPI). Applies to the whole
# RP2 family (RP2040 and RP2350); the chip-specific W5100 caveat in the
# comment above is about ESP-IDF driver coverage, not the RP2 platform.
@@ -534,6 +537,12 @@ SPI_SCHEMA_ENC28J60 = _spi_schema(default_clock="20MHz", max_clock=int(20e6))
# https://www.wch-ic.com/downloads/CH390DS1_PDF.html
SPI_SCHEMA_CH390 = _spi_schema(max_clock=int(72e6))
# The KSZ8851SNL rates fSCLK at 40 MHz maximum, so the shared 80 MHz ceiling is
# out of spec while the 26.67 MHz default is not. KSZ8851SNL/SNLI datasheet
# DS00002381C, table 7-1:
# https://ww1.microchip.com/downloads/aemDocuments/documents/UNG/ProductDocuments/DataSheets/KSZ8851SNL-Single-Port-Ethernet-Controller-with-SPI-DS00002381C.pdf
SPI_SCHEMA_KSZ8851SNL = _spi_schema(max_clock=int(40e6))
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
@@ -549,6 +558,7 @@ CONFIG_SCHEMA = cv.All(
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
"DM9051": SPI_SCHEMA,
"CH390": SPI_SCHEMA_CH390,
"KSZ8851SNL": SPI_SCHEMA_KSZ8851SNL,
"ENC28J60": SPI_SCHEMA_ENC28J60,
"W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])),
"W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])),
@@ -92,6 +92,7 @@ enum EthernetType : uint8_t {
ETHERNET_TYPE_GENERIC,
ETHERNET_TYPE_YT8531,
ETHERNET_TYPE_CH390,
ETHERNET_TYPE_KSZ8851SNL,
};
struct ManualIP {
@@ -41,6 +41,10 @@
#include "esp_eth_mac_dm9051.h"
#include "esp_eth_phy_dm9051.h"
#endif
#ifdef USE_ETHERNET_KSZ8851SNL
#include "esp_eth_mac_ksz8851snl.h"
#include "esp_eth_phy_ksz8851snl.h"
#endif
#endif // ESP_IDF_VERSION >= 6.0.0
// LAN867x header exists on all IDF versions (external component since IDF 5.3)
@@ -264,6 +268,8 @@ void EthernetComponent::ethernet_lazy_init_() {
eth_enc28j60_config_t enc28j60_config = ETH_ENC28J60_DEFAULT_CONFIG(host, &devcfg);
#elif defined(USE_ETHERNET_CH390)
eth_ch390_config_t ch390_config = ETH_CH390_DEFAULT_CONFIG(host, &devcfg);
#elif defined(USE_ETHERNET_KSZ8851SNL)
eth_ksz8851snl_config_t ksz8851snl_config = ETH_KSZ8851SNL_DEFAULT_CONFIG(host, &devcfg);
#endif
#if defined(USE_ETHERNET_W5500)
@@ -290,6 +296,11 @@ void EthernetComponent::ethernet_lazy_init_() {
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
ch390_config.poll_period_ms = this->polling_interval_;
#endif
#elif defined(USE_ETHERNET_KSZ8851SNL)
ksz8851snl_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
ksz8851snl_config.poll_period_ms = this->polling_interval_;
#endif
#endif
phy_config.phy_addr = this->phy_addr_spi_;
@@ -420,6 +431,12 @@ void EthernetComponent::ethernet_lazy_init_() {
this->phy_ = esp_eth_phy_new_ch390(&phy_config);
break;
}
#elif defined(USE_ETHERNET_KSZ8851SNL)
case ETHERNET_TYPE_KSZ8851SNL: {
mac = esp_eth_mac_new_ksz8851snl(&ksz8851snl_config, &mac_config);
this->phy_ = esp_eth_phy_new_ksz8851snl(&phy_config);
break;
}
#endif
#endif
default: {
@@ -593,6 +610,10 @@ void EthernetComponent::dump_config() {
case ETHERNET_TYPE_CH390:
eth_type = "CH390";
break;
#elif defined(USE_ETHERNET_KSZ8851SNL)
case ETHERNET_TYPE_KSZ8851SNL:
eth_type = "KSZ8851SNL";
break;
#endif
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH:
+2
View File
@@ -473,8 +473,10 @@
#define USE_ETHERNET_W6300
#define USE_ETHERNET_DM9051
#define USE_ETHERNET_CH390
#define USE_ETHERNET_KSZ8851SNL
#define CONFIG_ETH_SPI_ETHERNET_W5500 1
#define CONFIG_ETH_SPI_ETHERNET_DM9051 1
#define CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL 1
#define CONFIG_ETH_USE_ESP32_EMAC 1
#define USE_ETHERNET_MANUAL_IP
#define USE_ETHERNET_IP_STATE_LISTENERS
@@ -0,0 +1,16 @@
esphome:
name: test
esp32:
board: esp32dev
ethernet:
id: eth_component
type: KSZ8851SNL
clk_pin: GPIO18
mosi_pin: GPIO23
miso_pin: GPIO19
cs_pin: GPIO5
interrupt_pin: GPIO36
reset_pin: GPIO22
clock_speed: 20MHz
@@ -0,0 +1,18 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
version: 6.0.0
ethernet:
id: eth_component
type: KSZ8851SNL
clk_pin: GPIO18
mosi_pin: GPIO23
miso_pin: GPIO19
cs_pin: GPIO5
interrupt_pin: GPIO36
reset_pin: GPIO22
clock_speed: 20MHz
@@ -0,0 +1,105 @@
"""Tests for the KSZ8851SNL SPI ethernet type."""
from collections.abc import Callable
from pathlib import Path
import pytest
from voluptuous import Invalid
from esphome import config_validation as cv
from esphome.components.esp32 import (
KEY_BOARD,
KEY_IDF_VERSION,
KEY_VARIANT,
VARIANT_ESP32S3,
)
from esphome.components.esp32.const import (
KEY_COMPONENTS,
KEY_ESP32,
KEY_REF,
KEY_SDKCONFIG_OPTIONS,
)
from esphome.components.ethernet import CONF_CLOCK_SPEED, CONFIG_SCHEMA
from esphome.const import PlatformFramework
from esphome.core import CORE
from ..types import SetCoreConfigCallable
_BASE_CONFIG = {
"type": "KSZ8851SNL",
"clk_pin": 47,
"mosi_pin": 48,
"miso_pin": 14,
"cs_pin": 21,
}
_SDKCONFIG_OPTION = "CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL"
_IDF_COMPONENT = "espressif/ksz8851snl"
def _sdkconfig() -> dict[str, object]:
return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
def _idf_components() -> dict[str, dict[str, str | None]]:
return CORE.data[KEY_ESP32][KEY_COMPONENTS]
def test_codegen_on_idf5_wires_the_spi_driver(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""On IDF 5.x the driver is built into esp_eth and enabled by its Kconfig option."""
main_cpp = generate_main(component_config_path("ksz8851snl_idf5.yaml"))
assert "eth_component->set_type(ethernet::ETHERNET_TYPE_KSZ8851SNL);" in main_cpp
assert "eth_component->set_cs_pin(5);" in main_cpp
assert "eth_component->set_interrupt_pin(36);" in main_cpp
assert "USE_ETHERNET_KSZ8851SNL" in {d.name for d in CORE.defines}
assert _sdkconfig()[_SDKCONFIG_OPTION] is True
# Built into IDF 5.x, so the registry component must not be pulled in.
assert _IDF_COMPONENT not in _idf_components()
def test_codegen_on_idf6_pulls_the_registry_component(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""IDF 6.0 dropped the built-in driver, so the managed component is required."""
generate_main(component_config_path("ksz8851snl_idf6.yaml"))
assert _idf_components()[_IDF_COMPONENT][KEY_REF] == "1.2.0"
# The Kconfig option no longer exists on IDF 6.0.
assert _SDKCONFIG_OPTION not in _sdkconfig()
def _set_esp32_s3(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
KEY_IDF_VERSION: cv.Version(5, 3, 2),
},
)
# _validate derives use_address from the node name, which has no default here.
CORE.name = "ksz8851snl-test"
@pytest.mark.parametrize("clock_speed", ["26.67MHz", "40MHz"])
def test_accepts_clock_speed_up_to_the_datasheet_maximum(
set_core_config: SetCoreConfigCallable, clock_speed: str
) -> None:
"""The datasheet rates fSCLK to 40MHz, so the whole range must be accepted."""
_set_esp32_s3(set_core_config)
config = CONFIG_SCHEMA({**_BASE_CONFIG, CONF_CLOCK_SPEED: clock_speed})
assert config[CONF_CLOCK_SPEED] == cv.frequency(clock_speed)
def test_rejects_clock_speed_above_the_datasheet_maximum(
set_core_config: SetCoreConfigCallable,
) -> None:
"""The shared 80MHz ceiling is out of spec for this part."""
_set_esp32_s3(set_core_config)
with pytest.raises(Invalid, match="value must be at most 40000000"):
CONFIG_SCHEMA({**_BASE_CONFIG, CONF_CLOCK_SPEED: "80MHz"})
@@ -0,0 +1,19 @@
ethernet:
type: KSZ8851SNL
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!"
@@ -0,0 +1,2 @@
packages:
ethernet: !include common-ksz8851snl.yaml