前面數次連載我們以較長的篇幅講解了串口通信的硬件原理、DOS平台控制以及基於WIN32 API、控件和第三方類的串口編程。作為本系列文章的最後一次連載,本章將給出一個典型的 應用實例:西門子短信服務模塊TC35的串口控制。
1.短信控制終端
作為短信 (Short Message Service,SMS)一族,想必你有這樣的體會:用手機編輯短信息十分不便、 容易出錯,而且修改費時,若能用計算機來收發短信則方便許多。注意,本文所說的用計算 機收發短信並不是說通過"網易短信王"等方式在Internet上收發短信,而是直接 用計算機控制運行了GSM通信系統的短信終端進行收發,因而其收發短信的原理與手機是本質 相同的。
實際上,一大堆的垃圾短信也是采用這種短信終端發出來的!
我們 來介紹一款GSM模塊,它就是西門子公司的TC35,它由GSM基帶處理器、電源專用集成電路、 射頻電路和閃速存儲器等部分組成,負責處理GSM蜂窩設備中的音頻、數據和信號,內嵌的軟 件部分執行應用接口和所有GSM協議棧的功能。TC35支持中文短信息,工作在EGSM900和 GSM1800雙頻段,電源范圍為3.3~5.5V,可傳輸語音和數據信號,消耗功率在EGSM900(4類)和 GSM1800(1類)分別為2W和1W,通過接口連接器和天線連接器分別連接SIM卡讀卡器和天線。 TC35的數據接口(CMOS電平)通過AT命令可雙向傳輸指令和數據,可選波特率為 300bit/s~115kbit/s,自動波特率為1.2k~115kbit/s。它支持文本和PDU格式的,可通過AT命 令或關斷信號實現重啟和故障恢復。
我們需要利用以TC35模塊為主的硬件組成一個 TC35終端設備,並與電腦通過RS-232C串口相連,並自行編制在PC上運行的短信息收發軟件, 就可以組成一個短信收發系統。TC35終端電路如下圖所示:
TC35的控制主要 包含如下幾類指令:
(1)初始化指令
設置短消息發送格式 AT+CMGF=1<CR>,設置1代表PDU模式,<CR>是回車符號,也就是0x0d,指令正確 則模塊返回<CRLF>OK<CRLF>,<CRLF>是回車換行符號。
(2)設 置/讀取短消息中心
短消息中心號碼由移動運營商提供。
設置短消息中心的指 令格式為:
AT+CSCA=″+8613800531500″(短消息中心) <CR>
設置正確則模塊返回<CRLF>OK<CRLF>。
讀取短消息 服務中心則使用命令:
AT+CSCA=?<CR>
TC35模塊應該返回:
<CRLF>+CSCA:″8613800531500″<CRLF>。
(3)設 置短消息到達自動提示
設置短消息到達自動提示的指令格式為:
AT+CNMI=1,1,0,0,1<CR>
設置正確則TC35模塊返回:
<CRLF>OK<CRLF>。
設置此命令可使模塊在短消息到達後向串口 發送指令:
<CRLF>+CMTI:″SM″,INDEX(信息存儲位置) <CRLF>。
通過TC35發送短消息的方法為:
PC上的控制軟件按照PDU的格 式發送和接收數據,短消息的內容可以是中文或者其他字符。在PDU模式,如果發送短消息, 則首先發送短消息數據的長度:
AT+CMGS=<length><CR>
等待 TC35模塊返回ASCII字符">",則可以將PDU數據輸入,PDU數據以<Z>(也 就是0x1a)作為結束符。短消息發送成功,模塊返回:
<CRLF>OK<CRLF>
通過TC35接收短消息的方法為:
短消息 到來後,串口上會接收到指令
<CRLF>+CMTI:″SM″,INDEX(信息存 儲位置)<CRLF>
PC上的控制軟件通過讀取PDU數據的AT命令
AT+CMGR=INDEX<CRLF>
將TC35模塊中PDU格式的短消息內容讀出。如果 用+CMGL代替+CMGR,則可一次性讀出全部短消息。
通過TC35刪除短消息的方法為:
PC上的控制軟件收到一條短消息並處理後,需要將其在SIM卡上刪除,以防止SIM卡飽 和。刪除短消息的指令為:
AT+CMGD=INDEX<CR>
刪除後模塊返回
<CRLF>OK<CRLF>
2.程序實例
由於本文的宗旨在於講解串 口通信,因此,我們屏蔽圖形用戶界面的細節,制作一個簡單的短信收發軟件,它包含了控 制短信終端的所有串口通信內容。實際上,一個理想的短信收發軟件的界面應類似於Outlook 或Foxmail,包含收件箱、發件箱、已發送短信箱等內容,但是這些東西都與我們要介紹的串 口通信無關,因此,下面的軟件界面雖"敗絮其外",但仍可稱得上"金玉其 中":
關於界面上控件的描述如下:
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
對話框類的消息映 射為:
BEGIN_MESSAGE_MAP(CSMSControlDlg, CDialog)
//{{AFX_MSG_MAP (CSMSControlDlg)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_CLEAR_BUTTON, OnClearButton)
ON_BN_CLICKED(IDC_SEND_BUTTON, OnSendButton)
ON_BN_CLICKED (IDC_RECV_BUTTON, OnRecvButton)
ON_BN_CLICKED(IDC_DELETEALL_BUTTON, OnDeleteallButton)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
感謝 《通過串口收發短消息》一文的作者bhw98,他為我們編寫了數個獨立於操作系統平台的C函 數,使得我們可以在應用程序中直接對這些函數進行調用。在本控制軟件中,也對這些函數 進行了充分利用。
下面是對本例程軟件的主要數據結構和核心函數的介紹:
數據結構
// 用戶信息編碼方式
#define GSM_7BIT 0
#define GSM_8BIT 4
#define GSM_UCS2 8
// 短消息參數結構,編碼/解碼共用
// 其中,字符串以0結尾
typedef struct
{
char SCA[16]; // 短消息服務 中心號碼(SMSC地址)
char TPA[16]; // 目標號碼或回復號碼(TP-DA或TP-RA)
char TP_PID; // 用戶信息協議標識(TP-PID)
char TP_DCS; // 用戶信息編碼方 式(TP-DCS)
char TP_SCTS[16]; // 服務時間戳字符串(TP_SCTS), 接收時用到
char TP_UD[161]; // 原始用戶信息(編碼前或解碼後的TP-UD)
char index; // 短消息序號,在讀取時用到
} SM_PARAM;
發送短消息
發送按鈕 對應的函數為CSMSControlDlg::OnSendButton,它讀取用戶輸出並根據目標電話號碼和短信 息內容形成SM_PARAM(源PDU參數)的內容,接著進行發送:
void CSMSControlDlg::OnSendButton()
{
// TODO: Add your control notification handler code here
//獲得用戶輸入
CString desPhoneNum;
CString smsContent;
GetDlgItemText (IDC_PHONENUM_EDIT,desPhoneNum);
GetDlgItemText (IDC_SMSCONTENT_EDIT,smsContent);
//填充SM_PARAM結構體內容
SM_PARAM smParam;
smParam = CreateSMPARAMStruct(desPhoneNum,smsContent);
// 發送短信息
gsmSendMessage(smParam);
}
其中調用的 gsmSendMessage函數體現了串口通信的核心內容,它按照第1節闡述的GSM模塊發送短消息的 串口控制流程進行短信的發送:
BOOL gsmSendMessage(const SM_PARAM *pSrc // pSrc: 源PDU參數指針) { int nPduLength; // PDU串長度 unsigned char nSmscLength; // SMSC串長度 int nLength; // 串口收到的數據長度 char cmd[16]; // 命令串 char pdu[512]; // PDU串 char ans[128]; // 應答串 nPduLength = gsmEncodePdu(pSrc, pdu); // 根據PDU參數,編碼PDU串 strcat(pdu, "\x01a"); // 以Ctrl-Z結束 gsmString2Bytes(pdu, &nSmscLength, 2); // 取PDU串中的SMSC信息長度 nSmscLength++; // 加上長度字節本身 // 命令中的長度,不包括SMSC信息長度,以數據字節計 sprintf(cmd, "AT+CMGS=%d\r", nPduLength / 2-nSmscLength); // 生成命令 WriteComm(cmd, strlen(cmd)); // 先輸出命令串 nLength = ReadComm(ans, 128); // 讀應答數據 // 根據能否找到"\r\n> "決定成功與否 if (nLength == 4 && strncmp(ans, "\r\n> ", 4) == 0) { WriteComm(pdu, strlen(pdu)); // 得到肯定回答,繼續輸出PDU串 nLength = ReadComm(ans, 128); // 讀應答數據 // 根據能否找到"+CMS ERROR"決定成功與否 if (nLength > 0 && strncmp(ans, "+CMS ERROR", 10) != 0) { return TRUE; } } return FALSE; }
讀取短消息
點擊"接收"按鈕會通過gsmReadMessage函 數的調用獲得所有短消息,最後在列表控件中顯示所有短信:
void CSMSControlDlg::OnRecvButton()
{
// TODO: Add your control notification handler code here
SM_PARAM smParam[100];//短信緩沖區
int smsNum;//短信條數
smsNum = gsmReadMessage(smParam);//讀取短信
//顯示短信
for(int i=0;i<smsNum;i++)
{
m_recvlist.AddString(CString(smsNum[i].TPA)+smsNum[i].TP_UD);
}
}
其中調用的gsmReadMessage函數完成最核心的短信接收功能,它按照第1節闡 述的GSM模塊接收短消息的串口控制流程進行短信的接收:
// 參數:pMsg 短消息 緩沖區,必須足夠大
// 返回:短消息條數
int gsmReadMessage(SM_PARAM* pMsg)
{
int nLength; // 串口收到的數據長度
int nMsg; // 短消息 計數值
char* ptr; // 內部用的數據指針
char cmd[16]; // 命令串
char ans[1024]; // 應答串
nMsg = 0;
ptr = ans;
sprintf (cmd, "AT+CMGL\r"); // 生成命令,用+CMGL可一次性讀出全部短消息
WriteComm(cmd, strlen(cmd)); // 輸出命令串
nLength = ReadComm (ans, 1024); // 讀應答數據
// 根據能否找到"+CMS ERROR"決定成功與 否
if(nLength > 0 && strncmp(ans, "+CMS ERROR", 10) != 0)
{
// 循環讀取每一條短消息, 以"+CMGL:"開頭
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++; // 短消息計數加1
}
}
return nMsg;
}
刪除短消息
我們可以在讀取完所有短信息後調用 gsmDeleteMessage函數在GSM模塊上刪除那些已經被接收到PC上的短信息,它按照第1節闡述 的GSM模塊刪除短消息的串口控制流程進行短信的刪除:
// index: 短消息序號, 從1開始
BOOL gsmDeleteMessage(const int index)
{
int nLength; // 串口收到的數據長度
char cmd[16]; // 命令串
char ans[128]; // 應答串
sprintf(cmd, "AT+CMGD=%d\r", index); // 生成命令
// 輸出命令串
WriteComm(cmd, strlen(cmd));
// 讀應答數據
nLength = ReadComm(ans, 128);
// 根據能否找到"+CMS ERROR"決定成 功與否
if (nLength > 0 && strncmp(ans, "+CMS ERROR", 10) != 0)
{
return TRUE;
}
return FALSE;
}
在PC控制軟件的短信列表框中刪除所有短消息的"清空"按鈕函數 為:
void CSMSControlDlg::OnDeleteallButton()
{
// TODO: Add your control notification handler code here
m_recvlist.ResetContent();
}
設置/讀/寫串口
在應用程序啟動與退出及gsmSendMessage、 gsmReadMessage和gsmDeleteMessage函數中廣泛使用的串口相關函數用WIN32 API實現:
// 串口設備句柄
HANDLE hComm;
// 打開串口
// pPort: 串口 名稱或設備路徑,可用"COM1"或"\\.\COM1"兩種方式,建議用後者
// nBaudRate: 波特率
// nParity: 奇偶校驗
// nByteSize: 數據字節寬度
// nStopBits: 停止位
BOOL OpenComm(const char *pPort, int nBaudRate, int nParity, int nByteSize, int
nStopBits)
{
DCB dcb; // 串口控 制塊
COMMTIMEOUTS timeouts =
{
// 串口超時控制參數
100, // 讀字符間隔超時
編/解碼GSM短消息
陷於本文的篇幅,這裡只給出 編解碼函數的原型,具體請參看GSM標准及《通過串口收發短消息》一文。
// UCS2編碼 返回: 目標編碼串長度
int gsmEncodeUcs2(const char *pSrc, // 源字符 串指針
unsigned char *pDst, // pDst: 目標編碼串指針
int nSrcLength // nSrcLength: 源字符串長度
);
// UCS2解碼 返回: 目標字符串長度
int gsmDecodeUcs2(const unsigned char *pSrc, //源編碼串指針
char *pDst, // pDst: 目標字符串指針
int nSrcLength // nSrcLength: 源編碼串長度
);
//可打印字符串轉換為字節數據 返回: 目標數據長度
//如: "C8329BFD0E01" --> {0xC8, 0x32, 0x9B, 0xFD, 0x0E, 0x01}
int gsmString2Bytes(const char *pSrc, // pSrc: 源字符串指針
unsigned char *pDst, // pDst: 目標數據指針
int nSrcLength // nSrcLength: 源字符串長度
);
// 字節數據轉換為可打印字符串 返回: 目標字符串長度
// 如:{0xC8, 0x32, 0x9B, 0xFD, 0x0E, 0x01} --> "C8329BFD0E01"
int gsmBytes2String (const unsigned char *pSrc, // pSrc: 源數據指針
char *pDst, // pDst: 目標字 符串指針
int nSrcLength // nSrcLength: 源數據長度
);
3.總 結
串口編程的核心在於串口通信方式(發送、接收和握手)的控制,而具體的應用領 域反而是次要的。掌握了根本的原理,就可以靈活地將其應用於任意領域,綜合實例中的例 子"短信控制終端"只是冰山一角。