资讯动态

STM32 ADC采集光敏电阻5516:从分压电路到滤波算法的完整实践

发布时间:2026/10/16 1:21:24 来源:尧图企业网站定制
1. 从光敏电阻到数字世界为什么这个方案值得动手做光敏电阻5516这个型号玩过单片机的人大概率都见过。它便宜、好买、电路简单几乎是用几块钱就能跑通“模拟量采集”全流程的最佳练手器件。但很多人第一次做的时候往往卡在几个很具体的地方ADC读出来的数值飘得厉害、不知道到底该用DMA还是轮询、分压电阻选多大合适、光照强度和ADC值之间到底是不是线性关系。这些问题在教科书里通常一笔带过但在实际调试中会消耗大量时间。这篇文章围绕STM32的ADC外设把光敏电阻5516的采集链路从头到尾拆一遍。从硬件分压电路的设计逻辑到ADC的采样时间、参考电压、分辨率配置再到软件层面的滤波算法和阈值判断最后给出可以直接复现的代码框架。适合刚接触STM32的嵌入式新手也适合想重新梳理ADC采集细节的老手。整套方案不依赖任何特定开发板只要有一颗带ADC的STM32芯片和一只5516光敏电阻就能跟着做下来。我自己的经验是光敏电阻采集看起来简单但它涉及的知识点非常典型模拟前端设计、ADC配置、噪声处理、阈值标定。把这套流程走通一遍以后换成热敏电阻、压力传感器、电位器思路完全一样。所以这个项目值得认真做而不是随便接上线读个数就完事。2. 光敏电阻5516的电气特性与分压电路设计2.1 5516的阻值-光照曲线到底长什么样5516属于硫化镉CdS光敏电阻系列它的核心参数是亮电阻和暗电阻。在标准测试条件下亮电阻10 lux光照大约在5kΩ到10kΩ之间暗电阻完全遮光则可以达到0.5MΩ甚至1MΩ以上。这个跨度非常大意味着它输出的信号动态范围很宽对ADC来说既是好事也是挑战。好事在于明暗变化引起的阻值差异足够大ADC很容易分辨出状态差异。挑战在于这个变化不是线性的。5516的阻值-光照关系近似服从幂函数规律光照每增加一个数量级阻值下降大约一个数量级左右但具体斜率因批次和个体差异而不同。所以如果你打算用ADC值直接换算成勒克斯值那需要逐个体标定否则误差会很大。大多数应用场景其实不需要精确的光照数值只需要判断“亮”和“暗”的阈值这样就简单很多。我在实际使用中习惯先做一件事拿万用表测一下手头这只5516在室内灯光下的阻值再用手遮住测一次。这两个数值能帮你快速确定分压电阻的取值区间比直接查手册更靠谱因为不同批次的5516一致性并不好。2.2 分压电阻选值的取舍逻辑光敏电阻本身是阻值变化的器件STM32的ADC只能测电压所以必须用分压电路把阻值变化转换成电压变化。典型接法是VCC接光敏电阻一端另一端接分压电阻再接地分压点接到ADC输入引脚。或者反过来分压电阻接VCC光敏电阻接地。两种接法的电压变化方向相反但本质一样。关键问题是分压电阻选多大很多人随便拿一个10kΩ就焊上去了结果发现暗光下ADC值几乎不动。原因很简单当分压电阻远小于光敏电阻的暗阻时分压点的电压会被拉得很低ADC读数接近0变化量被压缩了。合理的选值原则是让分压电阻接近光敏电阻在目标光照范围内的几何平均值。5516在室内常见光照下阻值大约在10kΩ到50kΩ之间暗光下可能到200kΩ以上。如果你主要关心的是“有没有光”这个判断分压电阻取10kΩ到20kΩ比较合适。如果你关心的是暗光下的细微变化分压电阻可以取大一些比如47kΩ或100kΩ。我一般会用一个可调电阻先做实验把光敏电阻接好用可调电阻代替固定分压电阻然后在目标光照条件下调节可调电阻用万用表观察分压点电压找到变化最明显的那个阻值再换成同阻值的固定电阻。这个方法虽然土但非常有效。2.3 滤波电容该不该加加多大分压点到ADC引脚之间通常建议加一个小电容到地作用是低通滤波抑制高频噪声。但这个电容不是随便加的。如果电容太大ADC采样时内部采样电容充电时间不够会导致读数偏低。STM32的ADC输入等效为一个采样开关加采样电容采样时间可配置。如果外部电容太大而采样时间太短外部电容上的电荷来不及补充读数就会失真。我的经验是如果ADC采样时间配置在239.5个ADC时钟周期这是STM32F1系列最长的采样时间外部电容可以用到100nF甚至1μF。如果采样时间较短比如7.5个周期外部电容最好不要超过10nF。更稳妥的做法是先用示波器看一下分压点的噪声情况如果噪声不大干脆不加电容靠软件滤波解决。另外光敏电阻本身响应速度不快典型响应时间在几十毫秒量级所以高频噪声主要来自电源和空间耦合而不是光敏电阻本身。电源去耦电容VCC到地加0.1μF比信号线上的滤波电容更重要。3. STM32 ADC配置中那些容易踩坑的参数3.1 参考电压的选择与影响STM32的ADC参考电压决定了量化精度。大多数开发板把VREF直接接到VDDA通常是3.3V这意味着ADC满量程对应3.3V。但实际VDDA可能不是精确的3.3V比如USB供电时可能在3.2V到3.4V之间波动。如果你的应用需要绝对精度最好用外部基准源比如2.5V或3.0V的基准芯片。对于光敏电阻这种应用绝对精度通常不重要因为光敏电阻本身的个体差异远大于参考电压的波动。但如果你用电池供电VDDA会随着电池放电而下降这时候ADC读数会整体偏移。解决办法有两种一是用内部参考电压通道如果芯片支持反推VDDA实际值二是直接用外部基准。我在用STM32F103的时候习惯在初始化时读一次内部参考电压通道的ADC值算出VDDA的实际电压然后在后续计算中做补偿。这个操作不复杂但能显著提升电池供电场景下的稳定性。3.2 采样时间与输入阻抗的匹配STM32的ADC输入可以等效为一个RC网络。数据手册里会给出一个公式用来计算最大允许的信号源阻抗。简单说如果信号源阻抗太高采样电容充电不够读数就会偏低。光敏电阻加分压电阻的等效输出阻抗在最坏情况下可能达到几十kΩ。以STM32F1为例如果采样时间设为55.5个ADC时钟周期ADC时钟14MHz时约4μs允许的最大信号源阻抗大约在几十kΩ量级。如果分压电阻取100kΩ光敏电阻也接近100kΩ等效输出阻抗就是50kΩ这时候采样时间需要设到最长档239.5周期否则读数会明显偏低。我实测过同样电路采样时间从7.5周期换到239.5周期ADC读数能差出几十个LSB。所以如果你发现读数偏低或者不稳定先检查采样时间是不是设得太短了。3.3 单次转换、连续转换与DMA的适用场景STM32的ADC支持多种工作模式。对于光敏电阻这种单通道采集最简单的做法是单次转换加轮询启动转换等EOC标志读数据。这种方式代码简单适合低速采集。如果你需要同时采集多个通道或者需要高速连续采集那就用连续转换加DMA。DMA把ADC数据自动搬到内存数组里CPU不用干预效率高很多。但光敏电阻响应慢通常不需要高速采集所以单次转换加软件滤波就足够了。我一般会这样设计用定时器触发ADC单次转换比如每100ms触发一次转换完成后进中断读数据。这样采集节奏稳定不会因为主循环里其他任务耗时导致采集间隔不均匀。定时器触发的好处是采集频率精确可控而且ADC只在需要的时候工作功耗更低。4. 从原始ADC值到可用光照判断的软件处理链路4.1 为什么原始ADC值不能直接用原始ADC值直接拿来用的问题主要有三个噪声、抖动和温漂。噪声来自电源和空间耦合表现为读数在几个LSB范围内随机跳动。抖动来自光敏电阻本身的特性即使光照恒定阻值也会有微小波动。温漂则是光敏电阻和分压电阻的温度系数不一致导致的。如果你只是做一个“亮/暗”判断原始值可能也能用但阈值附近会频繁误触发。比如阈值设为2000光照刚好在临界点时读数在1990到2010之间跳输出就会反复翻转。所以必须做滤波和迟滞处理。4.2 滑动平均与中值滤波的组合使用我常用的滤波方案是先做中值滤波去掉突发尖峰再做滑动平均平滑数据。中值滤波窗口取5个点滑动平均窗口取8到16个点。这个组合对光敏电阻这种慢变信号非常有效。具体实现上维护一个环形缓冲区每次新数据进来先替换最旧的数据然后对缓冲区排序取中值再对中值序列做平均。如果RAM紧张可以简化成只做滑动平均但中值滤波对去除偶发干扰很有用建议保留。还有一个细节滤波窗口大小要和采集频率匹配。如果每100ms采集一次16点滑动平均意味着响应延迟1.6秒。对于光照变化缓慢的场景没问题但如果你需要快速响应比如检测突然的遮挡窗口就要缩小到4到8个点。4.3 阈值判断中的迟滞设计迟滞Hysteresis是解决阈值附近抖动问题的标准方法。思路是设置两个阈值上限阈值和下限阈值。当读数高于上限时判定为“亮”低于下限时判定为“暗”在上下限之间时保持上一次的状态不变。上下限的间距取决于噪声幅度。我一般会先采集一段数据观察峰峰值然后取峰峰值的2到3倍作为迟滞带宽。比如噪声峰峰值是50个LSB迟滞带宽就取100到150个LSB。这样既能避免误触发又不会让响应过于迟钝。如果你需要更精细的判断比如区分“强光”“弱光”“暗光”三个等级那就设置两组迟滞阈值逻辑是一样的。5. 完整代码框架与关键配置注释5.1 ADC初始化与GPIO配置以下代码基于STM32F1系列的标准外设库其他系列思路相同寄存器名称和库函数略有差异。// ADC1初始化单通道规则组单次转换 void ADC_Config_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; // 使能GPIOA和ADC1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC时钟72MHz/612MHz // PA0作为模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); // ADC配置 ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); // 配置通道0采样时间239.5周期 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_Cmd(ADC1, ENABLE); // ADC校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }这段代码里有两个地方值得注意。一是ADC时钟分频STM32F1的ADC时钟不能超过14MHz所以72MHz主频下至少6分频。二是校准步骤不能省不校准的话读数偏差可能达到几十个LSB。5.2 单次采集与滤波实现#define FILTER_WINDOW 8 static u16 filter_buf[FILTER_WINDOW] {0}; static u8 filter_idx 0; static u8 filter_cnt 0; u16 Get_Adc_Value(void) { ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); return ADC_GetConversionValue(ADC1); } u16 Get_Filtered_Value(void) { u16 raw Get_Adc_Value(); u16 temp[FILTER_WINDOW]; u8 i, j; u16 sum 0; filter_buf[filter_idx] raw; filter_idx (filter_idx 1) % FILTER_WINDOW; if(filter_cnt FILTER_WINDOW) filter_cnt; // 复制到临时数组排序 for(i 0; i filter_cnt; i) temp[i] filter_buf[i]; for(i 0; i filter_cnt - 1; i) { for(j i 1; j filter_cnt; j) { if(temp[i] temp[j]) { u16 t temp[i]; temp[i] temp[j]; temp[j] t; } } } // 去掉最大最小各一个剩余求平均 for(i 1; i filter_cnt - 1; i) sum temp[i]; return sum / (filter_cnt - 2); }这个滤波函数做了两件事环形缓冲存储最近8次采样排序后去掉最大最小各一个剩余6个求平均。这样既能抑制尖峰干扰又能平滑数据。如果你觉得排序开销大可以改成只做滑动平均但去极值的效果确实更好。5.3 迟滞比较与状态输出#define THRESHOLD_HIGH 2200 #define THRESHOLD_LOW 1800 static u8 light_state 0; // 0暗1亮 void Update_Light_State(u16 adc_val) { if(adc_val THRESHOLD_HIGH) { light_state 1; } else if(adc_val THRESHOLD_LOW) { light_state 0; } // 在上下限之间时保持原状态 }阈值需要根据实际电路标定。标定方法是在目标“亮”环境下采集100个滤波后的值取平均再减去3倍标准差作为上限参考在目标“暗”环境下同样操作取平均加上3倍标准差作为下限参考。这样得到的阈值有统计依据比拍脑袋定一个数靠谱得多。6. 实测中遇到的典型问题与排查思路6.1 读数始终接近满量程或零这是最常见的问题通常原因是分压电路接反了或者ADC引脚配置错了。如果光敏电阻接VCC、分压电阻接地分压点电压随光照增强而降低反过来则随光照增强而升高。先确认你的接法和你期望的变化方向是否一致。另一个可能原因是GPIO模式没设成模拟输入。如果设成了浮空输入或上拉输入ADC读数会异常。STM32的ADC引脚必须配置为GPIO_Mode_AIN这一点在F1系列上尤其重要。6.2 读数跳动超过预期如果滤波后读数仍然跳动很大先检查电源。用示波器看VDDA和VSSA之间有没有明显纹波。如果电源纹波大ADC读数必然不稳。可以在VDDA引脚附近加一个10μF钽电容并联0.1μF陶瓷电容。如果电源没问题检查采样时间是否足够。采样时间太短会导致采样电容充电不足读数偏低且不稳定。把采样时间设到最长档试试如果读数明显变大且更稳定那就是采样时间的问题。还有一个容易被忽略的点光敏电阻的引线太长会引入干扰。如果光敏电阻离MCU较远建议用屏蔽线或者双绞线并在ADC输入端加RC低通滤波。6.3 不同板子之间读数不一致这通常是参考电压差异导致的。不同板子的VDDA可能略有不同比如一块是3.28V另一块是3.35V同样的分压电压下ADC读数会差几个LSB。如果应用对一致性要求高要么用外部基准要么在出厂时逐板标定。光敏电阻本身的个体差异也是原因之一。同一批次5516的亮电阻可能相差20%以上。如果产品需要一致性要么筛选光敏电阻要么在软件里做标定补偿。7. 从光敏电阻采集延伸出去的经验光敏电阻5516加STM32 ADC这套组合麻雀虽小五脏俱全。它涵盖了模拟前端设计、ADC配置、数字滤波、阈值判断这几个嵌入式采集系统的核心环节。把这套流程走通之后换成NTC热敏电阻测温度只需要把分压电阻换成和热敏电阻匹配的阻值软件里把线性判断换成查表或Steinhart-Hart公式其余框架完全复用。我在多个项目里用过类似的采集链路最大的体会是模拟部分的设计决定了上限软件滤波只能在这个上限之内做优化。如果分压电阻选得不合理或者采样时间配置不当后面再怎么滤波也救不回来。所以每次动手之前先花十分钟算一下分压电阻和采样时间比事后调试省几个小时。另外光敏电阻的响应速度虽然不快但在需要快速检测遮挡的场景下可以通过降低滤波窗口来提升响应速度代价是噪声变大。这个取舍需要根据具体应用来定没有万能参数。我一般会预留一个编译宏方便在不同场景下切换滤波窗口大小和阈值参数这样同一套代码可以适配多种需求。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑