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

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

[复制链接]
STMCU小助手 发布时间:2021-11-20 23:00
1.MCU 代码如何启动
/ K0 W- w/ Z% w2 L: r% q# C首先我们需要澄清一个问题,什么是 Startup Code,什么是 Bootloader?因为总看到有同学混用这两个概念。" ?4 m3 \  [! k+ a& P" O& n; u
0 l6 H; x  C5 u! s. n+ N2 B8 A
Bootloader 可以译为引导程序。早期的单片机是没有 Bootloader 这种概念的。如大家熟悉的 MCS51,最初芯片内是不能存储代码的,需要外挂EPROM,就是下面这种带个小玻璃窗的存储器。擦除 EPROM 中的代码需要用紫外线照射几分钟才行。9 c& g: T! m3 j# O9 B2 _
- J' W" q" `/ i# w: t9 \" D0 i+ {
20200207173108336.jpg

% K1 [8 g1 U# f; K$ E" w" Y, Q/ I3 ^6 S
后来出现了 Flash 这种可电擦写的存储器,并集成在了单片机内部。但出厂的时候单片机的程序存储区仍然是空白的,没有任何代码。用户编译程序后,下载到单片机后才能运行。那么在产品发给用户后,如果发现有Bug怎么办呢?就得用编程器把新代码重新下载一次。这实在是有点儿麻烦,特别是如果客户距离很远的话。于是有聪明的程序猿想了一个办法,写一小段特殊的代码放在程序里,这段代码可以通过一定方式,比如用按键触发进入运行,它可以通过串口(早期的 PC 串口是标配)接收新的代码并写入Flash,从而在没有硬件编程器的情况下也能完成代码的更新。
. b* E' \. H' _% n% Q4 p9 g0 R5 _  A. p( K5 W
程序猿们也是现代历史前进的重要推动力啊!0 O+ n5 j# ?4 x- E
1 E! h. `' A/ z
后来,有芯片厂商把这种代码在出厂时就固化在芯片里,极大的方便了代码下载和程序更新。STM32F030内部就固化了Bootloader。当我们把一个引脚 BOOT0 拉高的同时,重新给芯片上电或复位,就会触发Boootloader进入运行。此时我们通过单片机的串口就可以把新程序发送给单片机,发送完后把 BOOT0 拉低,再复位单片机,新程序就会运行起来。2 q# Y6 L2 [7 e! O
3 q: b+ A0 D. g
Startup Code 可以译为启动代码。单片机上电或复位后最先执行的一段代码。一般主要会完成堆栈指针的设置,复位向量的获取和加载,然后初始化变量,最后跳转到用户代码。在详细看启动代码之前,我们先看一下 STM32F030 的内存映射。
% C5 B9 o$ M7 N9 \5 \6 k  W5 @, {2 G* p& _3 c- i7 L- w
2.STM32F030内存映射(Memory Map)
3 I7 ]& y; m! u4 z: |  g' B8 S5 D下面是 STM32F030 的内存映射,其它芯片会因为 Flash,SRAM 空间大小不同而略有不同。' `, x8 c7 B$ y! N4 s7 B$ H/ j7 v) L# q( t
( K- a$ K0 F; `1 F
20200207173223663.png
0 V  R0 h0 u% Z; L6 W8 m1 a

, d% [; g) K( H( j因为是32位机,所以可寻址从 0x0000_0000 到 0xFFFF_FFFF 的总共 4G 空间。
/ `# G  `, k' h' d% [. X- o* B
8 Y+ o9 j  d/ P# n3 b& U这是采用32位机的好处,地址空间足够用。不像8位或16位机,很容易出现地址空间不够用,动不动就需要用 Page 来间接寻址。6 G2 Z. a/ A/ T. U3 J; E

" D; I# ?) N5 U. B我们从低地址到高地址逐段看一下:  T. j4 z! ~  t
4 a# X' E6 C8 f4 ]+ R; @+ Q
0x0000 0000 Virtual memory1 y& R* c: T* k/ r7 |6 h

$ `# {7 P7 B, [. ~7 v这段地址空间,会因为不同的 BOOT 模式而映射到不同的物理内存。
& w5 j2 f# U7 A' ]+ x: F- D  k5 ^+ J, x' a
当芯片复位,或从 Standby 低功耗模式唤醒时:
/ @" {- ^1 G! s. B  U& B
2 p, e9 `. F8 T- V如果引脚 BOOT0 是被拉低的,将映射到 Flash memory。这是最常用的代码运行模式;6 _( A: ?: I) ?* w& `

! Z; h9 U6 V! d, h, B, J  v如果引脚 BOOT0 是被拉高的,且nBOOT1为 1 ,将映射到 System memory。进入bootloader模式;
6 U' |; o9 b; N: d# x5 y4 @
5 b4 Z  b1 E0 P( a- W% A如果引脚 BOOT0 是被拉高的,且nBOOT1为 0 ,将映射到 SRAM。$ [* |( _# }9 l; j9 ~: {3 Q' n
' r8 Q. M9 T6 C$ o' Q" Q, j
注:nBOOT1 为Flash寄存器中的一位,用户何以设置。0 O- ^6 J: c! J: S2 t5 Z
( @0 E- W# c) h+ X, o/ l; ]$ S
0x0800 0000 Flash memory
- C/ ?* a7 r) [' z存放用户代码9 `+ j' }3 d8 p) N: W+ \. x

: U+ r" a" O9 d, @% `( C" A0x1FFF EC00 System memory
) U$ v- u" B2 t4 f5 h存放 bootloader, 片内集成温度传感器的校正数据,和片内集成电压参考的校正数据
% @4 M; W" H/ b这些代码和数据是在工厂固化好的。
% e2 s; y% b' X  Z2 h% Q
+ g& E+ t1 j- F7 b) T4 P0x2000 0000 SRAM
9 J6 o  |3 v7 u存放用户变量,堆(Heap)和栈(Stack)。也可以把代码加载到 SRAM 运行。
5 I) M9 a/ w! C* e1 K/ I9 d6 q4 O! Q1 T; B- U9 p+ b
0x4000 0000 Pheriperals
5 b5 Z/ Z4 o" ~6 ?6 {$ u; q" t2 L4 F芯片集成的外设,如 USART, SPI, GPIO等的寄存器地址在这一区域。1 w5 K; K7 M9 ^
8 B) H- w$ n0 k/ ~7 T
0xE000 0000 Cortex-M0 internal pheriperals
+ w- G, o9 Z" ~1 ]M0内核的外设映射到此区域。如 systick (System Tick),NVIC,Debug Registers。这些寄存器在芯片手册里是查不到的,需要到 ARM 的手册里查找。) ^$ U3 a: o: [& Y- `' P

; H  N  X1 X3 |% [0 K3 S3. 启动代码(Startup Code)  v. n& m( c$ v* A! s6 i
我们还是以下面这个最简单的GPIO翻转代码为例:
$ x) x* L' v0 y% b' H) @STM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\+ @6 k  m4 G" O9 c: b5 b
Examples\GPIO\GPIO_IOToggle\MDK-ARM\Project.uvprojx
% f' q% O* A" U; s( e) N把此工程下载到单片机后,用调试器观察下面两个地址的内容:2 L1 M# O5 R. z" K/ W. }
我们会发现0x0000_0000开始的区域, 和0x0800_0000开始的区域,内容完全相同。这说明Flash 区的内容映射到了 0x0000_0000起始的这一段地址区域。
: Z! d7 y" u& q0 r
/ N9 I! a8 p7 g/ Z( M
20200207173410739.png

6 z1 c, L+ y8 N; O2 T% P3 j/ C
0 ]5 A$ I) B6 ~$ g2 `注意STM32F030使用的是小端模式(Litlle Edian)。& X3 j# H5 ^: z# p$ \' J& b

5 W; f8 S) h$ a' X/ K9 R3 x/ q- ?& a不同于 MCS51 在 0x0000 放的是复位向量,STM32F030 还有其它 ARM 芯片在零地址存放的是初始堆栈指针地址。) Z4 n6 U# u# t. ^. a6 k2 ?

! D' o5 G# l" `, c: t0x0000 0000: (0x2000 0428) 初始堆栈指针: J/ h1 N/ w( u7 C/ w
7 j0 J& C1 i& P8 Z
0x0000 0004: (0x0800 00C9) 复位向量,上电或复位后最先加载入PC3 c/ ~' p1 W3 K8 e1 U% D

! k" f. D3 T4 b* ^9 w( _注:单片机上电或复位后,堆栈指针初始化和 PC 初始值的加载总是从地址 0x0000_0000,0x0000_0004获取。在上面这种用户模式下,实际是从 Flash 区的 0x0800_0000,0x0800_0004 获取的。
: o) u; U# B( W# X0 K; v; V7 m
1 e9 L+ K; S6 m! r4 H% K我们可以通过调试器观察一下芯片复位后 M0 内核的寄存器:
/ W  W) o$ H6 h5 ^* N2 p- s6 G1 p) |% o' N9 J. d$ N6 k' q
20200207173413306.png
- `3 j0 _. T$ i8 R. B
7 b. S+ Y9 ^9 @) j
细心的同学这时可能发现了一个问题。; b+ V* d8 }1 e$ [1 T
6 |. b2 u0 o5 G4 L: I1 [" x, T: g% _6 x
堆栈指针 SP 的内容和前面存储器中的内容是对的上的。但是 PC 里的内容好像对不上啊?PC 里的值是 0x0800_00C8,存储器里明明是 0x0800_00C9 啊!* d, b0 S# m) x& ^  f; X( D

% y! Z- P( e  @这里牵涉到了 ARM 体系里的两种工作状态 ARM 和 Thumb。ARM 状态下执行32位指令,Thumb状态下执行16位指令。那么如何在这两者之间切换呢,一个方法就是靠跳转地址的最低位(Bit0), 当 Bit0 设为 1 时进入 Thumb 状态,当 Bit0 设为 0 时进入 ARM 状态。
0 w' p. c7 d8 r7 d$ W5 Z( N' T' S- b) a8 l, _) {) K9 r4 H
对于单片机来说,16位的 Thumb 指令就足够了,而且16位指令比32位指令能节省存储器空间。所以 M0 内核只支持 Thumb 指令。: R. g. m& {- O& @  L
, _6 f: x+ p% w6 i
到这里我们就可以理解复位向量为什么是 0x0800_00C9 了。
/ P6 U! E, H& W8 @% W. I. y& p* C+ Y9 R1 `" B8 K  V; \
接下来我们来看复位向量 0x0800_00C8 指向的第一条指令:% P3 q! V7 w0 z5 M3 F. ]1 R
+ n$ _- A1 B4 s
20200207173440317.png
+ H" j5 b. X7 ~, z2 B& m+ u7 U

( m, Q; G" [" K  {; R* {$ g2 r% l
# M) W2 ~/ Y7 l& f& C# d$ W/ x* o: s+ X4 a4 I6 n
单片机将要执行的第一条指令 0x4804,这是什么意思呢?3 |) a; f0 T6 b/ _$ }1 G/ L/ _

; n" m4 ]$ p0 B5 T5 Q先说结论:它就是下图中,单片机复位后光标指向的这条指令:
9 Y" d0 u% ?9 w& H5 i
- ]/ C9 M0 t; q/ }8 g. pLDR     R0, =SystemInit
! ~; F. D; ~2 J: G. r) {) f$ H( t
% ?0 D* g8 d* ~# O6 y4 q
20200207173500363.png

; e( D! Z" x( J: C% L% S
' c* T, H6 H6 [5 n+ k. o
' [- y; ^: O4 m! k5 }; I
1 Z1 y& u# x2 k6 P# _在这里详细解释一下 0x4804 这条指令:
* d) U$ b1 ~' Q6 g: F* |* k9 h) R4 e
它对应的机器码是 0100100000000100+ i! V8 i; {+ v5 }) m

5 r% k6 ]: t3 |) sBit15 to Bit11 (01001)为LDR(literal)指令,既从PC偏移地址取数据送至寄存器Rt。
6 r& @& R0 `! w: K  l! b: j  w9 f
Bit10 to Bit8  (000)表明目的寄存器Rt为 R0/ C1 @8 U+ ~1 U

8 R% `  c* V" H6 }3 o1 `Bit7 to Bit0  (00000100)表明相对于 PC 的偏移量为 0b10000,既0x10。
2 q' Q3 y" }7 F* D) V4 y& X+ T7 p, }: t+ g; a2 m* c+ r' ]8 D
注意PC的值是当前地址+4。, V6 o* @0 t2 `2 O

5 u: ^* e/ n8 N那么从 0x080000C8 + 0x4 + 0x10 = 0x080000DC 取出数据 0x0800092D 送至寄存器 R0。此地址是 SystemInit( )函数的地址。下一条语句 BLX R0 就是调用此系统初始化函数。
  _7 m: s, |# [8 g2 e' n  `
) b: g4 _# p, v! s; D) `' NSystemInit( ) 这个函数在 system_stm32f0xx.c 这个文件里,主要完成系统时钟的初始化。可以点进去看一下具体的内容。: p) N1 h5 @9 d; E7 D

! s/ S' F5 v+ j9 C6 k/ P9 k, D- N
20200207173508532.png
' {, F! W! ^  ?/ s  p

) \- I4 \  o( j* D: p
3 H) j2 P  G9 o, P7 T  u& ?5 A( [# d: p4 A7 f. o, y
函数 SystemInit( ) 执行完之后,程序跳转回来,取得 __main( ) 函数的地址,跳转到 __main() 函数执行。需要注意,这个函数不是我们用户代码里的 main( ) 函数。/ f8 y, k# t" H
! t2 ~$ L5 ?* w: |' K4 i9 ^. t
__main() 函数是 Keil 的库提供的,我们看不到代码,它主要完成变量的初始化。这里不用太纠结,如果想进一步深究可以看一下 ARM Compiler User Guide 的 Reset and initialization 这一节。7 l1 N3 j" {  V0 G. j6 Z/ X( b1 U

( h; [# j- e5 U" [: E' |0 m3 s1 _+ R__main() 函数执行完,基本工作就做完了,这才跳转到用户代码的 main( ) 函数。
2 h) `* N* _( u  h/ }5 ~7 Q( k9 Y: {$ E  m& A+ k; c
! i# _3 ]+ t! j

6 K/ t. X& [1 {2 w: g1 x$ w
20200723225010732.png
收藏 评论0 发布时间:2021-11-20 23:00

举报

0个回答

所属标签

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