STM32鏈接腳本詳解

程序的編譯分爲四個步驟:預處理、彙編、編譯、鏈接。在開發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://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;
    }
}  

 

發佈了7 篇原創文章 · 獲贊 3 · 訪問量 1326
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章