STM32F103实战:MQ2烟雾传感器ADC采集与报警系统设计

发布时间:2026/9/9 15:33:57
STM32F103实战:MQ2烟雾传感器ADC采集与报警系统设计 简介这份基于MQ2烟雾传感器与STM32F103微控制器的嵌入式开发例程适合正在学习STM32、传感器采集或串口通信的开发者也可用于电子竞赛或课程设计的快速起步与参考。压缩包共179个文件大小约4.3MB除.c/.h源码和Keil工程配置外还包含编译中间文件、链接映射文件以及hex烧录文件既能读驱动逻辑也能直接烧录到开发板验证。截至目前该资源已有16152人学习使用。项目展示了如何通过STM32的ADC读取MQ2在烟雾环境中的电压变化并转换为气体浓度信息再利用串口将数据发送至调试助手实时显示。认真梳理这些代码可以掌握MQ2传感器的工作原理与驱动写法、STM32F103的GPIO、ADC和UART初始化流程还能熟悉Keil工程的构建与常见外设调试方法是一份兼顾原理讲解与实际操作的入门参考。 MQ2烟雾传感器配STM32F103算是入门嵌入式传感项目里比较热门的一个组合。核心玩法不复杂传感器把烟雾浓度折算成电平或模拟电压MCU用ADC读一下超过阈值就触发报警再配合串口输出实时数据就能做出一个完整的烟雾检测原型。这个项目能锻炼到的基本功挺齐全——ADC采集、信号滤波、外设驱动、状态机处理做厨房烟雾预警、实验室环境监测、防盗报警辅助判断都够用。适合刚把STM32F103最小系统跑起来、会点灯但还没碰过ADC的初学者也适合手里有现成烟雾检测需求、想快速验证方案的开发者。先说清楚这套方案到底能干什么。MQ2模块会输出两路信号一路是模拟输出AO电压随烟雾浓度变化适合接到STM32的ADC引脚做量化分析另一路是数字输出DO板载电位器设定阈值后直接输出高低电平。实际开发中AO口才是主力DO口可以用来做简单验证或者冗余触发。下面我就从硬件接线、软件设计、校准逻辑、避坑经验这几个角度把整个项目完整拆开讲。1. 项目概述与核心思路MQ2传感器的核心部件是一个二氧化锡半导体气敏电阻。没有烟雾或可燃气体时它在空气中的电导率很低一旦接触到目标气体表面发生化学反应电导率迅速升高等效电阻跟着下降。模块内部把气敏电阻和负载电阻串联做成一个分压电路气体浓度越高气敏电阻越小输出电压就越高。这个浓度-电压关系并不是线性的而且受温湿度影响很大。不同批次的MQ2模块在同样环境下的输出电压都可能差不少。所以严格来说不要指望用MQ2测出准确的烟雾浓度数值它更适合做定性判断——检测有没有烟雾、浓度有没有超过阈值。理解了这一点后面的软件设计思路就清晰了我们不追求精确的浓度值而是做可靠的超标判定。STM32F103在整个系统里负责三件事一是通过ADC定时读取传感器模拟电压二是用软件滤波和阈值判断处理采样数据三是控制LED、蜂鸣器报警并通过串口输出状态信息。整个流程就像一个不断循环的哨兵读数、判断、报警、汇报。硬件上推荐用STM32F103C8T6最小系统板Flash 64KB内存20KB跑这个项目绰绰有余而且蓝色开发板遍地都是几块钱就能买到。晶振、复位电路、启动配置这些最小系统该有的东西板子都已经帮你做好了直接拿来用就行。2. 硬件准备与传感器原理2.1 MQ2模块引脚说明MQ2传感器模块常用的引脚有四个VCC、GND、DO、AO个别模块还带A、B两个检测极标识。接线看起来很简单但有几个地方处理不好就会踩大坑。引脚功能接法说明VCC电源正极接5V给传感器加热丝和检测电路供电GND电源负极与STM32共地DO数字输出可选接MCU的GPIO输入AO模拟输出经电阻分压后接STM32的ADC输入引脚第一个坑是供电电压。常见MQ2模块的VCC要接5V因为内部有加热丝需要靠电流加热气敏元件到工作温度。如果像我最早那样直接接3.3V传感器虽然也在工作但加热不足灵敏度会明显下降反应速度也变慢测出来的数据没有参考价值。第二个坑是模拟电压超限。AO口输出电压范围接近VCC也就是最高可能到5V而STM32F103的ADC参考电压是3.3V直接接上去轻则读数封顶没意义重则损坏引脚。所以AO和MCU之间必须加一级分压电阻把电压压到3.3V以内。如果手头正好没有分压电阻也可以买输出3.3V的MQ2模块但大多数常见模块默认还是5V选型时先看清楚。第三个坑是共地。传感器供电是5VMCU供电是3.3V两个电源的地必须连在一起否则ADC读出来的数据会乱跳甚至根本没有输出。很多新手排查了半天代码最后发现是地线没接。2.2 STM32F103引脚分配建议引脚分配我建议用下面这套方案兼容常见的蓝色最小系统板也方便后续扩展功能引脚说明ADC采集PA1ADC1通道1经分压后接入报警LEDPB0推挽输出低电平点亮蜂鸣器PB1推挽输出高电平鸣叫串口调试PA9/PA10USART1 TX/RX为什么选PA1不选PA0只能说PA0完全能用关键是避开几组特殊引脚PA13/PA14/PA15是SWD下载口PB3/PB4是JTAG复用口用这些引脚做ADC输入可能干扰下载或者引脚电平不受控。新手经常踩的坑就是把传感器输出接到PA13上结果下载器连不上芯片还以为是芯片烧了。2.3 ADC采样原理简述STM32F103内部有多个12位逐次逼近型ADC采样结果范围是0到4095。参考电压3.3V所以理论上每个数字单位对应的电压是3.3V除以4096约0.8mV。当AO电压经过分压后变化时ADC读数也会线性变化。这里提一点硬件设计经验分压电阻取值不用太讲究两个10k电阻串联就能用关键是分压比例要让全量程范围落在ADC的安全读数区间内。比如AO最高5V分压一半后最大2.5V对应ADC读数大约3102留出了3.3V以上的安全余量。如果想更充分利用ADC量程可以计算调整分压比但对烟雾检测这种定性项目没必要这么精细。3. 软件设计与ADC采集实现3.1 标准库还是HAL库STM32F103开发主要有两条路线标准外设库V3.5和HAL库。就这个项目来说我更倾向标准库理由很实际代码量少ADC配置逻辑直接摆在你面前能看清每一步寄存器操作背后的意义。HAL库功能全面但封装层数多一个ADC初始化函数拉出来几十行的结构体嵌套新手容易看晕出了问题也不好定位。我后面给的代码使用标准库V3.5风格配合Keil MDK编译。如果你已经习惯CubeMX加HAL的环境逻辑也是一样的把API换成HAL_ADC_Start和HAL_ADC_PollForConversion就差不多了。3.2 ADC初始化配置详解ADC初始化的关键步骤是开时钟、配置GPIO为模拟输入、配置ADC工作模式、校准、使能。我先把代码贴出来再逐步解释每一个配置的含义。void ADC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; // 1. 开启GPIOA和ADC1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); // 2. PA1配置为模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 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); // 4. 配置采样通道和采样时间 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); // 5. 使能ADC ADC_Cmd(ADC1, ENABLE); }这里的几个配置项解释一下。ADC_Mode选择独立模式因为我们只用一个ADC1不需要双ADC同步。ScanConvMode和ContinuousConvMode都关闭表示单通道、单次转换每次需要结果时启动一次转换。ExternalTrigConv设置为None表示不用定时器或外部引脚触发完全靠软件启动转换。DataAlign右对齐方便直接读12位数据。采样时间我选了55.5个周期。ADC时钟通常配置为12MHz换算下来大约4.6微秒完成一次采样对气敏电阻这种慢变信号足够了。如果测的是快速变化信号可以降压到13.5个周期但对MQ2没必要采样时间稍微长一点反而能减少内部电容充电不充分带来的误差。3.3 单次读取与软件滤波读取单次采样值用这段代码uint16_t MQ2_ReadADC(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); return ADC_GetConversionValue(ADC1); }软件启动转换等待转换结束标志位置位然后返回寄存器里的12位数据。这个流程本身没问题但直接用单次值判断阈值会非常不稳。原因在于传感器模块输出本身带有噪声加上电源纹波、电磁干扰ADC读数波动几十甚至上百都很正常。我曾经见过一个电源做得比较差的系统同一个气体环境下ADC值能从800跳到980这种数据直接判断阈值误报率会高到没法用。所以必须做软件滤波。最常用也最抗差的办法是去极值平均滤波连续采几次去掉最大值和最小值剩下的取平均。这样既能滤掉毛刺又不会像单纯取平均那样被极端值带偏。uint16_t MQ2_GetAverageValue(uint8_t times) { uint32_t sum 0; uint16_t buffer[10]; uint8_t i, minIndex 0, maxIndex 0; times (times 20) ? 20 : times; for (i 0; i times; i) { buffer[i] MQ2_ReadADC(); } for (i 1; i times; i) { if (buffer[i] buffer[minIndex]) minIndex i; if (buffer[i] buffer[maxIndex]) maxIndex i; } for (i 0; i times; i) { if (i minIndex || i maxIndex) continue; sum buffer[i]; } return (uint16_t)(sum / (times - 2)); }实测下来10次采样去极值平均对MQ2的效果很好波动能压到±20以内而且响应延迟仍然在秒级以下完全够用。3.4 串口输出与调试手段没有串口输出传感器调试基本等于摸着黑过河。USART1的初始化代码中规中矩关键是重定向printf在Keil里勾选Use MicroLIB然后重写fputcint fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }之后就能用printf打印实时的ADC值和报警状态了。每次读取间隔建议500ms到1sMQ2的响应时间本来就在秒级采样太快没有意义反而会让串口刷屏。4. 校准流程与阈值判定逻辑4.1 为什么不能写死阈值很多人拿到MQ2模块第一反应是设一个固定阈值比如ADC大于1000就报警。这种思路也不是完全不能用但误报和漏报的概率都比较高。关键在于每块传感器在洁净空气中的输出电压不一样有的模块输出0.8V有的可能1.5V换算成ADC值从几百到上千都有跨度。同一个固定阈值对这个板子太敏感对那个板子又太迟钝。所以正确的做法是让系统在启动时自动学习环境基线。程序先采集一段时间洁净空气中的ADC稳定值把这个值记为baseline再在baseline基础上计算报警阈值。这样每一块板子都能自适应自己的基线不用手动调参数。4.2 动态阈值加迟滞的设计阈值计算公式我建议这样设计当前读数为curValue标定基线为baseValue报警门限使用百分比偏移。alarmThreshold baseValue (4095 - baseValue) * ALARM_RATIOALARM_RATIO取0.1到0.3之间。这种用百分比算偏移的方式对不同基线的传感器适应性更好因为绝对偏移量会随着基线水平自动缩放。但光有阈值还不够还必须加迟滞控制。具体来说就是进入报警和解除报警使用两个不同的门限浓度高于alarmThreshold进入报警浓度低于releaseThreshold才解除报警其中releaseThreshold略低于alarmThreshold。不加迟滞会遇到什么情况当烟雾浓度正好在阈值附近波动时报警状态会反复翻转蜂鸣器跟着一通乱叫非常烦人。加入迟滞带之后报警状态一旦建立需要明显低于触发门限才能解除这样系统稳定得多。4.3 预热与基线自校准MQ2传感器依赖加热丝把气敏元件加热到工作温度上电初期元件温度还不稳定输出基线会明显漂移。我实测过冷态开机后的前五分钟ADC读数从800慢慢爬到1100是很正常的现象完全稳定可能要更久。所以如果开机瞬间就读基线做校准系统会在几分钟内把正常环境误判成烟雾超标。工程上建议这样处理开机后先进入预热状态持续5秒到5分钟不等期间不参与报警判断只做基线学习。调试阶段可以把预热时间设短一点比如10秒方便观察实际部署时设成5分钟以上。预热完成后记录baseline、alarmThreshold、releaseThreshold三个全局变量然后进入正常巡检。#define PREHEAT_MS 5000 #define ALARM_RATIO 0.2f #define HYSTERESIS_RATIO 0.08f uint16_t baseline 0; uint16_t alarmThreshold 0; uint16_t releaseThreshold 0; void MQ2_CalibrateBaseline(void) { uint32_t sum 0; uint8_t i; for (i 0; i 10; i) { sum MQ2_GetAverageValue(10); delay_ms(200); } baseline sum / 10; alarmThreshold baseline (uint16_t)((4095 - baseline) * ALARM_RATIO); releaseThreshold baseline (uint16_t)((4095 - baseline) * (ALARM_RATIO - HYSTERESIS_RATIO)); }这套设计在实际项目里表现很稳。ALARM_RATIO取0.2HYSTERESIS_RATIO取0.08对厨房油烟和真实烟雾测试都能覆盖。如果你发现误报多就把ALARM_RATIO调大发现漏报就调小。改系数不改代码结构非常灵活。5. 完整代码实现与烧录调试5.1 主循环逻辑把所有模块组在一起一个清晰的主循环长这样int main(void) { SystemInit(); delay_init(); LED_Init(); BEEP_Init(); USART1_Init(115200); ADC1_Init(); printf(MQ2 Smoke Detector Starting...\r\n); MQ2_CalibrateBaseline(); printf(Baseline: %d, Alarm: %d, Release: %d\r\n, baseline, alarmThreshold, releaseThreshold); while (1) { uint16_t adcValue MQ2_GetAverageValue(10); uint8_t smokeFlag 0; // 迟滞判断进入报警和解除报警使用不同门限 if (!alarmFlag adcValue alarmThreshold) { alarmFlag 1; } else if (alarmFlag adcValue releaseThreshold) { alarmFlag 0; } if (alarmFlag) { LED_On(); beepToggle !beepToggle; if (beepToggle) BEEP_On(); else BEEP_Off(); } else { LED_Off(); BEEP_Off(); } printf(ADC:%4d Smoke:%d Alarm:%d\r\n, adcValue, smokeFlag, alarmFlag); delay_ms(500); } }smokeFlag这个变量在代码里暂时只作为状态展示实际可以把它和报警状态一起通过串口上报方便上位机判断。蜂鸣器鸣叫方式我喜欢用方波驱动每500ms翻转一次声音有节奏感比一直长响更容易引起注意。5.2 编译下载与复现注意事项编译环境用Keil MDK加ST-Link。工程配置里三个容易出问题的地方芯片型号要选STM32F103C8不要选成ZE系列否则Flash地址对不上C/C选项里勾选Use MicroLIB否则printf重定向可能不生效下载器接线是SWDIO到PA13SWCLK到PA14GND共地3V3接3.3V。烧录完成后按一下复位键串口应该能看到启动信息和预热提示。如果串口完全没输出按这个顺序排查串口TX是否接错、波特率是否一致、GND是否共地。这三步查完90%的串口问题都能解决。5.3 常见硬件问题排查表这里把我实际调试中遇到的现象整理成一张速查表方便大家对照排查现象可能原因排查方法ADC一直都是4095AO分压没接或分压电阻虚焊万用表测PA1对地电压应小于3.3VADC一直在0附近模块没供电或AO接错引脚量模块VCC和AO的实际电压数据跳变很大电源纹波大、模块离干扰源近在模块电源脚加10uF电容信号线尽量短报警后无法解除没有迟滞带或环境烟雾残留检查releaseThreshold通风测试串口乱码波特率不匹配或时钟不对确认串口工具和代码都是115200一上电就一直报警预热时间不够基线被错误标定延长预热时间等传感器稳定再校准6. 常见问题与排查技巧实录6.1 DO口和AO口到底用哪个很多新手在DO和AO之间纠结。DO口接一个GPIO就能实现报警代码很简单但灵活性很差。DO的阈值被板上电位器固定了具体对应多大烟雾浓度全靠拧电位器猜程序里没法动态调整。AO口则把阈值的控制权完全交给程序灵敏度、迟滞、校准都能在线修改。所以正经做一个能用的烟雾检测器AO口是必选DO口顶多用来做快速验证。6.2 一个ADC偏低又反跳的典型案例我记得有一次调试PA1接AO后串口显示ADC值偏低只有几百把传感器靠近烟雾读数反而往下掉。当时查代码查了很久滤波、校准、打印都看了完全没问题。后来用万用表量PA1电压发现分压电阻接错了位置采样点接到分压末端而不是分压中点导致电压方向变了。这类问题在代码里是什么都看不出来的只有先把电压量对程序才有意义。6.3 定时器输出占空比到不了100的问题如果你后续想用PWM驱动蜂鸣器或者风扇很可能遇到占空比设到最大还是不满载的问题。原因一般是定时器的ARR重载值配置不对或者占空比函数里CCR值最大只能等于ARR而库函数有自动减一的逻辑。解决方法是把ARR设大一点比如999CCR直接写999这样就是100%占空比。另外PWM模式和输出极性一定要对应否则波形高低电平完全反相听觉和视觉效果都会很奇怪。6.4 蜂鸣器不响的硬件细节有源蜂鸣器和无源蜂鸣器的驱动方式完全不同。有源蜂鸣器内部带振荡电路只要有直流电平就会响但电流比较大不能直接由GPIO驱动必须加三极管。我用的是S8050基极串1k电阻接PB1集电极接蜂鸣器负极蜂鸣器正极接5V。无源蜂鸣器就不一样了必须用PWM波形驱动只给高低电平只能听到嗒嗒声不会产生连续声音。很多人拿着无源蜂鸣器直接给GPIO电平然后怀疑程序写错了其实换个驱动方式立刻就能响。6.5 基线漂移导致的假报警最后提醒一个很容易误导人的现象。刚上电读取的基线是800十分钟后慢慢爬到1100如果不做预热就直接标定传感器会被误判成烟雾浓度持续升高然后系统进入假报警状态。我自己就犯过这个错误当时盯着串口看了一个多小时怎么都想不通为什么干净环境里报警会触发。后来对比传感器手册才反应过来这是加热稳定过程的正常漂移。所以把预热时间当作项目的一部分来设计而不是可有可无的细节。7. 写在最后这个项目做完之后我最大的体会是三句话。第一传感器项目能不能稳定工作八成在硬件两成在软件出问题别一上来就怀疑代码。第二阈值判断一定不要写死基线校准和迟滞处理是灵魂少了这两个功能系统就算当时能跑也维持不了多久。第三慢慢来先把LED点亮再读出一个ADC值再通过串口看到数据一个功能一个功能往上加不要指望一次性写成完整工程然后直接跑通。最后再分享一个调试小技巧如果你手头有稳压电源可以在传感器旁边放一个打火机按住放气阀但不要点火这样能产生稳定的测试气体。测试时先用打火机气体靠近传感器验证报警再喷一点普通烟雾验证灵敏度对比两种场景下的ADC波形你就能非常直观地理解传感器响应特性了。本文还有配套的精品资源点击获取

相关新闻