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

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

[复制链接]
STMCU小助手 发布时间:2021-11-20 23:00
1.MCU 代码如何启动" v% x' B( f" z8 I
首先我们需要澄清一个问题,什么是 Startup Code,什么是 Bootloader?因为总看到有同学混用这两个概念。+ K  }* E3 i. l9 z* e: S5 Y' G
5 {" p2 G4 s  p# n4 Y# m
Bootloader 可以译为引导程序。早期的单片机是没有 Bootloader 这种概念的。如大家熟悉的 MCS51,最初芯片内是不能存储代码的,需要外挂EPROM,就是下面这种带个小玻璃窗的存储器。擦除 EPROM 中的代码需要用紫外线照射几分钟才行。( H) C, w$ T% W9 U* [. @2 _: v

  B8 `3 n) n& J3 A7 Y" @
20200207173108336.jpg

% m# `% x% C9 ?* |% [% I1 m3 \4 a0 O( w; i1 U
后来出现了 Flash 这种可电擦写的存储器,并集成在了单片机内部。但出厂的时候单片机的程序存储区仍然是空白的,没有任何代码。用户编译程序后,下载到单片机后才能运行。那么在产品发给用户后,如果发现有Bug怎么办呢?就得用编程器把新代码重新下载一次。这实在是有点儿麻烦,特别是如果客户距离很远的话。于是有聪明的程序猿想了一个办法,写一小段特殊的代码放在程序里,这段代码可以通过一定方式,比如用按键触发进入运行,它可以通过串口(早期的 PC 串口是标配)接收新的代码并写入Flash,从而在没有硬件编程器的情况下也能完成代码的更新。
1 R, O. G" `; c3 S' m! I4 ^' I
/ w' ?, ?+ U0 T! `4 }6 ?, v 程序猿们也是现代历史前进的重要推动力啊!
  Z) h, Y# d5 T& C( R; A0 B8 X1 h5 p; J8 ~9 Y
后来,有芯片厂商把这种代码在出厂时就固化在芯片里,极大的方便了代码下载和程序更新。STM32F030内部就固化了Bootloader。当我们把一个引脚 BOOT0 拉高的同时,重新给芯片上电或复位,就会触发Boootloader进入运行。此时我们通过单片机的串口就可以把新程序发送给单片机,发送完后把 BOOT0 拉低,再复位单片机,新程序就会运行起来。
! t" t' Y7 ?$ Z- y' V+ s) n& W, K( T' d" H2 h/ K7 g
Startup Code 可以译为启动代码。单片机上电或复位后最先执行的一段代码。一般主要会完成堆栈指针的设置,复位向量的获取和加载,然后初始化变量,最后跳转到用户代码。在详细看启动代码之前,我们先看一下 STM32F030 的内存映射。
1 V& S3 V4 A) i9 O. }; p$ H  z2 _, o5 p, u
2.STM32F030内存映射(Memory Map)
& @9 s0 G9 a/ ]  E下面是 STM32F030 的内存映射,其它芯片会因为 Flash,SRAM 空间大小不同而略有不同。
6 P! j6 e, V) ?! U9 n! F: W
9 R5 m# L1 b; k8 `  X+ i6 S
20200207173223663.png

/ M1 a6 V' ^9 V; R% K4 Y& V8 O& f& f8 D. r
因为是32位机,所以可寻址从 0x0000_0000 到 0xFFFF_FFFF 的总共 4G 空间。' e4 ?& \5 s% o6 V& {7 E3 O

. l, A: ~3 Y2 R, G7 d- b这是采用32位机的好处,地址空间足够用。不像8位或16位机,很容易出现地址空间不够用,动不动就需要用 Page 来间接寻址。
( Z1 G, A2 N" G4 x; R% f; J3 b! f+ B9 S2 }. P# T. A1 O
我们从低地址到高地址逐段看一下:
" k9 D: C1 |' |/ L" B: q$ k- F+ d
0x0000 0000 Virtual memory
. u" q, K4 g( Z' Z% m6 M4 p9 |  L+ o/ [: k" K
这段地址空间,会因为不同的 BOOT 模式而映射到不同的物理内存。" w/ T8 U2 t, G

/ ]7 N; ~2 r* I/ U0 n4 T* J当芯片复位,或从 Standby 低功耗模式唤醒时:% A7 b; h, [  y0 B4 l8 T: X
0 [% P* b% B1 V: V1 Q. G' C
如果引脚 BOOT0 是被拉低的,将映射到 Flash memory。这是最常用的代码运行模式;
3 D: `# V/ _- T# Z" C
/ P1 m2 H' _& i, ]. }如果引脚 BOOT0 是被拉高的,且nBOOT1为 1 ,将映射到 System memory。进入bootloader模式;8 w) n$ c' }* G1 V' s: ^0 N0 \
' n1 u3 U+ t1 l8 H6 l, j1 d
如果引脚 BOOT0 是被拉高的,且nBOOT1为 0 ,将映射到 SRAM。/ r" ]; R1 Z$ r, M/ f

) ^# n+ I! B) b) ~' h注:nBOOT1 为Flash寄存器中的一位,用户何以设置。# G. }1 r! M- v" G3 E
3 n/ s7 e1 k9 i& F# X% N) ~1 H
0x0800 0000 Flash memory7 C1 r. l, u! }8 h" G2 y
存放用户代码
* k0 c" U. Q+ \' D# L, k
3 U' X! F* Q. X% B: Q1 V0x1FFF EC00 System memory
4 f* p2 g6 U; T存放 bootloader, 片内集成温度传感器的校正数据,和片内集成电压参考的校正数据5 Y  D: E% g* z& B
这些代码和数据是在工厂固化好的。- [6 [- a( N. ?8 ~+ p& S
0 J) C+ C" j8 @% b
0x2000 0000 SRAM
- c0 m+ t$ i8 ]2 A& v0 s/ [存放用户变量,堆(Heap)和栈(Stack)。也可以把代码加载到 SRAM 运行。9 O) w, z/ A1 f
- `) D$ ?) q9 G3 C
0x4000 0000 Pheriperals
, R  y, h$ N. o% f5 f芯片集成的外设,如 USART, SPI, GPIO等的寄存器地址在这一区域。
7 {1 ]% B& N7 y/ E
3 U6 C' v6 x, }5 |" z0 y7 A0xE000 0000 Cortex-M0 internal pheriperals( Q2 j4 O) K9 M' l3 q  K
M0内核的外设映射到此区域。如 systick (System Tick),NVIC,Debug Registers。这些寄存器在芯片手册里是查不到的,需要到 ARM 的手册里查找。" f4 \/ C! C+ \+ W
' U# @( ^6 A0 V( _$ y7 p: e( z
3. 启动代码(Startup Code)
0 q' G8 u; K9 P9 k' W7 E我们还是以下面这个最简单的GPIO翻转代码为例:& H$ n8 v6 b1 J8 b
STM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\
- v7 I, W9 [5 s! F5 e. \- cExamples\GPIO\GPIO_IOToggle\MDK-ARM\Project.uvprojx
6 d% K) o2 C1 F$ [  j  R3 h把此工程下载到单片机后,用调试器观察下面两个地址的内容:* S  d, p7 k7 e$ O
我们会发现0x0000_0000开始的区域, 和0x0800_0000开始的区域,内容完全相同。这说明Flash 区的内容映射到了 0x0000_0000起始的这一段地址区域。6 ?9 H' I! c- h8 m  t
* r% g  G  R! w
20200207173410739.png

, H! u# K9 s! r+ V5 A2 z% P4 {  y- j1 Z* v. l; T
注意STM32F030使用的是小端模式(Litlle Edian)。7 h* x3 [; ]; i
' F3 O6 G/ Q0 a; f3 P
不同于 MCS51 在 0x0000 放的是复位向量,STM32F030 还有其它 ARM 芯片在零地址存放的是初始堆栈指针地址。% N5 Q! o% d7 \9 H7 `- p
/ E0 a- B9 Q/ A) X4 T+ w& m
0x0000 0000: (0x2000 0428) 初始堆栈指针
( }& q: s' g. J/ \7 p: M2 a/ i7 @6 [. C- q& t; r( _$ A! ?' R
0x0000 0004: (0x0800 00C9) 复位向量,上电或复位后最先加载入PC: D4 K( }8 m/ M8 L1 u! H+ J/ X& ?

2 r$ j- F2 ~$ R# y注:单片机上电或复位后,堆栈指针初始化和 PC 初始值的加载总是从地址 0x0000_0000,0x0000_0004获取。在上面这种用户模式下,实际是从 Flash 区的 0x0800_0000,0x0800_0004 获取的。# p( w; Y; r/ B, Q7 d

2 k0 q2 D( |4 F( d) N我们可以通过调试器观察一下芯片复位后 M0 内核的寄存器:
. q/ e9 }2 b9 A$ u  F, ^9 J2 b( o+ @; l8 n6 d
20200207173413306.png
% X+ ~; [- e; }5 s: B. U' @+ h) G
% Z2 r" X$ O: L' Z* C
细心的同学这时可能发现了一个问题。
: @/ S4 t4 e' u) Y
; _9 p$ z, |9 f& `9 X0 Z: m堆栈指针 SP 的内容和前面存储器中的内容是对的上的。但是 PC 里的内容好像对不上啊?PC 里的值是 0x0800_00C8,存储器里明明是 0x0800_00C9 啊!3 g. H& p" T$ Y& a3 {% o  j
9 v' {, }- H7 c
这里牵涉到了 ARM 体系里的两种工作状态 ARM 和 Thumb。ARM 状态下执行32位指令,Thumb状态下执行16位指令。那么如何在这两者之间切换呢,一个方法就是靠跳转地址的最低位(Bit0), 当 Bit0 设为 1 时进入 Thumb 状态,当 Bit0 设为 0 时进入 ARM 状态。
9 O; t( V2 |* D* D9 v7 i, Y7 O& l  f8 ~. G  ]! R) b/ C8 o
对于单片机来说,16位的 Thumb 指令就足够了,而且16位指令比32位指令能节省存储器空间。所以 M0 内核只支持 Thumb 指令。
/ |& d7 d6 S; \" K$ r1 X: O- G
: w# X, g9 y! I. |' z& c: b* }到这里我们就可以理解复位向量为什么是 0x0800_00C9 了。9 T& \1 `% N0 J& P  C4 i; z
3 D0 ?" L' M0 r: c" B2 r
接下来我们来看复位向量 0x0800_00C8 指向的第一条指令:
' W! g% z# @8 R7 Q, Y
0 J# o! g, R1 F" m1 `* f
20200207173440317.png

9 O' |/ Y, W' l+ k$ r
7 X# n% x+ m' B- y/ O4 s. ?! l
& R/ ?8 H& L9 ?4 j3 b3 o7 ~* ]& U, T0 H* O0 W! G
单片机将要执行的第一条指令 0x4804,这是什么意思呢?
5 g% r. H0 g$ W/ p, o; |- N3 X2 t& @
先说结论:它就是下图中,单片机复位后光标指向的这条指令:3 u9 i6 j2 D0 o9 m

; G3 P9 g5 L  ]' Y. b$ a- gLDR     R0, =SystemInit
, Q1 ]1 d. D% G+ h
! r+ v/ S* u* N3 j- X. c
20200207173500363.png
5 V# P8 K, [, N( h
1 \  `1 T& }3 P& G+ o1 P

% S2 K8 q& S  i* I/ I2 u/ z  c
" u. c* u) [& e9 K+ L. g在这里详细解释一下 0x4804 这条指令:* Q* D& @" }/ i" C: ], g1 z

+ M: Y0 h" j9 ^( H3 i6 \0 A它对应的机器码是 01001000000001006 g0 P% ^# @  M& P

3 ^6 t) U" ^) C/ f9 y- }* oBit15 to Bit11 (01001)为LDR(literal)指令,既从PC偏移地址取数据送至寄存器Rt。+ Z6 l: C* {2 x# l2 _4 t

4 I1 ^, c  \& g- \Bit10 to Bit8  (000)表明目的寄存器Rt为 R0
/ s8 K" |$ s# B- y2 P
$ p0 `% Q2 M$ |+ RBit7 to Bit0  (00000100)表明相对于 PC 的偏移量为 0b10000,既0x10。5 p$ |. X: c0 L/ E% D
# t( h. I0 w. u, {  b
注意PC的值是当前地址+4。
3 a: E9 s; O- M1 p- a$ v0 P" `4 o9 x( K" }$ b8 {' V0 q
那么从 0x080000C8 + 0x4 + 0x10 = 0x080000DC 取出数据 0x0800092D 送至寄存器 R0。此地址是 SystemInit( )函数的地址。下一条语句 BLX R0 就是调用此系统初始化函数。
+ j4 Y8 v) v: A: C$ ~+ I1 F+ p7 f/ J5 G5 h4 i! s7 [
SystemInit( ) 这个函数在 system_stm32f0xx.c 这个文件里,主要完成系统时钟的初始化。可以点进去看一下具体的内容。4 {2 ?, n: ~% |  l
7 S5 n" N% ~8 t* R  G9 n" _' {+ M
20200207173508532.png
( C2 g8 ^# _- u$ @  }! k$ ?: C3 V- q
  I( P: ^6 c  q* D+ T- Z$ O* V

7 m6 u# ]! @! E" P: f4 R+ S! _* W
函数 SystemInit( ) 执行完之后,程序跳转回来,取得 __main( ) 函数的地址,跳转到 __main() 函数执行。需要注意,这个函数不是我们用户代码里的 main( ) 函数。
- C- ]4 S, a, r5 Z9 s% s( p* k7 w' w
__main() 函数是 Keil 的库提供的,我们看不到代码,它主要完成变量的初始化。这里不用太纠结,如果想进一步深究可以看一下 ARM Compiler User Guide 的 Reset and initialization 这一节。
3 Z5 ]2 Q+ s' d4 v2 I
# m0 j2 r/ S& ~% T__main() 函数执行完,基本工作就做完了,这才跳转到用户代码的 main( ) 函数。
% L" b/ \, M; S5 K' F+ u/ O
+ \( V* d7 X: {" _4 p
8 G1 \  K! Z: E& Y& ]8 u. A; d' N" K2 m- R- j% W! g
20200723225010732.png
收藏 评论0 发布时间:2021-11-20 23:00

举报

0个回答

所属标签

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