STM32H7的启动过程分析' i/ ?: u' Q. }- c" B: T' s9 w
启动文件* Q6 T6 V. v! l5 K
不同编译器对应的启动文件不同,在MDK下,以startup_stm32h743xx.s为例,这是一个汇编文件,启动文件中主要做了如下事情:8 J- o3 Q" H8 O8 m4 m# W1 y
设置堆栈指针 SP = __initial_sp。 S8 Z" J8 Q! C) _! G
设置 PC 指针 = Reset_Handler。. k w0 X$ g5 @1 \& Q) J
设置中断向量表。: k4 n) M9 v4 a, j g& H/ t8 P
配置系统时钟。
8 [! ?$ H* G% m/ L7 L! O配置外部 SRAM/SDRAM 用于程序变量等数据存储(这是可选的)。5 W* r4 e8 Q8 A# V
跳转到 C 库中的 __main ,最终会调用用户程序的 main()函数。 l2 @9 @# r. Q2 M4 |* S/ o0 W
Cortex-M 内核处理器复位后,处于线程模式,指令权限是特权级别(最高级别),堆栈设置为用主堆栈 MSP。* t( {, q& W; q0 z& Q
堆栈指针$ c( S) N% C2 y
通用寄存器组
( c, L- T% K6 G. Q* v6 sCortex – M7/M4/M3 处理器拥有 R0-R15 的通用寄存器组。其中 R13 作为堆栈指针 SP。 SP 有两个,但在同一时刻只能有一个可以用。
7 B# [' J/ k9 w9 J/ }主堆栈指针(MSP):这是缺省的堆栈指针,它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。- o* B* u$ n5 u" y/ h' P
进程堆栈指针(PSP):用于常规的应用程序代码(不处于异常服务例程中时)。$ C6 {4 D; g; B7 q2 l
另外以下两点要注意:
5 W! @( @. _$ `0 ]/ W大多数情况下的应用,只需使用指针 MSP,而 PSP 多用于 RTOS 中。+ v2 w# }" }5 s- e- O1 R0 M' T
R13 的最低两位被硬线连接到 0,并且总是读出 0,这意味着堆栈总是 4 字节对齐的。! y; c B( |; b
- Y% S5 {" z2 s6 y& I3 S
Cortex-M7/M4/M3 向下生长的满栈" X+ {' d9 F2 c! r1 {
PUSH 入栈操作:SP 先自减 4,再存入新的数值. Z1 ]; n* g" d) f
POP 出栈操作:先从 SP 指针处读出上一次被压入的值,再把 SP 指针自增 4
G9 Y) }4 ^2 q/ {8 n0 u* J8 i+ S5 F4 w* U, t: P! U2 n
分析启动过程+ Y- c% U0 r: \& d5 Z* Z6 I+ P
硬件上电后,会触发硬件复位,复位之后,CPU 内的时序逻辑电路首先完成如下两个工作(程序代码下载到内部 flash 为例,flash首地址 0x0800 0000): G8 |# Z, Y s1 H1 G+ f( B3 S% }
: a, O2 X6 R- V- |将 0x08000000 位置存放的堆栈栈顶地址存放到 SP 中(MSP)。7 @3 ~( V' d1 I* [! u( j$ w
将 0x08000004 位置存放的向量地址装入 PC 程序计数器。
# z) \" `' j7 Q$ N" G- HCPU 从 PC 寄存器指向的物理地址取出第 1 条指令开始执行程序,也就是开始执行复位中断服务程序 Reset_Handler。为啥,因为在启动文件中,最先做的两件事情是3 @ m' _6 M1 g2 r/ o
设置堆栈指针 SP = __initial_sp5 c! U& ]8 d2 l0 R. G
设置 PC 指针 = Reset_Handler# L+ r" j7 w$ D/ N7 z
复位中断服务程序会调用SystemInit()函数来配置系统时钟、配置FMC总线上的外部SRAM/SDRAM,然后跳转到 C 库中__main 函数。由 C 库中的__main 函数完成用户程序的初始化工作(比如:变量赋初值等),最后由__main 函数调用用户写的 main()函数开始执行 C 程序。
& N' w6 T) g$ I5 e' m1 \; s3 n# R! O5 U+ X% a
代码分析
; |& Y; ^# ]: Q: T0 i! H G①、开辟栈(stack)空间,用于局部变量、函数调用、函数的参数等
. K% H% G" H% q: w, i1 W" C( U4 n8 v8 R }
- //类似宏定义,这是个伪指令,定义栈大小,这里是以字节为单位
. }# V1 @, U0 S8 j6 e$ ]" i: F - Stack_Size EQU 0x00001000 , v9 W( B9 b' j
: D% f9 q6 K2 H) c- /*
( } E3 h! C' o* p* {) C5 t - 开辟一段数据空间可读可写,段名 STACK,按照 8 字节对齐。 ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。 ARER 后面的关键字表示这个段的属性。
) Z, n. @4 ]' h* \2 |( _) z - STACK :表示这个段的名字,可以任意命名。# J6 P" b4 r& z9 P, I: L7 ^
- NOINIT:表示此数据段不需要填入初始数据。' ~. e$ L+ u2 _! `% a9 b$ T
- READWRITE:表示此段可读可写。" G. M ~8 ~/ P: {5 N* P7 ^" f8 X
- ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(地址对 8 求余数等于 0)。
# ]- W. e+ w" P$ n2 @ - */
% M1 @. j. H8 e2 U$ k; P% g- P6 a - AREA STACK, NOINIT, READWRITE, ALIGN=3" H9 o+ l2 W+ E7 c+ H) `) Z0 l
3 T% d j- t+ |1 A( ?! m- //SPACE 这行指令告诉汇编器给 STACK 段分配 0x00001000 字节的连续内存空间。
' K0 o7 Z* v6 `. V8 _/ @ - Stack_Mem SPACE Stack_Size
; N1 m% D/ |* D9 }" ?+ F# b
# T9 J; N# k4 F( o- /*5 v; M: N# }. s
- __initial_sp 紧接着 SPACE 语句放置,表示了栈顶地址。 __initial_sp 只是一个标号,标号主要用于表示一片内存空间的某个位置,等价于 C 语言中的“地址”概念。地址仅仅表示存储空间的一个位置,从 C 语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。
! `9 J* H) E( S- l6 Q; Q% f0 b# z* Q- \ - */9 b9 h+ W% g" n/ S5 d- J0 z. J
- __initial_sp
复制代码 0 x, e% x9 d. |; h w0 W" m
①、开辟堆(heap)空间,主要用于动态内存分配,也就是说用 malloc,calloc, realloc 等函数
0 B: k+ W7 g/ h3 W& ?" m分配的变量空间是在堆上
+ n5 N v3 f( W1 G" F6 t: I6 L' F2 Q) E$ ^6 F( V7 l
- //定义堆空间大小: y2 G' S' H* l4 U3 S) ^- r
- Heap_Size EQU 0x0000800
8 V8 e( s2 x: X |7 o
7 k$ a5 i( l k$ A8 {9 R- //分配一段名字为HEAP、可读可写、不需要初值、8字节对齐的数据空间# q- r& d, Z+ O. K' h* X6 Y% `
- AREA HEAP, NOINIT, READWRITE, ALIGN=33 i a2 P( _/ ^8 e g! ^* r- Y
- ' P9 @ u- K# C( G/ Q+ C
- //__heap_base 表示堆的开始地址。
5 R7 G5 m5 g, d4 j - __heap_base1 p% ?( u1 P: T" w+ A2 N
+ X+ w i* U e- //SPACE 这行指令告诉汇编器给 HEAP段分配 0x0000800字节的连续内存空间。" M- m0 X: b# q& M# Z2 ^) H3 V
- Heap_Mem SPACE Heap_Size
) Y$ } g3 J8 w9 Z# _ - 6 V" R" O/ d. e
- __heap_limit 表示堆的结束地址
# E- D4 V* R* m) s - __heap_limit
复制代码 - V9 w+ F! S) l f5 J2 d1 r
③、生成属性设置、定义RESET代码段
, e6 X4 b* L' W
: n, U+ `: _+ s, ]- //PRESERVE8 指定当前文件保持堆栈八字节对齐。
3 ]* ^6 C, `0 _( M( Y - PRESERVE8
4 D( M$ U+ k- Q! H4 V( c9 H
0 ?' m: U& g: ~0 f* s7 b" Y- //THUMB 表示后面的指令是 THUMB 指令集 ,CM7 采用的是 THUMB - 2 指令集
3 `9 t/ T) H' X8 }" d - THUMB! t0 A6 e/ V2 h* G
- - a8 y% G7 `# a4 n% }
- //:AREA 定义一块代码段,只读,段名字是 RESET。 READONLY 表示只读,缺省就表示代码段了。
n: l( x% p% A. i" B5 M - ; Vector Table Mapped to Address 0 at Reset
, |7 F/ X0 f% M7 z - AREA RESET, DATA, READONLY4 y/ S) o4 Z# p
3 m; h i8 E+ P- //3 行 EXPORT 语句将 3 个标号申明为可被外部引用, 主要提供给链接器用于连接库文件或其他文件。) n: i0 i5 H8 F: o8 X
- EXPORT __Vectors
4 l9 f+ ]( P1 l - EXPORT __Vectors_End
( y% l/ E* e# Q" i& k - EXPORT __Vectors_Size
复制代码
6 y% k9 p3 ]8 W3 E/ N④、中断向量变定义9 c+ \4 B2 `1 T, y# k
5 K" ^' B. r! k
- __Vectors DCD __initial_sp ; Top of Stack5 {! }2 _1 |, \" x. e, L/ w9 O$ ~! h
- DCD Reset_Handler ; Reset Handler. \8 A, M: h" V
- DCD NMI_Handler ; NMI Handler1 F s0 j2 t/ Q B$ P( n# p4 [
- DCD HardFault_Handler ; Hard Fault Handler
) M* p: _: `6 u' d6 x - /* 省略部分代码 */
1 H# ~7 d& x/ i& n- \ - DCD 0 ; Reserved ; e: H: j' t; s, F1 {" Z
- DCD WAKEUP_PIN_IRQHandler ; Interrupt for all 6 wake-up pins 6 p2 c& f+ M; i& W
- . b, k- Y: f. [* ]* N. U g
; x2 z' m& q& d- __Vectors_End( l$ r: O( c5 k% ] L% i9 ^
- 4 W- I8 B5 w# B; P
- //定义向量表大小
4 s g( f9 G# E - __Vectors_Size EQU __Vectors_End - __Vectors
复制代码 $ f8 f* }" [9 c' M' B. U* r* ?
上面的这段代码是建立中断向量表,中断向量表定位在代码段的最前面。具体的物理地址由链接器的配置参数(IROM1 的地址)决定。如果程序在 Flash 运行,则中断向量表的起始地址是0x08000000。以 MDK 为例,就是如下配置选项:
9 L. H/ S( `7 [( \% \8 v2 @
* ^; [" M/ D% M6 F: H
9 H3 E) ?! ~1 e
; K2 @8 x% x/ q" I7 nDCD 表示分配 1 个 4 字节的空间。每行 DCD 都会生成一个 4 字节的二进制代码。中断向量表存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序
8 `2 E; S- k$ Z9 G$ Z8 N5 Q2 z( J: S- s7 M. F4 C
⑤、代码段定义、Reset_Handler过程处理
! A$ V. ?/ l/ M' F" W/ l2 r1 K/ S2 Y1 d* L
- AREA |.text|, CODE, READONLY
2 X' X7 o7 C7 r2 d
5 O- J$ O! }% F) I- s- ; Reset handler
" A8 o7 \7 [) S0 q d& _) L - Reset_Handler PROC
: x z. e5 G1 L" \6 }3 o/ j - EXPORT Reset_Handler [WEAK]" ^% N: q4 K) N) M2 N {3 b
- IMPORT SystemInit- `+ m& W, d$ b2 L9 V
- IMPORT __main. F9 H1 x' s. o! s
- ; V) T5 W8 I" d5 ?2 f/ o. I
- LDR R0, =SystemInit: d4 k- }# q4 p! `
- BLX R0. O% ? z9 X7 t! v+ h7 R( B) a3 E0 f
- LDR R0, =__main% w! I8 k' K/ F0 H' f L" ?# f
- BX R0
% w! L' n& o; D3 B. ]! k8 }4 f - ENDP
复制代码
8 ?$ m% Z+ Y, X. T f/ Q6 iAREA 定义一块代码段,只读,段名字是 .text 。 READONLY 表示只读% |% N" Y! f& Q8 ]& ~
利用 PROC、 ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
, H& y0 S2 O! p5 E2 B# f- IWEAK 声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话会调用外面的。 这个声明很重要,它让我们可以在 C 文件中任意地方放置中断服务程序,只要保证 C 函数的名字和向量表中的名字一致即可。
/ P7 @* ~* C! I, T" D* Y& UIMPORT:伪指令用于通知编译器要使用的标号在其他的源文件中定义。但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。
, P9 l9 ~+ A, |SystemInit 函数在文件 system_stm32h7xx.c 里面,主要实现 RCC 相关寄存器复位和中断向量表位置设置。
6 m: j: m7 Q# K4 W__main 标号表示 C/C++标准实时库函数里的一个初始化子程序__main 的入口地址。该程序的一个主要作用是初始化堆栈(跳转__user_initial_stackheap 标号进行初始化堆栈的,下面会讲到这个标号),并初始化映像文件,最后跳转到 C 程序中的 main 函数。这就解释了为何所有的 C 程序必须有一个 main 函数作为程序的起点。因为这是由 C/C++标准实时库所规,并且不能更改。
# o+ h+ t% c/ p) L7 X2 b
+ w7 j9 k7 y3 [+ Q⑥、缺省中断服务函数定义- ; Dummy Exception Handlers (infinite loops which can be modified) A2 Q; k) F; ]& V/ U2 D
; w$ K7 X) @7 n3 J" G) m- NMI_Handler PROC
1 s) d4 y% q. Z3 B. ?1 K - EXPORT NMI_Handler [WEAK]
! v7 ~3 } K1 U' h+ l - B . //死循环
- y9 W6 L1 G8 g! P* V - ENDP
6 X3 A" C: }4 y - ...省略
. Z' z( z G& m$ O7 a - EXPORT PendSV_Handler [WEAK]5 k- f B6 A0 ?5 @9 h3 N
- B .. @- g" l, f; k9 P1 Y
- ENDP
8 ]* {% |, C# N - SysTick_Handler PROC0 X$ M6 q! R$ c( g
- EXPORT SysTick_Handler [WEAK]
( ^7 q. V' N! O5 @ - B .$ ?9 {* \7 b4 a4 z, N- S. |! R4 R8 c
- ENDP
* H, `' Y2 c$ U8 [/ b' `, J
0 q% n: e; @; p/ b3 J3 h- Default_Handler PROC # Y1 h6 T# @. `9 v6 i
- 1 n' h2 P. a1 L/ W1 ^& \" y7 C
- EXPORT WWDG_IRQHandler [WEAK]
% d* q; {) @5 W# }$ \- H0 F - EXPORT PVD_AVD_IRQHandler [WEAK]
3 D# |3 }4 ^6 x - ...省略
- n+ A! d5 j; G1 U - EXPORT WAKEUP_PIN_IRQHandler [WEAK] . @& z! w" T) O; Y) o
- 8 F6 c5 R4 x# \; e
- 6 c- R7 U8 D4 i X
- WWDG_IRQHandler # T% M! X# H; p9 C- \9 d, O) r# U
- ...省略 : b. R0 q- k7 M' T3 ]7 t6 X
- WAKEUP_PIN_IRQHandler
0 y1 H# c0 i1 j1 o# | i# L" x
, O( V7 M) m5 x$ `" B9 u ^8 I- B .
$ b6 p1 k! I* F+ e$ ?
( {6 {/ ?5 x: d- ENDP
1 @1 I+ E: C6 p0 `5 h9 `5 O - 8 H9 o4 ?8 A% X. p# d- W1 L6 c
- ALIGN
复制代码
% U7 k; s. C9 w& o. I) _这里全部的中断服务函数都是用[WEAK]来声明,假如没有在其他文件中定义同名的中断服务函数,来了中断,就会进入到这里。3 P1 N, s8 o: a- U
0 L) j& Q: z, }) c# E2 P⑦、堆和栈的初始化
8 w2 c, u- p5 X8 e3 l( R
& F# n( y* F9 P' f- ;******************************************************************************** q5 }% G7 T. A! j
- ; User Stack and Heap initialization/ r. b6 g# j9 ?
- ;*******************************************************************************# b" m, t$ v0 v* I0 D
- //假如定义了MICROLIB,这里类似于if...else...
+ y3 F) S+ J4 D+ \6 K0 z' z - IF :DEF:__MICROLIB
" z" X- s3 p# m3 e; l1 W' w4 S0 u
" a& w) b3 z/ `# \+ ^- EXPORT __initial_sp
/ x9 R- y/ J9 ^/ v9 G0 H - EXPORT __heap_base
2 `+ J A- b% v4 J - EXPORT __heap_limit( X. Y' k& Y2 H8 @
. n( b8 @# O, N, V9 H: L2 P2 C- ELSE5 c! q2 r: h/ q% t# M" ?
1 E6 K* h r6 s+ d: L9 E- IMPORT __use_two_region_memory4 C3 Q7 Z" X5 c
- EXPORT __user_initial_stackheap
( P* ^* d2 D8 t( x1 c7 F
5 T3 c9 e" V' z' |- //__user_initial_stackheap 将由__main 函数进行调用。 9 ]! t a8 f' v
- __user_initial_stackheap$ K, v2 W1 x7 r A
# g u$ l) _) ]1 e0 X2 N) Y- LDR R0, = Heap_Mem$ h0 A5 X2 W6 E) l+ W5 N- s; G* K
- LDR R1, =(Stack_Mem + Stack_Size)- o* Y1 m7 d# w" A
- LDR R2, = (Heap_Mem + Heap_Size)
# U, h% o% e1 P6 Q) D3 w - LDR R3, = Stack_Mem
% Y" e' V% _' M+ C' T8 m7 Y+ B4 M6 B - BX LR; d' ^ A1 Q+ p1 u
- K# |0 C7 o' Z" H R- ALIGN* f' v9 t. c+ f" C! |) o1 T
- H6 ^4 A) q7 ?* C* k
- ENDIF4 G" m8 q+ t! ^- f# o" J
- $ D8 S" R. i# j0 u9 @$ S$ {6 _
- END
复制代码 Boot的启动模式不同于以往的M3、M4内核的ST MCU,H7的boot引脚只有一个,但是H7 专门配套了两个 option bytes 选项字节配置,如此以来就可以方便设置各种存储器地址了。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9J2Qpbcf-1574601496448)(./1574512528932.png)]
) B# n. r5 s3 D5 A. S* Z% e! xBOOT_ADD0 和 BOOT_ADD1 对应 32 位地址到高 16 位,这点要特别注意。 通过这两个选项字节,所有 0x0000 0000 到 0x3FFF 0000 的存储器地址都可以设置,包括:
& H- x( v$ c, @' R7 p3 u1 t: y
4 U% M1 x1 U, D' ]# z; r, C所有 Flash 地址空间。5 J5 T! E1 M+ ?' _1 Q% B
所有 RAM 地址空间,ITCM,DTCM 和 SRAM。
2 f1 v2 ~6 V# m5 `5 ?设置了选项字节后,掉电不会丢失,下次上电或者复位后,会根据 BOOT 引脚状态从 BOOT_ADD0,或 BOOT_ADD1 所设置的地址进行启动。
3 `) Z M6 s! L1 o使用 BOOT 功能,注意以下几个问题:8 U5 t9 {# B2 N1 w& b& K. C y
如果用户不慎,设置的地址范围不在有效的存储器地址,那么 BOOT = 0 时,会从 Flash 首地址 0x08000000 启动,BOOT = 1 时,会从 ITCM 首地址 0x0000 0000 启动。0 S6 Y4 N4 m2 c9 r! g; G+ P
当 Flash 的保护级别被配置为级别 2 之后, 只能从 Flash 自举。 如果 BOOT_ADD0/BOOT_ADD1选项字节中自举地址被配置为位于存储器范围之外或属于 RAM 地址范围,则系统只能从地址 0x0800 0000 上的 Flash 开始执行
6 J! G; A5 ~5 F, ~# m4 M& ~* o( A" L! I
/ l1 @# ]2 c6 q8 u3 A! G9 @9 S: B, `) l- y d" x& W
|