|
简介 本文主要讲解在线升级IAP的基础知识, 主要是针对IAP 从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对在线升级的认识. 1. 在线升级知识0 u- V* a( _% ~+ h+ j
$ |$ U5 W3 q* C ^2 {什么是BootLoader?BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.
* S6 T0 k& Y6 G/ b- l STM32中的程序在哪儿?正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码 " y4 q5 T% m+ {9 M# |6 ?
; Z0 |8 ~; V/ V; [) R0 M接下来就可以进入正题了.
1 q$ V3 K& g" A 进行分区既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图: % j( v/ O1 K( Y- S
5 K+ O5 `% _* b) z- V
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图: BootLoader区存放启动代码 App1区存放应用代码 App2区存放暂存的升级代码
2 @9 k2 A5 ^$ W9 ~9 l! w! Z
- f4 D0 U! a* m. u& g+ F$ i) J# V7 C. q7 ~/ z( N
总体流程图先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序. 然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序. 在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示: . @+ u! W; Y' Q, U
0 z" `9 S: d% A9 @8 n& A
0 m. H1 O: j( r' q- v, u8 C
2. BootLoader的编写本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。
) ]# v6 Y A/ z* `1 p) h8 s 流程图分析以我例程的BootLoader为例: 我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
( U. _$ k8 d( w) a% i# y; L; M' H$ u/ ~ c
程序编写和分析所需STM32的资源有: 发送USART数据和printf重定向 Flash的读写 程序跳转指令,可以参考如下代码:
0 H) _$ F6 o! y
- 1/* 采用汇编设置栈的值 */6 A2 u/ R4 b# Z0 C# ~$ S
- 2__asm void MSR_MSP (uint32_t ulAddr)
4 [7 I$ A A9 l4 f2 ?5 o- Z - 3{! ~! `3 o* W& k& |0 ^
- 4 MSR MSP, r0 //设置Main Stack的值3 N) Q( O/ I1 M& N! Y0 }$ j% P) o
- 5 BX r14
# A/ C+ C0 k! u( h! E* k T" Y - 6}
+ t9 A! n5 @6 n9 E% X - 73 x) h% O. Z( k7 d i8 I
- 8; W1 g0 `6 W$ W7 q
- 9/* 程序跳转函数 */
2 F! ?* b! w8 P7 `& J5 x* K - 10typedef void (*Jump_Fun)(void);
2 \) E) {, A9 N9 Y% Y! `+ a, \ - 11void IAP_ExecuteApp (uint32_t App_Addr)' i7 m' R" Q( w
- 12{
5 {3 [2 A3 \5 R9 \ - 13 Jump_Fun JumpToApp;; @! Q8 @1 a: @- g6 S' E3 v1 o% M
- 14/ D* K7 c5 ?. c! q
- 15 if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法., v" N" k4 w; p# ^
- 16 {% t7 O( U) E6 I- s/ v
- 17 JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
7 O% X5 M3 o+ g% _3 n - 18 MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)0 P* r( _$ d' N, f
- 19 JumpToApp(); //跳转到APP.& l5 u6 n. s7 _- `6 v7 e1 A& s4 R
- 20 }
+ T! O: D) l3 g - 21}
复制代码 2 R! K G# ^. V
( R% `6 m; |5 B0 u! }( Z
3. APP的编写本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。 ! C! g+ s0 p' A7 D; t
流程图分析以我例程的App1为例: 先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题; 打印版本信息, 方便查看不同的App版本; 本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
( I. m! f& y, t8 b 4 F7 O/ L$ F+ p; o& h
程序编写和分析所需STM32的资源有: 发送USART数据和printf重定向 Flash的读写 串口的DMA收发 YModem协议相关 4 R9 G6 P' a3 F' ^) P8 F/ ]& y
Ymodem协议# ^, d0 x& _* i6 h
代码分析- 代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
- 后面放了我的源代码, 详情请参考我的源代码.
- 主函数添加修改向量表的指令2 v3 b) W1 A P" H4 L& ^
- R2 ^4 a1 K, N& B, ^
+ s) c2 F1 U; z: y4 y4 E- 打印版本信息以及跳转指令
: `8 j" k7 A! v" O* C% X
3 Q0 x, y$ U. J& ]( O6 D/ f' Z: a0 B- q+ B4 A2 c. d6 d8 F
- YModem相关的文件接收部分
2 ~: _% k+ C6 G4 _, K8 @0 D5 R- K
- 1/**9 P- N5 j% g a. R, @& k
- 2 * @bieaf YModem升级; z8 X# ]2 M' P( y! T) `+ }
- 3 *
# M: {- P, S3 l - 4 * @param none
7 t7 ^2 P6 Y7 Z) K+ F - 5 * @return none
; z0 b O5 Y- K) Z( z/ L# y - 6 */
5 Y* N& H+ _( z7 f8 v - 7void ymodem_fun(void)
9 w( g1 Z! k9 Y' F' y( w/ ~5 D - 8{
0 [' X+ l8 X, ^0 F: B: J- }9 P - 9 int i;" g. K3 m. i0 |' f6 h4 D; w
- 10 if(Get_state()==TO_START)+ V6 U/ ^! ^* q- ^! Y! O4 b
- 11 {3 D' c1 Y% k- T4 i' @
- 12 send_command(CCC);* ?3 a. l# r4 i$ V. q8 o
- 13 HAL_Delay(1000);* o$ S5 h' G$ X7 x" ]/ M) x$ }
- 14 }
# x1 k9 l! D: y' W - 15 if(Rx_Flag) // Receive flag
: K( f) w/ f) g, T2 [ n; \ ~ - 16 { p/ V* F& v Q( T3 Y
- 17 Rx_Flag=0; // clean flag
& s1 ]$ Q, P: r0 k+ W; N3 J - 18: v3 f5 D& J( _+ Q, A1 V3 _: q
- 19 /* 拷贝 */7 e1 q% A/ r$ ]% n$ R- t
- 20 temp_len = Rx_Len;
( e8 B5 ?2 D) j3 |/ B - 21 for(i = 0; i < temp_len; i++)3 U% M2 |6 @7 W, L
- 22 {3 m% U4 P! _- P" b" v3 C4 }7 a6 N
- 23 temp_buf[i] = Rx_Buf[i]; D7 H6 H( Z, |% n3 [
- 24 }7 e+ G, y( ^+ L4 I" _3 f, C8 C# @
- 25
9 \" ?1 R$ m" h: W# z$ S - 26 switch(temp_buf[0])
5 l( h# O4 A/ I T2 z' X - 27 {
; G& O, M( ^7 E# T+ q+ @: k! p - 28 case SOH:///<数据包开始# n8 d$ s4 f- l1 a& _. m5 `/ O. Z' Y
- 29 {( k* ?0 n R$ p* [, O) I
- 30 static unsigned char data_state = 0;
4 ^& n: V, z' E( A: } - 31 static unsigned int app2_size = 0;
$ _+ x0 {9 W2 F1 h' A' E - 32 if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验
/ Q2 ]- q+ Z) z, C) i - 33 { 3 S- v4 i! v# b% s' R) n- j( |" \
- 34 if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始9 }8 K! @, X: Y
- 35 {; B" r* S7 m2 X/ t P
- 36 printf("> Receive start...\r\n");1 L& F9 P& E a" v- b- n ^
- 379 b2 Q5 l8 p0 g" L
- 38 Set_state(TO_RECEIVE_DATA); p" m+ i2 n# t0 M, c1 L6 I2 t
- 39 data_state = 0x01; 5 O7 G# ?) Y1 Z, A, ^/ G( Y
- 40 send_command(ACK);
; l. p. M& o' v# T - 41 send_command(CCC);' S5 n' m$ L* H# K4 b
- 420 ?9 N; a3 k! L) i- Z1 F# u
- 43 /* 擦除App2 */
) g- c- Z) d4 r1 g/ g( u$ }% L - 44 Erase_page(Application_2_Addr, 40);# m% F, V, v4 b8 B
- 45 }
$ W8 {! w/ n# E! v3 r - 46 else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束2 s3 K+ b9 \5 ~
- 47 {
0 }% N! F# m, r, v9 [% J - 48 printf("> Receive end...\r\n");( O4 i! X) e$ g% n4 l7 q% s% Q
- 497 y' W* ~/ i8 {( Q# e
- 50 Set_Update_Down();
( B/ ^9 S! Q# u" U( E' c ~- A - 51 Set_state(TO_START);
" w! j) A# Q! Z6 m6 w9 [9 T0 D m - 52 send_command(ACK);
; J0 T, a" h# U* P, t - 53 HAL_NVIC_SystemReset();
7 k/ [$ I- O0 p" _ - 54 }
/ F2 C+ t( z8 O1 Q - 55 else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据3 e9 |9 u$ \' K5 S+ U
- 56 {
" r6 i5 ^, e" C - 57 printf("> Receive data bag:%d byte\r\n",data_state * 128);
4 J- G$ W8 ?" @+ T) X$ A - 58
- K9 E2 q# {4 j6 C$ e( s - 59 /* 烧录程序 */
9 {9 U/ {1 R% H! \$ z - 60 WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
4 V& ]6 x/ }( O3 o6 |% | - 61 data_state++;& k! ]% X( M e; e* e
- 62
- i' ^$ U- e; f y - 63 send_command(ACK); , N4 ~: l6 ^' N3 }% Y0 l
- 64 }* U% e' c4 D6 ^! ]2 ]
- 65 }
) n7 S" Z5 T5 I% e8 I6 i - 66 else
, E( U' U4 l5 q; _) F! B" y( e - 67 {
- R: F U$ v0 f ]: x - 68 printf("> Notpass crc\r\n");6 K2 B/ y# s# p# k( Q O
- 69 }( }7 l8 M- i* k; q! H
- 70, h; v4 v8 I1 {8 b& s) b
- 71 }break; y4 z7 y @, F p1 ^' Q( ?$ r
- 72 case EOT://数据包开始
! o8 L3 h' j- k6 r. ~9 C9 t4 S: @- m - 73 {
8 s5 Z# H6 w- K+ W9 q& d1 x - 74 if(Get_state()==TO_RECEIVE_DATA)! _6 ~3 ?( X% W, Z/ o6 N' T0 g& Y
- 75 {
% W" E; r! b* x0 L1 M3 q3 m - 76 printf("> Receive EOT1...\r\n");8 P# q5 d" m6 \+ S% c
- 77" g* f5 H, w% |7 f, T7 I! M1 Y
- 78 Set_state(TO_RECEIVE_EOT2);
: Q$ m$ a) X7 G6 \: a' X4 } - 79 send_command(NACK);& s- c" H: e9 P2 d
- 80 }
& X' V7 T% R4 `$ M - 81 else if(Get_state()==TO_RECEIVE_EOT2)# W8 I# u; B: v, y, x2 h% F$ ?8 J0 E; I
- 82 {
/ `1 O- ?9 ]2 ]' }7 l" g( N' ^ - 83 printf("> Receive EOT2...\r\n");
8 h& d8 d$ ^3 z3 I5 P8 t - 84! L" q- N; _1 S* B. o' I! ~
- 85 Set_state(TO_RECEIVE_END); - z6 E4 F" h5 M! f/ I2 i7 f, a
- 86 send_command(ACK);
' Z" p, O5 u1 g$ d- E2 _0 _1 u& P - 87 send_command(CCC);
3 Z3 ~& n" w- y8 S% h - 88 }
) c- P8 R) ]$ q- X - 89 else
0 X v' z ], ~ ` - 90 {' s6 T5 h8 q6 a4 [5 B% z
- 91 printf("> Receive EOT, But error...\r\n");# `: ^ j! C! ^# G: f
- 92 }6 B3 f6 t' ?9 D; v5 @4 @ f; c
- 93 }break; 1 u/ w9 i1 y }2 H6 o8 q
- 94 }
- |3 L' {4 ^0 T" h; [9 A1 L2 S - 95 }
& J2 E& ?3 H# c& f; e- v4 S - 96}
复制代码
% i" `9 O# \. ?% v" {2 u* i$ {
' g! E$ F5 x, V- 其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.( u p+ ^% V/ n& t+ B
8 d. a& d( W/ L$ I# O
" y k/ ]- {3 m+ Q+ @4. 整体测试
1 k2 K1 y, w3 z/ q) A本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。
: P6 ~: H0 F# e" [; l# S# i& ~7 J! z+ Z5 f
* s' n+ v+ j. d( W l7 b, m源代码
M; u* L3 [& Z: @BootLoader源代码和App1源代码可以在原作者的gitee获取:- 由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
" g! ]2 ~2 ~& j8 s7 n
. I2 X2 W+ l8 M# j6 ^* F 2 B/ K6 F! b: x3 m- ?' W
BootLoader的下载- BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
- 按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)" p5 r3 |1 S+ F' Z
& h& L, @: ], F! D u& n5 \
9 @- p$ @7 [+ d4 J& _- D5 H; y' O- 烧录代码
- 运行, 通过串口1打印输出, 会看到以下打印消息
- 说明BootLoader已经成功运行' X- ]1 B* a' c' n% [+ A+ l
; k2 t- a3 b) P& P
/ V7 M& R% G! N4 d/ F
App1的下载- App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
- 同时也要修改擦除方式为Erase Sectors, 见下图
% w: s9 P6 B* M! G, w4 p 1 ~6 N5 }& M" {# L& p* Z# k
) `5 S, D6 S) ^5 S2 _* u
4 p( M9 a# G+ p$ Q7 A+ Z' T. Q- 烧录代码
- 运行, 通过串口1打印输出, 会看到以下打印消息
- 说明BootLoader已经成功跳转到版本号为0.0.1的App1
# b+ Z4 m5 n2 a
/ [" q3 K8 N7 A: |- D! P! }3 A+ ~% X9 @3 W" _, s, }" n
生成App2的.bin文件- Keil如何生成.bin文件, 请参考这篇博文 Keil如何生成.bin文件
/ X$ _7 ^1 Q. ~- k# `$ v0 x https://blog.csdn.net/weixin_41294615/article/details/104656577- 修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
- 生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件
9 Q4 v7 }2 J( ? 6 W l$ y2 b! ^ M: J& S$ Y I/ u4 B
! K; F2 [2 u" a! m# V4 `使用Xshell进行文件传输- 打开Xshell
- 代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
- 所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
- 你会看到App的版本成功升级到0.0.2了.
- 如果你到了这一步.
- 那么恭喜你! 你已经能够使用在线升级了!: B* D5 A- ^% W4 {, a" R
8 ?2 a$ g( ]; `1 Y) x/ Q
; Q4 o6 C1 ~! v5. 总结
/ H5 y/ W3 e' R! p2 U; U) \通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多。
8 x' N$ U% r9 v+ Z8 B3 T2 s1 P
% P# ]0 T* Y, k1 y0 |
4 o8 ]5 l0 O9 @4 C
( V7 N! [5 _& p3 T. K0 S转载自: ARM与嵌入式
- R; E8 X) Z% L" t0 l( \如有侵权请联系删除
; T$ Z! m' g1 G6 o4 V9 w
. W% {5 z! V- A$ K5 M1 a; V7 i5 [( @6 U+ t" d
; Q5 r1 I9 {1 w8 o. E |