ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32以太网开发实战:CubeMX+LwIP+PHY芯片踩坑记录

STM32以太网开发实战:CubeMX+LwIP+PHY芯片踩坑记录 1. 为什么2024年还在折腾STM32以太网先说背景。今年手头一个设备项目要加联网功能工业现场既没有Wi-Fi也不方便上4G模块唯独每个机柜都预留了RJ45网口。一圈调研下来方案基本锁定在MCU内置MAC加外部PHY芯片配合LwIP协议栈这条路。市面上主流的MCU里STM32的ETH外设资料最多、CubeMX还有图形化配置加持踩坑门槛相对低于是就有了这个“24-ETH”项目。名字里的“反思”不是矫情。这一年里从打板回来点不亮PHY到LwIP ping通了但TCP收发几小时就崩再到排查出是内存池配置失误前前后后折腾了快两个月。回头再看80%的时间其实都耗在了几个固定的坑上。这篇博文就把整个过程中我认为最值得记录的思路、配置、代码和排错经历整理出来希望对准备做STM32以太网开发的朋友有点帮助。适合谁看用STM32F4/F7/H7系列做以太网通信的工程师尤其是第一次碰MACPHYLwIP这套组合的人。如果你已经有了点基础只是想看一些冷门坑可以直接跳到第5节。2. 方案选型为什么是内部MAC加外部PHY而不是SPI网口2.1 先分清“以太网”这件事里的三个角色很多人第一次搞以太网容易被一堆术语绕晕。其实从MCU角度看要跑通以太网硬件上就三块MAC层负责数据帧的封装、解封装、地址过滤、流量控制。在STM32里这个模块叫ETH是芯片内部自带的。PHY层负责把数字信号变成差分模拟信号往网线上送同时处理链路协商、状态检测。这就是外部挂的那颗PHY芯片。变压器和RJ45负责电气隔离和物理接口。MCU内部只有MAC没有PHY因为PHY涉及的模拟电路部分很难跟数字逻辑集成在同一颗芯片上成本和工艺都不划算。所以必须外挂一颗PHY芯片。2.2 为什么不用SPI接口的以太网模块有人会问直接用W5500这种SPI接口的以太网控制芯片不是更省事吗确实W5500把MAC和PHY全集成在内部MCU通过SPI读写寄存器不用碰LwIP硬件上也算简单。但我这次没选它原因有两条吞吐量瓶颈。SPI时钟即便跑到几十MHz实际TCP带宽也就在几MB/s量级。工业现场虽然不追求极限速度但我的项目里需要把一段时间内的波形数据打包上传单次传输量接近1MBSPI方案传输耗时明显偏长。协议栈可定制性太差。W5500内部硬件协议栈虽然方便但遇到特殊需求比如自定义应用层协议、非标准的UDP组播行为就非常受限。LwIP是源码级的想怎么改都行。所以最终确定用STM32F407内部的ETH外设加一颗外置PHY跑LwIP。F407的ETH自带DMA支持RMII和MII两种接口性能足够资料也多。2.3 PHY芯片选型LAN8720A还是DP83848PHY芯片我用过LAN8720A和DP83848各有优缺点简单做个对比项目LAN8720ADP83848接口RMIIMII / RMII工作电压3.3V内部1.2V LDO3.3V内部1.8V LDO功耗较低较高50MHz时钟源必须外部提供可外部提供也可自振价格便宜稍贵常见产地国产芯片兼容型号多TI原厂我最终选了LAN8720A核心原因就是RMII接口下PHY需要的50MHz参考时钟可以直接由STM32的MCO引脚输出省一个有源晶振。DP83848也能这么干但它对时钟质量更敏感而且整体功耗偏高。不过要注意LAN8720A的寄存器布局比较特殊有些寄存器地址和标准PHY不完全一致后面调试部分会专门讲。3. 硬件设计里最容易被忽略的几个点3.1 RMII接口只有7根线但时序要求并不低RMII全称是Reduced Media Independent Interface相比MII把数据线从8根减到2根时钟频率从25MHz提高到50MHz。好处是MCU引脚占用少坏处是50MHz时钟对PCB走线长度匹配有要求。STM32F407的ETH RMII接口信号一共7根ETH_RMII_REF_CLK50MHz参考时钟ETH_RMII_CRS_DV载波侦听/数据有效ETH_RMII_TX_EN发送使能ETH_RMII_TXD[1:0]发送数据ETH_RMII_RXD[1:0]接收数据ETH_RMII_MDIO管理接口数据线ETH_RMII_MDC管理接口时钟这里有个坑REF_CLK到底谁提供如果让STM32的MCO输出50MHz给PHY那么PHY的时钟输入和MAC侧接收逻辑都用这一路时钟相位关系是固定的相对简单。但有的PHY要求REF_CLK由外部晶振提供PHY内部再把它转发给MAC这种情况下如果电路设计没留好跳线调试时就会非常痛苦。3.2 复位电路和PHY地址别想当然LAN8720A的PHY地址由RXER/PHYAD0引脚的上拉下拉决定默认地址是0x00但很多开发板把它配置成0x01。如果你按着默认地址去写代码读不到PHY ID链路状态永远是Down而且这种问题用示波器量信号往往量不出来因为物理波形都正常纯粹是寄存器读写地址对不上。复位引脚同样不能随意接。LAN8720A的NRST要求低电平复位复位脉冲宽度最少1ms。MCU上电瞬间如果GPIO默认输出高电平而PHY的复位引脚恰好通过一个电容做上电延迟有可能导致复位不彻底。我后来干脆用一个普通GPIO控制PHY复位上电后先拉低50ms再拉高然后延时200ms等PHY内部初始化完成问题就消失了。3.3 变压器和RJ45的选择以太网变压器不是随便选个型号都能用的。带PoE供电需求的要选带中心抽头供电的型号普通数据通信选常见的HR911105A这类集成RJ45加变压器的连接器即可。集成式连接器最大的好处是BOM少、Layout方便但要注意不同厂家的引脚定义存在差异打板前一定要对着数据手册核对封装。另外PHY芯片的TX/RX差分对走线要做阻抗控制单端50欧姆、差分100欧姆。两层板做不了严格的阻抗匹配但至少要做到差分对等长、远离晶振和电源走线这个经验很重要。4. 基于STM32CubeMX的ETH与LwIP配置4.1 时钟配置别把50MHz从MCO引出的坑留给后续打开CubeMX选好芯片型号后第一件事是配置时钟树。STM32F407最高主频168MHz我习惯把HCLK拉到168MHzAPB2时钟84MHz。ETH外设挂载在AHB1总线上时钟使能后它的时钟是HCLK。RMII模式下PHY需要的50MHz REF_CLK由MCO1引脚输出时钟源选PLL的PLLQ分频得到50MHz。CubeMX里RCC配置中MCOx settings选MCO1时钟源选择PLLCLK分频系数填5因为PLLQ默认是336MHz168MHz x 2除以5以后是67.2MHz不对。这里有一个很隐蔽的地方PLLQ不是336MHz需要打开Clock Configuration页面仔细看。我实际用的事PLLCLK的168MHz经MCO1二分频得到84MHz再配PHY内部PLL不对LAN8720A不支持这么干。所以务必在CubeMX的Clock Configuration里确认PLLQ的实际数值确保MCO1输出50MHz。以F407配置168MHz系统时钟为例PLLM8、PLLN336、PLLP2、PLLQ7那么PLLQ输出是336/748MHz。48MHz对LAN8720A虽然能出链路但RMII时序已经不严格满足100Mbps的要求。要么把PLLQ调成能整除50的数值要么干脆外接50MHz有源晶振不折腾MCO。我当时第一版板子为了省一颗晶振在MCO这条路上反复调整时钟树最后还是换了50MHz有源晶振方案一劳永逸。如果你不是特别缺引脚和成本我建议直接用有源晶振给PHY提供50MHz省心很多。4.2 ETH外设配置RMII模式和DMA参数CubeMX里将ETH使能后选择RMII接口MAC地址填入自己规划的地址比如2C:F7:F1:08:1A:2B。PHY Address按硬件设计填LAN8720A默认是0如果硬件上改了地址这里必须对应改。Speed/duplex mode如果不会配选AutoNegotiation自动协商让PHY自己跟交换机谈速率。DMA参数默认即可但有几个地方值得说明一下参数值说明Receive DMA ModeDescriptor Ring环形描述符接收连续性强Transmit DMA ModeDescriptor Ring同上Number of DMA RX Descriptors4~6太少丢包太多费内存Number of DMA TX Descriptors4~6同上Ethernet Control Field默认一般不用动TCP/IP Checksum OffloadEnable由硬件计算校验和CPU负担小很多DMA描述符是ETH外设在内存里维护的一张表告诉DMA控制器数据要搬到哪、搬到多长。STM32F407默认描述符大小是32字节如果配置了校验和卸载描述符里还会包含校验和状态信息。描述符数量直接决定DMA能缓存几个数据包太小了接收溢出丢包太大了每个描述符占用内存8个描述符需要32字节×2个字段×8个描述符也就是不足1KB根本不影响所以至少配4个。4.3 LwIP参数配置这里最花心思CubeMX生成代码后LwIP参数在lwipopts.h里体现。配置界面里几个关键的Memory Heap Size默认是某个值我改成 100KB 左右。这个值代表可动态分配的PBUF内存总量TCP收发窗口都从这里出。改太大RAM吃紧改太小吞吐上不去。Memory Pool SizePBUF池的大小默认大概几十个PBUF。我用默认即可。TCP Window SizeTCP接收窗口我设置为 20KB配合LwIP的滑动窗口机制足够应付工业场景。TCP_SND_BUF发送缓冲区大小我设置为 20KB。这个值太小应用层往TCP写数据时容易阻塞太大占用RAM。LWIP_NETIF_API如果不用多线程关掉可以省资源。NO_SYS如果不用RTOS设为1直接在裸机while循环里跑LwIP。再说LwIP的核心文件处理。CubeMX生成LwIP初始化后在ethernetif.c里有两个关键函数low_level_input和low_level_output这是LwIP跟ETH驱动之间的桥梁。CubeMX生成的代码默认是不带校验和卸载处理的如果你在ETH配置里开了硬件校验和就得在low_level_input里检查描述符的校验和状态否则不会出错但也没享受到硬件加速。4.4 初始化顺序一个看似简单却容易翻车的点CubeMX生成的MX_LWIP_Init函数内部调用lwip_init然后执行netif_add、netif_set_default和netif_set_up。但有一个细节很容易忽略netif_add之后到link真正up之间LwIP不知道底层链路状态需要我们在ethernetif.c的low_level_init里调用PHY读取寄存器判断link状态。我建议初始化顺序这样安排调用HAL_ETH_Init完成ETH外设配置读取PHY芯片的Basic Mode Status Register地址1检查bit2Link Status如果链路正常设置PHY的AutoNegotiation完成标志调用netif_set_link_up让LwIP感知链路状态启动ARP定时器等相关任务一个问题如果在while循环里检测到link down要不要调用netif_set_link_down要。而且最好延时几秒再重新读状态因为PHY在协商过程中会产生抖动polling太快会把状态反复切换导致ARP表频繁失效。5. 代码实现从裸机ping通到数据流畅传输5.1 初始化代码的框架结构CubeMX生成代码放在main.c里结构大概是int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ETH_Init(); MX_LWIP_Init(); while (1) { MX_LWIP_Process(); } }MX_LWIP_Process在LwIP里就是轮询方式处理定时器和接收。注意CubeMX生成的MX_LWIP_Process内部调用的是ethernetif_input它会把底层收到的数据帧转换成pbuf投递到LwIP协议栈。如果你的应用里还需要周期性处理TCP重传、ARP超时等这些也都在这个循环里通过sys_check_timeouts完成。如果用了RTOS可以把MX_LWIP_Process放到一个专用任务里优先级设置成比普通应用任务更高防止网络任务饿死。5.2 一个裸机环境下的最小TCP服务器实现TCP服务器是嵌入式设备最常用的网络功能。下面是一段最简实现用raw API不依赖操作系统static struct tcp_pcb *server_pcb; static err_t server_recv_cb(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (p NULL) { // 对端关闭连接 tcp_close(tpcb); return ERR_OK; } // 这里p是收到的数据处理完后必须释放 tcp_recved(tpcb, p-len); pbuf_free(p); return ERR_OK; } static err_t server_accept_cb(void *arg, struct tcp_pcb *newpcb, err_t err) { tcp_recv(newpcb, server_recv_cb); return ERR_OK; } void server_init(void) { server_pcb tcp_new(); tcp_bind(server_pcb, IP_ADDR_ANY, 8080); server_pcb tcp_listen(server_pcb); tcp_accept(server_pcb, server_accept_cb); }调用tcp_recved函数很关键。LwIP的TCP接收窗口是有限的你从回调里取走数据后一定要调用tcp_recved告诉协议栈“我已经处理了n个字节”它才会更新窗口否则发送端很快就会因窗口耗尽而停止发送。5.3 发送数据小心指针生命周期发送方向的坑也不少。tcp_write需要把数据拷贝到协议栈内部缓冲区但如果数据量大于发送缓冲需要等对方ACK后再发下一批。最可靠的方式是配合tcp_sent回调static err_t server_sent_cb(void *arg, struct tcp_pcb *tpcb, u16_t len) { // 可以在这里发送下一段数据 return ERR_OK; }有一点要特别注意传给tcp_write的指针必须指向在调用期间保持有效的数据。如果你用一个局部数组tcp_write会立刻拷贝还好但如果数据量大到需要排队等ACK数据先被拷贝到pbuf里此时数据生命周期已经被LwIP接管不会出问题。容易出问题的是你直接把某块DMA缓冲区的地址传进去而这块缓冲区又被其他模块复用数据就被改了。5.4 UDP和TCP如何选很多嵌入式项目纠结用TCP还是UDP。我的判断标准很简单远程配置、指令下发选UDP。因为UDP无连接状态简单丢包重传由应用层自己做。大数据量、要求顺序可靠选TCP。TCP的乱序重组、丢失重传、流量控制是现成的。但是UDP也有一个容易被忽视的病接收缓冲区溢出。LwIP的UDP接收是每个PCB有自己的接收队列如果应用层处理慢队列会一直被塞满后续数据包被协议栈直接丢弃没有通知。排查时表现为“偶发性丢包”其实不是网络丢是处理不过来。6. 调试实录我从“ping不通”到稳定运行的全过程6.1 PHY读不到ID地址错了第一次上电代码烧进去后用调试器看HAL_ETH_Init返回值发现返回HAL_ERROR。进一步读PHY寄存器发现读出来全是0xFFFF。第一反应以为芯片虚焊补焊之后还是不行。后来看原理图才发现RXER/PHYAD0引脚被拉高了PHY地址是0x01而我在代码里用的是0x00。CubeMX里把PHY Address改成1重新生成代码再读PHY ID0x0007C0F1LAN8720A的ID就跳出来了。这个坑很多人遇到过提醒大家画板子时就在原理图上把PHY地址标清楚代码和硬件对齐。6.2 有链接但ping不通问题在MAC地址和ARP缓存PHY能读到IDlink状态也是up但ping就是不通。PC端抓包发现ARP请求发出去了但设备没有回复。排查方法在lwip的etharp.c里打断点看是否有ARP请求进来。结果发现根本没有进入etharp_input。检查ethernetif.c的low_level_input发现接收描述符里的事件标志被错误处理导致收包后数据没传给LwIP。修复后能回复ARP了但ping还是通不了。继续查发现MAC地址在CubeMX里填的和实际发送的不一致。因为有些PHY的驱动会修改MAC不会MAC地址完全是由HAL_ETH_SetMACAddr设置的问题在于我初始化顺序里MAC地址设置之后有个地方又把eth-Init.MACAddr覆盖了。改成在MX_ETH_Init之后立刻设置MAC地址并确认netif的hwaddr跟它一致才解决。6.3 TCP连接成功但传输一会儿就卡死内存池耗尽了TCP能连上PC端能发几十KB数据然后设备端就卡死不响应。用调试器挂上查看内存池状态发现PBUF池已经用光。根因是接收回调里没有及时释放pbuf。LwIP文档里有一句话在TCP接收回调里当你不需要这个pbuf时必须调用pbuf_free。我在早期代码里用了pbuf_copy把数据拷贝到应用缓冲区后忘了释放再加上tcp_recved没调用窗口越来越小最后协议栈卡死。这个问题在调试器下看内存池数值非常明显。在lwipopts.h里定义PBUF_POOL_SIZE然后用一个定时器周期性打印memp_pools状态就能看到数值一路下降。6.4 偶发丢包描述符数量战胜利于玄学稳定运行几小时后偶尔出现UDP丢包PC端发的1000个UDP包设备收到998个。开始怀疑PHY或变压器各种硬件排查都做了问题依旧。最后怀疑DMA描述符太少。接收描述符默认4个在突发流量下DMA来不及搬运新到的包只能丢弃。把接收描述符改成8个丢包现象明显减少。再把CXMX配置里ETH的DMA接收描述符数量调大同时把PBUF池开到足够大就基本不再丢了。6.5 一个软硬件交叉的坑交换机和直连网线调试时还会遇到一个特别迷惑的现象设备通过交换机跟PC通信正常但网线直连PC就link down。原因要么是PHY没有开启Auto-MDIX自动翻转线序要么PC网卡没开翻转。LAN8720A默认是支持Auto-MDIX的但如果直连时没生效检查PHY的BCR寄存器bit12Auto-Negotiation Enable是否置1以及PHY的Auto-MDIX模式是否开启。网线交叉/直通我测试时反而没遇到问题这里提一句是建议大家在硬件验证时准备一根交叉线备用。7. 排查工具和方法别只靠printf7.1 Wireshark 抓包是最有用的调试手段无论PC端还是设备端只要网卡支持用Wireshark抓包能最直观看到数据流。比如PCping设备不通抓包发现设备根本没回ARP问题就锁定在设备端的接收路径。设备回了ARP但ICMP不回问题可能在协议栈路由或校验和。很多情况下抓包比看代码更高效。7.2 用CubeMX重新生成代码后要注意手动改动CubeMX有个Bug修改配置重新生成代码时ethernetif.c里你手动添加的代码可能会被保留也可能被覆盖取决于代码是放在用户代码区还是普通位置。我建议把对ethernetif.c、lwip.c的手动改动都用用户代码区包裹或者干脆把自定义代码放到单独文件中别放在CubeMX生成的文件里否则某天误点生成把你一上午的修改全冲了。7.3 关于PHY寄存器调试的一个可选工具如果有逻辑分析仪可以拉MDIO和MDC信号确认MCU是否在正确地址上周期性轮询PHY状态。数据手册上MDC最高频率是2.5MHz有的PHY可以到12.5MHz但读数时宁慢勿快速率不对读出来的寄存器会出错。8. 性能优化和注意事项总结8.1 接收路径优化用硬件校验和卸载配置ETH时开启了TCP/IP Checksum Offload之后以太网帧的校验和计算由DMA硬件完成CPU不用介入。好处是CPU占用率下降尤其在高吞吐场景下差距非常明显。但如果同时LwIP也要计算校验和就会做重复劳动。在CubeMX的lwip配置里把LWIP_CHECKSUM_CTRL_PER_NETIF设为0或者按网卡区分可以避免重复计算。8.2 防止协议栈任务饿死如果用了RTOSLwIP协议栈处理任务的优先级不要低于其他业务任务否则高优先级中断或任务持续占用CPU网络任务就一直得不到调度底层DMA接收队列很快填满然后丢包。我一般把网络任务放在中等优先级周期5ms轮询一次。8.3 关于TCP_ACK和Nagle算法嵌入式TCP还经常遇到一个问题小数据包发送时Nagle算法会把小包合并成大包发送导致对端收到数据有延迟。比如你每100ms发一个设备的温度值只有几字节默认Nagle会等ACK再发延迟可能到500ms。解决方法是调用tcp_nagle_disable(pcb)让每个小包都立即发送。8.4 功耗和射频考虑PHY芯片在空闲时也会消耗电流LAN8720A大概几十mA。对于电池供电的设备要用PHY的Power Down模式在不需要网口时把PHY拉入低功耗需要时再唤醒。但要注意唤醒后链路重新协商需要几百毫秒到几秒网络层要考虑这个阶段的超时重传。9. 写在最后的几点实操体会做这个24-ETH项目最大的收获是以太网调试80%的问题不是出在写代码而是出在对硬件行为和协议机制的理解上。比如PHY地址、复位时序、时钟源、描述符数量、内存池管理这些交互点才是真正决定系统稳不稳定的地方。我后来在公司内部做了一次分享主题就是用STM32CubeMX做ETH加LwIP的坑大家反馈最有帮助的内容有两个一是把所有关键配置项整理成一张对照表二是把调试过程中遇到的现象、原因、解法记录下来形成排查手册。这个思路分享给大家也可以在自己项目的README里维护一份踩过的坑记录下来比什么都有价值。最后再分享一个小技巧如果你第一次做以太网项目打板时把PHY复位引脚、PHY地址配置引脚都引到排针上方便调试时改跳线。甚至把50MHz有源晶振的封装做成可焊可不焊的样式一旦MCO方案行不通还能直接改成有源晶振方案。这些小设计改动成本极低但关键时刻能省下至少一周的折腾时间。
返回列表