简介
6 Q( p. W. O( V \. Q' A2 T3 ^本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.3 t7 n* p7 L5 b3 C# C" y
* ^- R* C L6 f" c0 h% a W8 F1 q2 N- e* Y& \$ i
1. OTA基础知识
5 C% O9 M0 x8 F5 {7 N! V! G% n! u& y7 m# W什么是BootLoader?
+ i+ [2 o/ V( D* m {BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.2 v5 ]0 Q! d: `8 r. r7 f) H0 k
# d: H v, e' C* s7 ?# rSTM32中的程序在哪儿?1 v4 G" ~: H* V4 A1 @5 Z
正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码
# U3 a7 k# Q5 `6 @4 a' I3 ~, A" |
% }3 t/ H5 j/ ?4 K0 U
; q' f$ f' ~3 ]# C
9 M" y) ~- o8 L" a4 N接下来就可以进入正题了.
) |4 l! d5 i% S- G' C! e3 g0 g
9 h4 d& k: r/ f3 a5 _7 V进行分区
3 b ~- M, L5 P既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:# f$ ]7 Z; |4 w& I2 D
( [# i1 s, o/ ]0 h5 E$ `
; h+ T7 }' P( e+ ~6 \& S9 |3 |* P- G* [
2 x( }0 L1 j2 @ p, s+ K
5 c+ F( B: [3 d9 n以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:
0 i h- }- D* S3 T6 SBootLoader区存放启动代码
4 V- d! `" X/ x2 Y/ IApp1区存放应用代码
* ~" c& m d, E$ T$ Z3 ^App2区存放暂存的升级代码
; P, j& p6 x: T( g( `# r1 c, J# s \" c7 W
# X/ O2 X1 O* B }+ d7 N% y/ e, s+ L
7 a, S/ O+ l9 l ]* t( |总体流程图
- a K* J, |1 v- p+ `先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.
2 q6 X4 |& X5 H7 }9 J然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.7 e) c. C& F2 Y I
7 ]7 i, s! P" F+ p1 s! r4 G+ B
在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:* o! ?& E$ d, V# c
) ?* r! I" s8 q2 E7 y) K9 _
- H. q8 M0 v2 t ; `/ I* J4 Y& P+ L$ A9 X/ ~2 C
; R% E9 g, K1 s8 @9 J0 {9 b% c& u7 \; E1 R+ o7 ]# @5 b
2. BootLoader的编写( a5 S. J# F6 [( T! y$ ?
本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。- M( h; P! T/ _! `% u
, a5 W3 b" }# ^% m
流程图分析- @4 i6 o7 R+ ]! Q2 W+ A5 T K B
以我例程的BootLoader为例:
$ X4 h$ J5 w/ g" q, C9 u* i我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
' b6 h6 ^% a& n A9 W1 m
, g% X- j) d7 \
5 \# w7 V! y _1 |: B8 J G. n( K2 q T4 \7 d. [2 ?
程序编写和分析; _! Q+ H$ B& R6 a/ x
所需STM32的资源有:' J# A1 L, u+ A9 D3 H6 I+ \- U
发送USART数据和printf重定向2 p, W Z/ c! z, m
Flash的读写
# m% s5 m0 ]! {, [+ V程序跳转指令,可以参考如下代码:( w; f; e: h; g4 [+ F9 q
; v7 a9 Z. \: s0 Y Z' P
$ _+ p3 c! f0 A. Z6 o- /* 采用汇编设置栈的值 */! W- t8 Q7 a, N2 n2 W( h$ I
- __asm void MSR_MSP (uint32_t ulAddr)
1 i8 J3 K2 T. L7 I3 P( I& T L - {8 G4 {0 r1 W% c0 ~- v# f
- MSR MSP, r0 //设置Main Stack的值
5 c7 T# O$ O7 J9 P - BX r14
" I7 f a1 I4 j5 p - }
5 m& L+ a% {$ G: R- a7 m - 2 {7 M; j) m2 u" [
; e0 j. M0 W' e6 W- /* 程序跳转函数 */
9 S2 V1 {7 }1 d9 [$ l2 E* q! A2 V - typedef void (*Jump_Fun)(void);
7 g& H) K5 u7 I) b& H3 C, d" L$ q/ M - void IAP_ExecuteApp (uint32_t App_Addr)
% ~3 {4 @% |5 w2 I. j, ]. x& x! P0 Y# E - {2 L3 g. |4 w" v% {* p2 d$ M
- Jump_Fun JumpToApp;
/ j; ~, j3 M) N3 k3 V) S
/ h# x& C: p! E8 q& A, {+ s P: J- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.3 k9 C- \; L4 v% f0 P6 [9 g2 l
- {8 t' j% |& G- W B# x! M2 {. r
- JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址): a! t5 {! @% h x
- MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)6 C7 ]8 Z; G0 R0 _: }; h! n7 u
- JumpToApp(); //跳转到APP.
0 b6 s5 g% {3 c9 W - }
; ?/ n O5 h+ |/ ~0 s3 A/ s h - }
复制代码 , R7 K4 w \1 `
在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);
+ P0 V& U9 U% W其他的代码请参考BootLoader源代码2 f- M: P- e: |
Z# c& }% I2 d1 [! v
3. APP的编写& k9 v1 t* t2 {% k$ [5 \
本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。3 Q2 o7 E6 l8 X) D- G3 o; o
# T: j$ g* q/ u0 X4 d7 o `1 U7 C% u! ^' \' V" w& q
流程图分析* }( o5 P# a$ @3 [' `7 ]" T$ q
以我例程的App1为例:0 R/ A( c1 i& _4 [2 ?2 M/ `% Q" s
先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;
# ^" a H9 \& ^( D4 }打印版本信息, 方便查看不同的App版本;$ k& a( V: U' H
本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:2 I' v* \' L+ O, ]: i
2 A- d9 l" `/ H; c" J/ o+ K; u
) u$ x7 l5 o* d, I8 _8 h+ L+ L- A; Y- U( |/ J
程序编写和分析! ^ I! u4 G, `. U& i7 E, S- ?, ^
所需STM32的资源有:! ^* ]$ b; j c Y( E
发送USART数据和printf重定向
9 \. Y0 _' d3 K3 aFlash的读写
6 W# s7 }& B) N6 b) u+ f5 i% ~, Q串口的DMA收发1 ?' p# V* C+ t, A3 K) s# J9 K2 c
YModem协议相关. G8 e) l& O; [9 ? R. D& b
Ymodem协议2 }: Q+ A6 g. v6 b
百度百科[Ymodem协议]$ u* b& m7 K9 l o: J
具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).* A& n; B/ `! ~/ _3 ]
Ymodem协议相关介绍可参考我的这篇教程 YModem介绍
. r: ?0 L3 o) E# C( G# |1 V+ k+ a' o9 o% R% L
代码分析
; E3 T M& [( h8 A& x2 i代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明8 Q) U( j" E- @2 T
后面放了我的源代码, 详情请参考我的源代码.
9 J$ y. }! o& D主函数添加修改向量表的指令& \# t* b+ B7 `& Z0 c
' m$ c G5 m. J
1 f3 q6 l$ R/ i4 h( ?6 a: c. r/ |
- ?" R+ z. ]5 _0 u打印版本信息以及跳转指令 p1 |. Q5 @9 Y4 D1 `
( u8 Z; k. ~6 N9 w3 l
' c( F. x5 t7 U; U2 A- _: e( q% ^) d+ B% b+ ^
YModem相关的文件接收部分
' T" R2 i! D" s, n. h! s% S8 K
$ n4 r R: f, D- /**
! w4 \+ {; v d9 [- x% T6 G8 f - * @bieaf YModem升级
6 H( L9 I5 S' i: @$ h - *" }, Q/ d1 a7 c9 l3 U* ~
- * @param none& ?0 n1 U1 Y( N/ y! w* o! F: w
- * @return none
3 i* B5 B+ ~; M7 I5 Q2 [ - */
; v r6 u+ b- q- B, f - void ymodem_fun(void)
, p5 T1 a# a2 }) W3 \ - {
; B- _1 n! y5 C' T% v7 [! b( Z' v; b - int i;3 t5 I6 y: g L8 b3 ~' M, _
- if(Get_state()==TO_START)# F2 q" i9 _: x6 s8 S* i$ p
- {
/ y p9 U. S4 [8 I8 V. l# {3 W2 A& } - send_command(CCC);
8 d# _$ Z% I8 M9 ~ @ - HAL_Delay(1000);: |; X4 u/ q2 W% v, ?# L. U3 B
- }
7 O% W4 L& K) c - if(Rx_Flag) // Receive flag
" E, E6 _' @% q/ E* N% Z+ V - {- C: C" l3 G2 m% W
- Rx_Flag=0; // clean flag
: L. m8 T+ n3 R! l -
# A4 I% u) V/ t - /* 拷贝 */( l4 k, l8 J/ K
- temp_len = Rx_Len;
7 ^9 d3 h* ^3 _6 ?! y - for(i = 0; i < temp_len; i++)7 P( S5 s+ ]/ l) k* A" \
- {: r, Z/ V, u- S; v# S0 o) H7 x
- temp_buf[i] = Rx_Buf[i];
% l2 D9 `8 ~! E. q8 N: {' Q - }* m: o1 ?+ e j! ^$ v0 \- A# U
- 0 Y0 c2 j5 U3 _0 ?7 \
- switch(temp_buf[0])
. G# _0 o3 K/ A4 M - {
1 U* _7 |% O O9 l$ \ - case SOH:///<数据包开始
) D4 g6 s5 w3 ]% v+ h - {; O- I [; b9 G
- static unsigned char data_state = 0;1 k/ I; y) M4 h8 J1 h) P" E. S
- static unsigned int app2_size = 0;
) ]/ v6 H) D7 }7 } - if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验
* O5 M1 t! @! j- ^5 `5 k* O - {
, X1 C$ } z0 ^ R$ e* q - if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始+ v- F, `% d& X" H4 y6 M
- {
% N' m1 L s& }# R* V& j) W/ N& P - printf("> Receive start...\r\n");
8 e/ B8 `8 L* [ - $ O3 \: }- @ }9 Z4 T4 y: X
- Set_state(TO_RECEIVE_DATA);
& T% k0 C" B, A8 m4 h! b0 Q - data_state = 0x01; # k, J, s% R, |+ f7 t9 i+ v* c( ^
- send_command(ACK);/ e) @; H2 I' p" \, N, n
- send_command(CCC);" H: u. j' S3 m! C/ m1 [
- 2 u: l+ w* v5 i8 z6 ^3 S' h( d! q
- /* 擦除App2 */
4 ^. F/ f# J1 m3 a' i - Erase_page(Application_2_Addr, 40); K1 Q& _4 r$ z6 |; w
- }
, h5 Z; o$ w: D' Z - else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
* i4 L. j; P: E7 _9 N: k* a2 E - {0 A( M; k2 G+ O, b+ u4 c6 z
- printf("> Receive end...\r\n");
8 y& E7 D$ J% h8 T0 a
; U0 e4 r+ S) H( M3 I- Set_Update_Down();
/ u! @5 M* w6 Q - Set_state(TO_START);
) L. e8 ^# n4 `( ^* j - send_command(ACK);" w) m: [/ }2 }( D0 B
- HAL_NVIC_SystemReset();
. G8 R- t) Z: p: }3 k - } 6 ~& J+ R: s' y8 R! P: x% D8 O
- else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据+ m/ {" b% I; i5 q
- {
, r& Q" w3 a `, j0 |# i* O- E6 n - printf("> Receive data bag:%d byte\r\n",data_state * 128);
$ ?- k3 B! i6 E( B -
: z' K9 ?4 K3 o3 P7 v. p - /* 烧录程序 */- p& G S" K, `3 _' s$ j1 F0 M- n
- WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
$ O! K) ]) y/ P% T - data_state++;
% K* [9 I" m# e, z9 E. x - 2 i) `8 Z* {8 o2 e5 E
- send_command(ACK); $ j% J: S' k) i5 E9 ?" k( H/ B, k5 t
- }
5 ]* X. A( U* C8 o6 \( p: j! {; ? - }% _- r# {2 k+ S
- else
& A5 ?) n0 k. C' w9 p4 ` - {
4 T& l' J( s+ j9 J. h( ^( _ - printf("> Notpass crc\r\n");% q4 J) R' V: R& A7 l+ G( W
- }9 g0 O; s t" P3 J- B/ {
- ) \$ B8 w2 A% X3 S8 R; l" @
- }break;2 B' ~- e0 H6 D+ k+ P, ]
- case EOT://数据包开始
+ L2 T) p2 W0 ~1 D; ?* t% Y - {8 n5 U3 G) f) M, p6 v e# m
- if(Get_state()==TO_RECEIVE_DATA)2 X1 Y" E& g" m: k: b0 B
- {9 c- z J3 K4 F1 V
- printf("> Receive EOT1...\r\n");
7 @( y, I: u) r2 H, e* \" e+ s -
2 w& A& V- @0 k/ C( h* S - Set_state(TO_RECEIVE_EOT2); 7 }1 m, A. k" X$ P% _2 a
- send_command(NACK);
* u) x9 d" U4 o, f - }
2 p7 T" n( ]0 R' |$ l/ y) n* V1 r - else if(Get_state()==TO_RECEIVE_EOT2)9 f: v, V! p; [2 p( \& e- f9 P
- {
8 I y& X* Q3 s' F' B) H8 Y - printf("> Receive EOT2...\r\n");( g/ [" H6 N* }' k' l$ e) n
- 7 `) F+ M2 G7 c( _& L$ J) B% f+ ?
- Set_state(TO_RECEIVE_END); " t1 @6 |) ^( t L. W3 @
- send_command(ACK);$ e0 i" B' T6 N$ e% @
- send_command(CCC);
2 ?" k& u& H( {8 c - }
3 w% D, p( o8 Z! J - else
1 `+ s5 b3 ?! g6 |8 j3 `2 t - {
) l/ t7 L G, }* T2 v4 y - printf("> Receive EOT, But error...\r\n");: n. ?! z, u/ m
- }
5 i/ v! ?0 F7 k; ~& [( ? - }break;
1 [& M/ |+ }. L6 n: [ - }1 `6 f) W0 l- s1 g7 y! E
- }; S) l6 s! g" O, V
- }
复制代码 7 m% }1 z3 D0 l$ A# g2 v
其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.4 \' i) j1 ~5 ~5 U) e
) m: @1 o8 b- M. \2 |
/ q3 o9 A! Q% J4. 整体测试
- @: V% j; G* p _, G; n: t本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。
4 A1 {: ?( |# M4 T0 G5 c4 c( a
( N+ I- V( K) s* p4 F源代码5 l9 r; ~$ H% s$ ?
BootLoader源代码和App1源代码可以在原作者的gitee获取
. p( P! l) P' c
9 t, J! r8 Q0 R0 X/ J- x代码的下载
! w. D/ \/ F; A% U9 q9 j7 _由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。 M% h/ d+ K# w; K; _# |
$ t5 u) u- ~9 b) s) n7 v/ k
& y' L: z# X6 }( k4 {! _& { 3 v7 g4 B+ r; S+ q8 w
) f+ X6 C7 l$ a2 {, X
BootLoader的下载# K. A( n* }( B7 [/ \& E4 L
BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置* c1 P" V1 x7 r; y$ l( k) u- s! p
按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
0 [( y& Z H# e2 \" n7 @3 T; P( ]8 t5 g
# c* b& `, i8 t% k% n! y! B( d
0 v" T1 H- `: m, o* z' J
# y) }; T A. K0 W+ \' y烧录代码
3 e" v( t, M# [8 t: ]+ T运行, 通过串口1打印输出, 会看到以下打印消息
/ t, |3 g5 p0 r; I% {7 h. w说明BootLoader已经成功运行
3 q3 Y* n- ]: _0 J( a3 ]# r+ i) r6 b- h# N# O
8 R: {% C& G1 K& q8 C% Y 3 c5 O! G+ Y; _/ j! V
) y. P8 O$ B; |4 L0 d+ CApp1的下载 q0 o* Y; Z3 `1 [& a
App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
% i4 Z# M' `1 @: X/ d( \, @同时也要修改擦除方式为Erase Sectors, 见下图( |$ ~3 Q/ ]# G
4 w4 {; D4 i! p- D
# b8 c, Y; v8 F0 ^3 \) O' _ 2 g3 |, j+ c3 ~, k* G
9 z5 ?) `, a8 ^4 R) a3 H( n7 ?) |
8 W, @0 Z. c' ?- F: {
7 E/ c' p, O& d: J
$ _, |" A# q% |! k/ q; ?7 e4 V5 M烧录代码
) ^: A2 ^! j B* x8 H运行, 通过串口1打印输出, 会看到以下打印消息
9 v2 _+ r5 z$ x+ M* G说明BootLoader已经成功跳转到版本号为0.0.1的App1
8 x% i2 F! V1 h% g# @
+ p+ z: o' @4 ` i* N
* {5 R) y5 T3 h7 I
3 i2 Y( o0 I3 x' ]+ g2 `; i生成App2的.bin文件 a6 l' |' o7 v% p: f
Keil生成.bin文件
' p4 b* V" S, i1 V8 h
& J- v4 \4 x/ C修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
# S. H( x5 O! h# e. \7 T/ p4 Q' K+ |3 T/ m5 Y
生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件( a9 W* N( p) ^: s
& j: x- S2 q4 V. H3 y
" [5 u. E' w6 C# l" y' y! Q I/ R# M% a& i/ N3 D! \* ^9 O& R' \
$ g, P- V: c! L2 N使用Xshell进行文件传输' R+ N) z, r9 S: j/ ]. W
打开Xshell8 _# I. g7 F. y) W1 X1 ]7 B
代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的9 h6 L0 \+ i2 }5 P( Y- |
所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息2 l# g3 N' k$ @" s9 v i8 m5 U
你会看到App的版本成功升级到0.0.2了.
: ?9 w: N$ V* {6 |如果你到了这一步.- p$ n$ Y6 p' ]3 c! q
那么恭喜你! 你已经能够使用在线升级了!& R2 x- I7 _* [3 v* F3 i
" S0 F8 h1 G1 |7 C) K5 a5. 总结4 g2 q5 {; l/ V! b4 p
通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.
9 Y7 J- g2 `5 J& y* G9 f) ?! R7 x& ^: c
转载自: IOT全栈技术
9 D |' v) H; [! F" E& S; Y8 P如有侵权请联系删除
* j6 V9 |" x5 [4 z# H
, I$ N0 C2 x5 K9 T3 a! l |