你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

基于STM32上电启动过程经验分享

[复制链接]
攻城狮Melo 发布时间:2023-9-26 16:22
一、概述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
微信图片_20230926162154.jpg
' 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
  1. 0x0800033c        Flash上的数据段(初始化数据段和未初始化数据段)起始地址% d; {, h/ d7 W
  2. 0x20000000        加载到SRAM上的目的地址1 M3 S! J1 b2 {2 c
  3. 0x0000000c        数据段的总大小
      b% T) M: r) Y& @: c  @, J
  4. 0x080002f4         调用函数_scatterload_copy
复制代码
需要说明的是初始化栈的函数-- 0x08000304与加载数据段的函数不一样,为 _scatterload_zeroinit,它的目的就是将栈空间清零。
5 s* M  I* L" P/ v, \8 B1 _2 B' m+ n
微信图片_20230926162150.jpg
  ^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
微信图片_20230926162144.jpg
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
微信图片_20230926162135.jpg 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$ `
微信图片_20230926162132.png 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
微信图片_20230926162128.jpg . 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
  1. LDR   R0,=__main+ B! Y& b4 @4 X( V" r7 f& l
  2. 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
  1.    BL.W     __scatterload_rt2
复制代码

. J( g' {5 F. T; T) L# t$ c% y进入 __scatterload_rt2代码段。
9 ]+ r! F+ {" Y5 u% W' M# i
  1. __scatterload_rt2:% v. Q/ ]$ x! R
  2.   0x080001684C06                  LDR         r4,[pc,#24]  ; @0x08000184
    $ g$ F4 Q! {4 P$ p! n; f
  3. 0x0800016A4D07                 LDR         r5,[pc,#28]  ; @0x08000188
    " D* ?3 \! G: l  k  e3 Q+ m
  4. 0x0800016CE006                  B             0x0800017C
    3 g8 [4 V1 I1 G' Q, v6 f
  5. 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
  1. 0x0800016E68E0               LDR             r0,[r4,#0x0C]
    7 K) {. p5 W) u( a7 e6 p% Y
  2. 0x08000170   F0400301     ORR             r3,r0,#' o/ U* k6 |9 s9 K) l
  3. 0x010x08000174
    7 v- Z; k, \4 S* \# P- o
  4. 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
  1.   __scatterload_copy:
      K: a. z6 H9 b
  2. 0x080002F4  E002              B              0x080002FC
    2 @; Q& L+ K5 z  i
  3. 0x080002F6  C808             LDM           r0!,{r3}
    - g2 u  h% T- u" H( I0 U; V
  4. 0x080002F81F12                SUBS         r2,r2,#4# ?8 ?3 {0 l% N& `# m, O
  5. 0x080002FA  C108             STM            r1!,{r3}( p$ ?  O- E1 O! }; _! e; t
  6. 0x080002FC2A00               CMP            r2,#0x00
    : R( M+ }6 m  E' E3 Z0 M9 ?
  7. 0x080002FE   D1FA             BNE            0x080002F6
    $ H; h. O4 s1 j! f$ C# ^
  8. 0x080003004770                 BX              lr
    + d5 M& y" m$ t( B' }' \
  9. __scatterload_null:
    & V2 W9 d/ B7 B5 R; \3 B
  10. 0x080003024770                   BX             lr
    % N# ?$ q  c$ F% v; p/ V' k% p) A
  11. __scatterload_zeroinit:9 S8 P* e4 v" `0 e' Z
  12. 0x080003042000                  MOVS          r0,#0x00* h, |8 V; i% t- z! E* x
  13. 0x08000306  E001                 B               0x0800030C0 B+ I% }# m* x- {+ I* d9 l
  14. 0x08000308  C101              STM               r1!,{r0}
    ' n) e  k7 |: _9 A0 \* x1 w" v/ F
  15.   0x0800030A1F12                SUBS             r2,r2,#4) D2 O/ S) j9 Y+ |/ `5 X
  16. 0x0800030C2A00                CMP             r2,#0x00& ~2 I( N# d/ b% U1 Q
  17.   0x0800030E   D1FB             BNE              0x08000308
    " r; ?/ C, W5 E" ?  V  @. P
  18. 0x080003104770                 BX                    lr
复制代码
& d6 K) D0 k/ S0 W7 z4 ]
五、异常向量与中断向量表
2 s# ^' n- o' f3 Z( r. |, L

' }6 u' \8 s+ o6 b$ _/ [ OWMJC{X{Z@7}VU@A]`23JOH.png 0 N$ ^6 }2 U/ S" |- J4 P1 n* h
HEDD4MIA$J~O0`@}2EEG1}9.png N974V43(91]9CB5X`33`U]L.png + 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
  1. startup_stm32f10x_hd.s内容:/ v, G: h1 ?+ x
  2. N MI_Handler     PROC
    + x3 ?" [  Z, c% x* [
  3.                          EXPORT  NMI_Handler                [WEAK]: ?& j" K) e  D  @
  4.                          B       .
    $ L) a' O* a: F
  5.                          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
微信图片_20230926162123.png , 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
  1. IF      :DEF:__MICROLIB
    0 }9 d- q' J8 H! P8 M4 t7 f
  2. 9 _( Y' ?  U8 q0 r$ a6 Q& q5 f$ d
  3.                  EXPORT  __initial_sp. D4 N. Y, N0 w
  4. # y: f9 a# Z9 M' B5 a" y6 t
  5.                  EXPORT  __heap_base
    ! b  o: Q6 D+ ^( y  S, o3 D
  6. . t8 J% T7 ]: x5 V: G. c
  7.                  EXPORT  __heap_limit6 {: j# w$ M, b! U0 g+ ]

  8. . D! v  t/ F- u
  9.                  ELSE
    ; j! t1 p1 W8 B5 Q, I' j
  10. , L1 l6 J/ Z0 O$ l
  11.                  IMPORT  __use_two_region_memory
    1 Z* |5 C* m: I  N6 h! c. t

  12. & U: f" C6 z" p. }
  13.                  EXPORT  __user_initial_stackheap
    1 S6 A, f! M: j8 f! y' }9 o

  14. ) j% X. |9 R8 n7 V
  15. __user_initial_stackheap
    3 X2 M; y- S1 t7 ?5 t) ]( {  ^2 T

  16. . s( ~' Z0 F1 k: ^
  17. LDR     R0, =Heap_Mem* P2 `: W) [$ P/ |
  18. LDR     R1, =(Stack_Mem+Stack_Size); T2 s1 R: x$ \3 ~! ^2 Z
  19. LDR     R2, = (Heap_Mem+Heap_Size). Y9 T1 S% a- B0 d6 u
  20.                  LDR     R3, =Stack_Mem+ P0 ^  W2 S! @1 [2 V; F8 i
  21. BX      LR# r* T- M6 s- U5 R* b
  22. ( R2 o. Q) s( M( z+ V4 F5 j
  23.                  ALIGN- I$ W8 \: A; ^

  24. * m/ Z1 m# }( c5 i2 c: ?$ p
  25.                  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
UAX42%MZ`SVUA2@Q]0AYV@H.png # Y0 y7 B' m. g: S2 C
%IJF((2BG(O`1SWDR%6(S.png / _: X/ y/ P0 A: C
{%EG7U]KL9L{SMV63U}`]_D.png
' _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
赞 收藏 评论0 发布时间:2023-9-26 16:22

举报

0个回答

所属标签

相似技术帖

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版