十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

lwip-2.0.3移植实战:基于STM32的以太网开发与调试

lwip-2.0.3移植实战:基于STM32的以太网开发与调试 简介这是一份面向嵌入式软件工程师、物联网产品开发者和进阶学习者的 lwip-2.0.3 轻量级 TCP/IP 协议栈资源包。lwip 由瑞典计算机科学院的 Adam Dunkels 发起设计目标是在 RAM 紧缺的微控制器环境中提供高效网络能力以太网、PPP、Wi-Fi 等接口均可接入协议栈完整支持 IPv4/IPv6、TCP、UDP、ICMP、DHCP、DNS其中 TCP 实现了拥塞控制、滑动窗口与重传机制UDP 则面向低延迟实时场景。zip 压缩包约 2.94MB内容以协议栈核心源码、示例程序、配置模板与 API 文档为主用户可依据项目需求裁剪协议、调整内存池大小和最大连接数多任务模型与动态内存管理也让并发连接处理更灵活。已有 160 人学习适合物联网终端、智能家居、工业自动化等场景既能直接集成网络通信能力也能帮助开发者深入理解 lwip 的移植路径与调优方法。 干了这么多年嵌入式网络开发lwip-2.0.3 这个版本算是我接触最多、踩坑也最深的协议栈之一。直到现在不少 STM32 项目、尤其是基于 CubeMX 自动生成代码的工程底子依然是它。所以今天想把这个老伙计的移植、配置和常用玩法从头到尾梳理一遍重点放在实操层面——包括 CubeMX 下怎么和 FreeRTOS 搭配、lwip 数据格式怎么理解、cJSON 怎么集成、串口调试怎么搞。这篇东西适合正在做以太网设备、或者准备在单片机上调通网络功能的朋友无论是刚接触协议栈的初学者还是被各种疑难杂症折磨过几轮的开发者应该都能找到点有用的东西。1. lwip-2.0.3 到底是什么来头1.1 这版协议栈的核心能力lwiplightweight IP是一个开源的轻量级 TCP/IP 协议栈专门为资源受限的嵌入式系统设计。2.0.3 属于 2.0 系列的一个稳定小版本它支持完整的 IPv4 和 IPv6 双栈TCP、UDP、ICMP、IGMP 这些常用协议全覆盖还带 netconn 和 socket 两种 API 接口。对于那些需要跑 HTTP、MQTT、Modbus TCP 等应用的 MCU 设备来说这已经足够撑起绝大多数业务了。版本号看着不起眼但 2.0.3 里已经包含了不少让嵌入式开发省心的重要特性。比如它支持校验和硬件卸载CHECKSUM_CHECK/CHECKSUM_GEN 配合网卡驱动有内存池memp和内存堆mem两种分配方式可切换还支持零拷贝能让数据在协议栈和应用层之间少做一次复制。在 Cortex-M7 内核的 STM32H7 上跑如果时钟和 DMA 配置到位双向吞吐做到几十 Mbps 是没问题的。1.2 为什么工控和物联网项目还在用它你要说新协议栈2.1.x、2.2.x 甚至 3.x 都有了为什么还有这么多项目锁死在 2.0.3我自己的体会就一个字稳。2.0.3 的代码路径非常成熟网上能找到的教程、驱动和案例基本都是围绕这个版本写的踩坑记录也丰富真出问题不会孤立无援。另一个关键原因是工具链的绑定。STM32CubeMX 的早期 H7/F7 固件包内置的正是 lwip 2.0.3。很多公司从老产品上继承下来的代码底层驱动、PHY 芯片初始化、中断处理跟这个版本的协议栈是深度耦合的贸然升级到 2.1.x 要改不少接口。所以对于商业项目来说“能稳定跑、没人愿意动”就是最大的选型理由。2. 拿到源码之后先别急着开编译器2.1 源码目录里都有啥哪些必须自己改lwip 2.0.3 的源码目录结构很清晰。src/core/是协议栈核心代码包括 TCP/IP 协议实现、内存管理、超时处理等这部分基本不用改。src/api/是 netconn 和 socket API 的实现。src/netif/是网络接口层里面有一个ethernet.c负责以太网帧的输入输出还有个loopif.c是回环接口调试时偶尔用得上。真正需要你动手的是src/port/或者你自己的 port 目录这里要提供操作系统抽象层和网卡驱动。如果你是从 STM32CubeMX 生成的工程里看到的 lwip通常代码会被放到Middlewares/Third_Party/LwIP/下面。这个目录里已经集成了适配 STM32 的网卡驱动在src/netif/ethernet.c和src/netif/etharp.c配合下以及基于 FreeRTOS 的 sys_arch 实现。即便这样你还是需要确认 PHY 芯片的地址、中断引脚、复位引脚这些配置对不对。2.2 对照 lwipopts.h 和 opt.h 理清配置思路lwip 的配置分层很有意思。opt.h是协议栈自己的默认配置位于src/include/lwip/opt.h。你自己工程里的lwipopts.h则通过宏覆盖默认值优先级更高。这个机制一定要搞明白——有些人改了opt.h半天不生效其实是被lwipopts.h里的宏顶掉了。配置里最核心的是这几个NO_SYS决定要不要跑操作系统。配合 FreeRTOS 时设为 0表示使用多线程模式裸机跑就设为 1此时 netconn 和 socket API 不可用只能用 raw API。MEM_SIZE整个协议栈使用的堆内存大小单位是字节。TCP 传输大数据量时这个值太小会导致内存分配失败。MEMP_NUM_TCP_SEGTCP 分段的数量影响同时能缓存多少个 TCP 段。发送窗口较大时这里也需要跟着加大。TCP_WNDTCP 接收窗口大小决定了接收方一次能通告给对端的缓存能力。在 H7 这种内存大的 MCU 上可以适当调大比如 64KB能明显提升吞吐。CHECKSUM_GEN_IP/CHECKSUM_GEN_UDP/CHECKSUM_GEN_TCP如果网卡驱动里做了硬件校验和生成就把这些宏关掉否则校验和逻辑会重复计算白耗 CPU 时间。在 CubeMX 生成的工程里这些配置通常集中在lwipopts.h的一个大段注释下面。我的习惯是先建立一个小型测试工程用默认配置跑通 DHCP然后再逐步调大内存去压吞吐这样排查问题的时候能快速缩小范围。3. 在 STM32H7 上把 lwip-2.0.3 真正跑起来3.1 CubeMX 里的版本锁定和固件包问题用 CubeMX 生成 lwip 工程第一步其实是选对固件包版本这一点很多人会忽略。CubeMX 左侧的“Firmware Package Version”会决定集成的 lwip 是哪个版本。同一个 MCU 型号有的固件包内置 2.0.3有的内置 2.1.3版本不一致会导致sys_arch.h和arch.h的实现方式有差异。所以当你发现网上代码和自己工程对不上时先检查固件包版本别急着改代码。在 H7 系列上我强烈建议第一次直接生成一个最小工程开启 ETH 外设、LAN8720A PHY大部分 H7 开发板都配这个、配置中断引脚、接好 RMII 接口然后在 Middleware 里勾选 lwip并且把操作系统那一栏选择为 CMSIS_V2 或 FreeRTOS。CubeMX 会自动把网卡驱动和 lwip 串起来生成ethernetif.c里面已经实现了网卡发送、接收和底层初始化接口你只需要保证 PHY 地址和时钟没问题。3.2 网卡初始化与中断模型的细节STM32H7 的以太网 MAC 使用 DMA 描述符来收发数据CubeMX 生成的代码里默认有两个 RX 描述符和两个 TX 描述符。这里有一个非常容易踩的坑如果你在lwipopts.h里把PBUF_POOL_SIZE调小了但 RX 描述符数量是 4、8那么接收缓存的实际数量可能比描述符还少最终导致丢包。我的习惯是让PBUF_POOL_SIZE和 RX 描述符数量保持一致或者更大。中断处理上Ethernet_IRQHandler触发后会调用HAL_ETH_IRQHandler然后通过osSemaphoreRelease释放一个信号量唤醒ethernetif_input线程。这个线程调用netif-input(pbuf)把数据交到协议栈。发送方向则由应用层线程或 socket 层直接调用网卡发送函数。这种“中断唤醒 线程处理”的模型之所以是主流是因为它把数据接收从 ISR 上下文搬到了线程上下文协议栈可以在处理 TCP 分段、滑动窗口、重传这些逻辑时自由调用会阻塞的操作。如果你在裸机上跑就得改成轮询模式或者在主循环里不断调用ethernetif_check那样实时性和 CPU 占用会差一些。3.3 和 FreeRTOS 集成时最容易被忽略的配置lwip 2.0.3 在 CubeMX 中默认和 FreeRTOS 的 CMSIS_V1 或 CMSIS_V2 配合。你需要确认sys_arch.c里创建了几个线程通常有tcpip_thread协议栈核心线程、ethernetif_input网卡接收线程、dhcp_thread动态获取 IP等。每个线程的栈大小直接关系到稳定性。tcpip_thread默认栈可能只有 1KB 到 2KB如果同时跑 HTTP server 和 MQTT很容易栈溢出。排查线程栈问题时除了看现象死机、HardFault、TCP 连接反复断开还可以在 FreeRTOS 的vApplicationStackOverflowHook里挂一个断点。这个钩子函数会被调用说明确实有线程把栈用穿了。我的经验是tcpip_thread和ethernetif_input的栈至少给 2KB 以上LWIP_TCPIP_CORE_LOCKING如果开启还要小心并发访问对共享资源的同步问题。4. 数据格式、串口调试与 cJSON 集成4.1 从网口到串口把数据包变成可观测的调试流开发网络设备时最大的痛点之一就是“看不见网络数据”。在 PC 上我们可以开 Wireshark但在单片机上最方便的手段就是把串口变成调试透传口把 lwip 收到的原始帧或者协议解析结果打印出来。最简单直接的方式是在ethernetif_input里把struct pbuf *p的数据内容打印出来。pbuf可能是一个链表所以要用pbuf_copy_partial把数据拷贝到本地缓冲区或者直接遍历 pbuf 链逐个打印。注意千万不要直接用p-payload去 printf因为一个 pbuf 往往只是一段数据的一部分真正的载荷跨越了多个 pbuf。“把数据包装成服务 lwip 的数据格式通过串口发出”这个需求我实际做过的方案是在串口接收中断里把整帧数据收到一个环形缓冲区然后在主循环或独立线程中把这块数据复制到一个struct pbuf *再用netif-output发送出去。这样做的本质是在单片机上实现了一个串口转以太网的网关。数据格式上需要手动构造以太网头目的 MAC、源 MAC、类型字段 0x0800再加上 IP 头、TCP/UDP 头。这个工作听起来麻烦但借助网卡驱动已有的接口实际做完之后对协议理解的帮助非常大。4.2 用 cJSON 在 lwip 上解析和构造应用数据当一个嵌入式设备要接入 IoT 平台或提供 HTTP API 时JSON 几乎成了事实标准。lwip 本身不管 JSON我们一般会引入 cJSON 这个单文件库。cJSON 非常轻量只有一个.c文件和一个.h文件可以直接塞进工程里编译完全不折腾。在 socket 或 netconn API 上集成 cJSON 很简单。比如设备收到一个 HTTP POST 请求body 里是 JSON 数据。先用netconn_recv拿到netbuf拷贝成以\0结尾的字符串然后用cJSON_Parse解析。如果解析失败用cJSON_GetErrorPtr()定位是哪个位置出了问题。返回数据时用cJSON_CreateObject、cJSON_AddNumberToObject这些函数构造一个 JSON 对象再cJSON_PrintUnformatted转成字符串走netconn_write或者send发送回去最后cJSON_Delete释放内存。这里有几个容易踩的坑一是 cJSON 在堆上分配内存如果lwipopts.h里MEM_SIZE和MEMP_NUM_NETBUF不够JSON 对象构造到一半会返回空指针二是 JSON 打印出来的字符串长度是不固定的发送前一定要用strlen重新算长度别用之前分配的缓冲区大小三是在多线程环境下cJSON 默认用的是malloc/free而嵌入式工程里malloc未必线程安全最好把cJSON_hooks配置成 lwip 的mem_malloc/mem_free。5. 常见问题排查与避坑实录5.1 我实际遇到的几个典型故障现象一能 ping 通但 TCP 连接打不开。这种问题通常出在端口监听和防火墙之外最可能是 PCB 布局或 PHY 配置问题但也有可能是 lwip 配置太保守。我的排查顺序是先确认 DHCP 是否拿到地址看netif-ip_addr再检查本机是否能 telnet 通最后抓包。如果 ping 响应正常但 TCP 超时重点看TCP_MSS和TCP_WND的配置。H7 上以太网 MTU 是 1500TCP_MSS如果被设成 1024 而你知道抓包显示对端通告了 1460那就要注意分片和 MSS 协商的差异。现象二DHCP 一直拿不到地址。PHY 芯片的 link 状态没起来是最常见的。LAN8720A 的地址配置错有的开发板是 0x00有的是 0x01或者晶振频率不对LAN8720A 必须用 50MHz 外部有源晶振都会导致 PHY 无法完成自协商。调试时先读 PHY 寄存器 1 的 bit2看 link 状态位是否为 1。另外lwip 的 DHCP 有时需要等一段时间不要一启动就急着看 IP。现象三系统在长时间跑之后发生 HardFault。这种情况十有八九是内存泄漏或者栈溢出。lwip 的内存泄漏通常是因为pbuf_free没被正确调用或者应用层把netconn打开后没关闭。排查时可以在lwipopts.h里打开LWIP_STATS和LWIP_STATS_DISPLAY用stats_display()看memp的可用计数。如果发现PBUF_POOL的 used 数不断增长且不回落那就是哪里有 pbuf 没释放。5.2 调试经验与工程习惯总结用 lwip 这几年我最大的体会是协议栈本身代码很成熟出问题基本都在驱动、内存和线程这三个外围因素上。所以我的调试经验基本围绕这三方面展开。第一移植网卡驱动时第一件做的事是先把HAL_ETH_ReadPHYRegister和HAL_ETH_WritePHYRegister调通。用 JTAG 或串口直接读 PHY 的 ID 寄存器比如 LAN8720A 应该是 0x0007。如果读不到检查电源、时钟、地址引脚再谈协议栈。第二在ethernetif_input里临时加一个计数器变量每收到一帧就加一再配合串口打印。这个方法能在不引入调试器的情况下快速判断是“没收到包”还是“收到了但处理失败”。第三多线程下注意信号量的初始化和超时。CubeMX 生成的sys_arch里信号量默认是 1 个计数。中断频繁触发时如果信号量被多次释放内核会丢失部分唤醒但因为有 pbuf 队列数据本身不会丢只是处理延迟稍微变大。对大多数应用来说没问题但如果你做的是高实时性控制就需要改造成带计数上限的信号量或加一个环形队列。第四开启LWIP_DEBUG时要注意调试信息输出会极大占用 CPU 和串口带宽。千万别在产品上开着全量调试跑。我一般只在问题复现阶段临时打开TCP_DEBUG和ETHARP_DEBUG定位完立刻关掉。第五如果你要用固定 IP别在lwipopts.h里直接硬编码到netif结构体里而是通过 CubeMX 的Static IP Address配置项生成这样重生成代码后配置还在不会被覆盖。最后分享一个我一直在用的小技巧在串口调试终端里同步打印 lwip 的stats每次 HTTP 请求完成后打印一次MEM_STATS和MEMP_STATS。一段时间后拉出日志如果发现某个内存池的 max used 明显低于配置值那就说明配置有余量可以适当调低如果无限接近配置值趁早加大。用数据说话比拍脑袋定 buffer 大小靠谱得多。本文还有配套的精品资源点击获取
返回列表