STM32H7的启动过程分析: q# T0 b. y) T1 v
启动文件
6 I0 P) }) ^8 N不同编译器对应的启动文件不同,在MDK下,以startup_stm32h743xx.s为例,这是一个汇编文件,启动文件中主要做了如下事情:1 F3 ]2 C+ \/ Z
设置堆栈指针 SP = __initial_sp。/ Y% y9 h- Y! F/ s! O2 \
设置 PC 指针 = Reset_Handler。
1 D- p s7 Y6 @- y设置中断向量表。
* u+ W3 M8 w, M& G) Y配置系统时钟。
% D/ N( N+ e8 P ]3 `" I+ O, s7 v6 c配置外部 SRAM/SDRAM 用于程序变量等数据存储(这是可选的)。
2 D$ p9 R! X5 @跳转到 C 库中的 __main ,最终会调用用户程序的 main()函数。
: Q% V# h; s5 Z8 t6 L* L- X1 uCortex-M 内核处理器复位后,处于线程模式,指令权限是特权级别(最高级别),堆栈设置为用主堆栈 MSP。% h# [, x0 w( T, @5 k
堆栈指针
9 L, H6 ^8 F$ ~6 x* G通用寄存器组* I2 T; g3 t7 A, }) m. t% @
Cortex – M7/M4/M3 处理器拥有 R0-R15 的通用寄存器组。其中 R13 作为堆栈指针 SP。 SP 有两个,但在同一时刻只能有一个可以用。* }# s) ~8 Z7 I% A
主堆栈指针(MSP):这是缺省的堆栈指针,它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。
0 \ e3 n6 `7 f进程堆栈指针(PSP):用于常规的应用程序代码(不处于异常服务例程中时)。' f6 s5 a! ^) K( K
另外以下两点要注意:5 x/ L- V' y4 K% a) B3 W
大多数情况下的应用,只需使用指针 MSP,而 PSP 多用于 RTOS 中。
9 F" x$ j, X# z! x& v+ B4 {$ l+ yR13 的最低两位被硬线连接到 0,并且总是读出 0,这意味着堆栈总是 4 字节对齐的。
q4 h. h. C. s) m6 c3 [' a. |& K6 s
Cortex-M7/M4/M3 向下生长的满栈0 S: ^- ~% Y* i6 R3 e
PUSH 入栈操作:SP 先自减 4,再存入新的数值
S0 ?" h# o8 u! d. LPOP 出栈操作:先从 SP 指针处读出上一次被压入的值,再把 SP 指针自增 4
, \' {3 V2 i8 D% B: J2 S
4 t0 [4 J7 G& Y+ V. W分析启动过程
c9 _7 t3 M+ R/ @硬件上电后,会触发硬件复位,复位之后,CPU 内的时序逻辑电路首先完成如下两个工作(程序代码下载到内部 flash 为例,flash首地址 0x0800 0000)
6 ]7 q* G; y0 s/ i1 Y. h
5 S1 u5 k" M0 A) T将 0x08000000 位置存放的堆栈栈顶地址存放到 SP 中(MSP)。+ w. v8 W; O" U$ U4 Y
将 0x08000004 位置存放的向量地址装入 PC 程序计数器。
3 B( x( h0 u& ]# B( oCPU 从 PC 寄存器指向的物理地址取出第 1 条指令开始执行程序,也就是开始执行复位中断服务程序 Reset_Handler。为啥,因为在启动文件中,最先做的两件事情是; Q0 B: I( s1 c8 b
设置堆栈指针 SP = __initial_sp& E" z& J+ M# P2 Q1 Z1 P* k$ n
设置 PC 指针 = Reset_Handler, s9 K* K1 b# C& x% [
复位中断服务程序会调用SystemInit()函数来配置系统时钟、配置FMC总线上的外部SRAM/SDRAM,然后跳转到 C 库中__main 函数。由 C 库中的__main 函数完成用户程序的初始化工作(比如:变量赋初值等),最后由__main 函数调用用户写的 main()函数开始执行 C 程序。) f9 V9 U9 ~6 R0 B6 p
. j" s' i6 y: Q6 F8 j代码分析
6 n( a' K1 a2 u, z Y& N. k6 h①、开辟栈(stack)空间,用于局部变量、函数调用、函数的参数等" J1 ^, j& N1 ~2 K% ?1 e1 Q! X
* k2 ^& g" D) ^) d
- //类似宏定义,这是个伪指令,定义栈大小,这里是以字节为单位- w {# ?4 I8 t; l+ R
- Stack_Size EQU 0x00001000 8 v, p; K/ {8 c
- ( y, g. X2 z r4 U* w( C! G- q
- /*0 z! x0 c0 a. R, Q
- 开辟一段数据空间可读可写,段名 STACK,按照 8 字节对齐。 ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。 ARER 后面的关键字表示这个段的属性。9 r" q# g" P1 _
- STACK :表示这个段的名字,可以任意命名。
& l B$ k! z: g8 _" i - NOINIT:表示此数据段不需要填入初始数据。! z+ M9 r7 o% q3 O# X7 X& i
- READWRITE:表示此段可读可写。 _3 S2 M2 d0 _ c m' n, B
- ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(地址对 8 求余数等于 0)。
8 `$ q# h( d! v0 Y: p - */0 B+ P/ o' w0 l+ A+ ~! G
- AREA STACK, NOINIT, READWRITE, ALIGN=3
( Y$ r: l( o, [) A* n5 U - . _& q8 S* V, u# y( b4 U
- //SPACE 这行指令告诉汇编器给 STACK 段分配 0x00001000 字节的连续内存空间。3 o! e! S. Q+ Q3 y$ n1 J- G
- Stack_Mem SPACE Stack_Size% D# j; U" [# w5 ?! r
' ?- g0 |, J4 `# j" ?* s% c; R3 ~- /*
2 r# M7 H, a; I" i6 a L - __initial_sp 紧接着 SPACE 语句放置,表示了栈顶地址。 __initial_sp 只是一个标号,标号主要用于表示一片内存空间的某个位置,等价于 C 语言中的“地址”概念。地址仅仅表示存储空间的一个位置,从 C 语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。7 m1 u9 D+ J. T
- */
. S) _# o8 ?! z7 P; c: b - __initial_sp
复制代码 ) m: K' [: G* [$ ^
①、开辟堆(heap)空间,主要用于动态内存分配,也就是说用 malloc,calloc, realloc 等函数
1 U4 G& U9 h8 e5 J分配的变量空间是在堆上
. [: L( a* a7 L4 V/ C" k5 e3 u' [7 L' w, |8 m! b7 Y
- //定义堆空间大小& t; v; ?' S9 j( j- r
- Heap_Size EQU 0x0000800
5 a8 h$ q' Q+ d ]" [0 G - ( o8 O! u' v ^. D
- //分配一段名字为HEAP、可读可写、不需要初值、8字节对齐的数据空间
, y; d' ]6 B- e# h4 ~ - AREA HEAP, NOINIT, READWRITE, ALIGN=3* l+ n1 Y6 {! }0 V; K
) B" W# E+ V- G, X. p: X) I! j- //__heap_base 表示堆的开始地址。
6 T. O, \5 e& A6 C" s3 C _ - __heap_base
, D9 v$ [2 X; q6 Q8 ]9 H1 @ - # C- G. j% K. ^: [& {
- //SPACE 这行指令告诉汇编器给 HEAP段分配 0x0000800字节的连续内存空间。9 d. w2 U' {7 }' V$ [: K
- Heap_Mem SPACE Heap_Size
$ m) `7 O/ j* c3 V - : S9 \* C3 | f6 [5 M* s
- __heap_limit 表示堆的结束地址/ U; f$ a9 p1 W+ m/ W# c2 w; K
- __heap_limit
复制代码 : P e" m! g+ ~/ e v+ |
③、生成属性设置、定义RESET代码段! p+ q3 n! p& V1 x. C: q3 C2 l. i
8 C- E/ v- A* _
- //PRESERVE8 指定当前文件保持堆栈八字节对齐。7 A9 e* X! R8 G5 _
- PRESERVE8
) O$ ~5 y8 R5 [0 ]7 u - 7 N' o$ p' v+ k; U( j. k
- //THUMB 表示后面的指令是 THUMB 指令集 ,CM7 采用的是 THUMB - 2 指令集9 }) n. O( |1 k; i: _9 I
- THUMB& P2 g9 `! l4 Z! |' u+ v
- 1 D$ v. s4 X& t, y
- //:AREA 定义一块代码段,只读,段名字是 RESET。 READONLY 表示只读,缺省就表示代码段了。/ _+ w" e. @$ r6 ]
- ; Vector Table Mapped to Address 0 at Reset
, f) }+ ^# ?+ t - AREA RESET, DATA, READONLY
5 r3 v) h/ s- u$ n! h - ( a$ j, J7 U4 L2 ?9 W" s; g$ t5 F: N
- //3 行 EXPORT 语句将 3 个标号申明为可被外部引用, 主要提供给链接器用于连接库文件或其他文件。/ U4 a) K% s l
- EXPORT __Vectors
! P2 P$ C& s4 Y5 v+ ^* f" M' [ - EXPORT __Vectors_End' {2 H) M; P9 E9 K/ }7 [# r+ ^
- EXPORT __Vectors_Size
复制代码 8 B; ?- r- j. {2 U, X
④、中断向量变定义0 O3 ?7 x( I/ y' u# Q
/ v& a# O: y# n* N" h2 |& M5 ~, {+ L
- __Vectors DCD __initial_sp ; Top of Stack& [% q5 o" G8 L! _. Z7 f3 U: B
- DCD Reset_Handler ; Reset Handler) l; H( t" `* Q7 Z; N* [- s6 V7 A# P
- DCD NMI_Handler ; NMI Handler
, p& E3 f6 g: X2 d, ]/ o/ p0 ~$ ] - DCD HardFault_Handler ; Hard Fault Handler$ z6 D3 \6 {' q4 h9 l
- /* 省略部分代码 */ 5 w$ Z9 i6 J7 \3 d
- DCD 0 ; Reserved
1 @+ _+ ~% O& I$ _; p2 f - DCD WAKEUP_PIN_IRQHandler ; Interrupt for all 6 wake-up pins , H. u; C" W/ ^6 O K$ p
' x! {6 j2 x: u1 Q, q5 R( V# v
# k1 | q7 k0 w0 _9 ]- __Vectors_End8 N* ?3 K4 L1 G% T( a0 G# Y2 U
" Q6 e3 A7 h e; M* Y# J4 G- //定义向量表大小; K9 ], I7 o; y- Z5 L, [
- __Vectors_Size EQU __Vectors_End - __Vectors
复制代码
9 m- o: w0 ^2 r- z& \2 A上面的这段代码是建立中断向量表,中断向量表定位在代码段的最前面。具体的物理地址由链接器的配置参数(IROM1 的地址)决定。如果程序在 Flash 运行,则中断向量表的起始地址是0x08000000。以 MDK 为例,就是如下配置选项:
7 H7 a- ^$ ~7 W% k- b% h, {3 q( O
0 @4 F& X+ d) ~; ^. z: Q
/ x/ [* D$ e0 I4 t' oDCD 表示分配 1 个 4 字节的空间。每行 DCD 都会生成一个 4 字节的二进制代码。中断向量表存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序% b9 G1 K2 s" C0 s% S2 k
* k0 B3 v# i: d! }: k, j0 z⑤、代码段定义、Reset_Handler过程处理# n& x# p, H6 u5 R4 z$ C0 r8 s
( R& x6 @, z2 i' h
- AREA |.text|, CODE, READONLY" } l- h9 V' U6 q
7 |: {2 H' D' Q- ; Reset handler
# \$ |$ U" Q* |. {* M - Reset_Handler PROC
( n F6 q3 }7 L8 l& U& H, o - EXPORT Reset_Handler [WEAK]
* s+ y& l; u. n - IMPORT SystemInit
9 H+ f" O! |+ q5 U2 w$ s - IMPORT __main
; K6 c1 T/ x: p L# C2 z. W - / q* d* T1 Y9 c$ @- V0 b% N
- LDR R0, =SystemInit
, j; P- `( R6 Q. Y0 H$ _8 V; p* u - BLX R0! V( m2 m! O" }4 m ?
- LDR R0, =__main; _2 r/ W; |3 K. c% q
- BX R0
& J: {! z$ @' Q; x& p - ENDP
复制代码 j# p+ B0 z% V& { ~1 k$ h. n
AREA 定义一块代码段,只读,段名字是 .text 。 READONLY 表示只读
5 j8 S$ F+ K8 Y: _8 V, F% o1 ?利用 PROC、 ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
* l0 c8 H$ g" U/ b4 }( i5 rWEAK 声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话会调用外面的。 这个声明很重要,它让我们可以在 C 文件中任意地方放置中断服务程序,只要保证 C 函数的名字和向量表中的名字一致即可。
) P: W% K0 a' Z5 K6 S' D/ G8 M6 ]IMPORT:伪指令用于通知编译器要使用的标号在其他的源文件中定义。但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。5 n$ F A @' k) d, H
SystemInit 函数在文件 system_stm32h7xx.c 里面,主要实现 RCC 相关寄存器复位和中断向量表位置设置。) r3 {. [ e9 @* K+ s3 `
__main 标号表示 C/C++标准实时库函数里的一个初始化子程序__main 的入口地址。该程序的一个主要作用是初始化堆栈(跳转__user_initial_stackheap 标号进行初始化堆栈的,下面会讲到这个标号),并初始化映像文件,最后跳转到 C 程序中的 main 函数。这就解释了为何所有的 C 程序必须有一个 main 函数作为程序的起点。因为这是由 C/C++标准实时库所规,并且不能更改。% }$ K6 D8 T9 d
. P1 y$ _2 n5 p, L$ J+ k# S
⑥、缺省中断服务函数定义- ; Dummy Exception Handlers (infinite loops which can be modified)
. y- x" e E) n! C) Z - . }9 n- ]/ s" C5 q8 r7 ~
- NMI_Handler PROC* V" V4 d5 G7 E
- EXPORT NMI_Handler [WEAK]$ { z @- f- k# c2 D
- B . //死循环- @, p- U& E& ]* t' z$ `+ R% ~$ o2 m
- ENDP6 N1 ~! q# p& b
- ...省略) {: f) w! i3 u2 |2 G
- EXPORT PendSV_Handler [WEAK], b# w! q. z3 ~& [5 Q
- B .1 J. w- G9 G, i
- ENDP
& z3 p) ] p7 t$ l b3 _6 Z - SysTick_Handler PROC
% D) y5 N! y0 e2 r+ L, i - EXPORT SysTick_Handler [WEAK]. d' m0 q) y+ Q6 X$ n; L9 }( j
- B .) h% M9 r' X9 K
- ENDP
# S9 W" q# M, ]4 n* L
+ n# {4 T1 D0 n( _8 d& x' D8 U, [- Default_Handler PROC
( u4 `1 p6 B" o
1 E, c }; i0 F: \- EXPORT WWDG_IRQHandler [WEAK] 4 a; `! f. B1 c- Y
- EXPORT PVD_AVD_IRQHandler [WEAK]
8 _$ R+ e6 l1 p0 H; {& q/ Y - ...省略 * ]3 X4 |2 n# \' m: f0 y, N
- EXPORT WAKEUP_PIN_IRQHandler [WEAK]
& ]+ |$ @* j; J6 r h% c - * u5 S& V" _& Y7 B; a' H( G
) D& q- G9 R9 Q& K5 \- WWDG_IRQHandler
5 c! b9 F0 _9 W0 b H% R& P - ...省略
, r& c. R2 N5 s. z - WAKEUP_PIN_IRQHandler
J: b, M* W) Z7 l - 7 ~) Y4 w6 }4 S' j" K/ k
- B .
- D/ Y7 Z% M" q5 D: O2 f0 J( S5 B - 7 h2 j, u! A9 e- O8 c( ~
- ENDP
) p9 l) ]. K: |: M; M& _ - " c+ ?8 X! h7 }; f/ u
- ALIGN
复制代码 & s: \5 Y# ?5 n. L7 g
这里全部的中断服务函数都是用[WEAK]来声明,假如没有在其他文件中定义同名的中断服务函数,来了中断,就会进入到这里。
; S5 Z* }& |' `7 E
6 B; A4 L, ]1 b⑦、堆和栈的初始化& V4 L/ p0 R; K* u' G# E4 d; C7 m
' H8 \+ P p# }' X- f& e D
- ;*******************************************************************************
& U2 E# x8 q7 f8 Z L5 D& r - ; User Stack and Heap initialization8 b# C. T* B' X! S. `- o ~
- ;*******************************************************************************
3 t3 S+ o" X4 h6 E - //假如定义了MICROLIB,这里类似于if...else...# P0 W0 ]. `) X+ C
- IF :DEF:__MICROLIB5 E E+ i/ R5 ?
, v# N% _, G* I! i' u- EXPORT __initial_sp
+ a+ _$ D( k- q - EXPORT __heap_base
( i5 C) W4 v9 c& b* V9 C - EXPORT __heap_limit
N. ?/ F R: d* |" d1 L- s& `2 g
. T+ v4 I7 ~; |1 d7 X3 l7 `- ELSE
6 Q a) h, h4 a( Q0 D
- _5 e1 f( L; B7 B- IMPORT __use_two_region_memory. L1 o- o( m: U7 k
- EXPORT __user_initial_stackheap
* A7 E& e# O+ Q) V* O# F5 L
( m+ x/ }) [4 M; K3 z- //__user_initial_stackheap 将由__main 函数进行调用。
% z7 x( l* L" I1 e$ g - __user_initial_stackheap8 O* Z0 P, |* B. `+ w$ ~
5 ~9 F% D; D7 l7 p: P \- LDR R0, = Heap_Mem* O+ z. n& P; o$ @
- LDR R1, =(Stack_Mem + Stack_Size)
7 t2 M" l8 B+ D# Y1 ?& h - LDR R2, = (Heap_Mem + Heap_Size)4 W0 ]8 g5 M3 F/ T3 z0 K' c: M
- LDR R3, = Stack_Mem% W, g7 \) C7 P: [3 b
- BX LR. T7 R' S4 H2 u3 F+ c+ K+ j
6 G, u" e S9 \* Q0 b% L- ALIGN5 J9 K$ f1 e' I$ N u( Q( u7 @
0 e* a8 h t7 K' e- ENDIF; x D) u% b3 J7 c5 |" N( b
- ( C; M- w$ q3 C( [% N
- END
复制代码 Boot的启动模式不同于以往的M3、M4内核的ST MCU,H7的boot引脚只有一个,但是H7 专门配套了两个 option bytes 选项字节配置,如此以来就可以方便设置各种存储器地址了。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9J2Qpbcf-1574601496448)(./1574512528932.png)]4 [2 h+ M# i7 X | Q+ u
BOOT_ADD0 和 BOOT_ADD1 对应 32 位地址到高 16 位,这点要特别注意。 通过这两个选项字节,所有 0x0000 0000 到 0x3FFF 0000 的存储器地址都可以设置,包括:
* p K; m2 `- e7 ?4 d. r# X) n, r7 B5 {: k# c# V
所有 Flash 地址空间。) }' Z: q; W. v4 o# ^
所有 RAM 地址空间,ITCM,DTCM 和 SRAM。
1 ]5 d4 c2 M W2 `2 N设置了选项字节后,掉电不会丢失,下次上电或者复位后,会根据 BOOT 引脚状态从 BOOT_ADD0,或 BOOT_ADD1 所设置的地址进行启动。8 p. j' U m) }
使用 BOOT 功能,注意以下几个问题:1 Q0 X) o& l# U* C: B( f
如果用户不慎,设置的地址范围不在有效的存储器地址,那么 BOOT = 0 时,会从 Flash 首地址 0x08000000 启动,BOOT = 1 时,会从 ITCM 首地址 0x0000 0000 启动。7 V0 e. Z( ]0 h6 C
当 Flash 的保护级别被配置为级别 2 之后, 只能从 Flash 自举。 如果 BOOT_ADD0/BOOT_ADD1选项字节中自举地址被配置为位于存储器范围之外或属于 RAM 地址范围,则系统只能从地址 0x0800 0000 上的 Flash 开始执行/ x( { \) L- R; J( s8 ^' j1 G
! d) j1 i, @+ E6 {! @4 B* m
8 K+ u9 v# ^7 M9 X, b) K2 S; \1 l. f1 p8 I" Z6 E% t; H; \
|