• 课程设计要求

STM32为主控平台:本系统以STM32微控制器为核心,能够实时检测环境温度、湿度和PM2.5粉尘浓度。

定时与人工设置显示功能:系统支持定时自动显示监测数据,并且允许用户通过人工设置显示内容。

超标报警功能:当监测的环境指标超出预设的安全阈值时,系统能够自动触发报警机制。整个系统分为三大部分:检测、显示和报警。检测部分采用SHT20温湿度传感器实时监测环境的温度和湿度,同时使用PM2.5传感器检测空气中的粉尘颗粒浓度。显示部分则通过STM32将传感器采集到的数据进行处理后,通过LCD1602液晶屏显示当前的温度、湿度和PM2.5浓度。此外,系统还通过按键实现人机交互,用户可设置PM2.5浓度的上限,超过该上限时,单片机会控制三极管驱动蜂鸣器发出报警音。

数据存储功能:系统能够记录并存储一定时间范围内的监测数据,便于后续查阅与分析。

  • 系统架构

2.1硬件系统组成架构

本设计使用单片机作为控制核心,采用单个温湿度传感器(DHT11)对温度和湿度进行监测,GP2Y10传感器则负责检测空气中的PM2.5浓度,以液晶显示屏(OLED)显示检测温度,当PM2.5浓度超过设定阈值时,系统会通过蜂鸣器发出声音报警,系统总体控制框图如图2.1所示:

该系统软件功能有环境温湿度获取,环境PM2.5浓度获取,环境质量(温湿度及PM2.5浓度)展示,PM2.5浓度报警。下面图2是软件系统框图:

  • 硬件系统设计

3.1 STM32F103C8T6最小系统板

STM32F103C8T6 最小系统板主要包括以下几个关键部分:

微控制器  型号:STM32F103C8T6  

电源供应 输入电压:3.3V  去耦电容:0.1μF 电容  10μF 电容

复位电路 复位引脚:NRST 上拉电阻:10kΩ 复位按钮:连接到 NRST 引脚

晶振电路 主晶振:8MHz 实时时钟晶振:32.768kHz

调试接口 SWD接口:SWDIO引脚 SWCLK引脚 上拉电阻:每个10kΩ

3.2 DHT11温湿度传感器

DHT11 是一种常用的温湿度传感器,它集成了温度和湿度的测量功能,广泛应用于各种环境监测系统中。DHT11 的工作原理基于电阻式湿度传感原理和数字温度传感技术。以下是 DHT11 温湿度传感器的工作原理:

1.湿度测量原理(电阻式)

DHT11 内部的湿度传感器部分采用的是电阻式测量原理。其核心是一个吸湿性的电阻材料(通常是聚合物),该材料的电阻随着空气中湿度的变化而变化:

湿度增加时,吸湿材料会吸收水分,导致其电阻值减小。

湿度降低时,吸湿材料中的水分减少,电阻值增大。

DHT11 内置一个传感器电路,实时监测电阻值的变化,并通过内部的模数转换(ADC)将电阻值转化为数字信号。这个数字信号被用来表示环境湿度的值,并传输给主控单元(如 STM32 等微控制器)。

2.温度测量原理

DHT11 的温度测量是通过内置的数字温度传感器来实现的,通常这种传感器是基于热敏电阻(NTC 或 PTC)原理。NTC热敏电阻的电阻值随温度变化而变化,在 DHT11 中,传感器通过内部的处理电路将温度的变化转换为对应的数字信号。

温度升高时,NTC 电阻的电阻值减小。

温度降低时,NTC 电阻的电阻值增大。

DHT11 将温度变化转化为数字信号,经过处理后,输出对应的温度值。

3.3 GP2Y10烟雾传感器

GP2Y10烟雾传感器的工作原理:

GP2Y10是一款光学传感器,使用了红外LED和光电二极管来探测烟雾颗粒。其基本原理可以分为以下几个步骤:

红外光源(LED)发射:GP2Y10 内部有一个红外LED,它会发射红外光。这种光在传感器内部被照射到空气中。

光散射:当空气中有颗粒物(如烟雾、灰尘)存在时,这些颗粒物会散射红外光。散射的光的强度与颗粒物的浓度成正比。

烟雾中的微小颗粒物比大气中的其他颗粒物更加有效地散射红外光,因此当空气中烟雾浓度较高时,散射光会增强。

光电二极管接收散射光:在传感器内部,配有一个 光电二极管,用于接收被颗粒物散射回来的红外光。光电二极管将接收到的光转换为电信号。

电信号处理:传感器的内部电路会处理光电二极管产生的电信号,输出一个与空气中颗粒物浓度成正比的电压信号。这个信号可以被外部微控制器(如 STM32)读取,进而根据电压值推测出空气中的烟雾浓度。

输出信号:GP2Y10 提供的是一个模拟信号,通常是一个与烟雾浓度相关的 模拟电压输出。该信号可以用来判断烟雾的浓度,或者进一步进行报警、显示等操作。

3.4 OLED显示屏

OLED(有机发光二极管)显示屏通过电流激励有机材料自发光来显示图像。其基本结构包括基板、透明阳极、发光层、阴极等。电流通过阴极注入电子,阳极注入空穴,电子和空穴在发光层中结合形成激子,并释放能量以光的形式发射出来。不同颜色的发光材料(红、绿、蓝)生成不同波长的光,结合成全色显示。由于每个像素都是自发光的,OLED 显示屏具有高对比度和宽视角,能实现真正的黑色和鲜艳的色彩。相比LCD,OLED 不需要背光源,薄且轻,功耗低。主要应用于智能手机、电视等领域,但也存在蓝光寿命较短和屏幕烧录问题。

3.5有源蜂鸣器

有源蜂鸣器是一种能够自发产生声音的电子元件,通常由压电陶瓷、振荡电路和电源组成。其工作原理基于压电效应,即材料在电场作用下会发生形变,从而产生机械振动并发出声音。具体来说,当电压施加到蜂鸣器的压电陶瓷元件时,压电材料会根据电场的变化发生形变。这种形变产生的振动会使蜂鸣器的外壳(通常是金属材料)产生振动,进而发出声音。蜂鸣器内部通常有一个内置的振荡电路,这个电路在接收到电源后,会自动产生一定频率的信号,控制蜂鸣器的振动频率,产生一定的音调。与无源蜂鸣器不同,有源蜂鸣器不需要外部的信号驱动。它自带振荡电路,连接电源后即可产生声音。常见的工作频率为2kHz至4kHz,适用于各种警报、提示音等场合。当电源电压施加到蜂鸣器时,它会产生一个稳定的声音信号,直到电压去除。有源蜂鸣器的优点是易于使用,无需额外的驱动电路,适合简单的报警系统。它的缺点是音调通常不如无源蜂鸣器可调,且声音较为单一。

  • 软件系统设计

4.1主函数:

这段代码主要是实现了一个基于STM32的环境监测系统,集成了PM2.5传感器(GP2Y10)、温湿度传感器(DHT11)、OLED显示屏和蜂鸣器功能。代码的主要功能包括实时显示环境数据、调节PM2.5警告值、触发警报并通过OLED显示屏展示信息。以下是详细功能分解:

4.1.1. 初始化操作:

`OLED_Init()`:初始化OLED显示屏,用于显示信息。

`EXIT_Init()`:初始化外部中断,设置按键(PB5和PB6)用于调节警告值。

`Adc_Init()`:初始化ADC模块(虽然在代码中没有进一步使用,为了读取模拟传感器数据)。

`Beep_Init()`:初始化蜂鸣器,连接到GPIOA的Pin4,用于报警。

4.1.2 OLED显示:

在OLED屏幕上显示环境数据,如PM2.5浓度、温度和湿度以及当前的警告阈值。用于在特定位置显示字符串,用于显示数字。

4.1.3警告阈值的调整:

初始警告阈值`warn_num`为10。

通过按键PB5(增加警告阈值)和PB6(减少警告阈值)来调节`warn_num`值。按下PB5时,`warn_num`增加5,按下PB6时,`warn_num`减少5,且在0到1000之间限制。`Warning_Num()`会在OLED上实时显示当前的警告阈值。

4.1.4PM2.5监测和报警:

`Show_PM()`通过`GetGP2Y()`获取PM2.5传感器的值,并在OLED上显示。如果PM2.5值超过设定的`warn_num`阈值,蜂鸣器会触发报警(通过控制GPIOA的Pin4状态,蜂鸣器发出声音)。

4.1.5温湿度显示:

`Show_T_And_Wet()`通过`DHT11_REC_Data()`获取温湿度数据,并在OLED屏幕上显示。温度和湿度数据从`rec_data`数组中提取,并以特定格式显示在屏幕上。

4.1.6外部中断处理:

`EXIT_Init()`配置了外部中断,当按下PB5或PB6时会触发相应的中断,调整警告阈值。`EXTI9_5_IRQHandler()`是外部中断服务程序,用于处理PB5和PB6的按键输入。按键按下时,检查引脚电平,增加或减少`warn_num`值,并更新OLED显示。

工作流程:

系统启动时,初始化OLED显示、蜂鸣器、传感器等硬件,并在OLED上显示"PM2.5"、"T"(温度)、"Wet"(湿度)等信息。

`Show_PM()`函数不断读取PM2.5值,并判断是否超过警告阈值,若超过,则触发蜂鸣器报警。

`Show_T_And_Wet()`定时获取温湿度数据并显示在OLED上。

通过按下PB5(增加)和PB6(减少)来调整`warn_num`值,系统会实时更新警告阈值。每次按键事件会触发中断,调整`warn_num`的值,并通过OLED更新显示。

这段代码的核心功能是读取环境传感器数据(PM2.5、温湿度),并在OLED屏幕上显示。通过按钮输入,可以调节PM2.5报警阈值,并在值超过设定阈值时触发蜂鸣器报警。系统通过外部中断响应按键输入,实现用户交互。

4.2OLED模块:

这段代码是用于控制基于 I2C 协议的 OLED 显示屏,通过 STM32 微控制器进行通信。代码主要功能包括初始化 OLED 显示屏、发送命令、显示字符、数字等内容。具体来说,这段代码的主要功能可以分为以下几个部分:

4.2.1 引脚配置与初始化:

OLED_W_SCL(x)和OLED_W_SDA(x)宏定义用于设置 OLED 显示屏的 I2C 时钟 (SCL) 和数据 (SDA) 引脚。这些引脚是通过 STM32 的 GPIOB 端口控制的。

`OLED_I2C_Init()` 函数初始化 GPIOB 的 8 号和 9 号引脚为开漏输出模式,并使能时钟。

4.2.2 I2C 通信相关函数:

OLED_I2C_Start()和OLED_I2C_Stop(): 用于控制 I2C 总线的启动和停止。I2C 的启动信号是通过拉低数据线和时钟线产生的,停止信号则是将数据线拉高。OLED_I2C_SendByte(uint8_t Byte): 用于通过 I2C 发送一个字节的数据。该函数逐位发送数据,每发送一个 bit 会产生一个时钟信号。

4.2.3 OLED 命令与数据发送:

OLED_WriteCommand(uint8_t Command): 发送 OLED 显示屏命令。OLED 显示屏通常在接收到命令后执行初始化或其他设置操作。OLED_WriteData(uint8_t Data): 发送显示数据。数据通常是用于控制显示内容,例如显示字符或图形。

4.2.4 OLED 显示相关函数:

OLED_SetCursor(uint8_t Y, uint8_t X): 设置 OLED 显示屏的光标位置。OLED 屏幕通常有多个行列,通过设置光标来指定要显示内容的位置。

OLED_Clear(): 清空 OLED 显示屏,即将所有显示内容设置为 0(黑色显示)。

OLED_ShowChar(uint8_t Line, uint8_t Column, char Char): 显示一个字符。字符通过调用 `OLED_F8x16` 字体数组来显示,其中该数组包含了每个字符的字形数据。

OLED_ShowString(uint8_t Line, uint8_t Column, char *String): 显示一个字符串,通过调用 `OLED_ShowChar` 来依次显示每个字符。

4.2.5 显示数字与进制转换相关函数:

OLED_Pow(uint32_t X, uint32_t Y): 计算 X 的 Y 次方,供后续的数字显示使用。

OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length): 显示一个正数(十进制),根据提供的长度显示该数字。

OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length): 显示一个带符号的数字(十进制)。

OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length): 显示一个十六进制数字。

OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length): 显示一个二进制数字。

4.2.6 OLED 初始化:

OLED_Init(): 初始化 OLED 显示屏,进行一系列配置操作,设置显示模式、对比度、时钟频率等。它通过一系列的命令来配置显示屏的硬件设置,如复用率、偏移、显示方向、对比度等。

在初始化过程中,首先关闭显示,然后设置显示参数,最后开启显示并清屏。

这段代码实现了与 OLED 显示屏进行通信并控制显示内容的功能,包含了:初始化显示屏的硬件设置;支持字符、字符串、数字、进制转换等显示功;通过 I2C 协议发送命令和数据到 OLED 显示屏,控制屏幕的显示内容。

4.3 DHT11 温湿度传感器:

这段代码实现了与 DHT11 温湿度传感器的通信,目的是通过 STM32 微控制器获取环境的温度和湿度数据。代码分为几个主要部分,下面是每个部分的功能概括:

4.3.1 GPIO 初始化:

DH11_GPIO_Init_OUT()**: 初始化 STM32 的 GPIO 引脚为推挽输出模式,设置 DHT11 数据引脚为输出,用于发送信号到 DHT11。

DH11_GPIO_Init_IN()**: 初始化 STM32 的 GPIO 引脚为浮空输入模式,设置 DHT11 数据引脚为输入,用于接收 DHT11 返回的数据。

4.3.2主机发送开始信号:

DHT11_Start(); 通过控制 GPIO 引脚,向 DHT11 发送启动信号。DHT11 在接收到启动信号后会开始发送温湿度数据。信号的发送过程包括:拉高数据线,并保持一定时间。拉低数据线至少 18 毫秒,准备开始通信。拉高数据线,并等待 DHT11 设备响应。

4.3.3接收 DHT11 数据:

DHT11_Rec_Byte(): 该函数用于接收 DHT11 传输的一个字节数据。数据是按位传输的,函数通过检测数据线的电平变化来解析每一位。

DHT11 通过低电平开始信号,接着将每一位的数据按时序传输,每一位数据的时长区分为 0 或 1。

该函数使用一个循环接收 8 位数据,拼接成一个字节并返回。

DHT11_REC_Data(): 获取 DHT11 温湿度数据的主函数。该函数会按照 DHT11 的协议发送启动信号,并接收返回的数据。

启动信号发送后,DHT11 会返回 5 个字节的数据:湿度高字节(R_H)、湿度低字节(R_L)、温度高字节(T_H)、温度低字节(T_L)和校验字节(CHECK)。

如果接收到的数据的和与校验字节相符,则认为数据接收成功,存储湿度和温度数据。

4.3.4数据校验:

在接收到湿度和温度数据后,通过校验和验证接收到的数据是否正确。校验方式是:湿度高字节 + 湿度低字节 + 温度高字节 + 温度低字节 应该等于校验字节(CHECK)。

如果校验成功,则将温湿度数据存储到 `rec_data[]` 数组中,其中:

`rec_data[0]` 存储湿度高字节(RH)。

`rec_data[1]` 存储湿度低字节(RL)。

`rec_data[2]` 存储温度高字节(TH)。

`rec_data[3]` 存储温度低字节(TL)。

这段代码的主要功能是通过 STM32 与 DHT11 温湿度传感器进行通信,并获取温湿度数据。具体流程包括:初始化 GPIO 引脚为输出(发送信号)和输入(接收数据)。向 DHT11 发送启动信号,等待其响应并返回数据。解析 DHT11 返回的温湿度数据,并进行校验。如果数据校验通过,将湿度和温度的数据存储到 `rec_data` 数组中供后续使用。

4.4GP2Y10模块:

这段代码主要实现了通过 STM32 获取 GP2Y10 PM2.5 传感器的输出数据,并进行处理。它包括 ADC 初始化、读取传感器数据以及计算平均值等功能。具体来说,代码的功能可概括如下:

4.4.1 ADC 初始化 (`Adc_Init`):

配置 STM32 的 ADC 模块,使其能够从模拟输入通道读取数据。特别地:启用了 `ADC1` 和 `GPIOA`(PA0)及 `GPIOB`(PB1)的时钟。设置了 PA0 为模拟输入,用于连接 GP2Y10 传感器的模拟输出信号。配置了 ADC 的工作模式(独立模式、单次转换模式、右对齐数据等)和采样时间(239.5 个周期)。对 ADC 进行了复位和校准,确保准确的模拟转换。GP2Y_High` 和 `GP2Y_Low` 控制信号引脚,用来启动和停止 GP2Y10 传感器的工作。

4.4.2获取 ADC 转换值 (`Get_Adc`):

该函数通过指定 ADC 通道(0~3)来获取对应通道的 ADC 值。配置 ADC 的通道和采样时间,启动 ADC 转换并等待转换结束。最后返回 ADC 转换结果,即采样到的模拟值。

4.4.3获取 GP2Y10 数据 (`GetGP2Y`):

控制 GP2Y10 的工作状态:通过设置 `GP2Y_Low` 使传感器工作,并延时 280 微秒采样。通过 `Get_Adc(ADC_Channel_0)` 获取来自 GP2Y10 模拟输出的 ADC 值。设置 `GP2Y_High` 停止传感器输出数据。最后通过一个转换公式 `pm = 0.17*AD_PM - 0.1` 将 ADC 值转换为 PM2.5 浓度值,并返回该值。

4.4.4获取 GP2Y10 数据的平均值 (`Get_GP2Y_Average`):

该函数重复多次获取 GP2Y10 数据,并计算其平均值,以减小波动或噪声。`times` 参数表示测量次数,通过多次采样后,返回传感器输出值的平均值。这段代码的核心功能是通过 STM32 控制 ADC 采集 GP2Y10 PM2.5 传感器的模拟输出信号,并将其转换为 PM2.5 浓度值。通过控制 GPIO 引脚启动/停止传感器工作,并使用 ADC 获取模拟数据后,利用公式转换为实际的 PM2.5 浓度值。同时,该代码支持多次采样并返回平均值,从而提高数据的准确性和稳定性。

五、课程设计总结

5.1系统完成情况及功能说明

5.1.1 温湿度监测 (SHT20 传感器)

实现情况:已成功接入 SHT20 温湿度传感器,利用 I2C 总线与 STM32 进行数据通信。系统可以实时读取温湿度数据并显示在 OLED 屏幕上。

功能:实时获取环境的温度和湿度,并在 OLED 屏幕上显示。

5.1.2 PM2.5 浓度监测 (GP2Y10 传感器)

实现情况:已成功通过 ADC 通道获取 GP2Y10 传感器的模拟输出数据,并将其转换为 PM2.5 浓度值。

功能:通过控制 GP2Y10 的工作状态,实时监测空气中的 PM2.5 浓度。

数据处理:采用公式将 ADC 数据转换为 PM2.5 浓度,且支持多次采样取平均值来提高数据的稳定性。

5.1.3 OLED 显示功能

实现情况:已通过 I2C 接口连接 SSD1306 OLED 显示屏,实时显示温度、湿度、PM2.5 浓度等数据。

功能:显示当前环境质量信息,周期性刷新以确保数据实时更新。

5.1.4报警功能(蜂鸣器)

实现情况:已实现温湿度和 PM2.5 超过设定阈值时,触发蜂鸣器报警功能。

功能:当监测数据超出设定阈值时,系统会通过蜂鸣器发出声音提示,帮助用户注意空气质量变化。

用户设置:允许用户设置温湿度和 PM2.5 的报警阈值。

5.1.5数据采集与显示

实现情况:通过周期性采集传感器数据,并显示在 OLED 屏幕上。数据更新频率和刷新机制已经调整为合理的工作状态,以保证显示内容及时准确。

功能:温湿度、PM2.5 浓度实时显示,确保用户能够即时了解环境质量。

5.1.6按键设置功*

实现情况:用户可以通过按键调整报警阈值,设置温度、湿度和 PM2.5 的报警上限和下限值。

功能:通过外部按键实现对系统的手动配置,便于用户根据实际需求调整警报条件。

5.2仪器缺陷

数据存储:目前系统尚未实现数据存储功能。可以通过增加外部存储(如 SD 卡)来存储监测数据,便于后续查看历史数据和趋势分析。

联网功能:当前系统没有联网功能,未来可以通过 Wi-Fi 或蓝牙模块实现数据上传,进行远程监控和报警。可将数据上传至云平台或手机应用,实现远程查看和报警通知。

功耗优化:系统当前功耗适合实验室使用,但若部署于实际环境中(例如野外或无电源的地方),需要进一步优化功耗,增加电池管理功能。可能需要进入低功耗模式,延长电池寿命。

支持更多传感器:目前只支持温湿度和 PM2.5 监测,未来可以扩展更多传感器,如 CO2、气压、空气质量传感器等,进一步提升系统的功能和环境监测能力。

用户界面改:OLED 显示屏的界面可以进一步美化,显示更多的环境质量信息,或通过按键实现多页面显示,便于用户查看不同类型的数据。

参考文献

  1. 吴建平,传感器原理及应用[M]北京: 机械工业出版社, 2021.
  2. 董锟、何娜,基于STM32的多参数环境检测仪的设计与实现,电脑知识与技术,2024.06.05.

[3] 王珅,基于GP2Y1010AU0F传感器的PM2.5检测仪设计,工业加热,2020.02.28..

附录

附录一:硬件系统原理图

附录二:软件程序清单

Main.c:

#include "Config.h"

int warn_num = 10;

int main(void)

{

 OLED_Init();

 EXIT_Init();

 OLED_ShowString(1, 1, "PM2.5:");

 OLED_ShowString(2, 1, "T:");

 OLED_ShowString(3, 1, "Wet:");

 Adc_Init();

 Beep_Init();

 //GP2Yinit();

 while(1)

 {

  Warning_Num();

  Show_PM();

  Show_T_And_Wet();

 }

}

void Show_PM(void)

{

 float num = GetGP2Y();

 OLED_ShowNum(1, 7, (int)num, 3);

 if(num > warn_num)

  GPIO_ResetBits(GPIOA, GPIO_Pin_4);

 else

  GPIO_SetBits(GPIOA, GPIO_Pin_4);

}

void Show_T_And_Wet()

{

 Delay_s(1);

 DHT11_REC_Data();

 OLED_ShowNum(2, 3, rec_data[2], 3);

 OLED_ShowString(2, 6, ".");

 OLED_ShowNum(2, 7, rec_data[3], 2);

 OLED_ShowNum(3, 5, rec_data[0], 3); 

}

void Warning_Num(void)

{

 OLED_ShowString(4, 1, "Warn:");

 OLED_ShowNum(4, 6, warn_num, 3);

}

void Beep_Init(void)

{

 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

 GPIO_InitTypeDef GPIO_InitStructer;

 GPIO_InitStructer.GPIO_Mode=GPIO_Mode_Out_PP;

 GPIO_InitStructer.GPIO_Pin=GPIO_Pin_4;

 GPIO_InitStructer.GPIO_Speed=GPIO_Speed_50MHz;

 GPIO_Init(GPIOA,&GPIO_InitStructer);

 GPIO_SetBits(GPIOA, GPIO_Pin_4);

}

void EXIT_Init(void)//PB5和6为key设置,中断进行设置

{//5+   6-

 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

 GPIO_InitTypeDef GPIO_InitStructure;

 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;

 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6;

 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

 GPIO_Init(GPIOB, &GPIO_InitStructure);

 GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource5);

 GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource6);

 EXTI_InitTypeDef EXTI_InitStructure;

 EXTI_InitStructure.EXTI_Line = EXTI_Line5 | EXTI_Line6;

 EXTI_InitStructure.EXTI_LineCmd = ENABLE;

 EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;

 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//下降沿产生中断

 EXTI_Init(&EXTI_InitStructure);

 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

 NVIC_InitTypeDef NVIC_InitStructure;

 NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;

 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;

 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;

 NVIC_Init(&NVIC_InitStructure);

}

void EXTI9_5_IRQHandler(void)

{

 /*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/

 if (EXTI_GetITStatus(EXTI_Line5) == SET)

 {

  /*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/

  if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5) == 0)

  {

   if(warn_num > 1000)

   {

    warn_num = 1000;

   }

   else

   {

    warn_num += 5;

   }

  }

  EXTI_ClearITPendingBit(EXTI_Line5);

 }

 if (EXTI_GetITStatus(EXTI_Line6) == SET)

 {

  /*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/

  if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_6) == 0)

  {

   if(warn_num < 0)

   {

    warn_num = 0;

   }

   else

   {

    warn_num -= 5;

   }

  }

  EXTI_ClearITPendingBit(EXTI_Line6);

 }

}

Delay.c:

#include "stm32f10x.h"

/**

  * @brief  微秒级延时

  * @param  xus 延时时长,范围:0~233015

  * @retval 无

  */

void Delay_us(uint32_t xus)

{

 SysTick->LOAD = 72 * xus;    //设置定时器重装值

 SysTick->VAL = 0x00;     //清空当前计数值

 SysTick->CTRL = 0x00000005;    //设置时钟源为HCLK,启动定时器

 while(!(SysTick->CTRL & 0x00010000)); //等待计数到0

 SysTick->CTRL = 0x00000004;    //关闭定时器

}

/**

  * @brief  毫秒级延时

  * @param  xms 延时时长,范围:0~4294967295

  * @retval 无

  */

void Delay_ms(uint32_t xms)

{

 while(xms--)

 {

  Delay_us(1000);

 }

}

/**

  * @brief  秒级延时

  * @param  xs 延时时长,范围:0~4294967295

  * @retval 无

  */

void Delay_s(uint32_t xs)

{

 while(xs--)

 {

  Delay_ms(1000);

 }

}

OLED.c:

#include "stm32f10x.h"

#include "OLED_Font.h"

/*引脚配置*/

#define OLED_W_SCL(x)  GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))

#define OLED_W_SDA(x)  GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction)(x))

/*引脚初始化*/

void OLED_I2C_Init(void)

{

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

 GPIO_InitTypeDef GPIO_InitStructure;

  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;

 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;

  GPIO_Init(GPIOB, &GPIO_InitStructure);

 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;

  GPIO_Init(GPIOB, &GPIO_InitStructure);

 OLED_W_SCL(1);

 OLED_W_SDA(1);

}

/**

  * @brief  I2C开始

  * @param  无

  * @retval 无

  */

void OLED_I2C_Start(void)

{

 OLED_W_SDA(1);

 OLED_W_SCL(1);

 OLED_W_SDA(0);

 OLED_W_SCL(0);

}

/**

  * @brief  I2C停止

  * @param  无

  * @retval 无

  */

void OLED_I2C_Stop(void)

{

 OLED_W_SDA(0);

 OLED_W_SCL(1);

 OLED_W_SDA(1);

}

/**

  * @brief  I2C发送一个字节

  * @param  Byte 要发送的一个字节

  * @retval 无

  */

void OLED_I2C_SendByte(uint8_t Byte)

{

 uint8_t i;

 for (i = 0; i < 8; i++)

 {

  OLED_W_SDA(Byte & (0x80 >> i));

  OLED_W_SCL(1);

  OLED_W_SCL(0);

 }

 OLED_W_SCL(1); //额外的一个时钟,不处理应答信号

 OLED_W_SCL(0);

}

/**

  * @brief  OLED写命令

  * @param  Command 要写入的命令

  * @retval 无

  */

void OLED_WriteCommand(uint8_t Command)

{

 OLED_I2C_Start();

 OLED_I2C_SendByte(0x78);  //从机地址

 OLED_I2C_SendByte(0x00);  //写命令

 OLED_I2C_SendByte(Command);

 OLED_I2C_Stop();

}

/**

  * @brief  OLED写数据

  * @param  Data 要写入的数据

  * @retval 无

  */

void OLED_WriteData(uint8_t Data)

{

 OLED_I2C_Start();

 OLED_I2C_SendByte(0x78);  //从机地址

 OLED_I2C_SendByte(0x40);  //写数据

 OLED_I2C_SendByte(Data);

 OLED_I2C_Stop();

}

/**

  * @brief  OLED设置光标位置

  * @param  Y 以左上角为原点,向下方向的坐标,范围:0~7

  * @param  X 以左上角为原点,向右方向的坐标,范围:0~127

  * @retval 无

  */

void OLED_SetCursor(uint8_t Y, uint8_t X)

{

 OLED_WriteCommand(0xB0 | Y);     //设置Y位置

 OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //设置X位置高4位

 OLED_WriteCommand(0x00 | (X & 0x0F));   //设置X位置低4位

}

/**

  * @brief  OLED清屏

  * @param  无

  * @retval 无

  */

void OLED_Clear(void)

{  

 uint8_t i, j;

 for (j = 0; j < 8; j++)

 {

  OLED_SetCursor(j, 0);

  for(i = 0; i < 128; i++)

  {

   OLED_WriteData(0x00);

  }

 }

}

/**

  * @brief  OLED显示一个字符

  * @param  Line 行位置,范围:1~4

  * @param  Column 列位置,范围:1~16

  * @param  Char 要显示的一个字符,范围:ASCII可见字符

  * @retval 无

  */

void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)

{       

 uint8_t i;

 OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8);  //设置光标位置在上半部分

 for (i = 0; i < 8; i++)

 {

  OLED_WriteData(OLED_F8x16[Char - ' '][i]);   //显示上半部分内容

 }

 OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //设置光标位置在下半部分

 for (i = 0; i < 8; i++)

 {

  OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]);  //显示下半部分内容

 }

}

/**

  * @brief  OLED显示字符串

  * @param  Line 起始行位置,范围:1~4

  * @param  Column 起始列位置,范围:1~16

  * @param  String 要显示的字符串,范围:ASCII可见字符

  * @retval 无

  */

void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)

{

 uint8_t i;

 for (i = 0; String[i] != '\0'; i++)

 {

  OLED_ShowChar(Line, Column + i, String[i]);

 }

}

/**

  * @brief  OLED次方函数

  * @retval 返回值等于X的Y次方

  */

uint32_t OLED_Pow(uint32_t X, uint32_t Y)

{

 uint32_t Result = 1;

 while (Y--)

 {

  Result *= X;

 }

 return Result;

}

/**

  * @brief  OLED显示数字(十进制,正数)

  * @param  Line 起始行位置,范围:1~4

  * @param  Column 起始列位置,范围:1~16

  * @param  Number 要显示的数字,范围:0~4294967295

  * @param  Length 要显示数字的长度,范围:1~10

  * @retval 无

  */

void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)

{

 uint8_t i;

 for (i = 0; i < Length; i++)       

 {

  OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');

 }

}

/**

  * @brief  OLED显示数字(十进制,带符号数)

  * @param  Line 起始行位置,范围:1~4

  * @param  Column 起始列位置,范围:1~16

  * @param  Number 要显示的数字,范围:-2147483648~2147483647

  * @param  Length 要显示数字的长度,范围:1~10

  * @retval 无

  */

void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)

{

 uint8_t i;

 uint32_t Number1;

 if (Number >= 0)

 {

  OLED_ShowChar(Line, Column, '+');

  Number1 = Number;

 }

 else

 {

  OLED_ShowChar(Line, Column, '-');

  Number1 = -Number;

 }

 for (i = 0; i < Length; i++)       

 {

  OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');

 }

}

/**

  * @brief  OLED显示数字(十六进制,正数)

  * @param  Line 起始行位置,范围:1~4

  * @param  Column 起始列位置,范围:1~16

  * @param  Number 要显示的数字,范围:0~0xFFFFFFFF

  * @param  Length 要显示数字的长度,范围:1~8

  * @retval 无

  */

void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)

{

 uint8_t i, SingleNumber;

 for (i = 0; i < Length; i++)       

 {

  SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;

  if (SingleNumber < 10)

  {

   OLED_ShowChar(Line, Column + i, SingleNumber + '0');

  }

  else

  {

   OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');

  }

 }

}

/**

  * @brief  OLED显示数字(二进制,正数)

  * @param  Line 起始行位置,范围:1~4

  * @param  Column 起始列位置,范围:1~16

  * @param  Number 要显示的数字,范围:0~1111 1111 1111 1111

  * @param  Length 要显示数字的长度,范围:1~16

  * @retval 无

  */

void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)

{

 uint8_t i;

 for (i = 0; i < Length; i++)       

 {

  OLED_ShowChar(Line, Column + i, Number / OLED_Pow(2, Length - i - 1) % 2 + '0');

 }

}

/**

  * @brief  OLED初始化

  * @param  无

  * @retval 无

  */

void OLED_Init(void)

{

 uint32_t i, j;

 for (i = 0; i < 1000; i++)   //上电延时

 {

  for (j = 0; j < 1000; j++);

 }

 OLED_I2C_Init();   //端口初始化

 OLED_WriteCommand(0xAE); //关闭显示

 OLED_WriteCommand(0xD5); //设置显示时钟分频比/振荡器频率

 OLED_WriteCommand(0x80);

 OLED_WriteCommand(0xA8); //设置多路复用率

 OLED_WriteCommand(0x3F);

 OLED_WriteCommand(0xD3); //设置显示偏移

 OLED_WriteCommand(0x00);

 OLED_WriteCommand(0x40); //设置显示开始行

 OLED_WriteCommand(0xA1); //设置左右方向,0xA1正常 0xA0左右反置

 OLED_WriteCommand(0xC8); //设置上下方向,0xC8正常 0xC0上下反置

 OLED_WriteCommand(0xDA); //设置COM引脚硬件配置

 OLED_WriteCommand(0x12);

 OLED_WriteCommand(0x81); //设置对比度控制

 OLED_WriteCommand(0xCF);

 OLED_WriteCommand(0xD9); //设置预充电周期

 OLED_WriteCommand(0xF1);

 OLED_WriteCommand(0xDB); //设置VCOMH取消选择级别

 OLED_WriteCommand(0x30);

 OLED_WriteCommand(0xA4); //设置整个显示打开/关

 OLED_WriteCommand(0xA6); //设置正常/倒转显示

 OLED_WriteCommand(0x8D); //设置充电泵

 OLED_WriteCommand(0x14);

 OLED_WriteCommand(0xAF); //开启显示

 OLED_Clear();    //OLED清屏

DHT11.c:

#include "Config.h"

//数据

unsigned int rec_data[4];

//对于stm32来说,是输出

void DH11_GPIO_Init_OUT(void)

{

 GPIO_InitTypeDef GPIO_InitStructure;

 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

 GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_Out_PP; //推挽输出

 GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_11;

 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

 GPIO_Init(GPIOB, &GPIO_InitStructure);

}

//对于stm32来说,是输入

void DH11_GPIO_Init_IN(void)

{

 GPIO_InitTypeDef GPIO_InitStructure;

 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

 GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IN_FLOATING; //浮空输入

 GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_11;

 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

 GPIO_Init(GPIOB, &GPIO_InitStructure);

}

//主机发送开始信号

void DHT11_Start(void)

{

 DH11_GPIO_Init_OUT(); //输出模式

 dht11_high; //先拉高

 Delay_us(30);

 dht11_low; //拉低电平至少18us

 Delay_ms(20);

 dht11_high; //拉高电平20~40us

 Delay_us(30);

 DH11_GPIO_Init_IN(); //输入模式

}

//获取一个字节

char DHT11_Rec_Byte(void)

{

 unsigned char i = 0;

 unsigned char data;

 for(i=0;i<8;i++) //1个数据就是1个字节byte,1个字节byte有8位bit

 {

  while( Read_Data == 0); //从1bit开始,低电平变高电平,等待低电平结束

  Delay_us(30); //延迟30us是为了区别数据0和数据1,0只有26~28us

  data <<= 1; //左移

  if( Read_Data == 1 ) //如果过了30us还是高电平的话就是数据1

  {

   data |= 1; //数据+1

  }

  while( Read_Data == 1 ); //高电平变低电平,等待高电平结束

 }

 return data;

}

//获取数据

void DHT11_REC_Data(void)

{

 unsigned int R_H,R_L,T_H,T_L;

 unsigned char RH,RL,TH,TL,CHECK;

 DHT11_Start(); //主机发送信号

 dht11_high; //拉高电平

 if( Read_Data == 0 ) //判断DHT11是否响应

 {

  while( Read_Data == 0); //低电平变高电平,等待低电平结束

  while( Read_Data == 1); //高电平变低电平,等待高电平结束

  R_H = DHT11_Rec_Byte();

  R_L = DHT11_Rec_Byte();

  T_H = DHT11_Rec_Byte();

  T_L = DHT11_Rec_Byte();

  CHECK = DHT11_Rec_Byte(); //接收5个数据

  dht11_low; //当最后一bit数据传送完毕后,DHT11拉低总线 50us

  Delay_us(55); //这里延时55us

  dht11_high; //随后总线由上拉电阻拉高进入空闲状态。

  if(R_H + R_L + T_H + T_L == CHECK) //和检验位对比,判断校验接收到的数据是否正确

  {

   RH = R_H;

   RL = R_L;

   TH = T_H;

   TL = T_L;

  }

 }

 rec_data[0] = RH;

 rec_data[1] = RL;

 rec_data[2] = TH;

 rec_data[3] = TL;

}

GP2Y10.c:

#include "Config.h"

//初始化ADC

//这里我们仅以规则通道为例

//我们默认将开启通道0~3 

void  Adc_Init(void)

{  

 ADC_InitTypeDef ADC_InitStructure;

 GPIO_InitTypeDef GPIO_InitStructure;

 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE );   //使能ADC1通道时钟

 RCC_ADCCLKConfig(RCC_PCLK2_Div6);   //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M

 //PA0 作为模拟通道输入引脚                         

 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;

 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;  //模拟输入引脚

 GPIO_Init(GPIOA, &GPIO_InitStructure);

 //PB1 作为脉冲输出引脚   

 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;                  

 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

 GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;           

 GPIO_Init(GPIOB, &GPIO_InitStructure);

 GP2Y_High;

 ADC_DeInit(ADC1);  //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值

 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1和ADC2工作在独立模式

 ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式

 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //模数转换工作在单次转换模式

 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动

 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐

 ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目

 ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器  

 ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1

 ADC_ResetCalibration(ADC1); //使能复位校准  

 while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束

 ADC_StartCalibration(ADC1);  //开启AD校准

 while(ADC_GetCalibrationStatus(ADC1));  //等待校准结束

// ADC_SoftwareStartConvCmd(ADC1, ENABLE);  //使能指定的ADC1的软件转换启动功能

}      

//获得ADC值

//ch:通道值 0~3

u16 Get_Adc(u8 ch)   

{

   //设置指定ADC的规则组通道,一个序列,采样时间

 ADC_RegularChannelConfig(ADC1, ch, 0, ADC_SampleTime_239Cycles5 ); //ADC1,ADC通道,采样时间为239.5周期          

  

 ADC_SoftwareStartConvCmd(ADC1, ENABLE);  //使能指定的ADC1的软件转换启动功能 

  

 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束

 return ADC_GetConversionValue(ADC1); //返回最近一次ADC1规则组的转换结果

}

float GetGP2Y(void)

{

    u32 AD_PM;  

 float pm;

    GP2Y_Low;

    Delay_us(280);

    AD_PM = Get_Adc(ADC_Channel_0); //PA0

    Delay_us(40);

    GP2Y_High;

    Delay_us(9680);

    pm = 0.17*AD_PM-0.1; //转换公式

    //printf("%f\n",pm);

 return pm;

}

u16 Get_GP2Y_Average(u8 times)

{

 u32 pm_val=0;

 u8 t;

 for(t=0;t<times;t++)

 {

  pm_val+=GetGP2Y();

  Delay_ms(5);

 }

 return pm_val/times;

}

附录三:元器件清单表

元器件

个数(/个)

Stm32f103z8t6最小系统板

1

OLED

1

GP2Y10

1

DHT11

1

按键

2

滤波电容

1

限流电阻

1

附录五:硬件实物图

附录六:PCB版图

附录七:操作说明书

接通电源后,系统便可正常工作,每间隔1s会主动检测环境温湿度和PM2.5浓度,并显示在OLED屏上,并当PM2.5浓度超过设定阈值时通过蜂鸣器报警。除此外,装置上装有FB5、FB6两个按键,按下FB5按键,则设定阈值加5,按下FB6按键,则设定阈值减5。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐