简介
/ g- u. ~2 k1 i* G8 ]本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识. m! w: \% H8 J* G
7 ]2 z4 w3 }7 S- }/ s
' R+ S+ m9 R# z; v! D1. OTA基础知识
3 V4 v! R1 t5 C, e; A) Z什么是BootLoader?
1 l, U( c" J$ ?BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.
) G O4 s# l" K5 p8 o% a
/ B* ?; x j( K) D7 kSTM32中的程序在哪儿?
- n+ S7 _5 m& \. o" v6 {正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码
! e- X- h2 T" ]1 W8 k3 n( p5 E. e6 l4 S
+ v/ Y* ~0 B; D6 @4 |, O2 ^2 }. M4 [
接下来就可以进入正题了. c7 U) O# C3 |" e$ G3 @
8 u- D# ^/ a2 D( e! |. a
进行分区* I2 I5 o( r( B9 `, D
既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:
) t- d( D4 r Y( q
" d9 O1 F, h2 e
* j, B/ f) ~- ]5 P! T& q$ h7 T" V" P# @' G( @+ T) t! y
# `9 Q, X+ E) s5 c9 K# g
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:
1 z& I! d4 z$ g3 F0 z9 Y3 ZBootLoader区存放启动代码. o4 _: j# Q1 W4 b+ ^
App1区存放应用代码
# C: M1 L7 i2 H( d) g1 @: G/ VApp2区存放暂存的升级代码
& S; W1 r) M: h5 F2 O
: w$ K, b5 T& Q/ n( }. g. p1 p# x4 g/ c8 g, v
7 H% w6 ^" n( C5 ?总体流程图
r" }9 V8 b, A0 W; O* a! ]先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.- |) S8 s: E7 z# j# h
然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
9 h5 u W9 k \) l) Z$ [; S) b/ w
) l0 v" d( L4 H8 `* @% H {在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:
9 P \# L6 n- ^/ b2 S0 L/ Z1 J' x, z7 I. S
% I+ A+ f% _) w. C5 H4 ^
" i4 s" `3 E, s8 E2 Q' P& c& }% F1 y4 w+ a3 g0 e
5 c9 s; p8 G, a
2. BootLoader的编写
3 D5 c( C/ X. ^本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。
1 f& J& R. D8 o. ?: k
' I' O9 |! |. j& Q9 B+ u" N流程图分析' b7 @/ F, ~( q5 r0 d
以我例程的BootLoader为例:8 {; Y+ [( h# w( C% C! D2 E& B
我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
6 X, {4 C% D9 S/ ]7 t, ~( z5 K+ H4 v3 I4 D- O2 D
( N" V9 w$ K5 p, p, ]$ O
/ |, L8 j- Z f- q" j# H' x
程序编写和分析; E8 F/ t1 ?& r3 M: e
所需STM32的资源有:! ~* z3 M+ ?: F6 K- ?& y9 o
发送USART数据和printf重定向
, z: l3 Q$ I6 p( a" p+ `# v5 kFlash的读写: B5 J$ |0 M9 Y6 V7 Y' @
程序跳转指令,可以参考如下代码:2 G; c5 L8 t! Z Z; N1 q& J# C
2 N5 R/ w* @# e* D4 S! J$ H1 K
- /* 采用汇编设置栈的值 */& Z9 X: j0 n* f% u5 G) p. S1 n
- __asm void MSR_MSP (uint32_t ulAddr)
/ u$ L+ D6 D( F8 O0 D& a0 A - {* J$ A1 F+ F D. f$ n) d- `& k& ?
- MSR MSP, r0 //设置Main Stack的值
l2 v9 N7 v! ^; J* ] - BX r14& [7 v3 Y* o/ x4 `
- }
4 {" E0 U; C+ z( b+ j
6 h' S& K" w6 a2 i, F( E! ~
! l6 D- ^: C. J* m3 s& @, J- R& p- /* 程序跳转函数 */
4 p6 m7 t3 r4 r - typedef void (*Jump_Fun)(void);+ V1 `0 \: [4 R8 B& X* c) ^; D$ Z
- void IAP_ExecuteApp (uint32_t App_Addr)
4 }! O8 @8 N# C! j: | - {
2 k* ~- T" g) n/ I/ k' e - Jump_Fun JumpToApp;3 W: g6 Y) O* {2 h
$ L" x% g. `8 I; C5 m) q: n& |- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
7 [3 o* E4 K, T Q3 Y! ] - { ]. [2 q. ]2 O( B i( ?. N
- JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)) D7 }! G" s2 j% @% L2 g [& N+ h
- MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
) F6 o2 D# E' ~, q - JumpToApp(); //跳转到APP.
. q+ Y0 Y8 B8 f. @ - }9 H6 V% Q9 L E4 K* ~7 Q6 Q! C
- }
复制代码 9 v- ?5 `: Z- ]% R: d
在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);9 O' U; F, f1 n1 m
其他的代码请参考BootLoader源代码
7 F) O0 ?/ k& b" D. g, T* k* @
) h/ ^& W" w* T0 w# _, A3. APP的编写
2 |# K; O) V+ R! k9 L本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。
$ ]0 }& `8 Y) q& ], ] ~
: W( Z: _3 \9 D2 M2 N. Y7 L( t" [" z4 p5 G
流程图分析
, B% W1 u* S5 n7 Y1 j以我例程的App1为例:
% d5 Z m" E4 H( S9 l/ G先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;3 F |; v& _3 c
打印版本信息, 方便查看不同的App版本;
2 P* X& L6 [/ U( s. ?本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:% `5 v* E8 Z* h/ e$ ^+ B7 s
6 S' b, O7 P" h( H
$ O8 _( o+ ^8 {; E; a1 i
; F4 N) C! B2 o' Y程序编写和分析9 J$ [' Z( {+ c, r0 |1 L
所需STM32的资源有:' Y& u, O- ~7 ], Q
发送USART数据和printf重定向
+ q9 W$ k* ~% e8 AFlash的读写" X, q! ~4 C% G9 n/ L8 s# |2 F7 B# x
串口的DMA收发
3 e9 |) i! T6 H2 jYModem协议相关
4 s0 s }$ {3 ?Ymodem协议
6 w: S* v/ O1 b6 L7 ~# D2 E; X百度百科[Ymodem协议]
( I* t: m; T) K4 _+ z) J' d具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).
- Y9 {& g4 _5 m+ z$ dYmodem协议相关介绍可参考我的这篇教程 YModem介绍
+ o* O) \( `# @6 ~9 S* K( F5 n/ q0 _% p: Q2 G2 j+ c) U
代码分析
/ H* J. J8 r2 D# E: [代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明, K P3 f/ c5 o- e5 v1 v
后面放了我的源代码, 详情请参考我的源代码.
/ N& ^' n. T' f8 D; q' @! k主函数添加修改向量表的指令
6 l2 b c# j/ x( C$ I, J Y4 M* J3 C5 x7 h
+ K! S! |& ^* B _8 x) r' K
" m# z& f$ n# b3 @$ Y
打印版本信息以及跳转指令# \- S. a& Q; n/ D
2 }7 s0 ?" @1 e K6 ]7 }% o6 T3 h/ X ]' Z9 ?1 {
- X2 f! P& t5 f& ~YModem相关的文件接收部分4 Z& B. x* c" i+ j9 Z. X
. b$ H! P: S3 k5 f; y2 b- /**
9 n; i' V4 p! K0 T% A9 z - * @bieaf YModem升级$ h% q4 R$ t& ^! X
- *# ~1 e' V( _ J x$ e# l* p# X2 J
- * @param none, G& p6 ~6 v+ U; v
- * @return none
5 w+ X" K9 n& Y. @ - */
4 g1 r- x. S) Q2 t - void ymodem_fun(void)2 I# L3 w* e* c ^" u
- {1 T/ S- S3 k7 x1 B0 { \& M
- int i;* n% h' q- s' |+ U
- if(Get_state()==TO_START)- y0 y: M1 R* j* A6 x
- {
9 `1 m7 x; \! X - send_command(CCC);# X7 o' J7 D2 W
- HAL_Delay(1000);* @& R$ O7 m+ Y0 M0 Q2 `; c4 v
- }
& H, M; C& m" X q - if(Rx_Flag) // Receive flag
9 k. ^3 a6 K9 ` t `8 X - {
6 h( T! {" D3 A0 u$ o - Rx_Flag=0; // clean flag
4 ~% L( Y7 |1 F2 P - ! ~. Z0 Q7 D$ _" c2 |# U
- /* 拷贝 */6 j: _+ b0 u" {4 Z
- temp_len = Rx_Len;% u \. Z0 c( n; P! @# U! O
- for(i = 0; i < temp_len; i++)* _( ^' Z/ q q4 W3 @; ?
- {" f+ v+ B/ C9 K$ O
- temp_buf[i] = Rx_Buf[i];3 {2 F+ I6 H( W h7 ]
- }
5 v1 H- R0 p: |* j! c, a - - b; ?% z8 E4 `
- switch(temp_buf[0]). C2 r1 w' Q9 c: E" ~& M
- {
: V3 o* M; O/ d - case SOH:///<数据包开始: a1 e4 W7 F; E0 Z' R
- {' w$ L, W" n+ G3 m, [+ E, W
- static unsigned char data_state = 0;
~) i0 n1 c- N1 O - static unsigned int app2_size = 0;- d8 Q7 l+ _+ M+ }# e
- if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验0 j3 H Y& P' D; z' r
- {
5 D: U$ A* j; ]" W X6 L- g - if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始
# V6 o6 S! m2 B* b - {1 E8 b7 Q: X) j
- printf("> Receive start...\r\n");6 L* \* u8 t/ o9 p+ D( \) p2 x9 m
0 [- _( i9 {6 n. J- Set_state(TO_RECEIVE_DATA);' P5 ?8 f' C4 A- j' q9 e
- data_state = 0x01;
4 k4 n7 [: r; c9 ` - send_command(ACK);
8 E* o* w+ `( V6 ]0 a; { - send_command(CCC);0 n7 L z0 B: Z
- 1 G5 e) q0 b5 `9 O. B3 s" Z
- /* 擦除App2 */
6 p5 a2 o9 i( r, @9 E; W - Erase_page(Application_2_Addr, 40);/ ~' q, \: J9 i
- }0 X1 U4 ^2 K8 y: X: A% J8 O8 G, ]# \
- else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束8 k& s8 W+ l, y. k
- {
O4 ]3 C) {' @! n; m4 s" y& r - printf("> Receive end...\r\n");7 v4 p2 m* a/ P% W( Q6 O
- 5 b8 ]# o! {; g# a8 L/ V
- Set_Update_Down(); ( ?3 w( p1 l) E$ E6 U
- Set_state(TO_START);
: j2 T3 m9 b/ ^9 M; b3 S- f - send_command(ACK);
1 Z! W a$ U+ s - HAL_NVIC_SystemReset();
: n6 T5 Z1 W, R( J! ~8 e5 R - }
. N) Z$ Y3 k+ m. m& e6 t - else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据4 C4 x" D" L$ ~9 ]
- {
' J5 q4 k( p1 t0 t# ` - printf("> Receive data bag:%d byte\r\n",data_state * 128);" f% V! x2 q/ G5 b
-
2 N( y8 p* K9 `0 e4 h* G - /* 烧录程序 */6 }6 D' }& B0 R8 G r" L" q1 e$ e
- WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
2 w [4 S0 G& P7 v' t - data_state++;
8 ?4 U* K( j, @ d/ k - 5 w' A8 u2 p, x' p E. }# g# c
- send_command(ACK);
; L8 c) L- K0 m' D3 d2 F" E9 u3 I2 g - }
6 Y$ b% [1 e; G; y - }$ y0 ^3 X. @) w7 S
- else
. T, O }: }/ f/ X: O. l - {
3 o4 h9 ?( g9 ]3 a' D, k! n - printf("> Notpass crc\r\n");" Z9 W9 z! R" v$ u0 ^5 t3 i/ ]! E9 ^
- }4 B# g2 C6 s6 s& ? |2 C8 {; Z
-
7 h' Z0 r0 k8 S3 ` - }break;% V: C2 `$ t/ X
- case EOT://数据包开始
! T$ T# V; H: \; Y; l, n0 o - {+ z% B) e. j# q" M+ J2 r
- if(Get_state()==TO_RECEIVE_DATA)9 X4 b# N# W! M
- {+ Z8 E" q2 o1 g5 r" h, X8 M
- printf("> Receive EOT1...\r\n");
" {! {8 P1 B3 ?2 _$ ~2 p% e2 C -
& \$ a8 ~! f7 D+ G- i8 y7 L9 { - Set_state(TO_RECEIVE_EOT2); 7 r" O# k5 Z% l3 x- F2 R1 e
- send_command(NACK);
) N) u/ H( Y1 J; n7 i5 e2 L8 k( G - }7 G- B/ |# `$ h" z8 i9 i
- else if(Get_state()==TO_RECEIVE_EOT2)
; v, L: s6 Q+ Z - {/ H3 e7 Y; M0 f
- printf("> Receive EOT2...\r\n");
' R: u( P& q4 W1 A$ h* j$ k6 e$ V - y K6 T, A' k1 @, H" w) H
- Set_state(TO_RECEIVE_END); 4 {( p8 Y: Y% M& E6 |$ G8 ?. u
- send_command(ACK);
# X' y ], ~8 `% {2 G3 S - send_command(CCC);2 N: u3 |# V8 G2 _- A
- }
. l$ Z: `: f- m4 g* y f( Z7 s5 u - else
. P3 V; M& I( K& h7 N. H% F4 B$ E2 o - {, b$ \& F1 U, m* K; h: z3 W2 o6 {
- printf("> Receive EOT, But error...\r\n");7 k C! e) q4 C: U. H, X" U% r
- }- Y+ a! H: ?5 ] ]7 Y& n
- }break; / Z+ B: @' u) q! m; n
- }+ m, T1 x9 s+ g' f; i
- }
6 Y8 l$ T. [% F- z - }
复制代码 ( Q. A1 t, N1 i# V5 R
其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接., d/ N- G7 r4 o6 {2 n5 b- k
3 K6 S3 t$ B) [4 M& d d Q& U# ]
/ ~4 C; `# v9 G& S3 O: r1 T
4. 整体测试
9 T& C% R+ v* P本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。1 X1 |9 j: j) w4 c( x) o& W
' X6 D5 Z6 D$ _' d
源代码1 b, ?. A2 W+ s# a% E2 f v' o
BootLoader源代码和App1源代码可以在原作者的gitee获取
( B7 B) O& A: ~$ L+ C2 Y) U& p3 m3 G
代码的下载1 [7 M# }* p6 }2 h% I
由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
7 {4 a3 ]5 N" n$ t/ S9 i
& K' o# ?6 _& h/ ~( e5 u4 Q
# |' [5 q% v+ U
9 G7 Z1 U2 W5 K' l: {" ?" C) i5 q _, X
BootLoader的下载. S/ |1 }! i4 t6 @4 b0 M& S5 ^
BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置4 u+ }8 }5 e- N+ ?4 {' d( ?) l
按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
$ J3 a x1 A& _* u! L7 l" N% a8 ~, i, z' s+ A
+ d- T4 J1 d) ~) K9 B" ?
& w3 P1 ?3 z M8 z
" P: s: {+ J8 V, J1 p: V, R0 Z9 {烧录代码1 N+ s5 I! ~# a( b9 O$ {5 C6 P }
运行, 通过串口1打印输出, 会看到以下打印消息
- k; l- E+ D: M4 q- u# d/ n说明BootLoader已经成功运行
2 C! J* u) y7 P7 r
; X6 f) \: U+ z7 L. G
0 P7 e9 c5 l- m2 U5 ~7 U8 j
N) w4 {1 \; J$ E' S9 ?
1 b2 j2 E+ Y x; R X; UApp1的下载+ K. Y4 Z+ s! z" h0 m5 A
App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
+ j- f& I& a9 A8 R# R$ r$ o同时也要修改擦除方式为Erase Sectors, 见下图, S+ F8 F, ]- a+ d/ f1 l7 g
3 ^/ l; f7 H7 n6 T
+ L, K8 a8 J8 f; j# x3 n# c) z! y
* d3 g) o5 ~# y1 {0 B
, a* G: x9 |( h; h& l8 u% z) }
d- z9 Z. n) M0 V4 y4 [# u
3 w" x1 l! W1 M8 x( |3 p. b- `4 w/ ]. ^. H& Y6 z) R3 m+ q; P
烧录代码- t; J+ g0 {" q& Z q
运行, 通过串口1打印输出, 会看到以下打印消息3 i8 n0 N8 r3 K6 x. t
说明BootLoader已经成功跳转到版本号为0.0.1的App1
" b% o! [- Z# u+ ]6 A6 A
+ M1 b0 n& N$ k2 U
; b* M6 {- s# U) E3 o E6 h& o: P5 \8 n6 k. q- A9 _
生成App2的.bin文件' S" N. ?7 P4 R7 b+ h, u
Keil生成.bin文件
6 Y0 I* K; ]# y8 H5 W; j
. v) d6 Q, Y9 Z$ O @修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件4 Z5 V0 s% W& g# d
) I/ G* _$ ]' _: A: _7 x7 J5 g生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件
( U$ {7 z; r0 ^8 U8 u+ V' D% ?
3 ^' K4 m) w$ [7 x6 F# a+ |; T* l! k* u3 W2 l. m% Q# r
# {* x( I1 ?. ]2 h
+ }) z% Y3 i8 Q* V6 k
使用Xshell进行文件传输
8 U$ i2 c; A; B打开Xshell
$ j; `+ X+ U. o- y! G代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
+ X5 }" I- `6 j% ~" \! Y2 C所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息5 o5 p) L2 A1 J! [
你会看到App的版本成功升级到0.0.2了.
. A$ V2 }5 F+ G! c如果你到了这一步., v6 j# P/ l, |( U0 W) G
那么恭喜你! 你已经能够使用在线升级了!
4 V" ] c; }: v- D/ T/ t7 b- m) b8 h- w4 k3 q
5. 总结7 n+ y) V; ~! x4 E- T/ ^& q
通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.: F. w; I. c8 @- ~- C
# y' K- b. m+ s O9 `2 O5 Y. K转载自: IOT全栈技术
& C+ N D2 V/ A! s1 }- R' G% H2 z: o, F如有侵权请联系删除0 G/ ^% o: @1 s; `0 k' j* a+ s
( K, @6 S; Q" J) v9 U( p4 F0 b2 |
|
你好,有程序源码或者链接吗