ARM系列之分散加載描述符(scatte)文件的應用
但是對于一些復雜的場(chǎng)景,就需要分散家在描述符文件,比如:
1 . 定位目標外設
使用分散加載,可以將用戶(hù)定義的結構體或代碼定位到指定物理地址上的外設,這種外設可以是定時(shí)器、實(shí)時(shí)時(shí)鐘、靜態(tài)SRAM或者是兩個(gè)處理器間用于數據和指令通信的雙端口存儲器等。在程序中不必直接訪(fǎng)問(wèn)相應外設,只需訪(fǎng)問(wèn)相應的內存變量即可實(shí)現對指定外設的操作,因為相應的內存變量定位在指定的外設上。這樣,對外設的訪(fǎng)問(wèn)看不到相應的指針操作,對結構體成員的訪(fǎng)問(wèn)即可實(shí)現對外設相應存儲單元的訪(fǎng)問(wèn),讓程序員感覺(jué)到仿佛沒(méi)有外設,只有內存。
例如,一個(gè)帶有兩個(gè)32位寄存器的定時(shí)器外設,在系統中的物理地址為Ox04000000,其C語(yǔ)言結構描述如下:
要使用分散加載將上述結構體定位到Ox04000000的物理地址,可以將上述結構體放在一個(gè)文件名為timer_regs.c中,并在分散加載文件中指定即可,如下:
屬性UNINIT是避免在應用程序啟動(dòng)時(shí)對該執行段的ZI數據段初始化為零。
在程序連接后,通過(guò)Image map文件可查看該ZI數據段的存儲器分配情況:
Execution Region TIMER(Base:Ox04000000,Size:0x00000008,Max:0xffffffff,ABSOLUTE,UNINIT)Base Addr Size Type Attr Idx E Section Name 0bi ectOx04000000 0x00000008 Zero RW 32.bss tlmer_regs.o從Image map文件可以看出,該TIMER執行區定位在物理地址0x04000000,即結構體timer_regs定位在Ox04000000,因此,在程序中對結構體的操作即是對定時(shí)器的操作。
2 . 定義超大型結構體數組
分散加載機制在提供將指定代碼和數據定位在指定物理地址的能力的同時(shí),也提供了一種代碼分割機制——可以將指定的零初始化段(ZI段)從可執行代碼中分離出來(lái)。這樣最終生成的燒入ROM或Flash中的鏡像文件就不包括那部分分割了的零初始化段,即使該零初始化段再大,也不影響最終生成的鏡像文件的大小。但不采用分散加載機制,零初始化段在編譯連接后是直接生成到鏡像文件中的。它的大小直接影響最終要燒寫(xiě)的文件的大小,且零初始化段的大小還取決于內存的大小,它不能大到超過(guò)內存的大??;而采用分散加載機制,可以將某個(gè)零初始化段定位到非內存地址的一個(gè)存儲器外設上,如NVRAM(非易失性隨機存儲器)。
筆者曾在一個(gè)實(shí)際工程中采用這種分散加載機制,將一個(gè)2MB的結構體數組定位到外部NVRAM中,用于記錄設備在工作過(guò)程中采集到的數據;而在本系統中,ARM處理器的內存只有256 KB,Flash存儲器也只有2 MB。如果不采用分散加載,程序根本無(wú)法運行,也不能燒寫(xiě)到Flash中。
采用分散加載,把對復雜外設的訪(fǎng)問(wèn)變成對結構體數組的訪(fǎng)問(wèn),使程序代碼精簡(jiǎn)易懂。對程序員來(lái)說(shuō),對結構體數組的操作還是和內存變量的操作一樣的。
3. 某些特殊應用,需要將代碼段或者數據段的部分放置在指定位置,方便更新,或者其他加密等原因。
4. 某些特殊應用,需要固定函數地址的時(shí)候,可以將那個(gè)函數放于固定的區域,而不管其他程序有誤變化。
5. 對于程序中的一些配置型的變量,可能需要集中放置在一個(gè)區域,方便下次直接更新那塊存儲空間。
-------------------o-----------------------------------------------
下面著(zhù)重描述分散描述符的寫(xiě)法,分散描述文件的類(lèi)型為 .scf 。寫(xiě)完后,可以通過(guò)ADS進(jìn)行加載。
分散裝載(Scatlerloading)
在實(shí)際的嵌入式系統中,ADS提供的缺省存儲器映射是不能滿(mǎn)足要求的。用戶(hù)的目標硬件通常有多個(gè)存儲器設備位于不同的位置,并且這些存儲器設備在程序裝載和運行時(shí)可能還有不同的配置。
Scattertoading可以通過(guò)一個(gè)文本文件來(lái)指定一段代碼或數據在加載和運行時(shí)在存儲器中的不同位置。這個(gè)文本文件scatterfile在命令行中由-scatter開(kāi)關(guān)指定,例如:
armlink_scatterscat.scffilel.ofile2.0
在scatterfile中可以為每一個(gè)代碼或數據區在裝載和執行時(shí)指定不同的存儲區域地址,Scatlertoading的存儲區塊可以分成二種類(lèi)型:
裝載區:當系統啟動(dòng)或加載時(shí)應用程序的存放區。
執行區:系統啟動(dòng)后,應用程序進(jìn)行執行和數據訪(fǎng)問(wèn)的存儲器區域,系統在實(shí)時(shí)運行時(shí)可以有一個(gè)或多個(gè)執行塊。
映像中所有的代碼和數據都有一個(gè)裝載地址和運行地址(二者可能相同也可能不同,視具體情況而定)。在系統啟動(dòng)時(shí),C函數庫中的__main初始化代碼會(huì )執行必要的復制及清零操作,使應用程序的相應代碼和數據段從裝載狀態(tài)轉入執行狀態(tài)。
1.scatter文件語(yǔ)法
scatter文件是一個(gè)簡(jiǎn)單的文本文件,包含一些簡(jiǎn)單的語(yǔ)法。
My_Region0x00000x1000
{
thecontextofregion
}
每個(gè)塊由一個(gè)頭標題開(kāi)始定義,頭中至少包含塊的名字和起始地址,另外還有最大長(cháng)度和其他一些屬性選項。塊定義的內容包括在緊接的一對花括號內,依賴(lài)于具體的系統情況。
一個(gè)加載塊必須至少含有一個(gè)執行塊;實(shí)踐中通常有多個(gè)執行塊。
一個(gè)執行塊必須至少含有一個(gè)代碼或數據段;這些通常來(lái)自源文件或庫函數等的目標文件;通配符號*可以匹配指定屬性項中所有沒(méi)有在文件中定義的余下部分。
2.簡(jiǎn)單分散加載樣例
圖8所示樣例中,只有一個(gè)加載塊,包含了所有的代碼和數據,起始地址為0。這個(gè)加載塊一共對應兩個(gè)執行塊。一個(gè)包含所有的RO代碼和數據,執行地址與裝載地址相同;同時(shí)另一個(gè)起始地址為0x10000的執行塊,包含所有的RW和ZI數據。這樣當系統開(kāi)始啟動(dòng)時(shí),從第一個(gè)執行塊開(kāi)始運行(執行地址等于裝載地址),在執行過(guò)程中,有一段初始化代碼會(huì )把裝載塊中的一部分代碼轉移到另外的執行塊中。
下面是這個(gè)scatter描述文件,該文件描述了上述存儲器映射方式。
LOAD_ROM0x4000
{
EXE_ROM0x00000x4000;Rootregion
{
*〈+RO〉;Allcodeandconstantdata
}
RAM0x100000x8000
{
*〈+RW,+ZI〉;Allnon-constantdata
}
}
3.在分散文件中放置對象
在大多數應用中,并不是像前例那樣,簡(jiǎn)單地把所有屬性都放在一起,用戶(hù)需要控制特定代碼和數據段的放置位置。這可以通過(guò)在scatter文件中對單個(gè)目標文件進(jìn)行定義實(shí)現,而不是只簡(jiǎn)單地依靠通配符。
為了覆蓋標準的連接器布局規則,我們可以使用+FIRST和+LAST分散加載指令。典型的例子是在執行塊的開(kāi)始處放置中斷向量表格:
LOAD_ROM0x00000x4000
{
EXEC_ROM0x00000x4000
{
vectors.o〈Vect,+FIRST〉
*〈+RO〉
}
;moreexecregions...
}
在這個(gè)scatter文件中,保證了vextors.o中的Vect域被放置于地址0x0000。
4.RootRegion(根區)
根區是一個(gè)執行塊,它的加載地址與執行地址是一致的。每個(gè)scatter文件至少有一個(gè)根區。分散加載有一個(gè)限制:創(chuàng )建執行塊的代碼和數據(即完成復制和清零的代碼和數據)無(wú)法自行復制到另一個(gè)位置。因此,在根區中必須含有下面的部分:
_main.o,包含復制代碼/數據的代碼;
連接器輸出變量$$Table和ZISection$$Table,包含被復制代碼/數據的地址。
由于上面兩個(gè)部分的屬性是只讀的,因此他們被*〈+RO〉通配符語(yǔ)法匹配。如果*〈+RO〉被用在了非根區中,則在根區中必須顯式地指明另一個(gè)RO區域。
下面是一個(gè)例子:
LOAD_ROM0x00000x4000
{
EXE_ROM0x00000x4000;rootregion
{
_main.o〈+RO〉;copyingcode
*〈Region$$Tabl0e〉;RO/RWaddressestocopy
*〈ZISection$$Table〉;ZIaddressestozero
}
RAM0x100000x8000
{
*〈+RO〉;allotherROsections
*〈+RW,+ZI〉;allRWandZIsections
}
}
-------------------------------------------
放置堆棧和heap
Scatterloading機制提供了一種指定代碼和靜態(tài)數據布局的方法。下面介紹如何放置應用程序的堆棧和heap。
*_user_initial_stackheap重定向
應用程序的堆棧和heap是在C庫函數初始化過(guò)程中建立起來(lái)的??梢酝ㄟ^(guò)重定向對應的子程序來(lái)改變堆棧和heap的位置,在A(yíng)DS的庫函數中,即_user_initial_stackheap()函數。
_user_initial_stackheap()可以用C或匯編來(lái)實(shí)現,它必須返回如下參數:
r0:heap基地址;
r1:堆?;刂?;
r2:heap長(cháng)度限制值(需要的話(huà));
r3:堆棧長(cháng)度限制值。
當用戶(hù)使用分散裝載功能的時(shí)候,必須重調用_user_initial_stackheap(),否則連接器會(huì )報錯:
Error: L6218E: Undefined symbol Image$$ZI$$Limit (referred from sys_stackheap.o)
*存儲器模型
ADS提供了兩種實(shí)時(shí)存儲器模型。缺省時(shí)為one-region,應用程序的堆棧和heap位于同一個(gè)存儲器區塊,使用的時(shí)候相向生長(cháng),當在heap區分配一塊存儲器空間時(shí)需要檢查堆棧指針。另一種情況是堆棧和heap使用兩塊獨立的存儲器區域。對于速度特別快的RAM,可選擇只用來(lái)作堆棧使用。為了使用這種two-region模型,用戶(hù)需要導入符號use_two_region_memory,heap使用需要檢查heap的長(cháng)度限制值。
對這兩種模型來(lái)說(shuō),缺省情況下對堆棧的生長(cháng)都不進(jìn)行檢查。用戶(hù)可以在程序編譯時(shí)使用 -apcs/swst 編譯器選項來(lái)進(jìn)行軟件堆棧檢查。如果使用two-region模型,必須得在執行_user_initial_stackheap時(shí)指定一個(gè)堆棧限制值。
圖9 重定向_user_initial_stackheap()
圖10 基本初始化過(guò)程
圖11 ROM/RAM重定向和映射
表1
系統復位和初始化
目前情況,一般假設程序從C庫函數的初始化入口_main開(kāi)始執行。實(shí)際上,所有的嵌入式程序在啟動(dòng)時(shí)都要執行一些系統級的初始化操作。在此討論這方面的內容。
初始化過(guò)程
圖10中顯示了一個(gè)基于A(yíng)RM的嵌入式系統的基本初始化過(guò)程??梢钥吹?,在_main之前加入了一個(gè)復位處理模塊reset handler,它在系統上電復位時(shí)立即啟動(dòng)。標識為$sub$$main的新代碼塊在進(jìn)入主程序之前執行。
復位處理模塊reset handler通常是一小段匯編代碼,在系統復位時(shí)執行。它至少完成應用程序中使用到的所有處理器模式的堆棧初始化工作。對于含有本地存儲器系統的內核(比如含cache的ARM內核),配置工作也必須在這一段初始化過(guò)程中完成。當完成系統初始化之后,通常程序會(huì )跳向_main,開(kāi)始C庫函數的初始化過(guò)程。
系統初始化過(guò)程一般還包括另外一些內容,中斷使能等,這些大多安排在C庫函數的初始化完成之后執行。$sub$$main()完成這部分功能。
向量表(vector table)
所有的ARM系統都有一張中斷向量表當出現異常需要處理時(shí),必須調用向量表。向量表一般要位于0地址處。
表2
表3
表4
表5
表6
表7
表8
表9
表10
存儲器配置
*ROM/RAM重定向
當系統啟動(dòng)的時(shí)候,為了保證0地址處有正確的啟動(dòng)代碼存在,需要非易失性的存儲器。
一種簡(jiǎn)單的方法,就是把系統0x0000開(kāi)始的一塊地址分配給ROM。其缺點(diǎn)是,由于ROM的訪(fǎng)問(wèn)速度比RAM慢很多,當執行中斷響應需要從中斷向量表跳轉時(shí),會(huì )給系統性能帶來(lái)?yè)p失;同時(shí),在ROM中的向量表內容也不能被用戶(hù)程序動(dòng)態(tài)修改。
另外一種可行的方案如圖11所示。ROM位于地址0x1000開(kāi)始的地方,但是在系統復位時(shí)又被存儲器控制器映射到0x0000地址處。這樣當系統啟動(dòng)之后,在地址0x0000看到的是ROM,系統執行這塊ROM中的啟動(dòng)代碼,啟動(dòng)代碼跳轉到真正的ROM的地址,并讓存儲器控制器移除對ROM的地址映射。這時(shí)0x0000地址處的存儲器又恢復回了RAM。__main中的代碼把向量表copy到0x0000處的RAM中去,使得異常時(shí)能被正確響應。
表1為ARM匯編中執行ROM/RAM重定向和映射的一個(gè)例子。它以ARM公司的Integrator平臺為基礎的,該方法適用于類(lèi)似ROM/RAM重定向方法的所有平臺。第一條指令完成從ROM的映射地址(0x00000)到真實(shí)地址的跳轉。地址標號instruct_2是ROM的真實(shí)地址(0x180004)。然后通過(guò)設置Integrator平臺上的相應控制寄存器,移除ROM的地址映射。代碼在系統一啟動(dòng)就被執行。所有關(guān)于地址重定向/映射的操作必須在C庫函數初始化之前完成。
*本地存儲器配置
許多ARM處理器都有片上存儲器系統,如cache和緊密耦合存儲器(TCM)、存儲器管理單元(MMU)或存儲器保護單元(MPU)。這些設備都要在系統初始化過(guò)程中正確配置,并且有一些特殊的要求需要考慮。
由前文可知,_main中的C庫函數初始化代碼負責程序運行時(shí)的存儲器系統設置。因此,整個(gè)存儲器系統本身必須得在__main之前完成初始化工作,如MMU或MPU必須在reset handler里面完成配置。
緊密耦合存儲器(TCM)的初始化同樣須在_main之前完成(通常在MMU/MPU之前),因為一般程序都需要把代碼和數據分散裝入TCM。需要注意的是當TCM被使能后,不再訪(fǎng)問(wèn)被TCM屏蔽的存儲器。
關(guān)于cache的一致性問(wèn)題,如果cache在_main之前使能的話(huà),那么當_main里面進(jìn)行從裝載區到執行區的代碼和數據拷貝時(shí)(因為在拷貝過(guò)程中指令和數據在本質(zhì)上都是被當作數據處理),指令會(huì )出現在數據緩沖區。避免此問(wèn)題的方法是在C庫函數初始化完成后再使能cache。
*Scatter loading與存儲器配置
無(wú)論是通過(guò)ROM/RAM重定向還是MMU配置的方法,如果系統在啟動(dòng)和運行時(shí)存儲器分布不一致,scatterloading文件中的定義就要按照系統重定向后的存儲器分布情況進(jìn)行。
以上文ROM/RAM重定向為例:
LOAD_ROM 0x10000 0x8000
{
EXE_ROM 0x10000 0x8000
{
reset_handler.o (+RO, +FIRST)
...
}
RAM 0x0000 0x4000
{
vectors.o (+RO, +FIRST)
...
}
}
裝載區LOAD_ROM被放置在0x10000處,代表了重定向之后代碼和數據的裝載地址。
堆棧的初始化
程序中可能用到的處理器模式,都需要定義一個(gè)堆棧指針。
在表2中,堆棧位于stack_base標識的地址中。這個(gè)符號可以是存儲器系統中的一個(gè)直接地址,也可以在另外的匯編文件中定義,由scatter文件來(lái)定義分配地址。表2代碼為FIQ和IRQ模式各分配了一個(gè)256字節的堆棧,用戶(hù)可以用同樣的方法為其他模式也分配堆棧。最簡(jiǎn)單的方法就是進(jìn)入相應的模式,然后為SP寄存器指定相應的值。如果想使用軟件堆棧檢查,還必須指定一個(gè)堆棧長(cháng)度限制值。
堆棧指針和堆棧限制的數值會(huì )作為參數自動(dòng)傳遞到C庫函數的初始化代碼__user_initial_stackheap中,在__user_initial_stackheap中不應該修改這些值。
硬件初始化 $sub$$main()
一般來(lái)說(shuō),應該把所有的系統初始化代碼與主應用程序分離開(kāi)來(lái),但是有幾個(gè)例外,比如cache和中斷的使能,需要在C庫函數初始化之后執行。
表3代碼顯示了如何使用 $sub和 $supper 。連接器把呼叫main()的函數替換成呼叫$sub$$main(),完成cache和中斷的使能,并最終跳向main()。
執行模式考慮
為主應用程序選擇一個(gè)處理器執行模式非常重要,這取決于系統的初始化代碼。
許多在啟動(dòng)過(guò)程中使用到的功能,如MMU/MPU的配置、中斷的使能等,只能在特權級模式下進(jìn)行。如果需要在特權極模式下運行自己的應用程序,只要在退出初始化過(guò)程之前改變到相應的模式就行了,沒(méi)有其他任何問(wèn)題。
如果使用user模式,必須保證所有只能在特權模式下執行的功能完成之后,才能進(jìn)入user模式。因為system模式和user模式使用相同的寄存器組,reset handler應該從system模式退出,_user_initial_stackheap在system模式下完成應用程序堆棧的初始化。這樣在處理器進(jìn)入user模式后,所有的堆??臻g都已經(jīng)被正確設置好了。
對存儲器布局的進(jìn)一步考慮
在scatter文件中分配硬件地址
雖然可以在一個(gè)scatter文件中描述代碼和數據的分散布局,但是目標硬件中的外設寄存器,堆棧和heap配置仍然直接采用硬件地址在程序源代碼中進(jìn)行設置。如果把所有存儲器地址相關(guān)的信息都在scatter文件中進(jìn)行定義,避免在源文件中引用絕對硬件地址,對程序的工程化管理是有大好處的。
*在scatter文件中定義目標外設地址
通常外設寄存器的地址在程序文件或頭文件中定義,也可以聲明一個(gè)結構類(lèi)型指向外設寄存器,結構的地址定位在scatter文件中完成。
舉例來(lái)說(shuō),目標定時(shí)器上有2個(gè)32位的寄存器,可以用表4來(lái)映射這些寄存器。為了把結構放置在指定的存儲器地址上面,創(chuàng )建一個(gè)新的執行區(見(jiàn)表5)。scatter文件便把timer_regs結構定位在了地址0x40000000。
注意,在啟動(dòng)過(guò)程當中這些寄存器的內容不需要清零,改變寄存器的內容可能影響系統狀態(tài)。在執行區上加UNINIT屬性可以防止ZI數據在初始化過(guò)程中被清零。
在scatter文件中分配堆棧和heap
在許多情況下,用scatter文件來(lái)定義堆棧和heap的地址會(huì )帶來(lái)一些好處,主要有:所有的存儲器分配信息集中在一個(gè)文件里;改變堆棧和heap的地址只要重新連接就行了,不需要重新編譯。
*顯式地放置符號
在A(yíng)DS1.2環(huán)境下,這是最簡(jiǎn)單的方法。在前文中引用過(guò)2個(gè)符號stack_base和heap_base,這2個(gè)符號在匯編模塊中創(chuàng )建,在scatter文件中各自的執行區里定位(見(jiàn)表6)。
表7文件中,heap基地址定位在0x20000上,堆?;刂肺挥?x40000?,F在heap和堆棧的位置就可以非常方便地進(jìn)行編輯了。
*使用連接器產(chǎn)生的符號
這種方法需要在目標文件中指定好heap和堆棧的長(cháng)度。這在一定程度上減弱了本節開(kāi)頭描述的兩個(gè)優(yōu)點(diǎn)。
首先在匯編源程序中定義heap和堆棧的長(cháng)度。關(guān)鍵詞SPACE用來(lái)保留一塊存儲器空間,NOINT則可以阻止清零操作(見(jiàn)表8)。注意在這里的源文件中并不需要地址標號。
然后這些部分就可以在scatter文件中對應的執行區里定位了(見(jiàn)表9)。連接器產(chǎn)生的符號指向每一個(gè)執行區的基地址和長(cháng)度限制,這些符號可以被_user_initial_stackheap調用的重定向代碼使用。在代碼中使用DCD來(lái)給這些值定義更有意義的名字,可以增強代碼的可讀性(見(jiàn)表10)。
文件把heap基地址定位在0x15000,堆棧地址定位在0x4000。Heap和堆棧的位置可以通過(guò)編輯對應執行區的地址方便地改變。
------------------------o---------------------------------------
樣例如下所示:
分散加載描述文件供ARM-ADS鏈接器使用,用來(lái)決定各個(gè)代碼段和數據段的存儲位置,下面為一個(gè)添加注釋后的.scf文件例子:
;YL-LPC2294片內FLASH分散加載文件
;Internal Flash 256kBytes, Address range:0x00000000~0x0003ffff
;Internal SRAM 16KBytes, Address range:0x40000000~0x40003fff
;External Flash 2MBytes,SST39VF1601,Address range:0x80000000~0x401fffff
;External SRAM 512KBytes,IS61LV25616,Address range:0x81000000~0x81080000
ROM_LOAD 0x0;ROM_LOAD:> ;0x0:Start address for ROM_LOAD region.
{
ROM_EXEC 0x00000000;ROM_EXEC:> ;0x00000000:Start address for the execture region.
{
Startup.o (vectors, +First)
* (+RO) ;Place all code and RO data into this exec region,
;and make sure the "vectors" section from "Startup.o"
;be placed first.
}
IRAM 0x40000000 ;The second execute region;start address is 0x40000000.
{
Startup.o (+RW,+ZI) ;Place all RW and ZI data from Startup.o here.
}
ERAM 0x81068000 ;The third execute region;Start address:0x81068000.
{
* (+RW,+ZI) ;All reset RW/ZI data to be placed here.
}
HEAP +0 UNINIT ;The fourth execute region;Start address:Follow the
;end of ERAM region.
{
heap.o (+ZI) ;All ZI data from heap.o to be placed here.
}
STACKS 0x40004000 UNINIT ;The fifth execute region.
{
stack.o (+ZI) ;All ZI data from stack.o to be placed here.
}
}
一般一個(gè)簡(jiǎn)單的分散加載描述文件包含三部分:Loader region、Execute region、Input section。各部分的格式及定義細節參見(jiàn)文件:ADS_LinkerGuide.pdf
評論