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
- 1. GPS làm việc thế nào
- 2. Module GPS NEO-6M
- 3. Linh kiện cần chuẩn bị
- 4. Sơ đồ nối dây
- 5. Câu NMEA module gửi ra
- 6. Chương trình đọc GPS không cần thư viện
- 7. Giải thích các phần chính
- 8. Đổi số liệu NMEA ra số dùng được
- 9. Kết quả trên Serial Monitor
- 10. Mẹo bắt tín hiệu nhanh, đo chính xác hơn
- 11. Xử lý lỗi thường gặp
- 12. Xem thêm
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

| Thông số | Giá trị (theo datasheet NEO-6M) |
|---|---|
| Hệ vệ tinh | GPS (Mỹ), băng L1 |
| Thời gian định vị lần đầu | Khoả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ật | 1 lần mỗi giây (mặc định), tối đa 5 lần mỗi giây |
| Giao tiếp | UART, mặc định 9600 baud, gửi câu NMEA dạng chữ |
| Nguồn module | 3,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ị
- ESP32 DevKit V1 30 chân (hoặc loại 38 chân).
- Module GPS NEO-6M kèm anten gốm.
- Dây cắm test board và dây USB nạp code.
- Muốn mang ra ngoài trời thử: dùng mạch ESP32 tích hợp pin 18650 hoặc sạc dự phòng.
4. Sơ đồ nối dây

| Module GPS | ESP32 DevKit V1 |
|---|---|
| VCC | 3V3 |
| RX | TX2 (GPIO17) |
| TX | RX2 (GPIO16) |
| GND | GND |
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

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ặcV(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

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

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ượng | Nguyên nhân thường gặp | Cá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 IPEX | Ra chỗ thoáng, chờ 2–5 phút; ấn chặt đầu cáp anten |
| Không in gì từ GPS | Nối TX vào TX; sai tốc độ baud; chưa cấp nguồn | TX của GPS vào RX2 (GPIO16); giữ 9600 baud |
| Chỉ in câu có nhiều dấu phẩy trống | Module đ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ếng | Dùng giờ UTC trong câu NMEA | Cộng múi giờ như chương trình |
| Thỉnh thoảng có tọa độ nhảy xa | Câu bị nhiễu mà không kiểm checksum; HDOP lớn | Kiểm checksum; bỏ điểm có HDOP trên 5 |
12. Xem thêm
- Module thẻ nhớ Micro SD với ESP32: ghi lại hành trình để mở trên máy tính.
- Gateway LoRa sang MQTT: gửi vị trí từ nơi không có Wi-Fi về trạm thu cách vài km.
- Deep Sleep trên ESP32: thiết bị định vị chạy pin lâu hơn.
- Màn hình OLED 0.96 inch để hiện tọa độ, tốc độ ngay trên thiết bị.
Xem thêm