自己動手編寫嵌入式Bootloader之(2)

來源:互聯網
上載者:User

第二部分:通過網口下載核心映像

要實現通過網口下載檔案的功能,從底層到上層需要做的工作包括:開發板上的網卡晶片的驅動程式;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伺服器下載核心和檔案系統映像,而伺服器的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));

屬性 __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來說取值為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) );

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資料包獲得。

view plain
  1. int tftp_download(unsigned char *addr, const char *filename)  
  2. {  
  3.     int i=0;  
  4.     unsigned short curblock = 1;  
  5.     tftp_send_request( &TxBuf[256], filename );  
  6.     msdelay(100);  
  7.     while (1)  
  8.     {  
  9.         eth_rx();  
  10.         
  11.         if( pGtftp == NULL )  
  12.             continue;  
  13.           
  14.         if ( ntohs(pGtftp->opcode) == TFTP_DATA )  
  15.         {  
  16.             if (ntohs(pGtftp->u.blocknum) == curblock)  
  17.             {  
  18.                 printf("/r Current Block Number = %d", curblock);  
  19.                 for (i=0; i<iGLen-4; i++)  
  20.                 {  
  21.                     *(addr++) = *(pGtftp->data+i);  
  22.                 }  
  23.                 tftp_send_ack( &TxBuf[256], curblock);  
  24.                   
  25.                 if (iGLen < TFTP_DATASIZE+4)  
  26.                 {  
  27.                     break;  
  28.                 }  
  29.                 curblock += 1;  
  30.             }  
  31.             else if (ntohs(pGtftp->u.blocknum) < curblock)  
  32.             {  
  33.                 tftp_send_ack( &TxBuf[256], ntohs(pGtftp->u.blocknum));  
  34.             }  
  35.             else  
  36.             {  
  37.                 printf("/n/rBlock Number Not Match.");  
  38.                 printf("Block Number = %d, curblock = %d/n", ntohs(pGtftp->u.blocknum), curblock);    
  39.             }  
  40.         }  
  41.         else if ( ntohs(pGtftp->opcode) == TFTP_ERROR )  
  42.         {  
  43.             switch( ntohs(pGtftp->u.errcode) )  
  44.             {  
  45.                // 此處省略  
  46.             }  
  47.         }  
  48.         else if ( ntohs(pGtftp->opcode) == TFTP_RRQ )  
  49.         {}// 此處省略若干 else if  
  50.          
  51.         pGtftp = NULL;  
  52.         iGLen = 0;  
  53.     }  
  54.       
  55.     printf("/n/rTransfer complete: %d Bytes./n/r", (curblock-1)*TFTP_DATASIZE + iGLen-4 );  
  56.       
  57.     return 0;  
  58. }  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.