嵌入式系统内存架构详解¶
学习目标¶
完成本文学习后,你将能够:
- 理解嵌入式系统中各种存储器的特性和区别
- 掌握Flash存储器的工作原理和使用方法
- 了解SRAM的特点和配置方式
- 区分ROM、EEPROM和Flash的应用场景
- 理解内存映射机制和地址空间布局
- 能够编写访问不同内存区域的代码
前置要求¶
在开始本文学习之前,你需要:
知识要求: - 了解二进制和十六进制数制 - 熟悉C语言基础(指针、数组) - 了解微控制器的基本概念 - 有一定的数字电路基础
技能要求: - 能够阅读简单的C代码 - 了解基本的计算机组成原理 - 熟悉常用的数据类型和大小
推荐但非必需: - 有ARM Cortex-M或其他MCU的使用经验 - 了解汇编语言基础 - 熟悉嵌入式开发环境
概述¶
内存是嵌入式系统的核心组成部分,就像人的大脑需要记忆一样,微控制器也需要各种类型的存储器来保存程序代码、运行数据和配置信息。理解不同类型存储器的特性和使用方法,是掌握嵌入式开发的基础。
为什么要学习内存架构¶
- 程序存储:代码需要存储在非易失性存储器中
- 数据管理:运行时数据需要快速的RAM支持
- 性能优化:合理使用内存可以提升系统性能
- 资源规划:有限的内存需要精心规划和管理
- 故障排查:很多问题都与内存使用有关
嵌入式系统内存层次¶
┌─────────────────────────────────────┐
│ CPU寄存器 │ 最快,容量最小
│ (32个 × 32位) │
├─────────────────────────────────────┤
│ Cache缓存 │ 快速,容量小
│ (可选,高端MCU) │
├─────────────────────────────────────┤
│ SRAM (静态RAM) │ 快速,易失性
│ (8KB - 256KB) │
├─────────────────────────────────────┤
│ Flash (闪存) │ 较慢,非易失性
│ (32KB - 2MB) │
├─────────────────────────────────────┤
│ 外部存储 │ 最慢,容量最大
│ (SD卡、SPI Flash等) │
└─────────────────────────────────────┘
第一部分:Flash存储器¶
什么是Flash存储器¶
Flash(闪存)是一种非易失性存储器,即使断电后数据也不会丢失。在嵌入式系统中,Flash主要用于存储程序代码和常量数据。
Flash的核心特点:
- 非易失性:
- 断电后数据保持
- 适合存储程序代码
-
可靠性高
-
读取快速:
- 读取速度接近SRAM
- 支持随机访问
-
可以直接执行代码(XIP)
-
写入较慢:
- 需要先擦除后写入
- 擦除以块为单位
-
写入次数有限(10万-100万次)
-
容量较大:
- 典型容量:32KB - 2MB
- 成本相对较低
- 集成在芯片内部
Flash的工作原理¶
Flash存储器基于浮栅晶体管技术,通过电荷的存储和释放来表示0和1。
存储单元结构:
控制栅极
│
┌──────┴──────┐
│ │
│ 浮栅极 │ ← 存储电荷
│ │
├─────────────┤
│ 氧化层 │
├─────────────┤
│ 硅基底 │
└─────────────┘
源极 漏极
操作过程:
- 编程(写入):
- 施加高电压
- 电子注入浮栅极
-
存储单元变为"0"状态
-
擦除:
- 施加反向高电压
- 电子从浮栅极移除
-
存储单元变为"1"状态
-
读取:
- 施加正常电压
- 检测电流大小
- 判断存储状态
Flash的组织结构¶
Flash存储器通常按照页(Page)和扇区(Sector)组织:
Flash存储器 (128KB)
├── 扇区0 (4KB)
│ ├── 页0 (256字节)
│ ├── 页1 (256字节)
│ ├── ...
│ └── 页15 (256字节)
├── 扇区1 (4KB)
│ └── ...
├── ...
└── 扇区31 (4KB)
关键概念:
- 页(Page):最小写入单位,通常256字节或512字节
- 扇区(Sector):最小擦除单位,通常4KB或更大
- 块(Block):多个扇区组成,某些Flash使用
Flash的使用限制¶
- 擦除限制:
- 必须先擦除后写入
- 擦除以扇区为单位
-
不能单独擦除某个字节
-
写入限制:
- 只能将1改为0
- 不能将0改为1(需要先擦除)
-
写入以页为单位
-
寿命限制:
- 擦写次数有限
- 典型值:10万-100万次
- 需要磨损均衡算法
Flash编程示例¶
以STM32为例,演示Flash的读写操作:
#include "stm32f1xx_hal.h"
// Flash基地址
#define FLASH_BASE_ADDR 0x08000000
#define FLASH_USER_START 0x0800F000 // 用户数据区起始地址
#define FLASH_PAGE_SIZE 0x400 // 1KB页大小
/**
* @brief 读取Flash数据
* @param address: Flash地址
* @retval 读取的32位数据
*/
uint32_t Flash_Read(uint32_t address) {
return *(__IO uint32_t*)address;
}
/**
* @brief 擦除Flash页
* @param page_address: 页起始地址
* @retval HAL状态
*/
HAL_StatusTypeDef Flash_ErasePage(uint32_t page_address) {
HAL_StatusTypeDef status;
FLASH_EraseInitTypeDef erase_init;
uint32_t page_error = 0;
// 解锁Flash
HAL_FLASH_Unlock();
// 配置擦除参数
erase_init.TypeErase = FLASH_TYPEERASE_PAGES;
erase_init.PageAddress = page_address;
erase_init.NbPages = 1;
// 执行擦除
status = HAL_FLASHEx_Erase(&erase_init, &page_error);
// 锁定Flash
HAL_FLASH_Lock();
return status;
}
/**
* @brief 写入Flash数据
* @param address: 写入地址(必须是字对齐)
* @param data: 要写入的数据
* @retval HAL状态
*/
HAL_StatusTypeDef Flash_Write(uint32_t address, uint32_t data) {
HAL_StatusTypeDef status;
// 解锁Flash
HAL_FLASH_Unlock();
// 写入数据
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, data);
// 锁定Flash
HAL_FLASH_Lock();
return status;
}
/**
* @brief Flash读写示例
*/
void Flash_Example(void) {
uint32_t data_to_write = 0x12345678;
uint32_t data_read;
// 1. 擦除页
if (Flash_ErasePage(FLASH_USER_START) == HAL_OK) {
printf("Flash页擦除成功\n");
}
// 2. 写入数据
if (Flash_Write(FLASH_USER_START, data_to_write) == HAL_OK) {
printf("Flash写入成功\n");
}
// 3. 读取数据
data_read = Flash_Read(FLASH_USER_START);
// 4. 验证数据
if (data_read == data_to_write) {
printf("Flash读写验证成功: 0x%08X\n", data_read);
} else {
printf("Flash读写验证失败\n");
}
}
代码说明:
- Flash_Read:直接通过指针读取Flash内容
- Flash_ErasePage:擦除指定页,必须在写入前执行
- Flash_Write:写入32位数据到指定地址
- Flash_Example:完整的擦除-写入-读取-验证流程
注意事项:
- 写入前必须先擦除
- 地址必须对齐(通常4字节对齐)
- 操作Flash时需要解锁和锁定
- 避免频繁擦写同一区域
Flash的应用场景¶
- 程序代码存储:
- 主程序代码
- 中断向量表
-
常量数据
-
配置参数存储:
- 系统配置
- 校准数据
-
用户设置
-
固件升级:
- Bootloader
- 应用程序更新
-
双区备份
-
数据记录:
- 日志信息
- 历史数据
- 事件记录
第二部分:SRAM存储器¶
什么是SRAM¶
SRAM(Static Random Access Memory,静态随机存取存储器)是一种易失性存储器,用于存储程序运行时的变量、堆栈和临时数据。
SRAM的核心特点:
- 易失性:
- 断电后数据丢失
- 需要持续供电
-
适合临时数据
-
读写快速:
- 访问速度快(几纳秒)
- 无需刷新
-
支持随机访问
-
容量有限:
- 典型容量:8KB - 256KB
- 成本较高
-
功耗相对较大
-
使用简单:
- 读写操作简单
- 无擦除限制
- 无寿命限制
SRAM的工作原理¶
SRAM使用双稳态触发器存储数据,每个存储单元由6个晶体管组成。
存储单元结构:
工作特点:
- 使用锁存器结构
- 数据稳定存储
- 无需刷新电路
- 功耗相对较高
SRAM的组织结构¶
在嵌入式系统中,SRAM通常分为几个区域:
SRAM (32KB)
├── 栈区 (Stack)
│ ├── 局部变量
│ ├── 函数参数
│ └── 返回地址
├── 堆区 (Heap)
│ ├── 动态分配内存
│ └── malloc/free管理
├── BSS段
│ └── 未初始化全局变量
├── Data段
│ └── 已初始化全局变量
└── 保留区
└── 系统使用
各区域说明:
- 栈区(Stack):
- 从高地址向低地址增长
- 存储局部变量和函数调用信息
- 自动管理,LIFO结构
-
大小固定,溢出会导致系统崩溃
-
堆区(Heap):
- 从低地址向高地址增长
- 用于动态内存分配
- 需要手动管理(malloc/free)
-
大小可变,但总量有限
-
BSS段:
- 存储未初始化的全局变量
- 启动时自动清零
-
不占用Flash空间
-
Data段:
- 存储已初始化的全局变量
- 启动时从Flash复制到SRAM
- 占用Flash和SRAM空间
SRAM访问示例¶
#include <stdint.h>
#include <stdlib.h>
// 全局变量(Data段)
uint32_t g_initialized_var = 0x12345678;
// 未初始化全局变量(BSS段)
uint32_t g_uninitialized_var;
// 静态变量(Data段或BSS段)
static uint32_t s_static_var = 100;
/**
* @brief SRAM使用示例
*/
void SRAM_Example(void) {
// 局部变量(栈区)
uint32_t local_var = 0xABCDEF00;
uint32_t array[10]; // 栈上的数组
// 动态分配内存(堆区)
uint32_t *heap_ptr = (uint32_t*)malloc(sizeof(uint32_t) * 100);
if (heap_ptr != NULL) {
// 使用动态分配的内存
for (int i = 0; i < 100; i++) {
heap_ptr[i] = i;
}
// 释放内存
free(heap_ptr);
}
// 访问全局变量
g_initialized_var++;
g_uninitialized_var = 0x55AA;
// 访问静态变量
s_static_var += 10;
}
/**
* @brief 获取栈使用情况
* @note 需要在链接脚本中定义_estack符号
*/
uint32_t Get_Stack_Usage(void) {
extern uint32_t _estack; // 栈顶地址(链接脚本定义)
uint32_t stack_top = (uint32_t)&_estack;
uint32_t current_sp;
// 获取当前栈指针
__asm volatile ("mov %0, sp" : "=r" (current_sp));
// 计算已使用的栈空间
return stack_top - current_sp;
}
/**
* @brief 检测栈溢出
* @retval 1: 栈溢出, 0: 正常
*/
int Check_Stack_Overflow(void) {
extern uint32_t _sstack; // 栈底地址
uint32_t stack_bottom = (uint32_t)&_sstack;
uint32_t current_sp;
__asm volatile ("mov %0, sp" : "=r" (current_sp));
// 检查是否接近栈底
if (current_sp < stack_bottom + 256) { // 保留256字节安全区
return 1; // 栈溢出警告
}
return 0;
}
代码说明:
- 变量存储位置:
- 全局变量存储在Data段或BSS段
- 局部变量存储在栈区
-
动态分配的内存在堆区
-
栈使用监控:
- 通过SP寄存器获取当前栈位置
- 计算已使用的栈空间
-
检测栈溢出风险
-
内存管理:
- 使用malloc动态分配
- 使用free释放内存
- 避免内存泄漏
SRAM使用注意事项¶
-
栈溢出预防:
-
内存泄漏避免:
-
全局变量使用:
SRAM的应用场景¶
- 程序运行数据:
- 局部变量
- 函数参数
-
返回地址
-
全局数据存储:
- 全局变量
- 静态变量
-
共享数据
-
缓冲区:
- 通信缓冲
- 数据缓存
-
DMA缓冲
-
动态内存:
- 临时数据结构
- 可变大小数据
- 运行时分配
第三部分:ROM与EEPROM¶
ROM(只读存储器)¶
ROM(Read-Only Memory)是一种只读存储器,数据在制造时写入,之后无法修改。
ROM的特点:
- 完全只读:
- 数据在芯片制造时固化
- 用户无法修改
-
成本低,适合大批量生产
-
非易失性:
- 断电后数据保持
- 可靠性极高
-
适合存储固定数据
-
现代应用:
- 在嵌入式系统中较少使用
- 被Flash取代
- 主要用于Bootloader
EEPROM(电可擦除可编程只读存储器)¶
EEPROM(Electrically Erasable Programmable Read-Only Memory)是一种可电擦除的非易失性存储器。
EEPROM的特点:
- 字节级擦写:
- 可以单独擦除和写入每个字节
- 不需要整块擦除
-
操作灵活
-
擦写次数:
- 典型值:100万-1000万次
- 远高于Flash
-
适合频繁更新的数据
-
容量较小:
- 典型容量:几KB
- 成本较高
-
访问速度较慢
-
独立操作:
- 读写时CPU可以继续运行
- 不影响程序执行
- 适合后台操作
Flash vs EEPROM对比¶
| 特性 | Flash | EEPROM |
|---|---|---|
| 擦除单位 | 页/扇区(KB级) | 字节 |
| 擦写次数 | 10万-100万次 | 100万-1000万次 |
| 容量 | 较大(MB级) | 较小(KB级) |
| 成本 | 较低 | 较高 |
| 速度 | 读取快,写入慢 | 读写都较慢 |
| 应用 | 程序代码、大量数据 | 配置参数、小量数据 |
EEPROM使用示例¶
#include "stm32f1xx_hal.h"
// EEPROM模拟(使用Flash实现)
#define EEPROM_START_ADDR 0x0800F000
#define EEPROM_SIZE 1024
/**
* @brief EEPROM写入字节
* @param address: 相对地址(0-1023)
* @param data: 要写入的字节
* @retval HAL状态
*/
HAL_StatusTypeDef EEPROM_WriteByte(uint16_t address, uint8_t data) {
uint32_t flash_addr = EEPROM_START_ADDR + address;
// 读取当前页的所有数据
uint8_t page_buffer[FLASH_PAGE_SIZE];
uint32_t page_start = flash_addr & ~(FLASH_PAGE_SIZE - 1);
for (int i = 0; i < FLASH_PAGE_SIZE; i++) {
page_buffer[i] = *(__IO uint8_t*)(page_start + i);
}
// 修改目标字节
page_buffer[flash_addr - page_start] = data;
// 擦除页
Flash_ErasePage(page_start);
// 写回整页数据
HAL_FLASH_Unlock();
for (int i = 0; i < FLASH_PAGE_SIZE; i += 4) {
uint32_t word = *(uint32_t*)&page_buffer[i];
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, page_start + i, word);
}
HAL_FLASH_Lock();
return HAL_OK;
}
/**
* @brief EEPROM读取字节
* @param address: 相对地址(0-1023)
* @retval 读取的字节
*/
uint8_t EEPROM_ReadByte(uint16_t address) {
uint32_t flash_addr = EEPROM_START_ADDR + address;
return *(__IO uint8_t*)flash_addr;
}
/**
* @brief EEPROM使用示例
*/
void EEPROM_Example(void) {
uint8_t config_value;
// 写入配置参数
EEPROM_WriteByte(0, 0x55);
EEPROM_WriteByte(1, 0xAA);
// 读取配置参数
config_value = EEPROM_ReadByte(0);
printf("Config value: 0x%02X\n", config_value);
}
注意:STM32F1系列没有独立的EEPROM,上述代码演示了如何使用Flash模拟EEPROM功能。
EEPROM的应用场景¶
- 配置参数:
- 系统设置
- 用户偏好
-
网络配置
-
校准数据:
- 传感器校准值
- ADC校准参数
-
温度补偿系数
-
运行统计:
- 运行时间
- 开机次数
-
错误计数
-
小量数据记录:
- 最后状态
- 历史记录
- 事件标记
第四部分:内存映射机制¶
什么是内存映射¶
内存映射(Memory Mapping)是将不同类型的存储器和外设寄存器映射到统一的地址空间,使CPU可以通过地址访问它们。
内存映射的优势:
- 统一访问:
- 使用相同的指令访问不同资源
- 简化编程模型
-
提高代码可移植性
-
地址空间管理:
- 合理分配地址空间
- 避免地址冲突
-
支持内存保护
-
外设访问:
- 外设寄存器映射到内存空间
- 通过指针直接访问
- 无需特殊指令
ARM Cortex-M内存映射¶
ARM Cortex-M系列使用32位地址空间(4GB),划分为多个区域:
地址范围 区域名称 用途
0xFFFFFFFF ┌──────────────────────────┐
│ 保留区 │
0xE0100000 ├──────────────────────────┤
│ 私有外设区 │ NVIC、SysTick等
0xE0000000 ├──────────────────────────┤
│ 外部设备区 │ 外部RAM、外设
0xA0000000 ├──────────────────────────┤
│ 外部RAM区 │ 外部SRAM
0x60000000 ├──────────────────────────┤
│ 外设区 │ APB、AHB外设
0x40000000 ├──────────────────────────┤
│ SRAM区 │ 片上SRAM
0x20000000 ├──────────────────────────┤
│ Flash区 │ 程序代码
0x08000000 ├──────────────────────────┤
│ 系统存储区 │ Bootloader
0x1FFFF000 ├──────────────────────────┤
│ 保留区 │
0x00000000 └──────────────────────────┘
各区域详细说明:
- Flash区(0x08000000 - 0x0801FFFF):
- 存储程序代码
- 存储常量数据
- 中断向量表
-
可执行(XIP)
-
SRAM区(0x20000000 - 0x20007FFF):
- 存储运行时数据
- 栈和堆
- 全局变量
-
可读写
-
外设区(0x40000000 - 0x5FFFFFFF):
- APB外设(0x40000000)
- AHB外设(0x40020000)
- GPIO、UART、SPI等
-
寄存器访问
-
私有外设区(0xE0000000 - 0xE00FFFFF):
- NVIC(中断控制器)
- SysTick(系统定时器)
- MPU(内存保护单元)
- 调试组件
内存映射示例¶
#include <stdint.h>
// Flash区域访问
#define FLASH_BASE 0x08000000
#define FLASH_SIZE (128 * 1024) // 128KB
// SRAM区域访问
#define SRAM_BASE 0x20000000
#define SRAM_SIZE (20 * 1024) // 20KB
// 外设基地址
#define PERIPH_BASE 0x40000000
#define APB1_BASE PERIPH_BASE
#define APB2_BASE (PERIPH_BASE + 0x10000)
#define AHB_BASE (PERIPH_BASE + 0x20000)
// GPIO外设地址
#define GPIOA_BASE (APB2_BASE + 0x0800)
#define GPIOB_BASE (APB2_BASE + 0x0C00)
#define GPIOC_BASE (APB2_BASE + 0x1000)
// GPIO寄存器结构体
typedef struct {
volatile uint32_t CRL; // 配置寄存器低
volatile uint32_t CRH; // 配置寄存器高
volatile uint32_t IDR; // 输入数据寄存器
volatile uint32_t ODR; // 输出数据寄存器
volatile uint32_t BSRR; // 位设置/复位寄存器
volatile uint32_t BRR; // 位复位寄存器
volatile uint32_t LCKR; // 锁定寄存器
} GPIO_TypeDef;
// GPIO外设指针
#define GPIOA ((GPIO_TypeDef*)GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef*)GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef*)GPIOC_BASE)
/**
* @brief 内存映射访问示例
*/
void Memory_Mapping_Example(void) {
// 1. 访问Flash(读取)
uint32_t *flash_ptr = (uint32_t*)FLASH_BASE;
uint32_t flash_data = *flash_ptr;
printf("Flash第一个字: 0x%08X\n", flash_data);
// 2. 访问SRAM(读写)
uint32_t *sram_ptr = (uint32_t*)SRAM_BASE;
*sram_ptr = 0x12345678;
uint32_t sram_data = *sram_ptr;
printf("SRAM数据: 0x%08X\n", sram_data);
// 3. 访问GPIO外设
// 设置PA5为输出
GPIOA->CRL &= ~(0xF << 20); // 清除配置位
GPIOA->CRL |= (0x3 << 20); // 设置为推挽输出,50MHz
// 控制PA5输出
GPIOA->BSRR = (1 << 5); // 设置PA5为高电平
GPIOA->BRR = (1 << 5); // 设置PA5为低电平
// 4. 读取GPIO输入
uint32_t input_value = GPIOA->IDR;
printf("GPIOA输入值: 0x%08X\n", input_value);
}
/**
* @brief 检查地址所属区域
* @param address: 要检查的地址
* @retval 区域名称字符串
*/
const char* Get_Memory_Region(uint32_t address) {
if (address >= 0x08000000 && address < 0x08000000 + FLASH_SIZE) {
return "Flash区";
} else if (address >= 0x20000000 && address < 0x20000000 + SRAM_SIZE) {
return "SRAM区";
} else if (address >= 0x40000000 && address < 0x60000000) {
return "外设区";
} else if (address >= 0xE0000000 && address < 0xE0100000) {
return "私有外设区";
} else {
return "未知区域";
}
}
/**
* @brief 内存区域信息打印
*/
void Print_Memory_Map(void) {
printf("=== 内存映射信息 ===\n");
printf("Flash起始地址: 0x%08X\n", FLASH_BASE);
printf("Flash大小: %d KB\n", FLASH_SIZE / 1024);
printf("SRAM起始地址: 0x%08X\n", SRAM_BASE);
printf("SRAM大小: %d KB\n", SRAM_SIZE / 1024);
printf("外设基地址: 0x%08X\n", PERIPH_BASE);
printf("GPIOA地址: 0x%08X\n", GPIOA_BASE);
}
代码说明:
- 地址定义:
- 使用宏定义各区域基地址
-
便于代码移植和维护
-
结构体映射:
- 定义外设寄存器结构体
- 通过指针访问寄存器
-
类型安全,易于理解
-
区域判断:
- 根据地址判断所属区域
- 用于调试和错误检查
位带操作¶
位带(Bit-banding)是ARM Cortex-M的特殊功能,允许对单个位进行原子操作。
位带区域:
- SRAM位带区:
- 位带区:0x20000000 - 0x200FFFFF(1MB)
-
位带别名区:0x22000000 - 0x23FFFFFF(32MB)
-
外设位带区:
- 位带区:0x40000000 - 0x400FFFFF(1MB)
- 位带别名区:0x42000000 - 0x43FFFFFF(32MB)
位带地址计算:
// 位带地址计算宏
#define BITBAND_SRAM(addr, bit) \
((0x22000000 + ((addr) - 0x20000000) * 32 + (bit) * 4))
#define BITBAND_PERI(addr, bit) \
((0x42000000 + ((addr) - 0x40000000) * 32 + (bit) * 4))
/**
* @brief 位带操作示例
*/
void Bitband_Example(void) {
// 定义一个变量
volatile uint32_t test_var = 0;
uint32_t var_addr = (uint32_t)&test_var;
// 计算位0的位带地址
uint32_t *bit0_addr = (uint32_t*)BITBAND_SRAM(var_addr, 0);
// 通过位带地址设置位0
*bit0_addr = 1; // test_var的bit0被设置为1
// 读取位0
uint32_t bit0_value = *bit0_addr;
printf("Bit 0 value: %d\n", bit0_value);
// GPIO位带操作
// 设置GPIOA ODR的bit5(PA5)
uint32_t *pa5_bit = (uint32_t*)BITBAND_PERI((uint32_t)&GPIOA->ODR, 5);
*pa5_bit = 1; // PA5输出高电平
*pa5_bit = 0; // PA5输出低电平
}
位带操作的优势:
- 原子操作,无需关中断
- 代码简洁,易于理解
- 提高执行效率
第五部分:地址空间布局¶
程序的内存布局¶
一个典型的嵌入式程序在内存中的布局如下:
高地址
┌─────────────────────┐
│ 栈区 (Stack) │ ← SP(栈指针)
│ ↓ 向下增长 │
├─────────────────────┤
│ 未使用空间 │
├─────────────────────┤
│ 堆区 (Heap) │
│ ↑ 向上增长 │
├─────────────────────┤
│ BSS段 │ 未初始化全局变量
├─────────────────────┤
│ Data段 │ 已初始化全局变量
├─────────────────────┤
│ 常量区 (Rodata) │ 只读数据
├─────────────────────┤
│ 代码区 (Text) │ 程序代码
├─────────────────────┤
│ 中断向量表 │ 异常和中断向量
└─────────────────────┘
低地址
链接脚本示例¶
链接脚本定义了程序各段在内存中的位置:
/* 链接脚本示例 (STM32F103) */
/* 内存定义 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
/* 段定义 */
SECTIONS
{
/* 中断向量表 */
.isr_vector : {
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
/* 代码段 */
.text : {
. = ALIGN(4);
*(.text)
*(.text*)
*(.rodata)
*(.rodata*)
. = ALIGN(4);
_etext = .;
} > FLASH
/* 已初始化数据段 */
.data : {
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .;
} > RAM AT > FLASH
/* 未初始化数据段 */
.bss : {
. = ALIGN(4);
_sbss = .;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
/* 堆栈定义 */
._user_heap_stack : {
. = ALIGN(8);
PROVIDE(end = .);
PROVIDE(_end = .);
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} > RAM
/* 栈顶 */
_estack = ORIGIN(RAM) + LENGTH(RAM);
}
启动代码中的内存初始化¶
/**
* @brief 系统启动时的内存初始化
* @note 在main函数之前执行
*/
void SystemInit_Memory(void) {
extern uint32_t _sdata, _edata, _sidata;
extern uint32_t _sbss, _ebss;
uint32_t *src, *dst;
// 1. 复制Data段从Flash到SRAM
src = &_sidata; // Flash中的数据源
dst = &_sdata; // SRAM中的目标地址
while (dst < &_edata) {
*dst++ = *src++;
}
// 2. 清零BSS段
dst = &_sbss;
while (dst < &_ebss) {
*dst++ = 0;
}
}
内存使用优化¶
-
减少全局变量:
-
使用const修饰常量:
-
合理使用栈和堆:
总结¶
本文详细介绍了嵌入式系统的内存架构,主要内容包括:
核心要点¶
- Flash存储器:
- 非易失性,存储程序代码
- 擦除以块为单位,写入以页为单位
-
擦写次数有限,需要注意磨损均衡
-
SRAM存储器:
- 易失性,存储运行时数据
- 读写快速,无擦除限制
-
分为栈、堆、BSS和Data段
-
ROM与EEPROM:
- ROM完全只读,现代系统较少使用
- EEPROM可字节级擦写,适合配置数据
-
Flash逐渐取代EEPROM
-
内存映射:
- 统一的地址空间访问不同资源
- ARM Cortex-M有明确的区域划分
-
支持位带操作
-
地址空间布局:
- 程序分为多个段存储
- 链接脚本定义内存布局
- 启动代码负责初始化
最佳实践¶
- Flash使用:
- 写入前必须擦除
- 避免频繁擦写同一区域
-
使用const存储常量数据
-
SRAM使用:
- 监控栈使用情况
- 避免栈溢出
-
及时释放动态内存
-
内存优化:
- 减少全局变量
- 合理使用栈和堆
- 使用const减少SRAM占用
延伸阅读¶
推荐进一步学习的内容:
- 内存管理:
- 动态内存管理与malloc实现
- 内存池技术详解
-
Flash编程:
- Flash磨损均衡算法
- 固件升级与Bootloader
-
高级主题:
- MPU内存保护单元
- Cache缓存机制
- DMA与内存访问
参考资料¶
- ARM Cortex-M3权威指南 - Joseph Yiu
- STM32参考手册 - ST Microelectronics
- 嵌入式系统设计与实践 - Elecia White
- ARM Architecture Reference Manual
练习题:
-
解释Flash和SRAM的主要区别,并说明各自的应用场景。
-
编写代码实现以下功能:
- 在Flash中存储一个配置结构体
- 读取并验证配置数据
-
修改配置并重新写入
-
分析以下代码的内存使用情况:
说明每个变量存储在哪个内存区域。 -
设计一个简单的EEPROM模拟方案,要求:
- 支持字节级读写
- 实现磨损均衡
- 提供掉电保护
下一步:建议学习 STM32启动过程深度分析,了解系统如何初始化内存并跳转到main函数。