บทความหลัก: RTPT ใช้พิกัด GPS และเซอร์โวความแม่นยำสูงเพื่อชี้เลเซอร์ไปยังดาวเคราะห์ดวงใดก็ได้ในระบบสุริยะ มันเป็นตัวอย่างที่เจ๋งมากว่าอาร์ดูโน่เมก้าจัดการกับภาระงานหนักของเทเลเมทรีทางดาราศาสตร์ได้ยังไง
การนำทางบนฟากฟ้า: วิสัยทัศน์ของ RTPT
Real-Time Planet Tracker (RTPT) คือการผสานฮาร์ดแวร์-ซอฟต์แวร์ระดับสูง ที่ออกแบบมาเพื่อเชื่อมช่องว่างระหว่างพิกัดคณิตศาสตร์ที่ดูนามธรรม กับความเป็นจริงทางกายภาพ ในขณะที่แอปท้องฟ้าจำลองส่วนใหญ่แค่แสดงให้เห็นว่าดาวอังคารหรือดาวศุกร์ อยู่ที่ไหน บนหน้าจอ แต่ RTPT ใช้ กลไกเลเซอร์ Pan-Tilt อัตโนมัติ เพื่อชี้ไปยังดาวเคราะห์เหล่านั้นบนท้องฟ้ายามค่ำคืนจริงๆ (หรือบนเพดานสำหรับใช้สอนก็ได้) ด้วยการผสมผสานความแม่นยำของ GPS กับเซอร์โวขั้นสูง โปรเจกต์นี้จึงสร้าง "หอดูดาวส่วนตัว" ที่ปรับตามตำแหน่งที่แน่นอนของเราบนโลก
ของที่ต้องเตรียม:
- Arduino Mega (หรือ MCU ที่มีแฟลชมากกว่า 33Kbyte) สำคัญมาก เพราะโค้ดสุดท้ายของพี่อยู่ที่ประมาณ 33KB และ ใส่ใน UNO ไม่ลงแน่นอน
- U-BLOX GPS Receiver (NEO-6M) หรือโมดูล GPS อะไรก็ได้
- MPU9250 (ไจโร-แอคเซลเลโร-แมกเนโต 9 แกน) สำหรับปรับค่าการหัน (Yaw) อัตโนมัติ (ใส่ก็ได้ ไม่ใส่ก็ได้)
- กลไก Pan-tilt พร้อมเซอร์โว (เซอร์โว pan ควรเป็นแบบหมุนได้ 3.5 รอบ ส่วนเซอร์โว tilt เป็นเซอร์โวธรรมดา 180 องศาก็ได้)
- เลเซอร์พอยน์เตอร์ – เอาไว้ติดกับเซอร์โว tilt เพื่อแสดงตำแหน่งดาวในห้องปิด
สมองของระบบ: ทำไมต้อง Arduino Mega?
หลุมพรางที่พบบ่อยในโปรเจกต์เทเลเมทรีขั้นสูงคือการใช้งานเกินขีดจำกัดหน่วยความจำของ Arduino Uno โปรเจกต์นี้ใช้ Arduino Mega 2560 อย่างชัดเจนเพราะไฟล์ไบนารีสุดท้ายใหญ่กว่า 33KB แฟลชเมมโมรีที่เยอะขึ้นเป็นสิ่งจำเป็นสำหรับ:
- การประมวลผล TinyGPS++: การประมวลผลประโยค NMEA จาก U-BLOX NEO-6M ต้องการการจัดการสตริงและการจัดการบัฟเฟอร์ที่ค่อนข้างหนัก
- การคำนวณ Ephemeris: การคำนวณ Right Ascension และ Declination ที่แม่นยำของดาวเคราะห์เกี่ยวข้องกับคณิตศาสตร์ทศนิยมที่ซับซ้อนและไลบรารีทางคณิตศาสตร์ที่กินหน่วยความจำเร็วมาก
- การทำงานหลายอย่างผ่าน Serial พร้อมกัน: พอร์ตซีเรียลฮาร์ดแวร์หลายพอร์ตของ Mega (ใช้
Serial2สำหรับ GPS) ทำให้การติดตามราบรื่นโดยไม่มีแล็กที่มักมากับ SoftwareSerial
เริ่มต้นกับ GPS
การใช้ GPS นั้นง่ายมากถ้าเราใช้ [TinyGPS++ Libraries] GPS ใช้การสื่อสารแบบ Serial พี่เลยต่อมันเข้ากับ Serial2 ของ Mega ซึ่งก็คือพิน 16 และพิน 17

หลังจากต่อเสร็จแล้ว ให้อัปโหลดโค้ดทดสอบจากไลบรารี่ชื่อ "FullExample" ไลบรารี่นี้ใช้ Software serial อย่าลืมเปลี่ยนมันเป็น Serial2 - และเปลี่ยน GPSBaud เป็น 9600 ด้วย นี่คือโค้ดที่พี่ปรับแล้ว ต้องเข้าใจโค้ดนี้ให้ดีนะ เพราะเราจะใช้แค่บางบรรทัดและฟังก์ชันที่ สำคัญ (ไม่ใช่ทั้งหมด) จากโค้ดนี้ (เช่น ฟังก์ชัน SmartDelay() เพื่ออัปเดตค่าจาก GPS):
#include <TinyGPS++.h>
/*
This sample code demonstrates the normal use of a TinyGPS++ (TinyGPSPlus) object.
It requires the use of SoftwareSerial, and aSerial2umes that you have a
4800-baud serial GPS device hooked up on pins 4(rx) and 3(tx).
*/
static const uint32_t GPSBaud = 9600;
// The TinyGPS++ object
TinyGPSPlus gps;
void setup()
{
Serial.begin(115200);
Serial2.begin(GPSBaud);
Serial.println(F(“FullExample.ino”));
Serial.println(F(“An extensive example of many interesting TinyGPS++ features”));
Serial.print(F(“Testing TinyGPS++ library v. “)); Serial.println(TinyGPSPlus::libraryVersion());
Serial.println(F(“by Mikal Hart”));
Serial.println();
Serial.println(F(“Sats HDOP Latitude Longitude Fix Date Time Date Alt Course Speed Card Distance Course Card Chars Sentences Checksum”));
Serial.println(F(” (deg) (deg) Age Age (m) — from GPS —- —- to London —- RX RX Fail”));
Serial.println(F(“—————————————————————————————————————————————“));
}
void loop()
{ static const double LONDON_LAT = 51.508131, LONDON_LON = -0.128002; printInt(gps.satellites.value(), gps.satellites.isValid(), 5); printInt(gps.hdop.value(), gps.hdop.isValid(), 5); printFloat(gps.location.lat(), gps.location.isValid(), 11, 6); printFloat(gps.location.lng(), gps.location.isValid(), 12, 6); printInt(gps.location.age(), gps.location.isValid(), 5); printDateTime(gps.date, gps.time); printFloat(gps.altitude.meters(), gps.altitude.isValid(), 7, 2); printFloat(gps.course.deg(), gps.course.isValid(), 7, 2); printFloat(gps.speed.kmph(), gps.speed.isValid(), 6, 2); printStr(gps.course.isValid() ? TinyGPSPlus::cardinal(gps.course.value()) : “*** “, 6); unsigned long distanceKmToLondon = (unsigned long)TinyGPSPlus::distanceBetween( gps.location.lat(), gps.location.lng(), LONDON_LAT, LONDON_LON) / 1000; printInt(distanceKmToLondon, gps.location.isValid(), 9); double courseToLondon = TinyGPSPlus::courseTo( gps.location.lat(), gps.location.lng(), LONDON_LAT, LONDON_LON); printFloat(courseToLondon, gps.location.isValid(), 7, 2); const char cardinalToLondon = TinyGPSPlus::cardinal(courseToLondon); printStr(gps.location.isValid() ? cardinalToLondon : “** “, 6); printInt(gps.charsProcessed(), true, 6); printInt(gps.sentencesWithFix(), true, 10); printInt(gps.failedChecksum(), true, 9); Serial.println(); smartDelay(1000); if (millis() > 5000 && gps.charsProcessed() < 10) Serial.println(F(“No GPS data received: check wiring”)); } // ฟังก์ชัน delay แบบพิเศษของเรา ตัวนี้จะคอยป้อนข้อมูลให้ GPS object อยู่ตลอดเวลา static void smartDelay(unsigned long ms) { unsigned long start = millis(); do { while (Serial2.available()) gps.encode(Serial2.read()); } while (millis() – start < ms); } static void printFloat(float val, bool valid, int len, int prec) { if (!valid) { while (len– > 1) Serial.print(‘’); Serial.print(‘ ‘); } else { Serial.print(val, prec); int vi = abs((int)val); int flen = prec + (val < 0.0 ? 2 : 1); // . and – flen += vi >= 1000 ? 4 : vi >= 100 ? 3 : vi >= 10 ? 2 : 1; for (int i=flen; i<len; ++i) Serial.print(‘ ‘); } smartDelay(0); } static void printInt(unsigned long val, bool valid, int len) { char sz[32] = “*****************”; if (valid) sprintf(sz, “%ld”, val); sz[len] = 0; for (int i=strlen(sz); i<len; ++i) sz[i] = ‘ ‘; if (len > 0) sz[len-1] = ‘ ‘; Serial.print(sz); smartDelay(0); } static void printDateTime(TinyGPSDate &d, TinyGPSTime &t) { if (!d.isValid()) { Serial.print(F(“********* “)); } else { char sz[32]; sprintf(sz, “%02d/%02d/%02d “, d.month(), d.day(), d.year()); Serial.print(sz); } if (!t.isValid()) { Serial.print(F(“******** “)); } else { char sz[32]; sprintf(sz, “%02d:%02d:%02d “, t.hour(), t.minute(), t.second()); Serial.print(sz); } printInt(d.age(), d.isValid(), 5); smartDelay(0); } static void printStr(const char *str, int len) { int slen = strlen(str); for (int i=0; i<len; ++i) Serial.print(i smartDelay(0); }
เราก็จะหยิบโค้ดบางส่วนจากตัวอย่าง "Deviceexample" ในไลบรารี TinyGPS++ มาใช้ด้วยนะ นี่คือโค้ดหลักเลย:
```cpp
#include <TinyGPS++.h>
/*
โค้ดตัวอย่างนี้จะแสดงการใช้งานปกติของ object TinyGPS++ (TinyGPSPlus)
มันต้องใช้ SoftwareSerial และสมมติว่าคุณต่ออุปกรณ์ GPS แบบ serial 4800-baud ไว้ที่ขา 4(rx) และ 3(tx) แล้วนะ
*/
static const uint32_t GPSBaud = 9600;
// Object TinyGPS++ ของเรา
TinyGPSPlus gps;
void setup()
{
Serial.begin(115200);
Serial2.begin(GPSBaud);
Serial.println(F(“DeviceExample.ino”));
Serial.println(F(“A simple demonstration of TinyGPS++ with an attached GPS module”));
Serial.print(F(“Testing TinyGPS++ library v. “)); Serial.println(TinyGPSPlus::libraryVersion());
Serial.println(F(“by Mikal Hart”));
Serial.println();
}
void loop()
{
// สเก็ตช์นี้จะแสดงข้อมูลทุกครั้งที่มีประโยคใหม่ถูกเข้ารหัสถูกต้อง
while (Serial2.available() > 0)
if (gps.encode(Serial2.read()))
displayInfo();
if (millis() > 5000 && gps.charsProcessed() < 10)
{
Serial.println(F(“No GPS detected: check wiring.”));
while(true);
}
}
void displayInfo()
{
Serial.print(F(“Location: “));
if (gps.location.isValid())
{
Serial.print(gps.location.lat(), 6);
Serial.print(F(“,”));
Serial.print(gps.location.lng(), 6);
}
else
{
Serial.print(F(“INVALID”));
}
Serial.print(F(” Date/Time: “));
if (gps.date.isValid())
{
Serial.print(gps.date.month());
Serial.print(F(“/”));
Serial.print(gps.date.day());
Serial.print(F(“/”));
Serial.print(gps.date.year());
}
else
{
Serial.print(F(“INVALID”));
}
Serial.print(F(” “));
if (gps.time.isValid())
{
if (gps.time.hour() < 10) Serial.print(F(“0”));
Serial.print(gps.time.hour());
Serial.print(F(“:”));
if (gps.time.minute() < 10) Serial.print(F(“0”));
Serial.print(gps.time.minute());
Serial.print(F(“:”));
if (gps.time.second() < 10) Serial.print(F(“0”));
Serial.print(gps.time.second());
Serial.print(F(“.”));
if (gps.time.centisecond() < 10) Serial.print(F(“0”));
Serial.print(gps.time.centisecond());
}
else
{
Serial.print(F(“INVALID”));
}
Serial.println();
}
เหตุผลที่พี่ให้ดูโค้ดพวกนี้ก็เพราะว่า เราจะใช้ SmartDelay ในการอัปเดตค่าจาก GPS buffer ยังไงล่ะ:
static void smartDelay(unsigned long ms)
{
unsigned long start = millis();
do
{
while (Serial2.available())
gps.encode(Serial2.read());
} while (millis() – start < ms);
}
และเราจะใช้ฟังก์ชันพวกนี้ในการดึงค่าพวก ละติจูด, ลองจิจูด, ปี, เดือน, วันที่, ชั่วโมง, นาที ออกมา:
gps.location.lat()
gps.location.lng()
gps.date.year()
gps.date.month()
gps.date.day()
gps.time.hour()
gps.time.minute()
เอาล่ะ พร้อมกับ GPS แล้ว ไปดูกลไกแพน-ทิลท์กับเลเซอร์กันดีกว่า
เราต้องหา เซอร์โว 3.5 รอบ สำหรับ PAN และ เซอร์โว 180° ปกติสำหรับ TILT พี่จะไม่ลงลึกเรื่องหลักการแพน-ทิลท์มากนัก แต่จะเน้นไปที่การต่อสายและการแมปค่ามากกว่า
หลักการ: กลไกแพน-ทิลท์ที่เราใช้เนี่ย มันเหมือนกับที่ใช้ขยับกล้องโทรทรรศน์ใหญ่ๆในหอดูดาวเลยนะ!
- แพน-ทิลท์สามารถหมุนไปทางซ้าย-ขวาได้ประมาณ 360° (มอเตอร์แพน) และสามารถก้ม-เงยขึ้นลงได้ประมาณ 180° (มอเตอร์ทิลท์)
- เลเซอร์พอยน์เตอร์จะถูกติดตั้งบนกลไกแพน-ทิลท์นี้
- มุมแอซิมัท (Azimuth) ที่คำนวณได้จะถูกไล่ตามด้วยมอเตอร์แพน ส่วนมุมเอเลเวชัน (Elevation) จะถูกไล่ตามด้วยมอเตอร์ทิลท์
เริ่มจากต่อสายก่อน เราใช้ Pin 9 สำหรับเซอร์โวแพน และ Pin 10 สำหรับเซอร์โวทิลท์ จัดไปวัยรุ่น

การขับเคลื่อนแบบแม่นยำ: เซอร์โว 3.5 รอบ
ในการติดตามเนี่ย "ความละเอียด" คือหัวใจสำคัญ แทนที่จะใช้เซอร์โวมาตรฐาน $180^\circ$ ระบบ RTPT ใช้ เซอร์โวหลายรอบ HS-785HB สำหรับแกนแอซิมัท (แพน):
- ครอบคลุมเต็มวง: เซอร์โว $3.5$ รอบทำให้หมุนได้เต็ม $360^\circ$ ด้วยความละเอียดที่มากกว่าเซอร์โวธรรมดาเยอะเลย
- การแมปด้วยซอฟต์แวร์: โค้ดใช้ฟังก์ชัน
map()แบบเฉพาะทางเพื่อแปลงมุมแอซิมัท $0-360^\circ$ ให้เป็นช่วงพัลส์-วิธ ($5-29$) ที่แน่นอน ทำให้เลเซอร์ชี้ตามตำแหน่งดาวเคราะห์เป้าหมายในโลกจริงได้แม่นยำ - ฟีดแบ็คจากเลเซอร์: การติดตั้งเลเซอร์พอยน์เตอร์บนแกนทิลท์ทำให้ระบบแสดงผลตำแหน่งดาวเคราะห์ให้เห็นได้ทันที เป็นเครื่องมือที่เจ๋งมากสำหรับการสาธิต STEM งานนี้ห้ามช็อตนะตัวนี้
การแมปเซอร์โว (Servo mapping)
แค่ดูตัวอย่างจากไลบรารีเซอร์โว ก็เข้าใจแล้วว่าทำให้เซอร์โวขยับยังไง เราแค่ต้องใช้โค้ดสามบรรทัดจากไลบรารีสำหรับแต่ละเซอร์โว
เราต้องประกาศออบเจ็กต์ Servo แล้วกำหนดขา (pin) และตั้งค่าเริ่มต้นให้มัน:
#include
Servo myservoAz; // สร้างออบเจ็กต์เซอร์โวเพื่อควบคุมเซอร์โว
Servo myservoEl;
myservoAz.attach(9); // กำหนดขาที่ต่อเซอร์โว
myservoEl.attach(10);
myservoAz.write(5); // ตั้งค่าเริ่มต้นให้เซอร์โวไปที่ตำแหน่งศูนย์
myservoEl.write(2); // สำหรับเซอร์โวของพี่ มันคือ 5 (สำหรับเซอร์โวแพน/อซิมัท) และ 2 (สำหรับเซอร์โวทิลท์)
เซอร์โวแพนของเราเป็นแบบ 3.5 รอบ (turn) และรับค่าอินพุตจาก 5 ถึง 29 เพื่อหมุนจาก 0° ถึง 360° จากนั้นเราก็ทำการแมปค่าซึ่งง่ายมาก สมมติตัวแปร "Azim" ให้ค่าอซิมัทมา และตัวแปร "Elevation" ให้ค่าอัลติจูดมา เราก็แมปแบบนี้:
Azi = map(Azim, 0, 360, 5, 29); // สำหรับเซอร์โว 3.5 รอบของพี่ที่หมุนได้ 360° ทำงานบนค่าตั้งแต่ 5 - 29
Az = (int)Azi;
Elev = map(Elevation, -90, 90, 2, 178); // สำหรับเซอร์โว 180° ของพี่ที่ทำงานบนค่าตั้งแต่ 0 - 180
El = (int)Elev;
Azi และ El คือค่าที่แมปถูกต้องแล้ว และสามารถส่งเข้าไปให้แพนทิลท์ได้แบบนี้:
myservoAz.write(Az);
myservoEl.write(El);
ตอนนี้สิ่งที่ต้องทำก็คือ แปลงการคำนวณอซิมัท-อัลติจูดจากโพสต์ก่อนหน้านี้ให้เป็นโค้ด Arduino แล้วใช้ข้อมูลจาก GPS มาคำนวณและส่งผลลัพธ์ไปให้เซอร์โว
หมายเหตุ: น้องต้องจัดตำแหน่งตั้งต้นของแพนทิลท์ให้ตรงกับ 0° อซิมัทของตำแหน่งจริงของน้องให้ได้นะ (ก็คือทิศเหนือนั่นแหละ!) ไม่งั้นมันจะชี้ดาวพลุ๊ตแทนดาวอังคารแน่ะ
แผนผังการทำงาน: จาก GPS สู่เซอร์โว
เวิร์กโฟลว์นี้คือคลาสมาสเตอร์ในการจัดการข้อมูลแบบซิงโครนัสเลย:
- จับสัญญาณ GPS: ใช้
smartDelay()เพื่อ "ป้อน" ข้อมูลให้บัฟเฟอร์ GPS ในขณะที่ลูปการคำนวณยังทำงานอยู่ - ประสานพิกัด: ระบบดึงค่าละติจูด ลองจิจูด และเวลาปัจจุบัน (UTC) มาคำนวณหา "เวลาดาวฤกษ์ท้องถิ่น" (Local Sidereal Time) ของผู้สังเกต
- ปรับสมดุลอัตโนมัติ: ถ้าเพิ่ม MPU9250 เข้าไป ระบบจะสามารถชดเชยการวางตัวของฐานได้อัตโนมัติ หมายความว่าไม่ต้องมานั่งจัดอุปกรณ์ให้ตรงทิศเหนือจริง (True North) ด้วยมือให้เมื่อย
ในโพสต์หน้า พี่จะเอา MPU9250 มาบูรณาการด้วย เพื่อให้เซอร์โวแพนของพี่ตรวจจับทิศเหนือของตำแหน่งนั้นๆ ได้เอง และปรับตัวชี้ตำแหน่งดาวเคราะห์ให้อัตโนมัติ แม้ว่าพี่จะไม่ตั้งมันไว้ที่อซิมัท 0° ก็ตาม (ขั้นตอนนี้เป็นตัวเลือกนะ อยากเพิ่มเมื่อไหร่ค่อยทำ)
เจ้า RTPT นี้คือหลักฐานเชิงประจักษ์ระดับโปรที่เปลี่ยนข้อมูลดาราศาสตร์ซับซ้อนให้กลายเป็นประสบการณ์ที่จับต้องและโต้ตอบได้ สุดยอดไปเลยวัยรุ่น!