跳到主要内容

中速物流分拣线

硬件配置

  • 主站: Windows 10 × 1 + Intel i5 × 1
  • 耦合器: EK1100 × 1
  • 高速计数器: EL5001 × 1
  • 数字量输出: EL2008 × 1 (分拣挡板控制)
  • 数字量输入: EL1008 × 1 (物品检测传感器)
  • 传送带: 变频器 × 1 + 增量式编码器 × 1
  • 识别系统: 工业相机 × 1 (依据需求选型: 扫码相机、线阵相机、3D相机)

成本分析

类别型号/规格参考单价 (¥)数量小计 (¥)
耦合器EK11008001800
高速计数器EL50011,20011,200
数字量输出EL20084001400
数字量输入EL10083501350
变频器通用型 0.75~1.5kW1,00011,000
增量式编码器1000~2000 线1001100
工业相机扫码相机/其他相机2,000~8,00012,000~8,000
工控机Intel i5 级别3,00013,000
整线参考总计≈ ¥8,850~14,850

性能指标

  • 输送速度: 1~2 m/s (中速)
  • 控制周期: 1 ms
  • 分拣通道: 8 路

应用场景

低成本中小型物流仓库分拣线,性价比首选,常用于规则物料分拣。
输送速度 1~2 m/s。编码器实时追踪传送带位置,视觉识别包裹条码/外观后按类别触发分拣挡板,完成自动分流。
也可用于产线质检剔除场景,视觉检测不合格品后触发 IO 剔除。

代码示例

PDO 结构体定义

using System.Runtime.InteropServices;

/// <summary>
/// EL5001 高速计数器输入 PDO
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct EL5001_Input
{
public byte Status; // 状态字节
public int CounterValue; // 32位计数值
public int LatchValue; // 锁存值
}

/// <summary>
/// EL1008 数字量输入 PDO (8路输入)
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct EL1008_Input
{
public byte DigitalInputs; // 8位数字输入状态
}

/// <summary>
/// EL2008 数字量输出 PDO (8路输出)
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct EL2008_Output
{
public byte DigitalOutputs; // 8位数字输出控制
}

控制代码

using System;
using System.Threading;
using System.Collections.Generic;
using System.Collections.Concurrent;
using DarraEtherCAT_Master;

class SortingController
{
// 从站索引 (根据实际拓扑调整)
const int SLAVE_EL5001 = 1; // 高速计数器
const int SLAVE_EL2008 = 2; // 数字量输出
const int SLAVE_EL1008 = 3; // 数字量输入

// ============ 可配置参数 (外部设置) ============

/// <summary> 每个剔除口到检测点的编码器脉冲距离 (根据实际机械安装位置标定) </summary>
public static int[] CylinderDistance { get; set; } = new int[]
{
3000, 5500, 8000, 10500, 13000, 15500, 18000, 20500
};

/// <summary> 每个剔除口气缸的输出保持时间 (ms) </summary>
public static int[] CylinderHoldMs { get; set; } = new int[]
{
200, 200, 200, 200, 250, 250, 300, 300
};

// ============ 任务数据结构 ============

struct SortTask
{
public int TriggerPos; // 到达此编码器位置时触发气缸
public int Channel; // 目标剔除口 (0-7)
}

struct ActiveCylinder
{
public int Channel; // 通道
public DateTime OffTime; // 超过此时刻收回气缸
}

// ============ 三级队列 ============
//
// 视觉线程 ──EnqueueTask()──→ [_inbox] (线程安全, 仅入队)
// │
// 主循环每周期取出 ───────────→ [_pending] (在途物料, 等待到达触发位置)
// │
// 编码器到位 ────────────────→ [_active] (气缸已伸出, 等待保持时间结束)
// │
// 保持超时 ──────────────────→ 清除, 气缸收回

// 1. 入队缓冲 (视觉线程写, 主循环读)
static readonly ConcurrentQueue<SortTask> _inbox = new ConcurrentQueue<SortTask>();

// 2. 在途物料列表 (多个物料同时在输送带上, 各自等待到达不同剔除口)
static readonly List<SortTask> _pending = new List<SortTask>();

// 3. 已激活气缸列表 (多个气缸可同时动作)
static readonly List<ActiveCylinder> _active = new List<ActiveCylinder>();

// 当前编码器位置 (主循环实时更新, 外部线程可读)
public static volatile int CurrentEncoderPosition;

/// <summary>
/// 外部视觉/条码系统调用: 投递剔除任务
/// 自动读取当前编码器位置, 加上目标剔除口距离后存入队列
/// </summary>
/// <param name="channel">目标剔除口 (0-7)</param>
public static void EnqueueTask(int channel)
{
_inbox.Enqueue(new SortTask
{
TriggerPos = CurrentEncoderPosition + CylinderDistance[channel],
Channel = channel
});
}

static void Main(string[] args)
{
var master = new DarraEtherCAT()
.LoadENI("config/smart_logistics.xml")
.Build();

if (master == null)
{
Console.WriteLine("主站初始化失败!");
return;
}

try
{
var (success, msg) = master.SetState(EcState.OP);
if (!success)
{
Console.WriteLine($"无法进入 OP 状态: {msg}");
return;
}

Console.WriteLine("物流分拣线启动!");

// 传感器边沿检测 (防止信号持续为高时重复入队)
bool lastSensorState = false;

// 一次性映射 PDO (引用指向共享内存, 数据由主站循环自动更新)
ref var counter = ref master.Slaves[SLAVE_EL5001].InputsMapping<EL5001_Input>();
ref var input = ref master.Slaves[SLAVE_EL1008].InputsMapping<EL1008_Input>();
ref var output = ref master.Slaves[SLAVE_EL2008].OutputsMapping<EL2008_Output>();

while (true)
{
var now = DateTime.Now;

// 1. 读取编码器位置
CurrentEncoderPosition = counter.CounterValue;

// 2. 物品检测传感器 — 上升沿触发
bool sensorHigh = (input.DigitalInputs & 0x01) != 0;
if (sensorHigh && !lastSensorState)
{
// 检测到新物料, 此时视觉系统应调用 EnqueueTask(channel)
// 如果不接视觉, 可在此直接入队默认通道:
// EnqueueTask(0);
}
lastSensorState = sensorHigh;

// 3. 从 inbox 取出新任务 → 加入 pending (在途物料列表)
while (_inbox.TryDequeue(out var task))
_pending.Add(task);

// 4. 扫描在途物料: 编码器到达触发位置 → 激活气缸
for (int i = _pending.Count - 1; i >= 0; i--)
{
if (CurrentEncoderPosition >= _pending[i].TriggerPos)
{
_active.Add(new ActiveCylinder
{
Channel = _pending[i].Channel,
OffTime = now.AddMilliseconds(CylinderHoldMs[_pending[i].Channel])
});
_pending.RemoveAt(i);
}
}

// 5. 合成输出位: 所有已激活气缸 OR 在一起, 超时的移除
byte outputBits = 0;
for (int i = _active.Count - 1; i >= 0; i--)
{
if (now < _active[i].OffTime)
outputBits |= (byte)(1 << _active[i].Channel);
else
_active.RemoveAt(i); // 保持时间到, 气缸收回
}

output.DigitalOutputs = outputBits;

Thread.Sleep(0);
}
}
finally
{
master.Dispose();
}
}
}