一.什么是分散加載及其作用
1.簡單來說MCU是將Flash,SRAM等存儲介質映射成一個虛擬地址來訪問的而映射的虛擬地址是划分了很多個區域,再不自己定義分散加載文件時,Keil MDK工程的默認程序各個部分的擺放如下圖所示(示例,其他Cortex-M系列MCU的程序擺放與之類似):
而分散加載文件就是可以通過這個腳本文件來自己定義各個不同的位置,哪里存的是代碼、哪里存的是數據,去哪個特定的地址找到下一步需要運行的函數的東東,就是高速編譯器把每一個編譯好的函數、數據放到具體的哪一個物理地址,分散加載腳本有以下幾種用途
a:Bootloader & 程序升級
一個Bootloader程序和一個用戶程序,那么這就需要調整分散加載文件,以達成在一個Flash里面同時擺放兩個不同程序的目的。
b:訪問擴展存儲&對存儲區的划分
如果你要把外擴的存儲用於運行代碼/擴展RW數據段等用途,簡單來說就是把片內地址映射到片外,需要按照尋址空間的方式來訪問擴展存儲的話,比如擴展Nor-Flash、擴展SDRAM、擴展SRAM等,那就需要分散加載配合。
所以分散加載的根本目的就是:
<1>把RO-data數據段、RW數據段、從片內程序存儲區里面(一般是片內Flash),搬到片內程序運行區(一般是片內SRAM);
<2>在片內程序運行區(一般是片內SRAM)內分配ZI數據段運行需要的空間並把這段數據初始化為0;
<3>初始化堆棧;
<4>對於有些指定加載到程序運行區(一般是片內SRAM)的RO數據段,把他們加載到程序運行區(一般是片內SRAM)里面。

分散加載文件的基本節構,如下:
LOAD_ROM_1 0x0000 【加載域描述】這段是要告訴鏈接器,你的程序是存在哪里?我從哪里去找需要執行的代碼。
{
EXEC_ROM_1 0x0000 【運行域描述】這段是要告訴鏈接器,你的程序在哪里執行,在ARM Cortex-M系列的絕大多 數MCU中加載域以及運行域是在同一個空間上的,即片內Flash。
{
program1.o (+RO) 【輸入節描述】就是告訴鏈接器,具體把哪一個以及怎么把這一個obj文件放到運行域里面
}
DRAM 0x18000 0x8000 【運行域描述】這里指的是RAM空間的運行域,下面會解釋為什么這里會有兩個運行域
{
program1.o (+RW,+ZI) 【輸入節描述】告訴鏈接器,去哪里找執行程序是需要使用的變量以及數據
}
}
LOAD_ROM_2 0x4000 【另一個加載域描述】同一個工程可以有多個加載域,就好像同一台電腦可以裝幾個操作系統
{
EXEC_ROM_2 0x4000 【另一個運行域描述】
{
program2.o (+RO) 【另一個輸入節】
}
SRAM 0x8000 0x8000 【另一個運行域描述】
{
program2.o (+RW,+ZI) 【另一個輸入節】
}
}
所以分散加載可以簡單理解為的最基本結構就是至少3個域(這個事實上不對,但是對於大多數Cortex-M系列MCU的分散加載可以這樣簡單理解): 至少一個加載域、建議兩個運行域(一個RO運行域、一個RW+ZI運行域), 就是你要告訴鏈接器至少3個信息,即:從哪里加載程序(至少一個域)、在哪里運行程序(至少一個域)、在哪里讀寫程序運行中用到的變量(至少一個域,實際上也可以跟運行程序的域在一起,但強烈建議分開)。
三、分散加載的語法
分散加載的語言屬於C語言也不是匯編語言而是一種特殊的腳本語言,他有自己的語法規則而且他不能被調試意味着寫錯了只能自己慢慢找錯誤
1、加載域描述
加載域
load_region_name (base_address | ("+" offset)) [attribute_list] [max_size]
{
execution_region_description+
}
2、執行域描述
執行域
exec_region_name (base_address | "+" offset) [attribute_list] [max_size | length]
{
input_section_description*
}
3、輸入節描述
輸入節:
input_section_description ::=
module_select_pattern
[ "(" input_section_selector ( "," input_section_selector )* ")" ]
input_section_selector ::=
("+" input_section_attr | input_section_pattern | input_symbol_pattern)

“InRoot$$Sections”與“根區”的說明:
分散加載受到的一個限制是負責創建執行區的代碼和數據不能將自己復制到另一個位置,分散加載需要有一個根區,根區本質是一個RO執行域,其執行地址與其加載地址相同。
根區中需要包含以下幾個節:
<1>復制代碼和數據的程序代碼,也就是__main.o和__scatter*.o(__scatter.o、__scatter_copy.o、__scatter_zi.o等)
<2>執行壓縮的__dc*.o(同樣包含很多以__dc開頭的obj文件)
<3>Region$$Table節,包含要復制和壓縮的帶么和數據的地址
如果要在其他非根區內指定*(+RO)那么需要使用“InRoot$$Sections”來在根區顯示的指定這些節。簡單來說,就是每個分散加載都要有一個根區,根區需要指定“InRoot$$Sections”節或者等效的其他節(簡單來說就是分散加載、__main、數據壓縮這些東西必須要放到根區)。
不用“InRoot$$Sections”來指定根區,那么如下的編寫方式也是OK的:
要注意的是__main.o、__scatter.o、__scatter_copy.o 等不能被加載到第二塊 Flash的運行時域 ER_IROM2,也就是說這幾項目數據只能加載到 ER_IROM1 的運行時域。因為分散加載文件有一項很強大的功能,就是可以將Flash的代碼拷貝到RAM中運行,這一段拷貝代碼就存在於__main()函數中,但拷貝代碼不能拷貝自身,所以必須規定有一個運行時域中存放的代碼是不會被拷貝的,這個指的就是第一個運行時域。
三、分散加載的單獨函數/變量的指定加載
編譯器通過單個源文件生成RO、RW和ZI節。要將單個函數或者數據固定放在特定的地址上,我們必須允許鏈接器單獨處理這個函數或數據並且與其他的部分分開。
<1>使用--split_sections這個編譯器選項,為每個函數單獨生成一個節(分散加載操作以節為單位),再單獨分配這些節;
<2>使用__attribute__((at(address)))來指定放置;
<3>使用__attribute__((section("name")))來放置一個被命名的節;
第一種較為吃資源不討論
使用__attribute__((at(address)))來指定放置 例如
int var1 __attribute__((at(0x10000000))) = 0x55;
代碼直接向0x10000000地址分配一個變量,並賦值0x55;這種方式會有所限制
• __at 節地址范圍不能重疊,除非將重疊節放在不同的重疊區中
• 不允許在與位置無關的運行域中使用 __at 節
• 不能在 System V 和 BPABI 可執行文件和 BPABI DLL 中使用 __at 節
• __at 節地址必須是其對齊邊界的倍數
• __at 節忽略所有 +FIRST 或 +LAST 排序約束。
使用__attribute__((section("name")))來放置一個被命名的節
LR_IROM1 0x00000000 0x00008000 {
ER_IROM1 0x00000000 0x00008000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
ER_mySection 0x10000000
{
main.o(mySection)
}
RW_IRAM1 0x1000000 0x00002000 {
.ANY (+RW +ZI)
}
ARM_LIB_STACK 0x10002000 EMPTY -0x200{}
}
使用__attribute__((section("name")))同樣可以放置函數如下
__attribute__((section("mySection"))) void GPIOInit (void)
{
xxxx
xxxx
}
四、Flash 特殊要求應用
普通方式生成的 Bin文件有兩個,也可以用FIXED來修飾只生成一個 Bin文件的方式來加載兩個時域,但這種方式也有一個缺點,就是其是以填充的方式產生的成一個 Bin 文件,當兩個運行時域地址相距很大時,就會導致填充出來的 Bin 文件非常大,所以不適合於雙 Flash 應用。