手把手教你能存储数据的环境监测系统:DHT11 + 烟雾 + 燃气 + OLED + AT24C02

这是一个 STM32F103C8T6 小项目,能同时监测环境温度、湿度、烟雾浓度和燃气浓度,本地 OLED 实时显示,超限蜂鸣器 + LED 声光报警,阈值掉电保存在 AT24C02 中。硬件简单、代码清晰,很适合作为单片机课程设计或毕业设计参考。

一、项目背景与功能

系统面向厨房、仓库、实验室等场景,核心需求如下:

  • 温湿度监测:DHT11 采集温度和湿度。
  • 烟雾监测:MQ-2 烟雾传感器检测烟雾浓度。
  • 燃气监测:MQ-7 / 燃气传感器检测一氧化碳/燃气浓度。
  • 本地显示:0.96 寸 IIC OLED 实时显示四路数据。
  • 声光报警:温度、湿度、烟雾、燃气任一超过阈值,蜂鸣器和 LED 同时报警。
  • 阈值可调:4 个独立按键设置温度/湿度/烟雾/燃气报警阈值。
  • 掉电保存:阈值写入 AT24C02,重新上电自动恢复。

二、硬件清单

元器件数量说明
STM32F103C8T6 最小系统板1主控 MCU,72MHz Cortex-M3
0.96 寸 OLED 显示屏1IIC 接口,128×64 分辨率
DHT11 温湿度传感器1单总线协议
MQ-2 烟雾传感器1模拟输出,接 PA0
MQ-7 燃气传感器1模拟输出,接 PA1
AT24C02 存储芯片1IIC 接口,掉电保存阈值
有源蜂鸣器1PA15 驱动
LED1PA12 驱动
独立按键4PB12~PB15,上拉输入
DC-005 电源接口15V 输入供电

IO 资源分配

// DHT11 数据:PA11
// MQ-2 烟雾:PA0(ADC1_CH0)
// MQ-7 燃气:PA1(ADC1_CH1)
// OLED IIC:SCL-PB10、SDA-PB11
// AT24C02 IIC:SCL-PB9、SDA-PB8
// 蜂鸣器:PA15
// LED:PA12
// 按键:SET-PB12、CHOICE-PB13、REDUCE-PB14、ADD-PB15

三、硬件设计

3.1 原理图

原理图

原理图分为几个区域:

  • 输入供电:DC-005 接口 + 330uF/100nF 滤波,5V 转 3.3V 供 MCU 和传感器。
  • 主控开发板:STM32F103C8T6 最小系统,引出 ADC、IIC、GPIO 资源。
  • 传感器接口:DHT11 接 PA11;MQ-2/MQ-7 模拟量分别接 PA0、PA1。
  • OLED 屏幕:IIC 接口接 PB10/PB11。
  • 存储模块:AT24C02 接 PB8/PB9,4.7K 上拉电阻。
  • 蜂鸣器与光报警:PA15 驱动蜂鸣器,PA12 驱动 LED。
  • 按键:4 个独立按键共用下拉方式,接 PB12~PB15。

四、软件设计

4.1 主循环

main.c 完成系统初始化、阈值读取和主循环等待:

#include "stm32f10x.h"
#include "main.h"
#include "delay.h"
#include "key.h"
#include "beep.h"
#include "oled.h"
#include "show.h"
#include "dht11.h"
#include "adc.h"
#include "led.h"
#include "timer1.h"
#include "special_pin.h"
#include "24cxx.h"

u8 show_mode;
u8 baojing_status;
u8 tiaozheng_status;
u16 smooth_data, smooth_yuzhi = 1000;
u16 ranqi_data, ranqi_yuzhi = 500;
u8 temp, temp_yuzhi = 30;
u8 humi, humi_yuzhi = 70;
u8 cunchu_shuzu[6];

int main(void)
{
    u8 datatemp[6];
    delay_init();
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SPECIAL_PIN_Init();

    LED_Init();
    BEEP_Init();
    Init_adc();
    KEY_Init();
    while (DHT11_Init()) { Delay_Ms(200); }
    AT24CXX_Init();
    while (AT24CXX_Check()) { Delay_Ms(500); LED = !LED; }

    // 上电读取 AT24C02 保存的阈值
    AT24CXX_Read(0, datatemp, 6);
    temp_yuzhi  = datatemp[0];
    humi_yuzhi  = datatemp[1];
    smooth_yuzhi = datatemp[2] * 256 + datatemp[3];
    ranqi_yuzhi  = datatemp[4] * 256 + datatemp[5];

    OLED_Init();
    OLED_Clear();
    page_one();
    TIM1_Time_Init(99, 7199);   // 10ms 定时中断
    while (1) { }
}

4.2 ADC + DMA 双通道采集

烟雾和燃气传感器都是模拟输出,使用 ADC1 的通道 0 和通道 1,配合 DMA1 通道 1 循环传输:

__IO uint16_t ADC_ConvertedValue[2];

void Init_adc(void)
{
    // 开启 DMA1、ADC1、GPIOA 时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // PA0、PA1 配置为模拟输入
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0 | GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // DMA1 通道 1 循环传输 2 个半字
    DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address;
    DMA_InitStructure.DMA_MemoryBaseAddr     = (uint32_t)ADC_ConvertedValue;
    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize         = 2;
    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_Mode               = DMA_Mode_Circular;
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
    DMA_Cmd(DMA1_Channel1, ENABLE);

    // ADC1 独立模式、扫描、连续转换
    ADC_InitStructure.ADC_Mode               = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode       = ENABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
    ADC_InitStructure.ADC_NbrOfChannel       = 2;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5);

    ADC_DMACmd(ADC1, ENABLE);
    ADC_Cmd(ADC1, ENABLE);
    ADC_ResetCalibration(ADC1);
    while (ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1));
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

4.3 定时器中断:数据采集 + 按键 + 显示 + 报警

TIM1 每 10ms 进入一次更新中断,完成所有周期性任务:

void TIM1_UP_IRQHandler(void)
{
    static u8 KEY_count = 0, ADC_count = 0;
    static u8 TEMP_count = 0, MESSAGE_count = 0;
    static float smooth_zong = 0.0f, ranqi_zong = 0.0f;
    u8 key;

    if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET)
    {
        TEMP_count++; KEY_count++; ADC_count++; MESSAGE_count++;

        // 每 50ms 扫描一次按键
        if (KEY_count >= 5)
        {
            KEY_count = 0;
            key = KEY_Scan(0);
            if (key == SET_PRES)          // 切换显示页面
            {
                OLED_Clear();
                show_mode = (show_mode + 1) % 5;
                if (show_mode == 0) page_one();
                else if (show_mode == 1) page_two();
                else if (show_mode == 2) page_three();
                else if (show_mode == 3) page_four();
                else if (show_mode == 4) page_five();
            }
            else if (key == CHOICE_PRES)  // 切换粗调/细调,或保存阈值
            {
                if (show_mode != 0) tiaozheng_status = !tiaozheng_status;
                else
                {
                    cunchu_shuzu[0] = temp_yuzhi;
                    cunchu_shuzu[1] = humi_yuzhi;
                    cunchu_shuzu[2] = smooth_yuzhi / 256;
                    cunchu_shuzu[3] = smooth_yuzhi % 256;
                    cunchu_shuzu[4] = ranqi_yuzhi / 256;
                    cunchu_shuzu[5] = ranqi_yuzhi % 256;
                    AT24CXX_Write(0, (u8*)cunchu_shuzu, 6);
                }
            }
            else if (key == ADD_PRES)     // 阈值加
            {
                if (show_mode == 1) temp_yuzhi += (tiaozheng_status ? 10 : 1);
                else if (show_mode == 2) humi_yuzhi += (tiaozheng_status ? 10 : 1);
                else if (show_mode == 3) smooth_yuzhi += (tiaozheng_status ? 100 : 10);
                else if (show_mode == 4) ranqi_yuzhi += (tiaozheng_status ? 100 : 10);
            }
            else if (key == REDUCE_PRES)  // 阈值减
            {
                if (show_mode == 1) temp_yuzhi -= (tiaozheng_status ? 10 : 1);
                else if (show_mode == 2) humi_yuzhi -= (tiaozheng_status ? 10 : 1);
                else if (show_mode == 3) smooth_yuzhi -= (tiaozheng_status ? 100 : 10);
                else if (show_mode == 4) ranqi_yuzhi -= (tiaozheng_status ? 100 : 10);
            }
        }

        // 每 500ms 读取一次 DHT11
        if (TEMP_count >= 50)
        {
            DHT11_Read_Data(&temp, &humi);
            TEMP_count = 0;
        }

        // ADC 累加,每 500ms 取平均并换算为 PPM
        adc1 = (float)ADC_ConvertedValue[0] / 4096;
        adc2 = (float)ADC_ConvertedValue[1] / 4096;
        smooth_zong += adc1;
        ranqi_zong  += adc2;
        if (ADC_count >= 50)
        {
            ADC_count = 0;
            smooth_data = smooth_zong / 50 * 6600;
            ranqi_data  = ranqi_zong / 50 * 3300;
            smooth_zong = 0;
            ranqi_zong  = 0;
        }

        // 报警判断
        if (smooth_data >= smooth_yuzhi || temp >= temp_yuzhi ||
            humi >= humi_yuzhi || ranqi_data >= ranqi_yuzhi)
            baojing_status = 1;
        else
            baojing_status = 0;

        if (baojing_status) BEEP = 1, LED = 1;
        else                BEEP = 0, LED = 0;

        // 每 500ms 刷新一次显示
        if (MESSAGE_count >= 50)
        {
            MESSAGE_count = 0;
            if (show_mode == 0)
            {
                OLED_ShowNum(72, 0, temp, 3, 16);
                OLED_ShowNum(72, 2, humi, 3, 16);
                OLED_ShowNum(72, 4, smooth_data, 4, 16);
                OLED_ShowNum(72, 6, ranqi_data, 4, 16);
            }
            else if (show_mode == 1) OLED_ShowNum(36, 4, temp_yuzhi, 3, 16);
            else if (show_mode == 2) OLED_ShowNum(36, 4, humi_yuzhi, 3, 16);
            else if (show_mode == 3) OLED_ShowNum(36, 4, smooth_yuzhi, 4, 16);
            else if (show_mode == 4) OLED_ShowNum(36, 4, ranqi_yuzhi, 4, 16);
        }

        TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
    }
}

4.4 AT24C02 掉电保存

阈值在上电时从 AT24C02 读取,在首页按 CHOICE 键时写入:

// 写入 6 字节:温度阈值、湿度阈值、烟雾阈值高/低字节、燃气阈值高/低字节
AT24CXX_Write(0, (u8*)cunchu_shuzu, 6);

这样即使断电,用户设置的报警阈值也不会丢失。

4.5 显示页面

系统有 5 个显示页面:

  • page_one:首页,显示当前温度、湿度、烟雾 PPM、燃气 PPM。
  • page_two:温度阈值设置页。
  • page_three:湿度阈值设置页。
  • page_four:烟雾阈值设置页。
  • page_five:燃气阈值设置页。

SET 键循环切换页面,在阈值页按 CHOICE 切换粗调/细调,ADD/REDUCE 调整数值。

五、实物展示

实物图

实物板采用绿色 PCB,中间放置 STM32F103C8T6 最小系统板,右侧为 0.96 寸 OLED,下方为 4 个独立按键,左侧为蜂鸣器和 LED,外接 MQ 系列传感器模块和 DHT11。上电后首页实时显示温度、湿度、烟雾浓度、燃气浓度。

六、功能特点

功能说明
四路监测温度、湿度、烟雾、燃气同时采集
本地显示0.96 寸 OLED 五页菜单,数据/阈值一目了然
声光报警任意一路超阈值,蜂鸣器 + LED 同时响应
阈值可调4 按键设置温度/湿度/烟雾/燃气报警阈值
掉电保存AT24C02 保存阈值,重启自动恢复
定时调度TIM1 10ms 中断统一处理按键、ADC、DHT11、显示、报警
成本可控整机 BOM 约 80~120 元,适合课程设计复刻

七、项目总结

这个项目完整覆盖了一个嵌入式小产品的常见要素:

  • ADC + DMA:双通道模拟量采集。
  • 单总线协议:DHT11 温湿度读取。
  • IIC 总线:OLED 显示 + AT24C02 存储。
  • 定时器中断:多任务周期调度。
  • GPIO 输入/输出:按键、蜂鸣器、LED。
  • 人机交互:OLED 菜单 + 独立按键。

无论是课程设计、毕业设计,还是想快速搭建一个环境监测原型,这套方案都可以直接参考复刻。

资料下载

完整资料包含:

  • STM32 工程源码(Keil 标准库工程)
  • 原理图 PDF
  • 嘉立创 EDA 工程文件
  • 测试视频
  • 硬件清单 BOM

资料链接见博客侧边栏头像下方的 资料链接(下载图标)处。

通过网盘分享的文件:嵌入式-手把手教你能存储数据的环境监测系统 链接: https://pan.baidu.com/s/1VPzLpk2IzHB2c1n_O80EHQ?pwd=u1ab 提取码: u1ab