STM32H7的启动过程分析) s" v- h" ]" E# U
启动文件* k0 j \( J: T3 h- ^. d' L' a
不同编译器对应的启动文件不同,在MDK下,以startup_stm32h743xx.s为例,这是一个汇编文件,启动文件中主要做了如下事情:' @0 z/ h! h( k! T! P0 |, p+ x! i
设置堆栈指针 SP = __initial_sp。- ~; M9 x) ~# K7 u
设置 PC 指针 = Reset_Handler。. `7 K8 V2 t; k/ Z7 d6 f
设置中断向量表。: ]) ?9 ~: h% F o# L- K6 R8 c' `
配置系统时钟。
0 M3 X, v1 w4 j4 h, p/ Y% S( p1 k配置外部 SRAM/SDRAM 用于程序变量等数据存储(这是可选的)。/ T# U0 m e9 y1 t+ z
跳转到 C 库中的 __main ,最终会调用用户程序的 main()函数。
6 J( `- N4 ^. ]& a* ?1 \Cortex-M 内核处理器复位后,处于线程模式,指令权限是特权级别(最高级别),堆栈设置为用主堆栈 MSP。6 e9 _" H; C9 h d, w X- X$ ^ |
堆栈指针$ m" y7 r6 l7 o; Z
通用寄存器组
. j/ R$ N+ Z6 p1 f8 PCortex – M7/M4/M3 处理器拥有 R0-R15 的通用寄存器组。其中 R13 作为堆栈指针 SP。 SP 有两个,但在同一时刻只能有一个可以用。: Q. I& I4 I! B1 j) S
主堆栈指针(MSP):这是缺省的堆栈指针,它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。
3 q0 n) m/ U9 V* h" x: U进程堆栈指针(PSP):用于常规的应用程序代码(不处于异常服务例程中时)。5 }$ k3 j: o/ b3 e/ u( j) A
另外以下两点要注意:% F X2 A, Z9 g. _; s
大多数情况下的应用,只需使用指针 MSP,而 PSP 多用于 RTOS 中。
; V& Z8 ?2 M3 y9 U7 A1 ?6 MR13 的最低两位被硬线连接到 0,并且总是读出 0,这意味着堆栈总是 4 字节对齐的。; F6 F/ x8 T/ _2 X
6 G% M$ {! y$ h- a+ D% R; }+ P
Cortex-M7/M4/M3 向下生长的满栈
; Y; v4 _: K+ i0 |7 u' d) fPUSH 入栈操作:SP 先自减 4,再存入新的数值; F( V, F5 P* y9 m6 K0 r8 Q( ~/ F
POP 出栈操作:先从 SP 指针处读出上一次被压入的值,再把 SP 指针自增 4) S3 i" E% X; ~- U9 q
2 d0 j1 U! s9 n) _分析启动过程
* l b, G/ {- \+ u- o$ x硬件上电后,会触发硬件复位,复位之后,CPU 内的时序逻辑电路首先完成如下两个工作(程序代码下载到内部 flash 为例,flash首地址 0x0800 0000)% o+ ~6 j0 D5 E" F
5 f2 b0 j. {/ F( Y
将 0x08000000 位置存放的堆栈栈顶地址存放到 SP 中(MSP)。
1 a9 T4 ], j5 x$ O+ @将 0x08000004 位置存放的向量地址装入 PC 程序计数器。
. Y, k# A) d j QCPU 从 PC 寄存器指向的物理地址取出第 1 条指令开始执行程序,也就是开始执行复位中断服务程序 Reset_Handler。为啥,因为在启动文件中,最先做的两件事情是
( x U% R5 E; i3 H1 q) E) i& f设置堆栈指针 SP = __initial_sp! n, b# M3 ^ S3 ]; l2 r4 S
设置 PC 指针 = Reset_Handler
& o1 A& V2 `4 s复位中断服务程序会调用SystemInit()函数来配置系统时钟、配置FMC总线上的外部SRAM/SDRAM,然后跳转到 C 库中__main 函数。由 C 库中的__main 函数完成用户程序的初始化工作(比如:变量赋初值等),最后由__main 函数调用用户写的 main()函数开始执行 C 程序。0 _% j$ ~( {$ J# L7 O% l
9 Y- e8 R. C" L8 p3 X; g7 T
代码分析
, {9 I/ ^ r1 H8 {3 m3 j8 d* H①、开辟栈(stack)空间,用于局部变量、函数调用、函数的参数等
! b4 c" Q$ ]- Q) b5 q! Y3 V4 r- d% j) a% q- s) l5 c1 R4 N2 m0 [
- //类似宏定义,这是个伪指令,定义栈大小,这里是以字节为单位
1 J1 e7 ^0 \7 f; o, O - Stack_Size EQU 0x00001000 6 ?: ?" I( \ ^5 o% Z
5 E9 v$ Y! u6 h# `. H0 Q/ \8 K% \- /*
( _1 p. A5 u7 _4 k' ?! N: C - 开辟一段数据空间可读可写,段名 STACK,按照 8 字节对齐。 ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。 ARER 后面的关键字表示这个段的属性。
6 e: b9 h' h8 W: u4 \# M - STACK :表示这个段的名字,可以任意命名。7 c3 s7 T7 e! v: b% `
- NOINIT:表示此数据段不需要填入初始数据。
) `3 I/ c. j2 L - READWRITE:表示此段可读可写。6 ?% ]2 r6 z& `+ \
- ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(地址对 8 求余数等于 0)。
/ a$ ^5 @5 J/ ?+ R$ [ - */ M4 ]( E" b9 Z% a# Z5 H+ z" N, x& a
- AREA STACK, NOINIT, READWRITE, ALIGN=39 G# e% i/ \; r' D9 V0 I
- + B- ?: L2 }' L# Q+ s' y2 Z+ t; |
- //SPACE 这行指令告诉汇编器给 STACK 段分配 0x00001000 字节的连续内存空间。7 f: d- z- W/ I2 B/ {% X- G
- Stack_Mem SPACE Stack_Size: d/ V& m6 U$ P, D/ Y( W& ^
) x; m. m O; _! o3 X" y4 {2 Y+ m- /*4 G) j$ U7 Q9 M ]# d
- __initial_sp 紧接着 SPACE 语句放置,表示了栈顶地址。 __initial_sp 只是一个标号,标号主要用于表示一片内存空间的某个位置,等价于 C 语言中的“地址”概念。地址仅仅表示存储空间的一个位置,从 C 语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。 f- |: v& D2 j# o( t% U. r0 C
- */& n$ c; W; t1 L6 ~8 B
- __initial_sp
复制代码
9 ^1 T5 a& a* {0 |0 L9 J9 x# m4 B①、开辟堆(heap)空间,主要用于动态内存分配,也就是说用 malloc,calloc, realloc 等函数
( ?$ t) K" g: _+ k s* @分配的变量空间是在堆上2 T. A5 a( r; B1 J1 a7 ^
' V/ f) ?8 ~( x9 r4 v- //定义堆空间大小
* l4 E- u! \& v - Heap_Size EQU 0x0000800/ n1 g& v! y% {8 x' E9 F6 C& X
- 9 p a1 C; H, g8 U3 X7 M
- //分配一段名字为HEAP、可读可写、不需要初值、8字节对齐的数据空间
" q. H# L# S% p& C( J% V/ k& n - AREA HEAP, NOINIT, READWRITE, ALIGN=3
, O- y$ J$ ?! \; E
& Z9 C+ T/ g% U- h+ P8 ]! A' G( D& R- //__heap_base 表示堆的开始地址。
8 ]' `5 K- t4 L3 ]$ | - __heap_base
3 [& q' H& [7 E- c Q) }/ N+ P - - C4 m/ Q+ `* B( Y6 S0 V# P
- //SPACE 这行指令告诉汇编器给 HEAP段分配 0x0000800字节的连续内存空间。* l% x2 l4 M f; ^+ H1 I
- Heap_Mem SPACE Heap_Size: M, W8 r, P3 m; D. R" U
6 I+ }' c# Q7 Q: ]& d+ F/ b ~- __heap_limit 表示堆的结束地址
9 s8 B3 V6 m( J4 l% g6 k0 u* z6 f# W - __heap_limit
复制代码
$ s; L4 ]1 r1 ^# M. g' ?③、生成属性设置、定义RESET代码段: s6 L" v* |* a7 U
, E; e" d( k; ^3 q4 K1 K- k" x- //PRESERVE8 指定当前文件保持堆栈八字节对齐。
; Q2 c% g. Q% K( E - PRESERVE8
8 v! e7 [2 B' \" D7 q - $ u, h* ]# D- ~0 I
- //THUMB 表示后面的指令是 THUMB 指令集 ,CM7 采用的是 THUMB - 2 指令集
7 s/ w* v/ y6 a4 R/ z0 P% b/ }7 @ - THUMB
1 |2 c. L( t1 L$ Y/ K
% B/ a; T4 F/ q1 E- //:AREA 定义一块代码段,只读,段名字是 RESET。 READONLY 表示只读,缺省就表示代码段了。
1 V7 Q+ X; W( C) |& u - ; Vector Table Mapped to Address 0 at Reset9 z7 B- j: T6 @0 n1 |# s3 Z" ^
- AREA RESET, DATA, READONLY3 K1 \1 W8 ]7 p0 a5 q
- - R# n% @8 z) n% o/ H
- //3 行 EXPORT 语句将 3 个标号申明为可被外部引用, 主要提供给链接器用于连接库文件或其他文件。
+ v7 R$ z3 x2 I3 O$ K - EXPORT __Vectors. s m* `4 N3 y) b5 {
- EXPORT __Vectors_End
, U8 [( A) ~+ _" Z9 X! f& w. e - EXPORT __Vectors_Size
复制代码 , w( L1 w" T5 V4 K% P8 W7 h
④、中断向量变定义/ f( f! o2 t1 K- g6 Q
b2 D @3 t* E- __Vectors DCD __initial_sp ; Top of Stack
- A5 j$ h# U, p: r) q( k2 ^/ b - DCD Reset_Handler ; Reset Handler
& x, R7 W# N. \0 U- n/ t2 o; h - DCD NMI_Handler ; NMI Handler8 U4 @0 x* l) K# k, f& a: ~
- DCD HardFault_Handler ; Hard Fault Handler; A0 Y2 V4 A1 q( G0 P' E
- /* 省略部分代码 */
# |* [/ @3 D- C: a: z - DCD 0 ; Reserved
9 [- F5 V0 H, }2 Y. I6 K1 E - DCD WAKEUP_PIN_IRQHandler ; Interrupt for all 6 wake-up pins
! d- ~8 e6 q) A- m6 s5 J - & x' {' z: X& t1 w5 u
* W) S* y' Z% K4 R1 f! T- __Vectors_End
5 w, N& _! R5 _1 o4 t - 7 i4 N) x. M0 j6 }) }$ P9 R
- //定义向量表大小, U2 x" R2 N8 f0 U8 ]1 ^
- __Vectors_Size EQU __Vectors_End - __Vectors
复制代码 ; Z+ H& N, W( R9 R. p) Z! Q M! F
上面的这段代码是建立中断向量表,中断向量表定位在代码段的最前面。具体的物理地址由链接器的配置参数(IROM1 的地址)决定。如果程序在 Flash 运行,则中断向量表的起始地址是0x08000000。以 MDK 为例,就是如下配置选项:
0 ~3 {" h0 w8 n. G) O2 r$ D3 r
5 h9 j4 b% l9 {( C. D7 f9 M. D
. C$ X3 _- Q; y8 O9 @* N
' y* d6 \' M% A+ U, @/ mDCD 表示分配 1 个 4 字节的空间。每行 DCD 都会生成一个 4 字节的二进制代码。中断向量表存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序& F& B J5 H8 Z. w3 D
! m V$ {: b7 a) T⑤、代码段定义、Reset_Handler过程处理" t0 \6 F3 u5 ~2 i0 ?9 Z) F1 _
% M$ O5 n r. @5 `4 d F5 j- AREA |.text|, CODE, READONLY2 b8 c% d& X& @
- 5 y2 P1 Q! R! v w
- ; Reset handler
; a; N! ?! R$ F - Reset_Handler PROC! m+ ?1 _/ j4 L. u; z# G: l
- EXPORT Reset_Handler [WEAK]
j; }4 P, S: F8 Z* ?" c; w8 m* C - IMPORT SystemInit* @0 R( V+ `+ l" y
- IMPORT __main* B. v) R" k4 o; O+ X" b9 F- s
) A, S! F* N/ H2 a. i3 h2 v2 r- LDR R0, =SystemInit y+ _. Z! D& ]" d) }2 C, Y
- BLX R0
. N# A6 z1 [+ \2 G$ K - LDR R0, =__main7 f% _( H; ?; b8 v
- BX R0
$ |1 y) M c5 U" D: x1 M: U6 q - ENDP
复制代码 ; n8 |0 L0 S9 W( K3 J. L9 O
AREA 定义一块代码段,只读,段名字是 .text 。 READONLY 表示只读 {3 n% h1 i6 c: E6 a/ J
利用 PROC、 ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。" {) F }9 a& w. l
WEAK 声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话会调用外面的。 这个声明很重要,它让我们可以在 C 文件中任意地方放置中断服务程序,只要保证 C 函数的名字和向量表中的名字一致即可。
U4 x( f$ A% q8 e' BIMPORT:伪指令用于通知编译器要使用的标号在其他的源文件中定义。但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。
. G) X& E( _0 U, U8 oSystemInit 函数在文件 system_stm32h7xx.c 里面,主要实现 RCC 相关寄存器复位和中断向量表位置设置。
" n/ W D/ k- k__main 标号表示 C/C++标准实时库函数里的一个初始化子程序__main 的入口地址。该程序的一个主要作用是初始化堆栈(跳转__user_initial_stackheap 标号进行初始化堆栈的,下面会讲到这个标号),并初始化映像文件,最后跳转到 C 程序中的 main 函数。这就解释了为何所有的 C 程序必须有一个 main 函数作为程序的起点。因为这是由 C/C++标准实时库所规,并且不能更改。( E; r+ R2 t/ }7 I e5 E
' a6 Q+ g0 h# c" [6 D$ J⑥、缺省中断服务函数定义- ; Dummy Exception Handlers (infinite loops which can be modified)
2 B ~$ C" P/ N B5 e2 ~ - 1 s' [3 e) g- s: Y4 u4 M" e' O
- NMI_Handler PROC7 L1 O8 P4 c d& A0 p3 d4 }
- EXPORT NMI_Handler [WEAK]
0 d0 ~1 n, A i% |, m" R5 I9 o4 c; f - B . //死循环
1 p0 X+ r) z( K/ X8 ? - ENDP `5 d0 P6 j, L0 A- v1 p% r! u1 k
- ...省略
, N% }" l+ S2 l% g4 P" W - EXPORT PendSV_Handler [WEAK]- b h" C y& n& v5 o. E9 z
- B .
- F7 d: `' a$ Q& e# a: o% | - ENDP( x! s$ X6 s& A# x( O. v0 v
- SysTick_Handler PROC9 v' Z u5 H, r6 C k
- EXPORT SysTick_Handler [WEAK]
: h/ G0 G- K; o* s } - B .: h' Q; ]. `4 D% f5 |3 s8 E
- ENDP
: j# B6 k) |. y: ~" Z/ r
) ?3 j! g8 k* |% N9 i- Default_Handler PROC
* @" _+ [4 ~# _+ w - 5 e; e2 W8 D D7 y9 g
- EXPORT WWDG_IRQHandler [WEAK] 4 _# o9 t# y7 a( n3 u, B! C
- EXPORT PVD_AVD_IRQHandler [WEAK]
6 o; i w, [0 D5 e; w6 @# C8 [ - ...省略 , T$ E m8 ^2 E- Q
- EXPORT WAKEUP_PIN_IRQHandler [WEAK]
; Y+ t7 n8 r0 M - 3 F V; F4 ]/ S3 g9 a V
: t3 _; y5 a! b1 D i- WWDG_IRQHandler
" l' b& e/ S, W- T - ...省略 j- w/ g) R. ^' ]1 {4 M
- WAKEUP_PIN_IRQHandler. q' e ^) j3 A7 L; g6 f/ f
8 Z5 i' U! I: b' V) e1 v- B .# W) l z! s& R) V$ J0 [
5 n Z6 g7 y) R- ENDP( _2 N2 K* u. H# |' L
- ' @: }4 I0 E# F& E
- ALIGN
复制代码 ' x- |0 K- [8 s# [# ~3 v/ S
这里全部的中断服务函数都是用[WEAK]来声明,假如没有在其他文件中定义同名的中断服务函数,来了中断,就会进入到这里。; g$ X9 f4 I7 L; ~5 ~
. D' ?' W( }- f7 D0 X v0 H⑦、堆和栈的初始化
+ H3 F4 C1 _, y5 @" h5 O
; g( v( T c2 F4 b: ]+ L3 q- ;*******************************************************************************
$ J& o: a* @0 X% t - ; User Stack and Heap initialization" D" ]- b- g4 `: [2 B) j' S# K
- ;*******************************************************************************- M5 h& q( p4 s0 j
- //假如定义了MICROLIB,这里类似于if...else...: l. }+ g8 ^' x2 f, m$ a5 j/ t
- IF :DEF:__MICROLIB
l2 ?6 Z- l5 S& I4 k' i
+ }& ^6 G) E4 l5 h c S- EXPORT __initial_sp
4 M3 }) z A) B9 \' M - EXPORT __heap_base
, @& ^9 z( K( G" M: | - EXPORT __heap_limit' n) a7 C* x/ o. o) J, w
8 a0 P0 W' v$ l W1 r( h: a0 {- ELSE
% t: J! W% q7 d% M$ M" q) ?
G4 x, X+ E3 M. {- IMPORT __use_two_region_memory: H- y( x6 o- [" T$ k
- EXPORT __user_initial_stackheap% A9 H4 m4 Y3 f) J% J: T5 w
- 1 k. _1 ?$ K' a; b* Y7 p, s; n
- //__user_initial_stackheap 将由__main 函数进行调用。
% R& J7 l9 y8 f4 d8 s - __user_initial_stackheap* \. Y6 Z6 p ^
- 9 \- Q, _2 Q. d$ k) R5 \8 x
- LDR R0, = Heap_Mem
! o3 R6 _& I: w: t' J: n2 J2 _ @ - LDR R1, =(Stack_Mem + Stack_Size), e$ Y2 W1 g" P9 L/ I
- LDR R2, = (Heap_Mem + Heap_Size)/ G7 {! _9 M+ Y7 E
- LDR R3, = Stack_Mem0 N* h7 i8 H d
- BX LR/ V% Q5 W( V1 a# A
- ; O% }9 @6 |4 A4 y# a+ B1 e
- ALIGN
# _& u' s/ [3 _' [% O" S
. A; I, S) E. w# P5 A* `- ENDIF q2 Z6 j9 n. |1 b
- 7 p9 k! I: B, W
- END
复制代码 Boot的启动模式不同于以往的M3、M4内核的ST MCU,H7的boot引脚只有一个,但是H7 专门配套了两个 option bytes 选项字节配置,如此以来就可以方便设置各种存储器地址了。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9J2Qpbcf-1574601496448)(./1574512528932.png)]% f1 e2 D+ f, D# J
BOOT_ADD0 和 BOOT_ADD1 对应 32 位地址到高 16 位,这点要特别注意。 通过这两个选项字节,所有 0x0000 0000 到 0x3FFF 0000 的存储器地址都可以设置,包括:$ X7 r1 @) V+ G* A4 ?
2 W) A, R* f5 F4 j L5 q所有 Flash 地址空间。
! Z( B; h& e. ]2 A1 s: D0 t所有 RAM 地址空间,ITCM,DTCM 和 SRAM。" ^# e" E2 @( G8 l
设置了选项字节后,掉电不会丢失,下次上电或者复位后,会根据 BOOT 引脚状态从 BOOT_ADD0,或 BOOT_ADD1 所设置的地址进行启动。3 P- C/ A0 C' V3 J; U: T% O o
使用 BOOT 功能,注意以下几个问题:: D9 H$ _# w) g2 @1 ?+ A- G
如果用户不慎,设置的地址范围不在有效的存储器地址,那么 BOOT = 0 时,会从 Flash 首地址 0x08000000 启动,BOOT = 1 时,会从 ITCM 首地址 0x0000 0000 启动。
1 o A Y" U% i; M1 L* ~当 Flash 的保护级别被配置为级别 2 之后, 只能从 Flash 自举。 如果 BOOT_ADD0/BOOT_ADD1选项字节中自举地址被配置为位于存储器范围之外或属于 RAM 地址范围,则系统只能从地址 0x0800 0000 上的 Flash 开始执行
( M9 t& f' d+ U' j
% `9 v! d+ b3 z* W; j/ O. O( G* B" b8 C9 Z0 L' c
: l6 r6 P1 `" S) q' }, l5 v3 f" | |