简介本资源为台达PLC Modbus通信上位机64位C#实例源码面向工控上位机开发新手及有一定经验的程序员帮助读者理解并实现PC端与台达PLC之间的Modbus协议通信。源码基于VS2010亲测可用包含完整的解决方案与工程文件可直接编译运行并作为项目模板参考。压缩包共52个文件约542KB以cs源码、resx资源、exe可执行文件、dll类库、csproj工程与sln解决方案为主另含配置文件、说明文档及少量图片资源目录结构清晰便于按模块阅读与调试。目前已有615人学习下载适合需要快速上手台达PLC通信、研究Modbus报文收发与串口参数配置的开发者可从中掌握通信流程、异常处理与界面交互的完整实现思路。1. 台达PLC Modbus上位机源码64位C#工程能直接跑通什么车间里一台台达DVP系列PLC用RS485接了个USB转串口模块上位机要读D寄存器、写M线圈还要在64位Windows上跑——这个场景我见过太多次。网上能找到的Modbus上位机源码十份里有八份是32位工程或者只给了个DLL没给调用示例拿过来编译就报“试图加载格式不正确的程序”。这份台达PLC Modbus通信上位机64位C#实例源码解决的就是这个具体问题它是一套完整的C# WinForms工程目标框架锁定64位内置Modbus RTU和Modbus TCP两种通信方式的实现针对台达PLC的寄存器地址映射做了适配。适合谁做产线数据采集的工控上位机开发者、需要快速搭一个能读台达PLC数据的原型验证工具的人、以及被32位/64位编译问题卡过的C#工控新手。源码里通信层和界面层是分开的你不需要把整个工程拆了重写改几个参数就能接自己的PLC。2. 通信层拆解Modbus RTU与TCP在C#里怎么落地2.1 为什么选Modbus而不是台达私有协议台达PLC本身支持多种通信协议包括自家的ISPSoft协议和标准Modbus。选Modbus的理由很直接通用。你换一台汇川、信捷或者三菱的PLC只要它支持Modbus通信层代码几乎不用动。这份源码用的是Modbus RTU over串口和Modbus TCP over以太网两种模式覆盖了车间里最常见的两种物理连接方式。RTU走RS485适合距离远、干扰大的场景TCP走网口适合设备自带以太网口或者用了串口服务器的情况。源码里通信层的核心是一个ModbusMaster类它把功能码封装成了方法。读保持寄存器用功能码03写单个寄存器用06写多个寄存器用10十六进制读线圈用01写线圈用05。这些功能码在Modbus协议里是固定的你不需要记但要知道源码里哪个方法对应哪个功能码改的时候才不会找错地方。2.2 串口参数配置与RTU帧组装串口通信最容易翻车的地方就是参数对不上。台达PLC默认的通信参数通常是9600波特率、8数据位、1停止位、无校验但现场经常被改过。源码里串口初始化部分是这样写的// 串口初始化参数从配置文件读取避免硬编码 private SerialPort _serialPort; public void InitSerialPort(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits) { _serialPort new SerialPort(portName) { BaudRate baudRate, // 台达默认9600现场可能改成19200或38400 Parity parity, // 默认None部分场景用Even DataBits dataBits, // 固定8 StopBits stopBits, // 固定1 ReadTimeout 500, // 读超时500ms太短会误判断线 WriteTimeout 500 // 写超时500ms }; _serialPort.Open(); }这段代码的关键在于超时设置。ReadTimeout设成500毫秒是我踩过坑之后的习惯——设成100毫秒PLC稍微忙一点就超时设成2000毫秒界面卡死等两秒才报错。500毫秒是个平衡点。另外串口打开之后不要立刻发数据台达PLC上电后需要一点时间初始化通信模块我一般会加一个200毫秒的延时再发第一帧。RTU帧的组装包括从站地址、功能码、起始地址、数据、CRC校验。源码里CRC计算是单独一个静态方法用的是查表法比逐位计算快。从站地址默认是1台达PLC的通信地址在D1121寄存器里可以改如果你读不到数据先确认这个地址。2.3 TCP模式下的连接管理与异常重连Modbus TCP和RTU的区别在于多了MBAP头少了CRC校验。源码里TCP部分用的是TcpClient连接建立之后发送请求、接收响应。这里有个容易被忽略的点Modbus TCP的Transaction ID。每次请求的Transaction ID应该递增响应里的Transaction ID要和请求一致否则说明响应错位了。源码里用了一个ushort变量做递增溢出后归零。// Modbus TCP请求组装Transaction ID递增 private ushort _transactionId 0; private byte[] BuildTcpRequest(byte unitId, byte functionCode, ushort startAddr, ushort quantity) { _transactionId; var request new byte[12]; request[0] (byte)(_transactionId 8); // Transaction ID高字节 request[1] (byte)(_transactionId 0xFF); // Transaction ID低字节 request[2] 0; request[3] 0; // Protocol ID固定0 request[4] 0; request[5] 6; // Length固定6 request[6] unitId; // 单元标识台达PLC一般填1 request[7] functionCode; // 功能码 request[8] (byte)(startAddr 8); // 起始地址高字节 request[9] (byte)(startAddr 0xFF); // 起始地址低字节 request[10] (byte)(quantity 8); // 数量高字节 request[11] (byte)(quantity 0xFF); // 数量低字节 return request; }异常重连的逻辑在源码里是一个定时器每隔3秒检查一次连接状态。如果TcpClient的Connected属性为false就重新连接。注意Connected属性不可靠它只表示上次操作的状态更稳妥的做法是发一个读请求看能不能收到响应。源码里用的是后者虽然多了一次通信但判断准确。3. 寄存器地址映射台达PLC的D、M、X、Y怎么对应Modbus地址3.1 台达PLC软元件与Modbus地址的对应关系这是整个项目里最容易搞混的部分。台达PLC的D寄存器、M线圈、X输入、Y输出在Modbus协议里都有对应的地址范围但不是一一对应的。源码里有一个AddressMapping类专门做转换核心映射关系如下台达软元件Modbus功能码Modbus地址范围十进制说明D0-D25503/06/100-255保持寄存器读写M0-M25501/050-255线圈读写X0-X17020-17离散输入只读Y0-Y1701/050-17线圈读写注意X和Y的地址是重叠的但功能码不同。X用02功能码读Y用01功能码读。源码里读X和读Y是两个独立的方法不要混用。3.2 地址偏移的坑0基和1基的转换Modbus协议文档里地址是从1开始编号的但实际报文里是从0开始的。比如你想读D0报文里的起始地址是0想读D100报文里是100。但有些PLC手册写的是“40001”这种格式40001对应的是保持寄存器的第一个实际报文地址是0。台达PLC的手册一般直接写D0、D100所以源码里直接用D寄存器的编号作为Modbus地址不需要加减1。但如果你从别的PLC迁移过来比如西门子的40001就要做转换。// 地址转换处理不同来源的地址格式 public static ushort ConvertToModbusAddress(string address, out byte functionCode) { // 台达D寄存器D100 - 地址100功能码03 if (address.StartsWith(D)) { functionCode 0x03; return ushort.Parse(address.Substring(1)); } // 台达M线圈M50 - 地址50功能码01 if (address.StartsWith(M)) { functionCode 0x01; return ushort.Parse(address.Substring(1)); } // 兼容40001格式40001 - 地址0功能码03 if (address.StartsWith(4)) { functionCode 0x03; return (ushort)(int.Parse(address) - 40001); } throw new ArgumentException($不支持的地址格式: {address}); }这段代码里40001格式的处理是给迁移场景用的。如果你只做台达项目可以忽略这个分支但留着不碍事。3.3 批量读取与数据解析单个寄存器读取效率太低产线上通常一次读几十个。源码里批量读取用的是功能码03一次最多读125个寄存器Modbus协议限制。读回来的字节数组要按寄存器拆开每个寄存器两个字节高字节在前。// 批量读取D寄存器解析为ushort数组 public ushort[] ReadHoldingRegisters(byte unitId, ushort startAddr, ushort quantity) { var request BuildRtuRequest(unitId, 0x03, startAddr, quantity); _serialPort.Write(request, 0, request.Length); // 响应长度 从站地址(1) 功能码(1) 字节数(1) 数据(quantity*2) CRC(2) int expectedLen 5 quantity * 2; var response new byte[expectedLen]; int read 0; while (read expectedLen) { read _serialPort.Read(response, read, expectedLen - read); } // 校验CRC if (!CheckCrc(response)) throw new Exception(CRC校验失败数据可能被干扰); // 解析数据高字节在前 var result new ushort[quantity]; for (int i 0; i quantity; i) { result[i] (ushort)((response[3 i * 2] 8) | response[4 i * 2]); } return result; }这里有个细节串口Read方法不保证一次读完所有字节所以要用while循环累积读取。我见过不少源码直接Read一次就解析在低速波特率下经常读不全表现就是数据错位、CRC校验失败。CRC校验失败不要重试太多次连续三次失败基本可以判断是接线或者参数问题重试解决不了。4. 避坑与排查串口通信和64位编译的五个血泪教训4.1 现象编译报错“试图加载格式不正确的程序”原因工程目标平台是x64但引用的DLL是x86的或者反过来。工控领域很多串口库、旧版DLL只有32位版本。解决在Visual Studio里把工程的目标平台改成x64同时确认所有引用的DLL都有64位版本。如果某个DLL只有32位要么找替代方案要么把整个工程改成x86。这份源码之所以强调64位就是因为现在新装的Windows系统基本都是64位32位工程在64位系统上跑虽然能兼容但调用某些系统API时会出问题。4.2 现象能连上PLC但读不到数据返回超时原因串口参数不匹配或者从站地址不对。台达PLC的默认从站地址是1但现场可能被改成别的。另外RS485接线A、B反了也会导致通信失败。解决先用Modbus Poll或者串口助手手动发一帧测试。发01 03 00 00 00 01 84 0A读D0一个寄存器如果返回01 03 02 00 00 B8 44之类的数据说明参数和接线没问题。如果没有返回检查波特率、校验位、从站地址。A、B线反了不会烧设备但通信不上调换一下就行。4.3 现象读回来的数据偶尔错位CRC校验时好时坏原因串口ReadTimeout设得太短或者没有用while循环累积读取。另外RS485总线没有终端电阻长距离通信时信号反射也会导致数据错误。解决ReadTimeout设成500毫秒以上读取用while循环。如果距离超过50米在总线两端各加一个120欧姆的终端电阻。源码里CRC校验失败会抛异常不要忽略这个异常它是在帮你发现问题。4.4 现象写寄存器成功但PLC没反应原因写的是D寄存器但PLC程序里没有把D寄存器的值映射到实际输出。或者写的是M线圈但PLC程序里M线圈被其他逻辑覆盖了。解决先用PLC编程软件监控D寄存器和M线圈的值确认写入是否生效。如果值写进去了但设备没动作检查PLC程序里的逻辑。这不是通信问题是PLC程序问题。4.5 现象64位工程在部分工控机上运行报“未找到串口”原因工控机上的串口驱动是32位的64位程序调用不了。或者USB转串口模块的驱动没有安装64位版本。解决换用64位驱动的串口模块比如FTDI芯片的模块通常有64位驱动。如果工控机自带串口去设备管理器里看驱动版本更新到64位。实在不行用串口服务器把串口转成TCP走Modbus TCP绕开驱动问题。5. 进阶用法把通信层抽成独立类库与多设备轮询5.1 把ModbusMaster抽成独立DLL源码里通信层和界面层在同一个工程里方便调试但不利于复用。我一般会把ModbusMaster、AddressMapping、CRC计算这几个类抽到一个独立的类库工程里编译成DLL。这样下次做新项目直接引用DLL不用复制粘贴代码。抽的时候注意把串口配置和TCP配置做成接口方便替换。// 通信接口定义方便替换RTU和TCP实现 public interface IModbusTransport { ushort[] ReadHoldingRegisters(byte unitId, ushort startAddr, ushort quantity); void WriteSingleRegister(byte unitId, ushort addr, ushort value); bool[] ReadCoils(byte unitId, ushort startAddr, ushort quantity); void WriteSingleCoil(byte unitId, ushort addr, bool value); void Connect(); void Disconnect(); }RTU和TCP各实现一个类界面层只依赖接口。这样你在车间调试时用RTU到了办公室用TCP模拟器测试切换只改一行代码。5.2 多设备轮询的节奏控制一条RS485总线上挂多台台达PLC是常见场景。轮询的时候要注意两点一是每台设备的从站地址不能重复二是轮询间隔要留够。台达PLC处理一帧请求大概需要10到50毫秒如果轮询太快PLC来不及响应就会丢帧。我一般用定时器做轮询间隔设200毫秒。每轮读一台设备读完切换下一台。如果某台设备连续三次超时就跳过它标记为离线避免一台设备故障拖慢整个轮询周期。源码里没有多设备轮询的逻辑但通信层的方法都是现成的你加一个设备列表和索引就能实现。// 多设备轮询的简化逻辑 private Listbyte _deviceAddresses new Listbyte { 1, 2, 3 }; private int _currentIndex 0; private Dictionarybyte, int _failCount new Dictionarybyte, int(); private void PollTimer_Tick(object sender, EventArgs e) { var addr _deviceAddresses[_currentIndex]; try { var data _modbus.ReadHoldingRegisters(addr, 0, 10); UpdateUi(addr, data); _failCount[addr] 0; // 成功则清零 } catch { _failCount[addr]; if (_failCount[addr] 3) { MarkOffline(addr); // 连续三次失败标记离线 } } _currentIndex (_currentIndex 1) % _deviceAddresses.Count; }5.3 用Modbus Poll和Modbus Slave做对照验证调试通信代码的时候不要一上来就连PLC。先用Modbus Slave模拟一个从站把你的上位机连上去确认收发正常。然后再用Modbus Poll模拟主站连你的PLC确认PLC的通信参数和地址映射。两边都通了再把上位机和PLC对接。这样能把问题定位在通信层还是PLC程序层。Modbus Slave里设置从站地址、功能码、起始地址、寄存器数量和你源码里的参数一一对应。如果Modbus Slave能读到数据你的代码读不到那就是代码问题如果Modbus Slave也读不到那就是PLC参数或者接线问题。从那以后我每次调新的PLC通信都强制走一遍“先模拟、再实机”的流程省下来的排查时间比多花的那十分钟多得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?