USB DFU培训 1 前言 本文根据2017年度广州USB DFU培训内容进行整理而成,主要目的是为了方便那些由于各种原因未到现场参加培训的碟粉们参阅学习。本文主要是介绍如何使用CubeMx这个工具,一步一步制作一个BOOT(DFU)程序,并使用它来升级用户APP程序,这种应用场合在产品开发中具有普遍性。 4 r5 @( z. e" K9 n1 T; v; u* w4 d" g$ S + n! M5 m6 Z& ^4 y* H) [ 2 实验环境介绍. c4 n7 x; p1 |2 \ b) s 2.1 实验目标+ g9 N9 S/ n/ T Figure 1 BOOT与APP( w. ~/ h& W) I3 M# P! b6 b $ v7 @3 s2 r3 P5 [ 如上图,本实验主要目的是制作BOOT程序,并通过BOOT来升级用户的APP程序,上图是BOOT和APP在MCU内部FLASH的位置示意图。当然,我们也将简单的介绍这个用户APP需要注意哪些方面。 , C9 _; D' \# k2 r0 N' ? 注:这里我们将使用USB DFU作为IAP的这个程序叫做BOOT程序,功能与内置BootLoader类似,主要负责升级APP或直接跳转到APP去运行(MCU内置BootLoader,为了与其区分,这里取名为BOOT,后面不再解释)。 9 V/ a/ x: W( u/ L& ]/ j 2.2实验环境及STM32F072-Discovery板简介' B# ?* q# R7 C% y1 @8 `* Y 硬件平台: ●STM32F072 Discovery板一块 ●Mini USB线两根 ●PC一台 软件准备: ●IAR V6.7.0 或者以上版本3 j" {0 ^# ?6 j. b* \% } ●STM32CubeF0 V1.7.0 ●STM32CubeMX V4.19 ●DfuSeDemo(V3.0.5) Figure 2 STM32F072-Discovery板 Figure 3 STM32F071-Discovery板上的一些外围电 * E! c6 c w9 P7 V$ G . a) f g" x) {" F" W+ z+ x2 q# y | 3 制作BOOT工程: k' ~; r2 j/ J7 R6 r+ h* i, T6 O 3.1 了解MCU内部FLASH的组织结构; g4 L7 H- [ ]0 u Figure 4 BOOT与APP在内部FLASH的位置 4 L4 P, S1 N' L% H. D/ z0 v" K 如上图,BOOT程序位于内部FLASH的起始位置,其后跟着的APP程序假设放在0x0800 7000的位置。也就是说前面0x7000的空间用来存放BOOT。 在制作BOOT程序之前,首先我们必须得先了解目标MCU的内部FLASH结构,这个是与我们的BOOT程序相关的,因为不同的MCU类型其内部FLASH组织结构不尽相同,尤其在擦除FLASH的时候,我们必须得知道擦除是以页为单位还是以扇区为单位,以及总共有多少个单元,每个单元多大等等。 这里我们实验的目标MCU为STM32F072RBT6,打开其参考手册,得到其内部FLASH的组织结构如下图所示: Figure 5 STM32F072RB 内部FLASH组织结构5 a% R) o( R- \ # W* [. K: ?9 ^' R$ n7 _ 从上图我们可以得到如下信息:) {0 F; G- y3 [ 1> 内部FLASH起始位置0x0800 0000,结束位置0x0801 FFFF;1 T6 W D: L7 |# M+ \' m# K4 z E 2> 总共有64个页(0~63),每页大小2K,共128K大小; 3> 擦除FLASH是以页为单位。 8 R& C& k G* p' r4 T, H G3 M 3.2 制作CubeMx工程* ]& i, E. `+ s; Q 下面我们来从零开始为这个BOOT程序制作一个CubeMx工程,在选取STM32F072RBx作为目标MCU后,使能USB_FS外设,并将PA0设为GPIO_Input模式,PA0对应着STM32F072-Discoery板上的用户按键,我们将在程序中根据PA0管脚的电平状态来判断是否直接跳转到APP还是进入到DFU模式,准备升级用户APP程序。 Figure 6 BOOT程序Pinout# J* q8 u2 L% D: a n R' b. s; [ 接下来是时钟树配置:6 j# x* c# y7 t _/ y Figure 7 BOOT程序时钟树配置 , N% s7 J0 y2 q 如上图,我们使用内置HSI48RC作为高速时钟源,主频配为48MHz,另外需要注意的是,USB外设必须给它提供固定的48M时钟源。 Figure 8 BOOT配置 如上图左边,为USB Device配置DFU子类。然后点击右侧中间件下的USB_DEVICE配置,弹出如下对话框:5 @ T! t. H3 H5 T* C) A2 p7 P Figure 9 USB Deivce Configration6 b9 D3 y$ d. H6 {* H* k. ] 如上图红框中所示,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 ; @Internal Flash: MCU内部FLASH在DfuSeDemo这个PC软件中显示的字符串名,这里显示为”Internal Flash”; /0x08000000 : 内部FLASH的起始位置为0x0800 0000 ;7 a, \* }" X0 C8 j( I' r /09*002Ka : 9个只读单元,每个单元大小为2K,后缀a表示只读,一般用来放置BOOT程序 ; 55*002Kg : 55个可读写单元,每个单元大小为2K,后缀g表示可读写,一般用来表示用户APP空间。 . d8 _+ n6 ^( i BOOT程序通过这个一个带某种特殊格式的字符串来告诉PC端主机的上位机软件DfuSeDemo,当前连接的MCU内部FLASH的组织结构,这个字符串的表达方式,是通过BOOT程序与PC端软件DfuSeDemo预先约定好的。最终在DfuSeDemo这个PC端软件可以显示当前连接USB 设备MCU内置FLASH的结构,这个在后续将会介绍到。- v, P4 F8 `% ?, b: J5 n2 S 最后生成代码时设置下堆栈大小: Figure 10 堆栈大小设置 如上图,我们设置BOOT程序的堆为0x500,栈大小为:0x2000; 并生成IAR工程。 ( v, Y' d i, V( @2 ~4 g; V; B 3.3 完善BOOT工程, P4 @; q( [. S# p) E+ u) [ 生成的IAR工程如下所示: Figure 11 BOOT源文件与USB软件框架的对应关系6 T% V1 e+ h6 w/ A, X) y8 t+ n CubeMx工具已经自动帮我们生成了大部分代码,绝大部分我们都不需要修改,我们主要是根据STM32F072这款MCU内部FLASH本生的一些特性来修改usbd_duf_if.c源文件,其次,根据应用上的一些特点,我们稍微修改下main函数,使其达到我们需要的结果。这些我们都将逐步在后续介绍。* o5 I6 ~1 h1 L7 p ! Q) L0 A+ w& T 3.3.1 usbd_dfu_if.c源文件的修改$ |& h) g+ V! W7 A usbd_dfu_if.c源文件主要是实现对FLASH的一些操作,主要有这些操作: 1 V$ H, t& Y! \" Y v- p CubeMx自动生成的usbd_dfu_if.c源文件中对这些函数都已经生成了大概框架,用户只需要完成其具体实现内容即可,这好比做填空题一样。 ● 初始化: # {. j) a2 n: z% l8 Z 在初始化函数中,完成对FLASH的解锁。5 C9 _, p$ P( `; U) I, G 3 }* F& ~. b1 o" P# F- \ ● 反初始化: B9 O% X- d) F( b2 Y1 I 对应的,在反初始化函数中,恢复FLASH的锁定状态。/ _# r' p. W# q+ N 1 B1 j& E1 v9 K& J/ V ● 擦除FLASH# ^6 |7 U W$ D2 P c0 [% G" _! v; q # e9 Y( M8 [9 t 需要注意地是,这里擦除的是指擦除整个APP所在的空间,因此,将会擦除掉page8~63的所有内容。1 }: P% n$ l' J& d n# F& X 上述函数用到的一些宏定义如下: ● 写FLASH/ O9 O( u8 J0 W, K; J ● 读FLASH & g* b3 [; v( u! ~ ● 获取状态 上面函数中用到的一些宏如下定义: 3 ^4 m$ Q4 \4 Z 上述函数接口的具体实现是根据具体MCU的特点来实现的,若换成其他类型的STM32,则需要根据当前的STM32型号做相应修改。如此,usbd_dfu_if.c源文件就修改完了。 & t9 y, Q- v) J! n Z0 ^ 3.3.2 main源文件修改6 R' U9 I. P: j$ V9 A4 q 在main函数内,我们需要根据外部用户按键的状态来判断是否跳转到APP还是停留在BOOT程序内并准备对APP进行升级。Main函数修改如下: % ~+ j$ m. B, M. N q 上述代码中: 这行代码是判断FLASH中是否存在APP程序。它的判断原理是这样的,若存在APP,则USBD_DFU_APP_DEFAULT_ADD这个地址保存的是栈顶指针MSP,栈指针肯定是指向内存的,而内存有效地址分为0x2000 0000起始的128K范围内。' `$ f3 e/ O6 R% V4 u! h( H * J* P, g& b8 W+ |& X & 0x2FFE0000的意思是将MSP的值过滤掉低17位,17位二进制刚好表示128K空间范围,也就是说,只有MSP的值指向SRAM的128K有效范围内时,才是合法的,此时则表示APP存在,否则,APP不存在。 跳转地址为APP起始地址+4,因为前4个字节表示的是MSP的值。所以接下来有如下初始化MSP的操作: 8 _, W7 _: u2 b" _+ a1 D# k 到此,BOOT程序已经完成了。 4 制作APP工程 制作APP的过程我们也讲一下,因为有些地方还是需要注意下。 我们再次回顾下BOOT与APP在内部FLASH中的位置: Figure 12 APP在内部FLASH中的位置2 N6 O Z5 r% E8 }: K9 H5 t 如上,用户APP是放在起始位置为0x0800 7000的位置,当然这个位置是示例,实际工程中用户得根据自己的情况来设置,比如BOOT实际大小多少,那么APP肯定得放在BOOT之后,绝对不能有重叠的地方,第二个,APP的起始位置一定要放在Page起始的地方,因为STM32F072内部FLASH擦除是以Page为单位的,换做其他MCU,比如STM32F407,则擦除是以SECTOR为单位,且每个Sector的SIZE不尽相同,那么这个APP的起始位置一定要注意放在Sector/Page起始的位置。 下面我们一步一步详细介绍APP的创建过程,以一个简单的LED灯Toggle的例程向读者介绍用户APP的方方面面。2 S# ^4 ^2 Y# J3 v$ O: M" B0 f* | 4 M( E) ?2 |% ^1 `! D 4.1 使用STM32CubeMx制作APP工程 Figure 13 Pinout设置 如上图,在Pinout中设置LED1~4的四个管脚为GPIO_Output输出模式。/ R% c7 Q" h1 j: |' Y Figure 14 时钟树设置; H! P0 }% [1 K 6 F# j2 g8 b% Y9 @+ \3 K; F! r 时钟树设置还是配置内部48M专用晶振为时钟源。4 n& Z L- L/ Y Q ]# L; s, Z Figure 15 生成APP代码 然后直接点击生成代码。 4.2 完善代码与工程配置7 Y( o& w7 \) t; S6 \: ~ 下面我们完善下代码: 在main函数中,我们让LED等闪烁起来: - t( ?; ~: C) Y9 j$ [3 z3 o 然后在IAR的链接选项内配置中断向量表的位置以及APP在内部FLASH的起始位置:, q; C9 O9 T+ q Figure 16 调整中断向量表和ROM的起始位置5 `3 @1 [; v" f 7 O1 s! t$ e% R: Z# u 然后我们得将中断向量表重映射到SRAM中: ; b( I$ v7 `6 ^; X& L) s4 t( n6 T 在main函数中进行重映射: ; m. l/ Z4 n) e 讲到这里,相信很多读者可能存在疑问,为什么这里一定要这么重映射到SRAM?这个是应为M0内核的MCU没有VTOR这个寄存器,当存在BOOT时,APP中原先本来应该位于0x0800 0000这个位置的中断向量表被BOOT占据了,那么M0内核在运行后怎么找到偏移到0x0800 7000位置的中断向量表的呢? 在M3/M4内核的MCU中,是存在VTOR这个寄存器,M3/M4内核启动后会根据VTOR寄存器的值来寻址中断入口,但M0由于缺少这个寄存器,那么我们就将中断向量表整个搬到SRAM中,再重映射到SRAM,这样一来,M0内核在启动后将不再从内部FLASH寻址中断入口,转而从SRAM中寻址,这样APP的中断就能一样正常响应了。换句话说,如果APP不进行重映射,那么一旦中断产生,那么内核还会从内部FLASH 0x0800 0000的地址中去寻址中断入口,这不成了APP的中断在BOOT中执行么?很明显,会出现错误直接导致崩溃。这就是为什么要重映射的原因。) o. Z" M8 l4 | - Q' N) k2 l$ A7 f+ r5 F" @- v ' E3 [. m: e, C 5 制作DFU文件 后续我们将演示如何使用BOOT来对APP进行升级,我们将使用到一个PC端软件DfuDemo来实现这个过程,这个DfuDemo软件只能识别固定了后缀名为dfu格式的文件,因此,首先我们需要将APP程序转化成dfu文件。, E/ s$ g5 ^' H3 ]* G6 d* t$ B9 q 5.1 生成HEX文件# _1 y: j( @6 b 我们先将APP程序生成hex格式文件: Figure 17 IAR生成HEX格式文件1 Q7 K3 C3 |2 q % {* |# s P7 L4 a# V 如上图所示,在IAR中先设置生成HEX格式文件,最后我们将得到后缀名为hex的文件。2 F4 |0 @8 P" {9 M J, p- {$ o( p. E# l7 m% ] 5.2 将HEX文件转成DFU文件 然后使用DFU File Manager软件将hex文件转成dfu文件: Figure 18导入HEX文件: X2 |( A" R. j. I* f; t 最终转为dfu文件: Figure 19 转成dfu文件 5 s/ h& [0 D* s& Z- B - M% x' a0 A1 c9 o" F; b5 N. L ( z$ v K9 O; d, U# U 6 使用BOOT烧录APP2 Y( n1 q* K U- I N 首先,我们预先将BOOT工程烧录进STM32F072-Discovery板。在连接USB线到PC,打开DfuDemo这个软件,在导入APP.dfu这个转化好的dfu文件并开始烧录。 Figure 20 DfuSe Demo软件# M) Q, p1 |8 y% Q: ?9 u Figure 21 Dfu内部FLASH结构与string对应关系 如上图所示,原先在CubeMx中对USBD_DFU_MEDIA_Interface的字符串设备将在DfuSe Demo这个PC端软件的Flash结构中显示出来,字符串与显示结构的对应关系如上图标志。. Q1 |% d, H- n' ^9 p# l+ Z 烧录完后进行验证 :' ~$ ?3 g' k3 z& Y1 b9 ^& | Figure 22 结果验证 如上图,烧录APP完成后验证是通过的,LED能正常闪烁。$ ^; R# w' y% y3 g ) S. Y4 E- Q5 C4 n 3 B# f" P$ W7 a) O: e* @0 |+ Y 2 D0 f+ O' w2 B+ j- X/ R1 T; G 7 注意事项7 @1 S3 b1 |$ E3 k7 {$ _ 1. BOOT本身实际所占FLASH空间不能与APP空间有重叠,即BOOT不能超过USBD_DFU_APP_DEFAULT_ADD所表示的地址; 2. BOOT本身实际所占FLASH空间必须在string所表示的只读地址范围内;而APP也必须在string所表示的可读写地址范围内; 3. USBD_DFU_APP_DEFAULT_ADD必须向Page/Sector的位置对齐,即向可擦除单元块对齐。9 _' b8 g; K8 c# s 4. 对于Coretex-M0核的MCU,由于没有VTOR这个寄存器,APP若不在默认地址0x0800 0000,则必须将中断向量表重映射到SRAM,APP才能正常工作,对于M3/M4/M7核的MCU,在APP内则修改VTOR这个寄存器即可。 5. 有些MCU内置BootLoader也是支持DFU模式,也是可以通过DfuSeDemo这个软件配合升级用户程序,它的通信协议与本文所述并没有太大差异,只不过本文所述内容为IAP方式,而那个是属于BootLoader方式,这个读者注意区分下,读者可以通过参考应用笔记AN2606了解详情。 " \9 C) @0 S& M; e& z 文档下载 更多实战经验7 V. |! P# w) @6 u# @ |
【经验分享】在进行 USB CDC 类开发时,无法发送 64整数倍的数据
【源码】STLINK-V3MINI 高速USB仿真器,成功改刷【高速CMSIS-DAP】
在线直播|无需编写任何代码即可在STM32上实现USB-C Power Delivery
STM32 USB CDC 虚拟多串口
最全USB HID开发资料,悉心整理一个月,亲自测试
圈圈发布USB图书第二版有感,以及分享一些我学习USB过程...
USB Audio设计与实现
【MCU实战经验】+STM32F107的USB使用
STM32F4-DISC 实现USB主机(U盘)和USB设备(虚拟串口)自动切换
STM32 USB-HID通信移植步骤STM32 USB HID键盘例程
请教个问题啊,我在使用DFU功能更新程序时,没有外部按键来控制,只能是通过向内部flash中写标识,然后重启CPU来控制,重启CPU后出现程序无法启动的情况,单步查找发现,在CPU重启后,会出现读取内部flash不成功的情况,从而导致无法判断是更新程序还是启动app程序,不知道是否遇到过这种情况?
还有就是在没有外部触发的情况下,有什么好的方式来控制升级程序啊?
你尝试读取备份域里面的寄存器来做。
thanks