前言:
相信大家在用《ROS by example》学习ROS的过程中,基本上都是使用书本中的例程,在终端输入几行别人已经写好的代码,看看仿真效果。可是这样一来,笔者在跟着书本初略过了一遍后,却还是不知道如何通过ROS去具体操作实实在在的机器人,譬如想做导航和定位,自己的机器人该如何跟ROS这个框架结合起来,网上这方面中文教程很少,同时,英文教程里关于navigation有一个很系统的教程(点击将跳转),可不足的是很多细节没有涉及到,对于初学者还是有些难度。因此,我想结合自己的开发经验来写作此教程,希望能节省大家的开发时间。
笔者的机器人为两轮驱动移动机器人,前端有一个万向的支撑轮。电机驱动模块为DSP,通过串口和电脑相连。在继续下面的教程前,读者最好已经入门了ROS:看完了wiki社区的ROS初级教程(链接为中文教程),按着教程建立了你自己catkin_ws工作空间,以及建立了第一个package beginner_tutorials。并且,最好已经看完《ROS by Example 1》中的7-8章。
本教程将具体涉及如下问题:
系列(1):move_base发出的控制指令是什么?该如何转化为移动机器人左右轮的速度。
系列(2):移动机器人的左右轮的编码器信息如何转化为ROS的/odom;
系列(3):navigation的几个坐标系(/map frame , /odom frame , /base_link frame)是什么,他们之间该如何建立tf转换;如何使用move_base在一个空白的地图(blank map)上完成对实际机器人的控制,对应《ROS by Example 1》的8.3节。
控制系统的架构:
关于机器人导航与定位的系统架构,在《ros by example》 chapter 7一章中介绍了控制机器人的5个层次。这里笔者按照自己的理解以及开发经验给出三个层次。
最底层:机器人本身的电机驱动部分(我用的是DSP,其实最简单的51单片机都可以满足要求),该部分通过串口接收电脑端输出的左右轮期望速度,对左右轮分别进行PID控速。同时,定时采样电机码盘值,并转化为左右轮速度值通过串口上传给电脑。当然PID控速这一部分也可以放到电脑ROS端,这样的话,电脑串口输出的是直接的PWM值而不是之前的期望速度了。
中间通信层:电脑端和底层电机的控制通信,以及将传感器信息发布给ROS的通信。这一层主要通过串口(ROS已经集成了pyserial 用python操作这个模块进行串口控制)收集左右轮速度值,用航迹推演法将左右轮速度转化为机器人的x轴方向速度和机器人的旋转速度,然后发布/odom主题,好让ROS的相应package收到这个消息,进行机器人位置的估计。同时,这一部分还要关注ROS相应部分发出的机器人控制指令,转化为左右轮的期望速度,再通过串口传给DSP。这一层是自己写程序完成。
决策层:就是与导航有关的了,建立地图和定位,然后用move_base根据你发布的传感器信息做出路径规划以及机器人的速度和转向控制。这一部分为ROS相应的package已经完成,我们只需要调用即可。
在这个系列里,我们只关注如何用 move_base package 做出的控制对机器人进行实际控制。文章接下来的部分将按照从上到下的顺序,一个问题接一个问题的来介绍如何使用move_base控制实际机器人。
1. move_base package的系统介绍:
ROS提供的move_base 包让我们能够在已建立好的地图中指定目标位置和方向后,move_base根据机器人的传感器信息控制机器人到达我们想要的目标位置。它主要功能包括:结合机器人码盘推算出的odometry信息,作出路径规划,输出前进速度和转向速度。这两个速度是根据你在配置文件里设定的最大速度和最小速度而自动作出的加减速决策。下面的白色底色方框内就是move_base的内容:
图中我们可以看到move_base package 的输入和输出。要使得它能运行起来,我们就得构建好这些输入和输出。
必要的输入:
goal : 期望机器人在地图中的目标位置。
tf : 各个坐标系之间的转换关系。(具体/map frame --> /odom frame ,/odom frame --> /base_link frame)
odom:根据机器人左右轮速度推算出的航向信息(即/odom 坐标系中机器人x,y坐标以及航向角yaw,下面会具体介绍)
LaserScan:激光传感器的信息,用于定位。(在这个系列教程中,我们没有用到这个激光信息,而是在一个假的空白地图上对机器人进行控制,并假定/map坐标系和/odom坐标系完全重合,在后面会有关于这两个坐标系的介绍)
输出:
cmd_vel:在cmd_vel这个主题上发布Twist消息,这个消息包含的就是机器人的期望前进速度和转向速度。
再整理下思路:move_base收到goal以后,将目标goal通过基于actionlib的client(客户端)向服务器发送,服务器根据你的tf关系以及发布的odom消息不断反馈机器人的状态(feedbackcall)到客户端, 让move_base做路径规划和控制twist。
知道了move_base的这些外围消息接口以后,move_base的运行还需要一些内部的配置参数,如机器人的最大最小速度,已经路径规划时的最大转弯半径等等,这些参数配置在《Ros by Example 1》的8.1.2节有详细介绍。关于move_base更详细的介绍请点击官网wiki教程。
至此,我们已经熟悉了move_base的各种接口,它订阅了什么消息,会发布什么消息都已经清楚了。因此让move_base控制实际的机器人最主要的就是要解决实际机器人如何发布这些消息给move_base,以及如何接受move_base发出的消息。
2. Twist 消息转化为机器人左右轮期望速度。
首先,假设move_base能够正常工作了,它将把控制命令Twist发布到cmd_vel这个主题上。我们现在来解决如何利用这个Twist消息来对机器人进行控制。
在ROS by example 一书中的第七章开头就规定了机器人自身的坐标系系统,如下图。注意两个坐标系的建立都是右手坐标系,左图中的右手就是机器人本身,x轴就是前进的方向,垂直于两轮之间的轴连线,Y轴就是两个轮之间的轴连线。右图表示机器人的旋转坐标系,大拇指指向Z轴,逆时针方向为正值。
清楚了坐标系以后,再来看看Twist这个消息里包含的是什么东西。
使用ctrl + alt + t 打开一个新的终端以后,输入如下命令,就可以查看Twist的消息类型了。
rosmsg show geometry_msgs/Twist
其中linear 的x就是代表前进方向的速度,单位为m/s。angular 的z就代表机器人的绕中心旋转的角速度,单位为 弧度/s (rad/s)。
因此,我们只要在自己写的中间通信层程序中订阅cmd_twist这个主题(topic),就可以收到move_base发出的命令了。 下面给出一个如何订阅cmd_twist主题的demo。
首先在你之前建立的package的scripts文件夹下,笔者的是 beginner_tutorials/scripts文件夹,将下列代码复制进去,保存为your_filename.py。保存以后记得要chmod一下,让这个文件成为可执行的节点,具体操作如下。
roscd beginner_tutorials
cd scripts
chmod +x lis_cmdvel.py
程序代码:
#!/usr/bin/env python
#refernence: http://answers.ros.org/question/29706/twist-message-example-and-cmd_vel/
import roslib; roslib.load_manifest('beginner_tutorials')
import rospy
import tf.transformations
from geometry_msgs.msg import Twist
def callback(msg):
rospy.loginfo("Received a /cmd_vel message!")
rospy.loginfo("Linear Components: [%f, %f, %f]"%(msg.linear.x, msg.linear.y, msg.linear.z))
rospy.loginfo("Angular Components: [%f, %f, %f]"%(msg.angular.x, msg.angular.y, msg.angular.z))
# Do velocity processing here:
# Use the kinematics of your robot to map linear and angular velocities into motor commands
# v_l = ...
# v_r = ...
# Then set your wheel speeds (using wheel_left and wheel_right as examples)
# wheel_left.set_speed(v_l)
# wheel_right.set_speed(v_r)
def listener():
rospy.init_node('cmd_vel_listener')
rospy.Subscriber("/cmd_vel", Twist, callback)#/cmd_vel
rospy.spin()
if __name__ == '__main__':
listener()
执行这些操作以后,这个文件就可以用rosrun指令执行了。
rosrun beginner_tutorials lis_cmdvel.py
注意这个demo里的每当有Twist消息时,就会调用callback这个函数,callback这个函数里就是我们要处理的内容。这里只是简单的打印收到的消息,还没有对消息进行处理。
机器人期望的前进速度linear.x和转弯速度angular.z都由move_base输出了,那么如何将他们转化成机器人左右轮的期望速度呢?关于如何转化为左右轮的期望速度,我先贴出自己程序中的源代码部分,下面这三个函数是一个属于同一个类:
def callback(self,msg ):
cmd_twist_rotation = msg.angular.z #
cmd_twist_x = msg.linear.x
cmd_twist_y = msg.linear.y #这个一般为0
#将twist消息转化为左右轮各自的期望速度
wheelspeed = self.odom_to_speed(cmd_twist_x, cmd_twist_y,cmd_twist_rotation)
print 'msg:', msg #打印得到的twist消息
print wheelspeed #打印转化后的速度
#蓝牙串口发送到DSP wheelspeed[0]左轮速度, wheelspeed[1]右轮速度
self.blue_tooth_send([wheelspeed[0], self.speed_kp, self.speed_ki, wheelspeed[1]])
def odom_to_speed(self, cmd_twist_x =0, cmd_twist_y=0,cmd_twist_rotation=0):
'一般情况下,linear_y = 0 所以只需关注twist.linear.x 和 twist.angle.z的转换'
#这部分本来还有一段,关于twist.linear.y不为0时,如何转化的程序,Lz自己写的,实际可运行,但是不知道是否正确,所以这里删掉了。
cent_speed = cmd_twist_x #前进的速度,即两轮的中心速度
#将 指定的转速twist.angular.z 转化为左右轮的差速
yawrate2 = self.yawrate_to_speed(cmd_twist_rotation)
Lwheelspeed = cent_speed - yawrate2/2
Rwheelspeed = cent_speed + yawrate2/2
return Lwheelspeed, Rwheelspeed
def yawrate_to_speed(self, yawrate):
if yawrate > 0:
theta_to_speed = 0.0077 #右转系数
else:
theta_to_speed = 0.0076 #左转系数
#yawrate :rad/s *0.02表示 20ms内应该转多少弧度,/0.0076是把 要转的弧度转化为左右轮速度差
x = (yawrate * 0.02) / theta_to_speed
return x
这一段程序里最主要的是如何将指定的转速转化为两轮的差速。主程序中订阅了cmd_vel主题,一旦收到move_base发出的twist消息,就调用callback函数进行转化。如果linear.y 不为0,说明小车要沿着y轴运动,这会导致两轮的差速,但是对于两轮控制的移动机器人,twist.linear.y = 0 是在move_base的配置文件base_local_planner_params.yaml中有明确指定,所以不需关注linear.y的转换:
max_vel_y = 0.0 #zero for a differential drive robot
min_vel_y = 0.0
也就是只要关注前进速度linear.x和旋转速度angular.z的转换。
在直线行驶时,前进速度linear.x就是左右轮的期望速度。最主要的是将转速转化为左右轮的差速,一个轮子转的快,一个轮子转的慢,就有了转速。
下面介绍如何将指定的转速转化为两轮差速:
1.dsp采样的是单位时间内的码盘值,将码盘值转化为左右轮速度值后(Lwheelspeed,Rwheelspeed)通过串口发送给电脑端。
2.关于航迹推演(Odometry) 的公式中有一个关于如何有左右轮差速转化为旋转速度的计算公式。即yaw_rate = (Rwheelspeed - Lwheelspeed) / d .其中d为两轮间的间距,得到的转速单位rad/s。(如果不知道什么是Odometry 方法,可以点击这里。)或者也可以看看这个,还有这个链接。直接用这个公式可以计算,但是在测量这个公式中的d的时候有测量误差。因此,楼主这里采用的拟合的方法得到这个差速到转速之间的转换系数。具体操作如下:
先从0度开始逆时钟旋转小车(角速度为正),分别记下转到pi/2,pi,pi*3/2,2pi时两轮差速的累计值,(即右轮速度减去左轮速度的累计值)。思路是:两轮差速乘以系数为转速,两轮差速累计值乘以系数就是旋转的角度。因此多次记下这些数据后,拟合就能得到得到特定的差速值到对应角度之间的转换关系。
如楼主的数据如下:
角度 pi/2 pi 3/2*pi 2*pi
差速累计和 209.21 415 620.54 825.6
208.8 414.1 611.49 812.39
由于是线性关系,我们进行拟合以后得到这个转换系数为0.0077,拟合曲线如图,在实际操作中我记录了5组数据:
顺时针也可以采用这个系数,但是笔者,了防止左右轮机械上的差异导致这个系数不同,对顺时针也单独拟合了一次,得到0.0076。
右轮减去左轮的差速转化为角速度或者角度的系数有了,反过来,就可以将指定的旋转速度得到左右轮差速。
将指定的转速twist.angular.z * 0.02得到一个速度控制周期内(DSP底层设定的速度采样时间为20ms)应该旋转的角度量,这个角度量除以前面的系数就得到了单位控制周期内两轮之间速度的差异值。这就是上面程序中yawrate_to_speed()函数的计算思路。最后将这个速度的差异值/2,分别添加在中心速度上就分别得到了左右轮的期望速度。
现在完成了从cmd_vel twist 发送到电机这一部分的程序,在下一篇博客中,将介绍如何将介绍如何将电机左右轮的速度发布出来,让move_base接收到。