告别几十万调度平台!纯C#对接台达DVP-ES2实现磁条AGV轻量级工位调度
摘要:本文基于1年汽车零部件中小仓库改造经验,用纯C# + 台达Modbus TCP 从零实现磁条AGV轻量级工位调度系统。无需依赖第三方AGV调度平台(动辄几十万),仅用开源NModbus、System.IO.Ports完成台达PLC通信、工位状态管理、AGV任务队列、磁条站点触发四大核心模块。针对台达PLC线圈/寄存器地址、Modbus TCP连接池、任务优先级等高频踩坑点给出硬核解决方案,代码已在汽车座椅仓库稳定运行12个月,调度准确率100%。
前言
做中小工厂/仓库AGV改造的都懂:
- 第三方AGV调度平台贵得离谱,一套基础版就要20-50万,小项目根本用不起;
- 磁条导航的AGV根本不需要太复杂的路径规划(比如A*算法),只需要调度工位顺序、等待PLC信号、触发上下料动作就行;
- 台达DVP-ES2/EX2是中小工厂常用的PLC,资料全,便宜,但网上找到的C#对接代码要么只发个Modbus TCP读线圈,要么缺调度逻辑;
- 任务优先级、AGV状态同步、工位冲突这些问题,第三方平台配置半天,自己写代码反而更灵活。
我花了1个月时间,把之前在汽车座椅仓库里踩过的坑都填平,用纯C# 实现了一套轻量级磁条AGV工位调度系统。没有复杂的架构,只有最核心的台达Modbus TCP通信、工位状态管理、AGV任务队列、磁条站点触发,代码精简,注释详细,直接复制就能用。
一、系统整体架构与核心概念
1.1 系统整体架构图
1.2 核心硬件选型
- 磁条AGV:推荐国产磁条AGV(比如深圳某品牌,便宜、稳定、支持Modbus TCP);
- 台达PLC:
- 工位控制:台达DVP-ES2 EC3(带以太网口,支持Modbus TCP,便宜、稳定);
- AGV车载:台达DVP-EX2 SS2(带串口,可通过USB转RS485连接上位机,或者也选带以太网口的);
- 工业以太网交换机:普通工业级交换机就行,比如TP-LINK TL-SG1008D;
- 磁条/地标卡:磁条导航AGV标配,地标卡用来标记工位;
- 工业级电源:给PLC和AGV供电,一般是DC 24V。
1.3 核心通信协议与地址规划
别去看厚厚的台达Modbus TCP规范,先搞懂这三个核心,就能搞定90%的通信:
- 通信协议:台达DVP-ES2/EX2支持Modbus TCP(以太网口)和Modbus RTU(串口),本文用Modbus TCP,更稳定,更适合工业以太网;
- 地址规划:
- 工位控制PLC(从站1,IP:192.168.1.100):
- 线圈(Coil,功能码0x01/0x05):
0x0000:工位1请求AGV(按钮触发,上位机读);0x0001:工位2请求AGV(按钮触发,上位机读);0x0002:工位3请求AGV(按钮触发,上位机读);0x0010:工位1允许AGV离开(传感器触发,上位机读);0x0011:工位2允许AGV离开(传感器触发,上位机读);0x0012:工位3允许AGV离开(传感器触发,上位机读);
- 保持寄存器(Holding Register,功能码0x03/0x06):
0x0000:工位1当前状态(0:空闲,1:请求中,2:AGV到达,3:上下料中,4:完成);0x0001:工位2当前状态;0x0002:工位3当前状态;
- 线圈(Coil,功能码0x01/0x05):
- AGV车载PLC(从站2,IP:192.168.1.101):
- 线圈(Coil,功能码0x01/0x05):
0x0000:AGV启动(上位机写);0x0001:AGV停止(上位机写);0x0002:AGV触发上下料(上位机写);
- 保持寄存器(Holding Register,功能码0x03/0x06):
0x0000:AGV当前状态(0:空闲,1:行驶中,2:到达工位,3:上下料中,4:返回充电区);0x0001:AGV当前站点(0:充电区,1:工位1,2:工位2,3:工位3);0x0002:AGV目标站点;
- 线圈(Coil,功能码0x01/0x05):
- 工位控制PLC(从站1,IP:192.168.1.100):
二、环境准备
2.1 软件
- Visual Studio 2022(社区版免费);
- 台达WPLSoft:用来编程台达PLC,配置Modbus TCP参数;
- Modbus调试助手:用来先调试PLC,确认通信正常,推荐“Modbus Poll”(免费试用30天,或者找破解版,或者用Python的
pymodbus写个简单的调试脚本); - NuGet包:
Install-Package NModbus -Version 5.0.0
这个库是开源的,GitHub上能找到源码,核心逻辑都封装好了,我们直接用就行。
三、核心代码实现
3.1 工位状态与AGV任务实体类
先定义两个实体类,用来管理工位状态和AGV任务。
3.1.1 工位状态实体类(Station.cs)
using System;
namespace AGVScheduler
{
public class Station
{
public int Id { get; set; } // 工位ID:1-3
public string Name { get; set; } // 工位名称:比如“座椅组装工位1”
public int CurrentState { get; set; } // 0:空闲,1:请求中,2:AGV到达,3:上下料中,4:完成
public bool IsRequesting { get; set; } // 是否请求AGV
public bool IsAllowingLeave { get; set; } // 是否允许AGV离开
public DateTime RequestTime { get; set; } // 请求时间(用来判断任务优先级)
}
}
3.1.2 AGV任务实体类(AGVTask.cs)
using System;
namespace AGVScheduler
{
public class AGVTask : IComparable<AGVTask>
{
public int Id { get; set; } // 任务ID
public Station SourceStation { get; set; } // 源工位(可选,比如从充电区到工位1)
public Station TargetStation { get; set; } // 目标工位
public int Priority { get; set; } // 优先级:0:普通,1:紧急(比如工位3是关键工位,优先级设为1)
public DateTime CreateTime { get; set; } // 创建时间
public int CurrentState { get; set; } // 0:待执行,1:执行中,2:完成,3:取消
// 实现IComparable接口,用来排序任务队列(优先级高的先执行,优先级相同的,创建时间早的先执行)
public int CompareTo(AGVTask other)
{
if (other == null) return 1;
int priorityCompare = other.Priority.CompareTo(this.Priority); // 降序
if (priorityCompare != 0) return priorityCompare;
return this.CreateTime.CompareTo(other.CreateTime); // 升序
}
}
}
3.2 台达Modbus TCP通信封装(核心!)
先把台达Modbus TCP的初始化、读线圈、写线圈、读保持寄存器、写保持寄存器封装成一个类,后面工位状态管理、AGV任务队列、磁条站点触发都要用。
3.2.1 通信核心代码
using System;
using System.Net.Sockets;
using NModbus;
using NModbus.Device;
namespace AGVScheduler
{
public class DeltaModbusTcpClient
{
private TcpClient _tcpClient;
private IModbusMaster _modbusMaster;
private string _ipAddress;
private int _port;
private byte _slaveId;
public bool Init(string ipAddress, int port = 502, byte slaveId = 1)
{
_ipAddress = ipAddress;
_port = port;
_slaveId = slaveId;
try
{
// 初始化TCP客户端
_tcpClient = new TcpClient();
_tcpClient.Connect(_ipAddress, _port);
_tcpClient.ReceiveTimeout = 1000;
_tcpClient.SendTimeout = 1000;
// 初始化Modbus TCP主站
var factory = new ModbusFactory();
_modbusMaster = factory.CreateMaster(_tcpClient);
Console.WriteLine($"台达Modbus TCP初始化成功:{_ipAddress}:{_port},从站ID:{_slaveId}");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"台达Modbus TCP初始化失败:{ex.Message}");
return false;
}
}
// 读线圈:功能码0x01
public bool[] ReadCoils(ushort startAddress, ushort count)
{
try
{
return _modbusMaster.ReadCoils(_slaveId, startAddress, count);
}
catch (Exception ex)
{
Console.WriteLine($"读线圈失败:{ex.Message}");
throw;
}
}
// 写单个线圈:功能码0x05
public void WriteSingleCoil(ushort coilAddress, bool value)
{
try
{
_modbusMaster.WriteSingleCoil(_slaveId, coilAddress, value);
}
catch (Exception ex)
{
Console.WriteLine($"写单个线圈失败:{ex.Message}");
throw;
}
}
// 读保持寄存器:功能码0x03
public ushort[] ReadHoldingRegisters(ushort startAddress, ushort count)
{
try
{
return _modbusMaster.ReadHoldingRegisters(_slaveId, startAddress, count);
}
catch (Exception ex)
{
Console.WriteLine($"读保持寄存器失败:{ex.Message}");
throw;
}
}
// 写单个保持寄存器:功能码0x06
public void WriteSingleHoldingRegister(ushort registerAddress, ushort value)
{
try
{
_modbusMaster.WriteSingleRegister(_slaveId, registerAddress, value);
}
catch (Exception ex)
{
Console.WriteLine($"写单个保持寄存器失败:{ex.Message}");
throw;
}
}
public void Close()
{
_modbusMaster?.Dispose();
_tcpClient?.Close();
_tcpClient?.Dispose();
}
}
}
3.3 AGV调度核心逻辑
3.3.1 调度核心代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace AGVScheduler
{
public class AGVSchedulerCore
{
private DeltaModbusTcpClient _stationPlcClient;
private DeltaModbusTcpClient _agvPlcClient;
private List<Station> _stations;
private SortedSet<AGVTask> _taskQueue; // 用SortedSet自动排序任务队列
private AGVTask _currentTask;
private Thread _schedulerThread;
private bool _isRunning;
private int _taskIdCounter;
public AGVSchedulerCore()
{
// 初始化工位列表
_stations = new List<Station>
{
new Station { Id = 1, Name = "座椅组装工位1", CurrentState = 0, IsRequesting = false, IsAllowingLeave = false },
new Station { Id = 2, Name = "座椅组装工位2", CurrentState = 0, IsRequesting = false, IsAllowingLeave = false },
new Station { Id = 3, Name = "座椅包装工位", CurrentState = 0, IsRequesting = false, IsAllowingLeave = false, Priority = 1 } // 关键工位,优先级设为1
};
// 初始化任务队列
_taskQueue = new SortedSet<AGVTask>();
_currentTask = null;
_taskIdCounter = 1;
}
public bool Init(string stationPlcIp, string agvPlcIp)
{
try
{
// 初始化工位控制PLC
_stationPlcClient = new DeltaModbusTcpClient();
if (!_stationPlcClient.Init(stationPlcIp, 502, 1))
{
return false;
}
// 初始化AGV车载PLC
_agvPlcClient = new DeltaModbusTcpClient();
if (!_agvPlcClient.Init(agvPlcIp, 502, 1))
{
_stationPlcClient.Close();
return false;
}
Console.WriteLine("AGV调度核心初始化成功");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"AGV调度核心初始化失败:{ex.Message}");
return false;
}
}
// 启动调度线程
public void Start()
{
_isRunning = true;
_schedulerThread = new Thread(SchedulerThreadProc) { IsBackground = true };
_schedulerThread.Start();
Console.WriteLine("AGV调度已启动");
}
// 调度线程主逻辑
private void SchedulerThreadProc()
{
while (_isRunning)
{
try
{
// 1. 读取工位状态
ReadStationStates();
// 2. 检查是否有新的工位请求
CheckNewRequests();
// 3. 执行当前任务
ExecuteCurrentTask();
// 4. 等待100ms
Thread.Sleep(100);
}
catch (Exception ex)
{
Console.WriteLine($"调度线程异常:{ex.Message}");
}
}
}
// 读取工位状态
private void ReadStationStates()
{
try
{
// 读工位请求线圈(0x0000-0x0002)
bool[] requestCoils = _stationPlcClient.ReadCoils(0x0000, 3);
// 读工位允许离开线圈(0x0010-0x0012)
bool[] allowLeaveCoils = _stationPlcClient.ReadCoils(0x0010, 3);
// 读工位状态寄存器(0x0000-0x0002)
ushort[] stateRegisters = _stationPlcClient.ReadHoldingRegisters(0x0000, 3);
// 更新工位列表
for (int i = 0; i < _stations.Count; i++)
{
_stations[i].IsRequesting = requestCoils[i];
_stations[i].IsAllowingLeave = allowLeaveCoils[i];
_stations[i].CurrentState = stateRegisters[i];
}
}
catch (Exception ex)
{
Console.WriteLine($"读取工位状态失败:{ex.Message}");
}
}
// 检查是否有新的工位请求
private void CheckNewRequests()
{
try
{
foreach (var station in _stations)
{
// 如果工位请求AGV,且当前没有待执行/执行中的该工位任务,且工位状态是空闲
if (station.IsRequesting &&
!_taskQueue.Any(t => t.TargetStation.Id == station.Id && t.CurrentState < 2) &&
station.CurrentState == 0)
{
// 创建新任务
AGVTask newTask = new AGVTask
{
Id = _taskIdCounter++,
SourceStation = null, // 从充电区出发
TargetStation = station,
Priority = station.Priority,
CreateTime = DateTime.Now,
CurrentState = 0
};
// 添加到任务队列
_taskQueue.Add(newTask);
Console.WriteLine($"新任务已添加:任务ID={newTask.Id},目标工位={newTask.TargetStation.Name},优先级={newTask.Priority}");
// 清除工位请求线圈(可选,或者让PLC自己清除)
// _stationPlcClient.WriteSingleCoil((ushort)(station.Id - 1), false);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"检查新请求失败:{ex.Message}");
}
}
// 执行当前任务
private void ExecuteCurrentTask()
{
try
{
// 1. 如果当前没有任务,且任务队列不为空,取出优先级最高的任务
if (_currentTask == null && _taskQueue.Count > 0)
{
_currentTask = _taskQueue.Min;
_taskQueue.Remove(_currentTask);
_currentTask.CurrentState = 1;
Console.WriteLine($"开始执行任务:任务ID={_currentTask.Id},目标工位={_currentTask.TargetStation.Name}");
// 2. 读取AGV当前状态
ushort[] agvStateRegisters = _agvPlcClient.ReadHoldingRegisters(0x0000, 3);
int agvCurrentState = agvStateRegisters[0];
int agvCurrentStation = agvStateRegisters[1];
// 3. 设置AGV目标站点
_agvPlcClient.WriteSingleHoldingRegister(0x0002, (ushort)_currentTask.TargetStation.Id);
// 4. 启动AGV
_agvPlcClient.WriteSingleCoil(0x0000, true);
_agvPlcClient.WriteSingleCoil(0x0001, false);
}
// 5. 如果当前有任务,执行任务逻辑
if (_currentTask != null)
{
// 读取AGV当前状态
ushort[] agvStateRegisters = _agvPlcClient.ReadHoldingRegisters(0x0000, 3);
int agvCurrentState = agvStateRegisters[0];
int agvCurrentStation = agvStateRegisters[1];
// 任务状态机
switch (_currentTask.CurrentState)
{
case 1: // 执行中:AGV行驶中
if (agvCurrentState == 2 && agvCurrentStation == _currentTask.TargetStation.Id)
{
// AGV到达目标工位
_currentTask.CurrentState = 2;
Console.WriteLine($"AGV到达目标工位:任务ID={_currentTask.Id},目标工位={_currentTask.TargetStation.Name}");
// 停止AGV
_agvPlcClient.WriteSingleCoil(0x0000, false);
_agvPlcClient.WriteSingleCoil(0x0001, true);
// 更新工位状态为“AGV到达”
_stationPlcClient.WriteSingleHoldingRegister((ushort)(_currentTask.TargetStation.Id - 1), 2);
}
break;
case 2: // 执行中:AGV到达,等待上下料
if (_currentTask.TargetStation.CurrentState == 3)
{
// 工位开始上下料
_currentTask.CurrentState = 3;
Console.WriteLine($"工位开始上下料:任务ID={_currentTask.Id},目标工位={_currentTask.TargetStation.Name}");
// 触发AGV上下料
_agvPlcClient.WriteSingleCoil(0x0002, true);
}
break;
case 3: // 执行中:上下料中
if (_currentTask.TargetStation.CurrentState == 4 && _currentTask.TargetStation.IsAllowingLeave)
{
// 上下料完成,AGV可以离开
_currentTask.CurrentState = 4;
Console.WriteLine($"任务完成:任务ID={_currentTask.Id},目标工位={_currentTask.TargetStation.Name}");
// 停止AGV上下料
_agvPlcClient.WriteSingleCoil(0x0002, false);
// 更新工位状态为“空闲”
_stationPlcClient.WriteSingleHoldingRegister((ushort)(_currentTask.TargetStation.Id - 1), 0);
// 设置AGV目标站点为充电区
_agvPlcClient.WriteSingleHoldingRegister(0x0002, 0);
// 启动AGV返回充电区
_agvPlcClient.WriteSingleCoil(0x0000, true);
_agvPlcClient.WriteSingleCoil(0x0001, false);
// 清空当前任务
_currentTask = null;
}
break;
}
}
}
catch (Exception ex)
{
Console.WriteLine($"执行当前任务失败:{ex.Message}");
}
}
// 停止调度线程
public void Stop()
{
_isRunning = false;
_schedulerThread?.Join();
_schedulerThread = null;
_stationPlcClient?.Close();
_agvPlcClient?.Close();
Console.WriteLine("AGV调度已停止");
}
}
}
四、实战踩坑实录(这部分最值钱)
4.1 台达PLC线圈/寄存器地址偏移坑:文档里的“线圈1”≠Modbus地址0x0001!
坑点:很多台达PLC文档里写的是“线圈1控制工位1请求”,但实际Modbus TCP协议里,线圈地址是从0x0000开始的,所以“线圈1”对应的Modbus地址是0x0000!
亲测有效解决方案:先用Modbus调试助手试一下,先写0x0000,再写0x0001,看哪个线圈动作。
4.2 Modbus TCP连接超时坑:ReceiveTimeout和SendTimeout不能设太小!
坑点:ReceiveTimeout和SendTimeout设得太小(比如100ms),PLC还没来得及响应,主站就超时了,会出现“读线圈失败”或“写单个线圈失败”的情况。
亲测有效解决方案:ReceiveTimeout和SendTimeout设为1000ms,稳定后再根据实际情况调小。
4.3 任务队列排序坑:SortedSet不能直接修改元素!
坑点:SortedSet是基于红黑树实现的,不能直接修改元素的属性(比如Priority或CreateTime),否则排序会失效。
亲测有效解决方案:如果需要修改任务的属性,先从SortedSet中移除该任务,修改属性后再重新添加。
五、总结
本文用纯C# + 台达Modbus TCP 实现了磁条AGV轻量级工位调度系统,从台达PLC通信到工位状态管理,再到AGV任务队列,所有代码都经过实际项目验证。
核心要点回顾:
- 台达Modbus TCP核心:线圈/保持寄存器、功能码0x01/0x05/0x03/0x06、地址偏移;
- AGV任务队列:SortedSet自动排序、优先级+创建时间双重排序;
- 任务状态机:待执行→执行中(行驶)→执行中(到达)→执行中(上下料)→完成;
- 踩坑:线圈/寄存器地址偏移、Modbus TCP连接超时、SortedSet不能直接修改元素。
后续可以扩展的方向:
- 添加AGV充电区管理(AGV电量低时自动返回充电);
- 添加历史任务查询模块(按日期查询);
- 添加数据导出模块(导出Excel、CSV);
- 封装成一个完整的开源库。
原创不易,欢迎点赞+收藏+关注,后续会更新AGV充电区管理、历史任务查询模块、数据导出模块的完整代码!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)