1. 前言
我国独生子女,以及人口老龄化等问题,正逐渐成为一个重大的社会问题,老年人机体能力的下降,摔倒引起的安全和危害愈来愈突出,国家和社会越来越关注老年人的健康和安全,开发一个能够实时检测出老年人是否摔倒,并且能及时告知监护人的摔倒检测以及报警系统具有重要的现实意义。本系统包括检测摔倒模块、GPS 定位模块和通信模块三部分,通过检测老年人日常状态,可以得知老年人的状态,如果监测到老年人摔倒了,此时会通过网络把检测结果上传到物联网云平台,获得老年人摔倒地点的 GPS 定位,并且通过 GPRS 通讯发短信给预设的监护人。
2. 设计需求
(1)采用 STM32 单片机作为主控芯片,配合其他模块完成功能设计
(2)通信模块采用 SIM800C,支持上传采集的 GPS 经纬度数据到云端服务器,云端采用华为云物联网平台。
(3)老人摔倒检测采用 MPU6050 陀螺仪检测,当检测到老人摔倒之后,会通过 SIM800C 发送短信到紧急联系人,设备上的蜂鸣器会发出警报声,周围行人听到也可以进行帮助;并且会将 GPS 数据上传到云端,通过地图显示老人的位置,家人通过短信知道老人摔倒后,通过云端地图显示的位置,可以快速赶到老人身边,或者报警求助,报告位置。
(4)老人摔倒后,如果自己能行动,没有大问题,可以自己按下设备上的按键取消蜂鸣器报警,并且通过 SIM800C 向家人发送一条短信,报平安。
3. 设计的实物效果
为了快速验证方案的可行性,这里采用现成的模块采用杜邦线连接完成整个预想的功能设计。
下面就是硬件连接好之后的效果图,选用的硬件型号在第 4 章节已经全部贴出来了;为了方便户外测试,这里的供电电源采用了充电宝,也可以采用电池盒供电。
可以设置电子围栏,坐标超出之后进行提示。
4. 硬件选型
主控芯片采用 STM32RCT6,通信模块采用 SIM800C,GPS 采集使用 ATGM336 北斗 BDS+GPS 双模模块,老人摔倒检测模块采用 MPU6050 陀螺仪。
这些都是采用现成的成品模块,都是在淘宝上买的,下面都贴出了模块的型号,模块的实物截图,如果自己想做一个,可以在淘宝上找到一样的模块型号购买。
4.1 SIM800C
SIM800C 模块是一款高性能高性价比工业级的 GSM/GPRS 模块。本模块采用 SIMCOM 公司的工业级四频 850/900/ 1800/1900MHz SM800 芯片,可以低功耗实现语音、SMS、数据和传真信息的传输。
模块特点:
1、支持极限 DC5V-18V 宽电压输入
2、有电源使能开关引脚 EN
3、支持锂电池供电接口 VBAT3.5-4.5V
4、输入支持移动和联通手机卡 Micro SIM 卡
5、送 51/STM32/ARDUINO 驱动例程
1、DC 5V-18V 电源输入,推荐使用 DC 9V
2、电源开始使能引脚默认使能
3、电源地
4、GSM 模块的 TXD 引脚接其它模块的 RXD
5、GSM 模块的 RXD 引脚接其它模块的 TXD
6、数据终端准备
7、内核音频输出引脚
8、内核音频输出引脚
9、锂电池输入引脚,DC 3.5 - 4.5V
10、电源地
11、启动引脚和 GND 短路可实现开机自启动
12、电源地
13、RTC 外置电池引脚
14、内核振铃提示引脚
15、内合音频输入引脚
16、内核音频输入引脚
加粗的引脚一般都用到。
建议使用 V_IN 单独供电 DC5-18V 输入(推荐使用 9V),或者 VBAT 供电锂电池两种供电方式这两种供电方式最稳定。如果只是简单调试,也可使用 USB-TTL 或者开发板的 5V 直接给模块供电。不过一般电脑或者开发板的功率有限,可能会不稳定。请根据具体情况自己取舍选择合适电源。
总结:
模块本身支持自适应波特率,可以自动根据发送过去的指令计算对应的波特率,一般使用 115200 即可。
模块调试总结:
(1)供电电压 5V 也可以,采用电脑 USB 供电(直接插电脑 USB 口)。正常供电之后,模块上有电源指示灯。
(2)SIM800C 的 TX 脚接单片机的 RX 脚
(3)SIM800C 的 RX 脚接单片机的 TX 脚
(4)SIM800C 的第 11 个引脚(PWK)和 12 个引脚(GND)短接接在一起,才可以开机。
电源正常后,右上角有一个黄色的电源灯。
通过串口发送 AT 指令过去测试模块效果。
4.2 STM32F103C8T6 开发板
3.3 GPS 模块
GPS 模块正常定位后,模块上的 LED 灯会按照 1 秒钟闪烁一次。
返回的字段里GNRMC表示当前定位的GPS经纬度,解析代码只需要解析GNRMC 字段。
第一次启动 GPS 模块,定位差不多要几分钟时间,定位成功后,第二次启动定位就很快,最好是在室外,室内信号差,定位时间更久。
$GNGGA,114955.000,2842.4158,N,11549.5439,E,1,05,3.8,54.8,M,0.0,M,,*4F
$GNGLL,2842.4158,N,11549.5439,E,114955.000,A,A*4D
$GPGSA,A,3,10,31,18,,,,,,,,,,5.7,3.8,4.2*37
$BDGSA,A,3,07,10,,,,,,,,,,,5.7,3.8,4.2*2A
$GPGSV,3,1,10,10,49,184,42,12,16,039,,14,54,341,,18,22,165,23*7B
$GPGSV,3,2,10,22,11,318,,25,51,055,,26,24,205,,29,13,110,*7C
$GPGSV,3,3,10,31,50,287,36,32,66,018,*7F
$BDGSV,1,1,04,03,,,07,05,,,29,07,79,246,33,10,52,232,19*62
$GNRMC,114955.000,A,2842.4158,N,11549.5439,E,0.00,44.25,061117,,,A*4D
$GNVTG,44.25,T,,M,0.00,N,0.00,K,A*14
$GNZDA,114955.000,06,11,2017,00,00*47
$GPTXT,01,01,01,ANTENNA OK*35
复制代码
4.4 MPU6050 陀螺仪
陀螺仪选择的是正点原子的模块,比较稳定,质量较好。
4.5 蜂鸣器
蜂鸣器选择的高电平触发。
5. 创建云端物联网服务器
为了方便查看老人摔倒之后的位置,需要通过 SIM800C 将设备采集的 GPS 数据上传到云平台服务器保存,就算老人没有跌倒,也可以实时关注老人的位置,在地图上绘制出轨迹线路,方便家人随时联系,了解老人的情况。这里物联网的平台选择是华为云物联网平台,目前是免费使用的,在云端创建产品等信息后,设备再通过 MQTT 协议连接云平台上传 GPS 数据。目前华为云的拖拽试网页开发页面已经下架,目前要开发对应的上位机,可以采用最近主推的低代码开发平台或者自己通过云平台的应用侧开发接口自己开发上位机,我这里是自己开发的上位机,通过 QT 编写的上位机 APP,支持 windows、Android、Linux 等多个平台运行,跨平台使用还是比较方便。
下面接着就介绍如何登陆官网创建产品、设备、完成云端的产品部署。
5.1 创建产品
官网地址: https://www.huaweicloud.com/product/iothub.html打开官网后没有华为云账号需要先注册账号,这些步骤就不多说了,接下来就直接介绍如何创建产品、设备、配置属性、完成数据上传交互的流程。
点击免费使用进去页面。
点击左边产品选项,点击右上角创建产品按钮,弹出参数填充对话框。
根据自己的设备情况填入信息之后保存。
产品创建成功,点击查看详细信息。
5.2 创建模型文件
在现在的详情页面往下翻,可以看到模型创建的选项。点击自定义模型选项,创建模型。这里的模型就是设备上传的数据属性。
添加服务 ID。
点击创建属性,这里选择 JSON 类型的数据,上传的 GPS 有经纬度两个数据,方便保存。
创建成功。
5.3 创建设备
产品是一个大框架的模型,下面可以创建很多具体的设备,目前我这里只有一个硬件设备,就创建一个设备就行了。设备可以手动创建,也支持自动创建,就像现在市面上的智能设备产品,拿到设备后,扫描设备上二维码再通手机 APP 就可以完成产品的创建,设备的添加。 目前我这里设备就只有一个,而且还要演示整个流程,就在网页上完成整个设备的创建。
链接地址: https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-portal/device/all-device
点击左边的设备选项,再点击右上角的注册设备。
填充好信息之后,点击确定。
创建后保存设备的数据。
{
"device_id": "GPS1",
"secret": "12345678"
}
复制代码
创建成功,目前设备处于未激活状态。
5.4 获取 MQTT 登录参数
目前产品、设备创建好之后就需要通过设备连接上来上传数据,要完成这个步骤,还需要知道一些前提的流程。【A】华为云服务器 IP 地址、域名、端口号【B】主题订阅的格式、主题发布的格式【C】MQTT 协议登录的三元组信息充分了解了这 3 个信息之后就可以编写设备端代码了。下面就详细介绍这些信息怎么得到。
【1】华为云的服务器地址信息在这里查看: https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-portal/instance/detail?id=8fe87243-d97d-4c1e-bb34-186a60ca2d14&type=public
华为云物联网平台的域名是: 161a58a78.iot-mqtts.cn-north-4.myhuaweicloud.com
华为云物联网平台的IP地址是:121.36.42.100
端口号是1883
复制代码
【2】主题订阅的格式、主题发布的格式主题订阅上报的格式在产品的详情页面可以看到。
主题发布官方的详细介绍在这里:https://support.huaweicloud.com/devg-iothub/iot_01_2127.html#ZH-CN_TOPIC_0240834853__zh-cn_topic_0251997880_li365284516112
主题上报的属性格式说明文档地址:https://support.huaweicloud.com/api-iothub/iot_06_v5_3010.html
根据当前设备的信息总结,得到的信息如下:
//订阅主题: 平台下发消息给设备
$oc/devices/GPS1/sys/messages/down
//设备上报数据
$oc/devices/GPS1/sys/properties/report
//上报的属性消息 (一次可以上报多个属性,在json里增加就行了)
{"services": [{"service_id": "GPS","properties":{"GPS":{"lon":106.53,"lat":29.46}}}]}
复制代码
【3】MQTT 协议登录的三元组信息华为云提供了 MQTT 协议参数的生成工具,非常方便,根据提示填入参数一键生成三元组。
MQTT 设备登陆密匙生成地址: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/
得到的三元组如下:
ClientId GPS1_0_0_2022060716
Username GPS1
Password 27a2d2dd716fac29a0041beec1d7cf5f5b529fac65cc815c7eed9adb04d7364b
复制代码
5.5 采用 MQTT 客户工具登录测试
为了方便验证服务器的配置以及主题、属性这些是否 OK,可以先使用 MQTT 客户端模拟真实设备登录测试。下面这个 MQTT 工具是我自己开发的,为了方便测试对接物联网平台,使用 QT 写了这么一个工具软件。工具软件的名称: MQTT客户端_v2.5(协议3.1.1).exe
我已经上传到 CSDN 的资源库里了,可以直接去 CSDN 里搜索就能找到软件的下载地址,下面的文章的附件里我也会上传一份。
在软件左边根据提示填入对应的参数,依次点击登录,订阅主题,发布主题即可。
这时打开网页可以看到设备已经在线了。
在设备影子页面上可以看到上传的数据内容。
启动消息跟踪,可以了解通信的过程。
6. STM32 硬件设备端程序设计
在第 5 章完成了物联网云平台的构建,接下来的第 6 章节,就编写 STM32 设备端代码。
STM32 设备端开发环境采用 keil5 进行开发,编程风格采用寄存器风格形式,不管使用库函数,还是寄存器,还是 HAL 库,本身都一样,没有太大区别,我编写 STM32 代码习惯了寄存器开发,主要是寄存器的代码比较简洁,工程文件精简。
关于 keil5 软件的下载流程、安装流程、基本使用办法这里就不在详细介绍,相应看这篇文章的道友
应该这些会这些基操,这里主要是以项目为导向,介绍比较核心的知识点。
MQTT 协议连接华为云 IOT 源码工程: https://download.csdn.net/download/xiaolong1126626497/81993720
6.1 硬件接线
下面是介绍使用的硬件模块与 STM32 开发板之间的硬件连线。
SIM800C接线说明:
GND----GND
PA2----SIM800C_RXD
PA3----SIM800C_TXD
CH340模块接线说明:
GND----GND
RX-----PA9
GPS接线说明: (波特率需要根据GPS模块实际情况进行修改)
GND----GND
VCC---3.3V
PB11----GPS_TX
蜂鸣器模块: 高电平响
BEEP----->PB8
板载LED灯:
LED1--PC13 低电平亮
板载按键:
KEY1--PA0 按下为高电平
外接按键:
KEY1 -PB3 按下是低电平
KEY2 -PB2 按下是低电平
外接LED灯模块:
LED1-PB4 低电平亮
LED2-PB5 低电平亮
硬件接线:
1 VCC 3.3V/5V 电源输入 ---->接3.3V
2 GND 地线 --->接GND
3 IIC_SDA IIC 通信数据线 -->PB6
4 IIC_SCL IIC 通信时钟线 -->PB7
5 MPU_INT 中断输出引脚 ---->未接
6 MPU_AD0 IIC 从机地址设置引脚-->PA15
AD0引脚说明:ID=0X68(悬空/接 GND) ID=0X69(接 VCC)
注意:陀螺仪初始化的时候,必须正常摆放才可以初始化成功
复制代码
这是通过杜邦线接好模块后的效果图:
6.2 keil 工程截图
6.3 原理图
下面是绘制的原理图。
6.4 MQTT 协议实现代码以及 MQTT 参数
SIM800C 本身没有内置 MQTT 协议指令,只有 TCP 通信的指令,需要自己封装 MQTT 协议,然后通过 TCP 通信的相关指令完成云端服务器连接,实现数据交互。
下面这份代码是 MQTT 协议的参数定义,程序里为了方便修改,采用宏定义方式赋值这些参数。
#include "stm32f10x.h"
#include "led.h"
#include "delay.h"
#include "key.h"
#include "usart.h"
#include <string.h>
#include "timer.h"
#include "bluetooth.h"
#include "sim800c.h"
#include "mqtt.h"
//华为物联网服务器的设备信息
#define MQTT_ClientID "GPS1_0_0_2022060716"
#define MQTT_UserName "GPS1"
#define MQTT_PassWord "27a2d2dd716fac29a0041beec1d7cf5f5b529fac65cc815c7eed9adb04d7364b"
//订阅与发布的主题
#define SET_TOPIC "$oc/devices/GPS1/sys/messages/down" //订阅
#define POST_TOPIC "$oc/devices/GPS1/sys/properties/report" //发布
char mqtt_message[200];//上报数据缓存区
复制代码
这是封装的几个 MQTT 协议核心函数:
/*
函数功能: 登录服务器
函数返回值: 0表示成功 1表示失败
*/
u8 MQTT_Connect(char *ClientID,char *Username,char *Password)
{
u8 i,j;
int ClientIDLen = strlen(ClientID);
int UsernameLen = strlen(Username);
int PasswordLen = strlen(Password);
int DataLen;
mqtt_txlen=0;
//可变报头+Payload 每个字段包含两个字节的长度标识
DataLen = 10 + (ClientIDLen+2) + (UsernameLen+2) + (PasswordLen+2);
//固定报头
//控制报文类型
mqtt_txbuf[mqtt_txlen++] = 0x10; //MQTT Message Type CONNECT
//剩余长度(不包括固定头部)
do
{
u8 encodedByte = DataLen % 128;
DataLen = DataLen / 128;
// if there are more data to encode, set the top bit of this byte
if ( DataLen > 0 )
encodedByte = encodedByte | 128;
mqtt_txbuf[mqtt_txlen++] = encodedByte;
}while ( DataLen > 0 );
//可变报头
//协议名
mqtt_txbuf[mqtt_txlen++] = 0; // Protocol Name Length MSB
mqtt_txbuf[mqtt_txlen++] = 4; // Protocol Name Length LSB
mqtt_txbuf[mqtt_txlen++] = 'M'; // ASCII Code for M
mqtt_txbuf[mqtt_txlen++] = 'Q'; // ASCII Code for Q
mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T
mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T
//协议级别
mqtt_txbuf[mqtt_txlen++] = 4; // MQTT Protocol version = 4 对于 3.1.1 版协议,协议级别字段的值是 4(0x04)
//连接标志
mqtt_txbuf[mqtt_txlen++] = 0xc2; // conn flags
mqtt_txbuf[mqtt_txlen++] = 0; // Keep-alive Time Length MSB
mqtt_txbuf[mqtt_txlen++] = 100; // Keep-alive Time Length LSB 100S心跳包 保活时间
mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);// Client ID length MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);// Client ID length LSB
memcpy(&mqtt_txbuf[mqtt_txlen],ClientID,ClientIDLen);
mqtt_txlen += ClientIDLen;
if(UsernameLen > 0)
{
mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen); //username length MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen); //username length LSB
memcpy(&mqtt_txbuf[mqtt_txlen],Username,UsernameLen);
mqtt_txlen += UsernameLen;
}
if(PasswordLen > 0)
{
mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen); //password length MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen); //password length LSB
memcpy(&mqtt_txbuf[mqtt_txlen],Password,PasswordLen);
mqtt_txlen += PasswordLen;
}
memset(mqtt_rxbuf,0,mqtt_rxlen);
MQTT_SendBuf(mqtt_txbuf,mqtt_txlen);
for(j=0;j<10;j++)
{
delay_ms(50);
if(USART3_RX_FLAG)
{
memcpy((char *)mqtt_rxbuf,USART3_RX_BUFFER,USART3_RX_CNT);
//memcpy
for(i=0;i<USART3_RX_CNT;i++)USART1_Printf("%#x ",USART3_RX_BUFFER[i]);
USART3_RX_FLAG=0;
USART3_RX_CNT=0;
}
//CONNECT
if(mqtt_rxbuf[0]==parket_connetAck[0] && mqtt_rxbuf[1]==parket_connetAck[1]) //连接成功
{
return 0;//连接成功
}
}
return 1;
}
/*
函数功能: MQTT订阅/取消订阅数据打包函数
函数参数:
topic 主题
qos 消息等级 0:最多分发一次 1: 至少分发一次 2: 仅分发一次
whether 订阅/取消订阅请求包 (1表示订阅,0表示取消订阅)
返回值: 0表示成功 1表示失败
*/
u8 MQTT_SubscribeTopic(char *topic,u8 qos,u8 whether)
{
u8 i,j;
mqtt_txlen=0;
int topiclen = strlen(topic);
int DataLen = 2 + (topiclen+2) + (whether?1:0);//可变报头的长度(2字节)加上有效载荷的长度
//固定报头
//控制报文类型
if(whether)mqtt_txbuf[mqtt_txlen++] = 0x82; //消息类型和标志订阅
else mqtt_txbuf[mqtt_txlen++] = 0xA2; //取消订阅
//剩余长度
do
{
u8 encodedByte = DataLen % 128;
DataLen = DataLen / 128;
// if there are more data to encode, set the top bit of this byte
if ( DataLen > 0 )
encodedByte = encodedByte | 128;
mqtt_txbuf[mqtt_txlen++] = encodedByte;
}while ( DataLen > 0 );
//可变报头
mqtt_txbuf[mqtt_txlen++] = 0; //消息标识符 MSB
mqtt_txbuf[mqtt_txlen++] = 0x0A; //消息标识符 LSB
//有效载荷
mqtt_txbuf[mqtt_txlen++] = BYTE1(topiclen);//主题长度 MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(topiclen);//主题长度 LSB
memcpy(&mqtt_txbuf[mqtt_txlen],topic,topiclen);
mqtt_txlen += topiclen;
if(whether)
{
mqtt_txbuf[mqtt_txlen++] = qos;//QoS级别
}
for(i=0;i<10;i++)
{
memset(mqtt_rxbuf,0,mqtt_rxlen);
MQTT_SendBuf(mqtt_txbuf,mqtt_txlen);
for(j=0;j<10;j++)
{
delay_ms(50);
if(USART3_RX_FLAG)
{
memcpy((char *)mqtt_rxbuf,(char*)USART3_RX_BUFFER,USART3_RX_CNT);
USART3_RX_FLAG=0;
USART3_RX_CNT=0;
}
if(mqtt_rxbuf[0]==parket_subAck[0] && mqtt_rxbuf[1]==parket_subAck[1]) //订阅成功
{
return 0;//订阅成功
}
}
}
return 1; //失败
}
//MQTT发布数据打包函数
//topic 主题
//message 消息
//qos 消息等级
u8 MQTT_PublishData(char *topic, char *message, u8 qos)
{
int topicLength = strlen(topic);
int messageLength = strlen(message);
static u16 id=0;
int DataLen;
mqtt_txlen=0;
//有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度
//QOS为0时没有标识符
//数据长度 主题名 报文标识符 有效载荷
if(qos) DataLen = (2+topicLength) + 2 + messageLength;
else DataLen = (2+topicLength) + messageLength;
//固定报头
//控制报文类型
mqtt_txbuf[mqtt_txlen++] = 0x30; // MQTT Message Type PUBLISH
//剩余长度
do
{
u8 encodedByte = DataLen % 128;
DataLen = DataLen / 128;
// if there are more data to encode, set the top bit of this byte
if ( DataLen > 0 )
encodedByte = encodedByte | 128;
mqtt_txbuf[mqtt_txlen++] = encodedByte;
}while ( DataLen > 0 );
mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);//主题长度MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);//主题长度LSB
memcpy(&mqtt_txbuf[mqtt_txlen],topic,topicLength);//拷贝主题
mqtt_txlen += topicLength;
//报文标识符
if(qos)
{
mqtt_txbuf[mqtt_txlen++] = BYTE1(id);
mqtt_txbuf[mqtt_txlen++] = BYTE0(id);
id++;
}
memcpy(&mqtt_txbuf[mqtt_txlen],message,messageLength);
mqtt_txlen += messageLength;
MQTT_SendBuf(mqtt_txbuf,mqtt_txlen);
return mqtt_txlen;
}
复制代码
6.4 MPU6050.c 代码
这是 MPU6050 陀螺仪的核心驱动代码,方便检测老人的姿态,判断是否摔倒。
#include "mpu6050.h"
#include "sys.h"
#include "delay.h"
#include <stdio.h>
/*--------------------------------------------------------------------IIC协议底层模拟时序--------------------------------------------------------------------------------*/
/*
函数功能:MPU IIC 延时函数
*/
void MPU6050_IIC_Delay(void)
{
DelayUs(2);
}
/*
函数功能: 初始化IIC
3 IIC_SDA IIC 通信数据线 -->PB6
4 IIC_SCL IIC 通信时钟线 -->PB7
*/
void MPU6050_IIC_Init(void)
{
RCC->APB2ENR|=1<<3; //先使能外设IO PORTB时钟
GPIOB->CRL&=0X00FFFFFF; //6/7 推挽输出
GPIOB->CRL|=0X33000000;
GPIOB->ODR|=3<<6; //6,7 输出高
}
/*
函数功能: 产生IIC起始信号
*/
void MPU6050_IIC_Start(void)
{
MPU6050_SDA_OUT(); //sda线输出
MPU6050_IIC_SDA=1;
MPU6050_IIC_SCL=1;
MPU6050_IIC_Delay();
MPU6050_IIC_SDA=0;//START:when CLK is high,DATA change form high to low
MPU6050_IIC_Delay();
MPU6050_IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
}
/*
函数功能: 产生IIC停止信号
*/
void MPU6050_IIC_Stop(void)
{
MPU6050_SDA_OUT();//sda线输出
MPU6050_IIC_SCL=0;
MPU6050_IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
MPU6050_IIC_Delay();
MPU6050_IIC_SCL=1;
MPU6050_IIC_SDA=1;//发送I2C总线结束信号
MPU6050_IIC_Delay();
}
/*
函数功能: 等待应答信号到来
返 回 值:1,接收应答失败
0,接收应答成功
*/
u8 MPU6050_IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
MPU6050_SDA_IN(); //SDA设置为输入
MPU6050_IIC_SDA=1;MPU6050_IIC_Delay();
MPU6050_IIC_SCL=1;MPU6050_IIC_Delay();
while(MPU6050_READ_SDA)
{
ucErrTime++;
if(ucErrTime>250)
{
MPU6050_IIC_Stop();
return 1;
}
}
MPU6050_IIC_SCL=0;//时钟输出0
return 0;
}
/*
函数功能:产生ACK应答
*/
void MPU6050_IIC_Ack(void)
{
MPU6050_IIC_SCL=0;
MPU6050_SDA_OUT();
MPU6050_IIC_SDA=0;
MPU6050_IIC_Delay();
MPU6050_IIC_SCL=1;
MPU6050_IIC_Delay();
MPU6050_IIC_SCL=0;
}
/*
函数功能:不产生ACK应答
*/
void MPU6050_IIC_NAck(void)
{
MPU6050_IIC_SCL=0;
MPU6050_SDA_OUT();
MPU6050_IIC_SDA=1;
MPU6050_IIC_Delay();
MPU6050_IIC_SCL=1;
MPU6050_IIC_Delay();
MPU6050_IIC_SCL=0;
}
/*
函数功能:IIC发送一个字节
返回从机有无应答
1,有应答
0,无应答
*/
void MPU6050_IIC_Send_Byte(u8 txd)
{
u8 t;
MPU6050_SDA_OUT();
MPU6050_IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
MPU6050_IIC_SDA=(txd&0x80)>>7;
txd<<=1;
MPU6050_IIC_SCL=1;
MPU6050_IIC_Delay();
MPU6050_IIC_SCL=0;
MPU6050_IIC_Delay();
}
}
/*
函数功能:读1个字节,ack=1时,发送ACK,ack=0,发送nACK
*/
u8 MPU6050_IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
MPU6050_SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
MPU6050_IIC_SCL=0;
MPU6050_IIC_Delay();
MPU6050_IIC_SCL=1;
receive<<=1;
if(MPU6050_READ_SDA)receive++;
MPU6050_IIC_Delay();
}
if(!ack)
MPU6050_IIC_NAck();//发送nACK
else
MPU6050_IIC_Ack(); //发送ACK
return receive;
}
/*--------------------------------------------------------------------MPU6050底层驱动代码--------------------------------------------------------------------------------*/
//MPU6050 AD0控制脚
#define MPU6050_AD0_CTRL PAout(15) //控制AD0电平,从而控制MPU地址
/*
函数功能:初始化MPU6050
返 回 值:0,成功
其他,错误代码
*/
u8 MPU6050_Init(void)
{
u8 res;
RCC->APB2ENR|=1<<2; //使能PORTA时钟
GPIOA->CRH&=0X0FFFFFFF; //PA15设置成推挽输出
GPIOA->CRH|=0X30000000;
JTAG_Set(SWD_ENABLE); //禁止JTAG,从而PA15可以做普通IO使用,否则PA15不能做普通IO!!!
MPU6050_AD0_CTRL=0; //控制MPU6050的AD0脚为低电平,从机地址为:0X68
MPU6050_IIC_Init();//初始化IIC总线
MPU6050_Write_Byte(MPU_PWR_MGMT1_REG,0X80); //复位MPU6050
DelayMs(100);
MPU6050_Write_Byte(MPU_PWR_MGMT1_REG,0X00); //唤醒MPU6050
MPU6050_Set_Gyro_Fsr(3); //陀螺仪传感器,±2000dps
MPU6050_Set_Accel_Fsr(0); //加速度传感器,±2g
MPU6050_Set_Rate(50); //设置采样率50Hz
MPU6050_Write_Byte(MPU_INT_EN_REG,0X00); //关闭所有中断
MPU6050_Write_Byte(MPU_USER_CTRL_REG,0X00); //I2C主模式关闭
MPU6050_Write_Byte(MPU_FIFO_EN_REG,0X00); //关闭FIFO
MPU6050_Write_Byte(MPU_INTBP_CFG_REG,0X80); //INT引脚低电平有效
res=MPU6050_Read_Byte(MPU_DEVICE_ID_REG);
if(res==MPU6050_ADDR)//器件ID正确
{
MPU6050_Write_Byte(MPU_PWR_MGMT1_REG,0X01); //设置CLKSEL,PLL X轴为参考
MPU6050_Write_Byte(MPU_PWR_MGMT2_REG,0X00); //加速度与陀螺仪都工作
MPU6050_Set_Rate(50); //设置采样率为50Hz
}else return 1;
return 0;
}
/*
设置MPU6050陀螺仪传感器满量程范围
fsr:0,±250dps;1,±500dps;2,±1000dps;3,±2000dps
返回值:0,设置成功
其他,设置失败
*/
u8 MPU6050_Set_Gyro_Fsr(u8 fsr)
{
return MPU6050_Write_Byte(MPU_GYRO_CFG_REG,fsr<<3);//设置陀螺仪满量程范围
}
/*
函数功能:设置MPU6050加速度传感器满量程范围
函数功能:fsr:0,±2g;1,±4g;2,±8g;3,±16g
返 回 值:0,设置成功
其他,设置失败
*/
u8 MPU6050_Set_Accel_Fsr(u8 fsr)
{
return MPU6050_Write_Byte(MPU_ACCEL_CFG_REG,fsr<<3);//设置加速度传感器满量程范围
}
/*
函数功能:设置MPU6050的数字低通滤波器
函数参数:lpf:数字低通滤波频率(Hz)
返 回 值:0,设置成功
其他,设置失败
*/
u8 MPU6050_Set_LPF(u16 lpf)
{
u8 data=0;
if(lpf>=188)data=1;
else if(lpf>=98)data=2;
else if(lpf>=42)data=3;
else if(lpf>=20)data=4;
else if(lpf>=10)data=5;
else data=6;
return MPU6050_Write_Byte(MPU_CFG_REG,data);//设置数字低通滤波器
}
/*
函数功能:设置MPU6050的采样率(假定Fs=1KHz)
函数参数:rate:4~1000(Hz)
返 回 值:0,设置成功
其他,设置失败
*/
u8 MPU6050_Set_Rate(u16 rate)
{
u8 data;
if(rate>1000)rate=1000;
if(rate<4)rate=4;
data=1000/rate-1;
data=MPU6050_Write_Byte(MPU_SAMPLE_RATE_REG,data); //设置数字低通滤波器
return MPU6050_Set_LPF(rate/2); //自动设置LPF为采样率的一半
}
/*
函数功能:得到温度值
返 回 值:返回值:温度值(扩大了100倍)
*/
short MPU6050_Get_Temperature(void)
{
u8 buf[2];
short raw;
float temp;
MPU6050_Read_Len(MPU6050_ADDR,MPU_TEMP_OUTH_REG,2,buf);
raw=((u16)buf[0]<<8)|buf[1];
temp=36.53+((double)raw)/340;
return temp*100;;
}
/*
函数功能:得到陀螺仪值(原始值)
函数参数:gx,gy,gz:陀螺仪x,y,z轴的原始读数(带符号)
返 回 值:0,成功,其他,错误代码
*/
u8 MPU6050_Get_Gyroscope(short *gx,short *gy,short *gz)
{
u8 buf[6],res;
res=MPU6050_Read_Len(MPU6050_ADDR,MPU_GYRO_XOUTH_REG,6,buf);
if(res==0)
{
*gx=((u16)buf[0]<<8)|buf[1];
*gy=((u16)buf[2]<<8)|buf[3];
*gz=((u16)buf[4]<<8)|buf[5];
}
return res;;
}
/*
函数功能:得到加速度值(原始值)
函数参数:gx,gy,gz:陀螺仪x,y,z轴的原始读数(带符号)
返 回 值:0,成功,其他,错误代码
*/
u8 MPU6050_Get_Accelerometer(short *ax,short *ay,short *az)
{
u8 buf[6],res;
res=MPU6050_Read_Len(MPU6050_ADDR,MPU_ACCEL_XOUTH_REG,6,buf);
if(res==0)
{
*ax=((u16)buf[0]<<8)|buf[1];
*ay=((u16)buf[2]<<8)|buf[3];
*az=((u16)buf[4]<<8)|buf[5];
}
return res;;
}
//获取数据
void MPU6050_Get_Data(short *ax,short *ay,short *az)
{
short ax_buff[10]={0};
short ay_buff[10]={0};
short az_buff[10]={0};
int i=0,j=0;
short tmp;
u32 data=0;
for(i=0;i<10;i++)
{
MPU6050_Get_Accelerometer(&ax_buff[i],&ay_buff[i],&az_buff[i]);
}
//排序
for(i=0;i<10-1;i++)
{
for(j=0;j<10-1-i;j++)
{
if(ax_buff[j]>ax_buff[j+1])
{
tmp=ax_buff[j];
ax_buff[j]=ax_buff[j+1];
ax_buff[j+1]=tmp;
}
}
}
for(i=1;i<9;i++)
{
data+=ax_buff[i];
}
*ax=data/8;
//排序
for(i=0;i<10-1;i++)
{
for(j=0;j<10-1-i;j++)
{
if(ay_buff[j]>ay_buff[j+1])
{
tmp=ay_buff[j];
ay_buff[j]=ay_buff[j+1];
ay_buff[j+1]=tmp;
}
}
}
for(i=1;i<9;i++)
{
data+=ay_buff[i];
}
*ay=data/8;
//排序
for(i=0;i<10-1;i++)
{
for(j=0;j<10-1-i;j++)
{
if(az_buff[j]>az_buff[j+1])
{
tmp=az_buff[j];
az_buff[j]=az_buff[j+1];
az_buff[j+1]=tmp;
}
}
}
for(i=1;i<9;i++)
{
data+=az_buff[i];
}
*az=data/8;
}
/*
函数功能:IIC连续写
函数参数:
addr:器件地址
reg:寄存器地址
len:写入长度
buf:数据区
返 回 值:0,成功,其他,错误代码
*/
u8 MPU6050_Write_Len(u8 addr,u8 reg,u8 len,u8 *buf)
{
u8 i;
MPU6050_IIC_Start();
MPU6050_IIC_Send_Byte((addr<<1)|0);//发送器件地址+写命令
if(MPU6050_IIC_Wait_Ack()) //等待应答
{
MPU6050_IIC_Stop();
printf("等待应答失败\r\n");
return 1;
}
MPU6050_IIC_Send_Byte(reg); //写寄存器地址
MPU6050_IIC_Wait_Ack(); //等待应答
for(i=0;i<len;i++)
{
MPU6050_IIC_Send_Byte(buf[i]); //发送数据
if(MPU6050_IIC_Wait_Ack()) //等待ACK
{
MPU6050_IIC_Stop();
printf("等待ACK失败\r\n");
return 1;
}
}
MPU6050_IIC_Stop();
return 0;
}
/*
函数功能:IIC连续写
函数参数:
IIC连续读
addr:器件地址
reg:要读取的寄存器地址
len:要读取的长度
buf:读取到的数据存储区
返 回 值:0,成功,其他,错误代码
*/
u8 MPU6050_Read_Len(u8 addr,u8 reg,u8 len,u8 *buf)
{
MPU6050_IIC_Start();
MPU6050_IIC_Send_Byte((addr<<1)|0);//发送器件地址+写命令
if(MPU6050_IIC_Wait_Ack()) //等待应答
{
MPU6050_IIC_Stop();
return 1;
}
MPU6050_IIC_Send_Byte(reg); //写寄存器地址
MPU6050_IIC_Wait_Ack(); //等待应答
MPU6050_IIC_Start();
MPU6050_IIC_Send_Byte((addr<<1)|1);//发送器件地址+读命令
MPU6050_IIC_Wait_Ack(); //等待应答
while(len)
{
if(len==1)*buf=MPU6050_IIC_Read_Byte(0);//读数据,发送nACK
else *buf=MPU6050_IIC_Read_Byte(1); //读数据,发送ACK
len--;
buf++;
}
MPU6050_IIC_Stop(); //产生一个停止条件
return 0;
}
/*
函数功能:IIC写一个字节
函数参数:
reg:寄存器地址
data:数据
返 回 值:0,成功,其他,错误代码
*/
u8 MPU6050_Write_Byte(u8 reg,u8 data)
{
MPU6050_IIC_Start();
MPU6050_IIC_Send_Byte((MPU6050_ADDR<<1)|0);//发送器件地址+写命令
if(MPU6050_IIC_Wait_Ack()) //等待应答
{
MPU6050_IIC_Stop();
return 1;
}
MPU6050_IIC_Send_Byte(reg); //写寄存器地址
MPU6050_IIC_Wait_Ack(); //等待应答
MPU6050_IIC_Send_Byte(data);//发送数据
if(MPU6050_IIC_Wait_Ack()) //等待ACK
{
MPU6050_IIC_Stop();
return 1;
}
MPU6050_IIC_Stop();
return 0;
}
/*
函数功能:IIC读一个字节
函数参数:
reg:寄存器地址
data:数据
返 回 值:返回值:读到的数据
*/
u8 MPU6050_Read_Byte(u8 reg)
{
u8 res;
MPU6050_IIC_Start();
MPU6050_IIC_Send_Byte((MPU6050_ADDR<<1)|0);//发送器件地址+写命令
MPU6050_IIC_Wait_Ack(); //等待应答
MPU6050_IIC_Send_Byte(reg); //写寄存器地址
MPU6050_IIC_Wait_Ack(); //等待应答
MPU6050_IIC_Start();
MPU6050_IIC_Send_Byte((MPU6050_ADDR<<1)|1);//发送器件地址+读命令
MPU6050_IIC_Wait_Ack(); //等待应答
res=MPU6050_IIC_Read_Byte(0);//读取数据,发送nACK
MPU6050_IIC_Stop(); //产生一个停止条件
return res;
}
复制代码
6.5 GPS.c 代码
接收 GPS 数据之后进行解析,得到经纬度,方便上传到物联网云平台。
#include "gps.h"
/*
函数功能:从buf里面得到第cnt个逗号所在的位置
返 回 值:0~254,代表逗号所在位置的偏移.
255,代表不存在第cnt个逗号
*/
u8 GPS_GetCommaOffset(char *buf,u8 cnt)
{
char *p=buf;
while(cnt)
{
if(*buf=='*'||*buf<' '||*buf>'z')return 255;//遇到'*'或者非法字符,则不存在第cx个逗号
if(*buf==',')cnt--;
buf++;
}
return buf-p; //计算偏移量
}
/*
函数功能: 获取GPS经纬度数据值
函数参数:
double *Longitude :经度
double *latitude :纬度
返回值: 0表示定位成功,1表示定位失败
说明: 解析$GPRMC命令,得到经纬度
$GNRMC,023705.000,A,2842.4164,N,11549.5713,E,1.73,91.65,150319,,,A*41
转换公式示例:
经度: dddmm.mmmm 东经 11408.4790 114+(08.4790/60)=114.141317
纬度: ddmm.mmmm 北纬 2236.9453 22+(36.9453/60)= 22.615755
中科微返回的数据
$GNRMC,144435.000,A,2942.1201,N,10636.6466,E,1.50,64.42,190422,,,A*40
*/
u8 GPS_GPRMC_Decoding(u8 *gps_buffer,double *Longitude,double *latitude)
{
u8 Offset;
u32 int_data;
double s_Longitude,s_latitude;
char *p;
/*1. 确定下定位是否成功*/
p=strstr((char*)gps_buffer,"$GNRMC");
if(!p)return 1;
Offset=GPS_GetCommaOffset(p,2);
if(Offset==255)return 2;
if(*(p+Offset)!='A')return 3; //定位不准确
/*2. 得到纬度*/
Offset=GPS_GetCommaOffset(p,3);
if(Offset==255)return 4;
sscanf(p+Offset,"%lf",&s_latitude);
s_latitude=s_latitude/100;
int_data=s_latitude;//得到纬度整数部分
s_latitude=s_latitude-int_data;//得到纬度小数部分
s_latitude=(s_latitude)*100;
*latitude=int_data+(s_latitude/60.0); //得到转换后的值
/*3. 得到经度*/
Offset=GPS_GetCommaOffset(p,5);
if(Offset==255)return 5;
sscanf(p+Offset,"%lf",&s_Longitude);
s_Longitude=s_Longitude/100;
int_data=s_Longitude;//得到经度整数部分
s_Longitude=s_Longitude-int_data; //得到经度小数部分
s_Longitude=s_Longitude*100;
*Longitude=int_data+(s_Longitude/60.0);
return 0;
}
复制代码
7. 上位机 APP 开发
为了方便查看地图位置,轨迹等信息,当前采用 QT 编写了一个配套的上位机,通过华为云 IOT 的应用侧开发接口,获取设备的影子数据,然后再调用百度地图进行显示目标位置。接下来就介绍上位机软件的开发流程。
7.1 安装 Qt 开发环境
Qt 是个跨平台的 C++开发框架,一份代码支持在不同系统平台编译运行。支持 Android、IOS、Windows、Linux 等平台。目前我使用的开发环境是:QT 5.12.6 ,其他版本也可以的。
QT5.12.6 的下载地址:https://download.qt.io/archive/qt/5.12/5.12.6/
打开下载链接后选择下面的版本进行下载:
qt-opensource-windows-x86-5.12.6.exe 13-Nov-2019 07:28 3.7G Details
复制代码
软件安装时断网安装,否则会提示输入账户。安装的时候,第一个复选框里勾选一个 mingw 32 编译器即可,其他的不管默认就行,直接点击下一步继续安装。
如果只是在 windows 下开发,最简单的安装就只选择 MinGW 编译器即可,其他的编译器不用勾选。
7.2 应用侧开发接口文档
帮助文档地址: https://support.huaweicloud.com/usermanual-iothub/iot_01_0045.html当前我这个应用主要是读取设备上传的 GPS 数据即可,要得到数据有两种方式:【1】读取设备影子数据,也就是获取设备上传到服务器之后的历史数据(非实时数据)就是设备最后一次传上来的数据,获取影子数据,不需要关心设备是否在线都可以获取。我这里 GPS 获取是获取的影子设备数据,也就是得到设备最后一次上传的数据。地址:https://support.huaweicloud.com/api-iothub/iot_06_v5_0079.html
【2】(查询设备属性)读取实时数据,如果需要设备立即发布当前的数据上来,可以发送同步指令给设备,设备端需要编写解析指令的代码,收到指令后设备根据格式回应数据回来。这种同步实时数据需要设备保持在线才可以响应指令。 具体的使用场景可以根据自己需求设计。
通过查询属性的接口,可以主动请求获取设备详细属性。
流程是:应用层调用这个 API 接口---->请求服务器----->请求客户端设备------>返回给服务器----->返回给应用层调用处。
文档地址:https://support.huaweicloud.com/api-iothub/iot_06_v5_0034.html
7.3 创建 IAM 账户
这一步很重要,在开发上位机时,需要调用应用侧的一些接口,这些接口都需要带上 token 登录密匙。而 token 登录密匙的生成需要 IAM 账户才获取。
地址:https://console.huaweicloud.com/iam/?region=cn-north-4#/iam/users
(1)创建用户
(2)填充参数
(3)完成创建
7.4 实现代码
下面贴出请求接口的核心代码。
/*
功能: 获取token
*/
void Widget::GetToken()
{
//表示获取token
function_select=3;
QString requestUrl;
QNetworkRequest request;
//设置请求地址
QUrl url;
//获取token请求地址
requestUrl = QString("https://iam.%1.myhuaweicloud.com/v3/auth/tokens")
.arg(SERVER_ID);
//自己创建的TCP服务器,测试用
//requestUrl="http://10.0.0.6:8080";
//设置数据提交格式
request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json;charset=UTF-8"));
//构造请求
url.setUrl(requestUrl);
request.setUrl(url);
QString text =QString("{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":"
"{\"user\":{\"domain\": {"
"\"name\":\"%1\"},\"name\": \"%2\",\"password\": \"%3\"}}},"
"\"scope\":{\"project\":{\"name\":\"%4\"}}}}")
.arg(MAIN_USER)
.arg(IAM_USER)
.arg(IAM_PASSWORD)
.arg(SERVER_ID);
//发送请求
manager->post(request, text.toUtf8());
}
//获取影子设备数据
void Widget::on_pushButton_addr_clicked()
{
//表示影子设备数据获取
function_select=0;
QString requestUrl;
QNetworkRequest request;
//设置请求地址
QUrl url;
//获取token请求地址
requestUrl = QString("https://iotda.%1.myhuaweicloud.com/v5/iot/%2/devices/%3/shadow")
.arg(SERVER_ID)
.arg(PROJECT_ID)
.arg(Device_id);
//自己创建的TCP服务器,测试用
//requestUrl="http://10.0.0.6:8080";
//设置数据提交格式
request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json"));
//设置token
request.setRawHeader("X-Auth-Token",Token);
//构造请求
url.setUrl(requestUrl);
request.setUrl(url);
//发送请求
manager->get(request);
}
复制代码
主要贴出 2 个比较重要的函数,一个获取 token,一个查询设备影子数据。
评论