Modbus. Что это и с чем его едят

Modbus это протокол передачи данных по типу клиент-сервер. обеспечивающий передачу данных между устройствами подключенным к различным шинам или сетям. Разработан был в далеком далеком 1979 году и с тех пор обрел неплохую популярность среди миллионов автоматических устройств. Поддержка протокола происходит и по сей день.

Как и свойственно клиент-серверному общению, коммуникации происходят по правилу запрос-ответ, сообщения снабжаются функциональными кодами, которые и говорят о его типе. Передача может происходить по нескольким типам сетей:

  • TCP/IP
  • Асинхронные последовательные линии передач (EIA/TIA-232-E, EIA-422, EIA/TIA-485-A, оптоволокно, радио и пр.

Структура пакета

Протокол Modbus описывает простейший типа пакета PDU (Protocol data unit), который лежит в основе всех пакетов Modbus. В зависимости от типа передачи, пакет может дополняться дополнительными полями уже на уровне ADU (Application data unit).

Клиент создает ADU пакет и инициирует передачу, после получения пакета сервер разбирает пакет и по функциональному коду уже решает, что делать с данными. Функциональные коды могут быть в пределах от 1 до 255, где с 128 по 255 коды зарезервированные под ошибки. Поле данных же содержит дополнительную информацию для сервера которая используется в соответствии с функциональным кодом. Это может быть адрес регистра, количество пунктов которые будут обработаны или количество байт в поле. В каких то типах вопросов поле данных может отсутствовать вообще (нулевой длинны).

Если сервер успешно принял пакет и обработал данные, то он возвращает ответ в той форме, в которой этого требует действие декларированное функциональным кодом. Например, если клиент хочет изменить какой либо регистр сервера, то он отправляет соответствующий функциональный код и необходимые и если сервер успешно обработал запрос, то он возвращает точно такой же пакет, который и получил. Но если что то пошло не так, то сервер генерит ошибку, состоящая из функционального кода ошибки и самого кода ошибки:

Размер PDU ограничен по размеру. Для последовательной линии передачи данных это = 256 байт - Адрес сервера (1 байт) - CRC (2 байта). Получается, что размер пакета ограничен 253 байтами. Следовательно:

  • RS232/R485 размер ADU = 253 байта + Адрес сервера (1 байт) + CRC (2 байта) = 256 байт
  • TCP размер ADU = 253 байта + MBAP (7 байт) = **260 байт **

Протокол Modbus использует big-endian порядок байтов для адреса и данных. Это значит, что длинное число отправленное в пакете начинается с левого байта к правому. Например, значение 0x1234 разобьется на два байта и будет отсылаться в порядке, сначала 0x12, а затем 0x34.

Modbus основан на модели данных из серии таблиц, которые имеют различные характеристики

Адресация элементов таблицы задается 16-значным адресом начиная с 0, что позволяет каждой таблице содержать до 2^16 = 65536 элементов. Очевидно предположить, что все данные обрабатываемые с помощью Modbus (биты и регистры) должны быть расположены в памяти устройства. Но физический адрес в памяти не стоит путать ссылкой на данные. Единственным требованием является описание указанных адресов с физическим адресом в устройстве.

Такое представление типов данных можно сравнить с привычными нами таблицами регистров. Различием является только лишь то, что мы сами придумываем таблицу регистров для нашего сервера.

Обработка пакета PDU на сервере должна выглядеть примерно так:

Возврат ошибки от сервера должен выглядеть как PDU пакет, где в функциональным кодом является код ошибки + 0x80. А код исключения должен описывать причину ошибки.

Распределение функциональных кодов

Все функциональные код протокола Modbus можно разделить на три категории:

Публичные функциональные коды:

Четко определенные функциональные коды, подтвержденные сообществом Modbus.org, публично задокументированы и имеют в своем распоряжении тест соответствия, включают в себя как указанные функциональные коды так и не указанные, зарезервированные для использования в будущем.

Функциональные коды указанные пользователем:

Существует два диапазона таких кодов. от 65 до 72 и от 100 до 110. Пользователь волен выбирать свои действия которые сервер будет совершать получив пакет с данным кодом.

Зарезервированные функциональные коды:

Эти коды в настоящее время используются некоторыми компаниями для устаревших продуктов и не доступны для публичного использования.

Описание функциональных кодов

0x01 - Чтение регистров флагов

Этот код используется для чтения от 1 до 2000 регистров флагов по очереди. Пакеты такого запроса будут выглядеть следующим образом:

Функциональный код: 1 байт - 0x01
Адрес первого флага: 2 байта - от 0x0000 до 0xFFFF
Количество флагов: 2 байта - от 1 до 2000 (0x7D0)

Ответ же будет таким

Функциональный код: 1 байт - 0x01
Количество флагов: 1 байт - n штук
Флаги: n байт 

В случае ошибки ответ будет таким

Функциональный код: 1 байт - функциональный код+0x80
Код ошибки: 1 байт - или 01 или 02 или 03 или 04

По данной картинке можно понять какой код будет говорить о какой ошибке.

0x02 - Чтение дискретных входов

Эта функция работает аналогично 0x01

0x03 - Чтение регистров хранения

Здесь нам дозволено читать до 125 значений. Структура запроса и ответа аналогична 0x01 за тем лишь исключением, что принимать мы будем не биты а по 2 байта

0x04 - Чтение входных регистров

Аналогично 0x03

0x05 - Запись регистра флага

Здесь мы можем изменить значение регистра флага. С нуля на единичку или наоборот. Для единички используется 0xFF00, а для ноля 0x0000

Функциональный код: 1 байт - 0x01
Адрес флага: 2 байта - от 0x0000 до 0xFFFF
Состояние флага: 2 байта - либо 0x0000, либо 0xFF00

В случае успеха, мы получим точно такое сообщение, в случае провала код ошибки 0x85. Алгоритм работы будет нескольки отличаться:

0x06 - Запись одного регистра

Записывает данные в один регистр.

Функциональный код: 1 байт - 0x08
Адрес регистра: 2 байта - от 0x0000 до 0xFFFF
Состояние регистра: 2 байта - от 0x0000 до 0xFFFF

Блок-схема работы аналогична 0x05

0x07 - Чтение сигналов состояния

Запрос состоит из одного функционального кода, а ответом служит тот же функциональный код с одним байтом ответа. Схема работы такова:

0x08 - Диагностика (Только для последовательно линии)

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

Функциональный код: 1 байт - 0x08
Под-функция: 2 байта 
Данные : N x 2 байта

В качестве Под-функции указывается тип диагностики:

  • 00 - Вернуть отосланные данные
  • 01 - Перезапустить линию передачи
  • 02 - Вернуть регистр диагностики
  • 03 - Изменить ASCII код разделителя
  • 04 - Включить режим только чтения
  • 10 - Очистить счетчики и диагностические регистры
  • 11 - Вернуть количество принятых сообщений на линии
  • 12 - Вернуть количество ошибок полученных на линии
  • 13 - Вернуть количество исключений полученных на линии
  • 14 - Вернуть количество сообщений отправленных сервером
  • 15 - Вернуть количество не отвеченных сообщений
  • 16 - Вернуть количество NAK (Negative Acknowledge) сообщений
  • 17 - Вернуть количество сообщений полученных сервером, когда он был занят
  • 18 - Вернуть количество переполнений
  • 20 - Сбросить счетчик переполнения и флаг

Блок-схема работы:

0x0B  - Вернуть кол-во событий

Используется чтобы получить с сервера два байта статуса и кол-во событий. Пример:

Функциональный код: 1 байт - 0x0B

Ответ:

Функциональный код: 1 байт - 0x0B
Статус: 2 байта - от 0x0000 до 0xFFFF
Код-во событий: 2 байта - от 0x0000 до 0xFFFF

2 байта статуса означают, что если предыдущая команда все еще обрабатывается на удаленном устройстве, то ответ будет 0xFFFF, иначе 0x0000

0x0C - Вернуть лог событий

Возвращает лог событий с момента запуска сервера. Нормальный ответ будет содержать функциональный код, один байт содержащий длину следующего сообщения, два байта статуса, два байта с количеством событий, два байта с количеством сообщений и события длинной в один байт. Более подробное описание этой функции можно посмотреть в документации с сайта modbus.org.

0x0F - Записать несколько регистров флагов

Имеет такую структуру:

Функциональный код: 1 байт - 0x0F
Адрес первого флага: 2 байта - от 0x0000 до 0xFFFF
Количество байт для изменения: 2 байта - от 0x0000 до 0xFFFF
Кол-во байт с данными: 1 байт - N
Данные для записи: N байт

Ответ:

Функциональный код: 1 байт - 0x0F
Адрес первого флага: 2 байта - от 0x0000 до 0xFFFF
Количество байт для изменения: 2 байта - от 0x0000 до 0xFFFF

Например, возьмем такой пакет:

Функциональный код: 1 байт - 0x0F
Адрес первого флага: 2 байта - 0x0013
Количество байт для изменения: 2 байта - 0x000A
Кол-во байт с данными: 1 байт - 0x02
Данные для записи: 1 байт - 0xCD
Данные для записи: 1 байт - 0x01

Разложив данные для записи в бинарное число, мы получим, что 0xCD01 = 1100 1101 0000 0001. Количество байт под изменение 0x000A - 10 штук, начиная с адреса 0x0013. Изменение в этом случае будет происходить сначала в первом байте, а затем во втором. Тем самым получим такие флаги:

0xCD:

  • 1 - на адрес 0x1B
  • 1 - на адрес 0x1A
  • 0 - на адрес 0x19
  • 0 - на адрес 0x18
  • 1 - на адрес 0x17
  • 1 - на адрес 0x16
  • 0 - на адрес 0x15
  • 1 - на адрес 0x14

0x01:

  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0 - на адрес 0x1D
  • 1 - на адрес 0x1C

Алгоритм работы похожий как и с последовательной записью

0x10 - Запись нескольких регистров

Позволяет записать от 1 до 123 регистров на удаленное устройство.

Функциональный код: 1 байт - 0x10
Адрес первого регистра: 2 байта - от 0x0000 до 0xFFFF
Количество регистров для изменения: 2 байта - от 0x0001 до 0x007B
Кол-во байт с данными: 1 байт - 2*N
Данные для записи: N*2 байт

Ответ:

Функциональный код: 1 байт - 0x10
Адрес первого регистра: 2 байта - от 0x0000 до 0xFFFF
Количество регистров для изменения: 2 байта - от 0x0001 до 0x007B

В результате у нас последовательно будут заполняться регистры по типа того, что было в 0x0F

0x11 - Сообщить ID сервера

Используется для чтения описания типа, настоящего статуса и другой информации с устройства

Функциональный код: 1 байт - 0x11

Ответ:

Функциональный код: 1 байт - 0x11
Количество байт в сообщении: 1 байт
ID сервера: Длинна задается устройством
Индикатор запуска: 1 байт - 0x00 = Выключен, 0xFF = Включен
Прочая информация: Длинна задается устройством

0x14 - Чтение из файла

Файлы организованы в виде записей. Каждый файл может содержать до 10000 записей, адресованных от  0000 до 9999 или от 0x0000 до 0x270F.

Функциональный код: 1 байт - 0x14
Количество байт: 1 байт - от 0x07 до 0xF5
Подзапрос x - Тип: 1 байт - 06
Подзапрос x - Номер файла: 2 байта - от 0x0001 до 0xFFFF
Подзапрос x - Номер записи: 2 байта - от 0x0000 до 0x270F
Подзапрос x - Длинна записи: 2 байта - N
Подзапрос x+1 - ... и т.д.

Ответ:

Функциональный код: 1 байт - 0x14
Длинна ответа: 1 байт - от 0x07 до 0xF5
Подзапрос x - Длинна ответа: 1 байт - от 0x07 до 0xF5
Подзапрос x - Тип: 1 байт - 06
Подзапрос x - Данные: N x 2 байт
Подзапрос x+1 - ... и т.д.

0x15 - Запись в файл

Такие же правила как и в 0x14 только происходит запись

Функциональный код: 1 байт - 0x15
Количество байт: 1 байт - от 0x09 до 0xFB
Подзапрос x - Тип: 1 байт - 06
Подзапрос x - Номер файла: 2 байта - от 0x0001 до 0xFFFF
Подзапрос x - Номер записи: 2 байта - от 0x0000 до 0x270F
Подзапрос x - Длинна записи: 2 байта - N
Подзапрос x - Данные: N x 2 байт
Подзапрос x+1 - ... и т.д.

Ответ:

Функциональный код: 1 байт - 0x15
Длинна ответа: 1 байт - от 0x09 до 0xFB
Подзапрос x - Тип: 1 байт - 06
Подзапрос x - Данные: N x 2 байт
Подзапрос x - Номер файла: 2 байта - от 0x0001 до 0xFFFF
Подзапрос x - Номер записи: 2 байта - от 0x0000 до 0x270F
Подзапрос x - Длинна записи: 2 байта - N
Подзапрос x - Данные: N x 2 байт
Подзапрос x+1 - ... и т.д.

0x16 - Запись в регистр по маске

Помогает изменить регистр с использованием AND-маски и OR-маски. Результат будет примерно таким

(Исходное значение AND and-Маска) OR (or-Маска AND (NOT and-маска))

Например:

Исходное значение - 0001 0010
and-Маска         - 1111 0010
or-Маска          - 0010 0101

NOT and-Маска     - 0000 1101

Результат         - 0001 0111

Структура пакета:

Функциональный код: 1 байт - 0x16
Адресс регистра: 2 байт - от 0x0001 до 0xFFFF
And-Маска: 2 байт - от 0x0001 до 0xFFFF
Or-Маска: 2 байт - от 0x0001 до 0xFFFF

В качестве ответа пакет будет полностью дублирован

0x17 - Чтение/Запись нескольких регистров

Функция комбинирует одну операцию чтения и одну операцию записи в одной транзакции.

Функциональный код: 1 байт - 0x17
Адресс регистра для чтения: 2 байт - от 0x0001 до 0xFFFF
Количество регистров для чтения: 2 байт - от 0x0001 до 0x007D
Адресс регистра для записи: 2 байт - от 0x0001 до 0xFFFF
Количество регистров для записи: 2 байт - от 0x0001 до 0x007D
Количество байт: 1 байт - 2 x N
Регистры для записи: N x 2 байт

Ответ:

Функциональный код: 1 байт - 0x17
Количество байт: 1 байт - 2 x N
Байты для чтения: N x 2 байт

0x18 - Чтение данных из очереди

Функция позволяет читать очередь FIFO (первый вошел - первый вышел) из регистра на удаленном устройстве.

Функциональный код: 1 байт - 0x18
Адресс очереди: 2 байт - от 0x0001 до 0xFFFF

Ответ:

Функциональный код: 1 байт - 0x18
Количество байт: 2 байт
Количество элементов очереди: 2 байта

Ну и в заключение описание кодов ошибок, которое я тупо скопировал с википедии:

  • 01 — Принятый код функции не может быть обработан
  • 02 — Адрес данных, указанный в запросе, не доступен
  • 03 — Величина, содержащаяся в поле данных запроса, является недопустимой величиной
  • 04 — Невосстанавливаемая ошибка имела место, пока подчинённый пытался выполнить затребованное действие.
  • 05 — Подчинённый принял запрос и обрабатывает его, но это требует много времени. Этот ответ предохраняет главного от генерации ошибки тайм-аута.
  • 06 — Подчинённый занят обработкой команды. Главный должен повторить сообщение позже, когда подчинённый освободится.
  • 07 — Подчинённый не может выполнить программную функцию, принятую в запросе. Этот код возвращается для неудачного программного запроса, использующего функции с номерами 13 или 14. Главный должен запросить диагностическую информацию или информацию об ошибках от подчинённого.
  • 08 — Подчинённый пытается читать расширенную память, но обнаружил ошибку паритета. Главный может повторить запрос, но обычно в таких случаях требуется ремонт.