你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

MiniPro STM32H750 开发指南_V1.1-STM32启动过程分析

[复制链接]
STMCU小助手 发布时间:2022-10-5 21:42
STM32启动过程分析
! V% e' u4 z" O8 y" j  q0 ]本章给大家分析STM32H7的启动过程,这里的启动过程是指从STM32芯片上电复位执行的第一条指令开始,到执行用户编写的main函数这之间的过程。我们编写程序,基本都是用C语言编写,并且以main函数作为程序的入口。但是事实上,main函数并非最先执行的,在此之前需要做一些准备工作,准备工作通过启动文件的程序来完成。理解STM32启动过程,对今后的学习和分析STM32程序有很大的帮助。
# u9 z1 v5 a0 a7 f/ R: q注意:学习本章内容之前,请大家最好先阅读由正点原子团队编写的《STM32 启动文件浅析_V1.2.pdf》和《STM32 MAP文件浅析_V1.1.pdf》这两份文档(路径:A 盘→1,入门资料)。
1 ?" z0 \* {3 F' w$ s% T3 T, o7 ]* _9 w5 K% x
9.1 启动模式7 @$ R# z. c* S- z6 H1 c- t
我们知道的复位方式有三种:上电复位,硬件复位和软件复位。当产生复位,并且离开复位状态后,CM7内核做的第一件事就是读取下列两个32位整数的值:% f4 o6 _+ _8 v4 _
(1)从地址 0x0000 0000 处取出堆栈指针MSP 的初始值,该值就是栈顶地址。* h- P' K" c( U/ i* Z( I5 `! w
(2)从地址 0x0000 0004 处取出程序计数器指针PC 的初始值,该值指向复位后执行的第一条指令。下面用示意图表示,如图9.1.1所示。: R% _' F9 A) h# y  R0 ]1 \1 |

2 [, f' @1 e' Y: d- c 1b3a0104a63d4610b488d97d7a206b00.png
" T% q# k2 Y  ^" {7 M! s( D2 l9 C" q
% ^( D3 B& g+ Z$ a图9.1.1 复位序列+ n$ Y: r/ n/ {$ L7 F
上述过程中,内核是从0x0000 0000和0x0000 0004两个的地址获取堆栈指针SP和程序计数器指针PC。事实上,0x0000 0000和0x0000 0004两个的地址可以被重映射到其他的地址空间。例如:我们将0x0800 0000映射到 0x0000 0000,即从内部FLASH启动,那么内核会从地址0x0800 0000处取出堆栈指针MSP 的初始值,从地址0x0800 0004处取出程序计数器指针PC 的初始值。CPU 会从 PC 寄存器指向的地址空间取出的第1条指令开始执行程序,就是开始执行复位中断服务程序 Reset_Handler。
) t: s8 `6 g0 t7 g将0x0000 0000和0x0000 0004两个的地址重映射到其他地址空间,就是启动模式选择。
& j; s# i' D* S3 i2 k8 F' |对于STM32H7的启动模式(也称自举模式),我们看表9.1.1进行分析。( a5 w  ]0 ]- [5 {7 [; {; C! ~
) Z2 e0 i+ y# f: Y* m
a24e4ff122ff409fb0d30a23c257b087.png
' q$ {$ |+ l7 M0 s9 e& M0 A6 I$ S) e' N! Y5 j9 n: v
表9.1.1 启动模式选择表
- k4 k0 L$ O9 b$ @注:启动引脚(BOOT)的电平:0:低电平;1:高电平。
& L' j* _3 |* [( Y/ Q& {! J6 O$ K由表9.1.1可以看到,STM32H7的启动模式选择需要一个BOOT引脚,这个BOOT引脚对应了0和1两个状态,这两个状态都有自己对应的启动地址选项字节,用于选择自己的启动地址。
. ]6 F5 \' ?, w. C* d这里的BOOT_ADD0[15:0]和BOOT_ADD1[15:0]对应32位地址的高16位,也就是说我们只能设置高16位对应的地址(低16位必须是0),其设置范围:0X0000 0000 ~ 0X3FFF 0000,涵盖了所有FLASH地址空间,所有 RAM 地址空间:ITCM、DTCM RAM和 SRAM,还有TCM-RAM地址空间。基本上,可以设置任意地址启动(低16位必须是0),通过FLASH_BOOT_PRGR寄存器设置。! q& k6 e( @# u( U
在出厂的时候,ST默认给BOOT_ADD0和BOOT_ADD1编程为:0X0800 0000和0X1FF0 0000分别对应用户FLASH的起始地址和系统存储器地址,用于执行用户代码或者进入BOOTLOADER状态。一般情况下我们设置B00T引脚为低电平即可,即从0X0800 0000的FLASH地址启动,执行用户代码。# m$ E# L; t* q7 H- ]. F$ E
9.2 启动文件分析
' @7 e. u4 k" ^" h. O$ tSTM32启动文件由ST官方提供,在官方的STM32Cube固件包里,对于STM32H750系列芯片的启动文件,我们选用的是startup_stm32h750xx.s这个文件。启动文件用汇编编写,是系统上电复位后第一个执行的程序。( j+ ~4 N9 I7 |+ a8 t5 ^
启动文件主要做了以下工作:; L9 g# f% m8 a4 T5 d
1、初始化堆栈指针 SP = _initial_sp
% g7 j$ a, B8 S- T' q4 h5 _2、初始化程序计数器指针PC = Reset_Handler
# K$ Y; k' z; i# x! Q3、设置堆和栈的大小/ C( D" B' B6 B  U& s* g2 V' x0 s& C
4、初始化中断向量表
9 K  [: z2 }* K- L$ I2 X# w5、配置外部SRAM作为数据存储器(可选)- j2 R& [9 j4 }" H8 l
6、配置系统时钟,通过调用SystemInit函数(可选)! h8 [0 m) t1 i* q
7、调用 C库中的 _main 函数初始化用户堆栈,最终调用 main 函数
( b! I% ]1 {0 D2 Y3 g/ C8 V9.2.1 启动文件中的一些指令. w' D! Q( Q% V8 ?
5 K/ s4 S- N$ D& K8 ~2 g# g
03443363de7e443db62356e2b7045848.png . e9 C( Z. F( K

" N! Y- M0 {2 e表9.2.1.1 启动文件的汇编指令+ F/ l: ?" G) T4 n7 f- w
上表,列举了STM32启动文件的一些汇编和编译器指令,关于其他更多的ARM汇编指令,我们可以通过MDK的索引搜索工具中搜索找到。打开索引搜索工具的方法:MDK->Help->uVision Help,如图9.2.1.1所示。
6 Z' Y% L4 o& F! {3 l3 O# N, X8 Q- R/ M  i# B8 T2 ]; g
c4b8416fc11b40439f2d9687caa27621.png   `( n9 p; u( L! ]6 @' W
0 P+ c, b: o+ I& B7 B  ]
图9.2.1.1打开索引搜索工具的方法
" F4 g8 X: D; H( Q/ ]打开之后,我们以EQU为例,演示一下怎么使用,如图9.2.1.2所示。
  @2 Q: o5 }. A1 u3 O! a+ n( H) }4 t- w4 {" Y
e7647be5992d4211bc9d185cecba9d88.png
# }" i# y8 m8 P0 o8 h$ l+ f  v* D) g$ g2 {+ m* L; x) L
图9.2.1.2 搜索EQU汇编指令% x& K9 Y8 e+ {+ v, Z9 b6 \
搜索到的标题有很多,我们只需要看Assembler User Guide 这部分即可。
# c5 s; F  X2 R' I* W) R) k
& E7 i: N- T2 Q$ ~' {9.2.2 启动文件代码讲解7 N% {1 q- v8 |7 ^& p! J$ R
(1)栈空间的开辟
/ a. v$ ~- q" d9 y* ~栈空间的开辟,源码如图9.2.2.1所示:
' _7 x; i" R% O8 n& b5 k% P0 s3 z
- ^$ r$ j( r* D4 {" ]/ n 2a673c6d781f48b784a1de12f047a235.png 2 [* y& R5 o5 c' u
; Q9 e/ Y3 ~' _2 J; T
图9.2.2.1 栈空间的开辟$ p' `5 G2 `! W) h3 U1 x3 C
源码含义:开辟一段大小为0x0000 0400(1KB)的栈空间,段名为STACK,NOINIT 表示不初始化; READWRITE 表示可读可写;ALIGN=3,表示按照 2^3对齐,即 8 字节对齐。& t6 q, p3 B* ]' R
AREA汇编一个新的代码段或者数据段。; H& M. N/ d! i( ]* t
SPACE分配内存指令,分配大小为Stack_Size字节连续的存储单元给栈空间。
! U) H  A% T7 h2 R5 x' ?) ^__initial_sp紧挨着SPACE放置,表示栈的结束地址,栈是从高往低生长,所以结束地址就是栈顶地址。
6 w" C$ A& Y5 u5 T  x: L4 b栈主要用于存放局部变量,函数形参等,属于编译器自动分配和释放的内存,栈的大小不能超过内部SRAM 的大小。如果工程的程序量比较大,定义的局部变量比较多,那么就需要在启动代码中修改栈的大小,即修改Stack_Size的值。如果程序出现了莫名其妙的错误,并进入了HardFault的时候,你就要考虑下是不是栈空间不够大,溢出了的问题。4 Z2 i8 U6 w# s' g  f6 p" J$ X
(2)堆空间的开辟
; d( F$ Z5 b5 ~1 C( {# {6 E. d堆空间的开辟,源码如图9.2.2.2所示:" p4 e8 n. Q$ }' o' C) T# }  _; J

# Y! f/ o0 L7 {' e# G* V, ] 44613491a7a6439fbcc47ff41485273c.png 4 M  r8 J0 a: R/ b9 M; \

# l5 P# H% E( Y3 @: Q. v图9.2.2.2堆空间的开辟6 f  t" \8 W7 m
源码含义:开辟一段大小为0x0000 0200(512字节)的堆空间,段名为HEAP,不初始化,可读可写,8字节对齐。
7 ]5 c6 T" m; V- M__heap_base表示堆的起始地址,__heap_limit表示堆的结束地址。堆和栈的生长方向相反的,堆是由低向高生长,而栈是从高往低生长。
  B6 ?% T( M+ t堆主要用于动态内存的分配,像malloc()、calloc()和realloc()等函数申请的内存就在堆上面。堆中的内存一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。
; r; @2 ~3 H2 s+ t接下来是PRESERVE8和THUMB指令两行代码。如图9.2.2.3所示。
6 h1 m% H7 w* U9 P/ C7 D
5 ?, G, y1 P& H7 u' s$ q+ @) l b65f5f77541c4eafa828aa618ea1e54a.png , [0 y6 g& ?& D6 P3 i

! B0 _$ S+ s% Z图9.2.2.3 PRESERVE8和THUMB指令
* G' p  @! z9 ^% H( iPRESERVE8:指示编译器按照8字节对齐。
1 Y" D* {6 c4 M; s+ VTHUMB:指示编译器之后的指令为THUMB指令。& X  ^! B' }3 U* K
注意:由于正点原子提供了独立的内存管理实现方式(mymalloc,myfree等),并不需要使用C库的malloc和free等函数,也就用不到堆空间,因此我们可以设置Heap_Size的大小为0,以节省内存空间。
' d$ E9 h2 e- a3 J(3)中断向量表定义(简称:向量表)- W( v7 ^/ Z$ l$ ^$ b9 Q1 N
为中断向量表定义一个数据段,如图9.2.2.4所示:1 c+ w! X: x; C+ O

0 a" W4 E4 |# |2 H5 a3 O e64d9b55cd44419ca96e6b0bbcce7aeb.png 1 J, Y3 G6 V2 H+ Z

7 v  N4 C$ M3 S. X/ ]- {, a图9.2.2.4 为中断向量表定义一个数据段
( n3 B/ X9 O; i% T源码含义:定义一个数据段,名字为RESET, READONLY表示只读。EXPORT表示声明一个标号具有全局属性,可被外部的文件使用。这里是声明了__Vectors、__Vectors_End和__Vectors_Size三个标号具有全局性,可被外部的文件使用。# ?* ]* t- u+ m, T1 N
STM32H750的中断向量表定义代码,如图9.2.2.5所示。
) w/ n% e) ^3 }! R8 p6 ?7 \' k7 _" t; a2 _' S
b81aba33f3db4ad281ede43a1dea6822.png
' ^% e8 ^* D8 N7 T: i6 T! r
/ s0 W" h' d, c+ x% q2 n. a图9.2.2.5中断向量表定义代码0 g' s  G0 U  m2 k& V
__Vectors 为向量表起始地址, __Vectors_End 为向量表结束地址,__Vectors_Size为向量表大小,__Vectors_Size = __Vectors_End - __Vectors。( R9 Y# e" \- J* v& Y* m* m' p
DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。
6 b6 g+ B8 T; ]" D  G# P) ]7 S中断向量表被放置在代码段的最前面。例如:当我们的程序在FLASH运行时,那么向量表的起始地址是:0x0800 0000。结合图9.2.2.5可以知道,地址0x0800 0000存放的是栈顶地址。DCD:以四字节对齐分配内存,也就是下个地址是0x0800 0004,存放的是Reset_Handler中断函数入口地址。
9 w  i, d/ a6 v" J+ h  d2 E8 B从代码上看,向量表中存放的都是中断服务函数的函数名,所以 C 语言中的函数名对芯片来说实际上就是一个地址。; R, _; M& a/ N) s6 R# o
STM32H750的中断向量表可以在《STM32H7xx参考手册_V3(中文版).pdf》的第19章的19.1.2小节找到,与中断向量表定义代码是对应的。
  ]3 G1 W9 n) n% l! q7 f(4)复位程序
- P- n& S' M0 I' S+ W) _" e6 x% Y! C接下来是定义只读代码段,如图9.2.2.6所示:
% |5 u8 v; b" q7 |) @! r/ j3 k
' h' R+ H: k4 |& X 575df642359344559343611214aa8573.png
: Z- V: W8 @' g% O: _4 S1 D" O/ ]. g6 g* Z  M+ m
图9.2.2.6 定义只读代码段
% N0 C6 W' V2 P5 C! w: V定义一个段命为.text,只读的代码段,在CODE区。
7 ~" A' U# ?' i* H0 _/ O! U7 w: U复位子程序代码,如图9.2.2.7所示:/ ?7 d2 p9 A5 F
- }: X& ?! b# m3 k' E: y: ~
e5e72e7cbf524fbd9c0977a5e810be45.png
. l; l- V# H7 @$ z
, }+ Q! D$ K( k- Q图9.2.2.7 复位子程序代码
  a6 A. ?4 V) I& L) U利用PROC、ENDP这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
$ [* m2 v4 \' d. D6 n' o: B! b/ }复位子程序是复位后第一个被执行的程序,主要是调用SystemInit函数配置系统时钟、还有就是初始化FSMC/FMC总线上外挂的SRAM(可选)。然后在调用C 库函数__main,最终调用 main 函数去到 C 的世界。
0 Y  d' v4 d( f& D0 {EXPORT声明复位中断向量Reset_Handler为全局属性,这样外部文件就可以调用此复位中断服务。
! t9 I" J4 w* VWEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用外部定义的标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。& `) E/ ?. H& ]8 S* ^0 {6 j: D
IMPORT表示该标号来自外部文件。这里表示SystemInit 和__main 这两个函数均来自外部的文件。
9 G2 V9 v- ?- {6 R5 P5 [LDR、BLX、BX 是内核的指令,可在《CM3 权威指南 CnR2》第四章-指令集里面查询到。8 u- w* \, v# Q7 ?3 G5 y
LDR表示从存储器中加载字到一个存储器中。% Y. n% G' _, L% u
BLX表示跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR。
& _) W/ o( T! H' j# cBX表示跳转到由寄存器/标号给出的地址,不用返回。这里表示切换到__main地址,最终调用main函数,不返回,进入C的世界。
5 s6 {( @- o  ?8 c(5)中断服务程序7 e0 c6 E* ]9 j2 ~9 v. M
接下来就是中断服务程序了,如图9.2.2.8所示:
5 y0 T- L8 X, W- Z  X7 J' q/ ^; X3 B1 E% {
b6d59525343c445d91f10957627172b7.png
; b, t$ P9 [* S( {7 R5 B& N3 S+ M
图9.2.2.8 中断服务程序
0 F1 R7 u7 `1 z, E可以看到这些中断服务函数都被[WEAK]声明为弱定义函数,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不会出错。
: R6 P) K7 K  w% I/ c0 U) O这些中断函数分为系统异常中断和外部中断,外部中断根据不同芯片有所变化。B指令是跳转到一个标号,这里跳转到一个‘.’,表示无限循环。
$ M' z, b, y( G+ m- j! g# r0 }在启动文件代码中,已经把我们所有中断的中断服务函数写好了,但都是声明为弱定义,所以真正的中断服务函数需要我们在外部实现。  @8 h+ F9 [0 m4 n$ Q* e3 Q. ?
如果我们开启了某个中断,但是忘记写对应的中断服务程序函数又或者把中断服务函数名写错,那么中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,并且在B指令作用下跳转到一个‘.’中,无限循环。
% Z  |6 x/ j9 |/ e6 l这里的系统异常中断部分是内核的,外部中断部分是外设的。
) B0 H$ s  m  N& \* Y(6)用户堆栈初始化
" i7 i0 h9 N. c6 j% H; ]ALIGN指令,如图9.2.2.9所示:. B' Y9 W" {" J: ?
5 F2 E+ O2 s( k; e4 ~; V" r
88360910d42943f484abacfa17fac746.png % o8 f+ V) F$ S
# {. |9 `6 j; \/ a3 `; _+ u
图9.2.2.9 ALIGN指令
# ?( f5 `4 O% vALIGN表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4字节对齐。要注意的是,这个不是ARM的指令,是编译器的。
" _% Z7 S. ?. V接下就是启动文件最后一部分代码,用户堆栈初始化代码,如图9.2.2.10所示:3 Q4 p& x/ I: f! J
+ z5 E) V: ?& a# I
af7d3e59f7b84599a41c4a2ebb7ae10f.png " J) H1 B, R/ y8 x* E5 ^6 M

5 c$ _) j; I/ `) H图9.2.2.10 用户堆栈初始化代码9 Y8 I6 h( [5 w2 p3 u( J
IF, ELSE, ENDIF是汇编的条件分支语句。) e9 S, X, T# K/ R4 ~1 g6 R- v) U
588行判断是否定义了__MICROLIB。关于__MICROLIB这个宏定义,我们是在KEIL里面配置,具体方法如图9.2.2.11所示。
0 P7 k$ i! e9 r
% U" N# u4 O" s( e4 j b455e9afde41471cb98a0feb15fb7b14.png 0 m7 G3 c( k1 a) L% G
8 g" T( p" r8 R+ ]
图9.2.2.11 __MICROLIB定义方法' `! a9 j; Q: t% k2 F# l. P
勾选了Use MicroLIB就代表定义了__MICROLIB这个宏。/ u4 |" i0 V' C, y5 Y9 O
如果定义__MICROLIB,声明__initial_sp、__heap_base和__heap_limit这三个标号具有全局属性,可被外部的文件使用。__initial_sp表示栈顶地址,__heap_base表示堆起始地址,__heap_limit表示堆结束地址。
0 Z4 `# P$ K; V9 F# ~如果没有定义__MICROLIB,实际的情况就是我们没有定义__MICROLIB,所以使用默认的C库运行。那么堆栈的初始化由C库函数__main来完成。7 C- ]9 o% @& Y( _. X: N
IMPORT声明__use_two_region_memory标号来自外部文件。
: b2 j. ?- j& d2 h7 cEXPORT声明__user_initial_stackheap具有全局属性,可被外部的文件使用。2 w5 N, p6 A2 O1 E" \, I& x2 o! R) u
599行标号__user_initial_stackheap,表示用户堆栈初始化程序入口。8 ?  Y7 ^1 f0 P" H9 u% k- ]
接下来进行堆栈空间初始化,堆是从低到高生长,栈是从高到低生长,是两个互相独立的数据段,并且不能交叉使用。9 E3 l( q- {: Q* k- J. V: ^
601行保存堆起始地址。* e3 K8 w# v. A, {9 e; s9 c4 I
602行保存栈大小。9 `" H: Y1 S7 l& j9 f1 p1 u
603行保存堆大小。
! {$ y* v: ^/ ?$ ~6 U3 z1 p% ?) ?604行保存栈顶指针。) s; H! u. Y7 ]' ?# U
605行跳转到LR标号给出的地址,不用返回。+ O& ^' z" h4 f7 |9 c. n% h
611行END表示到达文件的末尾,文件结束。% Z7 R* `2 M& V' y' I
Use MicroLIB
9 _  g# q" v) x2 ^- C0 b& [# N7 g! nMicroLIB是MDK自带的微库,是缺省C库的备选库,MicroLIB进行了高度优化使得其代码变得很小,功能比缺省C库少。MicroLIB是没有源码的,只有库。
0 U4 C" I2 L2 ^# j- N
1 c3 w( c2 U7 t6 M* f8 g- u9.2.3 系统启动流程% P4 I% c4 v/ C% Y) [0 R" |" g
我们知道启动模式不同,启动的起始地址是不一样的,下面我们以代码下载到内部FLASH的情况举例,即代码从地址0x0800 0000开始被执行。2 B+ e: B% |' Z! q
当产生复位,并且离开复位状态后,CM7内核做的第一件事就是读取下列两个32位整数的值:
, [+ q. q& x8 \) e0 o4 S( |(1)从地址 0x0800 0000 处取出堆栈指针MSP 的初始值,该值就是栈顶地址。
- D5 s4 J" o* G* u% B(2)从地址 0x0800 0004 处取出程序计数器指针PC 的初始值,该值指向中断服务程序 Reset_Handler。下面用示意图表示,如图9.2.3.1所示。! G. |  j8 |: K. J  Q( K

# K* r7 h+ S  F% o6 p4 E4 ?+ c+ { 01b45f78bf7f432790ee7d896d4625d5.png
) I, z0 Y; ^! B; U$ Y4 N; t3 N- e* C2 O" y
图9.2.3.1 复位序列
: }. l% Q% u% ~, Z我们看看MiniPRO STM32H750开发板HAL库例程的实验1 跑马灯实验中,取出的MPS和PC的值是多少,方法如图9.2.3.2所示。
" d+ v8 N  o9 C5 e" n8 P& p
3 X! B. m0 K$ l5 ]* Q, i' \ 2036ddce39304e2eb21b70744502c693.png
+ p8 O4 j7 [% I8 v# q4 @. \- w2 e
  n2 }9 J0 q# a6 ]6 n图9.2.3.2 取出的MPS和PC的值
- K3 y4 i  d" o+ H: X0 V" A3 D0 K由图9.2.3.2可以知道地址0x0800 0000的值是0x2400 0BD8,地址0x0800 0004的值是0x0800 0339,即堆栈指针 SP =0x2400 0BD8,程序计数器指针PC = 0x0800 0339(即复位中断服务程序Reset_Handler的入口地址),即。因为CM7内核是小端模式,所以倒着读。- |# r( U  R% b4 i" L3 D
请注意,这与传统的 ARM 架构不同——其实也和绝大多数的其它单片机不同。传统的ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。而在 CM3内核中,0 地址处提供 MSP 的初始值,然后就是向量表(向量表在以后还可以被移至其它位置)。向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令,就是Reset_Handler这个函数。下面继续以正点原子MiniPRO STM32H750开发板HAL库例程的实验1跑马灯实验为例,代码从地址0x0800 0000开始被执行,讲解一下系统启动,初始化堆栈、MSP和PC后的内存情况。
) D7 q% A0 y! t; F. F6 a5 w5 t
# B+ E4 |; Q# J* M4 s9 j4 k 5b140163c5684b11b983d81eb24eec08.png
: j" }  d: `* w  ]) E, F. ?/ ]. O
5 @  I5 R  c% h/ H图9.2.3.3 初始化堆栈、MSP和PC后的内存情况
5 J( Q8 p0 I3 u6 e因为CM3使用的是向下生长的满栈,所以MSP的初始值必须是堆栈内存的末地址加1。
. A$ {# T8 g  |5 W# b9 F/ K* r举例来说,如果你的栈区域在 0x2400 03D8‐0x2400 0BD4 (2KB大小)之间,那么 MSP 的初始值就必须是0x2400 0BD8。
: t, P1 a* x: Q, z' L0 I! p向量表跟随在 MSP 的初始值之后——也就是第 2 个表目。
, {7 }7 z6 @" F. o  H! T* x; ^% hR15是程序计数器,在汇编代码中,可以使用名字“PC”来访问它。ARM规定:PC最低两位并不表示真实地址,最低位LSB用于表示是ARM指令(0)还是Thumb指令(1),因为 CM3 主要执行 Thumb指令,所以这些指令的最低位都是1(都是奇数)。因为 CM3 内部使用了指令流水线,读 PC 时返回的值是当前指令的地址+4。比如说:1 c3 w- h. I. F5 J
0x1000: MOV R0, PC ; R0 = 0x1004+ v' [5 E6 ^) I
如果向 PC 写数据,就会引起一次程序的分支(但是不更新 LR 寄存器)。CM3 中的指令至少是半字对齐的,所以 PC 的 LSB 总是读回 0。然而,在分支时,无论是直接写 PC 的值还是使用分支指令,都必须保证加载到 PC 的数值是奇数(即 LSB=1),表明是在Thumb 状态下执行。倘若写了0,则视为转入 ARM 模式,CM3 将产生一个fault异常。- U9 l. ?& f. z9 [2 ]
正因为上述原因,图9.2.3.3中使用 0x0800 0339 来表达地址 0x0800 0338。当0x0800 0339 处的指令得到执行后,就正式开始了程序的执行(即去到C的世界)。所以在此之前初始化 MSP 是必需的,因为可能第 1 条指令还没执行就会被 NMI 或是其它 fault 打断。MSP 初始化好后就已经为它们的服务例程准备好了堆栈。
1 Y# d: R* d+ H! }  C# l6 xSTM32启动文件分析就给大家介绍到这里,更多内容请看 《STM32 启动文件浅析_V1.1》 。4 c8 z  f! X  P9 a: E7 v
* B5 L" Y: h, b4 G' u% q8 E
9.3 map文件分析
2 ]9 \3 m* C, a# ~- t! i7 U6 {9.3.1 MDK编译生成文件简介
9 e) L% |% f7 I" ]  i, b
MDK 编译工程,会生成一些中间文件(如.o、.axf、.map 等),最终生成 hex 文件,以便下载到 MCU 上面执行,以MiniPRO STM32H750开发板HAL库例程的实验1 跑马灯实验为例(其他开发板类似),编译过程产生的所有文件,都存放在 OBJ 文件夹下,如图 9.3.1.1 所示:; i. S" M" [% _5 F

- I9 J. ]- K% O0 @* ]- K8 H! A( ^ 52ed71c9a10e4b09bfbe08386f7157fc.png 4 `# N2 i3 n& V+ i. u4 ?

4 w% G  G& A5 x' m1 S- x$ A图 9.3.1.1 MDK编译过程生成的文件
( A! F* U/ Y; Y可以看到,这里总共生成了 48 个文件,共 11 个类型,分别是:.axf、.crf、.d、.dep、
! G9 J( B/ @6 I' P6 E4 K.hex、.lnp、.lst、.o、.htm、bulild_log.htm 和.map。48 个文件看着不是很多,但是随着工程的增大,这些文件也会越来越多,大项目编译一次,可以生成几百甚至上千个这种文件,不过文件类型基本就是上面这些。9 M! d0 l; j+ _
对于 MDK 工程来说,基本上任何工程在编译过程中都会有这 11 类文件,常见的 MDK编译过程生产文件类型如表 9.3.1.1所示:: w- \* y0 o: V9 d8 O
/ @7 q7 A* E3 m+ N3 d  Q
be70879c28204985b040739660a4c3a6.png
, O9 P, j: f7 s+ d. }% p  Q
( V" R( y) R7 ~) k3 v. D' E表 9.3.1.1 常见的中间文件类型说明
: B; X1 a  v0 r$ R  H注 1,可重定向是指该文件包含数据/代码,但是并没有指定地址,它的地址可由后续链接的时候进行指定。
7 u, `9 s- T$ p# K注 2,不可重定向是指该文件所包含的数据/代码都已经指定地址了,不能再改变。
- z! ^3 f9 t% `0 _+ a2 N7 Q' D' z- ~
9.3.2 map文件分析8 x* r& A- h. |9 m
.map 文件是编译器链接时生成的一个文件,它主要包含了交叉链接信息。通过.map 文件,我们可以知道整个工程的函数调用关系、FLASH 和 RAM 占用情况及其详细汇总信息,能具体到单个源文件(.c/.s)的占用情况,根据这些信息,我们可以对代码进行优化。.map 文件可以分为以下 5 个组成部分:
: T4 l; p' n, W9 i3 N6 S6 H7 j1, 程序段交叉引用关系(Section Cross References)& s8 N5 u. J& d. k$ G
2, 删除映像未使用的程序段(Removing Unused input sections from the image); d4 ]: ]$ q4 o5 k
3, 映像符号表(Image Symbol Table)  B2 i4 u1 M! k* X# P  y% _
4, 映像内存分布图(Memory Map of the image)
" Y8 Q. T8 r# J5, 映像组件大小(Image component sizes)
6 d/ @8 q/ w8 I- Z9.3.2.1 map 文件的 MDK 设置
: s. W2 Z% b' B, K6 P要生成 map 文件,我们需要在 MDK 的魔术棒→Listing 选项卡里面,进行相关设置,如图 9.3.2.1.1所示:# A% _4 x0 s$ v( c. ]
; A( Y, G  L/ l- z
cbb062f747484a25aa688478f2b9016b.png   O# q2 E' f2 v" O
6 e, }* h5 n! Q$ s4 H) K
图 9.3.2.1.1 .map 文件生成设置
, u' U3 ?4 ^0 o& K: b图 9.3.2.1.1中红框框出的部分就是我们需要设置的,默认情况下,MDK 这部分设置就是全勾选的,如果我们想取消掉一些信息的输出,则取消相关勾选即可(一般不建议)。0 \$ k( ?1 R# l6 E0 v8 [' B
如图9.3.2.1.1设置好MDK以后,我全编译当前工程,当编译完成后(无错误),就会生成.map文件。在 MDK 里面打开.map 文件的方法如图 9.3.2.1.2所示:
# e9 r' M$ s. Y$ A
+ t6 `/ O! W) l* k! w: N) n 97afa890900c4329a338ecf9eba83d65.png ' p- p/ {0 W% ^$ L0 E8 q! M& @
4 K: W- j7 |; s+ B# [0 \
图9.3.2.1.2 打开.map 文件
+ `5 R5 T% b1 A6 e4 c①,先确保工程编译成功(无错误)。
, P; [6 n, C! [  c; i# y②,双击 LED,打开.map 文件。
- ~9 S& o) g; ~( [$ r6 b" }9 j& ^, \③,map 文件打开成功。
5 X$ Z7 P2 z' ~7 ~" E  Z9 M9.3.2.2 map 文件的基础概念
, r7 |$ g0 G' k1 h, F2 S7 o+ g为了更好的分析 map 文件,我们先对需要用到的一些基础概念进行一个简单介绍,相关概念如下:; z% R9 l* P9 S
Section:描述映像文件的代码或数据块,我们简称程序段2 x/ E+ G2 h3 D& x6 J
RO:Read Only 的缩写,包括只读数据(RO data)和代码(RO code)两部分内容,占用 FLASH 空间
9 C* {) v9 X# o/ ORW:Read Write 的缩写,包含可读写数据(RW data,有初值,且不为 0),占用 FLASH(存储初值)和 RAM(读写操作)
/ J; c5 V" F6 x& L0 n! r/ K" `ZI:Zero initialized 的缩写,包含初始化为 0 的数据(ZI data),占用 RAM 空间
. H0 x9 C, g' Y( }" ?. O/ e.text:相当于 RO code
! H" x! t/ i+ X" e4 h! m.constdata:相当于 RO data  x& d) ^/ ~$ U. j% N
.bss:相当于 ZI data2 \7 ~1 U9 G8 @2 Y) |1 r$ s- b
.data:相当于 RW data
; p# C* R  Q$ w
$ k! z: ^9 h" ]% S/ E. U# g9.3.2.3 map 文件的组成部分说明8 ]2 [4 u5 k- d: X, H0 t
我们前面说map 文件分为 5 个部分组成,下面以MiniPRO STM32H750开发板HAL库例程的实验1 跑马灯实验为例,简要讲解一下。
/ d$ _- X9 K- S# Q1.程序段交叉引用关系(S S ection Cross References s )9 T6 d7 o1 w. {* i% o- F' ^9 ]- C
这部分内容描述了各个文件(.c/.s 等)之间函数(程序段)的调用关系,举个例子如图9.3.2.3.1所示:
4 i! j( h, p; m: N* }
" K5 K8 [' W' I' J; v c09b1852bf4c461d84c96a067d9dd699.png
! @- p  Z" _. L, @* P8 {- j; b) @* q' N
图9.3.2.3.1 程序段交叉引用关系图
- P& ?) Q# z9 |$ _  v, W6 A, d上图中,框出部分:main.o(i.main) refers to sys.o(i.sys_stm32_clock_init) for sys_stm32_; Y9 x6 y4 n: d; q- |8 R3 P
clock_init表示:main.c 文件中的 main 函数,调用了 sys.c 中的 sys_stm32_clock_init 函数。其中:i.main表示 main 函数的入口地址,同理 i. sys_stm32_clock_init 表示 sys_stm32_' H# i- v* J* }2 f5 [: \
clock_init函数的入口地址。
- {$ p3 s: F0 a- ], A2. 删除映像未使用的程序段(Removing Unused input sections from the image)
% i" Z0 q2 i# x* D( z+ p9 G这部分内容描述了工程中由于未被调用而被删除的冗余程序段(函数/数据),如图; |: \9 q9 U( {3 e5 z9 _: [
9.3.2.3.2 所示:! ^7 w* _5 u! Y, u: n

% z, K& Z- S/ s( v1 G 819941de50d444d09c85f180b223a7a3.png
7 q; }6 [5 c2 G  c
; U" j6 x" _9 W5 Z8 H4 m) M  G图9.3.2.3.2 删除未用到的程序段
5 ]8 N# u% N/ ~4 e上图中,列出了所有被移除的程序段,比如 usart.c 里面的 usart_init 函数就被移除了,因为该例程没用到 usart_init 函数。
6 p- c" c4 E/ X$ Z9 j- _/ x另外,在最后还有一个统计信息:361 unused section(s) (total 43234 bytes) removed from the image. 表示总共移除了361个程序段(函数/数据),大小为43234字节。即给我们的 MCU 节省了 43234 字节的程序空间。- e; {( ~2 o  D5 a3 q& l( ?
为了更好的节省空间,我们一般在 MDK→魔术棒→C/C++选项卡里面勾选:One ELF
! m; p3 W7 a) h9 ySection per Function,如图 9.3.2.3.3所示:
  D1 u. b# }3 |) p. t" r) O. T) j# m/ J8 Y' O+ A" d
258372a3a7c8402495537c60571eac69.png 7 U: O; f2 F% H7 K
( L8 t$ h. H! d- D6 f5 D7 P$ C
图9.3.2.3.3 MDK 勾选 One ELF Section per Function
$ V/ n5 x: r+ J
" ~, o, t3 c/ ]( {+ I* h. Q3. 映像符号表(Image Symbol Table)" {2 C  l, ^: }! f! l
映像符号表(Image Symbol Table)描述了被引用的各个符号(程序段/数据)在存储器中的存储地址、类型、大小等信息。映像符号表分为两类:本地符号(Local Symbols)和全局符号(Global Symbols)。
4 P- r/ D* Y3 f本地符号(Local Symbols)记录了用 static 声明的全局变量地址和大小,c 文件中函数的地址和用 static 声明的函数代码大小,汇编文件中的标号地址(作用域:限本文件)。
# Q% a7 |$ r; z# h全局符号(Global Symbols)记录了全局变量的地址和大小,C 文件中函数的地址及其代码大小,汇编文件中的标号地址(作用域:全工程)。
& |$ D7 R, d; |' p4. 映像内存分布图(Memory Map of the image)
9 Y) n4 I9 _; n; F* w$ ~( B映像文件分为加载域(Load Region)和运行域(Execution Region),一个加载域必须有至少一个运行域(可以有多个运行域),而一个程序又可以有多个加载域。加载域为映像程序的实际存储区域,而运行域则是 MCU 上电后的运行状态。加载域和运行域的简化关系(这里仅表示一个加载域的情况)图,如图9.3.2.3.4所示:
- d$ _+ y1 t0 B: r) W; T" C( ]4 G3 W& P( S( X7 \
e1139251865c4e5cb0806982b7b13f8f.png 0 i! |9 W  J. h0 K' G
( a2 E1 G* g& x, f
图9.3.2.3.4 加载域运行域关系+ L: Z( E  A6 O; r. \
由图可知,RW 区也是存放在 ROM(FLASH)里面的,在执行 main 函数之前,RW(有初值且不为 0 的变量)数据会被拷贝到 RAM 区,同时还会在 RAM 里面创建 ZI 区(初始化为 0 的变量)。
! ~# N- w, l- W' W- K( T- I1 C1 g2 X8 i; n+ h3 S
5. 映像组件大小(Image component sizes)0 t0 n# u4 _( B2 G" q6 ]$ V# T0 L
映像组件大小(Image component sizes)给出了整个映像所有代码(.o)占用空间的汇总信息。
9 s* X* k& o: Z" N' T由于篇幅较长,更多内容请大家查阅 《STM32 MAP文件浅析_V1.1.pdf》文档的内容。
: I+ m: _) M, V7 h* d————————————————9 n# L# d4 w/ @1 s2 q1 N) Q3 s
版权声明:正点原子9 @0 h% ~7 o; `7 o( X$ G
7 `; {4 K6 T' R4 K4 ^, p/ J

/ E: G( L. r. `3 t5 F8 Z: t
收藏 评论0 发布时间:2022-10-5 21:42

举报

0个回答
关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新和工艺
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版