ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于STM32的烟雾报警器:MQ-2+DS18B20+OLED完整工程实战

基于STM32的烟雾报警器:MQ-2+DS18B20+OLED完整工程实战

简介:这是一套面向STM32嵌入式学习与毕业设计/课程设计场景的智慧家居环境监控系统源码工程,集成MQ-2烟雾传感器、DS18B20温度传感器、OLED显示屏与蜂鸣器,实现烟雾浓度与环境温度的实时监测、超阈值本地报警,并可将数据发送至串口调试助手。资源共237个文件,压缩包仅6.32MB,包含C源文件、头文件、Keil工程配置(.uvprojx/.uvoptx)以及编译中间文件(.o/.d/.crf)等,工程目录规整,下载解压后可直接编译烧录。目前已有1432人学习,适合需要快速完成STM32相关课设或毕设的开发者借鉴。源码采用模块化设计,外设驱动、业务逻辑与报警控制分离,方便读者理解传感器采样、I2C/模拟量读取、OLED显示和串口通信的关键实现,同时支持修改阈值或添加其他传感器进行二次扩展,也可按需定制功能,是一份兼顾教学与实战的完整参考。

1. STM32+MQ-2+DS18B20+OLED:一个基础烟雾报警器的完整工程,值得你亲手搭一遍

做嵌入式最怕的就是东拼西凑看教程,每个模块都能跑,合在一起就翻车。这套基于STM32的烟雾报警器工程,把MQ-2烟雾传感器、DS18B20温度传感器、OLED屏幕和蜂鸣器整合成了一套完整的联动逻辑:烟雾浓度超标或温度越界时,蜂鸣器报警,屏幕显示当前数值和状态。对于刚入门STM32的开发者,这是一份能跑通全流程的参考代码;对于已经有项目经验的人,它把传感器选型、ADC采样、单总线时序、I2C显示和报警阈值整定这些常见环节串成了一条可复用的链路。我是把它当作一个「最小但五脏俱全」的嵌入式示例来拆的,下面把硬件连接、代码逻辑和实际踩过的坑都给你过一遍。

2. 硬件选型与电路连接:四个外设如何与STM32协作

2.1 MQ-2烟雾传感器:模拟输出与ADC采样

MQ-2是典型的半导体式气敏传感器,核心是二氧化锡(SnO2)气敏层。当环境中的可燃气体或烟雾浓度升高时,气敏层电导率变化,导致传感器输出电压变化。模块有数字量输出(DO)和模拟量输出(AO)两路:DO接比较器,通过电位器调阈值后直接输出高低电平;AO输出的是与浓度成正比的模拟电压,范围大约在0.1V到4V之间。我们要做可靠的报警判断,必须用AO接STM32的ADC引脚,不能偷懒只读DO。

接线时AO接PA1(ADC1的通道1),模块的VCC接3.3V,GND共地。注意MQ-2的加热电阻需要电流,纯靠STM32的3.3V引脚供电容易造成压降,常见做法是模块VCC接5V(如果模块支持5V供电,看模块背面的丝印),AO输出仍能安全接入STM32的ADC引脚。如果你用的板子是3.3V供电体系,务必确认模块用的是独立供电并共地,否则ADC值会随电源负载波动,数据飘得没法看。

另一个关键参数是预热时间。MQ-2上电后内部加热丝需要时间让气敏层达到稳定工作温度,我看到不少人在代码里上电立即读ADC然后做报警判断,结果前几分钟浓度值偏高,直接误报。一般要等1分钟以上再启用报警逻辑,工程里我用了一个简单的计数等待:主循环中统计上电后的运行时间,前60秒只显示数据不做阈值判断,实测效果比立即判断稳定得多,这个细节我在第5章展开。

2.2 DS18B20温度传感器:单总线协议的物理层与时序基础

DS18B20是Dallas半导体的经典单总线数字温度传感器,测温范围-55℃到+125℃,12位分辨率下精度为±0.5℃。它只需要一根数据线DQ,加上VCC和GND,共三个引脚。关键是这根数据线需要外部上拉,典型值是4.7kΩ电阻接到3.3V。很多人直接拿杜邦线把DQ接到STM32引脚就开跑,结果读回来的数据要么全0xFF,要么画面卡死。原因很简单:单总线协议是开漏输出,没有上拉电阻,总线根本拉不到高电平,时序完全乱掉。

接线建议:DQ接PB12,VCC接3.3V,GND共地,DQ和VCC之间接4.7kΩ上拉电阻。有些现成的DS18B20模块把上拉电阻做在板子上,可以直接接。时序上DS18B20比较苛刻:初始化时主机拉低480μs以上再释放,然后读存在脉冲;写时序的时隙是60~120μs,读时序由主机拉低1μs触发后释放总线,在15μs内采样电平。这些时序必须用阻塞延时或定时器精确控制,后面代码部分我会给一个实测可用的HAL库延时版本。

2.3 OLED屏幕的I2C接线和蜂鸣器的驱动方式

0.96寸OLED屏是嵌入式调试神器,I2C接口版本只需要接VCC、GND、SCL、SDA四根线。我用的这块是SSD1306驱动芯片,I2C地址默认是0x78(7位地址0x3C)。有人会遇到屏幕显示不出来或花屏,先查地址对不对,再查SDA和SCL有没有接反。I2C的SDA和SCL通常需要上拉电阻,但几乎所有的OLED模块出厂时已经预留了上拉,所以直连STM32的I2C引脚即可。这里注意:如果你用的是软件模拟I2C(普通GPIO翻转),上拉更容易满足;如果用硬件I2C,要确认引脚复用配置正确,尤其是有的时候PA9/PA10和I2C1的SCL/SDA复用冲突,被调试器或串口占用导致初始化失败。

蜂鸣器分有源和无源两种。有源蜂鸣器内部自带振荡源,给高电平或低电平(取决于驱动方式)就会发声,频率固定;无源蜂鸣器需要外部提供几千赫兹的方波才能发声。工程里用的是有源低电平触发蜂鸣器,GPIO输出低电平时发声。接法上,蜂鸣器模块通常带一个NPN三极管驱动,可以直接接STM32的GPIO。如果接的是裸蜂鸣器,必须串一个限流电阻(典型330Ω到1kΩ),否则灌电流过大,轻则声音嘶哑,重则烧GPIO口。我用的是PB8引脚控制蜂鸣器,输出逻辑是低电平触发,代码里用蜂鸣器ON和OFF宏来控制,这样移植到其他板子时只需要改引脚定义。

2.4 供电与共地:新手翻车最多的环节

这套系统看着简单,供电问题却能制造大量灵异现象。STM32开发板USB供电,同时给MQ-2、DS18B20、OLED和蜂鸣器供电,如果不统一共地,I2C数据会乱,ADC采集值会跳,DS18B20干脆读不到数据。核心原因是不同电源域之间的地电位差导致信号电平参考不一致。所以接线铁律是:所有模块的GND必须接到开发板的同一个GND引脚或GND网络。另外,MQ-2的加热丝启动瞬间电流较大(约150mA到180mA),蜂鸣器发声时也有几十毫安,如果供电能力不足,系统电压会被拉低,STM32可能直接复位或ADC值瞬时跳到最大值。我遇到过MQ-2一预热,OLED刷新变慢甚至整个系统重启的事,最后是换了一个输出电流足够的USB口,并在蜂鸣器电源端并联了一个100μF电解电容才稳定下来。如果是电池供电场景,建议用锂电池分压或降压模块单独给传感器供电,信号线共地,别让所有电流都从STM32的3.3V稳压器走。

3. 代码实现:HAL库工程从初始化到完整报警逻辑

3.1 GPIO、ADC、I2C和定时器的初始化

工程基于STM32CubeMX生成的HAL库代码,核心外设包括ADC1(通道1采集MQ-2)、I2C1(驱动OLED)、GPIO输入/输出和SysTick定时器。下面是初始化片段:

// mq2_adc_init.c - ADC1通道1采集MQ-2模拟输出 void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换,配合DMA或轮询读取 hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发,简单可控 hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; HAL_ADC_Init(&hadc1); sConfig.Channel = ADC_CHANNEL_1; // PA1对应ADC1_IN1 sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; // 采样时间约55.5周期 HAL_ADC_ConfigChannel(&hadc1, &sConfig); }

说明:这里采用连续转换模式加软件触发,每次读取时先启动一次转换,等转换完成后读数据寄存器。采样时间选55.5周期是折中方案:MQ-2输出阻抗不低,采样时间太短会导致ADC内部采样电容充电不充分,测量值偏低且不稳定;太长又没必要,55.5周期在72MHz主频下约0.77μs,完全够用。

I2C初始化在CubeMX里直接勾选I2C1,速率设为100kHz(标准模式)。原因:SSD1306的I2C从机最高支持到400kHz,但杜邦线连接时线间电容会拖慢信号沿,400kHz容易波形畸变导致丢字节,100kHz最稳妥。GPIO初始化部分,蜂鸣器引脚设为推挽输出,DS18B20的数据引脚设为开漏输出模式,因为单总线本身依赖外部上拉电阻。

3.2 MQ-2的ADC读取与滑动平均滤波

MQ-2的ADC裸值跳动幅度不小,尤其是刚上电和风扇吹过的时候。我推荐用滑动平均滤波,而不是简单的一次读取。下面的代码实现了一个16点滑动平均:

// mq2_filter.c - MQ-2 ADC值滑动平均滤波 #define FILTER_SIZE 16 static uint16_t filter_buf[FILTER_SIZE]; static uint8_t filter_index = 0; static uint32_t filter_sum = 0; uint16_t MQ2_ReadFiltered(void) { uint16_t raw = 0; HAL_ADC_Start(&hadc1); // 启动转换 HAL_ADC_PollForConversion(&hadc1, 10); // 等待转换完成,超时10ms raw = HAL_ADC_GetValue(&hadc1); // 读取12位ADC值(0~4095) filter_sum -= filter_buf[filter_index]; // 先把旧值从累加和中减掉 filter_buf[filter_index] = raw; // 写入新值 filter_sum += raw; // 累加新值 filter_index = (filter_index + 1) % FILTER_SIZE; // 环形索引 return filter_sum / FILTER_SIZE; // 返回平均值 }

逻辑说明:这个环形缓冲区每读一次就更新一个槽位,累加和始终是最近16次采样的总和,平均后输出。相比简单均值滤波,它的优点是不需要每次重新累加16个数,计算开销小,适合主循环高频调用。参数方面,FILTER_SIZE取16是实践出来的:太小(比如4)滤波效果不明显,ADC值仍然在几十个单位间跳动;太大(比如64)响应变慢,烟雾浓度突变时报警延迟明显。16在灵敏度和稳定性之间比较平衡。

MQ-2的原始ADC值还需要换算成电压再映射成浓度百分比。12位ADC参考电压是3.3V,所以电压值 = raw * 3.3 / 4095。工程里直接用ADC值做阈值判断,省去浮点运算,但显示在OLED上的时候会换算成电压值。注意:MQ-2不是精密仪器,它的浓度-电压曲线是非线性的,工程上的常见做法是只做相对判断——标定一个当前环境浓度下的基准值,然后跟基准值做差值判断,而不是死盯绝对电压。

3.3 DS18B20的时序驱动与温度解析

DS18B20是这套系统里最容易出问题的外设,核心原因是时序敏感。下面的驱动代码基于HAL库的阻塞延时实现,主频72MHz:

// ds18b20_driver.c - 单总线时序驱动核心 #define DQ_PORT GPIOB #define DQ_PIN GPIO_PIN_12 // 微秒级阻塞延时(依赖DWT计数器,比HAL_Delay更精确) static void Delay_us(uint32_t us) { DWT->CYCCNT = 0; while(DWT->CYCCNT < us * 72); // 72MHz主频下1us约72个周期 } // 复位总线,返回1表示存在设备 uint8_t DS18B20_Reset(void) { uint8_t presence = 0; HAL_GPIO_WritePin(DQ_PORT, DQ_PIN, GPIO_PIN_RESET); // 拉低 Delay_us(480); // 至少480us复位脉冲 HAL_GPIO_WritePin(DQ_PORT, DQ_PIN, GPIO_PIN_SET); // 释放总线 Delay_us(70); // 等待设备响应 presence = HAL_GPIO_ReadPin(DQ_PORT, DQ_PIN); // 读取存在脉冲 Delay_us(410); // 复位序列总时间 return (presence == GPIO_PIN_RESET) ? 1 : 0; // 低电平表示设备存在 }

这里要注意DWT延时计数器的启用方法:在main函数里需要先使能DWT和CYCCNT寄存器,否则读出来是0,延时直接变成0微秒,DS18B20必死。具体代码是CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;这一对语句放在初始化开头。

写完时序基元后,读取温度的流程是:复位→发跳过ROM命令0xCC→发温度转换命令0x44→等待750ms(12位分辨率下最大转换时间)→复位→发跳过ROM命令→发读暂存器命令0xBE→连续读2个字节→拼接成16位温度数据。温度解析的关键是符号位:如果高字节的最高位是1,说明是负数,需要取反加1再乘以0.0625(12位分辨率下的LSB值)。工程中默认显示正温度,负温度只做了符号判断,没有做温度补偿。

3.4 OLED显示与蜂鸣器报警逻辑

OLED显示部分用了一个精简的SSD1306驱动库,核心API是OLED_ShowString(x, y, str, font_size)和OLED_ShowNum(x, y, num, len, font_size)。显示逻辑在主循环中定期刷新(我习惯500ms刷一次):

// main_loop.c - 主循环中的显示与报警判断 char line1[20], line2[20], line3[20]; uint16_t mq2_adc; float temp; while (1) { mq2_adc = MQ2_ReadFiltered(); // 读取滤波后的烟雾ADC值 temp = DS18B20_ReadTemp(); // 读取温度值(单位:摄氏度) // 显示烟雾浓度(电压形式)和温度 sprintf(line1, "SMOKE:%.2fV", mq2_adc * 3.3f / 4095.0f); sprintf(line2, "TEMP:%.1fC", temp); OLED_ShowString(0, 0, line1, 16); OLED_ShowString(0, 3, line2, 16); // 报警判断:烟雾阈值或温度阈值,先确认是否过了预热期 if (run_time_sec < 60) { OLED_ShowString(0, 6, "WARMING UP...", 16); HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_SET); // 蜂鸣器关闭 } else if (mq2_adc > SMOKE_THRESHOLD || temp > TEMP_THRESHOLD) { OLED_ShowString(0, 6, " ALARM! ", 16); HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_RESET); // 低电平触发 } else { OLED_ShowString(0, 6, " NORMAL ", 16); HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_SET); } HAL_Delay(500); // 500ms刷新一次,兼顾显示刷新率和功耗 run_time_sec++; }

这段逻辑的核心在于报警判定不是单一条件,而是「烟雾超标或温度超标」的或逻辑,同时用run_time_sec变量做了一个60秒预热窗口。这里有个容易被忽视的参数:OLED显示用16号字体,一屏8行,第6行是状态栏。如果你的OLED库显示行数计算方式不同,行号要对齐,否则状态文字会显示在屏幕外或被截断。蜂鸣器的触发电平必须和模块匹配——有源蜂鸣器有的高电平触发,有的低电平触发,最好看模块丝印或规格书。我自己就吃过亏,把高电平触发的蜂鸣器接到低电平逻辑上,报警时反而安静,正常时一直响。

报警阈值SMOKE_THRESHOLD和TEMP_THRESHOLD在配置头文件中定义,具体整定方法在第5章讲。这套代码的核心是「显示与报警并行」:屏幕上永远能看到实时数据,同时状态栏给出明确的报警状态,不会出现蜂鸣器响了但屏幕不显示任何警示的情况。

4. 避坑指南:六个最常见的翻车现场

4.1 OLED白屏或显示乱码

现象:上电后OLED完全白屏,或者显示一堆随机像素点,重置程序也没用。

原因:最常见的是I2C地址不对。SSD1306的7位地址一般是0x3C,但部分模块的地址选择电阻焊接不同,地址可能是0x3D。另一个原因是SDA和SCL接反,初始化时HAL_I2C_Mem_Write每次调用都返回错误码,但代码里没检查返回值,所以屏幕无反应。

解决:先用I2C扫描程序读一下总线上有哪些地址,比如往每个地址发一个零字节,能收到ACK就说明从机存在。确认地址后在初始化代码里改OLED_ADDR宏。另外检查接线,SDA接PB7(I2C1_SDA),SCL接PB6(I2C1_SCL),别沿用旧工程的PA11/PA12。

4.2 DS18B20读出来是85℃或0℃

现象:温度显示固定显示85℃或者0℃,无论环境温度怎么变都不动。

原因:85℃是DS18B20上电复位后暂存器ROM中的默认值,说明芯片没有正常执行温度转换命令。0℃通常是数据线被拉死或时钟时序太快导致直接通信失败。我在调试时发现,如果复位脉冲的延时时间小于480μs,芯片根本不会进入待命状态,读出来的数据就是从暂存器里残留的默认值。

解决:用逻辑分析仪抓单总线波形,重点看复位脉冲宽度和读时隙采样点。如果没有逻辑分析仪,先用示波器看DQ引脚的电平,正常复位时应该有明显的低脉冲。然后把延时函数换成DWT精确延时,不要用HAL_Delay(它的最小分辨率是1ms,误差太大了)。DS18B20对时序相当敏感,这绝对不是玄学,是物理约束。

4.3 MQ-2上电就报警

现象:程序一烧进去,蜂鸣器马上响,屏幕显示ALARM,等了几分钟也不恢复。

原因:MQ-2在上电预热阶段会输出一个很高的初始电压。加热丝从室温升到工作温度,气敏层电导率剧烈变化,AO输出电压甚至能超过报警阈值。如果代码没有做预热延时,上电第一时间就会误报。

解决:按第3章的思路,在启动后加一个60秒的预热窗口,窗口内只显示不判断。另一个做法是把ADC读取值做开机自校准,记录开机后第10秒到第30秒的平均值,作为环境基准,报警阈值设为基准值加固定偏移量。这样即使在不同环境下开机,也能自动适应,前提是开机时环境里没有真实的烟雾源。

4.4 ADC数值跳动严重

现象:屏幕显示的烟雾电压在正常空气中持续在0.3V到0.8V之间大幅波动,风扇一吹变化更明显。

原因:三个因素叠加——MQ-2本身对气流敏感、ADC采样时间不足、电源纹波大。前面两个可以通过滤波和调整采样时间解决,第三个是硬件问题:USB供电时负载变化必然导致电压波动,蜂鸣器响的一瞬间ADC值立刻跳上去。

解决:软件上把滑动平均窗口从16提高到32,但注意响应会变慢;硬件上在蜂鸣器电源脚并联100μF电解电容,MQ-2的AO引脚和GND之间并联一个0.1μF陶瓷电容做低通滤波。别小看这个电容,它能把高频噪声压掉一个数量级。

4.5 蜂鸣器不响或长响不停

现象:报警条件触发后蜂鸣器没有声音,或者一旦触发就一直响,即使烟雾散尽也不停。

原因:不响大概率是触发电平反了,或GPIO没配置成推挽输出。长响不停是因为报警后没有做「释放恢复」判断——当ADC值降到阈值以下之后,报警状态没有被清除,蜂鸣器一直保持触发状态。如果你用的是有源蜂鸣器,低电平触发模块在GPIO恢复高电平后应该停止,但如果GPIO被误配置为复用功能,电平就拉不回来了。

解决:电平问题查模块手册,用万用表测模块控制脚的电压;逻辑问题在代码里加一个「报警恢复」分支,当连续10次采样都低于阈值时清除报警标志。另一个小技巧:用PWM控制蜂鸣器(如果是有源蜂鸣器不支持PWM,就用GPIO翻转),配合不同频率可以实现「滴滴滴」的间歇报警,而不是刺耳的长鸣。

4.6 代码烧录后运行不稳定,偶尔卡死

现象:程序偶尔跑飞,OLED刷新停滞,蜂鸣器乱响,按复位键后恢复正常,但几十分钟后又犯。

原因:这一类问题通常和看门狗、中断优先级或堆栈溢出有关。如果开了IWDG但喂狗不及时,系统会被硬件强制复位。中断优先级配置不当会导致I2C中断和SysTick中断互相抢占,出现偶发死锁。DS18B20读时序的临界区没有做中断保护,一个串口中断插进来就会打乱单总线的时序。

解决:临时关闭看门狗,看现象是否消失。把按键和外部中断的抢占优先级分开,习惯把SysTick设为最低优先级,I2C中断设为较高优先级。DS18B20的时序读取在关键节骨眼上加上__disable_irq()和__enable_irq(),让单总线的时隙在读取期间不受中断干扰。虽然暴力,但实测有效。

5. 参数整定:让报警判断更可靠

5.1 阈值标定的具体方法

烟雾报警器的阈值不是拍脑袋定的,需要针对现场环境做标定。具体操作分三步:首先在清洁空气环境中开机,等预热60秒后,记录ADC值稳定后的平均值,记为BASE_ADC;然后用打火机(不点火,只放气)靠近MQ-2约5秒,观察ADC值的变化,记录峰值,记为PEAK_ADC;最后取阈值SMOKE_THRESHOLD = BASE_ADC + (PEAK_ADC - BASE_ADC) * 0.6。为什么不是0.5或0.8?0.6是我在实际测试中得到的折中值——太低容易把打火机靠近时的瞬态峰值误判为火警,太高会让真实烟雾(蜡烛燃烧、纸片燃烧的初期烟雾浓度通常比打火机气体低)触发不了。

在代码里这组参数是这么用的:

// smoke_config.h - 阈值参数配置 #define BASE_ADC 1200 // 清洁环境ADC基准值(实测标定) #define SMOKE_THRESHOLD (BASE_ADC + 600) // 阈值 = 基准 + 固定偏移 #define TEMP_THRESHOLD 50.0f // 温度阈值,单位摄氏度

逻辑说明:把阈值写成「基准+偏移」的形式,看起来比裸写一个1800更合理,因为不同批次、不同供电电压下,MQ-2的基准值会漂移。换一块板子时只需要重新标定BASE_ADC,偏移量可以保持不变。温度阈值50℃适合厨房和机房场景,如果是卧室,可以降到45℃,如果是设备间,可以升到60℃。这个参数没有标准答案,一定要结合应用场景。

5.2 延时消抖与恢复判定

烟雾报警最怕的是瞬时干扰——比如有人从传感器旁边走过带起一阵风,ADC值瞬间冲高又回落,这时候不该触发报警。消抖的方法是连续N次采样都超阈值才判定报警:

// alarm_logic.c - 消抖判定与恢复判定 uint8_t over_threshold_count = 0; uint8_t alarm_active = 0; void Alarm_Update(uint16_t mq2_adc, float temp) { uint8_t over = (mq2_adc > SMOKE_THRESHOLD) || (temp > TEMP_THRESHOLD); if (over) { if (over_threshold_count < 5) over_threshold_count++; // 连续5次才触发 if (over_threshold_count >= 5) alarm_active = 1; // 锁定报警状态 } else { if (over_threshold_count > 0) over_threshold_count--; // 渐减,防抖 else alarm_active = 0; // 完全恢复才解除 } HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, alarm_active ? GPIO_PIN_RESET : GPIO_PIN_SET); }

这里的关键设计是「计数渐减」:一次瞬时干扰只会让count加1,下一次正常就减1,不会误报;而真实的烟雾浓度持续超标会让count迅速累加到5,触发报警后即使浓度短暂回落,count还需要5个周期才能减到0,避免了蜂鸣器一响一停的抽搐现象。这个设计参考了按键消抖的思路,但比按键消抖多了一层「报警锁定」状态。实际测试中,打火机放气触发报警后立即移开,蜂鸣器还会响2.5秒左右(每周期500ms×5次),这个保留时间我认为是合理的,能避免漏报。

5.3 实际场景下的灵敏度调整

不同使用场景对灵敏度的需求差异很大。书房或卧室,希望极早期预警,阈值可以设得低一些(偏移量取300左右),但代价是炒菜时的油烟也可能触发报警;厨房必须设高(偏移量取800以上),否则天天误报;车库要考虑汽车尾气,MQ-2对汽油蒸汽也敏感,阈值同样需要抬高。

我的做法是把灵敏度做成一个可以通过OLED菜单调节的参数,而不是编译时写死。具体方法是:用两个按键(KEY1加灵敏度,KEY2减灵敏度),在OLED上显示当前的阈值偏移量,长按进入设置模式。虽然这套工程只覆盖了基本版本,但代码结构上我把SMOKE_THRESHOLD定义成了全局变量而非宏,就是为了方便扩展这个功能。对于新手,我的建议是先用编译时写死的参数跑通,再考虑动态调整——一口吃不成胖子,先让系统可靠工作最重要。

另一个工程细节是温度补偿。MQ-2的响应特性会随温度漂移,气温从20℃升到35℃时,同浓度的烟雾输出可能差10%到15%。严格的做法是采样一个温度-ADC修正表,用DS18B20读到的温度做线性插值修正。工程里我用了一个简化版:阈值偏移量随温度变化微调,每升高10℃,偏移量增加20个ADC单位。这不是严谨的补偿曲线,但相比完全不补偿,误报率确实降低了。如果你的应用场景温度变化不大,这部分可以直接忽略。

6. 进阶:用串口把数据送出,做上位机可视化

这套系统目前的显示局限在0.96寸OLED上,数据量有限且没有历史记录。一个很实用的进阶方向是增加串口输出,把烟雾ADC值、温度、报警状态打包发送到上位机,用串口助手或Python脚本做实时绘图。串口格式我推荐用简洁好解析的文本帧,不要用二进制,调试期能省很多事:

// uart_report.c - 每秒输出一帧数据,方便上位机解析 uint8_t uart_buf[64]; snprintf((char *)uart_buf, sizeof(uart_buf), "SMOKE:%d,TEMP:%.1f,STATUS:%s\r\n", mq2_adc, temp, alarm_active ? "ALARM" : "NORMAL"); HAL_UART_Transmit(&huart1, uart_buf, strlen((char *)uart_buf), 100);

逻辑说明:这里用snprintf格式化字符串,输出字段用逗号分隔,上位机用逗号切分就能拿到三个关键量。串口波特率设为115200,每秒发一帧约30字节,占用带宽极低。上位机用Python的pyserial读取后,配合matplotlib可以画出烟雾浓度随时间变化的曲线。这样做的价值在于:你能直观看到MQ-2预热阶段的曲线形态,标定阈值时不再靠肉眼盯OLED,数据记录也能帮你回溯报警时周围环境发生了什么变化。

从那以后,我每次做传感器类的嵌入式项目,都强制自己第一步先加串口调试输出,把原始数据可视化,而不是急着调阈值、写报警逻辑。哪怕是做一个小demo,数据曲线也会暴露出你在OLED上完全察觉不到的规律。希望这一套从硬件接线到代码逻辑,再到参数整定和上位机验证的完整链路,能帮你在做烟雾报警器或类似传感器项目时少走几段弯路。

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

返回列表