简介:这套上位机与安川PLC通讯控件及C#使用实例源码,由工控老马出品并亲测校正,主要面向工业自动化领域需要借助C#或VB.NET实现上位机与安川PLC通讯的开发者,既能帮助新手快速上手控件调用,也能为有经验的工程师提供可复用的通讯封装与调试思路。压缩包内共33个文件,核心为6个VB源码文件与3个DLL控件库,另含可直接运行的EXE测试程序、PDB调试符号、XML配置及Resx资源文件等,整体仅157KB,结构紧凑,便于对照学习与二次开发。目前已有720人学习下载。借助这份源码,读者既能查看控件在VB.NET中的底层实现,也能在C#测试程序中理解具体调用流程,配合工程文件、解决方案与界面截图,可大幅节省从零搭建上位机通讯环境的时间,是掌握安川PLC通讯机制的高性价比参考资料。
1. 从一次现场通讯失败说起:为什么我放弃了裸写串口
做上位机开发的同行应该都有过这种经历:设备联调现场,PLC那边程序下好了,触摸屏监控数值一切正常,可你的C#程序通过串口发指令,PLC就是没反应。用串口助手抓包看,报文格式明明是对的,CRC校验也算完了,问题到底出在哪?
这个场景几乎每个工控上位机开发者都踩过。安川PLC的MEMOBUS协议从报文结构到寄存器映射,和标准的Modbus协议有不少细节差异,你在网上搜到的Modbus例程直接搬过来用,十有八九在功能码和地址换算上出偏差。这也是为什么我拿到这套"上位机与安川PLC通讯控件及C#使用实例源码"时,会专门花时间去拆它——它把安川PLC的MB协议封装成了通用控件,测试程序是C#写的,控件本身用VB.net实现,正好覆盖了工控上位机开发中最常见的技术组合。
这套资源适合正在做上位机与安川PLC通讯开发的工程师,也适合刚入门想搞懂通讯控件封装思路的新人。它不只是一份能跑的代码,更是一个可以直接拿来做二次开发的通讯底座。下文我会从MEMOBUS协议原理、控件封装结构、C#调用细节到通讯排错技巧,一层层拆开讲。
2. MEMOBUS协议与安川PLC通讯的地址映射机制
2.1 MEMOBUS和Modbus到底差在哪
安川PLC的串口通讯基于MEMOBUS协议,底层是Modbus协议的一个工业变种。很多初学者直接用Modbus例程去连安川PLC,指令发出去能收到响应,但读写的数据就是不对,根源往往在于地址区映射方式不同。
在Modbus协议中,寄存器地址是线性的,比如40001到49999是保持寄存器区。但MEMOBUS把安川PLC的存储区分成了多个独立区域,每个区有独立的地址编号规则。举一个实际例子:写入PLC的保持寄存器,Modbus里面你直接操作40001地址即可,但在安川PLC的MEMOBUS协议下,你需要先把目标地址换算成协议报文内部使用的16进制地址。
安川PLC存储区主要分为以下几种:
| 存储区类型 | 对应PLC区域 | 通讯报文中的地址范围 | 读写支持 |
|---|---|---|---|
| CIO区 | 外部输出/输入继电器 | 0000~07FF | 读/写 |
| WR区 | 工作继电器 | 0000~01FF | 读/写 |
| HR区 | 保持继电器 | 0000~01FF | 读/写 |
| DM区 | 数据内存 | 0000~01FF | 读/写 |
| AR区 | 辅助继电器 | 0000~00FF | 读/写 |
地址映射的差异是通讯失败的高发区。你通过串口助手发一条读保持寄存器的指令,报文中地址字段用的是协议内部的十六进制编号,不是你在PLC编程软件里看到的那个地址。这个换算关系如果不搞清楚,程序写再多也白搭。
2.2 报文结构与CRC校验的实现细节
MEMOBUS的报文帧格式和标准Modbus RTU基本一致:从站地址、功能码、起始地址、数据、CRC16校验。串口参数是9600波特率、8数据位、1停止位、无校验,这个配置在安川PLC的默认通讯设定下是标准参数。
// CRC16校验计算,Modbus RTU标准多项式0xA001 public static ushort CRC16(byte[] data, int len) { ushort crc = 0xFFFF; for (int i = 0; i < len; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if ((crc & 0x0001) != 0) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc; }这里用的是查表法之外最直白的位运算法,每字节要循环8次。通讯频率不高时完全够用,但如果是高频读写场景,建议改成查表法,速度能提升一个数量级。CRC计算完成后,低字节在前、高字节在后填入报文的最后两位,这个字节顺序是初学最容易搞反的地方。
提示:MEMOBUS协议中,从站地址默认是0,即使用00作为站号时,PLC不做地址校验直接响应。多台PLC组网通讯时才需要设置不同站号。上位机开发时,单机调试直接用站号0可以省掉不少排查时间。
2.3 功能码选择与典型通讯指令实例
安川PLC的MEMOBUS协议支持的功能码不多,常用的是以下几个:
- 01H:读取线圈状态(位读取)
- 03H:读取保持寄存器内容
- 05H:写入单个线圈
- 10H:写入多个寄存器
其中03H和10H是使用频率最高的两个功能码。读取DM区数据和写入DM区数据是上位机最常见的操作,因为PLC的运算结果和生产参数大多存放在DM区中。
// 读取DM区数据的报文构造:站号0 + 功能码0x03 + 起始地址DM100 + 读取长度2个寄存器 byte[] readCommand = new byte[] { 0x00, // 从站地址 0x03, // 功能码 读保持寄存器 0x00, 0x64, // 起始地址 0x0064 = 100 0x00, 0x02, // 读取寄存器数量 0x00, 0x00 // CRC占位 }; ushort crc = CRC16(readCommand, 6); readCommand[6] = (byte)(crc & 0xFF); // CRC低字节 readCommand[7] = (byte)((crc >> 8) & 0xFF); // CRC高字节注意这里的地址映射规则:PLC编程软件里看到的是DM100,但在通讯报文中,地址字段填的是0x0064,也就是100的十六进制。这是MEMOBUS协议中DM区起始地址和实际编号一致的特例。换到CIO区就不是这个逻辑了,CIO区在协议中从0000开始映射。网上很多例程直接复制修改起始地址,不做区域换算,这也是半天调不通的原因之一。
3. 通讯控件的源码结构拆解与VB.net实现分析
3.1 控件工程的整体架构
打开这套资源里的MEMOBUS_COM工程,解决方案里包含两个项目:一个是VB.net编写的通讯控件,另一个是C#编写的WinForm测试程序。控件项目本身用的是VB.net,测试程序用C#,这种跨语言组合在实际工业项目里很常见——老牌工控企业用VB.net沉淀了不少组件库,而新写上位机界面的人大多用C#。
控件项目的类结构按职责拆成了三个层面:
- 串口通讯层:封装SerialPort的打开、关闭、收发数据
- 协议层:负责报文构造、响应解析、CRC校验
- 业务接口层:向上位机暴露读写PLC数据的公共方法
串联口收发是很多初学写上位机通讯最容易出问题的地方。如果收数据用DataReceived事件去拼接缓存,很容易出现半包、粘包的情况。这套控件里的做法是收到响应后按帧长度截取,实际开发中这个逻辑需要根据PLC的响应超时时间做调整。
3.2 报文收发与超时处理的实现
控件在实现上有几个细节值得单独拿出来说。先看C#测试程序里调用控件读PLC数据的代码:
// 创建通讯控件实例 MEMOBUS_COM.MemobusControl mb = new MEMOBUS_COM.MemobusControl(); // 设置串口参数并打开连接 mb.PortName = "COM3"; mb.BaudRate = 9600; mb.DataBits = 8; mb.StopBits = "1"; mb.Parity = "None"; mb.Open(); // 从DM100开始读取2个字的数据 short[] data = mb.ReadDM(100, 2);串口参数设置部分暴露的是字符串类型的停止位和校验位参数,这是VB.net控件的典型风格。调用方传进来的参数在控件内部会转换成SerialPort所需的枚举类型,屏蔽了部分细节。实际项目里如果把波特率写死成9600会有局限性,自动化设备中PLC通讯为了追求速度,很多现场改用19200甚至38400波特率,所以我在二次开发时会把这个参数做成可配置项。
超时处理是另一个值得关注的设计点。串口通讯不是HTTP请求,没有天然的"对方必须响应"的保证。PLC在总线繁忙或程序跑飞时,可能完全不回应你。控件内部对每条指令都设了超时时间,超时后会触发Timeout事件,上位机可以在这个事件里做重试或告警处理。
' VB.net控件内部读取DM区的实现 Public Function ReadDM(ByVal startAddr As Integer, ByVal count As Integer) As Short() ' 构造读取报文 Dim cmd(7) As Byte cmd(0) = 0 ' 站号 cmd(1) = &H3 ' 功能码 03H cmd(2) = CByte((startAddr >> 8) And &HFF) cmd(3) = CByte(startAddr And &HFF) cmd(4) = CByte((count >> 8) And &HFF) cmd(5) = CByte(count And &HFF) ' 计算CRC并填充 Dim crcValue As UShort = Crc16(cmd, 6) cmd(6) = CByte(crcValue And &HFF) cmd(7) = CByte((crcValue >> 8) And &HFF) ' 发送并等待响应 SendData(cmd) Dim response As Byte() = WaitResponse(128) ' 解析响应数据 End Function地址参数的处理上做了一次位运算拆分,高字节放前、低字节放后,这是Modbus RTU协议固定的大端序。收到响应后需要校验从站地址、功能码、数据长度三个字段,再取出有效数据做类型转换。这套代码的逻辑很完整,适合直接拿去做二次开发的模板。
3.3 位地址读写与字地址读写的差异处理
安川PLC的数据类型分成位和字两种。线圈类数据(如CIO区的单个输出点)按位操作,而DM区的数值数据按字操作。控件中对这两种操作分别提供了接口。
// 读取单个位地址 bool coilStatus = mb.ReadCoil(0, 100); // 写入单个位地址 mb.WriteCoil(0, 100, true); // 读取字地址数据 short[] values = mb.ReadDM(100, 10); // 写入多个字地址数据 short[] writeData = new short[] { 100, 200, 300 }; mb.WriteDM(100, writeData);位操作和字操作在报文构造上差别很大的地方在于功能码和地址表示方式。写入单个线圈用05H功能码,写入多个寄存器用10H功能码,而读取操作中读取线圈用01H,读取寄存器用03H。实际项目中常见一个坑是:你需要写多个连续的线圈状态,比如控制流水线上8个气缸的动作顺序,如果用05H逐条写,一次要发8条指令,通讯效率很低。标准Modbus协议支持用0FH功能码批量写线圈,但安川PLC的MEMOBUS协议支持范围有限,所以控件里没提供这个接口,上位机侧需要自行把这8次写操作合并成一次10H功能码的寄存器写入,在字节位与寄存器字之间做转换。
4. C#调用VB.net通讯控件的实战与跨语言互操作细节
4.1 在Visual Studio中引用VB.net控件工程
C#项目调用VB.net控件,本质上是程序集级别的引用。在解决方案中直接添加项目引用是首选方式,这样调试时可以直接跟进VB.net代码内部看报文收发细节。
具体操作是:在C#工程中右键引用,选择"项目"选项卡,勾选MEMOBUS_COM项目。这里有一个容易踩的坑:如果VB.net控件的目标框架版本和C#工程不一致,引用会报黄色警告图标,运行时会抛FileLoadException异常。一般需要把两个工程的目标框架统一到.NET Framework 4.6.2或4.7.2。
提示:如果控制器的目标框架设置不一样,务必统一。实际开发中遇到的大多数DLL引用失败,根源都是目标框架不兼容。
测试程序里控件的实例化方式和C#自建类库没什么区别,直接用类型名声明即可:
using MEMOBUS_COM; public partial class MainForm : Form { private MemobusControl _plcControl; public MainForm() { InitializeComponent(); _plcControl = new MemobusControl(); } }命名空间和类名保持一致,VB.net控件在使用上完全无缝。要注意的是VB.net中的模块级变量默认为静态,在控件设计时如果用了Module而不是Class来做内部共享状态,多线程访问时会有关键区问题。
4.2 从控件接口设计看C#调用VB.net的语法差异
VB.net控件对外暴露的方法名、参数顺序和C#代码直接对应,但在类型转换上有一个值得注意的地方。VB.net中Integer类型是32位有符号整型,Short类型是16位有符号整型,正好对应C#的int和short。看下面这个C#调用语句:
// 注意返回类型是short数组,不是int数组 short[] dmValues = mb.ReadDM(100, 2); // 如果PLC中存储的是无符号数值,需要做转换 ushort value = (ushort)dmValues[0];上述代码中从PLC读回来的的short类型,在C#中如果不注意符号位,读到的值一旦超过32767就会显示成负数。PLC的DM数据区中存放FIFO读写指针计数这类不断累加的数据时,数值很容易突破32767。这个坑在调试时很隐蔽,因为小数值调试时永远正常,数值一大就出现负数,第一反应往往是查PLC程序而不是查类型转换。
再看写入场景的对比:
// 写法一:直接传short数组 mb.WriteDM(100, new short[] { 1, 2, 3 }); // 写法二:int数组需要逐个强转 int[] sourceData = new int[] { 1000, 2000, 3000 }; short[] shortData = Array.ConvertAll(sourceData, x => (short)x); mb.WriteDM(100, shortData);控件内部如果没做溢出保护,传入超过short范围的数据会被截断,静默丢失高位。我在调试这套控件时,PLC侧写的是温度采集值,现场能到200°C,乘以10倍精度后是2000,在short范围内没事。但如果你做的是计数累加类应用,或者直接把传感器的32位原始值往DM区塞,就一定会出问题。
4.3 跨线程访问UI与通讯控件的协作
上位机通讯控件和界面的交互是另一个高频出错点。C#的WinForm中,串口的数据接收线程不是UI线程,直接在DataReceived回调里修改界面控件会抛InvalidOperationException异常。测试程序里用了Invoke机制做线程切换:
private void plcControl_DataReceived(object sender, EventArgs e) { if (this.InvokeRequired) { this.BeginInvoke(new Action(() => { // 在UI线程中更新显示 textBoxValue.Text = _currentValue.ToString(); })); } }这是工业上位机中典型的"回调线程回UI线程"模式。逻辑上分批处理:收到完整响应后内核触发事件,UI线程在事件处理器中安全更新。如果你在主线程中写一个while循环去调用ReadDM读取并且间隔很短,会直接卡死界面——串口操作的阻塞时间虽然短,但累加起来足以让窗口失去响应。
更稳妥的做法是开一个后台通讯线程做轮询,把读到的数据放进队列或共享变量,UI线程通过Timer定时刷新显示。这套资源里的测试程序用的是后一种方式,定时器每200ms触发一次读取刷新。
5. 通讯调试的三个关键技巧:抓包验证、参数调优与互操作避坑
拿到这套源码后,建议不要直接接PLC联调,先把串口通讯的验证链路搭起来。用虚拟串口工具(如Virtual Serial Port Driver)创建一对互联的COM口,再用Modbus Slave模拟软件挂在其中一个串口上,你的C#程序连另一个串口,先跑通读写,再上真机。
第一件事是验证CRC校验的实现是否正确。在网上找一个CRC16-Modbus在线计算器,用手工构造的报文对拍。如果一个报文算出结果一致,再换带不同长度数据字段的报文测试。CRC一旦错了,PLC侧直接静默丢弃,不会返回任何错误码,这是最折磨人的故障类型。
第二个技巧是分析功能码和地址映射的换算逻辑。对着安川PLC手册逐个区域排查:你要读写的是CIO区、WR区、HR区还是DM区,报文地址填对没有。CIO区的0000地址对应PLC的CIO 0通道。如果使用了CX-Programmer软件,打开内存视图对照着看,一条条核对比对着手册猜要高效得多。
调试中如果发现PLC能收到指令但执行结果不对,多在报文起始地址上找问题。比如你要读DM区的数据,PLC软件设置里起始地址是D100,但通讯协议报文里填的是100的十六进制0x0064。高位字节必须放在前一个字节,这一条错了PLC会回复错误码02H(非法地址)。
第三个关键点在VB.net与C#互操作的边界上。这套资源中控件是VB.net、测试程序是C#,这就涉及COM互操作的注册问题。如果你是直接引用工程或DLL文件这种方式,不需要额外注册;但如果通过COM组件方式引用VB.net控件,就需要在目标机器上执行RegAsm注册命令。部署到没有安装.NET框架环境的工控机上时,还有依赖项缺失的问题需要一并处理。
# 使用.NET Framework自带的RegAsm工具注册VB.net控件为COM组件 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe MEMOBUS_COM.dll /codebase /tlb:MEMOBUS_COM.tlb5.1 通讯参数的选型参考
| 参数项 | 推荐值 | 备注 |
|---|---|---|
| 波特率 | 9600 或 19200 | 通讯距离超过15米建议降速 |
| 数据位 | 8 | 固定 |
| 停止位 | 1 | 固定 |
| 校验位 | None | PLC默认配置 |
| 超时时间 | 500~1000ms | 依据PLC扫描周期调整 |
| 重试次数 | 2~3次 | 超过3次建议直接报错 |
超时时间与PLC的扫描周期相关。如果PLC的扫描周期较长(比如梯形图很复杂),从收到指令到返回响应的间隔就会变大。设的500ms超时实际调试时发现偶发超时,改成1000ms后情况明显改善。这个参数的设定原则是"能短则短,不能短就加长",太短误报故障,太长会让操作工感觉界面卡顿。
最后再提一个这套源码里隐藏的小技巧:控件中预留了通讯日志功能,可以在调试阶段把收发报文全部记录到文本文件里。日志文件里的报文内容比串口助手抓包多了时间戳和CRC校验结果,排错效率会高很多。联调完成后建议关闭日志功能,因为工控机长期运行会产生大量日志文件,把C盘塞满以后PLC通讯会异常中断。
安川PLC的通讯调试不是高深技术,关键在于把协议细节吃透、把地址映射搞对、把异常处理想全。这套源码把最底层的报文封装和解析都做完了,剩下就是根据你现场设备的实际情况做参数适配。拿到代码后第一件事不是改功能,而是把上面说的三个验证步骤跑一遍,确认通讯链路是通的,再去做数据交互。
本文还有配套的精品资源,点击获取