基于STM32的环境质量监测课程设计详解
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 显示屏的界面可以进一步美化,显示更多的环境质量信息,或通过按键实现多页面显示,便于用户查看不同类型的数据。
参考文献
- 吴建平,传感器原理及应用[M],北京: 机械工业出版社, 2021.
- 董锟、何娜,基于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。
作者:xixi….嘻嘻