Створення верстата з ЧПК з доступних деталей з мінімум слюсарної роботи

Продовжуємо огляд діяльності нашого Хакспейс клубу. vk.com/club71815206
Ми давно мріяли купити в наш клуб ЧПУ верстат. Але вирішили його зробити самі. З нуля, починаючи від заліза і закінчуючи програмного забезпечення (firmware) для контролера і керуюча програма). І у нас це вийшло.
Деталі для верстата намагалися обрати з доступних у вільному продажу, багато з яких навіть не потребують додаткової слюсарної обробки.




Контролер ми вибрали 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©;
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©;
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-код з комп'ютера передає в контролер. Може просто не знайшли потрібний.
Програма написана на 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, але для цього потрібно придумати з чого робити більш потужні напрямні. У прошивці контролера додати можливість змінювати швидкість обертання шпинделя. Особливо цікаво нам встановити камеру і зробити щось подібне системи технічного зору, що-б верстат сам визначав розміри заготовки, вычеслял початкову координату заготовки по всіх осях, у програмі мінімум. У програмі максимум, щоб з допомогою камери верстат контролював всі етапи своєї роботи, можливо навіть приймав рішення змінити програму. Ну наприклад побачив він, що шорсткість більше допустимого, взяв і послав фрезу по другому колу все відшліфувати з більш високою швидкістю.
Сподіваюся зможемо і надалі ділитися своїми розробками з Вами шановні хаброчитатели.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.