ประโยชน์หลักๆ ของนาฬิกาอาร์ดูโน่ที่ใช้ GPS ตัวนี้คือการออกไปสำรวจข้างนอกบ้านนั่นเอง มันเป็นเครื่องมืออเนกประสงค์ขนาดกะทัดรัดที่ใช้จับเวลา ตำแหน่ง และข้อมูลการเคลื่อนไหวได้ในตัวเดียว นอกจากนี้ยังเอาไปใช้ในงานวิจัยภาคสนามที่ต้องการข้อมูล GPS แม่นยำและข้อมูลที่มีการบันทึกเวลาเพื่อการศึกษาทางวิทยาศาสตร์ได้ด้วย การถอดชิ้นส่วนครั้งนี้เป็นแหล่งความรู้ชั้นดีสำหรับนักเล่นและนักเรียนสายช่าง งั้นเรามาดูกันเลยดีกว่า!
ภาพรวมโปรเจค
"Celestial-Chrono" คือการนำ การซิงค์เวลาผ่านเครือข่ายดาวเทียม และ การวิเคราะห์บิตสตรีม NMEA-0183 มาทำให้เป็นจริงแบบจัดเต็ม ออกแบบมาเป็นเครื่องมือนำทางอิสระที่มีความแม่นยำสูง ระบบนี้จะดึงข้อมูลเวลาจากนาฬิกาอะตอมและพิกัดทางภูมิศาสตร์จากกลุ่มดาวเทียมระบบกำหนดตำแหน่งบนโลก (GPS) อุปกรณ์นี้มาพร้อมกับเมนูลอจิกขับเคลื่อนผ่าน I2C ที่ซับซ้อน ทำให้ผู้ใช้สามารถสลับระหว่างแดชบอร์ดแบบเรียลไทม์ การวิเคราะห์พิกัดเชิงพื้นที่ และการแสดงข้อมูลเทเลเมทรีในรูปแบบ QR-code ได้ การสร้างนี้เน้นไปที่การวินิจฉัยการแยกวิเคราะห์แพ็กเก็ต NMEA, การแสดงผลแบบฮิวริสติกส์บน SSD1306 และการวิเคราะห์ความแม่นยำของเวลาระดับต่ำกว่าไมโครวินาที
ฮาร์ดแวร์
ปุ่มกดสามารถต่อสายได้ตามนี้:

จอแสดงผล OLED เป็นอีกชิ้นส่วนที่มีประโยชน์สุดๆ และสามารถใช้ได้กับหัวข้อที่หลากหลาย ตั้งแต่การแสดงผลอินพุต, GUI (อินเทอร์เฟซผู้ใช้แบบกราฟิก) และอื่นๆ อีกเพียบ! มาดูการต่อสายกัน:

โมดูล GPS เป็นหนึ่งในอุปกรณ์สื่อสารระยะไกลที่สะดวกและใช้ง่ายที่สุดในท้องตลาด แม้ว่าโมดูล LoRa และ GPS บางรุ่นจะส่งข้อมูลได้ไกลมาก แต่ NEO-6m ในกรณีนี้เราใช้เพื่อดึงข้อมูลเท่านั้น มาดูสรุปการทำงานของมันกัน:
- การสื่อสารกับดาวเทียม: รับสัญญาณจากดาวเทียม GPS เพื่อกำหนดตำแหน่งของมันบนโลก
- การส่งออกข้อมูล NMEA: ส่งประโยคมาตรฐาน NMEA ที่มีข้อมูลตำแหน่ง ความเร็ว และเวลาไปยัง Arduino ผ่านการสื่อสารแบบอนุกรม ข้อมูลเหล่านี้จะถูกแปลผ่านไลบรารี TinyGPS++
- ความต้องการเสาอากาศ: ต้องการเสาอากาศภายนอกหรือในตัวเพื่อรับสัญญาณดาวเทียมได้อย่างมีประสิทธิภาพ
นี่คือการต่อสายสำหรับโมดูล GPS Neo-6m:

สำหรับอุปกรณ์ตัวนี้ เราจะใช้ปุ่มกดแค่ 3 จากทั้งหมด 4 ปุ่ม (ดูแผนผังด้านล่าง) แต่คุณสามารถใช้ปุ่มเหล่านี้จำนวนเท่าไหร่ก็ได้สำหรับแอปพลิเคชันอื่นๆ (มาแชร์ไอเดียกันในคอมเมนต์นะ อยากเห็นจังเลย)
สุดท้ายแล้ว นี่คือแผนผังเต็มรูปแบบ:

ลงลึกแบบช่างๆ
- NEO-6M NMEA-0183 Serial Forensics:
- The Serial-Stream Bit-Buffer: ตัว Neo-6M มันส่งข้อมูลแบบ asynchronous serial packets $(\text{RX/TX})$ ที่ความเร็ว $9600\text{ baud}$ งานของเราคือมาจับแพ็กเก็ตให้ได้ โดยดูที่หัวข้อความ (sentence headers) พวกนี้:
$GPRMC(เวลา/ตำแหน่ง) และ$GPGGA(ความสูง/คุณภาพสัญญาณ) เป้าหมายคือให้ได้ "Fix-Time" ที่เร็วสุดๆ วิธีจัดเต็มคือใช้เสาอากาศภายนอก (external active antenna) เพื่อล็อกดาวเทียมให้ได้ $\ge 4$ ดวงแบบแน่นหนึบ จะได้ค่าตำแหน่งที่แม่นยำสุดๆ - SoftwareSerial Signal-Stiffness: เราจะสงวนฮาร์ดแวร์ UART ไว้ใช้ดีบั๊ก ดังนั้นลิงก์ GPS จะต่อผ่าน
SoftwareSerial(พิน D10, D11) งานสำคัญคือต้องรับสตริง NMEA แบบ interrupt-driven ให้ครบ อย่าให้มีบิตไหนหลุดหายระหว่างที่ข้อมูลวิ่งจากโมดูล GPS เข้ามายังสมองของ Nano
- The Serial-Stream Bit-Buffer: ตัว Neo-6M มันส่งข้อมูลแบบ asynchronous serial packets $(\text{RX/TX})$ ที่ความเร็ว $9600\text{ baud}$ งานของเราคือมาจับแพ็กเก็ตให้ได้ โดยดูที่หัวข้อความ (sentence headers) พวกนี้:
- I2C Unified Graphics & Menu Heuristics:
- The U8G Graphics-Engine Pipeline: จอ SSD1306 เราใช้บัส I2C $(SDA/SCL)$ ขับ ต้องตั้งค่าให้ใช้โหมด "Fast" I2C ในไลบรารี U8G เพื่อรีเฟรช UI ได้ไวสุดๆ เคล็ดลับคือใช้ฟังก์ชัน
drawBitmapP()ในการวาดภาพพวก QR code ขนาด $64\times 64$ และไอคอนแดชบอร์ด $16\times 16$ ที่เก็บไว้ในPROGMEMเพื่อประหยัดพื้นที่ SRAM อันน้อยนิดของ Nano - HMI Debounce & State-Machine Analytics: ระบบเมนูใช้ปุ่มกด 3 ปุ่มเป็นอินพุต ต้องจัดการเรื่องเด้งของสัญญาณ (debouncing) แบบซอฟต์แวร์ $(flag\text{-based})$ เพื่อสลับระหว่างหน้าจอต่างๆ ได้อย่างลื่นไหล ไร้ดีเลย์ ระหว่างหน้าแสดงความเร็ว GPS กับหน้าเมนู
- The U8G Graphics-Engine Pipeline: จอ SSD1306 เราใช้บัส I2C $(SDA/SCL)$ ขับ ต้องตั้งค่าให้ใช้โหมด "Fast" I2C ในไลบรารี U8G เพื่อรีเฟรช UI ได้ไวสุดๆ เคล็ดลับคือใช้ฟังก์ชัน
วิศวกรรมและการลงมือทำ
- Temporal-Timestamp Persistence Forensics:
- Leap-Second & Time-Zone Interpolation: ข้อมูลจาก GPS มันเป็นเวลา UTC หมดเลย งานของเราคือต้องเขียนลอจิกเพิ่ม offset เข้าไปเพื่อแสดงเวลาให้ตรงกับโซนเวลาของเรา และต้องแน่ใจว่าเวลาที่แสดงบนจอ OLED มันไม่ค่อย (time-drift $(\approx 10\text{-}100\text{ns})$) จนเกินไป
- Antenna-Impedance Diagnostics: ตัว Neo-6M ต้องเห็นท้องฟ้าโล่งๆ นะ ต้องตรวจสอบความสมบูรณ์ของสัญญาณ (signal integrity) บนลาย PCB ระหว่างโมดูลกับเสาอากาศเซรามิก อย่าให้เกิดการสูญเสียสัญญาณ (path-loss) จนอัตราส่วนสัญญาณต่อสัญญาณรบกวน $(SNR)$ ตกลงไป เพราะจะทำให้ล็อกตำแหน่งไม่ได้
- Structural Mechatronics & 3D-Printer Forensics:
- ตัวเครื่องทั้งหมดจะอยู่ในเคสที่พิมพ์จากเครื่อง Ultimaker S3 ต้องออกแบบให้สแตนออฟภายในมีระยะเผื่อ (tolerances) ที่พอดี เพื่อให้หน้า OLED และเสาอากาศ Neo-6M อยู่ชิดกับผิวด้านนอกสุด งานนี้เพื่อความชัดเจนในการใช้งาน (ergonomic-clarity) และให้สัญญาณผ่านได้ดี (structural signal-transparency) นั่นเอง
โค้ด
ก่อนเริ่ม ไปดาวน์โหลดไลบรารี U8g มาด้วยนะ แล้วเพิ่มเข้าไปในโปรเจค Arduino ผ่าน Sketch > Include Libraries > Add .ZIP Library ส่วนไลบรารี TinyGPS++ ก็ติดตั้งให้เรียบร้อย
มาแกะโค้ดกันทีละขั้นตอน:
Variables Declarations:
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_DEV_0 | U8G_I2C_OPT_NO_ACK | U8G_I2C_OPT_FAST);
TinyGPSPlus gps;
SoftwareSerial gpsSerial(10, 11)); //RX, TX
- ตั้งค่าไลบรารี u8g เพื่อให้ทำงานเร็วและรีเฟรชได้ไว
- ตั้งค่าโมดูล GPS โดยใช้พิน 10 เป็น RX และพิน 11 เป็น TX
const unsigned char epd_bitmap_qr_code [] PROGMEM = {...};
const unsigned char bitmap_icon_dashboard[] PROGMEM = {...};
const unsigned char bitmap_icon_gps_speed[] PROGMEM = {...};
const unsigned char bitmap_icon_knob_over_oled[] PROGMEM = {...};
const unsigned char* bitmap_icons[8] = {
bitmap_icon_dashboard,
bitmap_icon_gps_speed,
bitmap_icon_knob_over_oled,
};
const unsigned char bitmap_scrollbar_background[] PROGMEM = {...};
const unsigned char bitmap_item_sel_outline[] PROGMEM = {...};
1. `pd_bitmap_Aerospace_qr_code`: เก็บข้อมูล QR Code ขนาด 64x64 สำหรับอะไรก็ได้ที่อยากใส่ (ดูด้านล่างว่าจะสร้าง QR Code 64x64 ยังไง)
2. `bitmap_icon_...`: เก็บข้อมูลไอคอนขนาด 16x16 พิกเซล สำหรับแต่ละเมนู
3. `bitmap_icons[8]`: เก็บรายการเมนูตามลำดับที่จะแสดงบนหน้าจอ
4. `bitmap_scrollbar_background[]`: แถบข้างสำหรับแสดงตำแหน่งของเคอร์เซอร์
5. `bitmap_item_sel_outline`: วงแสงที่แสดงรอบๆ ไอเท็มที่ถูกเลือก
#define BUTTON_UP_PIN 12
#define BUTTON_SELECT_PIN 8
#define BUTTON_DOWN_PIN 4
1. การประกาศพิน (Pin) สำหรับปุ่มต่างๆ (มีปุ่มเพิ่มเติมบนพิน 10 สำหรับเคสอื่นๆ)
int button_up_clicked = 0;
int button_select_clicked = 0;
int button_down_clicked = 0;
int item_selected = 0;
int item_sel_previous;
int item_sel_next;
int current_screen = 0;
1. `button_up_clicked, button_select_clicked & button down clicked`: ตัวแปรสำหรับจัดการเดานซ์ (Debounce) ปุ่มกด (ค่าเป็น 1 เมื่อกด, 0 เมื่อปล่อย)
2. `item_selected, item_sel_previous & item_sel_next`: ใช้ติดตามตำแหน่งเคอร์เซอร์ในรายการเมนู
3. `current_screen`: ใช้ติดตามดัชนี (index) ของหน้าจอปัจจุบันจากอาร์เรย์ `menu_items`
float lat;
float lon;
int day;
int mon;
int yr;
int hr;
int minute;
int sec;
int speed;
int altitude;
1. ตัวแปรพวกนี้เก็บข้อมูลจากโมดูล GPS ถ้าข้อมูลนั้นถูกต้องและอัปเดตแล้ว
int progress = 0;
char buffer[32];
1. `progress`: ติดตามความคืบหน้าของ Progress Bar บนหน้าจอโหลด
2. `buffer`: ใช้สร้างสตริงเพื่อแสดงความคืบหน้าบน Progress Bar
**ฟังก์ชัน Loop():**
void loop() { if (current_screen == 0) { if ((digitalRead(BUTTON_UP_PIN) == LOW) && (button_up_clicked == 0)) { //item_selected goes down to previous item //If the item_selected is 0, wrap to last element } else if ((digitalRead(BUTTON_DOWN_PIN) == LOW) && (button_down_clicked == 0)) { //item selected goes up to next item //If the item selected is the last element, wrap to first index ( } if ((digitalRead(BUTTON_UP_PIN) == HIGH) && (button_up_clicked == 1)) {button_up_clicked = 0;} if ((digitalRead(BUTTON_DOWN_PIN) == HIGH) && (button_down_clicked == 1)) {button_down_clicked = 0;} } if ((digitalRead(BUTTON_SELECT_PIN) == LOW) && (button_select_clicked == 0)) { button_select_clicked = 1; if (current_screen == 0) { //Assign current screen to next screen (selected is pressed_ } else { //Set current screen to 0 } } if ((digitalRead(BUTTON_SELECT_PIN) == HIGH) && (button_select_clicked == 1)) {button_select_clicked = 0;} //Set item_sel_previous to previous index //Set item_sel_next to next index if (current_screen == 0) { u8g.firstPage(); do { //Draw the main menu with icons and menu names on specific locations based on index } while (u8g.nextPage()); } else if (current_screen == 1) { u8g.firstPage(); do { //Draw the QR code } while(u8g.nextPage()); } else if (current_screen == 2) { while (gpsSerial.available() > 0) { if (gps.encode(gpsSerial.read())) { //Get Data //Print Data //If selected is pressed, break out to menu screen } } } else if (current_screen == 3) { while (gpsSerial.available() > 0) { if (gps.encode(gpsSerial.read())) { //Get Data //Print Data //If selected is pressed, break out to menu screen } } } }
1. **จัดการการนำทางด้วยปุ่ม:** มันจะตรวจสอบการกดปุ่ม (`BUTTON_UP_PIN, BUTTON_DOWN_PIN, BUTTON_SELECT_PIN`) และปรับดัชนี `item_selected` ตามนั้น ตัวอย่างเช่น กด "ขึ้น" จะลดไอเท็มที่เลือก ส่วนกด "ลง" จะเพิ่มไอเท็มที่เลือก พอถึงจุดสิ้นสุดของเมนู การเลือกจะวนกลับไปเริ่มใหม่ (wrap around) จัดไปวัยรุ่น!
2. **การเดานซ์ (Debouncing):** ใช้แฟล็ก `button_up_clicked`, `button_down_clicked`, และ `button_select_clicked` เพื่อป้องกันไม่ให้การกดปุ่มซ้ำๆ ถูกนับหลายครั้ง พอปล่อยปุ่ม แฟล็กก็จะถูกรีเซ็ต ให้กดใหม่ได้อีกที ห้ามช็อตนะตัวนี้
3. **การสลับหน้าจอ:** ขึ้นอยู่กับหน้าจอปัจจุบัน (`current_screen`) มันจะสลับไปหน้าจอต่างๆ เมื่อปุ่ม "`select`" ถูกกด มันจะไปหน้าจอถัดไป (`menu_item`) หรือกลับไปที่หน้าจอเมนู (`current_screen = 0`)
4. **การแสดงเมนู:** ถ้า `current_screen == 0` เมนูจะถูกแสดงผล มันใช้ไลบรารีกราฟิก u8g เพื่อวาดรายการเมนูก่อนหน้า, ปัจจุบัน, และถัดไป พร้อมกับไอคอนที่เกี่ยวข้อง และแถบเลื่อนจะถูกอัปเดตเพื่อแสดงรายการที่ถูกเลือก
5. **การแสดงข้อมูล GPS:** บนหน้าจอเฉพาะ ( `current_screen == 2` สำหรับตำแหน่ง/ความเร็ว/ความสูง และ `current_screen == 3` สำหรับเวลา/วันที่) โปรแกรมจะอ่านข้อมูล GPS และแสดงค่าต่างๆ (ละติจูด, ลองจิจูด, ความเร็ว, ความสูง, เวลา, และวันที่) บนจอแสดงผล มันจะตรวจสอบความถูกต้องของข้อมูล GPS ก่อนแสดง
6. **การแสดง QR Code:** ถ้า `current_screen == 1` รูปภาพบิตแมปของ QR Code จะถูกแสดงบนหน้าจอโดยใช้ฟังก์ชัน `u8g.drawBitmapP()`
**ฟังก์ชัน PrintSomething():**
//Example Function void printAll() { u8g.firstPage(); do { u8g.setFont(u8g_font_6x13r); u8g.drawStr(0, 10, "Lat: "); u8g.drawStr(0, 25, String(lat, 6).c_str()); } while (u8g.nextPage()); }
1. พิมพ์ข้อมูลที่เก็บมาโดยแปลงค่า `float` เป็น `string` โดยใช้การประกาศคลาส `String()`
2. สตริงจะถูกแปลงเป็น `char[]` โดยใช้ฟังก์ชัน `c_str()` เพื่อพิมพ์บนจอแสดงผล OLED
**ฟังก์ชัน getData():**
//getSpeed() for example void getSpeed() { if (gps.speed.isValid()) { speed = gps.speed.mps(); } }
1. ตรวจสอบว่าความเร็วถูกต้องหรือไม่โดยใช้ `gps.speed.isValid()` และถ้าถูกต้อง ก็จะเซ็ตค่า `speed` เป็นความเร็วที่วัดได้ในหน่วยเมตรต่อวินาที
> หมายเหตุ: ฟังก์ชันอื่นๆ ที่ขึ้นต้นด้วย get() ทำงานในลักษณะเดียวกัน
### คำแนะนำการสร้าง QR Code
1. ทำให้ลิงก์สั้นที่สุด ไม่เกิน 15 ตัวอักษร
2. ไปที่เว็บไซต์สร้างบาร์โค้ดแล้วเลือกประเภท MicroQR เพื่อสร้างบาร์โค้ดขนาด 32x32 พิกเซล
3. ใช้เว็บไซต์แปลงรูปภาพเป็นโค้ด CPP เพื่อแปลง QR Code เป็นอาร์เรย์
4. คัดลอก/วางอาร์เรย์นี้ลงใน `epd_bitmap_qr_code`
### สรุป
Celestial-Chrono คือสุดยอดของ **ระบบวินิจฉัย HMI แบบผสานรวมดาวเทียม** ด้วยการเชี่ยวชาญ **การวิเคราะห์ Bitstream NMEA จาก GPS** และ **การจัดการเมนูแบบอะซิงโครนัส** ทำให้เราได้เครื่องมือนำทางระดับมืออาชีพที่แข็งแกร่ง มันให้ความชัดเจนเชิงเวลาและพื้นที่แบบสมบูรณ์ผ่านการวินิจฉัยข้อมูลดาวเทียมขั้นสูง
---
*Satellite Sync: Mastering navigational telemetry through NMEA forensics.*