一、文檔背景
掌握分散加載,如圖1所示,可以使我們方便地指定程序代碼和變量的存儲位置,達到優(yōu)化性能和降低成本的優(yōu)勢,比如我們想把時間關鍵代碼存放在ITCM RAM里面運行,而占用空間超大又不需要快速運行的代碼,我們可以放到QSPI Flash里面,因為QSPI Flash的容量一般都比較大,又或者,將內部Flash和外部的QSPI Flash混合使用。所以,通過學習分散加載,我們可以很方便地實現(xiàn)這些功能,獲取到更好的性能和成本優(yōu)勢。

圖1
二、分散加載
1、基礎知識
(1)、其實在我們建立工程的時候,肯定也是用過分散加載這個功能的,為什么這么說呢,我們打開option選項里的target,如圖2所示。圖2里對ROM配置還有RAM配置部分的修改,其實本質上就是對分散加載文件的修改。

圖2
(2)、分散加載文件在option選項里的Linker選項卡中可以找到,具體操作如圖3左側所示,點擊下方的Edit,即可在keil中打開如圖3右邊所示的分散加載文件。也可以看到,文件中的配置,和圖2中target內設置的地址、大小也是一 一對應的,其中每一行的代碼含義我們稍后也會一 一進行講解。然后,如圖4所示,改動一下target中的ROM分配,對應的,圖4右邊的分散加載文件就多出了對應的地址和大小。

圖3

圖4
(3)、如果用戶想自行修改分散加載文件,則需要在option選項里的Linker選項卡中,去掉Use Memory Layout from Target Dialog的勾選,去掉之后,就可以任意指定一個分散加載文件了,但是要注意,指定文件以后,我們Target選項卡中的設置就不再起作用了,具體操作如圖5所示。

圖5
2、代碼解讀

圖6
(4)、接下來,對圖6 中分散加載文件的代碼進行逐行解讀:
第5行:LR_IROM1 0x08000000 0x00200000 { ; load region size_region
LR是加載域(load region)的縮寫,加載域是指編譯時程序的實際存儲位置。LR_IROM1是加載域的名稱,開發(fā)者可以自定義。0x08000000 是加載域的起始地址,對應 Flash 的基地址(通常用于存儲代碼和常量數(shù)據(jù))。0x00200000 是加載域的大?。? MB)。
第6行: ER_IROM1 0x08000000 0x00200000 { ; load address = execution address
ER是執(zhí)行域(execution region)的縮寫,加載地址(Load Address)和執(zhí)行地址(Execution Address)一致,表明該段數(shù)據(jù)無需從 Flash 移動到 RAM 才能運行。0x08000000 是執(zhí)行域的起始地址,與 LR_IROM1 的地址相同,表示代碼和常量數(shù)據(jù)在 Flash 中加載后直接在該地址執(zhí)行。0x00200000 是執(zhí)行域的大?。? MB)。
第7行: *.o (RESET, +First)
*是通配符,*.o 表示所有的目標文件(.o),這行的意思就是說,把所有以.o結尾的目標文件的代碼,都放置在這里,但是,有一個條件,就是要把標識為RESET段的代碼放置在最前面,放置在最前面的操作,是由+First參數(shù)控制的。
這里其實要引申一個知識點,否則基本看不明白這里的操作含義,因為STM32的運行方式是這樣的:CPU 要從地址 0x0000 0000 獲取棧頂值,然后從始于 0x0000 0004 的自舉存儲器開始執(zhí)行代碼。前面已經(jīng)把0x800 0000的FLASH地址配置為啟動的存儲器,因此就會從這里偏移位置0取得棧頂值,從偏移位置4取得第一行執(zhí)行代碼。根據(jù)這個要求,那么每個項目工程的啟動文件,必須是這樣設置才可以運行。如下圖7所示:(DCD 表示分配 1 個 4 字節(jié)的空間)

圖7
前面說到CPU會從偏移位置0處來加載棧頂值,可以從這里的圖6看到,__initial_sp變量被放在第一個位置,意味著這個變量的值會放在這個區(qū)域的第一個位置,這樣就確保了棧頂值是保存在這段代碼布局的首位置了。但是編譯器怎么知道這段代碼一定會放在存儲器的首位置,而不是放置在存儲器的后面位置呢?
如果沒有的特別的聲明,編譯器是可能把它放置在任何位置上的,并不是首位置。關鍵之處,就是sct文件中的*.o (RESET, +First)這一行代碼,這里明確地說明,要把RESET的代碼放在首位置,就是偏移0的位置。
然后我們回頭再看圖6,就會發(fā)現(xiàn),第55行早就已經(jīng)說明,這一段代碼區(qū)域被命名為RESET了,那么,當編譯器看到這個名稱時,就會把這段代碼放置在存儲器的首位置,這樣CPU就會從這里獲得棧頂值,就可以開始加載代碼進行運行了,從偏移4的位置,也就是Reset_Handler的值,把它放到CPU的執(zhí)行指令寄存器,就進入代碼運行了。
第8行: *(InRoot$$Sections)
某些 Arm C 和 C++ 庫部分必須放置在根區(qū)域中,例如 _main.o、_scatter*.o、_ dc*.o 和 *Region$$Table。此列表可能會因版本而異。鏈接器可以使用 InRoot$$Sections 自動放置所有這些段,以防未來發(fā)生變化。如圖8所示:

圖8
第9行: .ANY (+RO)
把只讀數(shù)據(jù)(包括代碼、常量等)放入執(zhí)行區(qū)域中。(其實就是自動匹配未分配的只讀段)
第10行: .ANY (+XO)
把僅執(zhí)行數(shù)據(jù)(Executable Only)放入執(zhí)行區(qū)域中。(其實就是自動匹配未分配的僅執(zhí)行段)
第12行: RW_IRAM1 0x20000000 0x00050000 { ;
RW_IRAM1是讀寫區(qū)域的名稱。0x20000000 是 RAM 的基地址,0x00050000 是讀寫區(qū)域的大?。?20 KB)。這個區(qū)域,用于存放全局變量、堆棧等運行時需要讀寫的數(shù)據(jù)。
第13行: .ANY (+RW +ZI)
把所有需要讀寫的、已初始化全局變量和所有的零初始化變量放在 RW_IRAM1區(qū)域。(其實就是自動匹配未分配的讀寫和零初始化段)
(5)、然后.ANY和星號(*)在使用方面,其實是有一個比較關鍵的區(qū)別的,就是如果指定的區(qū)域滿了,* 不會自動向下分配,而是報錯,.ANY就可以連續(xù)向下分配,不報錯。具體示例如下圖9,我們在sct文件中劃分兩個RAM區(qū)域,第一個RAM區(qū)域只分配128KB,并且使用星號來進行匹配。

圖9
然后再在程序中,定義一個128KB的數(shù)組,如圖10所示,數(shù)組后面的__attribute__((used))其實是一種:函數(shù)或變量屬性修飾符,用于確保被標記的函數(shù)或變量在程序中不會被優(yōu)化器錯誤地移除。

圖10
然后,我們觀察一下是否可以分配成功,很明顯,如圖11所示,程序報錯了。
報錯信息表示,我們的RW_IRAM1被限制為只有131072bytes,也就是128KB,但是程序實際使用了168848bytes,也就是164KB。顯而易見,星號(*)無法自動向下分配。

圖11
同樣的,我們更改星號(*)為.ANY后,同樣的代碼,就可以完成數(shù)據(jù)的向下自動分配,而不報錯,如圖12所示:

圖12
3、SCT的應用
(6)、應用①:
在STM32里面,經(jīng)常會有很多RAM空間,比如AXI SRAM 、SARM 、SDRAM,這三種RAM的訪問速度都不太一樣,三者的速度排序通常是:AXI SRAM > SRAM > SDRAM。根據(jù)他們的特性,工程師往往會有不同的需求,比如在AXI SRAM中存放一些需要高速執(zhí)行的代碼和數(shù)據(jù);在SRAM中存放一些代碼、堆?;蝾l繁訪問的數(shù)據(jù);在SDRAM中適合存放一些大容量數(shù)據(jù),但不適合時間敏感的實時應用,比如一些字庫數(shù)據(jù)等。
所以以上這些需求,我們完全可以通過更改分散加載文件,來實現(xiàn)數(shù)據(jù)存儲的指定。如圖13所實現(xiàn)的樣例,其實就是通過更改圖14中的分散加載文件,來實現(xiàn)圖13中內存的劃分的。
注意,圖13的代碼中,__attribute__((section (".section_name"))),是用來指定變量或函數(shù)的存儲位置的,告訴編譯器,需要將特定的代碼或數(shù)據(jù)放置到用戶定義的內存區(qū)域(Section)中。然后SDRAM段那里多用了一個zeroinit參數(shù),且對應的在sct文件中,SDRAM段也有一個UNINIT,這里的作用是,在不掉電復位的場景下,用UNINIT保住sdram段里復位之前的數(shù)據(jù),復位后,需要被初始化為0的數(shù)據(jù),就用zeroinit初始化為0,實現(xiàn)對 SDRAM 數(shù)據(jù)的精確控制,這是需要注意的一點。
然后需要注意的是,我這里自定義的區(qū)域,都是用的星號(*)作為前綴,而不是.ANY,如圖14所示。這個意思其實也很明顯,就是想讓用戶對指定的這些內存,擁有絕對的把控,指定哪些變量在哪,就必須在哪里,不可以自動往下分配,防止把變量存儲位置給弄混了,不便于我們后續(xù)給不同的代碼分配不同的訪問速度。

圖13

圖14
最后我們點擊編譯一下工程,然后打開map文件,如圖15可以看到,map文件中列出了對應的 .RAM_D1、.RAM_D2、.RAM_D3 和 .RAM_SDRAM這些段的定義及內存基地址。如圖16可以看到,map文件中展示了變量(如 AXISRAMBuf、D2SRAMBuf、D3SRAMBuf 和 SDRAMSRAMBuf 等)及其所在的內存地址、類型、大小以及對應的段(如 .RAM_D1、.RAM_D2、.RAM_D3 和 .RAM_SDRAM)。在map文件中,這些內存分配結果,都是齊全且顯而易見的。

圖15

圖16
(7)、應用②:可以把一些時間關鍵代碼放置在ITCM RAM里,這個ITCM RAM可以被認為成一個更適合指令的運行RAM空間,速度更快一些,有這么一個優(yōu)勢。
在如下圖17中,可以看到手冊中規(guī)定了,ITCM的位置以及大小,所以我們根據(jù)手冊上的數(shù)據(jù),在keil的Option選項中,把ITCM RAM的空間給定義到里面。(注意,此應用示例,勾選使用了Option選項中l(wèi)inker里的Use Memory Layout from Target Dialog,所以target選項里的RAM定義才會生效。)

圖17
如果想要指定時間關鍵代碼,將其放在ITCM RAM中,可以不需要像之前那樣,在sct文件里手動添加導入,可以如圖18所示,右鍵工程內部的文件夾,直接對該文件夾下的全部代碼進行內存劃分。比如直接單獨把APP文件夾下的代碼數(shù)據(jù)全部劃分到IRAM2[0x0-0xFFFF]中。同理,也可以把BSP文件夾下的代碼數(shù)據(jù)和SEGGER/HardFault文件夾下的代碼數(shù)據(jù)都規(guī)劃到這里,以提高程序的運行速度,但是要注意的是,這里的ITCM RAM只有64KB,別放超了。

圖18
然后我們可以打開對應的sct文件,看一下keil是否已經(jīng)幫我們把特定的數(shù)據(jù)放到了ITCM RAM里,如圖19所示,可以看到,keil確實幫我們把特定的目標文件的數(shù)據(jù),給放入到了ITCM RAM中。

圖19
那么接下來,我們再進一步確認一下,當程序運行到特定文件時,他的運行地址是否會切換到ITCM RAM的地址范圍呢,如圖20所示,當我們從main.c運行到MainRAM.c中時,運行地址確實成功切換了。所以,我們想讓時間關鍵代碼運行在更快的RAM中的目標, 也確實實現(xiàn)了。

圖20
且如圖21所示,在map文件中,IRAM2執(zhí)行域確實也變成了0x0000 0000開始的地址。

圖21
(8)、應用③:我們可以利用內部Flash和外部QSPI Flash,做出一個混合運行的程序。這個應用的優(yōu)勢在于,比如我們的數(shù)據(jù)量很大,有一些字庫、圖庫之類的,全部存在CPU內部那點Flash上絕對是不現(xiàn)實的,肯定是要放到外部的,比如QSPI Flash中?;蛘哌€有一些對執(zhí)行速度要求不高的代碼,也都可以全部放置到QSPI Flash里。因為目前128MB的QSPI Flash還是很常見的,所以對于大多數(shù)程序來說,在大小方面已經(jīng)夠了。
然后這個示例,我們也不直接手動更改sct文件了,因為要演示放入到QSPI Flash中的文件會非常多,右鍵工程內部的文件夾,直接對該文件夾下的全部代碼進行劃分即可。
首先,我們要把外部Flash定義在keil的target中,這個定義可以參考ST手冊中對應內核的內存映射表,確定一下外部存儲器應該被定義的地址范圍,比如如下圖22所示,可以看到QSPI Flash的范圍應該是[0x9000 0000-0x9FFF FFFF]。然后由于我們的QSPI Flash只有32MB,所以這里只填入0x9000 0000和0x2000000即可。

圖22
然后我們通過右鍵工程文件夾,來為文件進行批量的劃分,把一些對執(zhí)行速度要求不高的代碼,或者字庫數(shù)據(jù),全部扔到我們的外部存儲器即可,如圖23所示操作即可。

圖23
接下來還是檢查一下,打開對應的sct文件、map文件,看一下keil是否已經(jīng)幫我們把特定的數(shù)據(jù)放到了QSPI Flash里。如圖24所示,沒有問題,都已經(jīng)被自動規(guī)劃好了。

圖24
然后我們可以調試一下,看一下效果,這里需要注意,在調試前,需要在Flash Download里,選擇上對應的燒錄算法,如圖25所示,內部Flash和外部QSPI Flash的兩個算法都要加載到軟件里來。

圖25
進入到調試后可以看到,程序現(xiàn)象也是符合的,如圖26,至此,我們就完成了一個內部Flash和外部QSPI Flash混合運行的程序。

圖26
三、討論分析
問①:將時間關鍵代碼放置在ITCM RAM里的優(yōu)勢是什么?
答①:ITCM RAM為指令緩存內存,通常具有極低的訪問延遲和較高的帶寬,適合存放時間關鍵代碼,如實時操作系統(tǒng)的調度、ISR(中斷服務程序)等。
四、結論
在嵌入式開發(fā)中,如何合理利用不同類型的內存存儲代碼是優(yōu)化性能和降低成本的關鍵。通過合理配置sct文件,可以實現(xiàn)如本文所描述的:將時間關鍵代碼放置在ITCM中,減小延遲并提高響應速度;而不常用的代碼則可以存放在外部SDRAM或QSPI Flash中,既能節(jié)省內存空間,又能提高程序的擴展性。
理解這些內存和存儲技術的優(yōu)勢和適用場景,可以幫助我們開發(fā)者在實際項目中做出更高效的內存管理決策。