Продолжаем обзор деятельности нашего Хакспейс клуба. vk.com/club71815206
Мы давно мечтали купить в наш клуб ЧПУ станок. Но решили его сделать сами. С нуля, начиная от железа и кончая программного обеспечение (прошивка контроллера и управляющая программа). И у нас это получилось.
Детали для станка старались выбирать из доступных в свободной продаже, многие из которых даже не требуют дополнительной слесарной обработки.
Контроллер мы выбрали 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-код и маленькими порциями запихивает его в контроллер.
Исходный код программы не знаю пока как выложить. Она хоть и сырая но свои функции выполняет. Там около 20 файлов.Как их залить сюда?
Сам G-код можно сделать в любой CAD/CAM системе например, мне понравился ARTCAM.
В планах у нас, сделать более мощный станок на двигателях NEMA 23, но для этого нужно придумать из чего делать более мощные направляющие. В прошивке контроллера добавить возможность изменять скорость вращения шпинделя. Особенно интересно нам установить камеру и сделать что-то подобное системе технического зрения, что-бы станок сам определял размеры заготовки, вычеслял начальную координату заготовки по всем осям, в программе минимум. В программе максимум, чтобы с помощью камеры станок контролировал все этапы своей работы, возможно даже принимал решения изменить программу. Ну например увидел он, что шероховатость больше допустимого, взял и послал фрезу по второму кругу все отшлифовать с более высокой скоростью.
Надеюсь сможем и в дальнейшем делиться своими разработками с Вами уважаемые хаброчитатели.
Автор: konstantin1970