外设

什么是外设?

大多数微控制器不仅仅有 CPU、RAM 或闪存——它们还包含硅片区域,用于与微控制器外部的系统交互,以及通过传感器、电机控制器或人机界面(如显示器或键盘)直接和间接地与周围环境互动。这些组件统称为外设。

这些外设很有用,因为它们允许开发人员将处理任务卸载到它们,从而避免必须在软件中处理所有事情。类似于桌面开发人员将图形处理卸载到显卡的方式,嵌入式开发人员可以将一些任务卸载到外设,从而使 CPU 可以将时间花在其他重要的事情上,或者为了节省功耗而什么都不做。

如果您查看 20 世纪 70 年代或 80 年代的老式家用计算机的主电路板(实际上,昨天的台式电脑与今天的嵌入式系统并没有太大区别),您会看到:

  • 一个处理器
  • 一个 RAM 芯片
  • 一个 ROM 芯片
  • 一个 I/O 控制器

RAM 芯片、ROM 芯片和 I/O 控制器(本系统中的外设)通过一系列称为“总线”的并行走线连接到处理器。该总线携带地址信息,该信息选择处理器希望与之通信的总线上的哪个设备,以及携带实际数据的总线。在我们的嵌入式微控制器中,相同的原理适用——只是所有东西都被打包到了一块硅片上。

然而,与通常具有 Vulkan、Metal 或 OpenGL 等软件 API 的显卡不同,外设通过硬件接口暴露给我们的微控制器,该接口映射到内存块。

线性内存空间和真实内存空间

在微控制器上,将一些数据写入其他任意地址,例如 0x4000_00000x0000_0000,也可能完全是有效的操作。

在桌面系统中,对内存的访问受到 MMU(内存管理单元)的严格控制。该组件有两个主要职责:强制执行对内存段的访问权限(防止一个进程读取或修改另一个进程的内存);以及将物理内存段重新映射到软件中使用的虚拟内存范围。微控制器通常没有 MMU,而是在软件中仅使用真实的物理地址。

虽然 32 位微控制器具有从 0x0000_00000xFFFF_FFFF 的真实线性地址空间,但它们通常只使用该范围中的几百 KB 来存储实际内存。这留下了大量的剩余地址空间。在前面的章节中,我们讨论过 RAM 位于地址 0x2000_0000。如果我们的 RAM 长 64 KiB(即最大地址为 0xFFFF),那么地址 0x2000_00000x2000_FFFF 将对应于我们的 RAM。当我们写入一个位于地址 0x2000_1234 的变量时,内部发生的事情是一些逻辑检测到地址的上半部分(在本例中为 0x2000),然后激活 RAM,以便它可以对地址的下半部分(在本例中为 0x1234)进行操作。在 Cortex-M 上,我们的闪存 ROM 也映射到地址 0x0000_0000,最高可达地址 0x0007_FFFF(如果我们有 512 KiB 的闪存 ROM)。微控制器设计者没有忽略这两个区域之间的所有剩余空间,而是将外设的接口映射到特定的内存位置。最终看起来像这样:

Nordic nRF52832 数据手册 (pdf)

内存映射外设

乍一看,与这些外设的交互很简单——将正确的数据写入正确的地址。例如,通过串行端口发送一个 32 位字就像将该 32 位字直接写入某个内存地址一样简单。然后,串行端口外设将接管并自动发送数据。

这些外设的配置方式类似。不是调用函数来配置外设,而是暴露一块内存作为硬件 API。将 0x8000_0000 写入 SPI 频率配置寄存器,SPI 端口将以每秒 8 兆比特的速度发送数据。将 0x0200_0000 写入同一地址,SPI 端口将以每秒 125 千比特的速度发送数据。这些配置寄存器看起来有点像这样:

Nordic nRF52832 数据手册 (pdf)

无论使用何种语言,无论是汇编语言、C 语言还是 Rust 语言,都通过此接口与硬件进行交互。