首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

Linux 串口程式設計實戰:C 語言解析 GPS NMEA 協議 (GPRMC 格式)

     編輯:關於C語言

專案背景與目標

本專案展示如何在 Linux 環境下,使用標準 POSIX 終端機介面 (termios) 存取串口裝置 (如 /dev/ttyS0),並從聯星 CC550-BG GPS 模組接收 NMEA-0183 句子,專門解析 GPRMC (Recommended Minimum Specific GPS/Transit Data) 格式,提取 UTC 時間、定位狀態、經緯度、速度與日期等關鍵資訊。

硬體與軟體環境

  • GPS 模組:聯星 CC550-BG (支援 NMEA-0183,波特率預設 9600)
  • 介面:RS-232 串列埠,對應 Linux 裝置節點 /dev/ttyS0 (或 USB 轉串口 /dev/ttyUSB*)
  • 作業系統:Linux (Kernel 2.6+),GCC 編譯環境
  • 關鍵函式庫:termios.h, fcntl.h, unistd.h, string.h

核心模組設計

1. 串口設定封裝 set_com_config()

統一配置鮑率、資料位、停止位、奇偶校驗,並啟用原始模式 (cfmakeraw) 以避免終端機驅動程式對控制字元的干擾。

int set_com_config(int fd, int baud_rate, int data_bits, char parity, int stop_bits)
{
    struct termios new_cfg, old_cfg;
    speed_t speed;

    if (tcgetattr(fd, &old_cfg) != 0) { perror("tcgetattr"); return -1; }
    tcflush(fd, TCIOFLUSH);
    new_cfg = old_cfg;
    cfmakeraw(&new_cfg);           // 原始模式:關閉回顯、緩衝、信號處理等
    new_cfg.c_cflag &= ~CSIZE;     // 清除資料位掩碼

    // 鮑率對應
    switch (baud_rate) {
        case 2400:   speed = B2400;   break;
        case 4800:   speed = B4800;   break;
        case 9600:   speed = B9600;   break;
        case 19200:  speed = B19200;  break;
        case 38400:  speed = B38400;  break;
        case 115200: speed = B115200; break;
        default:     return -1;
    }
    cfsetispeed(&new_cfg, speed);
    cfsetospeed(&new_cfg, speed);

    // 資料位
    new_cfg.c_cflag |= (data_bits == 7) ? CS7 : CS8;

    // 停止位
    new_cfg.c_cflag = (stop_bits == 2) ? (new_cfg.c_cflag | CSTOPB) : (new_cfg.c_cflag & ~CSTOPB);

    // 奇偶校驗
    switch (parity) {
        case 'o': case 'O': new_cfg.c_cflag |= (PARENB | PARODD); new_cfg.c_iflag |= (INPCK | ISTRIP); break;
        case 'e': case 'E': new_cfg.c_cflag |= PARENB; new_cfg.c_cflag &= ~PARODD; new_cfg.c_iflag |= (INPCK | ISTRIP); break;
        case 's': case 'S': new_cfg.c_cflag &= ~PARENB; new_cfg.c_cflag &= ~CSTOPB; break; // Space parity (罕用)
        case 'n': case 'N': default: new_cfg.c_cflag &= ~PARENB; new_cfg.c_iflag &= ~INPCK; break;
    }

    new_cfg.c_cc[VTIME] = 10;  // 讀取逾時 1 秒 (單位 0.1s)
    new_cfg.c_cc[VMIN]  = 5;   // 至少讀 5 字元才回傳
    tcflush(fd, TCIFLUSH);
    if (tcsetattr(fd, TCSANOW, &new_cfg) != 0) { perror("tcsetattr"); return -1; }
    return 0;
}

2. 串口開啟與封鎖模式切換 open_port()

int open_port(int com_port)
{
    const char *dev[] = { "/dev/ttyS0", "/dev/ttyS1", "/dev/ttyS2" };
    if (com_port < 1 || com_port > 3) return -1;

    int fd = open(dev[com_port - 1], O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd < 0) { perror("open serial port"); return -1; }

    // 關鍵:將非封鎖模式改為封鎖模式,配合 VMIN/VTIME 使用
    if (fcntl(fd, F_SETFL, 0) < 0) { perror("fcntl F_SETFL"); close(fd); return -1; }
    return fd;
}

3. GPRMC 句子逐字元解析器 GPS_resolve_GPRMC()

採用有限狀態機 (FSM) 設計:以逗號 ',' 為欄位分隔符,SectionID 追蹤當前欄位索引,i 記錄欄位內字元位置。支援即時流式處理,無需緩存完整行。

void GPS_resolve_GPRMC(char data)
{
    static int SectionID = 0, i = 0;  // 靜態變數保持狀態

    if (data == ',') { SectionID++; i = 0; return; }

    switch (SectionID) {
        case 1: // UTC 時間 hhmmss.ss -> hh:mm:ss
            GPS_DATA.GPS_time[i++] = data;
            if (i == 2 || i == 5) GPS_DATA.GPS_time[i++] = ':';
            if (i >= 8) GPS_DATA.GPS_time[8] = '\0';
            break;
        case 2: // 定位狀態 A=有效 V=無效
            GPS_DATA.GPS_sv = (data == 'A') ? '>' : '<';
            break;
        case 3: // 緯度 ddmm.mmmm
            GPS_DATA.GPS_wd[++i] = data; GPS_DATA.GPS_wd[11] = '\0'; break;
        case 4: // 緯度半球 N/S
            GPS_DATA.GPS_wd[0] = (data == 'N') ? 'N' : 'S'; break;
        case 5: // 經度 dddmm.mmmm
            GPS_DATA.GPS_jd[++i] = data; GPS_DATA.GPS_jd[11] = '\0'; break;
        case 6: // 經度半球 E/W
            GPS_DATA.GPS_jd[0] = (data == 'E') ? 'E' : 'W'; break;
        case 7: // 速度 (節) -> 顯示為 km/h (需換算 1.852)
            GPS_DATA.GPS_speed[i++] = data; GPS_DATA.GPS_speed[4] = '\0'; break;
        case 9: // 日期 ddmmyy -> dd-mm-yy
            GPS_DATA.GPS_date[i++] = data;
            if (i == 2 || i == 5) GPS_DATA.GPS_date[i++] = '-';
            if (i >= 8) GPS_DATA.GPS_date[8] = '\0';
            break;
    }
}

4. 資料讀取主迴圈 read_data()

逐字元讀取 (read(fd, buffer, 1)),累積至 dest 緩衝區,遇換行符 '\n' 判斷是否為 $GPRMC 開頭,若是則逐字元送入解析器並列印結果。

void read_data(int fd)
{
    char buffer[512], dest[1024];
    const char *header = "$GPRMC";
    int res, len, k;

    memset(dest, 0, sizeof(dest));
    while (1) {
        memset(buffer, 0, sizeof(buffer));
        if ((res = read(fd, buffer, 1)) > 0) {
            strcat(dest, buffer);
            if (buffer[0] == '\n') {
                if (strncmp(dest, header, 6) == 0) {
                    printf("Raw: %s", dest);
                    len = strlen(dest);
                    for (k = 0; k < len; k++) GPS_resolve_GPRMC(dest[k]);
                    print_info();  // 列印解析後的結構化資料
                }
                bzero(dest, sizeof(dest));
            }
        }
    }
    close(fd); // 理論上不可達
}

資料結構定義

struct gps_data {
    char GPS_time[9];   // hh:mm:ss
    char GPS_sv;        // '>' 有效 / '<' 無效
    char GPS_wd[12];    // N/S + ddmm.mmmm
    char GPS_jd[12];    // E/W + dddmm.mmmm
    char GPS_speed[5];  // 速度字串
    char GPS_date[9];   // dd-mm-yy
} GPS_DATA;

主程式進入點

int main(int argc, char *argv[])
{
    int fd = open_port(1);  // 使用 ttyS0
    if (fd < 0) return -1;
    printf("Open success!\n");

    if (set_com_config(fd, 9600, 8, 'N', 1) < 0) return -1;
    printf("Config done. Receiving data...\n");

    read_data(fd);
    return 0;
}

編譯與執行

gcc -o gps_parser gps_parser.c
sudo ./gps_parser

注意:存取 /dev/ttyS* 通常需 root 權限或將使用者加入 dialout 群組。

擴展建議

  • 多句型支援:在 read_data() 中以 switch 或查表方式擴充 GPGGA、GPGSA、GPGSV 等解析器。
  • 錯誤檢測:驗證 NMEA 校驗和 (星號後兩位十六進位),捨棄受損封包。
  • 非封鎖 + select/poll:避免 read() 永久封鎖,實現多執行緒或事件驅動架構。
  • 座標轉換:將度分格式 (ddmm.mmmm) 轉換為十進位度 (DD.DDDDDD) 便於地圖顯示。

總結

本範例完整示範了 Linux 串口程式設計的標準流程:開啟裝置 → 設定 termios → 封鎖讀取 → 協議解析 → 業務處理。採用逐字元 FSM 解析器,記憶體佔用極低,適合資源受限的嵌入式系統。理解 termios 各旗標含義,是進行可靠序列通訊開發的基石。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved