TCP/IP在51单片机上的实现特点和方法
李章林1 ,张立民1
( 1 南开大学信息技术科学学院, 天津 300071 )
摘要:为了实现51单片机接入internet,开发基于51单片机的TCP/IP具有重要意义。为此开发了zlIP,它是针对51单片机的特点使用KeilC51编程语言编写的TCP/IP,具有代码量小和兼容BSD套接字(socket)用户接口等特点。zlIP1.0版注重于运行速度,zlIP2.0版注重于用户接口的易用性,以从不同的角度试验在51单片机上实现TCP/IP的特点。通过比较两个版本的优缺点和吸收国内外其它TCP/IP的优点,分析了在单片机上实现TCP/IP的速度、程序大小、内存大小、编译器等特点,并针对这些特点总结和提出多种技巧和方法,并对这些技巧、方法的优缺点进行了分析。最后讲述了几点关键技术:设计清晰的TCP/IP和应用层的接口、采用前后台和多线程程序结构的比较,内存管理方法和防止多余的内存拷贝,实现数据包整序重发和窗口控制等。
关键词:TCP/IP;单片机;zlIP
中文分类号: 文献标识码:A 文章编号:1006-8740(2003)-00-0000-00
1 引言
随着网络应用的不断扩大,将各类电子设备接入Internet的需求越来越大。电子设备入网有多种解决方案:例如使用嵌入式系统,如使用ARM+Linux;一些实现TCP/IP的芯片也已经可以获得,例如Analog
Devices推出的Internet Modem(1);在51系列单片机运行TCP/IP协议栈等。前两种方案具有良好的性能,而在单片机上实现TCP/IP的方案具有很低的价格,在某些对网络速度要求不高的领域,有广阔的应用前景。
2 TCP/IP在单片机上实现的特点
2.1 速度慢
我们先来了解51单片机网络传输的极限速率。TCP/IP发送过程中主要的运算量集中在三个部分:应用程序将数据拷贝到RAM、计算TCP校验和、将RAM中的数据包拷贝到网络设备的发送缓冲区。对每一个字节数据,两次拷贝大致共使用12×2=24个指令周期;计算TCP校验和使用16个指令周期。采用12M的晶振,最高网络传输速度为25K字节/秒。实际上要比这个速度慢,zlIP第一版速度只有11.752K字节/秒。
为了提高速度可以采用快速的单片机比如Winbond公司的77E58或者AVR单片机,当然还可以提高晶振频率。除此之外还有:使用KeilC时,尽量避免使用Reentrant函数,Reentrant类型的函数比一般函数速度要慢很多,但是某些时候为了程序结构的需要必须使用Reentrant,这就需要在速度和结构之间作一个选择;指针使用“指定存储类型”的指针(memory-specific
pointer)(2);精简协议栈去除运算量大但是用处不大的功能,目前zlIP中TCP定时重发时间是固定的,也没有拥塞窗口控制,也没有IP层路由算法;防止数据包的不必要的拷贝;优化计算校验和和内存拷贝函数。
2.2 程序存储空间和外部RAM空间不大
通常TCP/IP协议栈需要大量的RAM来存储需要被应答的TCP包,如果规定时间内没有被应答则重发这个TCP包,被应答以后释放这个TCP包。
为了减小RAM使用量,能否不存储需要被应答的TCP包?(3)。当数据包需要重新发送时,如果能够重新产生数据包所需的数据,那时就可以不存储。例如存在于EEPROM中的html网页。但是这种方法存在以下两个缺点:一,TCPIP和应用层接口变得复杂。当需要重发时,必须需要应用层重新产生数据,实际上将TCP负责的重发机制转移到了应用层。应用层程序编写变得复杂。二,对数据无法重新产生的应用不适用。例如语音采集。
2.3编译器
TCP/IP一般采用C语言或者混合汇编来写。以KeilC516.0编译器为例。与X8086编辑的代码不同,使用KeilC要注意函数重入、指针、函数指针这三个问题。使用可重入函数和一般指针(generic
pointer)使得程序代码增大,运行速度变慢。使用函数指针时,要么需要手动重建调用树(Call tree),要么将通过函数指针调用的函数都设置为可重入函数。所以尽量少用重入函数、函数指针和一般指针。
3 zlIP的特点和实现技巧
3.1
特点
其它的TCP/IP有lwIP、uIP、ucIP、tinyTCP等。其中lwIP、uIP、tinyTCP已经成功地移植到了单片机。lwIP是专门为微处理器设计的TCP/IP协议栈,lwIP的功能很全面,但是相对来说代码较大,有人做过移植lwip+ucOSII代码量为60K(4)。uIP侧重于减小代码量(选择AVR为目标器件时,代码为5K左右)和减小RAM使用量(100字节左右)。uIP采用了不保存需要应答的数据包的RAM使用方案,没有和BSD的套接字接口兼容,应用层接口较复杂。zlIP介于uIP和lwIP之间,它针对单片机设计,有中等代码量和RAM使用量,使用套接字的应用层接口,所有的外部变量都使用了xdata类型,全部指针都为明确存储类型的指针,需要重入的函数已经声明为reentant,使用KeilC的小模式下编译。使用12M晶振、KeilC编译器、89C52单片下测试的技术参数如下:
表1:zlIP技术参数(Technical parameter of zlIP)
zlIP的版本 |
代码量(字节) |
外部RAM使用量(字节) |
发送速度(字节/秒) |
1.0 |
6791 |
20K |
11.752K |
2.0 |
14464 |
4K |
5.892K |
2.0版主要功能有:支持套接字形式的多个TCP连接。支持多个网络设备。支持通过网关发送数据包和数据包转发功能。响应ping命令。支持TCP包的整序、重发和窗口控制流量控制。
3.2 zlIP实现TCP/IP的技巧和方法
3.2.1设计套接字接口
zlIP接口函数基本和BSD的套接字接口相同。提供的用户接口函数有:
l
TCPSocket()。函数原型:socket
* TCPSocket(IP_ADDR ScrIP)。功能:申请一个套接字。ScrIP是这个套接字的本地IP地址。返回socket类型指针,如果申请失败返回NULL。
l
TCPConnect()。函数原型:BOOL
TCPConnect(socket * pTCB, IP_ADDR DestIP, WORD DestPort,void (* recv)(void *
buf,WORD size),void (* close)(socket * pSocket))。功能:向IP地址为DestIP的服务器的DestPort端口发起连接。参数recv和close用于设置当接收到数据包和对方要求关闭TCP连接时应该调用的回调函数指针。连接成功返回TRUE,否则返回FALSE。
l
TCPSend()。函数原型:BOOL
TCPSend(socket * pTCB,void *buf,WORD DataSize)。功能:发送数据。发送数据的TCP连接是套接字指针pTCB对应的连接,发送的数据的起始地址为buf,大小为DataSize。发送成功返回TRUE,否则返回FALSE。
l
TCPListen()。函数原型:BOOL
TCPListen(socket *pTCB,WORD ScrPort,void (* accept)(socket *pNewTCB)) 。功能:使用套接字pTCB在ScrPort端口监听。参数accept是当有客户端向这个监听端口连接成功时调用的回调函数指针。
l
TCPClose()。函数原型:void
TCPClose(socket *pTCB)。功能:我方主动关闭连接时调用TCPClose函数,它将要求关闭套接字pTCB对应的连接。TCPClose返回以后这个TCP连接可能保持,因为另一方还没有发起关闭请求。
l
TCPAbort()。函数原型:void
TCPAbort(socket *pTCB)。功能:当使用完这个套接字以后,调用TCPAbort,将这个套接字释放,还给系统。
TCP/IP协议运行中,接收数据包到达、另一方发起关闭连接、另一方向我方发起连接这些事件发生以后如何通知应用层?下面以收到数据包为例提供几种思路:(1)TCP/IP模块设置一个变量bRecv表征是否有数据包到达,应用层必须反复的查询这个变量,如果为TRUE,则调用一个接收函数接收这个数据包。但是这种方法增加了应用层程序的复杂性。(2)固定的回调函数。当TCP层接收到一个数据包后调用OnReceive(pTCB,buf,size)函数。用户必须在应用层定义一个函数名为OnReceive的函数。然后在OnReceive函数中处理接收的数据。(3)回调函数指针。每个套接字保存函数指针recv,接收到数据时TCP调用recv指向的函数。这样每个套接字可以独立定义接收函数,并且函数名可以任意。zlIP使用了第三种思路。它的回调函数指针有:
l
recv。原型为:void
(* recv)(void * buf,WORD size)。TCP接收到数据包时将调用这个函数。接收的数据的起始地址为buf,大小为size。
l
close。原型为:void (* close)(socket * pSocket)。TCP发现对方想关闭连接时调用这个函数。pSocket指出了是哪个连接。
l
accept。原型为:void (* accept)(socket *pNewTCB)。TCP发现另一方成功连接到我方某个端口时调用这个函数。pNewTCB是将要接管这个TCP连接的套接字指针。在accept()函数中还要设置pNewTCB的回调函数指针recv和close。
3.2.2 zlIP的输入输出流程简介
和其它的多数TCP/IP协议一样,zlIP采用了协议分层的结构。分为应用层、TCP层、IP层和网络设备接口层。图1描述了zlIP输入和输出数据包的流程以及需要调用的函数。输出时,TCP层先查看unsend队列,发现非空,将数据包插入队列;发现为空,则查看对方窗口是否够大能够接收这个数据包,然后填写TCP头部信息。IP层需要选择一个网络设备接口,选择的方法是:目的IP和该接口的子网掩码相与是否等于子网掩码。然后调用这个接口的Output函数来发送。zlIP提供了NetIfAdd()函数,可以动态添加网络设备接口。输入时,Timer()函数调用每个接口的Input函数。IP层判断IP版本、IP校验和、判断是否应该转发数据包,然后根据IP头部的protocol字段将包传给相应的高层处理。TCP层,需要判断TCP校验和,然后在现有的套接字中查找,判断是否有套接字可以接收这个数据包,判断TCP序号是否为希望的,然后更新这个连接的状态(包括释放被应答的数据包和TCP状态机的转化等),然后调用该套接字的回调函数recv。需要强调一下,如果接收的TCP的序号不在我方滑动窗口内,那么应该马上发送一个TCP应答包,因为这很可能是我方发送的应答包丢失了,我方接收的数据包是对方重发的TCP包。
3.2.3 单片机上实现TCP/IP的两种程序结构
从图1可以看到,右方有一个Timer()函数。它的一个功能是调用TCPTimer(),TCPTimer用于处理TCP数据包的重发等功能。另一个功能是调用每个接口的Input()函数接收到达的数据包。Timer()函数必须在短时间(一般20ms)内被调用一次,否则接收数据包和TCP定时等功能将停止。Timer()函数的调用有两种方式查询方式和中断方式,Timer()函数的不同调用方式决定了两种程序结构。
(1)前后台程序结构(5)
查询方式的调用对应前后台程序结构。实现方法是:设置一变量bTimerOut,在定时中断中将bTimerOut设置为真,应用层在程序流程中反复查询bTimerOut是否为真,真则调用Timer(),然后置bTimerOut为假。程序主流程必须是类似图2的形式:程序主流程是一个大循环,在循环中处理发送数据包等应用层协议同时查询bTimeOut。
缺点:由于Timer()必须在短时间内被反复调用,这就要求大循环循环一次的时间要在20ms以内。这给应用程序的编写带来了限制,例如有时程序可能需要在大循环中等待键盘按下,但是这里这种长时间的等待是不允许的。
(2)多线程程序结构
另一种方案是使用多线程。Timer()函数会自动地每隔20ms被调用一次。实现多线程有两种方法:①
在单片机的定时中断中调用Timer函数;② 使用操作系统。
缺点:多线程程序结构解决了前后台程序的缺点。应用程序再也不用套用固定的程序格式。但是,这是有代价的。使用多线程,这就意味着某些函数可能被重入,这些函数必须定义为reentrant类型,从而降低了运行速度。
多线程结构还要注意网络设备驱动函数被重入的问题。以NE2K的以太网卡驱动为例,拷贝数据包到网卡缓存前要先设置寄存器(例如起始地址),然后开始拷贝。如果设置完寄存器以后中断发生,并且驱动函数被重入,那么寄存器的设置被修改,中断返回以后拷贝将出错。可以使用禁止中断、全局标志位、信号量等方法防止重入。
3.2.4内存管理方法和无多余数据包拷贝的实现
TCP/IP的内存的管理方法这里介绍两种:分页方法和链表方法。
(1)分页方法(6):内存划分为多个128字节大小的小页和少量1536字节大小的大页。一个页分配给一个数据包。用一个数组memFlag记录各个内存页是否已经被分配。分配内存的时候只要查找数组membFlag,以获得一个空闲的内存页。为了提高查找的效率,可以将每次查找的起始页设为上次找到的空闲页的下一个页。释放时,将memFlag相应的元素置为FALSE。在协议层之间传送数据包只要传送页的序号就可以了。这种内存管理方法,分配和释放内存的速度较快。但是由于页的大小固定,不能和数据包大小相适应,造成内存的浪费。
(2)链表方法:链表方法根据数据包大小分配相应大小的内存块。如图3所示,链表将内存块链接起来,used字段表示该内存块是否正在使用,pSstart和pEend表示数据部分有效数据的开始地址和结束地址。分配时,搜索内存链表找到一个没有分配的比所需空间大的内存块,截取所需的大小。该内存块被截取以后可能还有较多剩余,这时将剩余部分从原内存块中分离出来,成为一个新的内存块,并插入链表。释放时,将used置为假,如果pNext或者pPre指向的内存块也是空闲的,将其和自己合并,以防止内存分片(7)。在协议层之间传送数据包只要传送内存块的起始地址就可以了。这种内存管理方法空间浪费小但是运算量相对较大。
无数据包拷贝是指除了获得数据到RAM和数据包到网络设备发送缓存这两次拷贝外没有数据包的拷贝。这节省了拷贝时间。介绍两种实现方法:
(1)链表方式:例如当应用层将DataSize大小的应用层数据交给TCP层发送,一般的做法是申请一个DataSize+TCPHeadSize大小的内存然后填写TCP头部,并将数据包拷贝到TCP的载荷中。使用链表方式:TCP层只申请TCPHeadSize大小的内存,然后将这个TCPHead用链表连接到应用层数据。这种方式缺点是:同一个数据包的内存不连续,这加大了计算校验和内存释放的复杂度,运算量大。
(2)预留空间方式:应用层为DataSize大小的数据包申请内存的时候,实际申请的是DataSize+AllHeadSize,其中AllHeadSize表示所有协议头部大小总和。拷贝应用层数据时在其前面留出AllHeadSize大小的空余空间。内存块头部的pStart指示了程序所在层的有效数据的开始,例如在应用层时指向应用层数据包的开始地址。应用层将这个数据包传给TCP层以后,TCP层只要在pStart-TCPHeadSize开始的内存空间加一个TCP头部即可。这种方式运算量很小,但是应用层必须事先知道其底层的协议头大小之和,违反了下层协议和上层无关的要求。
3.2.5如何实现整序、重发和窗口控制
zlIP使用了队列缓存的方式来实现。这里队列的一个元素指向一个数据包,队列的最大长度没有限制。对于整序,使用ooSeq队列(7),如果发现接收的TCP包序号并不是希望的,但是序号在接收窗口内,此时我们不能立刻接收这个包也不应丢弃,先将这个包放入ooSeq队列。每当,一个希望的TCP包被接收以后,再查看ooSeq队列现在是否有TCP包成为了希望的数据包,如果有则将其取出并处理。对于重发,使用unacked队列,每一个需要被应答的TCP数据包发送以后都要放入unacked队列,等到被应答以后才从队列中删除。TCP重发定时只针对unacked队列第一个TCP包,如果定时超出,重新发送,重发次数超出规定值,则报错。对于窗口控制,使用unsend队列,如果发现对方的窗口过小无法接收这个数据包,则只发送部分数据,将多余部分放入unsend队列,等待对方发来TCP包通知新的窗口大小时,再次判断是否可以发送了。如果在unsend队列不为空的情况下,我方应用层传来需要发送的数据包都应插入unsend队列。我方的TCP窗口的大小就是剩余内存空间的大小。
3.2.6 捎带应答的实现
捎带应答指的是,当对方一个需要应答的TCP包到达时,我方不马上给予应答,而是等待一个较短的时间。如果在这段时间内,我方有数据发送,则会捎带给予了应答,这减少了包的发送数量。
参考文献
[1] (电子文献)中国电子网.ADI具有TCP/IP栈的单片Modem[Z].http://www.21ic.com.2000-10-11.
[2] (电子文献)德国Keil公司.Cx51 Compiler[Z].http://www.keil.com.2001-5.103-110.
[3] (电子文献)Adam Dunkels.uIP - A Free
Small TCP/IP Stack[Z].http://dunkels.com/adam/uip/index.html.2002-1-15.1
[4] (电子文献)Adam Dunkels.lwIP - News
Archive[Z].http://www.sics.se/~adam/lwip/news.html.2001-1-9.
[5] (专著)jean j labrosse.μc/os-II-源码公开的实时嵌入式操作系统.邵贝贝等译.[M]北京:中国电力出版社,2001.
29-30.
[6] (专著)Douglas E Comer,
David L stevens.用TCP/IP进行网际互连第二卷[M]北京:电子工业出版社 ,2000.24-25.
[7] (电子文献)Adam Dunkels.Design and
Implementation of the lwIP TCP/IP Stack[Z].http://www.sics.se/~adam/lwip/
documentation.html.2001-2-20.10-19.
了解单片机TCP/IP更多方案:http://www.zlmcu.com/products_serial_server.htm
The specialty and
method in implementation
of TCP/IP in 51 serial MCU
Li Zhanglin1 ,Zhang Limin1
(1
ABSTRACT:In order to connect a 51 serial MCU to internet, it’s
necessary to develop a TCP/IP especially for 51 serial MCU. So We developed
zlIP, a TCP/IP designed especially for 51 serial MCU with KeilC51. Its
specialties include small code size, compatibility with BSD socket interface
etc. In order to test different aspects in implementation TCP/IP on 51, zlIP
1.0 emphasized on speed while zlIP 2.0 emphasized on facility of user
interface. The thesis analyzed specialties, which include speed, code size, ram
usage and complier, by comparing the tow editions of zlIP and absorbing the
strong points of other TCPIP. The thesis promoted and summarized some methods according
to these specialties and compared these methods. Finally, the thesis analyzed
some key techniques: TCP/IP user interface design, comparison of back-front and
multithread programming structure, memory management and avoidance of redundant
copy of packets, realization of packets arrangement, resending and window
control.
Key word: TCPIP;
MCU; zlIP