15、ROM与RAM的地址映射:不同存储介质的初始化差异

说到嵌入式系统的启动,有个绕不开的话题——ROM和RAM的地址映射。说白了,就是CPU怎么找到代码,怎么跑起来。

我刚开始做嵌入式那会儿,总觉得这玩意儿是硬件的事,软件工程师不用管。直到有一次,我写的程序死活跑不起来,查了三天才发现是链接脚本里地址配错了。嗯,从那以后,我再也不敢轻视地址映射了。

15.1 为什么地址映射这么重要?

你想想看,CPU上电后,它第一件事是干什么?去一个固定的地址取指令。这个地址,通常指向ROM(比如Flash)。但问题是,代码执行过程中,变量、堆栈这些需要读写的东西,必须放在RAM里。

所以,地址映射要解决的核心问题就两个:

  • 代码在哪里?——通常放在ROM里,掉电不丢失
  • 数据在哪里?——必须放在RAM里,能读能写

但事情没这么简单。不同芯片、不同架构,ROM和RAM的地址空间怎么分配,差别很大。我见过不少新手,把STM32的Flash地址和SRAM地址搞混,结果程序跑飞了还不知道为什么。

15.2 典型存储介质的地址特性

先看看我们常用的几种存储介质,它们的地址特性完全不同。

存储介质 地址空间 读写特性 掉电保持 初始化需求
Flash (ROM) 通常从0x08000000开始 可读,写需要擦除 无需初始化,上电即可读
SRAM 通常从0x20000000开始 可读可写,速度快 上电内容随机,需要初始化
SDRAM/DDR 外部扩展地址 可读可写,速度较快 需要初始化控制器和时序
EEPROM I2C/SPI总线地址 可读可写,速度慢 需要配置总线接口

这里有个关键点:Flash上电就能读,但SRAM上电后内容是随机的。这就是为什么启动代码必须放在Flash里,而变量必须由启动代码来初始化。

核心概念:ROM(只读存储器)和RAM(随机存取存储器)的地址映射,决定了CPU如何访问代码和数据。启动代码的本质,就是把代码从ROM搬到RAM(如果需要),并初始化RAM中的数据区域。

15.3 不同架构的地址映射差异

我做过ARM Cortex-M系列,也玩过RISC-V,还碰过一些老式的51单片机。它们的地址映射方式,各有各的脾气。

15.3.1 ARM Cortex-M 的地址映射

ARM Cortex-M系列有个好处——地址映射是固定的。比如STM32,Flash从0x08000000开始,SRAM从0x20000000开始。上电后,CPU直接从0x08000000读取向量表,找到复位向量,然后跳过去执行。

我个人习惯,在链接脚本里这样定义:

/* STM32F103 的链接脚本片段 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    SRAM  (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}

这样写的好处是,编译器知道代码该放哪,数据该放哪。但有个坑——如果你把变量定义到了Flash地址段,写操作就会触发硬件错误。我曾经有个同事,把一个大数组定义到了Flash区,结果程序一跑就进HardFault,查了半天才发现。

15.3.2 RISC-V 的地址映射

RISC-V就灵活多了。它的地址映射完全由芯片厂商自己定。有的芯片把Flash放在0x20000000,有的放在0x80000000。我第一次接触RISC-V时,拿到芯片手册第一件事就是找地址映射表。

我记得有个项目,用的是国产的RISC-V芯片。它的Flash地址是0x20000000,但SRAM地址是0x10000000。这个顺序跟ARM完全反过来了。如果不小心把ARM的习惯带过来,程序肯定跑不起来。

/* RISC-V 芯片的链接脚本片段 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x20000000, LENGTH = 1M
    SRAM  (rwx) : ORIGIN = 0x10000000, LENGTH = 128K
}

15.3.3 老式51单片机的地址映射

51单片机就更特殊了。它的程序存储器和数据存储器是分开编址的——哈佛架构。程序用ROM,数据用RAM,地址可以重叠。比如程序地址0x0000和数据地址0x0000,是两个完全不同的地方。

这种架构下,启动代码不需要搬移数据,因为变量直接就在RAM里。但问题是,RAM很小,通常只有128字节或256字节。我刚开始学单片机时,经常为了省几个字节的RAM,把变量定义成bit类型。

经验之谈:做地址映射时,一定要先看芯片手册的「Memory Map」章节。不同厂商、不同系列,地址分配可能完全不同。别指望靠经验猜,手册才是王道。

15.4 初始化差异:ROM vs RAM

ROM和RAM的初始化方式,差别很大。我画了一张图,帮你理清这个流程。

ROM与RAM初始化流程对比 ROM(Flash)初始化 上电后直接可读 无需初始化,CPU直接取指令 读取向量表 找到复位向量地址 跳转到复位向量 开始执行启动代码 ROM初始化完成 RAM(SRAM)初始化 上电内容随机 必须由软件初始化 清零BSS段 未初始化的全局变量 复制数据段 从ROM复制到RAM RAM初始化完成 启动代码执行

从这张图可以看得很清楚:ROM是「拿来就用」,RAM是「先初始化再用」。这个差异,直接决定了启动代码的结构。

15.5 启动代码中的地址映射处理

启动代码里,地址映射的处理主要集中在两个地方:

  • BSS段清零:把未初始化的全局变量所在的RAM区域全部写0
  • 数据段复制:把已初始化的全局变量从ROM复制到RAM

我见过一个典型的启动代码片段,是这样的:

/* 启动代码中的BSS清零 */
void __attribute__((naked)) Reset_Handler(void)
{
    extern uint32_t _sbss;   /* BSS段起始地址 */
    extern uint32_t _ebss;   /* BSS段结束地址 */
    extern uint32_t _sdata;  /* 数据段在RAM中的起始地址 */
    extern uint32_t _edata;  /* 数据段在RAM中的结束地址 */
    extern uint32_t _sidata; /* 数据段在ROM中的起始地址 */

    uint32_t *pSrc, *pDest;

    /* 1. 清零BSS段 */
    for (pDest = &_sbss; pDest < &_ebss; )
    {
        *pDest++ = 0;
    }

    /* 2. 复制数据段 */
    pSrc = &_sidata;
    for (pDest = &_sdata; pDest < &_edata; )
    {
        *pDest++ = *pSrc++;
    }

    /* 3. 跳转到main */
    main();
}

这里有个细节:_sidata指向的是ROM地址,_sdata指向的是RAM地址。这两个地址不同,但内容相同。说白了,就是ROM里存了一份初始值,启动时把它搬到RAM里。

注意:如果芯片支持从RAM启动(比如某些调试模式),那地址映射就完全不一样了。这时候,ROM和RAM的角色互换,启动代码也要相应调整。我曾经在调试一个bootloader时,不小心把启动地址设成了RAM,结果每次复位都跑飞,后来才发现是地址映射没改过来。

15.6 实际项目中的避坑指南

做了这么多年嵌入式,我总结了几条关于地址映射的经验,分享给你:

  1. 一定要确认芯片的地址映射表——不同型号、不同批次,地址可能不一样。别信网上的例程,信手册。
  2. 链接脚本里的地址要和硬件一致——我见过有人把Flash地址写错了一位,结果程序下载进去了,但跑不起来。查了三天,发现是0x08000000写成了0x08000001。
  3. 注意对齐——有些芯片要求代码或数据按4字节对齐,否则会触发异常。尤其是从ROM复制数据到RAM时,如果源地址或目标地址没对齐,复制就会出错。
  4. 小心SDRAM的初始化时序——如果用了外部SDRAM,它的初始化需要配置控制器、设置时序参数。这个初始化必须在访问SDRAM之前完成,否则一访问就死机。

避坑指南:我曾经在一个项目中,用了外部SDRAM来存放大的数据缓冲区。结果每次上电,程序都在访问缓冲区时卡死。查到最后,发现是SDRAM初始化代码放在了main函数里,而main函数之前就有代码访问了那个缓冲区。解决方案很简单——把SDRAM初始化放到启动代码里,在main之前完成。

15.7 不同存储介质的初始化差异总结

最后,我用一个表格来总结不同存储介质的初始化差异:

存储介质 初始化时机 初始化内容 注意事项
Flash (ROM) 无需初始化 写操作需要擦除,速度慢
SRAM 启动代码中 清零BSS,复制数据段 注意对齐,避免越界
SDRAM/DDR 启动代码中(main之前) 配置控制器,设置时序 时序参数必须准确,否则不稳定
EEPROM 使用时初始化 配置I2C/SPI接口 注意总线速率和地址

嗯,地址映射这个话题,说深了可以写一本书。但核心就一句话:ROM只管存,RAM只管用,启动代码就是那个「搬运工」。搞清楚了这一点,地址映射的问题就解决了一大半。

下次你写启动代码时,不妨多想想:我的代码在哪?我的数据在哪?它们之间的地址映射对不对?想清楚了,再动手写代码。


公众号:蓝海资料掘金营,微信deep3321