• 欢迎访问开心洋葱网站,在线教程,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站,欢迎加入开心洋葱 QQ群
  • 为方便开心洋葱网用户,开心洋葱官网已经开启复制功能!
  • 欢迎访问开心洋葱网站,手机也能访问哦~欢迎加入开心洋葱多维思维学习平台 QQ群
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏开心洋葱吧~~~~~~~~~~~~~!
  • 由于近期流量激增,小站的ECS没能经的起亲们的访问,本站依然没有盈利,如果各位看如果觉着文字不错,还请看官给小站打个赏~~~~~~~~~~~~~!

一起来聊聊ROS-Serial和Arduino吧

人工智能 Rugels.Dix 2132次浏览 0个评论

基于ROS-Serial和Arduino实现控制六轴舵机机械臂

  ​ 不久之前我在古月学院上架了《软硬结合带你玩转ROS机械臂》的课程,课程里面我们通过Arduino Nano和ROS的结合实现了机械臂在ROS系统下的驱动。有些细心小伙伴已经加到我的QQ开始更深层次的交流学习,不知道你们有没有发现呢~   ​ 在课程当中,我使用的是基于StandardFirmata协议的驱动,我们给Arduino烧录它的示例程序StandardFirmata,通过Python的pyFirmata库来调用硬件,我们在软件系统实际做的,仅仅只是订阅了joint_state,并且通过pyFirmata的方式来驱动我们的Arduino,Arduino实现控制舵机。   ​ StandardFirmata有着很大的局限性,它对于IO的操作确实很方便,但是涉及到通讯部分的话就很难受。pyFirmata并没有操作串口、IIC、SPI相关的接口,pyFirmata的升级版pyMata增加了超声波、IIC等方式,但是在我使用过后仍觉得较为不便。   ​ 我喜欢使用Arduino,它封装的库函数对我这种不懂寄存器的程序猿来说,实在是方便。ROS的官方对于Arduino也是有着库函数支持的,那就是ROS Serial,我们可以在使用Arduino的同时,增加ROS的话题机制来实现数据的交互。   ​ 在机械臂的课程当中,大家所使用到的驱动板上面有个OLED屏幕,我们先来聊聊这个屏幕该怎么来使用,怎么在ROS下使用。   ​ 我在这里使用的是1.3寸IIC接口的OLED屏幕,大家在这里有个注意点,0.96寸的OLED屏幕使用的是SSD_1306的驱动,而1.3寸的OLED屏幕使用的确实SH_1106的驱动。这些驱动文件大家可以自及网上找到,或者在淘宝上面购买的时候找店家要相关的资料即可。我们先来看看实例程序下的屏幕该怎么来使用。  

使用ROS-Serial来驱动OLED屏幕

  ​ 我在这里使用的是1.3寸的OLED屏幕,使用IIC通讯,驱动为SH_1106。我的想法是这样的,我们在ROS下发布一个String类型的话题,这个话题通过ROS-Serial被Arduino订阅到,然后Arduino把这个String类型的字符串通过IIC显示在OLED屏幕上面。   ​ 我们了可以先去淘宝或者github找下Arduino驱动SH_1106的libraries,这个方法比你在百度找资料要高效的很多,当然学习教程什么的,我还是建议你去百度寻找相关的资料。我是直接在github搜索sh_1106的,这个办法很有用。  
一起来聊聊ROS-Serial和Arduino吧 ​ 大家可以找的很多资料,使用哪个大家一定要自己去筛选一下,基本上OLED的话Adafruit的还是挺好使的。大家下载一下这个Adafruit_SH1106并且放到自己Arduino的libraries路径下,打开我们的Arduino IDE,就可以从example里面找到它的实例程序。我们先来看下示例程序的内容。  
一起来聊聊ROS-Serial和Arduino吧 ​ 由于内容过多,我只截取部分代码。大家在这里可以看到,我们通过clearDisplay进行清屏,通过display进行显示,在显示和清屏之间,我们加入我们要显示的内容,比如通过setTextSize设置字体大小、通过setTextColor设置背光情况、通过setCursor设置显示的位置、通过println设置显示的内容等等。当然,我们需要声明OLED在使用IIC时候的总线地址,这个是必须的。   ​ 我在这里为大家提供一个Arduino的代码,这个代码实现的功能是订阅一个叫做oled的话题,话题类型是String,将话题的内容在OLED屏幕上面进行显示的功能。我们来看看代码。  

#include <ros.h>    //引用ros头文件
#include <std_msgs/String.h>    //引用std_msgs的String类型的数据

#include <Wire.h>    //引用IIC协议库
#include <Adafruit_GFX.h>    //引用字体取模库
#include <Adafruit_SH1106.h>    //引用SH1106驱动库,1.3寸OLED驱动时SH1106

#define OLED_RESET 4    //定义OLED为4针引脚模式

Adafruit_SH1106 display(OLED_RESET);    //实例化OLED显示功能

ros::NodeHandle nh;    //声明ros句柄

/********************************************************
*函数名称:oled_callback                                 *
*函数类型:void                                         *
*函数功能:当订阅到oled的话题,在OLED屏幕显示订阅到的msg      *
*输入参数:msg,订阅到的数据                            *
*输出参数:无                                            *
*********************************************************/
void oled_callback(const std_msgs::String& msg)
{
  display.clearDisplay();    //清空当前屏幕内容
  display.setTextSize(1);    //设置显示字体的大小
  display.setTextColor(WHITE);    //设置显示字体的样式
  display.setCursor(0,0);    //设置显示字体的坐标
  display.println(msg.data);    //设置显示字体的内容
  display.display();    //显示
}

//实例化订阅,订阅的话题名为oled,话题消息类型为std_msgs::String,回调函数为oled_callback
ros::Subscriber<std_msgs::String> sub("oled", &oled_callback );    

/**************************
*函数名称:setup             *
*函数类型:void             *
*函数功能:初始化相关硬件    *
*输入参数:无                *
*输出参数:无                *
**************************/
void setup()
{
  display.begin(SH1106_SWITCHCAPVCC, 0x3C);    //初始化OLED屏幕,IIC默认地址0x3C

  display.display();    //显示
  delay(2000);    //延时2000ms
  display.clearDisplay();    //清空当前屏幕内容

  display.setTextSize(1);    //设置显示字体的大小
  display.setTextColor(WHITE);    //设置显示字体的样式
  display.setCursor(0,0);    //设置显示字体的坐标
  display.println("Hello, ROS!");    //设置显示字体的内容
  display.display();    //显示
  delay(2000);    //延时2000ms
  display.clearDisplay();    //清空当前屏幕内容

  nh.initNode();    //初始化节点
  nh.subscribe(sub);    //订阅
}

/*********************************
*函数名称:loop                    *
*函数类型:void                    *
*函数功能:程序执行内容,循环执行    *
*输入参数:无                       *
*输出参数:无                       *
**********************************/
void loop()
{
  nh.spinOnce();    //在Arduino的loop函数,调用nh.spinOnce(),这样所有的ROS回调函数就会被处理
  delay(1);    //延时1ms
}

​ 代码由三部分组成,我们先来聊聊setup函数。这块是我们初始化的代码,大家可以看到我先是声明了OLED屏幕所在的IIC的地址0x3C,紧跟着控制屏幕进行一次清屏和显示,在这次清屏和显示之间并没有任何要显示的内容,所以屏幕就不会由什么显示。然后是设定字体大小为1,字体为白色(即白色凸显,大家搞个OLED玩一下就知道了),从(0,0)坐标开始显示,要显示的内容为“Hello ROS!”,然后通过display进行显示。显示两秒后,进行清屏,最后是初始化我们的ROS节点。这里有个问题,可能会让大家以为是bug,在最后我们执行的clearDisplay当中,大家会发现屏幕并没清屏,但是随着订阅到的内容的显示,却可以正常显示。而且我们在开始的使用也用了一组display和clearDisplay,这是为什么呢?欢迎大家留言评论做出自己的理解,我们也会在下一篇文章为大家答疑。   ​ 然后是我们的loop部分,这部分在Arduino里面是要循环执行的,其实也就是我们C++实现ROS通讯时候是一样的效果。重要的地方在这里。  

//实例化订阅,订阅的话题名为oled,话题消息类型为std_msgs::String,回调函数为oled_callback
ros::Subscriber<std_msgs::String> sub("oled", &oled_callback );

​ 我们通过ros节点示例化一个Subscriber类的变量sub,它的类型是std_msgs::String(即std_msgs下的String类型)。它有两个参数,第一个是订阅的话题的名称,我们在这里订阅的话题叫做oled;第二个是订阅到这个话题时候所执行的函数,也就是回调函数。这里和我们在C++写ROS功能包有个区别,就是没有声明当前的节点名称,这个也是我当时懵逼的一点,这是因为我们在ROS下运行的ROS-Serial的时候,节点是由ROS-Serial功能包声明的,叫做serial_node。其实Arduino终归还是一块单片机,我们只是按照ROS的代码格式来写实现了ROS,在这当中ROS-Serial功能包和ros.h等文件已经为我们处理好了大量的工作。   ​ 在oled_callback里面,我们传入的参数是const std_msgs::String& msg,而msg的data内容,就是我们需要显示的。在ROS下,订阅的时候数据都在data里面,这个比较“梗”的问题,也是初学者比较容易困惑的。   ​ 接下来的任务就简单了!我们调用OLED屏幕显示这一块的功能,把msg的data变量在屏幕显示,就可以实现功能。我们将成宿下载好,运行roscore和rosserial节点,发布一个oled的话题,后面跟着字符串,就可以实现功能。   ​ 最最最后,咳咳,拿到我机械臂板子的小伙伴应该直接尝试了,但是悲剧的事情来了~   ​ Arduino Nano的内存不够,程序烧录不进去,这简直是何等的卧槽~   ​ 当我的发现这个Bug的时候,我的内心已经无法用言语来形容和描述了~  
一起来聊聊ROS-Serial和Arduino吧 ​ 当然,这个程序在Arduino Mega上面使用还是没问题的。我猜测可能是ROS的库+字体库导致的内存不足,也有可能是其他的原因。  

使用ROS-Serial来驱动机械臂

  ​ 课程最初的设计就是使用Arduino Nano来搭建一个低成本的ROS Moveit的机械臂,在这里我也建议大家买总线舵机试试自己写一下驱动(也可以在古月居后台回复获取资料,我基于幻尔的机械臂做了ROS的驱动功能包)。在这里,我们逐步来实现使用ROS-Serial来驱动我们的机械臂。   ​ 在机械臂课程当中我们提到,我们需要来订阅一个joint_states的话题,通过里面的position获取当前joint的弧度值,最后由舵机执行。这是我写的Arduino驱动代码。  

#include "Servo.h"    //引用Arduino舵机驱动头文件

#include <ros.h>    //引用ros头文件
#include <sensor_msgs/JointState.h>    //引用sensor_msgs的JointState类型的数据

ros::NodeHandle nh;    //声明ROS句柄

//实例化舵机对象
Servo joint1;
Servo joint2;
Servo joint3;
Servo joint4;
Servo joint5;
Servo joint6;

/****************************************************************************
*函数名称:arm_callback                                                        *
*函数类型:void                                                                *
*函数功能:当订阅到joint_states话题时,将该话题的positio数据内容给舵机执行    *
*输入参数:data,当前订阅到的joint_states的内容                                *
*输出参数:无                                                                *
****************************************************************************/
void arm_callback( const sensor_msgs::JointState& data )
{    
    //joint_states的position内容是对应joint的弧度值,position的0对应我们机械臂的90°
    //-1.57~1.57(弧度值)对应0°~180°(角度值),所以有了下面的计算
    //由于计算的结构取整可能会超过0~180的范围,所以用了if...else if ...

    int joint1_angle = data.position[0]*360/6.28+90;
    if(joint1_angle<0)
        joint1_angle = 0;
    else if(joint1_angle>180)
        joint1_angle = 180;
    joint1.write(joint1_angle);

    int joint2_angle = data.position[1]*360/6.28+90;
    if(joint2_angle<0)
        joint2_angle = 0;
    else if(joint2_angle>180)
        joint2_angle = 180;
    joint2.write(joint2_angle);

    int joint3_angle = data.position[2]*360/6.28+90;
    if(joint3_angle<0)
        joint3_angle = 0;
    else if(joint3_angle>180)
        joint3_angle = 180;
    joint3.write(joint3_angle);

    int joint4_angle = data.position[4]*360/6.28+90;
    if(joint4_angle<0)
        joint4_angle = 0;
    else if(joint4_angle>180)
        joint4_angle = 180;
    joint4.write(joint4_angle);

    int joint5_angle = data.position[5]*360/6.28+90;
    if(joint5_angle<0)
        joint5_angle = 0;
    else if(joint5_angle>180)
        joint5_angle = 180;
    joint5.write(joint5_angle);

    int joint6_angle = data.position[6]*360/6.28+90;    
    if(joint6_angle<0)
        joint6_angle = 0;
    else if(joint6_angle>180)
        joint6_angle = 180;
    joint6.write(joint6_angle);
}

//实例化订阅,订阅的话题名为joint_states,话题消息类型为sensor_msgs::JointState,回调函数为arm_callback
ros::Subscriber<sensor_msgs::JointState> sub("joint_states", &arm_callback );

/****************************
*函数名称:setup            *
*函数类型:void                *
*函数功能:初始化相关硬件    *
*输入参数:无                *
*输出参数:无                *
****************************/
void setup()
{
  nh.initNode();    //初始化节点
  nh.subscribe(sub);    //订阅

  joint1.attach(3);    //joint1在D3引脚
  joint2.attach(5);    //joint2在D5引脚
  joint3.attach(6);    //joint3在D6引脚
  joint4.attach(9);    //joint4在D9引脚
  joint5.attach(10);    //joint5在D10引脚
  joint6.attach(11);    //joint6在11引脚

  pinMode(13,OUTPUT);    //设置D13引脚为输出模式

  //D13引脚在Arduino上面默认有个LED
  //在我们的驱动板当中D13有连接了一个蜂鸣器
  //这里的功能是开机提示
  digitalWrite(13,HIGH);
  delay(500);
  digitalWrite(13,LOW);
  delay(500);

  digitalWrite(13,HIGH);
  delay(500);
  digitalWrite(13,LOW);
  delay(500);

  digitalWrite(13,HIGH);
  delay(500);
  digitalWrite(13,LOW);
  delay(500);
}

/************************************
*函数名称:loop                        *
*函数类型:void                        *
*函数功能:程序执行内容,循环执行    *
*输入参数:无                        *
*输出参数:无                        *
************************************/
void loop()
{
  nh.spinOnce();    //在Arduino的loop函数,调用nh.spinOnce(),这样所有的ROS回调函数就会被处理
  delay(1);    //延时1ms
}

​ 关于setup和loop的内容我就不再重复的叙述,我们直接来看订阅的初始化部分和回调函数。  

//实例化订阅,订阅的话题名为joint_states,话题消息类型为sensor_msgs::JointState,回调函数为arm_callback
ros::Subscriber<sensor_msgs::JointState> sub("joint_states", &arm_callback );

​ 这里和我们上面的一样的,只是订阅的话题是joint_states,类型是sensor_msgs::JointState,回调函数为arm_callback。   ​ 然后是我们arm_callback回调函数的内容,如下。  

/****************************************************************************
*函数名称:arm_callback                                                        *
*函数类型:void                                                                *
*函数功能:当订阅到joint_states话题时,将该话题的positio数据内容给舵机执行    *
*输入参数:data,当前订阅到的joint_states的内容                                *
*输出参数:无                                                                *
****************************************************************************/
void arm_callback( const sensor_msgs::JointState& data )
{    
    //joint_states的position内容是对应joint的弧度值,position的0对应我们机械臂的90°
    //-1.57~1.57(弧度值)对应0°~180°(角度值),所以有了下面的计算
    //由于计算的结构取整可能会超过0~180的范围,所以用了if...else if ...

    int joint1_angle = data.position[0]*360/6.28+90;
    if(joint1_angle<0)
        joint1_angle = 0;
    else if(joint1_angle>180)
        joint1_angle = 180;
    joint1.write(joint1_angle);

    int joint2_angle = data.position[1]*360/6.28+90;
    if(joint2_angle<0)
        joint2_angle = 0;
    else if(joint2_angle>180)
        joint2_angle = 180;
    joint2.write(joint2_angle);

    int joint3_angle = data.position[2]*360/6.28+90;
    if(joint3_angle<0)
        joint3_angle = 0;
    else if(joint3_angle>180)
        joint3_angle = 180;
    joint3.write(joint3_angle);

    int joint4_angle = data.position[4]*360/6.28+90;
    if(joint4_angle<0)
        joint4_angle = 0;
    else if(joint4_angle>180)
        joint4_angle = 180;
    joint4.write(joint4_angle);

    int joint5_angle = data.position[5]*360/6.28+90;
    if(joint5_angle<0)
        joint5_angle = 0;
    else if(joint5_angle>180)
        joint5_angle = 180;
    joint5.write(joint5_angle);

    int joint6_angle = data.position[6]*360/6.28+90;    
    if(joint6_angle<0)
        joint6_angle = 0;
    else if(joint6_angle>180)
        joint6_angle = 180;
    joint6.write(joint6_angle);
}

​ 这部分的代码就是我在StandardFirmata实现的代码,只是在驱动执行的位置发生变化——由协议的write变成了Arduino的write。我们的数据还是data,只是在这里提的是data里面的position。  
一起来聊聊ROS-Serial和Arduino吧   ​ 咳咳,大家放心,这个代码是可以在Arduino Nano运行的~   ​ 所以我觉得可能就是OLED的字体库太大了,造成的内存不足~  

使用ROS-Serial来驱动MPU6050

  ​ 咳咳,最近开学了还没写,一定出一定出!!!   ​ 在接下来的时间里,我也会就Modbus的数据格式,来给大家继续聊聊ROS的驱动层开发。我从18年跟着古月老师的星火计划开始学习ROS,当时刚刚接触到树莓派和Linux系统,很多内容简直就是天书。我也很庆幸当时参加了星火计划的课程,从这里开始我的机器人生涯~   ​ 即是生涯,也是深渊。搞硬件的就是比较烧钱,这是无可避免的,大学期间为学习的投资才是最好的投资。当时也觉得ROS啃不下来,这不是我这个萌新该接触的等级,对于发展方向和即将要学习的内容一片迷茫~   ​ 当时看到了YY硕的一篇文章机器人工程师学习计划,个人认为这篇文章对现在正迷茫困惑、想要放弃的小伙伴来说,绝对是一盏指路明灯。机器人是一个综合性的学科,机械、电气、单片机、嵌入式、通讯、前端、后端、运维、人工智能,甚至还特别的吃数学和英语,正如同YY说说的,机器人学的东西至少是全栈开发(程序员里面牛掰的存在,可以自行百度这个职位)的三倍。你的方向可以是是机器人,但是你的就业方向可能不是机器人,但是有这些知识储备的你,一定存在着绝对的竞争力吧~ ​


开心洋葱 , 版权所有丨如未注明 , 均为原创丨未经授权请勿修改 , 转载请注明一起来聊聊ROS-Serial和Arduino吧
喜欢 (0)

您必须 登录 才能发表评论!

加载中……