地址空间 - SoC 的"门牌号系统"
打开电脑玩游戏时, CPU 要调用显卡渲染画面, 要读取内存里的游戏数据, 还要接收键盘鼠标等外设的输入 —— 这些不同硬件之间的"数据串门", 靠什么才能精准找到对方? 就像你给同学寄快递需要准确的地址, SoC 里的所有硬件组件, 也需要一套统一的"门牌号"系统, 这就是地址空间.
什么是地址空间?
在开始学习复杂的 SoC 场景前, 先从熟悉的快递场景类比:你住的小区里, 每栋楼有楼号、每个单元有单元号、每户有门牌号, 如果把这些信息串联起来, 就形成了一串"地址". 通过这串"地址", 快递员便能精准找到收件人. SoC 里的地址空间, 本质就是给芯片内部所有"能存数据或处理数据的硬件"分配的唯一"门牌号"的集合, 也就是地址编码的集合.
计算机系统包含运算器、控制器、存储器、输入设备和输出设备五大部件. 由运算器和控制器构成的 CPU /处理器在内部通信中充当主设备, 存储器、输入设备和输出设备充当从设备. 因此需要对存储器和连接输入/输出设备的 IO 接口进行地址编码, 让主设备在发起通信时能够通过地址编码选择存储器或 IO 接口中的特定寄存器单元进行数据交换.

什么是统一编址?为什么要统一编址?
如果存储器和 IO 接口中的特定寄存器单元采用一套统一的地址编码, 那么我们将这一编址方式称为"统一编址"或"存储器映像"编址. 其本意在于将 IO 接口中的特定寄存器也视为一种存储器, 采用和存储器同样的访问方式. 采用统一编址的优点在于在指令集中不需要专门设计特定的 IO 指令, 对 IO 接口的读写可以使用较为丰富的寻址方式. 例如, 统一编址后, CPU 访问显卡的控制寄存器和读内存里的数据, 用的是完全相同的"读/写指令", 只需要改地址就能实现读写设备的选择. 而对我们做实验来说, 这意味着操作外设就像读写普通内存一样简单.
地址映射 - "门牌号"可灵活调整
SoC 的地址分配不是"固定死"的, 而是通过"地址映射"机制灵活配置. 比如有些 SoC 支持"Boot引导程序"在启动时, 把内存的地址范围从 0x00000000 映射到 0x80000000, 避免和启动代码的地址冲突; 还有些高端 SoC 支持"虚拟地址空间", CPU 看到的地址和硬件实际的物理地址不一样, 通过 MMU(内存管理单元)实时转换, 既能保护不同程序的地址安全, 又能让内存使用更灵活.
对于实验用到的 Cortex-M0 处理器, 其采用统一编址的编址方式. 所有的 Cortex-M 处理器都拥有 4GB 的存储器地址空间(即 32 位的地址线), 该空间在架构上被定义成多个区域, 为了提高不同设备间的软件可移植性, 每个空间区域都有推荐用途. 在 0x00000000-0x1FFFFFFF 范围内的地址空间用于存储程序代码, 其中一部分地址是用于存储异常向量表. 0x20000000-0x3FFFFFFF 范围内的地址空间用于数据存储器, 通常这部分的存储器会被集成到片上. 0x60000000-0x9FFFFFFF 范围内的地址空间用于外部 RAM, 这部分地址空间一般需要设计专门的外部存储器扩展接口来加以使用.

地址译码 - “门牌号”的精准分拣员
之前谈到 SoC 的地址空间是硬件的"门牌号"系统 —— 内存、显卡等都有了专属地址. 但问题来了:当 CPU 发出"读取 0x80000000 地址数据"的指令时, 总线上那么多硬件, 怎么确保只有显卡会"应答", 而内存不会"接错话"?这就需要一位精准的"分拣员" —— 地址译码. 它负责解析"门牌号", 找到唯一的目标硬件.
什么是地址译码?
为了确定当前的读/写对象, 需要根据总线上的相关信息产生地址选择信号, 这一过程称为地址译码. 除总线地址信号外, 译码电路也可能需要根据数据交换时的流向(读/写)、数据宽度(8 位、16 位和 32 位等)、中断传送方式或 DMA 传送方式等要求, 选择相关控制信号参与译码.
为减小电路规模, 常利用系统高位地址信号和相关控制信号产生片选信号, 以选中相关模块. 系统低位地址信号则作为字选信号, 在片选有效的模块(或芯片)上选中具体单元, 以进行数据存取. 通过两级译码以后, 既可以确定主设备所选定的具体读/写对象, 可以进一步在主设备和从设备之间进行数据交换.

在根据系统需求确定地址空间后, 片选信号可以采用全译码、部分译码或线译码三种方式(或三种方式的组合)来实现. 全译码指所有未参加字选的高位地址线全部参加译码, 以形成片选信号; 部分译码指只选用高位地址线中的一部分进行译码, 以产生片选信号, 未参加译码的高位地址线不做处理; 线译码则指使用单独的地址信号线作为片选信号.
假设某系统地址总线宽度为 32 位, 现需要将 000C0000H~000CFFFFH 地址范围划分为 8 个同样大小的地址空间, 提供给总线上的 8 个模块.
做 SoC 设计, 优先选哪种译码方式?
优先选全译码! 在 SoC 中一般会有多个模块和外设, 地址空间紧张且容易冲突. 全译码虽然电路稍复杂, 但能保证每个模块的地址唯一, 不用考虑软件避坑, 调试时更省心. 比如实验中如果用部分译码, 不小心写了重叠地址, 可能会同时触发两个不同的外设, 导致实验失败, 排查起来很麻烦.
Cortex M0 开源处理器采用统一编址的编址方式, 处理器核通过地址编码访问外设, 所有的外设在处理器核看来都是 memory map 上的一块连续区域, 访问这块区域就是访问对应的外设. 例如书中的 RAMCODE , 其地址编码 0x00000000-0x0000ffff, 那么处理器核通过 AHB 总线发出的任何一次总线操作, 只要地址总线上的值在 0x00000000-0x0000ffff 之间, 都认为是处理器核在向 RAMCODE 进行总线操作.

在地址空间划定后需要通过总线扩展来集成各个外围模块/IP. Decoder 的作用是对地址总线进行译码,生成对应的外设的选择信号, 同样以 RAMCODE 外设为例, 由于其地址编码为 0x00000000-0x0000ffff, 那么只要地址总线 HADDR 的高 16bit 为 0x0000 时, 地址总线的值必定处于 RAMCODE 的地址编码区域中, 则判定为处理器核对 RAMCODE 提起的一次总线操作, 因此 RAMCODE 对应的选择信号 HSEL 将会被置位有效;若 HADDR 的高 16bit 不为 0x0000 时(例如 0x4000), 地址总线的值不处于 RAMCODE 的地址编码区域, 则判定为不是对 RAMCODE 的总线操作, 因此对应的选择信号被置位无效.
Slave MUX 的作用则是通过每个外设的选择信号, 对所有外设返回的读取数据(HRDATA)、响应信号(HRESP)以及反馈信号(HREADYOUT)进行选择, 保证返回给Master 端口的数据来自于当前总线操作的目标外设.
可以采用如下图所示的总线扩展模块来构造 Cortex M0 与各个外围的模块/IP 的互联结构.

自己动手实践吧!
纸上得来终觉浅, 想要深入理解总线和地址空间的奥秘, 加深大家者对基于 CortexM0 的 SoC 的理解, 下一小节我们将动手修改代码, 利用 AHB-lite 总线将 Block RAM 与 Cortex-M0 裸核相连接, 搭建一个最小的 SoC 系统, 并通过 Modelsim 对这个系统进行仿真验证.