STM32NET学习笔记 UDP部分
扫描二维码
随时随地手机看文章
1.前言
嵌入式以太网开发是一个很有挑战性的工作。通过几个月的学习,我个人觉得大致有两条途径。第一条途径,先通过高级语言熟悉socket编程,例如C#或C++,对bind,listen,connect,accept等函数熟悉之后,应用 lwIP。第二种途径,通过分析嵌入式以太网代码,结合TCPIP协议栈规范逐步实践代码。第一种途径效率高,开发周期短,编写出来的代码性能稳定,第二种途径花的时间长,开发出来的代码功能不完善,但是由于紧紧结合TCPIP规范,可以了解的内容较多,适合学习。本文通过分析和修改AVRNET源码,逐步实现TCPIP协议栈的各个子部分,包括ETHERNET部分,ARP部分,IP部分,ICMP部分,UDP部分,TCP部分和HTTP部分。【STM32NET学习笔记——索引】【代码仓库】
本文将实现UDP部分。
UDP协议全称为用户数据协议,是一种简单有效的运输协议。和以太网首部和IP首部相似,UDP首部也有自身的数据结构定义。从运输协议开始引入端口的概念,端口相当于一个应用程序的标识符。相对于TCP协议而言,UDP协议简单的多。本文将实现UDP协议,并通过几个简单的案例说明UDP的使用。
1.2 相关资料
【ENC28J60学习笔记】
【AVRNET项目(国外)】
【AVR webserver项目(国外)】
1.3 代码仓库
【代码仓库】——CSDN Code代码仓库。
2 UDP部分实现
UDP功能的实现可分为UDP首部填充,UDP缓冲区填充和UDP报文查询。UDP首部填充是一个按部就班的过程,即填充源端口、目标端口、长度和校验和。UDP缓冲区填充即往UDP负载部分逐个填充数据。UDP报文查询功能即匹配本机UDP端口号并进行函数处理。为了实现这些功能,首先需要以下宏定义。需要注意以太网传输协议中数据被以大端的形式保存,即低地址存放了高字节内容。
//UDP默认端口号
#defineUDP_AVR_PORT_V3000
#defineUDP_AVR_PORT_H_V(UDP_AVR_PORT_V>>8)
#defineUDP_AVR_PORT_L_V(UDP_AVR_PORT_V&0xff)
//源端口
#defineUDP_SRC_PORT_H_P0x22
#defineUDP_SRC_PORT_L_P0x23
//目标端口
#defineUDP_DST_PORT_H_P0x24
#defineUDP_DST_PORT_L_P0x25
//UDP负载长度
#defineUDP_LENGTH_H_P0x26
#defineUDP_LENGTH_L_P0x27
//UDP校验和
#defineUDP_CHECKSUM_H_P0x28
#defineUDP_CHECKSUM_L_P0x29
//UDP负载起始地址
#defineUDP_DATA_P0x2A
2.1 UDP首部填充
UDP首部填充中需要明确UDP的端口号,STMNET项目中通过常数宏定义实现。
#defineUDP_AVR_PORT_V3000
#defineUDP_AVR_PORT_H_V(UDP_AVR_PORT_V>>8)
#defineUDP_AVR_PORT_L_V(UDP_AVR_PORT_V&0xff)
从这段代码中可以看出,STMNET的UDP端口号为3000。
voidudp_generate_header(BYTE*rxtx_buffer,WORD_BYTESdest_port,WORD_BYTESlength)
{
WORD_BYTESck;
//默认端口号3000
rxtx_buffer[UDP_SRC_PORT_H_P]=UDP_AVR_PORT_H_V;
rxtx_buffer[UDP_SRC_PORT_L_P]=UDP_AVR_PORT_L_V;
//目标端口地址
rxtx_buffer[UDP_DST_PORT_H_P]=dest_port.byte.high;
rxtx_buffer[UDP_DST_PORT_L_P]=dest_port.byte.low;
//负载长度
rxtx_buffer[UDP_LENGTH_H_P]=length.byte.high;
rxtx_buffer[UDP_LENGTH_L_P]=length.byte.low;
//计算校验和
rxtx_buffer[UDP_CHECKSUM_H_P]=0;
rxtx_buffer[UDP_CHECKSUM_L_P]=0;
//length+8forsource/destinationIPaddresslength(8-bytes)
ck.word=software_checksum((BYTE*)&rxtx_buffer[IP_SRC_IP_P],length.word+8,length.word+IP_PROTO_UDP_V);
rxtx_buffer[UDP_CHECKSUM_H_P]=ck.byte.high;
rxtx_buffer[UDP_CHECKSUM_L_P]=ck.byte.low;
}
2.2 UDP负载长度查询
UDP首部中包含UDP长度描述字节,长度占有两个字节并以大端格式保存,由于宏定义的提示作用,弱化了大端格式的影响。长度中也包括了UDP首部的长度,UDP首部的长度为固定的8字节。
WORDudp_get_dlength(BYTE*rxtx_buffer)
{
WORDlength=0;
//获得UDP长度
length=rxtx_buffer[UDP_LENGTH_H_P]<<8|rxtx_buffer[UDP_LENGTH_L_P];
//去除首部长度
length=length-8;
returnlength;
}
2.3 UDP负载区填充
UDP负载去填充即在UDP首部之后填充有用的数据。在这段真实负载之前包括了UDP首部,IP首部和以太网首部,分别占用了8字节,20字节和14字节。UDP负载的起始地址通过宏由UDP_DATA_P定义。
WORDudp_puts_data(BYTE*rxtx_buffer,BYTE*data,WORDoffset)
{
while(*data)
{
rxtx_buffer[UDP_DATA_P+offset]=*data++;
offset++;
}
returnoffset;
}
2.4 UDP报文查询
UDP报文查询需要匹配接收数据包中的UDP端口号,若匹配成功则可对输入数据包进行处理,这些处理包括解析数据包格式,分析出控制命令或查询命令。也可以通过udp_puts_data向发送缓冲区中填写响应数据。接着逐步生成以太网首部,IP首部和UDP首部,以太网首部中包含目标MAC地址,IP首部中包含目标IP地址,UDP首部中包含目标端口号。
BYTEudp_receive(BYTE*rxtx_buffer,BYTE*dest_mac,BYTE*dest_ip)
{
WORDdlength=0;
//udp负载长度
WORDudp_loadlen=0;
//匹配UDP协议UDP端口号
if(rxtx_buffer[IP_PROTO_P]==IP_PROTO_UDP_V&&rxtx_buffer[UDP_DST_PORT_H_P]==UDP_AVR_PORT_H_V&&rxtx_buffer[UDP_DST_PORT_L_P]==UDP_AVR_PORT_L_V)
{
//获得UDP负载长度
udp_loadlen=udp_get_dlength(rxtx_buffer);
//复制UDP接收
memcpy(udp_recbuf,(char*)&rxtx_buffer[UDP_DATA_P],udp_loadlen);
#ifUDP_DEBUG
printf("UDPMessage!rn");
printf("SendForm:%d.%d.%d.%d",
rxtx_buffer[IP_SRC_IP_P+0],rxtx_buffer[IP_SRC_IP_P+1],
rxtx_buffer[IP_SRC_IP_P+2],rxtx_buffer[IP_SRC_IP_P+3]);
printf("Port:%drn",(rxtx_buffer[UDP_SRC_PORT_H_P]<<8)|rxtx_buffer[UDP_SRC_PORT_L_P]);
printf("Reccive:%srn",udp_recbuf);
#endif
//生成以太网首部
eth_generate_header(rxtx_buffer,(WORD_BYTES){ETH_TYPE_IP_V},dest_mac);
//生成IP首部
ip_generate_header(rxtx_buffer, (WORD_BYTES){sizeof(IP_HEADER)+sizeof(UDP_HEADER)+dlen