
做嵌入式这些年我经常在论坛和问答平台看到两类问题一类是“刚学完STM32基础不知道做什么项目练手”另一类是“课程设计/毕业设计想做个带传感器、带报警、带上位机或者显示的单片机系统有没有完整资料参考”。今天要分享的这个开源项目——基于STM32的智能安防与燃气监测系统恰好能把这两类问题一次解决掉。整套内容包括完整的Keil工程代码、可生产的原理图文件和Proteus仿真工程核心功能一句话就能说清用一颗STM32F103C8T6配合人体红外传感器和气体浓度传感器实现入侵检测、燃气泄漏监测、声光报警、OLED实时显示整套逻辑在仿真环境里就能完整跑通。这个项目是我从带学生做课设的过程中沉淀下来的经历过好几轮答辩和实际打板验证不是那种只能点个灯的教学demo而是把传感器采集、数据处理、状态判断、联动控制这些真实工程问题串成了一个闭环。对刚入门的嵌入式爱好者来说它是很好的综合练手项目对正在准备课设、电赛或者找工作作品集的同学来说它又足够完整、能讲出故事来。文章我会从方案选型、原理图设计、代码逻辑到仿真验证一条线拆开讲最后把调试中踩过的坑和排查经验一并整理出来保证你看完能自己复现。1. 项目整体设计与方案选型1.1 系统功能拆解先把项目到底要干什么拆明白。智能安防和燃气监测本质上是两条传感链路同时工作安防链路由PIR人体热释电红外传感器完成用于探测是否有人员进入布防区域燃气链路由半导体气敏传感器完成用于检测空气中可燃气体主要是天然气、液化气的主要成分浓度是否达到危险阈值。两条链路的检测结果汇入主控STM32由主控根据当前系统状态决定是否触发报警并把实时数据、报警原因、系统状态显示在OLED屏幕上。按照这个拆解系统可以分成四个层次底层是传感器采集层负责把人体的红外信号和气体的浓度信号变成电信号中间是主控处理层负责ADC采样、数据滤波、阈值判断、逻辑控制上层是信息展示层负责显示浓度数值、布防状态和报警提示最后是执行输出层包括蜂鸣器、指示灯和继电器控制的排风扇等联动设备。整个项目从头到尾都围绕这四层展开每一层都有值得讲的工程细节。1.2 主控为什么选STM32F103C8T6主控的选择我几乎没有犹豫直接用STM32F103C8T6。这颗芯片是Cortex-M3内核主频72MHzFlash 64KBSRAM 20KB片上有3个USART、2个I2C、2个SPI、3个通用定时器和1个高级定时器还有2个12位ADC最多支持10个通道。对这类项目来说资源绰绰有余。选择它的理由第一是性价比实在太高市面上几块钱就能买到打样做板子整体物料成本能压到五六十块钱以内第二是资料生态太成熟从标准外设库到HAL库从Keil到STM32CubeMX网上随便一搜就是海量例程遇到问题不会卡死第三是从学习角度考虑这颗芯片的GPIO、外部中断、定时器、ADC、I2C这些外设正好是嵌入式开发的核心知识点做这个项目等于把这些外设全部实践了一遍比单纯跑例程的理解深入得多。当然用国产的GD32E103、CH32F103系列也可以引脚和寄存器基本兼容代码稍作调整就能跑。1.3 传感器选型思路燃气传感器我选了MQ-2没有选MQ-5。这两个型号看起来差不多但侧重方向有区别MQ-2对丙烷、丁烷、液化石油气、烟雾都有不错的灵敏度测量范围大概在300~10000ppm更适合作为“燃气烟雾”综合监测的传感器MQ-5对液化气和天然气的选择性更好如果项目明确只监测管道天然气MQ-5会更合适。我做这个项目时考虑到应用场景是家庭厨房环境既要防燃气泄漏也要兼顾烟雾预警所以选了MQ-2。型号检测对象优势适用场景MQ-2液化气、丙烷、丁烷、烟雾响应范围宽低成本厨房燃气烟雾综合监测MQ-5液化气、天然气对烷类气体选择性好管道天然气泄漏专用MQ-7一氧化碳CO检测灵敏度高燃气不完全燃烧产生CO的场景人体感应传感器选择HC-SR501热释电红外模块这个模块集成了菲涅尔透镜和信号处理电路输出直接是TTL电平有人进入检测区域时输出高电平人离开后自动延时拉低。模块上有两个电位器一个调节检测距离默认3~7米可调一个调节输出延时默认5秒到数分钟可调实际使用非常方便。显示方面用0.96寸I2C接口的OLEDSSD1306驱动四根线就能接好比LCD1602省了一半以上的IO口而且显示内容灵活能画图标、能动态刷新数值。2. 硬件原理图设计要点2.1 STM32最小系统与下载电路原理图设计从最小系统开始。STM32F103C8T6的最小系统包含电源、晶振、复位、BOOT配置和下载接口五个部分缺一不可。电源部分选用AMS1117-3.3稳压芯片输入5V输出3.3V给主控和传感器供电输入输出端各加一个10uF电解电容和一个0.1uF陶瓷电容做滤波。晶振用的是8MHz无源晶振两个20pF负载电容分别接到晶振两端和地同时并一个1MΩ电阻在晶振两端帮助起振。复位电路是一个10K上拉电阻加一个0.1uF电容到地再通过一个按键连接到复位引脚按下时拉低复位STM32。BOOT配置和下载电路是新手比较容易忽略的地方。STM32F103有三个启动方式通过BOOT0和BOOT1引脚的电平组合选择。我们正常跑程序用BOOT00、BOOT1任意即可所以BOOT0通过10K下拉电阻接地BOOT1也下拉接地。下载调试接口选择SWD而不是JTAG因为SWD只需要PA13SWDIO、PA14SWCLK两根线加电源和地就能完成下载和调试省下来的PA15、PB3、PB4这些JTAG占用引脚可以留给其他功能使用。板子上留一个4pin的排针座接ST-Link或者J-Link的SWD接口。2.2 传感器接口与信号调理MQ-2传感器的接口电路是整个原理图里最需要讲清楚的地方。MQ-2是一个半导体气敏元件内部有一个加热电阻和一个气敏电阻。使用时需要给加热丝提供5V电压让它预热气敏电阻的阻值会随气体浓度变化然后和负载电阻分压从分压点引出模拟电压信号给主控ADC。负载电阻的选择很关键MQ-2在洁净空气中的电阻大概在10kΩ左右气体浓度升高时电阻下降。我实际测试用的负载电阻是4.7kΩ这个阻值比较均衡浓度低时输出电压大概在1.5V左右浓度高时输出电压能到3.5V以上落在STM32的ADC采样范围内0~3.3V既不会测不到变化也不至于过早饱和。分压输出引脚接一个0.1uF电容到地做滤波再串联一个1K电阻限流后进入STM32的ADC输入引脚PA0。PIR模块的接线比较简单VCC接5VGND接地OUT接PA1模块自身有三颗电解电容做电源滤波大部分情况下不需要额外加电容但如果现场干扰严重可以在OUT输出端到地并联一个100nF电容不过要注意这会让信号边沿变缓消抖代码里要留一点余量。2.3 报警与联动电路蜂鸣器电路要特别说一下。很多新手直接把蜂鸣器接在GPIO上结果发现声音很小或者干脆不响原因是STM32的GPIO输出能力有限灌电流或者拉电流只有几毫安驱动不了蜂鸣器这种感性负载。正确做法是使用NPN三极管驱动GPIO通过一个1K电阻接到三极管基极蜂鸣器接在5V电源和三极管集电极之间发射极接地。GPIO输出高电平时三极管导通蜂鸣器得电发声GPIO输出低电平时关断。蜂鸣器最好用有源蜂鸣器一个高电平就能驱动单片机这边不用输出PWM波形省事很多如果愿意多写几行定时器代码用无源蜂鸣器可以输出不同频率的报警音比如燃气报警用急促的“滴-滴-滴”音安防报警用连续的“长鸣”音用户一听就能区分报警类型。继电器输出部分用于控制排风扇或电磁阀。STM32 GPIO同样不能直接驱动继电器线圈我用的是S8550三极管驱动5V继电器继电器线圈两端反向并联一个1N4007二极管做续流保护。这个二极管绝对不能省继电器线圈在断电瞬间会产生一个反向电动势如果没有续流二极管这个尖峰电压很可能打坏三极管甚至干扰主控复位。继电器触点一端接220V火线进线另一端接到排风扇插座的火线端零线直通这样继电器吸合时排风扇得电工作。2.4 供电设计与PCB布局避坑供电拓扑上用USB的5V输入经过AMS1117稳压到3.3V给主控5V直接给传感器加热丝、蜂鸣器和继电器供电。这样做的好处是3.3V的模拟信号数字部分和5V的功率驱动部分在电源上做了隔离避免继电器吸合瞬间的电流跌落影响ADC采样精度。PCB布局上我踩过一个比较大的坑MQ-2是发热元件加热丝功耗不小在PCB上长时间工作会把周围局部温度抬升几度影响PIR和ADC的稳定性。所以布局时MQ-2尽量放在板边用开孔焊盘把它架起来让它和PCB主体保持一点距离同时远离STM32主控和ADC采样电路。地线处理上模拟地和数字地要做单点连接。把MQ-2分压电路的地、ADC采样电路的地单独走一小块模拟地通过0Ω电阻或者磁珠和主数字地单点汇合避免数字信号的开关噪声通过地平面串进ADC参考地。这个细节在实物调测时不一定每次都能明显感知到差异但项目要长期稳定工作这种设计冗余是很必要的。3. 软件代码实现与核心逻辑3.1 整体程序框架状态机设计软件部分我采用一个主循环加状态机的结构不使用RTOS也没有做大循环里所有任务不分轻重缓急地轮流查询。系统划分出三个核心状态布防待机态、撤防态、报警态。布防待机态下两类传感器都在监测没有触发条件时屏幕显示实时浓度和“ARMED”字样一旦PIR检测到人体或者燃气浓度超限系统进入报警态蜂鸣器鸣叫、LED闪烁、继电器根据报警类型执行对应的动作撤防态是用户通过按键主动解除安防功能时进入的状态此时PIR不再触发报警但燃气监测仍然保留——燃气泄漏这种威胁不应该因为撤防而被忽略这个细节在答辩时是一个很加分的思考点。状态机的好处是可以把系统行为分得很清楚每个状态下的传感器行为、输出执行各自独立新增功能时不会牵一发动全身。初版代码我也写过“堆一坨if在循环里”的写法结果功能多了以后稍不留神就会在某个条件下漏掉蜂鸣器关闭逻辑导致报警声一直响。改成状态机之后所有状态切换就发生在明确的几个判断点出问题也容易定位。整个主循环的结构如下while (1) { // 周期性采集与处理每50ms执行一次 if (systick_flag_50ms) { adc_value read_mq2_avg(10); mq2_voltage adc_value * 3.3f / 4096.0f; gas_percent mq2_to_percent(mq2_voltage); pir_level HAL_GPIO_ReadPin(PIR_GPIO_Port, PIR_Pin); display_update(gas_percent, system_state, alarm_source); process_key(); } // 状态机转移和报警输出每100ms执行一次 if (systick_flag_100ms) { system_state fsm_update(system_state, pir_level, gas_percent); alarm_output_update(system_state, alarm_source); } // 按键检测不阻塞 key_scan_nonblocking(); }3.2 ADC采样与滑动滤波STM32F103的ADC是12位的参考电压3.3V所以ADC读数对应的电压就是ADC值 × 3.3 / 4096。我使用PA0作为MQ-2模拟信号输入ADC配置成扫描模式多通道采集时用DMA搬运结果这样在燃气和后续扩展的烟雾传感器同时接入时不用频繁打断主循环。实际项目中虽然只用了两个模拟通道但DMA的写法还是保留了后续有扩展需求直接加通道就行。MQ-2的模拟输出并不是一个干净的直流电平它会有明显的波动尤其在刚通电预热阶段或者环境气流变化时。为了解决这个问题我在软件里做了两层滤波第一层是单次的硬件均值每50ms连续采样10次取平均值消除高频噪声第二层是对多个均值再做滑动平均我取5个窗口相当于250ms内的数据做一次平滑。这里有个经验参数滑动窗口取太大系统对突发泄漏的响应会变慢窗口取太小浓度波动还是会带来误报警。实测5个窗口在响应速度和稳定性之间比较平衡。float read_mq2_voltage(void) { uint32_t sum 0; for (uint8_t i 0; i 10; i) { sum adc_read_channel(0); delay_us(20); // 相邻采样之间留出采样保持时间 } float avg sum / 10.0f; return avg * 3.3f / 4096.0f; } float get_filtered_voltage(void) { static float buf[5] {0}; static uint8_t index 0; static uint8_t fill 0; float v read_mq2_voltage(); buf[index] v; index (index 1) % 5; if (fill 5) fill; float sum 0; for (uint8_t i 0; i fill; i) { sum buf[i]; } return sum / fill; }需要注意的是STM32的ADC每个通道转换打开后第一次采样的结果通常不太准所以我在初始化后先做几次“空采”丢弃前几次结果然后再进入正式采样循环。3.3 人体感应与按键消抖PIR传感器模块输出的信号本身带有一个“占空比”特性人进入检测区后输出高电平保持一段时间然后自动变低即使人还站在原地模块也不一定持续输出高电平因为PIR检测的是红外辐射的变化量静止的人体不会持续触发。所以代码里对PIR状态做了脉冲锁存处理一旦检测到高电平就把它锁存为“有人”状态持续保持3分钟3分钟内没有新的触发才自动解锁。这样就不会出现在系统报警了几秒后因为PIR输出回落而报警停止的情况。锁存时间在代码里用定时器实现没有占用主循环的delay。按键方面系统需要一个“布防/撤防”切换键和一个“消音”键。机械按键的抖动是典型的毛刺信号我不推荐用delay延时消抖因为阻塞式消抖会让主循环卡顿影响ADC采样和状态机运转。正确的做法是在系统tick中断里每10ms扫描一次按键连续读到3次相同的稳定电平后认为按键有效即“10ms×330ms”消抖窗口。按键按下的状态标记为一个事件标志主循环查询到标志后执行对应逻辑这样按键处理完全非阻塞。void key_scan_10ms(void) { static uint8_t cnt 0; static uint8_t last_level KEY_RELEASE; uint8_t level HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin); if (level ! last_level) { cnt 0; last_level level; } else { cnt; } if (cnt 3) { // 连续3次采样一致 if (level KEY_PRESS) { key_event KEY_EVENT_BREAK_DEFENSE; // 产生切换布防事件 } cnt 0; } }3.4 报警联动与显示逻辑报警逻辑是两个独立判断的叠加不互相阻塞。燃气报警和安防报警各自维护各自的触发源状态谁触发了谁出现在“报警原因”里如果两个同时触发OLED屏幕上两种报警提示交替显示。报警阈值我设定为MQ-2输出电压大于2.2V时判定为燃气浓度超标对应大概0.1%的甲烷浓度具体标定方法放在后面的常见问题里讲PIR锁存状态为真且系统处于布防态时判定为安防入侵。输出执行部分蜂鸣器在燃气报警时输出持续音在安防报警时输出急促的间歇音LED指示灯在报警时以1Hz频率闪烁正常布防时0.5Hz慢闪撤防时熄灭。LED的状态指示虽然是小功能但能直观体现系统当前处于什么状态对演示和答辩都很好用。这里有一个新手很容易忽略的细节报警解除不能只看当前传感器读数恢复到正常值必须加一个“滞后回差”逻辑。比如燃气报警阈值是2.2V那么解除报警的阈值设为1.8V而不是2.0V这样在临界点附近气体浓度轻微抖动时系统不会频繁地“报警-解除-报警”来回切换。这个思路其实和温控器、电冰箱压缩机的控制逻辑一模一样是一个很经典的工程抗抖手段。OLED显示使用了SSD1306的I2C接口驱动主控用PB6I2C1_SCL和PB7I2C1_SDA两个引脚I2C速率400kHz。显示内容分三行第一行显示燃气浓度百分比和状态图标第二行显示系统布防/撤防状态第三行显示最近的报警记录。每次刷新时先更新一个局部缓冲区只在数值变化时才往OLED写数据避免无意义的刷屏这样也能降低I2C总线的占用率。4. Proteus仿真验证4.1 仿真环境搭建用Proteus做仿真最大的价值在于可以在不焊板子、不买传感器的情况下把系统整体逻辑跑通尤其适合前期调试程序框架和演示答辩。Proteus 8以上的版本支持STM32F103系列仿真模型新建工程后在元件库里搜索STM32F103C8T6放到原理图工作区再添加OLED或LCD显示模块、按键、LED、蜂鸣器等外围器件。仿真工程里需要建立一个和实物一致的硬件连接图。电源用Proteus自带的VCC/GND终端晶振电路可以简化但复位和BOOT建议按照原理图接好。程序生成方面用Keil MDK编译工程Output选项里勾选“Create HEX File”编译生成的.hex文件在仿真工程里双击STM32芯片在Program File一栏选择这个hex文件然后点击运行。这里的要点是Keil工程里的目标芯片型号、调试器配置都不会影响hex生成真正影响能否仿真的是代码里对时钟源的配置——Proteus的STM32模型默认使用内部RC时钟也能跑但如果你在代码里配置了外部高速时钟HSE且仿真的晶振参数设置不当程序会卡在启动时的时钟切换上。所以仿真工程里我给芯片外挂了8MHz晶振并在仿真设置中把量程调整为8MHz。4.2 如何模拟传感器输入仿真里没有真实的MQ-2模型和PIR模型需要用手动方式模拟传感器输出。MQ-2使用一个电位器来模拟电位器一端接5V另一端接地中间滑动端接STM32的PA0通过拖动电位器改变分压比就能模拟燃气浓度升高和降低时ADC电压的变化。这个做法虽然简单但非常直观答辩演示时直接把电位器往上一拧屏幕上的浓度数值和报警状态立刻发生变化效果相当有说服力。PIR信号用一个按键加一个上拉电阻代替按键一端接3.3V另一端接PA1同时PA1与地之间接一个10K下拉电阻。按下按键时PA1读到高电平模拟PIR检测到人体松开后回落到低电平模拟PIR输出结束。如果要在仿真里验证PIR的锁定逻辑可以在代码里把锁存时间从3分钟临时改短成10秒按下按键看状态变化10秒后再看是否自动恢复。OLED在Proteus里的仿真模型不是很好用早期版本对SSD1306的支持不太完善显示效果经常是乱码或者直接黑屏。我在仿真里退一步用LCD1602模块替代OLED把代码里显示驱动做一个接口抽象层定义一个display_show_gas(float percent, uint8_t state, uint8_t alarm_source)函数在实物工程里调用SSD1306的驱动在仿真工程里调用LCD1602的驱动两个实现各写一份主逻辑代码不做任何修改。这个方法体现了一个很重要的工程习惯驱动层和应用层解耦。仿真跑的是应用层逻辑底层驱动只要接口一致换起来非常容易。4.3 仿真与实物调试的差异要提醒大家的是仿真通过只代表逻辑正确不代表硬件直接能work。我在仿真里从没遇到过传感器预热漂移、继电器电磁干扰导致ADC跳变、蜂鸣器驱动电流不足这类模拟器感知不到的问题。仿真给你的是一张“逻辑免检卡”真正的考验永远在实物调试。所以做这个项目我强烈建议先仿真跑通逻辑再画板子做实物两者配合而不是互相替代。很多同学仿真好几个月实物一通电就懵了就是因为把仿真当成了终点。5. 常见问题与调试避坑实录5.1 MQ-2首次上电读数偏高MQ-2传感器内部有加热丝上电初期加热丝温度还没稳定气敏电阻的阻值会明显低于稳定值导致输出电压在一个比较高的水平代码里判定起来好像是“燃气浓度超标”。实际上不是这是传感器在预热。解决方法是硬件上电后延时等待代码里至少延时30秒我用的是2分钟再初始化报警判断逻辑或者启动时连续读取5分钟数据做一个动态基线校准。实操中最稳妥的做法是上电后在前2分钟只显示浓度不启用报警阈值2分钟后再进入正常监测状态。在这个项目代码里我用一个CONFIG_PREHEAT_MS的宏来控制预热时间去掉软件延时改用定时器计满时间后切状态避免阻塞主循环。注意MQ-2长期暴露在高浓度气体中也会导致基线漂移如果出现“清新空气下浓度忽高忽低”的现象可以先断电静置几小时让传感器恢复。5.2 误报警和频繁报警的排查误报警的原因通常有三个阈值设置得太低、滤波没有做好、PIR锁存逻辑有问题。阈值方面建议用500mL密封袋装一定浓度的标准气体实测标定但一般课设环境不具备标定条件可以退而求其次用“环境本底安全裕量”的方式设置先让系统在正常空气环境下运行5分钟记录稳定电压值再在这个基础上加0.5V作为报警阈值。滤波方面如果滑动平均窗口已经用了5次均值还有频繁波动可以把窗口调到10次代价是响应时间从250ms变成500ms对这个应用场景完全可接受。PIR侧的问题是模块本身的延时旋钮调得太短人离开检测区后输出很快拉低如果代码里没有做锁存就会出现人刚离开报警就停的情况。我是把模块上的延时旋钮调到了最小全部交给代码里的3分钟锁存来控制这样行为完全由软件决定不会因为模块旋钮被误碰导致行为不一致。5.3 程序烧录与仿真常见问题Keil编译没有报错但Proteus里程序不运行首先检查hex文件是否生成成功其次检查STMR32F103C8T6模型里加载的是不是最新编译的hex文件。如果芯片始终处于复位状态多半是晶振配置和仿真模型不匹配可以在代码里把SystemInit后的时钟切换错误死循环部分加一个串口打印提示辅助定位。实物烧录时找不到芯片常见原因是ST-Link驱动没装好、接线错误或者SWDIO/SWCLK被代码复用成普通GPIO。这里给一个保底建议STM32的SWD引脚在上电时默认是调试功能如果你的代码初始化了PA13/PA14做普通IO第一次烧录是没有问题的但第二次就下载不进去了。解决办法是在烧录时按住复位键点击下载后再松开如果是用ST-Link可以在MDK的Flash Download选项卡里勾选Reset and Run和Erase Sectors利用“先擦除后编程”的方式恢复调试口。5.4 开源包内容和用法整个开源资源包我已经整理好包含五部分内容完整的Keil MDK工程源码标准外设库版本无RTOS注释比较全、可生产的原理图文件包含PDF版本没有AD也能直接看、Proteus仿真工程版本要求Proteus 8.6以上、BOM物料清单所有元件的封装和购买参考按这个清单打样贴片基本一次成功、README说明文件包含接线表、引脚分配表和常见问题索引。拿到包之后建议按这样的顺序操作先看README里的引脚分配表再打开Proteus仿真跑一遍看整体效果然后对着原理图在面包板上搭建实物电路最后打开Keil工程从代码层面逐模块阅读。这样的路径能让硬件、软件、仿真三者互相印证理解会特别扎实。做这个项目我个人的体会是它最大的价值不在于某个传感器的驱动写得多么花哨而在于把“采集—处理—判断—输出—反馈”这个完整的嵌入式系统闭环走了一遍。你在这套代码里能看到状态机怎么设计、传感器噪声怎么处理、报警逻辑怎么抗抖、底层驱动怎么抽象这些能力是刷几十道单片机课后题都换不来的。项目本身后续还可以往几个方向扩展加ESP8266做WiFi远程报警推送、加SIM800模块发短信、加风扇联动控制降低燃气浓度、改用FreeRTOS做多任务版本都是很好的进阶方向。先把这套基本盘吃透后面每一步都会走得顺畅很多。