2.2 存储系统:程序与数据的住址安排


2.2 存储系统:程序与数据的住址安排

本节摘要:单片机的存储系统回答两个问题——程序放在哪、数据放在哪。本节讲清 Flash 与 RAM 的分工与特性差异,带你读懂数据手册里的存储映射表,解释栈与堆的生长机制,并用一个真实的"变量神秘被改"案例演示存储知识如何变成排错能力。这是从"会写代码"到"知道代码住在哪"的分水岭。

为什么变量会被"鬼改":一个经典案例

某项目的传感器读数每隔几小时出现一次乱值,重启后恢复。排查两周无果,最后发现是一个大局部数组压爆了任务栈,栈底越界改写了相邻的全局变量。这类故障在嵌入式圈有专门的外号叫"栈踩内存",它有几个识别特征:偶发、与特定执行路径相关、复位即消失。能快速定位它的人,脑中都装着一张存储地图——变量住在哪、栈往哪长、越界会踩到谁。本节就帮你把这张地图画进脑子。

Flash 与 RAM:性格迥异的两位室友

**Flash(闪存)**存程序与常量。它断电不丢、容量相对大(几十 KB 到几 MB),但有脾气:写入以"页"为单位,先擦除(整页清成全 1)再编程,擦写有寿命(一万到十万次量级),读取则又快又随意。所以程序烧进去一劳永逸,但若要在运行中往 Flash 里存数据(如保存用户设置),就得按页规划、磨损均衡,不能像写 RAM 那样随心所欲。

**RAM(随机存取存储器)**是运行时的数据现场。读写都极快、按字节随意访问,但断电即失、容量小得可怜(几 KB 到几百 KB,高端型号才上 MB)。MCU 的 RAM 多为 SRAM,不需要刷新,静态待命,这也让"低功耗模式下保持 RAM 内容"成为可能——2.4 节会用到这一点。

特性 Flash RAM
断电后 内容保留 内容丢失
读速度 快(配合预取)
写入方式 按页擦除再写,慢 按字节直接写,快
擦写寿命 有限(约万次级) 无限
典型容量 几十 KB 到几 MB 几 KB 到几百 KB
住客 程序代码、常量、查找表 全局变量、栈、堆

存储映射:一张地址地形图

MCU 上电后的一切访问都通过地址进行。芯片厂商在数据手册里给出的存储映射表,就是把 4GB 的 32 位地址空间切成若干区块:哪段是代码区、哪段是 RAM、哪段是外设寄存器、哪段留给 CPU 内部部件。以 Cortex-M 系列的通用约定为例:最低地址段放中断向量表与代码,接着是 SRAM 区,再往上是一大片外设寄存器区——你在 C 里写 GPIOA->ODR = 1;,编译后就是向某个外设区地址写一个字,寄存器操作的本质是"对地址赋值"

0xFFFFFFFF ┌─────────────────────┐ │ 系统级保留区 │ 0xE0100000 ├─────────────────────┤ │ Cortex 内核外设 │ SysTick、NVIC 等 0xE0000000 ├─────────────────────┤ │ 外设寄存器区 │ GPIO、定时器、串口…… 0x40000000 ├─────────────────────┤ │ (大段保留) │ ├─────────────────────┤ │ SRAM 区 │ 全局变量 / 栈 ↓ / 堆 ↑ 0x20000000 ├─────────────────────┤ │ 代码区 │ 向量表 + 程序 + 常量 0x08000000 ├─────────────────────┤ │ 别名区等 │ 0x00000000 └─────────────────────┘

读这张图要带走两个习惯。第一,链接脚本决定"住址分配":你的代码烧到 0x08000000 起、RAM 从 0x20000000 起,这些是链接脚本里写死的,改内存布局就是改它。第二,外设也是存储:统一编址是 Cortex-M 的设计哲学,读内存的指令同样能读外设寄存器,这让 C 语言操作硬件变得自然。

图 2-2:RAM 内部的空间分布示意

图 2-2:RAM 内部的空间分布示意

栈与堆:两种截然不同的内存生活方式

是编译器自动管理的区域。每次函数调用,返回地址、参数、局部变量依次压栈,函数返回时整体弹出——后进先出,秩序井然,用不着你操心释放。但栈的总量在链接时定死(几 KB 是常态),大局部数组、深递归、中断嵌套都会把栈推向深渊。

由程序员手动申请释放。理论上给了嵌入式系统灵活性,实际上 MCU 上要慎用:碎片化会让大块申请渐渐失败;释放遗漏直接内存泄漏,而 MCU 一跑就是数年,泄漏以"月"为单位积累成死机;分配耗时不确定,伤害实时性。不少嵌入式编码规范(如 MISRA 的衍生规范)干脆禁止动态分配,全部用静态分配——编译期就把内存账算清楚。

/* 静态分配:位置与大小在链接期确定,最安全 */ static uint8_t rx_buffer[256]; /* 全局缓冲区,落在静态区 */ /* 动态分配:嵌入式里的三宗罪(碎片、泄漏、耗时不确定) */ void bad_practice(void) { uint8_t *p = malloc(256); /* 从堆里挖一块 */ if (p == NULL) return; /* 挖失败可能发生在运行的第二年 */ process(p, 256); free(p); /* 忘了这行就是泄漏 */ }

Flash 里的特殊住户:向量表与常量

程序区的开头住着一位特殊住户——中断向量表:一串函数指针,登记着复位入口与每个中断服务程序的地址。芯片复位后做的第一件事就是从向量表取初始 PC 与 SP,2.3 节的复位流程与第 3.4 节(启动流程)都会回到这张表。另一个住户是常量数据:用 const 修饰的大查找表会被链接器安排进 Flash,为紧张的 RAM 省地方——"把表搬进 Flash"是嵌入式优化里性价比最高的一招。

存储外延:两类特殊的存储问题

补两个实战里会遇到的存储话题。Flash 的运行时写入:产品要在运行中保存用户设置或校准参数时,不得不写 Flash——按页擦除的特性意味着不能"哪里改写哪里",常见做法是划出一块参数页,采用"追加写加版本号"的结构(新参数写到页尾,读到最新版本号的那条),页写满才整体擦除一次。这样既减少擦写次数延长寿命,又保证断电瞬间参数不损坏。Bootloader 占位:需要远程升级的产品要预留一小段 Bootloader 固件常驻 Flash 开头,应用固件搬去后面的区域——这改变了整个存储映射的划分,必须在项目第一天就规划好,事后补 Bootloader 是嵌入式著名的返工重灾区。两件事的共同启示:存储布局是架构决策,不是实现细节,它在链接脚本与启动文件里落地(3.3 节详述),但决定它的时机在最开始。

排错演练:把地图用起来

回到开头的"鬼改"案例,现在你有了解剖工具。第一步,看链接生成的内存报告,确认栈顶地址与全局变量的相对位置;第二步,在调试器里观察可疑变量地址是否落在栈的生长路径上;第三步,临时把栈加大一倍观察故障是否消失(现象法),或用栈填充检测(把栈空间预先填满固定字节,运行一段时间后看填充被吃掉多深)定位真实用量。三步走完,"玄学"就变成了"第几个函数吃掉的栈"。

⚠️ 常见坑:在中断里定义大局部数组。中断栈与主栈共用或独立(看芯片设计),ISR 里的每个字节都会瞬间抬高栈用量,中断一嵌套就是栈雪崩的高发时刻。

本节要点回顾

  • 两位室友:Flash 存程序断电不丢但按页擦写;RAM 存现场读写随意但断电即失。
  • 存储映射是统一编址的地址地形图:外设寄存器也是地址,GPIOA->ODR 的本质是对地址赋值。
  • RAM 三分区:静态区编译期定死,堆向上长但 MCU 上慎用,栈向下长且每个函数调用都要占地。
  • "栈踩内存"识别三特征:偶发、路径相关、复位消失;排查靠内存报告加栈填充检测。
  • 体系位置:存储有了地址,访问有了次序,但一切运动都需要节拍——下一节看时钟与复位。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U