实战课题
第 26 课:光控小夜灯
第一个能拿回家的东西——天黑了它自己亮,天亮了它自己灭
我是吴老师。从这节课开始,你做的东西不一样了。
前面二十五节课,你手里过了一堆模块:点过灯、读过按钮、量过温度、让蜂鸣器响过。但那些东西最后都拆了,收回盒子里了。
这一节做的东西,是要拿回家的。
一、先说它长什么样、能干嘛
半夜两点,你妈起来倒水。房间里黑着,她不敢开大灯——一开,你和你爸全醒了。于是她摸着黑走:先摸床头柜,摸到你的水杯;再摸墙角,用脚尖探路。走到客厅,脚趾头撞在茶几角上。
我妈干这事干了十几年。
所以第一个实战课题,我们做一个放在床头或走廊的小夜灯:
- 房间亮着的时候,它不亮。 白天、开着灯的晚上,它一声不吭。
- 天一黑,它自己亮。 光不强,但够你看清拖鞋在哪。
- 天亮了,它自己灭。 没有任何开关,你妈什么都不用按。
这东西淘宝上十几块钱一个。但你做的这个不一样:它长什么样、亮到什么程度、多黑才亮,全是你自己定的。而且你知道它内部在干什么——这句话的分量,比"省了十几块钱"重得多。
二、这个课题的三个环节
第 9 课那张图,拿出来温习一遍:
| 环节 | 这个课题里是什么 | 具体到引脚 |
|---|---|---|
| 传感器(看) | 光敏传感器,一直在读"现在有多亮" | 读 GPIO0(模拟量) |
| 判断(想) | 板子比一下:这个数算"暗"吗? | if 加一个阈值 |
| 执行器(做) | LED 模块,亮或者灭 | 写 GPIO3 |
看着眼熟吗? 这跟第 11 课那个"光控灯"结构一模一样。区别只有两个:
- 第 11 课是搭在一个乱糟糟的板子上,这一课要装进一个纸皮灯罩里。
- 第 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 课一模一样,先跑通最丑的版本。
void setup() {
Serial.begin(115200);
}
void loop() {
int light = analogRead(0); // 读 GPIO0
Serial.println(light);
delay(200);
}烧进去,打开串口监视器,用手把光敏顶上那个小圆片遮住、再拿开。
数字跟着变,对吧?
现在做这节课最重要的一件小事:拿纸笔记三个数。
- 手完全遮住的时候,读到多少?
- 你打算放小夜灯的那个地方(床头、走廊),开着灯的时候读到多少?
- 关了灯,那个地方读到多少?
别嫌麻烦,这三个数是你这个作品的"地基"。 第 11 课讲标定的时候就说了:读数是环境相关的,抄不来。 你现在可是在给自己家做东西——更得用自己家的数。
第 2 步:加一条线,让它自己亮
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 之间这一大段——什么都不做,保持原样
那段"什么都不做"的区间,就是缓冲带。读数在里面怎么抖,灯都不理它。
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 也是我编的。你自己测一遍:
- 拿串口看着读数,慢慢把台灯调暗,暗到你希望灯亮的那一刻——记下这个数,再往下减一点(留余量),这就是你的开灯阈值。
- 再慢慢调亮,亮到你希望灯灭的那一刻——记下这个数,再往上加一点,这就是关灯阈值。
- 两个数别挨太近,中间至少留出 800~1000 的间隔,否则迟滞就白做了。
看到没有,这就是第 11 课那个"标定",只是这回要标两个数。 "先测,再定"这四个字,这次决定的是:你家的小夜灯好不好用。
六、组装:纸皮灯罩
第 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停一下。这就是线索。
数字只在 0 和 4095 这两个极端之间跳,中间那几千个数,一个都没有出现过。
真实的光线不可能这么"干脆"。 你用手遮住小圆片,光是一点点变暗的,中间应该出现 1200、800、600、300 这种过渡值。
一个只在两个极端之间跳的信号,说明它不是"模拟量",是"数字量"。 这是你现在掌握的最重要的一条判断依据。
第二步:用万用表量那根信号线
万用表拨直流电压档(V⎓)。先短接两支表笔确认表是好的(第 5 课那个动作)。
- 黑表笔搭拓展板的
GND - 红表笔搭拓展板上
GPIO0那个针(板子这一头,不是模块那一头) - 遮住光敏、拿开、遮住、拿开
你量到什么?
- 如果是在 0V 和 3.3V 之间直接跳,中间没有任何停留——那这就是数字信号,问题确认了。
- 正常的模拟量应该长这样:遮住 0.5V,半遮 1.4V,拿开 2.3V。是连续变化的。
第三步:顺着这根线,看它插在模块的哪个针上
红表笔不动(还在板子那头),眼睛顺着线走。
插到光敏模块那一头,看它套在哪个针上。模块上有四个针:AO、DO、GND、VCC。
你会看到——它插的是 DO。
找到了。
第四步:换到 AO,再量一次
拔出来,插到 AO 上(先断电再接)。再重复第二步:遮住 0.5V、半遮 1.4V、拿开 2.3V。
中间值出现了。 这才是"光有多亮"。
烧进去,再遮一次——灯听话了。
根因
你插的是 DO。
DO 是模块内部已经替你判断过一次的输出。它把模拟的"有多亮",在板上那个蓝色小电位器设定的门槛上,砍成了 0 和 1 两种答案。你拿到的不是"有多亮",是"比某个亮度亮还是暗"。 而那个"某个亮度"是拧出来的,你根本不知道它在哪。
所以你改阈值,怎么改都没用:
- 定在 1800 → 读数 0 时判"暗",4095 时判"亮" → 灯跟着数字闪
- 定在 400、2000、3000 → 一样。0 和 4095 中间隔着整整四千,无论你把线画在哪儿,它俩都各在一边。
一句话:诊断方向从一开始就错了。 你以为是"判断的问题",其实是"感知的问题"——你手上根本没有"有多亮",只有"亮不亮"。
这个坑,是我在这个课题里故意留的。
为什么留它?因为它是你这辈子最容易犯的一个错,而且它长得完全不像错——线插着、灯亮着、程序在跑、串口在打印。什么都对,就是不对。
第 11 课我提醒过"注意是
AO,不是DO"。但提醒和真踩一次,是两回事。
这套排查顺序,你以后每次都能用:
先看串口 → 看信号的"形状"对不对 → 用万用表把一根线拆成两段量 → 找到问题在哪一段。
注意最后一步的价值:一根线有两头,你分别量两头,就能判断是线的问题还是插的问题。这个"分段量"的办法,后面每个课题都会用到。
自测
答案与解析
先排除"模块坏了"——你能看到它跟着光线变化,就说明它是好的。真正的问题是一条线做判断这种做法的天生缺陷。加 delay 只会让它闪得慢一点,不会让它不闪——变成"一秒闪一次",一样是坏的。正解是迟滞。
答案与解析
关键在**"记住灯现在的状态"**(代码里那个 lampOn):灯关着时只有"更暗"能让它动,灯开着时只有"更亮"能让它动,中间那段缓冲带里怎么抖都不理。干扰项里"取平均"是个真实存在的办法(叫滤波),但它只是让抖动变小,治不了根。
答案与解析
0 和 4095 是"数字信号"的典型长相——只有高低两档,中间是空的。真实的模拟量(AO)遮住和拿开之间一定有过渡值。注意 GPIO0 是支持模拟输入的(GPIO0~GPIO5 那六个),这个干扰项是拿"引脚知识"来迷惑你的。判断依据是信号的形状,不是引脚号。
答题要点
答题要点应该包含:① 先看串口,确认读数是什么"形状"——连续变化,还是只有 0 和 4095 两个极端(第 7 课"先打印,再判断");② 用万用表直流电压档量信号线,黑笔 GND、红笔搭板子这头的 GPIO0,遮住和拿开看电压是连续变化还是在 0V/3.3V 之间直接跳;③ 顺着线看它插在模块的哪个针上,是不是 AO;④ 换到 AO 之后再量一遍,出现中间值就说明对了。核心思路:一根线有两头,分别量两头,就能判断是线的问题还是插的问题——这叫"分段量"。先看电,再看线,再看信号,最后才改代码。
拓展楼道里的声光控灯,白天有人经过不亮,晚上有声音才亮。它怎么把"光"和"声"两件事合起来的?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"声光控开关 原理 光敏 驻极体话筒"。想一想:如果先判断"现在很暗",再判断"有声音",代码大概长什么样?为什么必须两个条件都满足,不能只看声音?
拓展我这个灯是"啪"一下全亮、全灭。能不能做成慢慢变亮、慢慢变暗,像日出那样?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"PWM 呼吸灯 ESP32 LEDC analogWrite"。想一想:digitalWrite 只有 HIGH 和 LOW 两种,那"半亮"是怎么弄出来的?(第 29 课调光台灯就干这个,你可以先自己试试。)
本节你要动手做的事
- [ ] 接好线:光敏
AO→GPIO0,LEDSignal→GPIO3,先断电再接 - [ ] 记下三个数:手遮住的、开着灯的、关着灯的——这是你自己家的标定依据
- [ ] 跑通光控:遮住就亮、拿开就灭,阈值用你自己测的数
- [ ] 亲眼见一次抽风:故意把阈值调到读数正中间,看它疯狂闪——见过一次,以后就认得出
- [ ] 加上迟滞:两条线、一个
lampOn变量,再试同样的光线,确认它稳住了 - [ ] 做出灯罩:裁 10×8 底板 + 24×8 罩子,折、围、戳孔、粘
- [ ] 把它拿回家:放在床头或者走廊,关灯看它自己亮起来
- [ ] 做完自测题
这节课你做出来的东西,是这门课的第一个"成品"。
前面二十五节课,你做的所有东西都是"实验"。实验做完了就拆,这是正常的。但从今天开始不一样了——这个东西会一直亮在你家某个角落里。
而且它会遇到我给你演示不了的情况:你家的光线跟我这儿不一样,你家的纸皮跟我的不一样,你半夜起来看到的亮度也不一样。所以你必须自己标定、自己调阈值、自己决定孔戳在哪儿。这些事没人能替你做。你做了,它就是你的。
还有个细节你可能没意识到:刚才那套"先看串口 → 量电压 → 顺着线找"的排查方法,你是在一个真实的故障上用出来的。 很多成年人调硬件的方式是"换个模块试试""重装一遍软件"——那是碰运气,不是排查。你不是。
下一课我们做红外感应灯——手一凑近,灯就亮。
不过我得先给你打个预防针:它做不到"人走过来就自动亮"。 为什么做不到、它到底能做到什么,下一节课第一句话我就讲。