写在前面
由于从这篇博客开始要涉及代码编写了,为此笔者自行画板搭建了一个实验平台,以后的所有代码与步骤都会在此实验平台上验证,目前所有的硬件与软件文件都在gitee上开源了,地址:https://gitee.com/water_zhang/enc28j60_arduino_shield_board。这块实验板兼容Arduino UNO R3接口,只要提供此接口的开发板都可以使用。笔者目前是将其与自己的stm32f412g_discovery开发板搭配使用,所以如果你手头也有这块开发板,项目中提供的代码可以直接编译后使用。不过编译环境估计就要费事点了,笔者使用的是arm-nono-eabi-gcc+make进行编译。
一、网络接口与网络数据包
1、网络接口结构体netif
LwIP作为一个TCP/IP协议栈的轻量级实现,其负责的范围,其实只到网络互联层,那么要怎么解决与下层(也就是数据链路层往下)的连接问题呢?为此LwIP提供了网络接口用于与下层网络进行对接。网络接口的主要实现位于netif文件夹中,LwiP对多种不同的底层网络接口提供支持, 当然这里我们重点关注的是需要移植的以太网(Ethernet)接口。
LwIP支持同时添加多个网络接口,其将这些网络接口串联成一个链表以方便管理,每个链表项代表一个网络接口,由一个名叫struct netif的结构体对网络接口进行描述,由于笔者只关注以太网接口与IPv4,这里只保留了与之相关的结构体成员,如下图所示:
要真正理解LwIP的网络接口,我们需要理解LwIP是如何通过网络接口与底层网络进行交互的。主要来说LwIP在与底层网络交互中,它主要需要关心两件事:如何将数据包下发到底层网络以及底层网络收到的数据包如何上传给LwIP。对于以太网来说,当LwIP希望下发数据包时,其会在网络接口链表中寻找最合适的那个,通过其结构体中的output与linkoutput函数指针指向的函数将数据包发送到底层网络;当底层网络接收到数据包时,其会通过调用自己对应网络接口结构体中的input函数指针将数据包上传给LwIP处理,具体的交互流程,如下图所示:
2、网络数据包pbuf
不同的网络接口其收发的数据包格式可能是不一的,为了统一数据包格式,LwIP要求所有接收的数据包均需要封装于pbuf结构中,同时交付给底层网络接口的报文数据也是封装于pbuf结构中。除了收发的数据外,pbuf结构体中还包含很多其他成员变量,用于指示数据包的一些信息。下图说明了pbuf结构体中各个成员的作用,为了方便对照,笔者翻译了中文注释同时保留了英文注释。
pbuf的结构体中包括了指向同样数据类型的next指针,说明有时一个数据包可能是存储在多个pbuf组成的链表里的,之所以要这样,是考虑到有时数据包的数据在内存中可能并不是处于连续地址上的(比如由于没有足够大的完整的内存空间,动态内存分配时使用两块不连续的内存空间存储一个数据包;也可能由于IP协议的分片机制,一个大数据包被切割成多个小数据包交付到协议栈,这时需要先存在pbuf链表中,等所有数据接收完成后再排序拼接)。同时payload这个指针指向实际的数据空间,这个空间LwIP在分配pbuf时可以一同分配,这时payload指向的空间在内存中紧跟着pbuf,当然其也可以指向任何可以访问的地址的其他数据,这些数据可能在RAM中,也可能在ROM中,甚至有可能在可寻址的外部SPI FLASH中。根据payload指向的数据所在的位置不同,pbuf被分为不同的类型,由其成员变量type_internal指示,pbuf_type这个枚举类用于枚举不同的pbuf类型,如下图所示:
可以看出,LwIP在设计pbuf结构时,给足了灵活性。下面给出各种类型的pbuf的例子。
二、以太网接口移植模板
1、总体介绍
对LwIP进行移植,最重要的工作便是构建一个netif结构体变量,这个变量会在LwIP初始化时传入LwIP作为LwIP与底层网卡驱动沟通的桥梁。为了辅助用户进行以太网网络接口的移植,LwIP提供了一个名为ethernetif.c的模板文件以简化移植难度。笔者在LwIP应用笔记(一):LwIP移植的一些预备知识的最后有简单介绍此文件中各个函数的作用。模板文件中主要实现了底层网卡以及其对应网络接口结构体变量的初始化函数以及与网络接口对接的底层收发函数,其实一张图片就能说明了,这张图在笔者第一篇博客中也给出过,为了方便查阅这里再插入一次。
在LwIP-1.4.1版本中,此文件是位于netif文件夹中,后续版本可能考虑到此文件只是个移植模板,严格来说并不是网络接口的一部分,故而在LwIP-2.1.2中被丢到了contrib-2.1.0文件夹中,contrib压缩包可以在LwIP项目网站下载得到,里面包含了很多LwIP的移植与使用示例。
2、模板使用前的修改
在模板文件里面,作者给出了一些建议进行的准备工作,其实就是通过替换ethernetif前缀的方式,给自己的网卡起个名字(当然可以同时把文件名也给改了,然后按需要补充一个头文件),然后有需要的话,补充一些自定义的结构体,下面是注释原文与翻译。
接下来看看模板里的各个函数都在干什么。
3、ethernetif_init函数
此函数在初始化LwIP时被调用,用于初始化以太网接口对应的netif结构体变量,同时对底层网卡进行初始化,我们只关注核心的部分就好,这里省略了IPv6和一些非必要的初始化语句,看下图就明白了:
4、ethernetif_input函数
这个函数主要是负责从网卡读取收到的以太网数据包,然后将其封装在pbuf中并提交给LwIP进行处理。我们应该在网卡收到数据包后调用这个函数(一般网卡都带有中断输出引脚,会在收到数据包时产生中断信号,可以在中断中设置处理标志位,然后在主线程中判断到标志位置位时调用此函数读取数据包并提交给LwIP处理),同样用一张图说明这个函数:
从上图可以看出,ethernetif_input函数最终将收到的数据包递交给了netif中的input函数指针指向的接收函数,对于以太网,这个函数一般为LwIP自带的ethernet_input函数,位于ethernet.c文件中,其将会通过判断以太网首部中的类型字段,将去除以太网首部后的协议报文提交给对应的协议处理函数进行处理,这里就不进一步说明了。其实netif中的input函数指针指向netif.c文件中的netif_input函数也可以,这个函数多做了一点事,就是根据netif中的flag标志位判断网卡类型,来决定是将数据包传递给ethernet_input函数还是直接传递给IP协议的ip_input宏定义对应的函数(对于IPv4,这个宏定义其实就是函数ip4_input,实现于ip4.c文件中)。个人感觉,调用netif_input可能是更严谨的做法。
三、工程文件添加
从这节开始,其实就正式开始移植工作了,其实个人认为前面那些内容记录LwIP的工作机制相关的内容才是最重要的,后面的内容只是按部就班而已,除了用来下次移植时方便照抄,其实也没什么其他价值。
首先是将LwIP相关文件添加到自己的工程中,如果使用的是arm-none-eabi-gcc配合make进行工程编译的话,这项工作会简单很多,因为在LwIP的src文件夹下存在一个名为Filelists.mk的文件,里面将LwIP的各种文件分门别类定义为了make可以识别的变量,我们要做的就是在自己的makefile中include这个文件,然后将需要的文件所在的变量添加到自己的源文件变量即可,除此之外还需要将src文件夹下的include文件夹添加到包含目录。这里是笔者自己所用makefile的添加例子,如下图所示,此makefile文件由stm32cubeMX自动化生成。
如果是使用keil或IAR等集成开发环境,可以对照Filelists.mk自己手动添加对应文件,这里就不多解释了。
四、配置LwIP
添加完文件后需要对LwIP进行配置,用户需要在LwIP的include文件夹下新建lwipopts.h文件用于添加用户自定义的配置,所有可以配置的项目都可以在include/lwip/opt.c这个文件中查阅,如果对应的配置项用户没有进行自定义,则会默认调用这其中的配置。里面有很多配置项,但是我们只需要配置自己需要的就好,下面给出笔者自己的配置例子:
同时,用户还需要在include/arch(这个文件夹不存在就新建)下新建cc.h文件,里面需要定义一些平台相关的数据类型,以及调试相关的一些宏定义,这里依然给出笔者自己的例子:
五、完善底层以太网收发接口
这一步其实就是补充我们在以太网移植模板一节提到的三个low_level开头的函数了,具体它们在整个LwIP工作中所扮演的角色,请参考前面的内容,我们这里只讨论我们要如何补充这三个函数(无聊的贴代码环节,毕竟也没什么复杂逻辑,注释足够说清了,没必要上流程图)。
1、准备工作
为了简化移植,我们需要可以预先定义两个缓冲数组(典型的空间换时间),一个用于暂存从网卡收到的报文,之后再丢到pbuf中给LwIP;另一个用于暂存从LwIP下发的pbuf中的内容,拼接好后再一股脑丢给网卡,定义如下;
2、low_level_init函数
还记得前面提及的ethernetif_init函数吗,这个函数会在初始化时被模板里的ethernetif_init函数在函数的最后调用,用来配置netif中与底层以太网网卡息息相关的成员变量以及调用网卡驱动的初始化函数初始化底层网卡。函数内容不多,看图吧。
3、low_level_output函数
这个函数最终会在初始化时被挂载在网络接口结构体netif的linkoutput函数指针上,用来作为MAC层的数据包输出函数。下面是笔者自己的实现,这里去除了一些LwIP用来记录链路层信息以及支撑SNMP协议(简单网络管理协议)的相关语句,其实保留下来的这些部分已经可以支撑这个函数正常工作了,但是如果是在做项目,还是不要随意删除自己没有完全了解的东西。
4、low_level_input函数
在前面的内容有说过,当网卡收到数据包时,用户应该调用ethernetif_input函数,此函数的职责是接收数据包,并将数据包封装进pbuf,最后将这些pbuf传给网络接口结构体netif中的input函数指针指向的处理函数。而low_level_input函数便会在ethernetif_input函数执行时被调用,其负责从网卡中读取数据包并封装到pbuf中。具体函数实现如下所示,同样这里也去除了一些LwIP用来记录链路层信息以及支撑SNMP协议(简单网络管理协议)的相关语句。
六、LwIP时钟安装
总算快要完成了,还记得我们在配置LwIP一节编写cc.h文件时,在里面给出的sys_now函数吗,我们需要去实现这个函数。此函数用于获取当前系统的嘀嗒计时时间,用于LwIP处理超时事件,同时TCP协议中的一些超时判断机制也会用到它。在存在RTOS的情况下,一般都会提供用于获取系统内滴答计时的函数,这时在sys_now中直接将滴答计时值换算为毫秒单位返回就行(其实一般也不需要换算,比如FreeRTOS一般滴答周期就是1毫秒一次)。在不存在操作系统的情况下,我们可能就需要自己去使用一个定时器来提供滴答时钟信号了。其实实现也很简单,配置一个溢出周期为1毫秒的定时器,在其中断中每次将一个32位计时变量自增1,需要时直接返回这个变量的值就行。由于笔者使用的是stm32的HAL库,直接依托Cortex-M3内核中的滴答时钟实现了获取滴答计时的函数,这里就直接调用了。
七、初始化LwIP
要让LwIP跑起来我们还差临门一脚,那就是初始化整个协议栈,并且以轮询或中断的方式定期将收到的数据包上传给LwIP处理。
1、初始化与添加网卡
前面的一系列步骤,虽然我们完成了网络接口的移植,但是要想让LwIP使用这个网络接口,我们还需要调用netif_add函数将这个网络接口注册进LwIP的网络接口管理链表里,具体的初始化流程和怎么注册网卡,看下面这张图。
这里netif_add这个函数值得重点说明一下。这个函数的原型以及说明参考下图:
2、接收数据包并传送给LwIP
一旦网卡收到了数据包,则应该调用我们之前移植模板中的ethernetif_input函数,这个函数会读取网卡中的数据包然后交给LwIP处理。我们可以使用网卡的接收中断,在中断中设置标志位然后主循环中检测标志位来决定是否调用ethernetif_input函数,或者干脆周期性调用此函数也可以。由于ethernetif_input的实现是一次只读取一条网络报文,这里笔者在移植时对此函数进行了一定修改,使得函数可以在接收到数据包时返回ERR_OK,这样我们就可以通过判断返回值的方式来判断网卡中是否还缓冲了更多的未处理的网络报文,如果有就不停调用ethernetif_input函数,直到所有网络报文处理完再接着往下执行其他代码,这样一定程度上可以提高报文处理的实时性。下面给出笔者的实现,这里使用的是轮询方式,会在主循环中周期性调用此函数处理网路报文。
3、sys_check_timeouts函数
当不存在操作系统时,我们还需要在主循环中周期性调用sys_check_timeouts函数用来处理LwIP的超时事件。以下给出笔者的主循环实现。
八、移植验证
成功移植并运行后,将开发板接入路由器,应该可以通过同一局域网下的PC机Ping通开发板,同时通过命令行的arp查询命令可以查询到之前移植时设置的MAC地址。
这里需要注意的是,设置IP地址时一定要注意与路由器处于统一网段,比如路由器网关为192.168.31.1,则网卡的网关地址也应该设置为192.168.31.1,同时网卡IP地址则设置为192.168.31.xxx。这些概念可以参考这篇博文:IP地址、子网掩码、网关、DNS的关系,LwiP毕竟只是一个协议栈的实现,了解TCP/IP协议栈本身,才能更好的用好它,这也是笔者目前在做的事。
移植过程中由于涉及到软硬件以及具体的网络环境,指望跟着一篇文章一步步做就可以轻松搞定是一件基本不可能的事,这时就只有发挥自己的主观能动性,多多查阅各种资料,同时借助Wireshark等网络抓包工具多多调试了,笔者自己第一次调通也是花了将近一周时间,写下这篇文章,希望多多少少能够发挥点用途吧。
完结!,最后附一张PING通的照片。