1. 引言
用過幾款GPRS模塊,也從淘寶上買過多個GPRS模塊,一般的都會送一個驅動程序和使用demo,但是代碼質量都較低。
回頭看了下幾年前使用的GPRS代碼,從今天的角度來看,也就是買模塊贈送一個免費demo的那種水平,甚是汗顏。
GPRS模塊驅動主要是串口驅動,其本質是字符串處理,本文就從對比下幾種常見的驅動方式。
2. 版本1--初學者的驅動
思路:
1. 串口接收使用中斷,收到數據放到全局buffer。
2. 發送前清空接收buffer。
3. 拼接字符串,然后從串口發送出去。
4. 設定一個等待時間,然后while(1)不停的查看接收buffer里面是否有需要的字符串出現,即是否得到需要的響應。
5. 初始化過程使用一個簡單的狀態機輪轉,一步通過再進行下一步。
下面是一個我曾經用過的例子,問題很明顯:
1. 難維護,函數耦合度太高,簡單的堆功能,功能模塊沒有划分。
2. 低效,發送需要CPU停下來一個一個字符的發送,接收還要延時一段時間等待GPRS模塊回復足夠多的數據。
3. 接收buffer只是簡單的共享全局變量,沒有雙buffer切換也沒有讀寫互斥。
比如每次發送前清空buffer然后發送命令,用來判別此次接收都是對本次發送的命令的響應。
4. 不能精細控制,AT指令響應檢查全部放到一個函數里面處理,必然造成有些AT響應的回復無法區分對應哪個指令。
網絡連接的驅動:
...// 這一句是嵌在應用戶代碼中
while(gprs_start==0) //未連接 { gprs_start=gprs_conduct(); } ......
//--------------------------------------------------------- //函數名稱:void gprs_connect(void) //函數功能:配置socket //輸入參數:無 //返回參數:返回0 表示還沒完成注冊,返回1表示完成所有連接 //--------------------------------------------------------- uint8_t gprs_connect(uint8_t *ascData, uint8_t len) { uint8_t num,GPRS_Connect=0; char strBuf[20]; memset(strBuf,0x00,sizeof(strBuf)); if (gprs_mgr.stat == GPRS_GET_CSQ) // 查詢信號強度 { if (gprs_get_csq() > 15) gprs_mgr.stat = GPRS_WAIT_REGNET; delay_ms(1000); printf("gprs_mgr.stat = GPRS_GET_CSQ"); } else if (gprs_mgr.stat == GPRS_WAIT_REGNET) // 等待注冊到網絡 { if (!gprs_reg_network()) gprs_mgr.stat = GPRS_CONFIG_PARA; delay_ms(2000); printf("gprs_mgr.stat = GPRS_WAIT_REGNET"); } else if (gprs_mgr.stat == GPRS_CONFIG_PARA) // 配置通信參數 { if (!gprs_config_para()) gprs_mgr.stat = GPRS_CONFIG_SOCKET; delay_ms(1000); printf("gprs_mgr.stat = GPRS_CONFIG_PARA;"); } else if (gprs_mgr.stat == GPRS_CONFIG_SOCKET) // 配置socket { if (!gprs_config_socket()) //0 sucess gprs_mgr.stat = GPRS_DATA_RW; else //不成功可能由於服務器端口被占用, { gprs_send_cmd("AT%IPCLOSE=1\r\n"); for(num=0;num<5;num++) { delay_ms(1000); } } delay_ms(1000); printf("gprs_mgr.stat = GPRS_CONFIG_SOCKET;"); } else if (gprs_mgr.stat == GPRS_DATA_RW) // 數據讀寫 { GPRS_Connect=1; sprintf(strBuf,"AT+CIPSEND=%d\r\n",len+4); gprs_send_cmd(strBuf); gprs_write_data(ascData); delay_ms(1000); printf("GPRS_DATA_RW;"); gprs_send_cmd("AT+CIPCLOSE\r\n"); printf("disconnect grps;"); gprs_mgr.stat = GPRS_GET_CSQ; } return GPRS_Connect; }
發送函數,串口輸出加上查詢式解析:
//--------------------------------------------------------- // 函數名稱:uint8 gprs_send_cmd(char* pcmd) // 函數功能:gprs命令字發送函數 // 輸入參數: pcmd,要發送的命令 // 返回參數: // 0 ,命令發送成功 // 1 ,命令發送失敗 //--------------------------------------------------------- uint8_t gprs_send_cmd(char* pcmd) { uint16_t i; uint8_t ret=0, *GSM_ReturnInfo; memset(GSM_info, 0, sizeof(GSM_info)); // 清除串口緩沖區 GSM_Info_CNT=0; // 清除串口接收計數 debug_print(pcmd); //發送的命令,調試輸出 while(*pcmd) // 發送命令 { while(USART_GetFlagStatus(GPRS_USART, USART_FLAG_TXE)==0); USART_SendData(GPRS_USART, *pcmd++); } delay_ms(1000); GSM_ReturnInfo=GPRS_Get_Info(); for (i = 0; i < 15; i++) //15s 等待 { delay_ms(500); if (strstr(GSM_ReturnInfo, "OK")) // 命令發送成功 { ret = 0; break; } else if (strstr(GSM_ReturnInfo, "CONNECT")) { ret = 0; break; } else if (strstr(GSM_ReturnInfo, "ERROR")) // 命令發送失敗 { ret = 1; break; } else ret = 1; delay_ms(500); } debug_print(GSM_ReturnInfo); // 打印調試信息 return ret; }
3. 版本2--有模塊化思想的驅動
大體流程和第一種差別不大,但是在幾個關鍵點上有巨大改進,比如函數的模塊化和中斷的使用。
1. 發送和接收都用中斷提高效率,不再使用查詢方式。
2. 拼湊發送字符串處理和串口數據發送過程分開。
3. 初步的異常處理,如斷線重連、重啟等。
比上一版本進化很多,但是也有問題:
1. 模塊已經划分,但是在邏輯層次上區分不明顯,如下面例子中的發送的AT指令的響應處理,就和發送函數混在一起。
2.全局變量問題,比如記錄GPRS模塊當前狀態標識,可以多處進行修改。
比較獨立的功能做一定的提取,比如注冊網絡、SIM卡檢查等功能函數封裝起來。
uint8_t gprs_reg_network(void) { uint8_t ret, *uart_buf; ret = gprs_send_cmd("AT+CGREG?\r\n"); if (ret == 0) //命令發送成功 { uart_buf = get_gprs_rsp(); ret = 1; if (strstr(uart_buf, "+CGREG: 0,5")) // 已注冊,本地網 ret = 0; if (strstr(uart_buf, "+CGREG: 1,5")) // 已注冊,本地網 ret = 0; if (strstr(uart_buf, "+CGREG: 0,1")) // 已注冊,漫游 ret = 0; if (strstr(uart_buf, "+CGREG: 1,1")) // 已注冊,漫游 ret = 0; return ret; } else return 1; }
或者接收響應的buffer不使用全局變量,而在發送函數參數中直接傳入接收數組指針。
int gprs_check_sim(void) { int err = 0;
int retry = 0; char rsp_buf[100]; do{ err = gprs_send_atcmd("AT+CPIN?\r\n",rsp_buf,"+CPIN:"); if(strstr(rsp_buf,"+CPIN: READY")== 0){ GPRS_Status = IN_NO_SIM; break ; }if(retry++ > 2){ err = E_TIMEOUT ; break ; } }while(err != E_OK); return err ; }
4. 版本3--按邏輯層次划分功能
分兩個層次來實現需求,先是邏輯層次划分功能,然后在具體是實現層次按照功能單一原則編碼。
該方法可以用在產品中,驅動代碼的思路清晰,高效且易維護。
1. 使用RTOS來,提升CPU利用率,尤其是等待AT指令回復的過程中,系統可以執行其他任務。
2. GPRS操作的本質是寫字符串(發AT指令),然后讀回復的字符串(讀指令響應),那么可以從這個角度來設計驅動。
3. 屏蔽硬件細節,在寫GPRS驅動和邏輯處理的過程中,硬件讀寫都抽象成一個字符處理函數。
例1:查詢SIM卡,發送AT指令,然后等待接收響應字符串。
函數接口就負責填充期待的字符串,如果指定時間內沒等到字符串出現就認為出錯,具體怎么發出去怎么收到回復都是更加底層的處理。
具體的響應由SIM800_WaitResponse函數來處理,該函數自動匹配指定字符串,參數500是超時時間,如果匹配成功會提前退出,否則等待500ms然后回復超時。
由於使用的Free RTOS,那么該函數不是阻塞性的,不會影響CPU執行其他的任務。如果模塊很快響應了指令,那么還可以提前結束超時等待。
/*******************************************************************************
* Function Name : SIM800_Check_SIM
* Description : None
* Input : None
* Output : None
* Return : 1-OK, 0-NG
* Attention : None
*******************************************************************************/
uint8_t SIM800_Check_SIM(void)
{
uint8_t rtn = 0;
SIM800_SendATCmd("AT+CPIN?\r\n"); if (SIM800_WaitResponse("+CPIN: READY", 500)){ rtn = 1;
}
return rtn;
}
例2:注冊GSM網絡,發送AT指令,然后等待接收響應字符串。
有些指令回復參數種類較多,如果寫成上面的形式可能比較臃腫,可以把接收到的數據先放到buffer,然后從中搜索需要的字符。
SIM800_ReadResponse完成這個功能,但該函數要一直等待直至最大超時時間。
/******************************************************************************* * Function Name : SIM800_GsmCheck * Description : 檢查是否注冊到GSM網絡 * Input : None * Output : None * Return : 1—OK, 0-NG * Attention : None *******************************************************************************/ uint8_t SIM800_GsmCheck(void) { uint8_t rtn = 0; SIM800_SendATCmd("AT+CREG?\r\n"); SIM800_ReadResponse(gprs_rsp_buffer, sizeof(gprs_rsp_buffer), 500); if (strstr(gprs_rsp_buffer, "+CREG: 0,1") != NULL) { rtn = 1; } else if (strstr(gprs_rsp_buffer, "+CREG: 0,5") != NULL) { rtn = 1; } else rtn = 0; vTaskDelay(200); return rtn; }
例3:AT指令的發送,不需要等待硬件的響應,啟動硬件發送標識即可,具體發送由中斷或DMA去操作,代碼更加高效。
剩下的發送和接收都是CPU硬件操作,在這一層次CPU並不識別數據的含義,僅是把數據從串口輸出或讀入。
/******************************************************************************* * Function Name : SIM800_SendATCmd * Description : None * Input : _ucaBuf * Output : None * Return : None * Attention : 向GSM模塊發送AT命令。 命令需要自己加上<CR>字符 *******************************************************************************/ void SIM800_SendATCmd(const uint8_t *_ucaBuf) { SIM800_PrintRxData(_ucaBuf); gprs_msg.msgPtr = (uint8_t *)(_ucaBuf); gprs_msg.maxLen = strlen(_ucaBuf); gprs_msg.length = 0; Gprs_UART_TX(ON); }
/*******************************************************************************
* Function Name : Gprs_UART_TX_Mode
* Description : Configure uart TX irq on/off
* Input : None
* Output : None
* Return : None
* Attention : 1=ON, 0=OFF.
*******************************************************************************/
void Gprs_UART_TX(uint8_t flag)
{
if(ON == flag)
{
UART_Tx_IRQ_Enable(TRUE);
}
else if(OFF == flag)
{
UART_Tx_IRQ_Enable(FALSE);
}
}
例4:如果模塊已經連接到服務器,那么需要應用數據的發送。
常見的過程分3步:
1)應用數據預處理打包;
2)GPRS模塊發送數據可能需要一個特殊的指令來啟動,作用是告訴模塊,下面發過來的是用戶數據,不是控制字了;
3)啟動數據包發送(實際是初始化發送過程邏輯控制相關的標志和啟動硬件發送標志)。
下面驅動函數處理了用戶數據的發送,在發送AT+CIPSEND后,2s內收到">" 回復就可以開始發送數據。
/******************************************************************************* * Function Name : SIM800_Send * Description : None * Input : gprs_package * Output : None * Return : None * Attention : None *******************************************************************************/ uint8_t SIM800_Send(uint8_t *gprs_package) { uint8_t cmd[20]="AT+CIPSEND\r\n"; //啟動數據發送AT指令 uint8_t write_len; uint8_t rtn = 0; write_len = strlen(gprs_package); if(write_len>1500){ DEBUG_PrintRxData("Data length error"); return 0; } SIM800_SendATCmd(cmd); if (SIM800_WaitResponse(">", 2000) == 0) // 啟動發送應用數據失敗 { DEBUG_PrintRxData("CIPSEND failed"); return 0; } SIM800_SendPackage(gprs_package, write_len);// 開始接收收據 if (SIM800_WaitResponse("SEND OK", 5000)){ DEBUG_PrintRxData("get SEND OK"); return 1; } else{ DEBUG_PrintRxData("not get SEND OK"); return 0; } } /******************************************************************************* * Function Name : SIM800_SendPackage * Description : None * Input : 待發送的數據的指針和數據長度 * Output : None * Return : None * Attention : None *******************************************************************************/ void SIM800_SendPackage(const uint8_t *_ucaBuf, uint8_t len) { SIM800_PrintRxData(_ucaBuf); gprs_msg.msgPtr = (uint8_t *)(_ucaBuf); gprs_msg.maxLen = len; gprs_msg.length = 0; Gprs_UART_TX(ON); }