姓名:朱小鹏 学号:16010130023
转载:
http://blog.sina.com.cn/s/blog_62a85b950101am9n.html
【嵌牛导读】:low_level_init函数是与我们使用的与硬件密切相关初始化函数
【嵌牛鼻子】:以太网数据接收
【嵌牛提问】:LWIP是怎样来处理以太网数据接收?
【嵌牛正文】:
昨天说到low_level_init函数是与我们使用的与硬件密切相关初始化函数,看看:
static void low_level_init(struct netif *netif)
{
netif->hwaddr_len = ETHARP_HWADDR_LEN; //设置变量enc28j60的hwaddr_len字段
netif->hwaddr[0] = 'F';//初始化变量enc28j60的MAC地址
netif->hwaddr[1] = 'O';//设什么地址用户自由发挥吧,但是不要与其他
netif->hwaddr[2] = 'R';//网络设备的MAC地址重复。
netif->hwaddr[3] = 'E';
netif->hwaddr[4] = 'S';
netif->hwaddr[5] = 'T';
netif->mtu = 1500;//最大允许传输单元
//允许该网卡广播和ARP功能,并且该网卡允许有硬件链路连接
netif->flags= NETIF_FLAG_BROADCAST |\
NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
enc28j60_init(netif->hwaddr);//与底层驱动硬件驱动程序密切相关的硬件初始化函数
}
至此,终于变量enc28j60被初始化好了,而且它描述的网卡芯片enc28j60也被初始化好了,而且变量enc28j60也被链入链表netif_list。
接着上上上上面的语句(8)调用netif_set_default函数初始化缺省网络接口。协议栈除了有个netif_list全局变量指向netif网络接口结构的链表,还有个全局变量netif_default全局变量指向缺省的网络接口结构。当IP层有数据发送时,它首先会以netif_list为索引选择满足某个条件的网络接口发送数据包,但是,当找不到这样的接口时,协议栈就会调用缺省的网络接口直接发送数据包,所以(8)中的意思是把变量enc28j60描述的网络接口设置为缺省的网络接口。
(9)调用函数netif_set_up使能网络接口,这通过一个简单语句来实现:
netif->flags |= NETIF_FLAG_UP;
至此,网卡初始化完成,能正常接收和发送数据包了。下面我们来讨论讨论关于网卡数据包的接收和发送。
LWIP中实现了接收一个数据包和发送一个数据包函数的框架,这两个函数分别是low_level_input和low_level_output,用户需要使用实际网卡驱动程序完成这两个函数。在第一篇中讲过,一个典型的LWIP应用系统包括这样的三个进程:首先是上层应用程序进程,然后是LWIP协议栈进程,最后是底层硬件数据包接收进程。这里我们就来讲讲第三个进程,看看数据包是怎样被接收并往上层传递的。但在这之前,有必要说说以太网网卡所收到的数据包的格式。如下图,
LWIP使用了一个eth_hdr的数据结构来描述以太网数据包包头的14个字节。如下,
PACK_STRUCT_BEGIN
struct eth_hdr {
PACK_STRUCT_FIELD(struct eth_addr dest);//目标MAC地址
PACK_STRUCT_FIELD(struct eth_addr src);//源MAC地址
PACK_STRUCT_FIELD(u16_t type);//类型
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END
其中PACK_STRUCT_xxx都是与编译器字对齐相关的宏定义,这里不作详细介绍了。上面的dest、src和type三个字段分别和上图中的目的MAC地址、源MAC地址和类型域字段对应。
在上面讨论的基础上,我们来看看这个数据包接收进程,源代码如下:
voidethernetif_input(void *arg)//创建该进程时,要将某个网络接口结构的netif结构指
{//针作为参数传入
struct eth_hdr *ethhdr;
struct pbuf *p;
struct netif *netif = (struct netif *)arg;
while (1)
{
p = low_level_input (netif);//接收一个数据包
if (p == NULL)//如果数据包为空,
continue;//则循环结束,启动下次接收过程
ethhdr = p->payload;//取得数据包内数据
switch (htons(ethhdr->type))//判断数据包类型
{//只对IP数据包和ARP数据包进行处理
case ETHTYPE_IP://IP数据包
case ETHTYPE_ARP://ARP数据包
if (netif->input(p, netif)!=ERR_OK)//将数据包发送到上层应用函数
{
pbuf_free(p);
p = NULL;
}
break;
default:
pbuf_free(p);
p = NULL;
break;
}//switch
}//while
}//main函数
要创建上面的这个进程,需要把个网络接口结构的netif结构指针作为参数传入,在UC/OSII中要用到下面的语句实现,
OSTaskCreate(ethernetif_input,(void *)&enc28j60,
&T_ETHERNETIF_INPUT_STK[T_ETHERNETIF_INPUT_STKSIZE-1]
ETH_IF_TASK_PRIO);
在数据包接收进程中,有三个需要注意的地方。一是数据包接收的方法是查询方式,即处理器不断向网卡芯片中读取数据,如果读不到数据,则控制器会重新启动一个读取时序;如果能够成功读取到数据,则将数据通过网卡注册的input函数交往上层进行处理。使用查询方式实现的数据包接收进程其优先级必须低于系统中其他进程的优先级,否则它会阻塞比它优先级低的进程的运行。上面的程序有个可以改进的地方,即在读取到的数据包为空时,接收进程调用系统函数将自己延时一段时间再启动下一个读取过程,这样可以使其不能阻止优先级更低的进程的运行,缺点是数据包的接收得不到及时的响应。其实数据包的接收可以采用中断的方式来实现,这种方式是一种比较好的方式。一般的网卡芯片都有中断功能,即当网卡接收到一个数据包后,它可以产生中断信号告诉控制器自己接收到一个数据包。控制器此时启动一个读取数据包时序,就能有效的读取到非空数据包。所以可以这样来实现一个接收数据包进程:在无数据包收到时,数据包接收进程阻塞在一个信号量下,当有数据包到来时,网卡芯片产生一个中断信号,处理器进入中断处理,并释放一个信号量。中断退出后,数据包接收进程得到信号量,并从网卡芯片中读取数据包,并将数据包递交给上层进行处理。
第二个需要注意的地方是htons(ethhdr->type)函数的使用,htons函数的功能是将一个半字长的数据从网络字节顺序转换到我们的处理器支持的字节顺序。解释一下,在计算机体系结构和计算机通信领域中,对于半字、字等的存储机制有可能不同。目前通常采用的存储机制主要有两种:big-endian和little-endian,即大端和小端。对于大端模式,某个半字或字数据的高位字节被在内存的低地址端,低位字节排放在内存的高地址端。对于小端模式,则恰好相反。由于我们使用的ARM处理器使用的是小端模式,而接收到的网络字节数据用的是大端模式,所以这里调用函数htons实现大端与小端的转换,实际就是将两个字节交换顺序即可。这样调用htons(ethhdr->type)后,ethhdr->type的值就为0x0800或0x0806等。
最后需要注意的地方,netif->input在结构enc28j60初始化时已经被设置为指向tcpip_input函数,所以实际上上面是调用tcpip_input函数往上层递交数据包。tcpip_input属于IP层函数,从这里我们可以看出LWIP的一个很大的特点,即各层之间没有明显的界限划分。像前面所讲的那样,LWIP协议栈进程完成初始化相关工作后,会阻塞在一个邮箱上等待数据包的输入,这就对了,tcpip_input函数就是向这个邮箱发送一条消息,且该消息中包含了收到的数据包存储的地址。LWIP协议栈进程从邮箱中取到该地址后就可以对数据包进行处理了。
至此,数据包的接收可算大功告成,关于数据包的发送,这点很简单,因为它不必像数据包接收那样要使用一个专门的进程来实现,而是这样的:当上层有数据包要发送时,直接调用netif->linkoutput发送数据包就可以了。netif->linkoutput在结构enc28j60初始化时已经被设置为指向low_level_output函数,该函数和底层硬件驱动密切相关,用于实现发送一个数据包的功能。用户应该结合具体网卡驱动实现该函数。
网友评论