Python 物联网入门指南(六)(2)https://developer.aliyun.com/article/1507315
基础知识
每当我们谈论从一个地方移动到另一个地方时,我们都会想到轮子,同样,每当我们想到移动机器人的车轮时,我们都会想到电机。存在各种不同类型的电机。因此,让我们首先看看最基本的电机类型,即称为刷式直流电机的电机。顾名思义,它是在直流电上工作的。你可能会发现这样的电机:
相信我,这些东西无处不在,从你为邻居买的圣诞礼物到最大的坏家伙机器,你都会发现这些电机隐藏在引擎盖下。这些电机之所以常见,是因为它们非常非常简单。如此简单,以至于只需要一块电池和两根导线就可以为它们供电。只需将正极连接到一个端子,负极连接到另一个端子,电机就会开始旋转。交换这些连接,旋转方向将改变。取两个电池并加倍电压,电机将旋转得更快。就是这么简单。
现在你可能会认为我们只需将这个电机连接到树莓派,然后就可以开始了。但不幸的是,情况并非如此。你可能还记得前几章提到的,树莓派只能提供大约 50 毫安,但电机的消耗可能要高得多。因此,为了运行一个电机,我们需要一个中间设备。
你脑海中首先想到的可能是使用继电器,为什么不呢?它们可以传输大量电流并且可以处理高电压。这应该是理想的选择。如果你这样想,你是对的,但只是在某种程度上,因为继电器只是一个我们可以用来打开或关闭电机的开关。我们将无法控制电机的速度或旋转方向。现在,你可能会认为这个问题并不新鲜,我们可以很容易地通过使用脉宽调制(PWM)来解决,对吗?好吧,答案是否定的!因为这些继电器是机械设备,由于它们的机械性质,每秒开关的最大限制是有一些的。因此,它将无法应对 PWM 的频率。最后,我们仍然会面临改变电机的方向和速度的问题。那么现在我们该怎么办呢?
正如我经常说的,问题的美妙之处在于它总是有解决方案,而这里的解决方案被称为电机驱动器。电机驱动器主要是一组电子继电器——一种可以允许高电流但不是机械的开关。因此,我们可以每秒切换数百次。这些电子继电器要么由简单的晶体管制成,要么在高功率应用中,甚至可以使用 MOSFET 进行切换。我们可以简单地给这些电子开关提供 PWM,并在确保向电路传递足够电流的同时使电压调制。此外,正如我之前提到的,电机驱动器由一组这些电子继电器组成。它们排列的最常见和可行的方式称为全桥或 H 桥。在我进一步解释之前,让我们看看这到底是什么:
在全桥中,我们有四个与连接的电机相连的开关电路;根据需求,这些可以独立地打开或关闭。在关闭状态下,所有这些开关电路都处于断开状态,因此保持电机关闭。现在,每当我们想要启动电机时,我们将不得不打开两个开关,使电路完整,电机开始工作。让我们看看它会是什么样子:
在这里,我们打开了开关电路S2和S3;这反过来完成了电路,让电流在电机中流动。现在,为了控制速度,这些相同的开关电路可以以非常高的频率以不同的占空比打开和关闭,以实现特定的平均电压。现在我们可以通过这两个开关电路改变电压来实现电机的特定速度,让我们看看如何改变电机的旋转方向:
在这个电路中,我们关闭了之前连接的S2和S3,而是打开了S1和S4,因此电机的极性被颠倒了。正如我们之前讨论的,每当直流刷电机的极性改变时,方向也随之改变。市场上有各种类型的电机驱动器。我们在这里理解的是称为刷式直流 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) • 1 • 2
电机 | 引脚 | 输入 | 状态 |
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
的值在0
到101
之间。它还会在每次循环运行时增加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()
程序不可能比这更简单了;我们在这里所做的就是向电机驱动器发出命令,让电机单向旋转。可能会有一组电机会以相反方向旋转的情况,这种情况下你应该改变电机驱动器上的连接极性。这应该解决问题。有些人可能会认为我们也可以对代码进行更改来解决这个问题,但根据我的经验,从那里开始就会变得复杂,并且如果你选择其他路径,会给你带来麻烦。
好了,一切都准备就绪,一切都运行良好。继续尝试一些其他输出排列组合,看看车子会发生什么。别担心,无论你做什么,除非它从屋顶上跑下来,否则你都不会损坏车子!
通过蓝牙控制车辆
玩了一些尝试这些组合的乐趣吗?现在是时候我们继续前进,看看还有什么其他可能性了。我们都玩过遥控车,我相信每个人都会对那些敏捷的小玩具感到开心。我们将做类似的事情,但以一种更复杂的方式。
我们都知道蓝牙:这是与附近设备通信的最佳方式之一。蓝牙通信是一种中等数据速率、低功耗的通信方法。这在移动设备中几乎无处不在,因此是一个理想的开始方式。在本章中,我们将通过蓝牙使用手机来控制车辆。现在让我们看看我们如何做到这一点。
我们想要做的第一件事是将智能手机与机器人车配对,为此我们需要在树莓派上打开终端并执行以下步骤:
- 在命令行中输入
~ $ bluetoothctl
;这是一个蓝牙代理,允许两个蓝牙设备进行通信。没有蓝牙代理,这两个设备首先就无法进行通信。 [Bluetooth] # power on
命令简单地启动了树莓上的蓝牙。[Bluetooth] # agent on
命令启动代理,然后可以为我们启动连接。[Bluetooth] # discoverable on
命令使树莓派的蓝牙可发现。蓝牙可能已经打开,但我们必须使其可发现,以确保其他设备可以找到它并连接到它。[Bluetooth] # pairable on
命令使设备可配对。如果蓝牙已打开,这并不意味着您的设备将能够连接,因此我们需要使其可配对,这个命令正是这样做的。[Bluetooth] # scan on
命令开始扫描附近的蓝牙设备。这个命令的输出将是一些 MAC 地址以及蓝牙名称。MAC 地址是设备的物理地址;这是一个唯一的地址,因此它永远不会对两个设备相同。[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
现在,蓝牙有多个端口;这是一个非常有用的概念,通过一个单一的蓝牙连接,我们可以将各种数据流传输到各种设备和程序。这避免了数据的冲突,并确保数据安全地传输到确切的接收者。我们现在使用的程序非常简单,我们不需要多个端口进行数据通信。因此,我们可以使用任何1
到60
个可用端口进行通信。在程序的这一部分,您可以写任何端口,您的程序将正常运行:
server_socket.bind(("",port))
现在,每当我们连接两个设备时,我们需要确保它们在整个通信过程中保持连接。因此,在这里我们写下这个命令:server_socket.bind
。这将确保您的蓝牙连接在整个通信过程中保持连接。
正如您所看到的,参数中的第一个参数是空的。在这里,我们通常写下它必须绑定的 MAC 地址。然而,由于我们将其设置为空,它将自动绑定到我们已经配对的 MAC 地址。我们的第二个参数是它必须连接的端口。正如我们所知,port
变量的值被设置为1
。因此,它将自动连接到端口号1
:
server_socket.listen(1) • 1
这是一条非常有趣的线。正如我们所知,我们可能不是唯一一个尝试连接到树莓的蓝牙设备的人,因此当树莓接收到另一个连接请求时,树莓应该怎么做呢?
在这一行中,我们只是在定义:我们正在调用一个名为listen(1)
的方法。在这个函数中,我们已经将参数的值定义为1
。这意味着它只会连接到一个设备。任何其他尝试连接的设备都无法通过。如果我们将这个参数改为2
,那么它将连接到两个设备,但它会留在队列中,因此被称为队列连接:
client_socket,address = server_socket.accept()
现在大部分连接的事情都已经完成,我们还需要知道我们是否连接到了正确的地址。server_socket.accept()
方法的作用是返回套接字号和它正在服务的地址。因此,我们将其存储在两个名为client_socket
和address
的变量中。然而,正如我们所知,套接字将仅保持为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 度。你现在怎么想?我们如何用电机实现这种精度水平呢?
激光雷达
- 伺服电机
- 现在我们有的是一个通过多个减速齿轮与电位器耦合的电机,它将减慢电机的速度并增加扭矩。在最终齿轮处,轴向外安装到机身上并与电位器耦合。列表
- 现在,让我们把它放在一个有趣的组合中:
Python 物联网入门指南(六)(4)https://developer.aliyun.com/article/1507318