Modbus协议开发实战指南

Modbus协议开发实战指南

目录

  1. Modbus协议概述
  2. [Modbus RTU开发实战](#Modbus RTU开发实战)
  3. [Modbus TCP开发实战](#Modbus TCP开发实战)
  4. 从站设备开发
  5. 常见问题与调试技巧

1. Modbus协议概述

Modbus是一种工业通信协议,由Modicon公司于1979年发布。它广泛应用于工业自动化领域,具有简单、开放、可靠的特点。

1.1 协议类型

  • Modbus RTU: 基于串行通信(RS232/RS485),二进制编码
  • Modbus ASCII: 基于串行通信,ASCII编码
  • Modbus TCP: 基于以太网,TCP/IP协议

1.2 功能码

功能码 名称 描述
0x01 读线圈 读取单个或多个输出线圈状态
0x02 读离散输入 读取单个或多个输入线圈状态
0x03 读保持寄存器 读取单个或多个保持寄存器值
0x04 读输入寄存器 读取单个或多个输入寄存器值
0x05 写单个线圈 写入单个输出线圈
0x06 写单个寄存器 写入单个保持寄存器
0x0F 写多个线圈 写入多个输出线圈
0x10 写多个寄存器 写入多个保持寄存器

2. Modbus RTU开发实战

2.1 RTU帧格式

Modbus RTU帧结构如下:

复制代码
[设备地址(1字节)][功能码(1字节)][数据域(N字节)][CRC校验(2字节)]

2.2 Python实现RTU主站

以下代码使用pymodbus库实现Modbus RTU主站:

python 复制代码
# 导入必要的库
from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusException
import time

# 创建Modbus RTU客户端
# 参数说明:
# - port: 串口名称,Windows下为COMx,Linux下为/dev/ttyUSBx
# - baudrate: 波特率,常见值为9600、19200、38400等
# - parity: 校验位,'N'无校验,'E'偶校验,'O'奇校验
# - stopbits: 停止位,通常为1或2
# - bytesize: 数据位,通常为8
client = ModbusSerialClient(
    port='COM3',
    baudrate=9600,
    parity='N',
    stopbits=1,
    bytesize=8,
    timeout=1  # 超时时间(秒)
)

# 连接到串口
if client.connect():
    print("成功连接到Modbus RTU设备")
else:
    print("连接失败,请检查串口设置")
    exit(1)

# 设备地址(从站ID)
DEVICE_ID = 1

try:
    # 读取保持寄存器(功能码0x03)
    # 参数说明:
    # - address: 起始寄存器地址
    # - count: 要读取的寄存器数量
    # - slave: 从站地址
    result = client.read_holding_registers(
        address=0,
        count=10,
        slave=DEVICE_ID
    )
    
    if not result.isError():
        # 获取读取到的寄存器值
        registers = result.registers
        print(f"成功读取到 {len(registers)} 个寄存器:")
        for i, value in enumerate(registers):
            print(f"  寄存器 {i}: {value}")
    else:
        print(f"读取失败: {result}")

    # 写入单个寄存器(功能码0x06)
    # 参数说明:
    # - address: 寄存器地址
    # - value: 要写入的值(0-65535)
    # - slave: 从站地址
    write_result = client.write_register(
        address=0,
        value=100,
        slave=DEVICE_ID
    )
    
    if not write_result.isError():
        print("成功写入寄存器")
    else:
        print(f"写入失败: {write_result}")

    # 写入多个寄存器(功能码0x10)
    # 参数说明:
    # - address: 起始寄存器地址
    # - values: 要写入的值列表
    # - slave: 从站地址
    write_multiple_result = client.write_registers(
        address=10,
        values=[1, 2, 3, 4, 5],
        slave=DEVICE_ID
    )
    
    if not write_multiple_result.isError():
        print("成功写入多个寄存器")
    else:
        print(f"写入失败: {write_multiple_result}")

    # 读取输入寄存器(功能码0x04)
    # 输入寄存器是只读的,通常用于采集传感器数据
    input_result = client.read_input_registers(
        address=0,
        count=5,
        slave=DEVICE_ID
    )
    
    if not input_result.isError():
        print(f"输入寄存器值: {input_result.registers}")

except ModbusException as e:
    print(f"Modbus异常: {e}")
finally:
    # 关闭连接
    client.close()
    print("连接已关闭")

2.3 Python实现RTU从站

以下代码实现一个简单的Modbus RTU从站:

python 复制代码
# 导入必要的库
from pymodbus.server import StartSerialServer
from pymodbus.datastore import ModbusServerContext, ModbusSlaveContext
from pymodbus.datastore import ModbusSequentialDataBlock
import logging

# 配置日志
logging.basicConfig()
log = logging.getLogger()
log.setLevel(logging.DEBUG)

# 创建数据存储块
# 参数说明:
# - 第一个参数是起始地址的初始值
# - 后续参数是各地址的值
# 这里创建200个保持寄存器,初始值为0
store = ModbusSlaveContext(
    # 保持寄存器(可读写)
    di=ModbusSequentialDataBlock(0, [0]*100),  # 离散输入
    co=ModbusSequentialDataBlock(0, [0]*100),  # 线圈
    hr=ModbusSequentialDataBlock(0, [0]*100),  # 保持寄存器
    ir=ModbusSequentialDataBlock(0, [0]*100)   # 输入寄存器
)

# 创建服务器上下文
context = ModbusServerContext(slaves=store, single=True)

# 启动串口服务器
# 参数说明:
# - port: 串口名称
# - baudrate: 波特率
# - context: 数据存储上下文
StartSerialServer(
    context=context,
    port='COM4',
    baudrate=9600,
    parity='N',
    stopbits=1,
    bytesize=8
)

3. Modbus TCP开发实战

3.1 TCP帧格式

Modbus TCP帧结构如下:

复制代码
[MBAP头(7字节)][PDU(N字节)]

MBAP头结构:
- 事务标识符(2字节)
- 协议标识符(2字节)
- 长度(2字节)
- 单元标识符(1字节)

3.2 Python实现TCP主站

python 复制代码
# 导入必要的库
from pymodbus.client import ModbusTcpClient
from pymodbus.exceptions import ModbusException
import time

# 创建Modbus TCP客户端
# 参数说明:
# - host: 服务器IP地址
# - port: 服务器端口号,默认为502
# - timeout: 超时时间(秒)
client = ModbusTcpClient(
    host='192.168.1.100',
    port=502,
    timeout=3
)

# 连接到服务器
if client.connect():
    print("成功连接到Modbus TCP服务器")
else:
    print("连接失败,请检查IP地址和端口")
    exit(1)

# 设备地址
DEVICE_ID = 1

try:
    # 读取保持寄存器
    result = client.read_holding_registers(
        address=0,
        count=10,
        slave=DEVICE_ID
    )
    
    if not result.isError():
        print(f"读取成功,寄存器值: {result.registers}")
        
        # 处理寄存器数据
        # 示例:将两个16位寄存器组合成一个32位浮点数
        if len(result.registers) >= 2:
            # 假设前两个寄存器组成一个32位整数
            value_32 = (result.registers[0] << 16) | result.registers[1]
            print(f"组合后的32位值: {value_32}")
    else:
        print(f"读取失败: {result}")

    # 读取线圈(功能码0x01)
    # 线圈状态:True表示ON,False表示OFF
    coils_result = client.read_coils(
        address=0,
        count=16,
        slave=DEVICE_ID
    )
    
    if not coils_result.isError():
        coils = coils_result.bits
        print("线圈状态:")
        for i, state in enumerate(coils[:16]):
            print(f"  线圈 {i}: {'ON' if state else 'OFF'}")

    # 写入单个线圈(功能码0x05)
    coil_result = client.write_coil(
        address=0,
        value=True,  # True=ON, False=OFF
        slave=DEVICE_ID
    )
    
    if not coil_result.isError():
        print("成功写入线圈状态为ON")

    # 写入多个线圈(功能码0x0F)
    write_coils_result = client.write_coils(
        address=0,
        values=[True, False, True, False],
        slave=DEVICE_ID
    )
    
    if not write_coils_result.isError():
        print("成功写入多个线圈")

    # 读取离散输入(功能码0x02)
    # 离散输入是只读的
    discrete_result = client.read_discrete_inputs(
        address=0,
        count=8,
        slave=DEVICE_ID
    )
    
    if not discrete_result.isError():
        print(f"离散输入状态: {discrete_result.bits[:8]}")

except ModbusException as e:
    print(f"Modbus异常: {e}")
finally:
    client.close()
    print("连接已关闭")

3.3 Python实现TCP从站

python 复制代码
# 导入必要的库
from pymodbus.server import StartTcpServer
from pymodbus.datastore import ModbusServerContext, ModbusSlaveContext
from pymodbus.datastore import ModbusSequentialDataBlock
import threading
import time

# 创建自定义数据块类,支持动态更新数据
class CustomDataBlock(ModbusSequentialDataBlock):
    def __init__(self, address, values):
        super().__init__(address, values)
        self.start_time = time.time()
    
    # 重写get方法,模拟实时数据变化
    def getValues(self, address, count=1):
        values = super().getValues(address, count)
        # 模拟数据:让第一个寄存器的值随时间变化
        if address == 0:
            elapsed = time.time() - self.start_time
            values[0] = int(elapsed) % 65535
        return values

# 创建数据存储
# 使用自定义数据块实现动态数据
store = ModbusSlaveContext(
    di=ModbusSequentialDataBlock(0, [0]*100),  # 离散输入
    co=ModbusSequentialDataBlock(0, [0]*100),  # 线圈
    hr=CustomDataBlock(0, [0]*100),            # 保持寄存器(使用自定义数据块)
    ir=ModbusSequentialDataBlock(0, [0]*100)   # 输入寄存器
)

# 创建服务器上下文
context = ModbusServerContext(slaves=store, single=True)

# 数据更新线程函数
def update_data():
    """定期更新寄存器数据,模拟传感器输入"""
    while True:
        time.sleep(1)
        # 更新输入寄存器,模拟温度传感器数据
        # 假设温度范围:0-100度,转换为整数
        import random
        temp = int(random.uniform(200, 300))  # 20.0-30.0度,放大10倍
        store.setValues(3, 0, [temp])
        
        # 更新离散输入,模拟开关状态
        inputs = [random.choice([0, 1]) for _ in range(8)]
        store.setValues(1, 0, inputs)

# 启动数据更新线程
update_thread = threading.Thread(target=update_data, daemon=True)
update_thread.start()
print("数据更新线程已启动")

# 启动TCP服务器
# 参数说明:
# - context: 数据存储上下文
# - address: 监听地址,'0.0.0.0'表示监听所有网卡
# - port: 监听端口号,默认为502
print("Modbus TCP服务器启动中,监听端口 502...")
StartTcpServer(
    context=context,
    address='0.0.0.0',
    port=502
)

4. 从站设备开发

4.1 嵌入式设备实现(C语言)

以下代码适用于ARM Cortex-M等嵌入式平台,实现Modbus RTU从站:

c 复制代码
// Modbus RTU从站实现
// 适用于STM32等嵌入式平台

#include <stdint.h>
#include <stdbool.h>
#include <string.h>

// 定义常量
#define MODBUS_MAX_BUFFER_SIZE 128
#define MODBUS_MAX_REGISTERS 100
#define MODBUS_MAX_COILS 100

// 定义功能码
#define MODBUS_FC_READ_COILS              0x01
#define MODBUS_FC_READ_DISCRETE_INPUTS    0x02
#define MODBUS_FC_READ_HOLDING_REGISTERS  0x03
#define MODBUS_FC_READ_INPUT_REGISTERS    0x04
#define MODBUS_FC_WRITE_SINGLE_COIL       0x05
#define MODBUS_FC_WRITE_SINGLE_REGISTER   0x06
#define MODBUS_FC_WRITE_MULTIPLE_COILS    0x0F
#define MODBUS_FC_WRITE_MULTIPLE_REGISTERS 0x10

// 定义异常码
#define MODBUS_EXCEPTION_ILLEGAL_FUNCTION     0x01
#define MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS 0x02
#define MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE   0x03
#define MODBUS_EXCEPTION_SLAVE_DEVICE_FAILURE 0x04

// Modbus从站状态结构体
typedef struct {
    uint8_t slave_id;              // 从站地址
    uint16_t holding_registers[MODBUS_MAX_REGISTERS];  // 保持寄存器
    uint16_t input_registers[MODBUS_MAX_REGISTERS];     // 输入寄存器
    uint8_t coils[MODBUS_MAX_COILS];  // 线圈(位数组)
    uint8_t discrete_inputs[MODBUS_MAX_COILS];  // 离散输入(位数组)
} ModbusSlaveContext;

// 全局变量
ModbusSlaveContext modbus_slave;

// CRC16计算函数
// 参数:
//   data: 数据缓冲区
//   length: 数据长度
// 返回:CRC16校验值
uint16_t modbus_crc16(const uint8_t *data, uint16_t length) {
    uint16_t crc = 0xFFFF;
    
    for (uint16_t i = 0; i < length; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    
    return crc;
}

// 位操作函数:获取位数组中的某一位
// 参数:
//   bits: 位数组
//   index: 位索引
// 返回:位的值(0或1)
uint8_t get_bit(const uint8_t *bits, uint16_t index) {
    uint16_t byte_index = index / 8;
    uint8_t bit_index = index % 8;
    return (bits[byte_index] >> bit_index) & 0x01;
}

// 位操作函数:设置位数组中的某一位
// 参数:
//   bits: 位数组
//   index: 位索引
//   value: 要设置的值(0或1)
void set_bit(uint8_t *bits, uint16_t index, uint8_t value) {
    uint16_t byte_index = index / 8;
    uint8_t bit_index = index % 8;
    
    if (value) {
        bits[byte_index] |= (1 << bit_index);
    } else {
        bits[byte_index] &= ~(1 << bit_index);
    }
}

// 处理读线圈请求(功能码0x01)
// 参数:
//   request: 请求帧
//   request_length: 请求帧长度
//   response: 响应帧缓冲区
// 返回:响应帧长度
uint16_t handle_read_coils(const uint8_t *request, uint16_t request_length, 
                           uint8_t *response) {
    // 解析请求参数
    uint16_t start_addr = (request[2] << 8) | request[3];
    uint16_t coil_count = (request[4] << 8) | request[5];
    
    // 检查参数有效性
    if (coil_count > 2000 || start_addr + coil_count > MODBUS_MAX_COILS) {
        // 返回异常
        response[0] = modbus_slave.slave_id;
        response[1] = request[1] | 0x80;
        response[2] = MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS;
        return 3;
    }
    
    // 计算字节数
    uint8_t byte_count = (coil_count + 7) / 8;
    
    // 构建响应帧
    response[0] = modbus_slave.slave_id;
    response[1] = request[1];  // 功能码
    response[2] = byte_count;  // 字节数
    
    // 填充线圈数据
    for (uint16_t i = 0; i < coil_count; i++) {
        uint16_t addr = start_addr + i;
        uint8_t bit_value = get_bit(modbus_slave.coils, addr);
        
        if (bit_value) {
            response[3 + i/8] |= (1 << (i % 8));
        }
    }
    
    // 计算并添加CRC
    uint16_t crc = modbus_crc16(response, 3 + byte_count);
    response[3 + byte_count] = crc & 0xFF;
    response[4 + byte_count] = (crc >> 8) & 0xFF;
    
    return 5 + byte_count;
}

// 处理读保持寄存器请求(功能码0x03)
// 参数:
//   request: 请求帧
//   request_length: 请求帧长度
//   response: 响应帧缓冲区
// 返回:响应帧长度
uint16_t handle_read_holding_registers(const uint8_t *request, uint16_t request_length,
                                       uint8_t *response) {
    // 解析请求参数
    uint16_t start_addr = (request[2] << 8) | request[3];
    uint16_t reg_count = (request[4] << 8) | request[5];
    
    // 检查参数有效性
    if (reg_count > 125 || start_addr + reg_count > MODBUS_MAX_REGISTERS) {
        // 返回异常
        response[0] = modbus_slave.slave_id;
        response[1] = request[1] | 0x80;
        response[2] = MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS;
        return 3;
    }
    
    // 计算字节数
    uint8_t byte_count = reg_count * 2;
    
    // 构建响应帧
    response[0] = modbus_slave.slave_id;
    response[1] = request[1];  // 功能码
    response[2] = byte_count;  // 字节数
    
    // 填充寄存器数据(大端格式)
    for (uint16_t i = 0; i < reg_count; i++) {
        uint16_t addr = start_addr + i;
        uint16_t reg_value = modbus_slave.holding_registers[addr];
        
        response[3 + i*2] = (reg_value >> 8) & 0xFF;     // 高字节
        response[4 + i*2] = reg_value & 0xFF;           // 低字节
    }
    
    // 计算并添加CRC
    uint16_t crc = modbus_crc16(response, 3 + byte_count);
    response[3 + byte_count] = crc & 0xFF;
    response[4 + byte_count] = (crc >> 8) & 0xFF;
    
    return 5 + byte_count;
}

// 处理写单个寄存器请求(功能码0x06)
// 参数:
//   request: 请求帧
//   request_length: 请求帧长度
//   response: 响应帧缓冲区
// 返回:响应帧长度
uint16_t handle_write_single_register(const uint8_t *request, uint16_t request_length,
                                     uint8_t *response) {
    // 解析请求参数
    uint16_t reg_addr = (request[2] << 8) | request[3];
    uint16_t reg_value = (request[4] << 8) | request[5];
    
    // 检查地址有效性
    if (reg_addr >= MODBUS_MAX_REGISTERS) {
        response[0] = modbus_slave.slave_id;
        response[1] = request[1] | 0x80;
        response[2] = MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS;
        return 3;
    }
    
    // 写入寄存器
    modbus_slave.holding_registers[reg_addr] = reg_value;
    
    // 构建响应帧(回显请求)
    memcpy(response, request, 6);
    
    // 计算并添加CRC
    uint16_t crc = modbus_crc16(response, 6);
    response[6] = crc & 0xFF;
    response[7] = (crc >> 8) & 0xFF;
    
    return 8;
}

// 处理写多个寄存器请求(功能码0x10)
// 参数:
//   request: 请求帧
//   request_length: 请求帧长度
//   response: 响应帧缓冲区
// 返回:响应帧长度
uint16_t handle_write_multiple_registers(const uint8_t *request, uint16_t request_length,
                                        uint8_t *response) {
    // 解析请求参数
    uint16_t start_addr = (request[2] << 8) | request[3];
    uint16_t reg_count = (request[4] << 8) | request[5];
    uint8_t byte_count = request[6];
    
    // 检查参数有效性
    if (reg_count > 123 || start_addr + reg_count > MODBUS_MAX_REGISTERS) {
        response[0] = modbus_slave.slave_id;
        response[1] = request[1] | 0x80;
        response[2] = MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS;
        return 3;
    }
    
    // 写入寄存器数据
    for (uint16_t i = 0; i < reg_count; i++) {
        uint16_t addr = start_addr + i;
        modbus_slave.holding_registers[addr] = 
            (request[7 + i*2] << 8) | request[8 + i*2];
    }
    
    // 构建响应帧
    response[0] = modbus_slave.slave_id;
    response[1] = request[1];
    response[2] = (start_addr >> 8) & 0xFF;
    response[3] = start_addr & 0xFF;
    response[4] = (reg_count >> 8) & 0xFF;
    response[5] = reg_count & 0xFF;
    
    // 计算并添加CRC
    uint16_t crc = modbus_crc16(response, 6);
    response[6] = crc & 0xFF;
    response[7] = (crc >> 8) & 0xFF;
    
    return 8;
}

// Modbus请求处理主函数
// 参数:
//   request: 接收到的请求帧
//   request_length: 请求帧长度
//   response: 响应帧缓冲区
// 返回:响应帧长度,0表示不响应
uint16_t modbus_process_request(const uint8_t *request, uint16_t request_length,
                               uint8_t *response) {
    // 检查最小帧长度
    if (request_length < 4) {
        return 0;
    }
    
    // 验证从站地址
    if (request[0] != modbus_slave.slave_id && request[0] != 0) {
        return 0;  // 广播地址0不响应
    }
    
    // 验证CRC
    uint16_t received_crc = (request[request_length-2] | (request[request_length-1] << 8));
    uint16_t calculated_crc = modbus_crc16(request, request_length - 2);
    
    if (received_crc != calculated_crc) {
        return 0;  // CRC错误,不响应
    }
    
    // 根据功能码分发处理
    switch (request[1]) {
        case MODBUS_FC_READ_COILS:
            return handle_read_coils(request, request_length, response);
            
        case MODBUS_FC_READ_DISCRETE_INPUTS:
            // 类似于读线圈,处理方式相同
            return handle_read_coils(request, request_length, response);
            
        case MODBUS_FC_READ_HOLDING_REGISTERS:
            return handle_read_holding_registers(request, request_length, response);
            
        case MODBUS_FC_READ_INPUT_REGISTERS:
            // 类似于读保持寄存器
            return handle_read_holding_registers(request, request_length, response);
            
        case MODBUS_FC_WRITE_SINGLE_COIL:
            // 处理写单个线圈
            response[0] = modbus_slave.slave_id;
            response[1] = request[1];
            memcpy(response + 2, request + 2, 4);
            uint16_t crc = modbus_crc16(response, 6);
            response[6] = crc & 0xFF;
            response[7] = (crc >> 8) & 0xFF;
            return 8;
            
        case MODBUS_FC_WRITE_SINGLE_REGISTER:
            return handle_write_single_register(request, request_length, response);
            
        case MODBUS_FC_WRITE_MULTIPLE_COILS:
            // 处理写多个线圈
            return handle_write_multiple_registers(request, request_length, response);
            
        case MODBUS_FC_WRITE_MULTIPLE_REGISTERS:
            return handle_write_multiple_registers(request, request_length, response);
            
        default:
            // 未知功能码,返回异常
            response[0] = modbus_slave.slave_id;
            response[1] = request[1] | 0x80;
            response[2] = MODBUS_EXCEPTION_ILLEGAL_FUNCTION;
            crc = modbus_crc16(response, 3);
            response[3] = crc & 0xFF;
            response[4] = (crc >> 8) & 0xFF;
            return 5;
    }
}

// Modbus从站初始化
// 参数:
//   slave_id: 从站地址
void modbus_slave_init(uint8_t slave_id) {
    modbus_slave.slave_id = slave_id;
    memset(modbus_slave.holding_registers, 0, sizeof(modbus_slave.holding_registers));
    memset(modbus_slave.input_registers, 0, sizeof(modbus_slave.input_registers));
    memset(modbus_slave.coils, 0, sizeof(modbus_slave.coils));
    memset(modbus_slave.discrete_inputs, 0, sizeof(modbus_slave.discrete_inputs));
}

// 示例:在主循环中调用
void main_loop_example(void) {
    uint8_t rx_buffer[MODBUS_MAX_BUFFER_SIZE];
    uint8_t tx_buffer[MODBUS_MAX_BUFFER_SIZE];
    uint16_t rx_length = 0;
    
    // 初始化Modbus从站
    modbus_slave_init(1);
    
    // 主循环
    while (1) {
        // 1. 从串口接收数据(需要根据实际硬件实现)
        // rx_length = uart_receive(rx_buffer, MODBUS_MAX_BUFFER_SIZE, timeout);
        
        if (rx_length > 0) {
            // 2. 处理Modbus请求
            uint16_t tx_length = modbus_process_request(rx_buffer, rx_length, tx_buffer);
            
            if (tx_length > 0) {
                // 3. 发送响应
                // uart_send(tx_buffer, tx_length);
            }
        }
        
        // 4. 更新输入寄存器和离散输入数据(模拟传感器数据)
        // modbus_slave.input_registers[0] = read_sensor_temperature();
        // modbus_slave.discrete_inputs[0] = read_button_state();
    }
}

4.2 寄存器映射示例

c 复制代码
// 寄存器地址映射定义
#define REG_TEMPERATURE        0   // 温度传感器值(0.1°C分辨率)
#define REG_HUMIDITY          1   // 湿度传感器值(0.1%分辨率)
#define REG_PRESSURE           2   // 压力传感器值(0.1kPa分辨率)
#define REG_DEVICE_STATUS      10  // 设备状态字
#define REG_OUTPUT_CONTROL     20  // 输出控制寄存器
#define REG_ALARM_THRESHOLD    30  // 报警阈值
#define REG_FIRMWARE_VERSION   100 // 固件版本号

// 线圈地址映射定义
#define COIL_RELAY1            0   // 继电器1控制
#define COIL_RELAY2            1   // 继电器2控制
#define COIL_RELAY3            2   // 继电器3控制
#define COIL_ALARM_RESET       10  // 报警复位

// 更新输入寄存器函数
void update_input_registers(void) {
    // 模拟读取传感器数据
    modbus_slave.input_registers[REG_TEMPERATURE] = 250;  // 25.0°C
    modbus_slave.input_registers[REG_HUMIDITY] = 650;     // 65.0%
    modbus_slave.input_registers[REG_PRESSURE] = 1013;    // 101.3kPa
}

// 处理输出控制
void handle_output_control(void) {
    // 读取输出控制寄存器
    uint16_t control = modbus_slave.holding_registers[REG_OUTPUT_CONTROL];
    
    // 根据控制寄存器的值控制硬件
    // if (control & 0x0001) relay1_on(); else relay1_off();
    // if (control & 0x0002) relay2_on(); else relay2_off();
}

5. 常见问题与调试技巧

5.1 CRC校验工具

python 复制代码
# Modbus CRC16校验计算工具
def calculate_crc(data):
    """
    计算Modbus CRC16校验值
    
    参数:
        data: 字节列表或字节数组
    
    返回:
        CRC16校验值(两个字节,低字节在前)
    """
    crc = 0xFFFF
    
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    
    # 返回低字节在前,高字节在后
    return [crc & 0xFF, (crc >> 8) & 0xFF]

# 示例:验证RTU帧的CRC
def verify_modbus_rtu_frame(frame):
    """
    验证Modbus RTU帧的CRC
    
    参数:
        frame: 完整的Modbus RTU帧(包含CRC)
    
    返回:
        True: CRC正确
        False: CRC错误
    """
    if len(frame) < 4:
        return False
    
    # 分离数据和CRC
    data = frame[:-2]
    received_crc = frame[-2:]
    
    # 计算CRC
    calculated_crc = calculate_crc(data)
    
    # 比较
    return received_crc == calculated_crc

# 使用示例
if __name__ == "__main__":
    # 示例帧:设备地址1,功能码3,读取寄存器0-9
    frame = [0x01, 0x03, 0x00, 0x00, 0x00, 0x0A]
    
    # 计算并添加CRC
    crc = calculate_crc(frame)
    frame_with_crc = frame + crc
    
    print(f"完整帧: {' '.join(f'{b:02X}' for b in frame_with_crc)}")
    
    # 验证CRC
    is_valid = verify_modbus_rtu_frame(frame_with_crc)
    print(f"CRC验证: {'通过' if is_valid else '失败'}")

5.2 数据类型转换工具

python 复制代码
# Modbus数据类型转换工具类
class ModbusDataConverter:
    """Modbus寄存器数据转换工具"""
    
    @staticmethod
    def to_int16(register):
        """
        将单个16位寄存器转换为有符号整数
        
        参数:
            register: 寄存器值(0-65535)
        
        返回:
            有符号16位整数(-32768到32767)
        """
        if register > 32767:
            return register - 65536
        return register
    
    @staticmethod
    def to_uint16(register):
        """
        将单个16位寄存器转换为无符号整数
        
        参数:
            register: 寄存器值(0-65535)
        
        返回:
            无符号16位整数(0到65535)
        """
        return register & 0xFFFF
    
    @staticmethod
    def to_int32(high_reg, low_reg, byte_order='big'):
        """
        将两个16位寄存器转换为32位整数
        
        参数:
            high_reg: 高16位寄存器
            low_reg: 低16位寄存器
            byte_order: 字节顺序,'big'或'little'
        
        返回:
            32位整数
        """
        if byte_order == 'big':
            value = (high_reg << 16) | low_reg
        else:
            value = (low_reg << 16) | high_reg
        
        # 转换为有符号整数
        if value > 2147483647:
            value -= 4294967296
        return value
    
    @staticmethod
    def to_float32(high_reg, low_reg, byte_order='big'):
        """
        将两个16位寄存器转换为32位浮点数
        
        参数:
            high_reg: 高16位寄存器
            low_reg: 低16位寄存器
            byte_order: 字节顺序,'big'或'little'
        
        返回:
            32位浮点数
        """
        import struct
        
        if byte_order == 'big':
            # 大端格式
            bytes_data = bytes([(high_reg >> 8) & 0xFF, high_reg & 0xFF,
                               (low_reg >> 8) & 0xFF, low_reg & 0xFF])
        else:
            # 小端格式
            bytes_data = bytes([low_reg & 0xFF, (low_reg >> 8) & 0xFF,
                               high_reg & 0xFF, (high_reg >> 8) & 0xFF])
        
        return struct.unpack('!f', bytes_data)[0]
    
    @staticmethod
    def from_float32(value, byte_order='big'):
        """
        将32位浮点数转换为两个16位寄存器
        
        参数:
            value: 浮点数值
            byte_order: 字节顺序
        
        返回:
            (高16位寄存器, 低16位寄存器)
        """
        import struct
        
        bytes_data = struct.pack('!f', value)
        
        if byte_order == 'big':
            high_reg = (bytes_data[0] << 8) | bytes_data[1]
            low_reg = (bytes_data[2] << 8) | bytes_data[3]
        else:
            high_reg = (bytes_data[3] << 8) | bytes_data[2]
            low_reg = (bytes_data[1] << 8) | bytes_data[0]
        
        return high_reg, low_reg
    
    @staticmethod
    def to_string(registers, encoding='utf-8'):
        """
        将寄存器数组转换为字符串
        
        参数:
            registers: 寄存器列表
            encoding: 字符编码
        
        返回:
            解码后的字符串
        """
        # 将寄存器转换为字节
        bytes_data = bytes()
        for reg in registers:
            bytes_data += bytes([(reg >> 8) & 0xFF, reg & 0xFF])
        
        return bytes_data.decode(encoding).rstrip('\x00')

# 使用示例
if __name__ == "__main__":
    converter = ModbusDataConverter()
    
    # 16位整数转换
    print(f"无符号16位: {converter.to_uint16(50000)}")
    print(f"有符号16位: {converter.to_int16(50000)}")
    
    # 32位整数转换
    int32_value = converter.to_int32(0x0001, 0x86A0)
    print(f"32位整数: {int32_value}")
    
    # 浮点数转换
    float_value = converter.to_float32(0x4220, 0x0000)
    print(f"32位浮点数: {float_value}")
    
    # 将浮点数写入寄存器
    high, low = converter.from_float32(25.5)
    print(f"浮点数25.5转换为寄存器: 高={high:04X}, 低={low:04X}")
    
    # 字符串转换
    string_reg = [0x4865, 0x6C6C, 0x6F20]  # "Hello"
    print(f"字符串: {converter.to_string(string_reg)}")

5.3 调试助手工具

python 复制代码
# Modbus调试助手
import serial
import time
from pymodbus.client import ModbusSerialClient, ModbusTcpClient

class ModbusDebugHelper:
    """Modbus调试助手类"""
    
    def __init__(self, connection_type='rtu', **kwargs):
        """
        初始化Modbus调试助手
        
        参数:
            connection_type: 连接类型 'rtu' 或 'tcp'
            **kwargs: 连接参数
                RTU: port, baudrate, parity, stopbits, bytesize
                TCP: host, port
        """
        self.connection_type = connection_type
        self.client = None
        
        if connection_type == 'rtu':
            self.client = ModbusSerialClient(**kwargs)
        elif connection_type == 'tcp':
            self.client = ModbusTcpClient(**kwargs)
        else:
            raise ValueError("不支持的连接类型")
    
    def connect(self):
        """连接到设备"""
        if self.client.connect():
            print("连接成功")
            return True
        else:
            print("连接失败")
            return False
    
    def disconnect(self):
        """断开连接"""
        self.client.close()
        print("连接已断开")
    
    def scan_devices(self, start_id=1, end_id=247, timeout=1):
        """
        扫描Modbus网络中的设备
        
        参数:
            start_id: 起始设备地址
            end_id: 结束设备地址
            timeout: 超时时间
        
        返回:
            在线设备列表
        """
        online_devices = []
        
        print(f"正在扫描设备 {start_id}-{end_id}...")
        for device_id in range(start_id, end_id + 1):
            try:
                result = self.client.read_holding_registers(
                    address=0,
                    count=1,
                    slave=device_id,
                    timeout=timeout
                )
                if not result.isError():
                    online_devices.append(device_id)
                    print(f"  发现设备: ID={device_id}")
            except:
                pass
        
        print(f"扫描完成,发现 {len(online_devices)} 个设备")
        return online_devices
    
    def read_and_dump(self, slave_id, start_addr, count, data_type='uint16'):
        """
        读取寄存器并以多种格式显示
        
        参数:
            slave_id: 从站地址
            start_addr: 起始地址
            count: 读取数量
            data_type: 数据类型提示
        """
        result = self.client.read_holding_registers(
            address=start_addr,
            count=count,
            slave=slave_id
        )
        
        if result.isError():
            print(f"读取失败: {result}")
            return
        
        print(f"\n从站 {slave_id}, 地址 {start_addr}, 数量 {count}")
        print("-" * 60)
        print(f"{'地址':<8}{'原始值':<12}{'UINT16':<12}{'INT16':<12}{'二进制':<16}")
        print("-" * 60)
        
        for i, value in enumerate(result.registers):
            addr = start_addr + i
            converter = ModbusDataConverter()
            
            print(f"{addr:<8}{value:<12}{converter.to_uint16(value):<12}"
                  f"{converter.to_int16(value):<12}{value:016b}")
        
        # 如果数据对齐,尝试显示32位整数和浮点数
        if count >= 2 and (count % 2 == 0):
            print("\n32位数据解析:")
            print("-" * 60)
            print(f"{'地址':<8}{'INT32':<15}{'FLOAT32':<15}")
            print("-" * 60)
            
            for i in range(0, count, 2):
                addr = start_addr + i
                high = result.registers[i]
                low = result.registers[i+1]
                
                int32 = converter.to_int32(high, low)
                float32 = converter.to_float32(high, low)
                
                print(f"{addr:<8}{int32:<15}{float32:<15}")
    
    def continuous_read(self, slave_id, address, count=1, interval=1):
        """
        持续读取寄存器
        
        参数:
            slave_id: 从站地址
            address: 寄存器地址
            count: 读取数量
            interval: 读取间隔(秒)
        """
        print(f"开始持续读取 (Ctrl+C 停止)")
        print(f"从站: {slave_id}, 地址: {address}, 间隔: {interval}秒")
        print("-" * 60)
        
        try:
            while True:
                result = self.client.read_holding_registers(
                    address=address,
                    count=count,
                    slave=slave_id
                )
                
                if not result.isError():
                    timestamp = time.strftime("%H:%M:%S")
                    values = result.registers
                    print(f"[{timestamp}] {values}")
                
                time.sleep(interval)
        except KeyboardInterrupt:
            print("\n已停止读取")

# 使用示例
if __name__ == "__main__":
    # 创建RTU调试助手
    debug = ModbusDebugHelper(
        connection_type='rtu',
        port='COM3',
        baudrate=9600,
        parity='N',
        stopbits=1,
        bytesize=8
    )
    
    # 连接
    if debug.connect():
        try:
            # 扫描设备
            devices = debug.scan_devices(start_id=1, end_id=10)
            
            if devices:
                # 读取并显示寄存器
                slave_id = devices[0]
                debug.read_and_dump(slave_id, 0, 10)
                
                # 持续读取
                # debug.continuous_read(slave_id, 0, 1, 0.5)
        finally:
            debug.disconnect()

5.4 常见问题排查

问题1:CRC校验失败

现象:设备不响应或返回异常

原因:

  • 波特率不匹配
  • 数据位/停止位/校验位设置错误
  • 串口电气连接问题

解决方法:

python 复制代码
# 检查串口配置
import serial.tools.list_ports

# 列出所有可用串口
ports = serial.tools.list_ports.comports()
print("可用串口:")
for port in ports:
    print(f"  {port.device}: {port.description}")

# 尝试不同的波特率
baudrates = [1200, 2400, 4800, 9600, 19200, 38400]
for baudrate in baudrates:
    client = ModbusSerialClient(
        port='COM3',
        baudrate=baudrate,
        timeout=1
    )
    if client.connect():
        result = client.read_holding_registers(0, 1, slave=1)
        if not result.isError():
            print(f"成功连接,波特率: {baudrate}")
            break
        client.close()
问题2:功能码不支持

现象:返回异常码0x01

原因:设备不支持该功能码

解决方法:

python 复制代码
# 检查设备支持的功能码
def test_function_codes(client, slave_id):
    """测试设备支持的功能码"""
    function_codes = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x0F, 0x10]
    
    print(f"测试设备 {slave_id} 支持的功能码:")
    for fc in function_codes:
        try:
            if fc == 0x01:
                result = client.read_coils(0, 1, slave=slave_id)
            elif fc == 0x03:
                result = client.read_holding_registers(0, 1, slave=slave_id)
            elif fc == 0x06:
                result = client.write_register(0, 0, slave=slave_id)
            
            if not result.isError():
                print(f"  功能码 0x{fc:02X}: 支持")
            elif hasattr(result, 'exception_code'):
                if result.exception_code == 1:
                    print(f"  功能码 0x{fc:02X}: 不支持")
        except:
            pass
问题3:数据读取错误

现象:读取到的数据与实际不符

原因:

  • 寄存器地址映射错误
  • 数据类型转换错误
  • 字节顺序错误

解决方法:

python 复制代码
# 测试不同字节顺序和寄存器组合
def analyze_register_data(registers):
    """分析寄存器数据的不同解释"""
    converter = ModbusDataConverter()
    
    print("数据解释分析:")
    print("-" * 60)
    
    # 单寄存器
    print(f"UINT16: {converter.to_uint16(registers[0])}")
    print(f"INT16:  {converter.to_int16(registers[0])}")
    
    # 双寄存器 - 大端
    if len(registers) >= 2:
        print(f"\n大端格式 (ABCD):")
        print(f"  INT32:   {converter.to_int32(registers[0], registers[1], 'big')}")
        print(f"  FLOAT32: {converter.to_float32(registers[0], registers[1], 'big')}")
        
        # 小端
        print(f"\n小端格式 (DCBA):")
        print(f"  INT32:   {converter.to_int32(registers[0], registers[1], 'little')}")
        print(f"  FLOAT32: {converter.to_float32(registers[0], registers[1], 'little')}")

附录

A. 依赖库安装

bash 复制代码
# 安装pymodbus库
pip install pymodbus

# 安装serial库(用于串口通信)
pip install pyserial

B. 参考资料

C. 推荐阅读

  1. 《工业通信协议与应用》
  2. 《嵌入式系统开发实战指南》
  3. 《串口通信编程技术》
相关推荐
水饺编程1 天前
第2章:Linux 基础,解压缩【.tar.gz】压缩包
linux·c语言·汇编
水饺编程2 天前
第2章:Linux 基础,exit 命令
linux·c语言·汇编
水饺编程2 天前
第2章:Linux 基础,删除用户
linux·c语言·汇编
水饺编程4 天前
第2章:Linux 基础,切换用户
linux·c语言·汇编
水饺编程5 天前
第2章:Linux 基础,在 root 中创建用户账号
linux·c语言·汇编·1024程序员节
一个风轻云淡5 天前
从 int $0x80 到 iret:用 gdb 把 getpid 走一遍
linux·c语言·开发语言·汇编
水饺编程6 天前
第2章:Linux 基础,切换到超级用户
linux·c语言·汇编
水饺编程7 天前
第2章:Linux 基础,重启命令
linux·c语言·汇编
en.en..8 天前
volatile 完整深度教程(C 代码 + 带注释 ARM 汇编 + 硬件中断原理 + 分步运行时序)
c语言·汇编·arm开发