LWIP 移植和过程问题记录

网络LWIP协议使用心得记录整理

                                                                             20180227

      年前在STM32上使用两种PHY芯片对LWIP协议进行了移植通信,一个是STM32F4+ENC28J60+LWIP+RAWAPI使用固定IP地址作为TCP客户端与服务器进行局域网通信,另一个是STM32F746+LAN8742A+FREERTOS+LWIP+NetconnAPI使用DHCP作为TCP客户端与网络NTRIP服务器通信获取GPS差分数据。在实现过程中,经历了LWIP协议栈移植到PHY芯片,初始的链路PING不通,ping延迟很大,绑定不了服务器,向服务器长时间发送数据卡死,网线不能任意插拔,能和局域网通信却不能与网络服务器通信,网线状态的检测,DHCP在网线插拔时的异常,线程间CPU占用让出等等等等,虽然经历了一个月左右坎坎坷坷的都实现了相应的功能,但是感觉对LWIP网络这块还是有些模模糊糊,因此,写这篇心得进行下梳理记录。

这篇文章的框架是按照实际项目分成两个部分,一个是STM32F4+LWIP不带系统,另一个是STM32F7+LWIP+FREERTOS,大家可以对比这两个的区别,进而从大面上去理解LWIP,当然如果有什么不对或者不合适的还请大家讨论指正,共同学习,参考的例子是xiaoyan大神的帖子http://www.openedv.com/thread-25178-1-1.html。

不带操作系统的LWIP协议栈的实现:

实现的功能:

A.作为TCP客户端与客户的TCP服务器进行通信;

B.实现网线的任意插拔功能。

首先

1、LWIP协议栈的移植

      对于LWIP协议栈的介绍和由来就不做介绍,网上有很多,也不是我们关注的重点,因此直接上协议栈的移植。

       协议栈的移植的重点在ethernetif.c接口文件中的3个函数上,包括:low_level_init(), low_level_output(),low_level_input()三个函数,这3个函数对PHY的硬件和接收发送进行了初始化,不同的PHY芯片需要相应修改这部分内容,厦门大概说一下这3个函数的作用:

A.      Low_level_init(structnetif *netif)

网络开始初始化的时候,会首先调用ethernetif_init(struct netif *netif)这个函数用来注册建立网络接口,但是这个函数会调用Low_level_init(structnetif *netif)来完成实际的硬件初始化,包括网络的MAC地址,最大传输单元,然后再初始化PHY芯片ENC28J60,如果初始化失败则返回错误给上层调用的函数。

B.      Low_level_output(structnetif *netif , struct pbuf *p)

修改底层发送包的函数,在这个函数中将LWIP数据包中的缓冲区pbuf 复制到待发送的缓冲区lwip_buf中来,然后利用ENC28J60的发包函数发出去,如果是其他的PHY芯片,则需要修改这个函数中的PHY芯片发送函数,即:enc28j60_packet_send(send_len,lwip_buf)。

C.      Low_level_input(structnetif *netif)

这个过程就是把PHY芯片接收缓冲区lwip_buf中的数据复制到LWIP的pbuf中,如果收到了包,我们就可以在lwip_buf中读取数据了。注:lwip_buf作为接收和发送数据的缓冲。

对于移植的结构架次,可以分为3个层次,

对我们来说,是将LWIP协议栈移植到ENC28J60上,因此重点工作在ENC28J60的数据收发和协议栈结合上,至于协议栈里面不同的协议的实现,不是我们需要处理的重点。上面这个图就表述的很清楚,经PHY芯片的收发和协议栈底层的收发结合,其实协议栈移植就是数据收发链路的移植,这样就可以实现LWIP协议栈移植到不同PHY芯片上的工作。

2. 移植过程中遇到的问题小结

2.1ping 不通或延迟很大

很多情况下,发现底层链路ping不通,不要纠结,再回来找这部分吧,大部分情况下都是这里没有做好导致的。有时还会出现ping的延迟很大,发现要几十甚至几百毫秒才能返回,这个情况一般是链路通了,移植基本Ok了,但是PHY的接收数据是在轮训里面做的,如果把接收数据放在中断里面,就可以解决这个接收延迟的问题,我的就是这样解决的,而且也建议这样做。

2.2  不能绑定服务器

这个问题就比较尴尬了,绑定不上服务器主要是设置的本机的IP和服务器的地址没有在一个段里面,这个是个小白错误,记录下来留作纪念吧

2.3 向服务器长时间发送数据出现卡死现象

这个问题比较有价值。刚开始的时候百度啊查资料啊等等,发现都在说RAW API接口的弊端,这个问题就是其中之一。其实RAW API 这种方式在使用tcp_write时,它的本质是由一个数据包接收而触发的发送流程,所以,尽管在一直调用tcp_write进行发送,但它并没有发送出去,而是写到了tcp缓存里面,等到内核处理完了接收的数据包才会主动把缓存区中的数据拿出来发送。如果,在拿出来发送之前,一直调用tcp_write将往缓存里面谢书记,内存肯定会不够用,最后就会出现内存溢出的现象,这就是我在调试过程中遇到的发送一段时间后卡死现象。我的解决方式是:在调用tcp_write函数后,跟着调用tcp_output,直接将数据发送出去,不在等待内核处理完接收后再发送,这样以200hz的频率发送100字节左右,连续测试了70个小时左右,没有出现卡死现象。不过还是建议以后能用socket API的还是尽量别用RAW,这个API的本质就是回调,虽然思路简单,但是实现起来还是有点麻烦的。

2.4 网线不能任意插拔

这个问题是大家遇到比较多的热插拔问题。造成这个问题的主要原因是在拔掉网线后,底层链接断开,导致数据链路不通。这个问题一般有这么两种情况,一个是上电的时候没有插入网线,在一个是上电时插入了网线但是工作正常后有插拔动作。对于这两种情况的解决方法我是这么做的:上电初期初始化PHY芯片,实现对PHY的寄存器的读写,这样可以通过轮训的方式来不断检测网线连接状态,当检测到网线第一次插入或者已经插入后,开始初始化PHY芯片MAC和IP地址等,然后初始化协议栈等。实际上这个的思路就是什么时候检测到连接什么时候初始化。也有其他的思路是上电不管有没有网线连接都是直接初始化,后面检测到没有后再次初始化MAC自协商,我按照这个思路调试过,但是没有搞通,所以这个思路还需要再验证。对于正常工作后的插拔则比较好处理了,断开时关闭连接,释放相应占用的资源,连接时重新建立与服务器的连接实现通信即可。

2.5 DHCP和固定IP的区别

一般情况下,如果是和外网连接而不需要在局域网内进行数据通信的话,建议使用DHCP,虽然这样做需要建立一个DHCP的管理线程来对DHCP的状态进行管理,但是这样做的好处无疑是大大的,因为你不需要考虑网关,掩码等一些限制节点连接的因素,不需要在连到外网时随时修改IP等配置。当然使用固定IP连接外网也是可以的,还可通过一台设备来Ping一下链路通不通,只是需要注意将IP网关掩码等配置设置为和网络路由器一致。这两种方式在安全性上DHCP要相对安全一些,如果是由服务器发起的恶性连接的话,那没什么区别,服务器会向所有和它连接的客户端发起攻击。

2.6 线程间CPU占用的让出

这个就和网络这块关系不大了,是关于系统的一些东西。一般情况下,进程下的子线程之间会通过信号量或者邮箱等通信机制来进行线程的切换,一般情况下,不会使用延时等浪费CPU的操作。

2.7 带有FreeRTOS系统的任意插拔的实现思路

在调试带系统的网络功能时,插入了一个不带系统的项目,实际上不带系统的热插拔的实现要比有系统的早,所以FreeRTOS的热插拔的实现也使用了之前思路。在系统初始化完成后,在初始启动任务中,初始化PHY芯片(LAN8742A),但是不要初始化它的MAC,仅是初始化一些GPIO和SPI接口,可以通过读取它的寄存器得到网线连接状态就行。然后建立一个网线状态检测线程,每一段时间轮训一次网线状态,一旦检测到网线第一次插入,则初始化PHY MAC和LWIP协议栈,建立应用层线程(我的应用层是一个TCP客户端)。然后在正常工作后,对网线的插拔的异常再做些处理,如:拔掉网线时,关闭并删除当前的TCP连接,删除当前应用层线程等,插入网线时,新建TCP应用层线程等。

3. 关于网络的一些无章序的小白记录

3.1  组网才有IP地址

一台设备在不接入一个网络里面时,是不具有IP地址的,IP地址需要组网后才有,不管是局域网还是外网;

3.2 网线的交叉线和直连线的区别

先说明下这两种线的线序:

交叉线:又叫反线,线序一端是568A,一端是568B,并用RJ45水晶头夹好;

直连线:又叫正线或标准线,两端都用568B线序。

568A和568B两种线序:

标准568A线序:1-绿白,2-绿,3-橙白,4-蓝,5-蓝白,6-,7-棕白,8-棕;

标准568B线序:1-橙白,2-橙,3-绿白,4-蓝,5-蓝白,6-绿,7-棕白,8-棕;

直连和交叉网线的使用场景区分:

直通网线用来连接电脑和交换机,路由和交换机(或HUB);

交叉网线用来连接电脑和电脑,路由器和路由器的,交叉网线并不常用;

一个简单的区分办法:同种设备用交叉线,异种设备用直连线。

3.3 关于LWIP协议栈的3中API的大概区别

RAW API  是基于回调机制来实现应用层的功能,在初始化应用时,用户需要为不同的内核事件注册所需的回调函数,当相应事件发生时,LWIP会自发的调用相关的回调函数。这种API把协议栈和应用程序放在一个进程里面,显而易见的优点是发送和接收数据不再产生进程切换,但是缺点是应用程序不能使自己陷入长期的连续运算中,因为TCP/IP处理与连续运算是不能并行发生的,这很明显会导致通讯性能下降。当然,这个缺点可以通过把应用程序分成两部分来解决,一部分负责处理通讯,一部分处理运算。

LWIP API(NETCONN) 把接收与处理放在一个线程里面。这样只要处理流程被延迟,接收就会被阻塞,直接造成频繁丢包,响应不及时等问题。LWIP的作者用tcpip_input()函数来处理这个问题。这个函数将底层网络驱动组成的接收线程的数据包投递到mbox邮箱,投递结束后,接收线程继续下一个数据的接收,而被投递的IP数据包将由TCPIP线程继续处理。这样,即使某个IP数据包的处理时间过长也不会造成频繁丢包的情况。

BSD API (SOCKET),提供了基于 打开---读---写---关闭模型的UNIX标准API,它最大的特点是应用程序移植到其他系统时比较容易,但是在嵌入式系统中,socket的效率比较低,占用的资源比较多。

LWIP协议栈的作者推荐的是LWIP API即NETCONN。

3.4 关于交换机和路由器的区别

A. 交换机和路由器的区别有很多,但是主要的区别还是他们各自的主要功能

像交换机、集线器等,都是做端口扩展的,就是扩大局域网(通常都是以太网)的接入点,也就是能让局域网连接进来更多的电脑。交换机虽然也具有路由功能,但是它的路由功能通常比较简单,因为你它所面对的主要是简单的局域网的连接,路由路径远没有路由器那么复杂。它用在局域网中的主要用途还是提供快速数据交换功能,满足局域网数据交换频繁的特点。

路由器的主要功能是路由功能,更多的体现在不同类型的网络之间的互联上,如局域网和广域网的连接、不同协议的网络之间的连接等,所有路由器主要是用于不同类型的网络之间。它最主要的功能就是路由转发,解决好各种复杂路由路径网络的连接就是它的最终目的。所有路由器的路由功能很强大,不仅适用于同种协议的局域网间,更适用于不同协议的局域网与广域网间。

简单举个例子说明一下,家用交换机主要起到线路连通的功用,比如你家里有三台电脑,希望组建一个局域网,那么每台电脑拉出一根网线到交换机上,那么这三台电脑就组成了一个网,可以相互连通和共享文件。路由器呢,它也可以当普通交换机使用,具备交换机的线路连通的功能。但是路由器还有个功能交换机没有,那就是拨号上网功能。

B. 交换机工作在OSI参考模型的第二层(数据链路层),路由器在工作在第三层(网络层),这一区别决定了路由和交换机在移动信息的过程中需要使用不同的控制信息,所以说两者实现各种功能的方式是不同的。

3.5 LWIP 动态内存中的内存堆与内存池的区别

A. 内存堆分配策略原理是:

在一个事先定义好大小的内存块中进行管理,其内存分配的策略是采用最快合适(First Fit)方式,只要找到一个比请求内存大的空闲块,就从中切割出合适的块,并把剩余的部分返回到动态内存堆中。内存释放的过程是相反的,但是分配器会查看该内存块前后相邻的内存块是否空闲,如果空闲则会合并成一个大的内存空闲块。

这种策略的优点是内存浪费小,比较简单,适合用于小内存的管理,其缺点就是如果频繁的动态分配和释放,可能会造成严重的内存碎片,如果碎片情况严重的话,可能会导致内存分配不成功,对于动态内存的使用,推荐的方法是:分配——释放——分配——释放,这种方法能够减少内存碎片。

B. 动态内存池

内存池相当简单高效的一种分配策略,原理就类似我们去买鞋子,因为大家的脚无非就是这几种码数,所以厂商就先生产好确定码数的鞋子,比如这种球鞋厂商就生产39、40、41、42、43、44码,客户来买鞋子,直接试穿就可以买走了。所以直观的特点就是分配相当简单,相当快速。

设计目的

LWIP中存在很多固定的数据结构,这些结构的特点就是在使用之前就已经知道了数据结构的大小,而且这些是在使用的过程中不会发生大小改变的。比如在建立一个TCP连接的时候,LWIP需要使用一种叫做TCP控制块的数据结构,这种数据结构大小是固定的。所以为了满足这些数据类型分配的需要,在内存初始化的时候就建立了一定数量的动态内存池POOL。

原理探析

内存块就好像上面提到的鞋子,系统会根据用户的宏定义确定下初始化时需要预先生产确定数量和类型的内存块(就好像生产多少数量和类型的鞋子一样)。但是生产出来的内存块不能乱放,因为到时用户过来取内存块的时候你要很快的分配相应的内存块给用户。所以LWIP将相同类型的内存块放在一起,并用链表进行串起来,比如在初始化的时候用户确定下在使用的过程中,我大概会用10字节内存块3个,20字节内存块4个,30内存块2个,那么就会有如下组织示意图:

当用户正在过来说,我想要使用20字节内存块的时候,系统就会立马找到链表头2,直接将头两个已经初始化好的20字节内存块分配给用户,相当简单快捷。当用户使用完了释放内存块时,就会直接插入到队头就好。这就是动态内存池的逻辑结构。

动态内存堆和动态内存池的具体介绍与区别可以参考这个帖子:

https://blog.csdn.net/u012866052/article/details/53328134

3.6 CGI和SSI接口

 在做STM32的Web Server时,涉及到了STM32和网页进行数据交互的应用,应用的实现方式是使用CGI和SSI接口来实现数据交互。

CGI:公共网关接口(common gateway interface),在物理上是一段程序,运行在服务器上,提供同客户端HTML页面的接口。大部分的CGI接口被用来处理来自表单的输入信息,并在服务器产生相应的处理,CGI程序是网页具有交互功能。

SSI:服务器端嵌入(Server Side Include),是一种类似于ASP的基于服务器的网页制作技术。大多数的WEB服务器等均支持SSI命令。将内容发送到浏览器之前,可以使用SSI指令将文本、图形、或应用程序信息包含到网页中。例如,可以使用SSI包含时间/日期戳、版权声明等。

 

 

 

 

 

你可能感兴趣的:(LWIP)