前言
* Z5 J } R! `5 b, r6 | _6 E3 h大家都知道,任何 STM32 都包含有一块系统存储器(System Memory),里边存储着内部的启动代码 Bootloader。不同的STM32 型号所支持的用于升级代码的通讯口不尽相同,需要参考应用笔记 AN2606。但是,有一个问题避免不了,那就是如何进入 System Memory 去执行 Bootloader?通常的办法都是将 BOOT1 和 BOOT0 进行配置:BOOT0 拉高,BOOT1 拉低(有些型号的 BOOT1 由选项字节 nBOOT1 进行控制)。可是在一些产品中,由于外观的要求,往往不方便在外边开口去放置按键或跳线来改变 BOOT 脚的电平。而且,用户并不想自己写 IAP 代码,觉得麻烦。特别是一些产品,需要使用 USBDFU 来进行代码升级的,而在产品功能中 USB 又没用到,用户就会觉得自己为了一个通过 USB 进行代码升级的功能,去写IAP 的话,需要去熟悉 USB 的代码,觉得麻烦,而且这些 USB 的代码还占用了用户的程序空间。对于这些用户来讲,他们很希望能在不管 BOOT 脚的情况下能够去调用 STM32 中 System Memory 的 Bootloader,完成代码升级功能。
3 O# m+ h! S( h问题% v: g# o0 }/ O: b3 |6 c+ w
某客户在其产品的设计中,使用了 STM32F411。由于产品外观的要求,无法在外部对 BOOT 脚进行控制,而且外观上只有USB 接口是留在外边的,需要使用 USB DFU 进行升级。而且 USB 接口只用于代码升级,没有其他功能,所以客户不想去碰USB 代码,希望能够直接使用 System Memory 中的 Bootloader 进行代码升级。2 H! A* `6 T( Z0 ?
- B# u3 B c. E/ N
调研
* o, I4 T' m, o* @5 B1.判断其可行性
1 T. P+ u+ ?1 ~/ \# |首先,打开应用笔记 AN2606《STM32 microcontroller system memory boot mode》,翻到 3.1 Bootloader activation 一节的最后,可以看到如下信息:
& x, ~% G4 J) N4 e
4 M; T% n& p Q4 w4 r% W
4 D. X% _2 [8 y! f7 r2 D
9 E1 P' B+ j! g9 ]- p
这里的意思就是说,用户可以通过从用户代码跳转到系统存储器去执行 Bootloader。但是,在跳转到 Bootloader 之前,有几个事情必须要做好: o* u' p# i7 {2 m. J% k2 _/ i9 ?
1) 关闭所有外设的时钟" ]; J% C( R, a6 k0 X, K
2) 关闭使用的 PLL9 a& {( [5 Z6 q& n& f
3) 禁用所有中断3 e; B9 E% S1 K) ?
4) 清除所有挂起的中断标志位
. a8 h; y; Q2 w/ I" u% O% Q 最后,可以通过离开 Bootloader 激活条件且产生一个硬件复位或者直接使用 Go 命令去执行用户代码。
9 S. l( |5 U2 e# m' [
1 j! E6 e! Z% @' L A* G; t 那么,如何从用户代码跳转到 System Memory 中去呢?这个其实并不难,如果写过 IAP,或者看过关于 IAP 的应用笔记中的参考代码的话,比如应用笔记 AN3965“STM32F40x/STM32F41x in-application programming using the USART”及其参考代码 STSW-STM32067,都应该知道,IAP 的启动代码通过重新设置主堆栈指针并跳转到用户代码来执行用户代码的。同样的道理,只要知道 System Memory 的地址,一样可以从用户代码通过重新设置主堆栈指针并跳转到 System Memory 来执行Bootloader。而 System Memory 地址可以从参考手册来获得。比如,查看 STM32F411 的参考手册 RM0383,可以找到如下的表格:. b- p& j6 ?( d/ A" |
5 T2 D1 @: W" g, c' W
7 T) J# B/ e! d. T; u3 X$ k" ?- a; S L* x" D! F8 u; }$ H
可以知道 STM32F411 的 System memory 地址从 0x1FFF0000 开始。
* W/ x! E6 a$ A, R+ X" w2 v _
" C6 ~% V7 g5 C! q 那很多人又会问了,我的代码很复杂,用了很多外设,开了很多中断,可是要跳转到 System Memory 中的 Bootloader,需要关所有外设的时钟,需要关 PLL,需要关闭所有中断,需要禁用所有的中断,清除所有挂起的中断。这可是一项非常庞大的的任务啊!所以,在这里,我们需要一个更简单的事情来完成这项庞大的任务。其实真的就有这么简单的一个方法——复位!通过软件复位来实现这一目的。但是,复位后,又怎么知道还记得我们要去做代码升级呢?这又要用到 STM32 另一个特性了,那就是后备数据寄存器 Backup Data Registers 在软件复位后会保留其值,这样给了我们在复位前后做一个标志的机会。
' w# g; A# x5 I& x$ Q
7 d4 N* l3 v7 w* i0 C7 ]这样,考证下来,客户的需求是具备可行性的。接下来需要做的是理清思路。# C0 |2 q. d; ~
) S5 F. l$ V3 l 2.软件流程 h5 ~' r4 E/ ?% R4 T; D
这里使用 32F411EDISCOVERY 板来设计一个参考例程:设计一个用户程序,让 LED3 进行闪烁;当用户按键被按下,产生EXTI 中断,在中断中选择后备数据寄存器 RTC_BKP0R,写入值 0x32F2,然后产生软件复位;软件复位后,在运行代码的最前面对 RTC_BKP0R 进行判断,如果其值不是 0x32F2 则直接去运行用户代码,如果其值为 0x32F2 则是需要跳转到Bootloader 去进行代码升级,并在跳转前将 RTC_BKP0R 清零。这样,在进入 Bootloader 后,客户进行 USB DFU 升级后,将来不会因为非需要升级代码的复位而误入 Bootloader。5 G4 l8 D' }" n7 Q S7 Z
" Y6 v9 `3 W; a4 L; c 来看软件流程图,先来看主程序的流程图:5 F; E/ m9 h2 J; {+ E
# V. e `8 s1 E$ U# Q8 t$ `- b
. q/ q2 o5 h# z$ E& J8 C% m& Y! w7 v' K
再来看 EXTI 中断的流程图:0 M- o6 I) ]7 V: G, Y3 l( G0 B
: e- x4 ~1 i3 u/ P; x
& X/ N4 ~% s; _6 \7 Y; l2 K$ a5 g: i
8 \! K( a+ L3 Z( |% l 3.主要代码, a7 g. p2 i7 P' z. a
使用 STM32F4Cube 库来开发这个例程。先来看位于 main.c 中的 main 函数:
3 f1 w C9 Y9 {; v) ?& e- int main(void)
+ {. g2 p8 i+ r0 n) q) }' a - {
! B) V( p7 `3 r+ w* c- t3 j - /* STM32F4xx HAL library initialization:
: u. }/ a. @# S' h! a" e2 J8 S - $ P* A, c- B( H' i' L( K8 d3 c
- - Configure the Flash prefetch, Flash preread and Buffer caches s' i% l: b% W3 i! j2 l) a' T
- - Systick timer is configured by default as source of time base, but user
7 m3 L1 ]" {" { - can eventually implement his proper time base source (a general purpose
6 T. ?, f- G8 h& |" x - timer for example or other time source), keeping in mind that Time base
2 P5 l+ v; o' q7 s5 Z - duration should be kept 1ms since PPP_TIMEOUT_VALUEs are defined and
" |( C0 f9 D9 S# m) s% \6 J" ] - handled in milliseconds basis.
) N: P* [7 F. y8 m, }; ^, M9 P - - Low Level Initialization* K+ R0 z# Z, Y$ \( J
- */7 v% v& m: E. ^* T8 N+ u( i
- HAL_Init();
9 o2 V7 M: z- ?" h - /* Configure the System clock to have a frequency of 100 MHz */
3 K3 U- I3 D8 v - SystemClock_Config();
% H! X' g* W" v) f: e. G5 H - /* Configure LED3, LED4, LED5 and LED6 */, I! z* B4 [- k: L0 {: ~$ X+ d% B
- BSP_LED_Init(LED3);
- H5 `+ ]+ v; b2 W4 ] - BSP_LED_Init(LED4);
/ h# m( p. R$ o4 Z, R9 q6 _1 [# D - BSP_LED_Init(LED5);
: N, Q; [. ?6 i1 L$ g h8 b* H# V# X - BSP_LED_Init(LED6);
! p0 f. E" T0 Q! h& v& k - /* Configure EXTI Line0 (connected to PA0 pin) in interrupt mode */
5 S) i8 D5 x: B - EXTILine0_Config();
f- c0 `/ ]2 R2 O& U7 c2 @% l - /* Infinite loop */
% C$ s8 O( A) c, p - while (1)- p7 J* }* T% ]( F# o# @
- {" m! c$ K) S0 `4 x. a
- }
1 X& k1 b9 v- i - }
复制代码
3 t) ]. n& b0 \; i Main 函数很简单,配置系统时钟,对使用的 LED 进行初始化,然后配置了用户按键的 EXTI 中断,然后就进入主循环了。前面说到,要实现用户的功能程序为 LED3 闪烁,在主循环我们没看到,是因为在 Cube 库中,会使用 SysTick,所以把 LED3的闪烁放到 SysTick 的中断代码中了,查看 stm32f4xx_it.c,如下:
* Y: a ]8 ~! X7 H& b$ ^- void SysTick_Handler(void)
1 r! k8 F# b/ z, N- g - {
( q, x t9 `5 D5 m. y - HAL_IncTick();
* J/ l8 n) u$ k# \! S* H/ Y* c - 6 ]( L S Q- _) ?5 M5 K
- // LED3 Toggle
6 Y3 l8 [5 e% s* {2 _9 p0 l - led_toggle_counter++;
& f% r) ?# H5 h" T9 y0 I& \ - if (led_toggle_counter >= 500)& z% i) {; W1 S& I6 [; ~/ ?7 l0 Z
- {
* H7 o( A) Z# q1 }5 o- Y - led_toggle_counter = 0;
- y/ y9 {, ?+ Y( @' @8 @ - BSP_LED_Toggle(LED3);
' z! e( ^8 P2 J7 W - }
- h, z& V+ Y- E3 Q0 {5 t - }
复制代码 ) b1 t5 I7 M, V# A
从 main 函数最开始的那段注释中知道,跳入 main 函数前,在 startup_stm32f411xe.s 中早已经先调用执行了位于system_stm32f4xx.c 中的 SystemInit 函数。SystemInit 函数主要执行初始化 FPU、复位 RCC 时钟寄存器、配置向量表等功能。由于我们希望在最原始的状态下进入 System Memory,所以我们将跳转到 System Memory 放在这个函数的最前头,如下:5 r! S: S* }# J( t, x3 k5 B- c/ w
# T- x) J1 @+ s( V- k/ \' M4 I
- void SystemInit(void)7 i2 j) ~8 X. K
- {- {7 h$ ^8 l) T* w3 S
- /* Check if need to go into bootloader before configure clock*/2 W+ j P# _, |2 B& u
- RtcHandle.Instance = RTC;
* E5 o6 c, R O$ z6 K0 `! S0 n) n8 i - if(HAL_RTCEx_BKUPRead(&RtcHandle, RTC_BKP_DR0) == 0x32F2)
! y: W/ f% L$ [4 S, z - {- [0 B7 V4 R4 x* J6 @/ R! s8 g
- __HAL_RCC_PWR_CLK_ENABLE();, p+ Q+ b7 M- k' q
- HAL_PWR_EnableBkUpAccess();: }" } i. A; N5 t5 l" x' g0 [7 F& H, ^
- __HAL_RCC_RTC_CONFIG(RCC_RTCCLKSOURCE_HSE_DIV2);
8 z! \2 I5 s* P: o9 w. O- p* \7 Y - __HAL_RCC_RTC_ENABLE();1 h, W# h) ? d$ G6 g1 R/ N
- HAL_RTCEx_BKUPWrite(&RtcHandle, RTC_BKP_DR0, 0x0);5 q# E: f9 \. Z B1 S, ?
- __HAL_RCC_RTC_DISABLE();
5 Q1 q: Y5 @% \+ i" L; R1 g& i s3 W - HAL_PWR_DisableBkUpAccess();
0 e7 t: V9 R9 [4 d7 P8 V7 N - __HAL_RCC_PWR_CLK_DISABLE();" Z3 h; R9 t5 t2 O! c) H, ~
2 c O$ { f4 _( \$ q" e% F1 J- __set_MSP(*(__IO uint32_t*) 0x1FFF0000);
0 Y% ?: F" n3 b; i) }! O0 e8 X - SysMemBootJump = (void (*)(void)) (*((uint32_t *) 0x1FFF0004));+ A1 `/ Y- a& X2 D
- SysMemBootJump();
# W+ J }6 I$ i* f" Y. j - while (1);
. q/ `6 n/ ?' @; Q/ z - }
0 |, Y+ M: _7 o# t; I
2 N8 z, h: `+ w; A W- /* FPU settings ------------------------------------------------------------*/' }% {4 v% d% ^
- #if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
* F$ W1 h% f! l - SCB->CPACR |= ((3UL CFGR = 0x00000000;) S' R1 L9 @. g# b' m9 q5 c
- /* Reset HSEON, CSSON and PLLON bits */% t& k2 y0 u$ C% R- p6 m
- RCC->CR &= (uint32_t)0xFEF6FFFF;/ h' g% p( |' l
- /* Reset PLLCFGR register */
& k' e( l6 H% V3 T$ R - RCC->PLLCFGR = 0x24003010;& _: b, l8 S8 C
- /* Reset HSEBYP bit */
7 Z2 I0 I6 g- P$ p) C1 N! ] - RCC->CR &= (uint32_t)0xFFFBFFFF;: g8 a/ L# f# a& p5 C7 {4 h
- /* Disable all interrupts */
}3 z0 h- _% q X/ @ - RCC->CIR = 0x00000000;
, |0 m9 d, s1 t9 R4 t9 p7 C+ f7 e - /* Configure the Vector Table location add offset address ------------------*/ K' m$ L* p: c; ~" T
- #ifdef VECT_TAB_SRAM8 ]' B/ m; `) E3 o9 n; z) t1 C
- SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
; J7 I! @: a8 e) u- b" o) K" t" P - #else) T2 u( M# S$ Z6 \, Q# \/ Y8 l
- SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH
4 N+ S( Z3 ~4 z! P - */( ^! v& f6 Q' n! }9 J
- #endif( C& U5 P! r1 h/ X" G
- }
复制代码
; H, y0 F5 O/ e! l+ e可以看到,在函数的最前面对 RTC_BKP_DR0 进行了判断,如果其值为 0x32F2 的话,则先启动备份域的访问时序,如RM0383 中 5.1.2 Battery backup domain 所描述的:7 r/ x, p$ @+ x& I! `7 {: _! N
1 t% L+ d' K' h0 }" n" b8 ?9 Z" u8 O! ?9 H; g6 P
$ h6 Z+ M! H! L
然后将 RTC_BKP_DR0 清零,再关闭执行这次操作所打开的时钟。8 l l9 s% K1 c6 X6 E+ c
主堆栈指针 MSP 的初始值位于向量表偏移量为 0x00 的位置,复位 Reset 的值则位于向量表偏移量为 0x04 的位置。对于STM32F411 来说,当执行 System Memeory 中的 Bootloader 时,MSP 的初始值位于 0x1FFF0000,而 Reset 则位于0x1FFF0004。所以在程序中,使用__set_MSP(*(__IO uint32_t*) 0x1FFF0000);来重新设置主堆栈指针,而后再跳转到0x1FFF0004 去执行 Bootloader。
, H4 S/ t& Q0 L; d' B; t4 K8 _
9 T. p$ H& X% F/ ?1 G8 z再来看位于 stm32f4xx_it.c 中的 EXTI 中断程序:
# A/ W \: Z- a K$ o4 Z- void EXTI0_IRQHandler(void)0 g, I" \( ~+ R/ v* B E
- {
4 _) r1 v5 s3 F/ d! _+ l0 b - HAL_GPIO_EXTI_IRQHandler(KEY_BUTTON_PIN);
: `% P* s2 l2 `& s0 w+ u3 k7 j - }
复制代码
5 u+ a& ~( S% Y$ `" ^7 b8 K8 p$ g及其位于 main.c 中的 Callback 函数:
0 H2 |5 Z# Z: W( h2 W/ m8 Z- void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
, f o) @0 M) ^4 }7 |$ X/ i - {% `$ W @9 @8 M. Z, A$ t2 _0 }) O
- if(GPIO_Pin == KEY_BUTTON_PIN)1 Y/ t( r3 g k' K2 R' i
- {( V: c3 o$ Z) I5 D: M$ p2 \8 _
- __HAL_RCC_PWR_CLK_ENABLE();
9 j% a8 H- @( l2 j# x3 y7 C1 y+ \ - HAL_PWR_EnableBkUpAccess();
/ R8 x8 `, L+ f& n+ _+ U' ^ - __HAL_RCC_RTC_CONFIG(RCC_RTCCLKSOURCE_HSE_DIV2);' X7 |5 P6 j4 }1 Q. B; ]5 \
- __HAL_RCC_RTC_ENABLE();
4 E/ Q* E! b* I1 s: K1 K - RtcHandle.Instance = RTC;
, G5 Z, P1 U: O+ X( g2 \+ P - HAL_RTCEx_BKUPWrite(&RtcHandle, RTC_BKP_DR0, 0x32F2);6 a, O7 }# s7 [' q2 L7 l% [2 o! l
- if(HAL_RTCEx_BKUPRead(&RtcHandle, RTC_BKP_DR0) != 0x32F2)
+ p: a s' x' V8 O7 y - {" d! O; V% U+ L Q/ {
- // Write backup data memory failed6 \1 ^7 C* K: ]( o) J
- BSP_LED_On(LED5);
$ \9 V8 b* k' g, B - while (1) ; // Error6 y* `, i, S9 j: d3 L4 L! `
- }
# X# m9 K( J! R: L: G - else5 K7 M* ?/ T8 p4 g) I
- {
3 Q& A/ B6 h/ K- E2 k - // Write backup data memory succeeded.* m# [& }! }# b0 r) B7 A
- BSP_LED_On(LED6);
9 {" @% }3 L3 {* @/ @& v7 u - HAL_NVIC_SystemReset(); // Software reset for going into bootloader
) R% K5 H- f9 q0 x P - while (1) ;
% v# i' k O; R- ^ - }6 }5 t- l) B3 m# W
- }
7 }8 s7 E% n+ ^* e& j - }
复制代码 - Z5 P5 B9 p) e. v
当判断到用户按键按下,需要进行用户代码升级时,先启动备份域的访问时序,将 RTC_BKP_DR0 的值写为 0x32F2。再读回来判断是否写入成功,以方便调试。如果写入成功后,则就调用 HAL_NVIC_SystemReset()进行软件复位。重新复位后,就可以进入 System Memory 了。
- \( h+ f" r. J& J9 Y4 N" _2 `. c+ E3 `, U) E1 D* Y, [; d
4.实验
C$ U+ a, K, N- o8 A使用 32F411EDISCOVERY 来做实验。
2 J3 E p: c& V1) 先将程序编译,下载到 32F411EDISCOVERY 板,可以看到 LED3 在进行闪烁
9 V- B& l3 q# A/ [" O1 W6 }( @1 O2 V! M" I ~
8 T3 J" U7 D- {6 |3 W9 n& c* t1 o3 c, [3 _$ ?5 ^
2) 按下 User 按键,LED3 熄灭,已经进入 System Memory 中的 Bootloader
" d2 B/ S, I# X- ~3) 打开 DfuSeDemo 软件,此时 Available DFU Devices 中没有任何显示
- \4 Y M& F7 A; V* O5 }. {6 f* z! M8 c1 q3 t
" V# ?+ T) ?+ G; T* e
$ |8 s! B( L) ?+ I4) 将一根 USB Micro 连接线插入 32F411EDISCOVERY 板的 CN5,可见 LED7 亮起,USB 已连接8 h, g" a5 p! O9 \/ Z6 t' c: n, E
2 \+ A, q# a6 r/ ^/ h4 @2 M
% p& `: k( I) L* d% z2 X* }
8 r2 m% o! e$ b# C
5) 驱动完成后,可以再查看一下 DfuSeDemo,Available DFU Devices 已经显示为“STM Device in DFU Mode”,代表已经成功驱动并正常工作了
& W4 [0 K; V* m! ?8 v3 F( i
+ I4 I" W# q- x7 J- I
/ q2 I- u/ U6 @4 E/ X* t. \
0 Q6 P: s6 h2 F4 Q
6) 之后就是正常的升级代码的流程了,点“Choose”按钮选择要更新的代码,这里准备好了一个32F411EDISCOVERY 板的 Demo 程序经过 Dfu file manager 软件生成的 32f411ediscovery.dfu 的文件,导入% t ~9 q7 A& B+ ?' E
8 P0 X" z1 ~, }! T
, F0 D7 r% E1 [; J4 Z5 A2 Q; @
+ N' q( V7 H0 S/ l/ T+ U+ u
7) 点“Upgrade”按钮进行升级,弹出的对话框选 Yes 就可以了,之后就升级成功了9 @* r8 ~. O" X/ J" D" _1 [
3 V3 d x( W9 I
0 ~& N3 K; T, {, j
+ ]# j# |7 ?* F3 b' J4 k# t6 \( o+ D
" c, J5 Q7 `5 j! L3 Z/ h! V8) 再点一下“Leave DFU mode”,进度条显示“Successfully left DFU mode!”,就可以进入更新后的用户代码了,可以看到 4 个 LED 灯正常欢快的滚动和闪烁着……
4 ]0 V% w# D" ?$ b7 {$ R* b4 b. ~( ~) d: x
5 b' m5 `9 v2 S
+ k5 t! @9 s" Q- D+ g3 s3 k5 T 注意9 k, y( V0 Z- r9 V q3 h
此例程仅为验证其可行性,在实际应用中,有不尽完善的地方请用户自行完善。另外,有几个需要注意的地方:
$ Q, _+ _# L6 i% n- [1) 此 Demo 代码基于 STM32Cube_FW_F4_V1.11.0 撰写,解压缩后,可将其放入\STM32Cube_FW_F4_V1.11.0\Projects\STM32F411E-Discovery\Templates 替换掉原来的源代码文件,即可编译运行。
- i3 R( d. Z1 v- e b% ^2) 此程序使用按键按下作为条件来触发软件代码升级,用户可以根据自己的情况修改触发条件,如多个按键同时按下,等等。0 @ D" ^$ g. u9 o
3) 当用户应用中使用了 RTC 的话,RTC 时钟源一旦被选择后是无法修改的,除非备份域被复位。在 RM0383 关于RCC_BDCR 的描述中有提及:
- p; k! O- r, L
' I3 ~7 \* l. Q: ?
( Z- I* @2 X3 x
. m; X" m1 B; @* D
4) 关于如何使用 Dfu file manager 生成.dfu 文件,请参考 UM0412“Getting started with DfuSe USB device firmwareupgrade”或者实战经验“利用 USB DFU 实现 IAP 功能”。) D8 h; J h, u3 C' y( S4 U4 [
5) 关于 Bootloader 中所使用的 USB DFU 协议,请参考 AN3156“USB DFU protocol used in the STM32 bootloader”。
; G& k4 g9 `, I; F2 D, e
; [1 m+ K8 X: w0 Q |