裸机环境下不能直接用标准LwIP移植包,因其默认依赖POSIX接口(如select、pthread)和完整C库,而裸机单片机仅提供最小libc且无OS抽象层;必须关闭多线程相关选项(NO_SYS=1、LWIP_NETCONN=0等),手动实现sys_now(),用状态机+定时轮询替代阻塞调用,并确保DMA收发与pbuf管理正确。

为什么裸机环境下不能直接用标准LwIP移植包
LwIP 官方提供的 ports 目录里确实有 unix、win32 等适配,但没有“裸机单片机”这一类——它默认依赖 POSIX 接口(如 select()、pthread、sys/time.h)和 C 标准库的完整实现。而 Cortex-M3/M4 常见开发环境(如 Keil、IAR、GCC + CMSIS)通常只提供最小 libc(如 newlib-nano),不带线程/信号量/系统时钟抽象层。
关键矛盾点在于:LwIP 的核心调度模型是「轮询 + 超时事件驱动」,但它把底层定时器、网卡收发、中断同步都交给了移植层(sys_arch.c 和 ethernetif.c)实现。你必须亲手填平这三块:
-
sys_now():必须返回毫秒级单调递增时间戳,不能靠HAL_GetTick()直接返回(它可能被 HAL_Delay 阻塞或受 SysTick 重载影响) -
sys_sem_new()/sys_mbox_new():裸机没 OS,得用 CMSISosSemaphoreId_t(如果用了 CMSIS-RTOS 封装)或纯自旋+标志位模拟;但更常见的是直接禁用 LwIP 的多线程模式(LWIP_NETCONN=0、LWIP_TCPIP_CORE_LOCKING=0) -
low_level_input():必须在以太网 DMA 中断里调用ethernetif_input(),且确保 pbuf 不被复用——很多初学者在这里触发 double-free 或内存踩踏
如何配置 LwIP 让它在无 OS 下跑起来
裸机移植的本质是「关掉所有需要 OS 支持的模块」,不是硬扛线程安全。推荐最小可行配置(基于 LwIP 2.1.x):
- 关闭 TCP/IP 线程:
LWIP_TCPIP_CORE_LOCKING=0、LWIP_TCPIP_THREAD=0、LWIP_NETCONN=0 - 关闭动态内存:
MEMP_MEM_MALLOC=1(用mem_malloc替代 pool 分配),同时设MEMP_NUM_PBUF=16、MEMP_NUM_TCP_SEG=8(根据 RAM 大小调整) - 启用 NO_SYS 模式:
NO_SYS=1,此时sys_arch.c只需实现sys_now()和空桩函数(sys_sem_new()返回NULL即可) - 网卡驱动必须支持零拷贝接收:DMA 接收完成中断中,用
pbuf_alloc(PBUF_RAW, len, PBUF_POOL)分配 pbuf,再 memcpy 数据过去;切勿用PBUF_REF指向 DMA buffer——DMA 续传会覆盖未处理完的数据
示例 lwipopts.h 关键片段:
立即学习“C++免费学习笔记(深入)”;
#define NO_SYS 1 #define LWIP_SOCKET 0 #define LWIP_NETCONN 0 #define LWIP_TCPIP_CORE_LOCKING 0 #define MEMP_MEM_MALLOC 1 #define MEM_SIZE (16*1024) #define MEMP_NUM_PBUF 16 #define PBUF_POOL_SIZE 16
STM32 + ETH 外设移植最常卡住的三个地方
不是编译不过,而是 ping 通但无法建立 TCP 连接,或者 connect 后立即 RST。问题往往藏在硬件交互细节里:
-
ETH->DMABMR |= ETH_DMABMR_SR必须在初始化最后执行——很多例程把它放在 DMA 启动前,导致 descriptor 未就绪,收包中断永不触发 -
phy_init()后必须等ETH->MACSR & ETH_MACSR_MII置位再读 PHY 寄存器,否则PHY_BSR_LINK_STATUS总是 0 - 发送时若用
ethernet_output(),必须确保 pbuf chain 总长 ≤ MTU(默认 1500),否则 LwIP 自动分片——但裸机没 IP 分片重组能力,对方收到 fragment 就丢弃;解决办法是提前检查pbuf_clen(p)并截断或拒绝
一个典型调试技巧:在 tcp_input() 开头加 if (tcphdr->dest != htons(80)) return;,先锁定 HTTP 流量,排除其他协议干扰。
裸机下怎么发 HTTP 请求又不卡死主循环
没有 socket 阻塞等待,只能靠状态机 + 定时轮询。不要试图封装成阻塞式 API,那只会让代码变脆。
- 用
tcp_new()创建 PCB 后,注册connected回调,在里面发 GET 请求;再注册recv回调处理响应 - 主循环里每 10ms 调用一次
sys_check_timeouts()(LwIP 内部超时管理)和tcp_tmr()(TCP 定时器) - 发送缓冲区满时,
tcp_write()返回ERR_MEM,此时应暂停发送、等待sent回调触发后再继续——不能重试或忽略 - 务必在
sent回调里调用tcp_output(),否则数据卡在 write queue 不发出
容易被忽略的是:LwIP 的 tcp_tmr() 必须按严格周期调用(建议 250ms),否则重传、保活、窗口更新全失效。别指望靠 “只要发包就调一次” 来省事。


















