Микро-курс по программированию контроллеров SCADAPack на Си

Автор Сообщение
#1 / 16.11.2018 10:32
admin

45iBqO6VwrU.jpg

fKliMcmv-Mw.jpg

 

Итак, знакомьтесь. Это программируемый логический контроллер (ПЛК) под названием SCADAPack фирмы Schneider Electric (ранее Control Microsystems).

Программи́руемый логи́ческий контро́ллер (сокр. ПЛК; англ. programmable logic controller, сокр. PLC; более точный перевод на русский — контроллер с программируемой логикой), программируемый контроллер — промышленный контроллер, используемый для автоматизации технологических процессов. В качестве основного режима работы ПЛК выступает его длительное автономное использование, зачастую в неблагоприятных условиях окружающей среды, без серьёзного обслуживания и практически без вмешательства человека. [Википедия]


ПЛК эти заслужили славу своей надежностью и богатыми возможностями программирования. Внутри у контроллера в зависимости от серии стоит ARM-процессор на котором работает операционная система VxWorks.

В промышленной автоматизации общепризнанным стандартом являются языки МЭК, такие как LD/LAD, FDB и ST. Первый из них представляет собой ни что иное, как схемы, похожие на схемы релейной логики. Второй представляет собой ни что иное, как схемы, похожие на схемы с логическими элементами и электронными компонентами (таймеры, счетчики, и т.д.). Третий представляет собой текстовый язык, навевающий воспоминания о Паскале. Но сегодня мы поговорим не про них (желающие всегда могут погуглить), а про разработку под эти контроллеры на Си, что во-первых гораздо ближе «простым программистам», а во-вторых спасает при необходимости программирования сложных математических расчетов или реализации нестандартных коммуникационных протоколов.

Для компиляции нам понадобится, собствено, компилятор, заголовочные файлы и стандартная библиотека контроллера. Всё это можно найти на сайте производителя под названием C Tools, а описание API — там же.
Разработка начинается с написания Makefile'а (скрипта для сборки проекта из исходников в бинарный файл). Пример makefile'а можно найти в директории C Tools:
C:\Program Files\Control Microsystems\CTools\Controller\Framework Applications\TelePACE
Там же есть пример main.cpp и файла appstart.cpp (он тоже необходим для сборки).
Ctools.h лежит в C:\Program Files\Control Microsystems\CTools\Controller\TelePACE

Обратить внимание в нём стоит на 3 вещи:
objects = appstart.o main.o 
данная строка задает, какие файлы необходимо компилировать для сборки прошивки. Если у вас проект разделен (а так и должно быть) на .c- или .cpp-файлы с заголовками (.h- или .hpp-файлы), то они должны быть перечислены в этой секции. Если вы вдруг забудете что-нибудь, компилятор напомнит об этом ошибкой Undefined reference.

CTOOLS_PATH = C:\Program Files\Control Microsystems\CTools
Это путь к Ctools. Скореектируйте, если он у вас отличается.

TARGET = SCADAPack350
эта строка определяет, под какое семейство контроллеров мы компилируем прошивку. Возможные варианты: 
SCADAPack350 (сюда относятся 357, и т.д.), SCADAPack33x, 4203

Компиляция прошивки производится командой make из командной строки.
Если выводится сообщение, что не удалось найти эту команду, проверьте, что у вас в системной переменной PATH прописан путь к библиотекам C-Tools:
C:\Program Files\Control Microsystems\CTools\Arm7\host\x86-win32\bin

Простое приложение
Для компиляции простой (и пустой) прошивки, нам будет необходим Makefile, файлы appstart.cpp и main.cpp.

Их примеры можно найти в той же директории C Tools, что была упомянута выше. Appstart.cpp отвечает за инициализацию оборудования и среды выполнения, а в main.cpp мы уже можем писать нужный нам код.

Общая структура программы на Си для SP выглядит вот так

caVUXNNBFAk.jpg

Вызов release_processor() необходим в каждом цикле, потому что кроме нашей программы ОС контроллера выполняет также другие служебные процессы (обработчики портов и протоколов, среда исполнения, и т.д.). Без вызова этой функции, к примеру, после запуска программы будет невозможно остановить ее или перепрошить контроллер.

Стиль кодирования в C Tools, увы, оставляет желать лучшего: встречаются разные стили именования функций (process_io() и release_processor(), но ioReadDin16() и addRegAssignment(), а еще getclock()/setclock()), в некоторых схожих функциях с одинаковыми аргументами поменяны местами эти самые аргументы, короче говоря, будьте внимательны.

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

Как основу для правил можно взять отдельные пункты стандартов MISRA (стандарт разработки встраиваемого ПО для автомобилей) или JSF (для авиации).

Загрузка программы в контроллер
Для этого нам и понадобится TelePACE. К контроллеру можно подлючаться по RS232/485, по Ethernet и даже по USB (если он есть у используемой модели скадапака).

-K6SEF7hbNc.jpg

Принцип примерно один и тот же:

  1. Выбираем сверху в поле Protocol нужный нам протокол (Modbus RTU, Modbus TCP или Modbus USB)
  2. Нажимаем Configure Settings и задаем все нужные данные (RTU-адрес, скорость порта для RS232/485 или IP-адрес для TCP)
  3. Нажимаем Connect и убеждаемся, что соединились с контроллером.
  4. На вкладке Initialize можно сбросить контроллер к первозданному виду — удалить все программы, LAD-проекты, настройки портов и register assignments.
  5. На вкладке C/C++ можно посмотреть, какая программа загружена в контроллер, остановить/запустить ее, загрузить новую (исследуете кнопочки вверху вкладки!).

Работа с таймерами
Начнем с самой простенькой программы – Hello World, а именно, помигаем светодиодом на контроллере.

U50V73HaGh0.jpg

Работа с modbus-регистрами


Modbus — открытый коммуникационный протокол, основанный на архитектуре ведущий-ведомый (master-slave). Широко применяется в промышленности для организации связи между электронными устройствами. Может использоваться для передачи данных через последовательные линии связи RS-485, RS-422, RS-232, и сети TCP/IP (Modbus TCP). Также существуют нестандартные реализации, использующие UDP. Основные достоинства стандарта — открытость и массовость. Промышленностью сейчас выпускается очень много типов и моделей датчиков, исполнительных устройств, модулей обработки и нормализации сигналов и др. Практически все промышленные системы контроля и управления имеют программные драйверы для работы с MODBUS-сетями. [Википедия]


В SCADAPack работа с Modbus реализована красиво и удобно.

Память modbus-регистров у контроллера энергонезависимая, и не теряется даже после перезагрузки или пропадания питания. С одной стороны, это немного неудобно (не забывайте очищать или инициализировать регистры при старте, если есть такая необходимость), но с другой стороны, это позволяет хранить в адресном пространстве modbus настройки, уставки, и даже архивы небольшого объема.

Кроме того, в modbus-регистры помещаются результаты выполнения различных команд (чтение состояния дискретных и аналоговых входов, опроса внешних устройств, и т.д.).

Обработчик modbus в скадапаках реализован на уровне операционной системы, и поэтому после запуска программы, по всем COM-портам (и по Ethernet) мы сразу можем опрашивать контроллер по модбасу (о настройке портов будет чуть позже). Более того, на modbus-запросы контроллер будет отвечать, даже если программа остановлена.

Запись и чтения modbus-регистров осуществляется функциями dbase() и setdbase(), например, вот так:

gsdArdEdXNg.jpg

Данный пример читает два числа из регистров 0001 и 0002 зоны Inputs и сохраняет результат их умножения в регистр 0020 зоны Holding. Всё просто.

Работа с сигналами ввода-вывода
Работа модулями ввода и вывода может проходить тремя способами:

Первый способ
Первый вариант — создать «register assignment». Состояние (дискретные и аналоговые значения) входных каналов модулей DIN и AIN будет автоматически “отображаться” в modbus-регистры, и наоборот, для модулей DOUT и AOUT состояние выходов будет определяться значениями, записанными в регистрах. Для этого используются функции clearRegAssignmnet (очистка все старых назначений) и addRegAssignment (создание новых).

Первый аругмент функции addRegAssignment – тип модуля (список констант можно посмотреть в документации на TelePACE и в заголовочных файлах, могут быть DIN_5401, DIN_5404, AIN_5301 и другие), второй – адрес модуля (он обычно задается перемычкой на самом модуле, а если используются встроенные в SP входы, то равен 0), и далее идут адреса регистров, начиная с которых должна производиться запись (если модуль обеспечивается получение данных разных типов, то и групп регистров будет несколько – coils, status, inputs и т.д.)

mG6KfWNyOo0.jpg

Стоит иметь в виду, что если вы принудительно остановите среду выполнения (runTarget(FALSE)), то они работать не будут, но если вы не вносили изменений в стандартный файл appstart.c в целях оптимизации, то беспокоиться не о чем.

Не забывайте выполнять clearRegAssignment(); при запуске программы, даже если вы не пользуетесь ими – кто знает, кто и что делал на этом контроллере до вас.

Если вы не знаете точно, какая плата ввода-вывода будет стоять на контроллере, на котором будет запущена ваша прошивка, можно воспользовать решением, предложенным специалистами из ОЗНА.

Второй способ
Вызывать явно функции чтения, которые сохранят данные в заданные modbus-регистры.
Это могут быть функции ioRead8Din, ioRead8Ain, ioRead16Din, ioRead16Ain, ioRead5604Inputs, ioWrite16Dout, ioWrite5604Outputs, ioRead4Counter (для счетных входов), ioReadSP2 (для чтения встроенных входов, в зависимости от модели контроллера). Подробности и синтаксис этих команд можно прочитать в документации на C Tools, обычно первым аргументом следует адрес модуля (0 для встроенных входов), а вторыми и дальше – адреса modbus-регистров, начиная с которых нужно сохранить значения сигналов (или откуда их брать для записи в выходные каналы).

Пример:

ldtQWJt1ZJg.jpg

Вызов функций ioWrite* аналогично произведет установку нужных значений из modbus-регистров в выходные каналы контроллера и модулей.

Третий способ
То же самое, но с сохранением результатов не в modbus-регистры, а в переменные или массив. Функции называются так же, но имеют перегруженную реализацию с другими аргументами.

qOToR04VLF0.jpg

Нечто подобное можно сделать с данными, которые мы хотим записать в выходные каналы (например, замкнуть релейный выход):

DRxEqf4MnGk.jpg

Конкретные функции и константы для чтения и записи данных в используемые модули нужно смотреть в документации и в заголовочных файлах C Tools. К примеру, у функций для модуля 5607, как можно заметить выше, есть дополнительные опции для настройки типа входа, параметров фильтра, и т.д. Они также описаны в документации.

Есть еще функции получения текущей температуры контроллера и напряжения на батарее – readThermistor(T_CELSIUS), readBattery(). Мелочь, а полезно.

Масштабирование AI
Каналы аналогового ввода модулей контроллера могут работать в разных режимах (например, с входными диапазонами 0-20 или 4-20 мА, это определяется перемычками на модуле). Выдают данные они в единицах АЦП, и преобразовать их в нужную нам шкалу (например 4-20 мА будет соответствовать 0-100%) довольно просто:

C68Pfzv4ZU0.jpg

Естественно, вместо 100 можно умножить число на нужный вам верхний предел шкалы.

Настройка портов RS-232/RS-485
Здесь, опять же, никакой магии:

MiMTRNH0M3Q.jpg

Настройки портов сохраняются в EEPROM, однако в зависимости от конфигурации контроллера, версий библиотек, и т.д., можно по определенному событию (команда, замыкание перемычки, и др) читать, например, из тех же modbus-регистров, чтобы автоматически переконфигурировать и обновлять их.

Опрос устройств по modbus
Если вам хочется не просто работать как slave-устройство, но и самим опрашивать другие контроллеры или датчики, есть функции вида master_message(), позволяющая произвести опрос внешних устройств по модбасу, и сохранить результаты к себе в регистры, откуда потом их можно считать и использовать в алгоритме (или же просто предоставить верхнему уровню). Примеры есть в документации, только учтите два нюанса: необходимо обязательно проверять результат выполнения функции командой get_protocol_status(), перед тем как отправлять повторный запрос или работать с полученными данными, и второй нюанс: необходимо либо вообще отключать обработчик modbus на используемом порту, либо следить, чтобы его адрес не совпадал с адресом опрашиваемого устройства (иначе можно получить неопределенное поведение или странные ошибки).

WyJfVAkYDno.jpg

stream – порт, через который будет идти обмен данными (например, com1), function – номер функции modbus для запроса (например 3), slave_station – адрес RTU опрашиваемого устройства, slave_address – стартовый адрес регистров в удаленном устройстве, которые мы хотим прочитать, master_address – стартовый адрес регистров на нашем контроллере, куда будут записаны данные, length – количество регистров для чтения).

Пример:

I72SKeitB3I.jpg

Также можно установить свой обработчик Modbus-пакетов (installModbusHandler()) чтобы реализовывать какие-либо расширения протокола.
Система ведет статистику обмена (принято/отправлено, количество ошибок, и т.д.), которую тоже можно считать из соответствующих структур.

Коммуникации
Можно также установить свой обработчик данных, пришедших по COM-портам (install_handle()) для реализации каких-то нестадартных протоколов. 
С TCP/IP все тоже хорошо. Доступны стандартные функции работы с BSD Sockets: bind(), getsockopt(), и т.д. Настройки сети на контроллере можно программно считать и записать (ethernetGetIP(), ethernetSetIP()).

Ведение архивов
В документации SCADAPack для этих целей предлагается использовать DataLog из библиотеки C Tools, который, к сожалению, обладает большим количеством недостатков, главный из которых состоит в том, что он не обеспечивает прямой непоследовательный доступ к записям в архиве. Некоторые разработчики обходятся хранением архивов прямо в регистровой памяти, но, учитывая что количество Holding и даже Inputs (при желании их можно использовать тоже, да) в контроллере ограничено, это решение тоже не всегда подходит.

На самом же деле, учитывая что операционная система в контроллере вполне себе POSIX-совместимая, а сам контроллер несет у себя на борту вполне нормальную файловую систему (плюс есть возможность вставлять USB Flash-накопители), есть возможность хранить архивы в файлах на флешке на борту контроллера, о чем совершенно вскользь упомянули в документации.

Работать с файлами можно как и в любой Си-программе:

yS6h4KJOpiQ.jpg

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

yqzQc_XE6d4.jpg

В точку /d0/ монтируется файловая система, в /bd0/ — внешний USB-флеш накопитель. Опять же, нет никакой проблемы реализовать копирование архива из встроенной памяти на флешку при нажатии на кноку на контроллере, и много других вариантов.

В TelePACE файловая система просматривается совершенно спокойно, что может помочь для отладки или сбора данных.

Энергонезависимая память
Как я уже отмечал, в энергонезависимой памяти, прежде всего, хранятся данные Modbus-регистров, то есть значения, сохраненные в них, будуь доступны и после перезагрузки контроллера.
Кроме того, существует отдельная структура s_nvMemory, объявленная в файле nvMemory.h, с максимальным размером 8 килобайт, которую вы можете отредактировать как вам удобно (создав нужные поля) и хранить там данные, которые должны остаться целостными в том числе после пропадания питания и перезагрузки контроллера. Если этого недостаточно, то дополнительно методом allocateMemory() можно выделять блоки энергонезависимой памяти общим размером до 1 мегабайта и хранить в них архивы, логи, и т.д. Самое главное в этом случае — после выделения блока памяти не забыть сохранить указатель на нее в вышеупомянутую структуру s_nvMemory, иначе выделенная память будет потеряна, и вы не сможете ни использовать ее, ни освободить (freeMemory()). А еще при доступе к энергонезависимой памяти нужно не забывать захватывать ресурс DYNAMIC_MEMORY.

Многозадачность и многопроцессность
Можно как одновременно запускать несколько разных программ, так и внутри одной программы запускать одновременно несколько разных методов (функции createTask(), getTaskInfo() и т.д.), задавать им приоритеты и даже слать сигналы между задачами и сообщения между процессами (так называемые 'envelopes').
Многозадачночность кооперативная, то есть переключение между задачами и программами выполняется не принудительно операционной системой, а поочередно, когда они сами готовы передать исполнение дальше или при возникновении определенных событий. С одной стороны, это решает проблему с атомарностью операций при совместном доступе к данным, а с другой стороны, нужно не забывать вызывать release_processor() и не захватывать ресурсы на слишком долгое время. Пример многозадачной программы и граф изменения состояния при возникновении событий описаны в документации в главах «RTOS Example Application Program» и «Explanation of Task Execution».

Динамическая память
malloc() и free() вполне работают, как и new и delete. Единственный довольно важный нюанс — при остановке программы (например, нажатием кнопки Stop), выделенная память системе не возврращается: необходимо осознанно установить обработчик завершения работы программы (installExitHandler) и освободить там все запрошенные блоки из кучи.

Работа с часами реального времени
У контроллера есть также на борту часы реального времени.
Получать текущее время и устанавливать его можно функциями getclock и setclock, в документации есть подробные примеры.

Пример простого алгоритма
Допустим, у нас есть контроллер с платой ввода-вывода 5604, к каналу AI 2 которой подключен аналоговый датчик уровня в емкости, к каналу DO1 подключено реле пускателя насоса.

Необходимо поддерживать в емкости заданный уровень жидкости (требуемый уровень задается из SCADA или с панели HMI), то есть при слишком высоком уровне должен включаться насос, откачивающий излишек, и выключаться при достижении нужного уровня.

В регистр 0020 зоны Holding (40020) будем записывать приведенное значение уровня для отображения на SCADA или HMI-панели.

Также нужно предусмотреть небольшой гистерезис для защиты от «щелканья» реле насоса в случае, когда уровень колеблется около нужной отметки.

Опрос контроллера будет идти через порт COM2.

8q-HOyS4_R8.jpg

f9X0FQabmh0.jpg

fI_K7sEp3KU.jpg

Ka9vltrBwHc.jpg

Вот, собственно, и всё
Описание всех функций и примеры их использования есть в документации на C Tools.

Очень хорошо обладать целостными знаниями Си (например, при реализации алгоритмов это касается приведения типов), чтобы писать код красиво и без ошибок.

Изучайте, экспериментируйте, и всё получится!

Источник:

https://habr.com/post/350282/

Сообщения: 463