五、使用串行接口与高速传感器接口

在前一章中,您使用 I2C 总线与 FRAM 设备进行通信,该设备需要比 GPIOs 使用的简单开/关数字通信复杂得多的通信。I2C 非常强大和灵活,但它可能相当缓慢。

在本章中,您将学习如何编写一个安卓应用,该应用使用 BBB 的 SPI 功能从高速传感器中检索环境数据。我们将涵盖以下主题:

  • 理解 SPI
  • BBB 上 SPI 的多路复用
  • 在 Linux 内核中表示 SPI 设备
  • 构建 SPI 接口电路
  • 探索 SPI 传感器示例应用

理解 SPI

串行外设接口 ( SPI )总线是一种高速串行总线,最初由摩托罗拉开发。其目的是促进单个主设备和一个或多个从设备之间的点对点通信。SPI 总线通常使用四个信号来实现:

  • SCLK
  • MOSI
  • MISO
  • SS / CS

像 I2C 一样,SPI 总线上的主机通过产生时钟信号来设定主机和从机之间的通信速度。有了 SPI,这个时钟信号被称为 串行时钟 ( SCLK)。与 I2C 的双向数据总线不同,SPI 为每个设备使用专用的输出和输入数据线。使用专用线路导致 SPI 能够实现远高于 I2C 的通信速度。主设备通过 主设备输出、 ( MOSI)信号向从设备发送数据,通过 主设备输入、 ( MISO)信号从设备接收数据。 从设备选择 ( SS)信号,也称为 芯片选择 ( CS)告诉从设备是否应该唤醒,并注意SCLK上的任何时钟信号和通过MOSI发送给它的数据。这种四线 SPI 总线方案有多种变体,例如省略SS / CS信号的三线方案,但 BBB 的 SPI 总线使用四线方案。

Understanding SPI

SPI 总线上的 SPI 主设备和从设备

BBB 既可以作为 SPI 主机,也可以作为 SPI 从机,因此它不会将其 SPI 的数据输入和输出信号标记为MISOMOSI。相反,它使用名称D0D1来表示这些信号。如果 BBB 充当 SPI 总线上的主机,D0MISO信号,D1MOSI信号。如果 BBB 作为 SPI 总线上的从机,则这些是相反的(D1MISOD0MOSI)。对于这本书来说,BBB 将永远是 SPI 的主人。

类型

如何记住哪个 BBB SPI 信号是输入,哪个是输出?

当 BBB 使用信号名称D0D1时,记住哪个信号是MISO和哪个是MOSI可能会令人困惑。一种记忆方法是把D0中的0想象成一个 O (用于从机输出),把D1中的1想象成一个 I (用于从机输入)。如果 BBB 是 SPI 主机(几乎总是如此),那么D1是从机输入信号(MOSI),D0是从机输出信号(MISO)。

BBB 上 SPI 的最大SCLK速度为 48 兆赫,但通常使用 1 兆赫至 16 兆赫的速度。即使在这些降低的时钟速度下,考虑到每秒可以传输的原始数据量,SPI 也远远优于 I2C 总线的 400 千赫时钟速度。任何时候都只有一个设备可以在 I2C 总线上传输数据,但是主设备和从设备都可以在 SPI 总线上同时传输数据,因为每个设备都有一个专用的传输信号。

BBB 上 SPI 的复用

BBB 的 AM335X 处理器提供两个 SPI 总线:SPI0 和 SPI1。两辆公共汽车都可以通过 P9 集管到达。默认情况下,没有混合的 SPI 总线。下图显示了 P9 接口上的每个潜在引脚,SPI 信号可以在不同的引脚复用模式下复用:

Multiplexing for SPI on the BBB

具有不同引脚复用模式的 P9 报头上 SPI 总线的位置

在决定如何在项目中使用 SPI 混合引脚时,请记住以下几点:

  • 如有疑问,请继续使用固定在 P9.17、P9.18、P9.21 和 P9.22 引脚上的 SPI0 总线。
  • SPI1 通道与 capemgr 使用的 I2C 总线(P9.20)和音频输出(P9.28、P9.29、P9.31)冲突。请注意,多路复用这些引脚以使用 SPI1 可能会禁用一些其他功能,而这些功能是您在全功能安卓系统中所依赖的。
  • 如果您在项目中使用其他 cape 板,请确保这些 cape 不需要使用 SPI 总线。每个 SPI 总线上只能存在一个器件,除非您使用 GPIO 引脚和额外的逻辑电路来手动控制每个 SPI 器件的芯片选择信号。

代表 Linux 内核中的 SPI 设备

Linux 内核提供了一个名为spidev的通用 SPI 驱动。spidev驱动程序是一个简单的接口,它抽象了 SPI 通信中涉及的许多内务处理细节。spidev驱动程序通过/dev文件系统作为/dev/spidevX.Y文件公开。根据设备树中配置的 SPI 总线数量,这些spidev文件可以有多个版本。spidev文件名中的X值指的是 SPI 控制器编号(SPI0 为 1,SPI1 为 2),Y值指的是该控制器的 SPI 总线(第一条总线为 0,第二条总线为 1)。对于本书中的示例,您将只使用 SPI0 控制器的第一条 SPI 总线,因此/dev/spidev1.0是 PacktHAL 将与之交互的唯一文件。

准备安卓系统使用 SPI 传感器

第二章与安卓接口中,你使用adb将两个预建文件推送到你的安卓系统。这两个文件BB-PACKTPUB-00A0.dtboinit.{ro.hardware}.rc配置你的安卓系统,启用spidev内核设备驱动,处理 SPI 总线接口,多路复用管脚以启用 SPI0 总线,并允许你的应用访问它们。

就 SPI 而言,BB-PACKTPUB-00A0.dtbo叠加多路复用器分别将 P9.17、P9.18、P9.21 和 P9.22 引脚接入 SPI CS0D1D0SCLK信号。在PacktHAL.tgz文件中,覆盖的源代码位于cape/BB-PACKTPUB-00A0.dts文件中。负责复用这两个引脚的代码位于fragment@0内的bb_spi0_pins节点:

/* All SPI0 pins are PULL, MODE0 */
bb_spi0_pins: pinmux_bb_spi0_pins {
    pinctrl-single,pins = <
        0x150 0x30  /* P9.22, spi0_sclk, INPUT */
        0x154 0x30  /* P9.21, spi0_do, INPUT */
        0x158 0x10  /* P9.18, spi0_d1, OUTPUT */
        0x15c 0x10  /* P9.17, spi0_cs0, OUTPUT */
    >;
};

虽然这设置了多路复用,但它不会为这些引脚分配和配置设备驱动程序。fragment@2节点执行内核驱动程序分配:

fragment@2 {
    target = <&spi0>;
    __overlay__ {
        #address-cells = <1>;
        #size-cells = <0>;
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&bb_spi0_pins>;

        channel@0 {
            #address-cells = <1>;
            #size-cells = <0>;
            /* Kernel driver for this device */
            compatible = "spidev";

            reg = <0>;
            /* Setting the max frequency to 16MHz */
            spi-max-frequency = <16000000>;
            spi-cpha;
        };
        
    };
};

不用深究细枝末节,fragment@2中有三个设置是你感兴趣的:

  • pinctrl-0
  • compatible
  • spi-max-frequency

第一个是pinctrl-0,它将设备树的这个节点与bb_spi0_pins节点中固定的引脚联系起来。第二个是compatible,它指定了特定的内核驱动程序spidev,将处理我们的硬件设备。最后一个是spi-max-frequency,它规定了这个 SPI 总线的最大允许速度(16 MHz)。16 兆赫是 BBB 内核信号源提供的设备树覆盖图中为spidev指定的最大频率。

你推送到安卓系统的自定义init.{ro.hardware}.rc文件不需要为 PacktHAL 的 SPI 接口做任何特别的事情。默认情况下,BBBAndroid 使用chmod/dev/spidev*文件的权限设置为 777(所有人都可以完全访问)。这不是一种安全的做法,因为系统上的任何进程都有可能打开spidev设备并开始对硬件进行读写。然而,出于我们的目的,让每个进程都可以访问/dev/spidev*文件是允许我们的非特权示例应用访问 SPI 总线所必需的。

搭建 SPI 接口电路

现在您已经了解了 SPI 设备连接到 BBB 的位置,以及 Linux 内核如何向这些设备呈现接口,现在是时候将 SPI 设备连接到 BBB 了。

正如我们在第 1 章安卓和 BeagleBone Black的介绍中所提到的,在这一章中你将与一个传感器进行交互。具体来说,我们将使用博世 Sensortec BMP183 数字压力传感器。这个 7 针组件为导航、天气预报和测量垂直高度变化等应用提供压力数据样本(16 至 19 位分辨率)和温度数据样本(16 位分辨率)。

这种特殊的芯片仅在 陆地栅格阵列 ( LGA )中可用,这是一种表面贴装封装,在构建原型电路时可能很难使用。幸运的是,传感器的 ada 水果转接板已经安装了芯片,这使得原型制作变得简单易行。

Building an SPI interface circuit

传感器分线板(来源:www.adafruit.com)

分线板将SCLK信号标记为SCKMOSI标记为SDI(串行数据输入)、MISO标记为SDO(串行数据输出)、SS标记为CS(芯片选择)。为了给电路板供电,一个+3.3 V 信号连接到VCC,一个接地连接到GND。分线板的3Vo信号提供+3.3 V 信号,在我们的示例中不使用。

类型

不要拆你的电路!

本章中的传感器电路是第 6 章中使用的更大电路的一部分,创建了一个完整的接口解决方案。如果您按照图表中的位置(靠近试验板的中间)构建电路,您可以在构建本书中的其余电路时,简单地将传感器转接板和导线留在原位。这样,当你到达第六章时,它就已经建成并开始工作了。

连接传感器

下图显示了传感器转接板和 BBB 之间的连接。六个主要的 SPI 总线信号(+3.3 V,接地,以及 SPI SCLKMISOMOSISS)是使用 P9 连接器的引脚制作的,因此我们将试验板放在 BBB 的 P9 侧。

Connecting the sensor

完整的传感器接口电路

让我们开始吧:

  1. 将 P9.1(接地)连接至试验板的垂直接地母线,将 P9.3 (3.3 V)连接至试验板的垂直 VCC 母线。这些连接与您在第 3 章中使用 GPIO第 4 章处理输入和输出以及使用 I2C 存储和检索数据时创建的 GPIO 和 I2C 试验板电路相同。
  2. 四个 SPI 总线信号,SCLKMISO ( D0)、MOSI ( D1)和SS分别位于 P9.22、P9.21、P9.18 和 P9.17 引脚。将 P9.22 引脚连接到转接板上标记为 SCK 的引脚,并将 P9.21 引脚连接到标记为 SDO 的引脚。然后,将 P9.18 引脚连接到标有 SDI 的引脚,将 P9.17 引脚连接到标有 CS 的引脚。
  3. 将接地总线连接到转接板的 GND 引脚,将 VCC 总线连接到转接板的 VCC 引脚。断开转接板的 3Vo 引脚。

传感器转接板现已与 BBB 电连接,可供您使用。对照完整的传感器接口电路图仔细检查您的接线,以确保一切连接正确。

探索 SPI 传感器示例应用

在这一部分,您将研究在 BBB 上执行 SPI 总线接口的示例 Android 应用。本应用的目的是演示如何使用一组接口功能使用 PacktHAL 从实际应用中执行 SPI 读写。这些功能允许您在 SPI 总线主机(BBB)和 SPI 总线从机(SPI 传感器)之间发送和接收数据。硬件接口的底层细节在 PacktHAL 中实现,因此您可以快速轻松地让您的应用与传感器交互。

在挖掘 SPI 应用的代码之前,您必须将代码安装到您的开发系统中,并将应用安装到您的安卓系统中。该应用的源代码和预编译的.apk包位于chapter5.tgz文件中,该文件可从 Packt 的网站下载。按照第 3 章第 4 章【用 GPIOs 处理输入和输出】和第 4 章【用 I2C 存储和检索数据】中描述的相同过程下载应用并将其添加到您的 Eclipse ADT 环境中。**

应用的用户界面

应用使用非常简单的 UI 与传感器进行交互。就这么简单,app 唯一的活动(默认)就是MainActivity。用户界面仅由一个按钮和两个文本视图组成。

The app's user interface

从传感器接收第一组样本之前的传感器样本应用屏幕

顶部文本视图在activity_main.xml文件中有temperatureTextView标识符,底部文本视图有pressureTextView标识符。这些文本视图将显示从传感器获取的温度和压力数据。带有样品标签的按钮有sampleButton标识符。该按钮有一个名为onClickSampleButton()onClick()方法,触发与传感器接口的过程,对温度和压力数据进行采样,然后更新temperatureTextViewpressureTextView文本视图中显示的文本。

调用 PacktHAL 传感器功能

PacktHAL 中的传感器接口功能在sensor应用项目内的jni/bmp183.c文件中的各种 C 函数中实现。这些功能不仅与传感器接口,而且还执行各种转换和校准任务。

上一章中的fram应用使用了特定的内核驱动程序(即24c256 EEPROM 驱动程序)与 FRAM 芯片进行交互,因此在 PacktHAL 中实现的用户空间接口逻辑非常简单。PacktHAL 不使用特定于传感器的内核驱动程序与传感器通信,因此必须使用通用的spidev驱动程序进行通信。由 PacktHAL 来准备、发送、接收和解释去往或来自传感器的每个 SPI 消息的单个字节。

虽然 PacktHAL 中有许多功能可以处理这些任务,但只有四个功能被外部代码用来与传感器交互:

  • openSensor()
  • getSensorTemperature()
  • getSensorPressure()
  • closeSensor()

这些功能的原型位于jni/PacktHAL.h头文件中:

extern int openSensor(void);
extern float getSensorTemperature(void);
extern float getSensorPressure(void);
extern int closeSensor(void);

openSensor()功能通过打开/dev/spidev1.0并进行几次ioctl()调用来配置 SPI 总线的通信参数(如SCLK的时钟速率),从而初始化对 SPI 总线的访问。

一旦执行了这种配置,在 PacktHAL 内部执行的所有 SPI 通信都将使用该总线。调用对应的 closeSensor()函数关闭/dev/spidev1.0文件,该文件关闭 SPI 总线并将其释放给系统上的其他进程使用。 getSensorTemperature()getSensorPressure()功能执行从传感器获取和转换样本所需的所有 SPI 消息准备、SPI 通信和样本转换逻辑。

如果您正在使用一个专门的内核驱动程序来与我们正在使用的特定传感器进行对话,那么 PacktHAL 代码中的传感器读取逻辑将非常简单(只有一两次ioctl()调用)。在将 HAL 代码逻辑放入内核和将它保留在用户空间之间总是一种平衡。可以推入内核的代码越多,用户空间代码就越简单、越快。然而,开发内核代码可能非常困难,因此您必须在最容易实现的功能和为您的硬件设计提供必要性能的功能之间取得平衡。

sensor应用与前几章的应用有几个相似之处。与第 4 章、中的fram应用一样,利用 I2C 存储和检索数据,传感器应用使用自己从AsyncTaskHardwareTask派生的类,从 PacktHAL 对底层传感器接口功能进行 JNI 调用。与硬件的接口由应用用户按下的按钮的onClick()处理器触发,类似于gpiofram应用所做的。

就像您在第 3 章中使用的来自 PacktHAL 的 GPIO 接口功能一样,使用 GPIOs处理输入和输出,使用 I2C 存储和检索数据,HardwareTask中的传感器接口方法执行起来非常快。实际上没有必要在一个单独的线程中执行这些方法,因为它们的执行时间不会长到触发 ANR 对话。但是 SPI 可以用于多种设备,有可能需要更长的时间发送大量数据,安全总比抱歉好。

类型

什么时候应该使用异步收发器进行硬件接口?

对此的简短回答是“一直”。当您在第 3 章、中使用 GPIOs 处理输入和输出时,我们不想用AsyncTask类的细节来分散您的注意力,因此gpio应用对onClick()按钮处理程序中的 PacktHAL 函数进行了方法调用。然而,要遵循的一般规则是始终使用AsyncTask来执行任何类型的输入/输出。输入/输出是出了名的慢,因此任何输入/输出(联网、访问磁盘上的文件和硬件接口)都应该真正通过AsyncTask在自己的线程中进行。

使用硬件任务类

gpiofram应用一样,传感器应用中的HardwareTask类提供了四种本地方法,用于调用与传感器硬件接口相关的 PacktHAL JNI 函数:

public class HardwareTask extends AsyncTask<Void, Void, Boolean> {

  private native boolean openSensor();
  private native float getSensorTemperature();
  private native float getSensorPressure();
  private native boolean closeSensor();

由于 SPI 总线设置过程的细节被封装在 PacktHAL 函数中,并对应用隐藏,因此这些方法不采用任何参数。他们只是通过包装器 JNI 包装器函数来调用他们的包装器。

Using the HardwareTask class

执行HardwareTask方法和打包函数的线程上下文

在传感器应用的中,MainActivity类中采样按钮的onClick()处理程序实例化了一个新的HardwareTask方法。在这个实例化之后,立即调用HardwareTaskpollSensor()方法,从传感器请求一组当前的温度和压力数据:

    public void onClickSampleButton(View view) {
        hwTask = new HardwareTask();
        hwTask.pollSensor(this);  
    }

pollSensor()方法通过调用基础AsyncTask类的 execution()方法来创建新线程,从而开始硬件接口过程:

    public void pollSensor(Activity act) {
      mCallerActivity = act;
      execute();
    }

AsyncTaskexecute()方法调用HardwareTask使用的onPreExecute()方法,通过其openSensor()本地方法初始化 SPI 总线。在线程期间sampleButton方法也被禁用,以防止多个线程试图使用 SPI 总线同时与传感器对话的可能性:

   protected void onPreExecute() {  
      Log.i("HardwareTask", "onPreExecute");
      ...    
     if ( !openSensor() ) {
         Log.e("HardwareTask", "Error opening hardware");
        isDone = true;
      }
      // Disable the Button while talking to the hardware
      sampleButton.setEnabled(false);
   }

一旦onPreExecute()方法结束,AsyncTask基类旋转一个新线程并在该线程中执行doInBackground()方法。对于传感器应用,这是执行从传感器获取当前温度和压力样本所需的任何 SPI 总线通信的合适位置。HardwareTask类的getSensorTemperature()getSensorPressure()本地方法通过 PacktHAL 中的getSensorTemperature()getSensorPressure()功能从传感器获取最新样本:

    protected Boolean doInBackground(Void... params) { ) { 

      if (isDone) { // Was the hardware never opened?
        Log.e("HardwareTask", "doInBackground: Skipping hardware interfacing");
        return true;
      }

      Log.i("HardwareTask", "doInBackground: Interfacing with hardware");
      try {
        temperature = getSensorTemperature();
        pressure = getSensorPressure();
      } catch (Exception e) {
       ...

doInBackground()完成后,AsyncTask螺纹终止。这触发了用户界面线程对doPostExecute()的调用。现在,由于应用已经完成了其 SPI 通信任务,并从传感器接收到最新的温度和压力值,是时候关闭 SPI 连接了。doPostExecute()方法使用HardwareTask类的closeSensor()本地方法关闭 SPI 总线。然后doPostExecute()方法通过updateSensorData()方法提醒从传感器接收的新数据的MainActivity类别,并重新启用MainActivity样本按钮:

   protected void onPostExecute(Boolean result) {
      if (!closeSensor()) {
        Log.e("HardwareTask", "Error closing hardware");
     }
      ...
         Toast toast =  
            Toast.makeText(mCallerActivity.getApplicationContext(),
            "Sensor data received", Toast.LENGTH_SHORT);
         toast.show();
         ((MainActivity)mCallerActivity).updateSensorData(temperature,
            pressure);
      ...
      // Reenable the Button after talking to the hardware
      sampleButton.setEnabled(true);

MainActivity类的updateSensorData()方法负责更新temperatureTextViewpressureTextView文本视图中显示的值,以反映最新接收的传感器值:

    public void updateSensorData(float temperature, float pressure) {
      Toast toast = Toast.makeText(getApplicationContext(), 
          "Displaying new sensor data", Toast.LENGTH_SHORT);
      TextView tv = (TextView) findViewById(R.id.temperatureTextView);    
       tv.setText("Temperature: " + temperature);

    tv = (TextView) findViewById(R.id.pressureTextView);
       tv.setText("Pressure: " + pressure);

       toast.show();
    }

此时,sensor应用的执行已经回到了空闲状态。如果用户再次点击采样按钮,另一个HardwareTask实例被实例化,硬件的打开-采样-关闭交互循环将再次发生。

类型

你准备好迎接挑战了吗?

既然您已经看到了传感器应用的所有部分,为什么不更改它以添加一些新功能呢?对于挑战,尝试添加一个计数器,显示到目前为止已经采集了多少样本,以及所有样本的平均温度和压力。我们已经在chapter5_challenge.tgz文件中提供了一个可能的实现,该文件可以从 Packt 的网站上下载。

总结

在本章中,我们向您介绍了 SPI 总线。您构建了一个将 SPI 压力和温度传感器转接板连接到 BBB 的电路,并了解了 PacktHAL init.{ro.hardware}.rc文件的设备树覆盖中负责配置 SPI 总线和spidev设备驱动程序并使其可供应用使用的部分。本章中的传感器应用演示了如何使用一组隐藏低级细节的小功能来隐藏哈尔中的复杂任务。这些简化的 PacktHAL 函数调用可以从一个源自AsyncTask的类中进行,只需从一个应用中执行更复杂的接口任务。

在下一章中,您将了解到如何将 GPIO、I2C 和 SPI 结合到一个能够提供完整硬件解决方案的应用中,该解决方案使用了长时间的硬件接口线程。