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端口号并进行函数处理。为了实现这些功能,首先需要以下宏定义。需要注意以太网传输协议中数据被以大端的形式保存,即低地址存放了高字节内容。
[cpp] view plain copy
  1. // UDP默认端口号  
  2. #define UDP_AVR_PORT_V 3000  
  3. #define UDP_AVR_PORT_H_V (UDP_AVR_PORT_V>>8)  
  4. #define UDP_AVR_PORT_L_V (UDP_AVR_PORT_V&0xff)  
  5. // 源端口  
  6. #define UDP_SRC_PORT_H_P 0x22  
  7. #define UDP_SRC_PORT_L_P 0x23  
  8. // 目标端口  
  9. #define UDP_DST_PORT_H_P 0x24  
  10. #define UDP_DST_PORT_L_P 0x25  
  11. // UDP负载长度  
  12. #define UDP_LENGTH_H_P 0x26  
  13. #define UDP_LENGTH_L_P 0x27  
  14. // UDP校验和  
  15. #define UDP_CHECKSUM_H_P 0x28  
  16. #define UDP_CHECKSUM_L_P 0x29  
  17. // UDP负载起始地址  
  18. #define UDP_DATA_P 0x2A  

2.1 UDP首部填充
     UDP首部填充中需要明确UDP的端口号,STMNET项目中通过常数宏定义实现。
[cpp] view plain copy
  1. #define UDP_AVR_PORT_V 3000  
  2. #define UDP_AVR_PORT_H_V (UDP_AVR_PORT_V>>8)  
  3. #define UDP_AVR_PORT_L_V (UDP_AVR_PORT_V&0xff)  
从这段代码中可以看出,STMNET的UDP端口号为3000
[cpp] view plain copy
  1. void udp_generate_header ( BYTE *rxtx_buffer, WORD_BYTES dest_port, WORD_BYTES length )  
  2. {  
  3.     WORD_BYTES ck;  
  4.      
  5.     // 默认端口号 3000  
  6.     rxtx_buffer[UDP_SRC_PORT_H_P] = UDP_AVR_PORT_H_V;  
  7.     rxtx_buffer[UDP_SRC_PORT_L_P] = UDP_AVR_PORT_L_V;  
  8.      
  9.     // 目标端口地址  
  10.     rxtx_buffer[UDP_DST_PORT_H_P] = dest_port.byte.high;  
  11.     rxtx_buffer[UDP_DST_PORT_L_P] = dest_port.byte.low;  
  12.      
  13.     // 负载长度  
  14.     rxtx_buffer[UDP_LENGTH_H_P] = length.byte.high;  
  15.     rxtx_buffer[UDP_LENGTH_L_P] = length.byte.low;  
  16.      
  17.     // 计算校验和  
  18.     rxtx_buffer[UDP_CHECKSUM_H_P] = 0;  
  19.     rxtx_buffer[UDP_CHECKSUM_L_P] = 0;  
  20.     // length+8 for source/destination IP address length (8-bytes)  
  21.     ck.word = software_checksum ( (BYTE*)&rxtx_buffer[IP_SRC_IP_P], length.word+8, length.word+IP_PROTO_UDP_V);  
  22.     rxtx_buffer[UDP_CHECKSUM_H_P] = ck.byte.high;  
  23.     rxtx_buffer[UDP_CHECKSUM_L_P] = ck.byte.low;  
  24. }  

2.2 UDP负载长度查询
     UDP首部中包含UDP长度描述字节,长度占有两个字节并以大端格式保存,由于宏定义的提示作用,弱化了大端格式的影响。长度中也包括了UDP首部的长度,UDP首部的长度为固定的8字节。
[cpp] view plain copy
  1. WORD udp_get_dlength( BYTE *rxtx_buffer )  
  2. {  
  3.     WORD length = 0;  
  4.     // 获得UDP长度  
  5.     length = rxtx_buffer[UDP_LENGTH_H_P] << 8 | rxtx_buffer[UDP_LENGTH_L_P];  
  6.     // 去除首部长度  
  7.     length = length - 8;  
  8.      
  9.     return length;  
  10. }  

2.3 UDP负载区填充
     UDP负载去填充即在UDP首部之后填充有用的数据。在这段真实负载之前包括了UDP首部,IP首部和以太网首部,分别占用了8字节,20字节和14字节。UDP负载的起始地址通过宏由UDP_DATA_P定义。
[cpp] view plain copy
  1. WORD udp_puts_data ( BYTE *rxtx_buffer, BYTE *data, WORD offset )  
  2. {  
  3.     while(*data)  
  4.     {  
  5.         rxtx_buffer[UDP_DATA_P + offset] = *data++;  
  6.         offset++;  
  7.     }  
  8.     return offset;  
  9. }  

2.4 UDP报文查询
     UDP报文查询需要匹配接收数据包中的UDP端口号,若匹配成功则可对输入数据包进行处理,这些处理包括解析数据包格式,分析出控制命令或查询命令。也可以通过udp_puts_data向发送缓冲区中填写响应数据。接着逐步生成以太网首部,IP首部和UDP首部,以太网首部中包含目标MAC地址,IP首部中包含目标IP地址,UDP首部中包含目标端口号。
[cpp] view plain copy
  1. BYTE udp_receive ( BYTE *rxtx_buffer, BYTE *dest_mac, BYTE *dest_ip )  
  2. {  
  3.     WORD dlength = 0;  
  4.     // udp负载长度  
  5.     WORD udp_loadlen = 0;  
  6.      
  7.     // 匹配UDP协议 UDP端口号  
  8.     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 )  
  9.     {  
  10.         // 获得UDP负载长度  
  11.         udp_loadlen = udp_get_dlength(rxtx_buffer);  
  12.          
  13.         // 复制UDP接收  
  14.         memcpy(udp_recbuf, (char*)&rxtx_buffer[UDP_DATA_P], udp_loadlen);  
  15.          
  16. #if UDP_DEBUG  
  17.         printf("UDP Message!\r\n");  
  18.         printf("Send Form:%d.%d.%d.%d ",\  
  19.                 rxtx_buffer[IP_SRC_IP_P+0],rxtx_buffer[IP_SRC_IP_P+1],\  
  20.                 rxtx_buffer[IP_SRC_IP_P+2],rxtx_buffer[IP_SRC_IP_P+3]);  
  21.         printf("Port:%d\r\n",(rxtx_buffer[UDP_SRC_PORT_H_P] << 8) | rxtx_buffer[UDP_SRC_PORT_L_P]);  
  22.         printf("Reccive:%s\r\n",udp_recbuf);  
  23. #endif  
  24.          
  25.         // 生成以太网首部  
  26.         eth_generate_header(rxtx_buffer, (WORD_BYTES){ETH_TYPE_IP_V}, dest_mac );  
  27.         // 生成IP首部  
  28.         ip_generate_header(rxtx_buffer, (WORD_BYTES){sizeof(IP_HEADER)+sizeof(UDP_HEADER)+dlength}, IP_PROTO_UDP_V, dest_ip );  
  29.         // 生成UDP首部  
  30.         udp_generate_header(rxtx_buffer, (WORD_BYTES){(rxtx_buffer[UDP_SRC_PORT_H_P]<<8)|rxtx_buffer[UDP_SRC_PORT_L_P]}, (WORD_BYTES){sizeof(UDP_HEADER)+dlength});  
  31.         // 发送所有首部和UDP负载数据  
  32.         enc28j60_packet_send(rxtx_buffer, sizeof(ETH_HEADER)+sizeof(IP_HEADER)+sizeof(UDP_HEADER)+dlength );  
  33.          
  34.         // 返回1代表数据包已被处理  
  35.         return 1;  
  36.     }  
  37.      
  38.     // 返回0代表数据包未被处理  
  39.     return 0;  
  40. }  

3 实验
     实验部分主要是为了验证UDP协议,通过PC机上的网络调试软件开辟一个PC机的UDP端口,假定端口号为3001;STMNET的UDP默认端口号为3000,IP地址为192.168.1.115

3.1 程序结构
     在运行UDP程序之前,需要运行ARP,IP和ICMP各部分,并保存发起发的MAC地址和IP地址。
[cpp] view plain copy
  1.    // 获得新的IP报文  
  2.    plen = enc28j60_packet_receive( (BYTE*)&rxtx_buffer, MAX_RXTX_BUFFER );  
  3.    if(plen==0) return;  
  4.     
  5.    // 保存客服端的MAC地址  
  6.    memcpy ( (BYTE*)&client_mac, &rxtx_buffer[ ETH_SRC_MAC_P ], sizeof( MAC_ADDR) );  
  7.    // 检查该报文是不是ARP报文  
  8.    if ( arp_packet_is_arp( rxtx_buffer, (WORD_BYTES){ARP_OPCODE_REQUEST_V} ) )  
  9.    {  
  10.        // 向客户端返回ARP报文  
  11.        arp_send_reply ( (BYTE*)&rxtx_buffer, (BYTE*)&client_mac );  
  12.        return;  
  13.    }  
  14.     
  15.    // 保存客服端的IP地址  
  16.    memcpy ( (BYTE*)&client_ip, &rxtx_buffer[ IP_SRC_IP_P ], sizeof(IP_ADDR) );  
  17.    // 检查该报文是否为IP报文  
  18.    if ( ip_packet_is_ip ( (BYTE*)&rxtx_buffer ) == 0 )  
  19.    {  
  20.        return;  
  21.    }  
  22.     
  23.    // 如果是ICMP报文 向发起方返回数据  
  24.    if ( icmp_send_reply ( (BYTE*)&rxtx_buffer, (BYTE*)&client_mac, (BYTE*)&client_ip ) )  
  25.    {  
  26.        return;  
  27.    }  
  28.     
  29.    // 进行UDP处理  
  30. if (udp_receive ( (BYTE *)&rxtx_buffer, (BYTE *)&client_mac, (BYTE *)&client_ip ))  
  31. {  
  32.        return;  
  33. }  

3.2 源IP和源端口
    在udp_receive函数中需判断UDP端口号和目标IP地址是否匹配。若匹配则先获得UDP的负载长度,使用memcpy命令复制到udp_recbuf中,接着通过串口打印源IP地址(位于IP首部),源端口号(UDP首部)。
[cpp] view plain copy
  1.         // 获得UDP负载长度  
  2.         udp_loadlen = udp_get_dlength(rxtx_buffer);  
  3.         // 复制UDP接收  
  4.         memcpy(udp_recbuf, (char*)&rxtx_buffer[UDP_DATA_P], udp_loadlen);  
  5. #if UDP_DEBUG  
  6.         printf("UDP Message!\r\n");  
  7.         printf("Send Form:%d.%d.%d.%d ",\  
  8.                 rxtx_buffer[IP_SRC_IP_P+0],rxtx_buffer[IP_SRC_IP_P+1],\  
  9.                 rxtx_buffer[IP_SRC_IP_P+2],rxtx_buffer[IP_SRC_IP_P+3]);  
  10.         printf("Port:%d\r\n",(rxtx_buffer[UDP_SRC_PORT_H_P] << 8) | rxtx_buffer[UDP_SRC_PORT_L_P]);  
  11.         printf("Reccive:%s\r\n",udp_recbuf);  
  12. #endif  

STM32NET学习笔记 UDP部分_第1张图片
图1 网络调试助手

网络调试助手使用时,先设定协议类型为【UDP】,并修改本机端口号改为【3001】,点击【连接】。然后修改目标IP地址【192.168.1.115】,目标端口号改为【3000】,最后填入数据点击【发送】。
STM32NET学习笔记 UDP部分_第2张图片
图2 UDP调试输出结果
可以看出,PC机先发送UDP命令之前先发送了一个ARP请求,找出AVR的MAC地址,接着发送UDP数据包。这个实验可以验证UDP接收数据包正确。

3.3 返回Hello UDP
     接着稍微修改程序,接收到UDP数据包之后,在负载数据之前加入Hello字符,如果输入为xukai871105,则返回Hello xukai871105。可以通过网络调试助手看到返回结果。使用strcpy函数把Hello复制到udp_sendbuf数组中,接着使用strcat把udp_recbuf中的字符串连接到udp_sendbuf之后,最后调用udp_puts_date填充到发送缓冲区中,udp_puts_date的最后一个参数为UDP负载缓冲区的起始字节,第一次填入时应使用0
[cpp] view plain copy
  1. #if UDP_ECHO  
  2.         // Hello 范例  
  3.         // 复制Hello  
  4.         strcpy(udp_sendbuf,"UDP:Hello ");  
  5.         // 连接数据  
  6.         strcat(udp_sendbuf, udp_recbuf);  
  7.         dlength = udp_puts_data(rxtx_buffer, (BYTE*)udp_sendbuf, 0);  
  8. #endif  

STM32NET学习笔记 UDP部分_第3张图片
图3 UDP Hello Echo测试结果
    通过前面两个实验可以证明UDP的接收和发送工作正常。

3.4 LED控制
    验证了UDP的发送和接收,可以通过定义一组指令实现LED的控制。
    led,x,y
    x表示LED编号,取值范围为1或2
    y表示LED状态,1为打开,2为关闭

    具体代码如下:
[cpp] view plain copy
  1. #if UDP_LEDCTRL  
  2.         int match_count = 0;  
  3.         int led_index = 0;  
  4.         int led_status = 0;  
  5.         // 匹配led,x,y  
  6.         match_count = sscanf(udp_recbuf,"led,%d,%d", &led_index, &led_status);  
  7.         if(match_count == 2)  
  8.         {  
  9.             switch(led_index)  
  10.             {  
  11.             case 1:  
  12.                 led_status?BSP_LEDOn(1):BSP_LEDOff(1);  
  13.                 dlength = udp_puts_data(rxtx_buffer, (BYTE*)"LED1 Control OK\r\n", 0);  
  14.                 break;  
  15.             case 2:  
  16.                 led_status?BSP_LEDOn(2):BSP_LEDOff(2);  
  17.                 dlength = udp_puts_data(rxtx_buffer, (BYTE*)"LED2 Control OK\r\n", 0);  
  18.                 break;  
  19.             default:  
  20.                 dlength = udp_puts_data(rxtx_buffer, (BYTE*)"Invalid Led Index\r\n", 0);  
  21.                 break;  
  22.             }  
  23.         }  
  24.         else  
  25.         {  
  26.             dlength = udp_puts_data(rxtx_buffer, (BYTE*)"unknow command\r\n", 0);  
  27.         }  
  28. #endif   
  
STM32NET学习笔记 UDP部分_第4张图片
图4 通过UDP控制LED
4总结
     UDP是一个非常简答有效的协议。研究完UDP协议之后便可研究分析TCP协议,在熟悉的TCP协议的基础上才可实践HTTP制作WEB网页。使用嵌入式实践WEB服务器之前还需要练习静态网页动态网页的制作方法,熟悉HTTP请求格式等等工作。

你可能感兴趣的:(STM32NET学习笔记 UDP部分)