Trong quá trình phát triển các ứng dụng liên quan đến định vị GPS, việc đọc và phân tích dữ liệu NMEA (National Marine Electronics Association) từ cổng serial là một bài toán phổ biến. Bài viết này hướng dẫn xây dựng một bộ xử lý NMEA cơ bản bằng C++ trên nền tảng Windows, với khả năng lấy thông tin vị trí, thời gian, tốc độ và trạng thái vệ tinh.
Kiến trúc tổng quan
Hệ thống bao gồm các chức năng chính:
- Khởi tạo và cấu hình cổng serial
- Luồng đọc dữ liệu NMEA liên tục
- Phân tích các câu lệnh NMEA phổ biến: $GPRMC, $GPGGA, $GPGSV, $GPVTG
- Đồng bộ thời gian hệ thống từ dữ liệu UTC
- Giải phóng tài nguyên khi kết thúc
Khai báo header
File tiêu đề định nghĩa các hàm và biến toàn cục cần thiết:
#ifndef _GPS_MONITER_HH_
#define _GPS_MONITER_HH_
BOOL InitSerailPort(CString csSerialPort, LPVOID pParent = NULL);
DWORD WINAPI ReadNMEAThread(LPVOID lpParameter);
void SetSystemTimeFormUTC(CString csDate, CString csUTCTime);
void DeinitSerialPort(void);
#endif
Triển khai chi tiết
1. Khởi tạo cổng Serial
Hàm InitSerailPort mở kết nối với thiết bị GPS qua cổng COM, thiết lập tham số baudrate 9600, 8 bit dữ liệu, 1 stop bit, không parity. Ngoài ra, nó tạo luồng đọc dữ liệu và tính toán chênh lệch múi giờ:
BOOL InitSerailPort(CString csSerialPort, LPVOID pParent)
{
DCB commDCB;
COMMTIMEOUTS timeouts;
ghCommHandle = CreateFile(csSerialPort, GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, NULL);
if(INVALID_HANDLE_VALUE == ghCommHandle)
{
RETAILMSG(1, (TEXT("Opening GPS %s failed: %d!\r\n"), csSerialPort, (int)GetLastError()));
return FALSE;
}
// Thiết lập DCB
GetCommState(ghCommHandle, &commDCB);
commDCB.BaudRate = 9600;
commDCB.ByteSize = 8;
commDCB.Parity = NOPARITY;
commDCB.StopBits = ONESTOPBIT;
SetCommState(ghCommHandle, &commDCB);
// Cấu hình timeout đọc
GetCommTimeouts(ghCommHandle, &timeouts);
timeouts.ReadIntervalTimeout = 500;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.ReadTotalTimeoutConstant = 0;
SetCommTimeouts(ghCommHandle, &timeouts);
// Tạo luồng đọc NMEA
nmeathread_hand = CreateThread(NULL, 0, ReadNMEAThread, pParent, 0, NULL);
if(!nmeathread_hand)
return FALSE;
// Tính chênh lệch giờ địa phương so với UTC
SYSTEMTIME stUTC, stLocal;
GetLocalTime(&stLocal);
GetSystemTime(&stUTC);
giHourDiff = stLocal.wHour - stUTC.wHour;
return TRUE;
}
2. Luồng đọc NMEA
Luồng này chạy vô hạn, chờ sự kiện nhận dữ liệu trên cổng serial. Khi có dữ liệu, nó đọc vào bộ đệm và phân tích từng dòng NMEA dựa trên tiền tố $GPRMC, $GPGGA, $GPGSV, $GPVTG:
DWORD WINAPI ReadNMEAThread(LPVOID lpParameter)
{
int start, endline, onestart, oneend, linelen, degdig, iPos;
ULONG bytesRead;
DWORD EventMask = EV_RXCHAR;
CString field;
TCHAR *stopstring;
CWnd *mpNmea = (CWnd*)lpParameter;
while(WaitCommEvent(ghCommHandle, &EventMask, NULL))
{
memset(gcBuff, 0, 4096);
if (ReadFile(ghCommHandle, gcBuff, 4096, &bytesRead, NULL))
{
if(bytesRead == 0) continue;
CString dacstr(gcBuff);
start = 0;
while(1)
{
start = dacstr.Find(L"$G", start);
if(start < 0) break;
endline = dacstr.Find(L"\r\n", start);
if(endline < 0) break;
linelen = endline - start;
CString oneline = dacstr.Mid(start, linelen);
// Xử lý từng loại câu lệnh
if(oneline.Left(6) == L"$GPRMC")
{
// Trích xuất trạng thái GPS và ngày
int i = 0;
while((iPos = oneline.Find(L",")) >= 0)
{
field = oneline.Left(iPos);
i++;
oneline = oneline.Mid(iPos + 1);
if(i == 3) gcsGPSState = field;
else if(i == 10) gcsDate = field;
}
}
else if(oneline.Left(6) == L"$GPGGA")
{
// Trích xuất thời gian, vĩ độ, kinh độ, độ cao
int i = 0;
while((iPos = oneline.Find(L",")) >= 0)
{
field = oneline.Left(iPos);
i++;
oneline = oneline.Mid(iPos + 1);
if(i == 2)
{
gcsTimeOp = field;
// Chuyển đổi giờ UTC sang giờ địa phương
CString csHour = gcsTimeOp.Left(2);
int iHour = _wtoi(csHour);
if(iHour + giHourDiff < 24)
csHour.Format(L"%d", iHour + giHourDiff);
else
csHour.Format(L"%d", (iHour + giHourDiff) - 24);
gcsTime = csHour + gcsTimeOp.Mid(2,2) + gcsTimeOp.Right(5).Left(2);
}
else if(i == 3) // Vĩ độ
{
gcsLatField = field;
degdig = gcsLatField.GetLength() - 2;
csLat = gcsLatField.Left(2) + L" " + gcsLatField.Right(deggig);
}
else if(i == 4) csLatdir = field;
else if(i == 5) // Kinh độ
{
gcsLonField = field;
degdig = gcsLonField.GetLength() - 3;
csLon = gcsLonField.Left(3) + L" " + gcsLonField.Right(deggig);
}
else if(i == 6) csLondir = field;
else if(i == 7) giResult = atoi((const char*)((LPCTSTR)field));
else if(i == 10) csAltitude = field;
}
}
else if(oneline.Left(6) == L"$GPGSV")
{
// Xử lý thông tin vệ tinh
int i = 0;
while((iPos = oneline.Find(L",")) >= 0)
{
field = oneline.Left(iPos);
i++;
oneline = oneline.Mid(iPos + 1);
if(i == 3) // Số gói
{
giGSVCurrentPackage = _ttoi(field);
if(giGSVCurrentPackage == 1)
{
nNumDisplayed = 0;
for(int j = 0; j < SATTATOLNUMBER; j++)
gdSignalNumber[j] = -1;
}
}
else if(i == 4) giGSVSatNumber = (int)_tcstod(field, &stopstring);
else if((i - 5) % 4 == 0) giSalNumber = _ttoi(field);
else if((i - 5) % 4 == 3)
{
if(_ttoi(field) > 0)
{
gdSignalNumber[giSalNumber-1] = _ttoi(field);
nNumDisplayed++;
}
}
}
}
else if(oneline.Left(6) == L"$GPVTG")
{
// Trích xuất hướng đi và tốc độ
int i = 0;
while((iPos = oneline.Find(L",")) >= 0)
{
field = oneline.Left(iPos);
i++;
oneline = oneline.Mid(iPos + 1);
if(i == 2) csOrientation = field;
else if(i == 8) csSpeed = field;
}
}
start = endline + 2;
}
}
}
return 0;
}
3. Đồng bộ thời gian từ UTC
Hàm SetSystemTimeFormUTC nhận ngày và giờ UTC từ NMEA để thiết lập thời gian hệ thống:
void SetSystemTimeFormUTC(CString csDate, CString csUTCTime)
{
SYSTEMTIME st;
GetSystemTime(&st);
// Phân tích ngày: ddmmyy
CString csSubString = csDate.Left(2);
st.wDay = (int)_tcstod(csSubString, &stopstring);
csSubString = csDate.Mid(3,2);
st.wMonth = (int)_tcstod(csSubString, &stopstring);
csSubString = csDate.Right(2);
st.wYear = 2000 + (int)_tcstod(csSubString, &stopstring);
// Phân tích giờ: hhmmss
csSubString = csUTCTime.Left(2);
st.wHour = (int)_tcstod(csSubString, &stopstring);
csSubString = csUTCTime.Mid(3,2);
st.wMinute = (int)_tcstod(csSubString, &stopstring);
csSubString = csUTCTime.Mid(6,2);
st.wSecond = (int)_tcstod(csSubString, &stopstring);
SetSystemTime(&st);
}
4. Giải phóng tài nguyên
Hàm DeinitSerialPort đóng cổng serial và kết thúc luồng đọc an toàn:
void DeinitSerialPort(void)
{
SetCommMask(ghCommHandle, 0);
if(nmeathread_hand)
{
TerminateThread(nmeathread_hand, 1);
CloseHandle(nmeathread_hand);
}
if(INVALID_HANDLE_VALUE != ghCommHandle)
{
EscapeCommFunction(ghCommHandle, CLRDTR);
EscapeCommFunction(ghCommHandle, CLRRTS);
PurgeComm(ghCommHandle, PURGE_TXCLEAR | PURGE_RXCLEAR);
CloseHandle(ghCommHandle);
ghCommHandle = INVALID_HANDLE_VALUE;
}
}
Kết luận
Đoạn mã trên cung cấp một giải pháp hoàn chỉnh để đọc và phân tích dữ liệu NMEA từ thiết bị GPS qua cổng serial. Bạn có thể mở rộng để xử lý thêm các câu lệnh NMEA khác như $GPGSA, $GPGLL hoặc tích hợp giao diện người dùng để hiển thị thông tin vị trí, tốc độ và trạng thái vệ tinh một cách trực quan.