GPS NEO-6M với ESP32: đọc câu NMEA, lấy tọa độ, giờ Việt Nam, tốc độ không cần thư viện

GPS làm việc thế nào, module NEO-6M, nối với ESP32 qua UART2, đọc câu NMEA RMC và GGA, kiểm checksum, đổi tọa độ, giờ Việt Nam, tốc độ, tính khoảng cách, không cần TinyGPS.

Module GPS NEO-6M cho biết thiết bị đang ở đâu, đi nhanh bao nhiêu, kèm ngày giờ chuẩn lấy từ vệ tinh. Đây là bộ phận chính của thiết bị định vị xe, máy ghi hành trình, trạm đo đặt ngoài đồng, hoặc đồng hồ tự chỉnh giờ không cần Internet. Bài này giải thích GPS làm việc thế nào, nối NEO-6M với ESP32, đọc các câu NMEA mà module gửi ra, rồi viết chương trình lấy tọa độ, giờ Việt Nam, tốc độ, độ cao và khoảng cách đã đi, không cần thư viện TinyGPS.

1. GPS làm việc thế nào

Mỗi vệ tinh GPS liên tục phát đi giờ chính xác của nó và vị trí của nó trên quỹ đạo. Máy thu so giờ nhận được với giờ phát để biết tín hiệu đã bay mất bao lâu, từ đó ra khoảng cách tới vệ tinh. Có khoảng cách tới ít nhất 4 vệ tinh thì máy thu tính được vĩ độ, kinh độ, độ cao và cả sai lệch đồng hồ của chính nó.

Tín hiệu vệ tinh rất yếu khi tới mặt đất nên máy thu cần nhìn được bầu trời. Trong nhà, dưới mái tôn, giữa các nhà cao tầng thường không bắt được hoặc vị trí bị lệch. Lần đầu bật (khởi động lạnh), máy thu còn phải tải dữ liệu quỹ đạo từ vệ tinh nên mất từ 30 giây tới vài phút.

2. Module GPS NEO-6M

Module GPS NEO-6M màu xanh có chip thu GPS, pin nuôi, đèn LED, hàng 4 chân VCC RX TX GND và anten gốm rời nối bằng cáp nhỏ
Các phần chính của module GPS NEO-6M loại phổ biến (hình minh họa).
Thông sốGiá trị (theo datasheet NEO-6M)
Hệ vệ tinhGPS (Mỹ), băng L1
Thời gian định vị lần đầuKhoảng 27 giây khi khởi động lạnh, khoảng 1 giây khi khởi động nóng (trời thoáng)
Độ chính xác vị tríKhoảng 2,5 m (trời thoáng)
Tần số cập nhật1 lần mỗi giây (mặc định), tối đa 5 lần mỗi giây
Giao tiếpUART, mặc định 9600 baud, gửi câu NMEA dạng chữ
Nguồn module3,3–5 V (module có mạch ổn áp cho chip 3,3 V)

Pin cúc áo trên module giữ dữ liệu vệ tinh vài giờ sau khi tắt nguồn, nhờ đó lần bật sau định vị nhanh hơn. Đèn LED trên module nhấp nháy mỗi giây khi đã định vị được, rất tiện để biết module đã bắt được vệ tinh mà chưa cần mở Serial Monitor.

3. Linh kiện cần chuẩn bị

4. Sơ đồ nối dây

Sơ đồ nối module GPS NEO-6M với ESP32: VCC vào 3V3, RX vào TX2, TX vào RX2, GND vào GND
GPS nối vào cổng UART thứ hai của ESP32: TX của GPS vào RX2 (hình minh họa).
Module GPSESP32 DevKit V1
VCC3V3
RXTX2 (GPIO17)
TXRX2 (GPIO16)
GNDGND

ESP32 có ba cổng UART. UART0 đã dùng cho cổng USB và Serial Monitor, nên GPS dùng UART2 ở GPIO16, GPIO17. Ý nghĩa TX, RX và cách dùng Serial2 có trong bài UART là gì và cách dùng Serial2 trên ESP32.

5. Câu NMEA module gửi ra

Phân tích câu GPRMC và GPGGA: loại câu, giờ UTC, trạng thái, vĩ độ, kinh độ, tốc độ, ngày, số vệ tinh, HDOP, độ cao và checksum
Câu RMC cho giờ, ngày, tọa độ, tốc độ; câu GGA cho số vệ tinh, HDOP, độ cao (số liệu ví dụ).

Mỗi giây NEO-6M gửi khoảng 6 câu chữ theo chuẩn NMEA 0183. Mỗi câu bắt đầu bằng $, các trường ngăn bằng dấu phẩy, cuối câu là * và 2 chữ số checksum. Bài này chỉ cần hai câu:

  • RMC (Recommended Minimum): giờ UTC, trạng thái A (đã định vị) hoặc V (chưa), vĩ độ, kinh độ, tốc độ tính bằng hải lý/giờ, hướng đi, ngày.
  • GGA: chất lượng định vị, số vệ tinh đang dùng, HDOP (độ "tốt" của hình học vệ tinh), độ cao so với mực nước biển.

Các câu khác như GSV (danh sách vệ tinh nhìn thấy, cường độ tín hiệu), GSA, VTG có thể bỏ qua. Mở Serial Monitor của một chương trình chỉ chuyển dữ liệu từ Serial2 sang Serial là thấy toàn bộ các câu này.

6. Chương trình đọc GPS không cần thư viện

Chọn bo ESP32 Dev Module rồi nạp. Bài dùng gói esp32 phiên bản 3.3.10, không cần cài thêm thư viện.

// PYWORLD.VN - GPS NEO-6M với ESP32: đọc câu NMEA, kiểm tra checksum, lấy tọa độ, giờ Việt Nam, tốc độ, độ cao
// GPS TX vào RX2 (GPIO16), GPS RX vào TX2 (GPIO17), VCC vào 3V3, GND chung; tốc độ mặc định của NEO-6M là 9600 baud
// Tính khoảng cách tới điểm có tín hiệu đầu tiên bằng công thức haversine, không cần thư viện TinyGPS

HardwareSerial gps(2);
const int CHAN_RX2 = 16;
const int CHAN_TX2 = 17;
const int MUI_GIO = 7;                      // giờ Việt Nam = UTC + 7

struct ViTri {
  bool coTinHieu = false;                   // trường trạng thái của câu RMC là 'A'
  double viDo = 0, kinhDo = 0;
  float tocDoKmh = 0, doCao = 0, hdop = 99;
  int soVeTinh = 0;
  int gio = 0, phut = 0, giay = 0, ngay = 0, thang = 0, nam = 0;
};
ViTri vt;
bool coDiemDau = false;
double viDoDau, kinhDoDau;

// Checksum NMEA: XOR mọi ký tự giữa '$' và '*', so với 2 chữ số hex sau '*'.
bool dungChecksum(const char *cau) {
  if (cau[0] != '$') return false;
  uint8_t tong = 0;
  const char *p = cau + 1;
  while (*p && *p != '*') tong ^= (uint8_t)*p++;
  if (*p != '*') return false;
  return strtol(p + 1, NULL, 16) == tong;
}

// Lấy trường thứ n (0 là tên câu) giữa các dấu phẩy. Trường rỗng trả về chuỗi rỗng.
void layTruong(const char *cau, int n, char *ra, size_t toiDa) {
  const char *p = cau;
  for (int i = 0; i < n && p; i++) {
    p = strchr(p, ',');
    if (p) p++;
  }
  size_t k = 0;
  while (p && *p && *p != ',' && *p != '*' && k + 1 < toiDa) ra[k++] = *p++;
  ra[k] = 0;
}

// "1046.6134" + "N" -> 10,776890 độ. Vĩ độ dạng ddmm.mmmm, kinh độ dạng dddmm.mmmm.
double doiToaDo(const char *giaTri, const char *huong) {
  if (!*giaTri) return 0;
  double v = atof(giaTri);
  int doNguyen = (int)(v / 100);
  double kq = doNguyen + (v - doNguyen * 100) / 60.0;
  if (*huong == 'S' || *huong == 'W') kq = -kq;
  return kq;
}

int soNgayTrongThang(int thang, int nam) {
  static const int ngay[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  bool nhuan = (nam % 4 == 0 && nam % 100 != 0) || nam % 400 == 0;
  return (thang == 2 && nhuan) ? 29 : ngay[thang - 1];
}

// $GPRMC,hhmmss.ss,A,ddmm.mmmm,N,dddmm.mmmm,E,tốc độ (hải lý/giờ),hướng,ddmmyy,...
void xuLyRmc(const char *cau) {
  char t[16], tt[4], vd[16], hvd[4], kd[16], hkd[4], tocDo[12], ngay[8];
  layTruong(cau, 1, t, sizeof(t));
  layTruong(cau, 2, tt, sizeof(tt));
  layTruong(cau, 3, vd, sizeof(vd));
  layTruong(cau, 4, hvd, sizeof(hvd));
  layTruong(cau, 5, kd, sizeof(kd));
  layTruong(cau, 6, hkd, sizeof(hkd));
  layTruong(cau, 7, tocDo, sizeof(tocDo));
  layTruong(cau, 9, ngay, sizeof(ngay));
  vt.coTinHieu = tt[0] == 'A';
  if (strlen(t) >= 6 && strlen(ngay) == 6) {
    vt.gio = (t[0] - '0') * 10 + (t[1] - '0') + MUI_GIO;
    vt.phut = (t[2] - '0') * 10 + (t[3] - '0');
    vt.giay = (t[4] - '0') * 10 + (t[5] - '0');
    vt.ngay = (ngay[0] - '0') * 10 + (ngay[1] - '0');
    vt.thang = (ngay[2] - '0') * 10 + (ngay[3] - '0');
    vt.nam = 2000 + (ngay[4] - '0') * 10 + (ngay[5] - '0');
    if (vt.gio >= 24) {                     // cộng 7 giờ có thể sang ngày mới
      vt.gio -= 24;
      if (++vt.ngay > soNgayTrongThang(vt.thang, vt.nam)) {
        vt.ngay = 1;
        if (++vt.thang > 12) {
          vt.thang = 1;
          vt.nam++;
        }
      }
    }
  }
  if (vt.coTinHieu) {
    vt.viDo = doiToaDo(vd, hvd);
    vt.kinhDo = doiToaDo(kd, hkd);
    vt.tocDoKmh = atof(tocDo) * 1.852f;     // 1 hải lý/giờ = 1,852 km/h
  }
}

// $GPGGA,hhmmss.ss,vĩ độ,N,kinh độ,E,chất lượng,số vệ tinh,HDOP,độ cao,M,...
void xuLyGga(const char *cau) {
  char sv[6], hd[8], cao[12];
  layTruong(cau, 7, sv, sizeof(sv));
  layTruong(cau, 8, hd, sizeof(hd));
  layTruong(cau, 9, cao, sizeof(cao));
  vt.soVeTinh = atoi(sv);
  if (*hd) vt.hdop = atof(hd);
  if (*cao) vt.doCao = atof(cao);
}

// Khoảng cách trên mặt đất giữa hai điểm (mét), công thức haversine, bán kính Trái Đất 6371 km.
double khoangCachM(double vd1, double kd1, double vd2, double kd2) {
  const double R = 6371000.0, RAD = PI / 180.0;
  double dvd = (vd2 - vd1) * RAD, dkd = (kd2 - kd1) * RAD;
  double a = sin(dvd / 2) * sin(dvd / 2) + cos(vd1 * RAD) * cos(vd2 * RAD) * sin(dkd / 2) * sin(dkd / 2);
  return 2 * R * atan2(sqrt(a), sqrt(1 - a));
}

void xuLyCau(const char *cau) {
  if (!dungChecksum(cau)) return;           // câu hỏng do nhiễu: bỏ qua
  if (strlen(cau) < 7) return;
  // cau[1..2] là nguồn: GP (GPS), GN (nhiều hệ vệ tinh); cau[3..5] là loại câu
  if (strncmp(cau + 3, "RMC", 3) == 0) xuLyRmc(cau);
  else if (strncmp(cau + 3, "GGA", 3) == 0) xuLyGga(cau);
}

void setup() {
  Serial.begin(115200);
  gps.begin(9600, SERIAL_8N1, CHAN_RX2, CHAN_TX2);
  Serial.println("Dang cho tin hieu GPS...");
}

void loop() {
  static char cau[100];
  static size_t n = 0;
  while (gps.available()) {
    char c = gps.read();
    if (c == '$') n = 0;                    // đầu câu mới
    if (c == '\r' || c == '\n') {
      if (n) {
        cau[n] = 0;
        xuLyCau(cau);
        n = 0;
      }
    } else if (n < sizeof(cau) - 1) {
      cau[n++] = c;
    }
  }

  static uint32_t lanIn = 0;
  if (millis() - lanIn < 1000) return;
  lanIn = millis();
  if (!vt.coTinHieu) {
    Serial.printf("Chua co tin hieu: %d ve tinh. Dua anten ra noi thoang, cho 1-2 phut\n", vt.soVeTinh);
    return;
  }
  if (!coDiemDau) {
    coDiemDau = true;
    viDoDau = vt.viDo;
    kinhDoDau = vt.kinhDo;
    Serial.printf("Ban do: https://maps.google.com/?q=%.6f,%.6f\n", vt.viDo, vt.kinhDo);
  }
  Serial.printf("%02d:%02d:%02d %02d/%02d/%04d | %.6f, %.6f | %d ve tinh, HDOP %.1f | cao %.1f m | %.1f km/h | cach diem dau %.1f m\n",
                vt.gio, vt.phut, vt.giay, vt.ngay, vt.thang, vt.nam, vt.viDo, vt.kinhDo, vt.soVeTinh, vt.hdop, vt.doCao,
                vt.tocDoKmh, khoangCachM(viDoDau, kinhDoDau, vt.viDo, vt.kinhDo));
}

7. Giải thích các phần chính

  • Gom từng câu: loop() đọc từng ký tự từ Serial2, gặp $ thì bắt đầu câu mới, gặp xuống dòng thì xử lý câu vừa đủ. Bộ đệm 100 ký tự đủ cho câu NMEA (tối đa 82 ký tự).
  • dungChecksum(): XOR mọi ký tự giữa $ và *, so với số hex sau dấu *. Câu bị nhiễu, mất ký tự sẽ bị loại, tránh in ra tọa độ sai.
  • layTruong(): lấy trường thứ n, kể cả trường rỗng. Khi chưa định vị, nhiều trường để trống (,,), nên không dùng được cách tách chuỗi bỏ qua trường rỗng.
  • Nhận cả GP và GN: chương trình chỉ so ba chữ cái loại câu (RMC, GGA), nên vẫn chạy với module nhiều hệ vệ tinh như NEO-M8N gửi $GNRMC.
  • doiToaDo(): NMEA ghi vĩ độ dạng ddmm.mmmm (độ và phút). Phải đổi sang độ thập phân bằng cách chia phần phút cho 60, không được đọc thẳng 1046.6134 thành 10,466134 độ.
  • Giờ Việt Nam: cộng 7 vào giờ UTC; quá 24 giờ thì tăng ngày, xét đủ số ngày của tháng và năm nhuận.
  • khoangCachM(): công thức haversine tính khoảng cách trên mặt cầu giữa hai tọa độ. Chương trình lấy điểm định vị đầu tiên làm mốc.

Thư viện TinyGPSPlus làm đúng những việc này và dùng được với ESP32. Tự viết một lần giúp hiểu dữ liệu GPS, và chương trình gọn khi chỉ cần vài trường.

8. Đổi số liệu NMEA ra số dùng được

Bảng đổi số liệu NMEA: tọa độ ddmm.mmmm sang độ thập phân, giờ UTC sang giờ Việt Nam, hải lý trên giờ sang km trên giờ, khoảng cách bằng haversine
Cách chương trình đổi tọa độ, giờ, tốc độ (số liệu ví dụ).

9. Kết quả trên Serial Monitor

Serial Monitor in thông báo chưa có tín hiệu, đường link bản đồ, rồi mỗi giây in giờ Việt Nam, tọa độ, số vệ tinh, HDOP, độ cao, tốc độ và khoảng cách tới điểm đầu
Lúc chờ tín hiệu, rồi in mỗi giây khi đã định vị (hình minh họa, số liệu ví dụ).

Khi có tín hiệu, chương trình in một đường link bản đồ. Chép link vào trình duyệt là thấy vị trí trên bản đồ, đối chiếu được với vị trí thật.

10. Mẹo bắt tín hiệu nhanh, đo chính xác hơn

  • Đặt anten gốm nằm ngang, mặt có gân bạc hướng lên trời, cách xa ESP32 và dây nguồn vài cm: Wi-Fi và nguồn xung gây nhiễu cho máy thu GPS.
  • Thử lần đầu ở ngoài trời, sân thượng, hoặc sát cửa sổ. Bắt được rồi thì lần sau nhanh hơn nhờ pin nuôi.
  • Đứng yên mà tọa độ nhảy vài mét, tốc độ báo 0,3–1 km/h là bình thường. Muốn đo quãng đường thì chỉ cộng đoạn nào dài hơn 5–10 m để không cộng dồn sai số.
  • Giờ GPS rất chuẩn: có thể dùng GPS để chỉnh giờ cho module DS3231 ở nơi không có Wi-Fi.

11. Xử lý lỗi thường gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
Luôn báo "0 ve tinh", không thấy LED nháyĐang ở trong nhà; anten chưa cắm chặt vào đầu IPEXRa chỗ thoáng, chờ 2–5 phút; ấn chặt đầu cáp anten
Không in gì từ GPSNối TX vào TX; sai tốc độ baud; chưa cấp nguồnTX của GPS vào RX2 (GPIO16); giữ 9600 baud
Chỉ in câu có nhiều dấu phẩy trốngModule đang tìm vệ tinh, chưa định vịChờ thêm; trường trạng thái RMC chuyển từ V sang A là có vị trí
Tọa độ lệch vài trăm kmĐọc ddmm.mmmm như độ thập phânĐổi bằng doiToaDo()
Giờ chậm 7 tiếngDùng giờ UTC trong câu NMEACộng múi giờ như chương trình
Thỉnh thoảng có tọa độ nhảy xaCâu bị nhiễu mà không kiểm checksum; HDOP lớnKiểm checksum; bỏ điểm có HDOP trên 5

12. Xem thêm


Tin tức liên quan

Hướng dẫn bắt đầu MicroPython trên ESP32 Lite với Thonny
Hướng dẫn bắt đầu MicroPython trên ESP32 Lite với Thonny

Hướng dẫn cài driver, cài Thonny và nạp firmware MicroPython cho mạch ESP32 Lite, dùng REPL, nháy LED, lưu main.py, đọc nút nhấn, quét I2C và xử lý lỗi thường gặp.

ESP32 điều khiển động cơ bước 28BYJ-48 với ULN2003: nửa bước, quay đúng góc, kim giây
ESP32 điều khiển động cơ bước 28BYJ-48 với ULN2003: nửa bước, quay đúng góc, kim giây

Hướng dẫn điều khiển động cơ bước 28BYJ-48 bằng ESP32 qua mạch ULN2003: chế độ nửa bước, 4076 bước một vòng, quay đúng góc theo lệnh, kim giây đồng hồ và thí nghiệm STEM.

Đế ra chân ESP32 30 chân: cấp nguồn DC 6,5–16 V, hàng chân G V S và jumper chọn 5 V hay 3,3 V
Đế ra chân ESP32 30 chân: cấp nguồn DC 6,5–16 V, hàng chân G V S và jumper chọn 5 V hay 3,3 V

Cách dùng đế ra chân ESP32 30 chân: cấu tạo, cắm ESP32 đúng chiều, cấp nguồn jack DC 6,5–16 V và tính nhiệt IC ổn áp, hàng G V S, jumper 5V/3.3V, ví dụ relay và nút nhấn.

ESP32 điều khiển máy CNC, máy laser với FluidNC: Wi-Fi, file cấu hình và giao diện web
ESP32 điều khiển máy CNC, máy laser với FluidNC: Wi-Fi, file cấu hình và giao diện web

Dùng ESP32 DevKit V1 chạy FluidNC điều khiển máy khắc laser 2 trục: sơ đồ nối hai A4988, cài firmware từ trình duyệt, file config.yaml mẫu giải thích từng dòng, lệnh hay dùng, Wi-Fi.

ESP32 với Firebase Realtime Database qua REST: đăng nhập ẩn danh, làm mới token, điều khiển relay
ESP32 với Firebase Realtime Database qua REST: đăng nhập ẩn danh, làm mới token, điều khiển relay

ESP32 dùng Firebase Realtime Database không cần thư viện Firebase: đăng nhập ẩn danh, tự làm mới token, đọc lệnh relay, ghi trạng thái và lịch sử, Rules theo uid.

Dải LED WS2812B với ESP32: điều khiển từng bóng bằng RMT không cần thư viện, tính dòng và chọn nguồn
Dải LED WS2812B với ESP32: điều khiển từng bóng bằng RMT không cần thư viện, tính dòng và chọn nguồn

Bên trong bóng WS2812B, tín hiệu một dây 24 bit GRB, cách tính dòng để chọn nguồn, nối dải LED với ESP32 và tạo tín hiệu bằng bộ RMT không cần thư viện, kèm ba hiệu ứng.

Hướng dẫn sử dụng màn hình LCD 1602 I2C với Arduino Uno
Hướng dẫn sử dụng màn hình LCD 1602 I2C với Arduino Uno

Hướng dẫn nối màn hình LCD 1602 I2C với Arduino Uno, cài thư viện LiquidCrystal_I2C, chạy code hiển thị và quét địa chỉ I2C 0x27/0x3F.

Xe tránh vật cản với ESP32: cảm biến siêu âm HC-SR04 trên servo quét, tự chọn đường
Xe tránh vật cản với ESP32: cảm biến siêu âm HC-SR04 trên servo quét, tự chọn đường

Làm xe robot tự tránh vật cản với ESP32: cảm biến siêu âm HC-SR04 gắn trên servo quét trái phải, cầu phân áp cho chân ECHO, thuật toán chọn đường, lưu đồ và thí nghiệm STEM.


Bình luận
Đã thêm vào giỏ hàng