【以太网摄像头采集存储终端】A/B 双槽 Bootloader + EEPROM 启动管理信息备份

1. 引言

本文以当前 STM32H757XIH6 双核工程为例,说明 CM7 固定 Bootloader、CM4 Stub、双核 A/B 应用槽和 EEPROM 启动记录的协作方式。先介绍内存分区和试启动机制,再展示完整运行流程与关键代码。

本文聚焦启动与升级状态管理:应用将新镜像写入空闲槽并完成校验后,在 EEPROM 中登记 pending;复位后进入试启动流程,满足应用确认条件则转正,否则在试启动额度耗尽后回退。当前工程已接入双核 HTTP OTA,网络传输和在线 Flash 擦写的具体实现另文介绍。

启动职责分为三层:CM7 固定 Bootloader 选择并启动 CM7 应用;CM7 应用选择 CM4 槽并写入 SRAM4 交接信息;CM4 固定 Stub 校验交接目标后跳转。EEPROM 保存双核各自的正式槽、候选槽、试启动次数和镜像信息,串起这条启动链。

2. STM32H757 的主要内存区域

  1. 内部 Flash (Dual-Bank)
区域 起始地址 大小 说明
Flash Bank 1 0x0800 0000 1 MB 可读/写,双 Bank 支持 RWW
Flash Bank 2 0x0810 0000 1 MB 可读/写,双 Bank 支持 RWW
OTP (一次性可编程) 0x08FF F000 1 KB 应用非机密 OTP 数据
  1. TCM RAM (紧耦合内存, 零等待, 仅 Cortex-M7)
区域 起始地址 大小 说明
ITCM RAM 0x0000 0000 64 KB 指令紧耦合内存,存放关键实时代码
DTCM RAM 0x2000 0000 128 KB 数据紧耦合内存,存放关键实时数据/栈
  1. AXI SRAM (D1 域, 高性能)
区域 起始地址 大小 说明
AXI SRAM 0x2400 0000 512 KB 所有主设备可访问(除 BDMA),适合 DMA 缓冲
  1. D2 域 SRAM (通信外设域)
区域 起始地址 大小 Cortex-M4 别名地址 说明
SRAM1 0x3000 0000 128 KB 0x1000 0000 可被 M4 通过 AHB 访问
SRAM2 0x3002 0000 128 KB 0x1002 0000 可被 M4 通过 AHB 访问
SRAM3 0x3004 0000 32 KB 0x1004 0000 可被 M4 通过 AHB 访问

注:D2 域用户 SRAM 合计 288 KB。部分资料将 SRAM1/SRAM2 合并表述为 256 KB,SRAM3 单独列出 32 KB,本质相同。

  1. D3 域 SRAM (低功耗域)
区域 起始地址 大小 说明
SRAM4 0x3800 0000 64 KB 低功耗域 SRAM,可由 BDMA 访问
Backup SRAM 0x3880 0000 4 KB 备份域,VBAT/待机模式下保持
  1. 外部存储器接口 (FMC / QUADSPI)
区域 起始地址 大小 说明
FMC Bank 1 (NOR/PSRAM) 0x6000 0000 256 MB NOR/PSRAM 映射
FMC Bank 3 (NAND) 0x8000 0000 256 MB NAND Flash 映射
QUADSPI 0x9000 0000 256 MB 外部 QSPI Flash 映射

内部 SRAM 总量汇总

  • ITCM:64 KB
  • DTCM:128 KB
  • AXI SRAM:512 KB
  • D2 SRAM (SRAM1+2+3):128 + 128 + 32 = 288 KB
  • D3 SRAM4:64 KB
  • Backup SRAM:4 KB
  • 合计:不含 Backup SRAM 时为 1056 KiB,包含 4 KiB Backup SRAM 时为 1060 KiB(不含 Flash;上表容量按 1024 字节计)。

2.1 向量表

Cortex-M 应用的向量表通常至少包含:

偏移地址(相对于向量表基址) 内容含义
向量表地址 + 0x00 初始 MSP
向量表地址 + 0x04 Reset_Handler 地址
向量表地址 + 0x08 NMI_Handler 地址
向量表地址 + 0x0C HardFault_Handler 地址

例如 CM7 应用位于:
0x08020000
那么 Bootloader 跳转前需要做:

app_base = 0x08020000

initial_msp   = *(uint32_t *)(app_base + 0x00)
reset_handler = *(uint32_t *)(app_base + 0x04)

SCB->VTOR = app_base
MSP       = initial_msp
跳转到 reset_handler
  • VTOR 指向新应用的向量表,确保中断从应用自己的向量表获取处理函数;
  • MSP 设置为应用向量表第一个字中的初始主栈地址,确保应用从正确的栈开始执行;
  • 然后跳转到向量表第二个字中的 Reset_Handler。

Bootloader 跳转应用时,需要同时完成向量表重定位和运行栈切换:通过 SCB->VTOR 让中断使用应用的向量表,通过 MSP 让应用使用链接脚本为自己准备的初始主栈,最后跳转到应用的 Reset_Handler。
如果只跳到 reset_handler,但不修改 VTOR,应用运行后产生的中断仍可能从 Bootloader 的向量表取处理函数,导致系统异常。

其中关键动作包括:

  • 关闭中断;
  • 停止 SysTick;
  • 禁用和清理 NVIC;
  • 设置 SCB->VTOR;
  • 设置 MSP;
  • 跳转到目标向量表第二项。

本工程还会在 DTCM 写入自定义跳转标记,但当前 APP 的 SystemInit 未读取它。这是工程附加动作,不是 Cortex-M 应用跳转的通用必要条件,不能据此认为 APP 会跳过时钟初始化。

2.2 Bootloader Flash 双分区

芯片地址空间是硬件提供的范围,工程是否使用某一段地址,最终由链接文件和启动代码决定。下面的分区以默认 Makefile 和链接脚本为准。

区域 起始地址 大小 作用
CM7 Bootloader 0x08000000 128 KB CM7 引导程序/启动入口
CM7 应用 Slot A 0x08020000 384 KB CM7 应用 A 固件区/运行槽
预留区域 0x08080000 128 KB 预留区域,暂未分配
CM7 应用 Slot B 0x080A0000 384 KB CM7 应用 B 固件区/运行槽
CM4 Bootloader Stub 0x08100000 128 KB CM4 固定跳板/启动跳转入口
CM4 应用 Slot A 0x08120000 384 KB CM4 应用 A 固件区/运行槽
预留区域 0x08180000 128 KB 预留区域,暂未分配
CM4 应用 Slot B 0x081A0000 384 KB CM4 应用 B 固件区/运行槽

2.2.1 单分区与双分区的区别

维度 单分区 Bootloader 双分区 A/B
APP 区 1 个,OTA 就地擦写 2 个,OTA 写入非活动槽
升级中断电 若直接擦写唯一 APP 区,中断可能导致 APP 无法启动;可通过保留的 Bootloader 下载功能、ROM Bootloader 或 SWD 恢复 写入未完成只影响空闲槽;在旧镜像及启动记录仍可用的前提下,继续启动旧槽
升级失败回退 无法回退,只能重刷 EEPROM 标记 pending + 3 次试启动,失败自动回退 confirmed
状态存放 基本不需要(固定跳转) 必须有掉电不丢的状态记录 → 本工程用 EEPROM 双备份存 confirmed/pending/trial
Boot 逻辑 检查 APP 有效就跳 选槽链 pending→confirmed→A→B + 试启动扣减 + 落盘
Flash 代价 APP 只占 1 份空间 APP 占 2 份(384Kx2/核),换升级安全
双核复杂度 - CM7/CM4 各自独立 A/B,元数据 struct_ver=2 里存两套状态;可分别升级,但版本组合必须兼容,当前机制不提供双核成组提交与成组回滚

双分区通过保留旧镜像提高升级失败后的恢复能力。本工程叠加双核后需要管理 4 个应用槽,因此选择 EEPROM 双备份保存启动状态;持久化元数据也可以使用专门设计的 Flash 保留区,并非只能使用 EEPROM。

2.2.2 链接文件规划

由于当前 Bootloader 的策略是双分区 Bootloader —— CM7 和 CM4 各有 A/B 两个槽应用槽,共 4 个应用槽,由 EEPROM 里的一份元数据统一管理状态。
在写 Bootloader 代码之前,必须先规划好链接文件,否则很容易出现内存错误导致进入硬件错误中断函数中。

使用 A/B 双应用槽后,APP 工程的每个核都需要 A、B 两份 Flash 链接脚本,共四份;Bootloader 工程的 CM7 Boot 和 CM4 Stub 各使用一份固定地址的 Flash 链接脚本。A 槽镜像不能仅更换烧录地址后作为 B 槽镜像运行。

虽然是两份链接文件 app_slot_A 和 app_slot_B,但是其中只有内存分配部分内容不同,其他全部相同。

2.2.3 APP 链接文件

四份 APP 链接脚本为 SRAM4 采用同一布局,避免 OpenAMP 共享区覆盖启动交接信息:

地址范围 用途 大小
0x38000000~0x380003FF OpenAMP resource table 1 KiB
0x38000400~0x3800EFFF OpenAMP 共享区 59 KiB
0x3800F000~0x3800FFFF 启动交接、OTA 停核握手和 CM4 运行槽报告保留区 4 KiB

下文只摘录内存分配部分。完整链接脚本还需正确放置 .RamFunc,并由启动代码将其复制到可执行 RAM;CM4 的 OTA hold 等待函数依赖这一配置。

image

2.2.3.1 CM4 slot A/B

CM4 slot A:

/* Specify the memory areas */
MEMORY
{
/* Slot A:0x08120000..0x0817FFFF;Stub 占 0x08100000 */
FLASH (rx)      : ORIGIN = 0x08120000, LENGTH = 384K
/* CM4's 0x10000000 window aliases CM7 D2 SRAM at 0x30000000. CM7 reserves
 * 0x30000000..0x30024FFF for ETH descriptors, RX pool and the 128K LwIP heap. */
RAM (xrw)      : ORIGIN = 0x10028000, LENGTH = 128K
OPENAMP_RSC_TAB (xrw)  : ORIGIN = 0x38000000, LENGTH = 1K
OPEN_AMP_SHMEM (xrw) : ORIGIN = 0x38000400, LENGTH = 59K
}
/* Highest address of the user mode stack */
_estack = 0x10048000;    /* end of RAM = start RAM + LENGTH */
__OPENAMP_region_start__  = ORIGIN(OPEN_AMP_SHMEM);
__OPENAMP_region_end__    = ORIGIN(OPEN_AMP_SHMEM) + LENGTH(OPEN_AMP_SHMEM);
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

CM4 slot B:

/* Specify the memory areas */
MEMORY
{
/* Slot B:0x081A0000..0x081FFFFF */
FLASH (rx)      : ORIGIN = 0x081A0000, LENGTH = 384K
/* CM4's 0x10000000 window aliases CM7 D2 SRAM at 0x30000000. CM7 reserves
 * 0x30000000..0x30024FFF for ETH descriptors, RX pool and the 128K LwIP heap. */
RAM (xrw)      : ORIGIN = 0x10028000, LENGTH = 128K
OPENAMP_RSC_TAB (xrw)  : ORIGIN = 0x38000000, LENGTH = 1K
OPEN_AMP_SHMEM (xrw) : ORIGIN = 0x38000400, LENGTH = 59K
}
/* Highest address of the user mode stack */
_estack = 0x10048000;    /* end of RAM = start RAM + LENGTH */
__OPENAMP_region_start__  = ORIGIN(OPEN_AMP_SHMEM);
__OPENAMP_region_end__    = ORIGIN(OPEN_AMP_SHMEM) + LENGTH(OPEN_AMP_SHMEM);
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

2.2.3.2 CM7 slot A/B

CM7 slot A:

/* Specify the memory areas */
MEMORY
{
/* Slot A:0x08020000..0x0807FFFF */
FLASH (rx)      : ORIGIN = 0x08020000, LENGTH = 384K
/* AXI SRAM — ETH DMA can access this; DTCM (0x20000000) cannot */
RAM (xrw)       : ORIGIN = 0x24000000, LENGTH = 512K
RAM_D2 (xrw)    : ORIGIN = 0x30000000, LENGTH = 288K
ITCMRAM (xrw)      : ORIGIN = 0x00000000, LENGTH = 64K
OPENAMP_RSC_TAB (xrw)  : ORIGIN = 0x38000000, LENGTH = 1K
OPEN_AMP_SHMEM (xrw) : ORIGIN = 0x38000400, LENGTH = 59K
}
/* Highest address of the user mode stack */
_estack = 0x24080000;    /* end of RAM = start RAM + LENGTH */
__OPENAMP_region_start__  = ORIGIN(OPEN_AMP_SHMEM);
__OPENAMP_region_end__    = ORIGIN(OPEN_AMP_SHMEM) + LENGTH(OPEN_AMP_SHMEM);
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

CM7 slot B:

/* Specify the memory areas */
MEMORY
{
/* Slot B:0x080A0000..0x080FFFFF */
FLASH (rx)      : ORIGIN = 0x080A0000, LENGTH = 384K
/* AXI SRAM — ETH DMA can access this; DTCM (0x20000000) cannot */
RAM (xrw)       : ORIGIN = 0x24000000, LENGTH = 512K
RAM_D2 (xrw)    : ORIGIN = 0x30000000, LENGTH = 288K
ITCMRAM (xrw)      : ORIGIN = 0x00000000, LENGTH = 64K
OPENAMP_RSC_TAB (xrw)  : ORIGIN = 0x38000000, LENGTH = 1K
OPEN_AMP_SHMEM (xrw) : ORIGIN = 0x38000400, LENGTH = 59K
}
/* Highest address of the user mode stack */
_estack = 0x24080000;    /* end of RAM = start RAM + LENGTH */
__OPENAMP_region_start__  = ORIGIN(OPEN_AMP_SHMEM);
__OPENAMP_region_end__    = ORIGIN(OPEN_AMP_SHMEM) + LENGTH(OPEN_AMP_SHMEM);
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

2.2.4 Bootloader 链接文件

2.2.4.1 CM4

CM4:

/* Specify the memory areas */
MEMORY
{
FLASH (rx)      : ORIGIN = 0x08100000, LENGTH = 128K
RAM (xrw)      : ORIGIN = 0x10000000, LENGTH = 288K
}
/* Highest address of the user mode stack */
_estack = 0x10048000;    /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

2.2.4.2 CM7

CM7:

/* Specify the memory areas */
MEMORY
{
FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 128K
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 128K
ITCMRAM (xrw)      : ORIGIN = 0x00000000, LENGTH = 64K
}
/* Highest address of the user mode stack */
_estack = 0x20020000;    /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

3. Bootloader 双槽机制

A/B 是两个物理槽;confirmed 与 pending 是记录在 EEPROM 中的角色。confirmed 表示“已经验证可用的正式槽”,对应字段 confirmed_slot / cm4_confirmed_slot;pending 表示“准备试运行、尚未确认的候选槽”;running_slot 表示实际运行槽。Bootloader 根据启动记录和镜像校验结果决定启动 A 还是 B。

3.1 正常运行时:只有 confirmed

假设设备当前运行 A,而且 A 已经确认正常:

Flash:
  A 槽:旧版本,正常运行
  B 槽:空白或其他版本

EEPROM:
  confirmed = A
  pending 标志 = 0
每次复位,设备选择 confirmed,也就是 A。

注意:pending 是否存在,由 pending 标志决定。即使 pending_slot 字段里保存着 A 或 B,只要标志没有置位,就不表示有待试运行镜像。

3.2 升级完成后:把 B 设置为 pending

把新固件写到 B,校验通过后,更新 EEPROM:

confirmed = A
pending = B
pending 标志 = 1
trial = 3

这表示:
A 仍然是已确认的可靠版本;下次先尝试 B,给 B 一定次数证明自己能正常运行。
此时不能立即把 confirmed 改成 B。因为“固件写入正确”只说明数据完整,还不能证明新程序可以正常启动、运行任务和控制外设。

3.3 复位后:优先试运行 pending

启动选择大致是:

存在有效 pending?
    ├─ 有,而且还有试启动机会 → 启动 pending
    └─ 没有或机会耗尽         → 启动 confirmed

于是设备进入 B,但 EEPROM 此时仍然可以是:
实际运行槽 = B
confirmed = A
pending = B

“当前正在运行哪个槽”和“哪个槽已经被确认可靠”是两件事。
这是试运行期间最重要的区别:

信息 试运行 B 时的值 含义
running_slot B CPU 现在正在执行 B 的程序
confirmed A A 是最后一次确认可靠的版本
pending B B 正在接受验证

3.4 B 运行正常:提交,成为 confirmed

B 启动后达到应用约定的确认条件,再执行“确认”。本节说明 pending 的状态变化,具体确认位置以应用实现为准:

确认前:
  confirmed = A
  pending = B
  pending 标志 = 1

确认后:
  confirmed = B
  pending 标志 = 0
  trial = 0

这一步就是代码里的 commit_pending()。
之后每次复位,设备都从 B 启动。

3.5 B 启动失败:保留 A,最后回退

如果 B 卡死、异常复位,或者一直没有通过健康检查,就不能提交 confirmed。

confirmed 始终保持 A
pending 仍为 B
每次试启动前消耗一次机会。机会耗尽后:
confirmed = A
pending 标志 = 0
设备恢复启动 A。这就是 A/B 回滚机制。

不过,试启动计数本身不会让死机的程序复位。还需要看门狗或健康监控触发复位,设备才会重新进入 Bootloader、继续试启动或执行回退。
当前代码在试启动前扣减并保存次数,保存成功后仍进入本次候选槽。因此 trial=3 允许三次候选启动:3→2、2→1、1→0;若第三次仍未提交,下一次复位读取到 trial=0 时才回退。扣减记录保存失败时,本次不启动候选,而是回退。

3.6 下一次升级:A、B 的角色交换

当 B 已经成为 confirmed,下一次就把新固件写到 A:

第一次升级:
  A 是 confirmed → 写 B → B 是 pending → 确认 B

第二次升级:
  B 是 confirmed → 写 A → A 是 pending → 确认 A
这样始终保留当前已确认版本,直到新版本通过验证。

双核工程中,CM7 和 CM4 各有一套独立的 confirmed、pending、trial。例如可以出现:

CM7:confirmed=B,无 pending
CM4:confirmed=A,pending=B

表示 CM7 已经正式运行 B,CM4 还在试运行 B。CM7 的候选槽由固定 Bootloader 选择;CM4 的候选槽由 CM7 应用选择后,通过 handoff 交给 CM4 Stub 跳转。
最后有一个边界:pending 机制保护的是“新版本确认之前”的升级过程。一旦 B 已提交为 confirmed,随后才出现故障,当前机制不会仅因为 A 里还有旧镜像,就自动把 A 恢复为正式版本。

4. Bootloader + EEPROM 运行流程

Bootloader 运行流程图:

芯片复位
   ↓
CM7 Bootloader 启动
   ↓
初始化时钟、串口、I2C、EEPROM
   ↓
读取 EEPROM 启动记录
   ↓
检查双核槽向量表;若启动时持续按 KEY0 两秒,执行 CM7 双槽恢复登记
   ↓
选择 CM7 应用槽位
pending → confirmed → Slot A → Slot B
   ↓
选槽时验证长度、向量表和 Flash CRC32
pending 必须有合法长度/CRC;未登记的出厂正式槽允许兼容启动
试启动前保存 trial 扣减;无候选额度或保存失败则回退
跳转前再次复核 MSP、Reset_Handler 和 Thumb 位
   ↓
无效 ─────────────→ 停留在 CM7 Bootloader
   │
   │ 有效
   ↓
禁止 CM4 从固定入口启动
   ↓
关闭中断、停止 SysTick、清除 NVIC
   ↓
设置 CM7 VTOR 和 MSP
   ↓
跳转到 CM7 应用 Reset_Handler
   ↓
CM7 应用启动
   ↓
初始化 MPU、Cache
   ↓
清空 SRAM4 共享区
   ↓
SystemClock_Config 后释放 HSEM(当前源码位置)
   ↓
初始化外设并读取 EEPROM 中的 CM4 启动记录
   ↓
选择 CM4 应用槽位并校验长度、向量表和 Flash CRC
pending → confirmed → Slot A → Slot B
试启动前保存 CM4 trial 扣减,失败则回退
   ↓
将 CM4 应用地址写入 SRAM4
   ↓
写入 handoff magic
   ↓
使能 CM4 BOOT_C2
   ↓
CM4 Stub 启动
   ↓
读取 SRAM4 handoff 信息
   ↓
检查 CM4 应用向量表
   ↓
无效 ─────────────→ CM4 Stub 停留
   │
   │ 有效
   ↓
关闭中断、停止 SysTick
   ↓
设置 CM4 VTOR 和 MSP
   ↓
跳转到 CM4 应用 Reset_Handler
   ↓
CM4 应用启动,执行自己的启动同步分支
   ↓
CM4 配置 HSEM 通知并执行 D2 STOP/WFE
(CM7 的 HSEM 释放在自身 SystemClock_Config 后,早于此次使能 BOOT_C2;
不能将两段代码画成“先 STOP、后释放”的已验证时序)
   ↓
同步等待返回后
   ↓
CM4 初始化 IPC 和 FreeRTOS
   ↓
双核应用正常运行

也可以压缩成一张总图:

┌────────┐
│芯片复位 │
└───┬────┘
    ↓
┌──────────────┐
│CM7 Bootloader│
│ 读取 EEPROM   │
│ 选择 CM7 A/B  │
└───┬──────────┘
    ↓
┌──────────────┐
│镜像与向量表检查│
└───┬───────┬──┘
    │有效    │无效
    ↓        ↓
┌────────┐  ┌──────────────┐
│跳转 CM7│  │停留 Bootloader│
│应用    │  └──────────────┘
└───┬────┘
    ↓
┌────────────────────┐
│ CM7 应用            │
│ 清空 SRAM4          │
│ 选择 CM4 A/B        │
│ 写入 handoff 信息   │
│ 使能 CM4 BOOT_C2    │
└───┬────────────────┘
    ↓
┌───────────────┐
│ CM4 Boot Stub │
│ 读取 SRAM4     │
│ 校验交接目标   │
└───┬───────────┘
    ↓
┌──────────────┐
│检查 CM4 向量表│
└───┬───────┬──┘
    │有效    │无效
    ↓        ↓
┌────────┐ ┌──────────┐
│跳转 CM4│  │停留 Stub │
│应用    │  └──────────┘
└───┬────┘
    ↓
┌──────────────┐
│ CM4 应用      │
│ D2 STOP 等待  │
│ 同步等待返回后 │
│ 初始化 IPC    │
│ 启动 FreeRTOS │
└──────────────┘

上图仅概括启动职责。当前 CM7 在配置时钟后、使能 BOOT_C2 前释放 HSEM;CM4 APP 稍后执行 STOP/WFE,因此不能从图中推断“CM4 先进入 STOP,CM7 再释放 HSEM”。等待能否返回取决于实际启动状态和事件条件。

EEPROM 运行流程图:

Bootloader  ──读──→  EEPROM  ←──写──  APP (OTA 代码)
     │                                          │
     └──试启动扣减/回退时也写──┘                  └── set_pending / commit_pending
  • CM7 Bootloader 在首次建立默认记录、启动时 KEY0 恢复登记、CM7 试启动扣减,以及取消被拒绝或耗尽的 pending 时写 EEPROM。
  • CM7 APP 处理 CM4 trial 扣减/回退,并提交 CM7、CM4 的启动确认记录;CM4 不直接管理 EEPROM。
  • OTA 模块在空闲槽写入完成并校验后,调用目标核的 set_pending 接口。正常无 pending、无升级且未触发恢复登记时,启动选槽通常只读。
  • 文中的“一次写入”指一次 boot_eeprom_save 成功保存记录,底层会拆成多次 EEPROM 页写并读回,不是单个 I2C 事务。

Bootloader 的长按 KEY0 恢复登记:

启动检查阶段检测到 KEY0(PA8)持续为高两秒后,Bootloader 调用 boot_key0_recover,重新登记现有 CM7 双槽镜像。该入口要求已有合法 EEPROM 记录,且 CM7 A/B 两槽向量表均通过检查;条件不满足则拒绝恢复。

恢复登记会重算 CM7 两槽完整 384 KiB 的 CRC 和长度,清零两槽版本号,将 CM7 confirmed 设为 A、清除 pending 标志和 trial,保存并读回验证,同时保留 CM4 元数据。提前松开按键会取消操作。

该入口用于人工确认镜像来源后,在 ST-Link 重刷导致旧长度/CRC 记录失效时重新建立 CM7 镜像记录。它不修复损坏的固件,也不验证发布来源;直接恢复 confirmed=A 与正常 OTA 的 pending 试启动流程不同。恢复 CRC 覆盖整个槽,正常 OTA 则使用发布 BIN 的真实有效字节范围。

4.1 作用

EEPROM 保存断电后仍需使用的启动记录。双备份轮换写让一次更新中断后仍有机会读取上一份有效记录,回退还依赖旧镜像可用和系统能够重新进入启动链。

作用 说明
记录启动状态 保存 confirmed(已确认正式槽)、pending(候选槽)与 trial;本工程选择 EEPROM,也可另行设计 Flash 保留区等持久化方案
防变砖(试启动回退) OTA 后先标记 pending + 3 次试启动额度;新固件跑不起来就没机会 commit,3 次用尽自动退回旧版本
断电恢复 双备份 Copy0/Copy1 + seq 轮换写 + CRC32:已有有效副本且轮换写另一份时中断,可在重启后使用未被改写的旧副本;首次建立记录时还没有旧副本
镜像元数据台账 记录 A/B 槽的版本号、大小、CRC32,供升级判断与日志核对
双核共用 一份结构同时存 CM7 和 CM4 的 A/B 槽状态,两核升级状态统一管理

EEPROM 不可用时,已确认版本和试启动记录不能可靠保存。当前代码以默认 A 为首选,并可能继续按镜像检查结果兜底选择 B;两槽均不可用时无法正常启动。因此不能描述为“永远固定启动 A”或保证设备仍完全可用。

4.2 芯片与布局

BL24C16F,I2C4,2KB芯片:

0x0000 ~ 0x00FF   Copy0  (256B)   ← 启动元数据备份 0
0x0100 ~ 0x01FF   Copy1  (256B)   ← 启动元数据备份 1
0x0200 ~          用户区(APP 自检/业务参数,Boot 不管)

4.3 主要元数据

字段 含义
confirmed_slot 已转正的槽;无有效 pending 时优先选择,仍须通过镜像检查
pending_slot + flags OTA 写了新镜像后标记的"待验证槽"
boot_trial 试启动剩余次数(初始 3)
app_version/size/crc[] 各槽镜像信息
seq + crc32 双备份新旧裁决 + 完整性校验

4.4 EEPROM 运行流程

4.4.1 Bootloader 侧(CM7 Bootloader,main 函数)

状态识别依据(日志文字以当前源码为准) 场景 本次 EEPROM 写入
EEPROM empty/invalid + default metadata saved ①首次上电 1 次(默认值→Copy0)
只有 EEPROM ready + 状态行, 无 save ②正常开机 0 次
boot trial before prepare 显示试启动前次数 ③候选有效且 trial>0 1 次(扣减后保存,成功才启动候选)
不再选择候选,检查 pending/trial 记录 ④trial=0 或候选被拒绝 通常 1 次(清 pending);扣减保存失败可能产生额外保存尝试
EEPROM 探测失败日志;仍可打印内存默认/零值状态 ⑤探测失败 0 次成功保存
no valid APP (tried slot ...), stay in Boot ⑥停留 Boot 通常 0 次

下面按默认记录、正常启动、试启动、回退、EEPROM 失败和无有效槽分别说明;结合日志、EEPROM 记录和实际运行槽判断启动状态。

4.4.1.1 场景① 首次上电(全新芯片 / 双备份全损坏)

触发条件:芯片从没写过;或 Copy0、Copy1 两份 magic/CRC 全部校验失败。
识别特征:串口出现 EEPROM empty/invalid, use defaults 和 EEPROM default metadata saved。

上电/复位
   │
   ▼
[1] MX_I2C4_Init()               I2C4 总线就绪
   │
   ▼
[2] bsp_eeprom_init()
   │  BL24C16F_Init + IsReady → 有 ACK ✓
   ▼
[3] boot_eeprom_init()          s_ee_ready=1
   │
   ▼
[4] boot_eeprom_load(&ee)
   │  ├─ 先填默认值保底: confirmed=A pending=A trial=0 flags=0 seq=1
   │  ├─ 读 Copy0@0x0000 → 全 0xFF → magic 不符 → 无效
   │  ├─ 读 Copy1@0x0100 → 全 0xFF → 无效
   │  └─ ⇒ 双份无效 → 返回 1
   ▼
[5] ee_load_ret==1  ⇒ 进入首次落盘分支
   │  boot_eeprom_save(&ee)
   │  ├─ seq: 1 → 2, 重算 CRC
   │  ├─ 两份都无效 → target = Copy0
   │  └─ 写入 sizeof(boot_eeprom_meta_t)=72 字节,并读回验证 ✓
   ▼
[6] 打印状态
   │  CM7 EE: confirmed=0 pending=0 trial=0 flags=0x00 seq=2
   ▼
[7] boot_eeprom_prepare_boot(&ee)
   │  ├─ flags=0 → pending 分支直接跳过(即使 pending 值是 A)
   │  ├─ confirmed=0(A) 且 A 槽向量表有效 → slot = A
   │  └─ 不是试启动 → EEPROM 不再写
   ▼
[8] boot_app_is_valid(0x08020000) ✓
   │
   ▼
  跳转 A 槽 APP ✅   本次开机 EEPROM 写了 1 次(Copy0)

EEPROM 变化:无数据 → {confirmed=A, pending=A, flags=0, trial=0, seq=2}(落在 Copy0)。
之后:只要不升级,以后每次开机都走场景②。

4.4.1.2 场景② 日常正常开机(无 OTA 进行中)

触发条件:双备份里有合法数据,且 flags.PENDING=0。这是绝大多数开机走的路径。
识别特征:打印的 flags=0x00,且 EEPROM 相关日志只有 EEPROM ready 和状态行,没有 save。

上电/复位
   │
   ▼
[2] bsp_eeprom_init() ✓
   ▼
[4] boot_eeprom_load(&ee)
   │  ├─ 读 Copy0 → 合法 (seq=5)
   │  ├─ 读 Copy1 → 合法 (seq=7)
   │  └─ ⇒ 双份有效 → 取 seq 大的 Copy1 → 返回 0
   ▼
[5] ee_load_ret==0  ⇒ 跳过首次落盘       条件不成立
   ▼
[6] 打印状态   confirmed=B pending=B trial=0 flags=0x00 seq=7
   ▼
[7] boot_eeprom_prepare_boot(&ee)
   │  ├─ flags=0 → 不看 pending           ← 本次的关键分支
   │  ├─ confirmed=B<2 且 B 镜像长度、向量表与 CRC 有效 → slot = B
   │  └─ 非试启动 → EEPROM 一字节不写
   ▼
[8] boot_app_is_valid(0x080A0000) ✓
   │
   ▼
  跳转 B 槽 ✅   本次开机 EEPROM 零写入(纯读)

EEPROM 变化:无(seq 不变,仍为 7)。
这就是"静默路径":Boot 对 EEPROM 只读不写,对芯片寿命最友好。

4.4.1.3 场景③ OTA 试启动·次数未用尽(升级进行中)

触发条件:APP 调过 set_pending,flags.PENDING=1、trial>0,且候选长度、向量表与 Flash CRC 校验通过。
前提状态:{confirmed=A, pending=B, flags=1, trial=3}(APP 写完 B 槽镜像后复位)。

上电/复位
   │
   ▼
[4] boot_eeprom_load(&ee)
   │  └─ ⇒ 取 seq 大者 → ee = {confirmed=A, pending=B, flags=1, trial=3}
   ▼
[5] ee_load_ret==0  ⇒ 不落盘
   ▼
[6] 打印状态   confirmed=0 pending=1 trial=3 flags=0x01
   ▼
[7] boot_eeprom_prepare_boot(&ee)
   │  ├─ 选择链:
   │  │   ① flags、槽号、trial、长度、向量表和 Flash CRC 均通过 ⇒ 选 B
   │  │      (候选无效则进入清 pending/回退路径,不保留无效 pending)
   │  │
   │  ├─ 判定为试启动 (slot == pending && flags 置位):
   │  │   ├─ trial=3 > 0 → trial-- ⇒ trial = 2
   │  │   ├─ 保留 pending;即使本次扣到 0,也仍允许本次候选启动
   │  │   └─ boot_eeprom_save() 写入并读回;失败则回退,不启动候选
   │  │        seq: 7 → 8
   │  │        轮换写入较旧的那份(另一份保持旧状态作保底)
   │  └─ 返回 slot = B
   ▼
[8] boot_app_is_valid(B 槽地址) ✓
   │
   ▼
  跳转 B 槽(新版本试用)✅   本次 EEPROM 写 1 次

EEPROM 变化:{confirmed=A, pending=B, flags=1, trial=3, seq=7} → {…, trial=2, seq=8}。
后续分叉(决定接下来走场景②还是④):

跳进 B 槽之后
   │
   ├─ B 跑通, 调 commit_pending ──→ confirmed=B, flags=0, trial=0
   │      └─ 复位后 ⇒ 走场景② (新版本转正, 圈闭合)
   │
   ├─ B 挂了,保存的 trial 仍大于 0 → 复位后再试(2→1、1→0)
   │
   └─ B 挂了,保存的 trial 已为 0 → 下一次复位走场景④(回退)

4.4.1.4 场景④ OTA 试启动·次数用尽回退(升级失败保护)

触发条件:本次进入启动处理时 flags.PENDING=1 且 trial=0。
前提状态:三次候选启动都未提交;第三次启动前已经将 trial 从 1 保存为 0,但那次仍然进入了候选。

上电/复位
   │
   ▼
[4] boot_eeprom_load(&ee)
   │  └─ ⇒ ee = {confirmed=A, pending=B, flags=1, trial=0}
   ▼
[6] 打印状态   confirmed=0 pending=1 trial=0 flags=0x01
   ▼
[7] boot_eeprom_prepare_boot(&ee)
   │  ├─ pending=B 因 trial=0 被拒绝;选择时暂时排除被拒候选 B
   │  │
   │  ├─ 回退记账:
   │  │   ├─ 保持 trial=0,不再扣减
   │  │   ├─ 先在保留排除条件时选择正式槽/其他有效槽,再清 pending
   │  │   │    flags &= ~0x01      → flags = 0     清升级标记
   │  │   │    pending = confirmed    → pending = A   回填
   │  │   │    slot 已在清标志前选为 A,本次启动老版本
   │  │   │    A 槽镜像有效? ✓ → 返回 A
   │  │   │       (若无其他有效槽则返回 255,本次不重新选择被拒的 B)
   │  │   └─ boot_eeprom_save() 落盘
   │  │        seq: 8 → 9
   │  └─ 返回 slot = A
   ▼
[8] boot_app_is_valid(A 槽地址) ✓
   │
   ▼
  跳转 A 槽(老版本救场)✅   本次 EEPROM 写 1 次

EEPROM 变化:{confirmed=A, pending=B, flags=1, trial=0, seq=8} → {confirmed=A, pending=A, flags=0, trial=0, seq=9}。
之后正常优先启动 A。清 pending 不是永久拉黑 B;后续没有 pending 时,如果正式槽失效,B 仍可能参与兜底选择。
回退依赖系统能够再次复位、EEPROM 状态可持久化以及旧槽仍可启动;trial 不能主动复位卡死的应用。候选镜像损坏或 trial 保存失败也会走回退,而不必等待三次。

4.4.1.5 场景⑤ EEPROM 芯片探测失败(硬件不在/总线故障)

触发条件:BL24C16F_IsReady 收不到 ACK(芯片没贴、I2C 接线问题、地址不对)。
识别特征:串口出现 EEPROM not ready, status=…。

上电/复位
   │
   ▼
[2] bsp_eeprom_init()
   │  ├─ BL24C16F_Init(&s_eeprom, &hi2c4)
   │  └─ BL24C16F_IsReady → 无 ACK ⇒ return -1
   ▼
[初始化失败] 只跳过 boot_eeprom_init/load 与首次落盘
   │  boot_eeprom_init / load / 首次落盘 全部不执行
   │  ee 保持 static 零值: {confirmed=0(A), pending=0, flags=0, trial=0, seq=0}
   │  后续独立的槽检查和元数据打印仍会执行;打印的是内存零值,不是有效 EEPROM 记录
   ▼
[7] boot_eeprom_prepare_boot(&ee)    仍会执行, 用的是零值 ee
   │  ├─ flags=0 → 不看 pending
   │  ├─ confirmed=0 → 查 A 槽向量表 ✓ → slot = A
   │  └─ 非试启动 → 不写(也写不了)
   ▼
[8] boot_app_is_valid(A) ✓
   │
   ▼
  若 A 有效则跳转 A;A 无效时还会尝试 B;均无效则停留 Boot

后果:本次无法持久化 pending、commit 和 trial;可能靠有效的 A/B 镜像继续启动,但不能保证启动上次确认的版本,也不能保证设备功能完整。
排查方向:量 PD12(SCL)/PD13(SDA) 电平、确认 BL24C16F 供电、核对 I2C 从地址。

4.4.1.6 场景⑥ 选不出能启动的槽 / 跳转复核失败(停留 Boot)

触发条件:CM7 没有可启动镜像,或选中 CM7 槽在跳转前复核失败;不要求 CM4 两槽也同时损坏。
识别特征:串口出现 no valid APP (tried slot …),之后不再有任何输出(死在 while(1))。

上电/复位
   │
   ▼
[4] load 正常(或走场景⑤零值)
   ▼
[7] boot_eeprom_prepare_boot(&ee)
   │  选择链逐级尝试, 全部落空:
   │   ① pending 不满足额度、长度、向量表或 CRC 条件 → 拒绝
   │   ② 正式槽与其余允许兜底的槽均不满足镜像检查 → 无可用槽
   │   ③ ⇒ 返回 255;地址换算返回无效地址,跳转检查拒绝
   ▼
[8] boot_app_is_valid(app_base) ✗
   │  ├─ 打印: "no valid APP (tried slot x @ 0x…), stay in Boot."
   │  ├─ 打印 A/B 两槽的 SP / Reset 原始值, 供人工比对
   │  └─ 不调 boot_jump_to_app
   ▼
  while(1) {}  ★ 停留在 Bootloader
    (可通过 SWD 重新烧写有效 APP 镜像,并核对 EEPROM 记录后复位)

EEPROM 变化:没有 pending 时通常不写;存在被拒绝的 pending 时会尝试清标志并保存。只有候选确实被选中且 trial>0 时才尝试保存扣减次数。

4.4.1.7 场景⑦ 变体——读到 v1 旧版数据的迁移开机

这不是独立场景,而是在场景②/③的 load 内部发生的兼容分支:

boot_eeprom_load 内部
   │
   ├─ 读某份备份 → 按 v2 校验: struct_ver==2 ?  ✗ (读到的是 1)
   ├─ 改按 v1 校验: magic ✓ ver==1 ✓ length ✓ 槽号 ✓ CRC ✓
   └─ ⇒ boot_ee_upgrade_v1_to_v2: 仅在内存里补上 CM4 默认字段, 重算 CRC
        ⚠ 不回写 EEPROM
        └─ 之后一旦发生任何 save (场景①落盘 / 场景③扣减 / APP调commit),
           这份数据就以 v2 结构落盘, 迁移自动完成

特征:开机日志正常,但 EEPROM 里仍是 ver=1 的原始数据,直到下次写入才升到 v2。

4.4.2 APP 侧(OTA 时由 APP 调用,与 Bootloader 侧闭环)

状态识别依据(APP 日志文字以当前源码为准) 场景 对应 EEPROM 更新
两核均无 pending,CM7 无 committed 行 ①正常开机 0 次
EEPROM meta empty/invalid, using defaults (ret=1) ②首次/空盘 0 次(APP 不落盘)
CM7 OTA pending committed -> confirmed=1 ③CM7 试启动转正 CM7 commit 成功保存 1 次
CM7 flags=0x01,pending≠running_slot ④跳过 CM7 commit CM7 commit 不写;CM4 记账独立判断
检查 CM4 pending/trial、handoff 槽及运行报告 ⑤CM4 试启动、回退与转正 扣减或清 pending 各需保存;后续确认另需保存
CM7 EEPROM init fail, CM4 default Slot A ⑥探测失败 0 次

Boot 侧负责 CM7 选槽与 trial 记账,APP 侧负责 CM4 选槽/记账及双核候选提交。当前 OTA 模块将镜像写入目标核的空闲槽,核对发布文件 CRC,并在 set_pending 内检查实际 Flash 镜像后保存候选记录;长度与 CRC 使用发布 BIN 的真实有效字节范围,不是固定覆盖整个 384 KiB 槽。登记成功后系统复位,启动链按 pending 记录试运行新槽。

上表按对应操作说明写入;同一次启动若同时涉及两核状态更新,不能把某一行的次数当成整个 APP 的总写入次数。CM4 启动确认还会在调度器启动后由管理任务执行。

4.4.2.1 场景① 日常正常开机(最常见,EEPROM 零写入)

触发条件:双备份合法,flags.PENDING=0 且 cm4_flags 也无 pending。
前提状态:{confirmed=A, pending=A, flags=0, cm4_confirmed=A, cm4_flags=0}。

main()
  │
  ├─ running_slot = boot_get_running_slot() → A          (假设 Boot 起的是 A)
  ├─ 预设 cm4_slot=A, ee_ok=0
  │
  ├─ bsp_eeprom_init ✓
  ├─ boot_eeprom_init ✓
  ├─ boot_eeprom_load(&ee) → ret=0, ee=裁决结果
  ├─ ee_ok = 1
  ├─ 打印: CM7 running_slot=0 | EE confirmed=0 pending=0 flags=0x00
  │        CM4 EE confirmed=0 pending=0 flags=0x00
  │
  ├─ prepare_cm4_boot(&ee):
  │    选择链 ① cm4_flags=0 → 不看 cm4_pending
  │           ② cm4_confirmed=A 且镜像检查通过 → cm4_slot=A
  │    非试启动 → EEPROM 一字节不写
  │
  ├─ cm4_boot_handoff_write(0x08120000)   SRAM4 写 A 槽地址+magic
  ├─ EnableBootCore(C2)                   CM4 Stub 起跳 A
  │
  ├─ ETH/IPC/SDRAM/LCD/摄像头 初始化
  │
  ├─ commit 判断: flags=0 → 条件不成立, 跳过      ← 关键分支
  │
  └─ 启动 FreeRTOS 调度器 ✅   本次 EEPROM 纯读

EEPROM 变化:无。CM4 侧:Stub 从 SRAM4 读到 magic+0x08120000 → 跳 A 槽。

4.4.2.2 场景② 首次上电 / 双备份无效(APP 侧只 WARN,不落盘)

触发条件:EEPROM 全空/全坏,且 Bootloader 侧还没来得及写默认值(例如直接烧 APP 测试、或 Boot 的首次落盘也失败了)。

main()
  │
  ├─ bsp_eeprom_init ✓              (芯片在, 只是没数据)
  ├─ boot_eeprom_init ✓
  ├─ boot_eeprom_load(&ee)
  │    └─ 双份校验全失败 → ee = 内存默认值
  │       {confirmed=A, pending=A, flags=0, cm4_confirmed=A, cm4_flags=0}
  │       return 1
  ├─ ee_load_ret=1 → 仅 LOG_WARN: "EEPROM meta empty/invalid, using defaults"
  │    ⚠ 注意: 这里没有 Boot 侧那段 "save 默认值" 的分支
  │       APP 侧不负责建立 EEPROM 基线, 落盘是 Bootloader 的职责
  ├─ ee_ok = 0                      (load 返回 1,只有返回 0 才表示合法记录)
  │
  ├─ prepare_cm4_boot(&ee):
  │    cm4_flags=0 → confirmed=A → 查 A 槽向量表 → cm4_slot=A
  │    不写 EEPROM
  ├─ handoff(A) → EnableBootCore → CM4 起跳 A
  ├─ 外设初始化...
  ├─ commit 判断: flags=0 → 跳过
  └─ 调度器启动 ✅   本次启动不保存默认记录(双备份校验失败)
EEPROM 变化:无(保持空)。

与场景①的区别是 load 返回 1、ee_ok=0 并使用默认记录;能否启动还取决于默认/兜底槽的实际镜像是否有效。
后续:下次复位时 Bootloader 侧若正常,会在它自己的 main 里落盘默认值(Boot 侧场景①),把基线补上。

4.4.2.3 场景③ CM7 试启动成功 → commit 转正

触发条件:Boot 侧试启动了 pending 槽,本次 running_slot == pending_slot,并且初始化调用流程执行到提交位置而未复位。部分初始化失败只打印日志,因此执行到这里不等于所有外设均已初始化成功。
前提状态:{confirmed=A, pending=B, flags=1, trial=2、1 或 0},running_slot=B;最后一次允许启动的候选进入 APP 时 trial 可以已为 0。

main()
  │
  ├─ running_slot = boot_get_running_slot() → B          ← 我就是那个"新版"
  │
  ├─ bsp_eeprom_init ✓ → load ✓
  │    ee = {confirmed=A, pending=B, flags=0x01, trial=2, ...}
  │    (trial 已在 Boot 侧 prepare_boot 时扣过一次)
  │
  ├─ 打印: CM7 running_slot=1 | EE confirmed=0 pending=1 flags=0x01
  │
  ├─ prepare_cm4_boot(&ee)         ← 只管 CM4 字段, 与 CM7 pending 无关
  │    cm4 无 pending → 起 cm4_confirmed → 不写 EEPROM
  │
  ├─ handoff → EnableBootCore → CM4 启动
  │
  ├─ 外设初始化链(这段跑挂任何一步都可能 IWDG 复位 → 回 Boot 再扣 trial):
  │    eth_phy_init → app_ipc_cm7_init → sdram_init
  │    → lcd_init → app_ov5640_init
  │
  ├─ ★ commit 判断:
  │    flags & PENDING ?          ✓ (0x01)
  │    pending_slot == running_slot ?  ✓ (B == B)
  │    ⇒ boot_eeprom_commit_pending(&ee)
  │         confirmed = pending = B
  │         trial = 0
  │         flags &= ~PENDING → 0
  │         boot_eeprom_save(): seq++, 轮换写较旧份
  │    ├─ 返回 0:保存并读回成功,打印 committed,B 已转正
  │    └─ 返回失败:打印 commit failed,本次提交未获确认
  │
  ├─ 喂狗
  └─ 继续启动调度器;下一次复位以重新加载的有效 EEPROM 副本决策

保存并读回成功时,EEPROM 变化为:{confirmed=A, pending=B, flags=1, trial=2, seq=N} → {confirmed=B, pending=B, flags=0, trial=0, seq=N+1},之后在镜像有效的前提下优先启动 B。

保存或读回失败时不能宣称 B 已转正,也不能断言 EEPROM 必然保持旧记录:写入可能未完成,也可能已经写入但读回失败。当前 commit 函数先修改内存字段再保存,失败时不恢复内存,因此内存中的 confirmed/flags 不能代替持久化证据;下次启动重新选择有效 EEPROM 副本。
当前代码在初始化调用流程结束、调度器启动前尝试 commit,并检查保存结果;本文按现有位置说明状态变化,不据此声称已证明 RTOS 或所有业务持续健康。未提交且发生系统复位时,Bootloader 才会继续试启动或回退。

4.4.2.4 场景④ CM7 pending 存在但不满足 commit 条件(跳过,留给 Boot)

触发条件:flags.PENDING=1,但 pending_slot != running_slot,例如调试器直接启动 A,而 EEPROM 仍有 pending=B。正常 Boot 流程会对无效/耗尽的候选尝试清 pending;若清除保存成功,不会把原来的无效 pending 状态交给 APP。保存失败时则仍可能读到旧记录。

main() ... 到 commit 判断点:
  │
  ├─ flags & PENDING ?          ✓
  ├─ pending_slot == running_slot ?  ✗  (B ≠ A)        ← 条件不成立
  │
   └─ 不调 CM7 commit,不因本分支写 EEPROM
       后果: pending 状态继续留在 EEPROM 里
             由 Bootloader 下次开机接管:
                - 若 B 镜像有效且 trial>0 → 保存扣减后试启动 B
                - 若 B 无效、trial=0 或扣减保存失败 → 尝试清 pending 并回退
CM7 本分支的 EEPROM 更新:无;CM4 试启动记账与确认仍独立执行。

要点:pending_slot == running_slot 这个守卫防止了"A 槽程序误把 B 槽的 pending 提交转正"——commit 必须由"正主"亲自执行。

4.4.2.5 场景⑤ CM4 OTA 试启动记账(APP 代 CM4 扣次数 / 回退)

触发条件:cm4_flags.PENDING=1,由 OTA 模块在 CM4 空闲槽镜像写入并校验通过后调用 boot_eeprom_set_cm4_pending 登记。
前提状态:{cm4_confirmed=A, cm4_pending=B, cm4_flags=1, cm4_trial=3}。

main() → prepare_cm4_boot(&ee)
  │
  ├─ 选择链:
  │   ① cm4_flags、trial>0、槽号、长度、向量表及 Flash CRC 均通过 → 选 B
  │
  ├─ 判定为试启动 (slot == cm4_pending):
  │   ├─ cm4_trial 3→2
  │   ├─ 保留 pending;从 1 扣到 0 也仍允许本次启动
  │   └─ boot_eeprom_save() 写入并读回成功才返回 B;失败则回退
  │
  ├─ 返回 B → cm4_base = 0x081A0000
  ├─ handoff_write(0x081A0000) → EnableBootCore
  │    └─ CM4 Stub 读 SRAM4 → 跳 B 槽 CM4 应用
  │
  ├─ 外设初始化...
  ├─ commit 判断: 这里只处理 CM7 的 flags, 与 cm4_flags 无关 → 跳过
  └─ 调度器启动

CM4 的转正由 CM7 管理任务完成,提交接口为 boot_eeprom_commit_cm4_pending:

CM4 启动 IPC 任务
   ↓
根据自身向量表地址识别实际运行槽
   ↓
将运行标记写入 SRAM4 的 0x3800F020
   ↓
CM7 HTTP 管理任务调用 app_http_ota_poll
   ↓
OTA 会话空闲,且读到合法 CM4 运行槽?
   ├─ 否 → 等待后续检查
   └─ 是 → 加载 EEPROM
              ├─ 无 CM4 pending → 结束本轮启动确认检查
              ├─ pending 与实际运行槽不同 → 不提交
              └─ pending 与实际运行槽相同 → commit_cm4_pending
                     ├─ 保存并读回成功 → confirmed=实际运行槽,清 pending/trial
                     └─ 失败 → 保留后续重试机会

运行标记说明 CM4 已执行到发布标记的任务位置,不代表所有业务已持续健康。CM7 在初始化调用流程结束、调度器启动前确认自身候选;CM4 在启动任务发布运行槽后,由 CM7 管理任务确认。两个确认时点不同,但都必须匹配实际运行槽,不能仅凭 EEPROM 的 confirmed 字段推断当前运行位置。

失败路径与 CM7 对称:在触发整个启动链重新执行的系统复位后,CM7 APP 的 prepare_cm4_boot 按 3→2、2→1、1→0 保存并允许本次候选启动;再次启动时读到 0 才取消 pending、回退正式槽。CM4 局部故障本身不等于 CM7 已重新执行该流程,也不形成永久黑名单。
EEPROM 变化:每次试启动 cm4_trial-- + seq++;回退时清 cm4_flags;候选确认保存成功时,cm4_confirmed 更新为实际运行槽,cm4_flags 的 pending 位清除、cm4_trial=0,seq 再递增。

4.4.2.6 场景⑥ EEPROM 探测失败(按默认记录尝试启动,无法持久化记账)

触发条件:BL24C16F_IsReady 无 ACK(芯片缺失/I2C 故障)。

main()
  │
  ├─ running_slot = boot_get_running_slot()          ← 仍然有效, 不依赖 EEPROM
  ├─ 预设 cm4_slot=A, cm4_base=0x08120000, ee_ok=0
  │
  ├─ bsp_eeprom_init() ✗ 无 ACK
  │    └─ 记录 EEPROM init fail
  ├─ app_boot_eeprom_load 仍被调用,未就绪时填默认值,返回 1,ee_ok=0
  ├─ prepare_cm4_boot 仍被调用,按默认记录和实际镜像有效性选择 A/B
  │
  ├─ cm4_boot_handoff_write(cm4_base)           ← 写入选择结果
  ├─ EnableBootCore → CM4 Stub 检查并跳转;地址无效则停留
  │
  ├─ 外设初始化链照常
  ├─ commit 判断: 默认记录 ee.flags=0 → 本次不提交
  └─ 若其余启动环节通过,继续进入调度器;本次无成功 EEPROM 保存

后果:不能可靠保存双核升级状态;是否可继续运行取决于镜像与其他启动条件。CM4 通常首选 A,但不是永远固定 A,不能保证设备完全可用。
排查:同 Boot 侧——量 PD12/PD13、查 BL24C16F 供电与 I2C 从地址。

5. Bootloader 侧程序

5.1 CM7

这部分主要做这些事情

初始化硬件
    ↓
读取 EEPROM 中的启动元数据
    ↓
检查槽向量表,处理启动长按 KEY0 的 CM7 恢复登记
    ↓
判断 CM7 Slot A/B 哪个可以启动
    ↓
选槽时检查长度、向量表和 Flash CRC;跳转前再次复核向量表
    ↓
关闭当前 Bootloader 运行环境
    ↓
设置应用的 VTOR 和 MSP
    ↓
跳转到应用 Reset_Handler

5.1.1 Boot 相关代码文件

image
image
image

文件 类型 职责 核心内容 谁用它
boot_config.h 头文件 全局启动配置常量 BOOT_FW_TAG 固件版本串;BOOT_JUMP_FLAG_ADDR=0x2001FFF0、BOOT_JUMP_MAGIC=0xB00710AD;跳 APP 前向 DTCM 尾部写入标记,但当前应用 SystemInit 未读取该标记 main.c、boot_jump.c
boot_eeprom.h 头文件 EEPROM 元数据"契约" boot_eeprom_meta_t 结构(magic "EEMA"、结构版本 v2、CM7/CM4 各自的 confirmed/pending/trial/flags/版本/尺寸/CRC、seq、crc32);双备份地址 0x0000/0x0100;全部 API 声明 main.c、CM7 APP 的 OTA/启动管理代码
boot_eeprom.c 源文件 EEPROM 状态机核心 init/load/save:双备份读写、v1→v2 迁移、记录 CRC、seq 轮换和写后读回;select/prepare_boot:完整镜像校验、选槽、试启动扣减/回退;set/commit_pending:候选登记与转正;CM4 有对应接口 CM7 Bootloader、CM7 APP
boot_jump.h 头文件 跳转接口声明 boot_app_is_valid()、boot_jump_to_app() main.c
boot_jump.c 源文件 跳转执行与现场清理 boot_app_is_valid: 地址→槽映射后再查向量表;boot_jump_to_app: 清 CM4 启动位(RCC.GCR.BOOT_C2)→关全局中断→停 SysTick→清 NVIC 8 组→写跳转魔数→切 VTOR→换 MSP→bx 到 APP 的 Reset_Handler main.c
bl24c16f.h 头文件 EEPROM 芯片驱动接口 芯片参数:总容量 2048B、页写 16B、块 256B、基础设备地址 0xA0、写周期 5ms;BL24C16F_HandleTypeDef(I2C 句柄+超时);Init/IsReady/Read/Write/ReadByte/WriteByte 声明 bsp_eeprom.c、boot_eeprom.c
bl24c16f.c 源文件 芯片级 I2C 驱动实现 11 位地址拆分:A10~A8 编入 HAL 使用的 8 位设备地址,公式为 0xA0 OR (((addr >> 8) AND 0x07) << 1),得到 0xA0~0xAE 的偶数地址,共 8 块×256B;A7~A0 作字地址;Write 按 16B 页边界拆分并轮询写完成 bsp_eeprom.c、boot_eeprom.c
bsp_eeprom.h 头文件 板级封装接口 bsp_eeprom_init()、bsp_eeprom_handle()、bsp_eeprom_selftest() 三个声明 main.c、boot_eeprom.c
bsp_eeprom.c 源文件 板级绑定与探测 init: 绑定 &hi2c4 (PD12/PD13) + ACK 探测;handle: 暴露全局句柄给上层;selftest: 在用户区 0x0200 写 4 字节读回比对(不碰 Boot 元数据区) main.c、boot_eeprom.c
boot_slot.h 头文件 Flash 分区唯一权威定义 Bank1(CM7): Boot 0x08000000 128K / Slot A 0x08020000 384K / 预留 128K / Slot B 0x080A0000 384K; Bank2(CM4) 完全对称, Stub 在 0x08100000; 各核独立槽索引: CM7=0/1、CM4=0/1; 槽检查 API 声明 boot_eeprom.*、boot_jump.c、main.c、两核 Makefile 的链接脚本对照
boot_slot.c 源文件 槽向量表检查与地址换算 MSP 只按地址最高字节检查区域前缀,不是精确 SRAM 边界/栈对齐检查;Reset 必须在槽内且 LSB=1(Thumb);base_get/end_get:索引→地址;get_slot_index_from_address:地址→槽 boot_eeprom.c、main.c、boot_jump.c
system_stm32h7xx_dualcore_boot_cm4_cm7.c 源文件 CMSIS 系统启动代码 SystemInit:FPU 使能、SEVONPEND、CM7 分支将 RCC 配置复位到默认状态;ExitRun0Mode:供电配置;SystemCoreClock 更新。当前 APP 的对应文件同样没有跳转魔数判断 两核 startup 汇编在进 main 前调用

注意:boot_jump.c 会写入魔数 0xB00710AD,但当前 APP 实际编译的 Common/Src/system_stm32h7xx_dualcore_boot_cm4_cm7.c 未读取它,仍执行 CM7 RCC 默认复位流程。不能仅凭 Bootloader 注释就认定应用已经实现标记消费。

main.c (CM7/Core/Src)
  │
  ├── bsp_eeprom_init()          ──→ bsp_eeprom.c ──→ bl24c16f.c ──→ HAL I2C4
  ├── boot_eeprom_init/load/     ──→ boot_eeprom.c ──→ bl24c16f.c
  │     prepare_boot                   │
   │                                    └──→ 镜像长度、向量表与 Flash CRC 校验
  ├── boot_slot_is_valid         ──→ boot_slot.c (宏来自 boot_slot.h)
  ├── boot_slot_base_get         ──→ boot_slot.c(使用 boot_slot.h 的地址宏)
  └── boot_app_is_valid /
      boot_jump_to_app           ──→ boot_jump.c
                                       ├── boot_slot.c
                                       ├── boot_config.h (魔数)
                                       └── RCC/SCB/NVIC 寄存器

system_stm32h7xx_dualcore_boot_cm4_cm7.c ← startup_stm32h7xx.s 调用, 与上面无直接调用关系

bl24c16f 管"怎么跟芯片说话",bsp_eeprom 管"芯片在哪根线上",boot_eeprom 管"EEPROM 里记什么、怎么决策",boot_slot 管"Flash 里分区在哪",boot_jump 管"最后怎么跳",boot_config 管"跳的暗号",system_... 管"上电最早期的硬件初始化"。

5.1.2 main.c 启动逻辑摘录

下面省略时钟配置、日志辅助函数和 boot_key0_recover 的函数体,保留 EEPROM 加载、恢复登记入口、选槽与跳转的关键流程。长按 KEY0 的触发条件与副作用见第 4 章,完整函数定义位于 Bootloader 的 CM7/Core/Src/main.c。

#include "main.h"
#include "dma.h"
#include "i2c.h"
#include "usart.h"
#include "gpio.h"
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include "boot_slot.h"
#include "boot_config.h"
#include "boot_jump.h"
#include "mylog.h"
#include "bsp_eeprom.h"
#include "boot_eeprom.h"

static boot_eeprom_meta_t ee;          // EEPROM 中的 confirmed、pending、试启动次数等记录
static uint8_t boot_slot;              // 本次选中的 CM7 槽号
static uint32_t app_base;              // 本次选中的 CM7 应用 Flash 起始地址
static volatile uint8_t cm7_slot_a_ok; // CM7 A 槽向量表检查结果
static volatile uint8_t cm7_slot_b_ok; // CM7 B 槽向量表检查结果
static volatile uint8_t cm4_slot_a_ok; // CM4 A 槽向量表检查结果
static volatile uint8_t cm4_slot_b_ok; // CM4 B 槽向量表检查结果

static void boot_key0_recover(void); // 本摘录省略函数体,完整工程中已有定义

int main(void)
{
  int32_t timeout;

  /* 等待 CM4 进入 STOP,超时后继续启动,避免 CM4 未烧录时卡住 */
  timeout = 0xFFFF; // 限制等待次数
  while ((__HAL_RCC_GET_FLAG(RCC_FLAG_D2CKRDY) != RESET) && (timeout-- > 0))
    ; // D2 时钟仍就绪说明 CM4 尚未进入 STOP
  if (timeout < 0)
  {
    /* CM4 未进 STOP;跳转函数随后会禁止 CM4 从固定入口启动 */
  }

  HAL_Init();

  SystemClock_Config();

  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  MX_I2C4_Init();
  /* USER CODE BEGIN 2 */
  printf("\r\n");
  printf("\033[1;36m"); // 加粗;蓝色
  printf(" ____   ___   ___  _____ _      ___    _    ____  _____ ____          \n");
  printf("| __ ) / _ \\ / _ \\|_   _| |    / _ \\  / \\  |  _ \\| ____|  _ \\   \n");
  printf("|  _ \\| | | | | | | | | | |   | | | |/ _ \\ | | | |  _| | |_) |      \n");
  printf("| |_) | |_| | |_| | | | | |___| |_| / ___ \\| |_| | |___|  _ <        \n");
  printf("|____/ \\___/ \\___/  |_| |_____|\\___/_/   \\_\\____/|_____|_| \\_\\ \n");
  printf("\r\n");
  /* 两段预留区大小由相邻应用槽地址算出,仅用于打印分区表 */
  const uint32_t cm7_reserve_size =
      CM7_APP_SLOT_B_BASE - (CM7_APP_SLOT_A_BASE + CM7_APP_SLOT_A_SIZE);
  const uint32_t cm4_reserve_size =
      CM4_APP_SLOT_B_BASE - (CM4_APP_SLOT_A_BASE + CM4_APP_SLOT_A_SIZE);
  printf("BOOT Tag: %s\r\n", BOOT_FW_TAG); // 打印固件标签
  printf("\n");
  /* 打印双核 Flash 分区,便于对照实际烧录位置 */
  printf("============= flash partition table ==============\r\n");
  printf("---------------- CM7 (Bank1, 1MB) ----------------\r\n");
  printf("|     name     |   offset   |    size    | value |\r\n");
  printf("--------------------------------------------------\r\n");
  printf("| bootloader   | 0x%08lx | 0x%08x | %dKB |\r\n", (unsigned long)CM7_BOOT_FLASH_BASE, CM7_BOOT_FLASH_SIZE, CM7_BOOT_FLASH_SIZE / 1024U);
  printf("| slot A       | 0x%08lx | 0x%08x | %dKB |\r\n", (unsigned long)CM7_APP_SLOT_A_BASE, CM7_APP_SLOT_A_SIZE, CM7_APP_SLOT_A_SIZE / 1024U);
  printf("| reserve(S4)  | 0x%08lx | 0x%08x | %dKB |\r\n", (unsigned long)(CM7_APP_SLOT_A_BASE + CM7_APP_SLOT_A_SIZE), (unsigned int)cm7_reserve_size, (int)(cm7_reserve_size / 1024U));
  printf("| slot B       | 0x%08lx | 0x%08x | %dKB |\r\n", (unsigned long)CM7_APP_SLOT_B_BASE, CM7_APP_SLOT_B_SIZE, CM7_APP_SLOT_B_SIZE / 1024U);
  printf("---------------- CM4 (Bank2, 1MB) ----------------\r\n");
  printf("|     name     |   offset   |    size    | value |\r\n");
  printf("--------------------------------------------------\r\n");
  printf("| stub         | 0x%08lx | 0x%08x | %dKB |\r\n", (unsigned long)CM4_BOOT_STUB_BASE, CM4_BOOT_STUB_SIZE, CM4_BOOT_STUB_SIZE / 1024U);
  printf("| slot A       | 0x%08lx | 0x%08x | %dKB |\r\n", (unsigned long)CM4_APP_SLOT_A_BASE, CM4_APP_SLOT_A_SIZE, CM4_APP_SLOT_A_SIZE / 1024U);
  printf("| reserve(S4)  | 0x%08lx | 0x%08x | %dKB |\r\n", (unsigned long)(CM4_APP_SLOT_A_BASE + CM4_APP_SLOT_A_SIZE), (unsigned int)cm4_reserve_size, (int)(cm4_reserve_size / 1024U));
  printf("| slot B       | 0x%08lx | 0x%08x | %dKB |\r\n", (unsigned long)CM4_APP_SLOT_B_BASE, CM4_APP_SLOT_B_SIZE, CM4_APP_SLOT_B_SIZE / 1024U);
  printf("==================================================\r\n");
  printf("USART1 Baudrate: 115200, Pins: PA9/PA10\r\n");  // 打印 USART1 配置
  printf("EEPROM Peripheral: I2C4, Pins: PD12/PD13\r\n"); // 打印 EEPROM 配置
  printf("\r\n");

  int ee_load_ret; // 区分 EEPROM 已有启动记录与首次使用

  if (bsp_eeprom_init() == 0) // 初始化板载 EEPROM;成功返回 0
  {
    boot_eeprom_init();                  // 准备 Boot 元数据的 EEPROM 访问
    ee_load_ret = boot_eeprom_load(&ee); // 从双备份读取启动记录;无有效副本时填入默认值

    if (ee_load_ret == 1) // 无有效启动记录,首次写入默认值
    {
      LOG_INFO("BOOT", "EEPROM empty, saving default metadata"); // 双备份均无效,提示写入首份默认记录

      if (boot_eeprom_save(&ee) == 0) // 将默认启动记录写入 EEPROM
      {
        LOG_INFO("BOOT", "EEPROM default metadata saved"); // 默认元数据落盘成功
      }
      else // 保存失败:EEPROM 掉线或写超时
      {
        LOG_ERROR("BOOT", "EEPROM default metadata save failed"); // 落盘失败:本次仍按内存默认值继续,下次开机重试
      }
    }
  }

  /* 仅检查栈指针和复位入口等向量表条件,不计算整个镜像的 CRC */
  cm7_slot_a_ok = (uint8_t)boot_slot_is_valid(CM7_APP_SLOT_A_INDEX); // 检查 CM7 A 槽向量表
  cm7_slot_b_ok = (uint8_t)boot_slot_is_valid(CM7_APP_SLOT_B_INDEX); // 检查 CM7 B 槽向量表
  if (cm7_slot_a_ok)
    LOG_INFO("BOOT", "CM7 slot A: valid");
  else
    LOG_WARN("BOOT", "CM7 slot A: invalid");
  if (cm7_slot_b_ok)
    LOG_INFO("BOOT", "CM7 slot B: valid");
  else
    LOG_WARN("BOOT", "CM7 slot B: invalid");

  cm4_slot_a_ok = (uint8_t)cm4_boot_slot_is_valid(CM4_APP_SLOT_A_INDEX); // 检查 CM4 A 槽向量表,仅用于打印
  cm4_slot_b_ok = (uint8_t)cm4_boot_slot_is_valid(CM4_APP_SLOT_B_INDEX); // 检查 CM4 B 槽向量表,仅用于打印
  if (cm4_slot_a_ok)
    LOG_INFO("BOOT", "CM4 slot A: valid");
  else
    LOG_WARN("BOOT", "CM4 slot A: invalid");
  if (cm4_slot_b_ok)
    LOG_INFO("BOOT", "CM4 slot B: valid");
  else
    LOG_WARN("BOOT", "CM4 slot B: invalid");

  boot_key0_recover(); // 启动长按 KEY0 可重新登记 CM7 双槽并保存 EEPROM

  LOG_INFO("BOOT", "CM7 EE: confirmed=%u pending=%u trial=%u flags=0x%02X seq=%lu", // 打印 CM7 EEPROM 元数据
           (unsigned int)ee.confirmed_slot,                                         // 激活槽位索引
           (unsigned int)ee.pending_slot,                                        // 待处理槽位索引
           (unsigned int)ee.boot_trial,                                          // 启动试用次数
           (unsigned int)ee.flags,                                               // 标志位
           (unsigned long)ee.seq);                                               // 序列号

  LOG_INFO("BOOT", "CM7 EE ver A=%lu B=%lu | slotA valid=%u slotB valid=%u", // 打印 EEPROM 应用版本
           (unsigned long)ee.app_version[CM7_APP_SLOT_A_INDEX],              // 应用槽位 A 版本号
           (unsigned long)ee.app_version[CM7_APP_SLOT_B_INDEX],              // 应用槽位 B 版本号
           (unsigned int)cm7_slot_a_ok,                                      // 检查槽位 A 是否有效
           (unsigned int)cm7_slot_b_ok);                                     // 检查槽位 B 是否有效

  LOG_INFO("BOOT", "CM4 EE: confirmed=%u pending=%u trial=%u flags=0x%02X | ver A=%lu B=%lu",  // 打印 CM4 EEPROM 元数据
           (unsigned int)ee.cm4_confirmed_slot,                                                // 激活槽位索引
           (unsigned int)ee.cm4_pending_slot,                                               // 待处理槽位索引
           (unsigned int)ee.cm4_boot_trial,                                                 // 启动试用次数
           (unsigned int)ee.cm4_flags,                                                      // 标志位
           (unsigned long)ee.cm4_app_version[CM4_APP_SLOT_A_INDEX - CM4_APP_SLOT_A_INDEX],  // 应用槽位 A 版本号
           (unsigned long)ee.cm4_app_version[CM4_APP_SLOT_B_INDEX - CM4_APP_SLOT_A_INDEX]); // 应用槽位 B 版本号

  /* 按 pending、confirmed、A、B 的顺序选 CM7 槽;试启动可能扣减次数并回退 */
  boot_slot = boot_eeprom_prepare_boot(&ee); // 选择启动槽,并在需要时更新 EEPROM 试启动状态
  app_base = boot_slot_base_get(boot_slot);  // 将槽号换算为应用向量表的 Flash 地址

  if (boot_app_is_valid(app_base)) // 再次确认目标槽地址及向量表符合启动条件
  {
    uint32_t sp = *(__IO uint32_t *)app_base;        // 向量表第 1 项:应用启动时的栈顶 MSP
    uint32_t pc = *(__IO uint32_t *)(app_base + 4U); // 向量表第 2 项:应用的 Reset_Handler 地址

    LOG_INFO("BOOT", "jump slot %u @ 0x%08lX", // 打印要启动的槽号和地址
             (unsigned int)boot_slot, (unsigned long)app_base);
    LOG_INFO("BOOT", "vec SP=0x%08lX Reset=0x%08lX", // 打印应用栈顶和复位入口
             (unsigned long)sp, (unsigned long)pc);
    LOG_INFO("BOOT", "disable CM4 boot, then jump...");                // 提示跳转时将禁止 CM4 从固定入口启动
    boot_jump_to_app(app_base);                                      // 清理 Boot 中断、切换栈和向量表,跳到应用
    LOG_ERROR("BOOT", "ERROR: Jump returned (should never happen)"); // 正常跳转不返回;返回说明跳转异常
  }
  else
  {
    /* 无法启动所选 CM7 槽:打印 A/B 向量表帮助定位,随后留在 Bootloader */
    LOG_ERROR("BOOT", "no valid APP (tried slot %u @ 0x%08lX), stay in Boot.", // 报告失败槽号及地址
              (unsigned int)boot_slot, (unsigned long)app_base);
    LOG_INFO("BOOT", "  A: SP=0x%08lX Reset=0x%08lX valid=%u",              // A 槽栈顶、复位入口及检查结果
             (unsigned long)(*(__IO uint32_t *)CM7_APP_SLOT_A_BASE),        // A 槽向量表第 1 项
             (unsigned long)(*(__IO uint32_t *)(CM7_APP_SLOT_A_BASE + 4U)), // A 槽向量表第 2 项
             (unsigned int)cm7_slot_a_ok);
    LOG_INFO("BOOT", "  B: SP=0x%08lX Reset=0x%08lX valid=%u",              // B 槽栈顶、复位入口及检查结果
             (unsigned long)(*(__IO uint32_t *)CM7_APP_SLOT_B_BASE),        // B 槽向量表第 1 项
             (unsigned long)(*(__IO uint32_t *)(CM7_APP_SLOT_B_BASE + 4U)), // B 槽向量表第 2 项
             (unsigned int)cm7_slot_b_ok);
  }

  while (1)
  {
  }
}

5.2 CM4

这部分主要做这些事:

初始化必要环境
    ↓
从 SRAM4 读取 CM7 写入的交接信息
    ↓
无有效交接 magic 则默认尝试 CM4 Slot A;magic 有效而地址或向量表非法则拒绝跳转
    ↓
检查 CM4 应用向量表
    ↓
设置 CM4 的 VTOR 和 MSP
    ↓
跳转 CM4 应用

5.2.1 Boot 相关代码文件

文件 类型 职责 核心内容
cm4_boot_handoff.h 头文件 定义 CM7->CM4 握手协议 魔数 0x4A344D43(小端字节为 C、M、4、J);握手区在 D3 SRAM: magic@0x3800F010、base@0x3800F014;声明跳转与写入两个接口
cm4_boot_handoff.c 源文件 CM4 Stub 的跳转实现 上电从 D3 SRAM 读 magic+base,校验目标是 A/B 槽且向量表合法后,关中断、切 VTOR/MSP 跳转;无握手时默认跳 A 槽(兼容出厂状态);另提供写握手接口(由应用侧 CM7 调用,本仓库内无调用者)
boot_slot.h 头文件 Flash 分区唯一权威定义 Bank1/CM7: Boot 0x08000000 128K -> A 槽 0x08020000 384K -> 预留 128K -> B 槽 0x080A0000 384K; Bank2/CM4 对称, Stub 0x08100000; 槽索引: CM7=0/1、CM4=0/1; 槽检查 API 声明。两核链接脚本必须与此一致
boot_slot.c 源文件 槽向量表检查与索引↔地址换算(两核共编译) CM7 的 boot_slot_is_valid 和 CM4 的 cm4_boot_slot_is_valid 检查向量表前两项:MSP 仅检查区域前缀,尚无精确 RAM 边界和对齐检查;Reset 检查槽范围和 Thumb 位。这两个函数不计算完整 CRC,完整镜像校验在 boot_eeprom 层执行
system_stm32h7xx_dualcore_boot_cm4_cm7.c 源文件 CMSIS 系统启动代码(两核共编译) SystemInit:FPU、SEVONPEND;RCC 默认复位逻辑只在 CM7 分支执行;ExitRun0Mode:供电配置。当前 APP SystemInit 未读取跳转魔数;由两核 startup 汇编在 main 前调用

CM7 APP 调用 boot_eeprom_prepare_cm4_boot 选择 CM4 槽,再调用 cm4_boot_handoff_write 写入交接信息。Stub 仅在没有有效 magic 时默认尝试 A;magic 有效但 base 或向量表非法时,跳转失败并停留。CM4 的 EEPROM 决策与试启动记账在 CM7 APP 中执行。

5.2.2 main.c 详细代码

#include "main.h"
#include "dma.h"
#include "gpio.h"
#include "cm4_boot_handoff.h"

int main(void)
{
  HAL_Init();

  MX_GPIO_Init();
  MX_DMA_Init();

  /* 不进 STOP:CM7 已在 EnableBootCore 前写好 handoff;
   * App 自己会再进 STOP 等 HSEM。 */
  (void)cm4_boot_handoff_jump_from_d3();
  /* 跳转失败:停在此,便于调试 */
  __disable_irq();

  while (1)
  {
  }
}

6. APP 侧 Boot 程序

6.1 CM7

等待 CM4 进入预期状态
    ↓
初始化 CM7 的 HAL、MPU、Cache
    ↓
清理 SRAM4,配置时钟,然后释放 HSEM(当前源码顺序)
    ↓
初始化外设,读取 EEPROM
    ↓
选择 CM4 应用槽位
    ↓
将 CM4 应用地址写入 SRAM4
    ↓
使能 CM4 BOOT_C2
    ↓
初始化 OpenAMP 和业务系统

6.1.1 Boot 相关代码文件

image
image
image

文件 类型 职责 核心内容 与 Bootloader 侧差异
boot_eeprom.h 头文件 EEPROM 元数据结构与 API 声明 boot_eeprom_meta_t、双备份地址、load/save/select/prepare/set_pending/commit 系列声明;正式槽字段为 confirmed_slot / cm4_confirmed_slot 与 Bootloader 保持二进制协议一致,不以文件哈希作为接口约定
boot_eeprom.c 源文件 APP 的 EEPROM 状态机 load → prepare_cm4_boot(CM4 选槽与 trial 记账);commit_pending(CM7 转正)、commit_cm4_pending(CM4 转正);OTA 模块调用 set_pending 系列登记候选。保存记录包含写后读回,候选包含长度/向量表/CRC 校验 职责与 Bootloader 配套,日志和局部流程有差异
bl24c16f.h 头文件 EEPROM 芯片驱动接口 2048B、页写 16B、设备地址 0xA0~0xAE、写周期 5ms;Init/IsReady/Read/Write等声明 职责或协议对应,具体实现以各工程源码为准
bl24c16f.c 源文件 芯片级 I2C 驱动 11 位地址拆分、16B 页边界自动拆分写、写后轮询写周期 职责或协议对应,具体实现以各工程源码为准
bsp_eeprom.h 头文件 板级封装接口 init / handle / selftest 三接口声明 职责或协议对应,具体实现以各工程源码为准
bsp_eeprom.c 源文件 绑定 I2C4 + ACK探测 + 用户区自检 init 探测芯片应答;handle 暴露句柄给boot_eeprom 层;selftest 在 0x0200用户区读写比对(APP 里当前被注释掉) 逻辑相同;日志标签"BOOT"→"APP",注释风格精简
boot_slot.h 头文件 Flash分区唯一权威定义 Bank1/CM7 与 Bank2/CM4 的 Boot/Stub、A/B 槽、预留区地址;槽索引 CM7=0/1、CM4=0/1 职责或协议对应,具体实现以各工程源码为准
boot_slot.c 源文件 槽向量表检查与运行槽识别 CM7 使用 boot_slot_,CM4 使用 cm4_boot_slot_;当前 boot_get_running_slot 将真实向量表符号 g_pfnVectors 的地址交给 CM7 地址映射函数 该封装只识别 CM7 A/B;CM4 应使用 cm4_boot_get_slot_index_from_address 映射自己的向量表地址,直接调用 CM7 封装会返回 255
cm4_boot_handoff.h 头文件 CM7→CM4 握手协议定义 魔数 0x4A344D43;magic@0x3800F010、base@0x3800F014 (D3 SRAM) APP 放在 Common 供 CM7 使用,地址与 magic 必须与 Bootloader 的 CM4 Stub 一致
cm4_boot_handoff.c 源文件 握手协议实现(APP 使用写入接口) cm4_boot_handoff_write(cm4_base) 写入选定的 CM4 地址及 magic,再使能 BOOT_C2;Stub 读取后校验并跳转 与 Stub 的协议对应,不要求两边文件逐字相同
system_stm32h7xx_dualcore_boot_cm4_cm7.c 源文件 CMSIS 系统启动代码 SystemInit、ExitRun0Mode、SystemCoreClock;当前 APP SystemInit 未读取跳转魔数,CM7 分支仍复位 RCC 配置 核对实际编译文件的行为,不以文件相同或注释作为执行证据

6.1.2 main.c 启动逻辑摘录

以下摘录保留启动相关顺序,省略完整工程中的时钟函数、日志辅助函数等定义;完整工程以源码为准。

#include "main.h"
#include "FreeRTOS.h"
#include "cmsis_os2.h"
#include "dcmi.h"
#include "dma.h"
#include "dma2d.h"
#include "i2c.h"
#include "jpeg.h"
#include "ltdc.h"
#include "lwip.h"
#include "mdma.h"
#include "openamp.h"
#include "usart.h"
#include "gpio.h"
#include "fmc.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include "mpu.h"
#include "bsp_sys.h"
#include "sdram.h"
#include "lcd.h"
#include "app_ipc_cm7.h"
#include "app_ov5640.h"
#include "boot_slot.h"
#include "cm4_boot_handoff.h"
#include "mylog.h"
#include "bsp_eeprom.h"
#include "boot_eeprom.h"
#include "eth.h"
#include "eth_phy.h"
#include "iwdg.h"


/* DUAL_CORE_BOOT_SYNC_SEQUENCE: Define for dual core boot synchronization    */
/*                             demonstration code based on hardware semaphore */
/* This define is present in both CM7/CM4 projects                            */
/* To comment when developping/debugging on a single core                     */
#define DUAL_CORE_BOOT_SYNC_SEQUENCE

#if defined(DUAL_CORE_BOOT_SYNC_SEQUENCE)
#ifndef HSEM_ID_0
#define HSEM_ID_0 (0U) /* HW semaphore 0*/
#endif
#endif /* DUAL_CORE_BOOT_SYNC_SEQUENCE */

void SystemClock_Config(void);
void MX_FREERTOS_Init(void);

static void ipc_share_region_clear(void)
{
  memset((void *)0x38000000, 0, 0x00010000); // 启动前清空 64KB SRAM4;后续会在这里写 CM4 handoff
}

int main(void)
{
#if defined(DUAL_CORE_BOOT_SYNC_SEQUENCE)
  int32_t timeout;
#endif

#if defined(DUAL_CORE_BOOT_SYNC_SEQUENCE)
  /* Wait until CPU2 boots and enters in stop mode or timeout*/
  timeout = 0xFFFF;
  while ((__HAL_RCC_GET_FLAG(RCC_FLAG_D2CKRDY) != RESET) && (timeout-- > 0))
    ;
  if (timeout < 0)
  {
    // Error_Handler();
  }
#endif

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  __enable_irq();
  memory_protection_unit_init(); // 初始化内存保护单元
  scb_cache_init();              // 初始化缓存
  ipc_share_region_clear();      // 先清共享内存,再写入 CM4 启动地址

  SystemClock_Config();
#if defined(DUAL_CORE_BOOT_SYNC_SEQUENCE)
  /* When system initialization is finished, Cortex-M7 will release Cortex-M4 by means of
  HSEM notification */
  /*HW semaphore Clock enable*/
  __HAL_RCC_HSEM_CLK_ENABLE();
  /*Take HSEM */
  HAL_HSEM_FastTake(HSEM_ID_0);
  /*Release HSEM in order to notify the CPU2(CM4)*/
  HAL_HSEM_Release(HSEM_ID_0, 0);
#endif

  MX_GPIO_Init();
  MX_MDMA_Init();
  MX_DMA_Init();
  MX_FMC_Init();
  MX_LTDC_Init();
  MX_JPEG_Init();
  MX_DMA2D_Init();
  MX_DCMI_Init();
  MX_USART1_UART_Init();
  MX_I2C4_Init();
  MX_IWDG1_Init();
  
  {                                               /* 从 EEPROM 读取启动记录,并通知 CM4 Stub 启动哪个应用槽 */
    boot_eeprom_meta_t ee = {0};                        // 保存 EEPROM 中的 confirmed、pending 等启动记录
    uint8_t running_slot = boot_get_running_slot(); // 根据真实向量表地址判断 CM7 位于 A 槽还是 B 槽
    uint8_t cm4_slot = CM4_APP_SLOT_A_INDEX;      // EEPROM 不可用时,CM4 默认从 A 槽启动
    uint32_t cm4_base = CM4_APP_SLOT_A_BASE;      // CM4 默认应用的 Flash 起始地址
    int ee_ok = 0;                                // 记录本次是否读到合法 EEPROM 元数据
    printf("\033[1;36m");                         // 加粗;青色
    printf("    _     ____   ____   _      ___   ____     _     _____  ___   ___   _   _ \n");
    printf("   / \\   |  _ \\ |  _ \\ | |    |_ _| / ___|   / \\   |_   _||_ _| / _ \\ | \\ | |\n");
    printf("  / _ \\  | |_) || |_) || |     | | | |      / _ \\    | |   | | | | | ||  \\| |\n");
    printf(" / ___ \\ |  __/ |  __/ | |___  | | | |___  / ___ \\   | |   | | | |_| || |\\  |\n");
    printf("/_/   \\_\\|_|    |_|    |_____||___| \\____|/_/   \\_\\  |_|  |___| \\___/ |_| \\_|\n");
    printf("\n");
    if (bsp_eeprom_init() == 0)
    {
      (void)boot_eeprom_init();
    }
    else
    {
      LOG_ERROR("APP", "EEPROM init fail");
    }
    /* 无论是否初始化成功,load 都会先填充默认记录。 */
    int load_ret = boot_eeprom_load(&ee);
    ee_ok = (load_ret == 0);
    if (load_ret != 0)
      LOG_WARN("APP", "No valid EEPROM metadata, using defaults ret=%d", load_ret);
    cm4_slot = boot_eeprom_prepare_cm4_boot(&ee);
    cm4_base = cm4_boot_slot_base_get(cm4_slot);
    cm4_boot_handoff_write(cm4_base); // 将 CM4 应用地址写入 SRAM4,供固定 Stub 读取
    LOG_INFO("APP", "CM4 handoff slot=%u base=0x%08lX ee=%d",
             (unsigned int)cm4_slot,
             (unsigned long)cm4_base,
             ee_ok);
    /* 使能 CM4 后,Bank2 固定 Stub 读取 handoff 并跳转到所选应用槽 */
    HAL_RCCEx_EnableBootCore(RCC_BOOT_C2);
    LOG_INFO("APP", "CM4 boot core enabled");
    HAL_IWDG_Refresh(&hiwdg1);
    MX_ETH_Init();
    (void)eth_phy_init();
    app_ipc_cm7_init();
    sdram_init();
    lcd_init();
    app_ov5640_init();
    /* 按现有应用的位置展示提交,具体健康确认策略不在本节展开。 */
    if (((ee.flags & BOOT_EE_FLAG_PENDING) != 0U) &&
        (ee.pending_slot == running_slot))
    {
      int ret = boot_eeprom_commit_pending(&ee);
      if (ret == 0)
        LOG_INFO("APP", "CM7 pending committed -> confirmed=%u", (unsigned int)ee.confirmed_slot);
      else
        LOG_ERROR("APP", "CM7 pending commit failed ret=%d", ret);
    }
    HAL_IWDG_Refresh(&hiwdg1);
  }

  LOG_INFO("CM7", "main() user init OK");


  osKernelInitialize();  /* Call init function for freertos objects (in cmsis_os2.c) */
  MX_FREERTOS_Init();

  osKernelStart();

  while (1)
  {
  }
}

博客导航

博客导航

posted @ 2026-09-29 11:53  膝盖中箭卫兵  阅读(29)  评论(0)    收藏  举报
ORCID iD icon https://orcid.org/0000-0001-5102-772X