
简介在工业自动化上位机开发中与安川PLC建立稳定高效的通信往往是项目落地的关键环节。这份由工控老马出品的通讯控件源码包专为解决这一场景而生同时支持C#与VB.net两种开发语言可直接作为DLL引用或参考源码集成。压缩包体积仅147KB却内含33个文件包括vb源代码、编译生成的dll控件、可运行的exe演示程序以及pdb调试符号、xml文档、resx资源等配套文件结构清晰覆盖从通信协议封装到界面调用的完整链路。目前该资源已有831人学习下载其“亲测能用”的口碑尤其适合刚入手安川PLC通信的新手参考也方便有经验的工程师快速移植到自有框架。通过研读这份源码读者能了解MEMOBUS协议的寄存器读写流程、数据帧构造方法以及如何封装成通用控件供多语言调用调试符号与配置资源的存在也使得排查问题与二次定制更加便捷。从实际开发角度看免去从零编写协议栈的重复劳动直接套用经过验证的通信层让开发者更专注于业务逻辑从而大幅缩短项目开发周期。1. 一个安川PLC通讯控件解决的是上位机开发里最绕不开的环节做上位机的都清楚和PLC通讯这件事本身不难难的是通讯这套代码写得稳定、能复用、不出妖蛾子。自己用SerialPort从头写一版CRC算错一个字节、帧头发错一个数现场调试能熬掉半条命。这份安川PLC通讯控件最大的价值不是给你一个能用的dll而是它把上位机与安川PLC通讯的完整套路——从串口参数设置、MEMOBUS协议指令拼装、CRC16校验到读写的同步与超时处理——封装成了C#、VB.net都能直接调用的源码形式。工控老马把这套源码放出来你可以直接用也可以拆开看内部是怎么写的。本文从协议原理、控件源码结构、C#调用方法到排错技巧完整过一遍。2. 安川PLC通讯与MEMOBUS协议通讯控件背后的帧结构原理2.1 MEMOBUS是安川PLC的母语和Modbus是近亲安川PLC如CP系列、MP系列支持的串行通讯协议叫MEMOBUS它的帧结构和Modbus RTU高度相似命令字、寄存器寻址方式几乎一致但寄存器地址映射和部分功能码使用上安川有自己的定义。通讯控件要做的核心事就是把上位机要读写的地址和数据按MEMOBUS帧格式打包通过串口或以太网发出再解析PLC回应的帧。一个标准MEMOBUS RTU请求帧分四段段字节数说明示例值从站地址1PLC侧的通讯从站号范围1~2550x01功能码103读保持寄存器、06写单寄存器、10写多寄存器0x03数据区N起始地址、寄存器数量或写入数据按功能码变化0x00 0x64 0x00 0x01CRC162前面的字节做CRC16-Modbus计算低字节在前0x04 0x0D响应帧结构类似区别在数据区直接携带PLC返回的寄存器值。通讯控件的内部实现本质上就是这三件事的循环组帧、发帧、解帧。2.2 控件里通讯参数的设置逻辑这份源码里控件通过属性暴露了通讯参数常用的几个我按现场经验列一下PortName串口号枚举本机存在的COM口BaudRate波特率安川PLC默认通常是9600现场也有19200要和PLC侧拨码开关或参数设置一致DataBits数据位固定8Parity校验位安川出厂默认无校验但现场若PLC侧配了偶校验Even上位机必须同步修改StopBits停止位常规1位Timeout通讯超时时间单位毫秒参数校验的坑在Parity上。很多新手只改波特率和串口号忘了查PLC侧的校验位配置。安川PLC的串口通讯参数不是自动协商的两边不一致帧发出去PLC根本不响应表现就是上位机一直报超时。控件源码里有一个ValidateParams方法会在打开串口前检查参数合法性——设置非法时抛出异常。这个设计值得自己的项目也照抄一份。3. 拆解控件源码VB.net实现的核心通讯方法3.1 控件工程的文件结构与职责划分解压资源包后Visual Studio里打开MEMOBUS_COM.sln工程是VB.net写的。几个关键文件对应关系如下MEMOBUS_COM.vb控件主体SerialPort的封装、公共属性、读写入口MEMOBUS_COM.Designer.vbVS自动生成的组件设计器代码ResourceHome.png控件在设计器里显示的图标bin/obj目录编译输出和中间文件源码里可直接把bin目录下的dll引用到C#工程控件主体类继承自Component说明它是非可视化的通讯组件。类的成员变量里有几个值得注意的字段Private WithEvents m_Serial As SerialPort Private m_TimeOut As Integer 1000 Private m_StationNo As Byte 1 Private m_ComID As Integer这里把SerialPort声明为WithEvents是为了后面接收DataReceived事件时做异步数据缓存处理。m_TimeOut是读写等待响应的超时上限。m_StationNo是从站号MEMOBUS协议里每帧第一字节就是它。3.2 关键读寄存器方法的组帧与解帧实现读单条保持寄存器是现场最常用的操作源码里对应ReadRegister方法。核心代码如下Public Function ReadRegister(ByVal address As Integer, ByVal points As Integer, ByRef values() As Integer) As Boolean Dim sendData(7) As Byte sendData(0) m_StationNo sendData(1) H3 sendData(2) CByte((address 8) And HFF) sendData(3) CByte(address And HFF) sendData(4) CByte((points 8) And HFF) sendData(5) CByte(points And HFF) Dim crc As UInt16 CRC16(sendData, 6) sendData(6) CByte(crc And HFF) sendData(7) CByte((crc 8) And HFF) m_Serial.Write(sendData, 0, 8) ... End Function这段代码做的是功能码03的读保持寄存器请求。address左移8位再取高字节和低字节是把int型地址拆成帧里的两个字节。CRC计算只针对前6个字节结果是UInt16低字节先发是Modbus系协议的规定。Function返回Boolean调用方能立即判断命令是否正常下发但真正的成功与否还要看PLC回帧后内部的校验。响应帧的解析在控件的接收处理里。控件启动了一个后台接收线程把串口收到的字节存入缓存然后按帧头、功能码、数据区、CRC依次校验。只有CRC校验通过、从站号匹配的帧才会上抛给调用方。如果PLC返回异常码如功能码高位被置1控件会把错误码解析成中文描述比如02表示非法地址、03表示非法数据值。这个错误翻译表做上位机联调时特别实用。3.3 写寄存器与写多寄存器的差异处理写单寄存器用功能码06写连续多个寄存器用10。控件里分别封装了WriteRegister和WriteRegisters。两者的组帧区别在于数据区长度不固定写多寄存器要在数据区前部带上字节计数。核心代码片段Public Function WriteRegisters(ByVal startAddr As Integer, ByVal values() As Integer) As Boolean Dim count As Integer values.Length Dim byteCount As Integer count * 2 Dim sendLen As Integer 9 byteCount Dim sendData(sendLen - 1) As Byte sendData(0) m_StationNo sendData(1) H10 sendData(2) CByte((startAddr 8) And HFF) sendData(3) CByte(startAddr And HFF) sendData(4) CByte((count 8) And HFF) sendData(5) CByte(count And HFF) sendData(6) CByte(byteCount And HFF) For i As Integer 0 To count - 1 sendData(7 i * 2) CByte((values(i) 8) And HFF) sendData(8 i * 2) CByte(values(i) And HFF) Next Dim crc As UInt16 CRC16(sendData, sendLen - 2) sendData(sendLen - 2) CByte(crc And HFF) sendData(sendLen - 1) CByte((crc 8) And HFF) m_Serial.Write(sendData, 0, sendLen) ... End Function写多寄存器的返回值不再是数据而是PLC回显请求帧里的起始地址和寄存器数量。控件会比对回显值和发送值是否一致来判断这次写入是否被PLC接受。这里注意values数组长度和byteCount的换算。每个寄存器占两个字节所以字节计数是count * 2帧总长度在基础9字节上再叠加少了任一字节PLC会直接扔帧不响应。4. C#工程中调用VB.net控件跨语言引用的完整步骤4.1 在VS里把控件正确加入C#项目控件工程编译产物是MEMOBUS_COM.dllC#项目引用它有两种常见做法。一种是直接编译整个解决方案然后在你自己的C#WinForm项目里右键引用选择浏览定位到bin目录下的dll。另一种是把整个MEMOBUS_COM工程文件加入当前解决方案然后在C#项目里添加项目引用。推荐用第二种因为源码在你手里调试时可以F11跟进PLC通讯的每一行。添加完成后在C#窗体设计器的工具箱里右键选择选择项浏览dll控件图标ResourceHome.png会自动出现在工具箱。这样设计器里拖拽就能用和用原生TextBox没区别。4.2 C#代码里实例化与核心调用示例以下是一个完整的C#示例演示如何从C#调用该控件完成连接、读寄存器、写寄存器三件事using System; using System.Windows.Forms; public partial class MainForm : Form { private MEMOBUS_COM.MemobusCom plcCom; public MainForm() { InitializeComponent(); plcCom new MEMOBUS_COM.MemobusCom(); plcCom.Timeout 1000; // 超时时间设为1000ms } private void btnConnect_Click(object sender, EventArgs e) { try { plcCom.PortName COM3; plcCom.BaudRate 9600; plcCom.Parity System.IO.Ports.Parity.None; plcCom.DataBits 8; plcCom.StopBits System.IO.Ports.StopBits.One; plcCom.StationNo 1; plcCom.Open(); // 打开串口连接 } catch (Exception ex) { MessageBox.Show(打开失败 ex.Message); } } private void btnRead_Click(object sender, EventArgs e) { int[] values new int[10]; bool ok plcCom.ReadRegister(0x0000, 10, ref values); // 安川PLC0x0000起连续读10个保持寄存器注意ref修饰 if (ok) { txtValue.Text string.Join(,, Array.ConvertAll(values, v v.ToString())); } } private void btnWrite_Click(object sender, EventArgs e) { int[] data new int[] { 100, 200, 300 }; bool ok plcCom.WriteRegisters(0x0010, data); // 从0x0010地址连续写3个寄存器写的是PLC侧保持寄存器区 if (ok) MessageBox.Show(写入成功); } }代码里有几个容易忽略的注意点。ReadRegister的第三个参数values必须以ref传入因为VB.net方法内部会往数组里填充响应帧解析出的寄存器值C#里要加ref才能实现这种输出型参数漏掉了编译会报错。控件属性名PortName、BaudRate和原生SerialPort一致但Parity、StopBits这些枚举类型要写成System.IO.Ports.Parity形式避免和控件内部的枚举定义冲突。Open方法内部做了端口占用检测和参数校验如果PLC没上电或者串口线松动Open本身不会报错但第一次ReadRegister会在超时时间到达后返回false。4.3 C#侧的事件处理与DataReceived异步数据这个控件触发的DataReceived事件和SerialPort原生事件类似是异步到达的——PLC回帧的时刻不可预测。控件的内部实现是接收到完整帧时触发事件事件参数里带有帧类型读/写、寄存器地址、数值数组和错误码。C#侧订阅代码如下plcCom.DataReceived (sender, e) { // 控件内部已自动处理CRC、帧头校验这里直接取值 int val e.Value; int addr e.Address; this.Invoke(new Action(() { txtReadResult.Text 地址 addr.ToString(X4) 的值 val; })); };事件回调运行在通讯线程上直接更新UI控件会抛跨线程异常所以用了this.Invoke把更新操作切回UI线程。把先进先出队列用在这里很合适——如果PLC的扫描周期是50ms一次回传多个寄存器的值队列能让UI线程按序消费不丢数据也不卡界面。5. 现场调试与进阶优化帧日志、浮点读写和断线重连的实战技巧5.1 排错利器自己写一段帧日志语句现场联调最怕的是上位机报通讯失败但说不出原因。我的做法是在调用控件ReadRegister和WriteRegisters的外围包一层日志把发出的原始字节和返回的布尔值记录下来格式类似这样string frame BitConverter.ToString(sendBuf); // 输出结果示例01-03-00-00-00-0A-C5-CD - 读从站1地址0x000010个点把这条日志输出到文本文件或调试窗口对照手里的安川通讯手册看帧头、功能码、CRC每一个字节。假如波特率或校验位配错串口抓包软件里看到的往往是PLC对请求帧没有响应这时检查方向优先放在物理层参数而不是协议逻辑。5.2 浮点数如何跨过寄存器边界安川PLC里的浮点数在寄存器区占用两个连续寄存器32位IEEE754格式而控件的ReadRegister/WriteRegisters按16位整数处理。直接读出来做整数运算得到的是错的。完整处理方式是int lo intValues[0]; int hi intValues[1]; // 低字在前还是高字在前取决于PLC侧MOVD指令的设置 uint combined ((uint)lo 0xFFFF) | ((uint)hi 16); float result BitConverter.ToSingle(BitConverter.GetBytes(combined), 0);注意PLC内部的字序可能是高字在前也可能是低字在前取决于PLC程序的MOVD/双字传输指令的使用习惯。联调时先在PLC里写入一个已知浮点值如1.5上位机读出来对比大小即可唯一确定字序。控件本身不处理这种字序转换这是现场适配必须自己补的一段逻辑。5.3 断线自动重连的实用模式现场环境里插拔串口线、PLC断电重启都很常见。控件在通讯失败后会置内部错误标志下一次读写直接返回false。上位机里我会这样设计重连逻辑if (!plcCom.IsOpen()) { // 自动尝试重连最多3次间隔500ms for (int i 0; i 3; i) { try { plcCom.Open(); break; } catch { System.Threading.Thread.Sleep(500); } } }另外强烈的建议是把控件的超时时间调成2000到3000毫秒默认1000毫秒在PLC侧处理时间偏长时容易误报超时。重连成功后主动做一个读多个寄存器的动作既能验证连接质量也能让PLC侧有些需要握手才能恢复的通讯状态重新就绪。这套逻辑加在控件上层不动控件源码能保证控件本身保持简洁。本文还有配套的精品资源点击获取