简介6 {. S) L/ Y( w6 T4 h) F
本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.
8 i T( m8 M; `, ^5 F9 G( ?% z, b y
# J0 h( G- i3 G. n1. OTA基础知识( N3 o, S6 Z: i, v% m
什么是BootLoader?- M& Y' M4 _; I' Q1 H
BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.
; i G8 R+ h) n% Y7 g* T+ h$ g& J2 a& C- N% O1 q
STM32中的程序在哪儿?
& Z* Z; R: R8 Y! t" }8 z正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码6 y3 m, E7 w8 m& a4 M; j2 X
" P: P. R! U# b$ F
/ ~6 n! x: `8 q" i# [$ z1 v7 O: j3 I# @" F8 Q% S% W
接下来就可以进入正题了.* z# m; f1 D$ q9 y/ s) {. z8 {
* A4 k+ d2 q) a) v
进行分区
. `& O) M8 p) R0 c$ i. s$ [既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:4 L! M& ^+ h% {" _: T' s7 m# ~
; }, O( [) q& I" @7 ]: f& P
1 N3 t, H+ q% p7 ^* m
6 p p- C$ G' @. x# K, R$ P
( H% D1 {. T2 ^/ T/ \. I以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:
6 K0 A6 T [# G- t; j2 r" OBootLoader区存放启动代码
9 x8 \/ e' t$ a+ OApp1区存放应用代码
7 }% _( @7 f0 {3 M. T* RApp2区存放暂存的升级代码% |5 X% M# `1 R3 G
: d5 Q5 |- G8 V+ Y4 U! ]
8 B( _( a5 ]5 N2 P; m
4 W0 W4 H8 m. g4 I
总体流程图& s6 y3 Z9 S# i' J p' `
先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.
# x1 V5 P: W* [' t6 G: h然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
" N# U. r2 H+ l/ [" G; P0 O, k1 c+ T- |& M: f
在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:
- Q! {) f9 U3 f) w* [0 r, g) s) H( D* V8 g2 o
% h7 L) l- E+ d# T, {0 B) M2 o * W1 G. z7 D, f- u
( [* I! l% r! j. b- c( S" L2 J' `8 k
2. BootLoader的编写
$ l3 h+ T; z. N+ a6 q本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。
- N$ k: c2 U. j' G; F1 B; g9 [9 t9 C" A
流程图分析. I1 t2 l6 _6 N; z( h E$ W9 Z6 i
以我例程的BootLoader为例:. P1 G5 v# n9 s% X
我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
: }3 A( ]/ z1 j& p( g. i# E% d- s' c9 l# x
0 u" r9 ]3 N2 H" p) d0 j8 ?; L0 T
. d- k1 K/ P( t0 \* _. I程序编写和分析5 ^/ @! h0 G; m" Q s
所需STM32的资源有:% C8 I7 I5 C4 O4 l8 a" c9 j5 H
发送USART数据和printf重定向
5 r" `3 L# o3 m2 r4 }Flash的读写
6 {* c. y, V, j" h2 A程序跳转指令,可以参考如下代码:
! |/ v7 ^% b+ @! Q g! O7 O' k) V/ U# R u
& A9 `" \7 w# V( ~
- /* 采用汇编设置栈的值 */0 s! J8 c6 j4 [5 r0 S7 C7 w
- __asm void MSR_MSP (uint32_t ulAddr)0 b c7 w; M8 R* P. T
- {
7 ~( X" o% ^+ }! Z - MSR MSP, r0 //设置Main Stack的值
T7 t1 ~4 Q% d0 J0 K- \! h7 r - BX r14
5 Z: R- J& c' y - }
+ @2 W# Z! q* V) n5 r* B
0 s" D- d& a7 B- 5 |$ D; p) J: K% i8 j% f* Z% _% \
- /* 程序跳转函数 */3 b% g1 K' k. l. V# B6 C/ o, u
- typedef void (*Jump_Fun)(void);0 B3 x/ G) N' v' Y% e
- void IAP_ExecuteApp (uint32_t App_Addr) b9 T, H: y# \
- {: F5 w% H; U$ H. [
- Jump_Fun JumpToApp;# Y% e9 F7 x9 H: i
- 8 i8 S. p4 y% e% _
- if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
; Y& M; M0 S" i) N1 L8 w2 E - { L2 N& u* p* C1 C9 {6 ?* g
- JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)% Y5 [7 l, Y4 j4 E( x( r
- MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址), L& L. X0 V) @+ Z9 B8 j. k
- JumpToApp(); //跳转到APP./ c! X% h! y$ d$ s
- }
7 q& r) B- z& G- c7 b - }
复制代码
5 i- Z# [2 H, j( u3 H在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);
! `9 j, g1 |8 ~4 Q L1 K1 U# P8 D其他的代码请参考BootLoader源代码/ x8 M7 `- v6 v, q
. F! E& J/ F1 K% @ [8 Y5 Q
3. APP的编写9 x- r3 W2 [! W% M3 H! c; r
本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。
t% w" D$ C( @+ f5 ~5 e7 u4 x+ d) ]
9 ?* n1 v, _ Q: D5 V: p7 M8 v
流程图分析
" c% s B$ c7 O, e( \2 a以我例程的App1为例:
! F' K: d4 V+ \$ z' r先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;
& U9 b) G0 K* U: {1 L打印版本信息, 方便查看不同的App版本;
+ X$ ^9 F3 p0 k# T% _' Q本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
0 J8 e( [, l7 T! u& d. A4 d& G9 h+ {& s+ ^
6 Z" k3 ~; }) y: o1 |, k
3 k+ h8 E% E9 z5 ~) l程序编写和分析
0 v, |9 K: J# S# r# P2 [所需STM32的资源有:7 e# c1 B0 l% L& b, D
发送USART数据和printf重定向/ S0 ^4 T* u5 G' m1 _3 B
Flash的读写4 [; n5 K$ D% a2 Y# ~# H2 v1 a0 W9 n
串口的DMA收发4 {& O0 z; F; f
YModem协议相关( G& N; L$ ?2 z7 s8 c
Ymodem协议) }7 _7 o0 x; q; m; }) W5 |
百度百科[Ymodem协议]! `* r8 K6 x" z, O
具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).6 f; i& K8 y- e$ n! W( z
Ymodem协议相关介绍可参考我的这篇教程 YModem介绍
* q' f+ o+ L7 A+ f9 v7 P
0 |$ J& w+ c, y6 k* E+ K; ]" J代码分析
9 ?! r1 H6 }9 L$ c2 m5 p6 h! T代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
2 t9 O( m* ] t U+ t0 p+ N1 j后面放了我的源代码, 详情请参考我的源代码.
L$ v. Z/ n( ?# L, `% e主函数添加修改向量表的指令
% c( T" t- S' p1 t- M* J* J, U: O* F$ i: \, L" i
5 h0 P; _( A3 b3 J& b
1 H0 b8 D, I, A- {打印版本信息以及跳转指令! a u7 y) X5 v2 A L- n# U$ o
% A& @% o& ]8 L4 n
9 K/ [' u: c0 |1 {
- K s7 l, `% Q/ ?. aYModem相关的文件接收部分1 N( a7 M: b# X( [9 a4 @
) Z& M% R% f2 n ~8 |- /**0 e) U8 g2 c9 \3 V0 y+ M+ B' r
- * @bieaf YModem升级
9 A2 @$ ~% n! N$ N+ X& z - *+ ]2 A! P+ _8 A0 }5 e% c
- * @param none# R. s+ S- [$ u: h3 @4 T
- * @return none
+ g e4 L6 M, a2 Q - */$ D' f: I7 o& V) r7 ~2 i( Z
- void ymodem_fun(void)
* }3 r2 B! ]7 `. b! J4 s& t - {
+ h( G1 f1 [; m1 H; h - int i;
0 f. }$ i2 m" ]# |! |7 y - if(Get_state()==TO_START)$ }4 Y4 |& _ C |; g x* f. |
- {5 q6 Y ?$ L6 R6 e
- send_command(CCC);2 N) }' I' W9 B
- HAL_Delay(1000);- v8 Z3 J+ ~, U- W; t
- }
- o m& n- }2 E( r1 r! \ - if(Rx_Flag) // Receive flag- d8 L, h$ {+ K' w; W8 ]
- {
: O, D8 }. n3 M8 O+ _6 [7 Q$ t - Rx_Flag=0; // clean flag$ Y& E! U. Z# l( |
- " i, j6 p8 c% y8 D! Y
- /* 拷贝 */
1 C7 q- I; m. g - temp_len = Rx_Len;
+ w. h9 z; C( n+ \" } - for(i = 0; i < temp_len; i++)
H! ?7 D' A5 O& g - {* C! i, J1 c" m$ t
- temp_buf[i] = Rx_Buf[i];
* M" i/ U7 s( v. r2 `9 i7 S4 ` - }) V' g5 K4 U: e' s+ }; H
-
; g' d; C& z$ W; \ - switch(temp_buf[0])1 G6 z9 G7 q' e
- {
7 P* M. K. z: ?" j. P8 a - case SOH:///<数据包开始; `! V( N# [1 r' @* e& O
- {0 v- o+ [! K/ w0 O
- static unsigned char data_state = 0;
4 v3 `0 W5 x' Y; t$ ` - static unsigned int app2_size = 0;
4 x) l& g! C. c9 x3 H# r - if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验
% t1 `1 u: g- m5 K; P! d! ?) m2 d - { 9 u+ n* h* L" a2 w- u+ m4 ^; \5 G
- if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始3 g" ]$ E3 t+ e0 a# P( ? c
- {) {% m6 V, B$ s8 X+ f8 g4 J# R
- printf("> Receive start...\r\n");/ B* s* {2 m0 c B
- 8 N( \$ ]3 j" P
- Set_state(TO_RECEIVE_DATA);1 U5 J! f0 e; C- V; p
- data_state = 0x01; 9 c" ]9 S2 U7 s2 G& q9 l. s
- send_command(ACK);$ w/ [% M+ x% G! h/ W+ V8 q
- send_command(CCC);+ Q8 y4 Z! m, E5 L# o0 i8 z' \, U
8 W; n3 z- D7 D8 f- /* 擦除App2 */ , O' \' @% Y& l/ y( @& Y
- Erase_page(Application_2_Addr, 40);
* p" D& b5 T4 T) M5 R - }) O: n0 u5 V' n; j1 V
- else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束5 R4 Q* L8 Y4 Y8 M! U
- {
6 w& Z H$ j: B0 T- p3 v6 J# x - printf("> Receive end...\r\n");
& z$ K5 N8 n# x/ a
( x2 c: y7 m" X5 o3 R8 {- Set_Update_Down(); ) Z2 M5 f! d2 E' b- \; Y6 d* f- M
- Set_state(TO_START);6 m# k6 L; m. z
- send_command(ACK);+ i# T4 [. X: A! ]' q+ m
- HAL_NVIC_SystemReset();/ U k% o. G2 P5 k+ Z, q
- }
! R/ p* J% u( C- g. m - else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据
" n+ y5 y6 E' {/ ~2 i - {4 j! c6 e8 A3 ~) l+ L" L
- printf("> Receive data bag:%d byte\r\n",data_state * 128);$ W# ^8 u" `- p3 D' Y% q3 Y
- % s% l% J/ o# c0 r$ m( ^
- /* 烧录程序 */5 I- K1 C7 N6 C, }0 v, v/ ^+ X
- WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);: b' S1 |! v1 E6 o& c
- data_state++;7 E- b( e4 j4 v2 ]2 ~9 C$ }
-
& r4 P+ g# _# |# k/ X. g6 R - send_command(ACK); . O5 g* }5 O& B0 P
- }, V# H) X8 D! O. c/ G: u/ g
- }
+ @3 Y! P" i7 B1 o, o& | - else
8 x' n; ]9 l) U8 c) ^/ h* F - {8 D4 Z7 R2 L" W# r0 b- ?1 `/ ^
- printf("> Notpass crc\r\n");: K2 S8 B5 @ x, _
- }
0 U; u/ W: @' ~1 W# A -
" A# ~! ^) p$ u - }break;
0 L' i4 V5 P7 i* p H' b5 [ - case EOT://数据包开始
# s0 o! @2 H1 u8 [4 Y# N" } - {8 T3 L2 L0 z, l4 k; K
- if(Get_state()==TO_RECEIVE_DATA)' Q8 r' S$ S' G; S
- {1 v1 l* E8 i9 {
- printf("> Receive EOT1...\r\n");
3 S& H' K3 p) W2 B - , t9 [0 w6 o6 A* R" S1 C6 T {3 ]
- Set_state(TO_RECEIVE_EOT2);
) @$ n8 q, i A3 M+ A - send_command(NACK);
7 Q: B8 _5 h! K' I - }4 l) J9 }$ T8 l9 k" h Y
- else if(Get_state()==TO_RECEIVE_EOT2)/ N u8 w$ \6 f) H v- [+ H: [' X
- {
, m# F/ H: W/ ]% j - printf("> Receive EOT2...\r\n");
* a' s$ S; k# U' s! B - % t9 S- k7 s+ z% L* R( d( ^
- Set_state(TO_RECEIVE_END); / F8 N x! k |0 D+ n6 J. I! z
- send_command(ACK);( b+ ^+ u! a7 J, K
- send_command(CCC);2 h* S# d. P4 t/ R
- }
% \, f3 @) r9 X3 \ - else0 y( N) Z* z% z
- {. ]& Y# ^6 a4 W, ~0 ?2 Y
- printf("> Receive EOT, But error...\r\n");
$ ]: K6 U, g# `* J* J$ ? - }9 e4 _' M& Z+ {. `1 }+ p4 P$ R% P
- }break; - l/ x2 }$ A1 _7 {3 F2 ?$ N
- }
% @; j3 o$ L. R' W3 [! e - }
. o2 K9 _- l! i j8 s2 p0 J2 S m- [ - }
复制代码
: m( p, S2 m8 b" D# p3 \( T5 O其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.# g$ h. q3 s9 N# Z+ _ b% T
% x# M% W) B* z+ q c+ [- R0 q/ S7 A s6 ?
4. 整体测试; t) B& l: T E
本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。 ]5 [# ^+ `( z: z$ @, W
; P6 e3 M. S. J9 o, D' v7 z
源代码
$ P, w9 z% |8 U- N, j9 Z$ ^BootLoader源代码和App1源代码可以在原作者的gitee获取! n# {) y8 |, h& h; C! ^
3 I. |" N# r* u代码的下载: ~3 c+ A J1 O) [; g, _+ Y
由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
0 g9 \4 V4 f7 C! j( C4 O- E& \- i; m! C! e5 D
) x. W3 j9 }" M5 t S 1 G1 `" R0 i v9 y# d* ~1 I+ C
# j% h ]5 p; h+ c( I8 _. |BootLoader的下载
9 e1 K: g! B- M) r3 I6 B1 NBootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
1 j/ ^7 y k5 o按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)& k' ?7 _. ?, d: Q# k) @' o
n: F/ O6 N1 q/ P7 G% j0 o: o$ P: _1 C
( _2 d) ?2 }, r5 m( U
; X! D G1 x2 p, ^6 O5 {$ ]7 o$ U/ R2 U* @: l
烧录代码 a( }3 ?! i M4 ?2 F; i
运行, 通过串口1打印输出, 会看到以下打印消息
( e9 Q5 t: \$ V! P/ g' {, z说明BootLoader已经成功运行- L; S- d% Q0 w, t6 U
/ F! W( h, [. C9 _" g9 K
3 V+ F( Q) e- P" e) T# W
+ s1 ^0 u' t# Y: D$ w
0 J) G# C9 b: P* v' }( m c9 Z# wApp1的下载
2 b; q* E3 g1 f% o, E6 b vApp1稍微复杂一点, 需要将代码的起始位置设置为0x08005000- O$ c, w+ G; |% }3 R
同时也要修改擦除方式为Erase Sectors, 见下图/ X: x& x1 t- ^7 Y
- Z9 \- G5 J* ~ x2 Q
" Q) R; p4 {; i+ j
1 d" g; T/ ]1 u6 S" v# m% q6 _# Q$ ~
+ N. r! [0 u* ^+ d$ b- i- O
$ L1 d+ ^9 u/ u
^9 n/ O* s1 J' }' O8 C
烧录代码
. i8 I9 n# m( @% _) K* D运行, 通过串口1打印输出, 会看到以下打印消息
, q, b% s9 h G5 i/ h说明BootLoader已经成功跳转到版本号为0.0.1的App1) e2 l# h2 [. O. @
7 w6 w% T4 M* r7 T3 L% T( @; K: t" v2 x5 z) U; u) @3 X
* B/ D4 L, Y1 h$ q4 j3 W* T# U+ d生成App2的.bin文件
4 H1 X; H8 q; d0 o; c& u" g/ K( JKeil生成.bin文件- G2 D: a: u, w, V Y+ x/ K/ q
2 j3 j8 F% H5 _) E' Y
修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件. ]/ t1 {7 p1 |4 x: R1 U6 t1 V; b6 A, p
! J0 E5 b8 m& H+ J: M" O N生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件! Y$ D: x& t1 z: z" i
h1 i8 F, `% | B4 m. \; q: y# U$ t) d
1 I- M( `, d0 x, x3 K
. c4 i6 J9 @* q
使用Xshell进行文件传输, x% l6 L# V" j+ n% d8 L. |. j7 O8 S
打开Xshell! g8 y, V: m. ^0 {4 a0 i9 i
代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的 a( q. d0 D! h7 k7 I
所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息' X |/ s9 j3 m7 y1 c
你会看到App的版本成功升级到0.0.2了.
- [$ O9 f% r7 J% M如果你到了这一步.% G, ~. a; L$ C0 I! O
那么恭喜你! 你已经能够使用在线升级了!$ K0 d8 F# [* C- q
$ B" g4 j' M, S* {5. 总结' i) m2 o$ g4 m- Z+ d% m4 i" ^
通过本几节的教程, 想必你已经会使用在线升级了, 只要原理知道了其他的问题都可以迎刃而解了, 除了使用YModem协议传输.bin文件, 你还可以通过蓝牙, WIFI,等其他协议传输, 只要能够将.bin文件传输过去, 那其他的部分原理都差不多.' m3 O7 k0 W- U6 J4 H4 a
) r; [, _" r: b3 F7 ?
转载自: IOT全栈技术0 Z7 ^3 _% k4 z
如有侵权请联系删除: _- k6 |" l2 K) u
) G2 l7 N% l: n" @6 [ |