|
简介 本文主要讲解在线升级IAP的基础知识, 主要是针对IAP 从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对在线升级的认识. % @5 @1 g! D7 Y! {$ X+ V
1. 在线升级知识什么是BootLoader?BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.
7 X' n/ n( h% t3 s STM32中的程序在哪儿?正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码 9 }2 g0 y7 S7 @* W" [3 b. t
接下来就可以进入正题了.
/ j2 x1 _$ _+ L, s0 K 进行分区既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:
' C9 r( J+ _/ D. V% H+ ~, M
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图: BootLoader区存放启动代码 App1区存放应用代码 App2区存放暂存的升级代码
' s$ Y ?7 H1 i, N
0 C3 }+ n6 \/ y5 ~8 h
' h8 u1 C7 ^; p3 s! B6 J& ^ 总体流程图先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序. 然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序. 在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示: ' `$ k. G! Q3 y5 A a' E
" J9 d4 {) l# a8 h; L
' g: u* {5 g: s- B. A1 H
2. BootLoader的编写本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。 流程图分析以我例程的BootLoader为例: 我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
' K$ ^( S3 m& _+ ~( a1 F
, T) G0 ^7 Z8 T* f5 P5 V1 b H 程序编写和分析所需STM32的资源有: 发送USART数据和printf重定向 Flash的读写 程序跳转指令,可以参考如下代码: * S; {! w4 q$ G! h+ l
i6 X+ C$ ?- c2 L' E, g5 ~
- 1/* 采用汇编设置栈的值 */& x# K" e. E: q$ h, c( u2 Q
- 2__asm void MSR_MSP (uint32_t ulAddr)
/ b) A5 m: [6 D - 3{
! \# _# o* S' C* h - 4 MSR MSP, r0 //设置Main Stack的值
5 J% X% I0 A% \5 C6 W3 e6 s - 5 BX r14" o- p4 C1 Q; A! c8 N
- 6}8 x/ e# A5 l7 ?# u
- 7
8 s6 ?3 l' ^3 T4 z; l T$ e, |) I9 s - 85 h- g# U) B$ T2 X
- 9/* 程序跳转函数 */
# a4 O M2 b$ |1 u. N0 r" v - 10typedef void (*Jump_Fun)(void);6 C( F# }/ j# Q! l( u$ R' a
- 11void IAP_ExecuteApp (uint32_t App_Addr)
5 D: g) W v9 l" q - 12{6 k) k, L3 {& v5 y
- 13 Jump_Fun JumpToApp;- d9 [+ g: T* b8 m$ @! l
- 14
& U& w$ S _2 }& l; y$ \+ x5 Y - 15 if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.. w: l" o1 @! s5 R% b
- 16 {
l: |! Z/ m. H. s% H0 y - 17 JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
# x% [" R7 b: }; I8 X - 18 MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)% Y! B& {" p( L5 o+ \( m
- 19 JumpToApp(); //跳转到APP./ X/ r3 s+ u4 |- s
- 20 }
1 q- t8 x& V: ]* W! {& ~ - 21}
复制代码 7 d& D$ [3 [" }- e! r: S) ?
3. APP的编写本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。 流程图分析以我例程的App1为例: 先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题; 打印版本信息, 方便查看不同的App版本; 本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
+ F7 L6 ?0 W, c8 \6 p9 V) M" d
* h9 Q' p1 u6 @& H+ I0 \1 G$ R
, k* x7 y! h6 K2 P* x( ^* X8 e% Y7 W
程序编写和分析所需STM32的资源有: 发送USART数据和printf重定向 Flash的读写 串口的DMA收发 YModem协议相关
d& h' U9 G [8 h2 _; }) |; B; z, ~$ T g6 u% M; L
Ymodem协议
7 Z f" c @. E0 @) j7 Z, X2 y# R代码分析- 代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
- 后面放了我的源代码, 详情请参考我的源代码.
- 主函数添加修改向量表的指令! o) O- {3 i& A. w/ k) T2 ~# r
1 D4 ^3 J, L2 p2 D
4 m. q* T Y3 V0 r9 z; k# g1 O
- 打印版本信息以及跳转指令9 q2 ]9 ^7 b( `! q. M% d/ V) K( L
- 7 L: V; b$ ^' g! M7 V r# Y
9 I3 J. D3 ]5 U3 C* G; v
. I4 Z, Z6 X5 ~ P; X9 L
- YModem相关的文件接收部分7 O6 q8 u. { J0 [6 _( x9 }: V
- 1/**
+ }- K1 |+ b, X4 z1 w/ Q z - 2 * @bieaf YModem升级
1 Q' O4 e: z7 h6 P - 3 *
0 K" r4 s8 I. O6 m* X* T' v) B x - 4 * @param none
7 x7 [, A. x9 n. q9 S7 s8 |# w - 5 * @return none
+ }! p. w1 {6 u- C4 _& F - 6 */9 k" W! P' b. p9 {( w
- 7void ymodem_fun(void)
: m* f; w' g' _5 Y - 8{
" e" v- {9 b: r" W7 j - 9 int i;# C4 ^% y6 M# n; R& F6 `
- 10 if(Get_state()==TO_START)
/ i1 @0 A! s3 F0 d - 11 {
( l+ i5 T: u. d" \. z! _ - 12 send_command(CCC);$ z. ~+ b* I' W# n6 Z; d* v# }$ K
- 13 HAL_Delay(1000);2 s: _: T( ~8 H
- 14 }
3 Q& o$ Z# V* O' w$ L9 B - 15 if(Rx_Flag) // Receive flag) P: v* E) q: ?( J4 f8 q" n5 Q' o
- 16 {
T @: t6 D: }7 u9 d/ \ - 17 Rx_Flag=0; // clean flag
( k8 D$ B0 @' o: E- \8 Z1 \ - 18
% d9 d1 v: B/ j - 19 /* 拷贝 */
; i& Z P* ~$ a4 G& g - 20 temp_len = Rx_Len;
" m* p" h* [! m& X3 J - 21 for(i = 0; i < temp_len; i++)
2 {) Q8 g/ a0 o$ e+ h - 22 {
" y" P' i& c! L& [2 G. z Q - 23 temp_buf[i] = Rx_Buf[i];
# m9 @1 F$ s* B1 c3 Y: D - 24 }3 W7 D) i' c+ D! G% e4 t
- 25
& ]4 g. [" L# I3 u - 26 switch(temp_buf[0]): B+ [6 b. P3 c+ l. r" i
- 27 {% h3 q4 `" }9 a0 i" h& F
- 28 case SOH:///<数据包开始% W) P- ?# n+ o w+ H: `7 ^2 G- a# ~
- 29 {
- Q( q" F* D5 z7 l - 30 static unsigned char data_state = 0;5 f7 B O2 X1 e$ C, k4 Q; n
- 31 static unsigned int app2_size = 0;
+ x; _2 v9 l& O8 Z3 n - 32 if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验9 u+ O/ x1 V# @% J0 A
- 33 { ' W; l0 H+ X' R4 S% R
- 34 if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始
* x9 n# f+ v" {7 W - 35 {0 R# j( |# t- ~' O- s0 `
- 36 printf("> Receive start...\r\n");
0 r( p; ]6 f8 j9 G6 W7 x3 }5 ^$ u - 37
9 m& A: h* A3 x2 n0 X. S - 38 Set_state(TO_RECEIVE_DATA);9 k9 Y+ q3 S7 u3 B" E7 X) R/ O4 k5 `
- 39 data_state = 0x01; $ ~' O; I/ j( O1 N: y" w
- 40 send_command(ACK);: ~, @9 f+ R M. N3 C3 T
- 41 send_command(CCC);6 Z ]* U' u( Q& \, c. E/ o4 T
- 42
- i4 W% W) ?- g) @3 N, t - 43 /* 擦除App2 */
: s4 o4 Z" P9 f0 `( s6 w t0 | - 44 Erase_page(Application_2_Addr, 40);1 O" r& f1 I" A* X/ S/ T' g/ `
- 45 }% |; e) F4 K" @5 r) q6 D
- 46 else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束9 U. X! ` [4 l& A
- 47 {
5 }7 V$ L. N: X1 p - 48 printf("> Receive end...\r\n");% c. v7 l- I: X0 f8 U0 q
- 49
3 j: Y) ^3 d# j# E. V- u - 50 Set_Update_Down();
7 O9 M( l7 u/ }" } - 51 Set_state(TO_START);
- \' |' T, I1 V8 y8 s% I - 52 send_command(ACK);6 k6 i, J4 r' @6 R$ @6 u
- 53 HAL_NVIC_SystemReset();
8 q% Y* |7 a8 q4 D, U - 54 } # A( m8 q( b, Q- Z3 r; E0 V( I
- 55 else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据. E5 w7 \5 q V8 W
- 56 {
% b8 [& |; }& o2 g: t1 k; j - 57 printf("> Receive data bag:%d byte\r\n",data_state * 128);
7 F' {% F2 Y- c X3 P0 a* Q, n - 58
. V' g: z+ }8 {4 `4 L - 59 /* 烧录程序 */# Y- s% @) M3 \1 I# x) p/ ]& t8 q: r) g' _
- 60 WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);5 ]$ O9 d% S9 {$ n/ y. H0 Q1 {
- 61 data_state++;$ o( `( H' R5 o3 x& Y) \/ h$ Y
- 627 o) {! H9 o8 }" [; [
- 63 send_command(ACK);
7 K' }1 O% y% [5 O - 64 }
3 _: j. k9 P0 W. j, E6 N - 65 }
1 ]6 j) h2 `- @, Z( a - 66 else
4 I% n$ z9 D: G* c& u" T - 67 {! W& U M/ f$ }9 M/ I
- 68 printf("> Notpass crc\r\n");4 U. f! o5 I9 X% S/ Y( b7 Z
- 69 }, h7 Q* E- _: V- y6 F
- 70) o+ i2 X% [/ _' O
- 71 }break;5 P- m9 j( B. K% E" ?; C* N3 c
- 72 case EOT://数据包开始
4 b- E* Q" X; F/ F/ l - 73 {* ?- w4 q7 M9 P8 Y/ q& o
- 74 if(Get_state()==TO_RECEIVE_DATA)
: [# r7 }( b) L6 d$ S' ^ - 75 {
9 A9 Z8 w0 s% h8 p2 F4 P* c( m3 l - 76 printf("> Receive EOT1...\r\n");0 x* {/ O- [* N" @
- 77& d3 p' F+ I- k S$ A: g" P e+ f: i
- 78 Set_state(TO_RECEIVE_EOT2);
& V: Z2 |, c" ?. Y- O9 N: j - 79 send_command(NACK);$ K$ g! q- c3 f
- 80 }. \ `! ?2 O/ {/ D2 s! @ ~( Y
- 81 else if(Get_state()==TO_RECEIVE_EOT2)8 P$ o3 p. P# O+ \3 L
- 82 {
5 m* U: Z# ]) k8 ^ - 83 printf("> Receive EOT2...\r\n");
1 p* q* h9 C& s7 k& ~: L - 842 P; |' b9 n/ J
- 85 Set_state(TO_RECEIVE_END);
* v. ?$ b& p3 `. H# z- t) d) ? - 86 send_command(ACK);+ c% ^4 W- S$ @; y1 E
- 87 send_command(CCC);1 D7 G' w9 @1 w2 M8 j5 ]: X
- 88 }
/ t# D2 \8 t% v - 89 else
1 `5 q5 C6 A8 D4 E/ Y - 90 {8 ?# {; R& _+ Y6 i
- 91 printf("> Receive EOT, But error...\r\n"); z; F0 g1 N& G7 X! s' j
- 92 }
2 g& }6 D8 e1 y- o, { - 93 }break;
2 l8 c) f, v7 U5 i3 B - 94 }
4 ^% r, J5 i# s5 G1 V; f9 X( G# L) k - 95 }+ `1 O+ t& ~( {, e. D
- 96}
复制代码 + N# R5 ~1 i) y' P- \5 M; I
- ~1 G: \) U- n' d4 ~ @
- 其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.0 r" X) i& l& z( w, o1 p
$ {, t' k5 w+ u2 k0 w: X4. 整体测试本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。- 由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
- `4 c1 b& M( x: i% r8 r
^1 @ q5 R" B1 x* m E R
* |; A9 \; i4 e4 i* [. [3 l$ W* L BootLoader的下载- BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
- 按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)0 w6 g! _5 w3 U) t6 s l0 y/ Y
5 ]! r4 I9 F% a% k- _* C
- 烧录代码
- 运行, 通过串口1打印输出, 会看到以下打印消息
- 说明BootLoader已经成功运行
" U5 w+ G r7 p1 d2 H% S; Y# q# C( t0 q* t* ?, Y$ j! s
+ L; H# D: z! [. r" | Q t
App1的下载- App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
- 同时也要修改擦除方式为Erase Sectors, 见下图
0 H8 y- F+ M% P6 G& a; q - $ I n- z7 ?" t$ K3 V2 c( w) R
" I. g8 R* a% ~$ x$ S `( [& p' F
+ k- {' L- z% e8 S( q1 D7 `
# J% T6 @$ d- ~$ U x% G
- 烧录代码
- 运行, 通过串口1打印输出, 会看到以下打印消息
- 说明BootLoader已经成功跳转到版本号为0.0.1的App1
. Q: p9 w" i: I& F6 I) _: i* R
* J" n; p6 A& ^1 t# M; e2 g) o5 |) G% h9 T0 @
9 `& ]% j4 N, L+ \3 u$ ~- b2 ~# N+ ` 生成App2的.bin文件- Keil如何生成.bin文件, 请参考这篇博文 Keil如何生成.bin文件7 k( `) i+ }7 O
- ` O4 J; e# ^- K' ^
https://blog.csdn.net/weixin_41294615/article/details/104656577- 修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
- 生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件
9 i$ |8 Y2 h y; k7 v' l/ y: b a
# R- o' N! U: D+ ?2 k+ C4 Y3 B
* H2 t5 i# B0 u% s& u7 e
: J, L( R- f. n
2 u7 l% {) R: W3 I5 Q
使用Xshell进行文件传输- 打开Xshell
- 代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
- 所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
- 你会看到App的版本成功升级到0.0.2了.
- 如果你到了这一步.
- 那么恭喜你! 你已经能够使用在线升级了!
, p& [% l5 x( h! E% ^
' J' y) d& X1 J1 F; A 5. 总结通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多。
% R2 E5 n) [' f; u5 q
- W+ H q6 N) K' B7 o
. Y8 }* F3 G7 a- }2 l6 t8 X; }. e
1 L9 U2 B/ ?/ O+ @% P; R转载自:mculover666
3 \' u, V, t$ W, } |