程序的編譯分為四個步驟:預處理、彙編、編譯、鍊接。在開發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。
不使用KEIL的默認鍊接腳本
查看到的默認鍊接腳本如***:本例中使用的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:///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; }
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!