一、概述
; o# O, o4 x8 D: W/ e* W& B1、说明每一款芯片的启动文件都值得去研究,因为它可是你的程序跑的最初一段路,不可以不知道。通过了解启动文件,我们可以体会到处理器的架构、指令集、中断向量安排等内容,是非常值得玩味的。 / `' e$ {( G3 @
STM32作为一款高端 Cortex-M3系列单片机,有必要了解它的启动文件。打好基础,为以后优化程序,写出高质量的代码最准备。 本文以一个实际测试代码--START_TEST为例进行阐述。
, g+ Q5 B) K6 e. j7 ?( b 2、整体过程概括STM32整个启动过程是指从上电开始,一直到运行到 main函数之间的这段过程,步骤为(以使用微库为例): ①上电后硬件设置SP、PC ②设置系统时钟 ③软件设置SP ④加载.data、.bss,并初始化栈区 ⑤跳转到C文件的main函数 3 A& F6 B# l' E4 N. u5 W
3、整个启动过程涉及的代码推荐阅读启动过程涉及的文件不仅包含 startup_stm32f10x_hd.s,还涉及到了MDK自带的连接库文件 entry.o、entry2.o、entry5.o、entry7.o等(从生成的 map文件可以看出来)。
, g$ t$ ]# v4 l
" a) m$ U- `& V8 d- M4 r3 }二、程序在Flash上的存储结构5 C1 U: m* e7 v& h c/ N; [
在真正讲解启动过程之前,先要讲解程序下载到 Flash上的结构和程序运行时(执行到main函数)时的SRAM数据结构。程序在用户Flash上的结构如下图所示。下图是通过阅读hex文件和在MDK下调试综合提炼出来的。
0 _4 y, _# \7 n. x7 U c! Z n( p% u" G$ `
- o* T) I" e/ i; ]' }- g/ S
5 A; B n& W: O MSP初始值 编译器生成,主堆栈的初始值3 W H3 g- X. y. a
异常向量表 不多说3 y' ?0 p$ g7 q6 w3 {/ R; H8 |+ L
外部中断向量表 不多说
) B9 L3 m8 \3 c2 O" i3 ?, V8 s 代码段 存放代码
/ E: y8 U7 D3 i" o 初始化数据段 .: U& i4 r6 u/ B$ i* O/ K
data
6 ?7 j+ E' T0 x7 b, m7 t: D 未初始化数据段 .bss
9 D. g) p# B6 o; @0 [2 _7 V5 x* {4 r0 {! r! ] O
加载数据段和初始化栈的参数
7 W6 D# Z7 O2 r' p8 H. l: M) ?加载数据段和初始化栈的参数分别有4个,这里只讲解加载数据段的参数,至于初始化栈的参数类似。
0 H4 m/ K+ V* p7 Y+ o* e$ c9 v- 0x0800033c Flash上的数据段(初始化数据段和未初始化数据段)起始地址 \% s/ J( M# `6 x- N
- 0x20000000 加载到SRAM上的目的地址
# t; a% j+ p" g* u1 E; u6 d' d - 0x0000000c 数据段的总大小5 N9 c1 G1 `3 X7 c9 o
- 0x080002f4 调用函数_scatterload_copy
复制代码 需要说明的是初始化栈的函数-- 0x08000304与加载数据段的函数不一样,为 _scatterload_zeroinit,它的目的就是将栈空间清零。0 u$ M, ^7 f |
$ B Z1 _# r+ I2 x; W
: t% }, \0 H& d. p1 w& ^9 z2 Q$ }! D% C/ X. B' O" q
三、数据在SRAM上的结构: I, C4 x9 B. }% c2 u. d
程序运行时(执行到main函数)时的SRAM数据结构! q$ L% [% n4 L. a
: D" ^% O; b1 T" Y! r) g) i1 I
' y& I* J, K+ S& C! m! q- ?. \
& o& L( Z+ r6 S \% p# ]: ^四、详细过程分析# h! w% T G+ J7 X$ U9 G
有了以上的基础,现在详细分析启动过程。" s2 h! k+ t7 z/ s8 U! s7 e% i
4 z+ _9 X( M4 d( X
1、上电后硬件设置SP、PC
& `4 Q% d7 e) B刚上电复位后,硬件会自动根据向量表偏移地址找到向量表,向量表偏移地址的定义如下:
; C8 r( a* H, T. a( ~! s1 G8 M8 Y* p. t& n6 Y
% s2 u- R0 n4 b$ Q g! s
. u$ |1 k( ?8 Q' A( `
调试现象如下:- H& B! h- w# ?; r0 g) e
7 b, l" u+ ^! S) {; I8 v. P! R0 w
4 Y8 }, X9 b$ U) z
4 r& R& Z6 N5 Z看看我们的向量表内容(通过J-Flash打开hex文件)
. E9 Z+ P% A) ~
- ~$ K) T3 t! y& w
* D3 k5 v; ^# R$ v. P" L
% R/ y& B+ {0 H! x4 Y硬件这时自动从0x0800 0000位置处读取数据赋给栈指针SP,然后自动从0x0800 0004位置处读取数据赋给PC,完成复位,结果为:
$ Y8 S- x* L u# u' N1 P+ j' ]; U% [
SP =0x02000810
$ X, s. }+ ]' z( E+ V4 r PC =0x08000145
! }) w c3 f7 X7 I) {+ q8 P( V) L- ]7 [7 y* Z7 n4 l
2、设置系统时钟* @4 B4 O& A! ^+ X
上一步中令 PC=0x08000145的地址没有对齐,硬件自动对齐到 0x08000144,执行 SystemInit函数初始化系统时钟。
, P+ k, u1 H/ I8 N8 Y4 s" W- o$ C
8 M* h) Z. V( |0 p* R3、软件设置SP
( |2 U L3 F% n) l2 z( Q
" R# {- T; Q" q3 N3 ^- LDR R0,=__main9 L# T/ C4 A) r9 s( P- E
- BX R0
复制代码
) {* a- R+ G# V* a5 R% f% g执行上两条之类,跳转到 __main程序段运行,注意不是main函数, ___main的地址是0x0800 0130。3 `, { t9 _" `! Y+ [ G, h; w9 R" F* v
& Y. @4 q' ^! w! W' q
可以看到指令LDR.W sp,[pc,#12],结果SP=0x2000 0810。3 T* l* ~+ C( b, A
& L2 |& T+ c! [1 o; f3 U" Y: ^
4、加载.data、.bss,并初始化栈区2 m; \; Z, A$ Y" ]' |( U+ f
! G3 ~6 |3 i y$ @, i( t# k( X* X进入 __scatterload_rt2代码段。0 |+ `" G, [* k9 M T
- __scatterload_rt2:
& u- F" \# }* Z) n/ d - 0x080001684C06 LDR r4,[pc,#24] ; @0x08000184
( A) k, q; J3 H# [) W - 0x0800016A4D07 LDR r5,[pc,#28] ; @0x08000188
" {; n" W6 L- h' g# i - 0x0800016CE006 B 0x0800017C7 o& f# `2 p) b3 E& [" v: [
- 0x0800016E68E0 LDR r0,[r4,#0x0C]
复制代码
; U) a* ^' }# Y: @这段代码是个循环 (BCC0x0800016e),实际运行时候循环了两次。第一次运行的时候,读取“加载数据段的函数 (_scatterload_copy)”的地址并跳转到该函数处运行(注意加载已初始化数据段和未初始化数据段用的是同一个函数);第二次运行的时候,读取“初始化栈的函数 (_scatterload_zeroinit)”的地址并跳转到该函数处运行。相应的代码如下:
$ m% J0 E ^0 y; J- 0x0800016E68E0 LDR r0,[r4,#0x0C]
- S1 ]6 `, Q- A$ {% F& L- D, Y - 0x08000170 F0400301 ORR r3,r0,#
* L) ?3 S2 \4 `8 F - 0x010x08000174
0 o* R3 n: w) ?% Y. z; x% _, C7 H - 0x080001784798 BLX r3
复制代码 : `# G8 E5 z* G! C4 r
" |' K$ K7 f0 D2 Q当然执行这两个函数的时候,还需要传入参数。至于参数,我们在“加载数据段和初始化栈的参数”环节已经阐述过了。当这两个函数都执行完后,结果就是“数据在SRAM上的结构”所展示的图。最后,也把事实加载和初始化的两个函数代码奉上如下:
; u( ~/ t/ H+ A q e' b& J
& d c9 q/ T* C8 P- ?# B+ U9 E4 V5 `6 k. b6 @
- __scatterload_copy:
6 V$ C5 J) M& k( ^ - 0x080002F4 E002 B 0x080002FC' c; N7 e8 |5 ]5 q4 K) O- Z, t2 h
- 0x080002F6 C808 LDM r0!,{r3}+ t7 U1 O& s: y3 U& G: n/ O0 y4 I* Y
- 0x080002F81F12 SUBS r2,r2,#44 ~" J+ m) F( p. x: `
- 0x080002FA C108 STM r1!,{r3}
7 ?0 M( b5 ~& `9 N# _! w+ q - 0x080002FC2A00 CMP r2,#0x00' L* j! ~, g' z" z8 u* w
- 0x080002FE D1FA BNE 0x080002F6
% X6 n2 L) h( j# {1 V- S- X - 0x080003004770 BX lr
) ^: Q# Q. b, | - __scatterload_null:
* k5 G3 x* X- \& s$ a: k - 0x080003024770 BX lr8 g/ k. @+ n) k/ a7 b
- __scatterload_zeroinit:
9 k9 g( e' ~, x4 P - 0x080003042000 MOVS r0,#0x009 |% g6 e; k, y# w3 i
- 0x08000306 E001 B 0x0800030C" R* K* N, l5 t5 [; `3 t. [
- 0x08000308 C101 STM r1!,{r0}$ z! ?! J( i# x; ~
- 0x0800030A1F12 SUBS r2,r2,#4
# g3 n/ V' c: N s. S - 0x0800030C2A00 CMP r2,#0x009 _2 n B2 D; u) d! C) A5 ?
- 0x0800030E D1FB BNE 0x08000308$ h2 S5 H# r4 x( Y7 Z/ d
- 0x080003104770 BX lr
复制代码
6 @7 Z' Y& e& U3 |. G# s& b$ m+ ?7 B五、异常向量与中断向量表
2 o d# @1 J5 N: N5 L- p, x8 J: u$ r e0 Q! l( ]: V" ?
, }" X F& V7 ]4 g; I
: g% @) O6 E# N( c+ F' F1 |3 a* m) u+ u% {
这段代码就是定义异常向量表,在之前有一个“J-Flash打开hex文件”的图片跟这个表格是一一对应的。编译器根据我们定义的函数 Reset_Handler、NMI_Handler等,在连接程序阶段将这个向量表填入这些函数的地址。
P# N. F h$ G7 m& k; `% _
0 u; ], c9 _ M) ~" E3 Q- startup_stm32f10x_hd.s内容:2 h+ v' S9 v& j# [
- N MI_Handler PROC
% H5 u" Y+ g c2 Q: p8 g; G - EXPORT NMI_Handler [WEAK]' o8 ~1 X' S2 x, ~0 n
- B .
5 o7 V& w. X1 c - ENDP
复制代码
8 c) @+ n* h0 ?* B7 S1 i在启动汇编文件中已经定义了函数 NMI_Handler,但是使用了“弱”,它允许我们再重新定义一个 NMI_Handler函数,程序在编译的时候会将汇编文件中的弱函数“覆盖掉”--两个函数的代码在连接后都存在,只是在中断向量表中的地址填入的是我们重新定义函数的地址。9 Z1 X6 E4 g- |/ P. C1 B9 i4 R
8 ]+ l2 Q: _; J六、使用微库与不使用微库的区别
6 _* L# v0 b$ M# ?" G. f9 _
& t3 I0 R# Y5 s3 B
0 ^6 }- [. N2 X5 E; e; b% ~: d, L" B) U( |8 A3 J7 ^
使用微库就意味着我们不想使用MDK提供的库函数,而想用自己定义的库函数,比如说printf函数。那么这一点是怎样实现的呢?我们以printf函数为例进行说明。
( i- b. [/ k) i$ X- }3 N! ~8 y _ H4 }& q. d$ ?0 b- Y
1、不使用微库而使用系统库
1 b& r4 F8 F/ S2 m: W: ]; B$ L在连接程序时,肯定会把系统中包含printf函数的库拿来调用参与连接,即代码段有系统库的参与。
7 p3 D. c6 w0 L6 F) V% j3 s. Z: P1 |+ V) { s8 R F. |) {
在启动过程中,不使用微库而使用系统库在初始化栈的时候,还需要初始化堆(猜测系统库需要用到堆),而使用微库则是不需要的。9 o2 Y* ^5 Z% G0 A. B/ C6 ~
- IF :DEF:__MICROLIB0 g2 U+ S3 v) G$ y1 B2 a$ x
- " m% }2 H9 l: T F5 i1 r. p/ p( @" D4 L
- EXPORT __initial_sp, B& w5 ~; u6 u! s o5 Z4 r
9 D: S# P; j* u) g! i8 d- EXPORT __heap_base7 _3 p' H; z U: K( @
2 |* y( d3 W& x& C0 w: c- {$ K- EXPORT __heap_limit
/ q6 X- ~1 v% z" _0 U2 o - ; S% P0 k4 j4 d4 v
- ELSE! M5 _4 f. a: y9 ?' U, H
3 E1 p4 d0 S5 k/ G' f0 G7 O3 ~- IMPORT __use_two_region_memory
- J; W' ~; N) f, R
; E+ E" T) M7 u3 f0 E! O! f- EXPORT __user_initial_stackheap; e/ L8 Q0 {6 ?8 j; l3 p
- $ O5 H( W) m0 D/ C
- __user_initial_stackheap
& p7 ? L; t$ ~" |
9 n( P6 V @9 I$ Q& R. Y- LDR R0, =Heap_Mem8 _* Y, l0 E( H5 m3 ]; p
- LDR R1, =(Stack_Mem+Stack_Size)
( g/ o" ~5 b0 N$ x: P' _ - LDR R2, = (Heap_Mem+Heap_Size)
! r f! ?6 m" I: a7 O! i - LDR R3, =Stack_Mem3 n- d: R9 @7 O: M7 f# K
- BX LR
6 u7 Q( s+ y- S [
! o# F/ \- j* ^" G- ALIGN0 k7 X( V* v% V5 S) h! G! {
- ; q7 w# ~& x1 k B/ H$ c9 j
- ENDIF
复制代码
f1 P" U! z+ z! E* x& Z另外,在执行 __main函数的过程中,不仅需要完成“使用微库”情况下的所有工作,额外的工作还需要进行库的初始化,才能使用系统库(这一部分我还没有深入探讨)。附上 __main函数的内容:
0 [ v+ e' e: D4 W1 q H9 b' i6 d2 S
" b$ b0 J' U& W+ D- ]
' Y) T8 V2 N, }- @! Z$ S
8 N; A9 B$ W) Y! _- s' a
) N( Q" L6 b2 ^ R' u
2、使用微库而不使用系统库
8 P/ E) ]& w$ Y+ _" p @在程序连接时,不会把包含printf函数的库连接到终极目标文件中,而使用我们定义的库。$ d- @! M1 [( Z! N4 y( h
+ I1 U) u" }0 b- G3 n9 L' z
启动时需要完成的工作就是之前论述的步骤1、2、3、4、5,相比使用系统库,启动过程步骤更少。. d" X' x! G1 ]) e3 Q9 i6 C) C- k
7 k/ }0 \4 B1 _; U2 B5 b
转载自:嵌入式电子 ] s* _2 V8 X4 B9 U' {" ^6 {4 V
如有侵权请联系删除
; i3 i/ M/ ?, C5 c# x6 { j! W4 h3 I5 x6 T
2 ], @; S' N% g! r2 c; q5 C6 D7 v
S: ^9 \" Z) N2 E! b8 W |