程序的編譯分爲四個步驟:預處理、彙編、編譯、鏈接。在開發STM32時,我們只要在IDE中點擊編譯就能一次性完成這4個步驟,實際上IDE也是要經過這些步驟的,只不過IDE爲我們屏蔽了很多細節。
首先我們需要了解一個image文件的構成。image即編譯的產物,我們編譯STM32生成的bin文件此處稱之爲image。一個image文件由RO段和RW段組成,RO段包含只讀的代碼段和常量,RW段包含可讀可寫的全局變量和靜態變量。因爲程序剛運行時,RW段還在FLASH中,需要一段程序將這些變量複製到RAM中,STM32的啓動文件的__main函數幫我們完成了這一動作。RW段中初始值爲0的段爲ZI段,image文件無需包含ZI段,因爲ZI段包含的是全局或靜態初始值爲0的變量,只要在程序運行後,將對應的RAM區域清零即可。
這裏又涉及到另一個概念:加載地址和運行地址。加載地址是指讀取程序的地址,運行地址是指程序運行的入口地址。STM32因爲有XIP(executed in place)技術,加載地址和運行地址是一樣的,都是0x08000000。簡而言之,如果程序在FLASH中運行,加載地址和運行地址是相同的,例如STM32等單片機;如果程序存放在FLASH裏,而運行是在RAM裏,那麼加載地址指向FLASH,運行地址指向RAM,例如跑Linux系統的一些芯片。上面的RW段,其加載地址指向FLASH,而運行地址指向RAM,因此需要拷貝。
如何指定各個C文件的編譯產物(.o格式)在RO段的順序?又如何確定程序的加載地址和運行地址呢?這都是靠一個腳本來完成的,即鏈接腳本。在Linux下,鏈接腳本爲lds文件;在KEIL中,鏈接腳本爲sct文件;在IAR中,鏈接腳本爲icf文件。本文以KEIL下的sct文件爲例,講解鏈接腳本結構。
我們可以通過編寫一個分散加載文件來指定 ARM 連接器在生成映像文件時如何分配 Code、RO-Data, RW-Data, ZI-Data 等數據的存放地址。稱爲分散加載文件實際上就是鏈接腳本,如果不修改KEIL的鏈接腳本,那麼會使用默認的鏈接腳本,我們按照下圖的操作方式來查看默認的鏈接腳本,方法爲點擊工程設置,找到Link選項,去掉“Use Memory Layout from Target Dialog”前面的勾選,然後點擊Edit。
查看到的默認鏈接腳本如下,注:本例中使用的MCU型號爲STM32F103RC,FLASH容量爲256KB,RAM大小爲64KB。
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00040000 { ; 加載時域起始地址爲0x08000000,大小爲0x40000
ER_IROM1 0x08000000 0x00040000 { ; 第一個運行時域,運行地址爲0x08000000,大小爲0x40000
*.o (RESET, +First) ; RESET段最先鏈接,RESET段在啓動文件中有聲明
*(InRoot$$Sections) ; 鏈接__main函數,該函數用於RW段數據的拷貝和ZI段數據的清零
.ANY (+RO) ; 剩餘的code、RO數據隨意鏈接
}
RW_IRAM1 0x20000000 0x0000C000 { ; 第二個運行時域,運行地址爲0x20000000,大小爲0xC000
.ANY (+RW +ZI) ; 存放所有的RW段數據和ZI段數據
}
}
分散加載文件主要由一個加載時域和多個運行時域組成。
加載時域,顧名思義用於加載並存儲數據,包括 Code、 RO-Data 和 RW-Data。
運行時域, 用於爲運行時分配變量及代碼映射空間, 包含 Code、 ZI-Data、 RW-Data。
分散加載有3條規則需要特別注意:
1、第一個運行時域的基址必須與加載域基址相同。
2、第一個運行時域存放的代碼不會進行額外拷貝。
3、一個加載時域,有且僅有一個不拷貝的運行時域,FIXED關鍵字修飾除外。
分散加載的用途有很多,例如:
1、我們可以通過修改分散加載文件將部分代碼或整個代碼放到RAM中運行以提高運行速度。
2、可以將一組函數放在特定地址上,作爲Firmware供app程序調用。
3、將程序分成boot和app,實現升級功能。實現這個功能可以不修改分散加載文件,直接在keil裏設置即可。
4、定義section來靈活地存放特定的數據。
關於分散加載的更多知識,可以參考周立功寫的一篇文檔《keil分散加載文件淺釋》,我已經傳到百度網盤:
鏈接:https://pan.baidu.com/s/1heC-pLmi_eeqS_SU19dmIA 提取碼:iguq
有時候我們需要在程序運行時知道各個段的起始地址、結束地址、大小等信息,這些信息鏈接器已經幫我們導出了,下面給出了一個使用的例子,這個例子實際上完成了__main的部分功能,即把FLASH中的RW段數據拷貝到RAM的運行地址上,並將RAM中的ZI段數據清零。
void RW_And_ZI_Init (void)
{
extern unsigned char Image$$ER_IROM1$$Limit; // 獲取RW段在FLASH中的加載地址
extern unsigned char Image$$RW_IRAM1$$Base; // 獲取RW段在RAM中的運行地址
extern unsigned char Image$$RW_IRAM1$$RW$$Limit; // 獲取RW段在RAM中的結束地址
extern unsigned char Image$$RW_IRAM1$$ZI$$Limit; // 獲取ZI段在RAM中的結束地址
unsigned char * psrc, *pdst, *plimt;
psrc = (unsigned char *)&Image$$ER_IROM1$$Limit;
pdst = (unsigned char *)&Image$$RW_IRAM1$$Base;
plimt = (unsigned char *)&Image$$RW_IRAM1$$RW$$Limit;
while(pdst < plimt) // 將FLASH中的RW段拷貝到RAM的RW段運行地址上
{
*pdst++ = *psrc++;
}
psrc = (unsigned char *)&Image$$RW_IRAM1$$RW$$Limit;
plimt = (unsigned char *)&Image$$RW_IRAM1$$ZI$$Limit;
while(psrc < plimt) // 將RAM中的ZI段清零
{
*psrc++ = 0;
}
}