1. 最小Bootloader
任务目标
在STM32F103开发板上实现一个最小Bootloader:上电后从Flash启动,完成必要的硬件初始化(时钟、GPIO),最终点亮板载LED,以此验证芯片复位后能够正确执行Flash中的启动代码。
硬件与依赖
- 硬件:STM32F103C8T6最小系统板(蓝色Pill),板载LED接PC13(低电平点亮)
- 工具链:arm-none-eabi-gcc + OpenOCD + ST-Link(或使用Makefile + arm-none-eabi-gcc)
- 无外部库依赖,直接操作寄存器
核心实现
Bootloader的本质是芯片复位后执行的第一段代码。STM32从0x08000000(Flash起始地址)读取初始栈指针和复位向量,随后跳转到复位处理函数。因此最小Bootloader只需:定义向量表、编写复位函数完成时钟与GPIO初始化、点亮LED并进入死循环。
// main.c - 最小Bootloader:点亮LED
#include <stdint.h>
// STM32F103 寄存器地址
#define RCC_BASE 0x40021000
#define GPIOC_BASE 0x40011000
#define RCC_APB2ENR (*(volatile uint32_t *)(RCC_BASE + 0x18))
#define GPIOC_CRH (*(volatile uint32_t *)(GPIOC_BASE + 0x04))
#define GPIOC_ODR (*(volatile uint32_t *)(GPIOC_BASE + 0x0C))
// 向量表:栈顶地址 + 复位向量(必须位于0x08000000起始处)
// 栈顶指向RAM末尾(STM32F103C8 有20KB RAM,即0x20005000)
__attribute__((section(".isr_vector")))
const uint32_t vector_table[] = {
0x20005000, // 初始栈指针
(uint32_t)_reset // 复位向量
};
void _reset(void) __attribute__((naked, section(".text._reset")));
void _reset(void) {
// 1. 使能GPIOC时钟(APB2总线,位4)
RCC_APB2ENR |= (1 << 4);
// 2. 配置PC13为推挽输出,最大速度2MHz
// CRH寄存器控制PC13(对应位[23:20]),设置CNF13=00(推挽)、MODE13=10(2MHz)
GPIOC_CRH &= ~(0xF << 20); // 先清零
GPIOC_CRH |= (0x2 << 20); // MODE13=10, CNF13=00
// 3. 点亮LED(PC13输出低电平)
GPIOC_ODR &= ~(1 << 13);
// 4. 死循环,保持LED点亮
while(1);
}
链接脚本(linker.ld)
/* 最小链接脚本:将向量表放在Flash起始地址 */
ENTRY(_reset)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector))
} > FLASH
.text :
{
*(.text*)
} > FLASH
}
编译与烧录
# 编译命令
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb main.c -o main.o
arm-none-eabi-ld -T linker.ld main.o -o bootloader.elf
arm-none-eabi-objcopy -O binary bootloader.elf bootloader.bin
# 烧录(使用ST-Link)
st-flash write bootloader.bin 0x08000000
验证方法
- 烧录完成后,按一下开发板上的复位键(NRST)
- 通过条件:板载LED(PC13)点亮并保持常亮
- 若LED不亮,检查:
- 烧录地址是否为0x08000000
- 向量表首字是否为0x20005000(栈顶地址)
- PC13是否被其他跳线占用
此Bootloader验证了芯片上电后从Flash起始地址正确取指、加载栈指针并执行复位函数的完整启动链路。后续案例可在此基础上增加外设初始化、跳转至应用程序等功能。