前言

之前我们做了温湿度监测小项目,只是把温湿度数据上传到 OneNET,现在我们来优化这个小项目,让它更完整,更人性。我们在原有的基础上加上蜂鸣器和 LED 灯,当温湿度正常,蜂鸣器不报警,LED 灯不亮;当温湿度超出设定阈值时蜂鸣器报警,LED 灯闪烁。是不是有电影里保险柜红外检测到人偷窃,红灯连闪,报警声长鸣那感觉了。

1. 源码下载及前置阅读

本文首发 良许嵌入式网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

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 工作参数:

  1. 湿度测量范围:20~90%RH
  2. 湿度测量精度:±5%RH
  3. 温度测量范围:0~50℃
  4. 温度测量精度:±2℃
  5. 工作电压:DC 3.3V/5V

<img src="https://lxlinux.superbed.verylink.top/item/655b12b2c458853aef4b8a5b.jpg" style="zoom: 67%;" />

  • 蜂鸣器

蜂鸣器广泛应用于各种电子设备中,例如警报器、计时器、电子钟、雷达等。它们可以用来进行提醒、报警、指示等,通过发出特定的声音频率来引起用户的注意。可分为无源蜂鸣器和有源蜂鸣器。

  • 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 硬件接线

接线可参照下表:

ESP8266DHT11蜂鸣器LEDSTM32USB 转 TTL
3V3 3.3
TX A3
RX A2
GND G
VCC 3.3
DATA B9
GND G
I/C C13
INA6
A10TX
A9RX
GGND

烧录的时候接线如下表,如果不会烧录的话可以看我之前的文章【STM32下载程序的五种方法】。

ST-Link V2STM32
SWCLKSWCLK
SWDIOSWDIO
GNDGND
3.3V3V3

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。

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”即可。

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 发来的数据,去掉我们不需要的固定报头和可变报头,只保存有效载荷。

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。

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 和串口发布报警

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读取、数据上报、心跳请求、接收数据、报警上传的这几个任务定时。

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 蜂鸣器逻辑代码

蜂鸣器的代码简简单单,这里就不赘述了,下面是蜂鸣器初始化代码。

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

5.10 LED逻辑代码

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

5.11 主函数逻辑代码

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();                      //发布报警
    }
}

5.12 运行过程

烧录好后,串口和 OneNET 平台效果如下:

若温度/湿度超出阈值,串口报警,OneNet 上事件告警记录,蜂鸣器鸣叫,LED 闪烁。

总结

优化后的温湿度监测是不是更完整,更人性了。希望大家能够有所收获,接下来有机会还会继续优化更新这个项目。感谢各位看官,love and peace!


另外,想进大厂的同学,一定要好好学算法,这是面试必备的。这里准备了一份 BAT 大佬总结的 LeetCode 刷题宝典,很多人靠它们进了大厂。

刷题 | LeetCode算法刷题神器,看完 BAT 随你挑!

有收获?希望老铁们来个三连击,给更多的人看到这篇文章

推荐阅读:

欢迎关注我的博客:良许嵌入式教程网,满满都是干货!


良许
1k 声望1.8k 粉丝