☰
STM32红外PM2.5传感器驱动:定时器输入捕获测占空比与浓度换算
2026/9/27 10:52:35 网站建设 项目流程

1. 项目缘起与整体设计思路

1.1 为什么选红外PM2.5传感器而不是激光款

做环境监测类项目,PM2.5传感器基本绕不开两个选择:红外散射式和激光散射式。激光款精度高、能测到0.3微米颗粒,但价格普遍在几十到上百元,而且需要风扇或加热电阻持续工作,功耗和体积都上去了。红外款用的是红外LED加光电接收管,靠颗粒物对红外光的散射强度反推浓度,成本能压到十块钱以内,体积也小,特别适合嵌入式入门项目和批量部署场景。

我这次选红外款,核心原因是给一个室内空气监测节点做原型。节点本身用STM32F103C8T6最小系统板,整机预算控制在五十块以内,还要留出温湿度、显示屏和无线模块的位置。激光传感器光一个就超预算了,红外款虽然精度差一些,但用来判断“空气好不好、要不要开净化器”完全够用。这里要说明一点:红外PM2.5传感器的输出是PWM占空比或者模拟电压,不是直接的数字浓度值,需要STM32做换算,这也是这个项目最核心的技术点。

1.2 整体方案架构与数据流

整个系统的数据流是这样的:红外传感器内部的红外LED发出光束,空气中的颗粒物把光散射到光电接收管上,接收管输出的脉冲信号经过内部电路处理后,从输出引脚给出一个PWM波形。颗粒物浓度越高,散射光越强,PWM的占空比就越大。STM32要做的就是测量这个PWM的高电平时间,算出占空比,再根据传感器手册给出的对应关系换算成微克每立方米。

硬件连接上,传感器一般有四根线:VCC、GND、输出脚、可能还有一个LED控制脚。VCC接5V,因为内部红外LED需要5V驱动,但输出信号是3.3V电平,可以直接接STM32的GPIO或者定时器输入捕获引脚。我选的是PA0,对应TIM2的通道1,用输入捕获模式测占空比。为什么不用外部中断加延时?因为PWM频率大概在1kHz到10kHz之间,用中断测高电平时间会有较大误差,而定时器输入捕获是硬件自动记录边沿时刻,精度能到微秒级,这是最稳妥的做法。

1.3 关键设计决策与取舍

第一个决策是测占空比还是测频率。有些红外传感器输出的是频率变化的信号,浓度越高频率越高。但我手上这款是PWM占空比输出,低浓度时占空比约20%,高浓度能到80%以上。所以必须用输入捕获测占空比,不能测频率。

第二个决策是用标准库还是HAL库。网上很多教程用HAL库,配置起来图形化方便,但HAL库的输入捕获回调机制对初学者不太透明,出了问题不好查。我选标准库,因为寄存器操作直观,中断服务函数里直接读CCR寄存器就行,代码量小,实时性也好。当然如果你习惯HAL库,逻辑是一样的,只是API不同。

第三个决策是滤波策略。红外传感器输出抖动比较大,尤其是空气流动时读数会跳。我用了滑动平均滤波,取最近16次采样值做平均,既能平滑数据又不会太滞后。这个在后面的实操部分会详细说。

2. 硬件连接与传感器原理深挖

2.1 红外PM2.5传感器内部结构解析

红外PM2.5传感器的核心是一个光学腔体。腔体一端是红外LED,另一端是光电三极管,两者之间有一个遮光结构,正常情况下的红外光不会直接照到接收管上。当空气进入腔体,颗粒物经过光路时会把红外光散射到接收管,接收管产生光电流。这个光电流非常微弱,所以传感器内部还有一个运放电路做放大和整形,最终输出一个干净的PWM方波。

这里有个关键点:传感器输出的PWM占空比和颗粒物浓度不是线性关系,而是一条曲线。低浓度区间占空比变化快,高浓度区间变化慢。厂家手册一般会给一个表格,比如占空比20%对应浓度0,50%对应100微克每立方米,80%对应500微克每立方米。实际使用时需要做分段线性插值,不能简单按比例算。我实测下来,用三段折线拟合,误差能控制在正负15%以内,对于红外传感器来说已经不错了。

2.2 STM32最小系统板与传感器接线实操

接线之前先确认传感器引脚定义。我这款是四针接口,丝印分别是VCC、GND、OUT、LED。VCC接5V,GND接GND,OUT接PA0,LED脚悬空或者接3.3V都可以,它是用来控制内部红外LED开关的,不接默认常亮。注意VCC一定要接5V,我试过接3.3V,传感器完全不工作,因为红外LED的正向压降就要1.2V左右,加上内部电路,3.3V驱动不起来。

STM32这边,PA0配置成浮空输入或者上拉输入都行,因为传感器输出是推挽的,不需要外部上拉。但如果你用的传感器输出是开漏的,就必须加上拉电阻,一般4.7k到10k。我建议不管什么情况都在PA0和3.3V之间焊一个10k上拉,成本几分钱,能避免很多莫名其妙的读数问题。

电源部分要特别注意:传感器工作时内部LED是脉冲驱动的,瞬时电流能到几十毫安,如果和STM32共用一路LDO,可能会引起电源波动导致STM32复位。我的做法是传感器单独走一路5V,从USB直接取电,STM32的3.3V由板载LDO提供,两者共地但不共电源轨。实测这样最稳,连续跑72小时没有复位。

2.3 定时器输入捕获模式配置要点

TIM2挂在APB1总线上,STM32F103的APB1默认36MHz,但定时器时钟是72MHz,这个在时钟树配置时要注意。我用的配置是预分频器PSC=71,这样计数器时钟就是1MHz,每个计数代表1微秒。自动重装载值ARR设成0xFFFF,让计数器自由跑,溢出周期约65.5毫秒,足够覆盖PWM周期。

输入捕获配置成PWM输入模式,这是STM32定时器的一个特殊功能,一个通道测周期,另一个通道测高电平时间。具体来说,TIM2通道1配置成上升沿捕获,通道2配置成下降沿捕获,两者映射到同一个引脚PA0。这样硬件自动把周期值存到CCR1,高电平时间存到CCR2,中断里直接读就行,不需要软件反复切换边沿极性。这个模式很多人不知道,用普通输入捕获要写状态机,容易出错。

中断优先级方面,TIM2捕获中断设成中等优先级,不要太高,因为PM2.5数据变化慢,每秒采一次都够,没必要抢占其他任务。我设的抢占优先级2,子优先级0,和串口中断错开就行。

3. 代码实现与核心算法拆解

3.1 标准库工程搭建与定时器初始化

先建一个标准库工程,把启动文件、系统文件、外设驱动都加进去。我习惯用Keil5,器件包选STM32F10x_StdPeriph_Lib。新建工程后,在main.c里先写RCC配置,使能GPIOA和TIM2时钟。注意APB1外设要开AFIO时钟吗?这里不需要,因为没用重映射。

定时器初始化代码大概长这样:

void TIM2_Capture_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler = 71; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0F; TIM_PWMIConfig(TIM2, &TIM_ICInitStructure); TIM_SelectInputTrigger(TIM2, TIM_TS_TI1FP1); TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_Reset); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); TIM_Cmd(TIM2, ENABLE); }

这里TIM_PWMIConfig是关键,它会自动把通道2配成相反的极性。TIM_ICFilter设成0x0F,滤波强度最大,能滤掉传感器输出上的毛刺。TIM_SelectSlaveMode设成复位模式,这样每次上升沿到来时计数器自动清零,CCR1里就是周期值,CCR2里就是高电平时间,读出来的数直接就是微秒数,省去了减法运算。

3.2 中断服务函数与占空比计算

中断服务函数里要做的事情很简单:读CCR1和CCR2,算占空比,存到全局变量,清中断标志。

volatile uint32_t g_period = 0; volatile uint32_t g_high = 0; volatile float g_duty = 0.0f; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET) { g_period = TIM_GetCapture1(TIM2); g_high = TIM_GetCapture2(TIM2); if (g_period > 0) { g_duty = (float)g_high / (float)g_period; } TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } }

注意g_period和g_high要声明成volatile,因为它们在中断里被修改,主循环里读取,编译器优化时可能会把它们缓存到寄存器导致读不到新值。这个坑我踩过,调试了半天才发现是编译器优化的问题。

占空比算出来是0到1之间的小数。实测传感器在洁净空气中占空比约0.18到0.22,对应浓度0到10微克每立方米。随着浓度升高,占空比逐渐增大,到500微克每立方米时约0.78。这个关系不是线性的,需要做曲线拟合。

3.3 浓度换算算法与滑动平均滤波

浓度换算我用的是分段线性插值。根据手册和实测数据,我定了几个关键点:

占空比浓度(微克每立方米)
0.200
0.3550
0.50100
0.65250
0.78500

代码里写一个查表函数,找到占空比落在哪个区间,然后线性插值。比如占空比0.42,落在0.35和0.50之间,浓度就是50加上(0.42-0.35)/(0.50-0.35)乘以50,约73微克每立方米。

float duty_to_concentration(float duty) { const float duty_table[] = {0.20f, 0.35f, 0.50f, 0.65f, 0.78f}; const float conc_table[] = {0.0f, 50.0f, 100.0f, 250.0f, 500.0f}; uint8_t i; if (duty <= duty_table[0]) return 0.0f; if (duty >= duty_table[4]) return 500.0f; for (i = 0; i < 4; i++) { if (duty >= duty_table[i] && duty < duty_table[i+1]) { float ratio = (duty - duty_table[i]) / (duty_table[i+1] - duty_table[i]); return conc_table[i] + ratio * (conc_table[i+1] - conc_table[i]); } } return 0.0f; }

滑动平均滤波用一个长度为16的环形缓冲区,每次新数据进来就替换最旧的数据,然后求平均。这样既能平滑抖动,又不会引入太大延迟。16次采样在1秒内完成,平均后每秒更新一次浓度值,对于空气质量监测来说响应速度足够了。

#define FILTER_LEN 16 float filter_buf[FILTER_LEN] = {0}; uint8_t filter_idx = 0; float filter_update(float new_val) { float sum = 0; filter_buf[filter_idx] = new_val; filter_idx = (filter_idx + 1) % FILTER_LEN; for (uint8_t i = 0; i < FILTER_LEN; i++) { sum += filter_buf[i]; } return sum / FILTER_LEN; }

这里有个细节:缓冲区初始全是0,前16次采样的平均值会偏低。我的做法是上电后先连续采样16次填满缓冲区,再开始正常输出。或者你也可以在初始化时把缓冲区全部填成第一次采样的值,这样启动就稳定。

4. 调试过程与常见问题排查

4.1 串口打印调试与数据观察

调试阶段我强烈建议把数据通过串口打印出来看。STM32F103的USART1接PA9和PA10,配置成115200波特率,用printf重定向。注意要勾选Keil的Use MicroLIB,否则printf会卡死。重定向代码:

int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); return ch; }

打印格式我习惯用printf("duty=%.3f, conc=%.1f\r\n", g_duty, concentration);,这样在串口助手里能直接看到占空比和浓度的变化。实测在室内正常环境下,占空比稳定在0.21左右,浓度显示0到5之间跳动。点一根烟靠近传感器,占空比几秒内升到0.6以上,浓度显示300多,响应非常明显。

4.2 常见问题速查表

现象可能原因排查方法解决方法
占空比始终为0传感器未供电或接线错误万用表测VCC是否为5V检查接线,确保VCC接5V
占空比始终为1输出脚悬空或短路测OUT脚电压检查PA0连接,加上拉电阻
读数跳动大电源纹波或气流干扰示波器看电源纹波传感器单独供电,加滤波电容
浓度值偏高传感器未预热观察上电后读数变化预热3到5分钟再读数据
中断不触发定时器未使能或优先级配置错误检查TIM_Cmd和NVIC配置使能定时器和中断,检查优先级
编译报错undefined标准库文件未添加检查工程文件列表添加stm32f10x_tim.c等驱动文件

4.3 独家避坑经验分享

第一个坑:传感器预热。红外PM2.5传感器上电后需要预热,内部红外LED要稳定工作温度。我一开始上电就读数,发现浓度显示80多,以为空气差,结果等了五分钟降到10以下。所以正式使用前一定要预热,我一般等3分钟。

第二个坑:气流影响。传感器进气口如果对着风扇或者空调出风口,读数会剧烈波动。我的做法是在传感器外面套一个小的防风罩,用海绵或者无纺布做一个缓冲腔,让空气缓慢扩散进去。实测加罩后读数稳定性提升明显,波动从正负30降到正负5以内。

第三个坑:定时器溢出。虽然ARR设了0xFFFF,溢出周期65毫秒,但PWM周期一般只有1到2毫秒,不会溢出。但如果传感器故障输出低电平,计数器会一直累加到溢出,CCR1读到的是溢出后的值,算出来的占空比就乱了。我在中断里加了一个判断:如果g_period大于10000(即10毫秒),就认为信号异常,丢弃这次数据。这个保护很有必要。

第四个坑:Keil编译优化。前面提到过volatile的问题,这里再强调一次:所有在中断和主循环之间共享的变量,必须加volatile。我有个朋友没加,调试了一整天,最后发现是编译器把变量优化到寄存器了。另外,如果开了最高优化等级,有些延时循环会被优化掉,建议调试阶段用Level 0或Level 1。

5. 功能扩展与项目延伸方向

5.1 加OLED显示屏做本地显示

有了浓度数据,最自然的扩展就是加一个0.96寸OLED,用I2C接口,接PB6和PB7。显示内容可以包括当前浓度、平均值、最大值,还可以画一个简单的柱状图。OLED驱动用SSD1306,网上库很多,移植过来改一下I2C引脚就行。注意OLED刷新不要放在中断里,主循环里每500毫秒刷一次就够了,否则I2C通信会阻塞中断。

5.2 通过无线模块上传数据

如果要组网监测,可以加一个无线模块,比如常见的2.4G或者433M模块,通过串口和STM32通信。STM32把浓度数据打包成固定格式的帧,比如0xAA 0x55 浓度高字节 浓度低字节 校验和,然后发给无线模块。接收端再用另一个STM32或者USB转串口模块接收,上位机用Python或者串口助手解析。这里要注意无线模块的供电,有些模块发射瞬间电流能到100毫安,最好单独供电或者加一个大电容。

5.3 做成带报警功能的空气质量监测仪

加一个有源蜂鸣器和两个LED,浓度超过75微克每立方米时黄灯亮,超过150时红灯亮加蜂鸣器间歇报警。报警阈值可以通过按键调整,用两个按键分别做加和减,长按进入设置模式。这个功能适合放在卧室或者办公室,提醒及时通风。代码上就是加一个状态机,主循环里轮询按键,根据浓度值控制GPIO输出。

5.4 低功耗优化思路

如果要做电池供电的版本,低功耗是必须考虑的。STM32F103本身功耗不算低,但可以通过降低主频、关闭外设、使用停止模式来省电。传感器也可以间歇工作,比如每10秒采样一次,采样时给传感器供电,采完就断电。这样平均电流能降到毫安级,用一节18650电池能跑好几天。不过要注意传感器每次上电都需要预热,间歇工作会导致读数不准,所以这个方案适合对精度要求不高的场景。

6. 个人实操体会与建议

这个项目我从画板子到调通代码大概花了两个晚上,硬件焊接半小时,代码调试一个半小时。最大的感受是红外PM2.5传感器虽然便宜,但要想读准确实要花点心思。手册上的曲线只是参考,实际每个传感器都有差异,最好自己用标准仪器标定一下。我没有专业仪器,就用了一个已知精度的激光传感器做对比,在几个浓度点上记录占空比,重新拟合了曲线,精度提升了不少。

另外,STM32的定时器输入捕获功能非常强大,PWM输入模式更是测占空比的利器。很多人用外部中断加定时器延时来测,代码复杂还容易出错。建议直接上PWM输入模式,配置一次就稳定工作。如果以后要测频率或者编码器信号,也是同样的思路,用定时器的硬件捕获功能,不要用软件轮询。

最后说一个调试小技巧:如果手头没有示波器,可以用STM32自己的ADC去采传感器输出脚,虽然PWM信号用ADC采不准,但能看到大概的波形和频率,判断传感器有没有输出。或者用另一个定时器做频率计,也能辅助判断。总之,嵌入式调试就是多动手、多观察、多记录,数据不会骗人。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询