可运行在单片机上的UDP通讯协议的实现_海纳百川有容乃大_百度空间

    本文可以说是在我之前写的基础上增加了一些协议代码来实现具体UDP通信传输。在这里我重新强调一下,上篇文章是在介绍如何调硬件,目的是为了让应用程序可以使用这个芯片。而具体的使用就是使用三个函数:初始化、数据包发送和数据包接收。数据包接收是否基于中断还需要用户根据需要自行设置。总之,我们可以通过上篇文章了解到,对硬件的调试可以得到这三个有用的函数。

    不同网卡芯片的驱动可能略有不同。这里不一一例举,所以首先需要说明的是,本文所讲的内容主要是如何用C代码来实现协议,并利用数据包发送、接收函数来实现通讯,基本是与硬件无关的。除中断外,本文{wy}与硬件相关的地方就是大端或小端格式,这也在之前的文章中有提到过,在本文涉及到的地方会再次说明。也就是说使用不同网卡芯片都可以应用本文所写的代码。其次,本文所写出的协议部分是已经过简化的,代码较少,不需要操作系统支持。但仅能实现数据的收发,而且没有验证可靠性(需要时可自行验证)。适合用在资源有限的单片机系统中,或者需要用网络代替RS232通信的情况,当然也可以在操作系统中使用。可根据情况来选择或增减。

    顺便提一下资源要求:ram{zh0}大于2KB,实在不行也得要1KB(需要一定技巧,传输的数据内容很少,不需要全部读出数据包的情况);flash或rom4KB以上,基本的单片机都能达到;可用IO怎么也得有12个,控制个一般的芯片也需要这么多的。

    在进行正文之前,我再啰嗦几句,本文是讲协议的实现。这里的协议部分可以从《TCP/IP协议 {dy}卷 —— 协议》这本书中看到最详细最xx的讲解,如果有兴趣研究协议的话可以参考这本书(网上可以找到电子版的)。下面进入主题。

    1、UDP通讯的实现过程简述

    涉及到协议部分,很多人会感觉摸不清头绪,不知如何下手。所以看一看上面说的那本书还是很有帮助的。当然看了以下部分,你也会对协议有些了解的。

    (1)初始化网卡芯片和其他外设(在网卡驱动部分已经做好了,这里重新说了一遍);

    (2)arp通讯获得目标机地址信息;

    (3)udp通讯收发数据(利用IP协议作媒介)。

    看到这会不会有些失望呢,可实际上udp通讯就是这么简单的。

    UDP是User Datagram Protocol的简称,中文名是用户数据包协议,是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务(这句话是在网上抄的)。

    说的再简单点,udp通讯与RS232一样,只管将数据发送出去而不管对方是否正确的接收到了。在一些简单应用中,我们似乎也不大关心数据是否被对方正确的接收到了,因为传输过程中数据包损坏的情况也不多,肯定能被正确的接收到。或者我们有其他的办法去验证。这和TCP协议不一样,TCP协议是可靠的链接,发送一次数据需要三次握手来反复确认数据被正确无误的接收到了,否则会重新发送一遍,实现起来比较繁琐。有点跑题,不过看到这里应该可以明白udp协议是一种很简单的网络通讯协议。

    简单解释一下上面三个过程。初始化不用解释了。ARP通讯是整个网络传输的开始,而且只需要运行一次。在《单片机驱动DM9000网卡芯片(详细调试过程)【下】》{zh1}的部分已经讲清楚了。主要是解释第三条,udp通讯收发数据。

    OSI参考模型中arp协议属于链路层({zd2}层),ip协议比arp协议高一层属于网络层(这一层还包括icmp和igmp协议),在往上一层是运输层,包括tcp协议和udp协议。但是按我的理解,从数据包格式的角度看,我把arp协议与ip协议放在同一级别,我们接收到的数据包中的前几十个字节用来判断是arp协议还是ip协议,也就是说这两个协议是互补相容的(下面程序中会做个过滤,数据包只接收arp或ip协议,{zh1}处理的数据包中只能是arp协议或者是ip协议)。这种互补相容的协议同样也适用于tcp协议和udp协议上。既然有互不相容的协议,那么也就有相容的协议了,这种相容的协议就是指一个协议必须依赖于另一种协议才能实现,udp协议就是这样。我们可以这样理解,ip协议就像一件外套,udp协议好比一件衬衫,而真正的数据可以看做是穿衣服的人。穿衣服的人先穿上衬衫再穿外套,这两个协议之间的关系就是这样:udp协议将数据包起来,ip协议又将udp协议连同其中的数据一起包起来。也就是说,实际的数据经过udp协议的包装,在经过ip协议的包装之后才能发送出去。虽然看起来有些繁琐,但实际计算机端就是这样识别数据的。所谓的包装就是在被包装数据前加上一小段首部数据,一般几十个字节左右。

    2、ARP协议的实现

    这部分内容在《单片机驱动DM9000网卡芯片(详细调试过程)【下】》的后半部分已经讲过,这里为了完整性再重复一次。

    在写所有协议之前,有些全局变量需要事先设定一下,如ip地址、mac地址等信息。另外,统一规定一下我们的单片机系统为“基板”,计算机端为“上位机”,以下叫起来方便。再规定一下:char型是8位,short型是16位,long型是32位。OK!

/********************************************/

unsigned char my_macaddr[6] = { 0x00, 0x0a, 0x00, 0x01, 0x02, 0x03 };//基板上mac地址,这里随便写6个字节。
unsigned char my_ipaddr[4] = { 192, 168, 1, 207 };//基板上ip地址,根据网关写入合适值。
unsigned char server_macaddr[6] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };//上位机mac地址,通过arp协议获取后改变。
unsigned char server_ipaddr[4] = {192, 168, 1, 122};//上位机ip地址,看自己的电脑,这个应该会,不会就问问旁边的人吧。

unsigned char transmit_buffer[2048] = { 0 };//发送数据包缓存区
unsigned char receive_buffer[2048] = { 0 };//接收数据包缓存区
/*如果为了节省ram,这两个区可以共用一个(因为发送和接收数据可以分开进行,而且一般网卡芯片内部都会有自己独立的接收和发送缓存的),容量也可以改小,写成“unsigned char data_buffer[1000] = { 0 };”我的ram比较多,所以用了两个区,而且占用很大空间。*/

unsigned long send_packet_length = 0;//发送数据包长度
unsigned long receive_packet_length = 0;//接收数据包长度
/*如果上面的数据缓存区共用一个的话,这两个存储数据包长度的变量也可以共用一个,写成“packet_length”*/

/********************************************/

    这部分是全局变量,需要在所有函数之前定义。

    在写arp协议之前先定义两个结构体:

//以太网地址首部,我们用的网络就是以太网
struct eth_hdr {
unsigned char d_mac[6];    //目标mac地址
unsigned char s_mac[6];    //源mac地址
unsigned short type;     //协议类型,判断arp协议还是ip协议
};

//ARP首部=以太网首部+arp协议首部
struct arp_hdr {
struct eth_hdr ethhdr;    //以太网首部结构体,如上
unsigned short hwtype;    //硬件地址,“1”表示mac地址
unsigned short protocol;    //协议地址,“0x0800”表示ip地址
unsigned char hwlen;     //硬件地址长度,mac地址为6
unsigned char protolen;    //协议地址,ip地址为4
unsigned short opcode;    //操作码“1”表示arp请求,“2”表示arp应答
unsigned char smac[6];    //源mac地址
unsigned char sipaddr[4];    //源ip地址
unsigned char dmac[6];    //目标mac地址
unsigned char dipaddr[4];    //目标ip地址
};

#define ETH_TRBUF ((struct eth_hdr *)&transmit_buffer[0])
#define ETH_REBUF ((struct eth_hdr *)&receive_buffer[0])
/*宏定义均是为了书写方便,以以太网首部的方式指向数据包缓存

#define ARP_TRBUF   ((struct arp_hdr *)&transmit_buffer[0])
#define ARP_REBUF   ((struct arp_hdr *)&receive_buffer[0])

    接下来是arp协议的代码,具体含义在前一篇文章中有,这里对arp实现原理不做详细介绍了。另外,本文认为数据包发送和接收函数已经像前篇文章中那样写好了:

void sendpacket(unsigned char* datas, unsigned long length);//发送数据包
//要发送的数据包存放在“transmit_buffer[]”中。
unsigned long receivepacket(unsigned char* datas);//接收数据包
//接收到的数据包存放在“receive_buffer[]”中。可以与发送数据包公用。

//arp请求
void arp_request(void)
{
memcpy(ARP_TRBUF->ethhdr.d_mac, server_macaddr, 6);
memcpy(ARP_TRBUF->ethhdr.s_mac, my_macaddr, 6);
/*用到上面的函数,在文件中要写上“#include <string.h>”*/

ARP_TRBUF->ethhdr.type = 0x0806;//arp协议,此处按大端格式
/*注意!这里涉及到大端格式和小端格式问题,小端格式需要写成0x0608,也就是16位的高低两个字节转换一下,这个很重要,一定要弄清楚自己处理器的编译器的存储格式。我记得51、AVR等单片机的编译器是小端格式,所以要高低字节交换。我这里和下面的程序中都按大端格式写*/


ARP_TRBUF->hwtype = 1;//此处按大端格式
ARP_TRBUF->protocol = 0x0800;//ip协议,此处按大端格式
ARP_TRBUF->hwlen = 6;
ARP_TRBUF->protolen = 4;
ARP_TRBUF->opcode = 1;//arp请求,此处按大端格式。。。
/*以下凡是16位宽的数据我都按大端格式处理不在重复啦*/

memcpy(ARP_TRBUF->smac, my_macaddr, 6);
memcpy(ARP_TRBUF->sipaddr, my_ipaddr, 4);
memcpy(ARP_TRBUF->dipaddr, server_ipaddr, 4);

send_packet_length = 42;//14字节以太网首部+28字节arp首部=42字节

sendpacket(tr_data, send_packet_length);
}

//arp应答
void arp_reply(void)
{
//memcpy(ARP_TRBUF->ethhdr.d_mac, server_macaddr, 6);
//memcpy(ARP_TRBUF->ethhdr.s_mac, my_macaddr, 6);
ARP_TRBUF->ethhdr.type = 0x0806;

ARP_TRBUF->hwtype = 1;
ARP_TRBUF->protocol = 0x0800;
ARP_TRBUF->hwlen = 6;
ARP_TRBUF->protolen = 4;
ARP_TRBUF->opcode = 2;
//memcpy(ARP_TRBUF->smac, my_macaddr, 6);
memcpy(ARP_TRBUF->sipaddr, my_ipaddr, 4);
memcpy(ARP_TRBUF->dipaddr, server_ipaddr, 4);

send_packet_length = 42;//14+28=42

sendpacket(transmit_buffer, send_packet_length);
}

//arp处理
unsigned long arp_process(unsigned long len)
{
if(len < 42)return ARP_BAD_PACKET;//ARP_BAD_PACKET 这个宏自己随便定义个数,表示数据包是损坏的

if(ARP_REBUF->opcode == 1) //判断是否是arp请求包,记得这里也是大端格式,小端格式就变成0x0100了
{
   if(ARP_REBUF->dipaddr[0] == my_ipaddr[0] &&//判断是否是基板的mac地址
     ARP_REBUF->dipaddr[1] == my_ipaddr[1] &&
     ARP_REBUF->dipaddr[2] == my_ipaddr[2] &&
     ARP_REBUF->dipaddr[3] == my_ipaddr[3])
   {
    memcpy(ARP_TRBUF->ethhdr.d_mac, ARP_REBUF->ethhdr.s_mac, 6);
    memcpy(ARP_TRBUF->ethhdr.s_mac, ARP_REBUF->ethhdr.d_mac, 6);
    memcpy(ARP_TRBUF->dmac, ARP_REBUF->ethhdr.s_mac, 6);
    memcpy(ARP_TRBUF->smac, ARP_REBUF->ethhdr.d_mac, 6);
    arp_reply();//发送arp应答
    return ARP_REPLYTHEREQUEST;/*ARP_REPLYTHEREQUEST 这个宏也是自己随便定义的数,表示接收到的是arp请求数据包包*/
   }
   else
   {
    return ARP_NOTMYIP;//ARP_NOTMYIP 自定义宏,表示接收到的数据包不是基板的,放弃掉。
   }
}
else if(ARP_REBUF->opcode == 2) //判断是arp应答包
{
   if(ARP_REBUF->dipaddr[0] == my_ipaddr[0] &&
     ARP_REBUF->dipaddr[1] == my_ipaddr[1] &&
     ARP_REBUF->dipaddr[2] == my_ipaddr[2] &&
     ARP_REBUF->dipaddr[3] == my_ipaddr[3])
   {
    memcpy(server_macaddr, ARP_REBUF->smac, 6);
    return ARP_SAVETHEREPLY;//自定义宏,表示接收到arp应答,并且将上位机mac地址存储好了
   }
   else
   {
    return ARP_NOTMYIP;
   }
}
else
{
   return ARP_UNKNOWN;//自定义宏,表示数据包出错
}
}

    arp协议到这里就完成了。包含三个函数:

void arp_request(void);
void arp_reply(void);
unsigned long arp_process(unsigned long len);

    arp协议具体原理可参考也可参考前面介绍的书。

    3、IP协议的实现

    ip协议用途很重要,但对于本文仅使用udp协议来说,它只是用于装载udp协议的。

    首先,定义ip协议的首部结构体和一些必要的全局变量。

//ip首部=以太网首部+ip协议首部
struct ip_hdr {
struct eth_hdr ethhdr;//以太网首部
unsigned char vhl, //ip协议的版本和首部长度。固定写法0x45,表示IP4,首部长5*32bit(20字节)
               tos;//TOS(0)(可不关心)
unsigned short len,//整个ip数据包长度,包括ip首部
                ipid,//ip标识,也可以看做是ip计数器
                 ipoffset;//掩码和偏移(可不关心)
unsigned char ttl,//ip包的生命周期(写成固定值,可不关心)
               proto;//ip包装载的协议类型, "1"表示ICMP, "2"表示IGMP, "6"表示TCP, "17"表示UDP
unsigned short ipchksum;   //首部校验和
unsigned char srcipaddr[4], //源ip地址
                destipaddr[4]; //目标ip地址
};

    从这个首部看,ip协议首部结构体包含34个字节,其中ip首部有20个字节。根据首部可以确定一些信息,比如ip包中转载的是什么协议、ip地址、ip长度等。ip协议与arp协议的一个不同点是,ip协议中包含了一个首部校验和。这样的校验和在其他协议中也都有,而且计算方法也相同。校验和的作用是判断该协议被校验和检验过的区域(ip协议只检验首部的20个字节)是否正确,进而判断该数据包是否损坏。关于这个校验和的操作,有些芯片内部可以自动计算出来,有些却不能。为了统一,稍后会介绍校验和的计算方法。

    本文所涉及的ip协议除了装载其他协议外,没有其他用途,因此也并无很复杂的原理。接下来继续介绍涉及到ip协议的函数。这里再加一些宏和变量:

#define IP_TRBUF ((struct ip_hdr *)&transmit_buffer[0])
#define IP_REBUF ((struct ip_hdr *)&receive_buffer[0])
//宏定义:为书写方便,以ip协议首部的方式指向发送和接收的数据

unsigned char m_ipid = 0;
/*这是一个全局变量,可以看做是个发送ip协议包的计数器,专门用来填充ip协议首部结构体中的ipid成员*/

    接下来会完成两个函数,ip协议处理函数和ip协议数据包发送函数。前者用来完成ip协议的数据包分析和处理,后者用来发送一个ip协议数据包。具体函数如下:

//ip协议处理函数
unsigned long ip_process(void)
{
//判断是否是基板的ip地址
if( ( IP_REBUF->destipaddr[0] != my_ipaddr[0] )||
     ( IP_REBUF->destipaddr[1] != my_ipaddr[1] )||
   ( IP_REBUF->destipaddr[2] != my_ipaddr[2] )||
    ( IP_REBUF->destipaddr[3] != my_ipaddr[3] ) )
{
   return IP_NOTMYIP;//自定义宏,表示不是基板的ip包,放弃该包。
}

if(IP_REBUF->ttl != 0x45)
{
   return IP_BED_PACKET;//自定义宏,表示此ip包已经损坏
}
//我们仅做了以上两个简单判断,其实可以做更详细判断,但这些已经足够了

if(ip_chksum(&(IP_REBUF->vhl)) != 0)//校验和函数“ip_chksum()”在后面介绍,这里暂时标记一下
{
   return IP_BED_PACKET;
}

/*这里将接收到的完整数据包长度减掉20字节ip协议首部和14字节以太网首部,剩余字节数作为ip协议中数据内容的字节数*/
receive_packet_length = IP_REBUF->len - 34;

return IP_REBUF->proto;//返回ip包中包含的协议类型,以供后面程序继续处理
}

//发送ip数据包
void ip_send(unsigned long len)//参数为ip协议包中数据部分的字节数(这个数据部分包括udp协议)
{
//以太网首部填充
memcpy(IP_TRBUF->ethhdr.d_mac, server_macaddr, 6);
memcpy(IP_TRBUF->ethhdr.s_mac, my_macaddr, 6);
IP_TRBUF->ethhdr.type = 0x0800;//ip协议代码,大端格式

// IP header
len += 34;//更新数据包长度,增加ip首部和以太网首部共34字节

IP_TRBUF->vhl = 0x45;//固定写法,ip版本和首部长度,我们的网络貌似还没有用到IP6
IP_TRBUF->tos = 0x00;//可不关心,写0就行
IP_TRBUF->len = (unsigned short)(len - 14);//ip包总长度,包括ip首部和数据(不包括以太网首部)
IP_TRBUF->ipid = m_ipid;//ip包标识,也就是计数器,每次发完加1就行。
m_ipid++;
IP_TRBUF->ipoffset = 0x40;//可不关心,固定写法
IP_TRBUF->ttl = 255;//可不关心,表示此包可以经过路由器的个数,填为{zd0}值
IP_TRBUF->proto = 17;//表示ip包中装载着udp协议(大端格式),6为tcp协议

memcpy(IP_TRBUF->srcipaddr, my_ipaddr, 4);//基板ip地址
memcpy(IP_TRBUF->destipaddr, server_ipaddr, 4);//上位机ip地址

IP_TRBUF->ipchksum = 0;//校验和字段在计算结果之前用0填充,并参与计算
IP_TRBUF->ipchksum = ip_chksum(&(IP_TRBUF->vhl));//校验和的填充和检验的计算方法相同,用同一函数

sendpacket(transmit_buffer, len);//发送这个数据包
}

/*******************************************/
注释:值得注意的是,在这个函数中,要发送的数据包长度我用了传递参数的办法。而之前我又用了个全局变量来存放要发送的数据包长度,arp协议的实现,我使用了全局变量来传递要发送的数据包长度。其实这两个办法都可行,由于这两个函数我不是在同一个时间写的,实现方法上有些不同,现在看来是有点冗余,大家可以根据自己的书写习惯选择其中的一种方法,我就不在这里修改程序了。
/*******************************************/

    未完待续。。。。。。   



郑重声明:资讯 【可运行在单片机上的UDP通讯协议的实现_海纳百川有容乃大_百度空间】由 发布,版权归原作者及其所在单位,其原创性以及文中陈述文字和内容未经(企业库qiyeku.com)证实,请读者仅作参考,并请自行核实相关内容。若本文有侵犯到您的版权, 请你提供相关证明及申请并与我们联系(qiyeku # qq.com)或【在线投诉】,我们审核后将会尽快处理。
—— 相关资讯 ——