STM32 BootLoader 学习笔记

从单片机组成与启动流程,到跳转代码、蓝牙远程 IAP 固件升级与 CRC32 校验:STM32 BootLoader 原理与实践全记录。

BootLoader 学习

硬件:

  • stm32f407(嘉立创天空星)
  • HC-05蓝牙模块 软件:
  • 暂时使用python简陋演示功能

为什么要有bootloader

  • 类似于电脑的boot,可以让我们很方便的去重装系统,可以用u盘重装系统,可以在线重装系统等
  • 可以把一些固定参数放到bootloader里,比如屏幕亮度,初始参数等等,可以专注于APP的开发

前置知识

单片机组成

电脑组成 stm32内部,与电脑组成类似,主要强调:

  • 片内Flash:相当于电脑硬盘
  • 嵌入式 SRAM:相当与电脑内存条

你需要知道:

  • 嵌入式SRAM是被映射在$0x2000 0000$上的
  • 嵌入式flash是被映射在$0x0800 0000$

stm32启动流程

STM32微控制器从上电或复位到执行你编写的 main() 函数 之前,会经历一系列由硬件和启动代码(通常是汇编语言编写的)完成的初始化步骤

下面是 STM32 从上电到进入 main() 函数的详细过程:

一、硬件复位与启动模式选择

1. 上电复位 (POR) 或硬件复位

当STM32芯片上电或者复位引脚(NRST)被置为低电平并释放时,微控制器会进入复位状态。在复位期间,所有内部寄存器会被设置为预设的初始值,确保系统处于一个确定的、已知的状态。

2. 选择启动模式 (BOOT Pins)

CPU内核在复位结束后,首先会采样特定的启动引脚(如 BOOT0 和 BOOT1,具体名称和数量可能因型号而异)。这些引脚的状态决定了微控制器从哪个存储器空间开始执行代码:

  • 主闪存启动 (Main Flash Memory):最常用的模式,运行用户编写的程序。
  • 系统存储器启动 (System Memory):运行内置的 Bootloader,用于通过串口、USB等接口烧录程序。
  • SRAM 启动 (Embedded SRAM):运行驻留在SRAM中的代码,不常用。 通常,在运行用户程序时,会配置为从主闪存启动。

二、Cortex-M 内核的初始寻址

STM32 的核心是 Cortex-M 系列内核(如 M0、M3、M4、M7),它们遵循一个标准化的启动流程,这部分是由硬件自动完成的。

3. 读取中断向量表

微控制器根据启动模式,确定中断向量表的起始地址(在主闪存启动模式下,通常是 0x0800 0000)。中断向量表是存储了各种异常和中断服务程序入口地址的表格。

4. 初始化栈指针 (SP)

CPU 从向量表的第一个 32 位字(地址 Boot_Address + 0x00)中读取初始的 主栈指针 (MSP) 的值。这是程序中栈区的最高地址,用于存储局部变量、函数调用返回地址等。

5. 设置程序计数器 (PC)

CPU 从向量表的第二个 32 位字(地址 Boot_Address + 0x04)中读取 复位处理程序(Reset_Handler)的地址,并将这个地址加载到 程序计数器 (PC) 中。 至此,硬件初始化完成,程序控制权正式移交给 Reset_Handler。


三、执行启动文件代码 (Reset_Handler)

Reset_Handler 是由编译器和芯片厂商提供的一个汇编语言函数,通常位于项目中的 startup_stm32xxx.s 文件(即启动文件)中。它的任务是为C/C++程序的运行准备好环境,包括内存、时钟和库函数等。

6. 调用 SystemInit()

Reset_Handler 做的第一件大事是调用 SystemInit() 函数(通常在 system_stm32xxx.c 文件中)。该函数主要完成:

  • 配置系统时钟:设置主时钟源、PLL 倍频等,将系统时钟配置到所需的工作频率(但此时还不涉及用户外设时钟)。
  • 配置存储器:如设置 Flash 访问等待周期等。
7. 初始化 RAM 中的数据段
  • 初始化 .data 段:.data 段存储了已初始化的全局变量和静态变量。这些变量的初始值存储在 Flash 中,Reset_Handler 会将这些值从 Flash 复制到 RAM 中。
  • 初始化 .bss 段:.bss 段存储了未初始化的全局变量和静态变量。C 语言标准要求它们在程序开始时必须被清零。Reset_Handler 会将 .bss 段在 RAM 中的区域清零。
8. 调用 C 库初始化函数

对于标准 C/C++ 环境,Reset_Handler 还会调用 C 库的初始化函数(例如 __libc_init_array),用于:

  • 初始化 C 运行时环境。
  • 调用 C++ 程序的静态构造函数(如果你的代码中有使用)。
9. 跳转到 main()

在完成所有必要的硬件和软件环境准备后,Reset_Handler 的最后一步就是执行一条跳转指令,调用用户编写的 main() 函数。 代码段

; 假设这是 Reset_Handler 的最后几行伪汇编代码
; ...
BL      SystemInit          ; 调用系统时钟初始化函数
BL      __libc_init_array   ; 调用 C 库初始化函数
LDR     R0, =main           ; 将 main 函数地址加载到 R0
BX      R0                  ; 跳转到 main 函数执行

至此,STM32 终于进入了你的应用程序,开始执行用户代码。


总结流程图

  • 上电后检查boot引脚的高低决定从哪启动(在哪寻找代码)
  • 读取代码(编译后的2进制文件)的第一个字(主堆栈的地址),(工厂的工作地点)
  • 寻找第二个字以运行到main();
步骤执行者动作描述目标
1硬件/CPU上电复位 (POR)强制所有硬件到初始状态。
2硬件/CPU检查 BOOT 引脚确定代码的启动位置(如主 Flash)。
3硬件/CPU读取向量表确定 SP 和 PC 的初始值。
4硬件/CPU设置初始 SP/PC初始化主栈指针,跳转到 Reset_Handler。
5启动文件调用 SystemInit()配置系统时钟和 Flash 等基础硬件。
6启动文件初始化数据段从 Flash 复制 .data 到 RAM,将 .bss 段清零。
7启动文件调用库初始化初始化 C 运行时环境和静态构造函数。
8启动文件调用 main()进入用户应用程序的起点。

问题:

Question1为什么启动后需要先找到中断向量表呢,很重要吗?

Question2:堆栈是用来干嘛的,为什么要先初始化它,不初始化会怎么样

Question3:为什么会映射成0x08000000呢

简单跳转代码

使用O3优化可以减小boot大小 fromelf –bin -o “$L@L.bin” “#L

Boot

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
typedef void (*pFunction)(void);

void JumpToApplication(void) {
    // 1. 检查应用程序地址是否有效
    // 应用程序的栈顶地址位于其起始地址的第一个字
    if (((*(__IO uint32_t*)APP_ADDRESS) & 0x2FFE0000) == 0x20000000) {
        // 2. 将中断向量表重定向到应用程序地址
        SCB->VTOR = APP_ADDRESS;

        // 3. 获取应用程序的栈顶地址和复位处理函数地址
        pFunction app_reset_handler = (pFunction)(*(__IO uint32_t*)(APP_ADDRESS + 4));
        __set_MSP(*(__IO uint32_t*)APP_ADDRESS);

        // 4. 跳转到应用程序
        app_reset_handler();
    }
}

APP

随便,但是:

问题

question1 SCB是什么:

远程下载讲解

注意事项:HC-05的VCC接3V3,实测5V无法使用 蓝牙模块只有在通信状态才会显示已连接蓝牙,打不开蓝牙串口参考:HC05和电脑蓝牙通讯 HC05-python-stm32的波特率必须都设置为同样的速率,我这里为9600

蓝牙模块讲解

总结:当成远程串口用就行

常用AT指令

波特率为38400

指令返回意义
ATok检测固件是否可用
AT+RESETok模块软件复位
AT+VERSION?+VERSION:<Param> OK获取固件版本
AT+NAME=nameok设置模块名字
AT+NAME?+NAME:<Param> OK返回模块名字
AT+PSWD=“passwd”OK设置模块密码
AT+PSWD?+PSWD:<Param> OK获取模块密码
AT+UART=115200,0,0ok设置通信时的波特率
AT+UART?+UART:<Param1>,<Param2>,<Param3>
OK
获取串口参数

模块指示灯标志

  • 定时闪两下:已经连接蓝牙,处于通信状态
  • 快闪:未连接
  • 慢闪:工作模式,可使用AT指令调整参数(进入:保持按键按下,通电后松开按键即可)

测试流程

  1. 先进入工作模式,使用AT检查是否可用,
  2. 设置模块名字:AT+NAME=MyBluetooth
  3. 获取密码:AT+PSWD?
  4. 查看波特率:AT+UART?

嵌入式Flash操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void Flash_Erase_App(void)
{
    FLASH_EraseInitTypeDef EraseInit;
    uint32_t PageError;
    // 解除Flash保护
    HAL_FLASH_Unlock();

    // F407: sector2(扇区2) = 0x08008000 开始
    EraseInit.TypeErase    = FLASH_TYPEERASE_SECTORS;
    EraseInit.Sector       = FLASH_SECTOR_2;
    EraseInit.NbSectors    = 6;  // 到 sector7(扇区7)
    EraseInit.VoltageRange = FLASH_VOLTAGE_RANGE_3;

    if (HAL_FLASHEx_Erase(&EraseInit, &PageError) != HAL_OK) {
        // 错误处理
    }
    // 开启保护
    HAL_FLASH_Lock();
}

HAL_StatusTypeDef Flash_Write(uint32_t addr, uint8_t *data, uint32_t len)
{
    HAL_StatusTypeDef status;
    HAL_FLASH_Unlock();

    for(uint32_t i=0; i<len; i+=4) {
        uint32_t word = *(uint32_t*)(data+i);
        status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr+i, word);
        if(status != HAL_OK) break;
    }

    HAL_FLASH_Lock();
    return status;
}

主要逻辑

固件发送与接收

Bootloader代码逻辑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void Bootloader_Update(void)
{
    uint8_t cmd;
    uint8_t buf[256];
    uint32_t addr = APP_ADDR;

    Flash_Erase_App();

    while(1)
    {
        HAL_UART_Receive(&huart1, &cmd, 1, HAL_MAX_DELAY);

        if(cmd == CMD_DATA) {
            uint16_t len;
            HAL_UART_Receive(&huart1, (uint8_t*)&len, 2, HAL_MAX_DELAY);
            HAL_UART_Receive(&huart1, buf, len, HAL_MAX_DELAY);

            Flash_Write(addr, buf, len);
            addr += len;

            HAL_UART_Transmit(&huart1, (uint8_t*)"OK", 2, HAL_MAX_DELAY);
        }
        else if(cmd == CMD_END) {
            HAL_UART_Transmit(&huart1, (uint8_t*)"DONE", 4, HAL_MAX_DELAY);
            break;
        }
    }
}

电脑_python–发送

python要先安装pyserial跑:pip install pyserial 固件采用分批的形式发送,使用python脚本进行发送 ser = serial.Serial("COM4", 9600)这里的COM4需要改成你的com端口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import serial
import struct
# 初始化一个串口对象
ser = serial.Serial("COM4", 9600)

def send_data(data):
    ser.write(b"\x02")  # CMD_DATA
    # 把当前数据块的长度打包成两个字节的无符号整数(小端序)。
    # 比如 len(data) = 256,那么结果是 b'\x00\x01',开发板端可以据此知道这包数据有多长。
    ser.write(struct.pack("<H", len(data)))
    ser.write(data)
    # 阻塞读取开发板发送的信息,确保是否传递成功
    print(ser.read(2))  # OK

with open("app.bin", "rb") as f:
    while True:
        # 每次循环读取256个数据
        chunk = f.read(256)
        if not chunk:
            break
        send_data(chunk)

ser.write(b"\x03")  # CMD_END
print(ser.read(4))  # DONE

stm32_蓝牙–接收

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#define APP_ADDR 0x08008000
#define CMD_START 0x01
#define CMD_DATA  0x02
#define CMD_END   0x03
void Bootloader_Update(void)
{
    uint8_t cmd;
    uint8_t buf[256];
    uint32_t addr = APP_ADDR;
    // 擦除老APP,准备迎接新王
    Flash_Erase_App();

    while(1)
    {
        // 堵塞接收,根据先接收到的命令决定怎么做
        HAL_UART_Receive(&huart1, &cmd, 1, HAL_MAX_DELAY);
        // 数据命令,表示接收到的是数据
        if(cmd == CMD_DATA) {
            uint16_t len;
            // 获取长度
            HAL_UART_Receive(&huart1, (uint8_t*)&len, 2, HAL_MAX_DELAY);
            // 获取内容
            HAL_UART_Receive(&huart1, buf, len, HAL_MAX_DELAY);
            // 写入内容
            Flash_Write(addr, buf, len);
            // 位移地址,准备写入后续内容
            addr += len;
            // 回应OK
            HAL_UART_Transmit(&huart1, (uint8_t*)"OK", 2, HAL_MAX_DELAY);
        }
        // 结束命令,表示接收完成,退出该函数
        else if(cmd == CMD_END) {
            HAL_UART_Transmit(&huart1, (uint8_t*)"DONE", 4, HAL_MAX_DELAY);
            break;
        }
    }
}

增加校验-CRC32

一、CRC32 是什么

CRC 全称 Cyclic Redundancy Check(循环冗余校验)。
CRC32 表示它使用的是 32位的多项式运算结果。 它不是“加法求和”那种简单校验,而是用一种二进制多项式除法的思想:

  • 把整个数据看作一个很长的二进制数;
  • 用一个固定的“生成多项式”去做模2除法;
  • 最后得到一个 32 位的余数;
  • 这个余数就是 CRC 校验码。

🧠 模2除法:相当于普通除法,但所有加减都用“异或(XOR)”。

二、CRC32 的作用

CRC32 不是用来“防黑客”或“加密”的,而是为了 检测传输或存储过程中的错误。 例如:

  • 你通过串口发送 100KB 的固件;
  • 中途某一字节丢了、翻转了;
  • STM32 收到的文件计算出的 CRC 不等于原始文件的 CRC;
  • → 说明传输错误,不执行更新。 可以检测出几乎所有单比特错误、突发错误,非常可靠。

电脑端-python增加校验

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import serial
import struct
import binascii
from tqdm import tqdm   # pip install tqdm

# 打开串口
ser = serial.Serial("COM4", 9600, timeout=2)

def send_data(data):
    # 发送数据包
    ser.write(b"\x02")                       # CMD_DATA
    ser.write(struct.pack("<H", len(data)))  # 长度
    ser.write(data)                          # 数据本体
    resp = ser.read(2)                       # Bootloader 回复 OK
    if resp != b"OK":
        raise Exception("Bootloader写入失败,返回: %s" % resp)

def send_end(crc32, total_size):
    # 通知 Bootloader 固件结束 + CRC
    ser.write(b"\x03")                       # CMD_END
    ser.write(struct.pack("<I", crc32))      # 4字节 CRC32
    ser.write(struct.pack("<I", total_size)) # 固件总长度(可选)
    resp = ser.read(4)
    if resp != b"DONE":
        raise Exception("Bootloader校验失败,返回: %s" % resp)

# ---------------- 主流程 -----------------
with open("app.bin", "rb") as f:
    data = f.read()

# 计算 CRC32(和 Bootloader 保持一致)
crc32 = binascii.crc32(data) & 0xFFFFFFFF
total_size = len(data)
print(f"固件大小: {total_size} 字节, CRC32 = 0x{crc32:08X}")

# 分块发送(带进度条)
chunk_size = 256
with tqdm(total=total_size, unit="B", unit_scale=True, desc="升级中") as pbar:
    for i in range(0, total_size, chunk_size):
        chunk = data[i:i+chunk_size]
        send_data(chunk)
        pbar.update(len(chunk))

# 通知结束
send_end(crc32, total_size)
print("固件升级完成 ✅")

stm32端-增加校验

在CRC校验算法中,数据被视为一个巨大的二进制数,代表一个多项式的系数。生成多项式(CRC32_POLY)就是用来与这个数据多项式进行“模2除法”运算的除数,最终得到的余数就是CRC校验值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 多项式 (Ethernet, ZIP, etc. 通用): 0x04C11DB7
#define CRC32_POLY 0xEDB88320UL
uint32_t crc32_calc(uint8_t *data, uint32_t length) {
    uint32_t crc = 0xFFFFFFFFUL;
    for (uint32_t i = 0; i < length; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 1) {
                crc = (crc >> 1) ^ CRC32_POLY;
            } else {
                crc >>= 1;
            }
        }
    }
    return ~crc;
}

bootloader逻辑改变

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#define APP_ADDR 0x08008000
#define CMD_START 0x01
#define CMD_DATA  0x02
#define CMD_END   0x03
void Bootloader_Update(void)
{
    uint8_t cmd;
    uint8_t buf[256];
    uint32_t addr = APP_ADDR;

    Flash_Erase_App();

    while(1)
    {
        HAL_UART_Receive(&huart1, &cmd, 1, HAL_MAX_DELAY);

        if(cmd == CMD_DATA) {
            uint16_t len;
            HAL_UART_Receive(&huart1, (uint8_t*)&len, 2, HAL_MAX_DELAY);
            HAL_UART_Receive(&huart1, buf, len, HAL_MAX_DELAY);

            Flash_Write(addr, buf, len);
            addr += len;

            HAL_UART_Transmit(&huart1, (uint8_t*)"OK", 2, HAL_MAX_DELAY);
        }
        else if(cmd == CMD_END) {
            uint32_t host_crc, host_size;
			HAL_UART_Receive(&huart1, (uint8_t*)&host_crc, 4, HAL_MAX_DELAY);
			HAL_UART_Receive(&huart1, (uint8_t*)&host_size, 4, HAL_MAX_DELAY);

			// 自己计算 CRC32
			uint32_t calc_crc = crc32_calc((uint8_t*)APP_ADDR, host_size);

			if (calc_crc == host_crc) {
				HAL_UART_Transmit(&huart1, (uint8_t*)"DONE", 4, HAL_MAX_DELAY);
			} else {
				HAL_UART_Transmit(&huart1, (uint8_t*)"FAIL", 4, HAL_MAX_DELAY);
			}
            break;
        }
    }
}
使用 Hugo 构建
主题 Stack 由 Jimmy 设计