首页
登录 | 注册

[原创]自己动手编写嵌入式Bootloader之(2)

第二部分:通过网口下载内核映像

要实现通过网口下载文件的功能,从底层到上层需要做的工作包括:开发板上的网卡芯片的驱动程序;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;
}


读芯片ID: CS8900的芯片ID存放在PP_ChipID寄存器中,读该寄存器得到的正确值应该是0x630E,这可以初步判断一些地址/引脚的设置是否正确,如果读出的不是0x630E,那么CS8900肯定不能正常工作。

设置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);


因为是Little Endian, 所以0x09<<8, 但是在寄存器内存中还是 0x00放在前面。


寄存器初始化: 设置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;


使用TxCMD下达发送数据的命令后,再读取 PP_BusSTAT 总线状态寄存器判断是否做好发送数据的准备。当get_reg (PP_BusSTAT) & PP_BusSTAT_TxRDY 不等于零时表示可以发送了。 使用一个循环进行实际的发送操作:

for (addr = packet; length > 0; length -= 2)
        {
            CS8900_RTDATA = *addr++;
        }


这里 addr 也是unsigned short类型的指针, 每次向CS8900_RTDATA写入两个字节数据。这里假设要发送的数据包长度为偶数。

最后,通过读取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 为预先在内存中开辟的一块接收缓冲区。 每次循环读取两个字节,还需要处理长度为奇数的情况。

最后,把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);


4. ARP协议的实现


      一般的方式是建立一个全局的ARP映射缓存表,随着系统的运行不断查找、更新该表。但是我们要完成的功能仅仅是从TFTP服务器下载内核和文件系统映像,而服务器的IPMAC地址都是固定的,因此可以简化ARP映射表,只用两个变量分别保存服务器IPMAC,再用两个变量保存开发板IPMAC即可。并且更新映射表的功能也可以省略,只在系统初始化时把这四个地址都设置好,使用过程中不会发生改变,所以不需要更新。这样,我们的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));


属性 __attribute__((packet)) 告诉编译器使用紧缩方式存放结构体内容(1 Byte align), 不使用默认的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;
}


接收到的数据保存在pRx地址处,要发送的数据地址指定为pTx位于发送缓冲区中。如果接收到的是ARP请求包并且IP地址也符合,则在pTx处构造一个ARP应答包并交给mac_send()发送出去。

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协议的简化:IP协议在网络中主要完成路由选择和网络分段的功能。起始Bit 0-3表示版本号,对IPv4来说取值为40100即可。Header length域指明IP数据包header的长度(不包括数据Data域),以四字节为单位,因为Options域是可选的所以IP Header的长度并不固定。我们不使用Option域,所以取最小值5,表示Header长度为20字节。服务类型域(Type of Service, TOS)是为特殊的应用如VoIP等保留的,我们不使用,赋值为零即可。接下来2个字节的Total Length域表示整个数据包的长度,包括HeaderData,以字节为单位。 标识域(Identification)用来给数据包一个唯一的编号,用于验证和跟踪等,我们不使用,直接赋值为零即可。FlagsOffset用于分段包的重组,我们不使用,把Flags的第2位设为1表示是不可分段的,Offset赋值为零即可。生存时间(Time to Live, TTL)表示该数据包在网络上的有效期,我们简单的把它设为最大值0xFF即可。协议域(Protocol)表示传输层使用什么协议,RFC790文档为每个协议都规定了唯一的编号,如UDP编号为17Header ChecksumHeader区域的校验和,在校验之前该域初始为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) );


CheckSum 校验和:
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);
}



6. UDP协议的实现

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


关于源端口号和目的端口号的设定,在TFTP实现时会详细说明。

7. TFTP客户端的实现

tftp是一个很简单的文件传输协议,在传输层使用UDP协议。它有四种类型的包: 读请求RRQ包,DATA包,ACK包,ERROR包,每个包的前两个字节Opcode指定包的类型。(RRQ用于请求下载,WRQ用于请求上传,我们只用到RRQ)。

500)this.width=500;" border="0">

下载文件的过程分析如下: 客户端(A)从任意端口X向服务器(S)的端口69发送一个RRQ包,该包中指明了要求下载的文件名; 服务器(S)找到该文件,读取文件内容组成DATA包,从任意端口Y向客户端(A)的端口X发送这个DATA包,第一个DATA包编号为1; 从此以后,客户端确定使用端口X,服务器确定使用端口Y, 客户端向服务器发送ACK包,编号为1。 服务器接到编号为1的ACK包之后,发送第二个DATA包,如此继续下去。

怎样判断传输结束呢? 按照规定,DATA包中的数据段为512字节, 如果小于512字节,表示这是最后一个DATA包,文件已传输完毕。


500)this.width=500;" border="0">
(R1) Host A requests to read
500)this.width=500;" border="0">
(R2) Server S sends data packet 1
500)this.width=500;" border="0">
(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;
}


相关文章

  • 全能小将再次出发,Java助力物联网技术的发展。
    物联网的发展离不开背后数据的支持,同时它也需要一项技术赋予一种设备新的生命,而Java当之无愧可以接手了这项任务,将载体与信息通信串联在一起,向设备赋予新的动力,为人类提供更便捷的服务. 物联网的含义: 物联网是新一代信息技术的重要组成部分 ...
  • [原创] 转入颠覆的嵌入式“钱”途
    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://blog.chinaunix.net/space.php?uid=9708199&do=blog&id=3 ...
  • 亲爱的各位博主,博客评选活动又开始啦,感谢大家对活动的支持,希望大家的技术水平越来越好,博文也更加出色,获奖的博主还有机会晋级"推荐博客"."专家博客"!     博客评选将邀请技术专家作为点评嘉宾, ...
  • 转载:https://blog.csdn.net/hrdzkj/article/details/75507019 2.  The First Stop for the Latest ICs and Components 非常好的关于微处理器 ...
  •    这两种语言都是面向对象的语言,两者之间最大的不同是编译策略的不同,c++要编译成目标文件,所以编译器要与硬件紧关联,java编译成中间代码,但是需要一个与硬件紧关联的解释器.所以做嵌入式设备的时候,一个需要准备面向对象的编译器,一个准 ...
  • GNU C 通过 __atttribute__ 声明weak属性,可以将一个强符号转换为弱符号. 编译器在编译源程序时,无论你是变量名.函数名,在它眼里,都是一个符号而已,用来表征一个地址.编译器会将这些符号集中,存放到一个叫符号表的 se ...

2020 unjeep.com webmaster#unjeep.com
12 q. 0.012 s.
京ICP备10005923号