利用序列埠連接 GSM 手機收發簡訊,核心在於理解 ETSI 制定的 SMS 規範:GSM 03.38(字元集)、GSM 03.40(PDU 結構與編碼)、GSM 07.05(AT 指令集)。三種傳輸模式中,PDU Mode 為所有手機支援、預設模式,可使用任意字元集,適合中文環境與自訂資料傳輸。本文深入剖析 PDU 結構、三種編碼演算法(7-bit、8-bit、UCS2)與字串/位元組互轉工具。
PDU 表面為 ASCII 碼(0-9, A-F),實為 8 位元組十六進制或 BCD 碼。包含 SMSC 號碼、目標號碼、編碼方式、時間戳記等元資料。發送與接收 PDU 結構略有不同。
SMSC: +8613800250500,目標: 13851872468,內容: Hello!
08 91 68 31 08 20 05 05 F0 11 00 0D 91 68 31 58 81 27 64 F8 00 00 00 06 C8 32 9B FD 0E 01
| 欄位 | 含義 | 說明 |
|---|---|---|
| 08 | SMSC 地址長度 | 含 91 共 8 位元組 |
| 91 | TON/NPI | 國際格式(+ 號) |
| 68 31 08 20 05 05 F0 | SMSC 號碼 | 8613800250500,補 F 湊偶數 |
| 11 | TP-MTI/VPF | 發送,TP-VP 相對格式 |
| 00 | TP-MR | 訊息參考值 0 |
| 0D | 目標地址數字數 | 13 位十進制數字 |
| 91 | 目標 TON/NPI | 國際格式 |
| 68 31 58 81 27 64 F8 | TP-DA | 8613851872468,補 F |
| 00 | TP-PID | 普通 GSM 點對點 |
| 00 | TP-DCS | 7-bit 編碼 |
| 00 | TP-VP | 有效期 5 分鐘 |
| 06 | TP-UDL | 使用者資料長度 6 字元 |
| C8 32 9B FD 0E 01 | TP-UD | "Hello!" 7-bit 編碼結果 |
SMSC: +8613800250500,來源: 13851872468,內容: 你好! (UCS2)
08 91 68 31 08 20 05 05 F0 84 0D 91 68 31 58 81 27 64 F8 00 08 30 30 21 80 63 54 80 06 4F 60 59 7D 00 21
| 欄位 | 含義 | 說明 |
|---|---|---|
| 84 | TP-MTI/MMS/RP | 接收,無更多訊息,含回覆地址 |
| 08 | TP-DCS | UCS2 編碼 |
| 30 30 21 80 63 54 80 | TP-SCTS | 2003-03-12 08:36:45 +8 時區 |
| 06 | TP-UDL | 使用者資料長度 6 位元組 |
| 4F 60 59 7D 00 21 | TP-UD | "你好!" UCS2 編碼 |
關鍵觀察:號碼與時間戳記皆採「兩兩顛倒」BCD 格式,奇數長度補 'F'。
適用 ASCII 字元,將 7-bit 字元壓縮裝入 8-bit 位元組,每 8 字元壓縮為 7 位元組。TP-UDL 為編碼前字元數。最大容量 160 字元。

C 實作:
// 7-bit 編碼
int gsmEncode7bit(const char* pSrc, unsigned char* pDst, int nSrcLength) {
int nSrc = 0, nDst = 0, nChar; unsigned char nLeft = 0;
while (nSrc < nSrcLength) {
nChar = nSrc & 7;
if (nChar == 0) nLeft = *pSrc;
else {
*pDst = (*pSrc << (8 - nChar)) | nLeft;
nLeft = *pSrc >> nChar;
pDst++; nDst++;
}
pSrc++; nSrc++;
}
return nDst;
}
// 7-bit 解碼
int gsmDecode7bit(const unsigned char* pSrc, char* pDst, int nSrcLength) {
int nSrc = 0, nDst = 0, nByte = 0; unsigned char nLeft = 0;
while (nSrc < nSrcLength) {
*pDst = ((*pSrc << nByte) | nLeft) & 0x7F;
nLeft = *pSrc >> (7 - nByte);
pDst++; nDst++; nByte++;
if (nByte == 7) { *pDst = nLeft; pDst++; nDst++; nByte = 0; nLeft = 0; }
pSrc++; nSrc++;
}
*pDst = 0; return nDst;
}
注意:GSM 7-bit 字元集與 ANSI 不完全相同,0x20 以下含可列印字元,但英數字與常用符號位置相同。法德西等特殊字元需查表(參閱 GSM 03.38)。
無特定演算法,位元組直接對應,用於二進位資料(鈴聲、圖片)。最大容量 140 位元組。
每字元轉為 16-bit Unicode(ISO/IEC 10646)。Windows 下可呼叫 API 簡化實作。最大容量 70 字元(含中文)。
// UCS2 編碼 (Windows API)
int gsmEncodeUcs2(const char* pSrc, unsigned char* pDst, int nSrcLength) {
WCHAR wchar[128];
int nDstLength = MultiByteToWideChar(CP_ACP, 0, pSrc, nSrcLength, wchar, 128);
for (int i = 0; i < nDstLength; i++) {
*pDst++ = wchar[i] >> 8; // 高位元組
*pDst++ = wchar[i] & 0xFF; // 低位元組
}
return nDstLength * 2;
}
// UCS2 解碼
int gsmDecodeUcs2(const unsigned char* pSrc, char* pDst, int nSrcLength) {
WCHAR wchar[128];
for (int i = 0; i < nSrcLength/2; i++) {
wchar[i] = *pSrc++ << 8;
wchar[i] |= *pSrc++;
}
return WideCharToMultiByte(CP_ACP, 0, wchar, nSrcLength/2, pDst, 160, NULL, NULL);
}
PDU 字串為十六進制 ASCII,需與位元組陣列互轉。避免 sscanf/sprintf 以利嵌入式系統移植:
// "C8329BFD0E01" → {0xC8, 0x32, 0x9B, 0xFD, 0x0E, 0x01}
int gsmString2Bytes(const char* pSrc, unsigned char* pDst, int nSrcLength) {
for (int i = 0; i < nSrcLength; i++) {
if (*pSrc >= '0' && *pSrc <= '9') *pDst = (*pSrc - '0') << 4;
else *pDst = (*pSrc - 'A' + 10) << 4;
pSrc++;
if (*pSrc >= '0' && *pSrc <= '9') *pDst |= *pSrc - '0';
else *pDst |= *pSrc - 'A' + 10;
pSrc++; pDst++;
}
return nSrcLength / 2;
}
// {0xC8, 0x32, 0x9B, 0xFD, 0x0E, 0x01} → "C8329BFD0E01"
int gsmBytes2String(const unsigned char* pSrc, char* pDst, int nSrcLength) {
const char tab[] = "0123456789ABCDEF";
for (int i = 0; i < nSrcLength; i++) {
*pDst++ = tab[*pSrc >> 4];
*pDst++ = tab[*pSrc & 0x0F];
pSrc++;
}
*pDst = '\0'; return nSrcLength * 2;
}
GSM 03.42 壓縮演鮮少見實際應用,此不贅述。下篇將介紹完整 PDU 封裝、AT 指令流程與序列埠通訊實作。