做这个基于STM32的多路火灾报警系统,前前后后折腾了两周。从一开始拿洞洞板飞线,到后来PCB打样回来一次点亮,中间踩了不少坑,也攒了一堆实测数据。如果你正准备拿这个题目做课程设计、毕业设计,或者就是想给家里、实验室做一套多路报警装置,这篇文章应该能让你少走很多弯路。我会把整体方案、硬件选型、电路接线、软件逻辑、调试方法全部拆开讲,连“为什么这么设计”的原因也一并交代清楚,方便你照着做的时候心里有数。
1. 项目整体设计与方案选型
1.1 需求分析:为什么需要多路火灾报警
先说清楚这套系统要解决什么问题。传统的单点烟雾报警器,一个探测器管一个房间,一旦安装位置偏一点,等烟雾飘过去再报警,往往已经晚了。而且单点报警在大型空间里没法定位火源到底在哪一路,值班人员接到警情还得满世界找。多路火灾报警的思路,就是用一个主控板同时挂接多个传感器节点,每个节点独立采集烟雾、温度、火焰信号,任一通道触发报警后,主控端马上显示是哪一路、什么类型的信号超标,并联动声光提醒和继电器输出。
这套系统比较适合的场景包括:实验室多房间监测、厂房分区防火、学生宿舍楼层预警,甚至家用的话一个三居室也能覆盖。设计时要考虑的指标并不复杂,核心就是“反应速度”和“抗误报能力”。反应速度要求传感器采集周期不能太长,判定逻辑不能太拖沓;抗误报能力要求软件滤波和阈值判断配合好,不能用单个瞬时值就直接触发,否则炒个菜就乱叫。
1.2 主控选型:STM32F103C8T6还是F407?
主控我选了STM32F103C8T6,没有选F407系列。原因很简单:多路报警系统的计算量并不大,F103C8T6这颗芯片有72MHz主频、64KB Flash、20KB RAM,带3个USART、2个I2C、2个SPI、10个ADC通道、37个GPIO,对挂四路传感器加OLED屏加按键蜂鸣器来说绰绰有余。C8T6的封装是LQFP48,手工焊接难度也比较低,价格十几块钱一片,很适合学生党的预算。
有人可能会问,F407有FPU,带DSP指令,做报警系统不是更流畅吗?其实报警系统最耗时的部分只是ADC采样和简单滤波,F103的Cortex-M3都能轻松跑几百赫兹的采样率,没必要上F407。况且F407最小系统板体积稍大,功耗也高一些,对这套系统没有收益。如果后续你要扩展以太网、摄像头图像火焰识别这种重负载功能,再考虑换成F407或者H7系列不迟。
1.3 传感器选型:烟雾、温度、火焰传感器的组合策略
单一传感器很难可靠地判断火灾。我用的是“烟雾+温度+火焰”三合一判定策略。烟雾传感器选择MQ-2,它检测甲烷、丙烷、烟雾等气体的敏感度不错,价格便宜,经典应用广泛,输出有模拟量和数字量两路,方便接ADC。温度传感器选了DS18B20,单总线协议,一根线就能挂多个,精度0.5摄氏度以内,用于确认环境温度是否异常升高。火焰传感器选用模拟量输出的红外火焰探测器模块,它对波长760nm到1100nm的红外光敏感,能识别火焰中的红外辐射,模块上还带一个电位器可以调灵敏度。
选择这三个传感器的组合,是为了互相弥补缺点。MQ-2对酒精、水汽也有反应,单独用它容易误报;DS18B20只测温度,着火初期温度没起来时反应慢;火焰传感器对太阳光和灯光中的红外成分也可能触发,单独用也不靠谱。所以最终判定逻辑是:两个及以上传感器同时越限才报火灾,或者单一传感器越限超过设定时间也触发预警。这样既能保证灵敏度,又能避免日常干扰导致的误报警。
2. 硬件电路设计与接线细节
2.1 传感器接口电路设计:别让ADC噪声毁掉报警
ADC采样电路是这套系统中踩坑最多的地方。MQ-2的模拟输出本质上是一个分压电压信号,信号内阻比较高,直接接STM32的ADC引脚时,采样值会跳动得非常厉害。我第一次实测,把MQ-2直接连到PA0上,用默认配置采样,同一浓度下读数波动幅度达到正负200多(12位ADC满量程4096),这是完全不可接受的。
解决办法是加一个电压跟随器缓冲,或者至少加一个RC低通滤波。工程上更常用的是RC滤波,在MQ-2的AO引脚和STM32的ADC引脚之间串联一个1k电阻,对地并联一个10uF电容。这个组合能有效抑制高频干扰和采集瞬间的电荷注入效应。计算一下截止频率:f = 1 / (2π * R * C),R=1kΩ、C=10uF时,f约为15.9Hz,烟雾信号本身变化很慢,这个带宽完全够用,而50Hz以上的工频和开关噪声基本被滤掉了。实测加滤波后,ADC读数波动降到正负15以内,效果非常明显。
DS18B20的接线也有讲究。单总线设备需要外接一个4.7k上拉电阻到3.3V,否则数据线无法正常输出高电平。如果走线比较长,比如超过50cm,建议把上拉电阻降到2.2k,保证信号的上升沿足够陡。火焰传感器模块一般自带比较器和电位器,模拟输出直接接ADC即可,但注意它的电源最好用干净的3.3V,不要从电机驱动或者继电器的电源轨上取,否则继电器吸合的瞬间产生的电流冲击会导致火焰传感器读数毛刺。
2.2 多路采集的IO分配与复用
规划四路传感器时,我给每一路分配了独立的ADC引脚和独立的DS18B20数据引脚,不搞时分复用。为什么不用一个ADC引脚切换多路?因为AD的扫描切换虽然快,但每个传感器信号源阻抗不同,切换后需要等待电荷稳定,读到的值会混入上一路的残余电压。多路独立接入反而省心,采完一路再采下一路,完全不用处理串扰。
具体引脚分配如下表:
| 功能 | 引脚 | 说明 |
|---|---|---|
| 第一路MQ-2模拟量 | PA0 | ADC1_IN0 |
| 第二路MQ-2模拟量 | PA1 | ADC1_IN1 |
| 第三路MQ-2模拟量 | PA2 | ADC1_IN2 |
| 第四路MQ-2模拟量 | PA3 | ADC1_IN3 |
| 第一路火焰传感器 | PB0 | ADC1_IN8 |
| 第二路火焰传感器 | PB1 | ADC1_IN9 |
| DS18B20(四路共用一根?) | PB7 | 单总线,全部并联 |
| OLED I2C | PB8/PB9 | SCL/SDA |
| 蜂鸣器 | PB12 | 推挽输出,低电平触发 |
| 报警LED | PB13 | 推挽输出 |
| 继电器 | PB14 | 推挽输出,接ULN2003 |
这里要特别说明DS18B20的接线。虽然DS18B20支持单总线挂多个,但四路传感器如果挂在一根总线上,读取时需要通过64位ROM码区分设备,程序复杂度会上升。为了简化逻辑,实际项目中我用了两路DS18B20挂在一根线上,另外两路分别接到PB6和PB7,分成两组。反正每组只有两个设备,用默认的Skip ROM命令依次读取也是可行的,不过为了稳,我干脆每路一个引脚,省去地址匹配的麻烦。
GPIO分配有个原则:ADC引脚和PWM引脚尽量分开,避免高频PWM信号干扰模拟采样。我把蜂鸣器放在PB12,和ADC引脚隔开一大段距离,走线时也尽量远离模拟线。
2.3 声光报警与继电器输出电路
声光报警看着简单,其实有两个坑。第一个坑是蜂鸣器的驱动方式。直接用STM32的GPIO驱动有源蜂鸣器,虽然也能响,但电流可能超出GPIO的驱动能力极限,导致芯片发热或复位。我用的解决办法是加一个NPN三极管(S8050)驱动,GPIO输出高电平通过1k限流电阻接到三极管基极,蜂鸣器接在5V电源和集电极之间,发射极接地,这样GPIO只需要提供约3mA的基极电流,蜂鸣器的工作电流由三极管的放大能力提供,完全没问题。
第二个坑是继电器没有续流二极管。继电器线圈是一个感性负载,断电瞬间会产生一个很高的反向电动势。如果没有反向续流二极管,这个高压尖峰会顺着走线打回STM32的GPIO或者电源线,轻则导致系统复位,重则烧芯片。我在继电器线圈两端并联了一个1N4007二极管,注意二极管方向要反接,即阴极接继电器电源正极、阳极接线圈驱动端,让断电瞬间的电流通过二极管几乎短路释放掉。
四路报警指示我用了四颗不同颜色的LED,每路触发时对应LED点亮。实话说LED限流电阻的计算也要注意:STM32 GPIO高电平是3.3V,LED压降约2V,限流电阻取330Ω时电流约3.9mA,亮度已经足够了。不要用1k,亮度太暗,尤其是在有环境光的房间里看不清。
3. 软件架构与核心代码实现
3.1 多路数据采集与滤波算法
软件整体框架我用了简单的裸机状态机,没有上RTOS。四路传感器加上OLED显示和按键,裸机轮询完全够用。ADC采集使用定时器触发转换,避免主循环里阻塞等待。我配置TIM2每隔100ms触发一次ADC1,扫描规则通道列表,一次DMA传输完成四路MQ-2和两路火焰传感器的采样。DMA的好处是CPU不需要参与搬运,转换完数据自动落到内存数组里,主循环只要读取数组就行。
下面是我实际使用的ADC连续扫描加DMA配置核心代码(STM32标准库,常规写法):
void ADC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOB, &GPIO_InitStructure); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_values; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 6; 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); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = ENABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 6; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 5, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_9, 6, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }注意ADC采样时间我配置为239.5周期,这里的原理是:STM32的ADC输入阻抗要求信号源阻抗不能太高,普通RC滤波后的信号源阻抗大约在1kΩ这个量级,如果采样时间太短,内部采样电容充放电不完全,会导致读数偏低且非线性。239.5周期在72MHz时钟下大约是3.3us,配合RC滤波基本能准确采样。但如果你的传感器输出阻抗更高,建议把采样时间拉到最大,或者外加跟随器。
滤波算法我没有用复杂的卡尔曼,只用了一阶低通滤波。公式很简单:
filtered_value = alpha * raw_value + (1 - alpha) * filtered_value;alpha取0.2,也就是说新采集值只占五分之一权重,这样对一个速率很慢的烟雾信号来说,滤波后曲线非常平滑,同时延迟只有5倍的采样周期即500ms,对火灾报警来说完全够用。对比测试显示,不滤波时ADC波动正负150,滤波后波动正负20,且响应速度只慢了不到半秒,性价比极高。
DS18B20的读取我用的是常规的复位、跳过ROM、启动温度转换、延时750ms、读取暂存器的时序流程。这个协议时序要求比较严格,必须关闭中断或者将中断优先级调整到非常高,否则延时过程被打断就会产生忙时序,导致读取失败。我的做法是在读取函数开头进入临界区,整个字节读写期间不允许被打断。
3.2 阈值判断与火灾判定逻辑
阈值设置是整个系统准确性的核心。不同环境下传感器底噪差异很大,我在代码里定义了一个“标准阈值”和一个“报警保持时间”。对MQ-2来说,ADC读数范围是0到4095,清静环境中读数一般在300到500之间,我设定一级预警阈值为600,二级火警阈值为900。火焰传感器的模拟量正常环境中接近满量程,比如3500以上,有火焰时明显下降,所以我设定低于2500为火焰触发信号。DS18B20温度阈值设定为55摄氏度,一级预警50度,二级火警60度,可以通过按键调整范围。
真正的火灾判定逻辑采用投票和防抖机制。我的状态定义如下:
- 正常态:所有传感器读数均在阈值内。
- 预警态:任意一路通道有任一传感器超过一级阈值,但尚未达到火警条件。此时蜂鸣器慢速间歇鸣叫,OLED显示对应通道号和传感器类型。
- 火警态:满足以下任一条件进入火警:a)同一通道至少两种传感器超过一级阈值;b)任意单一传感器超过二级阈值;c)预警状态持续超过10秒仍未恢复。火警触发后蜂鸣器持续鸣叫,LED快闪,继电器吸合,OLED显示火警通道。
- 复位条件:只有按下“消音/复位”按键,且当前所有传感器读数全部回落到阈值以下,才会从火警态切换回正常态。
这个逻辑比单纯用或门触发可靠得多。我实际测试用打火机靠近火焰传感器,如果只用单一火焰触发,灯光下的红外干扰偶尔也会误报;改成投票机制后,日常开灯、手机闪光灯等干扰基本都能滤掉。
判定代码的核心片段:
typedef struct { uint16_t smoke_raw; uint16_t flame_raw; int16_t temp_x10; uint8_t smoke_alert; uint8_t flame_alert; uint8_t temp_alert; uint8_t alert_level; } Channel_Status; uint8_t Check_Alarm(Channel_Status *ch, uint8_t ch_id) { if (ch->temp_x10 > FIRE_TEMP_X10 || ch->smoke_raw > SMOKE_FIRE_THRESHOLD || ch->flame_raw < FLAME_FIRE_THRESHOLD) { return FIRE_ALARM; } uint8_t trigger_count = 0; if (ch->temp_x10 > WARN_TEMP_X10) trigger_count++; if (ch->smoke_raw > SMOKE_WARN_THRESHOLD) trigger_count++; if (ch->flame_raw < FLAME_WARN_THRESHOLD) trigger_count++; if (trigger_count >= 2) return FIRE_ALARM; if (trigger_count >= 1) return WARN_ALERT; return NO_ALARM; }这里有一处很值得注意:温度阈值用的“温度值乘以10”的整数,即t = temp_x10 / 10.0f,这样可以完全避免浮点数运算带来的RAM和Flash开销。F103虽然带FPU的是F4系列,Cortex-M3没有硬件浮点,用软件浮点库也行,但整数运算速度更快、结果更可控,尤其在做阈值比较时不会出现浮点精度误差。所以整个项目中我尽量用整数和位运算,只有OLED显示温度小数时临时转一下字符。
3.3 状态机设计与按键消抖
主循环逻辑我用了一个简单的状态机。四路通道状态轮流检测,每轮循环大约200ms,因为ADC数据是定时器后台收集的,所以主循环只读取和判断,不阻塞。按键用了短按和长按两种功能,短按切换显示页面,长按3秒执行消音复位。按键消抖用状态机加计数器,不是简单的delay延时,因为delay会阻塞整个巡检循环,导致传感器数据过期。
消抖的做法是:每10ms查询一次按键电平,如果连续20次(即200ms)都为稳定电平,就认为按键状态有效。状态机可以避免抖动期间的错误触发,代码也不难:
uint8_t Key_Scan(void) { static uint8_t key_state = KEY_IDLE; static uint8_t stable_count = 0; uint8_t current = GPIO_ReadInputDataBit(KEY_PORT, KEY_PIN); switch(key_state) { case KEY_IDLE: if (current == 0) { stable_count++; if (stable_count >= 20) key_state = KEY_PRESSED; } else stable_count = 0; break; case KEY_PRESSED: if (current == 0) key_state = KEY_LONG_TRIGGER; // 此处按需处理长按 else key_state = KEY_SHORT_TRIGGER; break; default: key_state = KEY_IDLE; break; } return key_state; }报警状态下主循环会不断刷新状态,确保继电器输出与蜂鸣器状态同步。继电器在火警态才吸合,预警态不动作。
3.4 OLED显示与串口打印调试
显示部分用0.96寸I2C接口OLED,SSD1306驱动,四线连接:VCC、GND、SCL、SDA。OLED显示内容分三层:正常态显示四路传感器实时值,用数字和简单图形表示;预警态显示“WARN CHx: SMOKE/TEMP/FLAME”;火警态显示“FIRE CHx!!”并闪烁。由于OLED刷新率不高,我设置每500ms刷新一次整个屏幕,每次刷新前清屏会导致轻微闪烁。后来我改成只刷新变化区域,闪烁明显改善。这也是一个实操经验:SSD1306支持页寻址模式,可以只更新变化的行列区域,没必要全屏重画。
串口打印调试是后续问题排查的重要工具。我在UART2上输出格式化日志,包括每个通道的ADC原始值、滤波值、温度值、状态标记、报警标志。调试时用USB转串口接到电脑,波特率115200,每秒输出一帧。这样在测试传感器响应时,不用看屏幕就能非常直观地看到曲线变化。量产或者最终交付时,可以把打印关闭或者降低频率,避免占用CPU。
串口重定向的代码需要注意,printf内部会占用较大的堆栈空间,在STM32F103上默认启动文件分配的堆栈是2KB,printf如果没设置合适的话可能溢出。我的做法是不用printf,直接使用自己写的格式化函数,将整数转为字符串再送串口。如果非要printf,记得在启动文件中把Stack_Size改大,至少4KB。
4. 实操过程:从零搭建与调试实录
4.1 硬件焊接与上电检查
我先是手工焊接了一套洞洞板原型,方便飞线改接。焊接顺序有讲究:先焊电源部分,再焊主控芯片和最小系统,然后焊传感器接口,最后焊显示和人机交互模块。别一上来就焊传感器,因为ST-Link烧录器还没接上,如果最小系统有问题需要排查时,传感器只会增加排查难度。
上电前的检查动作非常重要。首先用万用表二极管档测一下3.3V和GND之间的正反向压降,如果接近短路(压降很小,甚至直接响铃),说明电源有问题,绝对不能上电。我遇到过PCB打样回来,STM32芯片焊反了导致电源短路的情况,幸好提前测量救了一命。其次检查ST-Link的SWD四根线的通断:SWDIO、SWCLK、GND、3.3V,顺序别接反。很多新手把SWDIO和SWCLK接反,导致烧录器识别不到芯片。
给板子供电时,我习惯用稳压电源先设置3.3V,限制电流到100mA。如果上电瞬间电流超过300mA,几乎可以确定板上有短路或者焊接错误,需要立刻断电排查。正常STM32空跑程序时电流约30-50mA,加上OLED、MQ-2传感器和LED,总电流在150mA左右,最大值不会超过300mA。继电器吸合时会额外多60mA左右,这些参数心里有数,排查问题会方便很多。
4.2 单路传感器调试
焊好板子之后,第一步是烧录一个最简单的点灯程序,验证GPIO和晶振正常。然后逐个模块调试,不要一口气把所有功能都跑起来。我先把第一路MQ-2的ADC数据读出来,在串口打印原始值,放桌面上静止10分钟,记录底噪范围。这里有个容易被忽略的知识点:MQ-2这类半导体气体传感器刚开始上电时,内部加热丝会把敏感材料加热到工作温度,这段时间内输出并不稳定,有的传感器需要预热5分钟甚至更久。所以调试时一定要等待足够时间,否则标定出的阈值是错的。
传感器放稳后,我用打火机的丁烷气体轻轻靠近MQ-2(不点火,只放气),观察串口读数变化。正常情况下ADC值应在1秒内明显上升,3秒后达到峰值,移开后10秒内缓慢回落。如果读数变化迟缓,检查RC滤波是不是焊错了、电容是否接到相邻引脚;如果读数直接满量程4095,很可能MQ-2模块的电位器旋得太大,或者传感器坏了。
DS18B20的调试最简单。插上后串口打印温度,和室内的水银温度计对一下,偏差0.5度以内可接受。如果一直显示85度,这是DS18B20的典型开机复位错误信号,通常原因是上拉电阻没焊或者数据线接触不良;如果显示0度且数据在0和0.0625之间跳动,一般是时序太长导致转换未完成,需要调整延时或者关闭中断。火焰传感器的调试用打火机火焰在距探头20cm处晃动,看ADC值是否有明显下降;不要直接盯着阳光或者其他红外干扰源测试,不然你会以为模块坏了。
4.3 多路联调与阈值标定
单路都ok后,把四路数据同时跑起来。这时重点检查通道间串扰。我在第一路传感器旁点燃一支蜡烛,观察其他三路的读数变化。如果其他路读数也跟着小幅波动,大概率是ADC配置的采样时间太短,或者DMA传输的内存地址重叠了。把adc_values数组定义为6个uint16_t,确认每个通道的数据落在自己的下标里,别用越界赋值。另外检查RC滤波是否每个通道都独立,有人为了省事几个通道共用一个电容,结果一路波动全跟着动。
阈值标定是系统可靠性的决定性一步。四路分布在房间的不同位置,因为环境温度和空气流通不同,每路的底噪都不一样。我的做法是程序里预留一个“标定模式”:按住按键上电,系统进入10秒标定流程,自动采集当前环境各通道数据的平均底噪,然后把底噪加上一个固定偏移量作为预警阈值,偏移量在EEPROM里可以微调。比如MQ-2底噪是420,偏移量设为100,一级阈值就是520;温度底噪是28度,一级阈值设为50度。这样每套系统根据自己环境自动适配,不会出现装了A房间正常、搬到B房间就疯狂误报的问题。
当然,如果不想用自动标定,也可以直接在代码里改几个宏。但我要劝一句,固定阈值看着省事,后续现场调试会非常折磨人。因为传感器老化、灰尘积累都会导致底噪漂移,固定阈值用不了几个月就会出问题。自动标定加EEPROM保存参数,能大幅降低售后维护成本。
4.4 系统级测试场景模拟
全部联调完成后,做四组模拟测试。第一组是正常环境,四路传感器安静放置2小时,系统不能产生任何预警。第二组是点一支香烟放在第一路传感器下方约30cm处,验证系统在15秒内是否进入预警,30秒内是否进入火警。第三组是用打火机火焰靠近第四路火焰传感器,温度数值还未明显上升时,火焰信号触发预警,随后用热风枪加热DS18B20附近区域到60度,验证通道四温度触发火警。第四组是断电重启测试,检查EEPROM中的阈值是否保留,以及系统上电后能否自动恢复到正常巡检状态。
测试时我特意记录了从传感器信号变化到报警动作的延迟。我测得结果如下:MQ-2大约需要3到5秒响应时间,DS18B20转换周期最快也要750ms才能更新一次,火焰传感器瞬时响应在几十毫秒内。综合判定逻辑中,最坏情况下从信号出现到蜂鸣器拉响大约在7到8秒左右。这个速度对于一般火灾预警场景已经足够,因为火灾初期到形成明火通常有几十秒到几分钟的窗口期。
继电器输出的测试也很关键。我给继电器接了一个带12V水泵的小演示装置,模拟消防喷淋联动。注意继电器驱动电路使用的是ULN2003达林顿管阵列,这个芯片内部自带续流二极管,所以在继电器线圈两端不需要额外并联二极管。我之前在电路设计时看到继电器数据手册上讲需要吸收电路,但ULN2003本身已经把这件事做了,还傻乎乎地又焊了一个二极管上去,结果造成极性冲突,继电器无法正常吸合。拆掉外部二极管后一切正常。这个细节值得记录。
5. 常见问题与排查技巧速查表
5.1 传感器读数异常怎么办
我把实际调试中遇到的高频问题整理成一个速查表,按现象、可能原因、解决办法列出:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| ADC读数一直为4095或0 | GPIO未配置为模拟输入,或引脚复用冲突 | 检查GPIO_Mode_AIN配置,查看是否有其他外设占用了同一引脚 |
| 读数波动大,超过100 | RC滤波缺失或电容虚焊;采样时间太短 | 加RC滤波,采样时间配置为239.5周期;用万用表测引脚对地电容是否焊上 |
| 温度显示85度 | DS18B20上拉电阻异常或数据线接触不良 | 检查4.7k上拉电阻,重新插拔传感器,用示波器看数据线波形 |
| 火焰传感器一直满量程 | 电位器调节不当或环境红外光过强 | 用螺丝刀调电位器,避开阳光直射和强光源测试 |
| 某一路读数与其他路相差极大 | 该路传感器接线断线或传感器老化 | 用万用表测传感器供电电压,替换传感器验证 |
排查ADC问题时可以用一个土办法:把传感器拔掉,直接用跳线把ADC引脚接到3.3V,读值应该为4095附近;接到GND,读值应为0附近。如果读取数值和预期不符,说明ADC配置问题,与传感器无关。如果值和预期相符,再接回传感器,进一步判断是传感器模块的问题还是接口电路的问题。
5.2 误报和漏报的处理
误报大多出在MQ-2上。它会在厨房油烟、酒精喷雾、汽车尾气等场景下误触发。我处理误报的方法有三个层面:第一,增加投票机制,单路烟雾报警不能直接触发火警,必须温度或火焰协同确认;第二,对连续越限时间做限制,比如烟雾超过阈值低于5秒不触发预警;第三,调整传感器安装位置,远离门窗和排气口,防止气流直接吹到传感器上。
漏报则常出现在传感器失效或者阈值设置过高的情况。运营维护时建议每个月用打火机气体在每路传感器附近测试一次,验证报警响应是否正常。DS18B20损坏比较容易识别,因为读取数据会返回85度这个固定错误值,在代码里加一个传感器故障检测,如果连续10次读回85度,要输出“温度传感器故障”提示。
我还遇到过一个奇怪的漏报案例:四路系统正常,但某一时刻火焰传感器突然完全失效,无论有没有火焰,读数都固定为4095。排查后是火焰传感器模块的输出短路了,因为我在调灵敏度电位器时用力过猛,刮坏了PCB的铜箔。这种机械损伤只能靠仔细观察板子才能发现,所以调试时不要用力过猛。
5.3 程序跑飞和硬件复位问题
程序跑飞是嵌入式系统常见问题。我做这个项目时遇到过两次系统随机复位:一次是蜂鸣器连续响的时候复位,检查发现蜂鸣器驱动三极管的基极限流电阻被我用成了1Ω而不是1kΩ,导致GPIO电流过大,芯片触发过流保护复位;另一次是继电器吸合时复位,原因是继电器电源和STM32电源共用一个3.3V稳压器,继电器吸合的瞬间把电压拉低到2V以下,导致芯片掉电复位。
解决办法有两步:一是把继电器负载电源隔离出来,单独用5V电源或者12V电源,继电器线圈接在5V和ULN2003输出之间,不经过3.3V稳压器;二是给STM32的电源入口加一个大容量的储能电容,我用的是100uF电解电容并联0.1uF陶瓷电容,这样即使负载瞬态拉低,也有足够的能量维持MCU运行。
另外,开启独立看门狗对提高系统鲁棒性很有帮助。如果主循环因为某种原因卡死超过3秒,看门狗强迫复位,系统自动恢复,至少保证报警功能不会长期瘫痪。但注意看门狗时间不能太短,因为DS18B20的750ms转换期间如果被看门狗复位,转换会中断。我设置喂狗周期为1秒,在主循环末尾喂一次,确认所有阻塞操作不超过1秒即可。
最后再分享一个调试技巧:程序里设计一个简单的自检功能,上电时依次点亮所有LED、响一声蜂鸣器、检测四路传感器初始数据是否在合理范围内,如果异常则在OLED上显示错误代码。这个自检过程看起来简单,但在现场部署时特别有用,别人照着说明书按一下就能告诉我们哪里坏了,不用搬着设备跑到实验室来修。
做这套系统最大的体会是,硬件电路和软件逻辑必须咬合得很紧密。很多人觉得这种多路报警系统就是接个传感器然后比较一下阈值,真正做起来会发现从传感器响应曲线到判定逻辑,再到抗干扰处理,每一层都有细节。如果你也在做类似项目,建议先仔细调好一路,再复制到其他路,这样出问题时可以缩小排查范围。整个方案后续如果想升级,可以在主控上接入ESP8266模块,把报警信息通过MQTT推送到手机,也可以在OLED上增加更复杂的交互界面,甚至用FreeRTOS重写软件框架来支持更多通道。基础做扎实了,扩展也就是水到渠成的事。