本身動手編寫嵌入式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:ide

    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的工做模式spa

    /* 只接收目標地址爲本網卡的無錯誤數據包 */
    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)。

下載文件的過程分析以下: 客戶端(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;

}

相關文章
相關標籤/搜索