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

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

[复制链接]
STMCU小助手 发布时间:2021-11-20 23:00
1.MCU 代码如何启动5 h- M# _7 I& T/ b
首先我们需要澄清一个问题,什么是 Startup Code,什么是 Bootloader?因为总看到有同学混用这两个概念。3 f, n' a7 q8 G8 h3 }7 O" T! L- v1 K
; `& C0 n4 N: u9 I. {3 t: s
Bootloader 可以译为引导程序。早期的单片机是没有 Bootloader 这种概念的。如大家熟悉的 MCS51,最初芯片内是不能存储代码的,需要外挂EPROM,就是下面这种带个小玻璃窗的存储器。擦除 EPROM 中的代码需要用紫外线照射几分钟才行。
- p; @7 ?! J* R2 Y/ {& V- @  D0 z* M; S  Y; L; U
20200207173108336.jpg
. g/ k9 J1 m4 E3 Q2 v  K! l
3 |. j- v: A5 Z# n1 _9 t
后来出现了 Flash 这种可电擦写的存储器,并集成在了单片机内部。但出厂的时候单片机的程序存储区仍然是空白的,没有任何代码。用户编译程序后,下载到单片机后才能运行。那么在产品发给用户后,如果发现有Bug怎么办呢?就得用编程器把新代码重新下载一次。这实在是有点儿麻烦,特别是如果客户距离很远的话。于是有聪明的程序猿想了一个办法,写一小段特殊的代码放在程序里,这段代码可以通过一定方式,比如用按键触发进入运行,它可以通过串口(早期的 PC 串口是标配)接收新的代码并写入Flash,从而在没有硬件编程器的情况下也能完成代码的更新。2 J9 O" {% n- f/ ^" u( u/ @

5 x6 T/ O" l/ U 程序猿们也是现代历史前进的重要推动力啊!
. \" M7 Q, H. B7 x% `- |1 {3 A  ^
后来,有芯片厂商把这种代码在出厂时就固化在芯片里,极大的方便了代码下载和程序更新。STM32F030内部就固化了Bootloader。当我们把一个引脚 BOOT0 拉高的同时,重新给芯片上电或复位,就会触发Boootloader进入运行。此时我们通过单片机的串口就可以把新程序发送给单片机,发送完后把 BOOT0 拉低,再复位单片机,新程序就会运行起来。
5 v5 _9 w. v3 T4 f$ \1 D
3 V* r" M# H; `# ~& X- u$ ]Startup Code 可以译为启动代码。单片机上电或复位后最先执行的一段代码。一般主要会完成堆栈指针的设置,复位向量的获取和加载,然后初始化变量,最后跳转到用户代码。在详细看启动代码之前,我们先看一下 STM32F030 的内存映射。4 a+ j# j/ G+ K; v9 \  E7 G) B* k# ?! x
6 x6 r: j* n4 D: F  \
2.STM32F030内存映射(Memory Map)
3 x; ?5 ~# I% C0 W' d下面是 STM32F030 的内存映射,其它芯片会因为 Flash,SRAM 空间大小不同而略有不同。
. ~  V! w& N# {4 T' F' T5 v3 X+ Q& v; |# r; f
20200207173223663.png

& p& E) I) u( D. `" R& F
: @) @) m5 h( W) E! r* j因为是32位机,所以可寻址从 0x0000_0000 到 0xFFFF_FFFF 的总共 4G 空间。
) d" t* ^8 e% J' c5 @9 x
! ^. q2 W% J: G这是采用32位机的好处,地址空间足够用。不像8位或16位机,很容易出现地址空间不够用,动不动就需要用 Page 来间接寻址。, Z9 |4 ^4 Z# m
) [" i' n* |. H
我们从低地址到高地址逐段看一下:
2 x! u, T$ R3 U  i! M2 e1 i5 s& X0 i# Q2 J
0x0000 0000 Virtual memory+ V6 ^" _6 h4 ~
7 v4 Z1 T, [) z$ @: r# a! S+ Z
这段地址空间,会因为不同的 BOOT 模式而映射到不同的物理内存。
7 R1 g) Q8 a7 Q" r4 O8 e; t4 o( \& F% G; I
当芯片复位,或从 Standby 低功耗模式唤醒时:, X" q) x8 [9 }* B1 [* N
2 w# o3 s# p" G8 U
如果引脚 BOOT0 是被拉低的,将映射到 Flash memory。这是最常用的代码运行模式;
' ~+ q+ p& M# V) b, I; g: y2 C8 k8 R; W
如果引脚 BOOT0 是被拉高的,且nBOOT1为 1 ,将映射到 System memory。进入bootloader模式;: \) Y' s9 O+ n* d0 f

5 |" Y  G. k2 L  O9 W& P; G& R$ w3 f. L如果引脚 BOOT0 是被拉高的,且nBOOT1为 0 ,将映射到 SRAM。
7 }  v: [& E) |7 s
* o9 K) v3 q  f: X8 R2 i6 T注:nBOOT1 为Flash寄存器中的一位,用户何以设置。
2 O) z" q5 y9 c0 T; o# x5 X' L) Z0 ?) ]
0x0800 0000 Flash memory
+ K- h1 Y$ I% z# O! U/ c$ @; p存放用户代码
7 h% K8 [8 f. Z% O" b' G/ T0 N
" u+ \5 N3 f) [- k7 @$ _' T0x1FFF EC00 System memory
8 _; ]) f, V) F, h: t/ ?1 r4 @存放 bootloader, 片内集成温度传感器的校正数据,和片内集成电压参考的校正数据
0 }- h0 O1 o, L$ ?) T4 ?  Q这些代码和数据是在工厂固化好的。3 ^% S& d. ]3 Q/ C- W6 A! \- O7 c
" s# j' b/ r# O& n4 M, v# P7 @
0x2000 0000 SRAM
" L6 j+ r0 y3 r4 m7 e' O, A5 H存放用户变量,堆(Heap)和栈(Stack)。也可以把代码加载到 SRAM 运行。7 H7 f6 F' H% N8 @8 p& P; i9 N

' q8 J" B) g# W! R+ C0x4000 0000 Pheriperals
6 o( Y* H4 D, `5 }芯片集成的外设,如 USART, SPI, GPIO等的寄存器地址在这一区域。
; p5 q9 t/ K/ w4 }5 N
, B0 W4 l) i, h. Z3 D2 M; y0xE000 0000 Cortex-M0 internal pheriperals
' F" u5 X  v8 d2 S1 DM0内核的外设映射到此区域。如 systick (System Tick),NVIC,Debug Registers。这些寄存器在芯片手册里是查不到的,需要到 ARM 的手册里查找。8 Q7 y/ m3 [' U6 t. f. O
, d6 p3 y/ n& x/ k" F# m) V
3. 启动代码(Startup Code)/ o" U" F$ O; Z! K# v$ I
我们还是以下面这个最简单的GPIO翻转代码为例:# M  D- X0 q, p0 X9 Q1 e/ v
STM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\, s. l2 X1 d2 H" o. h
Examples\GPIO\GPIO_IOToggle\MDK-ARM\Project.uvprojx
" q' H2 b  k% K- X" e9 P) V. j把此工程下载到单片机后,用调试器观察下面两个地址的内容:
8 {; j7 c/ u. z% `我们会发现0x0000_0000开始的区域, 和0x0800_0000开始的区域,内容完全相同。这说明Flash 区的内容映射到了 0x0000_0000起始的这一段地址区域。8 U9 |  ], n8 S( T$ H% X7 P

( W' S) X2 D- \6 M
20200207173410739.png

3 f4 r- {2 l. P1 |+ e  z# n  c2 n) C; a4 U) j& D
注意STM32F030使用的是小端模式(Litlle Edian)。
4 V( Y+ x( Y; C1 H( F2 w
5 [$ K! D! Z+ M3 [不同于 MCS51 在 0x0000 放的是复位向量,STM32F030 还有其它 ARM 芯片在零地址存放的是初始堆栈指针地址。1 \4 S$ d3 H" a+ f6 R% W
4 I  T$ ?/ \% Z! ]9 N+ L6 B& l
0x0000 0000: (0x2000 0428) 初始堆栈指针
! b* X. H5 Q( v  N
6 J; w. v+ e4 j$ s# v" c: }0x0000 0004: (0x0800 00C9) 复位向量,上电或复位后最先加载入PC
2 K' n( G$ e' P3 d
3 i5 x4 S, {! m# e* T% W注:单片机上电或复位后,堆栈指针初始化和 PC 初始值的加载总是从地址 0x0000_0000,0x0000_0004获取。在上面这种用户模式下,实际是从 Flash 区的 0x0800_0000,0x0800_0004 获取的。' f8 w0 p7 ?" t3 a- ]8 ^

# Z$ E, m# M6 n, M我们可以通过调试器观察一下芯片复位后 M0 内核的寄存器:9 U8 S( y0 s# ^* @

2 e5 h* {0 [8 m0 K3 H# g) B8 h# L- x
20200207173413306.png
! S% j' T, ~( [1 _; ^) @! ]& h
' X% h, J% z* L$ U
细心的同学这时可能发现了一个问题。) S7 N2 [* k7 D: U5 `  H
2 u! M# b, K- `7 l/ S
堆栈指针 SP 的内容和前面存储器中的内容是对的上的。但是 PC 里的内容好像对不上啊?PC 里的值是 0x0800_00C8,存储器里明明是 0x0800_00C9 啊!
8 Z% m5 _& {1 Q- |' V% b( y6 b+ x2 D# w* Q8 k
这里牵涉到了 ARM 体系里的两种工作状态 ARM 和 Thumb。ARM 状态下执行32位指令,Thumb状态下执行16位指令。那么如何在这两者之间切换呢,一个方法就是靠跳转地址的最低位(Bit0), 当 Bit0 设为 1 时进入 Thumb 状态,当 Bit0 设为 0 时进入 ARM 状态。" l2 |. e# u' t, m' O

' b/ N2 }: @# Q  X5 ~/ [) }6 I对于单片机来说,16位的 Thumb 指令就足够了,而且16位指令比32位指令能节省存储器空间。所以 M0 内核只支持 Thumb 指令。2 Y: K; j) d0 x. c& }6 F

" H: X8 G7 J+ A- H1 L; i到这里我们就可以理解复位向量为什么是 0x0800_00C9 了。
5 x" [3 N. z# z. [7 i$ |' @
/ _( m  ]: ?) K5 x* H4 t接下来我们来看复位向量 0x0800_00C8 指向的第一条指令:+ _9 z/ ]% ~1 i5 {% [8 P8 w

: O$ D# D6 A( S& {- X, }: c& G& o
20200207173440317.png
2 d; i# {8 S$ G/ |  o' [
- O2 T. t9 {  T6 L* z8 M7 a" P$ i

, E0 [# Y4 M2 L+ v6 L
6 f. C& r6 U# o* P8 m% }单片机将要执行的第一条指令 0x4804,这是什么意思呢?
% D, H, Z" y# w- T5 F. u, F+ ?4 R2 R4 w* m9 ]  O0 h& X3 J; q$ t
先说结论:它就是下图中,单片机复位后光标指向的这条指令:1 y5 }1 s5 \) f2 o' H% `
$ y4 y7 S: `4 U6 L5 y3 R5 e$ s
LDR     R0, =SystemInit2 S, u5 V6 a' O

( D/ E+ a4 V6 {8 N4 ]2 `
20200207173500363.png

% `$ w, c$ j4 d: o) s: H$ r2 [1 x( p: C2 l4 F0 P* ^6 G/ ^9 j
) s! b& D8 g5 n* x- _

/ l8 ?* ]  K2 K, K在这里详细解释一下 0x4804 这条指令:
+ W* p& W0 E2 W" M5 @8 a" B$ H' |/ a& u
它对应的机器码是 0100100000000100) @/ d# ^) z6 q+ Q& z

9 @( v; _) _# h9 u  KBit15 to Bit11 (01001)为LDR(literal)指令,既从PC偏移地址取数据送至寄存器Rt。
, U/ B; S( ^% Z
' G' |# Q( S4 S' T. N* y3 WBit10 to Bit8  (000)表明目的寄存器Rt为 R0
5 j( V2 M, k/ v2 d. ?( Q" p/ T0 Z4 F, @9 t% d' @
Bit7 to Bit0  (00000100)表明相对于 PC 的偏移量为 0b10000,既0x10。
9 ~3 Q, \" m" z
, F6 ^; @+ t/ J, C2 o注意PC的值是当前地址+4。
; M/ x5 v1 `, B0 {8 \
9 _$ v$ L7 \" m  a那么从 0x080000C8 + 0x4 + 0x10 = 0x080000DC 取出数据 0x0800092D 送至寄存器 R0。此地址是 SystemInit( )函数的地址。下一条语句 BLX R0 就是调用此系统初始化函数。5 W5 z0 w' G$ }, D. P
/ w/ m) }8 R6 y4 l3 D5 b% P
SystemInit( ) 这个函数在 system_stm32f0xx.c 这个文件里,主要完成系统时钟的初始化。可以点进去看一下具体的内容。# r. m. f9 t  h( u$ x

  c. Y4 D1 ?" B
20200207173508532.png

1 X8 i- J+ z% {4 U! L& Z2 u4 B) R/ l) ~% w) {, h

/ q5 y. V8 T8 b3 w9 ^% q0 P* [6 ^
! a3 L- o1 L! Y! U# \0 l函数 SystemInit( ) 执行完之后,程序跳转回来,取得 __main( ) 函数的地址,跳转到 __main() 函数执行。需要注意,这个函数不是我们用户代码里的 main( ) 函数。* p5 U+ l7 \+ e% q* I* z
2 W' t+ D% w& g* L
__main() 函数是 Keil 的库提供的,我们看不到代码,它主要完成变量的初始化。这里不用太纠结,如果想进一步深究可以看一下 ARM Compiler User Guide 的 Reset and initialization 这一节。2 V2 M* S% S5 |; f  \. L( `
' f8 J$ Z+ Q! M6 j
__main() 函数执行完,基本工作就做完了,这才跳转到用户代码的 main( ) 函数。
) u  I+ \+ s: T( F) J! W% o- \
( y2 t9 x" g$ T' Y( Z0 _/ U2 m/ C+ y% P) Q& Z4 M
( X5 O: Z: q. O
20200723225010732.png
收藏 评论0 发布时间:2021-11-20 23:00

举报

0个回答

所属标签

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