- VxWorks設備驅動開發詳解
- 曹桂平等編著
- 10086字
- 2019-01-09 15:53:26
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函數執行階段,系統將死機,而這一般是比較低級的錯誤。