LWIP协议栈入门教程(1)
·
第一步:核心库函数梳理
1.1 什么是 lwIP?
lwIP = Lightweight IP(轻量级 TCP/IP 协议栈)。它是一个为嵌入式系统量身打造的开源 TCP/IP 协议栈,让你的单片机(如 STM32)也能联网通信。
打个比方:lwIP 就像给你的单片机装了一个"联网模块",让它可以像电脑一样通过网络发数据、收数据。
1.1.1 lwIP 协议栈分层架构
┌──────────────────────────────────────────────────────┐
│ 应用层 (Application) │
│ 你的 lwip_demo.c / httpd / MQTT / SNTP │
├──────────────────────────────────────────────────────┤
│ NETCONN API / Socket API (lwip/api.h) │
│ ↑ 封装层,阻塞式调用,面向应用开发者 │
├──────────────────────────────────────────────────────┤
│ 传输层 (Transport):TCP (tcp.c) / UDP (udp.c) │
│ ↑ 可靠传输 + 流量控制 / 不可靠传输 │
├──────────────────────────────────────────────────────┤
│ 网络层 (Network):IP (ip.c) / ICMP / ARP │
│ ↑ 寻址、路由、碎片重组 │
├──────────────────────────────────────────────────────┤
│ 网络接口层 (Link):netif / ethernetif / DMA驱动 │
│ ↑ 收发以太网帧,管理MAC │
├──────────────────────────────────────────────────────┤
│ 硬件层 (Hardware):STM32 ETH MAC + PHY + RJ45 │
└──────────────────────────────────────────────────────┘
初学者关键理解点:
- RAW API 直接操作传输层(TCP/UDP),不走 NETCONN/Socket 封装
- NETCONN API 在 TCP/UDP 上面加了一层抽象,提供阻塞式调用
- Socket API 在 NETCONN 上再封装一层,提供标准 BSD Socket 接口
- 这三层 API 从上到下,最底层的实现都是同一个 TCP/IP 协议栈,只是封装程度不同
1.1.2 pbuf —— 数据包的核心载体
pbuf (Packet Buffer) 是 lwIP 中最重要的数据结构,所有网络数据都由它承载。
| pbuf 分配类型 | 含义 | 内存来源 | 适用场景 |
|---|---|---|---|
PBUF_RAW |
原始数据包 | POOL/RAM | low_level_input 接收以太网帧,不含头部预留 |
PBUF_POOL |
从内存池快速分配 | memp 固定大小池 | DMA 接收缓冲、小数据快速收发 |
PBUF_RAM |
从堆中动态分配 | mem 堆(malloc) | 大尺寸数据、需要连续内存时 |
PBUF_TRANSPORT |
传输层发送用 | POOL/RAM | udp_send/tcp_write 时,会自动在 payload 前预留 UDP/TCP 头空间 |
pbuf 链的结构:
pbuf_1 pbuf_2 pbuf_3
┌──────────┐ next ┌──────────┐ next ┌──────────┐
│ payload ─┤──────────────→│ payload ─┤──────────────→│ payload ─┤→ NULL
│ len = N │ │ len = M │ │ len = K │
│ tot_len │ │ tot_len │ │ tot_len │
│ = N+M+K │ │ = N+M+K │ │ = N+M+K │
└──────────┘ └──────────┘ └──────────┘
遍历方式:
for (q = p; q != NULL; q = q->next) {
memcpy(dest + offset, q->payload, q->len); // 逐段拷贝数据
}
payload= 指向实际数据的指针len= 当前片段的长度tot_len= 整条链的总长度(所有链表中每片都存同样的值)next= 指向下一个 pbuf 片段
1.2 核心函数速查表
下表整理了本例程中用到的 lwIP 核心 API 函数。按模块分类,方便你以后查阅。
一、初始化与全局管理
| 函数名 | 所属模块 | 通俗解释 |
|---|---|---|
lwip_init() |
初始化 | “一键启动协议栈”——把 lwIP 内部的所有模块(内存、网络接口、协议等)全部初始化好。就像你开电脑时要先启动操作系统一样。 |
tcpip_init() |
初始化 | “带操作系统的启动”——在 FreeRTOS 下使用的初始化,内部创建一个 tcpip_thread 线程自动处理协议栈,比 lwip_init() 多了一层 OS 抽象。 |
lwip_pkt_handle() |
数据接收 | “取快递”——检查以太网 DMA 有没有收到新数据包,有的话通过 ethernetif_input() 送给 lwIP 协议栈处理。裸机下必须在主循环中定期调用。 |
sys_check_timeouts() |
定时管理 | “定时器心跳”——定期检查 lwIP 内部的各种定时器是否超时(比如 TCP 重传、ARP 老化等)。相当于你的闹钟提醒功能。 |
sys_now() |
定时管理 | “获取当前毫秒时间”——返回当前系统时钟的毫秒值,lwIP 用这个来计算各种超时。 |
二、内存管理
| 函数名 | 所属模块 | 通俗解释 |
|---|---|---|
pbuf_alloc() |
数据包管理 | “申请一个网络数据包”——在内存中分配一个 pbuf 结构体来存放网络数据。pbuf 就像是网络数据的一个"快递箱",可以装要发或刚收到的数据。 |
pbuf_free() |
数据包管理 | “释放快递箱”——用完数据包后释放内存,不然内存会越用越少(内存泄漏)。 |
pbuf_take() |
数据包管理 | “把数据装进快递箱”——将用户缓冲区中的数据复制到 pbuf 结构体中。 |
mem_malloc() |
内存堆管理 | “申请一块堆内存”——lwIP 自己实现的动态内存分配函数,类似于 C 语言的 malloc() 。 |
mem_free() |
内存堆管理 | “释放堆内存”——释放 mem_malloc 申请的内存。 |
三、网络接口(网卡)
| 函数名 | 所属模块 | 通俗解释 |
|---|---|---|
netif_add() |
网络接口 | “注册一张网卡”——告诉 lwIP:"我现在有一张网卡要加入网络,这是它的初始化函数和输入函数。"相当于给电脑插入网卡并安装驱动。 |
netif_set_default() |
网络接口 | “设为默认网卡”——如果有多张网卡,指定这一张为默认路由出口。就像你电脑有 WiFi 和有线网卡,系统会选一个默认的。 |
netif_set_up() |
网络接口 | “打开网卡”——把网卡设置为"已启用"状态,告诉 lwIP 这张网卡可以用了。 |
netif_set_down() |
网络接口 | “关闭网卡”——把网卡设置为"已禁用"状态。 |
netif_set_link_callback() |
网络接口 | “注册网线插拔通知”——当网线被拔掉或插上时,lwIP 会调用你注册的这个函数。 |
netif_is_link_up() |
网络接口 | “检查网线是否插好”——判断物理链路是否正常的函数。 |
四、地址设置
| 函数名 | 所属模块 | 通俗解释 |
|---|---|---|
ip4_addr() 宏 |
IP 地址 | “组装IP地址”——把 4 个数字(如 192,168,1,30)组装成 lwIP 认识的 IP 地址格式。 |
ip_addr_set_zero_ip4() |
IP 地址 | “清空IP地址”——把 IP 地址设为 0.0.0.0(未分配状态),等待 DHCP 分配。 |
dhcp_start() |
DHCP | “派地去要IP地址”——发送 DHCP 请求,让路由器自动分配一个 IP 地址。 |
dhcp_stop() |
DHCP | “停止要IP地址”——停止 DHCP 请求。 |
dhcp_supplied_address() |
DHCP | “检查IP地址要到没有”——判断 DHCP 是否成功分配到 IP 地址。 |
五、UDP 通信(RAW API)
| 函数名 | 所属模块 | 通俗解释 |
|---|---|---|
udp_new() |
UDP | “创建一个UDP插座”——创建一个新的 UDP 控制块(类似于一个"快递收发点")。 |
udp_remove() |
UDP | “拆除UDP插座”——删除 UDP 控制块,释放资源。 |
udp_bind() |
UDP | “绑定端口号”——把 UDP 控制块绑定到本地某个端口上,这样别人发到这个端口的数据你才能收到。就像你的快递箱上贴了房间号。 |
udp_connect() |
UDP | “指定对方的地址”——设置远程主机的 IP 地址和端口号,告诉 lwIP 你要跟谁通信。 |
udp_disconnect() |
UDP | “断开UDP连接”——清除远程地址设置。 |
udp_send() |
UDP | “发送UDP数据”——通过 UDP 把数据发送出去。 |
udp_recv() |
UDP | “注册UDP收件回调”——注册一个回调函数,当收到 UDP 数据时,lwIP 会自动调用这个函数。相当于告诉快递员:“有包裹送到就喊我!” |
六、TCP 通信(RAW API)
| 函数名 | 所属模块 | 通俗解释 |
|---|---|---|
tcp_new() |
TCP | “创建一个TCP插座”——创建一个新的 TCP 控制块。 |
tcp_bind() |
TCP | “绑定端口号”——绑定本地端口。 |
tcp_connect() |
TCP | “连接远程服务器”——向远程服务器发起 TCP 连接请求(三次握手)。 |
tcp_abort() |
TCP | “强行断开连接”——直接终止 TCP 连接,不等待挥手完成。 |
tcp_write() |
TCP | “把数据写进发送缓冲区”——将要发送的数据复制到 lwIP 内部的 TCP 发送缓冲区中。注意:这只是"准备发送",还不是"真正发出"。 |
tcp_output() |
TCP | “真正发出数据”——把 TCP 发送缓冲区中准备好的数据真正通过网络发出去。 |
tcp_recved() |
TCP | “告诉lwIP:数据我收好了”——当应用层读取了接收到的数据后,通知 lwIP 可以释放缓冲区接收更多数据。如果不调用这个函数,lwIP 会以为你还没看完数据,就不再收新的。 |
tcp_sndbuf() |
TCP | “查看发送缓冲区还剩多少”——返回当前 TCP 发送缓冲区的剩余空间大小。 |
tcp_arg() |
TCP | “给回调函数传参数”——注册一个自定义参数,在后续的回调函数中可以取出来用。 |
tcp_recv() |
TCP | “注册TCP收件回调”——当收到 TCP 数据时会调用这个函数。 |
tcp_sent() |
TCP | “注册发送确认回调”——当对方确认收到我们的数据后会调用这个函数。 |
tcp_err() |
TCP | “注册错误回调”——当 TCP 连接发生错误时会调用这个函数。 |
tcp_poll() |
TCP | “注册周期轮询回调”——lwIP 会定期(每隔 N 秒)调用这个函数,让你有机会做一些周期性检查。 |
tcp_listen(pcb) |
TCP | “进入监听状态”——TCP服务器专用,将绑定的 PCB 转为 LISTEN 状态,等待客户端连接。 |
tcp_accept(pcb, fn) |
TCP | “注册新连接回调”——TCP服务器专用,当有新客户端连接时 lwIP 会自动调用此函数,并传入一个新的 PCB。 |
tcp_close(pcb) |
TCP | “优雅关闭连接”——发起 TCP 四次挥手,正常关闭连接。对比 tcp_abort() 是暴力断开。 |
什么是 RAW API?
RAW API 是 lwIP 最底层的编程接口,直接与 TCP/UDP 协议交互,没有操作系统也可以使用。特点是:
- 没有阻塞等待(non-blocking)
- 全部通过回调函数来通知你
- 代码量少、效率高
- 适合裸机(无 RTOS)或对性能要求高的场景
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)