Здравствуй мой друг. Сегодня я расскажу тебе о том как устроен протокол обмена 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 достаточно прост и удобен. Главное — не усложняй.