☰
STM32F7+LAN8720以太网驱动调试:从RMII时钟到缓存一致性实战指南
2026/10/27 3:42:26 网站建设 项目流程

简介:本资源是面向嵌入式开发工程师与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输入接收数据位 0
PC5ETH_RMII_RXD1输入接收数据位 1
PG11ETH_RMII_TX_EN输出发送使能
PG13ETH_RMII_TXD0输出发送数据位 0
PG14ETH_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必须给准50MHz

RMII 接口把 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 的 PA1(ETH_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 Status,bit5 是 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不可能link

PHY 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 置 1,DMA 发送完成后会自动清 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/PLL3,RMII 的 50MHz 时钟来源和 F7 的配置方式不一样。移植时最容易踩的是直接用 F7 的 RCC 配置宏,编译通过但时钟不对,link 起不来。

另一个差异是 H7 的 Cache 和 MPU 机制比 F7 更复杂,ETHERNET DMA 的缓冲区必须明确配置 MPU 为非缓存区域,否则高速收发时会随机丢包。我在 H7 上遇到过一次很怪的问题:ping 大包偶尔通,数据量一大就卡死,最后定位到就是描述符区域没有设置成 non-cacheable,DMA 和 CPU 轮流访问同一个缓存行,互相踩脚。H7 上先把 MPU 区域配置好,再跑吞吐测试,这一步不能省。

6.3 LAN8720测试验收建议

最后给一个我常用的验收顺序,从底层往上逐步加码。第一步,MDIO 读 PHY ID。第二步,示波器确认 50MHz 时钟和 link 状态寄存器。第三步,用 LwIP 的 loopback 或者 UDP 回环测试,在板子上把收到的 UDP 包原样发回。第四步,PC 端用持续 UDP 打流工具跑 10 分钟,统计丢包率。第五步才是 TCP 下载大文件,验证吞吐和稳定性。这套顺序任何一步不过都往回查,不跳到下一步,能省大量时间。

调 LAN8720 这几个月下来,我最深的感觉是:真正的坑很少在协议栈本身,90% 的问题都集中在时钟、PHY 地址和缓存一致性这三个看起来“太基础”的地方。每次拿到新板子,先量时钟、再读 PHY ID、最后跑协议栈,顺序对了,这个驱动包基本就是一次点亮;顺序反了,就会在中断、缓冲区、协议栈里来回绕圈,既浪费时间又容易怀疑人生。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询