固件分区与内存布局设计¶
概述¶
在嵌入式系统开发中,合理的固件分区和内存布局设计是系统稳定运行的基础。无论是简单的单片机应用还是复杂的嵌入式Linux系统,都需要精心规划Flash和RAM的使用。本教程将带你深入理解固件分区的原理和实践,掌握内存布局设计的核心技能。
完成本教程后,你将能够:
- 理解Flash和RAM的特性及使用场景
- 掌握固件分区的设计原则和方法
- 学会编写和配置链接脚本
- 理解地址映射和重定位机制
- 设计合理的分区表并进行空间优化
- 解决常见的内存布局问题
准备工作¶
硬件要求¶
本教程使用以下硬件平台作为示例:
- 开发板: STM32F407VGT6开发板
- Flash容量: 1MB (0x100000字节)
- RAM容量: 192KB (128KB主RAM + 64KB CCM)
- 调试器: ST-Link V2或J-Link
软件要求¶
- 开发环境: Keil MDK 5.x 或 STM32CubeIDE
- 编译器: ARM Compiler ⅚ 或 GCC
- 工具: 文本编辑器(用于编辑链接脚本)
- 参考文档: STM32F407参考手册
前置知识¶
在开始本教程前,建议你已经:
- 了解Bootloader的基本概念
- 熟悉C语言和基本的汇编知识
- 理解程序的编译和链接过程
- 掌握基本的二进制和十六进制运算
核心内容¶
第一部分: 存储器基础知识¶
1.1 Flash存储器特性¶
Flash是嵌入式系统中最常用的非易失性存储器,用于存储程序代码和常量数据。
Flash的主要特性:
特性对比:
┌─────────────┬──────────────┬──────────────┐
│ 特性 │ NOR Flash │ NAND Flash │
├─────────────┼──────────────┼──────────────┤
│ 读取速度 │ 快速 │ 较慢 │
│ 写入速度 │ 较慢 │ 快速 │
│ 擦除单位 │ 扇区/块 │ 块/页 │
│ 随机访问 │ 支持 │ 不支持 │
│ 可靠性 │ 高 │ 需要ECC │
│ 成本 │ 较高 │ 较低 │
│ 应用场景 │ 代码存储 │ 数据存储 │
└─────────────┴──────────────┴──────────────┘
STM32F407的Flash特性:
- 容量: 1MB (1024KB)
- 扇区大小: 不均匀分布
- 扇区0-3: 16KB × 4 = 64KB
- 扇区4: 64KB
- 扇区5-11: 128KB × 7 = 896KB
- 编程单位: 字节/半字/字/双字
- 擦除单位: 扇区
- 编程时间: 约16μs/字
- 擦除时间: 约500ms-2s/扇区
Flash扇区分布图:
STM32F407 Flash布局 (1MB):
地址范围 扇区 大小 累计
0x0800 0000 ┌────────┐
│ 扇区0 │ 16KB 16KB
0x0800 4000 ├────────┤
│ 扇区1 │ 16KB 32KB
0x0800 8000 ├────────┤
│ 扇区2 │ 16KB 48KB
0x0800 C000 ├────────┤
│ 扇区3 │ 16KB 64KB
0x0801 0000 ├────────┤
│ 扇区4 │ 64KB 128KB
0x0802 0000 ├────────┤
│ 扇区5 │ 128KB 256KB
0x0804 0000 ├────────┤
│ 扇区6 │ 128KB 384KB
0x0806 0000 ├────────┤
│ 扇区7 │ 128KB 512KB
0x0808 0000 ├────────┤
│ 扇区8 │ 128KB 640KB
0x080A 0000 ├────────┤
│ 扇区9 │ 128KB 768KB
0x080C 0000 ├────────┤
│ 扇区10 │ 128KB 896KB
0x080E 0000 ├────────┤
│ 扇区11 │ 128KB 1024KB
0x0810 0000 └────────┘
1.2 RAM存储器特性¶
RAM是易失性存储器,用于存储运行时的变量、堆栈和临时数据。
RAM的主要特性:
- 易失性: 掉电后数据丢失
- 读写速度: 非常快,无需擦除
- 随机访问: 支持任意地址读写
- 无擦写次数限制: 可以无限次读写
- 成本: 相对Flash较高
STM32F407的RAM布局:
STM32F407 RAM布局 (192KB总容量):
地址范围 区域 大小 特性
0x2000 0000 ┌────────┐
│ │
│ SRAM1 │ 112KB 通用RAM
│ │
0x2001 C000 ├────────┤
│ SRAM2 │ 16KB 通用RAM
0x2002 0000 ├────────┤
│ SRAM3 │ 64KB 通用RAM(可选)
0x2003 0000 └────────┘
0x1000 0000 ┌────────┐
│ CCM │ 64KB 核心耦合内存
│ │ (仅CPU访问)
0x1001 0000 └────────┘
特点说明:
- SRAM1/2/3: 可被CPU、DMA、外设访问
- CCM: 仅CPU访问,速度最快,不能用于DMA
- 总容量: 128KB (SRAM) + 64KB (CCM) = 192KB
RAM使用分配:
典型RAM使用分配:
高地址
0x2002 0000 ┌──────────────┐
│ 堆栈(Stack)│ ← 向下增长
├──────────────┤
│ 堆(Heap) │ ← 向上增长
├──────────────┤
│ BSS段 │ 未初始化全局变量
├──────────────┤
│ Data段 │ 已初始化全局变量
0x2000 0000 └──────────────┘
低地址
第二部分: 固件分区设计原则¶
2.1 分区设计的基本原则¶
设计固件分区时需要遵循以下原则:
1. 功能隔离原则
不同功能的代码和数据应该分开存储:
功能分区示例:
┌─────────────────────────────────┐
│ Bootloader区 │ 系统引导
├─────────────────────────────────┤
│ 应用程序区 │ 主要功能
├─────────────────────────────────┤
│ 升级缓存区 │ 固件更新
├─────────────────────────────────┤
│ 参数存储区 │ 配置数据
├─────────────────────────────────┤
│ 日志存储区 │ 运行日志
└─────────────────────────────────┘
2. 安全保护原则
关键区域需要保护,防止意外擦写:
- Bootloader区应该设置写保护
- 参数区需要备份机制
- 敏感数据需要加密存储
3. 扩展性原则
预留足够的空间用于未来扩展:
- Bootloader预留50%以上空间
- 应用程序预留20-30%空间
- 参数区预留多个扇区
4. 对齐原则
分区边界应该对齐到扇区边界:
// 正确的分区对齐
#define BOOTLOADER_START 0x08000000 // 扇区0起始
#define APP_START 0x08008000 // 扇区2起始
#define PARAM_START 0x080E0000 // 扇区10起始
// 错误的分区对齐(不在扇区边界)
#define APP_START_WRONG 0x08005000 // 在扇区1中间!
5. 性能优化原则
- 频繁访问的代码放在低地址(访问速度快)
- 大块数据存储使用大扇区(减少擦除次数)
- 关键代码可以复制到RAM执行(提高速度)
2.2 常见分区方案¶
方案1: 简单双区方案
适用于不需要在线升级的简单应用:
Flash布局 (1MB):
0x0800 0000 ┌─────────────────┐
│ Bootloader │ 32KB
0x0800 8000 ├─────────────────┤
│ │
│ 应用程序 │ 960KB
│ │
0x080F 0000 ├─────────────────┤
│ 参数存储 │ 64KB
0x0810 0000 └─────────────────┘
优点: 简单,空间利用率高
缺点: 不支持在线升级
方案2: 标准IAP方案
支持在线升级的标准方案:
Flash布局 (1MB):
0x0800 0000 ┌─────────────────┐
│ Bootloader │ 32KB
0x0800 8000 ├─────────────────┤
│ 应用程序 │ 480KB
0x0808 0000 ├─────────────────┤
│ 升级缓存 │ 480KB
0x080F 8000 ├─────────────────┤
│ 参数存储 │ 32KB
0x0810 0000 └─────────────────┘
优点: 支持完整固件升级
缺点: 空间利用率较低(50%)
方案3: 差分升级方案
使用差分包升级,节省空间:
Flash布局 (1MB):
0x0800 0000 ┌─────────────────┐
│ Bootloader │ 32KB
0x0800 8000 ├─────────────────┤
│ 应用程序 │ 768KB
0x080C 8000 ├─────────────────┤
│ 差分包缓存 │ 192KB
0x080F 8000 ├─────────────────┤
│ 参数存储 │ 32KB
0x0810 0000 └─────────────────┘
优点: 空间利用率高,升级快
缺点: 实现复杂,需要差分算法
方案4: 双备份方案
高可靠性应用的双备份方案:
Flash布局 (1MB):
0x0800 0000 ┌─────────────────┐
│ Bootloader │ 64KB
0x0801 0000 ├─────────────────┤
│ 应用程序A │ 448KB
0x0808 0000 ├─────────────────┤
│ 应用程序B │ 448KB
0x080F 0000 ├─────────────────┤
│ 参数存储 │ 64KB
0x0810 0000 └─────────────────┘
优点: 高可靠性,支持回滚
缺点: 空间利用率低,升级慢
第三部分: 链接脚本详解¶
3.1 链接脚本的作用¶
链接脚本(Linker Script)告诉链接器如何组织程序的各个段,并将它们放置到正确的内存位置。
链接脚本的主要功能:
- 定义内存区域: 指定Flash和RAM的起始地址和大小
- 段的放置: 决定代码段、数据段等放在哪里
- 符号定义: 定义程序中使用的特殊符号
- 对齐要求: 指定段的对齐方式
3.2 GCC链接脚本语法¶
基本结构:
/* 链接脚本基本结构 */
/* 1. 内存定义 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
CCM (rwx) : ORIGIN = 0x10000000, LENGTH = 64K
}
/* 2. 段定义 */
SECTIONS
{
/* 代码段 */
.text :
{
/* 段内容 */
} > FLASH
/* 数据段 */
.data :
{
/* 段内容 */
} > RAM AT> FLASH
/* BSS段 */
.bss :
{
/* 段内容 */
} > RAM
}
内存区域属性:
MEMORY
{
/* 格式: 名称 (属性) : ORIGIN = 起始地址, LENGTH = 大小 */
/* 属性说明:
* r - 可读 (Read)
* w - 可写 (Write)
* x - 可执行 (Execute)
* a - 可分配 (Allocatable)
* i - 已初始化 (Initialized)
*/
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1M
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
CCM (rwx) : ORIGIN = 0x10000000, LENGTH = 64K
}
3.3 Bootloader链接脚本示例¶
完整的Bootloader链接脚本:
/* STM32F407 Bootloader链接脚本 */
/* Bootloader占用: 0x08000000 - 0x08008000 (32KB) */
/* 入口点 */
ENTRY(Reset_Handler)
/* 最小堆栈大小 */
_Min_Heap_Size = 0x200; /* 512字节 */
_Min_Stack_Size = 0x400; /* 1KB */
/* 内存定义 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
}
/* 段定义 */
SECTIONS
{
/* 中断向量表 - 必须放在Flash起始位置 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* 保持不被优化掉 */
. = ALIGN(4);
} > FLASH
/* 代码段 */
.text :
{
. = ALIGN(4);
*(.text) /* 所有.text段 */
*(.text*) /* 所有.text.*段 */
*(.glue_7) /* ARM/Thumb互操作代码 */
*(.glue_7t)
*(.eh_frame) /* 异常处理框架 */
KEEP (*(.init)) /* 初始化代码 */
KEEP (*(.fini)) /* 终止代码 */
. = ALIGN(4);
_etext = .; /* 代码段结束地址 */
} > FLASH
/* 只读数据段 */
.rodata :
{
. = ALIGN(4);
*(.rodata) /* 只读数据 */
*(.rodata*)
. = ALIGN(4);
} > FLASH
/* ARM异常表 */
.ARM.extab :
{
*(.ARM.extab* .gnu.linkonce.armextab.*)
} > FLASH
.ARM :
{
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
} > FLASH
/* 预初始化数组 */
.preinit_array :
{
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array*))
PROVIDE_HIDDEN (__preinit_array_end = .);
} > FLASH
/* 初始化数组 */
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
} > FLASH
/* 终止数组 */
.fini_array :
{
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT(.fini_array.*)))
KEEP (*(.fini_array*))
PROVIDE_HIDDEN (__fini_array_end = .);
} > FLASH
/* 已初始化数据段 - 存储在Flash,运行时复制到RAM */
_sidata = LOADADDR(.data); /* Flash中的起始地址 */
.data :
{
. = ALIGN(4);
_sdata = .; /* RAM中的起始地址 */
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .; /* RAM中的结束地址 */
} > RAM AT> FLASH
/* CCM数据段(可选) */
_siccmram = LOADADDR(.ccmram);
.ccmram :
{
. = ALIGN(4);
_sccmram = .;
*(.ccmram)
*(.ccmram*)
. = ALIGN(4);
_eccmram = .;
} > CCMRAM AT> FLASH
/* 未初始化数据段 - 启动时清零 */
.bss :
{
. = ALIGN(4);
_sbss = .; /* BSS起始地址 */
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .; /* BSS结束地址 */
__bss_end__ = _ebss;
} > RAM
/* 堆区 */
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} > RAM
/* 移除调试信息 */
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
/* 属性表 */
.ARM.attributes 0 : { *(.ARM.attributes) }
}
3.4 应用程序链接脚本示例¶
应用程序链接脚本(起始地址0x08008000):
/* STM32F407 应用程序链接脚本 */
/* 应用程序占用: 0x08008000 - 0x08080000 (480KB) */
ENTRY(Reset_Handler)
_Min_Heap_Size = 0x2000; /* 8KB */
_Min_Stack_Size = 0x1000; /* 4KB */
MEMORY
{
/* 注意: 起始地址改为0x08008000 */
FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 480K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
}
SECTIONS
{
/* 中断向量表 - 应用程序的向量表 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
/* 其余段定义与Bootloader相同 */
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
*(.glue_7)
*(.glue_7t)
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .;
} > FLASH
/* ... 其他段定义相同 ... */
}
关键区别:
// Bootloader链接脚本
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
// 应用程序链接脚本
FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 480K
// ^^^^^^^^^^ 注意起始地址不同!
第四部分: 地址映射与重定位¶
4.1 ARM Cortex-M地址空间¶
ARM Cortex-M系列使用统一的4GB地址空间:
ARM Cortex-M4 地址空间布局:
0xFFFF FFFF ┌─────────────────┐
│ 系统区域 │ 0xE0000000-0xFFFFFFFF
0xE000 0000 ├─────────────────┤
│ 外部设备 │ 0xA0000000-0xDFFFFFFF
0xA000 0000 ├─────────────────┤
│ 外部RAM │ 0x60000000-0x9FFFFFFF
0x6000 0000 ├─────────────────┤
│ 外设 │ 0x40000000-0x5FFFFFFF
0x4000 0000 ├─────────────────┤
│ SRAM │ 0x20000000-0x3FFFFFFF
0x2000 0000 ├─────────────────┤
│ 代码区 │ 0x00000000-0x1FFFFFFF
0x0000 0000 └─────────────────┘
STM32F407具体映射:
- 0x08000000: Flash起始地址
- 0x20000000: SRAM起始地址
- 0x10000000: CCM RAM起始地址
- 0x40000000: 外设寄存器起始地址
4.2 中断向量表重定位¶
为什么需要重定位:
- Bootloader和应用程序都有自己的中断向量表
- 跳转到应用程序后,需要使用应用程序的向量表
- ARM Cortex-M通过VTOR寄存器实现向量表重定位
VTOR寄存器:
/* VTOR寄存器定义 */
#define SCB_VTOR (*(volatile uint32_t*)0xE000ED08)
/* 向量表偏移寄存器的要求:
* 1. 地址必须512字节对齐(Cortex-M4)
* 2. 地址范围: 0x00000000 - 0x3FFFFF80
* 3. 通常设置为Flash或RAM的起始地址
*/
重定位代码示例:
/* 在Bootloader中跳转前设置VTOR */
void JumpToApplication(uint32_t app_addr)
{
// 检查应用程序是否有效
if (((*(__IO uint32_t*)app_addr) & 0x2FFE0000) == 0x20000000)
{
// 获取应用程序的栈指针和入口地址
uint32_t app_sp = *(__IO uint32_t*)app_addr;
uint32_t app_entry = *(__IO uint32_t*)(app_addr + 4);
// 定义函数指针
typedef void (*pFunction)(void);
pFunction app_reset_handler = (pFunction)app_entry;
// 关闭所有中断
__disable_irq();
// 关闭SysTick
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
// 重新设置中断向量表偏移
SCB->VTOR = app_addr; // 关键步骤!
// 设置主堆栈指针
__set_MSP(app_sp);
// 跳转到应用程序
app_reset_handler();
}
}
/* 在应用程序的SystemInit()中也要设置VTOR */
void SystemInit(void)
{
/* 重新设置向量表偏移 */
SCB->VTOR = 0x08008000; // 应用程序起始地址
/* 其他系统初始化... */
}
4.3 数据段的重定位¶
程序启动时需要将已初始化数据从Flash复制到RAM:
/* 启动代码中的数据复制 */
void Reset_Handler(void)
{
uint32_t *src, *dst;
/* 1. 复制.data段从Flash到RAM */
src = &_sidata; /* Flash中的源地址 */
dst = &_sdata; /* RAM中的目标地址 */
while (dst < &_edata)
{
*dst++ = *src++;
}
/* 2. 清零.bss段 */
dst = &_sbss;
while (dst < &_ebss)
{
*dst++ = 0;
}
/* 3. 调用SystemInit */
SystemInit();
/* 4. 调用main函数 */
main();
}
数据段布局示意图:
Flash中的布局:
0x08000000 ┌──────────────┐
│ .text │ 代码段
├──────────────┤
│ .rodata │ 只读数据
├──────────────┤
│ .data(副本) │ 已初始化数据的副本
└──────────────┘
RAM中的布局:
0x20000000 ┌──────────────┐
│ .data │ 已初始化数据(从Flash复制)
├──────────────┤
│ .bss │ 未初始化数据(清零)
├──────────────┤
│ heap │ 堆(向上增长)
├──────────────┤
│ stack │ 栈(向下增长)
└──────────────┘
第五部分: 分区表设计实践¶
5.1 定义分区表结构¶
分区表头文件:
/* partition_table.h */
#ifndef __PARTITION_TABLE_H
#define __PARTITION_TABLE_H
#include <stdint.h>
/* 分区类型定义 */
typedef enum {
PARTITION_TYPE_BOOTLOADER = 0,
PARTITION_TYPE_APPLICATION,
PARTITION_TYPE_UPGRADE,
PARTITION_TYPE_PARAMETER,
PARTITION_TYPE_LOG,
PARTITION_TYPE_RESERVED
} PartitionType;
/* 分区信息结构 */
typedef struct {
char name[16]; /* 分区名称 */
PartitionType type; /* 分区类型 */
uint32_t start_addr; /* 起始地址 */
uint32_t size; /* 分区大小 */
uint8_t start_sector; /* 起始扇区号 */
uint8_t sector_count; /* 扇区数量 */
uint8_t flags; /* 标志位 */
uint8_t reserved; /* 保留 */
} PartitionInfo;
/* 标志位定义 */
#define PARTITION_FLAG_READONLY (1 << 0) /* 只读 */
#define PARTITION_FLAG_PROTECTED (1 << 1) /* 写保护 */
#define PARTITION_FLAG_ENCRYPTED (1 << 2) /* 加密 */
#define PARTITION_FLAG_COMPRESSED (1 << 3) /* 压缩 */
/* 分区表定义 */
#define PARTITION_TABLE_MAGIC 0x50415254 /* "PART" */
#define PARTITION_TABLE_VERSION 0x0100 /* v1.0 */
#define MAX_PARTITIONS 8
typedef struct {
uint32_t magic; /* 魔数 */
uint16_t version; /* 版本号 */
uint16_t partition_count; /* 分区数量 */
PartitionInfo partitions[MAX_PARTITIONS]; /* 分区信息 */
uint32_t crc32; /* CRC校验 */
} PartitionTable;
/* 函数声明 */
void PartitionTable_Init(void);
const PartitionInfo* PartitionTable_GetInfo(PartitionType type);
uint8_t PartitionTable_Validate(void);
#endif /* __PARTITION_TABLE_H */
5.2 实现分区表管理¶
分区表实现:
/* partition_table.c */
#include "partition_table.h"
#include <string.h>
/* 分区表定义(存储在Flash中) */
const PartitionTable g_partition_table __attribute__((section(".partition_table"))) = {
.magic = PARTITION_TABLE_MAGIC,
.version = PARTITION_TABLE_VERSION,
.partition_count = 5,
.partitions = {
/* Bootloader分区 */
{
.name = "bootloader",
.type = PARTITION_TYPE_BOOTLOADER,
.start_addr = 0x08000000,
.size = 0x00008000, /* 32KB */
.start_sector = 0,
.sector_count = 2,
.flags = PARTITION_FLAG_READONLY | PARTITION_FLAG_PROTECTED,
.reserved = 0
},
/* 应用程序分区 */
{
.name = "application",
.type = PARTITION_TYPE_APPLICATION,
.start_addr = 0x08008000,
.size = 0x00078000, /* 480KB */
.start_sector = 2,
.sector_count = 6,
.flags = 0,
.reserved = 0
},
/* 升级缓存分区 */
{
.name = "upgrade",
.type = PARTITION_TYPE_UPGRADE,
.start_addr = 0x08080000,
.size = 0x00078000, /* 480KB */
.start_sector = 8,
.sector_count = 3,
.flags = 0,
.reserved = 0
},
/* 参数存储分区 */
{
.name = "parameter",
.type = PARTITION_TYPE_PARAMETER,
.start_addr = 0x080F8000,
.size = 0x00004000, /* 16KB */
.start_sector = 11,
.sector_count = 1,
.flags = 0,
.reserved = 0
},
/* 日志存储分区 */
{
.name = "log",
.type = PARTITION_TYPE_LOG,
.start_addr = 0x080FC000,
.size = 0x00004000, /* 16KB */
.start_sector = 11,
.sector_count = 1,
.flags = 0,
.reserved = 0
}
},
.crc32 = 0 /* 需要计算 */
};
/* 初始化分区表 */
void PartitionTable_Init(void)
{
/* 验证分区表 */
if (!PartitionTable_Validate()) {
/* 分区表无效,使用默认配置或报错 */
while(1); /* 错误处理 */
}
}
/* 获取分区信息 */
const PartitionInfo* PartitionTable_GetInfo(PartitionType type)
{
for (int i = 0; i < g_partition_table.partition_count; i++) {
if (g_partition_table.partitions[i].type == type) {
return &g_partition_table.partitions[i];
}
}
return NULL;
}
/* 验证分区表 */
uint8_t PartitionTable_Validate(void)
{
/* 1. 检查魔数 */
if (g_partition_table.magic != PARTITION_TABLE_MAGIC) {
return 0;
}
/* 2. 检查版本 */
if (g_partition_table.version != PARTITION_TABLE_VERSION) {
return 0;
}
/* 3. 检查分区数量 */
if (g_partition_table.partition_count > MAX_PARTITIONS) {
return 0;
}
/* 4. 检查分区是否重叠 */
for (int i = 0; i < g_partition_table.partition_count; i++) {
for (int j = i + 1; j < g_partition_table.partition_count; j++) {
uint32_t start1 = g_partition_table.partitions[i].start_addr;
uint32_t end1 = start1 + g_partition_table.partitions[i].size;
uint32_t start2 = g_partition_table.partitions[j].start_addr;
uint32_t end2 = start2 + g_partition_table.partitions[j].size;
/* 检查重叠 */
if ((start1 < end2) && (start2 < end1)) {
return 0; /* 分区重叠 */
}
}
}
/* 5. 检查CRC(可选) */
/* ... CRC校验代码 ... */
return 1; /* 验证通过 */
}
/* 打印分区表信息 */
void PartitionTable_Print(void)
{
printf("\n=== Partition Table ===\n");
printf("Magic: 0x%08X\n", g_partition_table.magic);
printf("Version: %d.%d\n",
g_partition_table.version >> 8,
g_partition_table.version & 0xFF);
printf("Partition Count: %d\n\n", g_partition_table.partition_count);
printf("%-12s %-10s %-10s %-8s %-8s\n",
"Name", "Start", "Size", "Sector", "Flags");
printf("--------------------------------------------------------\n");
for (int i = 0; i < g_partition_table.partition_count; i++) {
const PartitionInfo *p = &g_partition_table.partitions[i];
printf("%-12s 0x%08X 0x%08X %2d-%2d 0x%02X\n",
p->name,
p->start_addr,
p->size,
p->start_sector,
p->start_sector + p->sector_count - 1,
p->flags);
}
printf("\n");
}
5.3 使用分区表¶
在代码中使用分区表:
/* main.c */
#include "partition_table.h"
#include "flash.h"
int main(void)
{
/* 初始化分区表 */
PartitionTable_Init();
/* 打印分区信息 */
PartitionTable_Print();
/* 获取应用程序分区信息 */
const PartitionInfo *app_partition =
PartitionTable_GetInfo(PARTITION_TYPE_APPLICATION);
if (app_partition != NULL) {
printf("Application partition:\n");
printf(" Start: 0x%08X\n", app_partition->start_addr);
printf(" Size: %d KB\n", app_partition->size / 1024);
/* 跳转到应用程序 */
JumpToApplication(app_partition->start_addr);
}
while(1);
}
/* 固件升级时使用分区表 */
void FirmwareUpgrade(void)
{
/* 获取升级分区信息 */
const PartitionInfo *upgrade_partition =
PartitionTable_GetInfo(PARTITION_TYPE_UPGRADE);
if (upgrade_partition == NULL) {
printf("Upgrade partition not found!\n");
return;
}
/* 擦除升级分区 */
printf("Erasing upgrade partition...\n");
for (int i = 0; i < upgrade_partition->sector_count; i++) {
uint8_t sector = upgrade_partition->start_sector + i;
Flash_EraseSector(sector);
}
/* 接收并写入固件 */
uint32_t write_addr = upgrade_partition->start_addr;
/* ... 固件下载和写入代码 ... */
/* 验证固件 */
/* ... 固件校验代码 ... */
/* 复制到应用程序分区 */
const PartitionInfo *app_partition =
PartitionTable_GetInfo(PARTITION_TYPE_APPLICATION);
CopyFirmware(upgrade_partition->start_addr,
app_partition->start_addr,
app_partition->size);
}
第六部分: 空间优化技巧¶
6.1 代码优化¶
编译器优化选项:
# Makefile中的优化选项
# 优化级别
CFLAGS += -O2 # 平衡优化(推荐)
# CFLAGS += -Os # 优化代码大小
# CFLAGS += -O3 # 最大性能优化
# 链接时优化
CFLAGS += -flto # Link Time Optimization
# 移除未使用的函数和数据
CFLAGS += -ffunction-sections
CFLAGS += -fdata-sections
LDFLAGS += -Wl,--gc-sections
# 不使用标准库启动文件
LDFLAGS += -nostartfiles
代码大小对比:
优化级别对比(示例项目):
-O0 (无优化): 45.2 KB
-O1 (基本优化): 32.8 KB (-27%)
-O2 (推荐): 28.4 KB (-37%)
-Os (大小优化): 26.1 KB (-42%)
-O3 (性能优化): 35.6 KB (-21%)
结论: -Os或-O2适合空间受限的应用
6.2 数据优化¶
常量数据优化:
/* 1. 使用const修饰符,将数据放在Flash中 */
const uint8_t lookup_table[256] = { /* ... */ }; // 存储在Flash
uint8_t lookup_table[256] = { /* ... */ }; // 存储在RAM!
/* 2. 使用字符串常量 */
const char *msg = "Hello"; // 字符串在Flash,指针在RAM
char msg[] = "Hello"; // 整个数组在RAM!
/* 3. 大数组使用Flash存储 */
const uint32_t big_array[1000] __attribute__((section(".rodata"))) = {
/* ... */
};
RAM使用优化:
/* 1. 减少全局变量 */
// 不好的做法
uint8_t buffer1[1024];
uint8_t buffer2[1024];
uint8_t buffer3[1024];
// 好的做法 - 使用联合体共享内存
union {
uint8_t buffer1[1024];
uint8_t buffer2[1024];
uint8_t buffer3[1024];
} shared_buffer;
/* 2. 使用动态内存分配 */
// 不好 - 始终占用内存
uint8_t large_buffer[4096];
// 好 - 需要时才分配
uint8_t *large_buffer = malloc(4096);
/* 使用完后释放 */
free(large_buffer);
/* 3. 使用位域节省空间 */
// 不好 - 占用4字节
struct {
uint8_t flag1;
uint8_t flag2;
uint8_t flag3;
uint8_t flag4;
} flags;
// 好 - 占用1字节
struct {
uint8_t flag1 : 1;
uint8_t flag2 : 1;
uint8_t flag3 : 1;
uint8_t flag4 : 1;
} flags;
6.3 Flash使用优化¶
压缩技术:
/* 使用压缩算法减小固件大小 */
/* 1. 简单的RLE压缩(适合重复数据) */
typedef struct {
uint8_t value;
uint8_t count;
} RLE_Pair;
/* 2. LZ77压缩(通用压缩) */
/* 需要引入压缩库,如miniz */
/* 3. 差分压缩(固件升级) */
/* 只传输和存储变化的部分 */
分段加载:
/* 将大型数据分段存储和加载 */
#define SEGMENT_SIZE 4096
typedef struct {
uint32_t offset;
uint32_t size;
uint32_t crc;
} DataSegment;
/* 按需加载数据段 */
void LoadSegment(uint8_t segment_id)
{
const DataSegment *seg = &segments[segment_id];
/* 从Flash读取到RAM缓冲区 */
memcpy(ram_buffer,
(void*)(FLASH_BASE + seg->offset),
seg->size);
/* 验证CRC */
if (CalculateCRC(ram_buffer, seg->size) != seg->crc) {
/* 错误处理 */
}
}
实践示例¶
示例1: 创建完整的分区方案¶
需求: 为一个IoT设备设计分区方案,支持OTA升级。
硬件: STM32F407, 1MB Flash, 128KB RAM
设计方案:
/* partition_config.h */
/* Flash总容量 */
#define FLASH_BASE 0x08000000
#define FLASH_SIZE 0x00100000 /* 1MB */
/* 分区定义 */
#define BOOTLOADER_BASE 0x08000000
#define BOOTLOADER_SIZE 0x00008000 /* 32KB */
#define APP_BASE 0x08008000
#define APP_SIZE 0x00078000 /* 480KB */
#define UPGRADE_BASE 0x08080000
#define UPGRADE_SIZE 0x00078000 /* 480KB */
#define PARAM_BASE 0x080F8000
#define PARAM_SIZE 0x00004000 /* 16KB */
#define LOG_BASE 0x080FC000
#define LOG_SIZE 0x00004000 /* 16KB */
/* 分区表可视化 */
/*
* 0x08000000 ┌─────────────────┐
* │ Bootloader │ 32KB (扇区0-1)
* 0x08008000 ├─────────────────┤
* │ Application │ 480KB (扇区2-7)
* 0x08080000 ├─────────────────┤
* │ Upgrade Cache │ 480KB (扇区8-10)
* 0x080F8000 ├─────────────────┤
* │ Parameters │ 16KB (扇区11前半)
* 0x080FC000 ├─────────────────┤
* │ Log │ 16KB (扇区11后半)
* 0x08100000 └─────────────────┘
*/
/* 扇区映射 */
static const uint8_t partition_sectors[][2] = {
/* {起始扇区, 扇区数量} */
{0, 2}, /* Bootloader: 扇区0-1 */
{2, 6}, /* Application: 扇区2-7 */
{8, 3}, /* Upgrade: 扇区8-10 */
{11, 1}, /* Parameters: 扇区11 */
{11, 1} /* Log: 扇区11 */
};
链接脚本配置:
/* Bootloader链接脚本 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
/* 应用程序链接脚本 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 480K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
示例2: 实现参数存储管理¶
参数存储设计:
/* parameter.h */
#ifndef __PARAMETER_H
#define __PARAMETER_H
#include <stdint.h>
/* 参数区魔数 */
#define PARAM_MAGIC 0x50415241 /* "PARA" */
#define PARAM_VERSION 0x0100 /* v1.0 */
/* 参数结构 */
typedef struct {
uint32_t magic; /* 魔数 */
uint16_t version; /* 版本号 */
uint16_t length; /* 数据长度 */
/* 系统参数 */
uint32_t boot_count; /* 启动次数 */
uint32_t run_time; /* 运行时间(秒) */
uint8_t boot_mode; /* 启动模式 */
uint8_t reserved[3]; /* 保留 */
/* 网络参数 */
uint8_t ip_addr[4]; /* IP地址 */
uint8_t netmask[4]; /* 子网掩码 */
uint8_t gateway[4]; /* 网关 */
uint8_t mac_addr[6]; /* MAC地址 */
uint16_t port; /* 端口号 */
/* 用户参数 */
char device_name[32]; /* 设备名称 */
char device_id[16]; /* 设备ID */
/* 校验 */
uint32_t crc32; /* CRC校验 */
} SystemParameters;
/* 函数声明 */
void Param_Init(void);
void Param_Load(void);
void Param_Save(void);
void Param_Reset(void);
SystemParameters* Param_Get(void);
#endif /* __PARAMETER_H */
参数管理实现:
/* parameter.c */
#include "parameter.h"
#include "flash.h"
#include <string.h>
/* 参数存储地址 */
#define PARAM_FLASH_ADDR 0x080F8000
/* 默认参数 */
static const SystemParameters default_params = {
.magic = PARAM_MAGIC,
.version = PARAM_VERSION,
.length = sizeof(SystemParameters),
.boot_count = 0,
.run_time = 0,
.boot_mode = 0,
.ip_addr = {192, 168, 1, 100},
.netmask = {255, 255, 255, 0},
.gateway = {192, 168, 1, 1},
.mac_addr = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55},
.port = 8080,
.device_name = "IoT Device",
.device_id = "DEV001",
.crc32 = 0
};
/* 当前参数(RAM中) */
static SystemParameters current_params;
/* 初始化参数 */
void Param_Init(void)
{
/* 从Flash加载参数 */
Param_Load();
/* 增加启动次数 */
current_params.boot_count++;
/* 保存参数 */
Param_Save();
}
/* 从Flash加载参数 */
void Param_Load(void)
{
SystemParameters *flash_params = (SystemParameters*)PARAM_FLASH_ADDR;
/* 检查魔数 */
if (flash_params->magic != PARAM_MAGIC) {
/* 参数无效,使用默认值 */
memcpy(¤t_params, &default_params, sizeof(SystemParameters));
return;
}
/* 检查版本 */
if (flash_params->version != PARAM_VERSION) {
/* 版本不匹配,使用默认值 */
memcpy(¤t_params, &default_params, sizeof(SystemParameters));
return;
}
/* 验证CRC */
uint32_t calc_crc = CalculateCRC32((uint8_t*)flash_params,
sizeof(SystemParameters) - 4);
if (calc_crc != flash_params->crc32) {
/* CRC错误,使用默认值 */
memcpy(¤t_params, &default_params, sizeof(SystemParameters));
return;
}
/* 加载参数 */
memcpy(¤t_params, flash_params, sizeof(SystemParameters));
}
/* 保存参数到Flash */
void Param_Save(void)
{
/* 计算CRC */
current_params.crc32 = CalculateCRC32((uint8_t*)¤t_params,
sizeof(SystemParameters) - 4);
/* 解锁Flash */
Flash_Unlock();
/* 擦除参数扇区 */
Flash_EraseSector(11);
/* 写入参数 */
Flash_WriteBuffer(PARAM_FLASH_ADDR,
(uint8_t*)¤t_params,
sizeof(SystemParameters));
/* 锁定Flash */
Flash_Lock();
}
/* 重置为默认参数 */
void Param_Reset(void)
{
memcpy(¤t_params, &default_params, sizeof(SystemParameters));
Param_Save();
}
/* 获取参数指针 */
SystemParameters* Param_Get(void)
{
return ¤t_params;
}
示例3: 实现日志存储¶
循环日志缓冲区:
/* log_storage.h */
#ifndef __LOG_STORAGE_H
#define __LOG_STORAGE_H
#include <stdint.h>
/* 日志级别 */
typedef enum {
LOG_LEVEL_DEBUG = 0,
LOG_LEVEL_INFO,
LOG_LEVEL_WARNING,
LOG_LEVEL_ERROR
} LogLevel;
/* 日志条目 */
typedef struct {
uint32_t timestamp; /* 时间戳 */
LogLevel level; /* 日志级别 */
uint16_t length; /* 消息长度 */
char message[128]; /* 日志消息 */
} LogEntry;
/* 函数声明 */
void Log_Init(void);
void Log_Write(LogLevel level, const char *format, ...);
void Log_Read(LogEntry *entry, uint32_t index);
uint32_t Log_GetCount(void);
void Log_Clear(void);
#endif /* __LOG_STORAGE_H */
日志实现:
/* log_storage.c */
#include "log_storage.h"
#include "flash.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
/* 日志存储地址 */
#define LOG_FLASH_ADDR 0x080FC000
#define LOG_FLASH_SIZE 0x00004000 /* 16KB */
#define MAX_LOG_ENTRIES (LOG_FLASH_SIZE / sizeof(LogEntry))
/* 日志头部 */
typedef struct {
uint32_t magic; /* 魔数 */
uint32_t write_index; /* 写入索引 */
uint32_t count; /* 日志数量 */
} LogHeader;
#define LOG_MAGIC 0x4C4F4721 /* "LOG!" */
/* 日志头部地址 */
#define LOG_HEADER_ADDR LOG_FLASH_ADDR
#define LOG_DATA_ADDR (LOG_FLASH_ADDR + sizeof(LogHeader))
/* 初始化日志系统 */
void Log_Init(void)
{
LogHeader *header = (LogHeader*)LOG_HEADER_ADDR;
/* 检查魔数 */
if (header->magic != LOG_MAGIC) {
/* 初始化日志区域 */
Log_Clear();
}
}
/* 写入日志 */
void Log_Write(LogLevel level, const char *format, ...)
{
LogHeader *header = (LogHeader*)LOG_HEADER_ADDR;
LogEntry entry;
va_list args;
/* 格式化日志消息 */
va_start(args, format);
vsnprintf(entry.message, sizeof(entry.message), format, args);
va_end(args);
/* 填充日志条目 */
entry.timestamp = HAL_GetTick();
entry.level = level;
entry.length = strlen(entry.message);
/* 计算写入地址 */
uint32_t write_addr = LOG_DATA_ADDR +
(header->write_index % MAX_LOG_ENTRIES) * sizeof(LogEntry);
/* 如果跨越扇区边界,需要擦除 */
if ((header->write_index % (LOG_FLASH_SIZE / sizeof(LogEntry))) == 0) {
Flash_Unlock();
Flash_EraseSector(11);
Flash_Lock();
}
/* 写入日志 */
Flash_Unlock();
Flash_WriteBuffer(write_addr, (uint8_t*)&entry, sizeof(LogEntry));
/* 更新头部 */
header->write_index++;
if (header->count < MAX_LOG_ENTRIES) {
header->count++;
}
Flash_Lock();
}
/* 读取日志 */
void Log_Read(LogEntry *entry, uint32_t index)
{
if (index >= Log_GetCount()) {
return;
}
uint32_t read_addr = LOG_DATA_ADDR + index * sizeof(LogEntry);
memcpy(entry, (void*)read_addr, sizeof(LogEntry));
}
/* 获取日志数量 */
uint32_t Log_GetCount(void)
{
LogHeader *header = (LogHeader*)LOG_HEADER_ADDR;
return header->count;
}
/* 清空日志 */
void Log_Clear(void)
{
LogHeader header = {
.magic = LOG_MAGIC,
.write_index = 0,
.count = 0
};
Flash_Unlock();
Flash_EraseSector(11);
Flash_WriteBuffer(LOG_HEADER_ADDR, (uint8_t*)&header, sizeof(LogHeader));
Flash_Lock();
}
深入理解¶
内存对齐的重要性¶
为什么需要对齐:
- 性能: 未对齐的访问可能需要多次内存访问
- 硬件要求: 某些架构要求特定数据类型必须对齐
- 原子操作: 原子操作通常要求数据对齐
对齐规则:
/* ARM Cortex-M对齐要求 */
struct AlignmentExample {
uint8_t a; /* 1字节对齐 */
uint16_t b; /* 2字节对齐 */
uint32_t c; /* 4字节对齐 */
uint64_t d; /* 8字节对齐 */
};
/* 实际内存布局(考虑对齐) */
/*
* offset 0: a (1字节)
* offset 1: padding (1字节)
* offset 2: b (2字节)
* offset 4: c (4字节)
* offset 8: d (8字节)
* 总大小: 16字节
*/
/* 优化后的结构(减少padding) */
struct OptimizedAlignment {
uint64_t d; /* 8字节 */
uint32_t c; /* 4字节 */
uint16_t b; /* 2字节 */
uint8_t a; /* 1字节 */
uint8_t padding; /* 1字节padding */
};
/* 总大小: 16字节(相同),但更紧凑 */
强制对齐:
/* 使用__attribute__强制对齐 */
/* 4字节对齐 */
uint8_t buffer[100] __attribute__((aligned(4)));
/* 扇区对齐(4KB) */
const uint8_t data[4096] __attribute__((aligned(4096))) = { /* ... */ };
/* 在链接脚本中对齐 */
.my_section :
{
. = ALIGN(4096); /* 4KB对齐 */
*(.my_section)
. = ALIGN(4096);
} > FLASH
Flash磨损均衡¶
为什么需要磨损均衡:
Flash有擦写次数限制(通常10万-100万次),频繁擦写同一区域会导致Flash损坏。
磨损均衡策略:
/* 简单的轮转写入策略 */
#define SECTOR_COUNT 4
#define SECTOR_SIZE 0x4000
typedef struct {
uint32_t sequence; /* 序列号 */
uint32_t erase_count; /* 擦除次数 */
uint8_t data[SECTOR_SIZE - 8];
} WearLevelingSector;
/* 查找最新的扇区 */
uint8_t FindLatestSector(void)
{
uint32_t max_sequence = 0;
uint8_t latest_sector = 0;
for (uint8_t i = 0; i < SECTOR_COUNT; i++) {
WearLevelingSector *sector =
(WearLevelingSector*)(FLASH_BASE + i * SECTOR_SIZE);
if (sector->sequence > max_sequence) {
max_sequence = sector->sequence;
latest_sector = i;
}
}
return latest_sector;
}
/* 写入数据(轮转到下一个扇区) */
void WriteWithWearLeveling(const uint8_t *data, uint32_t len)
{
/* 找到当前扇区 */
uint8_t current_sector = FindLatestSector();
/* 计算下一个扇区 */
uint8_t next_sector = (current_sector + 1) % SECTOR_COUNT;
/* 读取当前扇区信息 */
WearLevelingSector *current =
(WearLevelingSector*)(FLASH_BASE + current_sector * SECTOR_SIZE);
/* 准备新扇区数据 */
WearLevelingSector new_sector;
new_sector.sequence = current->sequence + 1;
new_sector.erase_count = current->erase_count + 1;
memcpy(new_sector.data, data, len);
/* 擦除并写入新扇区 */
Flash_Unlock();
Flash_EraseSector(next_sector);
Flash_WriteBuffer(FLASH_BASE + next_sector * SECTOR_SIZE,
(uint8_t*)&new_sector,
sizeof(WearLevelingSector));
Flash_Lock();
}
安全启动与固件验证¶
固件签名验证:
/* 固件头部结构 */
typedef struct {
uint32_t magic; /* 魔数 */
uint32_t version; /* 版本号 */
uint32_t size; /* 固件大小 */
uint32_t crc32; /* CRC校验 */
uint8_t signature[256]; /* RSA签名 */
uint8_t reserved[256]; /* 保留 */
} FirmwareHeader;
/* 验证固件 */
bool VerifyFirmware(uint32_t firmware_addr)
{
FirmwareHeader *header = (FirmwareHeader*)firmware_addr;
/* 1. 检查魔数 */
if (header->magic != FIRMWARE_MAGIC) {
return false;
}
/* 2. 验证CRC */
uint32_t calc_crc = CalculateCRC32(
(uint8_t*)(firmware_addr + sizeof(FirmwareHeader)),
header->size
);
if (calc_crc != header->crc32) {
return false;
}
/* 3. 验证数字签名(可选) */
#ifdef ENABLE_SIGNATURE_VERIFY
if (!RSA_Verify(header->signature,
(uint8_t*)(firmware_addr + sizeof(FirmwareHeader)),
header->size)) {
return false;
}
#endif
return true;
}
常见问题¶
Q1: 如何确定合适的分区大小?¶
A: 确定分区大小需要考虑以下因素:
- 当前需求: 测量当前固件的实际大小
- 增长空间: 预留30-50%的增长空间
- 扇区对齐: 分区边界必须对齐到扇区边界
- 功能需求: 是否需要OTA升级、日志存储等
示例计算:
假设当前应用程序大小: 200KB
预留增长空间(50%): 200KB × 1.5 = 300KB
扇区对齐: 向上取整到扇区边界
- 如果使用128KB扇区: 3个扇区 = 384KB
- 如果使用64KB扇区: 5个扇区 = 320KB
推荐分配: 384KB或更大
Q2: Bootloader和应用程序可以共享RAM吗?¶
A: 可以,但需要注意:
- 跳转前清理: Bootloader跳转前应清理使用的RAM
- 栈指针重置: 应用程序要重新设置栈指针
- 全局变量: 应用程序的全局变量会覆盖Bootloader的
- 共享数据: 如需传递数据,使用固定地址的共享区域
共享RAM示例:
/* 定义共享数据区域 */
#define SHARED_RAM_ADDR 0x2001F000
#define SHARED_RAM_SIZE 0x1000 /* 4KB */
typedef struct {
uint32_t magic;
uint32_t boot_reason;
uint32_t upgrade_flag;
/* ... 其他共享数据 ... */
} SharedData;
/* Bootloader中写入共享数据 */
void Bootloader_SetSharedData(void)
{
SharedData *shared = (SharedData*)SHARED_RAM_ADDR;
shared->magic = 0x12345678;
shared->boot_reason = BOOT_REASON_NORMAL;
}
/* 应用程序中读取共享数据 */
void Application_GetSharedData(void)
{
SharedData *shared = (SharedData*)SHARED_RAM_ADDR;
if (shared->magic == 0x12345678) {
/* 读取共享数据 */
uint32_t reason = shared->boot_reason;
}
}
Q3: 如何处理Flash扇区大小不均匀的问题?¶
A: STM32F4系列Flash扇区大小不均匀,需要特殊处理:
策略1: 使用小扇区存储关键数据
/* 将Bootloader放在小扇区(16KB) */
#define BOOTLOADER_BASE 0x08000000 /* 扇区0-1 */
#define BOOTLOADER_SIZE 0x00008000 /* 32KB */
/* 将参数放在小扇区 */
#define PARAM_BASE 0x0800C000 /* 扇区3 */
#define PARAM_SIZE 0x00004000 /* 16KB */
策略2: 合并使用大扇区
策略3: 创建扇区映射表
/* 扇区信息表 */
typedef struct {
uint32_t base_addr;
uint32_t size;
} SectorInfo;
const SectorInfo sector_table[] = {
{0x08000000, 0x4000}, /* 扇区0: 16KB */
{0x08004000, 0x4000}, /* 扇区1: 16KB */
{0x08008000, 0x4000}, /* 扇区2: 16KB */
{0x0800C000, 0x4000}, /* 扇区3: 16KB */
{0x08010000, 0x10000}, /* 扇区4: 64KB */
{0x08020000, 0x20000}, /* 扇区5: 128KB */
/* ... */
};
/* 根据地址查找扇区 */
uint8_t GetSectorByAddress(uint32_t addr)
{
for (uint8_t i = 0; i < sizeof(sector_table)/sizeof(SectorInfo); i++) {
if (addr >= sector_table[i].base_addr &&
addr < sector_table[i].base_addr + sector_table[i].size) {
return i;
}
}
return 0xFF; /* 无效地址 */
}
Q4: 如何实现固件回滚功能?¶
A: 固件回滚需要保留旧版本固件:
方案1: 双区备份
/* 分区布局 */
#define APP_A_BASE 0x08008000 /* 应用程序A */
#define APP_B_BASE 0x08080000 /* 应用程序B */
/* 启动标志 */
typedef struct {
uint32_t magic;
uint8_t active_partition; /* 0=A, 1=B */
uint8_t boot_count; /* 启动计数 */
uint8_t max_boot_attempts; /* 最大尝试次数 */
} BootFlag;
/* Bootloader启动逻辑 */
void Bootloader_Main(void)
{
BootFlag *flag = (BootFlag*)BOOT_FLAG_ADDR;
/* 增加启动计数 */
flag->boot_count++;
/* 检查是否超过最大尝试次数 */
if (flag->boot_count > flag->max_boot_attempts) {
/* 切换到备份分区 */
flag->active_partition = 1 - flag->active_partition;
flag->boot_count = 0;
}
/* 跳转到活动分区 */
uint32_t app_addr = (flag->active_partition == 0) ?
APP_A_BASE : APP_B_BASE;
JumpToApplication(app_addr);
}
/* 应用程序启动成功后清零计数 */
void Application_Init(void)
{
BootFlag *flag = (BootFlag*)BOOT_FLAG_ADDR;
flag->boot_count = 0; /* 启动成功,清零计数 */
}
Q5: 如何优化链接脚本以减小固件大小?¶
A: 链接脚本优化技巧:
1. 移除未使用的段
2. 合并相似的段
3. 使用链接时优化
4. 查看段大小
# 查看各段大小
arm-none-eabi-size -A firmware.elf
# 查看符号大小
arm-none-eabi-nm --size-sort firmware.elf | tail -20
总结¶
本教程深入讲解了固件分区与内存布局设计的核心知识,让我们回顾一下要点:
- 存储器特性: 理解Flash和RAM的特性,合理分配使用
- 分区设计原则: 功能隔离、安全保护、扩展性、对齐、性能优化
- 链接脚本: 掌握链接脚本的语法和配置方法
- 地址映射: 理解ARM地址空间和中断向量表重定位
- 分区表管理: 实现灵活的分区表系统
- 空间优化: 通过编译优化、代码优化、数据优化减小固件大小
合理的固件分区和内存布局是嵌入式系统稳定运行的基础。在实际项目中,需要根据具体需求权衡各种因素,设计出最适合的方案。
关键要点:
- 分区边界必须对齐到扇区边界
- 预留足够的扩展空间
- 关键区域要设置保护
- 使用分区表管理更灵活
- 注意Flash磨损均衡
- 实现固件验证机制
延伸阅读¶
推荐进一步学习的资源:
相关文章: - Bootloader基础概念与工作原理 - 回顾基础知识 - 从零实现一个简单的Bootloader - 实践Bootloader开发 - U-Boot架构与移植概述 - 学习专业Bootloader - Bootloader与应用程序通信机制 - 数据传递方法 - IAP在线升级功能实现 - 固件升级实践
官方文档: - STM32F4参考手册 - Flash和内存详细说明 - ARM Cortex-M4编程手册 - 地址空间和VTOR - GNU LD链接器手册 - 链接脚本语法
推荐工具: - STM32CubeMX - 自动生成初始化代码 - STM32CubeProgrammer - Flash编程工具 - Binary Viewer - 查看二进制文件
参考资料¶
- STM32F4xx参考手册 - STMicroelectronics
- ARM Cortex-M4编程手册 - ARM Ltd.
- GNU Linker (LD) 手册 - Free Software Foundation
- 嵌入式系统设计与实践 - Elecia White
- The Definitive Guide to ARM Cortex-M3/M4 - Joseph Yiu
- Mastering STM32 - Carmine Noviello
练习题:
- 为一个512KB Flash的MCU设计分区方案,要求支持IAP升级和参数存储。
- 编写一个链接脚本,将Bootloader放在0x08000000,应用程序放在0x08010000。
- 实现一个函数,根据地址自动计算对应的Flash扇区号。
- 设计一个参数存储系统,支持参数备份和恢复功能。
- 实现一个简单的磨损均衡算法,用于频繁更新的数据存储。
- 计算以下结构体的实际大小(考虑对齐):
- 解释为什么应用程序需要在SystemInit()中重新设置VTOR寄存器。
实践任务:
- 分区方案设计: 为你的项目设计完整的Flash分区方案,画出分区图。
- 链接脚本编写: 为Bootloader和应用程序编写链接脚本,并验证编译结果。
- 分区表实现: 实现一个分区表管理系统,支持动态查询分区信息。
- 参数存储: 实现参数存储功能,支持参数的保存、加载和重置。
- 空间优化: 使用编译优化选项,对比不同优化级别的固件大小。
下一步: 建议学习 Bootloader与应用程序通信机制,了解如何在Bootloader和应用程序之间传递数据。