4、传感器集成:光敏电阻与ADC采集、人体红外传感器(PIR)检测、根据环境亮度自动调节灯光

这一章,我们开始给灯控系统装上「眼睛」和「耳朵」。

说白了,就是让灯能感知环境亮度和人体存在。我刚开始做智能家居项目时,觉得传感器就是读个数值而已,后来踩了不少坑才明白——传感器选型、电路设计、采样策略,每一步都藏着细节。

4.1 光敏电阻与ADC采集

光敏电阻,也叫光依赖电阻(LDR)。它的原理很简单:光照越强,电阻越小;光照越弱,电阻越大。我们通过测量电阻变化,就能反推出环境亮度。

但微控制器不能直接读电阻值,得用ADC(模数转换器)把模拟电压转成数字量。我习惯用分压电路来接光敏电阻。

4.1.1 典型分压电路

把光敏电阻和一个固定电阻串联,中间抽头接ADC引脚。固定电阻选10kΩ比较通用。供电电压3.3V或5V都行,但ADC参考电压要匹配。

电路连接示意:
VCC (3.3V)
   │
   ├── LDR (光敏电阻)
   │
   ├── ADC_PIN (中间抽头)
   │
   ├── R_fixed (10kΩ)
   │
  GND

ADC读到的值跟光照强度成反比。数值越大,说明环境越暗。嗯,这里要注意:不同型号的光敏电阻,暗电阻和亮电阻范围差别很大。我遇到过用国产5516和进口5537,同样的电路,读数差了30%。

我的习惯:每次换批次的LDR,都重新标定一次。用手机闪光灯和全黑环境测两个端点值,然后线性插值。

4.1.2 ADC采样代码示例

以ESP32为例,它的ADC是12位的,范围0~4095。我一般做多次采样取平均,滤掉噪声。

#include <driver/adc.h>

#define LDR_PIN ADC1_CHANNEL_0  // GPIO36
#define SAMPLE_COUNT 10

void setup() {
    Serial.begin(115200);
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(LDR_PIN, ADC_ATTEN_DB_11);
}

void loop() {
    uint32_t sum = 0;
    for (int i = 0; i < SAMPLE_COUNT; i++) {
        sum += adc1_get_raw(LDR_PIN);
        delay(10);
    }
    uint16_t avg = sum / SAMPLE_COUNT;
    Serial.printf("LDR ADC Value: %d\n", avg);
    delay(500);
}

你想想看,如果只采一次,刚好碰到电源纹波尖峰,读数就飘了。我吃过这个亏,后来一律用滑动平均。

4.2 人体红外传感器(PIR)检测

PIR传感器检测人体移动,靠的是热释电效应。人经过时,红外辐射变化被感应,输出高电平。静止不动时,输出低电平。

市面上常见的HC-SR501模块,我用了不下两百个。它有三个关键调节:

调节项 作用 我的推荐值
灵敏度 检测距离(3~7米) 室内用中档,约5米
延时时间 触发后保持高电平的时间 5~10秒,避免频繁翻转
光敏控制 是否只在暗光下工作 关闭,由主控统一判断
我曾经踩过的坑:PIR模块上电后需要30~60秒稳定期,期间会乱触发。我一开始没注意,灯在刚上电时疯狂闪烁。后来加了个启动延时,等稳定后再启用检测。

4.2.1 PIR读取代码

#define PIR_PIN 14

void setup() {
    pinMode(PIR_PIN, INPUT);
    Serial.println("PIR warming up...");
    delay(60000);  // 等待稳定
    Serial.println("PIR ready.");
}

void loop() {
    int motion = digitalRead(PIR_PIN);
    if (motion == HIGH) {
        Serial.println("Motion detected!");
        // 触发亮灯逻辑
    } else {
        Serial.println("No motion.");
    }
    delay(200);
}

这里有个细节:PIR输出是数字信号,直接接GPIO就行。但如果你用3.3V的MCU接5V的PIR模块,记得加电平转换。我烧过一块ESP32的引脚,就是因为没注意这个。

4.3 根据环境亮度自动调节灯光

现在传感器数据都有了,怎么联动?

我的策略很简单:

  • 环境暗 + 有人移动 → 开灯
  • 环境亮 → 关灯(不管有没有人)
  • 环境暗 + 无人移动超过N秒 → 关灯

说白了,就是「亮度优先,人体触发」。你想想看,白天灯开着也没意义,还费电。

4.3.1 完整联动逻辑

#define LIGHT_THRESHOLD 2500  // ADC值大于此认为环境暗
#define TIMEOUT_SECONDS 30    // 无人后延时关灯

uint16_t ldr_value;
int motion_state;
unsigned long last_motion_time = 0;
bool light_on = false;

void loop() {
    // 1. 读取传感器
    ldr_value = read_ldr_average();
    motion_state = digitalRead(PIR_PIN);

    // 2. 更新最后移动时间
    if (motion_state == HIGH) {
        last_motion_time = millis();
    }

    // 3. 判断是否开灯
    bool should_light_on = false;

    if (ldr_value > LIGHT_THRESHOLD) {
        // 环境暗
        if (motion_state == HIGH) {
            should_light_on = true;
        } else {
            // 无人移动,检查超时
            if (millis() - last_motion_time < TIMEOUT_SECONDS * 1000) {
                should_light_on = true;
            }
        }
    }

    // 4. 控制灯光
    if (should_light_on && !light_on) {
        digitalWrite(RELAY_PIN, HIGH);
        light_on = true;
        Serial.println("Light ON");
    } else if (!should_light_on && light_on) {
        digitalWrite(RELAY_PIN, LOW);
        light_on = false;
        Serial.println("Light OFF");
    }

    delay(100);
}
核心思路:用「最后移动时间戳」做超时判断,而不是单纯依赖PIR的当前状态。这样人坐下不动时,灯不会立刻灭掉。

4.4 本章知识体系

下面这张图,把传感器、ADC、PIR和灯光控制的整体流程串起来了。我画图时习惯把数据流向标清楚,调试时一眼就能看出问题出在哪一环。

传感器集成与自动调光流程图 传感器集成与自动调光流程 环境光 LDR + ADC 采集 人体移动 PIR 检测 决策逻辑 亮度阈值 + 人体超时 灯光控制(开/关) 环境暗 + 有人 → 开灯 环境亮 → 关灯

4.5 避坑与优化建议

最后,分享几个我实际项目中总结的经验:

  • ADC参考电压要稳。如果MCU用电池供电,电压会漂,ADC读数也跟着漂。我一般用内部参考电压或外接基准芯片。
  • PIR不要正对空调或暖气。热风会让PIR误触发。我遇到过客户说灯半夜自己亮,结果发现是空调出风口对着传感器吹。
  • 光敏电阻不要装在灯罩正下方。灯一开,光敏电阻直接照到,读数变亮,灯又关了,形成振荡。我习惯把传感器引到灯体侧面或单独开孔。
  • 超时时间别设太短。10秒以内的话,人稍微停一下灯就灭了,体验很差。30秒到1分钟比较合理。
一个小技巧:调试时把ADC值和PIR状态通过串口打印出来,用串口绘图工具看波形。一眼就能看出阈值设得合不合理、PIR有没有误触发。