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种工作模式,每种模式实际上都需要有自己的栈空间,此处只指定了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宏启动用户任务。