可移植性

在嵌入式环境中,可移植性是一个非常重要的主题:每个供应商,甚至单个制造商的每个系列都提供不同的外设和功能,同样,与外设交互的方式也会有所不同。

一种常见的均衡这种差异的方法是通过称为硬件抽象层或 **HAL** 的层。

硬件抽象是在软件中的一组例程,用于模拟一些平台特定的细节,使程序能够直接访问硬件资源。

它们通常通过提供对硬件的标准操作系统 (OS) 调用,允许程序员编写与设备无关的高性能应用程序。

维基百科:硬件抽象层

嵌入式系统在这方面有点特殊,因为我们通常没有操作系统和用户可安装的软件,而是固件映像,这些映像作为一个整体以及许多其他约束条件进行编译。因此,虽然维基百科定义的传统方法可能有效,但它可能不是确保可移植性的最有效方法。

我们如何在 Rust 中做到这一点?进入 **embedded-hal**...

什么是 embedded-hal?

简而言之,它是一组定义 **HAL 实现**、**驱动程序** 和 **应用程序(或固件)** 之间实现契约的特征。这些契约包括功能(即如果为某个类型实现了特征,则 **HAL 实现** 提供了某种功能)和方法(即如果您能够构造一个实现特征的类型,则保证您拥有特征中指定的方法可用)。

典型的分层可能如下所示

**embedded-hal** 中定义的一些特征是

  • GPIO(输入和输出引脚)
  • 串行通信
  • I2C
  • SPI
  • 定时器/倒计时
  • 模数转换

拥有 **embedded-hal** 特征和实现和使用它们的板条箱的主要原因是控制复杂性。如果您考虑到应用程序可能需要实现硬件中外设的使用以及应用程序和可能用于其他硬件组件的驱动程序,那么应该很容易看出可重用性非常有限。用数学表达,如果 **M** 是外设 HAL 实现的数量,而 **N** 是驱动程序的数量,那么如果我们要为每个应用程序重新发明轮子,那么我们最终将得到 **M*N** 个实现,而通过使用 **embedded-hal** 特征提供的 *API* 将使实现复杂度接近 **M+N**。当然,还有其他好处,例如由于定义明确且可立即使用的 API,因此减少了试错。

embedded-hal 的用户

如上所述,HAL 的主要用户有三个

HAL 实现

HAL 实现提供硬件和 HAL 特征用户之间的接口。典型的实现包括三个部分

  • 一个或多个硬件特定类型
  • 用于创建和初始化此类类型的函数,通常提供各种配置选项(速度、操作模式、使用引脚等)
  • 该类型的一个或多个 **embedded-hal** 特征的 trait impl

这种 **HAL 实现** 可以有各种形式

  • 通过低级硬件访问,例如通过寄存器
  • 通过操作系统,例如使用 Linux 下的 sysfs
  • 通过适配器,例如用于单元测试的类型模拟
  • 通过硬件适配器的驱动程序,例如 I2C 多路复用器或 GPIO 扩展器

驱动程序

驱动程序为连接到实现 embedded-hal 特征的外设的内部或外部组件实现一组自定义功能。此类驱动程序的典型示例包括各种传感器(温度、磁力计、加速度计、光)、显示设备(LED 阵列、LCD 显示器)和执行器(电机、发射器)。

驱动程序必须使用实现 embedded-hal 的某个 trait 的类型的实例进行初始化,这通过特征绑定来确保,并提供自己的类型实例,该实例具有允许与驱动设备交互的自定义方法集。

应用程序

应用程序将各个部分绑定在一起,并确保实现所需的功能。在不同系统之间移植时,这是需要最多适应工作量的一部分,因为应用程序需要通过 HAL 实现正确地初始化实际硬件,而不同硬件的初始化方式不同,有时差异很大。此外,用户选择通常也起着重要作用,因为组件可以物理连接到不同的端子,硬件总线有时需要外部硬件才能匹配配置,或者在使用内部外设时需要做出不同的权衡(例如,有多个具有不同功能的定时器可用,或者外设与其他外设冲突)。