아두이노 기반 스낵 자판기 - 설계 & 제작 가이드
부품 및 소모품
| 아두이노 우노 |
× |
1 |
| 센서 쉴드 |
× |
1 |
| DC 모터 6V (휠 포함) |
× |
3 |
| 적외선(반사형) 또는 라인 팔로워 센서, 상품 감지용 |
× |
3 |
| 근접(전자기) 센서, 동전 센서 1번용 |
× |
1 |
| 거리 인터럽터(적외선/옵토커플러) 센서, 동전 센서 2번용 |
× |
1 |
| DC 모터용 6V 전원 공급 장치 |
× |
1 |
| 릴레이 (일반) - 4채널 릴레이 |
× |
1 |
| LCD (노키아 5110 LCD) |
× |
1 |
| LED (일반) - 백색 LED |
× |
2 |
| 푸시 버튼 - 빨강, 노랑, 초록 |
× |
3 |
앱 및 온라인 서비스
| 아두이노 IDE |
| 솔리드웍스 (또는 3D 모델링 툴) |
프로젝트 소개
소개
안녕하세요, 인도네시아에서 인사드립니다.
이 스낵 자판기는 제 졸업 작품입니다. 실제로는 2015년 말에 완성했습니다.
이 프로젝트의 목표는 아두이노 마이크로컨트롤러로 구동되는 자판기를 만들고, "동네 가게에서 싸고 쉽게 구할 수 있는" 부품만 사용하는 것이었습니다.
아래 영상은 스낵 자판기 최종 테스트 영상으로, 결제/동전 테스트, 상품 선택, 상품 배출 과정을 보여줍니다.
목표를 달성하기 위해 제가 한 일은 다음과 같습니다:
1단계: 컨셉 스케치
가장 먼저, 컨셉을 잡습니다.
이 스케치는 제가 만들지도 모를 것에 대한 첫 아이디어입니다. 스낵 자판기, 동전 결제, 아크릴 케이스 사용 예정입니다.
컨셉을 골라 다듬으세요, 어떤 시나리오, 어떤 조건이든 다 생각해보세요.
모든 걸 스케치하세요. 그림 실력 걱정 마세요, 진심으로 제 그림 보세요. :D
프로젝트 시작 전이 상상도 못 한 것을 생각할 적기입니다.
2단계: 3D 모델 생성
모든 사람에게 필요한 단계는 아닐 수 있지만, 저와 이 프로젝트에는 필수적이었습니다.
솔리드웍스로 모델을 만들었습니다. 정말 완전히 완성된 모델입니다. 제품(스낵 포장) 치수, 허용 동전 크기, 거부 동전 크기, 동전 투입구 크기, 동전 경사로 크기, 푸시 버튼 크기, 거의 모든 것의 치수를 결정해야 했습니다. 이 단계에는 아두이노 마이크로컨트롤러 크기 비교/선정/측정, 사용할 센서와 향후 추가 가능 센서, 배치 위치, 치수, 허용 동전 보관 위치, 모노크롬 LCD 배치(필수는 아니지만 고집함)까지 포함됩니다.
마지막으로, 솔리드웍스 모델링할 때 모든 부품용 나사 구멍 만드는 걸 잊지 마세요.
케이스는 아크릴 소재로 레이저 커팅으로 만듭니다. 그래서 모델을 최대한 자세하고 정확하게 만들었습니다. 모델이 정확할수록 더 정밀한 케이스가 나오기 때문입니다.
3단계: 동전 투입구 설계
동전 선별을 위해 먼저 동전 치수를 이용합니다. 동전 투입구를 설계해 어떤 크기 동전이 기계에 들어올지 결정합니다.
두 번째 필터로 어떤 크기 동전이 허용되어 동전 홀더로 굴러갈지 결정합니다.
중력을 이용해 동전 크기(지름)가 동전 경사로/트랙 길이보다 작으면 떨어져서 거부됩니다.
- 위 사진에서 왼쪽 동전은 투입구에 안 들어갑니다.
- 가운데 동전은 떨어져서 거부됩니다.
- 오른쪽 동전은 허용 동전으로 홀더까지 내려갑니다.
이 방식이 어떻게 작동하는지 보여주는 동전 테스트 영상입니다.
4단계: 모든 부품 준비 및 테스트
아두이노 준비하고 필요한 모든 것 챙기세요.
모든 부품 테스트하세요. LCD, 푸시 버튼, 릴레이, LED, 이것저것 조합해보고 다양한 로직과 코딩을 익히세요. 자신이 뭘 하는지 확실히 아세요. 한계까지 테스트해보고, 한계를 찾되 부수지는 마세요.
왜 저항을 쓰나, 이게 저렇게 되면 어찌 되나, 왜 센서 읽기가 불안정한가, 센서가 실제 읽는 값은 뭔가 등등. 테스트해보니 근접 센서 읽기가 "깨끗하지 않아" 저항을 추가해 안정화해야 했습니다.
적외선 센서는 직사광선 아래서 매우 나쁘고 비정상적으로 작동합니다(이건 알고 있었지만 실제로 영향받아보니 처음이었습니다). 해결책은 덕트 테이프로 적외선 센서 수신부 주변을 격리하는 것입니다(사진 참조).
5단계: 나선형 메커니즘 제작
자판기에서 가장 중요한 부분 중 하나인 배출 메커니즘입니다.
나선형 메커니즘을 쓰는데, 알루미늄 와이어(지름 2mm)를 9cm 지름 PVC 파이프 도움을 받아 손으로 나선형으로 구부렸습니다.
나선형 와이어를 케이블 타이로 휠에 묶습니다. DC 모터가 켜지면 휠이 돌고 나선형 와이어가 회전합니다.
회전하는 나선은 상품의 회전 운동을 선형 운동으로 변환합니다.
6단계: 동전 식별 메커니즘 제작
첫 번째 사진은 동전 센서 1번으로, 근접 센서 또는 유도 전자기 센서를 사용합니다. 허용 동전이 이 센서를 지나가면 아두이노가 시간 측정(밀리초)을 시작합니다.
센서1 감지되면 밀리초 단위 시간 측정 시작
허용 동전이 동전 센서 2번(거리 인터럽터 센서)을 지나가면 아두이노가 시간 측정을 멈추고, 그 동전이 센서 1에서 센서 2까지 이동하는 데 걸린 시간을 얻습니다.
센서2 감지되면 밀리초 단위 시간 측정 멈춤. "이동 시간"은 xxx 밀리초
자석을 너무 많이 쓰면 동전이 동전 경사로 중간에 걸려 멈춥니다. 우리는 그런 거 원치 않습니다. 모든 동전의 센서 1과 2 사이 "이동 시간"을 측정해야 하니까요. 이 "이동 시간"(밀리초)은 아두이노가 허용 동전의 가치를 판단하는 데 쓰입니다.
"이동 시간"이 "yyy" 밀리초 이상이면 "aaa" 동전, 아니면 "bbb" 동전
자석으로 동전을 식별하는 이 방식을 테스트하는 영상입니다.
7단계: 상품 감지 메커니즘 제작
나선형 메커니즘으로 밀려/움직여서 떨어지는 상품을 감지하려고 적외선 센서 3개를 씁니다.
상품이 떨어지면 적외선 센서가 트리거돼 DC 모터에 OFF 신호를 보냅니다(릴레이 경유).
이 자판기에서 상품 감지가 어떻게 작동하는지 보여주는 영상입니다.
8단계: LCD로 정보 표시
이 프로젝트에서 LCD는 (인도네시아 통화로) 허용된 동전 가치, 동전 투입 시마다 총 잔액, 기계 작동 단계별 일반 안내를 표시합니다.
기계 켰을 때
- 메인 화면 = "동전을 넣으세요"
- 동전 투입 시 = "동전 가치 표시, 총 잔액 표시"
- 동전 미투입 시 = 메인 화면 유지
동전 성공적으로 식별 후
- 총 잔액 ≥ 상품 가격 = "상품을 선택하세요"
- 총 잔액 < 상품 가격 = "동전을 더 넣으세요"
나선형 메커니즘 작동 후(배출 발생)
- 상품 감지 메커니즘 트리거 = "감사합니다", 거래 완료
- 딜레이 후 메인 화면으로 복귀
9단계: 마무리 및 테스트
드디어 스낵 자판기 완성! ;)
3개월쯤 지나 최종 테스트를 했습니다. 결제/동전 테스트, 상품 선택, 상품 배출.
이게 스낵 자판기 최종 테스트 영상입니다:
이 프로젝트가 여러분께 도움이 되길, 혹시 다음 아두이노 자판기 제작에 영감이나 아이디어를 드리길 바랍니다.
10단계: 개선 여지
이 스낵 자판기를 개선할 아이디어들:
- 거스름돈 메커니즘 추가
- 지폐 인식 추가
- 비트코인/디지털 결제 추가
- LCD에 상품 재고 표시
- IoT: 거래 발생 시 인터넷으로 알림
- IoT: 상품 재고 부족/품절 시 알림 등
특별 감사: ITENAS 반둥, 기계공학과.
코드
스낵 자판기 소스 코드 아두이노
이 자판기 프로젝트에 쓴 소스 코드입니다
const byte SIAP = 0;
const byte KOIN_MASUK1 = 1;
const byte KOIN_MASUK2 = 2;
const byte KOIN_MASUK3 = 3;
const byte KOIN_MASUK4 = 4;
const byte KOIN_MASUK5 = 5;
const byte KOIN_MASUK6 = 6;
const byte KOIN_MASUK7 = 7;
const byte SALDO_500 = 8;
const byte SALDO_500500 = 9;
const byte SALDO_500500500 = 10;
const byte SALDO_500500500500 = 11;
const byte SALDO_5005005001000 = 12;
const byte SALDO_5001000 = 13;
const byte SALDO_5001000500 = 14;
const byte SALDO_50010001000 = 15;
const byte SALDO_5005001000 = 16;
const byte SALDO_1000 = 17;
const byte SALDO_10001000 = 18;
const byte SALDO_1000500 = 19;
const byte SALDO_1000500500 = 20;
const byte SALDO_10005001000 = 21;
const byte KOIN_KELUAR = 22;
byte state = SIAP;
const float jeda_waktu = 876;
unsigned long time1, time2, time3, time4, time5, time6, time7;
#define PIN_SCE 7
#define PIN_RESET 6
#define PIN_DC 5
#define PIN_SDIN 4
#define PIN_SCLK 3
#define LCD_C LOW
#define LCD_D HIGH
#define LCD_X 84
#define LCD_Y 48
static const byte ASCII[][5] = {
{0x00, 0x00, 0x00, 0x00, 0x00}, // 20
{0x00, 0x00, 0x5f, 0x00, 0x00}, // 21 !
{0x00, 0x07, 0x00, 0x07, 0x00}, // 22 "
{0x14, 0x7f, 0x14, 0x7f, 0x14}, // 23 #
{0x24, 0x2a, 0x7f, 0x2a, 0x12}, // 24 $
{0x23, 0x13, 0x08, 0x64, 0x62}, // 25 %
{0x36, 0x49, 0x55, 0x22, 0x50}, // 26 &
{0x00, 0x05, 0x03, 0x00, 0x00}, // 27 '
{0x00, 0x1c, 0x22, 0x41, 0x00}, // 28 (
{0x00, 0x41, 0x22, 0x1c, 0x00}, // 29 )
{0x14, 0x08, 0x3e, 0x08, 0x14}, // 2a *
{0x08, 0x08, 0x3e, 0x08, 0x08}, // 2b +
{0x00, 0x50, 0x30, 0x00, 0x00}, // 2c ,
{0x08, 0x08, 0x08, 0x08, 0x08}, // 2d -
{0x00, 0x60, 0x60, 0x00, 0x00}, // 2e .
{0x20, 0x10, 0x08, 0x04, 0x02}, // 2f /
{0x3e, 0x51, 0x49, 0x45, 0x3e}, // 30 0
{0x00, 0x42, 0x7f, 0x40, 0x00}, // 31 1
{0x42, 0x61, 0x51, 0x49, 0x46}, // 32 2
{0x21, 0x41, 0x45, 0x4b, 0x31}, // 33 3
{0x18, 0x14, 0x12, 0x7f, 0x10}, // 34 4
{0x27, 0x45, 0x45, 0x45, 0x39}, // 35 5
{0x3c, 0x4a, 0x49, 0x49, 0x30}, // 36 6
{0x01, 0x71, 0x09, 0x05, 0x03}, // 37 7
{0x36, 0x49, 0x49, 0x49, 0x36}, // 38 8
{0x06, 0x49, 0x49, 0x29, 0x1e}, // 39 9
{0x00, 0x36, 0x36, 0x00, 0x00}, // 3a :
{0x00, 0x56, 0x36, 0x00, 0x00}, // 3b ;
{0x08, 0x14, 0x22, 0x41, 0x00}, // 3c <
{0x14, 0x14, 0x14, 0x14, 0x14}, // 3d =
{0x00, 0x41, 0x22, 0x14, 0x08}, // 3e >
{0x02, 0x01, 0x51, 0x09, 0x06}, // 3f ?
{0x32, 0x49, 0x79, 0x41, 0x3e}, // 40 @
{0x7e, 0x11, 0x11, 0x11, 0x7e}, // 41 A
{0x7f, 0x49, 0x49, 0x49, 0x36}, // 42 B
{0x3e, 0x41, 0x41, 0x41, 0x22}, // 43 C
{0x7f, 0x41, 0x41, 0x22, 0x1c}, // 44 D
{0x7f, 0x49, 0x49, 0x49, 0x41}, // 45 E
{0x7f, 0x09, 0x09, 0x09, 0x01}, // 46 F
{0x3e, 0x41, 0x49, 0x49, 0x7a}, // 47 G
{0x7f, 0x08, 0x08, 0x08, 0x7f}, // 48 H
{0x00, 0x41, 0x7f, 0x41, 0x00}, // 49 I
{0x20, 0x40, 0x41, 0x3f, 0x01}, // 4a J
{0x7f, 0x08, 0x14, 0x22, 0x41}, // 4b K
{0x7f, 0x40, 0x40, 0x40, 0x40}, // 4c L
{0x7f, 0x02, 0x0c, 0x02, 0x7f}, // 4d M
{0x7f, 0x04, 0x08, 0x10, 0x7f}, // 4e N
{0x3e, 0x41, 0x41, 0x41, 0x3e}, // 4f O
{0x7f, 0x09, 0x09, 0x09, 0x06}, // 50 P
{0x3e, 0x41, 0x51, 0x21, 0x5e}, // 51 Q
{0x7f, 0x09, 0x19, 0x29, 0x46}, // 52 R
{0x46, 0x49, 0x49, 0x49, 0x31}, // 53 S
{0x01, 0x01, 0x7f, 0x01, 0x01}, // 54 T
{0x3f, 0x40, 0x40, 0x40, 0x3f}, // 55 U
{0x1f, 0x20, 0x40, 0x20, 0x1f}, // 56 V
{0x3f, 0x40, 0x38, 0x40, 0x3f}, // 57 W
{0x63, 0x14, 0x08, 0x14, 0x63}, // 58 X
{0x07, 0x08, 0x70, 0x08, 0x07}, // 59 Y
{0x61, 0x51, 0x49, 0x45, 0x43}, // 5a Z
{0x00, 0x7f, 0x41, 0x41, 0x00}, // 5b [
{0x02, 0x04, 0x08, 0x10, 0x20}, // 5c ¥
{0x00, 0x41, 0x41, 0x7f, 0x00}, // 5d ]
{0x04, 0x02, 0x01, 0x02, 0x04}, // 5e ^
{0x40, 0x40, 0x40, 0x40, 0x40}, // 5f _
{0x00, 0x01, 0x02, 0x04, 0x00}, // 60 `
{0x20, 0x54, 0x54, 0x54, 0x78}, // 61 a
{0x7f, 0x48, 0x44, 0x44, 0x38}, // 62 b
{0x38, 0x44, 0x44, 0x44, 0x20}, // 63 c
{0x38, 0x44, 0x44, 0x48, 0x7f}, // 64 d
{0x38, 0x54, 0x54, 0x54, 0x18}, // 65 e
{0x08, 0x7e, 0x09, 0x01, 0x02}, // 66 f
{0x0c, 0x52, 0x52, 0x52, 0x3e}, // 67 g
{0x7f, 0x08, 0x04, 0x04, 0x78}, // 68 h
{0x00, 0x44, 0x7d, 0x40, 0x00}, // 69 i
{0x20, 0x40, 0x44, 0x3d, 0x00}, // 6a j
{0x7f, 0x10, 0x28, 0x44, 0x00}, // 6b k
{0x00, 0x41, 0x7f, 0x40, 0x00}, // 6c l
{0x7c, 0x04, 0x18, 0x04, 0x78}, // 6d m
{0x7c, 0x08, 0x04, 0x04, 0x78}, // 6e n
{0x38, 0x44, 0x44, 0x44, 0x38}, // 6f o
{0x7c, 0x14, 0x14, 0x14, 0x08}, // 70 p
{0x08, 0x14, 0x14, 0x18, 0x7c}, // 71 q
{0x7c, 0x08, 0x19, 0x29, 0x46}, // 72 r
{0x48, 0x54, 0x54, 0x54, 0x20}, // 73 s
{0x04, 0x3f, 0x44, 0x40, 0x20}, // 74 t
{0x3c, 0x40, 0x40, 0x20, 0x7c}, // 75 u
{0x1c, 0x20, 0x40, 0x20, 0x1c}, // 76 v
{0x3c, 0x40, 0x30, 0x40, 0x3c}, // 77 w
{0x44, 0x28, 0x10, 0x28, 0x44}, // 78 x
{0x0c, 0x50, 0x50, 0x50, 0x3c}, // 79 y
{0x44, 0x64, 0x54, 0x4c, 0x44}, // 7a z
{0x00, 0x08, 0x36, 0x41, 0x00}, // 7b {
{0x00, 0x00, 0x7f, 0x00, 0x00}, // 7c |
{0x00, 0x41, 0x36, 0x08, 0x00}, // 7d }
{0x10, 0x08, 0x08, 0x10, 0x08}, // 7e ←
{0x78, 0x46, 0x41, 0x46, 0x78} // 7f →
};
void LcdWrite(byte dc, byte data) {
digitalWrite(PIN_DC, dc);
digitalWrite(PIN_SCE, LOW);
shiftOut(PIN_SDIN, PIN_SCLK, MSBFIRST, data);
digitalWrite(PIN_SCE, HIGH);
}
void LcdCharacter(char character) {
LcdWrite(LCD_D, 0x00);
for (int index = 0; index < 5; index++) {
LcdWrite(LCD_D, ASCII[character - 0x20][index]);
}
LcdWrite(LCD_D, 0x00);
}
void LcdClear(void) {
for (int index = 0; index < LCD_X * LCD_Y / 8; index++) {
LcdWrite(LCD_D, 0x00);
}
}
void LcdInitialise(void) {
pinMode(PIN_SCE, OUTPUT);
pinMode(PIN_RESET, OUTPUT);
pinMode(PIN_DC, OUTPUT);
pinMode(PIN_SDIN, OUTPUT);
pinMode(PIN_SCLK, OUTPUT);
digitalWrite(PIN_RESET, LOW);
digitalWrite(PIN_RESET, HIGH);
LcdWrite(LCD_C, 0x21);
LcdWrite(LCD_C, 0xB1);
LcdWrite(LCD_C, 0x04);
LcdWrite(LCD_C, 0x14);
LcdWrite(LCD_C, 0x20);
LcdWrite(LCD_C, 0x0C);
}
void LcdString(char *characters) {
while (*characters) {
LcdCharacter(*characters++);
}
}
void setup() {
pinMode(11, INPUT);
pinMode(12, INPUT);
pinMode(14, INPUT);
pinMode(15, INPUT);
pinMode(16, INPUT);
pinMode(17, INPUT);
pinMode(18, INPUT);
pinMode(19, INPUT);
pinMode(13, OUTPUT);
pinMode(8, OUTPUT);
pinMode(9, OUTPUT);
pinMode(10, OUTPUT);
pinMode(2, OUTPUT);
digitalWrite(8, HIGH);
digitalWrite(9, HIGH);
digitalWrite(10, HIGH);
digitalWrite(13, LOW);
Serial.begin(9600);
Serial.println("=> Siap");
LcdInitialise();
LcdClear();
LcdString(" Masukkan Koin Rp500 atau Koin Rp1000 @ Rp2000");
delay(500);
}
void loop() {
int x = analogRead(14);
if (state == SIAP) {
if (x >= 33) {
state = KOIN_MASUK1;
time1 = millis();
tone(2, 4200, 40);
Serial.println("snsr1,");
Serial.println("start wkt");
}
} else if ((state == KOIN_MASUK1) && (!(digitalRead(13)))) {
if ((digitalRead(12)) && (state == KOIN_MASUK1)) {
unsigned long diff1 = (millis() - time1);
Serial.println("snsr2,");
Serial.println("stop wkt");
Serial.print("jeda wkt: ");
Serial.print(diff1);
Serial.println(" ms");
if (diff1 > jeda_waktu) {
state = SALDO_1000;
LcdInitialise(); LcdClear();
LcdString(" diterima Saldo=Rp1000 Tambah Koin Koin Rp1000");
digitalWrite(13, HIGH);
Serial.println("koin=Rp 1000, Saldo: Rp1000");
tone(2, 4200, 299);
} else if (diff1 < jeda_waktu) {
state = SALDO_500;
digitalWrite(13, HIGH);
LcdInitialise(); LcdClear();
LcdString(" diterima Saldo= Rp500 Tambah Koin Koin Rp500");
Serial.println("koin=Rp 500, Saldo: Rp500");
tone(2, 4200, 299);
}
}
}
// ... (이하 상태 머신 로직 계속, 생략)
}
커스텀 파트 및 인클로저
이 프로젝트용 솔리드웍스 3D 모델 스크린샷입니다.