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

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

[复制链接]
STMCU小助手 发布时间:2021-11-20 23:00
1.MCU 代码如何启动! _7 c* j& y, t% M" ~$ F. z7 q
首先我们需要澄清一个问题,什么是 Startup Code,什么是 Bootloader?因为总看到有同学混用这两个概念。) n4 R0 R- u% x" f6 |
- V0 M" q6 B* C+ D/ k
Bootloader 可以译为引导程序。早期的单片机是没有 Bootloader 这种概念的。如大家熟悉的 MCS51,最初芯片内是不能存储代码的,需要外挂EPROM,就是下面这种带个小玻璃窗的存储器。擦除 EPROM 中的代码需要用紫外线照射几分钟才行。/ S* _8 b6 m$ ?' Q% s1 o. V

( R; j  P( u' c3 O
20200207173108336.jpg

/ y: r) F* f, A; V9 B4 j* b7 W
' Z6 i! \$ _- I( P3 C5 l后来出现了 Flash 这种可电擦写的存储器,并集成在了单片机内部。但出厂的时候单片机的程序存储区仍然是空白的,没有任何代码。用户编译程序后,下载到单片机后才能运行。那么在产品发给用户后,如果发现有Bug怎么办呢?就得用编程器把新代码重新下载一次。这实在是有点儿麻烦,特别是如果客户距离很远的话。于是有聪明的程序猿想了一个办法,写一小段特殊的代码放在程序里,这段代码可以通过一定方式,比如用按键触发进入运行,它可以通过串口(早期的 PC 串口是标配)接收新的代码并写入Flash,从而在没有硬件编程器的情况下也能完成代码的更新。9 e' H7 R* t  u# z* _
+ V7 \$ k3 {6 a7 n: h0 t% T" ~
程序猿们也是现代历史前进的重要推动力啊!
0 J  j( ]2 |* h! L( J' Q2 Y( g6 b! W! k  _. Z
后来,有芯片厂商把这种代码在出厂时就固化在芯片里,极大的方便了代码下载和程序更新。STM32F030内部就固化了Bootloader。当我们把一个引脚 BOOT0 拉高的同时,重新给芯片上电或复位,就会触发Boootloader进入运行。此时我们通过单片机的串口就可以把新程序发送给单片机,发送完后把 BOOT0 拉低,再复位单片机,新程序就会运行起来。! k: \1 Z' T; V$ w4 ^

- W# o9 o1 ~' v  |- l( w0 KStartup Code 可以译为启动代码。单片机上电或复位后最先执行的一段代码。一般主要会完成堆栈指针的设置,复位向量的获取和加载,然后初始化变量,最后跳转到用户代码。在详细看启动代码之前,我们先看一下 STM32F030 的内存映射。
- v1 Y9 r( n. p. N5 |0 X, q, a. T2 o" m. ^( @! a
2.STM32F030内存映射(Memory Map)
/ w7 l$ j& s8 a2 A2 @9 i+ w% y下面是 STM32F030 的内存映射,其它芯片会因为 Flash,SRAM 空间大小不同而略有不同。- ~: N1 o) T5 r+ Z2 M8 P: t
+ c& {! e2 `& \) ^9 b0 i4 G
20200207173223663.png

; }- x$ m; t/ P' x) i
! {! l, E; {6 u8 `! y8 U: w1 ?2 Y因为是32位机,所以可寻址从 0x0000_0000 到 0xFFFF_FFFF 的总共 4G 空间。
' V( W8 U5 F  q: s. x& H+ ]  t5 L2 a% ]1 k, W$ m# F- P
这是采用32位机的好处,地址空间足够用。不像8位或16位机,很容易出现地址空间不够用,动不动就需要用 Page 来间接寻址。/ ?; I1 F& k6 k) t

% Q: }* W$ ]8 _0 P我们从低地址到高地址逐段看一下:
. {, n5 q5 y  |$ J$ q# W4 \) G# P" z+ }7 o6 u7 ?0 C
0x0000 0000 Virtual memory9 ]; y1 |  s7 I$ M2 K% T$ H
5 _8 E) H: _7 n
这段地址空间,会因为不同的 BOOT 模式而映射到不同的物理内存。8 f7 T, m( X( x: A" }' M* X9 _
/ N* ]0 Q. o. H
当芯片复位,或从 Standby 低功耗模式唤醒时:# `9 g/ X! f2 k. l# c

- ]: q, p6 e% P/ ?如果引脚 BOOT0 是被拉低的,将映射到 Flash memory。这是最常用的代码运行模式;1 r9 n& h' G( T0 Q: _! ?

+ k9 I. x( v7 c# ]+ k/ J如果引脚 BOOT0 是被拉高的,且nBOOT1为 1 ,将映射到 System memory。进入bootloader模式;/ m6 h' h4 \! `1 m7 }! h$ q$ T! {

+ _  G6 _# [- z# J4 O3 b( o* A" _如果引脚 BOOT0 是被拉高的,且nBOOT1为 0 ,将映射到 SRAM。
$ g2 p" ]4 P. m8 L+ C" h, g
- G% B* [* a( E% v注:nBOOT1 为Flash寄存器中的一位,用户何以设置。! p5 d* e2 K- v9 G* f' y

5 Q& A; g5 ]% A! x# Q0x0800 0000 Flash memory
/ G- c. N8 a/ D1 ~# l2 b/ X存放用户代码" n1 R1 _7 D$ l3 Z9 Z8 _# L
  I3 i6 k% w7 A3 U; D, w4 A/ Q
0x1FFF EC00 System memory* {& G" f  T6 a6 f
存放 bootloader, 片内集成温度传感器的校正数据,和片内集成电压参考的校正数据
) |$ r! K2 W0 M0 ~+ Y+ S9 B+ U. B- P这些代码和数据是在工厂固化好的。) `1 N9 p* |- a4 w
6 a! y/ s. B& Z' y$ E! \3 h% ^
0x2000 0000 SRAM
& @& E% @" w4 }' V  o7 X3 A4 h/ G存放用户变量,堆(Heap)和栈(Stack)。也可以把代码加载到 SRAM 运行。: t) S9 T. m9 _% L$ @
6 y# C; V/ Y2 w6 D
0x4000 0000 Pheriperals1 X9 b; P: U* k1 _
芯片集成的外设,如 USART, SPI, GPIO等的寄存器地址在这一区域。
" R2 ?# u$ p5 G" M5 ]
' b% s5 @$ x" ^6 o' n0xE000 0000 Cortex-M0 internal pheriperals
( O" f, K9 _8 o: h8 C; i' K4 sM0内核的外设映射到此区域。如 systick (System Tick),NVIC,Debug Registers。这些寄存器在芯片手册里是查不到的,需要到 ARM 的手册里查找。
7 C" {4 }' x5 D# G% ^1 ^0 @6 B# R. @5 `: z
3. 启动代码(Startup Code)8 T/ Y$ W$ i" g# z" `
我们还是以下面这个最简单的GPIO翻转代码为例:
7 X- o, {7 X; R$ u" {- ^! @3 m) HSTM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\
3 w* S2 N7 ]! dExamples\GPIO\GPIO_IOToggle\MDK-ARM\Project.uvprojx' ?6 D% l9 {# X  A0 O4 ~
把此工程下载到单片机后,用调试器观察下面两个地址的内容:
5 a  d' V5 o! n% G. J我们会发现0x0000_0000开始的区域, 和0x0800_0000开始的区域,内容完全相同。这说明Flash 区的内容映射到了 0x0000_0000起始的这一段地址区域。
2 p$ N! e- F3 u2 ^4 {- _) O) q6 E
20200207173410739.png
8 B: i+ _3 k7 e/ B

; Y, j2 g9 u2 t6 f- R注意STM32F030使用的是小端模式(Litlle Edian)。$ a) T/ `* g6 S0 P8 `
/ ], [( D1 d7 L
不同于 MCS51 在 0x0000 放的是复位向量,STM32F030 还有其它 ARM 芯片在零地址存放的是初始堆栈指针地址。$ o; I% k7 K9 r$ ?# `' C* s

8 y# W0 c9 j% u% y- X9 N9 g7 D+ w0x0000 0000: (0x2000 0428) 初始堆栈指针
# A7 v1 L1 a$ [' a# [, i, B+ W( K& m4 `) g
0x0000 0004: (0x0800 00C9) 复位向量,上电或复位后最先加载入PC8 t# J1 P, T5 L) Z0 _% o$ }( c" Y
1 L* S' @& b+ v- ]
注:单片机上电或复位后,堆栈指针初始化和 PC 初始值的加载总是从地址 0x0000_0000,0x0000_0004获取。在上面这种用户模式下,实际是从 Flash 区的 0x0800_0000,0x0800_0004 获取的。& D2 A4 }4 z3 ?& o* H5 M
2 `& j4 h, Q3 ~
我们可以通过调试器观察一下芯片复位后 M0 内核的寄存器:3 F8 \: [, [  T& P1 ?/ v0 t

! g  t) p1 h4 S+ j$ S! d
20200207173413306.png

3 w, G* Q& ]; N8 o3 {
) I6 s. w7 v, m* m( k; e6 E细心的同学这时可能发现了一个问题。
# l' F/ G1 b; E9 K5 G% Y
5 n( j! v3 k( T9 i/ d堆栈指针 SP 的内容和前面存储器中的内容是对的上的。但是 PC 里的内容好像对不上啊?PC 里的值是 0x0800_00C8,存储器里明明是 0x0800_00C9 啊!: }+ @  d9 R& M0 h) z3 {' z
: x. u# A. c% A1 X: F7 G
这里牵涉到了 ARM 体系里的两种工作状态 ARM 和 Thumb。ARM 状态下执行32位指令,Thumb状态下执行16位指令。那么如何在这两者之间切换呢,一个方法就是靠跳转地址的最低位(Bit0), 当 Bit0 设为 1 时进入 Thumb 状态,当 Bit0 设为 0 时进入 ARM 状态。
/ A4 ?! b5 A1 }. j1 T/ l6 g9 r/ W. u6 }6 A
对于单片机来说,16位的 Thumb 指令就足够了,而且16位指令比32位指令能节省存储器空间。所以 M0 内核只支持 Thumb 指令。
+ v0 B) h" p+ H/ E2 w3 s
2 E% s% T) ?: ~$ Y: d. d  a到这里我们就可以理解复位向量为什么是 0x0800_00C9 了。
5 N. I3 q& U4 U2 H- x, }" ~/ N! `. D; b5 w
接下来我们来看复位向量 0x0800_00C8 指向的第一条指令:
& ^: r2 ^: ^) G7 [; c
5 s3 |& C5 M* }8 z2 u7 H0 U9 B
20200207173440317.png

. N- X; b" q8 S2 _5 B9 e0 |% g# O, d, Y) q* M7 l% H7 V/ F
+ s. }. K% E7 ^4 `7 h' V* e% y7 Q

! q+ ^. [1 A! o) E8 u* E  N单片机将要执行的第一条指令 0x4804,这是什么意思呢?
" G* n6 X) y3 l! O6 n5 |( _' o. c  ?
先说结论:它就是下图中,单片机复位后光标指向的这条指令:9 j& @% g/ G  K# i: J
" [- q/ u/ z( X& j% e
LDR     R0, =SystemInit
8 ^+ v! m6 O  U, e8 n5 Z2 ?9 ~* _& u6 U$ q0 \
20200207173500363.png

: _# l1 t/ j. p3 \8 }4 \( L
5 B( X# B' _4 q& V( ~2 b
6 @+ Q! I' ^% w& `0 Q( J$ B3 |# f# |: [9 f1 O4 g
在这里详细解释一下 0x4804 这条指令:
& K- N4 E% c5 [/ x; a* w% h% T1 B/ k7 o
它对应的机器码是 01001000000001002 V, j# C4 o3 X# `( a

5 ~! `/ u" w- CBit15 to Bit11 (01001)为LDR(literal)指令,既从PC偏移地址取数据送至寄存器Rt。4 e* q7 _% D. Z9 M9 x
# ]+ E$ k% `  Z. p8 l
Bit10 to Bit8  (000)表明目的寄存器Rt为 R0
3 ?3 n1 W/ g6 o. E, A2 i  H( U- D# ]1 G8 h
Bit7 to Bit0  (00000100)表明相对于 PC 的偏移量为 0b10000,既0x10。
+ n' a; D: A6 v  M* Y$ A5 H: R8 i! H0 Y- P+ X* l) E6 k/ E; n, M0 X
注意PC的值是当前地址+4。
. i( D( a# C4 L% F
& j+ Q& i8 f) h7 B& q那么从 0x080000C8 + 0x4 + 0x10 = 0x080000DC 取出数据 0x0800092D 送至寄存器 R0。此地址是 SystemInit( )函数的地址。下一条语句 BLX R0 就是调用此系统初始化函数。
1 n* l- b( A) W1 @5 L+ D
" T6 {5 H) c$ W0 ^- M+ gSystemInit( ) 这个函数在 system_stm32f0xx.c 这个文件里,主要完成系统时钟的初始化。可以点进去看一下具体的内容。9 y" ^0 \- {1 T7 ?- }

6 L3 D+ h( m! v- A8 ^( H
20200207173508532.png

7 y8 h" a7 ~/ V& ]' d2 L7 W, P  d
& h6 v5 F) V/ `- s1 @  E7 a% _1 X& `: Y: @
* Q* K$ }+ K7 }, l, H
函数 SystemInit( ) 执行完之后,程序跳转回来,取得 __main( ) 函数的地址,跳转到 __main() 函数执行。需要注意,这个函数不是我们用户代码里的 main( ) 函数。: l2 z  i7 {) T9 q

0 j! N0 w4 s8 C2 |__main() 函数是 Keil 的库提供的,我们看不到代码,它主要完成变量的初始化。这里不用太纠结,如果想进一步深究可以看一下 ARM Compiler User Guide 的 Reset and initialization 这一节。
8 G/ ^/ H0 E! @7 v
! o  ~5 ^6 z0 y7 w7 f__main() 函数执行完,基本工作就做完了,这才跳转到用户代码的 main( ) 函数。& X# j: f2 f6 {! ~8 ~/ z

8 q4 }) B) y) \; T7 m
" K2 J& {4 M( K* C
. H& q+ H$ L- B7 X; [' b+ \  |
20200723225010732.png
赞 收藏 评论0 发布时间:2021-11-20 23:00

举报

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