1. FLASH空间划分原理与工程意义

在基于STM32F103系列MCU实现IAP(In-Application Programming)功能时,FLASH存储器的合理划分是整个Bootloader架构的基石。IAP的本质是在应用程序运行过程中,通过通信接口(如CAN、UART、USB等)接收新固件并将其写入片内FLASH,从而实现远程升级。这一过程要求系统必须严格区分两个逻辑上独立但物理上共存于同一块FLASH芯片中的程序实体: Bootloader程序 Application程序(APP)

二者不能简单地以“先后顺序”或“覆盖写入”方式共存,而必须在地址空间上进行硬性隔离。其根本原因在于:
- Bootloader作为系统启动后首先执行的固件,承担着校验、跳转、擦除、编程、回滚等关键职责,其代码和数据必须在任何APP运行状态下保持绝对完整与可执行;
- APP是用户功能主体,其大小、更新频率、版本策略均动态可变,若与Bootloader混存,一次错误擦除或越界写入即可导致设备彻底“变砖”,丧失恢复能力;
- STM32F103的FLASH擦除操作以“页(Page)”为最小单位,无法按字节擦除。若Bootloader与APP交叉分布,一次APP升级擦除将不可避免地破坏Bootloader代码区,使设备无法再次启动。

因此,FLASH空间划分并非开发后期的配置技巧,而是IAP系统设计初期就必须明确的 硬件资源契约 。该契约定义了:
- Bootloader的起始地址与最大允许容量;
- APP的起始地址、可用容量及对齐边界;
- 各区域间保留的防护间隙(Guard Zone),用于应对编译器链接脚本偏移误差、中断向量表重映射偏移、以及未来功能扩展冗余;
- 多APP分区的可能性与管理机制(如默认固件区、备份固件区、A/B双区切换)。

这种划分直接决定了后续所有环节的技术选型与实现复杂度:中断向量表重映射的基址、APP跳转入口地址的计算逻辑、FLASH擦除页范围的判定算法、固件校验和(CRC)的计算区间、以及OTA升级包解析时的地址映射规则。一个未经审慎规划的空间布局,将在项目中后期引发大量难以复现的偶发性故障——例如APP跳转后进入非法指令陷阱、升级完成后首次重启失败、或特定条件下中断响应异常。这些现象往往被误判为“硬件问题”或“编译器Bug”,实则根源于初始FLASH分区的工程失当。

2. STM32F103 FLASH物理结构详解

理解FLASH空间划分的前提,是精确掌握目标MCU的FLASH物理组织特性。STM32F103系列属于Cortex-M3内核的通用型微控制器,其片内FLASH并非一块连续均匀的存储体,而是依据容量等级划分为三种不同的页(Page)结构,且页大小与总容量存在强耦合关系。这一设计直接影响擦除操作的粒度、时间开销及地址对齐要求,是IAP实现中不可绕过的底层约束。

2.1 型号命名与FLASH容量映射

STM32F103的型号命名遵循严格的编码规则,其中第6位字符(从左至右)直接标识主FLASH容量。以常见型号为例:
- STM32F103C8T6 :第6位为“8”,查ST官方数据手册《STM32F103xC/D/E datasheet》可知,对应FLASH容量为 64 KB
- STM32F103CBT6 :第6位为“B”,对应 128 KB
- STM32F103ZET6 :第6位为“E”,对应 512 KB
- STM32F103VCT6 :第6位为“C”,对应 256 KB

该映射关系并非经验推断,而是ST官方在产品定义阶段固化于芯片硅片中的物理参数。开发者必须依据实际焊接的MCU型号,而非原理图标注或BOM清单中的“可能型号”,来确定FLASH总容量。曾有项目因采购批次混用C8T6(64KB)与CBT6(128KB)器件,在未修改分区配置的情况下部署同一固件,导致大容量版本APP区溢出覆盖Bootloader,批量返工。

2.2 容量等级与页(Page)结构

ST根据FLASH总容量将F103系列划分为小容量(Low-density)、中容量(Medium-density)和大容量(High-density)三类,其页大小与页数量如下表所示:

容量等级 FLASH总容量范围 页大小 最大页数 典型型号示例
小容量 16–32 KB 1 KB 32页(0–31) F103C4T6, F103C6T6
中容量 64–128 KB 1 KB 128页(0–127) F103C8T6, F103CBT6
大容量 256–512 KB 2 KB 256页(0–255) F103ZET6, F103VCT6

需特别注意:
- 页大小是硬件固定属性,不可通过软件配置更改 。试图在大容量芯片上按1KB页擦除,将触发FLASH_ERRR寄存器中的PGSERR(Programming Sequence Error)标志;
- 页编号连续但物理地址不连续 。以中容量F103C8T6为例,第0页地址为0x08000000–0x080003FF,第1页为0x08000400–0x080007FF,依此类推。页地址 = 起始地址 + 页号 × 页大小;
- 擦除操作必须整页进行 。即使仅需更新APP中单个变量,也必须擦除其所在整页,再重新写入全部有效数据。这要求APP数据区设计必须考虑页内数据聚合,避免跨页分布导致频繁整页擦除。

2.3 实际页结构验证方法

理论参数需通过实测验证。最可靠的方法是查阅ST官方《Reference Manual》中“Memory mapping”章节,并结合调试器读取FLASH控制器寄存器:
- 读取 FLASH->ACR 寄存器的 LATENCY 位域,确认FLASH访问等待周期;
- 计算 FLASH_BASE (0x08000000)至 FLASH_END 的地址跨度, FLASH_END = FLASH_BASE + FLASH_SIZE - 1
- 通过 HAL_FLASHEx_Erase() 函数传入不同页号,观察返回状态。若传入页号超出 MAX_PAGE_NUMBER ,函数将返回 HAL_ERROR

曾遇一案例:某工程师依据网络资料误认为F103C8T6为“小容量”,按32页(32KB)规划APP区,实际编译后发现固件体积达42KB,远超预估。根源即在于未核查型号第6位“8”对应的64KB容量,导致分区严重失当。

3. Bootloader与APP分区策略设计

在明确FLASH物理结构后,分区策略需兼顾功能性、鲁棒性与可维护性。一个工业级IAP系统不应仅满足“能跑通”,而应为长期运维预留弹性空间。以下策略基于多年量产项目经验提炼,已验证于电力监控终端、工业网关等严苛场景。

3.1 分区基本原则

  1. Bootloader优先保障原则 :Bootloader区必须位于FLASH起始地址(0x08000000),且容量设定需留有至少30%冗余。原因在于:
    - Bootloader需集成CAN协议栈、AES加密、CRC32校验、FLASH页擦除/编程驱动、看门狗喂狗逻辑、低功耗唤醒处理等模块,功能迭代常导致体积增长;
    - 若Bootloader区满载,后续添加安全启动(Secure Boot)签名验证将无空间容纳公钥或签名数据;
    - 建议初始分配:64KB芯片取16KB(0x4000),256KB芯片取32KB(0x8000),512KB芯片取64KB(0x10000)。

  2. APP区对齐与防护原则 :APP起始地址必须严格对齐于页边界,且与Bootloader区之间设置不小于1页的防护间隙(Guard Zone)。
    - 对齐要求:若Bootloader占用N页,则APP起始地址 = 0x08000000 + N × PAGE_SIZE 。例如F103C8T6(1KB页),Bootloader占16页(16KB),则APP起始地址为 0x08000000 + 0x4000 = 0x08004000
    - 防护间隙:在Bootloader末尾与APP起始之间保留1页(如0x08003C00–0x08003FFF)作为空白区。此区永不写入任何代码或数据,专用于捕获因链接脚本错误、指针越界或编译器优化导致的非法写入,避免Bootloader核心代码被意外覆盖。

  3. 多APP分区扩展性原则 :为支持固件回滚(Rollback)与A/B双区升级,建议预留多APP分区结构:
    [0x08000000] — Bootloader (16KB) [0x08004000] — APP_Main (Primary, 32KB) [0x0800C000] — APP_Backup (Secondary, 32KB) [0x08014000] — Default_Firmware (Factory Reset, 16KB)
    此结构下,升级时先将新固件写入 APP_Backup ,校验通过后更新启动标志位,下次复位由Bootloader跳转至 APP_Backup 。若新固件运行异常,可通过特定按键组合触发Bootloader强制跳转至 Default_Firmware 恢复出厂状态。

3.2 容量核算实例:F103C8T6(64KB FLASH)

以教学视频中提及的F103C8T6为例,执行精确容量核算:
- 总FLASH容量:64 KB = 0x10000 字节;
- Bootloader分配:16 KB = 0x4000 字节 → 占用页0至页15(0x0000–0x3FFF);
- 防护间隙:1 KB = 0x400 字节 → 占用页16(0x4000–0x43FF);
- APP_Main起始地址: 0x08000000 + 0x4000 + 0x400 = 0x08004400
- APP_Main可用容量: 0x10000 - 0x4000 - 0x400 = 0xBB00 ≈ 47.75 KB
- 若采用双区,则APP_Main与APP_Backup各分23.5 KB(0x5D00),剩余空间用于存储升级元数据(版本号、CRC、时间戳)。

此核算结果与视频中直接使用 0x08004000 作为APP起始地址存在细微差异,但更符合工程实践。防护间隙的存在虽牺牲少量空间,却极大提升了系统可靠性。

4. MDK-ARM环境下的链接脚本配置

Keil MDK-ARM是STM32F103开发的主流IDE,其链接脚本(scatter file)直接控制代码与数据在FLASH和RAM中的布局。IAP分区必须通过修改此文件实现,而非仅依赖IDE图形界面设置。图形界面的“ROM Region”配置本质是生成或覆盖链接脚本,手动编辑才能确保精确控制。

4.1 标准链接脚本结构解析

MDK默认生成的scatter文件(如 STM32F103C8Tx_FLASH.sct )包含三个核心段:

LR_IROM1 0x08000000 0x00010000  {    ; load region size_region  
  ER_IROM1 0x08000000 0x00010000  {  ; load address = execution address  
    *.o (RESET, +First)  
    *(InRoot$$Sections)  
    .ANY (+RO)  
  }  
  RW_IRAM1 0x20000000 0x00005000  {  ; RW data  
    .ANY (+RW +ZI)  
  }  
}

其中:
- LR_IROM1 :加载区域(Load Region)名称,起始地址 0x08000000 ,大小 0x00010000 (64KB);
- ER_IROM1 :执行区域(Execution Region)名称,地址与大小同加载区域,表示代码从FLASH原地执行;
- *.o (RESET, +First) :将startup_stm32f10x_md.o中的复位向量置于区域起始;
- .ANY (+RO) :将所有只读代码(RO Code)放入此区域。

4.2 Bootloader链接脚本修改

为Bootloader创建独立scatter文件(如 bootloader.sct ),关键修改点:

LR_IROM1 0x08000000 0x00004000  {    ; Bootloader仅占16KB  
  ER_IROM1 0x08000000 0x00004000  {  
    *.o (RESET, +First)  
    *(InRoot$$Sections)  
    .ANY (+RO)  
    .ANY (+XO)          ; 包含所有初始化代码  
  }  
  RW_IRAM1 0x20000000 0x00005000  {  
    .ANY (+RW +ZI)  
  }  
}
  • LR_IROM1 大小由 0x00010000 改为 0x00004000 ,强制限制Bootloader最大体积;
  • 移除对 APP 相关对象文件的引用,确保链接器不会将APP代码误链入Bootloader区;
  • 在MDK工程选项中,Target页的“IROM1”区域Size同步设为 0x4000 ,与scatter文件一致。

4.3 APP链接脚本修改

为APP创建独立scatter文件(如 app.sct ),核心在于重定义执行区域起始地址:

LR_IROM1 0x08004400 0x0000BB00  {    ; APP起始0x08004400,大小47.75KB  
  ER_IROM1 0x08004400 0x0000BB00  {  
    *.o (RESET, +First)      ; 注意:APP无需复位向量,此行实际无效  
    *(InRoot$$Sections)  
    .ANY (+RO)  
  }  
  RW_IRAM1 0x20000000 0x00005000  {  
    .ANY (+RW +ZI)  
  }  
}
  • ER_IROM1 起始地址设为 0x08004400 ,严格对齐防护间隙之后;
  • 大小 0x0000BB00 精确匹配可用空间,防止链接器因空间不足报错;
  • 关键细节 :APP工程中必须禁用 Use Memory Layout from Target Dialog ,强制使用此自定义scatter文件。

4.4 编译后空间占用验证

编译完成后,MDK输出窗口显示的内存占用信息是唯一可信依据:

Program Size: Code=12344 RO-data=1288 RW-data=256 ZI-data=1024  
Total RO Size (Code + RO Data) : 13632 (13.31kB)  
Total RW Size (RW Data + ZI Data) : 1280 (1.25kB)  
  • RO Size (13.31KB)即为FLASH实际占用,必须小于Bootloader分配的16KB(0x4000);
  • 若RO Size接近上限(如>15KB),需审查代码:是否启用了未使用的外设驱动?是否包含调试打印字符串?是否可启用 -Os 优化级别?
  • 切勿依赖IDE左侧工程树中“.axf”文件大小,该值包含调试符号与填充,无工程参考价值。

5. 中断向量表重映射实现

当APP程序不从FLASH起始地址(0x08000000)运行时,其内置的中断向量表(位于代码段起始处)将位于非默认位置。若此时发生中断,CPU仍会从 0x08000000 处读取向量表,导致跳转至Bootloader的中断服务程序(ISR)或非法地址,系统崩溃。因此,APP启动前必须将中断向量表重映射至其实际位置。

5.1 重映射硬件机制

STM32F103的NVIC提供 VTOR (Vector Table Offset Register)寄存器,用于动态设置向量表基址。其操作流程为:
1. 确保APP的向量表首地址(即APP起始地址)已正确写入FLASH;
2. 将 VTOR 寄存器值设为APP向量表地址;
3. 执行 SCB->AIRCR 寄存器的 VECTCLRACT 位触发向量表重载。

5.2 APP主函数入口重映射代码

在APP的 main() 函数最顶端插入以下代码:

#include "stm32f1xx.h"

int main(void)
{
    /* 1. 关闭全局中断,防止重映射过程中中断打断 */
    __disable_irq();

    /* 2. 设置向量表偏移量:APP起始地址为0x08004400 */
    SCB->VTOR = 0x08004400;

    /* 3. 清除所有待处理的中断挂起标志(可选,增强鲁棒性) */
    NVIC_ICPR(0) = 0xFFFFFFFF;
    NVIC_ICPR(1) = 0xFFFFFFFF;

    /* 4. 重新使能全局中断 */
    __enable_irq();

    /* 5. 后续APP业务逻辑 */
    HAL_Init();
    SystemClock_Config();
    // ... 其他初始化
}
  • SCB->VTOR = 0x08004400 :将向量表基址指向APP区起始,CPU从此地址读取复位向量、NMI、HardFault等向量;
  • __disable_irq() __enable_irq() :确保重映射原子性,避免中断在 VTOR 写入中途发生;
  • NVIC_ICPR 清除挂起:若Bootloader运行时产生过中断(如SysTick),其挂起标志可能残留,重映射后若不清除,APP启动即触发该中断,造成逻辑混乱。

5.3 Bootloader跳转前的向量表清理

Bootloader在跳转至APP前,除跳转外,还应执行必要清理:

void Jump_To_Application(uint32_t ApplicationAddress)
{
    typedef void (*pFunction)(void);
    pFunction Jump_To_Application;

    /* 1. 检查APP向量表有效性:检查栈顶地址是否在合法RAM范围内 */
    if (((*(__IO uint32_t*)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000)
    {
        /* 2. 设置主栈指针MSP */
        __set_MSP(*(__IO uint32_t*) ApplicationAddress);

        /* 3. 获取APP复位处理函数地址 */
        Jump_To_Application = (pFunction)(*(__IO uint32_t*)(ApplicationAddress + 4));

        /* 4. 清除所有NVIC中断挂起与使能位 */
        for (uint8_t i = 0; i < 8; i++) {
            NVIC->ICER[i] = 0xFFFFFFFF;  // 清除使能
            NVIC->ICPR[i] = 0xFFFFFFFF;  // 清除挂起
        }

        /* 5. 关闭所有外设时钟,仅保留APP必需者(如SYSCLK) */
        RCC->AHBENR = 0x00000000;
        RCC->APB1ENR = 0x00000000;
        RCC->APB2ENR = 0x00000000;

        /* 6. 跳转 */
        Jump_To_Application();
    }
}
  • 栈顶地址校验( *(__IO uint32_t*)ApplicationAddress )是防止跳转至非法地址的关键防线;
  • NVIC->ICER[i] ICPR[i] 全清,确保APP启动时处于纯净中断环境;
  • 外设时钟关闭避免Bootloader配置干扰APP时钟树。

6. 工程实践中的典型问题与规避方案

在数十个IAP项目落地过程中,以下问题高频出现,其根源均指向FLASH分区环节的疏忽:

6.1 问题:APP跳转后立即HardFault

现象 :Bootloader成功跳转至APP地址,但APP第一条指令执行即触发HardFault。
根因 :APP链接脚本中 ER_IROM1 起始地址未对齐页边界,或APP向量表首地址( 0x08004400 )处数据被擦除/未编程。
排查步骤
- 使用ST-Link Utility连接芯片,读取 0x08004400 处4字节,确认是否为有效栈顶地址(应在0x20000000–0x20005000范围内);
- 检查烧录工具日志,确认 0x08004400–0x08004403 区间是否被写入;
- 验证scatter文件中 ER_IROM1 地址是否为页对齐(1KB页则地址低10位必须为0)。

6.2 问题:升级后APP功能异常,但Bootloader日志显示“升级成功”

现象 :新固件通过CAN接收并写入FLASH,Bootloader校验CRC通过,但APP运行逻辑错乱(如ADC采样值恒为0)。
根因 :APP分区大小设置过小,导致部分代码或常量数据被截断,或 .rodata 段溢出覆盖相邻区域。
解决方案
- 编译APP后,查看 map 文件中 ER_IROM1 段的 Max Length ,对比scatter文件中设定的Size;
- 若 Max Length > Size ,增大APP分区Size,并同步更新Bootloader中APP起始地址计算逻辑;
- 在APP工程中启用 -Wl,--print-memory-usage 链接器选项,强制输出内存占用报告。

6.3 问题:防护间隙被意外写入

现象 :多次升级后,Bootloader自身开始出现随机故障(如CAN通信中断)。
根因 :APP代码中存在指针越界写操作,将数据写入防护间隙,而该间隙恰与Bootloader末尾页重叠。
加固措施
- 在Bootloader末尾页(如F103C8T6的页15, 0x08003C00–0x08003FFF )写入特征值(如 0xDEADBEEF );
- APP启动时,读取该地址特征值,若被篡改则强制进入Bootloader安全模式;
- 使用MDK的 --diag_warning 1293 选项,启用数组越界警告。

分区设计不是静态配置,而是贯穿IAP全生命周期的动态契约。每一次固件功能增强、每一次编译器版本升级、每一次安全补丁注入,都需重新审视该契约的有效性。我曾在某能源计量项目中,因未预留足够防护间隙,导致AES加密库升级后新增的S盒数据溢出,覆盖Bootloader的CAN接收缓冲区,最终花费两周定位。自此,所有项目均将防护间隙设为2页,并纳入自动化CI流水线检查项——当编译后RO Size超过分区90%时,构建即失败。这看似保守,却是量产系统稳定性的无声基石。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐