可移植性

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

一种常见的消除此类差异的方法是通过一个称为硬件抽象层或 HAL 的层。

硬件抽象是软件中的一组例程,它们模拟一些平台特定的细节,使程序可以直接访问硬件资源。

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

维基百科:硬件抽象层

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

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

什么是 embedded-hal?

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

一个典型的分层可能如下所示

embedded-hal 中定义的一些 trait 是

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

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

embedded-hal 的用户

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

HAL 实现

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

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

这样的 HAL 实现 可以有多种形式

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

驱动程序

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

必须使用实现 embedded-hal 的特定 trait 的类型实例初始化驱动程序,这通过 trait 约束来确保,并提供其自己的类型实例以及一组自定义方法,从而允许与被驱动的设备进行交互。

应用程序

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