|
简介 本文主要讲解在线升级IAP的基础知识, 主要是针对IAP 从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对在线升级的认识. 1. 在线升级知识6 N$ z$ t$ X, I( A& Z @ S4 p
r4 q( s" g( J: i ~" W. ]什么是BootLoader?BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.
- O: J9 P$ v% |. D( n, k) p STM32中的程序在哪儿?正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码 ; h, C# V/ b, J9 c9 \2 X' r
& p; Z) x9 b( ^( L' g( A接下来就可以进入正题了.
) b! K' Q$ ?; H# j' C6 { 进行分区既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图: 0 u8 x% V P a& N, Z3 O
: B6 X" t$ X; E: ?+ T j% W% }
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图: BootLoader区存放启动代码 App1区存放应用代码 App2区存放暂存的升级代码 ' b% }3 K+ z" r9 _9 U; t( O$ y. ~' J
8 B( n1 X( `" o. `9 _4 ?: b9 _: z% N) g; r; f* e
总体流程图先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序. 然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序. 在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示: ) t4 V5 i) z# u8 ~6 H* ?
6 e) d: r# e- D8 p" Z' s: X ^7 P. Z* O6 ^4 z/ {7 n0 c+ Y j
2. BootLoader的编写本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。 7 L3 l1 }5 I2 p" [0 a
流程图分析以我例程的BootLoader为例: 我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示 , y' }! A0 h; {. V7 O, l* [
& s- P% N. i' x: ~* `' F程序编写和分析所需STM32的资源有: 发送USART数据和printf重定向 Flash的读写 程序跳转指令,可以参考如下代码: \1 K: w- J. V B4 D* E( J
- 1/* 采用汇编设置栈的值 */7 n1 e0 u0 e' n. @8 n4 I- ?: w
- 2__asm void MSR_MSP (uint32_t ulAddr)$ E) o: u P: f a5 Q# V/ E# S
- 3{$ s6 X( t& K+ g2 W2 r; R: i5 N# x
- 4 MSR MSP, r0 //设置Main Stack的值7 j: [- I+ [0 D+ X# |# O9 r
- 5 BX r14, i4 M' ~) i1 ]. q* R# z
- 6}
' s$ x# V2 z+ q9 R( \" N - 7
1 h0 U: C* `4 b" V5 J9 w - 88 w4 N* p$ q9 \/ R! w) ?' {8 W
- 9/* 程序跳转函数 */2 C2 i+ e6 j0 j
- 10typedef void (*Jump_Fun)(void);
. T7 ]5 C2 S5 _- H1 Q; U7 k - 11void IAP_ExecuteApp (uint32_t App_Addr)8 j. ?. h6 u, U$ v" I) ^( Z
- 12{
' q0 b# n9 _* T - 13 Jump_Fun JumpToApp;
( E1 X9 b7 F: f5 E/ v2 c0 b - 14
2 d/ \0 d8 a' J K3 C: [ - 15 if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
3 }. }9 U, o0 w; X" o - 16 {
/ o- u: L. F$ k3 [3 A3 `* G d - 17 JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
: g: t1 R' |: g; W B. q - 18 MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)1 \' Q# K0 n% q) C$ t9 V$ a" A( }
- 19 JumpToApp(); //跳转到APP.$ H5 U: u5 S- B2 ]& N& ?& A
- 20 }2 @3 v' j) _! \. }. E* |! L" z
- 21}
复制代码
' B& m; o4 F% A9 P; T' |9 q7 X' _$ P+ k& L' Y0 r$ G! O7 s: d* f
3. APP的编写本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。 2 Q- h/ a8 d1 d- z* H
流程图分析以我例程的App1为例: 先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题; 打印版本信息, 方便查看不同的App版本; 本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
! F, f7 P0 g# w3 M3 H& I 8 j) [8 d) d$ s, T" X
程序编写和分析所需STM32的资源有: 发送USART数据和printf重定向 Flash的读写 串口的DMA收发 YModem协议相关 % v* e4 Q; o W8 l" b" W
Ymodem协议
. `0 S2 @7 v; @- M' y6 [$ Z, F代码分析- 代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
- 后面放了我的源代码, 详情请参考我的源代码.
- 主函数添加修改向量表的指令
; p- m" B) U r/ ?$ Q+ a! | Z: [3 C
$ ^5 f) E( c" }( z0 j3 D: n, p# A8 J; e3 l- o
- 打印版本信息以及跳转指令
4 H4 D5 m: a' z% Y0 C( t( f* F & U# w! w# q" n2 W
8 a6 u8 n1 [+ S" Z" [% h- YModem相关的文件接收部分) g. w, x; L/ O3 [
- 1/**$ X1 F2 t% |6 N1 C9 `! W
- 2 * @bieaf YModem升级
% r3 w8 ?- h4 w - 3 *6 {5 s0 x: q- f8 l
- 4 * @param none4 I2 B0 _" ?" a7 j6 E7 m6 L9 J. m
- 5 * @return none0 B0 [% Z/ D7 H1 I+ r3 ?
- 6 */4 u4 w9 s7 b1 q+ @0 U6 ~. q
- 7void ymodem_fun(void)
: `% d3 Q2 F+ Z) ?9 l4 S - 8{" I! t# R* t! E5 v- _3 x! |
- 9 int i;1 \# Z$ A9 [* I: Z
- 10 if(Get_state()==TO_START)
7 `7 j! p8 }# `+ U, C5 ]* |( p/ O - 11 {/ i2 a2 ]: M8 g5 m" g1 I4 l& h
- 12 send_command(CCC);
: m+ F5 e( t8 V3 B4 O - 13 HAL_Delay(1000);6 M) e; |- w$ _2 o
- 14 }7 s- p" R# Y$ P" j* j3 U% V
- 15 if(Rx_Flag) // Receive flag
# r: q' D" l8 ^: a! i# ` - 16 {
( W/ W% L$ y; U. E: C - 17 Rx_Flag=0; // clean flag
1 w7 H; o' c; A - 184 E& G! J3 o- i: n2 S4 P
- 19 /* 拷贝 */+ Z4 A1 i* _& @8 f ~- E
- 20 temp_len = Rx_Len;: A4 {8 r# ?) p8 b+ \5 Y9 N
- 21 for(i = 0; i < temp_len; i++)
1 k; K6 H9 [( j$ s - 22 {
$ Y' ^7 y" `. Q2 l+ P$ a8 [( o - 23 temp_buf[i] = Rx_Buf[i];- n0 N7 M& @% G; C' m
- 24 }' o. t' O5 O/ N. z8 b7 x+ m
- 25% I9 ^: w& u, n8 C! k
- 26 switch(temp_buf[0])
1 _* R, [/ j. a5 x& [, B - 27 {
$ K: J+ t. u0 h5 U- J' l - 28 case SOH:///<数据包开始, V5 ^1 m5 E* {9 h
- 29 {
2 v' Z% V/ t5 h U - 30 static unsigned char data_state = 0;4 h2 [# O4 b; I7 {; }
- 31 static unsigned int app2_size = 0;
* a- Q3 W( N+ b: G1 b! k9 o2 L - 32 if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验+ G* { X2 o! M o+ y
- 33 { ( G# W: P- N+ j: K i* f
- 34 if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始
! G$ p5 y7 W. o8 m - 35 {
( r6 a- n ~' [% A - 36 printf("> Receive start...\r\n");) Y& P' X/ \- q7 k' w5 O" B
- 377 l0 T2 b- N6 |1 T9 I
- 38 Set_state(TO_RECEIVE_DATA);
( h3 S1 \3 R3 c$ _% I* @8 @" {9 n - 39 data_state = 0x01;
5 s- K( @1 m$ g8 J, J, V, O - 40 send_command(ACK);
: H0 T% ~, k, n+ O# U - 41 send_command(CCC);
% K) S3 j# a# C; ]1 ? v" D - 42
: V k6 o+ R u8 x- W - 43 /* 擦除App2 */ . _0 n, V) C" V$ B) P! J3 i! a
- 44 Erase_page(Application_2_Addr, 40);
7 L1 \) X; D3 A - 45 }" a) ^0 w: _% U: I0 |3 H
- 46 else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
% \$ H$ c& b% m4 U - 47 {
& A! i/ S: R k1 S: {# b8 } z3 U - 48 printf("> Receive end...\r\n");
8 y& v% I7 A6 Z - 49
$ j. p) E P8 ]+ `. A - 50 Set_Update_Down();
& n9 p$ s n- I! I: t - 51 Set_state(TO_START);2 ?! q, }) C3 a! P' @
- 52 send_command(ACK);: l2 L3 J' |6 J
- 53 HAL_NVIC_SystemReset();
9 F& `/ a2 n- O- N - 54 } , Y: s0 Y9 I7 n M9 L! c9 Z
- 55 else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据# z3 \$ s% ~" J, U8 {, ~0 C) v
- 56 {
" k4 L% t( ]2 R7 S - 57 printf("> Receive data bag:%d byte\r\n",data_state * 128);! r- q& b0 B3 }
- 58# B+ M( B- [ w& M/ g
- 59 /* 烧录程序 */0 i' _$ h% e; H, _5 f
- 60 WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
! B) y$ o8 N k5 _6 R/ q7 I - 61 data_state++; q; u8 V7 P" s* @) K
- 623 m) v6 w& j/ Q
- 63 send_command(ACK); " ^/ e* _/ ]" d7 @; j# L! t
- 64 }: @& m; L2 `7 i* K6 }7 j9 j2 N, m
- 65 }( d! N+ p9 P6 K6 y
- 66 else
3 Q5 S% W& Z" v \ - 67 {2 i0 ?% v. |/ `
- 68 printf("> Notpass crc\r\n");
$ `( _7 O! i9 G2 E; Y" c% b' F - 69 }
) |; f3 ^. B6 L9 v+ @ - 70
i( [3 z( S' V2 ]: x - 71 }break;& u# s( u+ t. S; D6 |
- 72 case EOT://数据包开始
9 l2 U! A/ j( O' b: Y8 y. Q - 73 {
: | a5 m4 X, s; B8 ^ - 74 if(Get_state()==TO_RECEIVE_DATA)
% V2 h: M# E& F. f - 75 {
/ \6 d2 \, \. a n$ t. c8 Q) e - 76 printf("> Receive EOT1...\r\n");- \7 u1 y( {! `4 h: |; R K% W
- 77
; K D5 i+ L# J1 h. U6 u - 78 Set_state(TO_RECEIVE_EOT2); / `% e7 L5 [4 I0 i
- 79 send_command(NACK);- }8 ^1 Z7 A: `' K) H4 H( f
- 80 }
7 P8 i g$ V2 D# E' J9 t* s& I3 _ - 81 else if(Get_state()==TO_RECEIVE_EOT2)
& F5 }) _( }, ~2 S% c0 Y" l# y2 ? - 82 {
% l0 E/ N2 e4 h) y - 83 printf("> Receive EOT2...\r\n");
) t" K% p1 E- i! R7 Z/ _( v - 84
! t- O' J" Q# l - 85 Set_state(TO_RECEIVE_END);
8 f1 ^6 H; V# B1 Y% `6 e - 86 send_command(ACK);/ Y/ v. l# Y+ T* k
- 87 send_command(CCC);/ i0 F/ S/ M$ @) A
- 88 }+ U, v. h6 ^& w/ ]+ f3 O, N
- 89 else' D) u0 e) c5 p
- 90 {
% }0 j7 M( {0 @* \ y4 v: ~1 P) p - 91 printf("> Receive EOT, But error...\r\n");7 p7 x" ?2 m! X, e7 l6 K. B
- 92 }, {. f1 i- E8 P* A
- 93 }break; 5 w% \1 K( T5 e0 V. o" N" s
- 94 }
4 G0 z% ]6 n: O+ T' l7 m( R - 95 }3 |- U. L9 O- T, [8 c, Q G5 r
- 96}
复制代码 3 r( o' S' }& B2 Y: [9 }& T
' J M8 _5 v1 D+ C5 g
- 其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.
' G" C2 t4 D5 M2 N. e$ q
+ }8 X _# s W& C# s: K2 s3 y @2 U
8 Q# ^, l% g% Y4 ]- x9 u# e6 i7 M4. 整体测试; ]- A# R2 X0 k, |* c
本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。
7 b+ ^% c; c- V/ ]$ s7 L3 E% o/ w2 ]5 }0 j: X: n0 w
2 R+ o- Y# B" P
源代码
2 W/ k! u7 E$ D3 u& _! S7 C! vBootLoader源代码和App1源代码可以在原作者的gitee获取:- 由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。$ w/ B4 \& L2 _3 N. b% ?
) `# @5 x C9 y. V5 V
) Q$ W+ B- u# }/ ~$ ?$ e
BootLoader的下载- BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
- 按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
a5 l4 K4 x$ b! I) ]. o; \ ( Q. ~/ e5 ^& O6 A
( U* _9 l/ Q3 Y" \& j
- 烧录代码
- 运行, 通过串口1打印输出, 会看到以下打印消息
- 说明BootLoader已经成功运行
, i. B6 c2 W% \, N. m5 v" O- L ' e7 A4 o6 M; ?, P
# K" h, r C w, l; [/ y$ g- X* x
App1的下载- App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
- 同时也要修改擦除方式为Erase Sectors, 见下图6 v8 e1 D6 ^+ \
, h: W8 ~ z% h0 I: f3 R
' U+ r* F) [ H
; X# V' f; M i& c/ A* Y) s! y- 烧录代码
- 运行, 通过串口1打印输出, 会看到以下打印消息
- 说明BootLoader已经成功跳转到版本号为0.0.1的App1
# g* f2 Z {, B7 M/ D. T
3 D2 r4 q! x' z+ T* Z g( A' z# r. l6 ^! L
生成App2的.bin文件- Keil如何生成.bin文件, 请参考这篇博文 Keil如何生成.bin文件
7 d$ N: N Z7 F% [* \ https://blog.csdn.net/weixin_41294615/article/details/104656577- 修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
- 生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件
5 Q* r2 S P) k2 j
" s0 |; _4 m1 t5 M1 n
3 o) I; R: w0 x. U使用Xshell进行文件传输- 打开Xshell
- 代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
- 所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
- 你会看到App的版本成功升级到0.0.2了.
- 如果你到了这一步.
- 那么恭喜你! 你已经能够使用在线升级了!
6 ?9 b0 c! y9 \1 U
% z* e* Z& K3 O# N. |
8 z, C: G+ e* T, Z/ w: u5. 总结1 A7 K9 e8 o; _, x
通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多。
+ @8 _" x2 C2 b+ [
8 Q* q; S: Y# x0 {+ j0 w+ `$ O8 f* q+ x; R
3 U5 Z2 |5 L) [1 p转载自: ARM与嵌入式
5 L! ~2 A4 n7 l8 g+ U如有侵权请联系删除$ D$ @7 M% j2 q
1 B' T: p1 N. U8 }1 _2 o
5 p3 i7 x0 n; R7 F8 @, e
& c: T2 I! X# c% _ |