以太网与 LwIP 协议栈在 MCU 上的移植

2026年8月15日 0 By admin

以太网与 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 为例,关键配置如下:

  1. 使能 ETH 外设,接口选择 RMII
  2. 配置 PHY 地址,LAN8720 默认地址为 0(需查硬件原理图,有的板子用 1);
  3. 使能 LWIP 中间件(CubeMX 已集成,无需自己下载);
  4. 网卡缓冲区大小根据应用调整,默认 ETH_RX_BUF_SIZE = 1524 够用;
  5. 若用 DHCP,勾选 LWIP_DHCP

生成代码后,最核心的适配点在 ethernetif.clow_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。经验是:先用默认值跑通,再根据实际吞吐逐步调优,不要一上来就盲目加内存。

六、常见坑与排查思路

  1. ping 不通:先查 PHY 链路状态(ETH_ReadPHYRegister 读 BSR 寄存器),确认网线、PHY 地址、50MHz 时钟都对。
  2. 能 ping 但 TCP 建链慢/失败:几乎都是忘了周期调用 MX_LWIP_Process(),或任务优先级过低被饿死。
  3. 大量数据收发掉包:加大 ETH_RX_BUF_SIZEMEMP_NUM_NETBUFMEMP_NUM_TCP_SEG,并确认以太网中断优先级合理。
  4. IP 重复导致网络冲突:DHCP 未成功时回退的静态 IP 可能和局域网内其他设备冲突。

总结

把 LwIP 跑起来,本质上是理解四件事:硬件链路(RMII/PHY)→ 胶水层(ethernetif.c)→ 协议栈调度(MX_LWIP_Process)→ 内存裁剪(lwipopts.h)。这四环任何一环出问题,表现都各不相同,排查时要顺着这条链逐段定位。

跑通之后,一个 MCU 就真正从”单片机”变成了”网络设备”——无论是做 Modbus-TCP 网关、MQTT 节点,还是简单的内置 Web 配置页,都建立在这个基础之上。