1. 项目概述与目标拆解1.1 为什么选红外PM2.5传感器而非激光传感器先把话说明白STM32接PM2.5传感器这事核心不在STM32在传感器。市面上能买到的PM2.5传感器基本分两派——红外散射式和激光散射式。激光的精度高、能测到更小的颗粒物浓度但价格动辄五六十起步好的上百红外的便宜十几二十块钱能拿下用在室内空气质量监测、智能家居、DIY环境监测站这些场景完全够用。这个项目选红外PM2.5传感器核心诉求就一个字性价比。比如夏普的GP2Y1010AU0F或者兼容的GP2Y1014AU0F这两个型号在DIY圈子里非常常见。它们的原理是内置一个红外LED和一个光电二极管空气通过传感器中间的孔洞时颗粒物散射红外光光电二极管接收到散射光后输出一个与浓度相关的电压信号。这个原理决定了它的响应速度不会太快精度也不如激光的但胜在结构简单、驱动电路成熟、数据读取方便和STM32搭起来非常顺畅。1.2 这个项目能解决什么问题做这个项目的典型场景有这么几种室内空气质量监测实时采集PM2.5浓度超过阈值就联动继电器开新风、开净化器。毕业设计/课程设计STM32作为主控搭配OLED屏显示浓度数值外加蜂鸣器报警这个组合在本科生课设里出现频率极高。小型环境监测节点接入ESP8266/ESP32或者LoRa模块把数据上传到云端组一个多点位监测网络。我这次做的时候顺带把数据在0.96寸OLED上显示出来同时通过串口打印原始ADC值和换算后的浓度值。这样调试阶段不用额外买调试器一根USB转TTL线就能把数据看明白。1.3 整体技术路线概览整个项目的技术链路是红外PM2.5传感器输出PWM或模拟电压信号 → STM32通过ADC采样或定时器输入捕获读取信号 → 单片机内部做滤波和浓度换算 → 结果输出到OLED/串口/继电器。这里有个重要的选型决策GP2Y1010AU0F和GP2Y1014AU0F两个型号引脚兼容但输出方式有区别。GP2Y1010AU0F输出的是模拟电压需要STM32的ADC来采样GP2Y1014AU0F除了模拟电压还支持PWM输出PWM的高电平脉宽对应浓度值。后面我会详细讲这两种读取方式各自的优劣以及我在实际项目中为什么最终选了ADC方案。2. 传感器原理与读取方案选型2.1 红外散射法的物理本质先把原理讲透这对后面写代码和理解数据非常有帮助。红外PM2.5传感器内部结构大致是这样的一个红外LED作为光源一个光电二极管作为接收端中间是一个能让空气自由流通的通道。工作时LED以固定频率发射红外光GP2Y1010AU0F的驱动脉冲频率典型值是10kHz左右脉宽约0.32ms当空气中存在颗粒物时颗粒物会散射红外光其中一部分散射光被光电二极管接收。颗粒物浓度越高散射光强度越大光电二极管输出的光电流就越大。这里要特别强调一点红外散射法对粒径较小的颗粒物比如PM1.0以下的细颗粒响应不灵敏因为散射强度与粒径的六次方相关瑞利散射区域粒径小一截散射光强会衰减得很厉害。这就是为什么红外传感器在空气质量很好的时候浓度低于20ug/m³读出来的数据波动很大甚至会出现“虚高”的情况。这不是传感器坏了是物理原理决定的。2.2 三种读取方式对比ADC、PWM捕获、UART红外PM2.5传感器和STM32的接口方式主要有三种我分别说一下优缺点读取方式传感器输出STM32外设优点缺点模拟电压0~3.6V模拟信号ADC接线简单单引脚读取实时性好需要外部RC滤波电路受电源纹波影响PWM脉宽周期性PWM高电平脉宽对应浓度定时器输入捕获抗干扰能力强不需要ADC占用定时器通道代码稍复杂UART串口主动或被动上报数字浓度USART数据直接是处理好的浓度值需要协议解析部分传感器是单向输出我在实际项目里最先试的是ADC方案原因很直接GP2Y1010AU0F的模拟输出最经典网上现成的参考电路和代码最多出问题了好排查。后来为了验证PWM方案的可行性也写了一个基于定时器输入捕获的读取代码实测下来PWM方案确实更稳数据跳变幅度比ADC小不少但这个结论是在加了比较好的外部滤波电路之后对比得出的。2.3 为什么最终选了ADC 定时器捕获双方案其实我这个项目最后做成了“一个传感器、两种读取方式”的兼容设计。主板上有跳线帽拨到A侧走ADC采样拨到P侧走PWM捕获。这样设计的好处是兼容不同型号的红外PM2.5传感器不用换板子。调试时可快速切换读取方式对比数据稳定性找到最适合的采样策略。后续如果换激光传感器很多激光传感器直接输出UART数据板子的硬件改动也只是加一个串口接口的事。从“做一个能用的东西”角度来说ADC方案就够了但如果你想把这个项目作为学习STM32外设用法的练手项目我强烈建议把PWM捕获也实现一遍。定时器输入捕获是STM32非常实用的功能测频率、测脉宽、做编码器读数全靠它这个项目里正好用上。3. 硬件接线与电路设计3.1 传感器引脚定义与接线对照以GP2Y1010AU0F为例它一共6个引脚功能如下引脚号名称功能接STM321V-LEDLED供电正极5V5V电源2LED-GNDLED地GND3LEDLED驱动脉冲输入STM32定时器PWM输出引脚4S-GND传感器地GND5Vo模拟电压输出STM32 ADC引脚6Vcc传感器供电正极5V5V电源注意这里面最容易被坑的是第3脚LED驱动脉冲。GP2Y1010AU0F的内部LED不是常亮的而是需要外部提供一个约10kHz、脉宽0.32ms的脉冲信号来驱动。这个脉冲信号有两个来源一是用STM32的定时器输出PWM频率和脉宽都要配准确二是用NE555这类硬件电路产生固定脉冲。如果你偷懒直接把LED引脚接高电平让LED常亮传感器会很快发热而且输出数据完全不对。这一点我第一次做的时候踩过坑后来老老实实配了定时器PWM输出。3.2 供电电路AMS1117的坑和电容选型红外传感器的LED需要瞬间大电流GP2Y1010AU0F的数据手册里写的是LED峰值电流典型值20mA但脉冲驱动时瞬间电流会更大。如果你用AMS1117-3.3给传感器供电大概率会出现电压跌落导致ADC读数异常波动。我的做法是传感器用5V单独供电不和STM32的3.3V共用一路LDO。如果只能用3.3V供电必须在传感器电源引脚旁边加一个100uF的电解电容和一个0.1uF的陶瓷电容并联。模拟输出Vo引脚对地加一个0.1uF电容做低通滤波滤掉高频噪声。关于电容选型网上有讨论过“AMS1117把钽电容换成陶瓷电容对STM32有影响吗”这类问题。这里我说一下自己的经验AMS1117这类LDO对输出电容的ESR有要求换陶瓷电容后稳定性通常会更好但前提是容值不能太小。AMS1117的数据手册推荐输出电容至少22uF我用的是22uF陶瓷电容并联0.1uF高频去耦实测非常稳没出现振荡问题。3.3 STM32引脚分配参考我用的主控是STM32F103C8T6蓝 pill最小系统板引脚分配如下功能引脚说明ADC输入PA1ADC1_IN1采样传感器Vo输出PWM输出PA8TIM1_CH110kHz脉宽0.32ms驱动传感器LEDOLED-SCLPB6I2C1时钟OLED-SDAPB7I2C1数据蜂鸣器PB0浓度超标报警USART1-TXPA9串口打印数据PA8做PWM输出是因为TIM1是高级定时器挂在APB2总线上时钟频率72MHz配置10kHz非常方便。ADC选择PA1是因为它和PA0相邻后续如果想扩展双通道监测比如同时接温湿度传感器引脚布局上也方便。4. 核心代码实现与参数计算4.1 环境准备Keil5工程搭建和芯片包安装开发环境我用的是Keil MDK 5搭配STM32标准外设库。如果你手头只有Keil5装不上芯片包大概率是Pack Installer没配好。这里说两个常见问题Keil5兼容C51和STM32安装MDK和C51可以共存安装时选不同安装目录C51版装到Keil_v5的C51子目录MDK版装到ARM子目录两个共存互不影响。芯片包安装在Keil官网下载STM32F1xx_DFP包双击安装安装路径要指向Keil_v5的ARM/PACK目录。装完后在工程配置的Device选项卡里就能看到STM32F103C8。4.2 定时器PWM配置10kHz脉冲驱动传感器GP2Y1010AU0F的LED驱动脉冲要求是频率约10kHz脉宽0.32ms。这意味着PWM周期是100us1/10kHz高电平时间320ns占周期比例是0.32%。这个占空比非常小所以在计算定时器重载值和比较值时要用64位精度去算避免出现整数截断误差。以STM32F103C8T6的TIM1为例时钟源是72MHzAPB272MHzTIM1倍频后也是72MHz周期100us对应的计数器值72MHz × 100us 7200。所以设置TIM_Prescaler 0TIM_Period 7199。高电平320ns对应的计数器值72MHz × 0.32us 23.04 ≈ 23。所以设置TIM_Pulse 23。这么算下来的结果是PWM频率 72MHz / 7200 10kHz高电平时间 23 / 72MHz 319.4ns非常接近数据手册要求的320ns。下面是定时器PWM初始化代码用的标准外设库void TIM1_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 7199; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 23; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }注意最后那行TIM_CtrlPWMOutputs(TIM1, ENABLE)这是高级定时器TIM1/TIM8特有的必须调用否则PWM波形不会输出。低级定时器如TIM2/TIM3不需要这行。4.3 ADC采样配置连续采样加均值滤波ADC读取传感器Vo输出的思路是开启ADC1的PA1通道使用软件触发连续转换模式每次转换完成后触发DMA搬运结果这样CPU不用一直等着ADC转换省下来的时间可以处理显示和逻辑。ADC初始化代码如下void ADC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 16; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode ENABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }这里我把DMA缓冲区设成了16个半字也就是说DMA会自动连续采样16次我只需要在main循环里对这16个值做平均就能得到一个比较稳定的ADC结果。ADC采样时间用239.5周期这是为了降低信号源阻抗带来的采样误差毕竟传感器Vo输出端的等效阻抗不算低。4.4 浓度换算公式从电压到ug/m³浓度换算是这个项目最容易出错的地方。GP2Y1010AU0F的数据手册给出的典型输出电压-浓度关系近似是线性的但不同批次的传感器个体差异比较大手册给的只是典型曲线。我的做法分两步第一步是标定。在传感器上电且空气流通稳定后记录一个“零点电压”纯净空气下的Vo电压这个值通常在0.5V~1.0V左右。然后在已知浓度环境下比如雾霾天或密闭房间点一根烟制造高浓度再记录一个高浓度点的电压。两点连线得出这个传感器的实际电压-浓度斜率。第二步是换算。STM32读到的ADC值是12位0~4095参考电压3.3V所以电压 ADC值 × 3.3 / 4095浓度 (电压 - 零点电压) × 斜率系数我用的经验公式是float adc_to_pm25(uint16_t adc_value) { float voltage (float)adc_value * 3.3f / 4095.0f; float concentration (voltage - 0.6f) * 100.0f; if (concentration 0) concentration 0; return concentration; }这里的0.6f是零点电压我实测这块传感器的零点在0.55~0.65V之间100.0f是换算斜率系数。这个斜率系数不是固定的需要根据你自己的传感器实际标定结果来调整。如果你的传感器在空气里读到5V电压说明零点标定错了或者数据完全没经过滤波。4.5 数据滤波滑动平均和中值滤波ADC原始数据跳变非常剧烈尤其是浓度低的时候信号本身信噪比差读数可能一会儿5ug/m³一会儿30ug/m³。我的处理策略是两级滤波DMA采样16次取平均消除高频噪声。每200ms做一次采样采10次去掉最大值和最小值剩下的8次取平均。代码实现#define FILTER_N 10 float pm25_get_average(void) { float values[FILTER_N]; float sum 0, max 0, min 10000; for (int i 0; i FILTER_N; i) { values[i] adc_to_pm25(get_adc_average()); HAL_Delay(20); } for (int i 0; i FILTER_N; i) { if (values[i] max) max values[i]; if (values[i] min) min values[i]; sum values[i]; } return (sum - max - min) / (FILTER_N - 2); }每次采样间隔20ms10次下来总共200ms左右实时性上完全够用。如果做的是需要快速响应的报警系统可以把间隔缩短到10ms但要注意蜂鸣器或继电器的动作需要有阈值迟滞避免浓度在阈值附近来回跳导致继电器频繁通断。5. 常见问题排查与避坑指南5.1 传感器数据虚高或者一直很大这个现象我在调试中遇到过两次原因完全不同第一次是LED驱动脉冲没有配好。PWM频率不对或者高电平时间不对LED亮度不够传感器增益异常导致输出信号虚高。排查方法是直接用示波器看PA8引脚的波形确认频率是10kHz、高电平约320ns。第二次是传感器旁边有其他红外干扰源比如手、衣服、墙壁反射红外光。红外散射传感器的探测区域不只是传感器正面的孔旁边如果有强反射物会把红外光反射回光电二极管导致读数偏高。解决方法是传感器安装时周围要留出至少1cm的空隙不要紧贴外壳。5.2 串口打印数据显示NaN或者负数这是典型的浮点数运算问题。出现负数说明电压值小于零点电压此时浓度按0处理。NaN通常是因为除零或者未初始化变量。我在代码里做了保护if (concentration 0) concentration 0; if (isnan(concentration)) concentration 0;还有一种情况是ADC值一直为4095说明ADC引脚悬空或者配置错误。检查GPIO是否配置为模拟输入模式另外DMA通道是否正确对应ADC1的通道1。5.3 STM32无法识别USB设备这个和本项目关系不大但凡是做STM32项目基本都会遇到。USB识别不了一般是几种情况板子上电后没有进入ISP模式BOOT0拉高再复位、USB线是纯充电线没有数据线、或者驱动没装。如果你用的是STM32F103C8T6最小系统板把BOOT0接到3.3V然后按一下复位键再插USB通常就能识别到。下载完程序后记得把BOOT0跳回GND再复位一次否则程序不运行。5.4 传感器上电后发热严重这是个大坑很多人第一次用GP2Y1010AU0F都会遇到。发热的原因是LED驱动引脚被直接接高电平LED持续导通。GP2Y1010AU0F的LED只能在脉冲驱动下工作占空比约0.32%如果持续导通电流会远超额定值发热是必然的时间长了LED还会烧掉。判断方法很简单传感器正常工作时用万用表测LED引脚应该看到的是PWM波形而非持续高电平。如果没有示波器可以摸一下传感器外壳微热是正常的烫手就是有问题。5.5 数据波动大怎么办虽然加了滑动平均和中值滤波数据还是会有一定波动。这里有两个思路一是提高传感器供电质量。传感器的Vo输出直接反映了内部光电流光电流本来就很小很容易被电源噪声干扰。用示波器看5V电源轨上的纹波如果超过50mV建议在电源入口加一个100uF电解电容和0.1uF陶瓷电容。二是适当延长滤波时间窗口。把中值滤波的采样次数从10提高到25数据平滑度会明显改善。代价是响应时间变长浓度突变后读数要1~2秒才能跟上。对于室内空气质量监测这个场景响应慢两三秒完全无所谓反而视觉上数据更稳定。6. 项目扩展思路6.1 和空气质量监测开源项目结合网上有不少“基于STM32的空气质量检测开源项目”核心功能就是PM2.5浓度采集加阈值报警。我这个项目配合OLED显示、按键阈值设置、蜂鸣器报警就是一个完整的课设/毕设方案。如果你想做得更完整可以加一个DHT11或SHT30温湿度传感器凑齐“温度湿度PM2.5”三要素数据展示更有说服力。6.2 数据上报平台串口转WiFi/4GSTM32把浓度数据通过串口发给ESP8266ESP8266用MQTT协议上报到云平台如巴法云、点灯科技、阿里云IoT就能实现手机远程查看空气质量。需要注意串口波特率设置一致两端都是115200或者9600均可。6.3 联动控制继电器空气净化器这是很自然的扩展方向PM2.5浓度超过35ug/m³国家标准的一级限值时继电器闭合打开净化器或新风系统低于20ug/m³时继电器断开。控制策略上要加迟滞逻辑避免频繁开关。我的经验是阈值上限设为40下限设为25。浓度超过40启动降到25以下才停止中间这15ug/m³的迟滞带就是防止“临界点震荡”用的。6.4 红外传感器和激光传感器混合使用如果你后面预算充足想换成激光传感器比如攀藤PMS7003UART输出精度高很多代码改动也不大。PMS7003的串口协议是标准的读帧头0x42 0x4D校验通过后解析出PM2.5浓度。我当时在同一块板子上加了PMS7003的接口红外传感器作为辅助两个数据一对比才发现红外传感器在低浓度时的偏差确实比激光传感器大不少但高浓度下两者趋势是接近的。所以如果只是做“超标报警”这种定性判断红外完全够用要拿来做科研数据或者和国标站点的数据对比那还是得上激光的。7. 最后的实操总结把这次做项目的经验浓缩成几条红外PM2.5传感器便宜好用但精度有限适合做趋势监测和阈值报警不适合做精密测量。ADC方案最简单PWM捕获方案更稳双方案设计可以让你在不同场景下灵活切换。LED驱动脉冲一定要配准10kHz、0.32ms高电平少一个条件数据就全是错的。DMA加滑动平均加中值滤波三级滤波下来数据已经非常能看了不需要上卡尔曼滤波这种复杂算法。供电是最大的坑传感器独立供电电源引脚加100uF和0.1uF电容数据稳定性提升一个档次。我个人在实际项目迭代中的体会是这类传感器项目硬件电路占六成功力固件占四成。很多人一上来就急着写代码结果数据不好看就开始怀疑代码写错了其实多数时候问题在供电和信号完整性上。先把硬件调理干净再用软件滤波做平滑基本不会翻车。这个项目做完以后你手里相当于有了一套完整的“STM32采集模拟信号”的模板——改一改ADC配置就能接其他模拟量传感器改一改定时器配置就能接PWM输出型传感器复用率非常高。
