1. 项目缘起:从“捡瓶子”到“分瓶子”的自动化构想
几年前,我在一个朋友的小型回收站帮忙,亲眼看到工人们需要手动将传送带上混杂的塑料瓶、玻璃瓶和金属罐分拣到不同的收集箱里。这活儿不仅枯燥,效率也低,还容易出错——一个没看清,玻璃瓶就可能混进塑料堆里,给后续的粉碎回收带来麻烦。当时我就想,这种重复性高、规则明确的任务,不正是自动化设备大显身手的地方吗?用机器视觉识别材质,再用机械臂或推杆进行分类,理论上完全可行。
这个念头一直埋在心里。后来接触到Arduino和各种传感器,我发现实现一个基础版的“自动分拣机器人”的门槛,远比想象中要低。它不需要昂贵的工业机器视觉系统,几个常见的传感器加上一些巧妙的机械结构,就能搭建出一个能稳定工作的原型。今天分享的这个“基于Arduino的自动瓶子分拣机器人”项目,就是我经过多次迭代后的一个成果。它核心解决了“识别”与“执行”两个问题:通过传感器判断瓶子的材质(比如是塑料还是金属),然后控制一个简单的执行机构(比如舵机驱动的挡板或推杆)将瓶子引导到正确的路径。
无论你是对Arduino感兴趣的硬件爱好者,还是正在寻找一个综合性实践项目的学生,甚至是小型社区回收点想提升效率的管理者,这个项目都能提供一个从电路搭建、编程逻辑到机械组装的完整思路。它不只是一个玩具,其核心逻辑——感知、决策、执行——是绝大多数自动化设备的基石。接下来,我会拆解每一个环节,从为什么这么选型,到具体怎么做,再到我踩过的那些坑,希望能给你一个清晰、可复现的指南。
2. 核心系统设计:传感器选型与决策逻辑的权衡
自动分拣,第一步是“看见”或“感知”瓶子。市面上传感器种类繁多,选择哪种直接决定了项目的成本、复杂度和可靠性。我经过多轮测试,最终为这个项目定下了一套高性价比且可靠的方案。
2.1 材质识别的“火眼金睛”:红外接近传感器与电感式接近传感器的组合
单纯靠颜色或形状来区分材质是不可靠的,因为瓶子颜色各异,形状也差不多。我们需要找到不同材质的物理特性差异。对于常见的饮料瓶,最主要的区分点是塑料(PET)和金属(铝罐)。玻璃瓶暂不在此版本考虑,因为它需要更精密的抓取机构。
我的方案是使用两个传感器协同工作:
- 红外接近传感器:它发射红外光并接收反射光。塑料瓶对红外光的反射率与金属罐截然不同。通过读取传感器输出的模拟电压值,可以设定一个阈值。当检测到物体时,如果数值高于阈值,则初步判断为塑料(反射强);低于阈值,则初步判断为金属(反射弱,吸收多)。我常用的是E18-D80NK这款,它自带调节旋钮,可以微调检测距离,非常方便。
- 电感式接近传感器:这是一种专门检测金属的传感器。它内部有一个振荡电路,当金属物体靠近时,会在金属内部产生涡流,导致振荡减弱或停止,从而输出开关信号。对于铝罐,它是“一锤定音”的检测器。我选用的是LJ12A3-4-Z/BX,检测距离约4mm,输出数字信号(高/低电平),接线和编程都简单。
注意:红外传感器容易受环境光干扰,尤其是强光。务必将其安装在光线相对稳定的位置,或者加上遮光罩。电感式传感器则只对金属有效,对塑料、玻璃毫无反应,这正是我们需要的特性。
为什么是组合,而不是只用一种?只用红外传感器,虽然能通过反射值差异区分大部分塑料和金属,但遇到深色、哑光的塑料瓶时,反射值可能偏低,容易误判为金属。加入电感式传感器作为金属的“专有裁判”,可以极大提高金属识别的准确率。决策逻辑变得非常清晰:电感式传感器触发 -> 一定是金属;电感式未触发,但红外传感器有信号且反射值高 -> 判断为塑料。
2.2 瓶子到位检测与流程控制:漫反射光电传感器
我们需要知道瓶子什么时候到达了检测工位,以及什么时候离开了分拣口。这里我选用漫反射光电开关。它同样是发射和接收红外光,但与接近传感器不同,它检测的是前方一定距离内是否有物体遮挡或反射。我将其安装在传送带两侧,一个作为“开始检测”的触发点,一个作为“分拣完成”的复位点。
当瓶子经过第一个光电传感器时,它触发一个信号,告诉Arduino:“有物体进入,准备检测!” Arduino随即启动对红外和电感传感器的读数。经过一段非常短的延时(由传送带速度和传感器间距决定),瓶子到达传感器阵列下方,此时进行材质判断。判断完成后,Arduino控制执行机构动作。当瓶子离开分拣区域,经过第二个光电传感器时,系统复位,等待下一个瓶子。
2.3 执行机构的选择:舵机驱动挡板 vs. 推杆
识别之后是动作。对于水平传送带,最简洁有效的分拣方式是用一个挡板改变瓶子的下落路径。我测试过两种方案:
- 方案A:舵机驱动单臂挡板。用一个大扭矩的舵机(如MG996R)带动一个L型挡板。默认位置让瓶子直行落入A箱;当需要分拣时,舵机旋转一定角度(如45度),将瓶子拨向侧面滑道,落入B箱。优点是结构简单,速度快。缺点是对瓶子的冲击较大,如果瓶子较轻或形状不规则,可能被卡住或拨飞。
- 方案B:直线推杆(电动推杆)。推杆水平安装在分流口。默认状态下缩回,瓶子直行。需要分拣时,推杆伸出,将瓶子推入侧面滑道。优点是动作平稳,力度可控,适应性更好。缺点是成本稍高,速度比舵机慢一点。
对于本项目,我最终选择了方案A的舵机挡板。原因在于,我们分拣的是具有一定重量和刚性的饮料瓶,舵机的瞬间扭矩足以完成拨动,且响应速度更快,有利于提高分拣节拍。关键在于要设计一个带有弧面的挡板头,并用海绵或橡胶包裹,减少冲击和噪音。
2.4 大脑与连接:Arduino Uno的枢纽作用
整个系统的大脑是一块Arduino Uno R3。它拥有14个数字I/O口和6个模拟输入口,完全满足我们的需求:
- 数字输入:连接两个漫反射光电传感器(输出数字开关信号)和电感式接近传感器。
- 模拟输入:连接红外接近传感器,读取其模拟电压值。
- 数字输出:连接舵机信号线,输出PWM信号控制角度。
- 供电:传感器和舵机功耗较大,切记不要直接从Arduino的5V引脚取电!否则极易导致板子重启或损坏。正确的做法是使用一个独立的5V/2A以上的电源模块为舵机和传感器供电,并将此电源的“地”(GND)与Arduino的“地”连接在一起,确保共地。
3. 硬件搭建详解:从电路连接到机械结构
理论设计完成后,动手搭建是下一个挑战。这部分我会详细到接线颜色和螺丝尺寸,力求让你能照着做出来。
3.1 电路连接图与接线表
首先,将所有元件连接到Arduino Uno上。下图是接线示意图,下表是详细的引脚对应关系:
| 元件 | 引脚/线色 | 连接到 Arduino Uno 引脚 | 说明 |
|---|---|---|---|
| 红外接近传感器 (E18-D80NK) | 棕色线 (VCC) | 5V (外部电源) | 供电,务必接外部电源 |
| 蓝色线 (GND) | GND (外部电源 & Arduino) | 接地,共地很重要 | |
| 黑色线 (OUT) | A0 | 模拟信号输出,读取反射强度 | |
| 电感式接近传感器 (LJ12A3-4-Z/BX) | 棕色线 (VCC) | 5V (外部电源) | 供电 |
| 蓝色线 (GND) | GND | 接地 | |
| 黑色线 (OUT) | 2 | 数字信号输出,高电平有效(金属靠近时) | |
| 漫反射光电传感器1 (到位检测) | VCC (通常红色) | 5V (外部电源) | 供电 |
| GND (通常黑色) | GND | 接地 | |
| OUT (信号线,通常黄色) | 3 | 数字输入,低电平触发(物体遮挡时) | |
| 漫反射光电传感器2 (复位检测) | VCC | 5V (外部电源) | 供电 |
| GND | GND | 接地 | |
| OUT | 4 | 数字输入,低电平触发 | |
| 舵机 (MG996R) | 红色线 (VCC) | 5V (外部电源正极) | 重要:必须外接电源! |
| 棕色线/黑色线 (GND) | GND (外部电源负极) | 接地 | |
| 橙色线/白色线 (信号) | 9 | PWM信号控制引脚 | |
| 外部电源 | 正极 (+) | 所有元件的VCC引脚 | 提供充足电流 |
| 负极 (-) | 所有元件的GND引脚及Arduino GND | 实现共地 |
实操心得:接线时,强烈建议使用面包板先进行测试。将所有元件的GND端插到面包板的负电源轨,VCC端插到正电源轨,然后用杜邦线将电源轨引到外部电源。最后,用几根线将面包板电源轨的GND和VCC与Arduino的GND和外部电源对应连接。这样接线路清晰,排查故障也方便。一定要先确认电源极性再通电!
3.2 机械结构设计与制作要点
机械部分是项目的“骨架”,决定了运行的稳定性和流畅度。
- 传送带:可以使用现成的小型直流电机驱动的橡胶传送带模块,也可以自己DIY。我用的是木板做框架,两条平行的铝型材做导轨,用减速直流电机配合橡胶履带。关键是速度要慢且均匀,我通过PWM控制电机转速,最终调试出的速度大约是每秒5-8厘米,给传感器和舵机足够的反应时间。
- 传感器安装支架:用亚克力板或乐高积木搭建一个小门架,横跨在传送带上方。将红外传感器和电感式传感器并排固定在门架的横梁上,调整其高度,使传感器探头距离传送带上的瓶子顶部约1-2厘米。这个距离需要根据你选的传感器型号具体调试,确保检测稳定。
- 舵机挡板机构:这是核心动作部件。
- 材料:舵机、舵盘、一根长约15-20cm的硬质钢丝(如自行车辐条)或小铝杆、一块小塑料板(作为挡板)、热熔胶、扎带。
- 制作:将钢丝一端垂直固定在舵盘上(可以用舵盘自带的螺丝孔,或者打孔用螺丝固定)。另一端用热熔胶粘上塑料挡板。挡板接触瓶子的一面,务必粘贴一层3-5毫米厚的海绵或橡胶片,起到缓冲和防滑作用。
- 安装:将舵机用螺丝或扎带牢固地安装在传送带分流口的一侧。调整舵机的初始角度,使挡板处于收起状态,不干扰瓶子直行。当需要分拣时,舵机旋转,挡板伸出,将瓶子拨向侧面。侧面需要安装一个导向滑槽,让瓶子顺畅地滑入收集箱。
- 收集箱:在传送带末端(直行)和侧面滑槽出口放置篮子或箱子即可。
调试技巧:机械部分最难的是调试传感器位置和舵机动作时机。我的方法是:先不编程,手动推动瓶子经过传感器,用Arduino IDE的串口监视器查看各传感器的数值变化,确定瓶子触发每个传感器的精确位置。然后根据传送带速度,计算从“到位检测”传感器触发,到瓶子运动到挡板位置需要多长时间,这个时间就是编程中需要设置的“延时”。
4. 软件逻辑与Arduino编程实现
硬件就绪后,赋予它灵魂的就是代码。程序的核心是一个状态机,清晰地区分“等待”、“检测”、“执行”、“复位”这几个阶段。
4.1 主程序流程图与状态说明
整个控制逻辑可以用下面的状态图来描述,但请注意,在代码中我们通常用loop()函数和一系列if条件判断来实现这个状态迁移。
- 初始状态 (IDLE):系统上电,舵机归位(挡板收起),所有传感器初始化,程序等待第一个瓶子的到来。
- 触发检测 (TRIGGERED):当“到位检测”光电传感器被遮挡,程序进入此状态。记录触发时间,并开始连续读取红外和电感传感器的值。
- 判断决策 (DECISION):经过一个预设的延时(确保瓶子已运动到传感器正下方),对传感器的读数进行判断。逻辑如下:
- 如果
电感传感器 == HIGH(检测到金属),则标记为METAL。 - 否则,如果
红外传感器模拟值 > 塑料阈值,则标记为PLASTIC。 - 否则,标记为
UNKNOWN(可能是空档、或无法识别的物体)。
- 如果
- 执行动作 (ACTION):根据判断结果,控制舵机。
- 如果是
PLASTIC,舵机保持原位(或转动到直行通道)。 - 如果是
METAL,舵机转动到分拣角度,将瓶子拨向侧面。 - 动作完成后,进入“等待离开”状态。
- 如果是
- 复位等待 (RESET):当“复位检测”光电传感器被触发(瓶子离开),舵机归位,系统清空所有状态标志,回到初始状态 (IDLE),等待下一个瓶子。
4.2 核心代码段解析与参数校准
下面给出关键部分的代码和注释。完整代码会比较长,这里聚焦于逻辑核心。
#include <Servo.h> // 引入舵机库 // 引脚定义 const int irSensorPin = A0; // 红外传感器模拟引脚 const int inductiveSensorPin = 2; // 电感传感器数字引脚 const int startSensorPin = 3; // 到位检测光电传感器 const int resetSensorPin = 4; // 复位检测光电传感器 const int servoPin = 9; // 舵机信号引脚 // 阈值与状态变量 int plasticThreshold = 500; // 红外传感器判断为塑料的阈值,需校准 int metalThreshold = 300; // 红外传感器判断为金属的阈值(通常较低),需校准 Servo myServo; // 创建舵机对象 int servoHomePos = 90; // 舵机初始位置(直行) int servoSortPos = 135; // 舵机分拣位置(拨向侧面) enum SystemState { IDLE, TRIGGERED, ACTION, RESET_WAIT }; SystemState currentState = IDLE; unsigned long triggerTime = 0; const int sensorDelay = 300; // 从触发到检测的延时(ms),根据传送带速度调整 bool objectDetected = false; String materialType = "NONE"; void setup() { Serial.begin(9600); pinMode(inductiveSensorPin, INPUT); pinMode(startSensorPin, INPUT_PULLUP); // 使用内部上拉,传感器输出低电平有效 pinMode(resetSensorPin, INPUT_PULLUP); myServo.attach(servoPin); myServo.write(servoHomePos); // 初始化舵机位置 Serial.println("System Initialized. Waiting for bottle..."); } void loop() { switch (currentState) { case IDLE: // 检测是否有瓶子到达 if (digitalRead(startSensorPin) == LOW) { // 传感器被遮挡,输出低电平 currentState = TRIGGERED; triggerTime = millis(); // 记录触发时刻 Serial.println("Bottle detected! Entering TRIGGERED state."); } break; case TRIGGERED: // 等待瓶子运动到检测位 if (millis() - triggerTime > sensorDelay) { // 开始检测 int irValue = analogRead(irSensorPin); int inductiveValue = digitalRead(inductiveSensorPin); Serial.print("IR: "); Serial.print(irValue); Serial.print(" | Inductive: "); Serial.println(inductiveValue); // 决策逻辑 if (inductiveValue == HIGH) { materialType = "METAL"; Serial.println("Decision: METAL can."); } else if (irValue > plasticThreshold) { materialType = "PLASTIC"; Serial.println("Decision: PLASTIC bottle."); } else { materialType = "UNKNOWN"; Serial.println("Decision: UNKNOWN or no object."); } currentState = ACTION; // 进入执行状态 } break; case ACTION: // 根据材质类型执行动作 if (materialType == "METAL") { myServo.write(servoSortPos); // 转动舵机进行分拣 Serial.println("Servo activated for METAL."); } else { // 对于塑料或未知物体,舵机保持原位(或转到另一个角度) myServo.write(servoHomePos); // 确保在初始位置 Serial.println("Servo stays for PLASTIC/UNKNOWN."); } delay(100); // 给舵机一点时间动作 currentState = RESET_WAIT; // 等待瓶子离开 Serial.println("Waiting for bottle to leave..."); break; case RESET_WAIT: // 检测瓶子是否已离开分拣区 if (digitalRead(resetSensorPin) == LOW) { myServo.write(servoHomePos); // 舵机归位 materialType = "NONE"; currentState = IDLE; // 回到空闲状态,等待下一个 Serial.println("Bottle left. System RESET to IDLE."); } break; } // 一个小延时,防止循环过快 delay(10); }关键参数校准步骤(必须在硬件搭建好后进行):
- 红外阈值校准:分别将一个干净的塑料瓶和金属罐放在传感器正下方,打开串口监视器,记录下
irValue的读数。你会看到塑料瓶的读数远高于金属罐。取一个介于两者之间的值作为plasticThreshold。例如,塑料瓶读数800,金属罐读数200,那么阈值可以设为500。 - 电感传感器测试:让金属罐靠近,串口监视器应显示
Inductive: 1(HIGH);塑料瓶靠近则显示Inductive: 0(LOW)。 - 延时参数
sensorDelay校准:手动推动瓶子,用秒表或millis()函数计算从触发“到位检测”传感器到瓶子正好运动到红外/电感传感器下方所需的时间(毫秒)。这个值需要反复测试调整。 - 舵机角度校准:通过
servoHomePos和servoSortPos两个参数控制。你需要手动测试,找到能让挡板完全收起和完全伸出到位的角度值。使用myServo.write()函数配合串口输入进行测试非常方便。
5. 系统调试、优化与避坑指南
把代码烧录进去,硬件接好,通电,但很可能它第一次不会按你预想的那样工作。调试是项目中最耗时但也最能学到东西的环节。
5.1 常见故障排查清单
当你发现机器人不工作时,可以按照以下清单逐项检查:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无反应,舵机不转,传感器灯不亮 | 1. 主电源未接通或损坏。 2. Arduino未供电或USB线松动。 3. 电源共地未连接。 | 1. 检查外部电源适配器是否插好,用万用表测量输出电压是否为5V。 2. 检查Arduino板上的电源指示灯是否亮起。 3.重点检查:外部电源的GND是否用导线连接到了Arduino的任何一个GND引脚。 |
| 传感器有反应,但舵机不动作或乱动 | 1. 舵机供电不足(最常见)。 2. 舵机信号线接错引脚。 3. 舵机损坏。 | 1.确保舵机VCC接的是外部电源,不是Arduino的5V!MG996R堵转电流可达1.5A以上,Arduino板载稳压器根本扛不住。 2. 检查信号线是否接在了支持PWM的引脚(如9, 10, 11)。 3. 单独编写一个简单的舵机摆动测试程序,排除代码问题。 |
| 红外传感器读数不稳定,忽高忽低 | 1. 环境光干扰。 2. 检测距离不合适。 3. 传感器供电不稳。 | 1. 给传感器加装遮光筒(用黑色热缩管或电工胶带卷成筒状)。 2. 调整传感器高度,使其距离瓶盖1-2cm。 3. 确保电源能提供稳定5V电压,线材接触良好。 |
| 电感传感器一直触发或不触发 | 1. 检测距离太远或太近。 2. 传感器类型选错(NPN/PNP)。我推荐的是NPN常开型,金属靠近输出高电平。 | 1. 铝罐较薄,检测距离要近,确保罐身距离传感器探头在4mm以内。 2. 查阅传感器说明书,确认其输出逻辑。可以用万用表测量OUT脚和GND之间的电压,金属靠近时电压应从0V跳变到接近电源电压。 |
| 瓶子经常被卡住或拨飞 | 1. 舵机挡板动作时机不对。 2. 挡板形状或缓冲材料不佳。 3. 传送带速度过快。 | 1. 精细调整sensorDelay参数,确保挡板在瓶子到达的瞬间动作。2. 将挡板头部做成弧形,并粘贴厚海绵。确保滑槽入口光滑、角度合适。 3. 降低传送带电机速度,给机械动作留出足够时间。 |
| 程序逻辑混乱,状态切换错误 | 1. 传感器信号抖动(抖动)。 2. 延时计算不准。 3. 状态机逻辑有漏洞。 | 1. 在读取数字传感器(如光电开关)时,加入软件消抖。例如连续读取几次,结果一致才确认。 |
| 串口打印正常,但实际不动作 | 1. 可能进入了某个错误状态没跳出来。 2. 执行动作的代码块因为条件判断错误未能执行。 | 1. 在loop()中每个case里都打印当前状态,观察状态迁移是否正常。2. 检查所有 if判断条件,特别是==和=不要写错。 |
5.2 软件消抖与状态机健壮性优化
上面的基础代码在理想情况下可行,但现实中传感器信号会有毛刺。我们需要增强软件的鲁棒性。
为数字输入添加消抖:
bool debounceRead(int pin) { int count = 0; for (int i = 0; i < 10; i++) { // 快速采样10次 if (digitalRead(pin) == LOW) { // 假设低电平有效 count++; } delay(1); // 短暂延时 } // 如果10次中有7次以上为低电平,则认为有效触发 return (count > 7); }然后在IDLE状态的判断中,使用if (debounceRead(startSensorPin))来代替直接的digitalRead。
防止状态机卡死:在RESET_WAIT状态,如果复位传感器意外损坏或瓶子没触发它,系统就会永远卡住。可以增加一个超时机制:
case RESET_WAIT: unsigned long waitStartTime = millis(); const int resetTimeout = 5000; // 等待5秒超时 while (digitalRead(resetSensorPin) != LOW) { if (millis() - waitStartTime > resetTimeout) { Serial.println("Reset timeout! Force returning to IDLE."); myServo.write(servoHomePos); materialType = "NONE"; currentState = IDLE; break; // 跳出while循环和switch case } delay(50); } // ... 正常复位逻辑 ... break;5.3 性能提升与扩展思路
当基础版本稳定运行后,你可以考虑以下优化和扩展:
- 增加分拣类别:要区分透明塑料瓶和不透明塑料瓶?可以增加一个颜色传感器(如TCS3200),在红外判断为塑料后,再进行颜色判断。要识别玻璃瓶?这非常困难,需要更复杂的传感器(如电容感应或重量检测),或者直接引入廉价的USB摄像头配合OpenCV进行图像识别,但这会将项目复杂度提升一个数量级。
- 提高处理速度:当前代码使用
delay(),在延时期间单片机什么也做不了。可以使用非阻塞定时,利用millis()来管理时间,让系统在等待期间也能响应其他传感器信号(虽然本项目单线程足够,但这是好的编程习惯)。 - 添加人机交互:增加一个按钮和一个LCD屏幕。按钮用于手动切换模式(如学习模式、运行模式),LCD屏幕可以实时显示分拣计数(塑料瓶xx个,金属罐xx个),甚至显示当前传感器的读数,方便调试。
- 数据上传与监控:加一个ESP8266或ESP32模块,让Arduino具备Wi-Fi功能。可以将分拣数量、系统状态(正常/故障)通过MQTT协议发送到手机APP或云平台,实现远程监控。
- 机械结构强化:用3D打印设计更专业的传感器支架、挡板部件和滑槽,使整体结构更稳固、美观。使用同步带和步进电机替代普通的直流电机,可以实现更精确的传送带控制,甚至能让传送带“走停走停”,让瓶子在检测和分拣工位完全停下来,进一步提高精度。
这个项目最吸引我的地方在于,它像一个微缩的工业自动化产线,涵盖了信号采集、逻辑处理、运动控制这些核心概念。从最初几个传感器胡乱接线,到后来能稳定地将可乐罐和矿泉水瓶分开,中间每一次调试成功都带来巨大的成就感。硬件项目就是这样,理论和代码只是蓝图,真正的智慧都藏在那些拧螺丝、调参数、反复测试的细节里。希望这份超详细的指南,能帮你绕过我踩过的那些坑,顺利搭建出属于你自己的那个“捡瓶子”的机器人。