
STM32Cube固件包 第四章我们通过STM32CubeIDE在线下载了一个STM32Cube固件包,STM32CubeMX插件就是利用这个固件包来生成初始化代码的,大家肯定好奇这个固件包里有什么?是做什么用的?和HAL库有什么关系?本章节我们就来分析这个固件包。 本章将分为如下几个小节: 0 F1 ~* ?% n( j9 P _8 s- Q) I 6.1 获取STM32Cube固件包 ?, T4 V( i( Z( x5 @) v! j STM32Cube是ST公司提供的一套免费的开发工具和STM32Cube 固件包,覆盖了整个STM32产品,可在STM32平台上进行快速轻松的开发,从而简化了开发人员的工作。STM32Cube由以下组件组成,这些组件可以一起使用或独立使用:- u9 @$ O" r& h 允许用户通过图形化向导来生成C语言工程的图形配置工具STM32CubeMX。/ A) I5 g5 z; V6 K 适用于每个STM32 MCU和MPU系列的STM32Cube MCU和MPU软件包(也叫STM32Cube 固件包或者STM32Cube包)。 在前面STM32CubeIDE第一个工程实验的创建工程环节,STM32CubeIDE已经自动在C:\Users\自己的用户名称\STM32Cube\Repository\STM32Cube_FW_MP1_V1.2.0路径中下载好了STM32Cube_FW_MP1_V1.2.0包,这个包就是STM32MP1的固件包,此固件包也可以从ST官网上下载,目前最新版本是1.2.0版本。8 s, w5 ?6 f1 e4 G! { 8 j- C2 i7 C+ _ ![]() 图6.1. 1搜索STM32CubeMP1固件包! |( c5 j/ F/ }7 @: ] 在开发板光盘A-基础资料\7、STM32MP1参考资料\STM32MP157 Cube包中我们也有提供STM32MP1的固件包:3 e6 k, x! ? W: g# f: c0 T ![]() 图6.1. 2A盘中下载好的固件包 我们打开Drivers文件夹,看到的STM32MP1xx_HAL_Driver就是HAL库。' z$ g. R K0 W1 E y ![]() 图6.1. 3 HAL库 6.2 STM32CubeMP1固件包目录结构. ^3 [" I% w% V) M% b) L 接下来,我们看看前面下载好的STM32CubeMP1固件包目录结构,打开STM32Cube_FW_MP1_V1.2.0固件包,目录结构如下图。, r0 o+ b/ T' } I! I4 t) w ![]() 图6.1. 4固件包目录结构 _htmresc文件夹下是ST公司的LOGO图片和一些网站的资料,其实是用不到的,我们不去关注。对比较重要的文件夹,我们按照顺序进行介绍:( x0 f! y N3 M; Y3 H, h0 ?( C, N 6.2.1 Drivers文件夹 Drivers文件夹包含BSP,CMSIS和STM32MP1xx_HAL_Driver三个子文件夹。三个子文件夹具体说明请参考下表/ U5 S1 b$ e7 @1 l: ] a' E BSP 文件夹 BSP也叫板级支持包,此支持包提供的是直接与硬件打交道的API,例如触摸屏,LCD,SRAM以及EEPROM等板载硬件资源等驱动。目前在STM32cubeMP1固件包中,ST还未添加这部分内容(目前有LED、COM端口以及按钮相关的API),后期ST应该会逐渐添加这些文件。% U" Y% Y( t) b/ x0 r2 J: E# b BSP文件夹下还给了ST官方DISCO和EVAL开发板的硬件驱动API文件,每一种板对应一个文件夹。可以打开开发板文件夹,根据里边帮助文档查看API文件都有什么内容 CMSIS 文件夹 CMSIS文件夹用于存放符合CMSIS标准的文件,包括STM32启动文件、ARM Cortex内核文件和对应外设头文件。关于CMSIS文件夹里的文件,我们后面会专门讲解。4 U X/ q9 i9 @3 D Core 用于Cortex-M处理器内核和外围设备的API2 V' e* o1 Z( g3 Q% a Core_A 用于Cortex-A5 / A7 / A9处理器内核和外围设备的API/ @! c& W8 x, d) M' ]3 B9 K Device 微控制器专用头文件/启动代码/专用系统文件4 O9 L6 r/ ?& G% m# R) t DSP 适用于各种数据类型的DSP库集合 Include STM32MP1xx外围设备访问层头文件 Lib ARM、GCC 和 IAR格式的 DSP 库文件 NN 神经网络库集合,目的是在Cortex-M处理器内核上最大化神经网络的性能并最小化其内存占用 RTOS 实时操作系统通用API相关文件(V1版本),兼容RTX48 g. Z: @; Z( c. y RTOS2 对RTOS V1的拓展,兼容RTX5 STM32MP1xx_HAL_Driver文件夹 HAL库文件夹,处理STM32“内部”设备,它包含了所有的STM32MP1xx系列HAL库头文件和源文件,也就是所有底层硬件抽象层API声明和定义。它的作用是屏蔽了复杂的硬件寄存器操作,统一了外设的接口函数。该文件夹包含Src和Inc两个子文件夹,其中Src子文件夹存放的是.c源文件,Inc子文件夹存放的是与之对应的.h头文件。每个.c源文件对应一个.h头文件。在前面的STM32CubeIDE第一个工程实验中就有用到该文件夹的文件,我们后面会重点介绍该文件夹的文件。! \2 z% F7 B: {8 \& |4 I9 y 4 H7 R6 k" o1 _, ~% E# r ![]() 表6.2.1. 1 Drivers文件夹简介+ y. Y4 g1 t9 K% A+ B 6.2.2 Middlewares文件夹 Middlewares(中间件)文件夹下目前只有Third_Party文件夹,是提供一组服务的库,目前里边只有FreeRTOS实时系统支持包和OpenAMP文件夹。" Q e% g; p. b+ @' _$ H% }$ n5 x! p ![]() 图6.2.2. 1 Middlewares文件夹 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版本。* J9 w' ^. k. E2 P( l& z, M" u : _; d( f) c1 q* ? ![]() 图6.2.2. 2 FreeRTOS支持包 AMP是指非对称多处理, 非对称多处理是指各核的结构并非对称,例如STM32MP1是两个Cortex-A7内核加一个Cortex-M4内核的组合,各个核结构并非对称。OpenAMP常用于处理器间通信,OpenAMP软件框架为开发AMP系统提供了必要的API函数,可以实现核间通信。 6.2.3 Projects文件夹 该文件夹存放的是一些可以直接编译的实例工程,是STM32MP1xx系列的STM32CubeMP1固件示例。每个文件夹对应一个ST官方的Demo板。比如我们要查看STM32mp157相关工程,我们直接打开子文件夹STM32MP157C-DK2即可,里面有很多实例供我们参考。每个Demo板下都会有以下4个文件夹:/ ~) k- }/ }& N U: [1 W Applications: OpenAMP、FreeRTOS和CoproSync应用程序示例。 Demonstrations:AI相关示例。( U E' i4 U" o Examples:外围设备的功能和用法示例。9 K% I: [7 s$ e+ x Templates:固件库工程模板,允许用户在给定的板上快速构建任何固件应用程序。0 P6 L9 g( Y" [3 Z* _) a2 B8 E 我们查看其中的示例的时候,工程下面有MDK-ARM和STM32CubeIDE子文件夹,双击MDK-ARM子文件夹内部的Project.uvprojx的工程文件,可以在MDK中打开工程,双击STM32CubeIDE子文件夹下的.project工程文件,可以在STM32CubeIDE中打开工程。 ![]() 7 R- e2 T+ d: `1 E3 f* h2 q) h 图6.2.3. 1 STM32CubeIDE工程文件: |$ @! t/ P) m6 p2 \7 d: _+ m+ a 关于Projects文件夹的整体介绍,可以打开里边的STM32CubeProjectsList.html文件了解更加详细内容。在查看工程文件的时候,可以打开里边的readme.txt查看介绍内容。5 o( K5 l+ _# a9 F: J7 Q- p# Q9 H 6.2.4 Utilities文件夹" z- T7 k" S# z* J2 C6 X7 v 该文件夹中的文件介绍了如何配置STM32MP1xx的资源管理器,例如文件中提供了共享内存中的虚拟表地址、在ETZPC控制下的设备寄存器地址表、共享资源ID等,这些文件由ST官网提供,一般不能修改文件中的内容。 6.2.5 其它文件 Readme.md简单介绍STM32CubeMP1固件文件的内容。Release_Notes.html文件是固件库版本更新说明,关于STM32CubeMP1固件版本详细更新内容,我们可以查看此文件。License.md和package.xml文件只是协议说明和固件包版本的说明,不用怎么管。 ( Z+ B8 {% j" f- F2 e& `$ B 6.3 CMSIS文件夹关键文件介绍 随着32位处理器在嵌入式市场需求量逐渐增多,各家芯片公司推出新型芯片,伴随而来的是开发工具、软件兼容以及代码移植等问题。在这种情况下,各个硬件平台的供应商都寻求易于使用且高效的解决方案,其中,ARM与Atmel、IAR、KEIL、SEGGER和ST等诸多芯片和软件工具厂商合作,发布了一套CMSIS标准。% Z, c) m0 H0 N4 d$ ?7 n! m CMSIS(Cortex Microcontroller Software Interface Standard),即ARM Cortex微控制器软件接口标准。CMSIS标准提供了内核和外围设备、实时操作系统和中间组件之间的通用API接口,从而简化了软件的重复使用,缩短了微控制器开发人员的学习时间,并缩短了新设备的上市时间。下图是ARM公司的CMSIS标准结构框图: + \3 O- R3 v& Q* G7 k ![]() 8 j% c6 J- j$ \3 T 图6.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版本 基于ARM提供的CMSIS 软件包模板,ST官方结合自己芯片的差异进行了修改,并将其整合到了STM32Cube固件包中的CMSIS文件夹里。8 C2 i+ P) G; M" w( K$ y1 { 打开固件包中STM32Cube_FW_MP1_V1.2.0\Drivers\CMSIS目录,其中,Device文件夹和Include文件夹是每个工程都要用到的。 4 Q6 ]$ P) f% ^7 _9 d9 z ![]() 图6.3. 2 CMSIS目录5 z6 x7 z! T7 G: w# R. p; O Device文件夹下是具体芯片直接相关的文件,里边是ST官方的STM32MP1xx器件专用的头文件、启动代码文件和专用系统文件,此文件夹下我们重点介绍这几个文件:stm32mp1xx.h、system_stm32mp1xx.c、startup_stm32mp15xx.s和stm32mp15xx_m4.ld文件。 [6 I4 ?% X( X" h0 G5 K6 O5 ~ Include文件夹下是符合CMSIS标准的内核头文件,主要是核内外设文件,我们会重点介绍core_cm4.h文件。 6.3.1 stm32mp1xx.h文件 文件路径:Device\ST\STM32MP1xx\Include\stm32mp1xx.h; @6 W* [- [$ A$ h- G stm32mp1xx.h文件在工程中是一定要有的,文件的内容看起来不多,却非常重要。该文件主要就是确定代码中是否使用或者不使用某个底层驱动文件,我们简单分析stm32mp1xx.h文件。抛开文件中的主体代码,如下代码是笔者将主体部分代码删掉以后所看到的整体框架,其它的头文件框架也类似: 3 j4 a3 A! c9 n( z
学过C++的朋友应该很熟悉,上面第4行到第12行代码是为了在C++中尽可能的支持C代码和C库。意思是,如果这是一段C的代码,那么加入"extern "C"{" 和 " }"处理其中的代码,因为C++和C对产生的函数名字的处理是不一样的,C++中存在重载,C中没有重载,为了在C++代码中调用C写成的库文件,就需要用extern"C"来告诉编译器:这是一个用C写成的库文件,请用C的方式来链接它们。如果不这样处理,在C++中编译后会出现链接错误。这么做其实也是为了方便代码移植。 下面我们分析stm32mp1xx.h文件的主要代码实现部分。& \; O" G( ~7 f( @' _, T
如上图代码,大部分是一些条件编译,如果条件编译的宏有被定义,那么就参加编译。我们先看第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文件中定义这个宏。) m5 u1 [& C1 e& F9 f q4 W6 n7 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处理器和核心外设的配置,例如中断号定义、外设寄存器结构体声明、外设寄存器位定义和寄存器的操作的宏定义以及外围设备内存映射等等。- r" V8 I6 a. S( w9 M4 f. g5 S6 k5 e 我们接着往下看后面的代码。 2 N5 j3 `- T- N9 k; I+ W) O
第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。 我们查看最后的代码: . \2 v, H8 i j4 p+ q8 l; m! h
第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库的头文件集,一旦使用了相应的模块,就要定义相关的模块使能,然后相应模块的头文件才会被包含。) e/ z- Z3 t1 ~/ j3 Z* G5 Y/ q stm32mp1xx.h文件内容就这么多,经过前面的分析,如果要操作CM4的外设,我们需要定义CORE_CM4、STM32MP157Dxx和USE_HAL_DRIVER这3个宏定义,这3个宏定义在哪里定义呢?如果是用MDK来编译,点击Keil的魔术棒,在C/C++配置栏的Preprocessor Symbols(预处理器符号)的Dfine(定义)处加上CORE_CM4,USE_HAL_DRIVER,STM32MP157Dxx就可以了(注意,用英文格式的逗号隔开)。( {% ~5 C$ M1 H9 _3 w0 C- A+ ], Q3 \ ![]() 1 S/ g3 }0 v' z, Z% e2 U: ]2 G2 @ 图6.3.1. 1 MDK上添加宏* K2 a3 _! B1 L+ X& K- `. L 如果是用STM32CubeIDE来编译,我们不需再进行配置,STM32CubeIDE已经自动为我们配置好了,不过我们还是需要知道在哪里配置。打开STM32CubeIDE第一个工程的工程文件,选中HAL_LED_CM4(in CM4)工程,右键选择Properties,打开CM4工程属性以后,找到Paths and Symbols下的Symbols选项,里边的就是符号定义。& R3 ]; t0 Y+ Z0 Q8 E T' V/ z( I ![]() 图6.3.1. 2CubeIDE上添加宏" G) i# v `1 @5 | 也可以在C/C++ BuildSettingsTool SettingsMCU GCC CompilerPreprocessor中看到添加了哪些宏定义。 ![]() / c- w" ~- P- V 图6.3.1. 3查看添加了那些宏 6.3.2 stm32mp157dxx_cm4.h4 `* ?7 Q& ]- w! J3 i9 @& l/ Y1 [ 文件路径: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等等)的设备资源定义,例如外设中断号定义、外设寄存器结构体声明、外设寄存器位定义和寄存器的操作的宏定义以及外围设备内存映射等等。! }8 {7 k! n0 O Q 里边使用了大量的结构体来对寄存器进行封装,如果我们要访问某个寄存器,只需要定义一个结构体指针,然后通过指针来读写对应的寄存器(结构体成员)。下面我们以GPIO为例子介绍:
这段代码中,typedef是类型定义以及结构体定义的基本语法,我们在前面5.1.5小节和5.1.6小节有讲解。__IO表示volatile ,在core_cm4.h文件中有定义。其中,结构体成员MODER、OTYPER、和SIDR这些是GPIOx(x等于A~K和Z)对应的寄存器名称。- \$ ]: U# Q" Y* g4 Q% ?, j 这里,每个结构体成员均定义为uint32_t,即相邻每个成员偏移4个字节,寄存器MODER偏移地址为0x000,寄存器OTYPER偏移地址为0x004,以此类推。4 b$ y% K: r7 P- g 通过结构体,我们知道了偏移地址,要确定一个寄存器的实际地址,我们还需要知道基地址。通过参考手册我们知道GPIOI挂在了AHB总线上,且AHB总线的基地址是0x50000000, GPIOI的基地址就是0x5000A000,这个基地址在代码的哪里定义了呢?. }" g0 t9 [7 y) P- p* T' @! F ![]() 图6.3.2. 1参考手册部分截图" ?! b6 g- g( |; x+ l3 v; g 也是在stm32mp157dxx_cm4.h头文件中可以找到如下的代码: y3 d5 e3 s* s7 h/ K% O3 c
这部分代码是内存映射相关的宏定义。如上代码,第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;; `& t3 \4 u' {5 E7 w }2 ? 上面,GPIOI->ODR也可以改写为(*GPIOI).ODR。这段代码表示将GPIOI中的ODR寄存器赋值为0XFFFF。( Y- L; |- {7 U8 q& a! G, \ 实际上,在HAL库中很多函数里就是这么用的,例如在HAL库的stm32mp1xx_hal_gpio.c文件中,就有很多这样的代码: ![]() 图6.3.2. 2 HAL库函数部分截图 6.3.3 stm32mp157dxx_ca7.h文件 文件路径:Device\ST\STM32MP1xx\Include\stm32mp157dxx_ca7.h% O1 S" P, ]8 A& S, | 和stm32mp157dxx_cm4.h文件类似,只不过是对Cortex-A7处理器和核心外设的配置。 6.3.4 system_stm32mp1xx.c文件 文件路径: Device\ST\STM32MP1xx\Include\system_stm32mp1xx.h# Z. x, X: v& H( E Device\ST\STM32MP1xx\Source\Templates\system_stm32mp1xx.c2 H# i: @& K5 i* @3 a" N1 G: H 这两个文件提供了两个函数和一个全局变量:系统初始化函数SystemInit、系统时钟更新函数SystemCoreClockUpdate和SystemCoreClock全局变量。 SystemInit函数在系统复位后,在跳到主程序main.c之前被startup_stm32mp1xx.s文件调用。SystemInit函数中主要是初始化FPU设置、配置SRAM中的向量表和禁用所有中断和事件。我们简单分析一下代码。8 c8 \/ l. K9 ~/ ^: q
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就可以了。! h6 f. b. U# d0 ?; o9 G ![]() G; V0 q" e0 T b r5 `7 R 图6.3.4. 1 MDK中开启FPU' L; z& B' R( v- q- X 如果使用的是STM32CubeIED的话,可以打开STM32CubeIDE第一个工程的工程文件,选中HAL_LED_CM4(in CM4)工程,右键选择Properties,打开CM4工程属性,找到C/C++ Build SettingsTool SettingsMCU Settings,可以看到系统已经自动为我们设置好了支持FPU了。Floating-point ABI选择的是硬件浮点单元,浮点运算处理方式为FPv4-SP-D16,其中d16表示有16个64位的单精度寄存器,指令集选的是Thumb2。最后我们看到Use float with printf from newlib-nano(-u _printf_float) 和Use float with printf from newlib-nano(-u_scanf_float)选项,这两个选项通常是用于串口打印的时候设置的,在串口实验中设置这两项以后,串口支持浮点类型数据打印。2 C6 {% r; g: P- ~$ g0 S/ x* Y$ b ![]() 图6.3.4. 2 CubeIDE中设置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以屏蔽中断请求,即禁用所有中断和事件。/ R q1 W; Y1 B% J o: c3 a 接下来我们查看SystemCoreClockUpdate函数。SystemCoreClockUpdate函数的代码比较多,注释也比较详细,为了不占用篇幅,我们这里省略部分代码:5 I7 @4 z9 ^& g& f
根据注释,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的时钟(在前面STM32CubeIDE第一个工程章节有介绍)。 SystemCoreClockUpdate函数的作用就是,根据时钟寄存器的值来更新SystemCoreClock变量。SystemCoreClock变量包含核心时钟频率(HCLK),用户应用程序可以使用它来设置SysTick定时器或配置其他参数。在程序执行期间,每次内核时钟改变时,都必须调用SystemCoreClockUpdate函数来更新SystemCoreClock变量值,如果不这样,SystemCoreClock变量值将会不准确,任何基于SystemCoreClock变量的配置都是不正确的。这么做也就是为了保证SystemCoreClock的准确性。 时钟部分在STM32中比较复杂,也不是三言两语能说的清楚,我们后面会分出专门的章节来讲解,并结合对应的实验来加深理解。 8 y# C, A- n. w; v3 b 6.3.5 startup_stm32mp15xx.s文件 1.启动文件在哪 文件路径:Device\ST\STM32MP1xx\Source\Templates\gcc\startup_stm32mp15xx.s: M, ]9 T: V- Y startup_stm32mp15xx.s是由ST官方提供的,一般直接拿来用,有需要的时候才会改写。它主要是用汇编语言编写,是系统上电后第一个运行的程序文件,属于启动文件。Device\ST\STM32MP1xx\Source\Templates下面有3个文件夹,每个文件夹下均有一个startup_stm32mp15xx.s文件,不同的开发环境使用不同文件夹下的startup_stm32mp15xx.s文件,STM32CubeIDE软件使用的是gcc下的文件,MDK软件使用的是arm下的文件,每个文件夹下的文件内容均不相同,但是他们的功能是一样的。 d9 t1 J% \0 T/ Y& w ![]() & [, e$ n$ d7 G 图6.3.5. 1 Templates文件夹 2. 启动文件中的部分指令& `. N/ ?9 Q% h 在分析启动文件前,我们先来了解几个汇编语法: ![]() 表6.3.5. 1部分汇编指令' O9 B4 ?6 W8 r% o2 o9 S j- d& R' E 3. 启动文件分析 g& J+ O! E6 m/ Q g" y6 _: q5 X 上表列举了STM32启动文件的一些汇编和编译器指令,关于其他更多的ARM汇编指令,大家可以查阅汇编语法的书籍。下面,我们借助文件中的注释,我们来分析一下startup_stm32mp15xx.s文件做了些什么工作。 1)设置栈指针SP; 2)设置初始PC= Reset_Handler; 2)设置中断向量表入口地址,并初始化向量表; 3)初始化.data 和 .bss 段; 4)跳转到C库中的main(最终调用main函数)。8 F, Q5 o+ y; p. k' V0 P 在线程模式下复位了Cortex-M处理器后,优先级为Privileged(特权模式),栈顶设置为主函数。 我们将启动文件startup_stm32mp15xx.s的代码分成几个小段,简单分析一下代码实现的功能,代码中已经附上详细的注释,大家可以根据注释大概了解启动文件的整个工作过程。
第1行,.syntax命令是ARM架构独有的命令,指定按照怎样的语法规则进行汇编,.syntax unified表示下面的指令是ARM和THUMB通用格式。 第2行指定处理器为Cortex-M4。9 w* a4 u/ v, q$ c! O/ n$ I 第7和第8行定义两个全局符号,g_pfnVectors和Default_Handler。# D8 D+ R0 Y& e/ ^$ j7 h7 F 第14到18行,给出data段与bss段起始与结束地址。data段用来存储已经初始化的全局变量,bss段用来存储未初始化的全局变量。 第25行,表示标志Reset Handler符号的位置,Reset_Handler(复位中断函数)就是复位之后首先执行的那段代码。接下来我们查看Reset_Handler都做了些什么。 8 i9 P- t+ G* `7 z
以上就是Reset_Handler所做的工作,包括data段与bss段初始化过程。7 z) C9 Z6 f0 o' ~( M# s 第39行,转移到SystemInit函数起始处,SystemInit函数是在system_stm32mp1xx.c文件中定义的,它在主程序main.c执行之前被startup_stm32mp1xx.s文件调用,主要作用就是初始化FPU设置、配置SRAM中的向量表和禁用所有中断和事件,我们在前面已经有介绍。7 x( X2 K- i5 _% y5 Q 第47行,在调用SystemInit函数以后,跳转到main函数中。 到这里终于明白了,原来,Reset_Handler所做的工作就是:先跳到SystemInit函数起始处,执行完SystemInit函数以后再跳转到main函数处,这么说,main函数并不是程序执行的第一段代码,只能说,main函数是应用程序的入口函数。
第4到第8行,表示Default_Handler是一个无限空循环。 第14行表示定义了一个中断向量表的段,该段可分配,段内包含数据,这个表将会放置在地址为0x0000 0000处(也就是堆栈顶的地址),Cortex-M4复位后从此处取出数据用于初始化MSP寄存器。地址为0x0000 0004的表示复位向量(哪里得来的这些地址的?下面会结合一个Cortex-M4内核的中断映射表来讲解)。 第21到40行,定义了一个段来存放中断向量表,然后以字的形式分别填入了中断的指针。/ Y. v' f: k+ c 第49到59行,startup_stm32mp15xx.s文件中已经帮我们写好所有中断的中断服务函数了,不过这些中断服务函数都是空的,什么也不运行,即无限空循环。真正的中断服务函数需要我们自己去实现。其中,中断服务函数前面定义了一个弱(weak)符号,弱,就是表示此函数可以进行重写(重新定义),表示如果用户在其它地方重新定义一个同名函数,最终编译器编译的时候,就会选择用户定义的函数,如果用户没有重新定义这个函数(或者函数名字写错了),那么编译器就会默认执行带有弱符号的函数,并且编译器不会报错。带有弱符号的函数都可以进行重写。 什么意思呢,例如我们看到有很多类似的代码,如这部分代码:4 m5 ^" F* d2 J: W7 F .weak UART4_IRQHandler" L" \- k. ]9 {! o .thumb_set UART4_IRQHandler,Default_Handler .weak UART4_IRQHandler表示有一个中断处理函数,它的别名是UART4_IRQHandler,在前面有一个词weak(弱),表示此中断函数UART4_IRQHandler可以被用户进行重写(必须正确重写才会有效),重写的函数代替了这个函数。9 V+ h+ B) g/ L' l0 Q Q5 R .thumb_set UART4_IRQHandler表示如果不重写UART4_IRQHandler函数的话,那么默认执行Default_Handler函数,也就是执行死循环。weak的作用其实是为了防止用户使能了中断而没有编写中断服务函数,从而造成程序崩溃。$ k1 {# d2 E5 s' a 例如用户开启了串口4中断,根据前面中断向量表得出此中断服务函数名字为UART4_IRQHandler,如果用户只是开启中断,并没有去按照中断向量表给的中断函数名UART4_IRQHandler重写一个对应的中断处理函数(或者把中断函数名字写错了),那么,中断开启以后,系统默认执行Default_Handler,也就是一直执行死循环。如果按照中断向量表给的中断函数名UART4_IRQHandler重写了一个串口4的中断服务函数,那么中断向量表中的处理函数的地址就会更新为用户写的那个函数的地址了,即执行用户写的串口4中断函数,不会进入死循环。这点我们要注意,后面在中断有关实验章节会进行讲解怎么编写中断服务函数。3 h! T6 m7 \# O 从第26到第40行是STM32MP157内部指定的中断向量表,我们也可以通过查看《STM32MP157参考手册》来了解Cortex-M4内核的中断映射关系,STM32MP157的M4内核中断管理器叫做NVIC,其系统中断(也叫内部中断)有10个,外部中断有150个,下图只是截图了一部分。 从表中了解到,地址0x0000 0000 是保留的,但其实是reset后MSP(主堆栈指针)的地址,Reset 中断的地址为0x0000 0004,NMI中断的地址是0x0000 0008。M4的中断映射范围0x0000 0000~00000x00000294。表中,priority 一列表示中断优先级,参数越小表示中断优先级越高。Fixed表示此中断优先级是固定的,不可更改,Settable表示中断优先级是可编程的,可以通过编程来更改。Acronym一列表示中断的名称,Description表示中断的说明,Address表示中断的地址。 ; j+ C1 B% ]3 S6 ` ![]() , F7 L7 @2 g# i 图6.3.5. 2参考手册部分截图6 v" d% X1 r3 p+ F 根据上表了解到,M4内核的中断向量表是从地址0x0000 0000开始的,位于BOOT区的RETRAM(64kB),我们在用MDK或者STM32CubeIDE来调试程序的时候,M4的代码其实是放到了SRAM中运行了,其中M4可运行的SRAM是SRAM1(128kB)、SRAM2(128kB)、SRAM3(64kB)和SRAM4(64kB),地址范围是0X10000000~0X1005FFFF,共384KB。如下的内存映射表可以清楚的看出内存映射关系: 4 p) n/ M# g# e9 v S! [7 J7 \# b ![]() 0 l+ w9 s, X& l 图6.3.5. 3内存映射关系图 讲到这里发现还有一个疑问,在startup_stm32mp15xx.s文件中并没有看到有关设置堆和栈大小的代码,它是在哪里设置的呢?我们知道,栈一般是存放函数的参数值和局部变量的值,由编译器自动分配释放,而堆用于存放进程运行中被动态分配的内存段,一般由程序员分配和释放。若工程中使用的局部变量较多,定义的数据长度较大时,如果不调整栈的空间大小,会导致程序出现栈溢出,程序运行异常。在STM32CubeIDE中,堆和栈是在Project Manager配置窗口进行配置的。如下图,默认堆512B,栈1KB,用户可以在此处设置堆和栈的大小。 ; \1 F( r0 S# G7 p0 v$ d7 P ![]() 3 Y Z1 j6 X" Y+ Q6 w/ T9 N+ p3 L 图6.3.5. 4 CubeIDE中设置堆栈( o. s6 \5 A$ v# j9 Y2 q 下面我们来捋一下启动文件的工作过程: 上电复位后,硬件会自动根据向量表偏移地址找到向量表,首先从0x0000 0000地址处加载初始MSP,然后从偏移为4的地址(0x0000 0004)处加载PC,0x0000 0004地址处存放的是Reset_Handler,即执行复位中断服务程,Reset_Handler主要做了两件事,一个是跳转到SystemInit函数完成必要的系统初始化,另外一个是跳转到main函数。然后,如果有中断发生,如果此中断对应的中断服务函数没有被用户重写,则系统进入无限空循环,如果此中断对应的中断函数被用户重写了,则执行用户重写的中断服务函数。; Y+ d4 k' u" }2 o- p9 h% G B 4. 系统启动流程6 F- R/ d5 a6 G CM4内核启动,需要将拨码开关BOOT0、BOOT1和BOOT2设置为001,这个是芯片设计的时候就已经定好了的。STM32MP157 支持从多种不同的设备启动,通过设置拨码开关可以选择从指定的设备启动,启动方式如表:8 S) Z! U5 [" l$ U& F ![]() 表6.3.5. 2 STM32MP157启动模式 正点原子 STM32MP157 开发板上支持 USB、SD 卡、EMMC 以及 M4 内核这 4 种启动方式。& N2 L# ]) P/ u }' O 我们知道启动模式不同,启动的起始地址是不一样的,例如STM32F4系列的芯片,CM4内核有可用的FLASH,代码下载到内部FLASH时,代码从地址0x0800 0000开始被执行的。当产生复位,并且离开复位状态后,CM4内核做的第一件事就是读取下列两个32位整数的值: (1)从地址 0x0800 0000 处取出堆栈指针MSP 的初始值,该值就是栈顶地址。6 j- r! L& _3 b( \3 w" m* l (2)从地址 0x0800 0004 处取出程序计数器指针PC的初始值,该值指向中断服务程序 Reset_Handler。下面用示意图表示,如下图所示。 ![]() 图6.3.5. 5 STM32F4 FLASH启动+ l; }. Y7 }/ `! ]8 {) p 换做STM32MP157,因为CM4内核没有可用的FLASH,所以在MDK或者STM32CubeIDE上仿真的时候,是将程序放到了SRAM中运行了。根据前面的分析,开发板从MCU启动,当产生复位,并且离开复位状态后,CM4内核做的第一件事:( i/ W1 e6 |, n+ B (1)位于BOOT启动代码区RETRAM(64kB)的地址 0x0000 0000 处取出初始堆栈指针MSP 的初始值,该值就是栈顶地址。$ V0 |% U W3 S" h5 V- N! N (2)从地址0x00000004 处取出程序计数器指针PC的初始值,该值指向中断服务程序 Reset_Handler。下面用示意图表示,如下图所示。 ![]() 图6.3.5. 6 STM32MP1 M4内核启动 上述过程中,内核是从0x0000 0000和0x0000 0004两个的地址获取堆栈指针MSP和程序计数器指针PC。事实上,0x0000 0000和0x0000 0004两个的地址可以被重映射到其他的地址空间,因为可以通过修改定义宏VECT_TAB_SRAM以及修改向量表偏移VECT_TAB_OFFSET来实现,前面在system_stm32mp1xx.c文件中有介绍。 下面,我们看看第一个工程实验,在STM32CubeIDE上仿真的时候,MSP和PC的值是多少(注意,此值不再是初始值,已经发生变化了)。 W% H# {, b$ A% f8 d 进入Debug调试界面,然后打开Memmory窗口: n! E# y e- C ( K) Q) y7 t y: B ![]() k" ^# Y; c4 D& f 图6.3.5. 7打开Memmory窗口% n. {. z9 m2 S% m8 L 添加观察地址0x00000000:4 ?' h3 K, F& ^$ Q ![]() 图6.3.5. 8 添加观察地址0x00000000 要注意,CM4内核是小端模式,所以读取下面的参数的时候,要倒着来读。0x0000 0000地址处的值是0x1004 0000,0x0000 0004的值是0x1000 3271,即堆栈指针 SP =0x1004 0000,程序计数器指针PC = 0x1000 3271(即复位中断服务程序Reset_Handler的入口地址)。' a! `& i& c3 u/ i3 | 9 d0 A) e+ |: y: a ![]() 图6.3.5. 9查看地址 当芯片上电后采样到BOOT0、BOOT1和BOOT2引脚电平为001,地址0x00000000和0x00000004被映射到内部SRAM的首地址0x1004 0000和0x1000 3271,内核从SRAM空间获取内容。在实际应用中,由启动文件startup_stm32mp15xx.s决定了0x00000000和0x00000004地址存储什么内容,编译后,在链接时,由stm32mp15xx_m4.ld链接脚本决定这些内容的绝对地址,即分配SRAM的哪个位置。下面我们来看看这个链接脚本stm32mp15xx_m4.ld。3 x- p; ~0 a, z 6.3.6 stm32mp15xx_m4.ld链接脚本8 }5 p4 _: A1 l! T1 ] 前面我们通过启动文件了解了系统复位后做了些什么工作,但我们并不知道内存的分配信息是怎样的,当然很多时候我们不需关心这些,只要确保程序能正常运行就可以。关于内存排布,我们这里会介绍一个重要的文件:链接脚本。" ?! b! }* V! ^, M2 _ 本小节中,在介绍链接脚本的时候,我们也会介绍两个和链接脚本关系比较重要的文件,一个是链接时产生的map文件,另一个是编译后生成的反汇编文件。本小节只是作为一个了解性的内容,如感兴趣可以了解一下,也可以跳过本小节。 6 d8 S1 Y/ C/ H4 z% }1 q 链接脚本" J/ r$ a4 l& }& }8 i/ o 链接脚本路径:Device\ST\STM32MP1xx\Source\Templates\gcc\linker\stm32mp15xx_m4.ld ![]() 图6.3.6. 1链接脚本路径 在Device\ST\STM32MP1xx\Source\Templates\下的arm、gcc和iar下均有一个文件夹linker,里边放的就是STM32MP1系列的链接描述文件,其中,在STM32CubeIDE下,链接脚本为.ld文件,在KEIL中,链接脚本为.sct文件,在IAR中,链接脚本为.icf文件。 当构建工程的时候,STM32CubeIDE会按照我们选择的芯片型号生成一个.ld的链接脚本, 链接脚本是用于描述文件应该如何被链接在一起形成最终的可执行文件的脚本,其主要目的是描述输入文件中的段(section)如何被映射到输出文件中,并且控制在输出文件中的内存排布。利用链接脚本我们可以控制代码的加载区以及执行区的位置。- ^/ W. X% \% Q2 Y 程序的编译一般分为预处理、汇编、编译和链接这4个步骤,我们在STM32CubeIDE上只需点击编译图标就一次性完成了这4个步骤,其中的操作细节IDE已经通过层层封装屏蔽掉了。在编译过程中,编译器将.c和.s源文件编译生成很多以.o结尾的中间文件,这些中间文件包含了只读数据段、代码段、数据段、未初始化数据段等机器码信息,但是这些信息是放在最终可执行文件的哪个位置并没有确定下来,于是,链接脚本会告诉链接器,把所有的中间文件链接起来,并重定向它们的数据,然后链接生成可以被单片机运行的.elf文件。如果要生成.bin格式的文件,只需要通过格式转换就可以完成。4 V# b% k: _$ k N" `& Z ![]() 图6.3.6. 2程序编译过程 我们先回顾一下前面的STM32CubeIDE第一个工程编译信息,可以打开工程重新编译,然后将编译的信息拷贝到一个.txt文本文件中,这样方便浏览信息。如下图是删除部分编译信息后的内容: ![]() 6 j6 z" E0 t& q6 s' K+ G 图6.3.6. 3编译信息8 p- m! H* i Y% U2 |' C$ g 编译生成的文件在工程的CM4\Debug下,在里边的文件夹中有生成的中间文件。编译结束后提示Build Finished. 0 errors, 0 warnings,没有报错,最终生成HAL_LED_CM4.elf二进制文件。如果有配置编译生成其它格式的执行文件,那么编译器会执行相应的指令以生成对应的文件,这些配置可以在STM32CubeIDE中设置。 ![]() " u% D9 g; ]* F/ |' a 图6.3.6. 4配置编译生成的文件' C. w4 [3 z7 I 如上图,设置编译生成.bin格式文件和反汇编文件,那么编译器在生成HAL_LED_CM4.elf文件以后,还会再执行以下指令: arm-none-eabi-objdump -h -S HAL_LED_CM4.elf > “HAL_LED_CM4.list”: L- f, M0 L L; @. U _) A arm-none-eabi-objcopy -O binary HAL_LED_CM4.elf “HAL_LED_CM4.bin” arm-none-eabi-objdump是反汇编指令,将 HAL_LED_CM4.elf文件进行反汇编,并生成HAL_LED_CM4.list文件。arm-none-eabi-objcopy命令表示复制一个目标文件的内容到另一个文件中,可用于不同文件之间的格式转换,最后将HAL_LED_CM4.elf格式转换成HAL_LED_CM4.bin文件。( E5 ^ [1 f* d2 J STM32CubeIDE的这些编译过程都是由一个makefile文件来控制的,makefile里边是一些shell脚本,描控制工程的编译过程。关于makefile我们这里不做专门讲解,感兴趣的话,可以在网上查询更详细的说明,或者看正点原子的《STM32MP1嵌入式Linux驱动开发指南》或《I.MX6U嵌入式Linux驱动开发指南V1.5》这两个教程,这两个教程是基于Linux操作系统的,对makefile有做部分介绍。0 a4 q! f, U. a ![]() 1 I3 R& n+ J2 m. C' Z 图6.3.6. 5编译生成的文件$ i# j, e9 Z8 c2 v C语言程序编译完成以后,编译出来的代码一般都包含text、data、bss 和 rodata 这四个段(section)。已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。text和data段都在可执行文件中,程序运行的时候,由系统从可执行文件中加载。而bss段不在可执行文件中,由系统初始化并清零。四个段以及堆和栈的简单说明如下:: W) D6 R( X L& Z' h W0 `0 p & U5 J( [1 e7 y- _3 Z; l/ T* C ![]() 表6.3.6. 1代码中各个段简介 - [+ u" ^8 M, t" D ![]() * r0 {8 y2 ~7 r+ p4 W2 m 表6.3.6. 2堆栈简介 前面有看到通过STM32MP157DAAX_RAM.ld链接脚本,生成了HAL_LED_CM4.elf文件,这个链接脚本是怎样工作的呢?我们先看几个链接脚本的语法,然后再去查看链接脚本的代码实现过程。6 b0 d3 ?, u! L8 ? 1)入口地址% J) u) P( ~+ _! ^ ENTRY(SYMBOL)% J/ }8 j; l$ j8 o, J" ~ ENTRY(SYMBOL) ,表示将符号SYMBOL的值设置成入口地址,即程序执行的第一条指令的地址。 2)内存区域定义! A1 \+ T' F T 链接器在默认状态下可以为section 分配任意位置的存储区域,使用MEMORY命令可以用它来描述哪些内存区域可以被链接器使用,哪些内存区域避免使用。一个链接脚本最多可以包含一次MEMORY命令。
NAME 是在链接脚本中引用内存区域的名字,每块内存区域有一个唯一的名字。ORIGIN是起始地址,LENGTH是地址的长度。ATTR字符串是该内存区域的属性,其中:% P* A/ ~7 Q! q5 N8 S r表示读section8 a7 C8 r. H/ P+ T7 v3 q% Z7 X w表示写section# I) Z. I0 [% a# Z! o( s x表示执行section a可表示分配的section3 n* ?* f' }2 t% v+ l4 f l(L)表示初始化了的section& l/ R4 ?0 K1 V! E+ ]2 |" p( p ! 表示不满足该字符之后的任何一个属性的section" x" i/ g$ e( J. S1 V* i 一旦定义了一个内存区域,就可以指示链接器把指定的输出段放入到这个内存区域中,方法是:通过使用’>region区域’。例如已经描述一个名为’mem’的内存区域,可以在输出段定义中使用’>mem’。 3)段链接定义; V$ ? M3 m. ?( H SECTIONS 命令是链接脚本里非常重要的命令,它的作用是:告诉链接器,如何把输入文件的sections映射到输出文件的各个section,如何把输出section放入地址空间。跟MEMORY命令一样,一个链接脚本里只有一个SECTIONS 命令。如果整个链接脚本内没有SECTIONS命令, 那么链接器将所有同名输入section合成一个输出section内, 各输入section的顺序为它们被链接器发现的顺序。$ @& N a, s9 h- C+ I # i/ I+ ?0 O0 [
第 1 行先写了一个关键字“SECTIONS”,后面跟了一个大括号,这个大括号和第 8行! i4 V' [: l! O. T' }2 H 的大括号是一对,这是必须的。看起来就跟 C 语言里面的函数一样。 第3行,“.text”表示段名,段名后面先空2个空格,然后再有一个冒号,表示段定义。例如.text :表示定义一个.text段,这个段定义可以自己定,段名可以自己取。$ m; O$ l+ h, w8 L" F& I7 p! Y 第4行到第7行就是段的内容,这部分内容比较复杂。start.o (.text)表示将工程文件中的start.o的.text段(即代码段)链接到MEMORY定义的region中。(.text)中的是通配符,表示将工程中所有目标文件的.text段链接到region中,在链接(.text*) 时,不会重复链接start.o的.text段。 第7行的’>region’就表示指示链接器把上面花括号中指定的输出段放入到这个region内存区域中(内存区域是在前面的MEMORY中定义的)。 段的内容比较复杂,下面是一些常见的用法: . = ALIGN(4):表示4字节地址对齐。也就是说段的起始地址要能被 4 整除,一般常见的都是 ALIGN(4)或者 ALIGN(8),也就是 4 字节或者 8 字节对齐。 PROVIDE和PROVIDE_HIDDEN关键字:表示在链接脚本文件中定义一个符号,这个符号没有被目标文件定义,但是被目标文件引用。/ Y/ U6 [# }9 C& \& q4 ^9 D3 r( o9 l0 N KEEP()关键字:KEEP() 的作用是当启用连接器的–gc-sections垃圾回收选项时,这部分不能被回收。如KEEP(*(.text))表示不能将所有的.text段当做垃圾回收。 /DISCARD/:是一个特殊的段名,如果使用这个段名作为输出,那么所有符合条件的段都被丢弃。% u8 B2 D% v& W9 V& o+ X6 ? 下面我们将stm32mp15xx_m4.ld文件的代码分为几个部分,查看链接脚本是怎样实现链接的。( I3 w M& L4 Z2 d
ORIGIN(m_ipc_shm)+LENGTH(m_ipc_shm);! ?6 [1 s5 h5 k, [ L7 Z0 r 第2行,程序入口,程序将从Reset Handler函数开始执行,该函数在启动文件startup_stm32mp15xx.s中有定义。* {' A2 I6 W+ C S; N 第5行,设置堆栈的最高地址为0x10040000,这里注意了,它决定了SP的位置,0x10040000就是我们前面分析startup_stm32mp15xx.s文件时系统启动流程小节里仿真时得到的SP地址,即0x0000 0000映射到内部SRAM的地址。 第7、8行,定义了堆和栈的最小空间大小,其中,设置堆大小为512B,栈大小为1KB。& _! e: R4 j& s; J: ^" j) v 第11到17行,以MEMORY命令定义了系统中可用于放置代码和数据的内存区域: ①区域名为m_interrupts的地址范围是0x00000000~0x00000298,这个范围也就是M4内核中断向量表的范围,对应前面内存映射关系图中的RETRAM区域;" C$ P" z! L" s. w ②m_text区域的地址范围是0x10000000~0x10020000,刚好对应内存映射关系图中的SRAM1区域,链接的是tex代码段(Code);, k9 F, T2 [" ?; b u ③m_data区域的范围是0x10020000~0x10040000,对应内存映射关系图中的SRAM2区域,链接的是数据段(Data);* B% X; g6 ?6 j! K9 a ④m_ipc_shm区域的范围是0x10040000~0x10048000,此范围落在了SRAM3中,ipc(Inter Process Communicaton)即进程通信,这个区域可以作为IPC缓冲区(IPC Buffers),也可以用于其它用途。 以上的链接地址就是程序的执行地址,找到链接地址了就知道程序是在哪里执行了。我们前面有提到内存映射关系图,在IDE中调试程序的时候,M4的代码其实是放到了SRAM中运行了,其中M4可用的SRAM是SRAM1(128kB)、SRAM2(128kB)、SRAM3(64kB)和SRAM4(64kB),地址范围是0x100000000x1005FFFF,共384KB,如果只是M4跑裸机或者RTOS,不运行A7的话,这SRAM1SRAM4可以全部分配给M4,那如果要同时运行M4和A7的话,这些地址分配就要注意了:根据MEMORY命令定义的地址范围,我们知道m_text、m_data和m_ipc_shm占用了SRAM1SRAM3,SRAM1、SRAM2是完全分配给M4了,这里注意,如果要运行A7的话,M4并不是完全占用SRAM3,具体占用多少需要根据Linux下的设备树配置来决定,在A7和M4双核通信中,默认A7和M4共同占用SRAM3的0x100400000x10046000,这部分地址作为A7和M4通信的内存交换区,内核下的设备树如下:. }0 B T& D2 H" R, B0 t4 x ![]() 图6.3.6. 6 设备树部分截图1 k7 N) e6 L1 D" z6 Z 而m_interrupts其实是在RETRAM里,剩下的SRAM4用于做什么呢?如果不跑Linux操作系统,只是跑M4裸机程序的话,M4内核完全可以使用这部分区域,由用户来指定。如果跑Linux操作系统,在Linux设备树下已经默认将SRAM4当做了Linux功能的DMA了,如果要释放这部分区域,在设备树下将对应节点删除释放即可(但不建议这么做,A7可能会异常)。5 C. L0 I: i. V+ ~1 R1 U7 L. R9 E: g 根据上述描述,这几个区域对应关系如下图,从图中可以明显看出SRAM的地址分配情况:0 ]' ~/ ~, W3 {( H ![]() 6 r- v! B0 n: _ f$ w 图6.3.6. 7 几个SRAM区域 综合以上分析,总结如下:" y- Y) d+ y& Z5 a1 E: w 如果不跑A7,只运行M4(M4可以跑裸机、RTOS):SRAM1~SRAM4可以完全分配给M4; 如果同时跑A7和M4(例如双核通信):SRAM1和SRAM2是单独给M4用的,SRAM3的部分地址是M4和A7一起使用的,SRAM4在Linux下单独配置了DMA,即被A7占用了。) D8 { X* T0 d 如果要修改MEMORY中的地址区域范围,一定要联系内存映射表的地址范围来修改。7 @# N* C( d4 F1 [& e& Z: W 接下来是SECTIONS段链接定义,内容比较多已经省略部分:' e) d$ E: V: n( ^8 _# Y # ]: s; n( y1 t' v9 _( e
根据注释以及前面讲解的语法,整个链接脚本也很容易看懂。 第5到第10行,表示中断向量的内容,地址为0x00000000~0x00000298,这个区域是RETRAM。6 [2 W/ H U4 y5 g0 Z' A( E 第24行,_etext = .;中的小数点.表示当前地址,意思是_etext =的地址就是.text段的地址。 一般的程序中包含常见的几个段:text、rodata、data和bss段,这一部分实际上指定了程序的各个内容该如何放置在SRAM上。 在STM32CubeIDE上可以直接查看这些段的信息。例如第一个工程实验HAL_LED,编译完成以后,点击Window菜单Show ViewBuild Analyzer打开Build Analyzer窗口查看(注意,此时不是在Debug调试看的,是编译后查看的): 5 K( r7 K! q4 h/ ?* O6 P; _) T ![]() 7 k; W! y6 ~% f, ? 图6.3.6. 8选择打开Build Analyzer窗口% r5 Z. c9 ^- J6 W$ Y" Q 在Build Analyzer窗口中可以查看内存使用情况。其中在Memory Regions处可以看到m_interrupts、m_text、m_data和m_ipc_shm的起始地址和结束地址,范围大小,以及使用量和剩余量。 ! n* j! O- M |, f ![]() 图6.3.6. 9 Memory Regions窗口* K% e1 J- o4 u3 M8 E2 e) y' ~ 在Memory Details处可以查看更详细的信息,VMA是虚拟地址,一般是RAM位置这里指运行地址,LMA一般是加载地址,即ROM位置。 * v4 l+ f: H- K8 ], {3 X- U ![]() ) ?0 [( v/ K9 `( P4 r5 Z 图6.3.6. 10 Memory Details信息. z. e" b8 Y$ ?1 d( \ 通过链接脚本的内容,我们大概知道每段链接到了哪段内存区域,程序的链接地址一般也就等于运行地址(这点大家要记住)。其实我们也可以通过查看.map文件和反汇编文件来进一步研究我们链接脚本和代码的实现,下面我们分两个部分来讲解这两个文件。) M2 f \* L/ K 2. MAP(地图)文件 我们编写的代码,在IDE上经过编译和链接以后会生成一个.elf格式的文件,同时也会生成一个.map格式的文件,如前面我们看到的HAL_LED_CM4.map文件。map就是地图、示意图的意思,map文件是链接器的输出,提供有关所生成的.elf文件中的符号、地址和分配的内存的信息,当试图了解调试的程序大小和内存使用情况时,map文件非常有用。+ s' q# q; M% k! w* } 我们打开前面的HAL_LED_CM4.map文件,查看文件中都有什么内容,下面的截图中,由于文件路径比较长,于是只截图了一部分。# Z7 b, C7 {7 w Archive member included to satisfy reference by file (symbol)部分:& K* O W% n7 R6 l ! ^; y8 _6 b9 \" b ![]() 图6.3.6. 11 存档文件部分8 E9 I* I; i( m) G 此部分属于存档文件,通常存储目录结构,包含系统中各种归档文件中包含的所有成员,以满足文件(符号)的引用,这些信息并不是特别有用,但是可以看到所有的系统功能。 Allocating common symbols部分: ( l" T5 @" M4 e ![]() 图6.3.6. 12符号分配部分 此部分是常见的符号分配,显示了已在程序中分配的全局符号(即全局变量)的名称和大小,这是检查所有全局变量是否具有预期大小的好地方。一个常见的错误可能是在不知情的情况下,分配了一个大的全局变量,这会消耗大量内存空间。通过此处可以了解到工程中使用的全局变量的名称和大小,如果大小不合理,可以在工程中稍微调整。 Discarded input sections部分: : G1 q6 }4 ? J8 A8 i% R! t7 b! n& w5 y ![]() 图6.3.6. 13丢弃的输入部分 此部分是丢弃的输入部分,很多时候可以不用管。- i3 b" E3 W& z6 L Memory Configuration部分: ![]() : }2 m& A1 O E1 ?/ C) Z 图6.3.6. 14内存配置信息部分3 h& e, E7 e6 ~) v5 B" Z* G6 `4 I 此部分属于内存配置信息,这部分的信息应该与链接脚本中的内存配置范围相同。 Linker script and memory map部分: ![]() 图6.3.6. 15内存映射 此部分详细说明了按链接脚本文件中定义的部分而划分的内存映射,其提供了有关程序中所有内容映射位置的大量信息。每个顶级节(例如.text)都具有内存映射中的起始地址以及列出的大小(以字节为单位)。然后,将每个部分细分为各个目标文件,并列出起始地址和大小。最后,每个目标文件都分解为目标文件中的各个功能,并列出了每个功能的起始地址。这可以帮助我们能够了解哪些目标文件可能包含程序执行不必要的大功能。当查看程序中的指针地址时,它也可以提供上下文。 我们可以通过配置IDE来选择是否生成.map文件,选中工程,打开Properties配置项,点击C/C++ Build–Settings–MCU GCC Linker找到链接器的配置项General,如下图是系统默认的配置项,其中:7 C4 ~; ]$ L- E" n0 E6 ~ Linker Script(-T)是配置用哪个链接脚本,如果您有自己的链接脚本,也可以在此处配置以选择使用自己的链接脚本。4 L& \) y# l( r* F9 H Generate map file (-Wl,-Map=)此项表示是否要生成.map文件,一般是默认选择的,如果不需要生成.map文件,可以去掉此项。 Do not use standard start files (-nostartfiles)表示链接时不要使用标准的启动文件(-nostartfiles),此项一般不选,因为我们要用到启动文件。 No startup or default libs(-nostdlib)表示没有启动库或默认库(-nostdlib)。 Do not use default libraries(-nodefaultlibs)表示不要使用默认库(-nodefaultlibs)。6 h6 g5 P( v4 ^5 l m. j6 | - S0 t' Y9 p+ r ![]() 图6.3.6. 16 CubeIDE配置 其它选项一般不需要再配置,如果有必要,可以尝试配置,例如Add symbol cross reference table to map file(-Wl–cref)表示将符号打印出来,并按名称排序。如果勾选此项,对于每个符号,会给出一个文件名列表,如果定义了符号,则列出的第一个文件是定义的位置,其余文件包含对该符号的引用。 * k& ?1 D3 {0 k7 e ![]() 5 h7 t3 ]! N# x$ `- l 图6.3.6. 17其它配置 调试程序时,map文件是信息的重要来源,它记录项目中每一个Symbol的地址、每个段的区域范围、各区段的大小等重要信息,可以通过map文件查看程序段或数据段的大小,查看高地址数据范围等等。尽管map文件的信息非常密集并且有些令人生畏,但理解和使用map文件可以为我们提供很多有用的信息。 3. 反汇编文件 把机器语言转换为汇编语言代码的过程,我们叫反汇编(Disassembly)。反汇编常用于软件破解、软件汉化、病毒分析等。理解反汇编语言,对C语言代码理解、软件调试、程序漏洞分析是很有帮助的,大多数情况下我们都是用 C 语言写试验例程的,有时候需要查看其汇编代码来进行调试。STM32CubeIDE已经自动将HAL_LED_CM4.elf文件反汇编得到HAL_LED_CM4.list文件了,我们打开此文件大概浏览一下:* O; v ^; U2 @* b1 i( l
如上是反汇编文件开头的部分代码,第1行表示由HAL_LED_CM4.elf文件反汇编得到文件,文件格式是elf32位的,且是小端模式存放。第3行表示要列出段,接下来第4段到第8端列出段,其中Idx表示索引,Name表示段名,Size表示该段的大小。VMA是虚拟地址,一般是RAM位置,LMA一般是加载地址,即ROM位置。File off表示段所在位置(指距离.elf文件的 Header 00000000)的偏移。/ x. F8 u, f* B CONTENTS、ALLOC、LOAD、READONLY、DATA这些表示段的属性。 CONTENTS表示该段在文件中具有相应的内容;& q! [8 e. t" g5 p1 l0 S% F2 p) O ALLOC表示该部分占用内存; LOAD表示该段在可加载段中,当创建进程时,其内容可以从文件读入存储器; READONLY表示该段不可执行,也不可写;4 L. D7 V1 \8 t+ X& T9 I: r DATA表示该段不可执行,但可写;+ ?4 b: k2 F$ \) G CODE表示包含要执行的指令。 例如.text这段,它包含程序可执行的代码,所以显示CONTENTS;该部分占用内存,所以显示ALLOC;它的内容从文件中加载,所以显示LOAD;编译好的程序代码是不可修改的,它放置在只读存储器中,所以显示READONLY;.text段包含要执行的指令,因此显示CODE。 我们再看之后的反汇编部分:
第1行,说明反汇编文件是.text段。 第3行,10000000表示指令地址。 第4行,10000000表示指令地址,b510表示指令机器码,push {r4, lr}表示指令机器码反汇编得到的指令。& F. r* J7 H6 E9 Q" e3 C& z ![]() 7 G3 u) e5 B7 n U/ b0 a Y 在反汇编文件中找到如上图代码,Reset_Handler处指令地址是0x10003270,和我们前面分析startup_stm32mp15xx.s文件时系统启动流程小节里仿真时得到的PC = 0x1000 3271很相近,说明和我们前面猜想的一样。 查看反汇编代码,里边的地址从低到高排列,最高地址是0x10040000,这个是因为链接脚本里有定义栈的最高地址为_estack = 0x10040000,如果将_estack = 0x10040000改为_estack = 0x10020000,再编译工程,可以看到反汇编文件中最高的地址变成了0x10020000(也就是SP地址有原来的0x10040000变成了0x10020000),可以看到.text段的VMA(SRAM的地址)地址是0x10000000,这是前面的链接脚本中定义的m_text区域的起始地址,如果把m_text区域的地址进行调整,例如起始地址改为0x10000100,编译后,汇编文件中.text段的VMA地址就变成了0x10000100。 反汇编的好处是可以帮助我们理解链接脚本,有时候可能我们会自己写链接脚本,通过查看反汇编文件可以帮助我们检查链接脚本的错误。1 m5 `: y) }* @" P: o3 b/ G9 N 0 [: B) p3 f6 n0 g5 J: Y2 l9 [ 6.3.7 Include文件夹 Include文件夹下是符合CMSIS标准的内核头文件,我们在使用STM32CubeIDE创建工程的时候,系统会自动为我们添加这部分文件。" d+ t6 S4 Z/ n& W8 N2 n% I; I & I8 z J" f ^+ N ![]() ) q6 w/ v* e" b k) o 图6.3.7. 1 Include文件夹内容3 J2 ^* ?! y$ Y2 N 在这些文件中,以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>8 m) V3 |5 d# P+ l stdint.h是C99 (C语言规范)中引进的一个标准C库的头文件,其定义了几种扩展的整数类型和宏。现在编译器对C99的支持已经做的很好了,大部分单片机C编译器均支持C99标准,例如IAR、MDK和STM32CubeIDE等,linux 系统下的编译器也支持。在STM32CubeIDE安装目录\plugins\com.st.stm32cube.ide.mcu.externaltools.gnu-tools-for-stm32.7-2018-q2-update.win32_1.4.0.202007081208\tools\arm-none-eabi\include下就有stdint.h文件。stdint.h的作用就是提供了类型定义,其包含了_intsup.h和_stdint.h文件。 ) c6 m' {$ `8 w* J- P5 u
这些文件中定义了我们程序中用到的部分类型,在IDE上可以找到这些定义的实际类型,例如,通过查找,得出__UINT32_TYPE__表示long unsigned int,而: 5 y5 X7 Y" u2 G/ ]4 b
所以,今后我们在程序中看到的uint32_t实际上表示long unsigned int(无符号长整型),而uint8_t表示unsigned char(无符号字符型),int8_t表示signed char等。3 t8 C: ~3 O0 v F" O7 C0 \ 在core_cm4.h文件中,我们还看到很多关于中断相关的函数定义和类型定义,例如,开启中断函数NVIC_EnableIRQ、禁止中断函数NVIC_DisableIRQ、设置中断优先级分组函数NVIC_SetPriorityGrouping和中断优先级函数NVIC_SetPriority,这些函数会在HAL库中调用以实现中断功能。此外,还有内核的外设相关定义,如SysTick实时系统内核时钟相关寄存器和函数都在core_cm4.h文件中定义。如下是中断控制器(NVIC)类型定义。 ![]() ) W# C) j& i% l+ F9 K* o 图6.3.7. 2中断控制器(NVIC)类型定义 core_cm4.h文件就介绍到这里了,在这里我们不对core_cm4.h文件的内容做深入的讲解,相关的介绍我们后面会结合实验例程来加深理解。 6.4 章节小结 本章节洋洋洒洒地写了几十页,并不是为了“拉长战线和故意占用篇幅”, 实际上,要好好学习HAL库,要分析的东西还不仅仅这些。大家都知道,ST提供的这个固件库已经封装好了,在开发中我们只需要调用对应的API就可以实现想要的功能。不管它封装的多好,本质上还是操作寄存器。我们在学习过程中,不能只停留在理解的表面上,应该尝试去理解它的本质上的东西,通过分析,我们可以理解它的架构,这有助于日后的学习和开发。 本章节主要对STM32CubeMP1固件包的架构以及CMSIS文件夹中的部分重要文件做了介绍,重点对我们后面会用的CMSIS文件夹下的Device文件夹以及Include文件夹中的部分文件做了介绍。' _0 W% ^, H* [* J; X5 T1 A8 N 通过分析stm32mp1xx.h文件,我们可以确定代码中是否使用或者不使用某个底层驱动文件。通过定义宏CORE_CM4、STM32MP157Dxx和USE_HAL_DRIVER,我们可以在工程中包含必要的头文件,如果换了另一款STM32芯片,我们同样可以通过分析对应头文件来确定这些信息。+ p( a- z. R2 _! ^ ![]() s- S* Y! x! N& @6 i9 p3 \ 图6.3.7. 3几个宏定义# N7 Q# c7 K H8 v3 ~ 通过分析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函数入口。 2 M! o' i# Y( `% Y# r ![]() 图6.3.7. 4系统启动过程 通过分析stm32mp15xx_m4.ld链接脚本,我们知道了编译好的输入文件中的每个段是如何被映射到输出文件中的,其中,text代码段位于SRAM1,data数据段位于SRAM2。此外,我们还分析了HAL_LED_CM4.map地图文件和HAL_LED_CM4.list反汇编文件,编译生成的.elf文件中的符号、地址和分配的内存的信息都可以在地图文件中查看。反汇编文件可以辅助我们检查代码的缺陷,在实际项目开发中,这些文件是非常重要的。- I: v. _% K/ c, }6 @0 p u ![]() . N' B. h' U% v" {- m 图6.3.7. 5M4内核可用的SRAM: M- W* {' P; M) y/ d Inclue文件夹下主要是符合CMSIS标准的内核头文件,在M4裸机开发中,我们主要用的是core_cm4.h文件,此文件中主要是关于中断相关的函数定义和类型定义,还有内核的外设相关寄存器的定义,例如核外设SysTick。9 [- L5 S j9 X" Z0 Z, @+ k/ F ————————————————5 U1 I0 p" |4 w 版权声明:正点原子7 v3 @' c; ~9 b # ^4 m/ D. k, i0 c s5 U , M! g- _: j/ M) u' H, q9 G |
基于STM32MP1和STM32MP2在嵌入式Linux平台上部署有效的安全保护机制
利用STM32MP1和STM32MP2为嵌入式Linux提供有效的安全措施:供当今决策者参考的3条宝贵经验
STM32MP1 WiFi连接
【STM32MP157】从ST官方例程中分析RPMsg-TTY/SDB核间通信的使用方法
【STM32MPU 安全启动】 TF-A BL2 TrustedBoot原理学习
《STM32MPU安全启动》学**结
《STM32MPU安全启动》学习笔记之optee 如何加载CORTEX-M核和使能校验
《STM32MPU安全启动》学习笔记之TF-A BL2校验optee和uboot的流程以及如何使能
《STM32MPU 安全启动》课程学习心得+开启一扇通往嵌入式系统安全领域深处的大门。
《STM32MPU安全启动》 课程学习心得