Introduction
สวัสดีทุกคน! โพสต์วันนี้จะพาไปดูเครื่องหยอดเหรียญต้นแบบที่ชมรม Arduino ของเราไปติดตั้งไว้ที่โรงอาหารโรงเรียนกัน
เราต้องการเครื่องหยอดเหรียญสักเครื่อง ประสบการณ์ตอนอยู่สภานักเรียนปีที่แล้วสอนให้รู้ว่าเงินทุนมันขาดตลอด และโอกาสหารายได้เพิ่มก็มีน้อย เลยตัดสินใจสร้างแหล่งรายได้ให้กับสภานักเรียนในปีต่อๆ ไปซะเลย แถมยังให้นักเรียนและครูได้หยิบของว่างมากินเป็นกำลังใจกันด้วย
หลายคนอาจสงสัยว่าเครื่องนี้มันพิเศษยังไง? ก็เครื่องหยอดเหรียญ DIY ส่วนใหญ่ที่เห็นๆ กันมักทำไว้ใช้ส่วนตัว พอผู้ใช้แตะบัตรแล้วกดปุ่ม สกรูก็หมุนเพื่อปล่อยขนมออกมา แต่เครื่องของเรานี่สำหรับทั้งโรงเรียน มันเลยต้องมีฟีเจอร์เพิ่มเติมอีกหน่อย:
- เก็บชื่อผู้ใช้และจำนวนครั้งที่จ่ายได้ (available payments) ไว้ในบัตรของแต่ละคน
- อ่านบัตรผู้ใช้ด้วยเครื่องอ่านบัตร (card reader)
- จับคู่บัตรผู้ใช้กับชื่อผู้ใช้
- รอให้ผู้ใช้เลือกขนมได้นานสุด 10 วินาที
- ลดจำนวนครั้งที่จ่ายได้อัตโนมัติเมื่อผู้ใช้เลือกขนม
- ให้นักเรียนเติมเงิน (recharge) บัตรได้โดยเพิ่มจำนวนครั้งที่จ่ายได้ตรงจากสเปรดชีต
ข้อมูลทั้งหมด ทั้งชื่อนักเรียน, UID ของบัตร, จำนวนครั้งที่จ่ายได้ ถูกเก็บไว้ในสเปรดชีต นั่นหมายความว่าแอดมินหรือคนดูแลสามารถรับเงินจากนักเรียนแล้วเพิ่มจำนวนครั้งที่จ่ายได้ให้ สเปรดชีตนี้เป็นไฟล์ .csv (ซึ่งเก็บข้อมูลข้อความที่คั่นด้วยคอมม่าในแถวและคอลัมน์ของไฟล์ Excel) ไฟล์นี้จะถูกเก็บไว้ใน SD Card
Project Overview
เครื่องหยอดเหรียญ "สำหรับสมาชิกเท่านั้น" นี้เป็นโปรเจกต์ระดับเทพที่ออกแบบมาสำหรับสถาบันอย่างโรงเรียนหรือออฟฟิศ ไม่เหมือนเครื่องหยอดเหรียญธรรมดาๆ ที่หยอดเหรียญแล้วได้ของ ระบบนี้จัดการเรื่อง การระบุตัวตนผู้ใช้ (RFID) และ การจัดการยอดเงินดิจิทัล (SD Card CSV) ด้วย สถาปัตยกรรมของมันใช้ MCU หลายตัว โดยให้ Arduino Uno ดูแลงานเฉพาะทางอย่างการอ่านเซนเซอร์ ส่วน Arduino Mega จะจัดการ Logic หลักของเครื่องหยอดเหรียญ รวมถึงการควบคุมเซอร์โวหมุนต่อเนื่อง (continuous-rotation servos) 4 ตัว โปรเจกต์นี้คือคลาสเรียนระดับมาสเตอร์ในการรวมโปรโตคอลการสื่อสารต่างๆ (SPI, UART, I2C) เข้าด้วยกันเพื่อสร้างระบบการจ่ายเงินแบบไร้เงินสดที่ลื่นไหล
Technical Architecture & Multi-MCU Strategy
- Dual-Arduino UART Bridge: เพื่อแก้ปัญหาการชนกันของพินและความไวต่อเวลา (timing sensitivity) โปรเจกต์นี้ใช้ Arduino Uno เป็นแค่ Frontend สำหรับอ่าน RFID โดยเฉพาะ Uno จะอ่าน MFRC522 ผ่าน SPI แล้วส่ง raw UID ไปให้ Arduino Mega ผ่าน UART (Hardware Serial) วิธีนี้ช่วยถ่ายโอนภาระการโพลล์ข้อมูล SPI ความถี่สูงออกจาก Mega ซึ่งกำลังยุ่งกับการจัดการจอ LCD 20x4 และจังหวะเวลาเซอร์โวอยู่
- Persistent Data Store (SD Card CSV): "ระบบเศรษฐกิจ" ของเครื่องนี้สร้างขึ้นบนฐานข้อมูล
.csvที่เก็บไว้ใน MicroSD Card- Atomicity & File Modification: เพราะไลบรารีมาตรฐาน
SD.hไม่รองรับการแก้ไขไฟล์แบบ in-place โปรเจกต์นี้จึงใช้อัลกอริทึม "Shadow Copy" เวลาจะอัปเดตยอดเงิน Mega จะอ่านไฟล์ทั้งหมดเข้าไปในหน่วยความจำ แก้ไขแถวของผู้ใช้ที่ต้องการ จากนั้นทำการFILE_WRITE | O_TRUNCเพื่อเขียนทับฐานข้อมูลใหม่ทั้งหมด วิธีนี้ช่วยให้ข้อมูลมีความสอดคล้องกัน (data consistency)
- Atomicity & File Modification: เพราะไลบรารีมาตรฐาน
- Servo Actuation & Timing: เราใช้เซอร์โวหมุนต่อเนื่องรุ่น DS04-NFC เซอร์โวแบบนี้ต่างจากเซอร์โวมาตรฐานตรงที่มันจะหมุนด้วยความเร็วแปรผันตามความกว้างของสัญญาณ PWM แทนที่จะหมุนไปหยุดที่มุมใดมุมหนึ่ง การ "หยอด" ของแต่ละครั้งจะถูกจับเวลาอย่างแม่นยำด้วย
millis()— เมื่อผู้ใช้เลือกสินค้า เซอร์โวตัวที่เกี่ยวข้องจะหมุนเป็นเวลาที่เราคาลิเบรตไว้ (เช่น 2000ms) เพื่อให้แน่ใจว่าฟันเฟืองจะหมุนพอดีให้ขนมออกมาชิ้นเดียว - State Machine Logic: เฟิร์มแวร์ทำงานเป็น Finite State Machine (FSM) โดยมีสถานะต่างๆ เช่น
WAITING_FOR_CARD,VALIDATING_USER,PRODUCT_SELECTION, และVENDINGมันมีระบบหมดเวลา (timeout) 10 วินาทีเพื่อรีเซ็ตสถานะหากผู้ใช้ไม่เลือกสินค้า รับประกันว่าเครื่องจะไม่ค้างถ้ามีคนแตะบัตรแล้วทิ้งไว้
วิศวกรรมและฮาร์ดแวร์แบบเจาะลึก
- ฟีดแบ็กจากผู้ใช้ (LCD 20x4): จอใหญ่แบบ I2C LCD นี้โชว์ข้อมูลได้ละเอียดดีเลยนะ น้องจะเห็นชื่อและ "เครดิต" ที่เหลือของน้องที่ดึงมาจากไฟล์ใน SD การที่มันโปร่งใสแบบนี้ช่วยสร้างความเชื่อมั่นในระบบจ่ายเงินดิจิทัลได้ดีเลย
- การมัลติเพล็กซ์อินพุต: ปุ่มเลือกสินค้าต่อกับตัวต้านทานแบบ pull-down โค้ดใช้ลูป
while()ที่ทำงานแบบไม่บล็อกด้วยการเช็คmillis()เพื่อคอยดูปุ่มทั้งสี่พร้อมกันในช่วงเวลาที่ให้เลือกสินค้า - การทำต้นแบบระดับอุตสาหกรรม: ต้นแบบนี้ใช้อะคริลิคและชิ้นส่วนโลหะสำหรับกลไกการจ่ายสินค้า (สกรู) แสดงให้เห็นการเปลี่ยนผ่านจาก "อิเล็กทรอนิกส์บนเบรดบอร์ด" สู่การประกอบชิ้นส่วนทางวิศวกรรมเครื่องกลแล้ว
- ความสามารถในการขยาย: แม้ตอนนี้จะใช้ไฟล์ CSV เก็บในเครื่อง แต่โครงสร้างออกแบบมาให้สามารถเปลี่ยนจาก SD Card เป็น ESP8266/ESP32 เพื่อไปดึงยอดคงเหลือจาก Google Sheet หรือฐานข้อมูล SQL กลางในอนาคตได้เลย
แผนภาพวงจร
เซ็ตอัพวงจรตามแผนภาพนี้เลย:

ไลบรารีที่ใช้
- <Servo.h> : สำหรับควบคุมเซอร์โว
- <LiquidCrystal_I2C.h> : สำหรับจอ LCD ที่ใช้สื่อสารกับผู้ใช้
- <SPI.h> : สำหรับการสื่อสารแบบ SPI กับโมดูล SD Card และโมดูล RFID
- <SD.h> : สำหรับโมดูล SD Card
- <MFRC522.h> : สำหรับโมดูล RFID reader
มอเตอร์เซอร์โว
พี่ใช้เซอร์โวหมุนต่อเนื่องรุ่น DS04-NFC จำนวนสี่ตัว ต่อวงจรตัวต้านทาน pull-down สี่วงจรเข้ากับ MCU แต่ละวงจรดูแลเซอร์โวหนึ่งตัว ใช้ millis() เขียนลูป while() เพื่อคอยเช็คตลอดว่ามีปุ่มไหนถูกกดในช่วง 10 วินาทีถัดไปหรือเปล่า พี่ลองปรับอัตราการหมุนหลายรอบเพื่อหาความเร็วและดีเลย์ที่เหมาะสมให้หมุนครบ 360 องศาพอดี หลังจาก 10 วินาทีผ่านไปโดยที่ยังไม่หมุนเซอร์โว จอ LCD จะขึ้นว่า "Paused too long. Have a nice day!" และจำนวนเงินในไฟล์ Excel ก็จะไม่เปลี่ยนแปลง
lcd.print("Choose a snack...");
preTime = millis();
while (millis() - preTime < 10000) {
if (digitalRead(redButton)) {
myServo.attach(redServoPin);
ServoAction("Red");
servoTurned = 1;
lcd.clear();
break;
}
else if (digitalRead(yellowButton)) {
// similar code
}
else if (digitalRead(greenButton)) {
// similar code
}
else if (digitalRead(blueButton)) {
// similar code
}
else {
continue;
}
}
RFID Reader
มีทิวทอเรียลเจ๋งๆ ใน YouTube เต็มไปหมดที่สอนใช้โมดูล MFRC522 reader นะ แต่มันใช้เวลาและความอดทนพอสมควรกว่าจะให้โมดูลทำงานได้ สิ่งที่พี่หวังว่าจะรู้ตั้งแต่แรกคือ โมดูลของพี่และอีกหลายๆ โมดูลมักจะขึ้น Version unknown ตอนรันตัวอย่างโค้ด "firmware_check" ที่มากับไลบรารี แต่ถึงจะขึ้นแบบนั้น มันก็ยังทำงานได้ปกติอยู่นะ ไม่ต้องตกใจ
เพราะพี่ใช้พิน SPI ของ Arduino Mega ไปแล้ว เลยต้องใช้พิน SPI ของ Arduino Uno อีกตัวเพื่อใช้โมดูล reader แล้วค่อยส่งค่า UID ของการ์ดมายัง Mega อีกที
NOTE: พี่ลองใช้ I2C ผ่านไลบรารีWireเพื่อส่ง UID จาก Uno ไป Mega ดูนะ แต่ไม่รู้ทำไม การสื่อสารมันติดขัด แล้ว UID ก็หายไป การใช้ UART นี่แหละคือทางออก การส่งข้อมูลเลยทำผ่านSerial.print(); โดยที่ Mega จะส่งไบต์เดียวกันที่มันส่งไปให้ Uno ไปแสดงที่ Serial Monitor ด้วย
ถึงอย่างนั้น ก็ยังต้องลองผิดลองถูกอีกเพียบกว่าจะส่ง UID ที่ถูกต้องไปให้ Mega ได้:
- Mega ข้ามไปสองหลักทุกครั้งที่รับข้อมูลมาแต่ละหลัก ปรากฏว่าเป็นเพราะขาด
delay(10)หลังจากการส่งข้อมูลแต่ละหลักจาก Uno - ปรากฏว่าการเพิ่มหลักข้อมูลเข้าไปในตัวแปร
Stringของ UID บน Mega ต้องทำภายในคำสั่งif Serial.available(){}เท่านั้น ไม่งั้น Mega จะเอาหลักก่อนหน้ามาบวกซ้ำตอนที่มันรอรับหลักถัดไป
การเปรียบเทียบ UID
ฟังก์ชัน: CheckUID(String UID)
ตัวแปรโกลบอล:
File myFile: สำหรับอ่านเนื้อหาในไฟล์ Excel ที่อยู่ในโมดูล SD Card
ไฟล์ใน SD Card จะเก็บข้อมูลในรูปแบบนี้:
Paul,Atreides,10,639f9a0f
Frodo,Baggins,10,332c8b0f
Chani,Kynes,10,d39300f8
Sam,Gamgee,10,2305f6f7
ไฟล์จะจัดข้อความเป็นรายการโดยคั่นด้วยเครื่องหมายจุลภาค ใครเปิดไฟล์ก็จะเห็นแบบนี้:

ตัวแปรโลคอล:
String contents: สำหรับอ่านแต่ละบรรทัดในไฟล์ Excel และเก็บเป็นออบเจ็กต์Stringchar character: สำหรับอ่านแต่ละตัวอักษรในไฟล์ Excel เพื่อนำไปบวกเข้ากับcontentsเมื่อcharacterกลายเป็น\nโปรแกรมจะรู้ว่ามันถึงจุดสิ้นสุดบรรทัดในไฟล์แล้ว และหยุดอ่านชั่วคราว จากนั้น ถ้า UID ที่ตรวจจับได้ตรงกับ UID ในcontentsมันก็จะแสดงชื่อผู้ใช้บน LCD ตัวอักษร 8 ตัวสุดท้ายของแต่ละบรรทัด (ไม่นับตัวขึ้นบรรทัดใหม่) ควรจะเป็น UID ของการ์ดผู้ใช้แต่ละคนint startIndex: ดัชนีที่โปรแกรมเริ่มอ่านบรรทัดint endIndex: ดัชนีที่โปรแกรมจบการอ่านบรรทัด
การจ่ายเงินค่าขนม
ฟังก์ชัน: DecreaseCount(String contents)
ตัวแปรโกลบอล:
File prevFile: ไฟล์เดิมFile myFile: ไฟล์ที่มีชื่อเดียวกันแต่เนื้อหาถูกลบออก เนื้อหาจากไฟล์เก่าจะถูกเพิ่มเข้ามาที่นี่ โดยจำนวนเงินจะถูกหักออก 1 หน่วย พี่อยากแยกไฟล์ใหม่ออกจากไฟล์เก่า เลยสร้างตัวแปรใหม่ขึ้นมา
หลังจากค้นคว้าหน่อยนึง พี่ก็พบว่าเป็นไปไม่ได้ที่จะแก้ไขไฟล์ใน SD Card ด้วย Arduino โดยไม่ลบเนื้อหาทั้งหมดในไฟล์ออกก่อน เลยต้องเก็บเนื้อหาเดิมไว้ในตัวแปร String ลดจำนวนเงินของผู้ใช้ที่ถูกต้องลง 1 แล้วค่อยใส่เนื้อหากลับเข้าไปในไฟล์ที่ว่างเปล่าใหม่
myFile = SD.open("SA_cards.csv", FILE_WRITE | O_TRUNC);
พี่ใช้วิธีเดียวกันกับตอนเปรียบเทียบ UID กับ UID ของผู้ใช้รายอื่น เมื่อโปรแกรมมองหาบรรทัดที่ต้องแก้ไข
ตัวแปรโลคอล:
String prevFileContents: เนื้อหาเดิมของไฟล์char data: สำหรับอ่านแต่ละตัวอักษรในไฟล์String piece: บรรทัดที่นำมาเปรียบเทียบกับcontentsซึ่งคือบรรทัดของชื่อและ UID ของผู้ใช้ปัจจุบัน
ปัญหาของวิธีนี้คือ ในอนาคตถ้ามีการเพิ่มบรรทัดเข้าไปในไฟล์มากขึ้น ขนาดของออบเจ็กต์ String ที่ใหญ่ขึ้นอาจใช้หน่วยความจำมากกว่าที่ Arduino จะรับไหว อย่างไรก็ตาม ต้นแบบของพี่จนถึงตอนนี้ยังไม่มีปัญหาอะไรเลย