記錄一下 CAT1 模塊EC800 HTTP 使用后續遇到的問題 by 矜辰所致
目錄
- 前言
- 一、一些功能的完善
- 1.1 新的交互指令添加
- 1.2 連不上網絡處理
- 二、問題出現
- 三、分析及解決
- 3.1 定位問題
- 3.2 問題分析與解決
- 3.2.1 查看變量在內存中的位置
- 3.3 數據類型說明
- 3.3.1 常用格式化輸出符號
- 3.3.2 不同平臺數據類型定義
- 結語
前言
此前我寫過一篇文章 CAT1模塊 EC800M HTTP使用總結記錄 詳細講述了,如何使用 EC800M HTTP協議 的應用。
在后續的過程中,根據客戶的不同需求,應用有一定的改變,所以進行了一些定制的修改,同時也發現了以前留下的一個 bug , 覺得還是有必要來記錄一下,這不是也太久沒寫文章了,都要生銹了。
所以本文就繼續來說明一下CAT1模塊 EC800M HTTP 應用的后續問題吧。
我是矜辰所致,全網同名,盡量用心寫好每一系列文章,不浮夸,不將就,認真對待學知識的我們,矜辰所致,金石為開!
目錄
- 前言
- 一、一些功能的完善
- 1.1 新的交互指令添加
- 1.2 連不上網絡處理
- 二、問題出現
- 三、分析及解決
- 3.1 定位問題
- 3.2 問題分析與解決
- 3.2.1 查看變量在內存中的位置
- 3.3 數據類型說明
- 3.3.1 常用格式化輸出符號
- 3.3.2 不同平臺數據類型定義
- 結語
一、一些功能的完善
在前面的使用文章中,我們主要的目的在于介紹 EC800M HTTP 的使用,對于正式使用的產品來說,有一些問題還是需要考慮到的。
1.1 新的交互指令添加
使用 HTTP 協議,客戶端想要和服務器交換數據,也只能等到 HTTP POST 以后通過服務器返回的響應來進行交互。這個其實在上一篇文章已經講到過,所以呢,如果是后續有和服務器數據交互的需求,也只能在 HTTP POST 以后來實現,那對于我們來說,都是在讀取 HTTP 響應之后,通過 strstr
函數找到我們需要的數據進行處理,比如下面一段代碼:
...
else if(!strcmp(cmd,"AT+QHTTPREAD=2\r\n")){printf("%s\r\n", EC800_RX_BUF); //打印出響應用來參考char* position = strstr((char*)EC800_RX_BUF, "\"expectGear\":");//先找到expectGear 的位置if (position != NULL) {// Use sscanf to extract the integer value after "expectGear"sscanf(position + strlen("\"expectGear\":"), "%hd", &Http_set_mode);if((Http_set_mode > 0)&&(Http_set_mode < 5)&&(Http_set_mode != Value_Mode)) need_change = TRUE;// printf("Http_set_mode is %d\r\n",Http_set_mode);} else {printf("not found expectGear!!\r\n");}position = strstr((char*)EC800_RX_BUF, "\"voice\":");//先找到 voice 的位置if (position != NULL) {// Use sscanf to extract the integer value after "expectGear"sscanf(position + strlen("\"voice\":"), "%hhu", &Http_set_voice);// printf("Http_set_voice is %d\r\n",Http_set_voice); if(Http_set_voice)//這里要關閉聲音{Voice_close_state = TRUE;Voice_close_count = 0;}} else {}CLEAR_EC800_Buffer(EC800_RX_Data);return 0;}
...
這個問題說明一下即可,沒什么難點,可能需要注意的點就是 HTTP 的響應數據量會稍微有點多,達到幾百個字節,需要注意自己的串口緩存區的大小,這個在上一篇文章也有說明。但是這一塊的代碼有個問題確實也是本文要說明的地方,上看的代碼已經是修改過的版本。
1.2 連不上網絡處理
對于有些網絡產品,它具備本地功能,所以在實際工作的時候要根據需求來判斷它如果連不上網絡是不停聯網還是保持本地功能完善。
如果是必須要連上網絡才能運行,很簡單,可以開啟看門口,然后在配網的過程中如果失敗重試的時間長一點,那么就能夠不停的自動復位。
如果是需要保證沒有網絡也要使得本地功能正常,然后設備自動定時連接網絡,那么就需要考慮好配網失敗重試的時間,超時返回標志位,不能等到看門狗自動復位。再在程序中根據配網成功或者失敗的標志位進行判斷需不需要再次連接網絡。比如本次我們就是要修改成這種狀態。
所以我們需要在配網操作的時候做一個超時,同時還需要返回狀態值,所以我們把配網聯網的狀態定義一個結構體:
typedef struct
{uint8 Ec800_init_state; uint8 Ec800_pdp_prepare_state; uint8 Http_set_url_state;
} cat1_state_struct;extern char IMEI[15];
extern cat1_state_struct My_4g_state;
上面的 3個狀態分別對應 ec800_init
,ec800_pdp_prepare
,http_set_url
3個函數的狀態,以前函數沒有返回值,現在我們需要加上返回值:
uint8 ec800_init();
int Iot_SendCmd(const char* cmd, char* reply, int wait);
void Iot_SendNOCheck(char* cmd);uint8 ec800_pdp_prepare();
uint8 http_set_url(char *url);
void http_post_message(const char *message);
這里使用 ec800_init
作為示例看一下超時的實現:
uint8 ec800_init()
{u16 cat1_timeout = 0;while(Iot_SendCmd(AT,"OK", 200)){HAL_Delay(1);cat1_timeout ++;if(cat1_timeout >= 2000){printf("uart false\r\n");return false; }}cat1_timeout = 0;
...
上面這個原始的邏輯是有點問題的, cat1_timeout
應該是一個重試的次數,每次嘗試會等待 200ms ,然后再等待 1ms,再次嘗試,一共要嘗試 2000 次,那么不考慮程序運行指令的時間來算,都需要 201* 2000 ms 才會超時,除了時間太久了,失敗后1ms 就重試也不是那么合理。所以我們簡單修改一下:
while(Iot_SendCmd(AT,"OK", 200)){HAL_Delay(100);cat1_timeout ++;if(cat1_timeout >= 30){printf("uart false\r\n");return false; }}cat1_timeout = 0;printf("\r\nuart ok\r\n");while(Iot_SendCmd(CPIN,"READY", 200)){HAL_Delay(100);cat1_timeout ++;if(cat1_timeout >= 30){return false; }}
具體的可以根據自己看門狗的設定時間來確定每次 AT 指令的重試次數。
然后我們在具體使用的時候,先定義一個結構體變量,保存我們的聯網狀態,然后在開機的時候進行配網聯網操作,不管成功與否,我們可以得到對應的狀態,示例如下:
cat1_state_struct My_4g_state;...
//省略
...
int main(void)
{
//初始化省略MX_IWDG_Init();My_4g_state.Ec800_init_state = ec800_init();HAL_IWDG_Refresh(&hiwdg);printf("Ec800_init_state:%d\r\n",My_4g_state.Ec800_init_state);My_4g_state.Ec800_pdp_prepare_state = ec800_pdp_prepare();HAL_IWDG_Refresh(&hiwdg);printf("Ec800_pdp_prepare_state:%d\r\n",My_4g_state.Ec800_pdp_prepare_state);My_4g_state.Http_set_url_state = http_set_url(url);HAL_IWDG_Refresh(&hiwdg);printf("Http_set_url_state:%d\r\n",My_4g_state.Http_set_url_state);...//省略...//我這里使用了初始化狀態確定是否需要 postif(My_4g_state.Ec800_init_state) http_post_message(message);while(1){...//定時發送,如果網絡狀態異常,就重新連接網絡,再嘗試發送if(send_count >= 180){send_count = 0;snprintf(message, sizeof(message), pm_message, IMEI,PM10_Data,PM25_Data,PM1_Data,Value_Mode);if(My_4g_state.Ec800_init_state) {http_post_message(message);}else { //重新連接My_4g_state.Ec800_init_state = ec800_init(); HAL_IWDG_Refresh(&hiwdg);My_4g_state.Ec800_pdp_prepare_state = ec800_pdp_prepare();HAL_IWDG_Refresh(&hiwdg);My_4g_state.Http_set_url_state = http_set_url(url); HAL_IWDG_Refresh(&hiwdg); if(My_4g_state.Ec800_init_state) http_post_message(message);}}...}
}
上面是設備初始化上電后的聯網配置示例,上電先配網,然后讀取一次數據進行上傳,在主函數的 while
循環中,根據自己的需要的周期,定時的進行數據上傳,或者根據 My_4g_state.Ec800_init_state
狀態,進行重新配置網絡操作。
整體來說就是這么一個流程,不復雜,但是用起來卻出現了一個小 bug 。
二、問題出現
如果我們 SIM 卡正常,網絡信號正常,我們會經歷正常的初始化了,得到的My_4g_state
3個成員變量都為1,然后他可以正常的進行 POST 操作,但是我測試的時候發現,設備還是會定期聯網,但是奇怪的是,它能夠正常的進行 POST 操作。
確定的問題是只要運行過了http_post_message
函數 ,My_4g_state.Ec800_init_state
就會變成 0 ,如下圖:
簡單一想,在實際代碼中,http_post_message
函數里根本沒有改變這個變量的值, 那么出這種問題極大概率的就是數據溢出,最開始想的是不是串口緩存數據溢出,嘗試過加大串口緩沖區,沒有用。測試下來發現這個 bug 只會改變 1個字節,所以期間采用了一個辦法,就是在結構體成員變量上加上一個預留位置,如下圖:
typedef struct
{uint8 Reserved_state;uint8 Ec800_init_state; uint8 Ec800_pdp_prepare_state; uint8 Http_set_url_state;
} cat1_state_struct;
倒是能夠解決,程序邏輯正常的跑,但是這是治標不治本的方式,還是存在 bug 。
三、分析及解決
3.1 定位問題
還是得進一步的分析問題,于是進一步的修改了一下 http_post_message
函數,在每次操作后把My_4g_state.Ec800_init_state
值打印出來,如下 :
void http_post_message(const char *message) {int length = strlen(message);char at_post[32];u16 cat1_timeout = 0;snprintf(at_post, sizeof(at_post), "AT+QHTTPPOST=%d,%d,%d\r\n", length, 5, 10);printf("five:%d\r\n",My_4g_state.Ec800_init_state);while(Iot_SendCmd(at_post,"CONNECT", 500)){HAL_Delay(100);cat1_timeout ++;if(cat1_timeout >= 10){return; }}cat1_timeout = 0; printf("\r\nready to send post message!\r\n%s\r\n", message);printf("six:%d\r\n",My_4g_state.Ec800_init_state);while(Iot_SendCmd(message,"+QHTTPPOST:", 5000)){ // HAL_Delay(100);cat1_timeout ++;if(cat1_timeout >= 3){printf("http post wrong\r\n");return; }}cat1_timeout = 0;printf("\r\nhttp post OK\r\n");printf("seven:%d\r\n",My_4g_state.Ec800_init_state);HAL_IWDG_Refresh(&hiwdg);while(Iot_SendCmd("AT+QHTTPREAD=2\r\n","+QHTTPREAD:", 2000)){HAL_Delay(100);cat1_timeout ++;if(cat1_timeout >= 5){printf("read wrong\r\n");return; }}cat1_timeout = 0;printf("\r\nHTTPREAD OK\r\n");printf("eight:%d\r\n",My_4g_state.Ec800_init_state);
}
測試結果如下:
通過上面測試,已經可以直接定位到云運行過while(Iot_SendCmd("AT+QHTTPREAD=2\r\n","+QHTTPREAD:", 2000))
后,My_4g_state.Ec800_init_state
的值就變成了 0 ,我們看看上面這條語句會做什么工作,我只把 HTTPREAD 相關的部分代碼截取出來:
int Iot_SendCmd(const char* cmd, char* reply, int wait)
{u8 i=0;char* rss_str;int rssi,res;CLEAR_EC800_Buffer(EC800_RX_Data);Uart3_sendBuffer((u8*)cmd,strlen(cmd));/*此處串口回的不止是一幀數據,所以使用 IDLE 中斷不合適*/if ((!strcmp(reply,"+QHTTPREAD:"))||(!strcmp(reply,"+QHTTPPOST:"))){//讀取和發送的處理,直接等一段時間HAL_Delay(1000);// 500 600 800 1000 一直加大 }/*另外的設置指令大多都是等待一個 OK 返回,屬于一幀數據所以可以用 IDLE 中斷*/else{...}EC800ReceiveState = false;if (!strcmp(reply,"+CSQ")){...}else if (strstr((char*)EC800_RX_BUF, reply)){ if (!strcmp(cmd,"AT+CGSN\r\n")){}else if(!strcmp(cmd,"AT+QHTTPREAD=2\r\n")){printf("%s\r\n", EC800_RX_BUF); //打印出響應用來參考char* position = strstr((char*)EC800_RX_BUF, "\"expectGear\":");//先找到expectGear 的位置if (position != NULL) {// Use sscanf to extract the integer value after "expectGear"sscanf(position + strlen("\"expectGear\":"), "%d", &Http_set_mode);if((Http_set_mode > 0)&&(Http_set_mode < 5)&&(Http_set_mode != Value_Mode)) need_change = TRUE;} else {printf("not found expectGear!!\r\n");}CLEAR_EC800_Buffer(EC800_RX_Data);return 0;}CLEAR_EC800_Buffer(EC800_RX_Data);return 0;}return 1;
}
從上面可以看到,在這個操作中我們只會改變變量 Http_set_mode
和need_change
的值,但是 need_change
并不是每次都改變,所以基本上可以判斷是 Http_set_mode
的值變化使得My_4g_state.Ec800_init_state
變化了。
3.2 問題分析與解決
那我們前文也說過,此類問題極大概率的就是數據溢出問題,這兩個數據在內存中存放的地址應該是靠在一起的才會出現這種問題,為了更加直觀的說明這個問題,我們可以查看一下他們在內存中存放的位置。
3.2.1 查看變量在內存中的位置
如何查看變量的存放位置?那就是查看編譯過后的 .map
文件!
我們打開 .map
文件 搜索一下我們的變量,如下圖:
果然他們是緊靠著的,他們在內存中如下圖分部:
那我們回去看一下問題,Http_set_mode
我們定義的為一個字節的數據,怎么影響到了后面的數據,我們在出問題的地方有一條語句,注意看:
sscanf(position + strlen("\"expectGear\":"), "%d", &Http_set_mode);
把 expectGear
字符位置后面的數值,放到變量Http_set_mode
的地址,也就是 0x2000046d
位置處,放的數據類型為 %d
類型 !!!
%d
類型是幾個字節? 在 32 位系統中,%d
通常表示 4 字節(32 位)的有符號整數(int 類型)。
所以問題我們已經確定了,就是因為這個sscanf
函數中的 %d
導致的,比如我們收到的是一個 2 ,我們知道 STM32 為小端模式,那么操作過后我們的內存中的數據會變成如下這樣:
到這里,問題已經很明了了,我們沒有注意數據類型,導致我們改變了存放在地址 0x20000470
處的My_4g_state.Ec800_init_state
變量的值。
對于這個問題,我們只需要把 %d
改成 %hh
, 就解決了這個問題了!如下:
sscanf(position + strlen("\"expectGear\":"), "%hhu", &Http_set_mode);
3.3 數據類型說明
那既然遇到了這個問題,在解決的過程中,我也發現有一些值得說明的地方,所以接下來就順帶做做一個筆記說明把。
3.3.1 常用格式化輸出符號
首先,先來看一看我們常用的格式化輸出符號,這里只需要記住這張表格就好了:
格式符 | 含義 | 對應數據類型 | 位數(典型情況) |
---|---|---|---|
%d | 有符號十進制整數 | int | 32位(4字節) |
%u | 無符號十進制整數 | unsigned int | 32位(4字節) |
%hd | 有符號短整數 | short | 16位(2字節) |
%hu | 無符號短整數 | unsigned short | 16位(2字節) |
%ld | 有符號長整數 | long | 32/64位(平臺相關) |
%lu | 無符號長整數 | unsigned long | 32/64位(平臺相關) |
%lld | 有符號長長整數 | long long | 64位(8字節) |
%llu | 無符號長長整數 | unsigned long long | 64位(8字節) |
%hhd | 有符號單字節整數 | char | 8位(1字節) |
%hhu | 無符號單字節整數 | unsigned char | 8位(1字節) |
%f | 十進制浮點數 | float | 32位(4字節) |
%lf | 十進制雙精度浮點數 | double | 64位(8字節) |
%c | 單個字符 | char | 8位(1字節) |
%s | 字符串 | char[](以\0結尾) | 動態長度 |
%p | 指針地址 | 任意指針類型 | 32/64位(平臺相關) |
3.3.2 不同平臺數據類型定義
對于本文遇到的問題,除了上面的解決辦法,我們其實還可以修改一下Http_set_mode
的數據類型,如下:
這樣處理的話其實也能夠解決問題。但是我這里要說的一個問題是,我在修改的過程中,有把 Http_set_mode
定義為 u16
的數據類型,但是呢他還是占用 4 個字節,實際上這里就讓我發現了另外一個問題,按理來說u16
類型我本意是定義為 16位 的數據。
于是查看了一下u16
的定義:
我第一印象是,確實是 4 個字節的,為什么會這么定義? 想了一下,這個頭文件是在上次 51 單片機項目中復制過來的……
在 8 位的單片機中:
unsigned int
通常是16 位(2字節)
unsigned long
為 32 位
unsigned char
8位
所以這里我忽略了平臺的變換,直接使用 8 位單片機上的數據類型定義,如果要在 32 位單片機上定義 16位數據類型 ,建議使用uint16_t
(無符號) 和 int16_t
(有符號),因為這是在標準 C 語言庫文件 <stdint.h>
中定義的,在所有平臺上都明確表示16位,可以避免因編譯器差異導致的問題。
其實,即便我們真的記不得某一個數據類型到底占多少個字節,我們可以直接通過 sizeof
來判斷,示例如下:
printf("Size: %d bytes\n", (int)sizeof(uint16_t));
結語
本文我們講到的問題,是數據類型處理不當的問題,對數據類型的應用不夠熟練嚴謹導致的數據覆蓋。
我們通過問題,復習了一遍數據類型的一些基礎知識,也說明了如何通過 .map 文件檢查數據溢出或者覆蓋問題。希望對大家以后的產品開發有一定的幫助。
好了,本文就到這里,謝謝大家!