STM32H7的启动过程分析. W- b! a& f1 t S2 j
启动文件
$ d6 V- D& L U; ~8 K) D不同编译器对应的启动文件不同,在MDK下,以startup_stm32h743xx.s为例,这是一个汇编文件,启动文件中主要做了如下事情:( w& g$ c; d* S1 `7 \
设置堆栈指针 SP = __initial_sp。; O9 A# {5 g1 n: g4 u3 `: a
设置 PC 指针 = Reset_Handler。
% D" E! ^/ P7 q, @' K( ? t8 I6 g: |设置中断向量表。
4 W* d2 V" b; j. C配置系统时钟。
) s2 h6 ]' j+ Y' z. |- t9 [配置外部 SRAM/SDRAM 用于程序变量等数据存储(这是可选的)。3 V% N7 Q5 U" Z
跳转到 C 库中的 __main ,最终会调用用户程序的 main()函数。2 f9 N* W) i6 V8 J
Cortex-M 内核处理器复位后,处于线程模式,指令权限是特权级别(最高级别),堆栈设置为用主堆栈 MSP。
" [0 g3 _" _, F3 \堆栈指针
4 Q1 N5 A; Z8 f4 h& ~$ p& }. N通用寄存器组
' W: c2 k& L0 U3 PCortex – M7/M4/M3 处理器拥有 R0-R15 的通用寄存器组。其中 R13 作为堆栈指针 SP。 SP 有两个,但在同一时刻只能有一个可以用。
0 v/ ^: S# w1 M* |0 b+ V主堆栈指针(MSP):这是缺省的堆栈指针,它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。, n4 F. @3 M+ @0 v0 [
进程堆栈指针(PSP):用于常规的应用程序代码(不处于异常服务例程中时)。6 C& v7 C j. ^4 h. k( |
另外以下两点要注意:
. x. b6 p" k) p大多数情况下的应用,只需使用指针 MSP,而 PSP 多用于 RTOS 中。8 Q+ Z) h$ Q) S" ?3 `
R13 的最低两位被硬线连接到 0,并且总是读出 0,这意味着堆栈总是 4 字节对齐的。
3 N- Y; C" x" ]+ o' N2 ^- j& u5 I2 W! H1 x y5 V g! O9 s x3 b
Cortex-M7/M4/M3 向下生长的满栈( J/ [# k7 n+ G7 Z( T
PUSH 入栈操作:SP 先自减 4,再存入新的数值2 m9 ?3 X }' {* @8 [2 }
POP 出栈操作:先从 SP 指针处读出上一次被压入的值,再把 SP 指针自增 4
) \7 q2 y" j) v9 R0 n: u: K6 d( V3 h: ?- n) b; G7 o* \9 \& V
分析启动过程
: `; c6 M9 H* {* S; M, y3 o% g( x硬件上电后,会触发硬件复位,复位之后,CPU 内的时序逻辑电路首先完成如下两个工作(程序代码下载到内部 flash 为例,flash首地址 0x0800 0000)3 G* ?5 A, I9 Y$ q$ R
% c$ K# G, l6 n9 X
将 0x08000000 位置存放的堆栈栈顶地址存放到 SP 中(MSP)。
4 J# W; P* ]0 s& @0 Q% @将 0x08000004 位置存放的向量地址装入 PC 程序计数器。/ j9 D: x# M2 T
CPU 从 PC 寄存器指向的物理地址取出第 1 条指令开始执行程序,也就是开始执行复位中断服务程序 Reset_Handler。为啥,因为在启动文件中,最先做的两件事情是
2 n% b( ], ~+ h i3 ?% {, K设置堆栈指针 SP = __initial_sp8 Z& b& R$ F) s
设置 PC 指针 = Reset_Handler3 Q ]) L! e7 c2 `
复位中断服务程序会调用SystemInit()函数来配置系统时钟、配置FMC总线上的外部SRAM/SDRAM,然后跳转到 C 库中__main 函数。由 C 库中的__main 函数完成用户程序的初始化工作(比如:变量赋初值等),最后由__main 函数调用用户写的 main()函数开始执行 C 程序。! M5 I9 n; ~9 U, E, \- L
" Y$ L& _8 \' _. O" c0 l
代码分析0 n7 ]: Z# P- ^: ]* ~# \4 F- K) s# A
①、开辟栈(stack)空间,用于局部变量、函数调用、函数的参数等: z( f/ `" `3 Y& ]9 K$ O4 v
, R, `, v+ o7 j9 ~- f
- //类似宏定义,这是个伪指令,定义栈大小,这里是以字节为单位
9 a: N7 S+ l) _5 K, i - Stack_Size EQU 0x00001000
* k# y' \: J& e" J1 u' O3 L+ `
0 c7 U8 j8 n8 o) c- /* l: [. H! D& }) \
- 开辟一段数据空间可读可写,段名 STACK,按照 8 字节对齐。 ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。 ARER 后面的关键字表示这个段的属性。
& y5 k4 ~8 g6 r7 a! N( e - STACK :表示这个段的名字,可以任意命名。
/ \2 ~; z5 v9 v& H7 e8 C! e5 W - NOINIT:表示此数据段不需要填入初始数据。$ G, V, h7 [/ T. Z! n- l
- READWRITE:表示此段可读可写。- R m2 F, e. `) ?! k, d- L6 z+ f% F" [
- ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(地址对 8 求余数等于 0)。( w( p# \ M# p* P' O1 v
- */
# Y- ~1 P$ @$ ^+ l# y) z1 w - AREA STACK, NOINIT, READWRITE, ALIGN=3
; X/ q' G- |9 \ - ' O: K. V% L& ^
- //SPACE 这行指令告诉汇编器给 STACK 段分配 0x00001000 字节的连续内存空间。: @6 E- h( T6 a9 W5 G
- Stack_Mem SPACE Stack_Size
7 h7 T1 [9 M2 F) e. s# S% c$ S2 _ - # ?% Y8 L1 F7 k
- /*, T6 z' h9 l; B: a! P' E
- __initial_sp 紧接着 SPACE 语句放置,表示了栈顶地址。 __initial_sp 只是一个标号,标号主要用于表示一片内存空间的某个位置,等价于 C 语言中的“地址”概念。地址仅仅表示存储空间的一个位置,从 C 语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。
$ w/ a9 }! B5 ~% [3 A0 V% c3 J) I - */
; Y, r5 f: c4 j( e0 m - __initial_sp
复制代码
# }4 z) Y; T0 w7 Q- Z# X d) E+ F①、开辟堆(heap)空间,主要用于动态内存分配,也就是说用 malloc,calloc, realloc 等函数8 r2 Z k$ ^! L: P
分配的变量空间是在堆上; w" q" \; U& j# w/ C
6 H/ _6 Z; j. A" K$ R2 {" W+ s
- //定义堆空间大小
, a3 n7 ^. D! K3 ?$ J4 @ C3 D - Heap_Size EQU 0x0000800 d5 t/ p( i7 u5 @% Y: m. M1 N
8 j/ H' x- C2 ]6 T& q1 g- //分配一段名字为HEAP、可读可写、不需要初值、8字节对齐的数据空间
. }" ?4 j% V# m8 ?; G - AREA HEAP, NOINIT, READWRITE, ALIGN=3
7 r# }# g' u8 U+ u
5 f5 D1 J: d7 J1 C0 p2 Q- //__heap_base 表示堆的开始地址。8 n1 P' h- S9 D# {
- __heap_base8 ?, D1 A) ~* ]5 C" @% r9 C
- ! C6 k4 F! U/ `+ @* Q
- //SPACE 这行指令告诉汇编器给 HEAP段分配 0x0000800字节的连续内存空间。7 j# f/ O" G4 p& G- I" G
- Heap_Mem SPACE Heap_Size
6 a# i5 `8 F, j - / I5 Y0 O8 N( V2 X& k0 ?: S/ e2 B
- __heap_limit 表示堆的结束地址
4 ~$ a, }( G4 H - __heap_limit
复制代码 6 y# m& a( V5 }6 D' x! f7 Y+ C4 E4 b7 x, m
③、生成属性设置、定义RESET代码段
, Z& M* J4 k- w" P2 k
$ U N0 g' N1 S: |8 j- //PRESERVE8 指定当前文件保持堆栈八字节对齐。
# o' A8 I4 d4 K* z( | - PRESERVE8# j% D# ~# B2 A8 i! [" ?1 g( m
- % q/ R& n2 w1 _4 p. b
- //THUMB 表示后面的指令是 THUMB 指令集 ,CM7 采用的是 THUMB - 2 指令集: J2 T3 p! P/ U4 p
- THUMB8 x8 ~0 Z6 h/ n* }, J4 y- @
) E, F" O3 B9 p, }7 i$ o2 K- //:AREA 定义一块代码段,只读,段名字是 RESET。 READONLY 表示只读,缺省就表示代码段了。- F _% f. L' ` }" \
- ; Vector Table Mapped to Address 0 at Reset
0 ?9 _5 x& P5 i3 k) I6 r U/ L J - AREA RESET, DATA, READONLY5 Q$ @5 R. @7 Q: Q8 ~+ l9 H
- 7 i. L, L; e! H- Y" c1 f& f' }1 m; H
- //3 行 EXPORT 语句将 3 个标号申明为可被外部引用, 主要提供给链接器用于连接库文件或其他文件。
* _6 V+ R. k$ g; U/ L2 s - EXPORT __Vectors
) D; x0 {# P0 `/ w1 a9 [ - EXPORT __Vectors_End
% ^0 I! ?! D0 [$ e: H' g - EXPORT __Vectors_Size
复制代码 " I4 V; `. U4 }/ |6 B* s3 }9 W
④、中断向量变定义, @, n( f1 \, B/ S% |/ X( c, i
/ _+ K6 X S' r+ K1 E- __Vectors DCD __initial_sp ; Top of Stack
1 ^, h0 N4 E0 G6 a; r& K - DCD Reset_Handler ; Reset Handler3 g O( y* c) f. ?
- DCD NMI_Handler ; NMI Handler$ N( N+ D+ c6 i2 |; O9 i
- DCD HardFault_Handler ; Hard Fault Handler2 X. K7 q. |4 J! T- P$ Z
- /* 省略部分代码 */
$ v5 e5 j- M7 y0 j( D+ M - DCD 0 ; Reserved ) ]/ k, ~6 S) U& P. K- @; O
- DCD WAKEUP_PIN_IRQHandler ; Interrupt for all 6 wake-up pins
9 v0 t7 A* d8 U% I/ q - 6 F: q9 H, w. t; J. k
3 v( k2 U# z. s% \% o/ c- __Vectors_End: J, L6 \) g& C$ s; t, N. A# w! h
! H" N9 a5 i [& D+ D/ b. c1 r- //定义向量表大小' X( q; o2 ], w1 \* S' D! J0 p8 i
- __Vectors_Size EQU __Vectors_End - __Vectors
复制代码 $ I) }2 i }4 {* U6 H/ b9 s$ }
上面的这段代码是建立中断向量表,中断向量表定位在代码段的最前面。具体的物理地址由链接器的配置参数(IROM1 的地址)决定。如果程序在 Flash 运行,则中断向量表的起始地址是0x08000000。以 MDK 为例,就是如下配置选项:
) T% g% i7 M3 k1 B2 f0 v$ W# A X" M3 c& K# z
1 u# Z( C9 @, U8 e6 _" i7 @6 @8 n# q6 d7 }' L
DCD 表示分配 1 个 4 字节的空间。每行 DCD 都会生成一个 4 字节的二进制代码。中断向量表存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序% c7 \# r3 m! t1 |
7 s0 U% V# i2 n I) ^6 |⑤、代码段定义、Reset_Handler过程处理
8 p4 b! l0 e. ?" K5 u# A. x- C
9 e( r% @7 c! E3 P! ]: R- AREA |.text|, CODE, READONLY* _2 m) q5 y1 {" x
% \) ~2 Q: Q9 B3 k6 d/ [4 Q- ; Reset handler' d$ n/ L: h; `
- Reset_Handler PROC1 g5 J5 ~: u7 N4 ]
- EXPORT Reset_Handler [WEAK]( x; i4 _3 P; i/ c1 M% p
- IMPORT SystemInit
8 u/ r& P. ~+ [; w) U% v - IMPORT __main
, ]5 Q7 x1 v& ?7 z3 B+ Q5 m" H
; F$ {0 j* G' p7 n# ~- b- LDR R0, =SystemInit
) ]. @( U) M0 q5 x! r \ - BLX R0
; I' O; J* B8 d) j4 Y - LDR R0, =__main
+ x4 A1 A- n- r! Y( Q3 E - BX R0
/ N* L4 f, r1 i& G - ENDP
复制代码
8 d6 q4 @. |: @, B1 p! M1 bAREA 定义一块代码段,只读,段名字是 .text 。 READONLY 表示只读9 K% I5 N9 Y' O; v' T$ o1 M' X
利用 PROC、 ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。6 g3 ]9 k# d* [) F+ s( N* ]+ Q
WEAK 声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话会调用外面的。 这个声明很重要,它让我们可以在 C 文件中任意地方放置中断服务程序,只要保证 C 函数的名字和向量表中的名字一致即可。8 H4 o9 a, l9 M+ F& }* U
IMPORT:伪指令用于通知编译器要使用的标号在其他的源文件中定义。但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。0 K5 y O+ ]1 z1 b* N
SystemInit 函数在文件 system_stm32h7xx.c 里面,主要实现 RCC 相关寄存器复位和中断向量表位置设置。5 j3 h: J$ G0 u. N9 q* p
__main 标号表示 C/C++标准实时库函数里的一个初始化子程序__main 的入口地址。该程序的一个主要作用是初始化堆栈(跳转__user_initial_stackheap 标号进行初始化堆栈的,下面会讲到这个标号),并初始化映像文件,最后跳转到 C 程序中的 main 函数。这就解释了为何所有的 C 程序必须有一个 main 函数作为程序的起点。因为这是由 C/C++标准实时库所规,并且不能更改。
) T/ I7 ?+ ^7 k# _. r* N4 ]7 k; m5 \5 B
⑥、缺省中断服务函数定义- ; Dummy Exception Handlers (infinite loops which can be modified)
; \! \* n: Z3 r0 L, _
& j ]6 K% Z9 l- F6 z+ ~- NMI_Handler PROC8 B3 i1 u; H) L) k3 C- {' p
- EXPORT NMI_Handler [WEAK]
, K7 A$ k3 E2 V' R7 j( |- A6 q - B . //死循环7 h B7 s! M! p1 \
- ENDP
/ U4 M5 o0 H7 Q- C$ X& V# |4 S/ [ - ...省略
; A$ X. J# B3 t; }* f - EXPORT PendSV_Handler [WEAK]
, c; a. P+ {' n - B .' i4 V! K1 y+ N0 d) L% s( w/ ~. j$ D5 Y
- ENDP( b- Z5 T( F7 G8 W! X! Q, q q' e
- SysTick_Handler PROC
" @' f4 F% w1 M' k; l - EXPORT SysTick_Handler [WEAK]- w" s( [( K6 W% V' Q4 L4 y
- B .
8 L4 k- l7 D3 I! d) N - ENDP
+ S/ c* `7 b2 q. y( J1 U - / h u. v. z, y8 p! k! {; y
- Default_Handler PROC 2 M8 e* ~9 X+ q. u: m
- , ]1 b6 d- n8 i k+ k
- EXPORT WWDG_IRQHandler [WEAK] ( d) W( o! ^: c* C w
- EXPORT PVD_AVD_IRQHandler [WEAK]
' }. s" q U+ z: F. Z - ...省略
' w2 M1 a" ^+ h& W/ ^6 J - EXPORT WAKEUP_PIN_IRQHandler [WEAK]
8 q( |: P3 j D8 K8 \2 y - ) I9 g+ D C' o
. s3 |2 l! t) ]: t& K- WWDG_IRQHandler
8 O. Y5 A& z+ b; {+ a: E- W$ F6 L - ...省略
( O! m$ R/ a2 a j& ` - WAKEUP_PIN_IRQHandler
" x8 {0 @1 R6 g5 A( a
% M) H" Y8 Y9 W! X3 R, N- B .
0 C; ~. G1 y. J - ; L: g) x0 f; x D R$ g6 [
- ENDP
# D0 R- h( X5 I/ O6 ~# B3 y* i
: R* p; \/ Y( P/ I& h6 Z/ t- ALIGN
复制代码
$ K" A' ]! n3 \% L, |4 B这里全部的中断服务函数都是用[WEAK]来声明,假如没有在其他文件中定义同名的中断服务函数,来了中断,就会进入到这里。' t6 d' C0 y% p+ ?) }8 G
, |" P$ E( i3 O5 T0 Q
⑦、堆和栈的初始化% {3 e) p7 K5 e: X. S6 G: U" r4 `" L
: X$ r4 v3 k& W( ]
- ;*******************************************************************************
7 I/ o1 I/ G* H! i( l6 M - ; User Stack and Heap initialization* Y0 B! b8 }/ A E- x: N L
- ;*******************************************************************************
% Z- `% R+ L1 x [/ P - //假如定义了MICROLIB,这里类似于if...else...# x+ O5 R' v u
- IF :DEF:__MICROLIB2 {7 |& _- A; Q. c0 f" [! C( u
- + C9 e3 g' R) x) P
- EXPORT __initial_sp0 B1 O; H5 y* B5 p
- EXPORT __heap_base d/ m V( _( p4 X T8 Y
- EXPORT __heap_limit
! `- q& A W* a" F7 b
. A0 W; J7 U" G) H R# t1 W: b- ELSE
' E# A- Z O; V. p- K
) _8 M) D/ ], G( M- IMPORT __use_two_region_memory
* @$ w$ @+ W. r9 T5 q3 c - EXPORT __user_initial_stackheap. Z1 P/ l! L* {
" C, M4 r; I7 e" W- //__user_initial_stackheap 将由__main 函数进行调用。
/ Q7 M" W2 o# \ - __user_initial_stackheap
7 Z4 v0 b& U- Y! x) d
0 `3 d& p$ z, w3 q2 N4 P- LDR R0, = Heap_Mem
- v* O+ M8 }: Z# T. H& S5 _2 |. v - LDR R1, =(Stack_Mem + Stack_Size)
# T% y L6 c+ s+ b* X6 [' n - LDR R2, = (Heap_Mem + Heap_Size)1 q& m5 y# A* G N u4 r
- LDR R3, = Stack_Mem
5 @7 s! J! H7 _) u+ {; E - BX LR
- q+ ^1 r. b! y/ G4 } S - ! B; ^1 W7 F s4 Z
- ALIGN7 f k/ S0 v M) C2 v
, }: G, k) o# L; ]- a- ENDIF$ M" \1 v5 Q) U5 F+ e# ?! J
- 5 f- ^" p) t; z* a, E
- END
复制代码 Boot的启动模式不同于以往的M3、M4内核的ST MCU,H7的boot引脚只有一个,但是H7 专门配套了两个 option bytes 选项字节配置,如此以来就可以方便设置各种存储器地址了。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9J2Qpbcf-1574601496448)(./1574512528932.png)]
" f/ R1 g* G$ B$ }4 ~$ Q% JBOOT_ADD0 和 BOOT_ADD1 对应 32 位地址到高 16 位,这点要特别注意。 通过这两个选项字节,所有 0x0000 0000 到 0x3FFF 0000 的存储器地址都可以设置,包括:7 l- Y6 h$ s/ m0 p- D) c
( k7 |. M' Z8 U, {0 B所有 Flash 地址空间。
' q6 _5 X; L, I, h% J所有 RAM 地址空间,ITCM,DTCM 和 SRAM。3 }- v) k# j. s9 e U8 ]% M
设置了选项字节后,掉电不会丢失,下次上电或者复位后,会根据 BOOT 引脚状态从 BOOT_ADD0,或 BOOT_ADD1 所设置的地址进行启动。1 v3 l, R+ t. y0 a* {$ X" ~
使用 BOOT 功能,注意以下几个问题:
# c9 q: o1 `7 W0 a9 ~! @' }如果用户不慎,设置的地址范围不在有效的存储器地址,那么 BOOT = 0 时,会从 Flash 首地址 0x08000000 启动,BOOT = 1 时,会从 ITCM 首地址 0x0000 0000 启动。, b7 Y, [; k4 ?0 L/ j+ A
当 Flash 的保护级别被配置为级别 2 之后, 只能从 Flash 自举。 如果 BOOT_ADD0/BOOT_ADD1选项字节中自举地址被配置为位于存储器范围之外或属于 RAM 地址范围,则系统只能从地址 0x0800 0000 上的 Flash 开始执行0 p, w; Z, j* c6 Z* e: n) x% t5 n
+ i. K2 C) g; `7 _0 i5 u
1 l9 |$ J4 y- B) _5 V# U, e
1 Z; K5 ~, D* X# r5 A* J7 | |