- 1. 一、grid-x/modbus库概述
- 2. 二、安装与环境配置
- 3. 三、Modbus TCP客户端
- 4. 四、数据类型转换
- 5. 五、Modbus RTU串口通讯
- 6. 六、Modbus ASCII与UDP
- 7. 七、设备识别(function code43)
- 8. 八、完整工具类封装
- 9. 九、并发与多设备采集
- 10. 十、Modbus TCP服务端(slave)
- 11. 十一、常见问题与最佳实践
- 12. 十二、grid-x/modbus与其他语言Modbus库对比
grid-x/modbus是Go语言生态中最活跃、功能最完善的Modbus协议库之一,支持Modbus TCP、RTU、ASCII、UDP四种传输模式,提供基于context.Context的超时控制和简洁一致的API。本文详细介绍grid-x/modbus库的安装配置、TCP/RTU客户端实现、数据类型转换、并发采集、服务端实现以及完整的项目示例,帮助Go开发者快速掌握工业Modbus通讯开发。
一、grid-x/modbus库概述
1.1 核心特性
- 多协议支持:Modbus TCP、RTU、ASCII、UDP四种传输模式
- Context支持:所有操作接受context.Context,支持超时和取消
- 功能完整:支持01-16标准功能码,以及22(掩码写)、23(读写多个)、24(读FIFO)、43(设备识别)
- 主从兼备:既可作为客户端(master),也可作为服务端(slave)
- 故障容错:fail-fast设计,连接失败立即报错,不静默重试
- 跨平台:Windows、Linux、Mac OS、embeddedLinux均可运行
- 零依赖:仅依赖Go标准库,无第三方依赖
- 活跃维护:持续更新,社区活跃,文档完善
1.2 与其他Go Modbus库对比
| 库名 | 协议支持 | Context | 服务端 | 活跃度 | 推荐场景 |
|---|---|---|---|---|---|
| grid-x/modbus | TCP/RTU/ASCII/UDP | 支持 | 支持 | 高 | 新项目首选 |
| goburrow/modbus | TCP/RTU | 不支持 | 支持 | 中 | 遗留项目 |
| simonvetter/modbus | TCP/RTU | 支持 | 不支持 | 中 | 简单客户端 |
| aldas/go-modbus-client | TCP | 支持 | 不支持 | 中 | Request/响应模式 |
二、安装与环境配置
2.1 安装grid-x/modbus
# 初始化Go模块
go mod init modbus_demo
# 安装grid-x/modbus
go get github.com/grid-x/modbus
# 验证安装
go list -m all | grep grid-x
2.2 导入包
import (
"context"
"github.com/grid-x/modbus"
)
三、Modbus TCP客户端
3.1 Quick Start(简洁模式)
package main
import (
"context"
"fmt"
"log"
"github.com/grid-x/modbus"
)
func main() {
// 创建TCP客户端(简洁模式,自动管理连接)
client := modbus.TCPClient("192.168.1.100:502")
ctx := context.Background()
// read holding registers(function code03)
// 注意:返回值是[]byte,需要手动解析为uint16
results, err := client.ReadHoldingRegisters(ctx, 0, 10)
if err != nil {
log.Fatalf("读保持寄存器失败:%v", err)
}
// 解析寄存器值(每2字节为一个16位寄存器,高字节在前)
fmt.Printf("读取到%d字节数据:\n", len(results))
for i := 0; i < len(results); i += 2 {
value := uint16(results[i])<<8 | uint16(results[i+1])
fmt.Printf(" register%d:%d\n", i/2, value)
}
}
3.2 高级模式(手动管理连接)
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/grid-x/modbus"
)
func main() {
// 创建TCP Handler(可配置超时、slave address、日志等)
handler := modbus.NewTCPClientHandler("192.168.1.100:502")
handler.Timeout = 10 * time.Second
handler.SlaveID = 1
handler.Logger = log.New(os.Stdout, "modbus: ", log.LstdFlags)
ctx := context.Background()
// 手动建立连接(多次请求复用同一连接)
if err := handler.Connect(ctx); err != nil {
log.Fatalf("连接失败:%v", err)
}
defer handler.Close()
// 创建客户端
client := modbus.NewClient(handler)
// 连续多次请求(复用连接)
for i := 0; i < 5; i++ {
results, err := client.ReadHoldingRegisters(ctx, uint16(i*10), 10)
if err != nil {
log.Printf("No.%d次读取失败:%v", i, err)
continue
}
fmt.Printf("批次%d:读取到%dbyte\n", i, len(results))
}
}
3.3 完整功能码示例
package main
import (
"context"
"fmt"
"log"
"github.com/grid-x/modbus"
)
func main() {
client := modbus.TCPClient("192.168.1.100:502")
ctx := context.Background()
// === function code01:读线圈 ===
// 返回[]byte,每一位对应一个线圈状态
coils, err := client.ReadCoils(ctx, 0, 8)
if err != nil {
log.Fatalf("读线圈失败:%v", err)
}
fmt.Println("=== 读线圈(function code01)===")
for i := 0; i < 8; i++ {
byteIdx := i / 8
bitIdx := uint(i % 8)
state := (coils[byteIdx] >> bitIdx) & 0x01
fmt.Printf(" 线圈%d:%s\n", i, map[byte]string{0: "断开", 1: "吸合"}[state])
}
// === function code02:read discrete inputs ===
inputs, err := client.ReadDiscreteInputs(ctx, 0, 4)
if err != nil {
log.Fatalf("读离散输入失败:%v", err)
}
fmt.Println("\n=== read discrete inputs(function code02)===")
for i := 0; i < 4; i++ {
state := (inputs[i/8] >> uint(i%8)) & 0x01
fmt.Printf(" 输入%d:%d\n", i, state)
}
// === function code03:read holding registers ===
regs, err := client.ReadHoldingRegisters(ctx, 0, 10)
if err != nil {
log.Fatalf("读保持寄存器失败:%v", err)
}
fmt.Println("\n=== read holding registers(function code03)===")
for i := 0; i < len(regs); i += 2 {
value := uint16(regs[i])<<8 | uint16(regs[i+1])
fmt.Printf(" register%d:%d\n", i/2, value)
}
// === function code04:read input registers ===
inputRegs, err := client.ReadInputRegisters(ctx, 0, 2)
if err != nil {
log.Fatalf("读输入寄存器失败:%v", err)
}
fmt.Println("\n=== read input registers(function code04)===")
for i := 0; i < len(inputRegs); i += 2 {
value := uint16(inputRegs[i])<<8 | uint16(inputRegs[i+1])
fmt.Printf(" 输入寄存器%d:%d\n", i/2, value)
}
// === function code05:write single coil ===
// 0xFF00 = 吸合,0x0000 = 断开
_, err = client.WriteSingleCoil(ctx, 0, 0xFF00)
if err != nil {
log.Fatalf("写线圈失败:%v", err)
}
fmt.Println("\n=== write single coil(function code05)===")
fmt.Println(" 线圈0已置为吸合")
// === function code06:write single register ===
_, err = client.WriteSingleRegister(ctx, 100, 1234)
if err != nil {
log.Fatalf("写寄存器失败:%v", err)
}
fmt.Println("\n=== write single register(function code06)===")
fmt.Println(" register100已写入1234")
// === function code0F:write multiple coils ===
// value参数为[]byte,按位打包
coilData := []byte{0x05} // binary00000101 = 线圈0和线圈2吸合
_, err = client.WriteMultipleCoils(ctx, 0, 8, coilData)
if err != nil {
log.Fatalf("写多个线圈失败:%v", err)
}
fmt.Println("\n=== write multiple coils(function code0F)===")
fmt.Println(" 线圈0-7已批量写入")
// === function code10:write multiple registers ===
// value参数为[]byte,每2字节一个寄存器
regData := []byte{0x00, 0x64, 0x00, 0xC8, 0x01, 0x2C} // 100, 200, 300
_, err = client.WriteMultipleRegisters(ctx, 200, 3, regData)
if err != nil {
log.Fatalf("写多个寄存器失败:%v", err)
}
fmt.Println("\n=== write multiple registers(function code10)===")
fmt.Println(" register200-202已批量写入")
}
四、数据类型转换
grid-x/modbus的所有读方法返回[]byte,需要开发者手动解析为目标数据类型。以下提供完整的转换工具函数。
4.1 寄存器与字节转换
package modbusutil
import "encoding/binary"
// BytesToRegisters 将[]byte解析为[]uint16(大端序)
func BytesToRegisters(data []byte) []uint16 {
regs := make([]uint16, len(data)/2)
for i := 0; i < len(regs); i++ {
regs[i] = binary.BigEndian.Uint16(data[i*2 : i*2+2])
}
return regs
}
// RegistersToBytes 将[]uint16转换为[]byte(大端序)
func RegistersToBytes(regs []uint16) []byte {
data := make([]byte, len(regs)*2)
for i, v := range regs {
binary.BigEndian.PutUint16(data[i*2:i*2+2], v)
}
return data
}
// BytesToBools 将[]byte解析为[]bool(低位在前)
func BytesToBools(data []byte, count int) []bool {
bools := make([]bool, count)
for i := 0; i < count; i++ {
bools[i] = (data[i/8]>>uint(i%8))&0x01 == 1
}
return bools
}
// BoolsToBytes 将[]bool打包为[]byte
func BoolsToBytes(bools []bool) []byte {
byteCount := (len(bools) + 7) / 8
data := make([]byte, byteCount)
for i, v := range bools {
if v {
data[i/8] |= 1 << uint(i%8)
}
}
return data
}
4.2 32位数据类型(浮点数、整数)
import "math"
// BytesToFloat32ABCD Parse32位浮点数(ABCD大端序)
func BytesToFloat32ABCD(data []byte) float32 {
bits := uint32(data[0])<<24 | uint32(data[1])<<16 |
uint32(data[2])<<8 | uint32(data[3])
return math.Float32frombits(bits)
}
// BytesToFloat32CDAB Parse32位浮点数(CDAB字交换序)
func BytesToFloat32CDAB(data []byte) float32 {
bits := uint32(data[2])<<24 | uint32(data[3])<<16 |
uint32(data[0])<<8 | uint32(data[1])
return math.Float32frombits(bits)
}
// BytesToFloat32DCBA Parse32位浮点数(DCBA小端序)
func BytesToFloat32DCBA(data []byte) float32 {
bits := uint32(data[3])<<24 | uint32(data[2])<<16 |
uint32(data[1])<<8 | uint32(data[0])
return math.Float32frombits(bits)
}
// Float32ToBytesABCD 将float32转换为4字节(ABCD大端序)
func Float32ToBytesABCD(f float32) []byte {
bits := math.Float32bits(f)
return []byte{
byte(bits >> 24), byte(bits >> 16),
byte(bits >> 8), byte(bits),
}
}
// BytesToInt32 Parse32位有符号整数(大端序)
func BytesToInt32(data []byte) int32 {
return int32(uint32(data[0])<<24 | uint32(data[1])<<16 |
uint32(data[2])<<8 | uint32(data[3]))
}
// BytesToUint32 Parse32位无符号整数(大端序)
func BytesToUint32(data []byte) uint32 {
return uint32(data[0])<<24 | uint32(data[1])<<16 |
uint32(data[2])<<8 | uint32(data[3])
}
// Int32ToBytes 将int32转换为4字节(大端序)
func Int32ToBytes(v int32) []byte {
u := uint32(v)
return []byte{byte(u >> 24), byte(u >> 16), byte(u >> 8), byte(u)}
}
4.3 字节序对照表
| endianness | 说明 | 32位值0x41420000的存储方式 |
|---|---|---|
| ABCD | 大端序(标准) | reg[0]=0x4142, reg[1]=0x0000 |
| CDAB | Word exchange | reg[0]=0x0000, reg[1]=0x4142 |
| DCBA | 小端序 | reg[0]=0x0000, reg[1]=0x4241 |
| BADC | 字节交换 | reg[0]=0x4241, reg[1]=0x0000 |
五、Modbus RTU串口通讯
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/grid-x/modbus"
)
func main() {
// 创建RTU Handler
handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
handler.BaudRate = 9600 // Baud rate
handler.DataBits = 8 // data bit
handler.Parity = "N" // check digit:N=无,E=偶,O=奇
handler.StopBits = 1 // stop bit
handler.SlaveID = 1 // slave address
handler.Timeout = 5 * time.Second
ctx := context.Background()
// connect
if err := handler.Connect(ctx); err != nil {
log.Fatalf("RTU连接失败:%v", err)
}
defer handler.Close()
client := modbus.NewClient(handler)
// Read and hold register
results, err := client.ReadHoldingRegisters(ctx, 0, 10)
if err != nil {
log.Fatalf("Read failed:%v", err)
}
regs := make([]uint16, len(results)/2)
for i := range regs {
regs[i] = uint16(results[i*2])<<8 | uint16(results[i*2+1])
}
fmt.Printf("register value:%v\n", regs)
// Windows串口示例:
// handler := modbus.NewRTUClientHandler("COM3")
}
六、Modbus ASCII与UDP
// === Modbus ASCII ===
// ASCII模式使用可打印字符,适合调试和低可靠性链路
client := modbus.ASCIIClient("/dev/ttyUSB0")
// 配置方式与RTU相同,通过Handler设置
// === Modbus UDP ===
// UDP模式无连接,适合广播和简单查询
client := modbus.RTUOverUDPClient("192.168.1.255:502")
// === RTU over TCP ===
// 将RTU帧封装在TCP中(部分设备使用此模式)
client := modbus.RTUOverTCPClient("192.168.1.100:502")
// === ASCII over TCP ===
client := modbus.ASCIIOverTCPClient("192.168.1.100:502")
七、设备识别(function code43)
package main
import (
"context"
"fmt"
"log"
"github.com/grid-x/modbus"
)
func main() {
client := modbus.TCPClient("192.168.1.100:502")
ctx := context.Background()
// 读取基本设备信息(厂商名、产品代码、版本号)
deviceInfo, err := client.ReadDeviceIdentification(ctx, modbus.ReadDeviceIDCodeBasic)
if err != nil {
log.Fatalf("读取设备信息失败:%v", err)
}
fmt.Printf("设备信息:%+v\n", deviceInfo)
// 读取常规设备信息(厂商URL、产品名、型号、应用名)
regularInfo, err := client.ReadDeviceIdentification(ctx, modbus.ReadDeviceIDCodeRegular)
if err != nil {
log.Printf("读取常规信息失败:%v", err)
} else {
fmt.Printf("常规信息:%+v\n", regularInfo)
}
// 读取特定对象(对象ID=0通常是厂商名)
specificInfo, err := client.ReadDeviceIdentificationSpecificObject(ctx, 0)
if err != nil {
log.Printf("读取特定对象失败:%v", err)
} else {
fmt.Printf("对象0:%s\n", specificInfo)
}
}
八、完整工具类封装
package modbusutil
import (
"context"
"encoding/binary"
"math"
"sync"
"time"
"github.com/grid-x/modbus"
)
// ModbusClient 封装grid-x/modbus的工具类
type ModbusClient struct {
handler *modbus.TCPClientHandler
client modbus.Client
mu sync.Mutex
}
// NewModbusClient 创建TCP客户端
func NewModbusClient(address string, slaveID byte, timeout time.Duration) (*ModbusClient, error) {
handler := modbus.NewTCPClientHandler(address)
handler.SlaveID = slaveID
handler.Timeout = timeout
ctx := context.Background()
if err := handler.Connect(ctx); err != nil {
return nil, err
}
return &ModbusClient{
handler: handler,
client: modbus.NewClient(handler),
}, nil
}
// Close 关闭连接
func (m *ModbusClient) Close() error {
return m.handler.Close()
}
// ReadRegisters read holding registers,返回[]uint16
func (m *ModbusClient) ReadRegisters(ctx context.Context, addr, qty uint16) ([]uint16, error) {
m.mu.Lock()
defer m.mu.Unlock()
data, err := m.client.ReadHoldingRegisters(ctx, addr, qty)
if err != nil {
return nil, err
}
return BytesToRegisters(data), nil
}
// ReadInputRegisters read input registers
func (m *ModbusClient) ReadInputRegisters(ctx context.Context, addr, qty uint16) ([]uint16, error) {
m.mu.Lock()
defer m.mu.Unlock()
data, err := m.client.ReadInputRegisters(ctx, addr, qty)
if err != nil {
return nil, err
}
return BytesToRegisters(data), nil
}
// ReadCoils 读线圈,返回[]bool
func (m *ModbusClient) ReadCoils(ctx context.Context, addr, qty uint16) ([]bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
data, err := m.client.ReadCoils(ctx, addr, qty)
if err != nil {
return nil, err
}
return BytesToBools(data, int(qty)), nil
}
// ReadDiscreteInputs read discrete inputs
func (m *ModbusClient) ReadDiscreteInputs(ctx context.Context, addr, qty uint16) ([]bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
data, err := m.client.ReadDiscreteInputs(ctx, addr, qty)
if err != nil {
return nil, err
}
return BytesToBools(data, int(qty)), nil
}
// WriteRegister write single register
func (m *ModbusClient) WriteRegister(ctx context.Context, addr, value uint16) error {
m.mu.Lock()
defer m.mu.Unlock()
_, err := m.client.WriteSingleRegister(ctx, addr, value)
return err
}
// WriteRegisters write multiple registers
func (m *ModbusClient) WriteRegisters(ctx context.Context, addr uint16, values []uint16) error {
m.mu.Lock()
defer m.mu.Unlock()
data := RegistersToBytes(values)
_, err := m.client.WriteMultipleRegisters(ctx, addr, uint16(len(values)), data)
return err
}
// WriteCoil write single coil
func (m *ModbusClient) WriteCoil(ctx context.Context, addr uint16, value bool) error {
m.mu.Lock()
defer m.mu.Unlock()
var v uint16
if value {
v = 0xFF00
}
_, err := m.client.WriteSingleCoil(ctx, addr, v)
return err
}
// WriteCoils write multiple coils
func (m *ModbusClient) WriteCoils(ctx context.Context, addr uint16, values []bool) error {
m.mu.Lock()
defer m.mu.Unlock()
data := BoolsToBytes(values)
_, err := m.client.WriteMultipleCoils(ctx, addr, uint16(len(values)), data)
return err
}
// ReadFloat32 读32位浮点数(ABCD大端序)
func (m *ModbusClient) ReadFloat32(ctx context.Context, addr uint16) (float32, error) {
data, err := m.client.ReadHoldingRegisters(ctx, addr, 2)
if err != nil {
return 0, err
}
return BytesToFloat32ABCD(data), nil
}
// WriteFloat32 写32位浮点数(ABCD大端序)
func (m *ModbusClient) WriteFloat32(ctx context.Context, addr uint16, value float32) error {
data := Float32ToBytesABCD(value)
_, err := m.client.WriteMultipleRegisters(ctx, addr, 2, data)
return err
}
// ReadInt32 读32位有符号整数
func (m *ModbusClient) ReadInt32(ctx context.Context, addr uint16) (int32, error) {
data, err := m.client.ReadHoldingRegisters(ctx, addr, 2)
if err != nil {
return 0, err
}
return BytesToInt32(data), nil
}
// ReadString 读字符串(每个寄存器存2个字符)
func (m *ModbusClient) ReadString(ctx context.Context, addr, regCount uint16) (string, error) {
data, err := m.client.ReadHoldingRegisters(ctx, addr, regCount)
if err != nil {
return "", err
}
return string(data), nil
}
// 辅助函数(同前)
func BytesToRegisters(data []byte) []uint16 {
regs := make([]uint16, len(data)/2)
for i := range regs {
regs[i] = binary.BigEndian.Uint16(data[i*2 : i*2+2])
}
return regs
}
func RegistersToBytes(regs []uint16) []byte {
data := make([]byte, len(regs)*2)
for i, v := range regs {
binary.BigEndian.PutUint16(data[i*2:i*2+2], v)
}
return data
}
func BytesToBools(data []byte, count int) []bool {
bools := make([]bool, count)
for i := 0; i < count; i++ {
bools[i] = (data[i/8]>>uint(i%8))&0x01 == 1
}
return bools
}
func BoolsToBytes(bools []bool) []byte {
byteCount := (len(bools) + 7) / 8
data := make([]byte, byteCount)
for i, v := range bools {
if v {
data[i/8] |= 1 << uint(i%8)
}
}
return data
}
func BytesToFloat32ABCD(data []byte) float32 {
bits := uint32(data[0])<<24 | uint32(data[1])<<16 |
uint32(data[2])<<8 | uint32(data[3])
return math.Float32frombits(bits)
}
func Float32ToBytesABCD(f float32) []byte {
bits := math.Float32bits(f)
return []byte{byte(bits >> 24), byte(bits >> 16), byte(bits >> 8), byte(bits)}
}
func BytesToInt32(data []byte) int32 {
return int32(uint32(data[0])<<24 | uint32(data[1])<<16 |
uint32(data[2])<<8 | uint32(data[3]))
}
九、并发与多设备采集
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/grid-x/modbus"
)
// DeviceConfig 设备配置
type DeviceConfig struct {
Name string
Address string
SlaveID byte
}
// ReadResult 读取结果
type ReadResult struct {
Device string
Regs []uint16
Error error
}
func readDevice(ctx context.Context, cfg DeviceConfig, wg *sync.WaitGroup, results chan<- ReadResult) {
defer wg.Done()
handler := modbus.NewTCPClientHandler(cfg.Address)
handler.SlaveID = cfg.SlaveID
handler.Timeout = 3 * time.Second
if err := handler.Connect(ctx); err != nil {
results <- ReadResult{Device: cfg.Name, Error: err}
return
}
defer handler.Close()
client := modbus.NewClient(handler)
data, err := client.ReadHoldingRegisters(ctx, 0, 10)
if err != nil {
results <- ReadResult{Device: cfg.Name, Error: err}
return
}
regs := make([]uint16, len(data)/2)
for i := range regs {
regs[i] = uint16(data[i*2])<<8 | uint16(data[i*2+1])
}
results <- ReadResult{Device: cfg.Name, Regs: regs}
}
func main() {
devices := []DeviceConfig{
{Name: "PLC1", Address: "192.168.1.100:502", SlaveID: 1},
{Name: "PLC2", Address: "192.168.1.101:502", SlaveID: 1},
{Name: "PLC3", Address: "192.168.1.102:502", SlaveID: 1},
}
ctx := context.Background()
var wg sync.WaitGroup
results := make(chan ReadResult, len(devices))
// 并发读取所有设备
for _, dev := range devices {
wg.Add(1)
go readDevice(ctx, dev, &wg, results)
}
wg.Wait()
close(results)
// 处理结果
for r := range results {
if r.Error != nil {
fmt.Printf("device%sRead failed:%v\n", r.Device, r.Error)
} else {
fmt.Printf("device%s:%v\n", r.Device, r.Regs)
}
}
}
十、Modbus TCP服务端(slave)
grid-x/modbus库本身不直接提供服务端实现,但可以基于其协议编码/解码功能构建服务端。以下是一个简单的TCP从站实现示例。
package main
import (
"encoding/binary"
"fmt"
"log"
"net"
"sync"
)
// ModbusServer 简单的Modbus TCPslave
type ModbusServer struct {
coils []bool
inputs []bool
holdingRegs []uint16
inputRegs []uint16
mu sync.RWMutex
}
func NewModbusServer() *ModbusServer {
return &ModbusServer{
coils: make([]bool, 1024),
inputs: make([]bool, 1024),
holdingRegs: make([]uint16, 1024),
inputRegs: make([]uint16, 1024),
}
}
func (s *ModbusServer) handleConnection(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 260) // Modbus TCP最大ADULength
for {
n, err := conn.Read(buf)
if err != nil {
return
}
if n < 8 {
continue
}
// ParseMBAP Head
transID := binary.BigEndian.Uint16(buf[0:2])
// protoID := binary.BigEndian.Uint16(buf[2:4]) // 固定0x0000
// length := binary.BigEndian.Uint16(buf[4:6])
unitID := buf[6]
funcCode := buf[7]
// 处理请求
response := s.processRequest(funcCode, buf[8:n])
// 构建响应MBAP Head
resp := make([]byte, 7+len(response))
binary.BigEndian.PutUint16(resp[0:2], transID)
binary.BigEndian.PutUint16(resp[2:4], 0x0000) // ProtocolID
binary.BigEndian.PutUint16(resp[4:6], uint16(1+len(response)))
resp[6] = unitID
copy(resp[7:], response)
conn.Write(resp)
}
}
func (s *ModbusServer) processRequest(funcCode byte, data []byte) []byte {
s.mu.Lock()
defer s.mu.Unlock()
switch funcCode {
case 0x03: // read holding registers
addr := binary.BigEndian.Uint16(data[0:2])
qty := binary.BigEndian.Uint16(data[2:4])
byteCount := qty * 2
resp := make([]byte, 2+byteCount)
resp[0] = funcCode
resp[1] = byte(byteCount)
for i := uint16(0); i < qty; i++ {
binary.BigEndian.PutUint16(resp[2+i*2:], s.holdingRegs[addr+i])
}
return resp
case 0x06: // write single register
addr := binary.BigEndian.Uint16(data[0:2])
value := binary.BigEndian.Uint16(data[2:4])
s.holdingRegs[addr] = value
resp := make([]byte, 5)
resp[0] = funcCode
binary.BigEndian.PutUint16(resp[1:3], addr)
binary.BigEndian.PutUint16(resp[3:5], value)
return resp
case 0x01: // 读线圈
addr := binary.BigEndian.Uint16(data[0:2])
qty := binary.BigEndian.Uint16(data[2:4])
byteCount := (qty + 7) / 8
resp := make([]byte, 2+byteCount)
resp[0] = funcCode
resp[1] = byte(byteCount)
for i := uint16(0); i < qty; i++ {
if s.coils[addr+i] {
resp[2+i/8] |= 1 << (i % 8)
}
}
return resp
case 0x05: // write single coil
addr := binary.BigEndian.Uint16(data[0:2])
value := binary.BigEndian.Uint16(data[2:4])
s.coils[addr] = value == 0xFF00
resp := make([]byte, 5)
resp[0] = funcCode
binary.BigEndian.PutUint16(resp[1:3], addr)
binary.BigEndian.PutUint16(resp[3:5], value)
return resp
default:
// Exception Response:非法功能码
return []byte{funcCode | 0x80, 0x01}
}
}
func main() {
server := NewModbusServer()
// 设置初始值
server.holdingRegs[0] = 100
server.holdingRegs[1] = 200
server.coils[0] = true
listener, err := net.Listen("tcp", ":502")
if err != nil {
log.Fatalf("监听失败:%v", err)
}
defer listener.Close()
fmt.Println("Modbus TCP服务端已启动,监听:502")
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("接受连接失败:%v", err)
continue
}
go server.handleConnection(conn)
}
}
十一、常见问题与最佳实践
11.1 common problems
- 返回数据是[]byte不是[]uint16:grid-x/modbus的设计哲学是返回原始字节,由调用方解析。使用本文提供的BytesToRegisters工具函数转换
- 连接超时:检查IPaddress、端口、防火墙,确认设备在线。通过handler.Timeout设置超时时间
- Exception Response:检查SlaveID是否正确,功能码和地址是否在设备支持范围内
- 浮点数解析错误:确认设备字节序(ABCD/CDAB/DCBA/BADC),使用对应的转换函数
- RTU串口打不开:检查串口设备名,确认权限(Linux需加入dialout组)
- 并发冲突:同一个handler不要在多个goroutine中同时使用,加互斥锁或每个goroutine用独立连接
- 简洁模式每次新建连接:modbus.TCPClient()简洁模式每次请求都建立新连接,高频请求请用高级模式复用连接
11.2 最佳实践
- 使用context控制超时:所有操作传入带超时的context,避免永久阻塞
- 高频请求复用连接:使用NewTCPClientHandler + Connect/Close手动管理连接,减少TCP握手开销
- 批量读取:尽量一次读取多个连续寄存器,减少通讯次数
- 合理设置超时:局域网1-3秒,RTUserial port3-5秒
- 并发加锁:多goroutine共享同一客户端时使用sync.Mutex保护
- 错误处理:每次调用都检查error,不要忽略
- 日志记录:开发阶段设置handler.Logger,查看原始通讯数据
- 资源释放:使用defer handler.Close()确保连接释放
- 地址规划:提前规划寄存器地址,编写地址映射文档
- 测试验证:先用Modbus Poll确认设备通讯正常,再集成Go代码
十二、grid-x/modbus与其他语言Modbus库对比
| 语言 | 库名 | 返回类型 | Context支持 | 部署复杂度 |
|---|---|---|---|---|
| Go | grid-x/modbus | []byte(需手动解析) | 支持 | 低(单二进制) |
| C | libmodbus | uint16_t数组 | 不支持 | 中(需编译) |
| Python | pymodbus | 对象(.registers/.bits) | 支持(asyncio) | 低(解释执行) |
| Java | EasyModbus | int[]/boolean[] | 不支持 | 高(需JVM) |
| C# | NModbus | ushort[]/bool[] | 支持(async/await) | 中(需.NET) |
grid-x/modbus以其简洁的API、完善的协议支持和优秀的并发性能,成为Go语言工业Modbus开发的首选库。本文提供的工具类封装可直接用于项目开发,建议先使用Modbus Poll确认设备通讯参数,再将Go代码集成到实际系统中。
Leave a Reply