手头这块板子的主控是 STM32F407,PHY 选了 YT8512C,串口和 GPIO 那些基础功能前几篇已经打完底了,这篇正好是系列第九篇,专门把 STM32CubeMX 配 LWIP 这条线完整捋一遍,重点放在 YT8512C 的适配和联调上。做嵌入式以太网的都清楚,CubeMX 生成一个能编译通过的 LWIP 工程不难,真正难的是 PHY 驱动和硬件时序对上,网上一搜全是 LAN8720 的教程,YT8512C 的实战记录少得可怜,很多坑只能自己踩。这篇适合正在做以太网功能、手里刚好是 YT8512C 这颗 PHY、或者想对比 LAN8720 方案差异的开发者参考,内容从 CubeMX 图形化配置开始,一直讲到代码生成后的驱动润色和实测排查,全程按实际调板子的顺序来写。
1. YT8512C 这颗 PHY 到底有什么不一样
1.1 选型时为什么看中它
先说结论:YT8512C 是一颗 10/100M 自适应以太网 PHY,支持 MII 和 RMII 两种接口模式,内部集成了一些常用功能,在同价位里性价比很能打。很多国产设备主板上能看到它的身影,尤其是对成本敏感、又要求出货稳定的场景。
它跟 LAN8720A 的常见说法是引脚兼容,layout 上可以直接套用 LAN8720 的参考设计,这对做硬件的人来说非常省事。但"兼容引脚"不等于"软件也兼容",寄存器定义大方向是标准的 IEEE 802.3 寄存器组,可具体到状态位、PHY ID、上电默认行为,还是值得单独验证。我第一次调的时候直接复制 LAN8720 的驱动,结果发现链路状态读取的时机和 ID 寄存器的值都对不上,后面才静下心看 datasheet 慢慢改。
要特别提醒一句,YT8512C 跟 LAN8720A 的差异主要体现在供应商私有寄存器和部分上电配置引脚上。比如有的版本 PHY 地址的默认上下拉策略不一样,有的在 RMII 模式下对 REF_CLK 的驱动能力要求不同。这些都是原理图阶段就要定下来的事,等到代码跑不起来再回头改 layout 就晚了。
1.2 硬件设计上最容易埋雷的三个点
第一是 PHY 地址。YT8512C 的 PHY 地址一般由一个或多个 PHYAD 引脚的电平在复位瞬间锁存,常见板子会把地址设成 0x00,也有的设成 0x01,千万别想当然,必须对着原理图确认。CubeMX 里默认填的 PHY Address 是 0,如果跟实际不一致,HAL_ETH_Init 会直接超时,这是新手最容易碰到的第一个坎。
第二是 RMII 参考时钟的来路。RMII 模式需要 50MHz 的 REF_CLK,这个时钟可以由 PHY 从 25MHz 晶振倍频后输出给 STM32,也可以由外部 50MHz 有源晶振直接同时供给两边。YT8512C 比较常见的做法是核电路放 25MHz 晶振,PHY 自己产生 50MHz REF_CLK 送到 STM32 的 ETH_RMII_REF_CLK 引脚。这里最怕的是两边时钟不同源、相位抖动大,导致收发数据偶发错位,现象非常难查。
第三是复位时序。YT8512C 虽然内部有上电复位,但 datasheet 对上电稳定时间是有要求的。如果复位脚由 MCU 控制,至少要保证拉低脉冲宽度够、释放后等足够久再初始化 MAC;如果复位脚直接接 RC 或电源,那上电后主控侧也要延时再访问 MDIO,不然读 PHY 寄存器会读回来一堆 0xFFFF。
2. STM32CubeMX 图形化配置全流程
2.1 时钟树与引脚规划
CubeMX 里新建工程后,第一步永远是把时钟树理清楚。STM32F407 跑以太网,一般 SYSCLK 直接上 168MHz,HSE 外部晶振用 25MHz,PLL 配置成 168MHz 输出。这里有个关键点:RMII 的 50MHz REF_CLK 跟主频是两回事,不要指望从 PLL 里自动变出 50MHz 来,它来自 PHY 侧或专门的时钟源。
如果板子上接了 25MHz 晶振给 YT8512C,那么 STM32 侧不用输出参考时钟,PA1 作为 ETH_RMII_REF_CLK 输入模式即可。如果你的板子是 STM32 用 MCO2 引脚输出 50MHz 给 PHY 的方案,那 CubeMX 里要把 PC9 复用为 MCO2 并正确配置分频,两种方式的 CubeMX 操作不一样,但都要确保最终 PHY 和 MAC 的参考时钟频率准确、相位关系满足 RMII 规范。
RMII 模式下,STM32F407 的 ETH 引脚基本是固定的,没法随便挪,CubeMX 会自动分配好,建议直接让工具自动安排,别手动去跟其他外设抢引脚。默认 RMII 这 7 个引脚分别是:PA1 的 REF_CLK、PA7 的 CRS_DV、PC4/PC5 的 RXD0/RXD1、PB11 的 TX_EN、PB12/PB13 的 TXD0/TXD1,再加上管理接口 PA2 的 MDIO 和 PC1 的 MDC。这些引脚一旦被其他外设占用,生成代码后会冲突,编译报错还算好,最怕是编译通过但功能互相干扰。
2.2 ETH 外设与 RMII 模式选择
在 CubeMX 左侧 Connectivity 里找到 ETH,勾选 Activated,模式选择 RMII。这里有个容易被忽略的细节:PHY Address 一栏要马上改成原理图里实际的 PHY 地址,别留着默认值。
新版 CubeMX 在 ETH 配置界面里可能会让你选 PHY 器件型号,下拉列表里有 LAN8742A、DP83848 这类常见 PHY。YT8512C 通常不在列表里,这时候不用慌,选 User 或者选一个脚位兼容的型号都行,代码生成后反正要手动改 PHY 驱动,这里选什么只影响自动生成的 BSP 代码风格。
配置完 ETH 后,CubeMX 会自动把 RMII 需要的引脚标成绿色,同时可能弹出提示说某个引脚被复用冲突,逐个确认即可。记住一个原则:RMII 引脚只做以太网,不要跟 UART、调试口去抢。
2.3 LWIP 中间件参数怎么填
中间件里找到 LWIP,勾选 Enabled,版本一般选 2.x。IP 地址那一栏默认是 192.168.1.10,子网掩码 255.255.255.0,网关 192.168.1.1,先用这套默认值打通链路再改。如果打算用 DHCP,把 LWIP_DHCP 打开,但要确认你的网络环境里有 DHCP 服务器,否则板子会一直处于获取 IP 的状态,看起来像"网口不通",实际上物理链路是好的。
内存参数默认值跑一个 ping 完全够用,MEM_SIZE 默认 1600,PBUF 池大小默认 8 个,新手阶段不用动。等后面做 TCP 大数据收发或者多个连接并发时再回来调,我后面专门有一节讲这个。
生成代码之前,还有一件很重要的事:在 Project Manager 里把堆和栈调大。CubeMX 生成的项目默认堆比较小,跑 LWIP 容易在 malloc 的时候死掉。我的习惯是 Main Heap 至少调到 0x600,Main Stack 调到 0x800 以上。这一步不做,后面程序跑着跑着突然 HardFault,排查起来非常头疼。
3. 代码生成后的三处关键润色
3.1 PHY 驱动适配是重头戏
CubeMX 生成代码后,以太网相关的底层主要在 ethernetif.c 里,而 PHY 的 BSP 部分在 stm32f4xx_hal_eth.c 或者单独的中年件代码里,具体文件名跟 CubeMX 版本有关。关键是要找到这几个函数或者宏:PHY 地址宏、PHY 初始化函数、PHY 链路状态检测函数。
YT8512C 能不能直接用 LAN8720 的驱动,答案是一半能。标准寄存器组(寄存器 0 控制寄存器、寄存器 1 状态寄存器、寄存器 2/3 PHY ID、寄存器 4/5 协商通告和协商伙伴)这些地址定义是 IEEE 802.3 标准统一的,所以如果 CubeMX 里选的是 LAN8720,生成代码后很多时候能直接跑通基本功能,链路状态位也在寄存器 1 的 bit0。但正式项目我不建议直接这么干,原因有两个:
一是 PHY ID 完全不同。LAN8720A 的 ID2 读出来是一串特定值,YT8512C 读出来是另一串,如果代码里有根据 ID 做分支的逻辑,或者调试时靠 ID 确认芯片型号,就必须改成对的值。
二是部分 PHY 的协商状态寄存器位可能有细微差别。稳妥做法是写一个读寄存器的调试函数,把寄存器 2、3、4、5 的值都打印出来,对照 datasheet 确认 bit 含义,再写适配代码。我在实际项目里就是这么干的,花半小时读寄存器,比在错误的方向上调一整天值得多。
下面这段是典型的适配检查逻辑,放在 ethernetif.c 的初始化前:
uint32_t phy_id1, phy_id2; phy_id1 = HAL_ETH_ReadPHYRegister(&heth, ETH_PHY_ADDRESS, 2); phy_id2 = HAL_ETH_ReadPHYRegister(&heth, ETH_PHY_ADDRESS, 3); printf("PHY ID1: 0x%04X\r\n", phy_id1); printf("PHY ID2: 0x%04X\r\n", phy_id2); // 如果读回来全是 0xFFFF,说明 PHY 地址不对或者 MDIO 不通 // 如果 ID 值符合 YT8512C 手册,说明总线通信正常,可以继续初始化正常通信时,这个 ID 打印是调试路上最重要的一个路标。它能一次性区分出硬件问题、地址问题和驱动问题,比任何花哨的调试技巧都实用。
3.2 复位脚和初始化顺序必须自己把关
CubeMX 生成的代码不会自动帮你处理 PHY 复位时序,除非你在 CubeMX 里把复位引脚配好并自己写逻辑。绝大多数板子的 PHY 复位脚接在某个 GPIO 上,所以 ethernetif.c 的 low_level_init 里,在 HAL_ETH_Init 之前要加一段复位序列。
// 假定复位脚是 PB0,低电平复位 #define PHY_RESET_PORT GPIOB #define PHY_RESET_PIN GPIO_PIN_0 HAL_GPIO_WritePin(PHY_RESET_PORT, PHY_RESET_PIN, GPIO_PIN_RESET); HAL_Delay(20); // 拉低至少 10ms~20ms,确保内部完全复位 HAL_GPIO_WritePin(PHY_RESET_PORT, PHY_RESET_PIN, GPIO_PIN_SET); HAL_Delay(5); // 释放后等待 PHY 内部初始化完成这个延时不是玄学。YT8512C 内部有一堆模拟电路要稳定,寄存器要载入默认值,太早访问 MDIO 会失败。如果复位脚硬件上直接拉死或接了 RC,那就靠 HAL_Delay(100) 硬等,保证 PHY 先于 MAC 就绪。
还有一个容易忽略的点:如果 LWIP 初始化里把 PHY 的自动协商中断逻辑做成了阻塞等待,而 PHY 因为线缆类型或对端设备问题协商失败,程序可能会卡在等待超时上。我建议把自动协商超时时间设长一点,或者改成非阻塞轮询,这样系统启动过程更健壮。
3.3 MAC 地址和 IP 别用默认值裸奔
生成的 low_level_init 里会给 heth.Init.MACAddr 赋一组默认值,有些默认值看起来像模像样,实际可能是全 0 或者广播地址,这在网络上会引发奇怪的问题。稳妥做法是显式写一组自定义 MAC,用局部管理地址段,比如 02:00:00:xx:xx:xx,第一位第二个 bit 置 1 表示本地管理,这样不会跟真实厂商的 MAC 冲突。
IP 配置方面,LWIP 的静态配置在 lwipopts.h 或工程的头文件里,如果关掉 DHCP 用的是固定 IP,务必确认这个 IP 跟电脑在同一网段,否则 ping 不通时你会在物理层排查半天,最后发现是 IP 网段不一致。
DHCP 开启时,注意 LWIP 获取 IP 是异步过程,以太网链接建立后还要等 DHCP 交互完成。很多人看到网口灯亮了就开始 ping,而这会儿板子还没拿到 IP,自然 ping 不通,误判成网络栈有问题。实际等个几秒再试就好了。
4. 编译烧录与链路实测
4.1 上电后的第一件事不是 Ping
程序烧进去,插上网线,先别急着打开命令行 ping。我的习惯是:先通过串口把 PHY 寄存器状态打印出来,看看链路是否已经协商成功。重点看寄存器 1 的 bit0 链路状态位,以及寄存器 4/5 协商出来的速度双工结果。
uint32_t status = HAL_ETH_ReadPHYRegister(&heth, ETH_PHY_ADDRESS, 1); if (status & 0x0004) { printf("Link is UP\r\n"); } else { printf("Link is DOWN\r\n"); }链路状态位在不同 PHY 上有时不在 bit0,而在 bit2,这就是为什么我前面强调一定要读寄存器而不是想当然。YT8512C 的 datasheet 里寄存器 1 的位定义是标准的,bit2 是有效链路状态,bit5 是自动协商完成标志,按标准取就行,但打印出来确认一下更放心。
如果串口打印显示链路 UP,电脑端的网络连接也会弹出"已连接",这时候再 ping 才有意义。ping 之前把电脑网卡 IP 改成和板子同一网段,比如板子固定 192.168.1.10,电脑就设 192.168.1.50,注意关掉电脑的防火墙,这个坑太常见了。
4.2 用 Wireshark 判断问题出在第几层
ping 不通时,很多人会陷入盲调。我强烈建议此时打开电脑上的 Wireshark,插着网线看板子上电后的流量。如果能看到 ARP 请求或广播帧,说明 PHY 链路和 MAC 收发基本正常,问题在协议栈上层;如果什么都看不到,问题大概率在 PHY 配置、RMII 时钟或 MAC 初始化,这时候再回去查寄存器不迟。
这个观察能节省大量时间。举个实际例子,我之前调一块板子,现象是链路灯亮但 ping 不通,Wireshark 里完全收不到板子的任何帧。回头查 RMII REF_CLK,发现 PHY 的 50MHz 输出波形是有了,但幅度不够,STM32 侧采样不稳定,导致 CRS_DV 信号乱跳。这不是软件能救的问题,但如果不是先用 Wireshark 把问题锁定在物理层,我可能还会在 LWIP 配置里瞎折腾很久。
4.3 寄存器级验证是硬件联调的王牌
串口打印 PHY 寄存器是联调阶段最直接的手段,把这几条常用的读寄存器逻辑整理成函数,调板子的时候会用得非常频繁:
| 寄存器地址 | 名称 | 联调时看什么 |
|---|---|---|
| 0 | 控制寄存器 | bit9 是否半双工/全双工,bit12 自动协商使能 |
| 1 | 状态寄存器 | bit2 链路状态,bit5 自动协商完成 |
| 2/3 | PHY ID | 确认芯片型号和 MDIO 通信正常 |
| 4 | 协商通告 | 板子广播的速度/双工能力 |
| 5 | 协商伙伴 | 对端设备的能力,判断最终协商结果 |
把这几个寄存器打印出来,链路状态、协商结果、问题层次一目了然。我每次调网络板都会在工程里保留这段调试代码,联调完再注释掉,不去删,因为后续维护现场排查还会用到。
5. 常见问题与排查技巧实录
5.1 HAL_ETH_Init 超时卡死
现象是程序执行到以太网初始化时直接卡住,串口无输出,或者返回 HAL_TIMEOUT。绝大多数情况是 PHY 地址不对。CubeMX 里配的地址和原理图上的 PHYAD 引脚不一致,MDIO 命令根本没人应答。
排查办法很简单:把 HAL_ETH_ReadPHYRegister 的返回值打印出来,读寄存器 2,如果读到 0xFFFF,那基本可以断定地址错了或者 MDIO 引脚配置有问题。先核对 PHYAD 引脚上下拉,再核对 CubeMX 里的 PHY Address,两处统一后问题通常立即消失。
还有一种隐蔽情况:PHY 的复位时序不对,上电后 PHY 还在复位状态,MDIO 自然无应答。这时候量一下复位脚电平,确认它已经释放,并保证释放后等了足够时间再访问。
5.2 链路指示灯亮了但 ping 不通
指示灯亮说明 PHY 协商成功,物理层大概率没问题,问题在 MAC 或协议栈。按优先级检查:
- 用 Wireshark 看板子有没有发出任何帧,没有帧就是 MAC 侧的问题,查 RMII 引脚配置和 MAC 初始化寄存器。
- 看电脑 IP 和板子 IP 是否同网段,检查电脑防火墙是否拦截 ping。
- 确认 LWIP 的 netif 状态:串口打印 netif->ip_addr 和 netif_is_up。有时候 netif 没起来,跟 DHCP 还在协商中表现一模一样。
RMII 信号里 CRS_DV 是最容易出问题的一根线。它是复用信号,既承载载波侦听又在特定时刻兼作数据有效指示,如果 STM32 侧对它的采样时序不对,收包会非常不稳定。遇到收方异常、发方正常的场景,优先用示波器看 CRS_DV 波形是否干净,再查 RXD0/RXD1 的建立保持时间。
5.3 丢包、延迟、速率上不去
基础通信打通之后,你会发现跑 TCP 大包时速率上不去或者偶发丢包,这通常是 LWIP 内存配置跟不上。需要调的重点参数有三处:
| 参数 | 作用 | 经验值参考 |
|---|---|---|
| MEMP_NUM_PBUF | 内存 PBUF 数量 | 从默认值加大到 16~32 |
| PBUF_POOL_SIZE | 接收缓冲区池数量 | TCP 收大包时加大到 16~24 |
| MEM_SIZE | 堆内存大小 | TCP 连接多时从 1600 加大到 4096+ |
改这些参数没有统一标准,要结合实际负载。我的经验是每次只改一个参数,跑一轮压力测试,对比收发速率和丢包率,找到哪个参数是短板再动下一个。一次性全改大,内存可能不够用,反而触发 LWIP 内存不足断言。
另外确认一下 PHY 协商出来的双工模式。如果是半双工,TCP 吞吐会明显受限,尤其在网络有碰撞的环境下,表现就是速度忽高忽低。从寄存器 5 里能看到协商结果是 100M 全双工还是半双工,不正常时换根网线或换个交换机端口再试。
5.4 程序跑着跑着 HardFault
LWIP 跑起来后偶发 HardFault,是内存问题的典型症状。除了前面说的堆栈要加大,还要注意 LWIP 的处理线程栈。CubeMX 生成的 LWIP 默认有一个 tcpip_thread,其线程栈大小其实不小,但如果你在挂着 TCP 连接的同时频繁创建和销毁线程,还是可能爆栈。
还有一个容易被忽略的坑:在中断回调里直接调用 LWIP 的 API。LWIP 的 API 默认线程不安全,中断里调用可能导致链表被破坏,产生非常随机、极难复现的崩溃。正确姿势是中断里只做标志位置位或者直接不处理,把协议栈操作放到主循环或专用线程里。
5.5 换个板子就出问题,大概率是事务性细节
同一个工程,一块板子完全正常,另一块板子链路时好时坏,这种"复制粘贴"出来的问题,绝大多数出在晶振和电容的焊接一致性上。RMII 的 50MHz 参考时钟对质量很敏感,如果 PHY 晶振的负载电容匹配不好,起振不稳定,REF_CLK 上会有间歇性的抖动,导致随机丢包甚至链路反复断开。
遇到这种问题,先用示波器长时间盯着 REF_CLK 波形,看有没有间歇性的幅度跌落或毛刺。如果是晶振问题,软件层面几乎无解,只能从硬件上调整负载电容,或者把 PHY 配置成外部时钟输入模式,用更稳定的有源时钟替代。
写在最后的一点个人经验
这一篇把 STM32CubeMX 配置 LWIP、适配 YT8512C 的完整路径走了一遍,从时钟和引脚规划、ETH 和 LWIP 参数配置,到生成代码后的 PHY 驱动改写、复位时序补充,再到联调阶段用 Wireshark 和寄存器打印缩小问题范围,基本覆盖了实际调板时会碰到的所有关键环节。
我多次调不同 PHY 之后的体会是,以太网调试最大的陷阱不是哪一行代码写错,而是层次混淆。链路灯不亮就去翻 LWIP 配置,LWIP 起不来又回头量时钟,这样来回折腾效率极低。正确的做法是先读 PHY 寄存器确认硬件链路通,再用 Wireshark 确认帧有没有真的到线上,最后才轮到协议栈的事。YT8512C 这颗 PHY 整体用下来中规中矩,只要能确认 PHY 地址和复位时序没问题,后面的体验跟其他主流 PHY 差别不大。
最后再分享一个小技巧:如果你的项目以后可能要兼容多款 PHY,建议在驱动层做一个 PHY 型号 ID 的分支判断,初始化时先读 ID,根据 ID 选择对应的寄存器位定义和初始化序列。我后来复用这套代码换过好几款 PHY,每次只需要加一个分支,不用动上层逻辑,省了不少重复劳动。