简介:这是一套基于STM32F429与LAN8720A的以太网TCP Server通信工程资源,面向嵌入式网络开发学习者,解决STM32平台通过以太网与电脑端进行TCP数据交互的需求,适合需要快速搭建或参考以太网通信方案的开发者。资源共326个文件,压缩包约1.61MB,主要包含149个C源文件、123个H头文件、30个汇编/s启动与配置脚本等,另有Keil工程文件(uvprojx/uvoptx)、hex固件及readme说明,便于直接编译或烧录验证。已有413人学习下载。工程内提供完整源码与驱动,涵盖STM32F4标准外设库、以太网控制器驱动、TCP/IP协议栈相关实现,并包含启动文件与编译辅助脚本;通过该包可理清LAN8720A PHY的初始化、RMII接口配置以及TCP Server收发流程,也可作为二次开发的基础框架,节省底层移植时间。整体目录结构清晰,适合中级及以上嵌入式开发者对照学习。
1. 一块 F429 板子做 TCP 服务器,卡住你的通常不是 TCP
把 STM32F429、LAN8720A 和 LwIP 拼成一个以太网 TCP 服务器,很多人以为难点在写 server 代码,真正上手后才发现,板子插上网线灯都不亮。F429 自带 MAC,但物理层必须交给 LAN8720A 这类 PHY 芯片,两者之间走 RMII 还是 MII、时钟谁提供、PHY 地址是多少,这三件事错一个,链路就起不来。这个组合的落地场景很明确:设备需要联网上报数据、做远程配置,或者作为 Modbus TCP 网关接入工业网络。下面从 RMII 物理链路开始,一路讲到 netconn API 的 TCP 服务器骨架和调优参数。
2. STM32F429 的 MAC 与 LAN8720A 的 PHY:RMII 链路先立住
2.1 为什么 F429 必须外接 LAN8720A
STM32F429 内部集成了以太网 MAC 控制器,支持 10/100 Mbps 速率,也带了 DMA 和描述符队列,但它没有模拟前端,收发信号需要外接 PHY。PHY 负责把 MAC 送来的并行数据调制成差分信号送到网线,同时从网线接收信号解调回并行数据。LAN8720A 是低功耗百兆 PHY,RMS 电流小,封装也小,是 F429 方案里最常用的搭配。
MAC 和 PHY 之间的接口有两种:MII 和 RMII。MII 需要 16 根信号线,RMII 只要 7 根,把 TXD 和 RXD 都压到了 2 位。F429 的 ETH 外设同时支持两种模式,实际项目里选 RMII 几乎是唯一选择,因为 F4 系列引脚资源本来就紧。RMII 的时钟固定为 50MHz,而 MII 的 TX 时钟是 25MHz 或 2.5MHz,这一点直接决定了后面的时钟方案。
2.2 RMII 上的 7 根线:F429 到 LAN8720A 的信号表
RMII 接口的信号线里,发送和接收各有两条数据线,控制信号是 TX_EN 和 CRS_DV,时钟是 REF_CLK。下面这张表是原理图设计时 F429 与 LAN8720A 的对应关系。
| 信号 | STM32F429 引脚 | LAN8720A 引脚 | 方向 |
|---|---|---|---|
| ETH_RMII_REF_CLK | PA1 | REF_CLK | 双向或 PHY 输出 |
| ETH_RMII_CRS_DV | PA7 | CRS_DV | PHY 输出 |
| ETH_RMII_RXD0 | PC4 | RXD0 | PHY 输出 |
| ETH_RMII_RXD1 | PC5 | RXD1 | PHY 输出 |
| ETH_RMII_TX_EN | PG11 | TX_EN | MAC 输出 |
| ETH_RMII_TXD0 | PG13 | TXD0 | MAC 输出 |
| ETH_RMII_TXD1 | PG14 | TXD1 | MAC 输出 |
这 7 根线里最容易被忽略的是 REF_CLK 的方向。如果 LAN8720A 使用外部 50MHz 有源时钟,REF_CLK 由外部时钟源直接驱动,PA1 配置为输入;如果 LAN8720A 用 25MHz 晶振,REF_CLK 由 PHY 内部 2 倍频后输出,PA1 同样作为输入接收。千万不要把 PA1 配成推挽输出,否则两边互相驱动直接拉死总线。
2.3 PHY 地址 0x00 的由来与 SMI 管理接口
MAC 需要通过 SMI 接口访问 PHY 的寄存器,SMI 由 MDC 和 MDIO 两根线组成。MDC 是时钟,由 MAC 输出;MDIO 是双向数据线。访问任何寄存器之前,必须知道 PHY 地址。LAN8720A 的 PHY 地址由 PHYAD0 引脚决定,默认下拉电阻接到地,所以地址是 0x00。
很多原理图里 LAN8720A 的 PHYAD0 悬空或通过 10k 下拉,这没问题。但如果你的板子把 PHYAD0 接了上拉,地址就变成 0x01,CubeMX 里默认配置却是 0x00,link 检测会一直失败。上电后用示波器量 MDIO 上的波形,或者直接写一段读 PHY 寄存器代码,读出 ID 寄存器值 0x0007C0F1,就能确认地址没写错。
2.4 时钟要先落地:REF_CLK 50MHz 的两种来源
LAN8720A 有两种时钟接法,实际项目里这决定 PCB 走线和初始化顺序。第一种是外部 50MHz 有源时钟直接进 REF_CLK,F429 的 MCO 或专用时钟芯片都能提供;第二种是 25MHz 无源晶振接 XI/XO,PHY 内部经过 PLL 倍频后在 REF_CLK 引脚输出 50MHz 给 F429。两种方式等价,但要注意 LAN8720A 的 REF_CLK 在晶振模式下是输出,在外部时钟模式下是输入。
F429 在 RMII 模式下,MAC 的时钟源也必须是 50MHz。用 CubeMX 生成工程时,ETH 外设的时钟会自动从 RCC 配置里取,不需要手动设置。但最好在系统时钟初始化后用HAL_RCC_GetHCLKFreq()确认频率,PHY 的寄存器读写时序直接依赖 MDC 时钟,MDC 最大频率是 2.5MHz,HCLK 太快时需要在HAL_ETH_MspInit里对 MDC 分频。
3. 用 CubeMX 生成 LwIP 工程:LAN8720A 引脚与 netconn 参数一次配对
3.1 CubeMX 里的引脚映射
CubeMX 选好 STM32F429ZIT6 后,ETH 外设使能 RMII 模式,引脚会自动分配到 RMII 对应的 GPIO 上。检查 Pinout 视图里 PA1、PA7、PC4、PC5、PG11、PG13、PG14 有没有被正确复用为 ETH_RMII 功能。LAN8720A 的复位引脚通常接到 F429 的任意 GPIO,建议单独留一个引脚控制复位,而不是和系统复位并在一起,这样软件可以随时硬复位 PHY。
GPIO 速度要设置成 Very High,否则高速信号边沿会被拉缓。MDIO 和 MDC 用 10k 上拉,RMII 数据线不用上下拉,保持浮空即可。有一个细节:LAN8720A 的 nINT 引脚默认是中断输出,如果接了 F429 的外部中断,初始化 EXTI 时要选上升沿和下降沿都触发,因为 link 状态变化可能产生两种边沿。下面给出 CubeMX 里的关键配置。
| 配置项 | 值 | 说明 |
|---|---|---|
| ETH | RMII | 不是 MII |
| PHY Address | 0 | 与 LAN8720A PHYAD0 一致 |
| MAC Address | 任意不冲突的单播地址 | 出厂烧录或用户可改 |
| GPIO Speed | Very High | RMII 信号线必须 |
| RCC HCLK | 168MHz 或 180MHz | F429 常见主频 |
| MDC Clock | HCLK/42 或接近 2.5MHz | 保证 SMI 读写出错少 |
LAN8720A 原理图里 mdc 上拉、phyad0 下拉、复位脚 RC 复位这些细节,直接照搬即可,CubeMX 不会帮你生成。
3.2 netconn 不是裸机玩具:必须配合 RTOS
LwIP 有三种 API:raw API、netconn API、socket API。CubeMX 生成 LwIP 中间件时默认是 raw API,也就是 NO_SYS 等于 1,这时候 LwIP 完全靠主循环轮询,不提供阻塞函数。netconn API 是带阻塞语义的线程化封装,内部依赖信号量和邮箱,裸机环境没法跑。
所以用 netconn API 写 TCP server 之前,CubeMX 里要先开启 FreeRTOS。实际操作顺序是:先在 Middleware and Software Packs 里选 FreeRTOS,CMSIS_V1 和 CMSIS_V2 都可以,V2 对应新的osThreadNew接口;然后在 LWIP 的 Mode 里选 Netconn,这时 NO_SYS 自动变成 0。LwIP 的 TCP/IP 内核线程由 CubeMX 自动创建,你只需要写一个应用线程来调用 netconn 接口。
3.3 LwIP 参数:PBUF 池和 TCP 窗口不要用默认值
CubeMX 生成的lwipopts.h里有一组默认值,直接跑一个 echo server 没问题,但稍微加一点并发请求就容易触发断言。需要关注的是这几个:MEM_SIZE决定堆内存大小,PBUF_POOL_SIZE决定收到的报文能暂存多少,TCP_SND_BUF和TCP_WND决定 TCP 收发窗口。F429 内部 RAM 有 192KB,但MEM_SIZE一般给 40KB 以内比较稳,因为 LwIP 的 pbuf 池和线程栈都会额外占空间。
#define MEM_SIZE (40 * 1024) #define PBUF_POOL_SIZE 16 #define PBUF_POOL_BUFSIZE 1512 #define TCP_SND_BUF (8 * TCP_MSS) #define TCP_WND (8 * TCP_MSS) #define MEMP_NUM_NETCONN 8 #define MEMP_NUM_TCP_PCB 8这段配置里PBUF_POOL_BUFSIZE必须大于最大 MTU 加上链路层头部,1512 是按 1500 字节 MTU 算的。MEMP_NUM_NETCONN是 netconn 对象的数量上限,每个 accept 出来的客户端连接占一个,默认值是 4,并发的 TCP 连接超过这个数就会返回ERR_MEM,表现为客户端 connect 成功但 recv 不到数据。这组参数要在 CubeMX 的 LWIP 配置界面里改,生成代码后如果手动改lwipopts.h,每次重新生成会被覆盖。
3.4 link 状态怎么从 LwIP 里拿到
PHY 的 link 状态通过netif_is_link_up判断,它背后依赖底层 PHY link 检测。F429 HAL 驱动的HAL_ETH_ReadPHYRegister可以读 BSR 寄存器,LAN8720A 的寄存器 1 第 2 位是 link status。常见的处理是开一个定时器任务,每隔 500ms 读一次。
uint32_t regval = 0; HAL_ETH_ReadPHYRegister(&heth, PHY_ADDRESS, 0x01, ®val); if (regval & (1 << 2)) { /* link up */ }逻辑很简单,但要注意:LAN8720A 的 link status 位是锁存的,必须读两次或先写清除才能获得最新状态。HAL 驱动里ETH_LINK_UP和ETH_LINK_DOWN的切换也是靠这个位判断。如果网线拔了再插上 LwIP 没有反应,多半不是驱动问题,而是状态位没有清除。
4. 用 netconn API 写 TCP echo server:accept/recv/write 的最小骨架
4.1 netconn 比 raw API 省心在哪里
raw API 要求所有回调都在 TCP/IP 线程上下文里执行,每个回调都要设计成无阻塞,思考方式非常接近底层。netconn API 把 accept、recv、write 变成同步阻塞调用,业务线程可以挂起等待,收到数据再恢复,代码读起来和 PC 上的 socket 几乎一样。代价是多一层线程切换,GNU 性能大约比 raw 低 5% 到 10%,对绝大多数采集上报场景完全无感。
4.2 TCP server 线程的完整代码
CubeMX 开启 FreeRTOS 后,在defaultTask里直接写业务即可。下面是监听 8080 端口、收到数据后原样返回的最小实现。
#include "lwip/api.h" #include "lwip/netconn.h" void tcp_server_task(void *argument) { err_t err; struct netconn *server = netconn_new(NETCONN_TCP); struct netconn *client = NULL; struct netbuf *inbuf = NULL; void *data = NULL; u16_t len = 0; if (server == NULL) { vTaskDelete(NULL); return; } err = netconn_bind(server, IP_ADDR_ANY, 8080); if (err != ERR_OK) { netconn_delete(server); vTaskDelete(NULL); return; } err = netconn_listen(server); if (err != ERR_OK) { netconn_delete(server); vTaskDelete(NULL); return; } for (;;) { err = netconn_accept(server, &client); if (err != ERR_OK) { continue; } while ((err = netconn_recv(client, &inbuf)) == ERR_OK) { netbuf_data(inbuf, &data, &len); err = netconn_write(client, data, len, NETCONN_COPY); netbuf_delete(inbuf); inbuf = NULL; if (err != ERR_OK) { break; } } netconn_close(client); netconn_delete(client); client = NULL; } }创建任务是 CubeMX 代码生成的一部分,在MX_FREERTOS_Init末尾加一行osThreadNew(tcp_server_task, NULL, &tcp_server_attributes);即可。
这段代码的核心逻辑是外层循环负责 accept,内层循环负责每次客户端连接的 recv 和 write。netconn_recv返回ERR_OK时inbuf指向一个 netbuf,netbuf_data拆出数据指针和长度。NETCONN_COPY标志告诉 LwIP 在netconn_write内部把数据拷贝到发送缓冲区,这样函数返回后业务侧可以立即丢弃原数据,不会出现 DMA 读一半数据被覆盖的问题。
netconn_accept默认是阻塞的,没有客户端连接时任务挂起,不占用 CPU。这个行为来自内核线程的信号量等待模式。
4.3 三个必调参数:recv timeout、send timeout、NETCONN_COPY
裸写的 netconn echo server 有一个隐患:某个客户端连接之后一直不发数据,内层循环阻塞在netconn_recv,新的客户端无法被 accept。解决办法是给每个客户端设置接收超时。
netconn_set_recvtimeout(client, 5000);接收超时设为 5000ms 后,客户端空闲 5 秒会自动结束当前连接,外层循环回到 accept。netconn_set_sendtimeout同理,当对端接收窗口为零且长时间不读数据,发送端会阻塞直到超时,避免任务卡死在netconn_write。
NETCONN_COPY的取舍要注意,如果关掉 COPY,netconn_write会在内部持有当前 netbuf 的引用,直到数据真正发出。此时业务侧不能释放数据缓冲区,否则会发出脏数据。我的做法是统一打开 COPY,虽然多一次内存拷贝,但换来的是不容易踩 DMA 的坑,尤其在 debug 模式下排查问题会轻松很多。
4.4 用 nc 命令验证 TCP 三次握手和数据往返
板子上电并分配好静态 IP 后,在 PC 上先确认 ping 通,然后直接用 nc 测 TCP 连通性。
echo "hello f429" | nc -w 2 192.168.1.100 8080如果板子 echo 正常,控制台会打印hello f429。不放心的话可以开抓包工具,观察 192.168.1.100:8080 和 PC 源端口之间的 TCP 三次握手,SYN、SYN-ACK、ACK 完整出现后再看数据包,这个过程能直观看到 LAN8720A 的收发都在正常工作。
5. LAN8720A 连不通的排查顺序:从 PHY ID 到 TCP 握手
5.1 第一步:读 PHY ID,验证 SMI 通信
网口一个灯都不亮,先不要查 LwIP,而是确认 MAC 能不能通过 SMI 访问到 PHY。在上电初始化后加一段代码读寄存器 2 和寄存器 3。
uint32_t id1 = 0, id2 = 0; HAL_ETH_ReadPHYRegister(&heth, PHY_ADDRESS, 2, &id1); HAL_ETH_ReadPHYRegister(&heth, PHY_ADDRESS, 3, &id2);LAN8720A 的 id1 应该是 0x0007,id2 是 0xC0F1,合起来是 0x0007C0F1。如果读回来是 0xFFFF,有两种可能:一是 PHY 地址不对,二是 PHY 完全没工作。地址不对就检查 PHYAD0 引脚电平,用万用表量实际电压,别只看原理图。PHY 没工作就看复位时序,LAN8720A 的复位高电平要保持 25us 以上,复位释放后要等至少 1ms 再访问 SMI。
5.2 第二步:link up 在哪看
SMI 通了但网络不通,大概率是 link 没有建立。读寄存器 1,也就是 BSR,bit 2 是 link status。这个位为 1 才能继续查上层。如果始终为 0,检查网线是否插到百兆交换机口、RMII 时钟是否存在、TX/RX 差分线是否反了。RMII 的数据线有方向,TX 和 RX 接反不会报错,但永远连不上。
还有一种隐蔽情况:LAN8720A 的 BLKSZ 和 EDPD 功能没有禁用,空闲时 PHY 自动进入节能模式,link 会周期性消失。这在软件里表现为网络时通时断,建议在 PHY 初始化时把这个模式关掉。
5.3 能 ping 通但 TCP 连不上,检查内存池和 PCB 数量
ping 走的是 ICMP 协议,LwIP 对 ICMP echo 的处理非常轻量,不占 TCP PCB。TCP 连接失败或 accept 失败时,优先检查MEMP_NUM_TCP_PCB和MEMP_NUM_NETCONN,默认值在连接数超过 4 后就会触发失败。LwIP 内部断言或打印ERR_MEM时,在lwipopts.h里打开LWIP_DEBUG,把TCP_DEBUG和NETCONN_DEBUG设为 LWIP_DBG_ON,看串口日志里的具体错误。
5.4 用抓包和日志定位是 MAC 还是 PHY 的问题
LAN8720A的寄存器 0x1F 是 PHY 状态寄存器,bit 0 是 10/100M link 状态,bit 1 是全双工/半双工。如果 PHY 认为 link 正常,但 F429 的ETH外设 MAC 统计寄存器里 RX 错误计数持续增加,问题在 RMII 信号质量,比如时钟抖动大或布线过长。如果 MAC 统计正常但 TCP 重传多,问题在 LwIP 参数,优先调大TCP_SND_BUF。
推荐顺序:先读 ID,再查 link,然后看 MAC 统计,最后才抓包。抓包时注意过滤条件,只要源或目标端口是 8080 的 TCP 报文。
5.5 连接建立后立刻断开,看 RST 从哪来
客户端连上后立刻收到 RST,通常是 LwIP 没有及时处理 accept 队列里的连接,MEMP_NUM_NETCONN不够时内核直接发 RST。另一个常见原因是任务优先级太低,tcpip_thread 和高优先级中断把业务线程饿死,recv 和 accept 长时间得不到调度。FreeRTOS 里 LwIP 的 tcpip_thread 默认优先级是 3,业务线程优先级设为 1 或 2 即可。
6. 让 TCP 吞吐更稳的三个参数:TCP_SND_BUF、TCP_WND 与 keepalive
6.1 TCP 长连接与短连接:怎么选
设备和服务器通信的场景分为两类。短连接适合低频上报,每次连接完成一次数据交换就关闭,服务器资源占用少,但握手消耗明显,每秒连接数多时 LAN8720A 的中断会很频繁。长连接适合持续采集或 Modbus TCP 网关这类实时性要求高的场景,但必须处理半开连接问题。LwIP 默认没有周期性发送 keepalive 探测包,客户端断电后服务器要等很久才能发现连接失效。
开启方法是在lwipopts.h打开宏:
#define LWIP_TCP_KEEPALIVE 1 #define LWIP_TCP_KEEPIDLE_DEFAULT 10000 #define LWIP_TCP_KEEPINTVL_DEFAULT 3000 #define LWIP_TCP_KEEPCNT_DEFAULT 3这样 TCP 连接空闲 10 秒后,内核每 3 秒发一次探测包,连续 3 次无响应就判定连接断开,阻塞的netconn_recv会返回错误码,业务侧可以清理资源并回到 accept。一个连接断开的处理逻辑,比修改窗口参数更能提升服务器的长期稳定性。
6.2 调整三件套:TCP_SND_BUF、TCP_WND、PBUF_POOL_SIZE
吞吐瓶颈和延迟瓶颈对应的参数不同。TCP_SND_BUF决定一次能写多少数据而不阻塞,TCP_WND决定接收窗口大小,PBUF_POOL_SIZE决定内核缓存报文的数量。F429 百兆带宽下,单连接回显场景建议这样配。
| 参数 | 默认值 | 推荐值 | 作用 |
|---|---|---|---|
| TCP_SND_BUF | 2 * TCP_MSS | 8 * TCP_MSS | 发送缓冲不足时 write 阻塞 |
| TCP_WND | 4 * TCP_MSS | 8 * TCP_MSS | 接收窗口小会限制吞吐 |
| PBUF_POOL_SIZE | 8 | 16 | 缓存队列深度,突发流量时防止丢包 |
| MEMP_NUM_TCP_SEG | 16 | 32 | TCP 分段队列长度 |
TCP_MSS默认是 1460,8 倍大约是 11680 字节,对 F429 的 RAM 和 CPU 都合理。不要盲目写成 64 * TCP_MSS,因为 LwIP 的内存是和 FreeRTOS 共用堆的,RAM 占满后任务栈会溢出,表现为系统随机死机。
6.3 一个最容易忽略的坑:DMA 描述符和缓存对齐
F429 的 ETH DMA 要求描述符 4 字节对齐,如果用默认的__ALIGN_BEGIN宏定义数组,一般没问题。问题出在发送数据时,如果应用层自己分配了 DMA 缓冲区并绕过 LwIP 直接提交到描述符,缓冲区必须保持有效直到发送完成回调。netconn_write的NETCONN_COPY模式已经规避了这个问题,因此建议所有发送路径都走 LwIP 的缓冲区,不要裸操作描述符。
最后再检查一件事,stm32f4xx_hal_conf.h中的ETH_RX_DESC_CNT和ETH_TX_DESC_CNT默认值是 4,高吞吐场景可以改成 6 或 8,同时PBUF_POOL_SIZE也要按比例增加,否则 DMA 描述符数量够了但 pbuf 不够,仍然会触发丢弃。这三组参数互相影响,改一个就要检查另外两个。
本文还有配套的精品资源,点击获取