<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title><![CDATA[向东博客 专注WEB应用 构架之美 --- 构架之美，在于尽态极妍 | 应用之美，在于药到病除]]></title> 
<link>https://jackxiang.com/index.php</link> 
<description><![CDATA[赢在IT，Playin' with IT,Focus on Killer Application,Marketing Meets Technology.]]></description> 
<language>zh-cn</language> 
<copyright><![CDATA[向东博客 专注WEB应用 构架之美 --- 构架之美，在于尽态极妍 | 应用之美，在于药到病除]]></copyright>
<item>
<link>https://jackxiang.com/post//</link>
<title><![CDATA[[转载]TCP/IP在51单片机上的实现特点和方法]]></title> 
<author>jack &lt;xdy108@126.com&gt;</author>
<category><![CDATA[WEB2.0]]></category>
<pubDate>Sat, 17 Oct 2009 05:32:50 +0000</pubDate> 
<guid>https://jackxiang.com/post//</guid> 
<description>
<![CDATA[ 
	http://b.cnc.qzone.qq.com/cgi-bin/blognew/blog_output_data?uin=40317647&blogid=1228464038&imgdm=imgcache.qq.com&bdm=b.cnc.qzone.qq.com&mode=1&numperpage=15&blogseed=0.05020814931591866&property=GoRE&timestamp=1255757003<br/><br/><br/>摘要：为了实现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和应用层的接口、采用前后台和多线程程序结构的比较，内存管理方法和防止多余的内存拷贝，实现数据包整序重发和窗口控制等。 <br/>关键词：TCP/IP；单片机；zlIP <br/>1 引言&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <br/>&nbsp;&nbsp;&nbsp;&nbsp;随着网络应用的不断扩大，将各类电子设备接入Internet的需求越来越大。电子设备入网有多种解决方案：例如使用嵌入式系统，如使用ARM＋Linux；一些实现TCP/IP的芯片也已经可以获得，例如Analog Devices推出的Internet Modem（1）；在51系列单片机运行TCP/IP协议栈等。前两种方案具有良好的性能，而在单片机上实现TCP/IP的方案具有很低的价格，在某些对网络速度要求不高的领域，有广阔的应用前景。 <br/>2 TCP/IP在单片机上实现的特点 <br/>2.1 速度慢 <br/>我们先来了解51单片机网络传输的极限速率。TCP/IP发送过程中主要的运算量集中在三个部分：应用程序将数据拷贝到RAM、计算TCP校验和、将RAM中的数据包拷贝到网络设备的发送缓冲区。对每一个字节数据，两次拷贝大致共使用12×2＝24个指令周期；计算TCP校验和使用16个指令周期。采用12M的晶振，最高网络传输速度为25K字节/秒。实际上要比这个速度慢，zlIP第一版速度只有11.752K字节/秒。 <br/>为了提高速度可以采用快速的单片机比如Winbond公司的77E58或者AVR单片机，当然还可以提高晶振频率。除此之外还有：使用KeilC时，尽量避免使用Reentrant函数，Reentrant类型的函数比一般函数速度要慢很多，但是某些时候为了程序结构的需要必须使用Reentrant，这就需要在速度和结构之间作一个选择；指针使用“指定存储类型”的指针（memory-specific pointer）(2)；精简协议栈去除运算量大但是用处不大的功能，目前zlIP中TCP定时重发时间是固定的，也没有拥塞窗口控制，也没有IP层路由算法；防止数据包的不必要的拷贝；优化计算校验和和内存拷贝函数。 <br/>2.2 程序存储空间和外部RAM空间不大 <br/>&nbsp;&nbsp;&nbsp;&nbsp;通常TCP/IP协议栈需要大量的RAM来存储需要被应答的TCP包，如果规定时间内没有被应答则重发这个TCP包，被应答以后释放这个TCP包。 <br/>&nbsp;&nbsp;&nbsp;&nbsp;为了减小RAM使用量，能否不存储需要被应答的TCP包？（3）。当数据包需要重新发送时，如果能够重新产生数据包所需的数据，那时就可以不存储。例如存在于EEPROM中的html网页。但是这种方法存在以下两个缺点：一，TCPIP和应用层接口变得复杂。当需要重发时，必须需要应用层重新产生数据，实际上将TCP负责的重发机制转移到了应用层。应用层程序编写变得复杂。二，对数据无法重新产生的应用不适用。例如语音采集。 <br/>2.3编译器 <br/>&nbsp;&nbsp;&nbsp;&nbsp;TCP/IP一般采用C语言或者混合汇编来写。以KeilC516.0编译器为例。与X8086编辑的代码不同，使用KeilC要注意函数重入、指针、函数指针这三个问题。使用可重入函数和一般指针（generic pointer）使得程序代码增大，运行速度变慢。使用函数指针时，要么需要手动重建调用树(Call tree)，要么将通过函数指针调用的函数都设置为可重入函数。所以尽量少用重入函数、函数指针和一般指针。 <br/>3 zlIP的特点和实现技巧 <br/>3.1 特点 <br/>&nbsp;&nbsp;&nbsp;&nbsp;其它的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单片下测试的技术参数如下： <br/> <br/><br/>2.0版主要功能有：支持套接字形式的多个TCP连接。支持多个网络设备。支持通过网关发送数据包和数据包转发功能。响应ping命令。支持TCP包的整序、重发和窗口控制流量控制。 <br/>3.2 zlIP实现TCP/IP的技巧和方法 <br/>3.2.1设计套接字接口 <br/>&nbsp;&nbsp;&nbsp;&nbsp;zlIP接口函数基本和BSD的套接字接口相同。提供的用户接口函数有： <br/>*TCPSocket()。函数原型：socket * TCPSocket(IP_ADDR ScrIP)。功能：申请一个套接字。ScrIP是这个套接字的本地IP地址。返回socket类型指针，如果申请失败返回NULL。 <br/>&nbsp;&nbsp;*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。 <br/>&nbsp;&nbsp;*TCPSend()。函数原型：BOOL TCPSend(socket * pTCB,void *buf,WORD DataSize)。功能：发送数据。发送数据的TCP连接是套接字指针pTCB对应的连接，发送的数据的起始地址为buf，大小为DataSize。发送成功返回TRUE，否则返回FALSE。 <br/>&nbsp;&nbsp;*TCPListen()。函数原型：BOOL TCPListen(socket *pTCB,WORD ScrPort,void (* accept)(socket *pNewTCB)) 。功能：使用套接字pTCB在ScrPort端口监听。参数accept是当有客户端向这个监听端口连接成功时调用的回调函数指针。 <br/>&nbsp;&nbsp;*TCPClose()。函数原型：void TCPClose(socket *pTCB)。功能：我方主动关闭连接时调用TCPClose函数，它将要求关闭套接字pTCB对应的连接。TCPClose返回以后这个TCP连接可能保持，因为另一方还没有发起关闭请求。 <br/>&nbsp;&nbsp;*TCPAbort()。函数原型：void TCPAbort(socket *pTCB)。功能：当使用完这个套接字以后，调用TCPAbort，将这个套接字释放，还给系统。 <br/>TCP/IP协议运行中，接收数据包到达、另一方发起关闭连接、另一方向我方发起连接这些事件发生以后如何通知应用层？下面以收到数据包为例提供几种思路：(1)TCP/IP模块设置一个变量bRecv表征是否有数据包到达，应用层必须反复的查询这个变量，如果为TRUE，则调用一个接收函数接收这个数据包。但是这种方法增加了应用层程序的复杂性。(2)固定的回调函数。当TCP层接收到一个数据包后调用OnReceive(pTCB,buf,size)函数。用户必须在应用层定义一个函数名为OnReceive的函数。然后在OnReceive函数中处理接收的数据。(3)回调函数指针。每个套接字保存函数指针recv，接收到数据时TCP调用recv指向的函数。这样每个套接字可以独立定义接收函数，并且函数名可以任意。zlIP使用了第三种思路。它的回调函数指针有： <br/>&nbsp;&nbsp;*recv。原型为：void (* recv)(void * buf,WORD size)。TCP接收到数据包时将调用这个函数。接收的数据的起始地址为buf，大小为size。 <br/>&nbsp;&nbsp;*close。原型为：void (* close)(socket * pSocket)。TCP发现对方想关闭连接时调用这个函数。pSocket指出了是哪个连接。 <br/>accept。原型为：void (* accept)(socket *pNewTCB)。TCP发现另一方成功连接到我方某个端口时调用这个函数。pNewTCB是将要接管这个TCP连接的套接字指针。在accept()函数中还要设置pNewTCB的回调函数指针recv和close。 <br/>3.2.2 zlIP的输入输出流程简介 <br/>&nbsp;&nbsp;&nbsp;&nbsp;和其它的多数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包。 <br/>3.2.3 单片机上实现TCP/IP的两种程序结构 <br/> <br/>&nbsp;&nbsp;&nbsp;&nbsp;从图1可以看到，右方有一个Timer（）函数。它的一个功能是调用TCPTimer（），TCPTimer用于处理TCP数据包的重发等功能。另一个功能是调用每个接口的Input（）函数接收到达的数据包。Timer（）函数必须在短时间（一般20ms）内被调用一次，否则接收数据包和TCP定时等功能将停止。Timer（）函数的调用有两种方式查询方式和中断方式，Timer（）函数的不同调用方式决定了两种程序结构。 <br/>(1)前后台程序结构(5) <br/>&nbsp;&nbsp; 查询方式的调用对应前后台程序结构。实现方法是：设置一变量bTimerOut，在定时中断中将bTimerOut设置为真，应用层在程序流程中反复查询bTimerOut是否为真，真则调用Timer（），然后置bTimerOut为假。程序主流程必须是类似图2的形式：程序主流程是一个大循环，在循环中处理发送数据包等应用层协议同时查询bTimeOut。 <br/>缺点：由于Timer（）必须在短时间内被反复调用，这就要求大循环循环一次的时间要在20ms以内。这给应用程序的编写带来了限制，例如有时程序可能需要在大循环中等待键盘按下，但是这里这种长时间的等待是不允许的。 <br/>(2)多线程程序结构 <br/>&nbsp;&nbsp;&nbsp;&nbsp;另一种方案是使用多线程。Timer（）函数会自动地每隔20ms被调用一次。实现多线程有两种方法：① 在单片机的定时中断中调用Timer函数；② 使用操作系统。 <br/>&nbsp;&nbsp;&nbsp;&nbsp;缺点：多线程程序结构解决了前后台程序的缺点。应用程序再也不用套用固定的程序格式。但是，这是有代价的。使用多线程，这就意味着某些函数可能被重入，这些函数必须定义为reentrant类型，从而降低了运行速度。 <br/>&nbsp;&nbsp;&nbsp;&nbsp;多线程结构还要注意网络设备驱动函数被重入的问题。以NE2K的以太网卡驱动为例，拷贝数据包到网卡缓存前要先设置寄存器（例如起始地址），然后开始拷贝。如果设置完寄存器以后中断发生，并且驱动函数被重入，那么寄存器的设置被修改，中断返回以后拷贝将出错。可以使用禁止中断、全局标志位、信号量等方法防止重入。 <br/>3.2.4内存管理方法和无多余数据包拷贝的实现 <br/>&nbsp;&nbsp;&nbsp;&nbsp;TCP/IP的内存的管理方法这里介绍两种：分页方法和链表方法。 <br/>(1)分页方法(6)：内存划分为多个128字节大小的小页和少量1536字节大小的大页。一个页分配给一个数据包。用一个数组memFlag记录各个内存页是否已经被分配。分配内存的时候只要查找数组membFlag，以获得一个空闲的内存页。为了提高查找的效率，可以将每次查找的起始页设为上次找到的空闲页的下一个页。释放时，将memFlag相应的元素置为FALSE。在协议层之间传送数据包只要传送页的序号就可以了。这种内存管理方法，分配和释放内存的速度较快。但是由于页的大小固定，不能和数据包大小相适应，造成内存的浪费。<br/><br/>(2)链表方法：链表方法根据数据包大小分配相应大小的内存块。如图3所示，链表将内存块链接起来，used字段表示该内存块是否正在使用，pSstart和pEend表示数据部分有效数据的开始地址和结束地址。分配时，搜索内存链表找到一个没有分配的比所需空间大的内存块，截取所需的大小。该内存块被截取以后可能还有较多剩余，这时将剩余部分从原内存块中分离出来，成为一个新的内存块，并插入链表。释放时，将used置为假，如果pNext或者pPre指向的内存块也是空闲的，将其和自己合并，以防止内存分片(7)。在协议层之间传送数据包只要传送内存块的起始地址就可以了。这种内存管理方法空间浪费小但是运算量相对较大。<br/>无数据包拷贝是指除了获得数据到RAM和数据包到网络设备发送缓存这两次拷贝外没有数据包的拷贝。这节省了拷贝时间。介绍两种实现方法：<br/>(1)链表方式：例如当应用层将DataSize大小的应用层数据交给TCP层发送，一般的做法是申请一个DataSize＋TCPHeadSize大小的内存然后填写TCP头部，并将数据包拷贝到TCP的载荷中。使用链表方式：TCP层只申请TCPHeadSize大小的内存，然后将这个TCPHead用链表连接到应用层数据。这种方式缺点是：同一个数据包的内存不连续，这加大了计算校验和内存释放的复杂度，运算量大。<br/>(2)预留空间方式：应用层为DataSize大小的数据包申请内存的时候，实际申请的是DataSize＋AllHeadSize，其中AllHeadSize表示所有协议头部大小总和。拷贝应用层数据时在其前面留出AllHeadSize大小的空余空间。内存块头部的pStart指示了程序所在层的有效数据的开始，例如在应用层时指向应用层数据包的开始地址。应用层将这个数据包传给TCP层以后，TCP层只要在pStart-TCPHeadSize开始的内存空间加一个TCP头部即可。这种方式运算量很小，但是应用层必须事先知道其底层的协议头大小之和，违反了下层协议和上层无关的要求。<br/>3.2.5如何实现整序、重发和窗口控制<br/>&nbsp;&nbsp;&nbsp;&nbsp;zlIP使用了队列缓存的方式来实现。这里队列的一个元素指向一个数据包，队列的最大长度没有限制。对于整序，使用ooSeq队列(7)，如果发现接收的TCP包序号并不是希望的，但是序号在接收窗口内，此时我们不能立刻接收这个包也不应丢弃，先将这个包放入ooSeq队列。每当，一个希望的TCP包被接收以后，再查看ooSeq队列现在是否有TCP包成为了希望的数据包，如果有则将其取出并处理。对于重发，使用unacked队列，每一个需要被应答的TCP数据包发送以后都要放入unacked队列，等到被应答以后才从队列中删除。TCP重发定时只针对unacked队列第一个TCP包，如果定时超出，重新发送，重发次数超出规定值，则报错。对于窗口控制，使用unsend队列，如果发现对方的窗口过小无法接收这个数据包，则只发送部分数据，将多余部分放入unsend队列，等待对方发来TCP包通知新的窗口大小时，再次判断是否可以发送了。如果在unsend队列不为空的情况下，我方应用层传来需要发送的数据包都应插入unsend队列。我方的TCP窗口的大小就是剩余内存空间的大小。<br/>3.2.6 捎带应答的实现<br/>&nbsp;&nbsp;&nbsp;&nbsp;捎带应答指的是，当对方一个需要应答的TCP包到达时，我方不马上给予应答，而是等待一个较短的时间。如果在这段时间内，我方有数据发送，则会捎带给予了应答，这减少了包的发送数量。 
]]>
</description>
</item><item>
<link>https://jackxiang.com/post//#blogcomment</link>
<title><![CDATA[[评论] [转载]TCP/IP在51单片机上的实现特点和方法]]></title> 
<author> &lt;user@domain.com&gt;</author>
<category><![CDATA[评论]]></category>
<pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate> 
<guid>https://jackxiang.com/post//#blogcomment</guid> 
<description>
<![CDATA[ 
	
]]>
</description>
</item>
</channel>
</rss>