Здравствуй мой друг. Сегодня я расскажу тебе о том как устроен протокол обмена MODBUS, и мы оба погрузимся в этот замечательный мир

Надеюсь что ты уже прочитал о том как устроен RS485, потому что на практике MODBUS используется поверх него.
Вкратце, ситуация такова:
- Есть несколько устройств в группе
- У каждого устройства есть свой серийный номер
- В один момент времени только одно устройство может передавать данные, остальные будут их слышать.
- Если кто-то передает данные, остановить передачу другим устройством невозможно.
Устройства делятся на две категории:
- Ведущие (Master) — Терминалы, компьютеры, маршрутизаторы. Устройства что управляют сетью, хотят что-то от неё. Обычно такое устройство одно
- Ведомый (Slave) — Моторы, датчики, измерители, хранилища данных. Они ничего не хотят от мира сего и только делают то что сказано. Их может быть много (500+)
Наверное ты сам можешь придумать как лучше устроить обмен данными. Попробуй сейчас, если интересно. А после прочитай как поступил бы я
Я начинаю: наша цель — передача данных. В данной ситуации обязателен арбитраж передачи — то есть кто-то или что-то должно контролировать кто может сейчас передавать данные. Тут два варианта:
- Выполнить синхронизацию времени, а после выделить для каждого устройства свой промежуток времени для передачи (если количество секунд оканчивается на 1, то передает одно устройство, если на 2, то другое). Слишком сложно, до-свидания. Используйте CAN
- Использовать формат запрос-ответ. Мастер отправляет пакет и обязан получить ответ в течении определенного промежутка времени. Slave устройства могут только отвечать
Второй вариант звучит весьма практично. Вопрос только в структуре пакета
Хотелось бы иметь такую:

- 1 или 4 байта — номер устройства в сети (в его качестве можно использовать младшую часть серийного номера)
- Байт что определяет тип пакета: чтение/запись/что-то ещё?
- Дополнительные данные для команды:
- Для чтения/записи — адрес «ячейки» с которой производится запись или чтения
- + для чтения/записи — размер данных для записи/чтения
- + для записи — данные
- Дополнительно — CRC16 (или 32) по всему что можем на проверку того что данные не повреждены
В ответ получаем:
- Те-же самые байты адреса.
- Байт что определяет тип пакета: чтение успешно/запись успешна/ошибка/что-то ещё?
- Дополнительные данные для команды:
- Для успешных команд — адрес «ячейки» с которой производится запись или чтения
- + для успешных команд — размер данных для записи/чтения
- + для успешного чтения — данные
- Так-же CRC16

Конец статьи, начало драмы
Хорошо, но что с MODBUS?
Ахахах, всё хорошо.
Начнем с того что есть три типа MODBUS:
- MODBUS RTU — классический RS485 вариант, статья именно о нем
- MODBUS TCP — кансерный сетевой вариант, забудь о нем и лучше используй JSON-RPC/REST.
- MODBUS ASCII — старый кансерный RS485 вариант. К счастью, мертв.
MODBUS RTU во многом похож на то что я уже описал, но есть некоторые отличия. Вот структура пакета:

Начав с конца, всё очень хорошо (было бы так просто со всем). Для проверки на ошибку к концу пакета добавляется CRC16/MODBUS. Есть даже удобные онлайн калькуляторы. На этом всё про Error check. Совсем всё.
Для дальнейшего погружения, возьмём спецификацию, её не трудно найти, но если тебе интересно, вот она:
Смотри, видишь? Уже на первой странице мы видим что модбас поддерживает 21 ( ДВАДЦАТЬ ОДНУ БЛЯТЬ!!! ) Команду. Что только-что произошло? Чтож, давай посмотрим на первые ЧЕТЫРЕ:
- Команда чтения регистра — 0x03 . Она читает регистры
- Команда чтения регистра типа только чтение — 0x04 . Она работает как 0x03 но только с регистрами Read-only типа.

ЗАЧЕМ ЧТО ПОЧЕМУ? Я повторюсь, у нас есть ДВЕ команды которые делают ОДНО И ТО-ЖЕ. Только если в те данные что ты читаешь можно записывать, ты используешь 0x03, иначе 0x04. ЛОГИЧНО. Ладно, наверное не всё так плохо. Ну не может же у нас быть четыре(пять) команд что делают одно и то-же?
- Команда чтения однобитного регистра — 0x01 . Она читает регистры. Размер передается в битах
- Команда чтения однобитного регистра типа только чтение — 0x02 . Она работает как 0x01 но только с регистрами Read-only типа.
Чем отличается 0x01 от 0x03? Тем что для 0x03 размер передается в словах (2 байта), а для 0x01 размер передается в БИТАХ.
То-есть если ты используешь 0x03, говоришь, дай мне 4 байта, тебе дают 4 байта.
Либо ты используешь 0x01, говоришь, дай мне 32 бита, тебе дают 4 байта.
С записью похожие осложнения
Keep it simple, stupid
Утерянный принцип проектирования ПО
Команды, к сожалению, встречаемые на практике:
- 0x03 / 0x04 — Похожие команды. Чтение
- 0x06 — Запись одного регистра
- 0x10 — Запись нескольких регистров
- 0x14 / 0x15 — чтение/ запись с расширенным адресом ( позволяет работать с большими хранилищами)
Обычно устройство реализует только 2-4 команды

Структура пакетов команд выглядит достаточно просто, не вижу смысла всё разжевывать, но при необходимости разобраться не трудно, однако, и тут есть кое-что странное.
А именно — Количество передаваемых регистров (Quantity of Registers) передается в виде количества слов (слово MODBUS = 2 байта). Однако сдвиг чтения/записи (Starting Adress) передается без явного указания размерности. Почитав документацию можно заметить что имеется ввиду что размерность сдвига также в словах, однако на практике, многие приборы MODBUS работают с ней как с байтами.
Это ужасная правда — стандарта MODBUS не существует
Знаешь что с адресом? Тем что добавляется в начале. Я прочитаю стандарт:
- Адрес — 1 байт
- Значение 0 — мультикаст. Применяется при прямом подключении. Любое устройство должно ответить
- Значения 1-247 — Допустимые индивидуальные адреса
247 — Это очень мало адресов и крайне неудобно к настройке. Нужно вручную устанавливать ID каждому устройству (а они могут быть и без физической настройки, только по ID=0).
Гораздо удобнее было бы использовать слегка случайные 4 байта (например часть серийного номера) и в большинстве случаев настройка будет не нужна — именно так и поступает львиная доля устройств MODBUS
На практике встречаются устройства MODBUS как с адресом 4 байта, так и с адресом в 1 байт
Но это всё мелочи
.
.
.
.
.

Строжайшее табу программистов — Никогда, ни при каких условиях, не меняй байты местами (только если их до тебя не поменял кто-то другой)
Иначе за тобой придут, ими будет движить лишь жажда мести, придут те, кому пришлось поддерживать твой код
Стандарт MODBUS использует слова по 2 байта. В словах используется обратный порядок байт. uint16_t x=1
здорового человека в памяти хранится как два байта {0x01, 0x00}
(именно поэтому можно кастовать uint16_t*
в uint8_t*
) . MODBUS требует отправки в виде {0x00, 0x01}
То есть они говорят что перед пересылкой данных тебе нужно поменять каждый четный/нечетный байт местами, а после принятия возвращать обратно. БЕЛИССИМО!
Проблема заключается в том что одна половина разработчиков устройств следует этому правилу, другая половина игнорирует, считая их лишними, и ещё половина соблюдает эти правила ровно на половину.
Если тебе нужно прочитать число типа uint32_t
, 4 байта. То они могут прийти в абсолютно любом порядке вместо ожидаемого {0x01, 0x02, 0x03, 0x04}
может спокойно прийти как {0x02, 0x01, 0x04, 0x03}
так и {0x03, 0x04, 0x01, 0x02}
или {0x04, 0x03, 0x02, 0x01}
. При этом не важно что ты ждал, 3 из 4 что ты получишь что-то другое. Некоторые реализации даже упаковывает числа по 3 байта (uint24_t
, patent pending).
Стандарт никак не определяет передачу чисел типа float
/ double
/ uint64_t
. И каждый передает их как может. Даже если тебе кажется что предусмотрел все возможные способы передачи этих типов, где-нибудь обязательно будет устройство что сможет тебя удивить. А если его нет, то рано или поздно его кто-нибудь создаст.
И да, при передачи 64бит данных по MODBUS соблюдай align, а то привет hardfault
В сухом остатке:
MODBUS больше не стандарт, а принцип
Создавая MODBUS устройство — пытайся не усложнять и используй минимум фич стандарта
Используя MODBUS устройство — опирайся не на стандарт, а на документацию производителя устройства
Не стоит пытаться разработать универсальное решение. На практике, каждый производитель будет добавлять свою щепотку костылей.
Грандиозные провалы существуют для того чтобы их не повторил ты
Знаю, звучит как вызов 🙂
MODBUS — наглядный пример того чем заканчивается разработка ПО если гнаться за оптимизациями и красивой архитектурой. Результат — отсутствие практичности
Это одна из самых частых ошибок разработки, и чем больше усилий было приложено, тем ужаснее результат ( для смелых — почитай о таком кринге как Bluetooth Low Energy и протоколы ГОСТ )
Даже со всеми своими недостатками, MODBUS достаточно прост и удобен. Главное — не усложняй.