Первая спецификация протокола Modbus была опубликована в 1979 году. Протокол предназначен для опроса подчиненных устройств по принципу «запрос-ответ». Modbus RTU (Remote Terminal Unit) работает по последовательному интерфейсу передачи данных (RS-232, RS-485, RS-422). Сегодня речь пойдет о немного измененном протоколе, Modbus TCP, работающий на прикладном уровне стека протоколов TCP/IP.
Для начала посмотрим, как настраивается (программируется, если быть точнее) серверная часть. Modbus TCP Server — аналог Modbus RTU Slave, то есть, является подчиненным устройством. Это важно, не путайте. Сервер лишь отвечает на запросы, но не генерирует их.
В данном примере применяется CPU S7-1516 с версией прошивки 2.6. Серия S7-1200 программируется аналогично.
Для начала разместим в OB1 экземпляр функционального блока MB_SERVER (Instructions → Communications → Others → MODBUS TCP).
Далее необходимо сделать три вещи. Во-первых, подать что-нибудь на вход MB_HOLD_REG. Этот входной пин экземпляра ФБ должен содержать область памяти, которая выделяется на регистры хранения (holding registers).
Небольшое отступление. В версиях библиотек Modbus TCP до 5.0 переменные «дискретные входы» (Discrete inputs), т.е. те двоичные переменные, которые можно только читать — это непосредственно все BOOL'евые переменные из области процесса %I. Coils, «катушки» — дискретные переменные, которые можно и читать, и записывать, это область %Q. Input Registers, «входные регистры» — это слова данных из области %I, точнее %IW. Проще говоря, все дискретные переменные протокола Modbus и аналоговые входа являются переменными областей памяти I или Q. Это дает возможность читать непосредственно значение входов, а так же записывать значения дискретных выходов напрямую. С моей точки зрения нелогично отдавать возможность управлять дискретными выходами какой-нибудь сторонней системе, пусть даже и теоретическую, поскольку в зависимости от построения прикладного ПО контроллера, на эти выхода будет приходить «правильное» с точки зрения системы значение. Для того, чтобы ограничить клиентам Modbus TCP возможность прямого обращения к выходам, в экземпляре функционального блока есть несколько переменных.
Нас интересуют все переменные, которые начинаются на IB и QB. Указав в качестве значений QB_Count, QB_Read_Count и IB_Count нули, вместо значения по умолчанию 65535, мы запрещаем полностью чтение/запись входов/выходов напрямую.
Для чтения/записи регистров хранения, в свою очередь, необходимо отдельно вручную задать область данных. Мой личный опыт показывает, что наиболее удобный способ — это структура, объявленная в глобальном блоке данных со стандартным (а не оптимизированным) доступом. Я сейчас продемонстрирую, «как надо», а под конец данной заметки мы «пройдемся по граблям» и посмотрим типичные ошибки, которые возникнут, если заполнить данное поле неправильно.
В версии библиотеки, начиная с 5.0 (требуется прошивка 2.5 для S7-1500 и 4.2 для S7-1200) можно иначе переназначать входные дискреты, катушки и прочие переменные модбас. Например — завести все в битовые переменные глобального блока данных. Необходимо дополнительная конфигурация, которая описана в пункте «Access to data areas in DBs instead of direct access to MODBUS addresses as of version V5.0» встроенной справки.
Итак, добавляем глобальный блок данных с «неоптимизированным» доступом в картинках.
Нажать Add new block
Выбрать «Data block» и дать ему осмысленное имя, далее нажать ОК
Вызвать свойства свежедобавленного блока данных
Найти в атрибутах блока галочку «Оптимизированный доступ», снять ее, подтвердить снятие и нажать ОК
Открыть в редакторе блок данных и создать в нем отдельную структуру
Заполнить поля этой структуры. Имеет смысл сразу в комментариях делать пометки о номере(адресе) регистра хранения. Откомпилировать блок данных.
Подать созданную структуру на входной пин MB_HOLD_REG
Во-вторых, требуется создать и заполнить структуру типа TCON_IP_v4 или TCON_Configured. Данная структура содержит некоторые подробности для коммуникации контроллера. Лично я предпочитаю первый способ, он мне кажется более аскетичным, а кроме того — он не требует загрузки Hardware, в отличии от второго. В связи с тем, что структура относится к «настроечной» части протокола Modbus, ее можно разместить в уже созданном блоке данных (хотя, никто не запрещает объявить ее, где угодно).
Добавление структуры типа TCON_IP_v4
Поле InterfaceID заполним чуть позже, а сейчас пройдемся по остальным полям.
ID — внутренний идентификатор соединения. Допустимые значения от 1 до 4096. Каждое соединение (экземпляр блока MBSERVER, хотя на самом деле все немного сложнее). должно иметь свой уникальный идентификатор. Ставлю равным 1.
ConnectionType — тип соединения. По умолчанию стоит 11 (0B в шестнадцатеричной системе): TCP. Его и оставляем.
ActiveEstablished — оставляем false, в данном случае сервер не является инициатором связи, инициатором связи являются клиенты.
RemoteAddress — если оставить нули, то к серверу сможет подключиться любой клиент. Если задать удаленный IP-адрес конкретно, то к серверу может обратиться только один явно заданный клиент. Оставляем нули.
RemotePort — оставляю ноль, не ограничиваю и номер порта со стороны клиента
LocalPort — номер TCP порта, по которому будет отвечать сервер. В соответствии со старой-доброй традицией (и RFC) протокол Modbus TCP работает на порту 502 (а игра Doom — по порту 666, но это совсем другая история). Порт 502 я указываю явно.
В итоге получаем следующее:
Осталось задать лишь ID интерфейса. Это присвоение я делаю в программное коде, разместив network с присвоением (MOVE) до вызова блока Modbus. Идентификаторы интерфейсов уже созданы в Step 7 автоматически, необходимо лишь найти нужную переменную. В моем случае Modbus будет работать на интерфейсе X1. Его я и нахожу в списке переменных, выпадающем автоматически.
Можно так же подсмотреть название нужной переменной, зайдя в аппаратную конфигурацию, выбрав нужный интерфейс и вкладку System Constants этого интерфейса. Привожу скриншот всего экрана, чтобы было понятно, где искать.
Можно так же просто указать значение 64 для переменной "ModbusData".CONNECT_Struct.InterfaceId
Далее подаем на вход CONNECT заполненную структуру и получаем следующую программу:
И, наконец, в третьих, не забываем про переменные экземпляра MB_SERVER_DB. Я о них говорил выше, описание этих переменных можно почитать во встроенной справке. Если их не трогать, то обмен, все равно, будет работать, это уже вопрос «доводки» обмена и доступа ко «входам» и «выходам».
Компилируем программу, загружаем ее в контроллер и выходим Online:
Статус 7002 не означает ошибку, он говорит о том, что соединение устанавливается. Обязательно почитайте описание возможных значений поля STATUS, пригодится. Перед тем, как начать читать/записывать данные при помощи стороннего Modbus-клиента, дадим переменным ненулевые значения (разумеется, мои любимые — «число зверя» и «три топора»).
В качестве Modbus-клиента можно использовать любой проверенный софт. Главное — правильно сформировать запрос со стороны клиента. В нашем случае объявлено всего 5 регистров хранения, и если запросить 10, то сервер Modbus вернет ошибку, и будет прав. Второй немаловажный момент — не забывайте про порядок байт в слове: если little endian отображать, как big endian, или наоборот, то вместо разумных чисел на экране будет ерунда. На данном скриншоте клиент настроен на опрос 5 регистров хранения, представление данных, как float, настроено «переворачивание» байт в словах:
Чуть выше я говорил, что дискретные выхода контроллера (точнее, биты области %Q) — это и есть «койлы» с точки зрения протокола Modbus, и что при настройках по умолчанию клиент получит возможность как читать сигналы напрямую, так и записывать их. Давайте в этом убедимся. Для начала на модуле дискретных выходов я объявляю переменную, для дальнейшего удобства:
Нулевой бит восьмого байта выходной области. Номер 64, если считать с нуля (8 * 8 + 0 = 64). Задам в контроллере значение «истина» и прочитаю в Modbus-клиенте:
Вижу значение «истина» (читаю один койл с начальным смещением 64). Изменю это значение на «ложь» со стороны modbus:
Значение, разумеется, так же изменилось и в Step 7, и в контроллере, и на выходе модуля (это одно и то же):
Как я уже говорил, нехорошо давать прямой доступ к управлению всяким сторонним системам. Для формирования выходных сигналов есть внутренняя логика программы, продуманные и прописанные алгоритмы управления, защит и блокировок. Поэтому снизим до нуля количество доступных к записи «койлов» (байт выходного пространства, если точнее):
После этого изменения в блоке данных (прямо в online, без перезаливки и перезагрузки контроллера) клиент протокола modbus в ответ на требование записи «катушки» показал табличку «Illegal data address», а именно такое сообщение и вернул сервер. Дополнительную информацию читаем в справке: Restriction of read access to process images as of version V5.0.
Теперь давайте посмотрим, что происходит при некорректном назначении области памяти от регистров хранения. В первую очередь достаем встроенную справку Step 7 и читаем:
В качестве регистров хранения применять глобальный блок данных с оптимизированным доступом или битовую область. И вот тут очень интересно. Потому что справка в части «MBHOLDREG parameter» выглядит следующим образом:
Посередине в качестве примера приведен глобальный блок данных со стандартным доступом. Проведем несколько экспериментов и посмотрим, что из этого получится.
Эксперимент №1. Регистры хранения — это структура в блоке данных с оптимизированным доступом (почти, как сделано в этом примере). В этом случае получаем ошибку 8187 : The MBHOLD_REG parameter has an invalid pointer. Data area is too small.
Эксперимент №2. Массив переменных типа WORD, объявленный в «оптимизированном» блоке данных. Работает, со стороны клиента переменные меняются, ошибок нет.
Эксперимент №3. Меркерная область. Работает, с клиента удалось внести значения, ошибок нет.
С моей точки зрения, в документации недостаточно ясно. Должно быть написано «используйте блок данных со стандартным доступом или битовую (меркерную) память», а не «оптимизированным доступом». В случае оптимизированного доступа вполне подойдут массивы слов. И с моей точки зрения самым удобным способом является способ, описанный в изначальном примере. Эксперимент №2 в принципе тоже работоспособен (и тому есть объяснение), но с моей точки зрения неудобен для работы.
В следующий раз мы займемся клиентом Modbus TCP.