自己动手编写嵌入式Bootloader之(2)
2011-04-03 13:29
399 查看
第二部分:通过网口下载内核映像
要
实现通过网口下载文件的功能,从底层到上层需要做的工作包括:开发板上的网卡芯片的驱动程序;TCP/IP协议栈的实现;TFTP客户端应用程序的实现。
我们使用的OK2440开发板配备CS8900A网卡芯片。
为了简单起见,网络数据包的发送和接收都使用轮询方式,不使用中断;协议栈只使用ARP/IP/UDP协议,不涉及TCP及其他协议;应用程序只实现最简
单的TFTP客户端。
1. 全局配置信息
发送和接收的数据缓冲区,使用全局静态缓冲区,不使用动态内存分配。第一阶段运行结束之后,CPU内部4KB的SteppingStone可以用作其它用途,我们就用它做网络数据接收、发送的缓冲区。亦可用作标准输入输出的缓冲区。
unsigned char *TxBuf = (unsigned char *)0;
unsigned char *RxBuf = (unsigned char *)1024;
使用若干个全局变量来保存网络配置信息:
unsigned char NetOurEther[6] = /* Our ethernet address */
{0x00, 0x09, 0x58, 0xD8, 0x11, 0x22};
开发板的MAC地址,这个是任意设置的。
unsigned char NetServerEther[6] = /* Boot server enet address */
{0x00, 0x14, 0x2A, 0xA5, 0x50, 0x97};
服务器也就是主机的MAC地址,这个要跟主机MAC一致,可以在主机上运行ifconfig命令查到。
unsigned long NetOurIP = 0xC0A801FC; /* Our IP addr 192.168.1.252 */
unsigned long NetServerIP = 0xC0A801F9; /* Server IP 192.168.1.249 */
网络协议中IP地址一般是用一个4字节整型数表示的。
2. CS8900A以太网驱动程序
硬件电路决定了CS8900的物理地址是在BANK3的区间内,CS8900是16位的寄存器,故我们设置BANK3的BUS WIDTH也为16位。设置BANK3: 总线宽度16,使能nWait,使能UB/LB
BANKCON3:0x1F7C
网卡CS8900的访问基址为0x19000000,之所以再偏移0x300是由它的特性决定的
#define CS8900_BASE 0x19000300
CS8900
读写寄存器的方式有些特别。要读一个寄存器,先向CS8900_PPTR中写入该寄存器地址,再从CS8900_PDATA中读出该寄存器值;要写一个寄
存器,先向CS8900PPTR中写入该寄存器地址,再向CS8900_PDATA中写入要写入的值。不管是寄存器地址还是要读写的数值,都是16位的,
也就是说都是unsigned short类型的。因此,读写寄存器的函数如下:
读芯片ID: CS8900的芯片ID存放在PP_ChipID寄存器中,读该寄存器得到的正确值应该是0x630E,这可以初步判断一些地址/引脚的设置是否正确,如果读出的不是0x630E,那么CS8900肯定不能正常工作。
设置MAC地址:
MAC地址并不是固定的,可以由我们随意设置。从寄存器PP_IA开始的6个字节存放MAC地址。比如下面的代码把MAC地址设为 00 09 58 D8 11 22:
因为是Little Endian, 所以0x09<<8, 但是在寄存器内存中还是 0x00放在前面。
寄存器初始化: 设置CS8900的工作模式
发送数据包:
int eth_send (volatile void *packet, int length)
两个参数:要发送的数据包首地址、长度
TxCMD
和TxLen寄存器用来初始化数据包的发送,其具体含义见CS8900数据手册第70页。这里PP_TxCmd_TxStart_Full被定义为
0x00C0,表示直到整个数据侦都加载到CS8900内部缓存之后才开始发送,数据侦的长度为CS8900_TxLEN.
使用TxCMD下达发送数据的命令后,再读取 PP_BusSTAT 总线状态寄存器判断是否做好发送数据的准备。当get_reg
(PP_BusSTAT) & PP_BusSTAT_TxRDY 不等于零时表示可以发送了。 使用一个循环进行实际的发送操作:
这里 addr 也是unsigned short类型的指针, 每次向CS8900_RTDATA写入两个字节数据。这里假设要发送的数据包长度为偶数。
最后,通过读取PP_TER寄存器可以知道是否发送完毕,是否发送成功。
接收数据包:
首先,通过读取PP_RER寄存器判断是否接收到数据。如果接收到数据,则连续两次读取 CS8900_RTDATA 的值,
status = CS8900_RTDATA; /* stat */
rxlen = CS8900_RTDATA; /* len */
rxlen 为接收到的数据长度。
然后用一个循环连续读取 rxlen 长度的数据:
其中 RxBuf 为预先在内存中开辟的一块接收缓冲区。 每次循环读取两个字节,还需要处理长度为奇数的情况。
最后,把RxBuf交给上层的协议处理:net_receive( &RxBuf[0], rxlen );
3. Ethernet MAC层协议的实现
上层的数据包(如IP包、ARP包)到来时,需要添加一个14字节的MAC头, 然后再交给网卡发送出去。 MAC头包含目的MAC地址、源MAC地址、协议类型三个字段。如下图所示。数据包末尾的CRC校验我们不使用。
使用下面的代码填充MAC头。其中协议类型,对IP为0x0800, 对ARP为0x0806
4. ARP协议的实现
一般的方式是建立一个全局的
ARP映射缓存表,随着系统的运行不断查找、更新该表。但是我们要完成的功能仅仅是从
TFTP服务器下载内核和文件系统映像,而服务器的
IP和
MAC地址都是固定的,因此可以简化
ARP映射表,只用两个变量分别保存服务器
IP和
MAC,再用两个变量保存开发板
IP和
MAC即可。并且更新映射表的功能也可以省略,只在系统初始化时把这四个地址都设置好,使用过程中不会发生改变,所以不需要更新。这样,我们的
ARP协议只需要完成接受
ARP请求、发送
ARP应答的功能,而发送
ARP请求和接受
ARP应答的功能可以省略,这样大大简化了协议栈的设计。
按照维基百科上的介绍(http://en.wikipedia.org/wiki/Address_Resolution_Protocol),ARP
是一个数据链路层协议,(我感觉它应该是网络层的协议),它的作用是在只知道一个主机网络层IP地址的情况下找到它的硬件地址。在以太网上,它主要用来把
IP地址转换为以太网MAC地址。由于是链路层协议,ARP的作用范围仅限于本地局域网。
ARP数据包长度为28字节,其中各字节的含义如下图所示:
对各个段作简单的解释:
Hardware type (HTYPE) 每个数据链路层协议都被分配到一个数,比如,Ethernet 是 1
Protocol type (PTYPE) 在这个域,每个网络层协议都被分配到一个数(标号),比如,IP是0x0800
Hardware length (HLEN) 硬件地址的长度。以太网Ethernet的MAC地址长度是6个字节
Protocol length (PLEN) 维基上写的是“逻辑地址”的长度,其实也就是网络层地址的长度。IPv4地址的长度为4个字节。
Operation 表明发送者的操作,也就是数据包的类型:1表示ARP请求;2表示ARP回应;3表示RARP请求;4表示RARP回应。
Sender hardware address (SHA) 发送者的硬件地址
Sender protocol address (SPA) 发送者的协议地址,也就是发送者IP地址。
Target hardware address (THA) 目标接收者的硬件MAC地址。如果是ARP请求,这个域被忽略。
Target protocol address (TPA) 目标接收者的IP地址。
知道了包结构,我们就可以设计一个结构体:
属性 __attribute__((packet)) 告诉编译器使用紧缩方式存放结构体内容(1 Byte align),
不使用默认的4字节对齐, 这样就不会产生冗余字节。此时的 sizeof(struct arp_header) = 28。
如果不加packed属性, 运行 sizeof(struct arp_header) 得到 32, 而不是 28。 数据段就产生了错位。
前面已经说过,我们只实现接收ARP请求并发送ARP应答的功能,因此只用一个简单的函数就可实现:
接收到的数据保存在pRx地址处,要发送的数据地址指定为pTx位于发送缓冲区中。如果接收到的是ARP请求包并且IP地址也符合,则在pTx处构造一个ARP应答包并交给mac_send()发送出去。
5. IP协议的实现
IP数据包的格式如下表所示:
IP协议的简化:
IP协议在网络中主要完成路由选择和网络分段的功能
。起始
Bit 0-3表示版本号,对
IPv4来说取值为
4即
0100即可。
Header length域指明
IP数据包
header的长度(不包括数据
Data域),以四字节为单位,因为
Options域是可选的所以
IP Header的长度并不固定。我们不使用
Option域,所以取最小值
5,表示
Header长度为
20字节。服务类型域(
Type of Service, TOS)是为特殊的应用如
VoIP等保留的,我们不使用,赋值为零即可。接下来
2个字节的
Total Length域表示整个数据包的长度,包括
Header和
Data,以字节为单位。
标识域(
Identification)用来给数据包一个唯一的编号,用于验证和跟踪等,我们不使用,直接赋值为零即可。
Flags和
Offset用于分段包的重组,我们不使用,把
Flags的第
2位设为
1表示是不可分段的,
Offset赋值为零即可。生存时间(
Time to Live, TTL)表示该数据包在网络上的有效期,我们简单的把它设为最大值
0xFF即可。协议域(
Protocol)表示传输层使用什么协议,
RFC790文档为每个协议都规定了唯一的编号,如
UDP编号为
17。
Header Checksum为
Header区域的校验和,在校验之前该域初始为
0,然后计算整个头部的校验和,把结果存放在该域,计算校验的方法是把头部看成以
16位为单位的数字组成,依次进行二进制反码求和。接下来的八个字节是源
IP地址和目的
IP地址,没什么可说的。
综上所述,我们只保留了
IP协议中必须的关键字段,因而简化了设计,对
IP数据包进行填充的代码段如下:
CheckSum 校验和:
IP,TCP,UDP等许多协议的头部都设置了校验和项,它们采用的算法是一样的,将被校验的数据按16位进行划分(若数据字节长度为奇数,则在数据尾部补一个字节0),对每16位求反码和,然后再对和取反码。
代码如下:
6. UDP协议的实现
在传输层我们抛弃了复杂的
TCP协议而使用简单的
UDP协议。虽然
UDP是无连接的协议,它不保证数据包一定能够到达目的主机,但是在嵌入式开发中,开发板跟主机通常位于同一内部局域网内,网络环境良好,数据丢失的可能性很小,并且
UDP容易实现,占用资源小,因此更适合于嵌入式环境。
UDP头部包含了可选的校验和字段,而校验要涉及到伪报头,为了简化设计和减小开销,我们不使用校验,直接把该字段设为零,表示不使用校验。UDP包填充代码如下:
关于源端口号和目的端口号的设定,在TFTP实现时会详细说明。
7. TFTP客户端的实现
tftp是一个很简单的文件传输协议,在传输层使用UDP协议。它有四种类型的包: 读请求RRQ包,DATA包,ACK包,ERROR包,每个包的前两个字节Opcode指定包的类型。(RRQ用于请求下载,WRQ用于请求上传,我们只用到RRQ)。
下载文件的过程分析如下:
客户端(A)从任意端口X向服务器(S)的端口69发送一个RRQ包,该包中指明了要求下载的文件名;服务器(S)找到该文件,读取文件内容组成DATA
包,从任意端口Y向客户端(A)的端口X发送这个DATA包,第一个DATA包编号为1;从此以后,客户端确定使用端口X,服务器确定使用端口Y,
客户端向服务器发送ACK包,编号为1。服务器接到编号为1的ACK包之后,发送第二个DATA包,如此继续下去。
怎样判断传输结束呢? 按照规定,DATA包中的数据段为512字节, 如果小于512字节,表示这是最后一个DATA包,文件已传输完毕。
(R1) Host A requests to read
(R2) Server S sends data packet 1
(R3) Host A acknowledges data packet 1
注意在这个过程中端口的变化。开始RRQ是69,但是DATA和ACK都不是使用69,而是使用另外一个随机的端口。 服务器在接到RRQ后,不返回任何回应信息,直接发送第一个DATA包,而且DATA包编号从1开始,而不是从0开始。
编程时为简单起见,客户端使用了固定的端口号X=0x8DA4,服务器端口号Y是随机的,只能通过解析UDP数据包获得。
要
实现通过网口下载文件的功能,从底层到上层需要做的工作包括:开发板上的网卡芯片的驱动程序;TCP/IP协议栈的实现;TFTP客户端应用程序的实现。
我们使用的OK2440开发板配备CS8900A网卡芯片。
为了简单起见,网络数据包的发送和接收都使用轮询方式,不使用中断;协议栈只使用ARP/IP/UDP协议,不涉及TCP及其他协议;应用程序只实现最简
单的TFTP客户端。
1. 全局配置信息
发送和接收的数据缓冲区,使用全局静态缓冲区,不使用动态内存分配。第一阶段运行结束之后,CPU内部4KB的SteppingStone可以用作其它用途,我们就用它做网络数据接收、发送的缓冲区。亦可用作标准输入输出的缓冲区。
unsigned char *TxBuf = (unsigned char *)0;
unsigned char *RxBuf = (unsigned char *)1024;
使用若干个全局变量来保存网络配置信息:
unsigned char NetOurEther[6] = /* Our ethernet address */
{0x00, 0x09, 0x58, 0xD8, 0x11, 0x22};
开发板的MAC地址,这个是任意设置的。
unsigned char NetServerEther[6] = /* Boot server enet address */
{0x00, 0x14, 0x2A, 0xA5, 0x50, 0x97};
服务器也就是主机的MAC地址,这个要跟主机MAC一致,可以在主机上运行ifconfig命令查到。
unsigned long NetOurIP = 0xC0A801FC; /* Our IP addr 192.168.1.252 */
unsigned long NetServerIP = 0xC0A801F9; /* Server IP 192.168.1.249 */
网络协议中IP地址一般是用一个4字节整型数表示的。
2. CS8900A以太网驱动程序
硬件电路决定了CS8900的物理地址是在BANK3的区间内,CS8900是16位的寄存器,故我们设置BANK3的BUS WIDTH也为16位。设置BANK3: 总线宽度16,使能nWait,使能UB/LB
BANKCON3:0x1F7C
网卡CS8900的访问基址为0x19000000,之所以再偏移0x300是由它的特性决定的
#define CS8900_BASE 0x19000300
CS8900
读写寄存器的方式有些特别。要读一个寄存器,先向CS8900_PPTR中写入该寄存器地址,再从CS8900_PDATA中读出该寄存器值;要写一个寄
存器,先向CS8900PPTR中写入该寄存器地址,再向CS8900_PDATA中写入要写入的值。不管是寄存器地址还是要读写的数值,都是16位的,
也就是说都是unsigned short类型的。因此,读写寄存器的函数如下:
static unsigned short get_reg ( int regno) { CS8900_PPTR = regno; return CS8900_PDATA; } static void put_reg ( int regno, unsigned short val) { CS8900_PPTR = regno; CS8900_PDATA = val; } |
设置MAC地址:
MAC地址并不是固定的,可以由我们随意设置。从寄存器PP_IA开始的6个字节存放MAC地址。比如下面的代码把MAC地址设为 00 09 58 D8 11 22:
put_reg ( PP_IA + 0, 0x00 | 0x09 < < 8) ; put_reg ( PP_IA + 2, 0x58 | 0xD8 < < 8) ; put_reg ( PP_IA + 4, 0x11 | 0x22 < < 8) ; |
寄存器初始化: 设置CS8900的工作模式
/* 只接收目标地址为本网卡的无错误数据包 */ put_reg ( PP_RxCTL, PP_RxCTL_IA | PP_RxCTL_Broadcast | PP_RxCTL_RxOK) ; /* 当进行接收操作时,不要产生任何中断 */ put_reg ( PP_RxCFG, 0) ; /* 当进行发送操作时,不要产生任何中断 */ put_reg ( PP_TxCFG, 0) ; /* 当进行缓存操作时,不要产生任何中断 */ put_reg ( PP_BufCFG, 0) ; /* 使能发送和接收模式 */ put_reg ( PP_LineCTL, PP_LineCTL_Rx | PP_LineCTL_Tx) ; |
int eth_send (volatile void *packet, int length)
两个参数:要发送的数据包首地址、长度
TxCMD
和TxLen寄存器用来初始化数据包的发送,其具体含义见CS8900数据手册第70页。这里PP_TxCmd_TxStart_Full被定义为
0x00C0,表示直到整个数据侦都加载到CS8900内部缓存之后才开始发送,数据侦的长度为CS8900_TxLEN.
/* initiate a transmit sequence */ CS8900_TxCMD = PP_TxCmd_TxStart_Full; CS8900_TxLEN = length; |
(PP_BusSTAT) & PP_BusSTAT_TxRDY 不等于零时表示可以发送了。 使用一个循环进行实际的发送操作:
for ( addr = packet; length > 0; length - = 2) { CS8900_RTDATA = * addr+ + ; } |
最后,通过读取PP_TER寄存器可以知道是否发送完毕,是否发送成功。
接收数据包:
首先,通过读取PP_RER寄存器判断是否接收到数据。如果接收到数据,则连续两次读取 CS8900_RTDATA 的值,
status = CS8900_RTDATA; /* stat */
rxlen = CS8900_RTDATA; /* len */
rxlen 为接收到的数据长度。
然后用一个循环连续读取 rxlen 长度的数据:
for ( addr = ( unsigned short * ) & RxBuf[ 0] , i = rxlen > > 1; i > 0; i- - ) * addr+ + = CS8900_RTDATA; if ( rxlen & 1) * addr+ + = CS8900_RTDATA; |
最后,把RxBuf交给上层的协议处理:net_receive( &RxBuf[0], rxlen );
3. Ethernet MAC层协议的实现
上层的数据包(如IP包、ARP包)到来时,需要添加一个14字节的MAC头, 然后再交给网卡发送出去。 MAC头包含目的MAC地址、源MAC地址、协议类型三个字段。如下图所示。数据包末尾的CRC校验我们不使用。
使用下面的代码填充MAC头。其中协议类型,对IP为0x0800, 对ARP为0x0806
struct mac_header * p = ( struct mac_header* ) ( buf) ; memcpy ( p- > dest, NetServerEther, 6) ; memcpy ( p- > src, NetOurEther, 6) ; p- > proto = htons ( proto) ; |
一般的方式是建立一个全局的
ARP映射缓存表,随着系统的运行不断查找、更新该表。但是我们要完成的功能仅仅是从
TFTP服务器下载内核和文件系统映像,而服务器的
IP和
MAC地址都是固定的,因此可以简化
ARP映射表,只用两个变量分别保存服务器
IP和
MAC,再用两个变量保存开发板
IP和
MAC即可。并且更新映射表的功能也可以省略,只在系统初始化时把这四个地址都设置好,使用过程中不会发生改变,所以不需要更新。这样,我们的
ARP协议只需要完成接受
ARP请求、发送
ARP应答的功能,而发送
ARP请求和接受
ARP应答的功能可以省略,这样大大简化了协议栈的设计。
按照维基百科上的介绍(http://en.wikipedia.org/wiki/Address_Resolution_Protocol),ARP
是一个数据链路层协议,(我感觉它应该是网络层的协议),它的作用是在只知道一个主机网络层IP地址的情况下找到它的硬件地址。在以太网上,它主要用来把
IP地址转换为以太网MAC地址。由于是链路层协议,ARP的作用范围仅限于本地局域网。
ARP数据包长度为28字节,其中各字节的含义如下图所示:
对各个段作简单的解释:
Hardware type (HTYPE) 每个数据链路层协议都被分配到一个数,比如,Ethernet 是 1
Protocol type (PTYPE) 在这个域,每个网络层协议都被分配到一个数(标号),比如,IP是0x0800
Hardware length (HLEN) 硬件地址的长度。以太网Ethernet的MAC地址长度是6个字节
Protocol length (PLEN) 维基上写的是“逻辑地址”的长度,其实也就是网络层地址的长度。IPv4地址的长度为4个字节。
Operation 表明发送者的操作,也就是数据包的类型:1表示ARP请求;2表示ARP回应;3表示RARP请求;4表示RARP回应。
Sender hardware address (SHA) 发送者的硬件地址
Sender protocol address (SPA) 发送者的协议地址,也就是发送者IP地址。
Target hardware address (THA) 目标接收者的硬件MAC地址。如果是ARP请求,这个域被忽略。
Target protocol address (TPA) 目标接收者的IP地址。
知道了包结构,我们就可以设计一个结构体:
struct arp_header{ unsigned short ar_hrd; /* Format of hardware address */ unsigned short ar_pro; /* Format of protocol address */ unsigned char ar_hln; /* Length of hardware address */ unsigned char ar_pln; /* Length of protocol address */ unsigned short ar_op; /* Operation */ unsigned char ar_sha[ 6] ; /* Sender hardware address */ unsigned long ar_spa; /* Sender protocol address */ unsigned char ar_tha[ 6] ; /* Target hardware address */ unsigned long ar_tpa; /* Target protocol address */ } __attribute__ ( ( packed) ) ; |
不使用默认的4字节对齐, 这样就不会产生冗余字节。此时的 sizeof(struct arp_header) = 28。
如果不加packed属性, 运行 sizeof(struct arp_header) 得到 32, 而不是 28。 数据段就产生了错位。
前面已经说过,我们只实现接收ARP请求并发送ARP应答的功能,因此只用一个简单的函数就可实现:
static int arp_handle( unsigned char * buf, unsigned int len ) { struct arp_header * pRx, * pTx; pRx = ( struct arp_header * ) ( buf) ; pTx = ( struct arp_header * ) & TxBuf[ 256] ; switch ( htons ( pRx- > ar_op) ) { case ARP_REQUEST: if ( pRx- > ar_tpa = = htonl ( NetOurIP) ) { pTx- > ar_hrd = htons ( 0x01) ; pTx- > ar_pro = htons ( PROTO_IP) ; pTx- > ar_hln = 0x06; pTx- > ar_pln = 0x04; pTx- > ar_op = htons ( ARP_REPLY) ; memcpy ( pTx- > ar_sha, NetOurEther, 6) ; pTx- > ar_spa = htonl ( NetOurIP) ; memcpy ( pTx- > ar_tha, pRx- > ar_sha, 6) ; pTx- > ar_tpa = pRx- > ar_spa; mac_send( ( unsigned char * ) pTx, sizeof ( struct arp_header) , PROTO_ARP) ; } break ; case ARP_REPLY: printf ( "/n/rGot ARP reply/n" ) ; break ; default : printf ( "/n/r ar_op Not Support./n" ) ; break ; } return 0; } |
5. IP协议的实现
IP数据包的格式如下表所示:
+ | Bits 0–3 | 4–7 | 8–15 | 16–18 | 19–31 |
0 | Version | Header length | Type of Service | Total Length | |
32 | Identification | Flags | Fragment Offset | ||
64 | Time to Live | Protocol | Header Checksum | ||
96 | Source Address | ||||
128 | Destination Address | ||||
160 | Options | ||||
160 or 192+ | Data |
IP协议在网络中主要完成路由选择和网络分段的功能
。起始
Bit 0-3表示版本号,对
IPv4来说取值为
4即
0100即可。
Header length域指明
IP数据包
header的长度(不包括数据
Data域),以四字节为单位,因为
Options域是可选的所以
IP Header的长度并不固定。我们不使用
Option域,所以取最小值
5,表示
Header长度为
20字节。服务类型域(
Type of Service, TOS)是为特殊的应用如
VoIP等保留的,我们不使用,赋值为零即可。接下来
2个字节的
Total Length域表示整个数据包的长度,包括
Header和
Data,以字节为单位。
标识域(
Identification)用来给数据包一个唯一的编号,用于验证和跟踪等,我们不使用,直接赋值为零即可。
Flags和
Offset用于分段包的重组,我们不使用,把
Flags的第
2位设为
1表示是不可分段的,
Offset赋值为零即可。生存时间(
Time to Live, TTL)表示该数据包在网络上的有效期,我们简单的把它设为最大值
0xFF即可。协议域(
Protocol)表示传输层使用什么协议,
RFC790文档为每个协议都规定了唯一的编号,如
UDP编号为
17。
Header Checksum为
Header区域的校验和,在校验之前该域初始为
0,然后计算整个头部的校验和,把结果存放在该域,计算校验的方法是把头部看成以
16位为单位的数字组成,依次进行二进制反码求和。接下来的八个字节是源
IP地址和目的
IP地址,没什么可说的。
综上所述,我们只保留了
IP协议中必须的关键字段,因而简化了设计,对
IP数据包进行填充的代码段如下:
struct ip_header * p = ( struct ip_header* ) ( buf) ; p- > ver_ihl = 0x45; // 1 Byte p- > tos = 0x00; // 1 Byte p- > tlen = htons ( len) ; // 2 Byte p- > identification = htons ( 0x00) ; // 2 Byte p- > flags_fo = htons ( 0x4000) ; // 2 Byte p- > ttl = 0xFF; // 1 Byte p- > proto = 17; // 1 Byte, 17 for UDP p- > ip_src = htonl ( NetOurIP) ; // 4 Byte p- > ip_dest = htonl ( NetServerIP) ; // 4 Byte p- > crc = 0x0; // 2 Byte, To be p- > crc = checksum( buf, sizeof ( struct ip_header) ) ; |
IP,TCP,UDP等许多协议的头部都设置了校验和项,它们采用的算法是一样的,将被校验的数据按16位进行划分(若数据字节长度为奇数,则在数据尾部补一个字节0),对每16位求反码和,然后再对和取反码。
代码如下:
unsigned short checksum( unsigned char * ptr, int len) { unsigned long sum = 0; unsigned short * p = ( unsigned short * ) ptr; while ( len > 1) { sum + = * p+ + ; len - = 2; } if ( len = = 1) sum + = * ( unsigned char * ) p; while ( sum> > 16) sum = ( sum& 0xffff) + ( sum> > 16) ; return ( unsigned short ) ( ( ~ sum) & 0xffff) ; } |
bits | 0 - 15 | 16 - 31 |
---|---|---|
0 | Source Port | Destination Port |
32 | Length | Checksum |
64 | Data |
TCP协议而使用简单的
UDP协议。虽然
UDP是无连接的协议,它不保证数据包一定能够到达目的主机,但是在嵌入式开发中,开发板跟主机通常位于同一内部局域网内,网络环境良好,数据丢失的可能性很小,并且
UDP容易实现,占用资源小,因此更适合于嵌入式环境。
UDP头部包含了可选的校验和字段,而校验要涉及到伪报头,为了简化设计和减小开销,我们不使用校验,直接把该字段设为零,表示不使用校验。UDP包填充代码如下:
struct udp_header * P = ( struct udp_header* ) ( buf) ; P- > port_src = htons ( 0x8DA4) ; // 2 Byte P- > port_dest = htons ( port) ; // 2 Byte P- > tlen = htons ( len) ; // 2 Byte P- > crc = 0x00; // Do Not Checksum, 2 Byte |
7. TFTP客户端的实现
tftp是一个很简单的文件传输协议,在传输层使用UDP协议。它有四种类型的包: 读请求RRQ包,DATA包,ACK包,ERROR包,每个包的前两个字节Opcode指定包的类型。(RRQ用于请求下载,WRQ用于请求上传,我们只用到RRQ)。
下载文件的过程分析如下:
客户端(A)从任意端口X向服务器(S)的端口69发送一个RRQ包,该包中指明了要求下载的文件名;服务器(S)找到该文件,读取文件内容组成DATA
包,从任意端口Y向客户端(A)的端口X发送这个DATA包,第一个DATA包编号为1;从此以后,客户端确定使用端口X,服务器确定使用端口Y,
客户端向服务器发送ACK包,编号为1。服务器接到编号为1的ACK包之后,发送第二个DATA包,如此继续下去。
怎样判断传输结束呢? 按照规定,DATA包中的数据段为512字节, 如果小于512字节,表示这是最后一个DATA包,文件已传输完毕。
(R1) Host A requests to read
(R2) Server S sends data packet 1
(R3) Host A acknowledges data packet 1
注意在这个过程中端口的变化。开始RRQ是69,但是DATA和ACK都不是使用69,而是使用另外一个随机的端口。 服务器在接到RRQ后,不返回任何回应信息,直接发送第一个DATA包,而且DATA包编号从1开始,而不是从0开始。
编程时为简单起见,客户端使用了固定的端口号X=0x8DA4,服务器端口号Y是随机的,只能通过解析UDP数据包获得。
int tftp_download(unsigned char *addr, const char *filename) { int i=0; unsigned short curblock = 1; tftp_send_request( &TxBuf[256], filename ); msdelay(100); while (1) { eth_rx(); if( pGtftp == NULL ) continue; if ( ntohs(pGtftp->opcode) == TFTP_DATA ) { if (ntohs(pGtftp->u.blocknum) == curblock) { printf("/r Current Block Number = %d", curblock); for (i=0; i<iGLen-4; i++) { *(addr++) = *(pGtftp->data+i); } tftp_send_ack( &TxBuf[256], curblock); if (iGLen < TFTP_DATASIZE+4) { break; } curblock += 1; } else if (ntohs(pGtftp->u.blocknum) < curblock) { tftp_send_ack( &TxBuf[256], ntohs(pGtftp->u.blocknum)); } else { printf("/n/rBlock Number Not Match."); printf("Block Number = %d, curblock = %d/n", ntohs(pGtftp->u.blocknum), curblock); } } else if ( ntohs(pGtftp->opcode) == TFTP_ERROR ) { switch( ntohs(pGtftp->u.errcode) ) { // 此处省略 } } else if ( ntohs(pGtftp->opcode) == TFTP_RRQ ) {}// 此处省略若干 else if pGtftp = NULL; iGLen = 0; } printf("/n/rTransfer complete: %d Bytes./n/r", (curblock-1)*TFTP_DATASIZE + iGLen-4 ); return 0; }
相关文章推荐
- 自己动手编写嵌入式Bootloader之一
- 自己动手编写嵌入式Bootloader---基本功能流程
- 自己动手编写嵌入式Bootloader之(2)
- 自己动手编写嵌入式Bootloader之(1)
- 自己动手编写嵌入式Bootloader之(1)
- 自己动手编写嵌入式Bootloader之(3)
- 自己动手编写嵌入式Bootloader之(2)
- 自己动手编写嵌入式Bootloader之(2)
- 自己动手编写嵌入式Bootloader---基本功能流程
- 自己动手编写嵌入式Bootloader之(3)
- 自己动手编写嵌入式Bootloader之(3)
- 自己动手编写嵌入式Bootloader---基本功能流程
- [转载]自己动手编写嵌入式Bootloader之(1)
- [转载]自己动手编写嵌入式Bootloader之(2)
- 自己动手编写嵌入式Bootloader之(2)
- [转载]自己动手编写嵌入式Bootloader之(3)
- 自己动手编写嵌入式Bootloader之(1)
- 自己动手编写嵌入式Bootloader之(1)
- 自己动手编写嵌入式Bootloader之(3)
- 自己动手编写嵌入式Bootloader之(1)