lwip是一个轻型的网络协议栈,基本满足大部分应用,参考ucos的代码框架,lwip目录下arch/ucos-ii为ucos对于lwip的接口,这个接口是严格按照lwip的文档来做的;其他目录不需要修改任何文件,直接下载lwip代码即可,这样的代码可读性很强,非常的直观;
协议栈porting到ucos,主要涉及到:
lwip需要的任务接口,信号量接口,消息队列接口,时钟接口,这些wip官方的基于ucos的SDK,已经做了,主要功能就是和ucos的api做一个封装;
另外一方面就是lwip的底层硬件接口,也就是网卡驱动接口;
所以需要做的工作,一是lwip的初始化以及起lwip任务,二就是封装底层网卡驱动
1. lwip接口分析
SDK做了工作就是把ucos的API封装成lwip自己采用的API,下面就是lwip的API,包括任务,信号量,邮箱(类似消息队列),时钟,这些函数需要重新封装的,用ucos的API来实现,这些函数几乎和ucos的类似,区别的只是有一些参数的少许区别,arch/ucos-ii/sys_arch.c就是SDK的核心接口,用来实现封装,如果不是ucos,比如nucleus或者rtems等之类的,这个文件也是唯一需要更改的
#define sys_init()
#define sys_timeout(m,h,a)
#define sys_untimeout(m,a)
#define sys_sem_new(c) c
#define sys_sem_signal(s)
#define sys_sem_wait(s)
#define sys_sem_free(s)
#define sys_mbox_new() 0
#define sys_mbox_fetch(m,d)
#define sys_mbox_post(m,d)
#define sys_mbox_free(m)
#define sys_thread_new(t,a,p)
这里sys_timeout和sys_untimeout是定时器函数,sys_sem_wait和sys_mbox_fetch实现有点技巧的,因为lwip的增加定时函数,这就涉及到等待时间到后的处理,等待时间为0的时候持续等待,如果不为0,时间到后就就绪状态;
在创建lwip task的函数中维护每个一个指针数组,根据任务数目来的;这样每一个任务就维护一个定时事件链表,因为可能一个task中有几个定时事件,定时事件链表中就维护几个
struct timeoutlist {
struct sys_timeouts timeouts;
INT8U prio;
};
static struct timeoutlist timeoutlist[LWIP_MAX_TASKS];
sys_thread_t
sys_thread_new(void (* function)(void *arg), void *arg, int prio)
{
......
timeoutlist[sys_thread_no].timeouts.next = NULL;
timeoutlist[sys_thread_no].prio = prio;
++sys_thread_no; /* next task created will be one lower to this one */
......
}
sys_arch_timeouts返回值是对应任务的定时事件链表开始指针,默认创建的时候timeouts->next为空,所以第一个定时事件的timeout挂接后,增加一个定时事件就返回的
当第二个定时事件时执行时就要对定时事件排序了,当第二个定时时间短的话,第二个定时事件直接插入链首,并且next的时间取减去第一个和第二个定时值的时间差;第三个定时事件的时候,按照时间排序插到链表合适位置;这段代码处理算是蛮有技巧的,当然sys_untimeout是删除定时事件,过程刚好和创建相反,从链表中删除定时事件
timeouts = sys_arch_timeouts();
if (timeouts->next == NULL) {
timeouts->next = timeout;
return;
}
if (timeouts->next->time > msecs) {
timeouts->next->time -= msecs;
timeout->next = timeouts->next;
timeouts->next = timeout;
} else {
for(t = timeouts->next; t != NULL; t = t->next) {
timeout->time -= t->time;
if (t->next == NULL || t->next->time > timeout->time) {
if (t->next != NULL) {
t->next->time -= timeout->time;
}
timeout->next = t->next;
t->next = timeout;
break;
}
}
}
sys_mbox_fetch函数的实现也是非常的有技巧,将定时函数和消息队列一起处理,非常有意思的一代代码,参考下面注释
void
sys_mbox_fetch(sys_mbox_t mbox, void **msg)
{
u32_t time;
struct sys_timeouts *timeouts;
struct sys_timeout *tmptimeout;
sys_timeout_handler h;
void *arg;
again:
timeouts = sys_arch_timeouts();
if (!timeouts || !timeouts->next) {
sys_arch_mbox_fetch(mbox, msg, 0);
/* 没有定时事件最好了,直接无条件的持续等待消息 */
} else {
if (timeouts->next->time > 0) {
time = sys_arch_mbox_fetch(mbox, msg, timeouts->next->time);
/* 一个定时事件大于0,*/
} else {
time = SYS_ARCH_TIMEOUT;
/* 定时事件到,实际就是定时器函数可以跑了 */
}
if (time == SYS_ARCH_TIMEOUT) {
/* If time == SYS_ARCH_TIMEOUT, a timeout occured before a message
could be fetched. We should now call the timeout handler and
deallocate the memory allocated for the timeout. */
/**/
tmptimeout = timeouts->next;
timeouts->next = tmptimeout->next;
h = tmptimeout->h;
arg = tmptimeout->arg;
memp_free(MEMP_SYS_TIMEOUT, tmptimeout);
if (h != NULL) {
LWIP_DEBUGF(SYS_DEBUG, ("smf calling h=%p(%p)/n", (void *)h, (void *)arg));
h(arg);
/* 执行定时事件的函数 */
}
/* We try again to fetch a message from the mbox. */
/* 运行到这里:或者是定时函数的定时事件完成;或者是sys_arch_mbox_fetch超时没有收到消息,返回开始继续进入等待状态,和OSQPend超时没收到消息一样的处理 */
goto again;
} else {
/* If time != SYS_ARCH_TIMEOUT, a message was received before the timeout
occured. The time variable is set to the number of
milliseconds we waited for the message. */
/* 运行到这里是sys_arch_mbox_fetch在规定时间收到消息了,刷新时间参数 */
if (time <= timeouts->next->time) {
timeouts->next->time -= time;
} else {
timeouts->next->time = 0;
}
}
}
}
arch/ucos-ii/include/lwipopts.h主要是lwip的功能开关,函数的使能,类似ucos的os_cfg.h,默认的即可
2. 底层工作
底层网卡要工作起来,以及网卡数据接收丢给上层,上层丢数据给网卡,这个接口需要做,主要就是ucos-ii/netif的两个文件来实现的,还好lwip都提供了框架,自己只需要按照接口把函数填进去,下面是lwip提供的接口
err_t
ethernetif_init(struct netif *netif)
static void
ethernetif_input(struct netif *netif)
static err_t
ethernetif_output(struct netif *netif, struct pbuf *p,
struct ip_addr *ipaddr)
下面是网卡API,需要封装到lwip的API上,ethernetif_init调low_level_init用做网卡的初始化;中断到来的时候ne2k_isr调ethernetif_input(low_level_input)接收包最终丢给三层处理;ethernetif_output调low_level_output发送包
static void
low_level_init(struct netif *netif)
static struct pbuf *
low_level_input(struct netif *netif)
static err_t
low_level_output(struct netif *netif, struct pbuf *p)
/* void NICISR(void) interrupt */
void ne2k_isr(void)
下面是ethernetif_input收到ARP包etharp_arp_input或者IP包etharp_ip_input后的处理,之后就到lwip协议栈的工作了,API的工作到此为止
switch (htons(ethhdr->type)) {
case ETHTYPE_IP:
etharp_ip_input(netif, p);
pbuf_header(p, -14);
netif->input(p, netif);
break;
case ETHTYPE_ARP:
etharp_arp_input(netif, ethernetif->ethaddr, p);
break;
default:
pbuf_free(p);
p = NULL;
break;
}
这里驱动采用的SEND COMMAND方式(个人觉得比Remote DMA READ方式要简单)
初始化bnry读指针 = curr写指针 = PSTART
有数据包要读时,向DMA长度寄存器1写入0x0f(奇怪???不明白)
执行SEND COMMAND命令
然后网卡自动做了如下工作:
用当前的bnry读指针指向的地址写入DMA起址寄存器0,1
用以太网包头中的包长度初始化DMA长度寄存器0,1
先读18个字节的包头看看要不要这个包(4B的网卡物理状态信息,14B的以太网头)
从0x10端口读数据
可以看到这里处理和u-boot的轮询处理不太一样,是因为这里直接读以太网头出来了,就判断arp还是ip开始丢给协议栈处理了,u-boot那边是拿到整个包,在上层应用中再做处理的
3. lwip应用
lwip协议栈初始化及任务调用,按照lwip的例子代码,起一个task单独给lwip协议栈(当然实际上lwip协议栈内部另外再起了一些任务的),这个任务主要做一些协议栈,以及TCPIP的初始化之后,就添加硬件设备了,如下代码,这样的话,这个网卡就可以工作了;ping 192.168.18.119 -t,一切通信正常
//add loop interface
loop_netif = mem_malloc(sizeof(struct netif));
IP4_ADDR(&gw, 127,0,0,1);
IP4_ADDR(&ipaddr, 127,0,0,1);
IP4_ADDR(&netmask, 255,0,0,0);
netif_add(loop_netif, &ipaddr, &netmask, &gw, NULL, loopif_init, tcpip_input);
//add rtl8019 interface
rtl8019_netif = mem_malloc(sizeof(struct netif));
IP4_ADDR(&gw, 192,168,10,1);
IP4_ADDR(&ipaddr, 192,168,10,119);
IP4_ADDR(&netmask, 255,255,255,0);
netif_set_default(netif_add(rtl8019_netif, &ipaddr, &netmask, &gw, NULL, ethernetif_init, tcpip_input));
接着可以写一个小的socket代码测试协议栈的健壮性
4. lwip协议
这张图就是lwip协议栈的整个结构,网络层和传输层就是lwip做的工作,主要做包判断,填数据头,剥数据头,本身工作就是网络接口层的封装以及和底层驱动,当然还有就是黄色部分的mbox的os封装
5. 小结
编译就搞了好个晚上,这个SDK是基于lwip-1.3.0来实现的,我用的还是老的1.1.0,一些lwip的api参数不匹配了,不过还好,都是一些小问题
中断不建议边沿触发,建议电平触发,奇怪边沿触发经常无法进中断
lwip的定时函数代码加得有参考价值,移植到ucos是个非常不错的功能(ucos2 2.83增加了定时管理os_tmr.c 1)