【以太网摄像头采集存储终端】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 的主要内存区域
- 内部 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 数据 |
- TCM RAM (紧耦合内存, 零等待, 仅 Cortex-M7)
| 区域 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| ITCM RAM | 0x0000 0000 | 64 KB | 指令紧耦合内存,存放关键实时代码 |
| DTCM RAM | 0x2000 0000 | 128 KB | 数据紧耦合内存,存放关键实时数据/栈 |
- AXI SRAM (D1 域, 高性能)
| 区域 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| AXI SRAM | 0x2400 0000 | 512 KB | 所有主设备可访问(除 BDMA),适合 DMA 缓冲 |
- 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,本质相同。
- D3 域 SRAM (低功耗域)
| 区域 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| SRAM4 | 0x3800 0000 | 64 KB | 低功耗域 SRAM,可由 BDMA 访问 |
| Backup SRAM | 0x3880 0000 | 4 KB | 备份域,VBAT/待机模式下保持 |
- 外部存储器接口 (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 等待函数依赖这一配置。

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 相关代码文件



| 文件 | 类型 | 职责 | 核心内容 | 谁用它 |
|---|---|---|---|---|
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 相关代码文件



| 文件 | 类型 | 职责 | 核心内容 | 与 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)
{
}
}
博客导航
本文来自博客园,作者:膝盖中箭卫兵,转载请注明原文链接:https://www.cnblogs.com/hai-feng-dev/p/23154549

浙公网安备 33010602011771号
https://orcid.org/0000-0001-5102-772X