Попросили меня как-то друзья помочь с программированием контроллера поворотного стола для фотографирования. Эти столы используются для круговой съёмки. Часто результат такой съёмки можно видеть в интернет-магазинах, когда товар можно покрутить и рассмотреть с разных сторон.
Как сделать фото 360°? Можно поставить объект на поворотную платформу, затем вручную перемещать её на определённый угол и щёлкать затвором фотоаппарата. Но лучше этот процесс автоматизировать.
Друзья мои давно занимаются этим бизнесом, сами разрабатывают и делают автоматизированные поворотные столы разных размеров на основе шагового двигателя с контроллером. Раньше у них применялся контроллер, работающий под управлением компьютера. Потом производство этих контроллеров прекратилось, и потребовалось написать программу для нового автономного контроллера.
Дело для меня совершенно новое. Я никогда не занимался АСУ, с релейно-контактной логикой незнаком, про ПЛК не слышал. Ну что ж, тем интереснее будет разобраться, что такое релейная логика и что представляют из себя языки LD (Ladder Diagram) и IL (Instruction List).
Bот что можно получить при помощи поворотного стола:
А вот сам поворотный стол:
А это контроллер SMSD‑1.5Modbus. Кстати, отечественная разработка. Впрочем, на его месте мог бы быть любой другой ПЛК:
Контроллер поставляется с софтом. Используются оба языка программирования LD и IL. Расскажу немного про лестничные диаграммы. Вот основные элементы языка LD:
входной сигнал, нормально-открытый контакт | |
входной сигнал, нормально-закрытый контакт | |
выход, катушка |
Входные контакты обозначаются буквой X с циферкой. Нормально-открытый контакт срабатывает, когда на вход подаётся сигнал. Нормально-закрытый контакт в обычном состоянии замкнут и срабатывает, когда входной сигнал пропадает.
Входные контакты можно комбинировать. Это логическое И:
А это логическое ИЛИ:
Выходные контакты обозначаются буквой Y с номером. Есть и другие символы:
Символ для входного импульсного сигнала с опросом по переднему фронту | |
Символ для входного импульсного сигнала с опросом по заднему фронту | |
Символ для прикладных инструкций | |
Символ логической инверсии |
Вот пример простейшей диаграммы. Когда контакт X1 замкнут, мы запускаем двигатель и выставляем выход Y0. На панели контроллера при этом загорится соответствующая диодная лампочка:
А вот более интересный случай:
Здесь при замыкании контакта X5 выход Y3 изменит свое состояние на замкнутое, однако, при размыкании контакта X5 выход Y3 сохранит свое замкнутое состояние до тех пор, пока не будет включен вход X6. Контакт Y3 является самоблокировочным.
Язык лестничных диаграмм является производным от релейно-контактной принципиальной электрической схемы в упрощенном представлении. Вот для сравнения релейно-контактная электрическая схема и соответствующая LD-диаграмма:
В контроллере, по сути, происходит эмуляция работы релейной схемы. А схема эта описывается программой, которая загружается в контроллер. В реальной релейно-контактной электрической схеме все задаваемые управляющие процессы выполняются одновременно (параллельно). Каждое изменение состояние входных сигналов сразу же действует на изменение состояния выходных сигналов.
В контроллере же изменение состояния входных сигналов, произошедшее во время текущего прохода программы, опознаётся только на следующем цикле программы. Этот недостаток контроллера сглаживается благодаря короткому времени цикла. Время выполнения одного цикла программы зависит от количества выполняемых инструкций в программе и от типа используемых инструкций. Во время работы над программой оказалось, что время зависит ещё кое от чего, но об этом чуть позже.
В процессе работы контроллер непрерывно опрашивает текущее состояние входов и изменяет состояние выходов в зависимости от программы пользователя. На первом этапе происходит считывание состояния физических и виртуальных Modbus Coils входов и их буферизация во внутренней памяти контроллера. Да, контроллер может управляться и по протоколу Modbus, но поскольку я использовал его автономно, рассказывать про Modbus не буду.
На втором этапе происходит обработка состояния буферизированных входов и изменение состояния выходов в памяти контроллера по заданной программе пользователя. На третьем этапе контроллер изменяет состояние физических и виртуальных выходов.
IL-программа состоит из последовательности отдельных управляющих инструкций. Контроллер обрабатывает инструкции последовательно, одну за другой. Собственно, в контроллер загружается именно последовательность инструкций с операндами. Инструкций много. Есть команды проверки входных условий, есть арифметические, битовые и логические команды, возможна целочисленная арифметика и арифметика с плавающей точкой. Есть возможность использования прерываний и подпрограмм. Наконец, есть группа команд для управления двигателем.
Операндами являются регистры (общего назначения, энергонезависимые и индексные), меркеры (однобитные ячейки памяти), таймеры, счётчики и константы.
Можно считать, что контроллер – это десятки или сотни отдельных реле, счетчиков, таймеров и память. Все эти регистры, счётчики, таймеры физически не существуют, а моделируются процессором.
На этой картинке показана LD-диаграмма и соответствующая IL-программа:
IL внешне очень похож на ассемблер. Есть фиксированный набор команд, команды могут иметь один или несколько операндов. Это и ввело меня в заблуждение. Вначале я вообще вообразил, будто IL-программа транслируется в ассемблер и затем исполняется в контроллере. Оказалось, что это не так. Команды вместе с операндами переводятся во внутренний формат и на каждом цикле обрабатываются исполняющей средой, то бишь прошивкой контроллера.
Некоторой неожиданностью для меня стало то, что любой отдельный кусок программы обязательно должен начинаться с инструкции LD (нормально-открытый контакт) или LDI (нормально-закрытый контакт). Иными словами, большинство исполнительных инструкций требует наличие входного условия, их нельзя поставить на выполнение как одиночные инструкции. Это я не сразу усвоил. Есть лишь несколько исключений из этого правила: это указатели I, P, команды конца программы END, FEND, а также IRET, SRET, EI, DI, NEXT, FOR. То есть, получается, что вся программа – это набор альтернатив, только вместо IF или IF NOT надо использовать LD и LDI (есть ещё несколько входных инструкций, но не суть).
К сожалению, прилагаемый софт слишком терпимо относится к ошибкам, и отслеживает только совсем уж явные несообразности в коде. То есть можно иметь одиночные инструкции, или, например, поставить условие перед командой выхода из подпрограммы – это не помешает загрузить такую программу в контроллер и исполнять её. Вот только работать такая программа будет некорректно или нестабильно.
Мне нужно было реализовать четыре режима работы: видеорежим с возможностью регулировать скорость вращения стола кнопками пульта, ручной режим, где нужно совершать каждый шаг стола и спуск затвора фотоаппарата вручную посредством кнопок, автоматический режим, где стол совершает полный оборот за заданное количество шагов с автоматическим спуском затвора, и наконец, режим non-stop, в котором стол совершает полный оборот без остановок, а затвор фотоаппарата автоматически срабатывает в нужные моменты.
Должен сказать, что мой многолетний программистский опыт сыграл скорее отрицательную роль. Вот как мы, например, выставляем задержку выполнения на любом языке программирования? Вне зависимости от того, синхронный или асинхронный код мы пишем, мы вызовем функцию задержки и будем считать, что исполнение возобновится по истечении заданного времени со следующей инструкции.
Здесь не так. Логика работы размазывается по разным ветками исполнения. Сначала мы создаём таймер по входному условию и задаём время задержки, и уже где-то совсем в другом месте используем таймер в качестве входного контакта, который замкнётся по истечении заданного времени. Я, правда, проверял таймер сразу же, так привычней:
LD M109 ;произошла ошибка и горел индикатор
TMR T0 K10 ;запустим таймер на 100ms, он будет отсчитывать время, пока его вход M109 включён
AND T0 ;таймер сработал
RST M109 ;сбрасываем индикатор
Но самые большие трудности, как ни странно, у меня вызвали циклы. Расскажу чуть подробнее – это весело.
Цикл нужен, чтобы сделать полный оборот стола и остановиться. Тут меня немного сбил с толку код, написанный моими приятелями. Этот код использовал инструкции FOR-NEXT и пусть через пень-колоду, но работал. Начал я рефакторить код и столкнулся с проблемами. Пробую и так и сяк, и ничего не получается. Я даже задумался о полноте по Тьюрингу: что это за язык такой, в котором нельзя по-человечески цикл организовать! Долго бился, пока, наконец, меня не осенило: да ведь цикл-то и так есть, и это цикл исполнения программы в контроллере! Нужно просто использовать счётчик, поместив в него нужное количество шагов. Счётчик будет инкрементироваться на каждом прогоне программы. Тут, правда, есть некоторая тонкость: счётчик инкрементируется, когда его внутренний сигнал меняет своё состояние с 0 на 1, так что придётся менять состояние входного сигнала счётчика вручную. Когда счётчик полон, цикл надо считать законченным.
Я как-то в горячке упустил из виду, что если я буду пытаться использовать инструкции FOR-NEXT, очередной прогон программы не закончится до тех пор, пока не закончится мой цикл. Понятно, что управлять шаговым двигателем таким образом невозможно, ведь управляющие импульсы будут подаваться на двигатель только после завершения очередной итерации сканирования программы. А зачем же тогда предусмотрены инструкции FOR и NEXT? Ну, наверно, чтобы проинициализировать регистры, например.
Тем не менее, интереса ради я реализовал цикл в классическом виде (для его выполнения потребуется тысяча прогонов программы):
P 10 ;начало цикла
LD M108
DINC D0 ;тело цикла, инкрементируем регистр D0
LD> D0 K1000 ;если D0 > 1000
CJ P20 ;выходим
LD M108
CJ P10 ;переход на начало цикла
P 20
LD M108
SET Y10 ;выставляем выход Y10, чтобы убедиться, что цикл закончен
В общем, нужно было немного переформатировать мозги. Впрочем, должен упомянуть, что по ходу разработки я обнаружил небольшую проблему в прошивке, которую производитель быстро поправил.
А ещё выяснилось, что по мере увеличения скорости двигателя отзывчивость на нажатия кнопок снижается. Это произошло в видеорежиме, где стол должен плавно менять скорость при нажатии кнопок на пульте. То есть жмём кнопку – стол разгоняется. Я обнаружил, что по мере увеличения скорости время цикла программы увеличивается, и, соответственно, стол разгоняется медленней.
Производитель объяснил это вычислением момента переключения уровня сигнала STEP. C ростом скорости нужно чаще выполнять вычисления, данные операции являются максимально приоритетными, поэтому обработка программы пользователя уходит на второй план. Только при такой организации генератора STEP-сигнала, можно достичь широкого диапазона частот и реализовать опцию морфинга.
Для лучшей отзывчивости мне посоветовали использовать прерывания, но я решил проблему по рабоче-крестьянски – путём увеличения дельты, на которую изменяется скорость по мере её роста.
В общем, с программой я успешно справился, и друзья мои были довольны. Хорошо, что производитель контроллеров был отзывчивым: быстро отвечал на мои вопросы на форуме и помог в написании программы.
Выводы:
Порог вхождения невысок. Достаточно усвоить базовые концепции, и потом процесс программирования идёт легко.
Возможности ПЛК весьма широки: можно реализовать довольно сложные и замысловатые программы. Есть, конечно, ограничения по количеству регистров, меркеров и указателей перехода, или вот, скажем, уровень вложенности подпрограмм в используемом контроллере не более 8, но не думаю, что это сколько-нибудь серьёзные ограничения.
Предчувствую вопрос: а не лучше ли воспользоваться Ардуино? Да, написать программу под Ардуино для среднестатистического программиста гораздо проще, чем разбираться с релейной логикой. Впрочем, не факт, что это будет проще для инженера АСУ.
ПЛК предназначены, как правило, для промышленного использования. Это значит, что они могут работать в более суровых условиях, они мощнее и могут использовать больше входов. ПЛК надёжны, масштабируемы, имеют длительный срок службы. Кроме того, к промышленным ПЛК могут предъявляться повышенные требования безопасности. Отсюда вытекает главный недостаток: цена.
Недавно стоимость контроллера существенно увеличилась, и мои друзья стали подумывать о замене. И Ардуино – один из вариантов. Всё-таки поворотные столы эксплуатируются в мягких условиях, и использовать дорогой ПЛК совсем необязательно. Планируется использовать связку контроллера с силовым драйвером и управлять двигателем посредством ШИМ.
В заключение, пару слов о шаговых двигателях. Шаговый двигатель – вещь отличная, за исключением одного недостатка: они слишком шумные. Иногда стол попадает в резонанс и начинает довольно громко дребезжать. Поэтому мы будем пробовать коллекторные двигатели. Они не шумят, да и цена у них чуть ниже. Правда, в этом случае в схему придётся добавить энкодер для обеспечения обратной связи. Если тема заинтересует аудиторию, расскажу потом, что у нас из этого получилось.
Если вас заинтересовал поворотный стол, то вот видео:
Спасибо за внимание!