Skip to content

github.com/simonvetter/modbus rtu_transport.go源码分析

github.com/simonvetter/modbus v1.6.0

rtu_transport.go

这个代码是实现Modbus RTU(远程终端单元)传输协议的一部分。Modbus RTU是一个串行传输协议,用于连接工业电子设备。以下是对这段代码的逐行分析:

  1. 导入包:
1import (
2	"fmt"
3	"io"
4	"log"
5	"time"
6)

这些是标准库中的包,用于格式化输出、I/O操作、日志记录和时间操作。

  1. 常量定义:
1const (
2	maxRTUFrameLength	int = 256
3)

定义了一个常量,表示RTU帧的最大长度为256字节。

  1. 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: 记录最后一次活动的时间。
  • t35t1: 是RTU协议中特定的时间参数。
  1. rtuLink接口:
1type rtuLink interface {
2	Close()		(error)
3	Read([]byte)	(int, error)
4	Write([]byte)	(int, error)
5	SetDeadline(time.Time)	(error)
6}

这个接口定义了物理链接应该实现的方法,例如串行链接。

  1. 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规范的要求。

  1. Close方法:
1func (rt *rtuTransport) Close() (err error) { ... }

这个方法用于关闭物理链接。

  1. ExecuteRequest方法:
1func (rt *rtuTransport) ExecuteRequest(req *pdu) (res *pdu, err error) { ... }

这个方法用于执行一个Modbus请求,并等待响应。它处理了发送请求、等待响应和处理错误的全部过程。

ExecuteRequest函数中,发送速率和帧间的时间间隔都是根据Modbus RTU协议的规定来控制的。特别地,函数考虑了两个关键的时间参数:t1t3.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帧。

  1. ReadRequest和WriteResponse方法:
1func (rt *rtuTransport) ReadRequest() (req *pdu, err error) { ... }
2func (rt *rtuTransport) WriteResponse(res *pdu) (err error) { ... }

这些方法用于读取Modbus请求和写入Modbus响应。但目前ReadRequest还没有实现。

  1. 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字节)。

  1. assembleRTUFrame方法:
1func (rt *rtuTransport) assembleRTUFrame(p *pdu) (adu []byte) { ... }

这个方法用于将PDU(协议数据单元)对象转换为一个RTU帧。

  1. 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帧时是非常有用的,因为它可以告诉我们应该读取多少字节来完整地获取响应。

  1. 其他辅助函数: 例如discardserialCharTime等,它们提供了各种辅助功能,如计算期望的响应长度、丢弃无用的数据和计算发送一个字节所需的时间等。

总的来说,这段代码提供了Modbus RTU传输的基本实现,包括发送请求、读取响应、处理超时和错误等功能。

Related Posts

  1. github.com/simonvetter/modbus client.go源码分析
  2. github.com/simonvetter/modbus server.go源码分析
  3. github.com/simonvetter/modbus tcp_transport.go源码分析