STM32F7+LAN8720以太网驱动调试:从RMII时钟到缓存一致性实战指南
发布时间:2026/8/31 2:33:47 作者:尧图编辑部 阅读量:1,286

简介本资源是面向嵌入式开发工程师与STM32进阶学习者的LAN8720以太网PHY芯片驱动工程专为STM32F7系列Cortex-M7内核高性能MCU设计解决百兆以太网物理层通信的底层驱动集成难题适用于工业控制、物联网网关等需稳定网络接入的嵌入式项目。压缩包共627个文件主体为201个.h头文件定义寄存器、结构体及API接口、175个.c源文件含HAL库适配、RMII接口配置、DMA收发管理、MIIM寄存器读写、中断服务例程及LWIP协议栈对接逻辑辅以.o/.d/.crf等编译中间文件整体大小25.96MB工程已包含完整KEIL项目uvprojx/uvoptx、可烧录hex/axf镜像及调试配置结构清晰、模块解耦便于理解PHY-MAC协同机制与TCP/IP栈下层衔接。目前已有922人学习下载提供从硬件初始化、链路自动协商、数据包收发到状态调试的全流程实现参考特别适合掌握以太网驱动开发全链路的中高级开发者。 解压一个名为“LAN8720网口驱动STM32F7系列单片机驱动程序.zip”的工程包直接编译就希望网口跑通在真实项目里基本都要折腾一轮。LAN8720 这颗 PHY 配合 STM32F7 内置的 MAC 和 DMA是低成本 10/100M 以太网方案的经典组合但驱动代码只是最后那 20%剩下 80% 全在硬件接线、RMII 参考时钟、PHY 寄存器初始化、DMA 描述符和缓存一致性上。这篇文章把我从 F4 到 F7 再到 H7 多块板子上调 LAN8720 的关键步骤和踩坑链路完整串一遍适合正在做联网项目、准备移植这个驱动包的嵌入式工程师。1. LAN8720驱动包背后的硬件基础PHY与内置MAC怎么配合1.1 这个驱动包对应的是哪条数据链路很多工程师把 LAN8720 当成一个“网口芯片”来理解其实不准确。它是一颗 PHY负责物理层的编码、解码、电平转换和链路协商真正跑协议栈的是 STM32F7 内部的以太网 MAC也就是 ETH 外设。数据链路大致是这样的STM32F7 的 DMA 从内存里取数据包交给 MAC 打包成 802.3 帧再通过 RMII 接口送给 LAN8720由 PHY 转换为差分信号到网口变压器最后从 RJ45 出去。反过来收包就是完全对称的流程。驱动包的核心工作就是把这条链路里的每一环都配好GPIO 复用、PHY 的 MDIO 管理接口、MAC 工作模式、DMA 描述符、中断路径以及和 LwIP 或裸机协议栈的对接。任何一个环节没对齐结果就是 ping 不通或者收发包异常。1.2 STM32F7侧的RMII引脚分配与合法性RMII 模式比 MII 省了一半左右的引脚这也是 STM32F7 与 LAN8720 最常用的接法。RMII 只需要 2 位数据线、1 根发送使能、1 根载波侦听/数据有效信号CRS_DV加上管理接口 MDIO/MDC 和一根 50MHz 参考时钟。一个典型且合法的引脚分配如下表所示其中大部分引脚在 STM32F7 上的复用功能是 AF11。STM32F7 引脚信号名方向说明PA1ETH_RMII_REF_CLK输入50MHz 参考时钟来自 LAN8720 或独立晶振PA2ETH_MDIO双向管理接口数据需要上拉电阻PA7ETH_RMII_CRS_DV输入载波侦听和数据有效复用信号PC1ETH_MDC输出管理接口时钟PC4ETH_RMII_RXD0输入接收数据位 0PC5ETH_RMII_RXD1输入接收数据位 1PG11ETH_RMII_TX_EN输出发送使能PG13ETH_RMII_TXD0输出发送数据位 0PG14ETH_RMII_TXD1输出发送数据位 1不同封装的 F7 芯片可能映射有差异建议拿到原理图后先对着所选型号的 datasheet 核对一遍。我见过不少项目PCB 画好了才发现 PG11 被另一路功能占用最后只能飞线改板或换方案这个成本比写驱动高多了。1.3 PHY地址选择一个反直觉的坑LAN8720 的 PHY 地址由硬件 strapping 引脚决定最常见的是地址 0 和地址 1 两种。很多驱动包的默认值是 0但部分开发板或自绘板子会把 PHYAD0 上拉到高电平地址变成 1。如果你在 HAL 里填的PhyAddress和硬件不对MDIO 读出来永远是超时或者 0xFFFF。这里有个很实用的排查技巧不要只盯着一个地址死磕。代码里先用地址 0 读一次寄存器 2读不到再换地址 1 读一次把两个都试一遍再往下查。关于地址的坑后面排查链路那节我还会再展开。2. RMII的50MHz参考时钟最容易翻车的供电与时钟环节2.1 为什么RMII必须给准50MHzRMII 接口把 100M 以太网的数据位宽压到 2 位代价就是时钟频率翻倍。MII 模式下 25MHz 时钟就能跑 100M 速率RMII 必须提供 50MHz 参考时钟。这个时钟不是“接近就行”LAN8720 对 REF_CLK 的精度要求是按 ppm 级的器件规格来的实际调板经验是频率差个几百 kHz 可能还能勉强 link但一旦差到几 MHz 级别比如喂进去一个 48MHz链路建立就非常不稳定甚至完全起不来。很多 F7 工程原本配置的 PLLQ 分频出来的时钟是 48MHz因为它主要是给 USB 用的。如果你图省事直接把这个时钟接到 LAN8720 上就正好踩中这个坑。48MHz 只差 4%看起来不多但对 RMII 来说已经到了链路失效的临界区。2.2 外部有源晶振与MCO1二选一怎么选给 LAN8720 提供 50MHz 参考时钟常见有两种做法。第一种也是我最推荐的板子上放一个 50MHz 有源晶振输出直接接到 LAN8720 的 XI/CLKIN 引脚同时把同一路时钟接到 STM32F7 的 PA1ETH_RMII_REF_CLK。这样 PHY 和 MAC 共用同一个源相位关系最简单调试时少一堆事。第二种用 STM32F7 的 MCO1 引脚输出时钟。MCO1 可以选择 PLLQCLK 作为时钟源再经过一级分频输出。问题在于F7 主频通常跑到 180MHz 或 216MHz要同时满足主频、USB 48MHz、RMII 50MHz 三个时钟约束PLL 参数往往很难三全其美。不是不可能但每次调整 PLL 参数都可能影响系统主频牵一发动全身。我的建议是如果板子已经预留了 50MHz 有源晶振的位置优先用晶振方案代码里只需要保证 GPIO 时钟和 ETH 外设时钟使能不需要碰 PLL。如果板子只能走 MCO1那就必须用示波器实测 MCO 输出频率确认是 50MHz 再继续不要凭代码里的注释判断。2.3 用示波器确认时钟是“拿到就能进”的检查项我调网口驱动的固定流程里开完 GPIO 和 ETH 时钟后第一件事不是烧 LwIP而是接示波器量 PA1 和 LAN8720 的 XI 引脚。正常情况下能看到干净稳定的 50MHz 方波或正弦波。如果这里没波形后边的 PHY ID 读取、link 检测全部白搭。没有示波器时至少要用逻辑分析仪抓一下主频和分频后信号的脉冲宽度粗略估算频率。用万用表只能判断有没有电平变化判断不了频率别指望用它完成这一步。3. PHY初始化流程从复位到自动协商完成的细节顺序3.1 硬件复位与软件复位缺一不可LAN8720 的硬件复位引脚 NRST 需要拉低一段时间再释放不同板子复位电路时间常数不一样保守的做法是拉低至少 100 微秒以上然后等待稳定。释放复位之后PHY 内部的时钟和寄存器才可用这时候才能用 MDIO 访问。软件复位是在 BMCR寄存器 0的 bit15 写 1 触发。这里有个常见的错误写完软件复位立刻去配置寄存器结果发现配置写不进去或者被复位冲掉。正确做法是写完复位位之后轮询该位直到它自动清零这说明 PHY 内部复位流程走完了。代码大致这样HAL_ETH_WritePHYRegister(heth, PHY_ADDR, 0, 0x8000); // BMCR bit15 reset uint32_t val 0; uint32_t timeout 100000; do { HAL_ETH_ReadPHYRegister(heth, PHY_ADDR, 0, val); } while ((val 0x8000) (--timeout 0));复位完成后再写速度、双工、自动协商等配置。3.2 PHY ID校验MDIO链路好不好的第一判断MDIO 这条管理通道有没有通最直接的验证方式是读 PHY ID 寄存器。LAN8720 的寄存器 2 和寄存器 3 组合起来是固定的厂商/型号 ID寄存器 2 读出来应为 0x0007寄存器 3 应为 0xC0F1。如果读到的值不是这两个数基本可以判定 MDIO 这层就有问题。uint32_t idr1 0, idr2 0; HAL_ETH_ReadPHYRegister(heth, PHY_ADDR, 2, idr1); HAL_ETH_ReadPHYRegister(heth, PHY_ADDR, 3, idr2);读回 0xFFFF 通常是 MDIO 线没接对或者 PHY 没复位读回 0x0000 可能是 PHY 地址选错读回的值在变化可能是 MDC 时钟不稳定或者干扰严重。把 ID 确认好再接协议栈是效率最高的一步不要跳过。3.3 自动协商、速度/双工与链接状态轮询工业场景里我一般建议关掉自动协商直接强制 100M 全双工。原因很实际很多现场交换机或工控机的协商行为并不规范自动协商偶尔会协商成半双工导致高吞吐时疯狂冲突。强制 100M 全双工只要对端设备本身是 100M 全双工工作就非常稳定。eth_handle.Init.AutoNegotiation ETH_AUTONEGOTIATION_DISABLE; eth_handle.Init.Speed ETH_SPEED_100M; eth_handle.Init.DuplexMode ETH_DUPLEX_FULL;链接状态的判断最常用的是读 BSR寄存器 1。bit2 是 Link Statusbit5 是 Auto-negotiation Complete。轮询到 link 之后再调用netif_set_link_up(netif)协议栈才知道网线已经插上。LAN8720 还有一个扩展状态寄存器寄存器 31调试时可以直接读出速度和链路信息比反复解析 BSR 方便。不同批次的 LAN8720 寄存器细节略有差异以对应 datasheet 为准。4. STM32F7驱动主流程拆解DMA描述符、HAL配置与LwIP入口4.1 HAL_ETH_Init关键参数怎么填STM32F7 的标准做法是用 HAL 库的 ETH 驱动再配合 LwIP 做协议栈。HAL_ETH_Init里有一组参数看起来简单但填错一个网口就不工作heth.Instance ETH; heth.Init.AutoNegotiation ETH_AUTONEGOTIATION_DISABLE; heth.Init.Speed ETH_SPEED_100M; heth.Init.DuplexMode ETH_DUPLEX_FULL; heth.Init.PhyAddress LAN8720_PHY_ADDRESS; heth.Init.MediaInterface ETH_MEDIA_INTERFACE_RMII; heth.Init.RxDesc rx_desc; heth.Init.TxDesc tx_desc; heth.Init.RxBuffLen ETH_RX_BUF_SIZE; HAL_ETH_Init(heth);MediaInterface必须填ETH_MEDIA_INTERFACE_RMII填成 MII 的话引脚采样完全不对。PhyAddress要跟 1.3 节说的硬件 strapping 对上RxDesc和TxDesc指向后续定义的描述符数组。4.2 描述符与收发缓冲区的内存对齐细节ETH 外设的 DMA 和普通 DMA 一样对内存地址有对齐要求。描述符数组和缓冲区最好都按 4 字节对齐保险一点按 32 字节对齐定义。我习惯写成这样__ALIGN_BEGIN ETH_DMADescTypeDef tx_desc[ETH_TXBUFNB] __ALIGN_END; __ALIGN_BEGIN ETH_DMADescTypeDef rx_desc[ETH_RXBUFNB] __ALIGN_END; __ALIGN_BEGIN uint8_t rx_buff[ETH_RXBUFNB][ETH_RX_BUF_SIZE] __ALIGN_END; __ALIGN_BEGIN uint8_t tx_buff[ETH_TXBUFNB][ETH_TX_BUF_SIZE] __ALIGN_END;别小看这个align属性描述符如果没对齐DMA 访问会直接触发错误中断表现就是系统跑着跑着进了 HardFault。缓冲区不齐则可能偶尔丢包问题还很隐蔽。4.3 F7的D-Cache一致性缓存不处理ping会一塌糊涂这是 F7 和 F4 最大的区别。F4 没有 D-Cache所以老项目的以太网代码直接搬到 F7最常见的问题就是能收到 ping 请求但回包内容错乱或者收包时数据里出现陈旧数据。原因是 STM32F7 的 D-Cache 默认是开启的CPU 写入发送缓冲区后数据还留在 Cache 里DMA 去内存读的时候读到的不是最新数据DMA 往接收缓冲区写了数据CPU 再来读时Cache 又可能返回旧内容。最简单的调试期处理先关闭 D-Cache把网口跑通。这是我最常用的做法先确认软件逻辑没问题再回来处理性能。正式项目不能一直关缓存这时候有两个方案。一个是发送前做SCB_CleanDCache_by_Addr把发送缓冲刷出去接收后做SCB_InvalidateDCache_by_Addr把接收缓冲失效掉。另一个是配置 MPU把 DMA 描述符和收发缓冲区所在的内存区域设为 non-cacheable。第二种方案更彻底对性能影响更小但要花时间写 MPU 配置适合稳定后优化。4.4 中断路径与LwIP接收的对接ETH 的中断处理在ETH_IRQHandler里调用HAL_ETH_IRQHandler。LwIP 的接收通知通常用信号量或标志位实现void ETH_IRQHandler(void) { HAL_ETH_IRQHandler(heth); }在HAL_ETH_RxCpltCallback里释放信号量让tcpip_thread去调用 LwIP 的netif-input。这只是其中一种方式也可以用轮询方式在 main 循环里调用ethernetif_input但中断加信号量的实时性更好高负载下不容易丢包。注意HAL_ETH_IRQHandler里对错误中断的处理。DMA 总线错误、描述符未对齐、接收 FIFO 溢出都会在中断标志里体现。如果板子跑起来一会儿就卡死多半是 RX 溢出LwIP 的PBUF_POOL_SIZE和MEM_SIZE设太小了。5. 网口不通时的完整排查链路按顺序走不要跳步5.1 第一环MDIO层面根本读不到PHY ID只要网口不通我第一步永远是去 HAL 的调试窗口里手动读一次 PHY ID 寄存器。读不到后面所有网络层面的排查都没有意义。常见原因有四种。第一PHY 复位引脚一直被拉低程序里没释放。第二MDIO 线缺少上拉电阻导致信号电平不稳定。第三PHY 地址选错换地址 0 和 1 各试一次。第四GPIO 复用没配对PA2/PC1 没有切成 AF11。这里给一个很笨但很有效的办法把读 PHY ID 的返回值直接设成断点条件然后单步看每次读回的值。如果值是 0xFFFF、0x0000、无规律变化就能区分以上四种情况不用靠猜。5.2 第二环REF_CLK不对PHY不可能linkPHY ID 能读到但 link 一直起不来立刻去查时钟。RMII 的 50MHz 参考时钟如果没给、给错频率、或者波形质量太差PHY 内部的状态机就永远停在“链接断开”。我经历过一次非常隐蔽的故障示波器量 XI 引脚50MHz 频率完全正常但波形上升沿很缓最终发现是晶振输出串了 100 欧电阻后到 PHY 引脚的电平幅度不够。后来把串阻换小波形才恢复。所以光看频率不够还要看幅度和边沿。5.3 第三环GPIO复用与DMA描述符状态时钟正常、PHY 也能 link但是 ping 还是不通就看 MAC 和 DMA 这一层。先用 STM32CubeMX 重新生成一遍 GPIO 初始化和手写的初始化对比。很多手工代码漏了 PG11、PG13、PG14 这三个发送引脚只配了接收侧的 PA1/PA7/PC4/PC5发不出去包里还找不到原因。再看 DMA 描述符的状态位。调试器里查看tx_desc[0].Status最高位Own bit发送前必须由 CPU 置 1DMA 发送完成后会自动清 0。如果 Own bit 一直是 1 且 DMA 没反应多半是描述符或缓冲区地址问题。另外ETH-DMASR里的总线错误位和接收状态位能快速定位是 DMA 停了还是 FIFO 溢出。5.4 第四环LwIP配置与MAC地址到这一层才轮到 LwIP。最容易犯的错是把 MAC 地址全部初始化为零。MAC 全零时ARP 请求发出去对端设备不知道该回给谁表现为本机 ping 外网不通但本机能收到一些广播包。检查 LwIP 配置时优先看三个LWIP_ICMP是否开启PBUF_POOL_SIZE和MEM_SIZE是否够用tcpip_thread栈大小是否足够。栈小了会随机死机尤其在使用 DHCP 或断网重连时。5.5 第五环UDP/TCP验证链路通了之后我通常先用 UDP 做验证因为 UDP 无连接排查简单。PC 端开一个网络调试助手板子往 PC 的固定端口发数据PC 往板子发数据板子串口打印。双向都通再上 TCP。TCP 涉及连接状态机一旦不通先确认对端端口是否监听再查协议栈的tcp_active_pcbs之类的连接表。6. 从F7迁移到H7、F407时需要注意的差异6.1 F407上的移植差异F407 没有 D-Cache这是移植到 F4 时最舒服的一点。硬件接线、PHY 初始化、HAL_ETH 参数和 F7 几乎一模一样很多代码可以直接复用。拿到 F407 板子上沿用同一份 LAN8720 驱动时重点检查两处RMII 参考时钟的来源是否同样是 50MHz以及 GPIO 复用是否从 F7 的 AF11 换成了对应 AF 值F4 的大部分以太网引脚也是 AF11但引脚编号可能有差异。很多网上讨论 STM32F407 加 LAN8720 做 UDP 的项目其实就是把 F7 的驱动搬过去改一下 GPIO 初始化、时钟配置编译即可。F4 上不用处理缓存一致性问题逻辑上反而少一层坑。6.2 H7上的移植差异H7 的 ETH 外设和 F7 在 HAL 接口上很像但底层时钟树完全不同。H7 的 RCC 有独立的 PLL1/PLL2/PLL3RMII 的 50MHz 时钟来源和 F7 的配置方式不一样。移植时最容易踩的是直接用 F7 的 RCC 配置宏编译通过但时钟不对link 起不来。另一个差异是 H7 的 Cache 和 MPU 机制比 F7 更复杂ETHERNET DMA 的缓冲区必须明确配置 MPU 为非缓存区域否则高速收发时会随机丢包。我在 H7 上遇到过一次很怪的问题ping 大包偶尔通数据量一大就卡死最后定位到就是描述符区域没有设置成 non-cacheableDMA 和 CPU 轮流访问同一个缓存行互相踩脚。H7 上先把 MPU 区域配置好再跑吞吐测试这一步不能省。6.3 LAN8720测试验收建议最后给一个我常用的验收顺序从底层往上逐步加码。第一步MDIO 读 PHY ID。第二步示波器确认 50MHz 时钟和 link 状态寄存器。第三步用 LwIP 的 loopback 或者 UDP 回环测试在板子上把收到的 UDP 包原样发回。第四步PC 端用持续 UDP 打流工具跑 10 分钟统计丢包率。第五步才是 TCP 下载大文件验证吞吐和稳定性。这套顺序任何一步不过都往回查不跳到下一步能省大量时间。调 LAN8720 这几个月下来我最深的感觉是真正的坑很少在协议栈本身90% 的问题都集中在时钟、PHY 地址和缓存一致性这三个看起来“太基础”的地方。每次拿到新板子先量时钟、再读 PHY ID、最后跑协议栈顺序对了这个驱动包基本就是一次点亮顺序反了就会在中断、缓冲区、协议栈里来回绕圈既浪费时间又容易怀疑人生。本文还有配套的精品资源点击获取