
1. 项目概述为什么选择LDR与Arduino我儿子最近迷上了遥控车总想测测自己的圈速看看有没有进步。我在的时候还能帮他掐个表但我不在他就得一手拿遥控器一手去按手机秒表手忙脚乱根本没法专心开车。这事儿让我这个搞了十几年工业控制的老工程师有点坐不住——做个计时器还不简单正好手头一堆零件于是给自己立了两个规矩第一绝不买新东西只用现成的材料第二不上网查现成方案逼自己从头想。扫了一眼零件盒显示部分好说就剩个1602的LCD屏了。关键是传感器。怎么让车无接触地触发计时我翻出了红外对管、电容/电感式接近开关还有最不起眼的光敏电阻LDR。接近开关要求距离太近红外对管不错但最后我选了LDR。原因很简单它太便宜、太常见了几乎每个电子爱好者的入门套件里都有而且原理直观对环境光的变化极其敏感非常适合做这种“光束中断”式的检测。这个项目的核心就是利用一束聚焦的光线和一个LDR在赛道上搭建一个无形的“光栅”。当赛车穿过光束时LDR接收到的光强骤变其电阻值随之剧烈变化这个模拟信号被Arduino捕获并判断从而精准地控制计时器的启动与停止。整个系统分为发射端光源和接收处理端Arduino主控显示独立供电部署灵活。下面我就把从电路原理、代码逻辑到机械结构、调试心得的所有细节毫无保留地分享出来。2. 核心电路设计与传感器原理剖析2.1 LDR传感器电路从光强到电压信号LDR或者说光敏电阻它的核心是一个硫化镉CdS半导体元件。其工作原理是光电导效应当光子照射到半导体材料上如果光子能量足够大就能将价带中的电子激发到导带从而产生电子-空穴对显著降低材料的电阻。无光照时其阻值可达几兆欧姆MΩ在强光下可能只有几百甚至几十欧姆。在Arduino这类微控制器系统中我们无法直接读取电阻值需要将其转换为电压信号。最经典、最可靠的方法就是构建一个分压电路。具体接法如下将LDR的一端连接到Arduino的5V或3.3V电源。将LDR的另一端连接到一个固定阻值的上拉电阻我常用10kΩ然后这个连接点再接到Arduino的一个模拟输入引脚如A0。上拉电阻的另一端接地GND。这样LDR和固定电阻就构成了一个分压器。模拟引脚A0测量的是两者中间节点的电压Vout。根据分压公式Vout Vcc * (R_fixed / (R_LDR R_fixed))。当光照增强R_LDR减小A0点的电压Vout就会升高反之光照变弱R_LDR增大Vout降低。关键技巧上拉电阻的选择这个10kΩ的上拉电阻值不是固定的它需要根据你预期的光照条件范围来调整。其核心目的是让LDR在明、暗状态下的电阻变化能映射到Arduino模拟输入端口0-5V尽可能大的电压区间上以提高检测灵敏度。你可以先用万用表测量一下你的LDR在环境光无遮挡和完全遮挡时的阻值然后选择一个介于这两个阻值之间的电阻作为上拉电阻这样分压点电压的变化范围最宽。我手头10kΩ的最多实测在室内灯光下LDR阻值约2-5kΩ遮挡后升至200kΩ以上用10kΩ上拉电压变化范围大约在3.3V到0.2V之间动态范围足够大。2.2 主控与外围电路集成主控板我用的是一块Arduino Nano因为它体积小巧引脚功能与Uno完全兼容。整个接收端的电路连接如下LDR传感器按上述方法接入模拟引脚A0。LCD屏幕带I2C适配板这是极大的简化。早期的1602 LCD需要接7-10根线而I2C适配板将通信精简到只需4根线VCC, GND, SDA, SCL。将SDA接到A4SCL接到A5。功能按钮我用了两个弹簧复位式按钮。一个接在数字引脚D2和GND之间作为“复位/清零”按钮另一个接在D3和GND之间作为“模式切换”按钮例如切换显示单圈最快时间或上一圈时间。注意Arduino内部需要启用上拉电阻pinMode(pin, INPUT_PULLUP)这样按钮另一端只需接地按下时为低电平松开时引脚被内部上拉到高电平省去了外部电阻。蜂鸣器一个无源蜂鸣器正极接数字引脚D8负极接GND。用于在计时开始、结束或破纪录时提供声音反馈。电源整个系统由一节18650锂电池3.7V供电通过一个MT3608升压模块稳定到5V给Arduino Nano和LCD供电。同时用一个TP4056充电模块管理电池充电通过Micro USB口即可充电。电源通断由一个船型开关控制。实操心得电源隔离与稳定性数字噪声隔离蜂鸣器尤其是无源蜂鸣器在驱动时会产生较大的电流波动可能通过电源线干扰敏感的模拟电路LDR分压电路。如果发现LDR读数在蜂鸣器响时跳动可以在给Arduino供电的5V入口处并联一个100μF的电解电容和一个0.1μF的瓷片电容分别滤除低频和高频噪声。I2C上拉电阻很多I2C LCD适配板已经集成了上拉电阻。如果没有或者通讯不稳定屏幕乱码需要在SDA和SCL线上各自添加一个4.7kΩ的上拉电阻到5V。按钮防抖机械按钮在按下和弹起时触点会产生数毫秒的抖动会被单片机误判为多次按下。必须在软件中进行消抖处理这是稳定性的关键。3. 光束发射器的设计与制作要点一个稳定、聚焦的光束是可靠检测的前提。我的目标是做一个小型、可调焦的“手电筒”。3.1 光学与结构设计我拆了一个旧的强光手电取用了里面的草帽型超高亮LED和凸透镜。透镜是关键它能将LED发出的散射光汇聚成平行或略微汇聚的光束使得在几米外的LDR处仍能形成明亮的光斑。结构上我用了直径32mm的PVC排水管作为主体外壳因为它内部光滑直径合适。两个25mm的电工导管直接头一端有螺纹一端光滑充当可调节的座子LED座将LED焊接到一小块洞洞板上引出正负极导线。然后将这个洞洞板用热熔胶固定在第一个直接头内。这个直接头塞入PVC管一端。透镜座将透镜用热熔胶小心地固定在第二个直接头的一端。这个直接头塞入PVC管的另一端。通过旋转和推拉这两个直接头可以非常方便地调节LED与透镜之间的距离从而改变光束的聚焦程度。我们需要将光束调到在LDR位置处光斑最小、最亮的状态。3.2 光源驱动电路驱动电路极其简单就是一个恒流或限流电路。超高亮LED的工作电压一般在3-3.4V电流可能需要20-150mA视型号而定。我直接用了一节18650电池满电4.2V标称3.7V供电但必须串联一个限流电阻来保护LED。首先用万用表测量LED的正向电压Vf。假设Vf 3.2V电池电压Vbat 3.7V希望工作电流I 50mA。那么所需限流电阻R (Vbat - Vf) / I (3.7 - 3.2) / 0.05 10Ω。电阻的功率P I^2 * R 0.05^2 * 10 0.025W一个普通的1/4W电阻绰绰有余。实际接线电池正极 - 拨动开关 - LED正极 - LED负极 - 限流电阻 - 电池负极。同样用TP4056模块为这颗18650充电。整个光源部分独立封装在一个小盒子里。避坑指南光束稳定性环境光干扰这是LDR方案最大的挑战。避免在阳光直射或频繁变化的人造光源如闪烁的日光灯下使用。我的解决方案是给LDR加一个遮光筒。我用了一小段20mm的PVC电工管将LDR塞进一个橡胶水管垫片再紧紧塞入管子里。这个管子只正对光源方向开孔极大地减少了侧面环境光的干扰。聚焦调节调试时先将发射器和接收器相对放置间隔预定赛道宽度比如1米在昏暗环境下用一张白纸在LDR位置查看光斑调节透镜距离直到光斑成为一个清晰、明亮的小圆点。然后再将LDR放入遮光筒并对准这个光斑。LED老化与发热长期工作在额定电流上限会缩短LED寿命。如果没有散热措施建议将电流设置在额定值的70-80%。我的草帽LED标称80mA我实际用15Ω电阻电流约33mA亮度已完全足够且几乎不发热。4. 软件逻辑与Arduino代码详解代码的核心是状态机逻辑和可靠的阈值检测。我基于Arduino IDE自带的“Debounce”示例进行了大幅改造。4.1 核心状态机与计时逻辑系统主要有以下几个状态IDLE空闲状态屏幕显示“Ready”或最近的最佳圈速。等待光束第一次被遮挡。ARMED已触发第一次遮挡计时器启动屏幕开始动态显示递增的时间。等待光束第二次被遮挡赛车完成一圈。FINISHED第二次遮挡触发计时停止保存本次圈速更新最佳圈速蜂鸣器提示屏幕显示结果。等待复位或下一次开始。// 定义状态 enum TimerState { IDLE, ARMED, FINISHED }; TimerState state IDLE; unsigned long lapStartTime 0; unsigned long lastLapTime 0; unsigned long bestLapTime 0; void loop() { int ldrValue analogRead(LDR_PIN); // 读取LDR电压值 bool beamBlocked (ldrValue TRIGGER_THRESHOLD); // 判断是否遮挡 switch(state) { case IDLE: if (beamBlocked) { // 第一次遮挡启动计时 lapStartTime millis(); state ARMED; tone(BUZZER_PIN, 1000, 100); // 短促提示音 } break; case ARMED: // 动态显示已过去的时间 displayElapsedTime(millis() - lapStartTime); if (beamBlocked) { // 第二次遮挡停止计时 lastLapTime millis() - lapStartTime; if (lastLapTime bestLapTime || bestLapTime 0) { bestLapTime lastLapTime; tone(BUZZER_PIN, 1500, 300); // 破纪录长音提示 } else { tone(BUZZER_PIN, 800, 100); } state FINISHED; displayResults(); // 显示本次和最佳圈速 } break; case FINISHED: // 等待复位按钮或自动返回IDLE if (checkResetButton()) { resetTimer(); state IDLE; } break; } // ... 按钮检测等其他逻辑 }4.2 阈值判定与软件消抖直接使用原始的analogRead值进行判断会非常不稳定因为光线可能有微小波动。我们需要引入滞后比较和软件消抖。设定阈值在系统安装好后在Arduino IDE的串口监视器中观察LDR的读数。分别记录光束通畅时的数值较高假设为800和被完全遮挡时的数值较低假设为200。我们将触发阈值设定在两者之间比如500。滞后比较为了防止在阈值附近抖动导致误触发我们使用两个阈值。例如THRESHOLD_LOW 400从亮到暗的触发点。THRESHOLD_HIGH 600从暗到亮的恢复点。 只有当读数低于400才认为光束被遮挡只有当读数高于600才认为光束恢复。这中间的区域是“死区”防止了临界点的振荡。时间消抖即使有了滞后比较一次快速的干扰如飞虫也可能导致读数瞬间低于阈值。因此我们需要引入时间判断只有当“光束被遮挡”的状态持续超过一个短暂的时间例如50毫秒才认为是一次有效的触发。这可以通过记录状态改变的时间点来实现。const int THRESHOLD_LOW 400; const int THRESHOLD_HIGH 600; const unsigned long DEBOUNCE_DELAY 50; // 消抖时间50ms bool lastStableState false; // 上一次稳定的状态true遮挡 unsigned long lastDebounceTime 0; bool readStableBeamState() { bool currentRawState (analogRead(LDR_PIN) THRESHOLD_LOW); bool currentStableState lastStableState; if (currentRawState ! lastStableState) { // 状态发生变化记录时间 lastDebounceTime millis(); } if ((millis() - lastDebounceTime) DEBOUNCE_DELAY) { // 经过消抖时间后状态稳定更新稳定状态 // 但更新前需要判断是否恢复到“通畅”状态要用THRESHOLD_HIGH if (analogRead(LDR_PIN) THRESHOLD_HIGH) { currentStableState false; } else if (analogRead(LDR_PIN) THRESHOLD_LOW) { currentStableState true; } // 如果读数在死区内则保持原状态不变 } lastStableState currentStableState; return currentStableState; } // 在loop()中使用 readStableBeamState() 代替直接的 analogRead 判断4.3 时间处理与显示优化millis()函数返回的是Arduino启动后的毫秒数大约50天后会溢出归零。但对于圈速计时通常几分钟内这完全不是问题。关键在于如何精确计算和显示时间。我们通常希望显示格式为分:秒.百分秒。计算如下unsigned long currentElapsed millis() - lapStartTime; unsigned long minutes (currentElapsed / 60000) % 60; unsigned long seconds (currentElapsed / 1000) % 60; unsigned long hundredths (currentElapsed / 10) % 100; // 百分秒 // 在LCD上格式化显示例如 1:23.45 lcd.setCursor(0, 1); if (minutes 0) { lcd.print(minutes); lcd.print(:); if (seconds 10) lcd.print(0); } lcd.print(seconds); lcd.print(.); if (hundredths 10) lcd.print(0); lcd.print(hundredths);注意millis()在中断函数执行时会暂停递增。因此要确保你的代码中没有长时间阻塞的中断服务程序ISR或者考虑使用外部硬件计时器模块以获得更高的精度对于RC赛车毫秒级精度已完全足够。5. 机械结构组装与现场调试实录5.1 外壳制作与布局我用激光切割3mm的椴木板制作了主控盒。设计工具推荐使用makercase.com输入长宽高和板材厚度它能自动生成带指接榫的盒子图纸导出为DXF或SVG。然后我用AutoCAD LT也可以用免费的Inkscape或Fusion 360在生成的图纸上添加LCD开孔、按钮孔、电源开关孔和蜂鸣器出声孔。免胶水组装我在四个角设计了用M4螺丝紧固的方式而不是用胶水粘合。这样未来维修、升级会方便很多。在激光切割时将螺丝孔切割为3.5mm直径然后用M4的丝锥手动攻出螺纹这样螺丝拧进去非常牢固。LCD装饰框裸LCD屏看起来比较简陋。我用Fusion 360设计了一个带倾角的边框用3D打印机打出来背面贴上厚的双面胶然后粘在面板的开孔上瞬间就有了“产品感”。这个边框还能遮挡住LCD与面板之间的缝隙。内部布局内部空间要规划好。将18650电池盒、TP4056充电模块、MT3608升压模块这些可能发热或有高压的部件放在盒子底部一侧。Arduino Nano、LCD I2C板、蜂鸣器等用排母或杜邦线连接固定在另一侧。所有连线尽量用扎带捆扎整齐避免松动。5.2 现场部署与校准流程架设将光束发射器和接收器LDR遮光筒分别固定在赛道直道的两侧高度与赛车底盘中部平齐确保两者光路对正。可以用小三脚架、沉重的底座或者直接用扎带绑在栏杆上。上电预热打开电源让系统运行几分钟使电子元件和LED光强稳定。阈值校准将系统置于最终使用的环境光条件下。打开Arduino IDE的串口监视器观察LDR的实时读数。确保光束通畅记录稳定的高值VAL_HIGH。用一张卡片完全遮挡光束记录稳定的低值VAL_LOW。在代码中将THRESHOLD_LOW设置为(VAL_LOW 50)将THRESHOLD_HIGH设置为(VAL_HIGH - 50)。这50是一个安全余量用于对抗噪声。将新代码上传至Arduino。功能测试用手快速划过光束观察LCD显示和蜂鸣器声音。计时应准确启停。测试复位按钮和模式切换按钮。6. 常见问题排查与性能优化技巧在实际使用中你可能会遇到以下问题。这里是我的排查清单和解决方案问题现象可能原因排查步骤与解决方案计时不触发或误触发1. 环境光干扰太强。2. 光束未对准或聚焦不良。3. LDR阈值设置不当。4. 电源噪声导致模拟读数波动。1. 加强LDR遮光筒或在傍晚/室内测试。2. 重新对光确保接收器光斑明亮集中。3. 通过串口监视器重新校准阈值并检查代码中的滞后比较逻辑。4. 在Arduino的5V和GND间并联滤波电容100μF 0.1μF。计时数值明显不准1.millis()函数被中断长时间阻塞。2. 代码逻辑错误计时起点/终点判断有误。1. 检查代码中是否使用了delay()或执行时间很长的循环。改用非阻塞的时间判断逻辑。2. 用串口打印调试信息确认光束触发瞬间的lapStartTime和结束时间计算是否正确。LCD显示乱码或不显示1. I2C地址不对。2. 接线错误或接触不良。3. 背光未开启或对比度问题。1. 使用I2C扫描程序确认LCD的地址通常是0x27或0x3F。2. 检查VCC、GND、SDA、SCL四根线是否接牢。SDA接A4SCL接A5。3. 检查代码中是否初始化了LCD并开启了背光。调节LCD模块上的电位器改变对比度。蜂鸣器不响或声音小1. 引脚驱动能力不足。2. 蜂鸣器类型错误用了有源蜂鸣器。1. Arduino引脚直接驱动电流有限约20mA。可以尝试在代码中增加tone()函数的频率和时长或使用三极管放大驱动。2. 确认使用的是无源蜂鸣器内部无振荡电路有源蜂鸣器给电就响无法通过tone()控制音调。系统运行一段时间后复位1. 电池电量不足电压跌落。2. 升压模块MT3608带载能力不足或发热严重。1. 检查18650电池电压充满电应在4.2V左右低于3.5V可能不稳定。2. 测量Arduino Nano在运行时的总电流约100-150mA。确保MT3608模块能提供至少500mA的稳定输出。必要时增加散热片。性能优化建议提高精度对于追求极限的玩家millis()的精度可能不够。可以考虑使用Arduino的Timer1硬件计时器中断以更精确的周期如1ms更新一个计时变量完全不受主循环执行时间影响。增加功能多圈记忆使用数组存储最近10圈的圈速并计算平均圈速。无线传输增加一个HC-05或HC-12蓝牙/无线串口模块将圈速数据实时发送到电脑或手机APP上进行更复杂的分析。起步指示灯模仿真实赛车增加一个三色LED实现“红灯-红灯-红灯-绿灯”的起步倒计时功能。提升可靠性对于户外或比赛环境可以为整个接收器主控盒制作一个简单的亚克力防水罩。光束发射器和接收器的镜头也可以用透明的塑料片进行保护。这个项目最让我满意的不是它有多精密而是用最简单、最廉价的元件LDR可能就几毛钱解决了一个真实的小需求。看到儿子能自己专注地练习、挑战自己的最快圈速那种成就感比完成一个复杂的工业项目更直接。整个制作过程从电路调试、代码编写到激光切割、3D打印涵盖了电子DIY的多个方面是一个非常综合的练手项目。如果你手头正好有这些零件不妨花一个周末试试它带给你的乐趣和收获一定会超出预期。