03.10.2018
В этом году мы писали на Си, баловались ассемблером, вручную программировали таймеры микроконтроллера и игрались с ШИМом без использования Ардуиновских библиотек, паяли схему с полевым транзистором и пытались создать небольшой домашний дата-центр. Сегодня мы вновь окунёмся в далёкую и нетипичную для высокоуровневого разработчика область, вспомним основы дискретной математики и упрощения логических выражений, поработаем с промышленным контроллером Omron CP1L и обучающей моделью шлагбаума!
Статья составлена из материалов лабораторной работы по дисциплине «Программирование микроконтроллеров и микропроцессоров».
Дана модель шлагбаума с кнопками открытия/закрытия, датчиками максимального уровня открытия и минимального уровня закрытия, а также датчиком нахождения чего-либо в створе шлагбаума. Требуется разработать программу, которая будет запускать процесс открытия шлагбаума при нажатии кнопки «Открыть» и закрывать при нажатии кнопки «Закрыть». В случае появления в створе шлагбаума какого-либо объекта или возникновения иной внештатной ситуации, необходимо останавливать работу, чтобы позволить оператору устранить факторы, препятствующие дальнейшей работе, и возобновить работу шлагбаума.
Примечание
Некоторые могут сделать резонное замечание, что нет нужды останавливать шлагбаум при появлении в створе человека на подъёме. Так было сделано умышленно: в первую очередь для упрощения логики, ну а во-вторых почему бы и нет? Не лезь под работающий механизм! Мало ли что может случиться?!
Программируется контроллер Omron в специально предназначенной для этого среде разработки CX Programmer на языке контактно-релейных схем. Да-да, никакого кода — только графическая нотация! Нонсенс для обычного прикладного программиста, но как нам сказали на лекции, большинство программ для промышленных контроллеров пишется (рисуется?) именно на графическом языке контактно-релейных схем (она же LD — Ladder Diagram).
Поехали!
Начинается всё с теории конечных автоматов, которой я не хочу вас грузить (в принципе, про state machines и так должен знать любой программист), так что сразу рассмотрим всё на нашем конкретном примере. Итак, нам нужно представить шлагбаум в виде набора состояний и переходов между ними. Каждое состояние представляет собой определённое уникальное сочетание значений булевых переменных (поскольку мы работаем на максимально низком уровне и оперируем электрическими сигналами, которые могут находиться только в двух состояниях: ток или есть, или его нет). Например, состояние движения шлагбаума описывается тремя вариантами: он или открывается, или закрывается, или стоит на месте. Для хранения этого состояния требуется два бита (две переменные) — школьная формула по информатике 2i=N
, где N — количество вариантов, а i — количество бит, которое необходимо для их представления.
Давайте перечислим все переменные, которые нужны для выполнения поставленной задачи (в скобочках приведены сокращения, которые будут использоваться дальше):
- шлагбаум открывается (открыв., ОТКР или О);
- шлагбаум закрывается (закрыв., ЗАКР или З);
- достигнуто минимальное значение хода (мин. или MIN);
- достигнуто максимальное значение хода (макс. или MAX);
- объект (человек) находится в створе (чел.);
- нажата кнопка открытия (кн. откр. или КО);
- нажата кнопка закрытия (кн. закр. или КЗ).
Первые две переменные одновременно являются и результирующими значениями (нам же как раз надо описать состояние движения шлагбаума). Остальные представляют собой провода, идущие от определённого датчика.
Теперь нам нужно построить таблицу состояний. Самый простой способ — это перебрать все возможные состояния. Но составлять таблицу из 27=128
строк, большая часть из которых либо вообще не будет иметь смысла, либо не будет представлять для нас какого-либо интереса — удовольствие не из приятных. К счастью, при наличии хорошего воображения можно представить весь процесс работы шлагбаума в голове и выписать только нужные состояния — у меня получилось 19 основных и 3 внештатных. Но надо быть очень внимательным! Потому что изначально я всё-таки умудрился упустить два состояния.
Вопросительный знак означает, что данная переменная не играет роли для этого состояния и может принимать оба значения (и 0, и 1).
Теперь нам остаётся только вывести две функции (О и З) из полученных сочетаний. Для этого преподаватель предложил нам воспользоваться картами Карно. Вряд ли я смогу нормально объяснить, как ими пользоваться (правильно разбить развёртку на переменные нам всё равно помогли), так что лучше просто посмотрите видео:
Для каждого истинного значения обеих функций, проставляем все единички и вопросики из соответствующей строчки в карту. В результате у меня получились следующие карты Карно:
Пустые поля подразумевают логические нули, а вместо вопросительных знаков можно подставить любое значение
Теперь группируем и упрощаем получившиеся единицы, заменив на них вопросительные знаки. Но прежде чем перейти к записи получившихся функций, вынужден сделать важное отступление.
Эмулятор шлагбаума
Поскольку большýю часть работы я выполнял дома, нужно было как-то проверять правильность получаемых функций и логических выражений. Для решения этой проблемы был разработан простенький эмулятор, написанный на Котлине и использующий возможности этого прекрасного языка по созданию няшных DSLей. Опробовать его можно прямо из браузера.
Всё самое важное находится в файле Main.kt
: перечисление состояний с переходами между ними из таблицы (переменная cases), а также функции открытия (openingFunc) и закрытия (closingFunc). Внутри данных функций определены следующие переменные: opening (открыв.), closing (закрыв.), min (мин.), max (макс.), openButtonPressed (кн. откр.), closeButtonPressed (кн. закр.), somethingUnderBarrier (чел.). Их можно объединять либо традиционными операторами, либо перегруженными операторами сложения и умножения:
!opening && openButtonPressed || opening && !closing
// или
!opening * openButtonPressed + opening * !closing
Но в графическом изображении контактно-релейных схем элементы рисуются последовательно в линию (конъюнкция, логическое И), и несколько таких строк располагаются параллельно (дизъюнкция, логическое ИЛИ). Поэтому можно воспользоваться вспомогательной функцией line
, которая применяет конъюнкцию к переданным ей параметрам. Сами же линии можно дизъюнктивно объединять либо теми же плюсами, либо через точки, как в ООПшных фабриках:
line(!opening, closing, min, !max, openButtonPressed, !closeButtonPressed, !somethingUnderBarrier)
.line(!opening, !closing, !min, !max, openButtonPressed, !closeButtonPressed, !somethingUnderBarrier)
Поддерживается и компромиссный синтаксис, который может кому-нибудь показаться более красивым и наглядным:
line { !max * !closeButtonPressed * !somethingUnderBarrier } +
line { !min * !openButtonPressed * !somethingUnderBarrier }
Обратно к работе!
В эмуляторе получившиеся функции выглядят следующим образом:
val openingFunc: LD = {
line(!opening, closing, min, !max, openButtonPressed, !closeButtonPressed, !somethingUnderBarrier) +
line(!opening, closing, !min, !max, openButtonPressed, !closeButtonPressed, !somethingUnderBarrier) +
line(!opening, !closing, min, !max, openButtonPressed, !closeButtonPressed, !somethingUnderBarrier) +
line(opening, !closing, !min, !max, !closeButtonPressed, !somethingUnderBarrier) +
line(opening, !closing, min, !max, !closeButtonPressed, !somethingUnderBarrier) +
line(!opening, !closing, !min, !max, openButtonPressed, !closeButtonPressed, !somethingUnderBarrier)
}
val closingFunc: LD = {
line(opening, !closing, !min, !max, !openButtonPressed, closeButtonPressed, !somethingUnderBarrier) +
line(opening, !closing, !min, max, !openButtonPressed, closeButtonPressed, !somethingUnderBarrier) +
line(!opening, !closing, !min, max, !openButtonPressed, closeButtonPressed, !somethingUnderBarrier) +
line(!opening, closing, !min, max, !openButtonPressed, !somethingUnderBarrier) +
line(!opening, closing, !min, !max, !openButtonPressed, !somethingUnderBarrier) +
line(!opening, !closing, !min, !max, !openButtonPressed, closeButtonPressed, !somethingUnderBarrier)
}
Функции оказались достаточно сложными. При этом если присмотреться к ним и вспомнить базовые правила преобразования логических выражений, можно заметить здесь обширное поле для упрощений. А учитывая, что с готовым эмулятором мы сразу можем узнать, нарушилась ли логика выражения, грех не воспользоваться этой возможностью! К тому же не стоит забывать, что никто не запрещает выносить некоторые вычисления в отдельные переменные. Это особенно важно при бóльшем количестве переменных, так как в таком случае мы бы упёрлись в ограничение контроллера (или среды разработки?) на максимальное количество элементов в строке — не более 7 штук.
После танцев с гуглением уже успешно забытых формул у меня получилось сократить функции до следующего вида:
val openingFunc: LD = {
val x = !max * !closeButtonPressed * !somethingUnderBarrier
line(!opening, openButtonPressed, x) +
line(opening, !closing, x) +
line(!opening, !closing, !min, openButtonPressed, x)
}
val closingFunc: LD = {
val x = !min * !openButtonPressed * !somethingUnderBarrier
line(!closing, closeButtonPressed, x) +
line(!opening, closing, x) +
line(!opening, !closing, !max, closeButtonPressed, x)
}
В таком виде они и были загружены в контроллер, чтобы получить зачёт по лабораторной работе и снять видео из начала статьи:
Впоследствии эксперименты на эмуляторе показали, что можно спокойно выкинуть ещё по одной строке из каждой функции, но на практике корректность этого предположения не проверялась:
val openingFunc: LD = {
val x = !max * !closeButtonPressed * !somethingUnderBarrier
line(!opening, openButtonPressed, x) +
line(opening, !closing, x)
}
val closingFunc: LD = {
val x = !min * !openButtonPressed * !somethingUnderBarrier
line(!closing, closeButtonPressed, x) +
line(!opening, closing, x)
}
Заключение
Вот и подошёл к концу краткий экскурс в мир программирования промышленного железа. Надеюсь, вы узнали для себя хоть что-то новое — тогда мой труд не был напрасен. А если хотите больше подобного необычного контента в блоге, ставьте лайки и делитесь ссылками на пост с друзьями! =)