外设

什么是外设?

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

这些外设很有用,因为它们允许开发人员将处理过程卸载到它们,避免必须在软件中处理所有内容。类似于桌面开发人员如何将图形处理卸载到显卡一样,嵌入式开发人员可以将一些任务卸载到外设,从而使 CPU 可以腾出时间做其他重要的事情,或者什么也不做以节省电量。

如果您查看 1970 年代或 1980 年代老式家用计算机中的主电路板(实际上,昨天的台式 PC 与今天的嵌入式系统并没有那么大的区别),您会期望看到

  • 一个处理器
  • 一个 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,这种接口都是与硬件交互的方式。