可用于STM32F103C8T6串口OTA升级和Bootloader参考。项目基于CubeMX/HAL提供启动引导、应用固件与PC发送工具,实现上电检测、固件搬运、跳转和Flash页写入。【此简介由AI生成】
| Files | Last commit | Last update |
|---|---|---|
| 4 months ago | ||
| 4 months ago | ||
| 4 months ago | ||
| 4 months ago | ||
| 4 months ago | ||
| 4 months ago | ||
| 4 months ago |
STM32F103C8T6 Bootloader + APP + OTA
基于 STM32CubeMX + HAL 库的 Bootloader 方案,支持串口 OTA 固件升级。
硬件
| 项目 | 参数 |
|---|---|
| MCU | STM32F103C8T6(64K Flash / 20K SRAM) |
| 晶振 | 外部 8MHz HSE |
| 系统时钟 | 72MHz(PLL x9) |
| 调试串口 | USART1 — PA9(TX) / PA10(RX) / 115200-8-N-1 |
Flash 内存布局
0x0800FFFF ┌──────────────────────┐
│ │
│ OTA 下载区 │
│ 32K │
│ 起始: 0x08008000 │
0x08008000 ├──────────────────────┤
│ │
│ APP │
│ 16K │
│ 起始: 0x08004000 │
0x08004000 ├──────────────────────┤
│ │
│ Bootloader │
│ 16K │
│ 起始: 0x08000000 │
0x08000000 └──────────────────────┘
OTA 区内部结构:
0x08008000: [MAGIC: 4B] [固件向量表 + 代码] ... [0xFF 填充]
└ 0x5A5A5A5A └ 从 APP .bin 拷贝
Bootloader 通过检查 0x08008000 处是否为 0x5A5A5A5A 来判断是否有待升级的新固件。
工程结构
Boot/
├── README.md # 项目说明
├── CSDN.md # 博客文章
├── send_bin.py # PC 端 OTA 固件发送工具
├── test.bin # 测试固件 (1KB)
├── APP2.bin # CLion 编译的 APP2 固件
├── APP2_keil.bin # Keil 编译的 APP2 固件
│
├── clion/ # CLion 工程 (GCC + CubeMX)
│ ├── Bootloader/
│ │ ├── STM32F103XX_FLASH.ld # 链接脚本: Flash 0x08000000, 16K
│ │ ├── startup_stm32f103xb.s # 启动文件 (GCC)
│ │ ├── Drivers/ # HAL 库 + CMSIS
│ │ └── Core/
│ │ ├── Inc/
│ │ │ ├── bootloader.h # OTA Magic + APP 跳转宏定义
│ │ │ └── main.h / usart.h / gpio.h
│ │ └── Src/
│ │ ├── main.c # 主流程: 初始化 → 延时 → 跳转
│ │ ├── bootloader.c # OTA2APP 搬运 + App_Loading 跳转
│ │ ├── stm32f1xx_it.c # 中断服务 (SysTick)
│ │ ├── syscalls.c # newlib 系统调用 (printf 重定向)
│ │ ├── sysmem.c # newlib 堆管理
│ │ └── ...
│ │
│ └── APP/
│ ├── STM32F103XX_FLASH.ld # 链接脚本: Flash 0x08004000, 16K
│ ├── startup_stm32f103xb.s # 启动文件 (GCC)
│ ├── Drivers/ # HAL 库 + CMSIS
│ └── Core/
│ ├── Inc/
│ │ └── main.h / usart.h / gpio.h
│ ├── BSP/
│ │ ├── Serial.h # OTA 接收接口
│ │ └── Serial.c # 串口中断状态机 + Flash 页写入
│ └── Src/
│ ├── main.c # 初始化 + OTA 接收主循环
│ ├── stm32f1xx_it.c # 中断服务 (SysTick + USART1)
│ ├── syscalls.c # newlib 系统调用
│ ├── sysmem.c # newlib 堆管理
│ └── ...
│
└── keil/ # Keil MDK 工程 (ARM Compiler)
├── Bootloader/
│ ├── Bootloader.ioc # CubeMX 工程文件
│ ├── Drivers/ # HAL 库 + CMSIS
│ ├── MDK-ARM/ # Keil 工程文件 (.uvprojx) + 启动文件 + scatter
│ └── Core/
│ ├── Inc/
│ │ ├── bootloader.h # OTA Magic + APP 跳转宏定义
│ │ └── main.h / usart.h / gpio.h
│ └── Src/
│ ├── main.c # 主流程
│ ├── bootloader.c # OTA2APP + App_Loading (printf 用 fputc)
│ ├── stm32f1xx_it.c # 中断服务 (SysTick)
│ └── ... # (无 syscalls/sysmem — Keil 用 Microlib)
│
├── APP/ # 接收 OTA 数据的 APP
│ ├── APP.ioc # CubeMX 工程文件
│ ├── Drivers/ # HAL 库 + CMSIS
│ ├── MDK-ARM/ # Keil 工程文件 + 启动文件 + scatter
│ └── Core/
│ ├── Inc/
│ ├── BSP/
│ │ ├── Serial.h # OTA 接收接口
│ │ └── Serial.c # 串口中断状态机 + Flash 页写入
│ └── Src/
│ ├── main.c # 初始化 + OTA 接收主循环
│ └── stm32f1xx_it.c # 中断服务 (SysTick + USART1)
│
└── APP2/ # OTA 升级的目标固件 (另一份 APP)
├── Drivers/
├── MDK-ARM/
└── Core/
├── BSP/
│ └── Serial.c # 同上
└── Src/
├── main.c # 同上结构, 业务逻辑可不同
└── stm32f1xx_it.c
三个 APP 的职责:
| 工程 | 位置 | 用处 |
|---|---|---|
clion/APP / keil/APP |
烧录到 0x08004000 的运行时固件 |
正常运行 + 通过串口接收 OTA 数据写入暂存区 |
keil/APP2 |
编译生成 .bin, 通过 send_bin.py 发送 |
OTA 升级的目标固件 |
clion/Bootloader / keil/Bootloader |
烧录到 0x08000000 |
上电检查 OTA Magic, 有则搬运, 无则跳转 |
Bootloader 工作流程
上电 / 复位
│
▼
HAL_Init() / SystemClock_Config()
MX_GPIO_Init() / MX_USART1_UART_Init()
│
▼
printf("Bootloader Start...\r\n")
HAL_Delay(3000) ← 等待 3 秒
│
▼
App_Loading()
│
├── *(0x08008000) == 0x5A5A5A5A ?
│ ├── YES ──→ OTA2APP()
│ │ ├── 擦除 APP 区 (0x08004000 起 16 页)
│ │ ├── 从 OTA_DATA_ADDR(0x08008004) 逐字拷贝到 APP 区
│ │ └── 擦除 OTA Flag 页 (清除 Magic)
│ │
│ └── NO ──→ 跳过
│
├── 读取 APP 栈顶地址 (0x08004000 处第一个字)
├── 校验栈地址是否在 SRAM 合法范围内
│ ├── 合法 → 设 MSP → 关中断 → 跳转到 APP Reset_Handler
│ └── 非法 → printf("Address Not Found") → 死循环
│
▼
APP 启动
APP 工作流程 (OTA 接收模式)
Reset_Handler
│
▼
SystemInit() / HAL_Init() / SystemClock_Config()
SCB->VTOR = FLASH_BASE | 0x4000 ← 向量表指向 APP 区
__set_PRIMASK(0) ← 恢复中断
│
▼
MX_GPIO_Init() / MX_USART1_UART_Init()
HAL_UART_Transmit("Hello World!\r\n")
│
▼
OTA_Erase() ← 擦除 OTA 区 32KB (0x08008000 起)
write_addr = 0x08008000
HAL_UART_Receive_IT(...) ← 启动串口中断接收
│
▼
while (1) {
if (rx_flag == 1) {
OTA_update(); ← 写完一页后发 ACK, 进入 END_CHECK 状态
}
}
串口中断状态机 (Serial.c):
┌─────────────────────────────┐
│ │
▼ │
┌──────┐ 收到 0xAA │
上电 ──→│ IDLE │────────────────────┐ │
└──────┘ │ │
▲ ▼ │
│ ┌──────────┐ │
│ 回复 ACK │ HANDSHAKE│ │
│ └──────────┘ │
│ │ │
│ 收满 1024 字节 │
│ ▼ │
│ OTA_WritePage() │
│ 回复 ACK │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ 4B 不匹配 │ END_CHECK│ │
│ 退回数据 ───→│ (收4字节) │ │
│ └─────┬────┘ │
│ │ │
│ 4B == AA 55 FF 00
│ │ │
└─────── 回复 ACK ──────┘ │
│
固件数据 │
(归入 HANDSHAKE 继续收) ────┘
OTA 串口协议
电平信号
| 信号 | 方向 | 值 | 含义 |
|---|---|---|---|
| 握手 | PC → STM32 | 0xAA |
请求开始升级 |
| ACK | STM32 → PC | 0x06 |
操作成功 |
| 结束魔数 | PC → STM32 | AA 55 FF 00 (4B) |
全部数据发送完毕 |
完整交互时序
PC (send_bin.py) STM32 APP
│ │
│──── 0xAA ────────────────────────────────→ │ 握手
│←──── 0x06 ──────────────────────────────── │ 就绪 (OTA 区已擦除)
│ │
│──── 1024 bytes (raw) ────────────────────→ │ 写第 1 页 Flash
│←──── 0x06 ──────────────────────────────── │
│──── 1024 bytes (raw) ────────────────────→ │ 写第 2 页 Flash
│←──── 0x06 ──────────────────────────────── │
│ ... 重复所有块 ... │
│──── 1024 bytes (raw) ────────────────────→ │ 写最后 1 页 Flash
│←──── 0x06 ──────────────────────────────── │
│ │
│──── AA 55 FF 00 ─────────────────────────→ │ 结束魔数 (4字节)
│←──── 0x06 ──────────────────────────────── │ 传输完成
数据格式
PC 端在固件数据头部 插入 4 字节魔数 0x5A5A5A5A(小端序),然后对齐到 1KB 边界,以原始字节流形式发送。
发送数据 = [0x5A 0x5A 0x5A 0x5A] + [固件 .bin 原始内容] + [0xFF ... 填充到 1KB 对齐]
每发送 1024 字节后暂停,等待 STM32 回复 0x06,再发下一块。
防误触发
结束魔数使用 4 字节序列 AA 55 FF 00,误触发概率约为 1 / 2³² ≈ 1 / 42.9 亿。
STM32 端采用回退机制:如果收到的 4 字节与结束魔数不匹配,则将该 4 字节视为下一页固件数据的头部,切回接收模式继续累积。
上位机发送工具 (send_bin.py)
环境
依赖:pyserial
用法
# 发送固件
python send_bin.py COM24 APP2.bin
# 仅检查文件不发送
python send_bin.py COM24 APP2.bin --dry-run
# 查看帮助
python send_bin.py --help
注意 !!!
clion工程使用app2.bin keil工程使用App2_keil.bin 两种编译器对地址的处理不一样 混用容易导致问题
工作原理
- 读取
.bin文件 - 在头部插入 OTA Magic
0x5A5A5A5A - 用
0xFF填充到 1KB 边界 - 握手(发
0xAA,等0x06) - 逐块发送 1KB 数据,每块等
0x06 - 发送结束魔数
AA 55 FF 00,等0x06 - 完成
限制
- 固件原始大小不能超过 32KB(OTA 区限制)
- 自动对齐到 1KB 页边界
烧录方法
初次烧录(ST-Link)
# 烧 Bootloader (0x08000000)
STM32_Programmer_CLI -c port=SWD -w Bootloader.elf
# 烧 APP (0x08004000)
STM32_Programmer_CLI -c port=SWD -w APP.elf
OTA 升级(串口)
# 1. 编译 APP 工程, 生成 .bin 文件
# 2. STM32 上电, 运行 APP 固件
# 3. 发送新固件
conda activate DM_control
python send_bin.py COM24 APP2.bin
# 4. 复位 STM32, Bootloader 自动检测 OTA Magic 并搬运
OTA 升级完整过程
① STM32 上电, 运行 APP (0x08004000)
│
② PC 通过 send_bin.py 发送新固件
│ APP 中断接收 → 写入 OTA 区 (0x08008000)
│ 首 4 字节 = 0x5A5A5A5A (Magic)
│
③ 传输完成, 复位 STM32
│
④ Bootloader 启动, 检查 OTA 区
│ *(0x08008000) == 0x5A5A5A5A ? YES!
│
⑤ OTA2APP(): 擦除 APP 区 → 拷贝 OTA 数据到 APP → 擦除 OTA Magic
│
⑥ App_Loading(): 校验 APP 栈 → 跳转到新固件
│
⑦ 新固件运行!
串口输出示例
正常启动 (无 OTA 升级):
Bootloader Start...
APP SP: 20005000
Start To APP
Hello World!
有 OTA 升级:
Bootloader Start...
OTA firmware found, updating...
Erasing APP area...
Copying firmware from OTA to APP...
Clearing OTA flag...
OTA update complete!
APP SP: 20005000
Start To APP
Hello World! <-- 新固件
注意事项
- 烧录顺序:先烧 Bootloader,后烧 APP。Flash 起始地址不同,不会冲突。
- APP 必须在 main 中手动设置
SCB->VTOR,否则中断向量表不匹配。 - APP 必须调用
__set_PRIMASK(0),否则 Bootloader 留下的 PRIMASK=1 会屏蔽所有中断。 - OTA 升级时 STM32 必须运行 APP,接收逻辑在 APP 中。
- OTA 区 32KB 在 APP 启动时会被擦除,Bootloader 搬运完成后也会擦除 Magic 所在页。
- 固件大小限制:有效固件约 16KB(APP 区大小),但 OTA 区有 32KB 可用于未来扩展。