LwIP 协议栈源码详解 ——TCP/IP 协议的实现(七:以太网数据接收)

来源:互联网 发布:酷云eye收视数据实时 编辑:程序博客网 时间:2024/05/16 05:56
6  以太网数据接收
少壮不努力,长大写程序。悲剧!
        昨天说到 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 地址和类型域
字段对应。
         在上面讨论的基础上,我们来看看这个数据包接收进程,源代码如下:
void ethernetif_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); 
E-mail:for_rest@foxmail.com    老衲五木出品
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 函数,该函数和底层硬件驱动密切相关,用于实现发送一
个数据包的功能。用户应该结合具体网卡驱动实现该函数。 
原创粉丝点击