一、概述1 a. p6 N9 G6 b, y# M/ H
1、说明每一款芯片的启动文件都值得去研究,因为它可是你的程序跑的最初一段路,不可以不知道。通过了解启动文件,我们可以体会到处理器的架构、指令集、中断向量安排等内容,是非常值得玩味的。
# U) b; H9 @ I$ f% G
STM32作为一款高端 Cortex-M3系列单片机,有必要了解它的启动文件。打好基础,为以后优化程序,写出高质量的代码最准备。 本文以一个实际测试代码--START_TEST为例进行阐述。 8 B1 \1 L1 @2 r0 i! ~
2、整体过程概括STM32整个启动过程是指从上电开始,一直到运行到 main函数之间的这段过程,步骤为(以使用微库为例): ①上电后硬件设置SP、PC ②设置系统时钟 ③软件设置SP ④加载.data、.bss,并初始化栈区 ⑤跳转到C文件的main函数
# @+ \2 c: ^. e1 \: k, m 3、整个启动过程涉及的代码推荐阅读启动过程涉及的文件不仅包含 startup_stm32f10x_hd.s,还涉及到了MDK自带的连接库文件 entry.o、entry2.o、entry5.o、entry7.o等(从生成的 map文件可以看出来)。8 ^1 T* k- m/ k4 v6 e) h- g# N2 M @9 `
' ^6 N7 r' [: }! V% l$ l二、程序在Flash上的存储结构: d: ^" ?* N1 d0 `5 N
在真正讲解启动过程之前,先要讲解程序下载到 Flash上的结构和程序运行时(执行到main函数)时的SRAM数据结构。程序在用户Flash上的结构如下图所示。下图是通过阅读hex文件和在MDK下调试综合提炼出来的。
- M- u$ [& L$ k7 O( S& `5 T% ?( X3 `9 V# j) @, I; u4 N4 V( g$ h
' Q! P( {& J7 B- g0 f6 H8 D7 h
) S% r* j/ v7 h8 m' d MSP初始值 编译器生成,主堆栈的初始值" `: t3 g! d, C6 B7 q
异常向量表 不多说 X. ^! [# M% M+ }% W
外部中断向量表 不多说
; m& k6 T# l& f% r7 I 代码段 存放代码5 E6 g) d8 Y" E& I, }8 z
初始化数据段 .
4 a' F$ ^; \4 N& e$ adata; h% d9 \% N2 v2 R0 \" t6 X
未初始化数据段 .bss
+ b( h- a# z# ^0 p& m% U8 m# K6 ~/ M+ G+ g+ P& o# M' ]+ ~
加载数据段和初始化栈的参数
8 V: S3 G& x1 M* o8 K加载数据段和初始化栈的参数分别有4个,这里只讲解加载数据段的参数,至于初始化栈的参数类似。% i3 u# N; G I- ]' z
- 0x0800033c Flash上的数据段(初始化数据段和未初始化数据段)起始地址% d; {, h/ d7 W
- 0x20000000 加载到SRAM上的目的地址1 M3 S! J1 b2 {2 c
- 0x0000000c 数据段的总大小
b% T) M: r) Y& @: c @, J - 0x080002f4 调用函数_scatterload_copy
复制代码 需要说明的是初始化栈的函数-- 0x08000304与加载数据段的函数不一样,为 _scatterload_zeroinit,它的目的就是将栈空间清零。
5 s* M I* L" P/ v, \8 B1 _2 B' m+ n
^4 X4 k" l( V7 A
+ g6 ~+ ^# W# x9 n* _' \三、数据在SRAM上的结构
" N& x: [: H* m( w. @ D程序运行时(执行到main函数)时的SRAM数据结构/ `1 N/ I* @& _% y& d
4 k4 H$ V8 a8 u: K& y
6 C% J/ e6 ~/ O# i' j) S6 g+ T1 y( b# `; X- ?% f) c% ]# W- ~
四、详细过程分析
- D; b' i: n& D* e! P2 t有了以上的基础,现在详细分析启动过程。
* o l+ J: @; X/ l& X/ w7 U0 r9 W3 N! y& A B
1、上电后硬件设置SP、PC
3 q* t# s5 V# p刚上电复位后,硬件会自动根据向量表偏移地址找到向量表,向量表偏移地址的定义如下: ~9 l6 L* @3 K) K* p
; m0 R9 v2 L( }; P- c8 Q! z
4 K# B& x3 M( `
+ z3 L& t; L. c |1 s0 K# |0 d调试现象如下:4 P1 f) N1 N% m$ `) F! s
( H1 N T/ I6 l3 l+ V$ `
5 E3 r7 s! \6 \5 a, Q% N% v
: k( \+ t" Y0 N+ a0 Z0 `3 K看看我们的向量表内容(通过J-Flash打开hex文件)
4 R+ F+ h7 J/ G$ p- K J8 t* d# G" X+ N: L; {8 Q- d
. A; t$ x! f' u2 \' R
0 q" p- F6 A8 ^. J硬件这时自动从0x0800 0000位置处读取数据赋给栈指针SP,然后自动从0x0800 0004位置处读取数据赋给PC,完成复位,结果为:; g/ |$ ]" d8 }: L5 o( V5 x4 r' U4 h
) ^8 A5 N7 A7 R) I SP =0x02000810
8 `/ s+ @) x. [/ J PC =0x080001459 X2 F5 r* H. W5 r$ D% o2 Q
5 t3 H/ a) `* y: i2 u W
2、设置系统时钟/ h5 f+ b( n# p; ~( ]
上一步中令 PC=0x08000145的地址没有对齐,硬件自动对齐到 0x08000144,执行 SystemInit函数初始化系统时钟。
' O7 v5 E& E _- E9 U, o' a. V. O( p- ^0 U+ i: b! j; j/ X6 s: U
3、软件设置SP
: u% t0 `& U2 @9 W- m" |$ z+ P; \- M7 k
- LDR R0,=__main+ B! Y& b4 @4 X( V" r7 f& l
- BX R0
复制代码
, F& B8 x5 Q* [* G执行上两条之类,跳转到 __main程序段运行,注意不是main函数, ___main的地址是0x0800 0130。
- x* c _: r2 Q5 T! T; H1 j6 g' i
可以看到指令LDR.W sp,[pc,#12],结果SP=0x2000 0810。
8 p$ @- B! j/ x) G5 V
4 K# n7 [* `2 Q# L4、加载.data、.bss,并初始化栈区
2 q0 c; Z' t% Y% H% d
. J( g' {5 F. T; T) L# t$ c% y进入 __scatterload_rt2代码段。
9 ]+ r! F+ {" Y5 u% W' M# i- __scatterload_rt2:% v. Q/ ]$ x! R
- 0x080001684C06 LDR r4,[pc,#24] ; @0x08000184
$ g$ F4 Q! {4 P$ p! n; f - 0x0800016A4D07 LDR r5,[pc,#28] ; @0x08000188
" D* ?3 \! G: l k e3 Q+ m - 0x0800016CE006 B 0x0800017C
3 g8 [4 V1 I1 G' Q, v6 f - 0x0800016E68E0 LDR r0,[r4,#0x0C]
复制代码
% Q, p- p; z4 y y7 y- |这段代码是个循环 (BCC0x0800016e),实际运行时候循环了两次。第一次运行的时候,读取“加载数据段的函数 (_scatterload_copy)”的地址并跳转到该函数处运行(注意加载已初始化数据段和未初始化数据段用的是同一个函数);第二次运行的时候,读取“初始化栈的函数 (_scatterload_zeroinit)”的地址并跳转到该函数处运行。相应的代码如下:
9 o4 O& L0 k# v7 p6 v/ r7 b) Z+ G- 0x0800016E68E0 LDR r0,[r4,#0x0C]
7 K) {. p5 W) u( a7 e6 p% Y - 0x08000170 F0400301 ORR r3,r0,#' o/ U* k6 |9 s9 K) l
- 0x010x08000174
7 v- Z; k, \4 S* \# P- o - 0x080001784798 BLX r3
复制代码 + O* B L! D1 v1 I
, e$ S7 Q3 ?) R+ g- C# i
当然执行这两个函数的时候,还需要传入参数。至于参数,我们在“加载数据段和初始化栈的参数”环节已经阐述过了。当这两个函数都执行完后,结果就是“数据在SRAM上的结构”所展示的图。最后,也把事实加载和初始化的两个函数代码奉上如下:1 Y8 h2 h7 L$ v6 v
8 \5 E' h& X; ^% N! C1 _
9 q/ b3 l0 m" f% a: E% w
- __scatterload_copy:
K: a. z6 H9 b - 0x080002F4 E002 B 0x080002FC
2 @; Q& L+ K5 z i - 0x080002F6 C808 LDM r0!,{r3}
- g2 u h% T- u" H( I0 U; V - 0x080002F81F12 SUBS r2,r2,#4# ?8 ?3 {0 l% N& `# m, O
- 0x080002FA C108 STM r1!,{r3}( p$ ? O- E1 O! }; _! e; t
- 0x080002FC2A00 CMP r2,#0x00
: R( M+ }6 m E' E3 Z0 M9 ? - 0x080002FE D1FA BNE 0x080002F6
$ H; h. O4 s1 j! f$ C# ^ - 0x080003004770 BX lr
+ d5 M& y" m$ t( B' }' \ - __scatterload_null:
& V2 W9 d/ B7 B5 R; \3 B - 0x080003024770 BX lr
% N# ?$ q c$ F% v; p/ V' k% p) A - __scatterload_zeroinit:9 S8 P* e4 v" `0 e' Z
- 0x080003042000 MOVS r0,#0x00* h, |8 V; i% t- z! E* x
- 0x08000306 E001 B 0x0800030C0 B+ I% }# m* x- {+ I* d9 l
- 0x08000308 C101 STM r1!,{r0}
' n) e k7 |: _9 A0 \* x1 w" v/ F - 0x0800030A1F12 SUBS r2,r2,#4) D2 O/ S) j9 Y+ |/ `5 X
- 0x0800030C2A00 CMP r2,#0x00& ~2 I( N# d/ b% U1 Q
- 0x0800030E D1FB BNE 0x08000308
" r; ?/ C, W5 E" ? V @. P - 0x080003104770 BX lr
复制代码 & d6 K) D0 k/ S0 W7 z4 ]
五、异常向量与中断向量表
2 s# ^' n- o' f3 Z( r. |, L
' }6 u' \8 s+ o6 b$ _/ [
0 N$ ^6 }2 U/ S" |- J4 P1 n* h
+ r! f! f" ]2 Q4 N
5 m- y- k- T/ ?( E0 o5 w) ~
这段代码就是定义异常向量表,在之前有一个“J-Flash打开hex文件”的图片跟这个表格是一一对应的。编译器根据我们定义的函数 Reset_Handler、NMI_Handler等,在连接程序阶段将这个向量表填入这些函数的地址。
5 O6 j) z7 X, W! }, }+ ]4 C
! O" L+ Z& _- P. d3 g2 W- startup_stm32f10x_hd.s内容:/ v, G: h1 ?+ x
- N MI_Handler PROC
+ x3 ?" [ Z, c% x* [ - EXPORT NMI_Handler [WEAK]: ?& j" K) e D @
- B .
$ L) a' O* a: F - ENDP
复制代码
3 @8 U6 E C X: s在启动汇编文件中已经定义了函数 NMI_Handler,但是使用了“弱”,它允许我们再重新定义一个 NMI_Handler函数,程序在编译的时候会将汇编文件中的弱函数“覆盖掉”--两个函数的代码在连接后都存在,只是在中断向量表中的地址填入的是我们重新定义函数的地址。/ v$ A0 p5 y9 \0 U: r' B. T
4 b1 y( X1 E. C0 T6 D, s六、使用微库与不使用微库的区别/ K7 @' e4 z# W, B1 X8 X; R
. T' V& p( R q1 N( l' x# g4 j
, s M ]6 V$ g
: |) X# l4 b# F- {3 a* w, W2 @使用微库就意味着我们不想使用MDK提供的库函数,而想用自己定义的库函数,比如说printf函数。那么这一点是怎样实现的呢?我们以printf函数为例进行说明。7 r, ]4 V8 ~9 ?
$ O) j! N; v) D. B o, n: _
1、不使用微库而使用系统库
, ^) @7 U% x0 ?" {在连接程序时,肯定会把系统中包含printf函数的库拿来调用参与连接,即代码段有系统库的参与。
% R; r$ d7 |2 Q o+ l. f
% u; j4 r" E" H, v" D0 h在启动过程中,不使用微库而使用系统库在初始化栈的时候,还需要初始化堆(猜测系统库需要用到堆),而使用微库则是不需要的。
" O' h: w( n8 t0 W9 x- IF :DEF:__MICROLIB
0 }9 d- q' J8 H! P8 M4 t7 f - 9 _( Y' ? U8 q0 r$ a6 Q& q5 f$ d
- EXPORT __initial_sp. D4 N. Y, N0 w
- # y: f9 a# Z9 M' B5 a" y6 t
- EXPORT __heap_base
! b o: Q6 D+ ^( y S, o3 D - . t8 J% T7 ]: x5 V: G. c
- EXPORT __heap_limit6 {: j# w$ M, b! U0 g+ ]
. D! v t/ F- u- ELSE
; j! t1 p1 W8 B5 Q, I' j - , L1 l6 J/ Z0 O$ l
- IMPORT __use_two_region_memory
1 Z* |5 C* m: I N6 h! c. t
& U: f" C6 z" p. }- EXPORT __user_initial_stackheap
1 S6 A, f! M: j8 f! y' }9 o
) j% X. |9 R8 n7 V- __user_initial_stackheap
3 X2 M; y- S1 t7 ?5 t) ]( { ^2 T
. s( ~' Z0 F1 k: ^- LDR R0, =Heap_Mem* P2 `: W) [$ P/ |
- LDR R1, =(Stack_Mem+Stack_Size); T2 s1 R: x$ \3 ~! ^2 Z
- LDR R2, = (Heap_Mem+Heap_Size). Y9 T1 S% a- B0 d6 u
- LDR R3, =Stack_Mem+ P0 ^ W2 S! @1 [2 V; F8 i
- BX LR# r* T- M6 s- U5 R* b
- ( R2 o. Q) s( M( z+ V4 F5 j
- ALIGN- I$ W8 \: A; ^
* m/ Z1 m# }( c5 i2 c: ?$ p- ENDIF
复制代码 9 M; I* G0 M9 O9 Q/ P. l6 J9 N2 g
另外,在执行 __main函数的过程中,不仅需要完成“使用微库”情况下的所有工作,额外的工作还需要进行库的初始化,才能使用系统库(这一部分我还没有深入探讨)。附上 __main函数的内容:
, b6 a& t2 \( Q; o. q, N- Y
+ _2 R" B4 X4 s! K) \% b
# Y0 y7 B' m. g: S2 C
/ _: X/ y/ P0 A: C
' _2 K; _8 q E2 l2 L( i! F, q+ P- j$ K7 J+ m9 ` j8 }
2、使用微库而不使用系统库; T& a- s& @- j8 a2 s6 R
在程序连接时,不会把包含printf函数的库连接到终极目标文件中,而使用我们定义的库。
$ g2 \0 W0 S* O' u9 d/ T+ k! X; A1 j3 s# N ?6 V( k4 L5 Y7 [; t% r& b9 n8 H
启动时需要完成的工作就是之前论述的步骤1、2、3、4、5,相比使用系统库,启动过程步骤更少。- Z- p0 c7 X/ P5 X4 Z/ [# P
2 c6 M3 ~+ q7 {$ `% c7 e转载自:嵌入式电子
# `' D% r9 x, u/ j" R如有侵权请联系删除( [: U5 h4 W9 |) c: j
+ `$ v3 D2 ~3 Z- y8 I
: w8 y; K% }: ^3 Q# w5 k4 |( C
$ O+ U) u" S9 p* S, A5 i |