STM32 的项目我做过不少,但真正能把开源、代码、原理图、仿真三件套凑齐、还敢往自家厨房墙上装的,只有这套智能安防与燃气监测系统。它不是那种"点个灯、转个电机"的练手板,而是把燃气泄漏检测、人体入侵检测、门窗状态监测、声光报警、排风联动这几件事串成一条完整的闭环链路,从模拟前端一直做到报警决策和远程上报。对刚摸到 STM32 门槛的朋友来说,它是一个很好的"横向拉通"项目——你会被迫去搞明白 ADC 采样精度怎么保证、状态机怎么写才不打架、继电器和蜂鸣器为什么老干扰 MCU、仿真里跑得好好的东西为什么一上实物就翻车。
我在把它从零做到能稳定跑在家里的一年多里,前前后后改了四版原理图、重写了三轮固件,踩的坑比写的代码多。这篇就按我自己的复现路径来拆:先讲整体设计和选型逻辑,再抠原理图里的关键电路,接着是固件架构和核心代码,然后是仿真验证与"仿真发散"的排查,最后落到调试标定、常见问题速查,以及开源资料的整理和二次开发。内容偏实操,参数和取值我会尽量给出推算过程,你能直接抄,也能按自己的元器件改。适合的人群很明确:会点 C 语言、能看懂基本电路图、手上有一块 STM32 开发板和一支烙铁的嵌入式爱好者;纯小白也能看,基础概念我会用生活化的比方讲清楚。
1. 系统整体设计与方案选型思路
1.1 需求拆解:安防和燃气这两条主线到底要做到什么程度
很多人做这类项目,第一步就错了——直接打开 Keil 建工程,边写边想。我的习惯是先拿张纸把"这个盒子装在家里,一年 365 天不断电,它必须做到什么"列清楚,再反过来推硬件。安防这条线,核心需求其实只有三个:有人非法进入要能感知、感知到要能叫、叫了之后你不能误报到把邻居吵醒。燃气这条线更狠,它关系到人身安全,需求是:能测、测得准、测得久、坏了要能自己知道。
把需求翻译成技术指标,大致是这样一张表:
| 功能主线 | 用户诉求 | 技术指标 | 对应方案 |
|---|---|---|---|
| 燃气监测 | 泄漏能提前发现 | 检测下限优于 300ppm,响应小于 30s | MQ 系列气敏传感器 + 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、气敏传感器、热释电模块)我标注了必须匹配的型号,外围器件(电阻、电容、三极管)标注了参数范围,允许替换。
| 类别 | 器件 | 关键参数 | 备注 |
|---|---|---|---|
| 主控 | STM32F103C8T6 | 72MHz, 64KB Flash | 可用同系列替代 |
| 燃气传感 | 半导体气敏模块 | 5V 供电, 模拟输出 | 需预热和标定 |
| 人体感应 | 热释电模块 | 3~5V, 数字输出 | 注意电平匹配 |
| 门磁 | 干簧管 | 常闭型 | 配 10k 下拉 |
| 显示 | OLED 0.96 寸 | I2C 接口 | 需 4.7k 上拉 |
| 提示 | 无源蜂鸣器 | 3.3V, PWM 驱动 | 配三极管 |
| 执行 | 5V 继电器模块 | 触点容量 10A | 光耦隔离 |
| 电源 | DC-DC + LDO | 12V→5V→3.3V | 分路供电 |
替代方案我一般会给出两套。比如主控可以用 STM32F103 的其它封装,也可以用国产的兼容型号,把时钟配置和 HAL 库头文件改一下就行。传感器如果想升级精度,可以换成电化学式或者红外式,接口形式可能从模拟变成串口,此时只需要改 BSP 层的读取函数,App 层的判定逻辑基本不用动,这就是分层的价值。
6.3 二次开发的几个扩展方向
这套项目的架构留了不少扩展空间,我自己也在陆续加东西。第一个方向是多点位组网:把几个节点分布在不同房间,通过无线模块汇总到一个主节点,主节点做集中显示和联动。这个改造的关键是设计一个简洁的通信协议,我建议用固定长度的二进制帧而不是字符串,节省带宽也便于校验。
第二个方向是接入家居环境联动。比如燃气报警时不仅开排风扇,还可以联动智能插座切断气源附近的电器、推送消息到手机。这里要注意的是,联动动作必须有本地兜底,网络断了你不能连报警都不响,所以我的设计里所有联动都是"本地优先,远程为辅"。
第三个方向是把它改造成通用的环境监测节点。这套代码里的 ADC 采样、滤波、迟滞判定、日志记录这几个模块是通用的,把气敏传感器换成温湿度、空气质量、光照传感器,代码复用率能在 70% 以上。我甚至拿同一套框架做过一个小型的鱼缸监测装置,测水温、控制加热棒和补光灯,改动量比我想象的小得多。
第四个方向是固件在线升级。这个需要留出专门的 Bootloader 分区,实现起来有一定门槛,但一旦做通,后续修复 bug 就不用把设备拆下来重新烧录了。我的建议是:如果你打算把这个东西长期装在家里并且会不断改代码,那 IAP 功能值得投入时间去做,因为反复拆装设备的成本远高于写 Bootloader 的成本。
我在实际使用中体会到的一点是,这类嵌入式项目真正的门槛从来不在写代码,而在于把一整套东西从"能跑"打磨到"敢让它 24 小时无人值守地跑"。中间差的那段距离,全是靠对细节的反复推敲和大量真实场景的验证补起来的。如果你的板子第一次上电就能稳定工作一整天,那是运气;如果它连续报警了三次都是误报,那才是正常的开始,剩下的工作就是把这些误报一个一个找出来、干掉。