请选择 进入手机版 | 继续访问电脑版

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

【经验分享】STM32F0单片机快速入门三 MCU启动过程

[复制链接]
STMCU小助手 发布时间:2021-11-20 23:00
1.MCU 代码如何启动
8 r) [# s' ~* x( V. k首先我们需要澄清一个问题,什么是 Startup Code,什么是 Bootloader?因为总看到有同学混用这两个概念。
) Q9 ]  r$ I( s' A- q* _/ }5 {2 Q0 d/ Y' m! J' h8 W, @! D
Bootloader 可以译为引导程序。早期的单片机是没有 Bootloader 这种概念的。如大家熟悉的 MCS51,最初芯片内是不能存储代码的,需要外挂EPROM,就是下面这种带个小玻璃窗的存储器。擦除 EPROM 中的代码需要用紫外线照射几分钟才行。$ S  P: H7 q; @# B& S1 H/ ?- E, o! ~
- I/ L1 J$ \) a. B  j. M
20200207173108336.jpg

& U" n( U; w$ J1 w7 B; w1 j4 N8 d: k0 G( C; \2 B
后来出现了 Flash 这种可电擦写的存储器,并集成在了单片机内部。但出厂的时候单片机的程序存储区仍然是空白的,没有任何代码。用户编译程序后,下载到单片机后才能运行。那么在产品发给用户后,如果发现有Bug怎么办呢?就得用编程器把新代码重新下载一次。这实在是有点儿麻烦,特别是如果客户距离很远的话。于是有聪明的程序猿想了一个办法,写一小段特殊的代码放在程序里,这段代码可以通过一定方式,比如用按键触发进入运行,它可以通过串口(早期的 PC 串口是标配)接收新的代码并写入Flash,从而在没有硬件编程器的情况下也能完成代码的更新。# a0 N, ?; q: F8 o2 x! Q8 c
' C! d9 M* R1 K. G0 ?1 E) N  t
程序猿们也是现代历史前进的重要推动力啊!
/ q2 d  O2 ^9 N7 L- C- B/ c6 U( j9 _+ t& l' v! i4 q9 U# `) ]
后来,有芯片厂商把这种代码在出厂时就固化在芯片里,极大的方便了代码下载和程序更新。STM32F030内部就固化了Bootloader。当我们把一个引脚 BOOT0 拉高的同时,重新给芯片上电或复位,就会触发Boootloader进入运行。此时我们通过单片机的串口就可以把新程序发送给单片机,发送完后把 BOOT0 拉低,再复位单片机,新程序就会运行起来。2 |$ J: q$ U" _# N. o4 V+ V
6 @4 m- W9 c4 V" L
Startup Code 可以译为启动代码。单片机上电或复位后最先执行的一段代码。一般主要会完成堆栈指针的设置,复位向量的获取和加载,然后初始化变量,最后跳转到用户代码。在详细看启动代码之前,我们先看一下 STM32F030 的内存映射。, Y+ c; w7 b$ d$ ?+ _3 T# ]

5 m+ q, B  p( |# q$ z& S2.STM32F030内存映射(Memory Map)1 ~% k: ]5 u% I& N4 e
下面是 STM32F030 的内存映射,其它芯片会因为 Flash,SRAM 空间大小不同而略有不同。6 T  @5 o- y: Y' P1 k' Z( r7 `

  d4 [; W* q( i" ^0 z4 H
20200207173223663.png

! C; r: r2 f; D: D+ e
/ l+ W5 Z' s8 S5 L0 h6 t因为是32位机,所以可寻址从 0x0000_0000 到 0xFFFF_FFFF 的总共 4G 空间。
+ A# V4 R* ^5 ?+ Z5 v8 p# n
$ ~$ K* }# {! `1 f# h! ?这是采用32位机的好处,地址空间足够用。不像8位或16位机,很容易出现地址空间不够用,动不动就需要用 Page 来间接寻址。' Q" z- F9 v) y' f0 q) V+ W8 Q: B
2 W4 }8 J$ B0 `7 E' z; Z
我们从低地址到高地址逐段看一下:) {5 S+ I! z2 ^) A  l* \) {

% b3 q( V5 f  u0x0000 0000 Virtual memory  H* a3 T* f. f7 \( j" ~
+ l. |. [8 @- R; B4 V
这段地址空间,会因为不同的 BOOT 模式而映射到不同的物理内存。
; q) i; l& e/ K# h5 h  r$ Q
7 [9 M9 ?& h: x, u3 L) g当芯片复位,或从 Standby 低功耗模式唤醒时:
/ k, f4 w; n' y" _1 z1 [: m  [1 I4 l5 q
如果引脚 BOOT0 是被拉低的,将映射到 Flash memory。这是最常用的代码运行模式;
& U8 n4 I; m" n% A& X# ?$ I
; G7 r1 K4 Z0 z如果引脚 BOOT0 是被拉高的,且nBOOT1为 1 ,将映射到 System memory。进入bootloader模式;# ~: X+ T8 W+ U7 s0 N
2 Y  L) U! K& s+ Q( r
如果引脚 BOOT0 是被拉高的,且nBOOT1为 0 ,将映射到 SRAM。
4 V6 Q( C: I. E: @
6 v6 K4 }- i9 |; d( b, a1 L3 }注:nBOOT1 为Flash寄存器中的一位,用户何以设置。" _1 ]! N/ v; x" H7 ~6 M
" I; a9 I4 ?: W3 C
0x0800 0000 Flash memory4 m- j- u: L5 f
存放用户代码, T- ?" ]0 w. b: i6 M

: z: y( U5 F" V: f, _, d* L+ x# w0x1FFF EC00 System memory3 ]' K5 b) ~. H" J# T
存放 bootloader, 片内集成温度传感器的校正数据,和片内集成电压参考的校正数据
1 u# X2 v5 T/ r7 L6 t1 ]+ M2 X这些代码和数据是在工厂固化好的。
8 ]* |# a/ k) @- ~- O) x# G+ e+ V7 \5 }( w& l, _( T, Z" X
0x2000 0000 SRAM! W/ Y2 n( Q2 @7 d4 t: ]
存放用户变量,堆(Heap)和栈(Stack)。也可以把代码加载到 SRAM 运行。
# x- Z8 m( e' ]: [% j: _0 A$ `0 Z% ]) h, Y, ^4 ^$ y  b  Z) b
0x4000 0000 Pheriperals
  F+ U/ p) G+ |9 R' m" ^3 P! s芯片集成的外设,如 USART, SPI, GPIO等的寄存器地址在这一区域。
, B, K  y0 @5 A: ^. \, w
% t/ R" Q9 X- Q5 d) l+ x  _0xE000 0000 Cortex-M0 internal pheriperals
9 y* j; U4 C# S0 s; Y! A* g. sM0内核的外设映射到此区域。如 systick (System Tick),NVIC,Debug Registers。这些寄存器在芯片手册里是查不到的,需要到 ARM 的手册里查找。
% }1 d2 D+ _  P$ B6 g
1 w4 |' @1 r+ M5 U3. 启动代码(Startup Code)
2 h" E# \) [& g1 i- a) L; w我们还是以下面这个最简单的GPIO翻转代码为例:+ r8 ^- m# t& f& O8 C6 H1 J
STM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\
* P: @  V/ @' X- lExamples\GPIO\GPIO_IOToggle\MDK-ARM\Project.uvprojx$ T7 V2 X" [* m+ T
把此工程下载到单片机后,用调试器观察下面两个地址的内容:
) Q0 o6 ~  a' V$ `, c' I3 b我们会发现0x0000_0000开始的区域, 和0x0800_0000开始的区域,内容完全相同。这说明Flash 区的内容映射到了 0x0000_0000起始的这一段地址区域。' x5 L; G. G* b9 j% |3 Y& n
5 w/ u& l- e: l
20200207173410739.png

0 {. M6 y9 x- t2 n# `2 J. i) h/ o4 M& A& d
注意STM32F030使用的是小端模式(Litlle Edian)。% C! M4 C* Q/ h- f/ F, q
+ J$ [& `9 Y0 u" {/ S2 Y
不同于 MCS51 在 0x0000 放的是复位向量,STM32F030 还有其它 ARM 芯片在零地址存放的是初始堆栈指针地址。
* E$ S+ O& s- `' ]6 [9 m* T! o/ t+ P1 I$ |$ X' F2 h
0x0000 0000: (0x2000 0428) 初始堆栈指针) T" N: s4 p4 R* i3 F) N
% d8 P* H6 @0 A: r" e. R- T
0x0000 0004: (0x0800 00C9) 复位向量,上电或复位后最先加载入PC
/ q) h4 I: a  ]4 R, Z) E
* e  B8 l/ T+ T0 [& R. c注:单片机上电或复位后,堆栈指针初始化和 PC 初始值的加载总是从地址 0x0000_0000,0x0000_0004获取。在上面这种用户模式下,实际是从 Flash 区的 0x0800_0000,0x0800_0004 获取的。
2 i5 j( a2 L+ s2 S, q! z- b' q# N0 W8 `1 X# a
我们可以通过调试器观察一下芯片复位后 M0 内核的寄存器:. ?+ u3 T4 |( q. R  @- o

. h8 z/ Y1 b$ B, G) P( F. k  C
20200207173413306.png
8 e" ^! \1 u2 ?: s

# {/ n+ h! S; e% l4 ^+ I细心的同学这时可能发现了一个问题。
, a: @1 H8 [1 l  @. Z; I* t3 \! Q; w9 W" Q& {  ], ~/ l
堆栈指针 SP 的内容和前面存储器中的内容是对的上的。但是 PC 里的内容好像对不上啊?PC 里的值是 0x0800_00C8,存储器里明明是 0x0800_00C9 啊!( P+ J: }! a4 d/ K/ X6 v
6 U5 [4 U1 f7 H- Z- L6 [- b+ U
这里牵涉到了 ARM 体系里的两种工作状态 ARM 和 Thumb。ARM 状态下执行32位指令,Thumb状态下执行16位指令。那么如何在这两者之间切换呢,一个方法就是靠跳转地址的最低位(Bit0), 当 Bit0 设为 1 时进入 Thumb 状态,当 Bit0 设为 0 时进入 ARM 状态。
; L0 H9 Q% u$ q/ \3 V% O% Z! b: h& v# H4 N, ^! [. e
对于单片机来说,16位的 Thumb 指令就足够了,而且16位指令比32位指令能节省存储器空间。所以 M0 内核只支持 Thumb 指令。
& l9 H9 ]7 Y0 t3 b  a* a
6 y+ `2 d0 W+ P% K& K5 w6 ?# I+ G到这里我们就可以理解复位向量为什么是 0x0800_00C9 了。0 h+ v, ?9 Q# q0 O* T9 t  {

5 {! n. Q6 g$ N2 e3 K# E$ M接下来我们来看复位向量 0x0800_00C8 指向的第一条指令:8 I1 w9 w. m( S6 L0 W6 h
! Q6 m! l$ K5 b. q" n/ @( O+ o  r
20200207173440317.png
* K  Y& C9 S. u( d7 b" R
# o- Q6 n8 N# [

. L4 m' u9 e( M* ]* z; X1 m: {
单片机将要执行的第一条指令 0x4804,这是什么意思呢?
6 a6 Y  j# ]. ^% M( w8 V1 T" v: D! N0 l& t0 Y9 d5 T$ ?( y2 M
先说结论:它就是下图中,单片机复位后光标指向的这条指令:' ?* I9 ?9 \. e/ a2 M: T/ H, z/ G
' ?( d. ?$ N; F% c4 W
LDR     R0, =SystemInit
8 E! ?# e- c; V2 O+ _- D% ]
% W6 }! A: i# t1 B' O+ e
20200207173500363.png
8 E' s, L* X: S6 }

6 B: y, r+ Y$ r- w% P6 m' u
0 O, K: k) z; l% b  n* T) C- _3 s
7 ?5 K' @9 w3 }' h) y在这里详细解释一下 0x4804 这条指令:3 D8 j' Y  Q% N' T+ K1 x

  A& h$ n$ K; }2 b它对应的机器码是 0100100000000100
+ {. }8 x3 J- i. Y; x
: f- Q) U* B/ g0 ^7 o1 ?# BBit15 to Bit11 (01001)为LDR(literal)指令,既从PC偏移地址取数据送至寄存器Rt。
) u& J' A+ r3 v8 u9 b7 g) N, A8 E8 ]! A
Bit10 to Bit8  (000)表明目的寄存器Rt为 R0: Q9 h) Z6 z, c. f, U* ~1 V) W6 t- i

2 G: ^2 U1 B4 j2 n2 Y5 Y/ c. vBit7 to Bit0  (00000100)表明相对于 PC 的偏移量为 0b10000,既0x10。; o& G! H* ]' F  _# x9 L( F. p" n
7 Y/ y6 h  g) ?' g
注意PC的值是当前地址+4。: R4 G. [! ?3 }' f* i3 J$ D
6 F0 e2 Q% Y) q
那么从 0x080000C8 + 0x4 + 0x10 = 0x080000DC 取出数据 0x0800092D 送至寄存器 R0。此地址是 SystemInit( )函数的地址。下一条语句 BLX R0 就是调用此系统初始化函数。$ z9 T5 j6 B* B( {9 K1 J: ]) x( G1 {

& [% T4 o* a- }$ a/ X- ]$ USystemInit( ) 这个函数在 system_stm32f0xx.c 这个文件里,主要完成系统时钟的初始化。可以点进去看一下具体的内容。, q/ G: K; U7 ^

' w( t5 p6 W5 T. `: Q- e
20200207173508532.png

/ P9 p( S4 w/ n/ n0 H5 Q/ n) B( g5 L4 l  s5 U0 d- u9 A! U
$ ]' d7 x* z/ m0 _3 I3 b

  C; G+ {& v) [2 a. u函数 SystemInit( ) 执行完之后,程序跳转回来,取得 __main( ) 函数的地址,跳转到 __main() 函数执行。需要注意,这个函数不是我们用户代码里的 main( ) 函数。. \% \3 ^' c. n# t
4 t4 h# [& r* }8 S0 D( T
__main() 函数是 Keil 的库提供的,我们看不到代码,它主要完成变量的初始化。这里不用太纠结,如果想进一步深究可以看一下 ARM Compiler User Guide 的 Reset and initialization 这一节。: g7 h- Z& Q  R! B; _

; E" ~  r- J7 S" t__main() 函数执行完,基本工作就做完了,这才跳转到用户代码的 main( ) 函数。. l2 b" H7 f, x( s& P0 z% b3 Y$ |
% m% l, j: f6 P: l0 y' m  B! `

) |) c  Y: F1 i& Q
1 r! O( r# }( F5 y6 M. z
20200723225010732.png
收藏 评论0 发布时间:2021-11-20 23:00

举报

0个回答

所属标签

相似分享

关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新和工艺
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版