简介- W2 A/ V5 t0 F4 @3 U( \6 y
本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.1 k# V7 I: n! p
8 K" p* g6 |( C9 w& H. `* h3 l
! M; ?" {" s% T3 x& y& v9 y0 W1. OTA基础知识
4 i; f k& [6 g$ E" s2 Y5 e' W5 J0 }什么是BootLoader?, g# |2 U! |9 T
BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.( G, j, T, X. m, x# K$ L
" V) B0 X% m u8 d n" sSTM32中的程序在哪儿?- ^+ B; W! I& ]7 W! h* S
正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码0 H C' K) a+ s. U
# v5 o" X$ Y2 c+ P$ w
v0 y% m! d1 P( v) N4 A$ N
' u/ [, _. d5 ? T接下来就可以进入正题了.# e- p- H( ]4 I- h6 J
. N6 B. x; g; U; p, X. Z( M! h
进行分区: w/ {9 H2 X4 k
既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:/ U7 m4 H0 K8 O( a
* u* g. C- s$ _9 O7 f$ k( H/ D
, K) B- }+ h+ E% i F0 n
6 [+ |8 [, F" Y+ }, @* H* }. K) V- Y2 ?/ Y4 l
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:
9 \( N* i5 H/ n+ i4 jBootLoader区存放启动代码# b+ O7 D+ R+ n: U# G
App1区存放应用代码& W9 W5 o g. Q( a: w- p- [3 l' C
App2区存放暂存的升级代码
^$ Z7 {: X& D; Y& U X* {5 U$ ?& G7 h4 {
) _4 t, `/ a$ e: E2 S* n$ k
^ y, R3 U' c) @; t$ Z总体流程图
" q7 X& Y4 m; ]) w& z先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.$ ?4 m3 q' ]$ n+ c1 B \
然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.* _6 S; l$ M+ t: X: ~! p! D% X
% C5 j" j. {+ m# t* ~
在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:
9 H: g. T* \2 k! A( D5 x0 e$ ?: [5 U. a1 j2 i+ E) ^
- O/ E/ F: P1 {$ U6 z# p: u4 X ' n8 G7 e& m9 D$ r
7 k" o/ @- ~: X/ G8 ~& T
2 |, r8 o9 I2 ^, J. p, t
2. BootLoader的编写
, z, t, I) F0 ^( h) M0 X# B本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。
" U, M# K) ~% O' T/ P' F& F6 @ m& `, S# d8 ]9 Z( m0 [
流程图分析
" F8 f T! C- L2 u2 [以我例程的BootLoader为例:5 x( `4 V3 Y2 `& t/ R/ Z
我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
+ {1 Z9 I _) ]! P; F
/ b* M/ N- H4 h7 e# i8 b2 [4 [
( L5 B5 s4 \/ ]8 M
6 z5 h7 c& x0 A- s程序编写和分析
8 v; D* h2 l& @( j: w4 G: Q7 }所需STM32的资源有:4 X2 |2 w- P$ m% z5 b6 ~% o
发送USART数据和printf重定向; @; u4 f4 K% T' S) g8 s& S9 `4 ~6 J
Flash的读写
0 Y0 t' Z5 E) x/ y# u) C程序跳转指令,可以参考如下代码:
6 k7 \: G: b7 D) z. L; }" M0 l$ c: p, ~- r' a1 ~
1 ?+ e& | N' f/ H- N- /* 采用汇编设置栈的值 *// t% E2 y; ]' H( L
- __asm void MSR_MSP (uint32_t ulAddr); D l0 G# J' W4 [1 V1 v
- {
6 k' f1 T) C. Z: r) V8 @ - MSR MSP, r0 //设置Main Stack的值4 c: b4 I8 c$ K2 w/ q
- BX r14" K6 O5 `9 ^6 S1 f3 k- m ~
- }' W. S- k9 X2 z* U# z1 M
+ }$ O( m1 M& c' V6 M2 U3 ^
5 h7 E2 N" o5 W! k- /* 程序跳转函数 */( L L5 ]% E+ f% I7 D9 ^
- typedef void (*Jump_Fun)(void);. G/ e! l- f. I
- void IAP_ExecuteApp (uint32_t App_Addr)9 S1 E4 X- \9 { ~
- {
/ o, l, a& d# Q; r v - Jump_Fun JumpToApp;& D5 L" D; j1 [7 O
3 Y6 C: \, t1 g* R* ?" Z% C- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
* ^* ~: b: r [0 `3 | - {; w4 }( H! x1 l8 c* Y% s
- JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
( Z, Y' U5 Z# c! z5 @+ d$ l; `4 y; l - MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
" z4 X( E8 Z" ? - JumpToApp(); //跳转到APP.
$ |4 G6 W, _% i4 S j! q6 E - }
( B! c# p0 }' w: y! ? - }
复制代码
% M7 u; m9 z, f G2 z. K在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);
n e6 H+ z9 W: r其他的代码请参考BootLoader源代码4 W& Q! ]! u1 O) @- F
L; L `4 `+ q
3. APP的编写+ \5 \) M4 \% \! ]# n5 s
本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。7 z0 |% b& ~3 ^5 X2 h
3 \7 D7 {2 u4 m2 ]" h# C! m4 ?) N3 Y# \8 S) _2 k
流程图分析3 ]( a: b! j1 M4 r3 L
以我例程的App1为例:. k1 E, \3 r7 Q7 A
先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;
! S9 `4 O3 c/ t) D打印版本信息, 方便查看不同的App版本;/ e" v/ k7 ]) C# F, _
本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
7 r/ h# S1 ], g) r! Z) B# {3 q
5 y. r- v3 d g* i
$ w% h5 x4 W1 v, V! i7 a0 g0 D+ c
8 k$ \. T/ @, H0 ~5 u程序编写和分析 E' c" N: e* N! E
所需STM32的资源有:
4 I9 F) ?9 i* N9 |% C( w发送USART数据和printf重定向1 a$ |. S6 S" A* y2 R D
Flash的读写
$ |+ I2 n/ Q/ h% x串口的DMA收发& |. Z0 |& z4 {8 g9 n, i
YModem协议相关
2 r: |3 P, M' V2 D% a$ m* xYmodem协议
6 N/ C1 ^0 R& l百度百科[Ymodem协议]
/ X$ R4 L: n& ~; ^# i* }4 \具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供)., |' r J2 ]) }! V' Q7 z' E' ^" [
Ymodem协议相关介绍可参考我的这篇教程 YModem介绍 `) _2 [* J7 L, V5 _
+ _( n9 P: t- u+ y代码分析7 Q _& q, j8 r# w% i2 D% z
代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明) j$ A* |% u, J5 O) f! ~$ ?4 H2 [2 X4 k
后面放了我的源代码, 详情请参考我的源代码.
: B9 w" r2 ]6 \" R主函数添加修改向量表的指令
8 H7 v. U2 F, \; E' |7 p0 f2 E5 ^; c% ?. r) x
" z& ^& C2 ]4 E8 n B6 e* p3 C% {, f6 N, M8 B- O1 n
打印版本信息以及跳转指令' x! x$ p* M3 a; J. w% x) J
; u) l- y4 E8 |
; b- i) ?0 P+ N# X j% }- l8 K1 {$ C/ F
YModem相关的文件接收部分% B3 b5 ^ W8 G. T4 \0 C3 I( K3 I$ I
4 d' C% o v: |- /**7 V' f+ b; X/ R; h! l
- * @bieaf YModem升级' e! ~* g7 L, d$ a, J! K# K
- *
' p$ E) q7 h l% m - * @param none
) ~8 z3 l }$ \4 ]! d+ b9 k - * @return none
6 Z, {* i/ \+ B7 ?5 r7 _ - */
; T+ d4 _+ c6 q3 B& c2 D+ R - void ymodem_fun(void)4 }5 Z# B: v) z1 h% b
- {
( p) v/ L0 R. k4 H$ Y: @+ y - int i;
! _( S+ K2 O6 G( r1 L4 Z' M4 l( D - if(Get_state()==TO_START)
5 _2 L: H5 y9 S4 F - {
, r6 J$ p5 n* | - send_command(CCC);# q1 _& V g9 M6 O& a
- HAL_Delay(1000);
5 a9 |- w- }$ |/ J4 Y& X2 A - }' v' t- Y: S7 y( g1 i0 U
- if(Rx_Flag) // Receive flag
- z. n: I8 m% ~, i& r; y - {. r' e9 w4 r( V, h3 T* D
- Rx_Flag=0; // clean flag7 A- ]2 J. ]3 {9 z8 U
-
- C i) Q; E+ x3 J* V - /* 拷贝 */
' x B7 X5 m6 w4 `( q - temp_len = Rx_Len;
, v/ ~: u8 Q; P$ c- W - for(i = 0; i < temp_len; i++)9 `, K& C0 G$ w) }, N( e
- {
R! A! j9 b! M; v, } - temp_buf[i] = Rx_Buf[i];
& }! m: \, x& H' Q - }& l5 x$ w8 f9 V( {
-
7 ]' q" U8 l2 g+ Q. u" a! M1 C - switch(temp_buf[0])
# n( Q/ X7 G" h5 u/ W, Q; @0 G - {
/ F. M1 O6 g. Q+ | ~ - case SOH:///<数据包开始
* V5 p; i# ^- e6 m) V - {
3 l7 s4 B1 v/ _% L) w - static unsigned char data_state = 0;
0 H5 K' c( c5 N - static unsigned int app2_size = 0;+ |% _& m: n2 C
- if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验8 M% X2 S a J2 a4 M) F
- { H. c7 r* E, U$ F6 G- R
- if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始/ e! d* B2 @5 O e
- {
# r+ y) ?) @5 h1 R+ F; R7 T - printf("> Receive start...\r\n");9 G- S% k" _3 e4 V! @; ?
0 @5 L# @/ V4 Q& Y V1 ]- Set_state(TO_RECEIVE_DATA);0 N7 |* h1 I$ X5 i* s$ i0 z) Q
- data_state = 0x01; ) t$ R3 G6 E; ?: @
- send_command(ACK);4 C0 ~& m. Y9 h' T% f' R5 o
- send_command(CCC);4 ?+ Z% c- A( n' T) S6 m
/ w2 c8 ~* R5 g- /* 擦除App2 */ ) I/ B4 M+ T- t
- Erase_page(Application_2_Addr, 40);
0 C7 M7 |. K7 W6 d- D1 G - }' J- F: n) p# O, f9 F
- else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束- ~' y5 U! o2 ^7 j
- {( n% t/ u! b$ q' G
- printf("> Receive end...\r\n");( K9 c6 z6 y4 O
; R: t, D" P& T$ n, Y1 V- Set_Update_Down();
5 ?/ P! @ T/ l1 Z: b - Set_state(TO_START);
, R; ?2 e) H1 A1 g$ h& @* v - send_command(ACK);
8 q5 ?- O5 K+ b5 j; S' W - HAL_NVIC_SystemReset();4 ^. H& n/ `: a2 j
- } 7 S5 v0 N& b9 I# T( j" ]
- else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据' C1 y1 `) u' W4 N( I
- {
( i! ?* ^+ |# J! O) w$ g0 S% { - printf("> Receive data bag:%d byte\r\n",data_state * 128);
5 \! t& j' `; g. S3 ?4 V+ d - ! V; R0 u/ S3 T# ]7 r V- l% k
- /* 烧录程序 */
3 Q0 x. I) L; |. C5 u5 o - WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
- [6 z1 E( S6 b5 y! F* b) o6 k - data_state++;- D5 D, C0 ~3 h j5 s) J* W0 l
-
! |5 N; n. t8 l' Z8 |+ Z# ^ - send_command(ACK); 2 o# w* _ Z9 P& D
- }& Y; i- V7 ~1 K" e+ S0 W7 S
- }
- E* c, s# z9 s& l9 e2 R - else4 Z8 u% U! p' H# Z" Z, {
- {8 a% s6 I! H* [; y
- printf("> Notpass crc\r\n");$ k3 a T5 e1 ^; h7 R* D1 ?/ Z- I
- }
: ~8 t( u0 j/ `3 `7 b) | - % S9 L& x$ j/ p
- }break;7 J x; N" k7 g0 B+ ]
- case EOT://数据包开始
% j0 D6 W+ {, @ K, P - {& M$ g/ m% l6 S- @( O
- if(Get_state()==TO_RECEIVE_DATA); ]6 ]% c9 T0 P
- {! T9 i& e' r: }
- printf("> Receive EOT1...\r\n");
2 B- B' y* r4 n" M$ y - 2 a& x0 |7 b# I* p( e
- Set_state(TO_RECEIVE_EOT2); + G' z( d7 j+ j$ B( Y' e
- send_command(NACK);1 U) `/ n, A" r8 j2 O, A* P- B
- }
, g# i+ b* _* s& W# M - else if(Get_state()==TO_RECEIVE_EOT2)
1 r+ x1 @- p3 ^# t - {
4 L! N- e# Y3 n" _! d# i% \4 t - printf("> Receive EOT2...\r\n");
% J, V1 ?% S% P: n% f* S8 \* i" i -
0 i/ k2 ?. W: o - Set_state(TO_RECEIVE_END);
3 n8 T# T) J3 h, @- z - send_command(ACK);! H# p( t4 G d5 w5 r
- send_command(CCC);- i6 b8 D: y8 `2 g
- }
2 B+ K4 v% N. J - else" }8 F8 R! ?3 p
- {' L! z$ y9 }. n' n8 g1 b
- printf("> Receive EOT, But error...\r\n");
) I# W% d6 T# u- b - }8 r* s% i( Y0 A
- }break;
- d. Q* J2 T! P2 e - }; k) S; K' ?5 \; q N. k
- }
- C8 N' c; R+ _* L - }
复制代码
1 x$ A6 p5 J6 T- g. p其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.
5 V. O7 B# a# q; v
/ \7 N- I; D4 D, ]* S4 \0 Y7 R% t+ U0 U+ E0 K
4. 整体测试
. H# V& L' Z& `2 m3 ^- H2 q本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。
. k1 @2 m @: d7 A
+ M4 x. y5 j" G) C( I/ Z0 R/ {源代码
* Y$ X, `; G( i# W+ uBootLoader源代码和App1源代码可以在原作者的gitee获取
3 A) |0 y& z9 {4 R, G
& \* z% {# z! S8 @( Y' U3 V& \4 }% m代码的下载
& H7 A* K0 }* \/ N8 ?6 A由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。! k5 n' b( ]8 f9 `1 X- X' h: s* W
1 E1 U) D+ k4 H2 X# Y E4 i
/ [) O9 V U" C7 F+ J) P- ~
/ B# L2 i# b* D- \: p8 ~# L- H1 C! k- w6 o
BootLoader的下载( s3 q, L/ S; b$ A, x
BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置( }& M4 ?! T: h2 T. ~
按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
- r M4 M$ s* u. }; c' y" L3 T/ c6 j5 L8 T9 F0 q7 c F
1 f9 W' G+ R6 ^; q7 N- t ' i8 ]3 m7 U$ m. |2 I; ^% [! j
# `; F& p& k! B' G7 \( x" t烧录代码) k& h9 \; P- D9 ~* }" f4 v
运行, 通过串口1打印输出, 会看到以下打印消息1 _+ @7 t( F0 ~: t1 C; I, u7 D
说明BootLoader已经成功运行3 T% ]4 V2 W+ j4 @
1 H9 i9 e" u' M$ a$ i# A a& F- }
$ a6 B+ m0 O9 d; a7 s5 q' C9 y
6 w2 s& Y$ D" q7 o% u
8 ^. ]& G1 @: |App1的下载7 K% l6 M! b- T
App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
7 n( M/ y4 K; @# s同时也要修改擦除方式为Erase Sectors, 见下图
) n8 j, l) n& H f" p5 ]% z l1 f! x6 B- X) o5 d7 Z: m
0 ^# R# y) z' K, G( h% a4 Q
# h/ R0 e: K, C8 H4 p0 x$ \
; t, Q3 E. T7 L% n/ r: S* ]
9 S r7 J' [3 a& b! ?3 J$ d8 Q
- U2 ^% f9 ]0 o. n; S: o' _+ M+ U. K- ]
烧录代码
* L( a. f: d5 W" m( ~运行, 通过串口1打印输出, 会看到以下打印消息6 v# E8 G; w1 S8 ~: ?: E
说明BootLoader已经成功跳转到版本号为0.0.1的App14 F" I7 \( @' L; l8 C( F" U* a
3 u! O+ [; P, F& [. i+ t* X
: y6 e/ B! a- z( k
7 U7 ~% p# d* N& C生成App2的.bin文件: H2 o4 }" f) [. y8 K3 z
Keil生成.bin文件
! M8 {$ U9 ~( E; x) |* h, m; J! N" {
修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件% O5 |" h2 V, z, O- u W
/ U7 i) r2 F2 A5 ]* j) l, t生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件
+ W, d: r, @7 o& J8 m7 Q& z' k
5 y, j( {1 ?; H6 }
2 X( |- x6 `1 p% x% ~, Y; x* z! c v3 ]; Z! I& F, u
2 }7 s* a7 [3 L% m使用Xshell进行文件传输* h) q h1 e& ^8 Q# @- A; o
打开Xshell3 }8 o1 F9 J7 X; P% f5 r
代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
! ~6 E! z! i7 F9 ?* x# H: k0 y l3 {所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息8 h% Z2 m# b) B4 N8 l* i: C
你会看到App的版本成功升级到0.0.2了.8 N# Z' |* ^8 \2 N' {
如果你到了这一步.
( u5 ~' t& b6 n8 S( |" y那么恭喜你! 你已经能够使用在线升级了!6 u9 T' u- @% c; l7 A2 Q" a$ B
/ q* X# w- g) E. s5 A6 u% b
5. 总结
; j# B: C+ ~7 c通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.! {) ]; M; ?! S( T2 T9 `4 k
" S$ C' `4 P7 @' x. E. K9 M+ n转载自: IOT全栈技术1 @$ h8 e+ j) Y7 m
如有侵权请联系删除
+ o: t9 v& D( O! e9 Y" Y0 _% W, U# Z6 z) }0 H2 g# |
|