夜雨聆风学习资料网

ARTICLE · 1083078

Part3:BootLoader与App的Flash分区设计

Part3:BootLoader与App的Flash分区设计

引言

大家好,这里是比特奔流,祝大家中秋节快乐!

上一篇我们已经知道,BootLoader 想要启动 App,必须先找到 App 自己的向量表。

这马上带来一个新的问题:

BootLoader 和 App 到底应该放在 STM32 内部 Flash 的什么位置?

普通 STM32 工程通常把程序链接到 0x08000000,向量表也放在这个起始位置。但现在这里要留给 BootLoader,App 就不能继续放在这里。

很多初学者第一次做 BootLoader 时,可能会直接给 App 随便找一个后面的地址,比如 0x08003200、0x08005000,只要看起来没有和 BootLoader 重叠就行。

但 Flash 分区不能只看“地址有没有重叠”。

因为 Flash 和我们平时使用的 RAM 数组不同,它有自己的擦除结构。后面 BootLoader 更新 App 时,不是想擦哪几个字节就擦哪几个字节,而是必须按照 STM32F103C8T6 规定的 Page 进行擦除。

所以,在修改 App 地址之前,我们先把 STM32F103C8T6 的内部 Flash 真正看明白。

本系列按 BootLoader 24KB、App 40KB 的方案推进。后面的 App 地址修改、BootLoader 跳转、内部 Flash 擦写和 OTA 安装都是这个地址。


STM32F103C8T6 的 Flash 页结构

STM32F103C8T6 型号中的 8 表示它具有 64KB 内部 Flash。主 Flash 地址范围是:

0x08000000 ~ 0x0800FFFF

也就是从 0x08000000 开始,一共 64KB。本文的容量计算统一按 1KB = 1024 Bytes 处理。

如果按照普通数组去理解,很容易想象成:

STM32F103C8T6 主 Flash 连续地址范围

中间就是一整块连续空间,想从哪里切都可以。

从 CPU 的寻址角度来看,它确实是一段连续地址。

但从 Flash 擦除结构来看,它并不是一个整体。

STM32F103C8T6 的 64KB 主 Flash 被均匀分成 64 个 Page,每个 Page 都是 1KB。

查看 ST 的 Flash 编程手册 PM0075 的通用表格,列到了 128KB 型号的 Page 127,而本文使用的 C8 型号只有 64KB,因此有效范围是 Page 0~Page 63:

Page
起始地址
结束地址
大小
Page 0
0x080000000x080003FF
1KB
Page 1
0x080004000x080007FF
1KB
……
……
……
……
Page 23
0x08005C000x08005FFF
1KB
Page 24
0x080060000x080063FF
1KB
……
……
……
……
Page 63
0x0800FC000x0800FFFF
1KB

所以它的内部结构实际上是:

所有 Page 的大小都相同,相邻 Page 的起始地址相差 0x400,也就是 1024 字节。

这也是为什么在 STM32F103C8T6 上设计 BootLoader 分区时,不能简单地:

给 BootLoader 留 23.5KB,剩下全部给 App。

因为 Flash 擦除时只认识 1KB Page,不认识人为画出的半页边界。


Flash 页擦除粒度与分区约束

假设我们在 RAM 中定义一个数组:

/* 一个普通RAM数组 */uint8_t buffer[1024];

如果想修改 buffer[100],直接写入即可:

/* 直接修改数组中的某一个字节 */buffer[100] = 0x55U;

其他 1023 个字节完全不受影响。

因此,RAM 更接近我们平时理解的“想改哪里就改哪里”。

Flash 不一样。

Flash 的读取可以像普通存储器一样按地址访问,但擦除不是按字节进行。

STM32F103C8T6 支持 Page Erase 和主 Flash 整片擦除。我们在更新 App 时应对 App 分区逐页擦除,不能使用会连同 BootLoader 一起清除的主 Flash 整片擦除。

也就是说,如果要擦除 Page 24,0x08006000 ~ 0x080063FF 的整个 1KB 都会一起被擦除。

你不能告诉 Flash:

只擦:0x08006200 ~ 0x080062FF

因为 STM32F103C8T6 根本不存在这种 256 字节擦除粒度。

这件事对 BootLoader 非常重要。

假设 BootLoader 占用:

0x08000000 ~ 0x080061FF

然后 App 从:

0x08006200

开始。

从地址上看,它们完全没有重叠。

但是 0x08006200 位于 Page 24 中,而 Page 24 的范围是:

0x08006000 ~ 0x080063FF

这意味着 Page 24 的前一部分属于 BootLoader,后一部分属于 App。

以后升级 App 时,只要 BootLoader 想擦除 App 的这一部分,就必须把整个 Page 24 擦掉。

结果就是 BootLoader 位于 Page 24 中的代码也会一起消失。

设备很可能直接失去升级能力。

所以,BootLoader 和 App 的分区不能只做到“地址不重叠”,还必须保证它们的擦除区域互不重叠。


App 分区边界与页对齐要求

这里需要把一个经常出现的说法纠正一下。

有人会说:

App 起始地址必须是 Flash Page 的起始地址,否则程序不能运行。

这句话并不准确。

从 Cortex-M3 执行代码的角度,只要链接地址、向量表位置以及 VTOR 对齐等条件满足,App 并不是因为“不在 Page 边界”就一定不能运行。

真正的问题在于 IAP升级时的擦除管理。

对于我们这套 BootLoader 系统来说,App 后续需要被 BootLoader 独立擦除和重新写入。

因此,我们希望 BootLoader 和 App 各自完整占用不同的 Page。

这样升级 App 时,无论擦除多少个 App Page,都不会碰到 BootLoader。

所以更准确的说法应该是:

在本系列这种需要由 BootLoader 独立擦除 App 的设计中,BootLoader 和 App 的分界必须放在 Flash Page 边界。

这不是 CPU 对 App 起始地址的限制,而是 Flash 擦除粒度带来的工程要求。


BootLoader与App的Flash分区方案

现在就可以开始真正划分空间了。

STM32F103C8T6 一共有 64KB Flash,每个 Page 是 1KB。

考虑到后续 BootLoader 还要加入 W25Q64、状态管理、CRC、AES 和 HMAC,本教程先给 BootLoader 预留 24 个 Page:

1KB × 24 = 24KB

因此,本教程把 Page 0~Page 23 划给 BootLoader:

Page 0 ~ Page 23

也就是:

BootLoader区域起始地址:0x08000000结束地址:0x08005FFF总大小:24KB

App 从下一个 Page,也就是 Page 24 开始:

App起始地址:0x08006000

本教程当前采用的内部 Flash 分区为:

注:从这一篇开始,后面的文章统一使用:

BootLoader:0x08000000App:       0x08006000

BootLoader 预留 24KB 的容量规划

如果现在只写一个最简单的 BootLoader,它的功能可能只有:

这种程序可能连 16KB 都用不完。

那为什么不直接把 BootLoader 固定为 16KB?

原因是我们不能只看现在这几篇文章。

按照整个系列的设计,后面的 BootLoader 还会逐渐加入:

  • STM32内部Flash擦写;
  • W25Q64驱动;
  • 升级状态管理;
  • Candidate和Rollback槽管理;
  • CRC校验;
  • AES解密;
  • 固件包解析;
  • 版本判断;
  • 安装失败恢复;
  • 回滚逻辑;
  • 错误处理。

在本教程规划的大纲中,BootLoader 最终还要承担加密固件验证、解密、安装以及失败回滚等功能。

如果这一篇为了省空间,只给 BootLoader 分配 16 个 Page:

BootLoader = 16KBApp起始地址 = 0x08004000

前面几篇可能完全够用。

但等后面加入 AES 和升级状态机以后,16KB 很可能偏紧。STM32F103C8T6 的片内 Flash 总共只有 64KB,因此这里必须在 BootLoader 功能和 App 空间之间做取舍。

如果最终确实需要把 BootLoader 分区从 16KB 扩大到 24KB,App 起始地址就要从:

0x08004000

再次改成:

0x08006000

前面所有与链接地址、向量表、升级地址相关的工程配置都要跟着修改。

所以本教程先给 BootLoader 预留 24KB,也就是 Page 0~Page 23;App 从 Page 24 的 0x08006000 开始。

注意这里也不能理解成:

STM32F103C8T6 的 BootLoader 标准大小就是 24KB。

没有这种标准。

真正的产品是根据最终 BootLoader 的实际代码量、链接生成的 MAP 文件以及后续扩展需求来决定,并且保留一定余量。

如果后面加入全部安全功能后,BootLoader 超过了 24KB,我们会再通过 MAP 文件看看空间用在了哪里,再判断哪些库或功能可以裁剪。

如果裁剪以后仍然放不下,那就需要重新划分 STM32F103C8T6 的内部 Flash,并同步修改 App 链接地址、最大固件长度和所有边界检查。


App 分区容量计算与固件长度校验

BootLoader 使用了前 24KB,那么 App 剩余空间就是:

64KB - 24KB = 40KB

换成地址计算也是一样。

Flash 结束地址的下一字节是:

0x08010000

App 起始地址是:

0x08006000

因此:

App最大空间=0x08010000 - 0x08006000=0x0000A000

换算成十进制:

0xA000 = 40960 Bytes

再除以 1024:

40960 / 1024 = 40KB

因此,在当前分区方案下,App 最大可用 Flash 空间为:

40KB

这里的 40KB 留给的是整个 App 镜像。向量表、代码和只读数据都要占用这段空间;有初始值的变量虽然运行时放在 RAM 中,它们的初始内容也要保存在 Flash 中。

如果 BIN 为了保持地址连续还包含填充字节,这些字节同样会被写入 App 区。因此,检查容量时不能只看函数代码有多大。

如果 BIN 从 App 起始地址连续写入,长度恰好为 40KB,在容量上也是允许的;最后一个字节正好落在 0x0800FFFF。

实际工程中还应该预留一定空间,避免程序后期稍微增加功能就直接顶到分区末尾。

更重要的是,在 OTA 升级时,BootLoader 必须在擦写前检查固件长度,不能只依赖上位机检查。

固件长度:firmware_size ,指最终写入 App 分区的明文 BIN 长度。

后面加入加密时,发送的升级包还会带上包头、认证字段和加密填充,整份包可能比 BIN 更大。因此,外部 Flash 要检查能否保存整份包,内部 App 区则要检查能否放下明文固件,这两个长度不要弄混了。

例如收到一个固件头,其中写着:

firmware_size = 44KB

BootLoader 不应该尝试继续安装。

因为 App 分区只有 40KB。

如果不做长度检查,写入地址最终就会越过:

0x0800FFFF

进入非法地址。

所以后面验证升级包时,可以直接使用:

(firmware_size > 0U) && (firmware_size <= FW_APP_MAX_SIZE)

作为最基本的长度条件之一。长度为 0 的空固件也应拒绝。

假如包头写着 30KB,实际却只收到 20KB,即使声明长度没有超过 40KB,这份固件也不能安装。所以,通过分区容量检查以后,还要核对声明长度与实际镜像长度,再继续检查向量表、固件内容和实际写入范围。


BootLoader 与 App 的分区地址统一管理

做到这里以后,我们已经确定了两个非常重要的地址:

BootLoader起始地址:0x08000000App起始地址:       0x08006000

接下来最容易犯的错误,就是在工程的不同文件中直接写死这些数字。

比如 BootLoader 跳转代码里写:

/* 不推荐:直接写死App地址 */uint32_t app_addr = 0x08006000UL;

Flash 擦除代码里又写:

/* 不推荐:另一个地方再次写死地址 */if (addr >= 0x08006000UL){/* 擦除App区域 */}

以后还可能在升级包检查、CRC计算、BIN长度校验中再次出现同一个数字。

同一个地址分散在多个文件里,只要某一天分区发生变化,就必须全工程搜索替换。

上面的擦除示例还有另一处问题:它只判断地址不小于 App 起点。一个已经超出 App 末尾的地址,同样可能通过这个判断。因此,真实擦写接口还要检查上界和操作长度,不能直接照搬这里的条件。

更麻烦的是,如果漏改一个地方,代码可能依然能够正常编译,却在升级时擦错地址。

因此,分区信息应该集中管理。

可以建立一个公共头文件,例如:

/* flash_layout.h */#ifndef FLASH_LAYOUT_H#define FLASH_LAYOUT_H#include <stdint.h>/* STM32F103C8T6主Flash起始地址 */#define FW_FLASH_BASE_ADDR      (0x08000000UL)/* STM32F103C8T6主Flash结束地址的下一字节 */#define FW_FLASH_END_ADDR       (0x08010000UL)/* BootLoader起始地址 */#define FW_BOOT_BASE_ADDR       (FW_FLASH_BASE_ADDR)/* BootLoader暂定占用24KB:Page 0 ~ Page 23 */#define FW_BOOT_SIZE            (0x00006000UL)/* App起始地址:Page 24起始位置 */#define FW_APP_BASE_ADDR        (FW_BOOT_BASE_ADDR + FW_BOOT_SIZE)/* App结束地址的下一字节 */#define FW_APP_END_ADDR         (FW_FLASH_END_ADDR)/* App最大可用空间:40KB */#define FW_APP_MAX_SIZE         (FW_APP_END_ADDR - FW_APP_BASE_ADDR)#endif /* FLASH_LAYOUT_H */

这样后面的代码不再直接使用:

0x08006000

而是统一使用:

FW_APP_BASE_ADDR

例如 BootLoader 判断固件长度时:

/* firmware_size为无符号字节数:拒绝空固件和超大固件 */if ((firmware_size == 0U) || (firmware_size > FW_APP_MAX_SIZE)){/* 固件长度不合法,拒绝升级 */}

判断一个地址是否属于 App 区域时:

/* 判断地址是否位于App Flash区域 */if ((addr >= FW_APP_BASE_ADDR) &&    (addr < FW_APP_END_ADDR)){/* 当前地址属于App区域 */}

这里检查的只是一个地址。如果从这个位置连续写入一段数据,起点在 App 区内,并不代表最后一个字节也在。

所以,在确认起始地址合法以后,还要用 length <= FW_APP_END_ADDR - addr 比较数据长度与剩余容量。这样不需要先计算可能溢出的 addr + length,也能判断整段数据是否放得下。页擦除范围和半字写入的对齐、补齐要求,后面的内部 Flash 擦写篇再展开。

后面 BootLoader 跳转、Flash 擦除、固件校验和升级包安装都使用这一套宏。

不过,分区容量只是固件能占用的上限。例如实际固件只有 30KB,CRC 等校验通常就按这 30KB 计算。认证需要覆盖哪些字段,则由后面的升级包格式确定。

另外 BootLoader 和 App 是两个独立 Keil 工程,最好让两个工程引用同一个公共 flash_layout.h,而不是各自复制一份再分别维护。

因为 Flash 分区不是“BootLoader 自己的配置”,也不是“App 自己的配置”。

它属于整个设备的内存布局。

不过,公共头文件不会自动修改 Keil 的 IROM 配置或链接脚本。两个工程的链接起始地址和容量还需要分别配置,并与这些宏保持一致。


从分区定义到工程配置

两个工程的地址空间分别为:

工程
Start
Size
BootLoader
0x080000000x00006000
(24KB)
App
0x080060000x0000A000
(40KB)

BootLoader 也要受 24KB 容量限制,避免链接结果进入 App 分区。 下一篇我们会按这张表修改 Keil 工程,再用 MAP、BIN 和中断实验检查配置是否成功。

参考资料

  • ST 官方 STM32F103x8/xB 数据手册(DS5319)
  • ST 官方 STM32F10xxx Flash 编程手册(PM0075)
  • Keil µVision 用户指南:Arm Linker

相关学习资料