实战课题

第 29 课:调光台灯

第一次做区间映射——把 0~4095 的旋钮变成 0~255 的亮度,再装进一个纸皮灯罩

我是吴老师。这是实战单元的第四个课题,也是你第一次做"能调"的东西。

前面三个——26 光控小夜灯、27 红外感应灯、28 倒车雷达——做的都是**"自动"**的:天黑了它自己亮、有人来了它自己叫。板子替你做了决定。

这个课题反过来:决定权在你手上。

你要做一个台灯。桌上那种,能拧的。拧到最暗,它是一盏昏黄的小夜灯;拧到最亮,它能照着你写作业。 而且那个旋钮是真的能用手拧——不是拿小螺丝刀去戳一个小槽,是一个装在灯座上、手一摸就能转的大旋钮。

做完以后它会站在你桌上。你妈路过可能会问一句"这哪儿买的"——这句话我提前给你备好了。

一、【闭环】这个课题的三个环节

第 9 课那个骨架,再走一遍:

环节 这个课题里是什么
传感器(看) 电位器——你把它拧到哪儿了
判断(想) 把 0~4095 换算成 0~255
执行器(做) LED 模块——有多亮

只读旋钮不点灯,那叫"白算";灯一直亮着不理会旋钮,那叫"演示"。

中间那个"判断",就是这个课题全部的难点。 它有个正式名字叫映射(map)。这个词你今天第一次听,以后会一直用。

二、材料清单

套件里:ESP32-C3 主控 + 拓展板、电位器模块、LED 灯模块、杜邦线若干(多备两根,后面告诉你为什么)。

身边要准备:

东西 上哪儿找
纸皮 快递盒、牛奶箱。别挑太厚的瓦楞纸,薄一点好折
热熔胶枪 + 胶棒 文具店十几块钱一套
剪刀 + 美工刀 + 铅笔 + 尺子 美工刀贴着尺子切直线,剪刀修圆角。别徒手剪
牙签 + 一个塑料瓶盖 做旋钮的轴和手柄,矿泉水瓶盖就行
描图纸 / 半透明白纸 可选,做灯罩的透光面

三、接线

模块 针脚 接到
电位器 VCC 3.3V
电位器 GND GND
电位器 OUT GPIO0
LED 模块 VCC 3.3V
LED 模块 GND GND
LED 模块 Signal GPIO3

两个提醒。第一,电位器的 OUT 必须接在模拟脚上GPIO0 ~ GPIO5 这 6 个,第 10 课那张表)。接成别的脚,analogRead 读回来就是一团垃圾。LED 那条线随便接哪个都行——它是输出,不是模拟输入。我用 GPIO3,跟第 6 课保持一致。

第二,插线之前先断电。 第 8 课立的规矩,今天照样算数。

接好了先别写代码。看一眼这两个模块,问自己一句:电位器是"输入",LED 是"输出",中间那一步由谁来做?

答案是板子。它在中间当翻译。这个课题从头到尾就是"怎么翻得准"这一件事。

四、分步实现

第一步:先让灯"半亮"

这是你头一回让一个灯不只有"亮"和"灭"两种状态。

回想一下第 6 课点灯:digitalWrite(3, HIGH)——要么 3.3V,要么 0V,没有中间。 那"半亮"怎么办?板子吐不出 1.65V 这个电压。

这个问题的答案,第 18 课学舵机时你已经听过了:

PWM 用快速开关模拟中间值,占空比越大等效电压越高

水龙头只有"全开"和"全关"两个挡。现在你要半桶水——那就开一秒、关一秒、开一秒、关一秒。 平均下来正好是一半。

电也一样:让引脚飞快地在 3.3V 和 0V 之间切,切得够快,灯感受到的就是一个"中间亮度"。 这招叫 PWM,那个"开的时间占多少"叫占空比

灯跟舵机的区别在于:舵机对频率有意见(它要求每秒 50 次),灯完全无所谓。你把频率设到几千赫兹,灯只会乖乖按占空比亮。

C++
void setup() {
  ledcAttach(3, 5000, 8);   // 给 GPIO3 开一路 PWM:5000 Hz,8 位
}

void loop() {
  ledcWrite(3, 128);        // 128 / 255 ≈ 一半
}

那三个数字分别是3 是引脚;5000 是每秒开关 5000 次,快到你眼睛完全看不出来8 是用 8 位来数档位——2 的 8 次方 = 256 档,也就是 0~255

最后那个 8 是今天最要紧的东西,你记住它。 烧进去,灯是不是半亮的?恭喜,你做出了人生第一个"不只有开和关"的灯。

函数名对不上怎么办? 支持包不是最新版的话,ledcAttach 可能报"没这个函数"。老版本写法是先 ledcSetup(0, 5000, 8)、再 ledcAttachPin(3, 0),输出时写 ledcWrite(0, 128)——注意这里填的是通道号 0,不是引脚号 3,这是老版本最容易写错的地方。编译不过别硬扛,把报错原文整段丢给 AI(第 2 课教过)。有的版本还支持更短的 analogWrite(3, 128),能编译过就用它。

自己改个数玩: 0(全灭)、64(微亮)、255(最亮)。

第二步:先读懂旋钮

复习第 10 课,一分钟跑通:

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

void loop() {
  Serial.println(analogRead(0));
  delay(100);
}

烧进去,打开串口监视器,慢慢拧电位器。数字在 0 到 4095 之间平滑地走。

现在做那件老事:记下两个数。 拧到最左是几?拧到最右是几?

有可能不是标准的 0 和 4095——那很正常。电位器拧到头,中间点电压可能停在 0.05V,也可能停在 3.21V,不一定正好贴着两头。记下你实际量到的这两个数,这就是第 11 课那个"标定":先测,再定。

第三步:把两个接起来——映射

现在你手上有两样东西:一个 0 ~ 4095 的读数,一个只认 0 ~ 255 的亮度。直接接得上吗?接不上。

那我把读数除以 16 行不行? 4095 ÷ 16 = 256,好像差不多……但这是碰巧。 万一你那个电位器最大只能拧到 3800 呢?3800 ÷ 16 = 237,你就永远点不到最亮。

正确做法不是"除以某个数",而是"按比例搬"。

映射就是把 0~4095 的刻度按比例搬到 0~255 上,两头对齐、中间等比分配

上面一条尺子是 0~4095,下面一条是 0~255。两头钉死——上面的 0 对准下面的 0,上面最大的那个数对准下面的 255,中间按同样的比例分。上面走到一半(2048),下面也走到一半(约 127);上面走到四分之一(1024),下面也走到四分之一(64)。

这件事叫映射。 代码里有个现成的函数干这个活:

C++
void setup() {
  Serial.begin(115200);
  ledcAttach(3, 5000, 8);
}

void loop() {
  int raw    = analogRead(0);              // 0 ~ 4095
  int bright = map(raw, 0, 4095, 0, 255);  // 按比例缩到 0 ~ 255

  ledcWrite(3, bright);

  Serial.print(raw);
  Serial.print("  ->  ");
  Serial.println(bright);
  delay(20);
}

map(值, 从哪, 到哪, 变到哪, 变到哪)——括号里五个东西,读出来就是一句话:"把这个值,从这个范围,等比换到那个范围。"

烧进去,慢慢拧旋钮。灯跟着你的手亮了、暗了、又亮了。 串口里两个数字同步在走:4000 对 249,2000 对 124,500 对 31。看到没有——左边和右边是一一对应的。

这就是你第一次做区间映射。 你现在可能觉得"不就是个换算吗",但这个动作在硬件里到处都是:温度映射成颜色、声音映射成高度、电量映射成百分比。骨架永远是这一套:两头对齐,中间等比。

map 里为什么不写 4095,写你量出来的数行不行? 行,而且更准。如果你实测拧到头是 3800,那就写 map(raw, 0, 3800, 0, 255),最亮那一端才真的能到 255。这就是"先测,再定"用在了映射上。

第四步:把最暗那端垫高一点

跑通之后你多半会碰到一个别扭的地方:拧到最左边,灯彻底灭了。

这在电路上完全正确,但在作品上是个问题——用户(也就是你妈)会以为灯坏了。

C++
  int bright = map(raw, 0, 4095, 30, 255);   // 最暗也给 30,不让它彻底灭

最暗的时候留一点点余光,看起来才像一盏"能调的灯",而不是"接触不良的灯"。

这就是"做作品"和"做实验"的差别。 实验只要"原理对",作品还要"看着对"。这个念头你从现在起就该有了——后面每个课题都会用到。

第五步:如果你是图形化路线

图形化的积木是平台做好的,它不一定有"亮度"这一类积木。去积木区搜一下这几个词:亮度 / PWM / 模拟输出 / 设置引脚...为...

有——直接用,逻辑跟上面一模一样(可能还有现成的"映射"积木)。

没有——两个替代方案:

方案一:分档位。 别做无级调节,做 5 档。用几个"如果…否则如果"把 0~4095 切成 5 段,每段给一个固定亮度:

纯文本
读数 < 800 → 30    读数 < 1600 → 90    读数 < 2400 → 150
读数 < 3200 → 210   否则 → 255

效果没那么顺滑,但"能调"这件事是真的,而且它逼你想清楚"区间"这个概念,不亏。

方案二:认了。 如果平台连固定亮度都给不了(只有"亮/灭"),那这个课题在图形化路线上做不出"调光",只做得出"台灯"。这不丢人——这就是"平台给你什么,你就能做什么"的边界(第 0 课那张表最后一行)。你可以先用纸皮把灯罩做出来,等哪天转到代码路线,再回来给它装上"调光"。

五、组装:纸皮灯罩

先别急着粘。 把主控、电位器、LED 都断电,线还插着,等一下要试。

灯罩本体

  1. 量尺寸。 LED 模块大概 3cm × 1.5cm。用铅笔和尺子在纸皮上画一个"展开图":底面 8×8cm,四个侧面各 8×9cm,顶面留一块 8×8cm。
  2. 剪。 美工刀贴着尺子切直线——底下垫一张废纸皮,别切到桌子。四个角多留 5mm 的"粘接耳",折过来就能粘。
  3. 折。 别硬折,纸皮会裂。用尺子边缘压着折痕先压一道印,再顺着印折过去。多花三十秒,成品好看一倍。
  4. 开孔。 顶面正中间开一个 1.5cm 的圆孔(比 LED 模块略大)。LED 从孔里塞进去,朝下照。 台灯是照桌子的,不是照你眼睛的——让灯直射,你写作业十分钟就烦了。
  5. 留窗。 四个侧面挑一面,中间掏空只剩 5mm 的边,糊上一张半透明白纸。光主要从这儿出来。

装上电位器,再做一个能用手拧的旋钮

电位器本体是个小方块,两边有小小的凸耳。在灯罩侧面开一个刚好卡住它的方孔,把电位器卡在孔里,两面各点一点热熔胶。

注意:胶是点在纸皮上,不是糊在电位器身上。 里面是能转的零件,你把胶灌进去,它当场就废了。点胶的时候手稳一点,离元件本体留一毫米。

然后是最好玩、也最容易做砸的一步。

电位器上那个小十字槽是给小螺丝刀用的,手指头拧不动。给它装一个手柄:

  1. 拿一根牙签,用小刀把一头削扁,削到能插进那个十字槽里。
  2. 插进去试拧一下。能带着电位器一起转,就对了。
  3. 瓶盖中心先用针扎个眼,把牙签的另一头穿过去。
  4. 在瓶盖内侧点两滴热熔胶,把牙签和瓶盖粘死。
  5. 关键:粘的时候让瓶盖离电位器本体留 2 毫米缝隙。 胶一旦流到电位器上,前面全白干。
  6. 灯罩侧面开个小孔让牙签穿出来,瓶盖扣在外面。一个大旋钮,成了。

有个事必须提醒你:电位器只能转 270 度左右,拧到头就停。

别使劲。 你再用力,它不会转得更多,只会把里面的碳膜刮花——刮花了就是"拧到某一段读数乱跳",修不好,只能换一个。这跟第 18 课舵机那个"别硬掰"是一个道理:元件的行程是有极限的,你得知道它在哪。

合盖之前先试一遍

别粘死了再试。 把线接回去,上电,拧那个瓶盖——灯跟着亮了暗了,再合盖。 合上之后再试一次,确认旋钮没被纸皮卡住。

这一步会救你很多时间。 粘死了才发现旋钮转不动,你只能拆——拆纸皮比装纸皮难十倍。

六、【排查】灯在乱跳,跟旋钮没关系

这一节是给你准备的。做完如果一切正常,也建议读一遍——这些现象你早晚会碰上。

先说症状。 台灯装好了,上电。灯是亮的——说明供电没问题。但它不听你的:你拧旋钮,它的亮度完全不理你,自己在最亮和最暗之间乱跳。打开串口一看,读数也是一团乱麻,拧到哪儿都一样。

这时候你会怎么想? 我猜是这三种之一:"是不是我 map 写错了?""是不是 PWM 那个 8 写错了?""是不是电位器坏了?"

先别改代码。 按第 5 课那个顺序来:电 → 信号 → 代码

第一步:先问电位器本人

万用表拨到直流电压档。黑表笔搭电位器的 GND,红表笔点在 OUT 针上。 慢慢拧旋钮,看着表。

如果电压从 0V 平滑地走到 3.3V(反过来也行)——那电位器是好的。 它在好好干活,问题不在它。这一步很重要,因为它排除了一大片:传感器没坏。

第二步:那这个电压送到板子了吗

传感器的电压有了,可板子读到的是一团乱麻。中间隔着什么?一根杜邦线。

万用表拨到通断档(蜂鸣档)。黑表笔搭电位器的 OUT 针,红表笔搭拓展板的 GPIO0 排针,听声音。

不响。 换一根线,再量一次——响了,读数也正常了。

第三步:那根坏线是怎么回事

把它单独拿出来,两个表笔分别搭两头,量通断——不响。 可你盯着它看:外皮完好、没有折痕、插头也没松。跟旁边那根好线长得一模一样。

这就是答案:它断了。断在你完全看不见的地方。

杜邦线两头是压接的——铜丝被金属片硬挤在一起,不是焊的。你插拔的次数多了、折的角度大了,里面那几根细铜丝就断了。外皮是塑料的,有弹性,断在里面照样包得好好的。所以从外表上看,它是一根好线。

根因

这是一根内部断了的杜邦线。 它没把电位器的电压送到板子的 GPIO0 上,于是那个引脚悬空了——悬空的模拟脚读到什么,完全看当时的环境干扰,读数乱跳是它最典型的表现

这是我在这个课题里故意留的坑,也是真实世界里最常见的一种故障。

我让你碰上它,是因为它专治"一看到不对就改代码"。你今天要是没量那一下,你会把 map 改十遍、把 PWM 那个 8 换成 10、甚至怀疑板子烧了——而代码和板子一个字都没错。

顺便记住一件事:手上永远备几根新线。 这是硬件人的第一常识,比什么技巧都实在。

自测

单选题ledcAttach(3, 5000, 8) 里最后那个 8,决定了什么?
单选题电位器拧到正中间,读数是 2048。映射到 0~255 之后,大概是多少?
单选题为什么不直接写 ledcWrite(3, raw)raw 是 0~4095 的读数)?
问答题这个课题的闭环三个环节分别是什么?中间那个"判断"具体在干什么?
拓展为什么灯调到很暗的时候,用手机拍它会看到一道道横条纹?

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

提示:搜"PWM 频闪 手机摄像头 滚动快门"、"LED 调光 频闪 护眼"。想一想:手机摄像头是"一行一行扫"的,如果灯在这中间关了一下,画面里会留下什么?这算不算一个真实存在的缺陷?

拓展家里的台灯也能调光,它跟你的台灯是同一个原理吗?为什么有些灯调暗了会嗡嗡响?

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

提示:搜"可控硅调光 原理 嗡嗡声"、"PWM 调光 模拟调光 区别"。想一想:如果不用"快速开关",还有什么办法能让一个灯变暗?那种办法的代价是什么?

本节你要动手做的事

  • [ ] 接好两个模块:电位器 OUTGPIO0(模拟脚),LED SignalGPIO3断电再插
  • [ ] 让灯半亮:跑通 ledcAttach + ledcWrite(3, 128),再试试 0 / 64 / 255
  • [ ] 标定电位器:记下你拧到两端时的实际读数(不一定是 0 和 4095)
  • [ ] 做映射:拧旋钮看灯跟着变,串口里两个数字同步走
  • [ ] 把最暗垫高:改成 map(raw, 0, 4095, 30, 255),看看手感好在哪
  • [ ] 图形化路线的同学:去积木区搜"亮度 / PWM / 模拟输出",有就用,没有就做 5 档版本
  • [ ] 做纸皮灯罩:画展开图 → 剪 → 压折痕 → 折 → 开孔 → 留透光窗(合盖前先试一次
  • [ ] 装旋钮:牙签削扁插进十字槽 → 穿瓶盖 → 内侧点胶 → 离电位器留 2mm
  • [ ] 想一遍:如果你的灯"乱跳、不理旋钮",你的第一步是什么?(答案是拿万用表量 OUT 电压,不是改代码)

这个课题你做出来的东西不大,但有一件事变了。

前面二十几节课,你要么在"读",要么在"写",从来没做过"读进来、变个样子、再送出去"这种活。 今天你做了,而且用的是"按比例"这个办法。这个思路你在生活里天天见——手机屏幕亮度、音量条、进度条,背后都是"把一段范围等比搬到另一段范围"。

还有那个坑。 你没改一行代码,靠一把万用表找到了那根断在皮里面的线。这个判断力,比会写 map 值钱得多。

但我要提前给你打个预防针: 下一节课,你要同时接三个模块——超声波、蜂鸣器、OLED。而且那个课题里会出现一个你从来没见过的东西:时间

不是"现在近不近",而是"近了多久了"。 板子要学会看表。那是个什么感觉?下节课见。