以太网与 LwIP 协议栈在 MCU 上的移植
以太网与 LwIP 协议栈在 MCU 上的移植
嵌入式设备接入以太网,早已不是工控机和高端网关的专利。如今一颗几十块钱的 Cortex-M4,配上 RMII 接口的 PHY 芯片,就能让设备拥有标准 TCP/IP 通信能力。而谈到 MCU 上的以太网软件栈,LwIP(Lightweight IP)几乎是绕不开的选择。
本文以 STM32F4/F7 + LAN8720 这类主流组合为例,讲清楚 LwIP 移植的完整思路、关键配置和常见坑。
一、为什么是 LwIP
LwIP 是为嵌入式场景设计的轻量级 TCP/IP 协议栈,它的两个特性非常适合 MCU:
- 内存占用可控:所有协议结构体都通过内存池(memp)和内存堆(mem)管理,可用
lwipopts.h精细裁剪。 - 无需操作系统也能跑:支持裸机轮询模式,也支持 RTOS 下的专用任务/线程模式。
相比 uIP,LwIP 功能完整得多(完整的 TCP、UDP、IP、ICMP、DHCP、DNS),但代码量又远小于 BSD 栈,是资源受限设备接入以太网的黄金选择。
二、移植前的硬件准备
一个典型的以太网方案由三部分组成:
| 模块 | 作用 | 示例 |
|---|---|---|
| MCU 外设 | 提供 MAC 控制器 | STM32F407/F767 的 ETH 模块 |
| PHY 芯片 | 物理层收发 | LAN8720、DP83848 |
| 接口总线 | MAC 与 PHY 连接 | RMII 或 MII |
RMII 只用了 9 根信号线(相比 MII 的 16 根),引脚少、布线简单,是 STM32 系列最常用的选择。需要外接 50MHz 时钟源。
三、CubeMX 配置要点
以 STM32F407 + LAN8720 RMII 为例,关键配置如下:
- 使能 ETH 外设,接口选择 RMII;
- 配置 PHY 地址,LAN8720 默认地址为 0(需查硬件原理图,有的板子用 1);
- 使能
LWIP中间件(CubeMX 已集成,无需自己下载); - 网卡缓冲区大小根据应用调整,默认
ETH_RX_BUF_SIZE = 1524够用; - 若用 DHCP,勾选
LWIP_DHCP。
生成代码后,最核心的适配点在 ethernetif.c 的 low_level_init() 和 low_level_output()/low_level_input() 三个函数,这是 LwIP 与 STM32 HAL 库之间的”胶水层”。
/* low_level_init 中初始化 MAC 与 PHY */
static void low_level_init(struct netif *netif)
{
uint8_t macaddr[6] = {0x00, 0x80, 0xE1, 0x00, 0x00, 0x01};
netif->hwaddr_len = ETHARP_HWADDR_LEN;
netif->hwaddr[0] = macaddr[0];
netif->hwaddr[1] = macaddr[1];
netif->hwaddr[2] = macaddr[2];
netif->hwaddr[3] = macaddr[3];
netif->hwaddr[4] = macaddr[4];
netif->hwaddr[5] = macaddr[5];
netif->mtu = 1500;
netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
HAL_ETH_Start(&heth);
HAL_ETH_Start_IT(&heth);
}
MAC 地址一定不能是随机的 FF:FF:FF:FF:FF:FF,也不能多台设备冲突,否则链路层就乱了。
四、裸机模式 vs RTOS 模式
CubeMX 默认生成的工程会提供一个 MX_LWIP_Process() 函数,需要在主循环或某个任务里周期调用:
/* 裸机:主循环里轮询 */
while (1) {
MX_LWIP_Process();
}
/* RTOS(FreeRTOS):放到一个独立任务 */
void LwIP_task(void *arg) {
for (;;) {
MX_LWIP_Process();
osDelay(10);
}
}
这一点是最容易忽略的:不调用 MX_LWIP_Process(),协议栈根本不会处理任何收发的数据包,现象就是 ping 得通但 TCP 连接建立不起来,或者 UDP 完全没反应。
五、lwipopts.h 裁剪:内存与性能的平衡
lwipopts.h 是 LwIP 的”性能旋钮”,几个关键参数直接影响可用性:
#define MEM_SIZE (16 * 1024) /* 内存堆,跑多 socket 需加大 */
#define MEMP_NUM_TCP_SEG 16 /* TCP 段数量 */
#define TCP_SND_BUF (4 * 1024) /* TCP 发送缓冲区 */
#define TCP_WND (4 * 1024) /* TCP 接收窗口 */
#define MEMP_NUM_NETBUF 8 /* 网络缓冲区 */
#define LWIP_SOCKET 1 /* 启用 BSD socket API */
内存给小了,压力测试时容易出现 mem_malloc 失败、TCP 连接被复位。给大了又浪费 MCU 宝贵的 SRAM。经验是:先用默认值跑通,再根据实际吞吐逐步调优,不要一上来就盲目加内存。
六、常见坑与排查思路
- ping 不通:先查 PHY 链路状态(
ETH_ReadPHYRegister读 BSR 寄存器),确认网线、PHY 地址、50MHz 时钟都对。 - 能 ping 但 TCP 建链慢/失败:几乎都是忘了周期调用
MX_LWIP_Process(),或任务优先级过低被饿死。 - 大量数据收发掉包:加大
ETH_RX_BUF_SIZE、MEMP_NUM_NETBUF、MEMP_NUM_TCP_SEG,并确认以太网中断优先级合理。 - IP 重复导致网络冲突:DHCP 未成功时回退的静态 IP 可能和局域网内其他设备冲突。
总结
把 LwIP 跑起来,本质上是理解四件事:硬件链路(RMII/PHY)→ 胶水层(ethernetif.c)→ 协议栈调度(MX_LWIP_Process)→ 内存裁剪(lwipopts.h)。这四环任何一环出问题,表现都各不相同,排查时要顺着这条链逐段定位。
跑通之后,一个 MCU 就真正从”单片机”变成了”网络设备”——无论是做 Modbus-TCP 网关、MQTT 节点,还是简单的内置 Web 配置页,都建立在这个基础之上。