做嵌入式这么多年手头攒了不少项目但真正让我觉得“值得开源出来给大家参考”的实验室消防预警控制系统算一个。这名字听着挺大实际上拆开看就是一个典型的STM32多传感器数据采集与联动控制项目实时监测环境温度、烟雾浓度超阈值就报警、亮灯、启动排风扇顺便把数据扔到屏幕上显示。功能不复杂但麻雀虽小五脏俱全从GPIO、ADC、定时器、中断到UART、I2C几乎把STM32的基础外设都过了一遍。我把它完整整理了出来包括可直接编译的代码工程、手绘的原理图以及Proteus仿真文件。不管你是准备做课程设计、毕业设计还是刚入门想找个完整的项目练手这套东西都能直接“抄作业”改一改就能变成智能家居安防、机房温控报警之类的衍生项目。下面我把整个项目的设计思路、核心代码、仿真搭建和调试踩坑过程都交代清楚。1. 项目整体设计与思路拆解1.1 为什么选STM32做消防预警简单说就三个字够用、便宜、资料多。消防预警这种场景核心需求是实时采集环境参数、快速判断、可靠输出。STM32F103系列主频72MHz跑这种逻辑绰绰有余而且它的ADC采样、外部中断、定时器PWM这些外设刚好全覆盖我们的需求。市面上二手板子二三十块钱烧了也不心疼。有人可能会问用Arduino不是更简单但实验室消防预警讲究的是“可控”和“可扩展”。Arduino封装太狠很多底层细节看不到STM32则要求你自己配置时钟树、初始化寄存器、写中断服务函数这个过程虽然麻烦但能让你真正理解单片机是怎么工作的。而且后续想加CAN总线组网、加WiFi模块上传数据、加FreeRTOS跑多任务STM32的资源和生态都撑得住Arduino就有点吃力了。1.2 系统整体架构与工作流程整套系统我分成四个层次来设计感知层、处理层、执行层、交互层。感知层用了三个传感器DHT11测温度和湿度MQ-2烟雾传感器测可燃气体/烟雾浓度再加上一个可选的火焰传感器红外探测做双重确认。处理层就是STM32F103C8T6跑一个裸机主循环中断的架构没有上操作系统因为逻辑确实没必要。执行层包括蜂鸣器、LED报警灯、继电器控制排风扇、以及一个电磁阀接口预留用于切断燃气或喷淋。交互层则是一块0.96寸OLED显示屏和三个按键用来本地查看数据和手动控制。工作流程是这样的主循环里每200ms读一次DHT11每50ms采一次MQ-2的ADC值然后做滑动滤波。如果烟雾浓度超过一级阈值比如ADC值1200触发蜂鸣器间歇鸣叫黄色LED闪烁OLED上显示“预警”状态如果超过二级阈值比如ADC值2000或者火焰传感器触发系统进入“紧急报警”状态蜂鸣器连续长鸣红色LED常亮继电器吸合启动排风扇同时通过串口向上位机发送报警帧。按键则可以手动切换风扇开关、静音报警、调整阈值。1.3 开源包的内容与文件组织为了让大家拿到手就能用我把整个工程的目录结构设计成下面这样Hardware/—— 原理图PDF、PCB布局建议、元件清单BOM表Firmware/—— Keil5工程包含全部源码和启动文件Simulation/—— Proteus 8.11仿真工程含固件hex文件Docs/—— 项目说明文档、引脚分配表、通信协议说明Algorithms/—— 温度补偿计算脚本、阈值标定记录Excel表这样的结构对新手比较友好先看文档了解全貌再打开仿真跑一遍看看效果最后才动手焊板子和烧代码。很多开源项目就丢一堆代码连引脚定义都要自己猜很劝退我不干这种事。文档里我连每个按键按下去执行什么逻辑都画了流程图。2. 核心硬件设计与原理图解读2.1 主控最小系统与引脚分配STM32F103C8T6的最小系统其实就四部分电源滤波电路、晶振时钟电路、复位电路、下载调试接口。原理图里我这部分画得很常规——8MHz主晶振配两个20pF负载电容32.768kHz的RTC晶振也预留了位置不过这个项目没用到RTC所以没贴片。复位电路用经典的10kΩ电阻100nF电容NRST引脚低电平复位。关键的是引脚分配我踩过坑之后重新梳理了一遍务必按照下面的表来接线外设模块引脚说明DHT11数据线PA6开漏输出外部上拉4.7kΩ至3.3VMQ-2模拟输出PA1ADC1通道1采样时间拉长以稳定火焰传感器DOPA4数字输入内部上拉低电平有效蜂鸣器PB5三极管驱动低电平触发红色LEDPB6推挽输出高电平点亮需要串限流电阻黄色LEDPB7同上继电器控制PB8三极管续流二极管驱动控制220V排风扇OLED I2CPB8/PA9注意这里我用了软I2CPB8为SCL、PA9为SDA按键KEY1~KEY3PA0/PA8/PB0全部内部上拉按下为低电平这里有个细节值得展开说为什么OLED用软件I2C而不用硬件I2C因为STM32F103的硬件I2C模块被很多人吐槽过——偶发总线锁死、仲裁逻辑诡异、调试起来极其折磨。说实话我早期也折腾过硬件I2C后来发现软I2C在这个场景下完全够用OLED刷新率本来就不高40kHz的软件时序足够而且代码可移植性更强换成ESP32或者51都能用。对于新手我更推荐先把软I2C跑通。2.2 传感器接口电路的设计细节先说DHT11。这货是单总线协议一根线既传数据又传时钟时序要求比较严格。电路上需要注意两点第一数据线必须接上拉电阻4.7kΩ到10kΩ都行我实测4.7kΩ在5cm杜邦线情况下波形最干净第二DHT11供电要干净最好在VCC和GND之间并一个100nF去耦电容不然数据偶尔会跳变。再讲MQ-2。它是个半导体气敏传感器内部有个加热丝工作时先要把敏感层加热到一定温度才能正常工作所以上电后需要至少1分钟的预热时间这一分钟内数据漂移很厉害我一般在代码里做了“上电忽略前60秒报警”的处理。MQ-2模块在大多数开发板上已经集成了比较器电路输出DO数字量和AO模拟量。数字量那个阈值电位器默认拧得比較不灵敏我建议直接用AO接STM32的ADC在软件里做阈值判断灵活度更高。原理图上我在AO输出端加了一个100nF电容到地作用是把高频噪声滤掉ADC采样值稳定很多。火焰传感器用的是简单的红外接收管方案检测波长760nm-1100nm范围内的火焰光谱。这玩意儿有个著名的毛病——对阳光和普通白炽灯也敏感所以我在代码里用了“连续检测5次都触发才确认为火灾”的消抖策略物理安装时也建议加一个遮光罩只让前方视场的光进来。2.3 驱动电路与电源设计驱动部分我重点讲继电器。排风扇是220V交流负载绝对不能直接接在STM32的GPIO上必须通过继电器隔离。我用的继电器模块是低电平触发的线圈额定电压5V吸合电流大概70mA——这个电流STM32引脚带不动所以中间加了一颗S8050三极管做电流放大。原理图里要注意的是三极管基极必须串一个1kΩ限流电阻防止GPIO输出电流过大继电器线圈两端要并联一个1N4007二极管方向是反向的这是续流二极管用来在线圈断电瞬间吸收反向感应电动势不然那个高压尖峰会直接击穿三极管。这一点很多人画图时容易漏。电源方面整个系统用了双路供电STM32板和传感器用USB的5V经过AMS1117-3.3稳压得到3.3V继电器模块直接吃5V它有个光耦隔离不占3.3V的电流预算。需要注意MQ-2的加热丝电流在150mA左右加上其他负载USB口供电勉强够但如果你外接更多的传感器强烈建议换一个5V/2A以上的适配器或者用MP1584降压模块从12V电源转不然电压跌落会导致DHT11读数异常。3. 核心代码逻辑与实现解析3.1 工程结构与初始化流程代码用Keil5写的基于标准外设库StdPeriph_Lib V3.5没用HAL库。原因很简单标准库对寄存器的封装比较薄代码看起来更像“在写单片机”很多面试和考试也默认考标准库当然HAL库也不是不行但工程体积大、初始化代码啰嗦对一个逻辑不算复杂的项目来说有点杀鸡用牛刀。工程文件组织如下Firmware/ ├── USER/ # main.c, stm32f10x_it.c, 系统时钟配置 ├── HARDWARE/ # dht11.c, mq2.c, oled.c, buzzer.c, relay.c, key.c ├── SYSTEM/ # delay.c, serial.c调试串口 ├── CORE/ # 启动文件startup_stm32f10x_md.s, 内核寄存器定义 └── OBJ/ # 编译输出的hex文件主函数里的初始化顺序很关键先配置系统时钟默认72MHz再初始化延时函数因为后面所有外设都要用delay然后依次初始化串口、OLED、DHT11、ADC、按键、继电器和蜂鸣器。所有外设初始化完成后OLED会先显示一个“System OK”的启动画面然后才进入主循环。3.2 主循环状态机设计整个系统我用了一个简单的状态机来管理比在main函数里堆逻辑要清晰得多。定义了三个状态typedef enum { SYS_MONITOR, // 正常监控状态 SYS_WARNING, // 一级预警烟雾浓度超标 SYS_ALARM // 二级紧急报警严重超标或火焰触发 } SysState_t;主循环每轮先执行传感器数据更新非阻塞方式然后基于数据判断当前状态最后根据状态刷新执行器。关键代码如下while (1) { // 1. 周期性采集数据非阻塞调度 Sensor_Task(); // 2. 状态判断与迁移 uint16_t smoke_value MQ2_GetValue(); float temperature DHT11_GetTemperature(); uint8_t flame_detected Flame_GetStatus(); if (flame_detected || smoke_value ALARM_THRESHOLD) { current_state SYS_ALARM; } else if (smoke_value WARNING_THRESHOLD) { current_state SYS_WARNING; } else if (temperature TEMP_ALARM_THRESHOLD) { current_state SYS_WARNING; // 高温也触发预警 } else { current_state SYS_MONITOR; } // 3. 状态执行不同状态对执行器做不同操作 switch (current_state) { case SYS_MONITOR: Buzzer_Off(); Relay_Off(); LED_Set(LED_YELLOW, OFF); LED_Set(LED_RED, OFF); break; case SYS_WARNING: Buzzer_Beep(1000, 500); // 1kHz频率鸣叫500ms间隔 LED_Set(LED_YELLOW, ON); break; case SYS_ALARM: Buzzer_Beep(2000, 200); // 变调持续鸣叫 LED_Set(LED_RED, ON); Relay_On(); // 启动排风扇 UART_SendFrame(ALARM_FRAME); break; } // 4. 刷新OLED显示 OLED_UpdateScreen(); // 5. 按键扫描带消抖 Key_Task(); }这套逻辑写起来简洁读起来也直观。实际工作中如果你要加“烟雾浓度持续超标超过30秒才报警”这种防误报功能只需要在状态迁移前加一个计时判断就行状态机的好处就在这——改一处不影响其他状态。3.3 DHT11时序解析与代码实现DHT11的时序是网上问得最多的我在这里完整讲透。整个通信过程分四步主机发送起始信号、DHT11响应、DHT11发送40位数据8位湿度整数8位湿度小数8位温度整数8位温度小数8位校验和、总线释放。关键点在于“读0”和“读1”的判别。DHT11每一位数据都是这样先拉低50μs表示“准备发送”然后拉高。拉高持续26-28μs表示“0”拉高持续70μs表示“1”。所以代码里读取的策略是等引脚变高后死循环计数直到引脚变低计数值落在哪个区间就判0还是判1。uint8_t DHT11_ReadByte(void) { uint8_t data 0; for (int i 0; i 8; i) { while (DHT11_PIN_READ() 0); // 等待高电平开始 delay_us(40); // 跳过前40μs if (DHT11_PIN_READ() 1) { data | (0x80 i); // 40μs后仍为高说明是1 } while (DHT11_PIN_READ() 1); // 等待位结束 } return data; }这里有个经验值40μs这个判定点是我实测调出来的在5V供电和3.3V供电下略有差异如果你换了供电电压导致读数不对首先调这个值。还要注意DHT11两次读取间隔必须大于1秒我代码里用了一个software timer保证至少1.5秒才发起一次读取不然读回来的永远是上次的缓存数据。3.4 OLED显示与按键交互逻辑OLED部分是软I2C驱动SSD1306控制器96x64分辨率。我给这个项目设计了两个显示页面主页显示温度、湿度、烟雾值、系统状态按KEY1切换到设置页设置页用KEY2加减阈值KEY3确认保存。按键用的就是普通GPIO扫描20ms软件消抖没有上外部中断因为用户在正常操作时并不需要系统“立刻响应”主循环里每50ms扫一次足够。OLED刷屏我做了局部刷新优化只有数据变化超过0.1或者状态切换时才重绘整个屏幕不然哪个数据在动就只更新哪个区域。这样做的好处是减少I2C通信量间接降低了对电源的干扰。实际操作中整屏刷和局部刷在肉眼上很难分辨但局部刷在逻辑上更干净。4. 仿真环境搭建与Proteus实操4.1 Proteus仿真的器件选型与联调Proteus 8.11往上的版本里能直接搜到STM32F103C8T6模型和大部分外围器件。仿真工程里我用了以下元件STM32F103C8T6LQFP48封装模型DHT11模型从第三方库导入Proteus自带的传感器列表里可能没有MQ-2气敏传感器模型用可变电阻电压源模拟LM016L液晶屏作为OLED的替代显示方便初学者观看LED、蜂鸣器、按键、电阻、三极管需要特别说明的是Proteus里没有直接的OLED SSD1306模型除非你自己导入第三方库所以我用了LM016L字符液晶来显示数据。虽然是替代方案但逻辑验证的效果是一样的——只要你把I2C屏换成SPI的LCD或者改几行代码适配LM016L整个项目的判断、报警、风扇联动逻辑都能在仿真里跑通。4.2 仿真的关键操作步骤打开Simulation文件夹里的fire_alarm_sim.pdsprj后流程是这样的先双击STM32芯片模型在Program File一栏选择Firmware/OBJ目录下编译好的hex文件。设置晶振频率为8MHz勾选内部RC选项仿真环境下用外部晶振模型容易出时序问题。点击开始仿真按钮此时OLED/液晶屏应该显示初始数据。滑动MQ-2模型旁边的电位器或直接改电压源输出模拟烟雾浓度升高。观察现象烟雾值超过阈值后蜂鸣器图标会开始鸣叫LED点亮再调高继电器图标吸合风扇模型开始转动。仿真里有个坑DHT11模型对读取时序的容忍度和真实器件不太一样有时候你的代码在真实硬件上跑得好好的仿真里却读不到数据。我遇到这种情况直接把DHT11的读取代码改成仿真模式下固定返回一个模拟值比如温度25.4℃湿度60%用宏区分仿真版本和真实版本。你要是做毕设不要让这种问题卡你一周。#ifdef SIMULATION_MODE // 仿真模式下直接赋值模拟数据 temp 25.4f; humi 60.0f; #else // 真实硬件才走完整的DHT11时序读取 temp DHT11_GetTemperature(); humi DHT11_GetHumidity(); #endif我记得有些同学下了仿真文件后一直白屏大概率是hex文件路径不对或者芯片型号选成了F103RB导致的仿真模型的引脚映射和实物不是一回事一定要选C8T6这颗料对应的型号。5. 常见问题与排查技巧实录5.1 传感器数据异常的真实场景复盘这块我记录两个最典型的故障案例都是从实际调试中抠出来的经验。第一个是烟雾值乱跳。现象MQ-2输出值在300到900之间不规则跳变完全没法判断阈值。排查过程先用万用表量AO引脚电压发现也在跳说明不是ADC采样的问题是传感器输出本身不稳定。再查供电发现USB供电时板子上的3.3V有大约100mV的纹波而MQ-2模块的VCC接的是板子的5VC8T6的ADC参考电压是3.3V这中间没有做滤波。解决办法在MQ-2的AO输出和GND之间并联一个10μF电解电容然后代码里改成了“连续采样10次去掉最大最小再求平均”的滤波。改完之后波动控制在±20以内。第二个是DHT11偶尔读取出错。现象温度一会儿25度一会-3度湿度直接99.9%。排查后发现是我把DHT11的数据线接在PA6但PA6同时被其他代码配置成了复用功能初始化顺序一冲突就把引脚配置覆盖了。这算是新手典型问题——引脚复用冲突。我的建议是初始化时每个外设都显式设置GPIO模式别偷懒只初始化一次而且最好在头文件里做一个“引脚分配总表”开发时每占用一个引脚就勾一下排查起来效率高很多。5.2 代码烧录与调试的注意事项烧录我用的是ST-Link V2SWD模式只占PA13和PA14两个引脚不占用串口。具体操作注意三点第一确保BOOT0引脚接GND否则芯片会进入ISP模式ST-Link连接不上第二SWDIO和SWCLK线上各加一个10kΩ上拉电阻长线连接时更稳第三烧录前先在Keil的Debug设置里选“Connect under Reset”这样即使你之前的代码把调试引脚配置成了普通IO也能强连上。调试阶段串口是神器。我在代码里留了一个UART1打印函数可以实时把温度、湿度、烟雾值和系统状态打出来用USB转TTL小板接PA9(TX)和PA10(RX)波特率115200。实际调试时用串口看数据波形比看OLED方便多了因为你可以把历史数据全部拉出来画曲线判断传感器漂移规律。5.3 阈值标定与参数选择经验这个项目最容易被忽略的就是阈值标定。烟雾传感器的原始ADC值没有绝对意义——同样是2000在实验室正常环境可能表示严重烟雾在常年灰尘大的厂房可能只是正常波动。所以我建议拿到板子后先在你自己的环境里静置10分钟记录稳定值baseline然后以“baseline 300”作为预警阈值“baseline 800”作为报警阈值。这个标定数据我放在Docs/标定记录.xlsx里了你可以按自己的环境重新填一遍。关于温度和烟雾的联合判断我测试时发现一个有趣的现象单独用烟雾传感器触发报警误报率高但如果做了“温度超过55度且烟雾值同时上升”的与逻辑判断误报率明显下降。可惜的是硬件上只有一个DHT11温度传感位置和烟雾源头可能隔着一段距离响应会有延迟。所以我在最终版本里采用了“或逻辑为主、与逻辑可配置”的方案默认模式下任一条件超标即报警但在设置页里可以通过长按KEY2进入“严格模式”必须两个条件同时满足才报警。两种模式各有适用场景你们用的时候按需求调整。5.4 常见问题速查表问题可能原因解决办法烧录时报“No Target Connected”BOOT0未拉低或接线不良检查BOOT0跳帽确认SWD四线连接OLED白屏无显示I2C地址配置错误或上拉缺失确认SSD1306地址为0x78/0x7A检查SDA/SCL上拉DHT11读数为0或恒定上拉电阻缺失/时序被中断打断加4.7kΩ上拉读取期间关中断MQ-2数值缓慢漂移传感器预热不足上电后等60秒再启用报警排风扇不转但继电器有响声继电器触点规格不足或接线方向错改用250V/10A继电器确认常开端并联蜂鸣器声音很小驱动管放大倍数不够换成S8550或增加基极电阻串值按键按下没反应消抖时间太短/引脚复用了延长消抖到20ms检查引脚占用表6. 衍生方向与项目扩展建议这个项目做完之后稍微动动脑筋就能往好几个方向延伸。第一个是加通信模块变成物联网消防终端串口接一个ESP8266或者ESP-01S把传感器数据通过MQTT协议上传到Home Assistant或阿里云IoT平台手机端实时接收报警推送。你只需要改串口部分的代码传感器逻辑和状态机完全不用动。第二个方向是加本地存储和事件记录外挂一个AT24C02 EEPROM系统每次进入报警状态就记录一条事件时间戳触发原因持续时长之后通过串口命令导出。对于实验室安全管理来说事后追溯很重要——哪个时间段发生了什么导致报警查起来一目了然。第三个方向是升级多节点组网。一块STM32的覆盖范围有限你可以把每个实验室部署一套从机然后用RS485总线把数据汇总到一块主机板上。通信协议我已经在Docs里预留了一个简单的帧结构帧头节点ID温度湿度烟雾值状态CRC校验照着这个协议去写从机端的代码就行。我个人的建议是先把原项目的裸机版本吃透尤其是状态机和传感器时序然后再考虑上RTOS或者联网。嵌入式这个行业底层逻辑不扎实后面堆再多的功能也是空中楼阁。这个项目就是一块很好的磨刀石代码量不大但每个外设都逼着你去读数据手册、查时序图、解决实际问题。最后分享一个实际调试中的小技巧我在OLED显示页面上留了一个隐藏的“调试模式”快速连按KEY1六次进入在这个模式下可以看到ADC原始值、DHT11采集失败次数、每次状态切换的系统时间戳。这个调试页在实际开发和后续演示时帮了大忙——观众看到数据不稳你不用解释半天直接切到这个页面就能看到原始数据和采集成功率很多问题当场就能定位。你们拿到代码后也建议保留这个功能必要时再加一个类似的气象日志页。
