第二部分:通過網口下載核心映像
要實現通過網口下載檔案的功能,從底層到上層需要做的工作包括:開發板上的網卡晶片的驅動程式;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
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;
}