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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_37787043/article/details/82020259

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<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 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<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,接下来操作 gUDPRevBuf
                            UDP_Rec_Buf[UDP_DATA_P+i]=0;                                    //清除UDP数据帧内容
                        }
                            //////////////////////////
                        #if 0
                        udp_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可以打印字符串的
                        #endif
                        if(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接收环形缓冲区结构体,全局保存(重要)
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数据进行处理;

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

通过嵌入式设备向上位机发送UDP数据:
这里写图片描述

猜你喜欢

转载自blog.csdn.net/weixin_37787043/article/details/82020259
今日推荐