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

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

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

圖3

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

圖5
2、代碼解讀

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

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

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

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

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

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

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

圖13

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

圖15

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

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

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

圖19
那么接下來,我們?cè)龠M(jìn)一步確認(rèn)一下,當(dāng)程序運(yùn)行到特定文件時(shí),他的運(yùn)行地址是否會(huì)切換到ITCM RAM的地址范圍呢,如圖20所示,當(dāng)我們從main.c運(yùn)行到MainRAM.c中時(shí),運(yùn)行地址確實(shí)成功切換了。所以,我們想讓時(shí)間關(guān)鍵代碼運(yùn)行在更快的RAM中的目標(biāo), 也確實(shí)實(shí)現(xiàn)了。

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

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

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

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

圖24
然后我們可以調(diào)試一下,看一下效果,這里需要注意,在調(diào)試前,需要在Flash Download里,選擇上對(duì)應(yīng)的燒錄算法,如圖25所示,內(nèi)部Flash和外部QSPI Flash的兩個(gè)算法都要加載到軟件里來。

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

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