简介
# |/ E; I* h+ F! T本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.9 L) i1 y" |7 Z' @
( O$ v. d' H9 ?+ \: B. O# Y1 C- Z& q! \ Z9 P1 p
1. OTA基础知识$ m7 K) H8 U: m! n6 ~" X
什么是BootLoader?' j2 I. H/ }4 k
BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.+ e% g2 U* U4 b. A7 l
9 Q" `0 q0 A! `7 p% t0 v
STM32中的程序在哪儿?3 A/ d4 P, Z# o2 j2 q* n1 g
正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码1 g2 _3 M. l$ V6 S7 k4 ]3 ~: I
" }* j/ K8 L. o4 u: N; T; W/ m9 [4 H
4 ` N/ Q5 A, Q! z2 S' R+ j: K! i
接下来就可以进入正题了.! L0 x6 M: x9 r. ?/ g* N/ }
- R* P! _2 ^& P0 R; X
进行分区6 _9 a o3 [; X5 h2 f
既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:
( O& w* u- C- d2 z' [6 I
: n+ J8 r+ H) p& X) _* }# D! r: A1 E5 [7 T6 D5 }
( L7 e( U" Z4 b* M' m+ f7 M
: H7 Y# n: C6 }, M2 H0 ]) H6 _以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:" J2 x$ y1 j' J& |0 O
BootLoader区存放启动代码1 ^* `& f* q: k: `5 v1 t8 l8 L
App1区存放应用代码
( I4 ~2 C [' J$ Y9 gApp2区存放暂存的升级代码, I2 q7 C- v* {) Q$ P' b8 @# B
) v/ y1 m) C; k0 ~5 } L; ]6 Z! X* [" Z! ^$ r
. x6 {+ t: o7 q3 d% O5 m" [总体流程图
1 A7 O5 ?+ ~2 E: K先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.
1 E5 D }4 ^% l然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
8 n f/ A9 P; o) Q9 p1 L- t
; k& _9 w' H9 M7 V/ H% R在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:
6 U( ?( h& B0 ?! E' q( Y& P* W7 G+ X
: a3 g* H7 e% F8 B: |$ p3 E2 P
: U1 v7 b- x2 v 1 ]' C; e, B Z& h/ s
2 {+ y \0 w1 L% }) M1 p3 P
$ x) [1 `* ?3 l% x c
2. BootLoader的编写* E* t A, P+ G# k
本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。. _2 S( h) r% l4 L
, l; @' W$ u" Y2 [! o' x7 i
流程图分析
% f6 s2 ^6 ~: Y. s- ^+ Q以我例程的BootLoader为例:
2 Y+ S3 a- i( u8 t; O0 ]9 U我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示1 E7 v9 l) h4 u- v& l
! z- ]6 m8 M! C7 |. [5 u
' M7 C* I2 F6 l8 g) _' C
; n7 `) B. f1 ~. k程序编写和分析
+ M5 x0 P, _3 ?0 Y所需STM32的资源有:
3 c: p, Q7 C# K发送USART数据和printf重定向
8 Z- o: Q& K6 K3 I- k: OFlash的读写
5 t8 x; ^- y6 ^5 f程序跳转指令,可以参考如下代码:
3 K1 p& m- F( N9 r/ G5 {
7 [9 I+ \+ Y& l) }8 h# z; A
, i: Y; j& j4 _. Z- /* 采用汇编设置栈的值 */
& M2 K( N5 i! n5 H X - __asm void MSR_MSP (uint32_t ulAddr)
I: M0 y. J2 Y8 ?4 }$ H; A - {3 c: k' `2 G# W+ L1 [
- MSR MSP, r0 //设置Main Stack的值$ t- @2 j! X. v9 d5 G
- BX r141 H* s& z& t6 b0 S+ |
- }; H0 H* `$ S1 k) G2 x1 G% q, r
; T" D" F$ R4 |- ! g, _/ D7 p6 R- a2 h* \ K
- /* 程序跳转函数 */( g w# f' u v' N# B; C- Y9 K
- typedef void (*Jump_Fun)(void);
+ J, Y6 l) ^. |, w1 q* [ - void IAP_ExecuteApp (uint32_t App_Addr)
+ r3 E% K, H1 `7 L: n# a2 c - {) K$ F8 c! r+ u# q
- Jump_Fun JumpToApp;$ t; r8 |% V/ U4 H) n
5 m( J+ {) i; ^$ P- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
% v8 l( g* a$ ]+ v, Y - { y. r, D2 s0 C' G) F' T4 i5 `
- JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)1 K8 f: J w) U% ~5 Y5 R: p" r; `
- MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
) w3 p: ^1 z) R/ m% I - JumpToApp(); //跳转到APP.6 L3 l% ?+ \ r5 F1 [8 Z
- }
: V% @% \" |" P# | - }
复制代码
2 [& r9 C7 c$ T7 T& R在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);
$ Z8 W+ D c* m其他的代码请参考BootLoader源代码
. b1 Q* ^6 s5 c" R- Q6 P& ]0 |3 g0 E/ g9 U) s0 p
3. APP的编写
; x3 V& Q9 L' \9 A8 f, o9 u. j本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。( Q. z( o% p7 j5 ], Q2 D4 K* _$ O
}& X+ }- p5 R+ |
. |" Z0 M! E7 `6 [3 z流程图分析6 e, U4 J+ R, @
以我例程的App1为例:
# L& U" ]3 K2 `( s先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;! Y. P3 ?1 b5 F: C6 l' C
打印版本信息, 方便查看不同的App版本;- O+ `% k E+ m* ]! c6 P
本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:5 U9 U. i B+ C- V- ^6 y
F* j' @* Y( ` g
, }5 K2 c0 A" j, x# F @
- x" R4 V. D% ]; ]% z程序编写和分析
& G9 c( K. p$ @! t所需STM32的资源有:3 V' F D4 z$ q. O( g5 B3 b
发送USART数据和printf重定向 G. g: }+ H! F3 H4 [
Flash的读写
" x+ e1 W3 w9 u9 U" r$ v, q' ~串口的DMA收发
8 N, @7 Z' L9 i0 y! bYModem协议相关5 }6 V0 X3 m8 w. ~/ d: e* z
Ymodem协议
. C! l* Y& f0 E X; E# K& R百度百科[Ymodem协议]
6 x0 M7 W3 }, r3 K, _( T具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).
! e F6 z/ _; c' c8 A) ]0 {6 GYmodem协议相关介绍可参考我的这篇教程 YModem介绍
- p/ U$ K, r p
# V. E$ `- _4 @9 b代码分析
- g. i0 t. L3 R M% U$ m7 T代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
! E) \/ N) L$ [7 q ]* e后面放了我的源代码, 详情请参考我的源代码.6 M. z# i2 r$ c) ^1 \! K$ [
主函数添加修改向量表的指令
# k$ V5 Z- {9 Y J: l6 ]- s# x
8 S+ |+ [, n6 T' n- F6 ^! G0 a: _" J2 m. K) M6 }
) N0 f2 J I b- ?' P; T
打印版本信息以及跳转指令; {3 P' E; V- \; H K
8 G# c5 [3 w5 u. L! o2 q% \0 [$ l& ^' Y
% X5 S3 \# M# F [4 `/ Y4 |2 P8 n! y
YModem相关的文件接收部分: \: B0 } |9 v! R0 f
- . [) u( E: f& W6 p
- /**, H& l: _$ u/ k
- * @bieaf YModem升级
! d4 ~# k, f5 A' k; g - *
- `5 s: I& [+ n! r8 J; o - * @param none* F' q! C! x- e% @
- * @return none
7 z/ n/ N( ~: y% d! s - */% }! g/ s" M5 r5 w& z. p/ ^1 q
- void ymodem_fun(void)) f- t" F/ v7 ^% ]
- {5 a) B( p) m/ @ d
- int i;* U: V! a- W8 c
- if(Get_state()==TO_START)
9 z7 ^/ I- F2 ^, I: { - {, O+ A* ^: z# O, n2 t z: w
- send_command(CCC);2 g" C% G! k! V! x& k
- HAL_Delay(1000);
) k& S- t) `9 f& h/ X2 ` - }
) _9 R# W( o" F! ?& v - if(Rx_Flag) // Receive flag
# L8 _3 o' J0 i - {2 C9 {- |' n/ O/ ~
- Rx_Flag=0; // clean flag
0 c( E, C; w0 D/ V* u2 v! T -
) M3 A* K$ J6 ~, K/ T+ z3 u3 y - /* 拷贝 */3 ]# z1 |. s i6 z4 X0 s; y
- temp_len = Rx_Len;- W9 `3 w# n& B
- for(i = 0; i < temp_len; i++)
+ E+ b' |0 B- G1 S - {! R- }, o ^ h. }8 p2 Q
- temp_buf[i] = Rx_Buf[i];
6 |3 F+ a# A* X+ Z& _+ j3 c - }
7 n( W8 V2 y/ w -
% g; J2 ]5 e! c4 j. [$ E - switch(temp_buf[0])4 {2 T# y5 g* f3 E3 C
- {
: D' j4 z2 V5 W2 m/ E - case SOH:///<数据包开始9 c+ W' t5 C. w6 s# E
- {! T* H4 r. B$ O. {+ g' D, M
- static unsigned char data_state = 0;
. C& }% _/ s& F! K' ] - static unsigned int app2_size = 0;
6 t$ R4 g O2 A$ o8 ^ - if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验
- ^) x; v+ L8 A5 q" _1 z) y% Y7 s - { / l/ I3 O" `; x3 z1 p
- if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始% G9 n# O6 m5 c/ {8 K0 |
- {
3 M9 R8 X+ T7 ~4 q5 ]& W; s: p x - printf("> Receive start...\r\n");8 e: ^; x2 p6 o) D9 t7 v
- v8 ]3 F9 R3 F* u: ~- o+ y) p- Set_state(TO_RECEIVE_DATA);
* A D0 C0 J3 b) q' @0 o - data_state = 0x01;
9 l& O, I: f4 P, [& Q - send_command(ACK);
1 _% |* z9 V9 Q' B& h) g& h - send_command(CCC);
) E0 B- L7 @( N5 e$ Q3 M( z+ ~ - 6 g. ^7 t4 T1 X/ ~6 F! O
- /* 擦除App2 */
, w' F& G2 H. U: k# s0 n* t - Erase_page(Application_2_Addr, 40);
8 \' |+ h* O6 c. ? - }2 \) z6 A) Z9 B0 e
- else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
; }6 {" e1 C" `9 N. b( n1 U2 h - {
+ Q1 ^. x; ?! a7 I - printf("> Receive end...\r\n"); x$ F, w7 R) w3 a% n. {
- % S' P& t5 l8 Y* `) z" R- }
- Set_Update_Down(); : ~. I- q% _2 A
- Set_state(TO_START);) Z! E5 m1 d$ \) Y& I4 _& {( c
- send_command(ACK);
: r4 ^& m7 N L+ h* o: A - HAL_NVIC_SystemReset();5 D+ r# Z9 k, L/ g' z4 F0 ~% P
- }
4 _4 _/ B6 j$ W q# Q3 V1 |" q, o* l - else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据# B0 m2 @9 g5 V1 K
- {
, p5 }+ l0 _- s - printf("> Receive data bag:%d byte\r\n",data_state * 128);
1 D, H1 f Y7 Z# a# \3 a: { -
5 l3 e2 Z% g! Q- } - /* 烧录程序 */8 i j1 S1 t( a. q
- WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
! R- K: P( g `6 |: H q. I - data_state++;
: c& V1 d4 C4 L- N4 y T$ J6 j$ i -
1 q: O* V) d' m$ m- X# L% M - send_command(ACK);
% r2 V* c$ ^+ _/ V! c4 \5 w - }
# F' a* |. v3 Q. e - }4 R- X: k4 ]: ]
- else
8 T$ q& I& [6 y( _0 L; S - {; R) `" _& y- A* y6 [- \! U8 ^
- printf("> Notpass crc\r\n");
' @ N4 O6 K) f4 p9 o: A - }2 z4 o# I# u/ O X# @
-
# ?7 Q5 ?, X* P8 L1 c/ k% Z/ ] - }break;5 Z8 q) @3 R' }# L" }
- case EOT://数据包开始
5 U0 f4 l1 y B) U( R - {
2 Z% u( a1 u+ j+ d" K# v, X* i - if(Get_state()==TO_RECEIVE_DATA)
* p; L1 T2 P4 l8 ~ - {
" ~- l5 z% u$ ~, O" s# |7 j# y - printf("> Receive EOT1...\r\n");8 T$ D0 {! E8 V1 z2 e: T1 _: M' h/ u
-
9 _ o4 J! Q: W' W8 z6 v - Set_state(TO_RECEIVE_EOT2);
c6 d7 V5 v* J6 N - send_command(NACK);$ N6 ]8 [, k: W+ S: X3 f
- }
1 m+ h$ R# W$ U$ _1 |$ f$ H. t - else if(Get_state()==TO_RECEIVE_EOT2)
+ U3 p8 i+ x- x% s; W1 | - {% ~- S$ {$ u* m9 B" X
- printf("> Receive EOT2...\r\n");" d* \9 ~; c$ M
- 1 O( B9 R, M3 c( J
- Set_state(TO_RECEIVE_END); R7 U% a5 d- ^5 e
- send_command(ACK);; j4 X1 ?9 W/ @5 \
- send_command(CCC);6 F" ~1 G8 E) n& C& k
- }
4 a$ C3 B9 X% \, M4 M) F/ z8 V6 b+ c - else
# o! X- G& P6 A: W5 C5 H% f/ X5 L7 ` - {
/ ]# w1 b5 T- m* \" W M' x& u4 H - printf("> Receive EOT, But error...\r\n");, ?; _3 v) k# b/ M: g8 ~8 P
- }
' A8 Z- j! t0 N4 }9 F& j - }break;
+ ~- W/ F! r6 f' ?" H' D/ Y x$ y - }
+ L# S. V7 b9 k, \ d0 c - }
+ W0 n* O# |7 [* f - }
复制代码
( ` a8 r8 i- o4 R5 k其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.1 c0 A) n" N) s4 S# O' D9 V0 ]4 J
. t4 e @' J9 S: n; W! ^7 I5 C' C2 l
4. 整体测试: L' o; S; b0 u6 A* A; [* N8 ?8 |1 ~
本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。7 X8 i3 U9 J9 |
8 }$ Z% s _0 u& w
源代码
) t. C# G: x2 i$ D# q2 ~BootLoader源代码和App1源代码可以在原作者的gitee获取
9 h* H7 v2 G9 w! g- v" @8 }/ |8 @$ g/ k* g# {) r
代码的下载
2 |1 O) {0 @+ f) s8 `由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。3 H* L Y& ] \& P& k
- j" k' w* P8 n
5 y# w/ a- p q; ~# M/ r8 | ; F6 @ b4 S# W* r' p( l6 A4 v
3 ]6 `( n# l+ ~& L+ H, r' z+ u/ ^
BootLoader的下载2 z7 c8 {! S3 N6 S7 _; G- x
BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置 {, W4 y( V1 s) x% Y4 b
按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
, z6 [! r7 u( u4 O- f/ c: B, ]+ o& D0 ^4 t5 E7 w& {$ J
6 m7 Y2 i- H. |- c3 l
7 O; i3 W# {+ o* t2 C' j% f0 P/ y! d
9 u. r0 f7 w! S烧录代码
5 l7 d8 ~; G+ D2 `; S- u运行, 通过串口1打印输出, 会看到以下打印消息 B9 C2 U6 s0 x
说明BootLoader已经成功运行
7 H4 W+ g/ U8 E2 y% ^) e/ ]
# A0 x, U% ^/ B% z1 e$ ?
& }# ?3 p: B, N5 c" a- j
) e+ R1 {" v) {& U( `9 M+ l/ R
) g4 M8 e9 H7 ~: H
App1的下载5 Q8 l7 @1 W2 [% O5 v
App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
$ l, l( o( t7 @$ k# A1 o同时也要修改擦除方式为Erase Sectors, 见下图; Z1 r8 y( [, h& B" r( X/ ~. H
5 k! _1 W& D% S7 @% E0 s
( h8 P" a1 z2 {6 N / O! c4 ?- \4 u! F0 _3 V
- r' ^6 k5 Y+ ]3 u
/ g( W7 e; `, c7 r+ G 3 q# h9 _# _( x
1 F4 D2 B/ x3 `1 P& \
烧录代码0 Z2 S0 Q6 A# |# o0 H& J
运行, 通过串口1打印输出, 会看到以下打印消息
- u. r& b) \9 d说明BootLoader已经成功跳转到版本号为0.0.1的App1
C; j0 Y$ E K1 n! |
: z0 u7 M% o4 U/ @/ U9 J7 S: H
% L, f# t& D- `: v2 A4 U6 ~5 ^$ x& N* T( `$ v2 s
生成App2的.bin文件: y4 s6 N; L, b. X ^5 w) V
Keil生成.bin文件
* ` @+ h+ K% m1 k
6 F+ T" x5 L5 t5 G修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件+ {( |! g7 J" B3 Y* p6 B
/ f( X* O6 F5 P' a7 s生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件
( f( V3 t9 Q: D* [- c% D: L# t
) A+ X5 a5 S! b( i3 ]& X* Q5 h; O; j; R/ V+ @3 s
9 |. l5 N4 J; O; y( G! Q
/ u$ h* x$ i: }/ Q4 m使用Xshell进行文件传输
4 x! V" K6 o& b& s8 \" k! `打开Xshell
$ G# d' Z9 _5 X. \代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
5 J( ]* a4 H* e: b所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
1 z- s& x7 l# A) e: }你会看到App的版本成功升级到0.0.2了.
9 f: {7 b0 H- I4 v% ]4 y如果你到了这一步.
- J5 | d2 S# V; q那么恭喜你! 你已经能够使用在线升级了!
+ q0 d1 ` ]) M5 E4 R4 N% ]( W4 W! [) | M4 ^
5. 总结
/ H9 m; b. ~" s; Z' V通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.
+ V$ ^* n" M4 c) G' M+ V d9 @) `4 R# f0 Y1 M+ \
转载自: IOT全栈技术
. C0 T3 N, o; i4 `如有侵权请联系删除
; H1 M9 U) N( [. Z* j9 B) B0 \/ |% ^& i {6 c9 B" p5 @ u
|