简介
/ d$ @% N0 f! h" [本文主要讲解在线升级IAP的基础知识, 主要是针对IAP 从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对在线升级的认识.
1 b8 c- h! r: b6 H1 t+ V, {3 ~7 o, y" \' |0 L! R: I
1. 在线升级知识
. O4 t) `. T6 [3 [9 `7 t- Y什么是BootLoader?
0 ^3 o# w) R+ @% k1 U1 C3 }, N( o5 aBootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.4 Y: x3 K8 J; c
4 H C, P( z, b: X
STM32中的程序在哪儿?" S4 ` p/ i6 Q. C2 Y0 g" b/ w' o
正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码
n6 w k8 f# E. M2 O3 h7 P7 A: ]/ h8 H# @
" [: k( c! Z Y2 A% A
, ?) L9 N7 P1 x8 n接下来就可以进入正题了.
% @& t# A4 ~) w+ G0 D9 n. P7 w t2 H7 x8 ^ |# b( u/ e
进行分区
" ~+ {5 A4 z9 X: a5 G; {- n既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:
6 q' ^, j! P: U0 x) ~5 M( d* w9 K# C+ X1 D* W2 |2 N6 H
+ t! @! u" k% l, \: [4 e7 C
: R9 h% r! ~8 @2 K
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:9 ], N: g/ T2 U
BootLoader区存放启动代码
& k2 {2 S0 r* a4 s. FApp1区存放应用代码. c+ l5 }: z- X' y0 Q
App2区存放暂存的升级代码
, Z3 K8 Z+ u8 ?1 L1 a
% M1 v+ n! Q8 A, @4 s! D- y) B2 S! q
( P$ t! c5 L) j' d. C7 j
# U! S$ s3 g5 b2 ^总体流程图
8 h% \( N' i4 c& W* ?先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.8 K) @8 p; U9 A; x; k( F. f# d. a
$ C8 }/ b: f6 M/ }. h然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
: t- \3 S) w. A0 x8 n/ J8 f1 Q5 R1 A, i7 x- @/ p! }$ p: c9 z
在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:; \$ z4 E) _& [ @! @( b
' p* c: B0 J: u7 A, Y
- o2 z2 L: N8 G! Z& A3 T6 ~
) k. A5 Y: m+ W# R( C( J
2. BootLoader的编写$ t% d6 s4 x0 V! v6 b
本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。2 j% q4 F x3 O5 r! h
$ q; s/ @4 H: W9 X {. H; E* e2 c流程图分析! V3 D% K) `/ n f; D: C! I' T7 x
以我例程的BootLoader为例:
' J& y8 X s# G+ A/ K2 c; Z' P6 ?1 {5 q8 k1 S
我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
7 [5 h2 ` y1 E% l5 [+ L1 B2 o6 k4 Q- G) `. D
) ]" t: N9 Z( z
) I( u1 J3 Y0 Q. k程序编写和分析
' T- t: @8 D l) l# G: N+ n+ s所需STM32的资源有:
' I" A% R& H" p( D, p0 m发送USART数据和printf重定向2 R" P9 I3 g# y9 j" y
Flash的读写
/ m' b. X( s7 W6 V3 k* e4 ~/ g" Q# V程序跳转指令,可以参考如下代码:
, D& E# b, c& H3 ^; }0 j/ W# |" s; g8 ?
- 1/* 采用汇编设置栈的值 */2 D5 z# t( t( M) l" I
- 2__asm void MSR_MSP (uint32_t ulAddr)+ H9 I' I. [- Z% ~2 W# i6 Y
- 3{
0 i; ?* }, ]0 Y) N( k1 r+ Q3 q - 4 MSR MSP, r0 //设置Main Stack的值) P, z8 V& I' q; [4 `0 V# v
- 5 BX r14# `0 ]. e# \6 h- L$ W0 n
- 6}: o: X2 \7 r' l4 y$ ?* T" i+ |
- 7
; h9 N P1 S- j% l$ i - 8
8 ^$ ?' I) z& x0 r8 e$ w6 l+ j - 9/* 程序跳转函数 */
# N( i( n2 t! J - 10typedef void (*Jump_Fun)(void);
+ H3 G1 u5 O8 i - 11void IAP_ExecuteApp (uint32_t App_Addr)
) B/ f% M/ f+ y% C1 j1 [' s4 E - 12{
6 P# J: v" H1 F- k$ y0 w% v - 13 Jump_Fun JumpToApp;
/ b. v8 z7 I5 o, v P4 j - 14: x/ Q5 o3 _1 Y# @3 Q
- 15 if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.- G# s; s1 ?4 j4 l6 k; K! ?' b
- 16 {
+ D4 T5 K# g& Z0 I8 ^4 Y5 H% A - 17 JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
; |% ?! l6 X: o$ v0 e - 18 MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)& R4 T' |$ u0 m4 b& t8 c* G
- 19 JumpToApp(); //跳转到APP.) z5 N' x1 M" Z! A& w8 l# H9 u
- 20 }: T o. I9 K- L5 U
- 21}
复制代码 * u" [3 f; ?8 C2 H. Y w/ Z& f( q) w
在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);
, g3 E+ f) D5 e/ W- p# e
9 r7 `# m" N5 Q& ], j其他的代码请参考BootLoader源代码8 i6 Q3 T% `+ H4 w' t
3 ]7 _! l: X0 `! V: v7 j2 s3. APP的编写
3 N c# M) n/ e& Z _" K/ v本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。5 B3 s1 H9 U, z6 R+ m
3 G7 h3 D8 X, _& X流程图分析
5 v2 C' T& b T5 a& g以我例程的App1为例:
+ j& e+ X/ \8 I% \: ~先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;
% O7 a$ w1 N. D* O- `. _打印版本信息, 方便查看不同的App版本; i% u# Q) W" `& Q4 x5 g
本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
, c" q& X7 v5 A* W
- A% }- s3 H. h3 U
( X! z. H7 K2 ^" `9 T2 s) Q7 r
: I. f8 l3 M: U9 B- G* a程序编写和分析
2 H: }" E1 p% Q* X* P' ^- I所需STM32的资源有:% e" r5 V% A1 H
发送USART数据和printf重定向+ a0 z( L8 M. J7 a8 F- q
Flash的读写
+ W* X* }2 p+ [串口的DMA收发! {6 T4 H- h: ~* _
YModem协议相关
4 f& C4 g4 y1 ~5 q0 ^5 q7 z4 y& G$ s6 B# c6 s; Z
Ymodem协议# B% T1 N3 q/ J7 k
百度百科[Ymodem协议]2 z+ d( D" d: p+ O2 f& M7 U/ t
具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).7 N- s# n4 @$ i0 i# j
Ymodem协议相关介绍可参考我的这篇教程 YModem介绍
3 @1 \7 w( w5 u8 s$ g. f8 ^
; ^/ G6 c: F$ |( f代码分析8 Z6 S( S2 A7 ?; ]) T4 a
代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
. u+ u, Z* d! F: v/ m0 w后面放了我的源代码, 详情请参考我的源代码.
& F' R# W4 h" p/ a0 N2 O) E4 @主函数添加修改向量表的指令& ^7 V. J) V4 p9 ?/ W
; i# j; W, N/ w1 @7 C+ W) ^3 U: f" q
" H) \* \$ W1 x0 M# W+ e
4 K# I4 z5 | [打印版本信息以及跳转指令
3 g1 a$ s$ V% N# y UYModem相关的文件接收部分' O0 `% R0 o/ \! @0 x7 Q
- 1/**. Z y7 `- ~9 v8 l3 e6 y/ H
- 2 * @bieaf YModem升级
- r# V8 @; I9 S - 3 *# @7 F, C0 t! L% e* ?# M* A( G
- 4 * @param none
/ t3 \3 }5 x9 F' n' I* u4 l - 5 * @return none# z7 B: U& A2 P0 U
- 6 */
% z- l/ ]& ]+ M v: m4 B6 _, G - 7void ymodem_fun(void)
4 n$ L1 {2 i' u) d- [ - 8{; M* w" A) x" o7 p. ?* E3 k3 q
- 9 int i;
9 l% d+ g( S. @# I% Y7 i3 @0 ^ - 10 if(Get_state()==TO_START)7 w# U' m! q' o# u
- 11 {
/ X" t" D3 c) m6 L - 12 send_command(CCC);
# l8 K" j$ ^4 j/ Q - 13 HAL_Delay(1000);+ w( a7 [5 K! O- Z, U) v2 G4 j
- 14 }
& x3 a9 d Y% P0 {# ]' {% p - 15 if(Rx_Flag) // Receive flag
# Z( S. }# W; y7 S - 16 {' l9 z8 W: {% ^% X# q
- 17 Rx_Flag=0; // clean flag" z; J( W4 M+ i6 n+ [* f
- 18- A7 s0 _. \4 x1 ^
- 19 /* 拷贝 */7 h9 Z! n& K8 J
- 20 temp_len = Rx_Len;. t9 R9 g1 _9 u6 I$ E+ R- [
- 21 for(i = 0; i < temp_len; i++)
- ~ g6 |- ?0 n8 g - 22 {8 c" _% f7 N1 c" D* M# L
- 23 temp_buf= Rx_Buf;
8 i; I1 v4 P ?1 I% X - 24 }" V( L1 K1 }% e5 H! [
- 25
0 y: Z5 w4 h& O* h2 M - 26 switch(temp_buf[0])
* A4 H& S$ w: T - 27 {* H) |& A2 B( P1 ?
- 28 case SOH:///<数据包开始" w! l" t/ k6 S
- 29 {
" S) k7 z6 C6 N7 a) r6 |/ h. J* S - 30 static unsigned char data_state = 0;; [8 @! x0 r R$ P: ^2 Q
- 31 static unsigned int app2_size = 0;
; J8 Z1 b" l# n+ d/ ~' X - 32 if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验
$ D% R( p4 d1 L3 ]8 ]- A - 33 { ; \/ a! \1 s# t8 U! ]8 ^
- 34 if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始
, i# [' k5 [. t5 B$ E+ p: q5 n! ? - 35 {4 P- P6 l& e0 P8 G
- 36 printf("> Receive start...\r\n");* u9 M. [/ X+ {% @+ l
- 37
3 W. ^- E' n, m% G/ c: e4 z - 38 Set_state(TO_RECEIVE_DATA);
. T* T% v% S. A) W# j - 39 data_state = 0x01;
! C/ N0 l" P0 [0 e( U - 40 send_command(ACK);
9 p' T8 X/ j+ D9 a# o - 41 send_command(CCC);
. A M- I' s& j' ]# X) \ - 42
, {: F0 f* I& A' x# }, j# m4 h - 43 /* 擦除App2 */
w5 H1 h; @7 v% s4 ?. z - 44 Erase_page(Application_2_Addr, 40);4 e. I ] h+ S1 O/ |* } m
- 45 }
7 z1 I9 Z) J* v, e - 46 else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
% L8 n; l+ ?7 ^; q& r8 Z - 47 {, q' q$ F( d4 \3 \6 p
- 48 printf("> Receive end...\r\n");
5 I% D& p7 z' r+ s! x - 49
# ^' @; l8 F# x9 [' [0 a6 I i) T - 50 Set_Update_Down();
% h* u8 B' U7 j+ r4 D - 51 Set_state(TO_START);1 |+ ?9 ]9 M! U* J
- 52 send_command(ACK);$ |8 e" |- v# S z! A6 q, T
- 53 HAL_NVIC_SystemReset();6 H0 d9 g. Y" ]8 _
- 54 } - z, ?; J! F6 D$ I' z1 a8 Z; L8 X
- 55 else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据7 |9 |2 X# p7 E2 R5 [/ o- P6 S
- 56 {/ Z5 D: t" s9 U6 @
- 57 printf("> Receive data bag:%d byte\r\n",data_state * 128);) a# i; z5 f l% J4 j5 j2 D1 O; a
- 58# j, |/ s) \2 o9 b6 T' V# l; X6 v
- 59 /* 烧录程序 */8 |# ^( c+ y7 O2 i9 C5 a9 y
- 60 WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
1 K. O. s1 h2 p" e: `. ~1 X - 61 data_state++;7 y8 u% K( G: B2 m2 i
- 62
2 Y5 F A. C/ F6 o' K - 63 send_command(ACK);
^( q1 e5 y3 H) i) r8 B0 F - 64 }
4 Z& H- k# M. S) b) _ - 65 }/ K# D- H/ k. |" E
- 66 else, F, V; L$ H, J8 Y0 O0 _
- 67 {
+ o: e$ ~; ?1 G9 `$ {3 Y) B - 68 printf("> Notpass crc\r\n");
5 ]: V' {" p* W- q! S3 k4 ]) F d1 i - 69 }0 ~( q) N$ {+ @9 k% V
- 70% F1 P5 k, M$ [2 F
- 71 }break;
8 K# f' r. ], \% p: U/ ? - 72 case EOT://数据包开始
9 @5 M, F" N, k. X - 73 {; O, H# q! N& d! ]$ A% d, E
- 74 if(Get_state()==TO_RECEIVE_DATA)
1 N2 h2 W0 W: @. s) w- |. M/ E- H - 75 {4 q. g2 T! T4 U* l1 Q
- 76 printf("> Receive EOT1...\r\n");# h- H7 ~8 K9 r1 @) _& H
- 77! s' w4 }. I m; f3 l" \
- 78 Set_state(TO_RECEIVE_EOT2); # v! u8 H- A' l
- 79 send_command(NACK);/ `6 B! B% A! I$ T. ], _
- 80 }
N# i# J% A4 U5 t. u- u6 m/ N - 81 else if(Get_state()==TO_RECEIVE_EOT2)
* c! G+ M/ n/ S; w$ M - 82 {
! @) D: ?2 D2 h" l - 83 printf("> Receive EOT2...\r\n");
9 o- x8 F+ u& z - 84, Z/ q. K1 _, k- u% L
- 85 Set_state(TO_RECEIVE_END);
6 V$ O( v5 O/ e- y0 o - 86 send_command(ACK);
; w9 T' h8 s) V( R( v7 {7 R0 l - 87 send_command(CCC);
6 i$ m$ s$ @: {# m: c - 88 }( s6 |! J# D8 m2 _
- 89 else7 V, r( V9 z" j
- 90 {
8 ^# \6 e( ~% z2 A: \8 r, t H - 91 printf("> Receive EOT, But error...\r\n");
( @$ w) s5 [3 U$ M( e - 92 }4 X. Y% f6 x3 _1 c* A9 K+ p
- 93 }break; 3 m6 ~* v, K& E6 ~- U
- 94 }7 q/ u0 ~, ?& s: N! n7 S2 ~
- 95 }. ?: P' L$ s9 z9 v; x1 O# }" Z
- 96}
复制代码 ' a8 e: U: C1 F& R8 z; A
其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.0 i, H7 U/ G/ ^$ K) X
( O: j9 z8 ^) f; O4. 整体测试, w5 l! g+ E4 V* K/ e: L* l0 D
本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。: V8 F2 `; T7 k. _/ q
5 c3 p( ?- d3 v5 J2 F$ m代码的下载
- x* x2 f6 |, |' D0 T/ y+ L由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
* V( ^! H, V3 T6 U! n& V( t# h( \5 I; `* w! [+ L5 g
4 N$ I, r+ x" n% s) o$ b0 b/ K
9 Y" l9 [; o6 V2 v" T3 _* DBootLoader的下载: h3 `* _' i; Y/ e, C) |
BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置% v+ q* V; p. y U
按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
( D, M1 z/ y' y0 X5 ~; V9 a: [; @ x$ C& O
0 y5 L- j* X' S9 ~
$ |' `, @1 O8 u/ k+ \+ |烧录代码
: v/ x Q% X3 Z% j- ^运行, 通过串口1打印输出, 会看到以下打印消息
7 e! J; T3 Z3 V8 `说明BootLoader已经成功运行
: ?& N3 i5 P- m; c5 q6 f
; i, }# `5 U! S$ N9 v& u
# P& r, N( z& d
- u5 Z9 s1 N: D( v- B/ q4 n: b) iApp1的下载
* L; n% f7 X5 ?. a8 W: @App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000# d9 G& i; k% ]3 a, S, h! a0 j+ ^
同时也要修改擦除方式为Erase Sectors, 见下图) A5 @$ n5 J$ r% ~/ t3 {
/ m l2 S/ c- a( s2 _
. L% s6 s6 z3 c
& ]: f! Y/ P, i0 X+ W" N* A6 u/ }
$ W% u6 `3 b4 J; s3 E5 P. O3 E+ r1 u% H5 Z
烧录代码
! I! {: L' C. ]运行, 通过串口1打印输出, 会看到以下打印消息* m/ W) P6 n" C: p2 F+ M( _
说明BootLoader已经成功跳转到版本号为0.0.1的App13 s4 K( C) \, B
! _0 F2 f* w, q
+ i% Y. B' m% {' s
: }9 x6 I% R( C3 P& w& H生成App2的.bin文件
% [) T9 c" Y) D! b* _8 H5 ^$ ?Keil如何生成.bin文件, 请参考这篇博文 Keil如何生成.bin文件
5 P0 N3 m1 Z. b& N修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
+ m/ P0 O( m$ H7 B7 i, ^- r生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件/ @( S9 D$ J) t# {6 B) D
* x; w" y1 ?! S6 Z% U, e
0 z: t# h: P& ~. _4 }0 z
% q! c4 h! F+ A: O) M
使用Xshell进行文件传输
( x. Z/ ]: Z! k9 F" J/ e+ ~打开Xshell
2 r1 m0 s8 c1 K0 \! b. o7 x代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
) r$ b$ g/ n9 B+ W: N所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
$ W" k: K4 B% B$ }你会看到App的版本成功升级到0.0.2了.( ?2 h- x" ^; X. Y6 t" Y8 C @
如果你到了这一步.5 c/ D! p) L4 ^9 w& p6 }
那么恭喜你! 你已经能够使用在线升级了!1 \# @" \) Z9 c3 V0 I
" \6 v- \+ e& {& f) j
5. 总结
6 W8 C- j) b2 M通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多。
+ ~: `4 z7 `" q8 J! P$ X8 g) Q# B4 q6 P0 H7 }" S+ M5 s
转载自:单片机与嵌入式
' G2 [2 C& R4 V3 N+ z# `% S; u8 L$ O/ J0 _/ ^; _
|