简介$ H8 v2 {5 r) m7 p& C; d% X0 D% F2 u# U
本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.
4 y+ K+ @8 o; v, v9 N! f) E$ ?- ~# f$ p) z* \! M J
+ |- }: m' A7 y; u, q1. OTA基础知识, K9 @6 J V) a( x* F* R/ m
什么是BootLoader?
# E5 o8 B$ C# e: f0 CBootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.. m6 z$ V! k- P8 r; U- s* H
, @/ E! o6 c4 X% [$ ]4 p. U: u) {STM32中的程序在哪儿?
' d" f _- T9 |" E正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码0 i& T7 e) ^) v, y& R2 d' x, }
2 \/ Z" o' P- d( R( ~5 f
6 [( I7 y: n2 z2 f
5 s+ w: d; W/ a* n {3 w" T) @9 T
接下来就可以进入正题了./ O7 m" L2 V& F3 c% j7 u3 U
& B/ U/ z4 @ ?: T3 R. `/ P( |5 P& O
进行分区
1 U! p% v. l# P既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:- Q4 w7 n& b( A1 W
% N3 m$ x& h; L# V V: i) E
) h% j" T% n" S8 P1 N+ e4 z6 D
+ Q* h0 m- H/ R- H6 ?; D/ N) l4 ] c- I2 C- @. ^
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:
$ N$ s8 u" u8 Q3 a- h# R; u6 }, p7 iBootLoader区存放启动代码8 D$ B5 J1 L( b" W# L
App1区存放应用代码
6 @/ g7 c) S, S- B5 s6 L5 LApp2区存放暂存的升级代码: e$ [( `( o6 L. l) U$ }2 k! m- {
$ R2 J& d# ^' W$ M4 t5 C! ?* G
8 H$ T3 j* ?4 m- r& N f6 W
总体流程图5 M% i: K U; M' c3 |8 t
先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.) d' V* [: ^) `# N" P& ]
然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
; G+ x: i, J4 s- U' H5 f f n2 `- H9 g
在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:
6 P6 V2 c" B% E
N# o# P. {+ v. u$ V- u
) D7 }6 j8 C7 z% c
3 Z( A8 A* j4 v# v$ T- [- O- b# n D8 u8 C2 ~! Y) X8 p5 f
# y4 G& g# p0 c% F5 |! F2. BootLoader的编写
/ I5 G* S% o, d2 _/ H+ P6 u本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。, |* l) U0 @/ p* @7 o$ V
5 q: g" F, Z' N) [7 r1 g流程图分析
- w5 [# x" K( |- B以我例程的BootLoader为例:
3 N: a" W' U- p3 I- n- o我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
7 h. ^; ]% p" g. w5 E$ F/ V# d% }5 q% a2 B t; Z
0 a7 D8 t+ ?7 R0 M2 ^1 e
+ x) C f2 {7 e* [
程序编写和分析( a% t y L) X& ^2 A* a
所需STM32的资源有:
, q) s: {& u1 o/ K/ u发送USART数据和printf重定向# Y! R- w+ t9 L- y
Flash的读写
) w1 K9 n# C; c2 u0 c/ [3 b+ R程序跳转指令,可以参考如下代码:
# j k* ~" Z- F8 y6 R; z: O A+ Y
6 F( C% C$ ~( |) L
7 q& Z" p5 r( ^5 p7 ~, f- /* 采用汇编设置栈的值 */
m7 x3 d0 ?; G4 n& H - __asm void MSR_MSP (uint32_t ulAddr)( Y! \$ N( Q& d9 E! u$ d
- {( y3 R8 |# Z5 W9 i/ [/ f
- MSR MSP, r0 //设置Main Stack的值0 {1 O; ~+ W7 T& U4 }' S
- BX r14
- L% }# `* k' H& y9 q- h1 e- e, u - }0 Y- H3 ]' l. l; O
' l0 ]. ]. U" {4 a/ p0 \, N. O- / I+ a' g. ?: F1 w- J( f A7 F
- /* 程序跳转函数 */4 o5 e# W: B1 V S9 J
- typedef void (*Jump_Fun)(void);
2 B' v* R: B# C% b' I' L - void IAP_ExecuteApp (uint32_t App_Addr)% [; s! m' v6 j1 u
- {! K' `2 L3 s. A1 _, N8 [
- Jump_Fun JumpToApp;1 z T9 F9 X3 l
. k7 Y0 Z V j- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
) R; y2 M; z8 a; [ - {
- K6 {1 j( s, a. t4 o - JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)5 f/ {9 h& [% K- Q
- MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
, ?9 x& S3 l$ n, Q" k! Q; G& a - JumpToApp(); //跳转到APP.
& r" u2 V( Q/ E1 x# l - }
+ ~0 ?" \) l& l/ }; A - }
复制代码 $ x) B' Y0 n% N2 Z
在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);
* v6 g' y2 `# p其他的代码请参考BootLoader源代码
# Q* M- G6 W- V8 ?0 `( O, ^3 n! m1 m
3. APP的编写
& v. w* ]6 k* E2 W, Q本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。8 h1 }% S8 S& K4 |. N
* Z& [) O; F9 T: U8 [+ F" Z
, M" f3 ~9 e3 w% I, \8 V# J1 v流程图分析* `3 f( [1 E. Y" r4 `' T4 @ H
以我例程的App1为例:! F, c5 C2 N @9 `2 Z
先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;$ {( G* y$ Q1 ~, |8 k7 J. v, B
打印版本信息, 方便查看不同的App版本;, [# k8 h7 Y3 [0 b
本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
$ i {# L/ s: e
. D% v" l7 O" v9 m
9 e. Y$ V! x% X$ A! v+ ?) d4 C. _6 ^; |2 _ Z% a2 e
程序编写和分析+ u7 N- [. p y& l8 l# z
所需STM32的资源有:% }7 i& _4 O5 ~( }3 l
发送USART数据和printf重定向/ A7 H7 l/ ^/ |4 M3 o
Flash的读写& K+ n/ H, ~! Y8 Q
串口的DMA收发
5 K: o8 o! r& m1 g5 _YModem协议相关
! h7 x; w8 I9 _0 Y6 o* oYmodem协议
1 V, |6 N$ @! T7 ^百度百科[Ymodem协议]0 e( o4 o' q. U
具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).8 @+ Z) ~0 u k. \3 P
Ymodem协议相关介绍可参考我的这篇教程 YModem介绍# l4 ?5 [' f$ v- @ D; m, m
" w0 C) _1 F4 _# `. a7 J, \* C代码分析
" k+ P3 S- V- [5 K- j代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明1 t8 X9 e% |' O7 q2 Y
后面放了我的源代码, 详情请参考我的源代码.; V9 a. r; B3 k/ `
主函数添加修改向量表的指令
2 V8 C2 ^' V, s/ P9 o& M0 D G1 L2 z8 o: E
# \% S8 o2 ]# e0 b
$ S4 L4 Y# L, y打印版本信息以及跳转指令5 s# S- R9 b, o( r% ]6 E C2 c
' s( Z! y; w+ N) m! b2 U+ I
) @; Z& X2 k( e
) l. a# V$ S6 k' c/ v2 o6 S0 z
YModem相关的文件接收部分+ D; L, I9 o+ x6 S( o* g
8 Q- Q6 e4 P1 h. y5 Q4 `- /**
8 \1 `: ]- A' L% D% b - * @bieaf YModem升级
; ]$ l8 x8 {4 i* z0 L( z$ Q B" `7 k+ c - *; ^1 g7 B* I: C4 ]
- * @param none
8 s( l8 y6 p% G6 U. `; ` - * @return none
. l3 Y$ g1 F# E. L/ A - */
) h" `# F$ o6 }+ E( w. q; d8 y/ _+ L - void ymodem_fun(void)
" ?8 k5 h1 u2 d. I - {
& u8 S$ H# H- {+ G0 q - int i;$ c' S) w; P( ~" j
- if(Get_state()==TO_START)6 A: \2 y8 [% S Z8 H
- {/ [6 x2 C( y4 A" G* ^
- send_command(CCC);! x b* `: O+ j) R; V. E! v
- HAL_Delay(1000);
" |& l; o6 F7 b7 _8 \% ]5 b3 W - }
; ]1 m( A; C; m/ w" f/ S# k4 U - if(Rx_Flag) // Receive flag+ H/ o8 a$ L& ~: ^* T1 Z
- {# Q% o8 g+ T# d8 D2 \+ J
- Rx_Flag=0; // clean flag
+ l" u. c4 Y) q: c; w - & q1 W- E$ X; \7 M
- /* 拷贝 */" ]6 k* G' B$ N1 m# Z" u6 s
- temp_len = Rx_Len;
+ C4 f1 R- T8 b+ r7 A5 m - for(i = 0; i < temp_len; i++)& C `$ I9 \. F2 Y' F
- {( E! }/ o% O& n4 w% E: _
- temp_buf[i] = Rx_Buf[i];* e: P/ o, ~# _& c! x7 ~
- }) Z% @ o; y G
- 3 R. d, B5 l5 D4 S' `
- switch(temp_buf[0])
2 ?. K K# ]+ e& f& z2 o* |* F- x - {
, P# ]9 x! g8 }% S! f; b" K* } - case SOH:///<数据包开始
( U, L- `5 H. k4 }6 x" L. S - {
# j9 A7 P. s+ T - static unsigned char data_state = 0;" p$ L# [+ Q: Y& S/ C3 K! w9 S6 L
- static unsigned int app2_size = 0;2 ]" V, c2 \1 E3 ?+ S, I* U
- if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验
" i4 V8 D4 a' n3 h6 e - {
# F! ~1 o0 i5 L& Q& C f - if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始
( |! i0 M$ `, f( g3 u' i - {; s% J1 d. Y3 T! k& I' u0 o! S
- printf("> Receive start...\r\n");
; c4 K6 |- f) i* U
. E' O& Q2 @* ~1 E/ B: g- Set_state(TO_RECEIVE_DATA);
9 d* K. b( ~7 M9 d; [+ L - data_state = 0x01; ( [3 O( m6 j. p1 c4 A
- send_command(ACK);( u5 B- w4 O- V- b- o. y# m4 N
- send_command(CCC);' X0 b3 z& i3 B [
+ x j7 U x' W! n$ |- /* 擦除App2 */ 9 R/ W7 V5 O, X! p$ M0 ^
- Erase_page(Application_2_Addr, 40);# V: g. x. q0 W7 o" g* J$ v
- }
) \5 W7 h# L: i+ w - else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
% l5 H2 a \5 g6 o1 H$ X - {
# V v" S: }6 J1 L6 P& r - printf("> Receive end...\r\n");
! R) [5 O! [, B7 O( n* E, J- H4 O - , E) U" l/ r! D3 Q
- Set_Update_Down(); , i" }8 @1 I# P% e" K
- Set_state(TO_START);$ w( c0 a/ x6 Z
- send_command(ACK);* m- {; B8 ]# i" F' r! R, U1 r
- HAL_NVIC_SystemReset();
' [% q$ _( a6 l. v/ x* H7 I - } , J/ P( ~. C8 ?. |2 |- `1 k: k5 C
- else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据
# |5 X* D$ x$ M c T: c - {7 y. E5 t X# Z) q
- printf("> Receive data bag:%d byte\r\n",data_state * 128);" I9 D" ?' U7 K) E* _0 t% x, H
- / H# X; J& z# O
- /* 烧录程序 */: L7 _, e# w( _ c9 W1 y4 d
- WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
& H4 H5 v( L' d - data_state++;
3 b7 Z- H, F4 V: m -
" L8 O) T, W/ H! B - send_command(ACK); * _0 @7 _: T) \4 X
- }
+ Z1 ^1 t% r! q# ?" q4 c - }# F8 B+ X* }: g
- else
( ?0 ^* B K; V5 ^- W9 \ - {
7 G. s `! G0 q4 Q# S) ~ - printf("> Notpass crc\r\n");
2 j% c; a7 t t: @) D9 _ - }0 k& X6 ?0 F! P4 e. P# C1 [
-
0 A/ m$ Y1 D5 d0 h# [1 ?- F$ R - }break;+ [ V4 }/ y6 N# T0 f1 s! h; ]8 X
- case EOT://数据包开始
, a9 L4 p/ E' T4 ]$ j" j2 x: \ - {# z" [) k& m2 Z/ T
- if(Get_state()==TO_RECEIVE_DATA)
% O( [$ g8 \" S" x5 y+ o) P - {
: [& ^$ H% M) H - printf("> Receive EOT1...\r\n");
' V7 W9 M6 |6 f5 K( c - 5 D6 x, L6 s9 s
- Set_state(TO_RECEIVE_EOT2); 2 u: @* n4 H+ m1 M; o
- send_command(NACK);8 W9 h! H+ N" |9 ^, ?) [! M3 {
- }
# e0 S) z9 n5 O6 y% e - else if(Get_state()==TO_RECEIVE_EOT2)( N1 G$ _- W( G! C( e% t
- {% K. j0 x# u1 v
- printf("> Receive EOT2...\r\n");, y) X: v6 W3 o8 {2 _
-
2 o' v1 a6 Y$ E; r! N0 U# [3 F - Set_state(TO_RECEIVE_END);
( K- b! w {* [! K0 B( |# e - send_command(ACK);
4 D. D& A; J8 \8 L' G - send_command(CCC);/ o' r0 h" k+ u, {; Z6 P/ l& M
- }
, n" y) r4 l* R. ?3 e - else# o# _( U5 p. z- G1 @
- {3 _& z- h- ^/ ?; \" s
- printf("> Receive EOT, But error...\r\n");
( l# W0 k2 o! w# C; z/ E5 @ - }& Q+ {+ k4 B" \% ], q, g; q, M
- }break; : Y9 z% ]+ z* @( \! T7 S
- }5 S3 ^3 _4 b, F1 e- Z |0 i' s% J
- }
. ^ c) m* U: c& h - }
复制代码 / C, D, Z$ D' G2 u' q
其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.& q/ X5 R. P' H% T8 b
$ r$ P* p: t) b! T4 j& c
6 p% m8 p' ~7 z6 R
4. 整体测试
5 B3 D8 {- N, P% ^3 G8 U6 t本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。6 U+ w1 d+ I9 O- k* ]
. E7 l& D* J5 W! n, J$ h4 V5 p源代码* F3 l! a" ^. `# U: C% k% l- G/ G
BootLoader源代码和App1源代码可以在原作者的gitee获取
, p) l# [/ M6 K. H5 s* b1 Z
1 S; q% `7 u) Z代码的下载( g' k. c6 ^) A2 d4 E+ S" x
由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
# L/ U& Y4 j9 \2 a. D
: @& R# q6 d+ n! @. C/ _ c7 F5 B+ C
1 C/ d5 y0 J1 U/ ]# w1 m J, [$ M 7 q$ y0 t# g: \/ ]0 j2 s5 v
, {. V- u/ A3 V& x- K: z
BootLoader的下载
1 G2 U: t) v* ]" `+ G6 c# dBootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
" |0 B. s( E! ]2 o B0 X2 B按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)! S+ ?3 w$ F6 f! Y
- B% F6 |) } F
5 v2 [# i9 P6 l5 a9 h. h! o: {
" ^. T T: u; U& B( |0 i& [- ~! {
' A. X% _7 y4 Z4 U* w# v/ G烧录代码9 ^! O+ q& B8 G- s+ i
运行, 通过串口1打印输出, 会看到以下打印消息
) r7 ]; B! W' O" ~. O说明BootLoader已经成功运行+ D& f, N: b0 k5 j. y
d& n9 @1 J4 W6 X, A
, v# {8 l/ X# _, r$ A* n S1 O: m
' o3 ]/ D! L$ f8 k1 \$ n" o7 D4 \* i& K. l- Q o
App1的下载
5 @$ @5 c5 O% F4 k! h" vApp1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
1 M) V8 a, Q" U' a同时也要修改擦除方式为Erase Sectors, 见下图
2 E4 A. n( u7 z) ^: W" D% P- N, ~; R; o. j6 I5 b; U. v; ?+ V1 P% X% z; Y
6 m- y) ^ ~6 g6 D) I! e
2 r9 k5 I* P' F+ c% h. G9 {) j. @8 [8 ^! C/ G2 O4 a) [" L
* H8 k8 P+ d8 h" d% x0 ~
5 i* ?. r" d2 G# e4 k. o1 @% C
9 r& r; I& b- G" ~+ @# z) G# \6 K烧录代码6 o/ m+ v; \$ y$ }% ]) x
运行, 通过串口1打印输出, 会看到以下打印消息
: C: `$ d) k' O- Q说明BootLoader已经成功跳转到版本号为0.0.1的App17 ?* t0 Y; M2 s! w+ U) V) E
- C- u! V' W$ z7 l" Q4 N& g. {$ N- Z8 |8 d9 G* u+ s
7 F% O) L! R- i C5 g# B) I生成App2的.bin文件
& B" C6 i8 D5 _$ G4 _- ~Keil生成.bin文件5 L7 e9 v/ u: O6 @/ b
( t2 ]- W+ C& [/ [$ j- M8 I修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
, a- J# D* [5 }& w1 r
$ y! l& I4 T2 t# |+ ~7 t. H生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件# N" r" J8 [& ?8 t
. S+ X: W7 z3 `/ E
* s$ d/ @) l4 d6 X& G0 F) c8 s0 C
* N# E/ `9 D; l/ c
0 f# `# L8 h" @4 p( ]% A# ]6 V使用Xshell进行文件传输1 Q0 Z! d) l( Z; u' {
打开Xshell0 d$ [# u4 b" P
代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
9 [& w/ g. ]' Q所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息5 y x$ v4 j2 w: _1 @; |
你会看到App的版本成功升级到0.0.2了.
. f/ Y y ^' r! c如果你到了这一步.
Z& |! P0 {8 i; H9 ^- T那么恭喜你! 你已经能够使用在线升级了!$ {4 q+ Y' C8 _ E& ]2 S
& D3 k0 ]6 ?* ~; _! h5. 总结3 A1 L/ ~4 M" ?" C' T
通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多., Q2 ^" _4 D9 X0 [7 m
8 {( `4 {/ E# m/ D3 h
转载自: IOT全栈技术
: I9 B D* E3 t) p) V' ?' }, z如有侵权请联系删除
) d6 L( D8 j: p" a$ q2 e" x, _, B" y; E* T
|