Статьи: Пишем программу для шлагбаума
03.10.2018


В этом году мы писали на Си, баловались ассемблером, вручную программировали таймеры микроконтроллера и игрались с ШИМом без использования Ардуиновских библиотек, паяли схему с полевым транзистором и пытались создать небольшой домашний дата-центр. Сегодня мы вновь окунёмся в далёкую и нетипичную для высокоуровневого разработчика область, вспомним основы дискретной математики и упрощения логических выражений, поработаем с промышленным контроллером Omron CP1L и обучающей моделью шлагбаума!

Статья составлена из материалов лабораторной работы по дисциплине «Программирование микроконтроллеров и микропроцессоров».


Видео с демонстрацией работы (как файл)

Дана модель шлагбаума с кнопками открытия/закрытия, датчиками максимального уровня открытия и минимального уровня закрытия, а также датчиком нахождения чего-либо в створе шлагбаума. Требуется разработать программу, которая будет запускать процесс открытия шлагбаума при нажатии кнопки «Открыть» и закрывать при нажатии кнопки «Закрыть». В случае появления в створе шлагбаума какого-либо объекта или возникновения иной внештатной ситуации, необходимо останавливать работу, чтобы позволить оператору устранить факторы, препятствующие дальнейшей работе, и возобновить работу шлагбаума.

Примечание

Некоторые могут сделать резонное замечание, что нет нужды останавливать шлагбаум при появлении в створе человека на подъёме. Так было сделано умышленно: в первую очередь для упрощения логики, ну а во-вторых почему бы и нет? Не лезь под работающий механизм! Мало ли что может случиться?!

Программируется контроллер Omron в специально предназначенной для этого среде разработки CX Programmer на языке контактно-релейных схем. Да-да, никакого кода — только графическая нотация! Нонсенс для обычного прикладного программиста, но как нам сказали на лекции, большинство программ для промышленных контроллеров пишется (рисуется?) именно на графическом языке контактно-релейных схем (она же LD — Ladder Diagram).

Поехали!

Начинается всё с теории конечных автоматов, которой я не хочу вас грузить (в принципе, про state machines и так должен знать любой программист), так что сразу рассмотрим всё на нашем конкретном примере. Итак, нам нужно представить шлагбаум в виде набора состояний и переходов между ними. Каждое состояние представляет собой определённое уникальное сочетание значений булевых переменных (поскольку мы работаем на максимально низком уровне и оперируем электрическими сигналами, которые могут находиться только в двух состояниях: ток или есть, или его нет). Например, состояние движения шлагбаума описывается тремя вариантами: он или открывается, или закрывается, или стоит на месте. Для хранения этого состояния требуется два бита (две переменные) — школьная формула по информатике 2i=N, где N — количество вариантов, а i — количество бит, которое необходимо для их представления.

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

  1. шлагбаум открывается (открыв., ОТКР или О);
  2. шлагбаум закрывается (закрыв., ЗАКР или З);
  3. достигнуто минимальное значение хода (мин. или MIN);
  4. достигнуто максимальное значение хода (макс. или MAX);
  5. объект (человек) находится в створе (чел.);
  6. нажата кнопка открытия (кн. откр. или КО);
  7. нажата кнопка закрытия (кн. закр. или КЗ).

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

Теперь нам нужно построить таблицу состояний. Самый простой способ — это перебрать все возможные состояния. Но составлять таблицу из 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)
}

В таком виде они и были загружены в контроллер, чтобы получить зачёт по лабораторной работе и снять видео из начала статьи:

Контактно-релейная схема в CX Programmer

Впоследствии эксперименты на эмуляторе показали, что можно спокойно выкинуть ещё по одной строке из каждой функции, но на практике корректность этого предположения не проверялась:

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)
}

Заключение

Вот и подошёл к концу краткий экскурс в мир программирования промышленного железа. Надеюсь, вы узнали для себя хоть что-то новое — тогда мой труд не был напрасен. А если хотите больше подобного необычного контента в блоге, ставьте лайки и делитесь ссылками на пост с друзьями! =)