Skip to content

Latest commit

 

History

History
873 lines (628 loc) · 32 KB

File metadata and controls

873 lines (628 loc) · 32 KB

九、外部设备

与外部设备的通信是任何嵌入式应用的重要组成部分。 应用需要检查可用性和状态,并向各种设备发送数据和从各种设备接收数据。

每个目标平台都是不同的,并且存在许多将外部设备连接到计算单元的方式。 然而,有几种硬件和软件接口已经成为与外部设备通信的行业标准。 在本章中,我们将学习如何使用直接连接到处理器引脚或通过串行接口连接的外部设备。 本章涵盖以下主题:

  • 控制通过 GPIO 连接的设备
  • 探索脉宽调制
  • 在 Linux 中使用 ioctl 访问实时时钟
  • 用 libgpiod 控制 GPIO 引脚
  • 控制 I2C 外部设备

本章中的食谱涉及到与真实硬件的交互,并且打算在真实的 Raspberry Pi 板上运行。

控制通过 GPIO 连接的设备

通用输入输出(GPIO)是将外部设备连接到 CPU 的最简单方式。 每个处理器通常都有一定数量的引脚预留给一般用途。 这些管脚可以直接电连接到外部设备的管脚。 嵌入式应用可以通过改变配置用于输出的管脚的信号电平或通过读取输入管脚的信号电平来控制设备。

信号电平的解释不遵循任何协议,并且由外部设备确定。 开发人员需要查阅设备数据手册才能对通信进行正确编程。

这种类型的通信通常使用专用的设备驱动程序在内核端完成。 然而,这并不总是一个要求。 在本教程中,我们将学习如何从用户空间应用使用 Raspberry PI 板上的 GPIO 接口。

怎么做……

我们将创建一个简单的应用来控制连接到 Raspberry PI 板上的通用引脚的发光二极管(LED):

  1. 在您的~/test工作目录中,创建一个名为gpio的子目录。
  2. 使用您喜欢的文本编辑器在gpio子目录中创建gpio.cpp文件。
  3. 将以下代码片段放入文件中:
#include <chrono>
#include <iostream>
#include <thread>
#include <wiringPi.h>

using namespace std::literals::chrono_literals;
const int kLedPin = 0;

int main (void)
{
  if (wiringPiSetup () <0) {
    throw std::runtime_error("Failed to initialize wiringPi");
  }

  pinMode (kLedPin, OUTPUT);
  while (true) {
    digitalWrite (kLedPin, HIGH);
    std::cout << "LED on" << std::endl;
    std::this_thread::sleep_for(500ms) ;
    digitalWrite (kLedPin, LOW);
    std::cout << "LED off" << std::endl;
    std::this_thread::sleep_for(500ms) ;
  }
  return 0 ;
}
  1. 创建包含我们程序的构建规则的CMakeLists.txt文件:
cmake_minimum_required(VERSION 3.5.1)
project(gpio)
add_executable(gpio gpio.cpp)
target_link_libraries(gpio wiringPi)
  1. 按照http://wiringpi.com/examples/blink/WiringPI 示例部分中的说明将 LED 连接到树莓 PI 板。
  2. 设置到您的 Raspberry PI 板的 SSH 连接。 按照https://www.raspberrypi.org/documentation/remote-access/ssh/覆盆子 PI 文档部分的说明进行操作。
  3. 通过 SSH 将gpio文件夹的内容复制到 Raspberry PI 板。
  4. 通过 SSH 登录主板,然后构建并运行应用:
$ cd gpio && cmake . && make && sudo ./gpio

您的应用应该会运行,并且您应该能够观察到 LED 闪烁。

它是如何运作的..。

覆盆子 PI 板有 40 个引脚(第一个型号为 26 个),可使用内存映射输入输出(MMIO)机制进行编程。 MMIO 允许开发人员通过在系统的物理内存中读取或写入特定地址来查询或设置管脚的状态。

第 6 章内存管理中的使用专用内存配方中,我们学习了如何访问 MMIO 寄存器。 在本食谱中,我们将把 MMIO 地址的操作卸载到专门库wiringPi。 它隐藏了内存映射和在幕后查找适当偏移量的所有复杂性,而是公开了一个干净的 API。

这个库预装在 Raspberry PI 板上,因此为了简化构建过程,我们将直接在板上构建代码,而不是使用交叉编译。 与其他方法不同,我们的构建规则没有提到交叉编译器--我们将在主板上使用本机 ARM 编译器。 我们只向wiringPi库添加依赖项:

target_link_libraries(gpio wiringPi)

此示例的代码是对闪烁 LED 的wiringPi示例的修改。 首先,我们初始化wiringPi库:

if (wiringPiSetup () < 0) {
    throw std::runtime_error("Failed to initialize wiringPi");
}

接下来,我们进入无穷无尽的循环。 在每次迭代中,我们将管脚设置为HIGH状态:

    digitalWrite (kLedPin, HIGH);

500 ms延迟之后,我们将相同的凹坑设置为LOW状态,并添加另一个延迟:

 digitalWrite (kLedPin, LOW);
    std::cout << "LED off" << std::endl;
 std::this_thread::sleep_for(500ms) ;

我们将程序配置为使用管脚0,它对应于 Raspberry PI 的BCM2835芯片的GPIO.0或管脚17

const int kLedPin = 0;

如果 LED 连接到此引脚,它将闪烁,亮起 0.5 秒,然后熄灭 0.5 秒。 通过调整循环中的延迟,您可以更改闪烁模式。

由于程序进入死循环,我们可以随时通过在 SSH 控制台中按Ctrl+C来终止它;否则,它将永远运行。

当我们运行应用时,我们只看到以下输出:

我们在转动 LEDonoff时记录,但要检查程序是否正常工作,我们需要查看连接到引脚上的 LED。 如果我们按照接线说明操作,我们就能看到它是如何工作的。 程序运行时,板卡上的 LED 与程序输出同步闪烁:

我们能够控制直接连接到 CPU 引脚的简单设备,而无需编写复杂的设备驱动程序。

探索脉宽调制

数字引脚只能处于以下两种状态之一:HIGHLOW。 连接到数字引脚的 LED 也只能相应地处于以下两种状态之一:onoff。 但有没有办法控制这款 LED 的亮度呢? 是的,我们可以使用一种称为脉宽调制(PWM)的方法。

PWM 背后的想法很简单。 我们通过周期性地打开或关闭电信号来限制其提供的功率。 这使得信号脉冲具有一定的频率,且功率大小与脉冲宽度成正比,即信号为HIGH的时间。

例如,如果我们将引脚转到HIGH10 微秒,然后在循环中再将LOW转到 90 微秒,则连接到该引脚的设备将获得引脚始终为HIGH时所提供电力的 10%。

在本食谱中,我们将学习如何使用 PWM 来控制连接到树莓 PI 板上的数字 GPIO 引脚的 LED 的亮度。

怎么做……

我们将创建一个简单的应用,用于逐渐改变连接到 Raspberry PI 板上通用引脚的 LED 的亮度:

  1. 在您的~/test工作目录中,创建一个名为pwm的子目录。
  2. 使用您喜欢的文本编辑器在pwm子目录中创建pwm.cpp文件。
  3. 让我们放入所需的include函数,并定义一个名为Blink的函数:
#include <chrono>
#include <thread>

#include <wiringPi.h>

using namespace std::literals::chrono_literals;

const int kLedPin = 0;

void Blink(std::chrono::microseconds duration, int percent_on) {
    digitalWrite (kLedPin, HIGH);
    std::this_thread::sleep_for(
            duration * percent_on / 100) ;
    digitalWrite (kLedPin, LOW);
    std::this_thread::sleep_for(
            duration * (100 - percent_on) / 100) ;
}
  1. 然后是main函数:
int main (void)
{
  if (wiringPiSetup () <0) {
    throw std::runtime_error("Failed to initialize wiringPi");
  }

  pinMode (kLedPin, OUTPUT);

  int count = 0;
  int delta = 1;
  while (true) {
    Blink(10ms, count);
    count = count + delta;
    if (count == 101) {
      delta = -1;
    } else if (count == 0) {
      delta = 1;
    }
  }
  return 0 ;
}
  1. 创建包含我们程序的构建规则的CMakeLists.txt文件:
cmake_minimum_required(VERSION 3.5.1)
project(pwm)
add_executable(pwm pwm.cpp)
target_link_libraries(pwm wiringPi)
  1. 按照http://wiringpi.com/examples/blink/WiringPI 示例部分中的说明将 LED 连接到树莓 PI 板。
  2. 设置到您的 Raspberry PI 板的 SSH 连接。 按照https://www.raspberrypi.org/documentation/remote-access/ssh/覆盆子 PI 文档部分的说明进行操作。
  3. 通过 SSH 将pwm文件夹的内容复制到 Raspberry PI 板。
  4. 通过 SSH 登录主板,然后构建并运行应用:
$ cd pwm && cmake . && make && sudo ./pwm

您的应用现在应该运行了,您可以看到 LED 在闪烁。

它是如何运作的..。

此配方重用代码来闪烁 LED 和前面配方中的原理图。 我们将此代码从main函数移至一个新函数Blink

Blink函数接受两个参数-durationpercent_on

void Blink(std::chrono::microseconds duration, int percent_on)

duration确定脉冲的总宽度(以微秒为单位)。 percent_on定义信号为HIGH时的时间与脉冲总持续时间的比率。

实现很简单。 调用Blink时,它将管脚转到HIGH,并等待与percent_on成比例的时间量:

    digitalWrite (kLedPin, HIGH);
    std::this_thread::sleep_for(
            duration * percent_on / 100);

之后,它将引脚转到LOW,并等待剩余时间:

    digitalWrite (kLedPin, LOW);
    std::this_thread::sleep_for(
            duration * (100 - percent_on) / 100);

Blink是实现 PWM 的主要构件。 我们可以通过将percent_on0更改为100来控制亮度,如果我们选择足够短的duration,我们将不会看到任何闪烁。

等于或短于电视或监视器刷新率的持续时间就足够好了。 对于 60 Hz,持续时间为 16.6 毫秒。 为简单起见,我们使用 10 毫秒。

接下来,我们将所有内容包装在另一个无限循环中,但现在它有另一个参数count

  int count = 0;

它随着每次迭代而更新,并在0100之间反弹。 delta变量定义变化的方向(减少或增加)以及变化量,在我们的示例中始终为1

  int delta = 1;

当计数达到1010时,方向改变:

    if (count == 101) {
      delta = -1;
    } else if (count == 0) {
      delta = 1;
    }

在每次迭代中,我们调用Blink,将10ms作为脉冲传递,将count作为比率传递,该比率定义 LED 亮起的时间量,从而定义 LED 的亮度(如下图所示):

    Blink(10ms, count);

由于更新频率很高,我们无法判断 LED 何时从打开状态变为关闭状态。

当我们将所有东西连接起来并运行程序时,我们可以看到 LED 变得更亮或更暗。

还有更多的..。

PWM 在嵌入式系统中有着广泛的用途。 它是伺服控制和电压调节的常用机构。 使用脉宽调制维基百科页面(可从https://en.wikipedia.org/wiki/Pulse-width_modulation获取)作为了解该技术的更多信息的起点。

在 Linux 中使用 ioctl 访问实时时钟

在前面的配方中,我们使用 MMIO 从用户空间 Linux 应用访问外部设备。 但是,此接口不是推荐的用户空间应用和设备驱动程序之间的通信方式。

在类似 Unix 的操作系统(如 Linux)中,可以使用所谓的设备文件以与常规文件相同的方式访问大多数外部设备。 当应用打开设备文件时,它可以从该文件读取数据,从相应的设备获取数据,或者写入该文件,向该设备发送数据。

在许多情况下,设备驱动程序不能处理非结构化数据流。 他们希望以请求和响应的形式组织数据交换,其中每个请求和响应都有特定和固定的格式。

这种通信由ioctl系统调用涵盖。 它接受依赖于设备的请求代码作为其参数。 它还可以包含对请求数据进行编码或为输出数据提供存储的其他参数。 这些参数特定于特定设备和请求代码。

在本食谱中,我们将学习如何在用户空间应用中使用ioctl与设备驱动程序进行数据交换。

怎么做……

我们将创建一个应用,从连接到 Raspberry PI 板的实时时钟(RTC)读取当前时间:

  1. 在您的~/test工作目录中,创建一个名为rtc的子目录。
  2. 使用您喜欢的文本编辑器在rtc子目录中创建rtc.cpp文件。
  3. 让我们将所需的include函数放入rtc.cpp文件中:
#include <iostream>
#include <system_error>

#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/rtc.h>
  1. 现在,我们定义一个名为Rtc的类,它封装与实时时钟设备的通信:
class Rtc {
  int fd;
  public:
    Rtc() {
      fd = open("/dev/rtc", O_RDWR);
      if (fd < 0) {
        throw std::system_error(errno,
            std::system_category(),
            "Failed to open RTC device");
      }
    }

    ~Rtc() {
      close(fd);
    }

    time_t GetTime(void) {
      union {
        struct rtc_time rtc;
        struct tm tm;
      } tm;
      int ret = ioctl(fd, RTC_RD_TIME, &tm.rtc);
      if (ret < 0) {
        throw std::system_error(errno,
            std::system_category(),
            "ioctl failed");
      }
      return mktime(&tm.tm);
    }
};
  1. 定义类后,我们将一个简单的用法示例放入main函数:
int main (void)
{
  Rtc rtc;
  time_t t = rtc.GetTime();
  std::cout << "Current time is " << ctime(&t)
            << std::endl;

  return 0 ;
}
  1. 创建包含我们程序的构建规则的CMakeLists.txt文件:
cmake_minimum_required(VERSION 3.5.1)
project(rtc)
add_executable(rtc rtc.cpp)

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)
  1. 构建您的应用并将生成的rtc二进制文件复制到我们的 Raspberry PI 仿真器。

它是如何运作的..。

我们正在实现一个直接与连接到系统的硬件 RTC 对话的应用。 系统时钟和 RTC 之间存在差异。 系统时钟仅在系统运行时处于活动状态并保持不变。 当系统关机或进入休眠模式时,系统时钟将无效。 即使系统关闭,RTC 仍处于活动状态。 它维护用于在系统启动时配置系统时钟的实际时间。 此外,还可以对其进行编程,使其在睡眠模式下的特定时间唤醒系统。

我们将与 RTC 驱动程序的所有通信封装到一个名为Rtc的类中。 与驱动程序的所有数据交换都通过/dev/rtc特殊设备文件。 在Rtc类构造函数中,我们打开设备文件并将生成的文件描述符存储在fd实例变量中:

  fd = open("/dev/rtc", O_RDWR);

类似地,析构函数用于关闭文件:

    ~Rtc() {
      close(fd);
    }

由于设备在销毁Rtc实例后立即在析构函数中关闭,因此我们可以使用Resource Acquisition is Initialization(RAII)习惯用法在出现问题时抛出异常,而不会泄漏文件描述符:

      if (fd < 0) {
        throw std::system_error(errno,
            std::system_category(),
            "Failed to open RTC device");
      }

我们的类只定义了一个成员函数-GetTime。 它是RTC_RD_TIME``ioctl调用之上的包装器。 此调用期望rtc_time结构返回当前时间。 它与我们将用来将 RTC 驱动程序返回的时间转换为 POSIX 时间戳格式的tm结构几乎相同,因此我们将它们放入与union数据类型相同的内存位置:

      union {
        struct rtc_time rtc;
        struct tm tm;
      } tm;

这样,我们可以避免将相同的字段从一个结构复制到另一个结构。

一旦数据结构准备就绪,我们调用ioctl调用,将RTC_RD_TIME常量作为请求 ID 传递,并将指向我们的结构的指针作为存储数据的地址传递到:

  int ret = ioctl(fd, RTC_RD_TIME, &tm.rtc);

一旦成功,ioctl将返回0。 在本例中,我们使用mktime函数将结果数据结构转换为time_tPOSIX 时间戳格式:

  return mktime(&tm.tm);

main函数中,我们创建了Rtc类的一个实例,然后调用GetTime方法:

  Rtc rtc;
  time_t t = rtc.GetTime();

由于 POSIX 时间戳表示自 1970 年 1 月 1 日以来的秒数,因此我们使用ctime函数将其转换为友好的表示形式,并将结果输出到控制台:

  std::cout << "Current time is " << ctime(&t)

当我们运行应用时,我们可以看到以下输出:

我们可以使用ioctl直接从硬件时钟读取当前时间。 ioctlAPI 在 Linux 嵌入式应用中广泛用于与设备通信。

还有更多

在我们的简单示例中,我们学习了如何只使用一个ioctl请求。 RTC 设备支持许多其他请求,这些请求可用于设置警报、更新时间和控制 RTC 中断。 有关更多详细信息,请参阅https://linux.die.net/man/4/rtc上的RTCioctl 文档部分。

用 libgpiod 控制 GPIO 引脚

在前面的食谱中,我们了解了如何使用ioctlAPI 访问 RTC。 我们可以用它来控制 GPIO 引脚吗? 答案是肯定的。 最近,Linux 中添加了一个通用 GPIO 驱动程序,以及一个用户空间库libgpiod,通过在通用ioctlAPI 之上添加一个便利层,简化了对连接到 GPIO 的设备的访问。 该接口允许嵌入式开发人员在任何基于 Linux 的平台上管理他们的设备,而无需编写设备驱动程序。 此外,它还为 C++ 提供开箱即用的绑定。

因此,尽管wiringPi库因其易于使用的接口而仍被广泛使用,但它已被弃用。

在本食谱中,我们将学习如何使用libgpiodC++ 绑定。 我们将使用相同的 LED 闪烁示例来查看wiringPilibgpiod方法的不同和相似之处。

怎么做……

我们将使用新的libgpiodAPI 创建一个应用,使连接到 Raspberry PI 板的 LED 闪烁:

  1. 在您的~/test工作目录中,创建一个名为gpiod的子目录。
  2. 使用您喜欢的文本编辑器在gpiod子目录中创建gpiod.cpp文件。
  3. 将应用的代码放入rtc.cpp文件:
#include <chrono>
#include <iostream>
#include <thread>

#include <gpiod.h>
#include <gpiod.hpp>

using namespace std::literals::chrono_literals;

const int kLedPin = 17;

int main (void)
{

  gpiod::chip chip("gpiochip0");
  auto line = chip.get_line(kLedPin);
  line.request({"test",
                 gpiod::line_request::DIRECTION_OUTPUT, 
                 0}, 0);

  while (true) {
    line.set_value(1);
    std::cout << "ON" << std::endl;
    std::this_thread::sleep_for(500ms);
    line.set_value(0);
    std::cout << "OFF" << std::endl;
    std::this_thread::sleep_for(500ms);
  }

  return 0 ;
}
  1. 创建包含我们程序的构建规则的CMakeLists.txt文件:
cmake_minimum_required(VERSION 3.5.1)
project(gpiod)
add_executable(gpiod gpiod.cpp)
target_link_libraries(gpiod gpiodcxx)
  1. 按照http://wiringpi.com/examples/blink/WiringPI 示例部分中的说明将 LED 连接到树莓 PI 板。
  2. 设置到您的 Raspberry PI 板的 SSH 连接。 按照https://www.raspberrypi.org/documentation/remote-access/覆盆子 PI 文档部分的说明进行操作。
  3. 通过 SSH 将gpio文件夹的内容复制到 Raspberry PI 板。
  4. 安装libgpiod-dev包:
$ sudo apt-get install gpiod-dev
  1. 通过 SSH 登录主板,然后构建并运行应用:
$ cd gpiod && cmake . && make && sudo ./gpiod

您的应用应该会运行,并且您可以看到 LED 闪烁。

它是如何运作的..。

我们的应用使用一种新的、推荐的方式访问 Linux 中的 GPIO 设备。 因为它是最近才添加的,所以需要安装最新版本的 Raspbian 发行版buster

gpiod库本身提供高级包装器,以便使用ioctlAPI 与 GPIO 内核模块通信。 该接口是为 C 语言设计的,其上还有一层用于 C++ 绑定的附加层。 这一层位于libgpiocxx库中,该库与 C 的libgpiod库一起是libgpiod2包的一部分。

该库使用异常来报告错误,因此代码很简单,并且不会杂乱地检查返回代码。 而且,我们不需要费心释放捕获的资源;它是通过 C++ RAII 机制自动完成的。

当应用启动时,它创建类芯片的一个实例,该实例用作 GPIO 通信的入口点。 其构造函数接受要使用的设备的名称:

  gpiod::chip chip("gpiochip0");

接下来,我们创建该线的一个实例,该实例表示特定的 GPIO 管脚:

  auto line = chip.get_line(kLedPin);

请注意,与wiringPi实现不同,我们传递的是17管脚编号,因为libgpiod使用本机 Broadcom SOC 通道(BCM)管脚编号:

const int kLedPin = 17;

创建 LINE 实例后,我们需要配置所需的访问模式。 我们构造line_request结构的一个实例,传递消费者的名称("test")和一个常量,该常量指示管脚已配置为输出:

  line.request({"test",
                 gpiod::line_request::DIRECTION_OUTPUT, 
                 0}, 0);

之后,我们可以使用set_value方法更改引脚状态。 与wiringPi示例中一样,我们将500ms的管脚设置为1HIGH,然后将循环中的另一个500ms设置回0LOW

    line.set_value(1);
    std::cout << "ON" << std::endl;
    std::this_thread::sleep_for(500ms);
    line.set_value(0);
    std::cout << "OFF" << std::endl;
    std::this_thread::sleep_for(500ms);

该程序的输出与通过 GPIO 配方连接的控制设备的程序输出相同。 代码可能看起来更复杂,但新的 API 更通用,可以在任何 Linux 主板上运行,而不仅仅是 Raspberry PI。

还有更多的..。

有关libgpiod和 GPIO 接口的更多信息,请参见https://github.com/brgl/libgpiod

控制 I2C 外部设备

通过 GPIO 连接设备有一个缺点。 处理器可用于 GPIO 的管脚数量有限且相对较少。 当您需要使用大量设备或提供复杂功能的设备时,您可以很容易地用完引脚。

一种解决方案是使用其中一条标准串行总线连接外部设备。 其中之一是内部集成电路(I2C)。 这是广泛用于连接各种低速设备,因为它简单,因为一个设备可以只用主机控制器上的两条线连接。

该总线在硬件和软件层面都得到了很好的支持。 通过使用 I2C 外设,开发人员可以从用户空间应用控制它们,而无需编写复杂的设备驱动程序。

在本食谱中,我们将学习如何在覆盆子 PI 板上使用 I2C 设备。 我们将使用流行而廉价的液晶显示器。 它有 16 个针脚,这使得它很难直接连接到树莓板。 然而,使用 I2C 背包时,它只需要四根线就可以连接。

怎么做……

我们将创建一个应用,在连接到 Raspberry PI 板的 1602 LCD 显示屏上显示文本:

  1. 在您的~/test工作目录中,创建一个名为i2c的子目录。
  2. 使用您喜欢的文本编辑器在i2c子目录中创建一个i2c.cpp文件。
  3. 将以下include指令和常量定义放入i2c.cpp文件:
#include <thread>
#include <system_error>

#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>

using namespace std::literals::chrono_literals;

enum class Function : uint8_t {
  clear = 0x01,
  home = 0x02,
  entry_mode_set = 0x04,
  display_control = 0x08,
  cursor_shift = 0x10,
  fn_set = 0x20,
  set_ddram_addr = 0x80
};

constexpr int En = 0b00000100;
constexpr int Rs = 0b00000001;

constexpr int kDisplayOn = 0x04;
constexpr int kEntryLeft = 0x02;
constexpr int kTwoLine = 0x08;
constexpr int kBacklightOn = 0x08;
  1. 现在,我们定义了一个新类Lcd,它封装了显示控制逻辑。 我们从数据字段和public方法开始:
class Lcd {
  int fd;

  public:
    Lcd(const char* device, int address) {
      fd = open(device, O_RDWR);
      if (fd < 0) {
        throw std::system_error(errno,
            std::system_category(),
            "Failed to open RTC device");
      }
      if (ioctl(fd, I2C_SLAVE, address) < 0) {
        close(fd);
        throw std::system_error(errno,
            std::system_category(),
            "Failed to aquire bus address");
      }
      Init();
    }

    ~Lcd() {
      close(fd);
    }

    void Clear() {
      Call(Function::clear);
      std::this_thread::sleep_for(2000us);
    }

    void Display(const std::string& text,
                 bool second=false) {
      Call(Function::set_ddram_addr, second ? 0x40 : 0);
      for(char c : text) {
        Write(c, Rs);
      }
    }
  1. 它们之后是private方法。 首先使用低级帮助器方法:
private:

    void SendToI2C(uint8_t byte) {
 if (write(fd, &byte, 1) != 1) {
 throw std::system_error(errno,
 std::system_category(),
 "Write to i2c device failed");
 }
    }

    void SendToLcd(uint8_t value) {
      value |= kBacklightOn;
      SendToI2C(value);
      SendToI2C(value | En);
      std::this_thread::sleep_for(1us);
      SendToI2C(value & ~En);
      std::this_thread::sleep_for(50us);
    }

    void Write(uint8_t value, uint8_t mode=0) {
      SendToLcd((value & 0xF0) | mode);
      SendToLcd((value << 4) | mode);
    }
  1. 一旦定义了帮助器函数,我们将添加更高级别的方法:
    void Init() {
      // Switch to 4-bit mode
      for (int i = 0; i < 3; i++) {
        SendToLcd(0x30);
        std::this_thread::sleep_for(4500us);
      }
      SendToLcd(0x20);

      // Set display to two-line, 4 bit, 5x8 character mode
      Call(Function::fn_set, kTwoLine);
      Call(Function::display_control, kDisplayOn);
      Clear();
      Call(Function::entry_mode_set, kEntryLeft);
      Home();
    }

    void Call(Function function, uint8_t value=0) {
      Write((uint8_t)function | value);
    }

    void Home() {
      Call(Function::home);
      std::this_thread::sleep_for(2000us);
    }
};
  1. 添加使用Lcd类的main函数:
int main (int argc, char* argv[])
{
  Lcd lcd("/dev/i2c-1", 0x27);
  if (argc > 1) {
    lcd.Display(argv[1]);
    if (argc > 2) {
      lcd.Display(argv[2], true);
    }
  }
  return 0 ;
}
  1. 创建包含我们程序的构建规则的CMakeLists.txt文件:
cmake_minimum_required(VERSION 3.5.1)
project(i2c)
add_executable(i2c i2c.cpp)
  1. 根据下表将 1602LCD 显示屏的i2c背包上的针脚连接到 Raspberry PI 板上的针脚上:

| 覆盆子皮针名 | 物理引脚编号 | 1602 I2C 引脚 | | GND | 6. | GND | | + 5v | 2 个 | VSS | | SDA.1 | 3. | SDA | | SCL.1 | 5. | SCL |

  1. 设置到您的 Raspberry PI 板的 SSH 连接。 按照https://www.raspberrypi.org/documentation/remote-access/ssh/覆盆子 PI 文档部分的说明进行操作。
  2. 登录 Raspberry 板卡,运行raspi-config工具启用i2c
sudo raspi-config
  1. 在菜单中,选择接口选项|I2C|是。
  2. 重新启动主板以激活新设置。
  3. 通过 SSH 将i2c文件夹的内容复制到 Raspberry PI 板。
  4. 通过 SSH 登录主板,然后构建并运行应用:
$ cd i2c && cmake . && make && ./i2c Hello, world!

您的应用应该会运行,并且您可以看到 LED 闪烁。

它是如何运作的..。

在本方案中,我们的外部设备-LCD 屏幕-通过 I2C 总线连接到电路板。 它是串行接口的一种形式,因此连接只需要四条物理线路。 然而,LCD 屏幕可以做的远不止一个简单的 LED。 这意味着用于控制它的通信协议也更加复杂。

我们将只使用 1602 液晶屏提供的一小部分功能。 通信逻辑松散地基于 Arduino 的LiquidCrystal_I2C库,适用于 Raspberry Pi。

我们定义了一个Lcd类,它在其私有方法中隐藏了 I2C 通信的所有复杂性和 1602 控制协议的细节。 除了构造函数和析构函数外,它只公开了两个公共方法:ClearDisplay

在 Linux 中,我们通过设备文件与 I2C 设备通信。 要开始使用设备,我们需要使用常规的 OPEN 调用打开与 I2C 控制器对应的设备文件:

fd = open(device, O_RDWR);

可能有多个设备连接到同一总线。 我们需要选择要与之通信的设备。 我们使用ioctl调用来执行此操作:

if (ioctl(fd, I2C_SLAVE, address) < 0) {

此时,I2C 通信已配置完毕,我们可以通过将数据写入打开文件描述符来发出 I2C 命令。 然而,这些命令对于每个外部设备都是特定的。 因此,在通用 I2C 初始化之后,我们需要继续进行 LCD 初始化。

我们将所有特定于 LCD 的初始化放入Init私有函数中。 它配置操作模式、行数和显示字符的大小。 为此,我们定义了帮助器方法、数据类型和常量。

基本的辅助函数是SendToI2C。 这是一个简单的方法,将一个字节的数据写入为 I2C 通信配置的文件描述符中,并在出现错误时抛出异常:

      if (write(fd, &byte, 1) != 1) {
        throw std::system_error(errno,
            std::system_category(),
            "Write to i2c device failed");
      }

SendToI2C之上,我们定义了另一个帮助器方法SendToLcd。 它向 I2C 发送一个字节序列,形成 LCD 控制器可以解释的命令。 这包括设置不同的标志并处理数据块之间所需的延迟:

      SendToI2C(value);
      SendToI2C(value | En);
      std::this_thread::sleep_for(1us);
      SendToI2C(value & ~En);
      std::this_thread::sleep_for(50us);

LCD 在 4 位模式下工作,这意味着发送到显示器的每个字节都需要两个命令。 我们定义了Write方法来为我们做这件事:

      SendToLcd((value & 0xF0) | mode);
      SendToLcd((value << 4) | mode);

最后,我们定义了设备支持的所有可能的命令,并将它们放入Function枚举类中。 可以使用Call帮助器函数以类型安全的方式调用函数:

    void Call(Function function, uint8_t value=0) {
      Write((uint8_t)function | value);
    }

最后,我们使用这些助手函数定义公共方法来清除屏幕并显示字符串。

由于通信协议的所有复杂性都封装在Lcd类中,因此我们的main函数相对简单。

它创建类的一个实例,传入我们要使用的设备文件名和设备地址。 默认情况下,带 I2C 背包的 1620 LCD 具有0x27地址:

  Lcd lcd("/dev/i2c-1", 0x27);

Lcd类的构造函数执行所有初始化,实例一创建,我们就可以调用Display函数。 我们使用用户通过命令行参数传递的数据,而不是硬编码要显示的字符串。 第一个参数显示在第一行。 如果提供了第二个参数,它也会显示在显示屏的第二行中:

    lcd.Display(argv[1]);
    if (argc > 2) {
      lcd.Display(argv[2], true);
    }

我们的程序已经准备好了,我们可以把它复制到 Raspberry Pi 板上,然后在那里构建它。 但在运行之前,我们需要将显示器连线到电路板并启用 I2C 支持。

我们使用raspi-config工具使能 I2C。 我们只需执行一次,但除非 I2C 之前未启用,否则需要重新启动:

最后,我们可以运行我们的应用了。 它将在 LCD 显示屏上显示以下输出:

现在,我们知道如何从 Linux 用户空间程序控制通过 I2C 总线连接的设备。

还有更多的..。

有关使用 I2C 器件的更多信息,请参见与 I2C 器件接口页面,该页面位于https://elinux.org/Interfacing_with_I2C_Devices