实战课题

第 26 课:光控小夜灯

第一个能拿回家的东西——天黑了它自己亮,天亮了它自己灭

我是吴老师。从这节课开始,你做的东西不一样了。

前面二十五节课,你手里过了一堆模块:点过灯、读过按钮、量过温度、让蜂鸣器响过。但那些东西最后都拆了,收回盒子里了。

这一节做的东西,是要拿回家的。

一、先说它长什么样、能干嘛

半夜两点,你妈起来倒水。房间里黑着,她不敢开大灯——一开,你和你爸全醒了。于是她摸着黑走:先摸床头柜,摸到你的水杯;再摸墙角,用脚尖探路。走到客厅,脚趾头撞在茶几角上。

我妈干这事干了十几年。

所以第一个实战课题,我们做一个放在床头或走廊的小夜灯

  • 房间亮着的时候,它不亮。 白天、开着灯的晚上,它一声不吭。
  • 天一黑,它自己亮。 光不强,但够你看清拖鞋在哪。
  • 天亮了,它自己灭。 没有任何开关,你妈什么都不用按。

这东西淘宝上十几块钱一个。但你做的这个不一样:它长什么样、亮到什么程度、多黑才亮,全是你自己定的。而且你知道它内部在干什么——这句话的分量,比"省了十几块钱"重得多。

二、这个课题的三个环节

第 9 课那张图,拿出来温习一遍:

完整闭环:传感器感知、主控判断、执行器动作,三个环节缺一不可

环节 这个课题里是什么 具体到引脚
传感器(看) 光敏传感器,一直在读"现在有多亮" GPIO0(模拟量)
判断(想) 板子比一下:这个数算"暗"吗? if 加一个阈值
执行器(做) LED 模块,亮或者灭 GPIO3

看着眼熟吗? 这跟第 11 课那个"光控灯"结构一模一样。区别只有两个:

  1. 第 11 课是搭在一个乱糟糟的板子上,这一课要装进一个纸皮灯罩里
  2. 第 11 课那个灯会闪,这一课我们要把闪的问题真正解决掉

三、材料清单

套件里拿这些:

元件 数量 干什么用
ESP32-C3 主控 + 拓展板 1 大脑
光敏传感器 1 感知明暗
LED 灯模块(接 GPIO3 那个) 1 发光
母对母杜邦线 6 根 接线(多拿两根备用)

身边自己准备:

东西 干什么用 替代品
快递纸皮 做灯罩和底板 鞋盒、旧挂历纸(厚一点)
剪刀 / 美工刀 裁纸皮
热熔胶枪 + 胶棒 双面胶也能凑合,但不结实
尺子 + 铅笔 量尺寸、画折线 一把直尺就行
牙签 或 圆珠笔芯 戳透光的小孔 剪子尖也行

裁纸皮的时候,桌子底下垫一块废纸板。 剪刀和刀尖划穿纸皮、划到桌面上,是一瞬间的事。你妈不会喜欢那一道印子的。

四、接线

接之前先断电(USB 线拔了再插线,这个习惯从第 6 课讲到现在了)。

光敏传感器 接到
VCC 3.3V
GND GND
AO GPIO0(必须是模拟脚 GPIO0~GPIO5
LED 灯模块 接到
VCC 3.3V
GND GND
Signal GPIO3

两件事我要重复一遍,因为它们是这节课的地基:

第一,光敏接的是 AO,不是 DO 第 11 课讲过:AO 给你"有多亮",DO 只给你"比某个亮度亮还是暗"。

第二,光敏的信号线必须落在 GPIO0~GPIO5 上。 只有这六个脚能读模拟量(第 10 课那张表)。GPIO3 上挂着 LED,所以光敏走 GPIO0两个模块互不打架。

五、分步实现

第 1 步:先看见光

跟第 11 课一模一样,先跑通最丑的版本。

C++
void setup() {
  Serial.begin(115200);
}

void loop() {
  int light = analogRead(0);      // 读 GPIO0
  Serial.println(light);
  delay(200);
}

烧进去,打开串口监视器,用手把光敏顶上那个小圆片遮住、再拿开。

数字跟着变,对吧?

现在做这节课最重要的一件小事:拿纸笔记三个数。

  1. 手完全遮住的时候,读到多少?
  2. 你打算放小夜灯的那个地方(床头、走廊),开着灯的时候读到多少?
  3. 关了灯,那个地方读到多少?

别嫌麻烦,这三个数是你这个作品的"地基"。 第 11 课讲标定的时候就说了:读数是环境相关的,抄不来。 你现在可是在给自己家做东西——更得用自己家的数。

第 2 步:加一条线,让它自己亮

C++
void setup() {
  Serial.begin(115200);
  pinMode(3, OUTPUT);            // LED 模块
}

void loop() {
  int light = analogRead(0);

  if (light < 1800) {            // 比 1800 小 = 天黑了
    digitalWrite(3, HIGH);       // 开灯
  } else {
    digitalWrite(3, LOW);        // 关灯
  }

  delay(100);
}

烧进去。手一遮——灯亮了。手一拿开——灯灭了。

这就是一个完整的光控开关了。 判断标准特别朴素:你开着灯的时候它别亮,你关灯之后它能亮。

那个 1800 你别抄,它是我编的。 换成你第 1 步记下来的数——比如你测出来"开着灯 2600、关灯 400",那中间取个 1500 左右就挺合适。

第 3 步:现在,问题来了——它会抽风

这一步是这节课的核心,你八成已经撞上了。

找一个微妙的时候:把台灯调暗一点,或者把光敏半遮着,让读数刚好停在你的阈值旁边。

你会看到——灯开始疯狂地闪。 不是"闪一下",是一秒闪好几次,像坏了

为什么? 因为读数不是一块石头,它一直在抖

第几次读 读数(阈值定在 1800) 程序判断
第 1 次 1795 暗 → 开灯
第 2 次 1803 亮 → 关灯
第 3 次 1798 暗 → 开灯
第 4 次 1805 亮 → 关灯

每一次都跨过了那条线,所以每一次都改变主意。 板子一秒钟能循环十次,灯就闪十次。

这不是模块坏了,也不是代码写错了。 这是"一条线"这种做法的天生缺陷

第 11 课我在结尾埋过一句话——"读数在临界点附近会来回跳,解决办法叫迟滞"。当时只是提了一嘴,今天我们得把它用上:这次做的是要拿回家的东西,不能让它半夜抽风。

第 4 步:迟滞——两条线,中间留一段

思路特别简单:开灯和关灯,别用同一条线。

迟滞:开灯和关灯用两条不同的阈值,中间留一段缓冲带,读数在带子里晃动时灯的状态不变

  • 要掉到 1500 以下,才开灯(真的很暗了才开)
  • 要涨到 2500 以上,才关灯(真的亮了才关)
  • 1500 到 2500 之间这一大段——什么都不做,保持原样

那段"什么都不做"的区间,就是缓冲带。读数在里面怎么抖,灯都不理它。

C++
int lampOn = 0;      // 记住灯现在是开(1)还是关(0)

void setup() {
  Serial.begin(115200);
  pinMode(3, OUTPUT);
}

void loop() {
  int light = analogRead(0);

  if (lampOn == 0 && light < 1500) {
    lampOn = 1;                    // 真的很暗了,开灯
  } else if (lampOn == 1 && light > 2500) {
    lampOn = 0;                    // 真的亮了,关灯
  }

  if (lampOn == 1) {
    digitalWrite(3, HIGH);
  } else {
    digitalWrite(3, LOW);
  }

  delay(50);
}

注意 lampOn 这个变量——它是整段代码的关键。

板子除了"现在多亮",还得多记一件事:"我现在的灯是开是关"。因为"该不该改变"这件事,取决于它现在的状态

  • 关着的时候,只有"更暗"能让它动(掉到 1500 以下)
  • 开着的时候,只有"更亮"能让它动(涨到 2500 以上)

这个"记住上一次的状态",是很多控制逻辑的通用套路。 冰箱、空调、热水器,里面都是这么写的——不然它们也会在临界点上抽风。

现在再试一次那个微妙的光线。灯稳稳的,不闪了。

第 5 步:把两个数调成你自己的

1500 和 2500 也是我编的。你自己测一遍:

  1. 拿串口看着读数,慢慢把台灯调暗,暗到你希望灯亮的那一刻——记下这个数,再往下减一点(留余量),这就是你的开灯阈值
  2. 慢慢调亮,亮到你希望灯灭的那一刻——记下这个数,再往上加一点,这就是关灯阈值
  3. 两个数别挨太近,中间至少留出 800~1000 的间隔,否则迟滞就白做了。

看到没有,这就是第 11 课那个"标定",只是这回要标两个数。 "先测,再定"这四个字,这次决定的是:你家的小夜灯好不好用。

六、组装:纸皮灯罩

纸皮灯罩的做法:裁一条 24 厘米乘 8 厘米的纸皮,按虚线折四折围成方筒,戳几个小孔透光,罩在 LED 模块上

第 1 步,裁底板。 裁一块 10cm × 8cm 的纸皮,这是小夜灯的"地板"。

第 2 步,裁罩子。 裁一条 24cm × 8cm 的长条。用尺子量出四段 6cm,铅笔轻轻地画三条线。

画线别使劲——纸皮一压就有痕,画重了折出来的棱是歪的。

第 3 步,折。 把尺子压在线上,纸皮往上掰。三条线都折一遍。

折之前先用尺子边压一道印,折出来才是直的。光靠手捏,折出来是弧形的,围起来合不拢。

第 4 步,围成筒。 把它折成一个方筒,最后那 1cm 的边涂一点热熔胶,粘住。

第 5 步,戳孔。 用牙签或者剪子尖,在筒身上戳 8~12 个小孔,随便戳,像星座一样。

孔别戳太大,也别戳太多。 戳成筛子就变成"漏光"了,不是"星星"。

第 6 步,固定 LED 模块。 在底板正中间挤一小坨热熔胶,把 LED 模块按上去。

胶点在模块的边角上,别糊到灯珠上——灯珠被胶糊住,光就出不来。也别把杜邦线裹进胶里,第 4 课说过,热熔胶会把线皮烫软,一扯就断。

第 7 步,扣罩子。 把方筒扣在底板上,罩住 LED 模块。不要粘死! 以后你还要拆开改代码、换阈值。

第 8 步,走线。 杜邦线从筒底和底板之间的缝里穿出去,插到板子上。

做完之后,把它放到你床头,关灯试试。

那一刻你会有点不真实感——这三块纸皮、几个孔,是你自己做的,而它现在真的在响应这个房间的光。

七、排查:如果它一动不动

接下来这一段,是这节课最值钱的部分。

你按上面的步骤做完了,线接好了,代码也烧了,结果灯要么一直亮着,要么一直不亮——手遮上去也没反应。

你开始改代码里那个阈值:改成 500,还是这样;改成 4000,还是这样。像死了一样。

(还有一种表现:灯在疯狂地闪,一秒好几下。 症状不一样,根因是同一个,排查路径也完全一样。)

别急。这种情况我有一个固定的排查顺序,你照着走。

第一步:先看串口——它的"读数"长什么样

这是第 7 课那句话:"先打印,再判断。" 把串口监视器打开,用手遮住光敏、再拿开。

注意看那些数字。

如果你看到的是这样:

纯文本
4095
4095
0
4095
0
0
4095

停一下。这就是线索。

数字只在 04095 这两个极端之间跳,中间那几千个数,一个都没有出现过

真实的光线不可能这么"干脆"。 你用手遮住小圆片,光是一点点变暗的,中间应该出现 1200、800、600、300 这种过渡值。

一个只在两个极端之间跳的信号,说明它不是"模拟量",是"数字量"。 这是你现在掌握的最重要的一条判断依据。

第二步:用万用表量那根信号线

万用表拨直流电压档V⎓)。先短接两支表笔确认表是好的(第 5 课那个动作)。

  • 黑表笔搭拓展板的 GND
  • 红表笔搭拓展板上 GPIO0 那个针(板子这一头,不是模块那一头)
  • 遮住光敏、拿开、遮住、拿开

你量到什么?

  • 如果是在 0V 和 3.3V 之间直接跳,中间没有任何停留——那这就是数字信号,问题确认了。
  • 正常的模拟量应该长这样:遮住 0.5V,半遮 1.4V,拿开 2.3V。是连续变化的。

第三步:顺着这根线,看它插在模块的哪个针上

红表笔不动(还在板子那头),眼睛顺着线走。

插到光敏模块那一头,看它套在哪个针上。模块上有四个针:AODOGNDVCC

你会看到——它插的是 DO

找到了。

第四步:换到 AO,再量一次

拔出来,插到 AO 上(先断电再接)。再重复第二步:遮住 0.5V、半遮 1.4V、拿开 2.3V

中间值出现了。 这才是"光有多亮"。

烧进去,再遮一次——灯听话了。

根因

你插的是 DO

DO 是模块内部已经替你判断过一次的输出。它把模拟的"有多亮",在板上那个蓝色小电位器设定的门槛上,砍成了 0 和 1 两种答案。你拿到的不是"有多亮",是"比某个亮度亮还是暗"。 而那个"某个亮度"是拧出来的,你根本不知道它在哪。

所以你改阈值,怎么改都没用:

  • 定在 1800 → 读数 0 时判"暗",4095 时判"亮" → 灯跟着数字闪
  • 定在 400、2000、3000 → 一样。0 和 4095 中间隔着整整四千,无论你把线画在哪儿,它俩都各在一边。

一句话:诊断方向从一开始就错了。 你以为是"判断的问题",其实是"感知的问题"——你手上根本没有"有多亮",只有"亮不亮"。

这个坑,是我在这个课题里故意留的。

为什么留它?因为它是你这辈子最容易犯的一个错,而且它长得完全不像错——线插着、灯亮着、程序在跑、串口在打印。什么都对,就是不对。

第 11 课我提醒过"注意是 AO,不是 DO"。但提醒和真踩一次,是两回事。

这套排查顺序,你以后每次都能用:

先看串口 → 看信号的"形状"对不对 → 用万用表把一根线拆成两段量 → 找到问题在哪一段。

注意最后一步的价值:一根线有两头,你分别量两头,就能判断是线的问题还是插的问题。这个"分段量"的办法,后面每个课题都会用到。

自测

单选题小夜灯在某个光线亮度下疯狂闪烁,最根本的原因是什么?
单选题"迟滞"是怎么解决抖动问题的?
单选题串口打出来的读数只有 0 和 4095 两种,中间一个数都没有。最可能是哪里出了问题?
问答题做完发现灯一直亮着,怎么改代码里的阈值都没用。说说你的排查顺序,以及每一步想确认什么。
拓展楼道里的声光控灯,白天有人经过不亮,晚上有声音才亮。它怎么把"光"和"声"两件事合起来的?

这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。

提示:搜"声光控开关 原理 光敏 驻极体话筒"。想一想:如果先判断"现在很暗",再判断"有声音",代码大概长什么样?为什么必须两个条件都满足,不能只看声音?

拓展我这个灯是"啪"一下全亮、全灭。能不能做成慢慢变亮、慢慢变暗,像日出那样?

这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。

提示:搜"PWM 呼吸灯 ESP32 LEDC analogWrite"。想一想:digitalWrite 只有 HIGHLOW 两种,那"半亮"是怎么弄出来的?(第 29 课调光台灯就干这个,你可以先自己试试。)

本节你要动手做的事

  • [ ] 接好线:光敏 AOGPIO0,LED SignalGPIO3先断电再接
  • [ ] 记下三个数:手遮住的、开着灯的、关着灯的——这是你自己家的标定依据
  • [ ] 跑通光控:遮住就亮、拿开就灭,阈值用你自己测的数
  • [ ] 亲眼见一次抽风:故意把阈值调到读数正中间,看它疯狂闪——见过一次,以后就认得出
  • [ ] 加上迟滞:两条线、一个 lampOn 变量,再试同样的光线,确认它稳住了
  • [ ] 做出灯罩:裁 10×8 底板 + 24×8 罩子,折、围、戳孔、粘
  • [ ] 把它拿回家:放在床头或者走廊,关灯看它自己亮起来
  • [ ] 做完自测题

这节课你做出来的东西,是这门课的第一个"成品"。

前面二十五节课,你做的所有东西都是"实验"。实验做完了就拆,这是正常的。但从今天开始不一样了——这个东西会一直亮在你家某个角落里。

而且它会遇到我给你演示不了的情况:你家的光线跟我这儿不一样,你家的纸皮跟我的不一样,你半夜起来看到的亮度也不一样。所以你必须自己标定、自己调阈值、自己决定孔戳在哪儿。这些事没人能替你做。你做了,它就是你的。

还有个细节你可能没意识到:刚才那套"先看串口 → 量电压 → 顺着线找"的排查方法,你是在一个真实的故障上用出来的。 很多成年人调硬件的方式是"换个模块试试""重装一遍软件"——那是碰运气,不是排查。你不是。

下一课我们做红外感应灯——手一凑近,灯就亮。

不过我得先给你打个预防针:它做不到"人走过来就自动亮"。 为什么做不到、它到底能做到什么,下一节课第一句话我就讲。