1 前言, q2 a1 L2 i- s) J6 v! M
本文根据 2017 年度广州 USB DFU 培训内容进行整理而成,主要目的是为了方便那些由于各种原因未到现场参加培训的碟粉们参阅学习。本文主要是介绍如何使用 CubeMx 这个工具,一步一步制作一个 BOOT(DFU)程序,并使用它来升级用户 APP程序,这种应用场合在产品开发中具有普遍性。. s1 E) F U( @3 x. g
2 实验环境介绍; i1 ?. g! c0 y
2.1 实验目标
3 T; B* m# L! ~" a$ ]- l
; X2 ~# d$ g) ~( D
1 g) i/ w' H* q9 R! ~% V1 T" M# W( p
如上图,本实验主要目的是制作 BOOT 程序,并通过 BOOT 来升级用户的 APP 程序,上图是 BOOT 和 APP 在 MCU 内部FLASH 的位置示意图。当然,我们也将简单的介绍这个用户 APP 需要注意哪些方面。
/ E% L" Q, H& l注:这里我们将使用 USB DFU 作为 IAP 的这个程序叫做 BOOT 程序,功能与内置 BootLoader 类似,主要负责升级 APP 或直接跳转到 APP 去运行(MCU 内置 BootLoader,为了与其区分,这里取名为 BOOT,后面不再解释)。
( n5 z. ?+ p7 p; h4 p+ I# w, B+ |4 X3 m* Z
2.2 实验环境及 STM32F072-Discovery 板简介
! z5 j6 b6 y* l硬件平台:! r& f$ k1 @% G! C7 w
STM32F072 Discovery 板一块 _: s) u! T" u: B( Y/ ]6 |" ~! i
Mini USB 线两根& i F# H& I6 h% r- M
PC 一台7 V/ F& x9 K6 f+ P; x8 ~1 ?
软件准备:2 C$ M+ `9 f% c" A! \& Z
IAR V6.7.0 或者以上版本9 g' L7 q& t9 ^( L- i8 H. }
STM32CubeF0 V1.7.0
) H. R1 t% W7 ]' ? STM32CubeMX V4.19
& k+ m0 |$ H* I" C DfuSeDemo(V3.0.5)& ^, S5 q0 q: ]; G
( ~8 m/ T, V( u2 u( j* t
- o K( G( j. [
3 制作 BOOT 工程
: e9 d6 G4 ?5 K1 q' \3.1 了解 MCU 内部 FLASH 的组织结构
+ L. B0 |- O1 ~- T- h: A! N
* T! L# I' W! X) @8 n, F
; a& Q4 h- a# Z' h4 U- \
& a R( @2 y( t) i3 M, Y1 y% p如上图,BOOT 程序位于内部 FLASH 的起始位置,其后跟着的 APP 程序假设放在 0x0800 7000 的位置。也就是说前面0x7000 的空间用来存放 BOOT。6 x: T5 S/ u9 I* l
在制作 BOOT 程序之前,首先我们必须得先了解目标 MCU 的内部 FLASH 结构,这个是与我们的 BOOT 程序相关的,因为不同的 MCU 类型其内部 FLASH 组织结构不尽相同,尤其在擦除 FLASH 的时候,我们必须得知道擦除是以页为单位还是以扇区为单位,以及总共有多少个单元,每个单元多大等等。
' d" a! O( y* j! T8 W这里我们实验的目标 MCU 为 STM32F072RBT6,打开其参考手册,得到其内部 FLASH 的组织结构如下图所示:
# `& \' o! u) s% o3 E3 h& N0 N9 H& f1 {
8 b" D9 d: n; M9 k) g3 Q' m5 Y% `) k3 {' b2 e3 M
从上图我们可以得到如下信息:; v E( }; h( K! a
1> 内部 FLASH 起始位置 0x0800 0000,结束位置 0x0801 FFFF;
9 {5 F2 i. L" P- Q2> 总共有 64 个页(0~63),每页大小 2K,共 128K 大小;* t' O8 c5 o t
3> 擦除 FLASH 是以页为单位。& S. j* ^- B7 [3 f3 e3 ?# F; B
0 X. i9 s0 F2 x O. {( v3 t8 v: A6 E- n
3.2 制作 CubeMx 工程& q: c$ X! o9 V
下面我们来从零开始为这个 BOOT 程序制作一个 CubeMx 工程,在选取 STM32F072RBx 作为目标 MCU 后,使能 USB_FS外设,并将 PA0 设为 GPIO_Input 模式,PA0 对应着 STM32F072-Discoery 板上的用户按键,我们将在程序中根据 PA0 管脚的电平状态来判断是否直接跳转到 APP 还是进入到 DFU 模式,准备升级用户 APP 程序。
. x! N( A% L+ A7 F4 y1 a1 L! v8 G8 }
/ d+ [8 b8 Y2 X0 l: Y3 x3 z0 [( H' k2 A$ c% }5 }, ]
接下来是时钟树配置:" K0 @6 Z3 l( `7 {* S* h+ Q
3 Z, B. M! h0 y9 B6 ]
' Z7 ^! j% c$ d# Q% u3 B- G4 V
4 ~6 K1 ?3 @, C: t2 z
如上图,我们使用内置 HSI48RC 作为高速时钟源,主频配为 48MHz,另外需要注意的是,USB 外设必须给它提供固定的48M 时钟源。8 \6 `3 ^5 e6 i
6 F' K, g/ O4 H& v% |; b. |2 N
8 `! ^& B x( a; H: f( F
- S# ?4 B% v" \9 z* Z如上图左边,为 USB Device 配置 DFU 子类。然后点击右侧中间件下的 USB_DEVICE 配置,弹出如下对话框:& _: `9 V( K X1 ]6 Z" H
% j, z# w9 }2 ]$ N3 I
/ N/ \% D8 H7 s( [7 P* F" b4 @6 X! W* a, j/ X/ A5 Q ~2 ]1 e) S2 E6 N
如上图红框中所示,USBD_DFU_APP_DEFAULT_ADD 为用户 APP 程序的默认起始位置,这里设置为 0x0800 7000, 即0x0800 0000~0x0800 7000 之间的空间,都可以用来放 BOOT 程序。USBD_DFU_MEDIA_Interface 为 DFU 媒介接口,也就是 MCU 内部 FLASH 的组织结构,这里用字符串表示,其值为: @Internal Flash /0x08000000/09*002Ka,55*002Kg;* T+ W5 N1 v: T) O5 s; r% \
@Internal Flash: MCU 内部 FLASH 在 DfuSeDemo 这个 PC 软件中显示的字符串名,这里显示为”Internal Flash”;/0x08000000 : 内部 FLASH 的起始位置为 0x0800 0000 ;/09*002Ka : 9 个只读单元,每个单元大小为 2K,后缀 a 表示只读,一般用来放置 BOOT 程序 ;55*002Kg : 55 个可读写单元,每个单元大小为 2K,后缀 g 表示可读写,一般用来表示用户 APP 空间。
2 R/ h+ ], W% t8 t6 a9 dBOOT 程序通过这个一个带某种特殊格式的字符串来告诉 PC 端主机的上位机软件 DfuSeDemo,当前连接的 MCU 内部FLASH 的组织结构,这个字符串的表达方式,是通过 BOOT 程序与 PC 端软件 DfuSeDemo 预先约定好的。最终在DfuSeDemo 这个 PC 端软件可以显示当前连接 USB 设备 MCU 内置 FLASH 的结构,这个在后续将会介绍到。
7 {2 J: n& C8 X; Z/ U0 g( ~" {# ?最后生成代码时设置下堆栈大小:: r# u/ r* S J" e0 ^" V: H5 W: F! Q
4 d; J) R5 w2 }" O* F, d6 u
7 z6 C4 o5 M, T
# ]+ N0 c2 [* m& G6 O, G/ W如上图,我们设置 BOOT 程序的堆为 0x500,栈大小为:0x2000; 并生成 IAR 工程。! c, G. X" O5 V0 Q
/ [) K' B# N% q6 Z# l
3.3 完善 BOOT 工程& ?2 I @; y0 ?, s. o, W
生成的 IAR 工程如下所示:
0 u8 z6 I: {) Q! z. W3 i$ F: i9 u: `! X4 h% |3 a8 n4 e
6 h' ?5 D+ x9 v: ^2 k; g
& ]- j. i7 p3 P$ X
CubeMx 工具已经自动帮我们生成了大部分代码,绝大部分我们都不需要修改,我们主要是根据 STM32F072 这款 MCU 内部FLASH 本生的一些特性来修改 usbd_duf_if.c 源文件,其次,根据应用上的一些特点,我们稍微修改下 main 函数,使其达到我们需要的结果。这些我们都将逐步在后续介绍。
( c' d/ R- `) Z0 R. |% `, x+ L; b: c: X
3.3.1 usbd_dfu_if.c 源文件的修改8 b6 i& K9 p& \9 O# r: t
usbd_dfu_if.c 源文件主要是实现对 FLASH 的一些操作,主要有这些操作:& o7 u$ I4 w9 V9 {0 P e3 E* p( R4 l" G
- __ALIGN_BEGIN USBD_DFU_MediaTypeDef USBD_DFU_fops_FS __ALIGN_END =7 j9 C( s0 w7 h, Y1 t9 U
- {
9 @' |8 W+ R& }3 ?7 q - (uint8_t*)FLASH_DESC_STR,, C9 F. d0 W4 _; |/ K
- MEM_If_Init_FS, //初始化
$ d6 Q/ t& Y, b3 P - MEM_If_DeInit_FS, //反初始化# u7 D2 T5 y+ ~
- MEM_If_Erase_FS, //擦除 FLASH
, z4 d0 D$ t# r6 `$ B - MEM_If_Write_FS, //写 FLASH
+ f) ^% A" E" c0 K - MEM_If_Read_FS, //读 FLASH
! W+ m; ?& w" D( \7 s - MEM_If_GetStatus_FS, //获取状态
2 y; F# Y: x$ @; O) N - };
复制代码 2 i( v; T% d( X$ H/ |) u i
CubeMx 自动生成的 usbd_dfu_if.c 源文件中对这些函数都已经生成了大概框架,用户只需要完成其具体实现内容即可,这好比做填空题一样。5 ^+ M3 g9 k1 o. Y
初始化:
: q' B. T5 S1 G- uint16_t MEM_If_Init_FS(void)8 K, ^. j' ]8 ?- q" I
- {: B6 v8 F" J9 n( F$ ?6 T$ y
- /* USER CODE BEGIN 0 */
! A3 ?( ^9 l# Q- t5 X - HAL_FLASH_Unlock();# O- v9 m7 X3 @# l7 V
- return (USBD_OK);
6 j, \ @2 M! |2 R7 G - /* USER CODE END 0 */
) e; c+ b8 R" }. l! G; d - }
复制代码 w" \6 P9 q; c& d1 r8 I ~* R, q
在初始化函数中,完成对 FLASH 的解锁。2 J& ^5 _0 a5 ]# C( N+ H
反初始化:
) n. |# P. E9 O7 w- r% Z0 H- uint16_t MEM_If_DeInit_FS(void)
( a% W$ O5 ?7 a8 l5 L - {
; o; S" p0 k: O+ \1 } - /* USER CODE BEGIN 1 */
' ?4 k8 k# B/ Q9 _: W5 j - HAL_FLASH_Lock();
/ v( a: H' D& k4 x# H L t - return (USBD_OK);
5 Z6 A. a2 X0 t$ } - /* USER CODE END 1 */- V+ l! \% i) W) G0 N# p2 k
- }
复制代码 & p$ A) b/ T9 y U2 _
对应的,在反初始化函数中,恢复 FLASH 的锁定状态。
@, Y0 j! D1 Z8 W! n 擦除 FLASH
5 T: r7 p7 C# }- uint16_t MEM_If_Erase_FS(uint32_t Add)
) U6 s; |! A: P: M3 | T7 X: q - {
. f; z7 W" K+ L6 p) _ - /* USER CODE BEGIN 2 */7 F: P- G# T7 Z7 s8 \3 H
- uint32_t NbOfPages = 0;
; E4 ~* W1 ]( M( [0 E) w* j - uint32_t PageError = 0;
7 X3 {3 {: E5 M1 c - /* Variable contains Flash operation status */
+ V% P, g7 e9 G" H& Y( `' f - HAL_StatusTypeDef status;
: M. G3 Y/ P n4 p8 ? - FLASH_EraseInitTypeDef eraseinitstruct;! {8 P0 h, w# u4 z
- /* Get the number of sector to erase from 1st sector*/. U6 n9 \6 t' q: l
- NbOfPages = (USBD_DFU_APP_END_ADD - USBD_DFU_APP_DEFAULT_ADD) / FLASH_PAGE_SIZE;3 y5 B+ V3 p0 [: _& H
- eraseinitstruct.TypeErase = FLASH_TYPEERASE_PAGES;
9 c# L9 @. }8 Z; o2 v$ f" B - eraseinitstruct.PageAddress = USBD_DFU_APP_DEFAULT_ADD;
: r. B# y+ t) e# t/ @: S$ c - eraseinitstruct.NbPages = NbOfPages;3 h" S8 a% l2 U4 k: Y& o
- status = HAL_FLASHEx_Erase(&eraseinitstruct, &PageError);3 `/ v8 _# [8 W$ ]% x! t7 i+ M
- if (status != HAL_OK)
5 K# t/ L# {7 D4 k* i5 R8 D - {1 H5 D% t- s8 C8 N" x" B1 _. L
- return USBD_FAIL;* Y- P$ [; A8 c5 P
- }% F9 c; g& n4 l1 T7 N6 U7 I
- return (USBD_OK);
3 w0 ]$ q7 B9 D& e+ |, A - /* USER CODE END 2 */. z3 M7 p& y+ n' A
- }
复制代码 ; A7 s/ u. E4 D1 r$ m( k [
需要注意地是,这里擦除的是指擦除整个 APP 所在的空间,因此,将会擦除掉 page8~63 的所有内容。
: a: C/ {$ j0 A( p上述函数用到的一些宏定义如下:9 ~/ r# |1 k8 b! I0 l
- #define USBD_DFU_APP_END_ADD 0x0801F800 //ADDR_FLASH_PAGE_63, AP 程序结束位置" W2 x, N K J+ ?: D! G
- #define USBD_DFU_APP_DEFAULT_ADD 0x08007000 //APP 程序起始位置- c+ [4 @' n0 d# O' c/ G
- #define FLASH_PAGE_SIZE 0x800U //2K 大小/页
复制代码 ) d1 ?! a; g# O# U1 B6 S- R0 r
写 FLASH
3 x/ w5 z* o! s* i* }' T j- uint16_t MEM_If_Write_FS(uint8_t *src, uint8_t *dest, uint32_t Len)
+ Q! r4 U9 `6 N - {
- [) s: `7 v2 O; ~2 ~ - /* USER CODE BEGIN 3 */" B/ \/ H9 ^+ G% c$ v
- uint32_t i = 0;
, f( `. ? F9 k* `! ^ - for(i = 0; i < Len; i+=4)
& E/ f- y0 o: U" t - {
0 k& y8 q0 o. o' y2 d4 z( F E$ x) [ - /* Device voltage range supposed to be [2.7V to 3.6V], the operation will
3 i6 m7 L% C8 k# ~# V - be done by byte */8 ~7 n" y6 \0 X& l+ D
- if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, (uint32_t)(dest+i), *(uint32_t*)(src+i))
c# `4 j- g% A+ n8 P - == HAL_OK)- M: M6 L2 r7 l! l
- {$ G9 T5 p7 j! F* W
- /* Check the written value */
4 S; ~3 Z* x$ u& r# U* N9 h - if(*(uint32_t *)(src + i) != *(uint32_t*)(dest+i))& K+ s/ l" ?' {
- {) B k6 U, w/ n4 a+ O
- /* Flash content doesn't match SRAM content */
# S6 {1 G# W4 `3 A - return 2;
9 K. ]3 b* \1 t+ E: m; K3 k: K5 U- i - }) e2 [- ]3 `$ h m8 A1 u P
- }# a6 @6 O% B) Z k0 q& _
- else/ h+ ]2 U [4 F: C2 [/ \. J
- {3 O- n. I' N% U3 L
- /* Error occurred while writing data in Flash memory */5 t0 o3 r; _, @. M
- return 1;# w+ O2 G% E9 H" B! j
- }- p# m6 @1 n& ]# q" K
- }
2 o. H6 |) {6 `" H - return (USBD_OK);
! B% a( i1 I3 c+ h - /* USER CODE END 3 */* ?6 Y% g- N& K+ I! K( J
- }
复制代码
! [7 d! B5 ^' D( T& z; k 读 FLASH
) e( M0 v/ n9 H1 A2 `' _- uint8_t *MEM_If_Read_FS (uint8_t *src, uint8_t *dest, uint32_t Len)
: p) P8 F6 T1 N2 d) Y - {) m/ R! a5 p" M/ _1 V' g
- /* Return a valid address to avoid HardFault */
, w0 u3 w$ t+ n& ?. T0 V6 } - /* USER CODE BEGIN 4 */
* ^8 D5 C2 K* e& E - uint32_t i = 0;
3 m. l9 s; h8 H3 w - uint8_t *psrc = src;6 ^; v, N5 C* ^ C+ s8 W9 C
- for(i = 0; i < Len; i++)
8 ~1 g+ z1 q4 N/ e0 e* u - {
' m- N) ?8 V l. D& R. f7 \5 b - dest<i> = </i>*psrc++;
4 l8 x: `1 P3 Y: i, }3 [; M: m1 p/ n - }
4 F; [' K! u5 ]( s- T - /* Return a valid address to avoid HardFault */& i N' v0 b w: Y( V
- return (uint8_t*)(dest);
1 _4 { g* X3 j- a - /* USER CODE END 4 *// \, E: K% G( E# h: i: z
- }
复制代码 % v; q$ u, F3 n9 i
获取状态
6 ^& s) e1 V# t: N- d- uint16_t MEM_If_GetStatus_FS (uint32_t Add, uint8_t Cmd, uint8_t *buffer)8 v5 G5 N! s% ~2 `7 Z3 {- {! t
- {+ R7 s6 [$ i/ [3 S
- /* USER CODE BEGIN 5 */
. t8 \6 @" {. v, M - switch (Cmd)
6 i8 w" s6 i' F - {4 M3 `) ~* y( c! K. j
- case DFU_MEDIA_PROGRAM:
5 x5 c* i. b% R3 {+ F - buffer[1] = (uint8_t)FLASH_PROGRAM_TIME;8 i8 b2 w' o* E) W. ~
- buffer[2] = (uint8_t)(FLASH_PROGRAM_TIME << 8);2 ^+ x! R8 v9 z. x# g! j, g% A
- buffer[3] = 0;/ T( h7 u( }- ]
- break;6 V/ n' e8 G. n: L9 e
- case DFU_MEDIA_ERASE:
7 h W2 U/ T0 K9 f) a6 C - default:
# u& b \3 F9 C' R - buffer[1] = (uint8_t)FLASH_ERASE_TIME;
# ]' T2 _; r4 D8 z6 f i- r# n - buffer[2] = (uint8_t)(FLASH_ERASE_TIME << 8);/ m1 x: k' J) o5 Z( b
- buffer[3] = 0;0 G1 V2 T4 j, R) c
- break;
+ p, G5 z& k5 k" A/ T% [ - }
) n' ~' R- w6 p/ o8 R4 _; z! L/ g, W( G; b - return (USBD_OK);
+ H* Z( R# H o+ O+ } - /* USER CODE END 5 */
9 ?: Y* ^6 Q& c2 M) z, k$ t - }
复制代码
4 z A* A( t5 a+ s! M. y上面函数中用到的一些宏如下定义:# H* @8 ]' \5 O7 L2 i0 d. E9 x$ y
- #define FLASH_ERASE_TIME (uint16_t)50( `8 j" p [! _1 G, l& _0 K
- #define FLASH_PROGRAM_TIME (uint16_t)50
复制代码 ( o! K+ G# M' i8 ^4 m
上述函数接口的具体实现是根据具体 MCU 的特点来实现的,若换成其他类型的 STM32,则需要根据当前的 STM32 型号做相应修改。如此,usbd_dfu_if.c 源文件就修改完了。: I$ Y. a1 {; h: \- \
3.3.2 main 源文件修改
k& T+ k1 I3 _; Y& [在 main 函数内,我们需要根据外部用户按键的状态来判断是否跳转到 APP 还是停留在 BOOT 程序内并准备对 APP 进行升级。
, ]4 I" M5 R8 p9 i9 RMain 函数修改如下:9 m' _3 d" r% `% p0 p# X
- int main(void)( f$ {/ E- ^1 f/ B
- {
5 H. i2 E& J3 K; v+ N - /* USER CODE BEGIN 1 */
* T- H" F1 ?$ P1 x5 W+ }0 s) F - pFunction JumpToApplication;3 h( L. \; k1 J9 C2 {/ r0 w5 I! C3 R7 Y
- uint32_t JumpAddress;4 \0 [/ C' T A; V( b
- /* USER CODE END 1 */( W3 u& j; g6 r5 i9 q( _( Y4 a7 {0 F
- /* MCU Configuration----------------------------------------------------------*/( z/ W! r+ g3 Z. S9 Q m
- /* Reset of all peripherals, Initializes the Flash interface and the Systick. */% x5 @6 u8 b- m& X5 d
- HAL_Init();$ `! `/ m1 z% p) y8 k" K8 M
- /* USER CODE BEGIN Init */& l, {" h) }: i: i( C( w4 x
- /* USER CODE END Init */9 @& W0 R0 o! K" C
- /* Configure the system clock *// W# H6 W/ C: j W) U* e7 m
- SystemClock_Config();
3 q# a7 S% M' s - /* USER CODE BEGIN SysInit */
: R$ Y& d& T4 a3 w4 @ - /* USER CODE END SysInit */
5 j1 |6 g- x2 o - /* Initialize all configured peripherals */
- n( b% i* ]5 F- r - MX_GPIO_Init();
( l7 d) N8 U: l8 f - //MX_USB_DEVICE_Init();/ s, c0 c9 l, o5 D
- /* USER CODE BEGIN 2 */
3 e5 B; E8 r: ?) Z! z, Q( r - if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) !=GPIO_PIN_SET)//如果用户没有按下按键
: z# W' Y- |: k- o. A - {+ C" h6 S3 m' g. @/ | e
- if (((*(__IO uint32_t*)USBD_DFU_APP_DEFAULT_ADD) & 0x2FFE0000 ) ==% c- z/ K9 X; h' ]
- 0x20000000) //如果存在 APP,则跳转到 APP 执行
/ h- a0 t2 M* s6 F* o - {
1 y# t9 j/ D' H- ^9 w s- F; s- Q7 k - /* Jump to user application */. f' l: |% R F4 H+ w4 i4 k1 X
- JumpAddress = *(__IO uint32_t*) (USBD_DFU_APP_DEFAULT_ADD + 4);
+ B, z* u2 ^. \7 { - JumpToApplication = (pFunction) JumpAddress; x& o! u8 \6 \2 ?3 \3 ?+ l, z! b* {" L
- /* Initialize user application's Stack Pointer */
; [3 @/ ^( c7 O3 V. H+ u7 a - __set_MSP(*(__IO uint32_t*) USBD_DFU_APP_DEFAULT_ADD);# x" {2 Y! V8 J1 v- Y$ w
- JumpToApplication();
3 h$ ^. _" c+ f) y- m. U2 g4 O3 Y - }
( Y9 p, U2 J3 U; a1 ~) h - }0 ]3 h! V6 c( Q
- MX_USB_DEVICE_Init(); //如果不存在 APP 或者用户按下了按键,则进入 DFU,并准备升级 APP3 f: p ?2 C% b) d8 t
- /* USER CODE END 2 */' I/ c2 \& v3 a
- /* Infinite loop */
5 E7 U5 U. E) G8 @& y X! ? - /* USER CODE BEGIN WHILE */% d) q! ~* o% X/ G/ o
- while (1): b2 X9 Y$ v; J% ~3 F8 Z
- {
4 e+ Y8 f8 H' { Q0 h - /* USER CODE END WHILE */. \1 Q0 b4 x8 p
- /* USER CODE BEGIN 3 */4 t: z# l" B/ Z3 r ~3 Z% y
- }
7 \7 Z$ I# t) j- ]9 X - }
复制代码
5 j3 A" f% t9 W0 L6 G$ V; Y, N上述代码中:
3 F3 ]9 i) Q# y; u% @% S- if (((*(__IO uint32_t*)USBD_DFU_APP_DEFAULT_ADD) & 0x2FFE0000 ) == 0x20000000)
复制代码 2 A. Z1 s4 a& a
这行代码是判断 FLASH 中是否存在 APP 程序。它的判断原理是这样的,若存在 APP,则( ] k2 p5 H h+ x" x! A: r
USBD_DFU_APP_DEFAULT_ADD 这个地址保存的是栈顶指针 MSP,栈指针肯定是指向内存的,而内存有效地址分为0x2000 0000 起始的 128K 范围内。% b2 G' H) Y# {0 u( b0 ]: ?
& 0x2FFE0000 的意思是将 MSP 的值过滤掉低 17 位,17 位二进制刚好表示 128K 空间范围,也就是说,只有 MSP 的值指向SRAM 的 128K 有效范围内时,才是合法的,此时则表示 APP 存在,否则,APP 不存在。
) U4 f( A& j) \& K# V, x# G/ D- JumpAddress = *(__IO uint32_t*) (USBD_DFU_APP_DEFAULT_ADD + 4);
复制代码 ; V" v9 b' Q: Q$ M7 A
跳转地址为 APP 起始地址+4,因为前 4 个字节表示的是 MSP 的值。所以接下来有如下初始化 MSP 的操作:' X1 Z6 Z, G6 V( v/ Z5 F& m, {
- __set_MSP(*(__IO uint32_t*) USBD_DFU_APP_DEFAULT_ADD);
复制代码 ; p- c( s& K7 F. ~. c# Y1 i: W
到此,BOOT 程序已经完成了。; C n" `; J7 K5 t) o
# B& _2 ?! R6 o( r# v' o, @, a
; w$ m$ Y- v: L4 制作 APP 工程
9 k3 _5 P9 F& g9 k! J制作 APP 的过程我们也讲一下,因为有些地方还是需要注意下。2 f& ]- G) `+ Z( G; g% T8 R5 U& Q
我们再次回顾下 BOOT 与 APP 在内部 FLASH 中的位置:" ^$ Z, I& G% }/ I/ k
& d9 j0 d2 d g% Z9 D8 I; ?" v
* S: W7 }% u: r$ T9 \' A% }( J! v' j3 m# A- ?7 j, k; h$ Q
如上,用户 APP 是放在起始位置为 0x0800 7000 的位置,当然这个位置是示例,实际工程中用户得根据自己的情况来设置,比如 BOOT 实际大小多少,那么 APP 肯定得放在 BOOT 之后,绝对不能有重叠的地方,第二个,APP 的起始位置一定要放在 Page 起始的地方,因为 STM32F072 内部 FLASH 擦除是以 Page 为单位的,换做其他 MCU,比如 STM32F407,则擦除是以 SECTOR 为单位,且每个 Sector 的 SIZE 不尽相同,那么这个 APP 的起始位置一定要注意放在 Sector/Page 起始的位置。
5 |6 |/ R6 |- ~/ M( e: ~; X下面我们一步一步详细介绍 APP 的创建过程,以一个简单的 LED 灯 Toggle 的例程向读者介绍用户 APP 的方方面面。
2 q5 j2 L. i" j( J- {& d' Q: O% l9 T# v! A
: D! O% s+ i. I: s4 S, w
4.1 使用 STM32CubeMx 制作 APP 工程
, Y- V( E+ h* I i: u
/ Q/ ^( R4 c" W; X3 S, h
9 w2 u s# {( l2 \8 h) }
7 W! I5 V' e, w' Y如上图,在 Pinout 中设置 LED1~4 的四个管脚为 GPIO_Output 输出模式。
/ m" K0 S5 b" H5 O0 R
9 G6 `6 o4 q4 Z- H0 p; b
( v* G0 R6 x2 g/ I k
: e$ J! Z$ t+ j$ E时钟树设置还是配置内部 48M 专用晶振为时钟源。7 N Y0 e( D3 I# _4 |- X
4 x6 Y' m* v" s( Y: \: J- \$ t0 M
, ]3 F9 P4 ^0 G T! l& y, ]' g$ H% u9 ~4 f
然后直接点击生成代码。, [# P% P' o% o
6 x5 o/ j/ m% q; Z) X
4.2 完善代码与工程配置8 p- y7 B, ?& p w, O6 S6 I
下面我们完善下代码:
d: T* l% ]$ k( ]在 main 函数中,我们让 LED 等闪烁起来:2 T1 M) y. t. F) O
- while (1)
# C1 m" i8 K6 p( O5 P! K' d - {& }7 n/ [# y0 b" P) U- D4 X( x
- /* USER CODE END WHILE */% y% p1 @6 b$ v( ^7 Q
- /* USER CODE BEGIN 3 */
2 K4 B* ]. H. G* X* U* o, A - HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_6);
' x! b2 ?, X5 R0 A; w2 Q$ s- K7 O - HAL_Delay(200);: p8 R# P! X) E) b' q* s5 c
- HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_7);
1 j8 N# D# `6 K, N$ _- x: K - HAL_Delay(200);$ z w* I: c/ `+ @5 r' u0 X
- HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_8);
3 s$ A1 f4 B) U0 r! y) w - HAL_Delay(200);
. t6 K; V" P3 a* D+ E/ z: ? - HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_9);+ s4 p& O C) J5 U" E) L
- HAL_Delay(200);( z: n; @- B/ U# E# {
- }
复制代码 # k' \" n3 @2 O, X, m& ^
然后在 IAR 的链接选项内配置中断向量表的位置以及 APP 在内部 FLASH 的起始位置:
2 k6 ?9 r2 [. A- U$ Z: K+ V
5 J. e# P. { ^ h
3 `7 M" n" i* k5 o1 o, a; H' f7 c
1 A Z3 r4 n& V2 o% |3 w3 e: a4 D然后我们得将中断向量表重映射到 SRAM 中:5 b+ f6 m+ R& B8 {
- /* USER CODE BEGIN 0 */ m, U' t1 L c/ \
- #define APPLICATION_ADDRESS (uint32_t)0x08007000
6 q* y5 @ S% {# `4 [" b - #if (defined ( __CC_ARM ))
7 q3 X! y" t" F) g$ C7 p0 \( S - __IO uint32_t VectorTable[48] __attribute__((at(0x20000000)));
' W+ Z& e9 m( r - #elif (defined (__ICCARM__))
6 \* S2 }. ^! p - #pragma location = 0x20000000
8 e5 F" u+ ?* e* P" Z Y, W - __no_init __IO uint32_t VectorTable[48]; //定义 VectorTable 这么一个数组,位置放在) ^) C) D8 p. Y3 |7 i4 N
- 0x2000000 这个 SRAM 位置,用来存放重映射后的中断向量表
D- X% q$ M9 }8 X9 V; s+ \1 n - #elif defined ( __GNUC__ )
. P. R! n1 D7 o+ \* E6 @ - __IO uint32_t VectorTable[48] __attribute__((section(".RAMVectorTable")));9 f1 L% v9 h7 }4 [; K' A N' N" j
- #endif
- Y( w1 j' F' H( |& K8 | - /* USER CODE END 0 */
复制代码
. u8 b+ H7 I& a W$ N! L在 main 函数中进行重映射:
0 Z& l; F! y5 o+ }, C- int main(void)
6 l C0 v, U" c% Z - {( q; b# V4 o; W3 }# k
- /* USER CODE BEGIN 1 */3 N; X! j! Y! s7 {9 l) d! S) D
- uint32_t i=0;
" x# D2 z$ H2 w, b9 b0 d - /* USER CODE END 1 */( ?/ t8 j5 M$ u' `' H7 r q
- /* MCU Configuration----------------------------------------------------------*/' K1 q$ f3 R+ @5 X2 `
- /* Reset of all peripherals, Initializes the Flash interface and the Systick. */* ?: n/ F9 G' {% K+ S1 b9 S5 D
- HAL_Init();: @; [- y* e6 |5 Q# n
- /* USER CODE BEGIN Init */
# ^! G% [: Z4 {1 C) n7 X: V - for(i = 0; i < 48; i++)//拷贝中断向量到 VectorTable 数组
6 Z6 Q* S+ H9 t6 b: S) U; Q7 h - {+ E- l# [5 j# o" |4 `
- <span style="font-style: italic;"><span style="font-style: normal;"> VectorTable = *(__IO uint32_t*)(APPLICATION_ADDRESS + (i<<2));
" |, F$ C; I* M+ K - }2 Z/ F8 r4 J( {' Y3 j
- /* Enable the SYSCFG peripheral clock*/
N* {1 A- a& b - __HAL_RCC_SYSCFG_CLK_ENABLE();
8 k+ n* m& F1 B6 o- ] - /* Remap SRAM at 0x00000000 */ c% e0 `. X' o1 D C4 t1 ?
- __HAL_SYSCFG_REMAPMEMORY_SRAM();//重映射到 SRAM
3 {2 P5 b5 T6 |& D8 @* v - /* USER CODE END Init */
3 A9 s% Q3 H+ i8 h - /* Configure the system clock */
. @4 t% C' N5 c% _) W3 Q - SystemClock_Config();& c/ k1 ^4 x/ d% w1 q& m* D
- //…</span></span>
复制代码 / [. N, C3 p0 d& @3 l) d
讲到这里,相信很多读者可能存在疑问,为什么这里一定要这么重映射到 SRAM?这个是应为 M0 内核的 MCU 没有 VTOR这个寄存器,当存在 BOOT 时,APP 中原先本来应该位于 0x0800 0000 这个位置的中断向量表被 BOOT 占据了,那么 M0 内核在运行后怎么找到偏移到 0x0800 7000 位置的中断向量表的呢?
9 T" z2 \) M6 E& f在 M3/M4 内核的 MCU 中,是存在 VTOR 这个寄存器,M3/M4 内核启动后会根据 VTOR 寄存器的值来寻址中断入口,但 M0由于缺少这个寄存器,那么我们就将中断向量表整个搬到 SRAM 中,再重映射到 SRAM,这样一来,M0 内核在启动后将不再从内部 FLASH 寻址中断入口,转而从 SRAM 中寻址,这样 APP 的中断就能一样正常响应了。换句话说,如果 APP 不进行重映射,那么一旦中断产生,那么内核还会从内部 FLASH 0x0800 0000 的地址中去寻址中断入口,这不成了 APP 的中断在 BOOT 中执行么?很明显,会出现错误直接导致崩溃。这就是为什么要重映射的原因。
2 W$ Y3 n& e+ ]! S: M# R4 R! _" A9 M; q1 B O
/ f- M$ U$ @' r. b; x
7 Y! m5 Y/ D# m- X' w& M" C
5 制作 DFU 文件: W4 `' h: t& k7 J8 K w; B0 A
后续我们将演示如何使用 BOOT 来对 APP 进行升级,我们将使用到一个 PC 端软件 DfuDemo 来实现这个过程,这个DfuDemo 软件只能识别固定了后缀名为 dfu 格式的文件,因此,首先我们需要将 APP 程序转化成 dfu 文件。5 P1 ]; j- n$ n8 P) X
5.1 生成 HEX 文件/ b( i7 m- s3 a! D4 b
我们先将 APP 程序生成 hex 格式文件:/ W D6 d2 S4 {. ?1 H
$ k* f5 l3 L- w1 ^$ b) g$ v+ w5 T
# ]3 L3 J9 F8 |7 Y: `
! V6 i; E: c1 t
如上图所示,在 IAR 中先设置生成 HEX 格式文件,最后我们将得到后缀名为 hex 的文件。
' f3 l1 m$ J m- e" X/ j% ^ W4 E L' J$ \0 o2 u7 M* f
5.2 将 HEX 文件转成 DFU 文件) y1 H3 P, P. ]7 d+ R* \
然后使用 DFU File Manager 软件将 hex 文件转成 dfu 文件:
7 B% v, y0 ^8 f" r8 m8 V, Z! ]- ?& m( C
: k7 z+ N3 U4 ?5 a P2 Y
% A8 N' z/ X5 N) e6 w9 S ?
/ V! U& Y3 O1 _ I! x! `4 }/ r! Y
最终转为 dfu 文件:/ }! g6 H! Z7 f0 K' }+ D
; U9 y* F; o8 K' h+ P6 c
" D4 k, w7 m- w, F5 |8 H
6 x g6 L: D. {, P2 X8 M
3 N4 {: _) q) {$ b# N6 使用 BOOT 烧录 APP( j" Y" o1 x, b8 {7 i' {: Q
首先,我们预先将 BOOT 工程烧录进 STM32F072-Discovery 板。在连接 USB 线到 PC,打开 DfuDemo 这个软件,在导入APP.dfu 这个转化好的 dfu 文件并开始烧录。$ ?( X5 j2 K' @+ j# o; V! Z
% Y! `( l' r" v v4 j/ K% x- H/ o! A; R! t T
/ ^8 ^/ \$ t( o; z- P4 ~
% ^' U. L- x6 I) F1 B9 d7 l9 K! Z/ I5 L) }+ h1 }
+ n7 E$ V1 x6 p5 j
+ v) N; K: b8 {! K/ R. P5 V& l! V
- B3 @+ W1 k: u& a7 T1 P& I2 u
2 R* l( Y9 W, }4 i$ n8 I" A, I如上图所示,原先在 CubeMx 中对 USBD_DFU_MEDIA_Interface 的字符串设备将在 DfuSe Demo 这个 PC 端软件的 Flash结构中显示出来,字符串与显示结构的对应关系如上图标志。0 u S5 j7 G! W [" E
烧录完后进行验证 :* G4 i$ g- k5 b& T w. q
$ A7 e+ ]$ K3 t$ ?, v
* s7 y0 f0 f' O T: O
; b) \: ?/ P1 ]: X, f, t
5 Q! b+ ]: @0 w4 }, h如上图,烧录 APP 完成后验证是通过的,LED 能正常闪烁。0 v6 A/ B$ `( P0 q* Y5 t3 I" g
4 S) h7 Z$ Q2 T4 B A( e; p
$ k7 ]6 F, t: N/ C1 f! ~' z v) ]) j9 ~& j
7 注意事项7 j- @1 A) u W) m; C: I
1. BOOT 本身实际所占 FLASH 空间不能与 APP 空间有重叠,即 BOOT 不能超过 USBD_DFU_APP_DEFAULT_ADD 所表示的地址;
7 ]; D/ }. s) p2 `# J6 O+ s2. BOOT 本身实际所占 FLASH 空间必须在 string 所表示的只读地址范围内;而 APP 也必须在 string 所表示的可读写地址范围内;3 J0 Z# D# W* v. Z
3. USBD_DFU_APP_DEFAULT_ADD 必须向 Page/Sector 的位置对齐,即向可擦除单元块对齐。5 w8 s7 v4 C2 \4 E7 f+ g
4. 对于 Coretex-M0 核的 MCU,由于没有 VTOR 这个寄存器,APP 若不在默认地址 0x0800 0000,则必须将中断向量表重映射到 SRAM,APP 才能正常工作,对于 M3/M4/M7 核的 MCU,在 APP 内则修改 VTOR 这个寄存器即可。
L Y$ e* E$ k. z5. 有些 MCU 内置 BootLoader 也是支持 DFU 模式,也是可以通过 DfuSeDemo 这个软件配合升级用户程序,它的通信协议与本文所述并没有太大差异,只不过本文所述内容为 IAP 方式,而那个是属于 BootLoader 方式,这个读者注意区分下,读者可以通过参考应用笔记 AN2606 了解详情。
' M/ n, k9 T9 I% D3 K6 g! g5 n( O
4 [) @$ z8 t2 h6 P8 P% `: h* ^0 J f+ A K! b' e6 E
% n/ \& P7 n( E' Q& t7 y1 B: H) u- h& q( ^7 f. C
+ X- H# Z" f+ c6 c$ r, ^
|