本專案展示如何在 Linux 環境下,使用標準 POSIX 終端機介面 (termios) 存取串口裝置 (如 /dev/ttyS0),並從聯星 CC550-BG GPS 模組接收 NMEA-0183 句子,專門解析 GPRMC (Recommended Minimum Specific GPS/Transit Data) 格式,提取 UTC 時間、定位狀態、經緯度、速度與日期等關鍵資訊。
/dev/ttyS0 (或 USB 轉串口 /dev/ttyUSB*)termios.h, fcntl.h, unistd.h, string.hset_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;
}
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;
}
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;
}
}
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 等解析器。read() 永久封鎖,實現多執行緒或事件驅動架構。本範例完整示範了 Linux 串口程式設計的標準流程:開啟裝置 → 設定 termios → 封鎖讀取 → 協議解析 → 業務處理。採用逐字元 FSM 解析器,記憶體佔用極低,適合資源受限的嵌入式系統。理解 termios 各旗標含義,是進行可靠序列通訊開發的基石。