4. lwIP移植接口详解:sys_arch层、netif与OSAL适配
好,咱们进入正题。lwIP 的移植,说白了就是给它配三样东西:操作系统抽象层(sys_arch)、底层网络接口(netif),以及操作系统抽象层(OSAL)的适配。这三样搞定了,lwIP 就能在你的硬件上跑起来。
我个人习惯把这三层比作「三驾马车」。缺了哪一匹,协议栈都跑不稳。今天咱们就一匹一匹来驯。
4.1 sys_arch 层:操作系统的「翻译官」
sys_arch 层是 lwIP 和 RTOS 之间的桥梁。lwIP 本身不依赖任何特定操作系统,它通过 sys_arch 接口来调用操作系统的信号量、邮箱、互斥锁、线程创建等功能。
说白了,你用的 FreeRTOS、RT-Thread、uC/OS,都得通过这层「翻译」给 lwIP 用。
4.1.1 必须实现的接口
我列一下最核心的几个接口,你在移植时一个都不能少:
| 接口函数 | 功能描述 | 典型实现 |
|---|---|---|
sys_sem_new |
创建信号量 | 调用 RTOS 的 semaphore create API |
sys_sem_signal |
释放信号量 | 调用 RTOS 的 semaphore give API |
sys_arch_sem_wait |
等待信号量(带超时) | 调用 RTOS 的 semaphore take 并处理超时 |
sys_mbox_new |
创建邮箱 | 用 RTOS 的消息队列实现 |
sys_mbox_post |
发送消息(非阻塞) | 调用 RTOS 的 queue send |
sys_arch_mbox_fetch |
接收消息(带超时) | 调用 RTOS 的 queue receive |
sys_thread_new |
创建线程 | 调用 RTOS 的 task create |
sys_prot / sys_unprot |
临界区保护 | 关/开中断或使用互斥锁 |
sys_arch_timeouts 机制可以帮你省不少事。
4.1.2 一个典型的 sys_arch.c 片段
以 FreeRTOS 为例,信号量创建的实现大概是这样的:
// sys_arch.c - FreeRTOS 实现示例
#include "lwip/sys.h"
#include "FreeRTOS.h"
#include "semphr.h"
err_t sys_sem_new(sys_sem_t *sem, u8_t count) {
// 创建二值信号量
*sem = xSemaphoreCreateBinary();
if (*sem == NULL) {
return ERR_MEM;
}
// 如果初始 count 不为 0,释放一次
if (count > 0) {
xSemaphoreGive(*sem);
}
return ERR_OK;
}
void sys_sem_signal(sys_sem_t *sem) {
xSemaphoreGive(*sem);
}
u32_t sys_arch_sem_wait(sys_sem_t *sem, u32_t timeout) {
TickType_t ticks;
BaseType_t ret;
if (timeout == 0) {
// 永久等待
ret = xSemaphoreTake(*sem, portMAX_DELAY);
return SYS_ARCH_TIMEOUT; // 实际上不会超时
} else {
// 将毫秒转换为 tick
ticks = timeout / portTICK_PERIOD_MS;
ret = xSemaphoreTake(*sem, ticks);
if (ret == pdTRUE) {
// 返回实际等待时间(毫秒)
return timeout - (ticks * portTICK_PERIOD_MS);
} else {
return SYS_ARCH_TIMEOUT;
}
}
}
4.2 底层网络接口(netif):让协议栈「看见」硬件
netif 层是 lwIP 和网卡驱动之间的接口。每个网络接口(以太网、Wi-Fi、PPP)都需要注册一个 struct netif 结构体,并提供 input 和 output 函数。
4.2.1 netif 结构体核心字段
| 字段 | 说明 | 你该填什么 |
|---|---|---|
ip_addr |
IP 地址 | 静态配置或 DHCP 获取 |
netmask |
子网掩码 | 根据网络环境设置 |
gw |
默认网关 | 路由器地址 |
output |
IP 层发送回调 | 指向你的发送函数 |
linkoutput |
链路层发送回调 | 指向你的 MAC 发送函数 |
input |
接收数据入口 | 驱动收到数据后调用 |
state |
私有数据指针 | 指向你的驱动结构体 |
4.2.2 netif_add 注册流程
你想想看,网卡驱动初始化完成后,需要调用 netif_add 把接口注册到 lwIP。流程是这样的:
- 分配并初始化
struct netif结构体 - 调用
netif_add注册,传入init回调 - 在
init回调中设置output和linkoutput函数指针 - 调用
netif_set_default设为默认接口(如果只有一个网口) - 调用
netif_set_up使能接口
// 网卡初始化函数示例
static err_t ethernetif_init(struct netif *netif) {
// 设置输出函数
netif->output = etharp_output; // 使用 ARP 封装
netif->linkoutput = low_level_output; // 你的 MAC 发送函数
// 设置 MAC 地址
netif->hwaddr_len = 6;
memcpy(netif->hwaddr, my_mac_addr, 6);
// 设置 MTU
netif->mtu = 1500;
// 设置接口标志
netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
return ERR_OK;
}
// 注册接口
struct netif my_netif;
ip4_addr_t ipaddr, netmask, gw;
IP4_ADDR(&ipaddr, 192, 168, 1, 100);
IP4_ADDR(&netmask, 255, 255, 255, 0);
IP4_ADDR(&gw, 192, 168, 1, 1);
netif_add(&my_netif, &ipaddr, &netmask, &gw, NULL, ethernetif_init, ethernet_input);
netif_set_default(&my_netif);
netif_set_up(&my_netif);
output 和 linkoutput 的区别。output 是 IP 层调用的,会经过 ARP 封装;linkoutput 是 ARP 层调用的,直接发原始数据帧。如果你用 NO_SYS=1 模式,output 可以直接指向 etharp_output。
4.3 操作系统抽象层(OSAL):让 lwIP 融入你的系统
OSAL 是比 sys_arch 更高一层的抽象。它定义了 lwIP 如何与操作系统交互,包括:
- 线程模型:tcpip_thread 主线程、ethernet_input 接收线程
- 同步机制:信号量、邮箱、互斥锁的封装
- 内存管理:lwIP 内部的内存池和堆管理
- 超时机制:sys_timeout 和 sys_untimeout
4.3.1 两种运行模式
| 模式 | 宏定义 | 适用场景 | 特点 |
|---|---|---|---|
| 无操作系统 | NO_SYS=1 |
裸机、简单 MCU | 所有代码在同一个上下文运行,需要轮询 |
| 有操作系统 | NO_SYS=0 |
RTOS、Linux | 多线程运行,tcpip_thread 处理协议栈 |
我个人建议,只要你的项目用了 RTOS,就老老实实把 NO_SYS 设为 0。为什么?因为多线程模式下,tcpip_thread 可以独立处理网络事件,不会阻塞你的应用任务。我在一个项目里试过 NO_SYS 模式,结果主循环里既要处理传感器数据又要轮询网络,最后网络延迟高得离谱。
4.3.2 tcpip_thread 的启动
在 NO_SYS=0 模式下,你需要调用 tcpip_init 来启动协议栈线程:
// main.c
#include "lwip/tcpip.h"
void my_network_init(void) {
// 初始化 tcpip 线程
tcpip_init(NULL, NULL);
// 等待 tcpip 线程就绪(可以用信号量同步)
// ...
// 添加网络接口
netif_add(&my_netif, ...);
netif_set_default(&my_netif);
netif_set_up(&my_netif);
}
tcpip_init 之后立即调用 netif_add,结果发现 netif 的 init 回调在 tcpip_thread 里执行,而我的 init 回调里用了一个还没初始化的硬件外设。解决方案是:在 tcpip_init 的回调参数里传递一个信号量,等 tcpip_thread 启动完成后再添加接口。
4.4 知识结构总览
下面这张图把三者的关系画清楚了。你对照着看,心里就有谱了:
4.5 移植检查清单
最后,我给你一份自检清单。每次移植完,对着打勾:
- sys_sem_new/signal/wait 全部实现,超时处理正确
- sys_mbox_new/post/fetch 实现,支持 NULL 消息
- sys_thread_new 创建 tcpip_thread 成功
- netif_add 注册成功,init 回调设置 output/linkoutput
- 网卡中断接收调用 netif->input 或 tcpip_input
- NO_SYS 宏定义与你的运行模式一致
- MEM_SIZE / PBUF_POOL_SIZE 等内存配置合理
- 临界区保护(sys_prot/sys_unprot)正确实现