做单片机开发,只要一碰到需要联网、远程数据上报、OTA升级这类需求,嵌入式以太网基本就是绕不开的选择。STM32F407这颗芯片内部集成了10/100M以太网MAC控制器,外部再配一颗PHY芯片就能组成完整网口;协议栈用lwIP,轻量不占内存,任务调度交给FreeRTOS。这套组合在国产开发板和工业控制板里出现得非常多,也是我过去几个项目里反复在用的方案。这篇文章就从实际工程角度,把通过CubeMX配置STM32F407、驱动LAN8720A、整合lwIP和FreeRTOS的完整过程捋一遍,顺便把那些不翻手册根本发现不了的坑也一起说清楚。想给F407加网口、或者第一次玩lwIP的朋友,按这套流程走下来基本能少走不少弯路。
1. 整体架构与方案选型分析
1.1 STM32F407以太网外设到底是个什么结构
很多新手刚开始接触STM32以太网时会有一个误区:以为芯片本身集成了完整的以太网功能,外部只要接个网口座就能通。实际上STM32F407内部集成的只是以太网MAC层控制器(包括DMA控制器、流控、MAC地址过滤等),物理层信号编码、电平转换、线路驱动这些工作,必须由外部PHY芯片来完成。STM32F407通过MII或RMII两种接口和外部PHY芯片通信,再用SMI管理接口(也就是MDIO/MDC这一对线)去读写PHY的寄存器,实现对PHY的控制和状态查询。
这就引出了整套系统的分层逻辑:最底层是PHY芯片,负责把MAC发过来的并行数据变成差分信号送到网线上;中间是STM32自带的MAC外设,负责做以太网帧的发送接收、CRC校验、DMA搬运;上层才是lwIP协议栈,负责把IP包、TCP/UDP连接、ARP等协议逻辑跑起来;而FreeRTOS则负责把所有这些逻辑放进独立的线程里,让协议栈和用户应用互不阻塞。分层清晰的最大好处是调试好定位,哪一层出问题就查哪一层,不会出现牵一发动全身的窘境。
1.2 为什么大家普遍选LAN8720A + FreeRTOS这套组合
市面上能配合STM32F407使用的PHY芯片不少,DP83848、KSZ8081、LAN8720A都是经常见到的型号。但如果你去翻主流开发板的设计,会发现F407板载PHY几乎清一色用LAN8720A。原因也很实在:这颗芯片3.3V单电源供电,内部自带了1.2V稳压输出,外围电路极简,不需要额外一组电源;RMII接口信号线只有7根左右(数据、时钟、控制),占用的GPIO比MII少了快一半;芯片价格低、货源稳定,在淘宝上十几块就能买到带网络变压器的模块。更重要的是,这颗PHY和ST官方评估板上的LAN8742A寄存器定义基本同源,CubeMX生成的LAN8742驱动代码可以直接兼容,省去了自己手写PHY驱动的工作量。
FreeRTOS的选择更不用多说,开源免费、CubeMX原生支持、教程资料遍地都是。lwIP则是嵌入式TCP/IP协议栈事实上的标准,和FreeRTOS的适配层ST官方已经做好了。这仨组合在一起,从硬件到软件都有现成代码可用,对项目开发来说是最低成本的起步方式——你要做的不是发明轮子,而是把这些轮子正确地装到车上。
2. 硬件准备与关键电路细节
2.1 LAN8720A核心外围电路与时钟方案
LAN8720A最常用的时钟方案是25MHz无源晶振。芯片内部有PLL电路,会把25MHz倍频到50MHz,这个50MHz信号再通过芯片的REF_CLK输出引脚提供给STM32作为RMII参考时钟。需要注意的是,如果你用的是那种裸芯片自己画板,XI/XO两个引脚要接25MHz晶振,晶振两个引脚对地各接一个12pF~22pF负载电容;如果你买的是集成模块,一般在板子上已经画好了,直接供电就能用。
还有一种方案是用外部50MHz有源晶振给LAN8720A提供时钟,同时把这一路50MHz时钟也接到STM32的RMII_REF_CLK引脚上。两种方案各有利弊:25MHz无源晶振成本低,但需要确认LAN8720A的REF_CLK输出是否默认开启;50MHz有源晶振可靠性高,但成本和PCB面积都会增加。市面上主流的LAN8720A模块默认用25MHz晶振方案,因此在CubeMX里配置时,RMII_REF_CLK引脚(PA1)是作为输入使用的,时钟来自PHY芯片回传。
我还碰到过有人想省晶振,直接用STM32的MCO引脚输出50MHz时钟给LAN8720A。这种方案理论可行,但MCO输出能力有限,驱动PHY的时钟输入时需要留意波形质量,而且配置复杂,新手不推荐一上来就搞。
2.2 RMII接口引脚分配与注意事项
RMII接口相对MII省引脚,但信号时序要求更严格。一套典型的STM32F407 + LAN8720A的RMII连接长这样(具体引脚以你的开发板原理图为准,这是最常见映射):
| RMII信号 | STM32引脚 | 方向 |
|---|---|---|
| REF_CLK | PA1 | PHY → STM32 |
| CRS_DV | PA7 | PHY → STM32 |
| TX_EN | PB11 | STM32 → PHY |
| TXD0 | PB12 | STM32 → PHY |
| TXD1 | PB13 | STM32 → PHY |
| RXD0 | PC4 | PHY → STM32 |
| RXD1 | PC5 | PHY → STM32 |
| MDIO | PA2 | 双向 |
| MDC | PC1 | STM32 → PHY |
这里面有几个硬件设计上的坑值得单独提。首先,MDIO是双向信号,STM32这边要配置为推挽输出,但MDIO链路还需要上拉电阻,一般4.7kΩ拉高到3.3V。其次,LAN8720A芯片的PHY地址由PHYAD0引脚的电平决定,拉低地址是0x00,拉高地址是0x01,很多模块出厂默认是0x00,但也有一部分做模块的厂家会把PHYAD0拉高导致地址变成0x01,这在软件配置时要格外留意。
另外,LAN8720A的NRST复位引脚不要直接悬空,最好由STM32的GPIO控制,上电后拉低一段时间再拉高,给PHY一个可靠复位,否则很容易出现SMI接口读不到寄存器、link一直起不来的问题。复位引脚其实不挑IO,哪个空闲用哪个,但一定要在软件初始化PHY之前完成复位动作。
3. CubeMX工程配置全流程实操
3.1 时钟树配置:48MHz和50MHz从哪来
打开CubeMX,新建工程选择STM32F407系列对应型号。RCC这里要把HSE设成Crystal/Ceramic Resonator,因为以太网和系统时钟都依赖外部高速晶振提供基准。进入Clock Configuration页面,目标是把主频配置到168MHz(F407最高主频),同时保证PLL48CLK是48MHz。这里有个隐藏的要求:STM32F407的USB和以太网外设都依赖48MHz时钟,如果PLL48CLK不对,以太网的DMA可能会出现莫名其妙的问题,甚至完全不通。
我的建议是直接在CubeMX的时钟树图形界面里把HCLK那栏输入168并回车,让CubeMX自动求解;然后仔细看右边有没有出现PLL48CLK=48MHz的提示。如果求解失败,多半是PLL分频比设置问题,手动调整下PLL_M、PLL_N、PLL_P、PLL_Q即可。RMII的50MHz参考时钟不经过STM32内部时钟树,它由PHY芯片提供,这点不需要在CubeMX里配置时钟源,但你要理解整个链条:LAN8720A的25MHz晶振经过内部PLL倍频到50MHz,再送到PA1引脚给STM32的RMII接口做参考时钟。
3.2 ETH外设参数与PHY地址配置
在Pinout & Configuration界面的Connectivity分类里找到ETH,打开后把Mode选为RMII。此时CubeMX会自动把前面表格里那组RMII引脚分配到对应的GPIO上。如果你的开发板引脚与CubeMX自动分配的不一致,可以直接在芯片引脚视图上手动点击并重新选择引脚,然后右键设为ETH功能,后生成代码时就会按你的设计生成。
ETH外设本身可配置的参数不多,最重要的是确认PHY Address。CubeMX在ETH Configuration里会有一个PHY Address选项,有些版本在LWIP配置界面里也有。对LAN8720A来说,绝大多数模块地址是0x00,你填0即可;少数模块是0x01,如果后面PHY ID读不到,第一个排查点就是这里。PHY Reset GPIO这项,新版CubeMX可以直接选择一个复位引脚,选中后生成代码会自动包含复位时序;如果你用的版本没有这个选项,也不慌,初始化代码里自己控制GPIO拉低拉高就行。
这里顺带说一句,CubeMX在lwIP相关代码生成时会带上LAN8742A的PHY驱动代码(lan8742.c和lan8742.h),名字虽然是LAN8742,但它和LAN8720A的寄存器体系基本一致,可以直接沿用。你不需要去网上找个专门给LAN8720A写驱动,ST官方的这套代码就能用,只要PHY地址对上,后面读写寄存器、检测link状态都会正常。
3.3 FreeRTOS和lwIP中间件的正确打开方式
在Middleware分类下,先打开FreeRTOS,Interface选CMSIS_V1或CMSIS_V2,这个根据你的HAL库版本习惯来,CMSIS_V2对应较新的封装,功能上差别不大。再打开LWIP,General Settings里的OS Mode一定要选FreeRTOS,否则生成出来的是裸机版本,和FreeRTOS整合时还得自己改信号量、邮箱适配,非常痛苦。
lwIP参数这里有几个关键项。IP Address我习惯配192.168.1.20,Netmask配255.255.255.0,Gateway配192.168.1.1,这样静态IP方便调试。如果打算用DHCP自动获取IP,需要另外把DHCP选项打开并在代码里调用dhcp_start,刚开始调试不建议直接上DHCP,固定静态IP更容易定位网络问题。Memory Settings里的默认值大参数在F407上一般够用,如果编译时报内存不足或者运行时报pbuf不足,再调整PBUF_POOL_SIZE或MEMP_NUM_TCP_SEG这些选项。
配置完全部中间件后,记得在Project Manager里设置好工程名和IDE类型(MDK-ARM或STM32CubeIDE),然后Generate Code。生成完先编译一遍,确保基础工程能过,再往下写代码。
4. 代码整合与网络功能验证
4.1 生成代码结构分析:每个文件是干什么的
打开生成的工程,你会发现多出了好几个和网络相关的文件。eth.c和eth.h是由HAL库封装的以太网MAC控制器初始化代码,MX_ETH_Init函数就在里面,负责配置MAC地址、DMA描述符、速度模式等。ethernetif.c是网卡底层收发接口,lwIP的low_level_init、low_level_output、low_level_input都在这里实现,相当于协议栈和硬件之间的桥梁。lan8742.c是PHY驱动,提供LAN8742_Init、LAN8742_GetLinkState这类函数,虽然名字是LAN8742,实际工作就是通过SMI接口读写PHY寄存器。lwip.c里是MX_LWIP_Init和MX_LWIP_Process,前者初始化协议栈,后者在轮询模式下处理网络数据包接收。
还有个容易忽略的点:CubeMX在生成了FreeRTOS相关文件后,会在main函数里调用MX_FREERTOS_Init来创建任务,但MX_LWIP_Init默认不会自动执行,需要你手动安排调用时机。根据我的实践,最推荐的做法是在进入调度器之前调用MX_LWIP_Init,也就是在main函数里MX_FREERTOS_Init之后、osKernelStart之前调用。这样lwIP的tcpip_thread会在FreeRTOS启动后就开始跑,不需要额外写一个专门的初始化任务。
4.2 在FreeRTOS里规划任务与lwIP收包机制
到这一步,你需要在freertos.c里创建两个基础任务。第一个任务是网络链路监测,每隔500ms检查一次PHY的link状态,打印网线是否连接、协商速率等信息;第二个任务是TCP回环服务,启动一个TCP server监听端口,收到什么数据就原样返回,方便验证网络通路。如果你的应用是HTTP Server或者MQTT客户端,套路也是一样的,无非是任务里的业务逻辑换成对应协议。
lwIP在FreeRTOS模式下的收包机制是:底层网卡在接收到数据帧后,DMA把数据放到内存缓冲区,然后协议栈的任务定期调用MX_LWIP_Process来处理。因此需要在某个FreeRTOS任务的主循环里循环调用MX_LWIP_Process,不能像裸机那样放main的while里。我习惯单独开一个优先级略低的网络任务专门做这件事,这样即使网络流量大,也不会把高优先级的控制任务堵死。任务栈大小建议给到256 words以上,lwIP协议栈处理数据包时会使用栈空间,栈太小容易进HardFault。
这里有个整合时的关键细节:lwIP的tcpip_thread、ethernetif_input相关线程在FreeRTOS里运行,而eth的HAL库中断回调可能会访问一些共享变量。如果遇到数据收发不稳定,记得用临界区或信号量保护关键资源的访问,不要想当然觉得DMA和协议栈各跑各的没事。
4.3 编译下载与Ping通全程记录
编译烧录后,先用串口把调试信息打出来,确认PHY芯片ID能读到。LAN8720A的PHY ID通常是0x0007C0F1,能读到这个数,说明SMI通路正常,PHY地址没问题。如果读到0xFF或者0xFFFF,先检查PHY地址和复位时序。
把网线一头插到开发板的网口,另一头插到电脑或路由器上,这时串口应该会打印出link up的信息。接着给电脑的以太网卡配一个同网段的静态IP,比如192.168.1.10,然后ping 192.168.1.20。如果通了,TTL值一般显示64,说明整个以太网通路已经打通。如果这时候你发现ping的延迟偶尔很高,或者时通时断,先检查网线和交换机端口,再考虑RMII信号线是不是过长、GPIO有没有配置成复用推挽等问题。
TCP回环测试就更直接了,电脑上用网络调试工具连接192.168.1.20的7号端口,随便发一串字符串,能原样收到就说明TCP协议栈跑通了。到这一步,整个移植工作就算完成了,后面你在这个框架上加HTTP、MQTT、TCP自定义协议,都是在这个任务基础上填业务逻辑而已。
5. 踩坑记录与问题排查大全
5.1 常见问题速查与解决对照表
这几年做F407以太网项目,真正让我卡住的问题翻来覆去就是那几类。这里整理成一张速查表,按现象排了一下优先级。
| 故障现象 | 可能原因 | 解决办法 |
|---|---|---|
| PHY ID读不到(0xFF/0xFFFF) | PHY地址配置不对 | 尝试把PHY Address从0改为1,或反过来 |
| PHY ID读不到(0xFF/0xFFFF) | 复位不完整 | 确认复位GPIO拉低时间至少100μs,后拉高 |
| PHY ID读不到(0xFF/0xFFFF) | MDC/MDIO引脚电压异常 | 检查MDIO上拉电阻、接线正确性 |
| 网线插了但link一直down | REF_CLK没起来 | 示波器测PA1有无50MHz时钟 |
| 网线插了但link一直down | PHY地址不对导致自协商状态读不到 | 改PHY地址,重新初始化 |
| ping通但延迟巨大或丢包 | 网线质量问题或距离过长 | 换一根标准超五类及以上网线测试 |
| ping通但延迟巨大或丢包 | RMII信号线走线过长,干扰大 | 硬件上缩短走线,软件锁定10M全双工 |
| 编译通过但运行进入HardFault | FreeRTOS任务栈过小 | 网络相关任务栈加大到512 words |
| DHCP获取不到IP | 静态IP没关 | 确认lwIP里IP地址已设为0.0.0.0 |
| DHCP获取不到IP | MDIO不正常导致link状态判断失败 | 回退到静态IP,优先定位硬件通路 |
| 裸机版本正常,FreeRTOS版本不正常 | OS Mode没有选FreeRTOS | 重新在LWIP General Settings里设置OS Mode |
5.2 进阶避坑经验与调试建议
第一次调PHY时,不要上来就怀疑lwIP配置,先把裸PHY的寄存器层打通。比如在初始化代码里读一下寄存器1(BMSR),如果Bit5是1,说明link已经协商上了,这时候ping不通就要查MAC侧;如果Bit5是0,说明物理层都没通,你折腾协议栈都是白费功夫。我习惯在串口初始化后、lwIP启动前加一段测试代码,把PHY ID、BMSR寄存器的值打印出来,这对判断故障域非常有效。
另外,LAN8720A模块从淘宝买来,不同批次甚至不同店铺的PHY地址都可能不同,千万别想当然,我踩过最疼的一次就是换了个模块后整块板子“网络消失”,折腾了两天才发现新的模块把PHYAD0拉高了,地址从0变成了1。所以固定一个初始化的检查逻辑,PHY ID不对时自动尝试另一个地址,能省不少调试时间。
关于时钟还有个容易犯迷糊的地方:有些F407核心板或以太网模块上用的是50MHz有源晶振,PHY的参考时钟由晶振直接供给,不需要从PA1再回传。这种情况下CubeMX只管RMII配置,PA1作为REF_CLK输入就必须和外部时钟连接,不能空着。说实话,这两种方案我都在项目里见过,你手上的板子具体是哪种,最好看一眼原理图再决定怎么接线,不然很容易出现“明明外部有晶振,但PA1没接信号,STM32根本没拿到参考时钟”的困境。
5.3 从Link Up到稳定通信的调试节奏
如果你已经读到PHY ID,链路也已经link up,但上层ping还是不通,这时候建议按顺序排查MAC地址配置、DMA描述符、lwIP内存池。MAC地址在eth.c里有个静态数组,一般默认是ST的地址,网络通信其实不太在意MAC是否唯一,但如果撞到了网络里其他设备的MAC,就可能产生地址冲突。DMA描述符这块,CubeMX生成的代码是没问题的,但我见过有人动了编译器优化等级或者修改了内存段布局,导致描述符没有对齐,问题表现就是接收中断偶尔触发、数据包丢失严重。
调试节奏上我个人喜欢分三步走。第一步,只验证PHY,读寄存器能把ID和link标志读出来,物理层就算过了。第二步,验证裸MAC通没通,把网卡回环或直接用lwIP的回环测试接口做loopback。第三步,才把FreeRTOS任务调度和业务逻辑加进来。这样做的好处是每次引入的变量少,出了问题能在最小范围内定位,而不是打开调试器看到一堆不知所错的寄存器。
顺带分享一个小技巧:在FreeRTOS + lwIP这种多优先级的代码里,用串口打印调试信息时别直接在中断上下文里做,否则实时性会很难看。我是专门用一个调试任务,其他任务用队列把调试字符串丢进去,由调试任务统一打印,这样既不阻塞主流程,又能保证串口输出不会互相抢占。
做完整套LAN8720A驱动与FreeRTOS整合后,我的体会是这套方案真正的难点不在“把代码跑通”,而在“把底层的信号、时钟、PHY地址这些物理层细节抠明白”。前面可能因为一个地址不对折腾一整天,但当你把这些链路全部摸熟之后,后面再在这个基础上做HTTP服务器、MQTT上云、固件升级,都是很顺理成章的事。最后再把这套经验的边界补一句:不同厂家的PHY芯片寄存器体系会有差异,比如DP83848和LAN8720A的关键寄存器地址就不完全一致,换芯片时不能直接套驱动,但这个排查思路是通用的。