树莓派和-Arduino-机器人入门手册-全-

树莓派和 Arduino 机器人入门手册(全)

原文:Beginning Robotics with Raspberry Pi and Arduino Using Python and OpenCV

协议:CC BY-NC-SA 4.0

一、机器人学导论

机器人这个词可以有很多含义。对某些人来说,它是任何自己移动的东西;运动艺术是机器人技术。对其他人来说,机器人意味着可移动的东西或可以自己从一个地方移动到另一个地方的东西。实际上有一个领域叫做移动机器人学;自动吸尘器,如 Roomba 或 Neato,就属于这一类。对我来说,机器人学介于运动艺术和移动机器人学之间。

机器人是一种应用逻辑以自动方式执行任务的技术。这是一个相当宽泛的定义,但机器人学是一个相当宽泛的领域。它可以涵盖从儿童玩具到一些汽车的自动平行泊车功能的所有内容。在这本书里,我们制作了一个小型移动机器人。

你在本书中接触到的许多原则很容易转移到其他领域。事实上,我们将从头到尾经历制造一个机器人的整个过程。在本章的稍后部分,我将回顾我们将要构建的项目。到时候,我会提供一本书中用到的零件清单。这些部件包括传感器、驱动器、电机等等。欢迎您使用手头的任何东西,因为在很大程度上,我们在本书中讨论的所有内容都可以应用到其他项目中。

机器人基础

我喜欢告诉那些机器人新手,或者只是对机器人好奇的人,机器人由三个要素组成。

  • 收集数据的能力
  • 处理或处理收集到的数据的能力
  • 与环境互动的能力

在接下来的章节中,我们将应用这个原理来制作一个小型的移动机器人。我们将使用超声波测距仪和红外线传感器来收集环境数据。具体来说,我们将识别何时有要避开的物体,何时我们将开车离开桌子的边缘,以及桌子和我们将跟随的线之间的对比。一旦我们有了这些数据,我们将应用逻辑来确定适当的响应。

我们将在 Linux 环境中使用 Python 来处理信息,并向我们的电机发送命令。我选择 Python 作为编程语言,因为它很容易学习,而且你不必有复杂的开发环境来构建一些非常复杂的应用。

我们与环境的互动将只是控制马达的速度和方向。这将允许我们的机器人在桌子或地板上自由移动。驾驶汽车真的没什么难的。我们将研究两种实现方式:一种是为 Raspberry Pi 设计的电机驱动器,另一种是通用的电机控制器。

这本书旨在具有挑战性。我涵盖了一些相当复杂的材料,而且做得很快。我没有办法对这些话题中的任何一个提供详细的报道,但是我希望在本书结束的时候让你看到一个功能机器人。在每一章中,我都试图为您提供更多的资源来跟进所讨论的主题。你有时会挣扎;我过去是这样,现在也经常这样。

不是每个人都对所有的科目感兴趣。期望你能在本书之外拓展你最感兴趣的领域。坚持有回报。

在书的最后,我增加了一点挑战。在第九章中,我们开始利用树莓派的真正力量。我们看计算机视觉。具体来说,我们看一个叫做 OpenCV (CV 代表计算机视觉)的开源包。这是一个常见的和非常强大的实用程序集合,使处理图像和视频流变得非常容易。它也是在最新版本的树莓派基础上构建的六小时版本。为了让事情变得简单一些,节省更多的时间,我可以下载一个已经安装了 OpenCV 的操作系统版本。我将在第二章中详细讨论这一点。

Linux 和机器人技术

Linux 是基于 Unix 的操作系统。它非常受程序员和计算机科学家的欢迎,因为它简单明了。他们似乎很喜欢终端基于文本的界面。然而,对于包括我在内的许多其他人来说,Linux 可能非常具有挑战性。那么,我为什么要选择这个环境来写一本机器人导论的书呢?这个问题有三个答案。

首先,当你从事机器人工作时,你最终不得不面对 Linux。那只是事实。您可以不用输入一个sudo命令就做很多事情,但是您的能力有限。sudo命令代表 Linux 中的超级用户 do。这告诉操作系统,您将要执行一个受保护的功能,该功能需要比一般用户更多的访问权限。当我们开始使用树莓派时,你会了解到更多。

第二,Linux 具有挑战性。正如我之前所说的,这本书会挑战你。如果你之前在 Linux 工作过,那么这个理由对你不适用。然而,如果你不熟悉 Linux,Raspberry Pi,或者在命令行中工作,那么我们做的一些事情将会很有挑战性。这很好。你正在学习新的东西,这应该是一个挑战。

第三,也是最重要的一点,Raspberry Pi 使用 Linux。是的,您可以在 Pi 上安装其他操作系统,但是它是为使用 Linux 设计的。事实上,Raspberry Pi 有它自己的 Linux 风格,叫做 Raspbian。这是推荐的操作系统,所以我们将使用它。使用预构建的操作系统的好处之一,除了它的易用性,是许多工具已经安装并准备好了。

由于我们使用的是 Linux,我们将广泛使用命令行指令。这是大多数新用户遇到问题的地方。命令行代码是通过终端输入的。Raspbian 有一个我们将使用的 Windows 风格的界面,但它的大部分使用终端。图形用户界面(GUI)中有一个终端窗口,因此我们将使用它。然而,当我们设置 Pi 时,我们将默认设置它引导到终端模式。进入 GUI 只是一个简单的startx命令。所有这些都包含在第二章中。

传感器和 GPIO

GPIO 代表通用输入/输出。它表示设备的各种连接。Raspberry Pi 有很多 GPIO 选项:HDMI、USB、音频等等。然而,当我在本书中谈论 GPIO 时,我通常指的是 40 针 GPIO 接头。此接头提供了对大多数电路板功能的直接访问。我将在第二章中讨论这一点。

Arduino 也有 GPIO。事实上,有人可能会说 Arduino 完全是 GPIO。鉴于所有其他连接都允许您与 Arduino 核心的 AVR 芯片通信并为其供电,这与事实相差不远。

所有这些接头和 GPIO 连接都在那里,因此我们可以访问电路板外部的传感器。传感器是一种收集数据的设备。有许多不同类型的传感器,它们都有各自的用途。传感器可用于检测光线水平、与物体的距离、温度、速度等。特别是,我们将使用配有超声波测距仪和红外探测器的 GPIO 接头。

运动和控制

大多数机器人的定义都有一个共同点,那就是它需要能够移动。当然,你可以拥有一个实际上不动的机器人,但这种类型的设备通常被称为物联网。

有许多方法可以将运动添加到项目中。最常见的是使用马达。但是你也可以使用螺线管、空气或水压。我在第六章中详细讨论了电机。

虽然可以用 Raspberry Pi 或 Arduino 板直接驱动电机,但强烈建议不要这样做。电机消耗的电流往往超过电路板上的处理器所能处理的。相反,建议您使用电机控制器。像电机一样,电机控制器也有多种形式。我们将使用的电机控制板是通过 Raspberry Pi 的头部访问的。我还讨论了如何用 L298N 双电机控制器驱动电机。

树莓派和 Arduino

我们将使用 Raspberry Pi(见图 1-1 )结合 Arduino(见图 1-2 )作为我们机器人的处理平台。

A457480_1_En_1_Fig2_HTML.jpg

图 1-2

Arduino Uno

A457480_1_En_1_Fig1_HTML.jpg

图 1-1

Raspberry Pi 3 B+

Raspberry Pi 是一种单板计算机,大约有信用卡大小。尽管它的尺寸很小,但它是一个非常强大的设备。Pi 运行一个 Linux 版本,该版本被定制为在驱动它的 ARM 处理器上工作。这将许多功能放入一个小设备中,该设备很容易嵌入到像机器人这样的东西中。但是,尽管它是一台伟大的计算机,但也有一些地方它并不出色。一个领域是与外部设备接口。它可以与传感器和外部设备一起工作,但 Arduino 在这方面做得更好。

Arduino 是另一种易于获得和使用的小型处理设备。然而,与 Raspberry Pi 不同,它不具备完整操作系统的能力。它不是运行 ARM 这样的微处理器,而是使用一种不同类型的芯片,称为微控制器。不同之处在于,微控制器是专门设计来与传感器、电机、灯和各种设备进行交互的。它直接与这些外部设备交互。Pi 在到达设备所连接的引脚之前,需要经过多层处理。

通过结合 Raspberry Pi 和 Arduino,我们能够利用各自的优势。Raspberry Pi 提供了完整计算机的高级处理能力。Arduino 提供对外部设备的原始控制。Pi 允许我们处理来自简单 USB 摄像机的视频流;而 Arduino 允许我们从各种传感器收集信息,并应用逻辑来理解所有数据,然后将简明的结果返回给 Pi。

在第二章你会学到更多关于树莓派的知识。稍后,您将把 Arduino 连接到 Pi,并学习如何对它进行编程,以及如何在 Arduino 和 Pi 之间来回传递信息。

项目概述

在本书中,我们将构建一个小型移动机器人。这个机器人被设计用来演示你在每一章中学到的课程。然而,在我们实际建造机器人之前,我们需要涵盖大量的材料,并为未来的课程奠定基础。

机器人

我们将要建造的机器人是一个小型的两轮或四轮自主漫游车。它将能够探测障碍物和桌子的边缘,并跟随一条线。我选择的底盘是一个四轮机器人,但也有适合这个项目的其他设计(见图 1-3 和 1-4 )。

A457480_1_En_1_Fig4_HTML.jpg

图 1-4

The back of our robot shows the Raspberry Pi and motor control board

A457480_1_En_1_Fig3_HTML.jpg

图 1-3

The front of our robot shows the ultrasonic sensors and Pi T Cobbler on a breadboard

虽然我提供了一份我在这个项目中使用的零件的清单,但是你可以随意使用。重要的是,他们的行为方式与我所列举的方式相似。

材料清单

在很大程度上,我试图让材料列表尽可能通用。有几项是特定于供应商的。我选择它们是因为它们提供了很多功能和便利。DC &步进电机控制器和圆饼匠来自一家名为 Adafruit 的在线零售商,这是一个零件、教程和灵感的绝佳资源。底盘套件来自一家名为 ServoCity 的在线零售商,该零售商为机器人生产许多机械零件。

以下是我们在本书中使用的特殊零件(如图 1-5 所示):

A457480_1_En_1_Fig5_HTML.jpg

图 1-5

Runt Rover chassis parts and the Pi T Cobbler, ribbon cable, motor control hat, and extended header

  • 来自 ServoCity.com 的小型机器人底盘
  • 阿达果 DC &树莓派步进电机帽–迷你套件 PID: 2348
  • Pi A+/B+/Pi 2/Pi 3 的 GPIO 堆叠接头–超长 2×20 引脚 PID: 2223(允许使用额外的板和补鞋匠来连接到试验板)
  • 组装的 Pi T-Cobbler Plus–GPIO 分线点–Pi A+、B+、Pi 2、Pi 3、零 PID: 2028

以下零件(如图 1-6 所示)相当普通,可以从大多数供应商处购买:

  • raspberry Pi 3–型号 B–arm V8,带 1G RAM

  • Arduino 一号

  • 4 × AA 电池盒,带开/关开关(为电机供电)

  • USB 电池组–2200 mAh 容量–5V 1A 输出 PID: 1959(为 Raspberry Pi 供电)

  • 半尺寸试验板

  • 超声波传感器–HC-sr04 您可能想要几个这样的传感器。你会发现,超声波传感器在角度上是不可靠的,有一组这样的传感器是很好的。我在大部分项目中至少使用三种。

  • 一组跳线(见图 1-7 )您需要公对公跳线和公对母跳线。这是一个好主意,让他们在一些颜色。黑色和红色用于为您的设备供电。其他颜色的集合可以帮助你理解你的电路。幸运的是,你可以用彩色带状电缆制作各种类型的跳线。

  • Arduino 的 USB 电缆

  • 一根微型 USB 线,用于您的树莓派

  • 一个普通的 USB 手机充电器,最好是现代智能手机或平板电脑的充电器,可以提供 2 安培的电源

  • HDMI 电视或电脑显示器大多数电脑显示器上没有 HDMI 端口。不过,您可以购买 HDMI-DVI 转换器,以便使用现有的显示器。

  • USB 键盘和鼠标(我喜欢罗技 K400 无线键盘和触摸板的组合,但有无数的选择)

  • 联网的计算机

  • Wi-Fi or Ethernet cable for the Pi

    A457480_1_En_1_Fig7_HTML.jpg

    图 1-7

    Jumpers in ribbon cable form. Pull off what you need

    A457480_1_En_1_Fig6_HTML.jpg

    图 1-6

    Common parts: Raspberry Pi, Arduino Uno, ultrasonic sensor, battery holder, and breadboard

你不需要对显示器和键盘着迷。一旦你阅读了第二章,在那里我们安装和配置了 Raspberry Pi,你不再需要它们了。我有几个无线键盘,因为我通常会同时进行几个项目。对于显示器,我只需使用一台带有 HDMI-DVI 适配器的电脑显示器。

如果您没有使用包含电机和车轮的底盘套件,您还需要以下零件(参见图 1-8 ):

A457480_1_En_1_Fig8_HTML.jpg

图 1-8

DC geared motor and wheels

  • 业余齿轮马达-200 转/分(一对)
  • 车轮–65 毫米(橡胶轮胎,一对)

如果您不想使用 Adafruit 电机和步进器,您也可以使用几乎任何电机控制器,尽管每个控制器都有不同的接口和代码。一个常见且相当受欢迎的选项是 L298N 双电机控制器(见图 1-9 )。

A457480_1_En_1_Fig9_HTML.jpg

图 1-9

The L298N dual motor controller module comes in numerous varieties, but essentially work the same

我还保留了一些其他的供应品,因为它们几乎在每个项目中都要用到。第七章,我们组装机器人;你还需要双面胶、4 英寸的拉链和自粘 Velcro。随着你继续学习机器人技术,你会发现自己会经常接触到这些东西。事实上,你可能想储备各种尺寸的拉链。相信我。

摘要

机器人入门并不困难。然而,这很有挑战性。如果你想在这个领域取得成功,这本书介绍了一些你需要发展的技能。我们制造的机器人向你介绍树莓派、Linux、Arduino、传感器和计算机视觉。这些技能很容易扩展到更大的机器人和其他类似的项目。

二、树莓派简介

这本书的目的是挑战你建立一个简单的机器人,它将随着时间的推移而扩展。这本书被认为是难的;然而,这并不太困难或不必要的复杂。在这个过程中,你会经历很多复杂的事情,但是在你的 Raspberry Pi 上安装操作系统并不是其中之一。

下载并安装 Raspbian

本质上,在您的 Pi 上安装操作系统(OS)有两种方法。

第一步包括下载最新的 Raspbian 映像,将其写入 SD 卡,然后从那里开始。这种方法需要安装第三方软件包,该软件包在 SD 卡上写入可引导映像。好处是它在你的 SD 卡上占用的空间更少。如果你使用最低 8GB 的 SD 卡,这可能会有所帮助;如果你变得更大,那么这个考虑是没有意义的。

尽管直接安装并不复杂(实际上相当简单),但有一种更简单的方法,不需要在系统上安装额外的软件。NOOBS(新的开箱即用软件)旨在使您的树莓派 的安装和配置更容易。它允许您从多个操作系统中进行选择,并简单地安装。然而,NOOBS 包仍然在 SD 卡上,并吃掉宝贵的空间。它确实允许您返回并修复您的操作系统或完全更改操作系统,但这可以非常容易地手动处理。

最终,选择权在你。我将介绍这两个选项,以便您可以选择最适合您的安装路径。无论您选择哪个选项,您的旅程都是从 www.raspberrypi.org/downloads/ 的 Raspbian 下载页面开始的(见图 2-1 )。

A457480_1_En_2_Fig1_HTML.jpg

图 2-1

Raspbian download screen

使用 OpenCV 的 Raspbian

在这本书的结尾,我们将与计算机视觉一起向你展示为什么你应该使用一个树莓派 而不是一个能力较差的平台。然而,为了做到这一点,您需要在您的 Pi 上安装 OpenCV。不幸的是,对于 Raspberry Pi 没有简单的 OpenCV 安装程序。因为 Pi 运行在 ARM 处理器上,所以软件包必须从源代码编译,这是一个六小时的过程。

为了方便起见,我在 Raspbian Jesse 中预编译了 OpenCV,并在 https://github.com/jcicolani/Jesse-OpenCV 创建了一个可下载的映像。

您仍然需要完成安装和配置过程来定制安装。该映像包含您需要更改的默认设置(除了构建时需要的一些例外)。

“艰难”的方式

更困难的方法是将 Raspbian 操作系统映像直接安装在 SD 卡上——准备启动。这是我使用的方法,因为它并不比前面的方法更复杂,而且它允许我使用 NOOBS 没有的版本。

对于 Raspbian 安装,您有两种选择。Jessie 是操作系统的最新稳定版本;这是我们将要使用的。第一个选项是 Raspbian Jessie 和 PIXEL——他们新的优化 GUI。这是一个 1.5GB 的下载,解压缩后是一个 4.2GB 的图像。第二个选项是 Raspbian Jessie Lite,这是一个最小的映像,下载量小得多,只有 300MB(解压缩后只有 1.4GB)。然而,最小意味着没有 GUI,所以一切都是通过命令行完成的。如果你是无头 Linux 的粉丝,那么这是你的选择。我们将使用更大的像素安装。

如果安装了 BitTorrent 客户端,请单击“下载 Torrent”。这比下载。zip 文件。

  1. 导航到 www.raspberrypi.org/downloads/

  2. 单击 Raspbian 图像。

  3. 选择您想要安装的 Raspbian 版本。

  4. 下载完成后,将文件解压到你容易找到的地方。

  5. 下载并安装 Win32 磁盘映像器。这允许您将刚刚下载的图像文件写入 micro SD 卡。可以在 https://sourceforge.net/projects/win32diskimager/ 获得。

  6. 或者,您可能还想下载 SDFormatter,以确保您的 SD 卡已准备妥当。可以在 www.sdcard.org/downloads/formatter_4/ 获得。

  7. 将您的 micro SD 卡插入连接到计算机的读卡器。

  8. If you have downloaded and installed SDFormatter, open it. You should see a dialog box similar to the one shown in Figure 2-2.

    A457480_1_En_2_Fig2_HTML.jpg

    图 2-2

    SD Card Formatter

  9. 确保您选择的驱动器代表您的 SD 卡。你将要格式化它,所以如果你选择了错误的东西,它会清除你在那个驱动器上的所有东西。工具通常默认选择正确的,但是要仔细检查。明智的做法是断开任何其他外部存储设备。

  10. 确保“格式大小调整”设置为“开”。这将删除卡上的任何其他分区,并使用整个分区。将所有其他设置保留为默认值。

  11. 单击开始。该过程完成后,您就可以安装操作系统了。

  12. 要将图像刷新到 SD 卡,请打开 Win32 Disk Imager。

  13. 在“图像文件”字段中,选择您下载的 Raspbian 图像。您可以点击文件夹图标来浏览它。

  14. 确保在设备下拉框中选择了您的 SD 卡。同样,选择错误的设备可能会导致世界的伤害;所以要注意。

  15. 单击写入。

  16. 该过程完成后,从读卡器中取出卡。

  17. 将卡插入 Raspberry Pi 上的 micro SD 读卡器。

这听起来很长,但是做起来非常快速和容易。接下来,我们来看一下 NOOBS 的安装过程。

“简单”的方法

我把这个方法称为“简单”的方法,尽管困难的方法实际上很简单。让这变得容易的是,你不必直接写图像。你可能想要格式化卡片,但是如果是一张新卡,那就没有必要了。更简单的是,如果你购买的 Pi 是入门套件的一部分,那么它可能已经在 micro SD 卡上安装了 NOOBS。如果是这种情况,可以跳过前几步。

你有两个选择:NOOBS 和 NOOBS 建兴。NOOBS 在下载中包含了 Raspbian 的图像,所以一旦它存在你的 SD 卡上,你就不需要连接到网络来下载任何东西了。如果你愿意,你可以选择另一个操作系统,但是你需要把你的 Pi 连接到网络上,这样 NOOBS 才能下载它。NOOBS 建兴不包括完整的拉斯扁形象。出于我们的目的,选择标准 NOOBS 安装。

  1. 单击下载页面上的 NOOBS 图像。
  2. 选择你的 NOOBS 风味。如果安装了 BitTorrent 客户端,请单击“下载 Torrent”。这比下载。zip 文件。
  3. 或者,您可能还想下载 SDFormatter,以确保您的 SD 卡已准备妥当。可以在 www.sdcard.org/downloads/formatter_4/ 获得。
  4. 如果您下载并安装了 SDFormatter,请将其打开。
  5. 确保您选择的驱动器代表您的 SD 卡。你将要格式化它,所以如果你选择了错误的东西,它会清除你在那个驱动器上的所有东西。工具通常默认选择正确的,但是要仔细检查。明智的做法是断开任何其他外部存储设备。
  6. 确保“格式大小调整”设置为“开”。这将删除卡上的任何其他分区,并使用整个分区。将所有其他设置保留为默认值。
  7. 单击开始。该过程完成后,您就可以安装操作系统了。
  8. 将 NOOBS 文件直接解压到 SD 卡上。
  9. 从读卡器中取出卡。
  10. 将卡插入 Raspberry Pi 上的 micro SD 读卡器。
  11. 此时,您需要连接您的 Pi 以继续。所以,请跳到本章的“连接树莓派”一节。完成这些步骤后,请返回本部分继续设置。
  12. 当你连接电源到树莓派,它启动到 NOOBS 安装屏幕。如果你使用 NOOBS 建兴,你有你的操作系统的选择。如果您使用标准的 NOOBS 下载,您唯一的选择是 Raspbian(这是可以的,因为这是我们正在使用的)。
  13. 单击“Raspbian”以确保它已被选中。还要确保在屏幕底部选择了正确的语言(在我的例子中,是英语(美国))。
  14. 单击屏幕顶部的安装按钮。

安装可能需要一点时间,所以去喝杯咖啡吧。

连接树莓派

现在你的微型 SD 卡已经准备好了,你需要连接上你的树莓派。如果你使用的是原始的第一代 Pi,这就有点复杂了。

然而,第一代之后的每个型号都包括多个 USB 端口和一个 HDMI 连接器,使事情变得更简单。连接 Pi 非常简单。

  1. 通过 HDMI 电缆连接显示器。如果您使用的是配有分量连接而不是 HDMI 的小型电视机,Pi 上的音频插孔是一个四极分量插孔。你需要一个 RCA 到 3.5 毫米的转换器,通常是电缆形式,来做到这一点。
  2. 将键盘和鼠标连接到 USB 端口。我使用无线键盘/触摸板组合,因为它小巧便携。
  3. 确保您的带有 Raspbian 或 NOOBS 的 micro SD 卡安装在 Pi 上的 micro SD 端口中。本质上,这是你的小型电脑的硬盘,所以它必须在正确的位置。它不会通过连接到其中一个 USB 端口的 SD 读卡器读取操作系统。
  4. 如果您使用以太网电缆,请将其连接到以太网端口。您也可以将 Wi-Fi 加密狗插入 USB 端口。如果你和我一样用的是 Pi 3,Wi-Fi 是内置的。
  5. 将 5V 电源连接到微型 USB 端口。此端口仅用于电源。您不能通过 USB 访问主板。

就是这样。你的树莓派 应该看起来类似于图 2-3 所示。Pi 应该在您的显示器上启动。如果您正在安装 NOOBS,请返回 Noobian 安装的步骤 10 以完成安装过程。

A457480_1_En_2_Fig3_HTML.jpg

图 2-3

Raspberry Pi connections

现在您已经连接并启动,您需要登录。以下是 Raspbian 安装的默认凭据:

  • 用户名:pi
  • 密码:树莓

当然,默认的用户名和密码从来都不安全。因此,为了防止你的网络安全朋友带着你的机器人逃跑,我们要做的第一件事就是更改密码。在稍后的配置中,我们将更改默认用户名。

配置您的 Pi

现在我们已经完成了初始安装,接下来我们将进行一些定制。根据您的特定用途,Pi 有几个您可以启用的功能。最初,它们无法减少运行操作系统所需的一些开销。我们将要实现的配置设置是为了安全和方便。

使用 raspi-config

为了进行定制,Raspberry Pi Foundation 的优秀人员提供了一个名为 raspi-config 的实用程序。使用它需要命令行终端。现在只输入了一条命令,但是随着我们在研讨会中的深入,您会对终端窗口更加熟悉。如果你是 Linux 新手(Raspbian 是基于 Linux 的),这可能有点吓人。不需要这样,我会尽力让你适应。但你必须学会如何应对。

你可以在 www.raspberrypi.org/documentation/configuration/raspi-config.md 找到更多关于 raspi-config 实用程序的信息。

此时,您应该已经启动了您的 Raspberry Pi。如果没有,现在就做。

我们将做几件事来配置 Pi,首先扩展文件系统以利用整个 SD 卡。默认情况下,Raspbian 不使用整个 SD 卡,所以我们想告诉它。如果您正在使用 NOOBS,这已经为您完成,所以您可以跳过这一步。

  1. 单击屏幕顶部的 Raspberry Pi 图标。这将打开一个应用列表。

  2. Select Accessories ➤ Terminal, as shown in Figure 2-4. When opened, the terminal window is displayed (see Figure 2-5).

    A457480_1_En_2_Fig5_HTML.jpg

    图 2-5

    Terminal window

    A457480_1_En_2_Fig4_HTML.jpg

    图 2-4

    Terminal selection from the applications list. The terminal icon is also on the quick access bar.

  3. Type sudo raspi-config. This opens the Raspberry Pi Software Configuration Tool, as shown in Figure 2-6.

    A457480_1_En_2_Fig6_HTML.jpg

    图 2-6

    The raspi-config screen. Most OS-level options can be set here, including activating and deactivating services. Newer versions of Raspbian automatically expand your file system the first time you start the Pi. Unless you are using an older version of Raspbian, you should be able to skip this next step and move on to changing the password.

  4. 确保突出显示扩展文件系统。

  5. 按回车键。系统弹出一条关于扩展文件系统的消息,要求您重新启动。(我们将在完成大部分更改后重启。)接下来,我们将更改用户密码。

  6. 确保突出显示“更改用户密码”。

  7. 按回车键。系统显示一条消息,提示您输入新密码。

  8. 按回车键。这将使您进入终端,输入新密码。

  9. 输入您的新密码,然后按 Enter 键。

  10. Confirm your new password and press Enter. This displays a confirmation that the password was successfully updated (see Figure 2-7).

![A457480_1_En_2_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-cv-zh/raw/master/docs/begin-bot-raspi-arduino/img/A457480_1_En_2_Fig7_HTML.jpg)2-7

A password change confirmation in raspi-config  
  1. 按回车键。接下来的几个步骤将激活我们稍后会用到的一些服务。首先,我们将把您的 Pi 的主机名改为在网络上更容易找到的唯一名称。当你和另外 20 个树莓派共处一室时,这变得尤为重要。
  2. Make sure that advanced options is highlighted, and then press Enter. This displays the interface and other options (see Figure 2-8).
![A457480_1_En_2_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-cv-zh/raw/master/docs/begin-bot-raspi-arduino/img/A457480_1_En_2_Fig8_HTML.jpg)
2-8

raspi-config advanced options. Hostname and service activation is accessed here. The hostname is how your Raspberry Pi appears on the network. You’ll want to give your Pi a unique name, especially when you consider how many of them may be on the network at any given time. The hostname should be both meaningful to the application and unique.  
  1. 突出显示主机名,然后按回车键。
  2. 一个对话框解释了主机名的要求。它只能是字母数字字符:没有符号,没有连字符,没有下划线。按回车键继续。
  3. 输入您的新主机名,然后按 Enter 键。SSH 允许我们从另一台计算机通过终端窗口(SSH 客户端)访问 Pi。在 Windows 上,PuTTY 是一个非常流行的免费 SSH 客户端。SSH 不提供 GUI。所有交互都是使用终端命令完成的。如果您想要快速执行程序、安装软件等等,这是很有帮助的。随着您对终端越来越熟悉,您可能会发现自己使用 SSH 来连接简单的命令,而将 VNC(远程桌面)留给更复杂的任务,如编写程序。
  4. 返回高级选项菜单。
  5. 选择启用 SSH 并按回车键。
  6. 确认您想要启用 SSH,然后按 Enter 键。
  7. 再次按回车键返回菜单。I2C 是一种串行通信协议,在 Pi、Arduino 等嵌入式系统中非常流行。通过使用多个引脚,它可以与多个器件进行稳定的通信。我们将使用的电机控制板通过 I2C 进行通信。(如果你后来选择添加其他板,如伺服控制板,它也将使用 I2C。)只要设备有不同的地址,就可以一直堆叠。
  8. 返回高级选项菜单。
  9. 选择启用 I2C 并按回车键。
  10. 确认您要启用 SSH,然后按 Enter。
  11. 再次按回车键返回菜单。因为我们还计划使用 Raspberry Pi headless(没有连接显示器、键盘或鼠标),所以让我们将它设置为自动引导到控制台。不用担心;正如您将看到的,当您想启动桌面 GUI 时,它是非常容易的。
  12. 转到启动选项,然后按回车键。
  13. 选择控制台并按回车键。如果您相信只有您能直接访问您的 Pi,您可以选择控制台自动登录。自动登录不适用于远程会话,只能通过键盘和显示器直接访问。
  14. 更新所有设置后,突出显示 Finish 并按 Enter 键。
  15. Pi 会询问您是否要重新启动。选择是并按回车键。此时,您的 Pi 会重新启动。这可能需要几分钟,特别是如果您没有通过 NOOBS 安装,Pi 必须扩展您的文件系统。记住,我们设置 Pi 默认引导到控制台。因为接下来的几个步骤都是通过命令行完成的,所以我们不需要加载 GUI。然而,让我们无论如何做它以便你能看见它是多么容易。
  16. 键入startx并按回车键。

你现在在 GUI 桌面上。

要退出桌面,请执行以下操作

  1. 单击程序菜单(左上角的树莓图标)。
  2. 单击电源按钮。
  3. 选择退出到命令行。

您现在应该回到命令行了。

用户

Raspbian 的每个安装的默认用户是 pi。之前,我们更改了密码,使其更加安全。但是,您可能不希望总是以 pi 用户的身份登录。

记得我说过我们会更多地使用终端吗?好吧,现在开始。创建和管理用户最简单的方法是通过命令行。我们现在要走一遍这个过程。

保护根

除了默认用户 pi 之外,Pi 上还有另一个默认用户。这是根用户。根用户本质上是机器用来执行低级命令的管理用户。这个用户可以访问任何东西,可以做任何事情,因为它是一台机器。但是,与默认 pi 用户不同,root 没有默认密码。它没有密码。

因此,当我们为我们的机器人配置和保护计算机时,让我们给根用户一个密码。

  1. 打开终端窗口。
  2. 类型sudo passwd root(注意,passwd是正确的命令,而不是输入错误。)
  3. 输入 root 用户的新密码。
  4. 再次输入密码进行确认。

您的 root 用户现在是安全的,这很好,因为您将在配置的下一步需要它。

更改默认用户名

您要做的第一件事是将默认用户名更改为您选择的名称。这将把用户名 pi 替换为您自己的用户名。这在设备上提供了另一层安全性;现在,不仅有人需要找出密码,他们甚至没有默认的用户名。它还保留了默认用户被赋予的一些特殊的、未记录的权限。

  1. 注销 pi 用户。你可以通过菜单系统或者简单地在终端中输入logout来实现。

  2. 使用现在安全的 root 用户登录。

  3. 键入

    usermod -l <newname> pi
    
    

    <newname>是您选择的新用户名。命令中不要包含<>

  4. 要更新主目录名,请再次键入

    usermod -m -d /home/<newname> <newname>
    
    

    <newname>是您在上一步中使用的新用户名。

  5. 注销 root 用户,并使用您的新用户名重新登录。

此时,您已经更改了默认用户和 root 用户的默认用户凭据。您还更改了主机名。这是保护你的私人侦探和机器人的最低要求。

您的 Raspberry Pi 现在已经设置、配置好了,可以使用了。在我们进入下一章之前,我们还有一件事要做,那就是设置你的 Pi 为无头。

让一台机器“无头”仅仅意味着对它进行配置,这样你就不再需要连接显示器、键盘和鼠标来操作它。这通常通过两种方式实现:使用 KVM 交换机或设置远程访问。在移动机器人上,连接 KVM 并不是一个真正的选择。事实上,这与简单地将一切都连接到它没有什么不同。我们想要做的是设置 Pi,以便我们可以通过网络远程访问它。但首先,让我们确保您已连接到网络。

连接到无线网络

当您最初连接您的 Raspberry Pi 时,您可以选择连接以太网电缆。如果你这样做了,那么你已经在网络上了。但是,您仍然希望连接到无线网络。这允许你在机器人移动的时候遥控它。你可以向它发送命令,更新代码,甚至从第十章安装的网络摄像头观看视频。

若要连接到无线网络,您需要 Wi-Fi 连接。如果你使用的是 Raspberry Pi 3,那么你已经内置了一个;否则,你需要一个 Wi-Fi 加密狗,最好是可以通过 USB 端口供电的。这里的一点研究大有帮助。

  1. 通过键入startx登录 GUI 界面。

  2. Click the network icon at the top right of your screen. This icon looks like Figure 2-9.

    A457480_1_En_2_Fig9_HTML.jpg

    图 2-9

    Network connection icon

  3. 从列表中选择您的无线网络。

  4. 输入网络的安全密钥。

您现在应该已连接到无线网络。

变得没头了

在参加这些研讨会时,您不会想要携带额外的显示器、键盘和鼠标。为了让您的生活更加轻松,让我们设置一下,这样您就可以无头访问 Pi 了。

远程存取

有两种方法可以获得远程访问。一种方法是使用 SSH,它允许您使用终端客户端连接到远程设备。另一种方法是设置远程桌面。

使用 xrdp 的远程桌面

让我们从从另一台计算机远程访问桌面开始。以下说明适用于 Windows 用户。大多数现代的 Windows 安装都已经安装了远程桌面连接,这是我们在设置好 Pi 后用来连接它的。

让我们在 Pi 上安装几个服务:tightVNCserver 和 xrdp。理论上,xrdp 应该自己安装 VNC 服务器。事实上,并不是这样。此时,您应该在 Pi 的命令行上。

  1. 类型sudo apt-get install tightvncserver

  2. 完成安装。

  3. 类型sudo apt-get install xrdp。安装完成后,您就可以开始工作了。若要连接,请执行以下操作。

  4. 在 Pi 上,键入sudo ifconfig

  5. 如果您使用以太网电缆,请记下 eth0 块中的互联网地址(inet addr ),如果使用 Wi-Fi,请记下 wlan0 块中的互联网地址。

  6. On your laptop, open Remote Desktop Connection. This displays the connection dialog box, as shown in Figure 2-10.

    A457480_1_En_2_Fig10_HTML.jpg

    图 2-10

    Windows Remote Desktop Connection

  7. 从您的 Pi 输入 inet 地址。

  8. Click Connect. You should see the remote desktop screen with the xrdp login form (see Figure 2-11).

    A457480_1_En_2_Fig11_HTML.jpg

    图 2-11

    XRDP remote desktop login screen

  9. 输入您的用户名和密码。

  10. Click OK. This opens the desktop from your Pi (see Figure 2-12).

![A457480_1_En_2_Fig12_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-cv-zh/raw/master/docs/begin-bot-raspi-arduino/img/A457480_1_En_2_Fig12_HTML.jpg)2-12

Default Raspbian desktop viewed through a remote desktop session  

只要您的 Pi 的 IP 地址不变,您就不再需要键盘、鼠标或显示器来使用您的 Pi。

带 PuTTY 的 SSH

最常见的 SSH 客户端可能是 PuTTY。免费使用,可从 www.chiark.greenend.org.uk/~sgtatham/putty/download.html 下载。

您为 PuTTY 下载的文件是一个可执行文件,不需要安装。把它放在你的桌面或容易找到的地方。若要连接,请执行以下操作。

  1. Open the PuTTY interface (see Figure 2-13).

    A457480_1_En_2_Fig13_HTML.jpg

    图 2-13

    PuTTY configuration window

  2. 输入您的 Raspberry Pi 的 IP 地址。

  3. 单击打开。

  4. You will likely get a security warning , as shown in Figure 2-14, but we know that this is the proper connection, so click Yes.

    A457480_1_En_2_Fig14_HTML.jpg

    图 2-14

    Security warning on first SSH connection with PuTTY A terminal window opens, asking for your username and password.

  5. 输入您的用户名和密码。你现在应该看到终端提示,如图 2-15 所示。

就是这样。您现在通过 SSH 连接到您的 Raspberry Pi。你可以同时拥有多个连接,但是不要超过你所需要的。当你使用像机器人操作系统(ROS)这样的东西时,多重连接是很方便的。(别担心,还有一段路呢。)ROS 通过终端运行多个程序。每一个都需要自己的终端窗口。使用 PuTTY,您可以根据需要拥有任意多的远程终端连接。

A457480_1_En_2_Fig15_HTML.jpg

图 2-15

Open SSH connection

在网络上查找您的设备

要远程访问您的 Pi,您需要知道它在网络上的 IP 地址。通常,网络交换机从一个会话到另一个会话保留设备的 IP;然而,这并不能保证。

在网络上找到你的设备的 IP 地址可能会很棘手。如果您在家,并且可以访问路由器的管理面板,这可能是找到您的设备的最直接的方法。只需登录您的路由器,找到连接的设备列表,向下滚动,直到找到您的 Raspberry Pi 的主机名。

如果你需要找到 IP 地址,但不在家,有几种方法可以做到这一点。最简单的是在手机上使用 Nmap 应用。我在安卓手机上用一个叫 Fing 的应用。一旦手机连接到本地 Wi-Fi 网络,该应用就会扫描网络,并列出该网络上的所有其他设备。您可以向下滚动列表,直到找到您的主机名。

如果网络对您来说是陌生的,您的 Raspberry Pi 不会自动连接到它。这种情况让事情变得有点棘手。为了方便起见,出门前要做好准备。我是 Windows 用户;如果不是,您需要为您的操作系统查找正确的过程。我在旅行时用我的笔记本电脑做这个操作。它允许我远程进入 Pi 足够长的时间来连接到本地 Wi-Fi 并获得 wlan0 IP 地址。

请记住,IP 是由笔记本电脑分配的。您在此操作结束时获得的 IP 可能无法在任何其他机器上工作。

在 Windows 7 或更高版本的机器上,您可以执行以下步骤来直接远程访问您的 Pi 以获取其 IP 地址。如果您需要直接连接到 Pi 来建立新的 Wi-Fi 连接,请记下 IP 地址。你需要一根短的以太网电缆,它应该放在你的工具箱里。

确定您能够在设置了显示器、键盘和鼠标的情况下,或者通过 Wi-Fi 网络的远程连接来查看您的 Raspberry Pi。Pi 不能通过以太网电缆连接到网络,因为此操作需要该端口。

  1. 将以太网电缆连接到您的笔记本电脑。

  2. 将另一端连接到 Pi 上的以太网端口。

  3. 在 Pi 上打开一个终端窗口。

  4. 类型sudo ifconfig

  5. 找到 eth0 模块中的 inet 地址。

  6. 打开笔记本电脑上的终端窗口。您可以通过在开始菜单中搜索cmd来完成此操作。

  7. 在 Windows 终端中键入以下内容:

    ping <your.Pis.IP.address>
    
    

    <your.Pis.IP.address>是来自 Pi 的 eth0 IP 地址。

  8. 打开笔记本电脑上的远程桌面连接。

  9. 从 Pi 输入 IP 地址,然后按 Enter 键。

现在,您应该可以从笔记本电脑直接远程连接到 Raspberry Pi。请确保保存此 IP 地址,以便以后可以找到它。远程桌面连接应该记住它,但最好也将它保存在其他地方。

现在,无论您何时尝试连接到新的 Wi-Fi 网络,您都可以使用以太网电缆直接从笔记本电脑远程连接到您的 Pi。远程登录后,只需从可用网络列表中选择网络,然后输入密码(如果有)。

摘要

树莓派是为业余爱好者设计的。小型 Linux 计算机使它对许多不同类型的项目非常有用,但这意味着你需要学习一点 Linux。Raspberry Pi 基金会的开发人员提供了一个简单易用的 Debian Linux 版本,名为 Raspbian。

我们通过配置远程访问将基本安装向前推进了一步。这使你可以通过网络远程访问你的机器人,这意味着不再需要显示器和键盘。

三、Python 速成课

这本书的目的是挑战你建立一个简单的机器人,并随着时间的推移而扩展。这是有意为难的。它旨在提供一种实践经验,帮助你克服学习机器人最困难的部分:被技术吓倒。我将教你一些机器人的基础知识,就像我学习游泳一样——被扔进深水区,同时有更有经验的人在旁边看着,确保你不会淹死。

因此,我希望你能吸取你的经验,并通过自己的学习加以补充。我会带你去正确的方向,但不会有很多牵手。你必须自己填补空白,学习一些细节——尤其是一些特定应用的细节。

这篇 Python 入门也不例外。我将向您展示如何安装工具,使用编辑器,并编写一些简单的程序。我们将快速浏览程序结构、语法和格式问题、数据类型和变量,并直接进入 Python 的控制结构和一些面向对象的方面。如果这些听起来像是技术术语,不要担心,在本章结束之前你会明白的。

在这一章的最后,我不指望你能自己写程序。我希望你知道如何写代码,如何使用编辑器,如何编译和执行程序。最重要的是,你应该能够看别人的代码并且能够阅读它,对他们试图做的事情有一个基本的理解,并且能够识别构建模块。剖析他人的代码对快速学习很重要。这本书的一个前提是不要多此一举。你要做的大部分事情以前已经有人做过了,如果你稍微搜索一下就能找到。能够阅读和理解你的发现将有助于你达到目标。

就资源而言,这里有一些建议:

  • 社区对 Python 的支持非常好。Python 网站是学习和发展 Python 的宝贵资源。特别是,请务必查看 www.python.org/about/gettingstarted/ 的初学者页面。下一节我们将从这里开始。
  • 给自己找一本或两本关于 Python 的好书。这本书让你开始,但有很多细节不会涵盖。寻找关于 Python 中的设计模式和构建算法的不同方法的书籍,以使您的代码尽可能高效。找一些对你的申请有深入了解的书籍。
  • 不要认为你必须自己学习 Python,或者本书中的任何其他主题。外面有一个巨大的社区。寻找当地聚会、俱乐部和课程。找到你当地的黑客空间。我保证你会在那里找到能够帮助你的人。

Python 概述

Python 是一种高级编程语言,由吉多·范·罗苏姆在 20 世纪 80 年代末创建。它已经成为一种非常流行的通用语言,因为它灵活,相对容易学习和编码。在许多方面,Python 是一种非常宽容的语言,这有助于它的易用性。正如你将在本章后面看到的,Python 以一种对编程新手来说非常直观的方式管理数据。因此,它是一个非常受欢迎的编程基础教学工具。它使用变量管理大型数据集的独特方式也使它在不断发展的数据科学领域非常受欢迎。数据科学家可以导入大量数据,并用很少的代码对数据集执行操作。当然,Python 有一些特性,我们将在本章中更深入地探讨。

下载和安装 Python

首先,让我们讨论一些关于版本的事情。Python 本质上有两种风格:Python 2.7 和 Python 3。在 Python 3 中,创造者吉多·范·罗苏姆决定清理代码,而不太强调向后兼容性;因此,2.7 版本的一些代码根本无法在 3 版本中运行,反之亦然。Python 3 是当前版本,所有东西最终都会转移到它上面。事实上,在这一点上,几乎一切都有。在机器人学方面,最大的坚持者是 OpenCV,一个计算机视觉函数库的开源库,我们将在第九章中用到它。还有一些还没有完全迁移,所以您需要弄清楚您想要做什么,以及您需要的包是否已经被移植。我们将在我们的项目中使用 Python 2.7,因为您将在自己的研究中找到的许多例子都在 2.7 中。

如果你使用的是 Ubuntu 或 Debian Linux 系统,比如 Raspberry Pi,那就大功告成了。Python 工具已经安装完毕,可以使用了。大多数基于 Debian 的发行版安装 Python 作为基本映像的一部分。

如果您在 Windows 或另一个操作系统中跟随,您需要安装 Python。

  1. 导航到 www.python.org/about/gettingstarted/
  2. 单击下载。
  3. 如果您使用的是 Windows,请单击下载 Python 2.7.13。
  4. 如果您正在使用另一个操作系统,请从使用不同的操作系统查找 Python?这会将您带到相应的下载链接。
  5. 如果您想使用旧版本的 Python(出于某种奇怪的原因),您可以通过向下滚动页面找到合适的链接。
  6. 下载完成后,运行安装程序并按照屏幕上的指示进行操作。

Python 工具

有许多工具可以支持您的 Python 开发。像大多数编程语言一样,代码只是可以用任何文本编辑器编写的文本。可以在 Windows 机器上用记事本编写 Python 代码。我不会推荐它,但是你能做它。像 Notepad++这样的应用根据文件扩展名识别 Python 脚本,然后相应地突出显示代码。

你的选择范围很广。然而,在我们的练习中,我们将使用 Python 的每个安装都附带的本机工具:Python shell 和 Python 编辑器。

Python 外壳

Python shell 是 Python 解释器的一个接口。从技术上讲,如果你是命令行的爱好者,你可以启动一个终端窗口并调用 Python 解释器。然而,我们将使用随 Python 一起安装的 Python shell 接口,如图 3-1 所示。它为查看和执行命令提供了一个非常简洁的界面。

A457480_1_En_3_Fig1_HTML.jpg

图 3-1

The IDLE Python shell

当您打开本机空闲 IDE 时,Python shell 会启动。取决于你问谁,IDLE 或者代表集成开发环境,或者代表集成开发和学习环境。我喜欢后者,只是因为它对我来说更有意义。但本质上,它是 Python 解释器的窗口接口。它提供了一些在简单的命令行中无法获得的特性。

  • 简单的编辑功能,如查找、复制和粘贴
  • 语法突出显示
  • 带有语法突出显示的文本编辑器
  • 具有单步执行和断点的调试器

因为我们将在整本书中大量使用这个接口,所以谨慎的做法是学习更多关于 IDLE 接口及其提供的许多工具的知识。您可以从 https://docs.python.org/3/library/idle.html 的闲置文档页面开始。

Python 编辑器

IDLE 还有一个非常重要的方面:文本编辑器。我们将在整本书中使用它来编写我们的程序和模块。文本编辑器是 IDLE 的另一个方面,不是一个单独的程序,尽管它总是在一个单独的窗口中打开(见图 3-2 )。您可以在任何文本编辑器中编写 Python 程序,并且有许多支持 Python 的 ide。然而,正如我在上一节提到的,IDLE 接口提供了很多优势。

A457480_1_En_3_Fig2_HTML.jpg

图 3-2

The IDLE file editor

稍后您将会了解到,格式化在 Python 中非常重要。对于其他语言,如 C 和 Java,空白与编译器无关。空格、制表符、换行符和空行使代码对人们来说更具可读性。然而,在 Python 中,缩进表示代码块。IDLE 为您管理这一切。它自动缩进你的代码,减少由于不恰当的缩进而导致的语法错误的可能性。

还有几个工具可以帮助你编写代码。例如,当您键入时,IDLE 会显示一个适合您所在行的可能语句列表。有几种方法可以调用这个特性。很多时候,它会在你打字的时候自动弹出。这通常发生在函数调用中,可能性有限。您也可以在键入时按 Ctrl-space 来强制打开它。当您这样做时,您会看到一个可供选择的语句列表。当您选择其中一个语句时,它会为您补全单词,并为您提供其他可用的选项,例如任何适当的参数。这些被称为呼叫提示。

调用提示显示可访问函数的预期值,当您在函数名后键入"("时,调用提示会打开。它显示函数签名和文档字符串的第一行。它将保持打开状态,直到光标移出函数或输入结束的")"

上下文突出显示是通过颜色来实现的。当您键入代码时,一些单词会改变颜色。颜色是有意义的,是一种快速、直观的方式来验证你是否在正确的轨道上。以这种方式突出显示的上下文是输出、错误、用户输出和 Python 关键字、内置的类和函数名、类和定义后面的名称、字符串和注释。

让我们来看看实际情况。

  1. 空闲时打开。
  2. 单击文件➤新建文件。这将打开一个新的文本编辑器窗口。
  3. 类型pr
  4. 按 Ctrl-空格键。这将显示完成列表,单词print高亮显示。
  5. 类型(。这有几个作用。它选择突出显示的文本。在这种情况下print。它还显示打印功能的调用提示。
  6. 类型"Hello World"
  7. 在您键入结束语)后,calltip 会关闭。
  8. 按回车键。
  9. 将该文件另存为hello_world.py
  10. 按 F5 或从菜单中选择运行➤运行模块。

在 Python shell 窗口中,您应该会看到类似这样的内容:

RESTART: D:/Projects/The Robot Group/Workshops/Raspberry Pi Robot/hello_world.py
Hello World

如果您看到这个,那么您的代码是成功的。如果您收到某种类型的错误,请返回以确保您的代码如下所示:

print("Hello World")

哦,对了,你刚刚编写并运行了你的第一个 Python 程序。与其他语言相比,你会注意到 Python 的简单性。通常,在 C、C++或 Java 中,几行 Python 操作需要多几行。

Python 的禅

Python 的长期贡献者 Tim Peters 写了 Python 开发背后的管理原则。我认为它实际上适用于所有的代码,以及我们在机器人学中所做的一切,也许在生活中也是如此。它们作为复活节彩蛋被藏在 Python IDE 中。

A457480_1_En_3_Fig3_HTML.jpg

图 3-3

The Zen of Python

  1. 空闲时打开。
  2. 键入import this并按回车键。
  3. 它应该显示如图 3-3 所示的文本(但无论如何都要这样做)。

编写和运行 Python 程序

如果你按照你应该做的去做,那么你已经编写并运行了你的第一个 Python 程序。如果您没有跟上,不要担心,我们现在会再做一次,但是需要更多的编程。

你好世界

让我们添加一个简单的变量调用。我们会在不久的将来讨论变量。

  1. 打开hello_world.py

  2. 如果你是我前面提到的那些反叛者之一,请空闲地打开。

  3. 单击文件➤新建文件。

  4. 将该文件另存为hello_world.py。现在我们都在同一页上…

  5. 让程序看起来像这样:

    message = "Hello World"
    print(message)
    
    
  6. 保存文件。

  7. 按 F5 或从菜单中选择运行➤运行模块。

您应该会看到与之前相同的输出:

RESTART: D:/Projects/The Robot Group/Workshops/Raspberry Pi Robot/hello_world.py
Hello World

我们在这里所做的只是将文本从 print 函数移动到一个变量中,然后告诉 Python 打印该变量的内容。我将很快介绍变量。

基本结构

在我们开始研究程序细节之前,我们需要熟悉 Python 程序的结构。我们将看看程序的不同部分,如何使用缩进来格式化程序,以及如何使用注释来添加有意义的上下文。

程序部分

正如您所看到的,Python 程序没有太多必需的部分。大多数编程语言要求你至少创建某种主函数。对于 Arduino,它是loop()功能。在 C++里是main()。Python 没有这个功能。它会直接执行在文件中找到的任何命令。然而,这并不意味着它完全是线性的。我将在研讨会的后面讨论函数和类,但是我只知道解释器会扫描文件,并在执行其他命令之前构建它找到的任何函数和类。这是 Python 如此容易学习的原因之一。它不像大多数其他语言那样有严格的框架。

对了,对于编程语言纯粹主义者来说,Python 在脚本语言和编程语言之间游走,在脚本语言中一切都通过解释器来执行。一些代码被编译成可执行文件,如 C 和 C++。事实上,当我们开始构建模块时,这正是所发生的事情。然而,在本书中,我们通常通过解释器来运行它。

刻痕

随着我们在车间的工作,我们的程序变得更加复杂。特别是,我们将从代码块开始,代码块是组合在一起的命令,在函数或循环中执行,或者作为条件的一部分。这种结构对于编写有效的程序至关重要。

所有编程语言都有格式化代码块的语法。基于 c 的语言,包括 Java,使用花括号{}来包含代码块。Python 不会这样做。Python 使用缩进。代码块是缩进的,以表明它们是相关的块。如果块中的一行没有正确缩进,就会出现错误。这是我们使用闲置的关键原因之一。它会自动管理缩进。这并不意味着你,作为一个用户,不能修补程序;这只是意味着这种错误大大减少了。

随着课程的进行,你会看到缩进的重要性。同时,你要知道这很重要。

最后,我想对缩进和编辑器做一个简单的说明。文本编辑器使用不同形式的缩进。有些使用制表符,而有些使用两个或四个空格。您看不到正在使用的内容,因为这些是不可见的字符。当您从一个编辑器切换到另一个编辑器时,这会导致无尽的挫败感。更好的编辑器允许您选择 tab 键的工作方式(使用一个 tab 字符或多个空格,通常是四个)。默认情况下,IDLE 使用四个空格。

评论

随着时间的推移,注释代码变得越来越重要。这是一个众所周知程序员缺乏的领域。他们使用注释,但是他们经常含糊其辞,或者对程序的知识做出假设,这对于后来学习代码的人来说可能不成立。如果他们在其他形式的文档方面做得更好,这就不是问题了。但是,唉,他们不是。

撇开我的悲叹不谈,评论是很重要的,尤其是在你学习的时候。所以,要养成用评论的习惯。用它们来解释你在做什么,或者输入关于逻辑的小注释。

Python 中的注释是以#开头的任何一行。Python 忽略了从#到行尾的所有内容;例如:

# create a variable to hold the text
message = "Hello World"

# print the text stored in the variable
print(message)

在前面的代码中,我向我们的hello_world.py程序添加了两行注释。我还添加了一个空行来帮助使代码更容易阅读。如果您保存并运行这个程序,您将得到与之前完全相同的输出。

您还可以使用三重引号"""创建注释块。Python 编译器会忽略这些标记集之间的任何代码。你用它们打开和关闭街区。这允许你在多行上写尽可能多的信息。这种符号常用于文件开头的标题块。

"""
Hello World
the simplest of all programs
By: Everyone who has written a program, ever
"""

在编写代码之前,简单地用注释概述代码是一个好习惯。在你开始写之前,想想你需要你的代码做什么,以及你将如何去做。创建一个流程图,或者简单地写下实现目标的步骤。然后,在编写任何实际代码之前,在文件中将其转换成一系列注释。这有助于你在头脑中构建问题,并改善你的整体流程。如果你发现你正在重复的一个步骤,你可能就有了一个函数的候选者。如果你发现你引用了一个结构化的概念(比如机器人),你想赋予它特殊的属性和功能,你就有了一个类。稍后,我将进一步详细讨论函数和类。

运行程序

正如您之前看到的,有几种方法可以运行 Python 程序。

在空闲状态下,您只需按 F5 即可。您需要首先确保文件已保存,但这将运行您的文件。如果文件未保存,系统会提示您保存。它等同于从菜单栏中选择运行➤运行模块。

如果您的系统正确配置为从命令行运行 Python,您也可以从那里执行程序。您需要导航到文件的位置,或者在调用中使用完整的文件位置。要从命令行执行 Python 脚本,可以键入python,后跟要运行的文件。

> python hello_world.py
> python c:\exercises\hello_world.py
> python exercises\hello_world.py

这三个命令都将运行我们的 Hello World 程序,尽管其中两个是特定于操作系统的。第一个命令假设您正在文件存储的目录中执行该文件。第二个命令在 Windows 中运行程序,假设它保存在 C:\驱动器根目录下的一个名为exercises的文件夹中。第三个命令在 Linux 机器上运行程序,假设文件保存在主目录中名为exercises的文件中。

Python 编程

在接下来的几节中,我们将使用 Python shell 并直接输入命令。过了一会儿,我们又开始写程序文件。但是,现在,我们所做的一切都可以在 shell 窗口中演示。

变量

变量本质上是一个存储信息的便利容器。在 Python 中,变量非常灵活。不需要声明类型。类型通常是在您为其赋值时确定的。事实上,你通过给变量赋值来声明变量。当你从数字开始时,记住这一点很重要。在 Python 中,1 和 1.0 是有区别的。第一个数字是整数,第二个数字是浮点数。稍后会有更多内容。

以下是变量的一些通用规则:

  • 它们只能包含字母、数字和下划线
  • 它们区分大小写;比如variableVariable不一样。那以后会咬你一口的。
  • 不要使用 Python 关键字

除了这些严格的规则之外,这里还有一些提示:

  • 用尽可能少的字符使变量名有意义
  • 使用小写 L 和大写 o 时要小心,这些字符看起来非常类似于 1 和 0,会导致混淆。我不是说不要使用它们;确保你清楚自己在做什么。强烈建议不要将它们用作单字符变量名。

数据类型

Python 是一种动态类型语言。这意味着存储在变量中的数据类型直到程序被执行时才被检查,而不是在程序被编译时。这允许您在赋值之前推迟分配类型。然而,Python 也是强类型的,如果您试图执行对该数据类型无效的操作,它将会失败。例如,不能对包含字符串的变量执行数学运算。因此,跟踪变量引用的数据类型非常重要。

我将讨论两种基本的数据类型:字符串和数字。然后我将讨论一些更复杂的类型:元组、列表和字典。Python 允许您将自己的类型定义为类。我将在本章的最后介绍类,因为我们还需要先介绍一些其他的概念。

用线串

字符串是包含在引号中的一个或多个字符的集合。引号是表示字符串的方式。例如,“100”是一个字符串;100 是一个数字(更准确地说是一个整数)。你可以使用双引号或单引号(只要记住你用了什么)。您可以将一种类型的引号嵌套在另一种类型的引号中,但是如果您交叉使用引号,将会出现错误,或者更糟的是,会出现意想不到的结果。

这里有一个双引号的例子:

>>>print("This is text")
This is text

这里有一个单引号的例子:

>>>print('This is text')
This is text

下面是双引号内单引号的一个例子:

>>>print("'This is text'")
'This is text'

下面是单引号内双引号的一个例子:

>>>print('"This is text"')
"This is text"

三重引号用于跨越一个字符串中的多行。

这里有一个三重引号的例子:

>>>print("""this
is
text""")
this
is
text

如果您先对单引号进行转义,则可以使用单引号作为撇号。转义一个字符仅仅意味着你告诉解释器把一个字符看作一个字符串,而不是一个函数。

这里有一个转义引用的例子:

>>>print('This won't work')
File "<stdin>", line 1
        print('this won't work')
                        ^
SyntaxError: invalid syntax

>>>print('This won\'t error')
This won't error

您可以通过使整个字符串成为原始字符串来实现这一点。在下一个例子中,'/n'用于移动到新的一行。

下面是一个原始字符串的示例:

>>>print('something\new')
something
ew

>>>print(r'something\new')
something/new

字处理

操纵字符串有很多种方法。有些相当简单,比如串联——将字符串加在一起。不过,其中有些还是有点让人意外的。字符串被视为字符值的列表。在本章的后面,我们将更详细地探讨列表。然而,我们将使用列表的一些特性来处理字符串。

因为字符串是列表,类似于其他语言中的数组,所以我们可以引用字符串中的特定字符。像列表一样,字符串是零索引的。这意味着字符串的第一个字符位于零位置。

对于字符串,如列表,第一个字符位于索引[0]:

>>>robot = 'nomad'
>>>robot[0]
n

当使用负数时,索引从字符串的末尾开始并向后工作。

>>>robot[-1]
t

分割字符串允许您提取子字符串。对字符串进行切片时,需要提供两个用冒号分隔的数字。第一个数字是起始索引,第二个数字是结束索引。

>>>robot[0:3]
nom

请注意,切片时,第一个索引是包含性的,第二个索引是排他性的。在前面的示例中,索引[0]处的值返回“r”;而索引[3]处的值不是“0”。

切片时,如果您留下一个索引,则假定是字符串的开头或结尾。

>>>robot[:3]
nom

>>>robot[3:]
ad

将字符串相加称为串联。使用 Python,您可以轻松地将字符串添加到一起。这适用于字符串和字符串变量。您也可以将字符串相乘以获得有趣的效果。

你可以把两个字符串加在一起。

>>>print("Ro" + "bot")
Robot

您可以将字符串变量加在一起,如下所示:

>>>x = "Ro"
>>>y = "bot"
>>>z = x + y
>>>print(z)
Robot

您可以添加一个字符串变量和文字。

>>>print(x + "bot")
Robot

您可以将字符串文字相乘。

>>>print(2 * "ro" + "bot")
rorobot

然而,字符串的乘法只适用于文字。它对字符串变量不起作用。

我建议花些时间探索这些和其他字符串操作方法。更多信息,请访问 https://docs.python.org/3/tutorial/introduction.html#stringshttps://docs.python.org/3.1/library/stdtypes.html#string-methods

民数记

Python 中的数字有几种风格,最常见的是整数和浮点数。整数是整数,而浮点数是小数。Python 也使用值为 1 或 0 的布尔类型。这些常用作标志或状态,其中 1 表示“开”,0 表示“关”布尔值是整数的子类,在执行运算时被视为整数。

正如您所料,您可以对数字类型执行数学运算。通常,如果对一种类型执行算术运算,结果就是该类型。使用整数的数学通常会产生一个整数。但是,如果对整数执行除法,结果是浮点数。使用浮点运算会导致浮点运算。如果对这两种类型都执行算术运算,结果是浮点型。

两个整数相加得到一个整数。

>>>2+3
5

添加两个浮点导致一个浮点。

>>>0.2+0.3
0.5

将一个浮点数和一个整数相加会产生一个浮点数。

>>>1+0.5
1.5

减法和乘法的工作原理是一样的。

>>>3-2
1

>>>3-1.5
1.5

>>>2*3
6

>>>2*0.8
1.6

除法总是产生浮点数。

>>>3.2/2
1.6

>>>3/2
1.5

**运算符产生第一个数字的第二次幂。

>>>3**2
9

然而,Python 浮动有一个问题。解释程序有时会产生看似任意数量的小数位。这与浮点符号在 Python 中的存储方式以及解释器中的数学运算方式有关。

>>>0.2+0.1
0.30000000000000004

关于这种异常的更多信息,请访问 https://docs.python.org/3/tutorial/floatingpoint.html

列表

列表是按特定顺序排列的项目的集合。在其他语言中,它们通常被称为数组。你可以在列表中放入任何你想要的东西。存储在列表中的值不必是相同的数据类型。但是,如果您在一个列表中混合了多种数据类型,请确保您在使用它时知道您得到的是哪种类型。

当你处理字符串的时候,你处理的是列表。字符串本质上是一系列字符。因此,索引和切片也适用于列表。

使用方括号[]创建一个列表。

>>> robots = ["nomad","Ponginator","Alfred"]
>>> robots
['nomad', 'Ponginator', 'Alfred']

像字符串一样,列表也是零索引的。这意味着列表中的第一个元素位于位置 0,第二个元素位于位置 1,依此类推。您可以通过调用列表中的索引或位置来访问列表中的各个元素。

>>>robots[0]
'nomad'

>>>robots[-1]
'Alfred'

列表也可以切片。当列表被切片时,结果是一个包含原始列表子集的新列表。

>>>robots[1:3]
['Ponginator','Alfred']

使用切片和连接很容易添加、更改和删除列表成员。

本示例向列表中添加成员。

>>>more_bots = robots+['Roomba','Neato','InMoov']
>>>more_bots
['nomad', 'Ponginator', 'Alfred', 'Roomba', 'Neato', 'InMoov']

本示例更改列表中的成员。

>>>more_bots[3] = 'ASIMO'
>>>more_bots
['nomad', 'Ponginator', 'Alfred', 'ASIMO', 'Neato', 'InMoov']

本示例删除列表中的成员。

>>>more_bots[3:5] = []
>>>more_bots
['nomad', 'Ponginator', 'Alfred', 'InMoov']

将列表成员分配给变量:

>>>a,b = more_bots[0:2]
>>>a
'nomad'
>>>b
'Ponginator'

列表中会自动包含许多方法。例如,您可以强制名称的第一个字母大写。

>>> print(robots[0].title())
Nomad

正如我提到的,列表可以包含任何类型的数据,包括其他列表。事实上,当我们开始使用计算机视觉时,我们会频繁地使用列表来保存图像数据。

列表是 Python 的一个非常强大和重要的方面。访问 https://docs.python.org/3/tutorial/introduction.html#lists 花些时间探索列表。

元组

在使用 Python 时,您会经常听到术语 tuple。元组只是一种特殊的不能改变的列表。把一个元组想象成一个常量列表,或者一个常量列表。使用圆括号而不是方括号来声明元组。

元组是不可变的,这意味着一旦元组被创建,它就不能被改变。要更改元组的内容,必须创建一个新的元组。这是使用我们用于字符串和列表的相同切片技术来完成的。

>>> colors = ("red","yellow","blue")
>>> colors
('red', 'yellow', 'blue')
>>>colors2 = colors[0:2]
>>>colors2
('red','yellow')

注意,我们在分割元组时使用了列表符号,colors[0:2]而不是colors(0:2)。切片的结果仍然是一个元组。

然而,元组可以被替换。

>>>colors2 = (1,2,3)
>>>colors2
(1,2,3)

它们也可以用空元组替换。

>>>colors2 = ()
>>>colors2
()

字典

字典类似于列表,除了它允许您命名列表中的项目。这是通过使用键/值对来完成的。该键成为值的索引。这允许你给你的列表添加一些有意义的结构。它们对于保存参数或属性列表很有用。

字典是用花括号而不是方括号声明的。

>>> Nomad = {'type':'rover','color':'black','processor':'Jetson TX1'}
>>> print(Nomad['type'])
Rover

使用字典的方式与使用数组的方式非常相似。除了不是提供索引号,而是提供访问元素的键。

在使用字典之前,有几件事需要了解。

  • 该键必须是不可变的值,如数字或字符串。元组也可以用作键。

  • 一个键不能在字典中定义多次。像变量一样,键值是最后分配的。

    >>>BARB = {'type':'test-bed','color':'black','type':'wheeled'}
    >>>BARB
    {'color':'black','type':'wheeled'}
    
    

    在本例中,第一个'type'值被第二个值覆盖。

  • 字典可以作为值嵌套在其他字典中。在下面的例子中,我将正在进行的机器人项目 Nomad 的定义嵌入到我的机器人字典中。

    >>>myRobots = {'BARB':'WIP','Nomad':Nomad,'Llamabot':'WIP'}
    >>>myRobots
    {'BARB': {'color':'black','type':'wheeled'},'Nomad': {'color':'black','type':'wheeled'},'Llamabot':'WIP'}
    
    

当然,如果不能更新和操作字典中包含的值,那么字典就没有多大用处。对字典进行更改类似于对列表进行更改。唯一真正的区别是,您使用键而不是位置来访问各种元素。

要更新值,请使用键引用要更改的值。

>>>myRobots['Llamabot'] = 'Getting to it'
>>>myRobots
{'BARB': {'color':'black','type':'wheeled'},'Nomad': {'color':'black','type':'wheeled'},'Llamabot':'Getting to it'}

可以用del语句删除一个键/值对。

>>>del myRobots['Llamabot']
>>>myRobots
{'BARB': {'color':'black','type':'wheeled'},'Nomad': {'color':'black','type':'wheeled'}}

可以用 dictionary 类的copy方法复制一个字典。要访问copy方法,从字典名开始,在末尾添加.copy()

>>>workingRobots = myRobots.copy()
>>>workingRobots
{'BARB': {'color':'black','type':'wheeled'},'Nomad': {'color':'black','type':'wheeled'}}

要将一个字典附加到另一个字典的末尾,使用update方法。

>>>otherRobots = {'Rasbot-pi':'Pi-bot from book','spiderbot':'broken'}
>>>myRobots.update(otherRobots)
>>>myRobots
{'BARB': {'color':'black','type':'wheeled'},'Nomad': {'color':'black','type':'wheeled'},'Rasbot-pi':'Pi-bot from book','spiderbot':'broken'}

无类型

在处理从其他来源导入的类和对象时,有一种特殊的数据类型非常重要。这是 none 类型,它是一个空的占位符。当我们想声明一个对象,但后来又定义它时,就使用它。它也用来清空一个对象。在本章的后面,当我们讨论类的时候,你会看到 none 类型的作用。同时,要知道它是存在的,它本质上是一个空的占位符。

关于变量的最后一点说明

当您完成本节中的示例时,您正在处理变量。注意变量是如何接受您提供的任何值,并愉快地返回您指定的值的。如果将一个列表赋给一个变量,它将返回该列表—方括号和所有内容。这同样适用于元组、字典、字符串和数字。无论你分配给它什么,它就是你所得到的。当我们将一个字典嵌套在另一个字典中时,我们看到了这一点。通过简单地将字典名添加到另一个字典的定义中,我们将所有的值嵌入到新字典中。

我为什么指出这一点?

在本书的后面,当我们开始使用函数和类时,你将为你的变量分配复杂的数据结构。重要的是要知道,无论你赋给一个变量什么,它就包含什么,并且你可以应用任何适合该数据类型的方法或函数。

>>> robots = ["nomad","Ponginator","Alfred"]
>>> robots
['nomad', 'Ponginator', 'Alfred']
>>> myRobot = robots[0]
>>> myRobot
'nomad'
>>> myRobot.capitalize()
'Nomad'

我们在字符串变量myRobot上使用了字符串类的方法。方法是我们赋予类的功能。由于数据类型是一个内置的类,我们可以在变量上使用该类的方法。当我们在本章末尾开始处理类时,我会更详细地讨论方法。

控制结构

在这一节中,我们将探索如何给你的代码添加结构。你可能想要更多的控制,而不是一步一步地通过一个程序并执行遇到的每一行代码。这些控制结构允许您仅在特定条件存在时执行代码,并多次执行代码块。

在大多数情况下,向您介绍这些概念比试图描述它们要容易得多。

if 语句

if语句允许您在执行代码块之前测试一个条件。条件可以是计算结果为 true 或 false 的任何值或等式。

下一段代码遍历机器人列表,确定机器人是否是流浪者。

>>> for robot in robots:
        if robot=="Nomad":
                print("This is Nomad")
        else:
                print(robot + " is not Nomad")

This is Nomad
Ponginator is not Nomad
Alfred is not Nomad

同样,请注意键入时的缩进。IDLE 在以冒号结尾的每一行之后缩进另一级,这应该是表示一个新块的每一行,比如循环语句和条件语句。

同样重要的是要注意我们如何测试平等性。一个等号表示赋值。双等号告诉解释器比较是否相等。

>>> myRobot = "Nomad"
>>> myRobot == "Ponginator"
False
>>> myRobot == "Nomad"
True

以下是比较器列表:

| 等于 | `==` | | 不相等 | `!=` | | 不到 | `<` | | 大于 | `>` | | 小于或等于 | `<=` | | 大于或等于 | `>=` |

您还可以使用andor来测试多个条件。

>>> for robot in robots:
        if robot == "Ponginator" or robot == "Alfred":
                print("These aren't the droids I'm looking for.")

These aren't the droids I'm looking for.
These aren't the droids I'm looking for.

比较也经常用于确定一个对象是否存在或包含一个值。本质上,如果条件的计算结果不为 false、0 或 none,则条件的计算结果为 true。当您希望仅在对象存在的情况下执行一段代码时,这非常方便,例如当您通过串行端口或网络初始化传感器或连接时。

当您使用机器人技术时,有时您需要重复一段代码。无论是对一组对象执行一组指令,还是只要条件存在就执行一段代码,都需要使用循环。

循环允许您多次重复代码块来执行任务。有两种类型的循环:for循环和while循环。每一个都提供了特定的功能,这些功能对于编写高效的程序至关重要。

for 循环

一个for循环为列表中的每个元素执行一个代码块。值的集合——元组、列表或字典——被提供给for循环。然后,它遍历列表并执行代码块中包含的代码。当集合中的元素用完时,循环被退出,执行for块外的下一行代码。

if语句一样,您将想要作为循环的一部分运行的代码放入由缩进指示的块中。重要的是要确保你的缩进是正确的,否则你会得到一个错误。

当您将它输入 Python shell 时,请注意它对缩进做了什么。

在您输入了print命令并按下 Enter 键之后,您需要再次按下 Enter 键,这样 shell 就知道您已经完成了。

>>> for robot in robots:
        print(robot)

Nomad
Ponginator
Alfred

程序进入 robots 列表,取出第一个值Nomad,然后打印出来。由于这是块中的最后一行,解释器返回列表并提取下一个值。如此重复,直到列表中不再有值。此时,程序退出循环。

我倾向于用复数来命名我的列表。这允许我使用名称的单数形式来引用循环列表中的项目。例如,机器人元组中的每个元素都是一个机器人。

如果您想要遍历字典中的元素,您需要提供两个变量来存储各个元素。还需要使用 dictionary 类的items方法。这允许您依次访问每个键/值对。

>>>for name,data in Nomad.items():
        print(name + ': ' + data)

color: black
type: wheeled

您可以使用enumerate函数向for循环的输出添加一个连续的数值。

>>>for num,robot in enumerate(robots):
        print(num,robot)

(0, 'Nomad')
(1, 'Ponginator')
(2, 'Alfred')

while 循环

一个for循环为列表中的每个元素执行一段代码,而while循环执行一段代码,只要它的条件评估为真。它通常用于执行特定次数的代码,或者在系统处于特定状态时执行。

要在代码中循环特定的次数,可以使用一个变量来保存整数值。在下面的例子中,我们告诉程序只要 count 变量的值小于 5 就运行代码。

>>> count = 1
>>> while count < 5:
        print(count)
        count = count+1

1
2
3
4

我们首先声明一个变量count来保存我们的整数,我们给它赋值 1。我们进入循环,1 的值小于 5,因此代码将该值打印到控制台。然后我们将count的值增加 1。因为这是循环中的最后一条语句,并且由while条件评估的最后一个值小于 5,所以代码返回到while子句。count的值,现在是 2,仍然小于 5,所以代码再次执行。重复该过程,直到count的值为 5。五不小于五,所以解释器不执行块中的代码就退出循环。

如果我们忘记增加count变量,就会导致一个开放循环。因为count等于 1,并且我们从不递增它,所以count的值总是等于 1。一小于五,所以代码永远不会停止执行,我们需要按 Ctrl-C 来结束它。

它还被用作一种主循环,持续执行代码。你会在很多程序中看到以下内容:

while(true):

True 始终计算为 true;因此,该块中的代码会一直执行,直到程序退出。这被称为开环,因为没有闭环。幸运的是,有一种方便的方法可以退出开放循环。如果你发现自己处于一个开放循环中,或者你只是想随意退出一个程序,按 Ctrl-C。这将导致程序立即退出。你会经常用到这个。

这种技术也可以用来让程序等待一个特定的条件得到满足。例如,如果我们在继续之前需要一个串行连接,我们将首先启动 connection 命令。然后,我们可以等待连接完成,然后继续使用以下命令:

while(!connected):
        pass

感叹号,也叫砰,代表不。因此,在这种情况下,假设connected变量包含建立时评估为真的串行连接,只要它没有连接,我们就告诉程序执行包含在while块中的代码。

在这种情况下,我们告诉它执行的代码称为 pass,这是一个空命令。当你实际上不想做任何事情,但你需要一些东西的时候,就用它。因此,我们这样告诉系统:“当你没有连接时,不要做任何事情,直到你连接上为止。”

功能

函数是预定义的代码块,我们可以从程序中调用它来执行任务。在本章中,我们一直在使用print()函数。print()命令是 Python 中的内置函数。Python 中有许多预定义的函数,还有更多的函数可以使用模块来添加。有关可用函数的更多信息,请查看位于 https://docs.python.org/3/library/index.html 的 Python 标准库。

很多时候你都想创建自己的函数。函数有几个用途。

大多数情况下,您使用函数来包含要在整个程序中执行的代码。每当您发现自己在代码中重复相同的操作时,您就有了一个函数的候选对象。

功能也广泛用作一种内务管理形式。它们可以用来将长进程移动到主程序之外的地方。这可以使你的代码更容易阅读。例如,您可以将机器人的动作定义为函数。当您的主代码满足某个条件时,您只需调用该函数。比较这两个伪代码块:

while(true):
        if command==turnLeft:
              /*
              Lengthy list of instructions to turn left
              */
        if command==turnRight:
              /*
                Lengthy list of instructions to turn right
              */
        /* etc. */

while(true):
        if command==turnLeft:
                turnLeft()
        if command==turnRight:
                turnRight()
        /* etc. */

在第一个块中,移动机器人的代码包含在if语句中。如果左转需要 30 行代码(不太可能,但是请耐心等待),那么您的主代码将会多 30 行。如果向右转需要相同数量的代码,您将有另外 30 行代码——您必须遍历所有这些代码才能找到您要找的那一行。这变得非常乏味。

在第二个块中,要转换的代码被移动到一个单独的函数中。这个函数被定义在程序的其他地方,或者当我们讨论库和模块的时候你会知道,它可以存在于另一个文件中。这使得书写和阅读更容易。

定义函数

要定义自己的函数,需要创建函数名和包含要执行的操作的代码块。定义以关键字def开始,后面是函数名、括号和冒号。

让我们创建一个简单的函数:

>>> def hello_world():
        message = "Hello World"
        print(message)

>>> hello_world()
Hello World

在这段代码中,我们创建了一个简单的函数,它只打印消息“Hello World”。现在,每当我们想要打印该消息时,我们只需调用该函数。

>>> hello_world()
Hello World

为了让事情更有趣一点,我们可以为函数提供数据来使用。这些被称为争论。

传递参数

通常,我们希望给函数提供信息来使用或处理。为了提供信息,我们给函数一个或多个变量来存储这些信息,称为参数。

让我们创建一个问候用户的新函数。

>>> def hello_user(first_name, last_name):
        print("Hello " + first_name + " " + last_name + "!")

>>> hello_user("Jeff","Cicolani")
Hello Jeff Cicolani!

这里我们创建了一个名为 hello_user 的新函数。我们告诉它期望接收两条信息:用户的名和姓。函数定义中的变量名包含了我们想要使用的数据。该函数使用我们提供的两个参数打印问候语。

默认值

只要在声明函数时赋值,就可以为参数创建默认值。

>>> def favorite_thing(favorite = "robotics"):
        print("My favorite thing in the world is "+ favorite)

>>> favorite_thing("pie")
My favorite thing in the world is pie
>>> favorite_thing()
My favorite thing in the world is robotics

请注意,我们第二次调用该函数时,没有包含值。所以,函数只是使用了我们在创建函数时指定的默认值。

返回值

有时我们不仅仅希望函数自己做一些事情。有时我们希望它能给我们一些回报。这有助于将一个常见的计算移到一个函数中,或者如果我们希望函数验证它是否正确运行。许多内置函数和来自外部库的函数在函数成功时返回 1,在函数失败时返回 0。

要返回值,只需使用return关键字,后跟您想要返回的值或变量。请记住,return退出该函数,并将值提供给调用该函数的行。所以,确保你不要在return声明后做任何事情。

>>> def how_many(list_of_things):
        count = len(list_of_things)
        return count

>>> how_many(robots)
3

一个return语句可以返回多个值。要返回多个值,请用逗号分隔每个值。该函数将值放入一个可以被调用代码解析的元组中。

>>> def how_many(list_of_things):
        count = len(list_of_things)
        return count, 1

>>> (x, y) = how_many(robots)
>>> x
3
>>> y
1

通过模块添加功能

模块本质上是一个文件中的函数集合,可以包含在程序中。有数不清的模块让你的生活更轻松。许多模块都包含在标准 Python 安装中。其他的可以从不同的开发者那里下载。如果您找不到想要的内容,可以创建自己的自定义模块。

导入和使用模块

导入模块很容易。如您所见,您只需使用import关键字,后跟模块名称。这将加载该模块的所有功能供您使用。现在,要使用模块中的一个函数,您需要输入模块名,后跟函数。

>>> import math
>>> math.sqrt(9)
3.0

有些包非常大,您可能不想导入整个包。如果您知道程序中需要的特定函数,您可以只导入模块的该部分。

这将从数学模块导入sqrt函数。如果只导入函数,就不需要在函数前面加上模块名。

>>> from math import sqrt
>>> sqrt(9)
3.0

最后,您可以为导入的模块和函数提供一个别名。当您导入一个名称相当长的模块时,这变得非常方便。在这个例子中,我只是在偷懒:

>>> import math as m
>>> m.sqrt(9)
3.0
>>> from math import sqrt as s
>>> s(9)
3.0

内置模块

核心 Python 库为基本程序提供了许多功能。然而,还有其他开发人员和研究人员编写的更多可用功能。但是在我们进入第三方模块的奇妙世界之前,让我们看看 Python 附带了什么。

打开一个空闲实例,并键入以下内容:

>>> import sys
>>> sys.builtin_module_names

您应该得到如下所示的输出:

('_ast', '_bisect', '_codecs', '_codecs_cn', '_codecs_hk', '_codecs_iso2022', '_codecs_jp', '_codecs_kr', '_codecs_tw', '_collections', '_csv', '_datetime', '_functools', '_heapq', '_imp', '_io', '_json', '_locale', '_lsprof', '_md5', '_multibytecodec', '_opcode', '_operator', '_pickle', '_random', '_sha1', '_sha256', '_sha512', '_signal', '_sre', '_stat', '_string', '_struct', '_symtable', '_thread', '_tracemalloc', '_warnings', '_weakref', '_winapi', 'array', 'atexit', 'audioop', 'binascii', 'builtins', 'cmath', 'errno', 'faulthandler', 'gc', 'itertools', 'marshal', 'math', 'mmap', 'msvcrt', 'nt', 'parser', 'sys', 'time', 'winreg', 'xxsubtype', 'zipimport', 'zlib')

这是 Python 内置的模块列表,现在就可以使用。

要获得关于一个模块的更多信息,可以使用help()函数。它列出了当前安装并注册到 Python 的所有模块。(注意,为了打印,我必须截断列表。)

>>> help('modules')

Please wait a moment while I gather a list of all available modules...

AutoComplete        _random         errno            pyexpat
AutoCompleteWindow  _sha1           faulthandler     pylab
AutoExpand          _sha256         filecmp          pyparsing
Bindings            _sha512         fileinput        pytz
CallTipWindow       _signal         fnmatch          queue
...
Enter any module name to get more help.  Or, type "modules spam" to search
for modules whose name or summary contain the string "spam".

您还可以使用help()函数来获取特定模块的信息。首先,您需要导入模块。同样,为了简洁起见,下面的清单被截断了。

>>> import math
>>> help(math)
Help on built-in module math:

NAME
    math

DESCRIPTION
    This module is always available.  It provides access to the
    mathematical functions defined by the C standard.

FUNCTIONS
    acos(...)
        acos(x)

        Return the arc cosine (measured in radians) of x.
...
FILE
    (built-in)

你可以在 Python 文档网站 https://docs.python.org/3/py-modindex.html 上了解更多关于这些内置模块的信息。

扩展模块

除了每个 Python 安装都提供的内置模块之外,您还可以添加无数名为packages的扩展。幸运的是,Python 的优秀人员提供了一种了解第三方包的方法。更多信息请访问 https://pypi.python.org/pypi

一旦您找到了您想要或需要为您的应用安装的包,安装它最简单的方法就是使用 PIP。从 Python 2.7.9 和 Python 3.4 开始,下载中包含了 PIP 二进制文件。但是,由于软件包在不断发展,您可能需要升级它。如果一切安装和配置正确,您应该能够从命令行做到这一点。

  1. 打开终端窗口。

  2. 在 Windows 中,键入

    python -m pip install -U pip
    
    
  3. 在 Linux 或 macOS 中,键入

    pip install -U pip
    
    

一旦完成,您就可以使用 PIP 了。请记住,您将从终端运行 PIP,而不是从 Python shell 中运行。

在这个演示中,我们将安装一个用于绘制数学公式的包。matplotlib 是一个非常流行的使用 Python 可视化数据的包。此软件包的实际使用超出了本研讨会的范围。有关使用 matplotlib 的更多信息,请访问他们的网站 https://matplotlib.org

要安装新的软件包,请键入

pip install matplotlib

这将安装 matplotlib 库供您使用。

自定义模块

如果您有几个一直在使用的函数(通常称为帮助函数),您可以将它们保存在一个名为myHelperFunctions.py的文件中。然后,您可以使用import命令使这些功能在另一个程序中可用。

一般来说,您将要导入的自定义模块文件保存在与您正在使用的程序相同的文件位置。这是确保编译器能够找到文件的最简单也是最好的方法。可以将该文件保存在其他地方,但是您可以包含该文件的完整路径,或者对系统路径变量进行更改。现在,将您创建的任何模块文件保存在您的工作目录中(与您正在处理的程序在同一个位置)。这有助于你避免任何额外的心痛。

到目前为止,我们一直使用 IDLE shell。让我们创建一个自定义模块文件,然后将其导入到另一个程序中。

  1. 空闲时打开。

  2. 单击文件➤新建文件。这将打开一个新的文本编辑器窗口。

  3. 在新文件窗口中,点击文件➤保存并将其命名为myHelperFunctions.py

  4. 输入以下代码:

    def hello_helper():
            print("I'm helper. I help.")
    
    
  5. 保存文件。

  6. 单击文件➤新建文件创建新的代码文件。

  7. 键入以下内容:

    import myHelperFunctions
    myHelperFunctions.hello_helper()
    
    
  8. 在保存myHelperFunctions.py的同一目录下,将文件另存为hello_helper.py

  9. 按 F5 或从菜单中选择运行➤运行模块。

在 shell 窗口中,您应该看到以下内容:

I'm helper. I help.

班级

现在我们来看看好东西:类。类只不过是代码中物理或抽象实体的逻辑表示;例如,一个机器人。robot 类创建了一个向程序描述物理机器人的框架。你如何描述它完全取决于你,但它表现在你如何构建类。这种表达是抽象的,就像机器人这个词代表了机器人概念的抽象一样。如果我们站在一个满是机器人的房间里,我说,“把机器人递给我,”你的反应很可能是,“哪个机器人?”这是因为机器人这个词适用于房间里的每一个机器人。但是,如果我说,“把 Nomad 递给我”,你就会知道我说的那个机器人。Nomad 是机器人的一个实例。

这就是一个类的用法。从定义类开始。你可以通过构造你想要表现的实体的抽象来做到这一点;在这个例子中,是一个机器人。当您想要描述一个特定的机器人时,您可以创建一个应用于该机器人的类的实例。

关于类有很多要学的,但是下面是你需要知道的关键事情。

  • 一个类由称为方法的函数组成。方法是类中执行工作的函数。例如,你可能在机器人类中有一个名为drive_forward()的方法。在这个方法中,你添加代码使机器人向前行驶。
  • 一个方法总是需要self参数。此参数是对类实例的引用。
  • self总是一个方法的第一个参数。
  • 每个类都必须有一个叫做__init__的特殊方法。创建实例时会调用__init__方法,它会初始化该类的实例。在这个方法中,您执行类运行所需的任何操作。大多数情况下,您为类定义属性。
  • 类的属性是类中描述某些特性的变量。例如,对于 robot 类,我们想命名一些功能属性,比如方向和速度。这些是在__init__方法中创建的。

有几种类型的方法:

  • Mutator 方法:这些方法改变类中的值。例如,setters 是一种设置属性值的 mutator 方法。
  • 访问器方法:这些方法访问类中的属性。
  • 助手方法:这些方法包括在类中执行工作的任意数量的方法。例如,强制的__init__方法是一种叫做构造函数的助手。助手方法是在一个类中执行工作的任何东西,通常用于其他方法;例如,在输出前格式化字符串的方法。

创建一个类

在深入研究并开始编写代码之前,我建议您花一点时间来计划您将要构建的内容。这并不需要一个详尽的计划,但是在你开始构建之前,至少要有一个大概的轮廓。

规划

做计划最简单的方法是在一张纸上,但是如果你喜欢数字,你最喜欢的文本编辑器也可以。你想列一个课程清单或大纲。我们的示例类是针对一个模拟的轮式机器人的,所以我们想列出描述我们的机器人的属性,然后列出机器人将执行的动作。这些是我们的方法。

初始样本机器人类别
  • 属性
    • 名字
    • 描述
    • 原色
    • 物主
  • 方法
    • 向前行驶
    • 向后开
    • 向左转
    • 向右转

当你在写提纲的时候,想象你将如何使用每一种方法。你需要什么信息,如果有的话?它将返回什么信息(如果有)?如果你的方法需要参数形式的信息,有默认值吗?如果是这样,是否希望保留以编程方式更改默认值的能力?根据我的经验,最后一个问题的答案几乎总是肯定的。

所以,带着这些问题,让我们重温一下大纲。

初始样本机器人类别
  • 属性
    • 名字
    • 描述
    • 原色
    • 物主
    • 默认速度(默认值:125)
    • 默认持续时间(默认值:100)
  • 方法
    • 向前行驶(参数:速度)(返回:无)
    • 向后行驶(参数:速度)(返回:无)
    • 左转(参数:持续时间)(返回:无)
    • 右转(参数:持续时间)(返回:无)
    • 设定速度(参数:新速度)(返回:无)
    • 设置持续时间(参数:新持续时间)(返回:无)

正如您所看到的,在回顾了大纲之后,我们添加了一些新的属性和一些新的方法。默认速度为 0 到 255 之间的整数值。在本书的后面,我们用这个值来设置电机控制器的速度。半速是 125。默认持续时间是机器人移动的时间,以毫秒为单位。值 100 大约是 1/10 秒。我们还添加了两种方法来设置这两个属性的值。

在大多数编程语言中,属性是私有的,这意味着它们只能从类中包含的代码访问。因此,您可以创建get()set()方法来查看和更改这些值。在 Python 中,属性是公共的,可以通过简单的class.attribute调用来访问或更改。Python 属性不能成为私有属性;然而,Python 中的传统是在希望私有的属性前面加上下划线。这向其他开发人员表明,该属性应该被视为私有属性,不能在类的方法之外进行修改。

所以,严格来说,set speed 和 set duration 方法并不是严格需要的。如果我们想表明这些属性是私有的,并且只应使用方法进行更新,那么我们在名称前面加上一个下划线,如下所示:

_speed
_duration

您可以在代码中的任何地方创建一个类。类如此有用的原因是它们封装了允许您轻松地将其从一个项目移植到下一个项目的功能。因此,最好创建一个类作为它自己的模块,并将其导入到您的代码中。这就是我们在这里要做的。

让我们建立我们的机器人类,然后使用它。

  1. 创建一个新的 Python 文件,并将其另存为robot_sample_class.py。我们将从声明我们的类并创建所需的构造函数__init__开始。现在,我们需要__init__做的就是初始化属性并将值从参数移动到属性。注意,我们已经声明了speedduration的默认值分别为 125 和 100。

  2. 输入下面的代码:

    class Robot():
        """
        A simple robot class
        This multi-line comment is a good place
        to provide a description of what the class
        is.
        """
    
        # define the initiating function.
        # speed = value between 0 and 255
        # duration = value in milliseconds
        def __init__(self, name, desc, color, owner,
                    speed = 125, duration = 100):
                # initilaizes our robot
            self.name = name
            self.desc = desc
            self.color = color
            self.owner = owner
            self.speed = speed
            self.duration = duration
    
    

    初始化完成后,让我们看看如何编写我们的方法。如前所述,方法只是包含在类中的函数,在类中执行工作。由于我们目前没有要控制的机器人,所以我们简单地将确认消息打印到 shell 来模拟我们的机器人。

    def drive_forward(self):
            # simulates driving forward
            print(self.name.title() + " is driving" +
                    " forward " + str(self.duration) +
                    " milliseconds")
    
    def drive_backward(self):
            # simulates driving backward
            print(self.name.title() + " is driving" +
                    " backward " + str(self.duration) +
                    " milliseconds")
    
    def turn_left(self):
    # simulates turning left
            print(self.name.title() + " is turning " +
                    " right " + str(self.duration) +
                    " milliseconds")
    
    def turn_right(self):
            # simulates turning right
            print(self.name.title() + " is turning " +
                    " left " + str(self.duration) +
                    " milliseconds")
    
    def set_speed(self, speed):
            # sets the speed of the motors
            self.speed = speed
            print("the motor speed is now " +
                    str(self.speed))
    
    def set_duration(self, duration):
            # sets duration of travel
            self. duration = duration
            print("the duration is now " +
                    str(self. duration))
    
    
  3. 保存文件。现在我们已经创建了新的Robot类,我们将使用它将 Nomad 定义为一个Robot

  4. 创建一个新的 Python 文件,并将其另存为robot_sample.py。我们将从导入robot_sample_class代码开始,然后用它来创建一个名为 Nomad 的新机器人。

  5. 输入下面的代码:

    import robot_sample_class
    my_robot = Robot("Nomad", "Autonomous rover",
            black", "Jeff Cicolani")
    
    

    使用类定义创建类的新实例称为实例化。注意,我们没有提供最后两个参数的值,speedduration。因为我们为这些参数提供了默认值,所以不需要在实例化期间提供值。如果我们没有提供缺省值,当我们试图运行代码时会得到一个错误。对于我们的新机器人实例,让我们做一些工作。

    print("My robot is a " + my_robot.desc + " called " + my_robot.name)
    
    my_robot.drive_forward()
    my_robot.drive_backward()
    my_robot.turn_left()
    my_robot.turn_right()
    my_robot.set_speed(255)
    my_robot.set_duration(1000)
    
    
  6. 保存文件。

  7. 按 F5 运行程序。

在 Python shell 窗口中,您应该会看到类似这样的内容:

>>> ======================RESTART====================
>>>
My robot is an autonomous rover called Nomad
Nomad is driving forward 100 milliseconds
Nomad is driving backward 100 milliseconds
Nomad is turning left 100 milliseconds
Nomad is turning right 100 milliseconds
the motor speed is now 255
the duration is now 1000

式样

在我们结束这一章之前,我想花点时间讨论一下代码的样式。我们已经看到了缩进的重要性,它必须符合严格的准则来表示代码块等等。但是在一些领域,你可以影响不太重要的风格决定。当然,Python 社区中有一些传统是值得推荐的。

Python 的创建者和初级开发者提出了一些最佳实践。你可以在 www.python.org/dev/peps/pep-0008/ 的 Python 风格指南中读到他们所有的建议。我建议在你养成一些坏习惯(像我一样)之前,通读一下风格指南并实践他们的建议。现在,让我们关注如何命名变量、函数和类。

空白行

在代码块之间留出空行以实现逻辑上的、视觉上的分离是一个好主意。它使你的代码更容易阅读。

评论

在代码中编写注释。勤做,啰嗦。当您稍后回过头来阅读您的代码时(为了调试或者为了在另一个项目中重用它),您会想知道当代码被编写时您在想什么,以及您试图用它做什么。

如果你的代码被公之于众,被其他人阅读或审阅,他们也会需要评论。Python 是一个社区,代码共享频繁。良好的注释和描述的代码非常受欢迎。

命名规格

如何命名变量、函数和类是个人的决定。做你最舒服的事。Python 是一种区分大小写的语言。在一个地方使用大写字母而不在另一个地方使用会产生两个不同的变量和无尽的沮丧。

《样式指南》中没有提到常见的变量名,尽管惯例是使用混合大小写命名。大小写混合的名称以小写字符开头,但名称中的每个单词都是大写的;比如myRobots

函数和模块应该是小写的。为了便于阅读,在单词之间使用下划线。所以我们的hello world函数被命名为hello_world

类应该用大写字母命名。顾名思义,CapWords 将每个单词的首字母大写,包括名称中的第一个字符。这种风格更普遍地被称为骆驼案件。

最后,列表和其他集合应该是多元化的。这表明该变量代表多个对象。例如,robots是机器人的列表。如果我们处理列表中的单个项目,它看起来会像这样:

robot = robots[0]

摘要

我们在整本书中都使用 Python。这是一种非常容易学习的语言,并且它提供了很多强大的特性。很多软件开发者认为 Python 速度慢。但是当它在某些领域慢的时候,它会在其他领域弥补更多的时间,当我们在第九章开始使用计算机视觉的时候你会看到。

四、树莓派 GPIO

前几章介绍了 Raspberry Pi 硬件,您学习了如何使用 Python 对其进行编程。您安装了操作系统,对其进行了配置以供您使用,并设置了远程访问,这样您就可以在不直接连接键盘、鼠标和显示器的情况下对 Pi 进行编程。您学习了 Python 程序的基本结构、语法,以及足够开始编写程序的语言知识。

接下来,您将学习如何使用 Raspberry Pi 的 GPIO 接口与物理世界进行交互。这对机器人来说至关重要,因为这是处理器检测周围发生的事情并对外界刺激做出反应的方式。如果没有探测和作用于物理世界的能力,任何智能自主都是不可能的。

树莓派 GPIO

有几种方法可以连接到 Raspberry Pi。到目前为止,最简单的方法是通过内置在主板上的 USB 端口。USB 端口提供了四个串行连接,通过它们您可以访问外部组件,比如我们用来设置 Pi 的键盘和鼠标。然而,USB 端口需要特殊的硬件来将串行命令转换成操作设备所需的信号。Raspberry Pi 有一个更直接的连接外部设备的方法:GPIO 头。

GPIO 是电子设备与外界的接口。接头通常是指电路板上允许使用某些功能的一组引脚。GPIO 接头是一对 20 针的行,沿着电路板的一边延伸(见图 4-1 ),称为 40 针接头。

A457480_1_En_4_Fig1_HTML.jpg

图 4-1

Raspberry Pi with 40-pin header

务必注意,接头提供了与电路板上电子器件的直接连接。这些销既没有缓冲也没有内置安全功能。这意味着,如果你连接错误或使用错误的电压,你可能会损坏你的 Pi。在使用割台之前,您需要了解以下事项:

  • 虽然树莓派 采用 5 伏 USB 微型适配器供电,但电子设备为 3.3 伏。这意味着您需要注意传感器使用的电压。
  • GPIO 引脚上提供两种电压:5V 和 3.3V。请注意使用哪种电压,尤其是在试图通过 GPIO 为 Pi 供电时。
  • 可以通过 5V GPIO 引脚之一为 Raspberry Pi 供电;然而,没有提供电路保护和调节。如果提供的电压过高,或者出现电流尖峰,电路板可能会损坏。如果必须使用 GPIO 引脚为电路板供电,请务必提供外部稳压器。
  • GPIO 头有两种编号模式:板和 BCM。这意味着有两种不同的方法来引用代码中的管脚;你决定使用哪一个通常取决于你自己。您只需要记住您选择了哪个模式。

引脚编号

正如我提到的,40 引脚接头有两种编号模式:电路板和 BCM。

电路板编号只是按顺序对引脚进行编号。引脚 1 是最靠近 micro SD 卡的引脚,引脚 2 是最靠近 Pi 外边缘的相邻引脚。编号继续以这种方式进行,奇数编号的引脚在内侧,偶数编号的引脚在外侧。引脚 40 是电路板边缘的引脚,靠近 USB 端口。

BCM 编号远没有这么简单。BCM 代表博通,驱动 Pi 的 SoC(片上系统)的制造商。在树莓派 2 上,处理器是 BCM2836 在树莓派 3 上,是 BCM2837。BCM 编号是指 Broadcom 芯片的引脚编号,不同版本之间可能会有所不同。BCM2836 和 BCM2837 具有相同的引脚排列,因此 Pi 2 和 Pi 3 没有区别。

为了将电子元件连接到 40 针接头,我们将使用 Adafruit T-Cobbler Plus 和试验板。T-Cobbler 将 pin 信息印在板上,以便快速参考;然而,T-Cobbler 使用 BCM 编号。因此,我们将使用 BCM 编号。

连接到树莓派

有几种方法可以将接头的引脚连接到其他设备。我们将使用的电机控制器就是一个直接位于接头顶部的电路板。在 Raspberry Pi 术语中,这些板被称为帽子或盘子。

另一种选择是使用跳线直接连接到引脚。对于许多人来说,这是在原型开发期间的首选方法。

我更喜欢第三种方法,那就是使用 Adafruit 的另一个叫做 Pi 鹅卵石的板子。有几个版本的补鞋匠,但我更喜欢树莓派的 Adafruit T-Cobbler Plus(见图 4-2 )。该板设计用于通过带状电缆连接到试验板。它使用 40 引脚接头,垂直于插入试验板的引脚。这将带状电缆附件从试验板上移开,以便更好地接近孔。

A457480_1_En_4_Fig2_HTML.jpg

图 4-2

T-Cobbler mounted on the breadboard

使用补鞋匠的一个优点是引脚断点标记清晰。当我们开始构建我们的电路时,很容易就能看到你到底在连接什么。这也使得识别哪个管脚正被用于你的代码变得更加容易。当您将引脚 21 声明为输出引脚时,您将确切知道它是电路板上的哪个引脚。

Raspberry Pi 的 GPIO 局限性

在使用 GPIO 时,有几件事情需要记住。

首先,我们设置的 Raspberry Pi 不是一个实时设备。Debian Linux 是一个完整的操作系统,从硬件中抽象出许多层。这意味着对硬件的命令不是直接的。相反,在 CPU 将命令发送到电路板之前和之后,命令会经过几次操作。Python 在另一个抽象层中运行。每一层都会引入一定程度的滞后。我们一般感觉不到它,但它可以在机器人操作中产生巨大的差异。有一些 Debian 发行版更实时,是为工业应用设计的,但是我们正在使用的标准 Raspbian 版本不是其中之一。

第二,Pi 上没有模拟输入。嗯,有一个,但它是与串行端口共享的,我们以后可能会将它用于其他用途。因此,最好接受没有模拟输入引脚的事实。你会在第五章中看到为什么这很重要。

第三,Pi 只有两个支持 PWM 的引脚。PWM 代表脉宽调制,这是我们向外部设备发送不同信号的方式。这意味着接头上只有两个引脚可以模拟模拟输出。这两个引脚还与 Pi 的音频输出共享,这不是最佳选择。

好消息是,所有这些问题都有一个简单的解决方案,只需引入一个实时的外部微控制器,提供多个模拟输入,并提供两个以上的 PWM 输出。我们将在第五章中使用 Arduino。Arduino 基本上是 AVR AT 系列微控制器的原型板。这些芯片直接连接到硬件,没有大多数 SoC 处理器中的抽象层,如 Pi 上的那些。使用 Arduino 还有其他好处,我会在第五章中讨论。

用 Python 访问 GPIO

硬件只是等式的一部分。我们将使用新的 Python 技能来编程我们想要的行为。为了做到这一点,我们将使用 RPi。GPIO 库。你会从第三章回忆起,库是提供额外功能的类和函数的集合。

在机器人技术中,一个新的硬件、传感器或其他组件通常都有一个库,可以让你更容易地使用它。有时候库是通用的,比如 RPi。GPIO 其他时候,该库是为特定设备制作的。例如,我们将在第七章中使用特定于电机控制器板的库。当你给你的机器人增加更多的硬件时,你经常需要从制造商的网站上下载新的库。当我们开始使用电机控制器时,您将会看到这一点。

GPIO 库提供了访问 GPIO 管脚的对象和函数。Raspbian 附带了安装的库,所以应该可以使用了。有关如何使用该软件包的更多信息,请访问 https://sourceforge.net/p/raspberry-gpio-python/wiki/BasicUsage/

要使用 GPIO 库,我们需要做两件事:导入包,然后告诉它我们将使用哪种模式来访问管脚。如前所述,有两种模式——电路板和 BCM——从本质上告诉系统使用哪个编号参考。

电路板模式参考树莓派 的 P1 标题上的编号。由于这种编号保持不变,为了向后兼容,您无需根据电路板版本更改代码中的引脚编号。

相比之下,BCM 模式指的是 Broadcom SoC 的引脚编号,这意味着在新版 Pi 上,引脚布局可能会改变。幸运的是,在 Pi 2 中使用的 BCM2836 和 Pi3 中使用的 BCM2837 之间,这种引脚布局没有改变。

出于我们的目的,我们将使用 BCM 模式——因为这就是 T 形鞋匠上所展示的。

每个使用 GPIO 头的程序都包含下面两行代码:

import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)

简单输出:LED 示例

最简单的例子就是无处不在的硬件版“Hello World”——闪烁的 LED。我们的第一个 GPIO 项目是将 LED 连接到 Pi,并使用 Python 脚本使 LED 闪烁。让我们从连接电路开始。为此,您需要一个试验板、T 形补鞋匠、一个 LED、一个 220 欧姆(ω)电阻和两根用作跳线的短导线。

连接电路

  1. Attach the T-Cobbler as shown in Figure 4-3. One row of pins should be on either side of the split in the board. The placement is up to you; however, I generally attach it such that the ribbon cable header is off the board. This allows maximum access to the breadboard.

    A457480_1_En_4_Fig3_HTML.jpg

    图 4-3

    Circuit layout for the LED example

  2. 在地轨和一个空的 5 孔轨之间连接一个 220ω电阻。

  3. Connect the LED cathode to the same rail as the resistor. The cathode is the pin closest to the flat side of the LED. On some LEDs, this pin is shorter than the other pin (see Figure 4-4).

    A457480_1_En_4_Fig4_HTML.jpg

    图 4-4

    LED polarity

  4. 将 LED 阳极连接到另一个空的 5 引脚轨道。

  5. 将一根跳线从阳极的供电轨连接到与 T 形补钉上的引脚 16 相连的供电轨。

  6. 从 LED 连接的接地轨和连接到 T 形补鞋匠的任何接地引脚的轨连接一根跳线。

如果您想在进入代码之前测试 LED,可以将跳线从引脚 16 移至其中一个 3.3V 引脚。如果您的 Pi 已打开,LED 将会亮起。在继续之前,请确保将跳线移回第 16 针。

编写代码

这个项目的代码非常简单。它是用 Python 3 写的。尽管代码在任一版本中都可以工作,但其中一行在 Python 2.7 中不能工作。具体来说,末尾的打印行使用了end参数,这是不兼容的。如果您使用的是 Python 2.7,则需要省略此参数。

end参数用一个/r替换附加到每一个打印行的默认/n/r是回车,与/n代表的新行相反。这意味着光标返回到当前行的开头,任何新文本都会覆盖前面的字符。但是,它不会首先清除该行。因此,我们在新文本的末尾添加任意数量的空格,以确保之前的所有文本都被完全删除。

GPIO 命令访问系统级内存。所有系统级命令必须以超级用户或超级用户权限运行。这意味着你需要用sudo运行 Python 或者授予自己永久的 root 权限,这可能很危险。一旦我们编写了代码,我们将从命令中执行。在我们这样做之前,我们必须使文件可执行,但是从终端做起来很简单。

首先,让我们在终端上使用 IDLE 或创建一个新的 Python 3 文件。

如果使用 IDLE,请执行以下操作:

  1. Python 3 的 Open IDLE。
  2. 单击新建。
  3. 将文件另存为项目文件夹中的gpio_led.py

如果使用终端,请执行以下操作:

  1. 打开终端窗口。

  2. 导航到您的项目文件夹。在我的圆周率上,是$ cd ~/TRG-RasPi-Robot/code

  3. 类型touch gpio_led.py

  4. 类型idle3 gpio_led.py。这将在 Python 3 的空闲 IDE 中打开空文件。

  5. 一旦你的文件被创建并且你在空闲的编辑器中,输入下面的代码:

    # GPIO example blinking LED
    
    # Import the GPIO and time libraries
    import RPi.GPIO as GPIO
    import time
    
    # Set the GPIO mode to BCM and disable warnings
    GPIO.setmode(GPIO.BCM)
    GPIO.setwarnings(False)
    
    # Define pins
    led = 16
    
    GPIO.setup(led,GPIO.OUT)
    
    # Make sure LED is off
    GPIO.output(led,False)
    
    # Begin Loop
    while True:
    
        # Turn LED on
        GPIO.output(led,True)
    
        # Wait 1 second
        time.sleep(1)
    
        # Turn LED off
        GPIO.output(led,False)
    
        # Wait 1 second
        time.sleep(1)
    
    
  6. 保存文件。

接下来,我们将使用终端使文件可执行,然后运行它。

  1. 打开一个新的终端窗口,并导航到您的项目文件夹。
  2. 类型chmod +x gpio_led.py。这使得文件可执行。
  3. 要运行代码,请键入sudo python3 gpio_led.py

这就是了:一个闪烁的 LED。你好世界。

脉冲宽度调制

尽管 Pi 的 GPIO 头上只有两个 PWM 引脚,并且您可能不会使用它们,但知道如何正确控制它们还是很有用的。板上的两个 PWM 引脚是 18 和 19。在本例中,我们将 LED 设置为使用引脚 18,并向 LED 发送脉冲。

连接电路

好吧,这是复杂的部分。要设置这个电路,你需要非常仔细地遵循这些指示。使用我们为 LED 练习构建的电路。

  1. 将跳线从引脚 16 移至引脚 18。

唷。现在我们已经完成了所有这些,让我们编码。

编写代码

创建一个新的 Python 3 文件。

如果使用 IDLE,请执行以下操作:

  1. Python 3 的 Open IDLE。
  2. 单击新建。
  3. 将文件另存为项目文件夹中的gpio_pwm_led.py

如果使用终端,请执行以下操作:

  1. 在“终端”窗口中,导航到您的项目文件夹。我的圆周率上,是$ cd ~/TRG-RasPi-Robot/code

  2. 类型touch gpio_pwm_led.py

  3. 类型idle3 gpio_pwm_led.py。这将在 Python 3 的空闲 IDE 中打开空文件。

  4. 一旦你的文件被创建并且你在空闲的编辑器中,输入下面的代码:

    # GPIO example blinking LED
    
    # Import the GPIO and time libraries
    import RPi.GPIO as GPIO
    import time
    
    # Set the GPIO mode to BCM and disable warnings
    GPIO.setmode(GPIO.BCM)
    GPIO.setwarnings(False)
    
    # Define pins
    pwmPin = 18
    
    GPIO.setup(pwmPin,GPIO.OUT)
    pwm = GPIO.PWM(pwmPin,100)
    
    # Make sure LED is off
    pwm.start(0)
    
    # Begin Loop
    while True:
    
        count = 1
        # begin while loop to brighten LED
        while count < 100:
    
            # set duty cycle
            pwm.ChangeDutyCycle(count)
    
            # delay 1/100 of a second
            time.sleep(0.01)
    
            # increment count
            count = count + 1
    
        # begin while loop to dim LED
        while count > 1:
    
            pwm.ChangeDutyCycle(count)
    
            time.sleep(0.01)
    
            # set duty cycle
            pwm.ChangeDutyCycle(count)
    
            # delay 1/100 of a second
            time.sleep(0.01)
    
            # decrement count
            count = count – 1
    
    
  5. 打开一个新的终端窗口,并导航到您的项目文件夹。

  6. 键入chmod +x gpio_pwm_led.py使文件可执行。

  7. 要运行代码,请键入sudo python3 gpio_pwm_led.py

您的 LED 现在应该在闪烁。要改变脉冲的频率,改变time.sleep()函数调用中的值。

简单输入

既然我们已经看到了发出信号是多么容易,那么是时候将一些信息返回到 Pi 中了。我们将通过两个例子来说明这一点。第一,按钮;在这个例子中,Pi 被设置为从一个简单的按钮获取输入,并在按钮被按下时在终端中进行指示。第二个例子使用声波测距仪读取物体的距离。输出将显示在终端中。

按钮示例

最简单的输入形式是按钮。你按下一个按钮,电路闭合,就会发生一些事情。对于我们的第一个输入示例,我们将把一个按钮连接到 GPIO 头。

基本上有两种连接按钮的方式。您可以将其设置为在低状态下启动,这意味着当按钮未被按下时,没有信号到达该引脚,该引脚上的电压被处理器读取为“低”。也可以在高状态下连接。在此配置中,当按钮未按下时,引脚读数为高电平或 on。按下按钮时,引脚变为低电平状态。

你经常会听到拉高或拉低这两个词。拉高或拉低引脚是迫使引脚进入高电平或低电平状态的方法。在许多应用中,这是通过在电路中增加一个电阻来实现的。

连接在逻辑引脚和电压之间的电阻使引脚处于高电平状态。引脚被拉高。然后,按钮接地。按下按钮时,电压通过按钮流到地,绕过引脚。如果引脚上没有电压,它将进入低电平状态。

反之,在逻辑引脚和地之间连接电阻,然后在引脚和电压源之间连接按钮,引脚被下拉。当按钮断开时,引脚中的任何残余电压都被拉至地,使引脚处于低电平状态。当按钮被按下时,电压被施加到引脚,引脚进入高电平状态。

引脚被拉高或拉低,以确保当按钮被按下时,它们处于预期状态。这是一种明确告诉电路预期行为的方式,通常是一种好的做法。

幸运的是,Raspberry Pi 具有内置电路,可以将引脚拉高或拉低。这意味着我们可以通过代码将引脚拉到适当的状态,而不必担心添加额外的组件。

在这个练习中,让我们把引脚拉高。按下按钮时,引脚变为低电平,一条消息打印到终端窗口。

连接电路

本练习需要以下零件:

  1. Attach the T-Cobbler as shown in Figure 4-5. One row of pins should be on either side of the split in the board. The placement is up to you; however, I generally attach it so that the ribbon cable header is off the board. This allows maximum access to the breadboard.

    A457480_1_En_4_Fig5_HTML.jpg

    图 4-5

    Push-button example circuit layout

  2. 连接一个触觉按钮,使引脚桥接试验板中心的间隙。

  3. 在 3.3V 引脚和电压轨之间连接一根跳线。

  4. 在接地引脚和接地轨之间连接另一根跨接导线。

  5. 使用另一根跨接导线将触摸开关的一侧连接到接地轨。

  6. 使用另一根跨接导线将另一个按钮触针连接到触针 17。

  • 触觉按钮开关
  • 4 个男性对男性跳线

这些触摸开关是双刀单掷的(DPST)。这意味着当按下按钮时,试验板间隙一侧的两个引脚连接。间隙另一侧的引脚形成一个独立的电路。确保跳线连接到分割线同一侧的引脚。

编写代码

创建一个新的 Python 3 文件。

如果使用 IDLE,请执行以下操作:

  1. Python 3 的 Open IDLE。
  2. 单击新建。
  3. 将文件另存为项目文件夹中的gpio_button.py

如果使用终端窗口,请执行以下操作:

  1. 导航到您的项目文件夹。在我的 Pi 上是$ cd ~/TRG-RasPi-Robot/code

  2. 类型touch gpio_button.py

  3. 类型idle3 gpio_button.py。这将在 Python 3 的空闲 IDE 中打开空文件。

  4. 输入以下代码:

    # GPIO example using an NC-SR04 ultrasonic rangefinder
    
    # import the GPIO and time libraries
    import RPi.GPIO as GPIO
    
    # Set the GPIO mode to BCM mode and disable warnings
    GPIO.setmode(GPIO.BCM)
    GPIO.setwarnings(False) 
    
    # Define pin
    btnPin = 20
    GPIO.setup(btnPin, GPIO.IN, pull_up_down = GPIO.PUD_UP)
    
    # Begin while loop
    while True:
            btnVal = GPIO.input(btnPin)
    
            # If the pin is low, print to terminal
            if (btnVal == false):
                    print('Button pressed')
    
    
  5. 打开一个新的终端窗口,并导航到您的项目文件夹。

  6. 类型chmod +x gpio_button.py

  7. 要运行代码,请键入sudo python3 gpio_button.py

声波测距仪示例

在本例中,我们使用 HC-SR04 超声波传感器来确定到物体的距离。你将把呼叫放入一个循环中,让我们得到恒定的距离读数。您将使用前一个例子中使用的库来访问 GPIO 管脚。

本练习向您介绍了 Pi 和许多其他器件需要注意的一个关键因素:传感器和引脚之间的电压差。许多传感器被设计为在 5 伏下工作。然而,Pi 在其逻辑中使用 3.3 伏电压。这意味着所有 I/O 引脚都设计为在 3.3 伏电压下工作。向这些引脚中的任何一个施加 5V 信号都会对您的 Pi 造成严重损坏。Pi 确实提供了几个 5V 源引脚,但我们需要将返回信号降低到 3.3 伏。

连接电路

这一次,电路有点复杂。真的。请记住,传感器在 5 伏电压下工作。Pi 的 GPIO 引脚在 3.3 伏电压下工作。将 5V 返回信号馈入 3.3V 引脚可能会损坏 Pi。为了防止这种情况发生,我们在 echo 引脚上增加一个简单的分压器。

让我们做一些数学。

$$ {V}_{out}={V}_{in}\ast \frac{R1}{R1+R2} $$

$$ {V}_{out}={V}_{in}\ast \frac{R2}{R2+R1} $$

$$ \frac{V_{out}}{V_{in}}=\frac{R2}{R1+R2} $$

我们有 5 伏输入电压,想要 3.3 伏输出电压,我们使用 1kω电阻作为电路的一部分。所以…

$$ \frac{3.3}{5}=\frac{R2}{1000+R2} $$

$$ 0.66=\frac{R2}{1000+R2} $$

$$ 0.66\left(1000+R2\right)=R2 $$

$$ 660+0.66R2=R2 $$

$$ 660+0.34R2 $$

$$ 1941=R2 $$

以下是零件清单:

  • HC-SR04 战斗机
  • 1kω电阻
  • 2kω电阻您可以使用两个 1kω电阻串联,或者更受欢迎的类似电阻是 2.2k 电阻。我们就用这个。
  • 4 个男性对女性跳线
  • 4 个男性对男性跳线

这是设置。

  1. Attach the T-Cobbler, as shown in Figure 4-6. One row of pins should be on either side of the split in the board. The placement is up to you; however, I generally attach it so that the ribbon cable header is off the board. This allows maximum access to the breadboard.

    A457480_1_En_4_Fig6_HTML.jpg

    图 4-6

    Sonic rangefinder example circuit layout

  2. 确保接地跳线固定在接地引脚和接地轨之间。

  3. 在一个 5V 引脚和电源轨之间添加一根跳线。

  4. 使用一根阴阳跳线将 SR04 上的接地引脚连接到接地轨。

  5. 将 SR04 的 VCC 或 5V 引脚连接到电源轨。

  6. 将 SR04 上的 trig 引脚连接到 T 形补鞋匠的引脚 20。

  7. 将 2kω电阻从一个空的 5 引脚供电轨连接到接地供电轨。

  8. 将 1kω电阻从连接到 2kω电阻的供电轨连接到另一个空的 5 引脚供电轨。

  9. 在连接到 2kω电阻的供电轨和 T 形补钉上的引脚 21 之间连接另一根跳线。

  10. 将 SR04 echo 引脚连接到 1kω电阻另一端所连接的供电轨。

这就完成了布线。现在让我们设置代码。

编写代码

HC-SR04 超声波测距仪通过测量超声波脉冲返回传感器所需的时间来工作。我们将发出一个 10 微秒的脉冲,然后监听脉冲的返回。通过测量返回脉冲的长度,我们可以使用一点数学来计算厘米距离。

距离的计算方式为速度×时间。它是从公式速度=距离÷时间推导出来的。在海平面上,声音以每秒 343 米或每秒 34300 厘米的速度传播。由于我们实际上是在测量脉冲到达目标并返回所需的时间,所以我们实际上只需要该值的一半。让我们用下面的公式来计算:

距离= 17150×时间

代码只需发出一个 10μS 的脉冲,测量它返回所需的时间,计算以厘米为单位的估计距离,并在终端窗口中显示出来。

创建一个新的 Python 3 文件。

如果使用 IDLE,请执行以下操作:

  1. Python 3 的 Open IDLE。
  2. 单击新建。
  3. 将文件另存为项目文件夹中的gpio_sr04.py

如果使用终端窗口,请执行以下操作:

  1. 导航到您的项目文件夹。在我的 Pi 上是$ cd ~/TRG-RasPi-Robot/code

  2. 类型touch gpio_sr04.py

  3. 类型idle3 gpio_sr04.py。这将在 Python 3 的空闲 IDE 中打开空文件。

  4. 输入以下代码:

    # GPIO example using an NC-SR04 
    
    ultrasonic rangefinder
    
    # import the GPIO and time libraries
    import RPi.GPIO as GPIO
    import time
    
    # Set the GPIO mode to BCM mode and disable warnings
    GPIO.setmode(GPIO.BCM)
    GPIO.setwarnings(False)
    
    # Define pins
    trig = 20
    echo = 21
    
    GPIO.setup(trig,GPIO.OUT)
    GPIO.setup(echo,GPIO.IN)
    
    print("Measuring distance")
    
    # Begin while loop
    while True:
        # Set trigger pin low got 1/10 second
        GPIO.output(trig,False)
        time.sleep(0.1)
    
        # Send a 10uS pulse
        GPIO.output(trig,True)
        time.sleep(0.00001)
        GPIO.output(trig,False)
    
        # Get the start and end times of the return pulse
        while GPIO.input(echo)==0:
            pulse_start = time.time()
    
        while GPIO.input(echo)==1: 
    
            pulse_end = time.time()
    
        pulse_duration = pulse_end - pulse_start
    
        # Calculate the distance in centimeters
        distance = pulse_duration * 17150
        distance = round(distance, 2)
    
        # Display the results. end = '\r' forces the output to the same line
        print("Distance: " + str(distance) + "cm      ", end = '\r')
    
    
  5. 打开一个新的终端窗口,并导航到您的项目文件夹。

  6. 类型chmod +x gpio_sr04.py

  7. 要运行代码,请键入sudo python3 gpio_sr04.py

摘要

Raspberry Pi 的一大优点是 GPIO 头。40 针接头允许您直接与传感器和其他设备连接。除了我们用来连接 LED、按钮和超声波传感器的简单 GPIO 之外,还有具有其他特定功能的引脚。我建议探索一些其他的功能。标有 SCL、SDA、MISO 和 MOSI 的引脚是串行连接,允许您使用高级传感器,如加速度计和 GPS。

使用 GPIO 头时,有几件事需要记住。

  • 要运行您的脚本,首先使用chmod +x <filename>使文件可执行。
  • 无论何时运行使用 GPIO 管脚的脚本,都需要使用sudo
  • 请仔细注意传感器使用的电压。
  • 虽然接头可以为设备提供 5 伏电压,但逻辑引脚是 3.3 伏。如果你不减弱来自传感器的信号,你会损坏你的树莓派。
  • 分压电路——类似于为超声波传感器构建的分压电路——可用于将来自传感器的 5V 信号降至 3.3。
  • 被称为逻辑电平转换器的预制电路板降低了电压。
  • Raspberry Pi 没有实用的模拟引脚。
  • 它只有两个 PWM 通道。每个引脚都连接到两个引脚,因此看起来有四个可用的 PWM 引脚,但实际上没有。

在下一章中,我们将 Arduino 板连接到我们的 Raspberry Pi。Arduino 是为 IO 设计的微控制器。这就是它所做的一切,但它做得很好。通过结合这两种板,我们不仅克服了 Pi 的缺点,还增加了其他好处。

五、树莓派和 Arduino

在第四章中,我们使用 Raspberry Pi 上的 GPIO 引脚与 LED 和超声波传感器进行交互。很多时候,这就足够做你想做的事了。然而,我也讨论了 Raspberry Pi GPIO 的一些缺点,以及可能需要扩展 Pi 的功能来克服这些缺点。

在这一章中,我们将向我们的机器人介绍一个微控制器。微控制器是一种设备,通常是芯片形式,设计为通过输入和输出引脚直接与其他组件一起工作。每个引脚都连接到微控制器的电路,并有特定的用途。

因为引脚直接连接到微控制器敏感的内部工作部件,所以通常需要额外的电路来保证安全工作。许多制造商提供评估板,允许开发人员快速构建原型和概念验证器件。

一种这样的板实际上是由开发者而不是芯片制造商开发的,并提供给公众。由于其易用性、丰富的文档和出色的社区支持,这个设备很快成为了爱好者社区的最爱。当然,我说的是 Arduino。

我们涵盖了很多关于 Arduino 的信息:如何安装软件,编写程序(称为草图),以及将这些程序加载到 Arduino 板上。我们还将介绍如何让您的 Raspberry Pi 和 Arduino 板相互通信。这为你的机器人增加了指数级的能力。

但是在我们进入 Arduino 之前,让我们回顾一下 Raspberry Pi 的一些缺点。

树莓派 的 GPIO 回顾

具体来说,我们来谈谈缺乏足够的模拟和脉宽调制(PWM)引脚。

实时或接近实时的处理

实时处理是系统与 GPIO 和外部设备直接交互的能力。这对于 CNC 应用或其他需要即时响应的应用至关重要。在机器人学术语中,它对于要求对刺激做出即时反应的闭环系统是必要的。

一个很好的例子是移动机器人的边缘检测器。你希望机器人在自己驶下悬崖或桌子边缘之前停止移动。花费时间处理操作系统的许多抽象层以到达决定停止的逻辑,然后通过许多层将信号发送到电机控制器可能证明是灾难性的。而且,如果操作系统延迟操作或挂起,机器人将很高兴地直线下降到它的灭亡,永远不会知道。相反,你希望你的机器人立即停止。

尽管 Linux 有利于接近实时的处理,但这些是特殊的操作系统,我们使用的 Raspbian 安装不在其中。

模拟输入

我们已经看到数字输入在 Pi 上工作。事实上,当一个数字引脚打开然后关闭(变高,然后变低)时,我们使用超声波测距仪来检测范围。通过一点数学运算,我们能够将信号转换成有用的数据。那是一个数字信号;它只是检测一个引脚何时具有高电压,然后检测同一引脚何时具有低电压。

模拟信号有很多种类型;不仅仅是高或低、白或黑、开或关,还有一系列的值——或者用黑/白类比的灰色阴影。当你使用一个传感器来测量物体的强度或水平时,这是非常有用的。使用光敏电阻的光传感器就是一个例子。随着光强度的变化,传感器上的电阻也随之变化,电压也随之变化。一种称为模数转换器(ADC)的设备将模拟信号转换为程序可以使用的数字值。

Raspberry Pi 有一个模拟引脚。这不是很有用,尤其是当它与电路板可能使用的另一个功能相关联时——本例中是串行通信。如果我们将该引脚专用于模拟输入,我们将无法使用该串行通道。即使我们不打算使用那个特定的串行通道,单个模拟输入的用途也非常有限。

模拟输出

模拟输出本质上类似于模拟输入。在我们之前做的 LED 练习中,我们使用数字信号来打开和关闭 LED。模拟允许我们改变 LED 的亮度或强度。然而,数字系统,如计算机或微处理器,不能产生真正的模拟信号。

它调整数字信号的频率和持续时间。数字信号的持续时间称为脉冲。调整一个脉冲在给定时间段内的活动频率以及该脉冲的长度,称为脉宽调制(PWM)。当我们测量来自超声波测距仪的信号时,我们实际上是在测量从设备返回的脉冲。

Raspberry Pi 有四个 PWM 引脚可用。然而,这四个引脚只连接到两个 PWM 过程。这意味着我们只有两个 PWM 通道可用。同样,这并不像我们希望的那样有用。有了实时处理器,我们可以用软件模拟 PWM。然而,如前所述,Raspberry Pi 不是一个实时系统。因此,如果我们想要两个以上的 PWM 通道,我们需要找到另一种解决方案。

Arduino 来救援了

幸运的是,有一类设备是专门设计来实时管理各种类型的输入和输出的。这些是微处理器。有许多类型的微处理器。一些更常见和易于使用的是 AVR ATTiny 和 ATMega 处理器。

然而,这些是芯片,除非你习惯于使用它们,否则它们很难接近和使用。为了使这些设备更容易使用,制造商创造了所谓的开发板。这些板将芯片上的引脚连接到接头,以便于原型制作。他们还增加了使用引脚所需的电子器件,如稳压器、上拉电阻、滤波器电容、二极管等。因此,最后,你所要做的就是将你的特定电子设备连接到设备上,并制作出你的产品原型。

几年前,意大利的一群工程师聚在一起,做了一件前所未有的事情。他们围绕 AVR ATMega 芯片开发了自己的开发板,将设计向公众开放(开放硬件),然后向爱好者和学生推销。他们称这块板为 Arduino。图 5-1 显示了一个典型的 Arduino Uno。我敢肯定,它的预期结果是成为爱好者和制造者社区事实上的标准。

A457480_1_En_5_Fig1_HTML.jpg

图 5-1

Arduino Uno

我们将使用 Arduino Uno 搭配我们的树莓派。为什么?首先,它是一个实时处理器。Arduino 直接与引脚和连接的外设通信。不存在操作系统或程序层抽象导致的延迟。第二,它提供了更多的引脚。其中包括我们添加的六个模拟引脚和六个基于硬件的 PWM 引脚。它是“基于硬件”的,因为电路板是实时的,我们可以在任何引脚(顺便说一下,有 20 个)上模拟 PWM 信号(通过软件)。

这只是 Arduino Uno。有一个更大版本的 Arduino 板,称为 Mega。Mega 有 54 个数字引脚和 16 个模拟引脚。这总共是 70 针的 IO 品质。

Arduino 是开放的硬件,这意味着任何人都可以构建这些设计。因此,你会发现许多不同制造商以不同价格推出的许多不同版本。这是一个物有所值的典型例子。如果你是刚刚入门,我建议多花一点钱买一个更可靠的主板。稍后,随着您对故障诊断有了更好的理解和更高的容忍度,您可以尝试更便宜的主板。

使用 Arduino

Arduino 非常容易编程和使用。许多人被与电子和编程硬件一起工作的前景吓住了。但是,这么多人通过 Arduino 开始他们的机器人和物联网事业是有原因的。将设备连接到 Arduino 非常容易,尤其是使用名为 shields 的附件。

对 Arduino 进行编程也非常容易。Arduino 为电路板编程提供了一个接口,简称 Arduino。或者更准确的说,是 Arduino IDE(集成开发环境)。Arduino IDE 使用一种 C 编程风格,也称为 Arduino。如您所见,硬件、软件和开发环境在概念上是一样的。当你谈论 Arduino 编程时,软件和硬件之间没有区别,因为软件的唯一目的是与硬件交互。

在本章中,您需要安装 Arduino IDE,并将 Arduino 连接到您的计算机。假设安装说明和练习在您的 Raspberry Pi 上运行,但是老实说,在另一台机器上安装也同样容易。因此,如果你更喜欢在 Pi 之外的东西上工作,或者你只是不喜欢远程操作,你可以在你的 PC 或笔记本电脑上做所有的练习。

安装 Arduino IDE

在我们将 Arduino 连接到我们的 Raspberry Pi 之前,我们需要安装软件和驱动程序。还好,这个超级容易。安装 Arduino IDE 的同时也会安装使用 Pi 所需的所有驱动程序。

安装 Arduino IDE

  1. 打开终端窗口。
  2. 类型sudo apt-get install Arduino
  3. 对任何提示都回答是。
  4. 去喝一杯。这可能需要惊人的长时间。

安装过程完成后,它会将 Arduino IDE 添加到您的编程菜单中。

连接 Arduino

当我最初概述这本书的这一部分时,我的意图是提供多种方式将 Arduino 连接到 Raspberry Pi。然而,使用除 USB 端口之外的任何东西都会引入另一层复杂性和 Linux 细节,这超出了本文的介绍范围。它实际上是告诉 Pi 您正在激活 UART 引脚,然后禁用许多使用该通道的本机功能。这是一个不必要的过程,特别是因为有四个 USB 端口准备好了,如果你需要更多,你可以随时添加一个 USB 集线器。因此,我们将使用 USB 连接,这样我们就可以专注于 Arduino 的介绍,因为它与 Pi 相关。

要连接 Arduino,我们只需将 Raspberry Pi 的 USB 电缆连接到 Arduino,如图 5-2 所示。根据主板制造商的不同,您可能需要不同的 USB 电缆。因为我用的是原装的 Uno,所以我用的是 USB A-to-B 线。有的人用 USB 迷你线,有的人用 USB micro。

A457480_1_En_5_Fig2_HTML.jpg

图 5-2

USB A to B cable connected to the Arduino Uno

就是这样。由于 Arduino 板由您的 Pi 通过 USB 电缆供电,因此您不需要添加外部电源。您正准备开始使用您的 Arduino。接下来,我们要用无处不在的 blink 程序测试你的 Arduino。但首先,我们来看看界面。

编程 Arduino

正如我之前说过的,Arduino 的编程非常简单。然而,由于我们刚刚花了很多时间学习 Python,所以理解一些差异非常重要。

我们将从界面和一些使用它的技巧开始。然后我们将编写一个小程序来说明这种语言的结构和语法。所有这些都是为下一节做准备,在下一节中,我们将更深入地了解 Arduino 编程语言。

Arduino IDE

当你第一次打开 Arduino IDE 时,你会看到一个非常简单的界面(见图 5-3 )。开发人员在开发 Arduino 时采用了编程语言和 IDE 的接口。如果你过去做过编码,这个界面会显得缺乏特色。这既是有目的的,也有点误导。

A457480_1_En_5_Fig3_HTML.jpg

图 5-3

Arduino IDE

尽管界面简单,IDE 却异常健壮。最重要的是,它提供了交叉编译,使您在 Linux、Windows 或 Apple 机器上编写的代码能够在更简单的 AVC 处理器上工作。

让我们浏览一下 Arduino IDE 中的一些关键特性和操作。

图标和菜单

由于 Arduino 与众不同,界面顶部工具栏中的图标可能与你所习惯的不同。查看图 5-4 并从左至右移动,图标为编译、上传、新草图、打开、保存,最右边是串行监视器。

A457480_1_En_5_Fig4_HTML.jpg

图 5-4

Arduino IDE toolbar

前两个图标非常重要。

Compile 告诉 IDE 处理您的代码,并准备好加载到 Arduino 板上。它运行你的代码,并试图建立最终的机器级程序。此时,它会识别您可能输入的任何错误。Arduino 不提供任何调试功能,所以你很大程度上依赖于编译功能。

上传编译草图,然后上传到板上。因为上传功能首先运行编译器,所以您可以获得与编译功能相同的编译活动,但是,在过程结束时,它会尝试将编译后的代码加载到您的主板上。由于 AVR 处理器一次只能存储和运行一个程序,所以每次你上传到 Arduino 板上,你就覆盖了那里当前的内容。这并不总是可取的。有时你会间歇性地编译代码来检查语法并确保它是正确的。你不会总是想把这些中间步骤加载到板上。

然而,最后,你需要上传你的草图来看看会发生什么。编译草图可以确保你有工作代码。代码是否在做你想让它做的事情是另一回事。上传了你才知道这个。

创建新草图

可以通过单击工具栏中的“新建草图”图标或单击菜单中的“文件”“➤”“新建”来创建新草图。创建新草图总是会打开一个新的 IDE 实例。无论您在之前的窗口中做了什么,它都还在。

第一次打开 Arduino IDE 时,您会看到一个新草图的框架。这也是你后来创建一个的时候看到的。每个 Arduino 草图都包含这些元素。新的草图操作总是用这个框架预先填充 IDE。当我们写第一个草图时,你会看到这些元素是什么。

保存草图

在编译或运行草图之前,您需要保存它。你可以随时保存草图,只是必须在编译或上传之前完成。要保存草图,请单击保存图标或从菜单中选择文件➤保存。

首次保存草图时,系统会自动为其创建一个项目文件夹。这是保存代码文件(扩展名为.ino)的地方。为项目创建的任何其他文件也保存在该文件夹中。当您处理更大、更复杂的程序时,或者当您开始在 IDE 中将草图分成不同的选项卡时,这一点很重要。

打开现有草图

默认情况下,当您打开 IDE 时,您正在处理的最后一个草图会自动打开。当您在同一个项目上工作一段时间时,这很方便。

如果需要打开另一个草图,可以单击菜单栏中的打开草图图标,或者选择文件➤打开。或者,您也可以选择文件➤打开最近。这将列出您最近打开的几个草图。选择其中一个文件将在 IDE 的新实例中打开它。

板和端口选择

正确编译和加载草图的关键是选择合适的电路板和端口。从工具菜单中选择板和端口。

选择主板会告诉编译器您使用的是哪个版本的 Arduino。随着你的 Arduino、机器人和/或物联网经验的增长,你可能会使用不同的主板。Arduino IDE 最大的优点之一就是它的灵活性。您会发现自己正在使用熟悉而舒适的环境为不同制造商生产的大量不同的电路板编程。这有助于 Arduino 成为 maker 社区事实上的标准。

要为您的机器人选择电路板和端口,请确保您的 Arduino 通过 USB 连接,并且 Arduino IDE 已安装并打开。

  1. 从菜单中选择工具➤板。
  2. 从可用主板列表中选择 Arduino/Genuino Uno。
  3. 从菜单中选择工具➤港。列表中应该有一个类似于 TTYAMC0 上的 Arduino/Genuino Uno 的条目。
  4. 选择此条目。

此时,Arduino IDE 应该准备好编译草图并将其加载到您的主板上。我们将写我们的第一个草图来测试它。

举例作弊

当您安装 Arduino IDE 时,您也安装了一组示例草图(参见图 5-5 )。这些是学习 Arduino 编码的极好参考。在你学习的过程中,浏览这些草图,寻找与你试图完成的功能相似的功能。

A457480_1_En_5_Fig5_HTML.jpg

图 5-5

List of example code included with the base install

要查看或打开示例列表,请单击“文件”“➤示例”。随着您添加更多的库(如用于传感器和其他设备的库),您将添加到此示例列表中。所以当你扩展你自己的能力,以及你的机器人的能力时,一定要重温这些例子。

使用选项卡和多个文件

当我在前面讨论保存草图时,为单个文件创建一个项目文件夹似乎有点奇怪。这是因为一个项目可以包含多个文件。您可以为一个项目创建多个 Arduino 文件,或者您可能想要将包含的文件与项目放在一起。图 5-6 显示了三个标签打开的 Arduino IDE。

A457480_1_En_5_Fig6_HTML.jpg

图 5-6

Arduino IDE with multiple tabs

当项目文件夹中有多个代码文件时,当您打开该项目中的文件时,每个文件在 Arduino IDE 中都显示为一个选项卡。这允许您在工作时轻松地在文件之间导航。

当使用标签和多个文件时,有一些事情需要记住。通过 IDE 创建并保存为 INO 文件的标签中的代码被附加到主 INO 文件的末尾。这意味着您在这些选项卡中创建的任何功能都可以在这些选项卡中使用。但是,除非将文件包含在代码中,否则不在 IDE 中创建的文件的选项卡(如包含文件的选项卡)不可用。这既方便又令人沮丧,因为您需要跟踪特定函数来自哪个文件。

当我们回顾 Arduino 中的编码时,我会在这一章的后面更多地涉及到包含文件。

图 5-7 显示了选项卡管理菜单。

A457480_1_En_5_Fig7_HTML.jpg

图 5-7

Tab management menu

您可以创建新的选项卡来帮助组织您的代码。创建新选项卡时,该选项卡将作为新文件保存在项目文件中。

  1. 打开 Arduino IDE 并创建一个新文件。
  2. 保存文件以创建新的项目文件。
  3. 单击 IDE 标签栏中的箭头。
  4. 单击新建选项卡。
  5. 在打开的对话框中,输入选项卡的名称。请记住,这是项目文件夹中新文件的名称。
  6. 保存文件。所有未保存的选项卡也会被保存。

保存标签后,Arduino 会自动创建一个新文件来存储标签中的代码。

草图

Arduino 的程序被称为草图。这个想法就是你只是简单地勾画出代码——就像你在餐馆餐巾上勾画出一个想法一样。老实说,有时候确实会有这种感觉。

你在用一种叫做编程的语言写 Arduino 草图。它是 C 编程语言的精简版本,旨在使编码更容易。Arduino 实际上使用了为 Arduino 板设计的程序的修改版本。它本质上是 AVR 处理器能够运行的精简指令集。

像 Python 一样,当您添加功能和复杂性时,您可以添加库。在 C 语言中,我们使用include指令。它的作用与 Python 中的import命令相同。稍后,当我们在两个电路板之间进行交流时,我们会看到这一点。

你好 Arduino

为了理解编程 Arduino 和 Python 之间的区别,我们将编写一个简单的程序。与 GPIO 章节一样,第一个程序是 Hello World 的硬件版本——一个闪烁的 LED。加载程序后,您将了解更多关于编程、其结构以及如何使用它的知识。

在 GPIO 章节中,我们构建了一个带 LED 的小电路。然而,Arduino 有一个内置在电路板上的 LED 供我们使用,所以我们还不需要打开试验板。LED 与 UNO 上的 13 号插脚相连;其他版本可能会有所不同。

  1. 从编程菜单中打开 Arduino IDE。

  2. 验证板已连接并被检测到。

  3. 在 Arduino IDE 菜单上,转到工具并将鼠标悬停在电路板上。你应该看到 Arduino Uno 被选中。

  4. 现在悬停在串行端口上。它应该显示类似于/dev/ttyUSB0的内容。如果您的 Pi 分配了不同的端口,您的端口可能会有所不同。重点是那里有东西,而且检查过了。

  5. 通过单击菜单外的某个位置关闭工具菜单。

  6. 输入以下代码:

    int ledPin = 13;
    
    void setup() {
            pinMode(ledPin, OUTPUT);
    }
    
    void loop() {
            digitalWrite(ledPin, HIGH);
            delay(1000);
            digitalWrite(ledPin, LOW);
            delay(1000);
    }
    
    
  7. 将文件另存为blink_test

  8. 单击复选框图标以编译草图。

  9. 如果出现任何错误,请确保您输入的代码是正确的。请记住,与 Python 不同,您必须以分号结束每一行。和 Python 一样,大小写很重要。

  10. 当一切编译正确时,单击箭头图标(指向右侧)。这将草图上传到 Arduino。

等待几秒钟,让它上传。之后,您应该会看到连接到引脚 13 的 LED 闪烁。

恭喜你,你刚刚完成了你的第一个 Arduino 程序。另外,你是从你的 Pi 做到的。

素描剖析

我们刚刚写的草图不是最复杂的,但它确实说明了 Arduino 草图的基本结构。

int ledPin = 13;

我们首先创建一个名为ledPin的整数变量,并给它指定数字 13。给变量起一个有意义的名字是一个好习惯,即使程序很短,只有一个变量。

void setup() {

然后我们创建了一个名为setup()的函数。这个函数和loop()函数存在于每个 Arduino 草图中。setup功能是你放置预备代码的地方,比如打开串口,或者,正如我们在这个草图中所做的,定义我们如何使用管脚。setup功能在程序开始时运行一次。

pinMode(ledPin, OUTPUT);

setup函数中,我们有一个单独的命令。pinMode函数告诉编译器如何使用一个管脚。在这种情况下,我们声明ledPin(值为 13)为输出引脚。这告诉编译器我们正在发送信号,并且我们不期望通过这个管脚接收信号。

然后,在开始我们的loop函数之前,我们用右括号关闭setup函数。

void loop() {

loop功能是 Arduino 草图唯一需要的元素。顾名思义,loop是连续重复运行,直到板卡断电,或者板卡复位。它相当于 Python 中的while true:命令。loop函数中的任何代码都会以处理器所能处理的最快速度重复。

digitalWrite(ledPin, HIGH);

loop函数中,我们有闪烁 LED 的代码。我们首先使用digitalWrite功能将引脚设置为高电平状态。同样,我们传递它ledPin和我们想要设置的状态——在本例中是高。

delay(1000);

下一行在执行下一个命令之前增加了 1,000 毫秒或 1 秒的延迟。

digitalWrite(ledPin, LOW);

一秒钟延迟后,我们使用与设置高电平相同的命令digitalWrite将引脚设置为低电平状态。然而,这一次,我们通过它的常数低。

delay(1000);

同样,我们引入了一秒钟的延迟。因为这是loop功能中的最后一个命令,所以延迟后我们返回到loop功能的开始。直到我们拔掉 Arduino 或上传另一张草图。

Arduino 语言简介

如前所述,Arduino 编程语言是从编程语言派生出来的。反过来,编程植根于 C 语言。如果你熟悉 C 语言的编码,Arduino 很容易使用。Arduino 中的许多功能、语法和快捷键与 c 语言中的一样好用。

其余的,你很快就能理解。请记住,Arduino 不是 Python,当您使用它时,它的行为会有很大不同。

例如,Arduino 比 Python 更不关心空白和格式,Python 使用缩进来表示代码块。在 C 语言中,代码块是用花括号{}定义的。也就是说,你不能完全忽略空白。在行首多一个空格会导致无穷无尽的挫败感。

另一个让初学者和经验丰富的程序员都感到沮丧的关键区别是行终止。在 Python 中,您只需移动到下一行,不需要终止符。然而,在 Arduino 和 C 中,行以分号结束。如果编译器期望的地方没有分号,就会出现错误。这是初学者最常犯的错误。如果您的代码无法编译,首先要寻找的是丢失的分号。

Python 和 Arduino 的一个共同点是区分大小写。记住大小写很重要。intPinintpin不一样。如果您的代码没有正确编译或没有如预期的那样运行,这是第二件要寻找的事情。

包括其他文件

与 Python 非常相似,有时您需要包含其他文件或库。当您向 Arduino 添加传感器、电机或其他设备,并且需要将设备库添加到您的代码中时,这是最有可能的。

Arduino 使用 C 和 C++方法,通过#include指令从外部文件添加代码。下面一行包括标准伺服库:

#include <Servo.h>

像所有指令一样,include的语法略有不同。注意,这一行的末尾没有分号。分号会导致错误,并且代码不会编译。另外,include关键字前面有一个#(散列)。

变量和数据类型

像 Python 一样,Arduino 拥有所有常见的数据类型,尽管它们的行为可能略有不同。Python 和 Arduino 最大的区别之一是,你必须在使用变量之前声明它。一个很好的例子是for循环。在 Python 中,您可以这样做:

for i in range (0, 3):

在 C 和 Arduino 中,循环如下:

for (int i = 0; i < 3; i ++) {... }

这些说法大相径庭。我将在本章后面解释for循环语法。

这里要注意的关键是,在 Python 中,i变量是在没有类型的情况下创建的,当第一个值 0 赋给它时,它就变成了一个整数。在 Arduino 中,在给变量赋值之前,你必须告诉编译器这个变量是什么;否则,您会收到类似以下内容的错误:

Error: variable i not defined in this scope

声明变量的规则与 Python 相同,最佳实践也相同。

  • 变量只能包含字母、数字和下划线。
  • 它们区分大小写;variableVariable不一样。那以后会咬你一口的。
  • 不要使用 Python 关键字。
  • 用尽可能少的字符使变量名有意义。
  • 使用小写的 L 和大写的 O 时要小心,它们看起来很像 1 和 0,会导致混淆。我不是说不要使用它们;确保你清楚自己在做什么。强烈建议不要将它们用作单字符变量名。

字符和字符串

琴弦有三种味道;字符、作为字符数组的字符串和作为对象的字符串。每一个都以截然不同的方式处理。

字符(char)是存储为 ASCII 数值的单个字母数字字符。记住,计算机是以 1 和 0 工作的,所有的东西最终都会被分解成以 1 和 0 存储的数字。ASCII 码是代表单个字母数字字符的数值。例如,字母a实际上是 ASCII 码 97。即使不可见的字符也有 ASCII 表示。回车的 ASCII 码是 13。您经常会看到使用与char函数相同的符号来编写这些函数,例如char(13)

一个字符串可以用两种不同的方式处理。处理从 C 继承的字符串的本机方法是字符数组。您可以像这样声明这种类型的字符串:

string someWord[7];

或者

string someWord[] = "Arduino";

这将创建一个由 10 个字符组成的字符串,存储为一个数组。我们很快会学到更多关于数组的知识,但它们大致相当于 Python 列表。要访问这种类型的字符串中的字符,可以使用它在数组中的位置。someWord[0]示例返回字符 a。

字符串对象

尽管有时您可能希望以我刚才解释的方式操作字符和字符串,但 Arduino 提供了一种更方便的处理字符串的方式:String对象。注意大写的 s。

String对象提供了许多处理文本和将其他值转换成字符串的内置方法。使用简单的数组操作可以很容易地重新创建这些函数。String对象只是让它变得更容易;然而,如果你不打算做大量的字符串操作,这可能是多余的。

对字符串操作有用的函数的例子有trim()toUpperCase()toLowerCase()

有几种方法可以创建一个String对象。因为它是一个对象,所以您必须创建一个String对象的实例。对象的实例化方式通常与声明任何其他变量的方式相同。事实上,由于所有的数据类型本质上都是对象,所以完全相同。例如,这就是如何初始化一个名为myStringString对象的实例:

String myString;

或者

String myString = "Arduino";

民数记

像 Python 一样,有几种可用的数字格式。最常见的是整数(int)和小数(浮点)。您偶尔会使用布尔类型和一些其他类型。

整数表示–32,768 到 32,767 之间的 16 位数字。无符号整数可以保存 0 到 65,535 之间的正值。长整数(long)是一个从–2,147,483,648 到 2,147,483,647 的 32 位数字。所以根据你需要的数量,你有几个选择。

小数或非整数存储为浮点类型。浮点是从–3.4028235E+38 到 3.4028235 e+38 的 32 位数字。像 Python 一样,Arduino 中的浮点不是原生的,只是近似的。但是它们在 Arduino 中比在 Python 中更精确。

以下代码说明了如何在 Arduino 中创建数字变量:

int myNumber;
int myNumber = 10;
long myLongInt;
long myLongInt = 123456;
float myFloat;
float myFloat = 10.1;

一定要注意每行末尾的分号。除了代码块以外,每一行代码都必须以分号结束。

数组

如前所述,数组本质上与 Python 中的列表相同。它们用括号([ ])表示。在数组中寻址一个值的工作方式与在 Python 中完全一样。Arduino 数组也是从零开始的,这意味着数组中的第一个值位于位置 0。

下面的示例创建一个数组,遍历这些数组,然后将一些值输出到串行端口。

  1. 在 Arduino IDE 中创建新的草图。

  2. 将草图另存为array_example

  3. 更新代码,如下所示:

    int numbers[5];
    int moreNumbers[5] = {1,2,3,4,5};
    
    void setup() {
      // put your setup code here, to run once:
    Serial.begin(9600);
    }
    
    void loop() {
      // put your main code here, to run repeatedly:
    for(int i = 0; i < 5; i++){
      Serial.println(numbers[i]);
      }
    
    for(int i = 0; i < 5; i++){
      numbers[i] = moreNumbers[i];
    }
    
    for(int i = 0; i < 5; i++){
      Serial.println(numbers[i]);
      }
    
    numbers[1] = 12;
    
    for(int i = 0; i < 5; i++){
      Serial.println(numbers[i]);
      }
    }
    
    
  4. 保存文件。

  5. 将草图上传到你的 Arduino。

  6. 单击工具➤串行监视器。

控制结构

像 Python 一样,Arduino 提供了几个结构来给你的代码增加一些控制。这些应该相当熟悉,因为它们与 Python 中的对应部分非常相似。当然,语法是不同的,你需要注意你的分号和括号。

如果和否则

这通常被认为是最基本的构造。它只是允许您根据布尔条件的结果执行代码。如果条件评估为真,则代码执行;否则,程序跳过代码并执行下一个命令。下面是一个if语句的例子:

if(val == 1){doSomething();}

在这个例子中,我们简单地评估了变量val的内容。如果val包含整数 1,则执行括号内的代码;否则,程序跳过代码并继续下一行。

整个子句不需要,并且通常不局限于一行。一般来说,即使括号中的代码只有一行,我也会将语句扩展为使用多行。我只是觉得这更容易阅读。这段代码在功能上与前面的示例相同。

if(val == 1){
        doSomething();
        }

您可以使用else语句计算多个值,它的工作方式与您预期的完全一样。您只是告诉编译器,如果前一个条件的计算结果为 false,就要计算每个连续的条件。

if(val == 1){
        doSomething();
}
else if(val == 2){
        doSomethingElse();
}
else if(otherVal == 3){
        doAnotherThing();
}
else {
        doAlternateThing();
}

这段代码的第一部分与前面的例子相同。如果val的值是 1,那么做点什么。如果这个条件为假,val不为 1,那么检查一下是否为 2。如果是,那就做点别的。如果也不成立,那么检查otherVal的值。如果是 3,那么做另一件事。最后,如果前面的条件都不成立,那么执行下面的代码。

最后的else语句是不必要的。您可以省略这个语句,代码会继续运行后面的任何代码。最后一个else语句是针对那些你只希望在所有其他条件都不成立的情况下运行的代码。

另外,请注意第二条else/if语句。您不必为另一个条件计算同一个变量。任何评估为 true 或 false 的运算都是有效的。

while 循环

只要条件为真,循环就会重复执行一段代码。在 Python 中,我们用它来创建一个连续的循环,不断地执行我们的程序。这种做法在 Arduino 中是不必要的,因为标准的Loop()函数提供了这种功能。

if语句一样,while评估一个条件。如果条件评估为真,则执行代码块。一旦代码块执行,它将再次计算条件。如果条件仍然为真,则再次执行代码块。这将持续到条件评估为假。因此,确保在条件中计算的值在代码块中得到更新非常重要。

这是一个while循环的例子:

int i = 0;

while(i < 3){
        doSomething();
        i++;
}

在这个例子中,我们在进入while循环之前创建了一个值为 0 的整数。while语句计算值 I。由于它当前是 0,小于 3,所以它执行代码块。在代码块中,如果 I,while语句再次计算这个值,我们就增加这个值。这次是 1,仍然小于 3,所以再次执行代码块。这一直持续到i的值增加到 3。因为 3 不小于 3,所以while循环不执行代码块就退出。

像所有其他循环一样,while循环正在阻塞。这意味着只要条件评估为真,代码块就执行,防止任何其他代码被执行。

此功能通常用于防止代码在条件出现之前运行,以防止以后出现错误或意外结果。例如,如果您的代码需要串行连接才能继续,您可以将此代码添加到您的程序中:

Serial.begin(9600);
while(!Serial){}

Serial函数是标准 Arduino 库的一部分。它只是检查串行连接是否可用。如果串行连接已经建立,Serial评估为真。然而,它前面的惊叹号(!)表示不是。所以我们说,“只要没有串行连接,就执行这段代码。”代码块为空,因此没有要运行的代码。结果是代码停止,直到串行连接可用。

对于循环

while循环一样,for循环重复执行一个代码块,直到条件评估为真。这两者的区别在于,for循环也定义和转换被评估的变量。一般来说,这只是设置一个整数作为计数器,根据设置的阈值评估该值,然后递增该值。随着每一个增量,代码块被执行,直到条件不再评估为真;例如:

for(int i = 0; i < 3; i++){
        doSomething();
}

在这个例子中,我们声明了一个名为i的整数。只要 I 小于 3,我们就希望继续循环代码块。每次执行代码时,I 的值都会增加 1,直到 I 的值为 3。因为 3 不小于 3,所以循环在不执行代码块的情况下退出。

当您希望执行一段代码特定的次数时,这很有用。您也可以使用递增的值。例如,如果我们希望引脚 13 上的 LED 逐渐变亮,而不是简单地打开,我们可以这样编码:

pinMode(11, OUTPUT);
for(int i = 0; i < 255; i++){
        analogWrite(11, i);
}

首先,我们告诉 Arduino,我们想使用引脚 13 作为输出引脚。稍后您将了解更多关于使用大头针的信息。然后我们设置我们的for循环,将i的值从 0 增加到 254。然后i的值被写入引脚 13,设置 PWM 值。回想一下上一章,PWM 值通过确定给定周期内引脚为高电平的频率来控制 LED 的亮度。因此,我们有一个增加到最大亮度的 LED。

当我们开始处理引脚时,我们实际上编写了 LED 衰减代码。

功能

像 Python 一样,Arduino 允许你通过函数将代码分解成更小的部分。Arduino 函数与 Python 中的函数非常相似。当然,语法是不同的。但是,使用这两种方法,您需要声明函数名,列出任何需要的参数,并提供调用函数时要执行的代码块。

您已经熟悉了 Arduino 函数的语法。Arduino 草图中的setuploop块都是函数。唯一的区别是这些是在运行时自动调用的系统函数。如果你熟悉 C 或 C++,它们类似于这些语言的main()函数。

当你有一段可能要在多个地方使用的代码时,你就可以使用函数。这样,您只需编写一次,无论您从哪里调用它,它总是执行相同的操作。

函数的一般语法如下所示:

returnType functionName(parameterType parameterName){
        doSomething();
}

引导您完成函数的创建和使用可能更好也更容易。

在本练习中,我们将创建一个简单的函数,将两个数字相加。这不是一个特别实用的函数,但是它提供了一个如何创建函数的例子。

  1. 在 Arduino IDE 中创建新的草图。

  2. 将草图另存为function_example

  3. 将代码更新为:

    int a = 1;
    int b = 2;
    int val;
    int answer;
    
    int add_vars(){
      val = a+b;
      return val;
    }
    int add_params(int p1, int p2){
      val = p1+p2;
      return val;
    }
    
    void printVal(){
      Serial.println(val);
    }
    
    void setup() {
      // put your setup code here, to run once:
      Serial.begin(9600);
    }
    
    void loop() {
      // put your main code here, to run repeatedly:
      add_vars();
      printVal();
    
      add_params(a,b);
      printVal();
    
      answer = add_vars();
      Serial.println(answer);
    
      a++;
      b++;
    }
    
    
  4. 将草图上传到你的 Arduino。

  5. 从“工具”菜单打开串行监视器。

在本练习中,我们创建了三个函数。前两个函数返回一个类型为int的值。因此,我们在函数名前面加上数据类型int。第三个函数不返回数据;它只是执行一个任务,所以前面有void

第一个函数add_vars(),将两个全局变量相加。这强调了全局变量的好处和危险。程序中的任何代码都可以操作全局变量。这是对相同数据执行任务,然后将数据从一个函数传递到另一个函数的简单方法。但是,您必须意识到,您对变量所做的任何更改都会应用到使用该变量的任何地方。

一个更安全的替代方法是在函数中使用参数。这样,您就有了更多的控制权,因为您正在提供这些值。第二个函数add_params()演示了这一点。参数是作为函数声明的一部分创建的。我们提供了每个变量的数据类型以及函数中使用的变量名。因此,这就像声明一个变量,除了在运行时函数被调用时赋值。

最后一个函数不返回任何数据,也不需要任何参数。在这个特殊的例子中,我们将全局变量val的值打印到串行端口。

使用大头针

Arduino 的主要用途是与其他组件、传感器或其他设备连接。为此,我们需要知道如何与引脚交互。Arduino 的引脚直接连接到 AVR 处理器的核心。

Arduino 提供对 14 个数字引脚、6 个模拟引脚、6 个硬件 PWM 引脚、TTL 串行、SPI 和双线串行的访问。我强调硬件 PWM,因为任何数字或模拟引脚都可以用于软件 PWM。我不会在本书中涵盖所有这些功能,但是我建议您花时间了解它们。

我们将看看您的基本数字和模拟输入和输出。这些是您最常用的功能。

在使用任何管脚作为输入或输出之前,必须首先声明如何使用它。这是使用pinMode()功能完成的。为此,您只需提供 pin 码和模式。例如,此代码将引脚 13 设置为输出引脚:

pinMode(13, OUTPUT);

我经常使用变量来表示 pin 号。这使得识别您在代码中所做的事情变得更加容易;例如:

int servoPin = 11;
int LEDPin = 13;

现在,当我需要引用一个引脚时,就更容易理解了。

pinMode(LEDPin, OUTPUT);

数字操作

现在我们已经定义了 pin,我们可以开始使用它了。

与 Python 一样,可以通过将引脚设置为高或低来打开或关闭引脚。这是通过使用digitalWrite()函数来完成的,使用该函数,您可以提供 pin 号以及高电平或低电平;例如:

digitalWrite(LEDPin, HIGH);

pinMode()为例,这将使引脚 13 变为高电平或开启。

同样,您可以通过将引脚设为低电平来关闭引脚。

另一方面,您可以使用digitalRead()读取引脚的当前状态。为此,首先必须将模式设置为输入。

int buttonPin = 3;
int val;
pinMode(buttonPin, INPUT);
val = digitalRead(buttonPin);

这段代码片段将值 3 赋给buttonPin变量,我们创建一个变量来存储结果。然后,它将引脚模式设置为输入,以便我们可以读取它。最后,我们将引脚 13 的值读入val变量。

模拟输入

模拟输入的工作方式略有不同;虽然可以使用任何 IO 引脚进行数字操作,但只能使用指定的模拟引脚进行模拟输入。正如我在 Python 介绍中所讨论的,微控制器不能真正模拟。不管怎样,信号必须在模拟和数字之间转换。对于模拟输出,这是通过脉宽调制(PWM)完成的。对于模拟输入,我们使用模数转换器(ADC)将模拟信号转换为数字信号。这是一项硬件功能,因此必须在特定的引脚上执行。对于 Arduino Uno,这些引脚是 A0 至 A5。

由于这些引脚专用于模拟输入,因此没有必要将其声明为输入。我仍然建议这样做,因为这表明这些引脚正在使用中。

analogRead()函数用于读取管脚;例如:

val = analogRead(A0);

这将 A0 的值赋给变量val。这是一个 0 到 1023 之间的整数值。

模拟输出(PWM)

PWM 的工作方式与 Python 中的基本相同。在指定的管脚上,可以提供 0 到 255 之间的值来改变管脚的输出。值 0 相当于数字低电平或关闭;而值 255 类似于数字高或开。因此,值 127 提供 50%的占空比,大致相当于一半功率。

使用 Arduino,您可以使用analogWrite()来设置引脚的 PWM 信号。在 Arduino Uno 上,PWM 引脚为 5、11、12、15、16 和 17。以下代码片段将引脚 11 的输出设置为大约 25%。

int PWMPin = 11;
pinMode(PWMPin, OUTPUT);
analogWrite(PWMPin, 64);

脉冲 LED

在本练习中,我们将制作一个 LED 脉冲。引脚 13 不是 PWM 引脚,所以这次我们不能使用内置 LED,这意味着是时候断开试验板和一些跳线了。

该电路

为了连接电路,我们需要一个 220 欧姆的电阻、一个 5V 的 LED、你的 Arduino、试验板和一些跳线。参见图 5-8 来连接该练习。

A457480_1_En_5_Fig8_HTML.jpg

图 5-8

LED fade exercise circuit layout

  1. 将 LED 连接到试验板。
  2. 连接电阻,使其一端连接到与 LED 长引脚共享的通道。
  3. 将一根跳线从二极管的另一个引脚连接到 Arduino 上的 GND 引脚。
  4. 将一根跳线从电阻器的另一端连接到 Arduino 上的第 11 针。
代码

之前我们在一个for循环示例中使用了analogWrite()。现在我们编写代码在 Arduino 上实现这个例子。

  1. 在 Arduino IDE 中创建新的草图。

  2. 将草图另存为PWM_Example

  3. 将代码更新为:

    int PWMPin = 11;
    
    void setup() {
      // put your setup code here, to run once:
      pinMode(PWMPin, OUTPUT);
    }
    
    void loop() {
      // put your main code here, to run repeatedly:
      for(int i = 0; i < 255; i++){
        analogWrite(PWMPin, i);
      }
    
      for(int i = 255; i >= 0; i$$){
        analogWrite(PWMPin, i);
      }
      delay(100);
    }
    
    
  4. 保存草图并上传到 Arduino。

试验板上的 LED 现在应该会闪烁。要改变脉冲的速率,改变延迟功能中的值。

对象和类

创建对象和类超出了本书的范围。你很少需要在 Arduino 中创建一个。但是,您经常使用其他库中的对象或类。

对象的实例化方式通常与声明任何其他变量的方式相同;你告诉编译器对象的类型,后跟引用它的变量的名字。

ObjectType variableName;

一旦声明,您就可以访问该类的所有属性和方法。伺服类就是一个很好的例子。这是一个带有 Arduino 的标准库。以下代码片段创建了一个伺服对象,并将其附加到第 12 号插针:

#include <Servo.h>

Servo myServo;
myServo.attach(12);

首先,我们包括伺服库。一旦包含了伺服库,我们可以很容易地创建一个Servo类的实例。在本例中,我们创建了一个名为myServoservo对象。一旦创建了对象,我们就可以使用attach()方法来分配引脚 12 来控制伺服。

连续的

Arduino 上有几个串行通道。我们在 Raspberry Pi 和 Arduino 之间使用 USB 连接。这是目前为止两者之间最简单的沟通方式。

连接到串行

要使用串行通信,您必须首先启动它。为此,使用Serial.begin(baudRate)。例如,此行以 9600bps 的波特率启动串行连接:

Serial.begin(9600);

您选择的波特率完全取决于您和您的需求。重要的是,它与计算机连接的波特率相匹配。因此,当您初始化 Pi 上的串行连接时,您需要确保它们匹配。我稍后将讨论如何建立这种连接。

要验证串行连接是否成功,您可以查询Serial关键字。Serial 是一个布尔对象,表示串行连接是否可用。如果连接可用,则为真;否则就是假的。使用Serial其实有几种方法。您可以将它用作一个if语句的布尔条件,并将相关代码放在if语句的代码块中。或者,你可以用它作为一个while循环的布尔条件。

这里有两种方法来检查串行连接。如果有可用的代码,只运行代码。

if(Serial){
        doSomething();
}

while(Serial){
        doSomething();
}

如果串行连接可用,第一个块执行代码,然后继续执行if语句后面的代码。只要有连接,第二个块就会持续运行代码。任何跟随while循环的代码将不会运行,直到串行连接终止并且循环退出。

第三种方法是在连接不可用时暂停所有代码的运行。这是我们之前见过的另一个while循环。

while(!Serial){}

这使用了“非”运算符,或感叹号(!)。为了使条件评估为真,它必须不满足标准。在这种情况下,只要连接不可用,就执行块中的代码。但是,由于没有代码,它只是暂停程序,直到有可用的代码。

发送串行数据

我们要做的大部分工作只是简单地打印到串口。事实上,这就是我们在前面的例子中所做的。方法Serial.println()将括号内的数据发送到串口。Arduino IDE 中的串行监视器允许您查看此输出。

要将数据写入串行流,通常使用串行打印方法之一。Serial.print()将括号中的内容打印到串行流中,不带新的行结束符。这意味着使用这种方法打印的所有内容都出现在串行监视器的同一行上。

Serial.println()方法包括新的行结束符。所以用这种方法打印出来的东西后面都是新的一行。

接收串行数据

当然,串行端口的工作方式也是相反的。您可以使用 serial 对象的几种方法从 Pi 中读取串行流。许多从串行读取数据的方法都是针对单个字节的。如果您刚刚开始,这可能会令人困惑和麻烦。如果您熟悉并习惯于处理单个字节的数据,那么Serial.read()Serial.readByte()和其他一些可能会有用。

然而,这不是我们要用的。为了使事情简单一点,我们将使用Serial.parseInt()Serial.readString()方法。当从串行流中读取时,这两种方法完成了大部分工作。

Serial.parseInt()读取传入的串行流并返回;但是,它不会一次解析所有的整数。当您第一次调用它时,它返回遇到的第一个整数。下一个调用返回下一个整数。每次迭代返回找到的下一个整数,直到到达行尾。

我们来看看parseInt()是如何工作的。在下面的代码中,Arduino 等待接收来自串行流的输入。然后,它遍历输入并解析出整数,将每个整数打印在新的一行上。

  1. 在 Arduino IDE 中打开一个新的草图。

  2. 将草图另存为parseInt_example

  3. 输入此代码:

    int val;
    
    void setup() {
      // put your setup code here, to run once:
      Serial.begin(9600);
    }
    
    void loop() {
      // put your main code here, to run repeatedly:
      while(Serial.available() > 0){
        val = Serial.parseInt();
        Serial.println(val);
      }
    }
    
    
  4. 将草图上传到你的 Arduino。

  5. 打开串行监视器。

  6. 在串行监视器顶部的数据输入字段中,输入 1,2,3,4。请确保用逗号分隔每个值。

  7. 单击发送。

串行监视器将每个整数写在新的一行上。如果您输入一个字母字符,它会打印一个 0,因为它是一个字母数字字符,而不是一个整数。

Serial.readString()将串行流中的整行作为字符串读取。这可以分配给一个String变量以备后用。如果您正在向 Arduino 发送文本信息,这种方法非常有效。但是,它很慢,并且您会注意到发送一行的时间与接收、处理和提供该行的时间之间有很大的延迟。

Arduino 到 Pi,然后再回来

你需要知道一些关于串行通信的知识,因为这是我们在 Raspberry Pi 和 Arduino 之间通信的方式。Pi 和 Arduino 处理串行通信的方式不同。

我没有在 Python 这一章中讨论串行,因为结合 Arduino 进行讨论是很重要的。因此,在所有 Arduino 编码之后,你可能想跳回到第三章来快速回顾一下 Python。

我已经谈到了如何在 Arduino 上打开串行连接。树莓派只是稍微复杂一点。首先,串行通信不是默认框架的一部分。所以,我们需要安装它。安装后,我们的代码需要导入串行库。一旦完成,我们就创建了一个 serial 类的实例,它让我们可以访问我们需要的方法。

安装 PySerial

PySerial 包提供了串行功能。要使用它,首先需要确保它安装在 Python 实现中。

  1. 在你的 Raspberry Pi 上,打开一个终端窗口。
  2. 类型python –m pip install pyserial。如果尚未安装 PySerial 包,这将安装它。
  3. 类型python。这将在终端中开始一个新的 Python 会话。
  4. 类型import serial。这将验证您的 PySerial 版本。

现在 PySerial 已经安装好了,我们可以在我们的程序中使用它了。

要在 Python 中使用 serial,我们需要导入库并创建一个连接。以下代码片段可能出现在大多数与 Arduino 交互的脚本中:

import serial
ser = serial.Serial('/dev/ttyAMC0', 9600)

在 Python 中创建串行连接类似于我们在 Arduino 中所做的。最大的不同是我们将串行对象赋给了一个变量;在这种情况下,ser。在初始化调用中,我们提供 Arduino 所在的端口以及我们正在连接的波特率。同样,确保这与您在 Arduino 上设置的波特率相匹配。如果这些不匹配,您会得到奇怪的字符和意想不到的结果——如果您得到任何东西的话。

向 Raspberry Pi 发送数据

这与其说是关于向 Pi 发送数据,不如说是关于 Pi 如何接收数据以及如何处理数据。

在 Pi 上接收串行数据的最简单方法是使用串行对象的readLine()方法。这会从串行流中读取字节,直到它到达新的行字符。然后这些字节被转换成一个字符串。线路上发送的所有数据都存储在一个字符串中。根据从 Arduino 发送数据的方式,您可能需要使用split()方法将数据解析成一个元组。

值得注意的是,readLine()方法继续读取串行流,直到接收到一个新的行字符。如果不从 Arduino 发送,Pi 会继续尝试读取数据。为了帮助防止锁定您的程序,您可能希望在尝试readLine()之前设置超时间隔。这可以通过在创建连接时添加超时参数来实现。以下代码行创建了一秒钟超时的串行连接:

ser = serial.Serial('/dev/ttyAMC0', 9600, timeout=1)

我在 Pi 和 Arduino 之间发送数据的首选方法是通过一系列逗号分隔的值。根据项目的复杂程度,我可以直接读取,其中传递的每个值对应一个特定的变量。这样做的好处是非常简单。我所要做的就是将串行流解析成整数,并将每个整数按顺序分配给各自的变量,以备后用。

在更复杂的项目中,我可能会成对或成组地发送值。解析时,第一个整数通常表示消息的功能或设备;第二个是分配给变量的值。

在 Arduino 中,我简单地将值和它们的逗号分隔符写在许多以Serial.println()结尾的Serial.print()命令中,以确保该行被正确终止。

在 Pi 上,我使用readLine()方法将整行捕获为一个字符串,然后使用split()方法将该字符串解析为一个元组。可以根据需要将元组进一步解析为单个变量。

为了说明这一点,让我们创建一个简单的程序,每隔 500 毫秒从 Arduino 向 Raspberry Pi 发送一个数字序列。这足够频繁,不会超时。

在 Pi 上,我们解析这些值,并将它们分配给单独的变量。这是将传感器读数从 Arduino 发送到 Pi 的常见用例。

为此,我们必须编写两个程序:一个用于 Arduino,一个用于 Pi。让我们从 Arduino 开始。

  1. 在 Arduino IDE 中创建新的草图。

  2. 将草图另存为Arduino_to_Pi_example

  3. 输入以下代码:

    int a = 1;
    int b = 2;
    int c = 3;
    
    void setup() {
      // put your setup code here, to run once:
      Serial.begin(9600);
    }
    
    void loop() {
      // put your main code here, to run repeatedly:
      while(!Serial){};
      Serial.print(a); Serial.print(",");
      Serial.print(b); Serial.print(",");
      Serial.println(c);
      delay(500);
      a++;
      b++;
      c++;
    }
    
    
  4. 保存草图并上传到 Arduino。

  5. 在空闲时打开一个新的 Python 文件。

  6. 将文件另存为Arduino_to_pi_example.py

  7. 输入以下代码:

    import serial
    
    ser = serial.Serial('/dev/ttyACM0',9600,timeout=1)
    
    while 1:
        val = ser.readline().decode('utf-8')
        parsed = val.split(',')
        parsed = [x.rstrip() for x in parsed]
        if(len(parsed) > 2):
            print(parsed)
            a = int(int(parsed[0]+'0')/10)
            b = int(int(parsed[1]+'0')/10)
            c = int(int(parsed[2]+'0')/10)
        print(a)
        print(b)
        print(c)
        print(a+b+c)
    
    
  8. 保存并运行文件。

在空闲 shell 窗口中,您应该会看到类似如下的输出:

['1','2','3']
1
2
3
6

我们在这里做了一些需要回顾的 Python 魔术。

首先,我们导入串行库并打开一个串行连接。一旦连接打开,我们就进入了永久的while循环。在那之后,我引入了一些新的元素,

val = ser.readline().decode('utf-8')

我们阅读来自串行流的下一行。但是,该字符串是作为字节字符串检索的,这与标准字符串的工作方式不同。为了更容易使用,我们使用了decode()方法将字符串从字节字符串转换为标准字符串。这允许我们使用 string 类的方法来处理行。

parsed = val.split(',')

接下来,我们将字符串解析成一个列表。因为我们使用逗号将我们的数字与 Arduino 分开,所以将其提供给split()方法。然而,现在列表中的最后一个元素包含了行尾字符/n/r。我们不想要那些角色。

parsed = [x.rstrip() for x in parsed]

这一行重建没有额外字符的解析列表。rstrip()方法从字符串中删除任何空白。所以,这一行所做的是遍历列表中的每个成员,并应用rstrip()方法。留给我们是字符串形式的数字列表。

if(len(parsed) > 2):

两块板之间的串行通信面临的挑战之一是丢包。这在我们重置 Arduino 时尤其普遍,每次我们建立新的串行连接时都会发生这种情况。这种丢失会导致串行字符串中丢失字符。

为了克服这个挑战,我们在这个脚本中测试了列表的长度。函数的作用是:返回列表中成员的数量。因为我们知道我们的列表至少需要包含三个数字,所以我们只想在这个条件为真的情况下运行剩余的代码。

print(parsed)

这一行只是将解析后的列表打印到 shell 窗口。

a = int(int(parsed[0]+'0')/10)
b = int(int(parsed[1]+'0')/10)
c = int(int(parsed[2]+'0')/10)

当我们将值赋给它们各自的变量时,Python 的最后一个魔术就完成了。这些行包括字符串和数字操作。

为了理解这里发生的事情,我们必须从中间开始,在这里我们将字符'0'添加到每个列表成员的末尾。我们这样做是因为,尽管我们之前做了努力,列表中可能仍然有空字符串。空字符串不能转换为整数,代码也不会编译。通过加上 0,我们确信这里有一个实际值。

然后,我们将该字符串转换为整数。但是,该整数现在在末尾附加了一个 0,使 1 读作 10,依此类推。为了对此进行调整,我们将该数字除以 10,得到一个浮点数。因为我们在寻找一个整数,所以我们必须将最终结果转换成一个int

代码的最后一部分只是将每个变量的值打印到 shell 窗口。最后一行用来证明我们操作的是整数而不是字符串。

向 Arduino 发送数据

在 Arduino 方面,向 Arduino 发送数据是一件相当简单的事情。然而,Python 稍微复杂一些。使用与前面相同的场景,我们需要将值放入一个元组,然后使用join()方法将元组中的值合并成一个字符串。然后,该字符串被写入串行连接。

在 Arduino 上,我们所要做的就是再次使用parseInt()将字符串分成三个独立的整数。

在本练习中,我们将向 Arduino 发送三个整数。在现实世界中,这些数字可能代表 LED 的颜色或亮度或伺服角度。然而,这将很难验证 Arduino 端发生了什么,因为我们不能使用串行监视器。为了克服这个问题,我们将要求 Arduino 对整数求和,并将结果返回给 Pi。这意味着两块板都在读写串行流。

还是那句话,让我们从 Arduino 这边开始。

  1. 在 Arduino IDE 中打开一个新的草图。

  2. 将草图另存为roundtrip_example

  3. 输入以下代码:

    int a = 0;
    int b = 0;
    int c = 0;
    int d = 0;
    
    void setup() {
      // put your setup code here, to run once:
      Serial.begin(9600);
    }
    
    void loop() {
      // put your main code here, to run repeatedly:
      while(!Serial){}
      if(Serial.available()>0){
        a = Serial.parseInt();
        b = Serial.parseInt();
        c = Serial.parseInt();
      }
    
      d = a+b+c;
      Serial.print(a); Serial.print(",");
      Serial.print(b); Serial.print(",");
      Serial.print(c); Serial.print(",");
      Serial.println(d);
    
      //delay(500);
    }
    
    
  4. 保存草图并上传到您的 Arduino。

  5. 在空闲时打开一个新的 Python 文件。

  6. 将文件另存为roundtrip_example.py

  7. 输入以下代码:

    import serial
    import time
    
    ser = serial.Serial('/dev/ttyACM0',9600,timeout=1)
    a = 1
    b = 2
    c = 3
    
    while 1:
        valList = [str(a),str(b),str(c)]
        sendStr = ','.join(valList)
    
        print(sendStr)
    
        ser.write(sendStr.encode('utf-8'))
    
        time.sleep(0.1)
    
        recStr = ser.readline().decode('utf-8')
        print(recStr)
    
        a = a+1
        b = b+1
        c = c+1
    
    
  8. 保存并运行文件。

在 Python shell 窗口中,您应该会看到如下输出:

1,2,3
1,2,3,6

输出继续增加,直到您停止程序。

这里有一些新的元素,但是,在大多数情况下,它与我们以前所做的没有什么不同。让我们来看看一些新元素。

第一个区别是我们导入了时间库。这个库提供了很多与时间相关的功能。在本练习中,我们对sleep()函数感兴趣。sleep()功能会在规定的秒数内暂停处理。正如您在我们的代码中看到的,我们希望暂停处理 0.5 秒。这为串行流的两端提供了处理缓冲区的时间。如果您注释掉那一行并再次运行程序,您会得到一些有趣的结果。试试看。

valList = [str(a),str(b),str(c)]

这里我们把变量放在一个列表中。在下一步中,当我们将元素连接成一个字符串时,整数需要是字符串。因此,我们在这里进行了转换。

sendStr = ','.join(valList)

接下来,我们使用 string 类的join()方法将列表转换成字符串。注意join()方法是如何附加到','字符串上的。join是 string 类的方法,不是 list 类的方法,所以必须从 string 中调用。因为该操作实际上是在列表上工作,而不是在字符串上,所以您必须提供一个字符串才能使它工作。在这种情况下,所提供的字符串是您希望在列表的每个成员之间使用的分隔符。它可以是任何字符,但是为了让parseInt()在 Arduino 端工作,字符必须是非字母数字的。

ser.write(sendStr.encode('utf-8'))

另一个值得注意的区别是我们使用write()方法向 Arduino 发送数据。这类似于 Arduino 中的Serial.println()方法。最大的区别是你必须在发送之前对字符串进行编码。

企鹅!企鹅

连接一个或多个传感器以检测机器人周围世界的常见用例。在下一个练习中,我们将在 Arduino 上设置我们的 HC-SR04 超声波测距仪,并将距离信息作为串行字符串发送回 Pi。为此,我们需要在两块电路板之间建立一个串行连接。Arduino 触发传感器,并像我们之前的研讨会一样,读取返回的脉冲。我们将计算距离,然后将结果发送给 Pi。

在 Pi 端,我们只需要一个程序来监听串口,然后打印它从 Arduino 读取的任何内容。

设置电路

设置电路再简单不过了。事实上,我们不使用试验板。我们将传感器直接连接到 Arduino 接头(见图 5-9 )。

  1. 将 VCC 连接到 Arduino 上的 5V 引脚。
  2. 将 GND 连接到 Arduino 上的一个 GND 引脚。哪一个都无所谓,但是 5V 引脚旁边有两个。
  3. 将 TRIG 连接到 Arduino 上的引脚 7。
  4. 将 ECHO 连接到 Arduino 上的第 8 针。

A457480_1_En_5_Fig9_HTML.jpg

图 5-9

Pinguino exercise circuit layout

代码

我们需要为两块板都编写代码,这样才能工作。在 Arduino 上,我们触发超声波传感器并捕捉返回信号。然后,我们将把它转换成厘米,并把数值打印到串行端口。

Pi 从串行端口读取该行,并将结果打印到 Python shell 窗口。

阿尔杜伊诺
  1. 打开一个新的草图窗口,并将其另存为serial_test

  2. 输入以下代码:

    int trig = 7;
    int echo = 8;
    int duration = 0;
    int distance = 0;
    
    void setup() {
            Serial.begin(9600);
            pinMode(trig, OUTPUT);
            pinMode(echo, INPUT);
    
            digitalWrite(trig,LOW);
    }
    
    void loop() {
            digitalWrite(trig, HIGH);
            delayMicroseconds(10);
            digitalWrite(trig, LOW);
    
            duration = pulseIn(echo, HIGH);
            distance = duration/58.2;
    
            Serial.write(distance);
    
            delay(500);
    }
    
    

保存草图并上传到 Arduino。

树莓派
  1. 打开一个新的空闲文件,并将其另存为serial_test.py

  2. 输入以下代码:

    import serial
    import time
    
    ser = serial.Serial('/dev/ttyAMC0', 9600)
    
    while 1:
            recSer = ser.readline().decode('utf-8')
            recSer.rstrip()
    
            distance = int(recSer + '0')/10
    
            print("Distance: " + str(distance) + "cm     ", end = '\r')
            time.sleep(0.5)
    
    
  3. 保存并运行文件。

现在,您应该可以在 Python shell 窗口中看到以厘米为单位的距离文本。

该代码输出来自单个超声波传感器的结果。在现实中,你的机器人应该有三个或更多的传感器以不同的角度指向前方。原因是,只要障碍物在机器人的正前方,超声波传感器就能很好地工作。如果机器人以倾斜的角度接近墙壁或其他障碍物,声音不会反弹回传感器。在不同的角度有一个以上的传感器允许机器人检测不在其正前方的障碍物。

摘要

将 Arduino 添加到 Raspberry Pi 为您提供了更广泛的可能性。与 Pi 本身相比,您可以添加更多的传感器和 led。好处包括增加模拟输入数量、更多 PWM 输出和更多数字输出。

Arduino 非常适合编程。如果你已经熟悉 C 或 C++,那么为 Arduino 编写代码应该是非常熟悉的。然而,记住 Arduino 和 Python 之间的区别是非常重要的。Python 不在行尾使用字符,但是 Arduino 用分号结束每一行。编写条件和循环涉及到更多一点的语法。代码块包含在花括号中。缩进在 Arduino 中并不重要,但是如果缩进关闭,Python 将不会编译。

尽管有这些不同,Arduino 还是简化了一些事情。串行通信不需要太多设置,串行命令是核心 Arduino 库的一部分。在 Python 中,你必须导入串行库。两者都使得对串行端口的写入相当简单。然而,Python 需要编码和解码成 utf-8 才有用。此外,Arduino 使用parseInt()方法使得从串行流中解析一行中的数字变得容易。用 Python 从一个字符串中取出一个数字需要一点小小的操作。

当你使用 Arduino 时,不要忘记社区支持是极好的。很少有其他人没有做过和记录的事情。还要记住,在 IDE 中,您有大量的示例代码形式的资源。利用这一点。随着您为更多设备添加更多库,您会发现更多示例草图对您有所帮助。

六、驱动电机

在第四章中,我们使用 Raspberry Pi 的 GPIO 引脚来控制 LED 并接收来自超声波传感器的信息。在第五章中,我们讨论了 Arduino,并讨论了为什么它是通用 GPIO 功能的更好选择。我们将超声波测距仪和 LED 连接到 Arduino,并学习如何在两块电路板之间传递数据。

但这并不意味着我们已经完成了 Raspberry Pi 的 GPIO 头。在这一章中,我们将使用 GPIO 引脚连接到一个叫做电机驱动器的电路板,该电路板用于与 DC 电机和步进机交互。我将介绍一些不同类型的电机,并讨论什么是电机驱动器,以及为什么它在我们的工作中很重要。

我们将把 DC 马达连接到马达控制器上,并编写一个小程序使它们转动。作为示例程序的一部分,我们将研究如何控制电机的速度和方向。我们还将了解为研讨会选择的特定电机控制器的一些属性。

您可能选择不使用建议的电机控制器,因此我们也将看看一个常见的替代方案:L298N 电机驱动器。许多制造商提供的驱动板旨在连接到 L298N H 桥控制器芯片的核心。但是因为这些板依赖 PWM 信号来设置速度,我们必须通过 Arduino 连接它。我将在这一章的结尾回顾这一切。

在本次研讨会结束时,你将拥有开始制造机器人所需的最后一个组件:运动。在第七章中,我们将把所有东西和底盘套件放在一起,让你的机器人动起来。

电机和驱动器

在讨论电机控制器之前,让我们花点时间看看我们在控制什么。我们使用的驱动器是为简单的 DC 电机设计的,尽管它也可以用来驱动步进电机。让我们在这一节看看驱动器和电机。

电机类型

马达将电能转化为旋转能。它们有许多不同的类型,它们为几乎所有移动的东西提供动力。最常见的电机类型是简单的 DC 电机,它甚至用于许多其他类型的电机。例如,伺服电机是一种将 DC 电机与电位计或其他反馈设备以及控制精确运动的传动装置结合在一起的设备,无论是角度运动还是方向运动。其他类型的电机包括步进电机,它使用电脉冲来控制非常精确的运动,以及无芯电机,它重新排列 DC 电机的典型部件以提高效率。

DC 汽车公司

DC 发动机由磁场中的一系列线圈组成。当电荷加在线圈上时,会使线圈绕着它们共有的轴旋转。简单的电动机的线圈围绕一个中心轴排列和连接。当轴和线圈旋转时,与轴接触的电刷保持电气连接。轴又从组件中伸出,以利用旋转力做功。图 6-1 显示了典型的 DC 电机。

A457480_1_En_6_Fig1_HTML.jpg

图 6-1

DC motor operation

你通常会发现这些电机连接到变速箱、皮带或链条上,以转速为代价来放大电机的扭矩。这样做是因为一个裸露的 DC 发动机可以产生很大的速度,但原始速度很少有用。

我们正在使用的发动机就是这种类型的。它们是附在齿轮箱上的简单的 DC 发动机。

无刷电机

另一种类型的电机将机械连接移动到磁体。线圈保持静止。当施加电荷时,磁铁绕着共同轴上的线圈旋转(见图 6-2 )。这样就不需要电刷了,所以被称为无刷电机。

在业余爱好领域,无刷电机通常与多旋翼飞机联系在一起。它们也广泛应用于其他需要高速和高效率的领域,例如 CNC(计算机数字控制)主轴。你可能熟悉 Dremel 工具或路由器;这两种设备都是主轴类型,使用无刷电机。

A457480_1_En_6_Fig2_HTML.jpg

图 6-2

Brushless motor operation

步进电机

到目前为止,我所讨论的所有马达都有一个或多个线圈,利用单一电荷工作。电荷可以是正的,也可以是负的,从而改变马达的方向。

步进电机则不同。步进器使用带有不同电荷的多个线圈(见图 6-3 ),将一次完整的旋转分成多个步进。通过操纵这些电荷,我们可以使马达移动并保持在某一步的位置上。这使得这些电机非常适用于数控机床、3D 打印机和机器人等应用中的有限控制。

A457480_1_En_6_Fig3_HTML.jpg

图 6-3

Stepper motor operation

伺服系统

伺服电机是一个移动到特定角度并保持该位置的电机。它们通常在任一方向上具有 45 至 90 度的最大旋转。他们通过在最终输出齿轮上连接一个电位计来实现这一点。电位计向内部控制板提供反馈。当伺服系统接收到信号时,通常是以 PWM 的形式,电机旋转直到电位计和信号达到平衡。

图 6-4 显示了一个典型的爱好伺服。

A457480_1_En_6_Fig4_HTML.jpg

图 6-4

Common servomotor

去掉限制器和电位计的伺服系统称为连续旋转伺服系统。它们用于需要扭矩的应用中。许多机器人由连续旋转伺服系统驱动。

这是一个一种爱好极大地有益于另一种爱好的例子。常见的爱好伺服最初用于爱好遥控飞机。由于大多数爱好者买不起昂贵的设备来控制他们的工艺,他们想出了如何大幅降低价格。这当然有助于我们这些机器人爱好者。

电机属性

在我们的项目中,有一些关于电机的事情需要记住。最重要的是电机的电气特性,特别是电压和电流。

电压

你已经对电压有些熟悉了,电压是运行一个设备所需要的电能的一种度量。Pi 由 5 伏电压供电,但在 3.3 伏电压下运行。Arduino 在 5 伏电压下运行,由 Pi 的 USB 端口供电。我们正在使用的马达是 6 伏的。保持这些电压是很重要的。如果你在一个 3.3 伏的设备上加 5 伏电压,你可能会损坏你的设备。

有专门设计的设备来帮助管理项目中的电压。电压调节器(升压或降压)保持恒定的电压。常见的 7805 5V 稳压器,取 6 到 12 伏,转换成 5 伏。多余的能量以热的形式消散,它们会变得非常热。

电压调节器非常适合电压供应,但对于转换器件中的 5 伏和 3.3 伏电压用处不大。为此,我们使用逻辑电平转换器,它需要来自两个器件的基准电压,但可以安全地转换器件之间的电压。

现在,您已经意识到了设备电压需求的差异。接下来,我们看看安培数。

安培数

安培数是电流的量度,或者是我们的设备运行所需的电压。最常见的类比是流水线中的水,其中电压是流水线的大小,安培数是流经流水线的水量。实际上,我喜欢将类比改为使用橡胶管。如果你试图通过一个橡胶管推出太多的水,就会发生不好的事情。

在电子领域,这通常用较小的单位毫安来衡量,通常记为 mA。例如,大多数设备的 USB 端口被限制在 800 毫安的功率。这恰好与我们选择的电机使用的功率相同;但是,它没有考虑功率尖峰。

设备上的电压有些被动。该设备使用你提供的电压,从不试图获取更多。安培数完全相反。一个设备渴望安培数,并继续汲取它所需要的,直到它满意地做它的工作,或超过可用的供应。

组件和设备需要一定的功率来工作。他们也有一个他们可以承受的最大功率。由于额外的电能转化为热量,如果你超过一个设备的最大耐受电流,它就会变热并死亡。有时非常壮观。

这个故事的寓意是“永远注意你正在画的水流。”这不仅适用于电机。led 因消耗大量电流而臭名昭著。

电机和放大器

众所周知,电机是耗电设备。他们不断地试图实现他们的目的:旋转。当电机上没有负载、重量或阻力时,它会以最小的电流消耗愉快地旋转。然而,开始增加电阻,电机汲取越来越多的电流,直到它达到它可以汲取的最大值,这被称为失速电流。失速电流实质上是当轴被物理限制移动时电机的安培数。

当电机启动、快速改变方向或遇到太大的旋转阻力时,它消耗的功率会急剧增加。如果这种突然的汲取对于供应来说太多了,那么一定有什么东西损坏了。我们拿一个 800mA 的源来说,比如一个 USB 插口;如果电机突然消耗 1 安培或更大的电流,USB 插孔可能会损坏。

电机驱动器

大多数微控制器、微处理器和电子设备只能处理少量电流。如果电流过大,它就会开始烧坏。因为电机通常很容易超过这个最大电流,所以通常不希望将任何大尺寸的电机直接连接到处理器。因此,我们将使用一种称为电机驱动器或电机控制器的设备。

电机控制器就是为此目的而设计的。它使用来自微控制器的低功率信号来控制大得多的电流和/或电压。在我们的例子中,我们使用电机控制器来控制来自 GPIO 引脚的 3.3 伏电压和 6 伏电压。我们通过一系列具有更大 1.2A (1,200mA)电流容差的元件来实现这一点,这些元件可以处理高达 3.0A (3,000mA)的短暂尖峰。

使用电机控制器

让我们看看两个电机控制器。第一个是阿达果的 DC &步进电机帽。该控制板专门设计用于安装在 Raspberry Pi 上。实用性和便利性的结合使它成为我对像我们这样的项目的首选。

另一个电机控制器是 L298N,它是一个 H 桥 IC。虽然 L298N 实际上是一个独立的组件——一个芯片,但有许多制造商已经构建了一个方便的分线板。当有人提到 L298N 电机控制器时,通常指的就是这种类型的电路板。本书中使用的是我在亚马逊上花 5 美元找到的通用版本。我的一些朋友说我为此付出了太多。

Adafruit DC &步进电机

本项目中的电机驱动器为 Adafruit 的一款,可在 www.adafruit.com/products/2348 买到。关于如何使用它的信息在 https://learn.adafruit.com/adafruit-dc-and-stepper-motor-hat-for-raspberry-pi 。事实上,我们要讲的大部分内容都来自这个 Adafruit 网站。

为我们的机器人选择这个设备有几个原因;更重要的是,它直接安装在树莓皮上,从而限制了在机器人上安装电子设备所需的面积。你很快就会知道,安装空间对大多数机器人来说都是非常珍贵的;尤其是如果你想保持它的紧凑。以下是使用此板的一些其他原因:

  • 它可以控制多达四个 DC 电机或两个步进电机。
  • 通信是通过 I2C 串行通道处理的,它允许多个器件堆叠在一起(这就是为什么我们在接头上使用较长的引脚)。
  • 因为它使用 I 2 C,它有自己专用的 PWM 模块来控制电机,所以我们不必依赖 Pi 本身的 PWM。
  • 它有四个 H 桥电机控制电路,电流为 1.2A,峰值电流为 3.0A,具有热关断功能,内部保护二极管可保护您的主板。
  • 有四个双向电机控制,带 8 位速度控制(0 至 255)。
  • 使用端子板可以轻松连接。
  • 有现成的 Python 库。

需要一些组件

该板以套件形式提供,需要焊接。如果您还没有这样做,您需要在继续项目之前组装它。请记住,我们为接头指定了更长的引脚,所以不要使用套件附带的引脚。

焊接练习时间到了。

有许多小引脚(40 个)需要焊接。如果你不熟悉焊接,你需要花点时间来学习。尽管焊接指导非常简单,但它超出了本书的范围。互联网上有很多有用的视频。我也强烈建议你找到你当地的创客空间。那里肯定有人能给你上一堂速成课。图 6-5 显示了我的简单焊接设置。

A457480_1_En_6_Fig5_HTML.jpg

图 6-5

Preparing to assemble the motor HAT

组装电机帽非常容易,虽然有焊接涉及。你可以在 Adafruit 网站 https://learn.adafruit.com/adafruit-dc-and-stepper-motor-hat-for-raspberry-pi/assembly 找到组装的详细说明。

在这个练习中,你需要一个烙铁和焊料。我建议手边准备一些助焊剂,以及一些保持焊接头清洁的东西。回到学校后,我们用湿海绵来清洁顶端,但现在有更好的东西来做这项工作。你的覆盆子酱也会有帮助。

  1. Mount the extended header onto the Raspberry Pi’s 40-pin header (see Figure 6-6). This helps stabilize things as you solder.

    A457480_1_En_6_Fig6_HTML.jpg

    图 6-6

    Extended stacking header on the Pi’s 40-pin GPIO

  2. Mount the Motor HAT circuit board onto the headers (see Figure 6-7). To help hold the board at a better angle for soldering, you may want to put something to support the other side. One of the terminal blocks works well for this.

    A457480_1_En_6_Fig7_HTML.jpg

    图 6-7

    Circuit board mounted on the header

  3. 焊接第一个引脚。

  4. Once the first pin is soldered, heat it up again and adjust the board so that it sits properly (see Figure 6-8). When the solder for the pin cools, it will hold the board at the right angle while you solder the rest of the pins. If you supported the board with a terminal block or something else so that the board is sitting straight, you may be able to skip this step .

    A457480_1_En_6_Fig8_HTML.jpg

    图 6-8

    Adjusting the placement and angle of the board

  5. Solder the rest of the first row (see Figure 6-9). You want a nice, clean, shiny joint.

    A457480_1_En_6_Fig9_HTML.jpg

    图 6-9

    Solder the first row of pins

  6. Rotate the board 180 degrees and solder the second row (see Figure 6-10).

    A457480_1_En_6_Fig10_HTML.jpg

    图 6-10

    Rotate the Pi and solder the remaining pins

  7. 从码头上取下帽子。

  8. Mount the screw terminals onto the board (see Figure 6-11).

    A457480_1_En_6_Fig11_HTML.jpg

    图 6-11

    Adding the terminal blocks to the circuit board

  9. Use tape to hold the terminals in place while you flip the board over (see Figure 6-12).

    A457480_1_En_6_Fig12_HTML.jpg

    图 6-12

    Tape helps hold the terminal blocks on the board while you turn it over to solder them into place

  10. Solder the terminals in place (see Figure 6-13).

![A457480_1_En_6_Fig13_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-cv-zh/raw/master/docs/begin-bot-raspi-arduino/img/A457480_1_En_6_Fig13_HTML.jpg)6-13

Soldering the terminal pins  

一旦你拿掉胶带,你就完成了。马达帽已经可以使用了。将帽子安装到码头上。您需要支撑带有端子的一侧,使其不会在 HDMI 外壳上短路(参见图 6-14 )。

A457480_1_En_6_Fig14_HTML.jpg

图 6-14

The completed board mounted on the Raspberry Pi. The orange support piece is 3D printed .

连接电机控制器

连接发动机罩非常简单。只需将电路板安装在 Pi 的 GPIO 接头上。然而,有几件事需要注意。首先,小心不要弄弯树莓皮或帽子上的任何一个别针。这非常容易做到。电机帽上的接头销特别容易弯曲。

您还需要注意不要使端子板短路。你会注意到,安装时,焊点非常靠近 HDMI 连接的金属外壳(见图 6-15 )。对此有两个简单的解决方案。第一种解决方案(在您应用第二种解决方案之前,这是我们将在研讨会中进行的工作)是简单地在 Pi 的 micro USB 和 HDMI 连接器的金属外壳上放置一块绝缘胶带。第二个解决方案(推荐)是获得一些偏移量来支持帽子的侧面。垫片和螺钉也可以完成这项工作。重点是,你不希望电路板下垂并与外壳接触,这可能会导致短暂的灯光表演,并破坏电机帽和 Pi。

A457480_1_En_6_Fig15_HTML.jpg

图 6-15

Adafruit Motor HAT mounted on a Raspberry Pi

一旦电路板安装好,并安全地与短路绝缘,就该连接电机了。在第一个教程中,我们将只使用一个电机。第二段代码控制两个,所以我们现在最好把它们连接起来。

但在此之前,我们必须准备好马达。现在,如果你的马达附带了引线,那么你就领先了。否则,您需要将导线焊接到您的电机上,如图 6-16 所示。我倾向于为有问题的电机使用适当尺寸的黑色和红色电线。我还喜欢确保每个电机上的导线匹配(黑线连接到每个电机的同一个电极,红线连接到每个电机的同一个电极)。这样我就不必事后猜测事情是如何联系在一起的。

A457480_1_En_6_Fig16_HTML.jpg

图 6-16

Leads soldered to motor terminals

在这种情况下,我使用的是 26AWG 绞合线。铅丝一般有两种:绞合的和实心的。固体更加坚硬,非常适合跳投或者没有太多力矩的情况。绞合线由包裹在一个护套中的多根较细的线组成。它更加灵活,非常适合可能会有移动的应用。绞合线稍难处理,进入端子板的末端应镀锡,或涂上焊料(见图 6-17 )。这使得该端具有刚性,并且它在端子板中连接得更好。

A457480_1_En_6_Fig17_HTML.jpg

图 6-17

Tinned lead

接下来的步骤是将电机连接到端子板。

A457480_1_En_6_Fig18_HTML.jpg

图 6-18

Motor connected to the Motor HAT

  1. 确保接线盒是打开的,接线盒内的螺钉一直拧到顶部。确保不要拆下螺丝。你需要用一把相当好的十字螺丝刀。
  2. 将一根镀锡导线插入标有 M1 的端子板一侧的孔中(参见图 6-18 )。哪根线连接到哪个端口并不重要,只要两根线连接到同一个驱动程序的不同端口(在本例中是 M1)。
  3. 拧紧与电线插入的孔相对应的螺钉。
  4. 对电机的第二根导线重复该程序。

在这一点上,如果你是如此倾向,你可以连接第二个电机。我倾向于颠倒引线的顺序,因为它们是指向机器人的相反侧。您需要一个正向命令来使左马达朝一个方向转动,右马达朝另一个方向转动。如果它们都朝同一个方向转动,那么机器人就原地旋转。

您将对四节 AA 电池组的电源端子重复该过程。确保红色导线指向正极(+)侧,黑色导线指向负极(–)侧(参见图 6-19 )。

A457480_1_En_6_Fig19_HTML.jpg

图 6-19

External battery pack connected to the Motor HAT

您的电路板和电机看起来应该与图 6-20 相似。连接好电机和电池组后,您就可以开始编码了!

A457480_1_En_6_Fig20_HTML.jpg

图 6-20

Completed connections to the Motor HAT

使用摩托帽

安装好电机帽,连接好电机和电机电源后,就可以启动 Pi 并登录了。

安装库

一旦启动并连接到您的 Pi,您需要为 Motor HAT 安装 Python 库。这些可以从 Adafruit GitHub 站点获得。

  1. 打开终端窗口。

  2. 导航到您的 Python 代码目录。我的情况是TRG-RasPi_Robot

  3. 输入以下内容:

    git clone https://github.com/adafruit/Adafruit-Motor-HAT-Python-Library.git
    cd Adafruit-Motor-HAT-Python-Library
    
    
  4. 安装 Python 开发库。

    sudo apt-get install python-dev
    
    
  5. 安装马达帽库。

    sudo python setup.py install
    
    

至此,您的 Pi 已经用必要的库更新了,是时候开始编写代码了。

代码

在我们开始编码之前,有一个快速但重要的注意事项。以前,我们几乎什么都用 Python 3。在本次研讨会中,我们将使用 Python 2.7。为什么?嗯,Adafruit 提供的默认库在 Python 2.7 中。可能会有 Python 3.x 库,但我们将使用本次研讨会的默认库。

正如您在之前的研讨会中所发现的,无论何时使用 GPIO 引脚,您都需要以超级用户的身份运行您的代码(sudo)。有几种方法可以做到。一种方法是保存您的 Python 代码,使文件可执行,然后用sudo运行程序。这是您将来运行代码的正确方式。对于车间,我们将走捷径。我们将从命令行使用sudo启动 IDLE IDE,这使得任何程序都可以使用sudo从 IDLE run 实例运行。

转动单个电机
  1. 打开终端窗口。您可以使用安装时使用的同一台计算机;但是只要 IDLE 是打开的,这个终端窗口就是锁定的。

  2. 类型sudo idle

  3. 在空闲的 IDE 中,创建一个新文件并另存为motors.py

  4. 输入以下代码:

    from Adafruit_MotorHAT import Adafruit_MotorHAT as amhat
    from Adafruit_MotorHAT import Adafruit_DCMotor as adcm
    
    import time
    
    # create a motor object
    mh = amhat(addr=0x60)
    myMotor = mh.getMotor(1)
    
    # set start speed
    myMotor.setSpeed(150)
    
    while True:
        # set direction
        myMotor.run(amhat.FORWARD)
    
        # wait 1 second
        time.sleep(1)
    
        # stop motor
        myMotor.run(amhat.RELEASE)
    
        # wait 1 second
        time.sleep(1)
    
    
  5. 保存文件。

  6. 按 F5 运行程序。

让我们看一下代码。

我们首先从 Adafruit_MotorHAT 库中导入我们需要的对象,并为它们分配别名,这样我们就不必在每次使用它们时都写下完整的名称。我们还为代码后面的延迟导入了时间库。

from Adafruit_MotorHAT import Adafruit_MotorHAT as amhat
from Adafruit_MotorHAT import Adafruit_DCMotor as adcm

import time

接下来,我们创建一个电机对象的实例。为此,我们告诉 Python 我们正在使用位于默认 I2C 地址0x60的 Motor HAT。然后,我们为连接到 M1 的马达或马达 1 创建一个马达对象。这让我们可以访问马达的方法和属性。

mh = amhat(addr=0x60)
myMotor = mh.getMotor(1)

在我们打开马达之前,对于这个程序,我们将启动速度设置为半速多一点。

myMotor.setSpeed(150)

现在我们将电机驱动代码的剩余部分包装在一个while循环中。只要True值为真,这段代码就会一直执行。

while True:

驱动电机的代码非常简单。我们向前驱动电机一秒钟,然后停止电机一秒钟。程序一直这样做。

    # set direction
    myMotor.run(amhat.FORWARD)

    # wait 1 second
    time.sleep(1)

    # stop motor
    myMotor.run(amhat.RELEASE)

    # wait 1 second
    time.sleep(1)

在键盘上按 Ctrl-C。

请注意,程序结束,但电机继续转动。那是因为马达帽是自由运转的。这意味着控制器继续执行从 Pi 接收的最后一个命令。如果我们不让它停下来,它就不会停下来。

现在我们要做一些有趣的事情。一些我们以前没做过的事。我们将把电机驱动代码打包到一个try/except块中。这是一段代码,它允许我们捕获发生的任何错误,然后优雅地处理它们。

在这个特殊的例子中,我们将使用try/except块来捕获KeyboardInterrupt事件。当我们使用 Ctrl-C 退出程序时,这个事件被触发。

  1. while循环的代码修改如下:

    try:
        while True:
            # set direction
            myMotor.run(amhat.FORWARD)
    
            # wait 1 second
            time.sleep(1)
    
            # stop motor
            myMotor.run(amhat.RELEASE)
    
            # wait 1 second
            time.sleep(1)
    
    except KeyboardInterrupt:
        myMotor.run(amhat.RELEASE)
    
    
  2. 运行程序。

  3. 让它运行一会儿,然后按 Ctrl-C。

程序退出时,电机将停止。

Python 捕获了KeyboardInterrupt事件,并在退出之前执行最后一行代码。代码释放电机,并简单地关闭它。

转动两个电机

转动单个马达很好,但我们的机器人将有两个马达,我们希望它们独立运行。我们还希望它们能够改变速度和方向。

要操作多个电机,只需为每个电机创建一个不同的电机对象实例。假设您之前连接了两台电机,我们将创建两台电机并向每台电机发送命令。我们还改变了马达的速度和方向。

  1. 从空闲状态创建新的 Python 文件。

  2. 将文件另存为two_motors.py

  3. 输入以下代码:

    from Adafruit_MotorHAT import Adafruit_MotorHAT as amhat, Adafruit_DCMotor as adcm
    
    import time
    
    # create 2 motor objects
    mh = amhat(addr=0x60)
    
    motor1 = mh.getMotor(1)
    motor2 = mh.getMotor(2)
    
    # set start speed
    motor1.setSpeed(0)
    motor2.setSpeed(0)
    
    # direction variable
    direction = 0
    
    # wrap actions in try loop
    try:
        while True:
            # if direction = 1 then motor1 forward and motor2 backward
            # else motor1 backward and motor2 forward
            if direction == 0:
                motor1.run(amhat.FORWARD)
                motor2.run(amhat.BACKWARD)
            else:
                motor1.run(amhat.BACKWARD)
                motor2.run(amhat.FORWARD)
    
            # ramp up the speed from 1 to 255
            for i in range(255):
                j = 255-i
    
                motor1.setSpeed(i)
                motor2.setSpeed(j)
    
                time.sleep(0.01)
    
            # ramp down the speed from 255 to 1
            for i in reversed(range(255)):
                j = 255-i
    
                motor1.setSpeed(i)
                motor2.setSpeed(j)
    
                time.sleep(0.01)
    
            # wait half a second
            time.sleep(0.5)
    
            # change directions
            if direction == 0:
                direction = 1
            else:
                direction = 0
    
    # kill motors and exit program on ctrl-c
    except KeyboardInterrupt:
        motor1.run(amhat.RELEASE)
        motor2.run(amhat.RELEASE)
    
    
  4. 保存文件。

  5. 按 F5 运行程序。

在很大程度上,代码是相同的。我们添加了几个for循环来计数到 255,然后再返回。我们创建了两个变量来保存这个值;第二个函数通过从 255 中减去该值来反转该值。我们也有一个变量来跟踪电机转动的方向。一旦两个马达都再次加速和减速,我们改变方向,再做一次。我们使用和以前一样的退出代码。

L298N 通用电机驱动器

L298N 是一种常见的 H 桥电机控制器芯片。一些制造商已经将芯片安装在电路板上,并添加了所有必要的支持电子设备。最终结果是一个流行的,通用的电机控制器。

h 桥电机控制器

H 桥电机控制器是你会遇到的最常见的电机控制器。它的名字来源于示意图中独特的 H 形。H 桥本质上由四个控制电机电流的栅极组成。根据闸门打开和关闭的方式,您可以控制电机旋转的方向。

在 L298N 上,有两个使能引脚(每个电机一个)和四个输入引脚。in1 和 in2 引脚控制电机 1,而 in3 和 in4 控制电机 2。图 6-21 显示了浇口的排列方式;in1 控制着 S1 和 S4,in2 控制着 S3 和 S2。当 in1 或 in2 为高电平时,它们各自的栅极关闭。当它们处于低位时,门就会打开。

当 in1 为高电平,in2 为低电平时,电流流动,使电机顺时针旋转。如果 in1 为低电平,in2 为高电平,电机逆时针旋转。如果两个引脚都为高电平,电机就不会旋转,实际上是踩了刹车。如果两个引脚都为低电平,则没有电流流过电机,电机自由旋转。

A457480_1_En_6_Fig21_HTML.jpg

图 6-21

H-bridge motor controller operation

剩下使能引脚 enA 和 enB,用于设置电机速度。这就是我们在这些引脚上使用 PWM 的原因。PWM 允许我们改变每个电机的速度。如果我们使用标准的数字引脚,我们可以启动和停止电机,但它要么是全功率或无功率。PWM 让我们能够更好地控制电机。

使用 L298N

L298N 有几种使用方法;各有利弊。一种方法是将引脚连接到 Raspberry Pi,其优点是可以直接由 Pi 控制。缺点是您可能必须使用逻辑电平转换器,因为 Pi 的引脚是 3.3 伏,而控制器是 5 伏。而且,你失去了控制速度的能力。速度控制需要 PWM,正如我在前面章节中所讨论的,这是 Pi 所不具备的一个方面。

我首选的连接 L298N 的方法是通过 Arduino。这样,你就可以通过 PWM 来控制速度。此外,由于 Arduino 和控制器都是 5 伏,所以不需要使用逻辑电平转换器。当然,这里的缺点是,你必须通过串行将电机指令传递给 Arduino。

Arduino 伫列

在本练习中,Arduino 只是充当电机控制器的通道。我们将从串行流中读取指令,并将这些值传递给电机控制器。Arduino 将不执行任何逻辑。如果在现实世界中实现这一点,您可能希望传感器充当中断。通过允许传感器中断正常操作,您可以在项目中建立一些安全性。

  1. 在 Arduino IDE 中打开一个新的草图。

  2. 将草图另存为L298N_passthrough

  3. 输入以下代码:

    int enA = 9;
    int in1 = 8;
    int in2 = 7;
    int in3 = 5;
    int in4 = 4;
    int enB = 3;
    
    int enAVal, in1Val, in2Val, in3Val, in4Val, enBVal;
    
    void setup() {
      // put your setup code here, to run once:
      Serial.begin(9600);
    
      pinMode(enA, OUTPUT);
      pinMode(in1, OUTPUT);
      pinMode(in2, OUTPUT);
      pinMode(in3, OUTPUT);
      pinMode(in4, OUTPUT);
      pinMode(enB, OUTPUT); 
    
    }
    
    void loop() {
      // Only work if there is data in the serial buffer
      while(Serial.available() > 0){
    
        // Read the ints from the serial port
        enAVal = Serial.parseInt();
        in1Val = Serial.parseInt();
        in2Val = Serial.parseInt();
        // Only read the next three if there is data
        if(Serial.available() > 0){
          in3Val = Serial.parseInt();
          in4Val = Serial.parseInt();
          enBVal = Serial.parseInt();
        }
    
        // Write the values to the L298N
        analogWrite(enA, enAVal);
        digitalWrite(in1, in1Val);
        digitalWrite(in2, in2Val);
        digitalWrite(in3, in3Val);
        digitalWrite(in4, in4Val);
        analogWrite(enB, enBVal);
    
        // Purge any remaining data because we don't need it
        while(Serial.available() > 0){
          char x = Serial.read();
        }
      }
    }
    
    
  4. 保存草图并上传到 Arduino。

你不会在 Arduino 上看到任何事情发生。我们所做的是用代码加载 Arduino,该代码简单地读取串行端口并将读取的值传递给 L298N。

我们在这段代码中做了一些你想注意的事情。

if(Serial.available() > 2){

首先要注意的是我们读入in2Val的值后的if语句。接下来的两个练习中都会用到此代码。第一个练习将只传递三个值。第二个将传递六个值。我们只读取后三个值,如果它们存在的话;否则,我们会得到一个错误。为了确保避免错误,如果有三个或更多的值要读取,我们只想读取接下来的三个值。

while(Serial.available() > 0){
  char x = Serial.read();
}

在草图的最后,我们添加了一个小的while循环。如果在读取完所有六个值后,串行缓冲区中还有任何剩余数据,我们需要将其清除,以便下一个周期缓冲区中没有任何离散数据。这个块只是读取所有剩余的字节,并将它们从缓冲区中删除。

连接 L298N

连接电机控制器比仅仅将其插入割台要复杂一些。我们将通过 Arduino 进行连接,以利用 PWM 引脚。与电机帽一样,我们将从四节 AAA 电池组为电机控制器提供外部电源。这提供了电机需要的 6 伏电压,而不会烧坏 Arduino。

转动一个电机

在 L298N 的第一个练习中,您将学习如何转动单个马达。我们设定马达的速度和方向,改变方向,改变速度。图 6-22 显示了本练习的电路。

  1. 将电机控制器上的 enA 连接到 Arduino 上的引脚 9。您可能需要移除跳线。

  2. 将 in1 连接到引脚 8。

  3. 将 in2 连接到引脚 7。

  4. 将 Arduino 上的接地引脚连接到螺丝端子上的接地柱。这可能是中柱。

  5. 将一根引线连接到 out1,另一根引线连接到 out2,从而将电机连接到电机控制器。目前,哪个领导去哪个输出岗位并不重要。

  6. 将电池组的黑色导线连接到 L298N 上的接地端子。

  7. Connect the red lead from the battery pack to the positive terminal. It is usually labeled + or VCC.

    A457480_1_En_6_Fig22_HTML.jpg

    图 6-22

    L298N single motor wiring

  8. 在空闲状态下打开一个新文件。

  9. 将文件另存为L298N_1_motor_example.py

  10. 输入以下代码:

```py
import serial
import time

directon = 1

ser = serial.Serial("/dev/ttyACM0",9600,timeout=1)

def driveMotor(int speed, int drct):
    enA = speed

    # determine direction
    if drct == 1:
        in1 = 1
        in2 = 0
    else if drct == -1:
        in1 = 0
        in2 = 1
    else:
        in1 = 0
        in2 = 0

    valList = str(enA) + ',' + str(in1) + ',' + str(in2)
    serString = ','.join(valList)
    ser.write(serString)
    time.sleep(0.1)

while 1:
    # ramp up speed
    while motSpeed < 256:
        driveMotor(motSpeed, direction)
        motSpeed = motSpeed + 1

    # ramp down speed
    while motSpeed > 0:
        driveMotor(motSpeed, direction)
        motSpeed = motSpeed - 1

    # reverse direction
    direction = -direction

```
  1. 保存并运行文件

电机应该开始旋转,越来越快,直到它达到最高速度。在那时,它减速到停止,反转方向,然后重复。这种情况一直持续到您按 Ctrl-C 停止程序。

转动两个电机

接下来,我们旋转两个马达。设置和代码与我们刚才做的非常相似,只是多了一个马达。您应该已经连接了第一个电机。如果没有,请完成上一个练习中的步骤 1 到 7。让我们在增加第二个电机的情况下开始(见图 6-23 )。

  1. 将 Arduino 上的引脚 5 连接到电机控制器上的 in3。

  2. 将引线从引脚 4 连接到 in 4。

  3. 将引脚 3 连接到 enB。

  4. Connect the leads from the second motor to the out2 terminals. Again, it matters very little in this exercise which lead goes to which terminal. Later, when you are mounting the motors onto the robot, you want to make sure that the motors are connected so that they turn opposite to each other. For now, however, we only care that they actually turn.

    A457480_1_En_6_Fig23_HTML.jpg

    图 6-23

    L298N two motor wiring

  5. 在空闲状态下打开一个新文件。

  6. 将文件另存为L298N_2_motor_example.py

  7. 输入以下代码:

    import serial
    import time
    
    directon = 1
    
    ser = serial.Serial("/dev/ttyACM0",9600,timeout=1)
    
    def driveMotor(int motor, int speed, int drct):
        enA = speed
    
        # determine direction
        if drct == 1:
            in1 = 1
            in2 = 0
            in3 = 1
            in4 = 0
        else if drct == -1:
            in1 = 0
            in2 = 1
            in3 = 0
            in4 = 1
        else:
            in1 = 0
            in2 = 0
            in3 = 0
            in4 = 0
    
        valList = str(enA) + ',' + str(in1) + ',' + str(in2) +
            ',' + str(in3) + ',' + str(in4) + ',' + str(enB)
        serString = ','.join(valList)
        ser.write(serString)
        time.sleep(0.1)
    
    while 1:
        # ramp up speed
        while motSpeed < 256:
            driveMotor(motSpeed, direction)
            motSpeed = motSpeed + 1
    
        # ramp down speed
        while motSpeed > 0:
            driveMotor(motSpeed, direction)
            motSpeed = motSpeed - 1
    
        # reverse direction
        direction = -direction
    
    

这段代码与之前的练习没有太大的不同。我们所做的只是为第二台电机添加启用和输入变量。两个马达应该以相同的速度旋转。他们加速,减速,然后转向。看一下代码,确定如何让马达彼此独立地旋转。

摘要

在这一章,我们看了常见类型的电机:DC,无铁芯,步进电机和伺服电机。我们组装了阿达果 DC &步进电机帽子。(你现在应该对烙铁相当熟悉了。)然后,你学会了如何把你的马达连接到它上面,让它们旋转起来。

我们还研究了一种常见的通用电机控制器。L298N 的工作方式略有不同,因为方向是通过改变两个引脚的状态来设置的。我们通过 Arduino 连接 L298N,以利用 PWM 引脚来控制电机的速度和方向。我们可以轻松地将使能引脚连接到 Raspberry Pi GPIO 接头上的数字输出引脚。然而,对电机速度进行离散控制是很重要的。在接下来的一章中,你会看到为什么这很重要。

至此,您已经获得了构建一个简单的小机器人所需的所有信息。你已经学习了 Python 和 Arduino 编程。你已经用传感器让你的机器人探测周围环境。最后,你让你的马达旋转,所以你有运动。逻辑、感知和运动是每个机器人的本质。其他一切都是这些元素的更高级版本。

现在你已经了解了你需要的关于机器人的一切,我们将组装底盘套件并制造一个机器人。之后,我们开始让我们的机器人变得更有能力、更聪明。我们从红外传感器开始,继续控制算法,然后给机器人眼睛。嗯,一只眼睛。

七、组装机器人

在最后一章中,我们制作了 Adafruit 电机帽,这是一个电子设备,允许你用你的树莓派 控制多达四个 DC 电机。我们还研究了一个通用电机控制器,我们在 Arduino 板上运行了这个控制器。现在你知道如何让你的机器人移动,让我们开始建造它。

在这一章中,我们将建造我们的机器人。在这个过程中,我会给出一些我在构建中获得的提示和指示。组装机器人时有很多小事要考虑。你会遇到一些你没有考虑到的奇怪情况。最容易被忽视的是布线和电线管理。像操作顺序和组件放置这样的事情非常重要。在构建早期做出的决策可能会导致以后的复杂化。注意这些事情可以帮助你避免拆卸你的机器人来纠正你早期犯下的错误。

该建筑分为四个独立练习。我们将从构建 Whippersnapper 底盘套件开始。然后我们将安装电子设备,接着是电线。最后,我们来看看如何安装超声波传感器。在每个练习中,我将指出在构建自己的版本时需要考虑的一些事情。

组装底盘

对于这个构建,我选择使用一个商业上可用工具包。套件的好处在于,一个好的套件包含了您开始使用所需的一切。有许多不同价位和不同制造商的产品可供选择。许多低成本的工具包,通常在网上从外国卖家那里找到,不如其他的完整。通常,这些是流行设备的套件,但组装时很少考虑零件如何搭配。所以,如果你打算买一套工具,确保它有所有的硬件,并且零件设计成可以一起工作。

选择材料

选择机箱时,材料是另一个需要考虑的因素。金属底盘不错。它往往比塑料机箱更贵,但也更耐用。就塑料套件而言,请记住,并非所有塑料都是相同的。

丙烯酸是一种便宜且使用方便的材料;然而,它不是大多数应用的合适材料。丙烯酸易碎、不灵活,并且容易刮伤。当它断裂时,通常是锋利的碎片。记住不要在任何类型的高摩擦应用中使用丙烯酸树脂也是明智的,因为它容易分解成粗颗粒,从而放大摩擦。

如果你要用塑料,ABS 是更好的材料。像丙烯酸树脂一样,ABS 也是片状的,而且相当便宜。不像压克力,它更耐用。它不容易开裂或断裂,而且更耐刮擦。ABS 是可钻孔的,比亚克力更容易加工。

另一种选择是聚苯乙烯。苯乙烯是用于塑料模型套件的材料。所以,如果你熟悉使用这些工具,那么苯乙烯是一个简单的选择。它比丙烯酸树脂或 ABS 更柔韧。它往往比其他的贵一点,但是很容易操作。

自以为是的年轻人

Whippersnapper 是一款由激光切割 ABS 板材制成的商用套件。它是 Actobotics 的 Runt Rover 系列的一部分,由 ServoCity 制造。我曾经使用过 Actobotics 系列的几个套件,我知道它们是设计精良的优质产品。除了机器人套件之外,他们还生产一系列可以协同工作的零件。

所有这些都有助于选择自以为是者(见图 7-1 )作为该项目的基地。它是一个好看的机箱,有空间容纳所有的电子产品,并留有一些增长空间,这没有什么坏处。

为了清楚起见,树莓派将安装在机器人的后部。Arduino 会在最前面。这将使布线稍微容易一些。

A457480_1_En_7_Fig1_HTML.jpg

图 7-1

All the Whippersnapper parts

首先,我想展示一下各个部分。这有助于你确保所有的东西都在那里,并让你熟悉所有的部分。这套工具是按扣合在一起的。事实上,你唯一需要的工具是十字螺丝刀和尖嘴钳。将零件扣合在一起时,要注意配合很紧,需要一些力才能将所有零件合在一起。只要你保持这些部件笔直,它们就不会坏。牢牢抓住零件,均匀施加压力。

  1. Attach the center support to one of the sides. Make sure that the course side is facing out. Take note of the tabs on the center support. The single pair of tabs attaches to the bottom plate (see Figure 7-2).

    A457480_1_En_7_Fig2_HTML.jpg

    图 7-2

    Center support attached to an outer plate

  2. 将第二块侧板安装到中心支架上。再一次,确保路线在机器人的外面。

  3. Snap the top plate to the assembly. There are six sets of tabs that snap to the top plate (see Figure 7-3).

    A457480_1_En_7_Fig3_HTML.jpg

    图 7-3

    Top plate added

在接下来的步骤中,我们连接电机。电机的一侧是一个小销钉(见图 7-4 ),它有助于对齐电机并将其保持在适当的位置。

  1. 安装电机,使轴穿过下面的孔,销钉进入第二个孔。

  2. Use two screws and nuts to hold the motor in place (see Figure 7-5). Although not included in the kit, some #4 split lock washers would be good to use here. If you don’t have any, use Loctite Threadlocker Blue on the nuts. Without something to lock them into place, the nuts will rattle off.

    A457480_1_En_7_Fig5_HTML.jpg

    图 7-5

    Mounted motor

  3. Repeat the process for each of the three remaining motors (see Figure 7-6).

    A457480_1_En_7_Fig6_HTML.jpg

    图 7-6

    All motors mounted

  4. Flip the chassis over and attach the bottom plate. There are five sets of tabs holding the bottom plate on (see Figure 7-7).

    A457480_1_En_7_Fig7_HTML.jpg

    图 7-7

    Bottom plate added

  5. Feed the wires for each motor into the chassis through the hole behind the motor (see Figure 7-8). This bit of housekeeping keeps the wires from getting tangled in the wheels or caught onto something.

    A457480_1_En_7_Fig8_HTML.jpg

    图 7-8

    Motor wires fed through the hole behind the motor

  6. 将电子夹固定在顶板上。这些夹子将用于固定树莓酱。

  7. 将前马达的电线穿过中心支撑板上的孔。

A457480_1_En_7_Fig4_HTML.jpg

图 7-4

Motor with tab

现在,机箱已准备好安装电子设备。你的机器人底盘应该如图 7-9 所示。

A457480_1_En_7_Fig9_HTML.jpg

图 7-9

The completed Whippersnapper

安装电子设备

接下来,我们将把电子设备安装到底盘上。从 Raspberry Pi 开始,我们将连接每个组件,Arduino 和试验板安装在前面。

在这部分制作过程中,经常使用安装胶带和拉链。板子的位置由你决定。有些人将一些电子设备安装在机箱内。然而,我发现下面的安排对我最有效。它可以更容易地接触到电子设备,并为额外的组件节省内部空间。

  1. Snap the Raspberry Pi into the clips on the top plate (see Figure 7-10). The Pi should be held firmly in place by the top barbs.

    A457480_1_En_7_Fig10_HTML.jpg

    图 7-10

    Raspberry Pi mounted in the clips The tabs that hold the chassis together (see Figure 7-11) make mounting the Arduino and breadboard a challenge. This is one reason I like to use foam mounting tape—it provides some padding. To clear the tabs, we’ll need to double up on the tape.

    A457480_1_En_7_Fig11_HTML.jpg

    图 7-11

    Clip protruding from the top plate

  2. Stack two pieces of foam tape on top of each other and place them on the top plate. Use a second set of stacked foam tape to form a T (see Figure 7-12). This adds stability.

    A457480_1_En_7_Fig12_HTML.jpg

    图 7-12

    Double layer of mounting tape for the breadboard

  3. Remove the protective paper from the bottom of the breadboard and press the breadboard firmly into the T-shaped tape on the top plate (see Figure 7-13).

    A457480_1_En_7_Fig13_HTML.jpg

    图 7-13

    Mounted breadboard . Note that the T-cobbler has been moved forward to allow room for the power pack.

  4. Repeat the procedure for the Arduino (see Figure 7-14).

    A457480_1_En_7_Fig14_HTML.jpg

    图 7-14

    Arduino mounted on a double layer of mounting tape When mounting the Arduino, remember to leave room for the USB cable. I offset the Arduino from the center so that the USB plug is clear of the Raspberry Pi (see Figure 7-15).

    A457480_1_En_7_Fig15_HTML.jpg

    图 7-15

    Leaving clearance for the USB cable

  5. 将 4 节 AA 电池盒安装在背面的机箱内。如果适用的话,确保安装时能够接触到电池和电源开关。我用泡沫胶带来固定我的。

  6. 找到一个安全安装 5V 电源组的地方。我发现试验板和树莓皮之间的空间对我使用的小型电源组来说很好。您的位置将由您的电源组的外形决定。

电子设备就位后,是时候开始将各部分连接在一起了。

接线

试图把这一部分写成一步一步的说明是不合适的。如何连接机器人完全取决于你。每个机器人都不一样。布线由组件位置、您使用的电缆和个人偏好决定。相反,我将向您介绍我是如何连接我的机器人的,以及我做出决定背后的思考过程,并包括对您的项目的考虑。

我喜欢尽可能保持我的电缆整洁。有些人很少考虑他们是如何走线的。我在一些机器人的掩护下看到过一些纠结的乱七八糟的东西。对我来说,能够方便地接触零件很重要,这包括电线和电缆。

为 Pi 供电和连接 Arduino 的 USB 电缆比我在大多数项目中喜欢的要长一点。有多种类型的电缆可供选择,包括带有直角插头的电缆,这使得布线相当容易。因为电缆有点长,我用拉链把它们捆成小一点的东西。用于 Arduino 的较重的电缆然后被捆绑到用于 Pi 的安装夹上。从电源组到 Pi 的电缆塞在 Pi 下面(参见图 7-16 )。

A457480_1_En_7_Fig16_HTML.jpg

图 7-16

USB cables bundled for tidiness

接下来,我将电线从马达连接到马达帽。DC 发动机的发动机罩有四个输出。有四个马达。我可以将马达成对连接到两个不同的输出端:一个用于左侧,一个用于右侧;然而,小而便宜的马达在速度上往往不太稳定。即使两台电机接收到相同的信号,也不能保证它们以相同的速度转动。能够独立调节每个电机的速度是我利用的一个很好的功能。因此,每个电机都有自己的输出(见图 7-17 )。

我在每个马达的速度上加了一个乘数。只要稍微调整一下倍增器,我就能让马达更加稳定地转动。

A457480_1_En_7_Fig17_HTML.jpg

图 7-17

Motor and external battery pack wires connected to the Motor HAT

一旦马达连接好,我就接通电源。当你连接你的,注意极性。作为标准,红色为正,黑色为负。由于我的电池组是改装的,电线不是红黑相间的。我用伏特计来确定电线的极性,并适当地连接它们。

要连接的最后一根电缆是 T 形补鞋器的带状电缆(见图 7-18 )。只有一种方法将带状电缆连接到 T 形补鞋匠。塞子上的突出部对准塞子上的间隙。在 Pi 上,确保带有白色条纹的导线连接到引脚 1。对于 Pi,这是最靠近拐角的引脚。

A457480_1_En_7_Fig18_HTML.jpg

图 7-18

Ribbon cable attaches the T-cobbler to the Pi. Note the white stripe .

安装传感器

这是组装机器人最需要创造力的地方。大多数机箱没有传感器安装硬件。如果有,它们是针对您可能不会使用的特定传感器。

安装传感器有许多不同的方法。我发现简单地准备一些不同的材料对我来说很有用。

当我长大的时候,我有一套直立装置。如果你不熟悉 Erector,他们生产一种建筑玩具,包括许多金属部件:横梁、支架、螺丝、螺母、滑轮、皮带等等。我花了数小时制造卡车、拖拉机、飞机,是的,甚至在 20 世纪 80 年代,还有机器人。想象一下我在寻找一些项目中使用的通用零件时的喜悦,我在当地的业余爱好商店遇到了一个安装工。更让我高兴的是,我发现当地一家大型五金店在他们的杂件箱中出售单个零件。

安装工具是许多项目中所需的各种小零件的重要来源。在这种情况下,我使用其中一个横梁和一个支架来安装超声波测距仪(见图 7-19 )。

A457480_1_En_7_Fig19_HTML.jpg

图 7-19

A bracket and beam from the Erector Set. The beam is bent to provide angles for the sensors.

支架就位后,我使用安装胶带来固定传感器(见图 7-20 )。在这种特殊情况下,磁带有两个用途。首先,它将传感器固定在金属上。第二个目的是绝缘。传感器背面的电子元件暴露在外;将它们连接到金属部件上有导致短路的风险。泡沫安装带是很好的绝缘体。

A457480_1_En_7_Fig20_HTML.jpg

图 7-20

Ultrasonic sensors mounted

我学到的一件事是不要只相信安装带来固定传感器,尤其是金属。过去,胶带会变松,导致传感器出现故障。解决方法是我另一个最喜欢的方法:拉链。胶带将传感器固定到位并提供绝缘;然而,拉链增加了安全性和强度。在这一点上,我很确定事情不会有任何进展。

传感器安装好后,最后要做的就是将它们连接到 Arduino。我使用从传感器到 Arduino 的母到母跳线(参见图 7-21 )。在 Arduino 上,我安装了一个传感器屏蔽。传感器屏蔽为每个数字模拟引脚增加了一个 5V 的接地引脚。其中一些甚至有串行或无线设备专用接口。我用的是一个非常简单的没有很多专业头的。传感器护罩使传感器和其他设备的安装变得更加容易。

A457480_1_En_7_Fig21_HTML.jpg

图 7-21

Ultrasonic rangefinders secured with zip ties and wired to Arduino

成品机器人

加上传感器,我就有了一个完整的机器人。剩下唯一要做的就是写代码让它动起来。图 7-22 显示了我完成的机器人。

A457480_1_En_7_Fig22_HTML.jpg

图 7-22

The finished Whippersnapper with electronics

让机器人可以移动

目前,我们有一个非常好的零件集合。没有合适的软件,我们就没有真正的机器人。接下来,我概述了我们希望机器人做什么。我们将把它转化为行为,而这些行为又转化为让这个小机器人活起来所需的代码。

这个计划

在前面的章节中,我们用例子说明了不同的主题。由于这是我们第一次应用工作机器人,让我们花一点时间来概述我们希望机器人做什么。

这个计划是基于我在本章前面制作的机器人。它假设有三个超声波传感器和四个独立工作的电机。电机通过安装在 Pi 上的电机帽控制。传感器通过 Arduino 操作。

传感器

如前所述,我们将操作三个超声波传感器。传感器通过传感器护罩连接到 Arduino。因为我们使用串行与 Pi 通信,所以不能使用引脚 0 和 1。这些是串行端口使用的引脚。因此,我们的第一个传感器,即中间的传感器,位于引脚 2 和 3 上;左侧传感器在针脚 4 和 5 上;右边的传感器在引脚 6 和 7 上。

传感器按顺序触发,从中间开始,接着是左边,然后是右边。每个传感器都会等到前一个传感器完成后再触发。结果以半秒的时间间隔被发送回 Pi,作为一串浮点数,以厘米为单位表示距离每个传感器的距离。

发动机

电机连接到树莓皮上的电机帽。每个电机都连接到控制器上四个电机通道中的一个。电机 1,左前电机,连接到 M1。电机 2,即左后电机,与 M2 相连。电机 3,即右前电机,位于 M3 上。电机 4,右后电机,在 M4。

机器人使用差动转向驱动,也称为坦克驱动或滑动转向。为此,左侧电机一起驱动,右侧电机一起驱动。我称它们为左右声道。因此,同样的命令被发送到 M1 和 M2。同样,M3 和 M4 也接受共同指挥。

每个电机的代码都有乘数。乘数被应用于每个相应的电机以补偿速度差。这意味着我们需要一个缓冲区来容纳这种差异。因此,最高速度设置为 255 分中的 200 分。最初,乘数设置为 1。你需要调整你的乘数来适应你的机器人。

行为

这个机器人是一个简单的随机漫步者。它沿着直线行驶,直到检测到障碍物。然后它调整自己的路线以避免撞到障碍物。这并不是一个特别复杂的解决方案,但它说明了机器人操作的一些基础。

机器人的行为规则如下:

  • 它向前行驶。
  • 如果它发现左边有物体,它会向右转。
  • 如果它发现右边有物体,它就向左转。
  • 如果它检测到正前方有物体,它会停下来,转向距离最远的方向。
  • 如果两个方向的距离相等,或者两侧的传感器都超过截止值,机器人在继续前进之前会在一个随机的方向上转动一段预定的时间。

这种行为有些基础,但它应该提供一个在房子里自主走动的机器人。

代码

代码分为两部分:Arduino 和 Pi。在 Arduino 上,我们关心的只是操作传感器,并以预定的时间间隔将读数传回 Pi。在这种情况下,每 500 毫秒或半秒。

Raspberry Pi 使用传入的数据来执行行为。它从串行端口读取数据,并将数据解析为变量。Pi 使用这些变量来确定下一步行动。这个动作被翻译成电机的指令,然后被发送到电机控制器执行。

Arduino 伫列

这个程序简单地操作机器人前面的三个超声波传感器。然后,它通过串行连接将这些值作为一串浮点数返回给 Raspberry Pi。代码基本上与第五章中的 Pinguino 示例相同。不同的是,我们使用三个传感器,而不是一个。

  1. 在 Arduino IDE 中打开一个新的草图。

  2. 将草图另存为robot_sensors

  3. 输入以下代码:

    int trigMid = 2;
    int echoMid = 3;
    int trigLeft = 4;
    int echoLeft = 5;
    int trigRight = 6;
    int echoRight = 7;
    float distMid = 0.0;
    float distLeft = 0.0;
    float distRight = 0.0;
    String serialString;
    
    void setup() {
      // set the pinModes for the sensors
      pinMode(trigMid, OUTPUT);
      pinMode(echoMid, INPUT);
      pinMode(trigLeft, OUTPUT);
      pinMode(echoLeft, INPUT);
      pinMode(trigRight, OUTPUT);
      pinMode(echoRight, INPUT);
    
      // set trig pins to low;
      digitalWrite(trigMid,LOW);
      digitalWrite(trigLeft,LOW);
      digitalWrite(trigRight,LOW);
    
      // starting serial
      Serial.begin(115200);
    }
    
    // function to operate the sensors
    // returns distance in centimeters
    float ping(int trigPin, int echoPin){
      // Private variables, not available
      // outside the function
      int duration = 0;
      float distance = 0.0;
    
      // operate sensor
      digitalWrite(trigPin, HIGH);
      delayMicroseconds(10);
      digitalWrite(trigPin, LOW);
    
      // get results and calculate distance
      duration = pulseIn(echoPin, HIGH);
      distance = duration/58.2;
    
      // return the results
      return distance;
    }
    
    void loop() {
      // get the distance for each sensor
      distMid = ping(trigMid, echoMid);
      distLeft = ping(trigLeft, echoLeft);
      distRight = ping(trigRight, echoRight);
    
      // write the results to the serial port
      Serial.print(distMid); Serial.print(",");
      Serial.print(distLeft); Serial.print(",");
      Serial.println(distRight);
    
      // wait 500 milliseconds before looping
      delay(500);
    }
    
    
  4. 保存草图并上传到 Arduino。

Arduino 现在应该是 ping 通了,但是因为没有监听,所以我们还不知道。接下来,我们将编写树莓派的代码。

树莓派 代码

现在是时候编写在 Raspberry Pi 上运行的代码了。这是一个相当长的程序,所以我会边走边分解。这其中的绝大多数应该看起来非常熟悉。为了适应这种逻辑,这里或那里做了一些更改,但在大多数情况下,我们以前已经这样做过了。每当我们做新的事情时,我都会花时间带你经历一遍。

  1. Python 2.7 的 Open IDLE。记住,Adafruit 库在 Python 3 中还不能工作。

  2. 创建一个新文件。

  3. 另存为pi_roamer_01.py

  4. 输入以下代码。我一步一步地看每一部分,以确保你对这一过程中发生的事情有一个坚实的概念。

  5. 导入您需要的库。

    import serial
    import time
    import random
    
    from Adafruit_MotorHAT import Adafruit_MotorHAT as amhat
    from Adafruit_MotorHAT import Adafruit_DCMotor as adamo
    
    
  6. 创建电机变量并打开串口。Arduino 设置为以更高的波特率运行,因此 Pi 也需要以更高的波特率运行。

    # create motor objects
    motHAT = amhat(addr=0x60)
    mot1 = motHAT.getMotor(1)
    mot2 = motHAT.getMotor(2)
    mot3 = motHAT.getMotor(3)
    mot4 = motHAT.getMotor(4)
    
    # open serial port
    ser = serial.Serial('/dev/ttyACM0', 115200)
    
    
  7. 创建所需的变量。它们中的许多都是浮点数,因为我们使用的是小数。

    # create variables
    # sensors
    distMid = 0.0
    distLeft = 0.0
    distRight = 0.0
    
    # motor multipliers
    m1Mult = 1.0
    m2Mult = 1.0
    m3Mult = 1.0
    m4Mult = 1.0
    
    # distance threshold
    distThresh = 12.0
    distCutOff = 30.0
    
    
  8. 设置管理电机所需的变量。您会注意到,我已经创建了许多默认值,然后将这些值赋给了其他变量。leftSpeedrightSpeeddriveTime变量应该是我们在代码中真正改变的唯一变量。其余的是在整个程序中提供一致性。如果你想改变默认速度,你可以简单地改变speedDef,改变在任何地方都适用。

    # speeds
    speedDef = 200
    leftSpeed = speedDef
    rightSpeed = speedDef
    turnTime = 1.0
    defTime = 0.1
    driveTime = defTime
    
    
  9. 创建drive函数。它在程序主体的两个地方被调用。因为涉及到大量的工作,所以最好将代码分解到一个单独的功能块中。

    def driveMotors(leftChnl = speedDef, rightChnl = speedDef, duration = defTime):
        # determine the speed of each motor by multiplying
        # the channel by the motors multiplier
        m1Speed = leftChnl * m1Mult
        m2Speed = leftChnl * m2Mult
        m3Speed = rightChnl * m3Mult
        m4Speed = rightChnl * m4Mult
    
        # set each motor speed. Since the speed can be a
        # negative number, we take the absolute value
        mot1.setSpeed(abs(int(m1Speed)))
        mot2.setSpeed(abs(int(m2Speed)))
        mot3.setSpeed(abs(int(m3Speed)))
        mot4.setSpeed(abs(int(m4Speed)))
    
        # run the motors. if the channel is negative, run
        # reverse. else run forward
        if(leftChnl < 0):
            mot1.run(amhat.BACKWARD)
            mot2.run(amhat.BACKWARD)
        else:
            mot1.run(amhat.FORWARD)
            mot2.run(amhat.FORWARD)
    
        if (rightChnl > 0):
            mot3.run(amhat.BACKWARD)
            mot4.run(amhat.BACKWARD)
        else:
            mot3.run(amhat.FORWARD)
            mot4.run(amhat.FORWARD)
    
        # wait for duration
        time.sleep(duration)
    
    
  10. 通过将代码包装在try块中,开始程序的主块。这允许我们干净地退出程序。如果没有它和相应的 except 块,电机将继续执行它们收到的最后一个命令。

```py
try:
    while 1:

```
  1. 继续主程序块,读取串口并解析接收到的字符串
```py
# read the serial port
val = ser.readline().decode('utf=8')
print val

# parse the serial string
parsed = val.split(',')
parsed = [x.rstrip() for x in parsed]

# only assign new values if there are
# three or more available
if(len(parsed)>2):
    distMid = float(parsed[0] + str(0))
    distLeft = float(parsed[1] + str(0))
    distRight = float(parsed[2] + str(0))

```
  1. 输入逻辑代码。这是执行前面概述的行为的代码。请注意,中间传感器块(执行停止和转向的那个)写在左右避障代码之外。这样做是因为我们希望对这个逻辑进行评估,而不管左右代码的结果如何。通过将它包含在其他代码之后,中间代码会覆盖左/右代码创建的任何值。
```py
# apply cutoff distance
if(distMid > distCutOff):
    distMid = distCutOff
if(distLeft > distCutOff):
    distLeft = distCutOff
if(distRight > distCutOff):
    distRight = distCutOff

# reset driveTime
driveTime = defTime

# if obstacle to left, steer right by increasing
# leftSpeed and running rightSpeed negative defSpeed
# if obstacle to right, steer to left by increasing
# rightSpeed and running leftSpeed negative
if(distLeft <= distThresh):
    leftSpeed = speedDef
    rightSpeed = -speedDef
elif (distRight <= distThresh):
    leftSpeed = -speedDef
    rightSpeed = speedDef
else:
    leftSpeed = speedDef
    rightSpeed = speedDef

# if obstacle dead ahead, stop then turn toward most
# open direction. if both directions open, turn random
if(distMid <= distThresh):
    # stop
    leftSpeed = 0
    rightSpeed = 0
    driveMotors(leftSpeed, rightSpeed, 1)
    time.sleep(1)
    leftSpeed = -150
    rightSpeed = -150
    driveMotors(leftSpeed, rightSpeed, 1)
    # determine preferred direction. if distLeft >
    # distRight, turn left. if distRight > distLeft,
    # turn right. if equal, turn random
    dirPref = distRight - distLeft
    if(dirPref == 0):
        dirPref = random.random()
    if(dirPref < 0):
        leftSpeed = -speedDef
        rightSpeed = speedDef
    elif(dirPref > 0):
        leftSpeed = speedDef
        rightSpeed = -speedDef
    driveTime = turnTime

```
  1. 调用我们之前创建的driveMotors函数。
```py
# drive the motors
driveMotors(leftSpeed, rightSpeed, driveTime)

```
  1. 刷新串行缓冲区中的所有字节。
```py
ser.flushInput()

```
  1. 进入except块。它允许我们在退出程序前点击 Ctrl-C 来关闭电机。
```py
except KeyboardInterrupt:
    mot1.run(amhat.RELEASE)
    mot2.run(amhat.RELEASE)
    mot3.run(amhat.RELEASE)
    mot4.run(amhat.RELEASE)

```
  1. 保存文件。
  2. 按 F5 运行程序。

当你看完你的小机器人在房间里漫游时,按 Ctrl-C 结束程序。

恭喜你。您刚刚构建并编程了您的第一个基于 Raspberry Pi 的机器人。

我们在这个项目中做了很多——尽管真的没有什么是你以前没见过的。在程序的第一部分,我们导入了我们需要的库并创建了马达对象。在下一节中,我们定义了所有的变量。程序的一个重要部分是我们在变量之后创建的函数。在此功能中,我们驱动电机。电机速度和驱动时间作为函数的参数传递,用于设置每个电机的速度。我们使用速度的符号来确定电机的方向。之后,我们通过将主块包装在一个try块中来开始我们的主块。然后我们进入了while循环,它允许程序无限重复。

while循环中,我们从读取串行字符串开始,然后解析它以提取三个浮点值。将字符串转换成浮点数的算法与我们用来转换成整数的算法略有不同。更具体地说,我们不必将结果除以 10。在一个十进制数的末尾加一个 0 不会改变这个值,所以我们可以在它被转换的时候使用它。

距离测量决定了机器人的下一步行动。if/elsif/else模块评估传感器值。如果左侧或右侧传感器检测到预定义阈值内的障碍物,机器人将转向相反的方向。如果没有探测到障碍物,机器人继续前进。一个单独的if块确定机器人正前方是否有障碍物。如果有障碍物,机器人会停下来,然后转向。它使用左右传感器值来确定前进的方向。如果不能确定方向,机器人转向随机方向。

所有这些都需要时间,在此期间 Arduino 会愉快地发送串行字符串并填充 Pi 的缓冲区。在继续之前,必须清除这些字符串。我们使用串行对象的flushInput()方法来实现这一点。这样,我们只使用最新的信息。

最后,我们使用except块来捕获键盘中断命令。当它被接收时,马达被释放,停止它们。然后程序退出。

摘要

这一章是关于把我们到目前为止学到的所有东西整合到一个工作机器人中。我们组装了机器人底盘套件,并安装了所有的电子设备。一旦所有东西都安装到机器人上,我们就编写一个程序来运行机器人。这是一个相当简单的漫游程序。当你运行它时,你的新机器人应该可以在房间里成功地走动,这取决于房间里家具的拥挤程度。

在接下来的章节中,我们将致力于改进机器人——添加更多的传感器,改进逻辑,并添加一些更高级的功能。具体来说,我们将添加一个摄像头,并学习如何使用 OpenCV 来跟踪颜色和追球。

八、使用红外传感器

到目前为止,你应该已经有了一个可以工作的机器人。在前几章中,我介绍了安装和编程机器人需要知道的一切。您已经使用了电机、传感器以及 Raspberry Pi 和 Arduino 之间的通信。在第 3 和第五章中,你学会了使用 Python 和 Arduino 来操作超声波测距仪。这本书的其余部分介绍了新的传感器,处理算法和计算机视觉。

在本章中,我们将使用红外(IR)传感器。我们看不同类型的传感器。在本章的最后,我们使用了一系列的红外传感器来检测表面和直线的边缘。

红外传感器

红外(IR)传感器是使用针对 IR 光谱调谐的光检测器来检测 IR 信号的任何传感器。通常,红外传感器与红外发射 LED 配对以提供红外信号。测量 LED 的发射强度或存在。

红外传感器的类型

红外线很容易使用。因此,我们发现了许多不同的使用方法。有多种红外传感器可供选择。许多都用在你意想不到的应用中。像在零售店看到的自动门,使用一种叫做 PIR 或被动红外的传感器来检测运动。这种类型的传感器用于自动照明和安全系统。喷墨打印机使用红外传感器和红外发光二极管来测量打印头的精确移动。您的娱乐系统遥控器可能使用红外 LED 将编码脉冲传输到红外接收器。红外敏感摄像机用于制造过程中的质量保证。这样的例子不胜枚举。让我们来看看一些不同类型的红外传感器。

反射传感器

反射传感器包括被设计成检测从目标反射的信号的任何传感器。超声波测距仪是反射传感器,因为它们检测从它们前面的物体反射回来的声音的波长。红外反射传感器以类似的方式工作,它们读取物体反射的红外辐射强度(见图 8-1 )。

A457480_1_En_8_Fig1_HTML.jpg

图 8-1

Reflectance sensors measure the IR light returned from an IR diode

这种类型的传感器的变体被设计成检测 IR 信号的存在。该传感器使用红外强度阈值来确定附近是否有物体。传感器返回一个低信号,直到超过阈值,这时它返回一个高信号。这些传感器通常以反射或直接配置与发光 LED 配对。

线和边缘检测

红外探测器经常被用于建造探测线或壁架边缘的装置。当表面和线之间的对比度高时,这些传感器用于线检测;例如,白色桌面上的黑线。当传感器位于白色表面上方时,大部分红外信号返回到传感器。当传感器位于暗线上方时,返回的红外信号较少。这些传感器通常返回代表返回光量的模拟信号。

同样,传感器也可以检测表面的边缘。当传感器在表面上方时,传感器接收到更多的红外信号。当传感器超过边缘时,信号大大减弱,导致低值(见图 8-2 )。

A457480_1_En_8_Fig2_HTML.jpg

图 8-2

Lines and edges can be detected by the difference in reflected light

一些传感器具有可调阈值,允许它们提供数字信号。当反射率高于阈值时,传感器处于高状态。当反射率低于阈值时,传感器为低。

这种类型的传感器面临的挑战是很难设定精确的阈值来获得一致的结果。然后,即使你让它们在一个环境中拨号,一旦条件改变,或者你试图在一个事件中演示,它们必须重新校准。(并不是说这种情况在我身上反复发生过。)正因为如此,我更喜欢使用模拟传感器,这允许我包括一个自动校准程序,以便程序可以设置自己的阈值。

测距仪

与接近传感器非常相似,测距仪测量到物体的距离。测距仪使用光束更窄的更强的 LED,用于确定物体的大致距离。与超声波测距仪不同,红外测距仪是为检测特定范围而设计的。将传感器与应用相匹配非常重要。

中断传感器

中断传感器用于检测红外信号的存在。它们通常与一个发光二极管配对,并被配置为允许物体在发射器和检测器之间通过。当物体存在并阻挡发射器时,接收器返回低信号。当物体不存在,并且允许接收器检测发射器时,信号为高。

这些传感器经常用在被称为编码器的设备中。编码器通常由带有半透明和透明部分的盘或带组成。当磁盘或磁带经过传感器时,信号会不断地从高电平变为低电平。然后,微控制器或其他电子设备可以使用这种交变信号来计数脉冲。因为透明部分的数量是已知的,所以可以高置信度地计算移动。在其最简单的形式中,这些传感器只能为微控制器提供一个脉冲来计数。一些编码器使用许多传感器来提供精确的运动信息,包括方向。

pir 运动探测器

另一种非常常见的传感器称为 PIR 运动检测器(见图 8-3 )。这些传感器有一个多面透镜,将物体发射或反射的红外辐射反射和折射到其内部的红外传感器上。当这些传感器检测到变化时,会产生一个高电平信号。

A457480_1_En_8_Fig3_HTML.jpg

图 8-3

Common PIR sensor

这些传感器控制当地杂货店的自动门,并控制家中或办公室的自动灯。

使用红外传感器

正如我前面所讨论的,根据您使用的类型,有几种方法可以使用红外传感器。对于我们的项目,我们将使用五个 IR 线传感器,如图 8-4 所示。我更喜欢使用模拟类型的传感器。我们使用的特定传感器实际上可以进行模拟和数字读取。它有一个设置阈值的小电位计;然而,正如我在本章前面所讨论的,这些是出了名的难以拨入。我更喜欢直接使用模拟读数,并用软件计算阈值。

A457480_1_En_8_Fig4_HTML.jpg

图 8-4

IR sensors for line following

连接红外传感器

我为我的机器人使用的传感器是普通 3 针红外传感器的 4 针变体。3 针传感器是数字式的,对传感器的模拟信号施加阈值,以返回高电平或低电平信号。4 引脚版本使用相同的阈值设置返回数字信号,但它还有一个额外的引脚提供模拟读数。让我们使用这两种信号来演练一下。

我使用的传感器与大多数传感器略有不同。它们是专为循线应用而设计的。因此,返回值是反向的。这意味着当反射率高时,它返回低数值,而不是提供高数值。同样,数字信号也被反相。高值表示存在线条,低值表示空白。当你运行下一个练习时,如果你的结果不同,不要惊讶。我们正在寻找相当一致的行为。

我们将 4 针传感器连接到 Arduino,并使用串行监视器来查看传感器的输出。高/低信号可以使用一个数字引脚,模拟传感器可以使用一个模拟引脚,但为了简化布线,我们使用两个模拟引脚。连接到数字输出的模拟引脚用于数字模式,因此其作用与其它引脚完全一样。

由于 Arduino 现在安装在机器人上,让我们使用传感器屏蔽进行连接。还有,我不会断开超声波测距仪。红外传感器的草图没有使用这些引脚,因此没有理由断开它们。

对于这个练习,您还需要一个测试表面。有大块黑色区域或粗黑线的白纸效果最好。由于大多数循线比赛使用 3/4 英寸的黑色电工胶带作为线,将一条胶带放在一张纸上、白色海报板或泡沫芯板上是理想的。

  1. 使用母到母跳线,将传感器的接地引脚连接到 A0 3 引脚接头的接地引脚。

  2. 将传感器的 VCC 引脚连接到 3 引脚接头 A0 的电压引脚。这是中间的针。

  3. 将模拟引脚连接到 A0 的信号引脚。(在我的传感器上,模拟引脚标记为 A0。)

  4. 将传感器的数字引脚连接到 A1 的信号引脚。(在我的传感器上,标记为 D0。)

  5. 在 Arduino IDE 中创建新的草图。

  6. 将草图另存为 IR_test。

  7. 输入以下代码:

    int analogPin = A0;
    int digitalPin = A1;
    
    float analogVal = 0.0;
    int digitalVal = 0;
    
    void setup() {
      pinMode(analogPin, INPUT);
      pinMode(digitalPin, INPUT);
    
      Serial.begin(9600);
    }
    
    void loop() {
      analogVal = analogRead(analogPin);
      digitalVal = digitalRead(digitalPin);
    
      Serial.print("analogVal: "); Serial.print(analogVal);
      Serial.print(" - digitalVal: "); Serial.println(digitalVal);
    
      delay(500);
    }
    
    
  8. 将传感器移到表面的白色区域。传感器需要非常靠近表面而不接触它。

  9. 注意返回的值。(我得到了 30 到 45 范围内的模拟值。我的数字值是 0。)

  10. 将传感器移至直线或表面上的另一个黑色区域。

  11. 请注意这些值。(我得到了 700 到 900 范围内的模拟值。数字值是 1。)

你应该在表面的亮区和暗区得到非常不同的值。您可以看到这是如何很容易地转化为非常有用的功能。

安装红外传感器

接下来,我们将把传感器安装到机器人上,做一些有用的事情。同样,由于您的构建可能与我的有很大不同,我将介绍我是如何连接传感器的。如果你一直忠实地跟随,那么你应该复制我所做的。如果不是,那么这就是机器人开始变得有创造性的地方。你需要决定如何将传感器安装到你的机器人上。看看我的解决方案,了解你在寻找什么。

为了安装传感器,我转向(再一次)安装机的部件。这些部件非常方便易用。在这种情况下,我使用了一个酒吧和相同的角支架用于安装超声波测距仪。事实上,通过使用角支架,我延长了该组件,使红外传感器更接近地面。

在尝试安装红外传感器时,我遇到了一个问题。用于安装传感器的孔位于两个表面安装电阻之间。这意味着金属支架可能会导致短路。我库存中的尼龙支架太大,无法平放在那个空间。我可以使用垫片和一个长螺丝,但垫片太窄,不会直接对着安装杆上的孔。增加垫圈会使传感器离地面太近。

解决方案是将红外传感器安装在酒吧的顶部。挑战在于引脚的焊点肯定会与金属条短路。但是,通过在传感器背面贴上一片绝缘胶带并戳一个安装螺钉孔,这个问题很容易解决(参见图 8-5 )。

A457480_1_En_8_Fig5_HTML.jpg

图 8-5

Mounting the IR sensors on a bar. Electrical tape protects the leads from shorting.

一旦传感器安装完毕,我需要将传感器的引线连接到 Arduino 电路板。我只使用了传感器的模拟引脚,所以我需要在 Arduino 上为每个传感器使用一个逻辑引脚。如果我同时使用模拟和数字引脚,我将需要 Arduino 上相应的模拟和数字引脚。因此,我使用了 A0 到 A4 引脚。为了确保导线正确连接,而不会在连接上施加过大的压力,我使用了较短的公母跳线来延长它们。在连接和传感器周围贴上一点胶带即可(见图 8-6 )。

A457480_1_En_8_Fig6_HTML.jpg

图 8-6

The completed robot with IR sensors mounted and wired

代码

和上一个项目一样,这个项目使用 Arduino 作为 GPIO 设备。大部分逻辑由 Raspberry Pi 执行。我们将以 10 毫秒为间隔读取红外传感器,每秒 100 次。这些值被传递给 Raspberry Pi 来处理。正如您在前面的练习中看到的,读取传感器非常容易,因此 Arduino 代码相当简单。

圆周率方面要复杂得多。首先,我们必须校准传感器。然后,一旦校准,我们必须编写一个算法,使用来自传感器的读数来保持机器人在一条线上。这可能比你想象的要复杂。在这一章的后面,我们将看到一个好的解决方案,但是现在,我们将使用一个更直接的方法。

Arduino 伫列

Arduino 代码对于这个应用来说非常简单。我们将读取每个传感器,并通过串行连接将结果发送到 Pi,每秒 100 次。然而,由于我们需要在校准期间更频繁地读取传感器读数,我们需要知道校准何时运行,因为我们希望每秒更新 100 次,以确保我们获得良好的结果。

  1. 在 Arduino IDE 中开始一个新的草图。

  2. 将草图另存为line_follow1

  3. 输入以下代码:

    int ir1Pin = A0;
    int ir2Pin = A1;
    int ir3Pin = A2;
    int ir4Pin = A3;
    int ir5Pin = A4;
    
    int ir1Val = 0;
    int ir2Val = 0;
    int ir3Val = 0;
    int ir4Val = 0;
    int ir5Val = 0;
    
    void setup() {
      pinMode(ir1Pin, INPUT);
      pinMode(ir2Pin, INPUT);
      pinMode(ir3Pin, INPUT);
      pinMode(ir4Pin, INPUT);
      pinMode(ir5Pin, INPUT);
    
      Serial.begin(9600);
    }
    
    void loop() {
      ir1Val = analogRead(ir1Pin);
      ir2Val = analogRead(ir2Pin);
      ir3Val = analogRead(ir3Pin);
      ir4Val = analogRead(ir4Pin);
      ir5Val = analogRead(ir5Pin);
    
      Serial.print(ir1Val); Serial.print(",");
      Serial.print(ir2Val); Serial.print(",");
      Serial.print(ir3Val); Serial.print(",");
      Serial.print(ir4Val); Serial.print(",");
      Serial.println(ir5Val);
    
      delay(100);
    }
    
    
  4. 保存并上传草图。

这个小品很直白。我们所做的就是读取五个传感器中的每一个并将结果打印到串行端口。

Python 代码

大多数处理都是在 Pi 上完成的。我们需要做的第一件事是校准传感器,以获得高值和低值。要做到这一点,我们需要在这条线上来回扫描传感器,同时读取每个传感器的值。我们正在寻找最高值和最低值。一旦我们完成了一些过线,我们应该有好的价值观来工作。

传感器校准后,是时候开始移动了。驱动机器人前进。只要中间传感器检测到线,就一直往前开。如果左侧或右侧的一个传感器读取到该线,则向相反方向稍微校正,以重新对齐。如果一个外部传感器读取到这条线,就进行更剧烈的校正。这使得机器人能够沿着路线前进,并轻松转弯。

要正确运行这段代码,请为它创建一行代码。有几种方法可以做到这一点。如果你刚好有白色瓷砖地板,那么你可以直接在上面贴上电工胶带。电工胶带从瓷砖上剥离,不会损坏瓷砖。否则,你可以使用纸张、海报板或泡沫芯板,就像那些用于科学展览的一样。同样,使用电工胶带标记线路。一定要加一些曲线。

与 roamer 代码一样,我们将分几部分来介绍它。我们正在编写的代码越来越长。

  1. 在空闲的 IDE 中打开一个新文件。

  2. 将文件另存为line_follow1.py

  3. 导入必要的库:

    import serial
    import time
    
    from Adafruit_MotorHAT import Adafruit_MotorHAT as amhat
    from Adafruit_MotorHAT import Adafruit_DCMotor as adamo
    
    
  4. 创建电机对象。为了使代码更加 Pythonic 化,让我们将 motor 对象放在一个列表中。

    # create motor objects
    motHAT = amhat(addr=0x60)
    mot1 = motHAT.getMotor(1)
    mot2 = motHAT.getMotor(2)
    mot3 = motHAT.getMotor(3)
    mot4 = motHAT.getMotor(4)
    
    motors = [mot1, mot2, mot3, mot4]
    
    
  5. 定义控制电机所需的变量。同样,让我们创建列表。

    # motor multipliers
    motorMultiplier = [1.0, 1.0, 1.0, 1.0, 1.0]
    
    # motor speeds
    motorSpeed = [0,0,0,0]
    
    
  6. 打开串行端口。

    # open serial port
    ser = serial.Serial('/dev/ttyACM0', 9600)
    
    
  7. 定义必要的变量。与电机一样,将一些变量定义为列表。(这在代码的后面会有回报。我保证。)

    # create variables
    # sensors
    irSensors = [0,0,0,0,0]
    irMins = [0,0,0,0,0]
    irMaxs = [0,0,0,0,0]
    irThesh = 50
    
    # speeds
    speedDef = 200
    leftSpeed = speedDef
    rightSpeed = speedDef
    corMinor = 50
    corMajor = 100
    turnTime = 0.5
    defTime = 0.01
    driveTime = defTime
    sweepTime = 1000 #duration of a sweep in milliseconds
    
    
  8. 定义驱动电机的函数。虽然类似,但这段代码不同于 roamer 函数。

    def driveMotors(leftChnl = speedDef, rightChnl = speedDef,
                    duration = defTime):
        # determine the speed of each motor by multiplying
        # the channel by the motors multiplier
        motorSpeed[0] = leftChnl * motorMultiplier[0]
        motorSpeed[1] = leftChnl * motorMultiplier[1]
        motorSpeed[2] = rightChnl * motorMultiplier[2]
        motorSpeed[3] = rightChnl * motorMultiplier[3]
    
    
  9. 迭代电机列表以设置速度。同样,迭代 motorSpeed 列表。

    # set each motor speed. Since the speed can be a
    # negative number, we take the absolute value
    for x in range(4):
        motors[x].setSpeed(abs(int(motorSpeed[x])))
    
    
  10. 启动马达。

```py
# run the motors. if the channel is negative, run
# reverse. else run forward
if(leftChnl < 0):
    motors[0].run(amhat.BACKWARD)
    motors[1].run(amhat.BACKWARD)
else:
    motors[0].run(amhat.FORWARD)
    motors[1].run(amhat.FORWARD)

if (rightChnl > 0):
    motors[2].run(amhat.BACKWARD)
    motors[3].run(amhat.BACKWARD)
else:
    motors[2].run(amhat.FORWARD)
    motors[3].run(amhat.FORWARD)

# wait for duration
time.sleep(duration)

```
  1. 定义从串行流中读取红外传感器值并解析它们的函数。
```py
def getIR():
    # read the serial port
    val = ser.readline().decode('utf-8')

    # parse the serial string
    parsed = val.split(',')
    parsed = [x.rstrip() for x in parsed]

```
  1. 迭代 irSensors 列表以分配解析的值,然后从串行流中清除任何剩余的字节。
```py
if(len(parsed)==5):
    for x in range(5):
        irSensors[x] = int(parsed[x]+str(0))/10

# flush the serial buffer of any extra bytes
ser.flushInput()

```
  1. 定义校准传感器的功能。校准经过四个完整的周期,以读取传感器的最小和最大值。
```py
def calibrate():
    # set up cycle count loop
    direction = 1
    cycle = 0

    # get initial values for each sensor
    # and set initial min/max values
    getIR()

    for x in range(5):
        irMins[x] = irSensors[x]
        irMaxs[x] = irSensors[x]

```
  1. 在循环中循环五次,以确保获得四个完整的循环读数。
```py
while cycle < 5:

    #set up sweep loop
    millisOld = int(round(time.time()*1000))
    millisNew = millisOld

```
  1. 在扫描期间,驱动电机并读取红外传感器。
```py
while((millisNew-millisOld)<sweepTime):
    leftSpeed = speedDef * direction
    rightSpeed = speedDef * -direction

    # drive the motors
    driveMotors(leftSpeed, rightSpeed, driveTime)

    # read sensors
    getIR()

```
  1. 如果传感器值低于或高于当前的irMinsirMaxs值,则更新irMinsirMaxs
```py
# set min and max values for each sensor
for x in range(5):
    if(irSensors[x] < irMins[x]):
        irMins[x] = irSensors[x]
    elif(irSensors[x] > irMaxs[x]):
        irMaxs[x] = irSensors[x]

millisNew = int(round(time.time()*1000))

```
  1. 一个循环后,改变电机方向并增加循环值。
```py
# reverse direction
direction = -direction

# increment cycles
cycle += 1

```
  1. 循环完成后,驱动机器人前进。
```py
# drive forward
driveMotors(speedDef, speedDef, driveTime)

```
  1. 定义followLine功能。
```py
def followLine():
    leftSpeed = speedDef
    rightSpeed = speedDef

    getIR()

```
  1. 根据传感器读数定义行为。如果线被最右边或最左边的传感器检测到,则在另一个方向进行大的校正。如果内侧右侧或内侧左侧传感器检测到该线,则在另一个方向进行轻微校正;否则,直走。
```py
    # find line and correct if necessary
    if(irMaxs[0]-irThresh <= irSensors[0] <= irMaxs[0]+irThresh):
        leftSpeed = speedDef-corMajor
    elif(irMaxs[1]-irThresh <= irSensors[1] <= irMaxs[1]+irThresh):
        leftSpeed = speedDef-corMinor
    elif(irMaxs[3]-irThresh <= irSensors[3] <= irMaxs[3]+irThresh):
        rightSpeed = speedDef-corMinor
    elif(irMaxs[4]-irThresh <= irSensors[4] <= irMaxs[4]+irThresh):
        rightSpeed = speedDef-corMajor
    else:
        leftSpeed = speedDef
        rightSpeed = speedDef

    # drive the motors
    driveMotors(leftSpeed, rightSpeed, driveTime)

```
  1. 输入运行程序的代码。
```py
# execute program
try:
    calibrate()

    while 1:
        followLine()
        time.sleep(0.01)

except KeyboardInterrupt:
    mot1.run(amhat.RELEASE)
    mot2.run(amhat.RELEASE)
    mot3.run(amhat.RELEASE)
    mot4.run(amhat.RELEASE)

```
  1. 保存代码。
  2. 把机器人放到线上。机器人应该对齐,使线在左右轮之间运行,中心传感器在它的正上方。
  3. 运行程序。

你的机器人现在应该沿着这条线走,如果它开始偏离这条线就进行纠正。您可能需要使用corMinorcorMajor变量来微调行为。

我们在这里执行的是所谓的比例控制。这是最简单的控制算法。背后的基本逻辑是,如果你的机器人有点偏离路线,应用一点修正。如果机器人偏离轨道很多,应用更多的修正。应用于机器人的校正量取决于误差有多大。

仅使用比例控制,机器人会非常努力地跟随生产线。它甚至可能成功;然而,你会注意到它是如何沿着这条线曲折前进的。这种行为可能会随着时间的推移而减少,变得平滑;然而,当你引入一条曲线时,不稳定的行为又开始了。更有可能的是,你的机器人矫枉过正,向一个随机的方向偏离,把线远远地甩在后面。

有更好的方法来控制机器人。事实上,有几种更好的方法,都来自一个叫做控制回路的研究领域。控制回路是提高机器或程序响应的算法。它们中的大多数使用当前状态和期望状态之间的差异来控制机器。这种差异称为误差。

接下来让我们来看一下这样的控制系统。

了解 PID 控制

为了更好地控制机器人,你将学习 PID 控制,我将尝试在不涉及数学的情况下讨论它。PID 控制器是应用最广泛的控制回路之一,因为它的通用性和简单性。我们实际上已经使用了 PID 控制器的一部分:比例控制。其余部分有助于平稳反应并提供更好的响应。

控制回路

PID 控制器是一组称为控制回路的算法中的一员。控制回路的目的是使用来自测量过程的输入来改变一个或多个控制,以补偿当前状态和期望状态之间的差异。有许多不同类型的控制回路。事实上,控制回路是被称为控制理论的整个研究领域。就我们的目的而言,我们实际上只关心一个:比例、积分和微分——或 PID。

比例、积分和微分控制

根据维基百科,“PID 控制器连续计算误差值(e(t))作为期望的设定点和测量的过程变量之间的差,并基于比例、积分和微分项应用校正。PID 是比例-积分-微分的缩写,指的是对误差信号进行运算以产生控制信号的三项

控制器的目的是对某些输出进行增量调节,以达到期望的结果。在我们的应用中,我们使用来自红外传感器的反馈来改变我们的电机。理想的行为是机器人在向前移动时保持以一条线为中心。然而,该过程可以用于任何传感器和输出;例如,PID 用于多旋翼平台,以保持水平和稳定性。

顾名思义,PID 算法实际上由三部分组成:比例、积分和微分。每个部分都是一种控制类型;然而,如果单独使用,产生的行为将是不稳定的和难以预测的。

比例控制

在比例控制中,变化量完全根据误差的大小来设定。误差越大,应用的更改越多。纯比例控制将达到零误差状态,但难以处理剧烈变化,这会导致剧烈振荡。

积分控制

积分控制不仅考虑误差,还考虑误差持续的时间。用于补偿误差的变化量随着时间而增加。纯积分控制可以使器件达到零误差状态,但它的反应很慢,往往会过补偿和振荡。

导数调节

导数控制不考虑误差,因此它永远无法使设备达到零误差状态。然而,它确实试图将误差的变化减小到零。如果应用了过多的补偿,则算法会过冲,然后应用另一个校正。该过程以这种方式继续,产生不断增加或减少修正的模式。尽管振荡减少的状态被认为是“稳定的”,但算法永远不会达到真正的零误差状态。

将他们聚集在一起

PID 控制器是这三种方法的简单组合。通过将它们结合在一起,该算法旨在产生平滑的校正过程,使误差为零。是时候做一点数学了。

让我们从定义一些变量开始。

e(t)是时间上的误差,其中(t)是时间,或现在。

K p 是代表比例增益的参数。当我们开始编码时,这就是比例变量。

K i 为积分增益参数。它也是一个变量。

K d 是微分增益参数。你猜对了,又一个变量。

τ代表一段时间内的积分值。我会说的。

比例项基本上是当前误差乘以 K p 值。$$ {P}_{out}={K}_pe(t) $$

积分部分稍微复杂一点,因为它考虑了所有已经发生的误差。它是一段时间内的误差和累积校正的总和。

$$ {I}_{out}={K}_i\underset{0}{\overset{t}{\int }}e\left(\tau \right) d\tau $$

导数项是原始误差和当前误差随时间的差值,然后乘以导数参数。

$$ {D}_{out}={K}_d\frac{de(t)}{dt} $$

综上所述,我们的 PID 方程是这样的:

$$ u(t)=\left({K}_pe(t)\right)+\left({K}_i\underset{0}{\overset{t}{\int }}e\left(\tau \right) d\tau \right)+\left({K}_d\frac{de(t)}{dt}\right) $$

数学就是这样。幸运的是,我们不用自己解决。Python 让这变得非常容易。然而,理解等式内部发生的事情是很重要的。有三个参数需要调整来微调 PID 控制器。通过了解如何使用这些参数,您将能够确定哪些参数需要调整以及何时调整。

实现 PID 控制器

要实现控制器,我们需要知道一些事情。我们想要的结果是什么?我们的输入是什么?我们的产出是什么?

目标是提高我们的循线机器人的性能。因此,我们期望的结果是,当机器人向前行驶时,生产线保持在机器人的中心。

我们的输入是红外传感器。当外部传感器位于黑暗区域(直线)上方时,误差是内部传感器的两倍。这样,我们就知道机器人是有点偏离中心还是很多偏离中心。同样,两个左边的传感器将具有负值,而右边的传感器将具有正值,因此我们将知道哪个方向是关闭的。

最后,我们的输出是电机。更准确地说,我们的输出是左右马达通道之间的速度差。

代码

本练习的代码是对前面代码的修改。事实上,Arduino 代码根本不需要修改。更新的是我们在 Raspberry Pi 上实现的逻辑。

树莓派 代码

我们将修改line_follower1代码以使用 PID 而不是比例算法。为此,我们需要更新getIR函数来更新一个名为sensorErr的新变量。然后我们将用我们的 PID 代码替换followLine函数中的代码。

  1. 在空闲的 IDE 中打开文件line_follower1

  2. 从文件菜单中选择另存为,将文件另存为line_follower2.py

  3. 在变量部分的#sensors下,添加以下代码:

    # PID
    sensorErr = 0
    lastTime = int(round(time.time()*1000))
    lastError = 0
    target = 0
    kp = 0.5
    ki = 0.5
    kd = 1
    
    
  4. 创建 PID 函数。

    def PID(err):
        # check if variables are defined before use
        # the first time the PID is called these variables will
        # not have been defined
        try: lastTime
        except NameError: lastTime = int(round(time.time()*1000)-1)
    
        try: sumError
        except NameError: sumError = 0
    
        try: lastError
        except NameError: lastError = 0
    
        # get the current time
        now = int(round(time.time()*1000))
        duration = now-lastTime
    
        # calculate the error
        error = target - err
        sumError += (error * duration)
        dError = (error - lastError)/duration
    
        # calculate PID
        output = kp * error + ki * sumError + kd * dError
    
        # update variables
        lastError = error
        lastTime = now
    
        # return the output value
        return output
    
    
  5. followLine函数替换为:

    def followLine():
        leftSpeed = speedDef
        rightSpeed = speedDef
    
        getIR()
    
        prString = ''
        for x in range(5):
            prString += ('IR' + str(x) + ': ' + str(irSensors[x]) + ' ')
        print prString
    
        # find line and correct if necessary
        if(irMaxs[0]-irThresh <= irSensors[0] <= irMaxs[0]+irThresh):
            sensorErr = 2
        elif(irMaxs[1]-irThresh <= irSensors[1] <= irMaxs[1]+irThresh):
            sensorErr = 1
        elif(irMaxs[3]-irThresh <= irSensors[3] <= irMaxs[3]+irThresh):
            sensorErr = -1
        elif(irMaxs[4]-irThresh <= irSensors[4] <= irMaxs[4]+irThresh):
            sensorErr = -1
        else:
            sensorErr = 0
    
        # get PID results
        ratio = PID(sensorErr)
    
        # apply ratio
        leftSpeed = speedDef * ratio
        rightSpeed = speedDef * -ratio
    
        # drive the motors
        driveMotors(leftSpeed, rightSpeed, driveTime)
    
    
  6. 保存文件。

  7. 把机器人放到线上。

  8. 运行代码。

再说一次,你的机器人应该试着跟随这条线。如果这样做有问题,开始处理 K p ,K i ,K d 变量。这些变量需要微调以获得最佳结果。每个机器人都不一样。

摘要

在这一章中,我们给机器人增加了一些新的传感器。红外传感器应用于循线应用。它们也可以用于检测表面的边缘。如果你想防止你的机器人从桌子上或楼梯上掉下来,这个功能是很有用的。

我们第一次实现的线跟踪使用一个基本的比例控制来操纵机器人。这是功能,但勉强。一个更好的方法是使用一个称为 PID 控制器的控制回路,它使用几个因素,包括随时间的误差,使校正更平滑。您已经了解到,您可以通过使用我们代码中表示的 PID 参数来调整 ID 设置,其中包含 K p 、K i 和 K d 变量。使用适当的值,可以完全消除振荡,使机器人平稳地跟随生产线。

九、OpenCV 简介

自从第一章介绍树莓派以来,我们已经走过了很长的路。至此,您已经了解了 Pi 和 Arduino。你已经学会了如何对两块电路板进行编程。你和传感器和马达一起工作过。你已经制造了你的机器人,并给它编了程序,让它四处漫游并跟随一条线。

然而,老实说,你并没有真正需要树莓派的功能。事实上,这有点阻碍。你用机器人做的所有事情——漫游和循线,没有 Pi 你也可以用 Arduino 做得很好。现在是时候展示圆周率的真正力量,并了解为什么你想在你的机器人使用它。

在这一章中,我们将做一些你不能单独用 Arduino 做的事情。我们将连接一个简单的网络摄像头,并开始使用通常所说的计算机视觉。

计算机视觉

计算机视觉是允许计算机分析图像并提取有用信息的算法的集合。它应用广泛,并迅速成为日常生活的一部分。如果你有一部智能手机,你很可能至少有一个应用使用计算机视觉。大多数新的中高端相机都内置了面部检测功能。脸书使用计算机视觉进行面部检测。计算机视觉被运输公司用来跟踪他们仓库里的包裹。当然,它被用于机器人导航、物体探测、物体回避和许多其他行为。

这一切都始于一幅图像。计算机分析图像以识别线条、角落和大范围的颜色。这个过程被称为特征提取,它是几乎所有计算机视觉算法的第一步。一旦这些特征被提取出来,计算机就可以将这些信息用于许多不同的任务。

面部识别是通过将特征与包含面部特征数据的 XML 文件进行比较来完成的。这些 XML 文件被称为级联。它们可用于许多不同类型的对象,而不仅仅是面部。同样的技术也可以用于物体识别。您只需向应用提供您感兴趣的对象的特性信息。

计算机视觉也包含视频。运动跟踪是计算机视觉的一个常见应用。为了检测运动,计算机比较来自静止摄像机的单个帧。如果没有运动,特征将不会在帧之间改变。因此,如果计算机识别出帧之间的差异,最有可能的是运动。基于计算机视觉的运动跟踪比红外传感器更可靠,如第八章中讨论的 PIR 传感器。

计算机视觉的一个令人兴奋的最新应用是增强现实。从视频流中提取的特征可用于识别表面上的独特图案。因为计算机知道图案,所以它可以很容易地计算出表面的角度。然后在图案上叠加 3D 模型。这个 3D 模型可以是物理的东西,比如建筑物,也可以是具有二维文本的平面对象。建筑师用这种技术向客户展示一座建筑在地平线上的样子。博物馆用它来提供更多关于展览或艺术家的信息。

所有这些都是现代环境下计算机视觉的例子。但是应用的列表太大了,无法在这里深入讨论,而且还在不断增加。

开放计算机视觉

就在几年前,计算机视觉对业余爱好者来说还不太容易。它需要大量繁重的数学运算,甚至更繁重的处理。计算机视觉项目通常使用笔记本电脑完成,这限制了它的应用。

OpenCV 已经存在一段时间了。1999 年,英特尔研究院建立了一个促进计算机视觉发展的开放标准。2012 年,它被非营利组织 OpenCV 基金会接管。你可以在他们的网站上下载最新版本。然而,要让它在 Raspberry Pi 上运行还需要一点额外的努力。我们很快就会谈到这一点。

OpenCV 是用 C++原生编写的;但是,它可以在 C、Java 和 Python 中使用。我们对 Python 实现感兴趣。因为我们的电机控制器库与 Python 3 不兼容,所以我们需要安装 OpenCV for Python 2.7。

安装 OpenCV

我们将在树莓派 上安装 OpenCV。你要确保你的树莓皮插在充电器上,而不是电池组上,给自己足够的时间来安装。我们将从源代码编译 OpenCV,这意味着我们将从互联网上下载源代码,并直接在 Pi 上构建它。需要注意的是,尽管这个过程并不困难,但确实需要很长时间,并且需要输入许多 Linux 命令。我通常在晚上开始这个过程,让最终的构建运行一整夜。

  1. 登录您的树莓派。

  2. 在 Pi 上打开一个终端窗口。

  3. 确保 Raspberry Pi 已更新。

    sudo apt-get update
    sudo apt-get upgrade
    sudo rpi-update
    sudo reboot
    
    
  4. 这些命令安装了构建 OpenCV 的先决条件。

    sudo apt-get install build-essential git cmake pkg-config
    sudo apt-get install libjpeg-dev libtiff5-dev libjasper-dev libpng12-dev
    sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev
    sudo apt-get install libxvidcore-dev libx264-dev
    sudo apt-get install libgtk2.0-dev
    sudo apt-get install libatlas-base-dev gfortran
    
    
  5. 下载 OpenCV 源代码和 OpenCV 贡献的文件。贡献的文件包含了许多 OpenCV 主发行版中没有的功能。

    cd ~
    git clone https://github.com/Itseez/opencv.git
    cd opencv
    git checkout 3.1.0
    cd ~
    git clone https://github.com/Itseez/opencv_contrib.git
    cd opencv_contrib
    git checkout 3.1.0
    
    
  6. 安装 Python 开发库和 pip。

    sudo apt-get install python2.7-dev
    wget https://bootstrap.pypa.io/get-pip.py
    sudo python get-pip.py
    
    
  7. 确保安装了 NumPy。

    pip install numpy
    
    
  8. 准备编译的源代码。

    cd ~/opencv
    mkdir build
    cd build
    cmake -D CMAKE_BUILD_TYPE=RELEASE \
        -D CMAKE_INSTALL_PREFIX=/usr/local \
        -D INSTALL_C_EXAMPLES=OFF \
        -D INSTALL_PYTHON_EXAMPLES=ON \
        -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib/modules \
        -D BUILD_EXAMPLES=ON ..
    
    
  9. 现在让我们编译源代码。这部分需要一段时间。有些人试图利用树莓派 的 ARM CPU 中的所有四个核心。然而,我发现这很容易出错,而且它从来没有为我工作过。我的建议是咬紧牙关:让 Pi 决定要使用的内核数量,并让它运行。如果您想大胆尝试,您可以通过在下面的行

    make
    
    

    中添加–j4开关来强制 Pi 使用四个内核

  10. 如果您尝试了–j4 开关,但失败了,大约在第四个小时,请输入以下行:

```py
make clean
make

```
  1. 编译好源代码后,现在就可以安装它了。
```py
sudo make install
sudo ldconfig

```
  1. 通过打开 Python 命令行来测试安装。
```py
python

```
  1. 导入 OpenCV。
```py
>>>import cv2

```

现在,您的 Raspberry Pi 上应该已经安装了 OpenCV 的运行版本。如果导入命令不起作用,您需要确定它没有安装的原因。互联网是您排除故障的指南。

选择摄像机

在我们真正让 OpenCV 在我们的机器人上工作之前,我们需要安装一个摄像头。Raspberry Pi 有几个选项:Pi 摄像头或 USB 网络摄像头。

Pi 摄像机直接连接到专门为其设计的端口。一旦连接上,您需要进入 raspi-config 并启用它。Pi 摄像头的优势在于它比 USB 摄像头快一点,因为它直接连接到电路板。它不通过 USB 串行总线。这让它有了一点优势。

大多数 Pi 摄像机都配有一根短的 6 英寸带状电缆。由于树莓派在我们机器人上的位置,这是不够的。可以订购更长的电缆。Adafruit 有几个选择。但是,对于这个项目,我们将使用一个简单的网络摄像机。

USB 摄像头在任何电子产品零售商处都很容易买到。网上也有很多选择。对于这个基本的应用,我们不需要任何特别健壮的东西。任何能提供像样图像的相机都可以。拥有高分辨率也不是问题。由于我们是在 Raspberry Pi 的有限资源下运行相机,较低的分辨率实际上会有助于性能。记住,OpenCV 逐像素分析每一帧。图像中的像素越多,需要处理的事情就越多。

对于我的机器人,我选择了直播!Cam Sync HD by Creative(见图 9-1 ),这是一款基本的高清网络摄像头,内置麦克风,通过标准 USB 2 端口操作。这个项目我们不需要麦克风,但将来可能会需要。它可以捕捉 720p 高清视频,这对我们的机器人来说可能有点多,但如果性能受到影响,我总是在软件中降低分辨率。

A457480_1_En_9_Fig1_HTML.jpg

图 9-1

Creative Live! Cam Sync HD

安装摄像机

大多数网络摄像头都安装在显示器的顶部。他们通常有一个折叠夹,为监视器上的摄像机提供支撑。不幸的是,这些夹子通常是作为相机机身的一部分模制而成的,如果不损坏相机就无法拆除。这当然适用于生活!凸轮同步。所以,再一次,一点点创造力开始发挥作用。

我用来安装传感器的支架以 45 度角从机器人的前面脱落。为了让自己轻松一点,我选择不在相机支架上钻孔。相反,我使用我的可靠的安装胶带和一对安装支架。当我爬上去的时候,我想让它爬得相当高,并且稍微向下。这个想法是给它最好的视角,以及任何直接在它前面的物体。我还希望镜头尽可能靠近中心轴,以使软件方面的事情更简单。图 9-2 显示安装摄像头后的机器人。

A457480_1_En_9_Fig2_HTML.jpg

图 9-2

Camera mounted on robot

OpenCV 基础知识

OpenCV 有许多功能。它拥有 500 多个库和数千种功能。这是一个非常大的主题——太大了,一章无法涵盖。我将讨论在您的机器人上执行一些简单任务所需的基础知识。

我说简单的任务。这些任务非常简单,因为 OpenCV 抽象了后台发生的大量数学运算。当我考虑几年前机器人爱好的状态时,我发现即使是最基本的东西也能轻易获得,这真是令人惊讶。

目标是在本章结束时建造一个能够识别球并向球移动的机器人。我介绍的函数将帮助我们实现这个目标。我强烈建议花时间浏览 OpenCV 网站上的一些教程( https://opencv.org )。

要在 Python 代码中使用 OpenCV,您需要导入它。而且,当您这样做时,您可能还需要导入 NumPy 库。NumPy 增加了许多数学和数字处理功能,使得使用 OpenCV 更加容易。所有与图像相关的代码都应该以此开头:

import cv2
import numpy as np

在本章的代码讨论中,我假设这已经完成了。以cv2为前缀的函数是 OpenCV 函数。如果以np为前缀,则为 NumPy 函数。如果你想扩展你在这本书里读到的内容,这一点很重要。OpenCV 和 NumPy 是两个独立的库,但是 OpenCV 经常使用 NumPy。

使用图像

在本节中,您将学习如何从文件中打开图像,以及如何从摄像机中捕捉实时视频。然后,我们将看看如何处理和分析这些图像,从中获取有用的信息。具体来说,我们将研究如何识别特定颜色的球,并跟踪它在帧中的位置。

但是首先,我们有一个先有鸡还是先有蛋的问题。我们需要在所有的练习中看到我们图像处理的结果。为此,我们需要从如何显示图像开始。这是我们会广泛使用的东西,而且非常容易使用。但我想确保在你学习如何捕捉图像之前,我先覆盖它。

显示图像

在 OpenCV 中显示图像实际上非常容易。imshow()功能提供了这一功能。该函数适用于静态图像和视频图像,并且两者之间的实现不会改变。imshow()功能打开一个新窗口显示图像。当您调用它时,您必须为窗口以及您想要显示的图像或框架提供一个名称。

这是关于 OpenCV 如何处理视频的重要一点。因为 OpenCV 将视频视为一系列独立的帧,所以几乎所有用于修改或分析图像的功能都适用于视频。这显然包括imshow()

如果我们想显示一个加载到img变量中的图像,它看起来会像这样:

cv2.imshow('img', img)

在这个例子中,第一个参数是窗口的名称。它出现在窗口的标题栏中。第二个参数是保存图像的变量。显示视频的格式完全相同。我通常使用变量 cap 进行视频捕获,因此代码如下所示:

cv2.imshow('cap', cap)

如您所见,代码是相同的。同样,这是因为 OpenCV 将视频视为一系列独立的帧。实际上,视频捕获依靠一个循环来连续捕获下一帧。所以从本质上来说,显示一个文件中的静止图像和一个相机中的单独帧是完全一样的。

还有一个元素要显示。为了实际显示图像,imshow()函数要求也调用waitKey()waitKey()功能等待键盘按键按下指定的毫秒数。许多人用这个来捕捉一个退出键。除非我需要按键,否则我通常将它传递为零。

cv2.waitKey(0)

我们在本章中广泛使用了imshow()waitKey()

捕捉图像

使用 OpenCV 所需的图像有几个来源,所有这些来源都是两个因素的变体:文件或相机,以及静止或视频。在大多数情况下,我们只关心来自摄像机的视频,因为我们使用 OpenCV 进行导航。但是所有的方法都有优点。

打开一个静止图像文件是学习新技术的一个很好的方法,尤其是当您正在处理计算机视觉的特定方面时。例如,如果您正在学习如何使用滤镜来识别某种颜色的球,那么使用由三个不同颜色的球组成的静止图像(除此之外别无其他)可以让您专注于特定的目标,而不必担心用于捕捉实时视频流的底层框架。哦,这是一个伏笔,如果你没有意识到这一点。

从用相机捕捉静止图像中学到的技术可以应用到真实环境中。它允许您通过使用包含真实世界元素的图像来改进或微调代码。

显然,捕捉现场视频是我们在机器人中使用的目标。实时视频流是我们用来识别我们的目标对象,然后导航到它。随着您的计算机视觉经验的增长,您可能会将运动检测或其他方法添加到您的清单中。由于机器人上的摄像头的目的是实时采集环境信息,因此需要实时视频。

文件中的视频对于学习过程也非常有用。您可能希望从机器人捕捉实时视频,并将其保存到文件中供以后分析。比方说,你正在利用一天中你能找到的任何空闲时间从事你的机器人项目。你可以随身携带你的笔记本电脑,但是携带一个机器人却是另一回事。因此,如果你从你的机器人那里录制视频,你可以在没有机器人陪伴的情况下进行你的计算机视觉算法。

请记住,Python 和 OpenCV 的一大优点是它们是抽象的,并且在很大程度上是平台独立的。所以,你在 Windows 机器上写的代码移植到你的 Raspberry Pi 上。

出差,并希望在酒店休息一段时间?去家人那里度假,偶尔需要离开一下?在午餐时间或课间溜进一点机器人程序?将录制的视频与 Python 和 OpenCV 的本地实例一起使用,并使用您的检测算法。当你回家后,你可以把代码转移到你的机器人上,并进行现场测试。

在本节中,我们使用前三种技术。我向您展示了如何保存和打开视频文件,但在大多数情况下,我们将使用静止图像来学习检测算法,使用实时视频来学习跟踪。

打开图像文件

OpenCV 使得处理图像和文件变得非常容易,尤其是考虑到后台发生的事情使得这些操作成为可能。打开图像文件也不例外。我们使用imread()函数从本地存储器打开图像文件。imread()函数有两个参数:文件名和颜色类型标志。打开文件显然需要文件名。颜色类型标志决定了是以彩色还是灰度打开图像。

让我们打开并显示一个图像。我将使用三个彩色球的图像,这也是本章后面学习如何检测颜色的图像。如果您已经安装了 Python 和 OpenCV,这个练习可以在 Pi 或您的计算机上完成。

  1. 打开空闲的 IDE 并创建一个新文件。

  2. 将文件另存为open_image.py

  3. 输入以下代码:

    import cv2
    
    img = cv2.imread('color_balls_small.jpg')
    cv2.imshow('image',img)
    
    cv2.waitKey(0)
    
    
  4. 保存文件。

  5. 打开终端窗口。

  6. 导航到保存文件的文件夹。

  7. 输入python open_image.py并按回车键。

一个窗口打开,在白色背景上显示三个彩球的图像(参见图 9-3 )。按任意键关闭它。

A457480_1_En_9_Fig3_HTML.jpg

图 9-3

Three colored balls

由于 IDLE 与基于 Linux 的机器上的 GUI 系统的交互方式,如果您直接从 IDLE 运行代码,图像窗口将不会正常关闭。然而,通过从终端运行代码,我们没有这个问题。

捕捉视频

用相机捕捉视频与打开文件略有不同。使用视频还有几个步骤。一个变化是我们必须使用一个循环来获得多个帧;否则,OpenCV 只会捕获单个帧,这不是我们想要的。通常使用开放的while回路。这会捕捉视频,直到我们主动停止它。

为了使测试更容易,我将球直接放在摄像机的前面(见图 9-4 )。现在,我们只想捕捉图像。

为了从摄像机中捕捉视频,我们将创建一个videoCapture()对象,然后在一个循环中使用read()方法来捕捉帧。read()方法返回两个对象:一个返回值和一个图像帧。返回值只是一个验证读取成功或失败的整数。如果读取成功,则值为 1;否则,读取失败,并返回 0。为了防止导致代码出错的错误,可以测试读取是否成功。

A457480_1_En_9_Fig4_HTML.jpg

图 9-4

Ball positioned in front of the robot for testing

我们关心图像帧。如果读取成功,则返回一个图像。如果不是,则返回一个空对象来代替它。由于空对象不能访问 OpenCV 方法,所以当您试图修改或操作图像时,您的代码将会崩溃。这就是为什么测试读取操作是否成功是个好主意。

查看摄像机

在下一个练习中,我们将打开之前安装的摄像机来观看视频。

  1. 打开空闲的 IDE 并创建一个新文件。

  2. 将文件另存为 view_camera.py。

  3. 输入以下代码:

    import cv2
    import numpy as np
    
    cap = cv2.VideoCapture(0)
    
    while(True):
        ret,frame = cap.read()
    
        cv2.imshow('video', frame)
        if cv2.waitKey(1) & 0xff == ord('q'):
            break
    
    cap.release()
    cv2.destroyAllWindows()
    
    
  4. 保存文件。

  5. 打开终端窗口。

  6. 导航到保存脚本的工作文件夹。

  7. 类型sudo python view_camera.py

这将打开一个窗口,显示您的摄像机看到的内容。如果您使用远程桌面会话来处理 Pi,您可能会看到以下警告消息:Xlib:extension RANR missing on display:10。此消息意味着系统正在寻找 vncserver 中不包含的功能。可以忽略。

如果您担心视频图像的刷新率,请记住,当我们通过远程桌面会话运行几个窗口时,我们对 Raspberry Pi 的要求非常高。如果您连接显示器和键盘来访问 Pi,它会运行得更快。如果您在没有可视化的情况下运行视频捕获,它会运行得更快。

录制视频

录制视频是查看摄像机的延伸。要录制,您必须声明您将使用的视频编解码器,然后设置将传入视频写入 SD 卡的VideoWriter对象。

OpenCV 使用 FOURCC 码来指定编解码器。FOURCC 是视频编解码器的四字符代码。你可以在 www.fourcc.org 找到更多关于 FOURCC 的信息。

当创建VideoWriter对象时,我们需要提供一些信息。首先,我们必须提供文件名来保存视频。接下来,我们提供编解码器,然后是帧速率和分辨率。一旦创建了VideoWriter对象,我们只需使用VideoWriter对象的write()方法将每个从写到文件中。

让我们录制一些机器人的视频。我们将使用 XVID 编解码器写入一个名为test_video.avi的文件。我们将使用上一个练习中的视频捕获代码,而不是从头开始。

  1. 在空闲 IDE 中打开 view_camera.py 文件。

  2. 选择文件➤另存为并将文件另存为record_camera.py

  3. 更新代码。在下面,新行以粗体显示:

    import cv2
    import numpy as np
    
    cap = cv2.VideoCapture(0)
    fourcc = cv2.VideoWriter_fourcc(*'XVID')
    vidWrite = cv2.VideoWriter('test_video.avi', \
                    fourcc, 20, (640,480))
    
    while(True):
        ret,frame = cap.read()
    
        vidWrite.write(frame)
    
        cv2.imshow('video', frame)
        if cv2.waitKey(1) & 0xff == ord('q'):
            break
    
    cap.release()
    
    vidWrite.release()
    
    cv2.destroyAllWindows()
    
    
  4. 保存文件。

  5. 打开终端窗口。

  6. 导航到保存脚本的工作文件夹。

  7. 类型sudo python record_camera.py

  8. 让视频运行几秒钟,然后按 Q 结束程序并关闭窗口。

现在,您的工作目录中应该有一个视频文件。接下来,我们将看看从文件中读取视频。

代码中有几项需要注意。当我们创建VideoWriter对象时,我们以元组的形式提供了视频分辨率。这是整个 OpenCV 中非常常见的做法。此外,我们必须释放VideoWriter对象。这关闭了文件的写入。

从文件中读取视频

从文件中回放视频与从摄像机中观看视频完全相同。唯一的区别是,我们提供要播放的文件的名称,而不是向视频设备提供索引。我们将使用ret变量来测试视频文件的结尾;否则,当没有更多的视频播放时,我们会得到一个错误。

在本练习中,我们将简单地回放我们在上一个练习中录制的视频。代码应该看起来非常熟悉。

  1. 打开空闲的 IDE 并创建一个新文件。

  2. 将文件另存为view_video.py

  3. 输入以下代码:

    import cv2
    import numpy as np
    
    cap = cv2.VideoCapture('test_video.avi')
    
    while(True):
        ret,frame = cap.read()
    
        if ret:
            cv2.imshow('video', frame)
        if cv2.waitKey(1) & 0xff == ord('q'):
            break
    
    cap.release()
    cv2.destroyAllWindows()
    
    
  4. 保存文件。

  5. 打开终端窗口。

  6. 导航到保存脚本的工作文件夹。

  7. 类型sudo python view_video.py

一个新窗口打开。它显示了我们在上一个练习中记录的视频文件。当到达文件结尾时,视频停止。按 Q 结束程序并关闭窗口。

图像转换

现在你知道了更多关于如何获取图像的知识,让我们来看看我们可以用它们做些什么。我们将看一些非常基本的操作。选择这些操作是因为它们将帮助我们达到跟踪球的目标。OpenCV 非常强大,它拥有比我在这里介绍的更多的功能。

轻弹

很多时候,摄像机在项目中的摆放并不理想。经常,我不得不把相机倒过来装,或者因为这样或那样的原因,我需要翻转图像。

幸运的是,OpenCV 用flip()方法使这变得非常简单。flip()方法有三个参数:要翻转的图像、指示如何翻转的代码和翻转图像的目的地。最后一个参数仅在您想要将翻转的图像指定给另一个变量时使用,但是您可以在适当的位置翻转图像。

通过提供 flipCode,可以水平和/或垂直翻转图像。flipCode 为正、负或零。零水平翻转图像,正值垂直翻转图像,负值在两个轴上翻转图像。通常情况下,您会在两个轴上翻转图像,以有效地将其旋转 180 度。

让我们使用之前使用的三个球的图像来说明翻转帧。

  1. 打开空闲的 IDE 并创建一个新文件。

  2. 将文件另存为flip_image.py

  3. 输入以下代码:

    import cv2
    
    img = cv2.imread('color_balls_small.jpg')
    h_img = cv2.flip(img, 0)
    v_img = cv2.flip(img, 1)
    b_img = cv2.flip(img, -1)
    
    cv2.imshow('image', img)
    cv2.imshow('horizontal', h_img)
    cv2.imshow('vertical', v_img)
    cv2.imshow('both', b_img)
    
    cv2.waitKey(0)
    
    
  4. 保存文件。

  5. 打开终端窗口。

  6. 导航到保存文件的文件夹。

  7. 输入python flip_image.py并按回车键。

打开四个窗口,每个窗口都有不同版本的图像文件。按任意键退出。

调整大小

您可以调整图像的大小。这有助于减少处理图像所需的资源。图像越大,需要的内存和 CPU 资源就越多。为了调整图像的大小,我们使用了resize()方法。这些参数是要缩放的图像、作为元组的所需维度以及插值。

插值是一种数学方法,用于确定如何处理像素的删除或添加。请记住,在处理图像时,您实际上是在处理一个多维数组,该数组包含组成图像的每个点或像素的信息。缩小图像时,您正在删除像素。当你放大一幅图像时,你是在增加像素。插值是发生这种情况的方法。

有三种插值选项。INTER_AREA 最适合用于归约。INTER_CUBIC 和 INTER_LINEAR 都适合放大图像,INTER_LINEAR 是两者中速度较快的。如果没有提供插值,OpenCV 使用 INTER_LINEAR 作为缩小和放大的缺省值。

三个球的图像目前为 800×533 像素。虽然它不是大号的,但我们会把它做小一点。让我们把两个轴的大小都设为当前大小的一半。为此,我们将使用 INTER_AREA 插值。

  1. 打开空闲的 IDE 并创建一个新文件。

  2. 将文件另存为resize_image.py

  3. 输入以下代码:

    import cv2
    
    img = cv2.imread('color_balls_small.jpg')
    x,y = img.shape[:2]
    resImg = cv2.resize(img, (y/2, x/2), interpolation = cv2.INTER_AREA)
    
    cv2.imshow('image', img)
    cv2.imshow('resized', resImg)
    
    cv2.waitKey(0)
    
    
  4. 保存文件。

  5. 打开终端窗口。

  6. 导航到保存文件的文件夹。

  7. 输入python resize_image.py并按回车键。

应该开了两扇窗。第一个有原始图像。第二个显示缩小的图像。按任意键关闭窗口。

使用颜色

颜色显然是处理图像的一个非常重要的部分。因此,它是 OpenCV 中非常重要的一部分。颜色可以做很多事情。我们将重点关注完成我们的最终目标所需的几个关键要素,即识别和用机器人追球。

色彩空间

处理颜色的一个关键要素是颜色空间,它描述了 OpenCV 如何表达颜色。在 OpenCV 中,颜色由一系列数字表示。色彩空间决定了这些数字的含义。

OpenCV 的默认颜色空间是 BGR。这意味着每种颜色都由 0 到 255 之间的三个整数来描述,它们依次对应于三个颜色通道——蓝色、绿色和红色。表示为(255,0,0)的颜色在蓝色通道中具有最大值,绿色和红色都为零。这代表纯蓝色。鉴于此,(0,255,0)为绿色,(0,0,255)为红色。值(0,0,0)代表黑色,没有任何颜色,而(255,255,255)代表白色。

如果你过去曾与图形打交道,BGR 与你可能习惯的截然相反。大多数数字图形都是用 RGB 来描述的,即红色、绿色和蓝色。所以,这可能需要一点时间来适应。

有许多颜色空间。我们关心的是 BGR、RGB、HSV 和灰度。我们已经讨论了默认的颜色空间,BGR,和常见的 RGB 颜色空间。HSV 是色调、饱和度和值。色调代表 0 到 180 范围内的颜色。饱和度表示颜色从 0 到 255 有多白。值是从 0 到 255 的颜色与黑色的距离。如果饱和度和值都为 0,则颜色为灰色。饱和度和值 255 是色调的最亮版本。

色调有点复杂。它在 0 到 180 的范围内,0 和 180 都是红色。这就是记住色轮的重要性。如果 0 和 180 在红色空间中间的轮盘顶部相遇,当你围绕轮盘顺时针移动时,色相= 30 是黄色,色相= 60 是绿色,色相= 90 是蓝绿色,色相= 120 是蓝色,色相= 150 是紫色,色相= 180 将我们带回红色。

你最常遇到的是灰度。灰度就是它听起来的样子:图像的黑白版本。特征检测算法使用它来创建遮罩。我们用它来过滤物体。

要将图像转换到不同的色彩空间,可以使用cvtColor方法。它需要两个参数:图像和颜色空间常数。颜色空间常量内置于 OpenCV 中。分别是 COLOR_BGR2RGB、COLOR_BGR2HSV、COLOR_BGR2GRAY。你看到那里的模式了吗?如果您想从 RGB 颜色空间转换到 HSV 颜色空间,常量应该是 COLOR_RGB2HSV。

让我们把三个彩球的图像转换成灰度图像。

  1. 打开空闲的 IDE 并创建一个新文件。

  2. 将文件另存为gray_image.py

  3. 输入以下代码:

    import cv2
    
    img = cv2.imread('color_balls_small.jpg')
    grayImg = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    cv2.imshow('img', img)
    cv2.imshow('gray', grayImg)
    
    cv2.waitKey(0)
    
    
  4. 保存文件。

  5. 打开终端窗口。

  6. 导航到保存文件的文件夹。

  7. 输入python gray_image.py并按回车键。

这将打开两个窗口:一个显示原始彩色图像,另一个显示灰度图像。单击任意键退出程序并关闭窗口。

颜色过滤器

对颜色进行过滤需要的代码非常少,但同时,这可能会有点令人沮丧,因为您通常不是在寻找特定的颜色,而是一个颜色范围。颜色很少是纯粹的,只有一种价值。这就是为什么我们希望能够在色彩空间之间转换。当然,我们可以在 BGR 寻找红色。但是要做到这一点,我们需要这三个值的具体范围。在所有色彩空间都是如此的情况下,通常在 HSV 空间中更容易调整到您需要的范围。

用于过滤特定颜色的策略相当简单,但是需要几个步骤,并且需要记住一些事情。

首先,我们将在 HSV 颜色空间中复制图像。然后,我们应用我们的过滤范围,使其成为自己的图像。为此,我们使用inRange()方法。它有三个参数:我们要应用滤镜的图像、下限值范围和上限值范围。inRange方法扫描提供的图像中的所有像素,以确定它们是否在指定的范围内。如果是,则返回 true 或 1;否则,它返回 0。这留给我们的是一个黑白图像,我们可以用它作为一个面具。

接下来,我们使用bitwise_and()方法应用蒙版。该方法获取两幅图像,并返回像素匹配的区域。因为这不是我们想要的,我们需要耍点小花招。出于我们的目的,bitwise_and需要三个参数:图像 1、图像 2 和一个遮罩。因为我们想要返回遮罩显示的所有内容,所以图像 1 和图像 2 都使用我们的原始图像。然后,我们通过指定遮罩参数来应用遮罩。因为我们忽略了几个可选参数,所以我们需要显式地指定掩码参数,就像这样:mask = mask_image。结果是图像只显示了我们要过滤的颜色。

演示这一点的最简单的方法是通过它走一走。一旦你知道发生了什么,代码其实很简单。

  1. 打开空闲的 IDE 并创建一个新文件。

  2. 将文件另存为blue_filter.py

  3. 输入以下代码:

    import cv2
    
    img = cv2.imread("color_balls_small.jpg")
    imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    
    lower_blue = np.array([80,120,120])
    upper_blue = np.array([130,255,255])
    
    blueMask = cv2.inRange(imgHSV,lower_blue,upper_blue)
    
    res = cv2.bitwise_and(img, img, mask=blueMask)
    
    cv2.imshow('img', img)
    cv2.imshow('mask', blueMask)
    cv2.imshow('blue', res)
    
    cv2.waitKey(0)
    
    
  4. 保存文件。

  5. 打开终端窗口。

  6. 导航到保存文件的文件夹。

  7. 输入python blue_filter.py并按回车键。

打开三个窗口,显示我们图像的不同版本。首先是常规图像。第二张是黑白图像,充当我们的面具。第三个是最终的蒙版图像。仅显示蒙版白色区域下的像素。

让我们花点时间浏览一下代码,弄清楚我们在做什么以及为什么做。

我们像处理所有脚本一样开始,先导入 OpenCV 和 NumPy,然后加载图像。

import cv2
import numpy as np
img = cv2.imread("color_balls_small.jpg")

接下来,我们复制图像并将其转换到 HSV 颜色空间。

imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

一旦进入 HSV 颜色空间,就更容易过滤蓝色球。正如我所讨论的,我们知道纯蓝色的色调值为 120。因为我们过滤的对象不太可能是纯蓝色的,所以我们需要给它一个颜色范围。在这种情况下,我们要寻找从 80(介于绿色和蓝色之间)到 130 的所有内容。我们还想过滤不是接近白色或接近黑色的颜色,所以我们使用值 120 和 255 作为饱和度和值范围。为了确保过滤器范围是 OpenCV 能够理解的格式,我们将它们创建为 NumPy 数组。

lower_blue = np.array([80,120,120])
upper_blue = np.array([130,255,255])

有了指定的滤镜范围,我们可以使用它们和inRange()方法来确定图像的 HSV 版本中的像素是否在我们正在寻找的蓝色范围内。这将创建遮罩图像以排除所有非蓝色像素。

blueMask = cv2.inRange(imgHSV,lower_blue,upper_blue)

接下来,我们使用bitwise_and()来应用我们的面具。因为我们希望返回蒙版中图像的所有像素,所以我们将原始图像作为图像 1 和图像 2 传递。这将图像与其自身进行比较,并返回整个图像,因为图像中的每个像素都与其自身匹配。

res = cv2.bitwise_and(img, img, mask=blueMask)

最后,我们显示原始图像、遮罩和过滤后的图像。然后,在我们关闭窗口并退出程序之前,我们等待一个键被按下。

cv2.imshow('img', img)
cv2.imshow('mask', blueMask)
cv2.imshow('blue', res)

cv2.waitKey(0)

正如你所看到的,一旦你知道它是如何工作的,过滤颜色是非常容易的。当你过滤红色时,事情变得有点复杂。红色出现在色调光谱的低端和高端,因此您必须创建两个滤镜并组合生成的遮罩。这可以很容易地用 OpenCV 的add()方法来完成,它看起来像这样:

combinedMask = cv2.add(redMask1, redMask2)

最后,你得到的图像只有你想要的像素。对人眼来说,它很容易被识别为相关组。对于计算机来说,情况并非如此。本来,计算机不能识别黑色像素和蓝色像素之间的差异。这就是斑点检测发挥作用的地方。

斑点和斑点检测

斑点是相似像素的集合。它们可以是任何东西,从单调的圆形到 jpeg 图像。对计算机来说,像素就是像素,它无法区分球的图像和平面的图像。这就是计算机视觉如此具有挑战性的原因。我们已经开发了许多不同的技术来尝试推断关于图像的信息;每一种都在速度和准确性方面有所取舍。

大多数技术使用称为特征提取的过程,特征提取是一组算法的总称,这些算法对图像中的突出特征进行分类,如线条、边缘、大范围颜色等。一旦提取了这些特征,就可以对它们进行分析或与其他特征进行比较,以确定图像。这就是面部检测和运动检测等功能的工作原理。

我们将使用一种更简单的方法来跟踪一个对象。我们将使用上一节中的颜色过滤技术来识别大面积的颜色,而不是提取细节特征并进行分析。然后,我们将使用内置函数来收集关于像素组的信息。这种更简单的技术被称为斑点检测。

找到一个斑点

OpenCV 使得斑点检测变得相当容易,尤其是在我们过滤掉所有我们不想要的东西之后。一旦图像被过滤,我们可以使用掩模进行干净斑点检测。OpenCV 的SimpleBlobDetector类识别斑点的位置和大小。

这个类并不像你想象的那么简单。它内置了许多需要启用或禁用的参数。如果启用,您需要确保这些值适用于您的应用。

设置参数的方法是SimpleBlobDetector_Params()。创建检测器的方法是SimpleBlobDetector_create()。您将参数传递给 create 方法,以确保一切设置正确。

一旦设置好参数并正确创建了检测器,就可以使用detect()方法来识别关键点。在简单斑点检测器的情况下,关键点表示任何检测到的斑点的中心和大小。

最后,我们使用drawKeyPoints()方法在斑点周围画一个圆。默认情况下,这会在斑点的中心绘制一个小圆。然而,可以传递一个标志,使得圆的大小相对于斑点的大小。

让我们看一个例子。我们将使用之前练习中的过滤器代码,并添加斑点检测。在本练习中,我们过滤图像中的蓝色球。然后我们用蒙版找到球的中心,围绕它画一个圆。

  1. 打开空闲的 IDE 并创建一个新文件。

  2. 将文件另存为simple_blob_detect.py

  3. 输入以下代码:

    import cv2
    import numpy as np
    
    img = cv2.imread("color_balls_small.jpg")
    imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    
    # setup parameters
    params = cv2.SimpleBlobDetector_Params()
    
    params.filterByColor = False
    params.filterByArea = False
    params.filterByInertia = False
    params.filterByConvexity = False
    params.filterByCircularity = False
    
    # create blob detector
    det = cv2.SimpleBlobDetector_create(params)
    
    lower_blue = np.array([80,120,120])
    upper_blue = np.array([130,255,255])
    
    blueMask = cv2.inRange(imgHSV,lower_blue,upper_blue)
    
    res = cv2.bitwise_and(img, img, mask=blueMask)
    
    # get keypoints
    keypnts = det.detect(blueMask)
    
    # draw keypoints
    cv2.drawKeypoints(img, keypnts, img, (0,0,255),
           cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
    
    cv2.imshow('img', img)
    cv2.imshow('mask', blueMask)
    cv2.imshow('blue', res)
    
    # print the coordinates and size of keypoints to terminal
    for k in keypnts:
        print k.pt[0]
        print k.pt[1]
        print k.size
    
    cv2.waitKey(0)
    
    
  4. 保存文件。

  5. 打开终端窗口。

  6. 导航到保存文件的文件夹。

  7. 输入python simple_blob_detect.py并按回车键。

这将打开图像的三个版本。但是,原始图像现在有一个红色的圆圈围绕着蓝色的球。在终端窗口中,我们打印了球的中心坐标和大小。在本章的后面,当我们开始跟踪球时,会用到球的中心。

参数

SimpleBlobDetector类需要几个参数才能正常工作。强烈建议通过将相应的参数设置为 True 或 False 来显式启用或禁用所有过滤器选项。如果启用了过滤器,您还需要为其设置参数。默认参数被配置为提取黑色圆形斑点。

在上一个练习中,我们简单地禁用了所有过滤器。由于我们正在处理一个球的过滤图像,并且我们在图像中只有一个斑点,我们不需要添加其他过滤器。虽然从技术上讲,您可以单独使用 SimpleBlobDetector 的参数,而不屏蔽所有其他的参数,但是在拨入所有参数以获得我们想要的结果时,这可能会更具挑战性。此外,我们使用的方法允许您更深入地了解 OpenCV 在后台做什么。

理解 SimpleBlobDetector 的工作方式对于更好地理解如何使用过滤器是很重要的。有几个参数可用于微调结果。

首先发生的是通过应用阈值将图像转换成几个二值图像。minThresholdmaxThreshold决定整体范围,而thresholdStep决定阈值之间的距离。

然后使用findContours()对每个二进制图像进行轮廓处理。这允许系统计算每个斑点的中心。已知中心后,使用minDistanceBetweenBlobs参数将几个斑点组合成一组。

组的中心作为关键点返回,组的总直径也是如此。计算每个滤波器的参数并应用滤波器。

过滤器

下面列出了过滤器及其相应的参数。

filterByColor

这将过滤每个二进制图像的相对强度。它测量斑点中心的强度值,并将其与参数blobColor进行比较。如果它们不匹配,则该斑点不合格。强度从 0 到 255 测量;0 为暗,255 为亮。

过滤区域

当单个斑点被分组时,计算它们的总面积。该过滤器寻找minAreamaxArea之间的斑点。

过滤循环

圆度通过公式

$$ \frac{4^{\ast }{\pi}^{\ast } Area}{perimeter^{\ast } perimeter} $$

计算

这将返回一个介于 0 和 1 之间的比率,该比率将与minCircularitymaxCircularity进行比较。如果该值在这些参数之间,则结果中包含该斑点。

过滤惯性

惯性是对斑点被拉长程度的估计。它是一个介于 0 和 1 之间的比率。如果值在minInertiaRatiomaxInertiaRatio之间,则在关键点结果中返回斑点。

filterby 凸性

凸度是一个介于 0 和 1 之间的比值。它测量斑点中凸曲线和凹曲线之间的比率。凸度参数为minConvexitymaxConvexity

斑点跟踪

我们在上一节中看到,斑点中心的 x 和 y 坐标作为关键点的一部分返回,用于跟踪斑点。要跟踪 blob,您需要使用来自机器人摄像机的实时视频流,然后定义跟踪对您的项目意味着什么。最简单的跟踪形式是简单地用斑点移动生成的圆。

  1. 打开空闲的 IDE 并创建一个新文件。

  2. 将文件另存为blob_tracker.py

  3. 输入以下代码:

    import cv2
    import numpy as np
    
    cap = cv2.VideoCapture(0)
    
    # setup detector and parameters
    params = cv2.SimpleBlobDetector_Params()
    
    params.filterByColor = False
    params.filterByArea = True
    params.minArea = 20000
    params.maxArea = 30000
    params.filterByInertia = False
    params.filterByConvexity = False
    params.filterByCircularity = True
    params.minCircularity = 0.5
    params.maxCircularity = 1
    
    det = cv2.SimpleBlobDetector_create(params)
    
    # define blue
    lower_blue = np.array([80,60,20])
    upper_blue = np.array([130,255,255])
    
    while True:
        ret, frame = cap.read()
    
        imgHSV = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    
        blueMask = cv2.inRange(imgHSV,lower_blue,upper_blue)
        blur= cv2.blur(blueMask, (10,10))
    
        res = cv2.bitwise_and(frame, frame, mask=blueMask)
    
        # get and draw keypoint
        keypnts = det.detect(blur)
    
        cv2.drawKeypoints(frame, keypnts, frame, (0,0,255),
                          cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
    
        cv2.imshow('frame', frame)
        cv2.imshow('mask', blur)
    
        for k in keypnts:
            print k.size
    
        if cv2.waitKey(1) & 0xff == ord('q'):
            break
    
    cap.release()
    cv2.destroyAllWindows()
    
    
  4. 保存文件。

  5. 打开终端窗口。

  6. 导航到保存文件的文件夹。

  7. 输入sudo python blob_tracker.py并按回车键。

两个窗口打开:一个显示用于过滤颜色的遮罩,另一个显示视频流。应该在斑点周围画一个圆。

我启用了filterByAreafilterByCircularity来确保我只得到球。您可能需要调整检测器的参数来微调过滤器。

追球机器人

你现在知道如何用安装在机器人上的网络摄像头跟踪一个斑点了。在第八章中,你学习了一种叫做 PID 控制器的算法。当我们将 PID 控制器与我们的球跟踪程序结合起来时会发生什么?

接下来,让我们给这个小机器人编程,让它去追逐它一直在追踪的那个蓝色的球。为此,您将使用您刚刚学到的关于斑点跟踪的知识以及您在第八章中学到的知识。

PID 控制器期望以偏离期望结果的形式输入。因此,我们需要从定义期望的结果开始。在这种情况下,目标只是将球保持在框架的中间。因此,我们的误差值将是中心的方差,这也意味着我们需要定义帧的中心。一旦我们定义了中心,偏差就是从中心的 x 位置减去球的 x 位置。我们也将从中心的 y 位置减去球的 y 位置。

现在我们可以使用两个 PID 控制器来保持球在框架的中心。第一个控制器操纵机器人。当球在 x 轴上运动时,偏差要么是负的,要么是正的。如果是积极的,转向左边。如果是负数,转向右边。同样,我们可以用 y 轴来控制机器人的速度。正 y 方差驱动机器人前进,而负 y 方差驱动机器人后退。

  1. 打开空闲的 IDE 并创建一个新文件。

  2. 将文件另存为ball_chaser.py

  3. 输入以下代码:

    import cv2
    import numpy as np
    import time
    
    from Adafruit_MotorHAT import Adafruit_MotorHAT as amhat
    from Adafruit_MotorHAT import Adafruit_DCMotor as adamo
    
    # create motor objects
    motHAT = amhat(addr=0x60)
    mot1 = motHAT.getMotor(1)
    mot2 = motHAT.getMotor(2)
    mot3 = motHAT.getMotor(3)
    mot4 = motHAT.getMotor(4)
    
    motors = [mot1, mot2, mot3, mot4]
    
    # motor multipliers
    motorMultiplier = [1.0, 1.0, 1.0, 1.0, 1.0]
    
    # motor speeds
    motorSpeed = [0,0,0,0]
    
    # speeds
    speedDef = 100
    leftSpeed = speedDef
    rightSpeed = speedDef
    diff= 0
    maxDiff = 50
    turnTime = 0.5
    
    # create camera object
    cap = cv2.VideoCapture(0)
    time.sleep(1)
    
    # PID
    kp = 1.0
    ki = 1.0
    kd = 1.0
    ballX = 0.0
    ballY = 0.0
    
    x = {'axis':'X',
         'lastTime':int(round(time.time()*1000)),
         'lastError':0.0,
         'error':0.0,
         'duration':0.0,
         'sumError':0.0,
         'dError':0.0,
         'PID':0.0}
    y = {'axis':'Y',
         'lastTime':int(round(time.time()*1000)),
         'lastError':0.0,
         'error':0.0,
         'duration':0.0,
         'sumError':0.0,
         'dError':0.0,
         'PID':0.0}
    
    # setup detector
    params = cv2.SimpleBlobDetector_Params()
    
    # define detector parameters
    params.filterByColor = False
    params.filterByArea = True
    params.minArea = 15000
    params.maxArea = 40000
    params.filterByInertia = False
    params.filterByConvexity = False
    params.filterByCircularity = True
    params.minCircularity = 0.5
    params.maxCircularity = 1
    
    # create blob detector object
    det = cv2.SimpleBlobDetector_create(params)
    
    # define blue
    lower_blue = np.array([80,60,20])
    upper_blue = np.array([130,255,255])
    
    def driveMotors(leftChnl = speedDef, rightChnl = speedDef,
                    duration = defTime):
        # determine the speed of each motor by multiplying
        # the channel by the motors multiplier
        motorSpeed[0] = leftChnl * motorMultiplier[0]
        motorSpeed[1] = leftChnl * motorMultiplier[1]
        motorSpeed[2] = rightChnl * motorMultiplier[2]
        motorSpeed[3] = rightChnl * motorMultiplier[3]
    
        # set each motor speed. Since the speed can be a
        # negative number, we take the absolute value
        for x in range(4):
            motors[x].setSpeed(abs(int(motorSpeed[x])))
    
        # run the motors. if the channel is negative, run
        # reverse. else run forward
        if(leftChnl < 0):
            motors[0].run(amhat.BACKWARD)
            motors[1].run(amhat.BACKWARD)
        else:
            motors[0].run(amhat.FORWARD)
            motors[1].run(amhat.FORWARD)
    
        if (rightChnl > 0):
            motors[2].run(amhat.BACKWARD)
            motors[3].run(amhat.BACKWARD)
        else:
            motors[2].run(amhat.FORWARD)
            motors[3].run(amhat.FORWARD)
    
    def PID(axis):
        lastTime = axis['lastTime']
        lastError = axis['lastError']
    
        # get the current time
        now = int(round(time.time()*1000))
        duration = now-lastTime
    
        # calculate the error
        axis['sumError'] += axis['error'] * duration
        axis['dError'] = (axis['error'] - lastError)/duration
    
        # prevent runaway values
        if axis['sumError'] > 1:axis['sumError'] = 1
        if axis['sumError'] < -1: axis['sumError'] = -1
    
        # calculate PID
        axis['PID'] = kp * axis['error'] + ki * axis['sumError'] + kd * axis['dError']
    
        # update variables
        axis['lastError'] = axis['error']
        axis['lastTime'] = now
    
        # return the output value
        return axis
    
    def killMotors():
        mot1.run(amhat.RELEASE)
        mot2.run(amhat.RELEASE)
        mot3.run(amhat.RELEASE)
        mot4.run(amhat.RELEASE)
    
    # main program
    try:
        while True:
            # capture video frame
            ret, frame = cap.read()
    
            # calculate center of frame
            height, width, chan = np.shape(frame)
            xMid = width/2 * 1.0
            yMid = height/2 * 1.0
    
            # filter image for blue ball
            imgHSV = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    
            blueMask = cv2.inRange(imgHSV, lower_blue, upper_blue)
            blur = cv2.blur(blueMask, (10,10))
    
            res = cv2.bitwise_and(frame,frame,mask=blur)
    
            # get keypoints
            keypoints = det.detect(blur)
            try:
                ballX = int(keypoints[0].pt[0])
                ballY = int(keypoints[0].pt[1])
            except:
                pass
    
            # draw keypoints
            cv2.drawKeypoints(frame, keypoints, frame, (0,0,255),
                              cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
    
            # calculate error and get PID ratio
            xVariance = (ballX - xMid) / xMid
            yVariance = (yMid - ballY) / yMid
    
            x['error'] = xVariance/xMid
            y['error'] = yVariance/yMid
    
            x = PID(x)
            y = PID(y)
    
            # calculate left and right speeds
            leftSpeed = (speedDef * y['PID']) + (maxDiff * x['PID'])
            rightSpeed = (speedDef * y['PID']) - (maxDiff * x['PID'])
    
            # another safety check for runaway values
            if leftSpeed > (speedDef + maxDiff): leftSpeed = (speedDef + maxDiff)
            if leftSpeed < -(speedDef + maxDiff): leftSpeed = -(speedDef + maxDiff)
            if rightSpeed > (speedDef + maxDiff): rightSpeed = (speedDef + maxDiff)
            if rightSpeed < -(speedDef + maxDiff): rightSpeed = -(speedDef + maxDiff)
    
            # drive motors
            driveMotors(leftSpeed, rightSpeed, driveTime)
    
            # show frame
    ##        cv2.imshow('frame', frame)
    ##        cv2.waitKey(1)
    
    except KeyboardInterrupt:
        killMotors()
        cap.release()
        cv2.destroyAllWindows()
    
    
  4. 保存文件。

  5. 打开终端窗口。

  6. 导航到保存文件的文件夹。

  7. 输入sudo python ball_chaser.py并按回车键。

几秒钟后,你的机器人应该开始向前移动。如果框架内有一个蓝色的球,它应该转向它。机器人试图将球保持在框架的中心。

这段代码中的一些内容与我们过去的做法有些不同。最值得注意的是,我们将 x 和 y 轴的值放入字典中。我们这样做是为了在将值传递给 PID 控制器时将它们保持在一起,这是所做的另一项更改。PID 函数已更新为接受单个参数。然而,它所期望的参数是一个字典。它被赋给函数中的轴变量。然后所有的变量引用都被更新以使用字典。结果在 axis 字典中更新,然后分配给主程序中的适当字典。

我还确保消除任何会影响主循环或相机刷新率的延迟。因为整个程序是在单个进程中运行的,所以它没有我们将进程分解成不同线程时那么快。因此,机器人可能会错过球并跑偏。

摘要

在这一章中,我们开始利用 Raspberry Pi 提供的一些令人兴奋的功能。与单独使用微控制器相比,计算机视觉允许我们执行更复杂的任务。

为了准备使用视觉系统,我们在机器人上安装了一个基本的网络摄像头。这需要特别考虑,因为这些网络摄像头不是为安装而设计的。当然,你的解决方案可能与我的不同,所以你可以在安装相机时发挥一些创造力。之后,我们准备安装 OpenCV。

OpenCV 是一个开源社区开发的计算机视觉平台,它使许多视觉功能变得非常简单。在 Raspberry Pi 上安装软件需要相当长的时间,主要是因为我们必须从源代码编译它,尽管它的功能令人印象深刻,但 Raspberry Pi 没有笔记本电脑或 PC 的处理能力,所以编译代码需要一段时间。但是一旦编译和安装,我们就能做一些有趣的事情。

我们用静止图像做了一些练习。这让我们可以学习 OpenCV 的一些基础知识,而不需要处理视频。一旦我们学会了一些基础知识,我们就学会了从摄像机中提取现场视频,并运用我们在静态图像中学到的知识。使用我们在这一章学到的颜色过滤和斑点追踪技术,我们给了我们的机器人看见和跟随一个球的能力。

十、总结

从第一章开始,你已经走了很长一段路。如果你是机器人和编程新手,那么这可能是一本具有挑战性的书。这是命中注定的,所以恭喜你通过了。希望你能跟上,在这个过程中制造出你自己的机器人。

概括一下,在第一章,我介绍了一些机器人的基本概念,并讨论了这本书的目的。在第二章中,我们通过安装 Raspbian 操作系统并将其配置为远程访问来开始使用 Raspberry Pi。第三章向你介绍了 Python 编程语言。在第四章中,我们开始使用 Raspberry Pi 的 GPIO 头处理传感器。在这个过程中,我们学习了一些关于数字处理的知识,并讨论了一些局限性。

当我向您展示 Arduino 时,在第五章中已经介绍了 Pi 局限性的解决方案。我们学习了如何对 Arduino 编程,以及如何在它和 Pi 之间来回传递数据。在第六章中,我们组装了电机帽,并学习了如何用它和通用电机控制器驱动电机。在第七章中,我们终于组装好了机器人。在第八章中,我们安装了红外传感器,并给机器人编程,让它沿着一条线走。在第九章中,我们释放了树莓派的力量,使用计算机视觉过滤颜色和跟踪球。

机器人的类型

正如我在第一章中所讨论的,机器人可以有很多不同的含义。真的要看你想怎么定义了。为了进一步模糊定义,机器人技术中使用的许多技术可以很容易地转移到物联网(IoT)上。硬件、软件、传感器、通信信道等等,在你的自动化家庭中与在你的机器人中是一样的。编程是相似的,结果通常会影响物理世界。所以,本质上,物联网把你的家、办公室或工厂变成了机器人。

由于这个宽泛的定义,你对机器人的兴趣可能会比我大不相同。例如,你对小型桌上机器人还是大型机器人感兴趣?你主要是对沿着地面行驶的地面机器人感兴趣,还是希望你的自动化设备能够飞行?或者,也许你对用机器人潜水器探索海洋深处感兴趣。你想尝试自动驾驶汽车还是家庭自动化和物联网是你的事情?

了解你可能从事的领域决定了你使用的工具。如果你正在建造小型桌面机器人,你可能不需要焊工。该字段还决定了您将使用的一些设计工具。例如,大多数像我们在本书中建造的小机器人可以在飞行中或用笔和纸来设计。然而,如果你在建造更复杂的东西,比如四足动物,你可能需要 CAD 软件。

工具

机器人技术中的工具有两种:硬件和软件。我没有深入讨论您可能会使用的物理硬件工具,因为您将使用的工具类型取决于您感兴趣的机器人类型。我一会儿会谈到硬件。

首先,我想简单谈谈软件。软件是机器人所有领域共有的一个领域。像机器人领域的大多数事情一样,你对工具的选择完全取决于你自己。用你觉得舒服的方式完成工作。

软件

这本书涵盖的主题远非全面。关于 Linux、Python、Arduino,尤其是 OpenCV,还有很多东西需要学习。目的是向您介绍一些机器人的概念,并让您熟悉和熟悉一些工具。

选择 IDE

您使用的 IDE 或集成开发环境由您决定。这是所有不同领域共有的领域之一。有很多可以选择。我们使用的软件工具是 Raspberry Pi 和 Arduino 自带的。我所说的“本机”是指内置于操作系统中的工具,或者是硬件的推荐工具。

实际上,除了写这本书之外,我不再使用空闲的 ide 了。我的一般工作流程在我的 Windows 机器上开始。当代码以我喜欢的方式运行时,我会将它转移到 Raspberry Pi 进行最后的润色。

我编程 Python 的首选工具是 PyCharm ( www.jetbrains.com/pycharm )。社区版提供了我几乎所有项目所需的所有特性。这是一个专业级的 IDE,使得使用 Python 变得更加容易(见图 10-1 )。它适用于 Windows 和 Linux。因此,当我将文件转移到 Pi 时,我可以根据需要使用相同的工具来更新代码。

A457480_1_En_10_Fig1_HTML.jpg

图 10-1

PyCharm IDE

Spyder 是另一个使用 Python 的优秀 IDE。它包含在 Python 的 Anaconda 发行版中,这使得安装稍微容易一些。它为科学和学术团体提供了许多工具。Anaconda 很受我共事的许多数据科学家的欢迎。

如果你有兴趣看看 Anaconda,你可以在 www.anaconda.com 找到它。或者,如果你想尝试 Spyder IDE,可以在 https://pythonhosted.org/spyder/ 下载。

此外,微软的 Visual Studio 是一个非常强大且越来越容易使用的产品。同样,他们的社区版可以从他们的网站( www.visualstudio.com )下载。曾几何时,Visual Studio 只面向专业开发人员。即使当微软开始发布免费社区版时,初学者和业余爱好者也很难使用。然而,最近的几个版本更加用户友好。Visual Studio 的一个好处是它可以满足您的大多数开发需求。

不过,它也有缺点。例如,它只适用于 Windows。它还有一点学习曲线,但是有大量的资源可以提供帮助。作为一个基于 Windows 的 IDE,它可以为 Windows 编译。幸运的是 Python 是跨平台的。所以一旦你写好了代码,你就可以把 Python 文件转移到 Raspberry Pi,对串口等等做任何你需要的调整,然后从那里运行它。

我的大部分 Arduino 工作仍然使用 Arduino IDE。这仅仅是因为我还没有找到一个更好的独立工作环境。Visual Studio 有一个扩展,允许您开发 Arduino 代码并交叉编译到 Arduino;尽管它通过 Arduino IDE 进行编译。因此,如果您正在寻找一个单一的环境来开发您的机器人项目,Visual Studio 可能是一个不错的选择。

设计软件

你们中的许多人可能不经常使用设计软件。与其他任何东西一样,用于设计机器人各部分的软件将根据您的项目而有所不同。这也将取决于你的预算和你用来制造机器人的工具。一些项目,如套件或其他人的设计,根本不需要设计软件。许多项目和建筑风格都离不开简单的铅笔和纸。如果你正在使用模块化部件,你也许可以用列表或简单的草图来摆脱。然而,对于任何定制,您可能需要一种方法来设计系统。

2D 绘画

最简单和最容易使用的软件是 2D-或平面-设计。这些工具适用于设计可以使用板材(如 MDF、纸板、胶合板或丙烯酸板)建造的项目。不要低估你用平板材料所能做的事情。我的游牧项目是用 1/4 英寸的胶合板设计和建造的。

请记住,这些工具是为艺术家和插图画家设计的,而不是为精确的 CAD 工作设计的。所以你可能期望的一些特性根本不存在。例如,精确的测量是困难的。使用网格和标尺很有帮助,但是如果你需要一个精确的角度或长度,这些工具可能不是最好的。

最流行的 2D 设计工具之一是一个名为 Inkscape ( https://inkscape.org/en/ )的开源项目。Inkscape 非常容易使用,它有一个非常大的用户群体。可以免费下载使用,功能丰富。还有很多社区开发的插件。我最喜欢的是标签盒制造商。因为我可以使用激光切割机,所以我使用选项卡式盒子制作工具来设计简单的盒子,我可以切割并咬合在一起。图 10-2 显示了 Inkscape 界面。

A457480_1_En_10_Fig2_HTML.jpg

图 10-2

Inkscape

也有商业节目。Adobe Illustrator ( www.adobe.com/products/illustrator.html )和 Corel Draw ( www.coreldraw.com/en/pages/ppc/coreldraw/ )是这方面的两大领军人物。

电路板设计

在某些时候,你可能会发现自己需要设计自己的电路板或屏蔽。这没有你想象的那么复杂或困难。随着你越来越多地与机器人打交道,你会发现针对特定芯片或电路的建议。通常,只需在网上搜索就能找到电路示例的链接。在为 it 设计的工具中重新创建这些电路可以让您订购电路板。

有许多为电路板设计的程序。事实上,几乎每个电路板制造商都有一个可用的。

爱好社区里最流行的一个就是 Fritzing ( http://fritzing.org/home/ )。它是由德国波茨坦应用科学大学开发的。它的受欢迎程度导致它被分离出来,成为自己的组织:Fritzing 基金会之友。我使用 Fritzing 软件创建了这本书中的电路图(见图 10-3 )。

A457480_1_En_10_Fig3_HTML.jpg

图 10-3

Fritzing

也有商业产品可用;他们中的许多人可以免费使用社区附件。领先的行业标准是 Eagle,现在归欧特克( www.autodesk.com/products/eagle/overview )所有。大多数其他程序以流行的 Eagle 格式导入和/或导出最终设计。

3D 设计

如果你有定制的机箱和部件,或者你喜欢 3D 打印,你需要 3D CAD 软件。还是那句话,有很多可用的。但是我还没有找到与商业解决方案相匹配的免费或开源包。也就是说,许多商业解决方案都提供免费或低价的学生版。

SketchUp ( www.sketchup.com )提供了一个为创客设计的免费版软件。如果你以前没有用过 CAD 程序,它可能是最容易学的。控件非常直观,有大量的教程来帮助您学习如何使用它。如果你以前用过 CAD,那么这可能不适合你。我接触过的大多数有 CAD 经验的人都觉得这个工具不够直观。这是因为它不是作为标准 CAD 程序设计的。

对于那些更熟悉 CAD 的人,Autodesk 提供了 Fusion 360 ( www.autodesk.com/products/fusion-360/overview ),费用适中。该公司还为学生提供大部分产品的免费许可证,如 Fusin 360、Inventor 和许多其他产品。Fusion 360 和 Inventor 是专业的商业级 CAD 程序,具有许多功能,包括仿真。当我需要为我的机器人或其他项目设计一些东西时,我就用它(见图 10-4 )。

A457480_1_En_10_Fig4_HTML.jpg

图 10-4

Autodesk Inventor

五金器具

除了我描述的软件工具之外,您还需要一些实际的工具。您对工具的选择可能最依赖于您感兴趣的机器人类型,但是每个工具箱都应该有一些基础知识。

基本工具

在这一节中,我将介绍您可能需要的工具,无论您的机器人或项目采取何种形式,以及我的基本工具包中的工具。

首先,一套好的钳子是必须的。你需要不同的尺寸和类型。我用的最多的是珠宝商的那套钳子。我也经常使用伸缩钳。确保该套件包括一对对角切刀。

接下来,你需要一套好的螺丝刀。您使用的许多螺钉都很小,适合安装在狭窄的地方。确保您的套件中有各种六角头。我经常发现我要插入或移除的六角螺钉在我的套件中的两个尺寸之间。星星头通常适合这些。不过要小心,因为有可能会把牙齿从它们身上剥下来。

从这里,有很多杂七杂八的工具是很好的:一个实用刀,一组文件,一个压接工具,一个平齐的钢丝剪,一个万用表,卡钳,等等。你会收集到一批很好的工具。我强烈建议你购买你需要的工具,而不是试图用手头的东西凑合。使用正确的工具总是能带来更好的结果。而且,如果你花时间去获得合适的工具,下次你需要它的时候你就会拥有它。

你还需要一个焊接站。不必细说。一个好的烙铁,一个放置焊剂和焊嘴清洁剂的地方,以及一组帮手就是你所需要的。

确保你有一个存放工具的好地方,并尽量把它们放回原处。这为你节省了无数的时间,让你不用在不可避免的杂乱中寻找。我有几套工具。一套生活在我的工作台上。我买了一个紧凑的钉板系统,用来挂我的大部分工具。挂钉板上放不下的东西会放进长凳上的特定抽屉里。

另一套在我留给 Nomad 的工具箱里。因为 Nomad 经常被带去看演出,而且很快就要参加比赛,所以我想确保手头总是有我需要的东西。通常情况下,我最终会帮助节目中的其他主持人,因为他们经常准备不足。

我的第三套工具是浮动套。我把它们放在一个工具箱里,当我不带 Nomad 外出时,可以很容易地从一个房间转移到另一个房间或转移到车上。我活跃在奥斯汀当地的业余机器人领域,当有人需要帮助或工具时做好准备是很好的。

当我用完我的工具时,我尽量确保把它们放回原处。这保证了下次我伸手拿工具时,它就在那里。诚然,我并不像我希望的那样始终如一,但这确实是一个很好的习惯。

专业工具

拥有一些更大的专业工具总是让我更容易构建更大的版本。带锯和钻床是无价之宝。除非你计划建造一些非常大的机器人,否则这两种工具的台式版本通常就足够了。台式砂带/盘式砂光机组合有助于清理您的边缘或塑造您的零件。

除了所有这些工具之外,我还利用了更专业的工具。在大多数情况下,我家里没有这些工具。但是 3D 打印机现在很容易买到;如果可能的话,在你的工作室里放一两个是个好主意。我还使用了 120W 激光切割机、数控路由器和数控铣床。然而,它们不是我店里的工具。

创客空间

我家里的店里没有激光切割机和数控铣床,我想你们大多数人也没有。这些工具既笨重又昂贵。但是,我是奥斯丁本地创客空间的一员:ATX 创客空间( http://atxhs.org )。Hackerspace 是一个合作工作室,在这里我们可以集中资源购买一些大型机器。黑客空间不拥有的东西通常由一个成员托管,供其他成员使用。

让空间特别有价值的是社区。创客空间里都是喜欢创造东西的人。这些人来自各行各业,各有所长。当你试图做你以前从未做过的事情,或者你想学习一项新技能,或者你想从不同的角度看一个问题,或者你只是简单地卡住了,这是一个非常有价值的资源。

如今,几乎每个社区都有一个或多个创客空间。可用的资源因空间而异。有些在商业公园经营,有些在学校经营,有些在别人的车库经营。唯一不变的是社区。如果你还没有,找到你当地的创客空间并加入其中。你不会后悔的。

摘要

现在,你已经具备了开始学习机器人爱好所需的所有基础知识。在许多主题中,显然还有许多东西需要学习。但是,树莓派和 Arduino 将带你走很长的路。记住,你不需要在真空中学习一切。那里有一个巨大的社区,并且每天都在增长。接触你当地的创客空间,寻找志同道合的建设者。不要害怕问问题。不要害怕看别人的项目来寻找灵感。尽可能利用示例代码。最终,您将编写自己的代码,但在此之前,请向已经编写代码的人学习。

机器人领域令人兴奋。事实上,我们可以进入其中,进行实验,并学习是非凡的。好好利用这段时间。最重要的是,玩得开心。

祝好运和快乐的建设。

posted @   绝不原创的飞龙  阅读(258)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示