简介:本资源是一套面向嵌入式开发工程师与STM32进阶学习者的LAN8720以太网PHY芯片驱动工程,专为STM32F7系列(Cortex-M7内核)设计,解决百兆以太网在高性能MCU上的底层通信集成难题,适用于工业控制、物联网网关等需稳定网络接入的实战场景。压缩包共627个文件,涵盖201个头文件(.h,定义寄存器、结构体与API接口)、175个源文件(.c,含HAL库适配、RMII接口配置、DMA收发管理、MIIM寄存器读写及中断服务例程)、68个目标文件(.o)与调试相关文件(.dbgconf、.map、.axf等),整体大小25.96MB,目录结构完整,包含FSMC/ETH外设初始化、LwIP协议栈对接模块及典型网络测试用例。目前已有922人学习下载,代码已通过实际硬件验证,提供可直接移植的初始化流程、链路状态轮询机制、收发缓冲区管理策略及常见异常(如PHY未连接、DMA溢出)的排错提示,是理解STM32F7以太网底层驱动架构的优质参考工程。
1. 项目概述与驱动设计思路
最近处理了一个很典型的工程包,名字叫“LAN8720网口驱动STM32F7系列单片机驱动程序”。这类驱动包在嵌入式项目里出现频率极高,因为LAN8720这颗百兆PHY芯片太常见了,STM32F7系列又自带MAC控制器,俩一搭配就是标准的RMII以太网方案。这个包解决的问题很直接:让你手上的STM32F7(比如F746、F767)快速跑通网口,能ping通、能收发UDP/TCP数据,不用自己从零抠PHY寄存器。
先说清楚硬件分工:STM32F7内部集成的是MAC层(媒体访问控制层),负责数据帧的封装、拆封、CRC校验、DMA传输这些活儿;而LAN8720是PHY芯片(物理层),负责把MAC给它的并行数据转成差分信号怼到网线上,同时处理链路协商、速率匹配、MDI/MDIX自动翻转这些模拟和物理层面的东西。也就是说,MCU负责“想”,PHY负责“说”。没有PHY,MAC就是个哑巴。
1.1 核心需求解析
拆解这个驱动包,本质上是三件事的集合:
第一,RMII接口的引脚初始化。STM32F7的ETH外设支持MII和RMII两种模式,RMII只用了7根信号线(REF_CLK、CRS_DV、RXD[1:0]、TXD[1:0]、TX_EN、MDC、MDIO),比MII的16根线省了一半还多,非常适合引脚紧张的板子。驱动包里已经把这些引脚的GPIO复用模式、速度等级都配好了。
第二,PHY芯片LAN8720的寄存器初始化。PHY上电后不会自动进入理想工作状态,需要通过MDIO总线读写它的寄存器,完成软复位、速度/双工模式配置、LED配置、中断使能等操作。这一步跑通了,物理链路才算真正“活”了。
第三,与LwIP协议栈的对接。光有MAC和PHY只能发裸帧,要ping通、要跑TCP,必须把底层驱动接到LwIP上。驱动包里的低层接口(ethernetif.c这类文件)负责把LwIP的PBUF和STM32的DMA描述符接起来。
对谁有用?我觉得这东西适合三种人看:一是做物联网网关、数据采集器的工程师,需要快速给板子加上网口;二是学生做毕设,想用STM32F7跑以太网通信;三是想搞懂RMII底层机制、想自己手写PHY驱动的爱好者。驱动包是现成的,但如果你不知道它每一步在干什么,出了问题基本两眼一抹黑,所以我这篇会把它拆开来讲。
1.2 驱动文件构成与整体架构
一个规范的可用的驱动包,文件架构大致是这样几块,我列个表说明各自的职责:
| 文件/模块 | 职责 |
|---|---|
| eth.c / stm32f7xx_hal_eth.c | 外设寄存器级的MAC初始化、DMA描述符配置、收发中断处理 |
| lan8720.c / phy.c | PHY寄存器读写封装、复位、自动协商、状态查询 |
| ethernetif.c | LwIP底层接口:初始化、输出、接收、轮询状态,负责PBUF和DMA描述符的数据搬运 |
| lwipopts.h | LwIP配置裁剪:内存池大小、超时时间、协议开关 |
| stm32f7xx_hal_conf.h | HAL库功能开关,确认ETH模块已开启 |
| sys_arch.c / sys_arch.h | 操作系统适配层(如果跑FreeRTOS)或空实现(裸机轮询) |
典型的数据流向是这样的:网线进来→LAN8720把差分信号转成4位RMII数据(RXD[1:0]+CRS_DV)→STM32的ETH外设接收FIFO→DMA描述符搬运到内存→HAL以太网驱动丢给LwIP的接收线程(或裸机轮询)→应用程序拿到数据。发送方向完全反过来:应用层构造PBUF→LwIP封装→DMA从内存搬运→ETH外设转成RMII时序→LAN8720发到网线。
这个架构是ST官方和LwIP社区多年磨合的产物,稳定可靠,可裁剪性也强。如果后续要换PHY芯片(比如换成DP83848、KSZ8081),只需要替换lan8720.c里的寄存器配置函数,上层完全不用动,这就是分层设计的好处。
2. 核心底层细节与关键实操要点
2.1 RMII时钟配置:整个链路最容易翻车的地方
我在调试中见过太多人卡在RMII时钟上,说“PHY寄存器读不到”“RX一直没数据”,最后排查发现REF_CLK根本就没起振。RMII协议要求PHY和MAC共享一个50MHz的参考时钟,这个时钟可以是MAC输出给PHY,也可以是外部有源晶振同时供两边。STM32F7系列的数据手册里写得很清楚,ETH外设的时钟源有两个选择:AHB1时钟的二分频(一般72MHz或108MHz,不是精确的50MHz),或者MCO引脚输出的精确50MHz。
关键点来了:RMII模式下,REF_CLK的精度直接影响数据采样稳定性。如果你用AHB1分频出来的凑合时钟,跑百兆以太网时眼图余量会很差,偶尔能通、偶尔掉包。正确的做法是:用PLL的一个专用输出产生50MHz,从MCO2引脚(PC9)输出,同时接到LAN8720的REF_CLK引脚和STM32的ETH_RMII_REF_CLK引脚(PA1)。我第一次弄这个的时候还纳闷“同一个时钟为什么要分两路”,后来才想明白:LAN8720需要这个时钟做收发时序基准,STM32的MAC也需要这个时钟做数据采样同步,两边必须同源,否则收发数据就会有相位漂移。
代码上,用STM32CubeMX配置时要注意三点:一是RCC时钟树里把MCO2使能,时钟源选PLLCLK,分频系数选4(PLL输出200MHz的话,4分频正好50MHz);二是PC9引脚的复用功能必须设为MCO,不能默认当GPIO用;三是同时把PA1配置成ETH_RMII_REF_CLK复用功能。很多CubeMX版本会自动处理,但如果你用的是老版本或者手动改代码,这两个引脚的配置容易漏。
补充一句,如果你板子上用了外部50MHz有源晶振直接给LAN8720,那么STM32这边只需要把PA1配成输入模式接收REF_CLK,PC9不用动。这种方案的好处是PHY不依赖MCU的启动时序,坏处是得多焊一个晶振。以我经验,CubeMX默认生成的MCO2方案更省事,也够稳定。
2.2 PHY地址与复位时序问题
LAN8720的PHY地址由AD0引脚的上下拉决定,大多数模块上AD0默认下拉,PHY地址是0x00;如果AD0接了上拉电阻(比如1k到3.3V),地址就是0x01。有些模块甚至把这个引脚引出来让你飞线改地址,方便一总线上挂多个PHY,但很少见。我建议你拿到模块先看原理图确认,或者写段代码扫描0x00~0x1F所有地址,读到PHY ID寄存器(寄存器2和3)值正确的话就锁定地址。LAN8720的PHY ID是0x0007C0F1,这个值可以作为判断PHY是否响应的重要依据。
再说复位时序,这里有个坑。LAN8720的NRST脚需要至少保持低电平1ms,再释放高电平,内部复位才能完成。有些模块的复位引脚直接接MCU的GPIO,那你在初始化代码里要先输出低电平、延时至少10ms、再拉高,保守一点比较好。另一个坑是:某些开发板把PHY的复位脚和MCU的复位脚连在一起,那MCU复位后PHY会跟着复位,但PHY的RC复位电路时间常数可能不够,导致MCU跑起来时PHY还没准备好,这时你去读PHY寄存器大概率超时。解决办法是在PHY初始化函数开头加个毫秒级延时,等PHY稳定,再执行软复位(寄存器0的bit15拉1)。
2.3 DMA描述符与CACHE一致性:F7平台特有的大坑
Cortex-M7内核带L1-Cache,STM32F7的D-Cache默认是关闭的,但很多人一开D-Cache就发现以太网数据全乱了:发出去的数据是脏的,收进来的数据莫名其妙丢字节。原因在于DMA是直接搬运内存的,它不经过CPU的Cache。如果CPU写了发送缓冲区,数据还在D-Cache里没刷到SRAM,DMA读到的就是旧数据;反过来,DMA把收到的数据写进SRAM,CPU去读的时候命中的却是Cache里旧的内容。
解决办法有三个层次。最省事的是把DMA描述符和收发缓冲区放在不会被Cache“污染”的内存区,也就是MPU配置成non-cacheable属性;其次是在每次DMA操作前后调用SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr,手动维护一致性;最推荐的做法是两者结合:描述符放non-cacheable区,数据缓冲区用Cache维护函数,这样性能和安全都兼顾。驱动包里如果没做这些处理,你在开启D-Cache后必须自己补上。具体操作就是在MPU配置里,给ETH的DMA描述符数组和缓冲区数组所在的RAM区域设置一个region,Cacheable属性关掉,这个细节能帮你省下大量掉头发的时间。
3. 实操过程与核心环节实现
3.1 STM32CubeMX初始化ETH外设完整步骤
如果从零开始建项目,我建议直接用CubeMX生成基础工程,然后手动补PHY驱动和LwIP配置。具体步骤如下:
第一步,选芯片。以STM32F746ZGT6为例,在CubeMX里选好型号后,先配RCC和时钟树。以太网需要50MHz的RMII参考时钟,我在时钟树里把HSE设为25MHz(视板子而定),PLL源选HSE,PLLM=25、PLLN=200、PLLP=2、PLLQ=8(这个值用于USB等)、PLLR=2,这样SYSCLK是200MHz,PLL的另一个输出经MCO2分频后得到50MHz。具体参数跟着CubeMX时钟树可视化界面调,它自动计算合法性,省得我手算。
第二步,配置ETH外设。在Categories里找到Connectivity→ETH,Mode选“RMII”,全局中断使能打开。CubeMX会自动占用PA1、PA2、PA7、PC1、PC4、PC5、PB11、PB12等引脚,对应ETH_RMII_REF_CLK、ETH_MDIO、ETH_MDC、ETH_RMII_CRS_DV、ETH_RMII_RXD0、ETH_RMII_RXD1、ETH_RMII_TX_EN、ETH_RMII_TXD0、ETH_RMII_TXD1。这时候留意PA1,它默认可能被其他外设占用,优先把冲突打开。
第三步,配置MCO2引脚。在Pinout视图中找到PC9,把它的功能设为MCO2,然后在Clock Configuration里把MCO2源选为PLLCLK,分频系数选4。这样PC9输出50MHz给LAN8720的REF_CLK。有些板子REF_CLK从外部晶振来,那这步可以跳过,但大多数国内模块设计都是让MCU提供REF_CLK,所以我默认按这个做。
第四步,生成工程。在Project Manager里选好工具链(MDK-ARM或IAR),生成代码后,打开main.c,在while(1)之前把MX_ETH_Init()调用的返回值检查一下。如果返回HAL_ERROR,八成是PLL配置不对或者PHY地址不对,稍后说排查方法。
3.2 LAN8720驱动代码对接细节
CubeMX生成的HAL库本身不含LAN8720的专用寄存器配置,它只负责MAC和DMA初始化。真正和LAN8720打交道的是HAL_ETH_InitConfig结构体里的两个回调函数:PHY_Init和PHY_Write。在stm32f7xx_hal_eth.c的HAL_ETH_Init里,会调用HAL_ETH_ReadPHYRegister和HAL_ETH_WritePHYRegister来读写PHY寄存器。如果你的初始化代码里没有为PHY地址赋值,默认是0,而有些PHY地址是1,那就要改这里的PHY_ADDRESS宏。
驱动包里一般会封装一个lan8720_init函数,流程是:
- 配置PHY地址(LAN8720_ADDR,根据硬件设0或1)。
- 软复位:写寄存器0的0x8000,等待15.2bit清零。
- 启用自动协商:寄存器0设为0x1200(100M全双工使能、自动协商使能),让PHY自动和交换机对速率和双工。
- 配置LED:写寄存器0x1C,比如LED1=link/activity,LED2=100M速度指示。
- 延时100ms,查询寄存器1的bit5(自动协商完成标志),确认链路已建立。
需要特别提一点,速率和双工的匹配。如果自动协商没成功,PHY可能保持在10M半双工,这时候ping可能通但吞吐量惨不忍睹。可以在代码里做一次PHY状态检查:读寄存器1的bit2(连接状态)、bit5(自动协商完成)、寄存器0的bit13和bit8确认当前速度和双工模式。测试时用iperf或UDP高速多发,观察实际吞吐量是否接近100Mbps。
3.3 接入LwIP实现UDP/TCP通信
驱动包里的lwipopts.h怎么配置,直接决定内存占用和性能表现。基于F7平台,我推荐几个建议值:
| 配置项 | 建议值 | 原因 |
|---|---|---|
| MEM_SIZE | 1600~2560 | LwIP内部堆大小,太小会分配失败 |
| PBUF_POOL_SIZE | 6~16 | 接收缓冲数量,太小高负载下丢包 |
| PBUF_POOL_BUFSIZE | 1536 | 需大于最大以太网帧长,含FCS预留 |
| TCP_MSS | 1460 | 标准以太网MTU 1500去掉IP/TCP头 |
| NO_SYS | 0或1 | 裸机设1,跑RTOS设0 |
| LWIP_DHCP | 1 | 自动获取IP,方便测试 |
在ethernetif.c里,low_level_init函数会读MAC地址、初始化DMA收发描述符。MAC地址可以是48位随机数,但建议用STM32芯片唯一的UID生成,这样每台设备地址不冲突,后续项目也不会遇到“两台板子MAC一样导致局域网抖动”的问题。生成方法就是读UID的3个32位寄存器,取低16位合成后三个字节,前三个字节用固定OUI,比如0x02 0x00 0x00。
LwIP初始化后,配置IP地址。测试阶段我建议直接静态IP,比如192.168.1.250,掩码255.255.255.0,网关192.168.1.1。主要原因是DHCP失败时链路状态不好定位问题,静态IP一配就能拉起来测连通性。等稳定后再切DHCP也不迟。
实现一个最基础的回环功能,验证网络栈是否正常:在main循环里调用tcpip_thread或裸机轮询的ethernetif_input,收到UDP数据后原样回给发送方。LwIP的udp_recv回调里注册一个处理函数,把收到的payload长度和内容打印出来,再调用udp_sendto返回。这个回环测试能一次性验证发送、接收、中断、协议栈全部链路,比单纯ping更能说明问题。
3.4 实测流程与数据
我实际在一个STM32F746G-DISCO板子上跑过这套驱动,配置8MHz HSE、PLL倍频到216MHz、ETH走RMII、MCO2输出50MHz。LAN8720模块用的是淘宝常见的工业级小板,带网络变压器,RJ45接口。实测流程:
- 上电后串口打印“PHY init OK, link up, 100Mbps Full Duplex”。
- PC网口设置静态IP 192.168.1.1,板子接交换机,ping 192.168.1.250,延迟约1ms,丢包0%。
- 用Wireshark抓包,确认发送的UDP数据帧的源MAC、目的MAC、IP头部都正确。
- 用Iperf3测试UDP吞吐,MSS配置合理时达到约95Mbps,说明百兆链路基本打满,没有明显瓶颈。
这个流程里最耗时间的阶段其实是PHY寄存器调试,因为一上来总是读不到ID,后面发现是复位时序不够,加了20ms延时后一切正常。所以无论驱动包多“现成”,硬件时序问题永远是第一位要排查的。
4. 常见问题与排查技巧实录
4.1 问题速查表
结合我多年调试以太网的经验,把LAN8720+STM32F7最常见的故障和排查方向整理成下表,你可以直接拿来当排查清单用:
| 现象 | 可能原因 | 排查与解决方法 |
|---|---|---|
| PHY寄存器读超时或读到全F | PHY地址不对;REF_CLK没起来;复位没完成 | 扫描0x00-0x1F地址;示波器测PC9和PA1有无50MHz时钟;检查复位时序 |
| LINK状态一直down | 网线、交换机或PHY协商失败 | 换网线/换对端设备;读寄存器1确认bit2连接状态;用短路环自发自收排除外部问题 |
| 能ping通但UDP吞吐极低 | 自动协商卡在10M半双工;PBUF不足 | 读寄存器0确认速度和双工;增大PBUF_POOL_SIZE;检查代码固定了速度导致协商失败 |
| 发送正确但收不到数据 | 中断未使能;DMA描述符没配好;D-Cache未做一致性操作 | 检查ETH_IRQHandler是否调用HAL_ETH_IRQHandler;打印描述符状态;清理D-Cache |
| 收到的数据CRC错误 | 时钟抖动大;PCB走线干扰;MCO2时钟源没配准 | 确认REF_CLK是否精确50MHz;降低ETH引脚翻转速率;检查差分线阻抗匹配 |
| 一开D-Cache就死机 | DMA缓冲区在cacheable区,一致性被破坏 | 用MPU把DMA描述符和数据区配成non-cacheable;或手动Clean/Invalidate |
4.2 具体排查案例:PHY地址对不上
我遇到过最经典的案例:驱动包代码里写死PHY地址是0x00,但板子上的LAN8720模块AD0确实接了下拉,地址理论上是0。用ST-Link单步调试,读寄存器2和3却都返回0xFFFF,这代表MDIO总线上根本没PHY响应。
当时第一反应是MDIO和MDC引脚有虚焊,但用示波器量波形,MDC有稳定的2.5MHz时钟,MDIO上也有数据脉冲,说明MCU发得没问题,问题大概率在PHY侧没回应。后来仔细看了模块的规格书,发现它内部把AD0通过10k电阻上拉到3.3V,也就是模块层面已经把PHY地址定成了0x01。哪怕外部引脚看起来悬空,但内部设计已经决定地址了。我改代码里的地址为1,再读,立即读到0x0007C0F1,问题解决。
这个教训就是:千万不要用板子外观或经验臆断PHY地址,一切以模块原理图或至少一段扫描代码为准。直接写个for循环扫描0到31地址,读PHY ID寄存器,哪个地址返回0x0007C0F1就把地址定在哪,这比人肉猜可靠得多。
4.3 具体排查案例:RMII时钟源没起振
另一个高频问题,是在HAL_ETH_Init返回HAL_ERROR时,查看错误码经常是PHY read timeout。这个例子中CubeMX的时钟树配好了,MCO2也从PC9引出,用示波器测却是0Hz,但PC9的GPIO输出电平有1.2V。排查后发现CubeMX生成的GPIO代码如下:
GPIO_InitStruct.Pin = GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.Alternate = GPIO_AF0_MCO; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);看起来没问题,但MCO2的时钟源没使能。在main函数里MX_RCC_Init先执行了,但如果时钟配置时PLL没用的情况下,MCO2就无源可出。解决办法是在SystemClock_Config里确保PLL已启用,并把MCO2Prescaler和ClockSource正确赋给RCC_OscInitStruct和RCC_ClkInitStruct,最后调用HAL_RCC_MCOConfig(RCC_MCO2, RCC_MCO2SOURCE_PLLCLK, RCC_MCO2_DIV4)。设置完用示波器量PC9,能看到完整的50MHz方波,信号幅度约3.3V。
我总结了一个经验:调试以太网先测三个时钟——MCU主频、PC9的50MHz、PA1的REF_CLK。三个都对,再谈PHY寄存器,否则后面全是浪费时间。
4.4 调试技巧:手把手教你读PHY寄存器
调试过程中,串口打印PHY寄存器状态是非常有用的工具。我建议在初始化之后,至少把以下寄存器打印出来:寄存器1(状态寄存器,bit2是LINK,bit5是Auto-Negotiation Complete)、寄存器0(控制寄存器,bit13是速度选择,bit8是双工模式)、寄存器2和3(PHY ID确认)。一个简单的打印函数如下,配合串口重定向就能用:
void lan8720_dump_regs(void) { uint16_t reg = 0; for (uint8_t i = 0; i < 2; i++) { uint16_t val = lan8720_read_reg(1); printf("Phy Reg1 (BCR) = 0x%04X\r\n", val); val = lan8720_read_reg(1); printf("Phy Reg1 (BSR) = 0x%04X\r\n", val); val = lan8720_read_reg(2); printf("Phy Reg2 (ID1) = 0x%04X\r\n", val); val = lan8720_read_reg(3); printf("Phy Reg3 (ID2) = 0x%04X\r\n", val); } }如果读出的BCR确实为0x1000或0x1200,说明链路协商已完成;如果BSR的bit2=1,说明物理连接正常。我可以把“寄存器1 bit2=1且bit5=1”作为驱动就绪的核心判据,这一条符合了基本就稳了。
5. 驱动包裁剪与后续扩展建议
驱动包导入你的项目后,强烈建议做一次裁剪,去掉不需要的功能和调试代码,这样既能减小Flash占用,也让代码清晰。比如LwIP里如果你只跑UDP,那TCP相关的宏可以关掉;如果不跑DHCP,把LWIP_DHCP设为0;如果不做DNS,LWIP_DNS也可以裁剪。此外,日志打印建议加开关宏,生产版本默认关闭,不然串口打印那点时间在循环收发时也会拖慢吞吐。
扩展方向考虑的话,这个驱动接上不同上层业务就能演变成各种产品。最常见的扩展示例是Modbus TCP从站,可以用freemodbus的TCP版本,基于netconn API实现;也可以做MQTT客户端,把采集的传感器数据定时上报;如果跑FreeRTOS,LwIP的sys_arch层配好之后,还可以开HTTP服务器做设备管理页面,这些方向驱动层复用度极高。
有个环节提醒一下:如果你要把这套驱动移植到STM32H7系列,注意H7的D-Cache问题和F7类似但更严重,因为H7主频更高,Cache行为也不一样。务必把ETH DMA描述符和缓冲区放到配置正确的内存区域,MPU配置和链接脚本都要配合处理,不然同样会出现“发出去是错的、收进来是乱的”的情况。这个包虽然是F7专用,但移植到H7的技术路线基本一样,差异主要在HAL库的API和时钟配置上。
从操作上看,我做驱动的习惯是:先让LED亮(本地调试)→再让串口说话(能看到状态)→再让网口通(能ping)→最后才加业务逻辑。每一步都有可验证的中间结果,不会出现“改了一堆代码但不知道哪一步错误”的情况。驱动包也一样,建议你先用最小工程跑通ping,再逐步添加自己的业务代码。
本文还有配套的精品资源,点击获取