一、硬件介绍(Hardware)
1.1 主控板
Arduino Nano RP2040 ConnectMCU:Raspberry Pi RP2040(双核 ARM Cortex-M0+,133 MHz)
封装:Arduino Nano 经典尺寸(18 × 45 mm),兼容 Grove 扩展板
工作电压:3.3 V(逻辑电平 3.3 V,Grove 模块同电平域,无需电平转换)
数字 IO:20 个,全部支持 INPUT_PULLUP
模拟输入:8 个 12-bit ADC(本次用 A2 作为光强输入)
开发框架:ArduinoCore-mbed(PlatformIO platform = raspberrypi)
1.2 Grove 模块清单
板上集成了红外发射 LED + 红外敏感光电晶体管;
本项目将其作为"人体存在检测"使用:红外光被人体/物体反射回晶体管时输出 HIGH;
实测电平:无遮挡时输出 HIGH(ir_raw=1),被遮挡时输出 LOW(ir_raw=0)。
因此判定逻辑 presence = (ir_raw == LOW)。
不同批次模块的电平定义可能反转;首次使用时务必用串口确认。
板上 LDR(光依赖电阻)+ LM358 双运放;
光强 ↑ → LDR 阻值 ↓ → 运放输出电压 ↑ → analogRead 数值 ↑;
12-bit ADC 范围 0..4095;本工程实测典型值见 §3.4;
输出阻抗很低,可直接驱动 ADC,无需额外缓冲。
数字常开开关,控制端低电压(3.3 V / 5 V)即可控制高电压负载;
模块上有 LED 指示灯,吸合时点亮;
本项目用其控制外部照明负载,并把板载 LED 与 Grove LED 同步点亮以方便肉眼观察。
板上 LED + 保护电阻 ;
数字输数:HIGH 点亮,LOW 熄灭;
作为"继电器已触发"的本地控制。
1.3 接线总览
Arduino Nano RP2040 Connect ┌────────────────────────────────┐ │ D2 ──┼──► Line Finder v1.1 (SIG) │ A2 ──┼──► Light Sensor v1.2 (SIG, 模拟) │ | │ D6 ──┼──► Relay (吸合控制)-LED (模拟照明) └────────────────────────────────┘

二、软件设计(Software Design)
程序结构总览
整个固件只有一对 setup() / loop(),扁平结构:main.cpp ├── 文件头注释(接线、行为、ADC 范围、实测电平) ├── 引脚 / 参数常量 ├── 红外状态机结构 IRState + 光强结构 LightState ├── updatePresence() ← 红外防抖 + 状态翻转 ├── shouldLightOn() ← 滞回决策 ├── applyLight() ← 输出同步(Relay + LED) ├── setup() ← 引脚 + 串口 + 启动信息 └── loop() ├── 1) updatePresence(now) ← 每 30 ms 采样一次红外 ├── 2) analogRead(A2) ← 一次性读光强 ├── 3) shouldLightOn(L) / applyLight ← 决策 + 应用 └── 4) 节流日志
决策模型
2.3.1 真值表滞回窗口 [DARK_LAMP_OFF, DARK_LAMP_ON];
灯状态在窗口内不翻转,避免临界值反复跳变(chatter);
当前实测 DARK_LAMP_OFF = 500、DARK_LAMP_ON = 1000,开灯稳态 lightRaw ≈ 600 落在窗口正中,距两条边界各 400,充分抗自激。
关键算法
2红外防抖(连续 N 次一致)const uint8_t IR_DEBOUNCE_N = 3; const uint16_t IR_SAMPLE_MS = 30; // 30 ms × 3 = 90 ms 才允许翻状态
每 30 ms 采样一次 digitalRead(PIN_IR);
连续 IR_DEBOUNCE_N 次一致才允许 presence 翻转;
翻转瞬间重置两侧计数器,重新累计;
物理遮挡通常 ≥ 200 ms,90 ms 防抖不会漏检,但能完全吃掉电磁毛刺与抖动。
static bool shouldLightOn(uint16_t lightRaw) {
if (light.lightOn) {
if (!ir.presence) return false; // 无人 -> 关
if (lightRaw > DARK_LAMP_ON) return false; // 环境变亮 -> 关
return true; // 滞回区 -> 保持开
} else {
if (!ir.presence) return false;
if (lightRaw >= DARK_LAMP_OFF) return false; // 还没暗到能开
return true; // 暗 + 有人 -> 开
}
}输出同步static void applyLight(bool want) {
if (light.lightOn == want) return; // 状态未变 -> 不写引脚
light.lightOn = want;
digitalWrite(PIN_RELAY, want ? RELAY_ON : (uint8_t)!RELAY_ON);
}每次只写一次 Relay + Lamp;
状态未变时不写,避免抖动时引脚反复翻转;
RELAY_ON常量集中顶部,便于适配不同极性模块。
时间参数
三、功能展示

#include <Arduino.h>
const uint8_t PIN_IR = 2; // 丝印 D2, Line Finder SIG
const uint8_t PIN_LIGHT = A2; // 丝印 A2, Light Sensor SIG (模拟)
const uint8_t PIN_RELAY = 6; // 丝印 D6, Relay SIG
const uint8_t PIN_LAMP = 5; // 丝印 D5, Lamp LED SIG (模拟照明)
const uint16_t DARK_LAMP_OFF = 500;
const uint16_t DARK_LAMP_ON = 1000;
const uint8_t IR_DEBOUNCE_N = 3; // 连续 N 次一致才翻状态
const uint16_t IR_SAMPLE_MS = 30; // 每 30 ms 采样一次
const uint32_t LOG_INTERVAL_MS = 200;
const uint8_t RELAY_ON = HIGH;
const uint8_t LED_ON = HIGH;
struct IRState {
bool presence = false; // 当前是否判定"有人"
bool lastRawBlocked = false;// 上一次的红外原始值 (true=被遮挡)
uint8_t runBlocked = 0; // 连续被遮挡计数
uint8_t runClear = 0; // 连续通畅计数
uint32_t lastSampleMs = 0;
} ir;
struct LightState {
bool lightOn = false; // 当前灯状态 (true=开)
} light;
uint32_t lastLogMs = 0;
static inline bool irBlockedNow() {
return digitalRead(PIN_IR) == LOW; // LOW -> 被遮挡 -> presence
}
static void updatePresence(uint32_t now) {
if ((uint16_t)(now - ir.lastSampleMs) < IR_SAMPLE_MS) return;
ir.lastSampleMs = now;
const bool blocked = irBlockedNow();
if (blocked == ir.lastRawBlocked) {
// 跟上次一样: 清零对侧计数, 同侧 +1
if (blocked) { ir.runBlocked++; ir.runClear = 0; }
else { ir.runClear++; ir.runBlocked = 0; }
} else {
// 翻转: 重置两个计数, 重头开始累计
ir.lastRawBlocked = blocked;
if (blocked) { ir.runBlocked = 1; ir.runClear = 0; }
else { ir.runClear = 1; ir.runBlocked = 0; }
}
if ( ir.presence && ir.runClear >= IR_DEBOUNCE_N) ir.presence = false;
if (!ir.presence && ir.runBlocked >= IR_DEBOUNCE_N) ir.presence = true;
}
static bool shouldLightOn(uint16_t lightRaw) {
if (light.lightOn) {
if (!ir.presence) return false; // 没人 -> 关
if (lightRaw > DARK_LAMP_ON) return false; // 环境变亮 -> 关
return true; // 滞回区 -> 保持开
} else {
if (!ir.presence) return false;
if (lightRaw >= DARK_LAMP_OFF) return false; // 还没暗到能开
return true; // 暗 + 有人 -> 开
}
}
static void applyLight(bool want) {
if (light.lightOn == want) return;
light.lightOn = want;
digitalWrite(PIN_RELAY, want ? RELAY_ON : (uint8_t)!RELAY_ON);
digitalWrite(PIN_LAMP, want ? LED_ON : (uint8_t)!LED_ON);
}
void setup() {
pinMode(PIN_IR, INPUT);
pinMode(PIN_LIGHT, INPUT);
pinMode(PIN_RELAY, OUTPUT);
pinMode(PIN_LAMP, OUTPUT);
// 起始: 继电器断开 + 模拟照明熄灭
digitalWrite(PIN_RELAY, (uint8_t)!RELAY_ON);
digitalWrite(PIN_LAMP, (uint8_t)!LED_ON);
Serial.begin(115200);
uint32_t t0 = millis();
while (!Serial && (millis() - t0) < 2000) { /* 等待 USB CDC */ }
Serial.println();
Serial.println("=== Light + PIR Auto Lamp ===");
Serial.println(" IR=D2, Light=A2, Relay=D6, Lamp=D5");
Serial.print(" DARK_LAMP_OFF="); Serial.print(DARK_LAMP_OFF);
Serial.print(" DARK_LAMP_ON="); Serial.print(DARK_LAMP_ON);
Serial.println(" (12-bit ADC, lightRaw < 500 = dark, > 1000 = bright)");
Serial.println(" ON when blocked && dark (with hysteresis)");
Serial.println();
}
// ---------------------------------------------------------------------
// loop
// ---------------------------------------------------------------------
void loop() {
const uint32_t now = millis();
updatePresence(now);
const uint16_t lightRaw = analogRead(PIN_LIGHT);
const bool want = shouldLightOn(lightRaw);
applyLight(want);
if (now - lastLogMs >= LOG_INTERVAL_MS) {
lastLogMs = now;
Serial.print("ir_raw="); Serial.print(digitalRead(PIN_IR));
Serial.print(" L="); Serial.print(lightRaw);
Serial.print(" presence="); Serial.print((int)ir.presence);
Serial.print(" lamp="); Serial.print((int)light.lightOn);
Serial.println();
}
delay(20);
}
我要赚赏金
