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

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

[复制链接]
STMCU小助手 发布时间:2021-11-20 23:00
1.MCU 代码如何启动
. J/ C% }; y' n首先我们需要澄清一个问题,什么是 Startup Code,什么是 Bootloader?因为总看到有同学混用这两个概念。
$ a$ D0 o- D7 D. f  G1 g, A: R4 z# T4 L, P
Bootloader 可以译为引导程序。早期的单片机是没有 Bootloader 这种概念的。如大家熟悉的 MCS51,最初芯片内是不能存储代码的,需要外挂EPROM,就是下面这种带个小玻璃窗的存储器。擦除 EPROM 中的代码需要用紫外线照射几分钟才行。
& }5 q  r9 U3 _. f7 Q. o7 Z1 p5 H. ]( `) f6 n; ?8 w1 h9 T
20200207173108336.jpg

8 ]3 X3 u& M. J4 {( z
. f, X/ ~% V; z2 D) W! ?后来出现了 Flash 这种可电擦写的存储器,并集成在了单片机内部。但出厂的时候单片机的程序存储区仍然是空白的,没有任何代码。用户编译程序后,下载到单片机后才能运行。那么在产品发给用户后,如果发现有Bug怎么办呢?就得用编程器把新代码重新下载一次。这实在是有点儿麻烦,特别是如果客户距离很远的话。于是有聪明的程序猿想了一个办法,写一小段特殊的代码放在程序里,这段代码可以通过一定方式,比如用按键触发进入运行,它可以通过串口(早期的 PC 串口是标配)接收新的代码并写入Flash,从而在没有硬件编程器的情况下也能完成代码的更新。3 G$ ?4 x& i) P9 [9 M) d% o
' Q0 p3 Q! l& I1 W' a& h4 {
程序猿们也是现代历史前进的重要推动力啊!/ g( I% N8 l+ X4 Y0 r) j0 K, U6 v
$ i; C, t) U8 d9 X  ?
后来,有芯片厂商把这种代码在出厂时就固化在芯片里,极大的方便了代码下载和程序更新。STM32F030内部就固化了Bootloader。当我们把一个引脚 BOOT0 拉高的同时,重新给芯片上电或复位,就会触发Boootloader进入运行。此时我们通过单片机的串口就可以把新程序发送给单片机,发送完后把 BOOT0 拉低,再复位单片机,新程序就会运行起来。
% w: A9 D3 Y+ |) W# y% d7 U2 F0 x3 v/ W+ K- g/ K
Startup Code 可以译为启动代码。单片机上电或复位后最先执行的一段代码。一般主要会完成堆栈指针的设置,复位向量的获取和加载,然后初始化变量,最后跳转到用户代码。在详细看启动代码之前,我们先看一下 STM32F030 的内存映射。- d/ Z7 x; A. n0 W
/ p/ \3 d0 r5 y- M$ z6 m, R' G2 d4 \
2.STM32F030内存映射(Memory Map)
7 v4 @: \# X9 X下面是 STM32F030 的内存映射,其它芯片会因为 Flash,SRAM 空间大小不同而略有不同。
; v# n& o0 }6 ~% y# ^2 y# C- f1 K: v* V& ^4 W
20200207173223663.png
/ R4 d1 x" k* R& ?7 E

7 ?6 S, G+ V5 B$ K% `" T5 f因为是32位机,所以可寻址从 0x0000_0000 到 0xFFFF_FFFF 的总共 4G 空间。' z$ m$ Z) P9 @( J  E

: ~( P' W  O+ M2 \5 J) L1 K这是采用32位机的好处,地址空间足够用。不像8位或16位机,很容易出现地址空间不够用,动不动就需要用 Page 来间接寻址。5 u3 o9 B( u; V. ~- j2 q3 Z, a

1 S0 V3 ^5 G" B6 }3 d' c我们从低地址到高地址逐段看一下:
, o% P2 [4 |9 O  ?* C
( m: a& Q' \6 L! H( p1 {6 C0x0000 0000 Virtual memory3 s& m$ D" n* @. M6 P6 U$ z5 J

/ |2 y! t3 r! h这段地址空间,会因为不同的 BOOT 模式而映射到不同的物理内存。4 k. s! d2 ?3 F) N- T: y
8 r+ j) F: S9 N$ Q
当芯片复位,或从 Standby 低功耗模式唤醒时:
# b% L- ~$ P0 j7 m, X) p/ x: J7 M! ?
如果引脚 BOOT0 是被拉低的,将映射到 Flash memory。这是最常用的代码运行模式;7 _' h" q1 d7 u+ m
# @2 w! a2 @: p! E% a
如果引脚 BOOT0 是被拉高的,且nBOOT1为 1 ,将映射到 System memory。进入bootloader模式;# |. O/ l3 S1 B; H5 S! M7 ^

8 t6 n! `- {. |" T如果引脚 BOOT0 是被拉高的,且nBOOT1为 0 ,将映射到 SRAM。* O/ s7 n1 o- w5 Y9 g1 G
, i& M1 {$ N4 T4 o. \  G' \
注:nBOOT1 为Flash寄存器中的一位,用户何以设置。8 o8 W+ j0 {. G+ [3 W$ Q  i

) E( Q' {8 ~, t0x0800 0000 Flash memory3 ~- |& J5 J3 `; S( }5 V
存放用户代码
& d- _- ^5 I+ _. x, S$ Y0 A5 a3 X. N4 `& b# q4 z7 M
0x1FFF EC00 System memory& b; Y0 ?( [& Z/ E
存放 bootloader, 片内集成温度传感器的校正数据,和片内集成电压参考的校正数据
: S* Q( U. a1 T! }这些代码和数据是在工厂固化好的。% G8 R5 O8 f* ]. U3 V) X: F: \
% |* t5 j8 y6 D: R: s
0x2000 0000 SRAM1 }' J" [/ \( ~3 S5 G2 m% o  D  @! a5 l
存放用户变量,堆(Heap)和栈(Stack)。也可以把代码加载到 SRAM 运行。
2 w" v# i6 K  J$ p5 C5 o
- g- _) b. h' V0x4000 0000 Pheriperals* r, O' i1 i+ G- A- a5 D( X
芯片集成的外设,如 USART, SPI, GPIO等的寄存器地址在这一区域。  S& [5 r* x, j! ^- }% d# Y. E

; Z3 ~: C+ F- S2 W0xE000 0000 Cortex-M0 internal pheriperals
) D* d, n  H1 L( v8 RM0内核的外设映射到此区域。如 systick (System Tick),NVIC,Debug Registers。这些寄存器在芯片手册里是查不到的,需要到 ARM 的手册里查找。
, x* B* u; _% x/ ~1 u% x: f' k, l" K6 d8 r4 X0 g
3. 启动代码(Startup Code)
; Z8 v# }, l4 S; A* l  n/ P6 p- f. A我们还是以下面这个最简单的GPIO翻转代码为例:
5 E/ i/ L5 z; b9 uSTM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\5 a  P0 T- v/ g
Examples\GPIO\GPIO_IOToggle\MDK-ARM\Project.uvprojx* W9 A0 w9 C/ ]- o: {+ Y. i* n
把此工程下载到单片机后,用调试器观察下面两个地址的内容:
) e+ D% i, H5 u& X% o- o我们会发现0x0000_0000开始的区域, 和0x0800_0000开始的区域,内容完全相同。这说明Flash 区的内容映射到了 0x0000_0000起始的这一段地址区域。) w6 G( d7 X9 p1 X+ o/ p

# d* J  O- ]- P  n/ s# Z9 T) @% u
20200207173410739.png
% p; u. p# @( l2 _) t) _5 \
' M) ~  P1 o- G! g
注意STM32F030使用的是小端模式(Litlle Edian)。
* g$ r/ ~7 O* F% D* S* \1 R# p. @4 s3 ]$ c% }
不同于 MCS51 在 0x0000 放的是复位向量,STM32F030 还有其它 ARM 芯片在零地址存放的是初始堆栈指针地址。
, ^9 [; L8 I* O5 L# v; Y' [; h3 C2 B; L9 |+ Z$ i
0x0000 0000: (0x2000 0428) 初始堆栈指针
: s: |3 R- `* |. g1 N! @% ]8 M5 ?, A% e+ W
0x0000 0004: (0x0800 00C9) 复位向量,上电或复位后最先加载入PC7 _# _) L3 p+ F  S; {  Y# a

0 b0 |; X9 r% y/ U1 ?% K注:单片机上电或复位后,堆栈指针初始化和 PC 初始值的加载总是从地址 0x0000_0000,0x0000_0004获取。在上面这种用户模式下,实际是从 Flash 区的 0x0800_0000,0x0800_0004 获取的。, r9 u' A7 ?+ S$ W2 K

0 ?1 ^6 Y! ~$ b2 e' ?# K8 q+ v& f我们可以通过调试器观察一下芯片复位后 M0 内核的寄存器:
1 ]$ [) u: O- W" d, E6 i4 D+ Y5 G
- C& y( }5 s# V2 @! J2 U. v
20200207173413306.png
0 C9 E8 k& h9 i# [
6 p, y2 Z) ]* Q, O
细心的同学这时可能发现了一个问题。
5 k1 B% B2 I, A) a* h
- l7 ?/ g5 t$ x3 ^7 i$ {" G( C% }堆栈指针 SP 的内容和前面存储器中的内容是对的上的。但是 PC 里的内容好像对不上啊?PC 里的值是 0x0800_00C8,存储器里明明是 0x0800_00C9 啊!) N& u5 O) K, c2 {% W
; Y1 F! H' M) D3 n
这里牵涉到了 ARM 体系里的两种工作状态 ARM 和 Thumb。ARM 状态下执行32位指令,Thumb状态下执行16位指令。那么如何在这两者之间切换呢,一个方法就是靠跳转地址的最低位(Bit0), 当 Bit0 设为 1 时进入 Thumb 状态,当 Bit0 设为 0 时进入 ARM 状态。  v! [3 c5 L! @/ J1 Z

, a7 q) p* y, ~5 }对于单片机来说,16位的 Thumb 指令就足够了,而且16位指令比32位指令能节省存储器空间。所以 M0 内核只支持 Thumb 指令。
9 d! f" n1 O  b+ k0 X. P7 g/ m; G8 z. O5 y, T+ ?
到这里我们就可以理解复位向量为什么是 0x0800_00C9 了。
4 K" E: u/ y% W/ ?# J: {. ]& T! t2 U/ }: V0 ?) }
接下来我们来看复位向量 0x0800_00C8 指向的第一条指令:% G* N2 v  g6 [1 U- ~! X3 q0 C
4 R* T& T9 p0 P' l: f4 `
20200207173440317.png
: j+ j0 w; m, M; b7 q1 f- _4 ]" i1 U
# W* y/ @, U% O0 v. k: j) q
  W4 W, H8 |" W4 L" m

. a1 L$ T# R8 M/ \! X& ^单片机将要执行的第一条指令 0x4804,这是什么意思呢?
; K$ j% h. ^1 b9 e1 Y1 E
" T; q" l) c# X9 u$ I先说结论:它就是下图中,单片机复位后光标指向的这条指令:
0 l8 U* f; f! w2 R0 Y+ |2 J8 J5 y" X" w$ N# h5 I% J8 ?/ U8 N7 G6 d
LDR     R0, =SystemInit
% u4 ~2 S5 @5 _8 h3 C2 ^1 k; r  A1 i+ L
20200207173500363.png
/ Z7 n7 Z3 G6 P. h% B7 X

) D/ i7 `1 r2 ?" |3 x0 y7 p/ r1 U, X1 s& z% ?- Y3 I* E

/ v& N' f+ p( g+ C3 Y在这里详细解释一下 0x4804 这条指令:
4 f) C- E3 G# N# H
2 u* w. |+ }! O1 ?: L" S它对应的机器码是 0100100000000100
- d: Y6 |* F1 ~/ O/ C4 o* O
; u( i! a; M+ RBit15 to Bit11 (01001)为LDR(literal)指令,既从PC偏移地址取数据送至寄存器Rt。  B& t* f3 v& s$ K4 `1 c
( d5 \' X) A1 g: N( o0 _2 q; K0 [
Bit10 to Bit8  (000)表明目的寄存器Rt为 R0
, w  ]( S  R0 o: ~, }6 c9 x; ~, U. z* {; Z: M0 ?
Bit7 to Bit0  (00000100)表明相对于 PC 的偏移量为 0b10000,既0x10。9 b2 g, H" X; P8 ^
$ Q4 _6 L# x- h' o& C- }
注意PC的值是当前地址+4。8 A5 S$ M$ c1 N% m1 i  q
7 x1 C7 S6 K* j& E! v+ k6 j6 R
那么从 0x080000C8 + 0x4 + 0x10 = 0x080000DC 取出数据 0x0800092D 送至寄存器 R0。此地址是 SystemInit( )函数的地址。下一条语句 BLX R0 就是调用此系统初始化函数。
& `- A& J  w& n# n5 y9 J6 ~* L2 j+ {
SystemInit( ) 这个函数在 system_stm32f0xx.c 这个文件里,主要完成系统时钟的初始化。可以点进去看一下具体的内容。
6 a6 g; V2 U/ P9 w8 n
' R; O4 t) v1 h2 a4 `4 R+ B. M
20200207173508532.png

. r- v( X) |6 r8 X
+ c6 G! H! `5 C, s# O9 c6 n
9 P! R% f  I, z$ Z) q+ x6 O- H) {# f6 K! S, c7 p. w
函数 SystemInit( ) 执行完之后,程序跳转回来,取得 __main( ) 函数的地址,跳转到 __main() 函数执行。需要注意,这个函数不是我们用户代码里的 main( ) 函数。
. X4 D2 d9 K* d& t2 v. [
7 M- t1 K1 C' @- V+ D__main() 函数是 Keil 的库提供的,我们看不到代码,它主要完成变量的初始化。这里不用太纠结,如果想进一步深究可以看一下 ARM Compiler User Guide 的 Reset and initialization 这一节。, L* h. o5 o2 C( T9 V
9 ]! D3 b% i- F
__main() 函数执行完,基本工作就做完了,这才跳转到用户代码的 main( ) 函数。
+ n4 P+ t0 `/ @5 F# i  v! N6 d: g# N# H/ x
/ ^* c" a2 f( V* P
2 `: n7 Z' K* @  T3 c. U6 L2 z3 v. w. e
20200723225010732.png
收藏 评论0 发布时间:2021-11-20 23:00

举报

0个回答

所属标签

相似分享

关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新和工艺
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版