Lwip中实现DM9000/DM9003驱动之一

本文详细介绍了Lwip协议栈的结构,重点解析了两个关键数据结构Netif和Ethernetif,以及数据的收发流程。在数据收发部分,阐述了从分层实现到数据流程的每个步骤,包括函数注册、接口初始化、数据发送和接收,以及中断处理。此外,还展示了模块间的关系图,揭示了Lwip如何与硬件驱动交互以实现数据的高效传输。
摘要由CSDN通过智能技术生成

目录

一:Lwip协议栈结构

二:两个重要的数据结构

1.Netif

2.Ethernetif

三:数据收发流程

1.分层的实现方式

2.数据流程

1.函数注册

2.接口初始化

3.数据发送

4.数据接收

5.中断处理

6.关系图


一:Lwip协议栈结构

Lwip是TCP/IP协议栈的实现,Lwip协议栈关注于减少内存的使用和代码尺寸,从而使Lwip使用于像嵌入式系统这样的资源非常有限的小客户端。为了减少对处理能力和内存的要求,Lwip使用一个不需要任何数据复制的剪裁后的API。关于Lwip的详细资料可以参考作者网站。

Lwip的结构可简单描述如下:

图1

上面是一个简单的3层的分层结构,其中API提供了应用层的编程接口,CORE实现了协议栈的核心,包括TCP/IP协议等,另外还包括对缓冲结构体pbuf的操作以及对网络层设备抽象结构体netif的操作。NETIF提供了网络层的接口,实现了ARP协议以及PPP协议,驱动也在这一层实现。

二:两个重要的数据结构

这里介绍两个重要的数据结构,对了解数据的收发流程很有帮助。

1.Netif

这是需要介绍的第一个数据结构。Netif完成了对网络设备的抽象,也就是代表了网络设备,系统通过该数据结构感知设备的存在。该数据结构的定义如下:(为了便于了解netif的结构,各个字段的放置位置可能不同于本身实际的定义)

   struct netif {

          /* 用于形成netif链,为支持多个网络设备提供了支持 */

          struct netif *next;

          /* 保存了设备的地址信息 */

          struct ip_addr ip_addr;

          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, struct ip_addr *ipaddr);

          err_t (* linkoutput)(struct netif *netif, struct pbuf *p);

          /* 用于保存状态信息,实际上提供了加入我们自定义结构体的接口 */

          void *state;

          /* 其他需要的信息 */

          struct dhcp *dhcp;

          u8_t hwaddr_len;

          u8_t hwaddr[NETIF_MAX_HWADDR_LEN];

          u16_t mtu;

          u8_t flags;

          u8_t link_type;

          char name[2];

          u8_t num;

};

对于上述结构体,我们重点关注如下的简化图:

图2

首先是next指针,一个设备对应一个netif,所有的设备通过next域连接起来,便于管理;

Input函数指针,注册一个数据输入函数,当驱动收到一个数据包时会调用该函数将数据传向协议栈上层;

Output函数指针,输出指针,同样注册一个数据输出函数,当上层要发送一个数据包时会调用该函数,通常,该函数由IP模块调用,此时还没有设置目的mac地址,所以还需要解决硬件地址问题,不能直接发送;

Linkoutput函数指针,不同于output,调用linkoutput函数之前,数据的目的mac地址已经通过ARP协议解决了,所以可以将该函数注册为实际的硬件发送函数,完成实际的数据发送。该函数通常由ARP模块调用;

State指针,该指针被定义为指向void类型,所以你可以将它指向你想指向的数据或者数据结构,这也是该数据项设置的本意,提供对netif的扩展。因为一个netif结构体虽然可以描述设备的一些共性,但是不能描述设备的所有特性,针对不同的具体应用环境,你可以定义自己的私有的数据并将其赋给state,这样就可以利用netif载体完成一些其它的工作。这里,我们将其指向ethrenetif结构体。关于ethernetif结构体在下面介绍。

2.Ethernetif

            

图3

Ethernetif最初的定义只包含以太网地址相关的数据项,在后来的修改中添加了两个新的数据结构net_device_stats(包含了一些状态信息)和pkt_index_desc(用于模拟Two-Packet-Mode方式的数据发送)。在netif部分已经介绍了stats指针指向了ethernetif,这样在数据收发过程中状态的改变可以通过stats指针进行更新,同时也可以完成其它的一些控制,比如已经实现的Two-Packet-Mode方式的数据发送,也许在以后还有新的扩展。

对于这两个数据结构,这里有个大概的了解即可,在后面的模块说明中会根据具体使用进行比较详细的介绍。

Note:虽然netif结构体完成了对设备的抽象,但是对netif的定义和操作是在CORE层中实现的,并非NETIF层(大写以示区分)。NETIF实现了ARP协议并提供了驱动程序的框架。

三:数据收发流程

1.分层的实现方式

就网络的实现结构来讲,它是分层的,又是模块化的。用户的数据要想发送到网络上,需要经过协议栈,网络设备以及接口电路等。这里有软件(协议栈)也有硬件(设备),他们是怎么联系起来的呢?驱动程序。驱动程序完成了软件到硬件的衔接,接口就是我们前面介绍过的netif结构体。它的input和output函数指针联系了驱动和协议栈。不过netif并不包含具体到数据,数据是包含在pbuf这个结构体中的,对于pbuf这里暂不介绍。以上描述可以通过下面的图来简单说明:

图4

需要注意的是这里的划分只是逻辑上的,但对于理解数据的收发这已经足够了。

2.数据流程

以上给出了数据的收发是如何与上下层联系的,为了实现完整的数据收发流程,还需要介绍一下初始化与中断。初始化,顾名思义,就是完成准备工作,在驱动层需要完成硬件地址的设置,寄存器的初始化,以及启动收发等操作(这是通过设置中断寄存器完成的)。详细的实现在下面的模块部分介绍,这里提及只是为了便于完整的说明问题。其次就是中断,数据的发送可以有应用主动发起,但数数据的接收就不行,它是一种被动的行为,因为系统不知道什么时候有数据到达,什么时候没有数据 。计算机利用中断机制来解决这一问题。当有数据到达时,引发一个中断,通过中断服务例程来完成数据的接收工作。通常解决方法有两种:其一是在中断服务程序中完成数据的接收处理并传给上层(一般不建议这么做,因为总是希望中断能够很快的返回);其二是在中断服务程序中发送一个信号量,来通知应用程序,具体的处理也就由应用来处理。这里采用的就是第二种方法。

综上,我们就完成了一个驱动程序的框架,枝干包括初始化、数据接收、数据发送以及中断处理。我们只要实现这四个模块,就完成了一个基本的驱动程序,它具有驱动程序的最基本的功能。

下面就按照这四条线简单描述整个数据的收发流程:(只列出主要调用)

1.函数注册

完成函数注册相当于完成了接口配置,之后系统根据接口数据结构就可以完成大部分的工作,比如上下层的数据交换。函数的注册流程如下图所示:

图5

2.接口初始化

根据上图,系统在添加新的网络接口时会注册初始化函数,并在稍后会直接调用初始化函数(这是唯一一个注册后就直接调用的函数,因为它只执行一次,并且实在最开始的时候)进行初始化工作,完成后会进行返回以表明是否成功的进行了初始化。

在netif_add中我们除了完成输入函数的注册外,会对netif结构体进行初步的设置,包括IP地址,子码掩码以及网关的设置。而在ethernetif_init中会进一步对netif进行设置,其中最重要的就是输出函数的注册(如上图)以及状态的初始化,并在这里将state指针指向了ethernetif结构体。所以我们对状态的设置只需在ethernetif结构体中进行就可以了。之后ethernetif_init会调用lwo_level_init。就像它的名字,它是用来进行更低层的初始化。按照lwip的描述,我们就可以将设备的初始化放在这里完成。但是为了实现最大限度的设备无关,进一步提出共性的操作,我们将对具体的设备的初始化放在了另一个文件中来实现(dm9003),并提供了初始化接口给该函数,就是dm9003_init。该函数的功能会在后面(驱动实现部分)详细介绍,这里只需知道它是用来对设备进行初始化的,就是完成设备寄存器的初始设置。既然将设备初始化提出去了,那么在low_level_init中做那些共性的工作呢?按照low_level_init最初的本意是要包括设备的初始化的,设备初始化过程中需要接口结构体提供的参数主要是本地的MAC地址,设备要发送数据帧需要知道自己的MAC地址,这是可以提前设置的。因此在调用设备初始化函数之前low_level_init对netif的硬件地址进行了设置,这是最重要的,并最终完成对netif的初始设置。只要我们将netif参数提供给设备初始化函数,它就可以知道自己的MAC地址。

综上,初始化的基本流程就是:

main()lwipinit()netif_ip_set()netif_add()ethernetif_init()low_level_init()dm9003_init()

3.数据发送

从上图可以看出,数据发送相关函数是在初始化函数中注册的。在这里注册了两个相关的输出函数,output和linkoutput。从命名上还是可以看出它们的一点区别的。不同于IP层的接收,IP层发送数据时拿到的数据是不全的,目的MAC地址还不知道,所以需要output注册的函数ethernetif_output来首先解决目的MAC地址。目的MAC地址通过ARP协议获得,所以ethernetif_output调用etharp_output解决目的MAC地址的问题。如果是广播或者组播包,etharp_output会直接调用linkoutput注册的函数low_level_output进行数据的实际发送;如果是单播数据包,etharp_output会先判断它是发送到本地LAN上还是出本地,然后调用etharp_query完成ARP表的查询和设置,并最终调用Low_level_output完成数据发送。在这一部分我们不需要详细了解ARP,只需要知道ethernetif_output调用ARP部分提供的接口完成ARP协议,low_level_output调用驱动提供的接口完成实际的数据发送。实际上low_level_output提供了数据发送的框架,我们只需要在适当的位置加上驱动层提供的接口就可以了。关于驱动层提供的接口会在驱动实现部分详细介绍。

综上,数据发送的基本流程为:

第一条线为(对于广播和组播数据包):上层模块调用netif->outputethernetif_outputetharp_outputnetif->linkoutputlow_level_output

第二条线为(对于本地和出本地的单播数据包):上层模块调用netif->outputethernetif_outputetharp_outputetharp_querynetif->linkoutputlow_level_output

4.数据接收

对于数据的接收是在任务的循环中实现的。因为何时接收数据提前是不知道的,所以需要中断来通知。数据接收任务和中断处理函数通过二值信号量同步(这里以及下面的中断处理部分我们只介绍它们各自的流程,关于它们的同步会在后面的关系图中详细的介绍),这也是我们在前面的中断部分介绍的一种比较好的解决方法。在完成lwip的初始化后,我们会创建一个任务recievepackageproc(接收单词似乎为receive),该任务就是用来完成数据接收的。任务在进入到下一次循环后首先会等待一个信号量,如果不能获得信号量,则表明没有数据收到,任务就会进入挂起状态,系统会重新进行任务的调度,不会影响系统的运行;如果能够获得信号量,则表明有数据收到,就继续执行,在这里是调用ethernetif_recv完成接收数据的处理。

Ethernetif_recv首先会调用netif_find找到当前的设备对应的netif结构体(其实我们当前就只有一个设备,但是netif的设计是能够实现netif链的,也就是能够对应多个设备),将其作为参数调用ethernetif_input进行进一步的数据接收工作。Ethernetif_input首先调用low_level_input将芯片接收并缓存的数据放到本地buffer也就是pbuf中。之后会解析pbuf中的数据,也就是将它看作一个有结构的实体。如果判断收到的数据包是IP包,则会调用ARP模块的etharp_ip_input对源地址是本地地址的包进行ARP Table的更新。之后去掉以太网头部,调用netif->input注册的函数tcpip_input将完整的IP包送给上层进行进一步的处理。Tcpip_input会把数据(包括去头的pbuf和netif)放进一个消息结构体,然后调用操作系统仿真层的消息发送函数将消息发送到邮箱。(会在关系图部分对消息邮箱的相关内容进行简单的介绍)。如果判断收到的数据是ARP数据包,会调用ARP模块的接口etharp_arp_input进行ARP Table的更新。如果以上二者都不是,则将数据包丢弃。类似于发送部分的low_level_output,low_level_input提供了数据接收的框架,我们同样只需要在适当的位置提供底层的适当的接口即可。

综上,数据接收的流程如下:

Lwipinitrecievepackageprocethernetif_recvethernetif_input

low_level_input && netif->inputtcpip_input

5.中断处理

按照芯片手册提供的信息,我们通过读取寄存器可以处理六类中断,我们主要关注其中的两种:一类是有数据接收到,另一类是发送完成。当中断发生时,会调用在网络IO初始化时注册的中断处理函数stpio_ethernetif_isr,在该函数中我们会调用ethernetif_isr判断是否是数据接收到了,如果是则触发信号量通知接收任务进行数据的接收,否则返回。我们完全可以在ethernetif_isr中完成中断状态寄存器内容的读取以及处理,但是这里我们将它放到了dm9003_isr中,而在ethernetif_isr中只是简单的调用了dm9003_isr。因为在ethernetif文件的其它部分我们都没有涉及到对底层设备寄存器的直接操作,换种说法就是我们都是调用驱动文件提供的接口函数完成工作,没有涉及到任何直接读写寄存器的函数,所以为了一致,将中断部分的实际操作也放到驱动文件中。

基本的调用关系如下:

产生中断stpio_ethernetif_isrethernetif_isrdm9003_isr

6.关系图

上面主要以独立的方式介绍了各个功能模块,虽然其间也描述了一些模块间的联系。下面给出一个宏观的关系图,描述模块之间的关系。下图不包括初始化部分。

图6

下面做一个简单的说明:当应用层要发送数据时,最终的数据会以特定的消息结构体的形式发给消息邮箱,这里的消息处理任务是tcpip_thread,在初始化协议栈时创建。Tcpip_thread是一个消息循环,循环读取邮箱中的消息,判断消息类型,如果是输入的IP包,会调用ip_input函数进入协议栈进行处理,也就是上图的向上处理部分。如果是要输出的数据,会调用api_msg_input进行向下的处理,在协议栈的底层最终会调用netif->output进行数据的发送,该过程就是上图的向下输出部分。对左边部分而言,当产生中断后会进行中断处理,此时中断注册函数stpio_ethernetif_isr会被调用。该函数会通过信号量与数据接收任务,也就是recievepackageproc同步。如果是接收到了数据,最终会调用到netif->input注册的输入函数tcpip_input,该函数将pbuf中的数据以及netif结构体组织成一个消息结构体发送到消息邮箱,进入右边部分的处理。

继续:Lwip中实现DM9000/DM9003驱动之二_龙赤子的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙赤子

你的小小鼓励助我翻山越岭

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值