STM32的bootloader程序设计
9 D) P$ C0 q/ T: Q* I- F* [; H0 E通常在使用MCU进行开发设计的电子产品中,只存在固件的概念,并没有所谓的 Boot Loader。其实在此类设备中,同样是可以存在 Bootloader的。正因为有Bootloader,所以不需要拆机就能对产品进行固件升级。万一产品固件有缺陷,那么利用 Bootloader进行升级非常方便,非常节省精力和成本,因此越来越多的基于: B: u# u# w# U. m" j6 J+ f
MCU的产品也开始使用 Bootloader.
& g2 h3 b2 ]& C% `* p- Z2 P( K+ D& T* q7 W6 Y# K8 M- B. }0 u: c
要实现这种设计会用到IAP技术。IAP是 In Application Programming(在应用中编程)。一般情况下芯片的代码区只有一个用户程序。而在IAP方案下将代码区划分为两部分,各存放一个程序,一个为 Bootloader(引导加载程序),另一个为user application(用户应用程序)。 Bootloader在产品出厂时就固定下来了,当需要变更 user application I时,只需要通过触发 Bootloader对 user application的擦除和重新写入,就可完成对用户应用程序的升级1 o, ^ S% U/ N1 \7 V! I. i, P6 F
) k; N. O1 K( |0 g! z
8 l0 c6 l; g0 K# [5 g; _
% Q2 v4 w+ S3 n4 r! H- Q实现的机制如下。
7 P: c( ]/ [, h% b- f) |4 k当复位发生时,开始执行 Bootloader。 Bootloader会检测一个特定的条件,比如
: W8 L x i/ `说按下组合键。当条件触发时, Bootloader会进行一个分支判断,以更新用户应用程
; x7 Z& o8 P$ i( y* N序或者直接执行用户应用程序。8 |# L& F! Z1 j9 O% p, q7 c$ f
7 ^$ X3 a/ R1 k2 O7 i4 T
用户应用程序需要和 Bootl oader分开。最常见的做法是将 Bootloader放置在存储 Flash中的最开始处,并将用户应用程序放置在其后的可用的存储 Flash中;这样就可以独立配置两块区域的内存保护。( b3 h' G4 M. O. R0 n( A) l. a" Y' b
- U9 V/ L* y- {" [5 u% H2 X5 f( y
boot 工程配置截图如下: ]) g+ M* {5 P7 o: f! @- F/ p
6 `) S, P1 i0 m
' I8 e# o) N$ j! z% p
- o- j4 g0 a7 j) g; Q. z0 j; a$ ?4 \程序流程图
& B# b2 B5 ]) G
1 W1 O7 f8 b: C: N6 z% F; \2 ?) i' S# k
. B& j. J* `5 ~
- int main(void)
! o/ ^, T5 R9 x8 Q - {( D0 D# W# N9 W
- /* H7 V1.6.0版本 Hal库 2020-03-04更新 */
7 l8 t# T p2 e - HAL_Init();
; _) O1 n% P" m* H( g9 I {, Y8 n9 g8 d - ' }% v5 s7 ]/ f) D6 W/ q
- /* 系统时钟 */8 [6 \/ j. }# ~0 {
- SystemClock_Config();- p. c0 w. ?9 i9 N2 n
- 2 [, a- J4 W3 j/ x6 A: \) p8 }
- /* GPIO初始化 */! P8 M$ d8 H+ Z
- MX_GPIO_Init();+ U* l" G* B' }" N* p6 V
- 9 d, B8 Q; b# t i$ b+ k
- /* 打印串口LPUART */5 Q, E! }* J0 z1 n Y
- MX_LPUART1_UART_Init(); 6 n3 o4 h; M& z4 Z3 b, a
( r! ~. y+ R$ k: l* L# ?% T! z3 {/ I- /* 读取应用程序 */) N" E: {5 }6 m& i: y- t
- ReadAppPro();+ B# l* \" i3 F8 D% A' |
# p- ?! C% s m) D1 } [- /* 跳转到应用程序区 -> 0x08020000(512K) */# r* u) i, [1 `6 I! J9 {$ N
- Quit_IAP_Process();# h: @. g/ n+ a5 v! Y! q
- t b& g3 ~7 Z- }
复制代码 & r& y8 H+ Y* x1 r: b- u7 T- y
ReadAppPro();1 O. f2 f9 p# ?+ `
- void ReadAppPro(void)
% X' r6 i! x: y! |7 H( y: \ - {
. l( \2 g4 z/ e: `% j - updateInfo_TypeDef updateInfo; /*升级信息参数*/: P' h7 l7 z# W+ ?
- uint32_t NbOfSectors = 0; /*占用Flash扇区数*/
8 u! W% o7 U- E- M- V - uint32_t count = 0; /*分包写入个数*/
8 R" f5 j9 j) N* L+ v2 n# y - uint32_t i = 0;
5 N( P+ y' {8 ~! t - uint32_t calcCRC = 0;, S, U) u. Y0 ~, X
- uint32_t programCRC = 0;8 V8 E) j/ d) L& x, V: T
- uint32_t fileLen = 0;4 Y& s; ^) B1 y" X8 a% z& j
- uint8_t *papp_data = (uint8_t *)(0x24000000);/* 内存起始位置 */
* D- v3 P1 Q! }3 W* ?5 ?" ]8 z
3 D _. u0 `" `1 R! i- /* Step1: 读取升级信息 */
/ m& r4 D/ d! h+ b! o; @5 f% C3 V - ReadUpdateInfo(&updateInfo);
2 \: n, r( L% T0 w/ ? S* l7 ?
( ]) X( R, m& v* a- /* Step2: 判断是否需要升级 */
5 B4 h+ F4 o, A1 f3 n* r' P# t - if (1 == updateInfo.status)+ n* Q. S1 A) ]# [
- {: E: c% ^/ {- o$ P5 D
- /* 获取升级文件信息长度 */# m( j9 ~+ s: M2 x
- fileLen = updateInfo.len; 4 ?9 j% E$ w2 A+ ?2 V
- /* 占用Flash扇区数计算 */
- x* i0 a) m! T; C7 C - if (0 == updateInfo.len % FLASH_ONE_SECTOR_BYTES)
% x5 _# m8 ~9 X7 f' J - {
4 j/ E- Z* G* I% B - NbOfSectors = updateInfo.len / FLASH_ONE_SECTOR_BYTES;
3 J: J4 c+ I r3 O4 f - }1 h* ?/ w, V$ P% W% U6 C
- else6 |( i) f% K" r. [; X' U
- {
: a1 ~2 f/ s0 T. d$ X- d - NbOfSectors = updateInfo.len / FLASH_ONE_SECTOR_BYTES + 1;
( \1 i9 i5 D4 ] - }
1 g8 x2 I; H9 }( \0 n" G; t C7 X3 _
; S6 W* H$ @! Y- /* Step3: 读取升级程序 */
4 y+ k1 s) W3 \" X2 r" u' g - /* 分段段数计算 */
! p& ?0 P& o+ z) G - count = (updateInfo.len%4096)?(updateInfo.len/4096+1):(updateInfo.len/4096);
2 e, D$ b7 f& X( `: e - for(i = 0; i < count; i ++)
7 U9 V; V0 p* r$ C - {$ {- I& _* H3 J+ B: Y
- /* 程序数据从0x24000000开始,写入到APP_MAIN_ADDR 区域 */. d. F$ M1 ?& O+ z/ i
- (void)FlashDataRead(APP_MAIN_ADDR+i*4096,papp_data+i*4096,4096);% s, }( q5 X( S7 s
- }
- h7 V& k+ [; B' k6 l
$ i! e7 Y S* [% t- a1 w- /* Step4: check crc */ R) v( i# n& d" z
- calcCRC = ComputeCRC32(papp_data,fileLen-4);
$ Z$ s2 V& r$ m% u. C6 N1 i E* j6 p - programCRC = *(uint32_t *)(&papp_data[fileLen-4]);
5 D- U# R' S# @/ Z, o
. y; I# o2 B" | a- /* 如果CRC计算通过 */( }7 h0 p6 `9 g/ @5 R4 ~
- if(programCRC == calcCRC)' m; U8 @# p' C- e
- {
% U, ^; s4 `# g9 g1 t# Z - /* Step5: 需要升级时,擦除APP区 */
. r- B: @7 q: c - (void)FlashErase(APPLICATION_ADDRESS , NbOfSectors);
8 {' H; ~9 L/ J5 h3 \2 o/ L - for(i = 0; i < count; i ++)
' r& m( `6 n, J" q# E0 O - {
$ ^9 w5 ?4 \6 g) \ - /* 程序数据从0x24000000开始,写入APPLICATION_ADDRESS 区域 */( R7 ~% r2 n% p! k
- (void)FlashDataWrite(APPLICATION_ADDRESS+i*4096,papp_data+i*4096,4096);* Z4 s6 U( S3 i: i- l$ |
- }
0 ?. N: S9 ^% K. e! p4 U8 v$ @ - /* check:写入程序之后,立即再校验,防止出差 */
0 U7 c8 ]4 M* `9 o9 @9 N - uint8_t *pflash_data = (uint8_t *)(APPLICATION_ADDRESS);- w& O7 H; j% X3 W* Z; C
- calcCRC = ComputeCRC32(pflash_data, fileLen-4);' ~$ @( l, M: [1 M7 q1 J
- if(programCRC != calcCRC){8 G) E& e5 I1 |8 K
- printf("app flash data write err\r\n");
7 f, f, j, m1 |% p, @4 m - return;
$ l7 h4 m& k$ K" o - }
! J( A& i9 O& h) z8 u - }
" _. ~5 `6 ^# j R- M - else8 W7 i) M1 r& q# i
- {+ u/ \% H2 {3 G; Y0 q
- printf("APP_MAIN_ADDR CRC err: programCRC 0x%x, calcCRC 0x%x\r\n", programCRC, calcCRC);
1 G" t+ M! d1 Z/ J R. k% Q+ Y - }! W" v: Z! @) ^, T
- ( a( _1 r" l+ }( ]5 S a! z
- /* Step5: 升级完成之后,改变升级信息 */ % g4 a' \+ E+ t0 O3 @' a, ^
- memset(&updateInfo, 0 , sizeof(updateInfo_TypeDef)) ;
/ F4 Z- t# ^+ Y2 t* x - memcpy(gTempBuf, &updateInfo, sizeof(updateInfo_TypeDef));) R- N \/ [; }* f, n& n
- (void)FlashEraseAndWrite(APP_UPDATA_INFO_ADDR, 1, (uint8_t *)gTempBuf, sizeof(updateInfo_TypeDef));
/ D( G' z5 @" ?* j8 @ - }
1 m5 y1 e( _; u& D
8 g# W" `: }1 @; b$ I% P' r6 ]- return;+ y7 H3 J: R- G
! D ~" c# T2 g5 L4 H% q- }
复制代码
" y. i, `7 q- h" a" A6 o% mQuit_IAP_Process(); 跳转到应用程序区
% y! {% g( U) z3 n4 T( {' Q+ d8 x- void Quit_IAP_Process(void)" x0 o: A' {1 J' Z& S
- {
9 \* R4 [1 q. c, i% h0 r$ E - /*
* G. d$ F2 z9 }8 q - * 把用户代码的首地址里面的数据拿出来,看看是不是以0x24000000开头。
+ y; o* l- m) X. c& u - * (X & 0x2FFE0000 ) == 0x24000000意思是说X是不是在0x24000000与24080000之间,即栈顶是不是在以0x24000000开始的512K : Y3 G6 i* A# ]
- * 前面说了用户代码的首地址放的是堆栈指针,而堆栈应该指向RAM,而RAM的首地址是0x24000000,
2 t f; w+ ]$ `/ i5 N7 b8 i6 N - * 所以这句话用来判断是不是已经有一个正确的堆栈指针地址写在了用户代码的首地址,
' D( ]) A; A$ _ - * 进一步可以推测是不是有一个正确的用户代码写在了用户代码区,如果已经写入了,就可以跳过去运行,如果没有写入,就不跳过去。
7 \# ~1 H0 v8 Z8 z n' j% G - */
( ?' Z1 c3 I( \7 `# `/ ?9 [3 ? - __disable_irq(); 4 t, G" C, H. r: b4 _
- JumpAddress = *(__IO uint32_t*)APPLICATION_ADDRESS; /* APPLICATION_ADDRESS is 0x08020000,app 程序的跳转起始位置*/
s* j( @6 ?+ b8 N4 M) A& } - if ( ((JumpAddress&0x2FFE0000)==0x24000000) || ((JumpAddress&0x2FF80000)==0x24000000))4 z! l3 r7 N* D( B. o4 P
- {
) E! }9 B( ]/ T/ ]& b& Y' |" O - #ifdef PRINT_DEBUG
9 f8 c0 [1 d, l3 o) y5 R - printf("Start Jump IAP!\r\n");: A$ Z/ |7 u1 U. p; i
- #endif/ `4 x! P* e" q% J- w. X
- /* 是把用户代码的复位地址赋值给JumpAddress。 */: k4 z$ r& N" M+ F3 a
- JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4); ^8 K8 w/ i7 K
- /* 是把用户代码的堆栈地址写入堆栈指针 */! T/ g3 ~4 S1 h% ]# z
- Jump_To_Application = (pFunction) JumpAddress;! I" c$ C/ Q+ m& A2 R3 Z+ H
- __set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);
( n5 T/ y5 S9 A9 {+ z( _( ^ - /* 是把用户代码的复位地址付给PC指针 */& v, }$ p, o+ Q: ]
- Jump_To_Application();
+ k- p- ?! E- K; a% P2 C1 N; M - }
' n6 b7 x9 N0 j" |4 B
7 {( u6 B- S: [5 `4 y- file_size=0xFFFFFF;
4 \# s |6 G- i! V - while(file_size--)9 _6 F2 A& m. q' M! O# V
- {, J9 y/ N2 C5 c/ E* ~
- __nop();* r X( m; S/ A: n: |
- }; b+ {: v5 `2 O% ~( {
- # s; m$ k1 U$ h
- NVIC_SystemReset();8 O' i1 H* c. A5 c3 ?2 v
- 6 n0 v8 r4 u# {" @, E r( F0 _- N
- }
复制代码 # w* j- q6 d+ c0 s) `2 i
问题1:+ V' q% z5 N0 Y6 @% w9 P. o4 d
# C% V7 M( A4 h# O# I. ]: FJumpAddress 地址为什么指向 APPLICATION_ADDRESS + 4 ;( u: J6 @+ W1 b- `5 d# w$ L8 {& G$ t
MSP 主堆栈指针为什么指向 APPLICATION_ADDRESS ;; B3 q/ y9 Q& J5 X5 K4 g* `
9 J# C, g3 e4 `3 a/ S+ ~8 ~
从 startup_stm32fxxx.s 中的启动代码可以看出:程序开始第一条指令地址为 CSTACK,第二
- ?$ ?- O1 M* B条指令地址为复位指令 ,参考代码中的红色部分(蓝色为注释);" Y9 S6 s7 m+ ~; e0 G2 u
8 C4 M) ^$ m2 e2 }' t0 c# d__Vectors DCD __initial_sp ; Top of Stack ( APPLICATION_ADDRESS )) E- [) J2 T- @% z! U& ]9 Q
DCD Reset_Handler ; Reset Handler ( APPLICATION_ADDRESS + 4)
" `+ Z5 M( k) A+ B; y; c: d( S1 \当程序启动时首先要执行复位程序,因此 JumpAddress 地址指向复位指令地址(即APPLICATION_ADDRESS + 4) ;" N( [8 Z) N; q) R# A' ^# `
MSP 对应的是主堆栈指针,指向 __initial_sp 地址(即 APPLICATION_ADDRESS).
5 x$ T* C) S8 X6 F) h9 B' y* l$ G z! R K
问题2:
7 \0 b1 Z. F" R4 N
' w; Y" `' B( J! a# m为什么需要做以下判断 :
! c6 z2 }; L0 m# x
: V4 B$ z/ ^, q+ Zif (((JumpAddress&0x2FFE0000)==0x24000000) || ((JumpAddress&0x2FF80000)==0x24000000)) {& N, a8 j- ?+ J/ }! q
$ D/ `$ u- [8 ]
}# c7 k" t* `* I" q! S$ t: K& c
ApplicationAddress 存放的是程序的主堆栈地址,__initial_sp堆栈地址指向 RAM,而 RAM 的
" G5 K! A$ g! w起始地址是 0x24000000;- F% c) ^; |/ M9 M- l% J& }. ^( g
因此上面的判断语句执行:判断用户代码的堆栈地址是否落在:0x24000000~0x24080000区间# ?, ~7 \. N p' g& N: E
中,这个区间的大小为 512K., N, Q3 p. y9 _; \- v: \
例程中使用芯片 RAM=512K,因此做上面的判断;如果芯片 RAM 比 512K 大的话,可以在& u; r5 Y3 Z8 X$ \5 T
此判断语句做调整
: {) F7 [' O' {2 Y& z+ d6 e! f! W: m7 y1 @; d/ C
以上分析可以看出,程序跳转部分的设计与 APP 启动代码和芯片 RAM 的大小有关系;移植和
+ P) \' \" Y( a. `6 r调试时需加以注意/ U: G: U7 E0 A2 o
: w* R3 w/ N% X, T* n% X
APP 程序.
% a2 N$ X& w& Z. Q M( D M% cAPP 程序起始地址设置方法, V1 h9 T/ G/ D$ x' Z2 o4 V
' e f' [6 o2 {8 B) e0 O' a! M! z0 G! P2 F) ^- k% W
L2 y8 w8 g5 _2 M/ [中断向量表的偏移量设置方法, P! S7 N& X5 n: C+ Z- h
system_stm32h7xx.c
3 y# n5 h) z: E# z
, Z9 C7 W4 X E8 T1 x+ w2 F% G- /** @addtogroup STM32H7xx_System_Private_Defines
3 |$ H7 u; c9 C& j - * @{
1 T0 K+ w" C6 A w: F; W% g - */
; _2 V' {1 |' s. A0 Y - / u3 l- O, x; J
- /************************* Miscellaneous Configuration ************************/8 A- V" f& o, g; x1 a+ t
- /*!< Uncomment the following line if you need to use initialized data in D2 domain SRAM (AHB SRAM) */ G3 o, x* F3 _5 A& r
- /* #define DATA_IN_D2_SRAM */% N- t% Z4 a, N( N! A; W/ y7 n# b
- #define DATA_IN_D2_SRAM
) p( w# v+ Q0 [( k1 w' _& |! r - , M; i% I2 ?: G; c
- /*!< Uncomment the following line if you need to relocate your vector Table in
# I5 C! @5 k% d - Internal SRAM. */
$ o# M" Y7 X; ^' [ - /* #define VECT_TAB_SRAM */3 L1 H2 }& ^/ \: D
- #define VECT_TAB_OFFSET 0x20000UL /*!< Vector Table base offset field. This value must be a multiple of 0x200. */1 B' @1 F3 _8 `8 B! }
- 7 X6 h* b3 ?2 d: f, A9 A. P5 `( N
- //#define VECT_TAB_OFFSET 0x00000UL
" r. e% ]7 }# d - /******************************************************************************/
复制代码
' g# Q- {$ R: n2 U; D7 h1 |+ y) `0 p7 S
STM32H743 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,在执行完 IAP 以后(即将新的 APP 代码写入 STM32H743 的 FLASH,新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,并且注意到此时 STM32H743 的 FLASH,在不同位置上,共有两个中断向量表
. }. n* k1 k6 M6 H# M) ^) ~: I6 e8 a' A* ~6 k/ x* e( s
通过以上两个过程的分析,知道 IAP 程序必须满足两个要求:& A! w. C% o% O! Q# T& F
1) 新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;
; j3 \3 f9 A$ z) f4 Y% U Y2) 必须将新程序的中断向量表相应的移动,移动的偏移量为 x
, a8 q" C7 l/ G$ |% V7 j- y1 V: T2 K, x
( `; z G4 `5 s9 W0 Z. \ |' _
|