Xử lý và phân tích dữ liệu NMEA từ cổng Serial GPS

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.

Thẻ: NMEA GPS serial port C++ Windows API

Đăng vào ngày 23 tháng 6 lúc 11:02