前言
LWIP也是以分层的协议为参照来设计实现TCP/IP。LWIP从逻辑上看分为四层:链路层、网络层、传输层和应用层。
虽然 LWIP 也采用了分层机制,但它没有在各层之间进行严格的划分,各层协议之间可以进行或多或少的交叉存取,即上层可以意识到下层协议所使用的缓存处理机制。各层可以更有效的重用缓冲区。应用进程和协议栈可以使用相同的内存,应用可以直接读写内部缓存,节省了执行拷贝的开销。
链路层
在LWIP 中,是通过一个叫做 netif 的网络结构体来描述一个硬件网络接口的。源代码结构如下:
struct netif {
struct netif *next; // 指向下一个 netif 结构的指针
struct ip_addr ip_addr; // IP 地址相关配置
struct ip_addr netmask;
struct ip_addr gw;
err_t (* input)(struct pbuf *p, struct netif *inp); //调用这个函数可以从网卡上取得一个数据包
err_t (* output)(struct netif *netif, struct pbuf *p, // IP 层调用这个函数可以向网卡发送
struct ip_addr *ipaddr); // 一个数据包
err_t (* linkoutput)(struct netif *netif, struct pbuf *p); // ARP 模块调用这个函数向网
// 卡发送一个数据包
void *state; // 用户可以独立发挥该指针,用于指向用户关心的网卡信息
u8_t hwaddr_len; // 硬件地址长度,对于以太网就是 MAC 地址长度,为 6 各字节
u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; //MAC 地址
u16_t mtu; // 一次可以传送的最大字节数,对于以太网一般设为 1500
u8_t flags; // 网卡状态信息标志位
char name[2]; // 网络接口使用的设备驱动类型的种类
u8_t num; // 用来标示使用同种驱动类型的不同网络接口
};
- next字段是指向下一个netif结构的指针,一个产品可能会有多个网卡芯片,LWIP会把所有网卡芯片的结构体链成一个链表进行管理,有一个netif_list的全局变量指向该链表的头部,next字段就是用于链表用。
- ip_addr、 netmask、 gw 三个字段用于发送和处理数据包用,分别表示 IP 地址、子网掩码和网关地址。
- input字段指向一个函数,这个函数将网卡设备接收到的数据包提交给IP层,使用时将input指针指向该函数即可。
- output字段指向一个函数,这个函数和具体网络接口设备驱动密切相关,它用于IP层将一个数据包发送到网络接口上。用户需要根据实际网卡编写该函数,并将output字段指向该函数。
- linkoutput字段和上面的output基本上是起相同的作用,但是这个函数是在ARP模块中被调用的。实际上output字段函数的实现最终还是调用linkoutput字段函数将数据包发送出去。
- state字段可以指向用户关心的关于设备的一些信息。
示例程序
下面的例子是示范以太网网卡接口结构是如何被初始化的,以及数据包是如何接收和发送的。
static struct netif enc28j60; (1) // 声明一个netif结构的变量enc28j60
struct ip_addr ipaddr, netmask, gw; (2) // 声明了三个分别用于缓存IP地址、子网掩码和网关地址的变量
IP4_ADDR(&gw, 192,168,0,1); (3) // 分别对上述三个地址值得初始化
IP4_ADDR(&ipaddr, 192,168,0,60); (4)
IP4_ADDR(&netmask, 255,255,255,0); (5)
netif_init(); (6) // 只需初始化上面所述的全局变量netif_list即可,即netif_list = NULL。
netif_add(&enc28j60, &ipaddr, &netmask, &gw, NULL, ethernetif_init, tcpip_input); (7)
netif_set_default(&enc28j60); (8) // 调用netif_set_default函数初始化缺省网络接口
netif_set_up(&enc28j60); (9) // 调用函数netif_set_up使能网络接口,netif->flags |= NETIF_FLAG_UP;
需要强调的是对于(7),其作用是调用 netif_add 函数初始化变量 enc28j60,其中比较重要的两个参数是 ethernetif_init和 tcpip_input,前者是用户自己定义的底层接口初始化函数, tcpip_input 函数是向 IP 层递交数据包的函数,从前面的讲述中可以很明显的看出,该值会被传递给 enc28j60 的 input 字段。源码如下:
struct netif *
netif_add(struct netif *netif, struct ip_addr *ipaddr, struct ip_addr *netmask,
struct ip_addr *gw,
void *state,
err_t (* init)(struct netif *netif),
err_t (* input)(struct pbuf *p, struct netif *netif))
{
static u8_t netifnum = 0;
netif->ip_addr.addr = 0; //复位变量 enc28j60 中各字段的值
netif->netmask.addr = 0;
netif->gw.addr = 0;
netif->flags = 0; //该网卡不允许任何功能使能
netif->state = state; //指向用户关心的信息,这里为 NULL
netif->num = netifnum++; //设置 num 字段,
netif->input = input; //如前所诉, input 函数被赋值
netif_set_addr(netif, ipaddr, netmask, gw); //设置变量 enc28j60 的三个地址
if (init(netif) != ERR_OK) { //用户自己的底层接口初始化函数
return NULL;
}
netif->next = netif_list; //将初始化后的节点插入链表 netif_list
netif_list = netif; // netif_list 指向链表头
return netif;
}
上面的初始化函数调用了用户自己定义的底层接口初始化函数,在(7)中为ethernetif_init,其源代码为:
err_t ethernetif_init(struct netif *netif)
{
netif->name[0] = IFNAME0; //初始化变量 enc28j60 的 name 字段
netif->name[1] = IFNAME1; // IFNAME 在文件外定义的,这里不必关心它的具体值
netif->output = etharp_output; //IP 层发送数据包函数
netif->linkoutput = low_level_output; // //ARP 模块发送数据包函数
low_level_init(netif); //底层硬件初始化函数
return ERR_OK;
}
其中,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。
经过以上9个步骤,网卡初始化完成,能正常接收和发送数据包了。
网卡数据包的接收和发送
LWIP中实现了接收一个数据包和发送一个数据包函数的框架,这两个函数分别是low_level_input 和 low_level_output,用户需要使用实际网卡驱动程序完成这两个函数。
以太网网卡收到的数据包格式
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
数据包的接收
源代码如下:
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);
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);
注意事项:
- 使用查询方式实现的数据包接收进程其优先级必须低于系统中其他进程的优先级,否则会阻塞比它优先级低的进程的运行;
- htons函数的功能是将一个半字长的数据从网络字节顺序转换到我们的处理器支持的字节顺序。ARM处理器使用的是小端模式,接收到的网络字节数据用的是大端模式
- netif->input在结构enc28j60初始化时已经被设置为指向tcpip_input函数,所以实际上上面时调用tcpip_input函数往上层递交数据包。
数据包的发送
数据包的发送不必像数据包接收那样要使用一个专门的进程来实现,规则如下:
- 当上层有数据包要发送时,直接调用 netif->linkoutput 发送数据包就可以了。
- netif->linkoutput在结构enc28j60初始化时已经设置为指向low_level_output函数,该函数和底层硬件驱动密切相关,用于实现发送一个数据包的功能。