欢迎回到ArduRover系列!在第一部分 和 第二部分中,我们已经构建了一个Arduino六轮驱动漫游车。在最后一部分内容中,我们将致力于编写一个用于机器人控制的程序。在第二部分中,我们已经介绍了一些代码,但是那些代码仅仅是用于控制电机运行的。这一次,我们希望能够得到所有传感器的数据,并且能够通过使用Android手机来实现实际远程操控!
硬件
软件
- • Arduino IDE 1.8.1
- • Android Studio 3.0
- • Arduino 库:
- • Arduino 设计图 – GitHub
- • Android 应用程序 – GitHub
对接线的修正
在本系列的上一部分里,除了部件的制作,我还介绍了逻辑电路(所有传感器和模块)的接线。那时,我只测试了电机,因为VNH5019驱动器是其中唯一我没使用过的子系统,当然,我使用的代码也只能用来控制它们。由于电机运行得很好,我就以为直接把其他所有函数(例如控制传感器、相机和伺服器的代码)添加上去是没有问题的。所以您可以想象出当我看到所有电机在添加了那些函数后马上就停止运行时,我有多么惊讶。
当类似这种问题发生的时候,我们必须返回上一步,回顾一下一切运行正常时的情况。我更改了代码,这很简单,只需要上传一个不同的Arduino设计图就可以了,然后查看一下问题是出在硬件还是软件上。果然,电机在之前的代码下恢复了运行。通过对新代码进行谨慎的添加和删除,我发现Servo库在某种程度上导致了该错误的发生。在调用servo.attach()函数后,电机就会停止运转。这似乎很奇怪,直到我意识到了信号传输的问题:使用Servo库的时候,电机控制部分好像完全没有信号输出。
VNH5019允许您通过使用脉冲宽度调制(PWM)来控制电机转速。伺服器也使用PWM来将小型电机设定在某一位置并保持不变。事实证明,在Arduino Mega上,PWM控制具有一些没有在官方文件中提及的特点。起初,我的伺服器PWM输入端连接到Arduino Mega的引脚8和9上,电机PWM连接到引脚44和46。根据Arduino官方文件,这些引脚都是支持PWM的。但是,这些文件没有提及的是,当您通过引脚2和13使用Servo库时,会失去引脚44和46的PWM功能支持。这是因为Servo库使用了一个ATmega2560定时器,该定时器同样用于控制引脚44到46上的PWM,那么自然而然,定时器一次只能执行一个任务。
考虑到这一点,我修正了接线原理图。如下图所示,用于电机的PWM移至引脚5和6。经过这次更改,伺服器和电机就可以同时工作了。
Arduino 程序结构
在编写控制如此复杂的系统的程序时,最好把所有需要考虑的因素都写下来。对于ArduRover,需要考虑的有以下内容:
- • 电机控制 – 这可能是最重要的部分,因为显然我们需要控制机器人的行进方向。
- • 传感器控制 – 我们需要从连接在机器人上的所有传感器中获取一些数据。
- • 相机控制 – 这部分包括拍摄、传输图片,以及通过伺服器支架来转动相机。
- • 数据记录 – 我们有一个板载SD卡,可以通过它来记录从传感器和GPS获取的信息。
一个潜在的问题是回传图像数据。正如我们在Arduino教程JPEG解码中所讲的那样,传输图像数据会花费很长时间。如果机器人突然决定一次把整个图像都传送过去,那么它将会在相当长的一段时间内处于无响应状态。因此,在每次运行主循环时,我们都需要决定是否传输图像中的某些部分,或者是否对上次循环执行期间传达回来的最终指令作出响应。
以上述要求为要领,我总结出了以下程序逻辑框图。
在主循环函数的每次迭代过程中,Arduino会首先更新所有当前的传感器数据,并把它们记录到SD卡中,包括ROHM传感器、GPS以及流经每个电机驱动器的电流。然后,会检查LoRa模块是否接收到了新的程序包。如果接收到了,那么程序包中的指令将会被执行,作出的响应将会再次通过LoRa模块进行传输。如果没有接收到新的指令,程序会检查是否有图片需要被传输,如果有的话,就会传输一个图片数据包,如果没有需要传输的图片,这一步会被跳过。最后,程序回到循环的开始,然后重复该过程。
我们必须意识到,使用这个逻辑结构,处理新的指令会比JPEG传输拥有更高的优先级。这样的话,我们就可以巧妙地避免上文中提到的机器人无响应的问题。指令与响应之间的最长延迟时间只会是传输一个图像数据包的时间。
既然已经有了程序逻辑结构的总体思路,那么我们需要关注另一个关键的步骤了:这些指令将会是什么样子?
指令和指令包结构
既然我们使用LoRa来传输和接收数据,就必须记住该技术的设计用途:小容量数据的远距离传输。这意味着我们要尽量精简我们的指令,因为每一个字节都会被计数。我们可以只发送一些字符串,例如“电机左侧50”来将左侧的电机设置为50%的转速,但是这其实非常浪费空间。这个字符串占用了14个字节的数据容量,但是仅仅传输了三个信息块:该指令的子系统目标(“电机”)中哪些电机会被影响(“左侧”)以及它们将会被设置的转速所占最大转速的百分比(“50”)。可以肯定地说,我们可以做得更好。这种类型指令的优点是非常易于阅读,但其实,再重申一下,只要系统运行正常,在指令执行这一块是不需要人为干预的。
所以,我决定不使用那些长的字符串,而是开发一种简单的指令系统,在每个指令包中使用的指令最长仅为4个字节。指令包中的第一个字节叫做首地址,将会被进一步分为两半:高四位包含来自控制系统的指令分类,低四位用来作为机器人的响应。0表示成功,非零值是具有已知含义的错误代码。
第1个字节 | 指令描述 |
0x00 | 停止所有电机 |
0x10 | 设置所有电机的转速和转向 |
0x20 | 设置所有左侧电机的转速和转向 |
0x30 | 设置所有右侧电机的转速和转向 |
0x40 | 设置相机倾斜角度 |
0x50 | 设置相机平移距离 |
0x60 | 使用相机拍摄图片 |
0x70 | 开始JPEG 传输 |
0x80 | 强制启动新传感器测量 |
0x90 | 获取最新传感器数据 |
0xA0 | 重新发送最终指令包 |
0xB0 | 设置LoRa调制调节器配置 |
0xC0 | 获取当前所有存在的错误信息 |
0xD0 | 未被使用 |
0xE0 | 未被使用 |
0xF0 | 未被使用 |
指令包的下一部分内容是有效负载 – 这也是在有关头文件中指令的所有附加信息存储的地方。这部分比首地址更简单一些,因为它的内容取决于指令类型:有些指令不需要任何附加信息,比如说0x00(停止两侧电机)。所以这些指令的有效负载为空。然而,一些命令要求额外数据不能超过三个字节,比如指令0xD0(LoRa配置),要求每个主要设置,如带宽、扩频因子、以及编码率(有关LoRa细节请参考LoRaLib教程 或者 GitHub wiki)只占一个字节。下表描述了所有指令包的结构。
第1个字节 | 第2个字节 | 第3个字节 | 第4个字节 |
0x00 | – | – | – |
0x10 | 左侧PWM控制转速 | 右侧PWM控制转速 | 转向 |
0x20 | 左侧PWM控制转速 | 转向 | – |
0x30 | 右侧PWM控制转速 | 转向 | – |
0x40 | 倾斜角度 | – | – |
0x50 | 平移距离 | – | – |
0x60 | – | – | – |
0x70 | 图片编码 | – | – |
0x80 | 传感器 ID(s) | – | – |
0x90 | 传感器 ID(s) | – | – |
0xA0 | – | – | – |
0xB0 | 带宽 | 扩频因子 | 编码率 |
0xC0 | – | – | – |
0xD0 | – | – | – |
0xE0 | – | – | – |
0xF0 | – | – | – |
当然,这些只是由控制系统发送然后机器人来接收的指令包。我们也希望机器人能够作出响应。对于某些指令,这种响应可能非常简单,仅仅是为了让控制系统知道指令是否被成功执行。如果您仔细看上文中的表格,会注意到所有的指令都只用了第一个指令字节的高四位,这样剩下的低四位可以用于响应。这种方法有两个显著的优势:首先,始终可以追踪到每个响应所属的指令,因为指令位总是作为响应值的一部分返回;其次,对于每一个指令,都有足够的空间用于16种不同的响应。
让我们用一个例子来进行说明。假设我们想用相机拍摄一张图片,那么我们会发送指令0x60。那么机器人有16种不同的方式来响应。如果响应是0x60,这意味指令已经被成功执行。所有其他类型的响应,0x61到0x6F,都意味着出现了错误。这样我们不仅知道指令执行失败了,还会知道为什么失败并且进行修正。当然,这些响应不一定总是只有一个字节的长度,有些指令会要求机器发回一些附加信息,比如传感器数据。下表显示的是所有针对不同指令的响应包。
指令 | 第1个字节 | 第2个字节 | 第3个字节 | 第4~240字节 |
0x00 | 0x00 | – | – | – |
0x10 | 0x10 | – | – | – |
0x20 | 0x20 | – | – | – |
0x30 | 0x30 | – | – | – |
0x40 | 0x40 | – | – | – |
0x50 | 0x50 | – | – | – |
0x60 | 0x6_ | – | – | – |
0x70 | 0x7_ | 图像数据 | 图像数据 | 图像数据 |
0x80 | 0x8_ | 发生错误的传感器 | 传感器数据 | 传感器数据 |
0x90 | 0x9_ | 发生错误的传感器 | 传感器数据 | 传感器数据 |
0xA0 | 0xA0 | – | – | – |
0xB0 | 0xB0 | – | – | – |
0xC0 | 0xC_ | 错误标志 | 错误标志 | – |
0xD0 | 0xD0 | – | – | – |
0xE0 | 0xE0 | – | – | – |
0xF0 | 0xF0 | – | – | – |
您可以注意到某些指令(主要是关于电机和伺服器的那些指令)只能返回0(成功)。这是因为当前版本中的板载电子设备无法判断这些指令是否被成功执行。为了达到验证的目的,我们需要增加新的设备来检查电机是否在运转,或者伺服器是否在正确的位置。这应该不太困难,让我们暂时忽略它,来看看那些可能会有报错响应的指令:
- • 0x60 (用相机拍摄照片)
这项指令有多种报错响应。如果没有相机,那么机器人会返回0x61。如果有相机并且能够正常工作,但是无法拍摄照片,那么会返回0x62。最后,如果成功拍摄了照片,但是不能存储,那么返回值为0x63。 - • 0x70 (开始JPEG 传输)
这同样是一个有多种返回值的指令。如果您能够回想起我之前的文章 Arduino上的JPEG解码,就知道第一个数据包里是有关图片的信息,而非真实像素值。我们需要对这两种类型进行区分,所以说如果数据包仅包含图片信息,响应值为0x70,后跟类似宽度、高度和单片机(MCU)计数的信息。如果数据包包含原始像素值,机器人的响应值为0x71,后跟238字节的像素数据。现在我们介绍报错代码:如果响应值是0x72,这意味着无法找到SD卡。如果是0x73,就表示SD卡没问题,但是无法找到请求的图片。 - • 0x80 (强制启动新传感器测量)
该指令只有一种失败模式—所请求的传感器无法进行测量。这种情况下,响应值为0x81,后跟有包含暗示传感器故障的一个字节。在这个字节中,位的索引对应下列传感器的顺序(从MSB到LSB):左VNH5019,右VNH5019, BD1020HFV, ML8511A, BM1383GLV, KX022-1020, RPR-0521RS和BM1422GMV。比如,如果响应的第二个字节是0x03 (0b0000 00011),表示传感器RPR-0521RS 和 BM1422GMV故障。 - • 0x90 (获取最新传感器数据)
该指令与0x80非常相似—只有当某些传感器故障时才会显示报错。如果那种情况发生,将会返回0x91,后跟有错误标志的相同字节。之后,将会有来自传感器的最多46字节的数据。如果没有传感器发生故障,那么返回的第二个字节将会是简单的0x00,后跟有包含所有46字节的数据。 - • 0xB0 (设置LoRa调制调节器配置)
这是目前应用的指令中最后一个会报错的指令,虽然这项指令本不应该会执行失败。该指令将设置新的LoRa调制调节器配置,LoRaLib检验所提供配置的有效性。如果有一个或者更多的配置值无效,那么响应字节中低四位之一将会被设置为相应值。所以如果提供的带宽是无效的,那么机器人将返回0xB1 (0b1011 0001)。如果提供的所有配置值都不正确,那么会返回0xB7 (0b1011 0111)。这项指令本不应该会失败,因为用户只能从已知设定值中进行选择。但是,在执行该指令时最好还是再检查一遍。LoRa模块是最关键的子系统,没有了它,就无法对机器人进行控制。由于LoRa调制的属性,两个模块上的设置必须相同,否则,接收器就无法对传输内容进行解调。
在解释完所有内容后,让我们回到最开始的内容。我们想要把左侧电机的转速设置为50%。我们已经知道了指令首地址为0x20。所以根据表格内容,我们可以填写剩下的字节,从控制器发送的指令包如下所示:
0x20 0x7F 0x00 0x00
第一个字节显然是首地址—设置左侧电机转速。下一个字节是转速。电机驱动器通过PWM调制来改变电机转速,所以数字0xF7(十进制为127)对应50%占空比或50%转速。第三个字节是转向—0x00表示正向,0x01表示逆向。最后一个字节仅用来占位—添加该字节来使指令包始终为四字节长。一旦指令被接收并被机器人成功执行,会返回以下指令包:
0x20
对于机器人上所运行的Arduino程序,其指令系统的实现都可以在我的GitHub中查看。通常我在这上面发布实际代码。不幸的是,代码文件太大无法放在本文中。所以如果您对于代码实现的细节有兴趣的话,请查看实际代码中的注释。现在,我们来继续看一些有趣的东西吧,那就是实现远程操控的应用程序!
远程控制应用程序
在ArduRover系列的最后一部分,我发布了一个机器人在计算机的控制下行驶的视频。计算机上运行的程序通过一个串行端口将数据发送给Arduino,然后Arduino传送数据给机器人。但是,这种设置有几个比较大的问题。
首先,通过键盘来控制机器人是非常困难的。下图是我在视频中用来控制机器人的应用程序的一个截图。
这是在Visual Studio 中的.NET框架下用C#语言来编写的。虽然它能够对测试电机起到一些作用,但是对于一次只调整一侧电机来控制机器人仍是一个挑战。这真是令人心烦。用户唯一可以控制的就是设置电机转速的滑动按钮和使机器人立马停止工作的按钮。
我遇到的另一个比较大的问题是用来测试的笔记本电脑比较重,不能用一只手拿着随身携带,然后空出另一只手来控制机器人。好吧,一个随身带着笔记本电脑和宠物机器人的人看起来可能是有点奇怪。
为了使控制器变得更加简单和便携,我需要一些可以连接到Arduino,并且重量轻、便于长时间携带的、最好是可以手持的设备。如果隐蔽性比较好那就更好了。起初,我打算使用一个可以安装在Arduino上的旧RC飞机控制器,但是后来我想到了一个更简单的解决方案:我的Android手机!它完美地符合所有要求:很轻并且可以手持。我们只需要通过USB端口或蓝牙将其与Arduino连接即可。
我提出的下一个应用程序是通过Android Studio用Java编写的。这比上文中的C#语言应用程序高级得多。它具有多个屏幕,允许用户控制所有子系统,而不仅仅是电机。让我们来看一下其中一个屏幕上的布局。
每个屏幕都会被分成几个部分。在屏幕的两侧,有两个滑动按钮(上图中1和2标记的地方)来分别对左侧和右侧的电机设定转向和转速。这样的布置可以使用户能够以更自然的方式来对机器人的移动进行控制:滑动按钮向上移动使其前进,向下移动使其后退。按钮扭矩越大,那一侧的电机转速就越大。很简单!
接下来是下面(3)的一个大“停止”按钮。它的作用看起来很好解释:按下这个按钮,电机就会停止。当我用C#应用程序测试机器人时,这应该是最有用的按钮了,所以我决定在该应用程序中保留这个设计。在应用程序的顶部,有一个带有多个标签的栏(4)。由于控制内容太多,无法都呈现在一个屏幕上,这个条形栏可以使用户在不同屏幕间切换。该屏幕中的内容显示在中心位置(5)。这意味着用户可以通过任何屏幕上来控制机器人以及停止所有电机!
这些屏幕(按照应用程序顶部条形栏中的排列顺序)为:
- • “相机”屏。 – 用户可以更改相机位置,拍摄并保存图片。目前相机的倾斜角度和水平位移也都显示在这里。中间的蓝色大框中还能显示接收到的图像。
- • “GPS”屏。 – 在当前版本应用程序中还未实现。这个想法是希望有一个地图,可以显示最近一次接收到的机器人位置。
- • “图形”屏。 – 同样在最新版本中尚未实现。在这个屏幕中,传感器的输出将以图形显示,类似于Arduino IDE中的串行绘图仪。
- • “传感器”屏。 – 该页面显示传感器的最近一次输出,同时也包含一些按钮来强制启动传感器进行新的测量,或更新显示值。
- • “监视器”屏。 – 基本上与Arduino IDE中的串行监视器相同:用户打字输入,监视器显示输出。该屏幕也显示所有发送的指令(黑色文本),机器人的所有响应(蓝色文本),所有报错(红色文本),或者来自应用程序的常规信息(灰色文本)。
- • “设置”屏。 – 包含多种设置,大部分与USB、蓝牙和LoRa通信相关。此外,屏幕上有一个按钮,我们可以用它来从机器人获取当前存在的所有错误。
结论
像往常一样,我们呈现一些试驾的照片!
显然,在许多地方仍然有很大的改进空间。比如说,测试机器人的时候,我注意到抑制器会有向下垂的趋势。
尽管这不是一个很大的问题,但它表明出抑制器内部的弹簧设置得有点松。弹簧设置更紧一些,机器人的车体就会更高一些,这样在崎岖地形中被困住的可能性会更小。
另一个可以改进的是应用程序。显然,其中某些部分现在只是个占位符而已:GPS和图形选项不包含任何内容,所以如果能够实现这些功能就更好了。不幸的是,在编写程序的时候,我还没想到该怎么做,所以我可能还需要更多的Android Studio经验才能找到方法。
最后,传感器和相机的位置布局也是可以改善的部分。虽然通常的习惯是将相机放在机器人的前方,但是这样还是有些暴露。可能通过使用某种杆来放在中间会更好一些,就像你在所有火星探测器上看到的那样。既然说到这里,添加一些太阳能电池板并将整个设备用于某些太空任务可能也是个不错的注意!如果您有几亿美元的闲置资金并且愿意用来资助这项惊人的太空冒险计划,就跟我联系吧!
言归正传,非常感谢您阅读这篇文章以及对整个系列的关注。如果您对机器人有任何想法请在下面讨论区留言!如果您决定要亲自构建属于您自己的ArduRover,请记得与全世界分享您的成果!