ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 HAL库下LAN8720A PHY驱动适配指南:从LAN8742迁移的完整方案

STM32 HAL库下LAN8720A PHY驱动适配指南:从LAN8742迁移的完整方案 去年我把自己一块F407板子从ST官方例程迁移到以太网功能时卡了整整三天。问题说起来很简单板子上用的是国内模组最常见的LAN8720A而HAL库里只带了LAN8742的BSP驱动。打开CubeMX外设配置里PHY型号只有LAN8742可选翻到工程源码Drivers/BSP/Components目录下也只有lan8742.c和lan8742.h。网上一搜很多人丢下一句两个芯片寄存器差不多把PHY地址改了就行可实际照着改完不是PHY初始化超时就是Link Up了却arp不通甚至偶尔能通、重启又挂。这篇文章把我这次从LAN8742 HAL库驱动适配到LAN8720A的全部过程翻出来包括真正起作用的三处代码改动、一条完整的排查链路以及注释里不会写的那几个坑。如果你手里正好是LAN8720A模组又不想放弃ST官方例程和HAL库框架这篇东西应该能帮你少走几天弯路。1. 为什么HAL库里只有LAN8742LAN8720A却要自己动手1.1 两者血缘很近但ST官方只维护了LAN8742STM32官方评估板、NUCLEO开发板上的以太网PHY大部分用的是Microchip原SMSC的LAN8742。所以ST在HAL库的BSP层里只维护了lan8742这一个驱动CubeMX的PHY型号下拉框里也只有它。LAN8720A同样是Microchip/SMS C的百兆以太网PHY两者管脚兼容性很好寄存器组也都遵守IEEE 802.3标准像是寄存器0到寄存器6的定义几乎完全一样。加上LAN8720A模组便宜、外围电路简单国内大量STM32以太网板子都选了它。问题是ST爸爸没给它写BSP所以当你把官方例程往自己板上一搬第一个报错就是找不到PHY芯片、初始化超时。这里要澄清一个认知所谓改驱动不是把LAN8742驱动推倒重写而是搞清楚两个芯片在寄存器、地址、时钟链路上的细微差别然后针对性修改。绝大多数情况下核心改动量小得惊人。1.2 真正需要改的其实只有这么几个点我把两个芯片的关键差异整理成了一张表后面所有操作都围绕这几个点展开对比项LAN8742LAN8720A默认PHY地址常见硬件设计0x010x00PHY ID寄存器2/30x0007C1300x0007C131引脚锁存PHY地址方式PHYAD[2:0]三引脚PHYAD[1:0]两引脚与LED/REGOFF复用RMII参考时钟要求50MHz50MHz寄存器31定义LAN8742专用控制/状态PHY Special Control/Status Register位定义有差异CLKOUT输出可配置可配置常见从25MHz晶振倍频输出50MHz从这张表能看出来真正可能导致驱动跑不起来的硬差异第一是PHY地址第二是ID校验逻辑第三是寄存器31的写入内容。其余像自动协商、Link状态检测这些标准行为两个芯片是通用的lan8742.c里大部分代码可以直接复用。所以下一个问题变成了lan8742.c里到底干了哪些事哪些能复用哪些必须动。2. 先看懂lan8742.c的分工再决定动哪里2.1 HAL库以太网驱动的调用链ST的HAL库以太网工程从上到下的调用链大概是应用程序TCP/UDP业务 ↓ lwIP协议栈 / NetX协议栈 ↓ ethernetif.c网络接口适配层 ↓ stm32f4xx_nucleo_eth.cBSP板级以太网初始化 ↓ lan8742.c / lan8742.hPHY芯片驱动 ↓ HAL_ETH_ReadPHYRegister / HAL_ETH_WritePHYRegisterETH外设MDIO接口如果你拿到的是STM32Cube_FW_F4_Vxxx软件包lan8742驱动位于Drivers/BSP/Components/lan8742/ ├── lan8742.c └── lan8742.h板级BSP文件在Drivers/BSP/STM32F4xx-Nucleo/ └── stm32f4xx_nucleo_eth.c很多人一上来就改lan8742.c其实lan8742.c只负责PHY芯片的逻辑控制而真正把PHY绑定到HAL库ETH_HandleTypeDef、再把MAC配置传给外设的是板级BSP文件。所以改驱动前先分清自己改的是哪一层。2.2 lan8742.c里的三个关键角色打开lan8742.c核心函数不多我按重要性排一下lan8742_Init赋值PHY地址软复位PHY读PHY ID启动自动协商。这是适配过程中最需要关注的一个函数。lan8742_GetLinkState读取寄存器1解析出Link状态、协商速度、双工模式。HAL库的HAL_ETH_ReadPHYRegister在其内部被频繁调用。lan8742_ReadReg / lan8742_WriteReg底层寄存器读写封装本质上就是包了一层HAL的MDIO读写API。lan8742_Init里有一个非常重要的细节它在函数开头会把传入的LAN8742_PHY_ADDR写进驱动内部状态然后所有后续寄存器操作都基于这个地址。所以PHY地址错了后面全是空中楼阁。2.3 一个值得注意的设计MAC层和PHY层是分离的在HAL库以太网框架里MAC层STM32片内的Ethernet MAC和PHY层外部PHY芯片是分开配置的。MAC层由HAL_ETH_Init完成其中包括了EthInitStruct.PhyAddress这个成员它会被写进STM32以太网MAC的PHY地址寄存器作为MDIO访问时的目标地址。PHY层则由lan8742_Init完成它决定驱动内部访问芯片时使用哪个地址。这里就出现了一个经典坑两个地方都要设置PHY地址。ethernetif.c或者板级BSP里传给MAC层的PHY地址和lan8742.c里的PHY地址宏必须保持一致。否则MAC层用地址A去读PHYPHY驱动内部却用地址B结果就是寄存器读出来全是0xFFFF初始化超时。3. 三处改动让LAN8720A在HAL库下正常初始化3.1 第一处PHY地址宏从0x01改成0x00打开lan8742.h找到这一行#define LAN8742_PHY_ADDR ((uint32_t)0x01U)这是ST官方针对LAN8742的默认PHY地址。LAN8720A在绝大多数模组上的默认地址是0x00因为它的PHYAD[1:0]引脚和LED2/REGOFF、LED1/RMII复用在硬件上通常都拉低了。直接改成#define LAN8742_PHY_ADDR ((uint32_t)0x00U)如果你不想彻底放弃对LAN8742的支持可以用条件编译#if defined(USE_LAN8720A) #define LAN8742_PHY_ADDR ((uint32_t)0x00U) #else #define LAN8742_PHY_ADDR ((uint32_t)0x01U) #endif这样在编译选项里定义USE_LAN8720A走的就是LAN8720A的地址不定义则保持原样。这里要特别注意LAN8720A的PHY地址是由硬件引脚决定的。如果你的板子把PHYAD0或PHYAD1拉高了实际地址可能就是0x01、0x02或0x03那就不需要改这个宏。动手前先看原理图不要凭经验一刀切。3.2 第二处BSP初始化结构体里的PhyAddressPHY地址宏改了还不够。板级BSP文件里还有一个PHY地址需要同步改。以NUCLEO-F407的stm32f4xx_nucleo_eth.c为例BSP_ETH_Init函数中会有一个HAL_ETH_InitTypeDef结构体里面有类似这样的初始化heth.Init.PhyAddress LAN8742_PHY_ADDR;如果你比较幸运这个结构体直接用了lan8742.h里的宏那么第一处改动后这里自动同步。但很多版本的例程是硬编码的heth.Init.PhyAddress 1;所以全局搜一下PhyAddress把数值改成和PHY地址宏一致。这一处也是无数人漏掉的我就是其中之一。改完宏之后发现还是初始化超时最后定位到是这里写死了0x01。CubeMX用户还要注意图形界面里Ethernet配置的PHY Address选项它生成代码时同样会写进heth.Init.PhyAddress默认值通常也是1记得改成0。3.3 第三处寄存器31的PHY专用配置要慎写lan8742_Init里有一小段代码会向寄存器31写入一个LAN8742专用的配置值。具体写法在不同HAL版本里略有差异可能是直接lan8742_WriteReg(..., 31, ...)也可能是通过一个初始化数组。问题在于LAN8720A的寄存器31叫PHY Special Control/Status Register位定义和LAN8742不一样。你把LAN8742的配置值原样写进LAN8720A的寄存器31等于在别人的房间里按自己家的开关布局开灯很可能打开一些不该开的功能比如改变CLKOUT输出、误设PHY地址或进入特殊测试模式。我的建议是先查一下你的HAL版本里lan8742_Init到底往寄存器31写了什么。如果它写的数值是固定的而你的LAN8720A时钟方案不依赖寄存器31比如由外部有源晶振提供50MHz直接把这行写入注释掉或者改成写入0x0000然后实测。如果LAN8720A的CLKOUT方案需要设置寄存器31来开启时钟输出那就需要打开LAN8720A数据手册找到寄存器31的位定义按实际需求重新计算这个值。不要直接抄网上别人改好的数因为不同板子的PHY地址锁存方式、时钟来源不一样。3.4 编译烧录后先做一次验证改完这三处后先别急着跑协议栈。烧录后第一步打印PHY ID确认MDIO总线真的通了uint16_t id1 0, id2 0; HAL_ETH_ReadPHYRegister(heth, 2, id1); HAL_ETH_ReadPHYRegister(heth, 3, id2); printf(PHY ID: %04X %04X\r\n, id1, id2);LAN8720A正常读回的结果应该是PHY ID: 0007 C131如果读回全是0xFFFF说明MDIO总线没通优先检查PHY地址、复位引脚和MDC/MDIO引脚配置不要继续往下调。4. 时钟链路才是改完驱动后最容易翻车的隐藏关卡4.1 50MHz参考时钟的两种常见来源PHY地址改对、ID也能读到了Link状态依然起不来这是适配过程中非常常见的第二阶段卡点根源大多数在RMII的50MHz参考时钟。RMII接口要求参考时钟必须是50MHz精度和稳定性都很关键。LAN8720A模组上常见有两种时钟拓扑第一种模组自带25MHz无源晶振PHY内部通过PLL倍频到50MHz从CLKOUT引脚输出给STM32的RMII_REF_CLKF407上是PA1。这种方案最省事也是国内多数LAN8720A模组的默认设计。第二种外部50MHz有源晶振直接给PHY提供参考时钟。这种情况下PHY内部可能不再倍频STM32的RMII_REF_CLK也可以直接从外部时钟取得具体要看板子设计。判断你的板子是哪种方案最直接的办法是量PA1引脚用示波器看有没有稳定的50MHz方波。没有示波器的话至少用万用表确认晶振两端有电压再用逻辑分析仪粗略看频率实在都没有就优先怀疑时钟链路。4.2 STM32的MCO输出方案为什么经常出问题还有一种省钱设计不额外加有源晶振STM32的MCO1PB8输出时钟给PHY或者PHY的CLKOUT回给PA1。这个方案不是不行但它对MCO引脚配置要求很高。如果你的LAN8720A是25MHz晶振CLKOUT倍频输出方式STM32的PA1只需要作为RMII_REF_CLK输入配置成复用模式即可。但如果你的设计是MCO给PHY提供时钟那么MCO1的GPIO速率、时钟源选择、分频系数只要错一个PHY要么不工作要么工作极不稳定。我的个人建议是能不用MCO给PHY供时钟就不用。模组自带晶振的LAN8720A方案已经非常成熟没必要为了省一个晶振引入一堆时序不确定性。官方NUCLEO板用MCO是因为PCB和参考设计都经过验证自己画板子时复制这套方案踩坑概率高得多。4.3 改完驱动后必查的RMII引脚清单时钟没问题PHY ID能读到但网络还是半通就要检查RMII的引脚配置。以STM32F407为例RMII接口涉及PA1RMII_REF_CLKPA2MDIOPA7RMII_CRS_DVPC1RMII_MDCPC4RMII_RXD0PC5RMII_RXD1PG11RMII_TX_ENPG13RMII_TXD0PG14RMII_TXD1两个容易忽略的点第一CubeMX初始化时这些引脚必须全部配置为ETH复用功能少一个HAL_ETH_Init可能不会报错但数据根本收发不了。第二LAN8720A的RXD0/RXD1/CRS_DV引脚在芯片内部有上拉即使MCU侧没接好用万用表量可能也能看到电平容易造成明明接了的错觉。真正测的时候要把PHY的这些引脚和MCU断开后再量电平或者直接观察PHY的Link LED状态。5. 初始化成功但网络不通这套排查顺序帮我省了一周5.1 第一步读PHY ID确认MDIO真的通了遇到初始化成功但没有网络的情况很多人第一反应是查lwIP其实不对。先回到PHY层把该读的寄存器全部读一遍。我自己写了一个简单的寄存器转储逻辑uint16_t regs[32]; for (uint8_t i 0; i 6; i) { HAL_ETH_ReadPHYRegister(heth, i, regs[i]); printf(Reg[%d] 0x%04X\r\n, i, regs[i]); }重点看三个信息寄存器2/3PHY IDLAN8720A应为0x0007C131。寄存器1 bit5自动协商完成位1表示协商完成。寄存器1 bit2Link状态1表示物理链路已建立。如果寄存器2/3读出来的ID和你预期不一致不要继续往下调根因大概率在PHY地址或MDIO时序上。5.2 第二步看自动协商完成位和Link状态ID正确后看寄存器1。如果bit5一直是0说明PHY没有完成自动协商此时检查对端设备交换机、电脑网卡是否真的连上了网线是否正常。如果bit51但bit20说明协商过程完成了但链接没有建立。这种情况优先怀疑RMII时序尤其是CRS_DV引脚。CRS_DV在RMII模式下承载的是载波侦听和接收有效信号信号质量差会导致PHY频繁重协商。如果bit51、bit21但TCP/UDP就是不通那问题可能已经从PHY层转移到了MAC层或协议栈层此时再去查ethernetif.c的low_level_input和low_level_output不要继续在PHY寄存器里打转。5.3 第三步抓住能Link但ping不通的伪故障LAN8720A有一个让我印象深刻的现象Link状态正常、自动协商结果也正常但ping不通。排查了很久最后发现是PHY的TXD1引脚虚焊导致发送数据最高位丢失MAC发出去的数据包全是半个包。这类问题最坑的地方在于PHY的Link状态只反映物理层是否建立连接它不保证数据通路好坏。所以碰到Link Up但数据传输异常不要只盯着协商状态用示波器看TXD0/TXD1/TX_EN这组发送信号再用逻辑分析仪抓一次RMII总线比盲猜高效得多。5.4 第四步复位时序也不能忽略LAN8720A上电后硬件复位引脚需要拉低一段时间释放后还要等待内部时钟稳定之后才能访问MDIO寄存器。如果复位保持时间不够或者释放后立刻访问PHY读到的数据往往是随机的、不稳定的。我的做法是在PHY初始化前增加一段显式延时HAL_GPIO_WritePin(PHY_RST_GPIO_Port, PHY_RST_Pin, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(PHY_RST_GPIO_Port, PHY_RST_Pin, GPIO_PIN_SET); HAL_Delay(20);这个延时值远大于LAN8720A数据手册要求的最小复位时间多出来的部分是为了保证系统在不同电源上电曲线下都能稳定工作。很多HAL例程之所以大多数时候能跑、偶尔不行就是复位时序余量不够。6. 要不要单独建一个lan8720.c我的建议6.1 复制一份lan8720.c的代价看到这里你可能会想既然lan8742.c要改这么多干脆复制一份lan8720.c出来专用于LAN8720A这样不是更清晰吗这个思路在短时间内是可行的但从长期维护角度看代价不小。ST后续更新HAL库时lan8742.c里如果有BUG修复或新功能增加你维护的lan8720.c是跟随不到的。而且两个文件90%的内容是重复的每次对比代码差异都是一种精神消耗。6.2 直接在lan8742.c上改的隐患反过来直接在lan8742.c上改虽然省事但会把ST官方驱动变成一份只针对你当前板子的私有代码。下次换回LAN8742或者另一个项目复用这份代码很容易被这些改动误导尤其在多人协作时别人根本不知道这个lan8742.c已经被改成了披着LAN8742外衣的LAN8720A驱动。6.3 我推荐的PHY适配层做法我的折中方案是lan8742.c保持原封不动新建一个phy_adapter.c在里面封装PHY初始化、Link状态获取和寄存器访问三个接口内部再根据当前使用的PHY芯片调用对应驱动。// phy_adapter.h #include stm32f4xx_hal.h int8_t PHY_Init(void); uint8_t PHY_GetLinkState(void); // phy_adapter.c #include phy_adapter.h #include lan8742.h ETH_HandleTypeDef heth; int8_t PHY_Init(void) { // 统一走 lan8742_init因为驱动逻辑内部本身是通用的 // PHY地址差异已经在 lan8742.h 或 BSP 初始化时处理 return lan8742_Init(heth); } uint8_t PHY_GetLinkState(void) { return lan8742_GetLinkState(heth); }这样做的核心思想是让lan8742驱动继续作为标准BSP组件项目级代码依赖的是phy_adapter这个抽象层的接口。以后如果ST出了官方的LAN8720A驱动或者你要换其他PHY只需要改phy_adapter.c上层代码完全不用动。对以太网驱动这类硬件相关性很强、又需要长期维护的模块来说这层薄薄的适配是其值得保留的。我在自己的项目里最终就是采用了这个结构。后续代码review时别人看到phy_adapter就能明白这是一个为多PHY兼容设计的适配层而不是把官方驱动改得面目全非沟通成本也低了很多。最后分享一个非常实际的小教训适配完成后调试串口里打印的PHY信息还写着LAN8742看着PHY ID明明是0x0007C131我一度以为驱动读错了芯片。后来把打印文字一起改成LAN8720A才意识到驱动早就跑对了纯属日志误导。这种细节听起来很小但在排障时会让人多绕好几个弯建议你在适配时把相关打印信息一并改掉。
返回列表