STM32F103C8T6+MQ-2烟雾传感器+OLED完整教学工程|ADC 采样、均值滤波、气体报警实现
前言
最近做了一套面向单片机课堂教学的 MQ-2 烟雾检测工程,硬件采用 STM32F103C8T6 最小系统 + MQ-2 气敏传感器 + I2C OLED 屏幕,完整实现气体浓度采集、电压换算、20 次均值防抖滤波、阈值烟雾报警,所有原理、接线、代码逻辑、调试坑点全部整理完毕,新手也能直接复刻。
一、整体功能流程
整套工程信号流转逻辑非常清晰: 烟雾 / 可燃气体浓度改变 → MQ-2 内部敏感电阻变化 → AO 模拟输出电压变化 → STM32 PA0 ADC 采集模拟量 → 20 次均值滤波消除抖动 → 换算电压、相对电平 → OLED 实时打印数据 + 安全 / 报警状态 一句话概括:MQ-2 把气体信号转为电压,STM32 将电压转为数字量,最终 OLED 可视化展示。
二、硬件接线对照表
完整引脚连接
| 模块引脚 | STM32 引脚 | 功能说明 |
|---|---|---|
| MQ-2 AO | PA0 | ADC1 通道 0 模拟输入 |
| MQ-2 VCC | 5V | 传感器加热丝供电 |
| MQ-2 GND | GND | 共地必不可少 |
| OLED SCL | PB8 | 软件 I2C 时钟 |
| OLED SDA | PB9 | 软件 I2C 数据 |
| OLED VCC | 3.3V | 屏幕供电 |
| OLED GND | GND | 屏幕地 |

重点避坑:AO 分压保护电路
STM32F103 的 ADC 引脚耐压仅 0~3.3V,但 MQ-2 模块 5V 供电时 AO 最高输出接近 5V,直接直连会烧毁单片机 IO 口。 正确接法:MQ-2 AO 串联分压电路后再接 PA0,模块 GND 与单片机共地。
OLED 显示的 Volt 为分压后 PA0 端电压,并非 MQ-2 原始 AO 电压。
为什么工程不用 DO 数字输出?
MQ-2 模块自带 DO 数字引脚 + 可调电位器:
- DO:内置比较器,仅高低电平,只能判断是否超标,无法观察气体变化过程;
- AO:连续模拟电压,可实时读取浓度变化曲线,适合教学演示,因此本工程全程使用 AO 采集。
三、MQ-2 传感器核心工作原理
MQ-2 核心敏感材料为二氧化锡 SnO₂,内部集成加热丝,必须加热才能正常检测:
- 洁净空气:SnO₂导电性能差,传感器内阻 Rs 很大;
- 接触烟雾、液化气、甲烷、酒精、氢气等还原性气体:敏感层导电性提升,Rs 阻值下降;
- 模块配套测量电路设计为:Rs 减小 → AO 输出电压升高;
- 程序通过 ADC 数值大小判断是否触发烟雾报警。
⚠️ 重要局限:MQ-2 广谱响应可燃气体,无法区分具体是哪种气体,仅能检测有无有害气体 / 烟雾。

四、STM32 ADC 采样原理
单片机使用 12 位 ADC1 通道 0(PA0),参考电压 3.3V:
- 输入电压范围:0 ~ 3.3V
- ADC 数字输出范围:0 ~ 4095 换算公式:
采集电压(V) = ADC原始值 × 3.3 / 4095
对应参考数值: 0V → 0;0.6V → 744;1.65V → 2048;3.3V → 4095
程序封装转换函数MQ2_RawToMilliVolts(),将原始 ADC 值转为毫伏,格式化输出 x.xxV 电压。
五、20 次均值滤波(解决数值跳变问题)
MQ-2 模拟输出自带高频波动,单次采样屏幕数字疯狂跳动,因此采用多次采样取平均算法:
- 循环采集 20 次 ADC 值,单次间隔 2ms;
- 全部采集完成后求和取平均值;
- 使用均值计算电压与电平百分比,屏幕显示稳定无抖动。
工程文件与对应函数分工
| 文件 | 核心函数 | 功能 |
|---|---|---|
| mq2_adc.c | MQ2_ADC_Init() | ADC、PA0 初始化 + ADC 自校准 |
| 同上 | MQ2_ReadRaw() | 单次读取 ADC 原始值 |
| 同上 | MQ2_ReadAverage() | 20 次均值滤波采集 |
| 同上 | MQ2_RawToMilliVolts() | ADC 转电压 |
| main.c | MQ2_ShowData() | OLED 刷新所有数据、报警状态 |
main.c
/*
* MQ-2 smoke demonstration
* MQ-2 AO -> PA0 (ADC1 channel 0)
* OLED SCL -> PB8, SDA -> PB9
*/
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "mq2_adc.h"
#define MQ2_SAMPLE_COUNT 20
#define MQ2_REFRESH_MS 250
#define MQ2_ALARM_THRESHOLD 1800
static void MQ2_ShowData(u16 raw, u16 millivolts, u8 percent);
int main(void)
{
u16 raw;
u16 millivolts;
u8 percent;
delay_init();
NVIC_Configuration();
OLED_Init();
OLED_ColorTurn(0);
OLED_DisplayTurn(0);
OLED_Clear();
OLED_ShowString(16, 0, (u8 *)"MQ-2 Smoke", 16);
OLED_ShowString(16, 24, (u8 *)"AO -> PA0", 16);
OLED_ShowString(8, 48, (u8 *)"Initializing...", 16);
OLED_Refresh();
MQ2_ADC_Init();
delay_ms(1000);
while(1)
{
raw = MQ2_ReadAverage(MQ2_SAMPLE_COUNT);
millivolts = MQ2_RawToMilliVolts(raw);
percent = MQ2_RawToPercent(raw);
MQ2_ShowData(raw, millivolts, percent);
delay_ms(MQ2_REFRESH_MS);
}
}
static void MQ2_ShowData(u16 raw, u16 millivolts, u8 percent)
{
OLED_Clear();
OLED_ShowString(16, 0, (u8 *)"MQ-2 Smoke (PA0)", 12);
OLED_ShowString(0, 13, (u8 *)"ADC:", 12);
OLED_ShowNum(30, 13, raw, 4, 12);
OLED_ShowString(0, 26, (u8 *)"Volt:", 12);
OLED_ShowNum(30, 26, millivolts / 1000U, 1, 12);
OLED_ShowString(36, 26, (u8 *)".", 12);
OLED_ShowNum(42, 26, (millivolts % 1000U) / 10U, 2, 12);
OLED_ShowString(54, 26, (u8 *)"V", 12);
OLED_ShowString(0, 39, (u8 *)"Level:", 12);
OLED_ShowNum(36, 39, percent, 3, 12);
OLED_ShowString(54, 39, (u8 *)"%", 12);
if(raw >= MQ2_ALARM_THRESHOLD)
{
OLED_ShowString(16, 52, (u8 *)"SMOKE ALARM", 12);
}
else
{
OLED_ShowString(16, 52, (u8 *)"Status: SAFE", 12);
}
OLED_Refresh();
}
mq2_adc.c
#include "mq2_adc.h"
#include "delay.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
/*
* 函数名称:MQ2_ADC_Init
* 函数功能:初始化 MQ-2 模块的模拟量采集接口。
* 硬件配置:PA0 配置为模拟输入,使用 ADC1 的通道 0。
* 说明:模拟输入模式会关闭 PA0 的数字输入缓冲,避免影响电压采样;
* ADC 时钟配置为 12MHz,并在初始化后执行一次校准。
*/
void MQ2_ADC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); /* 72 MHz / 6 = 12 MHz */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_DeInit(ADC1);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
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_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
}
/*
* 函数名称:MQ2_ReadRaw
* 函数功能:读取一次 MQ-2 AO 引脚的 12 位 ADC 原始值。
* 返回值 :0~4095,数值越大表示 PA0 上的电压越高。
* 说明 :使用软件触发单次转换,等待转换完成后读取结果。
*/
u16 MQ2_ReadRaw(void)
{
ADC_RegularChannelConfig(ADC1, MQ2_ADC_CHANNEL, 1, ADC_SampleTime_239Cycles5);
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
return ADC_GetConversionValue(ADC1);
}
/*
* 函数名称:MQ2_ReadAverage
* 函数功能:连续采样多次并取平均值,降低 MQ-2 模拟输出的波动。
* 参数 :samples 为采样次数;当其为 0 时,直接返回一次采样结果。
* 返回值 :平均后的 ADC 原始值,范围为 0~4095。
* 说明 :每次采样之间延时 2ms,既能平滑显示又能保持较快响应。
*/
u16 MQ2_ReadAverage(u8 samples)
{
u32 total = 0;
u8 i;
if(samples == 0)
{
return MQ2_ReadRaw();
}
for(i = 0; i < samples; i++)
{
total += MQ2_ReadRaw();
delay_ms(2);
}
return (u16)(total / samples);
}
/*
* 函数名称:MQ2_RawToMilliVolts
* 函数功能:将 ADC 原始值按 3.3V 参考电压换算为 PA0 的电压。
* 参数 :raw 为 ADC 原始值,范围为 0~4095。
* 返回值 :PA0 电压,单位为 mV,范围为 0~3300mV。
* 注意 :若 AO 经过分压后接入 PA0,返回的是分压后的 PA0 电压。
*/
u16 MQ2_RawToMilliVolts(u16 raw)
{
if(raw >= MQ2_ADC_MAX_VALUE)
{
return MQ2_ADC_REFERENCE_MV;
}
return (u16)(((u32)raw * MQ2_ADC_REFERENCE_MV +
(MQ2_ADC_MAX_VALUE / 2U)) / MQ2_ADC_MAX_VALUE);
}
/*
* 函数名称:MQ2_RawToPercent
* 函数功能:将 ADC 原始值换算为 0~100 的相对电平,便于 OLED 显示。
* 参数 :raw 为 ADC 原始值,范围为 0~4095。
* 返回值 :相对电平百分比,范围为 0~100。
* 注意 :该百分比只用于观察变化趋势,不能代表实际烟雾浓度或 ppm。
*/
u8 MQ2_RawToPercent(u16 raw)
{
if(raw >= MQ2_ADC_MAX_VALUE)
{
return 100;
}
return (u8)(((u32)raw * 100U + (MQ2_ADC_MAX_VALUE / 2U)) /
MQ2_ADC_MAX_VALUE);
}
mq2_adc.h
#ifndef __MQ2_ADC_H
#define __MQ2_ADC_H
#include "stm32f10x.h"
#include "stm32f10x_adc.h"
/*
* 文件说明:MQ-2 烟雾模块模拟量采集驱动。
* 硬件连接:MQ-2 的 AO 引脚连接到 STM32 的 PA0(ADC1 通道 0)。
*/
/* ADC1 中与 PA0 对应的通道号。 */
#define MQ2_ADC_CHANNEL ADC_Channel_0
/* STM32F1 ADC 为 12 位,最大有效转换值为 4095。 */
#define MQ2_ADC_MAX_VALUE 4095U
/* ADC 模拟参考电压,单位:mV。 */
#define MQ2_ADC_REFERENCE_MV 3300U
/**
* @brief 初始化 PA0 和 ADC1,用于读取 MQ-2 的 AO 模拟电压。
* @note 初始化过程中会完成 ADC 校准。
*/
void MQ2_ADC_Init(void);
/**
* @brief 读取一次 MQ-2 的 ADC 原始值。
* @return 12 位 ADC 转换结果,范围为 0~4095。
*/
u16 MQ2_ReadRaw(void);
/**
* @brief 多次读取 MQ-2 并取平均值,降低显示抖动。
* @param samples 采样次数;传入 0 时仅采样一次。
* @return 平均后的 12 位 ADC 值,范围为 0~4095。
*/
u16 MQ2_ReadAverage(u8 samples);
/**
* @brief 将 ADC 原始值换算为 PA0 引脚电压。
* @param raw ADC 原始值,范围为 0~4095。
* @return 换算后的电压,单位:mV。
*/
u16 MQ2_RawToMilliVolts(u16 raw);
/**
* @brief 将 ADC 原始值换算为用于 OLED 显示的相对百分比。
* @param raw ADC 原始值,范围为 0~4095。
* @return 相对电平,范围为 0~100。
* @note 该值不是实际烟雾浓度,也不是 ppm。
*/
u8 MQ2_RawToPercent(u16 raw);
#endif
六、OLED 屏幕显示格式说明
上电后屏幕固定输出 4 组数据,示例:
MQ-2 Smoke (PA0)
ADC:0744
Volt:0.60V
Level:018%
Status: SAFE
参数释义:
- ADC:12 位原始采样值,0~4095;
- Volt:PA0 引脚实际采样电压;
- Level:相对变化百分比,仅作趋势参考,≠真实 ppm 浓度;
- Status:SAFE = 安全;SMOKE ALARM = 气体超标报警。
补充:Level 仅用于直观观察浓度升降,想要精准 ppm 数值需要专业标定设备、固定温湿度、固定负载电阻校准。
七、烟雾报警阈值逻辑
阈值宏定义(main.c)
#define MQ2_ALARM_THRESHOLD 1800
判断逻辑
if(平均ADC值 >= MQ2_ALARM_THRESHOLD)
屏幕显示SMOKE ALARM;
else
屏幕显示SAFE;
阈值 1800 对应 PA0 电压:1800*3.3/4095≈1.45V
现场调试阈值步骤
- MQ-2 上电预热数分钟,等待读数稳定;
- 记录干净空气下的 ADC 基线数值;
- 靠近烟雾观察 ADC 上升区间;
- 将阈值设置在基线与烟雾响应值中间,按需微调。
八、MQ-2 使用关键注意事项(踩坑总结)
- 预热必不可少:内部加热丝,上电数值不会立刻稳定;官方标准工况需预热 24h,课堂演示预热 3~5 分钟即可;
- 环境干扰大:温湿度、酒精、油烟、香水都会拉高 AO 电压,属于传感器正常特性;
- 基线电压不代表损坏:干净空气下 0.3V 左右属于正常,重点看接触烟雾后数值是否明显上升;
- IO 耐压保护:5V 供电模块务必加分压电路,禁止 AO 直连 3.3V ADC 引脚。
更多推荐


所有评论(0)