ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F407 LWIP以太网移植全攻略:ETH初始化到稳定ping通

STM32F407 LWIP以太网移植全攻略:ETH初始化到稳定ping通

简介:针对STM32F407微控制器的以太网开发资源包,面向嵌入式开发者、物联网及工业控制相关学习者。压缩包围绕LWIP轻量级TCP/IP协议栈,提供从以太网MAC配置到网络通信的完整示例工程,适合需要快速上手STM32网络功能的初中级开发者。资源共2000个文件,以1041个.h头文件和881个.c源文件为主,覆盖标准外设驱动、LWIP协议栈移植及ETH_LWIP例程源码,另有78个txt说明文档辅助阅读,整体体积28.86MB,目录结构清晰便于分类检索。目前已有447人学习下载,内容涉及网络接口初始化、MAC地址设置、PHY连接、TCP与UDP连接建立等关键步骤,结合STM32F407自带以太网MAC与浮点运算能力,可帮助读者掌握在Cortex-M4平台上构建基于LWIP的稳定网络应用,并进一步实现HTTP服务器、远程数据交互等场景。

1. 解开名为STM32F407固件库-ETH—LWIP以太网的压缩包:一份工程骨架的完整解剖

人们第一次拿到"STM32F407固件库-ETH—LWIP以太网.zip"时,通常都有一个共同误会:以为解压就能直接跑起网络。真正打开后,会发现里面是一个既支持裸机、又能挂RTOS的工程骨架——ETH外设驱动、LWIP协议栈、网络接口层全都在,但每个都需要你自己激活和拼装。

这项技术要解决的是STM32F407这颗带MAC控制器但不带PHY芯片的MCU,如何接上RJ45网口、跑通TCP/IP协议栈,实现真正的网络收发。它适合做物联网网关、局域网数据采集节点,或者想在一颗MCU上同时承担业务逻辑和网络通信的开发者。本文要讲清楚三件事:ETH外设到底怎么初始化、LWIP移植需要改哪些文件哪些参数、以及为什么你调了一整天网口还是不通。读完你就能照着步骤把板子ping通。

2. ETH外设与PHY物理层:先搞清RMII接口和时钟来源这两个前提

2.1 为什么我推荐F407上用RMII而不是MII

STM32F407的ETH外设同时支持MII和RMII两种MAC到PHY的接口方式,它们的差别首先体现在管脚数量上:MII需要16根数据线加上控制信号,RMII只需要7根。对于LQFP100甚至LQFP144封装的F407来说,RMII能省出将近10个GPIO给其他外设用,所以绝大多数实际产品选择RMII。

但RMII有一个硬性约束:收发时钟必须由外部50MHz时钟源提供,且MAC侧参考时钟来自PHY芯片的CLKOUT引脚。常见接法是在PHY芯片的XI/XO引脚放一颗25MHz晶振,由PHY内部PLL倍频到50MHz后,从CLKOUT输出给F407的PA1(ETH_RMII_REF_CLK)。这里有个新手最容易搞反的点:F407的RMII参考时钟是输入方向,不是输出,MAC自己产生不了这个时钟。

初始化ETH之前还有一层容易忽略的功课:GPIO的复用功能映射和时钟开关顺序。比如F407的以太网引脚几乎固定——PA1(RMII_REF_CLK)、PA2(MDIO)、PA7(RMII_CRS_DV)、PC1(RMII_MDC)、PC4(RMII_RXD0)、PC5(RMII_RXD1)、PB11(RMII_TX_EN)、PB12(RMII_TXD0)、PB13(RMII_TXD1),想换引脚基本没门。

2.2 ETH初始化时的GPIO复用、MAC配置与DMA描述符

硬件连接确认之后,软件第一步是把ETH相关GPIO配置为复用功能。这里我给出一个最小配置,包含了时钟、引脚和复用映射:

GPIO_InitTypeDef GPIO_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOC, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_ETH_MAC | RCC_AHB1Periph_ETH_MAC_TX | RCC_AHB1Periph_ETH_MAC_RX, ENABLE); GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; /* PA1: RMII_REF_CLK, PA2: MDIO, PA7: RMII_CRS_DV */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_7; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_PinAFConfig(GPIOA, GPIO_PinSource1, GPIO_AF_ETH); GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_ETH); GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_ETH);

这段代码的逻辑是:先打开三个GPIO组的时钟和ETH外设时钟。注意RCC_AHB1Periph_ETH_MAC、ETH_MAC_TX、ETH_MAC_RX这三个位必须同时置位,否则MAC寄存器能访问但DMA传输时钟没起来,数据根本发不出去。GPIO配置成复用推挽模式、速度100MHz,再用GPIO_PinAFConfig指定复用功能为ETH。

接下来分配DMA描述符和缓冲区。F407的ETH DMA用链表管理收发描述符,常见做法是TX配4个描述符、RX配6个,每个缓冲区大小设在1.5KB到2KB,要能装下一个完整以太网帧:

ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB] __attribute__((aligned(4))); ETH_DMADescTypeDef DMATxDscrTab[ETH_TXBUFNB] __attribute__((aligned(4))); uint8_t Rx_Buff[ETH_RXBUFNB][ETH_RX_BUF_SIZE] __attribute__((aligned(4))); uint8_t Tx_Buff[ETH_TXBUFNB][ETH_TX_BUF_SIZE] __attribute__((aligned(4)));

这里最关键的是__attribute__((aligned(4)))。DMA描述符的起始地址低两位必须为0,否则ETH DMA会直接报总线错误,而且这种错误极其隐蔽——你只会发现ETH_GetRxPktSize()的返回值一直是0。很多移植失败案例最后都查到这里。

2.3 PHY芯片选型与地址跳线:一个让初始化失败率最高的点

F407的MAC层只到RMII/MII接口,PHY芯片得单独选。常用PHY有LAN8720A、DP83848、KSZ8081这几颗,寄存器基本兼容IEEE 802.3,但读取PHY ID的时序和复位要求略有差别。固件库里通常带了一个PHY驱动文件,初始化时通过MDIO总线读取PHY ID寄存器来确认芯片是否存在。

PHY地址是由芯片的地址引脚上的上下拉电阻决定的。常见PHY地址是0x00或0x01,但很多第三方板子出厂默认拉到0x01,而固件库例程里写死的是0x00,结果必然是读不到PHY状态。另一个高频翻车点:PHY的复位引脚没有接或者复位时序不对,芯片一直处于复位态,MDIO读取自然失败。后面避坑章节会细说这个血泪经验。

3. LWIP协议栈移植:从解压到ping通要过的四道关

3.1 lwipopts.h、ethernetif.c、sys_arch.c:真正需要动的文件就这三个

很多单片开发者第一次打开LWIP工程,看到几十个.c文件会发懵。实际上LWIP源码分成四块:core(核心协议栈,TCP/IP实现)、netif(网络接口层)、api(socket或netconn API封装)以及底层驱动适配。绝大多数情况下你不需要改动core下的任何文件,真正需要关心的是三个文件。

第一个是lwipopts.h,LWIP的全局配置头文件,内存池大小、协议开关、超时时间全部在这里。第二个是ethernetif.c,网络接口底层驱动,负责把ETH DMA收到的帧交给LWIP,把LWIP要发的帧塞进DMA描述符。第三个是sys_arch.c,如果上了RTOS,这里做信号量、邮箱、互斥锁的适配;裸机则提供简单的超时实现。

移植第一步是打开lwipopts.h,决定LWIP_NO_SYS这个宏。裸机设为1,LWIP跑在单线程轮询模式;有RTOS设为0,LWIP跑在专网网络线程里。这个决定直接影响后续所有代码结构。

3.2 裸机轮询与RTOS线程:两种跑法各自的适用边界

裸机轮询模式最简单。主循环里反复调用网卡轮询函数,把收到的帧喂给LWIP,同时周期调用sys_check_timeouts()处理TCP超时重传。优点是代码直观、不依赖OS,适合业务逻辑简单的设备。缺点是网络数据量大时,主循环里的其他任务会拖慢收包速度;反过来业务代码执行太久,DMA接收描述符会被新帧覆盖丢包。

最常见的可靠配置是给LWIP开一个独立线程,优先级中等偏上,主循环只管业务。LWIP内部用sys_sem_t和sys_mbox_t完成线程间通信,ETH中断服务里只做一件事——置位信号量唤醒网络线程,收包和协议栈处理全部在线程上下文完成。这样既保证低延迟,又不阻塞业务。

void ETH_IRQHandler(void) { if (ETH_GetITStatus(ETH_IT_RX)) { ETH_ClearITPendingBit(ETH_IT_RX); osSemaphoreRelease(RxSemaphore); /* 唤醒网络线程 */ } }

中断里只释放信号量,不碰数据,这是避免丢包的关键设计。数据拷贝和协议栈处理放到网络线程里,即使被打断也不会丢描述符。

3.3 最小可跑的初始化顺序:从SystemInit到LwIP_Init

从解压到ping通,初始化顺序一般是:先配时钟和GPIO,再初始化ETH外设和PHY,然后创建LWIP内核并挂载网卡,最后进主循环。核心代码如下:

int main(void) { SystemInit(); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); ETH_BSP_Config(); /* GPIO、PHY、MAC、DMA全部初始化 */ LwIP_Init(); /* LWIP内核初始化,挂载网卡 */ while (1) { LwIP_Pkt_Handle(); /* 裸机轮询收包 + 周期超时处理 */ My_App_Process(); } }

ETH_BSP_Config()内部完成第2章描述的GPIO复用配置、PHY复位和地址检测、MAC地址写入、DMA描述符初始化。LwIP_Init()中会创建网卡、分配IP、挂载netif。以静态IP为例:

ip_addr_t ipaddr, netmask, gw; IP4_ADDR(&ipaddr, 192, 168, 1, 100); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gw, 192, 168, 1, 1); netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &tcpip_input); netif_set_default(&gnetif); netif_set_up(&gnetif);

netif_add把网卡挂进协议栈,ethernetif_init是网卡初始化回调,tcpip_input是带OS时收包交给协议栈的回调。裸机模式下不需要tcpip_input,数据直接在轮询函数里通过netif->input交给协议栈。ping通之后只能说明链路通,离稳定运行还差四个参数的调优。

4. 四个必须手调的LWIP参数:内存、窗口、校验和卸载与超时

4.1 MEM_SIZE和PBUF_POOL_SIZE:内存池写多大才不会让系统起不来

LWIP内存管理分两种:内存池(memp)和内存堆(mem)。MEM_SIZE控制内存堆总大小,PBUF_POOL_SIZE控制PBUF池的数量,每个PBUF默认大小由PBUF_POOL_BUFSIZE决定。PBUF是LWIP里描述数据包缓冲的结构,收发路径都要用。

对F407这种192KB RAM的芯片,如果还能跑点业务逻辑,MEM_SIZE给16KB到24KB比较合适,PBUF_POOL_SIZE给16到32个。给太小,TCP收发窗口一满就丢包;给太大,RTOS堆被挤占,任务创建直接失败。LWIP的内存是从静态数组里切的:

static u8_t mem_memory[MEM_SIZE] __attribute__((section(".bss.lwip")));

MEM_SIZE改大时这个数组也跟着变大,RAM不够时链接器直接报错。实际翻车现场往往是:调完LWIP后RTOS任务栈建不出来了,因为系统内存被LWIP吃掉了大半。

4.2 TCP_WND与TCP_SND_BUF:吞吐量瓶颈常常在协议栈参数而不是PHY

TCP_WND是接收窗口,决定对端能一次性发多少数据不等ACK;TCP_SND_BUF是发送缓冲,决定本地缓存多少待发数据。LWIP默认值都很保守,一般只有2KB左右,对F407的百兆网口来说这远远不够。

做文件传输应用时,建议把TCP_WND和TCP_SND_BUF调到8KB到16KB,同时把MEM_SIZE适当扩大。一个很典型的案例:某开发者用F407传文件,抓包发现对端发送窗口始终只有1460字节,速率一直上不去,排查半天发现是tcpip_thread的栈空间不够、PBUF池被占满,导致窗口协商降级。记住一个规律:接收端窗口小于报文段大小时,TCP协商结果会退化到很保守的数值,速度自然拉不起来。

4.3 硬件校验和卸载:把TCP/UDP校验和计算交给F407的MAC

F407的ETH外设硬件支持TCP、UDP、ICMP校验和的生成和校验。把这项工作卸载到硬件能省一部分CPU,对频繁收发大流量的场景有明显帮助。在lwipopts.h里这样开:

#define CHECKSUM_GEN_IP 1 #define CHECKSUM_GEN_UDP 1 #define CHECKSUM_GEN_TCP 1 #define CHECKSUM_CHECK_IP 1 #define CHECKSUM_CHECK_UDP 1 #define CHECKSUM_CHECK_TCP 1 #define LWIP_CHECKSUM_ON_COPY 0

GEN系列宏让发送时把校验和计算交给硬件描述符控制,CHECK系列让接收时由硬件验算。但有一个坑:开启硬件校验和卸载后,如果PHY接口配置错误或描述符控制字没设置好,PC端收到的数据包校验和会全部错误。

Wireshark抓包是最好的验证手段——看TCP校验和栏是否标记为correct。如果标记为incorrect或checksum offload error,大概率是接收方向也错误地开启了卸载,而接收方向硬件需要MAC正确解析帧边界后才做校验,这里依赖RMII时钟的稳定性。建议发送和接收分开调,先只开GEN,确认对端收到正确校验和,再开CHECK。

4.4 LWIP超时机制与TCP_MSL:短连接场景端口耗尽怎么救

LWIP的sys_timeout机制管理协议栈里所有定时事件:ARP老化、TCP重传、TCP的TIME_WAIT超时。裸机模式下必须在主循环里定期调用sys_check_timeouts(),否则TCP连接永远无法从TIME_WAIT进入关闭状态,端口会被占用殆尽。

TCP_MSL默认60秒,2*MSL就是120秒的TIME_WAIT时间。如果做的是大量短连接高频通信,端口会快速耗尽。症状很典型:连接建立后立刻超时失败,抓包看SYN已经发出但ACK不回来,因为四元组还在TIME_WAIT里没释放。这种情况建议把TCP_MSL调到30秒,同时检查是否支持快速复用TIME_WAIT连接,很多场景下这比单纯扩大端口数有效得多。

5. 移植避坑:六个让以太网跑不起来的真实翻车原因

5.1 PHY地址不匹配,读寄存器全是0xFFFF

现象:程序初始化PHY时,读取PHY ID或状态寄存器返回0xFFFF,ETH_Init返回错误码。这是整个移植过程中最常见的一道坎。

原因:PHY芯片的地址引脚上下拉与代码里宏定义不一致。例如某开发板PHY_ADDR引脚接了上拉电阻,实际地址变成0x01,而固件库例程写死的PHY_ADDRESS是0x00。MDIO总线上没有设备响应,读回来的自然是一堆0xF。

解决:先用总线扫描方式把MDIO上0x00到0x1F所有地址都读一遍,找到能返回正确PHY ID的那个地址,再把PHY驱动里的地址宏改成它。注意有些PHY在复位后会有一段等待时间,初始化流程里必须加上延时。

5.2 RMII参考时钟不稳定,PHY状态寄存器的Link位一直为0

现象:PHY的ID能正常读出,寄存器也能访问,但链接状态位始终显示无链路,或者收发完全不通。

原因:RMII所有信号都与50MHz参考时钟同步。如果PHY从25MHz晶振倍频出来的时钟抖动过大,或者根本没输出到PA1,MAC侧采样不到有效数据。很多廉价板子用的普通晶振,低温下频率偏差会超过PHY容忍范围。

解决:用示波器量PA1引脚,确认频率稳定在50MHz且无间歇丢失。有条件的话跑一下高低温环境,因为晶振频偏在冷热冲击下最容易暴露。我见过一个案例,板子在常温下稳定运行,进了高低温箱网络直接瘫痪,就是一颗普通晶振惹的祸。

5.3 DMA描述符未对齐,ETH接收函数返回0或死机

现象:网络收发偶尔成功,偶尔完全无反应,程序随机跑飞。

原因:DMA描述符或缓冲区数组没有4字节对齐。F407的ETH DMA要求描述符起始地址低两位为0,普通数组定义不保证对齐。

解决:定义数组时强制加__attribute__((aligned(4))),或者把缓冲区放到专门的DMA段。排查方法也很直接:在初始化ETH之前打印描述符的地址,看最低两位是否为0。如果用的IAR或GCC,还要确认链接脚本的段分配没有覆盖对齐属性。

5.4 LWIP的PBUF池耗尽,ARP请求被直接丢弃

现象:ping网关有响应,但ping局域网内其他设备不通,ARP表偶尔建立偶尔失败,lwip_stats里能看到arp相关计数异常。

原因:PBUF池被TCP数据占满,ARP请求到来时LWIP分配不到PBUF,无法构造ARP应答。

解决:增加PBUF_POOL_SIZE,或者用lwip_stats查看当前池使用峰值。裸机下16个PBUF往往不够,保守改成32个。同时检查是否有代码路径在长时间占用PBUF不释放,比如发送缓冲堆积。

5.5 ETH中断与系统临界区冲突,表现为丢包和重传风暴

现象:ping包时通时断,TCP传文件时速率大幅波动,重传计数明显偏高。

原因:ETH中断和RTOS的临界区保护冲突——中断里做数据拷贝时被更高优先级中断打断,或者临界区关中断时间过长,硬件接收描述符被新帧覆盖。

解决:把ETH中断优先级设置在比系统时钟节拍高的位置,但低于某些极端时延敏感的中断;中断服务里只做信号量释放和字节计数更新,数据拷贝留给网络线程。同时用RTOS的锁保护共享的RX缓冲索引,不要裸访问。

5.6 网线插拔后应用不自知,数据一直外发但不通

现象:插拔网线后,应用程序不知道网络断了继续发数据,重新插上后长时间无法恢复通信。

原因:F407的ETH外设没有硬件级链路变化中断,多数实现只是主循环里周期读PHY状态寄存器,但应用层没同步获取状态。

解决:把PHY link状态轮询周期设在500ms到1s,检测到链路断开时调用netif_set_link_down(),恢复后调netif_set_link_up(),让协议栈感知链路变化。TCP连接如果已经超时断掉,应用层还要有重连或重启客户端的逻辑,不能指望协议栈自动恢复。

6. 裸机低内存下的LWIP精简配置:一个能实际落地的参数组合

如果你的F407只剩48KB甚至更少RAM给网络用,上面那些参数得换一种配法。我这里给出一套经受过实际考验的精简组合:MEM_SIZE压到12KB,PBUF_POOL_SIZE设8,PBUF_POOL_BUFSIZE缩到1518字节,保证单帧能装下、池子不深。

不需要的协议全部关掉:LWIP_IGMP、LWIP_DHCP、LWIP_SNMP、LWIP_DNS一律置0,能省掉一大块静态内存。只留ARP、ICMP、UDP和一个TCP服务器。TCP接收乱序队列也关掉——TCP_QUEUE_OOSEQ设为0,乱序包直接丢,等待重传。对实时控制类场景,这反而比乱序重排更合适,因为控制报文讲究即时性,等重传比等重排更可靠。

验证这组配置的方法很直接:先跑一个大文件TCP传输打满吞吐,观察丢包计数和重传计数;然后把程序放进温箱跑12小时,确认晶振漂移没有让PHY脱锁;最后做一次连续1000次插座拔插,确认链路状态管理逻辑没漏洞。

我个人的习惯是每次改完lwipopts.h就烧一版进去,把lwip_stats里几个关键计数器打出来对比一次,比对着协议栈代码猜状态靠谱得多。几轮调下来你会发现,裸机LWIP的上限不在协议栈,而在你对内存的克制程度。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进