STM32H7的启动过程分析3 O" V+ J* E4 ~1 V+ ^( D( {
启动文件+ }" Y4 b2 R; r( f( S' D
不同编译器对应的启动文件不同,在MDK下,以startup_stm32h743xx.s为例,这是一个汇编文件,启动文件中主要做了如下事情:
2 `& k( |' j, ~8 s: Z0 m设置堆栈指针 SP = __initial_sp。
' x9 l$ @, S& S& s7 s设置 PC 指针 = Reset_Handler。
. }! h+ J0 H4 Y+ w$ r设置中断向量表。$ M& T6 b @1 Z% V8 J
配置系统时钟。
( q9 n. }0 P' |1 U" d! W配置外部 SRAM/SDRAM 用于程序变量等数据存储(这是可选的)。
# B6 e0 T0 G% h& I9 n跳转到 C 库中的 __main ,最终会调用用户程序的 main()函数。
' t% C6 e4 N# f M0 JCortex-M 内核处理器复位后,处于线程模式,指令权限是特权级别(最高级别),堆栈设置为用主堆栈 MSP。$ s* t; {5 s9 E7 l& M1 {
堆栈指针
) J4 D5 N$ J+ w. {% e通用寄存器组
; _1 E W) Z$ t g- ECortex – M7/M4/M3 处理器拥有 R0-R15 的通用寄存器组。其中 R13 作为堆栈指针 SP。 SP 有两个,但在同一时刻只能有一个可以用。
, o9 x+ d! J" R7 X4 n* z8 l7 L主堆栈指针(MSP):这是缺省的堆栈指针,它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。
4 y) A. M8 ~5 g# R$ c4 \" R进程堆栈指针(PSP):用于常规的应用程序代码(不处于异常服务例程中时)。
" u: W W4 T" V l' |另外以下两点要注意:
" S& ?. f2 V6 |7 H大多数情况下的应用,只需使用指针 MSP,而 PSP 多用于 RTOS 中。
" b2 e6 X& f" r* U; Q" F2 AR13 的最低两位被硬线连接到 0,并且总是读出 0,这意味着堆栈总是 4 字节对齐的。9 w$ w0 C4 Q; A! b
- Q r3 ^9 U( YCortex-M7/M4/M3 向下生长的满栈
6 X6 E1 m3 A/ L9 ~* P; Q+ ~PUSH 入栈操作:SP 先自减 4,再存入新的数值
2 X# W& w. A% ~& V, Y5 N0 hPOP 出栈操作:先从 SP 指针处读出上一次被压入的值,再把 SP 指针自增 4 q/ {' ~( e0 }2 m8 @5 Z1 L o
) R) q3 i0 k; G5 k: T分析启动过程
+ I+ u8 {1 E3 n. Q硬件上电后,会触发硬件复位,复位之后,CPU 内的时序逻辑电路首先完成如下两个工作(程序代码下载到内部 flash 为例,flash首地址 0x0800 0000)
' A" b3 Y6 L$ y7 k, F* W# ]9 I3 s8 M
将 0x08000000 位置存放的堆栈栈顶地址存放到 SP 中(MSP)。2 S P$ p9 @4 g! y# @
将 0x08000004 位置存放的向量地址装入 PC 程序计数器。2 `1 N1 @+ N3 [9 Q
CPU 从 PC 寄存器指向的物理地址取出第 1 条指令开始执行程序,也就是开始执行复位中断服务程序 Reset_Handler。为啥,因为在启动文件中,最先做的两件事情是6 j- F! ~6 J/ b' |" t8 ]7 k6 f! d( }
设置堆栈指针 SP = __initial_sp K; o" H5 k. a" ~& a% F& E3 @1 g
设置 PC 指针 = Reset_Handler. w, N+ }7 K, ~7 u( G
复位中断服务程序会调用SystemInit()函数来配置系统时钟、配置FMC总线上的外部SRAM/SDRAM,然后跳转到 C 库中__main 函数。由 C 库中的__main 函数完成用户程序的初始化工作(比如:变量赋初值等),最后由__main 函数调用用户写的 main()函数开始执行 C 程序。
1 f$ ~! A8 G9 G6 J
, L* S5 `) p9 f代码分析$ |$ @, `" { J7 c0 H i
①、开辟栈(stack)空间,用于局部变量、函数调用、函数的参数等, i9 y% @/ y+ Q C0 ~0 F
' W8 g: O& o5 U7 w# q) w) ^
- //类似宏定义,这是个伪指令,定义栈大小,这里是以字节为单位 C' w) R$ i2 ?
- Stack_Size EQU 0x00001000 / T$ L+ S. p. b w" F0 A* d) r
- * E" j4 J% W" I- H
- /*
! L2 [/ V( U7 d$ t' D. ~* F - 开辟一段数据空间可读可写,段名 STACK,按照 8 字节对齐。 ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。 ARER 后面的关键字表示这个段的属性。
, W* L$ ` M5 V7 c4 ]& c - STACK :表示这个段的名字,可以任意命名。
m/ v. e7 V4 ^8 L6 b6 f - NOINIT:表示此数据段不需要填入初始数据。
" h8 b, i N1 G6 L# `9 h7 [. a - READWRITE:表示此段可读可写。
9 \* [: _5 x t8 C2 z. T - ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(地址对 8 求余数等于 0)。
" f) C) n8 G* a9 r4 n+ M( ? - */8 D7 h0 W: y* \; ]0 h/ H5 \
- AREA STACK, NOINIT, READWRITE, ALIGN=3
( T! s& q) O$ l4 N! j& Y* e
' o4 }# R9 D0 e% v( X- //SPACE 这行指令告诉汇编器给 STACK 段分配 0x00001000 字节的连续内存空间。
* s# ~9 s7 r5 B t3 u! S' [ - Stack_Mem SPACE Stack_Size
4 t* K8 n. {- z" e! Q0 a/ T
* M1 L& A6 |# [4 g4 B8 N- /*
; H5 V) U% }( W& P - __initial_sp 紧接着 SPACE 语句放置,表示了栈顶地址。 __initial_sp 只是一个标号,标号主要用于表示一片内存空间的某个位置,等价于 C 语言中的“地址”概念。地址仅仅表示存储空间的一个位置,从 C 语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。
$ v$ W' y: U) a" ^8 ~) i2 N" e9 p3 h - */) I6 O) I- G4 l0 a. F* m) I
- __initial_sp
复制代码 - H6 C B0 Y' X" \- _& J
①、开辟堆(heap)空间,主要用于动态内存分配,也就是说用 malloc,calloc, realloc 等函数
5 l \; }# Y+ d( N8 B) t分配的变量空间是在堆上* l! {0 g. ]" S- p
0 O. }' u4 E; S, m1 a2 j
- //定义堆空间大小
9 @2 p' k5 S( U" n - Heap_Size EQU 0x0000800! a4 J' h2 @( P- B( h: z
0 X" O; Z/ f! I; W) Z1 ]. f- //分配一段名字为HEAP、可读可写、不需要初值、8字节对齐的数据空间/ G7 W& w4 R: n9 D1 k1 L) F& n' D1 ^
- AREA HEAP, NOINIT, READWRITE, ALIGN=3
V' w, y. v' x$ I5 n3 K2 \
% K* |% F% V- c8 P- //__heap_base 表示堆的开始地址。
8 A8 ~9 H% U6 n; e2 x - __heap_base# c- P/ C! T1 E4 z& S6 E
1 a% w9 V1 \& ~6 E! @3 f- //SPACE 这行指令告诉汇编器给 HEAP段分配 0x0000800字节的连续内存空间。: W6 V( p' W1 R" l
- Heap_Mem SPACE Heap_Size& a4 F5 L" A, @; h
- 5 X: p, @" w9 C, j, A( D7 F
- __heap_limit 表示堆的结束地址
" i4 d; K& N! I9 @ H - __heap_limit
复制代码
8 N$ Y8 T# {& F4 a③、生成属性设置、定义RESET代码段
& ^$ a7 q( _6 [& Y3 q5 C5 O+ N- U9 _
- //PRESERVE8 指定当前文件保持堆栈八字节对齐。
( x) y3 H5 Z: A& H0 s5 y - PRESERVE8
' o# J: `: d0 \' g; U3 T5 [2 d: l - * ?, W/ I4 u( `# R1 w; Z q
- //THUMB 表示后面的指令是 THUMB 指令集 ,CM7 采用的是 THUMB - 2 指令集
# c* v3 y* [' r) @0 `, z, G. } - THUMB+ r' z1 G, T2 j9 W' `
- " Z- i) g- X: M9 A1 ^
- //:AREA 定义一块代码段,只读,段名字是 RESET。 READONLY 表示只读,缺省就表示代码段了。
3 |. N6 i2 n, b0 P% h6 x# x! W - ; Vector Table Mapped to Address 0 at Reset
3 J+ J5 y& S2 l5 A: X( Z3 W! m - AREA RESET, DATA, READONLY4 ?1 k0 j/ I/ |) ?4 c
% V" `' N. I1 R" T( V+ N! ~& V3 j- //3 行 EXPORT 语句将 3 个标号申明为可被外部引用, 主要提供给链接器用于连接库文件或其他文件。
7 r4 G# U! E5 o. j1 x - EXPORT __Vectors
( [3 i; `, e! N9 b - EXPORT __Vectors_End
# O6 |5 `/ |" M, w4 O$ Z. O - EXPORT __Vectors_Size
复制代码 2 N6 c9 z- d# q% E/ a. D
④、中断向量变定义9 X: Q3 ]' E; S/ k" J
/ y& b: f P4 e5 X9 r( F
- __Vectors DCD __initial_sp ; Top of Stack
+ M0 Y% {. j/ i+ e* s! Z& i v7 t - DCD Reset_Handler ; Reset Handler
1 u( G# T8 b1 _* M8 O, S - DCD NMI_Handler ; NMI Handler2 v- ?: p1 K' D5 {0 q8 n5 u& J
- DCD HardFault_Handler ; Hard Fault Handler: U. s: e! X, x9 v* ^1 L
- /* 省略部分代码 */
! @9 a, k( L6 k$ ~% k: k3 }* g# P8 } - DCD 0 ; Reserved
4 |6 F! [7 e- B - DCD WAKEUP_PIN_IRQHandler ; Interrupt for all 6 wake-up pins d- E6 p6 D& C3 u0 n8 b- Y
- 7 i( ~% i' I: I5 s6 p3 I" g
- : u$ X+ J( P8 Q& i
- __Vectors_End
, f: a# E- U; z L0 b% m
: h# Z' Q0 a( @( d2 p& Q- a- //定义向量表大小
7 x) @; c( T- d - __Vectors_Size EQU __Vectors_End - __Vectors
复制代码
, i+ O: m' V3 D# m y5 r上面的这段代码是建立中断向量表,中断向量表定位在代码段的最前面。具体的物理地址由链接器的配置参数(IROM1 的地址)决定。如果程序在 Flash 运行,则中断向量表的起始地址是0x08000000。以 MDK 为例,就是如下配置选项:* j/ x& Q! t; D* s( l. K2 K
2 v( H2 U* s% t' y! X2 Z
1 d* g h+ J0 @7 h9 R: L% G- [. r2 U' t2 ~- Z7 K, ?
DCD 表示分配 1 个 4 字节的空间。每行 DCD 都会生成一个 4 字节的二进制代码。中断向量表存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序5 v' }3 b/ N, J
) u9 Q1 p0 O O6 R% |, `
⑤、代码段定义、Reset_Handler过程处理
. a4 Z" D, x2 y9 J
$ U% f& w' f, g8 M; h/ s- AREA |.text|, CODE, READONLY8 c; ]* t( V) b$ C: b+ T
- $ q0 y+ m" ?1 D' U' f5 y* b
- ; Reset handler
1 U9 W- G. E, k3 c( Z - Reset_Handler PROC, R4 N$ c ^5 s6 d+ s# e" b; T; O
- EXPORT Reset_Handler [WEAK]& ?, V# o6 O. z5 B; i, g9 t
- IMPORT SystemInit
2 m7 Z9 w" t: h - IMPORT __main& d; |# W$ Z6 B8 K$ G! o4 r
- s) [% e5 H C$ O: p+ F! r
- LDR R0, =SystemInit
( [0 e& N' @# i7 e. C" g; K8 O - BLX R0 f2 J# r; _+ o/ ]4 {( }
- LDR R0, =__main) W1 n% A' D2 I& W
- BX R0
7 a1 M( S3 r, I% }8 }! {; y9 @ - ENDP
复制代码 1 h7 L4 c4 D, n9 f# u
AREA 定义一块代码段,只读,段名字是 .text 。 READONLY 表示只读1 Y3 j( V- i1 ~2 f$ Z4 i
利用 PROC、 ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
4 U7 r3 d. D( X9 h" @& W& uWEAK 声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话会调用外面的。 这个声明很重要,它让我们可以在 C 文件中任意地方放置中断服务程序,只要保证 C 函数的名字和向量表中的名字一致即可。: [, N$ U- D5 S$ t6 i
IMPORT:伪指令用于通知编译器要使用的标号在其他的源文件中定义。但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。: r9 O* K$ K) |
SystemInit 函数在文件 system_stm32h7xx.c 里面,主要实现 RCC 相关寄存器复位和中断向量表位置设置。
6 C% `8 a3 D9 K$ A/ B__main 标号表示 C/C++标准实时库函数里的一个初始化子程序__main 的入口地址。该程序的一个主要作用是初始化堆栈(跳转__user_initial_stackheap 标号进行初始化堆栈的,下面会讲到这个标号),并初始化映像文件,最后跳转到 C 程序中的 main 函数。这就解释了为何所有的 C 程序必须有一个 main 函数作为程序的起点。因为这是由 C/C++标准实时库所规,并且不能更改。
Y& `1 D8 [. O+ X7 |8 l! U0 `$ d0 \5 p: S0 s7 k9 u
⑥、缺省中断服务函数定义- ; Dummy Exception Handlers (infinite loops which can be modified)
" M. v4 X$ \2 a; _
# _- X G' {; n, w! v' i/ f- NMI_Handler PROC% Y' N% N' ]+ n e% D1 u1 Q* r8 P
- EXPORT NMI_Handler [WEAK]
+ C3 G3 |: }5 V' x" i - B . //死循环" d$ H" O$ K% Z! _1 V4 [% R
- ENDP, s' n( {" j/ E7 R0 \3 z4 d
- ...省略; ^& t; ]% Y3 ]: f T% Q- P& n. ?
- EXPORT PendSV_Handler [WEAK]* L9 K6 g, r) j3 g7 C9 H
- B .
& B* k7 y- _% u5 }8 t+ e - ENDP
7 Y% R' ~, B. A$ q' N, h - SysTick_Handler PROC2 v. l8 C0 P( W6 w( ?/ y4 p
- EXPORT SysTick_Handler [WEAK]
8 c6 t4 @" }8 U* ]+ _- e - B .
! w" @: [4 M( X! S" F+ z: F - ENDP
8 `; a) p/ {/ p' n - # [9 R% v, d3 z4 z3 J& W( C/ K
- Default_Handler PROC 7 `* k3 c0 [3 t3 [- \: f
% O* Q* e$ d9 G0 W# d/ G {' f8 J- EXPORT WWDG_IRQHandler [WEAK] : u1 ^- [" x& _, B* h
- EXPORT PVD_AVD_IRQHandler [WEAK]
: e1 U8 N# L/ z1 ~* D - ...省略
0 A) ~7 m C T' M ? - EXPORT WAKEUP_PIN_IRQHandler [WEAK] 1 o- @' _' F/ j" d
% v" i9 z2 f: l/ M, C: [9 u
4 i( ^3 |+ @% N8 {6 s: s9 Y- WWDG_IRQHandler
4 b' n; X1 L2 }. i& Z( N - ...省略
% f" @7 J( f' D - WAKEUP_PIN_IRQHandler, |" \0 T8 {2 \# w
- ( l& `' n; C/ Y) c6 g! Q5 @, z
- B .8 I# U' |$ A- O& N2 c7 |
. t5 ^2 Q! l& X$ t( A+ @- ENDP+ W$ F1 ?2 U$ N7 g) ~7 S
- 6 }. K1 B4 c, R; F* F% e$ L* c1 v/ e
- ALIGN
复制代码
% B) c: V* Q+ E6 H, `这里全部的中断服务函数都是用[WEAK]来声明,假如没有在其他文件中定义同名的中断服务函数,来了中断,就会进入到这里。. I( z; u% W$ P1 ?
7 p0 D1 V! ]$ T0 d. J
⑦、堆和栈的初始化
5 _# c* {; A0 @" W& ~# r1 R* h+ N' G( a/ u- b
- ;*******************************************************************************( f, X# B+ M8 R
- ; User Stack and Heap initialization( a0 V: V5 @, ?
- ;*******************************************************************************
4 r/ j. G3 V0 \% C- r - //假如定义了MICROLIB,这里类似于if...else...
& H' ]- S7 F' J k - IF :DEF:__MICROLIB
+ ~. G, V/ J$ f$ b3 n
/ r: y: R& H0 T0 S- EXPORT __initial_sp
, I" {; j: Q% X - EXPORT __heap_base
; A3 ?4 |- D7 `- [: |# \ - EXPORT __heap_limit
/ W1 n Q& E3 K @
( N0 |& I& X! N- ELSE
, F( W. E# i3 Q - . t/ F7 Z; o) F" s- }- R
- IMPORT __use_two_region_memory
- O% {8 r/ f# y. U) u" e - EXPORT __user_initial_stackheap
; m, n9 U! X* C8 P1 x) z6 i4 O% W
1 ~3 s- B. S M8 C+ f! w- //__user_initial_stackheap 将由__main 函数进行调用。 / Y5 p" o3 \; U; x5 c5 G! g
- __user_initial_stackheap
: w) u9 g+ B+ ?& H - - |$ o* z1 P3 s
- LDR R0, = Heap_Mem$ n4 ]2 X- s2 }( @
- LDR R1, =(Stack_Mem + Stack_Size)
- I9 t7 U* U2 G6 G+ ~- | y - LDR R2, = (Heap_Mem + Heap_Size)
* D) m/ k, m- Z5 V/ L8 \) g, { - LDR R3, = Stack_Mem
0 W! {3 g, z( M# g - BX LR
- Q# E" X$ C% F, r/ b) j" c
# J( B4 G5 L! H+ c7 M% F% ]- ALIGN
9 h6 m, C$ Y2 e& R3 D. I+ F) L
1 J( F. t5 c5 `: r- ENDIF/ z3 h6 E" h/ @7 F0 e# P
- ; `5 v& N$ ^$ Q
- END
复制代码 Boot的启动模式不同于以往的M3、M4内核的ST MCU,H7的boot引脚只有一个,但是H7 专门配套了两个 option bytes 选项字节配置,如此以来就可以方便设置各种存储器地址了。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9J2Qpbcf-1574601496448)(./1574512528932.png)]
3 Q. H; r6 |% |4 C; Q9 f, R& VBOOT_ADD0 和 BOOT_ADD1 对应 32 位地址到高 16 位,这点要特别注意。 通过这两个选项字节,所有 0x0000 0000 到 0x3FFF 0000 的存储器地址都可以设置,包括:$ z4 K. P; ^% R+ B9 j
6 X- Y F1 K% b# ?: W& u% D所有 Flash 地址空间。
" A& D' p1 V, \; n& y所有 RAM 地址空间,ITCM,DTCM 和 SRAM。
$ q% ^( o6 G" V设置了选项字节后,掉电不会丢失,下次上电或者复位后,会根据 BOOT 引脚状态从 BOOT_ADD0,或 BOOT_ADD1 所设置的地址进行启动。 Y q# u2 |% y9 T
使用 BOOT 功能,注意以下几个问题: e: q: |0 a1 t7 l& f" Y6 {
如果用户不慎,设置的地址范围不在有效的存储器地址,那么 BOOT = 0 时,会从 Flash 首地址 0x08000000 启动,BOOT = 1 时,会从 ITCM 首地址 0x0000 0000 启动。4 i0 N. \2 V8 b% ^- v. F# V
当 Flash 的保护级别被配置为级别 2 之后, 只能从 Flash 自举。 如果 BOOT_ADD0/BOOT_ADD1选项字节中自举地址被配置为位于存储器范围之外或属于 RAM 地址范围,则系统只能从地址 0x0800 0000 上的 Flash 开始执行
, i- M' Q4 u) t# u: [: l4 T, `2 W6 A0 P
6 E# y0 z6 {' B4 `0 P
/ F/ t f( r( n G4 V
|