前阵子接了一个室内空气质量监测的小项目核心需求就是让STM32把一路红外PM2.5传感器的浓度数据稳定读出来、能在串口上看到实时数值。红外PM2.5传感器里最经典、也最便宜好买的就是夏普GP2Y1010AU0F这颗网上搜“STM32连接红外PM2.5传感器”的思路很多但大部分教程都是粘贴一段Arduino代码就完事真正讲清楚怎么接线、怎么处理采样时序、怎么把电压换算成浓度、怎么让数据不飘的文章很少。这篇就把我从选型、接线、CubeMX工程搭建到代码实现、滤波标定和问题排查的整个过程记录一遍可以直接对照操作。适合做空气质量检测、智能家居、净化器控制这类STM32项目的朋友尤其是正在做课程设计或毕业设计的同学按这条路径走能少踩很多坑。1. 项目选型与整体方案为什么这次用红外式PM2.5传感器拿到需求的第一件事不是写代码而是先确认传感器类型。环境监测类的颗粒物传感器目前主流有两类激光散射式和红外散射式。激光式的代表是攀藤PMS5003、PMS7003这一类带风扇、带串口测出来的PM2.5浓度分辨率高、稳定性好但价格普遍在30到60元而且因为带风扇体积和功耗都不小。红外式的代表就是夏普GP2Y1010AU0F很多老空气净化器里用的就是它价格几块钱到十几块钱模块很小功耗低不需要额外风扇吹气但测量精度低一些更适合做“室内空气质量趋势判断”而不是“实验室级定量检测”。我这次选红外式原因很直接项目要求是低成本、低功耗、长时间连续监测不需要精确到个位的浓度值只要能分辨室内空气质量是优、良、差并且把趋势抓准就行。实测下来GP2Y1010的响应速度虽然比激光式慢但做半小时级别的趋势监测完全够用。另外红外式的输出是一个模拟电压接STM32的ADC引脚就能读不用解析UART协议开发链路短很多对做毕设和入门项目的朋友尤其友好。1.1 红外式与激光式PM2.5传感器的取舍选传感器不能只看价格要把精度、接口、功耗、开发成本和场景需求放在一起掂量。红外式和激光式在原理上都是“光散射法”区别在于光源红外式用红外LED做光源发出的光波长在850nm左右当空气中颗粒物经过光路时产生散射光光电二极管接收散射光信号经过内部运放放大后输出电压。激光式用激光二极管做光源光束更集中、单色性更好配合光电二极管阵列能检测更微弱的散射信号还能通过多角度散射估算不同粒径分布。因为光源和结构的差异激光式的信噪比通常高出不少测量下限和分辨率都更好。红外式对0.3μm附近的细小颗粒灵敏度较差而PM2.5恰恰包含大量这个粒径范围内的颗粒所以红外式的读数更多是“等效浓度”比激光式会偏低或者偏高偏差跟颗粒物成分有关。这一点在做标定时要心里有数不要指望红外式和激光式数值完全一致。从实际选型角度我建议这样判断对比维度红外式GP2Y1010激光式PMS5003系列价格5~15元30~60元输出接口模拟电压UART串口测量精度较低适合趋势判断较高适合定量监测功耗很低LED脉冲驱动高带风扇体积小大一圈开发难度简单ADC读取中等解析串口协议对课程设计、低成本智能家居、环境监测演示项目来说红外式性价比确实更高。但如果你的项目是“空气质量检测仪”这类以数据准确性为核心卖点的建议直接上激光式后面少折腾。1.2 整体方案与硬件清单确定传感器之后整个方案的框架就很清晰了STM32通过ADC读取GP2Y1010的模拟输出把电压换算成PM2.5浓度再做滤波处理最后通过串口把数值打印出来。硬件清单如下主控板STM32F103C8T6最小系统板也就是大家常说的“蓝色药丸”成本低资料多ADC、定时器、串口这些外设完全够用。传感器模块GP2Y1010AU0F红外PM2.5传感器模块带LED限流电路和运放输出的那种。淘宝上几块钱到十几块钱都有建议买接口已经引出的模块省去自己搭外围电路。逻辑电平转换注意原厂GP2Y1010的VCC是5V输出引脚在无尘时约0.5~0.9V高浓度时最大能到3.6V左右。很多模块已经做了输出分压或者电平转换输出范围被限制在0~3.3V可以直接接STM32的ADC引脚。如果用的是裸传感器OUT引脚直连STM32在高浓度时可能超过3.3V建议串联一个10k电阻或者分压后再进ADC。USB转TTL串口模块用来查看STM32串口打印的浓度数据。杜邦线若干、面包板或洞洞板、5V电源。这套硬件总成本控制在30元以内没问题。如果你想做完整的环境监测小系统后面还可以加一块OLED显示屏和DHT11温湿度传感器数据维度就更丰富了我从这个起点扩展出来的很多东西都是基于这套基础硬件的。2. 传感器原理与数据换算电压怎么变成PM2.5浓度很多人在这一步卡住ADC读到一个电压值比如1.2V然后呢这个电压怎么变成“浓度”如果不理解传感器的工作原理和输出特性后面的换算公式、标定方法都无从谈起。这一章把原理讲透。2.1 光散射法工作原理GP2Y1010内部结构其实不复杂一个红外LED、一个光电二极管、一个信号处理电路加上一个供气体流过的检测腔体。工作时LED以脉冲方式点亮发出的红外光穿过检测腔体如果腔体里有颗粒物光线会被颗粒散射散射光被光电二极管接收。颗粒浓度越高散射光越强光电二极管的电流越大经过内部运算放大器转换后输出引脚的电压就越高。这里要特别注意的是红外传感器的信号强度同时跟颗粒浓度和粒径有关并不是所有颗粒都产生相同强度的散射。大颗粒的散射光更强所以GP2Y1010对PM10这类大颗粒更敏感对细颗粒的响应会弱一些。这就是为什么红外式的读数会被质疑“不够准”的根本原因。但作为趋势监测这个特性不影响使用你只需要记住读数反映的是“等效粉尘浓度”不是绝对的PM2.5质量浓度。另外一个实际经验是传感器的输出跟气流状态关系很大。静止空气中颗粒物不会自动穿过检测腔体读数会偏低。这也是为什么很多空气质量监测产品会加一个小风扇主动抽气。GP2Y1010模块上通常没有风扇所以安装时要尽量让传感器暴露在待测空气中或者放在空气流通的位置。如果闷在密闭外壳里浓度突变时响应速度会很慢。2.2 ADC采样与浓度换算公式GP2Y1010的输出特性在数据手册里有明确说明输出电压与粉尘浓度近似线性关系灵敏度典型值为0.5V每0.1mg/m³。也就是说浓度每增加100μg/m³输出电压大约升高0.5V。方向是对的浓度越高电压越高。但要注意不同批次、不同模块之间零浓度时的输出电压并不完全相同一般在0.3V到0.9V之间浮动。所以严谨的做法是先测量洁净环境下的“基线电压”然后用实测电压减去基线电压再除以灵敏度得到浓度值浓度(mg/m³) (V_out - V_baseline) / 0.5V × 0.1为了方便程序计算建议统一用μg/m³作为单位浓度(μg/m³) (V_out - V_baseline) / 0.5V × 100这个公式里的V_baseline需要通过实测标定而不是直接套数据手册里某个固定值。我之前第一次做的时候直接套了网上流传的0.6V做基线结果在空气很干净的环境下算出来还有50多μg/m³明显异常。后来把传感器放在一个密封盒子里静置一小时实测基线电压是0.42V替换之后读数就正常了。这个经验值得记住任何公式都要结合你自己的硬件和实际环境重新标定。在STM32端ADC采集到的是一个12位的数字量。如果STM32的参考电压是3.3V那么V_out ADC_value × 3.3 / 4095然后代入浓度公式。这个过程在代码里就是两行浮点运算但精度上需要注意浮点运算在STM32F103上没有硬件FPU会占用一些CPU时间好在这个项目采样频率很低每秒一次采样完全没问题。代码实现我会在第三节给出完整示例。2.3 采样时序为什么不能一直点着LED这是红外PM2.5传感器项目里最容易忽略、也最影响数据质量的一个细节。GP2Y1010的LED不是连续点亮工作的数据手册要求LED的驱动脉冲宽度为0.32ms周期为10ms也就是约1%的占空比。而且在LED点亮之后的0.28ms处进行采样这时候输出电压才最稳定能代表真实的颗粒物散射信号。为什么设计成脉冲式原因有两方面。一是功耗LED如果连续点亮峰值电流接近20mA长时间工作发热严重传感器内部温度变化会导致红外光强漂移输出电压跟着漂移数据就会越来越不准。二是抗干扰脉冲驱动可以配合同步采样把环境光的干扰抑制掉一部分——外部环境光通常是连续稳定的而LED脉冲是有时序的在特定时刻采样可以突出散射信号。在STM32项目里最简单的处理方式是把LED引脚直接拉高让LED一直亮这种方法能工作但实测下来读数会偏高而且长时间运行后电压会缓慢漂移。更好的方式是使用定时器产生一个周期10ms、脉宽0.32ms的PWM波形驱动LED同时在脉冲发生的合适时刻触发ADC采样。我用PWM驱动方案之后数据稳定性提升非常明显长时间运行时基线电压几乎不变。第三节会专门讲这个PWM驱动和同步采样的配置方法。3. 接线与工程搭建从CubeMX到串口打印现在进入实操阶段。我会以STM32F103C8T6 Keil5 STM32CubeMX为环境把整个工程从零到串口打印数据的过程走一遍。开发环境的搭建Keil5安装、STM32F1芯片包安装、ST-Link驱动识别网上已经有很多教程这里不再展开直接进入项目配置。3.1 引脚定义与接线表接线前先把引脚规划好。我使用PA1作为ADC输入通道接传感器的模拟输出PA3作为PWM输出接传感器的LED驱动引脚。两个引脚相邻接线方便也避免和其他外设冲突。传感器引脚/模块接口说明接STM32F103C8T6VCC传感器电源正极5VGND传感器电源地GND必须和STM32共地LEDLED驱动输入PA3PWMOUT或VO模拟电压输出PA1ADC1_IN1用PWM驱动LED时要注意电平匹配。GP2Y1010的典型接法是LED引脚通过一个150Ω电阻直接接5V的PWM信号而STM32的PA3输出的是3.3V高电平驱动能力有限直接驱动LED会偏暗。所以我在PA3和LED引脚之间加了一个NPN三极管做开关用PA3控制三极管通断三极管控制LED的5V供电回路。如果你买的模块上已经集成了LED驱动电路通常直接把模块的LED引脚和STM32 GPIO相连即可具体要看模块原理图。如果你只是想快速跑通也可以把LED引脚用电阻直接接5V让LED常亮先把数据链路打通后面再优化驱动方式。3.2 CubeMX关键配置CubeMX配置项目的核心步骤如下RCC设置HSE选择Crystal/Ceramic Resonator使用外部8MHz晶振。SYS设置Debug选择Serial Wire避免烧录一次后ST-Link无法识别。USART1设置模式选Asynchronous波特率1152008位数据、无校验、1位停止位。这是数据输出通道。ADC1设置IN1开启采样时间可以选55.5 Cycles或更长一些采样时间越长采样电容充电越充分测量结果越稳定代价是采样速度慢但对于这种慢速信号完全无影响。PA3设置为TIM2_CH1的PWM输出功能TIM2预分频PSC设为83自动重载ARR设为9999这样在72MHz的时钟下PWM周期就是10ms。CCR设为320对应0.32ms的脉宽。ADC的触发方式这里不依赖硬件触发简化处理为在主循环里手动采样。如果你用的模块是LED常亮模式那就在主循环里直接读取ADC。如果使用PWM驱动就需要在PWM脉冲的合适时刻采样具体做法见下文的代码逻辑。3.3 核心代码实现工程生成之后首先在main.c中加一个串口重定向方便直接使用printf打印数据#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }然后在主循环里实现ADC读取和浓度换算。下面是完整的核心代码逻辑#include math.h #define ADC_REF_VOLTAGE 3.3f #define ADC_RESOLUTION 4095.0f #define SENSITIVITY 0.5f // 0.5V / 0.1mg/m3 #define BASELINE_VOLTAGE 0.42f // 实测标定值必须根据你的模块重新标定 uint16_t adc_buf[8]; uint32_t adc_sum 0; uint8_t sample_cnt 0; float voltage, dust_ugm3; while (1) { // 连续采样8次取平均减少随机噪声 adc_sum 0; for (sample_cnt 0; sample_cnt 8; sample_cnt) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 100); adc_buf[sample_cnt] HAL_ADC_GetValue(hadc1); adc_sum adc_buf[sample_cnt]; HAL_Delay(5); // 每次采样间隔5ms } // 平均ADC值换算成电压 voltage (adc_sum / 8.0f) / ADC_RESOLUTION * ADC_REF_VOLTAGE; // 电压换算成浓度单位为ug/m3 dust_ugm3 (voltage - BASELINE_VOLTAGE) / SENSITIVITY * 100.0f; // 避免负值 if (dust_ugm3 0.0f) { dust_ugm3 0.0f; } printf(ADC:%u, Voltage:%.3fV, PM2.5:%.1f ug/m3\r\n, adc_sum / 8, voltage, dust_ugm3); HAL_Delay(1000); // 1秒打印一次 }这段代码的逻辑很直白连续采8次平均减少ADC本身的抖动用电压公式和浓度公式换算负值强制归零串口每秒输出一次。实测下来这样已经能拿到相对稳定的读数但距离“不乱跳”还有点距离下一章专门说滤波。如果你的传感器使用PWM脉冲驱动那采样时机需要调整。我的做法是在主循环里检测PWM计数器当前输出状态当检测到PWM进入高电平后延时0.28ms再启动ADC采样因为0.28ms正好是LED点亮后信号稳定的时刻。STM32的TIM2计数器值可以通过__HAL_TIM_GET_COUNTER(htim2)读取当计数值等于CCR附近的某个阈值时触发采样。这个做法的精度虽然比不上硬件定时触发但在这个项目中已经足够。追求更高精度的做法是配置ADC的硬件定时器触发或者使用注入通道。4. 滤波优化与标定让数据平稳可信接好线、跑通代码、串口能看到数值这只是第一步。你在串口上大概率会看到读数跳得很厉害有时候上一秒80下一秒150毫无规律。这一章的滤波和标定才是让数据真正可用的关键。4.1 数据跳动来源数据跳动的原因主要有四个气流扰动、传感器噪声、ADC量化噪声、电源波动。气流扰动是最容易被忽略的。GP2Y1010没有主动送风结构传感器周围的空气是自然扩散流动的。室内人员走动、空调吹风、呼吸气流都会让颗粒物浓度在局部剧烈波动。这种波动的特点是“真实的”但如果我们监测目标是室内整体空气质量而不是某一点瞬时值就需要通过时间维度上的平均来抹平。传感器噪声和ADC量化噪声是设备层面的。GP2Y1010的内部运放增益高对电源纹波敏感如果供电来自USB或者开关电源纹波会直接叠加到输出信号上。12位ADC在3.3V参考电压下每个LSB约0.8mV这个量化误差对应约0.16μg/m³本身很小但采样时间设置太短时采样电容充电不充分会引入额外误差。电源波动的影响可以通过在模块电源引脚并联一个10μF电解电容和0.1μF陶瓷电容来改善这是一个成本极低但效果明显的措施。如果能够单独使用线性稳压器为传感器供电数据稳定性会更好。4.2 组合滤波算法实现针对上面的噪声来源我最终采用了“滑动平均中值”的组合滤波方案。思路是先对连续N个采样值做中值滤波剔除明显的毛刺点再对多个中值做滑动平均平滑时间维度的波动。中值滤波的典型实现#define MEDIAN_N 5 #define MEAN_N 4 uint16_t median_filter(uint16_t *data, uint8_t len) { uint8_t i, j; uint16_t tmp; uint16_t arr[MEDIAN_N]; for (i 0; i len; i) arr[i] data[i]; // 冒泡排序只取中值 for (i 0; i len - 1; i) { for (j 0; j len - 1 - i; j) { if (arr[j] arr[j 1]) { tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; } } } return arr[len / 2]; }实际在主循环中先收集15个原始ADC采样值每5个一组取中值得到3个中值再对这3个中值取平均作为本次输出的有效值。这样既能抑制单点毛刺又能平滑趋势而且运算量很小STM32F103跑起来毫无压力。滤波窗口长度的选择需要平衡响应速度和稳定性。窗口太短滤波效果不明显窗口太长浓度突变时响应会滞后比如你拿着香烟靠近传感器可能十几秒后数值才开始明显上升。我实际调试下来15个采样点、每秒采样一次、输出周期设为2到3秒一次既能获得比较平滑的曲线又能感知几分钟尺度上的浓度变化。4.3 两点标定法滤波解决的是“数据乱跳”的问题标定解决的是“数据准不准”的问题。因为每一颗传感器、每一块模块的基线电压和灵敏度都可能不同必须针对实际硬件做标定。我采用的是两点标定法。第一点是“洁净环境点”找一个密闭容器放入干燥剂和少量活性炭把传感器放进去静置半小时读取此时的输出电压作为基线电压V_baseline。第二点是“高浓度点”用一个密闭的透明盒子点一小段蚊香让烟雾在盒子里扩散等烟雾浓度稳定后用标准检测仪或者激光式传感器测出参考浓度C_ref同时记录GP2Y1010的输出电压V_high。然后重新计算灵敏度灵敏度 (V_high - V_baseline) / C_ref × 1000把这个实测灵敏度替换代码里的固定值0.5V数据就会有明显的改善。我实测下来手头这颗传感器的实际灵敏度大约是0.43V每0.1mg/m³跟数据手册典型值有一定偏差。如果硬套手册值浓度读数会偏高约15%这个误差对趋势监测问题不大但对需要数据对比的场景来说还是有点影响的。标定时的注意事项蚊香的烟雾浓度要达到稳定不要一边燃烧一边记录因为燃烧过程浓度一直在变化。另外标定环境和实际使用环境的温湿度差异也会影响测量如果环境变化太大建议在目标环境重新标定一次。5. 常见问题排查数值不对先查这里最后这部分把所有我遇到过的问题集中整理成速查表再挑两个最有代表性的实战排查过程展开。如果你按照前面的步骤操作之后数据依然不对劲大概率能在这里找到答案。5.1 问题速查表现象可能原因解决方法ADC读数一直是0传感器LED未工作、信号线断开、未共地用万用表测传感器输出引脚确认模块VCC和GND电压正常确认GND与STM32共地串口乱码波特率不一致、HSE晶振频率配置错误确认串口助手波特率是115200检查CubeMX中HSE频率是否设置为8MHzPM2.5浓度一直是0基线电压设置高于实际输出、负值被强制归零串口打印原始电压值确认实际电压是否高于基线电压数值波动剧烈气流扰动、电源纹波、没有滤波加入组合滤波在模块电源引脚并联电容避免气流直吹传感器数值持续缓慢上升LED连续点亮导致发热漂移改用PWM脉冲驱动方式降低LED功耗和温升数值明显偏高灵敏度参数不对、传感器靠近热源或强光重新做两点标定避开强光直射和发热源数值长时间不变滤波窗口过长、传感器检测腔体堵塞缩短滤波窗口检查检测腔体是否有灰尘覆盖高浓度时ADC值接近4095传感器输出超过3.3V模块未做分压在OUT引脚串联分压电阻或购买带电平转换的模块5.2 两个实战排查案例第一个案例是我一次性把两个GP2Y1010模块接到同一块STM32上其中一个读数正常另一个ADC输出始终是0。用万用表去量出问题模块的OUT引脚发现电压恒定的0V而正常模块在无尘环境下是0.43V。判断问题出在模块本身拆开检查发现LED引脚虚焊LED供电没通所以一直检测不到散射光。重新补焊之后数据正常。这个案例提醒我接好线之后先用万用表确认传感器输出端有基础电压不要一上来就怀疑代码。第二个案例是数据在50~200μg/m³之间剧烈跳动怎么滤波都不见好。排查过程比较曲折先用示波器看传感器供电的5V纹波看到一个频率约几十kHz、幅度约200mV的尖峰叠加在上面。这个尖峰来自开发板上的DC-DC降压电路通过VCC引脚直接传导到传感器内部运放导致输出信号严重污染。解决办法是在传感器的VCC和GND之间并联一个100μF电解电容和一个0.1μF陶瓷电容电容尽量靠近传感器模块引脚尖峰被明显压住数据也平稳下来了。如果传感器和主控是分离供电这种问题会好很多。这个项目做到后面我个人的体会是STM32连接红外PM2.5传感器真正难的地方不在STM32本身而在信号链路的每一个环节——供电是否干净、采样时序是否合理、标定是否到位、滤波是否有效。把这些环节理顺了下次换任何一款模拟输出的传感器你都能很快搞定。最后再分享一个小技巧采样和滤波逻辑不要都堆在主循环里。主循环里跑串口打印、显示刷新这些耗时的操作会让采样时间不稳定。更好的做法是用一个定时器中断比如定时器每50ms触发一次在中断里做ADC采样和滤波主循环只负责读取滤波后的结果做显示和上报。这样采样间隔是严格一致的数据质量会再上一个台阶。如果想扩展后面加OLED显示当前浓度、加Wi-Fi模块把数据上报到云端、或者联动继电器控制净化器启停都是在这个基础上很容易延伸的方向。
