简介
0 v+ V- A3 u# ]" K9 X本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.
) K+ g5 b& C) w# o, r
7 P: L2 z) c' ]; S' t8 S- `( R3 k1 V2 ?4 L* i6 m
1. OTA基础知识
1 n/ i! a2 t& G; X# s1 f: Y7 Q什么是BootLoader?
5 k; r% ]: L, _! T/ JBootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.$ h1 L. p& _& h
% g! v; \5 K% ^4 D* `8 Y. K z2 |STM32中的程序在哪儿?/ v" f6 `7 l- x6 F
正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码& j0 ]6 }+ p" a1 {- E9 y! ?0 i
2 o3 A; I: s- O. k
# F9 M7 }& q) {1 n
1 \0 {5 k% i- _ z接下来就可以进入正题了.
" n# u* y! j' }" J
8 Z% P/ [- d7 z& n! f进行分区
: o V" Y; M- @7 ~" B既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:
, Y+ V- k5 s1 L: f/ `- @
; d0 |8 V7 k4 V# l
2 s: e' o$ z, ^9 }) D; a; S* z2 y* r( I! T6 I
$ \) v. L, X: l- y: n- P* L6 x* Y# E: {9 Y
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:+ ]" }2 J4 |% b+ S
BootLoader区存放启动代码
+ @5 v1 X- x/ S6 eApp1区存放应用代码
! _0 u; {9 y$ t% F+ SApp2区存放暂存的升级代码# [1 T& o+ ~/ B+ A" q; W
& n3 m* i$ p" o8 S: K/ p) c% d, p j0 L a: a( o
/ E& A9 T4 I; m( A
总体流程图
2 W0 d; G% r( x. R3 W: w先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.
; w1 ?8 p* j8 a! } r" x6 M2 ]7 B, j然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
+ J e& `9 e# G0 _: r2 M# s* n/ {* a7 n' k0 h8 S
在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:1 Y" P3 z1 [1 c/ I% u5 c: i7 j
9 O' X' p) S# g' p9 R/ v0 r9 A+ E) A: L
5 \8 p0 F7 f8 i/ ?: U( j3 N E
) ~* @0 E! m. e9 D% v( F8 @' I1 U4 o7 g' ?! r$ w
3 M3 `8 E5 H' L) n$ f, l [2. BootLoader的编写
( l9 [* V* B1 D5 ?6 x本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。
$ H8 v' h9 d2 k9 o
/ u+ ?7 Z8 n+ |( S: y; f流程图分析
+ F, ~7 N0 m5 s以我例程的BootLoader为例:
+ c) z: [5 `# M4 p6 X我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示% w% c S# b% ^
2 ]# M& _3 A7 N
" w! Q$ c+ a/ E; i0 {- K- O& e
) w- j, E7 a' L, ^' K程序编写和分析
! r% h* I0 X/ M* Z所需STM32的资源有:5 c, f& t4 c( q6 z: z5 a- a
发送USART数据和printf重定向
7 \$ ^8 L( r+ IFlash的读写
" W; U" T5 H! d% ~2 T程序跳转指令,可以参考如下代码:9 ~2 R1 u$ S5 U* ^1 }
' j& o: j. l7 ?' l6 K7 U5 K
: k0 x+ a/ F* o% W ]5 F! h
- /* 采用汇编设置栈的值 */
+ r0 @# K* y% k0 p! G, S - __asm void MSR_MSP (uint32_t ulAddr)
& n, L+ v! c! x% V; U o - {
- n7 f; h6 W" L% N! { - MSR MSP, r0 //设置Main Stack的值
/ O0 W% g+ [" c% J4 U/ X4 P - BX r14
/ {2 J% v$ @$ T9 z% C0 N7 S - }3 t( w& B5 W6 F
- 0 _1 h1 a' i5 z" }; T/ Q
- 5 L) y/ v4 \8 e9 K7 j" K
- /* 程序跳转函数 */1 Y0 W2 ^) f# k6 N& Y( ]* v2 u
- typedef void (*Jump_Fun)(void);
2 V* X; r3 W" V P - void IAP_ExecuteApp (uint32_t App_Addr)3 c- |4 D/ C6 e" y/ H
- {. f- P# x: Y. a) G1 p
- Jump_Fun JumpToApp;& L7 d3 {( D; R+ ~- h' l* u/ Z
5 M7 f. L: ~0 m; W7 @- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
0 a+ ~0 u% f$ f3 u0 f - {
9 W" L$ u- v9 k, Y6 ]9 W6 S7 C$ D - JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
9 a! S' V6 Y# H% P6 x" W8 N - MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址). x& Q0 b6 z0 l
- JumpToApp(); //跳转到APP./ @0 l, Y J6 K! w
- }
" r* o7 X0 A: g& y# o - }
复制代码 9 N3 w" s0 L$ K9 f3 m# c
在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);' I. }* c$ g3 t+ K! s* q) O
其他的代码请参考BootLoader源代码
! N. a ^7 J2 H8 Y: y2 L# S5 R) ]( k4 j' x! [
3. APP的编写8 j. V2 i; @2 p' S* A
本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。
) [7 m1 y" s7 D6 ]- V: z$ k; h$ K
; X8 w: W7 P8 e2 z3 ]2 g
+ }9 G) f% D' t4 L流程图分析
$ ]5 |$ |4 k8 G& X. K以我例程的App1为例:
- m1 b9 Y2 ~# s4 m先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;
; b) f/ a$ P; B打印版本信息, 方便查看不同的App版本;
1 t% F1 S( o$ [5 h9 a: K" v9 _ G7 [本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:) j2 @8 F1 m1 }9 {' P0 `
$ J: B! Q& L0 f' a, q9 S
9 ?% E" n" F- W# r) @; D7 W) w' }% g" F; k4 { S* g/ f
程序编写和分析
. B0 M: }% J$ k; ~所需STM32的资源有:0 J' M. w7 ~# c1 s6 t
发送USART数据和printf重定向
$ X* h) j5 J$ Y: V+ IFlash的读写
6 n' V! H4 F$ F, \3 `7 s串口的DMA收发6 V; ?7 d1 X' |0 w
YModem协议相关$ _8 o* x) Z# j4 K: X( U7 ^
Ymodem协议% Y# C; }$ d- [5 g& A4 ]6 |
百度百科[Ymodem协议]6 e- H& {+ z0 k; P9 F- k
具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).! `' H4 ^. Z( B' W
Ymodem协议相关介绍可参考我的这篇教程 YModem介绍$ V6 ?9 Y( O/ v; _
2 k ]- B' {# g代码分析0 C2 |9 X& w( @5 e" e7 Y
代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
- B/ G- ^8 f$ j4 z: p% W( z后面放了我的源代码, 详情请参考我的源代码.
) c# y. y) X: n; w# _主函数添加修改向量表的指令/ @! K) z1 z. S( W! S
. t6 a) d! W$ W* v6 c
% ]3 a( Q4 Q' u: q) k4 Q
; P# F2 ]9 T0 e" n6 I8 U# S
打印版本信息以及跳转指令4 o3 h/ p8 b2 h( F. M
# J! Q% b# I- S- z I2 j+ k
' F- c2 F _& ]( ]. A; J- N5 w3 N) |3 a. a! v6 V
YModem相关的文件接收部分: o; O4 o6 l+ b, a8 X
3 K; ]% B9 d* G2 S! r) g( f9 N) \$ _) Y- /**9 n; y( \. l) j4 ^. \- z$ t9 Z
- * @bieaf YModem升级
+ r0 k9 G- F3 ]4 }+ m - *
1 ?( |+ E4 F i `. } - * @param none, p. o7 @% F' ?0 N/ K' M+ X
- * @return none
/ C' u9 B% m: y5 ?+ g/ s6 Q - *// Z r1 e- R6 Y- y
- void ymodem_fun(void)
& n) n+ K c- T$ Z- S - {
: s3 b2 G" L6 _# j6 V' B: ?% k - int i;
& \! V0 z6 ?2 G - if(Get_state()==TO_START)5 b5 i; @1 a: U( X0 u
- {
& \' U& y5 I% [, H+ W$ F- V - send_command(CCC);
6 B8 V/ |5 R8 i I \" m0 Z& X - HAL_Delay(1000);
9 F, ]; p6 b5 o, Z8 w3 f' w( P - }
4 A! y# ^$ z6 C6 |" ]$ q - if(Rx_Flag) // Receive flag! z5 ?) I7 M0 U; Q* z
- {
& W7 @$ V6 k2 Z* S! W, H8 M) I - Rx_Flag=0; // clean flag
) I& v w8 K* E" S7 G! N -
. x! f* B" E$ D& w" J; d - /* 拷贝 */
9 f5 A) v, q8 X/ l8 T" S8 s - temp_len = Rx_Len;
" j- A3 [, D4 J - for(i = 0; i < temp_len; i++)
% H' I8 S- h v1 K+ y6 M - {
$ D& q) V# [; E6 z, C5 o8 F" C - temp_buf[i] = Rx_Buf[i];- t. b! |: f# ~0 N4 h
- }$ E* Z! i5 R; m: L$ y4 I% S
-
' m0 X* I% V# `) e4 G' c - switch(temp_buf[0])
+ G( [& d% J" r" s( b& z - {
6 g0 s8 ^& z/ T' O& p - case SOH:///<数据包开始6 l+ o" Z6 p U2 d
- {5 m s2 W. u& Z
- static unsigned char data_state = 0;) z9 u* J& B C6 S" J
- static unsigned int app2_size = 0;4 m* t# T7 |* D' b3 W9 G6 v/ {
- if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验
" i; x, t* ~+ A% z, k2 X - {
& [0 e, Q" t4 R+ v# B4 e - if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始
; x8 v0 l4 M. \" h - {
: u+ s2 s7 g# ?# O/ t# n' U - printf("> Receive start...\r\n");$ f j( c$ @) k$ \$ _% X
- ' }2 V% y3 t! _; k# W! x% Q: I! J
- Set_state(TO_RECEIVE_DATA);; \3 I5 C2 q2 V9 F& C
- data_state = 0x01;
- h4 ~# I w7 g0 L/ l5 j' Q - send_command(ACK);. K0 d V3 p& H; m3 _( K
- send_command(CCC);
1 W+ @# L2 J2 w- a' L - 4 y6 _* k9 g) }0 _! j
- /* 擦除App2 */
& U% k- W% _ w: h) T2 s - Erase_page(Application_2_Addr, 40);
5 g3 A/ o0 U4 n# w# Z4 ^ - }
. p0 ~/ y% a g3 ^ - else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
; x+ y/ r+ ?+ Y; B- [: K- A, S - {' q# E5 Z5 Z9 I, R4 W7 ]4 e
- printf("> Receive end...\r\n");* `9 o4 x$ s7 Q$ \
- ! E" ~, v& y' \& n# o
- Set_Update_Down();
& y2 l5 g5 n8 Z [3 d* N! x# _' ^ - Set_state(TO_START);
$ ]$ l- J8 U: [ P9 {- o - send_command(ACK);
9 L. U! k# a" A n, P( K0 `9 D - HAL_NVIC_SystemReset();
/ N) O4 ?( S6 I" B( ?: @ - } 5 q9 U7 v0 x" f2 Y
- else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据+ ~: R i& J6 `3 g- ^' E
- {
5 g7 X1 Z- M1 ]( ~6 b# ]: r# R7 ` - printf("> Receive data bag:%d byte\r\n",data_state * 128);
; \# `. u$ ~; k7 G/ h# h) m, } -
" ~& H% o! m1 y. T' {; @& { - /* 烧录程序 */( i6 K7 @6 T( N' m
- WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);# I5 X$ c5 F* i/ {
- data_state++;. g- Z0 r9 D7 |; ]3 P. E; ^
-
2 d* R- p9 Y+ e- V - send_command(ACK); |1 \" T3 G5 s: I# q5 r3 b
- }
! {1 h9 i' P" | - }
' j; ^( q) e4 y. H - else/ a1 k3 O. Y/ A+ c6 d
- {
/ b. q$ A1 k3 G% w6 ~ {, o - printf("> Notpass crc\r\n");
) P8 x# k7 r( V, G - }
c6 X w' M5 R0 b' N) c -
) }9 g, I; M3 ]1 `! T- E$ p8 Y - }break;
, V3 \- @. Z8 K. T ]; S - case EOT://数据包开始+ H/ ~! k( G8 B+ B3 N( Q
- {
( P* o. w9 R$ I" ? - if(Get_state()==TO_RECEIVE_DATA)2 ~( E) g# u2 J+ @
- {
9 \4 M6 Z/ t/ P8 p- w2 w1 g - printf("> Receive EOT1...\r\n");
, c) p# q @* S - 4 O$ W S+ @& U( ~: N% ?8 V
- Set_state(TO_RECEIVE_EOT2);
& V+ r# p. @) k9 N - send_command(NACK);
( k7 B$ r/ M A9 g# g* T - }
; r7 V- s7 s# ?' a H - else if(Get_state()==TO_RECEIVE_EOT2)
& t2 J9 V% L5 N; r - {( F$ z% i( G* c$ |1 u* B! ?7 L: Y
- printf("> Receive EOT2...\r\n");+ U& T* q, ~0 c" G x: u5 J
- " ]1 v& V( r/ x. W! m+ ?
- Set_state(TO_RECEIVE_END); ; }3 n$ P, r" r, E$ n
- send_command(ACK);
7 A' ?" n( x/ n/ R - send_command(CCC);1 ?) @5 j( k( N/ y
- }
4 |* _% c' z0 s- h: @- r - else
- f& v4 ^0 Q0 f7 p/ ]3 C$ j - {
. F& w$ E6 r0 c/ x3 c/ M+ i: T* L - printf("> Receive EOT, But error...\r\n");
p: Y9 M' s, ?/ n - }: @5 |# }# ]7 E# S
- }break; % d8 s# j! r3 K! j/ q1 V
- }# _$ R. i$ [& V% x M
- }
0 o* }: s1 a2 w3 I( ~/ O - }
复制代码 & ?0 J7 Y- V& I. }) ]2 d" {6 I3 K
其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.( k' {9 \) ^. [. _$ q9 `& I
# k+ y2 ?. q( ^. e
; h2 p0 L, n/ e4 m9 E2 _4. 整体测试
1 }* u7 H/ Y! v: B8 }本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。
3 h. |0 g+ U, Y* K2 W$ ]
3 U( L. E8 s g6 N源代码
9 }7 S0 l/ p) r: @, uBootLoader源代码和App1源代码可以在原作者的gitee获取
1 ^4 u( K" @ |2 v
) y$ [+ i+ [( k) |& I1 ^* H/ Z代码的下载
, q' V. Q9 ^( H$ l由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
% k% r7 V5 g. S! U& {/ y" j. O; Q! N' j
; y* J! e+ q2 x' y
9 \8 e, z& @2 j9 q
x' v# h& r5 }- M, D! iBootLoader的下载
! R( K9 j: e' G3 c) j( V( KBootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置! {% ~" q- [9 V m
按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
1 v9 P# z3 u$ u( ~
* w6 G8 Q$ f. Y. f$ l
% n2 M N& E( G3 v0 C1 ~# @
. Y: F% W3 y2 \6 x! m
' O& {4 E' ^" g& @" _) L! h烧录代码" m, \# `- E ?6 Z6 n" p
运行, 通过串口1打印输出, 会看到以下打印消息 p1 a8 w7 S f! u% D" y
说明BootLoader已经成功运行3 o& ^8 z( y& z h
% J- L0 k3 j" |4 Y, L
1 S; ^$ y0 M' i- U/ K/ g0 P# h # s t3 E- ]" b; l( `7 s
5 u- z6 d) d; h4 a0 e) cApp1的下载! y a9 m$ w3 |# t$ M% M( R
App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
: p# a8 U3 T1 H. Z1 a* k& H3 d同时也要修改擦除方式为Erase Sectors, 见下图$ h k& s& A* v/ Z$ ^1 x" d% z
% S1 \0 Z9 U( V
4 g9 { E* |; p 2 {: l& a1 M: w( k/ H- t
( E' R5 Y {3 ^7 c: X
+ J! `+ R2 _ D8 h ) }7 Y8 L+ b2 U* `/ q2 |
) j: k2 E9 Y) p5 o9 K4 ]
烧录代码# Y1 q8 Y. i' o
运行, 通过串口1打印输出, 会看到以下打印消息
/ m2 y! Q$ [& L5 _说明BootLoader已经成功跳转到版本号为0.0.1的App1
, p8 U: b0 U! ]: M( V& P
/ u) z q) @! h, y1 @6 l; ?. n6 b+ x0 E- z5 ^
( D9 k+ ~( y# A! }
生成App2的.bin文件 Q. k& J/ n5 L3 u! ?) @0 O$ B
Keil生成.bin文件8 J8 S- v# @& |8 K7 F2 z$ \ s* s
" p3 w. z4 n {
修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
- q3 @8 h/ l% N# u9 ^
( _4 X; W* \. l5 ]* ?; K7 B生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件- ?5 c" E' d/ g+ i5 U. r2 M
9 h5 r1 W1 |# R7 D5 w& Z5 h/ }; b. n! k7 w
9 p2 v% C/ o# t$ z
: z4 F# C, I2 }! U5 O7 ~8 Q# L# g! }
使用Xshell进行文件传输6 R: q5 k6 z: r7 X& v
打开Xshell
- E# p; Z* b3 L- O# p代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的1 E2 ~ z* P+ Q* h3 R. U" t9 i0 m n9 w
所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息. {. H# ~! f, v1 B8 Y; f" F) q) }
你会看到App的版本成功升级到0.0.2了.
3 J( T. S' e n" q如果你到了这一步.8 K$ o* n% e6 y7 q# Z7 v, G- L. Q
那么恭喜你! 你已经能够使用在线升级了!
' V, i F/ i: L/ I4 y' U E+ w# ~. l
5. 总结
7 l2 t4 z7 ~( I4 r$ @通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.
1 z/ y+ h/ [- Z% r$ N) d; c) y/ O1 h7 h
转载自: IOT全栈技术
* N5 j q: N! Y, h& v5 i( W* q如有侵权请联系删除
2 c6 @! a8 X7 q4 k% U4 [* Y) F0 N
8 o( n; d1 a/ ` |
你好,有程序源码或者链接吗