github.com/simonvetter/modbus v1.6.0
rtu_transport.go
这个代码是实现Modbus RTU(远程终端单元)传输协议的一部分。Modbus RTU是一个串行传输协议,用于连接工业电子设备。以下是对这段代码的逐行分析:
- 导入包:
1import (
2 "fmt"
3 "io"
4 "log"
5 "time"
6)
这些是标准库中的包,用于格式化输出、I/O操作、日志记录和时间操作。
- 常量定义:
1const (
2 maxRTUFrameLength int = 256
3)
定义了一个常量,表示RTU帧的最大长度为256字节。
- rtuTransport结构体:
1type rtuTransport struct {
2 logger *logger
3 link rtuLink
4 timeout time.Duration
5 lastActivity time.Time
6 t35 time.Duration
7 t1 time.Duration
8}
这个结构体定义了RTU传输的主要属性。其中:
logger
: 用于日志记录的对象。link
: 表示物理链接的接口(例如,串行链接)。timeout
: 指定I/O操作的超时时间。lastActivity
: 记录最后一次活动的时间。t35
和t1
: 是RTU协议中特定的时间参数。
- rtuLink接口:
1type rtuLink interface {
2 Close() (error)
3 Read([]byte) (int, error)
4 Write([]byte) (int, error)
5 SetDeadline(time.Time) (error)
6}
这个接口定义了物理链接应该实现的方法,例如串行链接。
- newRTUTransport函数:
1func newRTUTransport(link rtuLink, addr string, speed uint, timeout time.Duration, customLogger *log.Logger) (rt *rtuTransport) { ... }
这个函数用于创建一个新的RTU传输对象。它根据传入的参数(如波特率和超时)来初始化传输对象的属性。
Modbus RTU协议中一个关键时间参数——帧之间的时间延迟,通常被称为t3.5
。这个时间参数是两个连续RTU帧之间的最小延迟时间。
在Modbus RTU协议中,帧的开始和结束由时间延迟(而不是特定的字符或标志)标识。这意味着两个连续帧之间必须有一个时间间隔,这个间隔至少是1.5个字符时间,但通常是3.5个字符时间。这就是t3.5
的来源。
具体到这段代码:
高波特率:
1if speed >= 19200 {
2 // for baud rates equal to or greater than 19200 bauds, a fixed value of
3 // 1750 uS is specified for t3.5.
4 rt.t35 = 1750 * time.Microsecond
5}
对于波特率大于或等于19200的情况,Modbus规范明确指定t3.5
为固定的1750微秒。这意味着无论波特率多高,t3.5
都是1750微秒。
低波特率:
1else {
2 // for lower baud rates, the inter-frame delay should be 3.5 character times
3 rt.t35 = (serialCharTime(speed) * 35) / 10
4}
对于低于19200的波特率,t3.5
是基于字符时间的3.5倍来计算的。serialCharTime(speed)
计算了在给定的波特率下发送一个字符所需的时间。所以,t3.5
是这个时间的3.5倍。
综上所述,这种处理方式确保了在不同的波特率下,帧之间的时间延迟符合Modbus RTU规范的要求。
- Close方法:
1func (rt *rtuTransport) Close() (err error) { ... }
这个方法用于关闭物理链接。
- ExecuteRequest方法:
1func (rt *rtuTransport) ExecuteRequest(req *pdu) (res *pdu, err error) { ... }
这个方法用于执行一个Modbus请求,并等待响应。它处理了发送请求、等待响应和处理错误的全部过程。
在ExecuteRequest
函数中,发送速率和帧间的时间间隔都是根据Modbus RTU协议的规定来控制的。特别地,函数考虑了两个关键的时间参数:t1
和t3.5
。
1). 字符时间 (t1
):
字符时间是传输单个字符所需的时间。在函数中,t1
是这样计算的:
1rt.t1 = serialCharTime(speed)
它基于给定的串口速度(波特率)来计算。
2). 帧间时间 (t3.5
):
t3.5
是两个连续的RTU帧之间的时间间隔。前面已经描述过了如何计算这个时间间隔。对于高波特率(>=19200),它是固定的1750微秒。对于低波特率,它是3.5个字符时间。
接下来看ExecuteRequest
函数中的关键部分,了解如何使用这些时间参数来控制发送速率:
1// if the line was active less than 3.5 char times ago,
2// let t3.5 expire before transmitting
3t = time.Since(rt.lastActivity.Add(rt.t35))
4if t < 0 {
5 time.Sleep(t * (-1))
6}
在这里,函数首先检查上次活动(发送或接收)到现在的时间是否小于t3.5
。如果是这样,它会等待足够的时间,确保两个连续帧之间的间隔至少是t3.5
。这确保了帧间的正确时间间隔,从而满足了Modbus RTU协议的要求。
然后,函数继续发送请求并等待响应。在发送请求之后,函数更新了lastActivity
时间戳,这样在下次发送请求时可以再次确保正确的帧间时间间隔。
总的来说,ExecuteRequest
函数中的逻辑确保了在发送每个请求之前都有足够的时间间隔,这是为了满足Modbus RTU协议中帧间时间的要求。这确保了在串口线上正确地区分每个独立的RTU帧。
- ReadRequest和WriteResponse方法:
1func (rt *rtuTransport) ReadRequest() (req *pdu, err error) { ... }
2func (rt *rtuTransport) WriteResponse(res *pdu) (err error) { ... }
这些方法用于读取Modbus请求和写入Modbus响应。但目前ReadRequest
还没有实现。
- readRTUFrame方法:
1func (rt *rtuTransport) readRTUFrame() (res *pdu, err error) { ... }
这个方法用于从物理链接读取一个完整的RTU帧。
基于readRTUFrame
方法,我们可以推断出Modbus RTU帧的结构。以下是基于方法中的代码对RTU帧的描述:
Unit Identifier (1 byte):
1unitId: rxbuf[0]
此字节表示设备的地址或标识符。在Modbus网络上,每个设备都有一个唯一的地址。
Function Code (1 byte):
1functionCode: rxbuf[1]
此字节表示所请求的操作类型。例如,读取寄存器、写入寄存器等。
Payload (变长):
1payload: rxbuf[2:3 + bytesNeeded - 2]
载荷部分包含请求或响应的实际数据。其长度根据功能码和特定请求/响应的类型而变化。
CRC (Cyclic Redundancy Check) (2 bytes):
这是RTU帧的最后两个字节,并用于错误检查。在readRTUFrame
方法中,CRC是通过以下方式计算和验证的:
1// compute the CRC on the entire frame, excluding the CRC
2crc.init()
3crc.add(rxbuf[0:3 + bytesNeeded - 2])
4
5// compare CRC values
6if !crc.isEqual(rxbuf[3 + bytesNeeded - 2], rxbuf[3 + bytesNeeded - 1]) {
7 err = ErrBadCRC
8 return
9}
综上所述,Modbus RTU帧的基本结构为:
[Unit Identifier] [Function Code] [Payload] [CRC]
其中,[Payload]
的长度是可变的,但总帧长度(包括CRC)不能超过maxRTUFrameLength
(在给定的代码中为256字节)。
- assembleRTUFrame方法:
1func (rt *rtuTransport) assembleRTUFrame(p *pdu) (adu []byte) { ... }
这个方法用于将PDU(协议数据单元)对象转换为一个RTU帧。
- expectedResponseLength方法:
这个函数
expectedResponseLength
的目的是为了计算给定功能码的Modbus RTU响应的预期长度。函数的输入参数是功能码responseCode
和响应长度responseLength
。函数返回预期的字节长度byteCount
和任何可能的错误err
。
以下是函数内部的处理逻辑:
读取操作: 对于以下的功能码:
fcReadHoldingRegisters
fcReadInputRegisters
fcReadCoils
fcReadDiscreteInputs
预期的响应长度是responseLength
。这是因为读取操作的响应通常包括一个字节计数,后面跟着实际的数据字节。所以,responseLength
直接表示了预期的字节长度。
单个和多个写入操作: 对于以下的功能码:
fcWriteSingleRegister
fcWriteMultipleRegisters
fcWriteSingleCoil
fcWriteMultipleCoils
预期的响应长度是固定的,值为3字节。这是因为写入操作的标准响应包括地址和值或数量。
掩码写入操作:
对于功能码fcMaskWriteRegister
,预期的响应长度是5字节。
异常响应:
当功能码的最高位被设置(即功能码与0x80
进行“或”操作)时,表示这是一个异常响应。对于以下的功能码:
fcReadHoldingRegisters | 0x80
fcReadInputRegisters | 0x80
fcReadCoils | 0x80
fcReadDiscreteInputs | 0x80
fcWriteSingleRegister | 0x80
fcWriteMultipleRegisters | 0x80
fcWriteSingleCoil | 0x80
fcWriteMultipleCoils | 0x80
fcMaskWriteRegister | 0x80
预期的响应长度是0字节。这是因为异常响应只有功能码和异常码,不包括其他数据。
默认情况:
如果给定的功能码不匹配上述任何一个,函数返回一个协议错误ErrProtocolError
。
总的来说,这个函数根据提供的功能码和响应长度来确定Modbus RTU响应的预期长度。这在读取Modbus RTU帧时是非常有用的,因为它可以告诉我们应该读取多少字节来完整地获取响应。
- 其他辅助函数:
例如
discard
和serialCharTime
等,它们提供了各种辅助功能,如计算期望的响应长度、丢弃无用的数据和计算发送一个字节所需的时间等。
总的来说,这段代码提供了Modbus RTU传输的基本实现,包括发送请求、读取响应、处理超时和错误等功能。