记录一下 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 文件检查数据溢出或者覆盖问题。希望对大家以后的产品开发有一定的帮助。
好了,本文就到这里,谢谢大家!