简介8 w- ^: a6 j5 B, C
本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.
$ \2 _8 ^; E4 @; h& u8 X
5 k, j! N/ M( j- S5 Y
- F$ v' E* Z6 U8 b8 G2 _( o1. OTA基础知识
* b* v7 w7 q9 f; y7 z什么是BootLoader?
1 k& X/ {% D0 ]* v8 H+ a0 ]4 e+ MBootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.% |5 N9 C7 i7 J+ ^+ |# z
% }: M5 _/ u* k# |
STM32中的程序在哪儿?
5 H. J) B) N, c" R: f1 ~, k5 H, W正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码3 ]7 u" ~/ c/ E1 N. p& S
1 B4 V7 }9 x+ f9 R7 p
4 r. [& v- c% A+ k, T- }$ n
$ K* K2 k8 ` z$ w3 w* Q接下来就可以进入正题了.
0 }1 G+ q8 X, @$ `3 n2 o- o; m4 J" l8 h9 ^# g) a+ p7 z* A7 f# t
进行分区
4 _+ f. E& P: D既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:+ U/ K) e- o1 E* U; b# ^' r" I; N
( f% r% j1 r* d
: ~+ P$ ]0 h" ?+ ^
' n3 R6 I. z7 W. K9 E9 r' E, E+ k5 V! j4 e2 N, X
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:" h3 f3 e. [6 z: {9 Z4 ?
BootLoader区存放启动代码
' _3 ?: b( u* c: \App1区存放应用代码4 i E& c2 f$ V: U: s ~ t
App2区存放暂存的升级代码
$ E0 Q: y8 W7 R/ i" r1 b% ?0 \' ]6 t* L% F9 g" E
; u6 u9 U8 b, D" m7 L
' ?+ N, C" u5 E7 ]总体流程图
' O" B: Z" g7 I& n" q+ g+ B7 O先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.' X! T$ t9 k3 k# q
然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
4 t3 i' e' Y) Q5 I, E+ D# e+ [" q1 K" @" D4 Z
在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示: j/ |1 ~. @6 i% b1 p* I& g
( o1 L, D3 e5 p+ z
3 ?( x3 l& J# ]0 x8 i3 b) J # G1 U2 R! j. D# Y1 S& l
1 z% ]" `8 O; A& O7 D
; Z4 V; t% [# L6 Y2. BootLoader的编写7 |. `9 d) L) m# i4 k: N# a+ [- n( p( z
本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。( S4 x4 L o1 a$ L" B
; [2 `" M2 R' k3 J
流程图分析
2 D! O* u! x) w, I+ u以我例程的BootLoader为例:+ L1 F: a$ x4 ?- ]4 X
我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
' E H1 B- g2 M; c, ^( a; ^) W. E+ ^+ E; D" y- m1 s! o- ~5 `* m* g
! ?: U( G+ Z, s/ B6 e y5 R$ T
! C" T0 W! m( M2 A程序编写和分析" `2 o+ v! N# P9 k) Y/ e3 T
所需STM32的资源有:
" p% n: N4 s, A( g; y* P2 v- w发送USART数据和printf重定向* n8 z5 w( p* ^/ i0 G
Flash的读写9 y7 r* W+ u% b0 ^7 `$ e
程序跳转指令,可以参考如下代码:
( D! @- p) _) Z, ?6 Q) {) H/ F9 Q# L
v7 C2 A8 W5 o: F2 G) X4 Z- /* 采用汇编设置栈的值 */( r& e* @, I; v9 w4 S% F5 S
- __asm void MSR_MSP (uint32_t ulAddr)2 I/ f4 @- Y* |0 x% V9 V6 ~
- {
. v( x- I' q% U - MSR MSP, r0 //设置Main Stack的值( c' _, K8 R& R9 c
- BX r14. V* s9 B4 I& t( w! j9 {
- }
7 Y1 V" j3 C' G7 U! v$ \ - * K% t* Y$ h5 H3 m0 m
- & s" H3 P) M" R% _+ d
- /* 程序跳转函数 */$ g1 V; A" n+ Y( ^
- typedef void (*Jump_Fun)(void);
" j X. Y; S- w. G# n5 I0 d - void IAP_ExecuteApp (uint32_t App_Addr)
( ]$ [5 [8 B) l" o - {3 Z/ ]1 B2 y% z! o6 I
- Jump_Fun JumpToApp;
# E4 K2 H2 P4 c. w
+ F/ p* _2 |1 S) a- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
' H# d& Q: N, T u - {, u, ^2 f" M! X' O7 @9 u
- JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址). m/ l) }' |1 p
- MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
: F0 |2 B0 ]2 s# V - JumpToApp(); //跳转到APP.
) c. \) N+ g/ G! A/ ^( q; G8 T - }1 P9 V6 e: S+ ^6 c- I! O0 Y2 S- I
- }
复制代码
$ p3 f( p9 }) l& c" Y* b在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);
. h/ F1 b* Q+ u) g8 x其他的代码请参考BootLoader源代码
+ F" v; w$ L# W- E$ I( k6 Q( g+ p0 _: }. M
3. APP的编写
* a7 \" D0 ~/ i* U0 e L本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。
- @* o+ O! C- c+ P. r% @. i. K, }' c+ g. b: c2 x
$ z- G# E: N: j流程图分析
$ e6 K3 C* i9 u( D以我例程的App1为例:/ i/ o4 h5 R0 M; i& `1 h4 f, n4 L
先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题; Q+ F4 n' K1 n
打印版本信息, 方便查看不同的App版本;
+ q& Q! B! D# L- v9 g本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
+ ~% m- {3 [7 Q- R2 m) G+ \8 S, E$ p* f7 Z( Y/ g* v9 w3 [- V0 e( ~
* m8 r0 ^+ t3 x/ H$ i& a- b( U
c+ g& c9 n' G" k1 H) p程序编写和分析
+ Q( t/ l" N8 M- V9 f# c所需STM32的资源有:
% w- D8 N8 u- N; u9 n发送USART数据和printf重定向
1 k, B( w6 @: H. q2 U; OFlash的读写% d- g% s: ~+ w7 I; w$ Z
串口的DMA收发
) B1 }* m9 c; zYModem协议相关7 L3 K7 q/ ^( m3 j" E% C% C
Ymodem协议
0 U2 ?1 N7 b: [% q百度百科[Ymodem协议]
|1 m4 q+ A. Z4 S/ |! t7 Q; m具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).
% `! U: g+ h R( SYmodem协议相关介绍可参考我的这篇教程 YModem介绍
7 V8 U( n4 w$ W( o& P+ {0 c+ v
% @1 k# L# _0 |& s, L' R6 U9 v w3 T代码分析5 J- ] m* q. |, b( M8 r$ j
代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
% F3 O2 B" b1 H- e& K3 X6 _( ^" S( p. U后面放了我的源代码, 详情请参考我的源代码.
( q; u5 n- o3 Z$ a5 n主函数添加修改向量表的指令; Q' ?: n" i" _
+ m- u, A, C5 r0 G. _2 {1 e! M% a+ j# j1 i! Y0 M' ?
7 B9 ^1 ^* }6 }, m* Y打印版本信息以及跳转指令+ W, `7 V5 r, C
& T2 h8 ~& b3 @. j4 X" {+ J$ H# m7 V" B
$ u6 c; c, F* {/ ~8 A7 w0 _ Y
YModem相关的文件接收部分, F" L/ y0 i+ i
$ W8 p2 {% n `7 d8 Y- /**+ ?$ d: o8 K* N2 v5 Z
- * @bieaf YModem升级
' o* T+ d- U/ u5 F# F - *
M! X( x) c* ]3 u+ H - * @param none
; W- l/ c% O5 d) m7 E O. K - * @return none
$ t" ]$ V+ U6 y( y6 E - */
7 Z9 M0 h3 J( t# N) x - void ymodem_fun(void)
) F- @) g$ w0 ` - {- S) e+ R# p5 Y
- int i;
' |- b" s4 }8 m1 v& a - if(Get_state()==TO_START)
" `& b5 X% @2 @7 m - { e3 {# ?* c& Y
- send_command(CCC);1 O/ Q0 M4 n9 v
- HAL_Delay(1000);7 `. U! E) N, o; i) ]$ e& d* ~
- }$ i, W& l$ H9 u; T6 ~+ w1 g
- if(Rx_Flag) // Receive flag
+ W/ z" |# B# e! R a - {
. S' E% E" S7 s* m" F, D7 f+ F - Rx_Flag=0; // clean flag
, v8 N7 D+ B! a5 M7 \2 n6 F5 `) I - 8 l! F0 P" S* F% c0 C" x
- /* 拷贝 */: g& Q9 b$ W$ X1 c9 d5 z. i0 z
- temp_len = Rx_Len;/ `5 n G# @, ]6 l0 h6 q
- for(i = 0; i < temp_len; i++)
5 Z6 P% o' {- q7 b, Y5 O - {
2 J) ~, L) y# D8 T) y - temp_buf[i] = Rx_Buf[i];
5 f9 D) P4 p, ]' a" _1 W - }
+ e. ? f4 n. b1 l: ?4 S5 j3 j) Z -
7 x7 Y( }# @' R; B, t q! E - switch(temp_buf[0])
3 i9 g7 [% d- r+ B0 u% I; P _6 Z$ P - {
2 [0 {5 |8 {: N; y. u! N - case SOH:///<数据包开始
2 }( i3 v, N' G0 P7 C( z" ]3 |4 ] - {' k5 [3 y+ u0 s7 M
- static unsigned char data_state = 0;( }0 P8 a( {, m8 P) B# b' E
- static unsigned int app2_size = 0;( o( S K7 d4 V: {- N4 S: ?
- if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验# @+ Z: r' [. B& l
- {
) r/ H4 \9 @/ \7 ^& ~1 h0 p - if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始0 F5 g' y2 m& f1 G% j8 v5 r
- {
& T7 g1 I! r h0 i! m6 i" Y" ]5 Y - printf("> Receive start...\r\n");' y$ X% T) j9 s" N0 B
- , ^; ?/ v$ h, l- K# ?
- Set_state(TO_RECEIVE_DATA);3 G& s8 L8 G5 g0 j9 R
- data_state = 0x01;
9 `% R' x0 t1 s5 b% E6 T; o: Q$ Q - send_command(ACK);
3 o- C5 r. ?& Z5 E2 \9 M - send_command(CCC);2 ^0 t0 O, `* \4 w t
- 9 G- R3 p0 g' W
- /* 擦除App2 */ 1 I) V8 i' x0 p* e9 y. M7 A6 s" X
- Erase_page(Application_2_Addr, 40);
4 {% {3 R; K$ _9 z - }: J. g$ w% j+ ^. L5 a! u5 j/ h
- else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
+ |8 x1 S3 u/ `! p5 H$ h - {
% e" d) l" a6 T/ P c - printf("> Receive end...\r\n");# q5 Z( S: k& C" W5 V
- : j2 X3 \& @7 `$ `
- Set_Update_Down(); : v+ w4 J8 t7 p2 u. K( L
- Set_state(TO_START);% I3 Y' `. A- ], n* V3 ~7 }
- send_command(ACK);
2 C; K2 \6 [7 H4 _% T4 p2 S - HAL_NVIC_SystemReset();
3 `, ^2 a8 o# o6 |: v - }
2 H5 s& }7 Y* o# p - else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据
1 ] m" ]' {" R6 k' Z4 Y. ~ - {6 N0 |' J4 D* W- w' ~6 i) L
- printf("> Receive data bag:%d byte\r\n",data_state * 128);$ g) [) A+ Q, ~* u0 W
- 0 g* n$ @/ {) F; r
- /* 烧录程序 */, ?6 t+ i; s4 D/ X( q% t) _
- WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
7 ~/ u H3 V% P! ]5 y5 O( f4 J - data_state++;
! A! N8 y/ u: a9 q$ i& Y' v - 6 M" L' @# j! A$ c, @8 F
- send_command(ACK);
& H! O" u9 ?. w& w - }+ B, O2 D/ h' E) M
- }
8 Z5 f; e) P1 C3 m& Q - else
9 w' G3 F& ?' X4 a5 J# U - {
! p& y8 ?# R6 U- T. u+ l. S - printf("> Notpass crc\r\n");! j7 h4 e3 W. ] Z
- }
+ o+ m3 z) F; J9 g - 7 t) T ~8 J3 \3 L6 E: k
- }break;/ m% x! M+ F6 q' m8 H
- case EOT://数据包开始
" J3 M: d5 ?, s: H/ S - {/ D7 u2 M \! m. u' [
- if(Get_state()==TO_RECEIVE_DATA)
+ U4 W6 q( L; l3 F! ?* \2 O - {/ g- U0 K9 y$ z& l+ j
- printf("> Receive EOT1...\r\n");
4 `" C$ L( P( w4 s - & \* B2 [4 q! t( q o8 k
- Set_state(TO_RECEIVE_EOT2); & g: V5 N1 V2 v% Y( r
- send_command(NACK);5 g2 g* B% {( w: {4 a% D
- }! n/ L3 N. A: V3 |$ p
- else if(Get_state()==TO_RECEIVE_EOT2)- n- i$ x) S: d' C; c' Y* p1 E% `& ~
- {
) j- r! a, A/ r9 i3 |4 h# N - printf("> Receive EOT2...\r\n");, V- }% v, F, _; p c
- & Z! ?6 k2 r# N# m' o
- Set_state(TO_RECEIVE_END);
2 U9 {) z- r- ]& m* ] - send_command(ACK);
! E3 N' U* J+ l. C - send_command(CCC);# c; k* L* n: }# n# d/ b" w
- }
0 R1 F* S/ ]8 W3 X- N; i/ L8 @ - else4 Y2 {' z9 h8 F. {! @2 B
- {3 B2 T! V- P B2 R0 t) D' k
- printf("> Receive EOT, But error...\r\n");9 j+ z7 G% g |0 W, j0 M
- }/ f- R) p# A8 y% q' m( g
- }break;
& X+ K. p, c; r) \' S - }
- X# B3 ^8 D0 d! C+ ? N - }+ }9 b( b9 ?4 z9 p" H- f" ?
- }
复制代码 & J9 l" C. _* V
其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.7 |) v W' V; }3 V
! x! ^0 r+ T8 N8 w1 k9 |# ^
, W8 }0 F2 {* [4. 整体测试
9 _/ A) k" i1 X- q% T/ M0 U本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。
2 F# d, [1 j. z( b5 E! x" o
8 E+ u% X4 x7 V# }4 \, t- u3 `$ M% s源代码! _6 e% h2 {* L8 f5 I
BootLoader源代码和App1源代码可以在原作者的gitee获取
0 U& r4 {( v7 G/ B8 `
2 u) s3 \4 p# U代码的下载
5 p8 c- f& u( q* R; G* E- K由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
( q4 V9 P3 w8 {# {$ F3 w
" V% B, _% w. q1 p4 l: O0 t/ C
( a6 M, s2 i0 L, T% ?9 v, ~+ o0 S$ `, j
% \% ?% w/ O3 x1 _ V2 I# V) E7 Z X& i! v; }6 _7 |
BootLoader的下载9 c+ b+ j U( E9 z3 D! }6 b
BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
$ Z; h# S; u* T* [0 Y7 z I) Q按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
* q9 {& C7 b Y' r! W# p( Z& ]' R" M- f6 M9 S! T
( m9 r) e. h4 G1 {
}0 w. }- }8 }$ E2 u8 e. g: w4 n% j7 A
烧录代码/ S) W$ S6 N' b8 j5 I( \# ?& I
运行, 通过串口1打印输出, 会看到以下打印消息
5 G! [8 @: J; r* E {说明BootLoader已经成功运行4 P% O& a' L2 f
, O1 r. C# G' e5 L$ K" P4 y" ]
1 m* ~# o8 i, F+ l1 i 8 U- }* W$ \% u- E) u+ W
/ T; Z/ Q$ s5 ~. P M4 j& bApp1的下载8 i, C9 C- L1 }
App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
3 j/ e/ V1 C8 R1 S: r$ s* u同时也要修改擦除方式为Erase Sectors, 见下图
5 A" ?2 G+ `0 L9 g/ n1 x9 F, o L( D6 o- E
9 j* ^6 U& ?) [ y/ ? 3 U4 a( P" K# ~, C
) E5 v0 C& i$ x- b3 b, R
2 C8 H# ~1 }& G1 g! w
4 i- T8 O9 T n! D. |4 J3 Y6 C+ N( D3 b3 G& t# X* B! \& v. M
烧录代码
) B5 _$ k* f$ X- G* i9 j运行, 通过串口1打印输出, 会看到以下打印消息
7 J; ]1 U3 J) A2 Z3 N) `9 B3 R说明BootLoader已经成功跳转到版本号为0.0.1的App1
6 t/ ?5 |9 E/ V2 C3 D' t
1 I. w/ ^& e- r! b+ g0 a
+ p6 r8 n& m0 D X- S+ G/ ?! U1 R# v7 M( ~# B
生成App2的.bin文件3 ^( k P h% N- l
Keil生成.bin文件
4 w! o4 C; w! W* g
0 I! r! h' v& O3 i$ W& y$ _修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件" Q0 W$ {' F! @; ^
4 N- Y% k0 @4 R+ z0 _
生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件
5 d# ^+ b, `+ u1 [" e) m, a
9 n [1 @' r2 ~7 e7 H. n1 b2 m- t+ d7 e
/ E H" h& s5 z
% Y' Y$ }6 `) A! X8 j, Y" l/ B使用Xshell进行文件传输
$ u; ]& s2 ^ u) ]打开Xshell
' `! \, j- w% x2 _代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的+ a) j; r c, ~ l
所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息, w0 d, i- v- I% r# m
你会看到App的版本成功升级到0.0.2了.' \( e+ X$ ]" n0 ^4 \
如果你到了这一步.# w: c. m& Z4 L: U+ g
那么恭喜你! 你已经能够使用在线升级了!( G: T( z i1 D9 ]' R+ H+ I. [
" m) C" }3 |. }* E8 h
5. 总结
+ y2 b& J9 ^. W5 q. w+ X通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.- A X* w& a2 U: R
4 T: o/ K* \0 N
转载自: IOT全栈技术
" Z( I2 ]+ W% d/ L如有侵权请联系删除
8 i, x- B; P* s- Q9 _/ B( J) S0 I( ?% w+ M) c% p
|