第八章 STM32Cube固件包
本章節我們來認識STM32Cube固件包,因為HAL庫是STM32Cube的一個重要的組成部分,是以分析STM32Cube固件包是很有必要的。如果使用STM32CubeIDE來開發的話,軟體會自動下載下傳一個STM32Cube固件包,STM32CubeMX就是利用這個固件包來生成初始化代碼的。大家肯定好奇這個固件包裡有什麼?是做什麼用的?和HAL庫有什麼關系?本章節我們就來分析這個固件包。
本章将分為如下幾個小節:
8.1、擷取STM32Cube固件包;
8.2、STM32CubeMP1固件包目錄結構;
8.3、CMSIS檔案夾關鍵檔案介紹;
8.4、章節小結;
8.1 擷取STM32Cube固件包
STM32Cube是ST公司提供的一套免費的開發工具和STM32Cube 固件包,覆寫了整個STM32産品,可在STM32平台上進行快速輕松的開發,進而簡化了開發人員的工作。STM32Cube由以下元件組成,這些元件可以一起使用或獨立使用:
- 允許使用者通過圖形化向導來生成C語言工程的圖形配置工具STM32CubeMX。
- 适用于每個STM32 MCU和MPU系列的STM32Cube MCU和MPU軟體包(也叫STM32Cube 固件包或者STM32Cube包)。
進入ST官網https://www.st.com/content/st_com/en.html以後,在搜尋框中輸入STM32CubeMP1進行搜尋。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SY0AjNmFDZwYDZzMzLcBTMyIDMy8CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
圖8.1.1搜尋STM32CubeMP1固件包
在開發闆CD光牒A-基礎資料\7、STM32MP1參考資料\STM32MP157 Cube包中我們也有提供STM32MP1的固件包:
圖8.1.2A盤中下載下傳好的固件包
解壓此固件包後,我們打開Drivers檔案夾,看到的STM32MP1xx_HAL_Driver就是HAL庫了:
圖8.1.3庫
8.2 STM32CubeMP1固件包目錄結構
接下來,我們看看前面下載下傳好的STM32CubeMP1固件包目錄結構,打開STM32Cube_FW_MP1_V1.2.0固件包,目錄結構如下圖。
圖8.2.1固件包目錄結構
_htmresc檔案夾下是ST公司的LOGO圖檔和一些網站的資料,其實是用不到的,我們不去關注。對比較重要的檔案夾,我們按照順序進行介紹:
8.2.1 Drivers檔案夾
Drivers檔案夾包含BSP,CMSIS和STM32MP1xx_HAL_Driver三個子檔案夾。三個子檔案夾具體說明請參考下表
BSP 檔案夾 | BSP也叫闆級支援包,此支援包提供的是直接與硬體打交道的API,例如觸摸屏,LCD,SRAM以及EEPROM等闆載硬體資源等驅動。目前在STM32cubeMP1固件包中,ST還未添加這部分内容(目前有LED、COM端口以及按鈕相關的API),後期ST應該會逐漸添加這些檔案。 BSP檔案夾下還給了ST官方DISCO和EVAL開發闆的硬體驅動API檔案,每一種闆對應一個檔案夾。可以打開開發闆檔案夾,根據裡邊幫助文檔檢視API檔案都有什麼内容 |
CMSIS 檔案夾 | CMSIS檔案夾用于存放符合CMSIS标準的檔案,包括STM32啟動檔案、ARM Cortex核心檔案和對應外設頭檔案。關于CMSIS檔案夾裡的檔案,我們後面會專門講解。 |
Core | 用于Cortex-M處理器核心和外圍裝置的API |
Core_A | 用于Cortex-A5 / A7 / A9處理器核心和外圍裝置的API |
Device | 微控制器專用頭檔案/啟動代碼/專用系統檔案 |
DSP | 适用于各種資料類型的DSP庫集合 |
Include | STM32MP1xx外圍裝置通路層頭檔案 |
Lib | ARM、GCC 和 IAR格式的 DSP 庫檔案 |
NN | 神經網絡庫集合,目的是在Cortex-M處理器核心上最大化神經網絡的性能并最小化其記憶體占用 |
RTOS | 實時作業系統通用API相關檔案(V1版本),相容RTX4 |
RTOS2 | 對RTOS V1的拓展,相容RTX5 |
STM32MP1xx_HAL_Driver檔案夾 | HAL庫檔案夾,處理STM32“内部”裝置,它包含了所有的STM32MP1xx系列HAL庫頭檔案和源檔案,也就是所有底層硬體抽象層API聲明和定義。它的作用是屏蔽了複雜的硬體寄存器操作,統一了外設的接口函數。該檔案夾包含Src和Inc兩個子檔案夾,其中Src子檔案夾存放的是.c源檔案,Inc子檔案夾存放的是與之對應的.h頭檔案。每個.c源檔案對應一個.h頭檔案。在前面的STM32CubeIDE第一個工程實驗中就有用到該檔案夾的檔案,我們後面會重點介紹該檔案夾的檔案。 |
表8.2.1.1檔案夾簡介
8.2.2 Middlewares檔案夾
Middlewares(中間件)檔案夾下目前隻有Third_Party檔案夾,是提供一組服務的庫,目前裡邊隻有FreeRTOS實時系統支援包和OpenAMP檔案夾。
圖8.2.2. 1檔案夾
FreeRTOS是一個免費的實時作業系統(RTOS),它同時支援搶占優先級和協作優先級,具有非常彈性的任務優先級配置設定,可以快速響應中斷,在實時性要求較高的産品開發中應用很廣泛。關于FreeRTOS的學習,感興趣的可以檢視正點原子《STM32F429 FreeRTOS開發手冊》。 在FreeRTOS檔案夾下,其具有FreeRTOS實時系統支援包,Source目錄包含每個端口共有的三個檔案list.c,queue.c和tasks.c,核心包含在這三個檔案中。Source/Portable目錄包含用于于特定微控制器和/或編譯器的檔案。Source/include目錄包含實時核心頭檔案。Source/CMSIS_RTOS和Source/CMSIS_RTOS_V2下是FreeRTOS實時系統API檔案,一個是V1版本一個是V2版本。
圖8.2.2.2支援包
AMP是指非對稱多處理, 非對稱多處理是指各核的結構并非對稱,例如STM32MP1是兩個Cortex-A7核心加一個Cortex-M4核心的組合,各個核結構并非對稱。OpenAMP常用于處理器間通信,OpenAMP軟體架構為開發AMP系統提供了必要的API函數,可以實作核間通信。
8.2.3 Projects檔案夾
該檔案夾存放的是一些可以直接編譯的執行個體工程,是STM32MP1xx系列的STM32CubeMP1固件示例。每個檔案夾對應一個ST官方的Demo闆。比如我們要檢視STM32mp157相關工程,我們直接打開子檔案夾STM32MP157C-DK2即可,裡面有很多執行個體供我們參考。每個Demo闆下都會有以下4個檔案夾:
- Applications: OpenAMP、FreeRTOS和CoproSync應用程式示例。
- Demonstrations:AI相關示例。
- Examples:外圍裝置的功能和用法示例。
- Templates:固件庫工程模闆,允許使用者在給定的闆上快速建構任何固件應用程式。
我們檢視其中的示例的時候,工程下面有MDK-ARM和STM32CubeIDE等子檔案夾,輕按兩下MDK-ARM子檔案夾内部的Project.uvprojx的工程檔案,可以在MDK中打開工程,輕按兩下STM32CubeIDE子檔案夾下的.project工程檔案,可以在STM32CubeIDE中打開工程。
圖8.2.3. 1工程檔案
關于Projects檔案夾的整體介紹,可以打開裡邊的STM32CubeProjectsList.html檔案了解更加詳細内容。在檢視工程檔案的時候,可以打開裡邊的readme.txt檢視介紹内容。
8.2.4 Utilities檔案夾
該檔案夾中的檔案介紹了如何配置STM32MP1xx的資料總管,例如檔案中提供了共享記憶體中的虛拟表位址、在ETZPC控制下的裝置寄存器位址表、共享資源ID等,這些檔案由ST官網提供,一般不能修改檔案中的内容。
8.2.5 其它檔案
Readme.md簡單介紹STM32CubeMP1固件檔案的内容。Release_Notes.html檔案是固件庫版本更新說明,關于STM32CubeMP1固件版本詳細更新内容,我們可以檢視此檔案。License.md和package.xml檔案隻是協定說明和固件包版本的說明,不用怎麼管。
8.3 CMSIS檔案夾關鍵檔案介紹
随着32位處理器在嵌入式市場需求量逐漸增多,各家晶片公司推出新型晶片,伴随而來的是開發工具、軟體相容以及代碼移植等問題。在這種情況下,各個硬體平台的供應商都尋求易于使用且高效的解決方案,其中,ARM與Atmel、IAR、KEIL、SEGGER和ST等諸多晶片和軟體工具廠商合作,釋出了一套CMSIS标準。
CMSIS(Cortex Microcontroller Software Interface Standard),即ARM Cortex微控制器軟體接口标準。CMSIS标準提供了核心和外圍裝置、實時作業系統和中間元件之間的通用API接口,進而簡化了軟體的重複使用,縮短了微控制器開發人員的學習時間,并縮短了新裝置的上市時間。下圖是ARM公司的CMSIS标準結構框圖:
圖8.3.1CMSIS标準結構框圖
其中,CMSIS-CORE層定義了Cortex-M以及Cortex-A處理器(Cortex-A5/A7/A9)核心和外圍裝置的标準化API。CMSIS-Pack層包含了CMSIS-Driver驅動架構、CMSIS-DSP相關庫、CMSIS-RTOS作業系統API、中間件API和Peripheral HAL層API等。根據CMSIS的标準,ARM公司整合并提供了CMSIS 軟體包模闆,目前最新的是5.7.0版本,感興趣的小夥伴可以在CMSIS官網浏覽更多資訊: https://developer.arm.com/tools-and-software/embedded/cmsis
基于ARM提供的CMSIS 軟體包模闆,ST官方結合自己晶片的差異進行了修改,并将其整合到了STM32Cube固件包中的CMSIS檔案夾裡。
打開固件包中STM32Cube_FW_MP1_V1.2.0\Drivers\CMSIS目錄,其中,Device檔案夾和Include檔案夾是每個工程都要用到的。
圖8.3.2目錄
Device檔案夾下是具體晶片直接相關的檔案,裡邊是ST官方的STM32MP1xx器件專用的頭檔案、啟動代碼檔案和專用系統檔案,此檔案夾下我們重點介紹這幾個檔案:stm32mp1xx.h、system_stm32mp1xx.c、startup_stm32mp15xx.s和stm32mp15xx_m4.ld檔案。
Include檔案夾下是符合CMSIS标準的核心頭檔案,主要是核内外設檔案,我們會重點介紹core_cm4.h檔案。
8.3.1 stm32mp1xx.h檔案
檔案路徑:Device\ST\STM32MP1xx\Include\stm32mp1xx.h
stm32mp1xx.h檔案在工程中是一定要有的,檔案的内容看起來不多,卻非常重要。該檔案主要就是确定代碼中是否使用或者不使用某個底層驅動檔案,我們簡單分析stm32mp1xx.h檔案。抛開檔案中的主體代碼,如下代碼是筆者将主體部分代碼删掉以後所看到的整體架構,其它的頭檔案架構也類似:
1 #ifndef __STM32MP1xx_H
2 #define __STM32MP1xx_H
3
4 #ifdef __cplusplus /* c++編譯環境中才會定義__cplusplus (plus即"+"的意思) */
5 extern "C" { /* 告訴編譯器下面程式采用c方式編譯 */
6 #endif /* __cplusplus */
7
8 /****** 省略的代碼 ******/
9
10 #ifdef __cplusplus
11 }
12 #endif /* __cplusplus */
13 #endif /* __STM32MP1xx_H */
學過C++的朋友應該很熟悉,上面第4行到第12行代碼是為了在C++中盡可能的支援C代碼和C庫。意思是,如果這是一段C的代碼,那麼加入"extern "C"{" 和 " }"處理其中的代碼,因為C++和C對産生的函數名字的處理是不一樣的,C++中存在重載,C中沒有重載,為了在C++代碼中調用C寫成的庫檔案,就需要用extern"C"來告訴編譯器:這是一個用C寫成的庫檔案,請用C的方式來連結它們。如果不這樣處理,在C++中編譯後會出現連結錯誤。這麼做其實也是為了友善代碼移植。
下面我們分析stm32mp1xx.h檔案的主要代碼實作部分。
1 #if !defined (STM32MP1)
2 #define STM32MP1
3 #endif /* STM32MP1 */
4
5 #if !defined (USE_HAL_DRIVER)
6 #endif /* USE_HAL_DRIVER */
7
8 /****** 省略了CMSIS裝置版本号相關代碼 ******/
9
10 #if defined(CORE_CM4)
11 /* keep for backward compatibility STM32MP15xx = STM32MP157Cxx */
12 #if defined(STM32MP15xx)
13 #include "stm32mp157cxx_cm4.h"
14 #elif defined(STM32MP157Axx)
15 #include "stm32mp157axx_cm4.h"
16 #elif defined(STM32MP157Cxx)
17 #include "stm32mp157cxx_cm4.h"
18 /****** 此處省略部分内容 ******/
19 #elif defined(STM32MP151Fxx)
20 #include "stm32mp151fxx_cm4.h"
21 #else
22 #error "Please select first the target STM32MP1xx device used in your application (in stm32mp1xx.h file)"
23 #endif
24 #endif
25
如上圖代碼,大部分是一些條件編譯,如果條件編譯的宏有被定義,那麼就參加編譯。我們先看第10行到24行間的代碼,第10行到13行,如果定義了CORE_CM4這個宏,當再定義STM32MP15xx這個宏的時候,就會包含stm32mp157cxx_cm4.h頭檔案,同理第14行到20行也是類似的宏定義,隻要有定義某個宏,就會包含對應的頭檔案。
第37和38行,在定義宏CORE_CM4以後,沒有定義其它宏,那麼就會提示:Please select first the target STM32MP1xx device used in your application (in stm32mp1xx.h file),提示要在stm32mp1xx.h檔案中定義這個宏。
包含的stm32mp157cxx_cm4.h這些頭檔案也在Device\ST\STM32MP1xx\Include\目錄下,裡邊有很多stm32mp151Pxx_cm4.h、stm32mp153Pxx_cm4.h和stm32mp157Pxx_cm4.h檔案(這裡的P是一個代号,表示a、c、d和f)。這些檔案是幹嘛用的呢?我們打開其中一個檔案大概看看,例如stm32mp157dxx_cm4.h這個檔案,檔案中的内容很多,有上萬行的代碼,根據裡邊的注釋,了解到這個檔案主要就是對STM32MP1XX系列器件的Cortex-M處理器和核心外設的配置,例如中斷号定義、外設寄存器結構體聲明、外設寄存器位定義和寄存器的操作的宏定義以及外圍裝置記憶體映射等等。
我們接着往下看後面的代碼。
1 #if defined(CORE_CA7)
2 /* keep for backward compatibility STM32MP15xx = STM32MP157Cxx */
3 #if defined(STM32MP15xx)
4 #include "stm32mp157cxx_ca7.h"
5 #elif defined(STM32MP157Axx)
6 #include "stm32mp157axx_ca7.h"
7 #elif defined(STM32MP157Cxx)
8 #include "stm32mp157cxx_ca7.h"
9 /****** 此處省略部分内容 ******/
10 #elif defined(STM32MP151Fxx)
11 #include "stm32mp151fxx_ca7.h"
12 #else
13 #error "Please select first the target STM32MP1xx device used in your application (in stm32mp1xx.h file)"
14 #endif
15 #endif
第1行先定義一個宏CORE_CA7,在這個宏的基礎上,如果有定義其它的宏就會包含對應的頭檔案,如果沒有定義宏将提示Please select first the target STM32MP1xx device used in your application (in stm32mp1xx.h file)。這些包含的頭檔案,例如stm32mp157cxx_ca7.h檔案和前面的stm32mp157cxx_cm4.h頭檔案作用類似,隻不過stm32mp157cxx_ca7.h檔案是針對Cortex-A核心的。
經過前面的分析,正點原子的開發闆使用的是STM32MP157DAA1這顆晶片,根據前面的分析應該是要包含stm32mp157dxx_cm4.h和stm32mp157dxx_ca7.h檔案,則需要定義宏STM32MP157Dxx。
我們檢視最後的代碼:
1 typedef enum /* 布爾形變量定義 */
2 {
3 RESET = 0,
4 SET = !RESET
5 } FlagStatus, ITStatus;
6
7 typedef enum /* 功能型狀态變量 */
8 {
9 DISABLE = 0,
10 ENABLE = !DISABLE
11 } FunctionalState;
12 #define IS_FUNCTIONAL_STATE(STATE) (((STATE) == DISABLE) || ((STATE) == ENABLE))
13
14 typedef enum /* 錯誤型狀态變量 */
15 {
16 ERROR = 0,
17 SUCCESS = !ERROR
18 } ErrorStatus;
19
20 /******一些位操作定義******/
21 #define SET_BIT(REG, BIT) ((REG) |= (BIT))
22
23 #define CLEAR_BIT(REG, BIT) ((REG) &= ~(BIT))
24
25 #define READ_BIT(REG, BIT) ((REG) & (BIT))
26
27 #define CLEAR_REG(REG) ((REG) = (0x0))
28
29 #define WRITE_REG(REG, VAL) ((REG) = (VAL))
30
31 #define READ_REG(REG) ((REG))
32
33 #define MODIFY_REG(REG, CLEARMASK, SETMASK) WRITE_REG((REG), \ (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))
34
35 #define POSITION_VAL(VAL) (__CLZ(__RBIT(VAL)))
36
37 #if defined (USE_HAL_DRIVER)
38 #include "stm32mp1xx_hal_conf.h"
39 #endif /* USE_HAL_DRIVER */
第1行到第18行是一些通過枚舉類型定義變量,例如FlagStatus有RESET和SET兩個狀态,分别為0和1,ITStatus也是有兩個狀态0和1。這些枚舉類型變量會大量地用于HAL庫的檔案中隻要遇見這些變量,我們想到的是它的值要麼是0要麼是1。
第12行是用于參數檢查的,如果輸入的參數是DISABLE和ENABLE其中的一個,那麼(((STATE) == DISABLE) || ((STATE) == ENABLE))的值始終為1,否則為0。
第21到35行表示一些位操作定義,例如21行#define SET_BIT(REG, BIT) ((REG) |= (BIT))中有兩個參數REG和BIT,REG是一個寄存器,BIT表示這個寄存器的第幾位,這個宏表示将寄存器REG的第BIT位置1。這些位定義也大量用于HAL庫的檔案中。
第37、38行表示如果定義了USE_HAL_DRIVER這個宏,就包含stm32mp1xx_hal_conf.h頭檔案,此頭檔案是HAL庫的頭檔案集,一旦使用了相應的子產品,就要定義相關的子產品使能,然後相應子產品的頭檔案才會被包含。
stm32mp1xx.h檔案内容就這麼多,經過前面的分析,如果要操作CM4的外設,我們需要定義CORE_CM4、STM32MP157Dxx和USE_HAL_DRIVER這3個宏定義,這3個宏定義在哪裡定義呢?如果是用MDK來編譯,點選Keil的魔術棒
,在C/C++配置欄的Preprocessor Symbols(預處理器符号)的Dfine(定義)處加上CORE_CM4,USE_HAL_DRIVER,STM32MP157Dxx就可以了(注意,用英文格式的逗号隔開)。
圖8.3.1. 1上添加宏
8.3.2 stm32mp157dxx_cm4.h
檔案路徑:Device\ST\STM32MP1xx\Include\stm32mp157dxx_cm4.h
在stm32mp1xx.h檔案中有介紹到,通過同時定義CORE_CM4和STM32MP157Dxx宏來加載stm32mp157dxx_cm4.h檔案。前面我們也有介紹到stm32mp157dxx_cm4.h檔案,打開檔案進行浏覽,檔案中的内容很多,有上萬行的代碼,根據裡邊的注釋,了解到這個檔案主要就是對STM32MP157dxx系列器件的Cortex-M處理器和外設(GPIO、DMA、TTFD、ETH、CRC、TIM、UART、I2C等等)的裝置資源定義,例如外設中斷号定義、外設寄存器結構體聲明、外設寄存器位定義和寄存器的操作的宏定義以及外圍裝置記憶體映射等等。
裡邊使用了大量的結構體來對寄存器進行封裝,如果我們要通路某個寄存器,隻需要定義一個結構體指針,然後通過指針來讀寫對應的寄存器(結構體成員)。下面我們以GPIO為例子介紹:
typedef struct
{
__IO uint32_t MODER; /* GPIO端口模式寄存器,位址偏移量:0x000 */
__IO uint32_t OTYPER; /* GPIO端口輸出類型寄存器,位址偏移量:0x004 */
__IO uint32_t OSPEEDR; /* GPIO端口輸出速度寄存器,位址偏移量:0x008 */
__IO uint32_t PUPDR; /* GPIO端口上拉/下拉寄存器,位址偏移量:0x00C */
__IO uint32_t IDR; /* GPIO端口輸入資料寄存器,位址偏移量:0x010 */
__IO uint32_t ODR; /* GPIO端口輸出資料寄存器,位址偏移量:0x014 */
__IO uint32_t BSRR; /* GPIO端口位設定/重置寄存器,位址偏移量:0x018*/
__IO uint32_t LCKR; /*GPIO端口配置鎖定寄存器,位址偏移量:0x01C*/
__IO uint32_t AFR[2]; /* GPIO備用功能寄存器,位址偏移量:0x020-0x024*/
__IO uint32_t BRR; /* GPIO端口位複位寄存器,位址偏移量:0x028 */
uint32_t RESERVED0; /* 保留,位址偏移量:0x02C */
__IO uint32_t SECCFGR; /*用于GPIOZ的GPIO安全配置寄存器,位址偏移量:0x030*/
uint32_t RESERVED1[229]; /* 保留,位址偏移量:0x034-0x3C4*/
__IO uint32_t HWCFGR10; /* GPIO硬體配置寄存器10,位址偏移量:0x3C8*/
__IO uint32_t HWCFGR9; /* GPIO硬體配置寄存器9,位址偏移量:0x3CC*/
__IO uint32_t HWCFGR8; /* GPIO硬體配置寄存器8,位址偏移量:0x3D0*/
__IO uint32_t HWCFGR7; /* GPIO硬體配置寄存器7,位址偏移量:0x3D4*/
__IO uint32_t HWCFGR6; /* GPIO硬體配置寄存器6,位址偏移量:0x3D8*/
__IO uint32_t HWCFGR5; /* GPIO硬體配置寄存器5,位址偏移量:0x3DC*/
__IO uint32_t HWCFGR4; /*GPIO硬體配置寄存器4,位址偏移:0x3E0*/
__IO uint32_t HWCFGR3; /* GPIO硬體配置寄存器3,位址偏移量:0x3E4*/
__IO uint32_t HWCFGR2; /* GPIO硬體配置寄存器2,位址偏移量:0x3E8*/
__IO uint32_t HWCFGR1; /* GPIO硬體配置寄存器1,位址偏移量:0x3EC*/
__IO uint32_t HWCFGR0; /*GPIO硬體配置寄存器0,位址偏移量:0x3F0*/
__IO uint32_t VERR; /*GPIO版本寄存器,位址偏移量:0x3F4*/
__IO uint32_t IPIDR; /* GPIO識别寄存器,位址偏移量:0x3F8 */
__IO uint32_t SIDR; /* GPIO大小識别寄存器,位址偏移量:0x3FC */
} GPIO_TypeDef;
這段代碼中,typedef是類型定義以及結構體定義的基本文法,我們在前面5.1.5小節和5.1.6小節有講解。__IO表示volatile ,在core_cm4.h檔案中有定義。其中,結構體成員MODER、OTYPER、和SIDR這些是GPIOx(x等于A~K和Z)對應的寄存器名稱。
這裡,每個結構體成員均定義為uint32_t,即相鄰每個成員偏移4個位元組,寄存器MODER偏移位址為0x000,寄存器OTYPER偏移位址為0x004,以此類推。
通過結構體,我們知道了偏移位址,要确定一個寄存器的實際位址,我們還需要知道寄存器的基位址。通過參考手冊我們知道GPIOI挂在了AHB總線上,且AHB總線的基位址是0x50000000,GPIOI的基位址就是0x5000A000,這個基位址在代碼的哪裡定義了呢?
圖8.3.2.1參考手冊部分截圖
也是在stm32mp157dxx_cm4.h頭檔案中可以找到如下的代碼:
1外圍記憶體映射 */
2 #define MCU_AHB_SRAM ((uint32_t)0x10000000)
3 #define MCU_AHB_RETRAM ((uint32_t)0x00000000)
4
5 #define SYSRAM_BASE ((uint32_t)0x2FFC0000)
6 #define RETRAM_BASE MCU_AHB_RETRAM
7 #define SRAM_BASE MCU_AHB_SRAM
8 #define PERIPH_BASE ((uint32_t)0x40000000)
9 #define MPU_AXI_BUS_MEMORY_BASE ((uint32_t)0x60000000)
10
11 #define FMC_NOR_MEM_BASE (MPU_AXI_BUS_MEMORY_BASE)
12 #define QSPI_MEM_BASE (MPU_AXI_BUS_MEMORY_BASE + 0x10000000)
13 #define FMC_NAND_MEM_BASE (MPU_AXI_BUS_MEMORY_BASE + 0x20000000)
14 #define STM_DATA_BASE (MPU_AXI_BUS_MEMORY_BASE + 0x30000000)
15 #define DRAM_MEM_BASE (MPU_AXI_BUS_MEMORY_BASE + 0x60000000)
16
17 /*裝置電子簽名記憶體映射 */
18 #define UID_BASE (0x5C005234L)
19 #define PACKAGE_BASE (0x5C005240L)
20 #define RPN_BASE (0x5C005204L)
21 #define DV_BASE (0x50081000L)
22
23 /* 外圍記憶體映射 */
24 #define MCU_APB1_PERIPH_BASE (PERIPH_BASE + 0x00000000)
25 #define MCU_APB2_PERIPH_BASE (PERIPH_BASE + 0x04000000)
26 #define MCU_AHB2_PERIPH_BASE (PERIPH_BASE + 0x08000000)
27 #define MCU_AHB3_PERIPH_BASE (PERIPH_BASE + 0x0C000000)
28 #define MCU_AHB4_PERIPH_BASE (PERIPH_BASE + 0x10000000)
29 #define MCU_APB3_PERIPH_BASE (PERIPH_BASE + 0x10020000)
30 #define APB_DEBUG_PERIPH_BASE (PERIPH_BASE + 0x10080000)
31 #define MPU_AHB5_PERIPH_BASE (PERIPH_BASE + 0x14000000)
32 #define GPV_PERIPH_BASE (PERIPH_BASE + 0x17000000)
33 #define MPU_AHB6_PERIPH_BASE (PERIPH_BASE + 0x18000000)
34 #define MPU_APB4_PERIPH_BASE (PERIPH_BASE + 0x1A000000)
35 #define MPU_APB5_PERIPH_BASE (PERIPH_BASE + 0x1C000000)
36 /******省略APB1、APB2、AHB2、AHB3相關代碼******/
37 /*!< MCU_AHB4 */
38 #define RCC_BASE (MCU_AHB4_PERIPH_BASE + 0x0000)
39 #define PWR_BASE (MCU_AHB4_PERIPH_BASE + 0x1000)
40 #define GPIOA_BASE (MCU_AHB4_PERIPH_BASE + 0x2000)
41 #define GPIOB_BASE (MCU_AHB4_PERIPH_BASE + 0x3000)
42 #define GPIOC_BASE (MCU_AHB4_PERIPH_BASE + 0x4000)
43 #define GPIOD_BASE (MCU_AHB4_PERIPH_BASE + 0x5000)
44 #define GPIOE_BASE (MCU_AHB4_PERIPH_BASE + 0x6000)
45 #define GPIOF_BASE (MCU_AHB4_PERIPH_BASE + 0x7000)
46 #define GPIOG_BASE (MCU_AHB4_PERIPH_BASE + 0x8000)
47 #define GPIOH_BASE (MCU_AHB4_PERIPH_BASE + 0x9000)
48 #define GPIOI_BASE (MCU_AHB4_PERIPH_BASE + 0xA000)
49 #define GPIOJ_BASE (MCU_AHB4_PERIPH_BASE + 0xB000)
50 #define GPIOK_BASE (MCU_AHB4_PERIPH_BASE + 0xC000)
51 #define AIEC_BASE (MCU_AHB4_PERIPH_BASE + 0xD000)
52 #define AIEC_C1_BASE (AIEC_BASE + 0x0080)
53 #define AIEC_C2_BASE (AIEC_BASE + 0x00C0)
54 /* Alias EXTI_BASE defined because HAL code not yet reworked with new name AIEC*/
55 #define EXTI_BASE AIEC_BASE
56 #define EXTI_C1_BASE AIEC_C1_BASE
57 #define EXTI_C2_BASE AIEC_C2_BASE
這部分代碼是記憶體映射相關的宏定義。如上代碼,第8行定義PERIPH_BASE宏為0x40000000,第28行宏MCU_AHB4_PERIPH_BASE為(PERIPH_BASE + 0x10000000),計算得出0x5000 0000,此值剛好表示AHB4總線的基位址。第48行宏GPIOI_BASE為(MCU_AHB4_PERIPH_BASE + 0xA000),計算得出0x5000 A000,此值剛好是GPIOI的基位址。同樣的,其它的總線以及外設的基位址在stm32mp157dxx_cm4.h頭檔案中均有定義。
總線或者外設的偏移位址找到了,基位址也找到了,基位址+偏移位址就等于實際位址。如果我們要操作某個外設,也就是操作對應外設的寄存器,那麼,這些寄存器的位址又怎麼得來的呢?在stm32mp157dxx_cm4.h頭檔案中找到如下部分代碼:
#define GPIOI ((GPIO_TypeDef *) GPIOI_BASE)
這裡表示将宏GPIOI定義為((GPIO_TypeDef *) GPIOI_BASE)。
GPIOI_BASE 是一個uint32_t類型,我們已經計算得出0x5000 A000。GPIO_TypeDef結構體我們在前面有列出代碼,(GPIO_TypeDef *)裡邊加了一個*号,表示結構體指針類型。((GPIO_TypeDef *) GPIOI_BASE)表示将uint32_t類型的GPIOI_BASE強制轉化成結構體指針類型。
上面這一行代碼就表示:将GPIOI變成GPIO_typedef 類型的結構體指針,并且預設指向了基位址GPIOI_BASE,即從GPIOI_BASE開始,長度為RCC_TypeDef這個類型的長度。這樣一來,每個寄存器的位址也就确定下來了,通過指針即可通路結構體的成員(寄存器)。
在以後,我們要操作GPIOI中的某個寄存器,例如操作ODR寄存器,隻需要通過指針操作結構體成員就可以了:
GPIOI->ODR = 0XFFFF;
上面,GPIOI->ODR也可以改寫為(*GPIOI).ODR。這段代碼表示将GPIOI中的ODR寄存器指派為0XFFFF。
實際上,在HAL庫中很多函數裡就是這麼用的,例如在HAL庫的stm32mp1xx_hal_gpio.c檔案中,就有很多這樣的代碼:
圖8.3.2.2庫函數部分截圖
8.3.3 stm32mp157dxx_ca7.h檔案
檔案路徑:Device\ST\STM32MP1xx\Include\stm32mp157dxx_ca7.h和stm32mp157dxx_cm4.h檔案類似,隻不過是對Cortex-A7處理器和核心外設的配置。
8.3.4 system_stm32mp1xx.c檔案
檔案路徑:
Device\ST\STM32MP1xx\Include\system_stm32mp1xx.h
Device\ST\STM32MP1xx\Source\Templates\system_stm32mp1xx.c
這兩個檔案提供了兩個函數和一個全局變量:系統初始化函數SystemInit、系統時鐘更新函數SystemCoreClockUpdate和SystemCoreClock全局變量。
SystemInit函數在系統複位後,在跳到主程式main.c之前被startup_stm32mp1xx.s檔案調用。SystemInit函數中主要是初始化FPU設定、配置SRAM中的向量表和禁用所有中斷和事件。我們簡單分析一下代碼。
1 void SystemInit (void)
2 {
3 /* FPU settings */
4 #if defined (CORE_CM4)
5 #if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
6 /* set CP10 and CP11 Full Access */
7 SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2));
8 #endif
9
10 /*配置中斷向量表位址=基位址+偏移位址 */
11 #if defined (VECT_TAB_SRAM) /* 向量表存儲在 SRAM */
12 /* Vector Table Relocation in Internal SRAM */
13 SCB->VTOR = MCU_AHB_SRAM | VECT_TAB_OFFSET;
14 #endif
15 /* Disable all interrupts and events */
16 CLEAR_REG(EXTI_C2->IMR1);
17 CLEAR_REG(EXTI_C2->IMR2);
18 CLEAR_REG(EXTI_C2->IMR3);
19 CLEAR_REG(EXTI_C2->EMR1);
20 CLEAR_REG(EXTI_C2->EMR2);
21 CLEAR_REG(EXTI_C2->EMR3);
22 #else
23 #error Please #define CORE_CM4
24 #endif
25 }
FPU(Floating Point Unit,浮點單元)即用于處理浮點數運算的單元,可以大大加速浮點運算的處理速度。STM32MP1系列器件的Cortex-M4 核心是具有FPU單元的,支援浮點指令集,處理數學運算能力得以大大提高。
第4到第8行表示使用條件編譯來設定FPU,如果定義了CORE_CM4宏,當__FPU_PRESENT和__FPU_USED同時為1時,就使能FPU單元,編譯時就加入啟動FPU 的代碼,CPU 也就能正确高效的使用FPU 進行簡單的加減乘除運算了。第12行表示設定 CPACR 寄存器的 20~23 位為 1,以開啟STM32MP1的硬體 FPU 功能。
根據前面的分析,如果我們要開啟FPU,隻需要定義CORE_CM4宏,并将__FPU_PRESENT和__FPU_USED同時設定為1就可以了,在前面我們已經知道定義CORE_CM4宏了,剩下的__FPU_PRESENT和__FPU_USED将怎麼設定呢?
如果使用的是MDK的朋友,使用的是keil5的話,隻需要在點選魔術棒
,然後再Floating Point Hardware裡選擇Use Single Presicion就可以了。
圖8.3.4.1中開啟FPU
第11到第14行,這段代碼表示表示如果定義VECT_TAB_SRAM,則内部SRAM中的向量表被重定位。MCU_AHB_SRAM表示向量表基位址,其值為0x10000000(在stm32mp157dxx_cm4.h檔案中定義),VECT_TAB_OFFSET表示向量表偏移量,可以修改它的值,修改的時候,其值必須是0x400的倍數。VTOR 寄 存 器 存 放 的 是 中 斷 向 量 表 的 起 始 地 址(其有一個預設值),默 認 情 況,VECT_TAB_SRAM 是沒有定義的。在system_stm32mp1xx.c檔案的最前面有#define VECT_TAB_OFFSET 0x00這句,已經定義了向量表偏移量為0x00,如果将0x00修改0x10,同時也定義VECT_TAB_SRAM這個宏,那麼:
SCB->VTOR=0x10000000|0x10=0x10000010
這樣就設定了中斷向量表偏移。不過一般盡量不要修改system_stm32mp1xx.c這樣的系統級别檔案,如果要改的話,盡量在其他檔案中進行修改。
第16到第21行,表示清除中斷屏蔽寄存器EXTI_IMR1、EXTI_IMR2和EXTI_IMR3以屏蔽中斷請求,即禁用所有中斷和事件。
接下來我們檢視SystemCoreClockUpdate函數。SystemCoreClockUpdate函數的代碼比較多,注釋也比較詳細,為了不占用篇幅,我們這裡省略部分代碼:
1 uint32_t SystemCoreClock = HSI_VALUE;
2
3 void SystemCoreClockUpdate (void)
4 {
5 uint32_t pllsource, pll3m, pll3fracen;
6 float fracn1, pll3vco;
7
8 switch (RCC->MSSCKSELR & RCC_MSSCKSELR_MCUSSRC)
9 {
10 case 0x00: /* HSI used as system clock source */
11 SystemCoreClock = (HSI_VALUE >> (RCC->HSICFGR & \ RCC_HSICFGR_HSIDIV));
12 break;
13
14 case 0x01: /* HSE used as system clock source */
15 SystemCoreClock = HSE_VALUE;
16 break;
17
18 case 0x02: /* CSI used as system clock source */
19 SystemCoreClock = CSI_VALUE;
20 break;
21
22 case 0x03: /* PLL3_P used as system clock source */
23 /*******省略部分代碼*******/
24 break;
25 }
26
27 /* Compute mcu_ck */
28 SystemCoreClock = SystemCoreClock >> (RCC->MCUDIVR & \ RCC_MCUDIVR_MCUDIV);
29 }
根據注釋,System Clock 的時鐘源有:HSI(預設值64 MHz)、HSE(預設值為24 MHz)、CSI(預設值為4 MHz)和PLL3_P。在檔案前面有一行uint32_t SystemCoreClock = HSI_VALUE,其中HSI_VALUE的值為64000000(在stm32mp1xx_hal_conf.h檔案中定義)。根據代碼的注釋,SystemCoreClock是一個全局變量,系統複位以後,系統時鐘預設采用HSI_VALUE,即為64MHz。在本篇的CM4裸機實驗中,如果我們沒有配置時鐘樹,那麼MCU核心時鐘就預設64Hz的時鐘。
SystemCoreClockUpdate函數的作用就是,根據時鐘寄存器的值來更新SystemCoreClock變量。SystemCoreClock變量包含核心時鐘頻率(HCLK),使用者應用程式可以使用它來設定SysTick定時器或配置其他參數。在程式執行期間,每次核心時鐘改變時,都必須調用SystemCoreClockUpdate函數來更新SystemCoreClock變量值,如果不這樣,SystemCoreClock變量值将會不準确,任何基于SystemCoreClock變量的配置都是不正确的。這麼做也就是為了保證SystemCoreClock的準确性。 時鐘部分在STM32中比較複雜,也不是三言兩語能說的清楚,我們後面會分出專門的章節來講解,并結合對應的實驗來加深了解。
8.3.5 startup_stm32mp15xx.s檔案
1. 啟動檔案在哪
檔案路徑:Device\ST\STM32MP1xx\Source\Templates\arm\startup_stm32mp15xx.s
startup_stm32mp15xx.s是由ST官方提供的,一般直接拿來用,有需要的時候才會改寫。它主要是用彙編語言編寫,是系統上電後第一個運作的程式檔案,屬于啟動檔案。Device\ST\STM32MP1xx\Source\Templates下面有3個檔案夾,每個檔案夾下均有一個startup_stm32mp15xx.s檔案,不同的開發環境使用不同檔案夾下的startup_stm32mp15xx.s檔案,STM32CubeIDE軟體使用的是gcc下的檔案,MDK軟體使用的是arm下的檔案,每個檔案夾下的檔案内容均不相同,但是他們的功能是一樣的。
圖8.3.5.1檔案夾
2. 啟動檔案中的部分指令
在分析啟動檔案前,我們先來了解幾個彙編文法:
指令名稱 | 作用 |
EQU | 給數字常量取一個符号名,相當于C語言中的define |
AREA | 彙編一個新的代碼段或者資料段 |
ALIGN | 編譯器對指令或者資料的存放位址進行對齊,一般需要跟一個立即數,預設表示4位元組對齊。要注意的是,這個不是ARM的指令,是編譯器的,這裡放到一起為了友善。 |
SPACE | 配置設定記憶體空間 |
PRESERVE8 | 目前檔案堆棧需要按照8位元組對齊 |
THUMB | 表示後面指令相容THUMB指令。在ARM以前的指令集中有16位的THUMBM指令,現在Cortex-M系列使用的都是THUMB-2指令集,THUMB-2是32位的,相容16位和32位的指令,是THUMB的超級版。 |
EXPORT | 聲明一個标号具有全局屬性,可被外部的檔案使用 |
DCD | 以位元組為機關配置設定記憶體,要求4位元組對齊,并要求初始化這些記憶體 |
PROC | 定義子程式,與ENDP成對使用,表示子程式結束 |
WEAK | 弱定義,如果外部檔案聲明了一個标号,則優先使用外部檔案定義的标号,如果外部檔案沒有定義也不會出錯。要注意的是,這個不是ARM的指令,是編譯器的,這裡放到一起為了友善。 |
IMPORT | 聲明标号來自外部檔案,跟C語言中的extern關鍵字類似 |
LDR | 從存儲器中加載字到一個存儲器中 |
BLX | 跳轉到由寄存器給出的位址,并根據寄存器的 LSE 确定處理器的狀态,還要把跳轉前的下條指令位址儲存到 LR |
BX | 跳轉到由寄存器/标号給出的位址,不用傳回 |
B | 跳轉到一個标号 |
IF,ELSE,ENDIF | 彙編條件分支語句,跟C語言的類似 |
END | 到達檔案的末尾,檔案結束 |
表8.3.5.1部分彙編指令
上表,列舉了STM32啟動檔案的一些彙編和編譯器指令,關于其他更多的ARM彙編指令,我們可以通過MDK的索引搜尋工具中搜尋找到。打開索引搜尋工具的方法:MDK->Help->uVision Help,如圖8.3.5.2所示。
圖8.3.5.2打開索引搜尋工具的方法
打開之後,我們以EQU為例,示範一下怎麼使用,如圖8.3.5.3所示。
圖8.3.5.3搜尋EQU彙編指令
搜尋到的标題有很多,我們隻需要看Assembler User Guide 這部分即可。
3. 啟動檔案分析
上表列舉了STM32啟動檔案的一些彙編和編譯器指令,關于其他更多的ARM彙編指令,大家可以查閱彙編文法的書籍。下面,我們借助檔案中的注釋,我們來分析一下startup_stm32mp15xx.s檔案做了些什麼工作。
1)設定棧指針SP;
2)設定初始PC= Reset_Handler;
3)設定中斷向量表入口位址,并初始化向量表;
4)跳轉到C庫中的main(最終調用main函數)。
線上程模式下複位了Cortex-M處理器後,優先級為Privileged(特權模式),棧頂設定為主函數。
(1)棧空間的開辟
棧空間的開辟,源碼如下所示:
Stack_Size EQU 0x00000400 ;設定棧的大小為1KB
AREA STACK, NOINIT, READWRITE, ALIGN=3
__stack_limit
Stack_Mem SPACE Stack_Size 配置設定棧的空間
__initial_sp ;棧的結束位址,棧由高到低生長
符号’;’表示注釋,相當于C語言程式的’//’。
一段大小為0x0000 0400(1KB)的棧空間,段名為STACK,NOINIT表示不初始化;READWRITE表示可讀可寫;ALIGN=3,表示按照 2^3對齊,即 8 位元組對齊。
AREA彙編一個新的代碼段或者資料段。
SPACE配置設定記憶體指令,配置設定大小為Stack_Size位元組連續的存儲單元給棧空間。
__initial_sp緊挨着SPACE放置,表示棧的結束位址,棧是從高往低生長,是以結束位址就是棧頂位址。
棧主要用于存放局部變量,函數形參等,屬于編譯器自動配置設定和釋放的記憶體,棧的大小不能超過内部SRAM 的大小。如果工程的程式量比較大,定義的局部變量比較多,那麼就需要在啟動代碼中修改棧的大小,即修改Stack_Size的值。如果程式出現了莫名其妙的錯誤,并進入了HardFault的時候,你就要考慮下是不是棧空間不夠大,溢出了的問題。
(2)堆空間的開辟
堆空間的開辟,源碼如下所示:
Heap_Size EQU 0x00000200 ;堆的大小512MB
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size ;配置設定堆的空間
__heap_limit ;堆的結束位址
源碼含義:開辟一段大小為0x0000 0200(512位元組)的堆空間,段名為HEAP,不初始化,可讀可寫,8位元組對齊。
__heap_base表示堆的起始位址,__heap_limit表示堆的結束位址。堆和棧的生長方向相反的,堆是由低向高生長,而棧是從高往低生長。
堆主要用于動态記憶體的配置設定,像malloc()、calloc()和realloc()等函數申請的記憶體就在堆上面。堆中的記憶體一般由程式員配置設定和釋放,若程式員不釋放,程式結束時可能由作業系統回收。
接下來是PRESERVE8和THUMB指令兩行代碼。如下所示:
PRESERVE8
THUMB
PRESERVE8:訓示編譯器按照8位元組對齊。
THUMB:訓示編譯器之後的指令為THUMB指令。
注意:由于正點原子提供了獨立的記憶體管理實作方式(mymalloc,myfree等),并不需要使用C庫的malloc和free等函數,也就用不到堆空間,是以我們可以設定Heap_Size的大小為0,以節省記憶體空間。
(3)中斷向量表定義(簡稱:向量表)
為中斷向量表定義一個資料段,如下所示:
AREA RESET, DATA, READONLY
EXPORT __Vectors ;聲明全局變量
EXPORT __Vectors_End
EXPORT __Vectors_Size
源碼含義:定義一個資料段,名字為RESET, READONLY表示隻讀。EXPORT表示聲明一個标号具有全局屬性,可被外部的檔案使用。這裡是聲明了__Vectors、__Vectors_End和__Vectors_Size三個标号具有全局性,可被外部的檔案使用。
STM32MP157的中斷向量表定義代碼如下所示,由于中斷向量表部分代碼太多了,省略了部分,如需檢視具體的中斷向量表,可以直接看startup_stm32mp15xx.s檔案:
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; -14 NMI Handler
DCD HardFault_Handler ; -13 Hard Fault Handler
DCD MemManage_Handler ; -12 MPU Fault Handler
DCD BusFault_Handler ; -11 Bus Fault Handler
DCD UsageFault_Handler ; -10 Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; -5 SVCall Handler
DCD DebugMon_Handler ; -4 Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; -2 PendSV Handler
DCD SysTick_Handler ; -1 SysTick Handler
; Interrupts
DCD WWDG1_IRQHandler ;
;由于代碼太多了,這裡省略部分代碼 ;
DCD RESERVED148_IRQHandler ;
DCD WAKEUP_PIN_IRQHandler ;
SPACE (73 * 4) ; Interrupts 151 .. 224 are left out
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vector
__Vectors 為向量表起始位址, __Vectors_End 為向量表結束位址,__Vectors_Size為向量表大小,__Vectors_Size = __Vectors_End - __Vectors。DCD表示配置設定一個或者多個以字為機關的記憶體,以四位元組對齊,并要求初始化這些記憶體。從代碼上看,向量表中存放的都是中斷服務函數的函數名,是以 C 語言中的中斷服務函數名對晶片來說實際上就是一個位址。
以上的中斷向量表将會放置在位址為0x0000 0000處(也就是堆棧頂的位址),Cortex-M4複位後從此處取出資料用于初始化MSP寄存器,位址為0x0000 0004的表示複位向量,我們也可以通過檢視《STM32MP157參考手冊》來了解Cortex-M4核心的中斷映射關系,STM32MP157的M4核心中斷管理器叫做NVIC,其系統中斷(也叫内部中斷)有10個,外部中斷有150個,下圖隻是截圖了一部分。
圖8.3.5.4中斷向量表
從表中了解到,位址0x0000 0000 是保留的,但其實是reset後MSP(主堆棧指針)的位址,Reset 中斷的位址為0x0000 0004,NMI中斷的位址是0x0000 0008。M4的中斷映射範圍0x0000 0000~00000x00000294。表中,priority 一清單示中斷優先級,參數越小表示中斷優先級越高。Fixed表示此中斷優先級是固定的,不可更改,Settable表示中斷優先級是可程式設計的,可以通過程式設計來更改。Acronym一清單示中斷的名稱,Description表示中斷的說明,Address表示中斷的位址。
根據上表了解到,M4核心的中斷向量表是從位址0x0000 0000開始的,位于BOOT區的RETRAM(64kB),我們在用MDK或者STM32CubeIDE來調試程式的時候,M4的代碼其實是放到了SRAM中運作了,其中M4可運作的SRAM是SRAM1(128kB)、SRAM2(128kB)、SRAM3(64kB)和SRAM4(64kB),位址範圍是0X10000000~0X1005FFFF,共384KB。如下的記憶體映射表可以清楚的看出記憶體映射關系,堆棧大小(以位元組為機關)為0x0~0xFFFF FFFF:
圖8.3.5.5記憶體映射表
(4)複位程式
接下來是定義隻讀代碼段,如下所示:
AREA |.text|, CODE, READONLY
以上代碼是定義一個段,且命名為.text,屬于隻讀的代碼段,在CODE區。
接下來是複位子程式代碼,如下所示:
Reset_Handler PROC ;表示子程式的開始,這個是真正的複位程式
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit ;跳轉到SystemInit函數
BLX R0
LDR R0, =__main ;跳轉到main函數
BX R0
ENDP
利用PROC、ENDP這一對僞指令把程式段分為若幹個過程,使程式的結構加清晰。
複位子程式是複位後第一個被執行的程式,主要是調用SystemInit函數配置系統時鐘、還有就是初始化FSMC/FMC總線上外挂的SRAM(可選)。然後在調用C 庫函數__main,最終調用 main 函數去到 C 的世界。
EXPORT聲明複位中斷向量Reset_Handler為全局屬性,這樣外部檔案就可以調用此複位中斷服務。
WEAK:表示弱定義,弱,就是表示此函數可以被使用者進行重寫(重新定義),表示如果使用者在其它地方重新定義一個同名函數後,最終編譯器編譯的時候,就會選擇使用者定義的函數,如果使用者沒有重新定義這個函數(或者函數名字寫錯了),那麼編譯器就會預設執行帶有弱符号的函數,并且編譯器不會報錯。[WEAK]的作用其實是為了防止使用者使能了中斷而沒有編寫中斷服務函數,進而造成程式崩潰。帶有弱符号的函數都可以進行重寫。
IMPORT表示該标号來自外部檔案。這裡表示SystemInit 和__main 這兩個函數均來自外部的檔案。
LDR、BLX、BX 是核心的指令,可在《Cortex-M3權威指南》第四章-指令集裡面查詢到。
LDR表示從存儲器中加載字到一個存儲器中。
BLX表示跳轉到由寄存器給出的位址,并根據寄存器的 LSE 确定處理器的狀态,還要把跳轉前的下條指令位址儲存到 LR。
BX表示跳轉到由寄存器/标号給出的位址,不用傳回。這裡表示切換到__main位址,最終調用main函數,不傳回,進入C的世界。
以上代碼就是Reset_Handler所做的工作,先轉移到SystemInit函數起始處,然後才會跳轉到main函數中。到這裡終于明白了,函數并不是程式執行的第一段代碼,隻能說,main函數是應用程式的入口函數。SystemInit函數是在system_stm32mp1xx.c檔案中定義的,它在主程式main.c執行之前startup_stm32mp1xx.s檔案調用,主要作用就是初始化FPU設定、配置SRAM中的向量表和禁用所有中斷和事件,我們在前面已經有介紹。
(5)中斷服務程式
接下來就是中斷服務程式了,如下所示,列出了部分代碼:
;用于定義預設的中斷處理程式Set_Default_Handler
;預設的中斷處理程式Set_Default_Handler是個無限空循環函數
;是以,Set_Default_Handler可以被實際的中斷處理程式覆寫。
MACRO
Set_Default_Handler $Handler_Name
$Handler_Name PROC ;表示子程式的開始
EXPORT $Handler_Name [WEAK]
B . ;B表示跳轉到一個标号,這裡是跳轉到一個’.’,表示無限空循環
ENDP
MEND
; 預設異常/中斷處理程式
Set_Default_Handler NMI_Handler
Set_Default_Handler HardFault_Handler
Set_Default_Handler MemManage_Handler
Set_Default_Handler BusFault_Handler
Set_Default_Handler UsageFault_Handler
Set_Default_Handler SVC_Handler
;由于代碼太多了,這裡省略部分代碼
Set_Default_Handler DTS_IRQHandler ; Temperature sensor interrupt
Set_Default_Handler RESERVED148_IRQHandler ; Reserved
Set_Default_Handler WAKEUP_PIN_IRQHandler ; Interrupt for all 6 wake-up pins
ALIGN
以上代碼中,先定義一個中斷處理程式Set_Default_Handler,此中斷處理程式的功能是執行一個無限死循環,且被[WEAK]聲明為弱定義函數。
再往下看剩下的代碼,Set_Default_Handler NMI_Handler表示如果不重寫NMI_Handler函數的話,那麼就預設執行Default_Handler函數,也就是執行死循環。意思就是說,如果發生了NMI中斷,如果使用者沒有重新定義NMI_Handler函數,那麼發生中斷時就是預設執行Set_Default_Handler函數,也就是進入無限空循環。
在啟動檔案代碼中,預設已經幫我們把所有中斷的中斷服務函數寫好了,但是都是被弱定義的Set_Default_Handler替代,是以真正的中斷服務函數需要我們在外部實作,我們編寫中斷服務函數的時候,中斷服務函數的名字一定要寫正确,也就是根據中斷向量表定義的函數名來。例如,我們要編寫定時器1溢出中斷服務函數,那麼就按照中斷向量表定義的來,函數名就是TIM1_UP_IRQHandler,如果此函數名寫錯了的話,當發生定時器1溢出事件時,就預設執行以上的Set_Default_Handler函數,即一直執行空循環。
最後的ALIGN指令表示對指令或者資料的存放位址進行對齊,一般需要跟一個立即數,預設表示4位元組對齊。要注意的是,這個不是ARM的指令,是編譯器的。
(6)使用者堆棧初始化代碼
接下就是啟動檔案最後一部分代碼,使用者堆棧初始化代碼,如下所示:
EXPORT __stack_limit ; 聲明__stack_limit标号具有全局屬性
EXPORT __initial_sp ; 聲明__initial_sp标号具有全局屬性
IF Heap_Size != 0 ;是彙編的條件分支語句
EXPORT __heap_base ; 聲明__heap_base标号具有全局屬性
EXPORT __heap_limit ; 聲明__heap_limit标号具有全局屬性
ENDIF ; ENDIF是彙編的條件分支語句
END ; END表示到達檔案的末尾,檔案結束
__initial_sp表示棧頂位址,__heap_base表示堆起始位址,__heap_limit表示堆結束位址。
如果Heap_Size(堆的大小)不等于0,則聲明__heap_base和__heap_limit這兩個标号具有全局屬性,可被外部的檔案使用。
下面我們來捋一下啟動檔案的工作過程:
上電複位後,硬體會自動根據向量表偏移位址找到向量表,首先從0x0000 0000位址處加載初始MSP,然後從偏移為4的位址(0x0000 0004)處加載PC,0x0000 0004位址處存放的是Reset_Handler,即執行複位中斷服務程,Reset_Handler主要做了兩件事,一個是跳轉到SystemInit函數完成必要的系統初始化,另外一個是跳轉到main函數。然後,如果有中斷發生,如果此中斷對應的中斷服務函數沒有被使用者重寫,則系統進入無限空循環,如果此中斷對應的中斷函數被使用者重寫了,則執行使用者重寫的中斷服務函數。
4. 系統啟動流程
CM4核心啟動,需要将撥碼開關BOOT0、BOOT1和BOOT2設定為001,這個是晶片設計的時候就已經定好了的。STM32MP157 支援從多種不同的裝置啟動,通過設定撥碼開關可以選擇從指定的裝置啟動,啟動方式如表:
BOOT0 | BOOT1 | BOOT2 | 啟動模式 |
0 | 0 | 1 | 啟動 M4 核心 |
1 | 0 | 1 | SD 卡啟動 |
1 | 0 | 0 | NOR 啟動 |
0 | 1 | 0 | EMMC 啟動 |
1 | 1 | 0 | NAND 啟動 |
0 | 1 | 1 | USB/UART 啟動 |
0 | 0 | 0 |
表8.3.5. 2啟動模式
正點原子 STM32MP157 開發闆上支援 USB、SD 卡、EMMC 以及 M4 核心這 4 種啟動方式。
我們知道啟動模式不同,啟動的起始位址是不一樣的,例如STM32F4系列的晶片,CM4核心有可用的FLASH,代碼下載下傳到内部FLASH時,代碼從位址0x0800 0000開始被執行的。當産生複位,并且離開複位狀态後,CM4核心做的第一件事就是讀取下列兩個32位整數的值:
(1)從位址 0x0800 0000 處取出堆棧指針MSP 的初始值,該值就是棧頂位址。
(2)從位址 0x0800 0004 處取出程式計數器指針PC的初始值,該值指向中斷服務程式 Reset_Handler。下面用示意圖表示,如下圖所示。
圖8.3.5. 6 STM32F4 FLASH啟動
換做STM32MP157,因為CM4核心沒有可用的FLASH,是以在MDK或者STM32CubeIDE上仿真的時候,是将程式放到了SRAM中運作了。根據前面的分析,開發闆從MCU啟動,當産生複位,并且離開複位狀态後,CM4核心做的第一件事:
(1)位于BOOT啟動代碼區RETRAM(64kB)的位址 0x0000 0000 處取出初始堆棧指針MSP 的初始值,該值就是棧頂位址。
(2)從位址0x00000004 處取出程式計數器指針PC的初始值,該值指向中斷服務程式 Reset_Handler。下面用示意圖表示,如下圖所示。
圖8.3.5. 7 STM32MP1 M4核心啟動
上述過程中,核心是從0x0000 0000和0x0000 0004兩個的位址擷取堆棧指針MSP和程式計數器指針PC。事實上,0x0000 0000和0x0000 0004兩個的位址可以被重映射到其他的位址空間,因為可以通過修改定義宏VECT_TAB_SRAM以及修改向量表偏移VECT_TAB_OFFSET來實作,前面在system_stm32mp1xx.c檔案中有介紹。
下面,我們看看開發闆CD光牒A-基礎資料\1、程式源碼\3、M4裸機驅動例程\MP157-M4 HAL庫V1.1\3 HAL庫跑馬燈實驗 在進入仿真以後MSP和PC的值是多少(注意,此值不再是初始值,已經發生變化了)。進入Debug調試界面,然後打開Memmory視窗:
圖8.3.5. 8檢視位址
要注意,CM4核心是小端模式,是以讀取上面的位址參數的時候,要倒着來讀。0x0000 0000位址處的值是0x1002 0410,位址0x0000 0004的值是0x1000 00A1,即堆棧指針 SP =0x1002 0410,程式計數器指針PC = 0x1000 00A1(即複位中斷服務程式Reset_Handler的入口位址)。
當晶片上電後采樣到BOOT0、BOOT1和BOOT2引腳電平為001,位址0x00000000和0x00000004被映射到内部SRAM的首位址0x1002 0410和0x1000 00A1,核心從SRAM空間擷取内容。在實際應用中,由啟動檔案startup_stm32mp15xx.s決定了0x00000000和0x00000004位址存儲什麼内容,編譯後,在連結時,由分散加載檔案stm32mp15xx_m4.sct決定這些内容的絕對位址(分散加載檔案也叫做連結腳本),即配置設定SRAM的哪個位置。下面我們來看看這個連結腳本stm32mp15xx_m4.sct。
8.3.6 stm32mp15xx_m4.sct連結腳本
前面我們通過啟動檔案了解了系統複位後做了些什麼工作,但我們并不知道記憶體的配置設定資訊是怎樣的,當然很多時候我們不需關心這些,隻要確定程式能正常運作就可以。關于記憶體排布,我們這裡會介紹一個重要的檔案:連結腳本。
本小節中,在介紹連結腳本的時候,我們也會介紹和連結腳本關系比較重要的map檔案,本小節隻是作為一個了解性的内容,如感興趣可以了解一下,也可以跳過本小節。下面我們先來看看MDK編譯後生成哪些檔案。
1. MDK編譯生成檔案簡介
MDK 編譯工程,會生成一些中間檔案(如.o、.axf、.map 等),最終生成 hex 檔案,以開發闆CD光牒A-基礎資料\1、程式源碼\3、M4裸機驅動例程\MP157-M4 HAL庫V1.1\3 HAL庫跑馬燈實驗編譯結果為例子:
圖8.3.6.1MDK 編譯生成的檔案
可以看到,編譯後生成了很多個檔案,共 11 個類型,分别是:.axf、.crf、.d、.dep、
.hex、.lnp、.lst、.o、.htm、bulild_log.htm 和.map。這些檔案看着有很多,但是随着工程的增大,這些檔案也會越來越多,大項目編譯一次,可以生成幾百甚至上千個這種檔案,不過檔案類型基本就是上面這些。
對于 MDK 工程來說,基本上任何工程在編譯過程中都會有這 11 類檔案,常見的 MDK編譯過程生産檔案類型如表下表所示:
檔案類型 | 說明 |
.o | 可重定向1 對象檔案,每個源檔案(.c/.s 等)編譯都會生成一個.o 檔案 |
.axf | 由 ARMCC 編譯生産的可執行對象檔案,不可重定向2 (絕對位址) 多個.o 檔案連結生成.axf 檔案,我們在仿真的時候,需要用到該檔案 |
.hex | Intel Hex 格式檔案,可用于下載下傳到 MCU,.hex 檔案由.axf 檔案轉換而來 |
.crf | 交叉引用檔案,包含浏覽資訊(定義、辨別符、引用) |
.d | 由 ARMCC/GCC 編譯生産的依賴檔案(.o 檔案所對應的依賴檔案) 每個.o 檔案,都有一個對應的.d 檔案 |
.dep | 整個工程的依賴檔案 |
.lnp | MDK 生成的連結輸入檔案,用于指令輸入 |
.lst | C 語言或彙編編譯器生成的清單檔案 |
.htm | 連結生成的清單檔案 |
.build_log.htm | 最近一次編譯工程時的日志記錄檔案 |
.map | 連結器生成的清單檔案/MAP 檔案, 該檔案對我們非常有用 |
表 8.3.6.1 常見的中間檔案類型說明
注 1,可重定向是指該檔案包含資料/代碼,但是并沒有指定位址,它的位址可由後續連結的時候進行指定。
注 2,不可重定向是指該檔案所包含的資料/代碼都已經指定位址了,不能再改變。
2. 連結腳本
連結腳本路徑:Device\ST\STM32MP1xx\Source\Templates\arm\linker\stm32mp15xx_m4.sct。
圖8.3.6.2連結腳本路徑
在Device\ST\STM32MP1xx\Source\Templates\下的arm、gcc和iar下均有一個檔案夾linker,裡邊放的就是STM32MP1系列的連結描述檔案,其中,在STM32CubeIDE下,連結腳本為.ld檔案,在MDK中,連結腳本為.sct檔案,在IAR中,連結腳本為.icf檔案。
當建構工程的時候,MDK會按照我們選擇的晶片型号生成一個.sct的連結腳本(也叫分散加載檔案scatter file),連結腳本是用于描述檔案應該如何被連結在一起形成最終的可執行檔案的腳本,其主要目的是描述輸入檔案中的段(section)如何被映射到輸出檔案中,并且控制在輸出檔案中的記憶體排布。利用連結腳本我們可以控制代碼的加載區以及執行區的位置。
程式的編譯一般分為預處理、彙編、編譯和連結這4個步驟,我們在MDK上隻需點選編譯圖示就一次性完成了這4個步驟,其中的操作細節MDK已經通過層層封裝屏蔽掉了。在編譯過程中,編譯器将.c和.s源檔案編譯生成很多中間檔案(以.o、crf、.d等),這些中間檔案包含了隻讀資料段、代碼段、資料段、未初始化資料段等機器碼資訊,但是這些資訊是放在最終可執行檔案的哪個位置并沒有确定下來,于是,連結腳本會告訴連結器,把所有的中間檔案連結起來,并重定向它們的資料,然後連結生成可以被單片機運作的.hex檔案(多個.o 檔案連結生成.axf 檔案,.axf檔案也是一個中間檔案)。如果要生成.bin格式的檔案,隻需要通過格式轉換就可以完成。
圖8.3.6.3程式編譯過程
C語言程式編譯完成以後,編譯出來的代碼一般都包含text、data、bss 和 rodata 這四個段(section)。已初始化的全局變量儲存在.data 段中,未初始化的全局變量儲存在.bss 段中。text和data段都在可執行檔案中,程式運作的時候,由系統從可執行檔案中加載。而bss段不在可執行檔案中,由系統初始化并清零。四個段以及堆和棧的簡單說明如下:
常見段 | 讀寫 | 内容 |
text | 一般隻讀 | 代碼段,程式源代碼編譯後的機器指令會放在代碼段中,是用來存放程式執行代碼的一塊記憶體區域,這段區域的大小在編譯的時候已經确定。 |
data | 可讀可寫 | 存放程式中已初始化的全局變量或初始化為static的變量,屬于靜态記憶體配置設定。 |
bss | 可讀可寫 | 未初始化或初始化為0的全局變量的一塊記憶體區域,屬于靜态記憶體配置設定。 |
rodata | 隻讀資料 | 隻讀資料段,存放常量資料,比如程式中定義為const的全局變量或者#define定義的常量等。 |
表8.3.6.2代碼中各個段簡介
堆和棧 | 内容 |
堆 | 程式運作過程中被動态配置設定的記憶體段,動态配置設定資料,手動申請(malloc)和釋放(free)。 |
棧 | 棧又稱堆棧,存儲的是函數或代碼中的局部變量,也就是函數括弧“{}”中定義的變量(但不包括static聲明的變量,static意味着在資料段中存放變量)。 |
表8.3.6.3堆棧簡介
(1)連結腳本的格式
下面,我們來看看連結腳本裡做了些什麼工作,在介紹連結腳本時,我們先了解加載檔案的基本格式:
加載域1 加載域的基位址屬性加載域的空間大小
{
執行域名1 執行域基位址 執行域空間大小
{
描述
}
執行域名2 執行域基位址 執行域空間大小
{
描述
}
執行域名3 執行域基位址 執行域空間大小
{
描述
}
}
加載域名2 加載域的基位址 屬性 加載域的空間大小
{
執行域名1 執行域基位址 執行域空間大小
{
描述
}
執行域名2 執行域基位址 執行域空間大小
{
描述
}
}
一個連結腳本可以有多個加載域,如上面有兩個加載域1和2,加載域也就是初始化的區域。加載域裡邊的每個小括号都一個執行域,一個加載域可以包括幾個執行域。
加載域:
加載域是指編譯之後得到的二進制檔案燒寫到ROM或者RAM裡的區域,一般的程式中包含常見的幾個段:text、rodata、data和bss段,加載域實際上指定了程式的各個内容該如何放置在SRAM上。
①加載域的名稱用來辨別一個位址或者位址區域,可以在.map檔案中找到該辨別;
②加載域後面跟着的是加載域的基位址,加載域的基位址寫法裡可以使用一個’+’号來連接配接一個偏移位址:位址+偏移位址,最後得到結果就是加載域的基位址;
③屬性裡邊一般會描述多少個位元組對齊等資訊;
④加載域的空間大小位于加載域的基位址後面,如果配置設定到該區域的内容比設定的加載域的空間還要大的話,超出了位址範圍的話,編譯過程會給報錯提示。
執行域:
執行域的格式和加載域差不多,執行域的基位址可以等于/不等于加載域的基位址;
執行域的描述中,隻讀的代碼段和常量被稱作RO段(Read Only);可讀寫的全局變量和靜态變量被稱作RW段(Read Write);RW段中要被初始化為零的變量被稱為ZI段(Zero Init);XO為隻可執行的代碼段。
(2)連結腳本stm32mp15xx_m4.sct
下面我們打開stm32mp15xx_m4.sct檔案,其内容如下:
1 LR_VECTORS 0x00000000 0x00000400 { ;1KB
2 .isr_vector +0 { ;中斷向量表
3 startup*.o (RESET, +First) ; RESET放在0x00000000位置
4 }
5 }
6
7 LR_IROM1 0x10000000 0x00020000 {
8 ER_IROM1 0x10000000 0x00020000 { ;128KB
9 *(InRoot$$Sections)
10 .ANY (+RO) ;所有目标檔案的RO段
11 .ANY (+XO) ;所有目标檔案的XO段
12 }
13 RW_IRAM1 0x10020000 0x00020000 { ;128KB
14 .ANY (+RW +ZI) ;所有目标檔案的RW段和ZI段
15 }
16
17 ; ***** Create region for OPENAMP *****
18 ; *** These 4 lines can be commented if OPENAMP is not used ***
19 .resource_table +0 ALIGN 4 { ;表示4位元組對齊
20 *(.resource_table)
21 }
22 __OpenAMP_SHMEM__ 0x10040000 EMPTY 0x8000 {}
23 }
第1行,設定加載域為LR_VECTORS,基位址為0x00000000,大小為0x00000400(1KB);
第2行,執行域,.isr_vector +0表示中斷向量表,’+’後的0表示一個位址偏移為0;
第3行,startup*.o 表示目标檔案,啟動檔案中的中斷向量表放在最前面,(RESET, +First)中的RESET表示RESET段,用一個"+First"強行将啟動檔案中的RESET放在0x00000000位置,+First表示程式從RESET開始執行;
第7行,加載域為LR_IROM1,基位址0x10000000,大小為0x00020000(128KB);
第8行,執行域為ER_IROM1,基位址0x10000000,執行域大小0x00020000(128KB),這裡加載域位址=執行域位址,注意:執行域位址=連結位址;
第9行,*(InRoot$$Sections)表示一個選擇符号,是__main的代碼段(用于将加載域轉移到執行域),它的作用是存放 __main 到 main的這段代碼,主要将RW和ZI段加載到RAM中;
第10行,.ANY表示所有剩下的資料,這裡是将檔案所有剩下的隻讀資料(RO段) 連結到0x10000000這裡,配置設定的大小是0x00020000(但這個空間不一定會用完);
第11行,同理,将檔案所有剩下的隻執行段(XO段) 連結到0x10000000這裡,配置設定的大小是0x00020000(但這個空間不一定會用完);
第13行,執行域RW_IRAM1基位址0x10020000,位址大小0x00020000(128KB);
第14行,将檔案所有的可讀寫段(RW段)和被初始化為零的變量(ZI段)連結到0x10020000這裡,配置設定的大小是0x00020000(但這個空間不一定會用完);
第17~18行,注釋内容,表示為OPENAMP建立區域,如果不使用OPENAMP,則可以注釋以下這4行;
第19行,執行域,"+0"表示接着上一段"RW_IRAM1"的結尾,即0x10040000,繼續安排存儲區,ALIGN 4表示4位元組對齊;
第20行,資源表的内容放在這裡;
第22行,在執行區分散加載描述中使用 EMPTY 屬性,為堆棧保留一個空白記憶體塊,這段的意思是,OPENAMP開始于0x10040000這個位址,長度是正值,說明向高位址增長,大小是0x8000(32KB)。
以上的連結位址就是程式的執行位址,找到連結位址了就知道程式是在哪裡執行了。經過分析連結腳本,再結合前面提到的記憶體映射關系圖,我們知道,中斷向量表放在了0x0000 0000位址處,大小為1KB,這個範圍也就是在M4核心中斷向量表的範圍,對應前面記憶體映射關系圖中的RETRAM區域;隻讀資料(RO data)和代碼(RO code)以及隻執行段(XO段)連結到了0x10000000~0x10020000,大小為128KB,剛好對應記憶體映射關系圖中的SRAM1區域,存放的是代碼段(Code);所有目标檔案的RW段和ZI段連結到了0x10020000~0x10040000,大小為128KB,對應記憶體映射關系圖中的SRAM2區域,存放的是資料段(Data);0x10040000~0x10048000是OPENAMP的區域,此範圍落在了SRAM3中,ipc(Inter Process Communicaton)即程序通信,這個區域可以作為IPC緩沖區(IPC Buffers),也可以用于其它用途。如下圖所示:
圖8.3.6.4記憶體映射關系圖
從圖中看到還有一個SRAM4,該區域是用于做什麼呢?如果不跑Linux作業系統,隻是跑M4裸機程式的話,M4核心完全可以使用這部分區域,由使用者來指定。如果跑Linux作業系統,在Linux裝置樹下已經預設将SRAM4當做了Linux功能的DMA,如果要釋放這部分區域,在裝置樹下将對應節點删除釋放即可。
如果隻是M4跑裸機或者RTOS,不運作A7的話,這SRAM1~SRAM4可以全部配置設定給M4,那如果要同時運作M4和A7的話,這些位址配置設定就要注意了:M4并不是單獨占用SRAM3,具體占用多少需要根據Linux下的裝置樹配置來決定,在A7和M4雙核通信中,預設A7和M4共同占用SRAM3的0x10040000~0x10046000,這部分位址作為A7和M4通信的記憶體交換區,核心下的裝置樹如下:
圖8.3.6. 5裝置樹部分截圖
總之,如果在雙核通信中,M4的連結腳本要修改的話,核心源碼中的裝置樹也要修改,位址要保持一緻,不能有沖突,否則就會報錯,例如以上的裝置樹中,SRAM3的0x10040000~0x10046000位址是A7和M4共用了,如果在連結腳本中還将這部分位址當做其它用途,那就有可能報錯了。裝置樹的配置可以參考正點原子提供的裝置樹預設的配置,如果要自己修改為其它配置,可以根據以上的位址配置設定情況來配置設定,可自行修改和測試驗證。
綜合以上分析,總結如下:
如果不跑A7,隻運作M4(M4可以跑裸機、RTOS):SRAM1~SRAM4可以完全配置設定給M4;如果同時跑A7和M4(例如雙核通信):SRAM1和SRAM2是單獨給M4用的,SRAM3的部分位址是M4和A7一起使用的,SRAM4在Linux下單獨配置了DMA,即A7占用了。
3. MAP(地圖)檔案
為了更好的分析 map 檔案,我們先對需要用到的一些基礎概念進行一個簡單介紹,相關概念如下:
- Section:描述映像檔案的代碼或資料塊,我們簡稱程式段
- RO:Read Only 的縮寫,包括隻讀資料(RO data)和代碼(RO code)兩部分内容,占用 FLASH 空間
- RW:Read Write 的縮寫,包含可讀寫資料(RW data,有初值,且不為 0),占用 FLASH(存儲初值)和 RAM(讀寫操作)
- ZI:Zero initialized 的縮寫,包含初始化為 0 的資料(ZI data),占用 RAM 空間。
- .text:相當于 RO code
- .constdata:相當于 RO data
- .bss:相當于 ZI data
- .data:相當于 RW data
(1)map檔案的組成部分說明
我們前面說map 檔案分為 5 個部分組成,下面以STM32HMP157開發闆HAL庫例程的實驗3 HAL庫跑馬燈實驗 為例,先打開生成的.map檔案,然後簡要講解一下。
圖8.3.6.6生成的map檔案
(2)程式段交叉引用關系(S S ection Cross References s )
這部分内容描述了各個檔案(.c/.s 等)之間函數(程式段)的調用關系,舉個例子如下圖所示:
圖8.3.6.7程式段交叉引用關系圖
上圖中main.o(i.main) refers to delay.o(i.delay) for delay表示:main.c 檔案中的 main 函數,調用了delay.c 中的delay函數。其中:i.main表示 main 函數的入口位址,同理 i. delay表示delay函數的入口位址。
(3) 删除映像未使用的程式段(Removing Unused input sections from the image)
這部分内容描述了工程中由于未被調用而被删除的備援程式段(函數/資料),如下圖所示:
圖8.3.6.8删除未用到的程式段
上圖中,列出了所有被移除的程式段,比如stm32mp1xx_hal.c 裡面的HAL_Delay函數就被移除了,因為該例程沒用到HAL_Delay函數。
另外,在最後還有一個統計資訊:403 unused section(s) (total 37726 bytes) removed from the image.表示總共移除了403個程式段(函數/資料),大小為37726位元組。即給我們的 MCU 節省了37726位元組的程式空間。
為了更好的節省空間,我們一般在 MDK→魔術棒→C/C++頁籤裡面勾選:One ELF
Section per Function,如圖 8.3.6.9所示:
圖8.3.6.9 MDK 勾選 One ELF Section per Function
(4)映像符号表(Image Symbol Table)
映像符号表(Image Symbol Table)描述了被引用的各個符号(程式段/資料)在存儲器中的存儲位址、類型、大小等資訊。映像符号表分為兩類:本地符号(Local Symbols)和全局符号(Global Symbols)。
本地符号(Local Symbols)記錄了用 static 聲明的全局變量位址和大小,c 檔案中函數的位址和用 static 聲明的函數代碼大小,彙編檔案中的标号位址(作用域:限本檔案)。
全局符号(Global Symbols)記錄了全局變量的位址和大小,C 檔案中函數的位址及其代碼大小,彙編檔案中的标号位址(作用域:全工程)。
(5)映像記憶體分布圖(Memory Map of the image)
映像檔案分為加載域(Load Region)和運作域(Execution Region),一個加載域必須有至少一個運作域(可以有多個運作域),而一個程式又可以有多個加載域。如下圖是截圖的一部分,可以在.map中搜尋到多處加載域和執行域:
圖8.3.6.10加載域和執行域
加載域為映像程式的實際存儲區域,而運作域則是 MCU 上電後的運作狀态。加載域和運作域的簡化關系如下圖所示:
圖8.3.6.11加載域執行域關系
(6)映像元件大小(Image component sizes)
映像元件大小(Image component sizes)給出了整個映像所有代碼(.o)占用空間的彙總資訊。
由于篇幅較長,更多内容請大家查閱開發闆CD光牒A-基礎資料\4、參考資料\ 《MAP檔案淺析(正點原子)_V1.0》文檔的内容,文檔是基于STM32H750來分析的,放到STM32MP157下大部分知識點依然适用。
8.3.7 Include檔案夾
Include檔案夾下是符合CMSIS标準的核心頭檔案,我們在使用STM32CubeIDE建立工程的時候,系統會自動為我們添加這部分檔案。
圖8.3.7. 1檔案夾内容
在這些檔案中,以cmsis開頭的是和CMSIS編譯器相關的檔案,core開頭的是和 Cortex-M 核心相關的檔案, MPU開頭的是和MPU相關的檔案。普通的工程我們隻需要cmsis_compiler.h、cmsis_gcc.h、cmsis_version.h、core_cm4.h和mpu_armv7.h就可以了,如果是特殊的工程,則還會需要其它檔案,例如和TrustZone安全方面相關的工程,那就需要tz_context.h檔案。在這些檔案中,我們這裡稍微關注core_cm4.h 核心檔案,至于其它檔案,如果有想要深入學習核心的朋友可以配合核心相關的手冊去學習。下面,我們簡單介紹core_cm4.h這個檔案。
如下,我們看到core_cm4.h檔案包含了stdint.h檔案:
#include <stdint.h>
stdint.h是C99 (C語言規範)中引進的一個标準C庫的頭檔案,其定義了幾種擴充的整數類型和宏。現在編譯器對C99的支援已經做的很好了,大部分單片機C編譯器均支援C99标準,例如IAR、MDK和STM32CubeIDE等,linux 系統下的編譯器也支援。
stdint.h可以在MDK安裝目錄下ARM\ARMCC\include找到,stdint.h的作用就是提供了類型定義,其包含了_intsup.h和_stdint.h檔案。在今後的程式,我們都将會使用這些類型,比如:uint32_t(無符号整型)、int16_t等:
/* exact-width signed integer types */
typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed __INT64 int64_t;
/* exact-width unsigned integer types */
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned __INT64 uint64_t;
在core_cm4.h檔案中,我們還看到很多關于中斷相關的函數定義和類型定義,例如,開啟中斷函數NVIC_EnableIRQ、禁止中斷函數NVIC_DisableIRQ、設定中斷優先級分組函數NVIC_SetPriorityGrouping和中斷優先級函數NVIC_SetPriority,這些函數會在HAL庫中調用以實作中斷功能。此外,還有核心的外設相關定義,如SysTick實時系統核心時鐘相關寄存器和函數都在core_cm4.h檔案中定義。如下是中斷控制器(NVIC)類型定義。
圖8.3.7. 2中斷控制器(NVIC)類型定義
core_cm4.h檔案就介紹到這裡了,在這裡我們不對core_cm4.h檔案的内容做深入的講解,相關的介紹我們後面會結合實驗例程來加深了解。
8.4 章節小結
本章節洋洋灑灑地寫了幾十頁,并不是為了“拉長戰線和故意占用篇幅”, 實際上,要好好學習HAL庫,要分析的東西還不僅僅這些。大家都知道,ST提供的這個固件庫已經封裝好了,在開發中我們隻需要調用對應的API就可以實作想要的功能。不管它封裝的多好,本質上還是操作寄存器。我們在學習過程中,不能隻停留在了解的表面上,應該嘗試去了解它的本質上的東西,通過分析,我們可以了解它的架構,這有助于日後的學習和開發。
本章節主要對STM32CubeMP1固件包的架構以及CMSIS檔案夾中的部分重要檔案做了介紹,重點對我們後面會用的CMSIS檔案夾下的Device檔案夾以及Include檔案夾中的部分檔案做了介紹。
- 通過分析stm32mp1xx.h檔案,我們可以确定代碼中是否使用或者不使用某個底層驅動檔案。通過定義宏CORE_CM4、STM32MP157Dxx和USE_HAL_DRIVER,我們可以在工程中包含必要的頭檔案,如果換了另一款STM32晶片,我們同樣可以通過分析對應頭檔案來确定這些資訊。
圖8.4.1幾個宏定義
- 通過分析stm32mp157dxx_cm4.h頭檔案,我們知道了固件庫中對STM32MP157dxx系列器件的裝置資源采用結構體的形式進行了封裝,如果我們要通路某個寄存器,隻需要定義一個結構體指針,然後通過指針來讀寫對應的寄存器(結構體成員)就可以了,HAL庫中就是采用這樣的方式來操作外設的寄存器的。
- 通過分析system_stm32mp1xx.c檔案,我們認識了系統初始化函數SystemInit、系統時鐘更新函數SystemCoreClockUpdate和SystemCoreClock全局變量,同時也了解了怎麼開啟STM32MP1的硬體 FPU 功能。
- 通過分析startup_stm32mp15xx.s啟動檔案,我們知道了main函數并不是程式執行的第一段代碼。上電後,通過boot引腳設定可以将中斷向量表定位于起始位址0x0000 0000,同時複位後PC指針位于0x00000004位址處(Reset_Handler),Reset_Handler主要做了兩件事,一個是跳轉到SystemInit函數完成必要的系統初始化,另外一個是跳轉到main函數入口。
圖8.4.2系統啟動過程
- 通過分析stm32mp15xx_m4.ld連結腳本,我們知道了編譯好的輸入檔案中的每個段是如何被映射到輸出檔案中的,其中,text代碼段位于SRAM1,data資料段位于SRAM2。此外,我們還分析了HAL_LED_CM4.map地圖檔案和HAL_LED_CM4.list反彙編檔案,編譯生成的.elf檔案中的符号、位址和配置設定的記憶體的資訊都可以在地圖檔案中檢視。反彙編檔案可以輔助我們檢查代碼的缺陷,在實際項目開發中,這些檔案是非常重要的。
- Inclue檔案夾下主要是符合CMSIS标準的核心頭檔案,在M4裸機開發中,我們主要用的是core_cm4.h檔案,此檔案中主要是關于中斷相關的函數定義和類型定義,還有核心的外設相關寄存器的定義,例如核外設SysTick。