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

STM32F4系列-启动过程详解-二

[复制链接]
STMCU小助手 发布时间:2022-10-24 15:43
讲解启动过程之前先简单了解一下内存五区:
1.栈区stack:由编译器自动分配释放,存放函数的参数值,局部变量的值。
2.堆区heap:由程序员分配和释放,若程序员不释放,程序结束时由OS回收。
3.全局区(静态区 static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量、未初始化的静态变量在相邻的另一块区域。
4.文字常量区:常量字符串就是放在这里的。
5.程序代码区 : 存放函数体的二进制代码。
话不多说先看一段代码:
  1. int a = 0;         //全局初始化区, 可以被其他c文件 extern 引用
    $ g, v3 P- `% A" Z0 r7 U
  2. static int ss = 0; //静态变量,只允许在本文件使用+ o  t( A/ n+ g* X5 q! F, o5 ~7 W& q
  3. char *p1;          //全局未初始化区6 {( w9 J9 b+ C4 o+ I- G3 @
  4. void main(void)
    + d2 s& ], r3 L$ d: @- L; Q8 ^
  5. {4 p8 Y0 T$ ^* j9 T
  6. int b;             //栈
    : W; I$ }. |: ]3 `! I9 W- i
  7. char s[] = "abc";  //栈
    3 q, n+ e; O  Q
  8. char *p2;          //栈" ~0 E; o/ H# {
  9. char *p3 = "123456"; //123456\0在常量区,p3在栈上。
    ! w4 c& x4 i: S) a* R
  10. static int c =0;         //全局(静态)初始化区
    5 F' p* N0 F" v, T4 d5 S
  11. p1 = (char *)malloc(10); //在堆区申请了10个字节空间$ \1 K( P. _( V8 P7 k
  12. p2 = (char *)malloc(20); //在堆区申请了20个字节空间! r% c( X3 r3 `( r; a1 P
  13. strcpy(p1, "123456"); /* 123456字符串(结束符号是0,总长度7)放在常量区,编译器可能会
    & B  k) E- n, ]
  14. 将它与p3所指向的"123456"优化成一个地方 */5 U4 q9 Z/ v! S9 e1 N# x. }
  15. }
复制代码

0 x2 h# L7 U4 Z5 }  g) v简单的了解了内存五区之后再来看看STM32 的启动过程。STM32 的启动过程是指从 CPU 上电复位执行第 1 条指令开始到进入 C 程序 main()函数入口之间的部分。启动过程相对来说还是比较重要的,虽然不好理解但必须了解掌握。$ H. l; N; k+ Z1 p
1. 不同的系列芯片的的启动代码不同。
2. 启动过程主要完成的工作:(startup_stm32xxxx.s)

3 C0 s1 w! J' G; R9 h9 T
微信图片_20221024154332.png

) W# c% N  j; z: c
3. 打开你的工程,鼠标双击工程文件。就会出来对应的.out文件查看中断向量列表在内部flash的存储。1 p& O& b5 K3 P: X9 t( o0 {
$ n: i7 \; l, q( T6 o: c) P/ X& ]
微信图片_20221024154329.png # [& h+ }- u, b
微信图片_20221024154318.png " a- _- a1 w, R0 q$ E
4. 复位序列
; r, A" j" Y$ j5 `5 b; I8 u       硬件复位之后,就是按下复位开关后。CPU 内的时序逻辑电路首先完成如下两个工作(程序代码下载到内部flash为例,flash首地址 0x0800 0000)将 0x08000000 位置存放的堆栈栈顶地址存放到 SP 中(MSP)。将 0x08000004 位置存放的向量地址装入 PC 程序计数器。# l6 p6 ~1 r' `+ t0 D$ W+ d  C- U: M" O

! ^9 N1 }# O' T
# x& o1 H. f, q& T! j  w% D( ?9 n) W8 W) b- j  @! o( }

- V8 A: g* W; ~# ~+ n0 N4 f
       CPU 从 PC 寄存器指向的物理地址取出第 1 条指令开始执行程序,也就是开始执行复位中断服务程序 Reset_Handler。复位中断服务程序会调用SystemInit()函数来配置系统时钟、配置FMC总线上的外部SRAM/SDRAM,然后跳转到 C 库中__main 函数。由 C 库中的__main 函数完成用户程序的初始化工作(比如:变量赋初值等),最后由__main 函数调用用户写的 main()函数开始执行 C 程序。& f) u4 S1 c) h' |1 z/ h+ o" M& Z0 N
学习启动文件之前先来了解一下汇编语言中的一些指令操作。* e0 z2 M% Y% r, S. s: O

; u* b) W0 Z6 P 微信图片_20221024154313.jpg & ~& d- W: p. n' K% L
+ q. j7 l3 _0 o$ O+ W8 M
第 1 部分代码分析
3 e3 U% j1 J# r) j) T* b$ M     下面的代码实现开辟栈(stack)空间,用于局部变量、函数调用、函数的参数等。栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。如果编写的程序比较大,定义的局部变量很多,那么就需要修改栈的大小。如果某一天,你写的程序出现了莫名奇怪的错误,并进入了硬 fault的时候,这时你就要考虑下是不是栈不够大,溢出了。
  C- `! o/ P/ F4 t& s
5 A# a8 C9 ^. C( x 微信图片_20221024154309.png ( p9 `2 s" F# ]. C5 ?' p- m
8 r8 w! F) q1 r0 w6 _( i0 p
第 7 行:EQU 是表示宏定义的伪指令,类似于 C 语言中的#define。伪指令的意思是指这个“指令”并不会生成二进制程序代码,也不会引起变量空间分配。0x00008000 表示栈大小,注意这里是以字节为单位。0x00008000 =32768字节=32KB。% a% W; }$ l# q5 |" n
第 8 行:开辟一段数据空间可读可写,段名 STACK,按照 8 字节对齐。ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。ARER 后面的关键字表示这个段的属性。STACK :表示这个段的名字,可任意命名。NOINIT:表示此数据段不需填入初始数据。READWRITE:表示此段可读可写。ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(地址对 8 求余数等于0)。$ L- U  @2 o, t) b& n& M
第 9 行:SPACE 这行指令告诉汇编器给 STACK 段分配 0x00000800 字节的连续内存空间。" f4 k; l9 V% u0 n6 e
第 10 行:__initial_sp 紧接着 SPACE 语句放置,表示了栈顶地址。__initial_sp 只是一个标号,标号主要用于表示一片内存空间的某个位置,等价于 C 语言中的“地址”概念。地址仅仅表示存储空间的一个位置,从 C 语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。. k/ R" t+ }; G7 R' C! c6 [
, [% }: f, ?4 b1 V# s0 F" X
第 2 部分代码分析 1 V* R  K4 A( C8 ^
    下面的代码实现开辟堆(heap)空间,主要用于动态内存分配,像 malloc,calloc, realloc 等函数分配的变量空间是在堆上。
2 u" E/ h) C1 f+ i8 f: }& h  F
* K5 B$ S% U8 ?( ]
微信图片_20221024154304.png : x" A+ J7 u6 b5 ^* T

1 f1 |* g- {" X& ]; X3 G

* z/ w7 A8 x3 A. f/ x. \  \       这几行语句和上面第 1 部分代码类似。分配一片连续的内存空间给名字叫 HEAP 的段,也就是分配堆空间。堆的大小为 0x00000400,也就是1024字节=1KB。# L% G8 K4 Q8 N: E
__heap_base 表示堆的开始地址3 t( p1 k  C& [3 H# b1 Y
__heap_limit 表示堆的结束地址7 _, m+ p. b; I2 B, x1 s/ y
' }* ^1 q% V+ _' ?. F5 r9 _" o3 Y
第 3部分代码分析
1 L- R' L" u3 P# N. B  M  ~6 s- d. M: |$ S: ]2 s" I" N
微信图片_20221024154259.png 6 m3 i% H4 I0 [: O$ i! o8 m
第 24 行:PRESERVE8 指定当前文件保持堆栈8字节对齐& b( k8 u4 J  C; ]7 z
第 25 行:THUMB 表示后面的指令是 THUMB 指令集 ,CM4 采用的是 THUMB - 2 指令集。
( h5 i9 w* G, I  J& u0 S8 S第 29 行:AREA 定义一块代码段,只读,段名字是 RESET。READONLY 表示只读,缺省就表示代码段了。0 ^; k" t2 a  L: @2 L
第 30-32 行:3 行 EXPORT 语句将 3 个标号申明为可被外部引用, 主要提供给链接器用于连接库文件或其他文件。当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址, 内核使用了―向量表查表机制‖。这里使用一张向量表。向量表其实是一个WORD( 32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地
: ~6 x1 y9 T. T; d8 d址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0)处必须包含一张向量表,用于初始时的异常分配。要注意的是这里有个另类:0 号类型并不是什么入口地址,而是给出了复位后 MSP 的初值。4 f% I% M/ L6 d3 @

7 U# b5 S- ^0 F3 o; V% I  k3 Y) \第 4部分代码分析- R& ], [# }* c1 I$ ^
微信图片_20221024154256.png 7 @1 \: X3 Z+ C6 v% u$ R
+ ?6 H% V; O# Z+ S. p
; M) ]$ Z5 I, \
微信图片_20221024154251.jpg
% `' g" a( b8 {  T- }5 T$ |* X, l$ P  v) {

5 [( w1 V6 l  E: ~4 Y上面的这段代码是建立中断向量表,中断向量表定位在代码段的最前面。具体的物理地址由链接器的配置参数(IROM1 的地址)决定。如果程序在 Flash 运行,则中断向量表的起始地址是 0x08000000。在MDK 中,配置如下:/ x. L0 B; H' I2 C$ Y; n/ e; O2 x

; l3 e2 j1 j; f; k- O! I 微信图片_20221024154245.png . s1 d9 _5 v4 [0 P" b  x

( Y, ^6 O) l2 E8 ~      DCD 表示分配 1 个 4 字节的空间。每行 DCD 都会生成一个 4 字节的二进制代码。中断向量表存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序。
, f) a) `# ~6 n9 O( C  ~
1 r8 B+ e. ?' A! Z第 5 部分代码分析% q) ]: u0 w, l
  u: G. b0 G+ }9 o

/ j* R$ B# O  n- L' G& I 微信图片_20221024154242.png
/ `+ a3 J8 ]5 X+ A9 z  P0 P) j7 h; y2 B
第 53 行:AREA 定义一块代码段,只读,段名字是 .text 。READONLY 表示只读。
5 X3 K* y4 D2 [  v$ i# U3 h& ?第 56 行:利用 PROC、ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。, t4 r* D) S1 H5 b9 {1 k+ D
第 57 行:WEAK 声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话会调用外面的。这个声明很重要,它让我们可以在 C 文件中任意地方放置中断服务程序,只要保证 C 函数的名字和向量表中的名字一致即可。
, t6 x5 `3 ~0 s: F9 m第 58 行:IMPORT:伪指令用于通知编译器要使用的标号在其他的源文件中定义。但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。* ?( z- V! s9 Y- i4 r
第 61 行:SystemInit()是一个标准的库函数,在 system_stm32f4xx.c这个库文件宏定义。主要作用是配置系统时钟,这里调用这个函数之后,F429的系统时钟配被配置为 180M。8 O3 S  b7 j  }; {- G# `) l
第 63 行:__main 标号表示 C/C++标准实时库函数里的一个初始化子程序__main 的入口地址。该程序的一个主要作用是初始化堆栈,并初始化映像文件,最后跳转到 C 程序中的 main 函数。这就解释了为何所有的 C 程序必须有一个 main 函数作为程序的起点。因为这是由 C/C++标准实时库所规定,并且不能更改。如果我们在这里不调用__main,那么程序最终就不会调用我们 C文件里面的 main,如果是调皮的用户就可以修改主函数的名称,然后在这里面 IMPORT 你写的主函数名称即可。这个时候你在 C文件里面写的主函数名称就不是 main 了,而是 __main 了。LDR、BLX、BX 是 CM4内核的指令:% Y9 K4 V/ U7 P( `/ Q) Z
微信图片_20221024154231.png
; Z) k: X. {. _" q# ]8 t+ J% `* C( Q7 ], A9 Z
第 6 部分代码分析
" {3 R/ A# x5 F6 {+ ]
* x3 H6 T) x2 j( A- z" B4 b
( |3 d6 k/ w7 G  P" o
微信图片_20221024154227.png . l* Y% `& ^4 i5 F0 z
第 71 行:死循环,用户可以在此实现自己的中断服务程序。不过很少在这里实现中断服务程序,一般多是在其它的 C 文件里面重新写一个同样名字的中断服务程序,因为这里是 WEEK 弱定义的。如果没有在其它文件中写中断服务器程序,且使能了此中断,进入到这里后,会让程序卡在这个地方。
, S5 v$ t& F& t) d0 o8 F第 81 行:缺省中断服务程序(开始)" ]$ R  @' b9 R6 s" x: t" E; Y3 Q0 g
第 92 行:死循环,如果用户使能中断服务程序,而没有在 C 文件里面写中断服务程序的话,都会进入到这里。比如在程序里面使能了串口 1 中断,而没有写中断服务程序 ART1_IRQHandle,那么串口中断来了,会进入到这个死循环。
; Q7 b& h6 w( M) a- D4 X第 94 行:缺省中断服务程序(结束)。
* C  {  d2 v# {( m4 K7 R/ O4 c0 |1 a
第 7 部分代码分析
% T& }2 b, O6 n+ T启动代码的最后一部分:4 z$ l2 x  J) i

+ r6 k- u: G: j 微信图片_20221024154223.png : ]; ~; Q1 }; E4 E0 Z
2 u5 b1 M' y* e' H6 |
第 101 行:简单的汇编语言实现 IF…ELSE…语句。如果定义了 MICROLIB,那么程序是不会执行 ELSE分支的代码。__MICROLIB 可能大家并不陌生,就在 MDK 的 Target Option 里面设置。: J  j( [5 C7 O/ Y- Y

  [1 A( Q; o: J4 i) k
微信图片_20221024154217.png
) J* c' g* }/ l  g# P. h3 Y( z
4 A4 I+ v0 e0 p

/ k# S: {0 w) `+ T5 n$ w' n. }局外话,这一步的配置工作很重要,很多人串口用不了 printf 函数,编译有问题,下载有问题,都是这个步骤的配置出了错。Target中选中微库“ Use MicroLib”,为的是在日后编写串口驱动的时候可以使用printf 函数。而且有些应用中如果用了 STM32 的浮点运算单元 FPU,一定要同时开微库,不然有时会出现各种奇怪的现象。FPU 的开关选项在微库配置选项下方的“Use Single Precision”中,默认是开的
8 q. _# A3 Z! P% `, H
9 g" a/ w* f9 a0 N3 k2 v' ]- S' T) S- H$ S3 t* a* [

' `+ B3 U+ r$ x! p
; k/ q; d2 `- x! T& N6 T1 a3 w1 n
, _1 C3 U. O9 S3 l& u! ^/ I% m1 \9 h) J& d2 T

+ ~. q- c) x6 l. L4 E: a' ?( V- z  L, O5 d. ~/ |
转载自: 果果小师弟
: X. y- e9 D) [
收藏 评论0 发布时间:2022-10-24 15:43

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版