你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

STM32经验分享——Bootloader详解(1)

[复制链接]
STMCU小助手 发布时间:2022-8-28 20:55
当你熟悉了程序的仿真和下载,你就应该了解一下IAP了。本质上IAP和单片机内部固化的ISP程序一样,都是负责帮你把新程序下进单片机的FLASH。那为什么还需要IAP呢?
( i7 |( c' A- l9 K" W- e2 P5 Y$ Q9 V9 _/ w+ {$ r6 t  e+ z
        举个例子,ISP的启动一般需要硬件控制BOOT0,因此对于加USB转TTL和三极管电容电阻等组成自动下载电路这种烧钱做法一般做产品肯定是不提倡的。而即便是不使用自动下载电路,也需要你手动去设置这个管脚,而产品批量的版本BOOT0一般都是直接通过电阻接地甚至电阻也省略了。所以很难做到像开发板一样轻易使用ISP。这还是在硬件方面,你还需要抱着电脑连上位机去发送hex或者bin文件。即便是用下载器,不还是需要接线连上SWD接口才能下载嘛。! g; A% }/ A- f) W8 c
1 J# i. v& Z  R: o. ~2 y
        那如果是产品已经装壳了呢?如果还是那种胶封的防水外壳,那么这时候拆壳下载很明显外壳就报废了。而基本上产品也没有把下载接口引出来的,比如你家的路由器、WIFI灯泡等等。再例如如果是太阳能路灯控制器呢?如果是池塘水质检测器呢?总不能下一次程序抱电脑爬一次路灯杆、划一次水吧。虽然也有很多的脱机下载器了,但是终究还是需要连接产品去更新。如果是传感器节点的话可能同时还会有成百上千个产品需要下载程序。
3 n! w) T: L2 V& N8 n5 G4 G1 ]; {  w( ]5 m
        因此出现bootloader是必然的过程。通过前面的内容,我们知道下载新程序的办法主要是两个,ISP和仿真器。操作都比较麻烦。一个是系统存储区程序,一个是RAM存储区程序。那么IAP就是主存储区程序了。' y# p! }  |7 L7 S( ^2 l: v

  @5 Z. x' X' e% x# a7 n        IAP就是In Application Programming,也还是编程,也就是写FLASH程序。对于主存储区(也就是0x0800 0000那块存储)里面存储的就是用户程序(User Application)了。那么In Application Programming也就是在这块区域写FLASH。% ~) w, V0 P9 y' ?9 E6 T2 ]
# U# H* z# ~/ u+ ]2 }5 A5 _
        前面我们分析过,程序就运行在这里,那么再写新程序的话,无疑会把自己写挂掉。而IAP又无疑是在主存储区运行的。那怎么办呢?+ C9 X, n& W! Y

) [! Y+ q1 N- l  m1 Q        我们首先翻到复位中断入口处的程序。
$ G8 v+ G9 v8 M4 N3 A
EV(~O{EJTN%_]A7XIR]R~VY.png

' u& P/ P8 q) y1 F: E' F, A7 q/ C3 K( Q: A" j
复位程序入口

: v% b1 m  `5 P8 v3 G9 _+ C+ R+ s8 n7 W9 {) e% c6 u' Q
        然后跳转到系统初始化程序SystemInit,在这里我们可以看到中断向量表偏移的设置。
2 [5 u0 p5 D. n9 T4 V9 d& ~" o4 f4 |7 O: h. [  I
9)}4C%[PE%D}Z)ME(BCI.png
' i' m/ O+ S' O. [9 D! |
, E3 B) a3 C( d2 Z
中断向量表偏移量设置

# L7 x  f# h3 c0 ~, w6 z* E3 Y6 c
5 `) U* C, [, N* V8 O8 M* x
' J+ b2 z* y( Y- O# T# x. {5 f% V) o! j        由于工程中未启用宏:VECT_TAB_SRAM
  r& d2 ~/ D" G7 m, C" S% i( S' ^3 l3 p. y% z% J* Q! ^
O}V)XD[5MDP}12_72YUGEBQ.png

* w+ d. e5 O) V( Y, Y$ k' L1 ]0 l1 U; d& H
MDK工程宏定义

$ A; p5 |$ F: U2 _4 O" g& [- ~" ?0 S
  d  b' d4 C9 c/ A) y
        所以执行下方的语句:SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;翻译过来就是:内部FLASH中的向量表重定位。
+ [! f% r! O; k
' e% h" F9 @' z' Y$ [
YR6IA4LVVNG~CXGKJ}R3}%1.png
5 f5 k# ~3 `4 C/ i4 R
, E1 _9 Y! k6 y5 w
FLASH基地址定义

! V% i' f2 Q& @  A9 K3 W# q8 S6 Y  v3 G: a

9 h4 s4 D5 ?' g. D0 C+ \        说人话就是中断向量表设置的0x0800 0000,也就是我们的程序运行的位置。
: @1 V  m6 s' I3 E- P- w0 m
* \2 e4 r* A5 W# z4 c( [3 ^, Q2 }7 k        我们知道中断函数也是个函数,并且中断函数在发生中断时可以及时响应运行。那么CPU调用中断函数的时候就得知道中断函数的入口地址,而MDK完全不知道你用的什么单片机、有几块存储、多大存储、存储在哪(FLASH地址)等等。所以需要设置这些内容,当然这些东西你当初装的pack包都帮你做好了。
+ y5 D8 F# k4 T, U+ }" e; x4 `1 q0 b# h5 B. H5 U- Y
        其实如果程序是成0地址(0x0000 0000)运行的话那就不需要偏移,但是STM32的FLASH是0x0800 0000,所以这个入口地址要加上0x0800 0000(关于为什么stm32的FLASH地址为什么是0x0800 0000可以查看安富莱的帖子:【不是问题的问题】为什么STM32的Flash地址要设置到0x08000000),而如果是F7的XIP模式,那么外置FLASH的地址还需要设置,比如F730、F750就是0x0900 0000。
- j' F0 Q# h5 a) O
& g, }' Q+ P/ ^% H8 l" b
E6QJ$E4DS_S3~D4H5G}PY4U.png

8 t% k/ h3 D2 t# g* p& r% f5 }5 y4 `8 y4 ?# M1 U, ?
DM00514974.pdf 13页
! f8 m* V1 \3 v8 t9 L! I7 X/ m
, @3 I; o  R) z; {
        欸,不是说IAP嘛,怎么又扯到了中断向量表?因为存在中断像量表,并且是可以设置的,所以才具有了灵活性。比如我们给它往后挪一下呢?
- O6 l$ ~6 z& g0 o: r3 m
0 ^9 s4 l; \4 L  w        比如我们把中断向量表设置成0x0800 0000 | 0x10000,也就是FLASH_BASE | VECT_TAB_OFFSET,定义VECT_TAB_OFFSET为0x10000,这时候程序就在FLASH基地址也就是0x0800 0000往后64K的地址跑起来了。
3 d) y* {) A0 Y4 n" t3 j+ T6 ]" h
        是这样吗?并不是。从主存储区启动的时候程序依旧是从主存储区地址:0x0800 0000开始跑的。对劲吗?不对劲,不对劲吗?好像又对劲。如果电脑这么设置可以的话,那系统崩了是不是就没法重装系统了。所以从主存储区启动是对的,但是目前还没法跑挪过去后面的那段程序。
) b# |0 b! }/ X. |3 a  U
$ X3 A$ F+ ]% C; O" X3 I+ W        再回想一下中断怎么执行来着?是要知道中断函数的入口地址对吧。那么我们也这么做。首先定义一个函数指针,然后把函数指针指向往后挪的那段程序。是0x0800 0000 + 0x10000吗?不是的。4 P9 T1 Y+ k& N9 b
& J# o. ^6 ~& G! A
        回顾一下你的开发板上电第一个跑的复位中断程序,也就是Reset_Handler。它的地址在哪呢?
  N+ r% c# ^7 s+ w+ }1 c0 R3 n+ x2 ~2 U$ L  n9 H
81BV{}E`9[UZVL_AJ)W(0`0.png

+ p2 D# t; _' c1 W2 x& W2 p
0 p3 }: y' i! J- k4 F
中断函数偏移地址

  k; q' x1 Q; h' y2 E' T& g" O6 f% w, W

; t( V2 K8 U6 O2 H        是的,它在4字节的位置,也就是一个32位数之后。所以你的新程序的地址还要+4。
# S$ Y+ i- y. p0 S+ N( G1 }) r; i' N0 w$ `' i) h
  1. typedef  void (* iapfun)(void);
      R( p6 A9 ~: r: b; R
  2. iapfun go_app;* h: M" C$ Y+ w% c4 f
  3. go_app = (iapfun)*(__IO uint32_t *)((0x0800 0000 | 0x10000) + 4);- O/ Z' L! Q, I. a+ Y! T, c
  4. __set_MSP(*(__IO uint32_t *)(0x0800 0000 | 0x10000));
    ; |. S% [( i7 Z& y& Y
  5. go_app();
复制代码

" E5 k% }' `' N. n        新的程序还需要新的堆栈,因此再设置一下堆栈指针。之后直接调用函数就可以了。
7 @3 Q/ ~+ M( b, ^+ h/ Q' z3 R& [2 U) |4 \; j: {& \3 t
        栈顶指针也就是程序最开始的位置,即0x0000 0000,比如像这样。
6 h8 J4 t, w1 h5 ]- C
0 d4 h8 d  V4 V. ]
30E~15Q{}TJIHDPB86(ND8G.png
5 u: F( D4 z3 h; ]4 |
& j0 ]0 R6 o7 ^# H3 [: K
栈顶地址

. x; G+ A* [5 g
* k' _) X& k( A  S0 _* }: m0 e; n2 M! G1 e2 [
        同样的,往后4个字节就是复位中断函数入口地址。7 A* W# M0 s: q0 Y/ K
+ W) M* D* c, T+ T4 I8 Z
%UV4`FG`B4}F1WS)$Z0O(ET.png

3 U! V/ p, g% d( J6 [6 x' D6 S
2 o; o1 }# d8 s4 q
复位中断入口地址

5 a# v+ ?9 E8 z/ k6 l( o  O
0 M9 w' q$ e; |0 G
" s6 ^, _0 K# F2 H* x1 i& L        这些说明在Cortex权威说明手册里有写。7 S1 E  @$ `, C$ `

% W( V" U& Z3 g2 i
{~IV2J$)VM0O}~M4X)B~L.png
- q. Q& U/ D) N4 P$ j
8 N' V6 S2 h3 u* e
Cortex M3/4权威指南114页

( P. t0 K# E& U, Z
5 i2 q: n0 v7 W3 q/ T0 u6 ^4 D. [
         我们的程序不是存储的0x0800 0000吗?为什么在0这里也可以查到呢?这就是存储区重映射的。其实在0x0800 0000也是一样的。
: r: Y& r1 C( b4 @, k
1 F! H  {2 w; Y
OF5X8CRT[1[Z~QRFV7}L3W5.png
$ o( w! X" i  z3 k& S3 x# p

  W+ H  H' ^) R* A8 L# A- T+ C4 w
系统存储区数据
  K8 D4 ^# r' M7 {/ G
% h+ U2 n$ A$ e0 ]' G: P2 |
        好了,到现在为止,我们已经可以通过主存储区先启动的程序来创建函数指针然后让往后挪的程序跑起来了。还有一个问题,怎么把程序写进往后挪的那个位置呢?这其实就比较简单了。之前我们说的FLASH编程就行。把新程序编译出来bin文件。然后再用首先启动的这段程序把新程序写到往后挪的那个位置。4 W% N# T" u/ ^  ^  m

" P9 e! T& d2 q% a  G' E0 G
307_%WKZ10QZET[CECX7(6O.png

/ y) ?& O& B: U' F2 X1 X$ L5 L  K4 o# z$ c2 J7 _' l
        最简单的写法,把新程序存成数组,这样第一段程序就包含新程序了,再往后面写一遍属实没必要,所以一般不这么淦。
. x. c. J( [/ M, s0 P
5 P  M0 ^2 Z5 t+ t9 u( `        反正还是把新程序变成数组写进FLASH,那方法可就多了去了。跟ISP一样,用串口把新程序数组发给第一段程序让它写到后面。这样就行了。又或者你用SPI、CAN、USB等等,或者直接用文件系统,让第一段程序从SD卡或者SPI FLASH里面直接读取bin文件写到后面。当然如果你用了文件系统,或者屏幕显示,那么可能第一段程序就比较大了,这时候就得把新程序再往后挪一下了,比如0x0801 2000。如果没有串口、USB、卡槽呢?比如你的WIFI灯泡,对的,你还有WIFI和蓝牙,说白了它俩不也是串口嘛,所以你还可以无线更新,还是串口。
. d; y9 B& W# v" ]. S# r% E5 \; x6 b  r5 Z+ Q% x
        这样算一个IAP了吗?还不行,为什么呢?总不能每次插电都写一次FLASH吧,FLASH写寿命相对来说还是比较短的。这时候你可以做个标志位,写好之后就标记一下,下次上电开机就不再写了。但是,你如果还想再接着更新新的程序呢?显然这时候又不行了。
; X. u& [, d0 m7 D1 Q* v# r7 B- e$ i/ O
        比较常用的做法是,正常情况下不更新,比如100次开机可能99次都是正常使用。那什么时候不正常呢?正常开机怎么开呢?怼一下开关或者长按开机对吧,如果还有其他按键,我们可以让第一段程序检测是不是开机和其他的按键一起按下去了,是的话就更新,不是的话那就是正常开机,不去更新程序就是了。比如你的手机开机的时候和电源键一起长按音量-就可以刷机。
" |- a) q# O8 e/ q6 B# S, u# l# Y/ q, g3 C
1MPE8DK7~{AGR}D%LATRW6W.png

" L6 y6 D: M3 k. u4 h1 v
" S+ w- D  \- V! `+ V/ j7 {        对了,我们的标题不是Bootloader吗,怎么说了一堆的IAP呢?其实实现IAP功能方便我们更新程序的这个程序就是Bootloader,由它来引导新程序的运行。它总是最先运行的,因此才能引导和更新别的程序。就和linux的uboot、windiws的BOOT一样,它在一开机的时候就跑起来了,如果你快速点del按键就能让它不引导windows,这时候你就可以装新系统了。+ V2 w/ S$ d( e% g7 @5 _( m9 h( k

1 w& ?3 T. y8 T1 K2 K        再来归纳一下Bootloader,首先它是系统运行时跑的第一个程序。在stm32单片机中就是0x0800 0000这里的程序。其次如果没有更新需求(比如没有特殊组合按键按下),它就引导新的程序,就是跳转过去执行。如果有呢,它就从别处更新新的程序,然后再跳转过去引导刚更新过的程序。Bootloader还需要知道自己的体积,把新程序写在自己的后面。或者再往后写一点留一点空间,比如后期需要换成SD卡更新呢,可能之前的空间就不够了。
+ Z& y0 B. b" H6 C8 S  {& q+ C! g% J
% U9 ]% G8 n# _" y* T  g        对于新的程序,需要知道自己的位置,然后把中断向量表偏移过去,也就是自己存储的首地址。就像bootloader和普通的程序同样都是偏移到0x0800 0000一样。而新程序还要在此基础上加上自己存储的偏移量。比如0x0801 0000。7 T9 J: `( ^. O* ~% P
) M" O( x2 |  k: Q8 B% n$ l
        至此我们才设计完成了一个完整的最简单的IAP。
2 k  g* G# V; [1 T! V
4 m7 V' j: ]  Z2 f        在此基础上,你还可以设计多个新程序,存储在bootloader之后的不同的位置,选择性跳转,想跑哪个跑哪个,想怎么跑怎么跑。或者你还可以在新程序中设置变量,让bootloader重启的时候根据这个变量来更新,又或者让bootloader自己搜一下SD卡或者USB接口什么的甚至联网查一下有没有新的程序,有的话就让它自己更新。* k6 Z( j7 T  P/ ]

/ ~; z+ k. l  @% d        最后值得注意的是中断,因为bootloader是函数指针式跳转,不是重启,所以bootloader中有的中断服务函数在新程序中找不到的话,可能中断时找不到入口程序就崩了。因此在运行新程序的时候关闭所有的中断,之后再跳转。或者Bootloader程序直接触发一下复位。即调用__NVIC_SystemReset。
7 H/ G1 U& l% d- o. r作者:mymymind
$ L' x5 o  r7 _! q5 J9 r! ]  c9 a/ G+ |) J
- ^& \! j9 f/ u4 X
收藏 1 评论0 发布时间:2022-8-28 20:55

举报

0个回答

所属标签

相似技术帖

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版