在前幾篇連載中,我們詳細探討了串口通訊的硬體原理、DOS 平台控制,以及基於 WIN32 API、ActiveX 控制項與第三方類別的串口程式設計技巧。作為本系列的最後一篇,我們將透過一個典型的實戰案例——西門子短訊服務模組 TC35 的串口控制,將理論知識落地為具體應用。
對於頻繁使用簡訊(SMS)的用戶而言,手機鍵盤編輯訊息不僅繁瑣,還容易出錯且難以修改。若能透過電腦直接收發簡訊,將大幅提升效率。值得注意的是,此處所指的「電腦收發簡訊」並非透過網路平台(如早期的「網易短訊王」),而是直接控制運行 GSM 通訊系統的短訊終端,其收發原理與手機完全一致。事實上,大量垃圾簡訊正是利用此類終端設備批量發送的。
TC35 是西門子推出的 GSM 模組,集成了 GSM 基帶處理器、電源專用 IC、射頻電路與快閃記憶體等核心部件,負責處理音訊、數據與信令,內建軟體執行應用介面與所有 GSM 協定堆疊功能。其主要規格如下:

TC35 的控制主要包含以下幾類指令:
設定簡訊發送格式:AT+CMGF=1<CR>(1 代表 PDU 模式,<CR> 為回車符 0x0D)。指令正確時模組返回 <CRLF>OK<CRLF>。
簡訊中心號碼由移動運營商提供。設定指令格式:AT+CSCA="+8613800531500"<CR>;讀取指令:AT+CSCA=?<CR>,模組返回 <CRLF>+CSCA:"8613800531500"<CRLF>。
指令格式:AT+CNMI=1,1,0,0,1<CR>。設定成功後,簡訊到達時模組會主動向串口發送:<CRLF>+CMTI:"SM",INDEX<CRLF>(INDEX 為訊息存儲位置)。
AT+CMGS=<length><CR>,其中 length 為 PDU 數據長度(不含 SMSC 資訊)。> (提示符)。<Z> (0x1A, Ctrl+Z) 作為結束符。<CRLF>OK<CRLF>。<CRLF>+CMTI:"SM",INDEX<CRLF>。AT+CMGR=INDEX<CRLF> 讀取單條 PDU 簡訊,或用 AT+CMGL<CRLF> 一次性讀取所有簡訊。為防止 SIM 卡存儲飽和,處理完畢後應刪除:AT+CMGD=INDEX<CR>,成功返回 <CRLF>OK<CRLF>。
本範例專注於串口通訊核心邏輯,界面簡潔但功能完整,包含發送、接收、清空等操作。界面資源定義如下:

BEGIN
EDITTEXT IDC_SMSCONTENT_EDIT,39,61,242,38,ES_AUTOHSCROLL
PUSHBUTTON "發送 ",IDC_SEND_BUTTON,316,80,45,18
GROUPBOX "接收短消息 ",IDC_STATIC,28,124,361,167
LTEXT "對方手機號 ",IDC_STATIC,41,35,42,11
EDITTEXT IDC_PHONENUM_EDIT,88,30,192,17,ES_AUTOHSCROLL
PUSHBUTTON "清除 ",IDC_CLEAR_BUTTON,316,30,45,18
GROUPBOX "發送短消息 ",IDC_STATIC,29,19,361,95
LISTBOX IDC_RECVSMS_LIST,43,137,331,127,LBS_SORT | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP
PUSHBUTTON "接收 ",IDC_RECV_BUTTON,77,269,55,16
PUSHBUTTON "清空 ",IDC_DELETEALL_BUTTON,273,268,45,14
END
// 用戶資料編碼方式
#define GSM_7BIT 0
#define GSM_8BIT 4
#define GSM_UCS2 8
// 簡訊參數結構 (編碼/解碼共用)
typedef struct {
char SCA[16]; // 簡訊服務中心號碼
char TPA[16]; // 目標號碼或回覆號碼
char TP_PID; // 協定識別碼
char TP_DCS; // 資料編碼方案
char TP_SCTS[16]; // 服務中心時間戳 (接收時用)
char TP_UD[161]; // 用戶資料 (編碼前/解碼後)
char index; // 簡訊序號 (讀取時用)
} SM_PARAM;
gsmSendMessage該函數完整實現了 PDU 模式下的發送流程:
BOOL gsmSendMessage(const SM_PARAM *pSrc) {
int nPduLength, nLength;
unsigned char nSmscLength;
char cmd[16], pdu[512], ans[128];
nPduLength = gsmEncodePdu(pSrc, pdu); // 編碼 PDU 串
strcat(pdu, "\x1A"); // Ctrl-Z 結束符
gsmString2Bytes(pdu, &nSmscLength, 2); // 取 SMSC 長度
nSmscLength++; // 加上長度位元組本身
// 發送 AT+CMGS 指令 (長度不含 SMSC)
sprintf(cmd, "AT+CMGS=%d\r", nPduLength / 2 - nSmscLength);
WriteComm(cmd, strlen(cmd));
nLength = ReadComm(ans, 128);
// 檢查是否收到 ">" 提示符
if (nLength == 4 && strncmp(ans, "\r\n> ", 4) == 0) {
WriteComm(pdu, strlen(pdu)); // 發送 PDU 數據
nLength = ReadComm(ans, 128);
// 檢查是否有錯誤回應
if (nLength > 0 && strncmp(ans, "+CMS ERROR", 10) != 0) {
return TRUE;
}
}
return FALSE;
}
gsmReadMessage使用 AT+CMGL 一次性讀取所有簡訊,並逐條解碼:
int gsmReadMessage(SM_PARAM* pMsg) {
int nLength, nMsg = 0;
char *ptr, cmd[16], ans[1024];
sprintf(cmd, "AT+CMGL\r");
WriteComm(cmd, strlen(cmd));
nLength = ReadComm(ans, 1024);
if (nLength > 0 && strncmp(ans, "+CMS ERROR", 10) != 0) {
ptr = ans;
while ((ptr = strstr(ptr, "+CMGL:")) != NULL) {
ptr += 6; // 跳過 "+CMGL:"
sscanf(ptr, "%d", &pMsg->index); // 讀取序號
ptr = strstr(ptr, "\r\n"); // 找下一行
ptr += 2; // 跳過 "\r\n"
gsmDecodePdu(ptr, pMsg); // PDU 解碼
pMsg++; nMsg++;
}
}
return nMsg;
}
gsmDeleteMessageBOOL gsmDeleteMessage(const int index) {
int nLength;
char cmd[16], ans[128];
sprintf(cmd, "AT+CMGD=%d\r", index);
WriteComm(cmd, strlen(cmd));
nLength = ReadComm(ans, 128);
return (nLength > 0 && strncmp(ans, "+CMS ERROR", 10) != 0);
}
應用程式啟動/退出及收發流程中廣泛使用的串口函數:
HANDLE hComm;
BOOL OpenComm(const char *pPort, int nBaudRate, int nParity, int nByteSize, int nStopBits) {
DCB dcb;
COMMTIMEOUTS timeouts = {100, 0, 0, 0, 0}; // 讀字元間隔超時 100ms
// ... 具體實現略 ...
}
受限篇幅,僅列出關鍵函數原型,完整實現請參考 GSM 標準文檔:
int gsmEncodeUcs2(const char *pSrc, unsigned char *pDst, int nSrcLength);
int gsmDecodeUcs2(const unsigned char *pSrc, char *pDst, int nSrcLength);
int gsmString2Bytes(const char *pSrc, unsigned char *pDst, int nSrcLength);
int gsmBytes2String(const unsigned char *pSrc, char *pDst, int nSrcLength);
串口程式設計的核心在於通訊控制邏輯(發送、接收、握手)的掌握,具體應用領域反而是次要的。一旦掌握了底層原理,即可靈活應用於各類領域,本文的「短訊控制終端」僅是冰山一角。希望這個完整的實戰案例能幫助讀者將理論轉化為生產力。