
这个项目最早是我给老家厨房做的一台独立报警器老人家记性差烧水忘关火这种事一年总得出几回。市面上成品不少但要么是单一探头加个蜂鸣器联动和远程提醒都缺要么就是整套品牌方案贵得离谱。后来索性拿手头一块 STM32F103C8T6 最小系统板重新做了一版把燃气浓度采集、人体感应、门磁状态、声光报警、继电器联动排风和串口上报全塞进去顺便把开源、代码、原理图、仿真这几样都整理齐了。这篇文章就把整套东西从架构到落地讲透硬件原理图逐块拆、固件代码结构逐层说、Proteus 仿真怎么搭怎么调也一并给出。不管你是刚学完 GPIO 想找个像样的练手项目还是已经画过几块板子想补一套完整报警类产品的设计思路都能直接从里面挑能用的部分。1. 系统整体架构与方案选型拆解动手画图之前先把功能边界定死这一步看着啰嗦实际上决定了后面电路复杂度。我一开始想塞的东西特别多什么摄像头、WiFi 图传、语音播报都往上报画到一半发现板子面积和代码量都会失控。后来按本地必须能独立报警、远程只做通知这个原则砍了一刀才收敛成现在这套方案。1.1 功能清单与交互流程整套系统开机后进入自检OLED 显示传感器预热倒计时MQ-2 需要加热稳定PIR 和门磁进入布防状态继电器默认断开。运行阶段主循环每 200ms 采一次燃气浓度每 500ms 更新一次温湿度PIR 和门磁走外部中断触发。状态机分四档正常、预警、报警、传感器故障。燃气浓度超过第一阈值先亮黄色预警灯并蜂鸣器短促提示超过第二阈值立刻吸合继电器启动排风扇、蜂鸣器长鸣、OLED 红底报警、串口上报报警帧。人在布防状态下被 PIR 触发只上报不联动排风避免误把家庭成员活动当入侵。门磁同理。这套交互逻辑的价值在于报警动作分级不会因为厨房油烟飘一下就整个系统拉满同时保留人工消警按钮长按 3 秒清除报警并反锁 30 秒冷静期防止用户在浓度临界点反复触发。很多初学者做报警项目会把检测到就报警写死结果灵敏度调高全是误报调低又漏报分级加冷静期是更接近真实产品的做法。1.2 主控与外设的选型取舍主控选 STM32F103C8T6理由很直白ADC 有 12 位、10 个通道够采燃气、电池电压、备用模拟口定时器资源足一个跑采样节拍、一个跑 PWM 蜂鸣器、一个做串口超时成本低、资料多新手翻车也能搜到答案。有人问为什么不直接上 F407实话说这个项目对算力没有需求跑 FreeRTOS 都嫌重裸机加状态机足够了。外设清单如下模块器件接口关键点燃气检测MQ-2ADC1_IN0需 5V 加热模拟量分压后进 ADC温湿度DHT11单总线上拉 4.7k读间隔≥1s人体感应HC-SR501EXTI输出 3.3V 兼容延时电位器可调门窗状态干簧管EXTI常闭型上拉 10k显示SSD1306 OLEDI2C1地址 0x3C上拉 4.7k报警输出有源蜂鸣器 LEDGPIO蜂鸣器经三极管驱动执行器5V 继电器GPIO三极管 续流二极管远程ESP-01SUART2AT 指令峰值电流 300mA选型时还有个容易忽略的点MQ-2 的加热丝功耗约 800mW5V 下电流 150mA 左右如果整个系统用 USB 供电加上继电器吸合和 ESP-01S 发射瞬间的电流总峰值可能逼近 500mA普通 USB 口会拉低电压导致 MCU 复位。所以电源部分必须留足余量这一点后面原理图章节会具体算。1.3 分层软件架构的设计思路固件我按四层来组织底层驱动HAL 自写 sensor 驱动、中间件滤波、环形缓冲区、CRC、应用逻辑状态机、阈值判定、任务调度裸机时间片。这么分层的好处是换个 MCU 或者把 MQ-2 换成 MQ-5只需要动底层应用逻辑一行不改。很多开源项目把 ADC 读取和报警判断全写在一个 while 里别人想复用只能整段抄可维护性很差。任务调度没有用操作系统而是用一个 1ms 的 SysTick 软定时器打节拍各任务按自己的周期跑主循环里判断标志位。这套方案的实时性对报警类应用绰绰有余而且中断里绝对不做耗时操作PIR 中断只置一个标志位真正处理放到主循环。这是裸机多任务里最基本也最重要的纪律违反它迟早出玄学 bug。2. 原理图逐模块拆解与参数计算原理图用立创 EDA 画的整体不复杂但每个外围电路的参数都不是随手填的。这一章把关键模块的计算过程摊开讲你照着算一遍就明白为什么是这些值。2.1 最小系统与电源树设计STM32F103C8T6 最小系统包含8MHz 主晶振配两个 20pF 负载电容具体值按晶振规格书12pF 负载电容的晶振配两颗 15~18pF 更准32.768kHz 晶振给 RTC 备用复位电路 10k 上拉 100nF 到地BOOT0 下拉 10kSWD 四线引出。这里有个新手常踩的坑晶振离 MCU 引脚越近越好走线不要超过 10mm负载电容的地要就近打过孔汇到主地否则起振不稳表现为偶尔上电不工作或者 RTC 走时不准。电源树分三级。第一级 12V 输入适配器或电池组经自恢复保险丝 0.5A 保护再经 MP1584 降压到 5V。MP1584 的反馈电阻按下式算Vout 0.8 × (1 R1/R2)要 5V 就取 R1 52.3kR2 10k实际输出 4.98V完全够用。第二级 5V 经 AMS1117-3.3 得到 3.3V输入端 100uF 电解 100nF 陶瓷输出端同样配置。这里要特别注意AMS1117 压差约 1.1V5V 转 3.3V 没问题但如果想直接从 5V 用 LDO 转 3.3V 且电流超过 500mA发热会很明显所以 5V 大电流负载继电器、蜂鸣器、MQ-2 加热、ESP-01S全部走 5V3.3V 只给 MCU、OLED、数字传感器这条边界要划清楚。ESP-01S 单独给一颗 470uF 电解电容就近挂在 3.3V 上因为它在发射瞬间电流从 60mA 跳到 300mA没有这颗电容电压会瞬间跌到 2.8V 以下直接导致模块重启。这个细节我在第一版板子上吃过亏模块隔三差五掉线查了两天才定位到是电源瞬态问题。2.2 燃气检测前端MQ-2 负载电阻怎么定MQ-2 内部是一个二氧化锡敏感膜电阻 Rs随可燃气体浓度升高而下降。电路上加热丝两端接 5V敏感电阻与负载电阻 RL 串联分压输出 Vo Vc × RL / (Rs RL)。Vc 是回路电压一般取 5V 或 3.3V。用 5V 灵敏度更好但 Vo 上限会超过 STM32 的 3.3V ADC 输入范围所以必须分压或者限幅。我的做法是RL 取 10kΩVo 出来后经一个 10k/10k 的电阻分压网络把 0~5V 压到 0~2.5V再进 PA0。分压电阻要并联一颗 100nF 到地做抗混叠滤波。理论上这样做会牺牲一半分辨率但 12 位 ADC 还剩 11 位有效没必要为此纠结。如果你对分辨率要求更高可以选 RL 3.3kΩ回路用 3.3V避免分压但低回路电压下 MQ-2 的输出曲线会整体下移浓度高时更容易逼近满量程各有取舍。关于 ADC 源阻抗分压网络等效输出阻抗是 10k∥10k 5kΩ这个值偏高STM32 的 ADC 采样保持电容充电会不充分导致读数偏低或跳变。解决办法是把采样时间设到 71.5 个 ADC 时钟周期以上配合前面那颗 100nF 电容做电荷缓冲。实测下来采样时间 239.5 周期最稳代价是转换慢一点但燃气监测这种 200ms 一次的应用完全无所谓。预热线路上MQ-2 通电后需要几分钟才稳定二十四小时老化后曲线才真正可用。代码里我做了开机 120 秒倒计时期间 OLED 提示预热中报警逻辑禁用避免刚上电就误报。2.3 执行器驱动与电气隔离继电器选 5V 线圈、触点容量 10A/250VAC 的型号用 S8050 NPN 三极管驱动。基极电阻计算MCU 输出 3.3VVbe 约 0.7V取基极电流 3mA则 Rb (3.3 - 0.7) / 3mA ≈ 860Ω选 1kΩ 标称值。S8050 的 hFE 按 100 算可驱动 300mA 集电极电流继电器线圈 5V 下约 70~90mA余量充足。线圈两端必须反并联一颗 1N4148 续流二极管阴极接 5V阳极接三极管集电极。没有这颗二极管线圈断电瞬间的反向电动势能到上百伏三极管击穿不说还会通过共地干扰 MCU表现为继电器一动作单片机就复位。更稳妥的做法是在 MCU 和三极管之间加 PC817 光耦把控制地和负载地分开走最后单点汇到电源地。我第一版没加光耦实测发现排风扇启动瞬间 MCU 的 ADC 读数会跳 30 个码值加了光耦并让大电流走线单独绕开后跳动降到 5 个码值以内。所以如果项目里还接电机、水泵这类感性负载光耦隔离建议直接上省得后面反复调试。蜂鸣器分有源无源。有源蜂鸣器给高电平就响简单无源的需要 PWM 驱动我用 TIM3_CH1 输出 4kHz、50% 占空比的方波响度最大。驱动同样用 S8050串联 100Ω 限流。需要不同音调报警时无源蜂鸣器才是正解改一下定时器重装载值就换音调。2.4 人机交互与通信接口OLED 用 SSD1306 0.96 寸I2C 地址 0x3CSDA/SCL 各上拉 4.7kΩ 到 3.3V。上拉电阻不要省很多模块板载了但便宜模块没有缺了就是黑屏。DHT11 数据线同样上拉 4.7k主机拉低至少 18ms 触发然后切输入等响应时序对要精确到微秒代码里用 DWT 做微秒延时比空循环稳。PIR 模块 HC-SR501 工作电压 4.5~20V输出高电平 3.3V可以直接接 EXTI。它板上两个电位器分别调灵敏度和延时延时可调 0.3~200 秒我一般拧到最小延时配合代码里做二次确认。门磁用常闭干簧管配 10k 上拉到 3.3V门关时引脚低电平门开时高电平触发上升沿中断。远程通信走 ESP-01S接 UART2PA2/PA3波特率 115200。AT 指令流程是AT 测试、ATCWMODE1 设 STA、ATCWJAP 连热点、ATCIPSTART 建 TCP、ATCIPSEND 发数据。整个流程我封装成了非阻塞状态机每步设 5 秒超时超时重试三次后复位模块。这里的关键是串口接收要用 DMA 空闲中断因为 ESP 返回的字符串长度不定靠单字节中断拼包容易丢。3. 固件工程结构与核心代码实现代码基于 STM32CubeMX 生成的 HAL 库工程改没上 RTOS。下面挑几个核心模块讲实现思路和踩过的坑。3.1 目录结构与驱动移植层工程目录这样组织SmartGuard/ ├── Core/ // CubeMX 生成main.c 等 ├── Drivers/ // HAL 库 ├── Hardware/ // 自写驱动mq2.c/h, dht11.c/h, oled.c/h, relay.c/h ├── Middleware/ // filter.c/h, ringbuf.c/h, crc16.c/h ├── App/ // app_alarm.c/h, app_report.c/h, app_sched.c/h └── Docs/ // 原理图、仿真工程、标定记录所有硬件驱动对外只暴露 init、read、write 三个接口内部细节不外漏。比如 mq2.c 里封装了 ADC 采样、滤波、电压转浓度应用层调用 MQ2_GetPPM() 就行不用关心底层是轮询还是 DMA。这个边界划清楚以后我把 ADC 从轮询改成 DMA 时应用层一个字没动。3.2 ADC 多通道采集与数字滤波ADC1 用扫描模式规则组放三个通道IN0MQ-2、IN1电池电压、IN2备用DMA 循环搬运到数组。这样一次启动后数据自动刷新主循环直接读数组不用等转换。MQ-2 读数滤波我用了三级组合先去极值再滑动平均最后做一阶低通。去极值是因为燃气浓度在临界点附近本身抖动大单次采样可能偶然蹦出一个极端值滑动平均窗口取 10一阶低通系数 α 取 0.2公式y[n] 0.2 * x[n] 0.8 * y[n-1]。三级下来读数非常平滑代价是响应延迟约 1 秒对燃气报警这个场景可以接受因为报警阈值本身留了余量不追求毫秒级响应。// Middleware/filter.c #define FILTER_WIN 10 static uint16_t buf[FILTER_WIN]; static uint8_t idx 0; static float lowpass 0.0f; uint16_t Filter_MQ2(uint16_t raw) { // 1. 滑动窗口 buf[idx] raw; if (idx FILTER_WIN) idx 0; uint16_t max buf[0], min buf[0]; uint32_t sum 0; for (uint8_t i 0; i FILTER_WIN; i) { if (buf[i] max) max buf[i]; if (buf[i] min) min buf[i]; sum buf[i]; } // 去掉一个最大一个最小 uint16_t avg (sum - max - min) / (FILTER_WIN - 2); // 2. 一阶低通 lowpass 0.2f * avg 0.8f * lowpass; return (uint16_t)lowpass; }浓度换算上MQ-2 的 Rs 计算方式先由 ADC 值反推 Vo再由 Vo 反推 Rs。因为中间有 1/2 分压所以实际 Vo Vadc × 2。Rs (Vc - Vo) / Vo × RL。拿到 Rs 后除以前面标定出的洁净空气阻值 R0得到 Rs/R0再代入经验公式算近似浓度。要强调的是这个浓度是近似值不是计量级的报警应用够用别拿它当仪器。float MQ2_CalcRs(uint16_t adc) { float vadc adc / 4095.0f * 3.3f; // ADC 引脚电压 float vo vadc * 2.0f; // 还原分压前电压 if (vo 5.0f) vo 4.99f; // 防止除零 return (5.0f - vo) / vo * 10.0f; // RL 10k } float MQ2_GetRatio(uint16_t adc) { float rs MQ2_CalcRs(adc); return rs / g_R0; // g_R0 为标定值 }3.3 阈值判定与报警状态机状态机四个状态用枚举表示每个状态下的动作、输出、上报都在一个表里驱动。这样加新状态或者调整某一档行为只改表数据不改逻辑控制流。状态触发条件蜂鸣器继电器OLED上报正常浓度 阈值1静音断开正常显示周期心跳预警阈值1 ≤ 浓度 阈值2短促间歇断开黄底提示预警帧报警浓度 ≥ 阈值2 或 入侵长鸣吸合排风红底闪烁报警帧故障传感器异常/超量程双响断开故障码故障帧阈值我设了两档阈值1 对应约 1000ppm 报警下限阈值2 对应 2000ppm。这两个值不是拍脑袋定的是参考了家用可燃气体报警器的常见设定同时结合自己标定出的曲线做了微调。不同批次的 MQ-2 一致性一般所以每块板子出厂前都要跑一次洁净空气标定把 R0 写进 Flash代码里读出来用。报警状态加了一个冷静期机制报警触发后即使浓度回落也必须等 30 秒才允许解除并且消警按钮要长按 3 秒。这不是多此一举实际使用时燃气浓度在危险区边缘波动没有冷静期状态机会疯狂切换蜂鸣器滴滴答答更让人烦躁。typedef enum { ST_NORMAL, ST_WARN, ST_ALARM, ST_FAULT } sys_state_t; void App_Alarm_Tick(void) { uint16_t ppm MQ2_GetPPM(); sys_state_t next ST_NORMAL; if (g_sensor_fault) next ST_FAULT; else if (ppm PPM_ALARM) next ST_ALARM; else if (ppm PPM_WARN) next ST_WARN; // 冷静期已报警状态下不允许立即降级 if (g_state ST_ALARM next ! ST_ALARM) { if (HAL_GetTick() - g_alarm_tick 30000) next ST_ALARM; } if (next ! g_state) { App_EnterState(next); g_state next; } }3.4 串口协议与上位机通信上报帧格式自定义帧头 0xAA 0x55 长度 命令字 数据 CRC16 低字节 CRC16 高字节。CRC16 用 Modbus 多项式 0xA001代码不到二十行。为什么要加 CRC因为串口在长线或者电源干扰大的环境里会误码没有校验的帧被上位机当真可能触发误报警。uint16_t CRC16_Modbus(const uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ buf[i]; for (uint8_t j 0; j 8; j) { if (crc 1) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }接收侧用 DMA 空闲中断。串口 2 的 DMA 循环模式收数据到 256 字节缓冲空闲中断触发后判断缓冲区里是否有完整帧有就解析。这套组合比单字节中断效率高太多也不会在高波特率下丢字节。上位机这边我用 Python 写了个简单的解析脚本pyserial 读串口按帧解析后画浓度曲线调试阶段非常直观。4. 仿真验证从 Proteus 到实物的落差不是每个人都有条件立刻打板仿真就是拿来先验证逻辑和电路的。这个项目我在 Proteus 8.13 里搭了一套能跑通主流程但要清楚它和实物的差距在哪。4.1 Proteus 仿真工程搭建Proteus 里需要搜到的元件STM32F103C8、MQ-2用 POT-HG 电位器模拟更可控、SSD1306有的版本没模型可以直接省掉 OLED 或者用虚拟终端替代、BUZZER、RELAY-SPDT、LED、RES、BUTTON。MCU 属性里把 Program File 指向 Keil 编译出的 .hexCrystal Frequency 填 8MHz。燃气浓度的模拟我用两种方式都试过直接放 MQ-2 模型或者用滑动变阻器分压后接 ADC 引脚。推荐后者因为能手动调模拟浓度测试报警阈值非常方便。继电器仿真可以直接用 RELAY-SPDT 加一个虚拟灯泡做负载联动效果一眼可见。串口输出接 COMPIM 或者虚拟终端能看到打印的帧内容。4.2 仿真发散的成因与解决仿真跑起来最常见的两个问题是仿真发散和时间不走。发散多半出现在模拟部分比如 ADC 引脚没有限流、运放模型参数不合理、电源节点浮空。解决办法是检查所有模拟节点是否有确定直流通路ADC 输入串个 1k 限流电阻电源正负之间放个 1M 泄放电阻。另一个高频原因是 DC 分析初始条件不收敛可以在仿真设置里把使用初始条件关掉或者给关键节点加 .IC 设定值。时间不走表现为程序像卡死实际上是 Proteus 里晶振频率没设对或者电源引脚没接到 VDD/VSS。STM32 模型必须在属性里显式把 VDD 和 VSS 引脚连到电源网络否则软件层面看程序在跑硬件引脚全是悬空。另外注意 Proteus 对 HAL 的 SysTick 中断响应比较慢主循环跑得比实物慢很多调试时不要误判成死循环。4.3 仿真能验什么、不能验什么仿真最擅长验证逻辑流状态机切换、阈值判定、串口协议、继电器联动、OLED 显示。这些和实物几乎一致能省下大量改一遍烧一遍的时间。它不太靠谱的是模拟精度和时序MQ-2 的加热动力学、ADC 真实噪声、继电器线圈反电动势、ESP-01S 的 AT 交互这些在仿真里要么没有要么过于理想。我一般只用仿真跑通逻辑然后实物上再标定模拟部分。关于无线通信模块的仿真Proteus 没有 ESP-01S 的完整模型只能拿串口终端模拟收发验证协议解析逻辑真正的联网行为只能实物验证。这一点心里要清楚别指望仿真一把过就万事大吉。5. 常见问题排查速查与踩坑心得下面这些都是我在做实体板子和给别人排障时反复遇到的整理成速查表遇到问题先对着查一遍能省很多时间。5.1 调试期高频故障速查现象可能原因排查/解决MCU 上电不跑晶振不起振、BOOT0 未下拉示波器看晶振波形检查负载电容ADC 读数一直不变通道配置错、未校准、DMA 未启动单通道轮询先验证再上 DMAADC 读数大幅跳变源阻抗过高、无滤波电容加 100nF采样时间设 239.5 周期OLED 黑屏上拉缺失、地址错扫描 I2C 地址确认 0x3C继电器动作时 MCU 复位无续流二极管、地线干扰加 1N4148大电流走线独立串口乱码波特率/时钟不对核对 HSE 频率和分频值蜂鸣器不响有源无源搞混、三极管方向错直连 3.3V 测试蜂鸣器本身ESP-01S 频繁掉线供电瞬态跌落就近加 470uF独立 LDO5.2 稳定性与长期运行的设计细节报警类设备对稳定性要求比一般玩具项目高因为它是长期通电待机的。几个设计要点值得单独说。一是 IWDG 看门狗必须开主循环里定时喂狗一旦死机自动复位复位标志记进备份寄存器下次启动上报异常。二是传感器自检ADC 读到接近 0 或者接近满量程都判定为故障因为正常工作时 MQ-2 的 Rs 分压不会落在极端区。三是 Flash 存储要节制R0 标定值、阈值配置写进 Flash 时不要循环写虽然 F103 的擦写寿命有一万次但架不住逻辑写错天天擦。低功耗方面如果做电池版本可以在待机时把 MQ-2 加热丝断电每隔一段时间通电预热再采样这样功耗能降一大截代价是响应变慢。家用插电版本不需要考虑这些直接常供电最省心。5.3 标定与量产时的经验MQ-2 这个传感器个体差异不小同一批里 Rs 在洁净空气中的值能差 30% 以上。所以如果你想让报警阈值有意义就必须一块一块标定。我的流程是把板子放在通风洁净环境里预热 24 小时后连续采 100 次取平均算出 R0 写入 Flash标定值同时记录进标签贴到板子上。这样可以做到报警浓度基本一致。批量做的时候另一个容易被忽略的是共地处理。多块板子共用一个电源系统时地线上会有回流电流信号地和大电流地要单点汇接必要时加磁珠。我见过有人把排风扇和主控共用一个 5V 且共地混乱结果风扇一起采集全乱最后加了隔离电源才解决。这类问题在单板调试时未必出现装进整机系统里才暴露所以设计初期就要把地平面规划好。关于阈值调整我的建议是留一个设置接口不要在代码里写死。我用长按消警按钮进入设置模式OLED 显示当前阈值短按调节长按保存。这样现场人员不用重新烧录就能调整实际部署中这一点非常实用。整套东西从画原理图到实物调通大概花了我三个周末踩的坑基本都写在上面了。代码和原理图打包放在仓库里仿真工程也一并奉上欢迎拿去改。最后分享一个我在实际标定时的小习惯调试时从来不只烧一次就下结论每次改完阈值或滤波参数至少让系统连续跑一晚上第二天看日志再定因为很多偶发问题只有长时间运行才现形尤其电源干扰类的短时间跑根本看不出来。