DIY 3축 CNC VMC: 정밀 밀링 머신 직접 제작하기
부품 및 소모품
| Arduino Nano R3 | × | 1 |
| Raspberry Pi 3 Model B | × | 1 |
| SparkFun 스테퍼 모터 드라이버 보드 A4988 | × | 1 |
| Adafruit Arduino Nano 4-스테퍼 1-DC RepRap 쉴드 | × | 1 |
| OpenBuilds 알루미늄 스페이서 | × | 1 |
| 파워 서플라이 12V/5A | × | 1 |
| OpenBuilds L 브래킷 | × | 1 |
| OpenBuilds M5 T 너트 (10팩) | × | 1 |
| OpenBuilds 로우 프로파일 나사 M5 (10팩) | × | 1 |
| OpenBuilds M3 캡 헤드 나사 | × | 1 |
| OpenBuilds 세트 나사 | × | 1 |
| OpenBuilds 슬롯 와셔 15x5x2mm | × | 1 |
| OpenBuilds 안티-백래시 너트 블록 8mm 메트릭 ACME 리드 스크류용 | × | 1 |
| OpenBuilds 스레드 로드 플레이트 NEMA 17 스테퍼 모터용 | × | 1 |
| OpenBuilds 8mm 메트릭 ACME 리드 스크류 | × | 1 |
| OpenBuilds 볼 베어링 625 2RS 5x16x5 | × | 1 |
| 스무스 로드 | × | 1 |
| BLDC 쿨링 팬 | × | 1 |
| DC 모터 (범용) | × | 1 |
| 서보 (Tower Pro MG996R) | × | 1 |
| Texas Instruments 듀얼 H-브리지 모터 드라이버 L293D | × | 1 |
필요한 도구 및 장비
- 드라이버 세트
- 드릴 머신
- 드레멜/로터리 커팅 툴
- 글루건
- 납땜 인두
- 앵글 그라인더
앱 및 온라인 서비스
- Linux
- ArtCAM
- GRBL 0.9
- Autodesk Fusion 360
- Arduino IDE
- Google Android Things
- VNC
프로젝트 소개
우리의 프로토타입 모델은 IoT 기반으로, 인터넷 연결만 있으면 사실상 어디서든 사용할 수 있는 확장 가능한 아키텍처를 제공합니다. 이 기계는 350mm x 350mm 크기의 CNC로, 250mm x 240mm 작업 영역을 가지며 오픈 소스 모션 제어 소프트웨어(GRBL)로 구동됩니다. 하드웨어도 대부분 오픈 소스라 대폭적인 비용 절감이 가능했습니다. 이 기계는 현지에서 구매한 부품만으로 제작되었으며, 목재, 플라스틱, 경질 고무, 경질 레진 등을 가공할 수 있습니다. 주요 타겟은 저비용 PCB 제조입니다.
목재 조각 가능성도 있습니다.
레이저 가공 지원도 향후 프로젝트로 추가할 계획입니다.
현재 시스템은 로컬 인트라넷에서 무선으로 제어 가능하지만, 더 나아가 여러 대의 이런 장비를 연결하는 분산형 웹 애플리케이션 생태계를 구축하고자 합니다.
가능성은 무궁무진합니다.
프로토타입의 베이스 프레임은 2020 T-슬롯 알루미늄 익스트루전과 L-조인트로 제작되었습니다. 2000 RPM DC 제어 스핀들로 약 80-85와트 출력을 냅니다. 리드 스크류와 스레드 로드 이동 방식에 가이드용 스무스 로드와 래디얼 그루브 스냅핏 볼 베어링을 사용해 3축 모두 부드러운 동작을 구현했습니다.
최소 리드 피치는 0.8mm/회전, 2000 스텝/회전으로 모든 축에서 1600 해상도를 달성했습니다. 대략적인 기계 크기는 430x430x330mm, 작업 영역은 270x170x65mm, 위치 정밀도는 0.04mm입니다. Z축 갠트리는 이중 가이드 로드로 3D 프린팅해 처음부터 제작했습니다.
스핀들 비트 장착 시 Z축 이동 거리는 4.8cm입니다. PCB 조각용 45° V-비트 커팅 툴을 사용하며, 0.2mm~1.8mm 엔드밀 비트도 지원합니다. 각 축 메인 드라이브에 NEMA 23 스테퍼 모터(최대 3A 전류)를 사용하며, 2.5A A4988 드라이버로 1/16 최대 마이크로스테핑 해상도를 구현해 매우 경제적인 비용으로 궁극의 가공 정밀도를 달성했습니다.
IoT 부분으로는, 헤드리스 디스플레이(모바일/태블릿 화면이 HMI 역할)에서 인터넷 연결이 가능한 곳이면 어디서든 CNC 라우터를 제어할 수 있게 했습니다. 라즈베리 파이와 라우터(보안) 구성으로 인터넷에 연결된 웹 서버를 통해 CNC를 무선 제어하는 인터페이스를 제공합니다. 실시간 모터 토크, 전류 소모, 진동, 소음 등 귀중한 기계 부품 인사이트를 얻고 웹 플랫폼에서 실시간 인사이트를 도출해 더 나은 협업을 지원할 계획입니다. 이는 제조 분야에서 산업용 IoT(인더스트리 4.0)이라 불리는 개념입니다.
코드
샘플 G코드 VHDL
(Scribbled version of C:\Users\ABDERR~1\AppData\Local\Temp\ink_ext_XXXXXX.svgISF45X @ 3000.00)
(unicorn.py --tab="plotter_setup" --pen-up-angle=50 --pen-down-angle=30 --start-delay=160 --stop-delay=150 --xy-feedrate=3000 --z-feedrate=150 --z-height=0 --finished-height=0 --register-pen=true --x-home=0 --y-home=0 --num-copies=1 --continuous=false --pause-on-layer-change=true C:\Users\ABDERR~1\AppData\Local\Temp\ink_ext_XXXXXX.svgISF45X)
G21 (metric ftw)
G90 (absolute mode)
G92 X0.00 Y0.00 Z0.00 (you are here)
M300 S30 (pen down)
G4 P160 (wait 160ms)
M300 S50 (pen up)
G4 P150 (wait 150ms)
M18 (disengage drives)
M01 (Was registration test successful?)
M17 (engage drives if YES, and continue)
M01 (Plotting layer 'Calque 1')
(Polyline consisting of 29 segments.)
G1 X16.85 Y4.97 F3000.00
M300 S30.00 (pen down)
G4 P160 (wait 160ms)
... (이하 생략, 전체 파일 다운로드 필요) ...
스테퍼 코드 Arduino
//AMIT
#include <Servo.h>
#include <AFMotor.h>
#define LINE_BUFFER_LENGTH 512
char STEP = MICROSTEP;
const int penZUp = 115;
const int penZDown = 83;
const int penServoPin = 10;
const int stepsPerRevolution = 48;
Servo penServo;
AF_Stepper myStepperY(stepsPerRevolution,1);
AF_Stepper myStepperX(stepsPerRevolution,2);
struct point { float x; float y; float z; };
struct point actuatorPos;
float StepInc = 1;
int StepDelay = 0;
int LineDelay = 0;
int penDelay = 50;
float StepsPerMillimeterX = 100.0;
float StepsPerMillimeterY = 100.0;
float Xmin = 0, Xmax = 40, Ymin = 0, Ymax = 40, Zmin = 0, Zmax = 1;
float Xpos = Xmin, Ypos = Ymin, Zpos = Zmax;
boolean verbose = false;
void setup() {
Serial.begin(9600);
penServo.attach(penServoPin);
penServo.write(penZUp);
delay(100);
myStepperX.setSpeed(600);
myStepperY.setSpeed(600);
Serial.println("Mini CNC Plotter alive and kicking!");
Serial.print("X range is from "); Serial.print(Xmin); Serial.print(" to "); Serial.print(Xmax); Serial.println(" mm.");
Serial.print("Y range is from "); Serial.print(Ymin); Serial.print(" to "); Serial.print(Ymax); Serial.println(" mm.");
}
void loop() {
delay(100);
char line[LINE_BUFFER_LENGTH];
char c;
int lineIndex;
bool lineIsComment, lineSemiColon;
lineIndex = 0; lineSemiColon = false; lineIsComment = false;
while (1) {
while (Serial.available() > 0) {
c = Serial.read();
if ((c == '\n') || (c == '\r')) {
if (lineIndex > 0) {
line[lineIndex] = '\0';
if (verbose) { Serial.print("Received : "); Serial.println(line); }
processIncomingLine(line, lineIndex);
lineIndex = 0;
} else { /* 빈 줄 또는 주석 */ }
lineIsComment = false; lineSemiColon = false;
Serial.println("ok");
} else {
if (lineIsComment || lineSemiColon) {
if (c == ')') lineIsComment = false;
} else {
if (c <= ' ') { /* 공백/제어 문자 무시 */ }
else if (c == '/') { /* 블록 삭제 미지원 */ }
else if (c == '(') { lineIsComment = true; }
else if (c == ';') { lineSemiColon = true; }
else if (lineIndex >= LINE_BUFFER_LENGTH-1) { Serial.println("ERROR - lineBuffer overflow"); lineIsComment = false; lineSemiColon = false; }
else if (c >= 'a' && c <= 'z') { line[lineIndex++] = c-'a'+'A'; }
else { line[lineIndex++] = c; }
}
}
}
}
}
void processIncomingLine(char* line, int charNB) {
int currentIndex = 0;
char buffer[64];
struct point newPos; newPos.x = 0.0; newPos.y = 0.0;
while (currentIndex < charNB) {
switch (line[currentIndex++]) {
case 'U': penUp(); break;
case 'D': penDown(); break;
case 'G':
buffer[0] = line[currentIndex++]; buffer[1] = '\0';
switch (atoi(buffer)) {
case 0: case 1: // G00 & G01
char* indexX = strchr(line+currentIndex, 'X');
char* indexY = strchr(line+currentIndex, 'Y');
if (indexY <= 0) { newPos.x = atof(indexX + 1); newPos.y = actuatorPos.y; }
else if (indexX <= 0) { newPos.y = atof(indexY + 1); newPos.x = actuatorPos.x; }
else { newPos.y = atof(indexY + 1); indexY = '\0'; newPos.x = atof(indexX + 1); }
drawLine(newPos.x, newPos.y);
actuatorPos.x = newPos.x; actuatorPos.y = newPos.y;
break;
}
break;
case 'M':
buffer[0] = line[currentIndex++]; buffer[1] = line[currentIndex++]; buffer[2] = line[currentIndex++]; buffer[3] = '\0';
switch (atoi(buffer)) {
case 300:
char* indexS = strchr(line+currentIndex, 'S');
float Spos = atof(indexS + 1);
if (Spos == 30) penDown();
if (Spos == 50) penUp();
break;
case 114:
Serial.print("Absolute position : X = "); Serial.print(actuatorPos.x); Serial.print(" - Y = "); Serial.println(actuatorPos.y);
break;
default: Serial.print("Command not recognized : M"); Serial.println(buffer);
}
}
}
}
void drawLine(float x1, float y1) {
if (x1 >= Xmax) x1 = Xmax; if (x1 <= Xmin) x1 = Xmin;
if (y1 >= Ymax) y1 = Ymax; if (y1 <= Ymin) y1 = Ymin;
x1 = (int)(x1*StepsPerMillimeterX); y1 = (int)(y1*StepsPerMillimeterY);
float x0 = Xpos; float y0 = Ypos;
long dx = abs(x1-x0); long dy = abs(y1-y0);
int sx = x0 dy) {
for (i=0; i=dx) { over-=dx; myStepperY.onestep(sy,STEP); } delay(StepDelay); }
} else {
for (i=0; i=dy) { over-=dy; myStepperX.onestep(sx,STEP); } delay(StepDelay); }
}
delay(LineDelay);
Xpos = x1; Ypos = y1;
}
void penUp() { penServo.write(penZUp); delay(penDelay); Zpos=Zmax; digitalWrite(15, LOW); digitalWrite(16, HIGH); if (verbose) Serial.println("Pen up!"); }
void penDown() { penServo.write(penZDown); delay(penDelay); Zpos=Zmin; digitalWrite(15, HIGH); digitalWrite(16, LOW); if (verbose) Serial.println("Pen down."); }
라이브러리 Arduino
// Adafruit Motor Shield 라이브러리
// copyright Adafruit Industries LLC, 2009
// public domain
#if (ARDUINO >= 100) #include "Arduino.h" #else #if defined(__AVR__) #include #endif #include "WProgram.h" #endif
#include "AFMotor.h"
static uint8_t latch_state;
#if (MICROSTEPS == 8) uint8_t microstepcurve[] = {0, 50, 98, 142, 180, 212, 236, 250, 255};
#elif (MICROSTEPS == 16) uint8_t microstepcurve[] = {0, 25, 50, 74, 98, 120, 141, 162, 180, 197, 212, 225, 236, 244, 250, 253, 255};
#endif
... (이하 생략, 전체 라이브러리 코드는 다운로드 참조) ...