上一讲中了解了tftp的原理和代码的具体实现之后,这一讲具体对uboot客户端环境下的tftp升级进行说明。与之前自己实现的tftp客户端不同的是,uboot中还没有实现创建udp的接口,只是通过将传输信息必要的信息放入一定的字符数组位置中,然后将该字符数组放入网卡的发送缓冲区中,由网卡实现数据解析和发送。
接下来就uboot的tftp客户端实现和uboot怎么发送传输和升级进度进行详细的说明。
int cmd_tftp(int argc, char *argv[])
{
...
tftp:
if (net_tftp(ac, buf, byte_count, fname)) //调用net_tftp接口
return ERR_OK;
else
return ERR_FILE;
...
}
int net_tftp(int put, unsigned long dest_address, unsigned int bytecount, char *file)
{
img.addr = dest_address;
img.count = bytecount;
printf("tftp %s %s:%s \n", (put ? "put" : "get"),
inet_ntoa(&bootvars.server), file);
return ((tftp(file, (put ? upload_file : download_file)))? 1 : 0); //调用tftp接口
}
//uboot中具体实现tftp客户端的接口
int tftp(const char *name, int (*func) (unsigned char *, int, int, int))
{
static unsigned short iport = 2000;
unsigned short oport, len, block = 0, prevblock = 0, txblk = 0;
int bcounter = 0, retry = 0, timeout, result;
struct tftp_t *tr;
struct nbuf *pkt;
int packetsize = TFTP_DEFAULTSIZE_PACKET; //和前一章说明的一样,这里的数据包限定完整长度为TFTP_DEFAULTSIZE_PACKET,即512字节
unsigned long server = bootvars.server; //全局变量,这里表示的是uboot环境变量中设置的服务器IP
unsigned long data;
short put = (func == upload_file); //这里的put变量用于后续区分执行是上传文件还是下载文件
await_reply(AWAIT_QDRAIN, 0, 0);
tp.opcode = put ? htons(TFTP_WRQ) : htons(TFTP_RRQ); //前面讲过,TFTP_WRQ和TFTP_RRQ分别表示的是tftp协议中开始处的请求上传及请求下载选项
len = 1 + sizeof (tp.ip) + sizeof (tp.udp) + //发送必要信息:发送的总数据长度
sizeof (tp.opcode) + sprintf((char *) tp.u.rrq, "%s%coctet", name, 0);
//!!!
if (0 == udp_tx(server, ++iport, TFTP_PORT, len, &tp)) //!!!发现这里通过udp_tx进行信息发送,必要的发送信息包括:服务器地址,发送端口号,TFTP接收端端口号(69)以及发送信息;
return (0);
//在发送完上传请求或者下载请求后,进入接收服务器返回的信息并处理阶段
while (1)
{
timeout = rfc951_sleep(block ? TFTP_REXMT : TIMEOUT, retry);
data = (unsigned long) iport;
result = await_reply(AWAIT_TFTP, &data, timeout);
if (ESC == result)
return 0;
//超时,进入重发机制
if (0 == result)
{
if (!block && (MAX_TFTP_RETRIES > retry++))
{
if (0 == udp_tx(server, ++iport, TFTP_PORT, len, &tp))
return (0);
continue;
}
if (put)
goto retry;
if (block && ((retry += TFTP_REXMT) < TFTP_TIMEOUT))
{
udp_tx(server, iport, oport, TFTP_MIN_PACKET, &tp); /* re-send ack */
continue;
}
break;
}
//接收并解析数据
pkt = (struct nbuf *) data;
tr = (struct tftp_t *) &pkt->data[ETHER_HEAD_SZ];
if (ntohs(TFTP_ERROR) == tr->opcode) //服务器端返回错误
{
printf("Err %d (%s)\n", ntohs(tr->u.err.errcode), tr->u.err.errmsg);
break;
}
//download , ack
//下载文件处理部分:服务器端返回ACK包,表示服务器端已经接收到请求,可以继续下一步的执行
if (tr->opcode == ntohs(TFTP_OACK))
{
char *e, *p = tr->u.oack.data;
if (prevblock)
continue; /* skip */
len = ntohs(tr->udp.len) - UDPHDR_SZ - 2; //获取接收到的tftp整个包的长度,不大于1432字节(注意这里是整个包长度,不是数据域长度)
if (len > TFTP_MAX_PACKET)
goto noak;
e = p + len;
while (*p != '\0' && p < e)
{
//判断tftp包中的数据域长度是否符合<=512字节的标准
if (!strcasecmp("blksize", p))
{
p += 8;
if ((packetsize =
strtol(p, &p, 10)) < TFTP_DEFAULTSIZE_PACKET)
goto noak;
while (p < e && *p)
p++;
if (p < e)
p++;
}
else
{
noak:
tp.opcode = htons(TFTP_ERROR);
tp.u.err.errcode = 8;
len =
sizeof (tp.ip) + sizeof (tp.udp) + sizeof (tp.opcode) +
sizeof (tp.u.err.errcode) +
sprintf((char *) tp.u.err.errmsg, "RFC1782 error") + 1;
udp_tx(server, iport, ntohs(tr->udp.src), len, &tp);
return 0;
}
}
if (p > e)
goto noak;
block = tp.u.ack.block = 0;
} // request, response
else //data
if (tr->opcode == htons(TFTP_DATA)) //返回的是DATA数据选项
{
len = ntohs(tr->udp.len) - UDPHDR_SZ - 4;
if (len > packetsize) //数据域字节正确性判断
continue;
block = ntohs(tp.u.ack.block = tr->u.data.block);
}
else /* upload ack */ if (tr->opcode == ntohs(TFTP_ACK)) //文件上传处理(暂不详细说明)
{
static int prev_sent;
int upload_count;
block = ntohs(tr->u.ack.block);
if (block == txblk)
retry = 0;
else
{
printf("blk# dis-continuous txblk=%d, ack-blk=%d\n", txblk,
block);
len = prev_sent;
if (retry++ < 16)
{
img.addr -= prev_sent;
goto retry;
}
else
{
printf("max retry fail\n");
return 0;
}
}
if (0 >= img.count)
return 1;
txblk++;
tp.u.data.block = htons(txblk);
upload_count = (img.count >
TFTP_DEFAULTSIZE_PACKET) ? TFTP_DEFAULTSIZE_PACKET :
img.count;
img.count -= upload_count;
tp.opcode = htons(TFTP_DATA);
prev_sent = len = upload_count + sizeof (tp.ip) + sizeof (tp.udp) +
sizeof (tp.opcode) + sizeof (tp.u.data.block);
memcpy((char *) tp.u.data.download, (char *) img.addr, upload_count);
oport = ntohs(tr->udp.src);
retry:
udp_tx(server, iport, oport, len, &tp);
upload_file(0, 0, upload_count, 0);
continue;
}
if ((block || bcounter) && (block != prevblock + 1)) //块序号准确性判断(为防止丢包,发送的块序号会累加。前一章中有对此说明)
{ /* Block order should be continuous */
tp.u.ack.block = htons(block = prevblock);
}
tp.opcode = htons(TFTP_ACK); //回应答包(一发一答)
oport = ntohs(tr->udp.src);
udp_tx(server, iport, oport, TFTP_MIN_PACKET, &tp); //发送数据
if ((unsigned short) (block - prevblock) != 1)
continue;
prevblock = block;
retry = 0;
result = func(tr->u.data.download, ++bcounter, len, len < packetsize); //根据传入的函数指针参数,执行上传或下载数据处理
if (len < packetsize) {
#if defined(CONFIG_CMD_FLASH) && defined(CONFIG_FLASH_CMD_PROGRAM_AFTER_DOWNLOAD_FILE)
downloaded = 1;
#endif
return (1);
}
}
return (0);
}
可以看出,uboot和前面内核环境下的tftp客户端处理并没有太大区别,要有的话也只是发送方式不同,一个是建udp一个是存发送缓冲区,但说到底uboot的发送方式也是内核环境下的底层实现。
既然发现了uboot的发送,那么就顺便看下udp_tx接口的具体内容,从下面的代码可以发现,udp的发送还是需要ip头和udp头的。
//tftp中的传输的结构体,但udp_tx的接口不一定传的就是这个结构体,但其中的ip头,udp头和选项是必要的
struct tftpreq_t
{
struct iphdr ip;
struct udphdr udp;
unsigned short opcode;
union
{
char rrq[512];
struct
{
unsigned short block;
} ack;
struct
{
unsigned short errcode;
char errmsg[512 - 2];
} err;
struct
{
unsigned short block;
char download[TFTP_MAX_PACKET];
} data;
} u;
} __attribute__ ((packed));
//udp_tx发送接口
//tftp中的调用示例:udp_tx(server, ++iport, TFTP_PORT, len, &tp)
int udp_tx(unsigned long dip, unsigned int sp, unsigned int dp, int len, const void *buf)
{
struct udphdr *pudp;
//separate from original udp_transmit()
pudp = (struct udphdr *) ((char *) buf + IPHDR_SZ); //填充udp头前,预留一段位置给IP头
pudp->chksum = 0; //no checksum
pudp->dest = htons(dp); //填端口号
pudp->src = htons(sp); //填服务器端tftp的端口号(69)
pudp->len = htons(len - IPHDR_SZ); //填除ip头外的包长度
return ip_tx(dip, IP_UDP, len, buf); //进入下一阶段的字符填充
}
//ip发送接口
int ip_tx(unsigned long destip, char prot, int len, const void *buf)
{
struct iphdr *ip;
// struct udphdr *udp;
struct arprequest arpreq;
int arpentry, retry, timeout, result;
//进入ip头的填充
ip = (struct iphdr *) buf;
// udp = (struct udphdr *)((long)buf + sizeof(struct iphdr));
ip->verhdrlen = 0x45;
ip->service = 0;
ip->len = htons(len); //填充长度
ip->ident = 0;
ip->frags = 0;
ip->ttl = 64;
// ip->protocol = IP_UDP;
ip->protocol = prot; //参数中传下来的IP_UDP值是17
ip->chksum = 0;
ip->src.s_addr = arptable[ARP_CLIENT].ipaddr.s_addr; //填源地址
ip->dest.s_addr = destip; //填目的地址
ip->chksum = ~ipchksum((unsigned short *) buf, IPHDR_SZ);
/*
udp->src = htons(srcsock);
udp->dest = htons(destsock);
udp->len = htons(len - sizeof(struct iphdr));
udp->chksum = 0;
*/ //填充发送的数据
if (destip == IP_BROADCAST)
eth_tx(broadcast, IP, len, buf);
else
{
if (((destip & netmask) !=
(arptable[ARP_CLIENT].ipaddr.s_addr & netmask)) &&
arptable[ARP_GATEWAY].ipaddr.s_addr)
destip = arptable[ARP_GATEWAY].ipaddr.s_addr;
for (arpentry = 0; arpentry < MAX_ARP; arpentry++)
if (arptable[arpentry].ipaddr.s_addr == destip)
break;
if (arpentry == MAX_ARP)
{
arptable[--arpentry].ipaddr.s_addr = destip;
memset(arptable[arpentry].node, 0, 6);
}
if ( !memcmp(arptable[arpentry].node, zeros, 6))
{
arpreq.hwtype = htons(1);
arpreq.protocol = htons(IP);
arpreq.hwlen = ETHER_ADDR_SZ;
arpreq.protolen = 4;
arpreq.opcode = htons(ARP_REQUEST);
memcpy(arpreq.shwaddr, arptable[ARP_CLIENT].node, ETHER_ADDR_SZ);
memcpy(arpreq.sipaddr,
&arptable[ARP_CLIENT].ipaddr, sizeof (in_addr));
memset(arpreq.thwaddr, 0, ETHER_ADDR_SZ);
memcpy(arpreq.tipaddr, &destip, sizeof (in_addr));
for (retry = 1; retry <= MAX_ARP_RETRIES; retry++)
{
eth_tx(broadcast, ARP, sizeof (arpreq), &arpreq);
timeout = rfc951_sleep(TIMEOUT, retry);
if (ESC == (result = await_reply(AWAIT_ARP, &arpentry, timeout)))
return 0;
else if (1 == result)
goto xmit;
}
return (0);
}
xmit:
eth_tx(arptable[arpentry].node, IP, len, buf); //将填充好的数据存入buf中,然后当做参数传给eth_tx接口进行发送
}
return (1);
}
void eth_tx(const char *d, unsigned int t, unsigned int s, const void *p)
{
...
result = dev->hard_start_xmit(pkt, dev); //由设备驱动执行hard_start_xmit接口进行发送
...
}
结合上面介绍的udp_tx接口,就可以很容易仿造出uboot环境下的网络传输数据的方法了。接下来就通过实现一个从uboot端向服务端传输当前boot下擦除文件和烧写文件进度的功能来实质性地了解uboot的网络传输。
首先,从上面对udp_tx的接口分析发现在uboot中执行网络传输的流程其实只是将填有ip头和udp头的数据放入一个buffer中,然后再将buffer传入eth_tx,最后给设备驱动接口hard_start_xmit执行发送。但由于之前tftp的结构体tftpreq_t 中有很多我们普通传输不需要关心的内容,所以我们也可以在了解tftpreq_t 中哪些成员必要,哪些成员可以省略的前提下自己创建一个结构体并通过udp_tx传输。
观察之前能成功发送的tftp结构体:
struct tftpreq_t
{
struct iphdr ip; //ip头,组包过程中必要成员内容
struct udphdr udp; //udp头,组包过程中必要成员内容
unsigned short opcode; //包类型,在tftp中使用,可忽略
union
{
char rrq[512]; //包数据,可忽略自己创建
struct
{
unsigned short block;
} ack; //ack包内容,可忽略
struct
{
unsigned short errcode;
char errmsg[512 - 2];
} err; //err包内容,可忽略
struct
{
unsigned short block;
char download[TFTP_MAX_PACKET];
} data; //data包内容,可忽略
} u;
} __attribute__ ((packed));
由此可得,上面的结构体中仅ip头和udp是必要的,他们用于填充网络传输中必要的ip地址和端口号等。所以如果需要在uboot中自己创建一个发送擦除和烧写进度的结构体,就可以这样实现。
struct upgrade_schedule_t
{
struct iphdr ip; //ip头
struct udphdr udp; //udp头
char erase_schedule; //指示擦除进度
char write_schedule; //指示烧写进度
} __attribute__ ((packed));
有了发送的结构体后,接下来的发送就可以变得很简单了,具体程序如下代码所示:
int send_schedule_to_server(int eraseSchedule, int writeSchedule)
{
unsigned long server = bootvars.server; //服务端ip地址
unsigned short iport = 2100; //发送端口号
struct upgrade_schedule_t schedule; //自己建的发送结构体
memset(&schedule, 0, sizeof(schedule));
int ret = 0;
int txLen = 1 + sizeof(schedule.ip) + sizeof(schedule.udp) + 2; //发送长度,这里1个字节暂不知为什么会加 + ip头长度 + udp头长度 + 1个表示擦除的字节 + 1个表示烧写的字节
schedule.erase_schedule = eraseSchedule; //赋值擦除进度
schedule.write_schedule = writeSchedule; //赋值烧写进度
ret = udp_tx(server, ++iport, TFTP_RETURN_PORT, txLen, &schedule); //调用udp_tx接口发送
return ret;
}
至此,服务端也创建一个服务端,对应uboot的发送接口,这里服务端对应接收的端口号是TFTP_RETURN_PORT端口。这样uboot在调用该send_schedule_to_server接口时服务端就能正确接收到了。