1.前言
嵌入式以太网开发是一个很有挑战性的工作,通过半个月学习,我觉得大致有两条途径。第一条途径,先通过高级语言熟悉socket编程,例如C#或C++,对bind,listen,connect,accept等函数熟悉之后,应用 lwIP。第二种途径,通过分析嵌入式以太网代码,结合TCPIP协议栈规范逐步实践代码。第一种途径效率高,开发周期短,编写出来的代码性能稳定,第二种途径花的时间长,开发出来的代码功能不完善,但是由于紧紧结合TCPIP规范,可以了解的内容较多,适合学习。
本文通过分析和修改部门同事分享的代码,移植到我负责项目的工程里面,通过ENC28J60以太网模块,逐步实现UDP通信。
UDP协议全称为用户数据协议,是一种简单有效的运输协议。和以太网首部和IP首部相似,UDP首部也有自身的数据结构定义。从运输协议开始引入端口的概念,端口相当于一个应用程序的标识符。相对于TCP协议而言,UDP协议简单很多。
2.UDP实现部分
UDP功能的实现可分为UDP首部填充,UDP缓冲区填充和UDP报文查询。UDP首部填充是一个按部就班的过程,即填充源端口、目标端口、消息长度和校验和。UDP缓冲区填充即往UDP负载部分逐个填充数据。UDP报文查询功能即匹配本机UDP端口号并进行函数处理。
UDP数据包格式:
注:1、本文所阐述协议将封装于UDP数据包中的数据区,以下简称UDP数据帧。
2、UDP数据帧独立于UDP协议,UDP协议只将其封装。
为了实现UDP功能,首先需要以下宏定义。需要注意以太网传输协议中数据被以大端的形式保存,即低地址存放了高字节内容。
// ******* UDP *******
#define UDP_HEADER_LEN 8 //固定8字节
//源端口
#define UDP_SRC_PORT_H_P 0x22
#define UDP_SRC_PORT_L_P 0x23
//目标端口
#define UDP_DST_PORT_H_P 0x24
#define UDP_DST_PORT_L_P 0x25
// UDP负载长度
#define UDP_LEN_H_P 0x26
#define UDP_LEN_L_P 0x27
// UDP校验和
#define UDP_CHECKSUM_H_P 0x28
#define UDP_CHECKSUM_L_P 0x29
// UDP负载起始地址
#define UDP_DATA_P 0x2a
2.1 UDP首部填充
UDP首部填充中需要明确源端口、目标端口,本项目中通过常数宏定义实现。下面分两种情况讨论:一是接收到UDP数据包,二是发送UDP数据包;
u16 MYUDPPORT = 0x1F40;//8000
u8 MYIP[4]={192,168,1,220};
u8 MAC[6]={54,55,58,10,70,50};
2.1.1 当接收到UDP数据包, 可以通过判断UDP首部的目标端口号与本机端口,知道这个UDP包是否是发给本机设备的;
然后继续判断UDP数据包的源IP地址与本机指定的IP地址是否匹配,从而执行接收到不同上位机发送来的数据包,执行不同操作;
例如:服务器发送来的数据
if(((UDP_Rec_Buf[UDP_DST_PORT_H_P]==(MYUDPPORT>>8)) //校验目标端口,是的,为什么和本地端口比较,因为上位机发送的UDP首部,填充了要发送到的目标的端口
&& (UDP_Rec_Buf[UDP_DST_PORT_L_P]==(u8)MYUDPPORT)//校验目标端口
&&(UDP_Rec_Buf[IP_SRC_P]==Service_IP[0])&&(UDP_Rec_Buf[IP_SRC_P+1]==Service_IP[1])&&(UDP_Rec_Buf[IP_SRC_P+2]==Service_IP[2])&&(UDP_Rec_Buf[IP_SRC_P+3]==Service_IP[3]))//校验服务器IP
||((UDP_Rec_Buf[UDP_DST_PORT_H_P]==0x27) && (UDP_Rec_Buf[UDP_DST_PORT_L_P]==0x0F)))
又如:A2_A3设备发送来的数据
if(((UDP_Rec_Buf[UDP_DST_PORT_H_P]==(MYUDPPORT>>8)) //校验目标端口,是的,为什么和本地端口比较,因为上位机发送的UDP首部,填充了要发送到的目标的端口
&& (UDP_Rec_Buf[UDP_DST_PORT_L_P]==(u8)MYUDPPORT)//校验目标端口,注意:下面是校验发送数据来的IP地址
&&(UDP_Rec_Buf[IP_SRC_P]==A2_A3_IP[0])&&(UDP_Rec_Buf[IP_SRC_P+1]==A2_A3_IP[1])&&(UDP_Rec_Buf[IP_SRC_P+2]==A2_A3_IP[2])&&(UDP_Rec_Buf[IP_SRC_P+3]==A2_A3_IP[3]))//校验A2_A3设备IP
||((UDP_Rec_Buf[UDP_DST_PORT_H_P]==0x27) && (UDP_Rec_Buf[UDP_DST_PORT_L_P]==0x0F)))
2.1.2 当需要发送UDP数据包出去,使用封装函数:UDP_Data_Send();需要填充目标地址的MAC地址、目标地址的IP地址以及目标地址的端口;如下:
//发送UDP数据
//你可以发送最多220字节的数据
//参数1:定义缓冲区,最后通过ENC28J60发送出去
//参数2:UDP总的数据帧,看通讯协议
//参数3:数据长度
//参数4:要发送到的目标地址的MAC地址,数组首地址
//参数5:要发送到的目标地址的IP地址,数组首地址
//参数6:要发送到的目标地址的端口
//示例:UDP_Data_Send(Buf_Temp,TempUDP.udpdata,6+TempUDP.udpdata[4],DST_macaddr,DST_ipaddr,DST_port);
void UDP_Data_Send(u8 *Buffer,u8 *data,u8 datalen,u8 *DST_MAC,u8 *DST_IP,u16 DST_PORT)
{
u8 i=0;
u16 ck;
// make_eth(Buffer);
for(i=0;i<6;i++)
{
Buffer[ETH_DST_MAC+i]=DST_MAC[i];
Buffer[ETH_SRC_MAC+i]=macadr[i];
}
if (datalen>220)//你可以发送最多220字节的数据
{
datalen=220;
}
// total length field in the IP header must be set:
Buffer[IP_TOTLEN_H_P]=0;
Buffer[IP_TOTLEN_L_P]=IP_HEADER_LEN+UDP_HEADER_LEN+datalen;
// make_ip(Buffer);
for(i=0;i<4;i++)
{
Buffer[IP_DST_P+i]=DST_IP[i];
Buffer[IP_SRC_P+i]=ipaddr[i];
}
fill_ip_hdr_checksum(Buffer);
Buffer[UDP_DST_PORT_H_P]=DST_PORT>>8;
Buffer[UDP_DST_PORT_L_P]=(u8)DST_PORT;
Buffer[UDP_SRC_PORT_H_P]=(MYUDPPORT >> 8);
Buffer[UDP_SRC_PORT_L_P]=MYUDPPORT & 0xff;
// source port does not matter and is what the sender used.
// calculte the udp length:
Buffer[UDP_LEN_H_P]=0;
Buffer[UDP_LEN_L_P]=UDP_HEADER_LEN+datalen;
// zero the checksum
Buffer[UDP_CHECKSUM_H_P]=0;
Buffer[UDP_CHECKSUM_L_P]=0;
// copy the data:
for(i=0;i>8;
Buffer[UDP_CHECKSUM_L_P]=ck& 0xff;
// Uart1Write(Buffer,UDP_HEADER_LEN+IP_HEADER_LEN+ETH_HEADER_LEN+datalen);
//通过ENC28J60发送以太网数据包
enc28j60PacketSend(UDP_HEADER_LEN+IP_HEADER_LEN+ETH_HEADER_LEN+datalen,Buffer);
}
2.1.2 关于UDP_Data_Send()填充目标地址的MAC地址、目标地址的IP地址以及目标地址的端口
有2种方法,一是指定发送的目标,二是从接收到的数据包中提取源MAC地址、IP地址和端口,再发送回去;
2.1.2.1 例如:UDP_Data_Send(Buf_Temp,TempUDP_t.udpdata,TempUDP_t.length,Service_MAC,Service_IP,Service_Potr);
后面3个参数是程序锁定的,配置如下:
u8 Service_IP[4] = {192,168,1,187};
u16 Service_Potr = 3413;
u8 Service_MAC[6] = {0x1C,0x1B,0xD,0x55,0xFD,0xF};
这样就可以向指定目标发送数据包了。
2.1.2.2 又如:UDP_Data_Send(Buf_Temp,TempUDP_t.udpdata,TempUDP_t.length,DST_macaddr,DST_ipaddr,DST_port);
后面3个参数是从接收到的数据包中提取出来,保存为全局变量,再调用的。
for(i=0;i<4;i++)
{
//提取接收到UDP数据包的源IP地址,方便应答用
DST_ipaddr[i] = UDP_Rec_Buf[IP_SRC_P+i];//要发送到的目标地址的IP地址
}
for(i=0;i<6;i++)
{
//提取接收到UDP数据包的MAC地址
DST_macaddr[i] = UDP_Rec_Buf[ETH_SRC_MAC +i];//要发送到的目标地址的MAC地址
#if 1
SEGGER_RTT_printf(0,"DST_macaddr\n");
for(k=0;k<6;k++)
{
SEGGER_RTT_printf(0,"%d\n",DST_macaddr[k]);
}
#endif
}
2.2 读取UDP数据包
#define BUFFER_SIZE 570
u8 UDP_Rec_Buf[BUFFER_SIZE]; //保存一切通过ENC28J60以太网模块接收到的数据包
//ENC28J60模块读取以太网数据包,保存到指定缓冲区UDP_Rec_Buf(重要)
enc28j60PacketReceive(BUFFER_SIZE, UDP_Rec_Buf);
2.3 UDP负载长度查询
UDP首部中包含UDP长度描述字节,长度占有两个字节并以大端格式保存,由于宏定义的提示作用,弱化了大端格式的影响。长度中也包括了UDP首部的长度,UDP首部的长度为固定的8字节。
//获取UDP负载长度查询
//返回值:UDP数据帧总长度,不是UDP数据包的长度
u32 udp_get_dlength(u8 *rxtx_buffer)
{
u32 length = 0;
// 获得UDP长度
length = rxtx_buffer[UDP_LEN_H_P] << 8 | rxtx_buffer[UDP_LEN_L_P];
// 去除首部长度
length = length - 8;
return length;
}
2.4 UDP负载区填充和发送函数
看上面的UDP_Data_Send()函数;
2.5 UDP报文查询
UDP报文查询需要匹配接收数据包中的UDP端口号,若匹配成功则可对输入数据包进行处理,这些处理包括解析数据包格式,分析出控制命令或查询命令。
/**************************************************
//函数名称:make_echo_reply_from_request
//功 能: 生成差错报文
//参 数:帧地址,帧长度
//返 回 值:无
**************************************************/
void make_echo_reply_from_request(u8 *Buffer,u16 len)
{
make_eth(Buffer);
make_ip(Buffer);
Buffer[ICMP_TYPE_P]=ICMP_TYPE_ECHOREPLY_V;
// we changed only the icmp.type field from request(=8) to reply(=0).
// we can therefore easily correct the checksum:
if (Buffer[ICMP_CHECKSUM_P] > (0xff-0x08))
{
Buffer[ICMP_CHECKSUM_P+1]++;
}
Buffer[ICMP_CHECKSUM_P]+=0x08;
enc28j60PacketSend(len,Buffer);
}
2.6 实验
本实验通过PC端的网络调试助手向嵌入式设备发送UDP数据包,控制嵌入式设备,同时嵌入式设备也可以发送UDP数据包给PC端;
2.6.1 网络调试助手界面如下:
2.6.2 本地端口可以打开PC端的dos命令窗口,输入ipconfig查询到;本地端口随机设置即可。
2.6.3 目标主机由嵌入式设备程序决定,看程序代码;
u16 MYUDPPORT = 0x1F40;//8000
u8 MYIP[4]={192,168,1,225};
u8 MAC[6]={54,55,58,10,70,55};
2.6.4 实验开始时,应该打开PC端的dos命令窗口,ping嵌入式设备的IP地址,看是否可以连接成功;
下图表示连接成功;
2.6.5 ENC28J60以太网模块初始化
这里ENC28J60以太网模块通过SPI1与CPU传输数据,所以(1)SPI1配置,使能SPI1;(2)初始化ENC28J60以太网模块,包括配置PHY工作状态,初始化嵌入式设备MAC地址;(3)读写PHY寄存器,PHY寄存器和被ENC28J60控制的LED指示灯有关;(4)初始化以太网模块 IP 层,一般传参是嵌入式设备的MAC地址和IP地址;
初始化成功,即可通过以太网模块和其他设备以及上位机互传数据。
2.6.6 通过以太网模块读取UDP数据、
这里调用void UDP_DATA_CHECK(void)完成,详细看下面的代码和注释;
代码:
void UDP_DATA_CHECK(void)
{
u16 UDP_data_len; //UDP数据帧长度
u16 plen; //以太网帧长度
u16 i;
u8 k;
u8 UDP_CRC_t;
u8 DataCRC;
stcUDPBuf stcUDP_RcvTemp;
//ENC28J60模块读取以太网数据包,保存到指定缓冲区(重要)
plen = enc28j60PacketReceive(BUFFER_SIZE, UDP_Rec_Buf);
if(plen!=0)
{
if(eth_type_is_arp_and_my_ip(UDP_Rec_Buf,plen))//确认是否为对本机的ARP请求
{
make_arp_answer_from_request(UDP_Rec_Buf);//APR请求应答
#ifdef UDP_DEBUG
SEGGER_RTT_printf(0,"确认为对本机的ARP请求\n");//可以打印出来,连接路由器,一直有接收数据
#endif
#ifdef UDP_DEBUG
//调试用
DST_port = UDP_Rec_Buf[UDP_SRC_PORT_H_P]<<8|UDP_Rec_Buf[UDP_SRC_PORT_L_P];
SEGGER_RTT_printf(0,"服务器端口号 = %d\n",DST_port);//打印PC(服务器)端口号,都是3413???
#endif
}
if(eth_type_is_ip_and_my_ip(UDP_Rec_Buf,plen)==0)
{
return;//直接退出
}
#ifdef printf_DEBUG
SEGGER_RTT_printf(0,"1\n");
#endif
if(UDP_Rec_Buf[IP_PROTO_P]==IP_PROTO_ICMP_V && UDP_Rec_Buf[ICMP_TYPE_P]==ICMP_TYPE_ECHOREQUEST_V)
{
// a ping packet, let's send pong
//生成差错报文
make_echo_reply_from_request(UDP_Rec_Buf,plen);
SEGGER_RTT_printf(0,"2\n");
}
//校验是否是指定上位机发送来的数据,通过上位机端口号,IP地址判断
#ifdef printf_DEBUG
SEGGER_RTT_printf(0,"3\n");
#endif
if(UDP_Rec_Buf[IP_PROTO_P]==IP_PROTO_UDP_V)
{
#ifdef printf_DEBUG
SEGGER_RTT_printf(0,"4\n");
#endif
if(((UDP_Rec_Buf[UDP_DST_PORT_H_P]==(MYUDPPORT>>8)) //校验目标端口,是的,为什么和本地端口比较,因为上位机发送的UDP首部,填充了要发送到的目标的端口
&& (UDP_Rec_Buf[UDP_DST_PORT_L_P]==(u8)MYUDPPORT)//校验目标端口
&&(UDP_Rec_Buf[IP_SRC_P]==Service_IP[0])&&(UDP_Rec_Buf[IP_SRC_P+1]==Service_IP[1])&&(UDP_Rec_Buf[IP_SRC_P+2]==Service_IP[2])&&(UDP_Rec_Buf[IP_SRC_P+3]==Service_IP[3]))//校验服务器IP
||((UDP_Rec_Buf[UDP_DST_PORT_H_P]==0x27) && (UDP_Rec_Buf[UDP_DST_PORT_L_P]==0x0F)))//注意:需要指定上位机的IP
{
SEGGER_RTT_printf(0,"5\n");
SEGGER_RTT_printf(0,"UDP_Rec_Buf[UDP_DST_PORT_H_P] = %d\n",UDP_Rec_Buf[UDP_DST_PORT_H_P]);//31
SEGGER_RTT_printf(0,"UDP_Rec_Buf[UDP_DST_PORT_L_P] = %d\n",UDP_Rec_Buf[UDP_DST_PORT_L_P]);//64
UDP_data_len = UDP_Rec_Buf[UDP_LEN_H_P]*256+ UDP_Rec_Buf[UDP_LEN_L_P]-8;//看UDP数据包格式
if(UDP_data_len ==UDP_Rec_Buf[AREA_LEN_H_P]*256+ UDP_Rec_Buf[AREA_LEN_L_P]+6) //固定6字节
{
SEGGER_RTT_printf(0,"*********************\n");
for(i=0;i<4;i++)
{
//提取接收到UDP数据包的源IP地址,方便应答用
DST_ipaddr[i] = UDP_Rec_Buf[IP_SRC_P+i];//要发送到的目标地址的IP地址
}
for(i=0;i<6;i++)
{
//提取接收到UDP数据包的MAC地址
DST_macaddr[i] = UDP_Rec_Buf[ETH_SRC_MAC +i];//要发送到的目标地址的MAC地址
#if 1
SEGGER_RTT_printf(0,"DST_macaddr\n");
for(k=0;k<6;k++)
{
SEGGER_RTT_printf(0,"%d\n",DST_macaddr[k]);
}
#endif
}
if((UDP_Rec_Buf[UDP_DST_PORT_H_P]==0x27) && (UDP_Rec_Buf[UDP_DST_PORT_L_P]==0x0F))
{
DST_port = 0x1F90;
SEGGER_RTT_printf(0,"DST_port\n");
}
else
{
//提取接收到UDP数据包的端口
DST_port = UDP_Rec_Buf[UDP_SRC_PORT_H_P]<<8|UDP_Rec_Buf[UDP_SRC_PORT_L_P];
SEGGER_RTT_printf(0,"实时服务器端口号= %d\n",DST_port);//实时打印出PC端调试助手的本地端口号,即服务器端口号
}
for(i=0;i
这里经过目标端口以及源IP地址的判断(看2.1.1),最终把我们想要的UDP数据包里面的UDP数据帧(UDP负载)保存为一个全局的结构体UDPRcvCyBufAPP,方便我们随时调用;
2.6.7 UDP数据处理
//定义UDP接收环形缓冲区结构体,全局保存(重要)
typedef struct _RcvUDPDataCycleBuf_
{
u8 WritePoint ; //写指针
u8 ReadPoint ; //读指针
u8 FullFlag ; //缓冲区满标志,注意操作
stcUDPBuf RcvBuf[USE_UDP_cycRCV_BUF_SIZE];//结构体里面包含stcUDPBuf结构体
}stcRcvUDPCyBuf,*P_stcRcvUDPCyBuf;
stcRcvUDPCyBuf UDPRcvCyBufAPP; //应用UDP接收环形缓冲区(重要)
通过u32 ReadUDPRcvCyBuf(stcUDPBuf *Buf,u8 Mode) 操作UDPRcvCyBufAPP,分为预取模式和取出后即删除模式。
接着通过void UDP_RcvDeal(void)对UDP数据进行处理;