Давно хотел рассказать про тонкости программирования обмена по протоколу Modbus RTU в случае, когда контроллер (в нашем случае S7-1214) выступает RTU Master'ом. Недавно меня попросили помочь с обменом между ПЛК и частотным преобразователем Sinamics V20, ну и почему бы не написать заодно заметку, постаравшись приблизить решение задачи к боевым условиям. Собственно говоря, сами немцы эту тему давно осветили: SINAMICS V: Speed Control of a V20 with S7-1200 (TIA Portal) via USS® protocol/MODBUS RTU with HMI Смотрите этот пример, он сделан очень толково, с визуализацией, диалогами и квестами и возможностью расширить прикладную программу до опроса множества ПЧ V20 по нескольким интерфейсам (S7-1200 позволяет установить в свою корзину до 4 портов RS-485/422). Пример сделан очень хорошо и очень педантично. Вопросов коммуникаций по протоколу Modbus TCP я уже касался ранее, они есть на Хабре. Поэтому некоторые нюансы я повторно объяснять не буду, просто сразу напишу, как стоит делать правильно и удобно с моей точки зрения конкретно в случае опроса V20. Первоначальная настройка преобразователя частоты описана в документации, в том числе и в сопутствующей документации к вышеуказанному примеру. Вынесем лишь важные для нас пункты в качестве вводных. Адрес подчиненного устройства модбас в сети: 1 Параметры связи: 9600 8-Even-1 Регистры хранения подчиненного устройства для чтения: 40110 ZSW «Слово состояния» 40111 HIW «Текущая скорость» Регистры хранения для записи: 40100 STW «Слово управления» 40101 HSW «Задание скорости» Параметр частотника «Telegram off time (ms)» P2014[0] рекомендую оставить по умолчанию, равным в 2000 мс (2 секунды), хоть пример и рекомендует снизить эту величину до 130 мс. Конкретно к протоколу Modbus это замечание не относится, разумеется, просто у меня при таймауте в 130 мс, ПЧ терял связь и выдавал ошибку Fault 72. С частотником разобрались. Теперь о моей конфигурации ПЛК. Это S7-1214 с коммуникационным модулем 1241 под RS-485/422: Среда программирования Step 7 V15.1 Update 4, версия прошивки CPU — 4.3. Итак, приступим. Для опроса подчиненных устройств с контроллера Simatic нам необходимо применить два функциональных блока: Modbus_Comm_Load (единовременно, только для конфигурации коммуникационного процессора) и Modbus_Master (циклически для чтения и/или записи регистров/катушек). Поэтому в программе экземпляр FB Modbus_Comm_Load у нас будет встречаться только один раз, а экземпляр Modbus_Master — несколько раз, но с разными входными параметрами, в зависимости от адреса подчиненного устройства, типа читаемых данных и их количества, а так же направления передачи данных (чтение или запись). Обращаю ваше внимание, что для одного коммуникационного процессора (а их в системе может быть очень много) у вас не может быть больше одного экземпляра каждого блока данных. С моей точки зрения весь обмен удобнее завернуть в один внешний функциональный блок, а сам блок, с учетом необходимости разбирать данные, реализовать на текстовом языке SCL. Поэтому создаем в проекте функциональный блок с именем ModbusMasterV20 на языке SCL. Сразу после создания открываем его свойства и снимаем настройку «оптимизированный доступ», т.е. используем стандартный доступ. Личный опыт показал, что использование оптимизированного доступа рано или поздно приведет к ошибкам работы блока Modbus_Master и невозможности обмена. Это связано с порядком, в котором переменные идут в объявленной структуре данных, при стандартном доступе порядок соответствует заданному в программе, при оптимизированном — система сама «раскидывает» переменные, как сочтет нужным. Объявляем следующие входные переменные Init (Bool) — инициализация коммуникационного процессора, ее необходимо выполнить один раз перед началом обмена PORT (PORT) — аппаратный идентификатор коммуникационного процессора BAUD (UDINT) — скорость обмена по порту STOP_BITS (USINT) — количество стоповых бит «кадра» PARITY (USINT) — четность, где 0 — нет четности, 1 — odd, нечет, 2 — even, чет В статической области переменных так же прописываем переменную с именем Step и типом UInt, она отвечает за «номер опроса» или «шаг работы алгоритма» Так же в статической области объявляем экземпляры ФБ для работы по протоколу Modbus RTU Строки программы, отвечающие за инициализацию обмена. По флагу инициализации выставляем номер шага 1. Следующие строчки очень важны для работы #instModbusCommLoad.MODE := 4; //для линии RS-485 должна быть 4! #instModbusCommLoad.STOPBITS := #STOP_BITS; Тут мы задаем значения статических переменных экземпляра ФБ Modbus_Comm_Load, которые отвечают за «физику» передачи. Не понимаю, почему немцы поместили эти важные конфигурационные параметры в статическую область, а не в область входов. Дело в том, что они (переменные) все описаны во встроенной справке. Беда лишь в том, что большинство ленивых жоп новичков до этого пункта справку не читает, а потом тратят несколько часов, а то и дней, пока не найдут ответ. А справка-то, вот она: Переменная MODE отвечает за режим, в котором будет работать коммуникационный процессор. Как видно из справки, для RS-485 надо явно выставить 4. Значение по умолчанию 0, от этого большинство ошибок у программистов. STOP_BITS — количество стоповых бит. Далее следует вызов блока настройки коммуникационного интерфейса Modbus_Comm_Load. Про параметр PORT (аппаратный идентификатор) будет рассказано чуть ниже. Параметры BAUD и PARITY — скорость и четность — приходят на вход «внешнего» блока данных, куда мы и завернули весь обмен. А вот параметр MB_DB интересен. На этот вход надо подать структуру типа P2P_MB_BASE, которая находится в области статических переменных экземпляра функционального блока Modbus_Master. Этот экземпляр в нашем «большом» функциональном блоке уже объявлен, привожу скриншот: Следующая часть: функциональный блок приступает к циклическому обмену. Я сразу «заворачиваю» обмен в CASE, чтобы не переписывать код в дальнейшем, но пока мы ограничимся чтением слова состояния и скорости ПЧ, т.е. прочитаем два регистра хранения. Давайте посмотрим на вызов блока Modbus Master повнимательнее:  Входной параметр REQ — включить опрос. Пока на входе TRUE, он выполняется, если FALSE — не выполняется. Нет необходимости подавать положительный фронт на этот вход самостоятельно (в отличии от работы Modbus RTU в системах S7-300/S7-400), поэтому я просто даю TRUE константой MB_ADDR — адрес подчиненного устройства Modbus RTU. В моем случае адрес частотника = 1. MODE — направление передачи данных, 0 — чтение, 1 — запись DATA_ADDR — адрес интересуемых нас данных. В моем случае необходимо прочитать два регистра хранения (поэтому первая цифра 4), начиная со 110го. В протоколе Modbus (что RTU, что TCP) очень часто возникает путаница в понятиях «адрес» и «номер». И очень часто производитель оборудования эту путаницу добавляет в свою систему. Вот смотрите. Мы должны прочитать 2 регистра, начиная с адреса 40110. Для чтения регистров хранения в протоколе Modbus используется функция с номером 3. Именно 3 будет передаваться в телеграмме Modbus. А в качестве адреса в телеграмме будет передаваться не 40110, а 109. Связано это с тем, что код функции уже содержит описание области данных. А в самой телеграмме мы передаем не адрес, а номер требуемого регистра или катушки. И эта нумерация идет не с единицы, а с нуля. Сейчас я работаю именно с адресами и режимом (чтении или запись), поэтому мне достаточно указать то, что я нашел в документации. Если же в вашем устройстве будет указано «входной регистр номер 0 содержит текущий статус устройства», то вам на вход DATA_ADDR необходимо будет подать 30001. Так же имейте в виду, что из-за частой путаницы с номерами и адресами, иногда эта адресация съезжает на «единицу», поэтому не бойтесь экспериментировать. Если вместо полезных данных по запросу 16ого регистра вам прилетает полная чехарда, не имеющая ничего общего с документацией, прочитайте 15ый регистр. Не помогло? Опрашивайте 17ый. Более подробно с материалом необходимо ознакомиться опять же во встроенной справке. DATA_LEN — количество читаемых регистров, их 2 DATA_PTR — указатель на область памяти, куда необходимо «положить» результат чтения регистров. Собственно, это те данные, которые мы прочитали и необходимо подсказать функциональному блоку, куда эти данные надо записать. С моей точки зрения самый удобный способ в этом случае — это объявить в области STAT неименованную структуру должного размера. Поля структуры мы объявляем, в зависимости от самих читаемых данных, ведь это могут быть и наборы бит, и вещественные числа (расположенные в двух соседних регистрах). И нам еще повезет, если порядок байт в слове и слов в двойных словах контроллера и подчиненного устройства совпадут, иначе нам еще потребуется осуществить сдвиги байт/слов. В данном случае я счел уместным объявить структуру из двух слов и скормить ее на вход FB: , где ZSW — слово состояния (так оно называется в документации на ПЧ) HIW — скорость вращения двигателя После вызова блока мастера, необходимо проанализировать успех или неуспех его выполнения. В принципе, на этом скриншоте в комментариях уже все написано: В случае успешного чтения необходимо полученные сырые данные как-то разобрать или переложить в другую область, и перейти к следующему опросу (у нас пока только один опрос, так что мы остаемся на шаге №1). При ошибке чтения данных минимальный разумный ход — выставить где-нибудь флаг недостоверности и перейти к другому опросу. Пока оставляем прием данных без обработки, компилируем и грузим программу, смотрим на результат. Кстати, обращаю еще внимание на один факт. Поскольку мы работаем, завернув системные вызовы в свой функциональный блок, то любое изменение «своего» ФБ с последующей загрузкой ПЛК, будет нарушать обмен в связи с переинициализацией экземпляра нашего ФБ. Например, будет уходить в ноль значение «шага обмена». Да и внутренние статические переменные коммуникационных вызовов тоже пострадают. Самый простой способ — стоп и старт контроллера. В боевом проекте это опасно, поэтому там на вход Init я бы подал еще одну переменную и поднимал ее самостоятельно после изменений в коммуникациях. Пока же боремся с остановом обмена простым стоп-стартом ПЛК. Добавляем вызов нашего функционального блока в OB1 и грузим CPU: Переменная FirstScan имеет значение «истина» при первом цикле выполнения программы OB. Она назначается операционной системой ПЛК автоматически, ее применение настраивается в свойствах CPU. Port. Это значение смотрим в проекте Step 7, аппаратная конфигурация: Остальные параметры касаются скорости, четности и количества стоповых бит. Загружаем контроллер и смотрим, что нам приходит в ответ на единственный циклический запрос, открыв экземпляр нашего ФБ: В слове состояния что-то есть, скорость равна нулю. Открываем документацию и смотрим состав слова состояния ZSW: Low enabled в примечаниях означает инверсию. К примеру, бит №15, перегрузка частотника, возникает, когда этот бит равен 0, а в нормальном состоянии приходит значение 1. Посмотрим на это слово состояния в watch table и посмотрим, какие его биты выставлены, а какие — нет, оценим общее состояние ПЧ: Тут нам везет, порядок байт в словах совпадают. Если вкратце, то видно, что ПЧ не готов, не включен, и сейчас активен сигнал аварии (fault, бит №3). Далее я попытался разложить слово состояния в биты состояния, заменив WORD на структуру из бит, но что-то явно пошло не так. Если посмотреть внимательно, то в таком представлении нулевой и первый байты явно не на своих местах. В общем, вопрос порядка следования информации в зависимости от того или иного представления — он всегда важный и требует вдумчивости. Получил на этом этапе облом, я решаю вернуться к хранению внутри нашего ФБ только сырых данных, а удобочитаемый для человека формат представления информации перенести куда-нибудь во внешний глобальный блок. Добавляю в проект блок данных DataV20: После чего задумываюсь, убираю из имен переменных окончание Inv и дописываю функциональный блок: Теперь в глобальном блоке данных у нас находятся статусные биты преобразователя частоты без какой-либо инверсии: Думаю, что сразу в блок данных надо вписать переменную типа Real, которая будет содержать текущие обороты двигателя. Текущие обороты приходят от ПЧ в виде определенного численного кода, и мы вольны трактовать этот код, как нам удобнее. Допустим, что хочу трактовать этот код, как Герцы, поданные на двигатель. Пока не будем пересчитывать коды в физические величины и перейдем к следующему шагу — к записи слова управления и задания частоты. Вернемся к обработке текущей скорости чуть позже. Обратимся к документации и посмотрим состав слова управления частотным преобразователем: Знаете, мне, откровенно говоря, лень писать все эти переменные в глобальном интерфейсном блоке данных. В моей практике управление простыми технологическими процессами с применением преобразователей частоты ограничивалось командами включить, квитировать аварию и дать задание скорости. Поэтому, пойдем на поводу моей лени и в блоке данных V20Data пропишем всего лишь бит включить, бит квитировать и задание частоты в формате Real. Изменю алгоритм на шаге №1, при успешном или неуспешном завершении опроса сделаю переход на шаг №2. Добавим еще локальную структуру ФБ, которая содержит слово управления и слово задания скорости: Дорабатываю программу обмена. Не забываем, что при изменении переменных функционального блока, после загрузки изменений в ПЛК происходит его переинициализация, посему надо выполнять стоп/старт CPU. Параметры функционального блока модбас в данном случае отличаются от первого вызова. Разумеется, у нас тут другой адрес регистра. А так же отличается режим (MODE), он равен 1, так как в данном случае данные не читаются с частотника, а записываются в него. Разумеется, указатель на область данных так же другой. Обратите внимание, что некоторые биты слова управления я принудительно выставляю в истину, другие — в ложь. Всего два бита управления (включить и квитировать) доступны для внешней программы. Необходимое значение бит управления я вычитал в документации примера. Разумеется, это указано и в документации на сам преобразователь частоты. Изучая исходный пример, я обратил внимание, что если частотнику отдавать «пустое» (все биты выставлены в ноль) слово управления, то это подчиненное устройство модбас возвращает ошибку Invalid data. Однако, в этом примере я пробовал слать полностью «пустое» слово управления, и V20 принимал его. Однако, некоторые биты управления, все равно, должны быть установлены. К примеру, если снять бит «Control by PLC», то запускаться ПЧ не будет. RTFM, как говорится! Теперь пора перейти к регистру, который отвечает за задание скорости (ну и сразу же к регистру, который отображает текущую скорость). Из исходного примера я понял, что этот регистр меняет свое значение в пределах от 0 до 16384. Это же мельком нашел и в документации. Пока не будем делать никаких переводов величин, и зададим ПЧ максимальную скорость жестко прямо в программном коде. Откроем наш блок данных DataV20 и выставим команду «пуск»: V20 запустился и работает, судя по индикации своего экранчика, на максимальной скорости, т.е. на 50 Гц. Давайте посмотрим еще сырые данные его скорости, которые приходит по modbus. Значит, пришло время доработать шаг №1 обмена (перевести коды скорости в герцы), ну и шаг №2 в части обратного преобразования, герцы в численное значение задания скорости. «Математика» самая простая, без проверок на достоверность и выход за диапазон, хотя все это не помешает. После загрузки изменений откроем блок данных DataV20 и поуправляем частотником из него. Даем задание 25 Гц, даем пуск и наблюдаем за появлением сигнала Running и текущей скоростью. Все регистры, которые можно считать с V20, описаны в документе по ссылке. Судя по описанию регистров, есть и другие способы управления, но нас сейчас интересует не это. В моей практике с преобразователей частоты еще частенько просили считать ток(и), напряжение(я), мощность, детализацию ошибок и т.д. Давайте последуем этой старой-доброй традиции и считаем дополнительно вот эти параметры, а так же переведем их в понятное представление: Параметры, разумеется, могут быть любыми, но предположим, что заказчику очень хочется именно эти. Я не буду подробно описывать процесс программирования и сразу покажу результат. В принципе, мотор маленький, крутится без нагрузки, поэтому значения похожи на достоверные. Тем не менее, задача стоит в демонстрации считывания данных, поэтому будем считать наличие хоть каких-либо «цифры» за огромный технологический прорыв. Итак, вы уже заметили, что я добавил читаемые параметры в блок данных DataV20. Дополнительно был доработан функциональный блок коммуникаций: Читаются (mode = 0) четыре регистра хранения по адресу 40025. Результат помещается во внутренний статический массив [0..4] of WORD. Далее эти слова переводятся в формат Real и помещаются во внешний блок данных в результате несложных преобразований. Ну, и напоследок остается проанализировать качество связи. Ведь не зря же на каждом шаге после выполнения ФБ Modbus_Master смотрю его флаги DONE или Error (кстати, эти флаги имеют значение «истина» только на протяжении одного вызова после успешного или неуспешного выполнения запросы, в остальное время — ложь). Для этого я объявил массив из булевых переменных Массив размерностью три, по количеству запросов Modbus. Соответственно, если на шине будет 10 частотников, по три запроса к каждому, то размерность этого массива, как и количество «шагов» алгоритма, будет равно 30. Ну, и в конце каждого опроса, при анализе флагов, наконец, прописываем присвоение флагам значения. Будем считать, что частотник стабильно обменивается информацией с ПЛК, когда все три запроса к нему выполнены успешно. Поэтому самая последняя строчка нашего функционального блока будет такой (предварительно добавим булевую переменную Connected в блоке данных DataV20):  АСУ ТП