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