1. 从三个传感器到一块屏:这套环境检测终端到底在解决什么问题
工厂车间里飘着肉眼看不见的粉尘,办公室角落的噪音长期超标却没人管,仓库里烟雾浓度悄悄爬升——这些场景有个共同点:等发现的时候,往往已经晚了。传统做法是买三台独立设备,各测各的,数据不互通,人还得跑到现场抄表。这套基于STM32的环境检测终端,核心思路就是把烟雾、粉尘、噪音三个维度的采集整合到一块板子上,通过WiFi把数据推到云平台,手机或电脑打开网页就能看到实时曲线和历史记录。
说白了,它解决的是**"多点位、多参数、远程可视"**这三个需求。适合谁看?如果你正在做物联网相关的课程设计、毕业项目,或者公司想搭一套低成本的车间环境监测原型,这套方案可以直接参考。硬件成本控制在百元级别,软件层面从裸机驱动到云平台对接的链路是完整的,不需要你再去拼凑零散的教程。
我见过太多人卡在"传感器读出来了但数据传不上去"或者"传上去了但云端显示乱码"这类问题上。这篇内容会从器件选型的逻辑讲起,把每个环节的坑和取舍说清楚,最后给出一套经过实测的完整实现路径。
2. 三个传感器的选型逻辑与信号链设计
2.1 烟雾检测:为什么我最终选了MQ-2而不是光学式方案
烟雾检测常见的有两种路线:离子式和半导体式。离子式灵敏度高但有微量放射性物质,做DIY项目基本不用考虑。半导体式里MQ-2是绕不开的选项,它的核心是一个二氧化锡敏感层,当还原性气体接触表面时电导率发生变化,通过负载电阻转换成电压信号。
选MQ-2的理由很直接:便宜、驱动简单、对烟雾和可燃气体都有响应。但它有个致命缺点——输出是模拟量,且受温湿度影响大。我实测过,同一根香烟的烟雾,早上和中午读出来的ADC值能差15%左右。所以代码里必须做两件事:一是预热时间给够(至少24小时老化,上电后预热3分钟再读),二是用滑动平均滤波把突变噪声压下去。
具体电路上,MQ-2的加热丝需要5V供电,测量回路接3.3V,负载电阻选10KΩ。这里有个容易忽略的点:加热丝电流接近150mA,如果和STM32共用LDO,可能导致MCU复位。我的做法是加热丝直接走5V,MCU的3.3V由另一路LDO供给,两边地线在电源入口处单点汇合。
// MQ-2滑动平均滤波示例 #define MQ2_WINDOW 10 static uint16_t mq2_buf[MQ2_WINDOW]; static uint8_t mq2_idx = 0; uint16_t mq2_read_filtered(void) { uint32_t sum = 0; mq2_buf[mq2_idx] = adc_read(MQ2_CHANNEL); mq2_idx = (mq2_idx + 1) % MQ2_WINDOW; for (uint8_t i = 0; i < MQ2_WINDOW; i++) { sum += mq2_buf[i]; } return sum / MQ2_WINDOW; }注意:MQ-2的校准不能只做一次。建议在已知洁净空气环境下记录基准值,后续用比值法判断浓度趋势,而不是直接拿ADC值当浓度用。
2.2 粉尘浓度:GP2Y1010AU0F的红外光学原理与驱动时序
粉尘检测我选的是夏普的GP2Y1010AU0F,这是一个红外光学式传感器。内部有一只红外LED和一个光电接收管,LED以脉冲方式发光,光线经过空气腔体,粉尘颗粒反射的光被接收管捕获,输出电压与粉尘浓度正相关。
这个传感器的驱动时序非常关键,不是给电就能读。它需要你提供一个周期性的脉冲信号驱动内部LED,然后在特定时间窗口内采样输出。根据手册,LED脉冲周期建议10ms,脉冲宽度0.32ms,采样点应在脉冲上升沿之后0.28ms处。
// GP2Y1010AU0F驱动时序 void dust_read_task(void) { HAL_GPIO_WritePin(DUST_LED_PORT, DUST_LED_PIN, GPIO_PIN_RESET); // 拉低点亮LED delay_us(280); // 等待0.28ms uint16_t adc_val = adc_read(DUST_CHANNEL); // 采样 delay_us(40); // 保持40us HAL_GPIO_WritePin(DUST_LED_PORT, DUST_LED_PIN, GPIO_PIN_SET); // 拉高熄灭 delay_us(9680); // 补足10ms周期 }实测下来,这个传感器对香烟烟雾、面粉粉尘响应很明显,但对水雾几乎没反应,这是光学原理决定的。另外它的输出是模拟电压,对应关系大约是每0.1mg/m³对应约0.5V输出,但个体差异大,需要自己用标准仪器标定一次。
2.3 噪音采集:MAX9814麦克风模块的增益配置与RMS计算
噪音检测用的是MAX9814模块,它集成了驻极体麦克风、可调增益放大器、自动增益控制。相比直接用麦克风加运放自己搭电路,这个模块省事很多,输出直接是模拟电压,STM32的ADC就能采。
关键参数是增益设置。MAX9814通过GAIN引脚的电平组合选择40dB、50dB、60dB三档增益。环境噪音监测建议用50dB档,40dB在安静环境下分辨率不够,60dB在嘈杂环境容易饱和削顶。我试过在车间里用60dB档,结果ADC一直在满量程附近跳,完全没法用。
噪音的"分贝值"计算需要做RMS(均方根)运算,不能直接读瞬时ADC值。因为声音是交流信号,ADC采到的是围绕某个直流偏置上下波动的值。正确做法是:连续采样N个点,减去直流偏置,求平方和,开根号,再取对数换算成dB。
float noise_calculate_db(void) { uint32_t sum_sq = 0; int32_t dc_offset = 2048; // 假设1.65V偏置,12位ADC for (int i = 0; i < 256; i++) { int32_t sample = adc_read(NOISE_CHANNEL) - dc_offset; sum_sq += sample * sample; delay_us(50); // 采样间隔约50us,覆盖音频范围 } float rms = sqrtf((float)sum_sq / 256.0f); // 换算成相对分贝值,需要根据实测校准 float db = 20.0f * log10f(rms / 1.0f) + CALIBRATION_OFFSET; return db; }提示:这个算出来的是相对值,不是绝对声压级。要得到真实分贝值,需要用声级计对比标定,把偏移量CALIBRATION_OFFSET确定下来。没有声级计的话,可以用手机上的分贝测试App做粗略参考。
3. STM32主控的资源分配与任务调度策略
3.1 为什么选STM32F103C8T6而不是更便宜的方案
市面上有更便宜的MCU,比如某些8位机,但做这个项目我坚持用STM32F103C8T6,原因有三:第一,需要至少3路12位ADC,8位机的ADC精度不够,噪音信号动态范围大,8位根本分辨不出来;第二,需要UART和WiFi模块通信,还要留调试串口,至少两个串口;第三,RAM要够用,做滑动平均和RMS计算需要缓存数组。
F103C8T6有2个12位ADC、3个USART、20KB RAM、64KB Flash,价格在十元左右,性价比很合适。如果预算更紧,可以考虑F030系列,但ADC性能和RAM会缩水,做噪音RMS计算时数组不能开太大。
资源分配上,我的方案是:
| 外设 | 用途 | 引脚 | 备注 |
|---|---|---|---|
| ADC1_IN0 | MQ-2烟雾 | PA0 | 滑动平均滤波 |
| ADC1_IN1 | GP2Y粉尘 | PA1 | 需配合LED驱动时序 |
| ADC1_IN2 | MAX9814噪音 | PA2 | RMS计算 |
| USART1 | WiFi模块 | PA9/PA10 | 115200波特率 |
| USART2 | 调试串口 | PA2/PA3 | 复用需重映射 |
| TIM2 | 任务调度 | 内部 | 1ms中断 |
| GPIO | 粉尘LED驱动 | PB0 | 推挽输出 |
3.2 裸机还是RTOS:一个基于实际复杂度的判断
很多人一上来就上FreeRTOS,觉得这样才"专业"。但这个项目我建议先用裸机跑通,再考虑要不要上RTOS。原因很简单:任务数量少(三个传感器采集+一个WiFi发送),逻辑不复杂,裸机的前后台架构完全够用。
我的做法是用TIM2产生1ms基准中断,在中断里维护几个计数器,主循环里根据计数器值决定是否执行采集任务。比如烟雾每500ms采一次,粉尘每1s采一次,噪音每200ms采一次,WiFi每2s发一次数据。这样各任务的实时性都能保证,又不用引入RTOS的调度开销。
volatile uint32_t tick_ms = 0; volatile uint8_t flag_smoke = 0, flag_dust = 0, flag_noise = 0, flag_wifi = 0; void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE); tick_ms++; if (tick_ms % 500 == 0) flag_smoke = 1; if (tick_ms % 1000 == 0) flag_dust = 1; if (tick_ms % 200 == 0) flag_noise = 1; if (tick_ms % 2000 == 0) flag_wifi = 1; } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); MX_USART1_UART_Init(); MX_TIM2_Init(); HAL_TIM_Base_Start_IT(&htim2); while (1) { if (flag_smoke) { flag_smoke = 0; smoke_value = mq2_read_filtered(); } if (flag_dust) { flag_dust = 0; dust_value = dust_read_filtered(); } if (flag_noise) { flag_noise = 0; noise_value = noise_calculate_db(); } if (flag_wifi) { flag_wifi = 0; wifi_send_data(smoke_value, dust_value, noise_value); } } }如果后续要加OLED显示、SD卡存储、多路继电器控制,任务多到十个以上,再考虑上FreeRTOS也不迟。过早引入RTOS只会增加调试难度,尤其是栈溢出问题在裸机阶段根本不会遇到。
3.3 ADC多通道采集的坑:规则组与注入组的取舍
STM32的ADC有多通道采集模式,但这里有个坑:规则组扫描模式下,DMA传输是最稳的,但如果不用DMA,用轮询方式切换通道,每次切换后需要等待稳定时间。我一开始用轮询,发现通道间串扰严重,烟雾通道的读数会受噪音通道影响。
后来改成单次转换+手动切换通道+足够延时的方式,问题解决。具体是每次采集前重新配置ADC通道,启动转换,等待EOC标志,读值,然后延时至少10us再切下一个通道。虽然效率低一点,但数据干净。
uint16_t adc_read(uint8_t channel) { ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = channel; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); uint16_t val = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); delay_us(10); return val; }注意:如果非要用DMA,记得把三个通道的采样时间设成一样的,否则DMA传输的数据对应关系会乱。我见过有人采样时间设了不同的值,结果烟雾数据跑到噪音通道去了。
4. WiFi模块对接云平台:从AT指令到数据上云的完整链路
4.1 ESP-01S的AT指令模式:简单但需要处理超时和重连
WiFi模块我用的是ESP-01S,跑AT固件。有人喜欢用MQTT固件直接连,但AT模式更灵活,适合快速原型。核心指令就几条:AT+CWMODE=1设Station模式,AT+CWJAP连热点,AT+CIPSTART建TCP连接,AT+CIPSEND发数据。
但实际用起来,超时处理是最大的坑。AT指令的响应时间不确定,连热点可能3秒,也可能10秒。如果用阻塞式等待,整个系统就卡死了。我的做法是给每条指令设一个超时计数器,在1ms中断里递减,超时了就重发或报错。
uint8_t at_send_cmd(char *cmd, char *expect, uint32_t timeout_ms) { uart_send_string(cmd); uint32_t start = tick_ms; while ((tick_ms - start) < timeout_ms) { if (uart_rx_buffer_contains(expect)) { return 1; // 成功 } } return 0; // 超时 } void wifi_connect(void) { while (!at_send_cmd("AT+CWMODE=1\r\n", "OK", 2000)); while (!at_send_cmd("AT+CWJAP=\"SSID\",\"PASSWORD\"\r\n", "WIFI GOT IP", 15000)); while (!at_send_cmd("AT+CIPSTART=\"TCP\",\"api.example.com\",80\r\n", "CONNECT", 5000)); }这里有个细节:AT+CWJAP的响应是"WIFI GOT IP"而不是"OK",如果你等"OK"会一直超时。不同固件版本响应可能不同,建议先用串口助手手动发一遍,把实际响应记下来。
4.2 数据上云的协议选择:HTTP POST还是MQTT
云平台对接有两种常见方式:HTTP POST和MQTT。HTTP简单直接,每次发一个POST请求,服务器返回200就完事。MQTT是长连接,适合高频上报和下行控制。
这个项目我选的是HTTP POST,原因是:数据上报频率低(2秒一次),不需要服务器主动下发指令,HTTP的请求-响应模式足够用,而且调试方便,用串口助手就能模拟。MQTT需要处理心跳、重连、订阅回调,复杂度高一个量级。
数据格式用JSON,虽然比二进制占带宽,但可读性好,云端解析也方便。一个典型的POST请求体:
{ "device_id": "env_node_01", "timestamp": 1700000000, "smoke": 320, "dust": 0.08, "noise": 56.3 }发送时注意Content-Length要算准,包括JSON字符串的每一个字符。我踩过一次坑,长度少算了2个字节,服务器一直返回400,查了半天才发现是换行符没算进去。
4.3 断网重连与数据缓存:不让网络问题毁掉整个系统
WiFi断线是常态,尤其是工业环境。如果网络一断,数据就丢了,那这套系统就没法用。我的方案是在STM32的Flash里开一块区域做环形缓冲区,网络正常时直接发,网络异常时把数据存进去,恢复后补发。
F103C8T6有64KB Flash,划出最后4KB做数据缓存,每条记录12字节,能存300多条,按2秒一条算,能扛10分钟断网。写入时注意Flash擦除是按页的(1KB),不能单字节覆盖,需要先读到RAM,改完再整页写回。
#define FLASH_STORE_ADDR 0x0800F000 #define RECORD_SIZE 12 #define MAX_RECORDS 340 typedef struct { uint32_t timestamp; uint16_t smoke; uint16_t dust; uint16_t noise; uint16_t reserved; } env_record_t; void flash_store_record(env_record_t *rec) { static uint16_t write_idx = 0; uint32_t addr = FLASH_STORE_ADDR + write_idx * RECORD_SIZE; HAL_FLASH_Unlock(); HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr, *(uint16_t*)&rec->timestamp); // ... 继续写其他字段 HAL_FLASH_Lock(); write_idx = (write_idx + 1) % MAX_RECORDS; }提示:Flash写入前必须先擦除,而擦除操作会拖慢主循环。建议把Flash操作放在网络确认断开之后,不要每次采集都写,攒够10条再批量写一次。
5. 实测中暴露的三个典型问题与排查过程
5.1 噪音通道的50Hz工频干扰:从波形到根因的完整定位
调试时发现噪音读数一直在跳动,安静环境下也有明显的周期性波动。用调试串口把原始ADC值打到电脑上画波形,发现周期正好是20ms,对应50Hz。这是典型的工频干扰,来源是电源或者空间耦合。
排查步骤:第一步,把MAX9814的电源从开关电源换成电池供电,干扰明显减小,说明电源纹波是主要来源;第二步,在模块电源脚并一个100uF电解电容加一个0.1uF陶瓷电容,进一步改善;第三步,把麦克风信号线改成屏蔽线,屏蔽层单端接地,干扰基本消失。
最终方案是电池供电+LC滤波+屏蔽线三管齐下。如果必须用开关电源,建议在LDO之前加一级π型滤波,截止频率设在100Hz以下。
5.2 粉尘传感器读数漂移:清洁周期与零点校准
GP2Y1010AU0F用了一周后,发现洁净环境下读数从0.02mg/m³涨到了0.15mg/m³。拆开看,光学腔体内壁积了一层灰,红外光被灰尘散射,接收管误判为粉尘浓度高。
解决办法有两个层面:硬件上,给传感器加一个微型风扇,持续吹气形成正压,防止灰尘进入腔体;软件上,每次上电时做一次零点校准,把当前读数作为基准偏移,后续读数减去这个偏移。
float dust_zero_offset = 0.0f; void dust_calibrate(void) { float sum = 0; for (int i = 0; i < 50; i++) { sum += dust_read_raw(); delay_ms(20); } dust_zero_offset = sum / 50.0f; } float dust_read_calibrated(void) { float raw = dust_read_raw(); float val = raw - dust_zero_offset; return (val < 0) ? 0 : val; }注意:零点校准必须在洁净空气环境下做。如果开机时环境本身就有粉尘,校准反而会把真实数据减掉。建议加一个按键,手动触发校准。
5.3 WiFi模块发热导致ADC基准漂移的连锁反应
连续跑了几小时后,发现所有传感器的读数都整体偏高。用手摸ESP-01S,烫得厉害。查资料发现,ESP-01S在持续发送数据时电流能到170mA,LDO发热导致STM32的VDDA参考电压漂移,ADC读数自然跟着偏。
解决思路:第一,给WiFi模块单独供电,不要和MCU共用LDO;第二,在VDDA引脚加一个10uF钽电容和一个0.1uF陶瓷电容,稳定参考电压;第三,软件上定期用内部参考电压校准ADC。
STM32F103内部有一个1.2V基准电压通道(VREFINT),可以反推VDDA的实际值:
float get_vdda(void) { uint16_t vrefint = adc_read(ADC_CHANNEL_VREFINT); // VREFINT典型值1.20V,12位ADC满量程4095 return 1.20f * 4095.0f / vrefint; }实测下来,加了电容和独立供电后,连续运行8小时,读数漂移控制在2%以内,满足一般监测需求。
6. 云平台侧的数据展示与告警配置
6.1 用免费云平台搭建实时看板:从数据接收到图表渲染
云平台我选的是一个提供免费额度的物联网平台,支持HTTP接入和可视化看板。设备端POST上来的JSON数据,平台自动解析成数据流,然后在看板上拖拽组件就能生成折线图。
配置步骤:第一步,在平台创建设备,拿到设备ID和API Key;第二步,定义三个数据点:smoke、dust、noise;第三步,创建看板,添加三个折线图组件,分别绑定数据点;第四步,设置刷新间隔为5秒。
看板上我加了三个仪表盘组件显示当前值,三个折线图显示过去一小时的趋势。这样一眼就能看出哪个指标异常。比如噪音曲线突然飙升,说明现场有异常声响;粉尘曲线持续上升,可能需要启动除尘设备。
6.2 阈值告警:什么时候该触发,怎么避免误报
告警阈值不能拍脑袋定。我的做法是先跑一周只记录不告警,把数据导出来分析。烟雾ADC值正常在200-400之间,超过600持续10秒才告警;粉尘正常低于0.15mg/m³,超过0.3持续30秒告警;噪音正常低于65dB,超过75dB持续5秒告警。
持续时间的设置很关键。如果一超过阈值就告警,那有人打个喷嚏、开门关门的瞬间震动都会触发。加一个持续时间窗口,能过滤掉90%的误报。
平台侧配置告警规则时,注意告警恢复也要设阈值,比如触发值是75dB,恢复值设70dB,避免在阈值附近反复触发和恢复,造成告警风暴。
6.3 历史数据导出与趋势分析:从原始数据到决策依据
平台一般支持导出CSV格式的历史数据。我习惯每周导一次,用表格软件做个简单的透视分析。比如按小时统计粉尘浓度的平均值,找出每天的高峰时段;按天统计噪音超标次数,评估车间环境改善效果。
这些分析结果可以直接用于调整设备部署位置或者优化生产排班。比如发现某台设备在下午2点到4点之间粉尘浓度最高,对应的是某个打磨工序,那就可以考虑在这个时段加强通风,或者把工序调整到人员较少的时段。
数据导出时注意时区问题。平台默认可能是UTC时间,导出后要转换成当地时间再分析,否则高峰时段会偏移8小时,得出完全错误的结论。
7. 从原型到产品:外壳、供电与长期运行的工程化考量
7.1 外壳设计:散热与传感器开孔的平衡
原型阶段可以裸板跑,但要长期部署,外壳是必须的。设计外壳时有两个矛盾:散热需要通风,但传感器需要防尘。我的方案是分腔设计:主控和WiFi模块放在一个腔体,开散热孔;传感器放在另一个腔体,只开必要的进气孔,且进气孔加防尘网。
外壳材料用ABS塑料,3D打印或者激光切割亚克力都行。注意不要用金属外壳,会屏蔽WiFi信号,而且可能影响传感器读数。如果必须用金属,WiFi天线要引出壳外。
传感器开孔的位置也有讲究。粉尘传感器的进气口要朝下,利用重力防止大颗粒灰尘直接落入。噪音麦克风要朝外,但不要正对风口,否则风噪会干扰测量。
7.2 供电方案:从USB供电到PoE的演进路径
原型阶段用USB供电最方便,5V直接进,板载LDO转3.3V。但长期部署时,USB线是个隐患,容易松动,而且距离受限。
进阶方案是PoE供电,一根网线同时传数据和供电,最远能到100米。但PoE需要额外的PD模块,成本会增加。折中方案是12V直流集中供电,每个节点用DC-DC降压到5V,再LDO到3.3V。12V线损小,适合几十米的布线距离。
如果部署在户外或者没有市电的地方,可以考虑太阳能+锂电池方案。但要注意功耗优化,把WiFi发送间隔从2秒改成10秒,MCU在空闲时进入Stop模式,整体功耗能从200mA降到20mA以下,一块10Ah的电池能撑两周。
7.3 长期运行稳定性:看门狗、日志与远程重启
设备跑在无人值守的环境,看门狗是最后的保险。STM32内置独立看门狗(IWDG),由内部低速时钟驱动,即使主时钟挂了也能复位。我的配置是溢出时间4秒,主循环里每1秒喂一次狗。
void iwdg_init(void) { HAL_IWDG_Start(&hiwdg); } // 主循环中定期调用 void iwdg_feed(void) { HAL_IWDG_Refresh(&hiwdg); }除了看门狗,日志系统也很重要。把关键事件(WiFi连接成功/失败、传感器读数异常、看门狗复位)记录到Flash里,出问题时能回溯。日志不用太复杂,一个环形缓冲区,记录时间戳和事件码就够了。
远程重启功能可以通过云平台下发指令实现。设备定期查询是否有重启命令,收到后调用NVIC_SystemReset()。这个功能在设备死机但看门狗没触发时特别有用。
8. 一些踩过坑之后才明白的实操经验
关于传感器预热:MQ-2和GP2Y1010AU0F都需要预热,但两者的预热逻辑不同。MQ-2是加热丝需要达到工作温度,上电后3分钟才能读稳定值;GP2Y1010AU0F是红外LED需要稳定,上电后30秒就够了。代码里要分别处理,不要一刀切等3分钟。
关于ADC参考电压:如果项目对精度要求高,建议用外部基准芯片,比如REF3033,输出3.3V,温漂比LDO小一个数量级。成本增加不到五块钱,但读数稳定性提升明显。
关于WiFi天线:ESP-01S板载的是PCB天线,方向性很强。如果设备装在金属盒里,信号会差很多。要么把天线引出壳外,要么换用带IPEX接口的模块接外置天线。我试过在金属盒里贴一层吸波材料,效果有限,不如直接换外置天线。
关于数据上报频率:不要盲目追求高频上报。2秒一次对环境监测足够了,再快就是浪费带宽和电量。如果确实需要高频,考虑在设备端做边缘计算,只上报异常片段,正常时降频上报。
关于固件升级:原型阶段用串口烧录就行,但部署后最好支持OTA。STM32的IAP功能可以实现,但需要把Flash分成Bootloader区和App区,还要处理升级失败的回滚。这个复杂度不低,建议第一版先不做,等硬件稳定了再考虑。
关于成本控制:如果要做多个节点,BOM成本要精打细算。STM32F103C8T6十元,ESP-01S八元,三个传感器加起来三十元左右,加上PCB和外围元件,单节点能控制在六十元以内。批量采购还能再降。