Почему Modbus жив и актуален спустя 45 лет
Modbus был разработан компанией Modicon в 1979 году для связи ПЛК по последовательной шине. Прошло почти полвека — а протокол по-прежнему является самым распространённым в промышленной автоматизации. По различным оценкам, более 30 миллионов устройств в мире используют Modbus.
Секрет долголетия прост: протокол исключительно прост в понимании и реализации. Он работает по схеме "мастер-слейв", имеет открытую спецификацию, не требует лицензирования, поддерживается абсолютно всеми промышленными устройствами.
Сегодня существуют три основные реализации:
Modbus RTU — по последовательной шине (RS-232, RS-485)
Modbus ASCII — текстовое представление, устарело
Modbus TCP — поверх TCP/IP Ethernet
Архитектура: мастер и слейвы
Modbus — строго мастер-слейв (Master-Slave) протокол:
Мастер (Master): Инициирует все запросы. Только один мастер в сети. Обычно это ПЛК, SCADA-сервер, промышленный компьютер.
Слейв (Slave): Отвечает на запросы мастера. В сети RS-485 может быть до 247 слейвов с адресами 1–247. Адрес 0 — широковещательный (слейвы не отвечают).
Важно: Слейв никогда не инициирует передачу! Он только отвечает.
Мастер Слейв 1 Слейв 2 Слейв 3
| | | |
|── Request → Addr=1 ─────>| | |
|<── Response ────────────| | |
| | | |
|── Request → Addr=2 ──────────────>| |
|<── Response ──────────────────────| |
Типы данных (регистры)
Modbus оперирует четырьмя типами данных:
1. Coils (Coil Status) — Дискретные выходы
Размер: 1 бит
Доступ: Чтение и запись мастером
Функциональные коды: FC01 (читать), FC05 (записать один), FC15 (записать несколько)
Адреса: 00001–09999 (в Modbus-нотации), 0x0000–0xFFFF (в PDU)
Применение: Состояния реле, клапанов, двигателей
2. Discrete Inputs (Input Status) — Дискретные входы
Размер: 1 бит
Доступ: Только чтение мастером (данные поступают от физических входов)
Функциональные коды: FC02 (читать)
Адреса: 10001–19999
Применение: Состояния кнопок, датчиков, концевиков
3. Holding Registers — Регистры хранения
Размер: 16 бит (2 байта), беззнаковое целое 0–65535
Доступ: Чтение и запись мастером
Функциональные коды: FC03 (читать), FC06 (записать один), FC16 (записать несколько)
Адреса: 40001–49999
Применение: Уставки, параметры настройки, команды управления
4. Input Registers — Входные регистры
Размер: 16 бит
Доступ: Только чтение мастером
Функциональные коды: FC04 (читать)
Адреса: 30001–39999
Применение: Измеренные значения датчиков, счётчики
Хранение чисел с плавающей точкой
16 бит для float недостаточно. Для передачи float используют два последовательных регистра (32 бит = IEEE 754):
Регистр 40001 (HIGH word): первые 16 бит float
Регистр 40002 (LOW word): вторые 16 бит float
Значение 3.14159:
IEEE 754: 0x40490FDB
HIGH: 0x4049
LOW: 0x0FDB
Важная ловушка: порядок байт и слов (endianness) различается у разных производителей! Четыре варианта: Big-Endian, Little-Endian, Big-Endian Byte Swap, Little-Endian Byte Swap. Смотрите документацию устройства.
Modbus RTU: структура фрейма
RTU (Remote Terminal Unit) — бинарный формат, максимально компактный.
┌─────────┬──────────────┬──────┬────────────────────┬───────┐
│ Address │ Function Code│ Data │ Data │ CRC │
│ 1 байт │ 1 байт │ ... │ ... │ 2 байт│
└─────────┴──────────────┴──────┴────────────────────┴───────┘
Пример запроса FC03 (читать Holding Registers):
Запрос: "Слейв №1, дай мне 2 регистра начиная с адреса 0x0064 (100)"
01 03 00 64 00 02 85 D5
01 — Адрес слейва
03 — Код функции (читать Holding Registers)
00 64 — Начальный адрес (0x0064 = 100)
00 02 — Количество регистров (2)
85 D5 — CRC16 (контрольная сумма)
Ответ слейва:
01 03 04 01 F4 00 0A 2B 11
01 — Адрес слейва
03 — Код функции
04 — Количество байт данных (2 регистра × 2 байта = 4)
01 F4 — Значение регистра 100 (0x01F4 = 500)
00 0A — Значение регистра 101 (0x000A = 10)
2B 11 — CRC16
Ответ при ошибке (Exception Response):
01 83 02 C0 F1
01 — Адрес слейва
83 — Код функции + 0x80 (признак ошибки)
02 — Код исключения (02 = Illegal Data Address)
C0 F1 — CRC16
Коды исключений:
Код | Название | Описание |
|---|---|---|
01 | Illegal Function | Устройство не поддерживает данный FC |
02 | Illegal Data Address | Запрошенный адрес не существует |
03 | Illegal Data Value | Недопустимое значение данных |
04 | Server Device Failure | Внутренняя ошибка устройства |
06 | Server Device Busy | Устройство занято, повторите позже |
Расчёт CRC16
CRC16 (Cyclic Redundancy Check) — контрольная сумма для обнаружения ошибок передачи. Алгоритм несложный, но важный:
uint16_t ModbusCRC16(uint8_t *buffer, uint16_t length)
{
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < length; i++) {
crc ^= (uint16_t)buffer[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001; // Полином Modbus
} else {
crc >>= 1;
}
}
}
return crc; // Младший байт первый в фрейме!
}
// Использование:
uint8_t frame[] = {0x01, 0x03, 0x00, 0x64, 0x00, 0x02};
uint16_t crc = ModbusCRC16(frame, 6);
// Добавить в конец фрейма: (crc & 0xFF), (crc >> 8)
Важно: В Modbus RTU CRC передаётся младшим байтом вперёд (Little-Endian)!
RS-485: физический уровень
Modbus RTU работает поверх RS-485 — дифференциальной последовательной шины.
Параметры сети:
Длина: до 1200 м (при скорости 9600 бод), до 100 м (при 115200 бод)
Устройств: до 32 без репитеров, до 247 с репитерами
Скорости: 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200 бод
Линия: витая пара, лучше экранированная (STP)
Терминаторы: 120 Ом на каждом конце шины
Топология — только шина!
Мастер ──────┬──────────┬──────────┬──────── Терминатор 120Ом
120Ом ───┘ Слейв1 Слейв2 Слейв3
НЕЛЬЗЯ делать "звезду" — отражения сигнала разрушат связь.
Типичные проблемы RS-485:
Проблема 1: Нет терминирующих резисторов Симптомы: связь работает на малой скорости, не работает на высокой. Или работает с одним устройством, не работает с несколькими. Решение: 120 Ом строго на двух концах шины — и только там.
Проблема 2: Земля не подключена RS-485 — дифференциальный сигнал (A-B), не требует общего провода теоретически. Практически — без общей земли при большой разнице потенциалов (грозозащита, разные здания) трансиверы сгорают. Третий провод "GND" обязателен.
Проблема 3: Смешаны A и B Сигнальные линии перепутаны местами. Ошибка типичная при ручном монтаже. Симптом: нет ответа вообще или постоянные ошибки CRC.
Проблема 4: Нет pull-up/pull-down резисторов на линии Когда все устройства молчат (пауза между транзакциями), линия "висит в воздухе". Нужны подтягивающие резисторы: A через ~560 Ом на +5В, B через ~560 Ом на GND. Многие USB-RS485 адаптеры имеют их встроенными.
Modbus TCP: Ethernet-версия
Modbus TCP — это Modbus RTU без адреса устройства и без CRC, завёрнутый в TCP/IP пакет.
Структура Modbus TCP фрейма:
┌─────────────┬──────────────────────────────────────────────┐
│ MBAP Header (7 байт) │ PDU (Protocol Data Unit) │
├──────┬───────┬──────────┬───────┬──────────────────────────┤
│TrID │ Proto │ Length │ UnitID│ FC │ Data │
│2 байт│ 2 байт│ 2 байта │ 1 байт│1 байт│ N байт │
└──────┴───────┴──────────┴───────┴──────┴──────────────────┘
TrID — Transaction Identifier (любое число, повторяется в ответе)
Proto — 0x0000 (всегда)
Length — длина оставшейся части (Unit ID + FC + Data)
UnitID — адрес устройства (для RTU-TCP шлюзов)
Порт: 502 (стандартный, зарезервирован IANA)
TCP обеспечивает надёжную доставку — CRC не нужен. Но помните: TCP не обеспечивает реальное время. Задержки могут варьироваться от единиц миллисекунд до нескольких секунд при перегрузке сети.
Практика: Modbus на Python
Библиотека pymodbus (полноценная реализация)
# Установка: pip install pymodbus
from pymodbus.client import ModbusTcpClient, ModbusSerialClient
import struct
import time
# ========== MODBUS TCP ==========
def read_vfd_status_tcp(host: str, port: int = 502, unit_id: int = 1) -> dict:
"""
Читаем параметры частотника по Modbus TCP.
Пример для ABB ACS550.
"""
client = ModbusTcpClient(host=host, port=port, timeout=3)
if not client.connect():
raise ConnectionError(f"Не удалось подключиться к {host}:{port}")
try:
# Читаем Input Registers 30001-30006 (адрес 0-5)
result = client.read_input_registers(
address=0, # Начальный адрес (0 = регистр 30001)
count=6, # Количество регистров
slave=unit_id
)
if result.isError():
raise Exception(f"Ошибка Modbus: {result}")
regs = result.registers
return {
'status_word': regs[0], # Слово состояния
'speed_rpm': regs[1], # Скорость об/мин
'frequency_hz': regs[2] / 100.0, # Частота (0.01 Гц)
'current_a': regs[3] / 10.0, # Ток (0.1 А)
'voltage_v': regs[4], # Напряжение
'power_kw': regs[5] / 10.0, # Мощность (0.1 кВт)
'running': bool(regs[0] & 0x0001),
'fault': bool(regs[0] & 0x0008),
}
finally:
client.close()
def write_vfd_command_tcp(host: str, freq_hz: float, run: bool, unit_id: int = 1):
"""Управление частотником через Modbus TCP"""
client = ModbusTcpClient(host=host, port=502, timeout=3)
if not client.connect():
raise ConnectionError(f"Не удалось подключиться")
try:
# Задаём частоту (Holding Register 40002, адрес 1)
freq_raw = int(freq_hz * 100) # 50.00 Гц → 5000
client.write_register(address=1, value=freq_raw, slave=unit_id)
# Команда пуск/стоп (Holding Register 40001, адрес 0)
control_word = 0x0002 if run else 0x0001 # 2=Run, 1=Stop
client.write_register(address=0, value=control_word, slave=unit_id)
print(f"VFD: {'Пуск' if run else 'Стоп'}, частота {freq_hz} Гц")
finally:
client.close()
# ========== MODBUS RTU ==========
def create_rtu_client(port: str, baudrate: int = 9600) -> ModbusSerialClient:
"""Создаём RTU клиент для RS-485"""
return ModbusSerialClient(
port=port, # '/dev/ttyUSB0' или 'COM3'
baudrate=baudrate,
bytesize=8,
parity='N', # N=None, E=Even, O=Odd
stopbits=1,
timeout=1.0
)
def scan_modbus_rtu_network(port: str, baudrate: int = 9600) -> list:
"""
Сканирование Modbus RTU сети — ищем все активные устройства.
Возвращает список адресов ответивших устройств.
"""
client = create_rtu_client(port, baudrate)
client.connect()
found_devices = []
print(f"Сканирование Modbus RTU на {port}, {baudrate} бод...")
for address in range(1, 248):
try:
# Пробуем прочитать 1 регистр — если устройство есть, оно ответит
result = client.read_holding_registers(0, 1, slave=address)
if not result.isError():
found_devices.append(address)
print(f" ✅ Найдено устройство: адрес {address}")
except Exception: pass # Таймаут — устройства нет
time.sleep(0.05) # Пауза между запросами
client.close() print(f"Найдено устройств: {len(found_devices)}")
return found_devices # ========== РАБОТА С FLOAT ========== def registers_to_float(high_reg: int, low_reg: int, byte_order: str = 'big') -> float: """ Конвертация двух Modbus-регистров в float IEEE 754. byte_order: 'big' (ABCD), 'little' (DCBA), 'big_swap' (BADC), 'little_swap' (CDAB) """ if byte_order == 'big': raw = struct.pack('>HH', high_reg, low_reg) elif byte_order == 'little': raw = struct.pack('<HH', low_reg, high_reg) elif byte_order == 'big_swap': raw = struct.pack('>HH', ((high_reg & 0xFF) << 8) | (high_reg >> 8), ((low_reg & 0xFF) << 8) | (low_reg >> 8)) else: raw = struct.pack('>HH', high_reg, low_reg) return struct.unpack('>f', raw)[0] def float_to_registers(value: float, byte_order: str = 'big') -> tuple: """Конвертация float в два Modbus-регистра""" raw = struct.pack('>f', value) high_reg, low_reg = struct.unpack('>HH', raw) if byte_order == 'big': return high_reg, low_reg elif byte_order == 'little': return low_reg, high_reg return high_reg, low_reg # ========== ПРИМЕР OPROS SCADA ========== class ModbusDataCollector: """ Циклический опрос Modbus-устройств для SCADA/мониторинга. """ def __init__(self, host: str): self.client = ModbusTcpClient(host=host, port=502, timeout=5) self.data = {} def poll_all_devices(self) -> dict: """Опросить все устройства и вернуть данные""" if not self.client.connect(): return {'error': 'connection_failed'} try: results = {} # Насос 1 (Unit ID = 1) pump1 = self.client.read_input_registers(0, 4, slave=1) if not pump1.isError(): r = pump1.registers results['pump1'] = { 'running': bool(r[0] & 1), 'fault': bool(r[0] & 8), 'freq_hz': r[1] / 100.0, 'current_a': r[2] / 10.0, 'power_kw': r[3] / 10.0, } # Датчик давления (Unit ID = 5, счётчик давления) pressure = self.client.read_input_registers(0, 2, slave=5) if not pressure.isError(): r = pressure.registers results['pressure_bar'] = registers_to_float(r[0], r[1]) # Расходомер (Unit ID = 6) flow = self.client.read_input_registers(0, 4, slave=6) if not flow.isError(): r = flow.registers results['flow'] = { 'instant_m3h': registers_to_float(r[0], r[1]), 'total_m3': registers_to_float(r[2], r[3]), } return results finally: self.client.close() # ========== ЗАПУСК ========== if __name__ == "__main__": # Пример использования collector = ModbusDataCollector('192.168.1.100') while True: data = collector.poll_all_devices() print(f"Давление: {data.get('pressure_bar', 0):.2f} бар") print(f"Насос 1: {'Работает' if data.get('pump1', {}).get('running') else 'Стоит'}, " f"{data.get('pump1', {}).get('freq_hz', 0):.1f} Гц") time.sleep(1)
Реализация Modbus Slave на микроконтроллере (C)
Иногда нужно сделать собственное устройство с Modbus-интерфейсом. Вот минимальная реализация для STM32/Arduino:
#include <stdint.h>
#include <string.h>
#define MODBUS_ADDRESS 1 // Адрес нашего устройства
#define HOLDING_REG_COUNT 20 // Количество Holding Registers
#define INPUT_REG_COUNT 10 // Количество Input Registers
// Данные регистров
static uint16_t holding_regs[HOLDING_REG_COUNT] = {0};
static uint16_t input_regs[INPUT_REG_COUNT] = {0};
// Расчёт CRC16
static uint16_t crc16(uint8_t *buf, uint16_t len)
{
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= buf[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 1) { crc = (crc >> 1) ^ 0xA001; }
else { crc >>= 1; }
}
}
return crc;
}
// Отправка ответа (реализуйте под вашу платформу)
extern void uart_send(uint8_t *data, uint16_t len);
// Обработка Modbus запроса
void modbus_process_request(uint8_t *request, uint16_t req_len)
{
uint8_t response[256];
uint16_t resp_len = 0;
// Проверяем адрес
if (request[0] != MODBUS_ADDRESS) return;
// Проверяем CRC
uint16_t received_crc = (request[req_len-1] << 8) | request[req_len-2];
uint16_t calculated_crc = crc16(request, req_len - 2);
if (received_crc != calculated_crc) return; // CRC ошибка — игнорируем
uint8_t fc = request[1];
uint16_t addr = (request[2] << 8) | request[3];
uint16_t count = (request[4] << 8) | request[5];
response[resp_len++] = MODBUS_ADDRESS;
response[resp_len++] = fc;
switch (fc)
{
case 0x03: // Читать Holding Registers
{
if (addr + count > HOLDING_REG_COUNT) {
// Исключение: неверный адрес
response[1] = fc | 0x80;
response[resp_len++] = 0x02;
break;
}
response[resp_len++] = count * 2; // Количество байт
for (uint16_t i = 0; i < count; i++) {
response[resp_len++] = holding_regs[addr + i] >> 8;
response[resp_len++] = holding_regs[addr + i] & 0xFF;
}
break;
}
case 0x04: // Читать Input Registers
{
if (addr + count > INPUT_REG_COUNT) {
response[1] = fc | 0x80;
response[resp_len++] = 0x02;
break;
}
response[resp_len++] = count * 2;
for (uint16_t i = 0; i < count; i++) {
response[resp_len++] = input_regs[addr + i] >> 8;
response[resp_len++] = input_regs[addr + i] & 0xFF;
}
break;
}
case 0x06: // Записать один Holding Register
{
if (addr >= HOLDING_REG_COUNT) {
response[1] = fc | 0x80;
response[resp_len++] = 0x02;
break;
}
holding_regs[addr] = (request[4] << 8) | request[5];
// Эхо запроса как ответ
memcpy(response + 2, request + 2, 4);
resp_len += 4;
break;
}
case 0x10: // Записать несколько Holding Registers
{
if (addr + count > HOLDING_REG_COUNT) {
response[1] = fc | 0x80;
response[resp_len++] = 0x02;
break;
}
uint8_t byte_count = request[6];
for (uint16_t i = 0; i < count; i++) {
holding_regs[addr + i] = (request[7 + i*2] << 8) | request[8 + i*2];
}
// Ответ: адрес и количество записанных регистров
response[resp_len++] = request[2];
response[resp_len++] = request[3];
response[resp_len++] = request[4];
response[resp_len++] = request[5];
break;
}
default:
{
// Неизвестная функция
response[1] = fc | 0x80;
response[resp_len++] = 0x01; // Illegal Function
break;
}
}
// Добавляем CRC
uint16_t resp_crc = crc16(response, resp_len);
response[resp_len++] = resp_crc & 0xFF;
response[resp_len++] = resp_crc >> 8;
// Отправляем ответ
uart_send(response, resp_len);
}
// Обновление Input Registers из реальных данных
void modbus_update_inputs(uint16_t temp_x10, uint16_t pressure_x100,
uint16_t status_bits)
{
input_regs[0] = temp_x10; // Температура × 10 (250 = 25.0°C)
input_regs[1] = pressure_x100; // Давление × 100 (1013 = 10.13 бар)
input_regs[2] = status_bits; // Биты состояния
}
Диагностика сети Modbus: практические инструменты
Wireshark для Modbus TCP
Wireshark понимает Modbus TCP "из коробки". Фильтр для захвата:
modbus.func_code — фильтр по функциональному коду
tcp.port == 502 — весь Modbus TCP трафик
modbus.exception_code — только ошибки
ModRSsim2 / Diagslave — эмуляторы слейва
Незаменимы при разработке — тестируете мастер без реального оборудования.
Свой анализатор на Python:
import socket
import struct
def modbus_tcp_sniffer(host: str, port: int = 502):
"""Простой анализатор Modbus TCP запросов"""
FC_NAMES = {
1: 'Read Coils',
2: 'Read Discrete Inputs',
3: 'Read Holding Registers',
4: 'Read Input Registers',
5: 'Write Single Coil',
6: 'Write Single Register',
15: 'Write Multiple Coils',
16: 'Write Multiple Registers',
}
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
while True:
# Читаем MBAP-заголовок (7 байт)
header = sock.recv(7)
if len(header) < 7:
break
transaction_id = struct.unpack('>H', header[0:2])[0]
protocol_id = struct.unpack('>H', header[2:4])[0]
length = struct.unpack('>H', header[4:6])[0]
unit_id = header[6]
# Читаем PDU
pdu = sock.recv(length - 1)
fc = pdu[0]
fc_name = FC_NAMES.get(fc, f'Unknown FC {fc}')
if fc < 0x80: # Запрос или нормальный ответ
if len(pdu) >= 5:
addr = struct.unpack('>H', pdu[1:3])[0]
count = struct.unpack('>H', pdu[3:5])[0]
print(f"[{transaction_id}] Unit={unit_id} | {fc_name} | "
f"Addr={addr} Count={count}")
else: # Ошибка
exc_code = pdu[1]
print(f"[{transaction_id}] EXCEPTION: FC={fc & 0x7F} Code={exc_code}")
Типичные проблемы и решения
"Устройство не отвечает"
Алгоритм диагностики:
Проверьте физику: контакты, полярность A/B, терминаторы
Проверьте параметры порта: baudrate, parity, stopbits — должны совпадать с устройством
Проверьте адрес — он правильно задан в устройстве? Не все устройства имеют адрес "1" по умолчанию
Используйте осциллограф или логический анализатор — видны ли данные на линии?
Попробуйте другой кабель
"Иногда не отвечает, CRC-ошибки"
Слишком длинная линия без репитера
Отсутствуют или не там стоят терминаторы
Несколько устройств с одинаковым адресом
Помехи от силового оборудования — проложите кабель отдельно
"Данные неверные"
Неправильный порядок байт (endianness) для float
Смещение адреса: в документации адреса часто указываются с 1 (40001), а в запросе нужно с 0 (0x0000)
Неправильный масштабный коэффициент (×10, ×100, ×0.01...)
Заключение
Modbus — это фундамент промышленной коммуникации. Несмотря на почтенный возраст, он остаётся стандартом де-факто и будет таковым ещё долгие годы. Понимание структуры фрейма, типов данных и физического уровня RS-485 — это базовый навык любого инженера автоматики.
Для новых проектов, где нет ограничений совместимости, стоит рассматривать OPC UA или MQTT как более современные альтернативы. Но если перед вами стоит задача интегрировать любое промышленное оборудование — с вероятностью 90% оно имеет Modbus, и знание этого протокола решит задачу быстро и надёжно.
Create an account or sign in to leave a review
There are no reviews to display.