一. 背景
在做大型工业设备数据采集 监控的时候常遇到 PLC控制器,常见的三菱 台达 欧姆龙等,本文以三菱q系列为例,通过go语言编写MC协议客户端程序 实现数据寄存器的读写
二 . MC协议介绍
三菱官网有详细文档(参考q16 ,配备网络模块),此处不再赘述,文档下载链接
上位机可以通过网络调试助手测试通讯端口的配置状况,二进制与ASCII码两种通讯方式协议内容有所不同,具体可以查询三菱的通讯协议资料。我这边主要使用的是如下这份,主要是其中的第3章 通过 QnA 兼容 3E/3C/4C 帧、4E 帧进行通信时
下载地址:https://pan.baidu.com/s/1jQs8l2M7IZHvMKFqrgP0qw
三. PLC 配置
3.1 自带网口的CPU
GX Works 软件打开【参数】-->>【PLC参数】-->>【内置以太网端口设置】配置IP,协议格式等参数。

然后进【打开设置】,如下是按TCP协议开放了两个供上位机MC协议的端口。

这样PLC端的配置就结束了。
3.2 使用网络模块QJ71E71-100通讯(界面与上述的内置网口PLC有所不同)
GX Works 软件打开【参数】-->>【网络参数】-->>【以太网***】配置IP,协议格式等参数。下图中的初始时间设置,允许RUN中写入一定要勾对!!!

然后进【打开设置】,如下是按TCP协议开放了一个供上位机MC协议的端口3210。

三 . 读写实例
读写方式有两种,一种是用ASSIC 方式,另外一直是十六进制
1. 读写D7000 寄存器为例子
7000 的十六进制表示方式为 001B58,分配了三个字节,需要倒叙转换581B00
如下指令为读取D7000指令
发送:50 00 00 FF FF 03 00 0C 00 10 00 01 04 00 00 58 1B 00 A8 01 00
接收:D0 00 00 FF FF 03 00 04 00 00 00 0C 00
各个指令说明
副头部 :5000 指令为5000,响应为D000
网络编号:00
PLC编号:FF
IO编号:FF03
模块站号:00
请求数据长度:0C00 请求数据长度计算为之后的所有数据
时钟 :0100 表示等待PLC响应的timeout时间
高低位互换,实际为0001 即最大等待时间250ms*1=0.25秒
指令:0104 实际为0401,即为批量读取 (后面单独列出指令)
子指令:0000 值是0表示按字读取(1个字=16位),如果值是1就按位读取
首地址:58 1B 00 实际为001B58 十进制为7000
软元件:表示读取PLC寄存器的类型 A8 对应D点(后面有详细对应)
长度:01
结束代码:00
示例回复:
成功:D0 00 00 FF FF 03 00 04 00 00 00 0C 00(D7000寄存器数据为13)
副头部:D000 网络编号:00 PLC编号:FF
IO编号:FF03 模块站号:00
应答数据长度:0400 实际为0004 即为4
异常代码:0000 如果正常的话,就是0000
应答数据:0C00 实际为000C 即为13
2.写入
如下指令为向D7000写入H000C
发送:50 00 00 FF FF 03 00 OE 00 10 00 01 14 00 00 58 1B 00 A8 01 00 0C 00
接收:D0 00 00 FF FF 03 00 02 00 00 00

四. go 程序实现
可以用如下两种方式实现
1. 采用modelbus tcp协议,引用go的net包
import (
"encoding/csv"
"fmt"
"net"
"os"
"log"
"time"
)
// 当前给D7000 寄存器写值为 14
func writeTest(c net.Conn){
buf := make([]byte, 64)
// 写入指令 50 00 00 FF FF 03 00 0E 00 10 00 01 14 00 00 58 1B 00 A8 01 00 0E 00
input := []byte{0x50, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x0E, 0x00, 0x10, 0x00, 0x01, 0x14, 0x00, 0x00, 0x58, 0x1B, 0x00, 0xA8, 0x01, 0x00, 0x0E, 0x00}
//客户端请求数据写入 conn,并传输
c.Write([]byte(input))
//服务器端返回的数据写入空buf
cnt, err := c.Read(buf)
if err != nil {
fmt.Println(err)
}
fmt.Println("写入成功",cnt)
}
func ClientSocket() net.Conn {
conn, err := net.Dial("tcp", "192.168.18.44:5002")
if err != nil {
fmt.Println("客户端建立连接失败")
log.Printf("客户端建立连接失败:%s", err)
return nil
}
fmt.Println("客户端建立成功")
log.Printf("客户端建立连接成功:%s", "11")
return conn
}
2. 引用github.com/future-architect/go-mcprotocol/mcp包
代码如下:
package main
import (
// "encoding/hex"
// "os"
// "strconv"
"strings"
"github.com/future-architect/go-mcprotocol/mcp"
"fmt"
"time"
"log"
"encoding/hex"
)
func main() {
//opts.Host, opts.Port
//192.168.18.34 2000
fmt.Println("start connect")
log.Printf("tufei :%s", "start connect")
// 43 139,44 5002
client, err := mcp.New3EClient("192.168.18.44", 5002, mcp.NewLocalStation())
fmt.Println("end connect")
log.Printf("tufei :%s", "end connect")
if err != nil {
fmt.Println(err)
//log.Printf("New3EClient err :%s", err)
}
fmt.Println("connect success!")
fmt.Println("start read !")
// 1 device
resp1, err := client.Read("D", 7000, 1)
if err != nil {
log.Printf("unexpected mcp read err: %v", err)
}
if len(resp1) != 13 {
log.Printf("expected %v but actual is %v", 13, len(resp1))
}
if hex.EncodeToString(resp1) != strings.ReplaceAll("d000 00 ff ff03 0004 0000 0000 00", " ", "") {
log.Printf("expected %v but actual is %X", "d00000ffff0300040000000000", hex.EncodeToString(resp1))
}
fmt.Println(resp1)
time.Sleep(100 * time.Second)
}
两种方式都已验证 确认ok
五.参考资料
https://www.cnblogs.com/haozhanggy/p/12213159.html
https://blog.csdn.net/cmwanysys/article/details/106681255