3、内存映射基础:CPU地址空间、总线、片选信号、地址译码
搞嵌入式的人,每天都在跟内存映射打交道。说白了,就是让CPU能找到每个外设、每块内存,并且知道什么时候该跟谁说话。我刚开始做单片机开发时,总觉得这东西很虚——不就是读个地址吗?直到有一次板子死活不干活,查了两天才发现是地址译码搞错了,片选信号拉错了地方……从那以后我踏踏实实把这块啃透了。今天咱们就把这些基础概念拆开了讲清楚。
3.1 CPU地址空间——你的系统能看多远
CPU能访问的所有地址范围,就是地址空间。它由地址线的根数决定。例如32位地址总线,理论寻址范围是4GB(2^32)。你用多少根地址线,系统就能“看”多远。
- 地址线数量:n根地址线 → 2^n个地址单元。
- 地址空间划分:通常把空间分成几段——程序内存(Flash)、数据内存(RAM)、外设寄存器、系统配置区等等。
- 片内还是片外?:有的SoC内部集成了RAM和Flash,它们占用地址空间的一部分;剩下的留给外部总线扩展。
我见过一个项目,用了一颗Cortex-M3芯片,地址空间只有512KB。工程师硬是塞了三个外部设备,结果地址重叠导致系统随机死机。嗯,这种坑踩一次就记住了。
3.2 总线——数据流动的高速公路
CPU跟外设之间的通信靠总线。总线不是一根线,是三组信号的合称:
| 总线类型 | 方向 | 典型宽度 | 作用 |
|---|---|---|---|
| 地址总线 | CPU→外设 | 16/32/64位 | 输出要访问的地址 |
| 数据总线 | 双向 | 8/16/32/64位 | 传输数据 |
| 控制总线 | CPU→外设 / 外设→CPU | 若干控制线 | 读写信号、中断请求、时钟等 |
这里有个细节:地址总线和数据总线可能复用(比如ARM的AMBA AHB),也可能独立(比如8051)。我个人习惯在设计电路时把三组信号分开画,调试时用逻辑分析仪同时抓这三组,一眼就能看出是哪一步卡住了。
3.3 片选信号——谁在说话
多个外设挂在同一组总线上,怎么保证只有一个设备响应?靠片选信号(Chip Select,简称CS或CS#)。一般情况下CS为低电平时外设才工作,高电平则三态输出(不占用总线)。
3.4 地址译码——把地址翻译成操作
地址译码器的作用是把CPU发出的地址转换成对应的片选信号。常见实现方式有三种:
| 译码方式 | 硬件复杂度 | 地址连续性 | 地址利用效率 | 典型应用 |
|---|---|---|---|---|
| 全译码 | 高(用所有地址线) | 连续 | 高,无地址空洞 | 大容量存储器 |
| 部分译码 | 中等(用部分高位地址线) | 可能不连续 | 较低,有地址空洞 | 小系统、低成本 |
| 线选 | 低(用一根地址线选一个设备) | 分块,不连续 | 低,地址空间碎片化 | 简单裸机、少量外设 |
举个例子:我们有一个NOR Flash(地址0x60000000~0x60FFFFFF),一个SRAM(0x70000000~0x70FFFFFF),用3-8译码器根据地址的高位(A[31:28])产生8个片选。只要保证每个片选覆盖的地址范围不重叠即可。
代码里怎么体现?下面是在C头文件里定义外设基地址的常用写法:
#define FLASH_BASE 0x60000000U
#define FLASH_SIZE 0x01000000U // 16MB
#define SRAM_BASE 0x70000000U
#define SRAM_SIZE 0x01000000U
// 访问FLASH的第0个字
*(volatile uint32_t *)(FLASH_BASE) = 0xA5;
你想想看,如果地址译码错了,上面这行代码可能写到了SRAM里,板子表现就会非常诡异。
3.5 地址映射实战中的注意事项
归纳几条我踩过的坑,大家引以为戒:
- 地址重叠:两个外设的地址区间有交集,CS同时有效。解决方法:用全译码或保证高位地址唯一。
- 总线竞争:当CS无效时,外设的数据脚必须为高阻态,否则会和另一个有效设备冲突。检查外设输出使能逻辑。
- 时序匹配:从CPU发地址到外设准备好数据,中间有延迟。片选信号必须满足外设的建立/保持时间。
- 地址对齐:多数外设要求按字(4字节)对齐访问,如果CPU发送未对齐地址,可能产生异常。
下面这张图把CPU、总线、译码器和三个典型外设串了起来——你会在很多嵌入式系统的数据手册里看到类似的结构框图。
说白了,内存映射就是给每个外设发一张“门牌号”。CPU发地址,译码器看门牌号,然后只让对应的那扇门打开。只要门牌不重号、门闩不出错,系统就能正常跑。嗯,这部分理解透了,后面讲XIP原理才轻松。