简介0 g* _3 W9 H, X1 u# f3 F( A* }7 j
本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.
8 w6 o0 H+ e5 H# O" e/ ], V" X( G! Y# h" p; S6 j
& G2 m8 d/ }2 P$ L/ h5 @
1. OTA基础知识/ [' A H: Q" Y, m
什么是BootLoader?
7 O- Q, a8 M i: i+ GBootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.# W; J$ {3 @* r6 q1 q' W* r* x8 k
, T# `5 F- m! p3 r6 i: ?
STM32中的程序在哪儿?
7 e0 V6 v# o3 u* _0 c9 s2 |3 j正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码4 W% Z+ k6 @2 M8 U# [) h# q
: v' p0 ~! L+ S2 S+ o" }5 }4 \
+ O) ?0 b" u$ n0 s" g; q- v7 {- i3 {" }3 O
接下来就可以进入正题了.1 B# I6 `) c( A. ~
& O6 \$ d! g: x5 m
进行分区
4 x- R7 F6 J' D' f既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:1 l0 c, ^( u c7 A& Y. T$ F
5 M U% d, ?9 n* T3 p% g6 N) O% m" P% M6 x, l# z! Z
% k O$ a t' U6 w0 g9 [
) q9 H6 E* z% ~5 \$ }9 I7 n以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:/ ^4 j0 I, o3 M* k% ]! y
BootLoader区存放启动代码) f/ @1 S% P& \4 }. B: R) _( [4 q0 s5 q
App1区存放应用代码# Q, o! [ W. a- Z
App2区存放暂存的升级代码4 b0 b# n, }9 U# h' l' G4 T0 x
. V* t; ~! ]: e0 e m: D4 n& V( y# A% O: G/ |/ g) c; D
, l! v6 a5 W, K+ U# U0 J( ]" p0 z
总体流程图
& @* [9 ]' D: F, P3 h1 M先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.1 ]( U9 u: |- W$ t
然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
$ m8 w5 b# g) \/ X( v2 s; u
1 Z2 {( L, P: U+ w在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:
$ t- |5 L, l$ K' W
! x7 L* n, ?; a" N& X$ {$ c
' k7 l; s( D! b( t 5 S5 |" K- K$ v' B1 u* v2 w
7 z# \4 r; d i+ E/ y
% R+ ~8 D9 W% m# U7 x
2. BootLoader的编写
* D) e3 |4 [3 r; R9 w本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。
& I" e5 V% v) w! p: y0 {; X9 L8 x8 ~- g9 R* \( b" F
流程图分析6 W2 _7 X$ K) C
以我例程的BootLoader为例:3 F% }: s$ B) r4 S- D- l2 Z
我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
( _' w: q: @: t+ b" q2 \7 {$ B
W! }3 d) b1 ^. M3 ~' L5 `; s7 L" O& Q) R' @+ J B
* W+ g9 b ]: ]1 C0 L3 w8 x
程序编写和分析
9 z% C- n: `: n u所需STM32的资源有:6 x) J5 x3 C" @1 a
发送USART数据和printf重定向' w" T J3 r W( T
Flash的读写8 E7 ], v+ |) Q& t6 c
程序跳转指令,可以参考如下代码:
) b# ^9 w$ ^! H$ \0 V2 `+ k: J: g& k, }8 y. g3 L
4 k$ h/ R9 ]1 I* ^! M" W- /* 采用汇编设置栈的值 */ ]1 v. y1 Y2 X# K! }7 S% J( N0 F. P
- __asm void MSR_MSP (uint32_t ulAddr)2 T% Q4 o: f) r5 S. @' H8 ~
- {$ s7 \ e+ {1 q- b$ i
- MSR MSP, r0 //设置Main Stack的值. A$ M# k4 {8 u4 d# T: j& d
- BX r14
) h4 S6 S) e3 ^ J5 A* i - }
$ i' h3 r0 V, T, ^+ g; G - ' h/ i( i) {9 f, _$ {+ C4 l; `" }" W. T
3 U; _( s9 L8 ]3 C( U0 C. O9 \! ]- /* 程序跳转函数 */' D# D, j& R, c' Y
- typedef void (*Jump_Fun)(void);
Z/ h" z8 S, r4 W) C& f( k0 Q - void IAP_ExecuteApp (uint32_t App_Addr)# D5 v% z P3 N% W p
- { I L4 j+ O5 d3 H* q _1 j6 k
- Jump_Fun JumpToApp;* F' g/ j4 h8 w" g' Q* t1 ~2 z$ W
- 9 r$ o/ R5 m9 h- i. W
- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
, l& X1 c( B) r# V2 K - {. p: s/ u1 D2 w+ r6 a$ ?5 S
- JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
# {$ v6 o, m! s' |- G) \ - MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
+ ?$ R4 n( H* V; F/ A - JumpToApp(); //跳转到APP.
0 P: i/ [2 `1 O9 d6 g# g' N% ?6 r - }6 \# _" m1 E* b6 j$ \9 P
- }
复制代码 0 y" ^/ m1 h9 O' v
在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);
( ]4 E8 I9 [7 P h% g& ^其他的代码请参考BootLoader源代码
$ l& c9 N; q3 V- ~* c# j
( q/ r8 O. \4 u2 c2 S) V4 Z$ T3. APP的编写6 n9 a, z* U* x8 G/ o& s- M
本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。5 ]/ Y6 K% n# d
3 }+ a4 n5 d' I+ u: f, `8 ^. U U! d( [* o% R
流程图分析; F8 _; u% F+ F' J
以我例程的App1为例:3 x+ q( R' a5 N! i" C
先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;
& U5 m0 ]$ r1 B& O打印版本信息, 方便查看不同的App版本;
6 ^* a' |1 @' {; J9 h9 g' Y6 K本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
5 q% E- R" U9 T0 Z2 N. n+ x- V0 y
2 C* [# B! L$ m
7 ]0 z4 Z6 b% e" R/ _) w
! {/ F/ k& ?# ]+ G# L3 M程序编写和分析1 v) g. u R- t/ ^, E0 u
所需STM32的资源有:& Y+ \ e7 r8 @# W
发送USART数据和printf重定向2 _3 O H ?0 u# d# ?9 J8 }+ T
Flash的读写
) D; A, T- o5 L: q0 j, }串口的DMA收发
! m9 Y* K% I/ D0 P9 n' x& sYModem协议相关 E( {" U! G; w0 D, I1 x
Ymodem协议& w1 ^3 x% y& ]/ p+ k4 [( X& \/ O7 X
百度百科[Ymodem协议]
& S/ [9 k# a5 a& }具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).0 _4 ^5 [, D/ l
Ymodem协议相关介绍可参考我的这篇教程 YModem介绍
; `; N& U p8 a8 \' v& z5 L2 S! K; b2 q) J6 Y$ t
代码分析4 T8 O: j# K' S' Z6 J7 P
代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
7 Z' c+ w( [2 G8 q1 r2 `后面放了我的源代码, 详情请参考我的源代码.6 G& e, ~$ z9 h: j* E
主函数添加修改向量表的指令' e: }$ i# y0 _: c
; q; R9 ?/ ?) A4 ?
9 B0 {/ V2 L+ o( s2 M5 f- S& \5 ^5 s% Q# ^7 t5 v2 J4 c
打印版本信息以及跳转指令
( z& J7 B3 S# d6 M v% }, P( z; B! s9 V7 l2 Q! _
! y: m) ~ w0 c$ k9 g* E' d6 R% Q. _' d) N2 `& j
YModem相关的文件接收部分0 i5 i8 c% Q* k0 G% E, V
- & {! Y6 D' Y2 v
- /**. L& _/ `/ J% \
- * @bieaf YModem升级. \% Z+ V" ^% Z Z
- *
0 u0 m c0 r2 q4 }6 C5 Y' \6 q4 x# Y' f - * @param none
2 s- M. W& [( {6 G& L$ L; X, s* f- c* d* \ - * @return none
3 @% H) W' k2 e3 X7 g - */1 L; V* j! b& O% `: G i- j
- void ymodem_fun(void)
/ k8 r7 S, [0 X! b' ]# b Z - {* V3 M, O3 `! N- E% x
- int i;3 b l" ^% h. E& m
- if(Get_state()==TO_START)
* ?# u+ N1 c+ `8 |/ ?9 a - {) n, ^7 [: C6 A5 x6 @; ]3 W
- send_command(CCC);
" M# E# x* f+ |' F' D# X - HAL_Delay(1000);
' g L) W6 B; J6 k _+ u8 K - }$ k( r/ W7 q/ I+ d* N4 U
- if(Rx_Flag) // Receive flag. e, C- R7 e/ `: N4 N) \
- {* Q0 ]) C1 E, L' j8 r
- Rx_Flag=0; // clean flag
1 L4 L1 d$ [; B -
/ n. u2 Y/ \0 z4 ]$ F3 X, U/ x - /* 拷贝 */1 k- ]; J* n1 p4 S
- temp_len = Rx_Len;
: D& f S# q% [7 F9 f/ N; B - for(i = 0; i < temp_len; i++)
; Y- p0 J* ?7 f6 J$ h+ j& [( V - { ^; l, O0 |- {: {: n# o
- temp_buf[i] = Rx_Buf[i];
- A+ B* N- J) { - }
' i5 m* m$ G+ a$ S - " t$ a6 Y# ` d* m2 R
- switch(temp_buf[0]): d7 E, F/ _3 N7 P+ P
- {
! u- i. e& x1 y9 q - case SOH:///<数据包开始
/ M7 Q4 u3 A& s: G; e- _( { - {
& N3 r5 s0 \( p/ n5 `8 Y S+ I - static unsigned char data_state = 0;
0 H1 B3 y! S7 Q( p; ?5 w+ F - static unsigned int app2_size = 0;
e3 u& Q1 a+ X9 E - if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验# T5 {* c, ?' t* X: m& [
- { 8 {# z% @3 U& a: D3 b f
- if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始
- q1 Z4 ]: V' p" P: s% A7 h - {
: g0 I5 X# ?% Y- ~8 y% v3 `; } - printf("> Receive start...\r\n");0 s2 S) a8 r& H/ {; U I" ]( ]
+ x- Y4 l# Q" ?* W0 w- Set_state(TO_RECEIVE_DATA);
# ~& @; E- z) H, E - data_state = 0x01; ' Z$ a$ @* j. p- R H" z [
- send_command(ACK);
0 t0 c2 D6 F9 w4 H - send_command(CCC);# w, a: r, T g
- . ?& m) B9 m: y: U: I# _9 R
- /* 擦除App2 */
% W: A) L- d Y% }3 w - Erase_page(Application_2_Addr, 40);' T9 T0 `) O% Z' S
- }
, H! @" X4 v0 j3 J" ] - else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束. N$ G$ f: c; r; {% ~4 W
- {/ y+ c* b. K; T. h0 g
- printf("> Receive end...\r\n");4 [! ^& Y3 k! @" p
- . K) @& i; `# {5 [
- Set_Update_Down();
: A4 y2 Q3 c2 I3 R. j - Set_state(TO_START);
4 \8 W; R# n& U - send_command(ACK);
& Z7 C1 C7 I% n5 ~5 L# a - HAL_NVIC_SystemReset();- H: d1 r; ^% K* `5 q! [
- } & U4 `; X( f; S/ U! H& u# G* {
- else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据
, s3 f0 \# D; \4 e' S - {
8 G2 Y4 Z# m! n9 F0 D4 u - printf("> Receive data bag:%d byte\r\n",data_state * 128);# _6 ]5 m1 `+ V- }2 b2 F# g% Z) N
-
( T. W! `) A# W" d7 _ - /* 烧录程序 */
' X+ [% w7 ~8 A9 o( S4 \ - WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
5 @6 ~- z) d+ e9 _/ I - data_state++;
S3 n7 c( v6 F' g -
1 p2 H: b, t5 Z1 \! l - send_command(ACK); ( g6 t' G. b$ I% W( a7 N9 {" d
- }: \2 j, W8 [1 ^1 b. Y8 V% |
- }0 E9 W5 k1 H3 J8 Z$ H0 Z
- else
3 q8 e. c' r0 T$ K - {6 O$ z! i5 v) W0 L
- printf("> Notpass crc\r\n");/ X8 W; o$ l H9 L& o% k4 r% A
- }4 `; t, q8 m, B1 d0 x1 t) M
- & N5 g3 R9 g$ I8 M3 I
- }break;6 s* A4 {6 v u# _
- case EOT://数据包开始
/ ~5 X$ }: R! `& k& L8 J - {
9 u% Z4 J, q: i' s' | - if(Get_state()==TO_RECEIVE_DATA)- ?& |: V$ q& c7 \
- {
: `% U, D8 R2 |9 z0 r* ? - printf("> Receive EOT1...\r\n");2 }9 o( r/ b0 n) s
- + u% C4 @7 r5 I, p8 w; E
- Set_state(TO_RECEIVE_EOT2);
) q. O. |% b6 x' Q - send_command(NACK);1 |, H/ H; K+ q1 C3 f
- }0 M. F7 d! [& T9 ^9 ?
- else if(Get_state()==TO_RECEIVE_EOT2)8 {' E* n2 I8 n; J4 k
- {
* R: d; F7 F) ?' Y* s! E1 [" U) _ - printf("> Receive EOT2...\r\n");
; h$ [ V' j% m* b9 a4 ` -
k- D" M' `7 e( {8 U, J1 s; w2 f - Set_state(TO_RECEIVE_END);
6 J+ e; C, P7 B9 n4 j8 U# G! n - send_command(ACK);; h3 c- N) I7 z3 c9 A9 u
- send_command(CCC);( o+ I4 j& i$ y7 B: N1 }5 h
- }
, W- ^2 ~7 v! T I5 ^ - else- a2 k, E; `- m6 f
- {
; P% t: O) U: c. s6 q - printf("> Receive EOT, But error...\r\n");# n, z; n$ t2 y6 m
- }
7 ^* s4 A; a4 C% ]( p' p - }break; 1 G, b9 I, a+ A* k! @, Q: X
- }
6 Y7 y" w9 l- D8 ~! H0 N: M - }
! ~) R1 o' [7 `) i) l1 { - }
复制代码
4 e/ p0 H' T" l其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.
; C, u T8 R7 V2 l* f
6 _! q2 l* _4 |2 o) f
- ?& T' t4 n# P: l4. 整体测试4 g4 c6 l7 v& u
本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。0 T) _" _( c4 l- A/ k( h
1 @! L4 u6 v7 m |源代码
5 ?# W7 h0 f# i0 F# D+ E _BootLoader源代码和App1源代码可以在原作者的gitee获取
1 ]" |) f+ M& [- y
0 \, E1 V: w& s& d/ Q; g* W代码的下载
6 H2 `2 N2 i) W: b) }由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
6 `. @/ n, @( {7 j2 b" ^
! U- D0 n1 W4 f) J
8 U2 h1 R& O8 D" W Q, o7 D. f: p, x
! t+ T8 F# T( v3 B
B6 o5 m5 @" \9 c, Q8 d. Y$ L
BootLoader的下载8 n* P7 P$ @: d5 I9 U3 e
BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
- Q9 B ?9 l' I; b按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)- O2 l, V6 Y* d. X+ w
6 M& I; M8 I% r% S3 l1 ^
6 X* d6 s1 M+ O4 N) J" Q0 z ! V: {) L+ m1 R6 a3 W) i) ^
7 _# a* [: |9 f1 h* e) x F烧录代码
; m7 W8 E/ c& {" r8 {运行, 通过串口1打印输出, 会看到以下打印消息
/ K+ x7 c0 \* c说明BootLoader已经成功运行' i- C# U9 ]- G/ q4 c P
& e6 b! P3 H# C6 ~ n( f
8 [$ y7 _" j- t7 d6 ?9 H
- z1 C" s: o3 x2 o. F
8 h K6 j& g$ ~App1的下载
/ `! d! t) S8 a$ lApp1稍微复杂一点, 需要将代码的起始位置设置为0x08005000, ~7 B z; ^% c5 `! K
同时也要修改擦除方式为Erase Sectors, 见下图
% m" |: H6 D" `5 u ~! ~. C; Q
) w( A$ a" q9 l2 W5 J4 R& Z& e1 f
( ?1 B4 e1 n6 z- g " G7 h$ G2 z, n$ E2 r' G; G6 F* l/ ]
! X( v' [! Z! j
/ X7 j4 f# N u
$ t+ x9 @" x. `8 [' t8 I! l
% x1 x( F. j% ~4 {. @烧录代码: y8 c; r* u- [1 L: Y' P3 f' X; V
运行, 通过串口1打印输出, 会看到以下打印消息! m: v4 k8 V) x- \
说明BootLoader已经成功跳转到版本号为0.0.1的App1
8 T. }5 h' w1 \/ P% E
+ B! ?4 X. z5 f7 @7 N
2 t- B) p! Z5 U5 t2 R8 `2 H! \ S. n* I* G' Y& ~1 g
生成App2的.bin文件; `) |, h, @* z* A9 M5 o
Keil生成.bin文件
. D1 Y" ?% j/ r Z) Y; r! {0 V/ S! Q' y0 `
修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
: }4 y7 `! d z4 @* j- @' N- I6 l# s2 O) T2 U; P6 R' w
生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件7 L( K- V, q2 L9 L
I4 [, X! o, h3 Y6 Q7 O) Q. W
r0 d2 a4 \$ w, C9 o! b7 e
9 G/ I" r2 ~: |9 k+ e3 R% {4 Y! k( N% z- g5 N6 v0 R
使用Xshell进行文件传输+ A |( B; a+ ~ Q
打开Xshell& s- p9 H) P: L* {
代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
8 i, b9 N- o$ \- J' R! G9 Q; T所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
$ I: x( g6 L1 T% c你会看到App的版本成功升级到0.0.2了.4 Z1 T0 M g0 U: {' R2 o9 y# r
如果你到了这一步.( Y+ Q- z8 C1 H8 w( ?& h$ J1 F3 R
那么恭喜你! 你已经能够使用在线升级了!
p# r+ X1 l y6 e2 J$ N; k3 U$ T4 o! j. @) ^; ~
5. 总结
0 Z( A* @5 Y* H+ z$ R/ |通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.8 H7 o6 r6 l0 Q2 [2 _& ~
; L2 z, Y5 p; C" {6 B- d
转载自: IOT全栈技术
3 Q: v& m+ a# u& [2 r1 C. [$ V) ^8 x如有侵权请联系删除
0 e2 n5 X2 h) w4 h' b; X& P" T8 B/ _' Y) V; T' g% s5 z
|