Логические выражения и ветвление
Для работы с целыми числами в C/C++ существует тип переменных int . Но операции над целыми числами — не единственный инструмент в арсенале разработчика. Одними из важнейших элементов в программировании являются логические операции, выражения и типы данных.
В то время как переменные типа int могут хранить произвольные целые числа, а операции над ними подчиняются законам целочисленной арифметики; логические переменные могут хранить лишь одно из двух значений: истину или ложь, а операции над ними подчиняются законам алгебры логики.
Алгебра логики — умное название для интуитивно понятных вещей: операций «и», «или», «не» над высказываниями, которые либо справедливы, либо нет. Например, рассмотрим такое логическое выражение: « в коридоре темно и по коридору идёт человек ». Оно состоит из двух логических значений (утверждения про темноту и человека) и одного оператора (союз «и»). Его итоговым значением также будет логическое значение, которое можно использовать, скажем, как сигнал для включения света в коридоре гостиницы.
В программировании переменные, которые хранят логические значения называются булевыми переменными, а операторы, которые производят над ними действия называют булевыми операторами или просто логическими операторами.
Логические операции в C++
В C++ для булевых переменных существует тип bool . Для обозначения истины в C++ используется слово true , а для обозначения лжи — слово false . Таким образом, если для объявления целочисленных переменных мы используем:
Для логических переменных, используется:
Поскольку C++ для Arduino — это некоторая надстройка над голым C++, для обозначения логического типа наряду с bool существует ещё слово boolean . Это абсолютные синонимы. Можете использовать и то и другое. Просто из соображений единого стиля и повышения читаемости кода выберите один из терминов и используйте его всюду. Мы в статье будем использовать bool , просто потому что в нём меньше букв и он входит в стандарт языка.
Применение на практике: экологичный отельДавайте разовьём тему с энергосберегающим освещением и сделаем на Arduino устройство, которое включает свет в коридоре только тогда, когда это действительно необходимо.
Допустим, к Arduino подключён аналоговый датчик уровня освещённости, пироэлектрический цифровой датчик движения тёплых объектов и экологичная светодиодная лампа.
Тогда скетч будет выглядеть следующим образом:
Взглянем на loop и поймём что здесь происходит. С первой строкой всё понятно: мы считываем значение освещённости с аналогового сенсора с помощью встроенной функции analogRead и присваиваем его переменной с именем lightLevel .
Далее мы объявляем логическую переменную motionDetected в качестве значения которой присваиваем результат вызова встроенной функции digitalRead . Функция digitalRead похожа по своей сути на analogRead , но может возвращать лишь одно из двух значений: либо истину ( true ), либо ложь ( false ). То есть это функция, которая возвращает логическое значение. Она подходит для считывания показаний разнообразных бинарных цифровых датчиков. В нашем примере мы как раз использовали такой: пироэлектрический сенсор, который выдаёт 0 вольт, пока движения в его радиусе видимости нет и 5 вольт, когда замечено перемещение тёплого объекта: человека, кошки или кого-то ещё. Нулю вольт микроконтроллер ставит в соответствие значение false , а пяти вольтам — true .
Итак, по итогам исполнения второй строки, в переменной motionDetected будет храниться либо истина, либо ложь в зависимости от того замечено ли движение в коридоре.
Далее мы видим определение булевой переменной tooDark , которой в качестве значения присваивается значение выражения lightLevel < LIGHT_LEVEL_THRESHOLD . Символ < , как можно догадаться, в C++ означает оператор «меньше, чем». Из двух численных операндов, этот оператор делает один логический результат. Значение всего выражения считается истинным, если то, что записано слева от знака ( lightLevel в нашем случае) меньше, чем то, что записано справа от знака ( LIGHT_LEVEL_THRESHOLD ). Довольно логично и интуитивно понятно, не правда ли?!
Таким образом в переменной tooDark окажется значение true , только если значение уровня освещённости lightLevel , полученное ранее, окажется меньше 600. В противном случае, в переменной окажется false .
Конкретное значение, вроде 600 в нашем случае, в подобных случаях часто получают экспериментально: в зависимости от используемого сенсора и конфигурации помещения, где стоит устройство.
Идём дальше. Мы видим объявление булевой переменной lightningRequired , в качестве значения которой присваивается значение выражения tooDark && motionDetected . Символ && в C++ означает оператор логического «и». Как можно догадаться, всё выражение считается истинным тогда и только тогда, когда и то, что справа, и то что слева от оператора истинно. Таким образом, переменная lightningRequired примет значение true , если в коридоре одновременно: и слишком темно, и замечено движение человека. Если не выполнено хоть одно из условий, результатом будет false . Как раз то, что нужно.
И наконец, последним выражением идёт вызов функции digitalWrite для пина Arduino, к которому подключена лампа. Нам нужно включить лампу, если переменная lightningRequired содержит истинное значение и выключить, если в ней хранится ложь. Чтобы сделать это, мы используем тернарный условный оператор ? : . Где в качестве условия используем просто значение переменной lightningRequired . Помните? В тернарном операторе условие считается выполненным, если его значение — не ноль; и не выполненным если его значение — ноль.
На самом деле для процессора не существует понятия логических значений. Всё что он умеет — оперировать над целыми числами. Поэтому в C++ существует автоматическое преобразование типов. Там, где ожидается int , а мы используем bool , за кадром происходит автоматическое преобразование: true превращается в целое число 1 , а false — в целое число 0 .
Так что, булевы выражения и переменные — ни что иное, как удобство для программистов, синтаксический сахар, который позволяет писать программы более понятно и выразительно.
Возвращаясь к нашему примеру, в выражении:
второй аргумент примет значение HIGH , если lightningRequired — это true ; и LOW , если lightningRequired — это false . Таким образом, мы добились чего хотели: включения света, если он нужен и выключения, если он не уместен.
О краткости записиЕсли вспомнить об автоматическом преобразовании переменных разных типов, а ещё о том, что HIGH — это ничто иное, как макроопределение числа 1 , а LOW — макроопределение числа 0 , последнюю строку в нашем примере можно сократить до лаконичного выражения:
Мы обошлись без тернарного оператора: ведь всё равно функция digitalWrite получит:
Кроме того, логические выражения — это самые обычные выражения в C++, к которым применяются общие правила встраивания. Поэтому весь код loop в примере с экологичным освещением на самом деле мог бы быть записан в одну строку:
Да, код теперь находится на грани читаемости, но это всего лишь демонстрация возможностей. В реальной жизни лучше использовать пару промежуточных переменных:
Или хотя бы использовать перенос строк для наглядности:
А если свет таки нужен: условное выражение if
Внимательный читатель мог заметить, что приведённый скетч едва ли может работать в реальных условиях. Если мы включим свет, когда слишком темно и зафиксировано движение, при следующем же прогоне loop датчик «увидит» свет от нашей же лампы, программа посчитает, что и так достаточно светло и выключит лампу. Процесс постоянного выключения и включения так и будет продолжаться пока пироэлектрический датчик будет фиксировать движение. В лучшем случае мы получим свечение лампы в пол силы, в худшем — раздражающее мерцание. Как быть?
Можно после включения лампы усыплять программу на, скажем, 30 секунд. Вполне достаточно, чтобы дать постояльцу отеля пройти коридор и не так уж расточительно с точки зрения экономии электроэнергии.
Как сделать так, чтобы задержка на 30 секунд производилась только после включения лампы, но не после выключения? Для этого в C++ существует условное выражение «if». Используя его, скетч может выглядеть следующим образом:
Начало программы прежнее, но в loop появляется новое составное выражение, обозначаемое словами if , else и фигурными скобками. Давайте поймём в чём его суть.
Сразу после слова if компилятор C++ ожидает в круглых скобках увидеть логическое выражение, которое в этом случае называется условием. Оно имеет тот же смысл, что и для тернарного оператора. Если его значение — не ноль или истинно, выполняется блок кода, который следует сразу после условия в фигурных скобках. В нашем случае, если tooDark и motionDetected истины, будет выполнен блок кода из двух строк:
Если же условие было нулём, или что то же самое — ложно, блок кода следующий за условием пропускается и не выполняется вовсе. Зато, если после после этого пропущенного блока следует слово else , выполняется блок кода в фигурных скобках, следующий после этого слова. В нашем случае, если либо tooDark , либо motionDetected были false , выполнится блок кода из одной строки:
Стоит отметить, что в случае выполнения условия, блок кода следующий после else не выполняется, а пропускается. То есть, на самом деле, выражение if — это отображение в языке программирования простого и понятного утверждения: «если что-то, делай то-то, а иначе делай сё-то».
Возвращаясь к нашему примеру, код можно интерпретировать так: «если требуется освещение, включить свет и уснуть на 30 секунд, а если свет не нужен — выключить его». Довольно просто и логично.
Использование выражений, которые меняют ход программы в зависимости от каких-то условий называется ветвлением программы, а блоки кода после if и else называются ветками.
Краткая версия ifПри использовании выражения if совершенно не обязательно использовать ветку else . Если что-то должно произойти при выполнении условия, а при невыполнении не должно происходить ничего, ветку else можно просто опустить. Например, мы можем изменить loop нашей программы следующим образом:
Эта вариация кода делает абсолютно то же, что и раньше. Просто теперь код организован иначе. Мы в любом случае делаем указание лампе на включение или выключение, вызывая digitalWrite с булевым значением lightningRequired , но засыпаем на 30 секунд только если мы включаем свет, т.е. если переменная lightningRequired истинна. Если мы только что выключали свет, спать не нужно: нужно пропустить delay и сразу оказаться в конце функции loop . Поэтому мы не писали ветку else вовсе.
Более того, если в блоке кода после if или else содержится всего одно выражение, фигурные скобки можно не писать:
Вложенные выражения ifБлоки кода, используемые в условных выражениях — это самые обычные блоки, которые следуют общим правилам C++. Поэтому в ветку одного if можно запросто вкладывать другой if .
Для примера рассмотрим скетч устройства, которое контролирует открывание двери холодильника, автомобиля или чего-то такого. Мы хотим, чтобы при открытии двери включалась подсветка, а если дверь остаётся открытой более 20 секунд, раздавался бы предупреждающий писк. При этом, хочется, чтобы писк можно было отключить переключателем, на случай когда производится длительная загрузка/разгрузка: так он не будет нервировать.
Допустим на дверце установлен постоянный магнит, а на раме бинарный цифровой датчик магнитного поля. Если магнитное поле фиксируется — дверца закрыта; иначе магнит отходит от датчика слишком далеко, магнитного поля нет — можно понять, что дверца открыта. Также мы подключим к Arduino лампу подсветки, пьезо-пищалку (buzzer) и рокерный выключатель.
В этом случае код работающего устройства может выглядеть так:
Давайте разберём происходящее в loop . Первым делом мы определяем переменную doorOpened и присваиваем ей значение выражения !digitalRead(DOOR_SENSOR_PIN) . Как уже говорилось, дверь стоит считать открытой, если магнитного поля нет, т.е. digitalRead для нашего сенсора возвращает false . Символ ! перед логическим выражением в C++ означает оператор логического «не» или просто оператор отрицания. Он действует на значение, записанное после него, и из true делает false , а из false делает true . Как раз то, что нам нужно в этом случае.
Далее следует выражение if , проверяющее открыта ли дверь. Если да, начинается исполнение блока кода, следующего непосредственно за условием. В нём мы первым делом включаем подсветку. Затем засыпаем на 20 секунд (20×1000 мс).
Далее мы должны включить пищалку если только она не была умышленно выключена. Для этого мы проверяем состояние переключателя, который за это отвечает. Мы объявляем переменную buzzEnabled , которой присваиваем логическое значение, считанное с выключателя.
Обратите внимание, переменная buzzEnabled объявлена прямо внутри блока кода, следующего за if . Так делать можно и нужно: хорошей практикой является объявление переменных как можно ближе к тому месту, где они впервые используются.
Напомним о понятии области видимости переменных: переменные доступны для использования только внутри того блока, где они объявлены. В нашем случае buzzEnabled может быть использована в выражениях внутри блока кода, следующего за if (doorOpened) , но попытка обращения к ней откуда-то ещё: например, из блока кода ветки else или непосредственно в loop вне ветки if , приведёт к ошибке на этапе компиляции программы. И это хорошо: нам не стоит впутывать переменную, которая нужна сию секунду в другие происходящие процессы; это делает программу чище и нагляднее.
Вслед за чтением переключателя следует вложенное условное выражение. Его суть абсолютно та же, что и ранее: в зависимости от условия выполнить или не выполнить код. Одно лишь отличие: оно расположено прямо в блоке кода ветки другого if . Это распространённая практика, которая встречается в программировании довольно часто. На самом деле, во вложенный if можно вкладывать другие if , в них ещё одни и т.д. до бесконечности: язык C++ вас в этом не ограничивает.
Возвращаясь к примеру, если переключатель находится в положении «включён», т.е. buzzEnabled содержит true , мы включаем писк. Это делается с помощью встроенной функции tone . Она принимает 2 аргумента: номер пина Arduino, куда подключён пьезоизлучатель и частоту писка. В данном случае, мы выбрали частоту 4000 Гц, т.е. 4 КГц.
Наконец, если дверь была закрыта, т.е. doorOpened содержит ложь, мы убеждаемся, что подсветка и пищалка выключены. Это делается в ветке else первого условия if . Как выключить лампу вы понимаете, а функция noTone , как можно догадаться, отключает писк на указанном пине.
Вспоминая о правилах короткой записи и встраивании выражений, мы можем чуть упростить код loop . Он может выглядеть так:
Мы убрали фигурные скобки для внутреннего if и встроили вызовы digitalRead прямо в условные выражения.
Обратите внимание, как правильное использование отступов в блоках кода, помогает легко понять к какому if относится else и что за чем и при каких условиях следует. Никогда не пишите без отступов несмотря на то, что компилятору всё равно. Это делает код не читаемым и для других людей, и для вас самих:
Клики кнопок
Одной из типичных задач при создании устройств на микроконтроллерах является определение момента клика тактовой кнопки, чтобы на это можно было как-то отреагировать. Проблема в том, что кнопка, как сенсор, может лишь сообщить зажата ли она сейчас или отпущена. Она не может сообщить, что была «нажата только что».
Но эта проблема легко решается с помощью небольшого приёма в программной части проекта. Давайте сделаем примитивное устройство с лампой и кнопкой, которое бы включало и выключало лампу, поочерёдно, при нажатии кнопки: клик — включили, клик — выключили:
В скетче мы можем видеть 2 булевы переменные:
Теперь взглянем на сам loop . Первым делом мы считываем состояние кнопки в логическую переменную isButtonDown . Если кнопка зажата isButtonDown будет содержать true .
Далее следует условное выражение if с проверкой условия, суть которого в том, чтобы понять была ли нажата кнопка только что, или она зажата уже давно. Для этого и используется значение wasButtonDown .
Таким образом, условие стоит воспринимать как « кнопка зажата сейчас и не была зажата ранее ». Это и есть условие сиемоментного «клика» кнопкой.
Если условие клика выполнено — действуем. Переворачиваем значение lampState с ног на голову:
Далее, в ветке if следует delay(10) . Вызов delay здесь сделан исключительно из-за несовершенства механических кнопок. При нажатии, за микросекунды, когда соприкасаются пластины, кнопка может зафиксировать замыкание и размыкание десятки раз. Добавив delay(10) мы просто пережидаем шторм. Десять миллисекунд — более чем достаточно для успокоения кнопки, но достаточно мало, чтобы человек заметил это.
Далее мы присваиваем переменной wasButtonDown недавно считанное значение isButtonDown , говорящее о том нажата ли кнопка сейчас. Если кнопка была зажата только что, произойдёт переключение состояния лампы lampState , а переменная wasButtonDown в итоге примет значение true и будет оставаться с ним пока кнопку не отпустят. Таким образом, при следующем вызове loop состояние не будет изменено снова.
Если бы мы не прибегали к этому трюку, а использовали только текущее состояние кнопки без оглядки на предысторию, состояния бы менялись пока кнопка зажата, десятки тысяч раз в секунду. С точки зрения человека это выглядело бы как явная, назойливая проблема с устройством.
Цепочки if
Напоследок давайте запрограммируем устройство, которое является простым климат-контролем для, например, террариума. Допустим к Arduino подключён термистор, кондиционер, обогреватель и зелёный светодиод. Если получаемая температура слишком высокая, мы должны включать кондиционер, если слишком низкая — обогреватель, а если в пределах нормы — включать светодиод.
Тогда программа может выглядеть так:
В этой программе всё должно быть знакомым. Вопрос может вызвать лишь использование else и if рядом, на одной строке. На самом деле — это всего лишь иной способ записать несколько вложенных условных выражений. Мы могли бы написать то же самое таким образом:
Но вспомним, что условное выражение — это полноправное выражение в C++. В внешней ветке else оно одно, поэтому фигурные скобки можно опустить:
А вспомнив о том, что пустое пространство для компилятора ничего не значит, можем убрать несколько отступов и перенос строки, чтобы получить:
Подобные цепочки из выражений if — довольно частое явление. Если бы вариантов было больше трёх, вложенные if смотрелись бы громоздко и уродливо, в то время как такая цепочка выглядела бы всё равно понятно и хорошо.
На самом деле, программу климат-контроля можно написать компактнее. В этом виде она приведена просто для демонстрации if - else if - else цепочки. Для полноты картины приведём краткую версию:
Опять же, всё знакомо за исключением, быть может значения выражения !(tooCold || tooHot) . Символ || в C++ — это оператор логического «или». Значение выражения истинно если хотя бы одно из значений: слева или справа от оператора истинны.
Логические выражения — ни что иное, как арифметические выражения. Здесь действуют всё те же правила: в одном выражении может быть сколько угодно операторов, а очерёдность их применения можно обозначить скобками. В данном случае, мы сначала вычисляем логическое значение tooCold || tooHot , а затем оператором логического «не» ( ! ) инвертируем полученное значение.
Итак, вы познакомились с логическими переменными, выражениями и сопутствующими операторами. Это уже позволяет делать устройства, которые выглядят умными. Включайте фантазию, дерзайте!