EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
本帖最后由 Ber_thaw99 于 2020-12-3 13:33 编辑
" \; [( t3 j4 {" a' F* W" M
: m; }% W- o8 M8 t/ Z: ?; i4 }本文是博主在学习OTA时,up主阿正推荐学习的文章,原作者leafguo,写的非常简洁明了,在获得授权后整理发布,可以在文末点击阅读原文跳转到原文章。8 Z0 {5 L0 }4 L7 p
6 H5 M5 V' a3 Q, j/ j% k简介4 e. F5 e7 | H* X* |
) l0 \+ F5 L* T. ]5 v% }
本文主要讲解在线升级(OTA)的基础知识, 主要是针对IAP OTA从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对OTA的认识.
" Y/ F1 |! r* ~( T# l1. OTA基础知识" Y1 n j9 Y! r, [! C7 l# T8 @& b
. x- L) ^* g% g9 B. V* L
什么是BootLoader?
( H( L+ E. [6 ?% d
) _) G, I( p1 h! d$ u( _1 s. `
BootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序, App也是一个程序, BootLoader程序是用于启动App程序的.
" Y3 n) }: G1 fSTM32中的程序在哪儿?
# Z3 X/ m# u0 U. R
4 F3 Z c3 m3 ^4 ?( e# U
正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码
. u* c3 s. z/ q! {. F3 u9 I* g6 u
* c9 P- _! e; i6 U% { j7 }
接下来就可以进入正题了.
+ o* l' q: c* ?0 j进行分区0 }( h# m$ O& v6 o
9 ?* r8 ^; u z- ]# H* ?
既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:
2 Q& A. V4 l1 [1 U
f. Q: n$ f+ J" G; Y& y
以它为例, 我将它分为三个区.BootLoader区、 App1区、 App2区(备份区)具体划分如下图:
3 s# J5 b8 a* JBootLoader区存放启动代码
App1区存放应用代码
App2区存放暂存的升级代码
) T5 C) u9 e5 x" M( Q; I
* R' a! v1 o0 t: P" t
总体流程图
, `& S6 F+ X8 n9 e* R9 D/ M% z0 b- `, l+ I3 G- @+ Q
先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.
然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:
8 b& ~8 u4 t1 v6 i+ n2 i( t
2 x! N! v, z$ v$ f; F9 `# ~+ l
2. BootLoader的编写
! X# Z! h7 m- E4 o4 o+ Y+ W8 x: f( L' o
本节主要讲解在线升级(OTA)的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。
6 A- q2 y! N$ A0 n+ Y( d$ N/ Y流程图分析
/ d, q: d) Z3 {: Z4 ?! B5 v- f; K' @$ w C0 b1 |" u' w5 l- H
以我例程的BootLoader为例:
\ G( E: K0 U- Z+ W+ h
我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示
/ r) Q* M5 F. _0 w8 J" C
% T/ U& Z a& J8 I程序编写和分析3 l/ F/ P! m& C; W. |5 [1 ~
1 N0 o1 o' d" V% J+ @
所需STM32的资源有:
. n, u; a6 D( S4 V发送USART数据和printf重定向
Flash的读写
程序跳转指令,可以参考如下代码:+ i9 M# x9 l- S. G& J* T1 l2 u' Q/* 采用汇编设置栈的值 */% \- D# ~3 x8 t8 f, ?# S/ C) H% C1 ^
__asm void MSR_MSP (uint32_t ulAddr)# @, X1 H! Y' L& K
{/ _" K8 [$ y1 s! _% |+ y
MSR MSP, r0 //设置Main Stack的值
6 e' B- W; M& Z$ c1 ]. N) B( J! r BX r14
, Q: m8 v& I4 @( X9 `0 c% b5 c% h}
7 C; v) O4 N h% p; G- T9 {. Q) Y6 @' q m6 H
2 ^: U* Z) J- ?/ R/* 程序跳转函数 */7 S# A8 k5 q2 y. O5 {
typedef void (*Jump_Fun)(void);% A1 ]% p4 M" {) h3 U
void IAP_ExecuteApp (uint32_t App_Addr)
. [3 u7 W. \: i S0 e9 J0 V6 |# e{\; o9 J7 q1 k' N a3 a6 E; D
Jump_Fun JumpToApp;
) R8 d! u( b# U J; Q* B0 J
: n$ _5 F8 ^5 X: Y9 z/ y" B7 ? if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
3 b* z2 p6 |' j {
. E" Y, M( Q9 s, \9 K- }4 c5 g JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
( g! p0 ]- [: R# z MSR_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)/ x ^2 q( C* X q/ Z5 L5 W
JumpToApp(); //跳转到APP.
Y w& }1 b3 }( v+ N; m }
& p3 J: w6 ~( J# L}
/ E% ]; [) j: ]9 d% [在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);
其他的代码请参考BootLoader源代码
8 E8 A) K% Q% p3. APP的编写" Z5 Y$ Y7 W2 c% g( T1 J. X8 Y
# V; T( g4 t7 B6 A- y
本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。9 }5 g5 G( Z& Q- J# L1 F$ P" a3 Z
流程图分析
6 F* L+ q% |- n, ?' q7 M$ N6 y+ _" C' n; W8 y
以我例程的App1为例:: j' B6 G9 W9 W9 b
先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;
打印版本信息, 方便查看不同的App版本;
本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
! W, R% C1 S: I* V' }1 [
' i1 E, p! `: K6 O. C% j E
程序编写和分析
# a, m6 X/ x/ g* O) t% O
( f% n6 T7 s; v) F
所需STM32的资源有:+ v2 `2 u" M* a9 E3 e4 Y X
发送USART数据和printf重定向
Flash的读写
串口的DMA收发
YModem协议相关5 V! Y7 n; B: ]& XYmodem协议& q2 R% |9 x. C
3 i; j7 v |4 S7 R4 s( u* d3 W百度百科[Ymodem协议]
具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).
Ymodem协议相关介绍可参考我的这篇教程 YModem介绍& x! J# f+ H& w- V" t/ l2 u2 Y0 M1 h- E. W/ a i+ w1 M
代码分析
+ j7 ?5 i8 R( h/ u- Z1 L: ~0 E) U7 R
代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明
后面放了我的源代码, 详情请参考我的源代码.
主函数添加修改向量表的指令1 r9 e+ v5 R9 e/ d/ r
2 f1 D: C0 D. s3 K; Y
打印版本信息以及跳转指令
* N( Q1 G" Q0 x" |
4 `. f% A, a: z1 {9 a9 k1 d$ O- X. T
YModem相关的文件接收部分
6 r; I+ b+ _# D& G6 |# V) k( J/ w2 p5 H& \% g3 ~2 L }7 A
/**
4 u; O* B) J! |- J1 G * @bieaf YModem升级3 k" H( g$ o, i, ~0 i9 B
*
4 h$ d% H( v0 m, W * @param none( }3 y, I- _% m
* @return none
. s8 _- G7 c# f% x+ ?. X( H */+ ?9 X2 n/ \2 F% b
void ymodem_fun(void)
6 i5 |# ?0 t* a! t: ^{
1 o6 {8 B& h) n$ p/ Yint i;
; U" X9 w/ [2 M* F2 ]; m% m. o6 ^7 Cif(Get_state()==TO_START)* h3 l7 R! B: B* j! d
{
! ]2 S5 ^4 N" E) ^" K0 c# Isend_command(CCC);
% i+ M5 U" o O; o( mHAL_Delay(1000);
$ I# i: ~" Q* }}
- U4 T. G/ m2 @* [/ K4 t, |if(Rx_Flag) // Receive flag
- q( o _7 G* X, I1 q/ ~4 A{9 Z6 y2 L; Y* I
Rx_Flag=0;// clean flag* _1 j2 |0 X) v/ B, b
" ^% t$ m; \, H& d5 x, i9 _
/* 拷贝 */1 v* N' F7 R" U# X: n
temp_len = Rx_Len;
, g4 e$ E' Z0 H& I$ c1 nfor(i = 0; i < temp_len; i++)
0 A( U3 l- P0 U6 C' t3 i{~1 A( Z* P: m4 e9 f
temp_buf = Rx_Buf;: K. q/ ]1 F% M& e4 ~0 e$ u
}
9 X8 }" e% }; ?; v2 m$ [5 s/ ^
switch(temp_buf[0])( c1 ~2 B3 y9 G( ?1 Z+ n
{
. l* x' y& n) u! N \1 icase SOH:///
! H' T+ |2 v. S0 s6 c{
3 U6 o* h# |, H; jstatic unsigned char data_state = 0;7 `4 }% D! U$ A
static unsigned int app2_size = 0;
0 f$ M4 j- q4 \2 e5 bif(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验
$ S) i P. X: J* R6 r{* {# F% W {, J4 I2 I6 R
if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始
) T# I8 p( F% c H1 |{
6 h- }- m# L" p- E& N8 L/ x& Bprintf("> Receive start...\r\n");" Z0 Z* F$ j3 @& ? d$ Y' j& l+ ]/ o
# k- H \5 y1 E; KSet_state(TO_RECEIVE_DATA);
D& s1 p3 |5 X7 Wdata_state = 0x01;
o7 T, X2 F, b- w2 w6 U- W9 `send_command(ACK);
2 D8 q* k# U; A" C8 ^9 Wsend_command(CCC);- h% T; I5 G" ]4 l( B
$ o9 V+ @: S0 `+ F- c0 |/* 擦除App2 */
h! l3 @; a3 Z) ZErase_page(Application_2_Addr, 40);* Y% }; t: {) W) n- u% z4 m: c( @1 |
}9 _* Y+ X/ {8 [9 m' c: R% }8 t
else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束8 k3 M7 {1 i' E0 Q1 w; P- R/ b/ n; G$ t
{
1 }0 @* ?% N A" M1 y6 X% f# eprintf("> Receive end...\r\n");
9 ^, z8 l4 W: t6 H8 M9 V+ _. ?8 [) w
Set_Update_Down();
% c) Y) N4 `+ F" ?Set_state(TO_START);
. y& V, V* f6 h3 Z, ^- b+ j- Y: [send_command(ACK);
5 g; E2 X+ q& D* }* q0 p# D: s; pHAL_NVIC_SystemReset();# i1 l* _, w' {* C. z+ Y+ T
}! v& q% d; ]. I( o+ [
else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据
* A4 v! x6 ^( ]3 F# S) g) J{
5 v& x. ~5 B3 i w. tprintf("> Receive data bag:%d byte\r\n",data_state * 128);$ ]2 B. I/ m/ K" F1 W
9 G9 y' q H8 K+ e5 d/* 烧录程序 */
v7 e% G9 v" X9 m6 B6 BWriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);
& T9 h# I6 @( L# d, ?' N+ \: g# Edata_state++;
# a) o& B; x% iX; k% m3 y' ^; x- p
send_command(ACK);
; X- e: _8 ^' s7 k* z, [}
) t* Q/ v- V8 E}0 o* D" d( G! x! H
else
! p: X; W1 y. u8 `% B; Y{7 B" l. N! K; H8 ~
printf("> Notpass crc\r\n");G/ n* F0 V2 |0 C6 b3 P
}
5 Q& \! [) d+ E: v8 M
, R/ M2 ?9 t+ w}break;
% i+ }8 r1 h6 G2 \2 Ycase EOT://数据包开始6 Y3 I) R# ^' j' _6 m$ b1 ^
{
" O- A0 \. O" a, Qif(Get_state()==TO_RECEIVE_DATA)1 S4 z& t7 H+ Z% O1 a- d
{1 u# t$ O" y' R
printf("> Receive EOT1...\r\n");
3 I; _ C) {! {, J0 h
8 I: E9 r& A; l7 k0 x% mSet_state(TO_RECEIVE_EOT2);2 m9 q- b) y( |$ Y) G
send_command(NACK);8 D" `. _* f% y) X) Q
}
' w6 A" L) C/ M! P8 C/ Melse if(Get_state()==TO_RECEIVE_EOT2)
) c! t# P; H" o2 [2 {2 F5 f{: N, T! l+ @1 p: w5 c5 v) D
printf("> Receive EOT2...\r\n");
: f( q5 ^' G# ], I: X; H) A0 W& r7 ]) x8 i* d. ?7 ?: f2 j7 s
Set_state(TO_RECEIVE_END);( k6 c$ |7 k% |- T8 {/ Z8 p+ O+ L- _
send_command(ACK);
v) \( ]# m" W; usend_command(CCC);
! g% l G: C( \9 g}a% r# {, ?) f/ k
else4 t4 p7 @1 v: _7 S7 F2 S
{
6 k+ H4 o* D2 Uprintf("> Receive EOT, But error...\r\n");3 S) t; }. R- ]& |3 A' ]: c! x
}
2 j$ q; S4 s* t2 Q, v/ P}break;$ B; g- R+ a3 U5 {* }
}; j! f1 T% n% c5 b- v
}
, d" X0 ^- T. G}$ w5 W% |! c% `
其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.+ b% p, s7 v0 r6 z' @& H" T4. 整体测试8 p7 o9 U7 `7 R. _& J; ~( R' \
+ J6 w* p4 F, G7 Y: o0 I; {5 P4 X G
本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。1 F. _+ r8 [* x9 o5 O/ M: K
源代码; H" U( i% ~; U5 r$ L& l% W5 O N
2 r3 g! r5 F4 C1 Q: I. c f
BootLoader源代码和App1源代码可以在原作者的gitee获取:6 w6 @+ R2 G5 x
https://gitee.com/leafguo/leaf_notes/STM32CubeMX/STM32CubeMx_OTA
( N4 M" H3 T, u. L代码的下载2 g, X* l" F4 A9 I/ ?' Z$ _2 X
9 i& a* y" J* K0 v6 E: O# j
由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。
- V- I; M7 K, B( O* i" v2 {8 t
8 u& c+ b7 O! E8 R4 U+ S. a
BootLoader的下载
; P* y4 P; A- @$ E8 Y' C1 [0 o/ Y$ ]& [4 p$ t# j H
BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置
按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)1 S. q3 |( B. p
/ Y' Q6 F( E) [8 v# e" m' N
烧录代码
运行, 通过串口1打印输出, 会看到以下打印消息
说明BootLoader已经成功运行
r* _: |( T5 z5 U. K2 N
1 \* _( a {8 D0 r, }1 Z) BApp1的下载, \$ C2 Q6 }3 h( G# t6 Z! @
8 W0 `6 T& y3 B/ ^App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
同时也要修改擦除方式为Erase Sectors, 见下图: c5 I# n4 q: U) \; Y
- M0 ^) ]) a$ f! B; l. {9 E
E! Y& p& F2 p' b7 @
烧录代码
运行, 通过串口1打印输出, 会看到以下打印消息
说明BootLoader已经成功跳转到版本号为0.0.1的App16 m6 R2 ~8 {* a
6 b; m5 s. E) U% B6 D
生成App2的.bin文件
2 M, y5 V K) M( ~8 A" m5 g$ _1 s _+ N$ H& O3 z
Keil如何生成.bin文件, 请参考这篇博文 Keil如何生成.bin文件2 ~$ n6 A0 A9 o0 o7 \4 S
https://blog.csdn.net/weixin_41294615/article/details/104656577, Q7 {6 L! f9 L3 c
修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件- v( {8 ^9 _# c: n. t+ Y t
& @9 C, l. [+ x% v. G8 i( u( c3 _使用Xshell进行文件传输
3 ]- u5 I: F! u, t1 Gk2 u! \2 O& a6 t, p" E3 J
打开Xshell
代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的
所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息
你会看到App的版本成功升级到0.0.2了.
如果你到了这一步.
那么恭喜你! 你已经能够使用在线升级了!4 K- X/ E3 T; k$ S" |6 f4 m4 V5. 总结
* H0 `1 V5 l" v: Y# e" o) A/ C1 y! x1 ~3 ~
* F# I% N7 `4 ~7 q( a2 R
『本文转载自网络,版权归原作者所有,如有侵权请联系删除』