3.2 深入理解bootrom——下载启动方式下的“瑞士军刀”

下面以压缩版本的bootrom为例,详细介绍如何生成压缩的bootrom,以及其较为详细的执行流程和调试方法。一般在开发阶段,都需要采用bootrom+VxWorks的启动方式,因为bootrom相对较小,烧录时间较短,如果bootrom可以完成如下操作:将VxWorks内核映像从外部介质(通常为FTP主机)下载到开发板RAM中,则表示bootrom已经移植成功,这也基本预示着BSP已趋近于移植成功。此外,我们也借助压缩版本bootrom的生成方式,介绍二次链接的具体细节,读者也可以借此理解压缩版本VxWorks_rom的生成方式。

3.2.1 bootrom的构成

在开发阶段,VxWorks操作系统大多采用bootrom+ VxWorks方式启动,即下载型方式进行。一方面,由于VxWorks本身调试的需要,另一方面,bootrom相比VxWorks内核较小,可以较快地烧录到平台ROM中。在下载型方式中,bootrom的主要任务就是从主机端(相对运行VxWorks的目标板而言)通过串口或者网口将VxWorks内核映像载入目标板RAM中,而后跳转到VxWorks内核映像入口处执行。bootrom完成的所有工作基本上都是为了下载VxWorks内核映像做准备。

bootrom在构成上基本类似于VxWorks内核本身,即二者使用同一套函数,但是也有一个较大的区别:bootrom使用bootConfig.c文件,而VxWorks内核本身则使用usrConfig.c文件。

在下载型启动方式下使用的VxWorks内核映像由如下文件构成:sysALib.s、sysLib.c、usrConfig.c和设备驱动程序文件。bootrom映像则由如下文件构成:romInit.s、bootInit.c、sysALib.s、sysLib.c、bootConfig.c和设备驱动程序。

注意

bootrom映像中虽然包含sysALib.s文件代码,但是其并不使用其中定义的任何函数。

sysLib.c以及设备驱动程序都是相同的,在下载启动方式下,VxWorks内核映像不包含romInit.s和bootInit.c文件。但是一旦处于产品阶段,当采用ROM启动方式时,VxWorks内核映像构成将基本类似于bootrom映像构成,即为:romInit.s、bootInit.c、sysALib.s、sysLib.c、usrConfig.c、设备驱动程序文件。

注意

ROM启动方式下,sysALib.s文件没有使用,但是仍然包含在内核映像中,可以修改系统文件中的相关宏定义,去掉该文件,但如果需要下载型VxWorks内核映像,还是要加上sysALib.s文件,故建议一直包含该文件。其中romInit.s、bootInit.c、sysLib.c、设备驱动程序与bootrom中使用的都是同一套文件,然而无论VxWorks映像是基于下载方式的,还是ROM方式的,其总是使用usrConfig.c文件,而bootrom则总是使用bootConfig.c文件。这两个文件虽然定义有相同的函数(usrInit和usrRoot),但基本实现却大不相同,bootConfig.c也进行一些初始化,如当使用网口下载VxWorks内核映像时,其需要进行网口初始化,但是正如上文所述,bootConfig.c中完成的所有工作都是为了能够从外部主机上下载真正的VxWorks操作系统映像,其本身不具有VxWorks操作系统功能部件;而usrConfig.c则不然,其需要完成维持VxWorks操作系统正常运行时所需的所有组件的初始化工作,所以usrConfig.c才是真正进行VxWorks操作系统的启动工作的。

3.2.2 bootrom脚本的创建

以下以压缩版bootrom为例,基于Powerpc平台,详细介绍压缩版bootrom的生成过程及执行流程,从而使读者对bootrom有一个彻底的了解。这对于VxWorks内核本身的移植和BSP开发都具有重要意义。

bootrom是通过命令行脚本生成的,虽然Tornado开发环境中包含生成bootrom的菜单子命令,但是最终还是通过调用命令行脚本进行bootrom的生成。

在执行生成bootrom映像的make命令之前,我们首先需要设置一些环境变量,最直接的方式是从$(WIND_BASE)/host/$(WIND_HOST_TYPE)/bin目录下运行torVars脚本文件。该文件基本实现如下:

        rem Command line build environments
        set WIND_HOST_TYPE=x86-win32
        set WIND_BASE=C:\T22
        set PATH=%WIND_BASE%\host\%WIND_HOST_TYPE%\bin;%PATH%
        rem Diab Toolchain additions
        set DIABLIB=%WIND_BASE%\host\diab
        set PATH=%DIABLIB%\WIN32\bin;%PATH%

由此,我们可以在target/config/<bspName>(target/ config/wrSbc824x)目录下创建bootrom,生成脚本如下:

        rem bootrom creator file:bootrom.bat
        rem Command line build environments
        set WIND_HOST_TYPE=x86-win32
        set WIND_BASE=C:\T22\ppc
        set PATH=C:\T22\ppc\host\x86-win32\bin;C:\WINNT\SYSTEM32;C:\WINNT;
        rem Diab Toolchain additions
        set DIABLIB=C:\T22\ppc\host\diab
        set      PATH=C:\T22\ppc\host\diab\WIN32\bin;C:\T22\ppc\host\x86-win32\bin;C:\WINNT\
    SYSTEM32;C:\WINNT;
        make bootrom
        pause

最后,pause命令的加入是为了在执行完毕后,等待用户输入任意键关闭DOS窗口,这样做的目的是为了查看执行结果,否则运行过程将一闪而过,无法得知运行过程及结果。

3.2.3 脚本运行过程分析

现在我们可以执行3.2.2节创建的脚本生成bootrom,如下是使用Tornado 2.2 wrSbc824x BSP执行该脚本的结果。

        ccppc -M -MG -w -mcpu=603-mstrict-align -ansi -O2-fvolatile -fno-builtin -Wall -I/h
        -I.    -IC:\T22\ppc\target\config\all    -IC:\T22\ppc\target/h    -IC:\T22\ppc\target/
    src/config -IC:\T22\ppc\target/src/drv -DCPU=PPC603-DTOOL_FAMILY=gnu -DTOOL=gnu eeprom.c
    i8250Sio.c m8240AuxClk.c m8240Epic.c
        sysCacheLockLib.c sysFei82557End.c sysLib.c sysNet.c sysPci.c sysPciAutoConfig.c
        sysPnic169End.c sysSerial.c sysVware.c C:\T22\ppc\target\config\all/bootConfig.c
        C:\T22\ppc\target\config\all/bootInit.c C:\T22\ppc\target\config\all/dataSegPad.c
        C:\T22\ppc\target\config\all/usrConfig.c      C:\T22\ppc\target\config\all/version.c
    >depend.wrSbc824x
        ccppc  -E  -P  -M  -w  -mcpu=603  -mstrict-align  -E  -xassembler-with-cpp  -I/h  -I.
    -IC:\T22\ppc\target\config\all    -IC:\T22\ppc\target/h    -IC:\T22\ppc\target/src/config
    -IC:\T22\ppc\target/src/drv    -DCPU=PPC603    -DTOOL_FAMILY=gnu    -DTOOL=gnu    romInit.s
    >>depend.wrSbc824x
        ccppc  -E  -P  -M  -w  -mcpu=603  -mstrict-align  -E  -xassembler-with-cpp  -I/h  -I.
    -IC:\T22\ppc\target\config\all    -IC:\T22\ppc\target/h    -IC:\T22\ppc\target/src/config
    -IC:\T22\ppc\target/src/drv    -DCPU=PPC603    -DTOOL_FAMILY=gnu    -DTOOL=gnu    sysALib.s
    >>depend.wrSbc824x

对以上三个语句进行预处理,生成源文件所依赖的头文件列表,将这些头文件列表写入depend.wrSbc824x文件中。注意这个文件的扩展名,以BSP的目录名为后缀,这是一个约定。

        ccppc -c -mcpu=603-mstrict-align -ansi -O2-fvolatile -fno-builtin -Wall -I/h -I. -IC:\
    T22\ppc\target\config\all -IC:\T22\ppc\target/h -IC:\T22\ppc\target/src/config -IC:\T22\
    ppc\target/src/drv  -DCPU=PPC603  -DTOOL_FAMILY=gnu  -DTOOL=gnu  C:\T22\ppc\target\config\
    all\bootInit.c

bootInit.c文件包含romStart()函数,是第一个被执行的C函数,其完成将代码和数据从ROM复制到RAM中,如果代码存在压缩,其在复制过程中一并完成代码的解压缩工作,在完成复制后,其跳转到已复制到RAM中的usrInit函数进行执行。

        ccppc   -mcpu=603   -mstrict-align   -ansi   -O2   -fvolatile   -fno-builtin   -I/h   -I.
    -IC:\T22\ppc\target\config\all -IC:\T22\ppc\target/h -IC:\T22\ppc\target/src/config -IC:\
    T22\ppc\target/src/drv -DCPU=PPC603-DTOOL_FAMILY=gnu -DTOOL=gnu -P -xassemblerwith-cpp -c
    -o romInit.o romInit.s

romInit.s文件包含了bootrom入口函数romInit(),该函数在系统上电时是第一个被执行的函数,在编码时必须注意在函数起始处放置一个中断向量表或复位向量,因为系统上电起始阶段,CPU都会收到一个复位中断,跳转到复位中断向量表处执行。romInit完成硬件相关寄存器的初始化。注意:某些硬件寄存器只能在上电复位后被配置一次,这些寄存器的配置就在romInit()函数中完成。romInit()函数执行完毕后,将跳转到romStart()函数执行。

        ccppc  -c  -mcpu=603  -mstrict-align  -ansi  -O2  -fvolatile  -fno-builtin  -Wall  -I/h-I.
    -IC:\T22\ppc\target\config\all -IC:\T22\ppc\target/h -IC:\T22\ppc\target/src/config -IC:\
    T22\ppc\target/src/drv   -DCPU=PPC603   -DTOOL_FAMILY=gnu   -DTOOL=gnu   C:\T22\ppc\target\
    config\all\bootConfig.c

bootConfig.c文件包含usrInit()函数,完成平台的进一步初始化(主要是外围设备初始化)、VxWorks内核的下载等工作。不建议直接修改target/config/All目录下的bootConfig.c文件,因为All目录下的文件将被所有的BSP共享,所以对于一个特定的BSP,如果需要修改bootConfig.c文件,建议用户从All目录下复制一份bootConfig.c文件到BSP目录下(假设重命名为bootConfig_copy.c),并在Makefile中定义如下的宏:

        BOOTCONFIG=./bootConfig_copy.c

在编译时,将使用BSP目录下的bootConfig_copy.c文件,此时我们按照需要自动对bootConfig_copy.c文件进行修改,而不影响其他BSP对All目录下系统bootConfig.c文件的依赖。

        ccppc   -mcpu=603   -mstrict-align   -ansi   -O2   -fvolatile   -fno-builtin   -I/h   -I.
    -IC:\T22\ppc\target\config\all    -IC:\T22\ppc\target/h    -IC:\T22\ppc\target/src/config
    -IC:\T22\ppc\target/src/drv      -DCPU=PPC603      -DTOOL_FAMILY=gnu      -DTOOL=gnu      -P
    -xassemblerwith-cpp -c -o sysALib.o sysALib.s

sysALib.s文件虽然也包含在bootrom映像中,但是并非使用该文件中定义的任何函数。当然由于这是一个汇编文件,如果需要在romInit.s文件之外编写一段汇编代码实现某种特殊目的,可以加入到sysALib.s文件中。但是要保证sysInit函数必须是sysALib.s文件中定义的第一个函数,下载方式的VxWorks内核启动过程依赖这一点。一般而言,对于bootrom和ROM型VxWorks内核映像而言,都不需要使用sysALib.s文件中的代码。这个文件只被下载型VxWorks内核映像使用,该文件中定义的sysInit函数是下载型VxWorks内核映像执行的入口函数。

        ccppc  -mcpu=603  -mstrict-align  -ansi  -O2  -fvolatile  -fno-builtin  -Wall  -I/h  -I.
    -IC:\T22\ppc\target\config\all    -IC:\T22\ppc\target/h    -IC:\T22\ppc\target/src/config
    -IC:\T22\ppc\target/src/drv -DCPU=PPC603-DTOOL_FAMILY=gnu -DTOOL=gnu-c sysLib.c

sysLib.c文件中定义了关键结构数组,完成内存映射过程。一般驱动源码也被直接包含在该文件中。该文件是BSP中必需的文件,其中定义了一些初始化过程中调用的关键函数,包括文件名本身也是事先约定的,必须命名为sysLib.c,不可随意更改。

        ccppc  -c  -mcpu=603  -mstrict-align  -ansi  -O2  -fvolatile  -fno-builtin  -Wall  -I/h-I.
    -IC:\T22\ppc\target\config\all    -IC:\T22\ppc\target/h    -IC:\T22\ppc\target/src/config
    -IC:\T22\ppc\target/src/drv   -DCPU=PPC603   -DTOOL_FAMILY=gnu   -DTOOL=gnu   -o   version.o
    C:\T22\ppc\target\config\all/version.c

version.c文件是一个实现上较为简单的文件,用以生成映像的版本信息、映像创建时间和日期。

        ldppc -o tmp.o -X -N -e usrInit -Ttext 01F00000 bootConfig.o version.o sysALib.o
        sysLib.o               --start-group               -LC:\T22\ppc\target/lib/ppc/PPC603/gnu
    -LC:\T22\ppc\target/lib/ppc/PPC603/common  -lcplus  -lepcommon  -lepdes  -lgnucplus  -lsnmp
    -lvxcom -lvxdcom -larch -lcommoncc -ldcc -ldrv -lgcc -lnet -los -lrpc
        -ltffs-lusb -lvxfusion -lvxmp -lwdb -lwind -lwindview
        C:\T22\ppc\target/lib/libPPC603gnuvx.a  --end-group  -TC:\T22\ppc\target/h/tool/gnu/
    ldscripts/link.RAM

创建bootrom中压缩部分可执行代码,这部分代码在romStart()函数将被解压缩到RAM_HIGH_ADRS指定的内存地址处。这是压缩版bootrom映像类型中的压缩部分代码,这部分代码进行独立链接,我们称之为bootrom创建过程中的第一次链接。注意这次链接指定的链接地址“-Ttext 01F00000”,其中“01F00000”就是RAM_HIGH_ADRS常量的值。这一点非常重要,压缩版bootrom映像中的压缩部分被解压缩到RAM_HIGH_ADRS地址处,在完成romStart()函数执行后,将直接跳转到usrInit()函数进行执行,而usrInit()函数已被解压缩到RAM_HIGH_ARDS地址处,所以usrInit()函数的链接地址也必须是RAM_HIGH_ADRS。注意,sysALib.o也被包含进bootrom映像中,虽然实际上bootrom并不需要sysALib.s中定义的任何函数。

从以上语句可以看出,这次链接并不包括romInit.s、bootInit.c两个文件,因为这两个文件的代码作为压缩bootrom映像中的唯一非压缩代码存在,所有的压缩代码并不能直接执行,故硬件的原始初始化代码(即romInit.s)以及解压缩代码本身(即bootInit.c)都必须是非压缩状态的。

由于只有非压缩代码和压缩代码整合成单一映像文件时,还需要进行一次链接(即二次链接),且链接地址与本次并不相同,故必须对本次链接后的文件(tmp.o)进行处理,避免二次链接时对已链接的代码造成修改。我们首先调用objcopyppc将连接后的ELF格式文件转换成纯二进制可执行文件。

        C:\T22\ppc\host\x86-win32\bin\objcopyppc -O binary --binary-without-bss tmp.o tmp.out

而后对这个纯二进制可执行文件完成压缩操作:

        C:\T22\ppc\host\x86-win32\bin\deflate < tmp.out > tmp.Z
        Deflation: 60.52%

由于需要在二次链接中包含这个被压缩文件,所以必须以一种特殊的方式将这些二进制代码嵌入最后的bootrom映像中,且不对其中的内容(已链接的可执行二进制纯代码)造成任何影响。

        C:\T22\ppc\host\x86-win32\bin\binToAsm tmp.Z >bootrom.Z.s

binToAsm将压缩后的文件转换成一个汇编文件,压缩块作为汇编文件中的一个数据块而存在,这样避免了二次链接过程中对压缩块的链接操作。在这个汇编文件中,专门定义了两个变量表示这个压缩块的开始和结尾,便于解压缩时对压缩块进行定位。其中binArrayStart表示压缩块的起始地址,binArrayEnd则表示压缩块的结束地址。这两个变量包含的压缩数据最后将被romStart()函数解压缩到RAM_HIGH_ADRS指定的内存地址处。

由于是一个汇编源文件,当然,首先我们需要将其编译成目标文件,代码如下。

        ccppc   -mcpu=603   -mstrict-align   -ansi   -O2   -fvolatile   -fno-builtin   -I/h   -I.
    -IC:\T22\ppc\target\config\all    -IC:\T22\ppc\target/h    -IC:\T22\ppc\target/src/config
    -IC:\T22\ppc\target/src/drv      -DCPU=PPC603      -DTOOL_FAMILY=gnu      -DTOOL=gnu      -P
    -xassemblerwith-cpp -c -o bootrom.Z.o bootrom.Z.s

        ccppc  -c  -mcpu=603  -mstrict-align  -ansi  -O2  -fvolatile  -fno-builtin  -Wall  -I/h-I.
    -IC:\T22\ppc\target\config\all    -IC:\T22\ppc\target/h    -IC:\T22\ppc\target/src/config
    -IC:\T22\ppc\target/src/drv -DCPU=PPC603-DTOOL_FAMILY=gnu -DTOOL=gnu -o version.o
        C:\T22\ppc\target\config\all/version.c

再次编译version.c文件,这次是针对bootrom本身,而上一次则是用于压缩块中的代码。

        ldppc -X -N -e _romInit -Ttext 00100000-o bootrom romInit.o bootInit.o version.o
        bootrom.Z.o             --start-group             -LC:\T22\ppc\target/lib/ppc/PPC603/gnu
    -LC:\T22\ppc\target/lib/ppc/PPC603/common -lcplus -lepcommon -lepdes -lgnucplus
        -lsnmp -lvxcom -lvxdcom -larch -lcommoncc -ldcc -ldrv -lgcc -lnet -los -lrpc
        -ltffs -lusb -lvxfusion -lvxmp -lwdb -lwind -lwindview
        C:\T22\ppc\target/lib/libPPC603gnuvx.a    --end-group    -TC:\T22\ppc\target/h/tool/
    gnu/ldscripts/link.RAM

完成二次链接过程,将非压缩部分和压缩部分最终整合成压缩版本的bootrom映像文件。指定的入口函数为romInit,链接地址指定为RAM_LOW_ADRS=0x00100000。那么为何不是ROM_TEXT_ADRS?这一点在下文中做解释。

最后一步是检查bootrom的大小,查看平台ROM或Flash是否能够放得下,平台ROM或Flash大小通过ROM_SIZE定义,这个常量如同RAM_HIGH_ADRS、RAM_LOW_ADRS等常量一样,同时定义在Makefile和config.h文件中,且这两个文件中定义的值必须一致。

        C:\T22\ppc\host\x86-win32\bin\romsize ppc -b 00080000 bootrom
        bootrom: 16784(t) + 206048(d) = 222832 (301456 unused)

至此,我们完成压缩版本bootrom的生成。对于非压缩版本的bootrom,其生成过程相对比较简单,此时不需要第一次的链接,只需要在最后一次链接中包含所有的目标文件即可。结合以上的说明,这一点应该不难理解。

3.2.4 bootrom的重定位

bootrom中较为关键的一个函数就是代码从ROM向RAM重定位的过程,这个过程集中实现在romStart()函数中,对于压缩版本的bootrom映像,该函数将分两个阶段进行代码的复制过程:第一阶段完成非压缩代码从ROM到RAM的复制;第二阶段完成压缩代码从ROM到RAM的复制,并同时完成解压缩操作。非压缩代码和压缩代码在RAM中的目的地址将不同,这与各自链接时指定的链接地址有关。romStart()函数实现包含大量的宏定义,在阅读该函数时很不方便。可以使用一个有效的调试技巧,即通过编译器的预处理功能,去掉宏定义,这样可以让代码简洁得多。用户可以通过在命令行使用如下命令达到以上目的:

        make ADDED_CFLAGS=-E file.o >file.i

这将取出源代码中所有的空行和以“#”开头的语句,同时宏定义将被解析,此时可以直接看到哪些代码被使用,避免源码中大量的条件宏对阅读分析代码造成的严重不便。如下是在命令行使用命令“make ADDED_CFLAGS=-E bootInit.o >bootInit.i”得到的romStart()函数实现。注意:某些情况下,如上命令不可用,此时可以使用如下命令对源文件直接进行预处理。

        C:\Tornado2.2\target\config\all>ccarm  -E  -I\h  -I..\<bspName>  -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  bootInit.c >bootInit.i
        void romStart
        (
        register int startType
        )
        {
            Volatile FUNCPTR absEntry;
            ((FUNCPTR)(((UINT) copyLongs - (UINT)romInit) + 0xFFF00100 ) ) (0xFFF00100,
                  (UINT)romInit,((UINT)binArrayStart - (UINT)romInit)/ sizeof (long));
            ((FUNCPTR)(((UINT)   copyLongs   -   (UINT)romInit)   +   0xFFF00100   )   )   ((UINT*)
    ((UINT)0xFFF00100 + ((UINT)(((int)( binArrayEnd ) & ~( sizeof(long) -1)) ) -(UINT)romInit)),
    (UINT *)(((int)( binArrayEnd ) & ~( sizeof(long) -1)) ) ,
            ((UINT)wrs_kernel_data_end - (UINT)binArrayEnd) / sizeof (long));
        if (startType & 0x02 ) //检查是否是上电启动(即冷启动),如是,则对内存相关区域清零。
        {
            fillLongs ((UINT *)((0x00000000 + 0x4400 ) ),((UINT)romInit -0x1000-
                  (UINT)(0x00000000 + 0x4400 ) ) / sizeof(long), 0);
            fillLongs ((UINT *)wrs_kernel_data_end,((UINT)(0x00000000 + 0x04000000-
                  0x02000000 ) - (UINT)wrs_kernel_data_end) / sizeof (long), 0);
            *(((char *) (0x00000000 + 0x4200 )) ) = '\0' ;
        }
        {
            if (inflate ((UCHAR *)(((UINT) binArrayStart - (UINT)romInit) + 0xFFF00100) ,
                (UCHAR *)0x01F00000 , binArrayEnd - binArrayStart) != 0 )
                  return;
            absEntry = (FUNCPTR)0x01F00000 ;
        }
        (absEntry) (startType);
        }

代码行:

        ((FUNCPTR)(((UINT) copyLongs - (UINT)romInit) + 0xFFF00100 ) ) (0xFFF00100,
        (UINT)romInit,((UINT)binArrayStart - (UINT)romInit)/ sizeof (long));

完成第一阶段非压缩代码从ROM向RAM的复制过程。注意:由于代码仍然执行在ROM中,故对copyLongs函数的调用必须使用相对寻址,由于copyLongs函数链接地址是在RAM空间,而目前代码尚未从ROM复制到RAM,如果直接使用copyLongs,将造成非法指令异常,从而导致系统崩溃。此次copyLongs调用将romInit和romStart函数代码从ROM直接复制到RAM中,由于这些代码是非压缩的,直接复制即可。参数1(0xFFF00100)的值是ROM_TEXT_ADRS常量指定的值,也是bootrom烧录到ROM或Flash在全局地址空间的地址。如果ROM_TEXT_ADRS等于ROM_BASE_ADRS,那么就是ROM或Flash的起始地址,系统上电后,将首先从这里开始执行代码。参数直接使用romInit函数地址作为目的地址,在链接时,romInit函数被链接到RAM_LOW_ADRS地址处,也就是说,romInit()和romStart()函数被复制到了RAM_LOW_ADRS指定的内存处,这也是为何对于romInit实现中以及当前对于copyLongs函数的调用必须做到地址无关,或者只能使用相对地址进行调用,因为一旦直接进行调用,那么CPU的指令寄存器将使用链接时的地址作为地址去读取指令,即从RAM_LOW_ADRS指定的内存区域读取指令,而在romInit函数执行时以及当前copyLongs函数调用之时,ROM中代码尚未复制到RAM中,所以必须避免在这之前进行函数的直接调用,而要使用相对调用,即通常所说的PIC(Positon Independent Code,位置无关代码)。

语句行:

        ((FUNCPTR)(((UINT)     copyLongs     -     (UINT)romInit)     +     0xFFF00100     )     )
    ((UINT*)((UINT)0xFFF00100  +  ((UINT)(((int)(  binArrayEnd  )  & ~(  sizeof(long)  -  1))  )
    -(UINT)romInit)), (UINT *)(((int)( binArrayEnd ) & ~( sizeof(long) -1)) ) ,
              ((UINT)wrs_kernel_data_end - (UINT)binArrayEnd) / sizeof (long));

完成数据段由ROM到RAM的复制。注意:binArrayEnd指向压缩块的结束为止,即实际上是代码段的尾部,其后是数据段,而wrs_kernel_data_end变量则表示数据段的尾部,其后是BSS段,故以上代码即可完成数据段的复制。

语句行:

        if (inflate ((UCHAR *)(((UINT) binArrayStart - (UINT)romInit) + 0xFFF00100) ,
                (UCHAR *)0x01F00000 , binArrayEnd - binArrayStart) != 0 )
              return;

完成压缩块的解压缩。

● 参数1 (UINT) binArrayStart - (UINT)romInit) + 0xFFF00100:计算压缩块在ROM中的地址,注意:binArrayStart在二次链接中指向压缩块的起始地址,而binArrayEnd则指向压缩块的结束地址。0xFFF00100为ROM_TEXT_ADRS的值,即存放bootrom的起始ROM地址。

● 参数2 (UCHAR *)0x01F00000:这个常量实际上就是RAM_HIGH_ADRS,在经过预处理后,被直接置换成RAM_HIGH_ADRS表示的值。

● 参数3 binArrayEnd - binArrayStart:表示压缩块的大小,inflate函数调用时需要指定被解压缩块的大小。

以上inflate函数的操作实际上就是将压缩块从ROM中解压缩到RAM_HIGH_ADRS指定的RAM地址处,我们参见前文中压缩块的生成过程,则可以看到压缩块入口函数为usrInit,即romStart函数完成对压缩bootrom的两次复制后,就会跳转到usrInit函数执行。

另外注意:到此处对于inflate的调用已经是函数直接调用方式,而非位置无关调用方式,因为在romStart函数中对两个copyLongs的调用已经将inflate代码从ROM处复制到RAM_LOW_ADRS指定的RAM地址处,且数据段也已经复制完毕,故可以直接使用通常的函数调用方式,这个inflate函数实际上是执行的在RAM区域的代码,已经完全脱离ROM中代码了。

语句行:

        absEntry = (FUNCPTR)0x01F00000 ;
        (absEntry) (startType);

完成跳转,进入usrInit函数进行执行。

romStart函数中完成的二次复制过程见图3-1 所示。对于bootrom而言,其使用bootConfig.c文件,故romStart最后跳转到bootConfig.c文件中定义的usrInit函数。该函数在bootrom生成过程中的第一次链接中被链接到RAM_HIGH_ADRS指定的地址处(此处即为0x01F00000),进入usrInit函数后,接下来所有执行的代码都是在RAM中进行的了。

3.2.5 RAM中运行的bootrom代码

romStart函数完成执行后,跳转到usrInit函数执行,自此之后,bootrom代码的执行从ROM转移到RAM中。bootrom将依次执行usrInit、usrRoot、bootCmdLoop三个函数。现在我们看一下bootConfig.c文件中定义的这三个函数具体实现的功能。

1.usrInit函数

我们对bootConfig.o依然使用make ADDED_CFLAGS=-E bootConfig.o >bootConfig.i命令。

        void usrInit
        (
        int startType
        )
        {
            while (trapValue1 != 0x12348765 || trapValue2 != 0x5a5ac3c3 )
            {
                ;
            }
            cacheLibInit (0x02 , 0x02 );
            bzero (edata, end - edata);
            sysStartType = startType;
            intVecBaseSet ((FUNCPTR *) ((char *) 0x0) );
            excVecInit ();
            sysHwInit ();
            usrKernelInit ();
            cacheEnable (INSTRUCTION_CACHE);
            kernelInit ((FUNCPTR) usrRoot, (24000) ,(char *) (end) ,sysMemTop (), (5000) , 0x0 );
        }

语句行:

            while (trapValue1 != 0x12348765 || trapValue2 != 0x5a5ac3c3 )
            {
            ;
        }

用以进行检查代码段复制是否成功完成,成功完成的含义如是否对齐到合适的内存位置,以及在复制过程中数据是否完好等。

trapValue1和trapValue2是定义在bootConfig.c文件中的两个volatile型变量,如下所示。

        #define TRAP_VALUE_1  0x12348765
        #define TRAP_VALUE_2  0x5a5ac3c3
        LOCAL volatile UINT32 trapValue1    = TRAP_VALUE_1;
        LOCAL volatile UINT32 trapValue2    = TRAP_VALUE_2;

语句行:

cacheLibInit (0x02 , 0x02 );

初始化cache内核库,且根据参数值配置CPU相关寄存器、开启或者关闭系统cache。由于bootrom并不使用CPU MMU单元,故cache的使能与否只能通过系统寄存器进行控制,这是全局范围内控制cache的方式,VxWorks内核映像中使用MMU单元,此时可以控制单个页面cache的使能与否。如果在bootrom调试阶段配置外设时出现一些问题,建议关闭系统cache,可以通过在config.h文件定义如下语句完成系统cache的关闭。

        #undef INCLUDE_CACHE_SUPPORT

注意

外设寄存器操作必须设置为non-cachable,否则将可能出现一些很难调试的硬件问题。所以,如果使用cache,对于表示外设寄存器的变量都必须使用volatile修饰符进行修饰,或者如上文建议的,在bootrom中直接关闭系统cache,当然这会对RAM的使用造成一定的性能影响,但对于bootrom而言,这一点可以不用过分计较。

语句行:

        bzero (edata, end - edata);

对BSS段清零。从数据段尾部到bootrom映像尾部都将被清零。

语句行:

        sysStartType = startType;
        intVecBaseSet ((FUNCPTR *) ((char *) 0x0) );
        excVecInit ();

完成异常表(系统中断向量表)的建立。注意:intVecBaseSet函数内核实现为空,不依赖于输入参数,异常表的位置将根据特定平台上处理器的要求进行,如ARM处理器一般将系统中断向量表建立在绝对地址0处。此处我们还将启动类型赋值给一个全局变量sysStartType,便于bootConfig.c中定义的其他函数直接使用,而不用一直以参数形式进行传递。这三条语句的执行基本上都会成功,不会有什么问题,除非在创建系统中断向量表的内存区域不可写入。语句行:

        sysHwInit ();

完成平台所有外设的配置,将所有的外设配置到有效状态,但是都暂时进入“安静”或“预知”状态。这表示外设已经被配置到可以随时准备工作了,现在就剩下启动有关工作“使能”位了。当然对于有些需要使用中断方式工作的外设而言,此时还没有完成中断服务程序的注册,这个注册将在sysHwInit2函数中完成。sysHwInit函数定义在sysLib.c文件中。

注意

对于外设中断服务程序注册的时机,必须将其放在sysHwInit2函数被调用时,这一点非常重要,因为在intLibInit函数尚未调用,之前,bootrom映像尚未创建外设中断表,故如果在此之前进行注册,那么将无从对这些注册的中断服务程序句柄进行保存。而对于intLibInit函数的调用,一般将其放置在sysHwInit2函数开始处。

语句行:

        usrKernelInit ();

完成bootrom内核的一些初始化工作,bootrom主要的功能虽然是从外部下载一个VxWorks内核映像,但其本身也是一个小的内核,它支持任务创建、任务调度等一系列VxWorks内核功能,只是其并不作为一般应用开发的平台。usrKernelInit函数完成对内核一些关键数据结构的初始化,如涉及任务的三个队列readyQHead、activeQHead、tickQHead等。

usrKernelInit函数定义在$(WIND_BASE)/target/src/config/usrKernel.c文件中,用户可以直接查看其源代码。

语句行:

        cacheEnable (INSTRUCTION_CACHE);

开启系统指令cache。如果调试过程中出现一些异常的问题,建议首先关闭cache。语句行:

        kernelInit ((FUNCPTR) usrRoot, (24000) ,(char *) (end) ,sysMemTop (), (5000) , 0x0 );

创建内核任务。这是系统的第一个任务,usrRoot作为入口函数。

注意

在romInit函数中,我们从系统角度通过配置CPU的相关控制寄存器关闭了系统中断,而当任务创建时,系统中断将默认被开启,这一点非常重要。有时我们会怀疑既然在romInit函数中从全局角度禁止了中断,为何在后续代码中都找不到中断开启的代码,然而中断却能正常响应。实际上,这个系统中断重新开启的工作由此处任务创建过程中完成,这是作为任务本身创建(其中包括对硬件寄存器的初始化)的一部分完成的。故在后续代码中,我们只需要开启中断控制器相关中断屏蔽位即可。有关中断方面的详细说明,请参考本书中相关章节对中断的分析。

kernelInit函数调用可能会出现问题,因为其在任务创建过程中已经自动开启系统中断,故一旦进入usrRoot函数运行,CPU就可对中断进行响应,但是现在sysHwInit2函数尚未调用。换句话说,现在所有的中断服务程序都没有进行注册,如果某个外设由于在sysHwInit函数中配置不合理,没有进入应有的“安静”状态,从而产生了一个不适合的中断,那么将直接导致系统的死机,因为CPU跳转到对应中断句柄执行时,将遇到“undefined instruction”系统异常。

注意

在调试过程中,如果系统运行后就死机,那么首先怀疑的应该就是不合适的中断产生,而对应中断服务程序却尚未注册。笔者曾经调试过一个网卡驱动,一旦开始启动网卡驱动(即调用endStart函数后),系统就立刻死机。检查了所有的寄存器配置都没有发现问题,最后问题出在弄错了网卡设备的中断号,即将网卡中断服务程序注册到另一个设备的中断号上,结果是网卡中断号没有对应的中断服务程序,所以网卡一产生中断,系统就死机。故“不适合的中断产生”并非不应该产生中断,而是首先确定系统当前是否允许该中断产生,其次,该中断是否已经注册有中断服务程序。特别注意:不要弄错中断号。

2.usrRoot函数

kernelInit函数将不再返回,在任务创建后,usrRoot任务将立刻得到运行,因为当前系统内只有这一个任务。注意:之前的代码都没有任务上下文,读者可以将之前代码的运行想象为运行在中断上下文中。当进入usrRoot函数执行时,此时所有的代码将在一个任务上下文中运行,故可以调用malloc分配内存等。usrRoot函数用以初始化bootrom小系统的其他内核组件。

        void usrRoot
        (
            char * pMemPoolStart,
            unsigned memPoolSize
        )
        {
            char tyName [20];
            int ix;
            int count;
            END_TBL_ENTRY* pDevTbl;
            memInit (pMemPoolStart, memPoolSize);
            sysClkConnect ((FUNCPTR) usrClock, 0);
            sysClkRateSet (60 );
            sysClkEnable ();
            selectInit (50 );
            iosInit (20 , 50 , "/null");
            consoleFd = (-1) ;
            if (1 > 0)
            {
                ttyDrv();
                for (ix = 0; ix < 1 ; 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 , 9600 );
                        (void) ioctl (consoleFd, 3 ,0x01 | 0x02 | 0x04 | 0x08 );
                      }
                }
        }
        ioGlobalStdSet (0 , consoleFd);
        ioGlobalStdSet (1 , consoleFd);
        ioGlobalStdSet (2 , consoleFd);
        pipeDrv ();
        excShowInit ();
        excInit ();
        excHookAdd ((FUNCPTR) bootExcHandler);
        logInit (consoleFd, 5);
        bootElfInit ();
        muxMaxBinds = 8 ;
        if (muxLibInit() == (-1) )
            return;
        for (count = 0, pDevTbl = endDevTbl; pDevTbl->endLoadFunc != ((void *)0) ;
                pDevTbl++, count++)
        {
            cookieTbl[count].pCookie = muxDevLoad (pDevTbl->unit, pDevTbl->endLoadFunc,
            pDevTbl->endLoadString,pDevTbl->endLoan, pDevTbl->pBSP);
            if (cookieTbl[count].pCookie == ((void *)0) )
            {
                printf ("muxLoad failed!\n");
            }
            cookieTbl[count].unitNo=pDevTbl->unit;
            bzero((void *)cookieTbl[count].devName,8 );
            pDevTbl->endLoadFunc((char*)cookieTbl[count].devName, ((void *)0) );
        }
        taskSpawn  ("tBoot",  bootCmdTaskPriority,  bootCmdTaskOptions,  bootCmdTaskStackSize,
    (FUNCPTR) bootCmdLoop,0,0,0,0,0,0,0,0,0,0);
        }

语句行:

        memInit (pMemPoolStart, memPoolSize);

初始化系统内存堆,从而使malloc/free函数可用。此函数调用完成后,接下来的代码就可用malloc分配内存空间了。

语句行:

        sysClkConnect ((FUNCPTR) usrClock, 0);
        sysClkRateSet (60 );       //设置系统时钟tick间隔,即1s将产生60次系统时钟中断
        sysClkEnable ();

完成bootrom小系统内核外设中断表的创建,注册系统时钟中断,配置相关外设注册其中断服务程序。

注意

sysHwInit2函数在sysClkConnect函数中被调用。关于这三个函数的详细说明,请读者参考本书中相关章节对中断的说明。

以上三个函数完成执行后,系统将正式具备“脉搏”,系统时钟将以固定的时间间隔产生中断,此时taskDelay、wdStart函数都将变得有效。

语句行:

        selectInit (50 );
        iosInit (20 , 50 , "/null");
        consoleFd = (-1) ;
        if (1 > 0)
        {
            ttyDrv();
            for (ix = 0; ix < 1 ; 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 , 9600 );
        (void) ioctl (consoleFd, 3 ,0x01 | 0x02 | 0x04 | 0x08 );
                }
              }
        }
        ioGlobalStdSet (0 , consoleFd);
        ioGlobalStdSet (1 , consoleFd);
        ioGlobalStdSet (2 , consoleFd);

进行串行终端的初始化,并设置0、1、2三个默认句柄指向串口,此后程序中所有的打印将通过串口输出。串口设备属于字符设备的一种,但是VxWorks串口驱动编写又与普通字符设备存在一些差异,这是由于VxWorks内核本身为串口提供了一个额外的TTY中间层,如此可以简化串口驱动的设计并提供串口字符收发效率。有关串口和字符驱动,请参考本书后面章节的内容。

在完成以上语句的执行后,接下来的代码就可以调用printf语句显示信息了。当然logMsg函数尚不可进行调用,因为尚未初始化log库,这将在接下来的代码中完成。

如果在Shell下输入“devs”命令,将显示当前的设备列表,串口设备名显示为“/tyCo/0”,如果存在多个串口,最后的数字依次增加,如“/tyCo/1”表示第二个串口设备。

语句行:

        pipeDrv ();
        excShowInit ();
        excInit ();
        excHookAdd ((FUNCPTR) bootExcHandler);

完成管道设备内核初始化(pipeDrv)、异常信息(如异常发生时各寄存器值)显示库初始化(excShowInit)、异常处理任务tExcTask创建(excInit)以及异常发生时钩子函数的注册(excHookAdd)。

语句行:

        logInit (consoleFd, 5);
        bootElfInit ();

完成对tLogTask任务(logInit)的创建,此后就可以调用logMsg进行信息显示。logMsg可以在任何环境下(包括中断上下文)被调用。通过logMsg打印的信息将暂时存储到一个内核队列中,由tLogTask任务负责将这个队列中的信息在任务上下文中发送出去。在外设驱动调试时,如果使用logMsg打印,如果外设驱动存在问题造成系统死机,那么要显示的信息可能无法打印,此时可以使用printf替代。logMsg大多使用在系统一切正常时一般信息的显示,调试时建议直接使用printf函数。bootElfInit完成内核中ELF模块的初始化,将系统模块的默认读取器设置为ELF格式,如下载型VxWorks内核映像就是ELF格式的。

语句行:

        muxMaxBinds = 8 ;
        if (muxLibInit() == (-1) )
              return;
        for (count = 0, pDevTbl = endDevTbl; pDevTbl->endLoadFunc != ((void *)0) ;
                pDevTbl++, count++)
        {
              cookieTbl[count].pCookie = muxDevLoad (pDevTbl->unit, pDevTbl->endLoadFunc,
              pDevTbl->endLoadString,pDevTbl->endLoan, pDevTbl->pBSP);
            if (cookieTbl[count].pCookie == ((void *)0) )
            {
                printf ("muxLoad failed!\n");
            }
            cookieTbl[count].unitNo=pDevTbl->unit;
            bzero((void *)cookieTbl[count].devName,8 );
            pDevTbl->endLoadFunc((char*)cookieTbl[count].devName, ((void *)0) );
        }

完成网络设备驱动的初始化。endDevTbl数组定义在configNet.h文件中,定义了当前平台具有的所有可用网络接口设备以及对应的初始化入口函数(如armEndLoad)。

以上代码遍历endDevTbl数组中的各个元素,对其调用muxDevLoad函数,该函数将进一步调用用户网口驱动中实现的初始化函数(如armEndLoad)。

注意

● 对于网口驱动初始化函数,存在两次调用:第一次传入初始化函数的第一个参数被设置为NULL,这表示让初始化函数仅仅返回网口设备名称,不要对网络设备进行硬件初始化;第二次才是正规的调用,此时初始化函数需要对网络设备硬件寄存器进行配置,使网络设备进入准备工作状态。关于网络设备驱动的细节,请参见本书后续相关章节内容。

● 以上代码的执行是有条件的,如果VxWorks内核映像的下载是通过串口进行的(虽然较慢,但是没有办法),那么就不需要在bootrom中对网口进行配置,以上语句可以全部删除。然而对于外设驱动的调试,一般都是通过调试bootrom来进行的,而非直接调试VxWorks。因为二者使用同一套外设驱动程序,只是在内核组件初始化代码上存在差别。当然对于外设驱动调试,一般是通过先让bootrom运行到Shell下,而后在Shell下调用驱动的相关函数进行调试,而不是作为内核启动的一部分进行,这样更有效率。

如果成功地完成了网络设备驱动的初始化,且驱动可用,那么下面就要开始下载VxWorks内核了,当然在这之前还需要一系列准备工作,如启动网络设备工作(注意,以上仅仅是初始化,还没有启动进入工作,启动过程由muxEndStart函数完成,其调用驱动中启动函数(如armEndStart)完成中断注册,开启工作使能位等),解析bootline参数,获取主机地址,VxWorks内核映像名,主机服务器用户名和密码等,这些工作都将由bootCmdLoop函数完成。

语句行:

        taskSpawn  ("tBoot",  bootCmdTaskPriority,  bootCmdTaskOptions,  bootCmdTaskStackSize,
    (FUNCPTR) bootCmdLoop,0,0,0,0,0,0,0,0,0,0);

3.bootCmdLoop函数

创建“tBoot”任务,执行bootCmdLoop函数,由其具体完成VxWorks内核映像的下载工作。bootCmdLoop函数较长,我们将不再详细分析该函数的执行过程,读者可以自行对该文件进行分析。注意,bootCmdLoop函数定义在bootConfig.c文件中。

3.2.6 在bootrom中添加用户代码

有时需要在bootrom中加入自己的一些代码,如romInit函数调用的平台初始化代码(如对内存控制的初始化、平台PLL初始化、管脚复用寄存器初始化等),此时需要注意,由于这些代码需要在romInit函数中调用,故不可以是压缩的,因为在romInit执行时,还没有解压缩操作,需要romStart函数执行后才完成解压缩。那么如何做到让我们自定义的代码以非压缩形式进入bootrom映像中呢?用户需要借助BOOT_EXTRA宏,对于需要以非压缩形式加入bootrom映像中的用户代码,用户需要在Makefile中以如下形式操作BOOT_EXTRA宏定义。

        BOOT_EXTRA=myOwnCode.o myOwnAnotherCode.o

如此定义后,myOwnCode.o、myOwnAnotherCode.o中包含的代码将作为非压缩部分进入bootrom中,romStart函数将在第一次复制中将这些文件中的代码和romInit函数、romStart函数一并复制到RAM_LOW_ADRS指定的内存地址处。

如果需要以压缩形式进入bootrom,那么就需要使用到另一个宏MACH_EXTRA,如下所示:

        MACH_EXTRA=myOwnCode.o myOwnAnotherCode.o

如此定义后,myOwnCode.o、myOwnAnotherCode.o中包含的代码将作为压缩部分进入bootrom中,这些代码将在romStart函数的第二次复制中被解压缩到RAM_HIGH_ADRS指定的内存地址处。

注意

● BOOT_EXTRA和MACH_EXTRA宏对于压缩型ROM启动方式VxWorks内核映像同样有效。

● 在BOOT_EXTRA和MACH_EXTRA中可以指定相同的文件,此时该文件中的代码以压缩形式和非压缩形式同时存在于映像中。由于在载入VxWorks内核映像后,RAM_LOW_ADRS区域的bootrom代码将被覆盖,如果在跳转到VxWorks内核映像执行之前,还需要执行位于RAM_LOW_ADRS区域的bootrom代码,那么这些代码就需要以非压缩方式存在,这样RAM_HIGH_ADRS区域的代码就可以以相对方式进行调用,一个典型的文件就是version.o,它同时存在压缩和非压缩部分。虽然其并非以BOOT_EXTRA和MACH_EXTRA宏的方式指定的。

3.2.7 其他注意事项及说明

一些读者可能对同时以压缩和非压缩形式保存两份相同的代码是否会造成地址上的冲突有疑问,实际上,并不会出现读者所担心的问题。因为压缩部分代码只供压缩部分的其他代码调用,而非压缩部分代码一般只供romInit函数调用,且压缩和非压缩代码分别在不同的链接过程中进行包含的,属于完全不同的地址空间(一个位于RAM_LOW_ADRS,一个位于RAM_HIGH_ADRS),故不会造成冲突,因为二者都有代码的不同副本。

常量定义说明如下。

● LOCAL_MEM_LOCAL_ADRS:平台外部存储器(如DDR RAM)基地址。

● LOCAL_MEM_SIZE:平台外部存储器容量。

● RAM_LOW_ADRS:地址常量。指定了bootrom中非压缩代码在RAM中的运行地

址。同时还指定了VxWorks内核映像载入RAM时存放的基地址。

● RAM_HIGH_ADRS:bootrom中压缩代码解压缩基地址以及压缩代码运行起始地址。

● ROM_TEXT_ADRS:bootrom ROM中存储起始地址;romInit函数ROM中运行的地址、复位跳转指令地址。romInit函数开始处必须放置复位向量表。

注意

RAM_HIGH_ADRS与RAM_LOW_ADRS之间的内存容量必须足够大,需要能够放置被下载的VxWorks内核映像,否则将导致bootrom的一部分代码被覆盖,从而产生一些异常问题很难调试。

调试常用命令:nm<arch> -n

        C:\T22\ppc\target\config\wrSbc824x>nmppc -n bootrom
        00100000 T _romInit
        00100000 T _wrs_kernel_text_start
        00100000 T romInit
        00100000 T wrs_kernel_text_start
        00100038 t cold
        00100044 t warm
        00100048 t start
        …
        00103db4 T inflate
        00104190 A _etext
        00104190 D _wrs_kernel_data_start
        00104190 A _wrs_kernel_text_end
        00104190 A etext
        00104190 D runtimeName
        00104190 D wrs_kernel_data_start
        00104190 A wrs_kernel_text_end
        00104194 D runtimeVersion
        00104198 D VxWorksVersion
        0010419c D creationDate
        001041a0 D _binArrayStart
        001041a0 D binArrayStart
        0010c190 T _SDA2_BASE_
        001363d0 D _binArrayEnd
        001363d0 D binArrayEnd
        …
        001502f0 A _end
        001502f0 A _wrs_kernel_bss_end
        001502f0 A end
        001502f0 A wrs_kernel_bss_end

可以通过在终端命令行输入nm命令打印函数的链接地址进行查看,这在某些情况下将非常有利于调试代码,也有助于理解bootrom映像的内部函数组成。

最后需要提醒注意的一点,由于romInit的链接地址是在RAM_LOW_ADRS指定的RAM中,而该函数所有代码的执行在ROM中完成,所以,romInit代码的编写必须自始至终做到PIC(位置无关),不可以进行直接跳转,也不可以直接进行函数调用,读者可以查看romInit的具体实现代码,将看到对于函数调用都是通过相对调用完成的。在进行romInit函数的编写时必须特别注意这一点,否则在romInit函数执行阶段,系统将死机,而这一般是比较低级的错误。