반응형
아두이노 코드는 아래와 같다.
ESP32-S3 연결 회로도와 코드

/*
* test_gnsssamm10q.ino
* ESP32-S3 - u-blox SAM-M10Q GNSS 모듈 테스트
*
* 기능:
* - UART2로 SAM-M10Q NMEA 데이터 수신 및 raw 출력
* - TinyGPS++ 라이브러리로 전체 NMEA 필드 파싱
*
* 파싱 대상 NMEA 문장 및 필드:
* $GNGGA : 위도/경도, Fix 품질, 위성 수, HDOP, 고도, 지오이드 보정고, 차등보정 경과시간
* $GNRMC : 위도/경도, 상태, 속도(knots), 방위각, 날짜, 자기편차, 모드
* $GNGSA : 측위 모드(A/M), Fix 타입(1/2/3), PDOP, HDOP, VDOP
* $GNGSV : 가시 위성 수, 위성별 PRN/고도각/방위각/SNR (최대 4기/문장)
* $GNGLL : 위도/경도, 상태, 모드
* $GNVTG : 진북 방위각, 자북 방위각, 속도(knots/km/h), 모드
*
* 라이브러리 설치 (Arduino IDE Library Manager):
* - "TinyGPS++" by Mikal Hart
*
* Hardware Connections (SAM-M10Q <-> ESP32-S3):
* SAM-M10Q TXO ──► GPIO18 (RX2)
* SAM-M10Q RXI ──► GPIO17 (TX2)
* SAM-M10Q VCC ──► 3.3V
* SAM-M10Q GND ──► GND
*
* SAM-M10Q 기본 UART 설정:
* Baud rate : 9600 bps
* Protocol : NMEA 0183
*/
#include <TinyGPSPlus.h>
#include <HardwareSerial.h>
// ─────────────────────────────────────────────
// 핀 / 포트 설정
// ─────────────────────────────────────────────
#define GNSS_RX_PIN 18
#define GNSS_TX_PIN 17
#define GNSS_BAUD 9600
// ─────────────────────────────────────────────
// 타이밍 상수
// ─────────────────────────────────────────────
const unsigned long PRINT_INTERVAL_MS = 2000UL;
const unsigned long NO_FIX_WARN_MS = 30000UL;
// ─────────────────────────────────────────────
// TinyGPS++ 객체 및 UART
// ─────────────────────────────────────────────
TinyGPSPlus gps;
HardwareSerial gnssSerial(2);
// ─────────────────────────────────────────────
// 커스텀 파서 — $GNGGA
// 필드 인덱스는 문장 유형명 뒤 첫 번째 필드가 1
// ─────────────────────────────────────────────
// 6 : Fix 품질 0=invalid 1=GPS 2=DGPS 4=RTK Fixed 5=Float …
TinyGPSCustom ggaFixQuality (gps, "GNGGA", 6);
// 9 : 지오이드 보정고 (geoid separation, m)
TinyGPSCustom ggaGeoidSep (gps, "GNGGA", 9);
// 10 : 지오이드 단위 (M)
TinyGPSCustom ggaGeoidUnit (gps, "GNGGA", 10);
// 13 : 차등보정 데이터 경과시간 (초, DGPS 사용 시)
TinyGPSCustom ggaAgeDiff (gps, "GNGGA", 13);
// 14 : 차등보정 기준국 ID
TinyGPSCustom ggaDiffStaID (gps, "GNGGA", 14);
// ─────────────────────────────────────────────
// 커스텀 파서 — $GNGSA
// ─────────────────────────────────────────────
// 1 : 모드 A=자동선택 M=수동
TinyGPSCustom gsaMode (gps, "GNGSA", 1);
// 2 : Fix 타입 1=NoFix 2=2D 3=3D
TinyGPSCustom gsaFixType (gps, "GNGSA", 2);
// 15 : PDOP (위치정밀도 저하율)
TinyGPSCustom gsaPdop (gps, "GNGSA", 15);
// 16 : HDOP (수평 DOP) — TinyGPS++ 내장과 교차 확인용
TinyGPSCustom gsaHdop (gps, "GNGSA", 16);
// 17 : VDOP (수직 DOP)
TinyGPSCustom gsaVdop (gps, "GNGSA", 17);
// 사용 중인 위성 PRN (필드 3~14, 최대 12개)
TinyGPSCustom gsaPrn[12] = {
TinyGPSCustom(gps, "GNGSA", 3), TinyGPSCustom(gps, "GNGSA", 4),
TinyGPSCustom(gps, "GNGSA", 5), TinyGPSCustom(gps, "GNGSA", 6),
TinyGPSCustom(gps, "GNGSA", 7), TinyGPSCustom(gps, "GNGSA", 8),
TinyGPSCustom(gps, "GNGSA", 9), TinyGPSCustom(gps, "GNGSA", 10),
TinyGPSCustom(gps, "GNGSA", 11), TinyGPSCustom(gps, "GNGSA", 12),
TinyGPSCustom(gps, "GNGSA", 13), TinyGPSCustom(gps, "GNGSA", 14),
};
// ─────────────────────────────────────────────
// 커스텀 파서 — $GNRMC
// ─────────────────────────────────────────────
// 2 : 상태 A=유효 V=경고
TinyGPSCustom rmcStatus (gps, "GNRMC", 2);
// 10 : 자기편차 값 (도)
TinyGPSCustom rmcMagVar (gps, "GNRMC", 10);
// 11 : 자기편차 방향 E/W
TinyGPSCustom rmcMagDir (gps, "GNRMC", 11);
// 12 : 모드 A=Auto D=Diff E=Est N=Invalid
TinyGPSCustom rmcMode (gps, "GNRMC", 12);
// ─────────────────────────────────────────────
// 커스텀 파서 — $GNVTG
// ─────────────────────────────────────────────
// 1 : 진북 방위각 (Course over ground, true)
TinyGPSCustom vtgCogsT (gps, "GNVTG", 1);
// 3 : 자북 방위각 (Course over ground, magnetic)
TinyGPSCustom vtgCogsM (gps, "GNVTG", 3);
// 5 : 속도 (knots)
TinyGPSCustom vtgSpeedN (gps, "GNVTG", 5);
// 7 : 속도 (km/h)
TinyGPSCustom vtgSpeedK (gps, "GNVTG", 7);
// 9 : 모드 A=Auto D=Diff E=Est N=Invalid
TinyGPSCustom vtgMode (gps, "GNVTG", 9);
// ─────────────────────────────────────────────
// 커스텀 파서 — $GNGSV (첫 번째 문장 기준)
// SAM-M10Q는 다중 항법 시스템을 별도 GSV 문장으로 출력
// → 가시 위성 수는 시스템마다 다를 수 있음
// ─────────────────────────────────────────────
// 1 : 전체 문장 수
TinyGPSCustom gsvTotalMsg (gps, "GNGSV", 1);
// 3 : 가시 위성 총 수
TinyGPSCustom gsvTotalSats (gps, "GNGSV", 3);
// 첫 번째 위성 (필드 4~7)
TinyGPSCustom gsvPrn1 (gps, "GNGSV", 4);
TinyGPSCustom gsvElev1(gps, "GNGSV", 5);
TinyGPSCustom gsvAz1 (gps, "GNGSV", 6);
TinyGPSCustom gsvSnr1 (gps, "GNGSV", 7);
// 두 번째 위성 (필드 8~11)
TinyGPSCustom gsvPrn2 (gps, "GNGSV", 8);
TinyGPSCustom gsvElev2(gps, "GNGSV", 9);
TinyGPSCustom gsvAz2 (gps, "GNGSV", 10);
TinyGPSCustom gsvSnr2 (gps, "GNGSV", 11);
// 세 번째 위성 (필드 12~15)
TinyGPSCustom gsvPrn3 (gps, "GNGSV", 12);
TinyGPSCustom gsvElev3(gps, "GNGSV", 13);
TinyGPSCustom gsvAz3 (gps, "GNGSV", 14);
TinyGPSCustom gsvSnr3 (gps, "GNGSV", 15);
// 네 번째 위성 (필드 16~19)
TinyGPSCustom gsvPrn4 (gps, "GNGSV", 16);
TinyGPSCustom gsvElev4(gps, "GNGSV", 17);
TinyGPSCustom gsvAz4 (gps, "GNGSV", 18);
TinyGPSCustom gsvSnr4 (gps, "GNGSV", 19);
// ─────────────────────────────────────────────
// 커스텀 파서 — $GNGLL
// ─────────────────────────────────────────────
// 6 : 상태 A=유효 V=무효
TinyGPSCustom gllStatus (gps, "GNGLL", 6);
// 7 : 모드
TinyGPSCustom gllMode (gps, "GNGLL", 7);
// ─────────────────────────────────────────────
// 전역 변수
// ─────────────────────────────────────────────
unsigned long lastPrint = 0;
unsigned long lastNoFix = 0;
// ─────────────────────────────────────────────
// 함수 선언
// ─────────────────────────────────────────────
void readGnss();
void printGnssData();
void printFixStatus();
void printGgaSection();
void printRmcSection();
void printGsaSection();
void printVtgSection();
void printGsvSection();
void printGllSection();
String pad2(int v);
const char* fixQualityStr(const char* q);
const char* fixTypeStr(const char* t);
// ─────────────────────────────────────────────
// setup()
// ─────────────────────────────────────────────
void setup() {
pinMode(42, OUTPUT);
digitalWrite(42, LOW);
Serial.begin(115200);
delay(300);
Serial.println();
Serial.println(F("=== GNSS SAM-M10Q Test Start ==="));
gnssSerial.begin(GNSS_BAUD, SERIAL_8N1, GNSS_RX_PIN, GNSS_TX_PIN);
Serial.printf("[GNSS] UART2 초기화 완료 (RX=GPIO%d, %d bps)\n",
GNSS_RX_PIN, GNSS_BAUD);
Serial.println(F("[GNSS] NMEA 데이터 수신 대기 중...\n"));
}
// ─────────────────────────────────────────────
// loop()
// ─────────────────────────────────────────────
void loop() {
readGnss();
unsigned long now = millis();
if (now - lastPrint >= PRINT_INTERVAL_MS) {
printGnssData();
lastPrint = now;
}
if (!gps.location.isValid() && (now - lastNoFix >= NO_FIX_WARN_MS)) {
Serial.println(F("[경고] GNSS Fix 없음 - 하늘이 보이는 장소로 이동하세요."));
lastNoFix = now;
}
}
// ─────────────────────────────────────────────
// GNSS 수신 — raw NMEA + TinyGPS++ 파싱
// ─────────────────────────────────────────────
void readGnss() {
while (gnssSerial.available()) {
char c = gnssSerial.read();
Serial.print(c); // raw NMEA 그대로 출력
gps.encode(c);
}
}
// ─────────────────────────────────────────────
// 전체 파싱 결과 출력
// ─────────────────────────────────────────────
void printGnssData() {
Serial.println(F("\n======== GNSS SAM-M10Q 파싱 결과 ========"));
// ── 수신 통계 ─────────────────────────────
Serial.printf("[수신 문장] 통과 %lu / 실패 %lu / 문자 %lu\n",
gps.passedChecksum(),
gps.failedChecksum(),
gps.charsProcessed());
printFixStatus();
printGgaSection();
printRmcSection();
printGsaSection();
printVtgSection();
printGsvSection();
printGllSection();
Serial.println(F("==========================================\n"));
}
// ─────────────────────────────────────────────
// Fix 상태 요약
// ─────────────────────────────────────────────
void printFixStatus() {
Serial.println(F("---- [ Fix 상태 ] ----"));
// GSA fix type이 우선 (1=NoFix, 2=2D, 3=3D)
if (gsaFixType.isValid() && strlen(gsaFixType.value()) > 0) {
Serial.printf("[Fix 타입] %s\n", fixTypeStr(gsaFixType.value()));
} else if (!gps.location.isValid()) {
Serial.println(F("[Fix 타입] No Fix"));
} else {
Serial.println(F("[Fix 타입] Fix (GSA 미수신)"));
}
// GGA fix quality
if (ggaFixQuality.isValid()) {
Serial.printf("[Fix 품질] %s (%s)\n",
ggaFixQuality.value(),
fixQualityStr(ggaFixQuality.value()));
}
}
// ─────────────────────────────────────────────
// GGA 섹션 — 위치 / 고도 / 위성 / HDOP / 지오이드
// ─────────────────────────────────────────────
void printGgaSection() {
Serial.println(F("---- [ GGA ] ----"));
// 위도 / 경도
if (gps.location.isValid()) {
double lat = gps.location.lat();
double lng = gps.location.lng();
Serial.printf("[위도] %.8f °%c\n", fabs(lat), lat >= 0 ? 'N' : 'S');
Serial.printf("[경도] %.8f °%c\n", fabs(lng), lng >= 0 ? 'E' : 'W');
Serial.printf("[위치 정확도] ±%.1f m (추정, HDOP 기반)\n",
gps.hdop.isValid() ? gps.hdop.hdop() * 3.0 : -1.0);
} else {
Serial.println(F("[위도] --- (Fix 없음)"));
Serial.println(F("[경도] --- (Fix 없음)"));
}
// 고도 (타원체 기준)
if (gps.altitude.isValid()) {
Serial.printf("[고도(타원)] %.2f m\n", gps.altitude.meters());
} else {
Serial.println(F("[고도(타원)] ---"));
}
// 지오이드 보정고 및 실제 해발고도
if (ggaGeoidSep.isValid() && strlen(ggaGeoidSep.value()) > 0) {
float geoid = atof(ggaGeoidSep.value());
Serial.printf("[지오이드] %.2f m (%s)\n", geoid, ggaGeoidUnit.value());
if (gps.altitude.isValid()) {
Serial.printf("[고도(해발)] %.2f m\n",
(float)gps.altitude.meters() - geoid);
}
}
// 위성 수
if (gps.satellites.isValid()) {
Serial.printf("[위성 수] %d\n", gps.satellites.value());
}
// HDOP
if (gps.hdop.isValid()) {
Serial.printf("[HDOP] %.2f\n", gps.hdop.hdop());
}
// 차등보정 경과시간 / 기준국
if (ggaAgeDiff.isValid() && strlen(ggaAgeDiff.value()) > 0) {
Serial.printf("[차등보정] 경과 %s 초 기준국 ID: %s\n",
ggaAgeDiff.value(),
ggaDiffStaID.isValid() ? ggaDiffStaID.value() : "---");
}
}
// ─────────────────────────────────────────────
// RMC 섹션 — 속도 / 방위각 / UTC / 자기편차
// ─────────────────────────────────────────────
void printRmcSection() {
Serial.println(F("---- [ RMC ] ----"));
// 상태
if (rmcStatus.isValid()) {
Serial.printf("[RMC 상태] %s (%s)\n",
rmcStatus.value(),
strcmp(rmcStatus.value(), "A") == 0 ? "유효" : "경고/무효");
}
// UTC 날짜 / 시각 (centisecond 포함)
if (gps.date.isValid() && gps.time.isValid()) {
Serial.printf("[UTC] %04d-%s-%s %s:%s:%s.%02d\n",
gps.date.year(),
pad2(gps.date.month()).c_str(),
pad2(gps.date.day()).c_str(),
pad2(gps.time.hour()).c_str(),
pad2(gps.time.minute()).c_str(),
pad2(gps.time.second()).c_str(),
gps.time.centisecond());
} else {
Serial.println(F("[UTC] --- (수신 중)"));
}
// 속도
if (gps.speed.isValid()) {
Serial.printf("[속도] %.4f knots / %.4f km/h / %.4f m/s\n",
gps.speed.knots(),
gps.speed.kmph(),
gps.speed.mps());
}
// 방위각 (진북)
if (gps.course.isValid()) {
Serial.printf("[방위각] %.2f ° (진북 기준)\n", gps.course.deg());
}
// 자기편차
if (rmcMagVar.isValid() && strlen(rmcMagVar.value()) > 0) {
Serial.printf("[자기편차] %s ° %s\n",
rmcMagVar.value(),
rmcMagDir.isValid() ? rmcMagDir.value() : "");
}
// RMC 모드
if (rmcMode.isValid() && strlen(rmcMode.value()) > 0) {
Serial.printf("[RMC 모드] %s\n", rmcMode.value());
}
}
// ─────────────────────────────────────────────
// GSA 섹션 — DOP / 측위 모드 / 사용 위성 PRN
// ─────────────────────────────────────────────
void printGsaSection() {
Serial.println(F("---- [ GSA ] ----"));
if (gsaMode.isValid()) {
Serial.printf("[GSA 모드] %s (%s)\n",
gsaMode.value(),
strcmp(gsaMode.value(), "A") == 0 ? "자동 선택" : "수동");
}
if (gsaPdop.isValid() && strlen(gsaPdop.value()) > 0) {
Serial.printf("[PDOP] %s\n", gsaPdop.value());
}
if (gsaHdop.isValid() && strlen(gsaHdop.value()) > 0) {
Serial.printf("[HDOP(GSA)] %s\n", gsaHdop.value());
}
if (gsaVdop.isValid() && strlen(gsaVdop.value()) > 0) {
Serial.printf("[VDOP] %s\n", gsaVdop.value());
}
// 사용 중인 위성 PRN 목록
Serial.print(F("[사용 위성] PRN: "));
bool anyPrn = false;
for (int i = 0; i < 12; i++) {
if (gsaPrn[i].isValid() && strlen(gsaPrn[i].value()) > 0) {
Serial.printf("%s ", gsaPrn[i].value());
anyPrn = true;
}
}
if (!anyPrn) Serial.print(F("---"));
Serial.println();
}
// ─────────────────────────────────────────────
// VTG 섹션 — 이중 방위각 / 속도 재확인
// ─────────────────────────────────────────────
void printVtgSection() {
Serial.println(F("---- [ VTG ] ----"));
if (vtgCogsT.isValid() && strlen(vtgCogsT.value()) > 0) {
Serial.printf("[COG 진북] %s °\n", vtgCogsT.value());
}
if (vtgCogsM.isValid() && strlen(vtgCogsM.value()) > 0) {
Serial.printf("[COG 자북] %s °\n", vtgCogsM.value());
}
if (vtgSpeedN.isValid() && strlen(vtgSpeedN.value()) > 0) {
Serial.printf("[속도(VTG)] %s knots / %s km/h\n",
vtgSpeedN.value(),
vtgSpeedK.isValid() ? vtgSpeedK.value() : "---");
}
if (vtgMode.isValid() && strlen(vtgMode.value()) > 0) {
Serial.printf("[VTG 모드] %s\n", vtgMode.value());
}
}
// ─────────────────────────────────────────────
// GSV 섹션 — 가시 위성 목록 (첫 번째 문장 4기)
// ─────────────────────────────────────────────
void printGsvSection() {
Serial.println(F("---- [ GSV — 가시 위성 ] ----"));
if (gsvTotalSats.isValid() && strlen(gsvTotalSats.value()) > 0) {
Serial.printf("[가시 위성] 총 %s 기 (전체 문장 수: %s)\n",
gsvTotalSats.value(),
gsvTotalMsg.isValid() ? gsvTotalMsg.value() : "?");
}
// 위성별 상세 (PRN | 고도각 | 방위각 | SNR)
Serial.println(F(" PRN 고도각 방위각 SNR(dBHz)"));
struct SatEntry {
TinyGPSCustom *prn, *elev, *az, *snr;
} sats[4] = {
{&gsvPrn1, &gsvElev1, &gsvAz1, &gsvSnr1},
{&gsvPrn2, &gsvElev2, &gsvAz2, &gsvSnr2},
{&gsvPrn3, &gsvElev3, &gsvAz3, &gsvSnr3},
{&gsvPrn4, &gsvElev4, &gsvAz4, &gsvSnr4},
};
bool anySat = false;
for (int i = 0; i < 4; i++) {
if (sats[i].prn->isValid() && strlen(sats[i].prn->value()) > 0) {
Serial.printf(" %-5s %-6s %-6s %s\n",
sats[i].prn->value(),
sats[i].elev->isValid() ? sats[i].elev->value() : "--",
sats[i].az->isValid() ? sats[i].az->value() : "--",
sats[i].snr->isValid() ? sats[i].snr->value() : "--");
anySat = true;
}
}
if (!anySat) Serial.println(F(" (위성 데이터 없음)"));
}
// ─────────────────────────────────────────────
// GLL 섹션 — 위치 상태 / 모드 재확인
// ─────────────────────────────────────────────
void printGllSection() {
Serial.println(F("---- [ GLL ] ----"));
if (gllStatus.isValid() && strlen(gllStatus.value()) > 0) {
Serial.printf("[GLL 상태] %s (%s)\n",
gllStatus.value(),
strcmp(gllStatus.value(), "A") == 0 ? "유효" : "무효");
}
if (gllMode.isValid() && strlen(gllMode.value()) > 0) {
Serial.printf("[GLL 모드] %s\n", gllMode.value());
}
}
// ─────────────────────────────────────────────
// 헬퍼 함수들
// ─────────────────────────────────────────────
String pad2(int v) {
return v < 10 ? "0" + String(v) : String(v);
}
const char* fixQualityStr(const char* q) {
if (!q) return "Unknown";
switch (q[0]) {
case '0': return "Invalid";
case '1': return "GPS fix";
case '2': return "DGPS fix";
case '3': return "PPS fix";
case '4': return "RTK Fixed";
case '5': return "RTK Float";
case '6': return "Estimated (dead reckoning)";
default: return "Unknown";
}
}
const char* fixTypeStr(const char* t) {
if (!t) return "Unknown";
switch (t[0]) {
case '1': return "No Fix";
case '2': return "2D Fix";
case '3': return "3D Fix";
default: return "Unknown";
}
}
반응형
'ESP32' 카테고리의 다른 글
| Iridium 9603N ESP32-S3 위성 통신 송신 절차 (0) | 2026.04.11 |
|---|---|
| ESP32의 시리얼 포트(UART) 사용 방법 (0) | 2026.04.09 |
| ESP32 개발 자동화 강의 (0) | 2026.04.06 |
| ESP32 I2S(Inter-IC Sound) 고음질 디지털 오디오 인터페이스 (0) | 2026.04.01 |
| 용도에 꼭맞는 적합한 ESP32 선택하기 (0) | 2026.04.01 |
| ESP32 시스템에서 가능한 파일의 수와 구현 방안 (0) | 2026.03.31 |
| ESP32 C3 Supermini 시작 가이드 (1) | 2026.03.30 |
| ESP32 Super Mini 보드 Blink 이상할 때 (0) | 2026.03.30 |
취업, 창업의 막막함, 외주 관리, 제품 부재!
당신의 고민은 무엇입니까? 현실과 동떨어진 교육, 실패만 반복하는 외주 계약,
아이디어는 있지만 구현할 기술이 없는 막막함.
우리는 알고 있습니다. 문제의 원인은 '명확한 학습, 실전 경험과 신뢰할 수 있는 기술력의 부재'에서 시작됩니다.
이제 고민을 멈추고, 캐어랩을 만나세요!
코딩(펌웨어), 전자부품과 디지털 회로설계, PCB 설계 제작, 고객(시장/수출) 발굴과 마케팅 전략으로 당신을 지원합니다.
제품 설계의 고수는 성공이 만든 게 아니라 실패가 만듭니다. 아이디어를 양산 가능한 제품으로!
귀사의 제품을 만드세요. 교육과 개발 실적으로 신뢰할 수 있는 파트너를 확보하세요.
캐어랩