第一章:通信协议分析
———以微信宠物屋为例
微信宠物屋是机智云提供的一套完整的基础例程,在了解相关通信流程之后,我们可在该程序基础上开发自己的程序,实现与云服务的对接,进而达到远程控制的目的,接下来我们就来仔细分析下机智云的“M2M”机制到底是如何“跑起来的”。 第一节:文档说明部分详解第一步:我们打开微信宠物屋程序gokit-mcu-master:看到他的目录结构如下
其中:MDK_Project即是STM32的程序入口,在分析程序之前我们先来看一下《宠物屋产品设备端开发指南》了解一下该程序实现的功能和串口协议的规定是怎样的。 首先:我们来看产品信息,该处定义了相关的数据点(如何建立数据点请访问 www.gizwits.com)和宠物屋实现的功能,包括:调节RGB三色灯、电机转速、红外探测等。这些大家了解就行。其中需要大家特别注意的是“设备识别码”这个关键词,可以看到微信宠物屋的识别码是“6f3074fe43894547a4f1314bd7e3ae0b”,机智云对于每一款产品都会生成唯一的识别码以作区分,待会我们将在程序中找到定义识别码的位置。 其次:我们来看产品信息,可以看到流程图上清楚的介绍了整个协议工作的流程,简单分析一下,其实可以分为两部分处理: 第一部分、按键事件发生:主要用于MCU处理按键事件发生后与WIFI的通讯处理。 第二部分、串口事件发生:机智云是通过WIFI模块与MCU以串口通信来实现的,所以下面的这些有关WIFI的事件都可以归类为串口事件的发生,包括:Wifi 发送控制命令、Wifi 发送查询命令、Wifi 发送心跳命令、Wifi 状态更新、外设状态变化。
总体来说,整个系统上电之后的流程如下: 1)MCU 先上电,初始化完成后,给模块上电; 2) 模块初始化; 3) 模块向MCU 询问必要信息,MCU 返回信息(见协议举例); 4) 进入正常工作循环; a) 模块给MCU 下发控制命令(见协议举例); b) MCU 返回确认,表示收到命令,正在执行(见协议举例); c) 执行完新控制命令后,无论状态是否发生变化,MCU 都需要通知模块最新状态(见 协议举例); d) 若MCU 检测到环境属性变化或者用户在设备上按键引起的状态变化,MCU 需要 通知模块最新状态,但是其发送的频率不能快于2 秒每次(见协议举例); e) 若环境状态一直不变化,MCU 需要每隔10 分钟定期主动上报当前状态f) 模块会 向MCU 发送心跳,MCU 收到后按照格式返回即可(见协议举例);MCU 连续180 秒 收不到模块的数据,即可认为模块异常,可以给模块重新上电; 最后:我们来了解具体通讯协议的约定,可以看到 命令格式:header(2B)=0xFFFF, len(2B), cmd(1B), sn(1B), flags(2B),DATA(XB),checksum(1B) 说明: 1) 包头(header)固定为0xFFFF; 2) 长度(len)是指从cmd 开始到整个数据包结束所占用的字节数; 3) 命令字(cmd)表示具体的命令含义,详见协议举例; 4) 消息序号(sn)由发送方给出,接收方响应命令时需把消息序号返回给发送方; 5) 标志位(flag),本产品填写默认0; 6) p0 数据区(DATA),详细参见p0 数据区约定; 7) 检验和(checksum)的计算方式为从len~DATA,按字节求和; 8) 所有发送的命令都带有确认,如在200 毫秒内没有收到接收方的响应,发送方;应重 发,最多重发3 次; 9) 多于一个字节的整型数字以大端字节序编码(网络字节序); 10) 数字均用16 进制表示; 相信这一部分大家都能看懂,这里不再叙述,接下来看到“p0 数据区约定”,实现如下功能: 1)模块向MUC 发送控制命令时携带p0 命令和命令标志位以及可写数据区 2)MCU 主动发送状态时或者回复wifi 模块的状态查询时携带p0 命令和完整数据区 3)数据区会自动合并布尔和枚举变量,且有严格的顺序,不可任意改变 怎么来理解这三个功能呢?我们知道在机智云上定义数据点完成后,系统会自动生成对应的“串口通讯协议”如下图所示: 我们下载这份通讯协议文档打开,可以看到具体有如下命令: 1)WiFi模组请求设备信息 2)WiFi模组与设备MCU的心跳 3)设备MCU通知WiFi模组进入配置模式 4) 设备MCU重置WiFi模组 5)WiFi模组向设备MCU通知WiFi模组工作状态的变化 6) WiFi模组请求重启MCU 7)非法消息通知 8)WiFi模组读取设备的当前状态 9)设备MCU向WiFi模组主动上报当前状态 10)WiFi模组控制设备 这些命令中我们只要具体关注8、9、10三条命令即可,我们先来找到第10条命令如下: 4)对应上面“p0 数据区约定”中的功能1来看,模块向MUC 发送控制命令时携带p0 命令、命令标志位、及可写数据区三部分命令,至此大家应该清楚了,前6位代表p0 命令、attr_flags(1B)代表命令标志位、attr_vals(6B)代表可写数据区。这就告诉了我们编写mcu代码时,应该怎么样去识别WIFI发来的控制命令。那具体要怎么识别呢,往下看协议的注解: 1. 是否设置标志位(attr_flags)表示相关的数据值是否为有效值,相关的标志位为1表示值有效,为0表示值无效, 从右到左的标志位依次为: bit0: 设置LED_OnOff bit1: 设置LED_Color bit2: 设置LED_R bit3: 设置LED_G bit4: 设置LED_B bit5: 设置Motor_Speed 这里可以清楚的看到attr_flags占1B字节,其中bit0代表设置LED_OnOff......bit5: 设置Motor_Speed,那么对于我们的mcu接收到WIFI发来的控制命令后,我们通过识别attr_flags的每一位即可对应出需要控制的设备,如果还看不懂,一会我们分析mcu程序。 看完标志位之后我们看attr_vals(6B) 即可写数据区:
5)这里可以清楚的看到,只有相关的设置标志位(attr_flags)为1时,数据值才是有效的,需要特别注意的是“p0 数据区约定”约定第三条,数据区会自动合并布尔和枚举变量,且有严格的顺序,不可任意改变。对应上面的“byte0”合并了“bool”和“enum”类型。 至此p0 数据区约定 到此结束,后面在MCU程序中会对应具体代码讲解。 《宠物屋设备端开发指南》通讯写一部分最后一个包头排重约定 原则:我们的包头是两个连续的FF FF,如果此包中还有某字节出现FF,仅在传输和接收的时候处理,其他环节按正常数据处理; 举例: 1) 某设备上报状态帧:FF FF 00 15 05 03 00 00 04 01 01 02 03 01 00 00 00 32 FF 20 00 03 7D, 除包头外,出现了FF; 2) 在程序内部,作为正常的数据去处理; 3) 当需要传输时,将除包头外的FF后,增加一个55字节,其他不变; 4) 将上述数据处理成:FF FF 00 15 05 03 00 00 04 01 01 02 03 01 00 00 00 32 FF 55 20 00 03 7D,长度不变,校验码不变; 5) 接收方在接收过程中,如果收到字节是FF,及判断第二个字节是否也是FF,如果是FF,表示一个新包,按照新包处理; 6) 如果第二个字节是55,直接丢弃,不算接收长度,继续接收下一个字节; 7) 直到按照长度接收完,或者碰到下一个连续的FF FF; 这部分代码出现在mcu与WIFI 的串口通讯部分,所以我们一会找到mcu的串口接收程序,简而分析,即可清楚什么是包头排重约定。 《宠物屋设备端开发指南》第4部分协议举例,下节配合mcu程序讲解。
第二节:MCU程序详解(STM32) 注意:此节的分析需要读者有一定的STM32开发能力,如果不具备,请先学习STM32。 打开MCU程序,在main.c下我们可以看到这样一段注释: 意思已经很清楚了,大部分的命令代码机智云已经为我们实现了,这也是我在讲解《微信宠物屋-机智云接入串口通信协议文档》命令时说只需要关注8、9、10三条指令的原因了,这三条指令对应在protocol.c下的CmdSendMcuP0和CmdReportModuleStatus这两个函数下,即我们开发自己的程序时只需关注这两个函数即可。 首选来看主函数:
可以看到主函数下载完成了一堆初始化之后,在循环值进行了三个函数,第一个和第三个是串口事件处理,第二个是按键事件处理(对应之前第一节讲的流程图)。 我们先来看串口事件处理函数MessageHandle(),在这之前,我们需要考虑到串口事件发生的前提是串口已经接收到了数据帧,因此我们需要找到串口再那里接收这些命令的,我们找到串口接收函数,在STM32里串口接收是以串口中断出现的,而库函数里所有的中断都是默认在stm32f10x_it.c下的,所以在c文件下我们可找到void USART1_IRQHandler(void),这就是串口1的中断接收函数。这段代码我已经做了详细注释,大家可参考上一节的《宠物屋设备端开发指南》下的命令格式和包头排重约定仔细对照查看: void USART1_IRQHandler(void) { uint8_t vlue; short i;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { USART_ClearITPendingBit(USART1,USART_IT_RXNE); vlue = USART_ReceiveData(USART1); if(get_one_package ==0) //数据包还没有接收完 { if(cmd_flag1 ==0) //初始化为0 { if(vlue == 0xff)//接收到第一个0xff { uart_Count = 0; uart_buf[uart_Count]=vlue; //uart_buf[0]=0xff uart_Count++; cmd_flag1 = 1; //第一个0xff收到标志位 } return ; //如果没收到返回 } if(cmd_flag2 ==0) { if(cmd_flag2) //cmd_flag2=1 { uart_buf[uart_Count]=vlue; uart_Count++; if(uart_buf[1] == 0xff) { cmd_flag2 = 1; //已经收到了第二个0xFF } else { cmd_flag1 = 0; //两个0XFF接收完清标志位 } } cmd_flag2 = 1; } uart_buf[uart_Count] = vlue; if(uart_Count >=4 && uart_buf[uart_Count] == 0x55 && uart_buf[uart_Count-1] == 0xFF){} //包头排重 else uart_Count++; if(uart_Count == 0x04) { cmd_len = uart_buf[2]*256 + uart_buf[3]; } if(uart_Count == (cmd_len + 4)) //接收的数据包是否完整 { get_one_package = 1; //数据包接收完成标志位 cmd_flag1 = 0; } 看懂串口接收代码后,我们回到主函数,鼠标右键单击MessageHandle()选择gotodifinitionMessageHandle找到该处理函数: 可以看到if(get_one_package)这条语句,只有get_one_package=1的时候下面的程序才会执行,而get_one_package=1就是在串口接收函数里赋值的,表示接收到完整的数据帧。继续往下看到
这个switch语句下的各种命令可以对应在《《微信宠物屋-机智云接入串口通信协议文档》》下的命令列表中找到,前面说过这里面大部分命令机智云已经帮我们实现了,我们只需要关注两个函数即可,这两个函数就在这个switch下面:

其实说的在简单一点:我们只需要关注控制命令这一个函数即可
不信大家可以进入到void CmdReportModuleStatus(uint8_t *buf) 这个函数下看看都做了哪些工作:
从注释也可以看出这个函数只是描述WIFI状态变化的函数,比如我们连接的是AirLink模式还是softap模式,如果你想区分,就在这段代码下添加状态指示程序即可,我们不做过多讲解,重点用来解释控制命令函数void CmdSendMcuP0(uint8_t *buf) 大家拿到开发板之后,应该都知道第一步需要按下按键去配置WIFI连接到云端,才能实现远程控制,所以在讲解控制命令函数void CmdSendMcuP0(uint8_t *buf)之前,我们先来看看通过按键配置开发板的这些程序在哪里,既然是按键配置,那肯定是在按键处理函数里找喽,我们回到主函数下,找到按键处理函数 KeyHandle(); 鼠标右键单击 选择 goto difinition KeyHandle,可以看到
这里代码已经很清楚了,就不做过多解释,这里需要大家思考的地方是,按键的长按和短按是怎么实现的呢?这里告诉大家,本mcu程序定义了定时器3中断,去判断按键的长按与短按,超过2秒即认为是长按,既然是定时器中断函数,那肯定是在stm32f10x_it.c下面喽,我们找到它,看看它的函数原型void TIM3_IRQHandler(void) :(这部分代码我在原来的基础上做了更多的注解,相信大家都能看懂的)

了解的开发板配置的代码后,我们在回头看控制命令函数void CmdSendMcuP0(uint8_t *buf),这是最重点的部分啦,我们复制其中的重点代码做详细讲解。

这部分代码大体的工作流程是:如果串口0传来了数据(buf != NULL),先调用memcpy函数将其拷贝到m_w2m_controlMcu 结构体中去,其中结构体成员m_w2m_controlMcu.sub_cmd 是命令标志位,如果为SUB_CMD_REQUIRE_STATUS(0x02)则表示查询命令,当即上报当前的状态到服务器,如果为SUB_CMD_CONTROL_MC(0x01)则表示控制命令,控制部分就是我们需要重点实现的了。既然是控制部分,那肯定要和我们之前讲的《微信宠物屋-机智云接入串口通信协议文档.pdf》中的4.10节《WIFI模块控制设备》相联系起来了, 
我们之前讲过这部分 最重要的就是attr_flags(1B) 、attr_vals(6B)这两位;忘记的同学请返回前面看一看。这两位是怎么对应程序的呢?我们来看
我们鼠标右键 GOTO 到 m_w2m_controlMcu.cmd_tag 里面,可以看到该结构体内容如下:

现在是不是已经和《微信宠物屋-机智云接入串口通信协议文档.pdf》中的4.10节《WIFI模块控制设备》对应起来了,哈哈,我们接着往下看程序 
前面我们已经知道程序里的cmd_tag就对应《微信宠物屋-机智云接入串口通信协议文档.pdf》中的4.10节《WIFI模块控制设备》中的attr_flags(1B),是用来选择控制哪一位的,在文档中我们可以看到attr_flags的第0位是用来选择控制LED灯开关的,即只要设置了第0位为1就表示要控制LED等开关了,如下所示: 
对应程序中 if((m_w2m_controlMcu.cmd_tag & 0x01) == 0x01) 即标志第0位为1,接下来就可以控制LED灯的开关了,我们接着程序往下看;
这段程序就控制了LED灯的开关,即cmd_byte的第0位为1表示灯开,为0表示灯关。 这对应《微信宠物屋-机智云接入串口通信协议文档.pdf》中的4.10节《WIFI模块控制设备》中的attr_vals(6B),即设置数据位,如下所示: 可以清楚的看到只要控制byte0(0x07)的第0位就可以控制LED灯了,第一位是用来控制LED颜色的。 这里需要说明的是一般设置数据位的数据包的byte0 是用来控制bool型变量和枚举类型变量的了,应为机智云默认是把bool变量和枚举类型合并处理的。
下面的程序基本和上面一样了,只要大家看懂了《微信宠物屋-机智云接入串口通信协议文档.pdf》中的4.10节《WIFI模块控制设备》中的attr_flags(1B) 、attr_vals(6B)这两位和程序代码中的cmd_tag、status_w的对应关系就能自己编写控制程序了。
|