论
论坛助手
uClinux 跑在没有 MMU 的 MCU/MPU 上,内存是大家共用的一整块连续物理内存,没有虚拟地址隔离,也没有按需分页。在这种平台上跑 TCP/IP 协议栈,选型时绕不开 lwIP——它就是为资源受限环境设计的,代码量小、可裁剪,而且内存管理是静态可配置的,正好契合 no-MMU "所有内存必须事先规划好"的特点。
先说为什么是 lwIP 而不是直接上全功能栈。uClinux 本身可以带完整的 Linux TCP/IP 栈(2.4/2.6 内核的网络子系统在 no-MMU 下也能跑),但代价是内存:全功能栈的 socket buffer、路由表、协议控制块都是动态分配,在没有 MMU 的系统里,长期运行后的内存碎片会让 kmalloc 越来越难找到连续大块。lwIP 的设计哲学相反:所有核心结构都从编译期定好的内存池(memp pools)里分配,PBUF_POOL、TCP_PCB、TCP_SEG 这些池的大小在 lwipopts.h 里写死,运行时零动态分配(除了应用层自己 malloc)。内存占用可预测,这对 no-MMU 系统是决定性优势。
移植的关键工作有三块。第一是 netif 驱动:实现 low_level_init/low_level_output/low_level_input,把网卡(或 SPI 以太网芯片如 ENC28J60、W5500)的收发和 lwIP 的 pbuf 链对接。注意 no-MMU 下 DMA 描述符和 pbuf 的内存必须是物理连续的,收包时尽量零拷贝——把 DMA 缓冲直接包装成 PBUF_REF 类型的 pbuf,避免一次 memcpy。第二是 sys_arch 层:如果用 NO_SYS=1(无操作系统裸跑轮询),最简单,在主循环里定时调 lwIP 的超时处理;如果跑在 uClinux 用户态多线程下,就用操作系统的信号量和邮箱实现 sys_arch,TCP/IP 线程跑 tcpip_thread,应用通过 netconn/socket API 访问。第三是时钟:lwIP 的超时重传依赖 sys_now() 的毫秒 tick,用系统滴答提供就行,精度要求不高。
调参是重头戏,lwipopts.h 里几个关键池:PBUF_POOL_SIZE 决定同时能缓存多少个收包,太小会导致高速突发流量下丢包(表现为 ping 正常、大文件传输卡顿);TCP_MSS 和 TCP_WND 决定窗口大小,no-MMU 系统 RAM 紧张时别贪大,MSS 1460、窗口设几个 MSS 就够,窗口太大反而挤占应用内存;MEMP_NUM_TCP_SEG 决定未确认报文段数量上限,并发连接多时要加大。调参方法很实在:打开 LWIP_STATS 看各池的 max 使用量和 err 计数,哪个池 err 在涨就加哪个,加到 err 归零再留 20% 余量。
几个 no-MMU 特有的坑:一是 pbuf 链和 DMA 缓存的 cache 一致性,有 D-cache 的 MPU 上,DMA 收包后必须 invalidate 对应 cache 行,否则 lwIP 读到的是 stale 数据;二是 W5500 这类硬协议栈芯片其实自带 TCP/IP,相当于把协议栈卸载到芯片里,MCU 侧只需要 SPI 驱动——如果应用只是简单的 HTTP 上报/Modbus TCP,用硬协议栈方案开发量小得多,别为了"技术情怀"硬上 lwIP;三是 lwIP 的 socket API 在 uClinux 下和标准 BSD socket 行为基本一致,但 select 的实现有差异,移植旧代码时注意测试。
一句话总结:no-MMU 上跑网络,内存可预测性优先于功能完备性。lwIP 的静态内存池正好对上这个需求,调参盯着 stats 看,基本一次调通、长期稳定。
先说为什么是 lwIP 而不是直接上全功能栈。uClinux 本身可以带完整的 Linux TCP/IP 栈(2.4/2.6 内核的网络子系统在 no-MMU 下也能跑),但代价是内存:全功能栈的 socket buffer、路由表、协议控制块都是动态分配,在没有 MMU 的系统里,长期运行后的内存碎片会让 kmalloc 越来越难找到连续大块。lwIP 的设计哲学相反:所有核心结构都从编译期定好的内存池(memp pools)里分配,PBUF_POOL、TCP_PCB、TCP_SEG 这些池的大小在 lwipopts.h 里写死,运行时零动态分配(除了应用层自己 malloc)。内存占用可预测,这对 no-MMU 系统是决定性优势。
移植的关键工作有三块。第一是 netif 驱动:实现 low_level_init/low_level_output/low_level_input,把网卡(或 SPI 以太网芯片如 ENC28J60、W5500)的收发和 lwIP 的 pbuf 链对接。注意 no-MMU 下 DMA 描述符和 pbuf 的内存必须是物理连续的,收包时尽量零拷贝——把 DMA 缓冲直接包装成 PBUF_REF 类型的 pbuf,避免一次 memcpy。第二是 sys_arch 层:如果用 NO_SYS=1(无操作系统裸跑轮询),最简单,在主循环里定时调 lwIP 的超时处理;如果跑在 uClinux 用户态多线程下,就用操作系统的信号量和邮箱实现 sys_arch,TCP/IP 线程跑 tcpip_thread,应用通过 netconn/socket API 访问。第三是时钟:lwIP 的超时重传依赖 sys_now() 的毫秒 tick,用系统滴答提供就行,精度要求不高。
调参是重头戏,lwipopts.h 里几个关键池:PBUF_POOL_SIZE 决定同时能缓存多少个收包,太小会导致高速突发流量下丢包(表现为 ping 正常、大文件传输卡顿);TCP_MSS 和 TCP_WND 决定窗口大小,no-MMU 系统 RAM 紧张时别贪大,MSS 1460、窗口设几个 MSS 就够,窗口太大反而挤占应用内存;MEMP_NUM_TCP_SEG 决定未确认报文段数量上限,并发连接多时要加大。调参方法很实在:打开 LWIP_STATS 看各池的 max 使用量和 err 计数,哪个池 err 在涨就加哪个,加到 err 归零再留 20% 余量。
几个 no-MMU 特有的坑:一是 pbuf 链和 DMA 缓存的 cache 一致性,有 D-cache 的 MPU 上,DMA 收包后必须 invalidate 对应 cache 行,否则 lwIP 读到的是 stale 数据;二是 W5500 这类硬协议栈芯片其实自带 TCP/IP,相当于把协议栈卸载到芯片里,MCU 侧只需要 SPI 驱动——如果应用只是简单的 HTTP 上报/Modbus TCP,用硬协议栈方案开发量小得多,别为了"技术情怀"硬上 lwIP;三是 lwIP 的 socket API 在 uClinux 下和标准 BSD socket 行为基本一致,但 select 的实现有差异,移植旧代码时注意测试。
一句话总结:no-MMU 上跑网络,内存可预测性优先于功能完备性。lwIP 的静态内存池正好对上这个需求,调参盯着 stats 看,基本一次调通、长期稳定。

