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

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

[复制链接]
攻城狮Melo 发布时间:2023-9-26 16:22
一、概述1 K/ t7 n/ C( B* W
1、说明
每一款芯片的启动文件都值得去研究,因为它可是你的程序跑的最初一段路,不可以不知道。通过了解启动文件,我们可以体会到处理器的架构、指令集、中断向量安排等内容,是非常值得玩味的。

5 {$ P! e" G8 w' k% N/ Z
STM32作为一款高端 Cortex-M3系列单片机,有必要了解它的启动文件。打好基础,为以后优化程序,写出高质量的代码最准备。
本文以一个实际测试代码--START_TEST为例进行阐述。
; l1 q# k% S/ L( {  ?+ n
2、整体过程概括
STM32整个启动过程是指从上电开始,一直到运行到 main函数之间的这段过程,步骤为(以使用微库为例):
①上电后硬件设置SP、PC
②设置系统时钟
③软件设置SP
④加载.data、.bss,并初始化栈区
⑤跳转到C文件的main函数

3 v- W9 k& B- R* n5 J& X
3、整个启动过程涉及的代码推荐阅读启动过程涉及的文件不仅包含 startup_stm32f10x_hd.s,还涉及到了MDK自带的连接库文件 entry.o、entry2.o、entry5.o、entry7.o等(从生成的 map文件可以看出来)。
5 t- M9 u+ y; U3 I; [
. Y6 `( ]9 N% l  U2 H  W二、程序在Flash上的存储结构$ k2 Q# N* R7 {' Y3 r' p
在真正讲解启动过程之前,先要讲解程序下载到 Flash上的结构和程序运行时(执行到main函数)时的SRAM数据结构。程序在用户Flash上的结构如下图所示。下图是通过阅读hex文件和在MDK下调试综合提炼出来的。7 N% k8 n) C5 n  A; K5 Z" m0 B
+ P( s# |, C# m! T
微信图片_20230926162154.jpg + B, r0 n7 {6 b  `
1 k6 S8 k4 K2 Q$ R( H$ D
  MSP初始值        编译器生成,主堆栈的初始值, F  E+ Q4 |! N3 f( @3 D( {, {, H; r
  异常向量表        不多说- N9 m' m2 ]. A8 h. s( M0 J
  外部中断向量表      不多说
  i3 F; q5 z! q' u0 @  代码段          存放代码  h  u* g6 ]3 c% H* B- ]% F
  初始化数据段       .
$ w2 U9 [! R# r' @# b. [data6 R% K5 T* Y6 L3 O; o7 S1 ]
  未初始化数据段      .bss
. t+ Z1 f' p6 @0 j
! M# N+ Q5 I) C: E+ g; Q加载数据段和初始化栈的参数0 d& C6 r( ~) P
加载数据段和初始化栈的参数分别有4个,这里只讲解加载数据段的参数,至于初始化栈的参数类似。& i, R1 N4 X8 l$ q
  1. 0x0800033c        Flash上的数据段(初始化数据段和未初始化数据段)起始地址
    . O, Q0 G9 ^/ ?4 B: U. d
  2. 0x20000000        加载到SRAM上的目的地址
    6 b  Z: W1 E' S8 O  ?; D
  3. 0x0000000c        数据段的总大小
    8 F6 w5 o5 o! `5 O
  4. 0x080002f4         调用函数_scatterload_copy
复制代码
需要说明的是初始化栈的函数-- 0x08000304与加载数据段的函数不一样,为 _scatterload_zeroinit,它的目的就是将栈空间清零。3 l' R& c! i0 Q/ ]; n

! x$ S% }0 X/ j* L9 v* D
微信图片_20230926162150.jpg
* B7 I: R  h+ A2 j/ O! Y8 I( c( ?- Q+ C- H3 M# t8 f
三、数据在SRAM上的结构6 K# z. R# p# q. U7 ]. v' H
程序运行时(执行到main函数)时的SRAM数据结构
( u3 F6 @0 d/ p2 u3 u3 p  U  r# y- y& k! u) }% {+ H: f
微信图片_20230926162144.jpg / G9 N7 p' L  e8 W8 z
% t8 S/ j& b! v! H) c& l. b( |7 W3 u- h
四、详细过程分析! l# @* w5 @! k: c
有了以上的基础,现在详细分析启动过程。1 ?7 ]  P+ E5 ^/ C/ G0 v4 q
" z" N+ A6 U9 g
1、上电后硬件设置SP、PC6 y5 o. D- v( Q9 U! E, J6 ?8 N8 z
刚上电复位后,硬件会自动根据向量表偏移地址找到向量表,向量表偏移地址的定义如下:
2 z$ k  z, X5 B( H) n8 m! {& J( C. n, N- E! @! i7 A
微信图片_20230926162135.jpg
$ t4 A/ Q) ~2 ~1 k) r% n, o/ e9 b0 s0 P/ `! b
调试现象如下:
* j" t" G: v9 c* o, K, W- V& P1 M
4 j5 o' R# }& Y  j8 g6 N
微信图片_20230926162132.png : E4 Q. }! v8 K4 g5 z
/ w8 @6 y& Z' Q/ p; Y
看看我们的向量表内容(通过J-Flash打开hex文件)
! v& e; c! x! `4 J  X9 o8 a7 ~6 C% b% k0 y8 _0 `) E
微信图片_20230926162128.jpg
0 F0 Q9 L% {4 e& a
5 s( V: R! t  [  W% \硬件这时自动从0x0800 0000位置处读取数据赋给栈指针SP,然后自动从0x0800 0004位置处读取数据赋给PC,完成复位,结果为:
9 S$ _3 B8 ~% l2 u
, n2 r3 o$ I8 }8 h  SP =0x02000810
' X6 h, F* @% b, Z  r  PC =0x080001452 F6 V, J  V! G

, Q- q, M$ p5 v% }  @- i/ L2、设置系统时钟. H! r& ?2 B: X6 S0 f
上一步中令 PC=0x08000145的地址没有对齐,硬件自动对齐到 0x08000144,执行 SystemInit函数初始化系统时钟。
5 F: q" [" |7 Q# s% R4 t4 v0 c% ~4 H% T. v
3、软件设置SP! p6 J' x" m0 ?) I$ }

# M9 X3 Z7 c6 r* O5 n
  1. LDR   R0,=__main4 v6 C5 `# o( v* j
  2. BX   R0
复制代码

( Z  q* b; ~  P; `( s9 u  @3 f& Q执行上两条之类,跳转到 __main程序段运行,注意不是main函数, ___main的地址是0x0800 0130。
9 G9 H8 h1 v  d" c7 B: q, B9 _+ ^& w- M+ I
可以看到指令LDR.W sp,[pc,#12],结果SP=0x2000 0810。: l" v5 K, {5 i( B

2 M; E; s& {2 @4、加载.data、.bss,并初始化栈区
8 M! \+ ?( }' j' O
  1.    BL.W     __scatterload_rt2
复制代码

. D0 M" ^1 R7 D7 y4 Q  c$ E) h进入 __scatterload_rt2代码段。
  G% L2 W% ^+ G% o
  1. __scatterload_rt2:1 s# [' N/ n; |9 L& H
  2.   0x080001684C06                  LDR         r4,[pc,#24]  ; @0x080001844 B  {- T3 w5 Y9 h
  3. 0x0800016A4D07                 LDR         r5,[pc,#28]  ; @0x08000188' y' a8 ?! P: D1 }; s7 n1 Y
  4. 0x0800016CE006                  B             0x0800017C
    ) z8 L( m. g  j: Q
  5. 0x0800016E68E0                  LDR         r0,[r4,#0x0C]
复制代码

/ S# J0 i. _, _: ^这段代码是个循环 (BCC0x0800016e),实际运行时候循环了两次。第一次运行的时候,读取“加载数据段的函数 (_scatterload_copy)”的地址并跳转到该函数处运行(注意加载已初始化数据段和未初始化数据段用的是同一个函数);第二次运行的时候,读取“初始化栈的函数 (_scatterload_zeroinit)”的地址并跳转到该函数处运行。相应的代码如下:

$ Q+ Z- \' X# y+ |' o
  1. 0x0800016E68E0               LDR             r0,[r4,#0x0C]8 ]5 N' j0 V* m& T6 W: z6 n. @
  2. 0x08000170   F0400301     ORR             r3,r0,#/ p! }: c8 `: A' J( b7 V
  3. 0x010x08000174
    9 |0 F6 C( M; @8 C% G1 G7 G
  4. 0x080001784798                BLX              r3
复制代码

6 q: g/ K8 E/ C/ k
5 n' H: t; t3 \- P! C+ v1 ], L& K0 s
当然执行这两个函数的时候,还需要传入参数。至于参数,我们在“加载数据段和初始化栈的参数”环节已经阐述过了。当这两个函数都执行完后,结果就是“数据在SRAM上的结构”所展示的图。最后,也把事实加载和初始化的两个函数代码奉上如下:
' T3 D! A! G6 s& S6 d

& R" C! S, Z$ x$ a2 c

, R: Z, g1 b2 M# h4 S" b( w& d
  1.   __scatterload_copy:, v# j, R+ l9 z
  2. 0x080002F4  E002              B              0x080002FC+ D8 q2 Q- R' M! D) I5 {/ ^. w% S* A* l6 W
  3. 0x080002F6  C808             LDM           r0!,{r3}
    ( }5 G# `9 J) S6 ^9 w' d
  4. 0x080002F81F12                SUBS         r2,r2,#4
    6 ]- ?7 U+ `  G7 c' S* z( k
  5. 0x080002FA  C108             STM            r1!,{r3}
    8 q% @0 y  |5 s; X
  6. 0x080002FC2A00               CMP            r2,#0x00- Y0 l- i% I8 b1 M
  7. 0x080002FE   D1FA             BNE            0x080002F6+ l7 r. C, L' H( k: S+ h% s
  8. 0x080003004770                 BX              lr
    2 {8 G( l5 @/ f8 N$ N
  9. __scatterload_null:1 g! S; E  u: Q- u
  10. 0x080003024770                   BX             lr9 Y1 f+ J# K" a& }. K+ T8 N
  11. __scatterload_zeroinit:! |6 O# }8 H1 {+ E$ d/ P5 M
  12. 0x080003042000                  MOVS          r0,#0x00
    " @' l: ?& P; p- P) i1 m8 j
  13. 0x08000306  E001                 B               0x0800030C$ r1 i4 k" p8 }6 k9 _1 w3 L( ]
  14. 0x08000308  C101              STM               r1!,{r0}  [& S  C& E! ^' s  V: J  s7 Q
  15.   0x0800030A1F12                SUBS             r2,r2,#48 O+ t; s" U0 E4 N' n5 Z
  16. 0x0800030C2A00                CMP             r2,#0x001 @( n. J! j- F8 T2 {* @
  17.   0x0800030E   D1FB             BNE              0x08000308
    5 z/ R  c; e* m: \; M
  18. 0x080003104770                 BX                    lr
复制代码

4 m! Q4 K+ A4 G' v. C2 t五、异常向量与中断向量表! U$ ]! g; |4 F# s6 k4 R/ ~3 C& ]
5 \; s# @6 C3 D+ V$ h
OWMJC{X{Z@7}VU@A]`23JOH.png " K$ z( s+ {& P) l! n/ G9 W
HEDD4MIA$J~O0`@}2EEG1}9.png N974V43(91]9CB5X`33`U]L.png
4 S* U' P( P8 g+ z6 L2 G
* R& H/ S) D, z# U这段代码就是定义异常向量表,在之前有一个“J-Flash打开hex文件”的图片跟这个表格是一一对应的。编译器根据我们定义的函数 Reset_Handler、NMI_Handler等,在连接程序阶段将这个向量表填入这些函数的地址。8 r( r3 j" D6 }' D4 X. d
7 H! ^# |  d( h$ a* @6 o+ s
  1. startup_stm32f10x_hd.s内容:5 ~  t0 G; J& p( C" E* g
  2. N MI_Handler     PROC- z3 U9 q( |- R8 S, t
  3.                          EXPORT  NMI_Handler                [WEAK]6 X- ?& K# X0 d. o2 f
  4.                          B       .
    ! M, A& Z8 o1 W3 k' C  h+ y
  5.                          ENDP
复制代码
& h+ [- g1 f5 v" N. X
在启动汇编文件中已经定义了函数 NMI_Handler,但是使用了“弱”,它允许我们再重新定义一个 NMI_Handler函数,程序在编译的时候会将汇编文件中的弱函数“覆盖掉”--两个函数的代码在连接后都存在,只是在中断向量表中的地址填入的是我们重新定义函数的地址。
8 a5 f$ P2 _/ P  L9 T) k
' G1 Y1 Q$ q0 c! e9 n# {! _六、使用微库与不使用微库的区别
2 d9 _" M& t; K6 i! s, [7 V& N. G. W! j/ b0 L) A" k
微信图片_20230926162123.png
) _: O* k* S1 i! Y) N. ~, }# E& ]4 c
使用微库就意味着我们不想使用MDK提供的库函数,而想用自己定义的库函数,比如说printf函数。那么这一点是怎样实现的呢?我们以printf函数为例进行说明。  O* o; O; s6 z! D' j

3 }5 ~7 M3 a. [( Q* r% W1、不使用微库而使用系统库
' I0 ]- G  c/ |! L在连接程序时,肯定会把系统中包含printf函数的库拿来调用参与连接,即代码段有系统库的参与。+ h" J  h" \# q7 Q
2 _7 o7 O5 [0 j- Y
在启动过程中,不使用微库而使用系统库在初始化栈的时候,还需要初始化堆(猜测系统库需要用到堆),而使用微库则是不需要的。; K# b& K* ~+ ^' {9 i
  1. IF      :DEF:__MICROLIB& B$ d! S0 p. y1 r+ l
  2. , s8 @4 C& x- q& q
  3.                  EXPORT  __initial_sp
    , |% v  [: Y$ B' ?! J: V! P$ W

  4. % o9 X# m* c$ ^1 X: B
  5.                  EXPORT  __heap_base
    - H$ z; b" j8 F3 y
  6. 2 X% X+ i8 I+ i0 c/ L
  7.                  EXPORT  __heap_limit
    6 S: g$ Z+ L) h  y6 T* [
  8. 4 E* H' I7 [# ]
  9.                  ELSE
    2 c. q- _5 j! D% S% i# j

  10. * l0 {+ t- z$ |7 U3 P
  11.                  IMPORT  __use_two_region_memory, I5 G) O! {) m6 k, R5 g( T% G) p
  12. * ?! e; f: Y" \" v
  13.                  EXPORT  __user_initial_stackheap
    : k7 V$ y: |8 n

  14.   s& C* Q) N: h- k. e
  15. __user_initial_stackheap  ^1 ]( N( o- ?) J5 B
  16. ; m* Q1 c2 c  g; Q
  17. LDR     R0, =Heap_Mem7 ?& ]) h% I9 {1 ^. Q
  18. LDR     R1, =(Stack_Mem+Stack_Size)
    ' `3 H5 Z" p- [. e
  19. LDR     R2, = (Heap_Mem+Heap_Size)3 d2 }0 H" K) Z4 ^! J" W4 D: K+ O/ O
  20.                  LDR     R3, =Stack_Mem3 x! d) J- M3 G
  21. BX      LR' w5 B+ u+ e6 `! L

  22. & L" C$ D+ w* G: p! ^8 j1 p
  23.                  ALIGN% e: V' A7 {5 `. s6 R7 f1 f

  24. ' G: S- z0 E0 z( q$ v/ l) D3 s
  25.                  ENDIF
复制代码
" I9 {+ ?6 I! e% X: C4 X3 M
另外,在执行 __main函数的过程中,不仅需要完成“使用微库”情况下的所有工作,额外的工作还需要进行库的初始化,才能使用系统库(这一部分我还没有深入探讨)。附上 __main函数的内容:. b! q1 B. X, j$ X1 T- `
, c6 R  v! d- ~4 @- R
UAX42%MZ`SVUA2@Q]0AYV@H.png 1 W4 L$ ]' i/ V; r* \3 p7 Z4 N
%IJF((2BG(O`1SWDR%6(S.png ! T4 ]2 E. K) V1 P- f
{%EG7U]KL9L{SMV63U}`]_D.png 6 [! d$ \, ^" c3 {: G& r( V# u6 L" f

, p, w! c7 i, S6 u( ^) n2、使用微库而不使用系统库
0 {4 e! n3 y, t) ]2 \( ]在程序连接时,不会把包含printf函数的库连接到终极目标文件中,而使用我们定义的库。
4 x9 ^) @8 a/ g1 n9 E) x1 o% L. X( B
启动时需要完成的工作就是之前论述的步骤1、2、3、4、5,相比使用系统库,启动过程步骤更少。0 W7 f  p! e) G( N8 e
7 d: ^  Q/ W: d" g* {
转载自:嵌入式电子% {9 ?+ o) L0 R
如有侵权请联系删除
6 t% K# G1 B6 b3 U0 I
. ^: T" @/ }' K; x5 u" d% K# L& h5 D7 ]9 D7 D0 C+ O! V3 H

. K$ ~7 o  H+ j: q( i3 F' U! S/ c
赞 收藏 评论0 发布时间:2023-9-26 16:22

举报

0个回答

所属标签

相似技术帖

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