美文网首页
arduino开源项目——墙画机器人

arduino开源项目——墙画机器人

作者: 猛犸象和剑齿虎 | 来源:发表于2020-05-04 11:00 被阅读0次

    由于有的项目原作者限制了转载和引用,所以不便于公开整个制作过程,而这个项目是个开源的项目,所以可以当做学习笔记来记录。

    墙画机原理

    通过两个步进电机控制线轴转动,放线和收线来控制笔的移动位置,将这种位置转换成坐标,通过坐标定位放线和收线来绘制想要的图形。

    定位方法原理

    三角函数

    刚看的时候发现忘完了三角函数,于是看了下基本概念,并做了简单的推导。


    image.png

    1.求C的坐标(C为笔的位置)
    AC长度=b,CB长度=a,AB长度=c,A/B对称于y坐标,CD为辅助线,垂直于AB。
    解:设AD长为x,则BD为c-x
    根据勾股定理:
    bb-xx=aa-(c-x)(c-x)
    得:
    bb-xx=aa-(cc-2cx+xx)
    b
    b-xx=aa-cc+2cx-xx
    bb=aa-cc+2cx
    x=(b
    b+cc-aa)/2c
    那么:
    cos(A)=x/b=(bb+cc-aa)/2cb(余弦函数)
    设C坐标(X,Y)
    那么:
    X=cos(a)
    b-A0
    Y=sin(a)*b-D0
    已知AB为墙画机的两个轮盘的位置,以及轮盘距离中心点位置都是可以测量出来的,那么笔的位置坐标就确定下来了。
    2.已知C的坐标,求两条绳子的长度,也就是b和a的长度。
    相当于上面操作的逆运算。
    b=√(X+A0)2+(Y-D0)2(注意XY坐标可能为负数)
    a=√(X-A0)2+(Y-D0)2
    简单的勾股定理构成了墙画机的核心思路,通过收线放线控制a、b的长度,来确定坐标,通过坐标转换a、b来确定收线放线的长度,来进行绘画操作。

    收线、放线规则

    假如线的长度为无线长,那么不需要考虑两个步进电机的转动收线放线。
    1.每根线长度收线不能小于0。
    2.每根线的放线不能大于每根线的长度,否则会反向卷线。
    3.两线相加的长度不能少于AB的距离。

    步数转化

    已知步进电机2048步完成1周转动(360度),线轴(轮盘)直径35毫米,那么线轴的周长为35*π,步进电机一步的长度为35π/2048。
    那么就能将坐标转化成步进电机的步数了。

    抬笔落笔

    通过舵机简单控制。

    • (函数式编程和面向对象编程思路总觉得有点反逻辑。普遍代码是根据现有正向推导的顺序结构。而函数和面向对象总有种逆向思维的感觉。所以看到源码总是很难理解,很有可能作者是边写边封装,所以解读代码的时候,最重要的是抓住主干,否则很有可能陷入海量的变量和想的头疼都不得要领的境地。)
    #include <TinyStepper_28BYJ_48.h>
    #include <Servo.h>
    
    #include <SD.h>  //需要SD卡读卡器模块,或者tf读卡器模块
    
    
    
    //调试代码标志,去掉注释,可以输出调试信息(程序运行会慢)
    //#define VERBOSE         (1)
    //调试标志
    
    
    #define STEPS_PER_TURN  (2048)  //步进电机一周步长 2048步转360度
    #define SPOOL_DIAMETER  (35)    //线轴直径mm
    #define SPOOL_CIRC      (SPOOL_DIAMETER * 3.1416)  //线轴周长 35*3.14=109.956
    #define TPS             (SPOOL_CIRC / STEPS_PER_TURN)  //步进电机步距,最小分辨率 每步线绳被拉动的距离  0.0268447265625mm
    
    
    
    #define step_delay      1   //步进电机每步的等候时间 (微妙)
    #define TPD             300   //转弯等待时间(毫秒),由于惯性笔会继续运动,暂定等待笔静止再运动。
    
    
    //两个电机的旋转方向  1正转  -1反转  
    //调节进出方向可垂直反转图像
    #define M1_REEL_OUT     1     //放出线
    #define M1_REEL_IN      -1      //卷入线
    #define M2_REEL_OUT     -1      //放出线
    #define M2_REEL_IN      1     //卷入线
    
    
    static long laststep1, laststep2; //当前线长度 记录笔位置
    
    
    #define X_SEPARATION  507           //两绳上方的水平距离mm 
    #define LIMXMAX       ( X_SEPARATION*0.5)   //x轴最大值  0位在画板中心
    #define LIMXMIN       (-X_SEPARATION*0.5)   //x轴最小值
    
    /* 垂直距离的参数: 正值在画板下放,理论上只要画板够大可以无限大,负值区域在笔(开机前)的上方 
    详细介绍见说明文档 https://github.com/shihaipeng03/Walldraw
    */
    #define LIMYMAX         (-440)   //y轴最大值 画板最下方
    #define LIMYMIN         (440)    //y轴最小值 画板最上方  左右两线的固定点到笔的垂直距离,尽量测量摆放准确,误差过大会有畸变
                    //值缩小画图变瘦长,值加大画图变矮胖 
    
    
    
    //抬笔舵机的角度参数  具体数值要看摆臂的安放位置,需要调节
    #define PEN_UP_ANGLE    60  //抬笔
    #define PEN_DOWN_ANGLE  95  //落笔
    //需要调节的参数 =============================================
    
    
    #define PEN_DOWN 1  //笔状态  下笔
    #define PEN_UP 0    //笔状态  抬笔
    
    
    
    struct point { 
      float x; 
      float y; 
      float z; 
    };
    
    struct point actuatorPos;
    
    
    // plotter position 笔位置.
    static float posx;
    static float posy;
    static float posz;  // pen state
    static float feed_rate = 0;
    
    // pen state 笔状态(抬笔,落笔).
    static int ps;
    
    /*以下为G代码通讯参数 */
    #define BAUD            (115200)    //串口速率,用于传输G代码或调试 可选9600,57600,115200 或其他常用速率
    #define MAX_BUF         (64)      //串口缓冲区大小
    
    // Serial comm reception
    static int sofar;               // Serial buffer progress
    
    static float mode_scale;   //比例
    
    File myFile;
    
    Servo pen;
    
    TinyStepper_28BYJ_48 m1; //(7,8,9,10);  //M1 L步进电机   in1~4端口对应UNO  7 8 9 10
    TinyStepper_28BYJ_48 m2; //(2,3,5,6);  //M2 R步进电机   in1~4端口对应UNO 2 3 5 6
    
    
    
    
    
    //------------------------------------------------------------------------------
    //正向运动计算 - 将L1,L2长度转换为XY坐标
    // 使用余弦定律, theta = acos((a*a+b*b-c*c)/(2*a*b));
    //找到M1M2和M1P之间的角度,其中P是笔的位置
    void FK(float l1, float l2,float &x,float &y) {
      float a=l1 * TPS;
      float b=X_SEPARATION;
      float c=l2 * TPS;
      
       
      
      //方法1
      float theta = acos((a*a+b*b-c*c)/(2.0*a*b));
      x = cos(theta)*l1 + LIMXMIN;
      y = sin(theta)*l1 + LIMYMIN;          
    
      //方法2
    /*   float theta = (a*a+b*b-c*c)/(2.0*a*b);
      x = theta*l1 + LIMXMIN;
      y = sqrt (1.0 - theta * theta ) * l1 + LIMYMIN;*/
    }
    
    
    //------------------------------------------------------------------------------
    //反向运动 - 将XY坐标转换为长度L1,L2 
    void IK(float x,float y,long &l1, long &l2) {
      float dy = y - LIMYMIN;
      float dx = x - LIMXMIN;
      l1 = round(sqrt(dx*dx+dy*dy) / TPS);
      dx = x - LIMXMAX;
      l2 = round(sqrt(dx*dx+dy*dy) / TPS);
    }
    
    
    
    //------------------------------------------------------------------------------
    //笔状态
    void pen_state(int pen_st) {
      if(pen_st==PEN_DOWN) {
            ps=PEN_DOWN_ANGLE;
            // Serial.println("Pen down");
          } else {
            ps=PEN_UP_ANGLE;
            //Serial.println("Pen up");
          }
      pen.write(ps);
    }
    
    
    //
    void pen_down()
    {
      if (ps==PEN_UP_ANGLE)
      {
        ps=PEN_DOWN_ANGLE;
        pen.write(ps);
        delay(TPD);
      }
    
    }
    
    void pen_up()
    {
      if (ps==PEN_DOWN_ANGLE)
      {
        ps=PEN_UP_ANGLE;
        pen.write(ps);
      }
    
      
    }
    
    //------------------------------------------------------------------------------
    //调试代码串口输出机器状态
    void where() {
      Serial.print("X,Y=  ");
      Serial.print(posx);
      Serial.print(",");
      Serial.print(posy);
      Serial.print("\t");
      Serial.print("Lst1,Lst2=  ");
      Serial.print(laststep1);
      Serial.print(",");
      Serial.println(laststep2);
      Serial.println("");
    }
    
    
    
    //------------------------------------------------------------------------------
    // returns angle of dy/dx as a value from 0...2PI
    static float atan3(float dy, float dx) {
      float a = atan2(dy, dx);
      if (a < 0) a = (PI * 2.0) + a;
      return a;
    }
    
    
    //------------------------------------------------------------------------------
    //画圆弧
    static void arc(float cx, float cy, float x, float y,  float dir) {
      // get radius
      float dx = posx - cx;
      float dy = posy - cy;
      float radius = sqrt(dx * dx + dy * dy);
    
      // find angle of arc (sweep)
      float angle1 = atan3(dy, dx);
      float angle2 = atan3(y - cy, x - cx);
      float theta = angle2 - angle1;
    
      if (dir > 0 && theta < 0) angle2 += 2 * PI;
      else if (dir < 0 && theta>0) angle1 += 2 * PI;
    
      // get length of arc
      // float circ=PI*2.0*radius;
      // float len=theta*circ/(PI*2.0);
      // simplifies to
      float len = abs(theta) * radius;
    
      int i, segments = floor(len / TPS);
    
      float nx, ny, nz, angle3, scale;
    
      for (i = 0; i < segments; ++i) {
    
        if (i==0) 
          pen_up();
        else
          pen_down();  
        scale = ((float)i) / ((float)segments);
    
        angle3 = (theta * scale) + angle1;
        nx = cx + cos(angle3) * radius;
        ny = cy + sin(angle3) * radius;
        // send it to the planner
        line_safe(nx, ny);
      }
    
      line_safe(x, y);
      pen_up();
    }
    
    
    
    //------------------------------------------------------------------------------
    // instantly move the virtual plotter position
    // does not validate if the move is valid
    static void teleport(float x, float y) {
      posx = x;
      posy = y;
      long l1,l2;
      IK(posx, posy, l1, l2);
      laststep1 = l1;
      laststep2 = l2;
    }
    
    
    //==========================================================
    //参考————斜线程序
    void moveto(float x,float y) {
      #ifdef VERBOSE
      Serial.println("Jump in line() function");
      Serial.print("x:");
      Serial.print(x);
      Serial.print(" y:");
      Serial.println(y);
      #endif
    
      long l1,l2;
      IK(x,y,l1,l2);
      long d1 = l1 - laststep1;
      long d2 = l2 - laststep2;
    
      #ifdef VERBOSE
      Serial.print("l1:");
      Serial.print(l1);
      Serial.print(" laststep1:");
      Serial.print(laststep1);
      Serial.print(" d1:");
      Serial.println(d1);
      Serial.print("l2:");
      Serial.print(l2);
      Serial.print(" laststep2:");
      Serial.print(laststep2);
      Serial.print(" d2:");
      Serial.println(d2);
      #endif
    
      long ad1=abs(d1);
      long ad2=abs(d2);
      int dir1=d1>0 ? M1_REEL_IN : M1_REEL_OUT;
      int dir2=d2>0 ? M2_REEL_IN : M2_REEL_OUT;
      long over=0;
      long i;
    
    
      if(ad1>ad2) {
        for(i=0;i<ad1;++i) {
          
          m1.moveRelativeInSteps(dir1);
          over+=ad2;
          if(over>=ad1) {
            over-=ad1;
            m2.moveRelativeInSteps(dir2);
          }
          delayMicroseconds(step_delay);
         }
      } 
      else {
        for(i=0;i<ad2;++i) {
          m2.moveRelativeInSteps(dir2);
          over+=ad1;
          if(over>=ad2) {
            over-=ad2;
            m1.moveRelativeInSteps(dir1);
          }
          delayMicroseconds(step_delay);
        }
      }
    
      laststep1=l1;
      laststep2=l2;
      posx=x;
      posy=y;
    
      
    }
    
    //------------------------------------------------------------------------------
    //长距离移动会走圆弧轨迹,所以将长线切割成短线保持直线形态
    static void line_safe(float x,float y) {
      // split up long lines to make them straighter?
      float dx=x-posx;
      float dy=y-posy;
    
      float len=sqrt(dx*dx+dy*dy);
      
      if(len<=TPS) {
        moveto(x,y);
        return;
      }
      
      // too long!
      long pieces=floor(len/TPS);
      float x0=posx;
      float y0=posy;
      float a;
      for(long j=0;j<=pieces;++j) {
        a=(float)j/(float)pieces;
    
        moveto((x-x0)*a+x0,
             (y-y0)*a+y0);
      }
      moveto(x,y);
    }
    
    
    
    
    
    void line(float x,float y) 
    {
      line_safe(x,y);
    }
    
    
    
    //********************************
    void nc(String st)
    {
    
    String xx,yy,zz;
    int ok=1;
    
    
      st.toUpperCase();
      
      float x,y,z;
      int px,py,pz;
      px = st.indexOf('X');
      py = st.indexOf('Y');
      pz = st.indexOf('Z');
      if (px==-1 || py==-1) ok=0; 
      if (pz==-1) 
        {
          pz=st.length();
        }
        else
        {   
          zz = st.substring(pz+1,st.length());
          z  = zz.toFloat();
          if (z>0)  pen_up();
          if (z<=0) pen_down();
        }
    
      xx = st.substring(px+1,py);
      yy = st.substring(py+1,pz);
      
      
      
      xx.trim();//缩进,去掉末尾空格*/
      yy.trim();
    
      if (ok) line(xx.toFloat(),yy.toFloat());
      
    }
    
    //**********************
    void drawfile( String filename)
    {
    
      String rd="";
      int line=0;
      char rr=0;
      
    
      myFile = SD.open(filename);
      if (myFile) {
        Serial.println("OPEN:");
        
        while (myFile.available()) {
          rr=myFile.read();
          
          if (rr == char(10)) 
           {
              line++;
              Serial.print("Run nc #");
              Serial.print(line);
              Serial.println(" : "+rd);
              nc(rd);
              rd="";
            }
           else
             rd+=rr;
            
        }
        
        myFile.close();
        
      } 
    
    }
    
    
    
    void setup() {
      // put your setup code here, to run once:
      Serial.begin(BAUD);
      m1.connectToPins(7,8,9,10); //M1 L步进电机   in1~4端口对应UNO  7 8 9 10
      m2.connectToPins(2,3,5,6);  //M2 R步进电机   in1~4端口对应UNO 2 3 5 6
      m1.setSpeedInStepsPerSecond(10000);
      m1.setAccelerationInStepsPerSecondPerSecond(100000);
      m2.setSpeedInStepsPerSecond(10000);
      m2.setAccelerationInStepsPerSecondPerSecond(100000);
    
    
      //抬笔舵机
      pen.attach(A0);
      ps=PEN_UP_ANGLE;
      pen.write(ps);
    
      //将当前笔位置设置为0,0
      teleport(0, 0);
      //缩放比例
      mode_scale = 1;
    
      if (!SD.begin(4)) {
        Serial.println("initialization failed!");
        while (1);
      }
    
    
      Serial.println("Test OK!");
    }
    
    
    
    
    void loop() {
     //注意卡上的文件名要和程序一致。!!!!!
      drawfile("1.nc");  //1.nc 是Gcode代码的文件名 ,需要将g代码保存在sd卡上。
      while(1);
    }
    

    arduino需要动手,在程序之外的接线,调试硬件等等。
    代码很长,下面分享一下解读的办法:

    找到核心的主程序。也就是void step 和 void loop部分。

    image.png

    void setup部分

    ①BAUD是个变量指代的是波特率,那么在程序中找到。


    image.png

    可以用CTRL+F查找。
    ②③m1,m2指代的是两个步进电机。


    image.png
    ④pen指代的是舵机 image.png
    ps image.png
    PEN_UP_ANGLE image.png
    下一句写入ps也就是抬笔舵机的角度为60。
    ⑤teleport(0,0)是一个函数并传入两个参数0,0
    image.png
    ⑧posx,posy image.png 第一次运行为0,0因为传入了参数。
    ⑨ IK(posx, posy, l1, l2);它又是一个函数,程序再次跳转。 image.png
    ⑩①LIMXMIN,LIMYMIN image.png
    ⑩②TPS步进电机的步长 image.png
    在文章的开始部分已经详细介绍了坐标转换两根线的长度,这段代码就是这个意思。程序运行完转到⑩
    image.png
    image.png
    程序转到主程序⑥继续执行。
    mode_scale=1; image.png
    下面的: image.png
    如果SD的信道不是4那么打印加载失败并进入while(1)死循环中。
    Serial.println("Test OK!");串口打印:测试通过

    void loop()部分

    image.png
    drawfile("1.nc");是个函数,程序跳转。 image.png
    SD image.png
    (为了更好体现程序运行过程,公共变量不再贴图)
    drawfile()函数代码的意思为:打开文件并赋值给myFile,

    如果myFile为真,则串口打印OPEN,如果myFile的字节数为真(也就是大于0),在while循环中读值,然后串口输出打印什么,然后执行nc()函数,程序跳转。


    image.png
    nc()函数是将sd卡中的gcode码进行处理,当处理完成后,交给line()函数处理。④
    line()直接跳转给line_safe(),line_safe函数的作用是计算坐标点和上一个坐标点的距离,具体的操作和步进电机的运行交给moveto()函数进行具体的收线放线等操作。
    image.png
    整个程序按流程顺下来,等于熟悉了程序框架,剩下的就是些细节,比如,从sd卡读值和字符串处理过程,步进电机的收线放线等等。

    相关文章

      网友评论

          本文标题:arduino开源项目——墙画机器人

          本文链接:https://www.haomeiwen.com/subject/wbuxwhtx.html