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
| 指令 | 返回 | 意义 |
|---|
| AT | ok | 检测固件是否可用 |
| AT+RESET | ok | 模块软件复位 |
| AT+VERSION? | +VERSION:<Param> OK | 获取固件版本 |
| AT+NAME=name | ok | 设置模块名字 |
| AT+NAME? | +NAME:<Param> OK | 返回模块名字 |
| AT+PSWD=“passwd” | OK | 设置模块密码 |
| AT+PSWD? | +PSWD:<Param> OK | 获取模块密码 |
| AT+UART=115200,0,0 | ok | 设置通信时的波特率 |
| AT+UART? | +UART:<Param1>,<Param2>,<Param3> OK | 获取串口参数 |
模块指示灯标志
- 定时闪两下:已经连接蓝牙,处于通信状态
- 快闪:未连接
- 慢闪:工作模式,可使用AT指令调整参数(进入:保持按键按下,通电后松开按键即可)
测试流程

- 先进入工作模式,使用
AT检查是否可用, - 设置模块名字:
AT+NAME=MyBluetooth - 获取密码:
AT+PSWD? - 查看波特率:
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;
}
}
}
|