1. 项目概述:从串口到USB的固件升级进化
最近在调试一个基于STM32F103的项目,客户那边提了个需求,说能不能把固件升级做得更“傻瓜”一点。之前用的都是串口IAP,每次升级都得找根USB转TTL线,还得打开上位机软件,选择文件,点击下载,对于现场维护人员来说,步骤还是有点多,容易出错。我琢磨着,STM32不是自带USB接口吗,能不能直接用USB来升级?就像我们给手机刷机一样,插上USB线,电脑识别成一个U盘或者一个设备,把固件文件拖进去就完事了。这个想法让我想起了STM32内置的DFU功能。
DFU,全称Device Firmware Upgrade,直译过来就是设备固件升级。它本质上就是一种通过USB接口实现的IAP功能。IAP大家应该不陌生,In-Application Programming,即在应用运行中进行编程,允许微控制器通过某种通信接口(如UART、SPI、CAN)来更新自身的程序存储区。而DFU,你可以把它理解为“USB版的IAP”,它遵循USB-IF组织定义的DFU类设备规范,让STM32在进入DFU模式后,能被电脑识别为一个标准的DFU设备,然后通过专用的DFU工具(如DfuSe)来烧录固件。
这个功能对于没有预留调试接口(如SWD/JTAG)或者接口不便暴露的产品来说,简直是福音。特别是现在很多笔记本为了轻薄都砍掉了传统的串口,USB就成了最通用、最便捷的通道。我这次尝试,就是想把手头这个项目从传统的串口IAP迁移到USB DFU,看看实际用起来到底方不方便,过程中会遇到哪些坑。
2. DFU功能原理与方案选型解析
2.1 DFU与IAP的核心区别与联系
要搞懂DFU,得先理清它和普通IAP的关系。很多人容易混淆,其实它们不是一个层面的概念。
IAP是一种能力,一种机制。它指的是单片机在不需要外部编程器的情况下,通过一段预先烧录好的引导程序(Bootloader),来擦写自身主Flash区域代码的功能。这段引导程序通常驻留在Flash的起始地址(0x0800 0000),或者一个受保护的、不会被主程序覆盖的区域。实现IAP,你需要自己写这段引导程序的代码,处理通信协议(解析来自串口、CAN等的数据包),进行Flash的解锁、擦除、编程、校验等一系列操作。它的优点是灵活,你可以自定义任何通信协议和升级流程;缺点就是所有东西都得自己实现,包括上位机软件。
DFU则是一种标准的USB设备类和协议。它定义了一套标准的描述符、请求和状态机,使得任何支持DFU的设备(不限于STM32)在连接到主机时,都能被统一的管理软件识别和操作。对于STM32来说,实现DFU意味着:
- 芯片硬件支持:STM32的USB外设和内置的Bootloader支持DFU协议。
- 运行DFU模式的代码:这段代码可以是芯片出厂时固化在系统存储区(System Memory)的ROM Bootloader,也可以是你自己编写并烧录到Flash中的自定义Bootloader。
- 遵循DFU协议:无论是ROM Bootloader还是自定义Bootloader,在与主机通信时,都必须遵循DFU协议规范来响应各种标准请求(如下载、上传、获取状态、清除状态等)。
所以,DFU是IAP的一种具体实现方式,而且是标准化程度很高的一种。当你使用STM32的DFU功能时,你实际上是在利用芯片自带的硬件能力和协议规范,来实现通过USB接口的IAP。
2.2 STM32 DFU的两种实现路径
具体到STM32上,我们有两种主要路径来启用DFU功能:
路径一:使用内置的ROM DFU Bootloader这是最快捷的方式。ST在生产芯片时,已经在系统存储区(地址因系列而异,例如STM32F103是0x1FFFF000)固化了一段Bootloader代码。这段代码支持多种启动方式,包括通过USB进入DFU模式。
操作方法:通过配置芯片的启动引脚(BOOT0, BOOT1),使其从系统存储器启动。对于STM32F103,设置BOOT0=1,BOOT1=0,然后复位,芯片就会运行ROM中的Bootloader。如果此时USB(USB-DP引脚)上有正确的上拉电阻(1.5kΩ连接到3.3V),芯片的USB口就会被枚举为一个DFU设备。
优点:
- 无需编写额外代码:直接利用芯片原厂功能。
- 不占用用户Flash:Bootloader在独立的系统存储区。
- 稳定可靠:ST官方提供并测试。
缺点:
- 功能固定:只能使用ST官方DfuSe工具,升级流程固定,无法自定义(例如增加升级前的身份认证、数据加密)。
- 依赖启动引脚:需要硬件上设计BOOT引脚的控制电路,或者手动跳线。
- 无法实现“无感升级”:通常需要用户手动操作进入DFU模式。
路径二:编写自定义的USB DFU Bootloader这种方式就是自己实现一个支持DFU协议的IAP引导程序,并将其烧录到用户Flash的起始扇区(例如0x0800 0000)。
操作方法:你需要编写一个完整的USB设备程序,将其设备类(bDeviceClass)配置为0xFE(应用特定),并实现DFU类描述符和所有的DFU类请求。ST的CubeMX和CubeFirmware库中提供了完整的DFU设备类中间件(Middleware),可以大大简化开发。你的主应用程序则需要编译到Flash的另一个偏移地址(如0x0800 4000),并在需要升级时,通过某种触发方式(如长按某个按键、接收特定命令)跳转回这个自定义的Bootloader。
优点:
- 高度自定义:可以整合复杂的升级逻辑,如A/B分区、差分升级、安全校验、断点续传。
- 用户体验好:可以实现“一键升级”或后台静默升级,无需用户手动设置启动模式。
- 不依赖启动引脚:通过软件控制即可进入升级模式。
缺点:
- 开发复杂:需要深入理解USB协议和DFU规范。
- 占用Flash空间:Bootloader程序本身需要占用一部分用户Flash(通常至少8-16KB)。
- 需要自行处理跳转和内存布局:对链接脚本(.ld文件)的修改需要格外小心。
注意:我这次初步尝试,目的是快速验证DFU功能的便利性,所以选择了路径一:使用内置ROM DFU Bootloader。这也是大多数工程师初次接触DFU时会选择的入门方式。理解了这种方式,再去看自定义Bootloader,就会清晰很多。
2.3 硬件连接的关键细节
使用内置Bootloader时,硬件连接有一个极易被忽略但至关重要的点:USB上拉电阻。
STM32的USB接口是USB 2.0全速(12 Mbps)设备。根据USB规范,全速设备需要在USB数据线D+(对于STM32是USB_DP引脚)上连接一个1.5kΩ的电阻到3.3V电源,以向主机宣告自己是一个全速设备。
常见问题:很多STM32开发板为了节省成本或简化设计,这个1.5kΩ的上拉电阻是通过一个MOS管或跳线帽连接到USB_DP引脚的,而MOS管的控制信号通常来自STM32的某个GPIO(如PA8)。在正常应用程序中,初始化USB外设前,你需要先拉高这个GPIO,使能上拉电阻。
但在DFU模式下,问题来了:当芯片从系统存储器启动,运行ROM Bootloader时,它不会去初始化你的应用程序中配置的那个GPIO!因此,那个关键的上拉电阻可能没有被使能。这会导致电脑根本无法识别到USB设备,DFU也就无从谈起。
解决方案:
- 检查原理图:首先确认你的板子上是否有这个1.5kΩ上拉电阻,以及它是如何连接的。
- 硬件修改(推荐):最稳妥的方法,是将这个1.5kΩ电阻直接、永久地连接到
USB_DP引脚和3.3V之间,移除任何开关控制。这样无论芯片运行什么代码,只要上电,主机都能正确识别它是一个全速USB设备。 - 软件无法解决:不要指望在应用程序里做任何设置来影响ROM Bootloader的行为,这是行不通的。
我这次就踩了这个坑。一开始怎么都无法让电脑识别出DFU设备,排查了半天,最后用万用表测量USB_DP引脚电压才发现,在DFU启动模式下,电压只有0.几伏,远没有达到被识别所需的电平。飞线直接焊上一个1.5kΩ电阻到3.3V后,问题立刻解决。
3. 实操过程:从零开始完成一次DFU升级
3.1 环境与工具准备
工欲善其事,必先利其器。使用STM32的ROM DFU,你需要准备以下软件:
- STM32CubeProgrammer (STM32CubeProg):这是ST官方推出的多合一编程工具,支持ST-LINK、UART、USB DFU等多种连接方式。强烈建议使用这个替代旧的DfuSe工具,因为它更新更稳定,且与Cube生态集成更好。可以从ST官网下载。
- DFU驱动:Windows系统可能需要安装DFU设备的驱动程序。通常STM32CubeProgrammer安装包会自带,或者在连接设备时,系统会自动通过Windows Update搜索安装。如果遇到黄色感叹号,可以手动指定驱动路径到CubeProgrammer的安装目录下寻找。
- 目标固件文件:你需要准备一个要烧录的
.hex或.bin文件。这里有一个关键步骤:你的应用程序不能被编译到Flash的0x0800 0000地址。因为0x0800 0000开始的空间要留给(或即将运行)Bootloader。你需要修改IDE中的链接配置。
以Keil MDK为例,修改应用程序起始地址:
- 打开
Options for Target->Target选项卡。 - 将
IROM1的起始地址Start从默认的0x08000000修改为0x08004000(偏移16KB)。这个偏移量没有绝对标准,只要大于你预留的Bootloader空间即可,16KB或32KB是常见值。 - 同时,你需要修改中断向量表的偏移量。在
system_stm32f1xx.c文件(或其他系列对应文件)中,找到VECT_TAB_OFFSET的定义,将其修改为0x4000。或者,在应用程序初始化时(main函数开头)调用SCB->VTOR = FLASH_BASE | 0x4000;。 - 重新编译,生成的
.hex文件就是从0x08004000开始的了。
3.2 进入DFU模式的操作流程
这里以最常见的STM32F103C8T6核心板为例,描述操作步骤:
- 硬件连接:确保USB上拉电阻问题已解决(见2.3节)。将开发板的USB口(必须是USB Device口,不是USB转串口)连接到电脑。
- 配置启动模式:
- BOOT0引脚接高电平(3.3V)。
- BOOT1引脚接低电平(GND)。
- 很多核心板有BOOT0/BOOT1的跳线帽,将其设置到正确位置。如果没有,就需要自己飞线。
- 复位芯片:按下板子的
NRST复位键,或者重新上电。 - 系统识别:此时,Windows会在右下角提示“正在安装设备驱动程序”,稍等片刻。打开设备管理器,在“通用串行总线控制器”或“通用串行总线设备”下,你应该能看到一个名为“STM32 BOOTLOADER”的设备。
实操心得:第一次操作时,最容易出错的就是忘记设置BOOT引脚或者设置错误,以及USB上拉电阻问题。如果设备管理器里没有出现,请严格按照上述步骤检查。也可以尝试换一个USB口,有些电脑的USB口供电或识别能力较弱。
3.3 使用STM32CubeProgrammer进行烧录
- 打开STM32CubeProgrammer。
- 选择连接方式:在左上角选择
USB。 - 刷新端口:点击
Refresh按钮,如果连接正确,下方会显示检测到的DFU设备端口号。 - 连接:点击
Connect。连接成功后,右侧会显示芯片的信息(型号、UID等)。 - 下载固件:
- 点击
Open file按钮,选择你修改过链接地址后生成的.hex文件。 - 在
Download区域,确认编程地址(Start address)是否与你应用程序的起始地址一致(例如0x08004000)。CubeProgrammer通常能自动从hex文件中读取地址信息。 - 点击
Download按钮。进度条会开始走动。
- 点击
- 退出DFU模式与复位:
- 烧录完成后,不要急着断开USB线或关闭软件。
- 首先,将BOOT0跳线帽重新接回低电平(GND)。
- 然后,点击CubeProgrammer上的
Disconnect按钮断开连接。 - 最后,按下板子的复位键。此时,芯片将从0x08000000启动,但因为我们没有烧录Bootloader到那里(那里是空的),芯片会继续向后寻找有效的程序,从而跳转到我们刚刚烧录在0x08004000的应用程序并执行。
3.4 关于“49%出错”问题的分析与解决
你在描述中提到的“退出DFU模式时,发现也是49%出错问题”,这是一个非常经典的问题。我这次也遇到了。
现象:在使用旧的DfuSe Demo工具(v3.0.x)时,烧录过程顺利,但在最后一步“Leave DFU mode”时,进度卡在49%,并弹出错误提示。
原因分析: 这个错误通常不是烧录失败。事实上,固件已经成功写入Flash。问题出在DFU协议的状态切换上。当主机发送“离开DFU模式”的请求时,它期望设备复位并重新枚举为一个普通设备(而不是DFU设备)。然而,STM32的ROM Bootloader在接收到这个请求后,执行的操作可能因芯片系列、工具版本和驱动状态的微小差异而不同步,导致主机端工具没有收到预期的响应,从而报错。
解决方案:
- 升级/更换工具:这是最有效的方法。如前所述,放弃使用旧的DfuSe,改用STM32CubeProgrammer。CubeProgrammer在处理DFU协议的状态机时更加健壮,我使用后从未再遇到49%错误。
- 手动操作替代“Leave DFU”:如果坚持使用DfuSe,可以忽略这个错误。当烧录完成,即使报错,只要确认文件校验通过,你就可以:
- 断开USB线。
- 将BOOT0设置为低电平。
- 重新上电或复位。应用程序应该能正常运行。
- 检查驱动:确保使用的是ST官方最新的DFU驱动,老旧的或兼容性差的驱动也可能导致此问题。
结论:这个49%错误更像是一个工具与驱动兼容性的“假错误”,不代表固件损坏。切换为STM32CubeProgrammer是治本之策。
4. 进阶:实现按键触发的自定义DFU Bootloader
使用ROM Bootloader虽然简单,但每次升级都要拔插跳线帽,实在太不“产品化”了。接下来,我们尝试更实用的方案:实现一个自定义的USB DFU Bootloader,并通过按键触发。这也是你原文中提到的“第二次必须使PB.0按键接地”所对应的场景。
4.1 使用STM32CubeMX快速生成DFU Bootloader框架
ST的CubeMX工具极大地简化了USB DFU Bootloader的创建。
- 新建项目:选择你的STM32型号。
- 配置时钟树:确保系统时钟和USB时钟(48MHz)正确配置。USB对时钟精度要求较高,通常需要使用PLL。
- 启用USB外设:
- 在
Connectivity下,将USB模式选择为Device (FS)。 - 在
Middleware中间件部分,勾选USB_DEVICE。 - 在
Class For FS IP中,选择DFU (Device Firmware Upgrade)。
- 在
- 配置GPIO作为触发引脚:例如,将
PB0配置为GPIO_Input,并启用上拉电阻(这样按键另一端接地时,才能检测到低电平)。 - 生成代码:指定工具链(MDK-ARM/IAR/STM32CubeIDE),点击生成代码。
CubeMX会自动生成一个完整的USB DFU设备工程。这个工程编译后,就是一个可以烧录到0x08000000地址的Bootloader。
4.2 修改Bootloader代码以实现按键检测与跳转
生成的代码骨架已经实现了DFU协议的核心。我们需要添加按键检测逻辑,决定是进入DFU模式等待升级,还是跳转到应用程序。
关键代码通常在Src/main.c的main函数中:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USB_DEVICE_Init(); // 初始化USB为DFU设备 // 按键检测逻辑 // 假设按键按下为低电平,且我们使用上拉电阻 if (HAL_GPIO_ReadPin(TRIGGER_KEY_GPIO_Port, TRIGGER_KEY_Pin) == GPIO_PIN_RESET) { // 按键被按下,进入DFU模式 // USB已经初始化,DFU描述符已准备好,等待主机连接 while (1) { // DFU模式循环,USB中断会处理所有主机请求 // 这里可以加一个超时退出机制,比如等待30秒无操作则跳转到APP MX_USB_DEVICE_Process(); // 处理USB事件 } } else { // 按键未按下,尝试跳转到应用程序 JumpToApplication(); } }JumpToApplication()函数需要自己实现,其核心是:
- 检查目标地址(如0x08004000)是否有一个有效的应用程序(通常检查栈指针初始值是否在RAM范围内)。
- 关闭所有中断,重新设置向量表偏移。
- 将栈指针(MSP)设置为目标地址处的第一个字(即应用程序的初始栈顶)。
- 跳转到目标地址+4的位置(即应用程序的复位中断向量)。
void JumpToApplication(void) { typedef void (*pFunction)(void); pFunction Jump_To_Application; uint32_t JumpAddress; // 应用程序起始地址 const uint32_t APPLICATION_ADDRESS = 0x08004000; // 检查应用程序栈顶(第一个字)是否合法(在RAM范围内) if (((*(__IO uint32_t*)APPLICATION_ADDRESS) & 0x2FFE0000) == 0x20000000) { // 设置新的向量表位置 SCB->VTOR = APPLICATION_ADDRESS; // 设置应用程序的栈指针 __set_MSP(*(__IO uint32_t*)APPLICATION_ADDRESS); // 获取应用程序的复位中断服务程序地址(第二个字) JumpAddress = *(__IO uint32_t*)(APPLICATION_ADDRESS + 4); Jump_To_Application = (pFunction)JumpAddress; // 跳转前禁用所有中断 __disable_irq(); // 跳转到应用程序 Jump_To_Application(); } // 如果检查失败,可以停留在此处或进入错误处理 }4.3 应用程序的配合与链接脚本修改
Bootloader做好了,应用程序也需要相应调整:
- 修改中断向量表偏移:如前所述,在应用程序的
main函数最开始,或system_stm32xx.c中,设置SCB->VTOR = FLASH_BASE | 0x4000;。 - 修改链接脚本:这是确保代码被放到正确位置的关键。
- Keil MDK:在
Options for Target->Linker选项卡下,取消勾选Use Memory Layout from Target Dialog,然后编辑分散加载文件(.sct)。将LR_IROM1的起始地址改为0x08004000。 - STM32CubeIDE/GCC:编辑
STM32xxxx_FLASH.ld链接脚本文件,将FLASH区域的起始地址ORIGIN改为0x08004000,长度LENGTH相应减少。
- Keil MDK:在
- 编译生成应用程序:重新编译后,其二进制文件就是从0x08004000开始的了。
4.4 完整的升级流程体验
现在,一个产品化的升级流程就形成了:
- 产品出厂:将自定义的DFU Bootloader烧录到0x08000000,将第一版应用程序烧录到0x08004000。
- 用户正常使用:上电后,Bootloader检测按键未按下,直接跳转到应用程序运行。
- 触发升级:当需要升级时,用户长按我们指定的按键(如PB0)不放,然后给产品复位或重新上电。
- 进入DFU模式:Bootloader启动,检测到按键被按下,于是初始化USB DFU,等待电脑连接。此时电脑会识别到一个DFU设备。
- 烧录新固件:用户使用STM32CubeProgrammer,选择新的应用程序
.hex文件,下载到0x08004000地址。 - 完成升级:烧录完成后,用户松开按键,并给产品复位。Bootloader启动后检测到按键已释放,于是跳转到新的应用程序,升级完成。
这个过程完全摆脱了跳线帽,用户体验得到了质的提升。你原文中提到的“第一次IAP不用按任何按键,第二次必须按”,指的就是这种自定义Bootloader的流程:第一次烧录Bootloader和APP可能需要用ST-LINK,之后的每次升级,都只需要通过按键触发即可。
5. 常见问题、排查技巧与深度优化
5.1 DFU设备无法识别的全方位排查
如果电脑无法识别DFU设备,请按照以下清单排查:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 设备管理器无任何新设备 | 1. BOOT引脚设置错误。 2. USB上拉电阻未连接或未使能。 3. USB线或端口故障。 4. 芯片未正常复位。 | 1. 用万用表测量BOOT0/BOOT1电压。 2. 测量USB_DP引脚对地电压,在连接USB后应有约3.3V电压(通过1.5kΩ上拉)。 3. 更换USB线或电脑端口。 4. 确保进行了复位操作。 |
| 设备管理器出现“未知设备”或带感叹号设备 | 1. DFU驱动未正确安装。 2. 使用了不兼容的驱动。 | 1. 右键设备,选择“更新驱动程序”,手动指向STM32CubeProgrammer安装目录下的驱动文件夹(如Drivers/DFU)。2. 尝试完全卸载旧驱动后,重新插拔设备。 |
| 设备识别为其他USB设备(如HID) | 芯片运行的不是ROM DFU Bootloader,可能是用户程序中的USB代码。 | 确认BOOT引脚已正确设置为从系统存储器启动,并确保已复位。 |
5.2 烧录失败与校验错误处理
| 错误类型 | 可能原因 | 解决方案 |
|---|---|---|
| “Cannot connect to target” | 1. 其他软件占用了USB端口。 2. 芯片未处于DFU模式。 | 1. 关闭所有可能使用USB端口的软件(如串口助手、其他编程软件)。 2. 重新执行进入DFU模式的操作流程。 |
| “File download failed” 或校验错误 | 1. 应用程序链接地址与下载地址不匹配。 2. Flash被写保护。 3. 电源不稳定。 | 1.仔细核对CubeProgrammer中“Start address”和应用程序.hex文件的实际起始地址。用文本编辑器打开hex文件,看第一条扩展线性地址记录(:02...04)。2. 在CubeProgrammer的“OB” (Option Bytes) 页面,检查并解除读保护(RDP)。 3. 确保板子供电充足,特别是使用USB供电时,如果板载外设多,可能导致电压跌落。 |
| 烧录成功但程序不运行 | 1. 中断向量表偏移未设置。 2. Boot引脚未切回。 3. 应用程序本身有bug。 | 1. 确认应用程序代码中正确设置了SCB->VTOR。2. 烧录完成后,务必将BOOT0设置为0并复位。 3. 用调试器直接加载应用程序到0x08004000调试,排除程序逻辑问题。 |
5.3 从DFU升级到OTA的思维拓展
实现了USB DFU,其实已经为更高级的空中升级打下了基础。OTA的核心依然是IAP,只是数据传输的通道从USB变成了无线(如Wi-Fi、蓝牙、LoRa)。
思维转换:你可以将自定义的DFU Bootloader看作一个“通用的固件接收器”。它通过USB接收固件数据。如果要实现OTA,你只需要做一件事:把这个“接收器”的数据来源,从USB端点,换成无线模块的串口/SPI缓冲区。
一个简单的Wi-Fi OTA框架设想:
- Bootloader初始化后,不仅初始化USB,也初始化一个串口连接Wi-Fi模块。
- 按键触发或通过网络命令触发进入升级模式。
- Bootloader通过串口与Wi-Fi模块通信,从网络服务器分块下载固件数据包。
- 将接收到的数据包写入Flash的应用程序区域(0x08004000)。
- 下载完成后,校验、跳转。
这里的难点不再是IAP本身,而是无线通信的稳定性、数据包的校验重传机制、以及升级过程的安全保障(如签名校验)。有了USB DFU Bootloader的开发经验,再去理解OTA Bootloader的设计,就会清晰很多。
5.4 性能与安全考量
- 升级速度:USB DFU(全速12Mbps)的升级速度远快于串口(115200bps约合11.5KB/s)。实测烧录一个128KB的固件,串口需要10秒以上,而USB DFU仅需2-3秒。
- Flash寿命:频繁的擦写会影响Flash寿命。在升级流程设计中,应避免不必要的全片擦除。DFU协议支持擦除指定扇区,工具一般会智能处理。
- 安全性:这是产品化必须考虑的。公开的DFU接口存在被恶意刷机的风险。
- 建议1:增加触发门槛:不要使用简单的按键,而是使用复杂的组合键、密码序列或通过应用程序内的授权命令来触发进入DFU模式。
- 建议2:固件签名:在Bootloader中集成非对称加密算法(如ECDSA)验证,只烧录带有合法签名的固件。这是目前工业级产品的标配做法。
- 建议3:关闭调试接口:产品发布前,通过选项字节(Option Bytes)关闭SWD/JTAG调试接口,增加逆向工程难度。
这次对STM32 DFU功能的尝试,从最初为了解决一个具体的客户需求,到深入理解其原理,再到亲手实现一个可产品化的按键触发Bootloader,整个过程让我对STM32的启动流程、内存映射、USB协议和固件升级架构有了更立体、更深刻的认识。技术上的每一个小细节,比如那个不起眼的1.5kΩ上拉电阻,都可能成为项目推进路上的拦路虎。而把功能做出来只是第一步,如何让它稳定、安全、易用,才是工程师价值的真正体现。下次如果再遇到需要升级功能的项目,我脑子里可选的方案就又多了一个扎实可靠的选项。