第二部分:經過網口下載內核映像
要 實現經過網口下載文件的功能,從底層到上層須要作的工做包括:開發板上的網卡芯片的驅動程序;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:ide
|
寄存器初始化: 設置CS8900的工做模式spa
|
int eth_send (volatile void *packet, int length)
兩個參數:要發送的數據包首地址、長度
TxCMD 和TxLen寄存器用來初始化數據包的發送,其具體含義見CS8900數據手冊第70頁。這裏PP_TxCmd_TxStart_Full被定義爲 0x00C0,表示直到整個數據偵都加載到CS8900內部緩存以後纔開始發送,數據偵的長度爲CS8900_TxLEN.
|
|
|
上層的數據包(如IP包、ARP包)到來時,須要添加一個14字節的MAC頭, 而後再交給網卡發送出去。 MAC頭包含目的MAC地址、源MAC地址、協議類型三個字段。以下圖所示。數據包末尾的CRC校驗咱們不使用。
使用下面的代碼填充MAC頭。其中協議類型,對IP爲0x0800, 對ARP爲0x0806
|
通常的方式是創建一個全局的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地址。
知道了包結構,咱們就能夠設計一個結構體:
|
|
+ |
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數據包進行填充的代碼段以下:
|
|
bits | 0 - 15 | 16 - 31 |
---|---|---|
0 | Source Port | Destination Port |
32 | Length | Checksum |
64 | Data |
在傳輸層咱們拋棄了複雜的TCP協議而使用簡單的UDP協議。雖然UDP是無鏈接的協議,它不保證數據包必定可以到達目的主機,可是在嵌入式開發中,開發板跟主機一般位於同一內部局域網內,網絡環境良好,數據丟失的可能性很小,而且UDP容易實現,佔用資源小,所以更適合於嵌入式環境。 UDP頭部包含了可選的校驗和字段,而校驗要涉及到僞報頭,爲了簡化設計和減少開銷,咱們不使用校驗,直接把該字段設爲零,表示不使用校驗。UDP包填充代碼以下:
|
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
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;
}