
一种从用户代码调用系统存储器中Bootloader 的方法 前言5 z) X3 {: i9 _3 N- ] 大家都知道,任何STM32 都包含有一块系统存储器(System Memory),里边存储着内部的启动代码Bootloader。不同的, J1 o' \8 ~9 y1 [6 R) ?( @ X6 ~ STM32 型号所支持的用于升级代码的通讯口不尽相同,需要参考应用笔记AN2606。但是,有一个问题避免不了,那就是如 何进入System Memory 去执行Bootloader?通常的办法都是将BOOT1 和BOOT0 进行配置:BOOT0 拉高,BOOT1 拉低 (有些型号的BOOT1 由选项字节nBOOT1 进行控制)。可是在一些产品中,由于外观的要求,往往不方便在外边开口去放 置按键或跳线来改变BOOT 脚的电平。而且,用户并不想自己写IAP 代码,觉得麻烦。特别是一些产品,需要使用USB DFU 来进行代码升级的,而在产品功能中USB 又没用到,用户就会觉得自己为了一个通过USB 进行代码升级的功能,去写 IAP 的话,需要去熟悉USB 的代码,觉得麻烦,而且这些USB 的代码还占用了用户的程序空间。对于这些用户来讲,他们很 希望能在不管BOOT 脚的情况下能够去调用STM32 中System Memory 的Bootloader,完成代码升级功能。+ T3 c0 D' t# E6 \" ^4 o% M 问题 某客户在其产品的设计中,使用了STM32F411。由于产品外观的要求,无法在外部对BOOT 脚进行控制,而且外观上只有 USB 接口是留在外边的,需要使用USB DFU 进行升级。而且USB 接口只用于代码升级,没有其他功能,所以客户不想去碰7 G8 _$ K% P6 }5 G3 c" p4 ^ USB 代码,希望能够直接使用System Memory 中的Bootloader 进行代码升级。 调研 1.判断其可行性 首先,打开应用笔记AN2606《STM32 microcontroller system memory boot mode》,翻到3.1 Bootloader activation 一节的 最后,可以看到如下信息: ![]() 这里的意思就是说,用户可以通过从用户代码跳转到系统存储器去执行Bootloader。但是,在跳转到Bootloader 之前,有几 个事情必须要做好: 1) 关闭所有外设的时钟 2) 关闭使用的PLL: t- B9 C" V+ V( x! F# R7 T 3) 禁用所有中断: y7 r4 h( v4 p0 d4 s2 {. { 4) 清除所有挂起的中断标志位 最后,可以通过离开Bootloader 激活条件且产生一个硬件复位或者直接使用Go 命令去执行用户代码。( }0 F4 d: X, L. A+ G ?7 W 那么,如何从用户代码跳转到System Memory 中去呢?这个其实并不难,如果写过IAP,或者看过关于IAP 的应用笔记中的 参考代码的话,比如应用笔记AN3965“STM32F40x/STM32F41x in-application programming using the USART”及其参考 代码STSW-STM32067,都应该知道,IAP 的启动代码通过重新设置主堆栈指针并跳转到用户代码来执行用户代码的。同样* ~' q! l% y3 U# P! }4 c- Z 的道理,只要知道System Memory 的地址,一样可以从用户代码通过重新设置主堆栈指针并跳转到System Memory 来执行0 P) y; N6 G# I) C# @, K2 B Bootloader。而System Memory 地址可以从参考手册来获得。比如,查看STM32F411 的参考手册RM0383,可以找到如下& z- {$ p, }+ k# K9 _9 d3 b 的表格:1 Z4 B* x" t. U3 @7 f% U8 h ![]() 可以知道STM32F411 的System memory 地址从0x1FFF0000 开始。3 g% h# [2 t* c# Q9 x$ C. v 3 W; o( a- h+ g) p 那很多人又会问了,我的代码很复杂,用了很多外设,开了很多中断,可是要跳转到System Memory 中的Bootloader,需要 关所有外设的时钟,需要关PLL,需要关闭所有中断,需要禁用所有的中断,清除所有挂起的中断。这可是一项非常庞大的- H8 S+ \0 `5 v& S 的任务啊!所以,在这里,我们需要一个更简单的事情来完成这项庞大的任务。其实真的就有这么简单的一个方法——复位!6 z i5 s) R" J 通过软件复位来实现这一目的。但是,复位后,又怎么知道还记得我们要去做代码升级呢?这又要用到STM32 另一个特性了, 那就是后备数据寄存器Backup Data Registers 在软件复位后会保留其值,这样给了我们在复位前后做一个标志的机会。 这样,考证下来,客户的需求是具备可行性的。接下来需要做的是理清思路。 2.软件流程- ~/ v( K6 U; a2 L% | 这里使用32F411EDISCOVERY 板来设计一个参考例程:设计一个用户程序,让LED3 进行闪烁;当用户按键被按下,产生 EXTI 中断,在中断中选择后备数据寄存器RTC_BKP0R,写入值0x32F2,然后产生软件复位;软件复位后,在运行代码的3 ?( g, ?* @ W ?; m f* ^1 m" D 最前面对RTC_BKP0R 进行判断,如果其值不是0x32F2 则直接去运行用户代码,如果其值为0x32F2 则是需要跳转到 Bootloader 去进行代码升级,并在跳转前将RTC_BKP0R 清零。这样,在进入Bootloader 后,客户进行USB DFU 升级后, 将来不会因为非需要升级代码的复位而误入Bootloader。 来看软件流程图,先来看主程序的流程图: ![]() 再来看EXTI 中断的流程图: ![]() 3.主要代码2 j% U9 l# \; u9 j9 s 使用STM32F4Cube 库来开发这个例程。先来看位于main.c 中的main 函数:. }: I8 i& |# Y3 N ![]() Main 函数很简单,配置系统时钟,对使用的LED 进行初始化,然后配置了用户按键的EXTI 中断,然后就进入主循环了。前 面说到,要实现用户的功能程序为LED3 闪烁,在主循环我们没看到,是因为在Cube 库中,会使用SysTick,所以把LED3: ?6 o4 y1 s8 C- A9 O. L1 S* I 的闪烁放到SysTick 的中断代码中了,查看stm32f4xx_it.c,如下:8 F- Z1 S! U0 y5 ` ![]() 从main 函数最开始的那段注释中知道,跳入main 函数前,在startup_stm32f411xe.s 中早已经先调用执行了位于9 F" y; J# ^; U3 a! y/ \, | system_stm32f4xx.c 中的SystemInit 函数。SystemInit 函数主要执行初始化FPU、复位RCC 时钟寄存器、配置向量表等功* b" _3 P. i6 J 能。由于我们希望在最原始的状态下进入System Memory,所以我们将跳转到System Memory 放在这个函数的最前头,如 下:& x% N! m/ g2 i' m1 J1 u ![]() 可以看到,在函数的最前面对RTC_BKP_DR0 进行了判断,如果其值为0x32F2 的话,则先启动备份域的访问时序,如 RM0383 中5.1.2 Battery backup domain 所描述的: ![]() 2 `# {& y: o+ L% c) F. g f" { 然后将RTC_BKP_DR0 清零,再关闭执行这次操作所打开的时钟。 主堆栈指针MSP 的初始值位于向量表偏移量为0x00 的位置,复位Reset 的值则位于向量表偏移量为0x04 的位置。对于+ k) M1 ~, k9 `2 ~ ]6 [' V STM32F411 来说,当执行System Memeory 中的Bootloader 时,MSP 的初始值位于0x1FFF0000,而Reset 则位于/ B" d5 t! k3 M3 g+ @ 0x1FFF0004。所以在程序中,使用__set_MSP(*(__IO uint32_t*) 0x1FFF0000);来重新设置主堆栈指针,而后再跳转到 0x1FFF0004 去执行Bootloader。 再来看位于stm32f4xx_it.c 中的EXTI 中断程序:5 d0 l/ C7 c' y- U% u; ~* A% u ![]() - g2 P% f9 N% W# ] 及其位于main.c中的Callback函数:' L5 F; W. _" P6 R" E$ f* a( A ![]() 当判断到用户按键按下,需要进行用户代码升级时,先启动备份域的访问时序,将RTC_BKP_DR0 的值写为0x32F2。再读 回来判断是否写入成功,以方便调试。如果写入成功后,则就调用HAL_NVIC_SystemReset()进行软件复位。重新复位后,' F, |, N( I1 ?8 e7 a- p 就可以进入System Memory 了。, Z5 Z% ]& T; x 4.实验 使用32F411EDISCOVERY 来做实验。% c& n' d. W% `( a& J 1) 先将程序编译,下载到32F411EDISCOVERY 板,可以看到LED3 在进行闪烁( P- Z% \- Y _6 j9 v5 B ![]() 2) 按下User 按键,LED3 熄灭,已经进入System Memory 中的Bootloader, F: k% [5 D4 W( u `! L3 V 3) 打开DfuSeDemo 软件,此时Available DFU Devices 中没有任何显示% n: E; v4 U! }% P* Y1 \7 T6 g6 { ![]() 4) 将一根USB Micro 连接线插入32F411EDISCOVERY 板的CN5,可见LED7 亮起,USB 已连接 k: ~ M- j# @) P4 K0 v ![]() 5) 驱动完成后,可以再查看一下DfuSeDemo,Available DFU Devices 已经显示为“STM Device in DFU Mode”,代6 [4 v: o* m1 Z 表已经成功驱动并正常工作了7 G5 h; A l5 K ![]() 6 X t7 M! l& p5 `) }% ~8 {& X 6) 之后就是正常的升级代码的流程了,点“Choose”按钮选择要更新的代码,这里准备好了一个 32F411EDISCOVERY 板的Demo 程序经过Dfu file manager 软件生成的32f411ediscovery.dfu 的文件,导入 ![]() 7 q+ Z- r- U) q( z5 m# E 7) 点“Upgrade”按钮进行升级,弹出的对话框选Yes 就可以了,之后就升级成功了 ![]() 8) 再点一下“Leave DFU mode”,进度条显示“Successfully left DFU mode!”,就可以进入更新后的用户代码了, 可以看到4 个LED 灯正常欢快的滚动和闪烁着…… ![]() 7 ]% B! g$ A. ^2 I8 j3 @; } Z) O 注意 此例程仅为验证其可行性,在实际应用中,有不尽完善的地方请用户自行完善。另外,有几个需要注意的地方: 1) 此Demo 代码基于STM32Cube_FW_F4_V1.11.0 撰写,解压缩后,可将其放入. v2 w/ z6 u5 t. ?1 Y \STM32Cube_FW_F4_V1.11.0\Projects\STM32F411E-Discovery\Templates 替换掉原来的源代码文件,即可编译& D5 `0 F" g0 y 运行。3 Q$ {# M+ ?5 [; q- O! Y4 W 2) 此程序使用按键按下作为条件来触发软件代码升级,用户可以根据自己的情况修改触发条件,如多个按键同时按下,) w$ Q/ o! @/ i1 M6 m) V 等等。 3) 当用户应用中使用了RTC 的话,RTC 时钟源一旦被选择后是无法修改的,除非备份域被复位。在RM0383 关于 RCC_BDCR 的描述中有提及:; ~1 O# ?) P3 D& r8 K0 ~ ![]() 4) 关于如何使用Dfu file manager 生成.dfu 文件,请参考UM0412“Getting started with DfuSe USB device firmware, b$ m6 Y4 {$ X6 {6 E" Z upgrade”或者实战经验“利用USB DFU 实现IAP 功能”。5 `- u4 k) `& z6 d+ w 5) 关于Bootloader 中所使用的USB DFU 协议,请参考AN3156“USB DFU protocol used in the STM32 bootloader”。 / p% ]2 p& n0 M6 l( S5 n- t 文档和代码下载地址:: y4 O# l' m) @* Q https://www.stmcu.org.cn/document/detail/index/id-217309 https://www.stmcu.org.cn/document/download/index/id-212785' t; Y( G- Z* s 实战经验汇总:# k: S& W1 j( {$ X I' f https://www.stmcu.org.cn/module/forum/thread-576401-1-1.html : |4 N! o0 D/ Y, M) J 2 p( O+ B2 Z# u- o# U; ^+ P |
谢谢分享! I3 |# h4 `* {7 a$ R7 \. Q. m |
- L, K, Y( v8 P8 q9 B) Y ![]() ![]() |
绝好资料~ |
好东西,值得赞赏 |
楼主辛苦谢谢分享 |
想请教下stm32F411 怎么识别出来进入的是DFU bootloader 而不是usart1 bootloader ? 是因为USB功能enable ? |
厉害! |
好文章 |