基于marlin固件的SCARA机器人-Arduino中文社区 - Powered by Discuz!

Arduino中文社区

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 36480|回复: 22

[项目] 基于marlin固件的SCARA机器人

[复制链接]
发表于 2017-8-28 11:43 | 显示全部楼层 |阅读模式
本帖最后由 YANGGT 于 2017-8-28 11:55 编辑


其实这个项目很早就在做了,刚注册了Arduino社区,看到这个比赛就拿来水一下:
附件好像只能上传1M的,所有文件到这里下载吧:https://www.thingiverse.com/thing:2339480。DSR.zip里面包含了全部资料。

1.关于marlin和SCARA
   先简单介绍一下marlin和SCARA机器人
   marlin固件是一个开源的3d打印机固件,除了支持普通的xyz结构,还支持极坐标结构,机械臂结构的3d打印机。详细的介绍可以看这里:http://reprap.org/wiki/List_of_Firmware#Marlin
   SCARA的本意是选择顺应性装配机器手臂,不过现在都习惯把平面关节结构的机器人叫做SCARA,SCARA总共有4个自由度(也可以是3个),二连杆+一个平动的Z轴+手腕,结构简单。

2.图纸和装配
  设计源文件是Solidworks2016的,如果是用的是低版本的或者其他cad软件,通用格式文件夹下有.stp和.igs格式的装配图。详细的装配过程我就不发了,说说一些注意事项和装配中遇到的问题@_@(都是因为设计缺陷产生的)。
  1.部分模型文件根据3D打印的误差预留了余量(不同的机子可能会有点不一样)
  2.主臂体积比较大,一次打印容易打弯,导致机械臂不平行,所以加入了一个拆分版的,两部分用销钉连接。
  3.文件中包含两种执行机构,一是手爪,二是笔夹,都是用销钉和副臂相连,文件中的笔夹孔径为20。也可以根据需要自己设计执行机构。
  4.主臂和副臂的限位开关没有固定的位置,选择不会干涉的地方沾好。
  5.主臂(X)的减速比是6,臂长125mm。副臂(Y)的减速比是5,臂长125mm。
  6.底座用4mm的板子切,底座上有两个孔是放磁铁的,用于和平台吸合,顶端有个开口,用来引出ramps主板上的拓展接口的
  7.同步带使用开口的,两端是固定在大同步轮上的(大同步轮是没有齿的),张紧可能会有些困难。
  8.光轴开始我用的是250mm的,后来发现晃得厉害,改成150的了。
  9.最开始用的是直线轴承,用久了发现会有间隙,推荐用铜套。
  10.设计的时候没考虑布线,所以布线就。。。
  11.设计的不好,仅供学习和参照。
psb6.jpg
psb2.jpg
psb3.jpg
psb4.jpg
psb5.jpg
P70522-201317.jpg
3.接线
  主板用的是ramps1.4+arduino2560,常规的接线图网上可以找到很多了,这里讲讲怎么使用舵机和蓝牙模块
ramps接线.jpg
首先如果买的arduino2560质量较差,稳压芯片用的是ams1117的话,建议把ramps的二极管D1去掉,否则容易烧板子。不过这样ramps和2560就需要单独供电,想要同时供电的话,可以用一个稳压模块引过去。
因为二极管被去掉了,单靠usb的电是带不动2个舵机的,所以要在图中外接入5v。舵机从右到左依次为ABC,用(G5 A角度 B角度 C角度)控制, 手腕一定要接在A上,手爪的话随意。
串口通讯如图,可以接蓝牙或者串口屏,波特率是115200。
120856_IO0O_3503462.png
4.固件和调试
  固件基于marlin,做了部分修改,详细的笔记可以看这里https://my.oschina.net/u/3503462/blog/914027
  固件在配置中启用了3d打印机的屏幕,确保arduino ide已经有u8glib库。因为启用了这个功能,所以可能会出现不能编译的问题(我在arduino1.6.5版本下编译成功,但1.6.13就不行),如果编译不了,可以在配置中注释掉再试试。

调试方法:
上位机软件用3d打印机的就行,推荐用printrun
用于设置的G代码说明(使用大写)

M360 触发限位角度设置
M361 原点偏移设置 (中心转轴相对机械臂原点的坐标位置)
G5 舵机角度控制 (G5 A角度 B角度 C角度)

G4 延迟 (G4 S时间(秒))
G28 回归所有轴(home)
G92 当前坐标设定(G92 X坐标 Y坐标 Z坐标把当前机械手的位置设置成想要的坐标)
G95 切换绝对/相对坐标(相对坐标就是控制单独一轴的角度,切换后一定要home)

M17 启动步进电机
M84 解锁步进电机
M92 设置步进电机脉冲数 (M92 X? Y? Z? E?)下同) M105 获取温度
M112 紧急停止
M114 获取当前坐标和角度
M119 获取限位开关状态
M201 设置最大打印速度
M202 设置最大移动速度
M203 设置电机最大速度
M204 设置默认加速度
M500 保存设置到EEPROM(使用G代码设置如果没有保存到EEPROM,下次连接机械手的时候不再有效)
M501 读取EEPROM中的配置
M502 恢复出厂设置
M503 读取出厂设置

调试方法:触发限位开关的角度大致为笔记相对坐标定义图中的姿态3,在不干涉的前提下安装好限位开关

1.连接
2.M502恢复出厂设置
3.G28 home,检查是否正常。
4.G95 切换到角度模式,再G28,检查角度模式是否正常。
5.G28,home M360 X0 Y180 先假定当前角度
6.分别移动XY轴到0角度,读取当前坐标(X轴0角度为主臂与平台平行,Y轴0角度为副臂与主臂成直线)
7.X轴限位角度即负的坐标值,Y即180减坐标值,用M360 X? Y?设置限位角度, M500保存
8.使用G95可以把机械手的任意位置设置成想要的坐标。G28,移动机械手到想要设置坐标的位置,G95 X0 Y0 Z0就把现在的位置设置成原点,如果再输入M500,此位置就永久地保存为原点,否则只对此次连接有效。

使用方法画画教程使用激光刀路生成工具

1.导入图片后生成G代码
2.用记事本把所有的M03替换成G1 Z0 M05换成 G1 Z2-5(提笔高度)
3.由于不同的笔高度不一,因此需使用G95设置Z轴的原点。home后下降Z轴,直至刚好接触纸面,G95 Z0,如果一直使用这把笔可以用M500保存,这样下次就不用设置了。

写字教程(矢量图绘制)使用inkspace以及激光雕刻插件某旧版的inkspace才支持此插件

1.用cad绘制单线字体,由于不支持圆弧插补,所以要导出为图元文件(.sat)
2.导入到inkspace,使用插件里面的laser...(激光雕刻插件),页面中可以更改保存路径
3.找到保存的.nc文件,后续步骤同上。

3D打印教程

1.设置合适位置为XYZ轴原点
2.用切片软件生成G代码,并用记事本把前面十几行中的G28去掉

测试

测试

psb8.jpg

5.processing示教上位机
根据G代码结合python/processing可以给机器跟编程。下面是我用processing写的一个示教上位机。机械手的XY坐标由鼠标在白板中画画得到,Z轴坐标用鼠标滚轮控制,MOVE_UNIT是Z轴的移动单位(mm)teach开始示教,save保存gcode,鼠标右键清空屏幕。
20170828095816.png
[mw_shl_code=C,true]import processing.serial.*;
float x,y,z =100,a=90; //储存xyz坐标,以及第四轴角度
float unit =10;//移动单位
boolean teach =false;//示教模式
float e;//滚轮事件变量
String[] step =new String[1000];
String[] output;
int i=0;
Serial Port;
void setup()
{
  frameRate(15);
  String portName = Serial.list()[0];
  Port = new Serial(this, portName, 115200);//串口
  size(1000,650);//屏幕尺寸
  smooth();
  background(#FFFFFF);//背景色
  fill(#DBDBDB);//提示栏颜色
  noStroke();
  rect(0,0,1000,50);
  fill(#FFFFFF);
  textSize(30);
  text("MOVE_UNIT",10,35);//UI界面
  //text("save",815,35);
}

void draw()
{  
noStroke();
/*---------------------键盘事件------------------*/
if(keyPressed==true)
   {
     if(key=='h'||key=='H')//H键回零所有轴
      {
       Port.write("G28\r\n");
       x=y=0;
       z=100;
      }
     if(key=='x'||key=='X')//X键回零X轴
       {
         Port.write("G28 X0\r\n");
         x=0;
       }
     if(key=='y'||key=='Y')//Y键回零Y轴
       {
         Port.write("G28 Y0\r\n");
         y=0;
       }
     if(key=='z'||key=='Z')//Z键回零Z轴
       {
         Port.write("G28 Z0\r\n");
         z=150;
       }
     
      if((key=='a'||key=='A')&&a<=180+unit)//Z键回零Z轴
      {
         a=a+unit;
         Port.write("G5 A"+a+"\r\n");
        if(teach==true)
        {
         step="G5 A"+a;
         i++;
        }
       }
       if((key=='d'||key=='D')&&a>=0+unit)//Z键回零Z轴
       {
         a=a-unit;
         Port.write("G5 A"+a+"\r\n");
         if(teach==true)
        {
         step="G5 A"+a;
         i++;
        }
       }
        if(key=='w'||key=='W')//Z键回零Z轴
       {
         Port.write("G5 B60\r\n");
         if(teach==true)
        {
          step="G5 B60";
          i++;
        }
       }
       if(key=='s'||key=='S')//Z键回零Z轴
       {
         Port.write("G5 B150\r\n");
        if(teach==true)
        {
          step="G5 B150";
          i++;
        }
       }
   
     
     if(key==CODED)//编码键
       {
         if(keyCode==ALT)//清空屏幕
         {
            fill(#FFFFFF);
            noStroke();
            rect(0,50,1000,600);
         }
       }   
   }
/*-----------------------------------------------*/


/*---------------------鼠标事件------------------*/   
if(mouseButton==LEFT&&mouseY>50) //鼠标左键
{
   stroke(0,102);
   strokeWeight(3);     
   line(mouseX,mouseY,pmouseX,pmouseY);
   x=mouseX/4;
   y=(height-mouseY)/4;
            
   if(teach==true)//示教模式
   {
      
       Port.write("G1 X"+x+'Y'+y+"F12000\r\n"); //拖动鼠标移动并绘制
       step="G1 X"+x+'Y'+y+"F1200";
       i++;
   }
   else
   {
      Port.write("G1 X"+x+'Y'+y+"F12000\r\n"); //拖动鼠标移动并绘制
   }   
}

/*-----------------------------------------------*/
if((mouseX>900)&&(mouseX<1000)&&(mouseY>0)&&(mouseY<50))//save
  {
    fill(#DBDBDB);
    rect(900,0,100,50);
    fill(#FFFFFF);
    text("save",915,35);
  }
   
}
void mouseWheel(MouseEvent event)//滚轮事件,移动Z轴
{
  e = event.getCount();
  if(e==1&&z>0+unit)
     z=z-unit;
   else if(e==-1&&z<150+unit)
     z=z+unit;
   Port.write("G1 Z"+z+"F1200\r\n");
   if(teach==true)//示教模式
   {
       step="G1 Z"+z+"F1200";
       i++;
   }
  //println(z);
}
/*---------------------UI------------------*/
void mouseClicked()//UI按钮
{
  noStroke();
     
  if((mouseX>200)&&(mouseX<260)&&(mouseY>0)&&(mouseY<50))
     {
       fill(#DBDBDB);
       rect(250,0,55,50);
       rect(300,0,55,50);
       rect(350,0,55,50);
       fill(#FFFFFF);
       text("10",257,35);
       text("1",315,35);
       text("0.1",357,35);
       rect(200,0,55,50);
       fill(#DBDBDB);
       text("20",207,35);
       unit=20;  //  控制单位20
      }
  if((mouseX>250)&&(mouseX<310)&&(mouseY>0)&&(mouseY<50))
      {
        fill(#DBDBDB);
        rect(200,0,55,50);
        rect(300,0,55,50);
        rect(350,0,55,50);
        fill(#FFFFFF);
        text("20",207,35);
        text("1",315,35);
        text("0.1",357,35);
        rect(250,0,55,50);
        fill(#DBDBDB);
        text("10",257,35);
        unit=10;  //10
        }
  if((mouseX>300)&&(mouseX<360)&&(mouseY>0)&&(mouseY<50))
     {
       fill(#DBDBDB);
       rect(200,0,55,50);
       rect(250,0,55,50);      
       rect(350,0,55,50);
       fill(#FFFFFF);
       text("20",207,35);
       text("10",257,35);
       text("0.1",357,35);
       rect(300,0,55,50);
       fill(#DBDBDB);
       text("1",315,35);
       unit=1;  //1
       }
  if((mouseX>350)&&(mouseX<410)&&(mouseY>0)&&(mouseY<50))
      {
         fill(#DBDBDB);
        rect(200,0,55,50);
        rect(250,0,55,50);
        rect(300,0,55,50);
        fill(#FFFFFF);
        text("20",207,35);
        text("10",257,35);
        text("1",315,35);
        rect(350,0,55,50);
        fill(#DBDBDB);
        text("0.1",357,35);
        unit=0.1; //0.1
      }
  if((mouseX>800)&&(mouseX<900)&&(mouseY>0)&&(mouseY<50))//示教模式切换
  {                                                                                                            
    teach=!teach;
  }
  if((mouseX>900)&&(mouseX<1000)&&(mouseY>0)&&(mouseY<50))//save
  {
    fill(#FFFFFF);
    rect(900,0,100,50);
    fill(#DBDBDB);
    text("save",915,35);
    output =new String;
    for(int j=0;j<i;j++)
    {
      output[j]=step[j];
    }
    saveStrings("teach.gcode",output);
  }
  if(teach==false)//绘制示教模式按钮
  {
    fill(#DBDBDB);
    rect(800,0,100,50);
    fill(#FFFFFF);
    text("teach",815,35);
  }
  else
  {
    fill(#FFFFFF);
    rect(800,0,100,50);
    fill(#DBDBDB);
    text("teach",815,35);
  }
/*-----------------------------------------------*/
  //println(teach);
  println(a);
}[/mw_shl_code]

6.processing简单视觉抓取

我在手爪上装了个摄像头,用prcessing写了一个识别红绿蓝色块并顺序抓取的程序。演示过程可以看最后的视屏,摄像头的焦距有点不对,导致看到的范围很小,抓取的范围也很小。。
[mw_shl_code=c,true]import processing.serial.*;
import processing.video.*;

Serial Port;
Capture cam;
float[][] kernel ={{0.111,0.111,0.111},
                   {0.111,0.111,0.111},
                   {0.111,0.111,0.111}
                   };//卷积核
//中间像素的灰度值等于周围像素的红色分量减去蓝绿色分量的平均值
//从而使没有红色特征的像素灰度值变为零
//相当于滤镜的效果,修改卷积核的参数,可以达到不一样的效果
int sub_x=0,sub_y=0;//物体中心的像素坐标
float pre_x=0,pre_y=0;//前一帧物体中心的像素坐标
float tar_x=80,tar_y=80;//目标坐标,机械手的坐标
boolean event=false;//坐标计算事件
int c=0;


void setup()//初始化
{
  String portName = Serial.list()[0];
  Port = new Serial(this, portName, 115200);//连接串口
  size(640,480);//屏幕大小
  String[] cameras = Capture.list();
  printArray(cameras);//打印可使用的相机
  cam = new Capture(this, cameras[0]);
  cam.start();//启动相机
  frameRate(30);//帧率
  Port.write("G5 B90\r\n");
  Port.write("G28\r\n");
  Port.write("G1 X80 Y80 Z40\r\n");
  Port.write("G5 B50\r\n");//移动到初始位置
  delay(6000);//延迟,避免摄像头刚打开时的波动
}

void draw()//循环
{
  if (cam.available() == true)
  {
    cam.read();
    image(cam, 0, 0, width, height);
    cam.loadPixels();
    int sum_x=0,sum_y=0;//所有红色像素坐标的和
    int m=1;//红色像素的个数
    for(int y=1;y<cam.height-1;y++)//排除第一行、最后一行、第一列、最后一列像素(这几个地方周围的像素不完整)
    {
      for(int x=1;x<cam.width-1;x++)//经过所有像素
      {
        float sum=0;//最终的灰度值
        for(int ky =-1;ky<=1;ky++)
        {
          for(int kx=-1;kx<=1;kx++)//周围9个像素(包括自身)
          {
          int pos =(y+ky)*cam.width+(x+kx);//将xy坐标换算成pixels[]数组中的位置,pixels[]数组是可以直接调用的储存像素的一维数组(从左到右从上到下)
          int R = (cam.pixels[pos]>> 16) & 0xFF; //取颜色分量,与red()功能相似
          int G = (cam.pixels[pos] >> 8) & 0xFF;
          int B = cam.pixels[pos] & 0xFF;
          float val=0;
          switch(c)//顺序抓取红蓝绿色块
          {
            case 0:
              val =R-1.5*B-1.5*G;
              break;
            case 1:
              val =B-1.5*R-1.5*G;
              break;
            case 2:
              val =G-0.5*B-1*R;
              break;
          }
             //1.5为比例系数,改大可以让红色更突出
          sum +=kernel[ky+1][kx+1]*val;//计算灰度值
          }
        }
        if(sum>0)//新的灰度值非零
        {
          sum_x=sum_x+x;
          sum_y=sum_y+y;//累加坐标
          m=m+1;//计数
        }
       }
     }
   pre_x=sub_x;
   pre_y=sub_y;//前一帧的物体中心
   sub_x=sum_x/m;
   sub_y=sum_y/m;//取平均,计算出物体中心像素坐标
   
   cam.updatePixels();

    fill(#FF0000);
  }
  line(sub_x,0,sub_x,480);
  line(0,sub_y,640,sub_y);//作物体中心的两条交叉线

  if(abs(sub_x-pre_x)<5&&abs(sub_y-pre_y)<5&&sub_x>5&&sub_y>5)//判断是否在移动(波动小于5个像素即为静止),是否存在物体(没有物体时默认为0,为避免波动,这里设置成5)
  {
    if(event==true)//如果物体存在,机械手静止,且事件被触发,则进行计算
    {
     tar_x=tar_x-(0.0625*sub_x-20);
     tar_y=tar_y-(15-0.0625*sub_y);//把像素坐标转化成绝对坐标,根据实际比例
     event=false;//关闭计算事件,即只执行一次计算
     Port.write("G1 X"+tar_x+" Y"+tar_y+"\r\n");//移动到目标位置
     delay(100);//延迟,减小波动
    }
  }
  else //如果机械手在移动,触发计算事件,但不执行计算,为机械手静止时计算做准备
   event=true;
    /*由于机械手的通讯以及动作上有延迟,速度远赶不上摄像头对图像识别的速度,
    所以仅让机械臂在静止时进行一次计算*/
  if(sub_x<5&&sub_y<5)
  {
    if(c<3)
      c=c+1;
     else
       c=0;
  }
  float mx=tar_x;
  float my=tar_y+32;//手抓中心对的坐标,摄像头和手抓有32的y偏移量
  if(sub_x>315&&sub_x<325&&sub_y>235&&sub_y<245)//当物体在画面中心时(+-5个像素)
  {
     //执行以下动作
     Port.write("G1 X"+mx+'Y'+my+"Z12 F1200\r\n");
     delay(1500);//充分延时
     for(int i=50;i<=150;i+=5)
     {
       Port.write("G5 B"+i+"\r\n");
       delay(50);
     }
     
     delay(500);
     Port.write("G1 Z30\r\n");
     delay(1000);
     switch(c)//顺序摆放红蓝绿色块
     {
       case 0:
         Port.write("G1 X150 Y120\r\n");break;
       case 1:
         Port.write("G1 X150 Y100\r\n");break;
       case 2:
         Port.write("G1 X150 Y80\r\n");break;
     }
     delay(1500);
     Port.write("G1 Z10\r\n");
     delay(500);
     for(int j=150;j>=50;j-=5)
     {
       Port.write("G5 B"+j+"\r\n");
       delay(50);
     }
     delay(500);
     Port.write("G1 X80 Y80 Z40\r\n");
     delay(1500);
    if(c<3)
      c=c+1;
     else
       c=0;
   }
  
  println(tar_x);
  println(tar_y);
}[/mw_shl_code]
6.演示视频
 楼主| 发表于 2017-11-21 12:29 | 显示全部楼层
感谢arduino中文社区,哈哈
发表于 2017-10-19 23:17 | 显示全部楼层
1.感谢对社区比赛的支持,开发者积分和贡献值已发放,请点击以下链接领取纪念衫并参与抽奖~
http://www.arduino.cn/thread-48132-1-1.html
2.比赛结果会在11.15号前公布,请耐心等待。
发表于 2017-9-9 19:20 | 显示全部楼层
你好。可以加个好友吗。我照你的方法SCARA机器人,但是调试不出来。用角度能正常运行。但坐标就乱跑了。好像方向都跑反的还有设原点是用G95 X0 Y0 Z0?发过去没反应,G92好像可以。菜鸟真心不累。
发表于 2017-9-9 19:24 | 显示全部楼层
分别移动XY轴到0角度,读取当前坐标(X轴0角度为主臂与平台平行,Y轴0角度为副臂与主臂成直线)这是不是就是姿态2.机械臂在中间。一条直线
 楼主| 发表于 2017-9-9 19:36 | 显示全部楼层
kiss520 发表于 2017-9-9 19:24
分别移动XY轴到0角度,读取当前坐标(X轴0角度为主臂与平台平行,Y轴0角度为副臂与主臂成直线)这是不是就 ...

看资料吧,情况有很多,我没办法根据描述判断
发表于 2017-9-9 19:38 | 显示全部楼层
绝对坐标相对假定坐标的偏移量      是不是这个没设好?加个QQ39799049大师
 楼主| 发表于 2017-10-19 23:35 | 显示全部楼层
syl312 发表于 2017-10-19 23:17
1.感谢对社区比赛的支持,开发者积分和贡献值已发放,请点击以下链接领取纪念衫并参与抽奖~
http://www.ard ...

谢谢
发表于 2017-11-26 22:18 来自手机 | 显示全部楼层
请问能看看资料吗?
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|Archiver|手机版|Arduino中文社区

GMT+8, 2024-9-20 20:31 , Processed in 0.121808 second(s), 19 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表