STM32F103 Bootloader编译与烧录全攻略:从源码到3D打印机主板实战
2026/7/28 19:24:20 网站建设 项目流程

1. 项目缘起:为什么我们需要自定义 Bootloader?

如果你正在玩基于 Klipper 的 3D 打印机,尤其是像 Voron 这类 DIY 机型,那么对 STM32F103 这颗“神片”一定不陌生。它成本低廉、性能足够,是许多主板(如 SKR Mini E3、BTT 的诸多型号)和独立 MCU 板(如 Klipper 的专用“主机伴侣”)的核心。然而,当你拿到一块全新的、或者被刷成“砖”的 STM32F103 板子时,第一件事往往不是刷固件,而是先搞定 Bootloader。

Bootloader,中文常叫“启动程序”或“引导加载程序”,是芯片上电后运行的第一段代码。它的核心任务很简单:决定接下来要跳转到哪里去执行用户程序。对于 Klipper 来说,我们通常通过串口(UART)来给主板刷写固件,这就需要 Bootloader 提供一个“等待接收新固件”的窗口期。原厂芯片出厂时,要么没有 Bootloader(需要通过 SWD 接口直接烧录),要么自带的是通过 USB DFU 或系统存储器启动的 Bootloader,这对于我们常用的 3D 打印机主板串口刷机场景并不友好。

这就是为什么 Klipper 官方和社区推荐使用像stm32f103 bootloader这样的自定义 Bootloader。它体积小巧(通常 8KB),专为通过 UART 接收 Klipper 固件而设计,刷写成功后,主板一上电就会在串口等待几秒钟,如果检测到有效的刷机指令流,就进入刷机模式;否则就直接跳转到已有的 Klipper 固件运行。整个过程无需额外的硬件(如 USB 转 TTL 之外的工具),非常方便。

但问题来了:网上的教程往往告诉你“用这个 hex 文件”,或者“运行这个make flash命令”。一旦遇到不常见的板子(比如引脚定义不同),或者 Bootloader 本身需要更新,很多人就懵了。知其然,更要知其所以然。今天,我们就抛开现成的二进制文件,从零开始,手把手带你理解并编译一个属于你自己的 STM32F103 Bootloader。这不仅能让你在主板“变砖”时从容救砖,更能让你深入理解 Klipper 固件加载的底层机制。

2. 环境搭建:编译工具链与源码获取

编译 STM32 的程序,你需要一套 ARM 架构的交叉编译工具链。对于 Bootloader 这种相对底层的程序,我们通常使用 GNU Arm Embedded Toolchain。

2.1 安装 ARM GCC 工具链

在 Linux 系统(比如运行 Klipper 的树莓派)上安装最为方便。打开终端,执行以下命令:

sudo apt update sudo apt install gcc-arm-none-eabi

安装完成后,可以通过arm-none-eabi-gcc --version来验证。Windows 用户可以去 ARM 官网或 GNU MCU Eclipse 网站下载预编译的工具链,并设置好系统环境变量。

注意:确保安装的版本不要太旧。Bootloader 代码通常用 C 语言编写,对编译器版本有一定兼容性要求,太旧的编译器可能无法识别某些优化标志或语法。

2.2 获取 Bootloader 源代码

Klipper 社区维护了一个最常用的 STM32F103 Bootloader 项目,它源自 Marlin 的 Bootloader,并针对 Klipper 进行了优化。我们将以这个仓库为例。

打开终端,找一个合适的目录,克隆代码仓库:

git clone https://github.com/kevinOConnor/stm32f103_bootloader.git cd stm32f103_bootloader

这个仓库结构很清晰:

  • Makefile:编译的核心控制文件。
  • src/:存放所有 C 源文件和头文件。
  • lib/:可能包含一些底层库文件。
  • stm32f103.ld:链接器脚本,定义了程序在芯片内存中的布局,这是最关键的文件之一

进入目录后,先别急着编译。我们首先要理解一个核心概念:Bootloader 在芯片内存中的位置是固定的,它必须避开用户程序(即 Klipper 固件)的空间。对于 STM32F103,常见的配置是将 Bootloader 放在 Flash 的起始位置(0x08000000),大小通常为 8KB(0x2000)。这意味着用户程序的起始地址需要偏移 0x2000,即从 0x08002000 开始。这个偏移量必须在 Bootloader 和 Klipper 固件的编译配置中保持一致。

3. 核心配置详解:链接脚本与芯片选项

Bootloader 的编译行为几乎完全由Makefile和链接脚本stm32f103.ld控制。理解它们,你就能应对各种定制需求。

3.1 剖析链接脚本stm32f103.ld

用文本编辑器打开stm32f103.ld,你会看到类似下面的内容(不同版本可能有细微差别):

MEMORY { rom (rx) : ORIGIN = 0x08000000, LENGTH = 8K ram (rwx) : ORIGIN = 0x20000000, LENGTH = 20K } SECTIONS { .text : { *(.vectors) *(.text) *(.rodata) _etext = .; } > rom .data : { _sdata = .; *(.data) _edata = .; } > ram AT > rom .bss : { _sbss = .; *(.bss) _ebss = .; } > ram }

我们来拆解一下:

  • MEMORY部分:定义了芯片的物理内存布局。
    • rom:指 Flash 存储器,起始地址ORIGIN = 0x08000000,长度LENGTH = 8K。这明确规定了本 Bootloader 最大只能占用 8KB 空间。如果你想增大 Bootloader 功能(比如增加网络支持),可能需要修改这个值,但务必确保不会侵占后续 Klipper 固件的空间。
    • ram:指 SRAM,起始地址0x20000000,长度20K。STM32F103C8T6 只有 20KB RAM,这里定义准确。
  • SECTIONS部分:定义了如何将程序的不同部分放入上述内存。
    • .text:包含代码(.text)、只读数据(.rodata)和中断向量表(.vectors)。它被放入rom(Flash)中。_etext是一个符号,标记了代码段的结束地址。
    • .data:已初始化的全局变量和静态变量。这里有个关键点:> ram AT > rom。意思是.data段在运行时位于 RAM(> ram),但其初始值存储在 Flash 中(AT > rom)。Bootloader 启动时需要自己将这部分数据从 Flash 拷贝到 RAM。_sdata_edata标记了其在 RAM 中的起止地址。
    • .bss:未初始化的全局变量和静态变量,运行时在 RAM 中,启动时需要将其清零。_sbss_ebss标记了其起止地址。

这个脚本是 Bootloader 能在芯片上正确运行的“地图”。编译时,链接器会根据这个地图,将代码和数据安排到指定位置。

3.2 Makefile 中的关键参数

打开Makefile,重点关注以下几个变量:

MCU = cortex-m3 CFLAGS = -Os -g -Wall -Wstrict-prototypes -I. -mthumb -mcpu=$(MCU) LDFLAGS = -nostartfiles -Tstm32f103.ld -Wl,-Map=out/debug.map -Wl,--cref
  • MCU = cortex-m3:指定了芯片内核为 Cortex-M3,STM32F103 正是此内核。
  • CFLAGS:编译选项。
    • -Os:优化代码尺寸,这对空间紧张的 Bootloader 至关重要。
    • -mthumb -mcpu=$(MCU):指定生成 Thumb 指令集的代码,针对 Cortex-M3 优化。
  • LDFLAGS:链接选项。
    • -nostartfiles:不使用标准库的启动文件,因为我们有自己的启动代码(通常在src/下的startup.c或类似文件中)。
    • -Tstm32f103.ld:指定使用我们刚才分析的链接脚本。

有时,你可能需要为不同的硬件修改编译选项。例如,如果你的板子使用外部高速晶振(HSE),可能需要定义宏HSE_VALUE来指定晶振频率(通常是 8000000 或 12000000),这通常在MakefileCFLAGS中添加-DHSE_VALUE=8000000

4. 编译流程与产物分析

环境配置和代码理解清楚后,编译就是一行命令的事。在项目根目录下执行:

make

如果一切顺利,你会在out/目录下看到几个关键文件:

  • out/bootloader.bin二进制文件。这是我们最终要烧录到芯片 Flash 起始地址的文件。它是纯二进制数据,不包含地址信息,需要通过烧录工具指定烧录地址(0x08000000)。
  • out/bootloader.hexIntel HEX 格式文件。同样包含程序数据,但格式中自带了地址信息,一些烧录工具(如 STM32CubeProgrammer)使用它会更方便。
  • out/bootloader.elfELF 格式文件。包含完整的调试信息、符号表等,用于调试,文件体积最大。
  • out/debug.map链接映射文件。这个文件非常有用,它详细列出了每个函数、变量被链接到了哪个地址,占用了多少空间。当你怀疑 Bootloader 太大导致溢出时,查看这个文件就能定位是哪个模块占用了过多空间。

编译成功后,建议先用arm-none-eabi-size out/bootloader.elf命令查看一下生成程序的大小:

text data bss dec hex filename 3024 20 1572 4616 1208 out/bootloader.elf
  • text:代码段大小,3024 字节,约 3KB。
  • data:已初始化数据段大小,20 字节。
  • bss:未初始化数据段大小,1572 字节。
  • dec:总计约 4.6KB。

这远小于我们为 Bootloader 预留的 8KB Flash,说明空间绰绰有余。这个检查习惯能帮你提前避免“程序太大烧不进去”的问题。

5. 烧录 Bootloader 的多种方法与实践

编译出.bin.hex文件后,下一步就是将其烧录到芯片的 Flash 中。根据你的主板状态,有以下几种常见场景和方法。

5.1 场景一:全新或已“变砖”的芯片(无 Bootloader)

这种情况下,芯片的 Flash 是空的或者原有的程序无法运行,无法通过串口进行通信。你必须使用SWD(Serial Wire Debug)接口进行烧录。这是最底层、最可靠的烧录方式。

所需工具:

  1. SWD 调试器/编程器:最常见的是 ST-Link V2(便宜且兼容性好),或者 J-Link(功能更强大)。
  2. 连接线:需要连接调试器的 SWDIO、SWCLK、GND,有时还需要连接NRST(复位)和3.3V(供电)线。具体连接方式取决于你的主板。
  3. 烧录软件
    • OpenOCD:开源命令行工具,功能强大,在 Linux 上尤其方便。
    • STM32CubeProgrammer:ST 官方图形化工具,跨平台。
    • pyOCD:基于 Python 的烧录工具。

以 OpenOCD 命令行烧录为例:首先,确保你的 ST-Link 已连接主板和电脑。然后编写一个简单的 OpenOCD 配置文件,比如stlink.cfg

source [find interface/stlink.cfg] transport select hla_swd source [find target/stm32f1x.cfg]

接着,在终端中执行:

openocd -f stlink.cfg -c "program out/bootloader.bin 0x08000000 verify reset exit"

这条命令会:

  1. 连接 ST-Link 和 STM32F1x 目标。
  2. bootloader.bin文件烧录到地址0x08000000
  3. 进行校验(verify)。
  4. 复位芯片(reset)并退出(exit)。

烧录成功后,芯片复位,新的 Bootloader 就开始运行了。此时,你可以尝试通过串口连接,看看是否能在上电后的几秒内收到 Bootloader 的提示信息(比如CCC字符)。

5.2 场景二:芯片已有可用的 Bootloader(如 USB DFU)

有些主板出厂时可能预刷了通过 USB 进入 DFU(Device Firmware Upgrade)模式的 Bootloader。你可以利用这个现有的 Bootloader 来更新成我们编译的 UART Bootloader。

操作方法:

  1. 将主板通过 USB 连接到电脑。
  2. 让主板进入 DFU 模式。方法因板而异:可能是按住某个按钮再上电,也可能是通过发送特定命令。
  3. 使用dfu-util(Linux/macOS)或 STM32CubeProgrammer 的 DFU 模式,将bootloader.bin烧录到0x08000000地址。

使用 dfu-util 的命令示例:

sudo dfu-util -a 0 -s 0x08000000:leave -D out/bootloader.bin

参数解释:

  • -a 0:指定 DFU 交替设置(alternate setting),通常为 0。
  • -s 0x08000000:leave:指定烧录起始地址为0x08000000:leave表示烧录完成后让设备退出 DFU 模式并执行程序。
  • -D:指定要下载的文件。

5.3 场景三:芯片已有同类型 UART Bootloader(升级 Bootloader)

这是最理想的情况。如果你的主板已经运行着一个可以接收串口命令的 Bootloader(比如旧版本的stm32f103 bootloader),你可以直接通过串口工具向其发送命令,让它自己擦除并写入新的 Bootloader 数据。

Klipper 的scripts/flash_usb.py脚本或其底层工具bossac(针对 SAMD)或stm32flash(针对 STM32)就具备这个能力。但对于 Bootloader 自身的升级,需要格外小心,因为一旦过程中断电或出错,芯片就会“变砖”,必须回到场景一用 SWD 救砖。

重要警告:通过 Bootloader 自身升级 Bootloader 是高风险操作!除非有明确文档支持且你清楚每一步在做什么,否则不建议新手尝试。更安全的做法是,将 Bootloader 视为固件基础设施的一部分,刷好一次后就不再变动。需要升级时,宁愿使用 SWD 重新烧录。

6. 与 Klipper 固件的协同工作与地址对齐

Bootloader 刷好了,不代表万事大吉。最关键的一步是确保 Klipper 固件知道 Bootloader 的存在,并把自己安装到正确的位置。

6.1 配置 Klipper 编译时的 Flash 偏移

当你为带有 8KB Bootloader 的主板编译 Klipper 固件时,必须make menuconfig中指定正确的偏移量。

在 Klipper 源码目录中执行make menuconfig,进入微控制器架构选择STMicroelectronics STM32,然后选择具体的芯片型号(如STM32F103)。在接下来的配置页面中,找到类似“Bootloader offset”的选项。对于 8KB 的 Bootloader,这个值应该设置为8KiB或者0x2000(十六进制)。

这个配置会做两件事:

  1. 修改 Klipper 固件自身的链接脚本,使其代码从0x08002000开始存放,完美避开 Bootloader 的0x080000000x08001FFF区域。
  2. 在生成的固件二进制文件头部,可能包含一个指向0x08002000的跳转向量,确保芯片复位后,如果 Bootloader 决定跳转,能准确地跳到用户程序的入口。

6.2 上电启动流程详解

让我们梳理一下完整的启动时间线:

  1. 上电/复位:芯片硬件强制从0x08000000(Flash 起始地址)开始执行指令。这里存放的是 Bootloader 的中断向量表,其中第一个字是初始栈指针(SP),第二个字是复位向量(Reset_Handler 函数的地址)。芯片跳转到Reset_Handler
  2. Bootloader 初始化:Bootloader 代码开始执行。它会初始化最基本的系统时钟(可能使用内部 RC 振荡器 HSI 以加快启动)、GPIO(特别是串口引脚)和 UART。
  3. 等待窗口期:Bootloader 通过串口发送一个特定字符(例如CCC)并开始计时,进入一个持续数秒(通常是 1-3 秒)的等待状态,监听是否有来自主机的刷机命令(例如 Klipper 的make flash命令通过串口发送的特殊指令流)。
  4. 决策与跳转
    • 如果收到有效刷机命令:Bootloader 进入刷机模式,通过串口接收新的用户程序数据,将其写入 Flash 中从0x08002000开始的区域。完成后,通常会执行一次软复位,重新从步骤1开始。
    • 如果超时未收到命令:Bootloader 认为用户不想刷机,于是执行跳转。它从0x08002000地址读取用户程序的中断向量表,找到用户程序的复位向量,然后使用汇编指令(如bx)跳转到那个地址。此后,控制权完全交给 Klipper 固件。
  5. Klipper 运行:Klipper 固件开始执行,初始化所有外设(步进电机驱动、温度传感器、加热棒等),并等待来自 Klipper 主机(树莓派)的指令。

这个过程清晰说明了 Bootloader 和应用程序如何“无缝”衔接。地址对齐是这一切能正常工作的基石。

7. 常见问题排查与实战心得

即使按照教程操作,你也可能会遇到一些问题。这里分享一些我踩过的坑和排查思路。

7.1 问题:编译成功,但烧录后串口无任何输出

排查步骤:

  1. 检查硬件连接:这是最容易被忽略的。确认 USB 转 TTL 工具的 TX、RX 线与主板是否正确交叉连接(工具的 TX 接主板的 RX,工具的 RX 接主板的 TX),GND 是否共地。确认波特率是否匹配(Bootloader 常用 115200 或 250000)。
  2. 检查 Bootloader 串口引脚配置:打开src/目录下的源码(如serial.cconfig.h),查看USART1USART2具体映射到哪两个 GPIO 引脚(例如 PA9/PA10 或 PA2/PA3)。你的主板设计可能使用了不同的引脚。这是自定义 Bootloader 最常见的原因。你需要根据原理图修改源码中的引脚宏定义,并重新编译。
  3. 检查时钟源配置:Bootloader 为了快速启动,通常使用内部高速时钟(HSI,8MHz)。但有些主板设计依赖外部晶振(HSE)。如果 Bootloader 代码默认使用 HSI,而你的主板外部晶振电路有问题,或者 Bootloader 配置错误地尝试使用 HSE 但失败了,会导致系统时钟无法启动,串口自然无法工作。检查system_stm32f1xx.c或类似时钟初始化文件中的相关宏定义。
  4. 使用调试器验证:如果条件允许,用 ST-Link 通过 SWD 接口连接芯片,用 GDB 进行单步调试。看程序是否能运行到串口初始化的位置。这是最直接的排查手段。

7.2 问题:能进入 Bootloader 模式,但刷写 Klipper 固件失败

排查步骤:

  1. 确认 Flash 偏移量:再次核对 Klipper 固件编译时设置的Bootloader offset是否与 Bootloader 实际占用的大小一致(8KB Bootloader 对应 0x2000 偏移)。不一致会导致 Bootloader 试图将固件写到错误的位置,可能覆盖自身或写到了未定义的 Flash 区域。
  2. 检查 Flash 擦写函数:Bootloader 中的 Flash 编程和擦除函数是针对 STM32F103 特定 Flash 页大小(通常 1KB 或 2KB)编写的。如果函数有 bug,或者解锁 Flash(FLASH_Unlock)和加锁(FLASH_Lock)的流程不对,会导致写入失败。可以尝试在 Bootloader 代码中增加一些调试输出,打印 Flash 操作的状态寄存器(FLASH->SR)值。
  3. 电源稳定性:在刷写过程中,尤其是通过 USB 供电时,电压波动可能导致 Flash 写入错误。尝试使用更稳定的电源为板子供电。
  4. 降低波特率:尝试在刷机时使用更低的波特率(如 115200 而不是 250000),高波特率在长线或干扰环境下更容易出错。

7.3 实战心得:如何为一块新板子定制 Bootloader

假设你拿到一块全新的 STM32F103 板子,需要为其适配 Bootloader,可以遵循以下流程:

  1. 获取原理图:找到板子的原理图 PDF,这是所有工作的基础。
  2. 确定串口引脚:找到计划用于刷机的 UART 引脚(通常是 USART1)。记下其对应的 GPIO,例如PA9(TX) 和PA10(RX)。
  3. 修改源码:在 Bootloader 源码的配置头文件(如config.h)中,找到UARTx_TX_PINUARTx_RX_PIN的定义,将其修改为你的 GPIO 编号。同时,检查对应的时钟使能宏(如__HAL_RCC_USART1_CLK_ENABLE__HAL_RCC_GPIOA_CLK_ENABLE)是否正确。
  4. 确定启动模式引脚:STM32 的启动模式由BOOT0BOOT1引脚决定。大多数 3D 打印机主板将其硬件拉低,设置为从主 Flash 启动(即运行我们的 Bootloader)。确保你的原理图中这两个引脚状态正确,通常BOOT0通过电阻接地。
  5. 编译与测试:修改后编译,通过 SWD 烧录到板子上。用串口工具监听,看上电瞬间是否有输出。如果没有,返回步骤 2 和 3 仔细检查,并用调试器辅助。
  6. 验证跳转功能:先烧录一个已知正确的、设置了正确偏移量的 Klipper 固件。然后给板子上电,不进行任何串口操作,看 Bootloader 超时后是否能成功跳转并启动 Klipper(表现为加热头开始回温、电机上电等)。

这个过程需要耐心和细致的调试,但成功一次后,你对整个系统的理解会深刻得多。自定义 Bootloader 不仅仅是刷一个文件,更是你完全掌控硬件的第一步。当你能够根据原理图修改并编译出适配特定硬件的 Bootloader 时,就意味着你不再受限于预编译的二进制文件,真正具备了修复和改造底层固件的能力。这种能力在调试复杂的硬件问题或进行深度定制时,是无价的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询