一、明确背景
uboot中有许多通信协议,像TFTP、NFS等,这些协议底层都是基于UDP协议来实现的,由于有一个板子在 uboot 段进行固件下载更新的需求,本来想基于TCP协议来实现自定义通信协议(TCP有自带的拥塞控制和重传机制)。但是,uboot底层并不支持TCP协议栈,且TCP协议栈的默认超时重传时间过长,移植TCP协议栈到uboot中难度也是相当大。最后结合需求考虑下来,决定基于UDP协议来实现自定义通信协议。第一步,要先验证UDP协议能否走通(不用想大概率是可以的,因为uboot中支持的其他协议底层也是基于UDP的,但还是走一遍验证一下,也是为后面的自定义通信协议打好基础)
抓住一个思想:TFTP底层就是基于UDP的,有什么不清楚的地方就看uboot中的TFTP源码,往往会指出些方向。
二、代码实现
首先在common/cmd_net.c中添加如下代码,这一步的目的是在uboot的命令中添加一个名为udp的命令,当我们在控制台输入udp后便会执行do_udp函数,然后逐层往下调用其他函数
/*my UDP*/ int do_udp(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[]) { int ret; ret = netboot_common_udp(UDP, cmdtp, argc, argv); return ret; } U_BOOT_CMD( udp, 6, 1, do_udp, "send or recv message to from server using UDP protocol", "[udp]" );
接着继续在该文件中定义这样一个函数,该函数是用来处理uboot控制台输入的具体命令参数的
/*my netboot_common*/ static int netboot_common_udp(enum proto_t proto, cmd_tbl_t *cmdtp, int argc, char *argv[]) { int rcode = 0; int size; switch (argc) { case 1: break; case 2: pkt_data = argv[1]; break; default: bootstage_error(BOOTSTAGE_ID_NET_START); return CMD_RET_USAGE; } bootstage_mark(BOOTSTAGE_ID_NET_START); if ((size = NetLoop(proto)) < 0) { bootstage_error(BOOTSTAGE_ID_NET_NETLOOP_OK); return 1; } bootstage_mark(BOOTSTAGE_ID_NET_NETLOOP_OK); /* NetLoop ok, update environment */ netboot_update_env(); /* done if no file was loaded (no errors though) */ if (size == 0) { bootstage_error(BOOTSTAGE_ID_NET_LOADED); return 0; } if (rcode < 0) bootstage_error(BOOTSTAGE_ID_NET_DONE_ERR); else bootstage_mark(BOOTSTAGE_ID_NET_DONE); return rcode; }
这个函数调用了net/net.c中的net_loop()函数,到这里本路径下的修改工作已完成。跳转到net/net.c中,在netloop函数的switch中添加case:UDP
#ifdef CONFIG_CMD_TFTPSRV case TFTPSRV: TftpStartServer(); break; #endif case UDP: UdpStart(); //my udp break; #if defined(CONFIG_CMD_DHCP) case DHCP: BootpReset(); NetOurIP = 0; DhcpRequest(); /* Basically same as BOOTP */ break; #endif
还需要在include/net.h中添加UDP协议的声明
注意在net.h中要声明该变量是定义在别处的,这样编译器就能知道pkt_data(用来接终端输入的待传数据的)是一个在其他地方定义的全局变量,从而允许在common.c中正确地访问和使用这个变量。
至此UDP协议支持的相关配置工作已经完成,接下来要自己在net目录下写一个UDP协议的服务函数udp.c和udp.h
#include <common.h> #include <command.h> #include <net.h> #include "tftp.h" #include "bootp.h" #include <flash.h> #include "udp.h" uchar *pkt_data; static int UdpPktLen; static IPaddr_t UdpServerIP; static int UdpServerPort; /* The UDP port at their end */ static int UdpOurPort; /* The UDP port at our end */ static int UdpTimeoutCount; static void UdpSend (void); /**********************************************************************/ static void UdpSend (void) { uchar *pkt; //指向待发送数据包的指针 int len = 0; //数据包长度 pkt = NetTxPacket + NetEthHdrSize() + IP_UDP_HDR_SIZE; // printf("NetEthHdrSize() = %d\n", NetEthHdrSize()); // printf("IP_UDP_HDR_SIZE = %d\n", IP_UDP_HDR_SIZE); len = strlen(pkt_data); memcpy(pkt, pkt_data, len); printf("pkt_data = %s,len = %d\n", pkt_data, len); // printf("pkt = %s\n", pkt); NetSendUDPPacket(NetServerEther, UdpServerIP, UdpServerPort, UdpOurPort, len); } static void UdpHandler (uchar *pkt, unsigned dest, IPaddr_t UdpServerIP, unsigned src, unsigned len) { printf("---- receive udp packet ----\n"); printf("len = %d\n",len); printf("data = %s\n", pkt); UdpSendAck(); } void UdpStart (void) { char *ep; /* Environment pointer */ if ((ep = getenv("serverip")) != NULL) { printf("ep = %s\n", ep); UdpServerIP = string_to_ip((const char *)ep); } #if defined(CONFIG_NET_MULTI) printf ("Using %s device\n", eth_get_name()); #endif net_set_udp_handler(UdpHandler); // 设置UDP数据包处理函数为UdpHandler UdpServerPort = 50000; //主机端口号; UdpOurPort = 30000; memset(NetServerEther, 0, 6); //如果服务器IP地址发生了变化,清零服务器以太网地址(MAC地址) UdpSend (); }
#ifndef __UDP_H__ #define __UDP_H__ extern void UdpStart (void); #endif
别忘了在net目录下的Makefile中添加编译依赖项,保证udp.c被编译进镜像文件
至此uboot源码中的相关工作已经完成,接下来进行调试验证
写两个上位机程序来进行数据的收和发
收
#include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> int main(void) { int sockfd = 0; char tmpbuff[1024] = {0}; struct sockaddr_in recvaddr; size_t nsize = 0; int ret = 0; sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (-1 == sockfd) { perror("fail to socket\n"); return -1; } recvaddr.sin_family = AF_INET; recvaddr.sin_port = htons(50000); recvaddr.sin_addr.s_addr = INADDR_ANY; ret = bind(sockfd, (struct sockaddr *)&recvaddr, sizeof(recvaddr)); if (ret == -1) { perror("fail to bind"); return -1; } nsize = recvfrom(sockfd, tmpbuff, sizeof(tmpbuff), 0, NULL, NULL); if (nsize == -1) { perror("fail to recvfrom"); return -1; } printf("接收 %ld 个字节\n", nsize); printf("Recv: %s\n", tmpbuff); close(sockfd); }
发
#include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> int main(void) { int sockfd = 0; char tmpbuff[1024] = {0}; struct sockaddr_in recvaddr; size_t nsize = 0; sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (-1 == sockfd) { perror("fail to socket\n"); return -1; } gets(tmpbuff, strlen(tmpbuff), stdin); recvaddr.sin_family = AF_INET; recvaddr.sin_port = htons(30000); recvaddr.sin_addr.s_addr = inet_addr("172.31.13.207"); nsize = sendto(sockfd, tmpbuff, strlen(tmpbuff), 0, (struct sockaddr *)&recvaddr, sizeof(recvaddr)); if (nsize == -1) { perror("fail to sendto"); return -1; } printf("发送 %ld 个字节\n", nsize); close(sockfd); }
三、测试
在uboot中发,上位机来收
在上位机中发,uboot中来收
极限性能测试
测试验证发发现,在不经过网络转发设备的前提下能跑满UDP协议理论最大包长1472字节。
四、补充说明
以上只是简单在uboot中走通了UDP协议,故代码中不涉及待传数据的帧格式设计、重传机制、超时机制等,若要实现基于UDP协议自定义通信协议,还需要设计好以上几点。这个后面再说。
注:uboot版本为2017年