资讯详情

STM32F407 LwIP+FreeRTOS以太网工程搭建与故障排查

📅 2026/10/26 19:37:20 | 华诺云谱 👁 阅读
STM32F407 LwIP+FreeRTOS以太网工程搭建与故障排查
简介STM32F407 平台下融合 LWIP 轻量级 TCP/IP 协议栈与 FreeRTOS 实时操作系统的工程压缩包面向具备一定嵌入式基础、希望打通网络通信与多任务调度的开发者。内容围绕点钻机下位机远程控制场景展开FreeRTOS 负责任务调度、内存管理与中断协调并借助队列、信号量等机制协调网络收发与控制任务LWIP 提供 TCP/UDP 等网络服务STM32F407 借助丰富外设与 FPU 完成指令解析、状态反馈和实时控制可用于工业自动化、物联网通信等学习与二次开发。压缩包约 69.89MB文件总数及类型明细页面暂未给出下载后可据目录结构自行核对源码、工程配置与说明文档。已有 558 人浏览学习适合用作 RTOS 与嵌入式 TCP/IP 联合调试的参考案例帮助理解上位机指令下发、下位机响应及多任务协同的实现思路对协议栈移植、任务划分与上下位机联调也有参考价值。1. 网线插上去灯亮了却 ping 不通问题往往不在协议栈一块 STM32F407 的板子接上 RJ45链路灯常亮、ACT 灯偶尔闪一下电脑这头ping过去全是请求超时串口里 FreeRTOS 的任务还在正常打印心跳——这是很多人第一次把 LwIP 和 FreeRTOS 拼到一块时的典型画面。这份工程把以太网外设驱动、LwIP 协议栈、FreeRTOS 内核三层缝成一个能直接编译烧写的整体跑通之后设备就是一个带完整 TCP/IP 能力的终端DHCP 拿地址、开 TCP 服务端收数据、做 UDP 上报都能在几个任务里并行跑。它适合已经点过灯、用过串口、想给板子补上网口的产品开发做数据采集盒、协议转换器、简易控制器面板这类活最省事不适合连中断和指针都没摸过的新手那份协议栈的lwipopts.h会让人看得一晚上睡不着。2. 工程骨架从启动文件到 lwipopts.h 的目录地图压缩包解开之后第一件事不是编译是先把目录看明白。这类工程通常不会把所有文件平铺在一起而是按「芯片厂商驱动 / 中间件 / 板级支持 / 应用」四层切开四层各自的编译选项、头文件路径、内存占用差别很大混在一起改一处崩一片是常态。下面是我拆这类工程时习惯的视角。2.1 四层目录与各自职责典型的目录长这样路径名可能有出入但分层逻辑基本一致MyNetNode/ ├── Drivers/ │ ├── CMSIS/ # 内核头文件、启动汇编、system_stm32f4xx.c │ └── STM32F4xx_HAL_Driver/ # HAL 库ETH、GPIO、RCC、UART 等 ├── Middlewares/ │ └── Third_Party/ │ ├── FreeRTOS/Source/ # 内核源码 port/GCC|ARM_CM4F │ └── LwIP/src/ # 协议栈core / api / netif / apps ├── BSP/ │ ├── eth/ # ethernetif.c、PHY 初始化、DMA 描述符 │ └── uart/ # 调试串口通常重定向 printf ├── App/ │ ├── main.c # 只做初始化和创建任务 │ ├── app_tcp_server.c # 业务TCP 服务 │ └── app_udp_report.c # 业务UDP 上报 ├── Inc/ │ ├── lwipopts.h # 协议栈裁剪全工程最重要的一个头文件 │ └── FreeRTOSConfig.h # 内核配置堆大小、优先级数、tick 频率 └── MDK-ARM/ 或 Makefile分层的意义在于责任边界。Drivers和Middlewares属于「能不改就不改」的区域尤其是 LwIP 的src目录改一次后面换版本就要重来一遍BSP是把芯片外设翻译成协议栈能用的接口ethernetif.c就是这一层的核心App才是二次开发真正动刀的地方。我在项目里一般会在App下再按业务拆文件避免所有 socket 代码堆在main.c里几百行之后谁也找不着哪个任务在收数据。ethernetif.c这个文件值得单独拎出来说。它实现了 LwIP 的netif结构体要求的三个回调low_level_init负责把 MAC 地址、MTU、网卡标志位填进去并初始化 ETH 外设low_level_output把pbuf链上的数据拷进发送 DMA 缓冲区并触发发送ethernetif_input则挂在以太网接收中断或者轮询任务上把收到的帧包成pbuf往上递。这三个函数写错任意一个表现都是「灯亮、不通」而且不会有任何报错信息属于典型的黑匣子场景。2.2 lwipopts.h几个必须改的宏LwIP 之所以能塞进几十 KB 的 RAM靠的就是这个头文件里的裁剪开关。默认值偏向保守放到 F407 上往往不够用下面这几个宏是我每次都要过一遍的/* lwipopts.h —— 与 FreeRTOS 共存的关键配置 */ #define NO_SYS 0 /* 0使用操作系统模式必须为 0 才能开 socket */ #define LWIP_SOCKET 1 /* 打开 BSD socket 接口 */ #define LWIP_NETCONN 1 /* 打开 netconn 接口socket 依赖它 */ #define LWIP_DHCP 1 /* 需要自动获取 IP 就打开 */ #define LWIP_ICMP 1 /* ping 依赖它调试期千万别关 */ #define MEM_ALIGNMENT 4 /* Cortex-M4 按 4 字节对齐别填 1 */ #define MEM_SIZE (12 * 1024) /* 堆协议栈自己 malloc 用的池子 */ #define PBUF_POOL_SIZE 16 /* pbuf 池数量决定能同时挂多少收包 */ #define PBUF_POOL_BUFSIZE 1524 /* 单个 pbuf 至少容下一个以太网帧 */ #define TCP_MSS 1460 #define TCP_SND_BUF (4 * TCP_MSS) #define TCP_WND (4 * TCP_MSS) #define MEMP_NUM_TCP_PCB 8 /* 同时存在的 TCP 连接控制块上限 */ #define MEMP_NUM_TCP_PCB_LISTEN 4 /* 同时监听的端口数 */ #define LWIP_NETIF_LINK_CALLBACK 1 /* 网线插拔回调恢复通信用得上 */ #define LWIP_NETIF_STATUS_CALLBACK 1逐条说清楚。NO_SYS必须是 0这是整个组合成立的前提填 1 就等于退回裸机轮询模式sys_arch.c里的信号量实现全部失效socket 接口直接编译不过。MEM_SIZE是最容易翻车的一项它既不是芯片 RAM 大小也不是任务栈而是 LwIP 内部mem_malloc管理的独立堆开得太小会在pbuf_alloc返回 NULL表现为「收到数据后协议栈默默丢包」串口一句日志都没有。12KB 是个能跑通 TCP 收发加 DHCP 的保守值业务里再挂 MQTT 之类的大包协议就要往上加。PBUF_POOL_SIZE决定的是收包能力。以太网帧进来先占一个 pbuf交给上层处理完才释放如果池子只有 4 个而你的应用处理慢DMA 缓冲区满了之后新帧就被硬件丢弃ping 大包ping -l 1400会看到明显丢包率这就是原因。PBUF_POOL_BUFSIZE要略大于 1514以太网最大帧填 1524 是留了点余量别卡着 1514 填对齐问题会让你少几个字节。提示TCP_SND_BUF和TCP_WND并不是越大越好。F407 一共 192KB RAM扣掉 FreeRTOS 堆、任务栈和 LwIP 的池子剩不下多少。我一般先把这两个压到 4 个 MSS 跑通功能再根据实测吞吐往上试。LWIP_NETIF_LINK_CALLBACK这个宏很多人不开结果就是现场拔一次网线板子必须断电重启才能恢复。打开它并在netif上注册netif_status_callback链路断开时把netif置为 down、重连时重新走 DHCP才有后悔药。2.3 编译与烧录两条路都要能走工程里同时放 MDK 工程和 Makefile 的情况很常见前者给用 IDE 的人后者给 CI 或者习惯命令行的。两边的源文件列表、宏定义、头文件路径必须严格一致否则会出现「同事那边能编过、你这边报找不到lwip/opt.h」的玄学问题。核心的编译选项通常是这样# Makefile 片段关键是这几个宏和路径 CPU -mcpucortex-m4 -mthumb -mfpufpv4-sp-d16 -mfloat-abihard DEFS -DUSE_HAL_DRIVER -DSTM32F407xx INCLUDES -IDrivers/CMSIS/Include \ -IDrivers/STM32F4xx_HAL_Driver/Inc \ -IMiddlewares/Third_Party/LwIP/src/include \ -IMiddlewares/Third_Party/FreeRTOS/Source/include \ -IMiddlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F \ -IInc SRCS $(wildcard App/*.c BSP/eth/*.c BSP/uart/*.c) \ $(wildcard Middlewares/Third_Party/LwIP/src/core/*.c) \ $(wildcard Middlewares/Third_Party/LwIP/src/api/*.c) \ $(wildcard Middlewares/Third_Party/LwIP/src/netif/*.c)几个点必须对齐-mfloat-abihard要和 FreeRTOS 的 port 文件匹配用ARM_CM4F就得上硬浮点用错了会在任务切换时触发 HardFaultLwIP 的src/core下面还有ipv4、ipv6子目录wildcard只扫一层的话会漏掉ip4.c链接阶段报一堆undefined reference to ip4_input。另外中断优先级分组要跟 FreeRTOS 的configPRIO_BITSF407 是 4对齐以太网中断里调xSemaphoreGiveFromISR才安全。烧录之后第一件事是接串口看打印。正常启动顺序是时钟和外设初始化 → FreeRTOS 创建任务 →tcpip_init起协议栈线程 →netif_add挂网卡 → 启动 DHCP 或写静态 IP。任何一步卡住后面的日志都不出现通过最后一条打印就能定位到段落。3. 底层链路ETH 外设、PHY 自协商与第一个 ping协议栈跑通之前得先让硬件把包发出去。F407 的以太网外设是带 DMA 的 MAC外挂一颗 PHY 芯片常见的是 10/100M 自协商型号中间走 RMII 或者 MII 接口。这一层出问题上层再怎么调都没用所以值得单独拆开看。3.1 ETH 外设与 DMA 描述符MAC 收发靠的是两组描述符环加对应的缓冲区。描述符本身是内存里的一块结构体里面有「这个缓冲区归谁所有」「帧长度」「下一块在哪」这些字段硬件和 CPU 通过修改 owner 位来交接。初始化代码通常长这样/* BSP/eth/ethernetif.c —— DMA 描述符与缓冲区的定义 */ ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB] __attribute__((aligned(4))); ETH_DMADescTypeDef DMATxDscrTab[ETH_TXBUFNB] __attribute__((aligned(4))); uint8_t Rx_Buff[ETH_RXBUFNB][ETH_RX_BUF_SIZE] __attribute__((aligned(4))); uint8_t Tx_Buff[ETH_TXBUFNB][ETH_TX_BUF_SIZE] __attribute__((aligned(4))); static void low_level_init(struct netif *netif) { /* 1. 硬件复位 配置 GPIO 复用为 RMII */ HAL_ETH_DeInit(heth); HAL_ETH_Init(heth); /* 内部会配置 MAC 和 DMA */ /* 2. 填 MAC 地址从芯片 UID 派生避免每块板子都一样 */ uint32_t uid0 HAL_GetUIDw0(); netif-hwaddr[0] 0x02; netif-hwaddr[1] (uid0 0) 0xFF; netif-hwaddr[2] (uid0 8) 0xFF; netif-hwaddr[3] (uid0 16) 0xFF; netif-hwaddr[4] 0x5A; netif-hwaddr[5] 0xC3; netif-hwaddr_len ETHARP_HWADDR_LEN; HAL_ETH_SetMACAddress(heth, netif-hwaddr); /* 3. 描述符环挂到 DMA 上开中断 */ HAL_ETH_Start_IT(heth); }ETH_RXBUFNB一般取 4ETH_RX_BUF_SIZE取 1524。__attribute__((aligned(4)))不能省DMA 描述符要求 4 字节对齐少了这句在某些优化等级下会被放到奇数地址现象是「发送正常、接收永远为零」查起来非常难受。MAC 地址直接用芯片 UID 派生这一点在批量生产时能省很多事。如果所有板子都烧同一个 MAC同一个局域网里两台设备会互相抢包表现是「单独测试都正常一上现场就时断时续」属于那种不上量发现不了的坑。hwaddr[0]置 0x02 表示本地管理地址避开厂商 OUI 段路由器也更容易接受。3.2 PHY 自协商与链路状态轮询MAC 配好了不代表链路起来了。PHY 芯片上电后要和对面交换机做自协商确定速率10M/100M和双工模式这个过程需要几百毫秒到几秒。代码里用 SMI 接口也就是 MDIO/MDC 两根线读写 PHY 的寄存器来判断/* 读取 PHY 的基本状态寄存器判断链路是否建立、协商结果是什么 */ uint32_t phy_link_check(void) { uint32_t bsr 0, status 0; if (!ETH_ReadPHYRegister(PHY_ADDRESS, PHY_BSR, bsr)) { return PHY_LINK_DOWN; /* SMI 读失败多半是 MDIO 引脚配置错 */ } if (!(bsr PHY_LINK_STATUS)) { return PHY_LINK_DOWN; /* 网线没插或者对面没开 */ } /* 读第二次拿到具体的速率/双工有些 PHY 需要读两遍才更新 */ ETH_ReadPHYRegister(PHY_ADDRESS, PHY_BSR, status); if (status PHY_100M_STATUS) { return PHY_LINK_100M_FULL; } return PHY_LINK_10M_FULL; }PHY_ADDRESS是 PHY 芯片在 SMI 总线上的地址由硬件上拉电阻决定常见是 0 或者 1跟原理图对不上就永远读回 0xFFFF。PHY_BSR是标准寄存器 0x01PHY_LINK_STATUS是其中的 bit2。上面那段「读两遍」不是冗余写法标准寄存器里的速率位是 latch-low 语义第一次读会把旧值清掉读第二遍拿到的才是当前状态这个细节看手册才能发现。实际工程里我不会在low_level_init里死等链路建立而是起一个低优先级的链路监控任务每 500ms 查一次状态变化时回调netif_set_link_up/down。等在那里的写法一旦现场网线没插设备就卡在启动阶段看上去像死机。3.3 ping 通之前按这个顺序排查从「烧录完成」到「电脑能 ping 通」中间大概有七八个环节任何一环断了表现都一样。我习惯按下面的顺序走一步一验证比乱改代码快得多看链路状态。串口打印出协商结果是 100M 还是 10M如果是 down先查网线、查 PHY 地址、查 RMII 的 50MHz 时钟这个时钟必须由外部晶振或 MCU 的 MCO 提供频率不对 SMI 也读不通。看 MAC 地址。在电脑上arp -a看有没有对应条目没有说明 ARP 包根本没发出去问题在发送路径。看 IP 配置。DHCP 模式下串口要打印拿到的地址静态 IP 要确认和电脑在同一网段且不冲突。这里最常见的错误是子网掩码填成 255.255.255.0 但网关写成 192.168.1.1 而板子 IP 是 192.168.0.x跨网段发不出去。抓包确认方向。电脑上用抓包工具挂在网口上ping时如果能看到 ARP 请求但没有回应说明板子收到了但没处理问题在ethernetif_input或者 ARP 层如果连请求都看不到问题在电脑这头的路由或者防火墙。最后才是协议栈配置。前面四步都正常再看lwipopts.h里LWIP_ICMP是不是被关掉了——这个宏关掉之后 ping 就永远不通但 TCP 收发完全正常很多人卡在这里一下午。注意交换机端口和板子的双工模式必须一致。自协商失败被迫落到半双工时ping 小包正常、大包丢一半这个现象和 pbuf 池太小几乎一模一样靠抓包看有没有冲突帧才能区分。4. FreeRTOS 与 LwIP 缝合sys_arch、任务与内存池NO_SYS0之后LwIP 需要一个操作系统适配层也就是sys_arch.c里那几个函数。这份工程把 FreeRTOS 的信号量、互斥量、邮箱映射成 LwIP 期望的接口同时起一个tcpip_thread专门处理协议栈内部逻辑。缝合得好收发任务互不阻塞缝合得不好就是随机死机。4.1 sys_arch.c 里的三个核心实现LwIP 在操作系统模式下要求实现信号量、互斥量和邮箱。最简的一版用 FreeRTOS 原生 API 就能写出来/* Middlewares/Third_Party/LwIP/system/OS/sys_arch.c */ err_t sys_sem_new(sys_sem_t *sem, u8_t count) { /* 二值信号量count 决定初始状态0 表示阻塞1 表示可用 */ *sem xSemaphoreCreateBinary(); if (*sem NULL) { return ERR_MEM; /* FreeRTOS 堆不够configTOTAL_HEAP_SIZE 该加了 */ } if (count 0) { xSemaphoreGive(*sem); } return ERR_OK; } void sys_sem_signal(sys_sem_t *sem) { xSemaphoreGive(*sem); /* 在任务上下文调用不要在 ISR 里用 */ } u32_t sys_arch_sem_wait(sys_sem_t *sem, u32_t timeout) { TickType_t ticks; if (timeout 0) { ticks portMAX_DELAY; /* 0 在 LwIP 语义里表示永久等待 */ } else { ticks pdMS_TO_TICKS(timeout); if (ticks 0) { ticks 1; /* 至少等一个 tick避免 0 变成非阻塞 */ } } return (xSemaphoreTake(*sem, ticks) pdTRUE) ? 0 : SYS_ARCH_TIMEOUT; }第一个坑在sys_arch_sem_wait的超时单位上。LwIP 传进来的是毫秒FreeRTOS 等的是 tick中间必须用pdMS_TO_TICKS换算。换算后如果小于 1 个 tick有些实现直接返回非阻塞结果就是协议栈的重传定时器疯狂空转CPU 占用飙升但功能看着还正常。上面这段把最小值兜到 1 tick是踩过才加的。第二个坑是上下文。sys_sem_signal在 LwIP 里既可能被任务调用也可能被中断调用的路径触发比如底层收包回调如果确认会在中断里走到就得分出sys_sem_signal_isr用xSemaphoreGiveFromISR并且在ethernetif_input里判断当前是不是中断上下文。混用会导致中断里调用了带阻塞的 API触发configASSERT死循环。/* 邮箱在最简实现里可以直接退化成一个队列长度 1 就够 */ err_t sys_mbox_new(sys_mbox_t *mbox, int size) { *mbox xQueueCreate(size, sizeof(void *)); return (*mbox ! NULL) ? ERR_OK : ERR_MEM; } void sys_mbox_post(sys_mbox_t *mbox, void *msg) { /* 这里用 portMAX_DELAY 是安全的tcpip_thread 一定在消费 */ while (xQueueSend(*mbox, msg, portMAX_DELAY) ! pdTRUE) { } }邮箱的size参数 LwIP 内部调用时常常传 0表示「默认大小」实现里要把它兜成至少 1否则xQueueCreate(0, ...)直接返回 NULLtcpip_init起不来表现为程序卡在启动阶段不进主循环。4.2 任务划分与优先级缝合完成之后要规划任务。这套组合里至少有这么几个执行体在跑任务/中断优先级建议职责栈大小参考tcpip_thread高于应用任务处理协议栈内部事件、定时器1024 字ethernetif_input中高从 DMA 取帧、包成 pbuf、投递给协议栈512 字链路监控低每 500ms 查 PHY 状态256 字TCP 服务任务中阻塞在accept/recv上1024 字UDP 上报任务低周期性发数据512 字tcpip_thread的优先级必须高于所有调用 socket 的应用任务否则应用任务一直占着 CPU 不让出协议栈的定时器和输入处理排不上队表现出来就是「TCP 连接能建立但收发延迟极大」。反过来说链路监控这种纯轮询的活优先级要低别跟协议栈抢。栈大小这一列只是参考值。socket 接口内部调用层次比 raw API 深得多recv一次要走 netconn、api_msg、tcp 好几层栈给小了会在某个特定长度的大包上栈溢出现象是随机 HardFault 或者数据错乱用 FreeRTOS 的uxTaskGetStackHighWaterMark定期打印余量最稳妥。4.3 用 socket 写一个 TCP 回显任务底层通了之后应用层用 BSD socket 写起来最舒服。下面是一个能实际跑的回显服务收什么发什么拿来验证链路最直接/* App/app_tcp_server.c */ #include lwip/sockets.h #define ECHO_PORT 5000 #define BUF_SIZE 1024 static void tcp_echo_task(void *arg) { int listen_fd, conn_fd; struct sockaddr_in addr, cli; socklen_t cli_len sizeof(cli); char *buf NULL; int n; buf pvPortMalloc(BUF_SIZE); /* 用 FreeRTOS 堆别用栈上大数组 */ if (buf NULL) { vTaskDelete(NULL); } listen_fd lwip_socket(AF_INET, SOCK_STREAM, 0); if (listen_fd 0) { goto exit; } memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(ECHO_PORT); /* 端口必须转网络字节序 */ addr.sin_addr.s_addr htonl(INADDR_ANY); /* 监听所有本地地址 */ if (lwip_bind(listen_fd, (struct sockaddr *)addr, sizeof(addr)) 0) { goto exit; } if (lwip_listen(listen_fd, 2) 0) { /* backlog2MEMP_NUM_TCP_PCB_LISTEN 要够 */ goto exit; } for (;;) { conn_fd lwip_accept(listen_fd, (struct sockaddr *)cli, cli_len); if (conn_fd 0) { vTaskDelay(pdMS_TO_TICKS(100)); /* 描述符耗尽时别空转 */ continue; } while ((n lwip_recv(conn_fd, buf, BUF_SIZE, 0)) 0) { lwip_send(conn_fd, buf, n, 0); /* 回显原样发回 */ } lwip_close(conn_fd); /* 对端断开或出错必须关 */ } exit: if (listen_fd 0) { lwip_close(listen_fd); } if (buf) { vPortFree(buf); } vTaskDelete(NULL); }几个参数直接决定稳定性。htons(ECHO_PORT)不能省忘了转字节序端口会变成 0x8813 之类客户端连不上还查不出原因。backlog传 2 意味着最多两个已完成握手的连接排队超过就被拒和MEMP_NUM_TCP_PCB_LISTEN是两回事后者是监听控制块数量。lwip_recv返回 0 表示对端正常关闭返回负值才是错误while循环里把两种情况都退出来、然后lwip_close释放描述符这一步漏掉的话连接反复建立几次之后MEMP_NUM_TCP_PCB就被耗光之后accept一直失败。缓冲区用pvPortMalloc而不是在任务里开char buf[1024]是因为任务栈是稀缺资源1KB 的局部数组会直接把默认栈吃掉一大半。用完记得vPortFree虽然这个任务是个死循环但养成习惯能避免以后改成可退出任务时漏释放。5. 避坑与排查四个现场跑出来的真实故障协议栈这种东西功能跑通只是开始稳定跑一周才算数。下面这几条都是这类工程里反复出现的故障每条按「看到什么 → 为什么 → 怎么改」写遇到时可以直接对着查。5.1 网口灯亮ping 不通串口无任何报错现象链路灯常亮DHCP 也打印了拿到的 IP但电脑ping全是超时arp -a里没有板子的条目。原因最常见的是发送路径的 DMA 描述符没有正确链起来或者low_level_output里忘了把描述符的 owner 位交还给 DMA。另一个高频原因是 MAC 地址在netif_add之后才设置LwIP 内部缓存的地址和硬件里写的不一致ARP 回复用的源地址是旧的。解决在low_level_output里发完帧之后打印一次HAL_ETH_GetTxBufferOwnership的状态确认描述符确实被硬件拿走了MAC 地址的设置顺序调整成「先HAL_ETH_SetMACAddress再netif_add并填好hwaddr」两边保持一致。5.2 小包正常大包丢一半现象ping 192.168.x.x正常ping -l 1400丢包率 50% 甚至更高TCP 传小文件没事传几百 KB 就断。原因三种可能按概率排是 pbuf 池太小、双工模式不匹配、接收缓冲区溢出。PBUF_POOL_SIZE只有 4 的时候连续来几个大帧池子就空新帧被硬件直接丢弃没有日志。半双工则会在抓包里看到冲突帧。解决先把PBUF_POOL_SIZE提到 16、PBUF_POOL_BUFSIZE提到 1524 试一次如果没改善串口打印 PHY 协商结果确认是不是全双工再不行就要看ethernetif_input的处理速度是不是在里面做了耗时的业务逻辑导致取帧不及时。5.3 跑几小时后 malloc 失败功能逐渐停摆现象设备刚上电一切正常跑两三个小时后连接建不起来串口打印mem_malloc: out of memory。原因典型的内存泄漏。socket 的连接描述符、pbuf、netconn 结构体都是池化的用完不还就会耗尽。最常见的漏点是lwip_recv返回错误后没走lwip_close或者在low_level_output出错的分支里没pbuf_free。解决把LWIP_STATS和MEMP_STATS打开定期打印各池子的使用峰值看哪个先满。MEMP_NUM_TCP_PCB满了是连接没关PBUF_POOL满了是收包后没释放两种指向的代码位置完全不同。提示调试期把LWIP_DEBUG打开只开MEM_DEBUG和PBUF_DEBUG两个模块日志量可控又能在泄漏发生的第一现场看到是谁分配的。5.4 拔插网线后必须重启才能恢复现象现场调整线路网线拔掉再插上链路灯正常但设备再也通信不了。原因没有处理链路状态变化。LwIP 的netif一直认为是 up 状态ARP 表里缓存的对端地址还是旧的DHCP 租约也没有重新申请。解决打开LWIP_NETIF_LINK_CALLBACK注册回调函数在链路 down 时调netif_set_link_down、清 ARP 表etharp_cleanup_netif链路 up 时重新走一遍 DHCP 或者netif_set_up。再配套一个链路监控任务每 500ms 查 PHY 寄存器比等硬件中断更可靠。6. 二次开发改造把 demo 变成自己的设备拿到这样一份能跑的工程接下来真正花时间的是「改成自己的东西」。我的习惯是先定三件事再动代码。第一件是网络角色。设备是客户端还是服务端决定了任务模型完全不同。做数据采集盒设备主动连服务器那就要写断线重连逻辑连接失败等 5 秒重试重试次数指数退避避免服务器没起来时把局域网的连接请求打爆。做本地控制器面板设备监听端口等上位机来连重点是处理多连接和超时断开。这两种模型可以共存各起一个任务但要想清楚MEMP_NUM_TCP_PCB够不够两边用。第二件是参数持久化。IP 是静态还是 DHCP、服务器地址、端口号这些不该写死在代码里。F407 内部有 Flash 扇区可以模拟 EEPROM我一般留最后一个扇区存一个带 CRC 的结构体上电读出来校验失败就落回默认值。改参数的操作通常走一个简单的串口命令或者干脆在 TCP 服务里开一个配置通道。第三件是存储和内存边界。协议栈、FreeRTOS 堆、业务缓冲区三者的 RAM 分配必须一次性算清楚。我的经验值是LwIP 的MEM_SIZE加各个池子控制在 40KB 以内FreeRTOS 堆给 30KB剩下的留给全局变量和栈。改完配置先跑uxTaskGetStackHighWaterMark和memp_get_stat看余量别等到现场才炸。下面这段是我常用来加断线重连的骨架思路很简单把连接和业务拆开连接断了业务任务阻塞在信号量上重连成功再放行static SemaphoreHandle_t s_net_ready; static void client_task(void *arg) { int fd -1; int backoff 1; for (;;) { if (fd 0) { fd tcp_connect_to_server(SERVER_IP, SERVER_PORT, 3000); if (fd 0) { vTaskDelay(pdMS_TO_TICKS(backoff * 1000)); backoff (backoff 30) ? backoff * 2 : 30; /* 指数退避封顶 30s */ continue; } backoff 1; /* 连上了就复位退避计数 */ xSemaphoreGive(s_net_ready); } if (send_heartbeat(fd) 0) { /* 心跳失败说明连接已死 */ lwip_close(fd); fd -1; /* 下一轮循环重新连 */ } vTaskDelay(pdMS_TO_TICKS(10000)); } }退避那一行是血泪经验。早期我写的是固定 1 秒重试结果服务器维护的半小时里现场十几台设备每秒发一次 SYN交换机上的连接表直接打满维护结束后所有设备还在抢反而更慢恢复。改成指数退避、封顶 30 秒之后这台设备的恢复时间从几分钟稳定到十几秒。验证方法是把服务器端口故意关掉观察串口打印的重连间隔是否按 1、2、4、8、16、30、30 递增再开端口看能不能在 30 秒内自动恢复。从那以后我每次改网络相关的配置都会强制走一遍「拔网线、关服务器、改 IP 冲突、断电量重启」这四步不让它在实验室里顺顺利利地跑过去。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑