目录
[1.1 硬件清单](#1.1 硬件清单)
[1.2 接线方案](#1.2 接线方案)
[1.3 具体接线图](#1.3 具体接线图)
[1.4 连接实物图](#1.4 连接实物图)
[2.1 数据采集算法](#2.1 数据采集算法)
[2.2 数据可视化算法](#2.2 数据可视化算法)
[2.3 循环队列结构](#2.3 循环队列结构)
[2.4 数据访问模式](#2.4 数据访问模式)
[2.5 完整代码](#2.5 完整代码)
[3.1 操作流程](#3.1 操作流程)
[3.2 视频演示](#3.2 视频演示)
[4.1 光学检测原理](#4.1 光学检测原理)
[4.2 输出特性曲线](#4.2 输出特性曲线)
[4.3 工作原理](#4.3 工作原理)
[Q1: 传感器读数总是为0或负值怎么办?](#Q1: 传感器读数总是为0或负值怎么办?)
[Q2: 数据波动很大如何优化?](#Q2: 数据波动很大如何优化?)
[Q3: 如何校准传感器?](#Q3: 如何校准传感器?)
1)项目概述
本项目基于STM32F407VET6微控制器的零知增强板和GP2Y1014AU粉尘传感器,开发了一套完整的空气质量监测系统。系统能够实时检测空气中的PM2.5浓度,通过TFT显示屏直观显示当前空气质量状况和历史数据趋势图,并通过串口输出监测数据。适用于室内空气质量监测、环境监测站等场景,为用户提供准确的粉尘浓度数据和空气质量评估。
(2)项目难点及解决方案
问题描述:GP2Y1014AU输出信号微弱且易受干扰
**解决方案:**采用精确的时序控制,确保在LED开启后280us进行采样,此时输出信号最稳定
一、硬件系统设计
1.1 硬件清单
组件 | 规格 | 数量 |
---|---|---|
主控板 | STM32F407VET6 | 1 |
粉尘传感器 | GP2Y1014AU | 1 |
TFT显示屏 | ST7789 240x240 | 1 |
电阻 | 150Ω | 1 |
电容 | 220uF | 1 |
1.2 接线方案
根据代码中定义的引脚,硬件连接如下:
STM32F407VET6引脚 | 连接组件 | 引脚功能 |
---|---|---|
5V | GP2Y1014AU VCC | 电源正极 |
GND | GP2Y1014AU GND | 电源地 |
A0 | GP2Y1014AU AOUT | 模拟输出 |
3 | GP2Y1014AU ILED | LED驱动 |
53 | TFT_CS | 片选 |
7 | TFT_DC | 数据/命令 |
6 | TFT_RST | 复位 |
3.3V | TFT VCC | 电源 |
GND | TFT GND | 地 |
1.3 具体接线图
重要提示:GP2Y1014AU需要外接150Ω限流电阻和220uF滤波电容,以确保红外LED工作稳定
1.4 连接实物图
二、代码架构讲解
2.1 数据采集算法
cpp
void loop(void) {
/* === 阶段1: 传感器驱动时序控制 === */
digitalWrite(iled, HIGH); // 开启红外LED
delayMicroseconds(280); // 关键延时1 - 等待输出稳定
adcvalue = analogRead(vout); // ADC采样
delayMicroseconds(40); // 关键延时2 - 维持采样窗口
digitalWrite(iled, LOW); // 关闭LED以降低功耗
/* === 阶段2: 信号处理与转换 === */
// ADC值转电压值 (12位ADC, 0-4095对应0-5000mV)
voltage = (SYS_VOLTAGE / 1024.0) * adcvalue;
/* === 阶段3: 浓度计算算法 === */
if (voltage >= NO_DUST_VOLTAGE) {
voltage -= NO_DUST_VOLTAGE; // 扣除基准电压
density = voltage * COV_RATIO; // 线性转换
} else {
density = 0; // 低于基准电压视为无尘
}
/* === 阶段4: 数据存储与显示 === */
history.push(density, millis()); // 存入循环队列
delay(1000); // 1秒采样周期
}
红外LED开启后,需要等待280us延时至光电晶体管输出稳定、在40us最佳检测窗口期内完成ADC转换
2.2 数据可视化算法
cpp
void drawGraphLine() {
int maxDensity = 200; // Y轴最大值
int prevX = -1, prevY = -1; // 前一个点坐标
int dataCount = history.getCount();
for(int i = 0; i < dataCount; i++) {
float value = history.getValue(i);
if(value < 0.1) continue; // 数据有效性检查
// 坐标映射算法
int x = 30 + (i * 190 / min(dataCount, history.getSize()));
int y = 180 - constrain((int)(value * 110 / maxDensity), 0, 110);
// 线段绘制
if(prevX >= 0) {
uint16_t color = getColorByDensity(value);
tft.drawLine(prevX, prevY, x, y, color);
tft.fillCircle(x, y, 1, color); // 数据点标记
}
prevX = x;
prevY = y;
}
}
// 根据浓度值获取对应颜色
uint16_t getColorByDensity(float density) {
if(density < WARNING_THRESHOLD) return ST77XX_GREEN;
else if(density < DANGER_THRESHOLD) return ST77XX_YELLOW;
else if(density < CRITICAL_THRESHOLD) return ST77XX_ORANGE;
else return ST77XX_RED;
}
绘制溶度值可视化波形,根据溶度值设置对应曲线的颜色
2.3 循环队列结构
cpp
class CircularBuffer {
private:
static const int SIZE = 240; // 队列容量:4分钟数据(240秒)
float buffer[SIZE]; // 浓度数据存储数组
unsigned long timestamps[SIZE]; // 时间戳存储数组
int head; // 队首指针 - 指向下一个写入位置
int count; // 当前元素数量
public:
// 构造函数:初始化队列
CircularBuffer() : head(0), count(0) {
// 数组初始化为0
for(int i = 0; i < SIZE; i++) {
buffer[i] = 0;
timestamps[i] = 0;
}
}
// 数据插入算法 - O(1)时间复杂度
void push(float value, unsigned long timestamp) {
// 1. 数据写入队首位置
buffer[head] = value;
timestamps[head] = timestamp;
// 2. 队首指针循环前进
head = (head + 1) % SIZE;
// 3. 更新元素计数(队列未满时)
if(count < SIZE) count++;
}
// 数据访问算法 - O(1)时间复杂度
float getValue(int index) {
// 边界检查
if(index >= count) return 0;
// 计算环形索引:从最新数据向前推算
// head-1: 最新数据位置
// + SIZE: 避免负数
// % SIZE: 环形映射
int pos = (head - 1 - index + SIZE) % SIZE;
return buffer[pos];
}
}
使用固定大小数组模拟队列,当队列满时自动覆盖最旧数据
2.4 数据访问模式
cpp
// 获取历史数据的时间序列
void displayHistoricalData() {
int dataCount = history.getCount();
Serial.println("=== 历史数据 ===");
for(int i = 0; i < dataCount; i++) {
// i=0: 最新数据, i=1: 前一个数据, 以此类推
float value = history.getValue(i);
unsigned long time = history.getTimestamp(i);
Serial.print("T-");
Serial.print(i);
Serial.print("s: ");
Serial.print(value);
Serial.println(" ug/m3");
}
}
将循环队列中的数据通过串口打印输出
2.5 完整代码
cpp
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <SPI.h>
// ST7789 display pin definitions
#define TFT_CS 53
#define TFT_DC 7
#define TFT_RST 6
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
// Custom colors
#define ST77XX_NAVY 0x0010
#define ST77XX_DARKGREEN 0x0320
#define ST77XX_MAROON 0x8000
#define ST77XX_GRAY 0x8410
#define ST77XX_ORANGE 0xFC00
#define ST77XX_CYAN 0x07FF
#define ST77XX_PURPLE 0x8010
#define ST77XX_OLIVE 0x7BE0
// Dust sensor parameters
#define COV_RATIO 0.17 // (ug/m3) / mv
#define NO_DUST_VOLTAGE 200 // mv
#define SYS_VOLTAGE 5000 // ADC参考电压
// 报警阈值
#define WARNING_THRESHOLD 35.0
#define DANGER_THRESHOLD 75.0
#define CRITICAL_THRESHOLD 150.0
/*
I/O define
*/
const int iled = 3; //drive the led of sensor
const int vout = A0; //analog input
/*
variable
*/
float density, voltage;
int adcvalue;
// Variables
unsigned long lastScreenSwitch = 0;
unsigned long startTime = 0;
int currentScreen = 0; // 0: Data display, 1: Graph display
const int screenSwitchInterval = 10000; // 10 seconds switch
// 使用循环队列数据结构存储历史数据
class CircularBuffer {
private:
static const int SIZE = 240; // 4分钟数据(240秒)
float buffer[SIZE];
unsigned long timestamps[SIZE];
int head;
int count;
public:
CircularBuffer() : head(0), count(0) {
for(int i = 0; i < SIZE; i++) {
buffer[i] = 0;
timestamps[i] = 0;
}
}
void push(float value, unsigned long timestamp) {
buffer[head] = value;
timestamps[head] = timestamp;
head = (head + 1) % SIZE;
if(count < SIZE) count++;
}
float getValue(int index) {
if(index >= count) return 0;
int pos = (head - 1 - index + SIZE) % SIZE;
return buffer[pos];
}
unsigned long getTimestamp(int index) {
if(index >= count) return 0;
int pos = (head - 1 - index + SIZE) % SIZE;
return timestamps[pos];
}
int getCount() { return count; }
int getSize() { return SIZE; }
};
CircularBuffer history;
void showStartupScreen() {
tft.fillScreen(ST77XX_BLACK);
// 绘制渐变背景
for(int i = 0; i < 240; i++) {
tft.drawFastVLine(i, 0, 240, tft.color565(i/3, i/4, i/2));
}
// 标题
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(3);
tft.setCursor(40, 50);
tft.print("DUST");
tft.setTextColor(ST77XX_CYAN);
tft.setTextSize(2);
tft.setCursor(120, 50);
tft.print("MONITOR");
// 版本信息
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(1);
tft.setCursor(60, 90);
tft.print("v2.0 - Professional Edition");
// 加载动画
for(int i = 0; i < 200; i += 5) {
tft.fillRect(20, 150, i, 10, ST77XX_GREEN);
delay(30);
}
// 初始化信息
tft.setTextColor(ST77XX_YELLOW);
tft.setTextSize(1);
tft.setCursor(50, 180);
tft.print("Initializing Sensor...");
delay(1000);
tft.fillRect(50, 180, 140, 10, ST77XX_BLACK);
tft.setCursor(70, 180);
tft.print("Ready to Monitor!");
delay(1000);
}
void setup(void) {
pinMode(iled, OUTPUT);
digitalWrite(iled, LOW); //iled default closed
Serial.begin(9600); //send and receive at 9600 baud
Serial.println("Dust Monitor System Starting...");
// Initialize display
tft.init(240, 240);
tft.setRotation(1);
tft.fillScreen(ST77XX_BLACK);
// 显示启动界面
showStartupScreen();
startTime = millis();
// 绘制静态界面
drawStaticUI();
Serial.println("System initialized");
}
void drawStaticUI() {
// Clear screen
tft.fillScreen(ST77XX_BLACK);
// Draw title bar
tft.fillRect(0, 0, 240, 30, ST77XX_NAVY);
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(2);
tft.setCursor(60, 8);
tft.print("Dust Monitor");
// Draw bottom status bar
tft.fillRect(0, 210, 240, 30, ST77XX_DARKGREEN);
}
void drawDataScreen(float density, float voltage) {
// Partial refresh data area (30-210 pixel height)
tft.fillRect(0, 30, 240, 180, ST77XX_BLACK);
// 显示传感器状态
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(1);
tft.setCursor(10, 35);
tft.print("Sensor Status: ");
tft.setTextColor(ST77XX_GREEN);
tft.print("ACTIVE");
// 显示基准电压
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(10, 50);
tft.print("Base Voltage: ");
tft.print(NO_DUST_VOLTAGE);
tft.print(" mV");
// Display current concentration value (large font)
tft.setTextColor(ST77XX_CYAN);
tft.setTextSize(3);
tft.setCursor(40, 80);
tft.print("Dust:");
// Change color based on concentration value
uint16_t dustColor;
if(density < WARNING_THRESHOLD) {
dustColor = ST77XX_GREEN;
} else if(density < DANGER_THRESHOLD) {
dustColor = ST77XX_YELLOW;
} else if(density < CRITICAL_THRESHOLD) {
dustColor = ST77XX_ORANGE;
} else {
dustColor = ST77XX_RED;
}
tft.setTextColor(dustColor);
tft.setTextSize(4);
tft.setCursor(60, 110);
if(density < 0.1) {
tft.print("0.0");
} else {
tft.print(density, 1);
}
tft.setTextSize(2);
tft.setCursor(180, 120);
tft.print("ug/m3");
// Display voltage value
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(2);
tft.setCursor(40, 150);
tft.print("Voltage: ");
tft.print(voltage, 1);
tft.print(" mV");
// Display quality level with background
int levelWidth = 120;
tft.fillRect(40, 185, levelWidth, 20, ST77XX_DARKGREEN);
tft.setCursor(45, 190);
tft.print("Level: ");
String levelText;
if(density < WARNING_THRESHOLD) {
tft.setTextColor(ST77XX_GREEN);
levelText = "EXCELLENT";
} else if(density < DANGER_THRESHOLD) {
tft.setTextColor(ST77XX_YELLOW);
levelText = "GOOD";
} else if(density < CRITICAL_THRESHOLD) {
tft.setTextColor(ST77XX_ORANGE);
levelText = "LIGHT POLLUTED";
} else if(density < 200) {
tft.setTextColor(ST77XX_RED);
levelText = "MODERATE";
} else {
tft.setTextColor(ST77XX_MAROON);
levelText = "HEAVY";
}
tft.print(levelText);
// 报警指示器
if(density > WARNING_THRESHOLD) {
tft.fillCircle(220, 110, 8, dustColor);
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(1);
tft.setCursor(215, 108);
tft.print("!");
}
}
void drawGraphScreen() {
// Draw graph framework
tft.fillRect(0, 30, 240, 180, ST77XX_BLACK);
// Graph title with time info
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(2);
tft.setCursor(70, 35);
tft.print("TREND GRAPH");
// 显示时间范围
tft.setTextSize(1);
tft.setCursor(80, 55);
tft.print("Last 4 Minutes");
// Draw axes with labels
tft.drawRect(30, 70, 190, 110, ST77XX_GRAY);
// Y-axis scale and labels
for(int i = 0; i <= 200; i += 50) {
int y = 180 - (i * 110 / 200);
tft.drawLine(28, y, 32, y, ST77XX_GRAY);
tft.setTextSize(1);
tft.setCursor(5, y-4);
tft.print(i);
}
// X-axis time labels
tft.setCursor(30, 185);
tft.print("-4m");
tft.setCursor(100, 185);
tft.print("-2m");
tft.setCursor(170, 185);
tft.print("Now");
// 绘制阈值线
drawThresholdLines();
// Draw curve
drawGraphLine();
}
void drawThresholdLines() {
// 警告阈值线
int warnY = 180 - (WARNING_THRESHOLD * 110 / 200);
tft.drawLine(30, warnY, 220, warnY, ST77XX_GREEN);
tft.setTextSize(1);
tft.setCursor(222, warnY-4);
tft.print("Good");
// 危险阈值线
int dangerY = 180 - (DANGER_THRESHOLD * 110 / 200);
tft.drawLine(30, dangerY, 220, dangerY, ST77XX_ORANGE);
tft.setCursor(222, dangerY-4);
tft.print("Warn");
// 严重阈值线
int criticalY = 180 - (CRITICAL_THRESHOLD * 110 / 200);
tft.drawLine(30, criticalY, 220, criticalY, ST77XX_RED);
tft.setCursor(222, criticalY-4);
tft.print("Alert");
}
void drawGraphLine() {
int maxDensity = 200;
int prevX = -1, prevY = -1;
int dataCount = history.getCount();
for(int i = 0; i < dataCount; i++) {
float value = history.getValue(i);
if(value < 0.1) continue; // 忽略接近0的值
// 计算X坐标(时间轴)
int x = 30 + (i * 190 / min(dataCount, history.getSize()));
// 计算Y坐标(浓度轴)
int y = 180 - constrain((int)(value * 110 / maxDensity), 0, 110);
if(prevX >= 0) {
// 根据浓度值改变线条颜色
uint16_t color;
if(value < WARNING_THRESHOLD) color = ST77XX_GREEN;
else if(value < DANGER_THRESHOLD) color = ST77XX_YELLOW;
else if(value < CRITICAL_THRESHOLD) color = ST77XX_ORANGE;
else color = ST77XX_RED;
tft.drawLine(prevX, prevY, x, y, color);
// 在数据点处绘制小点
tft.fillCircle(x, y, 1, color);
}
prevX = x;
prevY = y;
}
}
void loop(void) {
/*
get adcvalue
*/
digitalWrite(iled, HIGH);
delayMicroseconds(280);
adcvalue = analogRead(vout);
delayMicroseconds(40);
digitalWrite(iled, LOW);
/*
covert voltage (mv)
*/
voltage = (SYS_VOLTAGE / 1024.0) * adcvalue;
/*
voltage to density
*/
if (voltage >= NO_DUST_VOLTAGE) {
voltage -= NO_DUST_VOLTAGE;
density = voltage * COV_RATIO;
} else
density = 0;
// Add to historical data with timestamp
history.push(density, millis());
// Check if screen needs to switch
if(millis() - lastScreenSwitch > screenSwitchInterval) {
currentScreen = (currentScreen + 1) % 2;
lastScreenSwitch = millis();
// Update bottom status bar
tft.fillRect(0, 210, 240, 30, ST77XX_DARKGREEN);
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(1);
tft.setCursor(10, 218);
tft.print(currentScreen == 0 ? "Data Screen" : "Graph Screen");
tft.setCursor(170, 218);
tft.print("Switch in 10s");
// Redraw current screen
if(currentScreen == 0) {
drawDataScreen(density, voltage);
} else {
drawGraphScreen();
}
} else {
// Partial refresh current screen
if(currentScreen == 0) {
drawDataScreen(density, voltage);
} else {
// Graph screen only updates graph part
tft.fillRect(30, 70, 190, 110, ST77XX_BLACK);
tft.drawRect(30, 70, 190, 110, ST77XX_GRAY);
drawThresholdLines();
drawGraphLine();
}
}
// 串口输出
Serial.print("The current dust concentration is: ");
Serial.print(density);
Serial.print(" ug/m3\n");
delay(1000); // 1秒采样间隔
}
循环队列算法工作原理
①初始状态(空队列)

②插入数据A

③插入数据B、C、D

④插入数据E(队列将满)

⑤插入数据F(覆盖最旧数据A)

**优势:**固定大小内存,避免动态分配开销、插入和访问都是O(1)操作
三、项目结果演示
3.1 操作流程
①系统启动
连接电源后,系统显示启动界面,包含加载动画

②数据监测
系统自动进入数据监测模式,显示当前粉尘浓度、实时电压值及空气质量等级

③趋势图界面
每10秒自动切换数据屏和趋势图屏,趋势波形图为绿色,表示当前空气质量优

④数据输出
系统通过零知IDE串口实时输出监测数据:
Dust Monitor System Starting...
System initialized
The current dust concentration is: 25.3 ug/m3
3.2 视频演示
GP2Y1014AU粉尘监测系统
系统启动并显示加载界面,随后进入主监测界面,显示当前空气质量的数据,实时检测气体溶度
四、GP2Y1014AU粉尘传感器工作原理
4.1 光学检测原理
GP2Y1014AU基于光散射原理工作。传感器内部有一个红外发光二极管和一个光电晶体管,成对角布置:

红外LED发射光束(发射阶段)、空气中粉尘颗粒对光线产生散射(散射阶段)、光电晶体管检测散射光强度(检测阶段)、输出与粉尘浓度成正比的电压信号(输出阶段)
4.2 输出特性曲线
GP2Y1014AU0F 传感器输出电压与灰尘浓度关系在 0 到 0.5mg/m3 范围内成线性关系,如下图所示:

在 0 ~ 0.5mg/m3 范围内取部分电压与浓度的对应值,得到如下转换公式,其中 v 为电压(单位 V),d 为浓度(单位 mg/m3)
v = 5.88∗d+0.6
转换为通过 v 计算 d,得到如下公式:
d = (v−0.6)∗0.17
4.3 工作原理
①置 ILED 引脚高电平,开启传感器内部红外二极管;
ILED 端输入脉冲波形要求

②等待 0.28ms(确保输出波形稳定),外部控制器对 AOUT 引脚电压采样,持续 0.04ms;
ILED 输入脉冲与 AOUT 的采样时序

③采样后置 ILED 引脚低电平,关闭红外二极管;
④依据电压与浓度的对应关系,计算当前空气中的灰尘浓度
ILED引脚驱动MOS管通断

五、常见问题解答
Q1: 传感器读数总是为0或负值怎么办?
A:这通常是由于电压转换公式不正确或ADC配置问题导致的:
检查ADC参考电压设置、验证NO_DUST_VOLTAGE值是否适合您的传感器、确保时序控制精确,特别是280us延时
Q2: 数据波动很大如何优化?
A:可以采取以下措施:
增加软件滤波算法(如移动平均)、确保传感器通气孔不被遮挡
Q3: 如何校准传感器?
A:校准步骤:
在清洁空气中运行传感器,记录基准电压、根据实际值调整NO_DUST_VOLTAGE参数
项目资源
GP2Y1014AU数据手册:GP2Y1010AU0F datasheet