STM32F103C8T6燃气报警器:原理图、固件与Proteus仿真
2026/9/18 1:54:42 网站建设 项目流程

这个项目最早是我给老家厨房做的一台独立报警器,老人家记性差,烧水忘关火这种事一年总得出几回。市面上成品不少,但要么是单一探头加个蜂鸣器,联动和远程提醒都缺,要么就是整套品牌方案贵得离谱。后来索性拿手头一块 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 的加热丝功耗约 800mW,5V 下电流 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 下拉 10k,SWD 四线引出。这里有个新手常踩的坑:晶振离 MCU 引脚越近越好,走线不要超过 10mm,负载电容的地要就近打过孔汇到主地,否则起振不稳,表现为偶尔上电不工作或者 RTC 走时不准。

电源树分三级。第一级 12V 输入(适配器或电池组)经自恢复保险丝 0.5A 保护,再经 MP1584 降压到 5V。MP1584 的反馈电阻按下式算:Vout = 0.8 × (1 + R1/R2),要 5V 就取 R1 = 52.3k,R2 = 10k,实际输出 4.98V,完全够用。第二级 5V 经 AMS1117-3.3 得到 3.3V,输入端 100uF 电解 + 100nF 陶瓷,输出端同样配置。这里要特别注意:AMS1117 压差约 1.1V,5V 转 3.3V 没问题,但如果想直接从 5V 用 LDO 转 3.3V 且电流超过 500mA,发热会很明显,所以 5V 大电流负载(继电器、蜂鸣器、MQ-2 加热、ESP-01S)全部走 5V,3.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.3V,Vbe 约 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 地址 0x3C,SDA/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,接 UART2(PA2/PA3),波特率 115200。AT 指令流程是:AT 测试、AT+CWMODE=1 设 STA、AT+CWJAP 连热点、AT+CIPSTART 建 TCP、AT+CIPSEND 发数据。整个流程我封装成了非阻塞状态机,每步设 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 用扫描模式,规则组放三个通道:IN0(MQ-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 编译出的 .hex,Crystal 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 未启动单通道轮询先验证,再上 DMA
ADC 读数大幅跳变源阻抗过高、无滤波电容加 100nF,采样时间设 239.5 周期
OLED 黑屏上拉缺失、地址错扫描 I2C 地址,确认 0x3C
继电器动作时 MCU 复位无续流二极管、地线干扰加 1N4148,大电流走线独立
串口乱码波特率/时钟不对核对 HSE 频率和分频值
蜂鸣器不响有源无源搞混、三极管方向错直连 3.3V 测试蜂鸣器本身
ESP-01S 频繁掉线供电瞬态跌落就近加 470uF,独立 LDO

5.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 显示当前阈值,短按调节,长按保存。这样现场人员不用重新烧录就能调整,实际部署中这一点非常实用。

整套东西从画原理图到实物调通大概花了我三个周末,踩的坑基本都写在上面了。代码和原理图打包放在仓库里,仿真工程也一并奉上,欢迎拿去改。最后分享一个我在实际标定时的小习惯:调试时从来不只烧一次就下结论,每次改完阈值或滤波参数,至少让系统连续跑一晚上,第二天看日志再定,因为很多偶发问题只有长时间运行才现形,尤其电源干扰类的,短时间跑根本看不出来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询