Skip to content

LV015-lwIP配置与初始化 ​

前置阅读:上一篇 LV010-lwIP协议栈移植 讲的是"port 目录怎么补出来",本篇讲"补完之后怎么把它裁小、配好、跑起来"。

一、 lwipopts.h 是裁剪的总开关 ​

1. 全部显式赋值的宏 ​

协议栈能不能塞进 STM32F103,全看这张表。本项目 port/lwipopts.h 里显式赋值的宏分三组:

分组宏与取值
运行模式NO_SYS = 1、SYS_LIGHTWEIGHT_PROT = 1、LWIP_NETCONN = 0、LWIP_SOCKET = 0、LWIP_TIMERS = 1
内存MEM_ALIGNMENT = 4、MEM_SIZE = 8 * 1024、MEMP_NUM_PBUF = 8、PBUF_POOL_SIZE = 8、MEMP_NUM_TCP_PCB = 5、MEMP_NUM_TCP_SEG = 32、MEMP_NUM_ARP_QUEUE = 8
协议开关LWIP_TCP = 1、LWIP_UDP = 1、LWIP_ICMP = 1、LWIP_ARP = 1、LWIP_ETHERNET = 1、LWIP_DHCP = 0、LWIP_DNS = 0、LWIP_AUTOIP = 0、LWIP_IGMP = 0、IP_REASSEMBLY = 0、IP_FRAG = 0
校验与统计六个 CHECKSUM_* 全为 1、LWIP_STATS = 0、LWIP_NETIF_STATUS_CALLBACK = 1、LWIP_NETIF_LINK_CALLBACK = 0
lwipopts.h 的三类配置及其取舍

2. 裁剪的依据只有两条 ​

(1)内存:这颗 MCU 只有 64 KB SRAM,而 128 KB 固件载荷还要占外部 SRAM 之外的周转空间,协议栈不能多吃。

(2)这个工程到底要用哪些协议:升级通道用 TCP,TFTP 用 UDP,调试用 ICMP,局域网寻址用 ARP。凡是不在这个清单里的,一律关掉。

二、 内存类取舍 ​

1. 堆与内存池 ​

lwIP 有两条内存来源,本项目两条都定得很小:

类别宏取值用在哪
堆MEM_SIZE8 KBmem.c 的 mem_malloc,PBUF_RAM 类型的 pbuf 等动态申请
池化PBUF_POOL_SIZE8接收 pbuf 池深度(每个约 1500 字节量级)
池化MEMP_NUM_PBUF8静态 pbuf 描述符数
池化MEMP_NUM_TCP_PCB5最多几个 TCP 连接控制块
池化MEMP_NUM_TCP_SEG32待发送的 TCP 分段数
池化MEMP_NUM_ARP_QUEUE8等 ARP 解析期间暂存的待发包数

池化部分是静态分配的定长块,上电就占住内存,不会产生碎片;堆则是按需分配。lwIP 里有"什么场景必须用堆、什么场景必须用池"的规矩(比如接收路径只能用池),本项目把两边都定得很小。

PBUF_POOL_SIZE = 8 这个值直接决定了"能不能连续猛发":池子用光时 pbuf_alloc 返回 NULL,驱动的表现就是打印 pbuf alloc failed 并丢包(见 LV005-DM9000驱动实现 第五节)。所以网络吞吐上不去时,这个宏是第一个要看的。

2. 没有定义的宏走默认值 ​

有几个宏本项目没有定义,走的是 lwIP 默认值,容易被忽略:

宏默认值含义
TCP_MSS536单段最大 536 字节
TCP_SND_BUF2 × TCP_MSS发送缓冲 1072 字节
TCP_WND4 × TCP_MSS接收窗口 2144 字节

注意这里的默认 TCP_MSS = 536 是"保守值"(为了兼容早期以太网),而以太网实际能承载到 1460。这条默认值的影响要分清方向:

  • 板子发出的数据按 536 切分,比以太网允许的小得多;
  • PC 发给板子的数据,由 PC 按板子在握手里宣布的 MSS(536)来切分——所以升级 128 KB 固件时,PC 若按 1460 切是 90 个段,实际却要切成约 245 个段。

代价是包数变多、每包开销占比变高;好处是 TCP_SND_BUF、TCP_WND 自动变小,对 8 KB 堆更友好。这是一个典型的"用吞吐换内存"的选择。

代价是包数变多、每包开销占比变高;好处是 TCP_SND_BUF、TCP_WND 自动变小,对 8 KB 堆更友好。这是一个典型的"用吞吐换内存"的选择。

三、 协议开关取舍 ​

1. 打开的部分 ​

宏为什么开
LWIP_TCPIAP 网络升级通道的载体
LWIP_UDPTFTP 建立在 UDP 之上
LWIP_ICMPping 是调试第一手段,也是 排错决策树 的入口
LWIP_ARP局域网里发任何帧都要先解析目的 MAC
LWIP_ETHERNET使用以太网帧格式
SYS_LIGHTWEIGHT_PROT用 sys_arch_protect() 那对函数保护共享数据

2. 关掉的部分与后果 ​

宏取值关掉的后果
LWIP_NETCONN / LWIP_SOCKET0没有 socket 风格 API,只能用 RAW API(回调式)
LWIP_DHCP0必须静态配地址;dhcp.c 被裁成空实现
LWIP_DNS0只能用 IP 访问,不能用域名
LWIP_AUTOIP0不会自动生成 169.254.x.x 地址
LWIP_IGMP0不支持组播管理
IP_REASSEMBLY0收到分片包直接丢
IP_FRAG0不会主动分片
LWIP_STATS0没有协议栈内部统计计数(只有驱动自己的计数)

两条最需要留意的:

(1)LWIP_NETCONN / LWIP_SOCKET 关掉意味着必须用 RAW API。所以本工程的 TCP 代码是回调风格(tcp_accept / tcp_recv),而不是"先连上再读写"。这也是 LV020-网络通信实验 里强调"回调里只做 O(1) 搬运"的根源。

(2)IP_REASSEMBLY / IP_FRAG 关掉意味着"大包不通、小包通"要第一个怀疑它。以太网 MTU 之外的场景(比如某些工具默认发 4 KB 的 ping)会直接失败,而这不代表链路有问题。

四、 网络参数与 lwip_comm_init() ​

1. 静态参数写在哪 ​

网络参数以宏的形式写在 lwip_comm.h 里,改 IP 不用进 CubeMX:

参数默认值宏
本机 IP192.168.16.30LWIP_LOCAL_IP0 ~ LWIP_LOCAL_IP3
子网掩码255.255.255.0LWIP_NETMASK0 ~ LWIP_NETMASK3
网关192.168.16.1LWIP_GATEWAY0 ~ LWIP_GATEWAY3

另外有一个总开关 LWIP_APP_EN,置 0 可以整体停用网络功能(连 DM9000_Init() 都不会调用),在"只想验证 IAP 串口通道"的场合很省事。

MAC 地址不在这里:它由 DM9000 驱动在初始化时用芯片 UID 生成,理由见 LV030-ARP与MAC地址 第一节。

2. 初始化顺序 ​

lwip_comm_init() 的顺序不能乱:

c
// Components/lwip-2.1.3/port/lwip_comm.c
if (DM9000_Init() != 0U) { return LWIP_COMM_DM9000_ERR; }   /* 1. 先认卡 */
lwip_init();                                                 /* 2. 再起协议栈 */
IP4_ADDR(&ipaddr, ...); IP4_ADDR(&netmask, ...); IP4_ADDR(&gw, ...);
printf("lwIP: MAC ..., IP ...\r\n");                         /* 3. 打印参数 */
netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ethernetif_init, ethernet_input);
netif_set_default(&gnetif);                                   /* 4. 设为默认接口 */
netif_set_up(&gnetif);                                        /* 5. 启用接口 */
return LWIP_COMM_OK;

三个要点:

(1)先 DM9000_Init():读不到 ID 就没必要起协议栈,直接返回错误码,串口上能看出是"卡没认到"还是"协议栈没起来"。

(2)netif_set_default() 与 netif_set_up() 缺一不可。前者决定"没有明确出口的包从哪个接口走",后者才真正把接口激活。

(3)调用前必须已完成 MX_FSMC_Init() / MX_GPIO_Init() / delay_init()——理由见 LV001-以太网与DM9000硬件接口 第五节。

对应的错误码很小巧,只有三个:

错误码值含义
LWIP_COMM_OK0初始化成功
LWIP_COMM_DM9000_ERR1网卡 ID 读失败
LWIP_COMM_NETIF_ERR2netif_add() 返回 NULL(内存池不足)

五、 轮询入口 lwip_comm_poll() ​

1. 每一拍做什么 ​

NO_SYS 模式下没有线程替协议栈干活,必须由主循环推。整个工程只有一个对外轮询入口:

c
// Components/lwip-2.1.3/port/lwip_comm.c
void lwip_comm_poll(void)
{
    if (DM9000_TakeRxFlag() != 0U)   /* 中断模式: 有标志才取包 */
    {
        ethernetif_input(&gnetif);
    }
    sys_check_timeouts();            /* 驱动 lwIP 内部定时器 */
    DM9000_DbgPoll();                /* 驱动诊断计数(默认关闭) */
}

中断模式下由 EXTI 置标志、轮询时取包;如果 DM9000_RX_INT_MODE = 0,则无条件调用 ethernetif_input()。

2. sys_check_timeouts() 为什么不能漏 ​

sys_check_timeouts() 是容易被忽略、漏了又极难查的一行:TCP 重传、ARP 表老化、TFTP 超时、连接回收全部挂在 lwIP 的内部定时器上。

不调用它的话,协议栈的表现是"能收能发,但永远不重传、永远不会超时":

症状少了什么
TCP 丢包后一直卡住重传定时器没被推进
ARP 表项永不老化,换了 MAC 也不更新老化定时器没被推进
TFTP 传输失败后不重发,一直等到死超时回调没被触发

所以这一行必须每 1 ms 稳定调用一次,这也是 LV010-lwIP协议栈移植 里那条硬约束"不要让长耗时操作挡住主循环"的直接原因。

六、 小结与参考资料 ​

1. 本篇小结 ​

  • lwipopts.h 是裁剪总开关,本项目按"内存够用 + 协议够用"两条依据把配置压到最小。
  • 内存分两路:8 KB 堆(MEM_SIZE)与定长内存池(PBUF_POOL_SIZE = 8 等);池子用光就直接丢包。
  • TCP_MSS 未定义、走默认 536,导致 TCP 分段比以太网允许的更碎——这是"用吞吐换内存"的取舍。
  • 关掉的是 NETCONN/SOCKET、DHCP、DNS、分片重组与统计,代价分别是"只能用回调式 API""必须静态配地址""大包直通失败""没有协议栈内部计数"。
  • 网络参数写在 lwip_comm.h 的宏里;MAC 由驱动用芯片 UID 生成。
  • lwip_comm_init() 的顺序是"先认卡、再起栈、打印参数、登记并启用接口";错误码只有三个。
  • lwip_comm_poll() 是全工程唯一的协议栈轮询入口,sys_check_timeouts() 绝对不能漏。

2. 参考资料 ​


本文档由 markdowncli 技能辅助生成