第二部分:通過網(wǎng)口下載內(nèi)核映像
要實現(xiàn)通過網(wǎng)口下載文件的功能,從底層到上層需要做的工作包括:開發(fā)板上的網(wǎng)卡芯片的驅(qū)動程序;TCP/IP協(xié)議棧的實現(xiàn);TFTP客戶端應(yīng)用程序的實現(xiàn)。我們使用的OK2440開發(fā)板配備CS8900A網(wǎng)卡芯片。 為了簡單起見,網(wǎng)絡(luò)數(shù)據(jù)包的發(fā)送和接收都使用輪詢方式,不使用中斷;協(xié)議棧只使用ARP/IP/UDP協(xié)議,不涉及TCP及其他協(xié)議;應(yīng)用程序只實現(xiàn)最簡單的TFTP客戶端。
1. 全局配置信息
發(fā)送和接收的數(shù)據(jù)緩沖區(qū),使用全局靜態(tài)緩沖區(qū),不使用動態(tài)內(nèi)存分配。第一階段運行結(jié)束之后,CPU內(nèi)部4KB的SteppingStone可以用作其它用途,我們就用它做網(wǎng)絡(luò)數(shù)據(jù)接收、發(fā)送的緩沖區(qū)。亦可用作標準輸入輸出的緩沖區(qū)。
unsigned char *TxBuf = (unsigned char *)0;
unsigned char *RxBuf = (unsigned char *)1024;
使用若干個全局變量來保存網(wǎng)絡(luò)配置信息:
unsigned char NetOurEther[6] = /* Our ethernet address */
{0x00, 0x09, 0x58, 0xD8, 0x11, 0x22};
開發(fā)板的MAC地址,這個是任意設(shè)置的。
unsigned char NetServerEther[6] = /* Boot server enet address */
{0x00, 0x14, 0x2A, 0xA5, 0x50, 0x97};
服務(wù)器也就是主機的MAC地址,這個要跟主機MAC一致,可以在主機上運行ifconfig命令查到。
unsigned long NetOurIP = 0xC0A801FC; /* Our IP addr 192.168.1.252 */
unsigned long NetServerIP = 0xC0A801F9; /* Server IP 192.168.1.249 */
網(wǎng)絡(luò)協(xié)議中IP地址一般是用一個4字節(jié)整型數(shù)表示的。
2. CS8900A以太網(wǎng)驅(qū)動程序
硬件電路決定了CS8900的物理地址是在BANK3的區(qū)間內(nèi),CS8900是16位的寄存器,故我們設(shè)置BANK3的BUS WIDTH也為16位。設(shè)置BANK3: 總線寬度16,使能nWait,使能UB/LB
BANKCON3:0x1F7C
網(wǎng)卡CS8900的訪問基址為0x19000000,之所以再偏移0x300是由它的特性決定的
#define CS8900_BASE 0x19000300
CS8900 讀寫寄存器的方式有些特別。要讀一個寄存器,先向CS8900_PPTR中寫入該寄存器地址,再從CS8900_PDATA中讀出該寄存器值;要寫一個寄存器,先向CS8900PPTR中寫入該寄存器地址,再向CS8900_PDATA中寫入要寫入的值。不管是寄存器地址還是要讀寫的數(shù)值,都是16位的,也就是說都是unsigned short類型的。因此,讀寫寄存器的函數(shù)如下:
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寄存器中,讀該寄存器得到的正確值應(yīng)該是0x630E,這可以初步判斷一些地址/引腳的設(shè)置是否正確,如果讀出的不是0x630E,那么CS8900肯定不能正常工作。
設(shè)置MAC地址:
MAC地址并不是固定的,可以由我們隨意設(shè)置。從寄存器PP_IA開始的6個字節(jié)存放MAC地址。比如下面的代碼把MAC地址設(shè)為 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, 但是在寄存器內(nèi)存中還是 0x00放在前面。
寄存器初始化: 設(shè)置CS8900的工作模式
/* 只接收目標地址為本網(wǎng)卡的無錯誤數(shù)據(jù)包 */
put_reg (PP_RxCTL, PP_RxCTL_IA | PP_RxCTL_Broadcast | PP_RxCTL_RxOK);
/* 當進行接收操作時,不要產(chǎn)生任何中斷 */
put_reg (PP_RxCFG, 0);
/* 當進行發(fā)送操作時,不要產(chǎn)生任何中斷 */
put_reg (PP_TxCFG, 0);
/* 當進行緩存操作時,不要產(chǎn)生任何中斷 */
put_reg (PP_BufCFG, 0);
/* 使能發(fā)送和接收模式 */
put_reg (PP_LineCTL, PP_LineCTL_Rx | PP_LineCTL_Tx);
發(fā)送數(shù)據(jù)包:
int eth_send (volatile void *packet, int length)
兩個參數(shù):要發(fā)送的數(shù)據(jù)包首地址、長度
TxCMD 和TxLen寄存器用來初始化數(shù)據(jù)包的發(fā)送,其具體含義見CS8900數(shù)據(jù)手冊第70頁。這里PP_TxCmd_TxStart_Full被定義為 0x00C0,表示直到整個數(shù)據(jù)偵都加載到CS8900內(nèi)部緩存之后才開始發(fā)送,數(shù)據(jù)偵的長度為CS8900_TxLEN.
/* initiate a transmit sequence */
CS8900_TxCMD = PP_TxCmd_TxStart_Full;
CS8900_TxLEN = length;
使用TxCMD下達發(fā)送數(shù)據(jù)的命令后,再讀取 PP_BusSTAT 總線狀態(tài)寄存器判斷是否做好發(fā)送數(shù)據(jù)的準備。當get_reg (PP_BusSTAT) & PP_BusSTAT_TxRDY 不等于零時表示可以發(fā)送了。 使用一個循環(huán)進行實際的發(fā)送操作:
for (addr = packet; length > 0; length -= 2)
{
CS8900_RTDATA = *addr++;
}
這里 addr 也是unsigned short類型的指針, 每次向CS8900_RTDATA寫入兩個字節(jié)數(shù)據(jù)。這里假設(shè)要發(fā)送的數(shù)據(jù)包長度為偶數(shù)。
最后,通過讀取PP_TER寄存器可以知道是否發(fā)送完畢,是否發(fā)送成功。
接收數(shù)據(jù)包:
首先,通過讀取PP_RER寄存器判斷是否接收到數(shù)據(jù)。如果接收到數(shù)據(jù),則連續(xù)兩次讀取 CS8900_RTDATA 的值,
status = CS8900_RTDATA; /* stat */
rxlen = CS8900_RTDATA; /* len */
rxlen 為接收到的數(shù)據(jù)長度。
然后用一個循環(huán)連續(xù)讀取 rxlen 長度的數(shù)據(jù):
for (addr = (unsigned short *) &RxBuf[0], i = rxlen >> 1; i > 0;
i--)
*addr++ = CS8900_RTDATA;
if (rxlen & 1)
*addr++ = CS8900_RTDATA;
其中 RxBuf 為預先在內(nèi)存中開辟的一塊接收緩沖區(qū)。 每次循環(huán)讀取兩個字節(jié),還需要處理長度為奇數(shù)的情況。
最后,把RxBuf交給上層的協(xié)議處理:net_receive( &RxBuf[0], rxlen );
3. Ethernet MAC層協(xié)議的實現(xiàn)
上層的數(shù)據(jù)包(如IP包、ARP包)到來時,需要添加一個14字節(jié)的MAC頭, 然后再交給網(wǎng)卡發(fā)送出去。 MAC頭包含目的MAC地址、源MAC地址、協(xié)議類型三個字段。如下圖所示。數(shù)據(jù)包末尾的CRC校驗我們不使用。
使用下面的代碼填充MAC頭。其中協(xié)議類型,對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協(xié)議的實現(xiàn)
一般的方式是建立一個全局的ARP映射緩存表,隨著系統(tǒng)的運行不斷查找、更新該表。但是我們要完成的功能僅僅是從TFTP服務(wù)器下載內(nèi)核和文件系統(tǒng)映像,而服務(wù)器的IP和MAC地址都是固定的,因此可以簡化ARP映射表,只用兩個變量分別保存服務(wù)器IP和MAC,再用兩個變量保存開發(fā)板IP和MAC即可。并且更新映射表的功能也可以省略,只在系統(tǒng)初始化時把這四個地址都設(shè)置好,使用過程中不會發(fā)生改變,所以不需要更新。這樣,我們的ARP協(xié)議只需要完成接受ARP請求、發(fā)送ARP應(yīng)答的功能,而發(fā)送ARP請求和接受ARP應(yīng)答的功能可以省略,這樣大大簡化了協(xié)議棧的設(shè)計。
按照維基百科上的介紹(http://en.wikipedia.org/wiki/Address_Resolution_Protocol),ARP 是一個數(shù)據(jù)鏈路層協(xié)議,(我感覺它應(yīng)該是網(wǎng)絡(luò)層的協(xié)議),它的作用是在只知道一個主機網(wǎng)絡(luò)層IP地址的情況下找到它的硬件地址。在以太網(wǎng)上,它主要用來把 IP地址轉(zhuǎn)換為以太網(wǎng)MAC地址。由于是鏈路層協(xié)議,ARP的作用范圍僅限于本地局域網(wǎng)。
ARP數(shù)據(jù)包長度為28字節(jié),其中各字節(jié)的含義如下圖所示:
對各個段作簡單的解釋:
Hardware type (HTYPE) 每個數(shù)據(jù)鏈路層協(xié)議都被分配到一個數(shù),比如,Ethernet 是 1
Protocol type (PTYPE) 在這個域,每個網(wǎng)絡(luò)層協(xié)議都被分配到一個數(shù)(標號),比如,IP是0x0800
Hardware length (HLEN) 硬件地址的長度。以太網(wǎng)Ethernet的MAC地址長度是6個字節(jié)
Protocol length (PLEN) 維基上寫的是“邏輯地址”的長度,其實也就是網(wǎng)絡(luò)層地址的長度。IPv4地址的長度為4個字節(jié)。
Operation 表明發(fā)送者的操作,也就是數(shù)據(jù)包的類型:1表示ARP請求;2表示ARP回應(yīng);3表示RARP請求;4表示RARP回應(yīng)。
Sender hardware address (SHA) 發(fā)送者的硬件地址
Sender protocol address (SPA) 發(fā)送者的協(xié)議地址,也就是發(fā)送者IP地址。
Target hardware address (THA) 目標接收者的硬件MAC地址。如果是ARP請求,這個域被忽略。
Target protocol address (TPA) 目標接收者的IP地址。
知道了包結(jié)構(gòu),我們就可以設(shè)計一個結(jié)構(gòu)體:
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)) 告訴編譯器使用緊縮方式存放結(jié)構(gòu)體內(nèi)容(1 Byte align), 不使用默認的4字節(jié)對齊, 這樣就不會產(chǎn)生冗余字節(jié)。此時的 sizeof(struct arp_header) = 28。 如果不加packed屬性, 運行 sizeof(struct arp_header) 得到 32, 而不是 28。 數(shù)據(jù)段就產(chǎn)生了錯位。
前面已經(jīng)說過,我們只實現(xiàn)接收ARP請求并發(fā)送ARP應(yīng)答的功能,因此只用一個簡單的函數(shù)就可實現(xiàn):
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;
}
接收到的數(shù)據(jù)保存在pRx地址處,要發(fā)送的數(shù)據(jù)地址指定為pTx位于發(fā)送緩沖區(qū)中。如果接收到的是ARP請求包并且IP地址也符合,則在pTx處構(gòu)造一個ARP應(yīng)答包并交給mac_send()發(fā)送出去。
5. IP協(xié)議的實現(xiàn)
IP數(shù)據(jù)包的格式如下表所示:
+
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協(xié)議的簡化:IP協(xié)議在網(wǎng)絡(luò)中主要完成路由選擇和網(wǎng)絡(luò)分段的功能。起始Bit 0-3表示版本號,對IPv4來說取值為4即0100即可。Header length域指明IP數(shù)據(jù)包header的長度(不包括數(shù)據(jù)Data域),以四字節(jié)為單位,因為Options域是可選的所以IP Header的長度并不固定。我們不使用Option域,所以取最小值5,表示Header長度為20字節(jié)。服務(wù)類型域(Type of Service, TOS)是為特殊的應(yīng)用如VoIP等保留的,我們不使用,賦值為零即可。接下來2個字節(jié)的Total Length域表示整個數(shù)據(jù)包的長度,包括Header和Data,以字節(jié)為單位。 標識域(Identification)用來給數(shù)據(jù)包一個唯一的編號,用于驗證和跟蹤等,我們不使用,直接賦值為零即可。Flags和Offset用于分段包的重組,我們不使用,把Flags的第2位設(shè)為1表示是不可分段的,Offset賦值為零即可。生存時間(Time to Live, TTL)表示該數(shù)據(jù)包在網(wǎng)絡(luò)上的有效期,我們簡單的把它設(shè)為最大值0xFF即可。協(xié)議域(Protocol)表示傳輸層使用什么協(xié)議,RFC790文檔為每個協(xié)議都規(guī)定了唯一的編號,如UDP編號為17。Header Checksum為Header區(qū)域的校驗和,在校驗之前該域初始為0,然后計算整個頭部的校驗和,把結(jié)果存放在該域,計算校驗的方法是把頭部看成以16位為單位的數(shù)字組成,依次進行二進制反碼求和。接下來的八個字節(jié)是源IP地址和目的IP地址,沒什么可說的。
綜上所述,我們只保留了IP協(xié)議中必須的關(guān)鍵字段,因而簡化了設(shè)計,對IP數(shù)據(jù)包進行填充的代碼段如下:
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等許多協(xié)議的頭部都設(shè)置了校驗和項,它們采用的算法是一樣的,將被校驗的數(shù)據(jù)按16位進行劃分(若數(shù)據(jù)字節(jié)長度為奇數(shù),則在數(shù)據(jù)尾部補一個字節(jié)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協(xié)議的實現(xiàn)
bits 0 - 15 16 - 31
0 Source Port Destination Port
32 Length Checksum
64
Data
在傳輸層我們拋棄了復雜的TCP協(xié)議而使用簡單的UDP協(xié)議。雖然UDP是無連接的協(xié)議,它不保證數(shù)據(jù)包一定能夠到達目的主機,但是在嵌入式開發(fā)中,開發(fā)板跟主機通常位于同一內(nèi)部局域網(wǎng)內(nèi),網(wǎng)絡(luò)環(huán)境良好,數(shù)據(jù)丟失的可能性很小,并且UDP容易實現(xiàn),占用資源小,因此更適合于嵌入式環(huán)境。 UDP頭部包含了可選的校驗和字段,而校驗要涉及到偽報頭,為了簡化設(shè)計和減小開銷,我們不使用校驗,直接把該字段設(shè)為零,表示不使用校驗。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
關(guān)于源端口號和目的端口號的設(shè)定,在TFTP實現(xiàn)時會詳細說明。
7. TFTP客戶端的實現(xiàn)
tftp是一個很簡單的文件傳輸協(xié)議,在傳輸層使用UDP協(xié)議。它有四種類型的包: 讀請求RRQ包,DATA包,ACK包,ERROR包,每個包的前兩個字節(jié)Opcode指定包的類型。(RRQ用于請求下載,WRQ用于請求上傳,我們只用到RRQ)。
下載文件的過程分析如下: 客戶端(A)從任意端口X向服務(wù)器(S)的端口69發(fā)送一個RRQ包,該包中指明了要求下載的文件名;服務(wù)器(S)找到該文件,讀取文件內(nèi)容組成DATA包,從任意端口Y向客戶端(A)的端口X發(fā)送這個DATA包,第一個DATA包編號為1;從此以后,客戶端確定使用端口X,服務(wù)器確定使用端口Y, 客戶端向服務(wù)器發(fā)送ACK包,編號為1。服務(wù)器接到編號為1的ACK包之后,發(fā)送第二個DATA包,如此繼續(xù)下去。
怎樣判斷傳輸結(jié)束呢? 按照規(guī)定,DATA包中的數(shù)據(jù)段為512字節(jié), 如果小于512字節(jié),表示這是最后一個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,而是使用另外一個隨機的端口。 服務(wù)器在接到RRQ后,不返回任何回應(yīng)信息,直接發(fā)送第一個DATA包,而且DATA包編號從1開始,而不是從0開始。
編程時為簡單起見,客戶端使用了固定的端口號X=0x8DA4,服務(wù)器端口號Y是隨機的,只能通過解析UDP數(shù)據(jù)包獲得。
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;
}
http://blog.chinaunix.net/u/7459/showart_2022532.html第三部分:源代碼,運行結(jié)果
這一部分將對前文沒有提到的幾段關(guān)鍵代碼進行簡單說明,介紹一下源代碼組織結(jié)構(gòu)和Makefile系統(tǒng),展示一下實驗運行結(jié)果,并提供全部源代碼下載。
1. 定時器初始化和延時程序
因為在 CS8900A的驅(qū)動程序中需要用到延時,因此有必要對S3C2440的計時器進行使能和初始化,并編寫延時程序。
S3C2440A共有5個定時器,編號為Timer0 ~ Timer4。其中Timer0 ~ Timer3都有輸出引腳,可以通過定時器來控制引腳電平周期性的變化,這稱為脈沖寬度調(diào)制(PWM:Pulse Width Modulation)功能。而Timer4沒有輸出引腳,也就沒有PWM功能,所以Timer4常被程序里的延時函數(shù)使用。
定時器部件的時鐘源為PCLK,但是需要經(jīng)過兩級預分頻之后才真正供定時器使用。第一級預分頻由TCFG0寄存器控制,其位[7:0]設(shè)置預分頻器0的值,供Timer0和Timer1使用,位[15:8]設(shè)置預分頻器1的值,供Timer2 ~ Timer4使用。第二級預分頻由TCFG1寄存器控制,其每四位控制一個定時器,可以從2分頻、4分頻、8分頻、16分頻、外接TCLK0/TCLK1 這五種頻率中選擇。
我們的延時函數(shù)使用Timer4,其它定時器全部關(guān)閉。初始化程序中設(shè)置:TCFG0 = 0x0f00; 表示Timer4的第一級預分頻值為 15+1 = 16。寄存器TCFG1使用默認值全0,表示第二級預分頻為2分頻。前面已經(jīng)設(shè)置PCLK為50MHz,這樣Timer4實際的工作頻率為:
50MHz/16/2 = 50000000/32 = 1562500Hz
注意計算時鐘頻率時的MHz是指10^6,而不是2^20;同理KHz是指1000Hz,而不是1024Hz。
我們在TCON中把Timer4設(shè)為”自動加載“。當Timer4啟動時,TCNTB4的值將被自動裝入內(nèi)部寄存器TCNT4,然后在工作頻率下,TCNT4開始減1計數(shù),當?shù)竭_0時,TCNTB4的值又被自動裝入TCNT4,下一個計數(shù)流程開始。我們把TCNTB4設(shè)為15625,則一個計數(shù)流程的的長度為10毫秒。
假設(shè)要延時的時間為msec毫秒,則共需要的計數(shù)值為 tmo = msec*15625/10,設(shè)一個變量timestamp保存已經(jīng)過去的時間戳,每次讀取TCNT4的值后更新timestamp,直到它大于 tmo 。程序如下:
while( timestamp < tmo )
{
thisdec = TCNTO4 & 0xffff;
if( lastdec >= thisdec ) /* normal mode */
{
timestamp += lastdec - thisdec;
}
else /* we have an overflow ... */
{
timestamp += lastdec + TIMER_LOAD_VAL - thisdec;
}
lastdec = thisdec;
}
TCNT4的值可由寄存器TCNTO4讀出。程序中保存了最近兩次讀出的TCNTO4值, 如果本次值比上次小,說明在同一個計數(shù)流程內(nèi);如果本次值比上次大,說明已經(jīng)進入了下一個計數(shù)流程。
2. 串口標準輸入輸出
要想在Bootloader中使用scanf()和print()并不容易,因為不能直接使用C庫函數(shù)。scanf()要從串口獲得輸入, print()要向串口進行輸出。必須自己實現(xiàn)常用的C庫函數(shù), 不僅包括輸入輸出函數(shù),還包括字符串操作函數(shù)如strcmp(), strcpy()等。幸好在《嵌入式Linux應(yīng)用開發(fā)完全手冊》這本書的源代碼中提供了這樣簡化的C庫,所以就直接拿來用了。
代碼中定義了兩個全局數(shù)組作為輸入輸出緩沖區(qū):
static unsigned char g_pcOutBuf[ 1024 ];
static unsigned char g_pcInBuf[ 1024 ];
其實我們可以把這兩個緩沖區(qū)定位在CPU的 SteppingStone 里面,這樣可以節(jié)省2K的空間。
scanf()的實現(xiàn)里面調(diào)用 getc() 函數(shù), printf() 的實現(xiàn)里面調(diào)用 putc() 函數(shù)。我們自己寫getc()函數(shù)為從串口讀取字符, putc()函數(shù)實現(xiàn)為向串口發(fā)送字符, 這樣標準輸入輸出就跟串口聯(lián)系在一起了。
/* 發(fā)送一個字符 */
void putc(unsigned char c)
{
/* 等待,直到發(fā)送緩沖區(qū)中的數(shù)據(jù)已經(jīng)全部發(fā)送出去 */
while (!(UTRSTAT0 & TXD0READY));
/* 向UTXH0寄存器中寫入數(shù)據(jù),UART即自動將它發(fā)送出去 */
UTXH0 = c;
}
/* 接收字符 */
unsigned char getc(void)
{
unsigned char ret;
/* 等待,直到接收緩沖區(qū)中的有數(shù)據(jù) */
while(!(UTRSTAT0 & RXD0READY));
/* 直接讀取URXH0寄存器,即可獲得接收到的數(shù)據(jù) */
ret = URXH0;
if (ret == 0x0d || ret == 0x0a)
{
putc(0x0d);
putc(0x0a);
}
else
{
putc(ret);
}
return ret;
}
3. 源代碼組織結(jié)構(gòu)
源代碼跟目錄下只有兩個文件, 主Makefile和鏈接腳本sboot.lds。
文件夾start內(nèi)有start.S和nand.c,前者是上電后最初運行的匯編代碼,后者含有Nand Flash的讀函數(shù),負責把S-Boot代碼從Nand拷貝到RAM中。
文件夾main內(nèi)有main.c,是一個死循環(huán),提供若干菜單供用戶選擇,然后調(diào)用相應(yīng)功能的程序。
文件夾lib內(nèi)是簡化和移植過的C標準庫,包括輸入輸出和字符串操作函數(shù)。
文件夾include內(nèi)是一些頭文件。
文件夾app內(nèi)有boot_linux.c和tftp.c,從名字就能看出它們的功能。
文件夾device內(nèi)含有設(shè)備驅(qū)動程序,如串口初始化、定時器初始化和延時函數(shù)、網(wǎng)卡驅(qū)動、網(wǎng)絡(luò)協(xié)議實現(xiàn)等。
每個文件夾內(nèi)都有自己的Makefile,根目錄下的主Makefile會進入各個子目錄并調(diào)用各自的Makefile。每個子目錄下的Makefile把自己編譯的代碼鏈接成一個build-in.o文件, 主Makefile把各個子目錄下的build-in.o鏈接成一個可執(zhí)行文件。
編譯器使用自己制作的 arm-hwlee-linux-gnueabi-gcc.
可以從這里下載。 給gcc增加 -nostdinc 選項, 表示不使用標準C庫函數(shù),不到/usr/include目錄下尋找包含文件, 只在-I$(INCLUDEDIR)指定的目錄尋找包含文件。
4. 提供全部源代碼下載:
文件: S-Boot.tar.gz
大小: 41KB
下載:
下載5. 運行結(jié)果截圖
圖中,首先選擇3從TFTP服務(wù)器下載內(nèi)核到RAM中, 然后選擇4從RAM成功啟動內(nèi)核。
選擇2還有通過串口Kermit協(xié)議下載內(nèi)核的功能,前文沒有對這部分代碼作分析,有時間再補上。下面附一張截圖:
http://blog.chinaunix.net/u/7459/showart_2022660.html