前言

最近做了一套面向单片机课堂教学的 MQ-2 烟雾检测工程,硬件采用 STM32F103C8T6 最小系统 + MQ-2 气敏传感器 + I2C OLED 屏幕,完整实现气体浓度采集、电压换算、20 次均值防抖滤波、阈值烟雾报警,所有原理、接线、代码逻辑、调试坑点全部整理完毕,新手也能直接复刻。

一、整体功能流程

整套工程信号流转逻辑非常清晰: 烟雾 / 可燃气体浓度改变 → MQ-2 内部敏感电阻变化 → AO 模拟输出电压变化 → STM32 PA0 ADC 采集模拟量 → 20 次均值滤波消除抖动 → 换算电压、相对电平 → OLED 实时打印数据 + 安全 / 报警状态 一句话概括:MQ-2 把气体信号转为电压,STM32 将电压转为数字量,最终 OLED 可视化展示。

功能演示以及视频讲解:STM32F103C8T6+MQ-2烟雾传感器+OLED完整教学工程|ADC 采样、均值滤波、气体报警实现https://www.bilibili.com/video/BV1cgNh6iEwb/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb

二、硬件接线对照表

完整引脚连接

模块引脚 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 数字引脚 + 可调电位器:

  1. DO:内置比较器,仅高低电平,只能判断是否超标,无法观察气体变化过程;
  2. AO:连续模拟电压,可实时读取浓度变化曲线,适合教学演示,因此本工程全程使用 AO 采集。

三、MQ-2 传感器核心工作原理

MQ-2 核心敏感材料为二氧化锡 SnO₂,内部集成加热丝,必须加热才能正常检测:

  1. 洁净空气:SnO₂导电性能差,传感器内阻 Rs 很大;
  2. 接触烟雾、液化气、甲烷、酒精、氢气等还原性气体:敏感层导电性提升,Rs 阻值下降;
  3. 模块配套测量电路设计为:Rs 减小 → AO 输出电压升高;
  4. 程序通过 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 模拟输出自带高频波动,单次采样屏幕数字疯狂跳动,因此采用多次采样取平均算法:

  1. 循环采集 20 次 ADC 值,单次间隔 2ms;
  2. 全部采集完成后求和取平均值;
  3. 使用均值计算电压与电平百分比,屏幕显示稳定无抖动。

工程文件与对应函数分工

文件 核心函数 功能
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

参数释义:

  1. ADC:12 位原始采样值,0~4095;
  2. Volt:PA0 引脚实际采样电压;
  3. Level:相对变化百分比,仅作趋势参考,≠真实 ppm 浓度;
  4. 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

现场调试阈值步骤

  1. MQ-2 上电预热数分钟,等待读数稳定;
  2. 记录干净空气下的 ADC 基线数值;
  3. 靠近烟雾观察 ADC 上升区间;
  4. 将阈值设置在基线与烟雾响应值中间,按需微调。

八、MQ-2 使用关键注意事项(踩坑总结)

  1. 预热必不可少:内部加热丝,上电数值不会立刻稳定;官方标准工况需预热 24h,课堂演示预热 3~5 分钟即可;
  2. 环境干扰大:温湿度、酒精、油烟、香水都会拉高 AO 电压,属于传感器正常特性;
  3. 基线电压不代表损坏:干净空气下 0.3V 左右属于正常,重点看接触烟雾后数值是否明显上升;
  4. IO 耐压保护:5V 供电模块务必加分压电路,禁止 AO 直连 3.3V ADC 引脚。
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐