之前我们做了温湿度监测小项目,只是把温湿度数据上传到 OneNET,现在我们来优化这个小项目,让它更完整,更人性。我们在原有的基础上加上蜂鸣器和 LED 灯,当温湿度正常,蜂鸣器不报警,LED 灯不亮;当温湿度超出设定阈值时蜂鸣器报警,LED 灯闪烁。是不是有电影里保险柜红外检测到人偷窃,红灯连闪,报警声长鸣那感觉了。
本文首发 良许嵌入式网 :https://www.lxlinux.net/e/ ,欢迎关注!
本文所涉及的源码及安装包如下(由于平台限制,请点击以下链接阅读原文下载):
https://www.lxlinux.net/e/stm32/temp-humi-detection-system.html
如果你是嵌入式开发小白,那么建议你先读读下面几篇文章。
前期教程,没看过的小伙伴可以先看下。
作者简介 |
---|
大家好,我是良许,博客里所有的文章皆为我的原创。 下面是我的一些个人介绍,欢迎交个朋友: · 211工科硕士,国家奖学金获得者; · 深耕嵌入式11年,前世界500强外企高级嵌入式工程师; · 书籍《速学Linux作者》,机械工业出版社专家委员会成员; · 全网60W粉丝,博客分享大量原创成体系文章,全网阅读量累计超4000万; · 靠自媒体连续年入百万,靠自己买房买车。 |
我本科及硕士都是学机械,通过自学成功进入世界500强外企。我已经将自己的学习经验写成了一本电子书,超千人通过此书学习并转行成功。现在将这本电子书免费分享给大家,希望对你们有帮助:
电子书链接:https://www.lxlinux.net/1024.html
使用 STM32 作主控,配合 DHT11 温湿度传感器,实时监测周围环境的温湿度变化。通过 ESP8266 模块以 MQTT 协议将获取到的温湿度数据通过无线网络连接上传至 OneNET 平台,以便用户可以随时随地通过手机或电脑查看数据。温湿度如有异常,蜂鸣器报警,LED 灯闪烁。
本教程使用的硬件如下:
这款单片机具有 64K flash,20K RAM,4 个定时器,3 个串口,网络上资料好几吨,非常适合初学者入门,强烈推荐。
ESP8266 可以利用串口与单片机进行通讯,从而编程实现控制。
DHT11 有 3 脚和 4 脚两款,在使用上没有差别,接线都一样,主要接三根,四脚的款式有一脚悬空。四脚款接杜邦线会有点不稳,适合插面包板或开发板上。
DHT11 工作参数:
蜂鸣器广泛应用于各种电子设备中,例如警报器、计时器、电子钟、雷达等。它们可以用来进行提醒、报警、指示等,通过发出特定的声音频率来引起用户的注意。可分为无源蜂鸣器和有源蜂鸣器。
LED灯是一种半导体光源,具有高效、耐用和可靠的特点,被广泛应用于各种电子设备和照明系统中,为我们的生活和工作提供了可靠的光源。它可以发出多种颜色的光,如下模块就有红、黄、蓝、绿、白的颜色,3.3~5V 的电压范围内供电。
这种设备主要作用是用来调试或下载程序,本文用于串口输出作调试。价格也很便宜,普遍 5~8 元。
ST-Link 是一种用于 STM32 微控制器的调试和编程工具,它可以通过 SWD 或 JTAG 接口与开发板进行通信。本文用做烧录。一般也很便宜,七八元左右。
我们上一篇已经配置过 OneNET 了,这里就不从头教了,没看的同学可以去看看【MQTT+DHT11链接】
书接上回,我们需要在之前的基础上创建以下物模型:
「MQTT 三元组」的获取方式也在上一篇详细介绍过了,这边不赘述啦,直接给出,没看的同学可以去看看【MQTT+DHT11链接】
接线可参照下表:
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 |
接好如下图,我用的是正点原子的开发板,大家用别的板子也是一样可以的,只要是 STM32F103C8T6 就行。
详细代码解析可以看手把手教你玩转DHT11(原理+驱动)。
详细代码解析可以看手把手教你玩转ESP8266(原理+驱动)。
CONNECT 报文和 PUBLISH 报文已经在上一篇讲过,没看的同学可以去看看【MQTT+DHT11链接】
我们讲新增的。
SUBSCRIBE 报文的编写及发送代码,报文编写就按照我们 万字猛文:MQTT原理及案例 的理论编写即可,报文内容:82+剩余长度+标识符+L+主题过滤器+QoS。
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; //失败 }
心跳请求就很简单,ESP8266 发送“C0 D0”即可。
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); }
接收 OneNET 发来的数据,去掉我们不需要的固定报头和可变报头,只保存有效载荷。
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; }
将我们得到的温湿度数据和设定的温湿度阈值通过 PUBLISH 报文上报到 OneNET。
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"); }
当 alarm_post_flag 为1,则表示超出设定的温/湿度的上/下限,向 OneNET 和串口发布报警
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"); } }
将我们需要用到的蜂鸣器和 LED 报警、DHT11读取、数据上报、心跳请求、接收数据、报警上传的这几个任务定时。
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; } }
蜂鸣器的代码简简单单,这里就不赘述了,下面是蜂鸣器初始化代码。
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文件:
#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
LED 灯的代码也简简单单,我们只用了 LED1,所以只设置了 LED1 的输出模式。
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文件:
#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
main.c 如下,调用各个函数以实现我们当温湿度正常,蜂鸣器不报警,LED 灯不亮;当温湿度超出设定阈值时蜂鸣器报警,LED 灯闪烁的目标。
#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(); //发布报警 } }
烧录好后,串口和 OneNET 平台效果如下:
若温度/湿度超出阈值,串口报警,OneNet 上事件告警记录,蜂鸣器鸣叫,LED 闪烁。
优化后的温湿度监测是不是更完整,更人性了。希望大家能够有所收获,接下来有机会还会继续优化更新这个项目。感谢各位看官,love and peace!
另外,想进大厂的同学,一定要好好学算法,这是面试必备的。这里准备了一份 BAT 大佬总结的 LeetCode 刷题宝典,很多人靠它们进了大厂。
刷题 | LeetCode算法刷题神器,看完 BAT 随你挑!
推荐阅读:
欢迎关注我的博客:良许嵌入式教程网,满满都是干货!