|
第二部分:通過網(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
讀芯片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:
因為是Little Endian, 所以0x09<<8, 但是在寄存器內(nèi)存中還是 0x00放在前面。 寄存器初始化: 設(shè)置CS8900的工作模式
發(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.
使用TxCMD下達發(fā)送數(shù)據(jù)的命令后,再讀取 PP_BusSTAT 總線狀態(tài)寄存器判斷是否做好發(fā)送數(shù)據(jù)的準備。當get_reg (PP_BusSTAT) & PP_BusSTAT_TxRDY 不等于零時表示可以發(fā)送了。 使用一個循環(huán)進行實際的發(fā)送操作:
這里 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ù):
其中 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
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./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)。 ![]() 對各個段作簡單的解釋: 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)體:
屬性 __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):
接收到的數(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ù)包的格式如下表所示:
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ù)包進行填充的代碼段如下:
CheckSum 校驗和: IP,TCP,UDP等許多協(xié)議的頭部都設(shè)置了校驗和項,它們采用的算法是一樣的,將被校驗的數(shù)據(jù)按16位進行劃分(若數(shù)據(jù)字節(jié)長度為奇數(shù),則在數(shù)據(jù)尾部補一個字節(jié)0),對每16位求反碼和,然后再對和取反碼。 代碼如下:
6. UDP協(xié)議的實現(xiàn)
在傳輸層我們拋棄了復雜的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包填充代碼如下:
關(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 ![]() 注意在這個過程中端口的變化。開始RRQ是69,但是DATA和ACK都不是使用69,而是使用另外一個隨機的端口。 服務(wù)器在接到RRQ后,不返回任何回應(yīng)信息,直接發(fā)送第一個DATA包,而且DATA包編號從1開始,而不是從0開始。
編程時為簡單起見,客戶端使用了固定的端口號X=0x8DA4,服務(wù)器端口號Y是隨機的,只能通過解析UDP數(shù)據(jù)包獲得。
第三部分:源代碼,運行結(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 。程序如下:
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)系在一起了。
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. 提供全部源代碼下載:
5. 運行結(jié)果截圖![]() 圖中,首先選擇3從TFTP服務(wù)器下載內(nèi)核到RAM中, 然后選擇4從RAM成功啟動內(nèi)核。 選擇2還有通過串口Kermit協(xié)議下載內(nèi)核的功能,前文沒有對這部分代碼作分析,有時間再補上。下面附一張截圖: ![]() http://blog./u/7459/showart_2022660.html |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|