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

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

[复制链接]
攻城狮Melo 发布时间:2023-9-26 16:22
一、概述
! a& G5 V* v" J
1、说明
每一款芯片的启动文件都值得去研究,因为它可是你的程序跑的最初一段路,不可以不知道。通过了解启动文件,我们可以体会到处理器的架构、指令集、中断向量安排等内容,是非常值得玩味的。
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 `
微信图片_20230926162154.jpg + 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
  1. 0x0800033c        Flash上的数据段(初始化数据段和未初始化数据段)起始地址
    3 N& i1 t, R6 M
  2. 0x20000000        加载到SRAM上的目的地址1 j# F. |* m! R  T
  3. 0x0000000c        数据段的总大小
    * m# n0 R; m* C
  4. 0x080002f4         调用函数_scatterload_copy
复制代码
需要说明的是初始化栈的函数-- 0x08000304与加载数据段的函数不一样,为 _scatterload_zeroinit,它的目的就是将栈空间清零。
* y1 Z0 T# V, m4 ^# P
1 x8 F: \( M6 B" W# i& E
微信图片_20230926162150.jpg
, _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 }
微信图片_20230926162144.jpg 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
微信图片_20230926162135.jpg
% 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
微信图片_20230926162132.png 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
微信图片_20230926162128.jpg ; 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
  1. LDR   R0,=__main
    + r6 i- M* @( H7 K! b) j% u
  2. 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# }
  1.    BL.W     __scatterload_rt2
复制代码

+ v3 N, Y) S, H6 ~2 e$ n/ A4 }进入 __scatterload_rt2代码段。" ?! m; J& e# ~8 A# k9 S* H
  1. __scatterload_rt2:. C( J1 A% C# O% m- c
  2.   0x080001684C06                  LDR         r4,[pc,#24]  ; @0x08000184
    ! s( d# K9 c7 b  r* f6 _) ]
  3. 0x0800016A4D07                 LDR         r5,[pc,#28]  ; @0x08000188
    ; P) J" h& y: o* e' N3 |2 \
  4. 0x0800016CE006                  B             0x0800017C" @# R2 L* D8 x9 X( b
  5. 0x0800016E68E0                  LDR         r0,[r4,#0x0C]
复制代码

5 W- [- F5 K( @5 p; B这段代码是个循环 (BCC0x0800016e),实际运行时候循环了两次。第一次运行的时候,读取“加载数据段的函数 (_scatterload_copy)”的地址并跳转到该函数处运行(注意加载已初始化数据段和未初始化数据段用的是同一个函数);第二次运行的时候,读取“初始化栈的函数 (_scatterload_zeroinit)”的地址并跳转到该函数处运行。相应的代码如下:

3 o1 Z* `4 \& B& Z
  1. 0x0800016E68E0               LDR             r0,[r4,#0x0C]7 H$ K1 q, e1 f2 l" I
  2. 0x08000170   F0400301     ORR             r3,r0,#  `/ _# Y8 r& t3 g" d5 n
  3. 0x010x080001742 [- R  u) t5 e$ Z2 \
  4. 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
  1.   __scatterload_copy:
    2 H! \6 |# }! b- n6 d7 o( a
  2. 0x080002F4  E002              B              0x080002FC1 D" ^, G: Q$ V, L1 A  b
  3. 0x080002F6  C808             LDM           r0!,{r3}9 c+ i1 I1 ?7 \# Q
  4. 0x080002F81F12                SUBS         r2,r2,#4
    & Q* d* P, C  Q3 Z$ G" |5 p% p" y
  5. 0x080002FA  C108             STM            r1!,{r3}& ^- F& P1 r: W7 n1 ]% _+ |( S9 g
  6. 0x080002FC2A00               CMP            r2,#0x00
    ) F* x, @2 d& V' Z9 F
  7. 0x080002FE   D1FA             BNE            0x080002F6- u7 c3 G8 e9 x$ {( y. d
  8. 0x080003004770                 BX              lr
    ! v, s8 |4 g- n9 \
  9. __scatterload_null:0 N: H* J' `: o) \
  10. 0x080003024770                   BX             lr
    % s' h) }  {" E
  11. __scatterload_zeroinit:; t* B7 b9 ]* }8 q2 L
  12. 0x080003042000                  MOVS          r0,#0x00  B1 |8 q% [4 j  S) H8 }  o2 U2 O
  13. 0x08000306  E001                 B               0x0800030C" k2 U$ W" c9 \$ B9 S) T+ v& o
  14. 0x08000308  C101              STM               r1!,{r0}& z1 ^- }+ E* ^, M6 c
  15.   0x0800030A1F12                SUBS             r2,r2,#4
    2 m: @' B$ [9 L6 Z7 {2 y
  16. 0x0800030C2A00                CMP             r2,#0x00# i# y# l4 H* @  E7 b' ]% b( N
  17.   0x0800030E   D1FB             BNE              0x08000308$ q; J* ~( {. R4 k% N0 q& D
  18. 0x080003104770                 BX                    lr
复制代码

% R* Y2 \" X/ l& h; `4 D五、异常向量与中断向量表: l1 X: [$ o1 j4 }! ]- L

6 O( i6 f) ^) E OWMJC{X{Z@7}VU@A]`23JOH.png
) e3 o! B4 x! ?, P% e( `
HEDD4MIA$J~O0`@}2EEG1}9.png N974V43(91]9CB5X`33`U]L.png + 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
  1. startup_stm32f10x_hd.s内容:0 I# x$ `2 X( _: x
  2. N MI_Handler     PROC% c3 x% U) t* o1 v3 @9 X) G
  3.                          EXPORT  NMI_Handler                [WEAK]( p9 m, P$ d- h7 Q( F
  4.                          B       ., }* C; o4 E) P& `: z) ?
  5.                          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
微信图片_20230926162123.png # 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 _
  1. IF      :DEF:__MICROLIB
    9 @" t- L5 j" E$ \% S; G
  2. 9 P& w4 }. I8 `( `+ W
  3.                  EXPORT  __initial_sp
    - X6 {' f- h; ]2 R9 j+ Q# l5 E
  4. , @# T1 o) |: K# Z3 u! W3 p4 F; @
  5.                  EXPORT  __heap_base
    * |% r; ]' P- S1 a. h& Y; i$ ]: H

  6. + ]  J, o: u# A/ o( E& K
  7.                  EXPORT  __heap_limit. I# E1 o, N, r

  8. . `, y! g- d/ g0 d: [" x
  9.                  ELSE
    6 v$ Q( Y& Z7 R, ?0 s* I9 V1 S

  10. 9 e, V+ F/ S& O. I; s
  11.                  IMPORT  __use_two_region_memory5 S" E( X8 X$ M% n' p0 g+ l/ a
  12. 7 L9 x+ L1 C, [1 V+ _+ h, o
  13.                  EXPORT  __user_initial_stackheap# }( ]( {+ z! A' u$ k( ]! J
  14. - k9 V% b9 Z4 d: B3 c
  15. __user_initial_stackheap) F. ~9 d8 R" f4 T# d/ o
  16. 0 V8 S9 W& a/ k7 J* A9 m
  17. LDR     R0, =Heap_Mem+ N% x: M4 V6 u+ x. V) Y! u
  18. LDR     R1, =(Stack_Mem+Stack_Size)) _6 s- ?, a4 v. x* K) @) q
  19. LDR     R2, = (Heap_Mem+Heap_Size)9 m. B6 h8 R" J+ R' Z  d, B
  20.                  LDR     R3, =Stack_Mem& Q9 Z6 v$ T2 }0 G, K- j
  21. BX      LR
    ) B& _, z0 B! ?$ q( _

  22. 5 M+ t$ }3 `: C; T
  23.                  ALIGN/ L- K# {1 G/ x4 H1 R7 z3 n
  24. 6 n! y% j: L5 Y: c7 L/ }1 t
  25.                  ENDIF
复制代码

* a. K* b$ S4 }' ~另外,在执行 __main函数的过程中,不仅需要完成“使用微库”情况下的所有工作,额外的工作还需要进行库的初始化,才能使用系统库(这一部分我还没有深入探讨)。附上 __main函数的内容:
' v/ ]7 s0 U2 h6 {- C6 k
2 v6 Q* ^6 ]; b
UAX42%MZ`SVUA2@Q]0AYV@H.png * Z) s+ A( r$ E, s
%IJF((2BG(O`1SWDR%6(S.png , N2 O. ]: t! H+ C% }2 }7 B5 n3 ]
{%EG7U]KL9L{SMV63U}`]_D.png
' ~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
赞 收藏 评论0 发布时间:2023-9-26 16:22

举报

0个回答

所属标签

相似技术帖

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