
简介基于STM32F407微控制器与LAN8720A以太网PHY芯片的TCP通信源码工程使用STM32CubeMX配合HAL库生成初始化代码并集成LWIP协议栈。工程将开发板配置为TCP客户端PC作为TCP服务端实现双向数据收发实验适合正在学习嵌入式网络编程、准备毕设或需要快速搭建以太网通信原型的工程师与学生。资源共342个文件以225个头文件和109个C源文件为主体涵盖以太网外设驱动、LWIP协议栈核心及套接字接口实现同时包含IOC工程配置及MDK工程文件可直接用开发环境打开编译运行压缩包整体只有1.75MB。目前已有599人学习下载。通过该工程可掌握STM32CubeMX中网络外设的配置流程、PHY芯片的驱动方式及LWIP协议栈的移植方法加深对TCP客户端编程框架的理解包括建立连接、数据收发与错误处理等关键环节。代码结构清晰模块划分合理便于在此基础上继续扩展为MQTT、HTTP等应用层协议也可减少重复移植工作快速验证网络通信功能。1. STM32CubeMX 生成 F407 的 TCP 客户端源码不是勾两下就完事当板子第一次上电串口打印出ETH transmit frame faild: 20或者 PC 上等了几分钟都看不到 TCP 连接建立很多人的第一反应是程序源码写得不对。其实 STM32CubeMX 开发 STM32F407 ETH LWIP 这条链路里从“能编译”到“能收发数据”中间还隔着 PHY 地址、RMII 时钟、DMA 描述符、lwIP 内存池和线程调度这几道坎。这篇文章顺着一个常见的 TCPclient 客户端需求出发把 CubeMX 配置、netconn API 编码、Wireshark 抓包联调以及现场断线重连都完整过一遍。适合刚接触以太网的新手也适合正在查“为什么连着连着就掉线”的老手。2. STM32F407 的 ETH 控制器、RMII 时钟与 CubeMX 骨架2.1 从 MAC、PHY 到 DMA数据是怎么跑起来的先建立底层画面。STM32F407 内部是 MAC 加 DMAPHY 芯片在片外。HAL 库将 MAC 寄存器封装成HAL_ETH_Init将 DMA 描述符的收发封装成HAL_ETH_TransmitFrame与HAL_ETH_GetReceivedFrame。lwIP 对上提供netif接口对下通过low_level_output调用 HAL。一次发送路径是netconn_write-tcp_write-tcp_output-netif-linkoutput-low_level_output- 描述符写寄存器 - PHY 发到网线。接收路径则由 MAC 中断通知HAL_ETH_RxCpltCallback只负责释放信号量实际收包在ethernetif_input线程里完成。这个模型决定了后面程序源码的组织方式不要在以太网中断回调里调用 lwIP 的阻塞 API不要在netconn_recv里加长延时否则协议栈线程会被卡死。F407 没有数据缓存DMA 与 CPU 不存在缓存一致性问题但这不代表内存顺序没有讲究。收发描述符和缓冲区数组必须 4 字节对齐CubeMX 生成的ethernetif.c通常用__ALIGN_BEGIN修饰。如果自己改过 linker 文件要小心别把 DMA 描述符段弄丢。2.2 50 MHz 参考时钟与 PHY 地址两个最容易踩的配置项RMII 接口由 7 根数据信号加 1 根 50 MHz 参考时钟组成。50 MHz 从哪里来必须看原理图外部有源晶振直接供给、PHY 锁相环倍频后再反馈给 MCU、MCU 的 MCO 输出给 PHY三种接法在 CubeMX 界面上没有区分。常见 PHY LAN8720A 有时钟输出引脚但它默认是否开启取决于外部电阻和复位时序。上电后如果 HAL 读 PHY 一切正常只是长时间跑会偶尔丢包要考虑参考时钟抖动。此时用示波器观察REF_CLK的占空比比逐行读手册更直接。PHY 地址的坑更常见。CubeMX 的ETH参数页里默认PHY Address 0部分核心板和自制板的 PHY 地址是 1HAL_ETH_Init读不到 PHY 会返回超时然后进入Error_Handler。用调试器单步发现MX_ETH_Init()里卡在检查HAL_ETH_ReadPHYRegister的返回值问题基本就在 PHY 地址上。先把地址改成原理图对应的数值再排查其他问题。2.3 用 STM32CubeMX 建立最小 ETH 工程时检查 3 个生成项CubeMX 生成工程后下面三项每次都要重新确认lwipopts.h里的LWIP_MEM_ALIGNMENT不能为 1至少 4 字节ethernetif.c的ETH_RXBUFNB与ETH_TXBUFNB不能一直用默认最小值HAL_ETH_RxCpltCallback在自己的业务代码里不要重复定义也不要把它改成包含 lwIP 阻塞调用的函数。检查方式是生成后再打开lwipopts.h和ethernetif.c搜索这几个符号。CubeMX 本身不会感知业务代码是否定义了同名回调如果两个.c文件同时定义HAL_ETH_RxCpltCallback链接时报重名错误反过来如果只有一个定义那另一个文件里的实现会被静默忽略。很多工程出现“接收中断信号量从来不被释放”原因就在这里。3. stm32cubemx 配置 eth 和 lwip 的 5 个关键参数3.1 在 CubeMX 选择 API ModeRAW、NETCONN 还是 SOCKETCubeMX 的Middleware LWIP General Settings里有API Mode下拉框这个选项直接决定整个 TCP 客户端程序源码的骨架。RAW 是纯事件回调裸机可用NETCONN 和 SOCKET 依赖底层信号量与消息队列需要配合 RTOS 运行。对于 TCP 客户端这种线性逻辑需求用 NETCONN 写起来最直观而且 CubeMX 会要求你启用一个 RTOS 内核因为netconn_connect需要阻塞等待连接完成。勾选 Netconn 后CubeMX 会同步激活FREERTOS并自动生成tcpip_thread。同时还要确认lwipopts.h中LWIP_NETCONN宏是 1。有些工程生成后链接期报undefined reference通常就是 FreeRTOS 的堆没配够或者 HAL 时基仍占用 SysTick。F407 的 HAL 时基应改到 TIM6否则HAL_GetTick和 FreeRTOS 心跳抢占同一个中断lwIP 的sys_now返回值错乱TCP 重传计时全部跑偏。3.2 影响 TCP 客户端稳定性的核心参数与 TCP 客户端最相关的可调参数按影响程度排列如下参数推荐值说明TCP_MSS1460以太网最大段大小超过 MTU 会触发 IP 分片TCP_WND4 * MSS接收窗口太小对端传输速度会被明显拖慢TCP_SND_BUF4 * MSS发送缓冲写入后未确认的数据都在这里排队PBUF_POOL_SIZE16接收缓冲池突发指令时不够会直接丢包MEM_SIZE16000lwIP 堆大小netconn 模式需要较多动态内存LWIP_DHCP0现场固定 IP 调试避开 DHCP 超时等待TCP_SND_BUF是一个容易被忽略的隐形阻塞点。用netconn_write发送 4 KB 数据时如果发送缓冲只有 1 个 MSS第一段发完后要等服务端 ACK剩余数据才能继续发出实操表现就是“发几包停一下”。把TCP_SND_BUF调成 4 * MSS停顿会明显改善代价是 RAM 多占用约 6 KB。MEMP_NUM_TCP_SEG也值得关注。它控制 TCP 发送段缓存数量网络抖动时如果段缓存耗尽tcp_enqueue会直接返回内存错误。在长期运行的设备上段缓存和发送缓冲必须同步调大。3.3 在 lwipopts.h 中打开必要功能关掉多余功能CubeMX 生成lwipopts.h后还需要确认几个宏#define LWIP_RAW 1 #define LWIP_NETCONN 1 #define LWIP_SOCKET 0 #define CHECKSUM_GEN_IP 1 #define CHECKSUM_GEN_TCP 1 #define CHECKSUM_CHECK_IP 1 #define CHECKSUM_CHECK_TCP 1LWIP_SOCKET用不到就置 0能省下不少内存。校验和全部由 lwIP 软件计算调试时不至于因为硬件 offload 和字节序问题出现“连接建立但数据总校验失败”的疑难杂症。CubeMX 界面里可能只暴露CHECKSUM总开关保持默认即可。4. TCP client 客户端程序源码连接、发送、接收与断线重连4.1 用 netconn API 建立客户端连接CubeMX 生成并启动 lwIP 后TCP 客户端代码从建立连接开始。以下是最小可用的连接函数struct netconn *tcp_client_connect(void) { struct netconn *conn; err_t err; ip_addr_t server_ip; conn netconn_new(NETCONN_TCP); if (conn NULL) { return NULL; } netconn_set_recvtimeout(conn, 1000); netconn_set_sendtimeout(conn, 3000); IP_ADDR4(server_ip, 192, 168, 1, 10); err netconn_connect(conn, server_ip, 8080); if (err ! ERR_OK) { netconn_delete(conn); return NULL; } return conn; }代码逻辑说明netconn_new(NETCONN_TCP)创建 TCP 连接控制块内部会分配 PCBnetconn_set_recvtimeout和netconn_set_sendtimeout分别设置收、发超时单位是毫秒IP_ADDR4是 lwIP 的 IPv4 地址赋值宏netconn_connect发起 TCP 三次握手成功返回ERR_OK失败返回ERR_CONN或超时错误。调试阶段如果该函数一直失败先排查服务器 IP 和端口是否可达再看 PC 防火墙。4.2 业务线程中发送一帧注意缓冲区生命周期发送数据建议遵守“只有一个线程写 socket”的原则。把待发数据放进 FreeRTOS 队列TCP 客户端任务作为唯一写者static void tcp_client_task(void *argument) { struct netconn *conn NULL; uint8_t txbuf[128]; for (;;) { if (conn NULL) { conn tcp_client_connect(); if (conn NULL) { vTaskDelay(pdMS_TO_TICKS(2000)); continue; } } if (xQueueReceive(tx_queue, txbuf, pdMS_TO_TICKS(100)) pdTRUE) { err_t w_err netconn_write(conn, txbuf, 128, NETCONN_COPY); if (w_err ! ERR_OK) { tcp_client_close(conn); } } tcp_client_recv(conn); } }xQueueReceive的超时参数设为 100 ms任务在等待发送数据的同时还能及时处理接收NETCONN_COPY让 lwIP 把数据拷贝到内部缓冲区调用方可以立刻复用txbuf如果发送失败调用关闭函数强制走重连逻辑。4.3 用 netbuf_copy 处理 pbuf 链和粘包问题服务端发来的数据可能跨越多个 pbuf也可能一次 recv 得到多个逻辑帧所以接收侧统一收进应用层缓冲区更稳妥void tcp_client_recv(struct netconn *conn) { struct netbuf *buf; err_t err netconn_recv(conn, buf); if (err ! ERR_OK) { if (err ERR_TIMEOUT) return; tcp_client_close(conn); return; } if (buf ! NULL) { uint16_t len netbuf_copy(buf, app_rx app_rx_len, sizeof(app_rx) - app_rx_len); app_rx_len len; netbuf_delete(buf); process_app_frame(); /* 按自定义协议拆包处理 */ } }逻辑说明netconn_recv阻塞直到有数据或超时netbuf_copy自动遍历 pbuf 链并连续复制到目标地址省去手动操作链表的麻烦。如果接收长度超过剩余缓冲区数据会被截断业务层可以通过请求重传协议补回避免内存越界。4.4 断线后不留下悬空指针断线重连最容易犯的错误是重复操作一个已经删除的 netconn。关闭函数写成这样void tcp_client_close(struct netconn **conn) { if (*conn ! NULL) { netconn_close(*conn); netconn_delete(*conn); *conn NULL; } }指针置空这一步最容易被忽略。如果不置空任务循环里conn ! NULL仍然成立往一块已释放的内存写数据最终会触发 HardFault。很多设备“运行几天后死机”最后查下来不是 lwIP 崩溃而是在重复断开阶段用了悬空指针。5. 联调与排错Wireshark、静态 IP、eth transmit frame faild: 205.1 用简单的网络命令验证链路层通不通把电脑 IP 设为 192.168.1.10板子 IP 固定为 192.168.1.20先跑一条命令验证基本链路ping 192.168.1.20 -t能 ping 通说明 ARP 和 ICMP 已经工作TCP 握手不成功的问题在端口或服务端。ping 不通就要优先查物理层、PHY 地址和 RMII 时钟。如果板子支持调试串口在连接任务里打印netif_is_link_up()的值返回 0 时直接说明链路没起来。5.2 定位 eth transmit frame faild: 20 的三个排查方向这个错误信息经常出现在 STM32F407 以太网调试日志里字符串中的数字20由底层驱动定义。不同驱动来源含义不一但排查方向是固定的分三路并行物理层未就绪。PHY 的 Link 状态是 Down这时候netconn_write会立即失败。用手摸一下网口灯或读 PHY 的BMSR寄存器能快速判断。DMA 发送描述符没有及时归还。TX 描述符数量不够或发送完成中断没有打开CPU 把描述符写满后 DMA 没有机会回收。在调试器里看heth-TxDescList[]的DESC0所有权位可以确认是不是卡在“下次写入”和“DMA 未释放”之间。多线程同时调用 HAL 发送函数。lwIP 的low_level_output如果没加互斥两个任务同时发送时第二帧可能直接返回 busy。用信号量把发送入口保护起来即可。排查时可以写一个定时器中断绕过 lwIP 直接循环发送测试帧。如果裸 HAL 发送也报错问题限定在 PHY 和 DMA 范围如果裸发送正常再回到线程调度查找原因。5.3 用 Wireshark 验证握手、重传和接收窗口电脑开启抓包过滤条件tcp.port 8080一次正常的 TCP 建立过程应看到三包SYN、SYNACK、ACK。如果只有 SYN 反复重传是服务端没有回 ACK如果服务端回了 SYNACK 但客户端不进入 ESTABLISHED重点查网卡 offload 与 MAC 校验和配置是否一致。在 Wireshark 中打开 “checksum status” 列确认没有校验和错误。板子端可以开启 lwIP 统计功能LWIP_STATS_DISPLAY();打开LWIP_STATS宏后控制台会输出tcp.drop、eth.rx、eth.tx的计数。rx一直增长而tcp.drop同步增长说明接收缓冲不足tx不增长说明应用层根本没把数据送进协议栈。6. 现场长期运行的 3 个加固点6.1 心跳与断线识别服务器和对端交换机会回收空闲连接。客户端每 10 秒发一个心跳包同时记录最近一次成功接收数据的时间if (HAL_GetTick() - last_recv_tick 30000) { tcp_client_close(conn); }注意last_recv_tick的刷新时机要放在连接成功建立之后而不是连接函数返回之前。否则握手刚完成就被误判为超时。6.2 随机退避重连避免多台设备同时冲击多台 STM32F407 在同一交换机上断电重启时如果所有设备等待相同秒数重连服务器会出现瞬时连接风暴。给重连等待加入随机量uint32_t delay_ms 2000 (rand() % 3000); vTaskDelay(pdMS_TO_TICKS(delay_ms));重连间隔的随机化也让反复断开的设备不会在固定时刻对服务器形成节拍性冲击。6.3 环形日志与错误码记录设备进入机柜调试困难的场景预先在 RAM 中维护一块 32 条记录的环形缓冲记录“连接成功”“发送失败”“超时断开”“内存不足”四类事件。下次复位后通过串口导出能直接还原崩溃前的动作序列。关键 else 分支里不要只写一个空注释别怕多占几个字节把错误码存下来排障效率会明显不同。提示不要把看门狗当成兜底方案。lwIP 内存耗尽和描述符占满这类软故障日志比复位更能说明问题无脑喂狗只会掩盖真正缺陷。在所有重连路径中EEPROM 或 Flash 只保存最近一次连接状态即可不要每断一次线就写入一次存储介质否则反复掉电会让配置区提前损坏。本文还有配套的精品资源点击获取