- 在之前的内容当中,我们了解到了ROS的基本使用,以及话题通讯和服务通讯的编写。那么可能会有小伙伴感到困惑,这玩意能干啥呢?
- ROS,机器人开源操作系统,它仅仅只是一个工具、一个框架,机器人的建造还是在你,只是你要合理的、高效的来使用这一套工具。这一节eider内容我们不讲ROS,只是来 谈谈机器人的设计理念。什么是机器人,我们该怎么样去设计机器人,机器人的功能该怎么实现。目前机器人领域的主要应用是在工业的机械臂,一个仓库的AGV小车,着两个也是 ROS的重点应用领域,会为你多学习ROS的小伙伴可能就是奔着我要造雷达车,我要造无人驾驶,我要造机械臂来的,困难吗?相当困难。
- 我们拿雷达车举例,一辆学习用的AGV雷达小车,淘宝最便宜的应该就是两轮差速小车,一辆价格在1500元左右,雷达价格500元左右,主控板用树莓派也得300到500元,小车用的电机得是直流编码电机就按一个50元来算,这就100元,然后你的ROS驱动板、车架、电池等等,自己手动DIY一辆的价格绝对在2000元左右。这还是没有算深度摄像头啊、外设传感器啊等等,要是再加一套机械臂,又是小2000元。
- 当然有的同学会说,ROS系统不是有仿真吗?我们为什么要花那么多的钱去买小车呢?可以说,成也仿真,败也仿真。仿真确实可以让你快速的学习ROS,但是没有实际操作,你可以很轻松的学会ROS的知识,但是你却无法应用。机器人的开发是一个多领域融合的开发,它需要机械、电气、嵌入式、网络通讯、人工智能等多专业的相互配合。
- 我们现在以设计一个两轮差速小车为例,来聊一下如何从零设计一个雷达SLAM小车。首先考虑的是小车底盘,这是我们整个机器人的载体部位,我们选择的是两轮差速小车,电机得是编码电机,我们需要通过编码器来计算里程和数据,从而知道小车的速度是否达到我们的计算。如果达到了就减速,如果不够不够就加速,这是通过PID来进行控制的,会在我们的电气和嵌入式部位考虑。我选择的底盘是这样的。
两轮差速小车底盘
直流TT马达编码电机
- 直流编码电机是六根线,M+和M-还是电机的两极,5V和GND是编码器的信号供电,而A和B就是编码器的信号输出。电机的编码器是磁编码器,上面有磁性,感知部位是霍尔原件,党磁性靠近霍尔元件时候,便会触发一个信号,我们可以使用中断的方式进行计数,再根据轮子的直径,从而计算出当前的历程数据。小车底盘30元,两个电机30元,电池根据情况预算40元,底盘100元敲定。
- 接下来是我们的电气部位,在这里我们要驱动我们的小车底盘,同时还有配合陀螺仪模块,获得加速度和角速度来校验我们的速度是否达到预计值。在这里,电机驱动我使用的是 L298N电机驱动模块(10元左右),驱动板使用Arduino UNO(15元左右),陀螺仪模块使用MPU6050(10元左右)总计成本35元到50元。我们需要向ROS主机,也就是我们的 上位机发送当前两个轮子的速度值,以及通过陀螺仪模块获取到的加速度和角速度;同时,我们也要接收上位机的发送过来的速度数值并执行。
- Arduino需要发送给ROS主机的数据是通过传感器获得的,左轮方向,左轮速度,右轮方向,右轮速度,X轴加速度,Y轴加速度,Z轴加速度,X轴角速度,Y轴角速度,Z轴角速 度,共计十个数据。ROS主机发个Arduino的数据是通过ROS计算得到的理论数据,左轮方向,左轮速度,右轮方向,右轮速度,共计四个数据。
- 大概的原理图就是这样,Arduino的D2和D3引脚具有外部中断功能,用于接编码器测速;D5、D6、D7控制电机M2的转速以及方向,D8、D9、D10控制电机M1的转速以及方向(L298N模块的使用不做介绍);A4和A5是IIC接口,接我们的MPU6050陀螺仪模块, 用于获得加速度和角速度。我们的通讯数据格式按照Modbus RTU协议格式来设计,如下。
- 然后是我们的嵌入式部分,在这里我们要写Arduino的驱动代码,以及ROS的功能包。先来聊聊MPU6050陀螺仪模块这里,我们通过IIC即可读取,参考代码如下。需要I2Cdev和MPU6050两个库,自行下载。
#include "Wire.h"
#include "I2Cdev.h"
#include "MPU6050.h"
MPU6050 accelgyro;
int16_t ax, ay, az;
int16_t gx, gy, gz;
void setup()
{
Wire.begin();
Serial.begin(9600);
Serial.println("Initializing I2C devices...");
accelgyro.initialize();
Serial.println("Testing device connections...");
Serial.println(accelgyro.testConnection() ? "MPU6050 connection successful" : "MPU6050 connection failed");
}
void loop()
{
accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
Serial.print("a/g:\t");
Serial.print(ax);
Serial.print("\t");
Serial.print(ay);
Serial.print("\t");
Serial.print(az);
Serial.print("\t");
Serial.print(gx);
Serial.print("\t");
Serial.print(gy);
Serial.print("\t");
Serial.println(gz);
}
我们的编码器部位,通过外部中断进行计数。参考代码如下。
#define BAUDRATE 115200
#define LEFT 0 //左轮
#define RIGHT 1 //右轮
#define FORWARDS true
#define BACKWARDS false
volatile long encoderLeft = 0L;
volatile long encoderRight = 0L;
//初始化编码器
void initEncoders(){
pinMode(2, INPUT);
pinMode(3, INPUT);
//中断函数(中断源,中断触发函数,中断触发信号)
//中断源可选值为0或1,一般分别对应2号和3号引脚
//需要中断的函数名
//LOW(低电平触发)、CHANGE(变化时触发)、RISING(低电平变为高电平触发)、FALLING(高电平变为低电平触发)
attachInterrupt(0, encoderLeftISR, CHANGE);
attachInterrupt(1, encoderRightISR, CHANGE);
}
//中断触发函数
void encoderLeftISR(){
encoderLeft++;
}
//中断触发函数
void encoderRightISR(){
encoderRight++;
}
//读左轮或右轮编码器
long readEncoder(int i) {
long encVal = 0L;
if (i == LEFT) {
noInterrupts(); //关中断
//detachInterrupt(0); //取消中断;取消指定类型的中断.
encVal = encoderLeft;
interrupts(); //开中断
//attachInterrupt(0, Code_left, FALLING);
}
else {
noInterrupts(); //关中断
//detachInterrupt(1);
encVal = encoderRight;
interrupts(); //开中断
//attachInterrupt(1, Code_right, FALLING);
}
return encVal;
}
//指定左右轮编码器复位,数值为0
void resetEncoder(int i) {
if (i == LEFT){
noInterrupts();
encoderLeft = 0L;
interrupts();
}else {
noInterrupts();
encoderRight = 0L;
interrupts();
}
}
//左右轮编码器复位,数值都为0
void resetEncoders()
{
resetEncoder(LEFT);
resetEncoder(RIGHT);
}
//初始化
void setup()
{
Serial.begin(9600);
initEncoders();
resetEncoders();
}
void loop() {
long lval=readEncoder(0);
long rval=readEncoder(1);
Serial.print("left: ");
Serial.print(lval);
Serial.print("; right: ");
Serial.println(rval);
delay(30);
}
- 至于左轮右轮的方向,我们通过代码的变量即可得知。在执行的部位,就是Arduino使用L298N电机驱动模块来实现控制的。以下是我设计的完整的Arduino代码,但是为经过测速,有兴趣的小伙伴客验证一下。目前只有发布部分。
#include "Wire.h"
#include "I2Cdev.h"
#include "MPU6050.h"
#define BAUDRATE 115200
#define LEFT 0 //左轮
#define RIGHT 1 //右轮
//如果一个变量所在的代码段可能会意外地导致变量值改变那此变量应声明为volatile,
//比如并行多线程等。在arduino中,唯一可能发生这种现象的地方就是和中断有关的代码段,成为中断服务程序。
// 中断函数中使用的变量需要定义为 volatile 类型.
volatile long encoderLeft = 0L;
volatile long encoderRight = 0L;
int IN1 = 9;
int IN2 = 8;
int IN3 = 7;
int IN4 = 6;
int ENA = 10;
int ENB = 5;
int L_flag = 0;
int R_flag = 0;
MPU6050 accelgyro;
int16_t ax, ay, az;
int16_t gx, gy, gz;
int last_time = 0;
int time = last_time
// 帧头 地址 长度 L方向 L速度 R方向 R速度 X加速度 Y加速度 Z加速度 X角速度 Y角速度 Z角速度 校验 帧尾
unsigned char msg[15] ={0x55,0x5A,0x0f,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0xBB}
//初始化编码器
void initEncoders()
{
pinMode(2, INPUT);
pinMode(3, INPUT);
//中断函数(中断源,中断触发函数,中断触发信号)
//中断源可选值为0或1,一般分别对应2号和3号引脚
//需要中断的函数名
//LOW(低电平触发)、CHANGE(变化时触发)、RISING(低电平变为高电平触发)、FALLING(高电平变为低电平触发)
attachInterrupt(0, encoderLeftISR, CHANGE);
attachInterrupt(1, encoderRightISR, CHANGE);
}
//中断触发函数
void encoderLeftISR()
{
encoderLeft++;
}
//中断触发函数
void encoderRightISR()
{
encoderRight++;
}
//读左轮或右轮编码器
long readEncoder(int i)
{
long encVal = 0L;
if (i == LEFT) {
noInterrupts(); //关中断
//detachInterrupt(0); //取消中断;取消指定类型的中断.
encVal = encoderLeft;
interrupts(); //开中断
//attachInterrupt(0, Code_left, FALLING);
}
else {
noInterrupts(); //关中断
//detachInterrupt(1);
encVal = encoderRight;
interrupts(); //开中断
//attachInterrupt(1, Code_right, FALLING);
}
return encVal;
}
//指定左右轮编码器复位,数值为0
void resetEncoder(int i)
{
if (i == LEFT){
noInterrupts();
encoderLeft = 0L;
interrupts();
}else {
noInterrupts();
encoderRight = 0L;
interrupts();
}
}
//左右轮编码器复位,数值都为0
void resetEncoders()
{
resetEncoder(LEFT);
resetEncoder(RIGHT);
}
void setup()
{
Wire.begin();
Serial.begin(BAUDRATE);
accelgyro.initialize();
initEncoders();
resetEncoders();
pinMode(IN1,OUTPUT);
pinMode(IN1,OUTPUT);
pinMode(IN1,OUTPUT);
pinMode(IN1,OUTPUT);
pinMode(ENA,OUTPUT);
pinMode(ENA,OUTPUT);
time = micros();
last_time = time;
}
void loop()
{
msg[0] = 0x55;
msg[1] = 0x5A;
msg[2] = 0x0F;
accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
long lval=readEncoder(0);
long rval=readEncoder(1);
time = micros();
if(L_flag = 0)
{
msg[3] = 0x00;
}else{
msg[3] = 0x01;
}
msg[4] = lval/(time-last_time); //直接计算数量,需要轮子周长
if(R_flag = 0)
{
msg[5] = 0x00;
}else{
msg[5] = 0x01;
}
msg[6] = rval/(time-last_time); //直接计算数量,需要轮子周长
last_time = time;
msg[7] = ax;
msg[8] = ay;
msg[9] = az;
msg[10] = gx;
msg[11] = gy;
msg[12] = gz;
msg[13] = (msg[3]+msg[4]+msg[5]+msg[6]+msg[7]+msg[8]+msg[9]+msg[10]+msg[11]+msg[12]) & 0xFF;
msg[14] = 0xBB;
Serial.write(msg,15);
}
- 在这里并不会向大家提供完整的代码(目前我这边也是开发阶段),大家可以根据这个 思路来进行自己ide设计。在我们的ROS当中,所有的速度数据是按照cmd_vel的话题来发 布的,我们需要将cmd_vel的话题内容转成我们两个轮子的速度。我们在之前的章节当中有 讲到cmd_vel的内容,在小乌龟的位置。cmd_vel内容如下。
rostopic pub /turtle1/cmd_vel geometry_msgs/Twist "linear:
x: 5.0
y: 5.0
z: 0.0
angular:
x: 0.0
y: 2.0
z: 2.0"
- 可以看到内容是这样的,分别是xyz的linaer(线速度)和angular(角速度)总共六个 数据。我们需要根据传感器获得的数据来计算当前的线速度和角速度,这个是实际数值;同 时也需要根据ROS系统计算出来的线速度和角速度来控制我们的小车运动,这里是理论值。 这个过程,就是运动学的正解和逆解。
- 在这里,我要提到右手笛卡尔坐标系。右手握拳,大拇指指向Z轴,食指指向X轴,中 指指向Y轴。对于到我们在平面运动的机器人,运动控制就是控制机器人在XY轴平面的运 动,Z轴不运动,线速度一直为0;而对于角速度来说,X轴和Y轴的角速度一直为0,Z轴角 速度不为0。(思考一下)
- 在底层控制代码当中,我们以及实现了两个轮子速度的获取,以及对两个轮子的速度控 制,那么将左右轮子的速度转变为XY轴平面的线速度和Z轴的角速度,这就是运动学模型的 正解问题。(Z轴没有线速度,X轴和Y轴没有角速度,因为在XY平面运动)
- 机器人系统的执行是一方面,执行的精准度是另一方面,我们一定要尽最大的程度上的精度。
- 机器人属于仿生学开发,类人。那么在这个过程当中机器人与机器人之间的通讯时必不可少的。加入我现在有一台运动小车机器人,有一台机械臂机器人,我现在想让两个机器人协调完成工作,那么我们使用ROS的话题,或者服务都可以完成这个任务。ROS系统是一个工具,个人认为它的核心在于通讯,但是依赖 于其强大的生态环境,才致使ROS的功能越来越庞大,越来越方便我们这些初学 者。我们可以说我的机器人是基于ROS设计的,但不能说这是我的ROS机器人
- ROS的通讯层次所使用的是XML/RPC的方式,RPC(Remote Procedure Call)即远程方法调用,是一种在本地的机器上调用远端机器上的一个过程(方法)的技术。这个过程也被大家称为“分布式计算”,是为了提高各个分立机器的“互操作性”而发明出来的技术。
- 什么是RPC?我现在有两台电脑,电脑A和电脑B,现在运行了两个程序,电脑B有个功能C,但是电脑A没有。那么电脑A就可远程调用电脑B的功能C,从而完成自己的任务,这就是远程过程调用。
- 这个机制在ROS机器人的分布式通讯当中,有着强大的功能。在这一讲的内容当中,我主要是阐述一下自己对于ROS的一些看法,以及如何低成本的搭建一个ROS雷达小车,关于XML/RPC的内容有个了解即可,我们是应用ROS并不是开发ROS。