本文記錄一道來自中國大學 MOOC《C 語言程序設計》第九周作業的實戰題目:解析 GPS 接收機輸出的 NMEA-0183 協議數據流,提取有效的 $GPRMC 語句,驗證校驗和,篩選定位狀態,最終將 UTC 時間轉換為北京時間輸出。涉及字符串處理、指針操作、位運算、時間計算等核心技能。
NMEA-0183 是海事電子設備通信標準。$GPRMC(推薦最小定位信息)語句格式:
$GPRMC,024813.640,A,3158.4608,N,11848.3737,E,10.05,324.27,150706,,,A*50
| 字段 | 含義 | 示例 |
|---|---|---|
| 0 | 語句 ID | $GPRMC |
| 1 | UTC 時間 (hhmmss.sss) | 024813.640 |
| 2 | 狀態:A=定位,V=未定位 | A |
| 3-4 | 緯度/半球 | 3158.4608,N |
| 5-6 | 經度/半球 | 11848.3737,E |
| 7 | 速度(節) | 10.05 |
| 8 | 方位角(度) | 324.27 |
| 9 | UTC 日期 (DDMMYY) | 150706 |
| 10-11 | 磁偏角/方向 | (空) |
| 12 | 校驗和標識 + 十六進制值 | *50 |
校驗規則:$ 與 * 之間所有字符異或 (XOR) 後對 65536 取模,結果應等於 * 後兩位十六進制數(大寫 A-F)。
END 終止$GPRMC 且狀態字段為 ,A, 的行$ 後第一字符開始,逐字符 ^= 直到 * 前%02X,與 * 後字符比對hour = (utc_hour + 8) % 24hh:mm:sschar line[200];
while (scanf("%199s", line) == 1) { // %s 自動跳過空白,遇換行停止
if (strcmp(line, "END") == 0) break;
// 處理 line...
}
注意:scanf("%s") 以空白符分隔,NMEA 語句無內部空格,完美適配。若語句可能含空格,需用 fgets()。
strstr() 組合if (strstr(line, "$GPRMC") && strstr(line, ",A,")) {
// 同時滿足語句類型與定位狀態
}
strstr(haystack, needle) 返回子串首次出現地址,非 NULL 即真。,A, 精確匹配狀態字段,避免誤匹配其他字段中的 A。
unsigned char checksum = 0;
for (char *p = line + 1; *p != '*'; ++p) // 跳過 '$'
checksum ^= *p;
checksum %= 65536; // 實際上 unsigned char 自動模 256,題目要求模 65536 此處無影響
原理:XOR 滿足結合律/交換律,順序無關。unsigned char 保證 8 位無符號,最終值 0-255。
sprintf() + strcmp()char calc[3];
sprintf(calc, "%02X", checksum); // 大寫,不足兩位前補 0
char *star = strchr(line, '*');
if (star && strcmp(calc, star + 1) == 0) {
// 校驗通過
}
%02X 確保單個十六進制數字(如 0x0A)輸出 0A 而非 A,與協議格式一致。
strchr() 定位 + strncpy() + atoi()char *comma = strchr(line, ','); // 定位第一個逗號(字段 0 結束)
comma++; // 指向字段 1 首字符
char time_str[7];
strncpy(time_str, comma, 6); // 取 hhmmss
time_str[6] = '\0';
int utc_time = atoi(time_str); // 整數 24813
int hour = utc_time / 10000;
int minute = (utc_time % 10000) / 100;
int second = utc_time % 100;
hour = (hour + 8) % 24; // UTC+8
printf("%02d:%02d:%02d\n", hour, minute, second); // %02d 雙位補零
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main() {
char line[200];
int final_hour = 0, final_min = 0, final_sec = 0;
int has_valid = 0;
while (scanf("%199s", line) == 1) {
if (strcmp(line, "END") == 0) break;
// 1. 篩選 $GPRMC 且定位有效
if (!strstr(line, "$GPRMC") || !strstr(line, ",A,")) continue;
// 2. 計算校驗和
unsigned char sum = 0;
char *p = line + 1; // 跳過 '$'
while (*p && *p != '*') sum ^= *p++;
// 3. 校驗比對
char expected[3];
sprintf(expected, "%02X", sum);
char *star = strchr(line, '*');
if (!star || strcmp(expected, star + 1) != 0) continue;
// 4. 提取 UTC 時間(字段 1)
char *comma = strchr(line, ',');
if (!comma) continue;
comma++; // 指向時間字段
char time_buf[7];
strncpy(time_buf, comma, 6);
time_buf[6] = '\0';
int utc = atoi(time_buf);
int h = utc / 10000;
int m = (utc % 10000) / 100;
int s = utc % 100;
// 5. 轉北京時間(保留最後一條有效)
final_hour = (h + 8) % 24;
final_min = m;
final_sec = s;
has_valid = 1;
}
if (has_valid)
printf("%02d:%02d:%02d\n", final_hour, final_min, final_sec);
return 0;
}
輸入:
$GPRMC,024813.640,A,3158.4608,N,11848.3737,E,10.05,324.27,150706,,,A*50
END
輸出:
10:48:13
驗證:02:48:13 UTC + 8h = 10:48:13 北京時間。校驗和手算:$ 後到 * 前所有字符 XOR = 0x50,匹配。
| 陷阱 | 後果 | 修正 |
|---|---|---|
scanf("%s") 遇空格截斷 | 語句含空格時解析不全 | 改用 fgets(line, sizeof(line), stdin) 讀整行,去尾部 \n |
校驗和用 int 累加而非 XOR | 算法錯誤,永不通過 | 嚴格用 ^= 逐字符異或 |
%X 不補零 | 0x0A 輸出 A,比對失敗 | 用 %02X 強制兩位大寫 |
時間字段直接 atoi(line+7) | 假設字段固定位置,實際長度可變 | 用 strchr 定位分隔符動態提取 |
| 時區跨日未處理 | 23:00 UTC + 8 = 07:00 次日 | (h+8)%24 正確處理跨日 |
$GPGGA、$GPGSA 等語句,輸出結構體這道題目將 C 語言「指針、字符串、位運算、標準庫」四大基礎能力串聯在真實工程場景中:從原始字節流提取語義信息,經過校驗保證可信度,再經單位轉換產出業務價值。這正是嵌入式/物聯網/車聯網開發的日常核心功夫。