第八章 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进行搜索。
图8.1.1搜索STM32CubeMP1固件包
在开发板光盘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文件中有介绍。
下面,我们看看开发板光盘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 文件,以开发板光盘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)占用空间的汇总信息。
由于篇幅较长,更多内容请大家查阅开发板光盘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文件中的符号、地址和分配的内存的信息都可以在地图文件中查看。反汇编文件可以辅助我们检查代码的缺陷,在实际项目开发中,这些文件是非常重要的。
图8.4.5 M4内核可用的SRAM
- Inclue文件夹下主要是符合CMSIS标准的内核头文件,在M4裸机开发中,我们主要用的是core_cm4.h文件,此文件中主要是关于中断相关的函数定义和类型定义,还有内核的外设相关寄存器的定义,例如核外设SysTick。