Modbus协议开发实战指南
目录
- Modbus协议概述
- [Modbus RTU开发实战](#Modbus RTU开发实战)
- [Modbus TCP开发实战](#Modbus TCP开发实战)
- 从站设备开发
- 常见问题与调试技巧
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. 参考资料
- Modbus协议规范:http://www.modbus.org/specs.php
- pymodbus文档:https://pymodbus.readthedocs.io/
- Modbus工具:https://www.modbustools.com/
C. 推荐阅读
- 《工业通信协议与应用》
- 《嵌入式系统开发实战指南》
- 《串口通信编程技术》