可移植性
在嵌入式环境中,可移植性是一个非常重要的主题:不同厂商,甚至同一厂商的不同家族的微控制器都提供不同的外围设备和功能,并且与这些外围设备进行交互的方式也会有所不同。
填平这种差异的常用方法是通过硬件抽象层(HAL)。
硬件抽象层是一组例程,它们可以模拟某些特定平台的详细信息,从而使程序可以直接访问硬件资源。
通过提供对硬件的标准操作系统(OS)调用,从而允许程序员编写与设备无关的高性能应用程序。
维基百科:硬件抽象层
嵌入式系统在这方面有点特殊,因为他们通常没有操作系统,也不允许用户安装自己的软件. 并且固件映像是作为一个整体编译的,此外还有许多其他限制。因此尽管维基百科定义的传统方法可能可行,但它很可能不是确保可移植性的最有效的方法。
我们如何在Rust中做到这一点?那就是embedded-hal ...
什么是Embedded-hal?
简而言之,它是一组Trait,它们定义了HAL实现,驱动程序和应用程序(或固件)之间的实现合约。这些合约包括功能(如果为某种类型实现了某种Trait,HAL实现会提供某种能力)和方法(如果某种类型实现了某个Trait,HAL确保这个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
这样的HAL实现可以有多种形式:
- 通过低级别的硬件访问,例如寄存器
- 通过操作系统,例如在Linux下使用
sysfs - 通过适配器,例如模拟单元测试的类型
- 通过硬件适配器的驱动程序,例如I2C多路复用器或GPIO扩展器
驱动
驱动程序为内部或外部组件实现了一组自定义功能,这些组件连接到实现了Embedded-hal trait的外围设备。这种驱动程序的典型示例包括各种传感器(温度,磁力计,加速度计,光线),显示设备(LED阵列,LCD显示屏)和执行器(电机,发射器)。
一个驱动程序必须用一个实现了Embedded-hal的相应trait的实例来初始化,并提供一组自定义方法,以允许与被驱动设备进行交互。
应用
该应用程序将各个部分绑定在一起,并确保实现所需的功能。在不同系统之间进行移植时,这是需要花费大量精力的部分,因为应用程序需要通过HAL实现正确地初始化实际硬件,并且不同硬件的初始化有时甚至完全不同。另外,用户的选择通常也起着很大的作用,因为组件可以连接到不同的终端,有时硬件总线需要外部硬件来匹配配置,或者在使用内部外设时需要进行不同的权衡(例如,多个具有不同功能的定时器或外设之间互相冲突)。