- VxWorks設備驅動開發詳解
- 曹桂平等編著
- 9218字
- 2019-01-09 15:53:27
3.3 深入VxWorks啟動過程
VxWorks內核映像類型總體上分為兩種:下載型VxWorks映像和ROM型VxWorks映像。下載型VxWorks映像需要借助bootrom的前期準備工作,由bootrom負責VxWorks內核映像的下載并最終跳轉到VxWorks內核入口函數執行。ROM型VxWorks映像不需要借助任何外部程序,系統上電時就跳轉到VxWorks內核入口函數進行執行。兩種映像類型在啟動過程的早期階段有些不同,當進入usrConfig.c文件中定義的usrInit函數后,則啟動流程將完全相同。故我們首先從兩種映像類型的不同角度分別介紹早期啟動過程的流程,在分別到達usrInit函數后再合二為一進行介紹。
3.3.1 ROM型映像早期啟動流程詳解
所謂ROM型映像,即不借助任何外部程序的幫助,從系統上電之時,就執行VxWorks內核代碼的啟動方式。ROM型映像將預先被燒錄入平臺ROM中(一般為Nor Flash介質),平臺被設計為上電時,CPU自動跳轉到ROM起始地址處開始執行第一條指令,而ROM型VxWorks內核映像的第一條指令也就被放置在ROM起始地址處。
ROM型VxWorks內核映像由如下文件生成:romInit.s、bootInit.c、sysALib.s、usrConfig.c、sysLib.c、外設驅動文件。注意:雖然ROM型VxWorks內核自始至終都沒有使用sysALib.s文件中的任何代碼,但這個文件總是作為一部分存在于映像之中的。系統上電執行的第一條語句就定義在romInit.s文件中,該文件完全使用匯編語言編程,其中主要實現了一個romInit函數,該函數開始處按照特定平臺要求,一般放置一個系統異常表,即一些跳轉指令。事實上,系統上電對于CPU而言就是一個復位異常,故其將跳轉到復位異常處理程序處執行代碼,這個復位異常處理程序就是整個系統的入口,這個入口通常被設置為romInit函數。基于romInit的鏈接地址位于RAM中,而其整個代碼的執行都處于ROM中,故romInit函數的實現必須自始至終做到位置無關(PIC),這就要求對于函數的調用不能以通常直接的形式,而必須使用如下形式。
#define ROM_ADRS(x) ((x) - _romInit + ROM_TEXT_ADRS) ((FUNCPTR)ROM_ADRS(romStart))(startType);
這個位置無關的要求直到romStart函數將代碼和數據從ROM復制到RAM后才解除。
1.romInit函數
作為上電啟動時執行的第一個函數,romInit函數需要實現如下功能:
● 配置CPU相關寄存器,如設置運行模式,屏蔽系統中斷。
● 初始化平臺,這包括外部存儲器控制器的初始化、目標板上關鍵硬件的初始化如時鐘、PLL、管腳復用模塊、電源管理模塊等。
● 初始化初始棧,供任務創建之前的代碼使用。具體地說,所有在usrRoot函數之前運行的代碼都沒有任務上下文,而是在復位異常上下文中運行,類似于在中斷上下文中運行,在涉及函數的調用時,需要一個棧進行參數傳遞,這個工作必須在romInit
函數中完成,因為只有它才有機會直接操作棧寄存器,對其賦予一個合理的值。
● 跳轉到romStart函數。
BSP開發中的一個重要組成部分:平臺初始化代碼幾乎全部在romInit函數中實現或者被其調用。對于CPU硬件寄存器的配置,一般直接在romInit函數中硬編碼,而平臺外設硬件資源如DDR控制器、管腳復用模塊等的配置則使用C語言編程,而后在romInit函數中進行調用。注意:由于在DDR控制器初始化之前,內存還不可用,而進行函數調用時一般都需要使用棧,此時可以將棧寄存器(SP)初始化成指向CPU內部的DRAM或者IRAM。事實上,這是唯一的途徑,用戶在到達usrRoot函數時可一直使用CPU內部RAM作為初始棧使用。
如下是一個實際系統中(ARM926EJS處理器核)使用romInit.s文件的例子。我們將分段對其進行簡單分析。
#define _ASMLANGUAGE #include "VxWorks.h" #include "sysLib.h" #include "asm.h" #include "regs.h" #include "config.h" #include "arch/arm/mmuArmLib.h" .data .globl VAR(copyright_wind_river) .long VAR(copyright_wind_river) /* internals */ .globl FUNC(romInit) /* start of system code */ .globl VAR(sdata) /* start of data */ .globl _sdata .globl VAR(daviciMemSize) /* actual memory size */ /* externals */ .extern FUNC(romStart) /* system initialization routine */ .extern FUNC(Platform) .extern FUNC(led_test) _sdata: VAR_LABEL(sdata) .asciz "start of data" .balign 4 .data VAR_LABEL(daviciMemSize) .long 0 .text .balign 4 /******************************************************************************* * * romInit - entry point for VxWorks in ROM * * romInit * ( * int startType /@ only used by 2nd entry point @/ * ) * INTERNAL * sysToMonitor examines the ROM for the first instruction and the string * "Copy" in the third word so if this changes, sysToMonitor must be updated. */ _ARM_FUNCTION(romInit) _romInit: B cold B myexcEnterUndef B myexcEnterSwi B myexcEnterPrefetchAbort B myexcEnterDataAbort B cold B myintEnt B cold
正如前文中一再聲稱的,在romInit函數的開始處必須放置一個系統異常向量表。前文中并沒有對此進行解釋,實際上,其中有一個十分重要的原因。對于ARM處理器而言,其硬件上要求必須在絕對地址0或者高端地址0xffff0000處建立系統異常向量表。至于是低端還是高端,則由一個寄存器位控制,通常情況下設置為低端地址。此時如當一個復位異常產生后,ARM CPU將IP寄存器的值設置為0,即跳轉到復位中斷向量處執行,完成復位異常的響應。由于系統異常向量表中每個異常只能使用4B,故一般都是一個跳轉語句,跳轉到真正的處理程序進行異常的處理。
某些平臺在集成ARM CPU時會修改該默認行為,如當一個復位異常產生時,其并非跳轉到絕對地址0處,而是跳轉到平臺Flash或者ROM占據的地址起始處,從ROM或者Flash起始地址處開始執行代碼,因為在絕對地址0處沒有存儲介質對應或者存儲介質為易失的,在上電之時尚無有效代碼。也就是說,這些平臺刻意將ARM處理器的系統異常向量表移到另一個地址處。這個地址通常就是系統上電時執行的第一條指令所在地址,也就是平臺上Flash或者ROM所在的起始地址。而對于ROM型啟動方式下的VxWorks映像而言,這個起始地址就存放著romInit函數的實現代碼,故在romInit函數的開始處必須放置一張系統異常向量表,這個向量表將在系統運行期間一直被使用,包括IRQ、FIQ中斷響應都要經過這個系統異常向量表的過渡,這一點十分重要,將直接影響代碼的編寫方式。
我們需要仔細編寫romInit函數實現將IRQ中斷響應程序設置為VxWorks內核提供的函數intEnt。事實上,在代碼預處理階段,所有的常量都將被設置為一個數字值,燒入ROM或者Flash后都是不可更改的,由于在romInit執行時,必須做到位置無關,故我們不可以直接將IRQ的跳轉指令寫成“B intEnt”,否則將造成執行路線跳轉到RAM中,因為intEnt鏈接的地址以RAM_LOW_ADRS為基地址的RAM中的地址,而此時RAM還不存在任何有效指令(直到romStart執行完畢才可以進行如此跳轉)。更深層次的原因在于ROM型VxWorks內核映像一般都是經過壓縮的,此時romInit以及romStart函數與壓縮部分不是在一起進行鏈接的,而是經過二次鏈接過程(二次鏈接過程請參考本書前文中“深入理解bootrom”一節的內容),即在romInit函數中根本無法“看到”intEnt函數的存在。必須在進入usrInit函數之后才可以使用intEnt函數地址。所以,在romInit函數IRQ向量的跳轉方式采用二次跳轉的方式進行。首先跳轉到romInit函數中定義的myintEnt標號處,這是一個相對跳轉,myintEnt則將(MY_EXC_BASE+0x10)RAM地址處的值作為PC值進行第二次跳轉,這個(MY_EXC_BASE)RAM地址指向的區域將在sysHwInit函數被初始化。具體地講,對于(MY_EXC_BASE+0x10)RAM地址處,將以intEnt函數地址進行初始化。
經過這樣一個二次跳轉,最終將IRQ的中斷入口設置為內核提供的IRQ入口函數intEnt。這個二次跳轉的思想十分巧妙和實用。
基于以上分析,我們的二次跳轉過渡語句定義如下,讀者需要結合romInit.s文件尾部一系列諸如L$_promUndef之類的定義來看。這些過渡語句將特定RAM內存處(MY_EXC_BASE)存儲的值作為PC值進行跳轉,內核初始化過程中(如在sysHwInit函數中完成)將使用VxWorks內核提供的異常響應函數地址初始化這片由MY_EXC_BASE指向的RAM內存區域,從而完成向VxWorks內核提供的異常處理程序的銜接。
myexcEnterUndef: sub sp,sp,#4 stmfd sp!,{r0} ldr r0,L$_promUndef ldr r0,[r0] str r0,[sp,#4] ldmfd sp!,{r0,pc} myexcEnterSwi: sub sp,sp,#4 stmfd sp!,{r0} ldr r0,L$_promSwi ldr r0,[r0] str r0,[sp,#4] ldmfd sp!,{r0,pc} myexcEnterPrefetchAbort: sub sp,sp,#4 stmfd sp!,{r0} ldr r0,L$_promPrefectchAbort ldr r0,[r0] str r0,[sp,#4] ldmfd sp!,{r0,pc} myexcEnterDataAbort: sub sp,sp,#4 stmfd sp!,{r0} ldr r0,L$_promDataAbort ldr r0,[r0] str r0,[sp,#4] ldmfd sp!,{r0,pc} myintEnt: sub sp,sp,#4 stmfd sp!,{r0} ldr r0,L$_promintEnt ldr r0,[r0] str r0,[sp,#4] ldmfd sp!,{r0,pc} cold: MOV r0, #BOOT_COLD /* fall through to warm boot entry */ warm: B start
冷啟動時設置對應的參數值,romStart函數將根據這個參數決定是否對有關內存區域進行清零處理。
/* copyright notice appears at beginning of ROM (in TEXT segment) */ .ascii "Copyright 1999-2004 ARM Limited" .ascii "\nCopyright 1999-2006 Windriver Limited" .balign 4 start: /* Enabling the Main Oscillator*/ /* * There have been reports of problems with certain boards and * certain power supplies not coming up after a power-on reset, * and adding a delay at the start of romInit appears to help * with this. */ TEQS r0, #BOOT_COLD MOVEQ r1, #ARM926EJS_DELAY_VALUE MOVNE r1, #1 delay_loop: SUBS r1, r1, #1 BNE delay_loop
如上代碼根據是否是冷啟動(即上電啟動)決定是否等待一段時間。某些平臺要求在上電后等待一段時間才能進行相關外設的初始化過程。當然,這個等待時間從宏觀上看比較短。
#if defined(CPU_926E) /* * Set processor and MMU to known state as follows (we may have not * been entered from a reset). We must do this before setting the CPU * mode as we must set PROG32/DATA32. * * MMU Control Register layout. * * bit * 0 M 0 MMU disabled * 1 A 0 Address alignment fault disabled, initially * 2 C 0 Data cache disabled * 3 W 0 Write Buffer disabled * 4 P 1 PROG32 * 5 D 1 DATA32 * 6 L 1 Should Be One (Late abort on earlier CPUs) * 7 B ? Endianness (1 => big) * 8 S 0 System bit to zero } Modifies MMU protections, not really * 9 R 1 ROM bit to one } relevant until MMU switched on later. * 10 F 0 Should Be Zero * 11 Z 0 Should Be Zero (Branch prediction control on 810) * 12 I 0 Instruction cache control */ /* Setup MMU Control Register */ MOV r1, #MMU_INIT_VALUE /* Defined in mmuArmLib.h */ MCR CP_MMU, 0, r1, c1, c0, 0 /* Write to MMU CR */ /* * If MMU was on before this, then we'd better hope it was set * up for flat translation or there will be problems. The next * 2/3 instructions will be fetched "translated" (number depends * on CPU). * * We would like to discard the contents of the Write-Buffer * altogether, but there is no facility to do this. Failing that, * we do not want any pending writes to happen at a later stage, * so drain the Write-Buffer, i.e. force any pending writes to * happen now. */ MOV r1, #0 /* data SBZ */ MCR CP_MMU, 0, r1, c7, c10, 4 /* drain write-buffer */ /* Flush (invalidate) both I and D caches */ MCR CP_MMU, 0, r1, c7, c7, 0 /* R1 = 0 from above, data SBZ*/ /* * Set Process ID Register to zero, this effectively disables * the process ID remapping feature. */ MOV r1, #0 MCR CP_MMU, 0, r1, c13, c0, 0 #endif /* disable interrupts in CPU and switch to SVC32 mode */ MRS r1, cpsr BIC r1, r1, #MASK_MODE ORR r1, r1, #MODE_SVC32 | I_BIT | F_BIT MSR cpsr, r1
如上代碼設置ARM CP15控制寄存器相關的控制位,如清除Cache、禁止MMU;其后設置運行模式為系統管理模式,禁止系統IRQ、FIQ中斷。
/* * CPU INTERRUPTS DISABLED * * disable individual interrupts in the interrupt controller */ MOV r1, #0x00000000 LDR r0, =IC_EINT0 STR r1, [r0] LDR r0, =IC_EINT1 STR r1, [r0]
配置外設中斷控制器寄存器,禁止所有的外設中斷。
/* * Jump to the Normal (higher) ROM Position. After a reset, the * ROM is mapped into memory from* location zero upwards as well * as in its Normal position at This code could be executing in * the lower position. We wish to be executing the code, still * in ROM, but in its Normal (higher) position before we remap * the machine so that the ROM is no longer dual-mapped from zero * upwards, but so that RAM appears from 0 upwards. */ LDR pc, L$_HiPosn HiPosn: /******************************************* set up Pinmiux *************************************************/ LDR R6, =_PINMUX0 LDR R7, =0x80000C1F STR R7,[R6] LDR R6, =_PINMUX1 LDR R7, =0x000404F1 STR R7,[R6] LDR R6, =VDD3P3V_PWDN LDR R7, =0x0 STR R7,[R6]
以上語句配置管腳復用寄存器,即根據平臺要完成的特定功能設定某些硬件管腳的功能。
/* * * Initialize the stack pointer to just before where the * uncompress code, copied from ROM to RAM, will run. */ LDR sp, =0xB000
設置初始棧,為下面調用平臺初始化代碼(用C語言編寫)做準備。注意:由于此時尚未初始化DDR控制器,故外部存儲器不可用,此處將SP設置為CPU內部的RAM空間地址,使用內存RAM作為初始棧。
mov lr,pc LDR pc,L$_rRomInit_C
以上兩個語句首先保存返回地址到lr寄存器中,其后調用Platform函數進行平臺初始化。注意函數的特殊調用方式,讀者結合romInit.s文件末尾對于L$_rRomInit_C的定義來看,這是采用一種相對調用方式(即位置無關調用方式)在進行。
/* jump to C entry point in ROM: routine - entry point + ROM base */ #if (ARM_THUMB) LDR r12, L$_rStrtInRom ORR r12, r12, #1 /* force Thumb state */ BX r12 #else /*LDR sp,=0x88000000*/ MOV r0, #BOOT_COLD LDR pc, L$_rStrtInRom #endif /* (ARM_THUMB) */
romInit函數最后通過位置無關方式跳轉到romStart函數執行。
/******************************************************************************/ /* * PC-relative-addressable pointers - LDR Rn,=sym is broken * note "_" after "$" to stop preprocessor performing substitution */ .balign 4 L$_HiPosn: .long ROM_TEXT_ADRS + HiPosn - FUNC(romInit) L$_rStrtInRom: .long ROM_TEXT_ADRS + FUNC(romStart) - FUNC(romInit) L$_rRomInit_C: .long ROM_TEXT_ADRS + FUNC(Platform) - FUNC(romInit) /* L$_rEmacInRom: .long ROM_TEXT_ADRS + FUNC(emacStart) - FUNC(romInit) */ L$_STACK_ADDR: .long STACK_ADRS L$_memSize: .long VAR(daviciMemSize) L$_promUndef: .long MY_EXC_BASE L$_promSwi: .long MY_EXC_BASE+4 L$_promPrefectchAbort: .long MY_EXC_BASE+8 L$_promDataAbort: .long MY_EXC_BASE+0xc L$_promintEnt: .long MY_EXC_BASE+0x10
romInit函數末尾是對一些符號常量的定義,前面一些表示函數的相對地址,后面則為異常向量表各表項在RAM中的位置。
2.romStart函數
romInit函數最后跳轉到romStart函數進行執行,romStart根據映像類型將原先存儲在ROM中的代碼和數據(對于rom resident映像類型而言,則是單純的數據,不包括代碼)復制到RAM中,如果存在壓縮,則在復制過程中進行解壓縮。本書前文“深入理解bootrom”一節已經對romStart函數做了詳細的分析,VxWorks內核映像與bootrom映像使用相同的romStart函數,故此處不再詳細論述。前文“映像類型“一節已經對于壓縮型和非壓縮型代碼的復制方式進行了總體敘述,此處為了加深讀者印象,我們再次對其進行說明。
非駐留ROM映像類型分為壓縮和非壓縮兩種,壓縮映像類型中只有romInit.s、bootInit.c文件是非壓縮的,其余部分代碼都是壓縮的。壓縮映像類型從ROM向RAM復制時需要分兩個階段進行,第一階段將非壓縮代碼(romInit.s、bootInit.c)從ROM中復制到RAM_HIGH_ADRS指向的RAM區域;第二階段將壓縮代碼(usrConfig.c、sysLib.c、sysALib.c、外設驅動代碼)從ROM中復制到RAM_LOW_ADRS指向的RAM區域,并在復制過程中進行解壓縮操作。注意:解壓后代碼的入口為定義在usrConfig.c文件中的usrInit函數。如圖3-6所示為壓縮型VxWorks映像類型ROM及完成復制后的RAM布局圖。

圖3-6 壓縮型VxWorks映像類型ROM及復制后的RAM布局圖
對于非ROM駐留非壓縮型VxWorks映像類型,復制過程則要簡單得多,此時只存在一次復制,所有ROM中的代碼和數據都被一次性復制到由RAM_LOW_ADRS指定的RAM區域,如圖3-7所示。

圖3-7 非壓縮型VxWorks映像類型ROM及復制后的RAM布局圖
讀者分析romStart函數實現會發現一個非常有趣的現象:當映像是非壓縮類型時,入口函數指針被賦值為usrInit函數地址;當映像是壓縮類型時,入口函數指針則被賦值為(FUNCPTR)RAM_DST_ADRS。對于VxWorks映像而言,RAM_DST_ADRS等于RAM_LOW_ADRS,即對于壓縮類型,入口函數指針被直接賦值為RAM_LOW_ADRS。實際上,這種不同的入口函數指針賦值方式是有深刻含義的。
對于非壓縮VxWorks映像類型而言,映像的生成過程只存在一次鏈接過程,所有的代碼以romInit為入口函數被鏈接到RAM_LOW_ADRS地址處。注意:在非壓縮映像復制過程中,所有的代碼從romInit開始到data段的結束都是一次性復制到RAM_LOW_ADRS指向的RAM地址處。換句話說,實際上,此時RAM_LOW_ADRS地址處放置的是romInit函數實現,其后跟隨著romStart函數實現和usrInit函數實現,所以此時我們不可以使用RAM_LOW_ADRS地址再次作為跳轉地址,否則又將跳轉回romInit函數執行。所以,必須明確以usrInit函數地址的方式對romStart之后執行的入口函數指針進行初始化。
對于壓縮性VxWorks映像類型則不然,該映像的生成使用了二次鏈接過程:第一次是壓縮代碼本身一次鏈接,其以usrInit函數為入口被鏈接到RAM_LOW_ADRS指向地址處;第二次是非壓縮代碼(romInit.s、bootInit.c)與壓縮代碼整合在一起時的鏈接過程,此時壓縮代碼作為數據部分被毫無改變地集成到最后的映像中,此時以romInit函數為入口鏈接到RAM_HIGH_ADRS指向的地址處。所以romStart復制過程中分為兩次進行復制,在第二次對壓縮代碼復制過程的同時進行了解壓縮,將usrInit為入口的代碼塊解壓到RAM_LOW_ADRS地址處,即此時RAM_LOW_ADRS地址處存儲著usrInit函數實現的第一條指令,故romStart之后執行的入口函數指針被簡單賦值為RAM_LOW_ADRS,實際上,此時賦值為usrInit也是可行的。VxWorks內核提供者使用RAM_LOW_ADRS直接賦值,其用意是否正是告知用戶其內在差別之處。所以,看似簡單的兩種不同賦值方式,其后有深刻的含義。
對于ROM駐留型映像類型,首先必須是非壓縮的,其次,romStart無須復制代碼部分,只需將數據部分復制到RAM_LOW_ADRS指向的RAM區域。讀者結合以上兩種映像類型復制方式,對此應不難理解。
無論何種ROM型映像類型,romStart完成復制后,都將跳轉到usrConfig.c文件中定義的usrInit函數執行,與下載型VxWorks映像類型殊途同歸。
3.3.2 下載型映像早期啟動流程詳解
相對于ROM型映像類型,下載型映像類型早期啟動流程則較為簡單。在進入usrInit函數前,其只執行一個函數,即sysALib.s文件中定義的sysInit函數。下載VxWorks內核映像既可以是壓縮的,也可以是非壓縮的。如果是非壓縮的,那么bootrom將直接下載VxWorks映像到RAM_LOW_ADRS指定的RAM地址處。而對于壓縮類型而言,則需要進行解壓縮的工作,此時首先需要將壓縮映像下載到其他地址處(相對于RAM_LOW_ADRS而言),再從這個地址解壓縮到RAM_LOW_ADRS,無論如何,最后位于RAM_LOW_ADRS地址處的一定是一個非壓縮的VxWorks內核映像,第一條指令就是sysInit函數實現。這就要求sysInit函數必須是sysALib.s文件中的第一個函數,否則無法做到RAM_LOW_ADRS的第一條指令就是sysInit函數實現。
當然這還要從bootrom執行完畢后跳轉到VxWorks內核映像的跳轉語句編碼方式說起。我們以常見的網絡下載方式為例,bootrom后期執行流程如下(以下這些函數都定義在bootConfig.c文件中):
usrInit→usrRoot→bootCmdLoop→autoboot→bootLoad→netLoad→bootLoadModule
bootLoadModule函數由VxWorks內核提供,傳遞給這個函數的參數有兩個:第一個參數為打開的遠程主機上VxWorks內核映像的文件句柄;第二個參數為VxWorks內核入口函數指針,該參數需要bootLoadModule函數進行初始化。當以上函數層層返回后,autoboot函數中將跳轉到bootLoadModule函數返回的VxWorks內核入口函數指針指向的地址處,即進入VxWorks內核映像執行。autoboot函數中的相關語句如下:
if (bootLoad (BOOT_LINE_ADRS, &entry) == OK) go (entry); /* ... and never return */
以上代碼中的第二個參數將層層傳遞,最后傳遞給bootLoadModule函數,由其進行賦值。事實上,bootLoadModule函數也只是一個封裝函數,其根據VxWorks內核映像的文件格式進一步調用下層函數。VxWorks映像格式一般為ELF格式,故bootLoadModule將調用bootElfModule函數,該函數最后根據ELF文件頭部中的入口函數地址對entry變量進行初始化。換句話說,是使用sysInit函數地址對entry變量進行了賦值。從這個意義上講,我們并不需要將sysInit函數定義為sysALib.s文件中的第一個函數,此時也不會對正常啟動構成任何映像,不過Wind River要求將sysInit函數定義為sysALib.s文件中的第一個函數,建議讀者遵守這個要求。
sysInit函數完成的功能如同romInit函數,但是由于其執行在其鏈接地址上,故無須位置無關。采用bootrom + VxWorks的下載啟動方式時,Wind River要求VxWorks的初始化完全不依賴于bootrom。換句話說,其要求在完成VxWorks映像的下載,VxWorks必須“忘記”bootrom的存在,即其必須進行一切使其進入正常運行狀態的準備工作,如平臺初始化。基于這個要求,事實上,我們可以將romInit函數實現中除了開始處的系統異常表之外的其他所有代碼原封不動地作為sysInit函數的實現。但是我們一般不這樣做,我們同時將平臺初始化代碼也刪除,當然sysInit最后是跳轉到usrInit,而非romStart。至此,我們完成sysInit函數的實現。
注意
在sysInit函數的實現中,并未“忘記”bootrom的存在,這一點看似與Wind River的建議相違背,但是這僅僅是一個建議,事實上,下載啟動方式下,VxWorks不可能脫離bootrom的存在,即bootrom完成VxWorks映像的下載后,依然起著關鍵作用,這就是romInit函數開始處的“系統異常向量表”,這個向量表一直被VxWorks內核使用或者說一直作為所有的系統異常到VxWorks內核的“橋梁”。所以,也就沒有必要在sysInit函數中做些無用功,還有一個根本的原因,就是某些平臺設備已經不可能重新初始化,如DDR控制器,因為VxWorks內核映像就是運行在RAM中,如重新初始化DDR控制器,初始化的基本流程是:先復位一個器件,再配置這個器件的寄存器,而一旦對DDR控制器進行復位,以后就不可能再運行VxWorks內核代碼了,談何再配置寄存器?系統將直接死機。基于這個根本原因,sysInit實現中需要將平臺初始化代碼剔除。
這一點非常重要,必須將平臺初始化代碼從sysInit函數中剔除,否則將造成系統死機。當然如果sysInit函數對其他一些非關鍵外設硬件(即不包括DDR控制器、電源管理模塊等)進行初始化,則是可行的。
sysInit函數最后跳轉到usrConfig.c文件中定義的usrInit函數執行,與ROM型VxWorks內核映像殊途同歸。下面將從usrInit函數出發,講述所有的VxWorks內核映像類型共同的啟動流程。
3.3.3 公共啟動流程詳解
任何VxWorks內核映像從usrInit函數開始,都將具有相同的執行流程。不同的映像類型在進入usrInit函數之前根據各自的特殊情況有所差別,這些差別主要是從平臺初始化的時機而言的,如ROM型映像類型,由于無其他任何外部的代碼可以依賴,其必須靠自身完成所有的初始化工作;而下載型映像類型,由于可以借助bootrom完成的工作,故其只是做些簡單的初始化。當所有映像類型的代碼執行到usrInit函數時,此時將開始內核組件和外設驅動的初始化,這對于所有的映像類型都將是一致的,所以,無論何種VxWorks內核映像類型,無論其早期啟動流程為何,在進入usrInit函數后,都具有相同的啟動流程。本節即從usrInit函數出發,較詳細地介紹所有的VxWorks內核映像類型共同的啟動流程,直到VxWorks操作系統完全啟動,進入正常運行狀態。
如下代碼示例是一個實際系統(基于ARM926EJS核)中運行的BSP usrConfig.c文件源碼,我們使用如下預處理命令對其進行了預處理。
C:\Tornado2.2\target\config\all>ccarm -E -I\h -I..\arm_cao -I. -Ic:\Tornado2.2\ target\config\all -Ic:\Tornado2.2\target/h -Ic:\Tornado2.2\target/src/config -Ic:\ Tornado2.2\target/src/drv -DCPU_926E usrConfig.c >usrConfig.i
其中,-E選項指定預處理,-I指定頭文件搜索路徑,arm_cao為BSP目錄名,CPU_926E為CPU類型定義。以下是經過預處理后的usrConfig.c文件中usrInit函數的實現代碼。
1.usrInit函數分析
void usrInit (int startType){ while (trapValue1 != 0x12348765 || trapValue2 != 0x5a5ac3c3){ ; } sysHwInit0 (); cacheLibInit (0x01, 0x02); bzero (edata, end - edata); sysStartType = startType; intVecBaseSet ((FUNCPTR *) ((char*)0)); excVecInit (); sysHwInit (); usrKernelInit (); cacheEnable (INSTRUCTION_CACHE); cacheEnable (DATA_CACHE); kernelInit ((FUNCPTR) usrRoot, 0x4000, (char *) ((end) + ((sysMemTop() - (end))/16)), sysMemTop (), 0x2000, 0); }
VxWorks內核映像中usrInit函數實現與bootrom映像中usrInit的實現基本類似,主要完成如下工作。
① 外設硬件初始化工作,此處的初始化將所有使用的外設硬件設置為“安靜”狀態:完成所有相關寄存器的配置,只等注冊中斷和使能工作。這部分工作由sysHwInit0、sysHwInit兩個函數完成。
② cache內核組件的初始化以及CPU內存cache控制寄存器的配置。這部分工作由cacheLibInit、cacheEnable兩個函數完成。
③ VxWorks內核在絕對地址0處(位于CPU的內部IRAM空間)創建系統異常向量表。這部分工作由excVecInit函數完成。注意:intVecBaseSet設置的是內核維護的外設中斷向量表的基地址,事實上,VxWorks內核并未使用該函數。換句話說,intVecBaseSet內部實現為空,外設中斷向量表的位置由內核自行進行分配。有關外設中斷向量表的更多內容,請參考本書前文“中斷”一節。
④ 對BSS段清零,設置全局變量sysStartType為啟動類型:冷啟動或熱啟動。此處設置避免了將啟動類型始終作為參數進行函數間傳遞的麻煩。
⑤ 最后創建系統的第一個任務,usrRoot為任務入口函數。注意:“end”表示VxWorks內核映像中BSS段的結束地址,即整個VxWorks內核映像的結束地址。kernelInit的調用原型如下,對照該原型可以看到各參數的含義。
void kernelInit ( FUNCPTR rootRtn, /* user start-up routine */ unsignedrootMemSize, /* memory for TCB and root stack */ char * pMemPoolStart, /* beginning of memory pool */ char * pMemPoolEnd, /* end of memory pool */ unsignedintStackSize,/* interrupt stack size */ int lockOutLevel /* interrupt lock-out level (1-7) */ );
自usrRoot函數開始,代碼開始運行在任務上下文中。VxWorks每個任務都具有自己的棧空間,這時與任務控制結構TCB作為一個整體進行分配。這個分配的連續地址空間底部用于TCB,頂部用于任務棧。第二、三個參數指定系統內存池的起始和結束地址。注意:起始地址相對于VxWorks內核映像偏移為((sysMemTop() - (end))/16),即整個可用內存空間的1/16被用于WDB內存池,我們可以從configAll.h文件中的如下定義看到如上計算式的由來。
#define WDB_POOL_SIZE ((sysMemTop() - FREE_RAM_ADRS)/16) /* memory pool for host tools */
intStackSize指預留給中斷棧的內存大小,這個內存不是由malloc分配的,而是直接從系統內存池預留,相關代碼如下:
vxIntStackBase = pMemPoolStart + intStackSize; vxIntStackEnd = pMemPoolStart; bfill (vxIntStackEnd, (int) intStackSize, 0xee); windIntStackSet (vxIntStackBase); pMemPoolStart = vxIntStackBase;
我們可以看到:中斷棧直接從內存池起始處預留,而內存池起始地址相應地被向上偏移了intStackSize。對于ARM體系結構,共有7種工作模式,每種模式實際上都需要有自己的??臻g,此處只指定了IRQ的中斷棧大小,VxWorks不使用FIQ中斷,而對于其他模式下棧的大小將按內核默認值進行分配。最后一個參數指定了intLock調用時被鎖定(即被禁止)的中斷優先級別,參數0表示禁止任何級別的中斷。
注意
實際上,中斷鎖定級別的使用十分有限,如在ARM體系結構下,此處傳入的任何參數都不會影響intLock的行為。ARM體系結構下intLock直接從系統角度禁止了IRQ中斷(即將模式寄存器中的I位置1),即禁止了所有的中斷,包括系統時鐘中斷。
2.usrRoot函數分析
void usrRoot(char * pMemPoolStart, unsigned memPoolSize){ char tyName [20]; int ix; memInit (pMemPoolStart, memPoolSize); memShowInit ();
初始化系統內存池以及內存信息顯示組件。
sysClkConnect ((FUNCPTR) usrClock, 0); sysClkRateSet (60); sysClkEnable ();
注冊系統時鐘中斷函數,設置時鐘中斷頻率,最后使能中斷。對于以上三個語句的底層含義,請參考本書前文“中斷”一節的討論。
sysTimestampEnable();
上述語句表示啟動系統時間戳定時器。
selectInit (50);
上述語句表示select內核組件初始化。select的功能是可以同時監聽多個文件輸入/輸出行為。該函數調用原型如下。
void selectInit ( int numFiles /* maximum number of open files */ );
參數表示的是最大同時可以監聽的文件句柄數。
iosInit (20, 50, "/null");
上述語句是初始化I/O子系統。iosInit調用原型如下,從中可以得知各參數含義。
STATUS iosInit ( int max_drivers, /* maximum number of drivers allowed */ int max_files, /* max number of files allowed open at once */ char *nullDevName /* name of the null device (bit bucket) */ );
iosInit函數中:
● 參數1(max_drivers):系統支持的最大驅動數。
● 參數2(max_files):系統支持的同時打開的最多文件數。
● 參數3(nullDevName):null文件的文件名。null文件可以吸收所有寫入它的內容,這個功能可以屏蔽某些打印。
consoleFd = (-1); if (3 > 0){ ttyDrv(); for (ix = 0; ix < 3; ix++) { sprintf (tyName, "%s%d", "/tyCo/", ix); (void) ttyDevCreate (tyName, sysSerialChanGet(ix), 512, 512); if (ix == 0) { strcpy (consoleName, tyName); consoleFd = open (consoleName, 2, 0); (void) ioctl (consoleFd, 4, 115200); (void) ioctl (consoleFd, 3, (0x01 | 0x02 | 0x04 | 0x10 | 0x08 | 0x20 | 0x40)); } } } ioGlobalStdSet (0, consoleFd); ioGlobalStdSet (1, consoleFd); ioGlobalStdSet (2, consoleFd);
串口設備初始化并設置標準輸入/輸出,錯誤輸出句柄均指向串口設備。自此之后,printf語句可用。
printf("Constructing components to enable cache and mmu!\n"); printf("This may take half a minute, please wait...\n\n"); usrMmuInit ();
MMU內核組件初始化,使其支持虛擬地址空間。有關MMU的詳細內容,請參考本書前文“內存管理”一節。
printf("Succeed enabling mmu and instruction,data cache!\n\n\n"); excShowInit (); excInit ();
創建tExcTask任務,在任務上下文中處理異常。當一個異常發生時,VxWorks可以暫緩對異常的處理,而將異常處理工作交給tExcTask任務,在任務上下文中進行處理。這種功能對于某些異常十分必要,因為這些異常需要一個上下文環境。注意:此處異常不同于處理器本身的異常,處理器本身的異常必須即時處理,一般不可延遲。
logInit (consoleFd, 50);
上述語句是系統打印任務初始化。該函數將創建一個系統任務,在任務上下文打印用戶信息。logMsg用于向任務維護的信息隊列中添加信息。這個函數可以在任何場合進行調用,包括中斷上下文。logInit函數調用原型如下。
STATUS logInit ( int fd, /* file descriptor to use as logging device */ int maxMsgs /* max. number of messages allowed in log queue */ );
參數1(fd):logMsg信息的輸出文件句柄。
參數2(maxMsgs):消息隊列中的最大信息條數。
sigInit ();
信號內核組件初始化。信號是任務間通信的一種有效方式,也是控制任務運行的一種方式。
pipeDrv ();
管道內核組件初始化。VxWorks下管道底層的實現建立在消息隊列之上,故提供了一種任務間面向包的信息交互方式。
stdioInit (); stdioShowInit ();
標準輸入/輸出內核組件初始化。這個函數完成標準輸入/輸出支持庫的安裝,這個庫是一個中間層,將用戶層和文件系統層銜接在一起。
hashLibInit (); dosFsLibInit( 0 ); dosVDirLibInit(); dosDirOldLibInit(); dosFsFatInit(); dosChkLibInit(); dosFsFmtLibInit(); hashLibInit (); dosFsInit (20);
MS-DOS兼容型文件系統組件初始化,該部分內容較多,此處不再展開介紹。
ramDrv ();
RAM文件系統內核組件初始化。RAM文件系統支持以RAM為介質創建一個文件系統。這個功能可用以暫存系統關鍵信息。我們可以創建一個RAM文件系統,以文件方式存儲運行時的信息,而系統下電后,這些信息又必須得到清除,RAM文件系統是一種十分有效的方式。
tffsDrv ();
TFFS內核組件初始化。TFFS是一個中間層,連接文件系統和底層Flash驅動。其將Flash模擬成硬盤設備進行讀寫,在Flash上創建文件系統。
fioLibInit (); floatInit (); mathSoftInit (); timexInit (); envLibInit (1); moduleLibInit (); loadElfInit ();
ELF文件格式解析模塊初始化。這個模塊將負責對ELF格式文件(如VxWorks內核映像)的解析,包括動態鏈接ELF文件等。
usrBootLineInit (sysStartType);
獲取啟動bootline參數,并將其寫入((0x80000000)+0x700)內存地址處,將被usrNetInit函數使用。usrBootLineInit、usrNetInit函數均定義在target/src/config/usrNetwork.c文件中。讀者可以查看二者的具體實現。
usrNetInit (((char *) ((0x80000000)+0x700)));
內核網絡組件初始化,usrNetInit的調用原型如下。
STATUS usrNetInit ( char *bootString /* boot parameter string */ );
參數表示bootline值,VxWorks內核將根據bootline解析出IP地址、網卡MAC地址等信息,用以對內核網絡支持組件進行初始化并設置網口IP地址等。
selTaskDeleteHookAdd (); cplusCtorsLink (); rBuffLibInit(); rBuffShowInit (); windviewConfig (); wdbConfig();
WDB調試引擎初始化,該引擎將負責對主機發送調試命令的響應。
printLogo (); printf (" "); printf ("CPU: %s. Processor #%d.\n", sysModel (), sysProcNumGet ()); printf (" "); printf ("Memory Size: 0x%x.", (UINT)(sysMemTop () - (char *)(0x80000000))); printf (" BSP version " "1.2" "/0" "."); printf ("\n "); printf ("WDB Comm Type: %s", "WDB_COMM_END"); printf ("\n "); printf ("WDB: %s.",((wdbRunsExternal () || wdbRunsTasking ())? "Ready" : "Agent configuration failed") ); printf ("\n\n"); shellInit (0x10000, 1);
啟動tShell任務,創建Shell命令終端。shellInit調用原型如下。
STATUS shellInit ( int stackSize, /* shell stack (0 = previous/default value) */ int arg /* argument to shell task */ ); startUsrRoutine();
啟動用戶任務。startUsrRoutine為用戶自定義函數,在其中啟動用戶層任務,完成平臺特定功能。在usrRoot函數最后調用一個用戶自定義函數是VxWorks提供給用戶的在操作系統啟動后啟動用戶任務的一種機制。對于嵌入式系統,一般都需要使用這種機制。
}
usrRoot最后啟動Shell終端命令行,完成VxWorks操作系統的啟動。整個啟動界面如下所示。(前面一部分斜體字體顯示為bootrom啟動和下載VxWorks內核映像過程,后面一部分加粗字體部分顯示為VxWorks內核自身啟動過程。)
VxWorks System Boot Copyright 1984-2002 Wind River Systems, Inc. CPU: DAVICI DM6446- ARM926E (SEED) Version: VxWorks5.5.1 BSP version: 1.2/0 Creation date: Apr 282009, 23:22:51 sysNvRamGet: 0 ff 10000 atemac-tffs=0,0(0,0)vxw:VxWorks e=192.168.1.176:ffffff00 h=192.168.1.210 g=192.168. 1.1 u=vxw pw=vxw tn=ARM926EJS o=12:87:59:e3:9b:06 Press any key to stop auto-boot... 0 auto-booting... boot device : atemac-tffs=0,0 unit number : 0 processor number : 0 host name : vxw file name : VxWorks inet on ethernet (e) : 192.168.1.176:ffffff00 host inet (h) : 192.168.1.210 gateway inet (g) : 192.168.1.1 user (u) : vxw ftp password (pw) : vxw flags (f) : 0x0 target name (tn) : ARM926EJS other (o) : 12:87:59:e3:9b:06 bootLoad:loading network interface. findCookie-input param:unitNo=0,devName=atemac. bootLoad:start netdriver... bootLoad:Done starting netdriver! Attached TCP/IP interface to atemac0. Attaching loopback interface ... Attaching network interface lo0... done. Attaching to Tffs ... done. Loading /tffs/VxWorks ... Cannot open "/tffs/VxWorks". tffsLoad Error: cannot load VxWorks image from tffs file system: errno = 0x380003. Trying load image from network ... netLoad-NetLoading... 1049840 Starting at 0x80004000... sysNvRamGet: 0 ff 10000 armGetMacAddr:mac addr=12:87:59:e3:9b:06 Attached TCP/IP interface to atemac unit 0 Attaching interface lo0...done Unable to add route to 192.168.1.0; errno = 0xffffffff. Adding 3456 symbols for standalone. ]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] ]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] ]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] ]]]]]]]]]]] ]]]] ]]]]]]]]]] ]] ]]]] (R) ] ]]]]]]]]] ]]]]]] ]]]]]]]] ]] ]]]] ]] ]]]]]]] ]]]]]]]] ]]]]]] ] ]] ]]]] ]]] ]]]]] ] ]]] ] ]]]] ]]] ]]]]]]]]] ]]]] ]] ]]]] ]] ]]]]] ]]]] ]]] ]] ] ]]] ]] ]]]]] ]]]]]] ]] ]]]]]]] ]]]] ]] ]]]] ]]]]] ] ]]]] ]]]]] ]]]]]]]] ]]]] ]] ]]]] ]]]]]]] ]]]] ]]]]]] ]]]]] ]]]]]] ] ]]]]] ]]]] ]] ]]]] ]]]]]]]] ]]]] ]]]]]]] ]]]]] ] ]]]]]] ] ]]] ]]]] ]] ]]]] ]]]] ]]]] ]]]] ]]]]]]]] ]]]]] ]]] ]]]]]]] ] ]]]]]]] ]]]] ]]]] ]]]] ]]]]] ]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] ]]]]]]]]]]]]]]]]]]]]]]]]]]]]] Development System ]]]]]]]]]]]]]]]]]]]]]]]]]]]] ]]]]]]]]]]]]]]]]]]]]]]]]]]] VxWorks version 5.5.1 ]]]]]]]]]]]]]]]]]]]]]]]]]] KERNEL: WIND version 2.6 ]]]]]]]]]]]]]]]]]]]]]]]]] Copyright Wind River Systems, Inc., 1984-2002 CPU: DAVICI DM6446- ARM926E (SEED). Processor #0. Memory Size: 0x6fffc00. BSP version 1.2/0. WDB Comm Type: WDB_COMM_END WDB: Ready. ->
注意
以上usrRoot函數是經過預處理后的輸出,對于usrConfig.c文件中定義的原函數,要比此處復雜得多。usrRoot函數執行完畢即表示VxWorks操作系統進入正常運行狀態。
3.啟動用戶代碼
一般我們需要在操作系統啟動后運行用戶應用程序,VxWorks提供如下一種機制(如下代碼被放置在usrRoot函數的最尾端,用以在VxWorks操作系統啟動完成后,啟動用戶任務):
#ifdef INCLUDE_USER_APPL /* Startup the user's application */ USER_APPL_INIT; /* must be a valid C statement or block */ #endif
用戶可以通過定義INCLUDE_USER_APPL宏,并將USER_APPL_INIT定義為用戶程序入口函數即可,如下示例。
/*config.h*/ #define INCLUDE_USER_APPL #define USER_APPL_INIT (startUsrRoutine)
用戶可以在startUsrRoutine自定義函數中創建用戶任務,實現用戶需要的功能。這種工作方式在操作系統啟動過程中啟動用戶層任務。一般嵌入式系統中都需要使用USER_APPL_INIT宏啟動用戶任務。