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

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

[复制链接]
STMCU小助手 发布时间:2021-11-20 23:00
1.MCU 代码如何启动* g9 f9 K' A$ S  l( P$ L
首先我们需要澄清一个问题,什么是 Startup Code,什么是 Bootloader?因为总看到有同学混用这两个概念。
, U# E) F4 f" b8 F$ M( d# E$ n$ g) q  g( ?5 N9 I( d" P
Bootloader 可以译为引导程序。早期的单片机是没有 Bootloader 这种概念的。如大家熟悉的 MCS51,最初芯片内是不能存储代码的,需要外挂EPROM,就是下面这种带个小玻璃窗的存储器。擦除 EPROM 中的代码需要用紫外线照射几分钟才行。
, o% P0 ^9 a) X; }
, N5 h) u7 P5 c" O$ e* w+ A
20200207173108336.jpg
& i# h+ m6 U% s% A3 O

" E( l0 U- G9 ?# D后来出现了 Flash 这种可电擦写的存储器,并集成在了单片机内部。但出厂的时候单片机的程序存储区仍然是空白的,没有任何代码。用户编译程序后,下载到单片机后才能运行。那么在产品发给用户后,如果发现有Bug怎么办呢?就得用编程器把新代码重新下载一次。这实在是有点儿麻烦,特别是如果客户距离很远的话。于是有聪明的程序猿想了一个办法,写一小段特殊的代码放在程序里,这段代码可以通过一定方式,比如用按键触发进入运行,它可以通过串口(早期的 PC 串口是标配)接收新的代码并写入Flash,从而在没有硬件编程器的情况下也能完成代码的更新。
% D" {& |! ~0 h
6 k- V2 p. T3 c8 Q  J; L 程序猿们也是现代历史前进的重要推动力啊!% a! `" Q2 T4 U: h. U& T4 j' w

3 W& b* U& Q  J; C# z$ N( D- Q5 _" f后来,有芯片厂商把这种代码在出厂时就固化在芯片里,极大的方便了代码下载和程序更新。STM32F030内部就固化了Bootloader。当我们把一个引脚 BOOT0 拉高的同时,重新给芯片上电或复位,就会触发Boootloader进入运行。此时我们通过单片机的串口就可以把新程序发送给单片机,发送完后把 BOOT0 拉低,再复位单片机,新程序就会运行起来。8 t- @& {' I+ w' C/ y

& S8 t0 q: {, L4 LStartup Code 可以译为启动代码。单片机上电或复位后最先执行的一段代码。一般主要会完成堆栈指针的设置,复位向量的获取和加载,然后初始化变量,最后跳转到用户代码。在详细看启动代码之前,我们先看一下 STM32F030 的内存映射。
; t& C1 ~* s0 Q
# k/ z9 e; x, [  Y' E3 e2.STM32F030内存映射(Memory Map)
  |; n: f% u5 \6 F% f下面是 STM32F030 的内存映射,其它芯片会因为 Flash,SRAM 空间大小不同而略有不同。
% A1 }( r. G: l$ E; q/ f
0 j' P5 t8 ~# Z
20200207173223663.png

! I9 \  @$ E3 F4 R; N/ N" }" d, I6 a
因为是32位机,所以可寻址从 0x0000_0000 到 0xFFFF_FFFF 的总共 4G 空间。
2 z: F; f, Q8 k3 t3 g8 }1 F
( N3 t' c" `  z& {7 N# s这是采用32位机的好处,地址空间足够用。不像8位或16位机,很容易出现地址空间不够用,动不动就需要用 Page 来间接寻址。
" z' N# ~2 C- e0 `! [. @  O$ O' u+ B
我们从低地址到高地址逐段看一下:6 \9 ]$ }* y" v& T/ X7 P: y# O* l7 M
6 B# ~  c3 A& h  J' q* F6 S1 Z7 F
0x0000 0000 Virtual memory
  A! H/ _* [2 {
4 B' E2 Y# R0 W8 H! A' x7 D( o  g这段地址空间,会因为不同的 BOOT 模式而映射到不同的物理内存。# w$ A0 V6 p1 v- v
7 u3 p2 U! Q0 M- T1 s" G3 Q* C
当芯片复位,或从 Standby 低功耗模式唤醒时:, P' H6 U/ \' x; Z% x

, N2 k3 p  t7 ~. L6 @如果引脚 BOOT0 是被拉低的,将映射到 Flash memory。这是最常用的代码运行模式;
  p8 Q/ }( N, g. s$ Z5 A& [' B2 W3 P( T$ I* d: M  A7 u
如果引脚 BOOT0 是被拉高的,且nBOOT1为 1 ,将映射到 System memory。进入bootloader模式;( }! d: E8 I, T( ]6 x6 t3 v9 F- c

: k4 v5 x! i! C9 C1 C3 D7 p如果引脚 BOOT0 是被拉高的,且nBOOT1为 0 ,将映射到 SRAM。. y: V. L% {# t
  l3 U- ^' S- m* j! M; h
注:nBOOT1 为Flash寄存器中的一位,用户何以设置。' x7 U# y7 k' K* a5 A. m7 t$ ?/ }

% i0 J- ~& |6 Z% L3 R2 N0x0800 0000 Flash memory
7 ~$ I1 x# m0 C* Q  P* a" J存放用户代码
( u4 n1 f2 |" B7 V2 s+ [* ^! E. c& J0 ?4 U/ M) H8 T  |! j
0x1FFF EC00 System memory# r$ a% K  e$ s* X- K: N
存放 bootloader, 片内集成温度传感器的校正数据,和片内集成电压参考的校正数据7 S- [, Y3 S5 Y  A
这些代码和数据是在工厂固化好的。
6 @& m  N+ w  K
6 R* h0 j& d" @3 Q9 s& V# F0x2000 0000 SRAM
$ u1 L5 ?3 Q# D/ U存放用户变量,堆(Heap)和栈(Stack)。也可以把代码加载到 SRAM 运行。0 e4 J: P* {0 n$ l3 A- H* z' Z
: u! U/ n. e: @/ b  n
0x4000 0000 Pheriperals
3 H5 I$ ^, F( ~# Z) r& A芯片集成的外设,如 USART, SPI, GPIO等的寄存器地址在这一区域。
& l. M& m+ m5 t: G! X4 f, G& b$ s$ Q- D
0xE000 0000 Cortex-M0 internal pheriperals9 t5 |, D" B* u
M0内核的外设映射到此区域。如 systick (System Tick),NVIC,Debug Registers。这些寄存器在芯片手册里是查不到的,需要到 ARM 的手册里查找。# N8 Q- T! `5 m) p
5 b: m/ A5 c* ^8 i% [- t
3. 启动代码(Startup Code)) B6 }" g# ]7 S4 {
我们还是以下面这个最简单的GPIO翻转代码为例:
2 @4 m" S- L9 K7 L& b, f& J5 ~STM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\6 A0 O* ?  `/ w3 `$ N9 z( j/ O
Examples\GPIO\GPIO_IOToggle\MDK-ARM\Project.uvprojx3 q# \8 x! P; X2 d- M
把此工程下载到单片机后,用调试器观察下面两个地址的内容:
2 @# b& g; ~4 i0 Z6 s1 i. z我们会发现0x0000_0000开始的区域, 和0x0800_0000开始的区域,内容完全相同。这说明Flash 区的内容映射到了 0x0000_0000起始的这一段地址区域。/ R/ |( ~- h* z8 K. C' J
- r8 s4 V2 G. c% _! Q2 Y3 g
20200207173410739.png
6 m  P% k& q  X% M- D3 C" p/ w
, X% F0 \: P1 {0 N, o! G( K1 \
注意STM32F030使用的是小端模式(Litlle Edian)。
. P! H3 C0 g. `% j
7 G1 M, d$ n" p0 w. z不同于 MCS51 在 0x0000 放的是复位向量,STM32F030 还有其它 ARM 芯片在零地址存放的是初始堆栈指针地址。3 b+ J+ B6 x( p- W' q' ]

6 J6 y1 ^; q  T! ]3 S0x0000 0000: (0x2000 0428) 初始堆栈指针
1 l; t: y; a$ R6 m+ o4 ?! O2 j# ~
, X+ |7 d  q2 W& P+ w0x0000 0004: (0x0800 00C9) 复位向量,上电或复位后最先加载入PC
3 N) l3 i$ s7 m6 Y& V' Z
/ L) c/ m3 ]% Z6 g# i注:单片机上电或复位后,堆栈指针初始化和 PC 初始值的加载总是从地址 0x0000_0000,0x0000_0004获取。在上面这种用户模式下,实际是从 Flash 区的 0x0800_0000,0x0800_0004 获取的。
7 N, w1 h" W3 b) Z0 s
$ k& s4 z9 b& J! m我们可以通过调试器观察一下芯片复位后 M0 内核的寄存器:
+ d$ E* n7 D8 ]5 K' i7 W2 a: r
20200207173413306.png
+ y& T4 s+ T0 |0 N' U
; ^% [, I3 D# @# F$ i7 j
细心的同学这时可能发现了一个问题。
) u! C4 j" R: a6 y) d# l9 ~2 {: s$ x3 K+ a1 N
堆栈指针 SP 的内容和前面存储器中的内容是对的上的。但是 PC 里的内容好像对不上啊?PC 里的值是 0x0800_00C8,存储器里明明是 0x0800_00C9 啊!& m+ L* [* v2 h; _/ E
/ x/ h- b8 Q6 K  }4 F+ A
这里牵涉到了 ARM 体系里的两种工作状态 ARM 和 Thumb。ARM 状态下执行32位指令,Thumb状态下执行16位指令。那么如何在这两者之间切换呢,一个方法就是靠跳转地址的最低位(Bit0), 当 Bit0 设为 1 时进入 Thumb 状态,当 Bit0 设为 0 时进入 ARM 状态。
% p" E+ i; W0 _% b0 N0 Z( c/ ?/ [2 d, O' ?, V" T
对于单片机来说,16位的 Thumb 指令就足够了,而且16位指令比32位指令能节省存储器空间。所以 M0 内核只支持 Thumb 指令。
# {+ y* e/ n) d) X. X% o( ]5 a
到这里我们就可以理解复位向量为什么是 0x0800_00C9 了。3 X3 r, s6 j) x3 _: c9 G' q

& h; c! B6 \. ~( o& y接下来我们来看复位向量 0x0800_00C8 指向的第一条指令:
6 ~5 ]4 {( z# m& j4 ?! P- C$ m. ]/ t# L) D/ ]5 Z9 N
20200207173440317.png
7 A* Z( s, u$ i; G

0 }7 F) B$ D5 E: k+ j! z# m, z% b" d
+ `1 r1 O; D5 i1 B$ a! u8 @
单片机将要执行的第一条指令 0x4804,这是什么意思呢?
" [( h" t2 ~8 H) J# u6 E6 G! L4 G' i: o; E
先说结论:它就是下图中,单片机复位后光标指向的这条指令:
- p' _. T6 `7 b; Y. g. f
: e( X- U+ ^8 O& ]LDR     R0, =SystemInit
9 t" U( V3 `6 e) A
2 d: T0 D, Q9 d
20200207173500363.png
# Y6 M! G. P* k6 V6 \: F
3 ~, \5 f' n4 T6 Q8 j6 N% H$ n
2 X/ y9 R% E* u2 B8 P
4 q# Y, b9 {0 m8 O' D
在这里详细解释一下 0x4804 这条指令:
3 ~, b/ Y9 N0 Q
4 y6 j  h3 D$ X! T! R: N, K它对应的机器码是 0100100000000100% Q5 f) G& {- @

. I5 V) N& t# j" c, e& ABit15 to Bit11 (01001)为LDR(literal)指令,既从PC偏移地址取数据送至寄存器Rt。
$ b& }- }, P+ I, h" |2 f) H
( d9 o# w  |" R) q$ l! E) tBit10 to Bit8  (000)表明目的寄存器Rt为 R04 I4 ]1 U7 ^& {. N- \  t0 ]

( U6 c' s( |; K5 A) u  a0 ^8 tBit7 to Bit0  (00000100)表明相对于 PC 的偏移量为 0b10000,既0x10。
+ W1 ?6 q8 A6 Y* F4 W: |6 ]2 O
注意PC的值是当前地址+4。. a8 A, G: J7 t* V5 _3 @

6 c' o- W0 \) E: f1 \9 A那么从 0x080000C8 + 0x4 + 0x10 = 0x080000DC 取出数据 0x0800092D 送至寄存器 R0。此地址是 SystemInit( )函数的地址。下一条语句 BLX R0 就是调用此系统初始化函数。$ R, S! K# B4 }& j  g1 y9 q! ]

; z" s! L: `& a' t6 YSystemInit( ) 这个函数在 system_stm32f0xx.c 这个文件里,主要完成系统时钟的初始化。可以点进去看一下具体的内容。' [, m, t: J9 e) q" u+ B
2 p/ r1 O! u( ~3 ?" A* X+ X
20200207173508532.png
6 N+ i* d  L  s  d8 v" s$ a
( A& J% O7 z+ l9 F3 Z
, G' `9 @. B( U7 f
% K. A: c# @0 `1 ]- @
函数 SystemInit( ) 执行完之后,程序跳转回来,取得 __main( ) 函数的地址,跳转到 __main() 函数执行。需要注意,这个函数不是我们用户代码里的 main( ) 函数。* y- U$ x* u1 ^

# J! ]4 J! S5 u" e__main() 函数是 Keil 的库提供的,我们看不到代码,它主要完成变量的初始化。这里不用太纠结,如果想进一步深究可以看一下 ARM Compiler User Guide 的 Reset and initialization 这一节。
, {- C% k! q3 R8 q# k9 P. ~
: w( B. g9 E0 K; p" a__main() 函数执行完,基本工作就做完了,这才跳转到用户代码的 main( ) 函数。
6 r7 o2 R) T& r2 Z6 X8 ?$ Z" r; K& S: v

" j3 j3 ~. i! S0 f( Q: i+ f" m; C" w" n' H0 ^: ?" u% ?
20200723225010732.png
收藏 评论0 发布时间:2021-11-20 23:00

举报

0个回答

所属标签

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32Cube扩展软件包
意法半导体边缘AI套件
ST - 理想汽车豪华SUV案例
ST意法半导体智能家居案例
STM32 ARM Cortex 32位微控制器
关注我们
st-img 微信公众号
st-img 手机版