作为入门本篇只实现微信小程序接收下位机上传的数据,之后会持续发布如下项目:①可以实现微信小程序控制下位机动作,真正意义上的智能家居;②将网络通讯协议换成MQTT协议再实现上述功能,此时的服务器也不再是ONENET,可以是公用的MQTT服务器也可以自己搭建或者租
这个项目参考的是b站up主:彼岸有光我们有船
最终效果
注意:本文章参考意义不大了,因为服务器已经升级,接口已经改变,想要裸机实现智能家居的,并且使用ONENET服务器的,就看本系列的第七篇文章。
实物图:
主控是STM32F103C8T6,这里arduino开发板我只是拿来给几个模块供电的,有面包板的话也可以用面包板,用到的模块有:MQ-4天然气传感器、MQ-9可燃气体传感器、0.96寸oled液晶屏、DHT11温湿度传感器、Esp8266-01s、J-Link下载器。
ONENET云平台:
微信小程序:
一、下位机模块测试与分析
1、MQ系列传感器
MQ气体传感器使用的气敏材料是在清洁空气中电导率较低的二氧化锡。当传感器所处环境中存在可燃气体时,传感器的电导率随空气中可燃气体浓度的增加而增大。使用简单的电路即可将电导率的变化转换为与该气体浓度相对应的输出信号。MQ气体传感器对甲烷的灵敏度高,对丙烷、丁烷也有较好的灵敏度。这种传感器可检测多种可燃性气体,特别是天然气。
关于这个传感器的详细资料可以下载阅读:我用夸克网盘分享了「MQ-2-135-3-7-9烟雾空气敏酒精氢一氧化碳可燃液化传感器模块探头.rar」,点击链接即可保存。打开「夸克APP」,无需下载在线播放视频,畅享原画5倍速,支持电视投屏。
链接:https://pan.quark.cn/s/22c08247dd8a
提取码:xLRC
在这个项目中只需要接三个引脚:VCC、GND、AO。AO输出接开发板的IO口,通过ADC将传感器的模拟输出转换成数字量。这里用到了开发板上ADC1的通道2、3,对应GPIOA-2、GPIOA-3。 关于 ADC的使用可以直接看视频:
https://www.bilibili.com/video/BV1th411z7sn/?p=21&spm_id_from=pageDriver&vd_source=2a10d30b8351190ea06d85c5d0bfcb2a
下面是多通道ADC源码,如果想再加的话只需要在初始化gpio的时候加上需要的io口即可,但是需要对应io口与adc通道的对应关系:
#include "stm32f10x.h" // Device header
void AD_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
ADC_InitTypeDef ADC_InitStructure; //定义结构体变量
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
/*GPIO初始化*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0、PA1、PA2和PA3引脚初始化为模拟输入
/*不在此处配置规则组序列,而是在每次AD转换前配置,这样可以灵活更改AD转换的通道*/
/*ADC初始化*/
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式,选择独立模式,即单独使用ADC1
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐,选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发,使用软件触发,不需要外部触发
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //连续转换,失能,每转换一次规则组序列后停止
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //扫描模式,失能,只转换规则组的序列1这一个位置
ADC_InitStructure.ADC_NbrOfChannel = 1; //通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1
ADC_Init(ADC1, &ADC_InitStructure); //将结构体变量交给ADC_Init,配置ADC1
/*ADC使能*/
ADC_Cmd(ADC1, ENABLE); //使能ADC1,ADC开始运行
/*ADC校准*/
ADC_ResetCalibration(ADC1); //固定流程,内部有电路会自动执行校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
}
/**
* 函 数:获取AD转换的值
* 参 数:ADC_Channel 指定AD转换的通道,范围:ADC_Channel_x,其中x可以是0/1/2/3
* 返 回 值:AD转换的值,范围:0~4095
*/
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5); //在每次转换前,根据函数形参灵活更改规则组的通道1
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发AD转换一次
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); //等待EOC标志位,即等待AD转换结束
return ADC_GetConversionValue(ADC1); //读数据寄存器,得到AD转换的结果
}
2、DHT11温湿度传感器
1、DHT11 采用单总线协议与单片机通信,概括起来是两个大过程:配对和数据传输,下面对两个过程进行分析:
①配对过程
(1)Data引脚在默认状态时处于高电平;
(2)在开始通信时,MCU将Data引脚拉低并保持18ms,然后再将Data引脚拉高20-40us;
(3)当DHT11收到命令后,它会主动拉低Data引脚,持续80us;
(4)DHT11再次拉高DATA引脚,80us后开始发送数据给MCU。
②数据传输
(1)在每次发送数据之前,DHT11会把Data引脚先拉低50us,这表示单片机要继续发送下一位数据;
(2)DHT11拉高Data引脚,如果拉高持续时间是26-28us,表示发送0;如果拉高的持续时间是116-118us,表示发送1。
2、驱动代码:
①c文件:
#include "dht11.h"
#include "delay.h"
//复位DHT11
void DHT11_Rst(void)
{
DHT11_IO_OUT(); //SET OUTPUT
DHT11_DQ_OUT=0; //拉低DQ
delay_ms(20); //拉低至少18ms
DHT11_DQ_OUT=1; //DQ=1
delay_us(30); //主机拉高20~40us
}
//等待DHT11的回应
//返回1:未检测到DHT11的存在
//返回0:存在
u8 DHT11_Check(void)
{
u8 retry=0;
DHT11_IO_IN();//SET INPUT
while (DHT11_DQ_IN&&retry<100)//DHT11会拉低40~80us
{
retry++;
delay_us(1);
};
if(retry>=100)return 1;
else retry=0;
while (!DHT11_DQ_IN&&retry<100)//DHT11拉低后会再次拉高40~80us
{
retry++;
delay_us(1);
};
if(retry>=100)return 1;
return 0;
}
//从DHT11读取一个位
//返回值:1/0
u8 DHT11_Read_Bit(void)
{
u8 retry=0;
while(DHT11_DQ_IN&&retry<100)//等待变为低电平
{
retry++;
delay_us(1);
}
retry=0;
while(!DHT11_DQ_IN&&retry<100)//等待变高电平
{
retry++;
delay_us(1);
}
delay_us(40);//等待40us
if(DHT11_DQ_IN)return 1;
else return 0;
}
//从DHT11读取一个字节
//返回值:读到的数据
u8 DHT11_Read_Byte(void)
{
u8 i,dat;
dat=0;
for (i=0;i<8;i++)
{
dat<<=1;
dat|=DHT11_Read_Bit();
}
return dat;
}
//从DHT11读取一次数据
//temp:温度值(范围:0~50°)
//humi:湿度值(范围:20%~90%)
//返回值:0,正常;1,读取失败
u8 DHT11_Read_Data(u8 *temp,u8 *humi)
{
u8 buf[5];
u8 i;
DHT11_Rst();
if(DHT11_Check()==0)
{
for(i=0;i<5;i++)//读取40位数据
{
buf[i]=DHT11_Read_Byte();
}
if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4])
{
*humi=buf[0];
*temp=buf[2];
}
}else return 1;
return 0;
}
//初始化DHT11的IO口 DQ 同时检测DHT11的存在
//返回1:不存在
//返回0:存在
u8 DHT11_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能PA端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //PA0端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化IO口
GPIO_SetBits(GPIOA,GPIO_Pin_8); //PA0 输出高
DHT11_Rst(); //复位DHT11
return DHT11_Check();//等待DHT11的回应
}
②头文件:
#ifndef __DHT11_H
#define __DHT11_H
#include "sys.h"
//IO方向设置
#define DHT11_IO_IN() {GPIOA->CRH&=0XFFFFFFF0;GPIOA->CRH|=8;}
#define DHT11_IO_OUT() {GPIOA->CRH&=0XFFFFFFF0;GPIOA->CRH|=3;}
IO操作函数
#define DHT11_DQ_OUT PAout(8) //数据端口 PA0出方向
#define DHT11_DQ_IN PAin(8) //数据端口 PA0入方向
u8 DHT11_Init(void);//初始化DHT11
u8 DHT11_Read_Data(u8 *temp,u8 *humi);//读取温湿度
u8 DHT11_Read_Byte(void);//读出一个字节
u8 DHT11_Read_Bit(void);//读出一个位
u8 DHT11_Check(void);//检测是否存在DHT11
void DHT11_Rst(void);//复位DHT11
#endif
主函数中直接调用DHT11_Read_Data,定义两个变量接收即可。
3、Esp8266-01s
在使用单片机连接此模块前最好先进行测试,测试可以参考之前的一篇博客(这篇博客还有如何在ONENET云平台创建产品和设备):https://blog.csdn.net/m0_71523511/article/details/135887108
驱动代码:
#include "esp8266.h"
char *str[4] = {"POST /devices/1038xxxxxxxx/datapoints HTTP/1.1",
"api-key:wfsF4bCGtQIQmW=xxxxxxxx",
"Host:api.heclouds.com",
""};
char strValue[8] = {0};
// 向onenet发送数据
u8 *esp8266_str_data(char *key, char *value)
{
u8 i;
u8 *back;
char temp[512];
char temp3[64]; // 长度
char temp5[128]; // 发送值
// 拼接post报文
strcpy(temp5, "{\"datastreams\":[{\"id\":\"");
strcat(temp5, key);
strcat(temp5, "\",\"datapoints\":[{\"value\":");
strcat(temp5, value);
strcat(temp5, "}]}]}");
strcpy(temp3, "Content-Length:");
sprintf(temp, "%d", strlen(temp5) + 1);
strcat(temp3, temp);
strcpy(temp, "");
for (i = 0; i < 3; i++)
{
strcat(temp, str[i]);
strcat(temp, "\r\n");
}
strcat(temp, temp3);
strcat(temp, "\r\n\r\n");
strcat(temp, temp5);
strcat(temp, "\r\n");
back = esp8266_send_data((u8 *)temp, 50);
// printf("server:%s\r\n", back);
if (strstr((char *)back, "ERROR")) //发送失败, 重新初始化,发送
{
esp8266_send_cmd("AT+RST", "OK", 50);
esp8266_send_cmd("AT+CIPCLOSE", "OK", 50);
esp8266_send_cmd("AT+CWMODE=1", "OK", 50);
esp8266_send_cmd("AT+CWDHCP=1", "OK", 50);
//esp8266_send_cmd("AT+CIPMUX=0", "OK", 50);
while (esp8266_send_cmd("AT+CIPSTART=\"TCP\",\"183.230.40.33\",80", "CONNECT", 100));
//esp8266_send_cmd("AT+CIPMODE=1", "OK", 50);
//esp8266_send_cmd("AT+CIPSEND", "OK", 20);
return esp8266_send_data((u8 *)temp, 50);
}
return back;
}
// 向esp8266请求数据
u16 esp8266_get_data(char *vStr)
{
u8 i;
u16 value = 0;
char *back;
char temp[160] = "GET /devices/1038269453/datastreams/";
// 拼接请求报文
strcat(temp, vStr);
strcat(temp, " HTTP/1.1\r\n");
for (i = 1; i < 4; i++)
{
strcat(temp, str[i]);
strcat(temp, "\r\n");
}
// 发送报文, 获取返回字符串
back = (char *)esp8266_send_data((u8 *)temp, 50);
// 在回送报文中截取出数值
back = strchr(strstr(back, "\"current_value\":"), ':') + 1;
while (*back != '}')
{
if(*back == '\"'){
back++;
continue;
}
value = value * 10 + (*back - '0');
back++;
}
return value;
}
//ESP8266模块和PC进入透传模式
void esp8266_start_trans(void)
{
//让Wifi模块重启的命令
esp8266_send_cmd("AT+RST", "OK", 50);
esp8266_send_cmd("AT+CIPCLOSE", "OK", 50);
esp8266_send_cmd("AT+CWMODE=1", "OK", 50);
esp8266_send_cmd("AT+CWDHCP=1", "OK", 50);
delay_ms(1000); //延时2S等待重启成功
delay_ms(1000);
//让模块连接上自己的路由WIFI GOT IP
while (esp8266_send_cmd("AT+CWJAP=\"WZQ\",\"1234567890\"", "WIFI GOT IP", 500)){
delay_ms(1);
};
//建立TCP连接 这四项分别代表了 要连接的ID号0~4 连接类型 远程服务器IP地址 远程服务器端口号
while (esp8266_send_cmd("AT+CIPSTART=\"TCP\",\"183.230.40.33\",80", "CONNECT", 200)){
delay_ms(1);
};
}
//ESP8266退出透传模式 返回值:0,退出成功;1,退出失败
//配置wifi模块,通过想wifi模块连续发送3个+(每个+号之间 超过10ms,这样认为是连续三次发送+)
u8 esp8266_quit_trans(void)
{
u8 result = 1;
u3_printf("+++");
delay_ms(1000); //等待500ms太少 要1000ms才可以退出
result = esp8266_send_cmd("AT", "OK", 20); //退出透传判断.
if (result)
printf("quit_trans failed!");
else
printf("quit_trans success!");
return result;
}
//向ESP8266发送命令
//cmd:发送的命令字符串;ack:期待的应答结果,如果为空,则表示不需要等待应答;waittime:等待时间(单位:10ms)
//返回值:0,发送成功(得到了期待的应答结果);1,发送失败
u8 esp8266_send_cmd(u8 *cmd, u8 *ack, u16 waittime)
{
u8 res = 0;
USART3_RX_STA = 0;
u3_printf("%s\r\n", cmd); //发送命令
delay_ms(1);
if (ack && waittime) //需要等待应答
{
while (--waittime) //等待倒计时
{
delay_ms(10);
if (USART3_RX_STA&0X8000) //接收到期待的应答结果
{
if (esp8266_check_cmd(ack))
{
printf("%s\r\n", (u8 *)USART3_RX_BUF);
break; //得到有效数据
}
USART3_RX_STA = 0;
//strcpy((char *)USART3_RX_BUF, ""); // 清空接收缓存区
}
}
if (waittime == 0) res = 1;
}
return res;
}
//ESP8266发送命令后,检测接收到的应答
//str:期待的应答结果
//返回值:0,没有得到期待的应答结果;其他,期待应答结果的位置(str的位置)
u8 *esp8266_check_cmd(u8 *str)
{
char *strx = 0;
if (USART3_RX_STA & 0X8000) //接收到一次数据了
{
USART3_RX_BUF[USART3_RX_STA & 0X7FFF] = 0; //添加结束符
strx = strstr((const char *)USART3_RX_BUF, (const char *)str);
}
return (u8 *)strx;
}
//向ESP8266发送数据
//cmd:发送的命令字符串;waittime:等待时间(单位:10ms)
//返回值:发送数据后,服务器的返回验证码
u8 *esp8266_send_data(u8 *cmd, u16 waittime)
{
char temp[1024];
char *ack = temp;
USART3_RX_STA = 0;
u3_printf("%s", cmd); //发送命令
delay_ms(1);
if (waittime) //需要等待应答
{
while (--waittime) //等待倒计时
{
delay_ms(10);
if (USART3_RX_STA & 0X8000) //接收到期待的应答结果
{
USART3_RX_BUF[USART3_RX_STA & 0X7FFF] = 0; //添加结束符
ack = (char *)USART3_RX_BUF;
USART3_RX_STA = 0;
break; //得到有效数据
}
}
}
return (u8 *)ack;
}
// 将数字转为字符串
void numToString(u16 value)
{
int k = 0, j = 0;
int num = (int)value;
char tem[10];
if (value == 0)
{
strValue[0] = '0';
strValue[1] = '\0';
return;
}
while (num)
{
tem[k++] = num % 10 + '0'; //将数字加字符0就变成相应字符
num /= 10; //此时的字符串为逆序
}
tem[k] = '\0';
k = k - 1;
while (k >= 0)
{
strValue[j++] = tem[k--]; //将逆序的字符串转为正序
}
strValue[j] = '\0'; //字符串结束标志
}
需要注意的是这段代码有两个地方需要修改:
分别换成自己云平台的设备ID和master-keyapi。
4、oled液晶屏
这是调试程序的好帮手,用的好可以很快找出程序是哪里出问题了。这个驱动代码网上都有很多封装好的,这里就不贴出来了。本项目用的是四引脚oled,使用IIC通信协议,IIC协议的原理可以看此视频:https://www.bilibili.com/video/BV1th411z7sn/?p=31&spm_id_from=pageDriver&vd_source=2a10d30b8351190ea06d85c5d0bfcb2a
想连接oled的详细代码可以看此视频:
https://www.bilibili.com/video/BV1EN41177Pc/?spm_id_from=333.337.search-card.all.click&vd_source=2a10d30b8351190ea06d85c5d0bfcb2a
二、微信小程序
微信小程序最关键的地方就是与云平台的数据交互,其他比如界面、功能都是在这个的基础上才有用。对微信小程序开发感兴趣的可以学一下javascript,比较简单。
下载文章末尾的开源项目压缩包,解压之后可以看到里面有一个文件夹叫:基于STM32的环境信息采集_微信小程序,打开微信开发者工具,选择导入,选择此小程序文件夹打开即可。
进入工程之后修改设备ID和master-keyapi:
index.js代码:
Page({
data: {
temp:0
},
// 事件处理函数
getinfo(){
var that = this
wx.request({
url: "https://api.heclouds.com/devices/1038269453/datapoints",
//将请求行中的数字换成自己的设备ID
header: {
"api-key": "wfsF4bCGtQIQmW=3wTsPnrdjuFA=" //自己的api-key
},
method: "GET",
success: function (e) {
console.log("获取成功",e)
that.setData({
temp:e.data.data.datastreams[2].datapoints[0].value,
humi:e.data.data.datastreams[7].datapoints[0].value,
gas_ch4:e.data.data.datastreams[0].datapoints[0].value,
ranqi:e.data.data.datastreams[4].datapoints[0].value
})
console.log("temp==",that.data.temp),
console.log("humi==",that.data.humi),
console.log("gas==",that.data.gas_ch4),
console.log("ranqi==",that.data.ranqi)
}
});
},
onLoad() {
var that = this
setInterval(function(){
that.getinfo()
},5000)
}
})