STM32详解1
* M/ |% C D( q v7 ^) D一、在进入主题之前我们先了解一些必要的基础知识----stm32系列芯片的种类和型号:
2 H: F+ u0 s! jstartup_stm32f10x_cl.s 互联型的器件,STM32F105xx,STM32F107xx3 T4 q* f9 x. T7 H& F. d
startup_stm32f10x_hd.s 大容量的STM32F101xx,STM32F102xx,STM32F103xx" h! G, C# k) z& ` j
startup_stm32f10x_hd_vl.s 大容量的STM32F100xx, H* T3 L5 [. M {- K8 G: i2 v
startup_stm32f10x_ld.s 小容量的STM32F101xx,STM32F102xx,STM32F103xx1 e* B, R, v" n: T6 Q
startup_stm32f10x_ld_vl.s 小容量的STM32F100xx
: R: W7 v1 D9 H3 Q$ W! `startup_stm32f10x_md.s 中容量的STM32F101xx,STM32F102xx,STM32F103xx
$ n! E6 A. p$ a4 U* Jstartup_stm32f10x_md_vl.s 中容量的STM32F100xx (我项目中用的是此款芯片 stm32f100CB)
& Z7 Y; R9 ?* s: I9 {6 _" z+ C+ Hstartup_stm32f10x_xl.s FLASH在512K到1024K字节的STM32F101xx,STM32F102xx,STM32F103xx
% O% x- g6 ?) p9 d, N# N
& L2 M: z1 q1 ^7 p/ s; m- v; q' _, z5 f- t
cl:互联型产品,stm32f105/107系列% U6 t$ Q& t+ F5 ` t1 T
vl:超值型产品,stm32f100系列
4 b1 m) o/ s; w' L; bxl:超高密度产品,stm32f101/103系列
- K. B0 ?% h) v2 }( @9 q8 _ld:低密度产品,FLASH小于64K7 I# s" T l0 ~6 v
md:中等密度产品,FLASH=64 or 1288 ]) N$ u6 U# t0 ]; o
hd:高密度产品,FLASH大于128
5 n% i; n4 I1 Z1 J- b. d; B; g( |. Z* G8 j, R' }' F2 j9 v/ c
+ k( K& J7 D9 p8 W7 m6 I二、在拿到ST公司官方的IAP 程序后 我们要思考几点:
3 z0 @+ J: _0 q1.ST 官方IAP是什么针对什么芯片型号的,我们要用的又是什么芯片型号;
9 D8 f- {" q% a5 R' t/ F2.我们要用官方IAP适合我们芯片的程序升级使用,要在原有的基础上做那些改变;
& O! O4 v! t2 I2 C. P+ H, _3 G $ M' z- L. E+ G& H: R q7 \7 d
初略看了一下IAP源码后,现在我们可以回答一下上面的2个问题了:
4 q- w) c0 o. U1.官网刚下载的IAP针对的是stm32f103c8芯片的,所以他的启动代码文件选择的是 startup_stm32f10x_md.s,而我的芯片是stm32f100cb,所以我的启动代码文件选择的是 startup_stm32f10x_md_lv.s
& L9 T1 w3 t; |( H5 L* g, K+ s( H: K! x- z1 [% f o' Z0 d& u
2 .第二个问题就是今天我们要做详细分析才能回答的问题了;' m* V! a% Q3 }* z1 N
(1).知道了IAP官方源码的芯片和我们要用芯片的差异,首先我们要在源码的基础上做芯片级的改动;, r$ J. g" S8 H0 t) M' J9 b
A.首先改变编译器keil的芯片型号上我们要改成我们的芯片类型---STM32F100CB;) {4 @8 Z/ J0 y8 u. g
B.在keil的options for targer 选项C/C++/PREPROMCESSOR symbols的Define栏里定义,把有关STM32F10X_MD的宏定义改成:STM32F10X_MD_VL K" h H f: _ x
也可以在STM32F10X.H里用宏定义
3 V. ]0 T0 U6 `5 ^ e2 x - /* Uncomment the line below according to the target STM32 device used in your
& y- n8 U9 D0 A- [5 r8 |$ v - application
. y- H9 i B: I( M. j6 N5 D, z - */2 Q( @' X* M7 ~6 ^% i8 ]
- @9 S k$ ~/ ?
: G$ U! l" V1 p- #if !defined (STM32F10X_LD) && !defined (STM32F10X_LD_VL) && !defined (STM32F10X_MD) && !defined (STM32F10X_MD_VL) && !defined (STM32F10X_HD) && !defined (STM32F10X_HD_VL) && !defined (STM32F10X_XL) && !defined (STM32F10X_CL) : ^/ E2 f0 Y% t. V5 R
- /* #define STM32F10X_LD */ /*!< STM32F10X_LD: STM32 Low density devices */
) D( H/ W/ c4 J7 [' Q - /* #define STM32F10X_LD_VL */ /*!< STM32F10X_LD_VL: STM32 Low density Value Line devices */ - L/ e" D+ D' U! q* N5 Z
- /* #define STM32F10X_MD */ /*!< STM32F10X_MD: STM32 Medium density devices */
2 W8 K9 K+ H x# D: n( ] - #define STM32F10X_MD_VL /*!< STM32F10X_MD_VL: STM32 Medium density Value Line devices */
N; Q# r, F! q7 f7 _2 U2 P - /* #define STM32F10X_HD */ /*!< STM32F10X_HD: STM32 High density devices */- {1 o$ {9 R% s, ^3 h. I% K
- /* #define STM32F10X_HD_VL */ /*!< STM32F10X_HD_VL: STM32 High density value line devices */ 5 D' l7 X# x% b- l
- /* #define STM32F10X_XL */ /*!< STM32F10X_XL: STM32 XL-density devices */' N9 K( U* I0 M& Y t" S
- /* #define STM32F10X_CL */ /*!< STM32F10X_CL: STM32 Connectivity line devices */& J' P. ]' z; y4 D
- #endif
复制代码
2 p$ U+ o1 z$ K7 S1 C5 A
8 [" j$ |1 }4 u2 Y! Z7 e上面代码说的是如果没有定义 STM32F10X_MD_VL, 则宏定义 STM32F10X_MD_VL
7 z# I# Y! ?. i1 U, M& rC.外部时钟问价在stm32f10x.h 依据实际修改,原文是 说如果没有宏定义外部时钟HES_VALUE的值,但是宏定义了stm32f10x_cl 则外部时钟设置为25MHZ, 否则外部时钟都设置为8MHZ; 我用的外部晶振是8MHZ的所以不必修改这部分代码;- #if !defined HSE_VALUE
6 w. L2 n- ^( H6 t( c6 V6 ~; F( S8 Q' @ - #ifdef STM32F10X_CL
9 }; p: y4 Q1 D4 H Y+ @0 g8 Z - #define HSE_VALUE ((uint32_t)25000000) /*!< Value of the External oscillator in Hz */: L8 K, V4 f- S K* c
- #else $ h1 P, ]1 H _0 m" C# A
- #define HSE_VALUE ((uint32_t)8000000) /*!< Value of the External oscillator in Hz */1 m9 Z. ?9 Y( M7 G' k1 ^+ ]2 o) G
- #endif /* STM32F10X_CL */4 ~1 Z1 i2 @- C% b
- #endif /* HSE_VALUE */
复制代码
( z$ @) N9 ^. w b2 m# ^
* \ V- }- X8 t% |$ o, _D.做系统主频时钟的更改' v! C. e( ]/ L
system_stm32f10x.c的系统主频率,依实际情况修改 ;我用的芯片主频时钟是24MHZ;
: S7 A1 b+ z8 H. ]" V+ d$ u - #if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL), t; G$ _, R0 d; m8 f9 d/ d ?
- /* #define SYSCLK_FREQ_HSE HSE_VALUE */$ R; x' h9 V4 X( i% ~' s7 P* B
- #define SYSCLK_FREQ_24MHz 24000000
# h j( Z: S* { g - #else) _+ w8 I: D2 r
- /* #define SYSCLK_FREQ_HSE HSE_VALUE */
. C. M* |& j8 _ Y- ]. n- a w! P - #define SYSCLK_FREQ_24MHz 24000000
2 M* \9 r5 \ J2 X9 w - /* #define SYSCLK_FREQ_36MHz 36000000 */
, A6 e+ b( R4 P$ H; A* t - /* #define SYSCLK_FREQ_48MHz 48000000 */
. S$ H2 }/ s; C* ? s - /* #define SYSCLK_FREQ_56MHz 56000000 */) J ^: f3 L" z2 c- [, s5 E" U
- /*#define SYSCLK_FREQ_72MHz 72000000*/
! \: X. x' ] ?; R5 {- X- v! L - #endif
复制代码
2 ^* V9 _- N: _0 ]# b' r) x. l$ M: c W9 d& {
E.下面是关键部分操作了,在说这部分操作前我们先来说一下内存映射:5 j& B' m) L" h3 R/ \* Q' m* I9 Q4 L
下图在stm32f100芯片手册的29页,我们只截取关键部分7 s3 _. R6 G9 L, ?) i% r7 e
1 e+ s- E0 i# n- G$ h
) K- x; o7 k5 O y7 \) o: K( C$ u, z8 r& {8 l
- Z+ B7 k/ [: G; m% \
从上图我们看出几个关键部分:
- e) M) u& s D6 r" |3 M7 }0 _! m1.内部flash 是从0x0800 0000开始 到0x0801 FFFF 结束, 0x0801FFFF-0x0800 0000= 0x20000 =128k 128也就是flash的大小;8 A2 `, x6 F( m! H# \
2.SRAM的开始地址是 0x2000 0000 ;9 N" [' R, ^) ]5 Q0 L4 B- S
我们要把我们的在线升级程序IAP放到FLASH里以0x0800 0000 开始的位置, 应用程序放APP放到以0x08003000开始的位置,中断向量表也放在0x0800 3000开始的位置;如图 E, B+ W6 E9 ~0 M
5 {5 x7 I5 c0 |- Y/ Z, G# I2 J
% ^9 i9 L y k* @3 t8 _, G* d- ?7 m: B; M$ R7 }+ m: Q: a
/ J) X1 K' `+ S) \# C# K( l所以我们需要先查看一下misc.h 文件中的中断项量表的初始位置宏定义 为NVIC_VectTab_Flash 0x0800000" x: o5 O% r/ J
那么要就要设置编译器keil 中的 options for target 的target选项中的 IROM1地址 为0x0800 0000 大小为 0x20000即128K;
0 |3 D8 }3 d2 I. }$ L IRAM1地址为0x2000 0000 大小为0x2000;# I4 L, f1 m! Q* q0 r9 l
(提示:这一项IROM1 地址 即为当前程序下载到flash的地址的起始位置)
2 {) g7 g9 O( k下面我们来分析一下修改后的IAP代码:- /*******************************************************************************
5 ~* @) G4 ] Q: U - * @函数名称 main
; B% p8 @$ W% s! h - * @函数说明 主函数 j @" M9 \6 O" K
- * @输入参数4 j' ~4 W8 h* v I
- * @输出参数
' T# s, N# A2 \& `5 I5 Q# H+ L- V - * @返回参数4 x4 k6 C3 t' ^7 m$ Q
- *******************************************************************************/
0 x8 a! `9 M0 q( d/ J2 n - int main(void){ //Flash 解锁 FLASH_Unlock();
+ m( Q2 Z p( b/ h! i5 L8 } - KEY_Configuration() ;
9 m0 ^- V% A( i. L: a2 N3 X( B# Q - //配置串口1
6 R. s. y1 X# e) x% S/ k - IAP_Init(); ; K0 U, E7 Y- i1 n. t
- //PA15管脚是否为低电平6 n5 @& q( e2 l8 C8 D
- if (GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_15) == 0x00) {
1 W' }. x! C* I - //执行IAP驱动程序更新Flash程序
! U# U* y4 f3 j/ ~ - SerialPutString("\r\n======================================================================"); SerialPutString("\r\n= (C) COPYRIGHT 2011 Lierda ="); SerialPutString("\r\n= ="); SerialPutString("\r\n= In-Application Programming Application (Version 1.0.0) ="); SerialPutString("\r\n= ="); SerialPutString("\r\n= By wuguoyan ="); SerialPutString("\r\n======================================================================"); SerialPutString("\r\n\r\n"); Main_Menu (); }
( O1 R7 w$ k2 }/ Q J' K - //执行用户程序
: Y" G/ K Y: p5 p2 s$ R9 r. y1 n - else {
- B9 W- j: L# Q8 X7 \, ` - //判断用户已经下载程序,因为正常情况下此地址是栈地址
5 w$ C9 B& P# Q- I6 T - //如没有这一句话,即使没有下载程序也会进入而导致跑飞# j* `/ X! O- D! d( ?/ C
- if (((*(__IO uint32_t*)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000) { SerialPutString("Execute user Program\r\n\n");( \# N4 h6 U n" x+ \( ?4 M: W i
- //跳转至用户代码; |( Z" U/ n! \" `8 r8 i d: `# n
- JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4); Jump_To_Application = (pFunction) JumpAddress;
! a' j' I3 I% `* c' ^! F% w: g! ~ - //初始化用户程序指针的堆栈指针
/ |4 |9 E; V% E' }( n- B7 b% W+ C - __set_MSP(*(__IO uint32_t*) ApplicationAddress); Jump_To_Application(); } else { SerialPutString("no user Program\r\n\n"); } } while (1) { }}
复制代码
1 f$ |2 b3 Q& [
% m+ M* v4 c0 Q8 U* w- H这里重点说一下几句经典且非常重要的代码:8 k8 W! I; k: c$ Q6 P
第一句: if (((*(__IO uint32_t*)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000) //判断栈定地址值是否在0x2000 0000 - 0x 2000 2000之间
, N* l* }: ~0 \. }( \: n& {1 Q怎么理解呢? (1),在程序里#define ApplicationAddress 0x8003000 ,*(__IO uint32_t*)ApplicationAddress) 即取0x8003000开始到0x8003003 的4个字节的值, 因为我们的应用程序APP中设置把 中断向量表 放置在0x08003000 开始的位置;而中断向量表里第一个放的就是栈顶地址的值9 y& G& C( ]: M, G3 z
也就是说,这句话即通过判断栈顶地址值是否正确(是否在0x2000 0000 - 0x 2000 2000之间) 来判断是否应用程序已经下载了;
! C* I" ~% V8 V! p( k/ Y
0 f+ {6 x1 u& L% ]
# a3 u# Y' z/ F% S
0 d/ i8 I! h' x' k& m; Q8 D; c9 p* h& S8 y, c* F1 ~
第二句: JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4); [ common.c文件第18行定义了: pFunction Jump_To_Application;]: j) x2 V- s* y0 Z
+ p2 n$ R. [2 H8 L) H. k1 d9 h5 O5 C
ApplicationAddress + 4 即为0x0800 3004 ,里面放的是中断向量表的第二项“复位地址” JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4); 之后此时JumpAddress
) n; @2 }, {; r+ V第三句: Jump_To_Application = (pFunction) JumpAddress;
* N, P, |4 j8 A startup_stm32f10x_md_lv. 文件中别名 typedef void (*pFunction)(void); 这个看上去有点奇怪;正常第一个整型变量 typedef int a; 就是给整型定义一个别名 a3 d f/ M' @9 X+ b
void (*pFunction)(void); 是声明一个函数指针,加上一个typedef 之后 pFunction只不过是类型 void (*)(void) 的一个别名;例如:
$ ^. p5 f6 t7 l: a! M, d8 WpFunction a1,a2,a3;
% ~* z0 u) {7 _: P
! {$ u; Z- P+ R$ p. c' A; J, ^void fun(void)
$ p8 F( F3 j/ T- Q' b6 n+ i{
n E4 }# A; N# [ ......8 f- R+ i; t, s2 {0 n, v3 X
}# }( g* z9 Z6 C' E5 Y9 z
, r X+ |- j4 m) Y8 ]+ ` ]* x/ Ca1 = fun;2 o$ k& |( L& [! b5 P# S
所以,Jump_To_Application = (pFunction) JumpAddress; 此时Jump_To_Application指向了复位函数所在的地址;3 x# @ _# F* B$ P$ _1 I+ V" U
第四 、五句: __set_MSP(*(__IO uint32_t*) ApplicationAddress); \\设置主函数栈指针1 G k" W8 k4 @3 _! H G
Jump_To_Application(); \\执行复位函数4 Z p; O5 g s% I- Q- p+ z% g# o
我们看一下启动文件startup_stm32f10x_md_vl。s 中的启动代码,更容易理解
( ^: D1 w% {! w k5 ^7 U
5 L6 F* g, t6 n& V# f( G9 J" n( B+ s" p
: u" _+ U5 R1 A+ u! r% r6 |( v$ X5 Z5 c P) T
" c8 b: O2 ?7 Q1 R% p1 A' |( m2 I
三、我们来简单看下启动文件中的启动代码,分析一下这更有利于我们对IAP的理解: (下面这篇文章写的非常好,有木有!)
( x1 B. N. f; ^( F& _: r解析STM32的启动过程
1 s) n& A# s$ o. U当前的嵌入式应用程序开发过程里,并且C语言成为了绝大部分场合的最佳选择。如此一来main函数似乎成为了理所当然的起点——因为C程序往往从main函数开始执行。但一个经常会被忽略的问题是:微控制器(单片机)上电后,是如何寻找到并执行main函数的呢?很显然微控制器无法从硬件上定位main函数的入口地址,因为使用C语言作为开发语言后,变量/函数的地址便由编译器在编译时自行分配,这样一来main函数的入口地址在微控制器的内部存储空间中不再是绝对不变的。相信读者都可以回答这个问题,答案也许大同小异,但肯定都有个关键词,叫“启动文件”,用英文单词来描述是“Bootloader”。
( [: `* r, U" T& l无论性能高下,结构简繁,价格贵贱,每一种微控制器(处理器)都必须有启动文件,启动文件的作用便是负责执行微控制器从“复位”到“开始执行main函数”中间这段时间(称为启动过程)所必须进行的工作。最为常见的51,AVR或MSP430等微控制器当然也有对应启动文件,但开发环境往往自动完整地提供了这个启动文件,不需要开发人员再行干预启动过程,只需要从main函数开始进行应用程序的设计即可。$ y+ Q0 [* O& x! M
话题转到STM32微控制器,无论是keil
! Z5 g, ^! B$ ?uvision4还是IAR EWARM开发环境,ST公司都提供了现成的直接可用的启动文件,程序开发人员可以直接引用启动文件后直接进行C应用程序的开发。这样能大大减小开发人员从其它微控制器平台跳转至STM32平台,也降低了适应STM32微控制器的难度(对于上一代ARM的当家花旦ARM9,启动文件往往是第一道难啃却又无法逾越的坎)。
0 u# e6 P( G( \0 X相对于ARM上一代的主流ARM7/ARM9内核架构,新一代Cortex内核架构的启动方式有了比较大的变化。ARM7/ARM9内核的控制器在复位后,CPU会从存储空间的绝对地址0x000000取出第一条指令执行复位中断服务程序的方式启动,即固定了复位后的起始地址为0x000000(PC = 0x000000)同时中断向量表的位置并不是固定的。而Cortex-M3内核则正好相反,有3种情况:4 a5 b( E/ s4 J- O T
1、 通过boot引脚设置可以将中断向量表定位于SRAM区,即起始地址为0x2000000,同时复位后PC指针位于0x2000000处;2 c# c% i) A* P6 G
2、 通过boot引脚设置可以将中断向量表定位于FLASH区,即起始地址为0x8000000,同时复位后PC指针位于0x8000000处;: E5 l# C5 \& g0 X- W/ }
3、 通过boot引脚设置可以将中断向量表定位于内置Bootloader区,本文不对这种情况做论述;1 e9 q% Y$ K- u8 g, f
而Cortex-M3内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在Cortex-M3内核复位后,会自动从起始地址的下一个32位空间取出复位中断入口向量,跳转执行复位中断服务程序。对比ARM7/ARM9内核,Cortex-M3内核则是固定了中断向量表的位置而起始地址是可变化的。
& C. u- n! S' V1 O; s有了上述准备只是后,下面以STM32的2.02固件库提供的启动文件“stm32f10x_vector.s”为模板,对STM32的启动过程做一个简要而全面的解析。
- n0 V) v( S2 g程序清单一:
$ F; p. X) e6 @) B# ^0 O( n4 L/ ?) s4 \- ;文件“stm32f10x_vector.s”,其中注释为行号
5 B; V# w. F3 t. k! Z9 g - DATA_IN_ExtSRAM EQU 0 ;1
( i& t- ^6 F$ q T - Stack_Size EQU 0x00000400 ;25 f5 t$ b3 _: g7 Q8 D
- AREA STACK, NOINIT, READWRITE, ALIGN = 3 ;3
) D F4 [* L) c" r* k* @) r - Stack_Mem SPACE Stack_Size ;4
3 q5 f1 j& f9 e1 G u+ k. T - __initial_sp ;5/ @. _) B+ x+ ~/ u* }1 R" @9 v
- Heap_Size EQU 0x00000400 ;64 G& m, h6 u1 n
- AREA HEAP, NOINIT, READWRITE, ALIGN = 3 ;7
h0 N5 h( c {: u' b - __heap_base ;8& Z" l9 v' G! D" ^: ~: F
- Heap_Mem SPACE Heap_Size ;9
, L4 I( W% A. Y( F1 g c5 H" j - __heap_limit ;102 j$ l9 t* r8 o' y; B
- THUMB ;11 S/ e d. W+ k6 }6 n& C+ X* O
- PRESERVE8 ;12
- t! t. H3 _) b5 e. ?8 ^# M8 z+ A* B - IMPORT NMIException ;13
$ A9 V8 S* V( a" @' [) M - IMPORT HardFaultException ;14 @4 h) s1 @, @9 y$ D" [" \# P
- IMPORT MemManageException ;155 t9 t$ t3 x0 e7 \* }2 j
- IMPORT BusFaultException ;16
$ r+ t1 v5 u* \ - IMPORT UsageFaultException ;17! |( Z1 W; \5 R9 O7 `* b
- IMPORT SVCHandler ;18
7 g3 R$ I; O! J6 `/ Z- m - IMPORT DebugMonitor ;19
- q0 [. K- u& u' [& D& F - IMPORT PendSVC ;20- N) B( X1 p# C& A: _2 `' G
- IMPORT SysTickHandler ;21
' z3 `& E( m2 a - IMPORT WWDG_IRQHandler ;22" B* @! x+ C/ H/ c; j* `
- IMPORT PVD_IRQHandler ;23
; G/ k& W E1 |, t' M* L8 _" q. c - IMPORT TAMPER_IRQHandler ;24
3 t5 A1 W" `& F b j" m; H+ T9 J - IMPORT RTC_IRQHandler ;25
% N8 x" E# T _" m - IMPORT FLASH_IRQHandler ;26
" w: ^' k+ F W - IMPORT RCC_IRQHandler ;275 i4 E5 J9 |/ Q
- IMPORT EXTI0_IRQHandler ;282 G! w4 b- u& }
- IMPORT EXTI1_IRQHandler ;29+ h$ S/ H4 ~4 b9 A* h A
- IMPORT EXTI2_IRQHandler ;30
4 y3 W" A* Z+ }" h+ ~ - IMPORT EXTI3_IRQHandler ;31
9 t% g& e# H- D4 v; y3 R! G" L3 L - IMPORT EXTI4_IRQHandler ;32
6 I% S4 Q6 z/ d8 @' b: x7 p - IMPORT DMA1_Channel1_IRQHandler ;33
8 R Y* z$ |! k: a t+ f) B- g - IMPORT DMA1_Channel2_IRQHandler ;34
. E! N" z2 I3 ^( ]: E - IMPORT DMA1_Channel3_IRQHandler ;356 t' `2 n$ |& p0 ^
- IMPORT DMA1_Channel4_IRQHandler ;36
: ], b, m1 Q6 R S - IMPORT DMA1_Channel5_IRQHandler ;37. v- l, r" [& Y3 P% O
- IMPORT DMA1_Channel6_IRQHandler ;38
/ m: L( m7 N) m4 i4 K, }- ]8 B7 j - IMPORT DMA1_Channel7_IRQHandler ;39
% C0 j" H: R$ H2 y, B& I! a1 c/ H1 } - IMPORT ADC1_2_IRQHandler ;40
% X: F3 F, Z; v, |4 A: ` - IMPORT USB_HP_CAN_TX_IRQHandler ;41
, j m3 J3 B7 ^5 i& B& t ^: W$ e2 J - IMPORT USB_LP_CAN_RX0_IRQHandler ;427 s: H; o2 T' X
- IMPORT CAN_RX1_IRQHandler ;43
. Q) i' _& S: a5 a# ?1 r- } - IMPORT CAN_SCE_IRQHandler ;440 M J0 i9 g' I" Z3 N
- IMPORT EXTI9_5_IRQHandler ;45
2 C; F: L/ [9 g: B! l - IMPORT TIM1_BRK_IRQHandler ;46: @6 L, c" F* n7 v
- IMPORT TIM1_UP_IRQHandler ;47
) B& D e- ^6 Y- I - IMPORT TIM1_TRG_COM_IRQHandler ;487 L% e: G9 W. t5 G
- IMPORT TIM1_CC_IRQHandler ;49
9 e" \5 w5 u6 X4 y& i& g% [ - IMPORT TIM2_IRQHandler ;50
3 F( u6 u, a: R- N7 K - IMPORT TIM3_IRQHandler ;51
! r# @' D; r8 r# Z! Z; w1 Q - IMPORT TIM4_IRQHandler ;52 B7 @* M. C- n& @2 {9 i$ d
- IMPORT I2C1_EV_IRQHandler ;53; J7 R' r2 l# U( P
- IMPORT I2C1_ER_IRQHandler ;54
% Y. r/ `: I) v+ [ - IMPORT I2C2_EV_IRQHandler ;55
! G- z* M, K" ?9 x) `! P - IMPORT I2C2_ER_IRQHandler ;56& o( y+ K4 ^" m/ l# M: {9 U
- IMPORT SPI1_IRQHandler ;57
' p4 g+ h; @, h - IMPORT SPI2_IRQHandler ;58
$ u- p% ^4 I9 O# f. }* z* G8 ~4 d - IMPORT USART1_IRQHandler ;595 a: |8 c8 z$ l) u: y
- IMPORT USART2_IRQHandler ;60
) w6 ?$ J: `/ C# |, i: K8 D - IMPORT USART3_IRQHandler ;61
6 ~8 l1 L1 ]7 Z6 A: v' Q - IMPORT EXTI15_10_IRQHandler ;62. Z/ V# @6 v" q' n# f
- IMPORT RTCAlarm_IRQHandler ;636 N1 r' r9 Z' X L$ ^; k) v$ j8 d) L
- IMPORT USBWakeUp_IRQHandler ;64
5 n- f5 k9 O0 L: M7 r, p0 J& ~ - IMPORT TIM8_BRK_IRQHandler ;65
# v2 p" U3 f0 f( T6 y% | - IMPORT TIM8_UP_IRQHandler ;668 z9 {/ ?( E9 ?$ x- u3 m
- IMPORT TIM8_TRG_COM_IRQHandler ;67! e7 M0 ~5 S$ w5 Q0 s% I
- IMPORT TIM8_CC_IRQHandler ;68! W* s3 Q+ b# H" C
- IMPORT ADC3_IRQHandler ;69
% w1 h7 s& r3 a5 k. [ - IMPORT FSMC_IRQHandler ;70
; @# S' \: t" e7 N4 F$ b - IMPORT SDIO_IRQHandler ;71" u6 n+ Y- f! _9 _
- IMPORT TIM5_IRQHandler ;72
, p$ \8 j+ N" U0 a5 `+ W7 ? - IMPORT SPI3_IRQHandler ;73
- a3 M' e. ~2 J- v - IMPORT UART4_IRQHandler ;74# x4 V% V: w5 p4 q/ r& ^
- IMPORT UART5_IRQHandler ;75
4 q3 @& [4 e" S" S - IMPORT TIM6_IRQHandler ;761 W' F7 W3 `/ J S. B `
- IMPORT TIM7_IRQHandler ;77
. o- ~+ U0 B. g9 M - IMPORT DMA2_Channel1_IRQHandler ;78 `4 j5 a2 X# W
- IMPORT DMA2_Channel2_IRQHandler ;79& m( }; C- ^2 ~# G% m2 z/ |( A
- IMPORT DMA2_Channel3_IRQHandler ;80; Q4 }# H/ D3 m1 }2 J
- IMPORT DMA2_Channel4_5_IRQHandler ;817 Z( @$ U6 `3 f) G f
- AREA RESET, DATA, READONLY ;82
" @: S- {4 n) {" l- d - EXPORT __Vectors ;83+ m0 x; d8 p; ~) R; F
- __Vectors ;840 }( I, \1 E. j* Z! S
- DCD __initial_sp ;85
- N, k* \, U" X8 W+ R& c" \ - DCD Reset_Handler ;86
- h% N9 l% y! j9 O) e0 c, M - DCD NMIException ;87/ ]2 H% Q; v. ~
- DCD HardFaultException ;88+ Y7 K, D, O B: O/ V& S
- DCD MemManageException ;89
( ~. y3 T% F, s( h - DCD BusFaultException ;90$ V+ Z2 v3 |. c" Q; J' d5 \
- DCD UsageFaultException ;91
; V) L$ w8 C& o" S - DCD 0 ;92. n% b% w4 N! w3 F5 n( V
- DCD 0 ;933 n* W0 K4 } J" X. c
- DCD 0 ;944 ]) S& l0 J% {# |7 z+ P
- DCD 0 ;95$ C' K9 d' F$ h- B7 @
- DCD SVCHandler ;96/ y v. S3 B& L( Z; W# b+ a
- DCD DebugMonitor ;97
8 D' T' t6 ~- W( T9 u/ q* H d9 x1 ] - DCD 0 ;984 ?! C4 @0 q- l; N( E1 m
- DCD PendSVC ;99$ ]7 b# q9 J; T
- DCD SysTickHandler ;100) k' h5 S p t( C: _
- DCD WWDG_IRQHandler ;101+ R0 [ t1 ?' y7 l+ _2 Q
- DCD PVD_IRQHandler ;102
8 m- h5 I6 z6 P - DCD TAMPER_IRQHandler ;1034 s' M& x# r. y% Q! Z+ v5 j
- DCD RTC_IRQHandler ;104
# P6 z6 f" Y5 L' ^" p1 T. O$ M - DCD FLASH_IRQHandler ;105
7 @: N6 b U& K8 L - DCD RCC_IRQHandler ;1067 K; U0 X. x" U6 b3 [/ f; g
- DCD EXTI0_IRQHandler ;107
6 _* J6 R6 Y/ H/ Q8 T! y - DCD EXTI1_IRQHandler ;108
% p7 T% X4 O/ O6 v - DCD EXTI2_IRQHandler ;109
7 `* R3 ]" G5 T, i# C( j: I+ f - DCD EXTI3_IRQHandler ;110* Q' L8 k, L4 z ~
- DCD EXTI4_IRQHandler ;1111 C7 g* Y# G) @) b
- DCD DMA1_Channel1_IRQHandler ;112/ Q# n, k0 U( {$ a
- DCD DMA1_Channel2_IRQHandler ;113
) ?# z- j4 m$ q- w6 U l0 z - DCD DMA1_Channel3_IRQHandler ;114+ Z% |$ S( }) z" d
- DCD DMA1_Channel4_IRQHandler ;115
9 f6 s% V/ t; a( T/ e& q" s - DCD DMA1_Channel5_IRQHandler ;1161 E7 ~% L$ q9 Q, Z2 k" z
- DCD DMA1_Channel6_IRQHandler ;117
k/ O5 m7 Z+ B - DCD DMA1_Channel7_IRQHandler ;1187 `/ ^; Q1 `9 o! K% L$ K
- DCD ADC1_2_IRQHandler ;119
! X% f9 U4 R$ G& p6 Q0 q - DCD USB_HP_CAN_TX_IRQHandler ;120
' B$ a& S$ ~0 a! E; I7 B# C - DCD USB_LP_CAN_RX0_IRQHandler ;121
6 O4 Y3 E2 w, ?, w' ~4 ` - DCD CAN_RX1_IRQHandler ;122
% m% J1 L6 `+ c5 E - DCD CAN_SCE_IRQHandler ;1236 f% V7 J3 H0 e
- DCD EXTI9_5_IRQHandler ;1244 `' c1 G5 F. q! m1 U9 _) O9 t
- DCD TIM1_BRK_IRQHandler ;125# ^3 v, } j+ c/ ?5 S
- DCD TIM1_UP_IRQHandler ;126* D* k# p L/ @6 [5 x
- DCD TIM1_TRG_COM_IRQHandler ;127$ m9 _: M& I* k7 O* O: e
- DCD TIM1_CC_IRQHandler ;128& \8 f" Q X% i1 S- O9 W
- DCD TIM2_IRQHandler ;129, O" Z, H' A6 {8 K: N V
- DCD TIM3_IRQHandler ;130* M: x! h+ |0 n ]# v
- DCD TIM4_IRQHandler ;1317 O( x" }5 v4 D6 G& D4 [
- DCD I2C1_EV_IRQHandler ;132
# H# ] b# c9 K! D' ~+ y - DCD I2C1_ER_IRQHandler ;133
& G% d1 B$ B2 r* r$ k, | - DCD I2C2_EV_IRQHandler ;134( O4 ^0 ]& l) T% ]
- DCD I2C2_ER_IRQHandler ;1357 @2 B2 W- e/ V9 N. J
- DCD SPI1_IRQHandler ;136
: L& d2 H( T7 j - DCD SPI2_IRQHandler ;137+ }& F/ U0 E) p% `
- DCD USART1_IRQHandler ;138
8 d P! V& k& T8 y& H) a - DCD USART2_IRQHandler ;139. a. i* Y2 `. E. x
- DCD USART3_IRQHandler ;140
% p g2 e9 [) H0 [7 k! N8 b - DCD EXTI15_10_IRQHandler ;1412 T. u, T; z! Y" k
- DCD RTCAlarm_IRQHandler ;142* ?8 Z2 R/ S: m$ b! d* m. S, M
- DCD USBWakeUp_IRQHandler ;143& z8 ?# f ?8 T
- DCD TIM8_BRK_IRQHandler ;144
+ }. v/ ~) }- @' P2 I; l - DCD TIM8_UP_IRQHandler ;145
. V R4 j) w, C - DCD TIM8_TRG_COM_IRQHandler ;1460 |4 u; f% f+ s W
- DCD TIM8_CC_IRQHandler ;147
5 }7 [/ p/ }6 s, G1 H: }+ S, ~ - DCD ADC3_IRQHandler ;148: x! J. X; A9 e }
- DCD FSMC_IRQHandler ;1490 O; q% }/ W' R* h* Y
- DCD SDIO_IRQHandler ;1507 j' r9 B. F, ?* }
- DCD TIM5_IRQHandler ;151 q+ I) F/ e; _6 r' F; Y" I& \5 g
- DCD SPI3_IRQHandler ;1525 ?2 u5 t% {0 H$ }, E- x4 t
- DCD UART4_IRQHandler ;153
) s! |) ? r9 V5 m( { - DCD UART5_IRQHandler ;154
% n3 G1 ?$ e' `, @3 i$ \4 n; S+ O7 l - DCD TIM6_IRQHandler ;155
# p; O& u. j( L( h) a - DCD TIM7_IRQHandler ;1566 j& i3 x7 c; L, d7 \/ ^- U8 c0 I
- DCD DMA2_Channel1_IRQHandler ;157! [5 T9 `0 `- N5 C2 u# ~
- DCD DMA2_Channel2_IRQHandler ;1589 Z9 H. t, y" c0 K% F0 @+ J9 H
- DCD DMA2_Channel3_IRQHandler ;159
1 w# Y4 K- c5 C0 K6 A" i9 z1 C - DCD DMA2_Channel4_5_IRQHandler ;160! G' b$ E- b# j' q
- AREA |.text|, CODE, READONLY ;161) o! o% y8 k3 n& j" I0 h
- Reset_Handler PROC ;162
+ v; y( e8 d. H2 J3 G - EXPORT Reset_Handler ;1632 J$ a; W ~6 q. ]; } t
- IF DATA_IN_ExtSRAM == 1 ;164
2 `( H, u4 O3 s( w - LDR R0,= 0x00000114 ;165% S- X d: J* D, ]+ X2 u
- LDR R1,= 0x40021014 ;1668 \, Z) R5 ^* Q# `! e
- STR R0,[R1] ;167
7 E! Q) }' l$ J7 d6 K1 Z - LDR R0,= 0x000001E0 ;168 S" ~: W" }7 [" W* h8 u
- LDR R1,= 0x40021018 ;169
3 _1 O$ F9 J" H( H, k - STR R0,[R1] ;170
4 Q# y r( t N i, b* d' D8 u - LDR R0,= 0x44BB44BB ;1710 X- ?+ t! i1 T/ e6 E
- LDR R1,= 0x40011400 ;172/ q8 z' q9 z7 b1 T
- STR R0,[R1] ;173
P; I# y* |' J* k: J) I - LDR R0,= 0xBBBBBBBB ;174
) d& ~# ], w8 P6 U4 O4 u0 z/ D8 e - LDR R1,= 0x40011404 ;175. P) C, b4 a" B5 |9 X" E
- STR R0,[R1] ;176
2 d: o% c5 W! p+ C - LDR R0,= 0xB44444BB ;177
, r; Z, _3 @( `+ o, f3 {9 c; l1 o - LDR R1,= 0x40011800 ;178# `) @/ e9 ?. a: |
- STR R0,[R1] ;179
0 X4 ^+ ~3 p! O0 f: u( i1 D$ g - LDR R0,= 0xBBBBBBBB ;180, J+ l) F% \. Z) G& z3 V: M/ K, w
- LDR R1,= 0x40011804 ;181
* n1 M" e9 y$ X0 s - STR R0,[R1] ;182
- s6 H( W( V( F) w ]& j - LDR R0,= 0x44BBBBBB ;183
; `/ ]. ~# O# o - LDR R1,= 0x40011C00 ;184& ~0 [8 l. ?7 _) r% Y
- STR R0,[R1] ;185
( K7 {7 u4 Q, o0 g5 i - LDR R0,= 0xBBBB4444 ;1864 ~: Y3 S- ~5 L
- LDR R1,= 0x40011C04 ;187
( U w& q( J8 w/ @- L - STR R0,[R1] ;188( J1 t7 D6 c) z4 L1 b
- LDR R0,= 0x44BBBBBB ;189
- K$ C W/ J' ?; B& w - LDR R1,= 0x40012000 ;190( E7 }. v6 _0 P
- STR R0,[R1] ;191
3 S& A* ]; e+ [9 q: q" O# c9 ^ - LDR R0,= 0x44444B44 ;192
( c1 j% d& ?9 J8 o% S5 i - LDR R1,= 0x40012004 ;193
% {( D, M/ O6 p+ D* ?- ]* F$ v - STR R0,[R1] ;1947 `* K' d& R" Y9 A) Y
- LDR R0,= 0x00001011 ;195" E7 S) [7 w' N5 ?8 C* _9 p8 C
- LDR R1,= 0xA0000010 ;196* p3 k/ j/ {% |5 |3 ~- l% P; g
- STR R0,[R1] ;197
, A' O& L+ P- \) A - LDR R0,= 0x00000200 ;1988 `% d* @% D" S! H: y: w
- LDR R1,= 0xA0000014 ;199
1 ?% r/ ~: ~( v) [! d - STR R0,[R1] ;200% L# y2 X! y. S$ t6 k" K
- ENDIF ;201
9 z5 t* B6 }: H& R1 p9 @ - IMPORT __main ;202' K' c9 k" v+ [
- LDR R0, =__main ;203% W- G1 t& t8 v3 b) u. m. |( f
- BX R0 ;204
2 {: N% Q: }" q - ENDP ;205
1 m/ X) T, b& |1 l+ ?& h$ q - ALIGN ;206
8 Z _+ U( m3 d) n! `/ ?* J" ]- b - IF :DEF:__MICROLIB ;207
9 [/ R1 {) D1 [) |: Y - EXPORT __initial_sp ;208
# T$ x& `9 Q6 Q9 u - EXPORT __heap_base ;209( M$ q8 d0 S1 K
- EXPORT __heap_limit ;210
# o2 }/ s0 s; O$ S* e# a/ {. A3 \ - ELSE ;211
0 T& u/ W3 J. F - IMPORT __use_two_region_memory ;212
% G9 s- m: C% D9 a, E( {. a; [ - EXPORT __user_initial_stackheap ;213- w0 S# I7 Y& ?# z
- __user_initial_stackheap ;2144 I! C. N# @# x+ X, L
- LDR R0, = Heap_Mem ;2153 ~& G7 [! p8 n7 G4 @( I
- LDR R1, = (Stack_Mem + Stack_Size) ;216" [( u w9 n, O' g4 m) e4 t1 F* [
- LDR R2, = (Heap_Mem + Heap_Size) ;217: y o4 G2 x+ _, O( G) k
- LDR R3, = Stack_Mem ;218
6 D/ c+ x$ E+ ^0 p" q0 f - BX LR ;219
: ]% I2 i6 U( Q0 f2 Q - ALIGN ;2206 H& r' R" a9 F& g8 h8 I* ]/ c) p7 z* z
- ENDIF ;2214 J$ ?, U# X- I: @" ~
- END ;222
$ i6 r6 G8 i2 E8 V; r$ ] - ENDIF ;223
+ {$ J1 s) ~$ q' u6 d" h - END ;224
复制代码
- u# l; e( v: w" P: A5 t& \1 y
$ d4 Y# U# }. E3 k* J+ P如程序清单一,STM32的启动代码一共224行,使用了汇编语言编写,这其中的主要原因下文将会给出交代。现在从第一行开始分析:, N: c$ T1 t+ y- @6 @* X
第1行:定义是否使用外部SRAM,为1则使用,为0则表示不使用。此语行若用C语言表达则等价于:
8 ~* M" k9 S) V/ n#define DATA_IN_ExtSRAM 0
7 y7 M5 q- z6 u( I/ g第2行:定义栈空间大小为0x00000400个字节,即1Kbyte。此语行亦等价于:
) Q/ [/ D7 l+ p( o& M1 ?#define Stack_Size 0x00000400
9 v' d* x# x1 u4 A第3行:伪指令AREA,表示9 T( t6 W2 q) q% W
第4行:开辟一段大小为Stack_Size的内存空间作为栈。
1 `+ y8 o; g/ U: s% G" X第5行:标号__initial_sp,表示栈空间顶地址。
$ m8 c! X8 M1 C5 j$ w$ x第6行:定义堆空间大小为0x00000400个字节,也为1Kbyte。
- D& E+ X, H) f" D( F: P第7行:伪指令AREA,表示 d* B6 B3 m: w% i' f$ I
第8行:标号__heap_base,表示堆空间起始地址。! |9 F( _3 W% ?& j& v
第9行:开辟一段大小为Heap_Size的内存空间作为堆。
: s& Q7 b% v, {7 i2 M第10行:标号__heap_limit,表示堆空间结束地址。& \) T7 c# W& S9 n$ R' p5 S+ r
第11行:告诉编译器使用THUMB指令集。3 q8 c- e7 M" i# W5 G
第12行:告诉编译器以8字节对齐。
- p9 `' a; t, Q+ ~第13—81行:IMPORT指令,指示后续符号是在外部文件定义的(类似C语言中的全局变量声明),而下文可能会使用到这些符号。
3 |" G, j% R. x第82行:定义只读数据段,实际上是在CODE区(假设STM32从FLASH启动,则此中断向量表起始地址即为0x8000000)( @4 y. P4 E% Z7 a" g
第83行:将标号__Vectors声明为全局标号,这样外部文件就可以使用这个标号。7 E7 e/ l4 R; w2 w
第84行:标号__Vectors,表示中断向量表入口地址。
& Q5 v9 J( ~9 a第85—160行:建立中断向量表。
4 w5 {9 `% g: D- Y第161行:
' i7 R. c+ t- a5 F$ D! {! y第162行:复位中断服务程序,PROC…ENDP结构表示程序的开始和结束。
# ^7 i/ T. S' X第163行:声明复位中断向量Reset_Handler为全局属性,这样外部文件就可以调用此复位中断服务。
, N4 H! g9 ]) V第164行:IF…ENDIF为预编译结构,判断是否使用外部SRAM,在第1行中已定义为“不使用”。
7 U; a$ G) F+ [9 `第165—201行:此部分代码的作用是设置FSMC总线以支持SRAM,因不使用外部SRAM因此此部分代码不会被编译。4 i) f- n+ B! j" o F0 X3 z
第202行:声明__main标号。
3 T* u4 ?" U# Q" E4 p& h: i第203—204行:跳转__main地址执行。
( P$ f) q% r2 s; b; n第207行:IF…ELSE…ENDIF结构,判断是否使用DEF:__MICROLIB(此处为不使用)。
" Y; X: A, D! A0 {) j第208—210行:若使用DEF:__MICROLIB,则将__initial_sp,__heap_base,__heap_limit亦即栈顶地址,堆始末地址赋予全局属性,使外部程序可以使用。: x, z- T: M* `& U- i* `
第212行:定义全局标号__use_two_region_memory。8 N, H" P) s+ Z" r+ V5 t8 x
第213行:声明全局标号__user_initial_stackheap,这样外程序也可调用此标号。
: G7 e t& D& E; A3 l第214行:标号__user_initial_stackheap,表示用户堆栈初始化程序入口。
9 V2 [" G* J" j f7 R第215—218行:分别保存栈顶指针和栈大小,堆始地址和堆大小至R0,R1,R2,R3寄存器。+ h0 V! G7 K" ~8 C
第224行:程序完毕。, X+ K* V6 H3 K6 j, Q8 I
以上便是STM32的启动代码的完整解析,接下来对几个小地方做解释:+ ~4 ~$ y' Q ?' h
1、 AREA指令:伪指令,用于定义代码段或数据段,后跟属性标号。其中比较重要的一个标号为“READONLY”或者“READWRITE”,其中“READONLY”表示该段为只读属性,联系到STM32的内部存储介质,可知具有只读属性的段保存于FLASH区,即0x8000000地址后。而“READONLY”表示该段为“可读写”属性,可知“可读写”段保存于SRAM区,即0x2000000地址后。由此可以从第3、7行代码知道,堆栈段位于SRAM空间。从第82行可知,中断向量表放置与FLASH区,而这也是整片启动代码中最先被放进FLASH区的数据。因此可以得到一条重要的信息:0x8000000地址存放的是栈顶地址__initial_sp,0x8000004地址存放的是复位中断向量Reset_Handler(STM32使用32位总线,因此存储空间为4字节对齐)。% \! C. K, [( C8 j& B) U) g/ l' O5 G
2、 DCD指令:作用是开辟一段空间,其意义等价于C语言中的地址符“&”。因此从第84行开始建立的中断向量表则类似于使用C语言定义了一个指针数组,其每一个成员都是一个函数指针,分别指向各个中断服务函数。$ c1 ]$ f8 @3 i' o
3、 标号:前文多处使用了“标号”一词。标号主要用于表示一片内存空间的某个位置,等价于C语言中的“地址”概念。地址仅仅表示存储空间的一个位置,从C语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。% z; x9 q7 t0 F' X: ~, n: |
4、 第202行中的__main标号并不表示C程序中的main函数入口地址,因此第204行也并不是跳转至main函数开始执行C程序。__main标号表示C/C++标准实时库函数里的一个初始化子程序__main的入口地址。该程序的一个主要作用是初始化堆栈(对于程序清单一来说则是跳转__user_initial_stackheap标号进行初始化堆栈的),并初始化映像文件,最后跳转C程序中的main函数。这就解释了为何所有的C程序必须有一个main函数作为程序的起点——因为这是由C/C++标准实时库所规定的——并且不能更改,因为C/C++标准实时库并不对外界开发源代码。因此,实际上在用户可见的前提下,程序在第204行后就跳转至.c文件中的main函数,开始执行C程序了。# e! j3 T) z) H/ J- K+ @* n
至此可以总结一下STM32的启动文件和启动过程。首先对栈和堆的大小进行定义,并在代码区的起始处建立中断向量表,其第一个表项是栈顶地址,第二个表项是复位中断服务入口地址。然后在复位中断服务程序中跳转??C/C++标准实时库的__main函数,完成用户堆栈等的初始化后,跳转.c文件中的main函数开始执行C程序。假设STM32被设置为从内部FLASH启动(这也是最常见的一种情况),中断向量表起始地位为0x8000000,则栈顶地址存放于0x8000000处,而复位中断服务入口地址存放于0x8000004处。当STM32遇到复位信号后,则从0x80000004处取出复位中断服务入口地址,继而执行复位中断服务程序,然后跳转__main函数,最后进入mian函数,来到C的世界。- E5 O5 Z2 ^+ L/ |9 M) Y
: @ X, Q! l! N$ \% { g# b9 g8 R% B6 m" ]+ M
; v" o& E' E1 U a( v9 t |