简介9 D! {7 k0 z7 M( S" [
本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.8 h: Z i& K/ F1 f
/ r' E9 d9 c) |* Z# q7 L9 A, j* W* }2 Y- V% A: I2 T
1. OTA基础知识' u( N0 D2 N" r' u4 E# l4 J3 q6 r
什么是BootLoader?
2 b; D$ y; {) U' T. p' dBootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.8 s- g7 y8 r) |+ c
; e8 v3 { ?# E" B
STM32中的程序在哪儿?( R% X% |' Z. a3 U F
正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码5 i( ]$ A! b- G) R, f) q
q5 l; t- E7 q) ~( e: C
" n, k, z! ~0 I" G, s3 m n Z0 ~: u3 K3 s9 v% {' `
接下来就可以进入正题了. x! i. K3 V) V0 E9 a6 s) K; ]' s
2 K ?4 Z4 t+ t5 P
进行分区
p: Y# F( a- @- W# }既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:
, N' ]4 b, L; |& e! k
4 E: _* ?9 E7 i C( A8 a- a$ E+ S7 X4 i
& G/ K# I1 G2 ?) j
0 M$ S6 y) C" \. A- d0 M以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:
$ w8 b% N8 o, P7 r; YBootLoader区存放启动代码. U( N( E# E4 g2 K: F
App1区存放应用代码
& B. b% Q% W& K% RApp2区存放暂存的升级代码
1 B" v6 K$ z3 ^, J
3 R. X" H5 ^9 U- a2 C* _0 l' r. I: [! t4 J8 Q1 R! C- Z+ v
" K+ E- Q3 G4 m
总体流程图( R) W7 T" e+ n
先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.
: }9 D7 [$ e7 t/ l4 x然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.. D6 V9 {: h7 {' t7 r
+ c$ I0 b) _$ Y' n, ?在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:
7 `) m4 G1 r. ]" N/ |' `8 J/ T0 Y, V% P& q" T5 L* U4 Z6 w
! W* R& W! g8 L# F6 R( k. _
4 S4 R: K9 R: L6 @: I
# p$ |, ~3 n0 P) e" k! k, n, V+ Z+ _ {
2. BootLoader的编写
, E: z, N! ^4 V/ C7 m本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。0 H3 O. c( Z* B* Q8 h
$ J' H- |6 V+ Y- H* ^/ V流程图分析
/ ~: p) F# k9 p: w以我例程的BootLoader为例:
4 q/ p/ ?* n; D5 H( M! C5 @我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示- Q, [$ w8 ~2 X! C2 _4 A8 b
* V, i! U9 H. p1 i3 _6 R
5 p/ D6 M1 l) x* |7 x$ _) N% ~$ d7 \! V5 Z& Y# B
程序编写和分析; X e- L6 E% U- i, d" m' V9 E
所需STM32的资源有:* J4 a1 R) Z/ m! S1 f& {, k+ H8 |. j
发送USART数据和printf重定向0 k- o2 Y R5 E; ~- j) v& u2 J
Flash的读写* l& [1 K( q( {- h, e* W5 E
程序跳转指令,可以参考如下代码:
# |1 Z" W1 B& j2 s, q) Y; L$ v8 r0 X/ Q( p
5 s: M6 R: u7 A0 }, K$ m$ A9 k
- /* 采用汇编设置栈的值 */
1 ]4 m4 M8 M8 _0 Q) b* l) n - __asm void MSR_MSP (uint32_t ulAddr) o+ V8 {! C. ?) A. u
- {
. W$ W/ X0 {% q! Y - MSR MSP, r0 //设置Main Stack的值
. y u/ w0 {7 [0 P u - BX r14
( H; W5 f9 A* k: Z* u - }8 z* D9 A% p: K
4 M: `& w g- J- Z# P* C8 r
1 A d- Y) U! }* x$ t7 X9 D9 @- ?/ I4 o+ L8 i- /* 程序跳转函数 */
: y2 H- O! ~7 p - typedef void (*Jump_Fun)(void);
" W0 x3 _+ T0 g( c9 Y* | - void IAP_ExecuteApp (uint32_t App_Addr)
- Q. i4 f6 e* R9 i Z( X - {
0 `1 g4 e, j3 V Y - Jump_Fun JumpToApp;; z4 R) y% H' {0 o
4 ]/ d7 B3 `/ a( O- @) K8 ?- [- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.5 _& H+ x9 n7 |* L7 |( U, L6 k
- {0 R9 B% u2 M7 X
- JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
) l! O3 U7 k& E* ~+ ^& B3 D - MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
. e) ]5 F$ J% T' F7 U - JumpToApp(); //跳转到APP.
8 z# Z$ C+ a* _' Z$ o t/ Z5 M - }
9 }+ W( P. g$ ]: n) K8 s# c! t - }
复制代码
* H" X: }# {$ f# L4 Q在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);
, N3 g7 N' l! N其他的代码请参考BootLoader源代码
6 Q" [ m8 t0 Z6 g( N3 |9 g
, W+ \8 z) G; p6 k) T; V% h4 n3. APP的编写
! T( D; |5 c$ h本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。 f2 t- v; p. l
|" A' B4 B5 ]* p" N+ t( d
" p+ v' u- H! r1 A6 I流程图分析
' W3 d& M% |! m7 t; H# \. C以我例程的App1为例:" i/ M0 t- l5 q" Z+ \' H
先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;7 l- e" v" ~* P% ]) S+ m0 L. q
打印版本信息, 方便查看不同的App版本;, {" @$ |6 e) X5 P! t% g* k
本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:% c$ v+ m) C6 W2 h* N8 \
6 ^4 z6 g, Y" e# A/ S) d0 M
V+ p- P( ]6 F; y: @. t6 s6 s% I/ Z X
程序编写和分析
; ]$ k7 }' X- p) E. h所需STM32的资源有:( s. `) x, w* W# t
发送USART数据和printf重定向8 X/ ~0 V0 X( o& y. J! D
Flash的读写8 d2 G8 r" o+ h! h3 I, ~: u) M
串口的DMA收发* C6 [9 \# l% K0 L2 i+ o
YModem协议相关$ h$ N ?) R' U8 D. u$ W
Ymodem协议
3 _: p# V I% ~+ {; O/ s. o; k) K百度百科[Ymodem协议]
7 Q! Z$ C: ^( B6 p- v0 z0 M( y5 l具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).1 Q4 b7 [: h' s; H* K
Ymodem协议相关介绍可参考我的这篇教程 YModem介绍0 c7 b4 K2 M$ W! y9 m
6 s6 c0 d& |3 ^* C, M( t* N3 ?0 H" B
代码分析
5 ^4 ?$ z/ Q* }4 a) d4 Z+ Y/ L$ g代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
/ r/ y; O1 I& x' C后面放了我的源代码, 详情请参考我的源代码.( Y0 p3 N% \5 P7 ?" _2 Z/ t N! ^
主函数添加修改向量表的指令
@/ z/ T( N' @7 c' Y0 n, c- ^" K) y% [9 C* m6 x% a' k
# t' e2 _+ Q' g7 {. Q
& x: @- m3 Y1 j2 O' ]# K打印版本信息以及跳转指令
7 a; s4 J! h+ o% H, ^2 z, _9 ]0 W8 n! @. u5 q2 E n- ?- J2 K" g
0 [" j- a% D' [( n8 o
. f9 L4 E' r o; nYModem相关的文件接收部分, Z+ S7 D6 b: ^6 M: j2 i( ]( Z4 f( a
5 |+ h9 }( M$ X- /*** J5 Q5 y) u2 @
- * @bieaf YModem升级1 l! k `; L; s1 I% L5 U0 ~
- *
, N; y4 {9 M2 ~ - * @param none
# U) G% N! i: u7 V. B- U; M X - * @return none
. r& ~- R; A" w7 o9 u/ l; g, K - */
% b/ f5 @5 I. t. C - void ymodem_fun(void)0 m' ?) c8 }5 A7 `6 U; Y9 v* \1 D
- {
8 i Q* _& F. k+ H* f6 C8 k8 d - int i;" O- o: A6 Z. C* c
- if(Get_state()==TO_START)
# q0 _# _8 a' X2 U8 T - {+ x( b, [1 x/ T6 l
- send_command(CCC);
* |+ M; \& l: Y1 W$ ]4 v - HAL_Delay(1000);
" z: ~3 d! ^- a - }. A: A* ^( ~4 p* l+ Y/ t9 k) S
- if(Rx_Flag) // Receive flag
* M8 \* F" [" n. G - {
+ Z- L: \; F2 r* s" G+ l, z - Rx_Flag=0; // clean flag
$ m# l, A/ ^3 ^$ n - # z0 ` i3 I% E9 }* _
- /* 拷贝 */
) }6 q# z4 Z* r$ U2 U - temp_len = Rx_Len;7 f$ E) q1 c. K0 m. U W" y/ F1 l
- for(i = 0; i < temp_len; i++)/ K& ] P/ x0 J
- {
$ \% \% V* i. a8 p - temp_buf[i] = Rx_Buf[i];' n/ b) }" u# w1 v4 b# Y8 i
- }' \: c$ [( D/ l3 v
-
5 j) {; v% U5 I- B - switch(temp_buf[0])0 P5 _0 Z' Y& T' O3 {* u9 x
- {* f8 O( Q% \7 @3 u" z I& g
- case SOH:///<数据包开始5 s' m7 a) X' E0 U' o6 m5 T
- {7 {4 _- K( b- Z2 ~- r
- static unsigned char data_state = 0;! X# }; `! `: x: C$ j
- static unsigned int app2_size = 0;
9 n- ^1 w* R, w - if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验1 U/ F1 B7 e/ }( N, Y/ ?2 p& `1 _
- {
7 l' y4 R9 j# _: q: D: e - if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始
: S* f" n3 I: y, g; z - {
; h/ t1 A$ S3 h0 I! T0 a - printf("> Receive start...\r\n");& a9 j+ G) j5 E; ^7 ^& ?: U+ y
- 5 X6 Z& |, V* n2 i# F5 \* y) W
- Set_state(TO_RECEIVE_DATA);
6 S- t- o6 T- d3 D - data_state = 0x01;
: [8 [. j. m9 d9 h) ?: V4 f - send_command(ACK);7 K( ^ z3 O! ], c/ t
- send_command(CCC);
: S4 U; k3 ^2 S0 |. |% J5 H - 8 E0 T1 ?) h2 i' _0 Z
- /* 擦除App2 */ 7 P, u( R0 n# m+ Z- R% e
- Erase_page(Application_2_Addr, 40);
5 }) z1 g% R- P- L O0 ~ - }
" ]9 n$ A" S9 E* j/ ? - else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
) y: X Z" C& m2 y) {3 K( x" B - {" p8 @( c) `+ R* e
- printf("> Receive end...\r\n");% Z) Q0 u, n) p! H, M1 q
3 {) L; _" O4 ~: {' w" F& B1 a# R4 d- Set_Update_Down(); 1 k8 e/ v! A# Z9 L! t3 m
- Set_state(TO_START);; s) y% D+ O% u2 |* ]: {+ O: s+ \: o
- send_command(ACK);8 M* z' k. ?$ f T. {6 O4 V3 `2 v
- HAL_NVIC_SystemReset();
3 l8 C+ i- k3 [7 ^, ]3 T/ Q! q - } $ h H9 A/ ?, P; x8 ^
- else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据
& |( ^9 h- ^) D3 h5 Q j: d3 j - {7 o. v; Q! C" E; ^
- printf("> Receive data bag:%d byte\r\n",data_state * 128);/ s- \- p# g* [% Q8 c! i9 I
- 7 F' W+ `& J/ Y
- /* 烧录程序 */
. t6 T. Q: w( }9 ] U - WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);/ p F: r2 H2 U9 Z8 u- j
- data_state++;, d3 {9 k: W- R7 P( u" ^
- # s2 L9 E1 O9 Q" F
- send_command(ACK); 8 y2 M5 H$ q& j8 \
- }
, K2 |; i2 r) z3 i$ Y7 Q - }8 X8 |' C9 v3 R) M+ O% L1 t' I4 W% u
- else. B& w2 w" I) n' {) T. F% R
- {, z7 v+ s6 W. l. F- \% \
- printf("> Notpass crc\r\n");8 i* m6 A$ p: U0 b5 b5 u" w
- }% O9 W4 ]7 M7 s
-
6 n4 E& J3 @, U! U - }break;
) B0 o' C2 c; r, j* _% C2 q7 p - case EOT://数据包开始9 ]8 c' F: W' v2 ]
- {6 Z9 g: t) x/ m3 z: G
- if(Get_state()==TO_RECEIVE_DATA)& U6 E) V$ {( [4 o4 H
- {5 ]6 w- j# Q _% ~9 j
- printf("> Receive EOT1...\r\n");
+ `/ l9 X H& G/ t+ k - $ m2 Z3 c" n& d' X
- Set_state(TO_RECEIVE_EOT2); 1 J+ d% m: n% r7 G' f, [! l
- send_command(NACK);/ V( m. ~, {5 x* r# Y
- }
& ?& y* o3 }1 R - else if(Get_state()==TO_RECEIVE_EOT2)
: h+ k7 _+ J/ @# L9 a) q! A - {! J ^1 n+ m( l5 X8 `; S: O( I' X
- printf("> Receive EOT2...\r\n");
6 c3 l0 e, k+ d: m/ _3 y7 c -
. O1 H1 T+ r' b8 y$ l* q- y" m - Set_state(TO_RECEIVE_END);
. s% E' c9 b4 u" d% ~% M) y - send_command(ACK);
# E; H1 V# S z: z* V# N/ H - send_command(CCC);: s' z$ P. F$ g( W# v
- }
7 [+ P3 K. ~( C - else3 B/ M" V& y: J- [ T: h
- {* S: B7 `( S) N" C" E
- printf("> Receive EOT, But error...\r\n");( h# f8 K2 m- ` j$ C6 m) ^. |
- }
: }& @& o& m% ]2 m t - }break;
# A/ H; z$ Q7 d) A; ^ - }
6 _, l1 U3 S- J p" F* Y; H - } h* O# y+ A* s( g4 p! F
- }
复制代码 ' y+ l! r$ ?; C$ g& @
其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.2 l, Z0 W. ?* s
2 ^" j! [1 r7 h0 U/ |! Q# o( ^3 u2 D
4. 整体测试: A( e0 c- _6 y% Z3 D% q
本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。+ y/ k" m; M% p7 l) N
# k- w4 l+ B- X9 t% d2 Z2 J' \源代码4 K- v! s/ ~! t) w8 m8 s
BootLoader源代码和App1源代码可以在原作者的gitee获取& R- a u7 E# N- Y$ y" N
4 U& \2 e4 M" x+ K; s' Y: @0 @$ _
代码的下载" q' B, L# N/ ]: b. t# g; C( Y1 [
由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。+ {; G9 W' z \% s/ t
( [) W5 Z" m& A. C4 S3 j* m( |/ a
) H O n' O: i# @$ h* q: X* i3 ?7 ~
, ~/ {) L5 _- d( _# j! a- d
; c2 O* {( P) pBootLoader的下载
4 k, k& |, G' [2 X2 T6 {BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
9 P: U0 A8 O3 u2 T& L按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
$ ^, C+ b+ D) l$ D V6 W2 d
* F/ g1 G W( U" ~
" H/ v2 v6 ^1 E0 M. j% ~ D 4 K! ^ E+ q2 n, L
( x# R% |4 `5 A4 v6 b" w7 I0 X
烧录代码: x1 ~0 c4 K, m! }3 Z$ v
运行, 通过串口1打印输出, 会看到以下打印消息
8 I) Y. j* C2 m/ J0 g. M说明BootLoader已经成功运行7 j5 J0 ~ b7 |. f! Z
) F4 l2 t# K( N
, r$ L! F% L6 p
5 j: _- ^! f( v5 W* u
) V: l2 t1 g4 f U0 x; T& z; QApp1的下载
5 F- r( H! v6 J. q3 K- RApp1稍微复杂一点, 需要将代码的起始位置设置为0x08005000$ ]6 H/ _# l& i$ X* K2 O$ |. N
同时也要修改擦除方式为Erase Sectors, 见下图
/ c: }% g. D1 h4 ^; m5 \" D/ Z' w7 s2 `, R: M! {; ~
" V' Z3 e) R& \8 H! U( s) ]( x " \. v2 T. @* f5 g P
& `! G% O$ T' _" ]! J8 u! |8 C5 ]
! Q @" G# ]8 f1 o) T* T
1 T# ^! V& v8 x* ~( z! D. C
+ y& w3 D x" }$ Y8 Y烧录代码
+ G/ [9 h6 O7 S运行, 通过串口1打印输出, 会看到以下打印消息) [4 Z2 M. ?+ H+ D" l. N9 x9 h, A q
说明BootLoader已经成功跳转到版本号为0.0.1的App1
/ b( M! d @ G. y: L2 K; H+ X* b0 O8 r9 f2 {# B
; K1 _# K1 v1 E
' ? T- Z. C t+ j7 \- n生成App2的.bin文件: ^2 F. y1 f( D# s& m
Keil生成.bin文件
9 x; |# ]% W0 \3 x% F1 g3 n& H" [( z0 u9 G
修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
: c4 d) A( {7 c3 H- ~( p: P
; w' ?5 o) a5 s4 h8 Y8 d/ Z生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件
5 X2 Y: H* }' {% |# D1 g1 G
! V5 D2 l6 _ _3 @$ i W$ E1 ]* u# j! z" K
! r4 f: e% E9 x
8 _# h9 U* }0 G! O( C使用Xshell进行文件传输0 s# T/ w. `: B- m) {2 k0 y5 s
打开Xshell
2 h, Q( j- J( W' \, p" {) {代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的; V/ d& y( x9 D7 m& S
所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
( ~/ w( ~- ]( K7 _4 T" d. M- {+ W你会看到App的版本成功升级到0.0.2了.7 n# t# I' U' V( P
如果你到了这一步.! k: K! h* W/ n! L
那么恭喜你! 你已经能够使用在线升级了!
/ ]* m+ _3 `, B1 E0 m' V% o: u4 d( u, f K' T8 n
5. 总结
4 [; K, }5 |! W7 J; J3 e5 P通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.
" u" \% D9 s9 B; o$ H, j7 }" N# z3 ?: _1 w
转载自: IOT全栈技术
4 _. g1 {4 `$ Y9 w如有侵权请联系删除. i1 |* @9 V$ \# @
0 T8 B9 N8 j7 }1 q8 ~: H# w# [; c
|
你好,有程序源码或者链接吗