BLOG8
跳转至

嵌入式系统内存架构详解

学习目标

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

  • 理解嵌入式系统中各种存储器的特性和区别
  • 掌握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函数。