1. 从零搭建一个环境检测节点的完整思路
烟雾、粉尘、噪音这三个指标凑在一起,其实是一个非常典型的"工业+民生"交叉场景。工厂车间要监测粉尘浓度防止爆炸,城市噪音监测站要记录分贝数据,地下车库要检测烟雾预警火灾——这些需求背后都是同一套技术逻辑:用一颗低功耗主控芯片采集多路传感器数据,通过无线链路把数据推到云端,再在远端做可视化与告警。我这次做的这个节点,主控选的是STM32系列,无线部分走的是WiFi模组,云端用的是通用的物联网云平台。整套方案成本可控、开发周期短,适合个人开发者练手,也适合小批量部署做原型验证。
先说清楚这个项目适合谁看。如果你已经玩过STM32的GPIO和串口,但还没做过完整的"采集-传输-上云"链路,那这篇内容基本可以当施工图用。如果你是完全零基础,建议先把STM32的ADC和UART搞明白再回来,不然中间会卡在调试环节。整套系统涉及的知识点包括:模拟传感器信号调理、ADC采样与滤波、UART通信协议、WiFi模组的AT指令交互、MQTT协议上云、以及云端的数据展示配置。我会按实际搭建顺序来讲,每一步都说明为什么这么做,以及我踩过的坑。
有一点需要提前说明:环境检测类项目最容易被忽视的不是代码,而是传感器的选型与标定。市面上同类型的烟雾传感器、粉尘传感器、噪音传感器价格从几块到几百块不等,输出信号形式也各不相同(模拟电压、PWM、UART、I2C都有)。选错了传感器,后面代码写得再漂亮数据也是废的。所以我会在传感器部分花比较多篇幅,把选型逻辑和标定方法讲透。
2. 三类传感器的选型逻辑与信号特性拆解
2.1 烟雾检测:为什么我最终选了模拟输出的MQ系列
烟雾传感器最常见的两类方案:一类是MQ系列气敏传感器(如MQ-2、MQ-135),输出模拟电压;另一类是光电式烟雾报警模块,输出开关量或PWM。我一开始用的是开关量模块,结果发现它只能告诉你"有烟/没烟",阈值还是电位器手动拧的,根本没法做浓度趋势分析。后来换成MQ-2模拟输出方案,虽然需要自己做ADC采样和标定,但能拿到连续的浓度变化曲线,这才是有价值的数据。
MQ-2的工作原理是:内部有一个二氧化锡气敏材料,接触还原性气体时电导率升高,通过一个负载电阻把电导率变化转成电压变化。这里有个关键细节——MQ系列传感器需要预热。冷启动时读数会从高往低漂移,通常要预热24小时以上才能稳定。我在代码里做了一个"上电后前5分钟数据标记为不可信"的处理,避免刚上电就触发误报警。
实际接线时,MQ-2模块一般有4个引脚:VCC、GND、DO(数字输出)、AO(模拟输出)。DO基本不用,我们只接AO到STM32的ADC通道。模块内部已经做了分压,输出范围大概在0到3.3V之间,可以直接接STM32的ADC引脚,不需要额外调理电路。但要注意:MQ-2的加热丝功耗不小,大概在150mA左右,如果用电池供电,这个功耗必须纳入考量。
2.2 粉尘检测:红外散射原理与串口输出的取舍
粉尘检测我试过两种方案。第一种是夏普式的光学粉尘传感器,输出PWM占空比,通过计算低电平时间占比来推算粉尘浓度。这个方案便宜,但精度一般,而且受环境光干扰明显。第二种是激光散射式粉尘传感器,直接通过UART输出PM2.5、PM10的数值,精度高很多,但价格也上去了。
我最终选了激光散射式方案,原因是:PWM方案需要占用一个定时器做输入捕获,而且标定曲线是非线性的,换算麻烦;串口方案直接给数值,STM32这边只需要解析协议帧就行。激光散射传感器的串口协议一般是固定的帧格式,比如起始字节0x42、0x4D,后面跟数据长度、数据内容、校验和。解析的时候要注意字节序和校验,不然会读到乱码。
这里有个实操经验:激光散射传感器的进风口和出风口不能堵,而且内部有风扇,长期运行会积灰。我在外壳设计时留了可拆卸的滤网,每隔一段时间清理一次。如果积灰严重,读数会持续偏高,你会以为是环境问题,其实是传感器脏了。
2.3 噪音检测:麦克风模块的偏置电压问题
噪音检测用的是驻极体麦克风模块,输出的是模拟电压。这里有个很容易踩的坑:麦克风模块输出的信号是叠加在直流偏置上的交流信号。也就是说,安静环境下输出大概是VCC/2(比如1.65V),有声音时电压会在1.65V上下波动。STM32的ADC只能读0到3.3V的正电压,所以直接采样是没问题的,但你要做的是计算交流分量的幅度,而不是直接读绝对值。
我的处理方法是:在软件里做一个滑动窗口,计算窗口内ADC采样的峰峰值(最大值减最小值),这个峰峰值就反映了声音的强度。然后再通过一个经验公式换算成分贝值。注意,这个分贝值是相对值,不是绝对声压级,因为没有经过声学校准。如果要测绝对分贝,需要用标准声源校准,这对个人项目来说成本太高,一般做趋势监测够用了。
麦克风模块的供电也要注意,尽量用独立的LDO供电,不要和WiFi模组共用一路电源,否则WiFi发射时的电流波动会串到麦克风上,导致噪音读数跳变。我一开始就是共用电源,结果数据里全是WiFi发射的周期性干扰,后来加了一个LC滤波才解决。
3. STM32端的采集框架与ADC多通道调度
3.1 用DMA+定时器触发做无阻塞采样
三个传感器里,烟雾和噪音是模拟输出,需要ADC采样;粉尘是串口输出,需要UART接收。如果ADC用轮询方式读,会阻塞主循环,影响串口数据的及时处理。我的做法是:用定时器触发ADC,ADC通过DMA把数据搬到内存缓冲区,主循环只负责从缓冲区取数据做处理。这样CPU占用率很低,也不会丢串口数据。
具体配置上,我用的定时器触发频率是1kHz,ADC采样时间设置为55.5个周期(STM32F1系列),这样单次转换大概几十微秒,完全来得及。DMA配置为循环模式,缓冲区大小设为64,半满和全满都触发中断。在中断里做数据搬运和标记,主循环检测到有新数据就取出来做滤波。
这里有个细节:ADC的多通道扫描顺序要和DMA缓冲区的索引对应好。比如我配置的是通道0(烟雾)、通道1(噪音),那么DMA缓冲区里就是[烟雾, 噪音, 烟雾, 噪音...]交替排列。取数据的时候要按索引取,不然会把噪音数据当成烟雾数据。
3.2 滑动平均滤波与异常值剔除
原始ADC数据抖动很大,直接上传云端曲线会像心电图一样。我用了两级滤波:第一级是中值滤波,连续采5个点取中间值,去掉脉冲干扰;第二级是滑动平均,维护一个长度为16的队列,每次新数据进来就替换最老的数据,然后算平均值。这样出来的曲线平滑很多,又不至于滞后太严重。
异常值剔除的逻辑是这样的:如果某个采样值超过了物理量程的上下限(比如ADC读数超过4000或低于100),直接丢弃,不参与滤波。这个判断很有必要,因为传感器偶尔会输出离谱的值,如果不剔除,滑动平均会被拉偏很久。
注意:滤波窗口长度不是越大越好。我试过用64点的滑动平均,结果响应太慢,烟雾出现后要好几秒才能反映到数据上。后来改成16点,响应速度和稳定性比较平衡。
3.3 串口粉尘数据的帧解析与校验
粉尘传感器的串口输出是固定帧格式,我用的这款是每帧32字节,起始是两个字节的帧头,然后是数据长度、数据内容、校验和。解析的时候我用了一个状态机:先找帧头,找到后开始接收后续字节,收满一帧后校验,校验通过就提取PM2.5和PM10的值,校验失败就丢弃重新找帧头。
这里有个坑:串口接收中断里不要做复杂处理。我一开始在中断里直接做帧解析,结果因为处理时间太长,导致后续字节丢失。后来改成中断里只把字节存入环形缓冲区,主循环再从缓冲区里取数据做解析,问题就解决了。
环形缓冲区的实现很简单:一个数组加两个指针(读指针和写指针),写指针在中断里移动,读指针在主循环里移动。要注意判断缓冲区满和空的条件,不然会覆盖未读数据。
4. WiFi模组的AT指令交互与MQTT上云
4.1 WiFi模组选型与硬件连接要点
WiFi模组我用的是常见的ESP系列模组,通过UART和STM32通信。接线就四根:VCC、GND、TX、RX。注意TX和RX要交叉接,STM32的TX接模组的RX,STM32的RX接模组的TX。供电方面,模组峰值电流可能到300mA以上,LDO要选足够余量的,不然模组启动时会复位。
STM32和模组之间的串口波特率一般设为115200,这个速率足够传输传感器数据了。我用的串口配置是8位数据位、1位停止位、无校验。流控不需要,因为数据量不大。
4.2 AT指令的状态机设计与超时重试
和WiFi模组交互的核心是AT指令。整个上云流程大概是:复位模组→设置WiFi模式→连接热点→建立TCP连接→发送MQTT连接报文→订阅主题→定时发布数据。每一步都是一条AT指令,都要等模组返回"OK"或相应结果。
我用了一个状态机来管理这个过程,每个状态对应一条AT指令,发送后启动超时定时器,收到预期响应就跳到下一个状态,超时就重试,重试3次还失败就回到初始状态重新来。这个逻辑看起来简单,但实际写的时候要注意:模组的响应可能分多行返回,不能只判断一次就认为完成了。比如连接热点时,模组会先返回"WIFI CONNECTED",再返回"WIFI GOT IP",最后才返回"OK"。要等到"OK"才能进行下一步。
实操心得:调试AT指令时,先把模组接到电脑上用串口助手手动发指令,把整个流程跑通,确认每条指令的响应格式,再写到代码里。这样能省很多调试时间。
4.3 MQTT报文的手动拼装与发布
有些WiFi模组内置了MQTT指令,可以直接发AT+MQTTPUB这样的指令。但我用的这款模组只支持TCP透传,所以MQTT报文需要自己拼装。MQTT的CONNECT报文和PUBLISH报文格式是固定的,我写了一个函数专门拼装。
CONNECT报文的关键字段包括:协议名("MQTT")、协议级别(4)、连接标志(清理会话、用户名密码标志等)、保持连接时间、客户端ID、用户名、密码。这些字段按MQTT协议规范拼成字节流,通过TCP发出去就行。
PUBLISH报文相对简单:固定报头(报文类型+QoS等级)、主题名长度+主题名、报文标识符(QoS>0时需要)、载荷内容。我发布的数据格式是JSON字符串,比如{"smoke":123,"dust":45,"noise":67},这样云端解析起来方便。
这里要注意:MQTT的剩余长度字段是变长编码,不是固定一个字节。如果载荷比较长,剩余长度会占多个字节。我一开始没注意这个,载荷短的时候没问题,载荷一长就发不出去。后来写了一个编码函数专门处理变长字段。
5. 云端数据接入与可视化配置的实操细节
5.1 云平台设备创建与三元组配置
云平台这边,首先要创建一个设备,平台会分配三元组:产品ID、设备名、设备密钥。这三个信息在MQTT CONNECT报文里要用到。产品ID和设备名拼成客户端ID,设备名和密钥用来计算用户名和密码(有些平台是直接填,有些需要做HMAC签名)。
我用的平台是直接填用户名密码的方式,比较简单。创建完设备后,要定义物模型,也就是数据点。我定义了三个属性:烟雾浓度、粉尘浓度、噪音值,数据类型都是浮点或整型。物模型定义好之后,平台会自动生成对应的Topic,设备往这个Topic发数据,平台就能解析并存储。
注意:不同平台的Topic格式不一样,有的平台Topic里带产品ID和设备名,有的只带设备名。创建完设备后一定要看清楚平台文档里的Topic格式,填错了数据发不上去。
5.2 数据上报频率与心跳保活策略
数据上报频率我设的是每5秒一次。这个频率是权衡的结果:太快了流量大、云端存储压力大;太慢了实时性差。对于环境监测场景,5秒一次足够反映趋势变化了。
MQTT的保持连接时间我设的是60秒,也就是说如果60秒内没有收到任何报文,平台会认为设备离线。为了保持在线,我在没有数据上报的时候会发PINGREQ心跳包。心跳包的发送周期一般是保持连接时间的一半,也就是30秒发一次。
这里有个坑:如果WiFi断线重连,MQTT连接也要重新建立。我一开始只做了WiFi重连,没做MQTT重连,结果WiFi恢复后设备显示在线但数据不更新。后来在状态机里加了MQTT重连逻辑,检测到TCP断开就重新走一遍CONNECT流程。
5.3 云端告警规则与数据看板搭建
数据上云之后,我在平台上配了告警规则:烟雾浓度超过阈值发通知,粉尘浓度持续偏高发通知,噪音超过阈值发通知。告警通知可以走邮件或平台内消息,我配的是平台内消息,因为邮件有延迟。
数据看板用的是平台自带的可视化工具,拖拽几个图表控件,绑定对应的数据点就行。我做了三个实时曲线图(烟雾、粉尘、噪音)和一个历史数据表格。看板可以分享链接,手机浏览器也能打开,很方便。
这里分享一个经验:看板的刷新频率不要设太高。我一开始设的1秒刷新,结果页面很卡,后来改成5秒刷新,流畅多了。因为数据本身就是5秒上报一次,看板刷新再快也没用。
6. 联调阶段遇到的典型问题与排查路径
6.1 数据跳变:从电源干扰到地线环路
联调时最先遇到的问题是噪音数据跳变严重,安静环境下读数也在大幅波动。排查过程是这样的:先用示波器看麦克风模块的输出,发现上面叠加了一个周期性的尖峰,频率和WiFi发射周期一致。确认是WiFi模组发射时的电流波动通过电源串到了麦克风。
解决方案分两步:第一,给麦克风模块单独加了一个LDO,和WiFi模组分开供电;第二,在麦克风输出和STM32 ADC输入之间加了一个RC低通滤波(1k电阻+0.1uF电容),把高频干扰滤掉。改完之后数据稳定多了。
这个问题的本质是共地干扰。虽然分开供电了,但地线还是连在一起的,WiFi的大电流还是会通过地线影响麦克风。后来我在PCB布局上把模拟地和数字地分开走,单点接地,效果更好。
6.2 上云断连:AT指令超时与TCP KeepAlive
第二个问题是设备运行几个小时后会掉线,而且不会自动重连。抓串口日志发现,是TCP连接被服务器断开了,但模组没有上报断开事件,STM32这边还以为连接正常。
解决方法是:在应用层加心跳检测。我让STM32每隔30秒发一次MQTT PINGREQ,如果连续两次没有收到PINGRESP,就主动断开TCP重新连接。另外,在AT指令状态机里加了"TCP断开"事件的检测,模组上报"CLOSED"时就触发重连流程。
这里要注意:模组的TCP断开事件可能是异步上报的,不一定在你发指令的时候才返回。所以串口接收中断里要一直监听,收到"CLOSED"就置一个标志位,主循环检测到标志位就启动重连。
6.3 粉尘传感器读数偏高:进风口积灰与校准
运行一周后发现粉尘读数持续偏高,放在干净环境里也显示PM2.5有七八十。拆开传感器一看,进风口和内部风扇上积了一层灰。清理之后读数恢复正常。
这件事让我意识到:粉尘传感器需要定期维护。后来我在外壳上做了可拆卸结构,用户自己就能清理。另外,我在云端加了一个"传感器维护提醒"的逻辑:如果连续24小时读数都偏高且波动很小,就发通知提醒清理。
校准方面,我用了一个简单的零点校准:在已知干净的环境里(比如刚下过雨的室外),记录传感器读数,把这个值作为零点偏移,后续数据都减去这个偏移。这个方法不严谨,但对趋势监测够用了。
7. 外壳设计与供电方案的工程化考量
7.1 传感器开孔位置对读数的影响
外壳设计不是随便打个孔就行。烟雾传感器需要空气流通,但又要避免灰尘直接落在传感头上。我的做法是在传感器上方开百叶窗式的斜孔,既能透气又能挡灰。粉尘传感器的进风口和出风口要错开,避免排出的空气又被吸回去。噪音传感器的麦克风孔要朝外,但孔不能太大,否则容易进异物。
我第一版外壳是把所有传感器都塞在一个盒子里,结果噪音传感器受风扇噪音影响,粉尘传感器受烟雾传感器加热丝的热气流影响,数据全是错的。第二版把三个传感器分仓放置,中间用隔板隔开,问题才解决。
7.2 供电方案:USB供电与电池供电的取舍
供电方案我做了两版。第一版是USB供电,直接插充电头,简单省事,但位置受限。第二版是电池供电,用了一节18650锂电池加升压模块,续航大概两天。如果要更长续航,需要降低上报频率和用低功耗模式,但STM32加WiFi模组的底电流就不小,低功耗空间有限。
如果要做长期部署,我建议还是走有线供电,或者用太阳能板加蓄电池。电池方案适合临时部署或移动场景。另外,供电电压要稳定,WiFi模组对电压波动很敏感,电压低于3.0V时可能会反复复位。
8. 从原型到小批量部署的几点经验
这套东西我从面包板搭到PCB打样,前后折腾了大概一个月。最大的体会是:环境检测项目的难点不在代码,在传感器和结构。代码框架搭好之后基本不用大改,但传感器选型、标定、外壳设计这些"硬件活"才是决定数据质量的关键。
如果让我重新做一遍,我会在选型阶段花更多时间做对比测试,而不是急着写代码。另外,云端的数据存储和告警规则要提前规划好,不然数据上来了才发现格式不对,改起来很麻烦。
最后分享一个小技巧:在STM32里加一个"模拟数据"模式,不接传感器也能生成假数据上传云端。这样调试云端配置的时候不用等硬件,效率高很多。这个模式在排查"是设备问题还是云端问题"的时候特别有用。