简介8 N3 q0 Q% c _" O, r
本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.
8 Q& ~1 d4 P! q+ k* {: n3 N6 g# _+ `- Z3 ]
* G1 U; c$ k# p9 f2 a9 e1. OTA基础知识
6 A6 L( a1 Z+ i1 G; v- l) b+ |什么是BootLoader?
0 `, V9 e2 N6 t3 LBootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.
3 B$ {1 B- ]! p" b$ W! V
: ?* r5 U+ p. C: R) H$ hSTM32中的程序在哪儿?
0 d( `! w2 R6 n正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码
8 y; S& i/ ^' J
$ r: P! u5 N8 ]# j/ x) Y7 E, J' {9 M+ }" K* b0 Q
" u, D- B0 q0 q- i2 i
接下来就可以进入正题了.) B: [ {8 `% G5 R& q* j9 D7 r: m
$ ^8 q+ x t, E" Z1 N6 Z, l进行分区
% M9 ~ G5 _0 F既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:
. Q+ S: h8 q. R" h
7 e2 r' ?) `* Y1 s5 }9 [8 ? I5 H( V2 p4 f6 v9 [( u
7 X% z4 h; f, w* y9 @0 p( @2 \/ T1 ^; _
8 m. l( B6 {& N3 h" u' G3 u
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:
) ^; |; d" K9 X" ]+ u0 Q- iBootLoader区存放启动代码3 T0 Q" B- {' K) S% c5 c3 O' n1 I
App1区存放应用代码* H# N* X& a5 M" A L
App2区存放暂存的升级代码- T/ i) Z' R# h- ~! X* a, U: A! A
2 o* T' l. P2 [* {3 F. g, Q0 ]
+ P8 i. E' j! d. B4 c, K. s+ v. x( v+ T9 ]4 a6 l. N
总体流程图
# b' S0 n5 f1 `* N- i先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.
5 i7 r, d9 L- M( D9 Z/ i然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.6 x& V* I. U) q1 }: m# t
5 P7 s0 e8 a0 _% i. [在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:! T* H5 s& j( F- |* s1 B
' C# f* E1 w: n5 m4 v
% x" A4 n7 }" W. D
) S5 b: s; v3 l1 n
0 { m: O& j, x, m! x: U0 Z; s4 U# ~% F8 p
2. BootLoader的编写
$ e# c! E2 N" \& I; j* R" D本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。' [5 ^! l. e5 T$ }$ p+ k5 C$ `1 Q# |, q
1 o$ n; {- ~7 _2 `+ g3 D0 [* D/ T, Z流程图分析" |: O, P4 I( d
以我例程的BootLoader为例:
7 ?4 A2 r% [( c% H( [我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示, Y2 i7 Y* x$ I9 {! E7 l- C
" q8 ?* V2 R- v2 N9 _* z9 V
5 z- T8 @- ~) s# z7 F$ u
8 H! i5 ]# e5 O" ?, r# f程序编写和分析' ?1 Q8 u8 A* q8 E5 V$ h! H- ?
所需STM32的资源有:
% G& E& {/ C' A3 W& v; a. F发送USART数据和printf重定向2 N+ R) l- y4 {
Flash的读写
( H" i6 ~! o6 |4 i+ W9 b7 h程序跳转指令,可以参考如下代码:; I" ]9 h: {, A8 ]' W( }
Z6 z; \1 Y7 G: t" J
, U0 e; C% E7 D6 { ^& i# C! l- /* 采用汇编设置栈的值 */
# z/ |" K+ a. S, { - __asm void MSR_MSP (uint32_t ulAddr) f$ ]5 G1 m# a8 U
- {
1 n @; q3 j+ M' J" H( C - MSR MSP, r0 //设置Main Stack的值
0 ?, d/ c/ z5 f" Y# R - BX r14$ o9 J* A2 k# O7 V/ C/ T' h: j4 ?
- }
/ ~, D, c5 P |7 w X - & |/ \6 y1 ~2 K$ I8 p
- , a$ D5 t! l" I# c4 l( w0 |) j
- /* 程序跳转函数 */! y3 X3 ?$ S& S" `0 q0 Y8 p, B
- typedef void (*Jump_Fun)(void);5 V9 C" ~8 ^% b- J
- void IAP_ExecuteApp (uint32_t App_Addr)
5 i1 B4 i9 H2 B: @0 A& `1 `) s - {
) p: D7 W# ]+ g& X$ |* ~7 m - Jump_Fun JumpToApp;, k; i# K+ k a
- + ^: l* e& }8 q/ a" ^
- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
5 o$ {" J/ Z `) ` - {6 E+ h5 x6 |2 w! {" Y; ]8 W
- JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
" a2 L) }: [+ @0 N E - MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
+ Y: n( ]) Z8 a3 @1 D$ f Y- `7 i% B - JumpToApp(); //跳转到APP.
- a0 V4 T# z6 f& {6 p9 Y8 U, ~ - }
5 |3 r$ [4 A- {. w: ^2 I5 } - }
复制代码
3 D& e$ x* V5 ^4 `' w; I* m在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);+ Y1 X1 b- M, Q
其他的代码请参考BootLoader源代码
% }& S- n: v* ?1 a
6 f1 ?& {; C+ t% A; p3. APP的编写5 R& S8 o- s( _3 x/ f
本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。7 v; U! r5 N4 z% O; r5 w
" w1 B& ^6 X& ]8 z9 A; }$ b( Q
! A* a/ ]- U: R w) [5 R流程图分析
9 Z# A# w2 I: ]5 ]以我例程的App1为例:4 @" j3 U* x! C0 \2 u2 x" c+ F) `
先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;) C6 ^, x/ F2 p* a
打印版本信息, 方便查看不同的App版本;
8 b6 w& D1 t4 \8 |本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:( k: P" k2 |) ~
0 V+ ]& {$ g2 A3 \1 ]* s" ]& H
5 v# W* R; |7 F" O* s0 j; J7 D5 L' \
程序编写和分析# G% z* E1 ]# c
所需STM32的资源有:
0 {* y7 o# E2 F$ q发送USART数据和printf重定向
f& v* T3 R' j- w0 ?Flash的读写
Y7 N6 k# u- @5 i串口的DMA收发: ~& d, u( a- d1 ~$ ^6 L
YModem协议相关. k' N+ C4 f- B3 ^. i
Ymodem协议3 l& X7 S- e% L, A- C
百度百科[Ymodem协议]8 t+ M+ b1 ]9 Z# v" L; c
具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).: }! U/ u0 ~7 v- U4 |* G! m
Ymodem协议相关介绍可参考我的这篇教程 YModem介绍3 R' B# R# _! B7 v
6 l" k; H: g& V( ^. ~. n5 w( Z代码分析
5 A& C& A2 Y& ^& i# K) v! k0 S1 f代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明- G) Z, u3 @1 v1 @! M, E
后面放了我的源代码, 详情请参考我的源代码.3 R& `8 n/ I$ O, {8 G2 s' T
主函数添加修改向量表的指令
' \3 g( s9 F) ]' v% A# _0 V* n$ `; x: @1 I9 e: _* H! _
k) [7 Q9 _$ G0 k
: W* G, i9 h) D" E6 q打印版本信息以及跳转指令
) \' H- I" B! q c l4 {, Z$ J" ~& |. }! q% ?( H9 J6 @
9 g- O+ N" R( ~( L6 o# U; _
8 V( k: Q ^# f: K: B; AYModem相关的文件接收部分
! R2 t9 U* ]* l; @! u% {
% x8 u1 \8 }: L7 E* \! a. W7 b- /**
" A8 h1 S- ^. o/ q( {/ ^: g - * @bieaf YModem升级; v9 K& d, N8 p; I. d
- *
3 s3 E/ G$ @2 I3 u$ ^8 | - * @param none% Z8 t: t$ @! M$ _% s' H3 A8 w9 V/ P( g
- * @return none$ t" v6 n H3 E' R
- */
2 ]# ~" s' s" d' M. I( |9 P - void ymodem_fun(void)7 o3 `" k! [5 N
- {
# ]2 C$ U+ Q( e) }( y4 k9 J% o - int i;( Q0 Z& H+ y# x( r( ^
- if(Get_state()==TO_START)
( `8 x" A; ]" D7 U - {
( x, L7 x- y* u; p. [ - send_command(CCC);
. Q3 r% S i* U8 h - HAL_Delay(1000);
: b: R- F' [4 G6 Z0 C) ~/ ^9 d - }: x9 L2 W$ x& e z4 q. O
- if(Rx_Flag) // Receive flag$ J8 f2 i& j, s
- {, k {/ U: e" F9 i) Q/ k& C
- Rx_Flag=0; // clean flag
. s+ D) r/ j0 N: \1 H -
! l+ c$ P' ]2 ]' V, X; H7 S+ o - /* 拷贝 */9 n m3 `5 P* \8 j# P' M
- temp_len = Rx_Len;
+ m8 p0 K' m; J+ X: t& G - for(i = 0; i < temp_len; i++)7 r( _2 z) V+ s5 j
- {6 R: Y" b s! _. B
- temp_buf[i] = Rx_Buf[i];
) X' G/ ~3 t) [ - }9 z' h N; k$ A- Q7 {5 I0 Q; U+ E
- $ h3 ~$ a; R' A9 W( T/ ~* m% _( k8 u
- switch(temp_buf[0])- L$ R3 M. ]7 R( Y
- {
" v, V& y( d; Q+ X# ]. E; P - case SOH:///<数据包开始0 }% R7 W* ]' ^
- {
4 o" K; l9 t8 q2 l - static unsigned char data_state = 0;
, v+ f+ P9 m9 }. R/ |9 v9 m - static unsigned int app2_size = 0;# _; z" R' Y$ w) z& k
- if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验
( I7 w8 v; q2 Z" b& K4 f7 i - { 6 i+ H% w& I. O: A
- if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始2 q3 g4 i' Q3 m
- {
! o- ?2 K, d$ T# e+ a a7 V1 O - printf("> Receive start...\r\n");
0 ~: Q V" S# o) s: m _ - 5 \2 O% A; d# _7 z& `1 _
- Set_state(TO_RECEIVE_DATA);4 _; U9 T/ h+ G* H
- data_state = 0x01;
) J+ @0 Z7 D4 M g - send_command(ACK);
# F. j4 k8 A6 x1 F - send_command(CCC);
4 h$ M$ K, r2 r! i, q1 {
% `' Y0 N% I* x% t; l* Q- /* 擦除App2 */ 4 c( L" m2 N5 B- A' m$ k( B1 Z _! W
- Erase_page(Application_2_Addr, 40);
" l$ L! t# n6 v; }" b+ e - }
" a+ g T% {2 o' } - else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束! X# S/ p3 E$ Q/ \* |
- {. l+ ]( l1 [ A! W K
- printf("> Receive end...\r\n");7 E* I8 K3 h) K( C
- - ~% w" g9 b2 ^
- Set_Update_Down();
2 }1 t, f% I; A1 p$ O - Set_state(TO_START);+ i" E8 _7 e g# q, O u; Y0 E
- send_command(ACK);9 ]# P4 }! H' Z; ~6 J, A/ Z0 e
- HAL_NVIC_SystemReset();+ p: g/ w) y% s7 U
- }
8 q: J1 [+ p6 d H - else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据# ^* y2 q/ m# I; U# [9 n( [, S
- {0 |9 t) [6 L* u0 d! O: ~! u
- printf("> Receive data bag:%d byte\r\n",data_state * 128);
0 Z" H' g3 y. ^6 B- J0 k - 4 k! c4 B7 I3 n& T
- /* 烧录程序 */; R( I5 f) f: }) W+ o
- WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
/ f. I" I4 ~1 S1 `2 _/ ~. j) X* a - data_state++;+ U: \) N8 {" n8 {3 a
- 4 Q: z7 w# R/ x( ]
- send_command(ACK); 5 L) [7 M% N. W7 N7 d1 l6 e
- }
& J9 O& e% Y! |! k6 U - }4 o; g8 C% P1 N/ z& R
- else: S( j; x- e8 u9 t
- {
1 `2 _# F2 l9 F. W+ a# j1 R9 x' _' I9 ~ - printf("> Notpass crc\r\n");8 r+ d5 C* k: [) F. j% U2 w0 Z
- }
5 \" I" c" f4 e, z -
, ^* X" `$ K) J" } E# x - }break;
+ X% p( I$ V, \$ H$ M. J - case EOT://数据包开始5 T4 {% A7 }5 c; r7 w
- {& i3 J' K; D( {2 @" B' M/ u: u4 ~, B$ |
- if(Get_state()==TO_RECEIVE_DATA)& [& ~% J# h" A/ P
- {$ e9 A4 V4 u K% P
- printf("> Receive EOT1...\r\n");: c( ]" u: i1 ^ c3 q! |% E6 m
-
3 i# w8 x" |& V! o. R0 l7 x9 t - Set_state(TO_RECEIVE_EOT2);
' o) g8 k" `: n! O W% k) F0 O - send_command(NACK);
" C) {) R T b0 p& ? - }$ i( F3 P2 V1 z
- else if(Get_state()==TO_RECEIVE_EOT2)
_, D2 f: h- }: Z7 c t6 U5 b - {
Y: T; \' [5 U& o- A9 t - printf("> Receive EOT2...\r\n");7 y) t6 j2 @, N4 K$ C3 Z( H
-
. j& E, P% v- }& Y8 A - Set_state(TO_RECEIVE_END);
4 I2 f5 @$ s" Y' u; ~4 \ k, E+ b - send_command(ACK);5 l; G* `1 @$ U: ~8 Y6 r
- send_command(CCC);: B1 a9 n/ J3 O5 Q# F2 U2 O8 d
- }
( k# S: l7 I7 B, N6 n+ U2 l - else/ x0 d! w' i* v2 N& d5 F- q
- {$ U* h0 L+ m; L% M/ m ]3 C
- printf("> Receive EOT, But error...\r\n");2 W9 s/ p7 p( G, e, B
- }
3 P" E; G7 }& R - }break;
) K, [4 w% p& H1 E - }" T6 o: |. l& f2 r! Z8 q
- }
7 d4 L2 ~' N, X7 ^) g - }
复制代码 + L, @- t3 [% e& H
其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.
& t) L- e/ r% ?% }
4 F! |6 |; h& ^) r0 N" s- h" S' Q0 z8 z" s
4. 整体测试
- t6 ?; l6 ]1 A" j; e) J- w; j. B' P本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。: g: m G" y' ]1 `- Y3 s9 a+ x3 D
! [6 Z6 x& W1 b2 a; j* o2 b源代码
% N5 h5 a8 P; @5 R5 E% D% nBootLoader源代码和App1源代码可以在原作者的gitee获取
+ a! P) e$ S0 d3 V, _) [- Z! W! C% W0 x* H& a( g- O
代码的下载* r3 _6 s- M7 B9 r) t- k
由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。- M* N2 g$ U9 h" n
1 o: O$ c) M& r0 c
, F# H3 ?2 U: q# N1 u( J/ a. z
/ t; v0 |! S! D/ d! x* [) y
( n% q, t$ j; U* R8 a& ~BootLoader的下载
5 [+ N! K8 F/ f3 Z: `9 c' R2 sBootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置/ n) H) c" K7 J3 K8 T* E
按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
" ?0 B# s! y3 q$ p! d( p8 h# w! A
& _* F6 S3 }2 k' t8 ]' m
% _" F5 W* b) |% Q9 \
9 E) p/ L: V- F* @烧录代码/ L R2 W/ N8 O/ i
运行, 通过串口1打印输出, 会看到以下打印消息9 y- a) P2 Q) A' c9 V- c7 V
说明BootLoader已经成功运行
R( `2 ~5 w% @" k
& L' C/ {# z4 Z9 @+ _
3 o* B( E; t1 P. K# p# Z
) ?1 L$ r# o! S$ L! U# I& q9 p m* u; h+ a+ D
App1的下载
1 c" p1 A! H! e/ j& T _$ F KApp1稍微复杂一点, 需要将代码的起始位置设置为0x080050006 Y) X* a/ l$ J* T N' F
同时也要修改擦除方式为Erase Sectors, 见下图
2 v* T" j6 s7 c! L; u( O. ^
, B3 V1 j& k& t7 {9 j: X% w
, C {: j9 @; R v8 t- Y * p1 [" _# ?0 H$ [8 B0 B6 O9 `
* w( x* T c0 x' y$ c
$ P8 K' o h2 f5 f. `. d8 n
) ^: R& b4 B6 z( f, ?! g% [
2 y% X2 [* u. p6 p) I7 ^+ ]烧录代码
* P9 a& B1 b9 t( e: p. ?1 x: o运行, 通过串口1打印输出, 会看到以下打印消息
, L" A4 W+ l9 S" \3 w4 o说明BootLoader已经成功跳转到版本号为0.0.1的App17 |' ?, d K2 j. o- V2 p3 \
* h% B4 z7 Q% T; v, @! h- M
1 A2 L. @: a+ ]: \9 M2 `8 `) J; D0 R) n
生成App2的.bin文件1 l% ^$ I4 F) t9 t* [5 g
Keil生成.bin文件
2 U% W# e& T; t* ?/ t
1 G/ i+ o4 b2 P) v- h2 v, i修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件* u% Y. z" _' o
' j, l/ j# E" a( R生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件) l- b+ U+ I$ t/ Z
6 h5 |5 }( r2 ?# `1 I
+ T, ^' e9 Y1 g
. A+ F1 X% D" j l# T% [% s% N3 i0 N1 l, U5 z
使用Xshell进行文件传输! J7 _* T3 o+ o- X. a1 U
打开Xshell% }4 E6 C) V' j! j/ x
代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
8 B! m3 N$ E Q) G4 q2 w' L# b所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
& b7 O! u' [4 X- C, y你会看到App的版本成功升级到0.0.2了.
7 h1 ^9 F; w; L; ~如果你到了这一步.
. O4 P8 R" c6 q9 @% s那么恭喜你! 你已经能够使用在线升级了!
/ G0 e8 ^5 P* G$ n; ^9 N5 {8 q3 Q9 e
5. 总结6 p; W' E, u$ W, N
通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.; ?8 A1 k2 V. N1 F+ ]
% p! b |1 n1 [/ ~/ i
转载自: IOT全栈技术: X& I# X- |' w5 W
如有侵权请联系删除
) e; P6 K4 C8 ?4 x' f6 |9 x& U- J# h, e7 S, z7 W
|