# STM32_Bootloader **Repository Path**: Hans_Rudle/stm32_bootloader ## Basic Information - **Project Name**: STM32_Bootloader - **Description**: 基于 STM32CubeMX + HAL 库的双工程 Bootloader 方案,适用于学习 Bootloader 原理。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-06-14 - **Last Updated**: 2026-07-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 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` ### 用法 ```bash # 发送固件 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 两种编译器对地址的处理不一样 混用容易导致问题 ### 工作原理 1. 读取 `.bin` 文件 2. 在头部插入 OTA Magic `0x5A5A5A5A` 3. 用 `0xFF` 填充到 1KB 边界 4. 握手(发 `0xAA`,等 `0x06`) 5. 逐块发送 1KB 数据,每块等 `0x06` 6. 发送结束魔数 `AA 55 FF 00`,等 `0x06` 7. 完成 ### 限制 - 固件原始大小不能超过 32KB(OTA 区限制) - 自动对齐到 1KB 页边界 --- ## 烧录方法 ### 初次烧录(ST-Link) ```bash # 烧 Bootloader (0x08000000) STM32_Programmer_CLI -c port=SWD -w Bootloader.elf # 烧 APP (0x08004000) STM32_Programmer_CLI -c port=SWD -w APP.elf ``` ### OTA 升级(串口) ```bash # 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! <-- 新固件 ``` --- ## 注意事项 1. **烧录顺序**:先烧 Bootloader,后烧 APP。Flash 起始地址不同,不会冲突。 2. **APP 必须在 main 中手动设置 `SCB->VTOR`**,否则中断向量表不匹配。 3. **APP 必须调用 `__set_PRIMASK(0)`**,否则 Bootloader 留下的 PRIMASK=1 会屏蔽所有中断。 4. **OTA 升级时 STM32 必须运行 APP**,接收逻辑在 APP 中。 5. **OTA 区 32KB 在 APP 启动时会被擦除**,Bootloader 搬运完成后也会擦除 Magic 所在页。 6. **固件大小限制**:有效固件约 16KB(APP 区大小),但 OTA 区有 32KB 可用于未来扩展。