🎯 学习目标 :全面掌握Modbus协议的原理、应用和实战技巧,成为工业通信领域的专家
🚀 协议概述 Modbus 是工业自动化领域使用最广泛的通信协议之一,具有简单、可靠、开放的特点。
协议特点 :开放性强、实现简单、设备兼容性好、在工业控制系统中应用广泛
🏗️ 架构模式 👥 主从架构 🌐 网络拓扑 📊 数据模型
基本概念:
🖥️ 主站(Master) :相当于客户端,主动发起通信请求
🔧 从站(Slave) :相当于服务端,被动响应主站请求
📡 通信特点 :半双工通信,主站轮询从站
通信流程 :主站发送请求 → 从站处理并响应 → 主站接收数据
支持的网络类型:
🔌 Modbus RTU :基于RS-485串行通信
🌍 Modbus TCP :基于以太网TCP/IP
⚡ Modbus ASCII :ASCII编码的串行通信
四种数据类型:
🔘 线圈(Coils) :单比特可读写数据
📥 离散输入(Discrete Inputs) :单比特只读数据
📋 保持寄存器(Holding Registers) :16位可读写数据
📤 输入寄存器(Input Registers) :16位只读数据
📚 基础概念理解 关键术语 ● 1字节(位) = 1byte(一个字节) = 8bit
1个字节可以表示0-255的数值
在Modbus中,地址、功能码等都以字节为单位
● 串行链路 :数据一位一位按顺序传输的方式
在广域网中提供远距离传输
分为”异步”和”同步”传输方式
以字节为单位进行数据传输
广域网(WAN):连接不同地区局域网的远程网络,覆盖范围从几十公里到几千公里
● PDU(协议数据单元) :网络通信中传输的数据单位
包含控制信息和用户数据
在不同网络层级有不同名称:包(Packet)、段(Segment)、帧(Frame)
Modbus中定义了与通信层无关的简单协议数据单元
● ASCII :美国信息交换标准代码,与UTF-8都是字符编码方式
🔧 Modbus操作对象(通用数据类型)
对象类型
地址范围
数据位数
权限
说明
线圈
00001-09999
1位
读写
PLC输出位,开关量
离散输入
10001-19999
1位
只读
PLC输入位,开关量
输入寄存器
30001-39999
16位
只读
模拟量输入
保持寄存器
40001-49999
16位
读写
模拟量输出
通用数据结构说明 :
1位数据 :线圈和离散输入,存储开关状态(0/1)
16位数据 :寄存器类型,存储数值(0-65535)
地址编码 :前缀数字区分不同类型,实际传输时使用相对地址
🔢 功能码详解
功能码
名称
操作对象
描述
01
读线圈状态
00001-09999
读取离散输出状态
02
读离散输入
10001-19999
读取离散输入状态
03
读保持寄存器
40001-49999
读取模拟输出值
04
读输入寄存器
30001-39999
读取模拟输入值
05
写单个线圈
00001-09999
写入单个线圈
06
写单个寄存器
40001-49999
写入单个寄存器
0F(15)
写多个线圈
00001-09999
写入多个线圈
10(16)
写多个寄存器
40001-49999
写入多个寄存器
📦 数据帧格式 通用PDU结构
MBAP报文头(TCP专用)
事务处理标识
协议标识
长度
单元标识符
2字节
2字节
2字节
1字节
字节含义说明 :
事务处理标识 :报文序列号,每次通信后加1
协议标识符 :00 00表示Modbus TCP协议
长度 :后续数据长度(字节数)
单元标识符 :设备地址
1. Modbus RTU帧模式
设备地址
功能代码
数据格式
CRC16校验
1字节
1字节
N字节
2字节
读/写单个操作 :
设备地址
功能代码
起始地址(高位)
起始地址(低位)
数量(高位)
数量(低位)
CRC16校验
1字节
1字节
1字节
1字节
1字节
1字节
2字节
读/写多个操作 :
设备地址
功能代码
起始地址(高位)
起始地址(低位)
数量(高位)
数量(低位)
字节数
数据
CRC16校验
1字节
1字节
1字节
1字节
1字节
1字节
1字节
N字节
2字节
2. Modbus ASCII帧模式
帧头
设备地址
功能码
数据
校验码LRC
回车
换行
1字符
2字符
2字符
N字符
2字符
1字符
1字符
:
01
03
00 00 00 01
FB
\r
\n
3. Modbus TCP帧模式
MBAP报文头
功能码
数据
7字节
1字节
N字节
🔧 通用数据封装思路 三种帧格式的共同点 :
核心数据 :设备地址、功能码、起始地址、数量/数值
数据部分 :根据功能码决定是否包含字节数和具体数值
校验方式 :RTU用CRC16,ASCII用LRC,TCP无需校验
封装策略 :
先构建通用的PDU部分(功能码+数据)
根据传输模式添加不同的头部和校验
字节序按照大端模式(高位在前)
💻 代码实现 RTU模式构建 1 2 3 4 5 6 fun build ( function: ModbusFunction , slave: Int , startAddress: Int , count: Int , value: Int ? = null , values: IntArray ? = null ) : ByteArray { val output = buildOutput(slave, function, startAddress, count, value, values) toCalculateCRC16(output.toByteArray(), output) return output.toByteArray() }
TCP模式构建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private var mTransactionId: Int = 0 private val mProtocol: Int = 0 fun build (function: ModbusFunction , slave: Int , startAddress: Int , count: Int ,value: Int ? = null , values: IntArray ? = null , transactionId: Int = mTransactionId) : ByteArray { val pdu = buildOutput(slave, function, startAddress, count, value, values, isTcp = true ) val size = pdu.size() val mbap = BytesOutput() mbap.writeInt16(transactionId) mbap.writeInt16(mProtocol) mbap.writeInt16(size + 1 ) mbap.writeInt8(slave) mbap.write(pdu.toByteArray()) mTransactionId++ return mbap.toByteArray() }
ASCII模式构建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private val HEAD = 0x3A private val END = byteArrayOf(0x0d , 0x0A )fun build ( function: ModbusFunction , slave: Int , startAddress: Int , count: Int , value: Int ? = null , values: IntArray ? = null ) : ByteArray { val bytes = BytesOutput() val output = buildOutput(slave, function, startAddress, count, value, values).toByteArray() val pLRC = fromAsciiInt8(toCalculateLRC(output)) val outputAscii = toAsciiHexBytes(output) bytes.writeInt8(HEAD) bytes.writeBytes(outputAscii, outputAscii.size) bytes.writeInt8(pLRC.first) bytes.writeInt8(pLRC.second) bytes.writeBytes(END, END.size) return bytes.toByteArray() }
核心构建函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 open class KModbus protected constructor () { fun buildOutput (slave: Int , function: ModbusFunction , startAddress: Int , count: Int , value: Int ?, values: IntArray ?, isTcp: Boolean = false ) : BytesOutput { when { slave !in 0. .0xFF -> throw ModbusException(ModbusErrorType.ModbusInvalidArgumentError, "Invalid slave $slave " ) startAddress !in 0. .0xFFFF -> throw ModbusException(ModbusErrorType.ModbusInvalidArgumentError, "Invalid startAddress $startAddress " ) count !in 1. .0xFF -> throw ModbusException(ModbusErrorType.ModbusInvalidArgumentError, "Invalid count $count " ) } val output = BytesOutput() if (!isTcp) { output.writeInt8(slave) } when (function) { ModbusFunction.READ_COILS, ModbusFunction.READ_DISCRETE_INPUTS, ModbusFunction.READ_INPUT_REGISTERS, ModbusFunction.READ_HOLDING_REGISTERS -> { output.writeInt8(function.mCode) output.writeInt16(startAddress) output.writeInt16(count) } ModbusFunction.WRITE_SINGLE_COIL, ModbusFunction.WRITE_SINGLE_REGISTER -> { var valueCopy = value ?: throw ModbusException(ModbusErrorType.ModbusInvalidArgumentError, "Function Is $function \t , Data must be passed in!" ) if (function == ModbusFunction.WRITE_SINGLE_COIL) if (value != 0 ) valueCopy = 0xff00 output.writeInt8(function.mCode) output.writeInt16(startAddress) output.writeInt16(valueCopy) } ModbusFunction.WRITE_HOLDING_REGISTERS -> { output.writeInt8(function.mCode) output.writeInt16(startAddress) output.writeInt16(count) output.writeInt8(2 * count) (values ?: throw ModbusException(ModbusErrorType.ModbusInvalidArgumentError, "Function Is $function , Data must be passed in!" )).forEach { output.writeInt16(it) } } ModbusFunction.WRITE_COILS -> { if (values == null || values.isEmpty()) throw ModbusException(ModbusErrorType.ModbusInvalidArgumentError, "Function Is $function , Data must be passed in and cannot be empty!" ) output.writeInt8(function.mCode) output.writeInt16(startAddress) output.writeInt16(count) output.writeInt8((count + 7 ) shr 3 ) val chunkedValues = values.toList().chunked(8 ) for (chunk in chunkedValues) { output.writeInt8(toDecimal(chunk.reversed().toIntArray())) } } } return output } fun toCalculateCRC16 (output: BytesOutput ) : BytesOutput { output.writeInt16Reversal(CRC16.compute(output.toByteArray())) return output } fun toCalculateLRC (data : ByteArray ) : Int { var iTmp = 0 for (x in data ) { iTmp += x.toInt() } iTmp %= 256 iTmp = (iTmp.inv() + 1 ) and 0xFF return iTmp } private fun toDecimal (data : IntArray ) : Int { var result = 0 for (bit in data ) { if (bit != 0 && bit != 1 ) { return -1 } result = (result shl 1 ) + bit } return result } }
📖 开源项目及参考资料
💡 总结 : 本文介绍了Modbus协议的基本概念、数据帧格式和Kotlin实现方法,适合工业自动化入门学习。