☰
STM32F103驱动MQ2烟雾传感器:ADC采样、滤波与标定实战指南
2026/10/15 16:07:51 网站建设 项目流程

简介:基于MQ2烟雾传感器的STM32F103程序是一套完整的嵌入式实验工程,面向物联网、嵌入式初学及开发人员,解决可燃气体浓度采集与串口实时显示的需求。工程基于ARM Cortex-M3内核的STM32F103,包含MQ2驱动、ADC采样、电压转换及UART串口发送等核心逻辑,并附带Keil工程配置文件。压缩包共179个文件,以C源文件、头文件、目标文件为主,同时包含hex烧写文件、axf调试文件及uvprojx工程文件,整体仅4.3MB,便于快速移植与二次开发。这份工程已有16000余人学习下载,适合通过实际代码理解传感器工作原理、串口通信机制及STM32外设配置。借助这套程序,开发者可掌握MQ2传感器的数据读取与浓度换算方法,并可参照工程结构将烟雾检测功能快速集成到自己的嵌入式项目中。

1. 用STM32F103驱动MQ2烟雾传感器:一块开发板和一颗气敏电阻能做的事

很多人第一次把MQ2烟雾传感器接到STM32F103上,第一反应是“读个ADC值”。但真跑起来才发现,上电头几分钟数值一路漂,串口每秒吐一个数,每个数都不一样;放在阳台通风处是800多,拿到室内抽口烟忽然飙到3600,再放回去两小时也降不回原来的值。如果你也被这颗气敏电阻折腾过,那这正是一篇针对MQ2烟雾传感器配合STM32F103程序的落地笔记:从硬件连接、ADC采样、滤波,到标定与阈值设置,再到我踩过的几个典型坑,全部按可复现的方式写出来。适合手里有开发板、想做一个烟雾报警或空气质量监测装置,又不想只停留在“能亮灯”阶段的从业者。

MQ2的本质是一颗二氧化锡半导体气敏电阻,它对丙烷、甲烷、烟雾、酒精都有响应,价格便宜、响应快,但精度和稳定性都不如电化学传感器。STM32F103自带12位ADC,资源完全够用,麻烦的是传感器本身的非线性和漂移。所以程序的核心并不是“读ADC”这一行代码,而是围绕采样时机、滤波、基准标定、报警阈值展开的一整套策略。下面从硬件原理开始,一步一步把它说透。

2. MQ2的工作原理与硬件连接:分压电路决定ADC读数的精度上限

2.1 从气敏电阻到电压信号:Rs、RL与分压公式

MQ2内部的敏感材料是二氧化锡(SnO2),它被加热丝加热到大约200°C左右。在洁净空气中,材料表面吸附氧分子,形成电子耗尽层,电阻Rs处于一个较高的稳定值;当还原性气体(比如烟雾中的可燃成分、甲烷、丙烷、酒精蒸气)接触到表面时,会与吸附氧发生反应,释放电子,Rs显著下降。MQ2的输出就是“Rs随气体浓度变化”这件事。

但单片机只能读电压,所以需要一个分压网络把电阻变化变成电压变化。常见的做法是把敏感电阻Rs与一个负载电阻RL串联,在RL两端取电压:

Vout = VCC × RL / (Rs + RL)

当烟雾浓度升高,Rs下降,RL分到的电压上升,ADC读数变大。这个方向关系务必记清楚,后面排查反向问题时会用到。RL的取值对输出电压范围和灵敏度影响很大。市面上常见的MQ2模块板载RL大多是1kΩ或4.7kΩ(也有5kΩ),如果你是自己搭电路,我一般建议RL取值在4.7kΩ到10kΩ之间:太小,洁净空气下的分压太低,浓度变化时电压变化不明显;太大,浓度高时输出很容易触顶饱和。

这里还有个新手常忽视的点:ADC读到的电压并不是Rs本身,而是RL上的分压。要算Rs,需要做一次反推:

Rs = (VCC × RL / Vout) - RL

这个公式在后面标定章节非常关键。因为你标定的不是“电压值”,而是传感器的基准电阻Ro。

2.2 引脚识别与最小硬件连接:别把加热丝和测量极搞混

MQ2是6个引脚,内部实际上分为两组:一组是加热丝(引脚H/H-H),一组是测量电极(引脚A/B)。焊接和接线时必须分清。很多初学翻车都是把测量电极接到了5V和GND上,导致ADC读数一直不变或者干脆烧掉敏感层。

按照我常用的接法,把6个引脚分成三对:

要点 | 接法 加热丝H-H | 接5V正极,另一脚接GND,给内部加热丝供电 测量电极A | 接VCC(5V),作为分压上端 测量电极B | 串联RL后接GND,B与GND之间取分压输出(即OUT信号)

用模块时通常省心,板上已经做好了分压和比较器输出(有的模块同时提供模拟输出和数字输出,模拟输出接STM32的ADC引脚)。但用模块也要确认一件事:模拟输出引脚的最大电压。有些模块的模拟输出在5V供电时最高能到4V多,直接进STM32F103的3.3V ADC引脚是有风险的。保守做法是加两个电阻做1:1分压(比如10k和10k),把最高电压压到2V左右;或者确认模块设计上已经把输出限制在3.3V以内。我自己的习惯是:数采模块用3.3V给传感器供电也行,但加热丝可能加热功率不足,灵敏度明显下降,所以更推荐5V供电加分压的方案。

供电去耦也值得单独提一句。加热丝是一个典型的电流型负载,工作时电流变化会造成电源轨波动,而这个波动会直接串到ADC参考电压上,表现为读数周期性抖动。我一般会在传感器的VCC和GND之间放一颗100uF电解电容,再并联一颗0.1uF陶瓷电容,位置尽量靠近传感器引脚。这算是最便宜的“后悔药”了。

连接关系整理成最小清单:

  • STM32F103的PA1引脚配置为模拟输入,接模块模拟输出OUT(或分压后的信号)。
  • 模块VCC接5V,GND共地。
  • 如果传感器是裸元件而非模块,按上文配RL并引出分压中点。
  • ADC参考电压默认接VDD=3.3V,如果你的板子有外部基准芯片,标准电压源更佳,但要保证外部基准电压不超过3.3V且与MCU共地。

我把这部分作为一个固定的先导检查项,每次上电前先量一下加热丝两端有没有5V、OUT对地电压是否在合理范围,再去烧录程序。直接上电开读,十有八九会读到一堆莫名其妙的数。

3. 核心程序实现:ADC采样、滑动滤波与浓度换算

3.1 初始化ADC:12位分辨率、采样时间与DMA配置

STM32F103的ADC是12位精度,转换结果范围0~4095(对应0V~3.3V)。如果使用DMA循环模式,可以让ADC连续采样并把结果持续搬运到内存数组里,CPU不需要等待每次转换。这里给出一个完整的初始化函数,代码基于标准外设库,这是目前F103项目里最常见的写法。

void ADC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; // 1. 使能时钟:ADC1挂载在APB2总线,GPIOA也在APB2,DMA1在AHB RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 2. PA1配置为模拟输入,注意不是复用推挽,是模拟输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. DMA配置:循环模式,每次搬运一个半字(16位)到ADC_Value数组 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_Value; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = ADC_SAMPLE_COUNT; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 4. ADC1配置:独立模式,连续转换,12位分辨率,软件触发 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 只转换一个通道 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换,配合DMA循环 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); // 5. 配置ADC1通道1(PA1对应ADC_Channel_1),采样时间取最长239.5周期 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); // 6. 使能ADC与DMA,并校准(F103每次上电建议执行一次校准) ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); // 7. 启动一次软件触发,之后ADC连续转换、DMA持续搬运 ADC_SoftwareStartConvCmd(ADC1, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }

逻辑说明:整个初始化分四块。第一块是时钟,ADC1挂在APB2上,DMA1挂在AHB上,遗漏任何一路都会导致外设不工作。第二块是GPIO,PA1必须配置为模拟输入模式,如果用复用推挽会破坏模拟信号通路。第三块是DMA循环模式,它让ADC转换结果不断写入ADC_Value数组,缓冲区满后从头覆盖,这就是循环的意义,你不需要在中断里手动读取结果。第四块是ADC本身的配置,这里最关键的是采样时间,MQ2的等效源阻抗不低,采样时间不足会导致读数偏低且不稳定,239.5周期是F103可配置的最长采样时间,实测信噪比最好。

参数说明:ADC_SAMPLE_COUNT由你自己定义,我一般取16。这个值影响DMA缓冲区大小和后续滤波窗口长度,太小滤波效果差,太大占用内存且响应变慢。16对我来说是一个兼顾两者的平衡点。采样时间如果用的默认1.5周期,你会观察到ADC数值后面几位一直在跳,那不是传感器的问题,是采样电路没采稳。

3.2 滑动平均滤波:为什么不能直接拿单次ADC值做判断

MQ2的瞬态输出噪声相当明显,尤其在燃气、油烟这类气流扰动的场景里,单次ADC读数跳变几十个单位是很常见的事。如果你拿着单次值去做阈值判断,高点可能报警,低点又不报警,表现为报警指示灯闪个不停。所以程序里必须加滤波。滑动平均是最实用、最不挑场景的方案。

#define ADC_SAMPLE_COUNT 16 uint16_t ADC_Value[ADC_SAMPLE_COUNT]; // 简单的滑动平均:取最近ADC_SAMPLE_COUNT次转换结果的均值 uint16_t ADC_GetAverageValue(void) { uint32_t sum = 0; uint16_t i; for (i = 0; i < ADC_SAMPLE_COUNT; i++) { sum += ADC_Value[i]; } return (uint16_t)(sum / ADC_SAMPLE_COUNT); }

逻辑说明:ADC_Value数组由DMA持续刷新,主循环里直接调用ADC_GetAverageValue()就能拿到最近16次采样的平均值。之所以用多个样本平均而不是在中端里读一个值,是因为DMA在硬件层面持续采样,软件读到的是稳定的缓冲区快照,不需要考虑采样时刻是否错过。

参数说明:窗口大小从8到32都有人用。窗口越大,滤波越平滑,但对浓度突变的响应越慢。烟雾报警场景我选16,实测从烟雾出现到数值反映出来大约延迟300~500ms,可接受。如果你做的是需要快速响应的场景,比如酒精检测,窗口减到8更合适。这里有个血泪教训:不要用每次转换都更新一次平均值再乘除运算的方式做递推均值,效果一样但代码可读性和可维护性差很多,DMA+整块平均是最朴实也最好维护的方案。

3.3 从电压到浓度的两种做法:查表插值与对数拟合

拿到稳定的ADC电压值之后,下一个问题是“这个值到底是多少PPM”。MQ2的数据手册会给出一组灵敏度特性曲线,横轴是气体浓度(PPM),纵轴是Rs/Ro,坐标都是对数刻度。不同气体的曲线斜率不同,说明同一颗传感器对甲烷的响应和对烟雾的响应是不一样的。

如果你想在程序里输出一个近似浓度值,常见做法有两种。第一种是查表插值:把曲线上的关键点存成数组,根据当前的Rs/Ro值在相邻两个点之间做线性插值。第二种是对数拟合:手册曲线在双对数坐标下近似直线,于是有:

log10(PPM) = A + B × log10(Rs/Ro)

其中A和B是某个气体的拟合系数。这里的问题是,不同气体的A、B差异明显,而你手上没有标准气源去精确标定,所以算出来的PPM只能作为参考。我个人的做法是:程序里不输出PPM,而是输出“归一化比率”Rs/Ro,再用相对变化去判断报警。原因是Ro是传感器在洁净空气下的电阻值,和个体、环境都有关系;只报绝对PPM,换一颗传感器就完全不准了,这在项目落地时是灾难。

如果确实要展示浓度,我提供一个简化到可用的近似转换代码,它假设你在洁净空气下标定过Ro:

float MQ2_GetRs(float adc_voltage, float rl_resistance) { float rs; // 防止除零:adc_voltage最小时相当于RL上没有压降,此时视作最大量程 if (adc_voltage < 0.001f) { adc_voltage = 0.001f; } // 分压公式反推:Rs = (VCC * RL / Vout) - RL rs = (MQ2_VCC * rl_resistance / adc_voltage) - rl_resistance; return rs; } // 通过Rs/Ro在双对数坐标下的线性关系估算PPM(以烟雾/丙烷为例,系数可调) float MQ2_GetPPM(float rs, float ro) { float ratio = rs / ro; float ppm_log; // 参数来自典型灵敏度曲线的近似,A、B对应丙烷/烟雾 ppm_log = (log10f(ratio) - MQ2_INTERCEPT_A) / MQ2_SLOPE_B; return powf(10.0f, ppm_log); }

逻辑说明:MQ2_GetRs做的是分压公式的反推。你是从ADC电压推Rs,所以第一步必须先确认VCC、RL和ADC参考电压三者之间的关系。MQ2_GetPPM则是把Rs/Ro通过线性公式从对数坐标换回PPM,其中MQ2_INTERCEPT_A和MQ2_SLOPE_B是两个可调浮点参数,你可以根据自己手上的传感器实测数据去修改,没有任何魔法。

参数说明:这个转换最大的不稳定因素不是代码,而是Ro。Ro必须每颗传感器单独标定,程序里千万别写死同一个出厂值。模块上那个蓝色电位器调的也不是Ro,它调的是比较器触发电平或模拟前端的增益,和程序里的Ro是两回事。

4. 标定与阈值设定:MQ2最大的坑不在代码,在传感器本身

4.1 预热与老化:为什么上电第一小时读数一路漂

MQ2的敏感材料需要加热到工作温度之后,表面吸脱附过程才会达到平衡。这个加热过程不是三秒钟就能完成的,实测数据是:上电第一分钟读数会快速下降(从几百到几十),接下来半小时内还会缓慢漂移,一小时之后才基本稳定。这个漂移不是故障,是敏感层温度分布和表面氧吸附尚未稳定的物理过程。

所以在代码里做任何基准设定之前,必须先解决“什么时候开始采样”的问题。我看过有人把基准值设定在开机后第2秒的读数,结果可想而知,后面整个阈值体系全部失效。正确的做法是:程序里加一个初始化阶段,开机后至少等待5分钟再开始更新环境基准。如果你需要更快进入工作状态,可以在代码里跳过基准学习,直接使用上一次保存在Flash里的Ro值,但要保证传感器已经预热足够时间。

另外还有一个“老化”概念:全新的MQ2敏感层表面有一层工艺残留物,首次通电后Rs会持续变化,通常要老化24到48小时才能进入稳定工作区间。所以新买的传感器不要急着标定,先连续通电一天再说。这是MQ2程序之外、但直接影响程序效果的一个步骤。

4.2 标定方法:用干净空气实测Ro,而不是看手册

标定Ro的标准做法是在洁净空气中测量Rs的稳定值。具体操作是:传感器上电预热至少30分钟,选择室外通风良好的地方,或者室内确认没有燃气、香烟、酒精等干扰源,然后用程序连续采集Rs值,等读数在一个稳定范围内波动时,取平均值作为Ro。

这里记录一个实操代码片段。我一般会用一个串口命令触发标定,这样不用重新烧录固件:

void MQ2_Calibrate(uint16_t sample_count) { uint32_t sum_rs = 0; uint32_t voltage_sum = 0; uint16_t i; float avg_vout, rs, ro; // 连续采集N次,计算出平均输出电压 for (i = 0; i < sample_count; i++) { voltage_sum += ADC_GetAverageValue(); // 每次取滑动平均值 delay_ms(50); // 采样间隔,避免ADC多路切换干扰 } avg_vout = (float)voltage_sum / sample_count / 4095.0f * 3.3f; // 反推Rs,并暂时作为Ro保存 rs = (MQ2_VCC * MQ2_RL / avg_vout) - MQ2_RL; ro = rs; // 写入内部Flash模拟的存储区,保证掉电不丢失 MQ2_SaveRo(ro); }

逻辑说明:标定函数做了三件事。第一,多次采样取平均,消除瞬时波动的影响,这里的sample_count我会取20到50之间,太大耗时多,太小不稳定;第二,把ADC原始值换算成实际电压,然后反推Rs;第三,把结果保存到非易失存储区。注意avg_vout计算中用到的3.3f是ADC参考电压,如果你的板子参考电压不是标准3.3V,需要实测修正。

参数说明:delay_ms(50)是为了让两次采样之间有一点间隔,虽然DMA模式下连续读取很简单,但加入间隔能让电路在每次触发后有充分的稳定时间。特别是当你在多路ADC通道之间切换时,这个间隔更加重要。

4.3 阈值怎么设:固定电压阈值与相对阈值各自的适用场景

阈值设定有两种思路,各有各的使用场景,强烈不建议混用。

第一种是固定阈值法:标定后记录洁净空气下的基准ADC值,然后设定一个固定偏移量,比如“基准值+300”作为报警线。这种方法的优点是简单直观、逻辑稳定;缺点是受环境温湿度影响明显,夏天的基准值和冬天可能相差几十甚至上百个ADC单位。

第二种是相对阈值法:程序动态维护一个慢速更新的环境基准,当实时值相对于基准的偏离比例(比如升高30%)达到一定值时触发报警。这种方法的优点是对环境和个体差异的适应性强;缺点是必须保证基准更新足够慢,否则浓度真正升高时基准也被带偏,程序永远不报警。

我的建议是按场景选择:固定安装的烟雾报警器,用固定阈值更符合直觉,因为报警动作需要有可重复性的客观标准;而便携设备、车载环境这种空气本底变化大的场景,用相对阈值更合适。还有一个折中方案:开机后完成一次标定并把基准存进Flash,程序定期(比如每小时)检查当前读数与基准的差值,超过一个大的范围才更新基准,这样兼顾两者。

阈值设定的一个关键参数是回滞(hysteresis)。报警动作在基准值+300附近触发,但解除报警不要在基准值+290就解除,否则读数在阈值附近抖动时,报警状态会反复翻转。我会设置报警触发为基准值+300,解除为基准值+220,这80个ADC单位的回滞区间能有效屏蔽抖动。

5. 避坑与排查:MQ2程序的常见问题清单

5.1 上电后ADC读数直接接近满量程

现象:程序烧录后串口输出4090附近的值,无论放在什么环境下都降不下来。传感器明明是新的,模块焊线也没问题。

原因:最常见的是OUT引脚在传感器供电为5V时输出超过了STM32 ADC的3.3V量程,ADC读满后不会再变。另一个可能是在裸传感器方案里,你没有把负载电阻RL接上,OUT直接浮空,电压被拉到了电源附近。

解决:先用万用表量OUT对地电压,确认是否超过3.3V。如果超过,加一个分压电阻网络或改用3.3V给测量极供电(注意加热丝仍用5V)。如果确认电压在ADC量程内,检查RL是否焊接、虚焊引脚是否接触不良。

5.2 读数在洁净空气中持续走高,几小时不回落

现象:传感器放在没有烟雾的房间里,开了一晚上,串口读数从早上800慢慢涨到1400,再到1800,像爬坡一样。

原因:这基本是加热丝供电时断时续、或者供电电压偏低导致敏感层温度不足的典型表现。温度不够时表面氧吸附变慢,Rs漂移加大。还有一种常见情况是焊接时使用的松香或助焊剂残留被加热后挥发吸附到了敏感层表面,导致读数失真。

解决:把MQ2单独放在通风处,先确认加热丝两端电压稳定在4.8V以上,然后连续通电24小时观察是否回落。如果还在爬坡,用无水酒精轻轻擦拭传感器外壳(不要接触敏感层),再通电老化。程序端不要反复重启,重启会打断加热平衡,让漂移时间重新开始。

5.3 读数周期性波动,波形和电源纹波同步

现象:串口打印的数值不是单调跳变,而是呈正弦波状上下起伏,周期大约几十毫秒到几百毫秒。

原因:加热丝是电流负载,工作电流变化会引起VCC波动。如果MQ2和STM32共用同一个LDO,VCC的波动会传导到ADC参考电压,使转换结果跟着电源一起抖。这类问题在电池供电或劣质USB供电时尤其严重。

解决:给传感器供电单独加一颗100uF电解电容和0.1uF陶瓷电容,布线时电容尽量靠近传感器VCC引脚。更进一步的做法是给MCU的VDDA引脚单独接一颗磁珠或小电阻,把数字电源噪声和模拟参考电压隔离开。如果手头没有磁珠,先并大电容也能明显改善。

5.4 换了传感器后,原有报警阈值全部失灵

现象:同一套程序板子,之前报警阈值设在1200,换了一颗新的MQ2模块后,放在同样的环境里读数变成2400,直接开始报警。

原因:MQ2个体差异极大。即使是同一批次,Rs在相同浓度下也可能相差一倍以上。把阈值写死在固件里,等于把个体差异全都暴露出来。

解决:每一次更换传感器都要重新标定。我建议程序里内置一个简单的串口标定指令,输入一个特定字符后进入标定模式,完成后更新Flash里的Ro,一切阈值都基于Ro重新计算。同时,把报警阈值也改成存储变量,不要用常量。这个习惯能省下大量现场调试的时间。

5.5 低温环境下读数偏低,加热丝温度起不来

现象:在北方冬天,户外或阳台场景下,MQ2读数一直偏低。把传感器拿到室内几分钟,读数就恢复正常。

原因:加热丝电阻随温度变化,寒冷环境导致加热丝起始电阻变小,同样的供电电压下发热功率降低,敏感层工作温度不足,灵敏度明显下降。MQ2本质上是一个温度依赖型器件,它并没有补偿机制。

解决:程序端做软件补偿很难,因为影响的是敏感层温度而不是信号链上的某个固定增益。更实际的做法是在结构上避免让传感器直接暴露在强气流和低温环境中,比如增加一个百叶窗式的防护罩,减缓热量的流失。如果必须户外使用,建议在固件里加一个温度传感器,低于5°C时直接提示环境不适,而不是给出不可信的浓度值。

6. 进阶:多级报警、低功耗策略与标定数据持久化

MQ2本身加热功耗在800mW级别,加热丝一直通电,所以“待机低功耗”和“周期性唤醒采样”这类思路在MQ2上基本不成立——你可以在唤醒后花几秒钟采样并立刻让MCU进入睡眠,但加热丝仍然在耗电。因此真正值得做的进阶方向有三个:多级报警逻辑、标定数据的妥善存储、以及把相对值基准做得更聪明。

多级报警的核心是改变蜂鸣器和LED的状态机。我的实现是分三档:浓度高于基准30%为“关注”,LED慢闪,不响蜂鸣器;高于70%为“警告”,LED快闪,蜂鸣器每两秒短鸣一次;高于150%为“报警”,LED常亮,蜂鸣器持续鸣叫。状态机之间的切换要加入回滞,否则在临界点附近会来回跳动。

标定数据持久化方面,STM32F103没有真正的EEPROM,但可以使用内部Flash的最后一个页来保存Ro和报警阈值。写入前需要先擦除整个页,然后写入固定格式的结构体,读取出错时回退到默认值。这里有个坑:Flash写操作会阻塞CPU数十毫秒,标定写入时务必确保电源稳定,避免写一半掉电导致数据损坏。

最后谈一下“动态基准”的做法。我会让代码维护两个基准:一个是长期基准,通过每小时采样一次并保存到Flash;另一个是短期基准,每30秒更新一次。比较实时值时先看短期基准偏离,但报警解除时参考长期基准。这样既避免了环境本底缓慢变化导致的误报,又不会因为短期基准更新太快而漏报。

这个方向的坑我已经踩得七七八八了,最深的体会是:MQ2程序的成败不在ADC读取那一行,而在有没有耐心做预热、标定和阈值回滞。哪怕你只是做个课设或毕设,也建议把这套流程完整写进代码里,它会让你的设备从“能读数”变成“可真用”。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询