STM32F103自定义Bootloader开发指南:实现Klipper固件一键更新

📅 2026/7/28 3:45:59
STM32F103自定义Bootloader开发指南:实现Klipper固件一键更新
1. 项目概述为什么我们需要自定义 Bootloader如果你正在玩基于 Klipper 的 3D 打印机并且手头恰好有一块经典的“蓝色药丸”Blue Pill——也就是 STM32F103C8T6 核心板那么你很可能已经尝试过刷写 Klipper 固件了。标准的流程是使用 USB 转 TTL 串口工具通过 BOOT0 引脚拉高进入系统内置的 ROM Bootloader然后用dfu-util或stm32flash这类工具进行刷写。这个方法很通用但每次刷机都要去捅那个小小的 BOOT0 跳线帽对于已经装在机器里的主板来说简直是“反人类”的操作。这就是我们今天要聊的核心自定义 Bootloader。它的目标很简单——摆脱物理跳线的束缚实现通过串口指令就能轻松更新固件。想象一下你只需要在 Klipper 的网页界面比如 Fluidd 或 Mainsail里点击“固件更新”或者通过一条简单的make flash命令主板就能自动进入刷写模式并完成更新整个过程无需打开机箱、寻找跳线帽。这对于追求极致便捷和自动化的工作流来说是至关重要的一步。更具体地说Klipper 社区为 STM32F1 系列包括 F103提供了一个优秀的开源 Bootloader 实现它基于 USB 或串口通信体积小巧功能专一。但直接使用它或者理解其背后的机制对于很多从 Arduino、Marlin 转过来的朋友来说可能有些门槛。本文将带你深入这个 Bootloader 的内部从原理到编译再到刷写和调试手把手教你打造属于自己的“一键刷机”体验。我们会用到 STM32duino 的核心开发环境本质上是基于 Arduino_Core_STM32 和 arm-none-eabi-gcc 工具链以及 Makefile 来管理整个构建过程。2. 核心原理与方案选型Bootloader 是如何工作的在深入代码之前我们必须搞清楚 Bootloader 在 STM32 单片机里扮演的角色以及它是如何与主应用程序对我们来说就是 Klipper 固件共存的。2.1 内存布局与启动流程STM32F103C8T6 拥有 64KB 的 Flash 存储器实际可能为 128KB但官方标称 64KB和 20KB 的 SRAM。单片机上电或复位后会从 Flash 的起始地址0x0800 0000开始执行代码。但是这个起始地址存放的可以是 Bootloader也可以是主应用程序。关键在于芯片的启动模式配置通常由 BOOT0 和 BOOT1 引脚决定。从主闪存存储器启动BOOT00这是最常见的模式。CPU 直接从 0x0800 0000 开始执行。如果这里存放的是 Bootloader那么 Bootloader 会先运行。从系统存储器启动BOOT01 BOOT10执行芯片内部 ROM 中固化的系统 Bootloader也就是我们常用来做串口 DFU 刷机的那个。从内置 SRAM 启动BOOT01 BOOT11用于调试。我们的自定义 Bootloader 方案采用的是第一种模式。我们将 Flash 的前面一部分空间例如 8KB划拨给 Bootloader从地址 0x0800 0000 开始存放。主应用程序Klipper 固件则从紧随其后的地址例如 0x0800 2000开始存放。当芯片复位并处于“从主闪存启动”模式时CPU 首先执行 Bootloader。Bootloader 会进行一些简单的初始化时钟、串口然后检查是否有“更新固件”的触发信号。这个信号可以是一个特定的 GPIO 引脚状态、串口接收到特定指令、或者等待一个超时时间。如果没有触发信号Bootloader 就会跳转到主应用程序的起始地址将控制权交给 Klipper。注意这里的“跳转”不是一个简单的函数调用。它需要禁用所有已开启的中断。将主应用程序的复位向量地址即应用程序的起始地址4加载到程序计数器PC。将主应用程序的堆栈指针初始值即应用程序的起始地址加载到 MSP。执行一个汇编指令如bx完成跳转。一旦跳转Bootloader 的上下文就完全丢失了。2.2 Klipper Bootloader 方案解析Klipper 为 STM32 提供的 Bootloader 源码通常位于其官方 GitHub 仓库的lib/bootloader目录下。对于 STM32F103它主要支持两种通信方式USB和USART1PA9/PA10。USB Bootloader这是最方便的方式主板通过 USB 线直接与上位机树莓派/电脑通信无需额外的串口工具。它模拟了一个 USB CDC ACM 设备虚拟串口上位机可以通过dfu-util或 Klipper 自带的flash_usb.py脚本与之通信并刷写固件。这是首选方案前提是你的主板有 USB接口如 Blue Pill 的 USB口是 PA11/PA12。USART Bootloader使用传统的串口通常是 USART1进行通信。你需要一个 USB 转 TTL 模块连接到主板的 RX/TX 引脚。上位机通过stm32flash工具进行刷写。这种方式更通用但需要外接模块。Bootloader 本身非常精简。它的主要任务序列如下初始化配置最基本的系统时钟HSI、GPIO、以及选择的通信外设USB 或 USART。检测触发条件例如检查某个 GPIO如连接按钮的 PA0是否被按下或者监听串口在短时间内如 100ms是否收到特定的连接字符如\n。决策与跳转如果触发条件满足则进入刷写模式。在此模式下Bootloader 会通过通信接口接收新的固件数据并将其写入到 Flash 中为应用程序预留的区域。刷写完成后通常会执行一次软复位重新开始流程。如果触发条件不满足例如上电后按钮没按串口也没指令则等待一个极短的超时时间如 10ms后立即跳转到主应用程序。刷写协议它通常实现一个简化的 STM32 系统内存 Bootloader 协议USART或基于 DFU 的协议USB以便与标准工具兼容。2.3 工具链与编译环境选择为了编译这个 Bootloader我们需要 ARM 架构的交叉编译工具链。这里我们选择STM32duino环境所依赖的arm-none-eabi-gcc工具链。这并不是说我们要用 Arduino IDE而是利用其成熟、易获取的工具链和芯片支持包CMSIS、HAL/LL 库的变体。为什么不用纯粹的 STM32CubeIDE 或 Keil开源与一致性Klipper 的构建系统Makefile本身就是基于arm-none-eabi-gcc的。使用相同的工具链可以最大程度避免库和编译选项的冲突。轻量级我们只需要编译器、链接器和基本的库文件不需要庞大的 IDE。易于自动化Makefile 可以完美地集成到 Klipper 的构建流程中实现一条命令编译 Bootloader 和应用程序。我们的开发环境将基于Linux树莓派/Raspbian OS 或 Ubuntu 等。在树莓派上直接操作可以实现与 Klipper 主机环境的无缝集成。3. 环境搭建与源码获取工欲善其事必先利其器。我们先来搭建一个干净的编译环境。3.1 安装 ARM 交叉编译工具链在树莓派或 Ubuntu 系统上安装arm-none-eabi-gcc非常简单sudo apt update sudo apt install gcc-arm-none-eabi安装完成后可以通过arm-none-eabi-gcc --version来验证。同时我们还需要make工具通常系统已自带如果没有则sudo apt install make。3.2 获取 Klipper Bootloader 源码最直接的方式是克隆完整的 Klipper 仓库其中就包含了 Bootloader。cd ~ git clone https://github.com/Klipper3d/klipper.gitBootloader 的源码位于~/klipper/lib/bootloader。针对 STM32F103我们主要关注以下目录和文件~/klipper/lib/bootloader通用 Bootloader 框架代码。~/klipper/lib/bootloader/armcm_bootARM Cortex-M 系列的启动文件和链接脚本。~/klipper/lib/bootloader/stm32f0.c/stm32f1.c注意F1 系列的具体实现可能在stm32f0.c中因为代码结构可能复用。需要查看Makefile来确定。实际上Klipper 的构建系统会根据芯片型号自动选择正确的源文件。对于stm32f103它使用的就是stm32f1.c这个文件。关键的芯片级头文件和系统初始化文件。3.3 理解目录结构与 Makefile进入 Bootloader 目录我们先看看其组织结构cd ~/klipper/lib/bootloader ls -la你会看到多个.c、.h文件以及最重要的Makefile。这个Makefile定义了如何为不同芯片编译 Bootloader。我们不需要从头编写而是要学会配置和调用它。Klipper 主目录下的Makefile其实已经封装了对 Bootloader 的编译支持。当我们为某个 MCU 配置如stm32f103执行make时如果检测到需要 Bootloader它会自动调用lib/bootloader下的构建逻辑。但我们也可以独立编译 Bootloader这对于学习和调试更有帮助。关键是要理解编译时需要传递哪些参数。通常你需要指定MCU微控制器型号如stm32f103。BOOTLOADER_SIZEBootloader 占用的 Flash 大小必须是 Flash 扇区大小的整数倍。对于 STM32F103扇区大小可能为 1KB 或 2KB取决于容量。通常预留8KB (0x2000)或16KB是安全且充足的选择。通信方式通过CONFIG_开头的宏定义来选择例如CONFIG_USB1或CONFIG_SERIAL1。4. 编译自定义 Bootloader从配置到生成 HEX 文件现在我们开始动手编译一个针对 STM32F103C8T6、使用 USB 通信、大小为 8KB 的 Bootloader。4.1 确定编译目标与参数我们计划将 Bootloader 烧录到 Flash 的起始位置0x0800 0000占用 8KB 空间即到0x0800 1FFF。那么主应用程序的起始地址就是0x0800 2000。这个0x2000的偏移量非常重要后续编译 Klipper 固件时必须与之匹配。编译参数可以这样组合。最方便的方法是使用 Klipper 顶层的Makefile。首先我们需要一个针对我们主板的配置文件例如~/printer.cfg中指定的 MCU 类型是stm32f103。但为了单独编译 Bootloader我们可以直接使用make命令并指定参数。进入 Klipper 主目录cd ~/klipper然后执行编译命令。Klipper 的构建系统为 Bootloader 提供了一个便捷的目标。对于 STM32F103USB Bootloader 的编译命令通常如下make clean make bootloader KCONFIG_CONFIGconfig.stm32f103-bootloader但是我们需要先创建或确认这个config.stm32f103-bootloader配置文件。实际上更常见的做法是使用menuconfig来交互式配置但针对 BootloaderKlipper 有预设的配置片段。我们可以查看src/目录下的configs/文件夹或者直接查阅文档。一个更直接、更底层的方法是进入 Bootloader 目录并使用其自带的Makefilecd ~/klipper/lib/bootloader make clean make distclean make BOARDstm32f103 CONFIG_USB1 BOOTLOADER_SIZE0x2000参数解释BOARDstm32f103指定目标板为 STM32F103。CONFIG_USB1启用 USB 支持。如果要使用串口则设置为CONFIG_SERIAL1。BOOTLOADER_SIZE0x2000指定 Bootloader 大小为 8KB十六进制 0x2000。实操心得第一次编译可能会失败提示缺少某些头文件如stm32f1xx.h或链接脚本。这是因为 Klipper 的 Bootloader 编译依赖于其主构建系统设置的环境变量和库路径。最稳妥的方式还是通过 Klipper 主目录的Makefile来编译 Bootloader。具体命令可能因 Klipper 版本略有差异建议查阅 Klipper 官方文档中关于 Bootloader 的章节。通常命令形如make bootloader BOARDgeneric-stm32f103。你可以通过make help查看所有可用的目标。4.2 通过 Klipper 主 Makefile 编译推荐经过对 Klipper 源码结构的分析编译特定 MCU 的 Bootloader 的标准流程是为 Bootloader 生成配置首先你需要为你的主板创建一个编译配置。这通常通过复制现有的配置文件并修改来实现。例如对于 STM32F103可以参考configs/stm32f103.config。cd ~/klipper cp configs/stm32f103.config .config然后你需要编辑这个.config文件确保其中包含了 Bootloader 的配置选项。一个更简单的方法是使用menuconfigmake menuconfig在menuconfig界面中选择Micro-controller Architecture为STMicroelectronics STM32。选择Processor model为STM32F103。在Bootloader offset中不要设置任何偏移量保持为空。这个偏移量是针对应用程序的Bootloader 自己编译时是从 0 开始。找到Build a bootloader选项并启用它。在Bootloader communication interface中选择USB (on PA11/PA12)。保存并退出。执行编译配置保存后直接运行make即可。Klipper 的构建系统会识别到需要编译 Bootloader并自动调用正确的流程。make clean make编译成功后你会在out目录下找到生成的 Bootloader 二进制文件名称可能类似于bootloader.bin或klipper_bootloader.bin同时也会有bootloader.hex和bootloader.elf文件。关键点通过menuconfig设置Build a bootloader后make命令生成的就是 Bootloader 固件而不是普通的 Klipper 固件。当你需要编译主应用程序时需要回到menuconfig禁用Build a bootloader并正确设置Bootloader offset例如0x2000。4.3 分析生成的链接脚本与内存映射编译过程中构建系统会生成或使用一个链接脚本Linker Script通常是.ld文件。这个文件定义了代码和数据在内存中的布局。理解它对于调试至关重要。编译完成后在out目录下寻找.ld文件或者查看编译输出信息。对于 Bootloader其链接脚本会明确指定FLASH (rx)的起始地址是0x08000000长度为你设置的BOOTLOADER_SIZE如8K。RAM (xrw)的起始地址是0x20000000长度根据芯片定义如20K。你可以用文本编辑器打开这个.ld文件查看。例如它会包含类似下面的内容MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 8K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K }这证实了我们的 Bootloader 被严格限制在 Flash 的前 8KB 内。5. 刷写 Bootloader 到芯片多种方法详解生成了bootloader.bin或bootloader.hex文件后下一步就是把它烧录到芯片的 Flash 起始区域。由于此时芯片里还没有 Bootloader我们必须使用系统内置的 ROM Bootloader即通过 BOOT0 跳线或者 SWD/JTAG 编程器来完成第一次烧写。5.1 方法一使用 USB 转 TTL 和 stm32flash推荐给初学者这是最通用、成本最低的方法只需要一个常见的 CH340G/CP2102 USB 转 TTL 模块。接线方式非常重要TTL 模块的TX接 主板 MCU 的PA9(USART1_RX)。TTL 模块的RX接 主板 MCU 的PA10(USART1_TX)。TTL 模块的GND接 主板GND。不需要接 VCC主板应由外部供电如 USB 口或 3.3V。找到主板上的BOOT0引脚和BOOT1引脚如果有。将BOOT0通过跳线帽连接到3.3V或VCCBOOT1连接到GND如果存在且需要配置。对于 Blue Pill通常有一个标有 “BOOT0” 的跳线将其短接到 “1” 的位置即可。给主板上电插入 USB 线或接通电源。将 TTL 模块插入电脑 USB 口。操作步骤安装stm32flash工具sudo apt install stm32flash查看 TTL 模块在系统中的设备名通常是/dev/ttyUSB0或/dev/ttyACM0。ls /dev/ttyUSB*使用stm32flash刷写 Bootloader。假设设备是/dev/ttyUSB0Hex 文件是bootloader.hexstm32flash -w ~/klipper/out/bootloader.hex -v -g 0x0 /dev/ttyUSB0参数解释-w file写入指定的文件。-v验证写入内容。-g 0x0刷写完成后从地址 0 开始执行即启动我们刚刷入的 Bootloader。/dev/ttyUSB0你的串口设备。如果使用.bin文件需要指定起始地址stm32flash -w ~/klipper/out/bootloader.bin -S 0x08000000 -v -g 0x0 /dev/ttyUSB0-S addr指定.bin文件烧录的起始地址必须是0x08000000。刷写成功后先断开主板电源将 BOOT0 跳线帽恢复至正常位置连接到 GND 或 “0” 的位置然后再重新上电。此时芯片应该运行我们刚刷入的自定义 Bootloader了。5.2 方法二使用 ST-LINK V2 编程器更稳定可靠如果你有一个 ST-LINK V2或 V3调试编程器过程会更简单稳定且不需要操作 BOOT0 跳线。接线方式ST-LINK 的SWDIO、SWCLK、GND、3.3V分别连接到主板的对应引脚。Blue Pill 上通常有清晰的标记SWDIO-PA13, SWCLK-PA14。操作步骤安装 OpenOCD开源片上调试工具sudo apt install openocd编写一个简单的 OpenOCD 配置文件例如flash_bootloader.cfgsource [find interface/stlink-v2.cfg] source [find target/stm32f1x.cfg] init reset halt flash write_image erase /home/pi/klipper/out/bootloader.hex reset run exit运行 OpenOCD 进行刷写openocd -f flash_bootloader.cfg刷写会自动完成包括擦除、编程、校验。完成后芯片会自动复位运行。注意事项使用 ST-LINK 刷写时确保连接可靠特别是 3.3V 供电要充足。如果主板已有其他电源可以只连接 GND、SWDIO、SWCLK 三根线使用目标板供电。5.3 方法三使用 DFU 模式如果原有 Bootloader 支持如果这块板子之前已经刷过某种支持 DFU 的 Bootloader包括我们即将刷的这个那么后续更新就可以通过 DFU 模式进行无需再动跳线。但第一次刷写通常还是要靠上述两种方法之一。6. 编译与刷写适配 Bootloader 的主应用程序Klipper固件Bootloader 就位后我们还需要编译一个“知道”自己应该住在哪里的 Klipper 固件。6.1 配置应用程序的 Flash 偏移量关键步骤来了我们必须告诉 Klipper 的编译系统“请不要从 Flash 的 0 地址开始放代码请从0x0800 2000开始”。再次运行make menuconfig。确保Build a bootloader选项是未选中状态。找到Bootloader offset选项将其设置为0x2000对应我们预留的 8KB Bootloader 空间。有些配置界面可能是8KiB bootloader这样的选项勾选即可效果相同。其他配置如通信接口、引脚定义等根据你的主板原理图进行设置。保存并退出。6.2 编译应用程序固件执行编译make clean make这次编译生成的klipper.bin或klipper.hex其内部的所有地址都已经自动偏移了0x2000。它的入口地址复位向量是0x0800 2004中断向量表也位于这个偏移之后的位置。6.3 通过自定义 Bootloader 刷写应用程序现在你可以通过我们刚刷进去的自定义 Bootloader 来更新这个应用程序固件了而无需再碰 BOOT0 跳线。对于 USB Bootloader确保主板通过 USB 连接到主机树莓派/电脑且 BOOT0 为低电平正常启动模式。主板首次上电运行 Bootloader 时如果没有检测到更新触发如按钮按下它会很快比如10ms内跳转到应用程序。如果应用程序区域是空的第一次刷或者应用程序运行了我们就需要触发 Bootloader 进入刷机模式。触发方式根据 Bootloader 的配置通常有两种GPIO 触发在 Bootloader 运行的短暂窗口期内上电后几毫秒将某个特定 GPIO如 PA0拉低接地。你需要按下一个连接在 PA0 和 GND 之间的按钮。串口触发在 Bootloader 运行的窗口期内向串口PA9/PA10发送一个特定字符如回车\n。这通常可以通过echo ‘\n’ /dev/ttyAMA0假设你连接了串口来实现。USB 触发对于 USB BootloaderKlipper 提供了flash_usb.py脚本它可以自动发送 DFU 命令使设备进入刷写模式。这是最方便的方式。使用 Klipper 提供的脚本刷写。在 Klipper 目录下找到你的 MCU 对应的make flash命令。对于配置了 USB Bootloader 的 STM32F103命令通常是make flash FLASH_DEVICE/dev/serial/by-id/usb-Klipper_stm32f103xxxxxx-if00你需要将FLASH_DEVICE替换为你的主板在系统中的实际 USB 设备路径。make flash命令会自动调用底层工具如dfu-util完成刷写。对于 USART Bootloader同样需要触发 Bootloader 进入模式GPIO 或串口指令。使用stm32flash工具但这次不需要设置 BOOT0 跳线且烧录地址是应用程序的起始地址stm32flash -w ~/klipper/out/klipper.bin -S 0x08002000 -v -g 0x0 /dev/ttyUSB0注意-S参数变成了0x08002000。刷写成功后Bootloader 会自动跳转到新的应用程序你的 Klipper 主机应该就能通过 USB 或串口连接到主板了。7. 调试与故障排除实录自定义 Bootloader 的过程很少一帆风顺以下是几个我踩过的坑和解决方案。7.1 Bootloader 刷写成功但无法启动应用程序现象通过 ST-LINK 刷好 Bootloader 后恢复 BOOT0 跳线重新上电主板毫无反应LED 不闪USB 无连接。排查思路检查复位引脚确保 NRST 引脚没有被意外拉低。用万用表测量 NRST 对地电压应为高电平接近 3.3V。检查时钟Bootloader 通常使用内部高速时钟HSI。确保没有在 Bootloader 里错误地配置了外部时钟HSE而又没接晶振。查看 Bootloader 源码的时钟初始化部分。验证跳转地址这是最常见的问题。使用 ST-LINK 和 OpenOCD 或 STM32CubeProgrammer 连接芯片读取内存内容。首先确认0x0800 2000应用程序起始地址和0x0800 2004复位向量地址的内容不是0xFFFFFFFF表示 Flash 为空。如果是说明应用程序没刷进去或刷错了位置。其次在 Bootloader 代码的跳转函数处设置断点单步调试查看它计算出的跳转地址是否正确应该是0x0800 2000和0x0800 2004。堆栈指针初始化在跳转前Bootloader 必须从应用程序的起始地址0x0800 2000加载初始堆栈指针MSP。如果这个地址的内容是无效的跳转后第一条指令就会发生硬件错误。确保应用程序的链接脚本正确且编译出的.bin文件开头 4 个字节是有效的堆栈顶地址通常指向 RAM 末端。解决方法仔细核对 Bootloader 和应用程序的链接脚本中的内存起始地址和长度。确保应用程序编译时指定的Bootloader offset与 Bootloader 实际占用大小完全一致。使用调试器进行单步跟踪是最有效的定位手段。7.2 USB Bootloader 无法被主机识别现象触发 Bootloader 进入刷机模式后电脑/树莓派没有出现新的 USB 设备如/dev/ttyACM0或lsusb里看不到 Klipper 相关的设备。排查思路USB 线缆与端口尝试更换 USB 线缆和电脑端口。有些线缆只能充电不能传输数据。USB 引脚配置STM32F103 的 USB 引脚是PA11 (DM)和PA12 (DP)。检查 Bootloader 代码中是否正确定义了这两个引脚为复用功能并且时钟已使能RCC_APB1ENR | RCC_APB1ENR_USBEN。上拉电阻USB D 线上需要一个 1.5kΩ 的上拉电阻到 3.3V以标识为全速设备。Blue Pill 板子上通常已经集成。如果没有需要外部添加。电源问题USB 供电可能不足。尝试给主板单独供电如通过 3.3V 或 5V 引脚同时连接 USB 的 D/D- 和 GND 进行通信。Bootloader 代码问题简化测试编写一个最简单的 USB CDC 回环测试程序不包含跳转逻辑直接烧录到芯片起始地址看 USB 是否能被识别。这可以隔离 Bootloader 跳转逻辑的影响。7.3 应用程序中串口/USB 通信异常现象应用程序Klipper刷入后主机无法通过配置的串口或 USB 与主板通信。排查思路中断向量表重映射这是最关键的一点对于 Cortex-M 芯片中断向量表默认位于0x0000 0000由芯片内存映射决定映射到 Flash 起始0x0800 0000。当 Bootloader 存在时应用程序的中断向量表位于0x0800 2000。但芯片发生中断时依然会去0x0000 0000找向量表。因此应用程序必须在启动早期在使能任何中断之前重新设置向量表偏移寄存器VTOR。对于 STM32通常在SystemInit()函数或主函数开头添加SCB-VTOR (uint32_t)0x08002000; // 根据你的偏移量修改幸运的是Klipper 固件已经很好地处理了这个问题。只要你在menuconfig中正确设置了Bootloader offsetKlipper 的构建系统会自动在链接脚本和启动代码中处理好 VTOR 的重映射。所以如果你用的是 Klipper这一步通常无需手动干预。外设时钟重复初始化冲突Bootloader 可能已经初始化了某些外设的时钟如 USART1、USB。应用程序在初始化时最好先检查外设是否已使能或者直接复用 Bootloader 的配置。更安全的做法是在 Bootloader 跳转前不要初始化应用程序需要用到的复杂外设只做最基本的系统时钟和 GPIO 初始化。把完整的初始化工作留给应用程序。引脚复用冲突检查 Bootloader 和应用程序是否配置了同一个引脚做不同功能。例如Bootloader 用 PA9/PA10 做 USART1 通信而应用程序也用它们做普通 GPIO 或其他功能就会冲突。应在 Bootloader 跳转前将不再使用的引脚恢复为高阻输入或默认状态。7.4 制作一个简单的“心跳”指示灯用于调试在调试 Bootloader 和应用程序的跳转时一个可视化的指示非常有帮助。我习惯在代码中添加一个简单的 LED 闪烁模式来区分不同阶段。在 Bootloader 中上电后让 LED 快速闪烁例如 100ms 间隔表示正在运行 Bootloader 并等待触发。进入刷写模式时让 LED 常亮或另一种闪烁模式。跳转到应用程序前短暂关闭 LED。在应用程序中使用另一种慢速闪烁模式例如 500ms 间隔表示应用程序正常运行。这样通过观察 LED 的行为你就能非常直观地知道代码执行到哪个阶段是卡在 Bootloader 了还是成功跳转到了应用程序。