ARTICLE DETAIL

资讯详情

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

STM32+lwip+FreeRTOS串口转TCP服务器实战解析

STM32+lwip+FreeRTOS串口转TCP服务器实战解析 简介lwip-2.0.3 是一份面向嵌入式与物联网开发者的轻量级 TCP/IP 协议栈源码资源专为单片机、传感器节点等 RAM 受限设备提供完整的网络通信能力并支持以太网、PPP、Wi-Fi 等不同硬件接口。资源包为 zip 格式压缩后约 2.94MB文件总数与具体类型明细暂未标注根据版本介绍包内可能包含协议栈核心源代码、示例程序、配置工具及使用文档便于直接研究或集成到项目中。该资源目前已吸引 160 人学习下载适合正在学习网络协议栈实现或准备为嵌入式设备添加联网功能的开发者参考。通过分析这份代码读者可以深入理解 IPv4/IPv6、TCP/UDP、ICMP、DHCP/DNS 等协议在轻量级环境下的设计取舍掌握多线程并发、动态内存管理与滑动窗口、拥塞控制等底层机制的实际写法并据此定制内存池和连接数等参数满足不同应用场景。 最近在给一块STM32H750板子做协议转换功能串口那头接的是传感器网络那头需要提供一个TCP服务端把串口收到的数据包装成报文送出去。这块板子从底层到应用全部自己搭协议栈选型时没有犹豫直接用了lwip-2.0.3跑在FreeRTOS环境下。前前后后调了两周踩的坑比预想的多想把完整链路记录下来从CubeMX怎么配到串口数据怎么进协议栈再到排查问题的思路给准备动手做同类项目的朋友一些参考。lwip这个名字在嵌入式联网领域基本绕不开它是专门为资源受限设备写的轻量级TCP/IP协议栈C语言实现事件驱动移植性极强。网上讲lwip的文章很多但绝大多数停在能ping通的层面真正把配置、数据包装和问题排查串起来的反而不多。1. 选2.0.3理由不是追新而是图稳1.1 版本对比用过lwip的人都知道它版本迭代不快但分支不少。2.0.x是进入2.0时代后的第一批稳定分支2.0.3作为2.0.x中期版本既有2.0.0开始引入的架构改进又修复了大量早期问题整体API已经非常稳定。后来虽然有2.1.x和2.2.x但2.0.3在嵌入式MCU项目里依然有大量存量使用CubeMX甚至长期把它作为默认可选版本。我选2.0.3而不是追新版本核心理由就三个生态成熟搜索lwip移植问题时2.0.x的资料最多遇到报错基本都能找到同类案例。2.1和2.2在一些内部实现上有调整遇到问题反而得靠翻源码。CubeMX原生支持STM32CubeMX的LWIP中间件在老版本里默认就是2.0.3生成代码直接可用不用自己改移植层。稳定性经过验证2.0.3不是实验性分支很多量产产品跑的就是它协议栈本身只要配置合理长时间运行基本不会出幺蛾子。如果你做的是全新产品且没有历史包袱当然可以考虑更高版本。但如果你像我一样手里有现成的板子、现成的BSP用2.0.3能少走很多弯路。选技术栈不是选最新的是选最不容易翻车的。1.2 协议栈结构里值得先搞懂的几个点lwip是纯C写的这也是Vitis、CubeMX这些工具链能直接集成它的前提。它把整个网络分层抽象成三块netif层管网络接口core层管内存和pbufprotocol层实现TCP、UDP、ICMP、DHCP、ARP这些协议。对应用开发者来说真正接触最多的是pbuf和TCP控制块。pbuf是lwip里最基础的数据结构理解它很重要。它是协议栈内部的数据包载体分PBUF_RAM和PBUF_POOL等类型前者是连续内存后者是链式内存池。很多初学者在发送数据时搞不清楚该用哪种直接导致内存分配失败或者效率低下。后面讲数据包装时会专门说这块。在配置前还要先想清楚用哪种APIraw API基于回调不用操作系统也能跑性能最好但逻辑复杂。netconn API基于线程和信号量的封装阻塞式操作适合配合FreeRTOS使用开发效率高。socket API最接近桌面编程习惯适合逻辑复杂的应用。我项目里用的是raw API因为数据流比较固定串口收、TCP发回调模型足够用而且占用的任务少、可控性更强。2. CubeMXFreeRTOSlwIP三件套的配置细节2.1 CubeMX配置流程用CubeMX生成基础工程时有两个地方容易忽略一个是时间基准源另一个是LWIP版本选择。STM32的HAL库默认用SysTick做时间基准但FreeRTOS也需要SysTick来跑系统心跳两者会冲突。所以配置里必须把SYS - Timebase Source从SysTick改成某个硬件定时器比如TIM6或者TIM7把SysTick让给FreeRTOS。这个不处理好工程一跑起来就卡死而且是那种查不出来为什么的卡。然后在Middleware and Software Packs里勾选LWIP在配置页里需要留意几个关键项LWIP版本选2.0.3NO_SYS如果用了FreeRTOS这里选OS也就是NO_SYS0这样协议栈会使用操作系统相关的线程和信号量机制IP地址、子网掩码、网关我是做TCP服务端直接用静态IPDHCP留给后续功能扩展内存设置MEM_SIZE、PBUF_POOL_SIZE这些参数CubeMX有默认值但实际要根据项目需求调整以太网外设那边根据板子上的PHY芯片配置RMII或者MII接口。我做的是RMII模式LAN8720APHY地址0时钟由STM32的MCO输出50MHz给PHY芯片。第一次调的时候没接MCO时钟网口完全起不来后来用示波器才找到原因这属于硬件层面最容易踩的坑。2.2 内存模式和被忽略的关键参数FreeRTOSlwIP的内存管理是整个项目稳定性的命门。CubeMX的LWIP配置里有一组默认参数对很多小项目够用但并发一大就会出问题。我这次用到的关键参数如下仅供参考参数我用的值说明MEM_SIZE16*1024协议栈堆内存大小用于TCP段、选项等PBUF_POOL_SIZE20PBUF_POOL类型pbuf数量影响接收缓冲TCP_MSS1460以太网最大报文段大小TCP_WND4*TCP_MSSTCP接收窗口窗口越大吞吐越高TCP_SND_BUF4*TCP_MSSTCP发送缓冲区TCP_SND_QUEUELEN8*TCP_SND_BUF/TCP_MSS发送队列长度这些参数不是随便填的。比如TCP_WND直接决定接收端能缓冲多少数据如果设得太小发送端发完一个窗口就得等ACK吞吐量上不去。又比如PBUF_POOL_SIZE它是接收路径上的pbuf池如果池被耗尽网卡中断里分配pbuf就会失败直接丢包。常见表现是ping还挺通但TCP传一会儿就卡住。CubeMX生成代码后一般会生成lwip.c、lwip_freertos.c和ethernetif.c三个文件。ethernetif.c是平台移植层low_level_init负责初始化网卡描述符和DMA描述符low_level_input把接收到的数据包转成pbuf送入协议栈low_level_output把待发送的pbuf拷贝到DMA描述符指向的内存。理解这个文件后面调试很多现象就不慌了。2.3 FreeRTOS任务划分与线程安全CubeMX的lwip_freertos.c会自动创建tcpip_thread线程这个线程内部运行协议栈主循环。默认优先级是osPriorityHigh这基本够用。真正需要注意的是你在自己的任务里操作lwip API时要确保线程安全。raw API本身不是线程安全的也就是说如果你创建了自己的任务去调用tcp_write或tcp_output而这些操作同时在tcpip_thread上下文里也可能发生就有并发风险。CubeMX的移植层里sys_arch.h会把SYS_LIGHTWEIGHT_PROT设为1用信号量在进入协议栈关键代码时做保护。但在使用raw API时这个保护只覆盖了内部核心并不是说应用任务啥都不用管。我实际的做法是用一个专用的发送任务它只负责从环形缓冲区取数据、调用lwip发送接口。发送任务和TCP回调之间加一个互斥锁保证同一时间只有一个路径在操作TCP控制块。如果你用的是netconn API或者socket API它们内部已经做了线程安全不用这么费劲——这也是很多人劝新手用netconn的原因。3. 把串口数据包装成lwIP的报文我是这样处理的3.1 串口接收与组帧串口侧的流程传感器通过UART发来一帧数据帧格式是帧头0xAA 0x55、长度、负载数据、CRC校验。我在串口接收上用了DMA空闲中断的方式收到一帧完整数据后触发HAL_UARTEx_RxEventCallback把数据从DMA缓冲区拷贝到应用层的环形缓冲区。这一步的关键是不要在中断回调里做复杂的处理。中断回调函数里只做拷贝和设置标志位真正的解析和打包放在FreeRTOS任务里。因为lwip的发送接口可能阻塞如果放在中断上下文轻则造成死锁重则直接HardFault。嵌入式开发里中断服务函数越短越好这条铁律到lwip这里尤其要遵守。组帧解析时要注意边界情况比如长帧、半帧、丢帧。我的做法是解析函数维护一个状态机帧头没匹配到位就一直等匹配到帧头后再攒够长度和校验字段校验通过才算一帧完整的。因为这个状态机写在任务里不阻塞中断所以即使有半帧也只是内存里多等一个周期而已。3.2 cJSON集成上报的数据需要结构化所以用到了cJSON。网上lwip cjson 集成搜得挺多其实就是把cJSON这个C语言库加进工程然后调用它的API把结构体转成JSON字符串。cJSON可以直接从GitHub拉源码有两个文件cJSON.c和cJSON.h。序列化步骤很简单cJSON *root cJSON_CreateObject(); cJSON_AddStringToObject(root, device_id, sensor_01); cJSON_AddNumberToObject(root, temperature, 25.6); cJSON_AddNumberToObject(root, humidity, 48.2); char *json_str cJSON_PrintUnformatted(root); // json_str 就是待发送的字符串 // 使用完成后必须释放 free(json_str); cJSON_Delete(root);这里有个很多新手容易踩的坑cJSON_PrintUnformatted返回的是malloc出来的内存用完必须free不然几次上报以后内存就泄漏了。在带RTOS的内存管理下内存泄漏很难直接看出来要跑很久才会发现系统越来越卡。我建议所有调用cJSON打印函数的地方都遵守用完即free的纪律。3.3 用tcp_write把数据送进协议栈数据准备好后就是大家搜的最多的如何把数据包装成服务lwip的数据格式。其实lwip的收发接口没那么玄乎它最终接收的就是一段内存地址和长度。关键在于调用哪个接口、用什么标志位。我是在TCP连接建立后在发送任务里调用err_t err tcp_write(pcb, (void *)json_str, len, TCP_WRITE_FLAG_COPY); if (err ERR_OK) { tcp_output(pcb); }第一个参数是TCP控制块指针第二个是数据地址第三个是长度第四个TCP_WRITE_FLAG_COPY表示数据会被协议栈拷贝到内部缓冲区。这个标志位特别重要如果不加COPY标志lwip默认只保存指针那么你在发送后如果立刻释放json_str的内存数据就会变成野指针。所以要么加COPY让协议栈自己拷贝要么严格控制数据的生命周期。tcp_write的返回值也要重点看返回ERR_MEM说明协议栈内存不足返回ERR_WOULDBLOCK说明发送缓冲区满了需要等待tcp_sent回调后再继续。很多人直接忽略返回值数据发不出去也不知道为什么。正确的做法是发送任务里维护一个待发队列tcp_write返回错误时就缓存下来等tcp_sent事件来了再补发。最终发送的数据流是串口DMA收到原始帧解析任务从环形缓冲区取出完整帧解析字段后用cJSON构建JSON对象cJSON_PrintUnformatted生成字符串调用tcp_write和tcp_output发送发送完成后free字符串、删除cJSON对象整个过程没有用任何特殊封装就是普通C函数的顺序调用。很多人问lwip的数据格式是什么其实就是指pbuf和TCP控制块你只要按API参数填就行。4. 移植和调试阶段遇到的那些坑4.1 网络链路起不来的排查项目刚开始出现最头疼的问题是网线插上lwip初始化的状态始终是NETIF_FLAG_LINK_UP无法置位ping不通。排查思路我建议按这个顺序来PHY芯片的复位时序LAN8720A的复位引脚由STM32控制复位后必须等待一段时间不然PHY没有准备好读寄存器全返回0xFF。RMII时钟确认MCO1引脚是否配置对输出的是不是50MHz。PHY地址LAN8720A的地址默认是0但有些板子做了地址跳线要看原理图。地址不对HAL库的HAL_ETH_Init会超时。中断引脚ETH的MDIO中断或RMII的REF_CLK有没有接对。我最终定位到是硬件复位引脚没拉高的时序问题。CubeMX生成的初始化代码在ethernetif.c的low_level_init里会读取PHY寄存器如果PHY没从复位状态恢复读出来的值就是错的。解决办法是在外部引脚初始化时强制给PHY的复位引脚一个完整的复位时序拉低至少100ms再拉高。4.2 中断优先级和线程安全这个坑排查了我将近一天典型的偶发卡死问题系统跑几分钟后TCP连接就完全没反应了但FreeRTOS的其他任务还在正常运行。用调试器挂上去一看tcpip_thread卡在了sys_arch_mbox_fetch上大概是邮箱等待超时。后来翻ST的参考手册和lwip的FAQ才知道ETH中断优先级必须低于FreeRTOS可管理的中断优先级上限configMAX_SYSCALL_INTERRUPT_PRIORITY。我在CubeMX里把ETH中断优先级设置成了5但是FreeRTOS的configMAX_SYSCALL_INTERRUPT_PRIORITY是5刚好踩线。因为中断里用了osSemaphoreRelease这类从ISR调用的FreeRTOS API如果优先级高于或等于这个上限系统行为未定义而且这种未定义特别隐蔽不是必现而是偶尔。正确做法是把ETH中断优先级设得比configMAX_SYSCALL_INTERRUPT_PRIORITY更低比如设为6或7。同理串口DMA中断、定时器中断也都要检查一遍。这个坑的本质是FreeRTOS在中断上下文调用API对中断优先级是有要求的不是随便设一个数字就行。4.3 数据收发不顺畅时如何定位问题TCP收发不顺畅时的调试我强烈建议开lwip的DEBUG输出。在lwipopts.h或CubeMX配置里打开#define LWIP_DEBUG 1 #define TCP_DEBUG LWIP_DBG_ON #define ETHARP_DEBUG LWIP_DBG_ON #define NETIF_DEBUG LWIP_DBG_ON再把LWIP_DBG_TYPES_ON设置为LWIP_DBG_TRACE | LWIP_DBG_STATE | LWIP_DBG_FRESH日志会详细很多。刚开始可能嫌日志多但排查问题真香。日志会显示TCP状态机的切换过程比如SYN_SENT到ESTABLISHED有没有正常走完重传有没有异常增多。另外一个更直观的办法是用抓包工具看实际网络报文。lwip开发板接交换机电脑跑Wireshark抓包能看到SYN、ACK、PSH、FIN这些标志位是否正常也能看到重传。如果lwip发出大量重传说明对端没有及时给ACK可能是接收窗口满也可能是网络线路问题。我记得有一次现象是TCP连接能建立数据能发但发一会儿就停住。抓包发现是lwip触发了零窗口也就是接收方的窗口被占满了。原因就是我在tcp_recv回调里处理完数据后忘了调用tcp_recved来更新接收窗口。这个API的作用是告诉协议栈数据我已经取走可以继续接收了。漏掉它接收窗口永远不会复位连接就慢慢卡死。回调处理里还有一个容易遗漏的在tcp_recv回调中如果返回错误或者不处理数据协议栈不会自动释放那个pbuf最后会造成pbuf池耗尽。正确的做法是接收回调里要么立即处理并tcp_recved要么pbuf_free释放掉。5. 调参建议和一点经验总结5.1 内存与窗口参数调整项目跑稳定后可以做一轮参数调优。很多人用lwip默认参数功能没问题但性能不好。对于STM32H7这种主频不低的MCUTCP吞吐量提升空间很大。我实测下来提升最明显的是这几个TCP_WND从默认值改成10 * TCP_MSS在局域网环境下吞吐量能明显上去。WND越大单次能确认的数据越多不用频繁等ACK。TCP_SND_BUF发送缓冲区要足够大不然应用层一写快tcp_write就返回ERR_MEM。我把它调到8 * TCP_MSS。MEM_SIZE如果用了上述较大的窗口和发送缓冲区堆内存也要相应增加否则系统一峰值就分配失败。实测MEM_SIZE从16K调到32K后稳定性明显提高。需要提醒的是这些参数是此消彼长的关系。MCU的RAM是固定的TCP窗口越大留给任务栈和业务逻辑的RAM就越少。建议根据实际报文大小和并发量折中。5.2 该用raw还是netconn这几个星期下来我对API选型的看法更明确了。如果项目简单、逻辑清晰比如我这个串口转TCP的透传场景raw API合适省内存、性能好、可预测性强。如果应用层逻辑复杂比如需要同时处理多个socket、做重连、做心跳超时推荐用netconn或者socket API因为它们在任务里能阻塞等待代码写起来像桌面程序一样自然。FreeRTOS配合lwip时还可以考虑一个折中方案把lwip操作都集中在一个专门的网络任务里所有业务模块通过消息队列给它发指令。这样既保留了raw API的高性能又把并发问题缩小到一个任务内部排查起来很轻松。5.3 后续还能怎么扩展这次项目只做了TCP服务端但lwip 2.0.3里的DHCP、UDP、IGMP这些协议都是现成的。后续如果要做动态获取IP只需要在CubeMX里打开DHCP初始化时调用dhcp_start就行。如果要做UDP广播做设备发现也是几行代码的事。另外一个很实用的扩展方向是和cJSON配合做自定义应用层协议比如定义{cmd:query,params:{}}的请求包服务端收到后解析JSON再组织JSON响应。这种模式的维护性远好于裸字节流协议而且跟lwip的结合很自然本质上就是收一个JSON、回一个JSON而已。最后想说的是lwip移植调试最忌讳一把梭。先把能ping通作为第一个里程碑再把TCP回环调通最后再串上业务逻辑。每步有明确的验证标准出了问题才知道是协议栈、硬件还是应用层的问题。我前两周之所以踩坑多就是因为太急着一步到位结果所有变量堆在一起根本没法定位。本文还有配套的精品资源点击获取
返回列表