Создание станка с ЧПУ из доступных деталей с минимум слесарной работы

в 5:33, , рубрики: arduino, diy или сделай сам, Алгоритмы, Блог компании Хакспейс клуб «minirobot», разработка, станок с чпу

Продолжаем обзор деятельности нашего Хакспейс клуба. vk.com/club71815206
Мы давно мечтали купить в наш клуб ЧПУ станок. Но решили его сделать сами. С нуля, начиная от железа и кончая программного обеспечение (прошивка контроллера и управляющая программа). И у нас это получилось.
Детали для станка старались выбирать из доступных в свободной продаже, многие из которых даже не требуют дополнительной слесарной обработки.
Создание станка с ЧПУ из доступных деталей с минимум слесарной работы - 1

Контроллер мы выбрали Arduino Mega 2560 и что бы много не думать, драйвер шаговых двигателей использовали RAMPS 1.4 (как у RepRap 3D принтера).
Создание станка с ЧПУ из доступных деталей с минимум слесарной работы - 2
Программу контроллера писали по алгоритму метода конечных автоматов. Последний раз я о нем слышал лет 20 назад в институте, не помню по какому предмету изучали. Это была очень удачная идея. Код получился маленький и легко расширяемый без потери читабельности (если в дальнейшем понадобится не только оси XYZ или использовать другой G-код). Программа контроллера принимает с USB порта G-код и собственно дает команду двигателям осей XYZ двигаться в заданном направлении. Кто не знает, G-код — это последовательность конструкций типа G1X10Y20Z10, которая говорит станку переместится по оси X на 10 мм, Y на 20 мм и Z на 10 мм. На самом деле в G-коде много различных конструций (например G90 — используется абсолютная система координат, G91 — относительная) и много модифиаций самого кода, В интернете много о нем описано.
Подробнее остановлюсь на описание скетча (прошивка контроллера).
Вначале в описании переменных прописываем к какому выходу контроллера будет подключены двигатели и концевые выключатели.
В этом коде метода конечных автоматов переменная принимает значение ждать с USB порта первый байт, во второй конструции case производится проверка наличия данных и переменная _s принимает значение get_cmd. Т.е считать данные с порта.

 switch(_s){
  case begin_cmd:         Serial.println("&"); //038
                          //to_begin_coord();
                          n_t=0;
                          _s=wait_first_byte;
                          len=0;
                          break;
                          
  case wait_first_byte:   if(Serial.available()){
                              Serial.print(">");
                             _s=get_cmd;
                              c_i=0;
                           }
                          break;

далее считываем все что есть в порту и переменная _s устанавливается в get_tag; т.е. переходим на прием буквенного значение G – кода.

case get_cmd:           c=Serial.read();
                          if(c!=-1){
                              if(c=='~'){
                                _s=get_tag;
                                c_i=0;
                                n_t=0;
                                break;
                              }
                           Serial.print(c);
                           if((c>=97)&&(c<=122))
                             c-=32;
                           if( (c==32)||(c==45)||(c==46)||((c>=48)&&(c<=57))||((c>=65)&&(c<=90)) ){
                             cmd[c_i++]=c;
                             len++;
                           }
                          }
                          break;

Ну и так далее.

полный код можно посмотреть здесь.

#include <Stepper.h>
#define STEPS 48
//#define SHAG 3.298701
#define STEP_MOTOR 1
double koefx = 1.333333;
double koefy = 1.694915;
Stepper stepper0(STEPS, 5, 4, 6, 3);
Stepper stepper1(STEPS, 8, 9, 10, 11);
Stepper stepper2(STEPS, 13, 12, 7, 2);
int x,y,x1,m;
int motor;
int inPinX = 15;                   // кнопка на входе 22
int inPinY = 14;                   // кнопка на входе 23
int inPinZ = 24;                   // кнопка на входе 24
int x_en = 62;
int x_dir = 48;
int x_step = 46;
int y_en = 56;
int y_dir = 61;
int y_step = 60;
const int begin_cmd=1;
const int wait_first_byte=2;
const int wait_cmd=3;
const int get_cmd=4;
const int test_cmd=5;
const int get_word=6;
const int get_tag=7;
const int get_val=8;
const int compilation_cmd=9;
const int run_cmd=10;
int abs_coord=1;
const int _X=1;
const int _Y=2;
//const int =10;
//const int =11;
//const int =12;


int _s=begin_cmd;
const int max_len_cmd=500;
char cmd[max_len_cmd];
char tag[100][20];
char val[100][20];
int n_t=0;
int c_i=0;
char c;
int i,j;
int amount_G=0;
int len=0;
char*trash;
int n_run_cmd=0;
int g_cmd_prev; //ya предыдущее значение G
class _g_cmd{
public:
_g_cmd(){
 reset();
}
int n; //ya
int g;
double x;
double y;
double z;
void reset(void){
 n=g=x=y=z=99999;
}
};
double _x,_y,_z;
double cur_abs_x,cur_abs_y,cur_abs_z; //ya stoyalo int
int f_abs_coord=1;

_g_cmd g_cmd[100];

void setup()
{

  stepper0.setSpeed(150);
  stepper1.setSpeed(150);
  stepper2.setSpeed(1000);
  Serial.begin(9600);
  pinMode(inPinX, INPUT);
  pinMode(inPinY, INPUT);
  pinMode(inPinZ, INPUT);
  pinMode(x_en, OUTPUT);
  pinMode(x_dir, OUTPUT);
  pinMode(x_step, OUTPUT);
  pinMode(y_en, OUTPUT);
  pinMode(y_dir, OUTPUT);
  pinMode(y_step, OUTPUT);
  digitalWrite(x_en, 1);
  digitalWrite(y_en, 1);
   to_begin_coord();
  //UNIimpstep(12,13,2,7,3,1000);
  //UNIimpstep(12,13,2,7,3,-1000);
}

void to_begin_coord(void)
{
 impstep(_X,-10000,1);
 impstep(_Y,-10000,1);
 cur_abs_x=cur_abs_y=cur_abs_z=0;
}

void loop()
{
  switch(_s){
  case begin_cmd:         Serial.println("&"); //038
                          //to_begin_coord();
                          n_t=0;
                          _s=wait_first_byte;
                          len=0;
                          break;
                          
  case wait_first_byte:   if(Serial.available()){
                              Serial.print(">");
                             _s=get_cmd;
                              c_i=0;
                           }
                          break;
                          
  case get_cmd:           c=Serial.read();
                          if(c!=-1){
                              if(c=='~'){
                                _s=get_tag;
                                c_i=0;
                                n_t=0;
                                break;
                              }
                           Serial.print(c);
                           if((c>=97)&&(c<=122))
                             c-=32;
                           if( (c==32)||(c==45)||(c==46)||((c>=48)&&(c<=57))||((c>=65)&&(c<=90)) ){
                             cmd[c_i++]=c;
                             len++;
                           }
                          }
                          break;

 
  case get_tag:           while((c_i<len)&&(cmd[c_i]<=32)) c_i++;
                          i=0;
                          while((c_i<len)&&(cmd[c_i]>=65)){
                             tag[n_t][i]=cmd[c_i];
                             i++;
                             c_i++;
                             while((c_i<len)&&(cmd[c_i]<=32)) c_i++;
                          }
                          if(i==0){
                            Serial.println("er2 cmd - 'no tag'");
                            _s=begin_cmd;
                            break;
                          }
                          tag[n_t][i]=0;
                          _s=get_val;
                          break;
                          
  case get_val:           while((c_i<len)&&(cmd[c_i]<=32)) c_i++;
                          i=0;
                          while((c_i<len)&& ( (cmd[c_i]=='.')||(cmd[c_i]=='-')||((cmd[c_i]>=48)&&(cmd[c_i]<=57)) ) ){
                             val[n_t][i]=cmd[c_i];
                             i++;
                             c_i++;
                             while((c_i<len)&&(cmd[c_i]<=32)) c_i++;
                          }
                          if(i==0){
                            Serial.println("er3 cmd - 'no val'");
                            _s=begin_cmd;
                            break;
                          }
                          val[n_t][i]=0;
                          n_t++;
                          _s=get_tag;
                          
                          if(c_i>=len)
                              _s=compilation_cmd;
                          break;
                        
  case compilation_cmd:   Serial.println("");
                          Serial.print("compilation cmd,input (");
                          Serial.print(n_t);
                          Serial.println("): ");

                          for(i=0;i<n_t;i++){
                            Serial.print(i);
                            Serial.print("=");
                            Serial.print(tag[i]);
                            Serial.print("(");
                            Serial.print(val[i]);
                            Serial.println(")");
                          }
                          for (int k=0; k<=j; k++)
                          {
                          g_cmd[k].reset();
                          }
                         j=0;
                         i=0;

                         while(i<n_t){
                          if(tag[i][0]=='N'){
                           g_cmd[j].n=(int)strtod(val[i],&trash);
                           i++;
                           while((i<n_t)&&(tag[i][0]!='N')){ //(g_cmd[j].g==1)&&
                                if(tag[i][0]=='G'){
                                   g_cmd[j].g=(int)strtod(val[i],&trash);
                                   g_cmd_prev = g_cmd[j].g;
                                }
                                else { g_cmd[j].g = g_cmd_prev;
                                }
                            if(tag[i][0]=='X')
                               g_cmd[j].x=(double)strtod(val[i],&trash);
                            if(tag[i][0]=='Y')
                               g_cmd[j].y=(double)strtod(val[i],&trash);
                            if(tag[i][0]=='Z')
                               g_cmd[j].z=(double)strtod(val[i],&trash);
                            i++;
                           }//while((i<n_t)&&(tag[i]!="G"))
                           j++;
                          }//if(tag[i]=="G")
                          else i++;
                         }//while(i<n_t)
                         amount_G=j;

                        Serial.print("compilation cmd,output (");
                        Serial.print(amount_G);
                        Serial.println("): ");
                  
                         for(j=0;j<amount_G;j++){
                            Serial.print(j);
                            Serial.print("=");
                            Serial.print("N");Serial.print(g_cmd[j].n);Serial.print(" ");
                            Serial.print("G");Serial.print(g_cmd[j].g);Serial.print(" ");
                            Serial.print("X");Serial.print(g_cmd[j].x);Serial.print(" ");
                            Serial.print("Y");Serial.print(g_cmd[j].y);Serial.print(" ");
                            Serial.print("Z");Serial.print(g_cmd[j].z);Serial.println(" ");
                         }
                         
                         n_run_cmd=0;
                         _s=run_cmd;
                         break;

  case run_cmd:          
                         Serial.print("run cmd [");
                         Serial.print("N");Serial.print(g_cmd[n_run_cmd].n);Serial.print(" ");
                         Serial.print("G");Serial.print(g_cmd[n_run_cmd].g);Serial.print(" ");
                         Serial.print("X");Serial.print(g_cmd[n_run_cmd].x);Serial.print(" ");
                         Serial.print("Y");Serial.print(g_cmd[n_run_cmd].y);Serial.print(" ");
                         Serial.print("Z");Serial.print(g_cmd[n_run_cmd].z);Serial.print(" ");
                         Serial.println("]");

                         int f_cmd_coord=0;
                         if(g_cmd[n_run_cmd].g==90){
                            f_abs_coord=1;
                            f_cmd_coord=1;
                            Serial.println("change to ABS coord");
                         }
                         if(g_cmd[n_run_cmd].g==91){
                            f_abs_coord=0;
                            f_cmd_coord=1;                            
                            Serial.println("change to REL coord");
                         }
                         Serial.print("cur_abs(");Serial.print(cur_abs_x);Serial.print(",");Serial.print(cur_abs_y);Serial.print(",");Serial.print(cur_abs_z);Serial.println(")");
                         if(f_cmd_coord){if(++n_run_cmd>=amount_G) _s=begin_cmd; break;}
                         
                         if(f_abs_coord){
                            Serial.println("zdes kosjak G90 ABS");
                            if (g_cmd[n_run_cmd].x==99999)
                            _x=0;
                            else
                           _x=g_cmd[n_run_cmd].x-cur_abs_x;
                            if (g_cmd[n_run_cmd].y==99999)
                            _y=0;
                            else
                           _y=g_cmd[n_run_cmd].y-cur_abs_y;
                           if (g_cmd[n_run_cmd].z==99999)
                            _z=0;
                            else
                           _z=g_cmd[n_run_cmd].z-cur_abs_z;
                           
                         }else{
                           Serial.println("normalno G91 REL");
                           _x=g_cmd[n_run_cmd].x;
                           _y=g_cmd[n_run_cmd].y;
                           _z=g_cmd[n_run_cmd].z;                         
                         }

                         if((_x==0)&&(_y==0)&&(_z==0)){
                           Serial.println("exit: _x=0,_y=0,_z=0");
                           if(++n_run_cmd>=amount_G) _s=begin_cmd; break;
                         }
                         
                        // _x=_x*koefx;
                        // _y=_y*koefy;
                         //_z=_z*koef;
                         double max_l=abs(_x); //ya
                         if(abs(_y)>max_l)
                             max_l=abs(_y);
                         if(abs(_z)>max_l)
                             max_l=abs(_z);
                         double unit_scale=90; // steps in 1.0
                         double unit_len=max_l*unit_scale,unit_step;
                         double px=0,py=0,pz=0,x=0,y=0,z=0,kx,ky,kz;
                         int all_x_steps=0,all_y_steps=0,all_z_steps=0;
                         kx=_x/unit_len;
                         ky=_y/unit_len;
                         kz=_z/unit_len;
                    //     Serial.print("unit_len - "); Serial.print(unit_len); Serial.print(" _x- "); Serial.print(_x); Serial.print(" max_l- "); Serial.println(max_l);
                    //     Serial.print("kx=");Serial.print(kx);Serial.print(" ky=");Serial.print(ky);Serial.print(" kz=");Serial.println(kz); 
                         if((kx==0)&&(ky==0)&&(kz==0)){if(++n_run_cmd>=amount_G) _s=begin_cmd; break;}
                         for(unit_step=0;unit_step<unit_len;unit_step++){

                          if((abs(x-px)*unit_scale)>=1){
                            impstep(_X,STEP_MOTOR*kx/abs(kx),1);  //stepper0.step(STEP_MOTOR*kx/abs(kx));
                            //Serial.print("x_step ");Serial.println(kx/abs(kx));
                            all_x_steps++;
                            px=x;
                          }
                          if((abs(y-py)*unit_scale)>=1){
                            impstep(_Y,STEP_MOTOR*ky/abs(ky),1); //stepper1.step(STEP_MOTOR*ky/abs(ky));
                            //Serial.print("y_step ");Serial.println(ky/abs(ky));
                            all_y_steps++;
                            py=y;
                          }
                          if((abs(z-pz)*unit_scale)>=1){
                            UNIimpstep(12,13,2,7,3,10*kz/abs(kz)); //stepper2.step(STEP_MOTOR*kz/abs(kz)); 
                            //Serial.print("z_step ");Serial.println(kz/abs(kz));
                            all_z_steps++;
                            pz=z;
                          }
                          x+=kx; y+=ky; z+=kz;
                       //   Serial.print(unit_step);Serial.print("  :  ");
                        //  Serial.print(x);Serial.print("  |  ");Serial.print(y);Serial.print("  |  ");Serial.println(z);                          
                         }
                          Serial.println("-----------------------------------------");
                          Serial.print("all_steps(");Serial.print(all_x_steps);Serial.print(",");Serial.print(all_y_steps);Serial.print(",");Serial.print(all_z_steps);Serial.print(")");
                          cur_abs_x+=_x; cur_abs_y+=_y; cur_abs_z+=_z;                         
                          Serial.print("cur_abs(");Serial.print(cur_abs_x);Serial.print(",");Serial.print(cur_abs_y);Serial.print(",");Serial.print(cur_abs_z);Serial.println(")");
                          Serial.println("-----------------------------------------");   
                         
                         if(++n_run_cmd>=amount_G) _s=begin_cmd;
                            
 }//switch(_s)
}

char end_button(int coord)
{
  int but=0;
  
  if(coord==_X)
      but=digitalRead(inPinX);
  if(coord==_Y)
      but=digitalRead(inPinY);
  
  if(but){
   if(coord==_X)
       Serial.println("[ X out of range ]"); 
   if(coord==_Y)
       Serial.println("[ Y out of range ]"); 
  }
  return but;
}
   
char impstep(int coord,int kol,int f_test_coord)
{
  int IN_en,IN_dir,IN_step,pause;
  pause=2; //35
  switch(coord){
   case _X:  IN_en=x_en; IN_dir=x_dir; IN_step=x_step;
     digitalWrite(IN_en, 0);
     break;
   case _Y:  IN_en=y_en; IN_dir=y_dir; IN_step=y_step;
     digitalWrite(IN_en, 0);
     break;
    }
  if(!f_test_coord)
     Serial.println("[ break step ]");
  //delay(100);
  if (kol<0)
    for (int i=0; i<=abs(kol); i++){
    if(f_test_coord&&end_button(coord)){
      impstep(coord,200,0);
      return 0;
    }
    digitalWrite(IN_dir, LOW);
    digitalWrite(IN_step, HIGH);
    delay(pause);
    digitalWrite(IN_step, LOW);
    delay(pause);
  }else
    for (int i=0; i <= kol; i++){
      if(f_test_coord&&end_button(coord)){
        impstep(coord,200,0);
        return 0;
      }
    digitalWrite(IN_dir, HIGH);
    digitalWrite(IN_step, HIGH);
    delay(pause);
    digitalWrite(IN_step, LOW);
    delay(pause);
    }
    digitalWrite(IN_en, 1);
    return 1;
  }

void UNIimpstep(int IN1,int IN2,int IN3,int IN4,int pause,int kol)
{
      //delay(100);
  if (kol<0)
    for (int i=0; i<=abs(kol); i++){
         digitalWrite(IN1, 0);
    digitalWrite(IN2, 1);
    digitalWrite(IN3, 0);
    digitalWrite(IN4, 0);
    delay(pause);
    digitalWrite(IN1, 1);
    digitalWrite(IN2, 0);
    digitalWrite(IN3, 0);
    digitalWrite(IN4, 0);
    delay(pause);
    digitalWrite(IN1, 0);
    digitalWrite(IN2, 0);
    digitalWrite(IN3, 0);
    digitalWrite(IN4, 1);
    delay(pause);
    digitalWrite(IN1, 0);
    digitalWrite(IN2, 0);
    digitalWrite(IN3, 1);
    digitalWrite(IN4, 0);
     delay(pause);
   }
   else
    for (int i=0; i <= kol; i++){
    digitalWrite(IN1, 1);
    digitalWrite(IN2, 0);
    digitalWrite(IN3, 0);
    digitalWrite(IN4, 0);
    delay(pause);
    digitalWrite(IN1, 0);
    digitalWrite(IN2, 1);
    digitalWrite(IN3, 0);
    digitalWrite(IN4, 0);
    delay(pause);
    digitalWrite(IN1, 0);
    digitalWrite(IN2, 0);
    digitalWrite(IN3, 1);
    digitalWrite(IN4, 0);
    delay(pause);
    digitalWrite(IN1, 0);
    digitalWrite(IN2, 0);
    digitalWrite(IN3, 0);
    digitalWrite(IN4, 1);
    delay(pause);
 }
}

Завершается конечный автомат конструкцией case run_cmd: где собственно и подается управляющий сигнал на двигатель. Управлением двигателем можно было бы использовать библиотеку #include <Stepper.h> но мы написали свою функцию(char impstep -для биполярного двигателя, void UNIimpstep — униполярного ), что бы можно было не ждать пока один двигатель отработает, что бы послать сигнал другому. Ну и на будущее, отдельная процедура позволит гибче использовать возможностями шагового двигателя. Например, если будем использовать другой драйвер двигателя программно задавать полушаг или шаг двигателя. В нынешнем исполнении с RAMPS получается 1/16 шага. Если кому интересно, про управление шаговыми двигателями можно отдельную статью часами писать.

Теперь немного о железе.
Создание станка с ЧПУ из доступных деталей с минимум слесарной работы - 3
Двигатели использовали 17HS8401, самые мощные из NEMA17, которые смогли на ebay найти. Там же купили подшипники и оптические концевики.
Создание станка с ЧПУ из доступных деталей с минимум слесарной работы - 4
Все остальное отечественное, родное. Оригинальная идея с направляющими, сделали их из длинных хромированных ручек для мебели, они как раз диаметром 12 мм под подшипники, длиной они продаются да метра, и прочности вполне хватает. В торцах ручек просверлили отверстия и метчиком нарезали резьбу. Это позволило просто болтом надежно соединить направляющие с несущим конструктивом. Для оси Z так вообще ручку прикрепили к пластине конструктива целиком, Вал продается в любом строительном магазине как шпилька с резьбой любого диаметра. Мы использовали на 8 мм. Соответственно и гайки 8 мм. Гайку с подшипником и несущим конструктивом оси Y соединили с помощью соединительной скобы. Скобы купили в специализированном магазине для витрин. Видели наверно такие хромированные конструкции в магазинах стоят на которых галстуки или рубашки висят, вот там используются такие скобы для соединения хромированных трубок. Двигатель соединили с валом муфтой, которую сделали из куска стального прута диаметром 14мм, просверлив по центру отверстие и пару отверстий сбоку, для зажимания винтами. Можно не заморачиваться и купить готовые на ebay по запросу cnc coupling их куча выпадает. Несущий конструктив нам вырубили на гильотине за 1000 р. Сборка всего этого заняло не много времени и получили на выходе вот такой станок, на фото еще не установлены концевики, контроллер и не установлен двигатель фрезы.
Создание станка с ЧПУ из доступных деталей с минимум слесарной работы - 5
Точность получилась просто изумительная, во-первых шаговый двигатель шагает 1/16 шага, во-вторых вал с мелкой резьбой. Когда в станок вставили ручку вместо фрезы, он нарисовал сложную фигуру, потом еще несколько раз обвел эту фигуру, а на рисунке видно как будто он один раз рисовал, под лупой рассматривали пытались другую линию найти. Жесткость станка тоже хорошая. Шатается только в пошипниках в допустимых пределах их собственого допуска и посадки. Немного шатается еще по оси Y, ну здесь я думаю конструктив оси Z надо доработать.
Фото получилось не качественное, на заднем плане стекло отражает. Не знаю какой я конструктор станка, но фотограф я просто никакой. Вот чуть получше.
Создание станка с ЧПУ из доступных деталей с минимум слесарной работы - 6
Теперь об управляющей программе. Не помню почему, но мы решили сделать свою программу, которая готовый G-код с компьютера передает в контроллер. Может просто не нашли подходящий.
Программа написана на Microsoft Visual C++, использовалась библиотеки:
Module: SERIALPORT.H
Purpose: Declaration for an MFC wrapper class for serial ports
Copyright © 1999 by PJ Naughter. All rights reserved.
Программа еще сырая, ну в двух словах используем

port.Open(8, 9600, CSerialPort::NoParity, 8, CSerialPort::OneStopBit, CSerialPort::XonXoffFlowControl);
для открытия порта
port.Write(text, l); - запись в порт
port.Read(sRxBuf, LEN_BUF); - чтение порта.

Использовался еще стандартный компонент msflexgrid таблица, в которую в реальном времени заносится выполняемый в настоящий момент G-код. Т.е. эта программа просто открывает готовый G-код и маленькими порциями запихивает его в контроллер.
Исходный код программы не знаю пока как выложить. Она хоть и сырая но свои функции выполняет. Там около 20 файлов.Как их залить сюда?
Сам G-код можно сделать в любой CAD/CAM системе например, мне понравился ARTCAM.
В планах у нас, сделать более мощный станок на двигателях NEMA 23, но для этого нужно придумать из чего делать более мощные направляющие. В прошивке контроллера добавить возможность изменять скорость вращения шпинделя. Особенно интересно нам установить камеру и сделать что-то подобное системе технического зрения, что-бы станок сам определял размеры заготовки, вычеслял начальную координату заготовки по всем осям, в программе минимум. В программе максимум, чтобы с помощью камеры станок контролировал все этапы своей работы, возможно даже принимал решения изменить программу. Ну например увидел он, что шероховатость больше допустимого, взял и послал фрезу по второму кругу все отшлифовать с более высокой скоростью.
Надеюсь сможем и в дальнейшем делиться своими разработками с Вами уважаемые хаброчитатели.

Автор: konstantin1970

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js