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

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

[复制链接]
STMCU小助手 发布时间:2021-11-20 23:00
1.MCU 代码如何启动- {" c* b# m% Z3 ]& U: O: H! q
首先我们需要澄清一个问题,什么是 Startup Code,什么是 Bootloader?因为总看到有同学混用这两个概念。
+ y, q8 E0 S+ z  i$ [
2 z6 ?9 k- q5 B0 C- s/ oBootloader 可以译为引导程序。早期的单片机是没有 Bootloader 这种概念的。如大家熟悉的 MCS51,最初芯片内是不能存储代码的,需要外挂EPROM,就是下面这种带个小玻璃窗的存储器。擦除 EPROM 中的代码需要用紫外线照射几分钟才行。1 t. ~( X  L- d7 A+ f# W) r# g8 g6 H
6 _. b) }* e2 f) i: `4 s. K
20200207173108336.jpg

  O6 ]& i+ x# K# w' G$ h( C  k8 m% P
9 O& Q+ _& h. S后来出现了 Flash 这种可电擦写的存储器,并集成在了单片机内部。但出厂的时候单片机的程序存储区仍然是空白的,没有任何代码。用户编译程序后,下载到单片机后才能运行。那么在产品发给用户后,如果发现有Bug怎么办呢?就得用编程器把新代码重新下载一次。这实在是有点儿麻烦,特别是如果客户距离很远的话。于是有聪明的程序猿想了一个办法,写一小段特殊的代码放在程序里,这段代码可以通过一定方式,比如用按键触发进入运行,它可以通过串口(早期的 PC 串口是标配)接收新的代码并写入Flash,从而在没有硬件编程器的情况下也能完成代码的更新。
5 [0 p/ _- N, v8 r0 I% q3 c# `( w( h. \" z3 l. M0 y
程序猿们也是现代历史前进的重要推动力啊!$ q: Q8 E8 J: D( N+ E1 g! {3 k: f
, a7 s3 g3 r9 S* h+ n& h
后来,有芯片厂商把这种代码在出厂时就固化在芯片里,极大的方便了代码下载和程序更新。STM32F030内部就固化了Bootloader。当我们把一个引脚 BOOT0 拉高的同时,重新给芯片上电或复位,就会触发Boootloader进入运行。此时我们通过单片机的串口就可以把新程序发送给单片机,发送完后把 BOOT0 拉低,再复位单片机,新程序就会运行起来。
+ y1 c  ?3 Y) z- r  k  S
' d2 P. h1 u! g) X5 w  j+ YStartup Code 可以译为启动代码。单片机上电或复位后最先执行的一段代码。一般主要会完成堆栈指针的设置,复位向量的获取和加载,然后初始化变量,最后跳转到用户代码。在详细看启动代码之前,我们先看一下 STM32F030 的内存映射。3 k3 h/ h' x- A9 u

; A) e) u6 e' K$ \8 K1 r; M: i2.STM32F030内存映射(Memory Map)
0 Y; {: Y, H/ r2 ~下面是 STM32F030 的内存映射,其它芯片会因为 Flash,SRAM 空间大小不同而略有不同。
% ~  b3 B$ |% P0 ]3 `5 |6 d, \
2 Z! ^( B! D' u5 d/ q
20200207173223663.png
& q- u* F4 `. S" T

7 q- K5 d. M4 J" B) H因为是32位机,所以可寻址从 0x0000_0000 到 0xFFFF_FFFF 的总共 4G 空间。
5 {/ }6 L$ R/ ~- s9 L& W$ q( \3 f! Z0 u' f
这是采用32位机的好处,地址空间足够用。不像8位或16位机,很容易出现地址空间不够用,动不动就需要用 Page 来间接寻址。- r( y, ]( e1 {  P& h7 u
& |! F/ @! y& E  L7 @
我们从低地址到高地址逐段看一下:
2 {" V) ]2 I7 c: {$ X2 j
" o$ {- A5 L' }  ]0x0000 0000 Virtual memory
  Q' _: i5 X$ L- D* v5 a: V
3 G) P* V5 N  N  R7 z, a" k这段地址空间,会因为不同的 BOOT 模式而映射到不同的物理内存。
* U# F* w; h# v5 V. G! E
1 {0 V) G( K1 }! X1 a/ r2 S当芯片复位,或从 Standby 低功耗模式唤醒时:; S: G8 S  e  O

. W( B; H; C6 a) `0 o7 V. K如果引脚 BOOT0 是被拉低的,将映射到 Flash memory。这是最常用的代码运行模式;
9 {$ U) ?% a+ O8 K1 [; k/ i) w- J3 Z& x& Q" f5 `7 w% {
如果引脚 BOOT0 是被拉高的,且nBOOT1为 1 ,将映射到 System memory。进入bootloader模式;/ {: e8 P; I3 x

# e. |  W( p2 c: e, i如果引脚 BOOT0 是被拉高的,且nBOOT1为 0 ,将映射到 SRAM。. f4 q6 q0 n$ w) t# e% m

# `7 n( X; y, O8 q/ ~( R+ e注:nBOOT1 为Flash寄存器中的一位,用户何以设置。9 b( \/ Y  u" q4 I7 y: q

0 g+ ~3 k! g# |5 D% w0x0800 0000 Flash memory
8 e9 m1 p# _4 U9 h存放用户代码! r# ?3 k! O- D% x

! a6 I/ R8 p! M. w" j) _0x1FFF EC00 System memory# C$ {9 v) p1 m; w9 w
存放 bootloader, 片内集成温度传感器的校正数据,和片内集成电压参考的校正数据
+ T- R/ E  A+ O# |这些代码和数据是在工厂固化好的。
9 U! [% S0 [! a  a3 `
/ h) {9 s! K; w: I) X8 i# u0x2000 0000 SRAM
! u8 `/ h# R+ \' `/ @5 b存放用户变量,堆(Heap)和栈(Stack)。也可以把代码加载到 SRAM 运行。8 D2 `: E. y! n* Q" i2 K% n
" m  C0 f( }% A1 r
0x4000 0000 Pheriperals
) d) u0 N9 V& b  g, a芯片集成的外设,如 USART, SPI, GPIO等的寄存器地址在这一区域。
; l4 D: _6 |# N  E! u6 ]( p# Y4 P% E/ M' g( p
0xE000 0000 Cortex-M0 internal pheriperals7 z# ?' G4 S. ?! S
M0内核的外设映射到此区域。如 systick (System Tick),NVIC,Debug Registers。这些寄存器在芯片手册里是查不到的,需要到 ARM 的手册里查找。
7 ~3 d0 b2 S$ D4 j' U* |. g& y+ ]0 Q0 L  Q1 F$ S! t: v2 K* {
3. 启动代码(Startup Code)
9 f- b4 a% e7 i( U我们还是以下面这个最简单的GPIO翻转代码为例:
3 ?& F$ L6 f& @& BSTM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\
  d. S& U; d+ G2 x& W5 n+ q* p$ wExamples\GPIO\GPIO_IOToggle\MDK-ARM\Project.uvprojx
7 h/ p7 L; l# D. z! M( J  o把此工程下载到单片机后,用调试器观察下面两个地址的内容:, B( A& I4 G, G- t# l, O. L
我们会发现0x0000_0000开始的区域, 和0x0800_0000开始的区域,内容完全相同。这说明Flash 区的内容映射到了 0x0000_0000起始的这一段地址区域。7 `1 p) W* x$ |0 [; D
7 n: {& z$ @8 R
20200207173410739.png

4 Q/ o3 P; ]4 i1 {, i$ V3 m# E- ^% m/ o1 ^5 X9 u; i1 S
注意STM32F030使用的是小端模式(Litlle Edian)。
  `+ S6 h% X. ^, o3 \- @) y/ g' p$ x7 c8 _8 v7 b
不同于 MCS51 在 0x0000 放的是复位向量,STM32F030 还有其它 ARM 芯片在零地址存放的是初始堆栈指针地址。/ Y' j, `$ |2 O6 w- w. [/ k

  g+ p( M6 t$ d* T: K6 _( M0x0000 0000: (0x2000 0428) 初始堆栈指针
7 u% k# T  C: a* t; R, K) K7 @8 \: _+ s) Y; H
0x0000 0004: (0x0800 00C9) 复位向量,上电或复位后最先加载入PC
- P* H9 C% I: N* |2 E, ^
2 _7 f2 }, ^' h2 S0 P! ]: Z+ l注:单片机上电或复位后,堆栈指针初始化和 PC 初始值的加载总是从地址 0x0000_0000,0x0000_0004获取。在上面这种用户模式下,实际是从 Flash 区的 0x0800_0000,0x0800_0004 获取的。
8 a: v, y- k* B: S  S" g& ^
+ M/ r8 W# p4 D' ^我们可以通过调试器观察一下芯片复位后 M0 内核的寄存器:: r% {. H9 X/ r5 H8 F

/ o% l' n9 `2 l" r' o2 o% ]+ Q5 |
20200207173413306.png
1 B. J8 M# L% d. }: X
6 b: X) b" \6 t% S  o
细心的同学这时可能发现了一个问题。
9 W  Q7 b7 h- W0 V( U) y6 Y8 t  s: g8 I( T9 Z7 _' u" F. Q
堆栈指针 SP 的内容和前面存储器中的内容是对的上的。但是 PC 里的内容好像对不上啊?PC 里的值是 0x0800_00C8,存储器里明明是 0x0800_00C9 啊!. G6 \1 m# Q4 U. |  R$ ^* u

' d* R8 n  t3 R& U; S6 `这里牵涉到了 ARM 体系里的两种工作状态 ARM 和 Thumb。ARM 状态下执行32位指令,Thumb状态下执行16位指令。那么如何在这两者之间切换呢,一个方法就是靠跳转地址的最低位(Bit0), 当 Bit0 设为 1 时进入 Thumb 状态,当 Bit0 设为 0 时进入 ARM 状态。
: ^4 D" O4 B4 m4 [- n) w7 }1 H* A+ g- [7 \8 q
对于单片机来说,16位的 Thumb 指令就足够了,而且16位指令比32位指令能节省存储器空间。所以 M0 内核只支持 Thumb 指令。+ N* @0 r% M, r8 |% F

5 }% R8 E9 y0 Z$ U到这里我们就可以理解复位向量为什么是 0x0800_00C9 了。" L  s( g% {. R3 L
; A1 y) E; O4 H/ K
接下来我们来看复位向量 0x0800_00C8 指向的第一条指令:
! v, H, s, u7 k* S0 @- [) h8 w. x, N! {) \1 C1 J0 n" j
20200207173440317.png
/ l* i! X. B$ w3 I5 J: R
0 ]8 p% w8 C; b  }4 |4 z" y

: L( ?6 [% z: U# N: k3 M/ Q+ K8 I! J/ t5 J, \8 c0 o( P* k5 o7 g* [# p# f
单片机将要执行的第一条指令 0x4804,这是什么意思呢?8 q8 {" F5 P, ^3 a% B( A( ~( y
" q/ L0 J  V+ M* M/ n! h: x
先说结论:它就是下图中,单片机复位后光标指向的这条指令:/ G3 z7 E4 L/ U% U

) G  j( z( k/ C: q- `8 ?3 h$ l% OLDR     R0, =SystemInit& z% U7 ~/ t) f9 l
9 o, M+ x3 X: F8 `
20200207173500363.png
4 W3 y: k% ~( }" l; Z
4 o1 g# v, l" O
& Y: j, y- P1 z: g* _

  K- t8 |; N# i7 V( G9 B8 X. f' w在这里详细解释一下 0x4804 这条指令:& y& s$ k' `# k( R7 n
$ A% X/ {: a; N5 P% ]* E& K
它对应的机器码是 0100100000000100
, w% h4 J" K6 \9 ]* ^! e, D+ T# K8 a- H% A
Bit15 to Bit11 (01001)为LDR(literal)指令,既从PC偏移地址取数据送至寄存器Rt。
& G- q3 S2 Q" H( o7 h+ A; O; i- x+ E1 L+ Z4 D8 S; ~7 Z& b- `
Bit10 to Bit8  (000)表明目的寄存器Rt为 R0
) U8 M6 S& u7 T5 A
5 R9 r: q. Z+ B  Z2 u8 b7 uBit7 to Bit0  (00000100)表明相对于 PC 的偏移量为 0b10000,既0x10。" J/ J7 l: N: p9 t% n% _

, H+ g2 V8 H, D! G8 U/ C' A7 d注意PC的值是当前地址+4。! B# V6 `* ~7 x% ?- k# _# s2 R

8 y' h4 z7 c! X  O: v6 r那么从 0x080000C8 + 0x4 + 0x10 = 0x080000DC 取出数据 0x0800092D 送至寄存器 R0。此地址是 SystemInit( )函数的地址。下一条语句 BLX R0 就是调用此系统初始化函数。5 ~  T. d- y2 a+ v% T% U, e8 X

) g; N. j0 H; |/ T. `SystemInit( ) 这个函数在 system_stm32f0xx.c 这个文件里,主要完成系统时钟的初始化。可以点进去看一下具体的内容。# l& A0 L  `) T
( H+ B7 i/ U9 D& X
20200207173508532.png
6 h4 P  u$ z. P

1 h$ Y2 S/ N- s
! y. N1 L5 Y9 a3 K
' M( ?4 D( i  A0 _. D' p函数 SystemInit( ) 执行完之后,程序跳转回来,取得 __main( ) 函数的地址,跳转到 __main() 函数执行。需要注意,这个函数不是我们用户代码里的 main( ) 函数。
) G" N$ U/ G5 k( f5 O
, k& H" B* M  W% b6 C  l__main() 函数是 Keil 的库提供的,我们看不到代码,它主要完成变量的初始化。这里不用太纠结,如果想进一步深究可以看一下 ARM Compiler User Guide 的 Reset and initialization 这一节。
8 E9 G" C4 {, `* ]7 q& a+ L) v7 F! g1 A2 k4 x; `% o
__main() 函数执行完,基本工作就做完了,这才跳转到用户代码的 main( ) 函数。
& b4 h, i9 e" }  L( e- {7 [$ i/ ]1 D% g& x% c9 _2 }

5 i' g* ~  n' W- M4 P) M
1 X3 a( j- r6 j/ W
20200723225010732.png
收藏 评论0 发布时间:2021-11-20 23:00

举报

0个回答

所属标签

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