Arduino光敏传感器与直流电机交互:从原理到“宵禁归家”游戏实现
2026/6/1 15:10:08 网站建设 项目流程

1. 项目概述与核心思路

几年前,我在带学生做嵌入式入门项目时,总在找一个能把传感器、执行器和趣味性结合起来的案例。太简单的流水灯没意思,复杂的机器人又容易劝退新手。后来看到国外创客社区一个叫“Home by the Curfew”(宵禁归家)的小装置,觉得这个点子特别妙:它用一个光敏传感器、一个直流电机、一个按钮和一个LED,就构建了一个充满随机性和紧张感的微型游戏。玩家按下按钮,电机带动一个画着钟表的转盘飞速旋转,目标是在转盘停止时,让指针恰好停在“12点”位置——那里安装的光敏传感器如果检测到足够暗的环境(模拟深夜),旁边的LED就会亮起,宣告“成功归家”。这本质上是一个基于Arduino的“感知-决策-执行”闭环系统的绝佳实践。

这个项目麻雀虽小,五脏俱全。它不单单是连几根线、抄一段代码,而是涉及了模拟信号采集(光敏)、数字输入读取(按钮)、PWM电机驱动、阈值逻辑判断等多个嵌入式开发的核心概念。对于初学者来说,成功复现这个项目,意味着你真正理解了如何让单片机“感知”世界并“操纵”物体。今天,我就把这个项目的完整实现过程、背后的硬件原理、代码的每一行含义,以及我调试过程中踩过的坑和总结的技巧,毫无保留地分享出来。无论你是电子爱好者、物联网初学者,还是想找个有趣课题的学生,跟着做一遍,收获会远超一个会转的小玩具。

2. 硬件系统深度解析与选型考量

一套稳定可靠的硬件是项目的基石。原项目清单比较精简,我这里会详细解释每个元件的选型原因、关键参数,以及如何根据你的实际情况进行调整。

2.1 核心控制器:为什么是Arduino Uno?

项目选用Arduino Uno,几乎是必然选择。对于这类传感器+执行器的互动项目,Uno的优势非常明显:

  1. 接口丰富:14个数字I/O口(其中6个支持PWM)和6个模拟输入口,完全满足我们连接按钮(数字输入)、LED(数字输出)、电机驱动(3个数字输出)和光敏传感器(模拟输入)的需求,且留有裕量。
  2. 生态完善:有最庞大的社区支持和库资源,遇到任何问题几乎都能找到解答。
  3. 供电灵活:可以通过USB口或外部7-12V电源供电,驱动一个小型直流电机和若干外设绰绰有余。
  4. 成本与易用性:价格低廉,且通过扩展板(Shield)或面包板可以快速搭建电路,无需焊接,特别适合原型开发。

注意:如果你手头只有Arduino Nano或Leonardo,也完全可以,只需注意引脚定义的对应关系。Mega更是大材小用。

2.2 感知核心:光敏传感器(LDR)的工作原理解析

光敏电阻(LDR)是这个游戏的“裁判”。它的电阻值会随着光照强度的增加而减小。我们在电路中将LDR与一个固定电阻(10kΩ)串联,构成一个分压电路。Arduino的模拟输入口(如A2)测量的是LDR与固定电阻之间的分压点电压。

计算过程:假设Vcc为5V,固定电阻R_fixed为10kΩ,LDR在某种光照下的电阻为R_ldr。

  • 分压点电压 V_sensor = 5V * [R_fixed / (R_ldr + R_fixed)]
  • Arduino的ADC(模数转换器)会将0-5V的电压映射为0-1023的整数值(10位精度)。
  • 所以,光照越强,R_ldr越小,V_sensor越低,analogRead()得到的数值也越小。反之,光照越暗,读数越大。

这就是我们代码中判断if (ldrValue <= 850)的逻辑基础。850这个阈值并非固定,它完全取决于你的LDR特性、固定电阻值、以及传感器安装位置的环境光。这就是为什么原作者强调必须用串口监视器测试确定。

2.3 执行核心:直流电机与驱动方案选择

小型直流电机(比如常见的N20减速电机)是转盘的动力源。绝对不可以将电机直接接在Arduino的数字引脚上!Arduino引脚最大只能提供约40mA电流,而一个小电机启动瞬间的电流可能高达100-200mA,这会直接烧毁芯片。

必须使用电机驱动模块。原项目提到了Motor Driver,但没有具体型号。最常见且适合新手的是L298N或TB6612FNG驱动模块。两者区别如下:

特性L298N 驱动模块TB6612FNG 驱动模块
驱动方式双H桥双H桥
最大电流单桥2A单桥1.2A (连续)
效率较低,发热明显高效率,发热小
控制信号需要3个控制引脚(IN1, IN2, ENA)需要3个控制引脚(AIN1, AIN2, PWMA)
待机控制无独立待机引脚有独立的STBY(待机)引脚
推荐指数★★★☆☆ (经典但过时)★★★★★ (性能更优)

原项目代码中出现了standByAIN1AIN2PWMA这样的引脚定义,这高度匹配TB6612FNG的驱动逻辑standBy引脚拉高使能芯片,拉低则进入低功耗待机状态,用于刹车。AIN1AIN2控制方向,PWMA接收PWM信号控制速度。因此,下文将基于TB6612FNG模块进行讲解。如果你只有L298N,代码需要调整(通常用ENA代替standBy,且无需待机控制)。

2.4 电路搭建详解与避坑指南

按照“分区块、理电源”的原则搭建电路,能极大减少错误。

1. 电源管理

  • 将面包板的两侧长排孔分别作为5V电源总线(Vcc)和地线总线(GND)
  • Arduino的5V引脚和GND引脚分别连接到这两条总线。
  • TB6612FNG模块的VCC(逻辑电源)接Arduino 5V,VM(电机电源)接一个独立的7-12V电源(如9V电池)。切勿将电机大电流电源与单片机共用,否则电机启停引起的电压波动会导致Arduino复位。
  • 驱动模块的GND必须与Arduino的GND相连,确保共地。

2. 光敏传感器电路

  • 连接LDR一端到5V总线,另一端连接至10kΩ电阻
  • 该10kΩ电阻的另一端连接至GND总线。
  • LDR与电阻的连接点,引出一根线接到Arduino的模拟引脚A2。这就是分压测量点。

3. 按钮电路

  • 按钮一脚接5V总线。
  • 按钮另一脚同时做两件事:一是接一个10kΩ的下拉电阻到GND(保证按钮未按下时引脚稳定为低电平),二是接一根线到Arduino的数字引脚13(配置为输入)。

4. LED电路

  • Arduino数字引脚12 →330Ω限流电阻→ LED正极 → LED负极 → GND。330Ω电阻在5V下能为LED提供约10mA的安全电流。

5. 电机驱动电路(TB6612FNG)

  • Arduino D9 → 驱动模块 AIN1
  • Arduino D8 → 驱动模块 AIN2
  • Arduino D3 → 驱动模块 PWMA (必须是PWM引脚)
  • Arduino D10 → 驱动模块 STBY
  • 驱动模块AO1、AO2连接直流电机的两根线。
  • 驱动模块GND接Arduino GND。
  • 驱动模块VM接外部电池正极,电池负极接GND。

实操心得:接线时,尽量使用不同颜色的杜邦线区分功能(如红色正极,黑色负极,黄色信号线)。每接好一个子系统(如按钮电路),就写一段简单的测试代码验证功能,比如按下按钮串口打印消息。这样化整为零,最后联调时问题会少很多。

3. 软件逻辑剖析与代码逐行解读

原项目的代码提供了一个很好的框架,但注释较少。我们来把它彻底拆解,并优化其稳定性和可读性。

3.1 引脚定义与全局变量

// 电机驱动引脚定义 (TB6612FNG) const int standByPin = 10; // 待机控制引脚 const int motorPWMPin = 3; // 电机速度控制引脚 (必须是PWM引脚) const int motorAIN1 = 9; // 电机方向控制引脚1 const int motorAIN2 = 8; // 电机方向控制引脚2 // 输入输出引脚定义 const int buttonPin = 13; // 按钮引脚 const int ldrPin = A2; // 光敏传感器模拟引脚 const int ledPin = 12; // LED指示灯引脚 // 全局变量 int buttonState = 0; // 存储按钮状态 int ldrValue = 0; // 存储光敏传感器读数 int lightThreshold = 850; // 光照阈值,需根据实测调整

优化点

  1. 将魔数850定义为变量lightThreshold,方便在代码开头统一调整,无需在逻辑里到处找。
  2. 引脚命名更清晰(如motorPWMPin),避免了原代码中PWMA可能引起的歧义。

3.2 Setup函数:初始化配置

void setup() { // 初始化串口通信,用于调试输出传感器数值 Serial.begin(9600); // 配置电机驱动相关引脚为输出模式 pinMode(standByPin, OUTPUT); pinMode(motorPWMPin, OUTPUT); pinMode(motorAIN1, OUTPUT); pinMode(motorAIN2, OUTPUT); // 初始状态:电机待机(停止) digitalWrite(standByPin, LOW); // 配置LED引脚为输出模式 pinMode(ledPin, OUTPUT); digitalWrite(ledPin, LOW); // 初始熄灭 // 配置按钮引脚为输入模式(内部上拉电阻未启用,依赖外部下拉电阻) pinMode(buttonPin, INPUT); // 光敏传感器引脚为模拟输入,无需pinMode设置,但显式声明更规范 // pinMode(ldrPin, INPUT); // 对于模拟引脚,此句可写可不写 Serial.println("系统初始化完成!"); }

关键解释

  • Serial.begin(9600):这是调试的生命线。务必打开Arduino IDE的串口监视器(工具 -> 串口监视器),设置波特率为9600,才能看到传感器数据。
  • 电机初始置于待机状态,是一个安全的好习惯。
  • 按钮配置为INPUT模式,依靠外部下拉电阻确保稳定。你也可以使用内部上拉电阻:pinMode(buttonPin, INPUT_PULLUP),但此时按钮接线逻辑要反转(按钮一脚接GND,另一脚接引脚),判断逻辑变为if (buttonState == LOW)

3.3 Loop函数:主循环逻辑精讲

这是游戏的核心逻辑,我将其重写并增加了详细注释:

void loop() { // 步骤1:持续读取环境光强度 ldrValue = analogRead(ldrPin); // 将实时数据打印到串口,用于确定阈值 Serial.print("当前光敏值: "); Serial.println(ldrValue); // 步骤2:根据光阈值控制LED —— “裁判”逻辑 if (ldrValue >= lightThreshold) { // 注意:这里条件改为 >=,更符合“越暗值越大”的直觉 digitalWrite(ledPin, HIGH); // 足够暗,LED亮,表示“成功停在12点” Serial.println("--> 命中目标区域!"); } else { digitalWrite(ledPin, LOW); // 不够暗,LED灭 } // 步骤3:读取按钮状态,控制电机 —— “玩家操作”逻辑 buttonState = digitalRead(buttonPin); if (buttonState == HIGH) { // 按钮被按下,启动电机以一定速度正向旋转 Serial.println("按钮按下,电机启动..."); runMotor(150, 0); // 速度值150(范围0-255),方向0为正转 } else { // 按钮释放,电机停止 stopMotor(); } // 添加一个短暂延时,稳定循环周期,避免串口输出过快 delay(100); }

逻辑流梳理

  1. 永不停止地感知loop()函数循环执行,不断读取A2引脚的光照模拟值。
  2. 实时判决:立刻用这个值和预设阈值比较,决定LED的亮灭。这意味着即使电机在转,只要指针经过“12点”暗区,LED会瞬间亮起,提供了即时的视觉反馈。
  3. 操作响应:检查按钮是否被按下。按下则启动电机,松开则停止。这里电机控制与传感器判决是两个独立的并行逻辑,它们通过共享ldrValue这个变量产生交互。

3.4 电机控制函数封装

将电机动作封装成函数,让主逻辑更清晰,也便于复用。

/** * 控制电机转动 * @param speed 速度,0-255,值越大越快 * @param direction 方向,0为正转,1为反转 */ void runMotor(int speed, int direction) { digitalWrite(standByPin, HIGH); // 使能驱动芯片 if (direction == 0) { // 正转 digitalWrite(motorAIN1, HIGH); digitalWrite(motorAIN2, LOW); } else { // 反转 digitalWrite(motorAIN1, LOW); digitalWrite(motorAIN2, HIGH); } // 使用PWM控制速度 analogWrite(motorPWMPin, speed); } /** * 停止电机,并进入待机省电模式 */ void stopMotor() { analogWrite(motorPWMPin, 0); // 先关闭PWM信号 digitalWrite(standByPin, LOW); // 再进入待机模式 // 可选:将方向引脚也置低,进一步省电 digitalWrite(motorAIN1, LOW); digitalWrite(motorAIN2, LOW); }

封装的好处:主程序中只需调用runMotor(150, 0)stopMotor(),无需关心底层哪个引脚该高该低。代码可读性和可维护性大大提升。

4. 机械结构设计与制作技巧

硬件和软件调通后,一个美观、稳固的外壳能让项目从“实验原型”升级为“可展示的作品”。

4.1 外壳设计思路

原项目使用了一个包裹面包板的“壳”。我们可以做得更精致一些。

  • 材料选择:推荐使用3mm厚的椴木板或亚克力板,易于激光切割,也方便用胶水粘合。没有条件的话,厚卡纸或瓦楞纸也是不错的低成本选择。
  • 设计要点
    1. 预留观察窗:在对应面包板LED、按钮、传感器和电机轴的位置开孔。
    2. 电机固定:这是关键!电机如果只是粘在壳上,转动时容易晃动甚至脱落。最好设计一个带卡槽或螺丝孔的固定座,将电机牢牢锁住。
    3. 传感器遮光罩:正如原作者所说,为LDR制作一个细长的遮光筒(可以用黑色热缩管或剪一段黑色笔芯),垂直安装在“12点”位置。这能极大地提高检测的灵敏度和准确性,避免侧面杂光干扰。
    4. 转盘(表盘)制作:用硬纸板或薄塑料片剪一个圆盘,中心打孔套在电机轴上。用笔画出时钟刻度,并在“12点”位置用黑色马克笔涂黑一个扇形区域,或者直接在这里粘上一小块黑纸。这就是触发传感器的“暗区”。

4.2 组装与校准流程

  1. 先内后外:先在面包板上完成所有电路连接并测试无误,再考虑装入外壳。
  2. 固定核心部件:将Arduino、面包板、电池盒(如果使用)用双面胶或螺丝稳妥地固定在外壳底板上。
  3. 安装传感器与执行器:将LDR(带遮光罩)穿过外壳上对应的孔,固定在“12点”位置。将电机固定在底座上,确保转轴垂直。将LED和按钮也安装到对应孔位。
  4. 连接延长线:由于部件被分开了,你需要用杜邦线(公对公或公对母)将传感器、按钮、LED与面包板上的对应电路连接起来。电机线也需要延长至驱动模块。务必确保连接牢固,可以用热熔胶固定线头。
  5. 最关键的一步:阈值校准
    • 暂时不要粘死转盘。将代码中的lightThreshold初始值设为1023(最大值)。
    • 上电,打开串口监视器。
    • 用手慢慢旋转转盘,让“暗区”(黑色扇形)完全对准LDR的遮光罩。观察串口输出的数值,这个值会显著升高(例如从200升到900)。记录下这个稳定后的高值。
    • 再将转盘转到其他亮区,记录一个典型的低值。
    • 你的lightThreshold应该设在这两个值之间。例如,暗区值900,亮区值300,阈值可以设为600。这样能确保可靠的触发。

5. 调试、优化与扩展玩法

项目基本完成后,你可能会遇到一些问题,或者想让它变得更好玩。这里是我总结的常见问题清单和进阶思路。

5.1 常见问题排查速查表

现象可能原因排查步骤
电机完全不转1. 电源问题(电机驱动未供电)
2. 待机引脚未使能
3. 程序未上传或引脚定义错误
1. 检查驱动模块VM是否接外部电源,电源开关是否打开。
2. 用万用表测standByPin引脚电压,按钮按下时是否为高电平(5V)。
3. 检查Arduino IDE是否选对板和端口,代码是否成功上传。
电机转动但转盘不转1. 电机轴与转盘打滑
2. 电机扭矩不足
1. 用热熔胶或胶水将转盘与电机轴粘牢。
2. 尝试提高runMotor函数中的速度值(最大255),或换用扭矩更大的减速电机。
LED始终不亮1. 阈值设置不当
2. LDR或遮光罩安装问题
3. LED或电阻接反、损坏
1. 打开串口监视器,观察转盘经过暗区时ldrValue是否超过阈值。重新校准。
2. 确保遮光罩紧密,外部光线无法从侧面漏入。
3. 将LED正负极调换试试,或用万用表通断档测试LED。
LED常亮1. 阈值设置过低
2. 环境光太暗
3. 传感器引脚短路或程序逻辑错误
1. 提高lightThreshold值。
2. 在光线充足的环境下测试。
3. 检查LDR电路连接,确认代码中判断条件是ldrValue >= lightThreshold
按钮反应不灵1. 下拉电阻未接或虚焊
2. 按钮引脚接触不良
1. 确认10kΩ电阻一端接按钮引脚,一端接GND。
2. 用万用表通断档测试按钮按下时是否导通。
串口监视器无数据1. 波特率不匹配
2. 串口线松动或端口被占用
1. 确认串口监视器右下角波特率设置为9600。
2. 拔插USB线,在IDE工具->端口中重新选择正确的COM口。

5.2 代码与游戏性优化

基础的“按下转,松开停”玩法有些简单。我们可以通过修改代码增加趣味性和挑战性。

优化一:增加随机旋转时间让每次按下按钮,电机转动一个随机的时间后自动停止,模拟“抽奖”的不可预测性。

unsigned long spinStartTime = 0; bool isSpinning = false; int spinDuration = 0; void loop() { // ... 读取传感器和LED控制逻辑保持不变 ... buttonState = digitalRead(buttonPin); if (buttonState == HIGH && !isSpinning) { // 首次按下按钮,开始旋转 isSpinning = true; spinDuration = random(2000, 5000); // 随机旋转2到5秒 spinStartTime = millis(); runMotor(180, 0); Serial.println("开始旋转!"); } if (isSpinning) { if (millis() - spinStartTime >= spinDuration) { // 旋转时间到,自动停止 stopMotor(); isSpinning = false; Serial.println("旋转停止!"); } // 在旋转期间,即使松开按钮,电机也不会停 } else { // 非旋转状态,按钮松开则确保电机停止(安全冗余) stopMotor(); } }

优化二:增加音效反馈接入一个有源蜂鸣器(接数字引脚),在游戏成功(LED亮)时发出提示音。

const int buzzerPin = 5; // 新增蜂鸣器引脚 void setup() { // ... 其他初始化 ... pinMode(buzzerPin, OUTPUT); } void loop() { // ... 原有逻辑 ... if (ldrValue >= lightThreshold) { digitalWrite(ledPin, HIGH); tone(buzzerPin, 1000, 200); // 发出1000Hz声音,持续200ms Serial.println("--> 成功归家!"); } else { digitalWrite(ledPin, LOW); noTone(buzzerPin); } // ... 其他逻辑 ... }

优化三:难度分级通过增加多个传感器(如在不同时间点位置安装LDR),或者要求指针在旋转中连续通过多个暗区才算成功,可以大幅提升游戏难度和可玩性。

这个“宵禁归家”项目,从电路原理到代码逻辑,从机械结构到调试技巧,完整地走完了一个嵌入式互动装置开发的全流程。它最宝贵的价值在于,用一个非常具象的游戏目标,串联起了多个抽象的知识点。当你看到转盘在电机驱动下飞速旋转,LED因为那一小片黑纸的经过而精准点亮时,你会对“模拟输入”、“PWM控制”、“阈值判断”这些概念有前所未有的真切理解。希望你在复现和改造它的过程中,不仅能收获一个有趣的桌面小玩具,更能点燃对硬件编程和智能交互的持续热情。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询