简介
- V2 n& J4 a2 R本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.
m \8 U' W! U8 R4 N
7 B' e( ]" G& h1 X9 ~3 d: R3 V9 `' C' R; h# D+ D
1. OTA基础知识
% _3 C. D u+ a9 J! M/ a什么是BootLoader?
, g* `) @8 j" y# B( M" i6 EBootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.
2 T; X. t$ k, G$ D
- j: C0 @3 f/ J1 {" _: H2 j0 v7 QSTM32中的程序在哪儿?4 q5 ]2 l: V1 q* R- G
正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码
5 j: y; k2 |# Y3 v: p9 D, [7 F9 h' \8 U/ {2 F6 i; F. w# O2 a
2 u5 n% @/ d2 J8 o$ h6 ?5 d% y4 l7 w
接下来就可以进入正题了.
' I9 G! S! h6 V+ C _; S% Z* G; ^' W* A- E& `+ p6 \
进行分区
/ @9 ^* E- S* e) n. J ]( e4 j既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:
1 W& {. b3 `+ A q C
0 g. E! I O& O( a1 v \" ~* p) ?# m+ P
w, _4 L1 l/ E7 [2 _
) H$ j# X; e/ L* V以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:
+ z1 d) ?5 A6 X) X c/ K2 QBootLoader区存放启动代码
J5 V% [( z: x, V7 S+ p0 }App1区存放应用代码& Y5 Y5 F! ?+ z: R; G, Z) M9 c
App2区存放暂存的升级代码1 F5 S; U! Y( M4 X# Q9 i; A
' i: R5 { j! q; t' H# M& H* r4 Z6 |# T3 s3 u9 W; Y
9 b; k2 Q' D5 U( q) S& S
总体流程图
8 y ~5 H5 i4 d: i先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.* O# j8 [8 ^$ l6 T; `
然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
5 S. g6 j: V0 x4 U6 J7 ]+ @( }
- ]3 l, O& ~9 Q% l# t% B& V- d* v在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:. N- Y# P. C r4 N9 r: p9 B" U% H
4 h; Q$ l9 U, `$ t
( ?" _- d$ ]9 @5 R g9 }1 h6 ^ 8 V3 o' m" q$ [/ U( U6 T
- w. `+ _- o# S
: ]& {6 ^4 O- N1 m" k8 C2. BootLoader的编写( u$ r1 Z+ K3 L p* U, |0 z# u
本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。1 h: f8 |* l( D% N" l
. e! e7 Q/ w! W
流程图分析
8 `& P5 S. g( {, ]1 v以我例程的BootLoader为例:, }9 p- }" c% t5 [% B
我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示0 _% U A! y1 J& l
, Z7 P+ x: O2 I/ M8 z+ w' q% D2 q u. q/ s' j& j/ [ T
K$ N% y/ v# X8 ?6 k0 ^" [6 I0 T- H程序编写和分析
/ X$ M3 l' s6 S* _- b) f0 C所需STM32的资源有:
: q3 ]- y* c6 T+ }发送USART数据和printf重定向" n" ^2 R- A ]9 j
Flash的读写
5 |1 |0 t) |' G1 R, t程序跳转指令,可以参考如下代码:
6 t( B0 t O1 ?4 X; T9 |% E* i) Q5 w* J# p# v3 q
! p7 f5 @# |7 U1 N, j+ h8 z$ }, V5 ]- /* 采用汇编设置栈的值 */% T8 |# x+ ?$ y. v
- __asm void MSR_MSP (uint32_t ulAddr)
/ X! l9 z' q- [! } - {
5 W5 {( P/ p; A m$ k L: Q2 [ - MSR MSP, r0 //设置Main Stack的值
4 l& n. d( ~/ [ - BX r14
8 {7 ?2 A1 G6 m - }
8 | J8 V3 [1 f; o
3 W. ?! I% X' X) B2 S @# B
% A$ }. p' m M1 b# M- /* 程序跳转函数 */
( ?" \4 _. }+ u) L& [! I7 Z4 g - typedef void (*Jump_Fun)(void);
9 Q; s/ X! G4 s" R1 I* F7 S% u. }% ?0 K - void IAP_ExecuteApp (uint32_t App_Addr)' W6 `+ @1 Z& i
- {
7 a2 Z7 Y: d! R g, k - Jump_Fun JumpToApp;* T3 W9 r7 J; U x" T9 M2 R
- & s1 s- T, ] P- \5 q, _ z8 q
- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.$ K- i& N4 `" J2 b1 u0 x
- {) K0 U( \/ ?# ?( Q9 w! w4 S; a
- JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
. D; o0 s; k3 b# l+ p6 n - MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)/ k* t2 d1 l9 P) M% q9 a ?
- JumpToApp(); //跳转到APP.) x2 A2 ~) z# A. S; _# k
- }8 Y( z1 h/ K9 W6 r- A' T2 M3 i! k2 G. i
- }
复制代码
; E0 a* x) ]* |7 @! m在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);2 g( Y4 e. L4 m( R3 t% c4 O
其他的代码请参考BootLoader源代码
! Y) X, Y# {$ X$ f
0 n7 o4 a. S( |3. APP的编写) D% b6 _1 t l/ i
本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。
" @, _& G" E! z7 [$ [9 w1 J: g: ^- i% i" u
3 G6 D, F% [8 e3 }
流程图分析
; y' s4 S; w) _以我例程的App1为例:
" j F# `7 `) t先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;- R, j! \' ?2 K
打印版本信息, 方便查看不同的App版本;4 h. m1 B n+ v3 w/ s( `
本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
6 ]; P' J5 O5 J8 T+ T6 G1 b4 Q. Q" C; i) p
% J/ k$ `3 g* U! J5 c8 b3 ]' s. L8 i
# @) J- B/ ^6 n7 \2 d程序编写和分析
5 x# i, H6 W h所需STM32的资源有:
: F- W* o9 P( I/ W9 Z! q发送USART数据和printf重定向4 Z/ u- [+ c6 @3 P T3 Z
Flash的读写, {" c4 [6 Q0 n$ Y; h: T* a o/ O, b
串口的DMA收发0 O; X$ R6 e# y
YModem协议相关
2 E2 N# S, r/ y& z# t+ C3 uYmodem协议
- p5 y! F/ @* _/ J9 B( \ L& ^百度百科[Ymodem协议]
. b' ?4 G5 N r, P具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).
9 ?: X. V2 Z' F+ K% x4 uYmodem协议相关介绍可参考我的这篇教程 YModem介绍7 ^# z& S7 E' U% F. v+ K
k8 A n- L6 Y' a0 {代码分析
9 H1 ]1 Y/ y6 w& O/ H$ B代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
+ ?4 ~% V, n4 U0 f后面放了我的源代码, 详情请参考我的源代码.
6 ~" g4 x" k8 r4 f0 z8 U主函数添加修改向量表的指令
8 A$ C/ v/ C0 C; g% _" `' C. K, F$ I3 e" k6 l S
7 V9 D- K7 r) E$ D4 R6 k3 r& M% F! C" \6 q1 T
打印版本信息以及跳转指令* j1 k9 N& q! }8 l" u
6 Z7 w$ m4 ]6 U y7 [" N4 |3 \1 a, f6 ^+ N/ p/ O/ u/ n8 M
8 `. b! x! m, L
YModem相关的文件接收部分
( R: Y6 |1 E) F" h3 n- 2 F) I, j/ T# V, b3 r
- /**0 U* n5 a# F+ T1 q4 r
- * @bieaf YModem升级. z0 C0 Y* k- o5 Q
- *7 _7 ^2 U. p- K7 H. ?9 t7 q
- * @param none& \7 y& a" }$ K% ~8 g3 }/ J a& S6 a
- * @return none
. z) L! O: ]# q! P - */* h9 w' O! W8 X" ~5 U+ n' z& b
- void ymodem_fun(void)/ n0 ]! O6 X/ F
- {: N+ g1 e4 V- R# H3 B) A8 l, \/ p
- int i;* D- U8 g3 H! b% p& f
- if(Get_state()==TO_START)
& s' W" w1 F3 F& f3 A) b) y - {) S# o4 }- F% S( G2 \
- send_command(CCC);/ z2 _4 O% x1 h8 J, q# b
- HAL_Delay(1000);
% Q# [/ s1 v: I6 y, P2 v1 E - }
( h3 B$ J0 }7 Y! u5 x - if(Rx_Flag) // Receive flag W" O, s. @: y# g
- {) v7 m$ ?: U9 q! w6 [7 w
- Rx_Flag=0; // clean flag
% j" @- |. X9 w: \/ R( Z - ! g1 h; e) x3 m
- /* 拷贝 */
; i4 {! E* R; p- K; S - temp_len = Rx_Len;
/ Y; [% G3 g% { - for(i = 0; i < temp_len; i++)
8 U' E" K1 Y' q( N3 e0 c - {
( v9 y/ O: P! R& `6 G6 o - temp_buf[i] = Rx_Buf[i];
1 }* |9 }) l' A1 b$ [* r - }
# H5 G* O$ ^1 I9 B0 r; } M -
3 Y* U: O: @( z" m' m) z! Y9 y: x - switch(temp_buf[0])% h& A6 H N0 o( s9 c5 T
- {5 A% h, r( j, ]# c- I Q9 j; O7 T
- case SOH:///<数据包开始8 m+ ?1 M5 w3 D- C9 { u
- {: Q$ U/ N! m9 o) R: V
- static unsigned char data_state = 0;: {% h4 T" d8 p; K6 t" Q
- static unsigned int app2_size = 0;
0 Q3 X9 w* I Y# u# Z: D - if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验
4 J/ H- l; \: [" p& p; m" P - { ( Y5 k4 }; t6 D2 q" C* \' g+ V2 G
- if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始3 z2 q! d' P( h. P: L* T P
- {4 R% `4 B$ {+ T& z2 _! I+ R6 Y5 C
- printf("> Receive start...\r\n");* E: v+ L" f7 P
5 L9 c: e2 ]+ l' K- Set_state(TO_RECEIVE_DATA);
% {/ F" S7 i0 E- S - data_state = 0x01; # Q3 L4 w: Y) k2 [
- send_command(ACK);; g( v' I2 P* C! d! x; p
- send_command(CCC);
. Y4 m5 s. V0 ?8 M5 h+ e+ z - 3 \* B1 w4 |' N# t* i w6 g7 N4 i- K+ d
- /* 擦除App2 */ ' b ] e; U& P: r" x5 F
- Erase_page(Application_2_Addr, 40);
8 i8 ]1 S: w* E - }
. ~) Q% X5 \( Y - else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
' t% _7 v: u. m9 f0 d - {2 l4 s4 u) |- d1 t' ?3 j7 b9 k" h
- printf("> Receive end...\r\n");5 l" A' }- o9 V+ |# g
- 4 d& h" W4 i' O
- Set_Update_Down(); 3 {/ r: s1 @' d5 M O
- Set_state(TO_START);& ~4 s( ]2 ^! s* h0 A( I
- send_command(ACK);
" ^- o1 W& A( w$ u; ^8 i4 P - HAL_NVIC_SystemReset();3 o0 E, Q. B+ F# k. D& f' e
- } + ]" P* u0 x# X% F* X9 k8 V6 m0 p, d
- else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据
" N. t0 r3 @* ]+ X o - {
5 ?* L# v6 u1 {% q - printf("> Receive data bag:%d byte\r\n",data_state * 128);9 R/ [& e+ A7 H- [1 J% P9 N7 ]' X, b
-
" i! B1 a5 J5 y0 R - /* 烧录程序 *// f( m; X* N; \1 V& W* H" V
- WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);# l) I9 U! |+ K% C
- data_state++;
" v7 c3 e8 C# z. p) g - 8 T* g! l1 G) S# v! h5 t( U
- send_command(ACK); , G' X) [% v$ K& ^- D4 L, u
- }
9 F/ s2 O0 E" u3 _ - }0 n. ?: c- o9 A3 g; H! B
- else5 W4 O7 `( \& h
- {
* R# P$ M3 N! m$ n# [ - printf("> Notpass crc\r\n");# j' y/ \: W7 R6 @
- }# r+ F% H) F( @9 H/ ^$ U& U) _9 Z
-
! r# P+ c; _! E6 e- T& B - }break;
, p; K/ _) l& N0 V5 P) |# K/ n - case EOT://数据包开始
+ `5 ]0 P; `: q2 `/ B8 h+ ^! q - {5 A, `3 ~/ w& c: o2 c6 P9 d
- if(Get_state()==TO_RECEIVE_DATA)+ ^0 ?& Q: U' I. A( J1 n/ `
- {
& |. _9 r, }, `- Y6 z2 } - printf("> Receive EOT1...\r\n");, E/ X# b* Z) E. Q
- - W8 X9 x7 L, ?6 u# b# j
- Set_state(TO_RECEIVE_EOT2); & c" o8 m4 D% s+ E0 A ? T
- send_command(NACK);" e5 K& X9 |- o$ e+ w5 U0 Z
- }
1 x0 j7 G, h3 w8 ^1 o* N+ c - else if(Get_state()==TO_RECEIVE_EOT2)
% z7 l/ d& Y3 C/ |' c - {4 V6 e. H5 a* t
- printf("> Receive EOT2...\r\n");0 {" N+ [4 [" e$ G& G
- " n* V! U3 S( a4 i
- Set_state(TO_RECEIVE_END);
4 \- @- Y0 S1 E1 N$ {5 h - send_command(ACK);, U# j7 |8 |# f4 M
- send_command(CCC);4 f! B9 f# j: l" q/ r
- }
* R/ y8 {0 v+ v% L - else
6 ~ T) d& L) W v% i1 f% d: F! L - {
* F( @" M# n9 y9 c: ? - printf("> Receive EOT, But error...\r\n");! i0 k2 {+ A- k5 b* `- B7 N
- }3 V3 E c, m1 q0 p' Z3 Q
- }break;
+ q$ E, j6 q+ `8 q" m9 G# C+ L - }! w( _2 ~4 E- V R. G- d
- }; h5 P: s5 E$ c. ^% J0 S' ]0 q
- }
复制代码
: m8 A3 m! Q6 U# L4 Q( L# W其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.; {' F2 ]! _) \/ T( K5 h
) T! e# I$ }2 l& F+ Q/ }; b+ Z- n' Q6 M1 Y' _
4. 整体测试
/ a! l% H" `/ [3 Q% Z8 q5 D, Z本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。2 h' D) G) E6 r) z3 a! r/ V4 ?
6 |0 q/ E l" J
源代码
2 G9 D O, I; C- |$ {5 LBootLoader源代码和App1源代码可以在原作者的gitee获取
. p x. H# A$ K" R$ N
! U2 L9 {) G- t+ x代码的下载
7 H$ |/ }9 R/ p( ?由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
W( K* v9 \% w& t
6 h' h- K. E1 A% R8 q: y
& V5 @% ~& L( g9 o1 h5 k) G
1 j4 F" t8 X" z6 T/ x* g# `$ \) H
! Y( V) m+ S3 j4 SBootLoader的下载
. X7 ?' b$ {; t0 I3 R( tBootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
- w7 B& t; R# ], R* B/ O7 n按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)! u+ A5 ?! `" U. v
6 `- e) D) g& B3 X
" M* x5 p7 }7 \9 U* l1 W
& U9 {% `! v3 _1 |8 j7 I4 k% P* G) ]) R( i
烧录代码9 y; b3 |6 l( d$ z: g
运行, 通过串口1打印输出, 会看到以下打印消息
0 w9 U) m" J: v说明BootLoader已经成功运行
2 H7 [0 O3 V% ]1 b: C: S) F$ P; `/ Q# M- S4 j- _$ k+ y2 l3 \
! n/ r! L2 m: \& ~ 8 I- y& h) i3 e+ m! }% l7 S* c. v
% m5 G x3 F% s, ]" X: ~App1的下载
. j3 s1 X1 O+ k5 JApp1稍微复杂一点, 需要将代码的起始位置设置为0x08005000% R' F! B5 v% M; U
同时也要修改擦除方式为Erase Sectors, 见下图
: J* v. }* V# T$ L* y: v
3 d$ i. c; ]' Y& T* K. F
0 K& y5 C+ k1 L5 ^, ?% R
: G3 u4 k2 \; P* E3 S6 H
5 f- S( l6 f& e/ @9 y- A( R
: X9 m) z" R* U; ]; I1 Q2 a. C
: r) d2 L6 _ u1 W' \6 t
! j2 e0 w7 _, E$ Q& o9 ?5 @! {烧录代码, t& c' x/ M" Q4 r/ s7 ^' o
运行, 通过串口1打印输出, 会看到以下打印消息1 O. F$ Q/ F% I
说明BootLoader已经成功跳转到版本号为0.0.1的App1
& ^/ }. t }% ?% h# R! A8 e: C1 a
6 p; C5 W- \, d. F2 b% M$ T. ^2 v* a0 U1 M4 D* m7 P$ T+ U
1 k( i+ v z/ v6 d- @生成App2的.bin文件3 ?2 l; B9 f. v/ u, \! q2 g
Keil生成.bin文件1 v; b s% E7 n, J, ]" a, S4 e
/ t9 Z1 e7 i( e, ?6 x修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件# |* c' G0 X" c2 o }! i9 A( G
; c3 a7 o/ e' o" L
生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件
4 c1 E+ Z t% h- w$ C
) A& |( i( y3 W! J1 Z- H$ y
8 c2 I, N1 N$ \6 L6 u& P' g) Y3 _. f8 X- s$ k9 L9 O
$ x: s+ Y5 {& J% n0 z使用Xshell进行文件传输
+ [( L) w0 r, ^7 m6 ?# o打开Xshell
( u o3 J; }' Z/ x8 A代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
2 o& T4 h* O$ a3 n1 c+ a8 W0 _所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
5 s6 C. q/ H% s. ]0 y: n. r$ ~你会看到App的版本成功升级到0.0.2了.
* W! H' n1 x. O! r, ^1 m如果你到了这一步.: w1 `! a3 \# i6 E2 T9 w
那么恭喜你! 你已经能够使用在线升级了!7 h+ z5 z9 W* |( G! w- T g; E
" |! J' ^7 g/ g! s7 M8 E( S5. 总结& C! X6 a) y2 W2 Z4 {0 Z; m+ Y( Q
通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.
- R$ C% p" f; x: C, Y6 S9 k2 w! n j5 c. Z$ b
转载自: IOT全栈技术
- c: N. s [/ W6 p8 t( P如有侵权请联系删除
5 j) g$ [7 c/ }
9 q( Q1 J! @2 \5 o, V1 T4 y* M! l |