前言
这是我与同学一起做的一个关于物联网空气清新度控制系统的项目,我们经过了一两周的调试,也是完成了基础功能,虽然代码界面不是特别美观,但是只有实现了就行了,我们也没想要求有多高,就给大家展示一下这个做的小项目,整个项目包含前端以及后端,这里只介绍我负责的单片机部分,我们会放一个百度云盘链接给大家,希望对大家有帮助。
设计步骤及功能介绍
使用C语言,keli开发软件,stm32f103VET6,继电器,PM2.5检测模块,风扇,温湿度传感器,ESP8266模块,光照传感器。项目使用stm32f103vet6作为主控芯片,主要设计了实时检测采集PM2.5、光照数据以及温湿度数据,通过ESP8266模块发送AT指令与服务器进行MQTT协议通信后将数据上发服务器,处理服务器下发的控制指令,列如控制继电器开关风扇、开关灯、PWM控制风扇转数等,以及自动化控制,当检测到空气中的PM2.5浓度异常或者温湿度过高时,自动打开继电器控制风扇强度,根据光照强度开关灯,向服务器发送错误指令,断线重连等。
本文完整版代码标准库百度网盘链接:百度网盘 请输入提取码
提取码:ITO2
HAL库只做了初始化连接服务器:百度网盘 请输入提取码
提取码:IOT3
所包含元器件大家可以参考(不包含MCU)
实物图:
模块对应MCU引脚图
设计步骤及方法
因涉及代码过多这里只讲核心代码部分,想看完整的可以自行下载源码查看噢
1.PM2.5模块初始化
在usar2.c中对PM2.5模块初始化,根据厂家提供的数据手册得出只要该模块上电后就会自动从串口发送PM2.5的数据,所以我们只需在单片机中编写串口接收解析数据程序即可获取相应的数据,如图是串口接收到的数据处理:
float mConcentration;//PM2.5数据
float PH_ReadData(void)
{
if (Serial_GetRxFlag() == 1)
{
mConcentration = (Serial_RxPacket[3] + Serial_RxPacket[4] /10.0)*2/10;
memset(Serial_RxPacket,0,9);
}
return mConcentration;
}
2.光照传感器初始化
在adc.c文件中对光照传感器是由ADC采集,在单片机中进行ADC的初始化开启相应的通道即可对接光照传感器的引脚后自动采集数据,如图是处理ADC采集的数据:
u16 Get_Adc_Average(u8 ch,u8 times)
{
u32 temp_val=0;
u8 t;
for(t=0;t<times;t++)
{
temp_val+=Get_Adc(ch);
delay_ms(5);
}
return temp_val/times;
}
3.七针OLED显示屏初始化
在oled.c中对OLED的初始化,主要是由使用软件模拟SPI协议进行通信
4.LED灯初始化
在led.c对LED灯的初始化,设置PE0-PE3引脚的初始化对应开发板上的四个led灯
5.KEY初始化
在key.c中对按键的初始化使用,设置PE9-PE12引脚的初始化,引脚采用上拉输入,按键检测方法采用定时器7非阻塞式中断检测,定时器每秒的中断计算公式为:PSC*ARR/Ft=us,定时器的初始化设置为7200*100/72 = 10000us = 10ms中断一次,在中断函数中调用按键检测及按键处理函数,中断检测部分:
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //检查TIM的 更新 中断标志位
{
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
KEY_Proc();//按键检测
KEY_Sta();//按键控制部分
}
}
按键检测部分:
void KEY_Proc(void)
{
KeyPort[0].Key_Sta=GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_9);
KeyPort[1].Key_Sta=GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_10);
KeyPort[2].Key_Sta=GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_11);
KeyPort[3].Key_Sta=GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_12);
for(i=0;i<4;i++)
{
switch(KeyPort[i].Key_Now)
{
case 0://一次检测
{
if(KeyPort[i].Key_Sta==0)
{
KeyPort[i].Key_Now = 1;
}
}
break;
case 1://二次消抖
{
if(KeyPort[i].Key_Sta==0)
{
KeyPort[i].Key_Now = 2;
}
else
{
KeyPort[i].Key_Now = 0;
}
}
break;
case 2://三次设置标志位
{
if(KeyPort[i].Key_Sta==1)
{
KeyPort[i].Key_Now = 0;
KeyPort[i].Key_Short=1;
}
}
break;
}
}
}
按键处理函数包含打开灯及打开继电器:
void KEY_Sta(void)
{
if(KeyPort[0].Key_Short==1)
{
KeyPort[0].Key_Short=0;
Key_Issue1=!Key_Issue1;
if(Key_Issue1==1)
{
LED_Issue_Open(0);//开启第一路继电器及LED1
}
else if(Key_Issue1==0)
{
LED_Issue_Close(0);//关闭第一路继电器及LED1
}
printf("KEY0=%d\r\n",Key_Issue1);
}
if(KeyPort[1].Key_Short==1)
{
KeyPort[1].Key_Short=0;
Key_Issue2=!Key_Issue2;
if(Key_Issue2==1)
{
LED_Issue_Open(1);
}
else if(Key_Issue2==0)
{
LED_Issue_Close(1);
}
}
if(KeyPort[2].Key_Short==1)
{
KeyPort[2].Key_Short=0;
Key_Issue3=!Key_Issue3;
if(Key_Issue3==1)
{
LED_Issue_Open(2);
}
else if(Key_Issue3==0)
{
LED_Issue_Close(2);
}
}
if(KeyPort[3].Key_Short==1)
{
KeyPort[3].Key_Short=0;
Key_Issue4=!Key_Issue4;
if(Key_Issue4==1)
{
LED_Issue_Open(3);
}
else if(Key_Issue4==0)
{
LED_Issue_Close(3);
}
}
}
6. 风扇的初始化
在PWM.c文件中对风扇进行初始化,主要由PWM进行控制,采用TIM4通用定时器设置成PWM进行风扇的挡位调节,PSC设置为720-1,ARR设置为100-1,启用定时器4的通道三,对定时器4的通道3设置占空比即可对风扇的转数调节,代码里采用传参(0-100)的方式自由设置占空比:
void PWM_SetComp(uint8_t Comp3)
{
TIM_SetCompare3(TIM4,Comp3);
}
7.DHT11初始化
在dht11.c中对温湿度模块进行初始化,对应引脚为PE7
8.ESP8266初始化
主要由ESP8266.c及hp_mqtt.c这俩个文件来进行连接,采用串口5与ESP8266通信,通过上发相应的AT指令连接服务器,上发的连接参数,实现数据的收发,需要先给main.c文件中以下的宏做修改自己的参数
#define ServerIP "192.168.100.100"//服务器IP
#define Port "9001"//端口号
#define SSID "Redmi_9209_2.4G"//wifi名称
#define WIFIPassword "920992099"//wifi密码
#define DEV_Humi "humi" //设备名称
#define DEV_Temp "temp" //需要定义为用户自己的参数
#define DEV_EMR1 "EMR1" //需要定义为用户自己的参数
#define DEV_EMR2 "EMR2" //需要定义为用户自己的参数
#define DEV_EMR3 "EMR3" //需要定义为用户自己的参数
#define DEV_EMR4 "EMR4" //需要定义为用户自己的参数
#define DEV_ADC "LIGHT" //需要定义为用户自己的参数
#define DEV_PM "PM2.5" //需要定义为用户自己的参数
#define DEV_Speed "Speed" //需要定义为用户自己的参数
#define DEV_TX "reception" //需要定义为用户自己的参数
#define ProductKey "0zcm3w" //标识产品
#define ClientID "123456|0zcm3w" //客户端的标识
#define Username "0zcm3w" //用户ID
#define Password "c4b380f8e11e7d158a8b504dd259f72b" //设备密钥
#define Topic "sys/0zcm3w/control" //订阅主题
#define TopicPost "sys/0zcm3w/post" //订阅主题
ESP8266连接MQTT函数:
void WIFI_Init(void)
{
u8 res=1;
while(res)
{
OLED_ShowString(10,2,(u8 *)"WIFI init ..");
res=WIFI_Dect((u8 *)SSID,(u8 *)WIFIPassword);
delay_ms(200);
}
OLED_ShowString(10,2,(u8 *)"WIFI init OK!");
res=1;
while(res)
{
OLED_ShowString(10,4,(u8 *)"CONNECTED ...");
res=ESP8266_CONNECT_SERVER((u8 *)ServerIP,(u8 *)Port);
delay_ms(2000);
}
OLED_ShowString(10,4,(u8 *)"CONNECTED OK");
res=0;
OLED_Clear();
Welcome();
delay_ms(500);
while(!res)
{
OLED_ShowString(10,2,(u8 *)"Init MQTT ..");
_mqtt.Init(rxbuf,sizeof(rxbuf),txbuf,sizeof(txbuf));
res=_mqtt.Connect(
ClientID,//ClientID
Username,//Username
Password//Password
);
if(res!= 0){
OLED_ShowString(10,2,(u8 *)"Enter MQTT OK!");
printf("Enter MQTT OK!\r\n");
}
else{
OLED_ShowString(10,2,(u8 *)"Enter MQTT Error!");
printf("Enter MQTT Error!\r\n");
}
}
printf("连接服务器成功\r\n");
if(_mqtt.SubscribeTopic("sys/0zcm3w/control",0,1) != 0){
OLED_ShowString(10,4,(u8 *)"Subscribe OK!");
printf("SubscribeTopic OK!\r\n");
}
else{
OLED_ShowString(10,4,(u8 *)"Subscribe Error!");
printf("SubscribeTopic Error!\r\n");
}
OLED_Clear();
Welcome();//顶部欢迎界面
}
向上发数据主要是先将数据获取存入对应的缓冲区,组合成相应的HTTP协议包,连带订阅主题一并发送
sprintf(tempstr, "%d.%d",DHT11_Data.temp_int,DHT11_Data.temp_deci);
sprintf(humistr, "%d.%d",DHT11_Data.humi_int,DHT11_Data.humi_deci);
sprintf(EMR1str, "%d",GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_0));
sprintf(EMR2str, "%d",GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_1));
sprintf(EMR3str, "%d",GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_2));
sprintf(EMR4str, "%d",GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_3));
sprintf(ADCstr, "%d",adcx);//adc
sprintf(pmstr, "%.2f",PM_Data);
sprintf(Speed_str,"%d",Speed_TCS);
if(strlen(ESP8266_ReadNow)>0)
{
sprintf(Esp_Tx,"%s",ESP8266_ReadNow);
}
else if(Read_Flag==0)
{
sprintf(Esp_Tx,"%s","NULL");
}
HTTP_PostPkt(HTTP_Buf,DEV_Humi,DEV_Temp,DEV_ADC,DEV_PM,DEV_EMR1,DEV_EMR2,DEV_EMR3,DEV_EMR4,DEV_Speed,DEV_TX,
humistr,tempstr,ADCstr,pmstr,EMR1str,EMR2str,EMR3str,EMR4str,Speed_str,Esp_Tx);
_mqtt.PublishData(TopicPost,HTTP_Buf,0);
//printf("HTTP_Buf=%s",HTTP_Buf);
}
服务器下发控制,由主函数中检测在串口中解析处理数据是否接收完成,以 } 结尾,服务器下发数据格式为{LED0},需要用花括号里面才为数据,解析及服务器下发数据控制部分代码
if(USART_RX_ESP&0x4000)//0x8000判断有没有回车
{
ESP9266_ReadMode=1;
Read_Flag = 1;
ESP8266_ReadIssueData();//下发控制检测
}
void ESP8266_ReadIssueData(void)
{
u16 len,t;
u8 pRxPacket=0,Rxsta=0,RxData=0;
len=USART_RX_ESP&0x3fff;//得到此次接收到的数据长度
printf("\r\n您发送的消息为:\r\n\r\n");
sprintf(ESP8266_ReadData, "%s",USART_RX_ESPBUF);
printf("RX=%s\r\n",ESP8266_ReadData);
printf("长度为:%d\r\n",len);
printf("原始数据为:%s\r\n",USART_RX_ESPBUF);
printf("\r\n\r\n");//插入换行
USART_RX_ESP=0;
Rxsta = 0;
for(t=0;t<=len+1;t++)//寻找{}里的数据发送回服务器
{
RxData = ESP8266_ReadData[t];
switch(Rxsta)
{
case 0:
{
if (RxData == 0x7B)
{
Rxsta = 1;
pRxPacket = 0;
}
}
break;
case 1:
{
if (RxData==0x3A)
{
Rxsta = 2;
}
else if(RxData!=0x22&&RxData!= 0x7B)
{
ESP8266_ReadNow[pRxPacket] = ESP8266_ReadData[t];
pRxPacket ++;
}
}
break;
case 2:
{
Rxsta = 0;
break;
}
}
}
printf("您发送的解析数据为 %s \r\n",ESP8266_ReadNow);
strcpy(Esp_Tx,ESP8266_ReadNow);
if(strstr((const char*)ESP8266_ReadNow,"EMR1O"))//打开继电器第一路
{
LED_Issue_Open(0);
memset(ESP8266_ReadNow,0,100);
}
else if(strstr((const char*)ESP8266_ReadNow,"EMR1S"))//关闭继电器第一路
{
LED_Issue_Close(0);
memset(ESP8266_ReadNow,0,100);
}
else if(strstr((const char*)ESP8266_ReadNow,"EMR2O"))
{
LED_Issue_Open(1);
memset(ESP8266_ReadNow,0,100);
}
else if(strstr((const char*)ESP8266_ReadNow,"EMR2S"))
{
LED_Issue_Close(1);
memset(ESP8266_ReadNow,0,100);
}
else if(strstr((const char*)ESP8266_ReadNow,"EMR3O"))
{
LED_Issue_Open(2);
memset(ESP8266_ReadNow,0,100);
}
else if(strstr((const char*)ESP8266_ReadNow,"EMR3S"))
{
LED_Issue_Close(2);
memset(ESP8266_ReadNow,0,100);
}
else if(strstr((const char*)ESP8266_ReadNow,"EMR4O"))
{
LED_Issue_Open(3);
memset(ESP8266_ReadNow,0,100);
}
else if(strstr((const char*)ESP8266_ReadNow,"EMR4S"))
{
LED_Issue_Close(3);
memset(ESP8266_ReadNow,0,100);
}
else if(strstr((const char*)ESP8266_ReadNow,"LOW"))
{
memset(ESP8266_ReadNow,0,100);
Speed_TCS = 1;
PWM_SetComp(10);
}
else if(strstr((const char*)ESP8266_ReadNow,"MIDDLE"))
{
memset(ESP8266_ReadNow,0,100);
Speed_TCS = 2;
PWM_SetComp(50);
}
else if(strstr((const char*)ESP8266_ReadNow,"HIGH"))
{
memset(ESP8266_ReadNow,0,100);
Speed_TCS = 3;
PWM_SetComp(90);
}
memset(ESP8266_ReadNow,0,100);
memset(ESP8266_ReadData,0,100);
}
ESP8266断线重连,设置名为UpFlag标志位,因为我们这个服务器会一直返回数据,所以当服务器有返回数据时就清零,无返回的时候UpFlag++当次数够二十次的时候就会重新启动连接服务器函数,当然也可以用AT指令来检测
if(UpFlag==20&&strlen(USART_RX_ESPBUF)==0)//采集二十次数据任然无法回收服务器数据
{
WIFI_Init();//重新连接服务器
ESP9266_ReadMode=1;
UpFlag = 0;
}
else if(strlen(USART_RX_ESPBUF))//如果有数据
{
UpFlag = 0;//清空计数
memset(USART_RX_ESPBUF,0,100);//清空缓冲区
}
演示视频
物联网空气清新度控制系统演示视频