跳转至

嵌入式系统内存架构详解

学习目标

完成本文学习后,你将能够:

  • 理解嵌入式系统中各种存储器的特性和区别
  • 掌握Flash存储器的工作原理和使用方法
  • 了解SRAM的特点和配置方式
  • 区分ROM、EEPROM和Flash的应用场景
  • 理解内存映射机制和地址空间布局
  • 能够编写访问不同内存区域的代码

前置要求

在开始本文学习之前,你需要:

知识要求: - 了解二进制和十六进制数制 - 熟悉C语言基础(指针、数组) - 了解微控制器的基本概念 - 有一定的数字电路基础

技能要求: - 能够阅读简单的C代码 - 了解基本的计算机组成原理 - 熟悉常用的数据类型和大小

推荐但非必需: - 有ARM Cortex-M或其他MCU的使用经验 - 了解汇编语言基础 - 熟悉嵌入式开发环境

概述

内存是嵌入式系统的核心组成部分,就像人的大脑需要记忆一样,微控制器也需要各种类型的存储器来保存程序代码、运行数据和配置信息。理解不同类型存储器的特性和使用方法,是掌握嵌入式开发的基础。

为什么要学习内存架构

  1. 程序存储:代码需要存储在非易失性存储器中
  2. 数据管理:运行时数据需要快速的RAM支持
  3. 性能优化:合理使用内存可以提升系统性能
  4. 资源规划:有限的内存需要精心规划和管理
  5. 故障排查:很多问题都与内存使用有关

嵌入式系统内存层次

┌─────────────────────────────────────┐
│         CPU寄存器                    │  最快,容量最小
│         (32个 × 32位)                │
├─────────────────────────────────────┤
│         Cache缓存                    │  快速,容量小
│         (可选,高端MCU)              │
├─────────────────────────────────────┤
│         SRAM (静态RAM)               │  快速,易失性
│         (8KB - 256KB)                │
├─────────────────────────────────────┤
│         Flash (闪存)                 │  较慢,非易失性
│         (32KB - 2MB)                 │
├─────────────────────────────────────┤
│         外部存储                      │  最慢,容量最大
│         (SD卡、SPI Flash等)          │
└─────────────────────────────────────┘

第一部分:Flash存储器

什么是Flash存储器

Flash(闪存)是一种非易失性存储器,即使断电后数据也不会丢失。在嵌入式系统中,Flash主要用于存储程序代码和常量数据。

Flash的核心特点

  1. 非易失性
  2. 断电后数据保持
  3. 适合存储程序代码
  4. 可靠性高

  5. 读取快速

  6. 读取速度接近SRAM
  7. 支持随机访问
  8. 可以直接执行代码(XIP)

  9. 写入较慢

  10. 需要先擦除后写入
  11. 擦除以块为单位
  12. 写入次数有限(10万-100万次)

  13. 容量较大

  14. 典型容量:32KB - 2MB
  15. 成本相对较低
  16. 集成在芯片内部

Flash的工作原理

Flash存储器基于浮栅晶体管技术,通过电荷的存储和释放来表示0和1。

存储单元结构

        控制栅极
    ┌──────┴──────┐
    │             │
    │  浮栅极     │  ← 存储电荷
    │             │
    ├─────────────┤
    │   氧化层    │
    ├─────────────┤
    │   硅基底    │
    └─────────────┘
     源极      漏极

操作过程

  1. 编程(写入)
  2. 施加高电压
  3. 电子注入浮栅极
  4. 存储单元变为"0"状态

  5. 擦除

  6. 施加反向高电压
  7. 电子从浮栅极移除
  8. 存储单元变为"1"状态

  9. 读取

  10. 施加正常电压
  11. 检测电流大小
  12. 判断存储状态

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. 擦除限制
  2. 必须先擦除后写入
  3. 擦除以扇区为单位
  4. 不能单独擦除某个字节

  5. 写入限制

  6. 只能将1改为0
  7. 不能将0改为1(需要先擦除)
  8. 写入以页为单位

  9. 寿命限制

  10. 擦写次数有限
  11. 典型值:10万-100万次
  12. 需要磨损均衡算法

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");
    }
}

代码说明

  1. Flash_Read:直接通过指针读取Flash内容
  2. Flash_ErasePage:擦除指定页,必须在写入前执行
  3. Flash_Write:写入32位数据到指定地址
  4. Flash_Example:完整的擦除-写入-读取-验证流程

注意事项

  • 写入前必须先擦除
  • 地址必须对齐(通常4字节对齐)
  • 操作Flash时需要解锁和锁定
  • 避免频繁擦写同一区域

Flash的应用场景

  1. 程序代码存储
  2. 主程序代码
  3. 中断向量表
  4. 常量数据

  5. 配置参数存储

  6. 系统配置
  7. 校准数据
  8. 用户设置

  9. 固件升级

  10. Bootloader
  11. 应用程序更新
  12. 双区备份

  13. 数据记录

  14. 日志信息
  15. 历史数据
  16. 事件记录

第二部分:SRAM存储器

什么是SRAM

SRAM(Static Random Access Memory,静态随机存取存储器)是一种易失性存储器,用于存储程序运行时的变量、堆栈和临时数据。

SRAM的核心特点

  1. 易失性
  2. 断电后数据丢失
  3. 需要持续供电
  4. 适合临时数据

  5. 读写快速

  6. 访问速度快(几纳秒)
  7. 无需刷新
  8. 支持随机访问

  9. 容量有限

  10. 典型容量:8KB - 256KB
  11. 成本较高
  12. 功耗相对较大

  13. 使用简单

  14. 读写操作简单
  15. 无擦除限制
  16. 无寿命限制

SRAM的工作原理

SRAM使用双稳态触发器存储数据,每个存储单元由6个晶体管组成。

存储单元结构

        VDD
    ┌────┴────┐
    │         │
   T1        T2
    │         │
    ├────┬────┤
    │    │    │
   T3   T5   T4
    │    │    │
   BL   WL   BL'
    │         │
   T6        T6
    │         │
   GND       GND

工作特点

  • 使用锁存器结构
  • 数据稳定存储
  • 无需刷新电路
  • 功耗相对较高

SRAM的组织结构

在嵌入式系统中,SRAM通常分为几个区域:

SRAM (32KB)
├── 栈区 (Stack)
│   ├── 局部变量
│   ├── 函数参数
│   └── 返回地址
├── 堆区 (Heap)
│   ├── 动态分配内存
│   └── malloc/free管理
├── BSS段
│   └── 未初始化全局变量
├── Data段
│   └── 已初始化全局变量
└── 保留区
    └── 系统使用

各区域说明

  1. 栈区(Stack)
  2. 从高地址向低地址增长
  3. 存储局部变量和函数调用信息
  4. 自动管理,LIFO结构
  5. 大小固定,溢出会导致系统崩溃

  6. 堆区(Heap)

  7. 从低地址向高地址增长
  8. 用于动态内存分配
  9. 需要手动管理(malloc/free)
  10. 大小可变,但总量有限

  11. BSS段

  12. 存储未初始化的全局变量
  13. 启动时自动清零
  14. 不占用Flash空间

  15. Data段

  16. 存储已初始化的全局变量
  17. 启动时从Flash复制到SRAM
  18. 占用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;
}

代码说明

  1. 变量存储位置
  2. 全局变量存储在Data段或BSS段
  3. 局部变量存储在栈区
  4. 动态分配的内存在堆区

  5. 栈使用监控

  6. 通过SP寄存器获取当前栈位置
  7. 计算已使用的栈空间
  8. 检测栈溢出风险

  9. 内存管理

  10. 使用malloc动态分配
  11. 使用free释放内存
  12. 避免内存泄漏

SRAM使用注意事项

  1. 栈溢出预防

    // 避免在栈上分配大数组
    void bad_example(void) {
        uint8_t large_array[10000];  // ❌ 可能导致栈溢出
        // ...
    }
    
    // 使用静态或动态分配
    void good_example(void) {
        static uint8_t large_array[10000];  // ✅ 使用静态存储
        // 或
        uint8_t *array = malloc(10000);     // ✅ 使用堆分配
        // ...
        free(array);
    }
    

  2. 内存泄漏避免

    void memory_leak_example(void) {
        uint8_t *ptr = malloc(100);
        // ... 使用ptr
        // ❌ 忘记free(ptr),导致内存泄漏
    }
    
    void correct_example(void) {
        uint8_t *ptr = malloc(100);
        if (ptr != NULL) {
            // ... 使用ptr
            free(ptr);  // ✅ 及时释放
        }
    }
    

  3. 全局变量使用

    // 尽量减少全局变量
    uint32_t g_counter = 0;  // 占用Data段和Flash
    
    // 使用const减少SRAM占用
    const uint32_t g_config = 0x1234;  // 只占用Flash
    

SRAM的应用场景

  1. 程序运行数据
  2. 局部变量
  3. 函数参数
  4. 返回地址

  5. 全局数据存储

  6. 全局变量
  7. 静态变量
  8. 共享数据

  9. 缓冲区

  10. 通信缓冲
  11. 数据缓存
  12. DMA缓冲

  13. 动态内存

  14. 临时数据结构
  15. 可变大小数据
  16. 运行时分配

第三部分:ROM与EEPROM

ROM(只读存储器)

ROM(Read-Only Memory)是一种只读存储器,数据在制造时写入,之后无法修改。

ROM的特点

  1. 完全只读
  2. 数据在芯片制造时固化
  3. 用户无法修改
  4. 成本低,适合大批量生产

  5. 非易失性

  6. 断电后数据保持
  7. 可靠性极高
  8. 适合存储固定数据

  9. 现代应用

  10. 在嵌入式系统中较少使用
  11. 被Flash取代
  12. 主要用于Bootloader

EEPROM(电可擦除可编程只读存储器)

EEPROM(Electrically Erasable Programmable Read-Only Memory)是一种可电擦除的非易失性存储器。

EEPROM的特点

  1. 字节级擦写
  2. 可以单独擦除和写入每个字节
  3. 不需要整块擦除
  4. 操作灵活

  5. 擦写次数

  6. 典型值:100万-1000万次
  7. 远高于Flash
  8. 适合频繁更新的数据

  9. 容量较小

  10. 典型容量:几KB
  11. 成本较高
  12. 访问速度较慢

  13. 独立操作

  14. 读写时CPU可以继续运行
  15. 不影响程序执行
  16. 适合后台操作

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的应用场景

  1. 配置参数
  2. 系统设置
  3. 用户偏好
  4. 网络配置

  5. 校准数据

  6. 传感器校准值
  7. ADC校准参数
  8. 温度补偿系数

  9. 运行统计

  10. 运行时间
  11. 开机次数
  12. 错误计数

  13. 小量数据记录

  14. 最后状态
  15. 历史记录
  16. 事件标记

第四部分:内存映射机制

什么是内存映射

内存映射(Memory Mapping)是将不同类型的存储器和外设寄存器映射到统一的地址空间,使CPU可以通过地址访问它们。

内存映射的优势

  1. 统一访问
  2. 使用相同的指令访问不同资源
  3. 简化编程模型
  4. 提高代码可移植性

  5. 地址空间管理

  6. 合理分配地址空间
  7. 避免地址冲突
  8. 支持内存保护

  9. 外设访问

  10. 外设寄存器映射到内存空间
  11. 通过指针直接访问
  12. 无需特殊指令

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  └──────────────────────────┘

各区域详细说明

  1. Flash区(0x08000000 - 0x0801FFFF)
  2. 存储程序代码
  3. 存储常量数据
  4. 中断向量表
  5. 可执行(XIP)

  6. SRAM区(0x20000000 - 0x20007FFF)

  7. 存储运行时数据
  8. 栈和堆
  9. 全局变量
  10. 可读写

  11. 外设区(0x40000000 - 0x5FFFFFFF)

  12. APB外设(0x40000000)
  13. AHB外设(0x40020000)
  14. GPIO、UART、SPI等
  15. 寄存器访问

  16. 私有外设区(0xE0000000 - 0xE00FFFFF)

  17. NVIC(中断控制器)
  18. SysTick(系统定时器)
  19. MPU(内存保护单元)
  20. 调试组件

内存映射示例

#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);
}

代码说明

  1. 地址定义
  2. 使用宏定义各区域基地址
  3. 便于代码移植和维护

  4. 结构体映射

  5. 定义外设寄存器结构体
  6. 通过指针访问寄存器
  7. 类型安全,易于理解

  8. 区域判断

  9. 根据地址判断所属区域
  10. 用于调试和错误检查

位带操作

位带(Bit-banding)是ARM Cortex-M的特殊功能,允许对单个位进行原子操作。

位带区域

  1. SRAM位带区
  2. 位带区:0x20000000 - 0x200FFFFF(1MB)
  3. 位带别名区:0x22000000 - 0x23FFFFFF(32MB)

  4. 外设位带区

  5. 位带区:0x40000000 - 0x400FFFFF(1MB)
  6. 位带别名区: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;
    }
}

内存使用优化

  1. 减少全局变量

    // 不推荐:大量全局变量
    uint8_t buffer1[1024];
    uint8_t buffer2[1024];
    uint8_t buffer3[1024];
    
    // 推荐:使用局部变量或动态分配
    void function(void) {
        uint8_t buffer[1024];  // 栈上分配
        // 或
        uint8_t *buffer = malloc(1024);  // 堆上分配
        // ...
        free(buffer);
    }
    

  2. 使用const修饰常量

    // 不推荐:占用SRAM
    uint8_t lookup_table[256] = {0, 1, 2, ...};
    
    // 推荐:存储在Flash
    const uint8_t lookup_table[256] = {0, 1, 2, ...};
    

  3. 合理使用栈和堆

    // 小数据使用栈
    void small_data(void) {
        uint8_t buffer[64];  // 栈上分配,快速
    }
    
    // 大数据使用堆或静态
    void large_data(void) {
        static uint8_t buffer[4096];  // 静态分配
        // 或
        uint8_t *buffer = malloc(4096);  // 堆分配
    }
    

总结

本文详细介绍了嵌入式系统的内存架构,主要内容包括:

核心要点

  1. Flash存储器
  2. 非易失性,存储程序代码
  3. 擦除以块为单位,写入以页为单位
  4. 擦写次数有限,需要注意磨损均衡

  5. SRAM存储器

  6. 易失性,存储运行时数据
  7. 读写快速,无擦除限制
  8. 分为栈、堆、BSS和Data段

  9. ROM与EEPROM

  10. ROM完全只读,现代系统较少使用
  11. EEPROM可字节级擦写,适合配置数据
  12. Flash逐渐取代EEPROM

  13. 内存映射

  14. 统一的地址空间访问不同资源
  15. ARM Cortex-M有明确的区域划分
  16. 支持位带操作

  17. 地址空间布局

  18. 程序分为多个段存储
  19. 链接脚本定义内存布局
  20. 启动代码负责初始化

最佳实践

  1. Flash使用
  2. 写入前必须擦除
  3. 避免频繁擦写同一区域
  4. 使用const存储常量数据

  5. SRAM使用

  6. 监控栈使用情况
  7. 避免栈溢出
  8. 及时释放动态内存

  9. 内存优化

  10. 减少全局变量
  11. 合理使用栈和堆
  12. 使用const减少SRAM占用

延伸阅读

推荐进一步学习的内容:

  1. 内存管理
  2. 动态内存管理与malloc实现
  3. 内存池技术详解
  4. 内存碎片问题与解决

  5. Flash编程

  6. Flash磨损均衡算法
  7. 固件升级与Bootloader
  8. Flash文件系统

  9. 高级主题

  10. MPU内存保护单元
  11. Cache缓存机制
  12. DMA与内存访问

参考资料

  1. ARM Cortex-M3权威指南 - Joseph Yiu
  2. STM32参考手册 - ST Microelectronics
  3. 嵌入式系统设计与实践 - Elecia White
  4. ARM Architecture Reference Manual

练习题

  1. 解释Flash和SRAM的主要区别,并说明各自的应用场景。

  2. 编写代码实现以下功能:

  3. 在Flash中存储一个配置结构体
  4. 读取并验证配置数据
  5. 修改配置并重新写入

  6. 分析以下代码的内存使用情况:

    uint32_t g_var1 = 100;
    uint32_t g_var2;
    const uint32_t g_const = 200;
    
    void function(void) {
        uint32_t local_var = 300;
        static uint32_t static_var = 400;
        uint32_t *heap_var = malloc(sizeof(uint32_t));
        *heap_var = 500;
        free(heap_var);
    }
    
    说明每个变量存储在哪个内存区域。

  7. 设计一个简单的EEPROM模拟方案,要求:

  8. 支持字节级读写
  9. 实现磨损均衡
  10. 提供掉电保护

下一步:建议学习 STM32启动过程深度分析,了解系统如何初始化内存并跳转到main函数。