简介 本文主要讲解在线升级IAP的基础知识, 主要是针对IAP 从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对在线升级的认识. 1. 在线升级知识
, M; V9 ?: q, g; }; Z6 @6 `! H; X y% L
什么是BootLoader?BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的. 0 C( j& ^' e3 _
STM32中的程序在哪儿?正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码
) U6 k% Z( J! |! s% c: y( I
$ z/ D/ x k8 O- Z. K! {2 e接下来就可以进入正题了.
4 f; y. H; R7 B# n% _/ l! W 进行分区既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图: 3 Y7 U: \7 V* J& Q# u* m2 N" B
$ h1 m$ y9 m$ {1 X
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图: BootLoader区存放启动代码 App1区存放应用代码 App2区存放暂存的升级代码
; B8 @& _) `0 C& ]
' [6 Q& ~) _. ~' d* m, ~5 c: j4 }; V
0 e3 L/ H, s$ I, b) m总体流程图先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序. 然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序. 在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示: & V3 @# D/ m1 }! u
( L8 Q6 \, {6 [& l9 x+ b0 e
. P/ L% F. c/ }1 s! \8 J1 |+ y2 W2. BootLoader的编写本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。 # _. e. R* R9 e7 Y$ o2 c
流程图分析以我例程的BootLoader为例: 我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
" @, L; j9 e, u: X" O. |2 U
6 _* f g6 m. A" r F2 s3 j: {程序编写和分析所需STM32的资源有: 发送USART数据和printf重定向 Flash的读写 程序跳转指令,可以参考如下代码: ! S, M& L, s9 @
- 1/* 采用汇编设置栈的值 */: C& B- I+ B; l5 g/ w
- 2__asm void MSR_MSP (uint32_t ulAddr)3 `% F2 [2 [! ^+ o R
- 3{
$ {+ Q/ C+ Z& j" x" m# ^ - 4 MSR MSP, r0 //设置Main Stack的值
& `7 X a) c5 J1 r5 \' S - 5 BX r14$ ^! B A+ A/ s2 j/ y
- 6} w( f" n0 I/ n) W0 [- h
- 72 O4 a" D5 I* g+ N
- 8
0 E; h1 m2 ]2 S; J5 O. K - 9/* 程序跳转函数 */
: w O6 I2 m9 A) j2 x - 10typedef void (*Jump_Fun)(void);4 ~; ?' d+ q' y" E( P& e2 o% P' h
- 11void IAP_ExecuteApp (uint32_t App_Addr)
: q+ V7 e, N% k* ]+ P8 N - 12{
+ V" C1 u/ _3 X% O) g# u6 |7 W - 13 Jump_Fun JumpToApp;
4 A" m. C, _2 E- ~! j; X - 14; A" d) d. o8 N, z1 `& U' s
- 15 if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法. V s1 A& t+ ` u: Q, b4 w
- 16 {/ x; X( `: O! x) O$ j7 l
- 17 JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)& L# t! \6 Z) {4 i, p& \2 z" c) u
- 18 MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
! u: n9 ?2 ^# c5 p0 V' E - 19 JumpToApp(); //跳转到APP.
, z$ |3 @& S; e - 20 }
+ \5 a: ]7 o5 S8 ?9 l8 ` - 21}
复制代码
/ U7 u. O+ y, ]$ g- [) u
: ]$ y) i0 l$ m3. APP的编写本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。 + ?! W8 V& F1 R- E5 A9 l6 J( L" d
流程图分析以我例程的App1为例: 先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题; 打印版本信息, 方便查看不同的App版本; 本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示: 1 m; E) _6 G9 N1 Z
1 j/ I8 }! L" D8 k. G7 b
程序编写和分析所需STM32的资源有: 发送USART数据和printf重定向 Flash的读写 串口的DMA收发 YModem协议相关
% ~# s& k/ Z4 C& b Ymodem协议. W2 m1 R) ?8 [( a1 Y F: n5 H h
代码分析- 代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
- 后面放了我的源代码, 详情请参考我的源代码.
- 主函数添加修改向量表的指令
3 R5 G; m0 H% K) I& K
# r+ b2 v4 Z% l1 D) w1 Y) o/ u, g9 t% L
- 打印版本信息以及跳转指令* K" E2 H H7 X; U2 N: H# G& u
# D2 Q+ p5 x! i' t& V* w* S1 s
# d/ c: Q7 C& Q; C/ G7 W- YModem相关的文件接收部分
! H$ F2 R B2 w- I, z( f
- 1/**
, z# [ w) z. K$ O3 S( w - 2 * @bieaf YModem升级
7 Q" {- S% p) W! K6 W - 3 *
6 E w$ p- _7 q: I+ \/ ` - 4 * @param none8 f7 M/ a/ X! S
- 5 * @return none7 c" @/ R. u( S9 H, D
- 6 */
- r. V1 _6 X0 z1 ^5 ]8 S. e - 7void ymodem_fun(void)7 L5 ^1 b' G5 E& u
- 8{
8 W, e& R- j* h1 _ - 9 int i;
" {6 F7 ]9 O. D" b9 ~3 e - 10 if(Get_state()==TO_START)) i7 A. v, S# P$ ]
- 11 {
1 w: |9 s' o! E1 K# Q: ]+ j - 12 send_command(CCC); F$ }4 J7 ]0 Y _
- 13 HAL_Delay(1000);
: e" _: ^4 f g5 G% u" R - 14 }
) ~7 e. Y% P4 h2 p& F2 z# o M - 15 if(Rx_Flag) // Receive flag
3 ]6 [# O) A& U* Q N- ~1 F! Z - 16 {
; q1 p$ t M& ^+ |, {" f; S0 f - 17 Rx_Flag=0; // clean flag
* H8 N# g! g: w: h - 18
- P( P6 j f( ] - 19 /* 拷贝 */
3 F5 n* f+ ]( Q - 20 temp_len = Rx_Len;
3 S5 T; C c# A3 L - 21 for(i = 0; i < temp_len; i++)
5 Q1 p0 H9 I6 t6 F - 22 {# f# Q; d- u4 i8 [' _
- 23 temp_buf[i] = Rx_Buf[i];" E7 p5 m: O. v8 }2 R
- 24 }
% h7 I7 u, v7 _- D2 ]9 f - 25
4 I1 i2 C N ^$ a) u( _$ Q - 26 switch(temp_buf[0])
( w. [3 @1 ~9 s - 27 {
) L4 e6 |8 p& ?& ?" }5 } - 28 case SOH:///<数据包开始! ?; s5 J7 D) _ \4 [8 T3 S( |
- 29 {6 z# T% Q8 \$ f* f0 c; r
- 30 static unsigned char data_state = 0;6 m0 `% |) T D$ ]
- 31 static unsigned int app2_size = 0;
/ w4 ~3 z1 z. k; h$ ^6 T - 32 if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验& N8 q7 i# p3 C: y" b. |
- 33 {
+ P8 ~2 A) N# q9 k7 r( D. ^ - 34 if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始& C# W8 U# S; v# j1 b9 X
- 35 {
+ L( q2 U2 n# j( F2 _$ G, L! d - 36 printf("> Receive start...\r\n");
5 K& U) o0 t. O: J [ - 37
$ |1 @/ W0 B, s; k# Q' a+ v1 O* Q1 v9 g - 38 Set_state(TO_RECEIVE_DATA);
b3 B" [7 X, ]! ^ - 39 data_state = 0x01;
9 o+ x! g8 M- D6 {* X - 40 send_command(ACK);! y" I) Z+ |8 b- j( x* k) t
- 41 send_command(CCC);9 b) p: A+ a2 W9 z" E- O
- 426 H7 ~! K8 {, \8 H+ O. W' z
- 43 /* 擦除App2 */
# W5 V6 l1 _$ m# y# g - 44 Erase_page(Application_2_Addr, 40);
: c& E% }7 W5 z2 q4 v8 V - 45 }
6 P6 U B& C& f$ V - 46 else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
& x2 J- M1 U5 E! O - 47 {+ C* I1 Y0 z0 \7 ]
- 48 printf("> Receive end...\r\n");
8 m3 w" C0 r. F+ @% V - 49
. @! w% d2 r6 l# C, _. { - 50 Set_Update_Down(); : u0 D% A' Z( h0 D! ?" E4 `
- 51 Set_state(TO_START);
: N0 ]4 w0 q7 J6 H# ` - 52 send_command(ACK);
" N( ?2 X% \ p9 | - 53 HAL_NVIC_SystemReset();
" G$ p; A, b% |- b% C' g: A8 N - 54 } ( |4 \9 d8 x" ]5 _
- 55 else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据3 T" b' _; d" T/ e1 E$ D; |/ N- m
- 56 {5 n% W4 L1 n5 y) e5 F) Y6 b) N; q# C1 T: i
- 57 printf("> Receive data bag:%d byte\r\n",data_state * 128);
7 u) v& K( k6 U+ P/ H3 E, ~9 m* {- O - 58, G5 z7 J A2 _$ A+ t6 g: s
- 59 /* 烧录程序 */# ] K6 M6 Q2 }! N7 j! V! ^3 Q
- 60 WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
: l9 s" u3 _# M# @6 c% I4 O) X* e - 61 data_state++;' \4 h! c% o: d$ }
- 62! Y3 t+ ]6 Q( f4 h4 v
- 63 send_command(ACK); " D; _ a1 G7 Y! s
- 64 }& [4 \8 x: n0 c# V! q
- 65 }: u0 a2 W' p o- ^& m2 p9 N% |
- 66 else
' ]5 u( g" f6 y k& U2 ] - 67 {
6 o2 g3 ]. ?4 q$ H" `2 D$ T" l; |+ ?( Y. w - 68 printf("> Notpass crc\r\n");
3 U0 s" ?( K5 g$ Q# ~2 M, V( B - 69 }
# @2 j9 e) m1 C3 \5 Q8 I: | - 705 b9 B2 q7 L- G) y8 b9 j
- 71 }break;
. ~; G9 ~% U% v - 72 case EOT://数据包开始
8 [# {: [& \% ?5 w. n; S - 73 {) g A* u. g1 ]2 U" ~
- 74 if(Get_state()==TO_RECEIVE_DATA)3 K! v* M1 c( \" t- \
- 75 {& F. s5 j7 I& F* k- n2 q/ K
- 76 printf("> Receive EOT1...\r\n");
5 d$ k5 U/ |$ {: k - 77
/ ]8 ~) T; R+ l0 a0 f1 {# I2 I - 78 Set_state(TO_RECEIVE_EOT2);
$ }+ }0 q6 f& u; a6 M9 Y! w - 79 send_command(NACK);; D' F6 J8 K" ]1 G
- 80 }
; Q* z% Q5 Z# T L - 81 else if(Get_state()==TO_RECEIVE_EOT2)2 n+ W* G n2 @, P# L) F5 k& F
- 82 {
( Y: h' h j$ u |8 o! g - 83 printf("> Receive EOT2...\r\n");
4 m$ P0 E( V" j* _- @0 \ q/ F5 Y, v* x - 843 A4 f& q& l4 T! i2 ?
- 85 Set_state(TO_RECEIVE_END); 3 B# x1 y3 a7 {3 j# M6 |( o% _
- 86 send_command(ACK);
# e8 e) Z; L( j. g4 |3 S3 F - 87 send_command(CCC);( p; K" k ]! k) Q( e' }
- 88 }) k& g) ]* `- [! X3 G L( z% [
- 89 else2 D# |+ r8 W5 Q+ y5 m+ Y
- 90 {5 e" ^. f# [1 Y: P
- 91 printf("> Receive EOT, But error...\r\n");
5 S8 l2 w% {% Q0 R - 92 }. \2 a; R! Z7 z! [! f+ W0 A
- 93 }break;
e; A7 [1 v/ P. | - 94 }. J) T8 P/ K( \4 J1 G
- 95 }
- X3 b- M4 c* S- w% O5 b - 96}
复制代码 & G7 i" @& ]5 C$ G8 h
/ l" l: n* X; m* Z, L- 其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.- e/ h7 `7 P. Y0 C: D
) I8 t: U- E {3 X p- m9 x2 a S
& m1 M6 M& ]% x# g/ C' r+ K4. 整体测试
: j8 x0 X& V% k. `1 f本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。7 ^0 X$ f+ d: U7 ?
! A9 |) N [) x( @
]; f0 ~* Z5 e1 A6 `4 Q源代码( o- H; `; T g% d4 S2 g" q
BootLoader源代码和App1源代码可以在原作者的gitee获取:- 由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
; s( ]; ^5 S% k5 _# P ' C; ~4 B7 s- V3 ~$ L
7 J3 {7 h2 C1 ^- n( e
BootLoader的下载- BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
- 按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
# K& v1 m6 e1 }5 Y6 Y) t ) e7 T! l! n/ K
% E; z1 ^+ e& H" r
- 烧录代码
- 运行, 通过串口1打印输出, 会看到以下打印消息
- 说明BootLoader已经成功运行) P* f) q: l9 ]1 t7 Z
& |# J! r8 Z7 V3 S' f) y7 v
' \4 U: e7 j& F& q* f$ g3 \App1的下载- App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
- 同时也要修改擦除方式为Erase Sectors, 见下图% v2 K& w7 B- D( C8 R, n! a$ d
8 ^1 c) H! b, o) }; j
% e |( z# |( ?! J4 b3 C
& g1 ~! @ {- Y2 X: r
- 烧录代码
- 运行, 通过串口1打印输出, 会看到以下打印消息
- 说明BootLoader已经成功跳转到版本号为0.0.1的App18 E% @% l1 n4 R: {& g: W; S
( U2 H3 ?4 }" V( J+ X
( }8 t6 K, {+ k; Q
生成App2的.bin文件- Keil如何生成.bin文件, 请参考这篇博文 Keil如何生成.bin文件* Q+ ^8 H$ ^2 ?
https://blog.csdn.net/weixin_41294615/article/details/104656577- 修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
- 生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件9 T% |, O( m) S" q, o
* y3 Q( X0 z# [% l
( c2 `$ [" G1 n) N' z; \
使用Xshell进行文件传输- 打开Xshell
- 代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
- 所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
- 你会看到App的版本成功升级到0.0.2了.
- 如果你到了这一步.
- 那么恭喜你! 你已经能够使用在线升级了!7 a8 M3 b5 e: P9 r+ w! B
6 y) x |& }- ]) u% o! n
0 T# A6 c1 F( m
5. 总结' p) c$ ]: r R
通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多。: m" n! N9 r4 ^
: E/ M1 x3 F6 g4 \ I
0 N, Y" X# V2 m* }+ c6 O' G+ o
# s/ a; T1 Y B/ Y$ c( N5 I
转载自: ARM与嵌入式
3 L4 ]2 I0 p+ ~) T w' ` V: T如有侵权请联系删除2 Y$ P% ~+ F& @
& o/ Q2 N# ~0 a$ X9 ?# L
6 L) k! q$ g1 g: j; n' r m7 E
' |+ u7 T. i. j% |, S1 Y |