在第1部分中,我们制作了一块定制的亚克力板底座,把NEMA 17步进电机安装到了底盘上。然后,我们将Arduino Uno 3和电机开发板也连到亚克力底座上,并安装了电机开发板库。在第2部分中,我们将添加机器人工作所需的其他系统部件,比如伺服器和激光测距仪(LRF),并编写一个程序,让机器人能够自主移动。
有关如何利用Arduino UNO R3从零开始搭建轮式机器人的信息,请参考我们在之前的文章《如何制作自己的机器人》 和《如何制作自己的机器人(第2部分)》。在本文中,我们将进一步增加机器人的功能:让该机器人动起来并为其增加激光测距(LRF)功能,以使该设备能够检测物体并测量距离。
该机器人的设计目标如下:
- 向前、向后移动;向左和向右旋转90度;向左和向右旋转45度。
- 避开障碍物时根据最佳可用路径朝不同的方向移动。
- 测量各个方向(前方、左右90度、左右45度)的距离。
- 根据可用的最长距离,确定朝哪个方向前进(向前、向后、向左或向右)。
硬件
制作此机器人所需的硬件请参见以下硬件明细,这些零部件在许多电商处都可以买到(明细中给出了部分地址)。
-
- Arduino Uno rev 3 (www.adafruit.com/products/50)
- 适用于Arduino的Adafruit步进电机开发套件(www.adafruit.com/products/1438)
- 步进电机 (www.adafruit.com/products/324)
- 底盘、螺丝和尾轮 Parallax.com
- 车轮、橡胶轮胎和轮毂 Makeblock.com
- 伺服装置和安装套件 (www.parallax.com/product/570-28015)
- Parallax激光测距仪 (www.parallax.com/product/28044)
- OLED显示屏 (www.seeedstudio.com/Grove-OLED-Display-1.12”-p-824.html)
- 红色包装纸/盒 (https://www.seeedstudio.com/Grove-Red-Wrapper-1*2(4-PCS-pack)-p-2585.html
- 锂聚合物电池3型(11.1V)和连接器 (https://www.hobbyking.com/hobbyking/store/__18203
- 195 x 195 x 3mm亚克力板 (https://www.jaycar.com.au/clear-acrylic-sheet/p/HM9509)
软件
- Arduino IDE
- Adafruit Motor Shield Library
- LCD Display9696 Library.zip
工具
- 圆锉
- Dremel 电动工具
- 电烙铁
- 迷你钢锯
安装并测试伺服器
下一步是安装用于平移的伺服器。首先,请用螺钉将两块小板拧到伺服基座上,然后用螺钉将其固定到亚克力底座上,如图1和图2所示。请用2个螺钉将铝制安装架安装到伺服器的顶部。
图1.用两小板将伺服器固定到亚克力底座上
图2.将铝制安装架安装到伺服器上
如图3所示,将伺服器的接头连至电机开发板。电机开发板上有2个伺服器接头,分别标记为“servo1”和“servo2”。本次连接请使用servo1接头(外侧的那个)。请注意,切勿接反。
图3.伺服与电机开发板的连接
现在,我们可以运行程序了,请将以下代码上传到Arduino。无需安装新库,系统所需的库(Servo.h)都已包含在Arduino IDE程序中。伺服器应使用Digital引脚10(servo 1),或者如果伺服器连接到电机开发板的servo 2,那么应使用引脚11。
//***********************************************************************************************
#include <Servo.h>
Servo panMotor; // servo for laser range finder (lrf) scanning
int pos = 0; // variable to store the servo position
const int a = 1000;
void setup()
{ panMotor.attach(10); } // Attach Servo for scanning to pin 10
void loop()
{
// *************************** Scan Left ***********************************
panMotor.write(180); // 90 degree
delay(a);
panMotor.write(135); // 45 degree
delay(a);
// ************************** Scan Right **********************************
panMotor.write(45); // 45 degree
delay(a);
panMotor.write(0); // 90 degree
delay(a);
// ************************* Neutral position *****************************
panMotor.write(90);
delay(a);
}
//**************************************************************************************************
代码很简单,它负责让机器人从左向右进行扫描并返回原始位置。
伺服器的工作原理请参见以下视频:
安装激光测距仪(LRF)
激光测距仪产品文档– Parallax
Parallax激光测距仪(LRF)模块是一款利用激光技术计算仪器到目标物体的距离的测量仪器。该设备根据光学三角测量法(激光、摄像头和物体质心之间的简单三角函数)来计算仪器到目标物体的距离。其最佳测量范围为6–48英寸(15–122厘米),测量精度误差<5%(平均3%)。
硬件安装很简单。只需在亚克力板上钻两个与LRF位置相匹配的孔,然后用塑料垫片和螺钉将LRF固定到铝底盘上即可(请参见图5)。
图4.将LRF电缆连至电机开发板
图5.安装在铝制安装架上的LRF
由于LRF的最佳测量值上限为122厘米,我们需要将铝制安装架稍微向前弯曲,以使该范围始终小于120厘米(图6)。
图6.向前弯曲铝制安装架,使激光到地的距离小于120厘米
请完全按照图7将电缆接头连至LRF。GND接地,VCC接5V,SOUT接引脚8,SIN接引脚9。
图7. LRF与电机开发板的连接
我们已经完成了LRF的安装和连接,现在就来上传代码吧!同样,无需安装库。我们要用的SoftwareSerial.h已经包含在Arduino IDE中。
下面的代码源自示例代码,我们只是进行了一些修改,将距离数据从字符串转换为整数。其作用是测量传感器到前方物体的距离并打印结果。我们用串行监视器显示结果。
// **************************************************************************************************************************************************
#include <SoftwareSerial.h>
#define rxPin 8 // Serial input (connects to the LRF's SOUT pin)
#define txPin 9 // Serial output (connects to the LRF's SIN pin)
#define ledPin 13 // Most Arduino boards have an on-board LED on this pin
#define BUFSIZE 16 // Size of buffer
int lrfDataInt;
SoftwareSerial lrfSerial = SoftwareSerial(rxPin, txPin);
void setup() // Set up code called once on start-up
{
// *************************************** setup for LRF ***********************************************
pinMode(ledPin, OUTPUT);
pinMode(rxPin, INPUT);
pinMode(txPin, OUTPUT);
digitalWrite(ledPin, LOW); // turn LED off
Serial.begin(9600);
while (!Serial); // Wait until ready
Serial.println("\n\nParallax Laser Range Finder");
lrfSerial.begin(9600);
Serial.print("Waiting for the LRF...");
delay(2000); // Delay to let LRF module start up
lrfSerial.print('U'); // Send character
while (lrfSerial.read() != ':');
delay(10); // Short delay
lrfSerial.flush(); // Flush the receive buffer
Serial.println("Ready!");
Serial.flush(); // Wait for all bytes to be transmitted to the Serial Monitor
}
// ****************************************** main loop ************************************************
void loop() // Main code, to run repeatedly
{
lrf();
}
// ****************************************** end main loop *********************************************
void lrf()
{
lrfSerial.print('R'); // Send command
digitalWrite(ledPin, HIGH); // Turn LED on while LRF is taking a measurement
char lrfData[BUFSIZE]; // Buffer for incoming data
int lrfDataInt1;
int lrfDataInt2;
int lrfDataInt3;
int lrfDataInt4;
int offset = 0; // Offset into buffer
lrfData[0] = 0; // Clear the buffer
while(1)
{
if (lrfSerial.available() > 0) // If there are any bytes available to read, then the LRF must have responded
{
lrfData[offset] = lrfSerial.read(); // Get the byte and store it in our buffer
if (lrfData[offset] == ':') // If a ":" character is received, all data has been sent and the LRF is ready to accept the next command
{ lrfData[offset] = 0; // Null terminate the string of bytes we just received
break; } // Break out of the loop
offset++; // Increment offset into array
if (offset >= BUFSIZE) offset = 0; // If the incoming data string is longer than our buffer, wrap around to avoid going out-of-bounds
}
}
lrfDataInt1 = ( lrfData[5] -'0');
lrfDataInt2 = ( lrfData[6] -'0');
lrfDataInt3 = ( lrfData[7] -'0');
lrfDataInt4 = ( lrfData[8] -'0');
lrfDataInt = (1000*lrfDataInt1)+ (100*lrfDataInt2)+(10*lrfDataInt3) + lrfDataInt4;
Serial.print("Distance = "); // The lrfData string should now contain the data returned by the LRF, so display it on the Serial Monitor
Serial.println(lrfDataInt);
Serial.flush(); // Wait for all bytes to be transmitted to the Serial Monitor
digitalWrite(ledPin, LOW); // Turn LED off
delay(1000);
}
//*************************************************************************************************************************************************
串行监视器显示的结果如下所示。所有尺寸的单位均为毫米。
安装OLED显示屏
首先,我们将OLED塑料盒安装到亚克力底座中(请参见图9),然后再将OLED显示屏附带的线缆一端连至显示屏。要将显示屏连至电机开发板,请将线缆另外一端jst接头上的线缆剪下,然后将红导线焊至5V,将黑导线焊至Ground,将黄导线焊至SDA引脚,将绿导线焊至SCL引脚。请确保OLED显示屏的背面朝外。
图8 . OLED与电机开发板之间的连接
将OLED固定在底座上并连接电缆后,我们就可以运行部分软件了。
首先,请确保已经安装了SeeedOLED.h。然后,将以下代码上传到Arduino。该代码使用了名为oled1的函数,稍后的最终编码也会使用该函数。其基本功能就是显示100到109的数字。
//*****************************************************************************************************************************************
#include <Wire.h>
#include <SeeedOLED.h>
int distanceFwd;
void setup()
{ Wire.begin();}
void loop()
{
int i = 0;
for (i; (i < 10); i ++)
{ distanceFwd = 100 + i;
oled1();
delay(1000); }
}
void oled1()
{
SeeedOled.clearDisplay(); //clear the screen and set start position to top left corner
SeeedOled.setNormalDisplay(); //Set display to Normal mode
SeeedOled.setPageMode(); //Set addressing mode to Page Mode
SeeedOled.setTextXY(3,3);
SeeedOled.putString("Forward :");
SeeedOled.setTextXY(5,9);
SeeedOled.putNumber(distanceFwd);
}
//*****************************************************************************************************************************************
程序正常运行时,显示屏应该会显示以下视频中的内容:
安装最终代码
现在,我们已经完成了所有硬件的安装并测试了各个设备。让我们把所有软硬件结合起来,构建一个可以自主移动的智能激光机器人吧。最终程序会执行以下功能:
- 测量前方距离
- 如果距离超过70厘米,机器人将向前移动500步(大约50厘米);
- 如果距离大于40厘米小于70cm,机器人将向前移动200步(20厘米);
- 如果距离小于40厘米,机器人会向左扫描90度,向左扫描45度,向右扫描45度,向右扫描90度;
- 测量每个方向的距离,然后计算哪个方向的测量距离最长;
- 转向距离最长的那个方向;
- 返回第一步。
请复制以下代码并将其上传到Arduino:
//*********************************************************************************************************************
#include <Wire.h>
#include <Adafruit_MotorShield.h>
#include "utility/Adafruit_MS_PWMServoDriver.h"
#include <SoftwareSerial.h>
#include <Servo.h>
#include <SeeedOLED.h>
#define ledPin 13
#define BUFSIZE 16
#define rxPin 8 // Serial input (connects to the LRF's SOUT pin)
#define txPin 9 // Serial output(connects to the LRF's SIN pin)
SoftwareSerial lrfSerial = SoftwareSerial(rxPin, txPin); // Size of buffer (in bytes) for incoming data
// Create the motor shield object with the default I2C address
Adafruit_MotorShield AFMS = Adafruit_MotorShield();
// Connect a stepper motor with 200 steps per revolution (1.8 degree)
Adafruit_StepperMotor *myMotor1 = AFMS.getStepper(200, 1); // motor port #1 (M1 and M2)
Adafruit_StepperMotor *myMotor2 = AFMS.getStepper(200, 2); // motor port #2 (M3 and M4)
Servo panMotor; // servo for laser range finder (lrf) scanning
int leftDistance1;
int leftDistance2;
int rightDistance1;
int rightDistance2;
int maxDistance;
int angleTurn;
int directions;
int distanceFwd;
const int a = 30;
//*********************************************************************************start set up **************************
void setup() {
Serial.begin(9600); // set up Serial library at 9600 bps
panMotor.attach(10); // Attach Servo for scanning to pin 10
AFMS.begin(); // create with the default frequency 1.6KHz
myMotor1->setSpeed(100); // Set stepmotor1 speed at 100 rpm
myMotor2->setSpeed(100); // Set stepmotor2 speed at 100 rpm
pinMode(ledPin, OUTPUT);
pinMode(rxPin, INPUT); // Input pin for LRF
pinMode(txPin, OUTPUT); // Output pin for LRF
digitalWrite(ledPin, LOW); // turn LED off
Serial.begin(9600);
while (!Serial); // Wait until ready
lrfSerial.begin(9600);
Serial.print("Waiting for the LRF...");
delay(2000); // Delay to let LRF module start up
lrfSerial.print('U'); // Send character
while (lrfSerial.read() != ':');
delay(10); // Short delay
lrfSerial.flush(); // Flush the receive buffer
Serial.println("Ready!");
Serial.flush(); // Wait for all bytes to be transmitted to the Serial Monitor
panMotor.write(90);
delay(a);
}
//******************************************************************* start Loop *****************************************
void loop()
{
distanceFwd = lrf();
maxDistance = distanceFwd;
oled1();
if (distanceFwd > 700)
{ Motor(500,1);}
else
if (distanceFwd > 400)
{ Motor(200,1);}
else // if path is blocked
{ checkTurn();
turn();}
}
//***************************************************************** check turn function ********************************
void checkTurn()
{
digitalWrite(ledPin, HIGH);
// ************************** Scan Left ***********************************
panMotor.write(180);
delay(a);
leftDistance1 = lrf();
panMotor.write(135);
delay(a);
leftDistance2 = lrf();
oled();
// *************************** Scan Right *********************************
panMotor.write(45);
delay(a);
rightDistance2 = lrf();
panMotor.write(0);
delay(a);
rightDistance1 = lrf();
oled();
panMotor.write(90);
digitalWrite(ledPin, LOW);
// ************************************ Turn Left ************************
maxDistance = leftDistance1;
angleTurn = 100;
directions = 0;
if (maxDistance <= leftDistance2)
{angleTurn = 50;
maxDistance = leftDistance2;
directions = 0;
}
//*********************************** Turn Right ***********************
if (maxDistance <= rightDistance2)
{angleTurn = 50;
maxDistance = rightDistance2;
directions = 1;
}
if (maxDistance <= rightDistance1)
{angleTurn = 100;
maxDistance = rightDistance1;
directions = 1;
}
// ******************************* Turn Back******************************
if ((leftDistance1 < 300) && (rightDistance1 <300) && (distanceFwd <300))
{angleTurn = 200;
directions = 3;
}
}
//************************************************ turn function *********************************************************
void turn()
{
rightDistance1 = 0;
rightDistance2 = 0;
leftDistance1 = 0;
leftDistance2 = 0;
if (directions == 0) // turn left
{ Motor(angleTurn,3);}
if (directions == 1) // turn right
{ Motor(angleTurn,4);}
if (directions == 3) // turn back
{ Motor(angleTurn,4);}
}
//*************************************** Stepper Motor function ****************************************************
void Motor(int x,int y)
{
int i = 0;
for ( i; (i < x); i ++)
{
if (y == 1) // move forward
{myMotor1->step(1, FORWARD, SINGLE);
myMotor2->step(1, BACKWARD, SINGLE);}
if (y == 2) // move backward
{myMotor1->step(1, BACKWARD, SINGLE);
myMotor2->step(1, FORWARD, SINGLE);}
if (y == 3) // move left
{ myMotor1->step(1, FORWARD, SINGLE);
myMotor2->step(1, FORWARD, SINGLE);}
if (y == 4) // move right
{ myMotor1->step(1, BACKWARD, SINGLE);
myMotor2->step(1, BACKWARD, SINGLE);}
}
}
//*********************************************************************** LRF function *******************************
long lrf()
{
lrfSerial.print('R'); // Send command
digitalWrite(ledPin, HIGH); // Turn LED on while LRF is taking a measurement
char lrfData[BUFSIZE]; // Buffer for incoming data
int lrfDataInt1;
int lrfDataInt2;
int lrfDataInt3;
int lrfDataInt4;
int lrfDataInt;
int offset = 0; // Offset into buffer
lrfData[0] = 0; // Clear the buffer
while(1)
{
if (lrfSerial.available() > 0)
{
lrfData[offset] = lrfSerial.read();
if (lrfData[offset] == ':')
{ lrfData[offset] = 0;
break;}
offset++;
if (offset >= BUFSIZE) offset = 0;
}
}
lrfDataInt1 = ( lrfData[5] -'0');
lrfDataInt2 = ( lrfData[6] -'0');
lrfDataInt3 = ( lrfData[7] -'0');
lrfDataInt4 = ( lrfData[8] -'0');
lrfDataInt = (1000*lrfDataInt1)+ (100*lrfDataInt2)+(10*lrfDataInt3) + lrfDataInt4;
Serial.flush();
digitalWrite(ledPin, LOW);
return lrfDataInt;
}
//********************************************************* Oled function ************************************************
void oled()
{
SeeedOled.clearDisplay(); //clear the screen and set start position to top left corner
SeeedOled.setNormalDisplay(); //Set display to Normal mode
SeeedOled.setPageMode(); //Set addressing mode to Page Mode
SeeedOled.setTextXY(0,0);
SeeedOled.putString("Left 1:");
SeeedOled.setTextXY(0,12);
SeeedOled.putNumber(leftDistance1);
SeeedOled.setTextXY(2,0);
SeeedOled.putString("Left 2:");
SeeedOled.setTextXY(2,12);
SeeedOled.putNumber(leftDistance2);
SeeedOled.setTextXY(4,0);
SeeedOled.putString("Right 1:");
SeeedOled.setTextXY(4,12);
SeeedOled.putNumber(rightDistance1);
SeeedOled.setTextXY(6,0);
SeeedOled.putString("Right 2:");
SeeedOled.setTextXY(6,12);
SeeedOled.putNumber(rightDistance2);
}
void oled1()
{
SeeedOled.clearDisplay(); //clear the screen and set start position to top left corner
SeeedOled.setNormalDisplay(); //Set display to Normal mode
SeeedOled.setPageMode(); //Set addressing mode to Page Mode
SeeedOled.setTextXY(3,3);
SeeedOled.putString("Forward :");
SeeedOled.setTextXY(5,9);
SeeedOled.putNumber(distanceFwd);
}
//***********************************************************************************************************************************
图9. OLED显示屏安装在亚克力底座上的最终效果
结论
在之前的文章《如何制作自己的机器人》和《如何制作自己的机器人(第2部分)》中,我们用步进电机制作了一款简单的轮式机器人。这次,我们对其进行了功能改进:为机器人增加了激光测距仪(LRF)功能,并且安装了轮子,让机器人能够自由移动。我一直想制作一款能够进行测量的设备。在本例中,凭借激光传感器,我们的机器人不仅能够检测并避开物体,同时还能获取更准确的距离数据。激光机器人还有许多其他应用场景。您也可以利用该激光传感器设计自己喜欢的有趣项目。
接下来,我们会做一些更炫酷的事情,敬请期待!