简介 本文主要讲解在线升级IAP的基础知识, 主要是针对IAP 从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对在线升级的认识. 1. 在线升级知识什么是BootLoader?BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的. STM32中的程序在哪儿?正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码 接下来就可以进入正题了. 进行分区既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图: 以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图: BootLoader区存放启动代码 App1区存放应用代码 App2区存放暂存的升级代码
5 ^$ I) \1 W0 A( s y* C1 l 总体流程图先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序. 然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序. 在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:
5 J8 y8 H/ }. X' [7 j; a 2. BootLoader的编写本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。 流程图分析以我例程的BootLoader为例: 我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示 程序编写和分析所需STM32的资源有: 发送USART数据和printf重定向 Flash的读写 程序跳转指令,可以参考如下代码:
5 ^0 U( A' _# q4 Y7 i3 X
- 1/* 采用汇编设置栈的值 */. {/ j# ^) [1 J$ h
- 2__asm void MSR_MSP (uint32_t ulAddr)
& }! T& ^& f9 F7 |9 C9 u. w9 N* s - 3{5 }$ r, \6 S9 P6 B- W7 S
- 4 MSR MSP, r0 //设置Main Stack的值+ s0 e" Y9 S5 B4 _% _) {
- 5 BX r14
; \) ^7 w5 C5 ~- a2 k/ w$ d - 6}
& K. _4 Q1 i8 { - 7* y4 e1 f4 q) g! O
- 80 E# K8 E0 g8 o2 I5 \* g# D
- 9/* 程序跳转函数 */
! W' g" H8 U% B+ Y& D/ W# R - 10typedef void (*Jump_Fun)(void);
" `3 r& c1 i; U V2 ?; w) M: B - 11void IAP_ExecuteApp (uint32_t App_Addr)$ L$ e2 i4 P/ q2 s! T) ]& M2 t& y
- 12{
, f0 R" d; h' j* O - 13 Jump_Fun JumpToApp;
( J1 U. {" v4 @+ }/ F- S8 s; M - 14
' b. X8 T' J" q: u/ P, e - 15 if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.9 a w7 a/ Q; ^8 @7 l9 B. f5 V2 `$ i
- 16 {2 F& @0 E* Y1 L! \) u2 M+ _
- 17 JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
" b. {3 Q o& O; R2 S: @& y- X m - 18 MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
; T6 V: E7 w, ?3 O - 19 JumpToApp(); //跳转到APP.& {- L& R e) T2 a: P6 r
- 20 }
; c! H% q: o3 W {: F2 U! p - 21}
复制代码 ( p( ^: ~- i; g: n; M% M$ V
3. APP的编写本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。 流程图分析以我例程的App1为例: 先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题; 打印版本信息, 方便查看不同的App版本; 本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示: 1 w: f: I @/ S: Z
程序编写和分析所需STM32的资源有: 发送USART数据和printf重定向 Flash的读写 串口的DMA收发 YModem协议相关 V, a8 w7 T4 A5 c
Ymodem协议3 v; u0 j# E9 p2 L7 }/ Y( B4 p' {
代码分析- 代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
- 后面放了我的源代码, 详情请参考我的源代码.
- 主函数添加修改向量表的指令
. M8 r0 d: B1 o4 h
- 打印版本信息以及跳转指令
9 e6 J c5 y# ]+ z
- YModem相关的文件接收部分
\/ h/ X6 z+ A; x' v
- <font color="#000000" face="Tahoma" size="2">1/**' Z7 A9 _/ Y0 r
- 2 * @bieaf YModem升级
0 D/ R* _+ M# d" a: q - 3 *. ~3 R* e J3 _
- 4 * @param none; |( D* s, i1 R4 U f; l; M7 H
- 5 * @return none
0 r4 }; T# G+ k) f3 u8 w, G - 6 */8 Z. j" R) J1 Y. Z% T. Q4 ]3 E
- 7void ymodem_fun(void)
: Q+ V A) M/ O# { - 8{
* X8 e5 r8 z, s. y# e0 u7 C - 9 int i;" k, @/ J3 {0 ^- ]
- 10 if(Get_state()==TO_START)
! D6 d/ U. {* z1 G+ t; p - 11 {9 w5 A- e' |1 @* z( ^
- 12 send_command(CCC);
) B/ i; s' G9 k. j5 | - 13 HAL_Delay(1000);' I7 W& L/ Q. L& O+ R O
- 14 }
; t6 ]' G4 g S5 v3 _; R6 Y" G - 15 if(Rx_Flag) // Receive flag
T7 d' p6 n* Y8 _7 U0 | - 16 { o" l4 v! k, d
- 17 Rx_Flag=0; // clean flag
# ~; s) y! \7 p1 y- n. q' m& o; B - 18
3 i" h4 G' D3 N - 19 /* 拷贝 */( y) j7 M+ ?7 |) Z4 Z) W' L$ C
- 20 temp_len = Rx_Len;% G- m1 o: u; v& P6 U4 S
- 21 for(i = 0; i < temp_len; i++)
5 |2 ]# O. E; c& o9 ]# V6 ]1 g - 22 {; Q3 n. o. Q; u0 X0 J
- 23 temp_buf = Rx_Buf;
) l" ]5 }, R$ Q. ]9 ]! H, E0 P - 24 }% M7 S! g( B. W7 k. C, g& C! J
- 25+ k# D" @+ _$ `% p$ Z( M. W
- 26 switch(temp_buf[0])% I9 E: x0 b( R( S9 G/ E; L: G
- 27 {" ?; A1 E! P; j) c
- 28 case SOH:///<数据包开始 x7 s9 r/ G8 W
- 29 {" Z; ]% Q' R/ C, Q6 Z- }) R) q
- 30 static unsigned char data_state = 0;7 b5 B, H3 j; ?( x& J
- 31 static unsigned int app2_size = 0;
- u% ~6 ], t6 j6 z1 M7 L" R6 Y - 32 if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验
; x& Z: w. w) @7 h, v8 N5 c - 33 { 1 q) a; T! ^: P1 |( O& @5 m
- 34 if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始. r' k( X1 K6 {
- 35 {8 m G* J s z, ]' e
- 36 printf("> Receive start...\r\n");
1 R; K2 U9 M. J - 37
: y" v5 e6 K ` - 38 Set_state(TO_RECEIVE_DATA);4 S/ k0 t; H! `1 B$ X, {
- 39 data_state = 0x01; ; P, U# Z: Y4 O2 X0 `
- 40 send_command(ACK);0 |; O/ U0 Y3 z( t7 L
- 41 send_command(CCC);; a H2 u0 o9 ?7 M8 n' I9 d$ u! U
- 42& e$ s |4 o+ j7 ], D
- 43 /* 擦除App2 */ ( z) W( o: s* M: b+ W/ s" l
- 44 Erase_page(Application_2_Addr, 40);
+ Q' ?/ s3 s/ H( m1 d0 r1 S( e+ _ - 45 }
: k% X; S7 _5 G5 x" ? - 46 else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
5 O- D, Q9 _( t- t: s+ {( b( m3 ^ - 47 {' k6 @- k. A% V/ ^" K T( @
- 48 printf("> Receive end...\r\n");2 l! Z( f* i, Z% N
- 49
5 T1 y, I* L+ ?9 R" {; R - 50 Set_Update_Down(); % V# ?; z/ q; | A( C7 F+ K
- 51 Set_state(TO_START);
W3 Z1 ]1 S( k0 o* w8 ` - 52 send_command(ACK);; }# t% E% I8 T) o2 H
- 53 HAL_NVIC_SystemReset();
8 |2 S% q( |3 y* Z4 T. X, K$ C - 54 } . u% [! }0 f/ F( H7 W3 Q) ?7 u( A6 H
- 55 else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据
! R* J# B, E5 H5 q6 y - 56 {7 q% [5 M, o% @$ ]
- 57 printf("> Receive data bag:%d byte\r\n",data_state * 128);- D: `6 j% {; I! e( m
- 589 c$ _- \2 U- v% d$ u. {
- 59 /* 烧录程序 */9 Y0 L3 v, F* x
- 60 WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);5 B2 X$ P4 Y# W% Y: S4 y6 T, V
- 61 data_state++;
+ Y4 s% q4 {6 i' r# x9 C' M - 62
, y' ]$ _9 q8 b# `* O - 63 send_command(ACK); 3 a7 h1 E- g5 N5 Z: Z% @0 Z* O3 Y& b
- 64 }: {6 f) `: M* ]7 u/ u) C
- 65 }
# e2 ]& H( u) g9 `9 d0 | - 66 else
- B1 p: t4 G2 d C2 b - 67 {
. o1 @ M \( k B9 U q! B - 68 printf("> Notpass crc\r\n");& m# m0 |( a# L5 J
- 69 }( l0 L! P& \+ m2 ?/ f
- 70& ~ |( E1 e8 N4 H! Z, R z0 q
- 71 }break;3 ^# w3 }$ t" K# |: q2 e! w
- 72 case EOT://数据包开始
( p1 o& ^* W5 W - 73 {
* O6 C5 N3 X0 e+ X4 M - 74 if(Get_state()==TO_RECEIVE_DATA)
n" r, S7 E/ u; _7 G - 75 {
3 u( V, D: V. G- C2 W } - 76 printf("> Receive EOT1...\r\n");
5 \4 R/ @3 o' K' g - 77
7 I @6 [0 D$ k$ x# r1 K4 g - 78 Set_state(TO_RECEIVE_EOT2);
( }. d3 G# `& e' K6 V9 Y$ R2 ]3 S - 79 send_command(NACK);
) B3 [9 K8 Y& ^$ M$ \+ d9 _3 N! g% z1 T - 80 }
! h- a2 O' i7 F+ D, R. E - 81 else if(Get_state()==TO_RECEIVE_EOT2)- r) Q- S( a" o; K2 Z
- 82 {! k! W' b- b. L& o3 t6 j2 ~* U% X
- 83 printf("> Receive EOT2...\r\n");( H. N, @2 E! ^
- 84
% O& W9 b% k! @" } U7 O - 85 Set_state(TO_RECEIVE_END);
( e. Y: h( Z$ N - 86 send_command(ACK);
4 Q( o3 n+ K( d$ { - 87 send_command(CCC);. R: f1 ^5 O& F/ ]8 v
- 88 }& d$ W; b8 m, K9 l# Y$ V. q
- 89 else
6 x0 q5 F2 Y, Q5 g6 I; ?: W: @ - 90 {9 U( `+ l$ J" H8 z! ^
- 91 printf("> Receive EOT, But error...\r\n");
P* A0 Y4 \# i0 K5 F - 92 }
! n( T: Y& Q2 T4 j E - 93 }break;
7 N) I3 Q9 T2 n8 Z. G8 w& J - 94 }( O% R7 ]. }8 ]% O
- 95 }
8 {6 Z3 m( E5 U/ r2 o - 96}</font>
复制代码
8 _5 e; ^6 g0 {' Q3 C- 其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.
5 b; ?" f' y! C X) B ' O7 j- H& G7 W# e0 m
; [& _1 |# e5 C2 j7 ]0 _: z. Y( n% M4. 整体测试" [. \# H- B8 r% W( X. k/ u6 Y
本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。- 由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。+ \4 T4 A, N) L$ q4 ?0 s6 e
BootLoader的下载- BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
- 按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)# u0 P3 y, x* R0 g% o+ i; ?
- 烧录代码
- 运行, 通过串口1打印输出, 会看到以下打印消息
- 说明BootLoader已经成功运行
& e" @. }5 N" |" M! p# A App1的下载- App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
- 同时也要修改擦除方式为Erase Sectors, 见下图
0 |$ a( G; e) A$ i2 w7 }( R
7 V6 s' A# t @- 烧录代码
- 运行, 通过串口1打印输出, 会看到以下打印消息
- 说明BootLoader已经成功跳转到版本号为0.0.1的App1( \) Z) t: D4 ?- S6 L
生成App2的.bin文件- Keil如何生成.bin文件, 请参考这篇博文 Keil如何生成.bin文件
9 Q6 j* @: b4 e$ ?2 G5 S" U https://blog.csdn.net/weixin_41294615/article/details/104656577- 修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
- 生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件9 f# d3 Z7 j! b: \2 E) O' ^% ^
使用Xshell进行文件传输- 打开Xshell
- 代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
- 所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
- 你会看到App的版本成功升级到0.0.2了.
- 如果你到了这一步.
- 那么恭喜你! 你已经能够使用在线升级了!
$ U% I9 O# p# P 5. 总结通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多。0 q8 m1 x& Y1 h
$ |, L+ U& |# l& F- l
7 k* I. I5 R& y, D" G) {
: R/ S. g7 f6 X+ u3 h0 p4 c5 y8 \6 ]5 `
; P }% t: v7 ]* J- H$ `
" ^! z- ]8 t; [+ o1 ~+ w" n' v1 K' r& n/ P8 i$ E f" o
|