ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32红外PM2.5传感器驱动实战:ADC采样、PWM捕获与数据标定

STM32红外PM2.5传感器驱动实战:ADC采样、PWM捕获与数据标定 1. 从一颗传感器说起为什么选择红外PM2.5方案PM2.5监测这两年从专业设备慢慢下沉到了家用和创客圈我最早接触这个需求是帮朋友做一个室内空气质量看板要求成本压到百元以内、能实时刷新、数据还得靠谱。翻了一圈方案激光散射的传感器精度确实好但价格普遍在几十到上百而且对气路结构有要求稍微进点灰数据就飘红外原理的PM2.5传感器价格能压到十几块结构简单驱动也省事特别适合STM32这类MCU直接对接。红外PM2.5传感器的核心原理其实不复杂内部一个红外发射管和一个光电接收管空气从预留的风道流过颗粒物把红外光散射到接收管上散射光强和颗粒物浓度正相关传感器内部把这个光强信号转成PWM占空比或者模拟电压输出。你拿STM32去读这个输出再按厂商给的公式换算一下就能得到浓度值。听起来简单但实际做的时候坑不少——输出信号抖动、温漂、供电噪声、采样时机不对都会让数据没法看。这篇内容我打算把整个链路拆开讲从传感器选型、STM32的GPIO和定时器配置、PWM捕获或者ADC采样的具体实现到数据滤波和标定最后给一份可以直接抄的代码框架。适合正在做STM32环境监测类项目的人也适合拿这个当毕业设计或者课程设计的同学。不管你是刚点亮LED的新手还是已经用过标准库做过几个项目的老手这里面的配置细节和避坑经验应该都能用得上。提示红外PM2.5传感器和激光传感器在数据可信度上差距明显如果你的项目对精度要求高比如需要区分优良中差等级并对外发布建议直接上激光方案红外方案更适合做趋势判断、联动控制比如超标自动开风扇这类场景。2. 硬件选型与电路连接别让供电毁掉数据2.1 红外PM2.5传感器的常见型号与输出类型市面上常见的红外PM2.5传感器典型代表是GP2Y1010AU0F这一类还有各种国产替代型号。它们的输出方式主要分两种一种是模拟电压输出浓度越高电压越高典型范围在0.5V到3.5V之间另一种是PWM输出浓度对应占空比比如低浓度时占空比小高浓度时占空比大。两种方式各有优劣选哪种直接决定你STM32这边怎么配置。模拟输出的好处是接线简单直接进ADC就行缺点是电压信号容易被电源噪声干扰尤其是传感器和MCU共用一路电源的时候电机、继电器一动作ADC读数就跳。PWM输出的好处是抗干扰能力强数字信号只有高低电平噪声容限大缺点是占用一个定时器通道做输入捕获而且PWM频率通常不高几百Hz到几kHz捕获配置要算清楚预分频和重装载值。我个人的建议是如果你板子上有电机、继电器这类大电流负载优先选PWM输出的传感器如果是纯监测、供电干净模拟输出也够用但ADC那边一定要做滤波。2.2 供电与滤波电路一个电容能救活一半的数据红外传感器内部有红外LED工作时会有脉冲电流这个电流会在供电线上产生纹波。如果你直接用STM32板子上的3.3V或者5V给传感器供电纹波会顺着电源线串到MCU的ADC参考电压上读数就会周期性抖动。解决办法很简单在传感器供电引脚旁边并一个100nF陶瓷电容加一个10uF电解电容陶瓷电容滤高频电解电容滤低频两个一起上。另外传感器的红外LED驱动脚有些型号叫LED脚或者IR脚需要周期性开关这个开关信号最好由STM32的一个GPIO或者定时器PWM来驱动而不是直接接电源常亮。常亮会让LED发热、寿命下降而且厂商的标定曲线通常是在特定驱动占空比下测的常亮会导致读数偏移。接线的时候注意几点传感器的VCC和GND要尽量短、尽量粗最好单独走线到电源入口不要和电机共用一段细线模拟输出脚到STM32的ADC输入脚之间如果距离超过5厘米建议加一个RC低通比如1k电阻加100nF电容截止频率大概1.6kHz能滤掉大部分开关噪声。2.3 STM32最小系统与引脚分配我一般用STM32F103C8T6这块最小系统板做这类项目资源够用、价格便宜、资料多。引脚分配大概是这样传感器模拟输出接PA0ADC1_IN0传感器LED驱动接PA1普通GPIO或者TIM2_CH2做PWM串口用PA9和PA10做数据输出方便在电脑上看读数。如果你用的是PWM输出的传感器那就把输出脚接到PA0对应的定时器通道上比如TIM2_CH1配置成输入捕获模式。这里有个细节STM32F103的ADC输入阻抗不是无限大传感器输出阻抗如果比较高比如几十k欧直接接ADC会导致采样保持电容充不满读数偏低。解决办法是在传感器输出和ADC之间加一个电压跟随器用运放搭一个或者选输出阻抗低的传感器型号。我实测过某款国产红外传感器输出阻抗大概10k欧直接接ADC读数比万用表测的低了0.2V左右加了个LM358跟随器之后就一致了。注意STM32的ADC参考电压默认是VDDA如果你用USB供电VDDA大概在3.3V左右但会有波动。如果对精度要求高建议用外部基准芯片比如REF3033或者至少在软件里做一次VDDA校准。3. STM32端配置ADC采样与PWM捕获的实操细节3.1 ADC采样配置时钟、通道与采样时间STM32F103的ADC时钟最高不能超过14MHz系统时钟72MHz的话ADC预分频要设成6分频得到12MHz。这个细节很多人会忽略ADC时钟超了会导致转换结果不准甚至死机。在标准库里的配置大概是RCC_ADCCLKConfig(RCC_PCLK2_Div6)然后ADC_InitStructure.ADC_ContinuousConvMode设成DISABLE用单次转换模式每次要读的时候手动触发。采样时间的选择很关键。红外传感器输出阻抗高采样时间太短的话采样保持电容充不满。STM32F103的ADC采样时间可选1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个ADC时钟周期。我一般选55.5或者71.5给电容足够的充电时间。实测下来采样时间从7.5改成71.5读数稳定性提升很明显波动从±0.1V降到±0.02V。通道配置方面如果你用PA0那就是ADC_Channel_0规则组里放一个通道就行。转换完成后读ADC_GetConversionValue得到0到4095的值再换算成电压电压 值 * 3.3 / 4096。这个电压再根据传感器手册的公式换算成浓度比如某型号的公式是浓度 (电压 - 0.5) * 200单位是微克每立方米。3.2 多次采样与软件滤波让数据能看单次ADC采样出来的数据基本没法直接用抖动太大。我通常的做法是连续采16次去掉最大和最小的各2个剩下的12个求平均。这个算法在STM32上跑起来很快16次采样加上排序和平均72MHz的核大概几十微秒就完事了。如果你用DMA加定时器触发还可以做成后台自动采样主循环只管读结果。除了去极值平均还可以加一阶低通滤波新值 旧值 * 0.9 当前值 * 0.1。这个系数根据你的刷新率调刷新率10Hz的话0.9对应的时间常数大概1秒数据会平滑很多但响应会变慢。如果要做超标报警建议用去极值平均加一个滑动窗口窗口大小取10到20个样本既能平滑又能及时响应。有个坑要注意ADC采样的时候如果CPU在干别的事比如刷OLED或者跑串口中断采样结果可能会受干扰。我试过在ADC转换期间关中断数据会稳一点但会影响串口接收。折中方案是用DMA搬运ADC结果CPU不参与转换过程干扰最小。3.3 PWM输入捕获测占空比的正确姿势如果你用的是PWM输出的传感器那就需要配置定时器输入捕获。以TIM2_CH1为例PA0接传感器输出配置成浮空输入或者上拉输入定时器设成PWM输入模式。具体步骤是TIM_ICInitStructure.TIM_Channel设成TIM_Channel_1TIM_ICPolarity设成TIM_ICPolarity_RisingTIM_ICSelection设成TIM_ICSelection_DirectTI然后调用TIM_PWMIConfig它会自动把通道1和通道2配成上升沿和下降沿捕获。捕获到之后TIM_GetCapture1得到周期TIM_GetCapture2得到高电平时间占空比 高电平时间 / 周期。这里有个细节TIM_PWMIConfig会把两个通道都配好你只需要读CCR1和CCR2就行。预分频和重装载值要根据传感器PWM频率来算比如传感器输出1kHz定时器时钟72MHz预分频设成71得到1MHz计数频率重装载值设成65535这样1kHz信号的周期是1000个计数分辨率足够。实测中我发现有些传感器的PWM输出边沿不够陡尤其是长线传输的时候上升沿可能几百纳秒这时候捕获到的周期会有误差。解决办法是在传感器输出和STM32之间加一个施密特触发器比如74HC14把边沿整形之后再进定时器。另外输入捕获的中断优先级不要设太高否则频繁中断会影响主循环用DMA或者定时器从模式自动复位的方式可以减少中断次数。4. 数据换算与标定从电压到浓度的最后一公里4.1 厂商公式与实际偏差传感器手册一般会给一个换算公式比如浓度 (Vout - Voffset) * K其中Voffset是洁净空气下的输出电压K是灵敏度系数。但实际用的时候你会发现这个公式在低浓度段偏差很大因为红外传感器的低浓度分辨率有限而且Voffset会随温度和湿度漂移。我实测某款传感器手册给的Voffset是0.5V但在25度、50%湿度下实测是0.6V差了0.1V换算成浓度就是20微克每立方米的误差。解决办法是做一次现场标定把传感器放在洁净空气里比如刚下过雨的室外或者用HEPA滤网过滤后的空气读一段时间的数据取平均作为Voffset。这个值存在STM32的Flash里每次上电读出来用。如果环境温湿度变化大还可以加一个温湿度传感器比如DHT22或者SHT30做温湿度补偿补偿系数根据实测数据拟合。4.2 分段线性化让低浓度段也能看红外传感器的输出和浓度关系不是完全线性的低浓度段斜率小高浓度段斜率大。如果直接用一条直线拟合低浓度段误差会很大。我的做法是分三段做线性化0到50微克每立方米用一段斜率50到200用一段200以上用一段。每段的斜率和截距通过实测几个点拟合出来比如用香烟烟雾做高浓度标定用洁净空气做零点标定。具体操作是把传感器放在密闭箱里用标准粉尘或者香烟烟雾逐步增加浓度同时用一台参考仪器读数值记录传感器输出电压和参考浓度的对应关系。然后把这些点画在Excel里分段拟合直线得到每段的斜率和截距。STM32里用if-else判断浓度区间套用对应的公式。这个方法虽然土但实测下来比单条直线准很多低浓度段的误差能从±30微克降到±10微克以内。4.3 温度漂移与长期稳定性红外传感器的红外LED发光效率随温度变化接收管暗电流也随温度变化所以输出电压会有温漂。我实测过从20度升到40度Voffset漂了大概0.15V换算成浓度就是30微克每立方米。如果项目要在户外或者温度变化大的环境用温补是必须的。简单做法是在标定时记录不同温度下的Voffset拟合一条Voffset随温度变化的曲线运行时用当前温度查表补偿。长期稳定性方面红外传感器的风道容易积灰积灰之后散射光路改变读数会慢慢偏高。我一般建议每三个月用气吹清理一次风道或者加一个防尘网。软件上可以做一个长期零点跟踪如果连续几个小时读数都在低位徘徊就认为当前是洁净空气把Voffset慢慢往当前读数靠这样能自动补偿一部分积灰和老化。5. 常见问题与排查技巧实录5.1 读数一直为零或者满量程这是最常见的问题先查供电。用万用表量传感器VCC脚看是不是在手册要求的范围内比如5V或者3.3V。如果供电正常再查输出脚电压洁净空气下应该在Voffset附近如果一直是0V或者VCC可能是传感器坏了或者接线反了。还有一种情况是STM32的ADC引脚配置错了比如配成了输出模式或者通道选错了读出来一直是0。如果用的是PWM输出传感器读数一直为零可能是捕获配置有问题。先示波器看传感器输出有没有波形有波形的话再查定时器配置重点看预分频和重装载值是不是把频率算错了。我遇到过把预分频设成0结果定时器时钟72MHz直接计数1kHz的信号周期只有72个计数分辨率太低占空比算出来一直是0或者100%。5.2 数据跳动大像随机数数据跳动大通常有三个原因电源噪声、采样时机不对、传感器本身输出抖动。先查电源用示波器看传感器VCC脚如果有几十毫伏的纹波加电容。再查采样时机如果ADC采样和传感器LED驱动同步了比如LED刚点亮就采样读数会偏低因为LED亮度还没稳定。解决办法是让LED驱动和ADC采样错开比如LED驱动用PWM在PWM高电平中间时刻触发ADC采样。传感器本身输出抖动的话只能靠软件滤波。我一般用去极值平均加一阶低通效果够用。如果还不行检查一下传感器是不是离电机或者继电器太近电磁干扰也会导致读数跳。把传感器挪远一点或者加个屏蔽罩能改善不少。5.3 串口输出乱码或者没数据串口乱码一般是波特率不对STM32和电脑端的波特率要一致常用9600或者115200。如果波特率对了还是乱码检查晶振频率和系统时钟配置标准库的SystemInit默认是72MHz如果你用的是8MHz晶振倍频系数要设对。没数据的话先查串口初始化有没有调用再查发送函数有没有执行可以在发送前翻转一个LED看程序有没有跑到发送那一步。还有一种情况是USB转串口模块的驱动问题电脑端设备管理器里看有没有识别到COM口。如果用的是STM32的USB虚拟串口那配置更复杂需要装VCP驱动而且USB时钟要设成48MHz这个在时钟树配置里容易搞错。5.4 常见问题速查表现象可能原因排查方法解决措施读数恒为0供电缺失或ADC通道错万用表量VCC和输出脚检查接线确认ADC通道读数恒为满量程输出脚短路或传感器损坏断开传感器单独量输出更换传感器检查焊接数据跳动大电源噪声或采样时机不对示波器看VCC纹波加滤波电容错开采样串口乱码波特率或时钟配置错核对波特率和晶振统一波特率检查时钟树PWM捕获值不对预分频或重装载值错示波器看输入波形重算定时器参数低浓度段误差大线性拟合不合适对比参考仪器分段线性化现场标定实操心得我踩过最大的坑是ADC采样时间和传感器输出阻抗不匹配读数一直偏低换了三个传感器才发现是采样时间太短。后来养成习惯拿到新传感器先量输出阻抗再算采样时间省了很多事。6. 完整代码框架与工程组织6.1 工程目录与文件划分我一般把工程分成几个文件main.c放主循环和业务逻辑adc.c和adc.h放ADC初始化和读取函数sensor.c和sensor.h放传感器换算和滤波uart.c和uart.h放串口输出。这样分工明确以后换传感器只需要改sensor.c换MCU只需要改底层驱动。标准库新建工程的时候记得把启动文件、库文件、头文件路径都配好Keil5里在Options for Target的C/C选项卡里加头文件路径在Target里选对晶振频率。如果你用VSCode加PlatformIO或者STM32CubeMX工程组织会更自动一些但底层配置还是建议手动过一遍尤其是时钟树和ADC分频自动生成的代码有时候默认值不是最优的。6.2 ADC读取与滤波函数#define SAMPLE_COUNT 16 u16 adc_read_filtered(u8 ch) { u16 buf[SAMPLE_COUNT]; u8 i, j; u16 temp; u32 sum 0; for (i 0; i SAMPLE_COUNT; i) { ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_71Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); buf[i] ADC_GetConversionValue(ADC1); } // 冒泡排序去掉最大最小各2个 for (i 0; i SAMPLE_COUNT - 1; i) { for (j 0; j SAMPLE_COUNT - 1 - i; j) { if (buf[j] buf[j 1]) { temp buf[j]; buf[j] buf[j 1]; buf[j 1] temp; } } } for (i 2; i SAMPLE_COUNT - 2; i) { sum buf[i]; } return sum / (SAMPLE_COUNT - 4); }这个函数每次调用采16次排序后去掉两端各2个剩下12个求平均。实测下来比单次采样稳定一个数量级。如果你嫌排序慢可以用选择法只找最大最小但16个数据冒泡也就几百个周期72MHz下几微秒的事没必要优化。6.3 浓度换算与串口输出float adc_to_voltage(u16 adc_val) { return (float)adc_val * 3.3f / 4096.0f; } float voltage_to_density(float voltage) { float density; if (voltage 0.6f) { density 0; } else if (voltage 1.0f) { density (voltage - 0.6f) * 125.0f; } else if (voltage 2.0f) { density 50.0f (voltage - 1.0f) * 150.0f; } else { density 200.0f (voltage - 2.0f) * 200.0f; } return density; }换算函数里用了分段线性化0.6V以下认为是00.6到1.0V对应0到50微克1.0到2.0V对应50到200微克2.0V以上对应200以上。这些分段点和斜率是根据实测拟合的你换传感器之后要重新标定。串口输出用printf重定向就行在uart.c里实现fputc然后主循环里每500毫秒打印一次浓度值。6.4 主循环与定时器调度主循环里不要用delay做长时间等待用定时器中断做调度。比如TIM3配成1毫秒中断在中断里计数每500毫秒置一个标志位主循环检测到标志位就采一次数据、算一次浓度、发一次串口。这样主循环不阻塞以后加按键、OLED、报警逻辑都方便。TIM3的配置和TIM2类似预分频72-1重装载1000-1得到1kHz中断频率。如果你用FreeRTOS那就更简单了建一个任务专门采数据vTaskDelay(500)就行。不过对于这种小项目裸机加定时器调度足够了上RTOS反而增加复杂度。7. 项目扩展与实战建议7.1 加OLED显示和报警联动数据采出来之后最直观的展示方式是加一个0.96寸OLEDI2C接口接PB6和PB7。OLED显示当前浓度、等级优、良、轻度污染等、以及一个简单的趋势图。报警联动可以用一个蜂鸣器或者继电器浓度超过阈值就动作。阈值可以设两档比如75微克每立方米蜂鸣器间歇响150微克每立方米继电器断开风扇或者空气净化器。这里有个细节OLED刷新和ADC采样如果都在主循环里做刷新OLED的时候ADC采样可能会受影响。解决办法是把OLED刷新放在定时器中断里或者用DMA刷OLED减少CPU占用。我一般用u8g2库刷一屏大概几毫秒500毫秒刷一次完全够用。7.2 数据上传与远程监测如果需要远程看数据可以加一个ESP8266或者ESP32模块通过串口和STM32通信把数据传到云平台。STM32这边只需要把浓度值格式化成字符串通过串口发给WiFi模块就行。WiFi模块那边用AT指令连路由器、连MQTT服务器这些配置可以存在模块的Flash里上电自动连。要注意的是WiFi模块发送数据的时候电流会突然增大如果和传感器共用电源传感器读数会跳。解决办法是给WiFi模块单独一路LDO供电或者在发送数据的时候暂停ADC采样发完再恢复。我试过用STM32的USB虚拟串口直接连电脑省掉USB转串口模块但USB时钟配置麻烦而且虚拟串口驱动在某些电脑上不稳定量产项目还是用CH340这类专用芯片省心。7.3 低功耗与电池供电如果要做电池供电的便携版本低功耗是重点。STM32F103的低功耗模式有睡眠、停止、待机三种停止模式功耗大概几十微安待机模式几微安。传感器本身功耗也不小红外LED驱动电流大概几十毫安不能一直开着。我的做法是每10秒唤醒一次开LED、采数据、算浓度、发出去然后关LED、进停止模式。这样平均电流能降到1毫安以下一节2000mAh的电池能用几个月。唤醒用RTC闹钟或者外部中断都行RTC闹钟更准但配置麻烦一点。停止模式唤醒后时钟会切回HSI需要重新配置系统时钟这个在标准库里有现成的函数调用SystemInit就行。传感器供电可以用一个MOS管控制不用的时候彻底断电进一步降低功耗。7.4 标定与长期维护的实战体会最后说标定。红外传感器出厂标定是在标准条件下做的实际使用环境千差万别不标定的话数据只能看趋势不能看绝对值。我一般建议至少做一次零点标定和一次跨度标定。零点标定用洁净空气跨度标定用香烟烟雾或者标准粉尘。如果没有参考仪器可以用一个已知精度的激光传感器做对比把两个传感器放在同一个密闭空间里读一段时间的数据拟合出红外传感器的修正曲线。长期维护方面风道清理是必须的我一般三个月清一次。软件上可以做零点自动跟踪但要注意别把真实的高浓度误判成零点漂移。我的做法是只有当读数连续低于某个阈值比如10微克超过1小时才启动零点跟踪而且每次只把Voffset往当前读数靠一点点比如0.01V慢慢修正。这样既能补偿老化又不会把真实数据带偏。这个项目我从最早的单片机读ADC到后来加滤波、标定、OLED、WiFi前后迭代了五六版踩过的坑基本都写在这了。如果你正在做类似的东西建议先把ADC采样和串口输出跑通看到稳定数据之后再往上加功能别一上来就堆一堆模块出了问题不好定位。传感器这东西硬件底子打好了软件稍微滤一滤就能用硬件没弄好软件再怎么滤波都是白搭。
返回列表