第48章 MDK的編譯過程及文件類型全解
全套200集視頻教程和1000頁PDF教程請到秉火論壇下載:www.firebbs.cn
野火視頻教程優酷觀看網址:http://i.youku.com/firege
本章參考資料:MDK的幫助手冊《ARM Development Tools》,點擊MDK界面的"help->uVision Help"菜單可打開該文件。關於ELF文件格式,參考配套資料里的《ELF文件格式》文件。
在本章中講解了非常多的文件類型,學習時請跟着教程的節奏,打開實際工程中的文件來了解。
相信您已經非常熟練地使用MDK創建應用程序了,平時使用MDK編寫源代碼,然后編譯生成機器碼,再把機器碼下載到STM32芯片上運行,但是這個編譯、下載的過程MDK究竟做了什么工作?它編譯后生成的各種文件又有什么作用?本章節將對這些過程進行講解,了解編譯及下載過程有助於理解芯片的工作原理,這些知識對制作IAP(bootloader)以及讀寫控制器內部FLASH的應用時非常重要。
48.1 編譯過程
48.1.1 編譯過程簡介
首先我們簡單了解下MDK的編譯過程,它與其它編譯器的工作過程是類似的,該過程見圖 481。
圖 481 MDK編譯過程
編譯過程生成的不同文件將在后面的小節詳細說明,此處先抓住主要流程來理解。
(1) 編譯,MDK軟件使用的編譯器是armcc和armasm,它們根據每個c/c++和匯編源文件編譯成對應的以".o"為后綴名的對象文件(Object Code,也稱目標文件),其內容主要是從源文件編譯得到的機器碼,包含了代碼、數據以及調試使用的信息;
(2) 鏈接,鏈接器armlink把各個.o文件及庫文件鏈接成一個映像文件".axf"或".elf";
(3) 格式轉換,一般來說Windows或Linux系統使用鏈接器直接生成可執行映像文件elf后,內核根據該文件的信息加載后,就可以運行程序了,但在單片機平台上,需要把該文件的內容加載到芯片上,所以還需要對鏈接器生成的elf映像文件利用格式轉換器fromelf轉換成".bin"或".hex"文件,交給下載器下載到芯片的FLASH或ROM中。
48.1.2 具體工程中的編譯過程
下面我們打開"多彩流水燈"的工程,以它為例進行講解,其它工程的編譯過程也是一樣的,只是文件有差異。打開工程后,點擊MDK的"rebuild"按鈕,它會重新構建整個工程,構建的過程會在MDK下方的"Build Output"窗口輸出提示信息,見圖 482。
圖 482 編譯工程時的編譯提示
構建工程的提示輸出主要分6個部分,說明如下:
(1) 提示信息的第一部分說明構建過程調用的編譯器。圖中的編譯器名字是"V5.06(build 20)",后面附帶了該編譯器所在的文件夾。在電腦上打開該路徑,可看到該編譯器包含圖 483中的各個編譯工具,如armar、armasm、armcc、armlink及fromelf,后面四個工具已在圖 481中已講解,而armar是用於把.o文件打包成lib文件的。
圖 483 編譯工具
(2) 使用armasm編譯匯編文件。圖中列出了編譯startup啟動文件時的提示,編譯后每個匯編源文件都對應有一個獨立的.o文件。
(3) 使用armcc編譯c/c++文件。圖中列出了工程中所有的c/c++文件的提示,同樣地,編譯后每個c/c++源文件都對應有一個獨立的.o文件。
(4) 使用armlink鏈接對象文件,根據程序的調用把各個.o文件的內容鏈接起來,最后生成程序的axf映像文件,並附帶程序各個域大小的說明,包括Code、RO-data、RW-data及ZI-data的大小。
(5) 使用fromelf生成下載格式文件,它根據axf映像文件轉化成hex文件,並列出編譯過程出現的錯誤(Error)和警告(Warning)數量。
(6) 最后一段提示給出了整個構建過程消耗的時間。
構建完成后,可在工程的"Output"及"Listing"目錄下找到由以上過程生成的各種文件,見圖 484。
圖 484 編譯后Output及Listing文件夾中的內容
可以看到,每個C源文件都對應生成了.o、.d及.crf后綴的文件,還有一些額外的.dep、.hex、.axf、.htm、.lnp、.sct、.lst及.map文件。
48.2 程序的組成、存儲與運行
48.2.1 CODE、RO、RW、ZI Data域及堆棧空間
在工程的編譯提示輸出信息中有一個語句"Program Size:Code=xx RO-data=xx RW-data=xx ZI-data=xx",它說明了程序各個域的大小,編譯后,應用程序中所有具有同一性質的數據(包括代碼)被歸到一個域,程序在存儲或運行的時候,不同的域會呈現不同的狀態,這些域的意義如下:
Code:即代碼域,它指的是編譯器生成的機器指令,這些內容被存儲到ROM區。
RO-data:Read Only data,即只讀數據域,它指程序中用到的只讀數據,這些數據被存儲在ROM區,因而程序不能修改其內容。例如C語言中const關鍵字定義的變量就是典型的RO-data。
RW-data:Read Write data,即可讀寫數據域,它指初始化為"非0值"的可讀寫數據,程序剛運行時,這些數據具有非0的初始值,且運行的時候它們會常駐在RAM區,因而應用程序可以修改其內容。例如C語言中使用定義的全局變量,且定義時賦予"非0值"給該變量進行初始化。
ZI-data:Zero Initialie data,即0初始化數據,它指初始化為"0值"的可讀寫數據域,它與RW-data的區別是程序剛運行時這些數據初始值全都為0,而后續運行過程與RW-data的性質一樣,它們也常駐在RAM區,因而應用程序可以更改其內容。例如C語言中使用定義的全局變量,且定義時賦予"0值"給該變量進行初始化(若定義該變量時沒有賦予初始值,編譯器會把它當ZI-data來對待,初始化為0);
ZI-data的棧空間(Stack)及堆空間(Heap):在C語言中,函數內部定義的局部變量屬於棧空間,進入函數的時候從向棧空間申請內存給局部變量,退出時釋放局部變量,歸還內存空間。而使用malloc動態分配的變量屬於堆空間。在程序中的棧空間和堆空間都是屬於ZI-data區域的,這些空間都會被初始值化為0值。編譯器給出的ZI-data占用的空間值中包含了堆棧的大小(經實際測試,若程序中完全沒有使用malloc動態申請堆空間,編譯器會優化,不把堆空間計算在內)。
綜上所述,以程序的組成構件為例,它們所屬的區域類別見表 481。
表 481 程序組件所屬的區域
程序組件 |
所屬類別 |
機器代碼指令 |
Code |
常量 |
RO-data |
初值非0的全局變量 |
RW-data |
初值為0的全局變量 |
ZI-data |
局部變量 |
ZI-data棧空間 |
使用malloc動態分配的空間 |
ZI-data堆空間 |
48.2.2 程序的存儲與運行
RW-data和ZI-data它們僅僅是初始值不一樣而已,為什么編譯器非要把它們區分開?這就涉及到程序的存儲狀態了,應用程序具有靜止狀態和運行狀態。靜止態的程序被存儲在非易失存儲器中,如STM32的內部FLASH,因而系統掉電后也能正常保存。但是當程序在運行狀態的時候,程序常常需要修改一些暫存數據,由於運行速度的要求,這些數據往往存放在內存中(RAM),掉電后這些數據會丟失。因此,程序在靜止與運行的時候它在存儲器中的表現是不一樣的,見圖 485。
圖 485 應用程序的加載視圖與執行視圖
圖中的左側是應用程序的存儲狀態,右側是運行狀態,而上方是RAM存儲器區域,下方是ROM存儲器區域。
程序在存儲狀態時,RO節(RO section)及RW節都被保存在ROM區。當程序開始運行時,內核直接從ROM中讀取代碼,並且在執行主體代碼前,會先執行一段加載代碼,它把RW節數據從ROM復制到RAM,並且在RAM加入ZI節,ZI節的數據都被初始化為0。加載完后RAM區准備完畢,正式開始執行主體程序。
編譯生成的RW-data的數據屬於圖中的RW節,ZI-data的數據屬於圖中的ZI節。是否需要掉電保存,這就是把RW-data與ZI-data區別開來的原因,因為在RAM創建數據的時候,默認值為0,但如果有的數據要求初值非0,那就需要使用ROM記錄該初始值,運行時再復制到RAM。
STM32的RO區域不需要加載到SRAM,內核直接從FLASH讀取指令運行。計算機系統的應用程序運行過程很類似,不過計算機系統的程序在存儲狀態時位於硬盤,執行的時候甚至會把上述的RO區域(代碼、只讀數據)加載到內存,加快運行速度,還有虛擬內存管理單元(MMU)輔助加載數據,使得可以運行比物理內存還大的應用程序。而STM32沒有MMU,所以無法支持Linux和Windows系統。
當程序存儲到STM32芯片的內部FLASH時(即ROM區),它占用的空間是Code、RO-data及RW-data的總和,所以如果這些內容比STM32芯片的FLASH空間大,程序就無法被正常保存了。當程序在執行的時候,需要占用內部SRAM空間(即RAM區),占用的空間包括RW-data和ZI-data。應用程序在各個狀態時各區域的組成見表 482。
表 482 程序狀態區域的組成
程序狀態與區域 |
組成 |
程序執行時的只讀區域(RO) |
Code + RO data |
程序執行時的可讀寫區域(RW) |
RW data + ZI data |
程序存儲時占用的ROM區 |
Code + RO data + RW data |
在MDK中,我們建立的工程一般會選擇芯片型號,選擇后就有確定的FLASH及SRAM大小,若代碼超出了芯片的存儲器的極限,編譯器會提示錯誤,這時就需要裁剪程序了,裁剪時可針對超出的區域來優化。
48.3 編譯工具鏈
在前面編譯過程中,MDK調用了各種編譯工具,平時我們直接配置MDK,不需要學習如何使用它們,但了解它們是非常有好處的。例如,若希望使用MDK編譯生成bin文件的,需要在MDK中輸入指令控制fromelf工具;在本章后面講解AXF及O文件的時候,需要利用fromelf工具查看其文件信息,這都是無法直接通過MDK做到的。關於這些工具鏈的說明,在MDK的幫助手冊《ARM Development Tools》都有詳細講解,點擊MDK界面的"help->uVision Help"菜單可打開該文件。
48.3.1 設置環境變量
調用這些編譯工具,需要用到Windows的"命令行提示符工具",為了讓命令行方便地找到這些工具,我們先把工具鏈的目錄添加到系統的環境變量中。查看本機工具鏈所在的具體目錄可根據上一小節講解的工程編譯提示輸出信息中找到,如本機的路徑為"D:\work\keil5\ARM\ARMCC\bin"。
1. 添加路徑到PATH環境變量
本文以Win7系統為例添加工具鏈的路徑到PATH環境變量,其它系統是類似的。
(1) 右鍵電腦系統的"計算機圖標",在彈出的菜單中選擇"屬性",見圖 486;
圖 486 計算機屬性頁面
(2) 在彈出的屬性頁面依次點擊"高級系統設置"->"環境變量",在用戶變量一欄中找到名為"PATH"的變量,若沒有該變量,則新建一個。編輯"PATH"變量,在它的變量值中輸入工具鏈的路徑,如本機的是";D:\work\keil5\ARM\ARMCC\bin",注意要使用"分號;"讓它與其它路徑分隔開,輸入完畢后依次點確定,見圖 487;
圖 487 添加工具鏈路徑到PATH變量
(3) 打開Windows的命令行,點擊系統的"開始菜單",在搜索框輸入"cmd",在搜索結果中點擊"cmd.exe"即可打開命令行,見圖 488;
圖 488 打開命令行
(4) 在彈出的命令行窗口中輸入"fromelf"回車,若窗口打印出formelf的幫助說明,那么路徑正常,就可以開始后面的工作了;若提示"不是內部名外部命令,也不是可運行的程序…"信息,說明路徑不對,請重新配置環境變量,並確認該工作目錄下有編譯工具鏈。
這個過程本質就是讓命令行通過"PATH"路徑找到"fromelf.exe"程序運行,默認運行"fromelf.exe"時它會輸出自己的幫助信息,這就是工具鏈的調用過程,MDK本質上也是如此調用工具鏈的,只是它集成為GUI,相對於命令行對用戶更友好,畢竟上述配置環境變量的過程已經讓新手煩躁了。
48.3.2 armcc、armasm及armlink
接下來我們看看各個工具鏈的具體用法,主要以armcc為例。
1. armcc
armcc用於把c/c++文件編譯成ARM指令代碼,編譯后會輸出ELF格式的O文件(對象、目標文件),在命令行中輸入"armcc"回車可調用該工具,它會打印幫助說明,見圖 489
圖 489 armcc的幫助提示
幫助提示中分三部分,第一部分是armcc版本信息,第二部分是命令的用法,第三部分是主要命令選項。
根據命令用法: armcc [options] file1 file2 ... filen ,在[option]位置可輸入下面的"--arm"、"--cpu list"選項,若選項帶文件輸入,則把文件名填充在file1 file2…的位置,這些文件一般是c/c++文件。
例如根據它的幫助說明,"--cpu list"可列出編譯器支持的所有cpu,我們在命令行中輸入"armcc --cpu list",可查看圖 4810中的cpu列表。
圖 4810 cpulist
打開MDK的Options for Targe->c/c++菜單,可看到MDK對編譯器的控制命令,見圖 4811。
圖 4811 MDK的ARMCC編譯選項
從該圖中的命令可看到,它調用了-c、-cpu –D –g –O1等編譯選項,當我們修改MDK的編譯配置時,可看到該控制命令也會有相應的變化。然而我們無法在該編譯選項框中輸入命令,只能通過MDK提供的選項修改。
了解這些,我們就可以查詢具體的MDK編譯選項的具體信息了,如c/c++選項中的"Optimization:Leve 1(-O1)"是什么功能呢?首先可了解到它是"-O"命令,命令后還帶個數字,查看MDK的幫助手冊,在armcc編譯器說明章節,可詳細了解,如圖 489。
圖 4812 編譯器選項說明
利用MDK,我們一般不需要自己調用armcc工具,但經過這樣的過程我們就會對MDK有更深入的認識,面對它的各種編譯選項,就不會那么頭疼了。
2. armasm
armasm是匯編器,它把匯編文件編譯成O文件。與armcc類似,MDK對armasm的調用選項可在"Option for Target->Asm"頁面進行配置,見圖 4813。
圖 4813 armasm與MDK的編譯選項
3. armlink
armlink是鏈接器,它把各個O文件鏈接組合在一起生成ELF格式的AXF文件,AXF文件是可執行的,下載器把該文件中的指令代碼下載到芯片后,該芯片就能運行程序了;利用armlink還可以控制程序存儲到指定的ROM或RAM地址。在MDK中可在"Option for Target->Linker"頁面配置armlink選項,見圖 4814。
圖 4814 armlink與MDK的配置選項
鏈接器默認是根據芯片類型的存儲器分布來生成程序的,該存儲器分布被記錄在工程里的sct后綴的文件中,有特殊需要的話可自行編輯該文件,改變鏈接器的鏈接方式,具體后面我們會詳細講解。
48.3.3 armar、fromelf及用戶指令
armar工具用於把工程打包成庫文件,fromelf可根據axf文件生成hex、bin文件,hex和bin文件是大多數下載器支持的下載文件格式。
在MDK中,針對armar和fromelf工具的選項幾乎沒有,僅集成了生成HEX或Lib的選項,見圖 4815。
圖 4815 MDK中,控制fromelf生成hex及控制armar生成lib的配置
例如如果我們想利用fromelf生成bin文件,可以在MDK的"Option for Target->User"頁中添加調用fromelf的指令,見圖 4816。
圖 4816 在MDK中添加指令
在User配置頁面中,提供了三種類型的用戶指令輸入框,在不同組的框輸入指令,可控制指令的執行時間,分別是編譯前(Before Compile c/c++ file)、構建前(Before Build/Rebuild)及構建后(After Build/Rebuild)執行。這些指令並沒有限制必須是arm的編譯工具鏈,例如如果您自己編寫了python腳本,也可以在這里輸入用戶指令執行該腳本。
圖中的生成bin文件指令調用了fromelf工具,緊跟后面的是工具的選項及輸出文件名、輸入文件名。由於fromelf是根據axf文件生成bin的,而axf文件又是構建(build)工程后才生成,所以我們把該指令放到"After Build/Rebuild"一欄。
48.4 MDK工程的文件類型
除了上述編譯過程生成的文件,MDK工程中還包含了各種各樣的文件,下面我們統一介紹,MDK工程的常見文件類型見表 483。
表 483 MDK常見的文件類型(不分大小寫)
后綴 |
說明 |
Project目錄下的工程文件 |
|
*.uvguix |
MDK5工程的窗口布局文件,在MDK4中*.UVGUI后綴的文件功能相同 |
*.uvprojx |
MDK5的工程文件,它使用了XML格式記錄了工程結構,雙擊它可以打開整個工程,在MDK4中*.UVPROJ后綴的文件功能相同 |
*.uvoptx |
MDK5的工程配置選項,包含debugger、trace configuration、breakpooints以及當前打開的文件,在MDK4中*.UVOPT后綴的文件功能相同 |
*.ini |
某些下載器的配置記錄文件 |
源文件 |
|
*.c |
C語言源文件 |
*.cpp |
C++語言源文件 |
*.h |
C/C++的頭文件 |
*.s |
匯編語言的源文件 |
*.inc |
匯編語言的頭文件(使用"$include"來包含) |
Output目錄下的文件 |
|
*.lib |
庫文件 |
*.dep |
整個工程的依賴文件 |
*.d |
描述了對應.o的依賴的文件 |
*.crf |
交叉引用文件,包含了瀏覽信息(定義、引用及標識符) |
*.o |
可重定位的對象文件(目標文件) |
*.bin |
二進制格式的映像文件,是純粹的FLASH映像,不含任何額外信息 |
*.hex |
Intel Hex格式的映像文件,可理解為帶存儲地址描述格式的bin文件 |
*.elf |
由GCC編譯生成的文件,功能跟axf文件一樣,該文件不可重定位 |
*.axf |
由ARMCC編譯生成的可執行對象文件,可用於調試,該文件不可重定位 |
*.sct |
鏈接器控制文件(分散加載) |
*.scr |
鏈接器產生的分散加載文件 |
*.lnp |
MDK生成的鏈接輸入文件,用於調用鏈接器時的命令輸入 |
*.htm |
鏈接器生成的靜態調用圖文件 |
*.build_log.htm |
構建工程的日志記錄文件 |
Listing目錄下的文件 |
|
*.lst |
C及匯編編譯器產生的列表文件 |
*.map |
鏈接器生成的列表文件,包含存儲器映像分布 |
其它 |
|
*.ini |
仿真、下載器的腳本文件 |
這些文件主要分為MDK相關文件、源文件以及編譯、鏈接器生成的文件。我們以"多彩流水燈"工程為例講解各種文件的功能。
48.4.1 uvprojx、uvoptx、uvguix及ini工程文件
在工程的"Project"目錄下主要是MDK工程相關的文件,見圖 4817。
圖 4817 Project目錄下的uvprojx、uvoptx、uvguix及ini文件
1. uvprojx文件
uvprojx文件就是我們平時雙擊打開的工程文件,它記錄了整個工程的結構,如芯片類型、工程包含了哪些源文件等內容,見圖 4818。
圖 4818 工程包含的文件、芯片類型等內容
2. uvoptx文件
uvoptx文件記錄了工程的配置選項,如下載器的類型、變量跟蹤配置、斷點位置以及當前已打開的文件等等,見圖 4819。
圖 4819 代碼編輯器中已打開的文件
3. uvguix文件
uvguix文件記錄了MDK軟件的GUI布局,如代碼編輯區窗口的大小、編譯輸出提示窗口的位置等等。
圖 4820 記錄MDK工作環境中各個窗口的大小
uvprojx、uvoptx及uvguix都是使用XML格式記錄的文件,若使用記事本打開可以看到XML代碼,見圖 4817。而當使用MDK軟件打開時,它根據這些文件的XML記錄加載工程的各種參數,使得我們每次重新打開工程時,都能恢復上一次的工作環境。
圖 4821 使用記事本打開uvprojx、uvoptx及uvguix文件可看到XML格式的記錄
這些工程參數都是當MDK正常退出時才會被寫入保存,所以若MDK錯誤退出時(如使用Windows的任務管理器強制關閉),工程配置參數的最新更改是不會被記錄的,重新打開工程時要再次配置。根據這幾個文件的記錄類型,可以知道uvprojx文件是最重要的,刪掉它我們就無法再正常打開工程了,而uvoptx及uvguix文件並不是必須的,可以刪除,重新使用MDK打開uvprojx工程文件后,會以默認參數重新創建uvoptx及uvguix文件。(所以當使用Git/SVN等代碼管理的時候,往往只保留uvprojx文件)
48.4.2 源文件
源文件是工程中我們最熟悉的內容了,它們就是我們編寫的各種源代碼,MDK支持c、cpp、h、s、inc類型的源代碼文件,其中c、cpp分別是c/c++語言的源代碼,h是它們的頭文件,s是匯編文件,inc是匯編文件的頭文件,可使用"$include"語法包含。編譯器根據工程中的源文件最終生成機器碼。
48.4.3 Output目錄下生成的文件
點擊MDK中的編譯按鈕,它會根據工程的配置及工程中的源文件輸出各種對象和列表文件,在工程的"Options for Targe->Output->Select Folder for Objects"和"Options for Targe->Listing->Select Folder for Listings"選項配置它們的輸出路徑,見圖 4822和圖 4823。
圖 4822 設置Output輸出路徑
圖 4823設置Listing輸出路徑
編譯后Output和Listing目錄下生成的文件見圖 4824。
圖 4824 編譯后Output及Listing文件夾中的內容
接下來我們講解Output路徑下的文件。
1. lib庫文件
在某些場合下我們希望提供給第三方一個可用的代碼庫,但不希望對方看到源碼,這個時候我們就可以把工程生成lib文件(Library file)提供給對方,在MDK中可配置"Options for Target->Create Library"選項把工程編譯成庫文件,見圖 4825。
圖 4825 生成庫文件或可執行文件
工程中生成可執行文件或庫文件只能二選一,默認編譯是生成可執行文件的,可執行文件即我們下載到芯片上直接運行的機器碼。
得到生成的*.lib文件后,可把它像C文件一樣添加到其它工程中,並在該工程調用lib提供的函數接口,除了不能看到*.lib文件的源碼,在應用方面它跟C源文件沒有區別。
2. dep、d依賴文件
*.dep和*.d文件(Dependency file)記錄的是工程或其它文件的依賴,主要記錄了引用的頭文件路徑,其中*.dep是整個工程的依賴,它以工程名命名,而*.d是單個源文件的依賴,它們以對應的源文件名命名。這些記錄使用文本格式存儲,我們可直接使用記事本打開,見圖 4826和圖 4827。
圖 4826 工程的dep文件內容
圖 4827 bsp_led.d文件的內容
3. crf交叉引用文件
*.crf是交叉引用文件(Cross-Reference file),它主要包含了瀏覽信息(browse information),即源代碼中的宏定義、變量及函數的定義和聲明的位置。
我們在代碼編輯器中點擊"Go To Definition Of 'xxxx'"可實現瀏覽跳轉,見圖 4828,跳轉的時候,MDK就是通過*.crf文件查找出跳轉位置的。
圖 4828 瀏覽信息
通過配置MDK中的"Option for Target->Output->Browse Information"選項可以設置編譯時是否生成瀏覽信息,見圖 4829。只有勾選該選項並編譯后,才能實現上面的瀏覽跳轉功能。
圖 4829 在Options forTarget中設置是否生成瀏覽信息
*.crf文件使用了特定的格式表示,直接用文本編輯器打開會看到大部分亂碼,見圖 4830,我們不作深入研究。
圖 4830 crf文件內容
4. o、axf及elf文件
*.o、*.elf、*.axf、*.bin及*.hex文件都存儲了編譯器根據源代碼生成的機器碼,根據應用場合的不同,它們又有所區別。
ELF文件說明
*.o、*.elf、*.axf以及前面提到的lib文件都是屬於目標文件,它們都是使用ELF格式來存儲的,關於ELF格式的詳細內容請參考配套資料里的《ELF文件格式》文檔了解,它講解的是Linux下的ELF格式,與MDK使用的格式有小區別,但大致相同。在本教程中,僅講解ELF文件的核心概念。
ELF是Executable and Linking Format的縮寫,譯為可執行鏈接格式,該格式用於記錄目標文件的內容。在Linux及Windows系統下都有使用該格式的文件(或類似格式)用於記錄應用程序的內容,告訴操作系統如何鏈接、加載及執行該應用程序。
目標文件主要有如下三種類型:
(1) 可重定位的文件(Relocatable File),包含基礎代碼和數據,但它的代碼及數據都沒有指定絕對地址,因此它適合於與其他目標文件鏈接來創建可執行文件或者共享目標文件。 這種文件一般由編譯器根據源代碼生成。
例如MDK的armcc和armasm生成的*.o文件就是這一類,另外還有Linux的*.o 文件,Windows的 *.obj文件。
(2) 可執行文件(Executable File),它包含適合於執行的程序,它內部組織的代碼數據都有固定的地址(或相對於基地址的偏移),系統可根據這些地址信息把程序加載到內存執行。這種文件一般由鏈接器根據可重定位文件鏈接而成,它主要是組織各個可重定位文件,給它們的代碼及數據一一打上地址標號,固定其在程序內部的位置,鏈接后,程序內部各種代碼及數據段不可再重定位(即不能再參與鏈接器的鏈接)。
例如MDK的armlink生成的*.elf及*.axf文件,(使用gcc編譯工具可生成*.elf文件,用armlink生成的是*.axf文件,*.axf文件在*.elf之外,增加了調試使用的信息,其余區別不大,后面我們僅講解*.axf文件),另外還有Linux的/bin/bash文件,Windows的*.exe文件。
(3) 共享目標文件(Shared Object File), 它的定義比較難理解,我們直接舉例,MDK生成的*.lib文件就屬於共享目標文件,它可以繼續參與鏈接,加入到可執行文件之中。另外,Linux的.so,如/lib/ glibc-2.5.so,Windows的DLL都屬於這一類。
o文件與axf文件的關系
根據上面的分類,我們了解到,*.axf文件是由多個*.o文件鏈接而成的,而*.o文件由相應的源文件編譯而成,一個源文件對應一個*.o文件。它們的關系見圖 4831。
圖 4831*.axf文件與*.o文件的關系
圖中的中間代表的是armlink鏈接器,在它的右側是輸入鏈接器的*.o文件,左側是它輸出的*axf文件。
可以看到,由於都使用ELF文件格式,*.o與*.axf文件的結構是類似的,它們包含ELF文件頭、程序頭、節區(section)以及節區頭部表。各個部分的功能說明如下:
ELF文件頭用來描述整個文件的組織,例如數據的大小端格式,程序頭、節區頭在文件中的位置等。
程序頭告訴系統如何加載程序,例如程序主體存儲在本文件的哪個位置,程序的大小,程序要加載到內存什么地址等等。MDK的可重定位文件*.o不包含這部分內容,因為它還不是可執行文件,而armlink輸出的*.axf文件就包含該內容了。
節區是*.o文件的獨立數據區域,它包含提供給鏈接視圖使用的大量信息,如指令(Code)、數據(RO、RW、ZI-data)、符號表(函數、變量名等)、重定位信息等,例如每個由C語言定義的函數在*.o文件中都會有一個獨立的節區;
存儲在最后的節區頭則包含了本文件節區的信息,如節區名稱、大小等等。
總的來說,鏈接器把各個*.o文件的節區歸類、排列,根據目標器件的情況編排地址生成輸出,匯總到*.axf文件。例如,見圖 4832,"多彩流水燈"工程中在"bsp_led.c"文件中有一個LED_GPIO_Config函數,而它內部調用了"stm32f4xx_gpio.c"的GPIO_Init函數,經過armcc編譯后,LED_GPIO_Config及GPIO_Iint函數都成了指令代碼,分別存儲在bsp_led.o及stm32f4xx_gpio.o文件中,這些指令在*.o文件都沒有指定地址,僅包含了內容、大小以及調用的鏈接信息,而經過鏈接器后,鏈接器給它們都分配了特定的地址,並且把地址根據調用指向鏈接起來。
圖 4832 具體的鏈接過程
ELF文件頭
接下來我們看看具體文件的內容,使用fromelf文件可以查看*.o、*.axf及*.lib文件的ELF信息。
使用命令行,切換到文件所在的目錄,輸入"fromelf –text –v bsp_led.o"命令,可控制輸出bsp_led.o的詳細信息,見圖 4833。利用"-c、-z"等選項還可輸出反匯編指令文件、代碼及數據文件等信息,請親手嘗試一下。
圖 4833 使用fromelf查看o文件信息
為了便於閱讀,我已使用fromelf指令生成了"多彩流水燈.axf"、"bsp_led"及"多彩流水燈.lib"的ELF信息,並已把這些信息保存在獨立的文件中,在配套資料的"elf信息輸出"文件夾下可查看,見表 484。
表 484 配套資料里使用fromelf生成的文件
fromelf選項 |
可查看的信息 |
生成到配套資料里相應的文件 |
-v |
詳細信息 |
bsp_led_o_elfInfo_v.txt/多彩流水燈_axf_elfInfo_v.txt |
-a |
數據的地址 |
bsp_led_o_elfInfo_a.txt/多彩流水燈_axf_elfInfo_a.txt |
-c |
反匯編代碼 |
bsp_led_o_elfInfo_c.txt/多彩流水燈_axf_elfInfo_c.txt |
-d |
data section的內容 |
bsp_led_o_elfInfo_d.txt/多彩流水燈_axf_elfInfo_d.txt |
-e |
異常表 |
bsp_led_o_elfInfo_e.txt/多彩流水燈_axf_elfInfo_e.txt |
-g |
調試表 |
bsp_led_o_elfInfo_g.txt/多彩流水燈_axf_elfInfo_g.txt |
-r |
重定位信息 |
bsp_led_o_elfInfo_r.txt/多彩流水燈_axf_elfInfo_r.txt |
-s |
符號表 |
bsp_led_o_elfInfo_s.txt/多彩流水燈_axf_elfInfo_s.txt |
-t |
字符串表 |
bsp_led_o_elfInfo_t.txt/多彩流水燈_axf_elfInfo_t.txt |
-y |
動態段內容 |
bsp_led_o_elfInfo_y.txt/多彩流水燈_axf_elfInfo_y.txt |
-z |
代碼及數據的大小信息 |
bsp_led_o_elfInfo_z.txt/多彩流水燈_axf_elfInfo_z.txt |
直接打開"elf信息輸出"目錄下的bsp_led_o_elfInfo_v.txt文件,可看到代碼清單 481中的內容。
代碼清單 481 bsp_led.o文件的ELF文件頭(可到"bsp_led_o_elfInfo_v.txt"文件查看)
1 ========================================================================
2
3 ** ELF Header Information
4
5 File Name:
6 .\bsp_led.o //bsp_led.o文件
7
8 Machine class: ELFCLASS32 (32-bit) //32位機
9 Data encoding: ELFDATA2LSB (Little endian) //小端格式
10 Header version: EV_CURRENT (Current version)
11 Operating System ABI: none
12 ABI Version: 0
13 File Type: ET_REL (Relocatable object) (1) //可重定位文件類型
14 Machine: EM_ARM (ARM)
15
16 Entry offset (in SHF_ENTRYSECT section): 0x00000000
17 Flags: None (0x05000000)
18
19 ARM ELF revision: 5 (ABI version 2)
20
21 Built with
22 Component: ARM Compiler 5.06 (build 20) Tool: armasm [4d35a2]
23 Component: ARM Compiler 5.06 (build 20) Tool: armlink [4d35a3]
24
25 Header size: 52 bytes (0x34)
26 Program header entry size: 0 bytes (0x0) //程序頭大小
27 Section header entry size: 40 bytes (0x28)
28
29 Program header entries: 0
30 Section header entries: 246
31
32 Program header offset: 0 (0x00000000) //程序頭在文件中的位置(沒有程序頭)
33 Section header offset: 507224 (0x0007bd58) //節區頭在文件中的位置
34
35 Section header string table index: 243
36
37 =====================================================================
在上述代碼中已加入了部分注釋,解釋了相應項的意義,值得一提的是在這個*.o文件中,它的ELF文件頭中告訴我們它的程序頭(Program header)大小為"0 bytes",且程序頭所在的文件位置偏移也為"0",這說明它是沒有程序頭的。
程序頭
接下來打開"多彩流水燈_axf_elfInfo_v.txt"文件,查看工程的*.axf文件的詳細信息,見代碼清單 482。
代碼清單 482 *.axf文件中的elf文件頭及程序頭(可到"多彩流水燈_axf_elfInfo_v.txt"文件查看)
1 ===================================================================
2
3 ** ELF Header Information
4
5 File Name:
6 .\多彩流水燈.axf //多彩流水燈.axf 文件
7
8 Machine class: ELFCLASS32 (32-bit) //32位機
9 Data encoding: ELFDATA2LSB (Little endian) //小端格式
10 Header version: EV_CURRENT (Current version)
11 Operating System ABI: none
12 ABI Version: 0
13 File Type: ET_EXEC (Executable) (2) //可執行文件類型
14 Machine: EM_ARM (ARM)
15
16 Image Entry point: 0x080001ad
17 Flags: EF_ARM_HASENTRY + EF_ARM_ABI_FLOAT_SOFT (0x05000202)
18
19 ARM ELF revision: 5 (ABI version 2)
20
21 Conforms to Soft float procedure-call standard
22
23 Built with
24 Component: ARM Compiler 5.06 (build 20) Tool: armasm [4d35a2]
25 Component: ARM Compiler 5.06 (build 20) Tool: armlink [4d35a3]
26
27 Header size: 52 bytes (0x34)
28 Program header entry size: 32 bytes (0x20)
29 Section header entry size: 40 bytes (0x28)
30
31 Program header entries: 1
32 Section header entries: 15
33
34 Program header offset: 335252 (0x00051d94) //程序頭在文件中的位置
35 Section header offset: 335284 (0x00051db4) //節區頭在文件中的位置
36
37 Section header string table index: 14
38
39 =================================================================
40
41 ** Program header #0
42
43 Type : PT_LOAD (1) //表示這是可加載的內容
44 File Offset : 52 (0x34) //在文件中的偏移
45 Virtual Addr : 0x08000000 //虛擬地址(此處等於物理地址)
46 Physical Addr : 0x08000000 //物理地址
47 Size in file : 1456 bytes (0x5b0) //程序在文件中占據的大小
48 Size in memory: 2480 bytes (0x9b0) //若程序加載到內存,占據的內存空間
49 Flags : PF_X + PF_W + PF_R + PF_ARM_ENTRY (0x80000007)
50 Alignment : 8 //地址對齊
51
52
53 ===============================================================
對比之下,可發現*.axf文件的ELF文件頭對程序頭的大小說明為非0值,且給出了它在文件的偏移地址,在輸出信息之中,包含了程序頭的詳細信息。可看到,程序頭的"Physical Addr"描述了本程序要加載到的內存地址"0x0800 0000",正好是STM32內部FLASH的首地址;"size in file"描述了本程序占據的空間大小為"1456 bytes",它正是程序燒錄到FLASH中需要占據的空間。
節區頭
在ELF的原文件中,緊接着程序頭的一般是節區的主體信息,在節區主體信息之后是描述節區主體信息的節區頭,我們先來看看節區頭中的信息了解概況。通過對比*.o文件及*.axf文件的節區頭部信息,可以清楚地看出這兩種文件的區別,見代碼清單 483。
代碼清單 483 *.o文件的節區信息("bsp_led_o_elfInfo_v.txt"文件)
1 ====================================
2 ** Section #4
3
4 Name : i.LED_GPIO_Config //節區名
6
7 //此節區包含程序定義的信息,其格式和含義都由程序來解釋。
8 Type : SHT_PROGBITS (0x00000001)
10
11 //此節區在進程執行過程中占用內存。節區包含可執行的機器指令。
12 Flags :SHF_ALLOC + SHF_EXECINSTR (0x00000006)
14 Addr : 0x00000000 //地址
15 File Offset : 68 (0x44) //在文件中的偏移
16 Size : 116 bytes (0x74) //大小
17 Link : SHN_UNDEF
19 Info : 0
20 Alignment : 4 //字節對齊
21 Entry Size : 0
22 ====================================
這個節區的名稱為LED_GPIO_Config,它正好是我們在bsp_led.c文件中定義的函數名,這個節區頭描述的是該函數被編譯后的節區信息,其中包含了節區的類型(指令類型)、節區應存儲到的地址(0x00000000)、它主體信息在文件位置中的偏移(68)以及節區的大小(116 bytes)。
由於*.o文件是可重定位文件,所以它的地址並沒有被分配,是0x00000000(假如文件中還有其它函數,該函數生成的節區中,對應的地址描述也都是0)。當鏈接器鏈接時,根據這個節區頭信息,在文件中找到它的主體內容,並根據它的類型,把它加入到主程序中,並分配實際地址,鏈接后生成的*.axf文件,我們再來看看它的內容,見代碼清單 484。
代碼清單 484 *.axf文件的節區信息("多彩流水燈_axf_elfInfo_v.txt"文件)
1 ========================================================================
2 ** Section #1
3
4 Name : ER_IROM1 //節區名
5
6 //此節區包含程序定義的信息,其格式和含義都由程序來解釋。
7 Type : SHT_PROGBITS (0x00000001)
8
9 //此節區在進程執行過程中占用內存。節區包含可執行的機器指令
10 Flags : SHF_ALLOC + SHF_EXECINSTR (0x00000006)
11 Addr : 0x08000000 //地址
12 File Offset : 52 (0x34)
13 Size : 1456 bytes (0x5b0) //大小
14 Link : SHN_UNDEF
15 Info : 0
16 Alignment : 4
17 Entry Size : 0
18
19 ====================================
20 ** Section #2
21
22 Name : RW_IRAM1 //節區名
23
24 //包含將出現在程序的內存映像中的為初始
25 //化數據。根據定義,當程序開始執行,系統
26 //將把這些數據初始化為 0。
27 Type : SHT_NOBITS (0x00000008)
28
29 //此節區在進程執行過程中占用內存。節區包含進程執行過程中將可寫的數據。
30 Flags : SHF_ALLOC + SHF_WRITE (0x00000003)
31 Addr : 0x20000000 //地址
32 File Offset : 1508 (0x5e4)
33 Size : 1024 bytes (0x400) //大小
34 Link : SHN_UNDEF
35 Info : 0
36 Alignment : 8
37 Entry Size : 0
38 ====================================
在*.axf文件中,主要包含了兩個節區,一個名為ER_IROM1,一個名為RW_IRAM1,這些節區頭信息中除了具有*.o文件中節區頭描述的節區類型、文件位置偏移、大小之外,更重要的是它們都有具體的地址描述,其中 ER_IROM1的地址為0x08000000,而RW_IRAM1的地址為0x20000000,它們正好是內部FLASH及SRAM的首地址,對應節區的大小就是程序需要占用FLASH及SRAM空間的實際大小。
也就是說,經過鏈接器后,它生成的*.axf文件已經匯總了其它*.o文件的所有內容,生成的ER_IROM1節區內容可直接寫入到STM32內部FLASH的具體位置。例如,前面*.o文件中的i.LED_GPIO_Config節區已經被加入到*.axf文件的ER_IROM1節區的某地址。
節區主體及反匯編代碼
使用fromelf的-c選項可以查看部分節區的主體信息,對於指令節區,可根據其內容查看相應的反匯編代碼,打開"bsp_led_o_elfInfo_c.txt"文件可查看這些信息,見代碼清單 485。
代碼清單 485 *.o文件的LED_GPIO_Config節區及反匯編代碼(bsp_led_o_elfInfo_c.txt文件)
1 ** Section #4 'i.LED_GPIO_Config' (SHT_PROGBITS) [SHF_ALLOC + SHF_EXECINSTR]
2 Size : 116 bytes (alignment 4)
3 Address: 0x00000000
4
5 $t
6 i.LED_GPIO_Config
7 LED_GPIO_Config
8 // 地址內容 (ASCII碼) 內容對應的代碼
9 // (無意義)
10 0x00000000: e92d41fc -..A PUSH {r2-r8,lr}
11 0x00000004: 2101 .! MOVS r1,#1
12 0x00000006: 2088 . MOVS r0,#0x88
13 0x00000008: f7fffffe .... BL RCC_AHB1PeriphClockCmd
14 0x0000000c: f44f6580 O..e MOV r5,#0x400
15 0x00000010: 9500 .. STR r5,[sp,#0]
16 0x00000012: 2101 .! MOVS r1,#1
17 0x00000014: f88d1004 .... STRB r1,[sp,#4]
18 0x00000018: 2000 . MOVS r0,#0
19 0x0000001a: f88d0006 .... STRB r0,[sp,#6]
20 0x0000001e: f88d1007 .... STRB r1,[sp,#7]
21 0x00000022: f88d0005 .... STRB r0,[sp,#5]
22 0x00000026: 4f11 .O LDR r7,[pc,#68] ;
23 0x00000028: 4669 iF MOV r1,sp
24 0x0000002a: 4638 8F MOV r0,r7
25 0x0000002c: f7fffffe .... BL GPIO_Init
26 0x00000030: 006c l. LSLS r4,r5,#1
27 /*....以下省略**/
可看到,由於這是*.o文件,它的節區地址還是沒有分配的,基地址為0x00000000,接着在LED_GPIO_Config標號之后,列出了一個表,表中包含了地址偏移、相應地址中的內容以及根據內容反匯編得到的指令。細看匯編指令,還可看到它包含了跳轉到RCC_AHB1PeriphClockCmd及GPIO_Init標號的語句,而且這兩個跳轉語句原來的內容都是"f7fffffe",這是因為還*.o文件中並沒有RCC_AHB1PeriphClockCmd及GPIO_Init標號的具體地址索引,在*.axf文件中,這是不一樣的。
接下來我們打開"多彩流水燈_axf_elfInfo_c.txt"文件,查看*.axf文件中,ER_IROM1節區中對應LED_GPIO_Config的內容,見代碼清單 486。
代碼清單 486*.axf文件的LED_GPIO_Config反匯編代碼(多彩流水燈_axf_elfInfo_c.txt文件)
1 i.LED_GPIO_Config
2 LED_GPIO_Config
3 0x080002a4: e92d41fc -..A PUSH {r2-r8,lr}
4 0x080002a8: 2101 .! MOVS r1,#1
5 0x080002aa: 2088 . MOVS r0,#0x88
6 0x080002ac: f000f838 ..8. BL RCC_AHB1PeriphClockCmd ; 0x8000320
7 0x080002b0: f44f6580 O..e MOV r5,#0x400
8 0x080002b4: 9500 .. STR r5,[sp,#0]
9 0x080002b6: 2101 .! MOVS r1,#1
10 0x080002b8: f88d1004 .... STRB r1,[sp,#4]
11 0x080002bc: 2000 . MOVS r0,#0
12 0x080002be: f88d0006 .... STRB r0,[sp,#6]
13 0x080002c2: f88d1007 .... STRB r1,[sp,#7]
14 0x080002c6: f88d0005 .... STRB r0,[sp,#5]
15 0x080002ca: 4f11 .O LDR r7,[pc,#68] ; [0x8000310] = 0x40021c00
16 0x080002cc: 4669 iF MOV r1,sp
17 0x080002ce: 4638 8F MOV r0,r7
18 0x080002d0: f7ffffa5 .... BL GPIO_Init ; 0x800021e
19 0x080002d4: 006c l. LSLS r4,r5,#1
20 /*....以下省略**/
可看到,除了基地址以及跳轉地址不同之外,LED_GPIO_Config中的內容跟*.o文件中的一樣。另外,由於*.o是獨立的文件,而*.axf是整個工程匯總的文件,所以在*.axf中包含了所有調用到*.o文件節區的內容。例如,在"bsp_led_o_elfInfo_c.txt"(bsp_led.o文件的反匯編信息)中不包含RCC_AHB1PeriphClockCmd及GPIO_Init的內容,而在"多彩流水燈_axf_elfInfo_c.txt" (多彩流水燈.axf文件的反匯編信息)中則可找到它們的具體信息,且它們也有具體的地址空間。
在*.axf文件中,跳轉到RCC_AHB1PeriphClockCmd及GPIO_Init標號的這兩個指令后都有注釋,分別是"; 0x8000320"及"; 0x800021e",它們是這兩個標號所在的具體地址,而且這兩個跳轉語句的跟*.o中的也有區別,內容分別為"f000f838e"及"f7ffffa5"(*.o中的均為f7fffffe)。這就是鏈接器鏈接的含義,它把不同*.o中的內容鏈接起來了。
分散加載代碼
學習至此,還有一個疑問,前面提到程序有存儲態及運行態,它們之間應有一個轉化過程,把存儲在FLASH中的RW-data數據拷貝至SRAM。然而我們的工程中並沒有編寫這樣的代碼,在匯編文件中也查不到該過程,芯片是如何知道FLASH的哪些數據應拷貝到SRAM的哪些區域呢?
通過查看"多彩流水燈_axf_elfInfo_c.txt"的反匯編信息,了解到程序中具有一段名為"__scatterload"的分散加載代碼,見代碼清單 487,它是由armlink鏈接器自動生成的。
代碼清單 487 分散加載代碼(多彩流水燈_axf_elfInfo_c.txt文件)
1
2 .text
3 __scatterload
4 __scatterload_rt2
5 0x080001e4: 4c06 .L LDR r4,[pc,#24] ; [0x8000200] = 0x80005a0
6 0x080001e6: 4d07 .M LDR r5,[pc,#28] ; [0x8000204] = 0x80005b0
7 0x080001e8: e006 .. B 0x80001f8 ; __scatterload + 20
8 0x080001ea: 68e0 .h LDR r0,[r4,#0xc]
9 0x080001ec: f0400301 @... ORR r3,r0,#1
10 0x080001f0: e8940007 .... LDM r4,{r0-r2}
11 0x080001f4: 4798 .G BLX r3
12 0x080001f6: 3410 .4 ADDS r4,r4,#0x10
13 0x080001f8: 42ac .B CMP r4,r5
14 0x080001fa: d3f6 .. BCC 0x80001ea ; __scatterload + 6
15 0x080001fc: f7ffffda .... BL __main_after_scatterload ; 0x80001b4
16 $d
17 0x08000200: 080005a0 .... DCD 134219168
18 0x08000204: 080005b0 .... DCD 134219184
這段分散加載代碼包含了拷貝過程(LDM復制指令),而LDM指令的操作數中包含了加載的源地址,這些地址中包含了內部FLASH存儲的RW-data數據。而"__scatterload "的代碼會被"__main"函數調用,見代碼清單 488,__main在啟動文件中的"Reset_Handler"會被調用,因而,在主體程序執行前,已經完成了分散加載過程。
代碼清單 488 __main的反匯編代碼(部分,多彩流水燈_axf_elfInfo_c.txt文件)
1 __main
2 _main_stk
3 0x080001ac: f8dfd00c .... LDR sp,__lit__00000000 ; [0x80001bc] = 0x20000400
4 .ARM.Collect$$$$00000004
5 _main_scatterload
6 0x080001b0: f000f818 .... BL __scatterload ; 0x80001e4
5. hex文件及bin文件
若編譯過程無誤,即可把工程生成前面對應的*.axf文件,而在MDK中使用下載器(DAP/JLINK/ULINK等)下載程序或仿真的時候,MDK調用的就是*.axf文件,它解釋該文件,然后控制下載器把*.axf中的代碼內容下載到STM32芯片對應的存儲空間,然后復位后芯片就開始執行代碼了。
然而,脫離了MDK或IAR等工具,下載器就無法直接使用*.axf文件下載代碼了,它們一般僅支持hex和bin格式的代碼數據文件。默認情況下MDK都不會生成hex及bin文件,需要配置工程選項或使用fromelf命令。
生成hex文件
生成hex文件的配置比較簡單,在"Options for Target->Output->Create Hex File"中勾選該選項,然后編譯工程即可,見圖 4834。
圖 4834 生成hex文件的配置
生成bin文件
使用MDK生成bin文件需要使用fromelf命令,在MDK的"Options For Target->Users"中加入圖 4835中的命令。
圖 4835 使用fromelf指令生成bin文件
圖中的指令內容為:
"fromelf --bin --output ..\..\Output\多彩流水燈.bin ..\..\Output\多彩流水燈.axf"
該指令是根據本機及工程的配置而寫的,在不同的系統環境或不同的工程中,指令內容都不一樣,我們需要理解它,才能為自己的工程定制指令,首先看看fromelf的幫助,見圖 4836。
圖 4836 fromelf的幫助
我們在MDK輸入的指令格式是遵守fromelf幫助里的指令格式說明的,其格式為:
"fromelf [options] input_file"
其中optinos是指令選項,一個指令支持輸入多個選項,每個選項之間使用空格隔開,我們的實例中使用"--bin"選項設置輸出bin文件,使用"--output file"選項設置輸出文件的名字為"..\..\Output\多彩流水燈.bin",這個名字是一個相對路徑格式,如果不了解如何使用"..\"表示路徑,可使用MDK命令輸入框后面的文件夾圖標打開文件瀏覽器選擇文件,在命令的最后使用"..\..\Output\多彩流水燈.axf"作為命令的輸入文件。具體的格式分解見圖 4837。
圖 4837 fromelf命令格式分解
fromelf需要根據工程的*.axf文件輸入來轉換得到bin文件,所以在命令的輸入文件參數中要選擇本工程對應的*.axf文件,在MDK命令輸入欄中,我們把fromelf指令放置在"After Build/Rebuild"(工程構建完成后執行)一欄也是基於這個考慮,這樣設置后,工程構建完成生成了最新的*.axf文件,MDK再執行fromelf指令,從而得到最新的bin文件。
設置完成生成hex的選項或添加了生成bin的用戶指令后,點擊工程的編譯(build)按鈕,重新編譯工程,成功后可看到圖 4838中的輸出。打開相應的目錄即可找到文件,若找不到bin文件,請查看提示輸出欄執行指令的信息,根據信息改正fromelf指令。
圖 4838 fromelf生成hxe及bin文件的提示
其中bin文件是純二進制數據,無特殊格式,接下來我們了解一下hex文件格式。
hex文件格式
hex是Intel公司制定的一種使用ASCII文本記錄機器碼或常量數據的文件格式,這種文件常常用來記錄將要存儲到ROM中的數據,絕大多數下載器支持該格式。
一個hex文件由多條記錄組成,而每條記錄由五個部分組成,格式形如":llaaaatt[dd…]cc",例如本"多彩流水燈"工程生成的hex文件前幾條記錄見代碼清單 489。
代碼清單 489 Hex文件實例(多彩流水燈.hex文件,可直接用記事本打開)
1 :020000040800F2
2 :1000000000040020C10100081B030008A30200082F
3 :100010001903000809020008690400080000000034
4 :100020000000000000000000000000003D03000888
5 :100030000B020008000000001D0300081504000862
6 :10004000DB010008DB010008DB010008DB01000820
記錄的各個部分介紹如下:
":":每條記錄的開頭都使用冒號來表示一條記錄的開始;
ll:以16進制數表示這條記錄的主體數據區的長度(即后面[dd…]的長度);
aaaa:表示這條記錄中的內容應存放到FLASH中的起始地址;
tt:表示這條記錄的類型,它包含中的各種類型;
表 485 tt值所代表的類型說明
tt的值 |
代表的類型 |
00 |
數據記錄 |
01 |
本文件結束記錄 |
02 |
擴展地址記錄 |
04 |
擴展線性地址記錄(表示后面的記錄按個這地址遞增) |
05 |
表示一個線性地址記錄的起始(只適用於ARM) |
dd:表示一個字節的數據,一條記錄中可以有多個字節數據,ll區表示了它有多少個字節的數據;
cc:表示本條記錄的校驗和,它是前面所有16進制數據 (除冒號外,兩個為一組)的和對256取模運算的結果的補碼。
例如,代碼清單 489中的第一條記錄解釋如下:
(1) 02:表示這條記錄數據區的長度為2字節;
(2) 0000:表示這條記錄要存儲到的地址;
(3) 04:表示這是一條擴展線性地址記錄;
(4) 0800:由於這是一條擴展線性地址記錄,所以這部分表示地址的高16位,與前面的"0000"結合在一起,表示要擴展的線性地址為"0x0800 0000",這正好是STM32內部FLASH的首地址;
(5) F2:表示校驗和,它的值為(0x02+0x00+0x00+0x04+0x08+0x00)%256的值再取補碼。
再來看第二條記錄:
(1) 10:表示這條記錄數據區的長度為2字節;
(2) 0000:表示這條記錄所在的地址,與前面的擴展記錄結合,表示這條記錄要存儲的FLASH首地址為(0x0800 0000+0x0000);
(3) 00:表示這是一條數據記錄,數據區的是地址;
(4) 00040020C10100081B030008A3020008:這是要按地址存儲的數據;
(5) 2F:校驗和
為了更清楚地對比bin、hex及axf文件的差異,我們來查看這些文件內部記錄的信息來進行對比。
hex、bin及axf文件的區別與聯系
bin、hex及axf文件都包含了指令代碼,但它們的信息豐富程度是不一樣的。
bin文件是最直接的代碼映像,它記錄的內容就是要存儲到FLASH的二進制數據(機器碼本質上就是二進制數據),在FLASH中是什么形式它就是什么形式,沒有任何輔助信息,包括大小端格式也沒有,因此下載器需要有針對芯片FLASH平台的輔助文件才能正常下載(一般下載器程序會有匹配的這些信息);
hex文件是一種使用十六進制符號表示的代碼記錄,記錄了代碼應該存儲到FLASH的哪個地址,下載器可以根據這些信息輔助下載;
axf文件在前文已經解釋,它不僅包含代碼數據,還包含了工程的各種信息,因此它也是三個文件中最大的。
同一個工程生成的bin、hex及axf文件的大小見圖 4839。
圖 4839 同一個工程的bin、bex及axf文件大小
實際上,這個工程要燒寫到FLASH的內容總大小為1456字節,然而在Windows中查看的bin文件卻比它大( bin文件是FLASH的代碼映像,大小應一致),這是因為Windows文件顯示單位的原因,使用右鍵查看文件的屬性,可以查看它實際記錄內容的大小,見圖 4840。
圖 4840 bin文件大小
接下來我們打開本工程的"多彩流水燈.bin"、"多彩流水燈.hex"及由"多彩流水燈.axf"使用fromelf工具輸出的反匯編文件"多彩流水燈_axf_elfInfo_c.txt"文件,清晰地對比它們的差異,見圖 4841。如果您想要親自閱讀自己電腦上的bin文件,推薦使用sublime軟件打開,它可以把二進制數以ASCII碼呈現出來,便於閱讀。
圖 4841 同一個工程的bin、hex及axf文件對代碼的記錄
在"多彩流水燈_axf_elfInfo_c.txt"文件中不僅可以看到代碼數據,還有具體的標號、地址以及反匯編得到的代碼,雖然它不是*.axf文件的原始內容,但因為它是通過*.axf文件fromelf工具生成的,我們可認為*.axf文件本身記錄了大量這些信息,它的內容非常豐富,熟悉匯編語言的人可輕松閱讀。
在hex文件中包含了地址信息以及地址中的內容,而在bin文件中僅包含了內容,連存儲的地址信息都沒有。觀察可知,bin、hex及axf文件中的數據內容都是相同的,它們存儲的都是機器碼。這就是它們三都之間的區別與聯系。
由於文件中存儲的都是機器碼,見圖 4842,該圖是我根據axf文件的GPIO_Init函數的機器碼,在bin及hex中找到的對應位置。所以經驗豐富的人是有可能從bin或hex文件中恢復出匯編代碼的,只是成本較高,但不是不可能。
圖 4842 GPIO_Init函數的代碼數據在三個文件中的表示
如果芯片沒有做任何加密措施,使用下載器可以直接從芯片讀回它存儲在FLASH中的數據,從而得到bin映像文件,根據芯片型號還原出部分代碼即可進行修改,甚至不用修改代碼,直接根據目標產品的硬件PCB,抄出一樣的板子,再把bin映像下載芯片,直接山寨出目標產品,所以在實際的生產中,一定要注意做好加密措施。由於axf文件中含有大量的信息,且直接使用fromelf即可反匯編代碼,所以更不要隨便泄露axf文件。lib文件也能反使用fromelf文件反匯編代碼,不過它不能還原出C代碼,由於lib文件的主要目的是為了保護C源代碼,也算是達到了它的要求。
6. htm靜態調用圖文件
在Output目錄下,有一個以工程文件命名的后綴為*.bulid_log.htm及*.htm文件,如"多彩流水燈.bulid_log.htm"及"多彩流水燈.htm",它們都可以使用瀏覽器打開。其中*.build_log.htm是工程的構建過程日志,而*.htm是鏈接器生成的靜態調用圖文件。
在靜態調用圖文件中包含了整個工程各種函數之間互相調用的關系圖,而且它還給出了靜態占用最深的棧空間數量以及它對應的調用關系鏈。
例如圖 4843是"多彩流水燈.htm"文件頂部的說明。
圖 4843"多彩流水燈.htm"中的靜態占用最深的棧空間說明
該文件說明了本工程的靜態棧空間最大占用56字節(Maximum Stack Usage:56bytes),這個占用最深的靜態調用為"main->LED_GPIO_Config->GPIO_Init"。注意這里給出的空間只是靜態的棧使用統計,鏈接器無法統計動態使用情況,例如鏈接器無法知道遞歸函數的遞歸深度。在本文件的后面還可查詢到其它函數的調用情況及其它細節。
利用這些信息,我們可以大致了解工程中應該分配多少空間給棧,有空間余量的情況下,一般會設置比這個靜態最深棧使用量大一倍,在STM32中可修改啟動文件改變堆棧的大小;如果空間不足,可從該文件中了解到調用深度的信息,然后優化該代碼。
注意:
查看了各個工程的靜態調用圖文件統計后,我們發現本書提供的一些比較大規模的工程例子,靜態棧調用最大深度都已超出STM32啟動文件默認的棧空間大小0x00000400,即1024字節,但在當時的調試過程中卻沒有發現錯誤,因此我們也沒有修改棧的默認大小(有一些工程調試時已發現問題,它們的棧空間就已經被我們改大了),雖然這些工程實際運行並沒有錯誤,但這可能只是因為它使用的棧溢出RAM空間恰好沒被程序其它部分修改而已。所以,建議您在實際的大型工程應用中(特別是使用了各種外部庫時,如Lwip/emWin/Fatfs等),要查看本靜態調用圖文件,了解程序的棧使用情況,給程序分配合適的棧空間。
48.4.4 Listing目錄下的文件
在Listing目錄下包含了*.map及*.lst文件,它們都是文本格式的,可使用Windows的記事本軟件打開。其中lst文件僅包含了一些匯編符號的鏈接信息,我們重點分析map文件。
1. map文件說明
map文件是由鏈接器生成的,它主要包含交叉鏈接信息,查看該文件可以了解工程中各種符號之間的引用以及整個工程的Code、RO-data、RW-data以及ZI-data的詳細及匯總信息。它的內容中主要包含了"節區的跨文件引用"、"刪除無用節區"、"符號映像表"、"存儲器映像索引"以及"映像組件大小",各部分介紹如下:
節區的跨文件引用
打開"多彩流水燈.map"文件,可看到它的第一部分——節區的跨文件引用(Section Cross References),見代碼清單 4810。
代碼清單 4810 節區的跨文件引用(部分,多彩流水燈.map文件)
1 ==============================================================================
2 Section Cross References
3
4 startup_stm32f429_439xx.o(RESET) refers to startup_stm32f429_439xx.o(STACK) for __initial_sp
5 startup_stm32f429_439xx.o(RESET) refers to startup_stm32f429_439xx.o(.text) for Reset_Handler
6 startup_stm32f429_439xx.o(RESET) refers to stm32f4xx_it.o(i.NMI_Handler) for NMI_Handler
7 startup_stm32f429_439xx.o(RESET) refers to stm32f4xx_it.o(i.HardFault_Handler) for HardFault_Handler
8 /**...以下部分省略****/
9
10 main.o(i.main) refers to bsp_led.o(i.LED_GPIO_Config) for LED_GPIO_Config
11 main.o(i.main) refers to stm32f4xx_gpio.o(i.GPIO_ResetBits) for GPIO_ResetBits
12 main.o(i.main) refers to main.o(i.Delay) for Delay
13 main.o(i.main) refers to stm32f4xx_gpio.o(i.GPIO_SetBits) for GPIO_SetBits
14 bsp_led.o(i.LED_GPIO_Config) refers to stm32f4xx_rcc.o(i.RCC_AHB1PeriphClockCmd) for RCC_AHB1PeriphClockCmd
15 bsp_led.o(i.LED_GPIO_Config) refers to stm32f4xx_gpio.o(i.GPIO_Init) for GPIO_Init
16 bsp_led.o(i.LED_GPIO_Config) refers to stm32f4xx_gpio.o(i.GPIO_ResetBits) for GPIO_ResetBits
17 /**...以下部分省略****/
18 ======================================================================
19
在這部分中,詳細列出了各個*.o文件之間的符號引用。由於*.o文件是由asm或c/c++源文件編譯后生成的,各個文件及文件內的節區間互相獨立,鏈接器根據它們之間的互相引用鏈接起來,鏈接的詳細信息在這個"Section Cross References"一一列出。
例如,開頭部分說明的是startup_stm32f429_439xx.o文件中的"RESET"節區分為它使用的"__initial_sp"符號引用了同文件"STACK"節區。
也許我們對啟動文件不熟悉,不清楚這究竟是什么,那我們繼續瀏覽,可看到main.o文件的引用說明,如說明main.o文件的i.main節區為它使用的LED_GPIO_Config符號引用了bsp_led.o文件的i.LED_GPIO_Config節區。
同樣地,下面還有bsp_led.o文件的引用說明,如說明了bsp_led.o文件的i.LED_GPIO_Config節區為它使用的GPIO_Init符號引用了stm32f4xx_gpio.o文件的i.GPIO_Init節區。
可以了解到,這些跨文件引用的符號其實就是源文件中的函數名、變量名。有時在構建工程的時候,編譯器會輸出"Undefined symbol xxx (referred from xxx.o)"這樣的提示,該提示的原因就是在鏈接過程中,某個文件無法在外部找到它引用的標號,因而產生鏈接錯誤。例如,見圖 4844,我們把bsp_led.c文件中定義的函數LED_GPIO_Config改名為LED_GPIO_ConfigABCD,而不修改main.c文件中的調用,就會出現main文件無法找到LED_GPIO_Config符號的提示。
圖 4844 找不到符號的錯誤提示
刪除無用節區
map文件的第二部分是刪除無用節區的說明(Removing Unused input sections from the image.),見代碼清單 4811。
代碼清單 4811 刪除無用節區(部分,多彩流水燈.map文件)
1 =================================================================
2 Removing Unused input sections from the image.
3
4 Removing startup_stm32f429_439xx.o(HEAP), (512 bytes).
5 Removing system_stm32f4xx.o(.rev16_text), (4 bytes).
6 Removing system_stm32f4xx.o(.revsh_text), (4 bytes).
7 Removing system_stm32f4xx.o(.rrx_text), (6 bytes).
8 Removing system_stm32f4xx.o(i.SystemCoreClockUpdate), (136 bytes).
9 Removing system_stm32f4xx.o(.data), (20 bytes).
10 Removing misc.o(.rev16_text), (4 bytes).
11 Removing misc.o(.revsh_text), (4 bytes).
12 Removing misc.o(.rrx_text), (6 bytes).
13 Removing misc.o(i.NVIC_Init), (104 bytes).
14 Removing misc.o(i.NVIC_PriorityGroupConfig), (20 bytes).
15 Removing misc.o(i.NVIC_SetVectorTable), (20 bytes).
16 Removing misc.o(i.NVIC_SystemLPConfig), (28 bytes).
17 Removing misc.o(i.SysTick_CLKSourceConfig), (28 bytes).
18 Removing stm32f4xx_adc.o(.rev16_text), (4 bytes).
19 Removing stm32f4xx_adc.o(.revsh_text), (4 bytes).
20 Removing stm32f4xx_adc.o(.rrx_text), (6 bytes).
21 Removing stm32f4xx_adc.o(i.ADC_AnalogWatchdogCmd), (16 bytes).
22 Removing stm32f4xx_adc.o(i.ADC_AnalogWatchdogSingleChannelConfig), (12 bytes).
23 Removing stm32f4xx_adc.o(i.ADC_AnalogWatchdogThresholdsConfig), (6 bytes).
24 Removing stm32f4xx_adc.o(i.ADC_AutoInjectedConvCmd), (24 bytes).
25 /**...以下部分省略****/
26 ========================================================================
這部分列出了在鏈接過程它發現工程中未被引用的節區,這些未被引用的節區將會被刪除(指不加入到*.axf文件,不是指在*.o文件刪除),這樣可以防止這些無用數據占用程序空間。
例如,上面的信息中說明startup_stm32f429_439xx.o中的HEAP(在啟動文件中定義的用於動態分配的"堆"區)以及 stm32f4xx_adc.o的各個節區都被刪除了,因為在我們這個工程中沒有使用動態內存分配,也沒有引用任何stm32f4xx_adc.c中的內容。由此也可以知道,雖然我們把STM32標准庫的各個外設對應的c庫文件都添加到了工程,但不必擔心這會使工程變得臃腫,因為未被引用的節區內容不會被加入到最終的機器碼文件中。
符號映像表
map文件的第三部分是符號映像表(Image Symbol Table),見代碼清單 4812。
代碼清單 4812 符號映像表(部分,多彩流水燈.map文件)
1 ==============================================================================
2 Image Symbol Table
3
4 Local Symbols
5
6 Symbol Name Value Ov Type Size Object(Section)
7 ../clib/microlib/init/entry.s 0x00000000 Number 0 entry.o ABSOLUTE
8 ../clib/microlib/init/entry.s 0x00000000 Number 0 entry9a.o ABSOLUTE
9 ../clib/microlib/init/entry.s 0x00000000 Number 0 entry9b.o ABSOLUTE
10 /*...省略部分*/
11 LED_GPIO_Config 0x080002a5 Thumb Code 106 bsp_led.o(i.LED_GPIO_Config)
12 MemManage_Handler 0x08000319 Thumb Code 2 stm32f4xx_it.o(i.MemManage_Handler)
13 NMI_Handler 0x0800031b Thumb Code 2 stm32f4xx_it.o(i.NMI_Handler)
14 PendSV_Handler 0x0800031d Thumb Code 2 stm32f4xx_it.o(i.PendSV_Handler)
15 RCC_AHB1PeriphClockCmd 0x08000321 Thumb Code 22 stm32f4xx_rcc.o(i.RCC_AHB1PeriphClockCmd)
16 SVC_Handler 0x0800033d Thumb Code 2 stm32f4xx_it.o(i.SVC_Handler)
17 SysTick_Handler 0x08000415 Thumb Code 2 stm32f4xx_it.o(i.SysTick_Handler)
18 SystemInit 0x08000419 Thumb Code 62 system_stm32f4xx.o(i.SystemInit)
19 UsageFault_Handler 0x08000469 Thumb Code 2 stm32f4xx_it.o(i.UsageFault_Handler)
20 __scatterload_copy 0x0800046b Thumb Code 14 handlers.o(i.__scatterload_copy)
21 __scatterload_null 0x08000479 Thumb Code 2 handlers.o(i.__scatterload_null)
22 __scatterload_zeroinit 0x0800047b Thumb Code 14 handlers.o(i.__scatterload_zeroinit)
23 main 0x08000489 Thumb Code 270 main.o(i.main)
24 /*...省略部分*/
25 ==============================================================================
這個表列出了被引用的各個符號在存儲器中的具體地址、占據的空間大小等信息。如我們可以查到LED_GPIO_Config符號存儲在0x080002a5地址,它屬於Thumb Code類型,大小為106字節,它所在的節區為bsp_led.o文件的i.LED_GPIO_Config節區。
存儲器映像索引
map文件的第四部分是存儲器映像索引(Memory Map of the image),見代碼清單 4813。
代碼清單 4813 存儲器映像索引(部分,多彩流水燈.map文件)
1 ==============================================================================
2 Memory Map of the image
3
4 Image Entry point : 0x080001ad
5 Load Region LR_IROM1 (Base: 0x08000000, Size: 0x000005b0, Max: 0x00100000, ABSOLUTE)
6
7 Execution Region ER_IROM1 (Base: 0x08000000, Size: 0x000005b0, Max: 0x00100000, ABSOLUTE)
8
9 Base Addr Size Type Attr Idx E Section Name Object
10
11 0x08000000 0x000001ac Data RO 3 RESET startup_stm32f429_439xx.o
12 /*..省略部分*/
13 0x0800020c 0x00000012 Code RO 5161 i.Delay main.o
14 0x0800021e 0x0000007c Code RO 2046 i.GPIO_Init stm32f4xx_gpio.o
15 0x0800029a 0x00000004 Code RO 2053 i.GPIO_ResetBits stm32f4xx_gpio.o
16 0x0800029e 0x00000004 Code RO 2054 i.GPIO_SetBits stm32f4xx_gpio.o
17 0x080002a2 0x00000002 Code RO 5196 i.HardFault_Handler stm32f4xx_it.o
18 0x080002a4 0x00000074 Code RO 5269 i.LED_GPIO_Config bsp_led.o
19 0x08000318 0x00000002 Code RO 5197 i.MemManage_Handler stm32f4xx_it.o
20 /*..省略部分*/
21 0x08000488 0x00000118 Code RO 5162 i.main main.o
22 0x080005a0 0x00000010 Data RO 5309 Region$$Table anon$$obj.o
23
24 Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x00000400, Max: 0x00030000, ABSOLUTE)
25
26 Base Addr Size Type Attr Idx E Section Name Object
27
28 0x20000000 0x00000400 Zero RW 1 STACK startup_stm32f429_439xx.o
29 ==============================================================================
本工程的存儲器映像索引分為ER_IROM1及RW_IRAM1部分,它們分別對應STM32內部FLASH及SRAM的空間。相對於符號映像表,這個索引表描述的單位是節區,而且它描述的主要信息中包含了節區的類型及屬性,由此可以區分Code、RO-data、RW-data及ZI-data。
例如,從上面的表中我們可以看到i.LED_GPIO_Config節區存儲在內部FLASH的0x080002a4地址,大小為0x00000074,類型為Code,屬性為RO。而程序的STACK節區(棧空間)存儲在SRAM的0x20000000地址,大小為0x00000400,類型為Zero,屬性為RW(即RW-data)。
映像組件大小
map文件的最后一部分是包含映像組件大小的信息(Image component sizes),這也是最常查詢的內容,見代碼清單 4814。
代碼清單 4814 映像組件大小(部分,多彩流水燈.map文件)
1 ==============================================================================
2 Image component sizes
3
4 Code (inc. data) RO Data RW Data ZI Data Debug Object Name
5
6 116 10 0 0 0 578 bsp_led.o
7 298 10 0 0 0 1459 main.o
8 36 8 428 0 1024 932 startup_stm32f429_439xx.o
9 132 0 0 0 0 2432 stm32f4xx_gpio.o
10 18 0 0 0 0 3946 stm32f4xx_it.o
11 28 6 0 0 0 645 stm32f4xx_rcc.o
12 292 34 0 0 0 253101 system_stm32f4xx.o
13
14 ----------------------------------------------------------------------
15 926 68 444 0 1024 263093 Object Totals
16 0 0 16 0 0 0 (incl. Generated)
17 6 0 0 0 0 0 (incl. Padding)
18
19 /*...省略部分*/
20 ==============================================================================
21 Code (inc. data) RO Data RW Data ZI Data Debug
22
23 1012 84 444 0 1024 262637 Grand Totals
241012 84 444 0 1024 262637 ELF Image Totals
25 1012 84 444 0 0 0 ROM Totals
26 ==============================================================================
27 Total RO Size (Code + RO Data) 1456 ( 1.42kB)
28 Total RW Size (RW Data + ZI Data) 1024 ( 1.00kB)
29 Total ROM Size (Code + RO Data + RW Data) 1456 ( 1.42kB)
30 ==============================================================================
這部分包含了各個使用到的*.o文件的空間匯總信息、整個工程的空間匯總信息以及占用不同類型存儲器的空間匯總信息,它們分類描述了具體占據的Code、RO-data、RW-data及ZI-data的大小,並根據這些大小統計出占據的ROM總空間。
我們僅分析最后兩部分信息,如Grand Totals一項,它表示整個代碼占據的所有空間信息,其中Code類型的數據大小為1012字節,這部分包含了84字節的指令數據(inc .data)已算在內,另外RO-data占444字節,RW-data占0字節,ZI-data占1024字節。在它的下面兩行有一項ROM Totals信息,它列出了各個段所占據的ROM空間,除了ZI-data不占ROM空間外,其余項都與Grand Totals中相等(RW-data也占據ROM空間,只是本工程中沒有RW-data類型的數據而已)。
最后一部分列出了只讀數據(RO)、可讀寫數據(RW)及占據的ROM大小。其中只讀數據大小為1456字節,它包含Code段及RO-data段; 可讀寫數據大小為1024字節,它包含RW-data及ZI-data段;占據的ROM大小為1456字節,它除了Code段和RO-data段,還包含了運行時需要從ROM加載到RAM的RW-data數據。
綜合整個map文件的信息,可以分析出,當程序下載到STM32的內部FLASH時,需要使用的內部FLASH是從0x0800 0000地址開始的大小為1456字節的空間;當程序運行時,需要使用的內部SRAM是從0x20000000地址開始的大小為1024字節的空間。
粗略一看,發現這個小程序竟然需要1024字節的SRAM,實在說不過去,但仔細分析map文件后,可了解到這1024字節都是STACK節區的空間(即棧空間),棧空間大小是在啟動文件中定義的,這1024字節是默認值(0x00000400)。它是提供給C語言程序局部變量申請使用的空間,若我們確認自己的應用程序不需要這么大的棧,完全可以修改啟動文件,把它改小一點,查看前面講解的htm靜態調用圖文件可了解靜態的棧調用情況,可以用它作為參考。
48.4.5 sct分散加載文件的格式與應用
1. sct分散加載文件簡介
當工程按默認配置構建時,MDK會根據我們選擇的芯片型號,獲知芯片的內部FLASH及內部SRAM存儲器概況,生成一個以工程名命名的后綴為*.sct的分散加載文件(Linker Control File,scatter loading),鏈接器根據該文件的配置分配各個節區地址,生成分散加載代碼,因此我們通過修改該文件可以定制具體節區的存儲位置。
例如可以設置源文件中定義的所有變量自動按地址分配到外部SDRAM,這樣就不需要再使用關鍵字"__attribute__"按具體地址來指定了;利用它還可以控制代碼的加載區與執行區的位置,例如可以把程序代碼存儲到單位容量價格便宜的NAND-FLASH中,但在NAND-FLASH中的代碼是不能像內部FLASH的代碼那樣直接提供給內核運行的,這時可通過修改分散加載文件,把代碼加載區設定為NAND-FLASH的程序位置,而程序的執行區設定為SDRAM中的位置,這樣鏈接器就會生成一個配套的分散加載代碼,該代碼會把NAND-FLASH中的代碼加載到SDRAM中,內核再從SDRAM中運行主體代碼,大部分運行Linux系統的代碼都是這樣加載的。
2. 分散加載文件的格式
下面先來看看MDK默認使用的sct文件,在Output目錄下可找到"多彩流水燈.sct",該文件記錄的內容見代碼清單 4815。
代碼清單 4815 默認的分散加載文件內容("多彩流水燈.sct")
1 ; *************************************************************
2 ; *** Scatter-Loading Description File generated by uVision ***
3 ; *************************************************************
4
5 LR_IROM1 0x08000000 0x00100000 { ; 注釋:加載域,基地址空間大小
6 ER_IROM1 0x08000000 0x00100000 { ; 注釋:加載地址 = 執行地址
7 *.o (RESET, +First)
8 *(InRoot$$Sections)
9 .ANY (+RO)
10 }
11 RW_IRAM1 0x20000000 0x00030000 { ; 注釋:可讀寫數據
12 .ANY (+RW +ZI)
13 }
14 }
15
在默認的sct文件配置中僅分配了Code、RO-data、RW-data及ZI-data這些大區域的地址,鏈接時各個節區(函數、變量等)直接根據屬性排列到具體的地址空間。
sct文件中主要包含描述加載域及執行域的部分,一個文件中可包含有多個加載域,而一個加載域可由多個部分的執行域組成。同等級的域之間使用花括號"{}"分隔開,最外層的是加載域,第二層"{}"內的是執行域,其整體結構見圖 4845。
圖 4845 分散加載文件的整體結構
加載域
sct文件的加載域格式見代碼清單 4816。
代碼清單 4816 加載域格式
1 //方括號中的為選填內容
2 加載域名 (基地址 | ("+"地址偏移)) [屬性列表] [最大容量]
3 "{"
4 執行區域描述+
5 "}"
配合前面代碼清單 4815中的分散加載文件內容,各部分介紹如下:
加載域名:名稱,在map文件中的描述會使用該名稱來標識空間。如本例中只有一個加載域,該域名為LR_IROM1。
基地址+地址偏移:這部分說明了本加載域的基地址,可以使用+號連接一個地址偏移,算進基地址中,整個加載域以它們的結果為基地址。如本例中的加載域基地址為0x08000000,剛好是STM32內部FLASH的基地址。
屬性列表:屬性列表說明了加載域的是否為絕對地址、N字節對齊等屬性,該配置是可選的。本例中沒有描述加載域的屬性。
最大容量:最大容量說明了這個加載域可使用的最大空間,該配置也是可選的,如果加上這個配置后,當鏈接器發現工程要分配到該區域的空間比容量還大,它會在工程構建過程給出提示。本例中的加載域最大容量為0x00100000,即1MB,正是本型號STM32內部FLASH的空間大小。
執行域
sct文件的執行域格式見代碼清單 4817。
代碼清單 4817 執行域格式
1 //方括號中的為選填內容
2 執行域名 (基地址 | "+"地址偏移) [屬性列表] [最大容量 ]
3 "{"
4 輸入節區描述
5 "}"
執行域的格式與加載域是類似的,區別只是輸入節區的描述有所不同,在代碼清單 4815的例子中包含了ER_IROM1及RW_IRAM兩個執行域,它們分別對應描述了STM32的內部FLASH及內部SRAM的基地址及空間大小。而它們內部的"輸入節區描述"說明了哪些節區要存儲到這些空間,鏈接器會根據它來處理編排這些節區。
輸入節區描述
配合加載域及執行域的配置,在相應的域配置"輸入節區描述"即可控制該節區存儲到域中,其格式見代碼清單 4818。
代碼清單 4818 輸入節區描述的幾種格式
1 //除模塊選擇樣式部分外,其余部分都可選選填
2 模塊選擇樣式"("輸入節區樣式",""+"輸入節區屬性")"
3 模塊選擇樣式"("輸入節區樣式",""+"節區特性")"
4
5 模塊選擇樣式"("輸入符號樣式",""+"節區特性")"
6 模塊選擇樣式"("輸入符號樣式",""+"輸入節區屬性")"
配合前面代碼清單 4815中的分散加載文件內容,各部分介紹如下:
模塊選擇樣式:模塊選擇樣式可用於選擇o及lib目標文件作為輸入節區,它可以直接使用目標文件名或"*"通配符,也可以使用".ANY"。例如,使用語句"bsp_led.o"可以選擇bsp_led.o文件,使用語句"*.o"可以選擇所有o文件,使用"*.lib"可以選擇所有lib文件,使用"*"或".ANY"可以選擇所有的o文件及lib文件。其中".ANY"選擇語句的優先級是最低的,所有其它選擇語句選擇完剩下的數據才會被".ANY"語句選中。
輸入節區樣式:我們知道在目標文件中會包含多個節區或符號,通過輸入節區樣式可以選擇要控制的節區。
示例文件中"(RESET,+First)"語句的RESET就是輸入節區樣式,它選擇了名為RESET的節區,並使用后面介紹的節區特性控制字"+First"表示它要存儲到本區域的第一個地址。示例文件中的"*(InRoot$$Sections)"是一個鏈接器支持的特殊選擇符號,它可以選擇所有標准庫里要求存儲到root區域的節區,如__main.o、__scatter*.o等內容。
輸入符號樣式:同樣地,使用輸入符號樣式可以選擇要控制的符號,符號樣式需要使用":gdef:"來修飾。例如可以使用"*(:gdef:Value_Test)"來控制選擇符號"Value_Test"。
輸入節區屬性:通過在模塊選擇樣式后面加入輸入節區屬性,可以選擇樣式中不同的內容,每個節區屬性描述符前要寫一個"+"號,使用空格或","號分隔開,可以使用的節區屬性描述符見表 486。
表 486 屬性描述符及其意義
節區屬性描述符 |
說明 |
RO-CODE及CODE |
只讀代碼段 |
RO-DATA及CONST |
只讀數據段 |
RO及TEXT |
包括RO-CODE及RO-DATA |
RW-DATA |
可讀寫數據段 |
RW-CODE |
可讀寫代碼段 |
RW及DATA |
包括RW-DATA及RW-CODE |
ZI及BSS |
初始化為0的可讀寫數據段 |
XO |
只可執行的區域 |
ENTRY |
節區的入口點 |
例如,示例文件中使用".ANY(+RO)"選擇剩余所有節區RO屬性的內容都分配到執行域ER_IROM1中,使用".ANY(+RW +ZI)"選擇剩余所有節區RW及ZI屬性的內容都分配到執行域RW_IRAM1中。
節區特性:節區特性可以使用"+FIRST"或"+LAST"選項配置它要存儲到的位置,FIRST存儲到區域的頭部,LAST存儲到尾部。通常重要的節區會放在頭部,而CheckSum(校驗和)之類的數據會放在尾部。
例如示例文件中使用"(RESET,+First)"選擇了RESET節區,並要求把它放置到本區域第一個位置,而RESET是工程啟動代碼中定義的向量表,見代碼清單 4819,該向量表中定義的堆棧頂和復位向量指針必須要存儲在內部FLASH的前兩個地址,這樣STM32才能正常啟動,所以必須使用FIRST控制它們存儲到首地址。
代碼清單 4819 startup_stm32f429_439xx.s文件中定義的RESET區(部分)
1 ; Vector Table Mapped to Address 0 at Reset
2 AREA RESET, DATA, READONLY
3 EXPORT __Vectors
4 EXPORT __Vectors_End
5 EXPORT __Vectors_Size
6
7 __Vectors DCD __initial_sp ; Top of Stack
8 DCD Reset_Handler ; Reset Handler
9 DCD NMI_Handler ; NMI Handler
總的來說,我們的sct示例文件配置如下:程序的加載域為內部FLASH的0x08000000,最大空間為0x00100000;程序的執行基地址與加載基地址相同,其中RESET節區定義的向量表要存儲在內部FLASH的首地址,且所有o文件及lib文件的RO屬性內容都存儲在內部FLASH中;程序執行時RW及ZI區域都存儲在以0x20000000為基地址,大小為0x00030000的空間(192KB),這部分正好是STM32內部主SRAM的大小。
鏈接器根據sct文件鏈接,鏈接后各個節區、符號的具體地址信息可以在map文件中查看。
3. 通過MDK配置選項來修改sct文件
了解sct文件的格式后,可以手動編輯該文件控制整個工程的分散加載配置,但sct文件格式比較復雜,所以MDK提供了相應的配置選項可以方便地修改該文件,這些選項配置能滿足基本的使用需求,本小節將對這些選項進行說明。
選擇sct文件的產生方式
首先需要選擇sct文件產生的方式,選擇使用MDK生成還是使用用戶自定義的sct文件。在MDK的"Options for Target->Linker->Use Memory Layout from Target Dialog"選項即可配置該選擇,見圖 4846。
圖 4846 選擇使用MDK生成的sct文件
該選項的譯文為"是否使用Target對話框中的存儲器分布配置",勾選后,它會根據"Options for Target"對話框中的選項生成sct文件,這種情況下,即使我們手動打開它生成的sct文件編輯也是無效的,因為每次構建工程的時候,MDK都會生成新的sct文件覆蓋舊文件。該選項在MDK中是默認勾選的,若希望MDK使用我們手動編輯的sct文件構建工程,需要取消勾選,並通過Scatter File框中指定sct文件的路徑,見圖 4847。
圖 4847 使用指定的sct文件構建工程
通過Target對話框控制存儲器分配
若我們在Linker中勾選了"使用Target對話框的存儲器布局"選項,那么"Options for Target"對話框中的存儲器配置就生效了。主要配置是在Device標簽頁中選擇芯片的類型,設定芯片基本的內部存儲器信息以及在Target標簽頁中細化具體的存儲器配置(包括外部存儲器),見圖 4848及圖 4849。
圖 4848 選擇芯片類型
圖中Device標簽頁中選定了芯片的型號為STM32F429IGTx,選中后,在Target標簽頁中的存儲器信息會根據芯片更新。
圖 4849 Target對話框中的存儲器分配
在Target標簽頁中存儲器信息分成只讀存儲器(Read/Only Memory Areas)和可讀寫存儲器(Read/Write Memory Areas)兩類,即ROM和RAM,而且它們又細分成了片外存儲器(off-chip)和片內存儲器(on-chip)兩類。
例如,由於我們已經選定了芯片的型號,MDK會自動根據芯片型號填充片內的ROM及RAM信息,其中的IROM1起始地址為0x80000000,大小為0x100000,正是該STM32型號的內部FLASH地址及大小;而IRAM1起始地址為0x20000000,大小為0x30000,正是該STM32內部主SRAM的地址及大小。圖中的IROM1及IRAM1前面都打上了勾,表示這個配置信息會被采用,若取消勾選,則該存儲配置信息是不會被使用的。
在標簽頁中的IRAM2一欄默認也填寫了配置信息,它的地址為0x10000000,大小為0x10000,這是STM32F4系列特有的內部64KB高速SRAM(被稱為CCM)。當我們希望使用這部分存儲空間的時候需要勾選該配置,另外要注意這部分高速SRAM僅支持CPU總線的訪問,不能通過外設訪問。
下面我們嘗試修改Target標簽頁中的這些存儲信息,例如,按照圖 4850中的1配置,把IRAM1的基地址改為0x20001000,然后編譯工程,查看到工程的sct文件如代碼清單 4820所示;當按照圖 4850中的2配置時,同時使用IRAM1和IRAM2,然后編譯工程,可查看到工程的sct文件如代碼清單 4821所示。
圖 4850 修改IRAM1的基地址及僅使用IRAM2的配置
代碼清單 4820 修改了IRAM1基地址后的sct文件內容
1 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
2 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
3 *.o (RESET, +First)
4 *(InRoot$$Sections)
5 .ANY (+RO)
6 }
7 RW_IRAM1 0x20001000 0x00030000 { ; RW data
8 .ANY (+RW +ZI)
9 }
10 }
代碼清單 4821 僅使用IRAM2時的sct文件內容
1 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
2 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
3 *.o (RESET, +First)
4 *(InRoot$$Sections)
5 .ANY (+RO)
6 }
7 RW_IRAM1 0x20000000 0x00030000 { ; RW data
8 .ANY (+RW +ZI)
9 }
10 RW_IRAM2 0x10000000 0x00010000 {
11 .ANY (+RW +ZI)
12 }
13 }
可以發現,sct文件都根據Target標簽頁做出了相應的改變,除了這種修改外,在Target標簽頁上還控制同時使用IRAM1和IRAM2、加入外部RAM(如外接的SDRAM),外部FLASH等。
控制文件分配到指定的存儲空間
設定好存儲器的信息后,可以控制各個源文件定制到哪個部分存儲器,在MDK的工程文件欄中,選中要配置的文件,右鍵,並在彈出的菜單中選擇"Options for File xxxx"即可彈出一個文件配置對話框,在該對話框中進行存儲器定制,見圖 4851。
圖 4851 使用右鍵打開文件配置並把它的RW區配置成使用IRAM2
在彈出的對話框中有一個"Memory Assignment"區域(存儲器分配),在該區域中可以針對文件的各種屬性內容進行分配,如Code/Const內容(RO)、Zero Initialized Data內容(ZI-data)以及Other Data內容(RW-data),點擊下拉菜單可以找到在前面Target頁面配置的IROM1、IRAM1、IRAM2等存儲器。例如圖中我們把這個bsp_led.c文件的Other Data屬性的內容分配到了IRAM2存儲器(在Target標簽頁中我們勾選了IRAM1及IRAM2),當在bsp_led.c文件定義了一些RW-data內容時(如初值非0的全局變量),該變量將會被分配到IRAM2空間,配置完成后點擊OK,然后編譯工程,查看到的sct文件內容見代碼清單 4822。
代碼清單 4822 修改bsp_led.c配置后的sct文件
1 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
2 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
3 *.o (RESET, +First)
4 *(InRoot$$Sections)
5 .ANY (+RO)
6 }
7 RW_IRAM1 0x20000000 0x00030000 { ; RW data
8 .ANY (+RW +ZI)
9 }
10 RW_IRAM2 0x10000000 0x00010000 {
11 bsp_led.o (+RW)
12 .ANY (+RW +ZI)
13 }
14 }
可以看到在sct文件中的RW_IRAM2執行域中增加了一個選擇bsp_led.o中RW內容的語句。
類似地,我們還可以設置某些文件的代碼段被存儲到特定的ROM中,或者設置某些文件使用的ZI-data或RW-data存儲到外部SDRAM中(控制ZI-data到SDRAM時注意還需要修改啟動文件設置堆棧對應的地址,原啟動文件中的地址是指向內部SRAM的)。
雖然MDK的這些存儲器配置選項很方便,但有很多高級的配置還是需要手動編寫sct文件實現的,例如MDK選項中的內部ROM選項最多只可以填充兩個選項位置,若想把內部ROM分成多片地址管理就無法實現了;另外MDK配置可控的最小粒度為文件,若想控制特定的節區也需要直接編輯sct文件。
接下來我們將講解幾個實驗,通過編寫sct文件定制存儲空間。
48.5 實驗:自動分配變量到外部SDRAM空間
由於內存管理對應用程序非常重要,若修改sct文件,不使用默認配置,對工程影響非常大,容易導致出錯,所以我們使用兩個實驗配置來講解sct文件的應用細節,希望您學習后不僅知其然而且知其所以然,清楚地了解修改后對應用程序的影響,還可以舉一反三根據自己的需求進行存儲器定制。
在本書前面的SDRAM實驗中,當我們需要讀寫SDRAM存儲的內容時,需要使用指針或者__attribute__((at(具體地址)))來指定變量的位置,當有多個這樣的變量時,就需要手動計算地址空間了,非常麻煩。在本實驗中我們將修改sct文件,讓鏈接器自動分配全局變量到SDRAM的地址並進行管理,使得利用SDRAM的空間就跟內部SRAM一樣簡單。
48.5.1 硬件設計
本小節中使用到的硬件跟"擴展外部SDRAM"實驗中的一致,若不了解,請參考該章節的原理圖說明。
48.5.2 軟件設計
本小節中提供的例程名為"SCT文件應用—自動分配變量到SDRAM",學習時請打開該工程來理解,該工程是基於"擴展外部SDRAM"實驗改寫而來的。
為方便講解,本實驗直接使用手動編寫的sct文件,所以在MDK的"Options for Target->Linker->Use Memory Layout from Target Dialog"選項被取消勾選,取消勾選后可直接點擊"Edit"按鈕編輯工程的sct文件,也可到工程目錄下打開編輯,見圖 4852。
圖 4852 使用手動編寫的sct文件
取消了這個勾選后,在MDK的Target對話框及文件配置的存儲器分布選項都會失效,僅以sct文件中的為准,更改對話框及文件配置選項都不會影響sct文件的內容。
1. 編程要點
(6) 修改啟動文件,在__main執行之前初始化SDRAM;
(7) 在sct文件中增加外部SDRAM空間對應的執行域;
(8) 使用節區選擇語句選擇要分配到SDRAM的內容;
(9) 編寫測試程序,編譯正常后,查看map文件的空間分配情況。
2. 代碼分析
在__main之前初始化SDRAM
在前面講解ELF文件格式的小節中我們了解到,芯片啟動后,會通過__main函數調用分散加載代碼__scatterload,分散加載代碼會把存儲在FLASH中的RW-data復制到RAM中,然后在RAM區開辟一塊ZI-data的空間,並將其初始化為0值。因此,為了保證在程序中定義到SDRAM中的變量能被正常初始化,我們需要在系統執行分散加載代碼之前使SDRAM存儲器正常運轉,使它能夠正常保存數據。
在本來的"擴展外部SDRAM"工程中,我們使用SDRAM_Init函數初始化SDRAM,且該函數在main函數里才被調用,所以在SDRAM正常運轉之前,分散加載過程復制到SDRAM中的數據都丟失了,因而需要在初始化SDRAM之后,需要重新給變量賦值才能正常使用(即定義變量時的初值無效,在調用SDRAM_Init函數之后的賦值才有效)。
為了解決這個問題,可修改工程的startup_stm32f429_439xx.s啟動文件,見代碼清單 4823。
代碼清單 4823 修改啟動文件中的Reset_handler函數(startup_stm32f429_439xx.s文件)
1 ; Reset handler
2 Reset_Handler PROC
3 EXPORT Reset_Handler [WEAK]
4 IMPORT SystemInit
5 IMPORT __main
6
7 ;從外部文件引入聲明
8 IMPORT SDRAM_Init
9
10 LDR R0, =SystemInit
11 BLX R0
12
13 ;在__main之前調用SDRAM_Init進行初始化
14 LDR R0, =SDRAM_Init
15 BLX R0
16
17 LDR R0, =__main
18 BX R0
19 ENDP
在原來的啟動文件中我們增加了上述加粗表示的代碼,增加的代碼中使用到匯編語法的IMPOR引入在bsp_sdram.c文件中定義的SDRAM_Init函數,接着使用LDR指令加載函數的代碼地址到寄存器R0,最后使用BLX R0指令跳轉到SDRAM_Init的代碼地址執行。
以上代碼實現了Reset_handler在執行__main函數前先調用了我們自定義的SDRAM_Init函數,從而為分散加載代碼准備好正常的硬件工作環境。
sct文件初步應用
接下來修改sct文件,控制使得在C源文件中定義的全局變量都自動由鏈接器分配到外部SDRAM中,見代碼清單 4824。
代碼清單 4824 配置sct文件(SDRAM.sct文件)
1 ; *************************************************************
2 ; *** Scatter-Loading Description File generated by uVision ***
3 ; *************************************************************
4
5 LR_IROM1 0x08000000 0x00100000 { ; 加載域
6 ER_IROM1 0x08000000 0x00100000 { ; 加載地址 = 執行地址
7 *.o (RESET, +First)
8 *(InRoot$$Sections)
9 .ANY (+RO)
10 }
11
12
13 RW_IRAM1 0x20000000 0x00030000 { ; 內部SRAM
14 *.o(STACK) ;選擇STACK節區,棧
15 stm32f4xx_rcc.o(+RW) ;選擇stm32f4xx_rcc的RW內容
16 .ANY (+RW +ZI) ;其余的RW/ZI-data都分配到這里
17 }
18
19 RW_ERAM1 0xD0000000 0x00800000 { ; 外部SDRAM
20
21 .ANY (+RW +ZI) ;其余的RW/ZI-data都分配到這里
22 }
23 }
加粗部分是本例子中增加的代碼,我們從后面開始,先分析比較簡單的SDRAM執行域部分。
RW_ERAM1 0xD0000000 0x00800000{}
RW_ERAM1是我們配置的SDRAM執行域。該執行域的名字是可以隨便取的,最重要的是它的基地址及空間大小,這兩個值與我們實驗板配置的SDRAM基地址及空間大小一致,所以該執行域會被映射到SDRAM的空間。在RW_ERAM1執行域內部,它使用".ANY(+RW +ZI)"語句,選擇了所有的RW/ZI類型的數據都分配到這個SDRAM區域,所以我們在工程中的C文件定義全局變量時,它都會被分配到這個SDRAM區域。
RW_IRAM1執行域
RW_IRAM1是STM32內部SRAM的執行域。我們在默認配置中增加了"*.o(STACK)及stm32f4xx_rcc.o(+RW)"語句。本來上面配置SDRAM執行域后已經達到使全局變量分配的目的,為何還要修改原內部SRAM的執行域呢?
這是由於我們在__main之前調用的SDRAM_Init函數調用了很多庫函數,且這些函數內部定義了一些局部變量,而函數內的局部變量是需要分配到"棧"空間(STACK),見圖 4853,查看靜態調用圖文件"SDRAM.htm"可了解它使用了多少棧空間以及調用了哪些函數。
圖 4853 SDRAM_Init的調用說明(SDRAM.htm文件)
從文件中可了解到SDRAM_Init使用的STACK的深度為148字節,它調用了FMC_SDRAMInit、RCC_AHB3PeriphClockCmd、SDRAM_InitSequence及SDRAM_GPIO_Config等函數。由於它使用了棧空間,所以在SDRAM_Init函數執行之前,棧空間必須要被准備好,然而在SDRAM_Init函數執行之前,SDRAM芯片卻並未正常工作,這樣的矛盾導致棧空間不能被分配到SDRAM。
雖然內部SRAM的執行域RW_IRAM1及SDRAM執行域RW_ERAM1中都使用".ANY(+RW +ZI)"語句選擇了所有RW及ZI屬性的內容,但對於符合兩個相同選擇語句的內容,鏈接器會優先選擇使用空間較大的執行域,即這種情況下只有當SDRAM執行域的空間使用完了,RW/ZI屬性的內容才會被分配到內部SRAM。
所以在大部分情況下,內部SRAM執行域中的".ANY(+RW +ZI)"語句是不起作用的(),而棧節區(STACK)又屬於ZI-data類,如果我們的內部SRAM執行域還是按原來的默認配置的話,棧節區會被分配到外部SDRAM,導致出錯。為了避免這個問題,我們把棧節區使用"*.o(STACK)"語句分配到內部SRAM的執行域。
增加"stm32f4xx_rcc.o(+RW)"語句是因為SDRAM_Init函數調用了stm32f4xx_rcc.c文件中的RCC_AHB3PeriphClockCmd函數,而查看map文件后了解到stm32f4xx_rcc.c定義了一些RW-data類型的變量,見圖 4854。不管這些數據是否在SDRAM_Init調用過程中使用到,保險起見,我們直接把這部分內容也分配到內部SRAM的執行區。
圖 4854 SDRAM.map文件中查看到stm32f4xx_rcc.o文件的RW-data使用統計信息
變量分配測試及結果
接下來查看本工程中的main文件,它定義了各種變量測試空間分配,見代碼清單 4825。
代碼清單 4825 main文件
1
2 //定義變量到SDRAM
3 uint32_t testValue =0 ;
4 //定義變量到SDRAM
5 uint32_t testValue2 =7;
6
7 //定義數組到SDRAM
9 //定義數組到SDRAM
10 uint8_t testGrup2[100] ={1,2,3};
11
12 /**
13 * @brief 主函數
14 * @param 無
15 * @retval 無
16 */
17 int main(void)
18 {
19 uint32_t inerTestValue =10;
20 /*SDRAM_Init已經在啟動文件的Reset_handler中調用,進入main之前已經完成初始化*/
21 // SDRAM_Init();
22
23 /* LED 端口初始化 */
24 LED_GPIO_Config();
25
26 /* 初始化串口 */
27 Debug_USART_Config();
28
29 printf("\r\nSCT文件應用——自動分配變量到SDRAM實驗\r\n");
30
31 printf("\r\n使用" uint32_t inerTestValue =10; "語句定義的局部變量:\r\n");
32 printf("結果:它的地址為:0x%x,變量值為:%d\r\n",(uint32_t)&inerTestValue,inerTestValue);
33
34 printf("\r\n使用"uint32_t testValue =0 ;"語句定義的全局變量:\r\n");
35 printf("結果:它的地址為:0x%x,變量值為:%d\r\n",(uint32_t)&testValue,testValue);
36
37 printf("\r\n使用"uint32_t testValue2 =7 ; "語句定義的全局變量:\r\n");
38 printf("結果:它的地址為:0x%x,變量值為:%d\r\n",(uint32_t)&testValue2,testValue2);
39
40 printf("\r\n使用"uint8_t testGrup[100] ={0};"語句定義的全局數組:\r\n");
41 printf("結果:它的地址為:0x%x,變量值為:%d,%d,%d\r\n",(uint32_t)&testGrup,testGrup[0],testGrup[1],testGrup[2]);
42
43 printf("\r\n使用"uint8_t testGrup2[100] ={1,2,3};"語句定義的全局數組:\r\n");
44 printf("結果:它的地址為:0x%x,變量值為:%d,%d,%d\r\n",(uint32_t)&testGrup2,testGrup2[0],testGrup2[1],testGrup2[2]);
45
46 uint32_t * pointer = (uint32_t*)malloc(sizeof(uint32_t)*3);
47 if(pointer != NULL)
48 {
49 *(pointer)=1;
50 *(++pointer)=2;
51 *(++pointer)=3;
52
53 printf("\r\n使用" uint32_t *pointer = (uint32_t*)malloc(sizeof(uint32_t)*3); "動態分配的變量\r\n");
54printf("\r\n定義后的操作為:\r\n*(pointer++)=1;\r\n*(pointer++)=2;\r\n*pointer=3;");
55 printf("結果:操作后它的地址為:0x%x,查看變量值操作:\r\n",(uint32_t)pointer);
56 printf("*(pointer--)=%d, \r\n",*(pointer--));
57 printf("*(pointer--)=%d, \r\n",*(pointer--));
58 printf("*(pointer)=%d, \r\n",*(pointer));
59 }
60 else
61 {
62 printf("\r\n使用malloc動態分配變量出錯!!!\r\n");
63 }
64 /*藍燈亮*/
65 LED_BLUE;
66 while(1);
67 }
68
代碼中定義了局部變量、初值非0的全局變量及數組、初值為0的全局變量及數組以及動態分配內存,並把它們的值和地址通過串口打印到上位機,通過這些變量,我們可以測試棧、ZI/RW-data及堆區的變量是否能正常分配。構建工程后,首先查看工程的map文件觀察變量的分配情況,見圖 4855及圖 4856。
圖 4855在map文件中查看工程的存儲分布1(SDRAM.map文件)
圖 4856 在map文件中查看工程的存儲分布2(SDRAM.map文件)
從map文件中,可看到stm32f4xx_rcc的RW-data及棧空間節區(STACK)都被分配到了RW_IRAM1區域,即STM32的內部SRAM空間中;而main文件中定義的RW-data、ZI-data以及堆空間節區(HEAP)都被分配到了RW_ERAM1區域,即我們擴展的SDRAM空間中,看起來一切都與我們的sct文件配置一致了。(堆空間屬於ZI-data,由於沒有像控制棧節區那樣指定到內部SRAM,所以它被默認分配到SDRAM空間了;在main文件中我們定義了一個初值為0的全局變量testValue2及初值為0的數組testGrup[100],它們本應占用的是104字節的ZI-data空間,但在map文件中卻查看到它僅使用了100字節的ZI-data空間,這是因為鏈接器把testValue2分配為RW-data類型的變量了,這是鏈接器本身的特性,它對像testGrup[100]這樣的數組才優化作為ZI-data分配,這不是我們sct文件導致的空間分配錯誤。)
接下來把程序下載到實驗板進行測試,串口打印的調試信息如圖 4857。
圖 4857 空間分配實驗實測結果
從調試信息中可發現,除了對堆區使用malloc函數動態分配的空間不正常,其它變量都定義到了正確的位置,如內部變量定義在內部SRAM的棧區域,全局變量定義到了外部SDRAM的區域。
經過我的測試,即使在sct文件中使用"*.o(HEAP)"語句指定堆區到內部SRAM或外部SDRAM區域,都無法正常使用malloc分配空間。另外,由於外部SDRAM的讀寫速度比內部SRAM的速度慢,所以我們更希望默認定義的變量先使用內部SRAM,當它的空間使用完畢后再把變量分配到外部SDRAM。
在下一小節中我們將改進sct的文件配置,解決這兩個問題。
48.6 實驗:優先使用內部SRAM並把堆區分配到SDRAM空間
本實驗使用另一種方案配置sct文件,使得默認情況下優先使用內部SRAM空間,在需要的時候使用一個關鍵字指定變量存儲到外部SDRAM,另外,我們還把系統默認的堆空間(HEAP)映射到外部SDRAM,從而可以使用C語言標准庫的malloc函數動態從SDRAM中分配空間,利用標准庫進行SDRAM的空間內存管理。
48.6.1 硬件設計
本小節中使用到的硬件跟"擴展外部SDRAM"實驗中的一致,若不了解,請參考該章節的原理圖說明。
48.6.2 軟件設計
本小節中提供的例程名為"SCT文件應用—優先使用內部SRAM並把堆分配到SDRAM空間",學習時請打開該工程來理解,該工程從上一小節的實驗改寫而來的,同樣地,本工程只使用手動編輯的sct文件配置,不使用MDK選項配置,在"Options for Target->linker"的選項見圖 4852。
圖 4858 使用手動編寫的sct文件
取消了這個默認的"Use Memory Layout from Target Dialog"勾選后,在MDK的Target對話框及文件配置的存儲器分布選項都會失效,僅以sct文件中的為准,更改對話框及文件配置選項都不會影響sct文件的內容。
1. 編程要點
(1) 修改啟動文件,在__main執行之前初始化SDRAM;
(2) 在sct文件中增加外部SDRAM空間對應的執行域;
(3) 在SDRAM中的執行域中選擇一個自定義節區"EXRAM";
(4) 使用__attribute__關鍵字指定變量分配到節區"EXRAM";
(5) 使用宏封裝__attribute__關鍵字,簡化變量定義;
(6) 根據需要,把堆區分配到內部SRAM或外部SDRAM中;
(7) 編寫測試程序,編譯正常后,查看map文件的空間分配情況。
2. 代碼分析
在__main之前初始化SDRAM
同樣地,為了使定義到外部SDRAM的變量能被正常初始化,需要修改工程startup_stm32f429_439xx.s啟動文件中的Reset_handler函數,在__main函數之前調用SDRAM_Init函數使SDRAM硬件正常運轉,見代碼清單 4823。
代碼清單 4826 修改啟動文件中的Reset_handler函數(startup_stm32f429_439xx.s文件)
1 ; Reset handler
2 Reset_Handler PROC
3 EXPORT Reset_Handler [WEAK]
4 IMPORT SystemInit
5 IMPORT __main
6
7 ;從外部文件引入聲明
8 IMPORT SDRAM_Init
9
10 LDR R0, =SystemInit
11 BLX R0
12
13 ;在__main之前調用SDRAM_Init進行初始化
14 LDR R0, =SDRAM_Init
15 BLX R0
16
17 LDR R0, =__main
18 BX R0
19 ENDP
它與上一小節中的改動一樣,當芯片上電運行Reset_handler函數時,在執行__main函數前先調用了我們自定義的SDRAM_Init函數,從而為分散加載代碼准備好正常的硬件工作環境。
sct文件配置
接下來分析本實驗中的sct文件配置與上一小節有什么差異,見代碼清單 4827。
代碼清單 4827 本實驗的sct文件內容(SDRAM.sct)
1 ; *************************************************************
2 ; *** Scatter-Loading Description File generated by uVision ***
3 ; *************************************************************
4 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
5 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
6 *.o (RESET, +First)
7 *(InRoot$$Sections)
8 .ANY (+RO)
9 }
10
11
12 RW_IRAM1 0x20000000 0x00030000 { ; 內部SRAM
13 .ANY (+RW +ZI) ;其余的RW/ZI-data都分配到這里
14 }
15
16 RW_ERAM1 0xD0000000 0x00800000 { ; 外部SDRAM
17 *.o(HEAP) ;選擇堆區
18 .ANY (EXRAM) ;選擇EXRAM節區
19 }
20 }
本實驗的sct文件中對內部SRAM的執行域保留了默認配置,沒有作任何改動,新增了一個外部SDRAM的執行域,並且使用了"*.o(HEAP)"語句把堆區分配到了SDRAM空間,使用".ANY(EXRAM)"語句把名為"EXRAM"的節區分配到SDRAM空間。
這個"EXRAM"節區是由我們自定義的,在語法上就跟在C文件中定義全局變量類似,只要它跟工程中的其它原有節區名不一樣即可。有了這個節區選擇配置,當我們需要定義變量到外部SDRAM時,只需要指定該變量分配到該節區,它就會被分配到SDRAM空間。
本實驗中的sct配置就是這么簡單,接下來直接使用就可以了。
指定變量分配到節區
當我們需要把變量分配到外部SDRAM時,需要使用__attribute__關鍵字指定節區,它的語法見代碼清單 4828。
代碼清單 4828 指定變量定義到某節區的語法
1 //使用 __attribute__ 關鍵字定義指定變量定義到某節區
2 //語法: 變量定義 __attribute__ ((section ("節區名"))) = 變量值;
3 uint32_t testValue __attribute__ ((section ("EXRAM"))) =7 ;
4
5 //使用宏封裝
6 //設置變量定義到"EXRAM"節區的宏
7 #define __EXRAM __attribute__ ((section ("EXRAM")))
8
9 //使用該宏定義變量到SDRAM
10 uint32_t testValue __EXRAM =7 ;
11
上述代碼介紹了基本的指定節區語法:"變量定義 __attribute__ ((section ("節區名"))) = 變量值;",它的主體跟普通的C語言變量定義語法無異,在賦值"="號前(可以不賦初值),加了個"__attribute__ ((section ("節區名")))"描述它要分配到的節區。本例中的節區名為"EXRAM",即我們在sct文件中選擇分配到SDRAM執行域的節區,所以該變量就被分配到SDRAM中了。
由於"__attribute__"關鍵字寫起來比較繁瑣,我們可以使用宏定義把它封裝起來,簡化代碼。本例中我們把指定到"EXRAM"的描述語句"__attribute__ ((section ("EXRAM")))"封裝成了宏"__EXRAM",應用時只需要使用宏的名字替換原來"__attribute__"關鍵字的位置即可,如"uint32_t testValue __EXRAM =7 ;"。有51單片機使用經驗的讀者會發現,這種變量定義方法就跟使用keil 51特有的關鍵字"xdata"定義變量到外部RAM空間差不多。
類似地,如果工程中還使用了其它存儲器也可以用這樣的方法實現變量分配,例如STM32的高速內部SRAM(CCM),可以在sct文件增加該高速SRAM的執行域,然后在執行域中選擇一個自定義節區,在工程源文件中使用"__attribute__"關鍵字指定變量到該節區,就可以可把變量分配到高速內部SRAM了。
根據我們sct文件的配置,如果定義變量時沒有指定節區,它會默認優先使用內部SRAM,把變量定義到內部SRAM空間,而且由於局部變量是屬於棧節區(STACK),它不能使用"__attribute__"關鍵字指定節區。在本例中的棧節區被分配到內部SRAM空間。
變量分配測試及結果
接下來查看本工程中的main文件,它定義了各種變量測試空間分配,見代碼清單 4825。
代碼清單 4829 main文件
1
2 //設置變量定義到"EXRAM"節區的宏
3 #define __EXRAM __attribute__ ((section ("EXRAM")))
4
5 //定義變量到SDRAM
6 uint32_t testValue __EXRAM =7 ;
7 //上述語句等效於:
8 //uint32_t testValue __attribute__ ((section ("EXRAM"))) =7 ;
9
10 //定義變量到SRAM
11 uint32_t testValue2 =7 ;
12
13 //定義數組到SDRAM
14 uint8_t testGrup[3] __EXRAM ={1,2,3};
15 //定義數組到SRAM
16 uint8_t testGrup2[3] ={1,2,3};
17
18 /**
19 * @brief 主函數
20 * @param 無
21 * @retval 無
22 */
23 int main(void)
24 {
25 uint32_t inerTestValue =10;
26 /*SDRAM_Init已經在啟動文件的Reset_handler中調用,進入main之前已經完成初始化*/
27 // SDRAM_Init();
28
29 /* LED 端口初始化 */
30 LED_GPIO_Config();
31
32 /* 初始化串口 */
33 Debug_USART_Config();
34
35 printf("\r\nSCT文件應用——自動分配變量到SDRAM實驗\r\n");
36
37 printf("\r\n使用" uint32_t inerTestValue =10; "語句定義的局部變量:\r\n");
38 printf("結果:它的地址為:0x%x,變量值為:%d\r\n",(uint32_t)&inerTestValue,inerTestValue);
39
40 printf("\r\n使用"uint32_t testValue __EXRAM =7 ;"語句定義的全局變量:\r\n");
41 printf("結果:它的地址為:0x%x,變量值為:%d\r\n",(uint32_t)&testValue,testValue);
42
43 printf("\r\n使用"uint32_t testValue2 =7 ; "語句定義的全局變量:\r\n");
44 printf("結果:它的地址為:0x%x,變量值為:%d\r\n",(uint32_t)&testValue2,testValue2);
45
46
47 printf("\r\n使用"uint8_t testGrup[3] __EXRAM ={1,2,3};"語句定義的全局數組:\r\n");
48 printf("結果:它的地址為:0x%x,變量值為:%d,%d,%d\r\n", (uint32_t)&testGrup,testGrup[0],testGrup[1],testGrup[2]);
49
50 printf("\r\n使用"uint8_t testGrup2[3] ={1,2,3};"語句定義的全局數組:\r\n");
51 printf("結果:它的地址為:0x%x,變量值為:%d,%d,%d\r\n", (uint32_t)&testGrup2,testGrup2[0],testGrup2[1],testGrup2[2]);
52
53 uint32_t *pointer = (uint32_t*)malloc(sizeof(uint32_t)*3);
54
55 if(pointer != NULL)
56 {
57 *(pointer)=1;
58 *(++pointer)=2;
59 *(++pointer)=3;
60
61 printf("\r\n使用" uint32_t *pointer = (uint32_t*)malloc(sizeof(uint32_t)*3); "動態分配的變量\r\n");
62 printf("\r\n定義后的操作為:\r\n*(pointer++)=1;\r\n*(pointer++)=2;\r\n*pointer=3;\r\n\r\n");
63 printf("結果:操作后它的地址為:0x%x,查看變量值操作:\r\n",(uint32_t)pointer);
64 printf("*(pointer--)=%d, \r\n",*(pointer--));
65 printf("*(pointer--)=%d, \r\n",*(pointer--));
66 printf("*(pointer)=%d, \r\n",*(pointer));
free(pointer);
67 }
68 else
69 {
70 printf("\r\n使用malloc動態分配變量出錯!!!\r\n");
71 }
72 /*藍燈亮*/
73 LED_BLUE;
74 while(1);
75 }
代碼中定義了普通變量、指定到EXRAM節區的變量並使用動態分配內存,還把它們的值和地址通過串口打印到上位機,通過這些變量,我們可以檢查變量是否能正常分配。
構建工程后,查看工程的map文件觀察變量的分配情況,見圖 4856。
圖 4859 在map文件中查看工程的存儲分布(SDRAM.map文件)
從map文件中可看到普通變量及棧節區都被分配到了內部SRAM的地址區域,而指定到EXRAM節區的變量及堆空間都被分配到了外部SDRAM的地址區域,與我們的要求一致。
再把程序下載到實驗板進行測試,串口打印的調試信息如圖 4860。
圖 4860 空間分配實驗實測結果
從調試信息中可發現,實際運行結果也完全正常,本實驗中的sct文件配置達到了優先分配變量到內部SRAM的目的,而且堆區也能使用malloc函數正常分配空間。
本實驗中的sct文件配置方案完全可以應用到您的實際工程項目中,下面再進一步強調其它應用細節。
使用malloc和free管理SDRAM的空間
SDRAM的內存空間非常大,為了管理這些空間,一些工程師會自己定義內存分配函數來管理SDRAM空間,這些分配過程本質上就是從SDRAM中動態分配內存。從本實驗中可了解到我們完全可以直接使用C標准庫的malloc從SDRAM中分配空間,只要在前面配置的基礎上修改啟動文件中的堆頂地址限制即可,見代碼清單 4830。
代碼清單 4830 修改啟動文件的堆頂地址(startup_stm32f429_439xx.s文件)
1 Heap_Size EQU 0x00000200
2
3 AREA HEAP, NOINIT, READWRITE, ALIGN=3
4
5
6 __heap_base
7 Heap_Mem SPACE Heap_Size
8 __heap_limit EQU 0xd0800000 ;設置堆空間的極限地址(SDRAM),
;0xd0000000+0x00800000
9
10 PRESERVE8
11 THUMB
C標准庫的malloc函數是根據__heap_base及__heap_limit地址限制分配空間的,在以上的代碼定義中,堆基地址__heap_base的由鏈接器自動分配未使用的基地址,而堆頂地址__heap_limit則被定義為外部SDRAM的最高地址0xD0000000+0x00800000(使用這種定義方式定義的__heap_limit值與Heap_Size定義的大小無關),經過這樣配置之后,SDRAM內除EXRAM節區外的空間都被分配為堆區,所以malloc函數可以管理剩余的所有SDRAM空間。修改后,它生成的map文件信息見圖 4861。
圖 4861 使用malloc管理剩余SDRAM空間
可看到__heap_base的地址緊跟在EXRAM之后,__heap_limit指向了SDRAM的最高地址,因此malloc函數就能利用所有SDRAM的剩余空間了。注意圖中顯示的HEAP節區大小為0x00000200字節,修改啟動文件中的Heap_Size大小可以改變該值,它的大小是不會影響malloc函數使用的,malloc實際可用的就是__heap_base與__heap_limit之間的空間。至於如何使Heap_Size的值能自動根據__heap_limit與__heap_base的值自動生成,我還沒找到方法,若您了解,請告知。
把堆區分配到內部SRAM空間。
若您希望堆區(HEAP)按照默認配置,使它還是分配到內部SRAM空間,只要把"*.o(HEAP)"選擇語句從SDRAM的執行域刪除掉即可,堆節區就會默認分配到內部SRAM,外部SDRAM僅選擇EXRAM節區的內容進行分配,見代碼清單 4831,若您更改了啟動文件中堆的默認配置,主注意空間地址的匹配。
代碼清單 4831 按默認配置分配堆區到內部SRAM的sct文件范例
1 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
2 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
3 *.o (RESET, +First)
4 *(InRoot$$Sections)
5 .ANY (+RO)
6 }
7 RW_IRAM1 0x20000000 0x00030000 { ; 內部SRAM
8 .ANY (+RW +ZI) ;其余的RW/ZI-data都分配到這里
9 }
10
11 RW_ERAM1 0xD0000000 0x00800000 { ; 外部SDRAM
12 .ANY (EXRAM) ;選擇EXRAM節區
13 }
14 }
屏蔽鏈接過程的warning
在我們的實驗配置的sct文件中使用了"*.o(HEAP)"語句選擇堆區,但有時我們的工程完全沒有使用堆(如整個工程都沒有使用malloc),這時鏈接器會把堆占用的空間刪除,構建工程后會輸出警告提示該語句僅匹配到無用節區,見圖 4862。
圖 4862 僅匹配到無用節區的warning
這並無什么大礙,但強迫症患者不希望看到warning,可以在"Options for Target->Linker->disable Warnings"中輸入warning號屏蔽它。warning號可在提示信息中找到,如上圖提示信息中"warning:L6329W"表示它的warning號為6329,把它輸入到圖 4863中的對話框中即可。
圖 4863 屏蔽鏈接過程的warning
注意SDRAM用於顯存的改變
根據本實驗的sct文件配置,鏈接器會自動分配SDRAM的空間,而本書以前的一些章節講解的實驗使用SDRAM空間的方式非常簡單粗暴,如果把這個sct文件配置直接應用到這些實驗中可能會引起錯誤,例如我們的液晶驅動,見代碼清單 4832。
代碼清單 4832 原液晶顯示驅動使用的顯存地址
1 /* LCD Size (Width and Height) */
2 #define LCD_PIXEL_WIDTH ((uint16_t)800)
3 #define LCD_PIXEL_HEIGHT ((uint16_t)480)
4
5 #define LCD_FRAME_BUFFER ((uint32_t)0xD0000000) //第一層首地址
6 #define BUFFER_OFFSET ((uint32_t)800*480*3) //一層液晶的數據量
7 #define LCD_PIXCELS ((uint32_t)800*480)
8
9 /**
10 * @brief 初始化LTD的層參數
11 * - 設置顯存空間
12 * - 設置分辨率
13 * @param None
14 * @retval None
15 */
16 void LCD_LayerInit(void)
17 {
18 /*其它部分省略*/
19 /* 配置本層的顯存首地址 */
20 LTDC_Layer_InitStruct.LTDC_CFBStartAdress = LCD_FRAME_BUFFER;
21 /*其它部分省略*/
22 }
在這段液晶驅動代碼中,我們直接使用一個宏定義了SDRAM的地址,然后把它作為顯存空間告訴LTDC外設(從0xD0000000地址開始的大小為800*480*3的內存空間),然而這樣的內存配置鏈接器是無法跟蹤的,鏈接器在自動分配變量到SDRAM時,極有可能使用這些空間,導致出錯。
解決方案之一是使用__EXRAM定義一個數組空間作為顯存,由鏈接器自動分配空間地址,最后把數組地址作為顯存地址告訴LTDC外設即可,其它類似的應用都可以使用這種方案解決。
代碼清單 4833 由鏈接器自動分配顯存空間
1 #define BUFFER_OFFSET ((uint32_t)800*480*3) //一層液晶的數據量
2 #define LCD_PIXCELS ((uint32_t)800*480)
3
4 uint8_t LCD_FRAME_BUFFER[BUFFER_OFFSET] __EXRAM;
5
6 /**
7 * @brief 初始化LTD的層參數
8 * - 設置顯存空間
9 * - 設置分辨率
10 * @param None
11 * @retval None
12 */
13 void LCD_LayerInit(void)
14 {
15 /*其它部分省略*/
16 /* 配置本層的顯存首地址 */
17 LTDC_Layer_InitStruct.LTDC_CFBStartAdress = &LCD_FRAME_BUFFER;
18 /*其它部分省略*/
19 }
總而言之,當不再使用默認的sct文件配置時,一定要注意修改后會引起內存空間發生什么變化,小心這些變化導致的存儲器問題。
如何把棧空間也分配到SDRAM
前面提到因為SDRAM_Init初始化函數本身使用了棧空間(STACK),而在執行SDRAM_Init函數之前SDRAM並未正常工作,這樣的矛盾導致無法把棧分配到SDRAM。其實換一個思路,只要我們的SDRAM初始化過程不使用棧空間,SDRAM正常運行后棧才被分配到SDRAM空間,這樣就沒有問題了。
由於原來的SDRAM_Init實現的SDRAM初始化過程使用了STM32標准庫函數,它不可避免地使用了棧空間(定義了局部變量),要完全不使用棧空間完成SDRAM的初始化,只能使用純粹的寄存器方式配置。在STM32標准庫的"system_stm32f4xx.c"文件已經給出了類似的解決方案,SystemInit_ExtMemCtl函數,見代碼清單 4834。
代碼清單 4834 SystemInit_ExtMemCtl函數(system_stm32f4xx.c文件)
1 #ifdef DATA_IN_ExtSDRAM
2 /**
3 * @brief Setup the external memory controller.
4 * Called in startup_stm32f4xx.s before jump to main.
5 * This function configures the external SDRAM mounted on STM324x9I_EVAL board
6 * This SDRAM will be used as program data memory (including heap and stack).
7 * @param None
8 * @retval None
9 */
10 void SystemInit_ExtMemCtl(void)
11 {
12 register uint32_t tmpreg = 0, timeout = 0xFFFF;
13 register uint32_t index;
14
15 /* Enable GPIOC, GPIOD, GPIOE, GPIOF, GPIOG, GPIOH and GPIOI interface
16 clock */
17 RCC->AHB1ENR |= 0x000001FC;
18
19 /* Connect PCx pins to FMC Alternate function */
20 GPIOC->AFR[0] = 0x0000000c;
21 GPIOC->AFR[1] = 0x00007700;
22 /* Configure PCx pins in Alternate function mode */
23 GPIOC->MODER = 0x00a00002;
24 /* Configure PCx pins speed to 50 MHz */
25 GPIOC->OSPEEDR = 0x00a00002;
26 /* Configure PCx pins Output type to push-pull */
27
28 /**********************具體配置省略*************************/
29 }
30 #endif /* DATA_IN_ExtSDRAM */
該函數沒有使用棧空間,僅使用register關鍵字定義了兩個分配到內核寄存器的變量,其余配置均通過直接操作寄存器完成。這個函數針對ST的一個官方評估編寫的,在其它硬件平台直接使用可能有錯誤,若有需要可仔細分析它的代碼再根據自己的硬件平台進行修改。
這個函數是使用條件編譯語句"#ifdef DATA_IN_ExtSDRAM"包裝起來的,默認情況下這個函數並不會被編譯,需要使用這個函數時只要定義這個宏即可。
定義了DATA_IN_ExtSDRAM宏之后,SystemInit_ExtMemCtl函數會被SystemInit函數調用,見代碼清單 4835,而我們知道SystemInit會在啟動文件中的Reset_handler函數中執行。
代碼清單 4835 SystemInit函數對SystemInit_ExtMemCtl的調用(system_stm32f4xx.c文件)
1 /**
2 * @brief Setup the microcontroller system
3 * Initialize the Embedded Flash Interface, the PLL and update the
4 * SystemFrequency variable.
5 * @param None
6 * @retval None
7 */
8 void SystemInit(void)
9 {
10
11 /******部分代碼省略******/
12 #if defined(DATA_IN_ExtSRAM) || defined(DATA_IN_ExtSDRAM)
13 SystemInit_ExtMemCtl();
14 #endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */
15 /******部分代碼省略******/
16 }
所以,如果希望把棧空間分配到外部SDRAM,可按以下步驟操作:
修改sct文件,使用"*.o(STACK)"語句把棧空間分配到SDRAM的執行域;
根據自己的硬件平台,修改SystemInit_ExtMemCtl函數,該函數要實現SDRAM的初始化過程,且該函數不能使用棧空間;
定義DATA_IN_ExtSDRAM宏,從而使得SystemInit_ExtMemCtl函數被加進編譯,並被SystemInit調用;
由於Reset_handler默認會調用SystemInit函數執行,所以不需要修改啟動文件。
48.7 每課一問
1. 在工程中分別增加全局變量(0值及非0值)、局部變量,查看map文件,觀察應用程序空間的變化。(定義的變量若不使用會被編譯器優化;即使使用了變量,受編譯器優化影響,空間變化也不一定完全與預計的一致,定義占用空間較大的變量觀察,效果更明顯,如uint32_t testValue[50]={0};或uint32_t testValue[50]={0,1,2,3};)
2. 查看MDK的對armcc編譯器的命令輸入(圖 4813),查看修改MDK配置后,命令會如何變化變化(如修改芯片類型、是否使用浮點單元、編譯優化等)。
3. 嘗試修改分散加載文件,然后編譯后在map文件中查看存儲器索引表的變化。
4. 參考圖 4837,在MDK中加入fromelf指令,控制它生成工程對應的bin文件。
5. 修改"自動分配變量到外部SDRAM"中的實驗,把SDRAM的初始化過程放在main函數里(刪掉啟動文件中執行SDRAM_Init的兩條語句),然后使用串口打印查看定義到SDRAM的變量內容是否正常。