1. 为什么我要把实验室消防预警系统做成开源项目实验室这个场景做消防预警跟普通办公室或者住宅完全不是一回事。普通场所你装个烟感报警器基本就交差了但实验室里有酒精灯、有电烙铁、有各种挥发性试剂还有大量长时间通电的测试设备。火灾隐患的特点是起火快、蔓延路径复杂、无人时段风险极高。我见过太多实验室的消防措施就是墙上挂两个灭火器真出了事根本来不及反应。这个项目最初是我给一个高校电子实验室做的定制方案前后迭代了三版硬件、重构了两次代码最后决定把整套东西开源出来。原因很简单市面上能买到的消防报警主机要么贵得离谱动辄大几千要么功能死板只能接自己的探头不能自定义逻辑而实验室场景恰恰需要灵活的逻辑配置和低成本的可复制性。用STM32来做这件事BOM成本能压到一百多块功能却可以按需裁剪。整套开源内容包含三块完整的Keil工程代码、立创EDA绘制的原理图、以及Proteus仿真文件。代码里我做了比较彻底的模块化分层传感器驱动、报警逻辑、通信协议各管各的你想换成别的传感器或者改报警阈值基本不用动主逻辑。原理图部分我特意标注了每个元件选型的理由仿真文件则方便那些手头没有实物硬件的朋友先跑通逻辑再打板。适合谁看如果你是嵌入式方向的学生想找一个能写进简历、能讲清楚技术细节的完整项目这个比那些温湿度显示的练手项目有说服力得多。如果你是在实验室做管理的老师或者工程师想低成本搭一套真正能用的预警系统代码和原理图可以直接拿去改。哪怕你只是想学STM32的多传感器融合和状态机设计这套代码的分层思路也值得参考。提醒一句消防类项目涉及人身安全开源代码仅供学习和原型验证使用。真要部署到实际实验室环境必须经过专业消防评估并配合合规的商用报警设备做冗余。2. 系统整体架构与核心器件选型逻辑2.1 三层架构感知层、决策层、执行层我把整个系统拆成三层这个划分不是拍脑袋定的而是为了让每一层都能独立测试和替换。感知层负责采集环境数据包括温度、烟雾浓度、火焰红外三个维度。为什么是这三个单一传感器误报率太高——比如只有温度传感器夏天实验室空调坏了温度升到40度它就报警但根本没着火。三个维度做与逻辑或者加权判断才能把误报压下去。决策层是STM32主控跑状态机和阈值判断逻辑。这里我用的是STM32F103C8T6也就是大家常说的最小系统板那颗芯片。选它的理由很实际72MHz主频足够跑多路ADC采样和状态机外设资源丰富3个USART、2个SPI、2个I2C关键是便宜且资料多淘宝上核心板十块钱出头学生党友好。执行层包括声光报警蜂鸣器LED、继电器输出可联动排风扇或切断电源、以及通信模块上报到上位机。执行层和决策层之间我做了光耦隔离因为继电器动作时会有反向电动势不隔离容易把MCU搞死机。2.2 传感器选型DHT11只是入门真正干活的是这几个很多人做环境监测第一反应就是DHT11这个项目里我也用了但只用来做环境温湿度的辅助参考不作为火灾判断的主依据。原因很直接DHT11响应速度慢采样周期至少1秒精度±2℃而且它测的是空气温湿度对局部火源的温度突变反应迟钝。真正承担火灾检测主力的是这三个传感器型号检测目标接口方式响应时间温度传感器DS18B20环境温度突变单总线750ms烟雾传感器MQ-2可燃气体/烟雾模拟量ADC10s火焰传感器红外火焰探头明火紫外/红外数字量GPIO1sDS18B20用单总线一根线就能挂多个探头我实际布了三个点实验台、试剂柜、电源区通过ROM地址区分。MQ-2输出模拟量接STM32的ADC1_IN0代码里做了滑动平均滤波。火焰探头输出数字量有火时拉低直接接外部中断响应最快。这里有个坑MQ-2需要预热。刚上电时它的输出漂移很大代码里我加了30秒预热期预热期间不参与报警判断否则一上电就误报。2.3 为什么用状态机而不是简单的if-else初学者最容易写成这样if(温度阈值 烟雾阈值) 报警;。这种写法在实验室场景下会出大问题——传感器抖动、瞬时干扰、探头老化都会导致误报或漏报。我用的是四状态有限状态机正常态、预警态、报警态、故障态。状态迁移不是看瞬时值而是看持续满足条件的时间。比如温度超过60℃且持续3秒才从正常态迁到预警态预警态下如果烟雾也超标才进报警态。这样能有效过滤掉电烙铁靠近探头这种瞬时干扰。状态机的另一个好处是可追溯。每个状态迁移我都记录了时间戳和触发源出问题的时候能回查是哪一路传感器在什么时刻触发的方便调试。3. 硬件原理图的关键设计细节3.1 电源部分别小看这一块烧板子多半是它的锅整个系统供电分两路5V给继电器和蜂鸣器3.3V给STM32和传感器。我用的是AMS1117-3.3做降压输入5V输出3.3V。这里有个细节很多人忽略AMS1117在压差大的时候发热严重5V降到3.3V压差1.7V如果电流到500mA功耗就是0.85WSOT-223封装会烫手。我的处理办法是在输入端并一个100μF电解电容加一个0.1μF陶瓷电容输出端同样配置。大电容储能应对继电器吸合瞬间的电流冲击小电容滤高频噪声。另外在PCB布局上AMS1117下面铺了一块铜皮散热实测满载温度能降15℃左右。原理图里我还加了一个自恢复保险丝PPTC串在5V输入总线上。实验室环境经常有人热插拔电源有了这个保险丝短路时它会自动断开冷却后恢复比一次性保险丝省心。3.2 传感器接口上拉电阻和滤波电容一个都不能少DS18B20的单总线必须接4.7kΩ上拉电阻这个大家都知道。但我要强调的是上拉电阻的位置——要尽量靠近MCU端而不是靠近传感器端。因为单总线通信时线缆有分布电容上拉电阻靠近MCU能让上升沿更陡通信更稳定。我第一版把电阻放在传感器那头结果线缆超过1米就通信失败挪到MCU端后3米线缆都没问题。MQ-2的模拟输出我加了一个RC低通滤波1kΩ电阻串在信号线上对地接0.1μF电容截止频率约1.6kHz。这个滤波能有效抑制电源纹波和空间干扰对ADC采样的影响。实测加滤波前后ADC读数的波动从±30降到±5。火焰探头的数字输出我加了施密特触发器74HC14做整形。因为火焰探头的输出信号边沿比较缓直接进MCU的外部中断可能触发多次。经过施密特触发器整形后边沿变得陡峭干净中断只触发一次。3.3 继电器驱动光耦隔离和续流二极管继电器我用的是5V驱动的SRD-05VDC线圈电流约70mASTM32的GPIO根本推不动必须加驱动。我用的是S8050三极管做开关基极串1kΩ电阻限流。关键来了继电器线圈两端必须反并一个续流二极管1N4148就行。继电器断电瞬间线圈电感会产生反向高压不加二极管的话这个高压会击穿三极管。我第一版忘了加上电测试时三极管直接冒烟换了个新的才反应过来。光耦我用的是PC817把MCU的GPIO和继电器驱动级隔离开。虽然STM32和继电器共用5V地理论上不隔离也能工作但实验室环境里继电器切换排风扇这种感性负载时地线上会有噪声隔离之后MCU复位的情况再没出现过。4. 代码分层设计与核心逻辑实现4.1 目录结构让每个模块都能单独测试我的Keil工程目录是这样组织的Project/ ├── Core/ │ ├── main.c │ ├── stm32f1xx_it.c │ └── system_stm32f1xx.c ├── Drivers/ │ ├── dht11.c/h │ ├── ds18b20.c/h │ ├── mq2.c/h │ └── flame.c/h ├── App/ │ ├── alarm_fsm.c/h │ ├── data_filter.c/h │ └── comm_protocol.c/h └── BSP/ ├── led.c/h ├── buzzer.c/h └── relay.c/h这个分层的核心思想是Drivers层只负责和硬件打交道App层只负责业务逻辑BSP层管板级外设。比如你要把MQ-2换成MQ-135只需要改Drivers/mq2.c里的ADC通道和换算公式App层的报警逻辑一行都不用动。4.2 数据滤波滑动平均加中值滤波的组合拳ADC采到的原始数据抖动很大直接用来判断会频繁误触发。我用了滑动平均中值滤波的组合。滑动平均窗口取10个点每采一个新值就丢掉最老的算平均。这能平滑随机噪声。但滑动平均对脉冲干扰无能为力——比如电机启动瞬间ADC突然跳到一个极大值平均之后还是会被拉高。所以我又加了一级中值滤波连续采5个值排序后取中间那个。中值滤波能有效剔除脉冲干扰。代码实现上我用了一个环形缓冲区存ADC值#define FILTER_WINDOW 10 static uint16_t adc_buf[FILTER_WINDOW]; static uint8_t buf_idx 0; uint16_t filter_adc(uint16_t new_val) { adc_buf[buf_idx] new_val; buf_idx (buf_idx 1) % FILTER_WINDOW; // 复制一份用于排序找中值 uint16_t temp[FILTER_WINDOW]; memcpy(temp, adc_buf, sizeof(adc_buf)); sort_ascending(temp, FILTER_WINDOW); uint16_t median temp[FILTER_WINDOW / 2]; // 滑动平均 uint32_t sum 0; for (int i 0; i FILTER_WINDOW; i) { sum adc_buf[i]; } return (uint16_t)(sum / FILTER_WINDOW); }实测这套滤波下来MQ-2的ADC读数波动从±40降到±3报警判断稳如老狗。4.3 状态机实现用查表法代替一堆if-else状态机的核心是一个二维数组行是当前状态列是触发事件数组值是下一个状态。这样写出来的代码清晰、好维护加新状态只需要改表。typedef enum { STATE_NORMAL 0, STATE_WARNING, STATE_ALARM, STATE_FAULT, STATE_MAX } SystemState; typedef enum { EVENT_NONE 0, EVENT_TEMP_HIGH, EVENT_SMOKE_HIGH, EVENT_FLAME_DETECTED, EVENT_SENSOR_FAULT, EVENT_CLEAR, EVENT_MAX } SystemEvent; // 状态迁移表 static const SystemState state_table[STATE_MAX][EVENT_MAX] { // NONE TEMP_HIGH SMOKE_HIGH FLAME FAULT CLEAR {STATE_NORMAL, STATE_WARNING, STATE_WARNING, STATE_ALARM, STATE_FAULT, STATE_NORMAL}, // 正常态 {STATE_WARNING, STATE_WARNING, STATE_ALARM, STATE_ALARM, STATE_FAULT, STATE_NORMAL}, // 预警态 {STATE_ALARM, STATE_ALARM, STATE_ALARM, STATE_ALARM, STATE_FAULT, STATE_NORMAL}, // 报警态 {STATE_FAULT, STATE_FAULT, STATE_FAULT, STATE_FAULT, STATE_FAULT, STATE_NORMAL}, // 故障态 };注意看正常态那一行只有火焰检测到才直接进报警态温度高和烟雾高都先进预警态。这是故意的——火焰是明火必须立即响应温度和烟雾可能是干扰先预警观察。预警态下如果烟雾也高了说明确实有火情才升级到报警。4.4 通信协议自定义帧格式比Modbus更轻量系统需要把报警信息上报到上位机我用的是自定义的串口帧格式比Modbus简单适合这种小数据量场景。帧格式0xAA 0x55 | 长度 | 命令字 | 数据 | 校验和校验和用的是累加和取低8位虽然不如CRC可靠但对于短帧来说够用了。命令字我定义了三种0x01上报状态、0x02查询状态、0x03设置阈值。上位机那边我用Python写了个简单的解析脚本pyserial读串口按帧头帧尾拆包解析后显示在控制台上。如果你要做成图形界面用PyQt或者Tkinter都行协议层不用改。5. 仿真验证没有硬件也能跑通全流程5.1 Proteus仿真文件的搭建要点Proteus里跑STM32仿真最关键的是加载正确的HEX文件和配置时钟频率。HEX文件在Keil的Output目录下Proteus里双击STM32芯片在Program File栏选中它。时钟频率要和你代码里SystemInit配置的一致我用的8MHz外部晶振Proteus里也设8MHz否则串口波特率会不对。仿真里我用电位器模拟MQ-2的模拟输出用按钮模拟火焰探头的数字输出用DS18B20的仿真模型模拟温度。这样不用买硬件就能测试状态机的迁移逻辑。我建议你先在仿真里把所有状态都跑一遍确认逻辑没问题再打板能省不少钱。5.2 仿真和实物的差异别被仿真骗了仿真跑通不代表实物就能用这里有几个差异必须注意第一仿真里没有噪声。Proteus里的ADC读数完美无瑕实物上电源纹波、空间干扰、传感器本身的噪声都会让读数抖动。所以滤波代码在仿真里看不出效果实物上才是刚需。第二仿真里没有时序问题。实物上DS18B20的时序对微秒级延时很敏感如果代码里用了中断中断服务程序执行时间过长会打断单总线时序导致通信失败。我在实物上就遇到过这个问题后来把DS18B20的读写操作放在临界区里关中断才解决。第三仿真里继电器不会打火。实物上继电器吸合瞬间的电磁干扰是实打实的光耦隔离和续流二极管在仿真里体现不出价值但实物上缺了它们系统就不稳定。我的建议是仿真用来验证逻辑实物用来验证电气特性。两者缺一不可但别指望仿真能替代实物测试。6. 调试过程中踩过的坑和排查思路6.1 串口乱码波特率对不上只是表象第一次上电串口助手收到的全是乱码。第一反应是波特率不对检查代码里设的是9600串口助手也是9600没错。用示波器量TX引脚发现实际波特率是4800左右。问题出在时钟配置上。STM32F103的串口波特率是从PCLK2分频来的我代码里SystemInit默认用的是8MHz外部晶振但实际板子上焊的是12MHz晶振买核心板时没注意看。晶振频率不对PCLK2就不对波特率自然偏了。排查思路先量晶振实际频率再核对时钟树配置。后来我把晶振换成8MHz或者改代码里的HSE_VALUE宏定义问题就解决了。这个坑告诉我买核心板一定要看清晶振频率别想当然。6.2 继电器误动作地线噪声在作怪系统跑起来后发现继电器偶尔会自己吸合一下没有任何报警触发。用示波器看MCU的GPIO发现继电器吸合瞬间GPIO上有一个几十毫伏的毛刺。根因是共地噪声。继电器和MCU共用5V地继电器线圈断电时产生的反向电动势通过地线耦合到了MCU的GPIO上虽然电压不高但足以让GPIO误判。解决方案有两个一是加光耦隔离我最终采用的二是给继电器单独供电地和MCU地只在一点连接。光耦方案成本低、效果好PC817几毛钱一个加上之后误动作再没出现过。6.3 DHT11读数据失败时序要求比想象中严格DHT11的时序要求很苛刻主机拉低至少18ms然后释放DHT11响应拉低80μs再拉高80μs之后才开始传数据。我一开始用HAL_Delay做延时结果读十次失败八次。原因是HAL_Delay的精度只有1ms而DHT11需要微秒级延时。后来我改用DWT计数器做微秒延时精度到纳秒级读取成功率直接到100%。void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) ticks); }用DWT之前记得在初始化里使能它DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk;。这个技巧在需要精确微秒延时的场合比如单总线、WS2812都通用。6.4 火焰探头误触发环境光干扰火焰探头对红外敏感但实验室里的白炽灯、太阳光里也有红外成分。我调试时发现下午阳光照进实验室火焰探头就频繁触发。解决办法是加调制。火焰探头本身输出的是脉冲信号频率在10-20Hz左右而环境光的红外是直流或者低频。我在代码里加了一个判断只有检测到连续多个脉冲才认为是真火焰单次触发忽略。具体实现是用定时器捕获脉冲频率频率在10-20Hz范围内才有效。这个改动之后阳光直射下也不再误报了。7. 开源资料的使用建议和二次开发方向7.1 代码移植到其他STM32型号这套代码用的是标准外设库不是HAL库移植到F4或者F0系列需要改几个地方启动文件、时钟配置、外设寄存器地址。如果你用的是HAL库那驱动层要重写但App层的状态机和滤波逻辑可以原样搬过去。我建议初学者先用F103把逻辑跑通再考虑换芯片。F103的资料最全遇到问题好查。7.2 增加WiFi上报功能现在系统是通过串口上报如果你想让它在无人时也能通知到人可以加一个ESP8266模块。ESP8266通过AT指令和STM32通信STM32把报警帧通过串口发给ESP8266ESP8266再转发到服务器。代码改动很小在comm_protocol.c里加一个ESP8266的发送函数报警时调用它就行。注意ESP8266的AT指令响应时间较长别放在中断里发会阻塞。7.3 多探头组网单个STM32最多能挂的探头数量有限ADC通道和GPIO数量限制。如果实验室面积大可以用多个STM32节点通过RS485总线组网一个主机轮询多个从机。RS485用MAX485芯片半双工通信一根双绞线能挂32个节点覆盖几百米没问题。主机代码里加一个轮询状态机依次问每个从机要数据。从机收到自己的地址才响应其他时候保持监听。这个架构扩展性很好但要注意总线冲突问题轮询间隔要留够。7.4 数据记录和趋势分析现在的系统只做实时报警不存历史数据。如果你想分析实验室的安全趋势可以加一个SD卡模块用SPI接口把每次报警的时间、触发源、传感器读数写到CSV文件里。积累一段时间后用Excel或者Python做趋势图能看出哪些时段、哪些位置风险高。SD卡用FatFs文件系统STM32的SPI读写速度够用。注意SD卡写入时会有几百毫秒的阻塞别放在主循环里用DMA或者放到低优先级任务里。8. 我在这个项目里最想分享的几条经验做这个项目前后花了大概两个月硬件改了三次代码重构了两次。踩过的坑上面基本都写了最后再补几条我觉得最有价值的经验。第一条先画状态图再写代码。我第一版代码是边想边写的结果状态迁移逻辑一团乱改一个地方崩三个地方。第二版我先在纸上把四个状态和所有迁移条件画清楚再照着图写代码一次通过。状态机这种东西想清楚了写起来很快想不清楚写多少遍都是错的。第二条传感器一定要做冗余。单靠一个传感器判断火灾要么误报要么漏报。我这个项目用了三种不同原理的传感器温度、烟雾、火焰互相印证。实际部署时我还建议再加一路手动报警按钮万一传感器全挂了人还能手动触发。第三条报警阈值要可配置。代码里我把阈值存在Flash里通过串口命令可以改。因为不同实验室的环境不一样有的地方常年温度就高阈值设60℃可能天天报警。做成可配置的部署时现场调一次就行。第四条别忽略故障检测。传感器坏了比传感器报警更危险——坏了之后系统以为一切正常实际上已经失去监测能力。我在代码里加了传感器在线检测DS18B20读不到ROM就报故障MQ-2的ADC值长时间不变说明传感器失效也报故障。故障态下蜂鸣器会发出不同于报警的提示音提醒人去检查。第五条外壳和布线也是系统的一部分。我第一版把电路板裸放在实验台上结果被试剂溅到短路了。后来做了个亚克力外壳探头用延长线引出来固定在墙上主机放在干燥通风的位置。探头的位置很讲究温度探头要远离空调出风口烟雾探头要装在天花板附近烟往上飘火焰探头要对着可能起火的区域。这套东西开源出来就是希望更多人能低成本地把实验室安全做起来。代码和原理图都在仓库里有问题可以提issue我看到会回。如果你基于这个项目做了改进也欢迎反馈好的改动我会合并到主分支。