ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F407+FreeRTOS+LWIP+LAN8720以太网通信实战详解

STM32F407+FreeRTOS+LWIP+LAN8720以太网通信实战详解 做嵌入式网络通信STM32CubeMX STM32F407 FreeRTOS LAN8720 这套组合我前后折腾了不少时间从最初点不亮 PHY、Ping 不通到最后 UDP/TCP 收发稳定跑起来踩的坑比想象中多。这篇文章就把整个项目的设计思路、Cubemx 配置流程、代码实现和排错经验完整梳理一遍尤其是 LAN8720 的时钟方案、PHY 地址匹配、LWIP 与 FreeRTOS 的配合这些容易卡住的地方尽量把细节讲透让新手少走弯路也让有基础的朋友能直接复用这套框架。项目本身的目标很明确STM32F407 通过 RMII 接口连接一颗 LAN8720A 以太网 PHY在 FreeRTOS 上运行 LWIP 协议栈实现 UDP 和 TCP 的数据收发。最终效果是在电脑上用网络调试助手就可以直接和板子通信做数据采集、远程控制、固件升级的前置通道都没问题。适合正在做物联网网关、工业采集板、设备联网功能的嵌入式开发者参考。1. 项目概述与整体设计思路1.1 这套组合为什么值得用先说选型。STM32F407 系列内部自带以太网 MAC 控制器但 MAC 不等于完整的以太网物理层它还需要一颗外部 PHY 芯片完成信号编解码、时钟恢复、电平转换这些物理层活。PHY 的选择很多常见的有 LAN8720A、DP83848、KSZ8081 等。我这次用 LAN8720A原因很实际体积小、成本低、外围电路简单RMII 模式只需要 8 根信号线而且这颗芯片能自己输出 50MHz 参考时钟对 MCU 的时钟要求更灵活市面上几乎所有 STM32F407 核心板、开发板都在用这颗料参考资料多遇到问题好查。RTOS 和协议栈的搭配也很关键。LWIP 是嵌入式领域最主流的开源 TCP/IP 协议栈支持裸机运行和 RTOS 运行两种模式。裸机模式下所有网络事件靠轮询和中断标志驱动逻辑简单但实时性差代码耦合度高RTOS 模式下 LWIP 会创建一个独立的 tcpip_thread 内核线程应用层通过 netconn 或 socket API 和内核通信任务挂起、唤醒都由操作系统调度网络处理不会阻塞主业务逻辑。项目里既然要跑 FreeRTOS就让 LWIP 跑在 RTOS 模式下这才是两个中间件配合的正确姿势。1.2 系统框架到底长什么样整个系统可以分成四层。最上面是应用层任务比如 UDP 收发任务、TCP 通信任务这些是你自己写的业务代码中间是 LWIP 协议栈它负责 TCP/UDP 协议解析、IP 分包重组、ARP 缓存、路由表这些再往下是 STM32 的以太网 MAC 外设通过 DMA 进行帧收发最底层就是 LAN8720 PHY把 MAC 传过来的数字信号转换成网线上的模拟差分信号。数据流向是应用任务调用 netconn/socket API 把数据交给 LWIPLWIP 组包后交给 MAC 的 DMA 描述符MAC 通过 RMII 接口传给 LAN8720PHY 再经网络变压器送到外部网络。这里要特别强调一点MAC 和 PHY 之间是靠MDIO/MDC 管理接口配置同步的。上电后 STM32 必须通过 MDIO 总线读写 PHY 的寄存器完成自协商、速度配置、链接状态检测这个过程就是 HAL_ETH_Init 内部做的。很多时候 Ping 不通问题不在 LWIP 代码而是 PHY 地址没配对、寄存器初始化失败这一点后面会专门讲。1.3 谁适合看这篇文章如果你已经熟悉基本的 STM32 裸机开发知道怎么用 CubeMX 生成工程、怎么写 GPIO 和中断但第一次接触以太网这篇文章可以直接当你的入门到入坑指南。如果你已经在裸机上跑过 LWIP这次想把协议栈挪到 FreeRTOS 下重点看第 3 章的中间件配置和第 4 章的 API 切换部分。如果你纯粹是照着买来的板子做实验遇到 PHY 检测失败、Ping 不通这些玄学问题第 5 章的问题排查表可以直接对照排障。2. 硬件方案与关键原理拆解2.1 STM32F407 的以太网 MAC 到底能干多少活STM32F407 内部集成的是符合 IEEE 802.3 协议的 MAC 层它支持 10M/100M 速率支持 MII 和 RMII 两种接口模式内置 DMA 控制器和描述符链表可以自动完成收发缓冲管理。也就是说CPU 只需要配置好描述符然后把数据写到内存里MAC 的 DMA 会自动把数据搬出去发送接收方向也一样DMA 收到数据后直接放到内存通过中断或轮询告诉应用程序有数据到了。但 MAC 不包含物理层收发器没有 PHY 它连不出去。所以才有“MACPHY 分离”的设计。用 MII 接口连接时信号线多达 16 根占用的引脚太多用 RMII 接口连接时只要 8 根信号线不包括 MDIO/MDC 管理线引脚利用率高得多。RMII 的工作频率要求是 50MHz不管实际网络速率是 10M 还是 100M参考时钟都固定 50MHz这也就引出了整个项目里最容易踩坑的时钟方案问题。2.2 LAN8720A 芯片的几个关键细节LAN8720A 是 SMSC 公司现在归 Microchip出的一款低功耗 10/100M 以太网 PHY支持 RMII 接口内部集成 1.2V 稳压器只需要单 3.3V 供电外围电路非常简单。它有两个地址引脚 PHYAD0 和 PHYAD1用来设置 PHY 的 MDIO 地址实际大多数模块只引出了 PHYAD0默认接地表示地址 0也有部分设计通过电阻上拉设置成地址 1。CubeMX 里配置 PHY 地址时一定要和硬件实际接线保持一致否则初始化阶段读写寄存器就会超时失败。LAN8720 的 nINT 中断引脚是可选的可以在 Link 状态变化时产生中断通知 MCU项目里没用到所以悬空。nRST 复位引脚很关键PHY 上电后需要等待一段时间才能稳定工作有些模块把这个引脚直接接 MCU 的 GPIO由软件控制复位时序有些模块硬件上直接拉高没有外部复位控制。推荐做法是留一个 GPIO 控制复位这样可以在代码里做“上电后拉低 10ms再拉高等待 150ms”的硬复位流程能有效避免 PHY 状态异常的问题。2.3 RMII 引脚连接与时钟方案这是最大的坑RMII 接口一共需要 7 根数据信号REF_CLK参考时钟、CRS_DV载波监听/数据有效、RXD0、RXD1接收数据、TXD0、TXD1发送数据、TX_EN发送使能再加上 MDIO、MDC 两根管理线一共 9 根信号。STM32F407 上的引脚分配是固定的CubeMX 会自动锁定这些引脚你不需要手动指定。问题出在 REF_CLK 这个 50MHz 参考时钟上。它有两种接法第一种外部 50MHz 有源晶振直接给 LAN8720 的 XI 引脚LAN8720 的 REF_CLK 输出引脚负责产生 50MHz 时钟给 STM32 的 ETH_RMII_REF_CLK。这个方案最干净MCU 不需要额外输出时钟但板上必须有一颗 50MHz 有源晶振。第二种STM32F407 通过 MCO1 引脚PA8输出 50MHz 时钟给 LAN8720 的 XILAN8720 再从 REF_CLK 脚输出 50MHz 返回给 STM32。这种方案适合板上没有 50M 晶振的情况很多开发板都是这么设计的比如正点原子的探索者、野火的指南者。注意PA8 在 USB OTG 设计里有时会和 VBUS 检测相关但在这个以太网项目里它的真正角色就是 MCO1 输出千万别把它当成普通的 IO 口来初始化。第三种的变体还有用 STM32 的 MCO 输出 25MHz 给 PHYPHY 内部 PLL 倍频到 50MHz 再返回但 LAN8720 不支持这种方式DP83848 才支持。市面上也有部分开发板在 PHY 旁边放置了 50MHz 无源晶振配合 LAN8720 内部振荡器工作一样能产生 REF_CLK但可靠性和一致性不如有源方案。做项目之前一定要先看你手上的板子原理图搞清楚 50MHz 时钟到底是谁生成的。如果板子上 LAN8720 的 XI 引脚连接到了 F407 的 PA8那 CubeMX 里必须额外配置 MCO1 输出 50MHz否则 PHY 完全没有时钟初始化必然失败。这是我第一次调这块板子时卡得最久的地方当时以为是 PHY 地址配错了反复查 MDIO 时序最后才发现时钟根本没有。2.4 网络变压器和指示灯外围除了 PHY 芯片本身RJ45 座子通常还集成网络变压器作用是隔离共模干扰、电平转换、保护芯片。有的板子 LAN8720 直接接分离式网络变压器再接 RJ45有的直接用一个带变压器的 HR911105A 这类 RJ45 座子。做硬件设计时注意 RXD 差分对的 49.9Ω 偏置电阻和 tx 的中心抽头电容软件上不用关心这些。LAN8720 的 LED0/LED1 可以配置成速度/链接/活动指示。调试的时候观察这两个灯很直观上电后如果 PHY 和 MCU 的 RMII 链路配置成功、网线另一侧有设备LED 会亮起如果一直不亮基本可以断定硬件链路或 PHY 初始化有问题不值得浪费时间看上层软件。3. CubeMX 配置全流程一步步来3.1 基础工程配置RCC、SYS、串口打开 STM32CubeMX新建工程选择芯片我这边用的是 STM32F407VET6 核心板。第一步先配置 RCC把 HSE 设为外部晶振Crystal/Ceramic Resonator这是后面整个时钟树的基础。还有 HSE Value 根据板载晶振频率填一般 F407 核心板都是 8MHz。SYS 里 Debug 模式选 Serial Wire否则下载一次程序之后 SWD 会被禁用下一次就没法烧录了。USART 选一个串口用于日志输出比如 USART1 或 USART2独立配置波特率 115200。建议通信调试阶段一定要有日志输出没有日志等于闭着眼调网络效率极低。串口初始化后面可以自己加重定向代码把 printf 输出到串口助手。3.2 时钟树168MHz 主频加 50MHz MCO时钟树是整个 CubeMX 配置里最需要理解的地方。STM32F407 的典型配置是外部 8MHz HSE - PLL 倍频到 168MHz 系统时钟AHB 168MHzAPB1 42MHzAPB2 84MHz这些在 Clock Configuration 页面里可以顺着配置向导调好CubeMX 会自动计算合法性红色警告就说明配置超出限制。以太网这块的时钟要注意F407 的 ETH 外设需要 25MHz 的 PLL 输出作为 MAC 时钟源同时如果需要 MCO1 输出 50MHz 给 LAN8720需要在 Clock Configuration 的 MCO 部分勾选 MCO1并把输出频率设成 50MHz。MCO1 的可选源包括 HSE、PLL 等CubeMX 里把 MCO1 的时钟源选为 PLL 并设置分频系数让最终输出为 50MHz 即可。设置完成后 PA8 自动变为 MCO1 功能。这里特别提醒一点如果板上已经有外部 50MHz 有源晶振直连 LAN8720MCO1 就不用配置了PA8 可以作为普通 IO 复用出其他用途。反过来如果用了 MCO1 方案但 MCO1 没配置PHY 没有参考时钟后面 HAL_ETH_Init 会直接超时。所以配置前先看原理图这一点说多少次都不为过。3.3 ETH 外设配置RMII 模式与 PHY 地址在 Pinout Configuration 里找到 Connectivity - ETH勾选 RMII 接口模式。CubeMX 会自动把 PA1ETH_RMII_REF_CLK、PA2ETH_MDIO、PA7ETH_RMII_CRS_DV、PC1ETH_MDC、PC4ETH_RMII_RXD0、PC5ETH_RMII_RXD1、PB11ETH_RMII_TX_EN、PB12ETH_RMII_TXD0、PB13ETH_RMII_TXD1这几根引脚设为复用功能。ETH 的参数设置里PHY Address 要根据你的硬件填写。LAN8720A 通常默认地址是 0因为 PHYAD0 接地。按键配置里还需要选择 PHY 芯片型号新版 CubeMX 的 PHY 下拉里可以直接选 LAN8720A选好后芯片相关的寄存器读写地址、握手时序都会被正确设置。如果你的 CubeMX 版本里没有这个选项也可以选 Custom然后手动填 PHY 地址和参数但推荐升级一下 CubeMX省去不少麻烦。另外ETH 的高级参数中还有 DMA 描述符数量、收发缓冲区大小、中断使能等默认值通常够用。DMA 描述符数量和收发缓存大小与内存占用直接相关F407 内部有足够 RAM保持默认就好。3.4 LWIP 中间件配置选 RTOS 模式左边 Middleware 里找到 LWIP勾选启用。首次勾选后你要注意一个最重要的选项OS 栏。默认可能是 No OS 裸机模式必须手动改成 FreeRTOS 模式这样 CubeMX 才会生成和 FreeRTOS 集成的 lwip 代码包括 tcpip_thread、sys_arch、信号量同步这些。很多初次接触的人忽略这个选项结果生成的代码里没有 OS 接口层后面 RTOS 任务一调度就崩溃。网络参数部分按静态 IP 填写比如 IP 地址 192.168.1.10子网掩码 255.255.255.0网关 192.168.1.1。如果你需要 DHCP 动态获取可以在 LWIP 参数里把 LWIP_DHCP 启用并在代码里主动调用 dhcp_start不过调试阶段强烈建议先上静态 IP排除 DHCP 超时带来的干扰等网络通信稳定后再换 DHCP。LWIP 的其他宏配置比如 LWIP_UDP、LWIP_TCP、LWIP_SOCKET、LWIP_NETCONN 默认都是启用的。如果你打算用 socket API确认 LWIP_SOCKET 为 Enable用 netconn API 则确认 LWIP_NETCONN 为 Enable两者都开也没问题后面写任务代码时更灵活。3.5 FreeRTOS 配置CMSIS v2、任务与堆大小Middleware 里找到 FreeRTOS启用。新版 CubeMX 默认使用 CMSIS-RTOS V2 接口层这个保持默认就行。配置界面里可以在 Tasks and Queues 里创建你自己的任务比如 UDP 通信任务 udp_task、TCP 通信任务 tcp_task优先级和栈大小在这里直接设定。网络相关的任务建议优先级不要低于 normal栈大小给 1024 字节起步调试阶段可以给到 2048避免因为栈溢出导致莫名的 hardfault。FreeRTOS 的 Heap Size 也要关注LWIP 在运行过程中会动态分配 pbuf、socket 结构体等内存这些都从 FreeRTOS 的 heap 里取。CubeMX 默认的 heap 是 3 x 1024 也就是 3KB 左右这个值明显不够建议改到 25KB 以上。这里面的换算关系是 LWIP 的内存分配通过 MEM_SIZE 宏和 FreeRTOS 堆共用p 缓冲、socket 描述符都会占用宁可多给也不能少给。Heap 不够时表现很奇怪可能任务创建失败、DHCP 不工作、socket 返回错误排查起来非常痛苦。3.6 生成代码与工程结构检查全部配置完成点击 GENERATE CODE。生成之后工程里会多出几个关键源文件eth.c、lan8720.c、lwip.c、lwip_arch.c、ethernetif.c、freertos.c、stm32f4xx_it.c 等。其中 eth.c 里是 MX_ETH_Initlan8720.c 里是硬件相关函数ethernetif.c 是 LWIP 的网卡驱动层lwip.c 是协议栈初始化。代码生成后建议先别急着写业务逻辑先编译一遍确认工具链正常。然后检查一下主函数里初始化顺序HAL 初始化 - 时钟 - 外设 - MX_LWIP_Init - osKernelStart。LWIP 的初始化必须在 FreeRTOS 内核启动前完成因为它会创建 tcpip_thread 等内核任务。有些新手指到哪里都像以前裸机一样加 while 循环结果内核都没调度起来网络自然不通。4. 代码实现UDP 和 TCP 数据收发4.1 PHY 复位与检测要自己补CubeMX 生成的 HAL_ETH_Init 内部会通过 MDIO 访问 PHY 的寄存器 0 和寄存器 1读取芯片 ID 和制造商 ID确定 PHY 正常。但前面说过如果 reset 引脚由 GPIO 控制CubeMX 不会主动做复位时序需要你自己写。推荐在 MX_ETH_Init 调用之前做一下硬件复位void lan8720_reset(void) { HAL_GPIO_WritePin(LAN8720_RESET_GPIO_Port, LAN8720_RESET_Pin, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(LAN8720_RESET_GPIO_Port, LAN8720_RESET_Pin, GPIO_PIN_SET); HAL_Delay(200); }如果 LAN8720 没有连接复位引脚而是硬件直接拉高这段代码可以不加。判断 PHY 初始化是否成功可以在 CubeMX 生成的 MX_ETH_Init 后增加寄存器读取验证比如读取 PHY 寄存器 0若值为 0x3100说明 LAN8720 响应正常。我自己调试时习惯在这里加一句串口打印把读到的 ID 打出来一步确认 MAC 和 PHY 之间的管理接口链路是通的。4.2 UDP 收发任务一个回环服务搞定UDP 收发最简单的方式是用 LWIP 的 netconn API。在 FreeRTOS 任务里创建 netconn 结构体绑定端口然后循环接收数据、回发数据。下面是一个带串口日志回显的 UDP 任务示例#include lwip/netconn.h #include lwip/api.h void udp_echo_task(void *argument) { struct netconn *conn; struct netbuf *buf; void *data; uint16_t len; err_t err; conn netconn_new(NETCONN_UDP); if (conn ! NULL) { err netconn_bind(conn, IP_ADDR_ANY, 8888); if (err ERR_OK) { for(;;) { err netconn_recv(conn, buf); if (err ERR_OK) { data (void *)netbuf_data(buf, len); printf([UDP] recv %d bytes\n, len); netconn_send(conn, buf); netbuf_delete(buf); } } } } vTaskDelete(NULL); }这里绑定了 8888 端口收到数据后原样回传。实际项目里可以根据业务解析数据内容比如收到控制指令后驱动 GPIO 输出或者把传感器数据打包发送给上位机。UDP 的特点是面向无连接发送时用netconn_send即可不需要像 TCP 那样维护连接状态非常适合上位机周期性下发命令的场景。4.3 TCP Server 任务用 socket API 更顺手TCP 通信用 socket API 更接近 PC 端编程习惯代码逻辑更清晰。下面是一个 TCP Server 的示例监听 8080 端口接受一个客户端连接接收数据并回显#include lwip/sockets.h void tcp_server_task(void *argument) { int listen_fd, conn_fd; struct sockaddr_in server_addr, client_addr; socklen_t addr_len; char buf[512]; int len; listen_fd socket(AF_INET, SOCK_STREAM, 0); if (listen_fd 0) { vTaskDelete(NULL); return; } memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(8080); server_addr.sin_addr.s_addr INADDR_ANY; bind(listen_fd, (struct sockaddr *)server_addr, sizeof(server_addr)); listen(listen_fd, 1); for (;;) { conn_fd accept(listen_fd, (struct sockaddr *)client_addr, addr_len); if (conn_fd 0) { printf([TCP] client connected\n); for (;;) { len recv(conn_fd, buf, sizeof(buf) - 1, 0); if (len 0) break; buf[len] \0; printf([TCP] recv: %s\n, buf); send(conn_fd, buf, len, 0); } closesocket(conn_fd); printf([TCP] client disconnected\n); } } }socket API 在嵌入式环境下使用要注意一个问题accept、recv、send这些函数是阻塞式的当 TCP 客户端没有发送数据时任务会卡在recv上如果这个任务优先级过高会影响其他任务运行。所以 TCP 任务优先级一般不要超过 tcpip_thread栈大小也要根据最大缓冲合理设置。多个客户端同时连接时需要为每个连接创建独立任务或使用多线程模型但 F407 资源有限简单的单连接模型就够了。4.4 任务优先级和内存分配建议CubeMX 生成的 FreeRTOS 配置里tcpip_thread 默认优先级是 3CMSIS-RTOS v2 默认 osPriorityNormal你的应用任务如果要保证实时响应可以设置成比它低一级或者同级。UDP 和 TCP 任务里大量调用了 LWIP API这些 API 会触发内核同步如果优先级高于 tcpip_thread会出现优先级反转和调度抖动表现就是网络吞吐异常、频繁超时。内存分配是另一个重点。LWIP 初始化时 pbuf 池、TCP 窗口、socket 缓冲区都会分配内存。CubeMX 的 LWIP 配置页里几个宏值得关注MEM_SIZE内存池大小、PBUF_POOL_SIZEpbuf 池数量、TCP_MSS最大报文段、TCP_WNDTCP 窗口。调试阶段建议把MEM_SIZE设成 20 * 1024 左右PBUF_POOL_SIZE设成 20 以上这样做大数据传输时才不容易出现通信卡死。这些参数如果设得太小发送大包会直接失败接收方向会丢包日志里能看到pbuf_alloc failed或者out of memory之类的错误。4.5 UDP 与 TCP 在调试中的取舍调试阶段我建议先调通 UDP再调 TCP。UDP 不用考虑连接、重传、拥塞控制只要能收到一条数据就能确认 IP 层和数据链路层没问题。而 TCP 涉及三次握手、窗口协商、重传机制出问题时很难分清是底层链路问题还是协议状态机问题不如先用 UDP 扫清底层障碍再用 TCP 验证协议栈完整性。实际项目中 UDB 适合丢包容忍度高的传感器数据上传、设备发现、时间同步这类场景TCP 适合指令下发、文件传输、固件升级这种要求可靠交付的场景。两个协议栈同时开启并没有冲突它们的任务可以并行运行只要注意绑定端口不要重复就行。5. 联调过程与常见问题排查实录5.1 PHY 初始化失败HAL_ETH_Init 一去不返这个现象最典型代码卡死在 HAL_ETH_Init 的 while 循环里等待 MDIO 操作完成或者返回 HAL_TIMEOUT。排查顺序基本是固定的。第一步查 PHY 地址。LAN8720 的地址由 PHYAD0/PHYAD1 引脚决定开发板模块背面有丝印或者直接看原理图。CubeMX 的 ETH 配置里 PHY Address 必须和这个一致。很多 STM32F407 核心板把 PHYAD0 接地那么地址就是 0模块供应商给的例程里如果写的是 1就直接改过来。第二步查时钟。用示波器量 LAN8720 的 XI 引脚或者 REF_CLK 输出确认有 50MHz 时钟。如果是 MCO1 方案用示波器量 PA8没有波形就是 CubeMX 时钟树没配好。没有示波器的话可以量 LED 灯PHY 没有时钟时灯基本全灭有正常自协商过程中灯会快速闪烁。第三步查复位。如果 PHY 复位引脚悬空且内部上拉不可信上电后 PHY 可能处于复位状态MDIO 永远无响应。手动加一个 GPIO 控制复位时序最稳妥。5.2 网线插着、灯也亮但是 Ping 不通PHY 初始化成功网口指示灯正常LLDP 灯也闪了但 Ping 的时候 host 提示超时。建议按下面几步走先在板子上用 UDP 定时向电脑的 IP 发送一段固定数据比如每 1 秒向 192.168.1.100 的 6666 端口发送 hello然后电脑上开网络调试助手监听 6666 端口。如果能收到说明发送通路完整问题大概率在上位机侧或 ARP 缓存。电脑的防火墙经常会拦截 ICMP 协议Ping 不通但 UDP 能通就是这个原因所以调试不能只依赖 Ping。如果 UDP 也收不到先用 Wireshark 在电脑上抓包。有 ARP 请求说明 STM32 的 MAC 地址已能发出没有 ARP 请求说明板子发送方向有问题检查 LWIP 的 netif 是否 up、MAC 地址是否异常。CubeMX 默认生成一个 MAC 地址但建议在 lwip.c 里显式设置一个自己的地址避免多个板子共用一个 MAC 导致 ARP 冲突。5.3 UDP 只能发不能收或者只能收不能发发送正常、接收异常优先检查中断和接收 DMA。F407 以太网接收有 DMA 中断和轮询两种模式CubeMX 默认使用的是 DMA 中断 ethernetif_input 轮询结合的机制实际收发链路里HAL_ETH_Receive_IT会在中断里被调用。如果中断优先级配置不当或者被更高优先级中断频繁打断接收数据会被丢弃或覆盖。接收正常、发送异常优先检查发送后是否及时清理 DMA 描述符。LWIP 通过 ethernetif_output 把数据交给底层驱动底层驱动如果没正确释放发送描述符下次发送时没有空闲描述符可用报文被丢掉却没有任何提示。可以在发送函数里打日志打印netif-tso或 TX 描述符状态标志确认是否卡在发送状态。还有一个常见的坑ethernetif_input任务轮询接收时占用了太多 CPU 时间而你的应用任务优先级又很低导致数据一直被网卡驱动读取但应用层没时间处理。这种情况表现为 UDP 收包延迟大、TCP 传输速度极慢。检查方法把应用任务优先级调到和网络任务一致或稍高观察是否改善。5.4 TCP 连接可以建立但数据传输卡死TCP 三次握手能成功说明网络链路完全没问题问题出在数据交换阶段。最典型的场景是客户端连接上板子连接建立成功但发送数据后服务器没有响应过一会连接被关闭。这时要重点检查 LWIP 接收缓冲区和窗口设置。TCP 窗口太小会导致滑窗卡死MSS 不匹配会导致分片重组失败。另外一个容易忽视的问题是socket API 的阻塞超时没有设置。当 LWIP 内存紧张或对端不发数据时recv会一直阻塞单任务模型下没问题但如果还跑着无人机控制、电机控制之类的实时任务这个阻塞会造成任务饥饿。建议在 socket 创建后用setsockopt设置SO_RCVTIMEO让接收调用在指定时间后超时返回业务逻辑里处理超时条件即可。5.5 链接电位和调试器的恩怨一接调试器死机这是 STM32 以太网项目最经典的坑之一用 ST-Link/J-Link 调试程序一跑到 ETH 初始化就死机或者 ETH 初始化通过但一调试就复位拔掉调试器反而正常。原因是 F407 以太网 DMA 调试时会停住调试器停止 CPU 时 MAC DMA 还在跑或者挂着导致复位时序异常。处理办法比较简单在调试前给代码加一个 2~3 秒的延时比如在主循环开始前HAL_Delay(3000)让上电后 PHY 和网络设备完全稳定再启动 ETH 初始化减少调试器干预时机。另一个更彻底的办法是硬件上给调试接口和以太网之间做电气隔离开发阶段没必要软件延时基本够用。如果还是死机检查调试器是否通过 SWD 占了 ETH 引脚部分板子的 SWDIO 和以太网引脚有冲突时很要命。5.6 常见问题速查表现象可能原因排查措施HAL_ETH_Init 超时PHY 地址错误 / PHY 无时钟 / PHY 复位不正常核对 PHYAD 引脚量 50MHz 时钟检查复位 GPIOPing 超时但 UDP 通防火墙拦截 ICMP / ARP 缓存异常放行 ICMP清电脑 ARP 缓存UDP 只能发不能收接收中断优先级 / DMA 描述符耗尽调整 IRQ 优先级检查 RBU 状态UDP 只能收不能发TX 描述符未释放 / netif down打印 netif 状态检查 ethernetif_outputTCP 连接后卡死窗口太小 / recv 阻塞 / 内存不足调大 TCP_WND、MEM_SIZE设置超时任务莫名 hardfaultFreeRTOS 栈溢出调大任务栈开启栈溢出检测下载一次程序后无法烧录SWD 被禁用改 SYS Debug 为 Serial Wire恢复启动5.7 排错到底靠什么三板斧排网络问题我最依赖的三个工具缺一不可。第一是串口日志所有关键节点都要打PHY ID、netif 状态、连接建立、数据收发长度日志越细越好定位。第二是电脑上的网络调试助手做 UDP 和 TCP 的收发回环测试比某些复杂的抓包工具直观得多。第三是 Wireshark遇到真正诡异的问题、网络层行为不符合预期时抓包能直接看到 ARP、IP、TCP 的状态迁移很多问题在包层面一眼就看出真相。6. 项目收尾做出的几个关键取舍做这个项目我反复调整过几个方案这里单独说一下决策过程可能能帮你省下一些试错时间。第一个取舍是 LWIP 的 API 选择。项目里我同时用了 netconn API 和 socket API。UDP 任务用 netconn 是因为它的接口更轻量、内存占用更小代码更直接TCP 任务用 socket 是因为代码可读性更高真要移植到 PC 平台也更容易。如果你是从零开始建议统一用 socket APILWIP 文档和网上的现成代码都很多学习成本低。第二个取舍是静态 IP 还是 DHCP。调试阶段一定用静态 IP省去 DHCP 的发现过程出问题少一个变量。产品阶段如果设备需要接入各种路由器、局域网再启用 DHCP但要处理好租约续期和 IP 冲突检测工作量并不小。第三个取舍是 LWIP 的内存参数。我把 MEM_SIZE 设到 20KB、PBUF_POOL_SIZE 设到 20 之后实测 UDP 连续高速收发和 TCP 文件传输都稳定了很多。这个参数不是越大越好F407 总共 192KB RAM系统还要跑实时业务分太多内存给协议栈会挤压其他任务的可用堆需要根据实际场景做权衡。第四点是任务的划分粒度。我没有把 UDP 和 TCP 放在同一个任务里而是拆成两个独立任务。虽然通信功能单一用单任务也能跑但拆开后两个方向的负载互不干扰改一个协议不会影响另一个代码维护性更好。不过任务多了也要注意优先级安排两个网络任务之间如果互相等待资源反而容易引入死锁。我最终的做法是两个任务独立运行、互不通信只共享同一个 LWIP 内核这样最安全。7. 写在最后的几点实操体会项目做完回头看这种“STM32CubeMX 初始化 FreeRTOS 调度 LWIP 协议栈”的开发模式已经是很成熟的主流路线了难度不高但细节非常多。我觉得最有价值的并不是最后调通的代码而是调通过程中建立起来的排错顺序感硬件问题先解决时钟问题先量波形PHY 问题先读寄存器上层协议问题再抓包每层都有明确的验证手段整个系统就能快速收敛。最后再分享一个调试技巧我习惯在 LWIP 初始化后主动打印 netif 的 IP、掩码、网关和连接状态同时周期性打印链表状态和内存池空闲情况比如每 10 秒输出一次tcp_active_pcbs、udp_pcbs的数量。这样做连续跑几天之后翻日志能清楚看到内存泄漏是增长还是稳定这对长期运行的设备非常关键。以太网通信这种东西调试时能通不算本事连续跑 72 小时稳定不掉线、不漂移才是真正能上线的状态。
返回列表