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为低电平时外设才工作,高电平则三态输出(不占用总线)。

关键点:CPU通过地址译码产生CS信号,每个外设分配一个唯一的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发送未对齐地址,可能产生异常。
小技巧:做板级调试时,用示波器同时观察CS和读/写信号。如果CS正确但数据不对,大概率是数据总线方向或上拉配置问题。

下面这张图把CPU、总线、译码器和三个典型外设串了起来——你会在很多嵌入式系统的数据手册里看到类似的结构框图。

内存映射体系结构(CPU → 译码 → 外设) CPU 地址总线 / 数据总线 / 控制总线 地址译码器 3-8译码 / PLD / 简单门 NOR Flash 0x6000_0000 - 0x60FF_FFFF SRAM 0x7000_0000 - 0x70FF_FFFF 外设寄存器组 0x8000_0000 - 0x8000_FFFF 地址[31:0] CS#0 CS#1 CS#2 数据总线 D[15:0](共享) 图注 CPU通过地址线发送地址 → 译码器根据高位地址产生唯一的片选信号 → 对应的外设被选中并响应数据操作。 译码器上方的虚线表示数据总线是所有外设共享的,但同一时刻只允许一个设备驱动。
⚠️ 注意:不要以为只有复杂系统才需要认真做地址映射。哪怕只有一个外部设备,也要确认它的片选是否在CPU的地址空间内、是否与其他内部模块冲突。很多人直接从参考设计里抄地址,结果原板子是ARM7,你用的是Cortex-M4,地址映射完全不同。

说白了,内存映射就是给每个外设发一张“门牌号”。CPU发地址,译码器看门牌号,然后只让对应的那扇门打开。只要门牌不重号、门闩不出错,系统就能正常跑。嗯,这部分理解透了,后面讲XIP原理才轻松。