一、概述
! a& G5 V* v" J1、说明每一款芯片的启动文件都值得去研究,因为它可是你的程序跑的最初一段路,不可以不知道。通过了解启动文件,我们可以体会到处理器的架构、指令集、中断向量安排等内容,是非常值得玩味的。 8 n) g4 l8 ^- A, U1 S3 b4 m
STM32作为一款高端 Cortex-M3系列单片机,有必要了解它的启动文件。打好基础,为以后优化程序,写出高质量的代码最准备。 本文以一个实际测试代码--START_TEST为例进行阐述。 5 V) J" V( C) s
2、整体过程概括STM32整个启动过程是指从上电开始,一直到运行到 main函数之间的这段过程,步骤为(以使用微库为例): ①上电后硬件设置SP、PC ②设置系统时钟 ③软件设置SP ④加载.data、.bss,并初始化栈区 ⑤跳转到C文件的main函数
$ z5 [' w* \) x8 o3 R, g 3、整个启动过程涉及的代码推荐阅读启动过程涉及的文件不仅包含 startup_stm32f10x_hd.s,还涉及到了MDK自带的连接库文件 entry.o、entry2.o、entry5.o、entry7.o等(从生成的 map文件可以看出来)。' N0 R* j+ F. p& [, x; R! h
$ v( K; S& Z* r/ ]; [二、程序在Flash上的存储结构
) _& Z {% k, F* u/ z+ v在真正讲解启动过程之前,先要讲解程序下载到 Flash上的结构和程序运行时(执行到main函数)时的SRAM数据结构。程序在用户Flash上的结构如下图所示。下图是通过阅读hex文件和在MDK下调试综合提炼出来的。. _1 y* L/ q" ]4 e
v+ }7 Z* B8 B# z2 `
+ r- T7 v9 d( c* D/ C% U
# }; Z9 d# B, b: \8 w3 _
MSP初始值 编译器生成,主堆栈的初始值5 v1 A, ^2 }# b3 u! K3 {
异常向量表 不多说
( m, g8 n" M, r. s 外部中断向量表 不多说! {" T7 b% `" a$ Q( T* A
代码段 存放代码
9 m, W* b. P; r 初始化数据段 .! ?& {$ @) x2 X3 a; O% L7 v
data
' [. R1 D, o! r( U2 q' ^! N1 T 未初始化数据段 .bss$ k$ E( x! S% t7 F6 K& x s
! X0 f3 t0 {3 ^; f2 v; F4 ^
加载数据段和初始化栈的参数
8 J) ] @& I$ w5 ?加载数据段和初始化栈的参数分别有4个,这里只讲解加载数据段的参数,至于初始化栈的参数类似。
; \4 {: E0 O$ j! p- 0x0800033c Flash上的数据段(初始化数据段和未初始化数据段)起始地址
3 N& i1 t, R6 M - 0x20000000 加载到SRAM上的目的地址1 j# F. |* m! R T
- 0x0000000c 数据段的总大小
* m# n0 R; m* C - 0x080002f4 调用函数_scatterload_copy
复制代码 需要说明的是初始化栈的函数-- 0x08000304与加载数据段的函数不一样,为 _scatterload_zeroinit,它的目的就是将栈空间清零。
* y1 Z0 T# V, m4 ^# P
1 x8 F: \( M6 B" W# i& E
, _6 y' }: [( J' ]3 Z
! G* t; E; U6 @- t# O" g三、数据在SRAM上的结构
$ N B4 G# O- B) _- l程序运行时(执行到main函数)时的SRAM数据结构3 Q3 e, y6 Q6 }8 p
: N4 e/ G. [7 C3 }
0 c! r$ X& R6 ~4 | o
' F7 L7 m0 R& s& V* d4 H四、详细过程分析2 r! n) \* m: f& |
有了以上的基础,现在详细分析启动过程。" ]' h, u# |$ T9 ^% d- Z
@. g9 |6 t: b8 _9 i3 O
1、上电后硬件设置SP、PC
/ t& b1 k6 Y0 ] m! b& e刚上电复位后,硬件会自动根据向量表偏移地址找到向量表,向量表偏移地址的定义如下:
1 k% [/ ?( n) k" R5 ?8 R, }4 o( l$ G1 l- k8 K
% O+ b& d5 @# H4 t
, U7 i a4 {) i8 W& t7 O# ?, a调试现象如下:2 d% W6 _1 U/ }3 m- I$ U% p4 @* \+ O
9 j h: x( u8 c+ U0 @# |" D
9 e. K0 ^) ?) {, q* H1 P4 F: f$ `7 k
G4 Z U+ y2 y% X$ a看看我们的向量表内容(通过J-Flash打开hex文件)
' e E5 G% U$ A; u7 V# J/ t7 H5 k3 ~$ N0 I2 m
; d2 j! q. f* U* ], G4 u" g' E8 L
7 }1 D! Y1 N# ^! o8 a硬件这时自动从0x0800 0000位置处读取数据赋给栈指针SP,然后自动从0x0800 0004位置处读取数据赋给PC,完成复位,结果为:
7 d! n( E% Q3 e6 ^9 E" |* }9 ]7 k+ C7 L* S
SP =0x02000810
7 e3 l) Z, U* V1 \+ s, B PC =0x08000145
9 V: w! R) z$ I6 \( g$ @/ q2 {; r2 W Q0 ?4 ]: G
2、设置系统时钟# Y( l3 r: X1 ^0 r. R9 S* p$ s
上一步中令 PC=0x08000145的地址没有对齐,硬件自动对齐到 0x08000144,执行 SystemInit函数初始化系统时钟。
. o9 D7 e9 I" S, N, q. o! K
' ^9 \/ y- |1 ?# e! H! ~3、软件设置SP
" |; G0 j9 d1 [6 K& l3 s( Q$ }' h& m0 d+ Q+ k
- LDR R0,=__main
+ r6 i- M* @( H7 K! b) j% u - BX R0
复制代码 3 x$ q( s j; S" F, @" e8 [
执行上两条之类,跳转到 __main程序段运行,注意不是main函数, ___main的地址是0x0800 0130。2 @9 s1 p1 ^ j* E2 g
: V/ P% I3 D2 K; a4 h
可以看到指令LDR.W sp,[pc,#12],结果SP=0x2000 0810。
* N" i/ g2 q0 P) [0 _) N
8 e$ t5 {% }! }4、加载.data、.bss,并初始化栈区
7 O( R6 {, B) t# }
+ v3 N, Y) S, H6 ~2 e$ n/ A4 }进入 __scatterload_rt2代码段。" ?! m; J& e# ~8 A# k9 S* H
- __scatterload_rt2:. C( J1 A% C# O% m- c
- 0x080001684C06 LDR r4,[pc,#24] ; @0x08000184
! s( d# K9 c7 b r* f6 _) ] - 0x0800016A4D07 LDR r5,[pc,#28] ; @0x08000188
; P) J" h& y: o* e' N3 |2 \ - 0x0800016CE006 B 0x0800017C" @# R2 L* D8 x9 X( b
- 0x0800016E68E0 LDR r0,[r4,#0x0C]
复制代码
5 W- [- F5 K( @5 p; B这段代码是个循环 (BCC0x0800016e),实际运行时候循环了两次。第一次运行的时候,读取“加载数据段的函数 (_scatterload_copy)”的地址并跳转到该函数处运行(注意加载已初始化数据段和未初始化数据段用的是同一个函数);第二次运行的时候,读取“初始化栈的函数 (_scatterload_zeroinit)”的地址并跳转到该函数处运行。相应的代码如下:
3 o1 Z* `4 \& B& Z- 0x0800016E68E0 LDR r0,[r4,#0x0C]7 H$ K1 q, e1 f2 l" I
- 0x08000170 F0400301 ORR r3,r0,# `/ _# Y8 r& t3 g" d5 n
- 0x010x080001742 [- R u) t5 e$ Z2 \
- 0x080001784798 BLX r3
复制代码 Y1 G+ n! ]$ O: B8 D5 _; V
/ |3 C+ I% F9 h0 K3 s+ O
当然执行这两个函数的时候,还需要传入参数。至于参数,我们在“加载数据段和初始化栈的参数”环节已经阐述过了。当这两个函数都执行完后,结果就是“数据在SRAM上的结构”所展示的图。最后,也把事实加载和初始化的两个函数代码奉上如下:
' r4 @) C! y+ {4 L5 k% l
5 v, r; y5 a9 |7 F* Q; J1 N3 X, f9 {& s& H# p9 ~% p! @. V% K/ X! k
- __scatterload_copy:
2 H! \6 |# }! b- n6 d7 o( a - 0x080002F4 E002 B 0x080002FC1 D" ^, G: Q$ V, L1 A b
- 0x080002F6 C808 LDM r0!,{r3}9 c+ i1 I1 ?7 \# Q
- 0x080002F81F12 SUBS r2,r2,#4
& Q* d* P, C Q3 Z$ G" |5 p% p" y - 0x080002FA C108 STM r1!,{r3}& ^- F& P1 r: W7 n1 ]% _+ |( S9 g
- 0x080002FC2A00 CMP r2,#0x00
) F* x, @2 d& V' Z9 F - 0x080002FE D1FA BNE 0x080002F6- u7 c3 G8 e9 x$ {( y. d
- 0x080003004770 BX lr
! v, s8 |4 g- n9 \ - __scatterload_null:0 N: H* J' `: o) \
- 0x080003024770 BX lr
% s' h) } {" E - __scatterload_zeroinit:; t* B7 b9 ]* }8 q2 L
- 0x080003042000 MOVS r0,#0x00 B1 |8 q% [4 j S) H8 } o2 U2 O
- 0x08000306 E001 B 0x0800030C" k2 U$ W" c9 \$ B9 S) T+ v& o
- 0x08000308 C101 STM r1!,{r0}& z1 ^- }+ E* ^, M6 c
- 0x0800030A1F12 SUBS r2,r2,#4
2 m: @' B$ [9 L6 Z7 {2 y - 0x0800030C2A00 CMP r2,#0x00# i# y# l4 H* @ E7 b' ]% b( N
- 0x0800030E D1FB BNE 0x08000308$ q; J* ~( {. R4 k% N0 q& D
- 0x080003104770 BX lr
复制代码
% R* Y2 \" X/ l& h; `4 D五、异常向量与中断向量表: l1 X: [$ o1 j4 }! ]- L
6 O( i6 f) ^) E
) e3 o! B4 x! ?, P% e( `
+ a3 v, `6 k; L$ z! z
: k8 h$ m" @0 t5 ?# z# D这段代码就是定义异常向量表,在之前有一个“J-Flash打开hex文件”的图片跟这个表格是一一对应的。编译器根据我们定义的函数 Reset_Handler、NMI_Handler等,在连接程序阶段将这个向量表填入这些函数的地址。
! l% I8 B+ @6 G0 \& V, f
% s+ C" a0 V( e* ^+ O$ g- startup_stm32f10x_hd.s内容:0 I# x$ `2 X( _: x
- N MI_Handler PROC% c3 x% U) t* o1 v3 @9 X) G
- EXPORT NMI_Handler [WEAK]( p9 m, P$ d- h7 Q( F
- B ., }* C; o4 E) P& `: z) ?
- ENDP
复制代码
8 k* Y$ s2 @9 V9 W0 Y( f* [+ x在启动汇编文件中已经定义了函数 NMI_Handler,但是使用了“弱”,它允许我们再重新定义一个 NMI_Handler函数,程序在编译的时候会将汇编文件中的弱函数“覆盖掉”--两个函数的代码在连接后都存在,只是在中断向量表中的地址填入的是我们重新定义函数的地址。
& ~3 {6 \! `( E! |# b
0 q e/ D" ~+ e, Z! _六、使用微库与不使用微库的区别
3 P1 K4 ?+ k3 X8 b" S! k1 ?- a! Y3 H2 i3 _! Y
# l$ V5 s, c9 K7 |
$ j4 G# p7 ~3 c @$ M; T9 s使用微库就意味着我们不想使用MDK提供的库函数,而想用自己定义的库函数,比如说printf函数。那么这一点是怎样实现的呢?我们以printf函数为例进行说明。( E1 ~, ]4 `0 m& n, O5 q# _1 I
( ^8 R Y. r6 a+ Q$ D" V2 Q
1、不使用微库而使用系统库' j( v3 |8 B. X4 S2 Q. g6 y
在连接程序时,肯定会把系统中包含printf函数的库拿来调用参与连接,即代码段有系统库的参与。& k8 l; Q' V* B% ?, _% O4 p
( J% P; T( X( |3 ~: d
在启动过程中,不使用微库而使用系统库在初始化栈的时候,还需要初始化堆(猜测系统库需要用到堆),而使用微库则是不需要的。
v$ i. W" R6 q: H% e6 _/ E8 _- IF :DEF:__MICROLIB
9 @" t- L5 j" E$ \% S; G - 9 P& w4 }. I8 `( `+ W
- EXPORT __initial_sp
- X6 {' f- h; ]2 R9 j+ Q# l5 E - , @# T1 o) |: K# Z3 u! W3 p4 F; @
- EXPORT __heap_base
* |% r; ]' P- S1 a. h& Y; i$ ]: H
+ ] J, o: u# A/ o( E& K- EXPORT __heap_limit. I# E1 o, N, r
. `, y! g- d/ g0 d: [" x- ELSE
6 v$ Q( Y& Z7 R, ?0 s* I9 V1 S
9 e, V+ F/ S& O. I; s- IMPORT __use_two_region_memory5 S" E( X8 X$ M% n' p0 g+ l/ a
- 7 L9 x+ L1 C, [1 V+ _+ h, o
- EXPORT __user_initial_stackheap# }( ]( {+ z! A' u$ k( ]! J
- - k9 V% b9 Z4 d: B3 c
- __user_initial_stackheap) F. ~9 d8 R" f4 T# d/ o
- 0 V8 S9 W& a/ k7 J* A9 m
- LDR R0, =Heap_Mem+ N% x: M4 V6 u+ x. V) Y! u
- LDR R1, =(Stack_Mem+Stack_Size)) _6 s- ?, a4 v. x* K) @) q
- LDR R2, = (Heap_Mem+Heap_Size)9 m. B6 h8 R" J+ R' Z d, B
- LDR R3, =Stack_Mem& Q9 Z6 v$ T2 }0 G, K- j
- BX LR
) B& _, z0 B! ?$ q( _
5 M+ t$ }3 `: C; T- ALIGN/ L- K# {1 G/ x4 H1 R7 z3 n
- 6 n! y% j: L5 Y: c7 L/ }1 t
- ENDIF
复制代码
* a. K* b$ S4 }' ~另外,在执行 __main函数的过程中,不仅需要完成“使用微库”情况下的所有工作,额外的工作还需要进行库的初始化,才能使用系统库(这一部分我还没有深入探讨)。附上 __main函数的内容:
' v/ ]7 s0 U2 h6 {- C6 k
2 v6 Q* ^6 ]; b
* Z) s+ A( r$ E, s
, N2 O. ]: t! H+ C% }2 }7 B5 n3 ]
' ~5 G: J1 S1 T, d' e' E1 H6 k& K; B! }6 c- ]5 W. h1 g% }
2、使用微库而不使用系统库; R6 Z* ?* s6 _5 M+ k3 w
在程序连接时,不会把包含printf函数的库连接到终极目标文件中,而使用我们定义的库。
5 @* G% L2 P( A$ o% h, U9 d5 b; N) i# n) I4 S
启动时需要完成的工作就是之前论述的步骤1、2、3、4、5,相比使用系统库,启动过程步骤更少。
9 j* C; X, f; e+ \9 D1 @3 S2 z. M* g/ c3 g! q. Z0 R, T- Z& Y
转载自:嵌入式电子3 P6 L f6 D& e0 \& _
如有侵权请联系删除5 M) [! V% g& f; b- w+ F* C
+ I" d6 I* a d- t4 _, E& V1 C# S' v) ~# u$ F5 q+ ^
; r$ G/ g x5 V* O& |/ J |