Appearance
项目:远程温湿度检测系统
前言
之前我们做了温湿度监测小项目,只是把温湿度数据上传到 OneNET,现在我们来优化这个小项目,让它更完整,更人性。我们在原有的基础上加上蜂鸣器和 LED 灯,当温湿度正常,蜂鸣器不报警,LED 灯不亮;当温湿度超出设定阈值时蜂鸣器报警,LED 灯闪烁。是不是有电影里保险柜红外检测到人偷窃,红灯连闪,报警声长鸣那感觉了。
1. 源码下载及前置阅读
- STM32F103C8T6模板工程
链接:https://pan.baidu.com/s/1n7XHCaMYtASWdJH2uA5yDA?pwd=lw59 提取码:lw59
- 本文的源码
链接:https://pan.baidu.com/s/1kPbLTC5HMRXCwqTDCwVORA?pwd=df4c 提取码:df4c
如果你是嵌入式开发小白,那么建议你先读读下面几篇文章。
前期教程,没看过的小伙伴可以先看下。
2. 整体系统设计
使用 STM32 作主控,配合 DHT11 温湿度传感器,实时监测周围环境的温湿度变化。通过 ESP8266 模块以 MQTT 协议将获取到的温湿度数据通过无线网络连接上传至 OneNET 平台,以便用户可以随时随地通过手机或电脑查看数据。温湿度如有异常,蜂鸣器报警,LED 灯闪烁。
3. 硬件选型
本教程使用的硬件如下:
- 单片机:STM32F103C8T6
这款单片机具有 64K flash,20K RAM,4 个定时器,3 个串口,网络上资料好几吨,非常适合初学者入门,强烈推荐。
- WiFi模块:ESP-01S(ESP8266)
ESP8266 可以利用串口与单片机进行通讯,从而编程实现控制。
- 温湿度传感器:DHT11
DHT11 有 3 脚和 4 脚两款,在使用上没有差别,接线都一样,主要接三根,四脚的款式有一脚悬空。四脚款接杜邦线会有点不稳,适合插面包板或开发板上。
DHT11 工作参数:
- 湿度测量范围:20~90%RH
- 湿度测量精度:±5%RH
- 温度测量范围:0~50℃
- 温度测量精度:±2℃
- 工作电压:DC 3.3V/5V
- 蜂鸣器
蜂鸣器广泛应用于各种电子设备中,例如警报器、计时器、电子钟、雷达等。它们可以用来进行提醒、报警、指示等,通过发出特定的声音频率来引起用户的注意。可分为无源蜂鸣器和有源蜂鸣器。
- LED灯
LED灯是一种半导体光源,具有高效、耐用和可靠的特点,被广泛应用于各种电子设备和照明系统中,为我们的生活和工作提供了可靠的光源。它可以发出多种颜色的光,如下模块就有红、黄、蓝、绿、白的颜色,3.3~5V 的电压范围内供电。
- 串口:USB 转 TTL
这种设备主要作用是用来调试或下载程序,本文用于串口输出作调试。价格也很便宜,普遍 5~8 元。
- 烧录器:ST-LINK V2
ST-Link 是一种用于 STM32 微控制器的调试和编程工具,它可以通过 SWD 或 JTAG 接口与开发板进行通信。本文用做烧录。一般也很便宜,七八元左右。
4. OneNET物联网平台
我们上一篇已经配置过 OneNET 了,这里就不从头教了,没看的同学可以去看看【MQTT+DHT11链接】
4.1 OneNET配置
书接上回,我们需要在之前的基础上创建以下物模型:
- 当前湿度,用于存储实时湿度数据。
- 当前温度,用于存储实时温度数据。
- 湿度上限,用于显示设定的湿度上限,湿度高于上限就报警。
- 温度上限,用于显示设定的温度上限,温度高于上限就报警。
- 湿度下限,用于显示设定的湿度下限,湿度低于下限就报警。
- 温度下限,用于显示设定的温度下限,温度低于下限就报警。
- 湿度告警,若湿度超出阈值即记录事件。
- 温度告警,若温度超出阈值即记录事件。
4.2 MQTT三元组
「MQTT 三元组」的获取方式也在上一篇详细介绍过了,这边不赘述啦,直接给出,没看的同学可以去看看【MQTT+DHT11链接】
- 设备 ID:temp01
- 产品 ID:P2k4KV0low
- token:version=2018-10-31&res=products%2FP2k4KV0low%2Fdevices%2Ftemp01&et=2017881776&method=sha1&sign=M3jVJvfeFLnggMrUPhYm5uRirXs%3D
5. STM32设备端开发
5.1 硬件接线
接线可参照下表:
ESP8266 | DHT11 | 蜂鸣器 | LED | STM32 | USB 转 TTL |
---|---|---|---|---|---|
3V3 | 3.3 | ||||
TX | A3 | ||||
RX | A2 | ||||
GND | G | ||||
VCC | 3.3 | ||||
DATA | B9 | ||||
GND | G | ||||
I/C | C13 | ||||
IN | A6 | ||||
A10 | TX | ||||
A9 | RX | ||||
G | GND |
烧录的时候接线如下表,如果不会烧录的话可以看我之前的文章【STM32下载程序的五种方法】。
ST-Link V2 | STM32 |
---|---|
SWCLK | SWCLK |
SWDIO | SWDIO |
GND | GND |
3.3V | 3V3 |
5.2 设备实物图
接好如下图,我用的是正点原子的开发板,大家用别的板子也是一样可以的,只要是 STM32F103C8T6 就行。
5.3 DHT11温湿度传感器代码
详细代码解析可以看手把手教你玩转DHT11(原理+驱动)。
5.4 ESP8266模块代码
详细代码解析可以看手把手教你玩转ESP8266(原理+驱动)。
5.5 MQTT代码
CONNECT 报文和 PUBLISH 报文已经在上一篇讲过,没看的同学可以去看看【MQTT+DHT11链接】
我们讲新增的。
5.5.1 SUBSCRIBE 报文
SUBSCRIBE 报文的编写及发送代码,报文编写就按照我们 万字猛文:MQTT原理及案例 的理论编写即可,报文内容:82+剩余长度+标识符+L+主题过滤器+QoS。
C
uint8_t mqtt_subscribe_topic(char *topic,uint8_t qos,uint8_t whether)
{
uint8_t 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
{
uint8_t 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++] = 0x01; //消息标识符 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_send_data(mqtt_txbuf,mqtt_txlen);
for(j=0;j<10;j++)
{
delay_ms(50);
if (esp8266_wait_receive() == ESP8266_EOK)
esp8266_copy_rxdata((char *)mqtt_rxbuf);
if(mqtt_rxbuf[0]==parket_subAck[0] && mqtt_rxbuf[1]==parket_subAck[1]) //订阅成功
{
return 0;//订阅成功
}
}
}
return 1; //失败
}
5.5.2 PINGREQ 报文
心跳请求就很简单,ESP8266 发送“C0 D0”即可。
c
const uint8_t parket_heart[] = {0xc0,0x00};
void mqtt_send_heart(void)
{
mqtt_send_data((uint8_t *)parket_heart,sizeof(parket_heart));
}
void mqtt_send_data(uint8_t *buf,uint16_t len)
{
esp8266_send_data((char *)buf, len);
}
5.5.3 接收句柄
接收 OneNET 发来的数据,去掉我们不需要的固定报头和可变报头,只保存有效载荷。
c
uint8_t mqtt_receive_handle(uint8_t *data_received, Mqtt_RxData_Type *rx_data)
{
uint8_t *p;
uint8_t encodeByte = 0;
uint32_t multiplier = 1, Remaining_len = 0;
uint8_t QS_level = 0;
p = data_received;
memset(rx_data, 0, sizeof(Mqtt_RxData_Type));
//解析接收数据
if((*p != 0x30)&&(*p != 0x32)&&(*p != 0x34)) //不是发布报文头
return 1;
if(*p != 0x30) QS_level = 1; //标记qs等级不为0
p++;
//提取剩余数据长度
do{
encodeByte = *p++;
Remaining_len += (encodeByte & 0x7F) * multiplier;
multiplier *= 128;
if(multiplier > 128*128*128) //超出剩余长度最大4个字节的要求,错误
return 2;
}while((encodeByte & 0x80) != 0);
//提取主题数据长度
rx_data->topic_len = *p++;
rx_data->topic_len = rx_data->topic_len * 256 + *p++;
//提取主题
memcpy(rx_data->topic,p,rx_data->topic_len);
p += rx_data->topic_len;
if(QS_level != 0) //跳过报文标识符
p += 2;
//提取payload
rx_data->payload_len = Remaining_len - rx_data->topic_len - 2;
memcpy(rx_data->payload, p, rx_data->payload_len);
return 0;
}
5.6 数据上报
将我们得到的温湿度数据和设定的温湿度阈值通过 PUBLISH 报文上报到 OneNET。
c
void status_post_task(void)
{
if(!status_post_flag)
return;
status_post_flag = 0;
printf("\r\n~~~~~~~~主动上报系统状态~~~~~~~~\r\n");
memset(data_send_buff, 0, sizeof(data_send_buff));
sprintf((char *)data_send_buff,"{\"id\":\"1386772172\",\"version\":\"1.0\",\"params\":{\"CurrentTemperature\":{\"value\":%d},\"CurrentHumidity\":{\"value\":%d},\"MaxTempSet\":{\"value\":%d},\"MiniTempSet\":{\"value\":%d},\"MaxHumSet\":{\"value\":%d},\"MiniHumSet\":{\"value\":%d}}}",
temperature, humidity, temp_upper_limit, temp_lower_limit, humi_upper_limit, humi_lower_limit);
mqtt_publish_data(POST_TOPIC, (char *)data_send_buff, 0);
printf("%s\r\n", data_send_buff);
printf("~~~~~~~~上报结束~~~~~~~~\r\n");
}
5.7 超出阈值报警
当 alarm_post_flag 为1,则表示超出设定的温/湿度的上/下限,向 OneNET 和串口发布报警
c
void alarm_post_task(void)
{
if(!alarm_post_flag)
return;
alarm_post_flag = 0;
if(alarm_flag){
printf("\r\n~~~~~~~~上报告警状态~~~~~~~~\r\n");
memset(data_send_buff, 0, sizeof(data_send_buff));
sprintf((char *)data_send_buff,"{\"id\":\"1386772172\",\"version\":\"1.0\",\"params\":{\"TempAlarm\":{\"value\":{\"high\":%d,\"low\":%d}},\"HumAlarm\":{\"value\":{\"high\":%d,\"low\":%d}}}}",
alarm_status.temp_upper_alarm, alarm_status.temp_lower_alarm, alarm_status.humi_upper_alarm, alarm_status.humi_lower_alarm);
mqtt_publish_data(EVENT_PUBLISH_TOPIC, (char *)data_send_buff, 0);
printf("%s\r\n", data_send_buff);
printf("~~~~~~~~上报结束~~~~~~~~\r\n");
}
}
5.8 定时任务
将我们需要用到的蜂鸣器和 LED 报警、DHT11读取、数据上报、心跳请求、接收数据、报警上传的这几个任务定时。
c
void systick_isr(void)
{
// sys任务,每1000ms运行一次
if(sys_task_cnt < 1000)
sys_task_cnt++;
else{
sys_task_cnt = 0;
if(alarm_flag){ /* 如果温湿度异常 */
BEEP_TOGGLE(); /* BEEP状态翻转 */
LED2_TOGGLE(); /* LED状态翻转 */
}else{
BEEP(0);
LED1(1);
}
}
// DHT11任务,每1000ms运行一次
if(dht11_task_cnt < 1000)
dht11_task_cnt++;
else{
dht11_task_cnt = 0;
dht11_update_flag = 1;
}
// MQTT数据上报任务,每30s运行一次
if(status_post_task_cnt < 10000)
status_post_task_cnt++;
else{
status_post_task_cnt = 0;
status_post_flag = 1;
}
// MQTT心跳任务,每30s运行一次
if(heart_task_cnt < 30000)
heart_task_cnt++;
else{
heart_task_cnt = 0;
heart_send_flag = 1;
}
// MQTT接收任务,每30ms运行一次
if(mqtt_receive_task_cnt < 30)
mqtt_receive_task_cnt++;
else{
mqtt_receive_task_cnt = 0;
mqtt_receive_flag = 1;
}
// 告警上传接收任务,每15s运行一次
if(alarm_post_task_cnt < 15000)
alarm_post_task_cnt++;
else{
alarm_post_task_cnt = 0;
alarm_post_flag = 1;
}
}
5.9 蜂鸣器逻辑代码
蜂鸣器的代码简简单单,这里就不赘述了,下面是蜂鸣器初始化代码。
C
void beep_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
BEEP_GPIO_CLK_ENABLE(); /* BEEP时钟使能 */
gpio_init_struct.Pin = BEEP_GPIO_PIN; /* 蜂鸣器引脚 */
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(BEEP_GPIO_PORT, &gpio_init_struct); /* 初始化蜂鸣器引脚 */
BEEP(0); /* 关闭蜂鸣器 */
}
蜂鸣器的 .h文件:
C
#ifndef __BEEP_H
#define __BEEP_H
#include "sys.h"
/******************************************************************************************/
/* 引脚 定义 */
#define BEEP_GPIO_PORT GPIOC
#define BEEP_GPIO_PIN GPIO_PIN_13
#define BEEP_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PB口时钟使能 */
/******************************************************************************************/
/* 蜂鸣器控制 */
#define BEEP(x) do{ x ? \
HAL_GPIO_WritePin(BEEP_GPIO_PORT, BEEP_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(BEEP_GPIO_PORT, BEEP_GPIO_PIN, GPIO_PIN_RESET); \
}while(0)
/* BEEP状态翻转 */
#define BEEP_TOGGLE() do{ HAL_GPIO_TogglePin(BEEP_GPIO_PORT, BEEP_GPIO_PIN); }while(0) /* BEEP = !BEEP */
void beep_init(void); /* 初始化蜂鸣器 */
#endif
5.10 LED逻辑代码
LED 灯的代码也简简单单,我们只用了 LED1,所以只设置了 LED1 的输出模式。
C
void led_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
LED1_GPIO_CLK_ENABLE(); /* LED1时钟使能 */
LED2_GPIO_CLK_ENABLE(); /* LED2时钟使能 */
LED3_GPIO_CLK_ENABLE(); /* LED3时钟使能 */
gpio_init_struct.Pin = LED1_GPIO_PIN; /* LED1引脚 */
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(LED1_GPIO_PORT, &gpio_init_struct); /* 初始化LED1引脚 */
gpio_init_struct.Pin = LED2_GPIO_PIN; /* LED2引脚 */
HAL_GPIO_Init(LED2_GPIO_PORT, &gpio_init_struct); /* 初始化LED2引脚 */
gpio_init_struct.Pin = LED3_GPIO_PIN; /* LED3引脚 */
HAL_GPIO_Init(LED3_GPIO_PORT, &gpio_init_struct); /* 初始化LED3引脚 */
LED1(0); /* 关闭 LED1 */
LED2(0); /* 关闭 LED2 */
LED3(0); /* 关闭 LED3 */
}
LED 的 .h文件:
C
#ifndef _LED_H
#define _LED_H
#include "sys.h"
/******************************************************************************************/
/* 引脚 定义 */
#define LED1_GPIO_PORT GPIOA
#define LED1_GPIO_PIN GPIO_PIN_6
#define LED1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */
#define LED2_GPIO_PORT GPIOA
#define LED2_GPIO_PIN GPIO_PIN_7
#define LED2_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */
#define LED3_GPIO_PORT GPIOB
#define LED3_GPIO_PIN GPIO_PIN_0
#define LED3_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 */
/******************************************************************************************/
/* LED端口定义 */
#define LED1(x) do{ x ? \
HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* LED1翻转 */
#define LED2(x) do{ x ? \
HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* LED2翻转 */
#define LED3(x) do{ x ? \
HAL_GPIO_WritePin(LED3_GPIO_PORT, LED3_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(LED3_GPIO_PORT, LED3_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* LED3翻转 */
/* LED取反定义 */
#define LED1_TOGGLE() do{ HAL_GPIO_TogglePin(LED1_GPIO_PORT, LED1_GPIO_PIN); }while(0) /* 翻转LED0 */
#define LED2_TOGGLE() do{ HAL_GPIO_TogglePin(LED2_GPIO_PORT, LED2_GPIO_PIN); }while(0) /* 翻转LED1 */
#define LED3_TOGGLE() do{ HAL_GPIO_TogglePin(LED3_GPIO_PORT, LED3_GPIO_PIN); }while(0) /* 翻转LED1 */
/******************************************************************************************/
/* 外部接口函数*/
void led_init(void); /* 初始化 */
#endif
5.11 主函数逻辑代码
main.c 如下,调用各个函数以实现我们当温湿度正常,蜂鸣器不报警,LED 灯不亮;当温湿度超出设定阈值时蜂鸣器报警,LED 灯闪烁的目标。
c
#include "sys.h"
#include "usart.h"
#include "delay.h"
#include "esp8266.h"
#include "mqtt.h"
#include "tasks.h"
#include "systick.h"
#include "led.h"
#include "beep.h"
#include "dht11.h"
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟,72M */
usart_init(115200); /* 波特率设为115200 */
SysTick_Init(systick_isr);
led_init();
beep_init();
LED1(1); //系统启动,LED1长亮
network_connection(); //ESP8266连接OneNET,订阅主题
while(1)
{
dht11_task(); //读取温湿度
status_post_task(); //数据上报
heart_send_task(); //发送心跳请求
mqtt_receive_task(); //接收心跳响应
alarm_post_task(); //发布报警
}
}
5.12 运行过程
烧录好后,串口和 OneNET 平台效果如下:
若温度/湿度超出阈值,串口报警,OneNet 上事件告警记录,蜂鸣器鸣叫,LED 闪烁。
总结
优化后的温湿度监测是不是更完整,更人性了。希望大家能够有所收获,接下来有机会还会继续优化更新这个项目。感谢各位看官,love and peace!