mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[ethernet] Add CH390 SPI ethernet support (#18226)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
co-authored by
Claude Opus 5
J. Nick Koston
parent
bae7f19323
commit
2184ec2928
@@ -132,6 +132,7 @@ ETHERNET_TYPES = {
|
|||||||
"W6300": EthernetType.ETHERNET_TYPE_W6300,
|
"W6300": EthernetType.ETHERNET_TYPE_W6300,
|
||||||
"GENERIC": EthernetType.ETHERNET_TYPE_GENERIC,
|
"GENERIC": EthernetType.ETHERNET_TYPE_GENERIC,
|
||||||
"YT8531": EthernetType.ETHERNET_TYPE_YT8531,
|
"YT8531": EthernetType.ETHERNET_TYPE_YT8531,
|
||||||
|
"CH390": EthernetType.ETHERNET_TYPE_CH390,
|
||||||
}
|
}
|
||||||
|
|
||||||
# PHY types that need compile-time defines for conditional compilation
|
# PHY types that need compile-time defines for conditional compilation
|
||||||
@@ -153,6 +154,7 @@ _PHY_TYPE_TO_DEFINE = {
|
|||||||
"W6300": "USE_ETHERNET_W6300",
|
"W6300": "USE_ETHERNET_W6300",
|
||||||
"GENERIC": "USE_ETHERNET_GENERIC",
|
"GENERIC": "USE_ETHERNET_GENERIC",
|
||||||
"YT8531": "USE_ETHERNET_YT8531",
|
"YT8531": "USE_ETHERNET_YT8531",
|
||||||
|
"CH390": "USE_ETHERNET_CH390",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -176,13 +178,14 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = {
|
|||||||
"DM9051": IDFRegistryComponent("espressif/dm9051", "1.1.0"),
|
"DM9051": IDFRegistryComponent("espressif/dm9051", "1.1.0"),
|
||||||
"ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"),
|
"ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"),
|
||||||
"LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"),
|
"LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"),
|
||||||
|
"CH390": IDFRegistryComponent("espressif/ch390", "0.3.0"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# These types are always external IDF components (never built-in to ESP-IDF)
|
# These types are always external IDF components (never built-in to ESP-IDF)
|
||||||
_ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"}
|
_ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60", "CH390"}
|
||||||
|
|
||||||
# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver)
|
# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver)
|
||||||
SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60"}
|
SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60", "CH390"}
|
||||||
# RP2-supported ethernet types (SPI and PIO QSPI). Applies to the whole
|
# RP2-supported ethernet types (SPI and PIO QSPI). Applies to the whole
|
||||||
# RP2 family (RP2040 and RP2350); the chip-specific W5100 caveat in the
|
# RP2 family (RP2040 and RP2350); the chip-specific W5100 caveat in the
|
||||||
# comment above is about ESP-IDF driver coverage, not the RP2 platform.
|
# comment above is about ESP-IDF driver coverage, not the RP2 platform.
|
||||||
@@ -480,6 +483,12 @@ SPI_SCHEMA = _spi_schema()
|
|||||||
# of spec for it and makes the driver's CS hold time helper compute no hold
|
# of spec for it and makes the driver's CS hold time helper compute no hold
|
||||||
SPI_SCHEMA_ENC28J60 = _spi_schema(default_clock="20MHz", max_clock=int(20e6))
|
SPI_SCHEMA_ENC28J60 = _spi_schema(default_clock="20MHz", max_clock=int(20e6))
|
||||||
|
|
||||||
|
# The CH390H/D rates SCK at 50 MHz typical and 72 MHz maximum with VDDIO at 3.3V,
|
||||||
|
# so the shared 80 MHz ceiling is out of spec while the 26.67 MHz default is not.
|
||||||
|
# CH390 datasheet v1.8, tables 9-4 and 9-5:
|
||||||
|
# https://www.wch-ic.com/downloads/CH390DS1_PDF.html
|
||||||
|
SPI_SCHEMA_CH390 = _spi_schema(max_clock=int(72e6))
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.typed_schema(
|
cv.typed_schema(
|
||||||
{
|
{
|
||||||
@@ -494,6 +503,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
"W5500": SPI_SCHEMA,
|
"W5500": SPI_SCHEMA,
|
||||||
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
|
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
|
||||||
"DM9051": SPI_SCHEMA,
|
"DM9051": SPI_SCHEMA,
|
||||||
|
"CH390": SPI_SCHEMA_CH390,
|
||||||
"ENC28J60": SPI_SCHEMA_ENC28J60,
|
"ENC28J60": SPI_SCHEMA_ENC28J60,
|
||||||
"W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])),
|
"W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])),
|
||||||
"W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])),
|
"W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])),
|
||||||
@@ -629,8 +639,11 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
|
|||||||
cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]]))
|
cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]]))
|
||||||
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
|
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
|
||||||
# CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0
|
# CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0
|
||||||
# ENC28J60 was never built-in to IDF, so it has no Kconfig option
|
# Types that are never built into IDF ship no Kconfig option at all
|
||||||
if idf_version() < cv.Version(6, 0, 0) and config[CONF_TYPE] != "ENC28J60":
|
if (
|
||||||
|
idf_version() < cv.Version(6, 0, 0)
|
||||||
|
and config[CONF_TYPE] not in _ALWAYS_EXTERNAL_IDF_COMPONENTS
|
||||||
|
):
|
||||||
add_idf_sdkconfig_option(
|
add_idf_sdkconfig_option(
|
||||||
f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True
|
f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ enum EthernetType : uint8_t {
|
|||||||
ETHERNET_TYPE_W6300,
|
ETHERNET_TYPE_W6300,
|
||||||
ETHERNET_TYPE_GENERIC,
|
ETHERNET_TYPE_GENERIC,
|
||||||
ETHERNET_TYPE_YT8531,
|
ETHERNET_TYPE_YT8531,
|
||||||
|
ETHERNET_TYPE_CH390,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ManualIP {
|
struct ManualIP {
|
||||||
|
|||||||
@@ -50,6 +50,12 @@
|
|||||||
#include "esp_eth_enc28j60.h"
|
#include "esp_eth_enc28j60.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// CH390 headers exist on all IDF versions (always an external component)
|
||||||
|
#ifdef USE_ETHERNET_CH390
|
||||||
|
#include "esp_eth_mac_ch390.h"
|
||||||
|
#include "esp_eth_phy_ch390.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ETHERNET_SPI
|
#ifdef USE_ETHERNET_SPI
|
||||||
#include <driver/gpio.h>
|
#include <driver/gpio.h>
|
||||||
#include <driver/spi_master.h>
|
#include <driver/spi_master.h>
|
||||||
@@ -215,6 +221,8 @@ void EthernetComponent::ethernet_lazy_init_() {
|
|||||||
eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg);
|
eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg);
|
||||||
#elif defined(USE_ETHERNET_ENC28J60)
|
#elif defined(USE_ETHERNET_ENC28J60)
|
||||||
eth_enc28j60_config_t enc28j60_config = ETH_ENC28J60_DEFAULT_CONFIG(host, &devcfg);
|
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);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(USE_ETHERNET_W5500)
|
#if defined(USE_ETHERNET_W5500)
|
||||||
@@ -236,6 +244,11 @@ void EthernetComponent::ethernet_lazy_init_() {
|
|||||||
// time (t10, 210 ns) after the last clock or MAC/MII register reads fail ("wrong chip ID")
|
// time (t10, 210 ns) after the last clock or MAC/MII register reads fail ("wrong chip ID")
|
||||||
enc28j60_config.spi_devcfg->cs_ena_posttrans = enc28j60_cal_spi_cs_hold_time((this->clock_speed_ + 999999) / 1000000);
|
enc28j60_config.spi_devcfg->cs_ena_posttrans = enc28j60_cal_spi_cs_hold_time((this->clock_speed_ + 999999) / 1000000);
|
||||||
enc28j60_config.int_gpio_num = this->interrupt_pin_;
|
enc28j60_config.int_gpio_num = this->interrupt_pin_;
|
||||||
|
#elif defined(USE_ETHERNET_CH390)
|
||||||
|
ch390_config.int_gpio_num = this->interrupt_pin_;
|
||||||
|
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||||
|
ch390_config.poll_period_ms = this->polling_interval_;
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
phy_config.phy_addr = this->phy_addr_spi_;
|
phy_config.phy_addr = this->phy_addr_spi_;
|
||||||
@@ -360,6 +373,12 @@ void EthernetComponent::ethernet_lazy_init_() {
|
|||||||
this->phy_ = esp_eth_phy_new_enc28j60(&phy_config);
|
this->phy_ = esp_eth_phy_new_enc28j60(&phy_config);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
#elif defined(USE_ETHERNET_CH390)
|
||||||
|
case ETHERNET_TYPE_CH390: {
|
||||||
|
mac = esp_eth_mac_new_ch390(&ch390_config, &mac_config);
|
||||||
|
this->phy_ = esp_eth_phy_new_ch390(&phy_config);
|
||||||
|
break;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
default: {
|
default: {
|
||||||
@@ -519,6 +538,10 @@ void EthernetComponent::dump_config() {
|
|||||||
case ETHERNET_TYPE_ENC28J60:
|
case ETHERNET_TYPE_ENC28J60:
|
||||||
eth_type = "ENC28J60";
|
eth_type = "ENC28J60";
|
||||||
break;
|
break;
|
||||||
|
#elif defined(USE_ETHERNET_CH390)
|
||||||
|
case ETHERNET_TYPE_CH390:
|
||||||
|
eth_type = "CH390";
|
||||||
|
break;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ETHERNET_OPENETH
|
#ifdef USE_ETHERNET_OPENETH
|
||||||
case ETHERNET_TYPE_OPENETH:
|
case ETHERNET_TYPE_OPENETH:
|
||||||
|
|||||||
@@ -397,6 +397,7 @@
|
|||||||
#define USE_ETHERNET_W6100
|
#define USE_ETHERNET_W6100
|
||||||
#define USE_ETHERNET_W6300
|
#define USE_ETHERNET_W6300
|
||||||
#define USE_ETHERNET_DM9051
|
#define USE_ETHERNET_DM9051
|
||||||
|
#define USE_ETHERNET_CH390
|
||||||
#define CONFIG_ETH_SPI_ETHERNET_W5500 1
|
#define CONFIG_ETH_SPI_ETHERNET_W5500 1
|
||||||
#define CONFIG_ETH_SPI_ETHERNET_DM9051 1
|
#define CONFIG_ETH_SPI_ETHERNET_DM9051 1
|
||||||
#define CONFIG_ETH_USE_ESP32_EMAC 1
|
#define CONFIG_ETH_USE_ESP32_EMAC 1
|
||||||
|
|||||||
@@ -1,13 +1,31 @@
|
|||||||
"""Tests for the ethernet final-validation coexistence gate."""
|
"""Tests for the ethernet final-validation coexistence gate and schema bounds."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from voluptuous import Invalid
|
from voluptuous import Invalid
|
||||||
|
|
||||||
from esphome.components.ethernet import _final_validate
|
from esphome import config_validation as cv
|
||||||
|
from esphome.components.esp32 import (
|
||||||
|
KEY_BOARD,
|
||||||
|
KEY_IDF_VERSION,
|
||||||
|
KEY_VARIANT,
|
||||||
|
VARIANT_ESP32S3,
|
||||||
|
)
|
||||||
|
from esphome.components.ethernet import CONF_CLOCK_SPEED, CONFIG_SCHEMA, _final_validate
|
||||||
from esphome.components.network import _validate_priority_list
|
from esphome.components.network import _validate_priority_list
|
||||||
from esphome.const import CONF_PRIORITY
|
from esphome.const import CONF_PRIORITY, PlatformFramework
|
||||||
|
from esphome.core import CORE
|
||||||
import esphome.final_validate as fv
|
import esphome.final_validate as fv
|
||||||
|
|
||||||
|
from ..types import SetCoreConfigCallable
|
||||||
|
|
||||||
|
_CH390_CONFIG = {
|
||||||
|
"type": "CH390",
|
||||||
|
"clk_pin": 47,
|
||||||
|
"mosi_pin": 48,
|
||||||
|
"miso_pin": 14,
|
||||||
|
"cs_pin": 21,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _reset_full_config():
|
def _reset_full_config():
|
||||||
@@ -35,3 +53,40 @@ def test_rejects_wifi_and_ethernet_with_incomplete_priority() -> None:
|
|||||||
)
|
)
|
||||||
with pytest.raises(Invalid, match=r"must.*list both interfaces; missing: wifi"):
|
with pytest.raises(Invalid, match=r"must.*list both interfaces; missing: wifi"):
|
||||||
_final_validate({})
|
_final_validate({})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("clock_speed", ["26.67MHz", "72MHz"])
|
||||||
|
def test_ch390_accepts_clock_speed_up_to_the_datasheet_maximum(
|
||||||
|
set_core_config: SetCoreConfigCallable, clock_speed: str
|
||||||
|
) -> None:
|
||||||
|
"""CH390 SCK is rated to 72MHz, so the schema must accept the whole range."""
|
||||||
|
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 = "ch390-test"
|
||||||
|
config = CONFIG_SCHEMA({**_CH390_CONFIG, CONF_CLOCK_SPEED: clock_speed})
|
||||||
|
assert config[CONF_CLOCK_SPEED] == cv.frequency(clock_speed)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ch390_rejects_clock_speed_above_the_datasheet_maximum(
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""The shared 80MHz ceiling is out of spec for this part."""
|
||||||
|
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 = "ch390-test"
|
||||||
|
with pytest.raises(Invalid, match="value must be at most 72000000"):
|
||||||
|
CONFIG_SCHEMA({**_CH390_CONFIG, CONF_CLOCK_SPEED: "80MHz"})
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
ethernet:
|
||||||
|
type: CH390
|
||||||
|
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 @@
|
|||||||
|
<<: !include common-ch390.yaml
|
||||||
Reference in New Issue
Block a user