정밀 GPS 기반 시계: 합리적인 가격으로 정확성과 신뢰성 확보
부품 및 소모품
| 아두이노 우노 (Arduino UNO) |
×1 |
| 아다프루트 RGB 백라이트 LCD - 16x2 |
×1 |
| 브레드보드 (일반) |
×1 |
| 점퍼 와이어 (일반) |
×1 |
| GPS 수신기 (일반) |
×1 |
앱 및 온라인 서비스
프로젝트 소개
지금까지 시계 프로젝트에는 다양한 RTC 모듈을 사용하거나 NTP 서버에서 시간을 가져왔습니다. 이 프로젝트에서는 전 세계 GPS 위성의 NMEA 문자열을 파싱하여 시간을 얻는 또 다른 소스를 소개합니다.
요즘 GPS 수신기가 얼마나 저렴한지 놀랐습니다. 하나 구입하세요 (제 경우 GY-GPS6Mv2, u-blox NEO-6M 사용).

a) 선택 사항: 윈도우 PC에서 첫 테스트
u-blox에서 u-center 다운로드 설치. FTDI 어댑터로 GPS 브레이크아웃을 PC COM 포트에 연결하면 약 1분 후 위성 고정(Fix) 완료. 브레이크아웃의 빨간 LED가 깜빡임. F8 - Text Console에서 다양한 NMEA 문자열 확인 가능.
b) 무료 온라인 서비스에서 디코딩 테스트
https://rl.se/gprmc
이제 작동하는 GPS 수신기가 확인되었으니(하늘이 잘 보이는 곳 필수), 원하는 정보를 NMEA 문자열에서 추출할 수 있습니다.

아두이노 IDE에 Adafruit GPS Library 설치 후 예제로 테스트해보세요.
c) 회로 구성

- 아두이노 A4 → 디스플레이 SDA
- 아두이노 A5 → 디스플레이 SCL
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x3F, 20, 4); // LCD 주소, 보통 0x27 또는 0x3F
가지고 있는 디스플레이 사용 (16x2 등)
#include <Adafruit_GPS.h>
#include <SoftwareSerial.h>
// GPS 전원 핀 → 5V
// GPS 그라운드 핀 → 그라운드
// GPS TX(송신) → 디지털 8
// GPS RX(수신) → 디지털 7
SoftwareSerial mySerial(8, 7);
Adafruit_GPS GPS(&mySerial);
> GPS 수신기는 3.3V/5V 호환.
d) 우리 시계의 기능
시간, 날짜, 풍속 표시, 고도 표시. 위치는 고정되어 있으므로 불필요.
NMEA 문자열은 UTC 표준으로 시간을 제공. 로컬 타임존 변환은 직접 구현. 현재는 유럽 베를린 기준 +1 추가.
int timezone = +1; // Europe/Berlin (UTC +0100) > NMEA는 UTC 기준
타임존을 취향에 맞게 조정. 변수 hour를 GPS.hour 대신 LCD 출력에 사용.
// LCD 출력
lcd.setCursor(5, 0); // 0 = 첫 줄
int hour = (GPS.hour) + timezone; // UTC를 로컬 타임존으로 변환
if (hour < 10) { lcd.print('0'); }
lcd.print(hour, DEC); lcd.print(':');
유럽에서는 풍속 단위로 노트 대신 km/h 사용. 상수 곱해 변환 후 등급 분류. 1 노트 = 1.852 km/h
float speed = (GPS.speed) * 1.852; // 노트 → km/h 변환
위키피디아 기준 평가:
if (speed <= 1) {lcd.print(" Windstille");}
else if ((speed > 1) && (speed <= 9)) {lcd.print(" leiser Zug");}
else if ((speed > 9) && (speed <= 46)) {lcd.print(" Brise");}
else if ((speed > 46) && (speed <= 56)) {lcd.print(" starker Wind");}
else if ((speed > 56) && (speed <= 74)) {lcd.print(" stuerm. Wind");}
else if ((speed > 74) && (speed <= 83)) {lcd.print(" Sturm");}
else if ((speed > 83) && (speed <= 102)) {lcd.print(" schwerer Sturm");}
else if (speed > 102) {lcd.print(" Orkan");}
else {lcd.print(" ohne Bewertung");}
결과는 디스플레이에 다음과 같이 표시되며 원하는 대로 수정 가능:

루프 업데이트 주기는 2초로 설정. 초와 고도 표시를 포기하면 16x2 디스플레이에도 적합.

다양한 GPS 옵션 탐험 즐기세요!
유럽에는 DCF77 전파 신호도 있지만 훨씬 비싸고 예민하다고 알려져 있습니다.
코드
- GPS_SoftwareSerial_Parsing_ilo
C/C++
// Adafruit GPS 모듈 테스트 코드 (MTK3329/MTK3339 드라이버)
// 인터럽트 방식으로 GPS 모듈 리스닝하여 프로그램 자유도 확보
// 새 NMEA 문장 사용 가능할 때만 파싱, 필요할 때 데이터 접근
// Adafruit Ultimate GPS 모듈(MTK33x9 칩셋)과 호환 검증
// https://www.adafruit.com/products/746
// 2020.02.01 수정 - Ingo Lohs
// GPRMC & GPGGA 디코더: https://rl.se/gprmc
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x3F, 20, 4); // 16x2용 0x27, 제 경우 20x4용 0x3F
#include <Adafruit_GPS.h>
#include <SoftwareSerial.h>
// GPS 전원 → 5V, 그라운드 → 그라운드
// GPS TX → 디지털 8, GPS RX → 디지털 7
SoftwareSerial mySerial(8, 7);
Adafruit_GPS GPS(&mySerial);
#define GPSECHO true
int timezone = +1; // Europe/Berlin (UTC +0100)
void setup() {
lcd.init();
lcd.backlight();
Serial.begin(115200);
delay(5000);
Serial.println("Adafruit GPS library basic test!");
GPS.begin(9600); // UBLOX 6M: GY-GPS6Mv2 사용
// RMC(권장 최소) + GGA(고정 데이터, 고도 포함) 활성화
GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
// GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCONLY); // 최소 데이터만
// 파싱을 위해 RMC만 또는 RMC+GGA 권장
GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ); // 1Hz 업데이트
GPS.sendCommand(PGCMD_ANTENNA); // 안테나 상태 요청
delay(1000);
mySerial.println(PMTK_Q_RELEASE); // 펌웨어 버전 요청
}
uint32_t timer = millis();
void loop() {
char c = GPS.read();
if ((c) && (GPSECHO)) Serial.write(c);
if (GPS.newNMEAreceived()) {
if (!GPS.parse(GPS.lastNMEA())) return;
}
if (timer > millis()) timer = millis();
if (millis() - timer > 2000) {
timer = millis();
// 시리얼 출력
Serial.print("\nTime: ");
if (GPS.hour < 10) Serial.print('0');
Serial.print(GPS.hour, DEC); Serial.print(':');
if (GPS.minute < 10) Serial.print('0');
Serial.print(GPS.minute, DEC); Serial.print(':');
if (GPS.seconds < 10) Serial.print('0');
Serial.print(GPS.seconds, DEC); Serial.print('.');
if (GPS.milliseconds < 10) Serial.print("00");
else if (GPS.milliseconds < 100) Serial.print("0");
Serial.println(GPS.milliseconds);
Serial.print("Date: ");
Serial.print(GPS.day, DEC); Serial.print('/');
Serial.print(GPS.month, DEC); Serial.print("/20");
Serial.println(GPS.year, DEC);
Serial.print("Fix: "); Serial.print((int)GPS.fix);
Serial.print(" quality: "); Serial.println((int)GPS.fixquality);
// LCD 출력
lcd.setCursor(5, 0);
int hour = (GPS.hour) + timezone;
if (hour < 10) { lcd.print('0'); }
lcd.print(hour, DEC); lcd.print(':');
if (GPS.minute < 10) { lcd.print('0'); }
lcd.print(GPS.minute, DEC); lcd.print(':');
if (GPS.seconds < 10) { lcd.print('0'); }
lcd.print(GPS.seconds, DEC);
lcd.setCursor(5, 1);
if (GPS.day < 10) { lcd.print('0'); }
lcd.print(GPS.day, DEC); lcd.print('.');
if (GPS.month < 10) { lcd.print('0'); }
lcd.print(GPS.month, DEC); lcd.print('.');
lcd.print(GPS.year, DEC);
if (GPS.fix) {
Serial.print("Location: ");
Serial.print(GPS.latitude, 4); Serial.print(GPS.lat);
Serial.print(", "); Serial.print(GPS.longitude, 4); Serial.println(GPS.lon);
Serial.print("Speed (knots): "); Serial.println(GPS.speed);
Serial.print("Angle: "); Serial.println(GPS.angle);
Serial.print("Altitude: "); Serial.println(GPS.altitude);
Serial.print("Satellites: "); Serial.println((int)GPS.satellites);
lcd.setCursor(0, 2);
float speed = (GPS.speed) * 1.852;
// lcd.print("Speed km/h: ");
lcd.print(speed);
// 풍속 등급 (위키피디아 기준)
if (speed <= 1) {lcd.print(" Windstille");}
else if ((speed > 1) && (speed <= 9)) {lcd.print(" leiser Zug");}
else if ((speed > 9) && (speed <= 46)) {lcd.print(" Brise");}
else if ((speed > 46) && (speed <= 56)) {lcd.print(" starker Wind");}
else if ((speed > 56) && (speed <= 74)) {lcd.print(" stuerm. Wind");}
else if ((speed > 74) && (speed <= 83)) {lcd.print(" Sturm");}
else if ((speed > 83) && (speed <= 102)) {lcd.print(" schwerer Sturm");}
else if (speed > 102) {lcd.print(" Orkan");}
else {lcd.print(" ohne Bewertung");}
lcd.setCursor(0, 3);
lcd.print("Hoehe: ");
lcd.print(GPS.altitude);
}
}
}