lwIP在RTX上的移植:AT91SAM7嵌入式网络实战
2026/9/15 18:49:10 网站建设 项目流程

简介:面向Atmel AT91SAM7X256微控制器的LWIP轻量级TCP/IP协议栈与RTX实时操作系统集成移植包,专门解决嵌入式环境中网络通信与实时任务调度的协同问题,适用于物联网终端、工业自动化控制器等对实时性要求较高的应用场景。资源以RAR压缩包提供,共49个文件,其中47个为C语言源文件,覆盖LWIP核心协议、网络接口驱动、操作系统适配层以及RTX钩子函数,另含readme说明文档等相关文件,整体仅277KB,便于直接加入Keil MDK工程开展二次开发。目前已有209人查看学习,适合具备一定ARM基础、希望深入网络协议栈移植的中级嵌入式工程师。移植实现可参考的关键内容包括:底层以太网控制器的EMAC驱动与中断服务程序编写、RTX内存池与LWIP动态内存管理的对接方式、多任务并发访问LWIP数据结构时的线程安全措施,以及针对UDP、TCP、DHCP、DNS等协议的裁剪配置思路。资源目录按照core、netif、api、port等模块划分,结构清晰,可帮助读者快速定位协议栈各层代码,理解LWIP在RTX调度下的完整运行机制。

1. 一个看似混乱的文件名,实际是 lwIP、RTX 与 at91sam7 的移植现场

可复制。你看到“lwip.rar_LWIP RTX_RTX LWIP_at91sam7 rtx_lwip + rtx_rtx+lwip”这种搜索串时,多半是有人在转移一个历史工程:lwIP 协议栈、Keil RTX 实时内核、Atmel AT91SAM7 微控制器,三样东西被塞进同一个压缩包里。这个组合在 ARM7 时代很常见,在 STM32 和 CubeMX 主导的今天,仍有大量老设备在维护,类似的移植模型也被继续搬进新产品。对于嵌入式开发者,压缩包里最有价值的不是 lwIP 源码本身,而是“lwIP 如何被放到 RTX 的线程模型里跑”的那一层接口代码。它决定了网络线程什么时候被调度、中断如何把数据包送给协议栈、内存池在抢占式调度下会不会被撕开。这篇博客就按这个顺序,把 lwIP 在 RTX 上的移植思路、at91sam7 上常见的落地方案和几个真正容易埋雷的参数讲透。

2. lwIP 与 RTX 的移植模型:网络线程为何不能跑成裸循环

2.1 lwIP 的 NO_SYS 开关决定了两种移植路线

lwIP 最核心的编译开关是NO_SYS。置 1 时,协议栈不使用操作系统,整个协议栈被当成一个不可重入的大函数,只能在一个超级循环里被调用;置 0 时,lwIP 内部会创建一个tcpip_thread线程,所有网络协议处理、应用回调都集中在这个线程里执行,外部只需要通过邮箱把数据包投递给它。用户看到的前缀lwip + rtx表达的就是后一种路线,而“lwip 裸机移植模型”则是前一种路线。两者没有绝对优劣,但一旦工程里已经有了 RTX,再使用裸机模型就浪费了 RTOS 的阻塞能力。裸机模型里,为了不让arp_timertcp_fasttmr这些定时任务卡死主循环,你得手工拆时间片;RTX 路线的做法是靠sys_timeout和操作系统滴答来维护。

一个容易混淆的点是:NO_SYS=0只是说你用了操作系统,协议栈和单个应用线程都不需要加锁;但如果应用开了多线程,比如同时有 RTX 任务在调用netconn_write,那么SYS_LIGHTWEIGHT_PROT和相关互斥锁就必须打开。at91sam7 这类 ARM7TDMI 核没有别的手段保证原子性,所以移植的第一步不是调以太网驱动,而是先把lwipopts.hNO_SYSSYS_LIGHTWEIGHT_PROTLWIP_NETCONN三个开关定下来。

2.2 sys_arch 层:RTX 的线程、邮箱和互斥锁如何映射到 lwIP

lwIP 对操作系统只有一组抽象接口,集中在sys_arch.hsys_arch.c,主要包含线程创建、邮箱、互斥锁、临界区保护、睡眠和信号量。RTX 在这些概念上基本是一一对应的,只是名称不同,习惯上把对应的硬件映射放在同一张表里:

lwIP 抽象RTX(CMSIS-RTOS v1 风格)用途
sys_thread_newosThreadCreate创建tcpip_threadethernetif_input
sys_mbox_newosMailCreate建立数据包投递邮箱
sys_mbox_fetchosMailGet阻塞获取取出一帧待处理的 pbuf
sys_mbox_tryfetchosMailGet非阻塞获取轮询场景下避免卡死
sys_mutex_new/lock/unlockosMutexCreate/Wait/Release保护套接字层和 netconn 层
sys_arch_protect/unprotect暂时全局中断或 RTX 互斥锁保护 lwIP 内部的短临界区
sys_msleeposDelay定时轮询链路状态、DHCP 重试

这里面最容易被照搬裸机代码写错的,是sys_arch_protect。很多移植示例直接把__disable_irq()放进去,理由是 lwIP 的临界区都很短。这个做法在 RTX 里会产生一个隐患:ATS 在关中断期间无法响应 SysTick,如果临界区里发生页面错误或死循环,整个系统直接挂死;同时,RTX 内核自己也会在调度时短暂关中断,嵌套调用会破坏内核的状态。我一般会定义一个专用互斥锁,并且在临界区里明确禁止阻塞操作。RTX 5 支持 Mutex 的优先级继承,对这类场景比裸关中断更稳。

2.3 一个 RTX 上的最小 sys_arch 骨架

把抽象层写成可编译的代码并不长,关键是每个函数都要绑到 RTX 的 API 上。以 CMSIS-RTOS v1 为例,常见的最小实现骨架是这样的:

#include "cmsis_os.h" #include "lwip/sys.h" static osMutexId lwip_prot_mutex; static osMailQId eth_rx_mailbox; void sys_arch_init(void) { osMutexDef(lwip_prot_mutex); lwip_prot_mutex = osMutexCreate(osMutex(lwip_prot_mutex)); } sys_prot_t sys_arch_protect(void) { osMutexWait(lwip_prot_mutex, osWaitForever); return 0; } void sys_arch_unprotect(sys_prot_t pval) { osMutexRelease(lwip_prot_mutex); } err_t sys_mbox_new(sys_mbox_t *mbox, int size) { osMailQDef(mbox, size, void *); *mbox = osMailCreate(osMailQ(mbox), NULL); return ERR_OK; } void sys_mbox_free(sys_mbox_t *mbox) { osMailFree(mbox, NULL); }

这段代码的意图是:每个 lwIP 邮箱直接映射为 RTX 的 Mail Queue。osMailQDef里的size就是队列深度,它决定了中断能积压多少帧;如果这个值小于以太网突发流量,osMailPut会返回错误,数据包只能丢弃。另一个关键点是sys_arch_protect的返回值,在 lwIP 的接口定义里它可以携带原中断状态,但使用互斥锁时不需要保存,统一返回 0 即可。这里没有做超时处理,实际工程中更建议在sys_mbox_fetch里把osWaitForever改成osMailGet的超时版本,以便网络异常时系统还能被外部看门狗拉回来。

2.4 RTX 线程优先级分配:以太网中断与 tcpip_thread 的配合

线程优先级是整个移植里最像玄学、其实可以用调度规则推出来的部分。RTX 是固定优先级抢占式调度,所以优先级数字越大越先跑。常见的分配方式是:MAC 中断优先级最高,ethernetif_input线程次高,tcpip_thread再低一档,应用层和调试打印任务最低。这个顺序保证了“中断收包 -> 交给输入线程 -> 投递到 tcpip_thread”的数据流不会被低优先级任务拖死。但要小心一个陷阱:如果ethernetif_input线程不做阻塞等待,而是一直轮询 DMA 描述符,它会占满 CPU,导致tcpip_thread永远得不到运行时间,表现出来就是 Ping 通但 TCP 握手超时。正确的做法是让ethernetif_input在邮箱为空时进入osMailGet阻塞态,由中断来唤醒它。

3. 在 at91sam7 上把 rtx_lwip 工程跑起来的移植步骤

3.1 解压后典型工程目录和需要重点改动的文件

拿工具解压一个 rar 只是一个前提,真正要读懂的是工程结构。常见的 lwIP+RTX 工程会包含四个区域:lwIP 源码目录、RTX 配置目录、以太网驱动目录、应用入口目录。一个典型解压后的目录结构大概是:

unrar x lwip.rar -d project/ find project/ -maxdepth 2 -type d | sort | head -30

执行之后会看到类似lwip/src/corelwip/src/netifRTX/ConfigBoard/at91sam7x这样的目录。lwipopts.h通常在lwip/ports或工程 include 路径里;RTX_Config.c则决定 OS 任务数、系统节拍和堆栈池。真正需要动的不是协议栈核心,而是五个文件:sys_arch.csys_arch.hethernetif.clwipopts.hRTX_Config.c。其余源码文件只需要保证编译路径正确,不建议大改,否则后续升级 lwIP 时会很痛苦。

3.2 以太网驱动与 RTX 邮箱对接:中断怎么通知协议栈

at91sam7 的 EMAC 控制器在收到帧后会置起接收状态位并触发中断。裸机写法通常是中断里直接解析 pbuf 再调用netif->input,但在 RTX 模式下,协议栈状态机属于另一个线程,不能在中断上下文直接触碰。一个稳妥的对接是:中断服务程序里只做两件事,把 DMA 收到的 pbuf 从接收描述符链表摘下,再投递到 RTX 邮箱;ethernetif_input线程通过邮箱拿到 pbuf 后,调用tcpip_input把它交给协议栈。代码形状大致如下:

void EMAC_IRQHandler(void) { struct pbuf *p; while (EMAC_RSR & EMAC_RSR_REC) { p = emac_rx_dequeue(); if (p != NULL) { osMailPut(eth_rx_mailbox, p); } EMAC_RSR = EMAC_RSR_REC; } } void ethernetif_input_thread(void const *arg) { osEvent evt; for (;;) { evt = osMailGet(eth_rx_mailbox, osWaitForever); if (evt.status == osEventMail) { ethernetif_input((struct pbuf *)evt.value.p); } } }

这里把接收描述符的回收、DMA 寄存器清中断都留在中断里,避免多次进中断;lwIP 的ethernetif_input则放在输入线程中执行。要用osMailPut而不是直接写 lwIP 的sys_mbox_trypost,因为eth_rx_mailbox就是sys_arch层已经和 lwIP 共享的那个队列。若两个邮箱并存,会引入二次拷贝和优先级不一致的问题,排错时很难判断丢包发生在哪一段。

3.3 lwipopts.h 与 RTX_Config.c 的十二个必调参数

从裸机或 STM32 工程搬 lwIP 过来时,只改NO_SYS是不够的。内存池、协议栈堆、线程栈必须一起调。下面的表格是 at91sam7 这类 64KB~128KB RAM 设备上比较常见的起步值,具体数值要看应用,但方向可以参考:

参数典型值作用与坑点
NO_SYS0开启 RTOS 模式,必须让 tcpip_thread 存在
SYS_LIGHTWEIGHT_PROT1打开核心临界区保护,避免并发访问 pbuf 链表
MEM_SIZE5~10KB堆内存大小,太小会 malloc 失败
PBUF_POOL_SIZE16~32接收队列能缓冲的 pbuf 数,太小会丢包
PBUF_POOL_BUFSIZE1280覆盖一帧最大以太网负载
MEMP_NUM_TCP_SEG16TCP 发送分段数量,影响并发连接数
TCP_WND2~16KB接收窗口,超过内存会申请失败
TCP_SND_BUF2~4KB发送缓冲区,超过 MEMP_NUM_TCP_SEG 会阻塞
LWIP_NETCONN1使用 netconn API 时打开
LWIP_SOCKET0at91sam7 上建议先用 netconn,省去 fd 数组
CHECKSUM_GEN_ICMP1关闭会导致 Ping 全部无响应
LWIP_DHCP1调试时可以先置 0 用静态 IP

RTX 侧,RTX_Config.c里的OS_TASKCNT至少要比实际线程数多 4,因为 RTX 内部还有一些系统任务和空闲任务;OS_SYSSTKSIZE默认值通常够用,但tcpip_thread的栈不要低于 2048 字节,lwIP 在解析大报文时栈用量会明显上涨。邮箱深度通过osMailQDef的 size 设置,接收链路建议至少 8 个槽位,否则 DHCP 广播风暴期间会频繁丢包。

3.4 用周期任务做 DHCP 和链路状态检测

RTX 下很多网络问题不是协议栈算错,而是链路状态变化没有通知 lwIP。裸机超级循环里可以做阻塞轮询,比如每 500ms 读一次 PHY 寄存器,RTX 下也建议用一个低速任务干这件事。它每 200ms 检查一次 PHY 状态,发现链路断开时调用netif_set_link_down,恢复时调用netif_set_link_up。如果缺少这两个调用,插拔网线之后 ARP 表会停留在旧状态,TCP 连接会一直重传直到超时。这个任务优先级最低即可,甚至可以和调试打印任务合并,不要让它参与数据面。

4. 移植完成后先做这三件事:Ping、吞吐和内存水位验证

4.1 最小联通:静态 IP 加 Ping,配合 RTX 调试串口

拿到一块至少能点灯的板子之后,先把 DHCP 关掉,用静态 IP 验证数据链路。AT91SAM7 的 EMAC 驱动里通常有一个low_level_init,里面的 MAC 地址是厂家写入的,也可以手动填充一个 00:04:25:xx:xx:xx。启动后通过调试串口打印 lwIP 版本和网络接口状态:

#include "lwip/init.h" #include "netif/ethernetif.h" printf("lwIP %s started\r\n", LWIP_VERSION_STRING); printf("IP: %s\r\n", ip4addr_ntoa(netif_ip4_addr(netif_default)));

然后从 PC 端ping 192.168.1.10。如果第一次 Ping 不通但第二次通,多半是 ARP 表问题,检查ethernetif_input线程的优先级是不是低于 MAC 中断;如果第一次通后面全丢,先看 DMA 描述符数量和PBUF_POOL_SIZE是否匹配。Ping 通了只能说明 ICMP 路径没问题,接下来必须测 TCP,因为 TCP 才真正使用邮箱、互斥锁和定时器系统。

4.2 吞吐与丢包对比:裸机移植模型和 RTX 服务端进程的差异

把同样的代码在裸机移植模型和 RTX 移植模型下跑,数据面行为会明显不同。裸机模型的优势是零上下文切换,收包到发 ICMP 响应的路径最短;RTX 模型引入了线程调度,但换来的是应用层可以写多个阻塞任务。运行一个简单的 TCP 回环服务端,用 PC 端 iperf 打流,观察几个数据点:

测试点裸机移植模型RTX 移植模型
小包 Ping 平均延迟略低略高 1~2ms
TCP 吞吐峰值受主循环分片影响受邮箱深度和线程切换影响
多连接稳定性较差,需手工拆协议状态每个连接可对应一个任务
CPU 空闲率难以统计RTX 事件查看器直接给出

如果 RTX 下 TCP 吞吐只有裸机的一半,不要马上怀疑 lwIP 本身,先看tcpip_thread栈是否溢出、邮箱是否反复满。用 RTX 的 Event Recorder 抓一段时间,如果osMailPut返回osErrorResource,说明接收路径积压,需要增加PBUF_POOL_SIZE或减少ethernetif_input优先级以免它独占总线。

4.3 检查和调整 lwIP 内存池在 RTX 多线程下的安全边界

lwIP 在NO_SYS=0下并不是完全线程安全的,它把保护责任推给了sys_arch。所以除了看功能通不通,还要统计内存使用水位。开启LWIP_STATSLWIP_STATS_DISPLAY之后,可以在一个 RTX 低优先级任务里周期调度:

#if LWIP_STATS #include "lwip/stats.h" void stats_show_task(void const *arg) { for (;;) { osDelay(5000); stats_display(); } } #endif

输出里重点看lwip_stats.mem.usedlwip_stats.mem.availlwip_stats.pbuferr计数。如果available持续下降而总任务数不变,说明某处 pbuf 申请后没人释放,通常发生在 TCP 发送失败分支里,错误处理路径没有pbuf_free。如果err在 DHCP 完成后持续增长,多半是邮箱缓冲不足而不是代码泄漏。这两类问题的修法完全不同,前者改代码,后者只改参数,先看统计再动手,避免瞎调MEM_SIZE

5. 排错与进阶:tcpip_thread 被 RTX 饿死时的特征和验证方法

5.1 饿死的典型特征

RTX 是固定优先级调度,不会自动做时间片轮转,除非多个任务同一优先级且打开了时间片。所以当网络接收中断过多、ethernetif_input线程又始终处于就绪态时,tcpip_thread可能长时间得不到 CPU,现象很具迷惑性:Ping 能通,因为 ICMP 请求在ethernetif_input里能处理;TCP 建立连接很慢或超时,因为三次握手要等tcpip_thread处理 SYN;App 任务还能打印日志,更加掩盖了网络线程被饿死的事实。要确认这一点,在 RTX 配置器里给所有非空闲任务加上不同的优先级,然后观察运行计数器。tcpip_thread 的切换次数长时间不增加,而 ethernetif_input 的切换次数暴涨,基本可以定性为饥饿。

5.2 三个验证和干预技巧

第一个技巧是给osMailGet加超时。把ethernetif_input线程里的osWaitForever换成50ms 的超时,超时后主动释放一次 CPU:osDelay(1)。这个改动不会降低小包吞吐,却能让tcpip_thread在每个超时点获得调度机会,短时间内就能区分“饿死”和“真死锁”。如果改成超时后 TCP 恢复,问题优先级确定;如果仍然超时,就要查tcpip_thread是否阻塞在sys_mbox_fetch上。

第二个技巧是用 RTX Event Recorder 看线程状态。Keil 的 RTX 调试视图会显示每个线程处于 Running、Ready、Wait_Delay、Wait_Mailbox 的耗时占比。手动制造一个大的数据包突发,比如 PC 端发一次ping -l 1000 -n 100 192.168.1.10,然后在调试器里暂停,观察 tcpip_thread 停在哪个函数。如果频繁停在sys_timeouttcp_input,说明它是活着的,只是时间不够;如果停在rtx_mbx_wait上且邮箱非空,才是 lwIP 邮箱实现与 RTX 邮箱不匹配的问题。

第三个技巧是做优先级互换实验。把ethernetif_inputtcpip_thread优先级对调,再次跑同一组测试。如果丢包率没有变化,说明调度不是瓶颈,此时才需要检查 DMA 描述符和 PHY 中断。这个实验成本极低,很多工程师却跳过它直接调MEMP_NUM_TCP_SEG,结果改了半天内存,问题依旧。记住:在 at91sam7 这种单核设备上,网络性能的前三位影响因素是中断处理时长、邮箱积压容量和线程优先级顺序,而不是 lwIP 的窗口参数;窗口参数只在内存足够的条件下才有意义。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询