
做课程设计或者DIY一台家用空气质量监测小盒子红外 PM2.5 传感器 STM32 是非常常见的一套组合。这类方案的核心器件大多是夏普 GP2Y1010 系列十几块钱一块成品模块引出电源、地、模拟输出和 LED 控制四根线看起来把 STM32 的 ADC 引脚接上就能读数。但等你真正开始调就会发现读数跳动、数据偏大、跟参考仪器对不上这类问题一个接一个。这篇文章按我实际调通一块 STM32F103 GP2Y1010 模块的完整过程来写从红外散射原理、硬件接线、ADC 采样时序到滤波换算、校准方法和最后踩过的几个坑尽量把每一步为什么这么做讲清楚适合正在做课程设计或者想低成本攒一台 PM2.5 监测仪的同学直接参考。1. 红外散射测尘的基本盘GP2Y1010 这类传感器是怎么输出浓度的1.1 红外散射法的物理基础红外 PM2.5 传感器的测量原理是光散射。传感器内部有一个很小的检测腔红外发光二极管朝腔内发射一束光光电二极管则放在一个特定角度上接收被颗粒物散射回来的光。空气中的颗粒物经过光路时散射光的强度会随颗粒浓度、粒径和折射率变化于是通过测量接收管的光电流就能反推出空气中的粉尘浓度。这个“特定角度”很关键。GP2Y1010 的发射管和接收管是错开布置的中间还有挡光结构干净空气时红外光不会直接照射到接收管输出是一个较低的基线电压。一旦腔体内有颗粒物散射光才会进入接收管输出电压升高。浓度越高落在接收管上的散射光越强电压越高。整个过程就是一台微型浊度仪只是测的对象从液体换成了空气。用生活化一点的说法就像手电筒在黑屋子里照一束光你站在侧面看光柱屋里灰尘越多光柱越明显。传感器里那束红外光就是手电筒光电二极管就是站在侧面的你。这个比喻能帮你理解为什么传感器“怕可见光干扰”——如果外界强光直接照进检测腔相当于屋子里开了大灯手电筒的光柱再亮也淹没在背景光里了。1.2 常见型号与引脚定义GP2Y1010AU0F 是这颗芯片最经典的型号后来还有 GP2Y1014AU0F规格基本一致。芯片本体带一个可开合的外壳里面就是检测腔体外观是一个带进气口的小方块。裸芯片一共 6 个引脚含义如下引脚名称说明1VLEDLED 电源5V2LED-GNDLED 地3LEDLED 控制端低电平点亮4S-GND系统地5Vo模拟电压输出6Vcc系统电源5V如果你买的是裸芯片必须用一个三极管或者 MOSFET 去切换 VLED 的通断LED 端接 GPIO 控制而市面上绝大多数成品模块已经把开关管做进去了引出 VCC、GND、AOUT、LED 四个脚直接用 STM32 的 GPIO 控制 LED 引脚即可不需要外加驱动。模块引脚对应芯片引脚接 STM32VCC6 Vcc 1 VLED5VGND2 LED-GND 4 S-GNDGNDAOUT5 VoPA0ADC 输入LED3 LED经板载三极管PA1GPIO 输出买模块之前一定要看卖家的引脚定义和电平要求有些模块把 LED 控制端做成了高电平有效跟数据手册正好相反接反了表现就是数据完全反向或者根本没有响应。1.3 为什么红外传感器只能算“浓度计”不是真正的 PM2.5 计数器这里必须先泼一盆冷水GP2Y1010 测的不是严格意义上的“粒径小于 2.5 微米的颗粒物浓度”。它的散射信号是检测腔内所有颗粒的综合响应直径 0.3µm 到 10µm 的粒子都会贡献信号而且大颗粒的散射强度远大于小颗粒。也就是说它本质上是一个总粉尘浓度指示器拿来当 PM2.5 的“近似值”可以拿来当科研级 PM2.5 数据源不行。后来的激光 PM2.5 传感器比如 PMS5003、SDS011内部有激光管和光电探测器阵列能逐个粒子数数按粒径分桶再换算质量浓度那才是真正的 PM2.5 传感器。但红外方案便宜、模块化、功耗低在很多课程设计和低成本空气质量监测项目里够用。后面要讲的换算公式本质上都是把散射电压线性映射到 PM2.5 浓度标称值这个映射必须经过校准才有一点可信度。所以这篇文章里的“浓度”两个字你心里要有数它是在特定校准条件下对 PM2.5 的一种近似。2. 硬件连线与供电先排除五个百分百会踩的坑2.1 引脚连接与电平匹配典型的模块接线就是把四根线按上面的表格接好。这个环节有两个容易翻车的点值得展开说。第一个是 AOUT 的输出电压范围。5V 供电下 GP2Y1010 在干净空气时输出约 0.9V粉尘浓度到 0.5mg/m³ 时可以到 3.4V 左右。室内正常环境一般到不了这么极端所以很多人直接接 STM32 的 3.3V ADC 用也确实能跑。但遇到烟尘环境、传感器个体差异或者模块放得离污染源太近输出超过 3.3V 会导致 ADC 读数被削顶甚至逼近引脚绝对最大额定值。稳妥的做法是加分压电阻把 Vo 经过两个电阻分压后再进 ADC。比如 10kΩ 串联加 10kΩ 对地信号减半然后换算浓度时把电压先乘回 2。这样虽然 ADC 的低端分辨率损失一半但安全系数高很多。第二个是 GPIO 的电平匹配。模块的 LED 控制端是低电平有效PA1 用推挽输出拉低即可点亮红外 LED。这个引脚开关频率不高STM32 的 3.3V 逻辑电平对模块来说完全够用。要避免的是把 LED 端直接接 5V 或者悬空——悬空状态下模块内部可能有上拉或下拉导致 LED 常亮或常灭数据完全错乱。2.2 供电与去耦电容GP2Y1010 的电源参数里有两个关键点Vcc 和 VLED 都要 5V ± 0.5VLED 脉冲电流峰值大约 320mA。320mA 是脉冲电流周期 10ms 里只有 0.32ms 导通平均电流并不大但如果你的 5V 来自 STM32 板载 LDO 或者 USB 口瞬间压降就会很明显。LED 亮度一旦波动同一个浓度下输出信号就会跟着抖。所以供电原则是别跟电机、继电器、舵机共用电源别用 USB 口直接怼。我给模块单独加了一个 5V 的 LDO比如 ME6211 或者 AMS1117-5.0输入用 7-12V 适配器输出专门给传感器。在 Vcc 和 VLED 的引脚附近分别放 100µF 电解电容和 0.1µF 陶瓷电容。别小看这两个电容脉冲电流靠的就是它们兜底。如果还想更讲究可以在 VDDA 和 VSSA 之间加一个 10Ω 电阻串联 1µF 电容的 π 型滤波把 ADC 参考电源和数字电源隔开。2.3 采样时序为什么必须在 LED 点亮 0.28ms 后读数这是整个项目里最重要的一个“为什么”。红外 LED 不会瞬间把电流全部转换成光光电二极管接收散射光后输出电压也需要建立时间。GP2Y1010 数据手册推荐的脉冲时序是周期 10ms脉冲宽度 0.32ms在 LED 点亮后约 0.28ms 处采样。用文字描述这段时间线0msLED 控制端拉低LED 导通红外光开始照射0ms 到 0.28ms散射光逐渐建立接收管输出电压快速上升0.28ms输出基本稳定此刻采样得到有效值0.32msLED 关闭输出开始回落10ms进入下一个周期。如果你不按这个时序来比如把 LED 常亮或者随机时刻采样会发现两个问题。第一是采样太早输出还在上升段读数偏低而且上升速率受环境光、传感器个体差异影响很大数据的重复性很差。第二是采样太晚LED 已经熄灭输出开始回落读数同样不稳定。把采样点卡在脉冲后段并稳定触发是后面所有滤波算法能生效的前提。我见过不少人一上来就上卡尔曼滤波结果数据还是飘回头一看采样点都在乱跳。3. 数据链路设计从 ADC 原始值到 μg/m³ 浓度3.1 STM32 ADC 配置要点如果用 STM32F103ADC1 是 12 位量化步长 3.3V/4096 ≈ 0.8mV。这个分辨率对 GP2Y1010 是够用的毕竟传感器本身的信号噪声就不小。我这里用单通道单次转换关键配置如下参考电压VREF 接 VDDA也就是 3.3V采样时间给 ADC 设最大采样周期 239.5 周期。因为传感器输出内阻不低如果你再加了分压电阻源阻抗会进一步变大采样时间太短会导致内部采样电容没充满读数系统性偏高转换触发先用软件触发紧跟在我规定的 0.28ms 采样时刻点执行逻辑简单可读性好。如果你用 CubeMX配置 ADC1 的 IN0不要开连续转换用单次转换模式。每次读值时执行 HAL_ADC_Start PollForConversion HAL_ADC_Stop 的经典组合稳定不踩坑。另外在 ADC 引脚到地之间放一个 100nF 陶瓷电容能有效吸收传感器输出端的瞬态毛刺也会让采样电压更稳定。注意这个电容和分压电阻之间有一个时间常数RC 不能太大否则电压建立速度会跟不上 10ms 的脉冲周期。3.2 电压-浓度换算公式的前世今生拿到 ADC 原始值后第一步还原成电压V ADC × 3.3 / 4096如果是 5V 供电、未加分压电路夏普数据手册给出的近似关系是干净空气输出约 0.9V灵敏度约 0.5V / (0.1mg/m³)。也就是说C(mg/m³) ≈ (V - 0.9) / 5换算成微克每立方米C(µg/m³) ≈ 200 × (V - 0.9)举个具体例子假设读到的 ADC 原始值是 1200那么V 1200 × 3.3 / 4096 ≈ 0.967V代入公式 C 200 × (0.967 - 0.9) ≈ 13.4µg/m³。这个数在室内算合理。但注意这个公式里的 0.9V 是典型值每一颗传感器的个体差异能达到 ±0.15V。如果你直接抄公式碰到零点在 1.0V 的传感器计算值就会是负数——很多人的数据出现大段 0就是这个原因。数据手册标称的灵敏度也只是近似值和真实颗粒物成分、粒径分布有关。所以我把这个公式定位成“初次上电能出数”的临时换算真正可用的读数必须做校准具体做法在第 5 节。如果加了分压电阻设分压系数为 K R2 / (R1R2)那就先反推传感器原始电压 V_sensor V_adc / K再代入公式。代码里我会放一个 VOLTAGE_DIVIDER_K 宏方便你直接改。这里还要提醒一下网上很多代码写density 0.17 × voltage - 0.1那是有人对着自己那块传感器做过线性拟合后的经验公式不是通用公式。直接抄会导致你的数据比别人高一大截或低一大截。最靠谱的做法是拿自己的传感器重新做两点标定。3.3 滑动平均与去极值滤波传感器原始读数在稳定环境里也会有波动因为颗粒物在腔体内是随机经过的瞬时散射强度天然有涨落。这是物理噪声不是电路问题。常见处理是滑动平均维护一个 N 点的队列每来一个新样本丢掉最旧的取平均。N 怎么选我实测过GP2Y1010 在室内稳定环境下单次读数的标准差大约在 10% 到 15% 左右。取 N10 能显著压低噪声N20 更平滑但同时响应时间会被拉长。我的做法是 N10配合去极值队列里剔除一个最大值和一个最小值再求平均这样能干掉偶发的灰尘团引起的尖峰。滑动平均是处理这类“随机噪声”性价比最高的手段。卡尔曼滤波在这个场景当然也能用但参数调起来麻烦对课程设计来说属于用力过猛。先把时序和滑动平均做好读数的稳定性已经足够交差了。4. 完整代码实现基于 STM32F103 HAL 的工程模板先说清楚整套代码的思路TIM3 产生 10ms 周期中断每次中断进入采样状态机DWT 提供 280µs 和 40µs 的微秒延时ADC 单次转换读到原始值主循环里用轮询拿浓度、打印串口。这个结构简单清晰适合直接改到自己的工程里。4.1 DWT 微秒延时与引脚初始化HAL_Delay 的精度只有 1ms给不了微秒级所以用 Cortex-M3 内核的 DWT 计数器做微秒延时。初始化一次即可void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; DWT-CYCCNT 0; } void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks); }注意 SystemCoreClock 要和实际主频一致。如果你的工程把主频改到 48MHz或者用了内部 RC 而 CubeMX 里没同步改延时就会偏。我这里是 72MHz 外部晶振。引脚初始化用标准 HALPA0 配成 ADC 输入PA1 配成推挽输出ADC_HandleTypeDef hadc1; void GPIO_Init_Dust(void) { __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio {0}; gpio.Mode GPIO_MODE_ANALOG; gpio.Pin GPIO_PIN_0; HAL_GPIO_Init(GPIOA, gpio); gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Pin GPIO_PIN_1; gpio.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, gpio); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // LED 默认熄灭 } void ADC1_Init(void) { __HAL_RCC_ADC1_CLK_ENABLE(); hadc1.Instance ADC1; hadc1.Init.ScanConvMode DISABLE; hadc1.Init.ContinuousConvMode DISABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; HAL_ADC_Init(hadc1); ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_0; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); }4.2 定时器调度采样TIM3 每隔 10ms 进一次中断在中断里完成一次完整的“点亮 LED → 延时280µs → 读 ADC → 熄灭 LED”流程。CubeMX 里 TIM3 的预分频设 71、自动重载值设 9999这样定时器输入时钟是 1MHz溢出周期正好 10ms。void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { uint16_t adc_raw; if (htim-Instance TIM3) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // LED 点亮 delay_us(280); // 等待光路稳定 HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); adc_raw (uint16_t)HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // LED 熄灭 delay_us(40); // 补足 0.32ms 脉宽 dust_raw_buf_push(adc_raw); // 送入环形缓冲 } }中断服务函数里做 280µs 微秒延时不是不能接受但注意把 TIM3 的中断优先级设置成普通级别别嵌套在急停、通信这类高优先级中断里太久。每次中断占用大约 320µs 的时间去完成采样也就是 3.2% 的 CPU 占用对课程设计来说完全没问题。如果你跑 FreeRTOS 并且有严格实时任务就不建议在中断里这么干可以考虑用定时器 PWM 输出驱动 LED再用另一个定时器触发 ADC改成纯中断异步方式不过复杂度会高不少。主函数里记得启动定时器中断HAL_TIM_Base_Start_IT(htim3);4.3 滤波与换算封装环形缓冲和滤波单独封装队列长度设 10。每次采样中断把原始 ADC 值丢进队列主循环再调函数取浓度#define DUST_FIFO_LEN 10 #define VOLTAGE_DIVIDER_K 1.0f // 无分压时 1.0加分压电阻时改成 2.0 static uint16_t fifo[DUST_FIFO_LEN]; static uint8_t fifo_idx 0; static uint8_t fifo_cnt 0; void dust_raw_buf_push(uint16_t raw) { fifo[fifo_idx] raw; fifo_idx (fifo_idx 1) % DUST_FIFO_LEN; if (fifo_cnt DUST_FIFO_LEN) fifo_cnt; } float dust_read_ugm3(void) { uint32_t sum 0; uint16_t sorted[DUST_FIFO_LEN]; uint8_t valid (fifo_cnt DUST_FIFO_LEN) ? fifo_cnt : DUST_FIFO_LEN; if (valid 0) return 0.0f; memcpy(sorted, fifo, sizeof(sorted)); // 冒泡排序找最大值最小值N10 无所谓 for (uint8_t i 0; i DUST_FIFO_LEN - 1; i) { for (uint8_t j i 1; j DUST_FIFO_LEN; j) { if (sorted[i] sorted[j]) { uint16_t t sorted[i]; sorted[i] sorted[j]; sorted[j] t; } } } uint8_t start 1; // 去掉最小 uint8_t end valid - 1; // 去掉最大 for (uint8_t i start; i end; i) sum sorted[i]; float avg_raw sum / (float)(end - start); float voltage avg_raw * 3.3f / 4096.0f * VOLTAGE_DIVIDER_K; // 无分压、5V 供电场景的临时公式校准后替换 float density (voltage - 0.9f) / 5.0f; // mg/m³ if (density 0.0f) density 0.0f; return density * 1000.0f; // µg/m³ }排序那一步其实可以改成线性扫描找最大最小值但 N10 差别不大用排序代码反而好读。滤波后的数据是过去 100ms 的平均已经相当平滑。很多教程会在这个基础上再叠一层 5 秒平均那是给显示刷新用的看你自己需求。4.4 串口输出与显示对接主循环里每秒打印一次char buf[32]; while (1) { float pm dust_read_ugm3(); snprintf(buf, sizeof(buf), PM2.5: %.1f ug/m3\r\n, pm); HAL_UART_Transmit(huart1, (uint8_t*)buf, strlen(buf), 100); HAL_Delay(1000); }接 OLED 就把 pm 变量直接丢给显示函数。需要上报云平台时把这段数据整理成 JSON 或简单协议帧通过 ESP8266 或 4G 模块转发。整个项目的数据流就是“TIM3 定时采样 → 滑动平均滤波 → 主循环换算 → 外设输出”改动起来非常方便。要注意的是如果你给 ADC 加分压电阻记得改 VOLTAGE_DIVIDER_K否则浓度会系统性偏小一半。5. 校准实操没有参考设备时怎么把读数调到能用的程度5.1 零点标定HEPA 滤网法我没有专业粉尘发生装置所以用的是最土但最有效的偏置标定法。找一个大号密封袋把传感器和一个小风扇放进去袋口夹一块 HEPA 滤网让风扇转几分钟袋内空气反复通过滤网过滤颗粒物浓度接近零。此时记录电压值 V0这就是这颗传感器的零点。注意密封袋不能完全封死留一个小口让气流可以缓慢补充否则风扇会憋住。过滤时间至少三五分钟读数稳定后取平均值。V0 大概率不是数据手册的 0.9V可能在 0.78V 到 1.02V 之间这取决于个体差异和光路污染程度。把这个 V0 代入公式 C (V - V0) / S就先把“负值一大片”的问题解决了。5.2 两点线性回归用一支激光传感器做参照光有零点还不够斜率 S 也得标。最现实的办法借一支激光 PM2.5 传感器比如 PMS5003或者直接用家用的空气检测仪当标准表。把红外传感器和标准表放在同一个空间位置制造几个不同浓度场景场景操作浓度区间参考低浓度开窗通风10-30 µg/m³中浓度关窗静置40-100 µg/m³高浓度点一小段无烟香轻轻扇到进气口150-300 µg/m³每组同步记录红外传感器的输出电压和标准表的浓度值多取几个平衡点最后用最小二乘拟合一条直线。比如我那次拿到的拟合结果是 C 190 × (V - 0.83)和手册的经验公式接近但零点差了不少。如果没条件做完整回归至少把公式里的 0.9 替换成实测零点斜率保留下册经验值的 5V/(mg/m³)误差也会小很多。注意制造污染时不要对着传感器猛吹烟红外散射模型在大浓度下会饱和拟合点全堆在低浓度区间反而更实用。测量时要保证两个传感器进风口在同一空间位置不要把标准表放窗边、红外模块放墙角那样拟合出来全是乱的。5.3 温湿度、气流和位置对测量的影响湿度是一个隐形杀手。颗粒物吸湿后体积变大散射截面明显增强同一个质量浓度下读数会偏大。这就是为什么空气湿度 90% 时的数据普遍比湿度 30% 时高。如果只是室内监测可以在代码里做粗略的湿度补偿比如 C_corrected C / (1 0.02 × (RH - 50))但这个系数完全是经验值精度有限只能算改善方向。课程设计答辩时能讲出这个点反而比一句“我测了误差”要加分。气流位置也一样重要。GP2Y1010 没有内置风扇全靠空气自然扩散进出检测腔响应时间很长。我实测过从一个浓度环境移到另一个环境读数要将近一分钟才能稳定。如果想加快响应可以在靠近检测腔进气口的地方加一个微型轴流风扇但风扇震动会影响光电信号风速也不要太猛避免吹散大颗粒。传感器安装位置要避开阳光直射、电热器附近等强对流和强光环境否则散射信号会被可见光干扰。6. 实测复盘从“读数跳动”到“渐趋稳定”的几个问题6.1 采样点偏移导致的数据抖动第一次画完板子我把 LED 控制脚直接接在 5V 上让 LED 常亮想着“反正一直照着随时读都行”。结果 ADC 读数在 1300 到 2200 之间乱跳根本没法用。后来才意识到这个传感器的光路是脉冲式的输出只在 LED 点亮且散射稳定后才有意义常亮模式下接收管同时收到大量环境背景光信号完全被淹没。改成 10ms 周期加 0.28ms 采样点后数据立刻稳定下来了。这个教训说明时序不是“优化项”而是“必需项”。6.2 电源纹波与共地问题第二次调试数据在低浓度时相对平稳但只要我一给步进电机发脉冲浓度读数就会出现和电机驱动脉冲同频率的尖峰。排查后发现问题出在供电上电机驱动器从同一个 5V 取电GP2Y1010 的 VLED 瞬时电流被拉低红外 LED 亮度随之波动。后来把电机电源单独出来传感器用一个独立 5V LDO 供电并且在模块 Vcc 引脚旁加了 100µF 加 0.1µF 电容尖峰就消失了。另外确认传感器地和 STM32 地是同一个网络并且铺了较宽的铜皮避免地线环路造成共模噪声。6.3 无风扇传感器的时延性格还有一个很多人不提的点GP2Y1010 测的其实是“穿过检测腔空气”的浓度而检测腔容积很小换气全靠扩散分子扩散速度很慢。所以你会发现读数变化总是慢半拍。出门测 PM2.5走到马路边读数要几十秒才往上爬回到室内又要几十秒才回落。如果你拿它去跟手机上显示的实时数据对比总觉得对不上大多数时候其实是时延问题不是传感器坏了。解决办法就是前面说的加个小风扇并且给足稳定时间。如果做比对测试最好让两台设备并排放置等 5 分钟读数稳定后再记录而不是频繁移动设备。最后再说一个购买上的小技巧买模块时优先选带金属屏蔽罩和导气口结构的版本外壳密闭性更好环境光和气流干扰都会小一些。拿到手也别急着焊先按第 5 节的步骤把 V0 测出来写进代码你会发现后面所有数据都顺眼很多。