Продолжаем тему программирования протокола Modbus TCP на контроллерах Simatic S7-1500. В прошлый раз речь шла о серверной части, сегодня опишем клиентскую. Клиент Modbus TCP — это узел, который генерирует запросы к серверу, т.е. запрашивает данные и передает уставки/команды. В терминологии Modbus RTU это «мастер», ведущее устройство. В отличии от RTU, в протоколе TCP может быть несколько «мастеров» (правильно — клиентов).

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

По этой причине имеет смысл программировать клиента на языке SCL (ST в терминологии МЭК 61131-3) и «завернуть» всю обработку в функциональный блок. Для большей реалистичности в данном примере контроллер будет общаться с двумя серверами Modbus TCP с несколькими запросами к каждому.

В первую очередь создадим функциональный блок ModbusClient на языке SCL и добавим вызов его экземпляра в OB1.

Далее в области STAT переменных функционального блока необходимо прописать две структуры TCON_IP_v4. Зачем две? Затем, что у нас два соединения с двумя разными серверами. Фактически у нас два разных соединения (connection) и каждое необходимо описать. Как я говорил ранее, возможно применить и конфигурируемые соединения, но в данном примере они не используются.

Объявлено две структуры для связи с двумя серверами

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

Первое поле, InterfaceId. Идентификатор интерфейса (или «сетевой карты») нашего контроллера. Клиент Modbus работает на интерфейсе №1 контроллера, смотрим его ID в конфигурации устройства.

Его ID равен 64. Обращаю внимание, что нужен идентификатор именно интерфейса, а не его портов.

Следующее поле структуры, ID. Это идентификатор соединения. Не путать с идентификатором интерфейса. Не путать с «номером» модбас-устройства. Это некий «внутренний логический номер» коннекшена, который программист назначает самостоятельно в диапазоне от 1 до 4096. У каждого «коннекшена» должен быть свой уникальный идентификатор. Ответственность за корректное присвоение целиком на ваших плечах. Назначаем ID = 1 и едем дальше.

Далее идет тип соединения, ConnectionType — TCP или UDP. По умолчанию значение этого поля 0x0B в hex или 11 в dec. Оставляем по умолчанию, TCP.

Флаг ActiveEstablished. Выставляем его в «истину». В случае клиента именно наша сторона должна инициировать соединение.

RemoteAddress. Тут пишем IP-адрес нашего первого сервера. Пусть будет 192.168.43.100.

RemotePort. Номер порта, по которому сервер Modbus TCP будет отвечать на наши запросы. По умолчанию все сервера этого протокола должны «слушать» порт 502.

LocalPort. Оставляем равным нулю.

В итоге, описание соединения с первым сервером выглядит следующим образом.

Описание соединения с первым сервером

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

Сделаем первые робкие шаги и попробуем прочитать один регистр хранения с одного сервера. Для начала надо перетащить ФБ MB_CLIENT из библиотеки в программу.

После перетаскивания появится диалогое окно о создании экземпляра. Выбираем мульти-экземпляр и немного корректируем имя.

Выбираем мультиэкзепляр

Промежуточный итог

Приведем вызов в «человеческий вид»

Кратко пройдемся по параметрам этого вызова. Подробное описание — в нашей горяч0 любимой документации, которую мало кто, почему-то, читает.

REQ активирует выполнение опроса. Пока REQ = TRUE, клиент проводит чтение данных с сервера или запись данных на сервер.

DISCONNECT — разорвать соединение

MB_MODE — «режим» работы клиента. В совокупности со входом MB_DATA_ADDR оказывает влияние на используемую функций Modbus TCP. Возможные значения описаны в документации. Для чтения одного или нескольких регистров хранения значение MODE должно быть равно 0.

MB_DATA_ADDR указывает адрес в адресном пространстве протокола Modbus TCP. Значение нашего примера 40001 — «первый регистр хранения»

MB_DATA_LEN — количество читаемых или записываемых величин. В нашем случае — единица. В итоге все три указанных выше параметра означают «читать один регистр хранения начиная с адреса 40001»

MB_DATA_PTR — переменная или структура данных, куда мы записываем прочитанное значение. Переменная может быть в любом блоке данных, я объявил локальную статическую переменную SingleHR типа INT, размер которой равен 2 байтам и совпадает с размером одного регистра хранения Modbus. При несовпадении размерности читаемой области данных с локальным «хранилищем» вызов функционального блока завершится ошибкой.

CONNECT — уже созданная нами структура типа TCON_IP_V4

Остается только запустить на ноутбуке сервер Modbus, скомпилировать и загрузить программу контроллера, и… не получить ничего. Сервер не отвечает. Ответов нет. Вообще ничего нет. Ничего. По буквам — Николай, Илья, Харитон… ( «Остапа понесло» ). Для того, чтобы уточнить ошибку, необходимо доработать программу следующим образом.

Дело в том, что флаги успешного (DONE) или неуспешного (ERROR) вызова блока «живут» всего один цикл сканирования программы. Естественно, невозможно заметить настолько быстрое изменение. Поэтому по флагу ошибки я копирую статус вызова в отдельную переменную. А по флагу успешного выполнения — обнуляю статус.

Кроме того, добавлены флаги «управления» запросом и соединением.

Немного прокоментирую ошибку, которая возникла у меня. В моем случае код ошибки был 80C6. В описании на блок MB_CLIENT этой ошибки нет, поэтому я вбил код ошибки в поиск справочной системы и нашел ссылку на функцию TCON (так же при неоднозначных ошибках можно искать и среди TSEND, TRECEIVE и прочих похожих блоках). Описание: The connection partner cannot be reached (network error). Ошибки сети. Ответ был очень прост и заключался в том, что программа-иммитатор Modbus не была прописана в разрешениях встроенного в Windows Firewall. Точнее, разрешение на ее работу было настроено только на частные сети, а интерфейс программатора был назначен в качестве публичной сети. Это лишний раз подчеркивает, что техника виновата в последнюю очередь, а чаще всего ошибку надо искать в радиусе закругления рук и в соответствии этого радиуса ГОСТам. После изменения настроек брэндмауэра ОС обмен заработал.

Усложним теперь задачу самую малость, и попробуем считать с сервера одну вещественную переменную. Одна вещественная переменная (REAL) — это 4 байта. Или 2 регистра. Итого, в вызове я увеличил количество читаемых регистров до 2, и изменил «указатель» на прочитанные данные. Этот указатель все еще весьма прост — это внутренняя статическая переменная типа REAL (дальше будет интереснее).

Хотелось бы обратить внимание на одну важную деталь. Если прогружать измененное прикладное ПО на горячую, с переинициализацией переменных нашего функционального блока ModbusClient в то время, когда контроллер ведет опрос сервера Modbus, то обмен может прекратиться, и на выходе блока будет стоять статус 80A3. Связано это, разумеется, со вмешательством во внутренние структуры обмена (из-за переинициализации всего блока). В моем случае это приводило к полной невозможности коммуникаций до рестарта контроллера переключателем старт/стоп. Я намеренно изменил сейчас функциональный блок (добавил еще одну переменную), чтобы продемонстрировать эту ошибку:

После стоп/старта контроллера и «поднятия» флага Srv1Req обмен успешно возобновляется. Чтобы не допустить такого «зависания» обмена (как ни крути, это частный случай) необходимо «поднимать» флаг Srv1Disconnect, проводить изменения в переменных функционального блока (имеются в виду именно переменные, т.е. интерфейсная часть блока, а не сам программный код), выполнять загрузку с переинициализацией, а потом вручную возобновлять обмен. Помните же, что флаги REQ и DISCONNECT у нас подключены к переменным, и этими переменными можно управлять, как вручную, так и посредством программного кода.

Пока мы не продвинулись дальше, хочется продемонстрировать описанный выше случай с «разноконечностью» (little-endian и big-endian) данных. В моем примере сервер modbus держит в двух регистрах хранения вещественную переменную со значением 0.666. Наш клиент Modbus вместо этого числа читает 1.663175E+38, что сильно отличается от нужного (увы, вне зависимости от того, покупаем мы или продаем). Связано это, конечно же, с порядком байт в словах и порядком самых слов в двойном слове. Пробуем разрешить ситуацию. Доработаем программный код следующим образом.

Инструкция SWAP меняет порядок байт (конкретно тут — в двойном слове). Но в данном случае она не помогает, на выходе (в переменной Data.Test) все еще находится неправильное значение. Скорее всего, это означает, что сервер отдает регистры в «неправильном» порядке, а байты в «правильном», то есть информация «перемешалась». Радует, что байта всего 4 и составить их в нужном порядке — дело техники.

Итак, снова модифицируем запрос. Складываем результат чтения двух регистров в массив из 4 байт и объявляем еще один массив из 4 байт, которым и будем манипулировать.

Функция Deserialize «складывает» массив из байт в какое-либо конкретное значение, в данном случае — в вещественную переменную. Потребовалась всего одна итерация, чтобы получить корректное значение переменной с сервера Modbus.

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

Кроме того, в статических переменных есть ряд полезных настроек, таких, как таймауты, количество повторных попыток запроса и т.д. Изучить эти переменные можно во встроенной справке.

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

Обращаю внимание, что блок данных Data должен быть «стандартным» или «неоптимизированным», в противном случае вы будете получать ошибку опроса, к примеру — 818B.

Разумеется, содержание этой структуры полностью зависит от того, в каком порядке и в какой форме «отдаются» данные от сервера.

Программа обретает следующий вид, и данные успешно читаются.

0.5, 0.7, 0.33 (с) ВИА «Несчастный случай»

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

В итоге получаем вот такую программу.

Переменная, на основании который выбирается опрос, называется у меня Server1Query (будет еще Server2Query). Выбор запроса выполнется в операторе CASE. «Номер запроса» меняется на следующий лишь в случае успешного или неуспешего выполнения текущего опроса.

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

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

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

Тем не менее, описание работы с протоколом Modbus TCP на этом я заканчиваю. В следующий раз посмотрим на программирование протокола Modbus RTU.