先给结论:这不像 DHCP 配置问题,更像冷上电时 LAN8720A 没有进入一个稳定的 RMII 接收状态 。你已经能看到 Link Up,并且电脑能抓到 ESP32 发出的 DHCP Discover,说明 MDIO、PHY 链路、TX 方向大概率是通的 ;但 ESP32 没拿到 IP,重点怀疑 RX 方向、RMII REF_CLK、PHY 上电/复位/strap 锁存、排线信号完整性。热插拔后恢复,本质上等价于给 LAN8720A 做了一次更"干净"的掉电重启和重新自协商。
为什么"拉低 100 µs 再拉高"不一定有效
LAN8720A 的要求不是只有 nRST 低电平 ≥100 µs。数据手册还要求:外部电源达到 80% 后,到 nRST 释放至少要 25 ms;strap 引脚必须在 nRST 释放前后满足建立/保持时间;nRST 释放沿还要单调。并且硬件复位期间,XTAL1/CLKIN 需要有时钟。也就是说,如果 50 MHz REF_CLK 此时还没稳定,或者 PHY 3.3 V 刚起来就释放复位,100 µs reset 仍然可能无效。
ESP32-P4 这里还多一个坑:ESP-IDF 明确要求 RMII 的 REF_CLK 在访问 MAC/PHY 时保持稳定;P4 的 RMII 时钟若用 EMAC_CLK_EXT_IN,输入管脚只能选 GPIO32/GPIO44/GPIO50;若用 EMAC_CLK_OUT,输出只能选 GPIO23/GPIO39,并且还必须把输出时钟从外部回环到 P4 的 RMII 时钟输入脚。
所以如果你的 50 MHz 是 ESP32-P4 输出给 LAN8720A,而你在"初始化以太网栈前"就手动复位 PHY,那个时刻 P4 的 RMII clock 很可能还没输出,LAN8720A 复位条件并不满足。
我建议按这个优先级改
第一,先把 PHY 上电复位做成硬件确定的。
不要只靠软件延时。给 LAN8720A 的 nRST 加外部下拉、RC 或 reset supervisor,让它在 PHY 3.3 V 上升期间一定保持低电平,等 3.3 V 稳定后至少 25 ms 再释放。若 PHY 是通过排线供电,建议加一个受控 load switch/MOS 管,软件可以真正断电 100 ms 再上电。LAN8720A 数据手册还特别说明:输入信号不应在器件上电前被驱动为高电平;而排线连接时,ESP32 的 RMII/MDC/MDIO/RESET 线很容易通过 IO 保护结构对 PHY 形成"半上电/背供电"。这类问题单纯拉 nRST 经常解决不了。
第二,确认你的 REF_CLK 拓扑。
如果 LAN8720A 板上有独立 50 MHz 振荡器,P4 应配置为 EMAC_CLK_EXT_IN,并把 50 MHz 接到 GPIO32/GPIO44/GPIO50 之一。如果 LAN8720A 用 25 MHz 晶振并从 nINT/REFCLKO 输出 50 MHz,nINTSEL 必须通过 strap 选到 REFCLKO 模式;同时这根 50 MHz 不建议走长排线。如果是 P4 输出 50 MHz,则必须同时送到 LAN8720A CLKIN,并外部回环到 P4 的 RMII_CLK 输入脚。REF_CLK 是整个 RMII 的采样基准,Espressif 也要求这根线越短越好并保证信号完整性。
第三,检查 LAN8720A 的 strap。
LAN8720A 的 MODE[2:0]、PHYAD0/RXER、REGOFF/LED1、nINTSEL/LED2 都是复用 strap,strap 会在 POR 或 nRST 释放时锁存;如果这些脚还连着 LED、ESP32 IO、排线负载,不能只依赖内部上下拉,要用 4.7 kΩ~10 kΩ 的外部电阻把上电瞬间的状态钉死。若冷启动和热插拔后的寄存器不同,基本就是 strap/复位时序问题。
第四,排线连接 RMII 风险很高。
RMII 是 50 MHz 单端同步接口,REF_CLK、CRS_DV、RXD0/1、TXD0/1、TX_EN 都不适合长排线飞线。TX 能发出 DHCP Discover,不代表 RX 方向采样可靠。建议至少做到:排线尽量短;REF_CLK 旁边有 GND;每几根信号夹一根 GND;P4 端或 PHY 端加 22 Ω~33 Ω 串阻做边沿阻尼;PHY 板本地 3.3 V 去耦充足;最终产品建议同板 PCB 走线。
软件上这样改,不要只改 100 µs
ESP-IDF 的 PHY 配置里本来就有硬复位拉低时间和硬复位后等待时间字段。你可以先把它们放大,排除默认时序过短的问题。
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
phy_config.phy_addr = -1; // 调试阶段先自动探测,确认 strap 后再固定 0 或 1
phy_config.reset_gpio_num = PHY_RST_GPIO; // 必须是真接到 LAN8720A nRST 的 GPIO
phy_config.hw_reset_assert_time_us = 10000; // 10 ms,远大于 100 us
phy_config.post_hw_reset_delay_ms = 100; // 复位释放后等待 PHY/REF_CLK 稳定
phy_config.reset_timeout_ms = 1000;
phy_config.autonego_timeout_ms = 5000;
// IDF v5+ 用 esp_eth_phy_new_lan87xx()
// 旧版本可能是 esp_eth_phy_new_lan8720()
esp_eth_phy_t *phy = esp_eth_phy_new_lan87xx(&phy_config);
但注意:如果 50 MHz 来自 ESP32-P4 的 EMAC_CLK_OUT,不要在 MAC 时钟输出前释放 LAN8720A 复位。这种情况下,手动"以太网栈初始化前 reset PHY"反而可能不满足 LAN8720A 的 reset 条件。更稳的方案是使用独立 50 MHz 振荡器同时送 PHY 和 P4,或者确保 P4 RMII clock 已经输出后再释放 PHY。
如果你要做一个和"热插拔 PHY 板"等效的软件补救,建议不是只 reset,而是受控 power-cycle PHY:
static void phy_power_cycle(void)
{
gpio_set_direction(PHY_RST_GPIO, GPIO_MODE_OUTPUT);
gpio_set_level(PHY_RST_GPIO, 0); // 先压住复位
gpio_set_direction(PHY_PWR_EN_GPIO, GPIO_MODE_OUTPUT);
gpio_set_level(PHY_PWR_EN_GPIO, 0); // 关闭 PHY 3.3V
vTaskDelay(pdMS_TO_TICKS(100)); // 放电,避免半上电
gpio_set_level(PHY_PWR_EN_GPIO, 1); // 打开 PHY 3.3V
vTaskDelay(pdMS_TO_TICKS(50)); // 等电源稳定,≥25ms 留裕量
// 这里必须保证 PHY 所需的 25/50MHz 时钟已经存在
gpio_set_level(PHY_RST_GPIO, 1); // 释放复位
vTaskDelay(pdMS_TO_TICKS(100)); // 等 PHY strap/clock/autoneg 状态稳定
}
启动后如果 10~15 秒没有 IP_EVENT_ETH_GOT_IP,可以临时做:
esp_eth_stop(eth_handle);
phy_power_cycle();
esp_eth_start(eth_handle);
这不是根治,但它能验证"受控 PHY 掉电重启"是否等价于你现在的热插拔现象。
最快定位:冷启动失败和热插拔成功后各 dump 一次 PHY 寄存器
ESP-IDF 支持通过 ETH_CMD_READ_PHY_REG 读 PHY 寄存器。
#include "esp_eth_com.h"
static void dump_phy_regs(esp_eth_handle_t eth_handle, const char *stage)
{
int phy_addr = -1;
esp_eth_ioctl(eth_handle, ETH_CMD_G_PHY_ADDR, &phy_addr);
ESP_LOGI("PHY", "[%s] phy_addr=%d", stage, phy_addr);
const uint32_t regs[] = {0, 1, 2, 3, 4, 5, 6, 17, 18, 26, 27, 31};
for (int i = 0; i < sizeof(regs) / sizeof(regs[0]); i++) {
uint32_t val = 0;
esp_eth_phy_reg_rw_data_t r = {
.reg_addr = regs[i],
.reg_value_p = &val,
};
if (esp_eth_ioctl(eth_handle, ETH_CMD_READ_PHY_REG, &r) == ESP_OK) {
ESP_LOGI("PHY", "[%s] R%02lu = 0x%04lx",
stage, (unsigned long)regs[i], (unsigned long)val);
} else {
ESP_LOGW("PHY", "[%s] R%02lu read failed",
stage, (unsigned long)regs[i]);
}
}
}
重点看:
R2/R3:PHY ID 是否稳定,不应是0x0000或0xffff。R0:是否误进 Power Down、Isolate、Loopback。R1:Link、Auto-neg 是否完成。R18:MODE[2:0]和PHYAD,冷启动失败与热插拔成功是否不同。R31:AutoDone、速率/双工状态。
判断规则很直接:如果冷启动和热插拔后的寄存器不同,是 strap/复位/上电问题;如果寄存器几乎相同但冷启动收不到 DHCP Offer,是 RMII RX/REF_CLK/排线信号完整性问题