基本原理
Modbus是PLC常用的通讯协议,经常用于与HMI通信。通过对此协议的分析,可以如同三菱MC协议一样,利用来与PC结合,发挥更大的作用。
Modbus 是一个应用层的通讯协议,位于 OSI 的第七层,在总线或者网络上的不同设备之间的,通过 客户端/服务端 的方式通讯,默认使用502端口进行通讯。
通讯示例:
// Remember: Big-endian values!
var responseADU = []byte{
// MBAP HEADER:
0x00, 0xFF, // Tx ID #255, typically an incremental value
0x00, 0x00, // Protocol ID, always 0
0x00, 0x0B, // Length of the following data (8+3=11)
0x00, // Unit identifier, 0 unless a Gateway is used
// PROTOCOL DATA UNIT:
0x03, // Function Code 03: Read Holding Registers
0x08, // Byte count of the following data (4*2=8)
0x00, 0x0A, // First register; value 10
0x0A, 0x00, // Second register; value 2560
0xFF, 0xFF, // Third register; value 65535 or -1 when signed.
0x00, 0x01, // Fourth register; value 1
}
功能码说明
代码 | 中文名称 | 位操作/字操作 | 操作数量 |
---|---|---|---|
01h | 读线圈状态 | 位操作 | 单个或多个 |
02h | 读离散输入状态(只能读到0或1) | 位操作 | 单个或多个 |
03h | 读保持寄存器(保持寄存器可以通过06h功能写入) | 字操作 | 单个或多个 |
04h | 读输入寄存器(输入寄存器只能读取,不能通过06h功能写入) | 字操作 | 单个或多个 |
05h | 写单个线圈(线圈表示用来控制输出IO控制) | 位操作 | 单个 |
06h | 写单个保持寄存器 | 字操作 | 单个 |
0Fh | 写多个线圈 | 位操作 | 多个 |
10h | 写多个保持寄存器 | 字操作 | 多个 |
调试工具
Modbus Slave version 6.0.2
官方工具,不太好用,检测地址长度不能超过125,无法正确显示高低位转换的数据
链接: https://pan.baidu.com/s/19tEx6KzM0bjbCv342cA3zg
提取码: z44q
Modbus/TCP Master
优点:可以同时显示多种类型的数据比较方便
缺点:检测地址长度不能超过125;无法正确显示高低位转换的数据;无法自动刷新
ModScan 32 version 8.A00-10
应该是最好用的工具了,长度没有限制,可以自动刷新,可以自定义数据的类型,并且可以处理高低位转换的场景
选择 connection -> connect
录入 modbus tcp server 的地址和端口号
setup -> Display Options -> floating point -> 选择高位优先,还是低位优先
ngrok
在车间连接外网的机器上,把502端口映射到外网,方便远程调试。
官网 (https://dashboard.ngrok.com/get-started/setup) 注册账号,下载客户端
# 进入 rgrok 命令行工具所在目录
# 配置token
ngrok config add-authtoken {ngrok token}
# 映射modbus tcp server 到外网
ngrok tcp {modbus tcp server ip}:502
高低位的问题
在plc中,有的是低位优先,也有的是高位优先,比如:
如果一个Int类型数组,占用4个字节,4个字节顺序为ABCD,那么采用big-endian大端字节顺序,那么在内存中即为ABCD,如果采用small-endian小端字节顺序,那么在内存中存储即为DCBA,但是在实际应用中,还有可能出现BADC或者CDAB的情况,因此我们在大小端的基础上做了一下扩展,定义了4种不同字节顺序,采用枚举类型表示,代码如下所示:
ABCD = 0, // 大端形式
BADC = 1, // 单字反转
CDAB = 2, // 双字反转
DCBA = 3, // 小端形式
示例程序 golang
import (
"bytes"
"encoding/binary"
"fmt"
"testing"
"time"
"github.com/simonvetter/modbus"
)
func Test_Modbus(t *testing.T) {
var client *modbus.ModbusClient
var err error
// for a TCP endpoint
// (see examples/tls_client.go for TLS usage and options)
client, err = modbus.NewClient(&modbus.ClientConfiguration{
URL: "tcp://ip:port", // 可以直接使用ngrok映射到外网的地址
Timeout: 1 * time.Second,
})
// note: use udp:// for modbus TCP over UDP
if err != nil {
fmt.Println(err.Error())
}
err = client.Open()
if err != nil {
fmt.Println(err.Error())
}
// uint
fmt.Println("uint read and write")
for i := 0; i <= 7; i++ {
address := uint16(i)
value := uint16(1)
v, err := client.ReadRegister(address, modbus.HOLDING_REGISTER)
if err != nil {
fmt.Println(err.Error())
}
fmt.Println("address:", address, ",old value:", v)
err = client.WriteRegister(address, value)
if err != nil {
fmt.Println(err.Error())
}
v, err = client.ReadRegister(address, modbus.HOLDING_REGISTER)
if err != nil {
fmt.Println(err.Error())
}
fmt.Println("address:", address, ",new value:", v)
}
// float
fmt.Println("float read and write")
bytes, err := client.ReadBytes(6, 4, modbus.HOLDING_REGISTER)
if err != nil {
fmt.Println(err.Error())
}
f, _ := bytesToFloat32(bytes)
fmt.Println("old value:", f)
err = client.WriteBytes(6, float32ToBytes(123.456))
if err != nil {
fmt.Println(err.Error())
}
bytes, err = client.ReadBytes(6, 4, modbus.HOLDING_REGISTER)
if err != nil {
fmt.Println(err.Error())
}
f, _ = bytesToFloat32(bytes)
fmt.Println("new value:", f)
}
参考资料
modbus tcp: https://www.ad.siemens.com.cn/productportal/Prods/published/Comm/Comm_9.1/Comm_9.1.html
golang modbus: https://github.com/goburrow/modbus
modbus server:https://blog.csdn.net/weixin_42330983/article/details/124860023
Modscan32 https://www.sohu.com/a/443539826_651846
https://github.com/ffffffff0x/1earn/blob/master/1earn/Security/ICS/实验/Modbus仿真环境搭建.md
读写:https://github.com/simonvetter/modbus
文章评论