[ethernet] Generic and YT8531 PHY over RGMII (gigabit) for ESP32-S31 (#17277)

This commit is contained in:
Jonathan Swoboda
2026-06-29 14:13:11 -04:00
committed by GitHub
parent b8690c8e31
commit 136e343988
4 changed files with 177 additions and 4 deletions
+55
View File
@@ -126,6 +126,8 @@ ETHERNET_TYPES = {
"ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60,
"W6100": EthernetType.ETHERNET_TYPE_W6100,
"W6300": EthernetType.ETHERNET_TYPE_W6300,
"GENERIC": EthernetType.ETHERNET_TYPE_GENERIC,
"YT8531": EthernetType.ETHERNET_TYPE_YT8531,
}
# PHY types that need compile-time defines for conditional compilation
@@ -145,6 +147,8 @@ _PHY_TYPE_TO_DEFINE = {
"ENC28J60": "USE_ETHERNET_ENC28J60",
"W6100": "USE_ETHERNET_W6100",
"W6300": "USE_ETHERNET_W6300",
"GENERIC": "USE_ETHERNET_GENERIC",
"YT8531": "USE_ETHERNET_YT8531",
}
@@ -309,6 +313,24 @@ def _validate(config):
f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]."
)
elif config[CONF_TYPE] in ("GENERIC", "YT8531"):
from esphome.components.esp32 import (
VARIANT_ESP32S31,
get_esp32_variant,
idf_version,
)
eth_type = config[CONF_TYPE]
variant = get_esp32_variant()
if variant != VARIANT_ESP32S31:
raise cv.Invalid(
f"The '{eth_type}' (RGMII) PHY is only supported on gigabit-capable "
f"variants (ESP32-S31), not {variant}"
)
if idf_version() < cv.Version(6, 0, 0):
raise cv.Invalid(
f"The '{eth_type}' (RGMII) PHY requires ESP-IDF 6.0 or newer."
)
elif config[CONF_TYPE] != "OPENETH":
from esphome.components.esp32 import (
VARIANT_ESP32,
@@ -392,6 +414,23 @@ RMII_SCHEMA = cv.All(
cv.only_on([Platform.ESP32]),
)
# Generic IEEE 802.3 PHY over the internal EMAC RGMII interface (e.g. ESP32-S31).
# RGMII data pins come from the IDF per-target default config.
GENERIC_SCHEMA = cv.All(
BASE_SCHEMA.extend(
cv.Schema(
{
cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31),
cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA),
}
)
),
cv.only_on([Platform.ESP32]),
)
SPI_SCHEMA = cv.All(
BASE_SCHEMA.extend(
cv.Schema(
@@ -442,6 +481,8 @@ CONFIG_SCHEMA = cv.All(
"W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"LAN8670": RMII_SCHEMA,
"GENERIC": GENERIC_SCHEMA,
"YT8531": GENERIC_SCHEMA,
},
upper=True,
),
@@ -571,6 +612,20 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
elif config[CONF_TYPE] == "OPENETH":
cg.add_define("USE_ETHERNET_OPENETH")
add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True)
elif config[CONF_TYPE] in ("GENERIC", "YT8531"):
# RGMII data pins come from the IDF default config; set MDC/MDIO + PHY addr.
cg.add(var.set_phy_addr(config[CONF_PHY_ADDR]))
cg.add(var.set_mdc_pin(config[CONF_MDC_PIN]))
cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN]))
if CONF_POWER_PIN in config:
cg.add(var.set_power_pin(config[CONF_POWER_PIN]))
for register_value in config.get(CONF_PHY_REGISTERS, []):
reg = phy_register(
register_value.get(CONF_ADDRESS),
register_value.get(CONF_VALUE),
register_value.get(CONF_PAGE_ID),
)
cg.add(var.add_phy_register(reg))
else:
cg.add(var.set_phy_addr(config[CONF_PHY_ADDR]))
cg.add(var.set_mdc_pin(config[CONF_MDC_PIN]))
@@ -86,6 +86,8 @@ enum EthernetType : uint8_t {
ETHERNET_TYPE_ENC28J60,
ETHERNET_TYPE_W6100,
ETHERNET_TYPE_W6300,
ETHERNET_TYPE_GENERIC,
ETHERNET_TYPE_YT8531,
};
struct ManualIP {
@@ -229,6 +231,11 @@ class EthernetComponent final : public Component {
#ifdef USE_ETHERNET_KSZ8081
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);
#endif
#ifdef USE_ETHERNET_YT8531
/// @brief Apply YT8531-specific config: re-enable auto-negotiation (disabled on
/// reset) and set the RGMII Tx/Rx clock delays needed for reliable data sampling.
void yt8531_phy_init_();
#endif
/// @brief Set arbitratry PHY registers from config.
void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data);
@@ -254,9 +254,14 @@ void EthernetComponent::ethernet_lazy_init_() {
esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_;
esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_;
#endif
esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_;
esp32_emac_config.clock_config.rmii.clock_gpio =
static_cast<decltype(esp32_emac_config.clock_config.rmii.clock_gpio)>(this->clk_pin_);
// The RGMII types (GENERIC, YT8531) use the RGMII interface and default GPIO map from
// eth_esp32_emac_default_config(); writing the RMII clock config would clobber that
// union, so skip the RMII clock override for them.
if (this->type_ != ETHERNET_TYPE_GENERIC && this->type_ != ETHERNET_TYPE_YT8531) {
esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_;
esp32_emac_config.clock_config.rmii.clock_gpio =
static_cast<decltype(esp32_emac_config.clock_config.rmii.clock_gpio)>(this->clk_pin_);
}
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
#endif
@@ -319,6 +324,20 @@ void EthernetComponent::ethernet_lazy_init_() {
break;
}
#endif
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
// GENERIC and YT8531 both use the built-in generic 802.3 PHY driver; YT8531 gets
// extra chip-specific tuning applied later in ethernet_lazy_init_().
#ifdef USE_ETHERNET_GENERIC
case ETHERNET_TYPE_GENERIC:
#endif
#ifdef USE_ETHERNET_YT8531
case ETHERNET_TYPE_YT8531:
#endif
#if defined(USE_ETHERNET_GENERIC) || defined(USE_ETHERNET_YT8531)
this->phy_ = esp_eth_phy_new_generic(&phy_config);
break;
#endif
#endif
#endif
#ifdef USE_ETHERNET_SPI
#if defined(USE_ETHERNET_W5500)
@@ -363,7 +382,30 @@ void EthernetComponent::ethernet_lazy_init_() {
for (const auto &phy_register : this->phy_registers_) {
this->write_phy_register_(mac, phy_register);
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
#ifdef USE_ETHERNET_GENERIC
// The generic 802.3 PHY driver only resets the PHY in its init; it never enables
// auto-negotiation. A PHY that resets into a forced-speed mode (BMCR auto-nego bit
// clear) therefore stays there, and esp_eth_start() skips negotiation because the
// driver cached auto_nego_en=false at install time. Force auto-negotiation on here
// (which also updates that cached state) so esp_eth_start() restarts a proper
// negotiation. (YT8531 does this as part of its own chip-specific init below.)
if (this->type_ == ETHERNET_TYPE_GENERIC) {
bool autoneg_enable = true;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_AUTONEGO, &autoneg_enable);
ESPHL_ERROR_CHECK(err, "Enable auto-negotiation failed");
}
#endif
#ifdef USE_ETHERNET_YT8531
if (this->type_ == ETHERNET_TYPE_YT8531) {
this->yt8531_phy_init_();
if (this->is_failed())
return;
}
#endif
#endif // ESP_IDF_VERSION >= 6.0.0
#endif // !USE_ETHERNET_SPI
// use ESP internal eth mac
uint8_t mac_addr[6];
@@ -486,6 +528,16 @@ void EthernetComponent::dump_config() {
eth_type = "LAN8670";
break;
#endif
#ifdef USE_ETHERNET_GENERIC
case ETHERNET_TYPE_GENERIC:
eth_type = "Generic (RGMII)";
break;
#endif
#ifdef USE_ETHERNET_YT8531
case ETHERNET_TYPE_YT8531:
eth_type = "YT8531 (RGMII)";
break;
#endif
default:
eth_type = "Unknown";
@@ -782,6 +834,19 @@ void EthernetComponent::dump_connect_params_() {
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
uint16_t link_speed = 10;
switch (this->get_link_speed()) {
case ETH_SPEED_100M:
link_speed = 100;
break;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
case ETH_SPEED_1000M:
link_speed = 1000;
break;
#endif
default:
break;
}
ESP_LOGCONFIG(TAG,
" IP Address: %s\n"
" Hostname: '%s'\n"
@@ -796,7 +861,7 @@ void EthernetComponent::dump_connect_params_() {
network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf),
network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf),
this->get_eth_mac_address_pretty_into_buffer(mac_buf),
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10);
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), link_speed);
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
@@ -958,6 +1023,50 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi
#endif
}
#ifdef USE_ETHERNET_YT8531
void EthernetComponent::yt8531_phy_init_() {
esp_err_t err;
// The YT8531 disables auto-negotiation on hardware reset (undocumented behavior), and the
// generic 802.3 driver only resets the PHY, so re-enable it (this also updates the driver's
// cached auto-nego state used by esp_eth_start()).
bool autoneg_enable = true;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_AUTONEGO, &autoneg_enable);
ESPHL_ERROR_CHECK(err, "YT8531 enable auto-negotiation failed");
// RGMII needs ~2 ns Tx and Rx clock delays for reliable data sampling. These are set through
// the YT8531 extended-register interface: write the ext-register address to 0x1E, then
// read/modify/write its value via 0x1F.
esp_eth_phy_reg_rw_data_t phy_reg;
uint32_t reg_val;
phy_reg.reg_value_p = &reg_val;
// RX ~2 ns coarse delay: EXT_CHIP_CONFIG (0xA001), set rxc_dly_en (bit 8).
reg_val = 0xA001;
phy_reg.reg_addr = 0x1E;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_WRITE_PHY_REG, &phy_reg);
ESPHL_ERROR_CHECK(err, "YT8531 select Chip_Config failed");
phy_reg.reg_addr = 0x1F;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_READ_PHY_REG, &phy_reg);
ESPHL_ERROR_CHECK(err, "YT8531 read Chip_Config failed");
reg_val |= (1U << 8);
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_WRITE_PHY_REG, &phy_reg);
ESPHL_ERROR_CHECK(err, "YT8531 write Chip_Config failed");
// TX ~2 ns delay: EXT_RGMII_CONFIG1 (0xA003), tx_delay_sel[3:0] and tx_delay_sel_fe[7:4] = 13.
reg_val = 0xA003;
phy_reg.reg_addr = 0x1E;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_WRITE_PHY_REG, &phy_reg);
ESPHL_ERROR_CHECK(err, "YT8531 select RGMII_Config1 failed");
phy_reg.reg_addr = 0x1F;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_READ_PHY_REG, &phy_reg);
ESPHL_ERROR_CHECK(err, "YT8531 read RGMII_Config1 failed");
reg_val = (reg_val & ~0x00FFU) | (13U << 4) | (13U << 0);
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_WRITE_PHY_REG, &phy_reg);
ESPHL_ERROR_CHECK(err, "YT8531 write RGMII_Config1 failed");
}
#endif
#endif
} // namespace esphome::ethernet
+2
View File
@@ -327,6 +327,8 @@
#define USE_ETHERNET_JL1101
#define USE_ETHERNET_KSZ8081
#define USE_ETHERNET_LAN8670
#define USE_ETHERNET_GENERIC
#define USE_ETHERNET_YT8531
#define USE_ETHERNET_SPI
#define USE_ETHERNET_SPI_POLLING_SUPPORT
#define USE_ETHERNET_OPENETH