3、LwIP协议栈移植(上):LwIP源码结构解析、操作系统抽象层(sys_arch)移植、网卡驱动接口实现
好,咱们今天正式开始啃LwIP移植这块硬骨头。说实话,很多工程师一提到协议栈移植就头大,觉得那是底层大牛才干的事。其实不然,你只要把LwIP的骨架摸清楚,移植就是个按部就班的体力活。
我个人习惯把LwIP移植拆成三个核心步骤:源码结构梳理 → 操作系统抽象层适配 → 网卡驱动对接。今天这上半部分,咱们先把前两步讲透,顺便把驱动接口的框架搭起来。
3.1 LwIP源码结构解析——别被文件夹吓到
我第一次打开LwIP源码包时,说实话也有点懵。一堆文件夹,几百个文件,从哪下手?后来我总结了一个规律:你只需要关注三个目录。
| 目录 | 作用 | 移植时改不改? |
|---|---|---|
src/core/ |
协议栈核心,TCP/IP、UDP、ICMP等协议实现 | 不动,这是灵魂 |
src/netif/ |
网卡接口层,包含ethernetif.c模板 | 要改,这是你的网卡驱动 |
src/include/lwip/ |
头文件,各种配置宏定义 | 要改,主要是opt.h和lwipopts.h |
嗯,这里要注意:src/arch/目录是空的,需要你自己创建。这个目录放的就是操作系统抽象层——sys_arch的实现。说白了,LwIP本身不依赖任何OS,但它留了一堆钩子函数,让你把OS的线程、信号量、邮箱这些东西挂上去。
核心原则:LwIP的core层你一个字都不要改。所有平台相关的东西,都通过宏定义和回调函数来隔离。这是嵌入式软件设计的精髓——分层隔离。
3.2 操作系统抽象层(sys_arch)移植——让LwIP跑在RTOS上
为什么需要sys_arch?你想想看,LwIP内部需要创建线程(比如tcpip_thread)、需要同步机制(比如信号量保护临界区)、需要消息传递(比如邮箱传递网络事件)。这些在裸机上没法直接做,必须借助RTOS。
sys_arch说白了就是一张适配表,把LwIP需要的OS服务,映射到你实际用的RTOS API上。我以FreeRTOS为例,给你看看核心的几个接口怎么实现。
3.2.1 信号量(Semaphore)实现
LwIP内部大量使用信号量来做同步。比如网卡收到数据后,通过信号量唤醒tcpip_thread来处理。
// sys_arch.c 中的信号量实现
#include "FreeRTOS.h"
#include "semphr.h"
#include "arch/sys_arch.h"
// 创建信号量
err_t sys_sem_new(sys_sem_t *sem, u8_t count) {
// 我习惯用二值信号量,初始值为0
*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 = pdMS_TO_TICKS(timeout);
ret = xSemaphoreTake(*sem, ticks);
if (ret == pdTRUE) {
return 0; // 成功,返回等待时间(简化处理)
} else {
return SYS_ARCH_TIMEOUT;
}
}
}
我的经验:信号量超时处理是个坑。很多新手直接把timeout参数忽略,全部用永久等待。这在网络负载高的时候,会导致tcpip_thread被长时间阻塞,其他任务饿死。我建议至少给tcpip_thread的邮箱等待设置一个100ms的超时。
3.2.2 邮箱(Mailbox)实现
邮箱是LwIP消息传递的核心机制。tcpip_thread通过邮箱接收来自各个协议层和网卡的消息。在FreeRTOS里,我一般用队列来实现。
// 邮箱实现
err_t sys_mbox_new(sys_mbox_t *mbox, int size) {
// 队列长度建议设置为:TCP_MSS * 2 + 一些余量
*mbox = xQueueCreate(size, sizeof(void *));
if (*mbox == NULL) {
return ERR_MEM;
}
return ERR_OK;
}
void sys_mbox_free(sys_mbox_t *mbox) {
vQueueDelete(*mbox);
}
void sys_mbox_post(sys_mbox_t *mbox, void *msg) {
// 这个函数不能在中断中调用
xQueueSend(*mbox, &msg, portMAX_DELAY);
}
u32_t sys_arch_mbox_fetch(sys_mbox_t *mbox, void **msg, u32_t timeout) {
TickType_t ticks;
BaseType_t ret;
if (timeout == 0) {
ret = xQueueReceive(*mbox, msg, portMAX_DELAY);
} else {
ticks = pdMS_TO_TICKS(timeout);
ret = xQueueReceive(*mbox, msg, ticks);
if (ret != pdTRUE) {
return SYS_ARCH_TIMEOUT;
}
}
return 0;
}
曾经踩过的坑:邮箱队列长度设置太小。有一次我在一个项目里把邮箱队列设成了4,结果网络一忙,消息堆积,tcpip_thread处理不过来,直接导致协议栈死锁。后来我改成TCP_MSS * 2,再也没出过问题。记住:邮箱队列宁可大一点,不要省那几百字节的RAM。
3.2.3 线程创建与临界区保护
LwIP需要创建至少一个线程——tcpip_thread。如果你用了PPP或DHCP,可能还需要额外的线程。
// 创建线程
sys_thread_t sys_thread_new(const char *name,
lwip_thread_fn thread,
void *arg,
int stacksize,
int prio) {
TaskHandle_t handle = NULL;
// 注意:FreeRTOS的优先级是数值越大优先级越高
// LwIP的优先级定义可能相反,需要做映射
UBaseType_t freertos_prio = LWIP_TO_FREERTOS_PRIO(prio);
xTaskCreate(thread, name, stacksize, arg, freertos_prio, &handle);
return (sys_thread_t)handle;
}
// 临界区保护
sys_prot_t sys_arch_protect(void) {
// 对于单核MCU,关中断即可
taskENTER_CRITICAL();
return 0; // 返回值用于恢复,这里简化处理
}
void sys_arch_unprotect(sys_prot_t pval) {
taskEXIT_CRITICAL();
}
3.3 网卡驱动接口实现——让数据流动起来
网卡驱动是LwIP和物理硬件的桥梁。LwIP定义了一个netif结构体,你需要实现它的output、input、linkoutput等回调函数。
3.3.1 netif结构体初始化
// 网卡初始化函数
err_t ethernetif_init(struct netif *netif) {
// 设置网卡硬件地址(MAC地址)
netif->hwaddr_len = 6;
netif->hwaddr[0] = 0x00;
netif->hwaddr[1] = 0x11;
netif->hwaddr[2] = 0x22;
netif->hwaddr[3] = 0x33;
netif->hwaddr[4] = 0x44;
netif->hwaddr[5] = 0x55;
// 设置最大传输单元
netif->mtu = 1500;
// 注册回调函数
netif->output = etharp_output; // IP层发包,走ARP
netif->linkoutput = low_level_output; // 底层实际发送
// 设置网卡状态
netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
// 初始化底层硬件
low_level_init(netif);
return ERR_OK;
}
3.3.2 数据发送与接收
发送函数相对简单:把pbuf链中的数据,通过DMA或轮询方式写入网卡寄存器。
static err_t low_level_output(struct netif *netif, struct pbuf *p) {
struct pbuf *q;
u8_t *buffer;
int len = 0;
// 计算总长度
for (q = p; q != NULL; q = q->next) {
len += q->len;
}
// 分配发送缓冲区(从网卡硬件获取)
buffer = get_tx_buffer();
if (buffer == NULL) {
return ERR_BUF;
}
// 将pbuf链拷贝到连续缓冲区
len = 0;
for (q = p; q != NULL; q = q->next) {
MEMCPY(buffer + len, q->payload, q->len);
len += q->len;
}
// 启动DMA发送
start_tx_dma(buffer, len);
return ERR_OK;
}
接收函数稍微复杂一点。它通常在中端服务程序中被调用,或者由轮询线程调用。
// 在网卡接收中断中调用
void ethernetif_input(struct netif *netif) {
struct pbuf *p;
u8_t *rx_buffer;
int len;
// 检查是否有新数据
if (!has_rx_data()) {
return;
}
// 获取接收缓冲区
rx_buffer = get_rx_buffer();
len = get_rx_length();
// 分配pbuf,注意使用PBUF_RAM类型
p = pbuf_alloc(PBUF_RAW, len, PBUF_RAM);
if (p != NULL) {
// 拷贝数据到pbuf
pbuf_take(p, rx_buffer, len);
// 将数据交给LwIP协议栈处理
if (netif->input(p, netif) != ERR_OK) {
pbuf_free(p);
}
}
// 释放硬件接收缓冲区
free_rx_buffer();
}
关键点:接收中断中不要做太多事情。我见过有人直接在中断里调用netif->input,结果中断服务时间过长,导致其他中断丢失。正确的做法是:中断里只做数据拷贝和标志位设置,然后通过信号量或消息通知tcpip_thread来处理。
3.4 移植检查清单
好了,理论讲完了。我给你列个清单,移植的时候对着检查,能省不少调试时间。
| 检查项 | 说明 | 常见问题 |
|---|---|---|
| sys_arch信号量 | 创建、释放、等待(带超时) | 超时参数被忽略,导致死等 |
| sys_arch邮箱 | 创建、发送、接收(带超时) | 队列长度太小,导致消息丢失 |
| sys_arch线程 | 创建tcpip_thread,栈空间足够 | 栈溢出,表现为随机死机 |
| 网卡发送 | pbuf链转连续缓冲区,DMA发送 | pbuf释放时机不对,内存泄漏 |
| 网卡接收 | 中断中拷贝数据,通知协议栈 | 中断中调用协议栈函数,导致重入 |
| lwipopts.h配置 | 内存池大小、超时时间、调试开关 | MEM_SIZE太小,分配失败 |
嗯,今天就先到这儿。下半部分我们会深入调试技巧、性能调优,以及一些高级特性(比如零拷贝、checksum卸载)的移植方法。到时候我会分享一个我实际项目中遇到的性能瓶颈案例——那个问题折腾了我整整三天,最后发现是一个宏定义没开。
记住:LwIP移植不是写代码,而是搭积木。你把sys_arch和网卡驱动这两块积木搭好了,剩下的就是配置和调试了。下节课见。