如果你手里正好有一块吃灰的 MakerBuddy IoT 套件,我强烈建议你把它翻出来试试这个项目:用一颗 LDR 光敏电阻感知环境亮度,用一只舵机顶着一根指针在表盘上摆动,再把实时读数通过 IoT 模块送到手机屏幕上。这其实就是标题里说的那台 DIY LDR Light Level Gauge——一台自带机械表盘指针的物联网光强仪表。听起来简单,但麻雀虽小、五脏俱全:分压电路、对数关系、标定方法、舵机平滑控制、无线数据上报,每个环节都有可以深挖的东西。这篇文章会把我从测量原理到零件选型、从写代码到画表盘踩过的坑全部过一遍,适合刚做完入门点灯项目、想挑战“一个完整闭环”的人参考,也适合想再捡起基本功的进阶玩家。
1. 为什么要做一台“带指针”的光强表
1.1 数字到处都是,指针的直觉不可替代
做这个项目前,我一度很纠结:手机上一堆照度计 App,一个 TSL2561 数字光线传感器模块也就十几块钱,为什么非要绕一大圈用 LDR 加舵机做一台机械指针表?做完之后我才真正理解了其中的差别。数字读数追求的是“精确”,但人眼和大脑天生不擅长快速理解一长串数字的物理意义。你说“当前照度 387 lux”,我得先想一下这数字放在哪个区间;但如果表盘指针稳稳停在“室光”和“日光”之间,我瞥一眼就知道这个角落的光线够不够看书,不需要任何换算。
这种“指针直觉”在现实场景里非常有用。比如我后来把它放在育苗架旁边,光线不足时指针会滑到表盘的暗区,孩子隔着两米都能看出“该补光了”;再比如做暗房或模型喷涂时,你需要的是“挡一下指针有没有越过警戒线”,而不是盯着屏幕上的数字发愣。机械表盘天然适合这类“快速判断边界”的任务,它牺牲了一点点精度,换来了极其直观的状态呈现。
1.2 一个组件一个职责:系统角色拆解
整个项目听起来是“一个设备”,但内部其实可以拆成四个职责明确的模块,理解了这条主线,后面无论怎么写代码、调参数都不会乱。
第一是感知模块,也就是 LDR 光敏电阻。它负责把“亮度”这个物理量变成“电阻值”,光照越强电阻越小,常见型号比如 GL5528,暗态电阻能到 1MΩ 级别,亮态能降到 1~2kΩ。第二是转换模块,在 MakerBuddy 开发板上,ADC 读到的不是电阻而是电压,所以必须搭一个分压电路把 LDR 的电阻变化换算成 0~Vcc 之间的电压。第三是执行模块,也就是舵机。它把处理好的数值转成机械动作,让指针在表盘上指向对应的刻度。第四是传输模块,也就是套件里的 WiFi 核心板,它负责把照度值定期推送到手机或平台,让这台“仪表”不只是一块挂在墙上的装饰物,而是物联网里的一个数据接点。
把系统切开来看之后,你会发现每个模块的调试边界非常清楚:指针不动是舵机问题,指针乱跳是供电或滤波问题,读数离谱是分压电路和标定问题,手机收不到数据是网络问题。排查效率会高很多。
1.3 适合谁,做完能带走哪些能力
如果你是一个刚把 LED 灯和按键玩明白的初学者,这个项目的难度曲线刚刚好:没有晦涩的算法,但每一步都需要你真正动手动脑。你会亲手算一次分压电阻,理解 ADC 的量化精度,学习如何用滑动平均处理传感器噪声,知道舵机 PWM 控制的基础原理,还会第一次把数据往 IoT 平台上推。
如果你是已经写过不少 Arduino 程序的老手,这个项目的价值在于“把旧知识串起来”。我很长一段时间觉得 LDR 就是初中物理里的光敏电阻,随便接个电压采样就行;直到认真做完标定才发现,轻飘飘一句“LDR 随光强呈非线性变化”背后藏着多少细节——它不是简单的反比例关系,而是近似幂律关系,你采出来的 ADC 和真实 lux 之间还隔着两层对数运算。这些细节课本上不会专门讲,所以我把整个推导过程放到下文。
2. 硬件选型和分压电路的计算逻辑
2.1 帮 LDR 找到合适的“搭档”电阻
只讲接线不给计算过程是耍流氓。LDR 本身是二端器件,不能直接插在 ADC 上,必须和一只固定电阻串联,组成分压器,让 ADC 引脚上的电压随光强变化。那么问题来了:这只固定电阻选多少合适?
先说 LDR 的脾气。GL5528 这类硫化镉光敏电阻,阻值变化范围大体如下:漆黑房间约 1MΩ,黄昏或微光环境约 50kΩ 到 100kΩ,室内灯光约 5kΩ 到 20kΩ,正午直射阳光或强光下约 1kΩ 到 2kΩ。而且它呈强烈的对数/幂律属性:从 10 lux 变到 100 lux,电阻可能从 50kΩ 掉到 10kΩ;从 100 lux 变到 1000 lux,电阻才从 10kΩ 掉到 3kΩ。这意味着你不能指望“ADC 电压 = 光强 × 常数”,后面一切都要围绕非线性做文章。
固定电阻的选型思路是取目标量程的几何中点,这样电压摆幅能覆盖 ADC 的最大动态范围。比如你希望量程覆盖 1kΩ 到 1MΩ,那就选 sqrt(1kΩ × 1MΩ) ≈ 31.6kΩ,市面上最接近的就是 33kΩ。如果固定电阻选得太小,比如 10kΩ,分压器在暗处的灵敏度就极低——ADC 电压几乎贴着地线,读数变化肉眼不可见;如果选得太大,比如 100kΩ,亮处电压又会早早顶到 5V,还没到强光指针就已经打满表盘了。所以这个 33kΩ 不是随手摸的,它是一个贴着“亮暗两头兼顾”思路算出来的平衡点。
2.2 分压电路图与电压换算公式
电路结构其实非常简单,一条线上四个节点:正极 Vcc 接到 LDR 一端,LDR 另一端接入节点 A,节点 A 再接 33kΩ 固定电阻到 GND,同时把节点 A 引到开发板的 ADC 引脚。读出 ADC 数字值之后,先换算成电压:
V_ldr = ADC / ADC_MAX × Vcc然后根据分压公式反推 LDR 当前阻值:
R_ldr = R_fixed × (Vcc - V_ldr) / V_ldr这里很多人容易踩一个坑:以为 ADC 最大读数是 1023 就一定代表 5V。其实 MakerBuddy 这类套件的核心板若是 ESP32/ESP8266 方案,ADC 参考电压可能是 3.3V,甚至 ESP32 的 ADC 在默认衰减配置下量程也不是 0~3.3V 全程精准。所以第一步永远是查你手上开发板的 ADC 位数和参考电压,然后把你代码里的ADC_MAX和ADC_REF这两个常量改对。我一开始图省事直接套 Arduino Uno 的 1023/5V,读出来的数据看起来像模像样,但仔细核对后误差大得离谱,后来改成 4095/3.3V 才恢复正常。这个小细节会决定你后面所有标定数据的可信度。
2.3 接线顺序与供电拆分:舵机必须单独喂
接线看起来不超过五根线,但供电方案千万别省。LDR 分压器这条支路用一个电压就行,我用的是开发板的 3.3V 输出,正好喂给 ADC 参考量程。舵机 SG90 是另一套逻辑,它需要 4.8V~6V 供电,而且启动瞬间的电流尖峰能冲到几百毫安,如果图省事把舵机和 LDR 分压器一起挂在开发板的 5V 引脚上,会发生两个问题:一是舵机启动时拉低系统电压,ADC 参考电压跟着波动,读数会突然跳一个台阶;二是舵机堵转或高速摆动时,板载稳压器发热严重,严重时直接复位重启。
我的接法是舵机红黑线单独接一个 5V 稳压模块或者四节电池盒,信号线接到开发板 PWM 引脚,黑线和开发板 GND 共地。注意“共地”是必须的,否则舵机信号线没有参考电平,指针会抽风。LDR 那一路和舵机地线在板上汇成一个点,别让高频 PWM 电流自己绕圈子。
2.4 MakerBuddy 套件里哪些东西能直接用
关于“套件里有什么”我不想写得太死,因为不同批次配置可能不一样,但一般都会包含:带 WiFi 的开发板、面包板、若干杜邦线、LED、传感器盒、舵机。前几项自不必说,值得注意的是舵机——如果你手上的套件配的是小号角舵机,比如常见的 SG90 蓝色壳子那类,完全可以直接用;如果只给了普通马达,那就需要另买。传感器盒里通常没有 LDR,但 LDR 单价极低,买一小包五毛钱一个的 GL5528 够用很久。表盘材料更简单,硬纸板或者塑料片都可以,后面机械部分我会详细讲。
| 元器件 | 型号/规格 | 用途 |
|---|---|---|
| LDR 光敏电阻 | GL5528 或参数相近 | 光强度感知 |
| 固定电阻 | 33kΩ | 分压搭档 |
| 舵机 | SG90 或类似 9g 舵机 | 驱动表盘指针 |
| 开发板 | MakerBuddy IoT 核心板 | 采样、运算、WiFi 上报 |
| 电源 | 5V 稳压模块/电池盒 | 舵机独立供电 |
3. 写固件:从 ADC 读数到舵机角度
3.1 先让读数稳定下来
写代码的第一步不是映射函数,而是滤波。裸读analogRead()的值在环境光线稳定的时候也会上下抖,尤其在日光灯下,因为交流电 50Hz/60Hz 频闪会被传感器捕捉到,读数能抖掉几十个数。我用的办法是滑动平均采样:一次取 10~20 个样本,去掉最大值最小值,再求均值,这样既能平滑频闪,又不会引入明显延迟。
#define LDR_PIN A0 #define ADC_MAX 4095.0 // ESP32 是 12bit;Arduino Uno 改 1023 #define ADC_REF 3.3 // 以实际开发板 ADC 参考电压为准 #define FIXED_RESISTOR 33000.0 // 33kΩ float readLDRVoltage() { int samples[10]; for (int i = 0; i < 10; i++) { samples[i] = analogRead(LDR_PIN); delay(2); } // 简单去极值后求均值 int minVal = samples[0], maxVal = samples[0]; long sum = 0; for (int i = 0; i < 10; i++) { if (samples[i] < minVal) minVal = samples[i]; if (samples[i] > maxVal) maxVal = samples[i]; sum += samples[i]; } sum -= minVal + maxVal; float adcAvg = sum / 8.0; return adcAvg / ADC_MAX * ADC_REF; }采样间隔不必太短,10 次采样加上每次 2ms 的延时,总共约 20ms,对光线这种慢变量来说完全可以接受。如果你后面要跑 IoT 上报,我建议把滤波函数独立出来,和数据上传模块解耦,这样调 WiFi 代码时不会连累采样。
3.2 电压、电阻和照度:三次换算的链条
拿到电压后,下一步是推 LDR 阻值,这一步是纯物理公式,没有任何玄学:
float voltage = readLDRVoltage(); float ldrResistance = FIXED_RESISTOR * (ADC_REF - voltage) / voltage;真正让人头疼的是第三步:从电阻换成 lux。我前面说过 LDR 的阻值和照度不是线性关系,实测可以在双对数坐标上获得近似直线关系,用公式表示就是:
lux = K / (R_ldr ^ γ)其中 K 和 γ 是每个传感器个体的“出厂性格参数”,严格来说每个电阻都不一样。网上很多文章直接给你一个经验公式,比如lux = 500 / (R_ldr / 1000)^1.1,这个公式对某个特定个体可能挺准,换一个 LDR 误差可能翻倍。那该怎么办?答案是别纠结“出厂参数”,直接进入第四步的标定流程,用你手头这颗传感器实际测两组数据,反推 K 和 γ。代码里我暂时放一个待标定版本:
float resistanceToLux(float r) { const float K = 500.0; // 待标定 const float G = 1.1; // 待标定 float rKohm = r / 1000.0; return K / pow(rKohm, G); }3.3 从照度到舵机角度:映射函数怎么写
拿到 lux 值之后,就要决定表盘的量程了。我的表盘量程定在 1 lux 到 1000 lux,对应角度 20° 到 160°。注意不要用 0° 和 180°,因为很多舵机的机械限位在两端会堵转,电流飙升还可能让舵机齿轮受力,留出 20° 的余量是长寿之道。
一种最常见的错误是直接用map()线性映射,我之前就这么干过,结果发现晴朗午后 1000 lux 的照度,换算出来的角度只有 120°——整个表盘上半段形同虚设。原因在于人眼感觉到的“亮一倍”对应的是照度翻十倍,所以表盘刻度必须按对数分布才“符合直觉”。我的映射函数对 lux 取对数后再做线性映射:
float mapLuxToAngle(float lux) { if (lux < 1.0) lux = 1.0; if (lux > 1000.0) lux = 1000.0; float logLux = log10(lux); // 0 ~ 3 float t = (logLux - 0.0) / (3.0 - 0.0); // 归一化 return 20.0 + t * (160.0 - 20.0); // 20° ~ 160° }这么一改,下午两点的直射光会稳稳停在 160° 附近,室内灯光落在 100° 到 130°,黄昏光线滑到 50° 左右,整块表盘的利用率立刻不一样了。
3.4 指针不要甩过去:平滑走表
新手最容易忽略的细节是舵机响应速度。如果你直接把目标角度servo.write(angle)写进去,输入变化一大,指针就会“嗖”地一下飞过去,看起来像表盘坏了。真实仪表指针是缓慢摆动的,这里需要给舵机加一个平滑逼近逻辑:每次目标角度只移动一两度,循环逼近,直到差值小于阈值。
#include <Servo.h> Servo gaugeServo; int currentAngle = 90; const int stepSize = 2; void smoothMoveTo(int target) { while (abs(target - currentAngle) > stepSize) { if (target > currentAngle) currentAngle += stepSize; else currentAngle -= stepSize; gaugeServo.write(currentAngle); delay(15); } currentAngle = target; gaugeServo.write(currentAngle); }这样一个大约 140° 的全行程摆动大约耗时 1 秒,非常有“仪表感”。实测下来,delay 15ms 配合 stepSize 2 是观感比较自然的一组参数,太快像闹钟指针,太慢又让人着急。如果你的应用场景需要快速响应,可以加大 stepSize,但要留意舵机连续高速摆动的发热问题。
3.5 IoT 上报:数据怎么送到手机
MakerBuddy 套件最大的价值就是自带网络能力,把数据留在本地串口打印太浪费。我在项目里用的是 MQTT 协议,上报频率设置成 5 秒一次,这是传感器能承受的量级,也不会给路由器造成负担。上报内容不复杂,就是 JSON 格式的照度、原始阻值和指针角度:
{"lux": 320, "r": 8.5, "angle": 103}这里有一个来自实测的提醒:WiFi 上报和舵机执行不要在同一毫秒段内抢占 CPU,否则舵机容易产生卡顿。我的办法是每 5 秒上报一次,上报期间暂时跳过采样,把这些开销错开。另外,上报失败要设计成静默重试,而不是堆在回调里死循环,否则一旦路由器抽风,你的仪表指针会跟着抽搐。
4. 标定:把“感觉”变成“刻度”
4.1 标定不是玄学,是两个点的事
回到 3.2 节悬而未决的问题:LDR 的 K 和 γ 到底怎么定?我的做法是“两点标定法”,也就是找两个已知照度的场景,记录对应的 LDR 阻值,然后用两对数点求解直线。对 lux 和 R 都取以 10 为底的对数,会得到一个直线关系:
log10(lux) = A × log10(R) + B把两个点的数据代进去,解出 A 和 B 即可。我用的两个参考场景分别是:手机关闭自动亮度的屏幕调到最高亮度贴近传感器,用照度计 App 测得大约 400 lux;以及阴天窗户旁边,App 实测大约 50 lux。设备测到这两个场景下 LDR 阻值分别是 2.8kΩ 和 18kΩ,代入计算后得到一条标定直线:
lux = 980 / (R_ldurkΩ) ^ 1.15这个公式看起来和我之前的经验公式差不多,但关键是常数是针对我这颗具体传感器的。你换一颗 LDR,最好重新标,成本极低。
4.2 非线性的“别扭感”:为什么表盘要按对数画
标定完成之后,我发现一个特别有意思的现象:如果按 lux 数值线性画表盘刻度,1、10、100、1000 四个刻度之间的距离是真的天差地别,1000 那一格挤在表盘最右边,根本没法用。换成对数刻度后,1、10、100、1000 变成等距分布,指针移动幅度和人的视觉感知才匹配。
更深层的原因是,人眼对亮度的感知本身接近对数量表:100 lux 到 1000 lux 的亮度翻倍感,和 10 lux 到 100 lux 的翻倍感差不多。所以 LDR 的非线性 + 人眼的非线性,最终要用一个对数表盘来“对齐”。理解这一点后,你就明白为什么很多专业光照仪表用的都是非线性表盘了。
4.3 把标定参数存进 EEPROM
标定出来的 A 和 B 如果每次开机都手动填进代码,那维护起来太痛苦。我的做法是开机时从 EEPROM 读取标定参数,如果读到异常值就回退到默认参数。EEPROM 是掉电不丢的,而且写入次数有限,所以标定完毕后再写一次即可,平时运行只管读。
做一个简单的串口标定命令会让这个过程舒服很多。我在代码里加了三个命令:输入CAL 400 2.8就表示“当前光环境是 400 lux,传感器阻值 2.8kΩ”,收集两个点之后自动算出 A、B 并写入 EEPROM;输入RESTORE清除标定数据。这样调试时不用反复烧录程序,非常顺手。
4.4 标定漂移了怎么办
硫化镉光敏电阻是有“记忆”的,长期暴露在强光下,阻值会老化漂移;潮湿环境也会让读数缓慢变化。我的经验是每两三个月重新标定一次,或者按季节调整——夏天窗外 1000 lux 很常见,冬天阴天可能只有 300 lux,量程边界和使用习惯变了,标定也该跟着变。
另外要注意传感器朝向。LDR 是半球形感光面,它的“视线”有方向性,如果标定时光线从左边照,使用时光线从右边照,读数会差出不少。我的做法是让传感器朝向固定,标定和使用都用同一个朝向,并且加一个遮光罩,只留正面小窗口进光,这样能有效减少杂散光干扰。
5. 机械组装:表盘、指针和舵机的相处之道
5.1 表盘刻度:先画后切
电子部分搞定之后,机械部分千万别随便糊弄,因为这台仪器的核心体验就是“看表盘”。我的表盘是一张 A5 硬卡纸,先用圆规画圆心,再用刻度尺把量程 1 lux~1000 lux 的对数刻度标出来。对数刻度的位置很好计算:每一格对应的角度就是20° + (log10(lux) / 3) × 140°,用计算器算好位置,直接描点。
描点时我有一个重要发现:先画轴线再写字,顺序别反。如果先在圆里写字再画刻度,字的位置七歪八扭;反过来先定刻度位置,数字作为辅助信息写在刻度内侧,整体会规整得多。表盘中心孔我用的是打孔器,直径比舵机转轴稍大一点,保证装配时能有微小调整的余量。
5.2 舵机固定与指针配重
指针我用的是一截塑料信用卡边角料,剪成箭头形状,再粘到舵机摇臂上。这里有一个反复调了很久的细节:指针重量要轻,但必须配重。轻是为了减少舵机负载,配重是为了消除重心偏移——不配重的话,指针会因为重力下垂几度,造成“静态误差”。
在剪好的指针背面用热熔胶粘一个小螺母,反复调整位置,让指针在水平、竖直、斜向不同姿态下都能停在同一个角度,就说明配重平衡了。这个步骤很琐碎,但做完之后舵机的工作稳定性和指针回零的一致性都会明显改善。
5.3 供电与抖动:最容易被低估的问题
机械部分的头号杀手是抖动。现象多半是:指针在某个位置小幅高频振动,像帕金森一样。原因通常有两类,一类是舵机 PWM 信号线受到干扰,导致控制信号抖动;另一类就是供电不足,舵机在怠速时维持不住位置,控制芯片不断修正,形成振荡。
排查方法也简单:用手轻轻扶住指针,感受舵机是不是在持续“较劲”。是的话先换独立电源,再考虑给信号线端并联一个 100nF 电容。别一上来就怀疑代码,舵机抖动的物理根源十有八九在电源和接线。我的项目最终用了两节锂电池串联稳压到 5V 的方案,负载电流余量充足,抖动问题彻底消失。
5.4 光敏电阻的安装位置:请给它开一扇窗
LDR 的安装位置决定这台仪表测的“到底是谁的光”。我把 LDR 装在表盘正上方一个小凸台里,凸台前侧开了一个直径 5mm 的小孔,光只能从这个小孔进入传感器表面。这样做有三个目的:一是减少侧面杂散光,提高测量指向性;二是遮挡能让读数更稳定,不会被手影干扰得乱跳;三是视觉上很自然——小孔看起来像仪表的一个“感光窗”,甚至有点复古仪表的意思。
如果不想做凸台,最简单的办法是拿一块黑色热缩管套在 LDR 上,只露出顶端平面,效果也不错。切忌直接裸放传感器在表盘旁边,那样你只是测了“这台设备背后墙面的光”,和表盘正上方想测的光差之千里。
6. 实测与翻车记录
6.1 三个最典型的翻车现场
翻车一:指针满偏复位后空转。现象是舵机每到 180° 就咔咔响,像要咬碎齿轮。原因是我最初映射角度用了 0°~180° 全程,舵机机械限位顶死,电流飙到 300mA 以上。改成 20°~160° 后症状消失。
翻车二:同一环境两次读数相差一倍。排查发现是 LDR 被手指捏过,汗渍导致表面电阻变化。光敏电阻表面非常敏感,装配时一定戴手套或者用镊子,别用裸手反复摸。
翻车三:WiFi 一连接,指针开始抽动。这是共地干扰类问题,WiFi 射频工作时会让地线电位瞬间波动,影响舵机信号。我用的是独立充电宝供电舵机,同时把天线朝上远离信号线,问题缓解。更彻底的做法是用光耦隔离舵机信号,或者用金属屏蔽罩把通信模块包起来。
6.2 实测数据长什么样
我连续记录了一整天生意的读数,挑几个典型时刻:清晨 6 点半室内约 8 lux,指针停在表盘左侧;上午 10 点书桌靠窗位置约 450 lux,指针接近中段偏右;中午直射光下冲到 1200 lux,已经超出量程上限,表盘在满偏处打顶,说明量程设计偏保守;傍晚 6 点回落到 30 lux。整体趋势、重复性和平滑度都达到了可用的水平。
| 时刻 | 实测 lux | LDR 阻值 | 指针角度 |
|---|---|---|---|
| 清晨室内 | 8 | 71kΩ | 45° |
| 上午靠窗 | 450 | 2.5kΩ | 118° |
| 午间直射 | 1200 | 1.2kΩ | 160°(打顶) |
| 傍晚室内 | 30 | 22kΩ | 70° |
6.3 还能往哪里扩展
这台仪表完成后,我顺手加了两个小功能,工作量不大但实用性提升明显。一个是“低光提醒”——当 lux 连续 30 秒低于 50 时,让 LED 闪烁提示;另一个是把 MQTT 数据接入了本地智能家居面板,让阳台上的花房补光灯根据窗外照度自动开关。如果你感兴趣,还可以把单点采样改成多点阵列,用几只不同朝向的 LDR 做一个简易光照方向识别器。核心代码和标定框架都不需要推翻,改改映射函数就能玩出新花样。
我个人做这个项目的最大心得是:很多教程把 LDR 当“能测光的电阻”一笔带过,但真正照着做一台能用的仪表,你会发现每一个环节——分压、ADC、滤波、标定、机械装配——都是“看起来简单,做起来有细节”。这些细节不像烧录程序那样有无情错误提示,它们藏在数据偏差和指针抖动里,恰恰是这些最考验人的耐心。希望你在交完表盘上的最后一根指针之后,也能体会到指针停在正确位置时那种说不出的满足感。