简介$ c' F. h C# ]% ~7 R( T
本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.
- E4 U; [/ m2 R( t4 h; r, w+ r/ H3 L1 i' R/ \5 g
3 [2 h1 s4 C; |3 {. y1. OTA基础知识3 h [- V: g$ J6 Y
什么是BootLoader?
. f& Q1 d8 q3 }BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的., z" Z$ e# O- O! _ O- F$ O7 b* H
8 I5 ~$ k0 e8 B2 V
STM32中的程序在哪儿?
$ @; e. `5 z, i: \! Q O3 P$ [- K正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码( ]* h6 u# x$ J) }( M. Y1 f; L
* l; f% D$ T8 ^5 D }: L5 h) g& c
' u, N* v, N) x1 p
接下来就可以进入正题了.2 z2 X6 o& \5 _# J o3 U' K. y( c
+ N- [2 c3 x4 D; a1 {, \进行分区- G3 y/ v1 k# L0 L0 U- S) p. \
既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:
" A$ L, Z& S( k! p4 A
Y1 K. {$ D3 e, p
0 A- L: s, e4 `9 z5 w( Y) r
) o# S& B N2 c0 Q# Y9 v# _& X/ \6 R4 |3 q
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:
& E" _" W) y3 mBootLoader区存放启动代码
) K) X3 p# ~" k" dApp1区存放应用代码. b7 ]: d9 z; y7 ]
App2区存放暂存的升级代码
2 ?8 Y. u) G) |, h% d6 H
4 @" i2 V- x9 }: |
7 y4 f5 U5 J/ m. M5 K; l) |1 T2 R6 h; |% ]- d0 u
总体流程图
6 S% A$ _, W- a u* H* f5 J先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.
+ t6 w' ~0 {4 h; R# ^然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
# R8 O6 V2 u+ L% R! C# q- |) H9 s) u0 x( D: c
在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:" r) A3 p7 n8 z; e2 [6 s
' U( Q$ g0 L B; {9 a0 w2 I/ p% I" `
/ V* _/ v6 Z( {" O/ ? 3 Z% z8 J- o# \ s
$ T& k- d$ f* g, U/ w
* S6 `( @: M1 e2 C
2. BootLoader的编写+ }. [0 _/ ]: o+ S! n$ A
本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。
- F4 W& @/ ]2 s% z7 [& ?' I
6 y8 {9 N% `, Q9 \' z& l! l流程图分析
! [+ |7 i/ q0 l. z以我例程的BootLoader为例:) O5 t8 G8 w1 T( W- S9 c
我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
' Z$ |2 Q' x$ t
$ S) X9 g/ R G) |2 z/ ~
- ?. L- r" c: b d. }" j3 u0 v" P, U* I' k: ^9 x# R
程序编写和分析5 O7 }: Q$ ~+ C' l3 V: N
所需STM32的资源有:. S6 ^5 V% W# D* h
发送USART数据和printf重定向3 j4 n4 f% D$ _* J
Flash的读写
, a+ f' O7 P' R: D程序跳转指令,可以参考如下代码:
, T+ K) B1 M+ q& u
2 a& P& e5 L, n; ~7 z6 X8 M( j
) |' ^: c2 s, k& |9 S' o- /* 采用汇编设置栈的值 */
" R% b9 P8 w$ v7 C( h - __asm void MSR_MSP (uint32_t ulAddr)! J: e! J8 Q2 N8 } u+ T
- {" d9 V, r& W0 F# L" _0 N
- MSR MSP, r0 //设置Main Stack的值( \2 {% E5 y5 t7 y/ i* k
- BX r14' q% u4 F( a6 `; T0 y9 R2 F
- }" w8 s' `' p+ j9 \5 U2 Y1 `
- ) p+ X" u, z1 Q, Y# n& i" q
- * s! ~, n1 n- r x% a0 [8 I
- /* 程序跳转函数 *// L0 Z* L$ ^& ]- `- S6 `! Z
- typedef void (*Jump_Fun)(void);6 k+ E9 Z: z1 u- u) p
- void IAP_ExecuteApp (uint32_t App_Addr)
. h4 X2 F: P, _; Q A - {; @% ~0 P# n& X( J4 ]( Z& b" ]
- Jump_Fun JumpToApp;, }( C; E: t0 x3 Y
- 3 y1 ~1 B q2 o' G2 }
- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.0 s: a5 z- O- B, Z
- {
2 G, s( B- [% w) U( G3 ~3 M - JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)$ G8 y# b6 x, W# ^
- MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)1 y/ G$ M W, V, ?( F& O- K
- JumpToApp(); //跳转到APP.
, w# [9 q% J2 V( M/ f- f - }
& U) z, m* c; Z. ?7 q - }
复制代码
. w- i* Q8 }: j; d$ W! v1 |- X在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);
9 E& `5 H2 p4 k其他的代码请参考BootLoader源代码$ ]9 D' K7 e6 ~6 f7 X/ d
2 ^& `0 c) I: A) E6 U: B5 X- e, S: i
3. APP的编写
( l; T4 w3 {1 O2 g( |. d j' _- U本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。
, d* j; o3 T _( Z
1 [# w4 Y' Y4 \) c' H
. \! x9 x: h! S流程图分析
Z3 f& N9 N# K8 H8 w以我例程的App1为例:
7 u8 y: C; f* K: n先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;
5 h( Y5 n* @& O打印版本信息, 方便查看不同的App版本;
/ D \' ~0 ?9 l- K. R' N# q/ Y本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
/ L' Z# s6 e) A% C, R' R9 n
$ {$ s+ W2 y. d+ Z2 W6 c/ I' ]9 a% C2 N: B1 A4 B
* N* {( o4 i7 w4 N/ ^程序编写和分析
" V. U3 {1 g8 |% `所需STM32的资源有:, W+ s* f; z4 U3 s
发送USART数据和printf重定向
2 x2 ]9 U; D) f" _" R8 F" A# |Flash的读写- E( L& f& ^$ G T. I) }" b
串口的DMA收发
, o8 Z3 s d/ L# Y2 LYModem协议相关
, X7 k# B8 Q, u* w. P# zYmodem协议& n/ g) Y( p1 ~5 Q
百度百科[Ymodem协议]& S+ g! W& S2 ^3 \1 i+ d2 K. ~
具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).4 s( o5 r& \" Q/ z& i. v
Ymodem协议相关介绍可参考我的这篇教程 YModem介绍
7 a1 ?% g8 t1 a$ Z+ Z) ?* M/ V! l) N: i5 P* n! q0 [
代码分析
; ^/ o9 V2 d$ |( h+ H1 X# [代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
3 E N" K! y& C }后面放了我的源代码, 详情请参考我的源代码.
1 B6 [: a4 ]; P# j! }主函数添加修改向量表的指令
9 Y. P( B! v( H7 V- y8 P) Y& @; [- U( n# |, I8 F% {( _
$ h) W9 H/ J0 S; ^
$ T; U; ]/ y4 L' S& H) n" I9 `1 m打印版本信息以及跳转指令
( ^2 ^1 v/ A4 d7 `) O6 m7 ^& v9 h! V2 j, d; p
8 _+ f: ^6 k r( t& }5 k8 C* i- }
( r6 n- ]6 L6 n2 ]4 K* W
YModem相关的文件接收部分1 x% ~! K. b" } {+ O
- ( ~- W. s. ~* k- X8 ? k
- /**1 j1 X. ^3 M; j7 o2 u
- * @bieaf YModem升级
5 d+ W8 U$ U f/ P: M& y# q! U" t6 Q - *2 n& a) t0 n+ T+ t$ {& w
- * @param none
1 ^" H; H0 W1 b$ v: X) O - * @return none
4 \! a. q1 x% ?9 O$ n( B; K - */
9 [5 V* j g3 k - void ymodem_fun(void) P" P$ K, |- W- W3 j
- {
7 X1 a+ a1 v+ s8 @# w/ N - int i;' {5 l: h7 y) S4 I2 B9 [
- if(Get_state()==TO_START)& n0 p4 [* Y- P8 N! w
- {
* q F7 J, B' n3 ]3 J - send_command(CCC);
" J, |* ]7 Y2 t3 u: q; i - HAL_Delay(1000);
+ ]7 l2 a" `& h - }6 M" U7 c2 K% p7 }
- if(Rx_Flag) // Receive flag
- x: L. j0 z9 o! T - {
; K3 t! d6 O& j+ l2 d8 v - Rx_Flag=0; // clean flag
6 ~9 ]5 V: s" j t5 ^1 |$ k: k! l' I -
) B. A: }$ L: d4 _3 C( j - /* 拷贝 */
* X; ^4 L; z) M! s, I - temp_len = Rx_Len;7 |5 g9 o/ w; L8 B& D2 |3 F2 d
- for(i = 0; i < temp_len; i++)0 q c/ d4 n& i* f6 I }
- {
! q H/ X7 z" y! q) C - temp_buf[i] = Rx_Buf[i];- Q( k. J$ j9 z6 \6 I, ?
- }: T& Z+ l# O8 v& |+ X
- ! u$ x5 h8 Z1 ]8 e
- switch(temp_buf[0])- l" @$ o; {+ Z/ T$ x
- {( p5 S& W0 e! h5 _7 w" d
- case SOH:///<数据包开始
) W4 A5 a5 v$ ]( d+ y( B - {
2 }& f. z/ E" B - static unsigned char data_state = 0;! S$ w4 D9 D* \
- static unsigned int app2_size = 0;$ e( M7 g# ~) p! w7 g& |4 c; l
- if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验# ^1 _" V1 z6 {+ ?1 b2 r
- { 7 t, Z4 K( O7 E3 @$ ]' }. C* W
- if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始8 u, E8 u: R" g, ?
- {
8 Z/ y8 y4 }8 r2 b( l4 b6 c* ~ - printf("> Receive start...\r\n");+ P, q4 Q% G: ]2 x
- y# |7 P* H+ G; K5 H* i/ U
- Set_state(TO_RECEIVE_DATA);6 {) w6 X# @: S8 f+ D
- data_state = 0x01;
% G- h" ]0 x3 u( {& M. F, `! m8 h" v - send_command(ACK);/ ^5 A0 ^3 Y ]: D
- send_command(CCC);& p$ q0 z; x& Z4 B+ Y/ }( \: T* D. P
- % z( {: J& R, H
- /* 擦除App2 */
! ^1 X9 [3 f# e2 [ - Erase_page(Application_2_Addr, 40);
1 t) A% f4 l8 a/ l7 Z - }
+ {0 O1 v: U( G: [ - else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束9 w" J0 _1 Q/ v& L3 T' E
- {; `/ T/ S/ l$ v X
- printf("> Receive end...\r\n");& M0 D/ B+ ]6 I4 Y
5 d1 d3 z% a5 _' W* U- Set_Update_Down();
- E+ N& t& x# S0 t+ X - Set_state(TO_START);$ f2 w- F) E; T" m5 J6 H1 s
- send_command(ACK);
' T4 a' d! C% O6 K1 f - HAL_NVIC_SystemReset();
* B, ~+ G0 G. t' l1 b* b+ [ - } - U& n5 n7 J1 }) e2 h8 T
- else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据
6 ^+ r( {2 e1 ?$ |$ G. P+ } - {4 m3 a+ @. G! I N; u- \
- printf("> Receive data bag:%d byte\r\n",data_state * 128);
. _: G9 W0 I3 e: \, l - 7 Y9 P4 _1 w* r8 h: |, |: f( r7 O
- /* 烧录程序 */
% g$ R9 m8 U, q) H0 Y, R - WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
& \* @2 l$ U( z% k - data_state++;/ n* v7 f9 [6 ~; `$ z
- 5 @4 Z6 k4 D/ \1 B
- send_command(ACK); 0 [ g5 h! |0 W( G+ S
- }
1 t9 i/ x. }- v& V3 _+ P - }; _/ A8 y& d$ R# J) k7 F4 \* s- G. p
- else2 `* h: N7 k8 |# R b
- {& p. s0 R5 D% k2 e: |% N7 x3 y
- printf("> Notpass crc\r\n");
) |4 g+ {" W4 `9 r) [& h - }
" h( C% s; ~; G8 ? -
$ ]1 ` F$ T+ z; M' W, w2 a - }break;7 u: ~0 o; l. C& s9 Q" b" v6 U/ K
- case EOT://数据包开始; W4 E0 E1 {5 @+ ^) f7 z
- {) t8 ?1 b0 {0 J' P" T6 N) ]- ]
- if(Get_state()==TO_RECEIVE_DATA): T" L+ o5 k& K4 ^: \
- {
9 g. W3 \) B4 u9 p - printf("> Receive EOT1...\r\n");9 P! Z. D$ U* X
- + z* N N2 u1 w) D, ~
- Set_state(TO_RECEIVE_EOT2); ( K! _5 Y" C! Z. t N
- send_command(NACK);
" N* O: _0 ^& s, s2 [, D. X! p - }4 E6 @( C, Q3 W8 c
- else if(Get_state()==TO_RECEIVE_EOT2)
: z1 X+ }, T4 ^ - {5 ~& e- r* R9 z% a6 r* G
- printf("> Receive EOT2...\r\n");5 [8 S( Z+ I4 R5 g8 h/ C
- 6 C2 y, q1 [* `. |3 O
- Set_state(TO_RECEIVE_END);
* ~, C" T) A) s# x- } - send_command(ACK);
8 o- m" v, C2 L4 Y7 ~# f, R8 I - send_command(CCC);1 _+ S+ k; h) v/ a
- }$ L4 z+ \1 Y9 P" t
- else- i) ?' }: K- s) E+ p, y* o
- {
: B5 l: z4 e5 T - printf("> Receive EOT, But error...\r\n");
+ ^+ j, u c/ x0 N; ~) K - }
, _( S4 J7 e3 \ - }break; 4 t5 q3 Y0 {1 b5 I1 |
- }
2 T7 p, l3 z% U/ m; G# B, F4 q - }: }/ r1 [0 t0 {* b! k
- }
复制代码 8 ]) i! p# w& u/ y/ h
其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.
7 V* G/ n4 o# j( K. G/ W6 u+ F% L" {: I) C* S) s* d5 h6 [/ E
1 P2 q* |3 R: p- ]. B6 W
4. 整体测试0 y$ r& o3 A4 { l
本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。3 G1 I2 v' a8 b7 I$ \+ g8 j
- f; I) e$ q: z7 _1 [源代码0 g5 c) P* K7 M- x
BootLoader源代码和App1源代码可以在原作者的gitee获取3 M/ R; ?9 u8 ^% k/ u; t2 Y0 J
9 l1 @ V) p4 N, X代码的下载3 U: T. I! V/ Q1 w, v# v
由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
$ p4 X3 \4 O! S
" F7 O; A' C/ i$ J9 D
' [7 r, U) h% y) O; u! x' a& I
" ? S( s9 L- d, ~2 e
- q& T5 q! ^$ s5 MBootLoader的下载& ^0 N2 h' U0 V
BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
- Y' j) ^8 [! b0 E. }5 D按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)! d' Y) f% e' u1 m4 U* O
1 E/ H2 b6 N+ ~6 b1 M
( o& {) r, r4 O8 Z5 ~
) a" t9 E- O- F% \
5 B" b2 U R" o- @# S' T烧录代码
3 u# z" E$ a4 a$ A4 B) f- t运行, 通过串口1打印输出, 会看到以下打印消息
0 |1 ?. g P6 I$ x! K0 D+ W说明BootLoader已经成功运行
" @" b- S# ?7 ?/ |1 e: j( Y1 f% Y; m Z% C; V
7 V! ?* C% U4 p$ a7 [( c" r3 V
! L* H/ f3 r8 q
5 v; K1 S9 x" y2 |7 LApp1的下载
M# P/ j6 T4 ]4 L/ X: I) mApp1稍微复杂一点, 需要将代码的起始位置设置为0x08005000" M1 Z4 d* H f$ H4 E- }
同时也要修改擦除方式为Erase Sectors, 见下图
. O# e) K i5 Y# K( K7 ~0 F0 I# f" G! R8 P- X8 U
1 [3 E2 T7 t, X* l# ^
$ |* N5 ?- A' ]0 Q$ r- K$ n1 z
1 l, t: |5 D$ w4 F( ^- f/ A3 [3 ^
+ B5 Q' i1 v# N- g: Y
x% Q* K( C5 t
( s, C+ [; I* M
烧录代码% E! Y/ j( {! @2 x
运行, 通过串口1打印输出, 会看到以下打印消息
& B9 F, h. H. e( @5 ^" B0 Y说明BootLoader已经成功跳转到版本号为0.0.1的App16 m+ \7 e& I4 z& N, P" f
0 B6 S" p, t* @2 P/ c5 T& M7 N9 S' s3 w. |& {( p
6 N: x# s; Z3 d生成App2的.bin文件% F: G3 ]) |# u; X
Keil生成.bin文件
1 V$ \1 _! F3 ?4 H
2 i. b4 V, `( i修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
" a- F; D- ]; K+ c( i$ p3 z& K3 `: h2 H* H
生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件
/ S% N9 q' J K3 {6 y6 h- s4 A
. f5 ?) U/ a" K" F+ ]# E% r. y$ J! h
1 X, I, [1 [" {' B( `4 B' C
使用Xshell进行文件传输
7 N/ C/ A# c5 [打开Xshell7 l: Z& E# A& b4 `4 T: n: F
代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的4 h$ p% n) t7 r# z! H# q
所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
4 b; R+ K- D) s( G7 E4 `你会看到App的版本成功升级到0.0.2了.7 X' }0 k! Y9 E# y3 K0 C& O
如果你到了这一步.% w9 B; S6 m7 H0 u0 Z" k( i! t) j
那么恭喜你! 你已经能够使用在线升级了!: Q4 f# c: [3 C8 r
& u! i" O* \4 y7 ^7 [
5. 总结
1 w2 g# Q; L& {* N. X通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.
5 T4 o M7 H% H" M3 T6 ~9 Y. ~: ]% k0 ^; P2 k
转载自: IOT全栈技术
, g7 n/ S- Z& V' N! W& K' ~/ H* d如有侵权请联系删除
9 w+ ^) B$ U5 h. S& D: }* v$ Y! O8 k" ]! q
|
你好,有程序源码或者链接吗