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

Контроллер мы выбрали Arduino Mega 2560 и что бы много не думать, драйвер шаговых двигателей использовали RAMPS 1.4 (как у RepRap 3D принтера).

Программу контроллера писали по алгоритму метода конечных автоматов. Последний раз я о нем слышал лет 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 шага. Если кому интересно, про управление шаговыми двигателями можно отдельную статью часами писать.
Теперь немного о железе.

Двигатели использовали 17HS8401, самые мощные из NEMA17, которые смогли на ebay найти. Там же купили подшипники и оптические концевики.

Все остальное отечественное, родное. Оригинальная идея с направляющими, сделали их из длинных хромированных ручек для мебели, они как раз диаметром 12 мм под подшипники, длиной они продаются до метра, и прочности вполне хватает. В торцах ручек просверлили отверстия и метчиком нарезали резьбу. Это позволило просто болтом надежно соединить направляющие с несущим конструктивом. Для оси Z так вообще ручку прикрепили к пластине конструктива целиком, Вал продается в любом строительном магазине как шпилька с резьбой любого диаметра. Мы использовали на 8 мм. Соответственно и гайки 8 мм. Гайку с подшипником и несущим конструктивом оси Y соединили с помощью соединительной скобы. Скобы купили в специализированном магазине для витрин. Видели наверно такие хромированные конструкции в магазинах стоят на которых галстуки или рубашки висят, вот там используются такие скобы для соединения хромированных трубок. Двигатель соединили с валом муфтой, которую сделали из куска стального прута диаметром 14мм, просверлив по центру отверстие и пару отверстий сбоку, для зажимания винтами. Можно не заморачиваться и купить готовые на ebay по запросу cnc coupling их куча выпадает. Несущий конструктив нам вырубили на гильотине за 1000 р. Сборка всего этого заняло не много времени и получили на выходе вот такой станок, на фото еще не установлены концевики, контроллер и не установлен двигатель фрезы.

Точность получилась просто изумительная, во-первых шаговый двигатель шагает 1/16 шага, во-вторых вал с мелкой резьбой. Когда в станок вставили ручку вместо фрезы, он нарисовал сложную фигуру, потом еще несколько раз обвел эту фигуру, а на рисунке видно как будто он один раз рисовал, под лупой рассматривали пытались другую линию найти. Жесткость станка тоже хорошая. Шатается только в пошипниках в допустимых пределах их собственого допуска и посадки. Немного шатается еще по оси Y, ну здесь я думаю конструктив оси Z надо доработать.
Фото получилось не качественное, на заднем плане стекло отражает. Не знаю какой я конструктор станка, но фотограф я просто никакой. Вот чуть получше.

Теперь об управляющей программе. Не помню почему, но мы решили сделать свою программу, которая готовый 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-код и маленькими порциями запихивает его в контроллер.
Исходный код управляющей программы можно посмотреть здесь github.com/konstantin1970/cnc.git
Для понимания добавлю еще, что стандартный windows hyperterminal или putty делает то-же самое, запихивает данные в контроллер.
Сам G-код можно сделать в любой CAD/CAM системе например, мне понравился ARTCAM.
В планах у нас сделать более мощный станок на двигателях NEMA 23, но для этого нужно придумать, из чего делать более мощные направляющие. В прошивке контроллера добавить возможность изменять скорость вращения шпинделя. Особенно интересно нам установить камеру и сделать что-то подобное системе технического зрения, что бы станок сам определял размеры заготовки, вычислял начальную координату заготовки по всем осям в программе минимум. В программе максимум, чтобы с помощью камеры станок контролировал все этапы своей работы, возможно даже принимал решения изменить программу. Ну, например, увидел он, что шероховатость больше допустимого, взял и послал фрезу по второму кругу все отшлифовать с более высокой скоростью.
Надеюсь, сможем и в дальнейшем делиться своими разработками с вами уважаемые хаброчитатели.
