简介
+ i5 X2 E: N6 r( F本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.. j7 u8 [1 p+ f! T
& \0 i# d+ c6 t$ `1 k+ n$ E/ j: C% }2 n
1. OTA基础知识
' [4 { j) K. u$ C什么是BootLoader?" l% _$ r8 S- g( \
BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.
; g& J* n. k1 @6 i: E7 @) H
n! N- h0 j0 TSTM32中的程序在哪儿?
' e* ], b: P D' I" B) K- ?正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码
* ?3 c$ {9 H. Y4 ]6 O |: H9 F/ v( p; _* j6 q2 F8 L
# {9 P4 s# R& @ T- a/ Q2 d) ]/ Z6 T% P4 U: ~+ T' f# ^
接下来就可以进入正题了.) ?0 c! h3 a* }
2 ?$ I! o# j; B$ S+ }
进行分区* ]% e' ]" K( j# c7 s4 W
既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:
9 l' M( L5 G8 T, N$ }( y% s/ t" X2 i& ?0 Z( f* E' e8 Q! b( ]
" @: M* @: d. x/ Z( B- j7 j
: t6 f$ z! e* Q& X
" H# |3 H1 ]3 U0 y2 A/ w9 Z% h以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:' s$ w( M( M r0 r
BootLoader区存放启动代码
D, m( }4 h3 f0 w, J( [App1区存放应用代码: O, c+ R9 @5 H" ]+ S3 ~
App2区存放暂存的升级代码
/ G* E0 f ]! Q/ _6 n
) m( O1 s ~% v% y+ u' x5 M* `: M0 s
9 K) N( }0 d# l/ S" h& B
总体流程图 I; ^& F# Q7 l1 o3 d$ P( u
先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.
" ^4 X% i, p) B4 y* b; B. [( S; S然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.6 \0 F2 { I: L& x# n8 C
8 X% A- r7 y( P在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:8 l6 Q' m( b/ ]2 f, g- G9 u
0 N- X1 L! w+ i
* d* H" b$ Z! T7 c0 I. h, X% B5 y
: ^( L- K) d* {. a; n) b W' U# X! X z- B9 n* B# E# c
! |& s4 A- X1 U
2. BootLoader的编写. O: Z5 p" o0 R }' W0 T' G
本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。8 k# C; |- D: [3 T/ H
8 K! {' e) @' g: Q3 j0 }+ p流程图分析3 ~, Z# R! b. B/ I; f# y
以我例程的BootLoader为例:% b# {0 I8 d3 Y' \
我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
; ?7 k8 E3 m* T- T7 o# b% m- X, S% @& T
' Q q3 z1 w0 f7 {
$ |; T7 Q0 Y* Y+ P& {
程序编写和分析
- U' h n' V! y所需STM32的资源有:9 G' T. t1 A0 {/ q/ x9 _$ K. U4 x( h
发送USART数据和printf重定向7 E J+ f7 w' ^* |4 Q
Flash的读写, V- L' i7 f4 B+ g+ d2 Z2 s
程序跳转指令,可以参考如下代码:' J! Y" h8 ^7 ^
4 W6 }- a9 Q4 [! M5 R3 h$ Z- `& Y, w' z3 ~- `8 K/ e! \
- /* 采用汇编设置栈的值 */
* g* L* W, Y$ x3 Z3 x: z6 Y - __asm void MSR_MSP (uint32_t ulAddr)
" v/ m3 Y$ d/ h1 h- b8 k) s" H - {2 e- {2 r, e$ r' K- n7 A
- MSR MSP, r0 //设置Main Stack的值/ G0 _& d+ b6 Z: x9 j9 v
- BX r14* q' Z4 y. K. w. Q3 e$ s0 Q
- }
z# G# F" m7 P& s9 N; }5 j - * K6 ~3 T/ L5 Q7 I* b* v
& N4 ?, n ^# p6 g6 K- /* 程序跳转函数 */
& @5 ^# F2 k" A' Q9 E8 P& V - typedef void (*Jump_Fun)(void);
1 n5 `1 q/ {1 A5 \ - void IAP_ExecuteApp (uint32_t App_Addr)# W+ z0 K+ [ `. L$ @' t
- {3 a2 r; K4 A- T' O# O+ K
- Jump_Fun JumpToApp;0 g8 P$ z0 z& w* ?
9 L7 U7 ^$ c$ x- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
3 Z8 ~- H$ w! T9 x% D- d - {+ t7 V; _$ N9 t
- JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
9 ~& I+ U# L' K - MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)8 s8 u7 y7 {! [
- JumpToApp(); //跳转到APP.3 _: X m7 L! x3 v
- }
: ^' }" J7 [) M; w- K) N, c - }
复制代码 8 H5 q( B- f' \5 a( F4 ~
在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);2 O& g4 B% {; X5 z1 G
其他的代码请参考BootLoader源代码9 N+ @: b" D! p+ F n5 g; S8 T
8 O7 @9 d( S, }: B
3. APP的编写# k z# w6 P) b( _/ i
本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。
- P; b2 y; [8 n% U4 Y6 B7 I7 Y2 Y. }8 Y- _/ q' O) w1 C
7 @5 Y1 P S: i3 e ?* i
流程图分析
# d9 @+ v6 \/ U以我例程的App1为例:
1 B/ B( c' X9 ^- L9 [3 w* l$ }先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;
+ k3 c+ W, j9 S打印版本信息, 方便查看不同的App版本;
: l% A& C1 d' J( f本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:( B( U% u3 ^) m, j. d( ] I( s
- O0 F7 E. Z6 ]4 {. p: D; b* @, ?5 o$ V
! ~" O' _0 V1 B7 T( F
程序编写和分析" w# F8 O3 J3 m* A+ J
所需STM32的资源有:
+ G$ G! R' Y) L6 q5 F5 M% b* W发送USART数据和printf重定向
T5 h# [/ J \% ^. M4 G- F) N$ XFlash的读写
) M: I, o/ V' [: e串口的DMA收发, c$ M( i6 t* C/ ~) }8 ^0 Q/ N8 J
YModem协议相关
$ \1 O" I$ q) N! b2 n) e% nYmodem协议
3 n; F# X+ R5 _. E4 H* {百度百科[Ymodem协议]
# @1 {* k, R2 }) D具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).
/ Z8 t5 t1 h/ s5 B' r6 [) ]Ymodem协议相关介绍可参考我的这篇教程 YModem介绍) H: y7 L* D- p5 |) R
% w$ Y: j; }! v' g. U0 s代码分析
- L! O. r g w* c代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
9 ~3 C4 ^( }' X- N% M: g8 k( i后面放了我的源代码, 详情请参考我的源代码.- _8 O! y$ O8 ]: E) M) T8 h( E6 r
主函数添加修改向量表的指令
5 B. G1 n0 n* f0 Z' G3 t/ T: u7 E& [: P. O: g; f8 v
z4 P3 C0 K+ M- @8 Q% _& L
! D, p% Y9 ?% Q打印版本信息以及跳转指令
3 q. z; n1 ?- o$ t. y7 J1 g! x' Z# V
/ c, C% F6 ~8 ?8 }: l
9 y" j1 K& A2 e9 r+ s0 A
YModem相关的文件接收部分
: V. {+ B5 @$ A1 F; x- $ r4 I" ^! z4 @/ c: z3 a
- /**$ p! F: y H$ P7 x- U$ h
- * @bieaf YModem升级
3 X6 C* \+ h3 P# N2 i; P4 l - *
9 \+ n; x9 p" {4 ?0 s7 [' s& E - * @param none
3 n: s8 L: r7 a0 G - * @return none
+ ~2 Z* L8 C0 V5 p" H& D& s - */0 i8 \2 D; |6 x0 L5 J4 c8 q
- void ymodem_fun(void)" G" v, F' F$ M, ]$ g
- {
; ^. w! {" F& A$ m - int i;
" `& u0 B# w/ R. B+ d - if(Get_state()==TO_START)8 K5 F5 ?# r$ M; s8 a5 j8 V
- {4 q2 q- P' A% t3 g: B3 M4 ~
- send_command(CCC);
5 l# j# i* p5 w% U3 I, q - HAL_Delay(1000);
$ F# Y# J' g7 w8 t1 ^! B% q$ y5 c - }$ z- g1 Y# n) G; n! d
- if(Rx_Flag) // Receive flag
; J5 R- [, c9 F. N, t - {
$ L( l$ K) L4 |/ T+ r5 H5 W - Rx_Flag=0; // clean flag" C, n. ]; s; H0 u6 b' I
- 8 q- f4 E# W8 N( B# x7 I2 S7 v) a
- /* 拷贝 */
. A4 Z( W4 L! T - temp_len = Rx_Len;7 T/ ]* Z: p7 G% m
- for(i = 0; i < temp_len; i++)
8 A* y1 B1 m ] - {, _0 I. T0 r- v% f! W, }
- temp_buf[i] = Rx_Buf[i];5 e% Z# Q0 B' V1 Q. d8 s A+ f. P
- }
. Q' C* C3 z: B6 A -
( @0 n: d5 E6 e8 _/ @: \ - switch(temp_buf[0])7 J1 t/ j! P3 L9 i8 u7 n8 X" `
- {9 e; y1 @* q2 y2 l
- case SOH:///<数据包开始: e7 X3 V2 S- P0 R$ t; B
- {
1 U1 m( ~& v5 ]8 K+ W, P5 {8 n - static unsigned char data_state = 0;+ C$ T5 ~) z8 Q' v# T- c; G$ ?
- static unsigned int app2_size = 0;
2 z4 \1 s% q6 d" R7 L) h - if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验
$ n p) ^* K- n8 f8 ?9 T- j - {
% n9 }& H$ `; w! V* [+ D0 u% ? - if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始5 n$ Y, q! h) B* T
- {) t! l" V7 }1 |# m- b+ J
- printf("> Receive start...\r\n");' S2 D7 z3 L+ W6 |5 R9 I4 d- C
- % M# x" }/ j, J
- Set_state(TO_RECEIVE_DATA);
E7 Q; P8 |4 K: r7 E; s - data_state = 0x01;
$ u/ C! G2 W+ ?2 [; ^% w - send_command(ACK);
- V* x/ j/ A9 M" i! x - send_command(CCC);
) H& O% }0 I/ L6 v
E4 V: h; z# M% H4 T* [6 {( O- /* 擦除App2 */
0 v) O6 q2 l( m' U. ~% v - Erase_page(Application_2_Addr, 40);
2 S [* S! h0 [' n - }
1 V5 S- P& e" ?/ ~) [* o - else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
0 D* m5 b# z! ^; b% c6 x- A - {
7 B3 m! m" N2 t, ^3 Y - printf("> Receive end...\r\n");( p5 w( P# ^3 J( r1 E% [% s# B
- + {( Q; @ n% C. M- d3 |; V
- Set_Update_Down(); - C5 n( J* D: z6 @
- Set_state(TO_START);
; k) x; u, J) t* r5 O - send_command(ACK);
, U' n. [0 l/ r - HAL_NVIC_SystemReset();) S( F: E- p# \( x4 B: Y6 R
- } " X' o1 e" K4 O" O8 p3 b# j& W
- else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据
@, ]0 N9 t4 J( g, l6 p1 [) _ - {
' }1 B+ V- Z- l - printf("> Receive data bag:%d byte\r\n",data_state * 128);
) Q# x; h2 f7 ?3 C; Q0 D/ q! p- k -
- \* w" l& [3 i5 W4 K% y$ m& t* X - /* 烧录程序 */
/ A0 n; _2 j N4 T1 S - WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);% X$ d2 \8 b9 w' u2 a, C+ M! \! H
- data_state++;
: m( r: m$ |: Z- V/ C -
9 q; E; `0 U, e R9 ?) m w - send_command(ACK);
- o; z. p( p; t9 Q6 \- @ - }! P7 ^$ L1 Y! t) ?" T
- }
# z, O- |, Q- N" Y9 m, V# ?- J - else4 Y4 K, Y9 W: `; S
- {5 }+ j0 i6 S% E8 P) e- o9 @$ L
- printf("> Notpass crc\r\n");) i0 I6 F( x) q
- }- u. k/ \, y- Y2 _
-
9 h, ^1 V* j9 _) R4 Y - }break;; }9 J5 `9 h: \8 l
- case EOT://数据包开始6 K/ _1 [6 _! w3 _. y' i" K
- {; E" [8 M* Y! H
- if(Get_state()==TO_RECEIVE_DATA)5 v. C5 X& `$ D5 Q2 j( z6 G
- {
7 d/ k7 ?* e Q0 j - printf("> Receive EOT1...\r\n");0 r* g4 C A- H5 O1 n
-
8 J* P+ m4 Z, M( g2 A- a' h - Set_state(TO_RECEIVE_EOT2); 9 p( f" h) q& n
- send_command(NACK);0 R+ i. ^/ l" w4 q5 d+ u. l
- }
& m9 |: a3 G% ]9 v" X - else if(Get_state()==TO_RECEIVE_EOT2)- G' E4 T4 m+ t( ?# w
- {
* B8 o7 u; e( I - printf("> Receive EOT2...\r\n");
' x# T6 q! j. ] - 7 D+ m4 M8 X+ F7 w( b! `9 z
- Set_state(TO_RECEIVE_END); 6 g/ T4 K* ~3 ~( m: C- S) R
- send_command(ACK);6 d: Q. k0 C7 L$ O) w1 H3 V
- send_command(CCC);
$ X1 u: e* g2 D9 t# N7 z- g1 | - }
5 O: e8 l9 Q( B& i; M- L3 \; C - else
+ h2 U% v8 K9 d1 D* `) ? - {' b w9 t( b) v l1 Z
- printf("> Receive EOT, But error...\r\n");
b* f. E) g+ V: P. E' L - }
# B% n4 H: j6 d j1 v6 r - }break;
, {! K A; f) ~ - }, ]7 w4 `; ~# Z) O$ P% E+ \5 X$ g6 X
- }, A8 m/ T0 b& E& ]9 I& d
- }
复制代码
N( g* [, l( G+ O+ }9 s其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.
& k+ n, B% o6 B0 V3 n5 L0 u9 `- F2 E _. g
0 V1 I' Y( a! M8 }
4. 整体测试8 V- U( c/ Z5 V0 @ U$ [ p; ~+ v
本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。/ N7 U5 b6 z# ?' }6 D7 @ }
# H7 O3 l8 ?/ w; @. Z7 ~源代码
0 c) A/ i' K7 q6 E2 i& R4 f; tBootLoader源代码和App1源代码可以在原作者的gitee获取! _7 u! J Y" e6 l) x5 S$ D
8 @% v. r$ K+ x. M! a/ J代码的下载
' u N9 U* M# S, }. K ]. I- J* p由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。$ q; D( t; S! e7 P
& C1 p: I0 P+ R) \: O; [9 o; V
. A( w# c5 A0 {5 Y2 P8 d% A
( E. I! ?7 t2 ]. ~ L Y& Q4 V9 r% H7 N/ [! A
BootLoader的下载
- J3 J8 Y& p: r$ P/ o4 {0 w7 lBootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置+ }$ T6 q' D8 P1 K: D
按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
3 P2 N1 C2 q( h$ M' g& y4 W, Y- Z
. V$ u8 n) E7 V ) a n3 x* s* `! \6 z* K4 t
4 N m( y8 }6 F0 o烧录代码/ g, L5 g+ k, h% _6 n
运行, 通过串口1打印输出, 会看到以下打印消息
+ Q. L( j# U4 C( u U) k说明BootLoader已经成功运行# t4 i# k5 S3 R3 G$ e
+ Q' T: ?; l8 l6 g6 ]+ q
, ]1 C" k, q" y; I% X, \ 4 U; S3 D7 f- Z0 C. Z2 a
$ v% v- ]: J7 A/ |# c, P# _
App1的下载
' S5 O4 V0 k6 `! u1 kApp1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
. y+ }- m1 [4 T' z0 O4 V+ Y同时也要修改擦除方式为Erase Sectors, 见下图
4 T2 B; o8 R& j
& V$ S2 e3 g* e5 m
; @: L# q, A, N7 w0 e , z+ }; e2 z) r% V; s% f0 l
+ \) d, I3 M; H# M/ k1 r
! F. u, ]& [7 S% l. z1 k / G, b+ t# [/ }
# Z! N- V) I+ A3 l7 M烧录代码
' q% }8 s: b. [& o; g运行, 通过串口1打印输出, 会看到以下打印消息9 k7 v% m' c* a* q4 r5 D, e
说明BootLoader已经成功跳转到版本号为0.0.1的App1
L! D8 Y. R$ P3 S- B
5 `7 g5 r. {# Q" a% q1 r+ B4 q- F" L! E
- b- P9 b- `2 r7 v+ D8 {# U生成App2的.bin文件
4 n8 \0 |8 M! z( {7 U D) TKeil生成.bin文件* `! G& A. H- c% F7 F
" F+ Z' H4 n- D$ N) @/ U, p
修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
5 [7 X7 h' r$ `% v0 ~) L
+ ~1 b0 F. G3 p, Y! G生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件: ^5 o1 Y- } V
% X! F7 x; V- M$ n3 o; a. q, K N6 f2 p+ W9 ]6 F. D7 K7 `9 E; X
* X3 T( X3 Q) f& w# A5 G# r- l
& z! c! V8 a/ `% C
使用Xshell进行文件传输( |, t7 S* @9 w& k0 j
打开Xshell
0 @- p9 ?; t6 r代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
$ o' z1 Z' \5 t所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
) |# P/ _; C- v, x5 U0 {# i你会看到App的版本成功升级到0.0.2了.7 V' b& \) |2 i; X1 n
如果你到了这一步.
7 |3 G0 u6 ?$ V6 z* s# z那么恭喜你! 你已经能够使用在线升级了!8 ] B# o. R1 v" t
' h Q% d. ]6 F$ U
5. 总结
2 c6 n% ^# Q' ^6 t通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.
* D) r7 A$ P- M3 F2 d1 R( q3 x' D- X
转载自: IOT全栈技术
# D: f, c8 f, o* W如有侵权请联系删除
* M; `7 |% ]9 |; p! _+ g2 {1 p/ e! Q* i: q5 t
|
你好,有程序源码或者链接吗