STM32开源智能安防与燃气监测:原理图、固件、仿真调试全链路
2026/9/17 14:17:08 网站建设 项目流程

STM32 的项目我做过不少,但真正能把开源、代码、原理图、仿真三件套凑齐、还敢往自家厨房墙上装的,只有这套智能安防与燃气监测系统。它不是那种"点个灯、转个电机"的练手板,而是把燃气泄漏检测、人体入侵检测、门窗状态监测、声光报警、排风联动这几件事串成一条完整的闭环链路,从模拟前端一直做到报警决策和远程上报。对刚摸到 STM32 门槛的朋友来说,它是一个很好的"横向拉通"项目——你会被迫去搞明白 ADC 采样精度怎么保证、状态机怎么写才不打架、继电器和蜂鸣器为什么老干扰 MCU、仿真里跑得好好的东西为什么一上实物就翻车。

我在把它从零做到能稳定跑在家里的一年多里,前前后后改了四版原理图、重写了三轮固件,踩的坑比写的代码多。这篇就按我自己的复现路径来拆:先讲整体设计和选型逻辑,再抠原理图里的关键电路,接着是固件架构和核心代码,然后是仿真验证与"仿真发散"的排查,最后落到调试标定、常见问题速查,以及开源资料的整理和二次开发。内容偏实操,参数和取值我会尽量给出推算过程,你能直接抄,也能按自己的元器件改。适合的人群很明确:会点 C 语言、能看懂基本电路图、手上有一块 STM32 开发板和一支烙铁的嵌入式爱好者;纯小白也能看,基础概念我会用生活化的比方讲清楚。

1. 系统整体设计与方案选型思路

1.1 需求拆解:安防和燃气这两条主线到底要做到什么程度

很多人做这类项目,第一步就错了——直接打开 Keil 建工程,边写边想。我的习惯是先拿张纸把"这个盒子装在家里,一年 365 天不断电,它必须做到什么"列清楚,再反过来推硬件。安防这条线,核心需求其实只有三个:有人非法进入要能感知、感知到要能叫、叫了之后你不能误报到把邻居吵醒。燃气这条线更狠,它关系到人身安全,需求是:能测、测得准、测得久、坏了要能自己知道。

把需求翻译成技术指标,大致是这样一张表:

功能主线用户诉求技术指标对应方案
燃气监测泄漏能提前发现检测下限优于 300ppm,响应小于 30sMQ 系列气敏传感器 + 12 位 ADC
燃气监测不能天天假报警阈值带回差,温湿度漂移要补偿迟滞比较 + 基线自校准
燃气监测失效要能自检传感器断线/短路可识别采样值上下限判据
人体入侵有人在布防时进房探测距离 5m 以上热释电红外模块
门窗状态门被推开要知道开合状态实时上报干簧管门磁
报警输出现场能听见、能看见声压 85dB 以上有源蜂鸣器 + 高亮 LED
联动控制泄漏时自动排风断气1 路继电器输出三极管驱动 + 光耦隔离
数据留存能看历史、能远程看本地显示 + 串口上报OLED + 无线模块

这张表看着简单,但它决定了后面所有的硬件端口分配和软件任务划分。我见过太多人先画板子再想需求,结果发现 ADC 通道不够、继电器和传感器抢引脚,只能飞线。先定需求再定资源,这是省时间的第一步。

1.2 主控与外围器件选型的取舍逻辑

主控我最终落在 STM32F103C8T6 上。理由很实在:价格便宜、资料多到烂大街、72MHz 主频足够跑这套逻辑、12 位 ADC 有 10 个外部通道、两路 SPI、两路 I2C、三路 USART,资源余量够我以后往上加东西。有人会问为什么不选 F4 或者 G0,我的判断是:这个项目里没有任何需要浮点运算或者高速信号处理的环节,5000 行以内的 C 代码在 F103 上跑得轻轻松松,多花钱买性能属于浪费。如果你手上只有 F407 开发板,也能直接移植,无非是改一下时钟配置和 HAL 库的头文件。

传感器这块是重头戏。燃气检测常见的方案有半导体式、催化燃烧式、电化学式和红外式。我选半导体式(MQ 系列),核心原因有三个:一是便宜且容易买,几十块钱能买一整套模块;二是它输出的是模拟电压,能让我练习 ADC 采样、滤波、标定这一整条信号链,教学价值高;三是它对甲烷、液化气这类可燃气体有足够响应,家用场景够用。代价是它需要预热、会受温湿度影响、长期会有基线漂移。这些缺点不是不能解决,但要靠软件补偿和定期校准来兜,后面章节会详细讲。

人体感应我用了 HC-SR501 这类热释电模块,原因就一个字:省。它内部已经做了菲涅尔透镜、信号放大和比较判决,输出一个干净的数字电平,MCU 只要读引脚就行。代价是它输出电平跟供电电压绑定,用 5V 供电时输出接近 5V,直接怼到 STM32 的 3.3V 引脚上会有风险,必须处理。这个细节后面单独讲。

1.3 供电、隔离与安全冗余的设计取舍

供电是这个项目里最容易被忽视、却最容易翻车的部分。我的思路是把电源分成三条独立路径:12V 输入经过一级降压到 5V,供给继电器、蜂鸣器和大功率外设;5V 再经过 LDO 降到 3.3V,单独供给 MCU、ADC 参考和 OLED;气敏传感器的加热丝单独从 5V 取电,并且加了大电容和磁珠,避免它的脉冲电流污染 MCU 的电源。

为什么要这么分?因为你把万用表接在 MCU 的 VDD 上看波形就明白了——蜂鸣器一响,电源线上会叠加一大坨毛刺,ADC 采出来的值能跳几十个码值,燃气读数跟着乱飘,报警逻辑直接被带偏。分开供电之后,这个现象基本消失。

安全冗余方面我做了三件事。第一,继电器回路和 MCU 之间用光耦隔离,驱动三极管和续流二极管一个都不能省,否则继电器断电瞬间的反向电动势会把三极管打穿。第二,燃气报警一旦触发,排风扇和燃气电磁阀的联动动作是写死在中断里的最高优先级,不经过任何可以被打断的普通流程。第三,固件里加了独立看门狗,保证程序跑飞之后能在 1 秒内复位重来。

注意:这套系统属于个人自制的辅助提醒装置,可以用于日常观察和技术学习,但它不能替代任何通过正规渠道安装的专业报警设备,也不能作为家庭安全的唯一依赖。厨房这类场所的气源管理,始终要以专业产品和规范操作为准。

2. 硬件原理图的关键细节与实操要点

2.1 主控最小系统:晶振、复位和 BOOT 引脚怎么配

最小系统看着简单,但它是整个项目能不能稳定跑起来的基石。外部高速晶振我用 8MHz 无源晶振,配两个 22pF 负载电容。这个 22pF 不是随便填的,它要跟晶振规格书里的负载电容匹配,经验算法是 CL ≈ 2×(C1×C2)/(C1+C2) + Cstray,其中 Cstray 一般取 3 到 5pF。如果晶振起振困难或者频率偏得厉害,先查这两个电容,再查板子走线是不是太长。晶振到 MCU 的走线要尽量短、尽量对称,底下不要走其他信号线,最好铺一层完整地。

复位电路我用的是 10k 上拉加 100nF 电容的标准组合,再并一个手动复位按键。有人喜欢省掉那个 100nF,我建议不要省,它对上电瞬间的电源抖动有抑制作用,尤其是你板子上有大电容和大电感的时候。

BOOT0 必须用一个 10k 电阻下拉到地,BOOT1(也就是 PB2)也是。这两个脚悬空是新手最容易犯的错,表现是程序烧进去不跑、或者偶尔能跑偶尔不跑。我在原理图上直接把 BOOT0 做成"下拉电阻 + 跳线帽"的形式,需要进串口下载模式的时候手动短接到 3.3V,平时保持下拉,省心。

2.2 燃气传感器的模拟前端与 ADC 采样链路

这是整个项目里最需要讲清楚的一段。MQ 系列气敏传感器本质上是一个随气体浓度变化的可变电阻,模块内部把它和一个负载电阻 RL 串起来,从中间取分压输出。所以输出电压和浓度不是线性关系,而是接近对数关系,这也是为什么不能简单地"电压超过多少就报警"——你要么做分段查表,要么在线性化之后再做判断。

我的做法是:模块 AO 输出先经过一个 RC 低通滤波(1k 电阻 + 100nF 电容,截止频率约 1.6kHz),再进 STM32 的 ADC 通道。这个 RC 不是摆设,传感器的输出本身噪声就不小,加上线缆耦合的空间干扰,不滤波的话 ADC 读数的抖动能达到 ±30 个码值,滤波之后能压到 ±5 以内。

关于 ADC 精度,这里要给个具体推算。STM32F103 的 ADC 是 12 位,参考电压 3.3V,那么理论分辨率是 3.3V / 4096 ≈ 0.806mV。看起来挺细,但实际有效位数(ENOB)受噪声、参考电压稳定性、采样时间影响,实测下来通常在 10 到 11 位之间。也就是说,你实际能分辨的最小电压变化大约是 1.6 到 3.2mV。这个数字决定了你的阈值区间不能设得太窄,否则读数在阈值附近来回跳,报警状态就会反复切换。

还有个容易被忽略的点:ADC 输入阻抗和采样时间的关系。STM32 的 ADC 采样保持电容大概 8pF,如果外部源阻抗太高(比如你直接用 100k 的分压电阻),采样时间不够的话电容充不满,读数会系统性偏低。我的经验是外部源阻抗尽量控制在 10k 以内,采样时间设置成 55.5 个周期或者更长,这样最保险。原理图里我在 ADC 输入脚前面加了一个 100Ω 的串联电阻,配合前面的 RC 一起,既限制了源阻抗又不影响带宽。

2.3 人体红外、门磁、蜂鸣器和继电器驱动电路

HC-SR501 的电平问题前面提了一句,这里展开说。这个模块的工作电压是 4.5V 到 20V,输出高电平理论上接近供电电压。如果你给它 5V,输出就是 5V 左右,而 STM32 的 IO 绝对最大额定值是 VDD+0.3V,也就是 3.6V,长时间这么接会把引脚内部的保护二极管一直导通,轻则漏电、重则损坏。我的处理方式有两种,你可以按手头器件选:一是用两个电阻搭个分压,比如 10k 和 20k,输出 5V 时得到约 3.3V;二是直接用光耦或者专用电平转换芯片。分压方案便宜,但要注意分压电阻会影响信号边沿,如果传感器输出变化不快(人体感应本来就是秒级),完全够用。

门磁用的是干簧管,一端接 3.3V,另一端接 MCU 引脚,同时用外部 10k 电阻下拉到地,并在引脚附近对地并一个 100nF 电容做硬件消抖。为什么要硬件消抖?因为干簧管在磁铁靠近的临界位置会抖动几十毫秒,如果软件消抖做得不够,一次关门能被识别成三次。硬件消抖加上软件 20ms 的确认,就非常稳了。

蜂鸣器分有源和无源两种,很多人搞混。有源蜂鸣器内部自带振荡电路,给它通电就响,频率固定,用 GPIO 直接驱动(经过三极管)就行;无源蜂鸣器需要外部给一定频率的方波才会响,用定时器输出 PWM 驱动,好处是能调频率、能发出不同的报警音。我选的是无源蜂鸣器配 PWM,因为燃气报警和入侵报警用不同的音调,用户不用看屏幕就能分辨出是哪一类事件。

继电器驱动就是最经典的三极管电路:MCU 引脚经过 1k 基极电阻驱动 S8050,集电极接继电器线圈,线圈两端反向并一个 1N4148 做续流。这里有两个细节。第一,续流二极管的方向绝对不能接反,接反了继电器根本不吸合,还容易烧三极管。第二,继电器线圈的电源要和 MCU 的电源分开走线,回到电源入口处再汇合,避免线圈电流在 MCU 的地平面上产生压降。我第一版板子没注意这个,结果继电器一吸合,OLED 就闪一下,后来在继电器电源脚旁边加了一个 100μF 电解电容和 0.1μF 陶瓷电容的并联组合,问题解决。

2.4 电源树设计和 PCB 布局的几条硬规矩

把前面说的画成电源树,大概是:12V 适配器输入 → 防反接二极管 / 自恢复保险丝 → 一级 DC-DC 降压到 5V → 分三路,一路给继电器和蜂鸣器,一路给传感器加热丝(加磁珠隔离),一路给 LDO 输入 → LDO 输出 3.3V → MCU、ADC 参考、OLED。

如果你要做便携版本,想用锂电池供电,那就涉及升压和充电管理,市面上有一类集成度很高的移动电源管理方案,能把充电、升压、电量显示都整合在一颗芯片里,配一套官方参考设计就能用。但我要提醒一句:锂电池的充放电管理涉及安全,一定要用成品模块或者经过验证的方案,不要自己裸搭充电电路,尤其是厨房这种高温高湿环境,宁可多花几十块钱用带保护板的成品电源板。

PCB 布局上我的几条硬规矩:模拟部分和数字部分分区布置,ADC 输入走线远离 PWM 和继电器驱动走线;地平面尽量完整,模拟地在靠近 ADC 入口处单点接到数字地;去耦电容紧贴每个电源引脚,0.1μF 和 10μF 成对出现;晶振周围做包地处理。这些规矩听着老生常谈,但真正做到之后,ADC 的噪声表现会有肉眼可见的变化。我实测过同一块板子改布局前后,ADC 的峰峰噪声从 40 个码值降到了 6 个码值,这个差距直接决定了你的报警阈值能不能设得精细。

提示:原理图阶段就把测试点留出来。我习惯在 5V、3.3V、传感器 AO、ADC 输入这几个位置各放一个测试焊盘,调试的时候万用表和示波器探针一夹就上,比到处找飞线点省太多时间。

3. 固件架构与核心代码实现

3.1 工程结构和初始化顺序

这套固件我没有用 RTOS,理由是任务数量少、实时性要求不高,裸机加状态机完全够用,而且调试更直观。工程目录我分成了这么几层:Core放主循环和中断入口,Drivers放 HAL 库和底层外设封装,BSP放板级支持(OLED、传感器、继电器这些具体器件),App放业务逻辑(状态机、报警判定、数据上报),Utils放滤波、CRC、环形缓冲这类通用工具。分层的好处是以后换传感器只需要改 BSP,App 层几乎不用动。

初始化顺序也很讲究。我的顺序是:HAL 库初始化 → 系统时钟配置 → GPIO 初始化(先把继电器和蜂鸣器设成安全的关闭状态)→ 看门狗启动 → ADC + DMA 初始化 → 定时器初始化 → 串口和 OLED 初始化 → 最后才打开中断。把继电器初始化放在最前面是因为上电瞬间 IO 状态不确定,如果继电器引脚恰好被拉高,燃气阀可能在开机时就误动作。把看门狗放在外设初始化之前,是为了防止某个外设初始化卡死导致系统一直挂在那里不重启。

3.2 ADC 多通道采样与数字滤波的完整实现

采样我用的是定时器触发 + DMA 循环搬运的方式,这样 CPU 完全不用管采样过程,数据自动填进缓冲区。下面是核心代码结构,可以直接套用:

#define ADC_CH_NUM 4 #define SAMPLE_DEPTH 32 #define ADC_BUF_SIZE (ADC_CH_NUM * SAMPLE_DEPTH) static uint16_t adc_dma_buf[ADC_BUF_SIZE]; void ADC_DMA_Init(void) { // 1. 校准ADC,这一步不能省,否则读数有系统偏差 HAL_ADCEx_Calibration_Start(&hadc1); // 2. 启动DMA循环搬运,缓冲区满一轮自动从头开始 HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_dma_buf, ADC_BUF_SIZE); } // 从DMA缓冲区里取出某个通道最近N次采样,做中值+滑动平均 uint16_t ADC_GetFiltered(uint8_t ch) { uint16_t tmp[SAMPLE_DEPTH]; uint32_t sum = 0; uint8_t i, j; for (i = 0; i < SAMPLE_DEPTH; i++) { tmp[i] = adc_dma_buf[i * ADC_CH_NUM + ch]; } // 冒泡排序,取中间值,先把脉冲干扰干掉 for (i = 0; i < SAMPLE_DEPTH - 1; i++) { for (j = 0; j < SAMPLE_DEPTH - 1 - i; j++) { if (tmp[j] > tmp[j + 1]) { uint16_t t = tmp[j]; tmp[j] = tmp[j + 1]; tmp[j + 1] = t; } } } // 去掉两端各1/4的极值,剩下的求平均 uint8_t start = SAMPLE_DEPTH / 4; uint8_t end = SAMPLE_DEPTH - start; for (i = start; i < end; i++) { sum += tmp[i]; } return (uint16_t)(sum / (end - start)); }

为什么是"中值 + 去极值平均"这种组合,而不是单纯的滑动平均或者卡尔曼?两个原因。第一,气敏传感器最怕的是突发脉冲干扰,比如继电器动作的瞬间,滑动平均会被这种孤立大值拖偏,而排序取中值天生免疫单点异常。第二,卡尔曼滤波需要建立噪声模型,参数调起来很玄学,我试过,调参的时间够我把中值滤波写三遍了,效果差别在这个应用里看不出来。实用主义优先。

这里有个性能细节要注意:32 个数的冒泡排序在 72MHz 的 F103 上大概要几百微秒,如果你在 1ms 的定时中断里调用,CPU 占用率会明显上升。我的做法是把这个滤波过程放在主循环里,以 100ms 为周期执行一次,中断里只负责采样和标志位,这样 CPU 负载控制在 5% 以内。

3.3 报警状态机的设计与防抖动处理

状态机是这个项目的大脑。我把它设计成七个状态,用一张 switch-case 表来实现,每个状态的迁移条件都写得非常明确,绝不允许多个条件同时成立导致行为不确定。

状态含义进入条件退出条件
INIT上电初始化复位初始化完成
WARMUP传感器预热初始化完成预热计时结束
DISARMED撤防预热完成或用户撤防用户布防
ARMING布防延时用户布防延时结束或用户撤防
ARMED警戒布防延时结束触发报警或用户撤防
ALARM报警中检测到入侵或燃气超标用户消音且条件解除
FAULT故障传感器断线或自检失败故障恢复

燃气报警和安防报警共用一个 ALARM 状态,但内部用标志位区分报警源,因为它们的联动动作不一样:燃气报警要开排风扇、关燃气阀;安防报警只响蜂鸣器和闪灯。把联动动作分开处理,能避免燃气泄漏时只是"响一声"而没有实际处置。

防抖动的核心是迟滞比较。假设燃气报警阈值设在 1200mV,解除阈值设在 800mV,那么只有当采样值连续 3 次超过 1200mV 才进入报警,只有连续 5 次低于 800mV 才解除报警。为什么要这么设计?因为如果进出都用同一个阈值,当浓度刚好卡在阈值附近时,继电器会以每秒好几次的频率"啪啪啪"地开关,既吵人又伤触点,我第一版就是这么翻车的,邻居以为我家在装修。

// 迟滞比较 + 连续确认,response 是要连续满足的次数 static uint8_t above_cnt = 0, below_cnt = 0; #define GAS_ALARM_MV 1200 #define GAS_CLEAR_MV 800 #define CONFIRM_ALARM 3 #define CONFIRM_CLEAR 5 uint8_t Gas_Judge(uint16_t mv) { static uint8_t gas_alarm = 0; if (mv >= GAS_ALARM_MV) { above_cnt++; below_cnt = 0; if (above_cnt >= CONFIRM_ALARM) gas_alarm = 1; } else if (mv <= GAS_CLEAR_MV) { below_cnt++; above_cnt = 0; if (below_cnt >= CONFIRM_CLEAR) gas_alarm = 0; } else { // 处在迟滞带内,计数清零但不改变状态,这就是"记忆"效果 above_cnt = 0; below_cnt = 0; } return gas_alarm; }

3.4 人机界面与数据上报的抽象层设计

OLED 我用的是 0.96 寸 I2C 屏,显存 128×64。I2C 引脚必须外接 4.7k 上拉电阻到 3.3V,有些模块自带上拉,有些没有,买之前一定要看清。刷新策略上我没有用全屏刷新,而是划分成三个固定区域:状态栏(当前布防状态、WiFi 连接情况)、数值区(燃气浓度、传感器原始电压)、事件区(最近一次报警时间和类型)。只刷新变化的区域,能把刷新时间从 30ms 降到 5ms,主循环跑起来更流畅。

串口上报我封装了一个Report_Send()接口,内部把数据打包成固定格式的字符串,具体是走无线模块还是走有线串口,由编译宏切换。这样做的好处是业务层完全不关心物理链路是什么。我的上报格式是$SEC,ARM=1,GAS=0456,DOOR=0,ALM=0#这种自定义的简洁格式,解析起来比 JSON 快,也不占带宽。如果你要接云平台,在中间加一层转换就行。

关于无线模块,有一点必须提醒:这类模块在发射瞬间的峰值电流能到 200mA 以上,而 STM32 的 3.3V LDO 如果只按 100mA 选型,模块一发数据电源就被拉垮,MCU 跟着复位。这个现象的典型表现是"每隔几十秒重启一次",很容易被误判成软件问题。解决方式是在无线模块的电源脚旁边并一个 470μF 以上的电容,或者干脆给模块单独配一路 LDO。

3.5 看门狗与异常恢复机制

独立看门狗(IWDG)我配的是 1 秒超时,喂狗动作放在主循环的最后一步。这样设计的关键在于:如果任何一个任务卡住导致主循环走不完一圈,狗就会咬。喂狗绝对不能放在定时中断里,那等于把狗拴在了不会停的地方,完全失去意义。

除了看门狗,我还做了几层恢复机制。第一层是通信超时重试,无线模块连不上就在后台静默重连,不影响本地报警功能。第二层是传感器自检,每隔 10 分钟检查一次 ADC 原始值是否落在合理区间(比如 50mV 到 3200mV 之间),超出范围就进入 FAULT 状态并在屏幕上显示"传感器异常",同时蜂鸣器发出特定节奏的提示音。第三层是配置参数的双备份,存在 Flash 的两个不同扇区,读取时做 CRC 校验,一个坏了就用另一个。

这里要说一个反直觉的经验:故障状态下,系统应该"降级运行"而不是"直接停机"。比如传感器坏了,安防功能仍然要正常工作,只是燃气监测能力暂时失效,并且必须明确告知用户。我在 FAULT 状态里就是这么做的,入侵检测保持在线,燃气报警逻辑挂起但持续提示异常。这个设计思路来自我对实际使用的理解——设备最怕的不是功能少,而是坏了你却不知道。

4. 仿真验证:从功能验证到"仿真发散"的排查

4.1 仿真平台的选择和它能做什么、不能做什么

这类项目能用的仿真工具大致分三类,各有各的用处,不要指望一个工具全包。第一类是电路级仿真,比如 LTspice、Multisim、TINA 这类 SPICE 系工具,适合验证模拟前端:RC 滤波的截止频率对不对、分压网络的静态工作点合不合理、运放跟随器的输入输出范围够不够。第二类是单片机联仿,比如 Proteus 配上 Keil 生成的 hex 文件,能让你在没打板子之前就跑通按键、显示、继电器逻辑,验证软件的流程是不是自洽。第三类是控制算法仿真,比如在 Simulink 里搭一个温度或浓度变化的模型,验证你的滤波和判定逻辑在时域上的表现。

它们做不到什么,这点更要说清楚。仿真给不了你真实的传感器特性曲线,给不了 PCB 上的噪声耦合,也给不了继电器动作时的电源跌落。所以我的态度是:仿真用来验证"逻辑对不对"和"电路算得准不准",实物用来验证"能不能稳定跑"。指望仿真过了一切就万事大吉,那必然要翻车。我做这套项目的时候,仿真只花了大概两天,实物调试花了将近三周,比例差不多就是这样。

4.2 用信号注入法验证报警逻辑

仿真里最有价值的操作是信号注入。在 Proteus 里,我没有真实的燃气传感器,就在 ADC 输入引脚上接一个可调电位器或者直接用信号源,手动改变电压值,观察状态机是否按预期迁移。具体要验证这几个场景:电压缓慢上升越过 1200mV 时,是否在连续 3 次确认后进入报警、继电器是否正确吸合;电压在 1000mV 附近小幅波动时,是否稳定保持原状态而不反复跳变;电压突然升到 3000mV(模拟传感器短路)时,是否进入 FAULT 而不是误报。

在 LTspice 里,我把模拟前端单独拎出来做了交流扫描,验证 RC 滤波的截止频率。理论上 1k + 100nF 的截止频率是 1/(2π×1000×100e-9) ≈ 1.59kHz,仿真出来是 1.58kHz,误差在可接受范围,说明我的计算和模型是一致的。这一步看着简单,但它能帮你确认元件取值,避免焊上去才发现带宽不够或者过度滤波导致响应变慢。

还有一点非常实用:仿真的时域响应能帮你估参数。比如我把滤波后的信号做了一个阶跃激励,观察到从 10% 到 90% 的上升时间大约是 40ms,这意味着我的采样周期取 10ms 是合理的,既能跟上变化又不至于过采样浪费 CPU。这种参数在实物上是很难单独测量的,因为有太多噪声干扰。

4.3 "仿真发散"到底是怎么产生的,怎么收敛

"仿真发散"这个词是我在查资料的时候经常看到的,很多人遇到仿真跑着跑着数值越来越大、最后直接报错或者输出无穷大。这块我踩过的坑不少,梳理一下原因和对策。

发散的本质是求解器在某个时间点无法找到满足所有节点方程的解,或者找到了一个数值上不稳定、误差被不断放大的解。常见原因有这么几类:

第一类是有节点没有直流通路。比如一个电容两端都悬空,或者某个节点只通过电容连接到其他部分。SPICE 在算直流工作点的时候不知道这个节点的电压该是多少,就会给出一个巨大的值或者报错。对策很简单,在关键节点到地之间加一个 1MΩ 到 10MΩ 的大电阻,给它一个确定的直流路径,对电路功能几乎没有影响。

第二类是步长设置不当。开关电源、继电器这类包含快速跳变的电路,如果最大步长设得太大,求解器会跳过跳变瞬间,导致误差累积。对策是在.options里限制最大步长,比如把TMAX设成开关周期的几十分之一,或者用.tran 0 10m 0 1u这样的写法明确指定最大步长。

第三类是模型本身有问题。理想开关、理想运放、没有寄生参数的理想电感,在数学上都是病态的,容易导致不收敛。对策是把理想器件换成带一点点寄生参数的现实模型,比如给电感加 1mΩ 的串联电阻,给开关加 0.1Ω 的导通电阻。这些看似微不足道的参数,正是让求解器"抓得住"的关键。

第四类是初始条件冲突。比如两个电容并联但初始电压设置矛盾,或者电感的初始电流和电路拓扑不匹配。对策是用.ic语句显式指定初始条件,或者先跑一次工作点分析(.op)再跑瞬态。

Proteus 这边的"发散"表现不太一样,它更多是仿真卡死或者逻辑不跑。常见原因有:MCU 的隐藏电源引脚没有接到 VDD/VSS 网络(这个坑我踩过,现象是单片机死活不工作);时钟频率设置的晶振值和 hex 文件里配置的不一致,导致定时器全乱;某个元件没有仿真模型,加载的时候直接报错;程序里有死循环阻塞了仿真调度。排查顺序建议是先看有没有报错信息,再检查电源网络,然后核对时钟频率,最后用点灯程序验证基本环境是不是通的。

注意:仿真里跑通的报警延时参数,搬到实物上一定要重新调。因为仿真里的浓度变化是理想阶跃,实物上气体扩散需要时间,房间大小、通风条件都会影响。我在仿真里设的 3 秒确认时间,实物上改成了 8 秒才合适。

5. 调试标定与常见问题速查实录

5.1 燃气传感器的标定流程和参数推算

MQ 系列传感器出厂时的一致性一般,同一批次的模块基线电压能差出 20% 以上,所以必须逐个标定,不能把别人的阈值直接拿来用。我的标定流程是这样的:

第一步,老化。新传感器要通电连续工作 24 到 48 小时,让加热丝稳定、敏感材料结构稳定。这期间读数是会缓慢变化的,不要着急调参数。很多人拿到模块就开始调,结果第二天发现全变了,白忙一场。

第二步,基线测定。把传感器放在洁净空气中(最好是室外或者通风良好的房间),预热至少 3 分钟之后记录稳定读数。以我手上这个模块为例,输出经过分压后进 ADC 的稳定值大约对应 380mV,ADC 码值约 470。这个值就是基线,记为 V0。

第三步,确定阈值。这里有两个思路。如果你有标准气体,可以直接测出对应浓度的输出电压,这叫绝对标定,最准但成本高。如果没有标准气体(大多数人都没有),就用相对法:把阈值设在基线的 2 到 3 倍。我的基线是 380mV,报警阈值设成 1200mV,大约是基线的 3.2 倍。这个取值不是拍脑袋的,我参考的是气敏传感器典型的灵敏度曲线,在这个浓度区间输出已经有明显变化,能有效区分"厨房正常炒菜油烟"和"真实泄漏"。解除阈值设成 800mV,是基线的 2.1 倍,留出了足够的迟滞带。

第四步,温湿度补偿。这个可选但很有用。我在板子上留了一个数字温湿度传感器的位置,采集到的温度和湿度用来对基线做修正。经验公式是基线随温度每升高 10℃ 大约漂移 3% 到 5%,方向上通常是温度升高基线下降。我在固件里做了一个简单的线性修正,实测下来能把夏季和冬季的误报率降低一半以上。如果你不想增加成本,也可以只用软件方案:每天凌晨厨房没人、通风良好时,自动采一次基线并更新,这就是所谓的自动基线校准,简单有效。

5.2 常见问题速查表

下面这张表是我这一年多攒下来的,基本覆盖了调试过程中最常遇到的症状。遇到问题先查这张表,能省掉大量抓耳挠腮的时间。

症状可能原因排查方法解决办法
程序烧进去不运行BOOT 引脚悬空或接错万用表测 BOOT0 电平BOOT0 加 10k 下拉
下载一次后就锁死关闭了调试引脚功能检查是否误配 SWD 引脚为普通 IO用串口 ISP 擦除后重配
ADC 读数一直为 0通道配置错或 DMA 没启动示波器测输入脚电压核对通道映射和 DMA 使能
ADC 读数乱跳电源噪声、滤波不足示波器看 VDD 纹波加去耦、改善 RC 滤波、分路供电
燃气读数缓慢上升不回落传感器预热不足或中毒观察 24 小时曲线延长预热、必要时更换传感器
人体感应误触发电平不匹配、探头朝向不当测输出电平是否为 5V加分压或电平转换,调整安装角度
继电器吸合导致复位电源被拉垮、地线干扰测复位瞬间 3.3V 波形分开供电、加大电容、优化地平面
蜂鸣器声音小或沙哑有源无源接错、驱动电流不足确认蜂鸣器类型换驱动方式,无源用 PWM
OLED 不显示上拉电阻缺失、地址不对用 I2C 扫描程序确认地址补上 4.7k 上拉,修正地址
无线模块频繁断连峰值电流不足测发射瞬间电源电压加大电容或单独配 LDO
看门狗频繁复位某个任务耗时过长在循环里打时间戳把耗时任务拆分或降到低频执行
仿真卡死不动隐藏电源未接、时钟配置不符检查网络标号和时钟频率接上 VDD/VSS,核对晶振值

5.3 几条踩过坑才明白的经验

第一,报警阈值宁高勿低。新手总怕漏报,把阈值调得很低,结果炒个辣椒就报警,家人两天就把设备拔了。一个被拔掉的报警器,防护效果是零。宁可灵敏度稍微保守一点,也要保证它长期在那个位置被信任。

第二,所有继电器动作前加延时。不要一检测到条件就立刻切换继电器,给它 200ms 的确认窗口。这个延时在电气上能避开电源瞬间跌落和干扰尖峰,在体验上也能避免继电器像打字机一样响个不停。

第三,屏幕和指示灯的信息要分层。正常状态下只显示关键信息(布防状态、燃气读数),异常状态下才弹出详细信息。我一开始把所有信息都堆在屏幕上,结果家人根本看不懂,现在简化为一个大字状态加一行小字,接受度高了很多。

第四,把调试口留出来。我在板子上留了四针的调试排针(3.3V、GND、SWDIO、SWCLK),并且固件里保留了一个"调试模式",进入后会通过串口输出详细的内部状态,包括每个通道的原始值、滤波值、当前状态机状态、计时器计数。这个模式在排查"偶发误报"这类难缠问题时价值极高,因为偶发问题靠肉眼观察根本抓不住。

第五,写日志到 Flash。每当发生报警、故障、状态切换,就把一条带时间戳的记录写进 Flash 的日志区。这里要注意 Flash 的擦写寿命,我采用环形缓冲的方式,写满之后从头部覆盖,并且只在重要事件时写,不要每秒都写。有了这个日志,你才能回过头分析"昨天晚上两点为什么响了一次"。

6. 开源资料的组织与二次开发建议

6.1 代码仓库的目录规范与文档习惯

开源一个项目,代码只是其中一部分,甚至不是最重要的部分。我做这套东西的时候,仓库结构是这样的:根目录放 README、LICENSE 和一个docs文件夹;firmware放完整工程,包含 Keil 工程文件和源码;hardware放原理图、PCB 源文件和导出的 PDF、Gerber;simulation放仿真工程文件和说明;docs里放元件清单、标定步骤、接线图。

README 我建议写清楚四件事:这个项目能干什么、需要买哪些元器件(附一张表,含型号、数量、大致价位)、怎么把代码烧进去(要具体到用哪个工具、哪个接口)、以及已知的限制和注意事项。很多人写 README 只写"这是一个基于 STM32 的智能安防系统",然后就没了,别人拿到手根本不知道怎么往下走。我在 README 里明确写了"本项目为个人学习用途,不构成安全设备的替代方案",这既是对别人负责,也避免了一些不必要的麻烦。

原理图导出 PDF 的时候要注意,如果用 AD 之类的工具,直接导出可能会出现只导出当前选中区域的情况。我的做法是先在原理图里确认没有选中任何对象,然后使用智能 PDF 导出功能,勾选"整个工程"或者逐页设置,导出之后再翻一遍每一页,确认元件标号和网络标号都能看清。如果图纸太大,导出时把分辨率调到 600dpi 以上,不然放大后字是糊的。Gerber 文件导出后一定要用 Gerber 查看器打开看一遍,确认每一层都有内容、钻孔文件和阻焊层没有错位。

6.2 元器件清单整理和替代方案

BOM 表我喜欢按功能分块排列,而不是按位号排序。这样别人一眼就能看出哪几个件是核心、哪几个是外围。核心器件(MCU、气敏传感器、热释电模块)我标注了必须匹配的型号,外围器件(电阻、电容、三极管)标注了参数范围,允许替换。

类别器件关键参数备注
主控STM32F103C8T672MHz, 64KB Flash可用同系列替代
燃气传感半导体气敏模块5V 供电, 模拟输出需预热和标定
人体感应热释电模块3~5V, 数字输出注意电平匹配
门磁干簧管常闭型配 10k 下拉
显示OLED 0.96 寸I2C 接口需 4.7k 上拉
提示无源蜂鸣器3.3V, PWM 驱动配三极管
执行5V 继电器模块触点容量 10A光耦隔离
电源DC-DC + LDO12V→5V→3.3V分路供电

替代方案我一般会给出两套。比如主控可以用 STM32F103 的其它封装,也可以用国产的兼容型号,把时钟配置和 HAL 库头文件改一下就行。传感器如果想升级精度,可以换成电化学式或者红外式,接口形式可能从模拟变成串口,此时只需要改 BSP 层的读取函数,App 层的判定逻辑基本不用动,这就是分层的价值。

6.3 二次开发的几个扩展方向

这套项目的架构留了不少扩展空间,我自己也在陆续加东西。第一个方向是多点位组网:把几个节点分布在不同房间,通过无线模块汇总到一个主节点,主节点做集中显示和联动。这个改造的关键是设计一个简洁的通信协议,我建议用固定长度的二进制帧而不是字符串,节省带宽也便于校验。

第二个方向是接入家居环境联动。比如燃气报警时不仅开排风扇,还可以联动智能插座切断气源附近的电器、推送消息到手机。这里要注意的是,联动动作必须有本地兜底,网络断了你不能连报警都不响,所以我的设计里所有联动都是"本地优先,远程为辅"。

第三个方向是把它改造成通用的环境监测节点。这套代码里的 ADC 采样、滤波、迟滞判定、日志记录这几个模块是通用的,把气敏传感器换成温湿度、空气质量、光照传感器,代码复用率能在 70% 以上。我甚至拿同一套框架做过一个小型的鱼缸监测装置,测水温、控制加热棒和补光灯,改动量比我想象的小得多。

第四个方向是固件在线升级。这个需要留出专门的 Bootloader 分区,实现起来有一定门槛,但一旦做通,后续修复 bug 就不用把设备拆下来重新烧录了。我的建议是:如果你打算把这个东西长期装在家里并且会不断改代码,那 IAP 功能值得投入时间去做,因为反复拆装设备的成本远高于写 Bootloader 的成本。

我在实际使用中体会到的一点是,这类嵌入式项目真正的门槛从来不在写代码,而在于把一整套东西从"能跑"打磨到"敢让它 24 小时无人值守地跑"。中间差的那段距离,全是靠对细节的反复推敲和大量真实场景的验证补起来的。如果你的板子第一次上电就能稳定工作一整天,那是运气;如果它连续报警了三次都是误报,那才是正常的开始,剩下的工作就是把这些误报一个一个找出来、干掉。

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

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

立即咨询