最近做环境监测的项目在STM32上又接了一次红外PM2.5传感器型号是用得最多的GP2Y1014AU0F。这类传感器比激光PM2.5模块便宜一大截十几元一片对精度要求不高的场景完全够用。这篇文章把整个环节照着可复现的标准写一遍为什么选红外、怎么接线、STM32CubeMX怎么配置、ADC怎么读、电压怎么换算成浓度、最后怎么标定和排坑。做课程设计、智能家居或者空气净化器改造的可以直接抄作业。1. 红外PM2.5传感器选型与基本原理1.1 红外式和激光式为什么我先选了红外红外PM2.5传感器的原理说起来很直白传感器内部有一个红外LEDLED持续或脉冲点亮后光线会穿过一个采样腔空气里的颗粒物把光散射到旁边的光电二极管上光电二极管把散射光转成电流再经过传感器内部的运算放大器输出一个模拟电压。颗粒物越多散射光越强输出电压就越高。但这里有个很多人容易误解的点红外方案并不是真正只测2.5微米以下的颗粒。它是把所有能引起散射的颗粒物混在一起总量估算再用经验曲线去近似PM2.5浓度。激光方案因为波长单一、散射角可控可以更准确地推算粒径分布所以精度更高、价格也更贵。做项目的时候如果只是判断“屋里空气质量变好了还是变差了”或者做净化器联动红外足够如果要做计量级监测那还是老老实实上激光传感器比如PMS5003系列。1.2 GP2Y1014AU0F 的输出特性GP2Y1014AU0F是夏普很经典的红外粉尘传感器GP2Y1010AU0F是它的老款两者接法和用法基本一样。传感器一共6个引脚V-LED、LED-GND、LED、S-GND、Vo、Vcc。V-LED和Vcc都要接电源LED是控制红外LED点亮的引脚Vo是模拟电压输出S-GND是信号地。裸传感器的输出电压特性和温度、湿度、光源、老化程度都有关数据手册上给的大致参考是无尘环境下输出电压约0.9V灵敏度约0.5V每0.1mg/m3换算下来就是5V每mg/m3。换句话说浓度每增加100ug/m3输出电压大约上升500mV。空气质量到500ug/m3已经是重度污染所以红外传感器在0到500ug/m3这个区间内还算能打再高就会进入饱和区线性度变差。这个特性决定了后面换算浓度时不能只看一个固定公式至少要修一次零漂。2. STM32与传感器的硬件连接2.1 引脚接线表新手照着插我用的主控是STM32F103C8T6最小系统板ADC参考电压就是3.3V。传感器供电我用5V因为数据手册的特性曲线基本都按5V供电给出来的3.3V供电也能工作但输出范围会变曲线也不是同一组。接线方案放在下面。传感器引脚接到哪里说明V-LED5V红外LED电源走线要尽量粗短LED-GNDGNDLED地LEDSTM32 PA1 或直接接GND我这边模块是低电平点亮S-GNDGND信号地和LED-GND共地Vo分压后到STM32 PA0模拟电压输出Vcc5V内部放大电路电源这里最容易踩的坑是Vo直接进STM32的ADC口。传感器5V供电时Vo最大能到3.6V以上而STM32的ADC输入范围是0到3.3V直接接进去轻则读数一直满量程重则长期使用有损伤风险。所以我在Vo和PA0之间加了一组分压电阻传感器Vo先串一个4.7k电阻再到PA0节点PA0节点再接一个22k电阻到GND。分压比大约是22/(4.722)0.82最大电压3.6V进来后PA0上大约是2.96V安全又能测满量程。2.2 那两个关键电容和电阻分压不能省如果你看过这类传感器的数据手册会发现V-LED和LED-GND之间要接一个220uF电容。很多人觉得电容只是滤波省略掉也能出数实测就会遇到数据乱跳的问题。红外LED点亮瞬间电流很大可能到几百毫安如果电源线细、稳压芯片反应慢Vo会被电源波动拉得抖来抖去。220uF电容相当于给LED脉冲电流一个就近的储能池能大幅减小供电跌落。另外Vcc和GND之间加一个0.1uF陶瓷电容用来滤高频噪声。分压电阻也不能随便选。电阻分压电路的等效内阻大概是两个电阻并联4.7k和22k并联后约3.9k。这个阻抗对ADC来说不低如果采样时间很短ADC内部的采样电容来不及充满到稳定值读数会偏低。解决办法后面会讲就是把ADC采样时间调到最长。实际项目里如果不想用分压可以改用轨到轨运放做电平搬移或者用外部ADC芯片但那样成本上去了没必要。3. 用STM32CubeMX配置ADC和串口3.1 ADC采样时间为什么拉满STM32CubeMX里操作很简单把PA0的模式设为ADC1_IN0也就是模拟输入。时钟照常用72MHz或者64MHz都行这个传感器本来就不需要高采样率。关键是ADC参数里的Sampling Time我直接选最长那一档F103上通常是239.5 Cycles部分芯片有640.5 Cycles能选更长就选更长。原因就是上面说的分压电阻输出阻抗。ADC采样过程可以理解成一个电容充电过程输入阻抗越高需要的充电时间越长。如果把采样时间设成1.5 Cycles这种极短档采样电容还没充满就结束了读出来的电压会明显偏低而且不同批次芯片之间偏差还不一样。索性把采样时间拉满用一点点时间换稳定性和一致性。一个PM2.5传感器本来就不需要每秒采几千次几十毫秒采一次都嫌多。3.2 串口输出和调试信息怎么加串口是这个项目里最实用的调试输出通道。CubeMX里打开USART1模式选Asynchronous波特率设1152008位数据位1位停止位。生成工程后可以用重定向fputc的方式让printf走串口也可以用HAL_UART_Transmit自己封装一个输出函数。我个人的习惯是优先用HAL_UART_Transmit把要发的数据拼成字符串再发这样不会依赖编译器对printf浮点支持的那套东西。Keil里默认的MicroLIB对printf支持还行但GCC环境下偶尔会踩坑。输出内容最好把ADC原始值、分压后的真实电压、最终浓度一起打出来接线和换算哪个环节有问题一眼就能看出来。4. ADC读取、滤波和浓度换算代码4.1 轮询读取ADC的最小代码整个读取逻辑最简单的状态就是轮询。我写了一个读取ADC原始值的函数HAL库模式下十六进制数可以直接拿。uint16_t read_adc_raw(ADC_HandleTypeDef *hadc) { HAL_ADC_Start(hadc); if (HAL_ADC_PollForConversion(hadc, 100) ! HAL_OK) { return 0; } return (uint16_t)HAL_ADC_GetValue(hadc); }HAL_ADC_PollForConversion的第二个参数是超时时间单位毫秒。单次ADC转换实际耗时只有几十微秒100ms的超时已经非常宽松了。如果程序一直卡在这里不返回优先检查CubeMX里有没有把ADC1初始化、PA0是不是模拟输入模式以及有没有把ADC时钟使能。4.2 数据滤波平均值、中位值、滑动滤波红外传感器的信号本身就带抖动单次采样直接拿来用显示出来的数字会有很大的视觉噪音。我的做法是先采5次每次间隔20ms然后取中位值。中位值的好处是能干掉偶尔出现的脉冲式尖峰比如某个瞬间电源波动或颗粒物正好飘过产生的异常值。#define SAMPLE_NUM 5 uint16_t samples[SAMPLE_NUM]; uint16_t median5(void) { uint16_t tmp[SAMPLE_NUM]; uint16_t key; int i, j; for (i 0; i SAMPLE_NUM; i) { tmp[i] samples[i]; } for (i 1; i SAMPLE_NUM; i) { key tmp[i]; j i - 1; while (j 0 tmp[j] key) { tmp[j 1] tmp[j]; j--; } tmp[j 1] key; } return tmp[SAMPLE_NUM / 2]; }对5个元素做插入排序完全够用不需要什么高级排序算法。拿中位值之后再做一个滑动滤波让数据更平滑。float smooth_value 0.0f; uint16_t med_value; float filtered_value; med_value median5(); filtered_value (float)med_value * 0.3f smooth_value * 0.7f; smooth_value filtered_value;这个式子的含义是让新数据占30%历史数据占70%变化快慢可以自己调。红外传感器的响应速度本来就是秒级的滤波激进一点不影响实际使用。4.3 从电压到PM2.5浓度的换算先把ADC值换算成PA0上的电压再乘回分压比例得到传感器Vo的真实电压。#define ADC_REF_MV 3300.0f #define ADC_FULL_SCALE 4095.0f #define DIVIDER_RECOVER 1.2136f #define NO_DUST_MV 900.0f #define SENS_MV_PER_UG 5.0f float get_pm25_ugm3(void) { float adc_mv filtered_value * ADC_REF_MV / ADC_FULL_SCALE; float real_vout_mv adc_mv * DIVIDER_RECOVER; float density_ugm3 (real_vout_mv - NO_DUST_MV) / SENS_MV_PER_UG; if (density_ugm3 0.0f) { density_ugm3 0.0f; } return density_ugm3; }几个常量的来路说一下。ADC_REF_MV是开发板3.3V参考电压如果你的板子实际供电不是3.3V要用万用表实测后改掉。DIVIDER_RECOVER是22k和4.7k分压的还原系数也就是(4.722)/22。NO_DUST_MV是零飘电压传感器标称典型0.9V实际每一片都不一样后面标定环节会讲。SENS_MV_PER_UG的意思是从0.5V每0.1mg/m3换算过来的结果0.1mg/m3就是100ug/m3对应500mV所以每1ug/m3对应5mV。5. 标定与准确性验证5.1 零点偏移怎么标定红外传感器最需要标定的就是零漂。你手里那片传感器无尘环境下的输出电压很可能不是0.9V而是0.85V或者1.1V不同批次差异不小。如果不标定直接用900mV当零点算出来的浓度整体偏高或偏低空气净化器联动可能还算正常做数据展示就非常难看。标零点的操作很简单找干净的HEPA滤网或者几层一次性口罩把传感器进风口包起来让它抽过滤后的空气。上电先跑10分钟让红外LED充分预热再连续采一两百个数据取平均值把这个平均值替换程序里的NO_DUST_MV。这里特别提醒开机就把数据标定零点是不行的传感器内部发热后电压会漂移必须等热平衡了再标。5.2 红外传感器算出来的数能不能当真不少人是拿红外传感器的数值和几百块钱的激光PM2.5检测仪比一比就觉得“我是不是做错了”。红外方案本身就不是计量级方案它的输出受颗粒物粒径、颜色、水汽、传感器老化程度影响很大。打个比方它更像是电子烟检漏那类“半定量”工具适合判断趋势和相对变化不适合做官方机构那种空气监测数据。所以做毕业设计或者技术博客时表述上也要严谨一点。别写“精确测量PM2.5浓度”建议写“基于红外光散射法的颗粒物浓度估算装置”。你把它当成一个环境变化趋势传感器去用很多数据波动根本不用纠结。如果有条件可以拿一个激光传感器并排放十几分钟根据两者数据做个线性修正把斜率和零漂都改成自己实测的这样可靠性会高一些。6. 常见问题排查与避坑6.1 数值满量程或一直为零时先查什么调试过程中最容易遇到的就是ADC读出来一直是4095或者一直是0。这类问题大多数不是代码问题而是硬件接线和CubeMX配置问题我整理了个排查表。现象大概率原因对策一直读到4095PA0接到超过3.3V的电压共地没做好用万用表量PA0电压确认分压后不超过3.3V一直读到0PA0没配置成ADC输入接线断开CubeMX里确认PA0模式是ADC1_IN0检查传感器供电读数有但很固定传感器Vo线虚接信号被分压拉到GND重新插线焊点上锡上电一瞬间有值后面稳定但偏低ADC采样时间太短采样电容没充满把Sampling Time调到最长档排查时不要只盯着代码。先量传感器各引脚电压Vcc和V-LED是不是5VVo是不是在0.9V到3.6V之间变化。如果Vo没变化问题在传感器侧如果Vo正常但PA0不对问题在分压电路和ADC配置上。6.2 数据跳动大的真凶是供电和采样时序数据乱跳最根本的原因就是供电。红外LED点亮瞬间电流很大如果传感器和STM32共用一根细长的面包板电源线LED一开整条线上的电压都会往下掉Vo自然跟着抖。解决办法是让传感器从5V入口直接取电地线用尽量粗短的线接到STM32系统地的同一个点中间不要串联其他外设。第二个容易忽略的是采样时刻。如果传感器要由LED控制脚触发点亮那GPIO电平切换后不能马上读ADC要延时等到散射光稳定。很多裸传感器数据手册都给出LED点亮后280us左右开始采样、持续40us左右的时序。顺序反了或者间隔不对读到的就是LED点亮前的漏电流数据自然乱七八糟。LED控制脚也不能悬空悬空等于电平不定传感器内部电路可能处于半开半关状态输出毫无意义。6.3 HAL库还是标准库怎么选做这类项目的过程中很快会碰到HAL库和标准库的选择问题。ST官方现在主推STM32CubeMX加HAL库工程由工具生成外设初始化基本不用手写换芯片型号时迁移也方便。我的建议是如果你是新手或者做毕业设计直接学HAL别再去折腾标准库手工建工程了。标准库的优势是代码结构相对直观网上老教程多很多寄存器范例也是标准库风格。但标准库面临前面的问题新出的STM32G0系列等型号标准库支持并不好遇上问题还得自己翻芯片寄存器。你如果只是想尽快把传感器数据跑起来用CubeMX生成HAL工程按这篇的代码去改上手会快很多。7. 进阶玩法定时器控制LED脉冲采样7.1 用DWT实现微秒延时的轮询采样前面说LED控制脚低电平点亮但如果你只把LED控制脚一直拉低LED就是常亮状态。常亮会让红外LED持续发热带来漂移和寿命损耗。规范的做法是按数据手册的时序用脉冲点亮LED点亮后延时280us左右读ADC再延时40us左右熄灭等将近10ms进入下一周期。HAL库自带的HAL_Delay最小精度是1ms根本没法满足280us这种微秒级延时。可以用DWT内核计数器做一个简单的微秒延时函数在F103这些Cortex-M3芯片上很可靠。void dwt_delay_init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } void delay_us_dwt(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks) { } }采样循环大致长这样。LED_PIN_ON(); delay_us_dwt(280); med_value read_adc_raw(hadc1); delay_us_dwt(40); LED_PIN_OFF(); HAL_Delay(9);注意LED控制引脚的低电平逻辑要对照你手上的模块说明书。如果模块是高电平点亮就把ON和OFF反过来。7.2 接OLED、串口屏或LoRa做环境节点数据跑通之后输出端可以玩出很多花样。我在本地接了一块0.96寸的OLED屏显示浓度和对应的空气状态同时把串口数据发给LoRa模块做成多节点环境监测。每一路节点就是一块STM32加红外传感器成本能被压得非常低。最后还有一个小细节传感器放进外壳时进风口和出风口一定要开在风道两端让空气能持续流过采样腔。很多人调试时裸板放在桌面上数据挺好装进盒子后数据就一直偏高就是因为风道被堵住了。红外散射需要空气流动不是放在一个密封壳子里就能测准的。这个坑我踩过两次写在这个位置你可以直接避开。
