STM32F407+DP83848+LWIP以太网移植实战与避坑指南
2026/9/9 18:05:16 网站建设 项目流程

简介:这份STM32F407+LWIP+DP83848移植例程,面向嵌入式网络开发初学者及需要快速搭建TCP/IP服务器原型的技术人员。例程在无操作系统环境下实现TCP/IP数据收发,未使用外部内存,仅需一块F407开发板和DP83848网络芯片即可运行,硬件门槛低,适合学习协议栈移植或产品预研。

资源包共3458个文件,压缩后22.92MB,以C源码(1646个)和头文件(1166个)为主,辅以汇编启动文件、Keil工程文件(uvproj/uvopt)、编译中间文件及说明文档,覆盖从工程创建、驱动适配到编译烧录的完整链路,可基于现有工程直接二次开发。

已有4748人浏览学习。包内说明文档梳理了无系统下LWIP初始化、PHY配置、网络接口驱动及回调收发流程,有助于避开常见移植坑点。对希望在不扩展外部存储的前提下快速实现以太网通信的STM32项目,提供了立即可用的工程基础和清晰代码结构,能显著缩短开发周期。 说句实话,网上STM32F407以太网移植的例程一抓一大把,但十套里有八套是给LAN8720写的。手头这块板子主控是STM32F407VGT6,PHY用的是TI的DP83848,LWIP协议栈版本还是经典的1.x风格。去年做固件升级,不得不把当年的移植例程从仓库里翻出来重新过一遍思路。这篇文章把我这次完整的移植过程、调通经验、还有几次“就差把网线吃了”的坑全部记录下来,给同样用这套组合的朋友一个能直接抄作业的参考。

这文章适合谁看?用STM32F4系列做以太网、PHY偏偏选了DP83848的开发者,以及拿到一个ETH例程却不知道怎么下手、ping不通不知道从哪查的初学者。我尽量不只给代码,而是把每个关键配置背后的原理讲清楚,这样你换板子、换PHY、改协议栈参数的时候也能自己推。

1. 为什么是DP83848:这套组合的真实定位

1.1 DP83848和LAN8720的核心差异

先说一下这套组合里最容易被人忽略的一件事:PHY选型不同,移植难度完全不同。LAN8720是RMII-only的PHY,引脚少,价格便宜,模块遍地都是。DP83848则是TI的老牌10/100M以太网PHY,支持完整的MII和RMII两种模式,还保留了不少工业级特性。以下是两款PHY在实际项目中最直观的对比:

对比项DP83848LAN8720
厂家TI(德州仪器)SMSC / Microchip
接口模式MII + RMII仅RMII
典型供电3.3V I/O,核心1.8V3.3V
工作温度范围工业级型号覆盖较广以商业级为主
25MHz晶体直连支持(MII/RMII均需外部时钟)支持(内部PLL倍频出50MHz)
抗干扰和EMI表现相对更稳一般
常见采购渠道工业渠道较多模块市场很多

这表格不是想把LAN8720说成一无是处,而是要说明一个事实:如果你的硬件工程师选型时用了DP83848,大概率是基于工业场景的稳定性考虑,而不是随手选的。

从LWIP移植的角度看,这两颗PHY在软件层面最大的区别是:DP83848的寄存器布局是标准的IEEE 802.3 PHY寄存器集,读0x00(BMCR)、0x01(BMSR)、0x02/0x03(PHY ID)就能拿到几乎所有状态信息。LAN8720也遵循标准,但它在CubeMX里因为模块普及率太高,几乎被当成了“默认PHY”,导致很多人的代码里写死的是LAN8720的寄存器特性和PHY地址,换到DP83848上就会出奇怪的问题。

1.2 MII还是RMII:这不是选择题,是原理图决定

很多初学者拿到例程第一反应是:“我要用MII还是RMII?”这个问题其实不该问软件,而是应该看硬件原理图。

MII模式需要大约16根信号线,数据线是4位收发并行,TX_CLK和RX_CLK都由PHY提供。RMII模式把数据线压缩到2位收发并行,所有时序统一到一个50MHz参考时钟上,引脚数量少得多,但代价是时序要求和时钟稳定性要求更高。简单类比:MII像8车道公路,路宽但红绿灯各自独立;RMII像2车道但全路段统一一个中心信号灯,省地皮但对时钟绝对精度很敏感。

DP83848在RMII模式下,XI/CLKIN引脚必须输入50MHz参考时钟。这个50MHz可以由外部有源晶振直接给,也可以由STM32的MCO引脚输出。而如果用MII模式,DP83848外接25MHz晶振即可。这两套时钟方案完全不能混用,你要是RMII模式但板上只有25MHz晶振,那PHY根本起不来,以太网链路永远处于down状态。

我建议拿到一块新板子,第一步就是看原理图:

  • 看DP83848的XI/XO怎么接的。如果XI是25MHz无源晶振,连着MX(OSC_OUT),多半是MII模式;如果XI接的是50MHz有源晶振或者来自MCU的某个引脚,那就要查RMII的REF_CLK通路。
  • 看TX_CLK、RX_CLK、TX_EN这些信号线的数量和连接。如果RXD只有两个引脚(RXD0/RXD1),那是RMII;如果RXD0~RXD3四个引脚都拉出来了,那就是MII。

1.3 硬件接线前必须确认的四件事

即使你用的是现成的开发板,我也建议上电前先确认这四件事,能省掉后面的大量调试时间。

第一,MCU的ETH引脚是否和PHY正确连接。STM32F407的ETH外设引脚是固定的,比如RMII模式下ETH_RMII_REF_CLK固定是PA1,ETH_RMII_CRS_DV固定是PA7,ETH_RMII_TXD0是PB12。这些引脚没有重映射选项,硬件上必须按数据手册接对。如果PCB上接了某个其他引脚,软件没办法改,只能修板子。

第二,MDIO管理接口不能漏。MDC和MDIO两根线(PC1和PA2)负责CPU和PHY之间的管理通信。没有这两根线,CPU读取不到PHY的状态,也配置不了PHY寄存器,后面所有PHY层的调试都无从谈起。

第三,PHY地址确认好。DP83848的PHY地址由PHYAD[0]引脚的电平决定,常见原理图把它拉高,地址就是0x01,也有拉到低的,地址是0x00。这个地址后面在CubeMX和代码里都要用,填错的话MDIO读写会返回无效值。

第四,复位信号。DP83848一般有一个硬件复位引脚接到MCU的GPIO,这个引脚必须在初始化ETH之前拉高释放,否则PHY一直处于复位状态,MDIO访问什么都不返回。我见过不止一次因为GPIO没配好导致PHY完全“消失”的例子。

2. CubeMX配置里的三个关键坑

2.1 ETH外设的打开方式

在STM32CubeMX里配置ETH本身不难,但有几个选项非常容易填错。先打开RCC,把HSE选为Crystal/Ceramic Resonator,然后左侧找到Connectivity下的ETH,勾选激活。

这里要特别留意:ETH配置界面里有一个PHY Address输入框。CubeMX默认可能是0x00,有的版本默认是LAN8742的PHY地址0x00。DP83848的地址根据原理图常见是0x01。你最好在代码里先写一个小函数把PHY地址几个常用值(0x00和0x01)都扫一遍,看哪个地址能读出正确的PHY ID寄存器值,然后用那个地址。

我自己的经验是:不要直接信CubeMX默认值,也不要直接信例程里写的PHY地址,通电后实测读PHY ID寄存器最可靠。

另一个容易忽略的是PHY接口模式。在ETH配置里,ETH Mode可以选MII或RMII,必须和硬件完全一致。选错了会怎样?现象就是PHY寄存器能正常读、链接看似也能起来,但收发数据时MAC侧状态异常,DMA描述符一直不更新,ping根本不通。因为MAC侧完全按另一种接口时序去采样数据了,对不上。

2.2 PHY参数和自协商

CubeMX的ETH高级配置里有一项可以选PHY的自动协商模式(AutoNegotiation),通常默认开启。DP83848支持标准的10/100M自协商,和交换机对接时一般都能协商出100M Full Duplex。

不过移植初期我不建议过度依赖自协商。如果你的调试环境是MCU直连电脑网口,且电脑网卡是千兆的,自协商可能会有兼容性问题。不同品牌的电脑网卡对百兆PHY的自协商处理方式略有差异。真遇到这种情况,可以先在PHY寄存器里强制设置为100M Full Duplex,两边调成一致,先把链路通起来,再去研究自协商。

DP83848支持通过软件把BMCR寄存器改成强制模式:bit 13 是Speed选择,bit 8 是Duplex Mode,bit 12 是Auto-Negotiation Enable。把自协商关闭,强制写0x2000(100M全双工)是一个很常见的调试手法。

2.3 50MHz参考时钟:时钟树里最容易漏的配置

前面提到RMII模式需要50MHz REF_CLK。如果板上是用STM32的MCO1(PA8)输出50MHz给DP83848,那CubeMX时钟树里的配置就必须额外注意。

具体来说,你要在Clock Configuration界面里把MCO1的时钟源和分频系数设对,让PA8上实际输出50MHz。很多人在这里漏了,生成代码后PHY直接没有参考时钟,RMII接口完全瘫痪。这个问题的典型特征是:PHY寄存器地址能扫到,BMSR读出来speed/duplex也有值,但RMII上就是没有任何数据活动。

检查方法很简单:用示波器或逻辑分析仪看PA8引脚有没有50MHz方波。不方便上仪器的话,可以在代码里读一下MCO配置寄存器,确认HAL_RCC_MCOConfig被正确调用。

如果板上是外部50MHz有源晶振直接给PHY,那STM32这边就不需要配MCO,反而可以省掉这个坑。

3. LWIP接入:从生成代码到跑通的第一条ping

3.1 生成代码里改动最集中的文件

CubeMX生成LWIP工程后,需要关注的文件主要就三个:ethernetif.clwip.c(或lwip.h里生成的初始化函数)和lwipopts.h

先说ethernetif.c里的low_level_init()函数。这个函数负责初始化以太网DMA描述符、设置MAC地址、启动PHY。生成代码里的MAC地址默认是一组预置值,比如00:00:00:00:00:02之类,量产时一定要改成自己设备实际的MAC地址。MAC地址由前3字节OUI和后3字节设备唯一标识组成,如果项目没有申请OUI,可以从前3字节用常用的私有地址段,比如02:00:00开头,这个段不会被公网设备路由,适合本地网调试。

low_level_init()里还包含了对PHY的访问。CubeMX生成代码通常只做了基础的PHY软件复位和等待自协商完成。但它等待的方式是一个死循环轮询HAL_ETH_ReadPHYRegister读取BMSR寄存器,直到Link status bit置位。这里有个隐患:如果PHY地址填错、或者网线没插好,这个循环会死等。量产固件里我一般会加超时退出,避免单板卡死在初始化。

3.2 内存参数与描述符数量

LWIP是个轻量级协议栈,但它在内存的使用上绝对不是“白嫖”。有两个内存参数对STM32F407来说特别关键:MEM_SIZEPBUF_POOL_SIZE

MEM_SIZE控制的是协议栈内部堆的大小,用于各种动态内存分配,比如TCP连接控制块、报文重组缓冲区等。PC上不太敏感,但在MCU上,RAM总共就那么大,给LWIP分多了应用就少,分少了协议栈容易分配失败。默认值1600在简单UDP收发场景勉强够了,但只要跑TCP,尤其是同时开多路连接,很容易在运行时出现“memory allocation failed”之类的错误。

我在这块板子上的配置是这样的:

#define MEM_SIZE (6 * 1024) #define PBUF_POOL_SIZE 32 #define TCP_WND (4 * TCP_MSS) #define TCP_SND_BUF (4 * TCP_MSS) #define NO_SYS 1 #define LWIP_DHCP 1

PBUF_POOL_SIZE控制的是接收DMA直接使用的PBUF池数量。DMA收到一个包,会放到一个PBUF里再交给协议栈。池子太小,在高负载下会丢包。但池子太大,每个PBUF默认是MEM_ALIGNMENT对齐的,也吃RAM。32个对于常见的Modbus TCP或自定义协议完全够用。

还要注意STM32F407的ETH DMA描述符数量。CubeMX的ETH配置里可以设置RX和TX描述符个数,一般各4个就够用。描述符数组必须4字节对齐,生成代码里已经加了ALIGN_32BYTES属性,这个不要动。

3.3 RX/TX回调链:到底是谁在驱动LWIP收包

STM32F407以太网有两种收包模式:轮询和中断。我强烈建议用中断。

中断模式下,数据流是这个链路:

ETH全局中断 -> HAL_ETH_IRQHandler(&heth) -> HAL_ETH_RxCpltCallback(&heth) -> ethernetif_input(&g_netif) -> netif->input -> tcpip_input

在裸机(NO_SYS=1)时,ethernetif_input里面会调用netif->input,最终把PBUF递交给LWIP协议栈处理。

这个链路上最容易被改坏的地方在HAL_ETH_RxCpltCallback。生成代码里这个回调默认是空的,你要在里面补上ethernetif_input调用。补上之后,收包中断就驱动了协议栈的输入路径。

发数据则反过来:应用层调用netif->outputtcp_write后,LWIP通过low_level_output把数据交给DMA描述符,最后写DMAC寄存器启动发送。

所以整个数据通路由三件事驱动:中断回调接收、netif的回调发送、系统滴答定时器驱动ARP和TCP定时器。任何一环断了,表现就是“不发不收”或“只能发不能收”。

4. 上电联调:ping不通的完整排查链路

4.1 先确认MDIO通路:PHY寄存器能读出有效值

网线插上、程序烧进去、ping不通,第一件事千万不要去改LWIP参数,而是先确认MDIO通路通不通。

MDIO是串行管理接口,STM32通过MDC和MDIO两个引脚访问PHY内部寄存器。如果PHY地址不对或硬件连接有问题,读寄存器往往会返回0xFFFF,这个值看起来像是芯片不存在。先用下面这个思路写一个扫描函数:

  1. 把PHY地址从0到31全扫一遍。
  2. 对每个地址读寄存器0x02(PHY IDR1)和0x03(PHY IDR2)。
  3. 打印出来,看哪组地址能读到非0xFFFF、非0x0000的有效值。

DP83848的有效PHY ID读出来不会是空值,这一点确认了,说明MDIO线路、PHY供电、复位都是正常的。如果扫描不到任何地址,优先级依次查:PHY供电有没有上、复位引脚有没有被拉低、MDC/MDIO的GPIO配置和实际硬件是否一致。

4.2 链路层状态:link up和自协商

寄存器通路确认后,下一步看链路。PHY的BMSR(寄存器0x01)的bit 2是Link Status位。读出来为1,表示PHY和交换机/电脑网卡之间的物理链路已经建立。

还有两个值得看的位:BMSR的bit 5表示自协商完成,bit 6表示远端支持自动协商。如果自协商没完成,即使Link Status为1,MAC侧也可能用错误的速度/双工模式发数据。

STM32F407的MAC需要知道当前网速是10M还是100M,全双工还是半双工。这个信息不是自动从PHY拿到的,而是软件读PHY寄存器后写入MAC配置寄存器的。具体来说,要调整ETH MACCR寄存器的FES和DM位,以及ETH MACFFR等配置。HAL库的HAL_ETH_ConfigMAC函数提供了这个能力。CubeMX生成的代码里通常已经把这部分处理好了,但如果你自己改过MAC配置或移植过其他PHY,这里非常容易漏。

调试方法:串口打印PHY的BMSR和ANLPAR(寄存器0x05)的值,看协商结果。如果显示100M全双工,那链路层通常是健康的。假如Link Status始终为0,优先怀疑网线质量、变压器连接、RJ45座子焊接。

4.3 网络层状态:MAC地址、IP配置和ARP

链路通了,下一步到网络层。ping命令第一步是ARP请求,如果ARP层不通,ICMP根本走不到。

先在STM32端确认三件事:

  • netif是否已经set_up。LWIP里netif_set_up之后,协议栈才会把该网口当成可用接口,否则发了ARP也不会回。
  • MAC地址是否和实际期望一致。用串口把netif->hwaddr打出来,确认不是全零或者乱值。
  • IP、掩码、网关是否配置正确。初期调试建议直接用静态IP,比如192.168.1.10,电脑网卡设成192.168.1.20,同网段。不要一上来就用DHCP,DHCP又慢又容易引入变量。

这里有个很多人踩过的坑:MAC地址前3字节如果设成00:00:00或者全FF,交换机和电脑网卡可能直接丢弃这些帧。我自己习惯把MAC地址设为02:00:00:xx:xx:xx,第二位为零表示本地管理地址,大部分交换机会放行。

4.4 终极手段:Wireshark抓包定位哪一层断了

软件层面查不出问题时,Wireshark是最能说明问题的工具。把网线插到电脑,开Wireshark抓包,然后从STM32端发几个ping,观察以下几种典型情况。

  • 情况一:Wireshark里只能看到电脑发出的ARP广播,看不到STM32的任何回复。这种情况说明ARP请求根本没有到达LWIP,或协议栈没正确处理。方向往底层查:DMA接收描述符有没有更新、ethernetif_input有没有被调用、网卡中断有没有进。
  • 情况二:ARP请求和回复都有,但ping的ICMP Echo Request发出去后没有Reply。这种情况说明IP层通了,问题可能出在ICMP处理或者路由回包上。检查LWIP是否配置了ICMP,以及回包路径上的源IP是不是正确的。
  • 情况三:Wireshark里STM32的包偶尔带CRC错误或FCS错误。这个多发生在RMII时钟不稳定或接地不良的板子上,可以再回头检查50MHz参考时钟的波形质量。

抓包这一招极其管用,它能把问题直接隔离到OSI模型的某一层,省得你自己猜。

5. 实测性能与长期运行的几个细节

5.1 实测吞吐量和延迟

在这块STM32F407VGT6 + DP83848 + 裸机LWIP的组合上,我实测的静态IP ping延迟基本在1到2毫秒之间,丢包率为0。吞吐量的上限取决于内存配置和协议栈效率,大致情况是:UDP单向发送,数据长度1440字节,在100M以太网下能达到6MB/s左右;TCP传输因为要等待ACK,实际吞吐会略低一些,但如果把TCP_WNDTCP_SND_BUF调大,也能接近UDP的水平。

这里给个经验值:如果你的应用是Modbus TCP这类小包交互协议,对吞吐量不敏感,内存配置可以保守一点,把更多RAM留给应用。如果你要传文件或者图像,TCP窗口不调大的话,即使网卡是100M,实际传输速度也会非常难看。

5.2 长时间运行的稳定性注意事项

项目跑起来之后,稳定性才是真正考验。我这套代码在连续跑了几十天之后,遇到过几个问题,这里列出来给你参考:

第一,LWIP内存碎片。裸机LWIP下,mem_malloc经常分配和释放小块内存,长时间运行会产生碎片。尽量避免在大循环里频繁调用mem_malloc,能用pbuf_alloc(PBUF池)就别用堆内存。另外TCP_PCB_MAX等限制连接数的宏会预分配控制块,如果连接频繁建立和断开,要注意连接数是否够用。

第二,DHCP租约。如果用了DHCP,租约时间到了之后需要续约,否则IP可能失效。MCU上跑的LWIP DHCP续约逻辑依赖LWIP的定时器节拍,要确保sys_check_timeouts()这类定时处理函数被周期调用,比如放在主循环里每250ms调用一次。

第三,中断优先级。ETH中断优先级如果比SysTick中断优先级低,收包处理可能会被其他中断长期抢占,造成DMA描述符无法及时释放而丢包。我一般把ETH中断优先级设为4或5(数值越小优先级越高),同时给被打断的处理函数减少耗时操作,绝不在中断回调里做耗时的应用逻辑。

第四,复位时序。DP83848的复位释放后,建议等至少1毫秒再操作MDIO。有些代码上电后立刻读PHY寄存器,读回一堆垃圾值,就是这个时序问题。

5.3 一个提升排查效率的小技巧

最后分享一个我调试以太网时离不开的懒人技巧:写一个简单的串口命令,把PHY所有核心寄存器(0x00到0x05)、MAC配置寄存器、DMA状态寄存器的值都打印出来。一旦出现链路异常,不管现场是嵌入式工程师还是测试人员,只需要把这串寄存器值发给我,我就能快速判断是PHY层没协商好,还是MAC配置被改乱,还是DMA挂死了。

这套方案我已经用了很多年,比任何调试器都直接。你把这套东西做成调试命令或者日志接口,后续产品出问题时的定位速度会快很多。

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

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

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

立即咨询