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 |
2. 裁剪的依据只有两条
(1)内存:这颗 MCU 只有 64 KB SRAM,而 128 KB 固件载荷还要占外部 SRAM 之外的周转空间,协议栈不能多吃。
(2)这个工程到底要用哪些协议:升级通道用 TCP,TFTP 用 UDP,调试用 ICMP,局域网寻址用 ARP。凡是不在这个清单里的,一律关掉。
二、 内存类取舍
1. 堆与内存池
lwIP 有两条内存来源,本项目两条都定得很小:
| 类别 | 宏 | 取值 | 用在哪 |
|---|---|---|---|
| 堆 | MEM_SIZE | 8 KB | mem.c 的 mem_malloc,PBUF_RAM 类型的 pbuf 等动态申请 |
| 池化 | PBUF_POOL_SIZE | 8 | 接收 pbuf 池深度(每个约 1500 字节量级) |
| 池化 | MEMP_NUM_PBUF | 8 | 静态 pbuf 描述符数 |
| 池化 | MEMP_NUM_TCP_PCB | 5 | 最多几个 TCP 连接控制块 |
| 池化 | MEMP_NUM_TCP_SEG | 32 | 待发送的 TCP 分段数 |
| 池化 | MEMP_NUM_ARP_QUEUE | 8 | 等 ARP 解析期间暂存的待发包数 |
池化部分是静态分配的定长块,上电就占住内存,不会产生碎片;堆则是按需分配。lwIP 里有"什么场景必须用堆、什么场景必须用池"的规矩(比如接收路径只能用池),本项目把两边都定得很小。
PBUF_POOL_SIZE = 8 这个值直接决定了"能不能连续猛发":池子用光时 pbuf_alloc 返回 NULL,驱动的表现就是打印 pbuf alloc failed 并丢包(见 LV005-DM9000驱动实现 第五节)。所以网络吞吐上不去时,这个宏是第一个要看的。
2. 没有定义的宏走默认值
有几个宏本项目没有定义,走的是 lwIP 默认值,容易被忽略:
| 宏 | 默认值 | 含义 |
|---|---|---|
TCP_MSS | 536 | 单段最大 536 字节 |
TCP_SND_BUF | 2 × TCP_MSS | 发送缓冲 1072 字节 |
TCP_WND | 4 × 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_TCP | IAP 网络升级通道的载体 |
LWIP_UDP | TFTP 建立在 UDP 之上 |
LWIP_ICMP | ping 是调试第一手段,也是 排错决策树 的入口 |
LWIP_ARP | 局域网里发任何帧都要先解析目的 MAC |
LWIP_ETHERNET | 使用以太网帧格式 |
SYS_LIGHTWEIGHT_PROT | 用 sys_arch_protect() 那对函数保护共享数据 |
2. 关掉的部分与后果
| 宏 | 取值 | 关掉的后果 |
|---|---|---|
LWIP_NETCONN / LWIP_SOCKET | 0 | 没有 socket 风格 API,只能用 RAW API(回调式) |
LWIP_DHCP | 0 | 必须静态配地址;dhcp.c 被裁成空实现 |
LWIP_DNS | 0 | 只能用 IP 访问,不能用域名 |
LWIP_AUTOIP | 0 | 不会自动生成 169.254.x.x 地址 |
LWIP_IGMP | 0 | 不支持组播管理 |
IP_REASSEMBLY | 0 | 收到分片包直接丢 |
IP_FRAG | 0 | 不会主动分片 |
LWIP_STATS | 0 | 没有协议栈内部统计计数(只有驱动自己的计数) |
两条最需要留意的:
(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:
| 参数 | 默认值 | 宏 |
|---|---|---|
| 本机 IP | 192.168.16.30 | LWIP_LOCAL_IP0 ~ LWIP_LOCAL_IP3 |
| 子网掩码 | 255.255.255.0 | LWIP_NETMASK0 ~ LWIP_NETMASK3 |
| 网关 | 192.168.16.1 | LWIP_GATEWAY0 ~ LWIP_GATEWAY3 |
另外有一个总开关 LWIP_APP_EN,置 0 可以整体停用网络功能(连 DM9000_Init() 都不会调用),在"只想验证 IAP 串口通道"的场合很省事。
MAC 地址不在这里:它由 DM9000 驱动在初始化时用芯片 UID 生成,理由见 LV030-ARP与MAC地址 第一节。
2. 初始化顺序
lwip_comm_init() 的顺序不能乱:
// 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_OK | 0 | 初始化成功 |
LWIP_COMM_DM9000_ERR | 1 | 网卡 ID 读失败 |
LWIP_COMM_NETIF_ERR | 2 | netif_add() 返回 NULL(内存池不足) |
五、 轮询入口 lwip_comm_poll()
1. 每一拍做什么
NO_SYS 模式下没有线程替协议栈干活,必须由主循环推。整个工程只有一个对外轮询入口:
// 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. 参考资料
- 裁剪配置:
lwipopts.h - 初始化与轮询:
lwip_comm.c、lwip_comm.h - 默认值出处:
opt.h - 手册:
STM32F1 LWIP开发手册(DM9000版)_V1.1.pdf第 1.4 节
本文档由 markdowncli 技能辅助生成