Flash 分区布局
STM32F103 拥有 512 KB 片内 Flash(0x08000000 ~ 0x0807FFFF),页大小 2 KB。我们将整个 Flash 划分为以下区域:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 32 KB | 引导加载程序 |
| Slot A | 0x08008000 | 236 KB | 应用固件槽位 A |
| Slot B | 0x08043000 | 236 KB | 应用固件槽位 B |
| Boot Control 页 | 0x0807E000 | 2 KB | 启动控制元数据 |
| State 页 | 0x0807E800 | 2 KB | 升级状态镜像 |
| Diagnostics 页 | 0x0807F000 | 2 KB | G780s 诊断数据 |
| Config 页 | 0x0807F800 | 2 KB | G780s 远程配置 |
这些地址定义在 BSP/Upgrade.h 中:
#define UPGRADE_BOOT_BASE_ADDR 0x08000000UL
#define UPGRADE_BOOT_MAX_SIZE 0x00008000UL // 32 KB
#define UPGRADE_SLOT_A_BASE_ADDR 0x08008000UL
#define UPGRADE_SLOT_B_BASE_ADDR 0x08043000UL
#define UPGRADE_SLOT_MAX_SIZE 0x0003B000UL // 236 KB
#define UPGRADE_BOOTCTRL_PAGE_ADDR 0x0807E000UL
#define UPGRADE_STATE_PAGE_ADDR 0x0807E800UL
Bootloader 固定在 Flash 起始位置,不会被升级覆盖。两个应用槽位大小相同,升级时写入”非当前运行”的那个槽位,实现无缝切换。
升级状态页与 BootControl 页
UpgradeStateImage — 升级状态镜像
持久化在 UPGRADE_STATE_PAGE_ADDR(0x0807E800),记录当前升级过程的完整状态:
typedef struct {
uint32_t magic; // 0x55504753 ("UPGS")
uint16_t version; // 结构体版本,当前 v2
uint16_t state; // IDLE/REQUESTED/ERASING/PROGRAMMING/VERIFYING/DONE/FAILED
uint16_t request_source; // NONE/LOCAL/G780S/REMOTE/MQTT
uint32_t target_fw_version;
uint32_t image_size;
uint32_t image_crc32;
uint8_t image_sha256[32]; // v2 新增
uint32_t written_bytes; // 写入进度
uint32_t last_ok_offset; // 断点恢复位置
uint16_t error_code;
uint16_t active_boot_count; // 卡在"升级活跃"状态的启动次数
uint16_t crc16; // Modbus CRC16 校验
} UpgradeStateImage;
状态机流转:IDLE → REQUESTED → ERASING → PROGRAMMING → VERIFYING → DONE(或任意阶段 → FAILED)。
UpgradeBootControl — 启动控制
持久化在 UPGRADE_BOOTCTRL_PAGE_ADDR(0x0807E000),管理 A/B 槽位的启动决策:
typedef struct {
uint32_t magic; // 0x55424743 ("UBGC")
uint16_t active_slot; // 当前运行槽位
uint16_t confirmed_slot; // 已确认稳定的槽位
uint16_t pending_slot; // 新写入、等待试运行的槽位
uint16_t boot_attempts; // 试运行启动次数
uint16_t watchdog_reset_count;// 连续 IWDG 复位次数
UpgradeSlotRecord slots[2]; // A/B 槽位元数据(版本、大小、CRC32、SHA-256)
uint16_t crc16;
} UpgradeBootControl;
两个页面均使用 CRC16(Modbus 多项式 0xA001)做完整性校验,并支持 v1 → v2 的向前兼容迁移。
YMODEM 接收与写入流程
Bootloader 通过 USART3(RS-485,115200 波特率)接收固件镜像,使用 YMODEM-C 协议。完整流程:
1. 会话初始化
Bootloader 配置 YMODEM 回调结构体:
const YmodemReceiveConfig config = {
.read_byte = BootProtocol_YmodemReadByte, // 逐字节读取
.write_bytes = BootProtocol_YmodemWriteBytes, // RS-485 方向切换 + 发送
.on_start = BootProtocol_YmodemStart, // 头包回调 → BootFlash_BeginImage
.on_data = BootProtocol_YmodemData, // 数据包回调 → BootFlash_WriteImageData
};
2. 头包解析
YMODEM 的 packet#0 包含文件名和元数据字符串:
"<size> 0x<crc32> <target_fw_version> <sha256_hex>"
解析器从中提取文件大小、CRC32、目标固件版本和 SHA-256 摘要(64 位十六进制解码为 32 字节)。
3. 擦除与编程
BootFlash_BeginImage() 选择下载槽位(始终写入”另一个”槽位),然后按需擦除 2KB 页。数据包到达后,BootFlash_WriteImageData() 以半字(16-bit)为单位编程,每 2KB 或最后一个包时将状态持久化到 Flash,支持断点恢复。
4. 传输协议
YMODEM-C 支持 SOH(128 字节)和 STX(1024 字节)两种包格式,每包 CRC-16 校验,错误重传最多 5 次。
三重校验机制
固件写入完成后,Bootloader 执行三重验证:
static uint16_t BootVerify_ValidateDigestsAndVector(...)
{
// 1. 大小校验
if (image_size == 0u || image_size > UPGRADE_SLOT_MAX_SIZE)
return BOOT_ERR_BAD_SIZE;
// 2. CRC32 校验
if (calc_crc32 != image_crc32)
return BOOT_ERR_VERIFY_CRC32;
// 3. SHA-256 校验(向后兼容:全零则跳过)
if (BootVerify_HasDigest(expected_digest) != 0u) {
if (memcmp(digest, expected_digest, 32) != 0)
return BOOT_ERR_VERIFY_SHA256;
}
// 4. 向量表合法性校验
if (Upgrade_IsSlotVectorValid(slot) == 0u)
return BOOT_ERR_VECTOR_INVALID;
return BOOT_ERR_NONE;
}
向量表校验检查 STM32 启动向量的合法性:
uint8_t Upgrade_IsAppVectorValid(uint32_t app_base_addr)
{
uint32_t app_stack = *(__IO uint32_t *)app_base_addr; // MSP
uint32_t app_reset = *(__IO uint32_t *)(app_base_addr + 4); // Reset Handler
// 栈指针必须在 SRAM 范围内
if ((app_stack & 0x2FFE0000u) != 0x20000000u) return 0u;
// 复位向量必须指向 App 区域内
if (app_reset < app_base_addr ||
app_reset >= (app_base_addr + UPGRADE_APP_MAX_SIZE)) return 0u;
return 1u;
}
CRC32 和 SHA-256 均直接在 Flash 上流式计算,无需将整个镜像拷贝到 RAM。SHA-256 使用纯软件实现,按 256 字节分块读取 Flash。
试运行与自动回退
升级写入完成后,Bootloader 设置 pending_slot 并复位。下次启动时进入试运行流程:
试运行逻辑
- 检测到
pending_slot != NONE,重新校验该槽位的 Flash 内容 - 校验通过且
boot_attempts < 3:设置active_slot = pending_slot,递增计数,跳转运行 boot_attempts >= 3仍未确认:回退到已确认槽位(优先级:confirmed_slot > active_slot > Slot A > Slot B)
App 侧确认
应用启动后,满足以下两个条件才确认当前槽位稳定:
- 运行时间 ≥ 60 秒(
APP_UPGRADE_CONFIRM_DELAY_MS) - 至少 3 次成功的传感器读取(
APP_UPGRADE_CONFIRM_MIN_HEALTHY_SAMPLES)
确认后清除 pending_slot 和 boot_attempts,将当前槽位标记为 confirmed_slot。
IWDG 复位回退
Bootloader 在每次启动时检查 RCC_FLAG_IWDGRST 标志:
if (g_boot_runtime.boot_control.watchdog_reset_count < UPGRADE_WDG_RESET_LIMIT)
return 0u; // 还没到阈值,允许继续启动
如果连续 3 次 IWDG 复位:
- 识别”嫌疑槽位”(pending_slot > active_slot > confirmed_slot)
- 找到稳定的回退槽位(不能是嫌疑槽位)
- 执行回退:重置 active_slot/confirmed_slot,清除 pending 计数,标记升级为
FAILED
超时回退
如果状态页在”升级活跃”状态(REQUESTED/ERASING/PROGRAMMING/VERIFYING)连续 3 次启动未推进,Bootloader 自动回退到稳定槽位,错误码 BOOT_ERR_TIMEOUT_RECOVERY。
升级请求来源
系统支持三种升级发起路径,通过 request_source 字段区分:
1. 本地升级(LOCAL)
用户通过 RS-485 连接 PC,使用 YMODEM-C 协议发送固件。进入 Bootloader 的方式:
- 硬件按键:PE4 在启动时保持低电平
- UART 魔术字节:启动 2500ms 内发送
'B'(0x42)或'b'(0x62)
2. G780s 远程升级(G780S / REMOTE)
两个子路径:
- G780S 路径:Modbus 主站发送
G780S_CMD_ENTER_BOOT_UPGRADE(0x0005)命令,App 调用Upgrade_RequestBootMode()后复位,Bootloader 进入 YMODEM 等待 - REMOTE 路径:云端发送
ota_prepareJSON 命令,App 进入 OTA 静默模式,复位后 Bootloader 等待 PC/工具通过串口发送固件
3. MQTT OTA(完全云端)
这是完全基于云端的固件更新路径,App 本身负责将固件写入 Flash,无需进入 Bootloader 接收数据:
ota_begin:云端下发会话参数(session_key、image_size、crc32、sha256、target_slot),App 擦除目标槽位ota_chunk:逐块接收 base64 编码的固件数据,每块独立 CRC32 校验,顺序写入 Flashota_commit:全部接收后,全量 CRC32 + SHA-256 重算,通过则设置pending_slot,复位进入试运行
关键安全特性:如果 MQTT OTA 期间 App 崩溃或设备复位,Bootloader 检测到 request_source == MQTT 且状态仍为”升级活跃”,立即回退到稳定槽位,不会进入 YMODEM 等待循环。
启动决策总览
Bootloader 的启动决策树:
加载 State 页 + BootControl 页
│
├─ 状态为"升级活跃"?
│ ├─ MQTT OTA 中断 → 立即回退
│ ├─ active_boot_count >= 3 → 超时回退
│ └─ 否则 → 留在 Loader 等待 YMODEM 恢复
│
├─ watchdog_reset_count >= 3?
│ └─ 是 → 回退到稳定槽位
│
├─ 硬件按键 / UART 魔术字节?
│ └─ 是 → 留在 Loader
│
├─ pending_slot 有效?
│ ├─ 校验通过 + boot_attempts < 3 → 试运行
│ └─ 否则 → 回退
│
└─ 默认 → 找到有效槽位,跳转运行