教程 39:从 lwip_system_init() 到 tcpip_input()——RT-Thread 如何把 lwIP 接进 RTOS 与 Ethernet Device
摘要:从 RT-Thread 的 lwIP 初始化入口追踪 Kconfig、sys_arch、eth_device、RX/TX 线程与 tcpip_input,建立真实 RTOS Port 的完整桥接流程。
[TOC]
Stage 38 已经把 lwIP Port Contract 拆成四块:lwipopts.h 配置、源码构建选择、OS/Arch Port、Network Port。Stage 39 不再重复这些抽象的原理,而是直接拿 RT-Thread 当前源码回答一个问题:RT-Thread 怎样把这些 contract 一项项落成可运行代码,并把一个 Ethernet Driver 最终接到 tcpip_thread。
RT-Thread 的完整 Device Framework、NetDev、SAL、DFS 并不是本篇目标。它们只在当前调用链真正出现时说明接口位置;Socket/SAL 与 NetDev 会留到 Stage 40~41。
阅读源码前:RT-Thread 官方文档先给框架,不替代目标 commit
RT-Thread 当前 User Guide 已经给出两张很适合先看的地图:Kernel Basics 说明 INIT_PREV_EXPORT、INIT_DEVICE_EXPORT、INIT_COMPONENT_EXPORT 等自动初始化阶段;Network Framework 则从系统层面说明 lwIP、SAL、Ethernet device 以及 tcpip/erx/etx 数据路径的职责。S7 这些页面适合先建立 RT-Thread 的术语和分层,不需要在本文重新写一遍框架教程。
但官方总览不会回答当前固定 revision 中 lwip_system_init() 怎样映射 Kconfig、sys_arch、eth_device、netifapi_netif_add() 与 tcpip_input()。因此下面仍以 commit dc8aaa7... 的源码为唯一主线;live 文档只承担阅读前置,不用于替代版本锁定的实现事实。
1. 从 RT-Thread 初始化入口 lwip_system_init() 开始
RT-Thread shared Port 的初始化入口位于:
1 | components/net/lwip/port/sys_arch.c |
lwip_system_init() 定义完成后,sys_arch.c 通过下面的导出宏把这个函数挂进 RT-Thread initialization sequence:
1 | INIT_PREV_EXPORT(lwip_system_init); |
因此系统初始化阶段会自动进入该函数。S3
下面进入完整的 lwip_system_init():
1 | int lwip_system_init(void) |
这个函数没有直接初始化 TCP、UDP、DHCP,而是先做两件更基础的事情:
eth_system_device_init_private()建立 RT-Thread 自己的 Ethernet RX/TX bridge context;tcpip_init()启动 upstream lwIP 的 TCPIP core thread。S3S4S6
主链因此从一开始就分成两个线程域:
1 | flowchart TD |
在继续进入两个子调用前,先追清 tcpip_init() 所依赖的编译期参数怎样由 RT-Thread 配置体系提供。
2. 从入口里出现的 TCPIP 参数反查 Kconfig:它并不直接替代 lwipopts.h
RT-Thread 的配置入口在 components/net/lwip/Kconfig。启用:
1 | RT_USING_LWIP |
之后,可以继续选择 lwIP 版本以及 TCP、UDP、DHCP、DNS、IPv6、PPP、资源数量和线程参数等。S1
当前 Kconfig 中可以直接看到这类配置:
1 | RT_LWIP_TCP |
但 lwIP Core 最终认识的仍然是:
1 | LWIP_TCP |
中间的翻译层就是 RT-Thread 的共享 components/net/lwip/port/lwipopts.h。S2
例如当前 Port 中存在明确映射:
1 |
TCPIP thread 参数也一样:
1 |
所以配置链是:
1 | flowchart LR |
这里已经能看出 RT-Thread 的第一个集成策略:上层使用 RT-Thread 自己的 Kconfig 命名,Port 层再把它翻译成 upstream lwIP contract。
3. Kconfig 最终锁定 NO_SYS=0:所以 tcpip_init() 必须创建 Core Thread
port/lwipopts.h 当前直接定义:S2
1 |
这就锁定了本文后面的运行模型:
1 | RT-Thread task / driver context |
Stage 11 已经解释过 NO_SYS=0 的线程模型,这里只关注 RT-Thread 怎么实现它。
4. 进入 eth_system_device_init_private():RT-Thread 先建立自己的 Ethernet RX/TX bridge
eth_system_device_init_private() 位于 port/ethernetif.c。当前默认没有定义 LWIP_NO_RX_THREAD/LWIP_NO_TX_THREAD 时,它分别初始化 mailbox 与静态 thread,然后启动 erx 与 etx。S4
源码主逻辑是:
1 | int eth_system_device_init_private(void) |
这里的 erx/etx 不是 lwIP 的 tcpip_thread。
它们属于 RT-Thread 的 Ethernet Port,用来在具体 Driver callback 与 lwIP netif 之间做线程桥接:
1 | erx |
这个区分非常重要。否则后面看到 erx -> tcpip_input -> tcpip 时容易误认为有两个 TCP/IP Core thread。
eth_system_device_init_private() 返回后,执行回到 lwip_system_init(),下一条关键语句是 tcpip_init(...)。
5. 回到 lwip_system_init(),进入 upstream tcpip_init()
RT-Thread vendored lwIP 2.1.2 的 tcpip_init() 仍遵循 upstream OS-mode contract:S6
1 | void |
Stage 38 里看到的 Port Contract 此刻开始落地:
1 | sys_mbox_new() |
RT-Thread sys_arch.c 中的线程适配函数是:S3
1 | sys_thread_t sys_thread_new(const char *name, |
所以 tcpip_thread 并不是 RT-Thread 重写的一套协议栈线程。线程函数仍来自 lwIP tcpip.c,RT-Thread 只实现“怎样创建一个线程去运行它”。
6. sys_arch 的核心思想就是“lwIP primitive → RT-Thread primitive”
无需再次逐函数学习 Stage 11 的 IPC 原理,把当前源码映射表看清即可:S2S3
| lwIP Port API | RT-Thread 当前实现 |
|---|---|
sys_sem_new() |
rt_sem_create() |
sys_sem_signal() |
rt_sem_release() |
sys_arch_sem_wait() |
rt_sem_take() + tick/ms conversion |
sys_mutex_new() |
rt_mutex_create() |
sys_mutex_lock() |
rt_mutex_take() |
sys_mbox_new() |
rt_mb_create() |
sys_mbox_post() |
rt_mb_send_wait() |
sys_arch_mbox_fetch() |
rt_mb_recv_interruptible() |
sys_thread_new() |
rt_thread_create() + rt_thread_startup() |
sys_now() |
rt_tick_get_millisecond() |
sys_jiffies() |
rt_tick_get() |
sys_arch_protect() |
SMP mutex 或 spinlock + IRQ save |
arch/sys_arch.h 则直接把类型对应起来:S2
1 | typedef rt_uint32_t sys_prot_t; |
所以 RT-Thread 对 lwIP 的 OS Port 不是修改 tcpip.c 的调度算法,而是在它要求 semaphore/mailbox/thread/time 时,用 RT-Thread 原语满足 contract。
7. tcpip_thread 启动以后,为什么 lwip_system_init() 才能继续
tcpip_init() 保存:
1 | initfunc = tcpip_init_done_callback |
然后启动 tcpip_thread。进入 tcpip_thread() 后,在主循环前会调用这个 callback:S6
1 | static void |
RT-Thread 提供的 callback 很简单:S3
1 | static void tcpip_init_done_callback(void *arg) |
因此初始化同步关系是:
1 | lwip_system_init() |
这个 semaphore 只承担初始化完成同步,不是 TCP/IP packet mailbox。
8. 到这里 lwIP Core 已经活了,但还没有任何具体 Ethernet Device
lwip_system_init() 完成意味着:
1 | sys_arch ready |
但实际网卡仍需要 BSP/Driver 提供。RT-Thread 为 Ethernet Driver 定义了一层很薄的桥接对象 struct eth_device:S4
1 | struct eth_device |
这一层把两个体系接在一起:
1 | RT-Thread side lwIP side |
具体 STM32 Driver 怎样实现 eth_rx/eth_tx 留到 Stage 42~43。本篇只追“Driver 已经提供这两个 callback 以后,RT-Thread 怎样把它注册进 lwIP”。
9. Driver 调 eth_device_init():这是 Ethernet Device 接入 Port 的入口
RT-Thread Driver 常见接入点是:
1 | eth_device_init(&device, "e0"); |
当前 eth_device_init() 本身很短:S4
1 | rt_err_t eth_device_init(struct eth_device * dev, const char *name) |
这里第一次把 Driver capability 转成 lwIP netif->flags 候选值:普通 Ethernet 需要 broadcast/ARP;如果启用 IGMP,再加入 multicast group capability。
继续进入 eth_device_init_with_flag()。
10. eth_device_init_with_flag():同时建立 RT Device 与 lwIP netif
eth_device_init_with_flag() 前半段完成这些动作:S4
1 | allocate struct netif |
这里已经出现了 Stage 38 的 Network Port contract:
1 | struct netif |
真正把它加入 lwIP Core 的位置在同一个函数后半段。继续阅读 eth_device_init_with_flag():
1 | /* if tcp thread has been started up, we add this netif to the system */ |
这行 netifapi_netif_add() 是本篇最重要的连接点之一。
它同时传入:
1 | netif -> 刚分配的 lwIP interface |
于是对象关系变成:
1 | flowchart LR |
11. 为什么这里用 netifapi_netif_add() 而不是直接 netif_add()
RT-Thread 已经选择 NO_SYS=0。因此从普通 RT-Thread context 修改 lwIP netif_list 时,需要遵守 Core thread contract。
vendored lwIP 2.1.2 的 netifapi_netif_add() 注释直接写明:它通过在 tcpip_thread context 运行 netif_add() 来实现 thread-safe add。S6
进入 netifapi_netif_add() 后,它把参数放进 netifapi_msg,最后:
1 | err = tcpip_api_call(netifapi_do_netif_add, &API_VAR_REF(msg).call); |
而 netifapi_do_netif_add() 才真正执行:
1 | if (!netif_add(msg->netif, |
所以这里不是多绕一层没有意义:
1 | RT-Thread initialization/driver context |
这正是 Stage 11/38 所讲 execution-context contract 的实际落地。
12. netif_add() 会回调 eth_netif_device_init():Port 开始填完整接口
netif_add() 保存传入的 state/input,并调用传入的 init callback。这里 init 就是 RT-Thread 的 eth_netif_device_init()。S4S6
进入 eth_netif_device_init(),当前路径依次做:S4
1 | netif->state -> struct eth_device |
当前源码在 RT_USING_NETDEV 开启时还会调用 netdev_add(netif)。这条支路在这里仅记录为“RT-Thread 上层网络接口抽象的注册点”;Stage 39 不展开 NetDev/SAL,因为它们不是 lwIP Port 能否收发 packet 的必要前置解释,Stage 40~41 会单独处理。
eth_netif_device_init() 返回后,netif_add() 完成注册;控制最终回到 Driver 的 eth_device_init() 调用方。此时 lwIP 已经拥有一个可工作的 struct netif。
13. 此时最关键的三条函数指针已经绑定
把初始化阶段压缩以后,一个 Ethernet netif 至少已经形成:
1 | netif->state |
因此后面的 RX/TX 不需要每次判断“这是哪家 MCU Driver”。netif->state 可以找回 eth_device,再通过 eth_rx/eth_tx 函数指针进入具体 Driver。
下面先追 RX。
14. RX 的第一跳不是 tcpip_input():Driver 先调用 eth_device_ready()
具体 Ethernet Driver 在 IRQ、DMA completion 或自己的 deferred context 中确认“有 packet 可取”后,调用:S4
1 | eth_device_ready(&device); |
当前函数完整实现是:
1 | rt_err_t eth_device_ready(struct eth_device* dev) |
这里传给 mailbox 的不是 packet pointer,而是:
1 | struct eth_device * |
rx_notice 用来避免同一个 device 在上一次通知尚未被 RX thread 消费时反复塞入相同 mailbox notification。
因此 ISR/Driver 此时只完成:
1 | “e0 有数据了” |
真正从 DMA/Driver 拉出 pbuf 的动作在 erx thread。
15. erx 线程被唤醒后进入 eth_rx_thread_entry()
eth_system_device_init_private() 已经把 erx thread 的入口绑定到 eth_rx_thread_entry()。因此 eth_device_ready() 的 mailbox message 到达以后,执行上下文切换到这个线程。S4
进入 eth_rx_thread_entry() 的 packet 主路径:
1 | static void eth_rx_thread_entry(void* parameter) |
这里同时解决了三个边界问题:
- link change 也被 deferred 到 Ethernet RX thread 处理;
- Driver 的
eth_rx()真正产生/返回pbuf; - 拿到
pbuf后不直接调用 TCP/UDP,而是调用此前绑定的netif->input()。
而 netif->input 在初始化时已经被设成 tcpip_input。
16. 从 netif->input() 进入 tcpip_input():这才真正跨进 lwIP Core
继续阅读 eth_rx_thread_entry() 的 RX 循环,此处调用此前绑定的 netif->input:
1 | device->netif->input(p, device->netif) |
由于 eth_device_init_with_flag() 注册 netif 时把 input 参数设为 tcpip_input,当前这次间接调用实际进入:
1 | tcpip_input(p, device->netif) |
RT-Thread vendored lwIP 2.1.2 的 tcpip_input() 会根据 netif flags 为 Ethernet packet 选择 ethernet_input,再进入 tcpip_inpkt()。S6 当前 shared lwipopts.h 没有打开 LWIP_TCPIP_CORE_LOCKING_INPUT,因此沿 lwIP 2.1.2 默认分支把 packet 封装成 input message 后投递到 TCPIP thread mailbox;若项目显式改用 core-locking-input,tcpip_inpkt() 的执行策略会变化,不能把“必经 mailbox”泛化成所有 lwIP Port 的固定规则。
关键入口是:
1 | err_t |
于是完整 RX context transition 才终于闭环:
1 | flowchart TD |
这也是 RT-Thread Port 最重要的线程边界:Driver/ISR 不直接执行 lwIP TCP/IP Core;RT-Thread 先把硬件事件 deferred 到 erx,再由 tcpip_input() 把 packet deferred 到 lwIP tcpip_thread。
具体 MCU 是否一定需要独立 erx,是 Port 设计选择;这里描述的是当前 RT-Thread shared Ethernet Port 的实现。
17. TX 方向从 netif->linkoutput 回到 RT-Thread eth_device
初始化时已经执行:
1 | netif->linkoutput = ethernetif_linkoutput; |
所以前面 Stage 19/20 学过的 Ethernet TX 最终来到 RT-Thread 的 ethernetif_linkoutput()。S4
当前默认存在 TX thread 时,这个函数把 netif + pbuf 包成 eth_tx_msg,发进 eth_tx_thread_mb,并等待 completion:
1 | static err_t ethernetif_linkoutput(struct netif *netif, struct pbuf *p) |
etx thread 收到 message 后,通过:
1 | msg->netif->state |
找回 struct eth_device,再调用:
1 | enetif->eth_tx(&(enetif->parent), msg->buf) |
最后 rt_completion_done() 唤醒等待者。S4
所以 TX chain 是:
1 | flowchart LR |
如果配置 LWIP_NO_TX_THREAD,ethernetif_linkoutput() 则直接从 netif->state 找到 eth_device 并调用 eth_tx()。这说明独立 TX thread 是 RT-Thread Port 的可选实现策略,不是 lwIP Core contract。
18. Link Up/Down 也通过同一 bridge,而不是 PHY 代码直接改 Core 状态
Driver/PHY 检测到 link 改变时可以调用:S4
1 | eth_device_linkchange(dev, up); |
有 RX thread 的默认配置下,它先在 spinlock 下更新:
1 | link_changed |
然后把 device 发到 eth_rx_thread_mb。erx thread 前面已经看到这段处理:
1 | link_status = up |
因此 Link event 同样遵守:
1 | Driver/PHY context |
PHY/MDIO/Auto-Negotiation 的具体 Driver 实现会留到 Stage 44;本篇只确认 RT-Thread Port 给它预留了怎样的上报路径。
19. SConscript 说明“Port 层”和“vendored lwIP”在构建上也是两部分
RT-Thread 当前构建结构进一步印证了这个分层。S5
共享 Port 的:
1 | components/net/lwip/port/SConscript |
会把 port 目录自己的 .c 作为 lwIP group 加入,依赖 RT_USING_LWIP。
而:
1 | components/net/lwip/lwip-2.1.2/SConscript |
单独列出 Core、IPv4、IPv6、API、netif、PPP、SNMP 等 upstream source,并根据 RT-Thread config 选择附加模块。S5
因此从构建视角也可以画成:
1 | RT-Thread configuration |
值得注意的是,当前内置 2.1.2 SConscript 虽然定义了 HTTP、SNTP、MQTT 等 app source list,但底部基础 src 并没有把这些 app group 全部无条件追加进去。S5 所以不能因为 upstream 目录里存在某个 app,就假设 RT-Thread 内置 lwIP 默认一定把它编进固件。产品仍要以实际 package/version/build script 为准。
20. RT-Thread 在这里“改了什么”:主要是 Port 与 Integration,不等于重写 lwIP Core
追完调用链后,Stage 38 的 contract 与 RT-Thread 当前实现可以逐项对应:
| Stage 38 contract | RT-Thread 当前实现 |
|---|---|
| Feature/resource config | Kconfig -> rtconfig.h -> port/lwipopts.h |
| Compiler/arch abstraction | port/arch/cc.h |
| OS handle types | port/arch/sys_arch.h |
| Semaphore/mailbox/thread/time | port/sys_arch.c |
| Core thread | vendored lwIP tcpip.c + RT sys_thread_new() |
| Ethernet interface object | struct netif + RT struct eth_device |
| RX bridge | eth_device_ready -> erx -> eth_rx -> tcpip_input |
| TX bridge | linkoutput -> etx -> eth_tx |
| Link bridge | eth_device_linkchange -> netifapi_netif_set_link_* |
这比“RT-Thread 修改了 lwIP”更准确。
RT-Thread 确实维护 vendored lwIP 版本和适配代码,也存在版本兼容条件,但当前这条主线真正关键的是:
1 | upstream/vendored lwIP Core contract |
Stage 40/41 才会继续研究 RT-Thread 为什么又在 Socket/Network Device 层增加 SAL、NetDev 等上层抽象。
21. Stage 39 的完整调用链
初始化:
1 | flowchart TD |
网卡注册:
1 | flowchart TD |
运行时 RX/TX:
1 | RX: |
到这里,Stage 38 的“抽象 Port Contract”已经通过 RT-Thread 真实源码完整落地。
下一篇 Stage 40 不再继续深入 rt_thread/rt_mailbox/rt_device 的内部实现,而是从应用真正调用的 socket() 开始,只追与 lwIP 相交的路径:为什么 RT-Thread 会先经过统一 fd/DFS 与 SAL,最后才进入 lwip_socket()。完整 DFS、Device Framework 与 RT-Thread Kernel 源码仍留给独立 RT-Thread Source Lab。
资料来源
[S1] RT-Thread lwIP Kconfig
- 类型:RT-Thread 官方仓库源码
- 版本:commit
dc8aaa73f2dbea255325ec058a083aeeb5381d0a,2026-09-28 - 定位:
components/net/lwip/Kconfig:RT_USING_LWIP、version choice、protocol/resource/thread options - URL/文档:RT-Thread lwIP Kconfig
- 使用位置:“版本与 feature/resource 配置入口”
- 支撑内容:证明当前 RT-Thread 可选 lwIP 版本以及 Kconfig 暴露的主要协议、资源与线程参数
[S2] RT-Thread lwIP lwipopts.h 与 arch Port
- 类型:RT-Thread 官方仓库源码
- 版本:同上
- 定位:
components/net/lwip/port/lwipopts.h、port/arch/sys_arch.h、port/arch/cc.h - URL/文档:RT-Thread lwipopts.h、RT-Thread sys_arch.h、RT-Thread cc.h
- 使用位置:“Kconfig 到 lwIP macro 映射”“NO_SYS 模式”“RTOS handle 类型”
- 支撑内容:说明
RT_LWIP_*如何转成 upstream option,并证明 RT-Thread 当前使用NO_SYS=0
[S3] RT-Thread lwIP OS Port
- 类型:RT-Thread 官方仓库源码
- 版本:同上
- 定位:
components/net/lwip/port/sys_arch.c:lwip_system_init()、tcpip_init_done_callback()、sys_sem_*、sys_mbox_*、sys_thread_new()、sys_now()、sys_arch_protect() - URL/文档:RT-Thread sys_arch.c
- 使用位置:“真实初始化入口”“sys_arch mapping”“tcpip_thread 创建”
- 支撑内容:证明 lwIP OS abstraction 如何映射到 RT-Thread IPC/thread/time/protection primitives
[S4] RT-Thread Ethernet Port
- 类型:RT-Thread 官方仓库源码
- 版本:同上
- 定位:
components/net/lwip/port/ethernetif.c、port/netif/ethernetif.h:struct eth_device、eth_system_device_init_private()、eth_device_init()、eth_device_ready()、eth_rx_thread_entry()、ethernetif_linkoutput()、eth_device_linkchange() - URL/文档:RT-Thread ethernetif.c、RT-Thread ethernetif.h
- 使用位置:“eth_device 与 netif 绑定”“RX/TX thread bridge”“Link change”
- 支撑内容:Stage 39 Ethernet integration 主调用链的直接实现证据
[S5] RT-Thread lwIP build integration
- 类型:RT-Thread 官方仓库源码
- 版本:同上
- 定位:
components/net/lwip/port/SConscript、components/net/lwip/lwip-2.1.2/SConscript - URL/文档:RT-Thread port SConscript、RT-Thread lwIP 2.1.2 SConscript
- 使用位置:“Port 与 vendored source 的构建关系”“应用源码是否默认加入”
- 支撑内容:说明 RT-Thread 如何把 shared Port 和选定 lwIP implementation 组合进 firmware
[S6] RT-Thread vendored lwIP 2.1.2 tcpip / netifapi / netif
- 类型:RT-Thread 仓库内置 lwIP 源码
- 版本:RT-Thread commit
dc8aaa73f2dbea255325ec058a083aeeb5381d0a中的 vendored 2.1.2 - 定位:
components/net/lwip/lwip-2.1.2/src/api/tcpip.c:tcpip_init()、tcpip_thread()、tcpip_input();src/api/netifapi.c:netifapi_netif_add()、netifapi_do_netif_add();src/core/netif.c:netif_add() - URL/文档:RT-Thread vendored tcpip.c、RT-Thread vendored netifapi.c、RT-Thread vendored netif.c
- 使用位置:“tcpip thread 启动”“thread-safe netif add”“tcpip_input RX bridge”
- 支撑内容:证明 RT-Thread Port 如何继续调用标准 lwIP 2.1.2 Core/API,而不是在 Port 层重写 TCP/IP processing
[S7] RT-Thread User Guide — Kernel Basics 与 Network Framework
- 类型:RT-Thread 官方在线文档
- 版本:访问日期 2026-10-03;用于概念/框架导读,源码事实仍固定到本文目标 commit
- URL/文档:Kernel Basics、Network Framework
- 使用位置:“阅读源码前”“自动初始化阶段”“RT-Thread 网络框架分层”
- 支撑内容:解释 RT-Thread 自动初始化宏的阶段语义,以及官方对 lwIP/SAL/Ethernet RX/TX framework 的整体划分









