殷戰寧,劉 琳
(中國電子科技集團公司51所,上海 201802)
對于一般編程人員尤其是Windows等通用平臺的編程人員來說,內存配置基本上只要考慮全局變量、局部變量(堆棧變量)的合理安排,內存管理基本上只是使用 Malloc、Free等編程接口。但對于Vx Works嵌入式操作系統而言,由于內存芯片選型、內存的地址空間分配[1]、中央處理器(CPU)級內存管理硬件初始化、映像文件的地址定位、內存的靜態和動態占用、內存碎片對系統穩定性的影響等諸多內容都由設計人員來規劃和控制,所以涉及內容廣泛,需要有一個較好的通篇認識才能解決好與內存有關的問題。
內存需要相關的內存控制器來操作,以便能保證內存訪問的時序、刷新等操作。內存控制器有些CPU自帶,如 MPC5200帶雙倍速率(DDR)控制器、同步動態隨機存儲(SDRAM)控制器,PPC860通過用戶可編程機制(UPM)來控制隨機存儲器(RAM)、S3c2410帶SDRAM控制器等;有些CPU不帶內存控制器,如X86類型,需要橋片來對內存進行時序控制。
對內存芯片進行選型,首先要弄清楚內存控制器的特性,如支持的芯片類型、配置參數的標度方法等,然后選取類型無誤、數據手冊方便進行參數轉換或計算的內存,合適的選型可以降低參數配置的難度,提高內存的訪問效率。
由于內存參數配置是主板上電后立即就要做的事情,所以,順利與否以及穩定與否,極大地影響隨后的代碼重定位等工作;通過硬件仿真器來調試主板,工作也往往是從配置和測試內存開始的。摘錄MPC5200內存配置寄存器2的部分位定義,如圖1所示。
可以看到該寄存器的設置牽涉到tRCD、tRP、tRFC等內存片的參數。內存片MT48LC32M8A2的數據手冊如表1所示。

表1 MT48LC32M8A2內存數據表

圖1 MPC5200配置寄存器2的部分定義
可以看到CPU寄存器配置值的參數很方便就從內存手冊里查到。如果選型不合適,則需要進行參數的換算,參數的具體關系可以參看專業介紹內存時序的資料。
這一步工作結果將具體反映到板級支持包(BSP)的 RomInit.s文件中,如 MPC5200的 BSP里面,由RomInitSdram函數來實現內存配置,其重要配置就有:


完成配置后,再通過內存控制器的Command寄存器等啟動Refresh,使能操作。
X86的Vx Works由于有基本輸入輸出系統(BIOS)完成了內存的配置,所以在RomInit.s里面看不到類似的過程,其它類型CPU的主板基本上都需要該操作。
內存的地址對不同的CPU可以有不同的分配方法,對于X86體系,內存一般從地址0開始,跳過A、B段(顯示內存),C、D段(卡式設備內存空間),E、F段(BIOS空間)后,繼續從0x100000連續分配。對于CPU命令架構()類CPU,則一般有CSx類寄存器,可以配置與某個CS引腳對應的地址范圍和操作寬度等,一般也配置成從0開始;對于增強的精簡指令集機制(ARM)類CPU,做法會有許多種,需要查詢具體的芯片手冊,如S3c2410,則固定CS6和CS7用作SDRAM的片選,地址范圍固定從0x30000000開始。
此類寄存器的設置也需要在RomInit.s里面設置好。
CPU一般都有存儲器管理單元(MMU)、塊地址翻譯(BAT)等硬件機制來對內存空間分段或分頁管理,對不同的段頁配置不同的虛實地址對應關系、讀寫屬性、Cache屬性、保護屬性等。這部分操作Vx Works開發人員無需直接訪問寄存器,只需填寫用來配置MMU或BAT的結構數組,如MMU配置:


該配置器就描述了一段內存的虛地址、實地址、大小、屬性使能、屬性等;主內存地址空間一般都配置為不做虛實轉換、可寫、可Cache等。該表項會由Usr MmuInit函數里面讀取并配置到MMU,而且也可以通過Sys Mmu Map Add等維護函數做增刪。
BAT表一般用于CPU命令架構(PPC)類寄存器,可以定義代碼段、數據段、段大小、Cache屬性等,可以與MMU組合使用,也可以單獨使用,相關的BSP也提供結構數組進行維護。
MMU和BAT表項一般在BSP的SysLib.c文件里面。
對于X86類CPU,還有全局描述符表(GDT)、中斷描述符表(IDT)等內存描述符需要初始化,內存描述符規定了一段空間的大小和屬性,組成表格由段寄存器來選取,段寄存器對地址選址的計算實際上是通過內存描述符翻譯后進行的。Vx Works的BSP簡化了該操作,幾個段寄存器都使用一個能覆蓋所有地址空間的全功能的GDT進行工作。
對于Cache使能否,一般還會有針對所有的Cache、代碼 Cache、數據 Cache、一級 Cache、二級Cache等不同的硬件開關,可能會在RomInit.s,Sys Alib.s通過匯編來操作,也可以在UsrInit,Usr-Root函數里面通過CacheEnable等函數來操作,需要檢查確認。
一般而言,CPU上電后先是在只讀存儲器(ROM)(或Flash等)內運行,為了提高運行速度,往往需要把代碼搬移到內存去繼續執行,數據相關段(Data、沒有初值的全局變量符號(BSS)段)則需指向內存區域并得到正確的初始化,堆棧段也需要指向內存,段與段之間需要滿足一定的定位關系,比如不互相重疊等。
以最常用的bootrom_uncmp+ Vx Works啟動組合為例:
(1)上電后執行在只讀存儲器(ROM)里面的bootrom_uncmp映像,初始化CPU、內存控制器等,堆棧底設置在宏RAM_HIGH_ADRS決定的位置(棧頂朝地址低端)。
(2)ROM 里 面 的 程 序 會 把 ROM 里 面 的bootrom_uncmp映像拷貝到起始位置是RAM_HIGH_ADRS的內存區域,然后跳轉到內存來繼續執行。此時代碼段在RAM_HIGH_ADRS位置,數據段緊跟在代碼段后面,BSS段緊跟在數據段后面,BSS段后面是中斷使用的堆棧,然后是bootrom_uncmp將要使用的內存池,任務堆棧段會從內存池申請。所以,要想讓bootrom_uncmp正常執行,需要確保RAM_HIGH_ADRS下面有足夠的空間夠ROM做搬移時的堆棧,RAM_HIGH_ADRS上面有足夠空間存放代碼、數據和用來做內存申請。
(3)內存里面bootrom_uncmp的執行會下載Vx Works,把Vx Works映像拷貝到起始位置是RAM_LOW_ADRS的內存區域,然后跳到該內存繼續執行。為了確保bootrom_uncmp拷貝 Vx-Works期間不會發生Vx Works覆蓋bootrom_uncmp的現象,“RAM_LOW_ADRS+Vx Works代碼段大小+Vx Works數據段大小”所占的空間要盡量避免與bootrom_uncmp需要使用的空間重疊。
(4)Vx Works獲得運行經過再次初始化后,代碼段在RAM_LOW_ADRS位置,數據段緊跟在代碼段后面,BSS段緊跟在數據段后面,BSS段后面是中斷使用的堆棧,然后是WDB專用的內存池,然后是Vx Works將要使用的內存池,堆棧會先設置在RAM_LOW_ADRS開始朝下生長以滿足usr Root啟動前的使用,然后會設置到靠近系統內存頂端以滿足usr Root的使用并利于回收到內存池,以后的任務堆棧就是從內存池里面申請。
可以看到RAM_HIGH_ADRS和RAM_LOW_ADRS 2個宏有著重要的定位意義,尤其是RAM_LOW_ADRS很大程度上決定了Vx Works的內存布局,需要斟酌一個好的位置以便既保證正常啟動、又不至于造成內存浪費(太高了會使內存池縮小)。因為既影響代碼,也影響鏈接,所以這兩個宏的修改需要在config.h里面和makefile里面同時修改并確保一致。
其它類型組合,如bootrom、bootrom_res、Vx-Works_rom等,結合編譯鏈接時產生的map文件或符號表文件,同樣可以做出類似的分析。
根據地址定位關系可以大概知道內存池的起始位置,通過map文件或符號表文件來看,一般是BSS段的結束 +ISR_STACK_SIZE + WDB_STACK_SIZE,而內存池的結束位置由Sys Mem-Top函數決定,一般來說是 LOCAL_MEM_LOCAL_ADRS+LOCAL_MEM_SIZE- USER_RESERVED_MEM。
接口函數主要是指創建內存池、動態申請和釋放內存的函數,但需要額外說明的是,當書寫C/C++源碼時,如果定義了一個全局變量(指函數體外的變量)并賦予了初值,則該變量會靜態占用數據段的空間,如果定義了一個全局變量但沒有賦予初值,則該變量會靜態占用BSS段的空間,如果定義了一個函數體內的變量,則該變量會動態占用堆棧空間,這些編程實際上是在隱蔽地申請(或釋放)內存。下面列舉常用的針對內存池的接口函數。
第1類函數是主內存的參數設置和使用:

設置內存池的屬性,包括:
MEM_ALLOC_ERROR_LOG_FLAG
當內存分配出錯則打出log信息:
MEM_ALLOC_ERROR_SUSPEND_FLAG
當任務內存分配出錯,則把任務掛起(除非任務設置了VX_UNBREAKABLE屬性):
MEM_BLOCK_ERROR_LOG_FLAG
當內存釋放出錯則打出log信息:
MEM_BLOCK_ERROR_SUSPEND_FLAG
當任務內存釋放出錯,則把任務掛起(除非任務設置了VX_UNBREAKABLE屬性):

申請大小為nBytes的內存,返回該內存的ptr(其實就是該段內存的起始地址):

釋放已申請的一段內存,傳入該段內存的ptr(起始地址)作為參數:

申請elemSize*elem Num大小的內存,該段內存會被清0:

申請size的內存,內存起始地址滿足alignment的要求。
第2類函數是單獨的內存池的使用,實際上,主內存池也是一個單獨的內存池,其指針是全局變量

創建一個新的內存池,起始地址是pPool,大小是poolSize,該段內存可以是原來系統內存池之外的某一段離散的內存,也可以是從系統內存池里面申請到的一段內存。建立了單獨的內存池,可以在該段內存里面單獨申請和釋放內存,而不影響其它的內存。返回值是內存池指針:

內存池partId的參數配值,含義同 memOptionsSet:

在partId的內存池里面申請nBytes的內存:

釋放已申請的pBlock內存回partId的內存池:

在partId里面申請nBytes的內存,內存起始地址滿足alignment的要求。
第3類函數是內存池的維護函數:增加一段新的內存給主內存池:


增加一段新的內存給內存池partId:

察看主內存池的信息列表:

察看內存池partId的信息列表。
Vx Works申請內存時使用空間首先滿足的算法,找到合適的塊,多出來的部分會單獨形成一個空塊,釋放內存時會進行相鄰空閑內存塊的歸并,卻不會做碎片搬移和整理。因此動態內存雖然使用方便,但大量的小內存操作偶爾再穿插大內存的操作會造成內存池的碎片,最終沒有足夠的內存使用。而且動態申請和釋放會帶來時間上的損失。所以,在應用層,需要考慮靜態和動態的平衡,考慮到動態情況下大量相同內存操作的優化。
頻繁申請和釋放的內存建議改成靜態的方式,以避免時間上的損失,如果不想使用全局數組或結構這樣的方式,也可以使用動態方式申請下來一塊內存,然后進行強制類型轉換。
如果跟任務動態運行有關,可以考慮放在任務的函數體內,成為堆棧變量(任務的堆棧大小在創建任務時確定,如果擔心堆棧緊張,可以考慮對較大的變量只是把指針放在堆棧里面,而指針所指的內存則動態申請),不僅是動態的,而且了實現了任務與任務的隔離。
Vx Works上的網卡驅動就是采用這樣的方式來管理接收和發送緩沖:通過動態方式申請下來一塊內存,建立不同大小的cluster的數組,并為cluster設置管理屬性,然后建立申請和釋放cluster的函數。
動態的大量相同內存操作建議自己建立一套機制來管理,如通過message queue的協助來管理。
Vx Works是一個實時嵌入式操作系統,所以從嵌入式的角度來說,內存配置工作是必不可少的。從實時的角度來說,掌握了通用的內存管理函數后,還需要進一步了解這些函數對實時性和安全性的影響,從而規劃一個比較穩健的內存管理系統。
[1]王金剛,高偉,蘇琪.Vx Work程序員指南[M].北京:清華大學出版社,2003.
[2]周戶平,張楊.Vx Work程序員速查手冊[M].北京:機械工業出版社,2005.