实战课题
第 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 课学舵机时你已经听过了:
水龙头只有"全开"和"全关"两个挡。现在你要半桶水——那就开一秒、关一秒、开一秒、关一秒。 平均下来正好是一半。
电也一样:让引脚飞快地在 3.3V 和 0V 之间切,切得够快,灯感受到的就是一个"中间亮度"。 这招叫 PWM,那个"开的时间占多少"叫占空比。
灯跟舵机的区别在于:舵机对频率有意见(它要求每秒 50 次),灯完全无所谓。你把频率设到几千赫兹,灯只会乖乖按占空比亮。
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 课,一分钟跑通:
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 对准下面的 0,上面最大的那个数对准下面的 255,中间按同样的比例分。上面走到一半(2048),下面也走到一半(约 127);上面走到四分之一(1024),下面也走到四分之一(64)。
这件事叫映射。 代码里有个现成的函数干这个活:
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。这就是"先测,再定"用在了映射上。
第四步:把最暗那端垫高一点
跑通之后你多半会碰到一个别扭的地方:拧到最左边,灯彻底灭了。
这在电路上完全正确,但在作品上是个问题——用户(也就是你妈)会以为灯坏了。
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 都断电,线还插着,等一下要试。
灯罩本体
- 量尺寸。 LED 模块大概 3cm × 1.5cm。用铅笔和尺子在纸皮上画一个"展开图":底面 8×8cm,四个侧面各 8×9cm,顶面留一块 8×8cm。
- 剪。 美工刀贴着尺子切直线——底下垫一张废纸皮,别切到桌子。四个角多留 5mm 的"粘接耳",折过来就能粘。
- 折。 别硬折,纸皮会裂。用尺子边缘压着折痕先压一道印,再顺着印折过去。多花三十秒,成品好看一倍。
- 开孔。 顶面正中间开一个 1.5cm 的圆孔(比 LED 模块略大)。LED 从孔里塞进去,朝下照。 台灯是照桌子的,不是照你眼睛的——让灯直射,你写作业十分钟就烦了。
- 留窗。 四个侧面挑一面,中间掏空只剩 5mm 的边,糊上一张半透明白纸。光主要从这儿出来。
装上电位器,再做一个能用手拧的旋钮
电位器本体是个小方块,两边有小小的凸耳。在灯罩侧面开一个刚好卡住它的方孔,把电位器卡在孔里,两面各点一点热熔胶。
注意:胶是点在纸皮上,不是糊在电位器身上。 里面是能转的零件,你把胶灌进去,它当场就废了。点胶的时候手稳一点,离元件本体留一毫米。
然后是最好玩、也最容易做砸的一步。
电位器上那个小十字槽是给小螺丝刀用的,手指头拧不动。给它装一个手柄:
- 拿一根牙签,用小刀把一头削扁,削到能插进那个十字槽里。
- 插进去试拧一下。能带着电位器一起转,就对了。
- 瓶盖中心先用针扎个眼,把牙签的另一头穿过去。
- 在瓶盖内侧点两滴热熔胶,把牙签和瓶盖粘死。
- 关键:粘的时候让瓶盖离电位器本体留 2 毫米缝隙。 胶一旦流到电位器上,前面全白干。
- 灯罩侧面开个小孔让牙签穿出来,瓶盖扣在外面。一个大旋钮,成了。
有个事必须提醒你:电位器只能转 270 度左右,拧到头就停。
别使劲。 你再用力,它不会转得更多,只会把里面的碳膜刮花——刮花了就是"拧到某一段读数乱跳",修不好,只能换一个。这跟第 18 课舵机那个"别硬掰"是一个道理:元件的行程是有极限的,你得知道它在哪。
合盖之前先试一遍
别粘死了再试。 把线接回去,上电,拧那个瓶盖——灯跟着亮了暗了,再合盖。 合上之后再试一次,确认旋钮没被纸皮卡住。
这一步会救你很多时间。 粘死了才发现旋钮转不动,你只能拆——拆纸皮比装纸皮难十倍。
六、【排查】灯在乱跳,跟旋钮没关系
这一节是给你准备的。做完如果一切正常,也建议读一遍——这些现象你早晚会碰上。
先说症状。 台灯装好了,上电。灯是亮的——说明供电没问题。但它不听你的:你拧旋钮,它的亮度完全不理你,自己在最亮和最暗之间乱跳。打开串口一看,读数也是一团乱麻,拧到哪儿都一样。
这时候你会怎么想? 我猜是这三种之一:"是不是我 map 写错了?""是不是 PWM 那个 8 写错了?""是不是电位器坏了?"
先别改代码。 按第 5 课那个顺序来:电 → 信号 → 代码。
第一步:先问电位器本人
万用表拨到直流电压档。黑表笔搭电位器的 GND,红表笔点在 OUT 针上。 慢慢拧旋钮,看着表。
如果电压从 0V 平滑地走到 3.3V(反过来也行)——那电位器是好的。 它在好好干活,问题不在它。这一步很重要,因为它排除了一大片:传感器没坏。
第二步:那这个电压送到板子了吗
传感器的电压有了,可板子读到的是一团乱麻。中间隔着什么?一根杜邦线。
万用表拨到通断档(蜂鸣档)。黑表笔搭电位器的 OUT 针,红表笔搭拓展板的 GPIO0 排针,听声音。
不响。 换一根线,再量一次——响了,读数也正常了。
第三步:那根坏线是怎么回事
把它单独拿出来,两个表笔分别搭两头,量通断——不响。 可你盯着它看:外皮完好、没有折痕、插头也没松。跟旁边那根好线长得一模一样。
这就是答案:它断了。断在你完全看不见的地方。
杜邦线两头是压接的——铜丝被金属片硬挤在一起,不是焊的。你插拔的次数多了、折的角度大了,里面那几根细铜丝就断了。外皮是塑料的,有弹性,断在里面照样包得好好的。所以从外表上看,它是一根好线。
根因
这是一根内部断了的杜邦线。 它没把电位器的电压送到板子的 GPIO0 上,于是那个引脚悬空了——悬空的模拟脚读到什么,完全看当时的环境干扰,读数乱跳是它最典型的表现。
这是我在这个课题里故意留的坑,也是真实世界里最常见的一种故障。
我让你碰上它,是因为它专治"一看到不对就改代码"。你今天要是没量那一下,你会把
map改十遍、把 PWM 那个8换成10、甚至怀疑板子烧了——而代码和板子一个字都没错。顺便记住一件事:手上永远备几根新线。 这是硬件人的第一常识,比什么技巧都实在。
自测
ledcAttach(3, 5000, 8) 里最后那个 8,决定了什么?答案与解析
5000 是频率(每秒开关 5000 次),8 是分辨率(用几位数来数档位)。8 位 = 2⁸ = 256 档,所以后面 ledcWrite 里能填的最大值是 255。这就是为什么 PWM 只认 0~255——不是谁规定的,是"用 8 位来数"算出来的。位数越高档位越细,但对一盏台灯来说 256 档已经细得看不出来了。
答案与解析
映射的关键是"按比例",不是"除以某个数"。 2048 在 0~4095 里正好是一半,所以映射到 0~255 之后也应该是一半,255 ÷ 2 ≈ 127。选 1024 的同学是把"除以 2"当成了映射——那是碰巧,换个读数就不对了。记住那张图:两头对齐,中间等比。
ledcWrite(3, raw)(raw 是 0~4095 的读数)?答案与解析
255 已经是最亮的天花板了,上面没有更高的档。你给一个 3000 进去,板子不会理解成"三倍亮"——超出的部分会被截掉,你得到的可能反而是个中等亮度。"超出范围"在嵌入式中从来不是"更多",而是"不可预料"。 这也正是需要映射的原因:两个世界用的刻度不一样,得先换算再送过去。
答题要点
答题要点应该包含:① 传感器是电位器,读回一个 0~4095 的模拟值,代表"你拧到哪儿了";② 判断是映射——用 map(raw, 0, 4095, 0, 255) 把读数按比例缩到 PWM 能接受的 0~255,这一步是板子在"翻译";③ 执行器是 LED 模块,用 PWM 输出对应的占空比,把"数值"变成"亮度"。三个环节缺一不可:只读旋钮不写 PWM(白算),只写固定亮度不读旋钮(演示)。这个骨架跟第 9 课的按钮点灯是同一个,只是把"按没按"换成了"拧到哪儿",把"亮/灭"换成了"多亮"。
拓展为什么灯调到很暗的时候,用手机拍它会看到一道道横条纹?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"PWM 频闪 手机摄像头 滚动快门"、"LED 调光 频闪 护眼"。想一想:手机摄像头是"一行一行扫"的,如果灯在这中间关了一下,画面里会留下什么?这算不算一个真实存在的缺陷?
拓展家里的台灯也能调光,它跟你的台灯是同一个原理吗?为什么有些灯调暗了会嗡嗡响?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"可控硅调光 原理 嗡嗡声"、"PWM 调光 模拟调光 区别"。想一想:如果不用"快速开关",还有什么办法能让一个灯变暗?那种办法的代价是什么?
本节你要动手做的事
- [ ] 接好两个模块:电位器
OUT→GPIO0(模拟脚),LEDSignal→GPIO3。断电再插 - [ ] 让灯半亮:跑通
ledcAttach+ledcWrite(3, 128),再试试 0 / 64 / 255 - [ ] 标定电位器:记下你拧到两端时的实际读数(不一定是 0 和 4095)
- [ ] 做映射:拧旋钮看灯跟着变,串口里两个数字同步走
- [ ] 把最暗垫高:改成
map(raw, 0, 4095, 30, 255),看看手感好在哪 - [ ] 图形化路线的同学:去积木区搜"亮度 / PWM / 模拟输出",有就用,没有就做 5 档版本
- [ ] 做纸皮灯罩:画展开图 → 剪 → 压折痕 → 折 → 开孔 → 留透光窗(合盖前先试一次)
- [ ] 装旋钮:牙签削扁插进十字槽 → 穿瓶盖 → 内侧点胶 → 离电位器留 2mm
- [ ] 想一遍:如果你的灯"乱跳、不理旋钮",你的第一步是什么?(答案是拿万用表量
OUT电压,不是改代码)
这个课题你做出来的东西不大,但有一件事变了。
前面二十几节课,你要么在"读",要么在"写",从来没做过"读进来、变个样子、再送出去"这种活。 今天你做了,而且用的是"按比例"这个办法。这个思路你在生活里天天见——手机屏幕亮度、音量条、进度条,背后都是"把一段范围等比搬到另一段范围"。
还有那个坑。 你没改一行代码,靠一把万用表找到了那根断在皮里面的线。这个判断力,比会写 map 值钱得多。
但我要提前给你打个预防针: 下一节课,你要同时接三个模块——超声波、蜂鸣器、OLED。而且那个课题里会出现一个你从来没见过的东西:时间。
不是"现在近不近",而是"近了多久了"。 板子要学会看表。那是个什么感觉?下节课见。