Клиент Modbus TCP для Simatic S7-1200 и S7-1500
Что такое клиент Modbus TCP?
Клиент Modbus TCP — это узел, который формирует запросы к серверу: запрашивает данные, передаёт уставки и команды. В терминологии Modbus RTU ему соответствует понятие «мастер» (ведущее устройство). Однако в TCP-версии протокола допускается наличие нескольких клиентов одновременно — в отличие от RTU, где мастер всегда один.
Почему клиент программировать сложнее?
Если для сервера достаточно одного вызова функционального блока и одного блока данных, то с клиентом всё несколько сложнее.
Во-первых, клиент может (и на практике так и происходит) обмениваться данными с несколькими серверами.
Во-вторых, к одному серверу нередко требуется больше одного запроса: прочитать входные регистры, прочитать регистры хранения, записать команды в выходные «койлы» — и всё это в рамках одного соединения.
В-третьих, нельзя забывать про порядок байт в словах. Разные аппаратные платформы используют разные схемы (little-endian и big-endian), и при несовпадении байты приходится переворачивать вручную.
Именно поэтому клиента имеет смысл программировать на языке SCL (ST в терминологии МЭК 61131-3) и «заворачивать» всю обработку в функциональный блок.
Практический пример: общение с двумя серверами
Для большей реалистичности рассмотрим случай, когда контроллер общается с двумя серверами Modbus TCP, отправляя несколько запросов к каждому.
Создание функционального блока
Первым делом создаём функциональный блок ModbusClient на языке SCL и добавляем вызов его экземпляра в OB1.

В области статических переменных (STAT) функционального блока прописываем две структуры TCON_IP_v4 — по одной на каждое соединение с сервером. Это два разных соединения, и каждое требует отдельного описания. В данном примере конфигурируемые соединения не используются.

Заполнение структур TCON_IP_v4
Заполнить структуры можно как программным кодом, так и вручную, просто занеся нужные значения. Выберем второй путь.
InterfaceId — идентификатор сетевого интерфейса контроллера. Клиент Modbus работает на первом интерфейсе. Его ID можно посмотреть в конфигурации устройства — в нашем случае он равен 64. Важно: нужен идентификатор именно интерфейса, а не его портов.

ID — идентификатор соединения. Не путать с идентификатором интерфейса и с «номером» Modbus-устройства. Это внутренний логический номер коннекшена, который программист назначает самостоятельно в диапазоне от 1 до 4096. У каждого соединения должен быть свой уникальный идентификатор. Назначаем ID = 1.
ConnectionType — тип соединения: TCP или UDP. По умолчанию значение 0x0B (11 в десятичной системе) — оставляем TCP.
ActiveEstablished — выставляем в «истину». Поскольку мы работаем как клиент, именно наша сторона должна инициировать соединение.
RemoteAddress — IP-адрес первого сервера. Пусть будет 192.168.43.100.
RemotePort — порт, на котором сервер Modbus TCP ожидает запросы. Стандартный порт — 502.
LocalPort — оставляем равным нулю.

Вторая структура заполняется аналогично, с другим ID и другим IP-адресом.

Первый запрос: читаем один регистр
Теперь пробуем прочитать один регистр хранения с сервера. Для этого перетаскиваем функциональный блок MB_CLIENT из библиотеки в программу. При перетаскивании появляется диалог создания экземпляра — выбираем мульти-экземпляр и корректируем имя.




Параметры вызова MB_CLIENT:
REQ — активирует выполнение опроса. Пока REQ = TRUE, клиент читает данные с сервера или записывает их на сервер.
DISCONNECT — разрывает соединение.
MB_MODE — режим работы клиента. В совокупности со входом MB_DATA_ADDR определяет, какая функция Modbus TCP используется. Для чтения одного или нескольких регистров хранения значение MODE должно быть равно 0.
MB_DATA_ADDR — адрес в адресном пространстве протокола Modbus TCP. В примере указан 40001 — это «первый регистр хранения».
MB_DATA_LEN — количество читаемых или записываемых величин. В нашем случае — единица.
MB_DATA_PTR — переменная или структура данных, куда записывается прочитанное значение. Переменная может находиться в любом блоке данных. В примере объявлена локальная статическая переменная SingleHR типа INT — её размер (2 байта) совпадает с размером одного регистра хранения Modbus. Если размерности не совпадают, вызов завершится ошибкой.
CONNECT — уже созданная структура TCON_IP_V4.
Диагностика ошибок.
После компиляции и загрузки программы сервер не отвечает. Никаких ответов нет.

Дело в том, что флаги успешного (DONE) или неуспешного (ERROR) вызова блока «живут» всего один цикл сканирования программы. Заметить такое быстрое изменение невозможно. Поэтому по флагу ошибки копируем статус вызова в отдельную переменную, а по флагу успешного выполнения — обнуляем статус. Также добавляем флаги управления запросом и соединением.
В описанном случае код ошибки был 80C6. В документации на блок MB_CLIENT этой ошибки нет, но поиск по справочной системе указал на функцию TCON (аналогичные ошибки можно искать среди TSEND, TRECEIVE и похожих блоков). Описание: «The connection partner cannot be reached (network error)».
Причина оказалась банальной: программа-имитатор Modbus не была прописана в разрешениях встроенного брандмауэра Windows. Разрешение было настроено только для частных сетей, а интерфейс программатора был определён как публичная сеть. После изменения настроек брандмауэра обмен заработал.
Чтение вещественной переменной.
Усложним задачу: попробуем считать с сервера одну вещественную переменную (REAL). Это 4 байта, то есть 2 регистра. В вызове увеличиваем количество читаемых регистров до 2 и меняем указатель на прочитанные данные — теперь это внутренняя статическая переменная типа REAL.

Важный нюанс при горячей загрузке.
Если загружать изменённое прикладное ПО «на горячую», с переинициализацией переменных функционального блока ModbusClient в тот момент, когда контроллер уже опрашивает сервер, обмен может прекратиться. На выходе блока появится статус 80A3. Это связано с вмешательством во внутренние структуры обмена из-за переинициализации всего блока. В некоторых случаях это приводило к полной невозможности коммуникаций до перезапуска контроллера переключателем старт/стоп.
Чтобы избежать такого зависания, необходимо сначала «поднять» флаг Srv1Disconnect, провести изменения в переменных функционального блока (имеются в виду переменные интерфейсной части, а не программный код), выполнить загрузку с переинициализацией, а затем вручную возобновить обмен.
Проблема порядка байт (endianness).
Сервер Modbus хранит в двух регистрах вещественную переменную со значением 0.666. Клиент вместо этого числа читает 1.663175E+38 — сильно отличающееся значение. Причина — разный порядок байт в словах и порядок самих слов в двойном слове.
Инструкция SWAP меняет порядок байт в двойном слове, но в данном случае она не помогает — значение всё равно неправильное. Это означает, что сервер отдаёт регистры в «неправильном» порядке, а байты внутри регистров — в «правильном». Информация оказывается «перемешанной».
Решение: складываем результат чтения двух регистров в массив из 4 байт, объявляем ещё один массив из 4 байт для манипуляций. Функция Deserialize «собирает» массив из байт в конкретное значение — в данном случае в вещественную переменную. После одной итерации получаем корректное значение.

Частая ошибка: Unit ID.
В протоколе Modbus RTU все ведомые устройства (слейвы) имеют уникальный адрес в сети. Мастер, формируя запрос, указывает этот адрес — однобайтовое поле в заголовке пакета.
В протоколе Modbus TCP адресом устройства является его IP-адрес. Однако поле Device ID в заголовке сохранилось — и это часто вносит путаницу.
Согласно спецификациям, «обычный» сервер Modbus TCP должен игнорировать это поле. Unit ID учитывается только для устройств, преобразующих Modbus RTU в Modbus TCP (шлюзы, конвертеры протоколов и т.п.).
На практике же многие серверы Modbus проверяют однобайтовый адрес Unit ID. При несовпадении с «собственным» адресом сервер чаще всего не отправляет ответную телеграмму, и клиент возвращает ошибку опроса.
Если столкнулись с таким поведением, откройте экземпляр функционального блока клиента Modbus и найдите в его статических переменных байтовую величину MB_Unit_ID. По умолчанию её значение равно 0xFF (255). «Нормальные» серверы его игнорируют — им достаточно самого факта установления TCP-соединения. Если же сервер «ненормальный», установите здесь вручную Unit ID вашего устройства.

В статических переменных есть и другие полезные настройки: таймауты, количество повторных попыток запроса и т.д. — их можно изучить во встроенной справке.
Работа с несколькими запросами к одному серверу
Теперь вернёмся к исходной задаче: считываем с первого сервера 3 вещественных переменных (6 регистров), начиная с адреса 40001, и записываем одну переменную (начальный адрес 40011). Предполагаем, что порядок слов и байт «правильный».
Шесть регистров — это шесть слов данных и три вещественных переменных. Можно читать информацию в локальный массив байт, а потом средствами дополнительной обработки представлять их в виде трёх вещественных величин (тем же Deserialize), но это лишняя работа.
Гораздо удобнее сразу «разложить» читаемую информацию в собственную структуру. В блоке данных Data создаём структуру из трёх полей типа REAL. Важно: блок данных Data должен быть «стандартным» или «неоптимизированным», иначе будет возникать ошибка опроса (например, 818B). Содержание структуры зависит от того, в каком порядке и в какой форме сервер отдаёт данные.



Организация последовательных запросов.
Для обмена с одним сервером по одному соединению выделен один экземпляр функционального блока коммуникаций. Если выполнять второй запрос, не дожидаясь результатов первого, работать не будет ни один. Запросы необходимо выполнять последовательно: сначала один, после его завершения — следующий.
Для этого добавляем в программный блок переменную «номер шага» (или «номер запроса»). Выбор запроса выполняем в операторе CASE. «Номер запроса» меняется на следующий только в случае успешного или неуспешного выполнения текущего опроса.

Добавление второго сервера
Для опроса второго сервера достаточно подготовить хранилище для локальных переменных (от сервера и к серверу) и дополнить существующую программу. Отличия минимальны: тот же принцип, но другие переменные (включая экземпляр функционального блока MODBUS_CLIENT) и другие запрашиваемые/записываемые данные.

Со вторым сервером настроено отдельное соединение и есть отдельный экземпляр функционального блока Modbus. Его можно вызывать «одновременно» с коммуникациями первого сервера, поэтому в общей программе предусмотрено два независимых оператора CASE.

Дальнейшие доработки
Логичным продолжением будет добавление анализа ответа на каждый запрос, формирование признака достоверности данных и другие улучшения. Например, можно отправлять команды и уставки не принудительно, а только по факту изменения.
На этом описание работы с протоколом Modbus TCP завершается. В следующий раз рассмотрим программирование протокола Modbus RTU.