STM32H7的启动过程分析: a' m% g/ K9 @( U6 h( o: ]4 f
启动文件
% n" v8 Z5 u& m" a0 m不同编译器对应的启动文件不同,在MDK下,以startup_stm32h743xx.s为例,这是一个汇编文件,启动文件中主要做了如下事情:
/ C. x4 t9 c4 O( ~# y- F设置堆栈指针 SP = __initial_sp。+ e6 k6 s. T% X1 ~) L3 g$ w3 E7 [
设置 PC 指针 = Reset_Handler。7 s1 \. }' T: @2 b, N8 c0 y
设置中断向量表。
. r! z# @+ u, N9 ^' c8 h* E配置系统时钟。: w7 O0 W' W' H3 \+ j: ]4 }( x
配置外部 SRAM/SDRAM 用于程序变量等数据存储(这是可选的)。% v* B/ |- c1 g( L; E& c" @
跳转到 C 库中的 __main ,最终会调用用户程序的 main()函数。
1 w8 ? Y; s) d A' q( x' G( uCortex-M 内核处理器复位后,处于线程模式,指令权限是特权级别(最高级别),堆栈设置为用主堆栈 MSP。0 I* D2 A8 A' p5 ~
堆栈指针0 t, R. G0 D8 h+ V A1 b. h
通用寄存器组8 e) X! e( B2 W' J
Cortex – M7/M4/M3 处理器拥有 R0-R15 的通用寄存器组。其中 R13 作为堆栈指针 SP。 SP 有两个,但在同一时刻只能有一个可以用。
9 \$ _- k9 X! O1 ?$ Z主堆栈指针(MSP):这是缺省的堆栈指针,它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。( q7 {. w2 B1 n- S9 ], r! {; \. ?6 D
进程堆栈指针(PSP):用于常规的应用程序代码(不处于异常服务例程中时)。: B0 I% H, f+ b# I% b: Y
另外以下两点要注意:
0 Y7 j9 L5 a4 U% t( b4 a7 H大多数情况下的应用,只需使用指针 MSP,而 PSP 多用于 RTOS 中。6 m- A- ]* |) W1 b
R13 的最低两位被硬线连接到 0,并且总是读出 0,这意味着堆栈总是 4 字节对齐的。
5 ?! D7 O: T/ m/ M4 \6 v) _# s) s. |& i4 C+ D
Cortex-M7/M4/M3 向下生长的满栈
" d' |, b6 G' i5 ~PUSH 入栈操作:SP 先自减 4,再存入新的数值
) W$ U2 t! h2 L* I' Y& y. E$ ZPOP 出栈操作:先从 SP 指针处读出上一次被压入的值,再把 SP 指针自增 4
* d G0 P7 [: m! O; I" d1 R1 K
# T, ?# T- P6 X% Q分析启动过程1 |. Y- _" a) q- w/ P7 R9 K% ?
硬件上电后,会触发硬件复位,复位之后,CPU 内的时序逻辑电路首先完成如下两个工作(程序代码下载到内部 flash 为例,flash首地址 0x0800 0000)
7 _ c- l6 L( }6 F% _7 P9 @8 G$ {8 n" M- X5 m" H
将 0x08000000 位置存放的堆栈栈顶地址存放到 SP 中(MSP)。$ c7 c% j- R; K+ d9 b2 b w
将 0x08000004 位置存放的向量地址装入 PC 程序计数器。
2 q4 M* p+ o# C: j! L0 _6 oCPU 从 PC 寄存器指向的物理地址取出第 1 条指令开始执行程序,也就是开始执行复位中断服务程序 Reset_Handler。为啥,因为在启动文件中,最先做的两件事情是
1 ]) S, P1 V6 L4 ]3 x设置堆栈指针 SP = __initial_sp
3 i: n7 L K* Q# @! X: _" a- V设置 PC 指针 = Reset_Handler, l$ Z- u& m0 y3 C" n4 w" y
复位中断服务程序会调用SystemInit()函数来配置系统时钟、配置FMC总线上的外部SRAM/SDRAM,然后跳转到 C 库中__main 函数。由 C 库中的__main 函数完成用户程序的初始化工作(比如:变量赋初值等),最后由__main 函数调用用户写的 main()函数开始执行 C 程序。
& `0 _2 S( K# G3 \& q# j$ f
9 D; S4 K' v8 B3 {# {# _! S7 {0 H代码分析7 d' U' H5 Z- X0 u. R( M+ x' z9 C& i
①、开辟栈(stack)空间,用于局部变量、函数调用、函数的参数等. m( a* |8 _: Z# k; o
" t( s6 a; `6 t; g' D- z2 T- R
- //类似宏定义,这是个伪指令,定义栈大小,这里是以字节为单位; z6 z0 C# s9 `/ Y" X) ^
- Stack_Size EQU 0x00001000 ) F% _9 \! l0 ?9 P
- * `* }: ^5 H; z5 Q1 d* _3 _
- /* p0 d- O+ X$ A! p H# t5 a
- 开辟一段数据空间可读可写,段名 STACK,按照 8 字节对齐。 ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。 ARER 后面的关键字表示这个段的属性。/ X; ~5 }2 m% S+ G% ~0 G/ t$ ^
- STACK :表示这个段的名字,可以任意命名。# w) M4 F, _) o/ m: C
- NOINIT:表示此数据段不需要填入初始数据。- d3 ]* n9 F8 g! i( Y
- READWRITE:表示此段可读可写。
) P/ [2 O2 q8 o5 r/ m - ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(地址对 8 求余数等于 0)。
6 V$ ?; t5 E& T3 T8 L: u a - */) ]$ M$ N. F4 v2 u m4 @. b
- AREA STACK, NOINIT, READWRITE, ALIGN=38 y7 F# y) r4 x8 U7 S9 k2 g$ r
- % f! N! Q; J, ?; _/ p+ ?
- //SPACE 这行指令告诉汇编器给 STACK 段分配 0x00001000 字节的连续内存空间。
( s3 [( l/ s- F - Stack_Mem SPACE Stack_Size
1 K7 E9 ~, L! o! @' J6 n) K
; O" k& L7 q' B; r- /*
, |6 S7 E9 g2 e' f+ P: c0 T# T - __initial_sp 紧接着 SPACE 语句放置,表示了栈顶地址。 __initial_sp 只是一个标号,标号主要用于表示一片内存空间的某个位置,等价于 C 语言中的“地址”概念。地址仅仅表示存储空间的一个位置,从 C 语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。
" m1 z, s7 h4 ]( @7 Q! I1 H - */) X8 x- Z$ `# T- `! U
- __initial_sp
复制代码
3 c9 | | o% ^4 A①、开辟堆(heap)空间,主要用于动态内存分配,也就是说用 malloc,calloc, realloc 等函数2 c/ `( x$ S/ o" f% @
分配的变量空间是在堆上
. P" s, d8 O9 Y/ O6 }
2 k8 `$ I l% Y- //定义堆空间大小
p* S* y2 S6 F( c( B% ~ - Heap_Size EQU 0x0000800
0 L) ~9 e3 w" z; p1 ]# L" U4 Z - . L7 m- H& \4 S9 g* M7 l Z2 _
- //分配一段名字为HEAP、可读可写、不需要初值、8字节对齐的数据空间6 F/ Y5 z* k& O8 e
- AREA HEAP, NOINIT, READWRITE, ALIGN=3 h3 v8 c) \7 Z. H0 ~" X
- ! ]' q/ X- j# U8 ~* m, h8 [8 L( J
- //__heap_base 表示堆的开始地址。/ N( h3 i/ M3 Y
- __heap_base0 ?$ ?0 ?2 |) i. h5 R3 A& O
- e2 {) p* t0 k, Q+ w7 k; v
- //SPACE 这行指令告诉汇编器给 HEAP段分配 0x0000800字节的连续内存空间。6 w# p7 j8 G& c. N% _
- Heap_Mem SPACE Heap_Size
+ U) ^4 ~* U( c' ]: }0 q2 D2 m
# w) h& ^/ h: Q- S- __heap_limit 表示堆的结束地址
/ K3 [5 }, `5 w6 m% V" b - __heap_limit
复制代码 ; ^& C% B1 y) L0 W. {
③、生成属性设置、定义RESET代码段! Y1 e" Z( i4 C( s5 u, l7 `3 G
/ F! N' F8 {1 G
- //PRESERVE8 指定当前文件保持堆栈八字节对齐。4 l" V/ B! q2 K
- PRESERVE86 a. R( ~1 B6 c) p) W. @0 g8 b1 J
. S! p' t4 g( @- //THUMB 表示后面的指令是 THUMB 指令集 ,CM7 采用的是 THUMB - 2 指令集
* Q! C, A1 X, C# c1 N- z - THUMB
/ F2 k3 y% f, d
) k: S x9 C4 Y, ]# ]9 ?8 x2 I- //:AREA 定义一块代码段,只读,段名字是 RESET。 READONLY 表示只读,缺省就表示代码段了。
! Q& K& ~7 b9 R& X+ R' W - ; Vector Table Mapped to Address 0 at Reset
6 S7 L! k! y* O1 L* {- J' q - AREA RESET, DATA, READONLY3 \4 X/ D$ ~5 \8 o5 S
. `" G4 A. n1 f$ t+ C" f# ~% U- //3 行 EXPORT 语句将 3 个标号申明为可被外部引用, 主要提供给链接器用于连接库文件或其他文件。4 b6 \9 B: z! t0 L1 r f7 w
- EXPORT __Vectors" t. O9 c' a/ E/ ]- B: C
- EXPORT __Vectors_End
" l8 Q, N; U2 N: ] - EXPORT __Vectors_Size
复制代码
. O1 e9 ^) i3 f4 W$ a④、中断向量变定义4 Y! E8 h& l1 U6 j; @! g
4 G8 t1 m1 [/ s
- __Vectors DCD __initial_sp ; Top of Stack
$ `5 ]2 k; {) M& @0 Q - DCD Reset_Handler ; Reset Handler; T6 h4 u0 L% {" c
- DCD NMI_Handler ; NMI Handler
- v1 G: i1 W4 F% J! m" v - DCD HardFault_Handler ; Hard Fault Handler
I/ s4 O1 f- M, D! S7 r - /* 省略部分代码 */
! c+ k, J/ r" G/ F8 d7 b - DCD 0 ; Reserved 3 s6 o0 a. j% n" D3 h- c
- DCD WAKEUP_PIN_IRQHandler ; Interrupt for all 6 wake-up pins
9 ^/ h1 s" Z" V! n( e- E - - x8 S L4 i& C. Z/ h
- & \! c% n- C# h! v4 T2 u4 D" W
- __Vectors_End8 R( t( l& k' [& a9 d8 ?& O( p
3 ]; T! b: p6 O2 Y- //定义向量表大小
; h5 Z1 s; v2 e8 { - __Vectors_Size EQU __Vectors_End - __Vectors
复制代码 2 F E. m0 N+ a* k: O: l
上面的这段代码是建立中断向量表,中断向量表定位在代码段的最前面。具体的物理地址由链接器的配置参数(IROM1 的地址)决定。如果程序在 Flash 运行,则中断向量表的起始地址是0x08000000。以 MDK 为例,就是如下配置选项:
& N+ [& o& w1 [) K( X% e: m1 P+ W- k' n6 a
3 j) s& a' X5 L% x7 n
a2 H/ ~ c: }' K) zDCD 表示分配 1 个 4 字节的空间。每行 DCD 都会生成一个 4 字节的二进制代码。中断向量表存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序
2 s2 r9 c7 O. R2 r. |! p. ~1 H5 H" J, e5 h& @: {
⑤、代码段定义、Reset_Handler过程处理/ F/ r, ]/ k+ u) w; @. [3 _7 I
/ L0 F: o% ?1 i2 k0 |$ a- AREA |.text|, CODE, READONLY
7 }( o& {/ r* Q6 P: g2 G
; { M5 g# ?2 Q% _2 m9 l* r. R0 `- ; Reset handler, j ?7 A4 k) J: W+ ?! L
- Reset_Handler PROC3 }2 @3 v( H: |, J O% |
- EXPORT Reset_Handler [WEAK]
# B8 w y# y* p/ X$ q: L! N - IMPORT SystemInit% C0 J& u/ d# `
- IMPORT __main+ K0 f9 X7 Z0 g: a9 c, i2 x: W
8 m5 d% ^& C' Q/ @- LDR R0, =SystemInit7 B& ]- H2 X* n1 A/ e
- BLX R0
- L" M5 K* J% ~; _ - LDR R0, =__main
0 ]; a5 O2 d3 R1 P) Q* Z$ ~ - BX R01 S. A( `) U# Y
- ENDP
复制代码 - G2 S( U! U3 ]0 C
AREA 定义一块代码段,只读,段名字是 .text 。 READONLY 表示只读! g! A7 M9 w' r* |1 l/ B
利用 PROC、 ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
& G) W0 }: K& R/ BWEAK 声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话会调用外面的。 这个声明很重要,它让我们可以在 C 文件中任意地方放置中断服务程序,只要保证 C 函数的名字和向量表中的名字一致即可。
. E' @/ o8 \9 N' H% P# j! qIMPORT:伪指令用于通知编译器要使用的标号在其他的源文件中定义。但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。9 D y6 [3 t" _# g
SystemInit 函数在文件 system_stm32h7xx.c 里面,主要实现 RCC 相关寄存器复位和中断向量表位置设置。& u3 e4 ^ M4 y) e: `, h6 ]
__main 标号表示 C/C++标准实时库函数里的一个初始化子程序__main 的入口地址。该程序的一个主要作用是初始化堆栈(跳转__user_initial_stackheap 标号进行初始化堆栈的,下面会讲到这个标号),并初始化映像文件,最后跳转到 C 程序中的 main 函数。这就解释了为何所有的 C 程序必须有一个 main 函数作为程序的起点。因为这是由 C/C++标准实时库所规,并且不能更改。
6 C b, ~! i0 S/ E# Y; S. w
- T \* Z% |6 H) `6 ]$ g⑥、缺省中断服务函数定义- ; Dummy Exception Handlers (infinite loops which can be modified)- I2 ?- m$ D5 m! M
- % n. i! \8 ?; v1 @, P( q
- NMI_Handler PROC3 G3 L, D5 \- a
- EXPORT NMI_Handler [WEAK]9 r- }* F: l$ L0 c. [/ c
- B . //死循环$ Y( {# R, M# ]- Z9 P( ^
- ENDP$ V+ R; g" N" l; i
- ...省略
3 t9 o2 x9 J+ q' f7 d' z3 p2 R - EXPORT PendSV_Handler [WEAK]) n' y- e, f/ j9 D) m2 N+ v
- B .$ u* t" y$ a5 D& P2 _
- ENDP( N+ S& \& g$ [3 X
- SysTick_Handler PROC* r+ Y- }6 Y7 t, n7 i
- EXPORT SysTick_Handler [WEAK]
7 H) q$ V6 B' I r1 E - B .
# {8 ~. _5 f" x. @& y - ENDP
1 k6 N; R0 J9 r; J$ u+ s' h - $ p" ~( p" B5 A
- Default_Handler PROC * I8 y" f) |0 D( V+ o
3 m: `6 R2 t* F0 r: `* p+ F- EXPORT WWDG_IRQHandler [WEAK] 8 k& g, o. ~3 p" w2 k1 m# X
- EXPORT PVD_AVD_IRQHandler [WEAK]
! }( |- X/ U( W2 u+ X) _9 Y) K4 [ - ...省略
/ C7 q1 z6 e0 J9 s - EXPORT WAKEUP_PIN_IRQHandler [WEAK] ; j/ g# r6 f" p1 l2 w4 F
' S9 p- v! s+ x
8 j' Y: F: J: K, h- WWDG_IRQHandler R I4 H% x; K. ~1 l
- ...省略 & [6 Y- g: ^3 B2 L
- WAKEUP_PIN_IRQHandler) \! D* X7 t/ ^# \( O7 n1 V
7 T: O% g l1 H. {$ p( \ j# \- i- B .& d; H: t3 J# E1 j2 e( z& k7 |8 m
4 n9 y: p( j/ A9 @+ G- ENDP
: B9 F0 @" [% m: B - 0 C. @5 |+ e5 v% x6 ^# Y5 i, g
- ALIGN
复制代码 * K* b3 ^4 p0 ~' V
这里全部的中断服务函数都是用[WEAK]来声明,假如没有在其他文件中定义同名的中断服务函数,来了中断,就会进入到这里。
% q9 @% t4 o7 ~8 s
3 R: {3 [! O& j5 F) ^+ _; U+ f) K⑦、堆和栈的初始化
' y! [- I4 u6 H
1 D, A/ f) o, E7 o- ;*******************************************************************************
/ V( f f# w" E: u0 J# w2 x - ; User Stack and Heap initialization+ v @# S- x, a* A6 N
- ;*******************************************************************************
, R; y! V9 _2 s2 N; Q$ q - //假如定义了MICROLIB,这里类似于if...else...- r- |, X( A7 E. J
- IF :DEF:__MICROLIB
1 D& Y9 v+ X4 w; @- O0 G" D
' p3 s% M( H$ I8 d- EXPORT __initial_sp4 l) m. P, A! j3 E" j0 u9 g
- EXPORT __heap_base
q* G- \5 L, C5 a - EXPORT __heap_limit# L6 |/ @! {& z$ ?' w" Z4 B
( Q9 K, d0 \: `1 g# s- q1 I1 p3 ]8 b- ELSE
4 G9 K. O7 |5 _9 @- R - & H( {9 t) b; G Z- M. X; |
- IMPORT __use_two_region_memory3 |/ Y& M' o- W4 T1 r
- EXPORT __user_initial_stackheap
) }- @* q) V& ]
, @: }( N0 }4 y2 P! j6 H) b; K- //__user_initial_stackheap 将由__main 函数进行调用。
) B* ^% ?" y7 p" u, k, R - __user_initial_stackheap0 t9 S7 W! i6 G; U
* g( d2 B4 G z# ]2 m- LDR R0, = Heap_Mem
' o. \7 C' S. @7 ~. m8 F - LDR R1, =(Stack_Mem + Stack_Size)
5 J# L& j' M& s' U! A9 {$ } - LDR R2, = (Heap_Mem + Heap_Size)8 t, ~7 ~* m& Z8 u8 X J
- LDR R3, = Stack_Mem3 h8 M9 U! F: n
- BX LR
5 o% y$ u P/ y - / m' G4 Y) W7 E- v7 `5 G3 ^
- ALIGN0 J! U d6 c- b/ @/ V: C
- & b' ^' Z( l& l5 p0 @9 L: ~
- ENDIF
. P1 t3 p+ A6 `6 e3 r) S4 j% ` - # _. E' s Q% C
- END
复制代码 Boot的启动模式不同于以往的M3、M4内核的ST MCU,H7的boot引脚只有一个,但是H7 专门配套了两个 option bytes 选项字节配置,如此以来就可以方便设置各种存储器地址了。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9J2Qpbcf-1574601496448)(./1574512528932.png)]
5 Q/ f: x$ x: m, mBOOT_ADD0 和 BOOT_ADD1 对应 32 位地址到高 16 位,这点要特别注意。 通过这两个选项字节,所有 0x0000 0000 到 0x3FFF 0000 的存储器地址都可以设置,包括:
. c; ^2 e* H% {4 x0 B, m) f: x6 \+ c
所有 Flash 地址空间。 T, y* O' t0 S& h
所有 RAM 地址空间,ITCM,DTCM 和 SRAM。6 P9 B& g) ~/ E Y" C5 i
设置了选项字节后,掉电不会丢失,下次上电或者复位后,会根据 BOOT 引脚状态从 BOOT_ADD0,或 BOOT_ADD1 所设置的地址进行启动。! l/ O6 p& q6 V g# K" p
使用 BOOT 功能,注意以下几个问题:
- d+ A! [# ]. i$ t1 Q% _ f: |如果用户不慎,设置的地址范围不在有效的存储器地址,那么 BOOT = 0 时,会从 Flash 首地址 0x08000000 启动,BOOT = 1 时,会从 ITCM 首地址 0x0000 0000 启动。3 c" A, y1 A( x2 f9 n& w0 u# }
当 Flash 的保护级别被配置为级别 2 之后, 只能从 Flash 自举。 如果 BOOT_ADD0/BOOT_ADD1选项字节中自举地址被配置为位于存储器范围之外或属于 RAM 地址范围,则系统只能从地址 0x0800 0000 上的 Flash 开始执行1 ]! x$ w7 o. ]2 J- Q. @
) @2 Y1 P' P+ M4 Y. x
0 T e! F5 V5 N( c$ W
0 M+ M3 v/ o4 L- E% d
|