糖尿病康复,内容丰富有趣,生活中的好帮手!
糖尿病康复 > STM32 UDP部分 基于ENC28J60以太网模块 项目笔记

STM32 UDP部分 基于ENC28J60以太网模块 项目笔记

时间:2019-05-03 22:40:12

相关推荐

STM32 UDP部分 基于ENC28J60以太网模块 项目笔记

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_LEN8 //固定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;//8000u8 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 checksumBuffer[UDP_CHECKSUM_H_P]=0;Buffer[UDP_CHECKSUM_L_P]=0;// copy the data:for(i=0;i<datalen;i++) {Buffer[UDP_DATA_P+i]=data[i];}ck=checksum(&Buffer[IP_SRC_P], 16 + datalen,1);Buffer[UDP_CHECKSUM_H_P]=ck>>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 1SEGGER_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 570u8 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;//8000u8 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_DEBUGSEGGER_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_DEBUGSEGGER_RTT_printf(0,"1\n");#endifif(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_DEBUGSEGGER_RTT_printf(0,"3\n");#endifif(UDP_Rec_Buf[IP_PROTO_P]==IP_PROTO_UDP_V){#ifdef printf_DEBUGSEGGER_RTT_printf(0,"4\n");#endifif(((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]);//31SEGGER_RTT_printf(0,"UDP_Rec_Buf[UDP_DST_PORT_L_P] = %d\n",UDP_Rec_Buf[UDP_DST_PORT_L_P]);//64UDP_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 1SEGGER_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<BUFFER_SIZE;i++){Buf_Temp[i] = UDP_Rec_Buf[i];//???}/******************** UDP_Rec_Buf的分界线***************/for(i=0;i<UDP_data_len;i++){gUDPRevBuf[i] = UDP_Rec_Buf[UDP_DATA_P+i];//取出UDP数据帧负载放到临时缓冲区 gUDPRevBuf,接下来操作 gUDPRevBufUDP_Rec_Buf[UDP_DATA_P+i]=0;//清除UDP数据帧内容}//#if 0udp_loadlen = udp_get_dlength(gUDPRevBuf);st_memcpy(udp_recbuf, &gUDPRevBuf[UDP_DATA_P], udp_loadlen);SEGGER_RTT_printf(0,"取出UDP数据帧内容放到临时缓冲区 = %s\n",udp_recbuf);//不能打印出UDP接收到的数据SEGGER_RTT_printf(0,"recieve from sever_t = %s\n","eeeee");//可以打印,证明SEGGER_RTT_printf可以打印字符串的#endifif(gUDPRevBuf[0] == 0xDF)//接收完成,0xDF表示UDP数据帧帧头{SEGGER_RTT_printf(0,"是正确的UDP数据帧\n");//可以打印//接收数据长度 不包括UDP帧头的8Byte//UDP数据帧数据长度高字节、低字节,转换成UDP数据区长度//新映射一个结构体存储数据和长度;stcUDP_RcvTemp.length = gUDPRevBuf[3]*256+gUDPRevBuf[4]+6;//UDP数据帧总长度for(i=0;i<stcUDP_RcvTemp.length;i++){stcUDP_RcvTemp.udpdata[i] = gUDPRevBuf[i];//把缓冲区填充到结构体的UDP数据帧gUDPRevBuf[i] = 0;//记得清除临时缓冲区if(i==0){DataCRC = 0;//置位CRC}else{DataCRC ^= stcUDP_RcvTemp.udpdata[i]; //CRC校验???}}//打印CRC的数值UDP_CRC_t = CRC_Check(&stcUDP_RcvTemp.udpdata[1],stcUDP_RcvTemp.length-2);SEGGER_RTT_printf(0,"CRC = %x\n",UDP_CRC_t);if(0 == DataCRC)//CRC校验正确{SEGGER_RTT_printf(0,"CRC校验正确\n");//可以打印了UDPRcvCyBufAPP.FullFlag = 0;SEGGER_RTT_printf(0,"FullFlag = %d\n",UDPRcvCyBufAPP.FullFlag);//打印标志位ReadUDPRxBuf(&UDPRcvCyBufAPP,&stcUDP_RcvTemp);//全局保存到结构体UDPRcvCyBufAPP}}}} }}}

这里经过目标端口以及源IP地址的判断(看2.1.1),最终把我们想要的UDP数据包里面的UDP数据帧(UDP负载)保存为一个全局的结构体UDPRcvCyBufAPP,方便我们随时调用;

2.6.7 UDP数据处理

//定义UDP接收环形缓冲区结构体,全局保存(重要)typedefstruct_RcvUDPDataCycleBuf_{u8WritePoint;//写指针u8ReadPoint;//读指针u8FullFlag ;//缓冲区满标志,注意操作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数据进行处理;

通过PC端调试助手向嵌入式设备发送通讯协议,嵌入式设备接收到之后,进行UDP数据处理;

通过嵌入式设备向上位机发送UDP数据:

如果觉得《STM32 UDP部分 基于ENC28J60以太网模块 项目笔记》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。