1. 引言

在物联网、智能家居、工业自动化和嵌入式系统中,光照检测是最常见、最基础的感知需求之一。无论是路灯自动控制、屏幕亮度自适应调整,还是安防夜灯、植物补光系统,都需要一种成本低、电路简单、可靠性高的光传感器。光敏电阻(Light Dependent Resistor,简称 LDR)正是满足这些需求的最经典器件之一。

光敏电阻的核心材料通常是硫化镉(CdS),其电阻值会随着照射到表面的光照强度发生变化:光照越强,电阻越小;光照越弱,电阻越大。这种“亮阻小、暗阻大”的特性使得 LDR 可以通过一个最简单的分压电路接入单片机或微处理器,结合模数转换器(ADC)即可完成光照强度的数字化采集。

与光敏二极管、光敏三极管以及数字环境光传感器相比,LDR 的响应速度较慢,精度和一致性也相对有限,但它具有价格低廉、驱动电路简单、光谱响应接近人眼、受温度影响在常见应用中可以接受等突出优点,因此在大量非高精度场合仍然被广泛采用。

本文将从光敏电阻的物理原理出发,系统介绍其主要技术参数、典型硬件电路,并分别给出基于 Arduino、STM32、ESP32 以及 Linux 系统(IIO 框架)的完整驱动实现。同时,文章还会深入讨论采样滤波、光照标定、迟滞控制等数据处理方法,并结合路灯控制、智能家居、屏幕亮度调节等实际场景给出工程建议。全文以可落地、可复制运行为目标,读者可以根据自己的平台选择对应章节直接实践。

阅读提示:本文默认读者具备基本的嵌入式开发知识,了解 ADC、GPIO、分压电路等基础概念。不同平台的驱动章节相互独立,读者既可以顺序阅读,也可以直接跳到与自身开发环境对应的章节。

2. 光敏电阻的工作原理

要驱动好 LDR,首先需要理解它的物理特性。只有清楚它在不同光照条件下如何变化,才能正确设计采样电路、选择采样电阻并编写可靠的驱动代码。

2.1 光电导效应

光敏电阻工作的物理基础是半导体的光电导效应。以最常见的硫化镉(CdS)材料为例,在黑暗环境中,硫化镉中的电子大多被束缚在价带中,自由载流子数量很少,因此材料呈现高阻态。当有光子照射到材料表面时,只要光子能量大于材料的禁带宽度,价带电子就会吸收光子能量跃迁到导带,形成电子-空穴对。这些新增的自由载流子在电场作用下参与导电,使材料的电导率升高,等效电阻下降。

入射光越强,单位时间内产生的电子-空穴对就越多,材料中自由载流子浓度越大,电导率越高,表现出的电阻就越低。反之,当光照消失后,电子与空穴逐渐复合,自由载流子减少,电阻重新增大并最终恢复为暗电阻。这个过程不是瞬时的,因此 LDR 的响应时间通常在几十毫秒到几百毫秒之间。

2.2 硫化镉材料的特性

硫化镉是一种 II-VI 族化合物半导体,具有以下与光敏应用密切相关的特性:

  • 禁带宽度适中:硫化镉的禁带宽度约为 2.42 eV,对应波长约为 512 nm。这使得它对绿光附近的光最敏感,光谱响应曲线与人眼非常接近。
  • 暗电阻很高:在完全黑暗条件下,硫化镉光敏电阻的阻值通常可以达到数百千欧甚至数兆欧级别。
  • 亮电阻较低:在强光照射下,阻值可以下降到几百欧到几千欧不等,亮暗阻值比可以达到 10 的 4 次方到 10 的 6 次方量级。
  • 制造工艺成熟:硫化镉光敏电阻可以通过烧结、薄膜沉积等工艺批量生产,成本极低。

需要说明的是,硫化镉含有镉元素。镉属于重金属,对环境有一定影响,工业产品中通常需要遵守相应的环保法规。在进行产品设计时,如果目标市场对有害物质限制较为严格,需要考虑使用光敏二极管、环境光传感器 IC 等替代方案。不过,在实验、教学和大量消费类产品中,CdS LDR 仍然非常普遍。

2.3 电阻与光照强度的关系

光敏电阻的阻值与光照强度的关系通常可以用一个近似的幂函数来描述:

R = A * E^(-gamma)

其中,R 是光敏电阻在当前光照下的阻值,单位为欧姆;E 是光照强度,单位为勒克斯(lux);A 是与器件几何尺寸、材料特性有关的常数;gamma 是照度指数,通常记为希腊字母 γ。

γ 值决定了阻值随光照变化的剧烈程度。γ 值越大,电阻随光照强度增加而下降得越快,传感器的灵敏度越高。常见 CdS 光敏电阻的 γ 值一般在 0.5 到 1.0 之间。这个公式体现了 LDR 的非线性特性:在低照度区域,较小的光照变化会引起较大的电阻变化;在高照度区域,电阻变化幅度相对减小。因此,在设计测量电路和标定算法时,通常需要对测量值进行对数变换或查表修正。

反过来,如果已知当前 LDR 的阻值 R,也可以近似推算光照强度:

E = (A / R)^(1/gamma)

实际工程中,由于器件离散性较大,A 和 γ 的具体数值需要根据所选型号的数据手册并结合实测标定来确定。

2.4 光谱响应特性

光敏电阻对不同波长的光具有不同的灵敏度。以典型 CdS 器件为例,其光谱灵敏度峰值约在 540 nm 到 560 nm 之间,处于可见光的绿光区域。这与正常人眼的明视觉光谱灵敏度曲线峰值(约 555 nm)非常接近。正是因为这一点,LDR 被广泛用于需要模拟人眼感知亮度的场合,例如相机测光、屏幕自动亮度、室内照明控制等。

需要注意的是,LDR 对红外光也有一定的响应。某些钨丝灯或白炽灯光源中含有大量红外成分,可能使测量结果与人眼主观感受产生偏差。如果应用对光谱响应有严格要求,通常需要在前端增加滤光片,或者直接采用经过光谱校正的数字环境光传感器。

2.5 亮态与暗态电阻

光敏电阻数据手册中通常会给出两个关键指标:亮电阻和暗电阻。

  • 亮电阻:在指定照度条件下(常见为 10 lux 或 100 lux)测得的电阻值。例如 GL5528 在 10 lux 下的亮电阻为 10 到 20 千欧。
  • 暗电阻:完全遮光条件下测得的电阻值。例如 GL5528 在闭光 10 秒后的最小暗电阻为 1 兆欧。

亮暗阻值比越大,说明器件对光照变化的动态范围越大,越有利于电路设计。驱动代码和分压电阻的选择都要围绕这两个参数展开,后续章节会详细推导。

3. LDR 的主要技术参数

在正式开始电路设计和代码编写之前,需要先熟悉光敏电阻数据手册中的常见参数。理解这些参数的含义,有助于正确选型,并在不同环境中合理设定采样范围和报警阈值。

3.1 亮电阻与测试照度

亮电阻是 LDR 在特定照度下的电阻值,是选型时最重要的参考指标之一。测试照度通常为 10 lux 或 100 lux。例如,GL5528 的规格书标明在 10 lux 光照下阻值范围为 10 到 20 千欧。这一范围并不是固定值,而是批量器件允许的离散区间。因此,不同批次的 LDR 在同一照度下的阻值可能相差一倍甚至更多,量产设计中通常需要设置标定流程或放宽阈值范围。

3.2 暗电阻与测试条件

暗电阻是 LDR 在完全无光照条件下的电阻值。数据手册中一般会注明“闭光 10 秒后测得”,因为光敏电阻受到光照后撤去光源,载流子复合需要时间,阻值不会立刻回到最大值。暗电阻越高,电路在黑暗状态下的泄漏电流越小,分压输出的对比度越好。常见暗电阻范围从 0.5 兆欧到 20 兆欧以上。

3.3 照度指数 γ 值

γ 值表示光敏电阻阻值随照度变化的速率,是描述 LDR 非线性特性的关键参数。它可以通过测量两个不同照度下的电阻值计算得到:

gamma = log(R1 / R2) / log(E2 / E1)

其中 R1、R2 分别是在照度 E1、E2 下的阻值。γ 值越大,说明传感器对光照变化越敏感。典型 CdS LDR 的 γ 值在 0.5 到 1.0 之间。γ 值高的器件适合用于需要较高灵敏度的光照检测,而 γ 值较低的器件则输出变化更平缓,适合宽动态范围测量。

3.4 响应时间

LDR 的响应时间分为上升时间和下降时间。上升时间通常指从黑暗状态暴露到强光后,电阻下降到稳定值 63% 所需的时间;下降时间则相反。CdS 光敏电阻的上升时间一般在几十毫秒左右,下降时间则可能长达数百毫秒甚至更长。这意味着 LDR 不适合检测快速变化的光信号,例如闪烁频率较高的 LED 调光或高速事件。对于普通环境光检测、昼夜判断等慢速应用,这种响应速度完全足够。

3.5 温度特性

光敏电阻的阻值不仅受光照影响,还受环境温度影响。温度升高时,半导体中的热激发载流子增多,阻值会下降。温度系数通常用每摄氏度电阻相对变化的百分比表示。在温度变化剧烈的室外场景中,尤其是温差较大的地区,温度变化可能导致检测阈值漂移。工程上可以采用温度补偿、定期基线校准或选择温度系数更低的器件来缓解。

3.6 最大功耗与额定电压

光敏电阻两端施加电压后会产生功耗。数据手册中会给出允许的最大功耗,常见值在 90 到 150 毫瓦之间。设计分压电路时,需要确保在最坏情况下(例如光照极强、LDR 阻值较低、分压电阻也较小)LDR 和分压电阻的功耗都不超过额定值。对于 3.3 V 或 5 V 的典型嵌入式系统,只要分压电阻选择在数千欧以上,通常不会超过功率限制。

3.7 光谱峰值波长

光谱峰值波长表示器件响应最灵敏的光波长。CdS 光敏电阻的峰值波长通常在 540 到 560 nm,处于可见光绿光波段。这与人眼明视觉响应峰值接近,也是其适合模拟人眼亮度感知的重要原因。

3.8 典型型号参数对比

下表列出了几款常见 CdS 光敏电阻的典型参数,数据来自公开数据手册,实际使用时应以具体批次和厂家规格为准。

型号 亮电阻(10 lux) 暗电阻(最小) γ 值 响应时间(典型) 最大功耗
GL5516 5-10 kΩ 0.5 MΩ 0.6 30 ms / 30 ms 90 mW
GL5528 10-20 kΩ 1 MΩ 0.6 20 ms / 30 ms 100 mW
GL5537-1 20-30 kΩ 2 MΩ 0.6 20 ms / 30 ms 100 mW
GL5537-2 30-50 kΩ 3 MΩ 0.7 20 ms / 30 ms 100 mW
GL5539 50-100 kΩ 5 MΩ 0.8 20 ms / 30 ms 100 mW
GL5549 100-200 kΩ 10 MΩ 0.9 20 ms / 30 ms 100 mW

从上表可以看出,GL55 系列的不同型号主要在亮电阻和暗电阻上存在差异,这也决定了它们适用于不同的照度范围和分压电路参数。初学者和 Arduino 入门项目中最常用的是 GL5528 和 GL5537。

3.9 选型建议

选择 LDR 型号时,可以重点考虑以下几个问题:

  • 目标照度范围:如果主要检测室内 10 到 100 lux 的照度,选择亮电阻较小的型号(如 GL5516、GL5528)可以获得更宽的电压变化范围;如果用于检测较暗环境或需要较高灵敏度,可以选择亮电阻较大的型号(如 GL5539、GL5549)。
  • 与 ADC 量程匹配:分压输出应尽量覆盖 ADC 量程的较大比例,以提高分辨率。具体匹配方法见下一章。
  • 电路功耗:分压支路会持续耗电,电池供电产品中应选择阻值较大的 LDR 并配以较大的分压电阻,必要时采用间歇采样的方式降低平均功耗。
  • 成本与供货:通用型号价格接近,批量采购时更多关注供货稳定性和一致性。

4. 硬件电路设计

光敏电阻本身是无源器件,无法直接输出数字信号。要让它能够被单片机读取,必须设计相应的信号调理电路。本章从最基本的分压电路开始,逐步深入介绍 ADC 采样电路、运算放大器信号调理、迟滞比较器以及工程保护措施。

4.1 基本分压电路

最常见的 LDR 驱动电路是一个简单的电阻分压器。将 LDR 与一个固定电阻串联,一端接电源 VCC,另一端接地,中间节点输出到单片机的 ADC 引脚。根据 LDR 和固定电阻的连接顺序,可以分为两种接法。

第一种接法:LDR 上端接 VCC,固定电阻下端接地,输出从中间节点取出。此时输出电压为:

Vout = VCC * R_fixed / (R_ldr + R_fixed)

在这种接法下,光照越强,LDR 阻值越小,Vout 越高;光照越弱,LDR 阻值越大,Vout 越低。也就是说,输出电压与光照强度正相关。

第二种接法:固定电阻上端接 VCC,LDR 下端接地,输出从中间节点取出。此时输出电压为:

Vout = VCC * R_ldr / (R_ldr + R_fixed)

这种接法下,光照越强,Vout 越低,输出电压与光照强度负相关。两种接法在功能上等价,只是输出极性相反,代码中可以根据习惯选择。

分压电阻 R_fixed 的选择非常关键。为了让分压输出在目标照度范围内具有最大的电压变化,通常选择 R_fixed 等于 LDR 在目标照度范围内的几何平均阻值,即:

R_fixed = sqrt(R_min * R_max)

例如,某 LDR 在目标照度范围内的阻值从 1 千欧变化到 100 千欧,那么分压电阻取 10 千欧左右较为合适。这样可以在整个范围内获得较为均衡的输出电压变化。如果分压电阻过大或过小,都会导致某一段照度区间内输出电压变化很小,降低有效分辨率。

4.2 与 ADC 直接连接

在大多数嵌入式系统中,LDR 分压输出可以直接接入 ADC 引脚。以 5 V 系统和 10 位 ADC 为例,ADC 的分辨率为 5 V / 1024,约为 4.88 mV。对于一般的光照检测,直接连接已经足够。

但直接连接也存在一些问题。首先是阻抗匹配。某些单片机 ADC 的采样保持电路需要较低的信号源阻抗,而 LDR 分压电路的等效输出阻抗可能达到数十千欧。如果 ADC 输入阻抗不足,采样结果会出现偏差。解决方法是适当降低分压电阻,或者在分压输出与 ADC 之间增加一个运放缓冲器。其次,LDR 输出信号可能存在电源噪声、环境干扰等,需要在采样端增加滤波电容。

一个典型的 ADC 前端电路可以在输出节点对地并联一个小电容,例如 10 nF 到 100 nF,用于滤除高频噪声。滤波电容与分压电阻构成低通滤波器的截止频率,可以根据响应速度和噪声水平折中选择。需要注意的是,如果电容过大,会与分压电阻形成较大的 RC 时间常数,减慢对光照变化的响应。

4.3 运算放大器信号调理

当 LDR 分压输出信号较弱、信号源阻抗较高,或者需要放大、平移、滤波后再送入 ADC 时,可以使用运算放大器进行信号调理。常见的同相放大电路和电压跟随器都能起到缓冲作用。

电压跟随器是最简单的缓冲方案。将 LDR 分压输出接到运放的同相输入端,输出端直接反馈到反相输入端,输出即可跟随输入。电压跟随器具有极高的输入阻抗和较低的输出阻抗,可以隔离 LDR 分压网络与 ADC 输入,避免采样电流影响分压精度。

如果需要将输出信号放大到更接近 ADC 满量程,可以使用同相比例放大电路。假设输入信号 Vi,R1 和 R2 构成反馈网络,则输出电压为:

Vo = Vi * (1 + R1 / R2)

选择运放时,应关注输入偏置电流、输入失调电压和电源范围。对于 3.3 V 或 5 V 系统,轨到轨输入输出运放(如 MCP6002、LMV358)较为合适。对于电池供电的低功耗应用,还需要关注运放的静态电流。

4.4 迟滞比较器电路

许多 LDR 应用并不需要连续测量光照强度,只需要判断“亮”或“暗”两种状态,例如路灯开关、夜灯控制。此时可以使用电压比较器将模拟信号转换为数字电平。

最简单的比较器电路将 LDR 分压输出接到比较器的一个输入端,另一个输入端接参考电压。当光照强度跨越设定阈值时,比较器输出翻转。如果直接将比较器输出接单片机 GPIO,可以省去 ADC 资源,并可以使用外部中断实时响应光照变化。

但简单比较器在阈值附近容易因噪声而产生反复抖动。例如,光照强度在阈值附近波动时,比较器输出会频繁翻转,导致负载继电器或 LED 快速开关,影响寿命。解决方法是引入迟滞,即施密特触发特性。反馈电阻 R_hyst 从比较器输出端连接到同相输入端,使阈值电压随输出状态变化,形成两个不同的比较电平。

假设比较器为推挽输出,电源为 VCC,基准电阻网络产生的中心电压为 Vref,迟滞电阻 R_hyst 与同相输入电阻网络共同作用。当输出为高电平 VCC 时,正向阈值升高;当输出为低电平 0 V 时,正向阈值降低。两个阈值之间的电压差称为迟滞窗口。迟滞窗口的大小可以通过电阻取值计算,工程上通常设置为使输出噪声不会引起频繁翻转即可,例如 50 到 200 mV。

对于使用开漏输出的比较器,迟滞计算需要考虑上拉电阻的影响。在实际电路调试中,可以先通过仿真或计算确定初始参数,再根据示波器观察结果微调反馈电阻。

4.5 保护电路设计

虽然 LDR 驱动电路结构简单,但在户外或工业环境中仍需考虑以下保护措施:

  • 静电防护:在 ADC 输入引脚或比较器输入端增加 TVS 二极管或钳位二极管,防止人体静电或感应雷击损坏器件。
  • 浪涌保护:外部长线引出的 LDR 信号线可能感应到浪涌电压,建议串入限流电阻并配合钳位二极管。
  • 电源去耦:在 LDR 分压支路的电源端增加 100 nF 陶瓷电容,运放供电引脚就近放置去耦电容,降低电源噪声。
  • 输入限幅:若 LDR 分压输出可能超出 ADC 输入范围,可在 ADC 引脚增加外部钳位电路,限制电压在 0 到 VREF 之间。

4.6 PCB 布局注意事项

LDR 属于高阻传感器,容易受到噪声干扰。PCB 布局时应注意:

  • LDR 的感光面应朝向外部环境,避免被外壳、排针或遮挡物挡住光线。
  • LDR 分压输出走线尽量短,远离高频开关节点(如 DC-DC 电感、PWM 调光输出)。
  • 模拟地与数字地尽量分区,通过单点连接,减少数字噪声串入模拟信号。
  • 如果使用运放,反馈电阻应靠近运放引脚,避免长走线引入寄生电容。
  • LDR 的安装位置应避免红外遥控发射器、显示屏背光等局部光源的直接影响,防止误判。

5. 基于 Arduino 平台的 LDR 驱动开发

Arduino 是最适合初学者验证 LDR 电路和算法的平台。本章从最基础的模拟量读取开始,逐步介绍阈值检测、电阻与光照计算、标定以及低功耗设计。

5.1 开发环境准备

假设使用 Arduino Uno 或兼容板,LDR 分压输出接到 A0 引脚。分压电阻可以选择 10 千欧。按照第一种接法,LDR 上端接 5 V,固定电阻 10 千欧下端接 GND,中间节点接到 A0。

在 Arduino IDE 中选择正确的开发板型号和串口,新建工程后即可开始编写代码。Arduino 的 ADC 为 10 位,analogRead 返回 0 到 1023。

5.2 最基础的模拟量读取

以下代码每隔 500 毫秒读取一次 LDR 分压值,并通过串口输出,用于观察不同光照条件下的电压变化。

const int ldrPin = A0;
void setup() {
Serial.begin(9600);
pinMode(ldrPin, INPUT);
}
void loop() {
int raw = analogRead(ldrPin);
float voltage = raw * (5.0 / 1023.0);
Serial.print("RAW=");
Serial.print(raw);
Serial.print("  VOLTAGE=");
Serial.print(voltage, 3);
Serial.println(" V");
delay(500);
}

将程序烧录后,用手遮挡 LDR 或用手电筒照射,可以在串口监视器中观察到数值的明显变化。这个简单实验可以帮助读者直观理解 LDR 的阻值变化与 ADC 输出之间的关系。

5.3 计算 LDR 的实时电阻

仅知道 ADC 原始值或电压还不够,很多时候我们需要反向计算出 LDR 当前的阻值,以便进行光照标定或阈值判断。以 LDR 上端接 VCC、固定电阻接地为例,设固定电阻为 R_fixed,ADC 采样电压为 Vadc,VCC 已知,则 LDR 阻值为:

R_ldr = R_fixed * (VCC - Vadc) / Vadc
const int ldrPin = A0;
const float fixedResistor = 10000.0;
const float vcc = 5.0;
void setup() {
Serial.begin(9600);
}
void loop() {
int raw = analogRead(ldrPin);
float voltage = raw * (vcc / 1023.0);
float ldrResistance;
if (voltage > 0.01) {
ldrResistance = fixedResistor * (vcc - voltage) / voltage;
} else {
ldrResistance = fixedResistor * (vcc - 0.01) / 0.01;
}
Serial.print("LDR Resistance=");
Serial.print(ldrResistance / 1000.0, 2);
Serial.println(" kOhm");
delay(500);
}

代码中增加了对电压接近 0 的处理,避免除零错误。当光照极强时,LDR 阻值趋于 0,ADC 电压接近 0,此时计算出的电阻值极大,这并不符合实际物理关系。更合理的做法是限制最小电压,或者当电压过低时直接判定为“强光”。

5.4 阈值判断控制 LED

最常见的 LDR 应用是“天黑自动点灯”。下面代码根据 LDR 采样值控制一个 LED:当环境变暗、采样值低于阈值时点亮 LED,否则熄灭。阈值需要根据实际电路和安装位置标定。

const int ldrPin = A0;
const int ledPin = 13;
const int darkThreshold = 300;
void setup() {
pinMode(ledPin, OUTPUT);
Serial.begin(9600);
}
void loop() {
int raw = analogRead(ldrPin);
Serial.println(raw);
if (raw < darkThreshold) {
digitalWrite(ledPin, HIGH);
Serial.println("Dark - LED ON");
} else {
digitalWrite(ledPin, LOW);
Serial.println("Bright - LED OFF");
}
delay(200);
}

需要注意的是,这种单一阈值判断在临界光照时容易抖动。下一章会介绍迟滞控制的实现。

5.5 多路 LDR 采样

某些应用需要检测多个方向或房间的光照,可以接入多路 LDR。Arduino Uno 有多个模拟输入引脚,可以分别读取。以下代码演示了读取两路 LDR,并对多路采样做简单的均值滤波。

const int ldrPins[] = {A0, A1};
const int pinCount = 2;
const int sampleCount = 20;
void setup() {
Serial.begin(9600);
}
int readAverage(int pin) {
long sum = 0;
for (int i = 0; i < sampleCount; i++) {
sum += analogRead(pin);
delayMicroseconds(500);
}
return (int)(sum / sampleCount);
}
void loop() {
for (int i = 0; i < pinCount; i++) {
int avg = readAverage(ldrPins[i]);
Serial.print("LDR");
Serial.print(i);
Serial.print("=");
Serial.print(avg);
Serial.print("  ");
}
Serial.println();
delay(300);
}

均值滤波能够有效平滑瞬时噪声,但会牺牲一定的响应速度。采样次数和延迟可以根据实际噪声水平调整。

5.6 使用中断实现光照事件触发

如果 LDR 连接的是模拟量,Arduino Uno 的模拟引脚不能直接触发硬件中断。但我们可以通过比较器或三极管将 LDR 信号转换为数字电平后接到数字引脚,再使用外部中断。以下是一个示例框架,假设比较器输出接 D2,使用下降沿中断检测“变暗”事件。

const int ldrDigitalPin = 2;
volatile bool darkEvent = false;
void setup() {
Serial.begin(9600);
pinMode(ldrDigitalPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ldrDigitalPin), onDark, FALLING);
}
void onDark() {
darkEvent = true;
}
void loop() {
if (darkEvent) {
darkEvent = false;
Serial.println("Dark interrupt triggered!");
// 执行开灯等操作
}
}

使用硬件中断可以减少主循环轮询频率,适合低功耗场景。需要注意中断服务函数中应尽量缩短执行时间,只设置标志位,实际处理放到主循环。

5.7 低功耗采样设计

在电池供电的无线光照传感器中,持续采样会消耗大量电能。常见做法是让单片机大部分时间处于睡眠状态,每隔一段时间唤醒采集一次。以下代码展示了使用 Arduino 的看门狗定时器周期性唤醒进行 LDR 采样的简易框架。该示例假设使用支持看门狗唤醒的 AVR 开发板。

#include <avr/sleep.h>
#include <avr/wdt.h>
const int ldrPin = A0;
const int sampleCount = 8;
ISR(WDT_vect) {
// 看门狗唤醒后什么都不做,继续执行主循环
}
void setup() {
Serial.begin(9600);
pinMode(ldrPin, INPUT);
wdt_enable(WDTO_8S);
WDTCSR |= (1 << WDIE);
}
void loop() {
int avg = 0;
for (int i = 0; i < sampleCount; i++) {
avg += analogRead(ldrPin);
delay(5);
}
avg /= sampleCount;
Serial.println(avg);
// 进入睡眠,等待看门狗定时唤醒
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
sleep_mode();
sleep_disable();
}

实际部署时需要关闭串口、外设和掉电域,并考虑 LDR 分压电路本身的静态功耗。分压支路会持续消耗电流,如果对功耗要求极高,可以增加一个 PMOS 开关,在采样前短暂接通 LDR 供电,采样完成后断开。

5.8 标定方法与照度推算

要得到近似的勒克斯值,需要进行标定。最简单的标定方法是使用一台照度计或手机环境光传感器作为参考。在多个不同照度环境下,同时记录参考照度和 LDR 计算出的电阻值,然后通过幂函数拟合得到参数。

以下代码示例使用已知标定点计算 γ 和常数 A,并在运行时推算光照。假设标定点 1 为 10 lux 时 LDR 阻值 12000 欧,标定点 2 为 100 lux 时 LDR 阻值 3000 欧。

const float e1 = 10.0;
const float r1 = 12000.0;
const float e2 = 100.0;
const float r2 = 3000.0;
const float gamma = log(r1 / r2) / log(e2 / e1);
const float aConst = r1 * pow(e1, gamma);
float resistanceToLux(float resistance) {
if (resistance <= 0) {
resistance = 1.0;
}
return pow(aConst / resistance, 1.0 / gamma);
}

需要强调的是,由于 LDR 离散性大,同一段代码在不同器件上可能得到明显不同的照度结果。量产时应为每个设备执行标定,将参数写入 EEPROM,而不是在代码中写死。

6. 基于 STM32 平台的 LDR 驱动开发

STM32 系列单片机具备高性能、多分辨率 ADC 和丰富的 DMA 外设,适合需要多路采样、实时滤波或与 RTOS 协同工作的 LDR 应用。本章以 STM32F103 和 STM32 HAL 库为例,介绍 ADC 单次采样、多路轮询、DMA 连续采样以及 RTOS 任务设计。

6.1 ADC 外设配置

首先在 CubeMX 中配置 ADC。以 STM32F103C8T6 为例,PA0 对应 ADC1 通道 0。在 CubeMX 中选择 ADC1,使能 IN0 通道,设置采样时间。采样时间越长,对外部高阻抗信号源的适应能力越强。对于 LDR 分压电路,如果输出阻抗较高,建议设置较长的采样时间,例如 55.5 或 71.5 个 ADC 时钟周期。

生成代码后,可以在用户代码中调用 HAL 库函数执行采样。以下代码演示单通道轮询采样:

ADC_HandleTypeDef hadc1;
void LDR_Init(void) {
// CubeMX 生成的初始化代码通常会放在这里
}
uint16_t LDR_ReadRaw(void) {
HAL_ADC_Start(&hadc1);
if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) {
return (uint16_t)HAL_ADC_GetValue(&hadc1);
}
return 0;
}

这种轮询方式适合低频率采样。若需要连续高频采样,应改用 DMA 模式。

6.2 DMA 连续采样与均值滤波

在 CubeMX 中配置 ADC1 的 DMA 连续转换模式,并设置数据宽度为半字。DMA 将 ADC 结果自动搬运到指定的数据缓冲区。以下代码启动 DMA 后循环读取采样值,并计算滑动平均。

#define LDR_BUF_SIZE 64
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
uint16_t adcBuf[LDR_BUF_SIZE];
void LDR_StartDMA(void) {
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adcBuf, LDR_BUF_SIZE);
}
uint16_t LDR_GetAverage(void) {
uint32_t sum = 0;
for (int i = 0; i < LDR_BUF_SIZE; i++) {
sum += adcBuf[i];
}
return (uint16_t)(sum / LDR_BUF_SIZE);
}

DMA 循环模式下缓冲区会不断被新的采样值覆盖,主程序可以随时读取最近的采样窗口并计算平均值。STM32 的 ADC 分辨率可配置为 12 位,相比 Arduino 的 10 位具有更高的量化精度。

6.3 多通道扫描采样

当需要同时采集多路 LDR 时,可以配置 ADC 为扫描模式,配合 DMA 将各通道结果依次存入缓冲区。以下示例以两路 LDR 为例,依次读取 PA0 和 PA1。

#define CHANNEL_COUNT 2
#define SAMPLE_PER_CHANNEL 32
#define BUF_SIZE (CHANNEL_COUNT * SAMPLE_PER_CHANNEL)
uint16_t adcBuf[BUF_SIZE];
void LDR_ReadMultiChannel(void) {
for (int ch = 0; ch < CHANNEL_COUNT; ch++) {
uint32_t sum = 0;
for (int i = 0; i < SAMPLE_PER_CHANNEL; i++) {
sum += adcBuf[ch + i * CHANNEL_COUNT];
}
uint16_t avg = (uint16_t)(sum / SAMPLE_PER_CHANNEL);
// 将 avg 存入对应通道的结果变量
}
}

配置扫描模式时,需要将 ADC 的规则序列设置为多个通道,并正确设置 DMA 数据长度。采样时间同样建议适当增大,以避免高阻抗信号源导致通道串扰。

6.4 数字滤波算法实现

STM32 算力充足,可以运行比简单均值更复杂的滤波算法。下面分别给出中值滤波和一阶低通滤波的实现。

中值滤波对脉冲噪声有良好的抑制效果,适合 LDR 信号线受到突发干扰的场合。以下代码对 5 个采样值排序取中值:

#include <stdlib.h>
int cmpU16(const void *a, const void b) {
return ((uint16_t *)a - *(uint16_t *)b);
}
uint16_t medianFilter(uint16_t *data, int len) {
uint16_t temp[5];
for (int i = 0; i < len; i++) {
temp[i] = data[i];
}
qsort(temp, len, sizeof(uint16_t), cmpU16);
return temp[len / 2];
}

一阶低通滤波(IIR 滤波)实现简单,计算量小,能有效平滑随机噪声:

float ldrFiltered = 0.0f;
const float alpha = 0.1f;
float lowPassFilter(uint16_t newSample) {
ldrFiltered = alpha * newSample + (1.0f - alpha) * ldrFiltered;
return ldrFiltered;
}

alpha 取值在 0 到 1 之间。alpha 越小,滤波越强,响应越慢;alpha 越大,响应越快,但噪声抑制能力越弱。实际工程中可以根据响应时间要求调整。

6.5 触发外部中断的阈值检测

如果使用比较器将 LDR 信号转换为数字电平,可以接到 STM32 的 EXTI 引脚,通过中断实现即时响应。以下示例假定 PA3 配置为外部中断输入,下降沿检测“变暗”事件。

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
  if (GPIO_Pin == GPIO_PIN_3) {
    // 设置标志位,例如 light_state_changed = true
  }
}

在中断回调中同样建议只设置标志位,真正的业务处理放在主循环或 RTOS 任务中。若比较器输出存在抖动,建议在硬件上使用迟滞比较器,或软件中增加去抖延时。

6.6 基于 FreeRTOS 的任务设计

在 STM32 上运行 FreeRTOS 时,可以将 LDR 采样放在一个独立任务中,通过信号量或消息队列与其他任务通信。以下示例创建一个光照采样任务,周期性读取 LDR 并通过队列发送数据。

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
QueueHandle_t lightQueue;
void LDR_Task(void *arg) {
const TickType_t samplePeriod = pdMS_TO_TICKS(200);
for (;;) {
uint16_t raw = LDR_GetAverage();
xQueueSend(lightQueue, &raw, 0);
vTaskDelay(samplePeriod);
}
}
void App_Init(void) {
lightQueue = xQueueCreate(8, sizeof(uint16_t));
xTaskCreate(LDR_Task, "ldr", 256, NULL, 3, NULL);
}

采样周期可以根据 LDR 响应时间和应用需求设置。对于路灯等慢速应用,200 毫秒到 1 秒的周期已经足够。任务栈大小需要根据局部变量和库函数占用合理分配,256 字通常足够,复杂算法可适当增大。

6.7 完整工程示例:LDR 控制继电器

下面给出一个较完整的 STM32 示例,功能是持续采样 LDR,当环境亮度低于阈值时点亮 LED 或驱动继电器,并且加入迟滞防止抖动。

#include "main.h"
#define LDR_DARK_THRESHOLD   2200U
#define LDR_HYSTERESIS       150U
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
#define LDR_BUF_SIZE 32
uint16_t adcBuf[LDR_BUF_SIZE];
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);
static uint16_t LDR_GetAverage(void) {
uint32_t sum = 0;
for (int i = 0; i < LDR_BUF_SIZE; i++) {
sum += adcBuf[i];
}
return (uint16_t)(sum / LDR_BUF_SIZE);
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
bool relayOn = false;
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adcBuf, LDR_BUF_SIZE);
while (1) {
uint16_t value = LDR_GetAverage();
if (!relayOn && value < LDR_DARK_THRESHOLD) {
relayOn = true;
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
} else if (relayOn && value > (LDR_DARK_THRESHOLD + LDR_HYSTERESIS)) {
relayOn = false;
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
}
HAL_Delay(100);
}
}

注意代码中的阈值仅为示例,实际应根据硬件电路、分压电阻、供电电压和安装环境重新标定。迟滞带宽选择 150 个 ADC 码值,可以抑制临界点的小幅波动。

7. 基于 ESP32 平台的 LDR 驱动开发

ESP32 在物联网领域应用广泛,自带 Wi-Fi 和蓝牙,非常适合实现远程光照监测、云端数据上传和联动控制。本章介绍 ESP32 的 ADC 特性、模拟输入配置、联网光照采集以及低功耗设计。

7.1 ESP32 ADC 特性概述

ESP32 的 ADC 为 12 位逐次逼近型,提供 ADC1 和 ADC2 两组。ADC1 包含 8 个通道,ADC2 包含 10 个通道。需要注意的是,ADC2 与 Wi-Fi 共用部分硬件资源,当 Wi-Fi 启用时,ADC2 的部分通道可能无法正常使用。因此,涉及 Wi-Fi 联网的应用应优先使用 ADC1 通道。

ESP32 的 ADC 量程约为 0 到 3.3 V,但输入需要经过衰减配置。不同衰减档位对应不同的测量范围,同时也影响线性度。常见衰减配置包括 0 dB、2.5 dB、6 dB 和 11 dB。对于 LDR 分压输出,如果电压不超过 1.1 V,可使用 0 dB 衰减,线性度最好;如果电压接近 3.3 V,可选用 11 dB 衰减。

ESP32 的 ADC 在全量程范围内并非完全线性,尤其在接近两端时偏差较大。官方提供了两点校准和曲线拟合方法,实际应用中可以对原始值进行修正。

7.2 Arduino 框架下的基础读取

如果使用 Arduino IDE 或 PlatformIO 开发 ESP32,API 与 Arduino 类似。以下代码读取 GPIO34 上的 LDR 分压值:

const int ldrPin = 34;
void setup() {
Serial.begin(115200);
analogReadResolution(12);
analogSetPinAttenuation(ldrPin, ADC_11db);
}
void loop() {
int raw = analogRead(ldrPin);
float voltage = raw * (3.3 / 4095.0);
Serial.print("RAW=");
Serial.print(raw);
Serial.print("  V=");
Serial.println(voltage, 3);
delay(500);
}

GPIO34 属于 ADC1 通道 6,适合在 Wi-Fi 开启时使用。模拟引脚无需在 setup 中通过 pinMode 配置为输入,直接调用 analogRead 即可。需要注意 ESP32 的 ADC 原始值与电压并非完美的线性关系,对于精度要求较高的场合应进行校准。

7.3 ESP-IDF 框架下的 ADC 驱动

使用 ESP-IDF 开发时,可以使用 ADC 单次采样 API。以下示例配置 ADC1 通道 6,以 12 位精度、11 dB 衰减进行单次采样:

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/adc.h"
#include "esp_adc_cal.h"
#define LDR_ADC_CHANNEL ADC1_CHANNEL_6
static esp_adc_cal_characteristics_t adcChars;
void app_main(void) {
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(LDR_ADC_CHANNEL, ADC_ATTEN_DB_11);
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11,
ADC_WIDTH_BIT_12, 1100, &adcChars);
while (1) {
int raw = adc1_get_raw(LDR_ADC_CHANNEL);
uint32_t voltage = esp_adc_cal_raw_to_voltage(raw, &adcChars);
printf("raw=%d, voltage=%lu mV\n", raw, voltage);
vTaskDelay(pdMS_TO_TICKS(500));
}
}

esp_adc_cal_characterize 会根据芯片内部的基准电压或默认参数生成校准特征,esp_adc_cal_raw_to_voltage 将原始值转换为更准确的毫伏值。第三参数 1100 是默认参考电压,单位为毫伏。对于有 eFuse 校准值的芯片,该函数会自动使用内部校准数据。

7.4 平均值与多采样

ESP32 ADC 在 Wi-Fi 射频工作时可能受到一定的电源噪声影响,建议采用多次采样求平均。以下代码对 64 次采样求平均,有效降低随机噪声:

#define SAMPLE_COUNT 64
int adcReadAverage(adc1_channel_t channel) {
uint64_t sum = 0;
for (int i = 0; i < SAMPLE_COUNT; i++) {
sum += adc1_get_raw(channel);
}
return (int)(sum / SAMPLE_COUNT);
}

如果需要更好的抗干扰能力,可以结合中值滤波,先对一组采样排序,去掉最大和最小的异常值后再求平均。

7.5 Wi-Fi 联网光照监测

ESP32 最大的优势是可以将光照数据发送到云端或局域网服务器。以下示例演示在 Arduino 框架下作为 HTTP 客户端,周期性读取 LDR 并通过 HTTP GET 请求上报数据。示例中的服务器地址为占位符,实际使用时应替换为真实的服务器地址。

#include <WiFi.h>
#include <HTTPClient.h>
const char *ssid = "YOUR_SSID";
const char *password = "YOUR_PASSWORD";
const char *serverUrl = "http://192.168.1.100:8080/api/light";
const int ldrPin = 34;
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("WiFi connected");
analogReadResolution(12);
analogSetPinAttenuation(ldrPin, ADC_11db);
}
void loop() {
if (WiFi.status() == WL_CONNECTED) {
int raw = analogRead(ldrPin);
String url = String(serverUrl) + "?raw=" + String(raw);
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode &gt; 0) {
  Serial.printf("GET code: %d\n", httpCode);
}
http.end();
}
delay(10000);
}

实际产品中通常使用 MQTT 协议上报,配合 JSON 载荷,便于云端接入和数据管理。也可以使用 ESP-NOW 或 BLE 实现短距离无线采集。

7.6 低功耗深度睡眠采集

电池供电的 ESP32 光照传感器可以使用深度睡眠模式大幅降低平均功耗。ESP32 支持定时器唤醒和 GPIO 唤醒。定时器唤醒适合周期性采集上报,GPIO 唤醒则可由 LDR 比较器输出的数字信号触发。

以下示例使用定时器每 60 秒唤醒一次,采集 LDR 后上报,然后重新进入深度睡眠:

#define uS_TO_S_FACTOR 1000000ULL
#define SLEEP_TIME_US (60ULL * uS_TO_S_FACTOR)
const int ldrPin = 34;
void setup() {
Serial.begin(115200);
analogReadResolution(12);
analogSetPinAttenuation(ldrPin, ADC_11db);
int raw = analogRead(ldrPin);
Serial.println(raw);
// 上报数据(省略 Wi-Fi 连接代码)
esp_sleep_enable_timer_wakeup(SLEEP_TIME_US);
esp_deep_sleep_start();
}
void loop() {
// 深度睡眠唤醒后会从 setup 重新执行,loop 不会执行
}

使用深度睡眠时,应关闭或断电不必要的传感器和分压支路,否则 LDR 分压电路本身的静态电流可能远大于 ESP32 睡眠电流。常见做法是使用 GPIO 控制的 MOS 管为 LDR 分压支路供电,仅在唤醒采样期间导通。

8. 基于 Linux 系统的 LDR 驱动开发

在嵌入式 Linux 平台上,LDR 通常通过片上 ADC 或外部 ADC 芯片接入系统。Linux 内核提供了工业 I/O 子系统(Industrial I/O,简称 IIO)来统一管理 ADC 等模拟量设备。本章介绍 IIO 子系统的使用方式、设备树配置、sysfs 接口以及用户空间读取方法,并简要说明如何编写简单的 IIO 内核驱动。

8.1 IIO 子系统简介

IIO 是 Linux 内核中专门面向传感器和模数转换器的子系统。它将 ADC、DAC、加速度计、光强传感器等设备抽象为统一的设备节点,提供 sysfs 和字符设备两种用户空间访问方式。对于 LDR 这类通过 ADC 采样的传感器,通常直接使用现有的 ADC 驱动,不一定需要单独编写 LDR 驱动。

IIO 的 sysfs 接口一般挂载在 /sys/bus/iio/devices/ 目录下。ADC 驱动会为每个通道提供对应的原始值文件,例如 in_voltage0_raw。如果 ADC 支持转换函数,还可能出现 in_voltage0_scale 等属性。

8.2 设备树配置 ADC 通道

以常见嵌入式 SoC 为例,假设其 ADC 控制器支持多个通道,设备树中需要声明使用哪些通道以及如何连接。以下是一个简化示例,表示 PA0 作为 ADC 输入:

&adc {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&adc_pin>;
channel@0 {
    reg = &lt;0&gt;;
    label = "ldr_sensor";
};
};

不同 SoC 的 ADC 设备树绑定方式不同,需要参考具体芯片的 Documentation/devicetree/bindings 目录。设备树中的 label 属性在部分驱动中会用于命名 sysfs 下的通道节点。

8.3 通过 sysfs 读取 ADC 值

驱动程序加载后,可以通过 sysfs 读取 ADC 原始值:

cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw

如果存在 scale 属性,可以查询原始值到毫伏的转换系数:

cat /sys/bus/iio/devices/iio:device0/in_voltage0_scale

读取到的 raw 值乘以 scale 即可得到毫伏电压。实际读取到的电压范围取决于 ADC 量程和 LDR 分压电路。

8.4 C 语言用户空间读取示例

在 Linux 用户空间程序中,可以打开 IIO 的 sysfs 文件或字符设备来获取采样值。以下代码演示通过 sysfs 读取 ADC 原始值并换算为电压:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define RAW_PATH "/sys/bus/iio/devices/iio:device0/in_voltage0_raw"
#define SCALE_PATH "/sys/bus/iio/devices/iio:device0/in_voltage0_scale"
static int readIntFile(const char *path) {
char buf[32];
int fd = open(path, O_RDONLY);
if (fd < 0) {
return -1;
}
int len = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (len <= 0) {
return -1;
}
buf[len] = '\0';
return atoi(buf);
}
static float readFloatFile(const char *path) {
char buf[64];
int fd = open(path, O_RDONLY);
if (fd < 0) {
return 0.0f;
}
int len = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (len <= 0) {
return 0.0f;
}
buf[len] = '\0';
return strtof(buf, NULL);
}
int main(void) {
int raw = readIntFile(RAW_PATH);
float scale = readFloatFile(SCALE_PATH);
if (raw < 0) {
printf("failed to read adc\n");
return 1;
}
float millivolts = raw * scale;
printf("raw=%d, voltage=%.2f mV\n", raw, millivolts);
return 0;
}

sysfs 文件读取方式实现简单,适合周期较长的采样任务。对于高速采样,IIO 提供缓冲区和触发机制,通过字符设备读取连续的采样数据。

8.5 IIO 缓冲区与触发器

现代 IIO 驱动支持基于触发器的缓冲采样。常见的触发器包括 hrtimer 软定时器触发和外部 GPIO 触发。对于 LDR 这类慢速信号,通常使用 hrtimer 触发器,按照固定周期填充缓冲区,用户空间通过异步读取或 poll 获取数据。

启用缓冲后,设备目录下会新增 buffer 相关文件,例如 /sys/bus/iio/devices/iio:device0/buffer/enable。使用前需要先选择触发器并将缓冲区长度设置为非零值。具体操作步骤如下:

# 关联 hrtimer 触发器
echo hrtimer > /sys/bus/iio/devices/iio:device0/trigger/current_trigger
# 设置缓冲区长度
echo 64 > /sys/bus/iio/devices/iio:device0/buffer/length
# 使能缓冲区
echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable

之后用户空间程序可以打开 /dev/iio:device0 读取二进制采样数据。读取到的数据按通道顺序排列,每个样本的字节数取决于 ADC 精度。

8.6 编写简单的 IIO ADC 驱动

如果 SoC 没有现成的 ADC 驱动,或者使用外部 I2C/SPI ADC 芯片,可以编写 IIO 驱动。完整的 IIO 驱动涉及设备树解析、通道定义、触发器和缓冲区支持,代码较长。以下给出一个极简的 IIO 驱动框架,帮助读者了解基本结构:

#include <linux/module.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
struct ldr_state {
struct iio_dev *indio_dev;
int adc_channel;
};
static int ldr_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask) {
struct ldr_state *st = iio_priv(indio_dev);
switch (mask) {
case IIO_CHAN_INFO_RAW:
// 调用硬件 ADC 读取接口,将结果写入 val
// val[0] = adc_read(st->adc_channel);
return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE:
// 提供 raw 到毫伏的转换系数
*val = 3300;
*val2 = 4095;
return IIO_VAL_FRACTIONAL;
default:
return -EINVAL;
}
}
static const struct iio_info ldr_info = {
.read_raw = ldr_read_raw,
};
static int ldr_probe(struct platform_device *pdev) {
struct iio_dev *indio_dev;
struct ldr_state *st;
int ret;
indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*st));
if (!indio_dev) {
return -ENOMEM;
}
st = iio_priv(indio_dev);
st->adc_channel = 0;
indio_dev->name = "ldr";
indio_dev->info = &ldr_info;
indio_dev->modes = INDIO_DIRECT_MODE;
ret = devm_iio_device_register(&pdev->dev, indio_dev);
if (ret) {
return ret;
}
return 0;
}
static const struct of_device_id ldr_of_match[] = {
{ .compatible = "vendor,ldr-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, ldr_of_match);
static struct platform_driver ldr_driver = {
.probe = ldr_probe,
.driver = {
.name = "ldr-sensor",
.of_match_table = ldr_of_match,
},
};
module_platform_driver(ldr_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple LDR IIO driver framework");
MODULE_AUTHOR("Embedded Developer");

上述代码只是一个框架,真正的 ADC 读取函数需要与具体硬件接口对接。对于生产项目,建议优先选用 SoC 厂商提供或内核主线已有的 ADC 驱动,避免重复开发。

9. 数据处理与驱动算法

采集到 LDR 的原始 ADC 值只是驱动工作的第一步。为了得到稳定可靠的光照信息,还需要进行数字滤波、标定、线性化和迟滞控制等处理。本章集中讨论这些算法。

9.1 均值滤波

均值滤波是最常用的平滑方法。对连续的 N 次采样求算术平均,可以显著减小随机噪声。实现方式有普通平均和滑动平均两种。

普通平均在固定时间窗口内采样 N 次后计算一次平均值。响应速度取决于 N 与采样周期的乘积。滑动平均则维护一个固定长度的队列,每来一个新样本就替换最旧样本并重新计算平均值。滑动平均的更新频率更高,适合需要连续输出的场合。

以下是一个通用滑动平均实现:

#define WINDOW_SIZE 8
typedef struct {
uint16_t buffer[WINDOW_SIZE];
uint8_t index;
uint32_t sum;
uint8_t count;
} moving_avg_t;
void movingAvgInit(moving_avg_t *filter) {
filter->index = 0;
filter->sum = 0;
filter->count = 0;
for (int i = 0; i < WINDOW_SIZE; i++) {
filter->buffer[i] = 0;
}
}
uint16_t movingAvgUpdate(moving_avg_t *filter, uint16_t sample) {
uint16_t oldest = filter->buffer[filter->index];
filter->buffer[filter->index] = sample;
filter->index = (filter->index + 1) % WINDOW_SIZE;
filter->sum = filter->sum - oldest + sample;
if (filter->count < WINDOW_SIZE) {
filter->count++;
return (uint16_t)(filter->sum / filter->count);
}
return (uint16_t)(filter->sum / WINDOW_SIZE);
}

9.2 中值滤波

中值滤波对尖锐的脉冲干扰特别有效。例如,继电器触点动作、电机启动或外部静电放电可能在 ADC 信号线上产生单个异常高或异常低的采样值。对这些值进行排序并取中值,可以几乎完全消除这种偶发毛刺,而不会像均值滤波那样将脉冲值平均进来。

中值滤波的窗口大小通常取奇数,例如 3、5 或 7。窗口越大,抑制脉冲的能力越强,但计算量越大。对于 STM32、ESP32 等具有一定算力的平台,窗口为 5 的中值滤波可以很容易实时运行。

9.3 一阶低通滤波

一阶低通滤波的公式如下:

Y[n] = alpha * X[n] + (1 - alpha) * Y[n-1]

其中 X[n] 是当前采样值,Y[n-1] 是上一次滤波输出,alpha 是滤波系数。该滤波器的直流增益为 1,可以有效滤除高频噪声。alpha 与截止频率和采样周期的关系为:

alpha = 2 * PI * fc * T / (2 * PI * fc * T + 1)

其中 fc 是期望截止频率,T 是采样周期。实际设计中可以先确定所需响应时间,再反推 alpha。

9.4 卡尔曼滤波

对于需要更精确估计的光照强度,可以使用一维卡尔曼滤波。卡尔曼滤波根据系统模型和测量噪声,在预测和更新两个步骤之间迭代,给出最小均方误差意义下的最优估计。

以下是一维卡尔曼滤波的简单实现,适合处理 LDR 这种变化缓慢的信号:

typedef struct {
  float q;      // 过程噪声协方差
  float r;      // 测量噪声协方差
  float x;      // 状态估计
  float p;      // 误差协方差
  float k;      // 卡尔曼增益
} kalman1d_t;
void kalmanInit(kalman1d_t *kf, float processNoise, float measureNoise) {
kf->q = processNoise;
kf->r = measureNoise;
kf->x = 0;
kf->p = 1;
kf->k = 0;
}
float kalmanUpdate(kalman1d_t *kf, float measurement) {
kf->p = kf->p + kf->q;
kf->k = kf->p / (kf->p + kf->r);
kf->x = kf->x + kf->k * (measurement - kf->x);
kf->p = (1 - kf->k) * kf->p;
return kf->x;
}

过程噪声 q 和测量噪声 r 的选择直接影响滤波性能。r 越小,滤波器越信任测量值,响应越快但噪声抑制越弱;q 越小,滤波器越信任模型,输出越平滑但越容易滞后。实际调试中可以先用均值滤波观察噪声方差,再据此设置参数。

9.5 光照标定与映射

由于 LDR 的非线性和离散性,直接把 ADC 值映射成勒克斯需要标定。标定方法分为单点标定和多点标定。

单点标定简单方便,适用于只需要粗略照度参考的场合。做法是在已知照度 E0 下记录 ADC 输出,然后假设阻值与照度满足幂函数关系,利用数据手册中的典型 γ 值推算其他照度。

多点标定精度更高。在多个照度点记录 ADC 值和参考照度,计算每个点对应的 LDR 阻值,再通过最小二乘拟合求出 A 和 γ。拟合可以在 PC 上用脚本完成,得到参数后写入固件。

以下是一个简化标定思路:

1. 搭建可调光源,使用照度计测量实际照度。
2. 记录不同照度下的 LDR 阻值。
3. 对 R = A * E^(-gamma) 两边取对数,得到
   log(R) = log(A) - gamma * log(E)。
4. 以 log(E) 为横坐标、log(R) 为纵坐标做线性拟合,
   斜率即 -gamma,截距即 log(A)。

标定完成后,固件中输入阻值即可按公式输出近似的勒克斯值。需要注意,标定结果对光源光谱敏感,日光灯、LED 和白炽灯下的结果可能存在差异。

9.6 分段线性化

如果不使用幂函数模型,也可以将 LDR 的响应曲线分段线性化。将关心的照度区间划分为若干段,每段内用直线近似。固件中通过查表或者分段插值得到光照值。这种方法实现直观,标定后精度较高,适合内存紧张但有一定 Flash 空间的单片机。

9.7 迟滞控制

在开关类应用中,迟滞控制可以避免临界点抖动。其核心思想是设置两个阈值:动作阈值和释放阈值。当状态为“关”时,信号必须低于动作阈值才执行“开”;当状态为“开”时,信号必须高于释放阈值才执行“关”。两个阈值之间的区域就是迟滞带。

例如,天黑自动开灯系统中,可以设置开灯阈值为 1800,关灯阈值为 2100。当 ADC 值低于 1800 时开灯,之后只有高于 2100 时才关灯。这样即使光照在 1900 附近波动,也不会发生频繁切换。

迟滞带宽的选择需要结合噪声幅度和实际需求。带宽过小,抗抖效果差;带宽过大,系统对光照变化的响应迟钝。通常可以先观察临界状态的采样波动,将迟滞带宽设置为噪声峰峰值的 2 到 3 倍。

9.8 自适应环境光补偿

在一些复杂场景中,环境光不仅包含自然光,还包含本设备发出的光。例如,智能台灯自身的光线可能照射到 LDR 上,形成反馈,导致亮度调节不稳定。此时可以引入自适应补偿:在关闭本设备光源的短时间内采样环境光,再开启光源并采样总光强,从而分离出人工光照分量。

另一种方法是在 LDR 安装位置上做物理遮挡,使传感器尽量只接收外部环境光。对于调光系统,还可以采用闭环控制,根据目标照度与当前照度的差值逐步调节输出,避免突变。

10. 典型应用场景与工程实践

理解了原理和驱动方法后,本章通过若干个实际场景,说明 LDR 在不同产品中的设计要点和常见问题。

10.1 自动路灯控制

自动路灯是 LDR 最经典的应用之一。其核心逻辑是:白天光照充足时关闭路灯,夜晚光线变暗时自动开启。工程中应注意:

  • LDR 应安装在能够直接感知天空亮度的位置,避免被灯罩、树荫或车灯频繁干扰。
  • 必须设计足够的迟滞,防止黄昏和黎明时段继电器或接触器频繁动作。
  • 考虑雷雨天气下天色骤暗又快速恢复的情况,可通过软件延长确认时间,只有持续暗超过一定时长才开灯。
  • 若使用继电器驱动高压路灯,应做好电气隔离和浪涌保护。

10.2 智能家居照明

在智能家居中,LDR 可以作为照明自动化的输入之一。例如,检测到房间较暗且有人时自动打开氛围灯。系统通常会将 LDR 数据上传到主控或云端,结合人体红外传感器、门磁等共同决策。设计时需要注意 LDR 安装位置避免被窗帘、家具遮挡,并定期上报数据以便统计白天夜间照度变化。

10.3 屏幕亮度自动调节

手机、平板和车载显示屏会根据环境光自动调节背光亮度。LDR 可以作为低成本方案之一,通过 ADC 采集环境光并调节背光 PWM 占空比。由于 LDR 响应较慢,屏幕背光自身的反光也可能干扰采样,因此安装时需要将传感器对准外部环境,并考虑屏幕发出的光是否会被外壳反射回传感器。

亮度映射通常不是线性的,人眼对亮度的感知近似对数关系。因此,将 LDR 测量值映射到背光占空比时,建议采用对数或指数查表曲线,使视觉亮度变化平滑自然。

10.4 相机测光

传统胶片相机和部分简易数码相机使用 CdS 光敏电阻进行测光。测光电路根据 LDR 阻值变化控制快门速度或光圈。现代数码相机大多使用精度更高的硅光敏器件,但 LDR 在低成本、教学演示和复古相机复刻项目中仍有应用。

10.5 安防夜灯与感应照明

安防夜灯通常要求白天不亮、夜间有人时点亮。LDR 负责昼夜判断,PIR 人体传感器负责人体检测,两者条件同时满足时驱动照明。LDR 在此场景中采样频率要求不高,重点是与 PIR 的配合时序和抗干扰设计。

10.6 植物补光系统

温室或室内种植常使用补光灯。LDR 可以检测自然光是否充足,当光照低于设定值时开启补光灯。需要注意的是,植物生长关注的光谱与 LDR 的响应光谱并非完全一致,特别是红光和蓝光成分。因此,LDR 更适合做粗略的昼夜或阴晴判断,精确的 PAR(光合有效辐射)测量应使用专用传感器。

10.7 遮阳帘与电动窗帘

自动遮阳系统可以根据太阳光强度控制窗帘开合。LDR 安装位置应优先选择能接收直射阳光的方向,同时要避免室内灯光干扰。由于遮阳帘动作会改变进入室内的光线,形成反馈,需要合理设置迟滞和动作保持时间,避免反复开合。

11. 调试技巧与故障排查

LDR 驱动电路虽然简单,但在实际调试中仍可能遇到各种问题。本章汇总常见现象、原因和排查方法。

11.1 采样值不随光照变化

如果 ADC 读数始终不变,应依次检查:

  • LDR 是否损坏或焊接不良,可用万用表电阻档测量 LDR 两端阻值,用手遮挡时光照变化应引起阻值明显变化。
  • 分压电阻是否接错、虚焊或选值过大过小。
  • ADC 引脚配置是否正确,是否存在引脚复用冲突。
  • 供电源是否稳定,LDR 分压网络是否真正通电。

11.2 采样值在临界点抖动

如果系统在亮暗临界点频繁开关,通常是缺少迟滞。应检查比较器是否加入了正反馈迟滞,或软件中是否设置了两个阈值。同时检查电源纹波和走线干扰,必要时增加滤波电容和数字滤波。

11.3 采样值整体偏低或偏高

这通常与分压电阻阻值、供电电压或接线方式有关。根据实际电路重新推导输出电压公式,并确认代码中使用的 VCC 和分压电阻值与实际一致。若使用 ESP32,还需确认衰减配置和校准数据是否正确。

11.4 不同器件读数差异大

LDR 的离散性较大,同一型号不同批次可能出现明显差异。如果产品对一致性有要求,应增加生产标定工序,或将动作阈值设置为较宽松的范围。必要时改用光敏二极管加运放的方案,或使用数字环境光传感器。

11.5 响应速度慢

LDR 本身下降时间较长,如果系统感觉响应迟钝,先确认是否因滤波过度或采样间隔过长。均值滤波窗口过大、低通滤波系数过小都会额外增加延迟。可以在不显著增加噪声的前提下适当减小滤波强度,或减小采样周期。

11.6 户外环境误动作

户外使用时,闪电、车灯、飞鸟遮挡等情况可能造成短时光照突变。可以在软件中增加持续采样确认,例如要求连续 5 秒满足暗阈值才动作。同时,硬件上增加屏蔽和密封,避免雨水和灰尘影响传感器表面透光率。

11.7 使用示波器与万用表快速定位

调试 LDR 电路时,万用表和示波器是重要工具。万用表可以测量 LDR 和分压电阻的静态阻值、工作电压;示波器可以观察分压输出的噪声、波动和响应波形。建议重点观察以下位置:

  • LDR 两端电压是否随光照变化。
  • 比较器输入端是否存在噪声毛刺。
  • ADC 引脚波形是否干净。
  • 电源轨是否存在周期性纹波。

12. 安全与可靠性设计

在面向产品的 LDR 应用中,可靠性和安全性同样重要。本章补充一些工程设计层面的注意事项。

12.1 器件老化与失效模式

CdS 光敏电阻长期暴露在紫外线和潮湿环境中可能出现光敏层老化、密封失效等问题,导致阻值漂移或灵敏度下降。户外产品应选择密封良好的封装,并做好防水防尘设计。对于高可靠性场景,可以在系统中加入传感器自检逻辑,例如定期检查暗电阻是否落在合理范围内。

12.2 冗余与容错

如果光照检测结果直接关系到安全(如应急照明、安防照明),可以考虑双 LDR 冗余采样,或者将 LDR 与定时钟、手动开关结合,形成多重判断机制。当 LDR 数据异常时,系统应能够切换到时间表控制或默认安全状态,而不是长期错误关灯。

12.3 电磁兼容

LDR 信号线在长距离布线时容易接收电磁干扰。建议使用屏蔽线,并在输入端增加 RC 滤波。对于传导干扰较大的环境,可在 ADC 前端增加共模电感或钳位保护器件,同时保证 PCB 地平面完整。

12.4 静电防护

LDR 暴露在外部环境中,人体触碰或雷雨天气可能引入静电。应在信号输入端增加 TVS 或 ESD 保护二极管,并在结构上尽量避免用户直接接触传感器引线。

13. 总结与进阶方向

本文从光敏电阻的物理原理出发,系统梳理了 LDR 的技术参数、硬件电路、多平台驱动方法、数据处理算法和典型工程应用。总结而言,LDR 驱动虽然入门门槛低,但要做得稳定、可靠并适应真实产品环境,仍然需要关注以下几个核心点:

  • 理解器件特性:掌握亮电阻、暗电阻、γ 值和响应时间,才能合理选择分压电阻和采样策略。
  • 设计合理的信号链路:根据精度和成本要求,选择直连 ADC、运放缓冲或迟滞比较器等方案。
  • 编写稳健的驱动代码:结合具体平台的 ADC 外设能力,使用 DMA、滤波和中断等机制提高采样质量。
  • 重视算法与标定:通过滤波、迟滞、标定和光照映射,把原始 ADC 值转化为可靠的光照信息。
  • 兼顾低功耗与可靠性:在电池供电和户外应用中,合理设计间歇采样、分压支路开关和保护电路。

对于希望进一步深入的读者,可以从以下几个方向继续探索:

  • 使用环境光传感器 IC(如 BH1750、VEML7700、OPT3001)替代 LDR,获得直接的数字光照读数、更好的线性度和一致性。
  • 研究 IIO 子系统的触发器和缓冲区机制,实现 Linux 下更高性能的传感器采集。
  • 结合机器学习或统计方法,对多路光照数据进行趋势预测和异常检测。
  • 设计完整的太阳能路灯或温室光照控制系统,综合运用电源管理、无线通信和云平台。

光敏电阻作为一个经典的模拟传感器,既是嵌入式学习的理想起点,也是众多真实产品中成本与性能的平衡选择。希望本文能够帮助读者从原理到实践真正掌握 LDR 的驱动方法,并在自己的项目中灵活运用。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐