Python 物联网入门指南(六)

发布于:2024-04-17 ⋅ 阅读:(81) ⋅ 点赞:(0)

原文:zh.annas-archive.org/md5/4fe4273add75ed738e70f3d05e428b06

译者:飞龙

协议:CC BY-NC-SA 4.0

第十七章:机器人学 101

一提到机器人,我们就会被科幻小说所包围。我们可能会想起动画片《杰森一家》或者电影《终结者》。但事实上,机器人已经不再属于科幻小说。它们是真实存在的。环顾四周,指出任何物体;它可能没有机器人就不会被制造出来。现代时代已经被机器人塑造了。

但是,你也可以退一步思考,等一下,他所说的东西不是叫做机器而不是机器人吗?嗯,是的,你说得很对,但同时也错得很离谱。正是卡通和科幻小说赋予了一个被称为机器人的人形机器人的形象。但机器人远不止于此。

不幸的是,我们没有一个具体的、普遍认可的机器人定义,但是,正如我喜欢说的那样,任何能够执行物理和智力任务的机器都可以被称为机器人

现在,你可能会说,根据我的定义,甚至自动洗衣机都可以被称为机器人。嗯,从技术上讲,是的,为什么我们不称它为机器人呢?想想它为你做了什么,以及多年来进行了什么样的自动化。在你输入布料类型后,它会自动洗涤和烘干,就像你在 19 世纪自己做的那样。我想说的是,我们可以想象有各种各样的机器人,它们可以从根本上改变我们的生活方式。我们需要以更广阔的视角思考——不仅仅将机器人限制为人形机器人的形式。

我们生活在机器人和自动化的黄金时代,新产品的开发就像它可以变得那么简单。十年前可能需要一个工程师团队才能完成的工作,现在可以由一个人在卧室里在几分钟内完成,这要感谢开源世界。与此同时,有硬件计算能力可供你使用,你可以用几百美元在家里建立一个超级计算机。我们周围有各种问题,有些简单,有些复杂,等待着解决。整个过程中唯一缺失的环节就是你:一个有能力利用这些技术解决世界问题的创新思维。

为了让你的思维能够做到这一点,我们将从理解机器人学的根源和基础开始。这本书的目标不仅是制作书中提到的项目,而且是让你了解如何利用资源来建立你的梦想项目。

最后,我要祝贺你在正确的时间进入了这个令人惊叹和未来感的领域。我总是告诉我的学生一个规则,我也想和你分享:

  • 首先是科学家

  • 第二是研究员

  • 第三是工程师

  • 第四是技术员

  • 最后是技工

这意味着你越早进入任何领域的生命周期,你就越有可能在层次结构中升级。你越晚进入,就越难爬到顶部。

说了这么多,现在让我们直奔主题!在本章中,我们将涵盖以下主题:

  • 硬件装备

  • 设置树莓派

  • 编程

  • 玩电压

硬件装备

谈到机器人,它们由一些基本的有形组件组成,包括:

  • 计算单元

  • 传感器

  • 执行器

  • 底盘

  • 电源

首先,我们将讨论微控制器,并在书的过程中根据需要详细讨论其他有形组件。

每当你去购买笔记本电脑或电脑时,你一定听过微处理器这个词。这是必须做出所有决定的主要单位。我称它为“国王”,但没有帝国的国王算什么?国王需要一些可以为他做事的下属,就像微处理器需要一些下属,比如 RAM、存储、I/O 设备等。问题是,当我们放入所有这些东西时,整体单元变得昂贵和笨重。但是,正如我们所知,重量和大小在机器人方面非常重要,所以我们不能承受一个庞大笨重的系统来运行机器人。

因此,我们制造了一个叫做 SoC 的东西。现在,这是一个独角戏,因为这个小芯片本身就具有所有必要的系统来使其工作。所以,现在你不需要添加 RAM 或存储或任何其他东西来使其工作。这些小型微控制器可以变得非常强大,但缺点是,一旦制造商制造了一个 SoC,以后就不能对其进行任何更改。存储器的大小、RAM 或 I/O 都不能更改。但是我们通常可以接受这些限制,因为在编程机器人时,你可能不会使用微控制器的全部功能,直到你运行一些严肃的人工智能或机器学习代码。

这样一个伟大的硬件是树莓派。是的,听起来非常美味,但它还有更多的功能。这是一个超小但非常强大的微控制器。它通常被称为原型板,因为它被世界各地的机器人学家用来实现他们的想法并在短时间内使它们成为现实。它在全球范围内都可以获得,并且非常便宜。你可以在一个仅售 10 美元的设备上随时观看高清电影,上网等等。我想不出还有什么比这更荒谬的了。它非常容易使用,你可以使用 Python 来编程。

因此,基本上它符合我们所有的要求。这将是我们在整本书中将要使用的主要武器。

所以让我向你介绍树莓派!它看起来是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

市场上有各种型号的树莓派。但我们将使用树莓派 Zero W;这将花费你大约 10 美元,比起大麦克汉堡更容易购买。确保你购买带有 W 的树莓派 Zero,这个 W 代表无线功能,比如 Wi-Fi 和蓝牙。还有一些其他东西,你需要订购或安排才能使其工作。以下是一些物品清单:

  • Micro USB 到标准 USB 适配器

  • 键盘

  • 鼠标

  • Micro SD 存储卡,16 或 32 GB

  • Micro SD 卡读卡器

  • Micro USB 电源适配器(2 安培或更高)

  • Micro HDMI 到 HDMI 端口

  • 面包板

  • 一堆跳线(公对公,公对母,母对母)

  • 3V LED

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你可以立即从图像中看出,板载了一个微型 HDMI 端口,你可以通过它连接高清显示器或电视屏幕。其次是一个 Micro SD 卡槽。这将是这台电脑的主要存储设备。除此之外,你还会发现两个 USB 插座和一个摄像头总线。你可能会认为这就是全部,但最好的还在后面。树莓派有一个叫做 GPIO 的东西,它代表通用输入/输出。这些被伪装成树莓派的一个角落上的 40 个插孔,这就是它变得非常特别的原因。

现在,传统上您会将与计算机兼容的设备连接到计算机。因此,连接鼠标、键盘或游戏手柄就像插入 USB 端口一样简单,但是如果您需要将计算机连接到灯泡或空调呢?确切地说,您不能。这就是 GPIO 发挥作用的地方。这些引脚在机器人技术方面非常有用,因为它们可以用于连接各种组件,如传感器/电机。这些引脚的美妙之处在于,根据我们为其编程的需求,它们可以用作输入或输出。因此,正如我们将在后面看到的那样,这些引脚中的每一个都可以在程序中定义为输入或输出。

现在,这 40 个引脚中有 26 个是 GPIO。其余的引脚是通用电源或地线端口。还有两个称为ID EEPROM的端口,目前我们不需要它们。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

正如您所看到的,树莓派能够为我们提供两种类型的电源供应:3.3V 和 5V。这基本上可以满足我们大部分的需求。

设置树莓派

我们将在一会儿讨论有关树莓派的 GPIO 和其他事项。首先,我们将了解如何首次设置此板。

您需要做的第一件事是确保树莓派的操作系统已经准备就绪。我假设您正在使用 Windows PC,但如果您也在其他操作系统上进行操作,那么差别不会太大。

要安装操作系统,请启动您的 PC 并按照以下步骤操作:

  1. 转到www.raspberrypi.org并单击 DOWNLOADS

  2. 现在点击 RASPBIAN,您将看到以下两个选项:

  • RASPBIAN STRETCH WITH DESKTOP

  • RASPBIAN STRETCH LITE

  1. 我们将下载 RASPBIAN STRETCH WITH DESKTOP;这将为我们提供树莓派的 GUI 界面

  2. 下载完成后,将包解压缩到一个文件夹中

现在我们需要将它复制到树莓派的存储卡上。您需要复制的存储卡必须经过低级格式化。基本上有两种格式化方式。一种只是擦除索引,另一种是我们所知道的低级格式化,即从索引和物理内存位置中删除所有数据。会有一个按钮来切换低级格式。确保在为此功能格式化存储卡之前点击它。我建议使用www.sdcard.org的 SD 卡格式化器。现在打开格式化器,您只需使用 32 KB 选项进行格式化。

在这里找到更多详细信息和更多最新信息:www.raspberrypi.org/documentation/installation/installing-images/README.md

完成后,您必须将映像复制到 SD 卡上。最简单的方法是使用 WinDisk Imager。您可以在线下载它而不会遇到任何问题。然后只需选择映像和 SD 卡上的位置,然后开始复制映像。

这可能需要几分钟。完成后,您的 SD 卡将准备就绪。将其插入树莓派,我们将准备好启动它。但在启动之前,使用 Micro HDMI 到 HDMI 线将显示器连接到树莓派,使用 Micro USB 将键盘和鼠标连接到树莓派,然后使用 Micro USB 适配器连接到标准 USB 适配器来为其供电。现在,使用树莓派上的另一个 USB 端口,使用 Micro USB 电源适配器为其供电。

一旦启动,您将看到一个启动屏幕,几秒钟后您将能够看到桌面。因此,我们的树莓派终于启动并运行了。

继续探索一些选项,上网冲浪,观看 YouTube 上的一些猫视频,并熟悉这个强大的设备。

到目前为止,您可能已经开始欣赏树莓派的强大。它可能比您平常使用的计算机稍慢。但是,拜托,这个东西只要 10 美元!

让我们编程

在本章中,我们将让您熟悉 Python 以及如何在此设备上使用 GPIO。要做到这一点,请点击左上角的树莓图标。您将看到 Python 控制台 3.0。也可能有旧版本的 Python。我们将在本书中使用更新的版本。

一旦窗口打开,您将看到您将在其中进行编码的游乐场。所以现在我们准备为 Python 机器人编写第一行代码。现在让我们看看它是如何完成的。

我们将首先写的是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

几乎所有的时候,当我们开始编写程序时,我们会首先写上述行。现在,在我们了解它的作用之前,我们需要了解库。通常在编写代码时,我们将不得不在多个地方一遍又一遍地编写代码。这需要很多时间,而且肯定不酷!

因此,为了解决这个问题,我们创建了函数。函数是一个微型程序,我们可能认为它会一遍又一遍地使用。在这个微型程序中,我们还提到它将被称为什么。

假设有一个代码,我们需要一遍又一遍地将两个数字相乘。所以,我们写一次代码并将其制作成一个函数。我们还将这个函数命名为Multiply

现在,每当我们需要相乘两个数字时,我们不必再次编写它的代码;相反,我们只需调用函数来代替我们编写相乘的代码。问题是,我们如何告诉程序要相乘哪个数字呢?

这也有一个解决方案。正如您以后可能看到的,每当调用一个函数时,我们在其后放上开放和关闭的括号,比如multiply()

如果括号是空的,那意味着没有给出用户输入。例如,如果我们要相乘23,我们只需写Multiply(2,3)

我们正在输入23。括号中的输入位置也很重要,因为括号中的位置将定义程序中的位置。

现在,假设您创建了这样的函数:

  • 加法

  • 减法

  • 相乘

  • 除法

假设您将它们堆叠在一起。然后,一堆函数组合在一起将被称为库。这些库可以有数百个函数。有一些函数已经在 Python 语言中,以便程序员更轻松地完成工作。其他可以定义为开源或根据您的方便开发。

现在,回到重点。我们正在调用RPi.GPIO库;这是由树莓派定义的库。这将使您在编程树莓派时更加轻松。因此,在程序中,一旦我们调用库,所有函数都可以随时使用。

在下一行,我们写Import.time。正如您可能已经猜到的那样,这是用来导入时间库的。我们很快就会了解它的作用。

下一行代码将如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在我们了解它的作用之前,让我们更多地了解一下 GPIO。这些引脚根据它们在树莓派中的物理位置进行了硬编号。但是,我们可以在软件中更改引脚的编号以便我们理解和方便使用。但在这个代码中,我们不会对此进行操作,而是将其设置为由 Broadcom 制造的默认设置,这是树莓派微控制器的制造商。

这一行使用了RPi.GPIO库的一个名为setmode的函数。这个函数的作用是将setmode的引脚配置设置为(GPIO.BCM)—BCMGPIO的一个进一步的函数。

现在我们可以使用基本引脚配置。GPIO 引脚的特点是可以同时用作输入和输出。但唯一的条件是我们必须在程序中指定它是要用作输入还是输出。它不能同时执行两个功能。下面是如何做到的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下一行代码将如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

同样,我们使用了 GPIO 库的一个函数output。它的作用是设置板子上特定引脚的状态。所以,这里我们指定了引脚号23必须设置为高电平。只是为了清楚起见,高电平表示开,低电平表示关。

下一行代码将如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这一行中,我们使用了来自 time 库的一个函数。sleep 函数基本上会冻结所有 GPIO 引脚的状态。所以,例如,如果引脚23是高电平,那么它将保持高电平,直到执行time函数sleep。在sleep函数中,我们定义了值为3秒。

因此,3 秒内,树莓派的引脚状态将保持在这行代码之前的状态。

最后,代码的最后一行将是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这将是每个程序之后的常见情景。GPIO 库的这个函数将重置程序中使用的每个引脚的状态,所有引脚的状态将变为低电平。记住,它只会影响程序中使用的引脚,而不会影响树莓派上的其他引脚。所以,例如,我们在程序中使用了引脚23,所以它只会影响引脚23,而不会影响树莓派上的其他引脚。

最后,你的程序会看起来像这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,你必须记住的一件事是,我们写的任何代码都将按顺序执行。所以,假设我们把import RPI.GPIO as GPIO放在底部,那么整个程序将无法工作。为什么?因为一旦它到达GPIO.setmode(GPIO.BCM),它将不理解GPIO是什么,也不会理解setmode是什么。因此,我们总是在开始编写代码时导入库。

现在,基于相同的概念,它将以以下方式执行程序:

  • GPIO.out(23,GPIO.High): 它会将引脚23设置为高电平

  • time.sleep(3): 它会等待 3 秒,而引脚仍然是高电平

  • GPIO.cleanup(): 最后,它会将引脚23的状态设置为低电平

现在,为了查看程序是否工作,让我们连接一些硬件来检查我们所写的是否真的发生了。

我假设读者已经知道如何使用面包板。如果你不熟悉,就去搜索一下。理解起来只需要 5 分钟。它非常简单,会派上用场。

现在继续连接 LED 到面包板上,然后将 LED 的地连接到树莓派上的地引脚,并将正极/VCC 连接到引脚号23(参考引脚图)。

你也可以参考以下图表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

完成后,运行代码看看会发生什么!

LED 将会发光 3 秒,然后再次关闭,正如我们预期的那样。现在让我们玩弄一下代码,做一点修改。这次,我们将添加一些用粗体标记的额外行:

import RPi.GPIO as GPIO
from time
import sleep
GPIO.setmode(GPIO.BOARD)
GPIO.setup(23, GPIO.OUT)
while True:
 for i in range(3):
  GPIO.output(23, GPIO.HIGH)
sleep(.5)
GPIO.output(23, GPIO.LOW)
sleep(.5)
sleep(1)
GPIO.cleanup()

在理解代码内部之前,你会注意到并不是每一行都对齐,它们已经被缩进了。这是什么意思?

一个与其他代码行一起缩进的行称为块。所以例如,如果你有一个语句如下

while True:
 for i in range(3):
  GPIO.output(23, GPIO.HIGH)
sleep(.5)
GPIO.output(23, GPIO.LOW)
sleep(.5)
sleep(1)
GPIO.cleanup()

现在在这行让我们看看代码将如何运行。

  • 一个 while true 循环将运行,这将运行其中的代码,即
for i in range(3):
  GPIO.output(23, GPIO.HIGH)
sleep(.5)
GPIO.output(23, GPIO.LOW)
sleep(.5)
sleep(1)
  • 之后,代码for I in range (3):将运行。它将运行 for 循环内的代码,直到I的值在范围内,因此下面的代码将运行。
GPIO.output(23, GPIO.HIGH)
sleep(.5)
GPIO.output(23, GPIO.LOW)
sleep(.5)

上面的代码可以称为一个代码块,它在for循环内。代码块可以通过缩进代码来制作。

现在,让我们看看它的作用。While True是一个循环,它将一遍又一遍地运行for循环,直到条件不再为假。我们在这里使用的条件是:

for i in range(3):

最大范围是3,每次语句运行时,i的值增加+1。因此,它基本上充当计数器。让我们看看程序实际会做什么。

它将检查i的值,并在此之后递增1。随着代码的进展,它将使 LED 在 0.5 秒内高亮,然后在 0.5 秒内关闭。然后它将等待 1 秒。这将重复,直到 while 循环为假,也就是i的值大于3,它将退出程序并终止。运行程序,看看它是否真的发生了。

到目前为止,你已经了解了在树莓派中编程是多么容易。为了更进一步,我们将编写另一个程序并对硬件进行一些更改。

我们将从引脚号 7 到 12 连接五个 LED。我们将使它们以一定模式开关。

连接后,我们将编写以下代码:

import RPi.GPIO as GPIO
from time
import sleep
GPIO.setmode(GPIO.BOARD)
GPIO.setup(7, GPIO.OUT)
GPIO.setup(8, GPIO.OUTPUT)
GPIO.setup(9, GPIO.OUTPUT)
GPIO.setup(10, GPIO.OUTPUT)
GPIO.setup(11, GPIO.OUTPUT)
while True:
  for i in range(7, 12):
  GPIO.output(i, GPIO.HIGH)
sleep(1)
GPIO.cleanup()

现在代码非常简单。让我们看看它的意思:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在我告诉你更多关于代码的事情之前,让我们继续运行它。

当你运行它时,你会明白,根据语句,它会逐个地址引脚,并在每隔 1 秒后将它们切换到高电平。

玩耍电压

到目前为止一切顺利!但你注意到了一件事吗?我们一直在使用树莓派作为开关,简单地打开和关闭各种组件。但是如果我们需要改变刚刚编程的 LED 的强度怎么办?这可能吗?答案是否定的。但我们仍然可以以某种方式完成!

让我们看看如何。计算机以二进制工作,这意味着它们可以表示01。这是因为任何系统中的主要计算单元都是基于可以表示01的晶体管。因此,从技术上讲,计算机只能进行二进制架构的切换。然而,有一个技巧。这个技巧叫做脉宽调制PWM)。

现在,在我详细解释任何内容之前,让我们继续将 LED 插入到引脚号为18,然后将此代码复制到树莓派并运行:

import RPi.GPIO as GPIO
import time                             
GPIO.setmode(GPIO.BCM)       
GPIO.setup(18,GPIO.OUT)         

pwm= GPIO.PWM(18,1)
duty_cycle = 50
pwm.start(duty_cycle)

time.sleep(10)

GPIO.cleanup()

你注意到了吗?LED 将以每秒一次的频率闪烁。现在让我们稍微调整一下,将PWM(18,1)改为PWM(18,5)。运行并看看会发生什么。

您可能已经注意到它现在每秒闪烁五次。因此数字5基本上表示频率,因为 LED 现在每秒闪烁五次。现在,再次重写代码并将5增加到50。一旦增加到50,它会在一秒内开关 LED 50 次,或者以 50 赫兹的频率。因此,对您来说,它看起来好像一直开着。

现在是有趣的部分。转到您的代码,并将duty_cycle = 50更改为duty_cycle = 10

你注意到了什么?你一定已经注意到 LED 的亮度现在低得多。实际上,它将是原来的一半。

让我们看看实际发生了什么:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从图表中可以看出,该函数基本上创建一个脉冲,我们正在更改其特性。第一个特性是频率,即每秒生成的脉冲。在代码行pwm= GPIO.PWM(18,1)中,我们基本上告诉微控制器在引脚号1上每秒生成一个脉冲。在第二行中,占空比是一个百分比值。它确定脉冲高电平的时间占总脉冲时间的百分比。对于以下代码,以下项目将是其特性:

pwm= GPIO.PWM(18,1)
duty_cycle = 50
  • 每个脉冲的时间/宽度为 1 秒

  • 它将打开的时间占 50%

  • 它将关闭的时间占 50%

  • 它将打开的时间为 0.5 秒

  • 它将关闭 0.5 秒

当我们增加频率超过 50 赫兹时,人眼很难分辨它是在开还是在关。理论上,引脚将保持高电平的时间占 50%,其余时间将是低电平。因此,如果我们取平均值,我们可以很容易地说整体电压将是原始电压的一半。使用这种方法,我们可以根据我们的需求调制任何引脚的电压输出。

总结

现在你一定已经明白了 GPIO 如何被用作输出,以及通过应用条件,我们如何改变它们的行为。

在下一章中,我们将了解这些引脚如何被用作输入。所以回来吧,我们在那里见!

第十八章:使用 GPIO 作为输入

在上一章中,我们了解了 GPIO 如何用于输出。但是,正如其名称所示,GPIO 既可以用于输入也可以用于输出。在本章中,我们将看到如何使用这些引脚将数据输入到树莓派上。

本章我们将涵盖的主题有:

  • 深入了解 GPIO

  • 与 PIR 传感器的接口

  • 与超声波接近传感器的接口

  • 通过 I2C 进行接口

深入了解 GPIO

我相信你还记得上一章的这行代码:

GPIO.setup(18,GPIO.OUT)

正如前面解释的,这基本上告诉我们在某个程序中 GPIO 引脚的行为。到现在为止,你一定已经猜到,通过改变这一行代码,我们可以改变引脚的行为,并将其从输出转换为输入。这就是你会这样做的方式:

GPIO.setup(18,GPIO.IN)

一旦在程序中写入这行代码,微控制器将知道在程序运行时,引脚号18只用于输入目的。

要理解这实际上是如何工作的,让我们回到我们的硬件,看看它是如何完成的。首先,你需要将 LED 连接到任何一个引脚;在这个程序中,我们将使用引脚号23。其次,你需要在引脚号24上连接一个开关。你可以参考接下来的图表来进行连接:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一旦连接好,你可以继续编写这个程序:

import time import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(24,GPIO.IN)
GPIO.setup(23,GPIO.OUT)
while True:
  button_state = GPIO.input(24)
    if button_state == True:
      GPIO.output(23,GPIO.HIGH)
    else:
      GPIO.output(23,GPIO.LOW)
  time.sleep(0.5)
GPIO.cleanup()

一旦程序上传,当你按下按钮时,LED 将自行打开。

让我们了解到底发生了什么。while True:基本上是一个无限循环;一旦应用了这个循环,循环内运行的代码会一遍又一遍地重复,直到有什么东西打破它,而所谓的打破是指导致程序停止并退出的某种中断。现在,理想情况下,我们通过按下Ctrl + C来退出程序,每当有一个无限循环时。

button_state = GPIO.input(24)  

在上面的一行中,程序理解它需要查找的地方;在这个程序中。在这一行中,我们告诉程序我们正在寻找 GPIO 24,这是一个输入:

if button_state == True:
   GPIO.output(23,GPIO.HIGH)  

如果按钮是高的,换句话说,当按钮被按下并且电流到达引脚号24时,那么 GPIO 引脚号23将被设置为高:

  else:
   GPIO.output(23,GPIO.LOW)

如果引脚号24不为真,它将遵循这行代码,并保持引脚号23低,换句话说关闭。

所以,这就是你用于输入目的的第一个 GPIO 程序。

与 PIR 传感器的接口

到目前为止一切顺利!在这个单元中,我们将继续接口我们的第一个传感器,即被动红外传感器,通常称为 PIR 传感器。这个传感器是一个非常特殊的传感器,在自动化项目中非常常见。它的低能耗使其成为物联网项目的绝佳竞争者。所以让我们看看它是如何工作的。

你一定注意到了,当我们将金属加热到高温时,它慢慢变成深红色,当我们进一步加热时,它变得更亮,慢慢从红色变成黄色,如下图所示,显示了一个红热的钢片。现在,随着温度的升高,发射辐射的波长减小;这就是为什么随着温度的升高,颜色从红色变成黄色,因为黄色的波长比红色短。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但有趣的是,即使物体没有被加热到足够的温度,它们也会发射辐射;事实上,任何高于绝对零度温度的物体都会发射某种形式的辐射。有些我们能用肉眼看到,有些我们看不到。因此,在室温下,物体会发射红外辐射,其波长比可见光更长。因此,我们的眼睛看不到它。尽管如此,它仍然存在。

这个 PIR 传感器的作用是感知周围物体发出的红外光,每当物体移动时,它可以感知其模式的整体变化,并且基于此可以检测到其附近是否发生了任何运动。

我们假设当房间里有人时,会有一些固有的运动发生,因此这种传感器非常常用作占用传感器。现在,让我们连接这个传感器,看看我们如何使用它:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一旦您按照上图连接好了,就可以上传代码了:

import time import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(23,GPIO.IN) 
GPIO.setup(24,GPIO.OUT)
while True:
 if GPIO.input(23) == 1: 
  GPIO.output(24,GPIO.HIGH)
 else: 
  GPIO.output(24,GPIO.LOW)

 time.sleep(1)
GPIO.cleanup()

现在,让我们看看发生了什么。逻辑非常简单。一旦 PIR 传感器检测到运动,它就会将输出引脚设置为高电平。我们所要做的就是监视该引脚,基本上就是这样。

逻辑与按键开关完全相似,它也会以类似的方式工作。因此,不需要太多解释。

接口超声波接近传感器

首先,基础知识。接近传感器是一种传感器,它可以感知到与其接近的物体。有许多传感器可以完成这项任务,以及许多技术可以让我们这样做。正如其名称所示,超声波接近传感器是基于超声波的工作原理。工作原理非常容易理解。超声波传感器发射一束超声波;这些波对人耳来说是听不见的,但它仍然是一种声波,它也像声波一样行为。

现在,我们知道声音会反射不同的表面并形成回声。当您在空房间说话时,您一定有过这种回声的经历。您可以听到自己的声音,但有轻微的延迟。这种延迟是由声音的特性引起的。声音是一种波,因此它有速度。声波有固定的传播速度。因此,为了覆盖特定的距离,它们需要一定的时间。通过计算这段时间,我们可以推导出声波在从表面反射之前走过的距离。

同样,在这种传感器中,我们向特定方向发射超声波,然后感知反射回来的回声。自然地,接收回声会有一定的延迟;这个延迟会与物体距离传感器的距离成正比,基于这个延迟,我们可以轻松计算出距离。

现在,要使用接近传感器,我们需要了解传感器的物理结构以正确接线。传感器上有四个引脚,它们是:

  • VCC(正极)

  • 触发

  • 回声

  • GND(地线)

显然我不需要解释 VCC 和地线的作用。因此,让我们直接转到触发。每当引脚高电平持续 10 微秒时,超声波传感器将向目标发送 8 个 40kHz 的声波周期。一旦触发周期完成,ECHO被设置为高电平。一旦接收到回声信号,ECHO引脚就会被设置回低电平。以下是一个图表,展示了它实际发生的过程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这就是我们现在需要知道的全部。随后,随着我们的学习,我们会了解更多。现在,继续并让它运行起来,按照图表连接:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

连接完成后,需要运行以下代码:

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
GPIO.setup(23,GPIO.OUT) 
GPIO.setup(24,GPIO.IN) 

while True:
     pulse_start = 0
     pulse_stop = 0
     duration = 0
     distance = 0

     GPIO.output(23,GPIO.LOW)
     time.sleep(0.1) 
     GPIO.output(23,GPIO.HIGH)
     time.sleep(0.000010)
     GPIO.output(23,GPIO.LOW)

     while GPIO.input(24)==0:
         pulse_start = time.time()

     while GPIO.input(24)==1:
         pulse_stop = time.time()

     duration = pulse_stop - pulse_start

     distance = duration*17150.0
     distance = round(distance,2)
     print ("distance" + str(distance)) 

     time.sleep(0.2)
}

现在,一旦您运行这个程序,屏幕上的输出将每 0.2 秒显示一次物体的距离。现在,您一定想知道这是如何传达所有这些读数的:

GPIO.setup(23,GPIO.OUT) 

我们将分配引脚23在需要时给传感器的TRIGGER引脚提供脉冲:

GPIO.setup(24,GPIO.IN)

我们将分配引脚24来接收逻辑以确认接收到回声信号:

pulse_start = 0
 pulse_stop = 0
 duration = 0
 distance = 0

我们将使用上述作为变量,并且每次循环开始时,我们都会给它们赋值为0;这是为了清除在程序过程中存储的先前读数:

GPIO.output(23,GPIO.HIGH)
  time.sleep(0.000010)
  GPIO.output(23,GPIO.LOW)

我们保持触发引脚编号23高 0.000010 秒,以便超声波传感器可以发送一脉冲超声波:

  while GPIO.input(24)==0: 
 pulse_start = time.time()

这个 while 语句将一直记录pulse_start变量的时间,直到引脚编号24的时间为低电平。最终的时间读数将存储在pulse_start变量中,即记录脉冲发送的时间:

while GPIO.input(24)==1:
 pulse_stop = time.time()

在这个循环中的while语句将开始记录引脚编号24高电平的时间,并将一直记录时间,直到引脚编号24保持高电平。时间的最终读数将存储在pulse_stop变量中,即记录脉冲接收的时间:

 duration = pulse_stop - pulse_start

在这个声明中,我们正在计算脉冲从传感器到物体再反弹到传感器接收器所需的总时间:

 distance = duration*17150.0

这是制造商提供的一个算术公式,用于将超声波传播所需的时间转换为厘米的实际距离。你可能会问我们是如何得到这个方程的?

让我简要介绍一下。通过初等物理,我们会记得这个简单的方程:速度 = 距离 / 时间

现在你可能还记得声音的速度是 343 米每秒。现在 1 米有 100 厘米,因此要将这个速度转换为每秒厘米,我们必须将速度乘以 100,因此速度将是每秒 34,300 厘米。

现在我们知道方程的一个元素,即速度。所以让我们把速度的值放入方程中。现在方程看起来会是这样:34,300 = 距离 / 时间

现在我们知道声音传播的距离是实际距离的两倍。为什么?因为声音首先从传感器传向物体。然后它从表面反射回来并到达传感器。因此,它实质上覆盖了两倍的距离。因此,为了适应这个方程,我们必须做出一个小改变:34,300 / 2 = 距离 / 时间

现在我们想从这个方程中得到距离,所以让我们把所有其他部分移到另一边。现在方程看起来会是这样:17,150 * 时间 = 距离

所以这里有距离的公式。

distance = round(distance,2)

由于超声波传播的距离是实际距离的两倍(一次是朝物体前进,第二次是反弹到传感器),我们将其除以二得到实际距离:

print 'Distance = ',distance

最后,我们将通过以下声明打印测得的距离。任何在引号内的内容'...'都将按原样写入。但是,distance没有引号,distance 是一个变量。因此,存储在距离中的变量将在屏幕上的最终输出中写入:

 time.sleep(0.25)

代码将在这一行暂停 0.2 秒。如果没有这个暂停,那么值将以令人难以理解的速度出现,这对我们来说将很难阅读或理解。如果你在摆弄,我建议删除这个声明并运行代码看看会发生什么。

通过 I2C 进行接口

到目前为止,一切都很好。电子电路可能非常有趣,虽然它们看起来非常复杂,但我们经常发现工作非常简单。在前一节中,我们一次只接口一个传感器。我们可以继续接口多个传感器,但我们受到现有 GPIO 数量的限制。我们还看到一些传感器,如超声波传感器可能使用多个 GPIO 引脚进行工作。这进一步减少了我们可以与微控制器接口的传感器数量。一旦我们转向更复杂的电路,我们还会意识到布线可能会变得非常混乱,如果出现问题,找出问题所在将变得非常繁琐。

现在,我们在设计机器人系统时面临的一个更大的问题是时间的问题——系统中的所有工作都必顶同步。目前大多数系统都是顺序的,即一个单元的输出成为另一个单元的输入:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,为了完成任务,当需要时,处理单元 1必须将输入传递给处理单元 2处理单元 3也是如此。如果数据的时间不完美,那么处理单元 2要么会一直等待处理单元 1的输入,要么更糟糕的是,处理单元 1会在处理单元 2不需要数据的时候发送数据。在这种情况下,数据将丢失,过程将出现一些错误。

因此,为了解决这个问题,当时的计算机科学家发明了一种脉冲系统。时钟脉冲是一个非常简单的方波,具有 50%的占空比(回想一下脉冲宽度调制(PWM))。电路被设计为在时钟脉冲的上升沿或下降沿执行一次操作。由于这种同步,电路的每个部分都知道何时工作。时钟脉冲的样子如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,回到问题上,我们有两个问题:

  • 机器人连接的设备/传感器存在物理限制

  • 如何使传感器和互连电路的时间协调工作

为了解决这些问题,我们使用了一个非常常用的协议,称为 I2C,代表着互联集成电路。当我们需要在相同的 GPIO 上连接多个设备时,比如只有一组 GPIO 引脚可以连接多个传感器时,这个协议非常有用。这是由于为每个硬件分配了唯一的地址。该地址用于识别传感器,然后相应地与其通信。现在,要实现 I2C 协议,我们需要两条线路;这些线路如下:

  • 数据

  • 时钟

正如你可能已经猜到的那样,时钟线用于向连接的设备发送时钟脉冲,数据是数据流动的总线。

现在,整个 I2C 架构是基于主从配置工作的,其中主设备始终为从设备生成时钟信号,从设备必须不断寻找主设备发送的时钟脉冲和数据包。让我们看看是如何完成的。

如前所述,有两条线路:数据线称为串行数据(SDA),时钟线称为串行时钟(SCL)。从现在开始,我们将使用 SCL 和 SDA 这些术语:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传让我们看看图中显示的主要要点:

  • 起始条件:为了开始通信,创建一个起始条件,表示通信即将发生。主设备通过在 SCL 之前保持 SDA 线低来表示这个条件。这表示所有从设备都准备好进行通信。

  • 地址帧:一旦通信开始,主设备发送需要通信的设备的地址。这是一个 7 位地址。在每个时钟脉冲中,一个位被发送,因此需要七个时钟脉冲来发送 7 位地址。在这 7 位地址之后是读/写位。这表明设备是否在这个操作中想要写入,还是想要读取一些数据。因此,总地址帧是 8 位,需要八个时钟脉冲来发送。在这八个脉冲之后,在第九个时钟脉冲期间,主设备等待来自设备的确认。当 SDA 线被被寻址的从设备拉低时,从设备发送这个确认。通过这种策略,主设备知道它发送的地址已经被接收,并且从设备现在准备好进行通信。如果没有发送确认,那么由主设备决定接下来该做什么。

  • 数据帧:一旦确认被发送,根据是读操作还是写操作,数据要么由主设备写入从设备,要么在读操作中,数据由从设备发送到主设备。这个数据帧的长度可以是任意的。

  • 停止帧:一旦数据传输完成,主设备发出停止条件,表示通信必须停止。当 SDA 线在 SCL 线从低电平变为高电平后,此条件被执行。

这基本上就是 I2C 通信的工作原理。对于每个设备,我们有一个 7 位地址,因此我们可以在单个总线上连接多达 128 个设备。这是很多设备。几乎可以忽略物理限制用完的可能性。现在让我们继续看看如何通过这种协议连接传感器。通常,不需要为 I2C 进行核心编程,因为这很冗长和繁琐。这就是开源的魔力所在。全球有很多开发人员正在研究这些传感器,其中大多数人足够慷慨,制作了一个库并分享给大家以便编程。这些库可以在线获取,其中大多数库都处理了通信的复杂过程。

现在是我们接口第一个 I2C 设备的时候了,这是一个模拟到数字转换器。你一定会想为什么我们首先使用这个转换器。回想一下我们开始理解 GPIO 引脚的时候。这些神奇的引脚可以用作输入和输出;你可能还记得这些引脚可以是开或关状态——这些都是数字引脚,不仅在输出时,也在输入时。但是有大量的传感器是通过模拟通信工作的。由于树莓派的数字架构,直接接口这些传感器是困难的。因此,我们使用模拟到数字转换器ADC),这个转换器将传感器的模拟值转换为树莓派可以理解的数字位。

我们将连接一个 LDR,电阻将根据光线的多少改变电阻值。因此,电压将取决于光线照射在 LDR 上的多少。

现在让我们看看如何实际操作。拿起你的树莓派,让我们开始吧。首先,我们需要在树莓派上启用 I2C;按照这里列出的步骤进行操作:

  1. 打开终端(Ctrl + Shift + T

  2. 输入sudo raspi-config

  3. 选择接口选项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 然后转到高级选项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 然后选择 I2C 以启用它。然后选择是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在安装adafruit库以接口 ADC1115:

  1. 打开终端并复制以下命令:
sudo apt-get install build-essential python-dev python-smbus python-pip 

这个命令将库和依赖项下载到树莓派上

  1. 现在输入以下内容:
sudo pip install adafruit-ads1x15

这个命令将库和依赖项安装到树莓派上。

现在软件已经设置好了,让我们准备好硬件。按照下图将树莓派连接到 ADS1115:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

准备好后,继续在 Pi 上上传这段代码:

import time
import Adafruit_ADS1x15
import RPi.GPIO as GPIO
LED =14

GPIO.setmode(GPIO.BCM)
GPIO.setup(LED,GPIO.OUT)

adc = Adafruit_ADS1x15.ADS1115()
GAIN = 1
channel=0
adc.start_adc(channel, gain=GAIN)

while True:
    value = adc.get_last_result()
    print(str(value))
    time.sleep(0.1)
    if value >= 100:
        GPIO.output(LED,1)
    else :
        GPIO.output(LED,0)

adc.stop_adc()

请注意,有时这段代码可能不起作用,如果是这样,请尝试调整阈值的值:

if value >= 100:

你可能已经注意到,每当 LDR 面向光源时,LED 也会亮起,而当它远离光线时,LED 会熄灭。

现在你已经接口了一个 I2C 设备。让我们了解这段代码实际上是如何工作的:

import Adafruit_ADS1x15

上一行代码导入了Adafruit_ADS1x15库,以便我们在程序中使用它的所有函数。

adc = Adafruit_ADS1x15.ADS1115()

上一行代码创建了库Adafruit_ADS1x115的实例。.ADS1115()是创建实例adc的函数。明白了吗?让我用英语解释一下。

现在,我们可以简单地写adc而不是一直写Adafruit_ADS1x15,来调用库函数。此外,你可以使用任何单词代替adc;它可以是你猫的名字或你邻居的名字,它仍然可以工作:

GAIN = 1

这是传感将进行的值。1表示传感将在整个范围内进行。对于我们的 ADC 来说,范围是从 0V 到+/-4.096V 的电压范围。现在改变增益会导致传感范围的改变。也就是说,如果我们将增益值更改为2,那么传感范围将是原始范围的一半,即 0 到+/-2.048 伏。

现在你可能会问电压范围是多少,为什么我们要改变增益?

原因很简单。有不同类型的模拟传感器。它们的输出电压范围各不相同。有些传感器的输出范围是 0.5 伏到 4 伏,其他的可以是 0.1 伏到 0.98 伏。现在,如果我们将增益设置为1,那么所有这些传感器都可以轻松接口。因为它们都在 0 到 4.098 伏的感应范围内。然而,由于它是一个 16 位 ADC,因此 ADC 可以提供的离散值的总数将在 2¹⁶或 65,536 个读数之间。因此,在增益为1时,ADC 可以检测到的最小电压变化为:4.096 / 65536 = 0.000062

但是,如果增益增加到4,那么传感范围将减少到仅为0到+/-1.0245。因此,这将能够处理 0.1 伏到 0.98 伏之间的输出范围。但现在让我们看看它可以检测到的最小电压变化:1.0245 / 65536 = 0.00001563

现在你可以看到可以检测到的最小电压非常低。这对于与传感器的兼容性是一件好事。

现在,你可以决定你想要什么增益值。LDR 在 5V 上工作,因此最好使用整个增益读数为1

channel=0

当你仔细观察 ADC 硬件时,你会注意到有各种引脚,包括A0A1A2A4。这是一个四通道 ADC——它可以将四个模拟输入转换为数字数据。由于我们只使用一个数据流,我们将让 Pi 知道它连接在哪个引脚上。通过下面的代码,我们告诉 Pi 开始转换数据的过程:

adc.start_adc(channel, gain=GAIN)

在下一行中,我们指示 ADC 停止转换,代码到此结束。

adc.stop_adc()

摘要

本章主要讲述了如何将传感器与 GPIO 进行接口,以便传感器可以检索数据。

第十九章:制作园丁机器人

好了,朋友们,你已经了解了一些输入和输出的基础知识;现在是时候制作一些我们可以交出一些日常责任的东西了。这个机器人可能看起来并不像一个机器人,但相信我,它会让你的生活更轻松。最重要的是,你花园中的大部分植物都会因为你的制作而祝福你。

我们将涵盖以下主题:

  • 与电磁阀一起工作

  • 制作机器人

  • 使它更智能

  • 使它真正智能

与电磁阀一起工作

我们要做的是一个自动系统,它会在植物需要时给它们浇水。所以从技术上讲,一旦它建立起来,你就不用担心给你的绿色生物浇水了。无论你是在家里、在办公室还是度假,它都会不管任何情况下继续工作。

现在,你一定在想它是如何给植物浇水的,所以让我告诉你,对于这个世界上的每个问题,都存在一个解决方案。在我们的情况下,这个解决方案被称为电磁阀。它的基本作用是切换液体的流动。市场上有各种各样的电磁阀;一些识别特征如下:

  • 尺寸:它们有各种尺寸,如半英寸、四分之三英寸、1 英寸等。这基本上将决定电磁阀的流量。

  • 介质:无论是液体、气体、蒸汽等。

  • 正常状态

  • 通常打开:这个阀门在关闭状态下会允许液体流动——当阀门没有供电时

  • 通常关闭:这个阀门在关闭状态下会阻止液体流动——当阀门没有供电时

  • 方式数量:一个简单的阀门会有一个进口和一个出口。所以,当它打开时,它会允许液体从进口流向出口。然而,还可以有其他类型的阀门,比如三通阀,可能有两个出口和一个进口。它会调节液体的流动方向。

阀门的一些具体细节也可能会有所不同,但目前我们只需要知道这些。关于电磁阀要注意的一点是,这些阀门可以打开或关闭。无法实现这些阀门之间的任何状态或通过这些阀门控制流动。为此,我们可以使用伺服阀或电动阀。但目前我们不需要。

在本章中,我们将使用一个半英寸的水/液体阀,它通常是关闭的。当你仔细看这个阀时,你会发现它在 12 伏特下运行,电流消耗接近 1 安培。这对树莓派来说是很大的电流。树莓派每个引脚可以提供的电流上限约为 50 毫安。所以如果我们把这个阀接到树莓派上,它肯定不会工作。

我们现在该怎么办?这个问题的答案是继电器。继电器的基本工作是重新布置电路。基本上,它是一个电子控制开关。继电器的基本工作是打开和关闭具有比控制单元提供的更高电流/电压消耗的设备。这是一个相当简单的设备,正如你在图中所看到的。有两个电路。一个是蓝色的,是低电压和低电流电路。这个电路正在给线圈供电。另一个电路是红色和黑色的。这个电路是高电压、高电流电路。

在初始阶段,正如你所看到的,高电压高电流电路不完整,烤箱不会工作:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,在这第二个图中,你可以看到蓝色电路连接到 5V 电源,线圈被激活。每当线圈被激活,它就形成一个电磁铁,吸引高功率电路的金属片,使电路完整,从而给烤箱供电:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这就是电磁阀的工作原理。线圈的消耗几乎只有几毫安,因此通过微控制器驱动线圈非常容易。这反过来使得最终电路之间产生接触。

市场上有各种类型的继电器;一些识别特征如下:

  • 最大输出电压:它可以处理的最大电压

  • 最大输出电流:它可以承受的连接到它的任何输出设备的最大电流

  • 信号电压:它需要开关组件的电压

  • 正常条件:

  • 正常关闭:这将不允许任何电流流动,直到接收到信号为止

  • 正常开启:它将允许电流流动,直到接收到信号为止

现在,回到我们的园艺机器人,连接到它的电磁阀将在 1 安培和 12V 上工作,因此任何可以提供等于或大于 1 安培和 12V 的继电器都可以工作。

通常,市场上可用的继电器是 120V 和 12 安培直流。要记住的一件重要事情是交流电压和直流电压和电流将有两个单独的等级。由于我们的电磁阀将在 12V 下工作,我们只考虑直流的上限。

制作机器人

现在,让我们开始制作机器人。首先,您需要从水龙头到电磁阀的水管连接,从电磁阀到洒水器的连接。您还需要进行以下连接:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在让我们开始编程。在这个机器人中,我们将接口一个土壤湿度传感器。该传感器的工作是确定土壤中的水量。通过确定这一点,我们可以了解花园是否需要水。这个土壤湿度传感器是一个模拟传感器,因此我们将使用 ADC 将模拟读数转换为树莓派可理解的数字值。所以让我们开始吧:

import time
import RPi.GPIO as GPIO
import Adafruit_ADS1x15
water_valve_pin = 23
moisture_percentage = 20
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(water_valve_pin, GPIO.OUT)
adc = Adafruit_ADS1x15.ADS1115()
channel = 0
GAIN = 1
while True:
 adc.start_adc(channel, gain=GAIN)
 moisture_value = adc.get_last_result()
 moisture_value= int(moisture_value/327)
 print moisture_value
 if moisture_value < moisture_percentage:
 GPIO.output(water_valve_pin, GPIO.HIGH)
 time.sleep(5)
 else:
 GPIO.output(water_valve_pin, GPIO.LOW)

在运行此代码之前,让我们先了解它实际上在做什么:

moisture_percentage = 20

moisture_percentage = 20是一个阈值百分比;如果土壤中的湿度水平低于 20%,那么您的花园就需要水。这是您的机器人将继续寻找的条件;一旦满足这个条件,就可以采取适当的行动。这个百分比也可以根据您花园的需要更改为3040或其他任何值:

moisture_value = int(moisture_value/327)

ADC 是一个 16 位设备——有 16 个二进制数字可以表示一个值。因此,该值可以在02¹⁵之间,换句话说,可以在032768之间。现在,很简单的数学,对于每个百分比的湿度,ADC 将给出以下读数:32768/100,或327.68。因此,要找出土壤中的湿度百分比,我们需要将 ADC 给出的实际值除以327.68

其余的代码非常简单,一旦您阅读它,您就不会很难理解。

使其更智能

祝贺您制作了您的第一个机器人!但您是否注意到了一个问题?我们制作的机器人一直在寻找湿度值,一旦注意到湿度值偏低,它就会突然泵水,并确保土壤的湿度始终高于 20%。然而,这是不必要的。一般来说,我们每天浇水一两次。如果我们浇水更多,那对植物可能不利。

因此,让我们继续使它稍微更智能化,并且只在特定时间土壤湿度低时给植物浇水。这一次,我们不需要对硬件进行任何更改;我们只需要微调代码。

让我们继续上传以下代码,然后看看到底发生了什么:

from time import sleep
from datetime import datetime
import RPi.GPIO as GPIO
import Adafruit_ADS1x15
water_valve_pin = 23
moisture_percentage = 20
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(water_valve_pin, GPIO.OUT)
adc = Adafruit_ADS1x15.ADS1115()
GAIN = 1
def check_moisture():
 adc.start_adc(0,gain= GAIN)
 moisture_value = adc.get_last_result()
 moisture_value = int(moisture_value/327)
 if moisture_value < moisture_level:
 GPIO.output(water_valve_pin, GPIO.HIGH)
 sleep(5)
 GPIO.output(water_valve_pin, GPIO.LOW)
 else:
 GPIO.output(water_valve_pin, GPIO.LOW)
while True:
 H = datetime.now().strftime('%H')
 M = datetime.now().strftime('%M')
 if H ==07and M <=10:
 check_moisture()
 if H ==17and M <=01:
 check_moisture()

这段代码可能对您来说有点陌生,但相信我,它就是这么简单。让我们一步一步地看看发生了什么:

from datetime import datetime

这行代码是从日期时间库中导入日期时间实例。这是 Python 中默认的一个库。我们只需要调用它。它的作用是在我们的代码中轻松确定时间。

def check_moisture():

有时我们必须一遍又一遍地做一些事情。这些代码集可以是几行重复的代码,也可以是多页的代码。因此,重写那些代码毫无意义。我们可以创建一个函数。在这个函数中,我们可以定义每次调用时会发生什么。在这行代码中,我们创建了一个名为check_moisture()的函数;现在,每当程序中调用这个函数时,将执行一系列活动。将要执行的一系列活动由用户定义。因此,每当我们写def时,就意味着我们正在定义一个函数;然后,我们写出需要定义的函数的名称。

完成后,然后我们在缩进中写的任何内容都将在调用函数时执行。请记住,每当我们调用或定义一个函数时,函数名称的末尾都有一个开放和关闭的()括号表示:

 moisture_value = adc.get_last_result()

adc.get_last_result()adc的一个函数。它的功能是简单地从之前定义的引脚(引脚号为0)获取结果,并将读数存储到变量moisture_value中。因此,在moisture_value之后将是 ADC 引脚号0的读数,或者换句话说,是湿度传感器的读数。

H = datetime.now().strftime('%H')

代码datetime.now()的一个实例和方法。这个函数的作用是更新时间。现在,datetime.now()已经更新了日期和时间的所有参数,包括小时、分钟、秒,甚至日期。我们可以选择是否要全部或者日期和时间的任何特定部分。目前,我们想要将小时的值放入变量H中,因此我们使用了.strftime('%H')方法。strftime代表时间的字符串格式。因此,它输出的任何值都是以字符串格式。('%H')表示它只会给我们小时的值。同样,我们也可以使用('%M')('%S)来获取分钟的时间。我们还可以使用以下语法获取日期、月份和年份的值:

  • 获取日期:('%d')

  • 获取月份:('%m')

  • 获取年份:('%Y')

if H ==07and M <=10:

在前面的条件中,我们正在检查时间是否为 7 点;此外,我们还在检查时间是否小于或等于 10 分钟。因此,只有当时间为 7 小时并且在 0 到 10 分钟之间时,此代码段才会运行if语句中的语句。

特别要注意的一点是,我们在两个条件之间使用了and,因此只有在两个语句都绝对为真时才会运行其中的代码。我们还可以在其中使用一些其他语句,比如or,在这种情况下,如果其中一个语句为真,它将运行代码。

如果我们在这个if语句中用or替换and,那么它将在每个小时的 0 到 10 分钟内运行代码,并且将在上午 7:00 到 7:59 之间的整个时间内连续运行代码:

check_moisture()

正如你可能记得的,之前我们定义了一个名为check_moisture()的函数。在定义该函数时,我们还定义了每次调用该函数时将发生的一系列活动。

现在是调用该函数的时候了。一旦程序到达代码的末尾,它将执行之前在函数中定义的一系列活动。

所以我们就是这样。现在,一旦你运行这段代码,它将等待程序中定义的时间。一旦达到特定的时间,它将检查湿度。如果湿度低于设定值,它将开始给植物浇水,直到湿度超过阈值为止。

真正智能化

了不起的工作!我们已经开始自己建造比我们更聪明的东西。但现在我们想要更进一步,让它比我们更聪明——这就是机器人存在的意义。不仅仅是做我们做的事情,而是以更好的方式做所有这些。

那么,我们能做些什么改进呢?在寒冷的冬天,我们不需要太多的水,但在夏天,我们需要比冬天喝的水多得多。植物也是一样的情况。

在冬天,它们需要的水量要少得多。此外,土壤中的水蒸发速度也较慢。因此,在这两种情况下,我们需要向花园供应不同数量的水。问题是,我们该如何做到呢?

首先,要知道外面是热还是冷,我们需要一个传感器。我们将使用一个名为 DHT11 的传感器。这是一个便宜但坚固的传感器,可以给我们提供温度和湿度的读数。最好的部分是,它的价格非常便宜,大约 2 美元。

它有四个引脚。但是,如果你认为它将适用于 I2C 协议,那么你就错了。它有自己的数据传输方法。拥有一个单一的协议来处理所有传感器是很好的,但通常你也会发现有各种传感器或设备使用不同或全新的协议。DHT11 就是这样的传感器。在这种情况下,我们可以选择要么理解整个通信方法,要么简单地从制造商那里获取库并随时使用。目前我们将选择后者。

现在让我们看看 DHT11 的引脚是什么样子的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你可以看到这里只有一个信号引脚,它将完成所有数字通信。有两个电源引脚,其中一个引脚没有使用。也就是说,这个引脚没有明显的用途。它可能只是用于焊接或将来使用。这个传感器使用 5V 电源,只需要几毫安,因此我们可以通过树莓派来为其供电。现在,对于数据通信,我们将把信号引脚连接到 GPIO 引脚号4

在我们开始编写代码之前,让我们先安装 DHT11 和树莓派之间的通信库。我们之前已经在 ADS1115 的库中做过这个,但在这个库中有一些小技巧需要我们注意。所以让我们开始吧。

首先,我们需要确保你的树莓派操作系统是最新的。所以将树莓派连接到互联网,打开树莓派的命令提示符,输入以下命令:

sudo apt-get update

这个命令将自动更新你的树莓派的 raspbian 操作系统。然后继续输入这个命令:

sudo apt-get install build-essential python-dev python-openssl

在这个命令中,我们正在安装以下软件包:

  • build-essential

  • python-dev

  • python-openssl

你一定在想为什么我们要安装所有这些。好吧,长话短说,这些是我们即将安装的 DHT11 通信库的依赖项。如果这些软件包没有安装在树莓派上,我们将无法使用该库。

最后,我们必须安装库;这是一个通用库,其中还包括与 DHT11 传感器通信的功能。这应该足以满足我们的简单通信需求。以下是安装它的命令:

sudo python setup.py install

好了,我们准备好了。我们的系统已经准备好与 DHT11 进行通信。让我们首先看看我们到目前为止所做的是否按我们想要的方式工作。为了做到这一点,按照以下方式连接 DHT11;你可以将其他组件如电磁阀和土壤湿度传感器连接好。它们不应该干扰。现在在树莓派上上传以下代码:

from time import sleep
from datetime import datetime
import RPi.GPIO as GPIO
import Adafruit_DHT
sensor = 11
pin = 4
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
while True:
 humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)
 print("Temperature: " +temperature+ "C")
 print("Humidity: " +humidity+ "%")
 time.sleep(2)

一旦你上传了这段代码,你将在屏幕上看到传感器的读数。这段代码只是简单地为你提供传感器的原始读数。这段代码非常简单,你会理解其中的一切,除了一些代码行,其中包括:

import Adafruit_DHT

在代码的这一行中,我们在代码中导入了Adafruit_DHT库。这是与 DHT11 传感器通信的相同库。

sensor = 11 

DHT 有不同的版本,如 DHT11、DHT22 等。我们需要告诉程序我们使用的是哪种传感器。因此,我们已经为变量传感器分配了一个值。稍后,你将看到我们将如何使用它:

pin = 4  

在这一行中,我们将值 4 赋给一个名为pin的变量。这个变量将用于告诉程序我们已经连接了 DHT11 的树莓派引脚。

humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)

在这一行中,我们使用了Adafruit库的一个方法,名为Adafruit_DHT.read_retry()。现在,它的作用是读取 DHT 传感器,并将传感器的读数给变量humiditytemperature。需要注意的一点是,DHT11 每 2 秒更新一次读数。因此,你将在每 2 秒后收到更新的读数。

一旦这段代码完成,我们就可以确信传感器正在按我们想要的方式工作。最后,是时候将所有传感器整合在一起,制作一个完全智能的机器人了。由于电磁阀、湿度传感器和温度传感器已经连接好,我们所需要做的就是将代码上传到树莓派上,然后看魔法发生。

from time import sleep
from datetime import datetime
import RPi.GPIO as GPIO
import Adafruit_ADS1x15
import Adafruit_DHT
water_valve_pin = 23
sensor = 11
pin = 4
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(water_valve_pin, GPIO.OUT)
Channel =0
GAIN = 1
adc = Adafruit_ADS1x15.ADS1115()
def check_moisture(m):
 adc.start_adc(channel, gain=GAIN)
 moisture_value = adc.get_last_result()
 moisture_value = int(moisture_value/327)
 print moisture_value
 if moisture_value < m:
 GPIO.output(water_valve_pin, GPIO.HIGH)
 sleep(5)
 GPIO.output(water_valve_pin, GPIO.LOW)
 else:
 GPIO.output(water_valve_pin, GPIO.LOW)
while True:
 humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)
 H = datetime.now().strftime(%H’)
 M = datetime.now().strftime(%M’)
 if H ==07and M <=10:
 if temperature < 15:
 check_moisture(20)
 elif temperature >= 15 and temperature < 28:
 check_moisture(30)
 elif temperature >= 28:
 check_moisture(40)
 if H ==17and M <=10:
 if temperature < 15:

 check_moisture(20)
 elif temperature >= 15 and temperature < 28:
 check_moisture(30)
 elif temperature >= 28:
 check_moisture(40)

代码很长,对吧?看起来是这样,但是一旦你逐行编写它,你肯定会明白,它可能比我们迄今为止编写的所有代码都长,但它一点也不复杂。你可能已经理解了大部分程序,但是让我解释一下我们在这里使用的一些新东西:

def check_moisture(m):
  adc.start_adc(channel, gain = GAIN)

moisture_value = adc.get_last_result()
moisture_value = int(moisture_value / 327)
print moisture_value

if moisture_value < m:
  GPIO.output(water_valve_pin, GPIO.HIGH)
  sleep(5)
  GPIO.output(water_valve_pin, GPIO.LOW)
else :
  GPIO.output(water_valve_pin, GPIO.LOW)

在这一行中,我们定义了一个名为check_moisture()的函数。以前,如果你还记得,当我们制作check_moisture函数时,我们基本上是在检查湿度值是否大于或小于 20%。如果我们需要检查 30%、40%和 50%的湿度怎么办?我们会为此制作一个单独的函数吗?

显然不是!我们所做的是向函数传递一个参数,参数基本上是放在函数括号内的变量。现在我们可以为这个变量分配值,例如check_moisture(30)-现在在执行该函数时m的值将为 30。然后,如果再次调用check_moisture(40),那么m的值将为 40。

现在,你可以看到我们在整个函数中比较m的值。

   if moisture_value < m:

if 语句将检查调用函数时分配的m的值。这使我们的工作变得非常简单。

让我们看看程序的其余部分在做什么:

            if temperature < 15:
                check_moisture(20)

每当达到所需的时间,它将继续检查温度。如果温度低于 15 度,它将调用函数check_moisture并将参数值设为 20。因此,如果湿度低于 20%,则会给花园浇水。

 elif temperature >= 15 and temperature < 28:
                check_moisture(30)

elifelse if语句在if语句之后使用。通俗地说,这意味着如果前面的if语句不成立,它将检查这个if语句。因此,在前一行中,它将检查温度是否在 15 到 28 摄氏度之间。如果是,它将检查土壤的湿度。在这一行中,函数的参数是 30。因此,它将检查湿度是否低于 30。如果是,它将给花园供水。

 elif temperature >= 28:
                check_moisture(40)

同样,在这行代码中,我们正在检查温度,如果温度等于或超过28摄氏度,那么它将把值40作为参数传递给函数check_moisture。因此,这次它将检查湿度是否达到或超过28

正如您所看到的,现在系统将检查环境温度,并根据此调节植物所需的水量。最好的部分是它是一致的,并将提供植物所需的正确水量。

本章中提到的数值仅为假设值。我强烈建议根据您所在地区和您花园中种植的植物来调整数值,以便系统发挥最佳效果。

总结

在本章中,我们涵盖了一些主题,如电磁阀集成和土壤湿度传感器,以构建一个可以自动给您的后院花园浇水的机器人。接下来,我们将介绍电机的基础知识。

第二十章:电机基础

好了!我们已经制作了一个照顾你花园的机器人,我希望它运行良好。现在是时候把事情提升到另一个水平了。

我们一直认为机器人就像 WALL-E 一样,四处移动并为我们做事。好吧,朋友,现在这个梦想并不遥远。事实上,在这一章中,我们将继续前进并制作一个。让我们看看如何做到。

我们将涵盖以下主题:

  • 基础知识

  • 让它滚动

  • 改变速度

基础知识

每当我们谈论从一个地方移动到另一个地方时,我们都会想到轮子,同样,每当我们想到移动机器人的车轮时,我们都会想到电机。存在各种不同类型的电机。因此,让我们首先看看最基本的电机类型,即称为刷式直流电机的电机。顾名思义,它是在直流电上工作的。你可能会发现这样的电机:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

相信我,这些东西无处不在,从你为邻居买的圣诞礼物到最大的坏家伙机器,你都会发现这些电机隐藏在引擎盖下。这些电机之所以常见,是因为它们非常非常简单。如此简单,以至于只需要一块电池和两根导线就可以为它们供电。只需将正极连接到一个端子,负极连接到另一个端子,电机就会开始旋转。交换这些连接,旋转方向将改变。取两个电池并加倍电压,电机将旋转得更快。就是这么简单。

现在你可能会认为我们只需将这个电机连接到树莓派,然后就可以开始了。但不幸的是,情况并非如此。你可能还记得前几章提到的,树莓派只能提供大约 50 毫安,但电机的消耗可能要高得多。因此,为了运行一个电机,我们需要一个中间设备。

你脑海中首先想到的可能是使用继电器,为什么不呢?它们可以传输大量电流并且可以处理高电压。这应该是理想的选择。如果你这样想,你是对的,但只是在某种程度上,因为继电器只是一个我们可以用来打开或关闭电机的开关。我们将无法控制电机的速度或旋转方向。现在,你可能会认为这个问题并不新鲜,我们可以很容易地通过使用脉宽调制PWM)来解决,对吗?好吧,答案是否定的!因为这些继电器是机械设备,由于它们的机械性质,每秒开关的最大限制是有一些的。因此,它将无法应对 PWM 的频率。最后,我们仍然会面临改变电机的方向和速度的问题。那么现在我们该怎么办呢?

正如我经常说的,问题的美妙之处在于它总是有解决方案,而这里的解决方案被称为电机驱动器。电机驱动器主要是一组电子继电器——一种可以允许高电流但不是机械的开关。因此,我们可以每秒切换数百次。这些电子继电器要么由简单的晶体管制成,要么在高功率应用中,甚至可以使用 MOSFET 进行切换。我们可以简单地给这些电子开关提供 PWM,并在确保向电路传递足够电流的同时使电压调制。此外,正如我之前提到的,电机驱动器由一组这些电子继电器组成。它们排列的最常见和可行的方式称为全桥或 H 桥。在我进一步解释之前,让我们看看这到底是什么:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在全桥中,我们有四个与连接的电机相连的开关电路;根据需求,这些可以独立地打开或关闭。在关闭状态下,所有这些开关电路都处于断开状态,因此保持电机关闭。现在,每当我们想要启动电机时,我们将不得不打开两个开关,使电路完整,电机开始工作。让我们看看它会是什么样子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里,我们打开了开关电路S2S3;这反过来完成了电路,让电流在电机中流动。现在,为了控制速度,这些相同的开关电路可以以非常高的频率以不同的占空比打开和关闭,以实现特定的平均电压。现在我们可以通过这两个开关电路改变电压来实现电机的特定速度,让我们看看如何改变电机的旋转方向:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个电路中,我们关闭了之前连接的S2S3,而是打开了S1S4,因此电机的极性被颠倒了。正如我们之前讨论的,每当直流刷电机的极性改变时,方向也随之改变。市场上有各种类型的电机驱动器。我们在这里理解的是称为刷式直流 H 桥电机驱动器;还有其他类型的电机驱动器用于控制其他类型的电机,但目前我们只会专注于刷式电机。在选择电机驱动器时,您应该非常仔细地检查电机驱动器的规格表。将提到的一些关键规格如下:

  • 电压等级:电机驱动器可以处理和调制的电压将有最小和最大限制。确保您的电机位于特定电压范围之间。

  • 电流评级:电机驱动器可以处理的绝对最大电流;超出这个范围将烧毁或损坏电机驱动器。这可能有点误导。让我们看看为什么。除了绝对最大值,可能会指定许多其他电流评级。这些可能是:

  • 重复最大电流:这是电机驱动器可以处理的最大电流,但不是持续的。给出这个评级是因为有时电机的负载可能会增加,可能会在短暂时刻需要更高的电流。电机驱动器将在重复的基础上提供足够的电流而不会损坏。但这种电流需求不应该是持续的。

  • 突发最大电流:这是电机驱动器能够处理的绝对最大电流;超过这个电流将损坏电机驱动器。直流电机在从静止状态启动时可能需要非常高的电流。因此,电机驱动器被设计为处理这些电流。但是这种电流的突发性不应该是重复的,否则会发生加热和随后的损坏。通常,制造商将突发最大电流称为最大电流。

  • 持续最大电流:这是真正的问题;持续最大电流是电机驱动器可以持续处理的最大电流。

  • 供电电压:这是电机驱动器的工作电压——必须将这个电压提供给电机驱动器进行内部工作。

  • 逻辑供电电压:这是提供给电机驱动器的控制信号,可以以 5V、3.3V 和 12V 等不同电压给出。因此,电机驱动器将规定它可以接受信号线上的最大逻辑电压。

现在,让我们看看我们得到了什么。在本书的过程中,我们将使用 L298N 电机驱动器模块,它目前是市场上最常见的电机驱动器模块之一。它有两个通道——您有两个 H 桥,因此可以将两个电机连接到它上。此外,该电机驱动器的规格也相当不错。以下是规格:

  • 电压等级:2.5V 至 46V

  • 重复最大电流:2.5 安培

  • 突发最大电流:3 安培

  • 连续最大电流:2 安培

  • 供电电压:4.5V 至 7V

  • 逻辑供电电压:4.5V 至 7V

一旦您拥有了物理电机驱动器,您将注意到以下引脚:

  • 电机 A:这是电机驱动器的第 1 通道。您可以将第一个电机连接到此端口。

  • 电机 B:这是电机驱动器的第 2 通道。您可以将第二个电机连接到此端口。如果您只有一个电机,可以简单地将此端口未连接。

  • GND:这是您将为电机连接的电源的接地。非常重要的是,您不仅要连接电源的接地,还要将树莓派的接地连接到此端口,以便树莓派和电机驱动器之间的电路完整。

  • VCC:这是电机驱动器的正极端口。这是您的电池或电源适配器的正极端子所在之处。

  • IN 1 和 IN 2:这是我们需要从微控制器提供给电机 A 的两个逻辑输入。每当 IN 1 接收到信号时,H 桥的一部分被激活——电机开始朝一个方向旋转。每当 IN 2 接收到信号时,H 桥的另一部分被激活,使电机朝相反方向旋转。

  • IN 3 和 IN 4:这是电机 B 的逻辑输入,其工作方式与 IN 1 和 IN 2 完全相同。

  • EN A 和 EN B:这些是两个通道的使能引脚。如果这些引脚不高,无论您在输入端口上发出什么信号,相应的通道都不会工作。您可能会注意到 EN 端口上有一个小电容。这被称为分流器。它的作用是使其连接的两个引脚之间接触。当存在于 EN 引脚上时,这意味着只要连接了这个分流器,它就会永久保持高电平。

开始运转

好的,这是很多理论,现在让我们通过树莓派启动其中一个电机。要做到这一点,继续连接电机和电机驱动器如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,一旦您完成了这一点,让我们上传代码并看看会发生什么:

import RPi.GPIO as GPIO
from time import sleep
GPIO.setmode(GPIO.BCM)

Motor1R = 20
Motor1L = 21

GPIO.setup(Motor1R,GPIO.OUT)
GPIO.setup(Motor1L,GPIO.OUT)

GPIO.output(Motor1R,GPIO.HIGH)
GPIO.output(Motor1L,GPIO.LOW)

sleep(5)

GPIO.output(Motor1R,GPIO.LOW)
GPIO.output(Motor1L,GPIO.HIGH)

sleep(5)

GPIO.cleanup()

现在,让我们稍微了解一下代码:

Motor1R = 20
Motor1L = 21

引脚编号20连接到电机驱动器的 IN 1。为了方便起见,我们已将电机 1 的右侧更改为Motor1R;实际上,电机可以以任何方向旋转,但我们只是为了方便和理解而这样写。同样,我们也对Motor1L做了同样的处理。这连接到 IN 2,因此这将导致电机以另一个方向旋转:

GPIO.output(Motor1R,GPIO.HIGH)
GPIO.output(Motor1L,GPIO.LOW)

在这里,我们使Motor1R或引脚编号20高,这意味着输入电机驱动器正在接收的是:

电机 引脚 输入 状态
Motor 1R 树莓派的引脚编号 20 IN 1
Motor 1L 树莓派的引脚编号 21 IN 2

现在,延迟 5 秒后,将运行以下代码,该代码将更改下表中所示的引脚状态:

GPIO.output(Motor1R,GPIO.LOW)
GPIO.output(Motor1L,GPIO.HIGH)
电机 引脚 输入 状态
Motor 1R 树莓派的引脚编号 20 IN 1
Motor 1L 树莓派的引脚编号 21 IN 2

现在,让我们看看一旦我们运行它会发生什么。电机首先会以一个方向旋转,然后会以另一个方向旋转。代码非常简单直接,我认为没有必要解释。我们在这里所做的就是简单地打开或关闭连接到电机驱动器的两个 GPIO 中的一个。一旦激活电机驱动器的 IN 1 输入,H 桥的一部分就会打开,导致电机朝一个方向旋转。每当电机驱动器的 IN 2 输入高时,那么 H 桥的另一部分就会打开,导致电机驱动器输出端的极性发生变化,因此电机朝另一个方向旋转。

改变速度

现在我们已经了解了如何使用电机驱动器改变电机的方向,是时候更进一步,使用电机驱动器控制电机的速度了。要做到这一点,我们实际上不需要做太多。电机驱动器是为了理解 PWM 信号而构建的。一旦向电机驱动器提供 PWM 信号,那么电机驱动器将调整电机的输出电压,从而改变电机驱动器的速度。PWM 必须在电机 A 的相同输入端口 IN 1 和 IN 2 上提供,并在电机 B 的输入端口 IN 3 和 IN 4 上提供。很明显,提供 PWM 的引脚将决定电机的移动方向,而 PWM 的占空比将决定电机旋转的速度。

现在我们已经了解了电机驱动器中的速度控制是如何工作的。现在是时候自己动手了。为此,我们不需要对连接进行任何更改;我们需要做的就是上传以下代码:

import RPi.GPIO as GPIO
from time
import sleep
GPIO.setmode(GPIO.BCM)

Motor1R = 20
Motor1L = 21

GPIO.setup(Motor1R, GPIO.OUT)
GPIO.setup(Motor1L, GPIO.OUT)

pwm = GPIO.PWM(Motor1R, 100)
pwm.start(0)

try:
while True:
  GPIO.output(Motor1L, GPIO.LOW)
for i in range(0, 101):
  pwm.ChangeDutyCycle(i)
sleep(0.1)

except KeyboardInterrupt:

  pwm.stop()
GPIO.cleanup()

你运行这段代码后发生了什么?我肯定电机开始缓慢转动,然后加速,最终达到最高速度,最终停止——这正是我们想要的。如果你记得,这段代码看起来非常熟悉。还记得在第一章中改变 LED 的亮度吗?它几乎是一样的;虽然有一些区别,所以让我们看看它们是什么:

pwm = GPIO.PWM(Motor1R, 100)

在这一行中,我们只是定义了我们需要在上面提供 PWM 的引脚——就是Motor1R,对应的是引脚号20。此外,我们定义了 PWM 的频率为100赫兹或每秒 100 次:

pwm.start(0)

如果你记得,前几章的先前命令pwm.start()主要用于定义信号的占空比。在这里,我们将占空比设置为0,即引脚将关闭:

GPIO.output(Motor1L,GPIO.LOW)

由于我们只在一个特定方向上运行电机,即1R,因此 H 桥的另一半应该关闭。通过上面的代码行,通过将1L置为 LOW 来实现。如果我们不这样做,那么引脚21可能处于任意状态,因此它可能是打开或关闭的。这可能会与电机移动的方向发生冲突,硬件将无法正常工作:

 for i in range(0,101):

现在,真正的问题来了;这一行,for i in range(0,101):将一直运行其中包含的程序,直到i的值在0101之间。它还会在每次循环运行时增加i的值。在这里,每次值都会增加一:

            pwm.ChangeDutyCycle(i)

现在,这是一个稍微新的命令。以前,我们使用了pwm.start(0)来为 PWM 分配占空比。由于我们已经为 PWM 分配了占空比值,要更改它,我们将使用先前提到的命令。占空比将与i的值相同。

因此,每次代码通过for循环时,值或占空比将增加一。非常简单,不是吗?

如果你做对了,机器人学中的一切都很容易。关键是将问题分解成小块并逐个解决;相信我,一旦你做到了,没有什么会让你觉得困难。

总结

在本章中,我们研究了电机的各个方面。接下来,通过使用所有的基础知识,我们将学习蓝牙与移动设备的交互,并构建一个蓝牙控制的机器人汽车。

第二十一章:蓝牙控制的机器人车

我们已经走了很长的路;现在是时候继续前进,做出更好的东西。世界正在为自动驾驶汽车的诞生而疯狂,这将成为新的常态。这些车辆中有很多技术。多个传感器、GPS 和遥测都实时计算,以确保车辆在正确的路线上安全行驶,因此制作一个机器人车辆被证明是学习机器人技术和未来技术的理想方式。在这本书中,我们将尝试制造不仅与现有技术一样好,而且在某些方面甚至更好的技术。所以,让我们继续,一步一步地制作这辆自动驾驶车辆。

本章将涵盖以下主题:

  • 车辆的基础知识

  • 准备车辆

  • 通过蓝牙控制车辆

车辆的基础知识

你一定在想:我们可能还能从这辆车上学到什么呢?这可能是真的,但在开始这一章之前,我们必须确保理解其中的一些内容。所以,让我们开始吧。

首先是我们将使用的底盘:这是一个四轮驱动底盘,所有四个车轮都由专用电机独立控制。因此,我们可以根据需要改变每个车轮的速度。我们选择了四轮驱动传动系统,因为它不容易在地毯和不平整的表面上被卡住。如果你愿意,你也可以选择两轮驱动传动系统,因为这不会有太大的区别。

现在,一旦你组装好底盘,你可能会发现它没有转向机构。这是否意味着车只能直行?显然不是。在制作小型车辆时,有许多方法可以改变车辆的方向。最好的方法被称为差速转向。

在传统汽车中,有一个发动机,这个发动机驱动车轮;因此原则上所有车轮以相同的速度转动。现在当我们直行时这很好用,但每当车要转弯时就会出现一个新问题。参考以下图表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你会看到内侧的车轮直径较小,外侧的车轮直径较大。你可能还记得小学的一个事实:直径越大,周长越大,反之亦然。因此,内侧的车轮在同一时间内将行驶较短的距离,或者简单地说,内侧的车轮会转得更慢,外侧的车轮会转得更快。

这个问题导致了汽车差速器的发现,它是汽车轴的中心有一个圆形的凸起。它的作用是根据转弯半径改变车轮的旋转速度。天才,不是吗?现在,你可能会想:这都没错,但你为什么要告诉我这些?因为我们将做相反的操作来转动机器人。如果我们改变转向圈内外边缘电机的速度,那么车辆将试图向内转向,同样,如果我们对另一端这样做,它将试图向另一个方向转向。在制作轮式机器人时,这种策略并不新鲜。转向机构很复杂,在小型机器人上实现它们只是一个挑战。因此,这是一个更简单和容易的方法来转动你的车辆。

这种方式不仅简单而且非常高效,需要的零部件也很少。车辆的转弯半径也更小。事实上,如果我们以相同速度将车轮的相对侧向相反方向旋转,那么车辆将完全围绕自己的轴旋转,使转弯半径完全为零。这种配置称为履带转向驱动。对于室内使用的轮式机器人来说,这是一个杀手功能。

要了解更多,请阅读这里:groups.csail.mit.edu/drl/courses/cs54-2001s/skidsteer.html

准备车辆

现在是时候继续让机器人车辆成为现实了。所以让我们打开车辆底盘并将每个零件螺丝拧在一起。组装手册通常随套件一起提供,所以你很快就能完成它。

完成组装套件后,继续将每个电机的电线分开。这将是使车辆准备就绪的非常重要的部分。因此,一旦车辆上所有的电线都出来了,拿一个电池,给每个车轮供电。注意连接的极性,车轮是向前旋转的。你需要做的就是拿一个永久性的记号笔或者指甲油,标记电线,当电机向前旋转时,连接到正极的电线。由于所有这些电机完全依赖于极性来确定方向,这一步是关键,以确保无论何时给它们供电,它们总是以相同的方向旋转。相信我,这将为你节省很多麻烦。

现在,一旦这一切都完成了,按照以下图示将电线连接到电机驱动器(红色标记的电线是你之前标记的电线):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

完美!现在一切似乎都准备好了,除了电机驱动器与电源和树莓派的连接。所以让我们看看我们将如何做到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

好了!是时候进行真正的交易了!所以我们要确保的第一件事是所有的连接都按照我们计划的方式工作。为此,我们将从一个简单的代码开始,它将简单地打开所有电机并向前旋转。所以这是代码:

import RPi.GPIO as GPIO
import time GPIO.setmode(GPIO.BCM) Motor1a = 20 Motor1b = 21 Motor2a = 2 Motor2b = 3  GPIO.setup(Motor1a,GPIO.OUT) GPIO.setup(Motor1b,GPIO.OUT) GPIO.setup(Motor2a,GPIO.OUT) GPIO.setup(Motor2b,GPIO.OUT) GPIO.output(Motor1a,1) GPIO.output(Motor1b,0) GPIO.output(Motor2a,1) GPIO.output(Motor2b,0)
time.sleep(10)
GPIO.cleanup() 

程序不可能比这更简单了;我们在这里所做的就是向电机驱动器发出命令,让电机单向旋转。可能会有一组电机会以相反方向旋转的情况,这种情况下你应该改变电机驱动器上的连接极性。这应该解决问题。有些人可能会认为我们也可以对代码进行更改来解决这个问题,但根据我的经验,从那里开始就会变得复杂,并且如果你选择其他路径,会给你带来麻烦。

好了,一切都准备就绪,一切都运行良好。继续尝试一些其他输出排列组合,看看车子会发生什么。别担心,无论你做什么,除非它从屋顶上跑下来,否则你都不会损坏车子!

通过蓝牙控制车辆

玩了一些尝试这些组合的乐趣吗?现在是时候我们继续前进,看看还有什么其他可能性了。我们都玩过遥控车,我相信每个人都会对那些敏捷的小玩具感到开心。我们将做类似的事情,但以一种更复杂的方式。

我们都知道蓝牙:这是与附近设备通信的最佳方式之一。蓝牙通信是一种中等数据速率、低功耗的通信方法。这在移动设备中几乎无处不在,因此是一个理想的开始方式。在本章中,我们将通过蓝牙使用手机来控制车辆。现在让我们看看我们如何做到这一点。

我们想要做的第一件事是将智能手机与机器人车配对,为此我们需要在树莓派上打开终端并执行以下步骤:

  1. 在命令行中输入~ $ bluetoothctl;这是一个蓝牙代理,允许两个蓝牙设备进行通信。没有蓝牙代理,这两个设备首先就无法进行通信。

  2. [Bluetooth] # power on命令简单地启动了树莓上的蓝牙。

  3. [Bluetooth] # agent on命令启动代理,然后可以为我们启动连接。

  4. [Bluetooth] # discoverable on命令使树莓派的蓝牙可发现。蓝牙可能已经打开,但我们必须使其可发现,以确保其他设备可以找到它并连接到它。

  5. [Bluetooth] # pairable on命令使设备可配对。如果蓝牙已打开,这并不意味着您的设备将能够连接,因此我们需要使其可配对,这个命令正是这样做的。

  6. [Bluetooth] # scan on命令开始扫描附近的蓝牙设备。这个命令的输出将是一些 MAC 地址以及蓝牙名称。MAC 地址是设备的物理地址;这是一个唯一的地址,因此它永远不会对两个设备相同。

  7. [Bluetooth] # pair 94:65:2D:94:9B:D3命令帮助您与您想要的设备配对。您只需输入带有 MAC 地址的命令。

只是为了明确,这是您的屏幕应该看起来的样子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

完成了这个过程后,您应该能够将树莓派连接到您的移动设备。现在您已经连接,是时候继续编写代码了,通过这些代码,我们将能够仅使用移动设备来控制蓝牙汽车。所以这是代码。继续,看一看,然后我们将进行解释:

import bluetooth
import time
import RPi.GPIO as GPIO
Motor1a = 20
Motor1b = 21
Motor2a = 2
Motor2b = 3
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(Motor1a,GPIO.OUT)
GPIO.setup(Motor1b,GPIO.OUT)
GPIO.setup(Motor2a,GPIO.OUT)
GPIO.setup(Motor2b,GPIO.OUT)
server_socket=bluetooth.BluetoothSocket( bluetooth.RFCOMM )
port = 1
server_socket.bind(("",port))
server_socket.listen(1)
client_socket,address = server_socket.accept()
print ("Accepted connection from "+str(address))
def stop_car():
  GPIO.output(Motor1a,0)
  GPIO.output(Motor1b,0)
  GPIO.output(Motor2a,0)
  GPIO.output(Motor2b,0)

while True:
  data = client_socket.recv(1024)
  if (data == "B" or data== "b"):
    GPIO.output(Motor1a,1)
    GPIO.output(Motor1b,0)
    GPIO.output(Motor2a,1)
    GPIO.output(Motor2b,0)
    time.sleep(1)
    stop_car()

  if (data == "F" or data == "f"):
    GPIO.output(Motor1a,0)
    GPIO.output(Motor1b,1)
    GPIO.output(Motor2a,0)
    GPIO.output(Motor2b,1)
    time.sleep(1)
    stop_car()

  if (data == "R" or data == "r"):
    GPIO.output(Motor1a,0)
    GPIO.output(Motor1b,1)
    GPIO.output(Motor2a,1)
    GPIO.output(Motor2b,0)
    time.sleep(1)
    stop_car()

  if (data == "L" or data == "l"):
    GPIO.output(Motor1a,1)
    GPIO.output(Motor1b,0)
    GPIO.output(Motor2a,0)
    GPIO.output(Motor2b,1)
    time.sleep(1)
    stop_car()

  if (data == "Q" or data =="q"):
    stop_car()

  if (data =='Z' or data == "z"):
    client_socket.close()
    server_socket.close() 

现在让我们看看这段代码实际在做什么:

import bluetooth

在这个程序中,我们将使用蓝牙的一些通用功能,因此我们调用bluetooth库,以便能够调用这些方法:

server_socket=bluetooth.BluetoothSocket( bluetooth.RFCOMM )

现在,每当我们连接两个蓝牙设备时,我们有各种通信方法;其中最简单的是无线电频率通信,这里称为RFCOMM。现在,在这一行中,我们使用bluetooth库的BluetoothSocket方法来定义我们在程序中使用的通信协议,现在你已经知道是RFCOMM。我们进一步将这些数据存储在一个名为server_socket的变量中,这样我们就不必一遍又一遍地重复这个步骤。而是,每当我们需要这些数据时,它将已经存储在名为server_socket的变量中:

port = 1

现在,蓝牙有多个端口;这是一个非常有用的概念,通过一个单一的蓝牙连接,我们可以将各种数据流传输到各种设备和程序。这避免了数据的冲突,并确保数据安全地传输到确切的接收者。我们现在使用的程序非常简单,我们不需要多个端口进行数据通信。因此,我们可以使用任何160个可用端口进行通信。在程序的这一部分,您可以写任何端口,您的程序将正常运行:

server_socket.bind(("",port))

现在,每当我们连接两个设备时,我们需要确保它们在整个通信过程中保持连接。因此,在这里我们写下这个命令:server_socket.bind。这将确保您的蓝牙连接在整个通信过程中保持连接。

正如您所看到的,参数中的第一个参数是空的。在这里,我们通常写下它必须绑定的 MAC 地址。然而,由于我们将其设置为空,它将自动绑定到我们已经配对的 MAC 地址。我们的第二个参数是它必须连接的端口。正如我们所知,port变量的值被设置为1。因此,它将自动连接到端口号1

server_socket.listen(1)

这是一条非常有趣的线。正如我们所知,我们可能不是唯一一个尝试连接到树莓的蓝牙设备的人,因此当树莓接收到另一个连接请求时,树莓应该怎么做呢?

在这一行中,我们只是在定义:我们正在调用一个名为listen(1)的方法。在这个函数中,我们已经将参数的值定义为1。这意味着它只会连接到一个设备。任何其他尝试连接的设备都无法通过。如果我们将这个参数改为2,那么它将连接到两个设备,但它会留在队列中,因此被称为队列连接

client_socket,address = server_socket.accept()

现在大部分连接的事情都已经完成,我们还需要知道我们是否连接到了正确的地址。server_socket.accept()方法的作用是返回套接字号和它正在服务的地址。因此,我们将其存储在两个名为client_socketaddress的变量中。然而,正如我们所知,套接字将仅保持为1,因此我们将不会再使用它:

print ("Accepted connection from "+str(address))

在这一行中,我们只是告诉用户连接已成功建立,通过使用str(address)函数,我们打印连接到的地址的值。这样我们可以确保连接已经建立到了正确的设备。

data = client_socket.recv(1024)

在这一行中,我们正在从客户端接收数据;同时,我们正在定义数据的长度。因此,在方法client_socket.recv(1024)中,我们在参数中传递了一个参数1024,这基本上表示数据包的最大长度为1024字节。一旦接收到数据,它就会传递给变量data供进一步使用。

在此之后,程序的其余部分非常简单。我们只需要比较移动设备接收到的值,并让汽车做我们想做的事情。在这里,我们让汽车向四个方向行驶,即前进、后退、右转和左转。您也可以根据自己的需求添加特定条件:

 client_socket.close()

在这一行中,我们正在关闭客户端套接字的连接,以便断开客户端并终止数据传输:

 server_socket.close()

在前一行中,我们正在关闭服务器套接字的连接,以便断开服务器连接。

总结

本章教会了我们如何使用蓝牙接口通过数据抓取和共享来自动化和控制汽车。接下来,我们将开发我们迄今为止所学到的内容,以便为避障和路径规划接口红外传感器。

第二十二章:障碍物避让的传感器接口

要制作一个能自行驾驶的机器人车,我们首先需要了解人类如何驾驶车辆。当我们开车时,我们不断分析空间和与其他物体的距离。然后,我们决定是否可以通过。这在我们的大脑-眼睛协调中不断发生。同样,机器人也需要做同样的事情。

在我们之前的章节中,你学到了我们可以使用传感器找到我们周围物体的接近程度。这些传感器可以告诉我们物体有多远,基于此,我们可以做出决定。我们之前使用超声波传感器主要是因为它非常便宜。然而,正如你记得的,附加超声波传感器并运行其代码稍微麻烦。现在是时候我们使用一个更简单的传感器并将其连接到汽车上了。

本章将涵盖以下主题:

  • 红外近距离传感器

  • 自主紧急制动

  • 赋予它自动转向能力

  • 使其完全自主

红外近距离传感器

以下照片描述了红外近距离传感器:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

它由两个主要部分组成-传感器和发射器。发射器发射红外波;这些红外(IR)波然后击中物体并返回到传感器,如下图所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,正如你在前面的图表中所看到的,发射的红外波从与传感器不同距离的表面反弹回来,然后它们以一定角度接近传感器。现在,因为发射器和传感器之间的距离在任何时间点都是固定的,所以对应于反射的红外波的角度将与其反弹之前所走过的距离成比例。红外近距离传感器中有超精密传感器,能够感知红外波接近它的角度。通过这个角度,它给用户一个相应的距离值。这种找到距离的方法被称为三角测量,它在工业中被广泛使用。我们需要记住的另一件事是,正如我们在前面的章节中提到的,我们都被红外辐射所包围;任何绝对零度以上的物体都会发射相应的波。此外,我们周围的阳光也有大量的红外辐射。因此,这些传感器具有内置电路来补偿它;然而,它只能做到这么多。这就是为什么在处理直射阳光时,这个解决方案可能会有些麻烦。

现在,理论够了,让我们看看汽车实际上是如何工作的。我们在这个例子中使用的 IR 近距离传感器是夏普的模拟传感器,部件代码为 GP2D12。它的有效感应范围为 1000-800 毫米。范围还取决于所询问对象表面的反射性。物体越暗,范围越短。这个传感器有三个引脚。正如你可能已经猜到的,一个是 VCC,另一个是地,最后一个是信号。这是一个模拟传感器;因此,距离读数将基于电压给出。通常,大多数模拟传感器都会得到一个图表,其中会描述各种感应范围的各种电压。输出基本上取决于传感器的内部硬件和其结构,因此可能大不相同。下面是我们的传感器及其输出的图表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

好吧,到目前为止一切都很好。正如我们所知,树莓派不接受模拟输入;因此,我们将继续使用我们之前使用过的 ADC。我们将使用之前使用过的相同 ADC。

自主紧急制动

有一种新技术,新车配备了这种技术。它被称为自动紧急制动;无论我们在驾驶时有多认真,我们都会分心,比如 Facebook 或 WhatsApp 的通知,这些会诱使我们从道路上的屏幕上看向手机。这可能是道路事故的主要原因;因此,汽车制造商正在使用自动制动技术。这通常依赖于远程和近程雷达,它检测车辆周围其他物体的接近,在即将发生碰撞的情况下,自动将车辆刹车,防止它们与其他车辆或行人相撞。这是一个非常酷的技术,但有趣的是,我们今天将亲手制作它。

为了实现这一点,我们将使用红外接近传感器来感知周围物体的接近。现在,继续,拿一张双面胶带,把红外距离传感器粘在车子的前面。一旦完成这一步,按照这里所示的连接电路。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

好了,我们已经准备好编写代码了。以下是代码,只需将其复制到你的树莓派上:

import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM)  import Adafruit_ADS1x15 adc0 = Adafruit_ADS1x15.ADS1115()   GAIN = 1  adc0.start_adc(0, gain=GAIN)   Motor1a = 20 Motor1b = 21 Motor2b = 23
Motor2a = 24  GPIO.setup(Motor1a,GPIO.OUT) GPIO.setup(Motor1b,GPIO.OUT) GPIO.setup(Motor2a,GPIO.OUT) GPIO.setup(Motor2b,GPIO.OUT)  def forward(): GPIO.output(Motor1a,0) GPIO.output(Motor1b,1) GPIO.output(Motor2a,0) GPIO.output(Motor2b,1)    def stop():  GPIO.output(Motor1a,0) GPIO.output(Motor1b,0) GPIO.output(Motor2a,0) GPIO.output(Motor2b,0) while True:  F_value = adc0.get_last_result()  F = (1.0  / (F_value /  13.15)) -  0.35  forward()  min_dist = 20  if F < min_dist:  stop()  

现在,让我们看看这段代码实际上发生了什么。一切都非常基础;红外线接近传感器感知到其前方物体的接近,并以模拟信号的形式给出相应的距离值。然后这些信号被 ADC 获取,并转换为数字值。这些数字值最终通过 I2C 协议传输到树莓派上。

到目前为止,一切都很好。但你一定想知道这行代码是做什么的?

 F = (1.0  / (F_value /  13.15)) -  0.35

这里我们并没有做太多事情,我们只是获取 ADC 给出的数字值,然后使用这个公式,将数字值转换为以厘米为单位的可理解的距离值。这个计算是由制造商提供的,我们不需要深究这个。大多数传感器都提供了这些计算。然而,如果你想了解我们为什么使用这个公式,我建议你查看传感器的数据表。数据表可以在以下链接上轻松找到:engineering.purdue.edu/ME588/SpecSheets/sharp_gp2d12.pdf

接下来,代码的主要部分如下:

min_dist = 20 If F < min_dist:
 stop()

这也很简单。我们输入了一个距离值,在这个程序中,我们将其设置为20。所以,每当F的值(红外接近传感器获取的距离)小于20时,就会调用stop()函数。stop函数只是让车子停下来,防止它与任何东西相撞。

让我们上传代码,看看它是否真的有效!确保你在室内测试这辆车;否则,如果没有障碍物,你将很难停下这辆车。玩得开心!

给车子自动转向的能力

希望你对这个小东西玩得开心。传感器的应用是如此简单,但它可以产生如此大的影响。既然你已经学会了基础知识,现在是时候向前迈进,给车子一些更多的能力了。

在之前的代码中,我们只是让机器人停在障碍物前面,为什么我们不让它绕过车子呢?这将非常简单又非常有趣。我们只需要调整stop()函数,使其能够转向。显然,我们还将把函数的名称从stop()改为turn(),只是为了清晰起见。要记住的一件事是,你不需要重写代码;我们只需要做一些微小的调整。所以,让我们看看代码,然后我会告诉你到底发生了什么变化以及为什么:

import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM)  import Adafruit_ADS1x15 adc0 = Adafruit_ADS1x15.ADS1115()   GAIN = 1  adc0.start_adc(0, gain=GAIN)   Motor1a = 20 Motor1b = 21 Motor2a = 23 Motor2b = 24  GPIO.setup(Motor1a,GPIO.OUT) GPIO.setup(Motor1b,GPIO.OUT) GPIO.setup(Motor2a,GPIO.OUT) GPIO.setup(Motor2b,GPIO.OUT)  def forward(): GPIO.output(Motor1a,0) GPIO.output(Motor1b,1) GPIO.output(Motor2a,0) GPIO.output(Motor2b,1)   def turn():
 GPIO.output(Motor1a,0) GPIO.output(Motor1b,1) GPIO.output(Motor2a,1) GPIO.output(Motor2b,0) )  while True:
   forward() F_value = adc0.get_last_result()  F = (1.0  / (F_value /  13.15)) -  0.35
     min_dist = 20

 while F < min_dist: turn()  

你可能已经注意到,除了以下内容,其他都基本保持不变:

def turn():
 GPIO.output(Motor1a,0) GPIO.output(Motor1b,1) GPIO.output(Motor2a,1) GPIO.output(Motor2b,0)

这部分代码定义了“转向()”函数,在这个函数中,车辆的对侧车轮会以相反的方向旋转;因此,使车辆绕着自己的轴转动:

 min_dist = 20 while F < min_dist: turn()

现在这是程序的主要部分;在这部分中,我们正在定义汽车在遇到任何障碍物时会做什么。在我们之前的程序中,我们主要是告诉机器人一旦遇到障碍物就停下来;然而,现在我们正在将“停止”函数与“转向”函数链接起来,这两个函数我们之前在程序中已经定义过了。

我们只是放入了一个条件,如下所示:

min_dist = 20 If F < min_dist:
 turn()

然后,它会转动一小段时间,因为微控制器会解析代码并执行它,然后跳出条件。为了做到这一点,我们的树莓派可能只需要几微秒。所以,我们甚至可能看不到发生了什么。因此,在我们的程序中,我们使用了一个while循环。这基本上保持循环运行,直到条件满足为止。我们的条件是while F < min_dist:,所以只要机器人在前面检测到物体,它就会继续执行其中的函数,而在我们的情况下,就是“转向()”函数。简而言之,直到它没有转到足够的程度来避开障碍物为止,车辆将继续转向,然后一旦循环执行完毕,它将再次跳回到主程序并继续直行。

简单吧?这就是编程的美妙之处!

使其完全自主

现在,你一定已经了解了使用简单的接近传感器进行自动驾驶的基础知识。现在是我们使其完全自主的时候了。要使其完全自主,我们必须了解并映射我们的环境,而不仅仅是在车辆遇到障碍物时转向。我们基本上需要将整个活动分为以下两个基本部分:

  • 扫描环境

  • 决定如何处理感知到的数据

现在,让我们先编写代码,然后看看我们需要做什么:

import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) import Adafruit_ADS1x15
adc0 = Adafruit_ADS1x15.ADS1115() GAIN = 1 adc0.start_adc(0, gain=GAIN) Motor1a = 20 Motor1b = 21 Motor2a = 23 Motor2b = 24 GPIO.setup(Motor1a,GPIO.OUT) GPIO.setup(Motor1b,GPIO.OUT) GPIO.setup(Motor2a,GPIO.OUT) GPIO.setup(Motor2b,GPIO.OUT)  def forward(): GPIO.output(Motor1a,0) GPIO.output(Motor1b,1) GPIO.output(Motor2a,0) GPIO.output(Motor2b,1)  def right(): GPIO.output(Motor1a,0) GPIO.output(Motor1b,1) GPIO.output(Motor2a,1) GPIO.output(Motor2b,0)  def left(): GPIO.output(Motor1a,1) GPIO.output(Motor1b,0) GPIO.output(Motor2a,0) GPIO.output(Motor2b,1)  def stop(): GPIO.output(Motor1a,0) GPIO.output(Motor1b,0) GPIO.output(Motor2a,0) GPIO.output(Motor2b,0)  while True:  forward()  F_value = adc0.get_last_result() F = (1.0  / (F_value /  13.15)) -  0.35  min_dist = 20 if F< min_dist: stop() right() time.sleep(1) F_value = adc0.get_last_result()  F = (1.0  / (F_value /  13.15)) -  0.35  R = F left() time.sleep(2)  F_value = adc0.get_last_result()   F = (1.0  / (F_value /  13.15)) -  0.3  L = F if L < R: right()
        time.sleep(2) else: forward()  

现在大部分程序就像我们之前的所有程序一样;在这个程序中,我们定义了以下函数:

  • “前进()”

  • “右()”

  • “左()”

  • “停止()”

关于定义函数,我没有太多需要告诉你的,所以让我们继续前进,看看我们还有什么。

主要的操作是在我们的无限循环while True:中进行的。让我们看看到底发生了什么:

while True:

 forward() F_value = adc0.get_last_result() F = (1.0  / (F_value /  13.15)) -  0.35

 min_dist = 20 if F< min_dist: stop()

让我们看看这部分代码在做什么:

  • 一旦我们的程序进入无限循环,首先执行的是“前进()”函数;也就是说,一旦无限循环执行,车辆就会开始向前行驶。

  • 此后,F_value = adc.get_last_result()正在从 ADC 中获取读数并将其存储在一个名为F_value的变量中

  • F = (1.0/(F-value/13.15))-0.35正在计算可理解的度量距离值

  • min_dist = 20,我们只是定义了稍后将使用的最小距离

一旦这部分代码完成,那么if语句将检查是否F < min_dist:。如果是这样,那么if语句下的代码将开始执行。这部分代码的第一行将是“停止()”函数。所以每当车辆在前面遇到障碍物时,它将首先停下来。

现在,正如我所提到的,我们代码的第一部分是了解环境,所以让我们继续看看我们是如何做到的:

right()
 time.sleep(1) F_value = adc0.get_last_result()  F = (1.0  / (F_value /  13.15)) -  0.35
 R = F left() time.sleep(2)  F_value = adc0.get_last_result()
  F = (1.0  / (F_value /  13.15)) -  0.35
 L = F 

车辆停下后,它将立即向右转。正如你所看到的,代码的下一行是time.sleep(1),所以在另外的1秒钟内,车辆将继续向右转。我们随机选择了1秒的时间,你可以稍后调整它。

一旦它向右转,它将再次从接近传感器中获取读数,并使用这段代码R=F,我们将这个值存储在一个名为R的变量中。

在这样做之后,车辆将转向另一侧,也就是向左侧,使用left()函数,并且它将持续向左转动2秒,因为我们有time.sleep(2)。这将使车辆转向障碍物的左侧。一旦它向左转,它将再次接收接近传感器的值,并使用代码L = F将该值存储在变量L中。

所以,我们所做的实质上是扫描我们周围的区域。在中心,有一个障碍物。它将首先向右转,并获取右侧的距离值;然后,我们将向左转并获取左侧的距离值。因此,我们基本上知道了障碍物周围的环境。

现在我们来到了必须做出决定的部分,即我们必须向前走的方向。让我们看看我们将如何做到:

 if L < R: right()
        time.sleep(2) else: forward()

使用if语句,我们通过这段代码if L < R:比较障碍物左右侧的接近传感器的值。如果L小于R,那么车辆将向右转动2秒。如果条件不成立,那么else:语句将生效,车辆将前进。

现在,如果我们从更大的角度看代码,以下事情正在发生:

  • 车辆会一直前进,直到遇到障碍物

  • 遇到障碍时,机器人会停下来

  • 它将首先向右转,并测量其前方物体的距离

  • 然后,它将向左转,并测量其前方物体的距离

  • 之后,它将比较左右两侧的距离,并选择它需要前进的方向

  • 如果它需要向右转,它将向右转,然后前进

  • 如果它需要向左转,那么它已经处于左转方向,所以它只需要直走

让我们上传代码,看看事情是否按计划进行。请记住,尽管每个环境都不同,每辆车也不同,所以你可能需要调整代码以使其顺利运行。

现在我给你留下一个问题。如果在两种情况下传感器的读数都是无穷大或者它能给出的最大可能值,那么机器人会怎么做?

继续,进行一些头脑风暴,看看我们能做些什么来解决这个问题!

总结

在本章中,利用你迄今为止学到的所有基础知识,以及引入红外接近传感器,我们能够更进一步地发展我们的机器人车,以便检测障碍物并相应地改变方向。在下一章中,我们将学习如何制作我们自己的区域扫描仪——到时见!

第二十三章:现在,假设在 0 度的位置,电位器的输出电压为 4.8V;当我们将它旋转到 90 度时,值会变为大约 3.2V,当完全旋转到 180 度时,由于电阻的改变,电压会降至仅有 2V。

电机是令人惊奇的东西;它们有各种各样的形状和大小。主要上,它们可以被认为是大多数机器人的支撑。然而,在这个世界上没有什么是完美的。这些电机肯定也有一些缺点。到现在为止,你可能已经自己发现了一些。在上一章中,当我们让车子转弯时,你可能已经注意到转弯的角度从来不是完全相同的。同样,当车辆被命令直行时,它实际上并不会这样做。相反,它会试图向一侧轻微偏离。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但是当我们谈论机器人时,即使 1 度的精度可能还不够。如今的机器人学家期待的精度在两位小数的数量级内。因此,我们所说的精度接近 0.01 度。你现在怎么想?我们如何用电机实现这种精度水平呢?

激光雷达

  • 伺服电机

  • 现在我们有的是一个通过多个减速齿轮与电位器耦合的电机,它将减慢电机的速度并增加扭矩。在最终齿轮处,轴向外安装到机身上并与电位器耦合。列表

  • 现在,让我们把它放在一个有趣的组合中:

伺服电机

当电阻器的值改变时,电阻器的输出电压也会改变。有趣的是,如果电位器的输入电压是已知的,那么它的输出电压可以用来推断轴的位置。让我们看看如何做到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

而不用真正看电位器的轴,我们可以很容易地推断出,如果电阻器的输出电压为 4.8V,那么轴必须处于 0 度的位置。同样,我们可以说,如果电压为 3.2V,那么它就处于 90 度的位置,当电压为 2V 时,它就处于 180 度的位置。

制作你自己的区域扫描仪

所以,让我向你介绍伺服电机。伺服电机基本上是一个带有一些附加组件的电机。现在,要理解这些附加组件是什么,让我们先通过这个例子来看一下。假设你想去伦敦。现在,要了解你如何去那里以及到达伦敦的路线,你首先需要知道的是你现在的确切位置。如果你不知道你目前在哪里,就不可能计算出一条路线。同样,如果我们想要到达电机的某个位置,我们需要知道电机的轴现在所处的位置。为了做到这一点,我们使用一个电位器。电位器基本上是一个变阻器,它有一个轴,当旋转时会改变电阻的值。一个变阻器看起来像这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里,我们只是绘制了三个点,但对于电位器上的任何给定点,都会有一个非常特定的电阻与之对应。通过这个,我们可以精确计算出电位器的轴会在哪里。

通过以下主题,本章将回答所有这些问题:

向第一个问题打个招呼——精度。控制这些电机非常简单,但当我们必须将电机旋转到特定角度时,这些电机的问题就出现了。如果你需要将你的机器人车的电机只旋转 90 度,那么你该怎么做呢?你脑海中可能首先想到的是调整电机的时间。在这一点上你可能是对的。但是,要确保每次都能旋转到确切的 90 度是不可能的。

因此,正如您所了解的,电位器将能够感知输出轴指向的角度。然后,电位器连接到一个控制电路,该电路从电位器中读取数据,并进一步指导电机移动多少才能达到目标位置。由于这种闭环安排中控制电路知道轴在哪里,它可以计算需要移动电机多少才能达到目标位置。因此,这种安排能够将输出轴精确地转到任何给定的位置。

这种安排通常被称为舵机。在机器人行业中,这是控制精确运动最广泛使用的硬件之一。基本上,有三根导线进入控制电路——VCC、地线和信号。信号线将接收来自我们的树莓派的数据,并在接收后,它将进行必要的电机运动,使轴达到所需的位置。舵机的图像如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这些可以从非常便宜的价格开始,大约 4 到 5 美元,但它们的价格可以上升到数千美元。但是是什么决定了这些舵机的定价呢?在选择舵机时,有几个因素需要记住,但其中最重要的是扭矩

扭矩基本上是电机可以转动输出轴的转动力。通常以千克·厘米或牛顿·米来衡量。那这实际上是什么意思呢?让我们看下面的图表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

假设在前面的图表中,我们有一个扭矩为 10 千克·厘米的电机,附在上面的转子是 1 厘米。因此,它应该能够垂直从地面上拉起 10 千克的重量。然而,当我们将转子的半径改为 2 厘米时,可以被提起的重量减半。同样,如果半径增加到 10 厘米,那么可以被提起的重量只会减少到 1 千克。因此,基本上可以提起的重量将是扭矩/半径。

但是对于我们大多数的目的,我们不会使用之前显示的机制,所以让我们看下一个图表,看看如何进行计算:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,假设我们有一个长度为L的轴和轴的极端边上的负载。为了方便计算,我们将轴的重量视为可以忽略不计。现在,如果舵机的扭矩为 100 千克·厘米,轴的长度(L)为 10 厘米,那么通过简单的计算,我们可以提起的负载将是 100/10 = 10 千克。同样,如果长度增加到 100 厘米,可以提起的负载将减少到仅为 1 千克。

好了,我们已经对舵机有了相当多的了解。现在的问题是我们如何控制舵机?正如我提到的,有不同类型的舵机可供选择,可以通过各种方式进行控制。然而,用于业余用途最常用的是数字舵机。这些舵机需要PWM,根据 PWM 的占空比,轴的角度会改变。因此,让我们看看它是如何发生的。

通常,大多数舵机的频率为 50 赫兹。因此,每个脉冲的长度通常为 1/50 = 0.02 秒,换句话说就是 20 毫秒。此外,可以给这些舵机的占空比可以是 2.5%到 12.5%,这基本上意味着脉冲宽度为 0.5 毫秒到 2.5 毫秒。现在让我们看看它是如何工作的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

正如你所看到的,当给定一个 2.5%的占空比时,轴会下降到最小位置 0 度,当占空比增加到 7.5%时,轴会到达中间位置 90 度。最后,当占空比增加到 12.5%时,轴会到达最大位置 180 度。如果你想要中间的任何位置,你可以简单地选择相应的 PWM,它会改变舵机的位置到所需的角度。

但是你可能会想,如果我们想要超过 180 度怎么办?好问题,但是大多数数字舵机只能旋转 180 度。有些舵机可以完全旋转其轴,即 360 度;然而,它们的寻址略有不同。在本章之后,你基本上可以查看任何数字舵机的数据表,并按照自己的方式进行控制。

好了,理论够了;是时候做一些有趣的事情了。所以,让我们继续设置硬件,并用我们的双手控制一个舵机!将舵机连接到树莓派如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

电线的颜色编码如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,我们需要上传以下代码并看看会发生什么:

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)

pwm = GPIO.PWM(14, 50)
pwm.start(0)

while 1:

        pwm.ChangeDutyCycle(2.5)
        time.sleep(2)

        pwm.ChangeDutyCycle(5)
        time.sleep(2)

        pwm.ChangeDutyCycle(7.5)
        time.sleep(2)

        pwm.ChangeDutyCycle(10)
        time.sleep(2)

        pwm.ChangeDutyCycle(12.5)
        time.sleep(2)

当你运行这个程序时,你会看到舵机的轴从左到右移动,分别在 0 度、45 度、90 度、135 度和最后 180 度位置上停下。

让我们看看我们在程序中做了什么来实现它:

pwm = GPIO.PWM(14, 50)
pwm.start(0)

通过pwm = GPIO.PWM(14, 50)这一行,我们已经定义了 GPIO 引脚号 14 将用于 PWM,PWM 的频率将为 50。我们在之前的章节中也使用了pwm.start(0)这一行。它基本上将 PWM 引脚设置为 0,即没有占空比:

        pwm.ChangeDutyCycle(2.5)
        time.sleep(2)

        pwm.ChangeDutyCycle(5)
        time.sleep(2)

        pwm.ChangeDutyCycle(7.5)
        time.sleep(2)

        pwm.ChangeDutyCycle(10)
        time.sleep(2)

        pwm.ChangeDutyCycle(12.5)
        time.sleep(2)

现在所有之前的程序都在while循环中,也就是说,它将一遍又一遍地执行,直到程序被强制退出。现在,pwm.ChangeDutyCycle(2.5)这一行发送了一个 2.5%的 PWM 占空比给舵机。这将简单地将舵机转到 0 度角。接下来,我们使用老式的time.sleep(2),我们都知道这会使程序暂停两秒。

相同的循环正在重复,使用不同的 PWM 值,5%的 PWM 会将轴转到 45 度,7.5%的 PWM 会转到 90 度,10%的 PWM 会转到 135 度,12.5%的 PWM 会转到 180 度。这是一个非常简单的程序,可以清楚地了解舵机的基础知识。

到目前为止,你已经学会了如何控制舵机并使其朝我们想要的方向移动。现在,让我们再进一步,稍微改变代码使舵机平稳运行:

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)

pwm = GPIO.PWM(14, 50)
pwm.start(0)

i=2.5
j=12.5

while 1:
        while i<=12.5:
                pwm.ChangeDutyCycle(i)
                time.sleep(0.1)
                i = i + 0.1

        while j>=2.5:
                pwm.ChangeDutyCycle(j)
                time.sleep(0.1)
                j = j - 0.1

当你在你的树莓派上上传了这段代码时发生了什么?你会注意到舵机非常平稳地从左到右刷过去,然后从右到左。我们做了一个非常简单的技巧;让我们看看是什么:

        while i<=12.5:
                pwm.ChangeDutyCycle(i)
                time.sleep(0.1)
                i = i + 0.1

在这里,我们正在运行一个循环,直到值i<=12.5,因为我们在程序的开头已经定义了值i的默认值为2.5。之后,每次代码运行时,占空比被设置为i的值,程序暂停 0.1 秒,然后i的值增加 0.1。这增加了 PWM 的占空比。一旦值达到 12.5,循环退出。

我们的整个 PWM 范围是 2.5%到 12.5%,所以我们有 10%的空间可以使用。现在,如果我们将其映射到舵机的角度旋转,那么每个百分比的 PWM 对应于 180/10 = 18 度的变化。同样,每 0.1%的变化将导致 180/100 = 1.8 度的变化。因此,每 0.1 秒,我们增加 0.1%的占空比,换句话说,我们增加 1.8 度的角度。因此,我们发现这个动作非常平滑。

在程序的下一部分中,我们正在做类似的事情;然而,我们是为了反向运动。

列表

好的,那么,我们非常确定如何使用伺服,并根据我们的需求进行控制运动。现在是时候继续前进,了解我们将大量使用的另一个概念。它被称为数组。如果你在任何其他语言中编程过,你一定很熟悉它。但我们需要了解一些基本概念,这将使我们的生活变得更加轻松。所以,让我们开始吧。

首先,首先。在 Python 中,数组不被称为数组,而是被称为列表。列表基本上是一种可以同时存储多个元素的数据结构。唯一的限制是元素必须是相同的数据类型。比如,如果你存储整数,那么所有的值都应该是int。同样,如果你存储一个字符,那么列表的每个元素都应该是char。要定义一个列表,你所需要做的就是给列表命名,比如我们通过myList所做的那样;列表的名称可以是任何东西,接下来我们需要告诉编译器它实际上是一个列表。为了做到这一点,我们需要将值放在方括号内。它看起来像这样:

myList = [14,35,108,64,9]

需要记住的一件事是,每个值都应该用逗号分隔。每当我们想要访问列表的任何单个元素时,我们可以通过调用它们的索引号来简单地使用它们。这是基于元素在列表中的位置。Python 列表中的索引值从 0 开始。所以根据前面的声明,在索引 0 处,值将是14,在地址 4 处,值将是9。现在,当我们需要在程序中间打印这些元素时,我们需要写下以下代码:

print myList[2] 

一旦我们写了这个,程序将打印列表中第二个值的值。在我们的例子中,它将是35

现在,这是访问列表元素的一种方式;我们也可以以相反的顺序访问它。所以,假设你想访问数组的最后一个项目。然后,我们可以编写以下代码:

print myList[-1] 

这段代码将返回数组的最后一个元素的值。现在,每当我们在列表中使用负值时,它将以相反的顺序开始索引。所以,假设我们输入print myList[-2],这将给我们数组中倒数第二个值的值。在整个方案中需要记住的一件事是,编号将从 0 开始,而当我们以相反的顺序开始时,编号将从-1 开始。

Python 真的很有趣,如果你知道正确的工具,它也很简单。Python 的开发人员包含了一些非常有用的函数,可以在列表上使用。所以,让我们去探索一下它们。

第一个是向数组添加元素。为此,我们使用一个名为append()的函数。append()函数的作用是在数组的末尾添加你想要的值。所以,写下以下内容:

myList.append(45)

这样做的效果是在myList的末尾添加元素45。所以现在列表将如下所示:

myList = [14,35,108,64,9, 45]

简单,不是吗?但是如果你想在列表中间添加一个元素怎么办?显然,开发人员不会让你措手不及。他们也包含了一个名为insert(index, element)的函数;现在每当你使用这个函数时,你需要确保提到你想要这个元素的索引位置,其次是你想要放置的元素。所以它看起来像这样:

myList.insert(3,23)

当你使用这个函数时,数组将如下所示:

myList = [14,35,108,23,64,9,45]

显然,当开发人员给出添加元素的功能时,他们肯定也会给出删除元素的功能。但诀窍在于你可以用两种方法做到这一点。首先,是常见的方法。我们只需选择索引号并删除它。我们现在要做的就是这样:

del myList[2]

现在这样做的效果是删除数组的第二个元素,所以在执行此操作后,数组将如下所示:

myList = [14,35,108,64,9,45]

但现在真正的技巧来了;你也可以通过简单地指定元素来删除元素。就是这样做的:

myList.remove(9)

一旦你这样做了,它会找到列表中元素9的位置并将其从中删除。所以你不必关心元素在哪里;这个函数会说,我会找到你并杀死你!

四处张望

好了,够了电影台词。我们可以讨论我们可以在列表上使用的许多其他功能,但我们现在所做的已经足够了。我们将根据需要看到其余的功能。但现在让我们在机器人技术上迈出一步。你可能已经在许多自动驾驶汽车的顶部看到旋转的物体。生产车辆通常不会有这种装置,主要是因为价格昂贵,但研究用途的车辆总是装备有它。

那么这个装置是什么?它被称为激光雷达;这是光探测和测距的缩写。我知道这是个糟糕的缩写。激光雷达之所以如此普遍,是有原因的。它以非常精确的方式给出了周围区域的距离读数。然而,为我们的项目购买它可能有点杀鸡用牛刀,因为一个好的激光雷达可能会花费你接近 500 到 10,000 美元。如果你仍然认为这在你的预算范围内,那么你会非常幸运!但对于那些不想购买它的人,我有一个好消息。今天,我们将建立我们自己的激光雷达扫描仪。因此,为了制作一个区域扫描仪,我们需要一个伺服电机,我们将在其上安装我们的红外近距传感器。现在要做到这一点,我们需要一个轻微的临时安排。你可以拿一块硬纸板,像我们在这里的图片中所做的那样固定它,或者你也可以使用一个直角铝材并钻孔来固定组件,如果你想要以专业的方式来做。要记住的一件事是,传感器必须正好平行于地面,而不是向上或向下。

安装完成后,就是连接其余的硬件的时候了。所以继续连接硬件,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

好的,让我们看看这个东西能做什么,准备好,上传这段代码:

import RPi.GPIO as GPIO
import time
import Adafruit_ADS1x15

adc = Adafruit_ADS1x15.ADS1115()
GAIN = 1

adc.start_adc(0, gain=GAIN)
GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)
GPIO.setwarnings(False)

servo = GPIO.PWM(14, 50)

servo.start(0)

Def Distance():
    D_value = adc0.get_last_result()
    D =    (1.0 / (F_value / 13.15)) - 0.35
    Return D

j=12.5
k=2.5
i=0

distLR=[] 
distRL=[]

while True:
        while k<=12.5:
                servo.ChangeDutyCycle(k)
                time.sleep(.1)
                distLR.insert(i,Distance())
                k = k + 2.5
                i = i + 1
        print distLR

        i=0
        k=0

        del distLR[:]

        while j>=2.5:
                servo.ChangeDutyCycle(j)
                time.sleep(.1)
                j = j - 2.5
                distRL.insert(i,Distance())
                i = i + 1

        print distRL

        i=0
        k=2.5
        j=12.5

       del distRL[:]

代码做了什么?如果它正常运行,那么它应该将整个 180 度的扫描读数分成 10 个均匀的步骤返回给你。继续——试一试,然后返回看看实际发生了什么。

现在大部分代码都是基础的,你可能也已经对这段代码实际在做什么有了一个概念。然而,让我们深入了解一下,看看具体情况:

Def Distance():
    D_value = adc0.get_last_result()
    D =    (1.0 / (F_value / 13.15)) - 0.35
    Return D

在程序的这一部分,我们定义了一个名为Distance()的函数。你可以看到,它只是从 ADC 中获取读数在步骤D_value = adc0.get_last_result();然后,存储在变量D中的值在行D = (1.0/F-value/13.15)) - 0.35中计算,以获取 ADC 读数的度量读数。最后,使用行Return D,我们从函数中返回值D

distLR=[] 
distRL=[]

我们声明了两个列表:distLR,用于伺服从左到右扫描的距离,distRL用于伺服从右到左扫描的距离。你可能会想这些括号里面为什么什么都没有。声明一个空数组是完全正常的。它们最初没有必要有值:


        while k<=12.5:
                servo.ChangeDutyCycle(k)
                time.sleep(.1)
                distLR.insert(i,Distance())
                k = k + 1
                i = i + 1
        print distLR

现在这就是真正的行动发生的地方。while循环只会在k的值小于或等于12.5的时候执行。在接下来的一行中,servo.ChangeDutyCycle(k),占空比的值将是k的值。最初,k的值将是2.5,因为我们已经在程序的开头定义了。现在我们添加另一行time sleep(.1),这将使程序暂停.1秒。这是必要的;否则,程序会在毫秒内解析完这个循环,舵机将无法跟上。因此,这是一个短暂的延迟。在接下来的一行中,我们有distLR.insert(I,Distance())。这行程序做了很多事情。首先,我们在这一行内命名了一个Distance()函数。正如我们定义的那样,它将使用 ADC 和红外接近传感器来计算距离。然后,它会将该距离值插入到列表distLR的位置I。在我们的程序中,我们之前已经赋值了i = 0的值;因此,距离值将被放在数组中的第一个位置。一旦整个过程完成,然后我们继续向前移动,并在这一行中将值增加一,k = k + 1;然后,我们在I = I + 1中做同样的事情。现在最后,一旦这个循环执行完毕,列表的值将使用print distLR打印出来:

        i=0
        k=0

在这一行中,我们只是为下一个循环重置i = 0k = 0的值:

        del distLR[:]

这对你来说可能有点新。每当我们在括号内使用冒号时,基本上意味着整个数组的元素将被删除:

 while j>=2.5:
                servo.ChangeDutyCycle(j)
                time.sleep(.1)
                j = j - 2.5
                distRL.insert(i,Distance())
                i = i + 1

        print distRL

在这段代码中,发生的事情与我们对左到右滑动所做的事情相同;唯一的区别是我们将它保存在一个名为distRL的新列表中,并且滑动从 12.5%的占空比开始,结束于 2.5%:

   i=0
        k=2.5
        j=12.5

       del distRL[:]

当我们打印出所有的值后,我们再次重置i = 1k = 2.5j = 12.5的值,以便我们的第一个循环可以无缝地开始,此外,我们还确保列表distRL中没有剩下任何东西。

这就是我们的代码是如何工作的,直截了当!

自动驾驶汽车上的激光雷达

还记得上次我们制作自动驾驶汽车吗?很酷,当然,这可能是你可以向朋友炫耀的东西。然而,现在我们要做的肯定比我们迄今为止所做的任何事情都要酷。

我们将把这个区域扫描仪放在我们的机器人车上。但等等,我们之前使用相同的传感器扫描过这个区域,把车转向其他方向。我们做到了,而且效果还不错,几乎不错。我敢打赌,有时它的准确性并不像你想象的那样。但这不是真正的问题。主要问题是它不是无缝的。它必须在中间停下来检查空间,然后向任一方向移动。我们现在要做的是更进一步的事情。所以在做任何更多的解释之前,让我们继续制作这辆新的机器人车,然后你来判断它是否更酷。

因此,为了制作它,你需要将区域扫描仪安装在车辆上。建议你将其设置在车辆的前端,并确保舵机的臂能够旋转 180 度。你可以使用我们用来固定红外传感器在舵机顶部的类似方法。在做所有这些的时候,尝试使用电缆束带来确保电缆不凌乱,并确保为轴和顶部的传感器的移动留出一些松弛。这些电缆束带可以让你的生活变得非常简单。一旦我们都准备好了,你应该将红外接近传感器使用 ADS1115 连接到树莓派,然后连接电机驱动器,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

完成后,继续上传以下代码:

import RPi.GPIO as GPIO
import time
import Adafruit_ADS1x15

adc0 = Adafruit_ADS1x15.ADS1115()
GAIN = 1
adc0.start_adc(0, gain=GAIN)

GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)

servo = GPIO.PWM(14, 50)
servo.start(0)

def Distance():
    D_value = adc0.get_last_result()
    D =    (1.0 / (F_value / 13.15)) - 0.35
    Return D

GPIO.setup(20,GPIO.OUT)
GPIO.setup(21,GPIO.OUT)
GPIO.setup(23,GPIO.OUT)
GPIO.setup(24,GPIO.OUT)

LForward = GPIO.PWM(20, 50)
LReverse = GPIO.PWM(21, 50)
RForward = GPIO.PWM(23,50)
RReverse = GPIO.PWM(24,50)

def stop():
    LForward.changeDutyCycle(0)
    LReverse.changeDutyCycle(0)
    RForward.changeDutyCycle(0)
    RReverse.changeDutyCycle(0)

def direction(index):

 if index == 0 :
    LForward.changeDutyCycle(0)
    LReverse.changeDutyCycle(30)
    RForward.changeDutyCycle(30)
    RReverse.changeDutyCycle(0)

elif index == 1

    LForward.changeDutyCycle(20)
    LReverse.changeDutyCycle(0)
    RForward.changeDutyCycle(50)
    RReverse.changeDutyCycle(0)

 elif index == 2 :

    LForward.changeDutyCycle(50)
    LReverse.changeDutyCycle(0)
    RForward.changeDutyCycle(50)
    RReverse.changeDutyCycle(0)

elif index == 3 :

    LForward.changeDutyCycle(50)
    LReverse.changeDutyCycle(0)
    RForward.changeDutyCycle(20)
    RReverse.changeDutyCycle(0)

 elif index == 4 :

    LForward.changeDutyCycle(20)
    LReverse.changeDutyCycle(0)
    RForward.changeDutyCycle(0)
    RReverse.changeDutyCycle(20)

 else:
 stop()

j=12.5
k=2.5
i=0

dist1=[]
dist2=[]

while True:

    while k<=12.5:
    servo.ChangeDutyCycle(k)
    time.sleep(.2)
    dist1.insert(i,Distance())
    k = k + 2.5
    i = i + 1

 print dist1

 i=0
 k=2

 max_dist1 = max(dist1)
 max_dist1_index = dist1.index(max_dist1)

 direction(max_dist1_index)

 del dist1[:]

 print max_dist1
 print max_dist1_index

 while j>=2.5:
    servo.ChangeDutyCycle(j)
    time.sleep(.2)
     j = j - 2.5
     dist2.insert(i,Distance())
    i = i + 1

print dist2

i=0
j=12

 max_dist2 = max(dist2)
 max_dist2_index = dist2.index(max_dist2)

 direction(max_dist2_index)

 del dist2[:]

 print max_dist2
 print max_dist2_index

呼!那是很长,不是吗?但相信我,可能很长,但不难。所以让我们看看这段代码在做什么:

LForward = GPIO.PWM(20, 50)
LReverse = GPIO.PWM(21, 50)
RForward = GPIO.PWM(23,50)
RReverse = GPIO.PWM(24,50)

这些东西可能对你来说看起来很新。但其实并不是。我们正在定义哪个引脚号将以什么 PWM 频率运行。此外,我们已经为用于电机控制的每个 GPIO 引脚命名。好吧,我们做所有这些都没问题,但为什么我们突然开始给电机驱动器提供 PWM。我们不是很满意给一个简单的高脉冲吗?

答案非常直接。通过使用 PWM,我们能够在之前的章节中改变 LED 的亮度。同样,通过改变 PWM 输出到电机驱动器的控制引脚,你不仅可以定义旋转的方向,还可以定义旋转的速度。这一切都是通过 PWM 完成的。所以假设引脚号20正在以 50%的占空比获得 PWM。这基本上意味着连接到它的电机将获得电机驱动器接收到的输入电压的一半。所以现在我们不仅可以控制电机旋转的方向,还可以控制旋转的速度:

def direction(index):

 if index == 0 :
    LForward.changeDutyCycle(0)
    LReverse.changeDutyCycle(30)
    RForward.changeDutyCycle(30)
    RReverse.changeDutyCycle(0)

elif index == 1
    LForward.changeDutyCycle(20)
    LReverse.changeDutyCycle(0)
    RForward.changeDutyCycle(50)
    RReverse.changeDutyCycle(0)

在这个语句中,我们定义了一个direction(index)函数。它的作用是比较索引的值,并根据它给予电机动力。所以假设索引是 0。在这种情况下,左侧的车轮将向相反方向移动,而右侧的车轮将向相反方向移动,这将使机器人绕着轴旋转。

在下一条语句中,我们写了一个elif语句,所以如果else语句不成立,它将检查主体中的其余elif语句。在direction(index)的整个定义中有四个elif语句,这基本上意味着它将检查每一个并根据参数的值执行其中之一的活动。在这种情况下,它是索引。此外,还有一个最终的else语句,如果没有一个情况成立,它将调用一个停止的函数。这将停止车辆:

max_dist1 = max(dist1)

这一行非常有趣,因为我们正在使用我们已经使用过的列表的另一个有趣部分。因此,通过max()方法,我们可以找到列表中的最大值。因此,在这一行中,我们只是找到最大值并将其放入一个名为max_dist1的变量中:

max_dist1_index = dist1.index(max_dist1)

列表的美妙似乎没有尽头。在这一行中,我们正在使用另一种名为index()的方法;这种方法给出了列表中数值的索引。因此,我们可以知道数值在列表中的位置。因此,在这一行中,我们正在证明max_dist1的值。index()方法搜索索引号,并将该值存储在一个名为max_dist1_index的变量中:

 direction(max_dist1_index)

由于我们已经定义了Direction()函数,现在我们所做的就是调用函数来决定前进的方向。那么,给你的车辆加油,看看它们驾驶得如何,不要忘记拍摄视频并在网上发布。

玩得开心!

总结

专业激光扫描仪非常昂贵,因此在本章中,我们自己制作了一种替代品,并将其安装在我们的车辆上。在下一章中,我们将涵盖诸如视觉处理、物体检测、物体跟踪等主题,这将使我们能够进行基本的视觉处理,并使汽车朝着特定物体(如球)的方向移动。


网站公告

今日签到

点亮在社区的每一天
去签到