Опубликовано

Прерывания и другие специальные функции

По материалам книги Джереми Блума “Изучаем Arduino: инструменты и методы технического волшебства. 2-е изд.”  (глава 13. Прерывания и другие специальные функции)

Изучаем Arduino: инструменты и методы технического волшебства. 2-е изд.

Исходный код и прочие электронные ресурсы:

Исходный код, видеоуроки и прочие электронные ресурсы для этой главы можно загрузить с веб-страницы https://www.exploringarduino.com/content2/ch13.

Исходный код для проектов этой главы можно также загрузить на вкладке Downloads веб-страницы издательства Wiley для этой книги: https://www.wiley.com/go/exploringarduino2e.

Что вы узнаете из этой главы

Все разрабатываемые нами до сих пор программы для Arduino были синхронного типа.

Это обстоятельство вызывает несколько проблем, одна из которых состоит в том, что пока исполняется функция delay(), не могут выполняться никакие другие операции. При этом плата Arduino способна решать несколько задач одновременно, поэтому при простое процессора теряются ценные процессорные циклы.

В этой главе мы рассмотрим, как использовать аппаратные и программные прерывания, чтобы придать скетчам Arduino возможность асинхронного исполнения. Прерывания обеспечивают асинхронное исполнение кода, инициируя реакцию программы на определенные события, например, истечение определенного периода времени, изменение состояния входного сигнала и т.п. Как можно судить по их названию, прерывания позволяют прервать исполнение любой исполняющейся в настоящее время задачи, выполнить какую-либо другую задачу, а затем возвратиться к продолжению исполнения прерванной задачи. Мы рассмотрим, как вызывать прерывания по запланированным событиям или по изменению состояния сигнала на входных контактах. На основе полученных знаний мы создадим систему неблокирующих аппаратных прерываний, а также разработаем программу генератора звуковых сигналов по прерываниям таймера.

Аппаратные прерывания

Аппаратные прерывания активируются изменением состояния контакта ввода-вывода. Прерывания этого типа могут быть особенно полезными в тех случаях, когда нужно изменить значение переменной состояния, не прибегая к постоянному опросу состояния выхода кнопки. В некоторых предыдущих главах при каждой итерации главного цикла loop() мы выполняли программную защиту от дребезга и проверяли состояние кнопки. Такой подход работает удовлетворительно в тех случаях, когда исполнение кода в цикле loop() не занимает много времени.

Но, предположим, что нам нужно исполнить в цикле loop() довольно длительную процедуру. Например, нам нужно медленно постепенно повышать яркость светодиода или скорость вращения электродвигателя, используя для этого цикл for() и несколько операторов задержки delay(). Если управлять цветом или скоростью повышения яркости посредством кнопки, то мы пропустим нажатия кнопки, происходящие в течение исполнения функции delay(). Обычно, человеческая реакция достаточно инерционна, что позволяет выполнить большое количество функций в цикле loop() программы Arduino, включая опрос кнопки при каждой его итерации, не пропуская нажатий кнопки. Но когда цикл loop() содержит медленно исполняющиеся компоненты, существует вероятность пропустить события внешнего ввода.

Для решения подобных проблем и предназначены прерывания. Определенные контакты платы Arduino могут вызывать внешние аппаратные прерывания. Аппаратура микроконтроллера знает состояние этих контактов и может асинхронно предоставлять их значение коду прикладной программы. Таким образом, при обнаружении внешнего аппаратного прерывания исполнение главной программы можно приостановить, чтобы выполнить соответствующую процедуру обработки прерывания. Это прерывание может произойти в любой точке исполнения программы. На рис. 13.1 показан алгоритм процесса прерывания.

Рис. 13.1. Влияние внешнего прерывания на ход исполнения главной программы

Опрос состояния и прерывания: преимущества и недостатки каждого подхода

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

Программная реализация

Замечательные качества языка программирования Arduino делают программную реализацию внешних прерываний очень прямолинейной задачей. Тем не менее, метод опроса для определения состояния входных контактов Arduino все же легче, поскольку требует только вызова функции digitalRead(). Поэтому, если нет особой необходимости в аппаратных прерываниях, не используйте их вместо опроса, поскольку для обеспечения прерываний нужен чуть больший объем кода.

Аппаратная реализация

С точки зрения аппаратной реализации для большинства цифровых контактов нет разницы между опросом контакта в цикле и прерыванием, т. к. в обоих случаях просто считывается состояние входа. Но в случае прерываний, активируемых перепадом уровней сигнала, аппаратную составляющую необходимо должным образом модифицировать. Как рассматривалось в главе 2, при нажатии многих кнопок (которые часто используются для активирования входа) их контакты испытывают дребезг. Это представляет серьезную проблему, поскольку дребезг будет много раз активировать процедуру обработки прерывания, тогда как мы хотим вызвать ее только один раз. Еще хуже то, что здесь не подойдет созданная нами ранее программная функция защиты от дребезга, поскольку в процедуре обработки прерывания недопустима функция delay(). Поэтому если для входного сигнала с дребезгом требуется аппаратное прерывание, то сначала нужно аппаратно устранить дребезг. Но в случае входного сигнала свободного от дребезга (например, с поворотного кодера или с другой микросхемы) аппаратное прерывание не будет отличаться от опроса.

Многозадачность

Одна из основных причин для использования прерывания — возможность получения псевдо-многозадачности. Плата Arduino не способна поддерживать настоящую многозадачность, поскольку она оснащена только одним главным микроконтроллером, которым может исполнять только одну команду за раз. Но одну задачу можно кратковременно прерывать для перехода к другой, возвращаясь после нее к первой, и благодаря очень высокой скорости исполнения команд создается видимость одновременного выполнения этих задач, что и называется псевдо-многозадачностью. Например, используя прерывания, можно в процессе уменьшения яркости светодиода реагировать на нажатие кнопки, сигнал с которой управляет скоростью изменения яркости. При методе опроса получить значение состояния контакта внешнего входа можно только тогда, когда процесс исполнения кода в цикле loop() дойдет до функции digitalRead(). Это означает, что наличие в программе каких-либо медленных функций может создать трудности с эффективным мониторингом состояния входного сигнала.

Точность сбора данных

Для некоторых задач высокоскоростного сбора данных прерывания являются абсолютно необходимыми. Предположим, что мы считываем состояние выходного сигнала поворотного кодера, установленного на электродвигателе постоянного тока и выдающего импульс при повороте вала двигателя на определенный угол. Этот сигнал можно подавать на плату Arduino для создания системы обратной связи, способной отслеживать положение вала двигателя, а не только скорость его вращения. Это позволит нам динамически регулировать скорость, в зависимости от нагрузки или следить за величиной поворота вала электродвигателя.

Но при этом нам необходимо не пропустить ни одного импульса от кодера. Эти импульсы довольно короткие (намного короче, чем импульсы, создаваемые нажатием кнопки), и если отслеживать их методом опроса в цикле loop(), то некоторые из них можно пропустить. Если поворотный кодер выдает только один импульс за полный поворот вала, то пропустив один такой импульс, программа будет считать, что двигатель вращается вдвое медленнее, чем в действительности. Таким образом, чтобы обеспечить захват важных событий, подобных рассмотренному, без аппаратных прерываний не обойтись. Но в случае сигналов с медленно изменяющимся состоянием (например, выходной сигнал кнопки) метод опроса может оказаться достаточным.

Возможности аппаратных прерываний Arduino

Для большинства плат Arduino сигналы прерываний можно подавать только на определенные контакты. Информацию, какие именно контакты вашей платы Arduino доступны для подачи на них сигналов прерываний, можно найти в документации по прерываниям на веб-сайте Arduino (blum.fyi/arduino-attach-interrupt).

Настройка контакта платы Arduino для работы в режиме источника аппаратных прерываний осуществляется с помощью функции attachInterrupt(). В первом аргументе функции указывается ID-номер прерывания, который не обязательно такой же самый, как и номер контакта. Определить, какой ID-номер прерывания можно присвоить конкретному контакту платы Arduino, можно с помощью функции digitalPinToInterrupt(). Для этого данной функции нужно просто передать номер цифрового контакта платы, и она сама определит соответствующий ID-номер прерывания в зависимости от версии платы Arduino, для которой компилируются скетч. Например, если мы хотим присвоить сигнал прерывания с кнопки контакту 2 платы Arduino Uno (или эквивалентной плате-клону с микроконтроллером ATmega328P), то в качестве первого аргумента функции attachInterrupt() нужно указать функцию digitalPinToInterrupt(2). Непосредственное указание одной функции в качестве аргумента другой функции вполне приемлемо[1], и, кроме того, делает код более аккуратным.

При возникновении аппаратного прерывания вызывается процедура его обработки. Данная функция указывается во втором аргументе функции attachInterrupt(). Например, менять значение булевой переменной на обратное при каждом прерывании можно с помощью следующей функции, которая передается функции attachInterrupt() во втором аргументе:

1
2
3
4
void toggleLed()
{
var = !var;
}

При вызове этой функции значение булевой переменной меняется на противоположное текущему значению, после чего продолжается исполнение команды главной программы, следующей команде, после исполнения которой произошло прерывание.

В последнем, третьем, аргументе функции attachInterrupt() указывается режим запуска прерывания. Прерывания Arduino могут запускаться низким уровнем сигнала (LOW) на контакте прерывания, изменением уровня сигнала (CHANGE), положительным перепадом уровня сигнала (RISING) или отрицательным перепадом уровня сигнала (FALLING). Некоторые платы Arduino также поддерживают запуск прерываний наличием сигнала высокого уровня (HIGH) на контакте прерывания. Наиболее часто используются режимы запуска CHANGE, RISING, and FALLING, поскольку они обеспечивают одноразовый запуск прерывания при изменении состояния внешнего входного сигнала, например, при смене уровня сигнала кнопки с низкого (LOW) на высокий (HIGH). Смена уровня сигнала с низкого (LOW) на высокий (HIGH) является положительным (RISING) перепадом, а с высокого (HIGH) на низкий (LOW) — отрицательным (FALLING). Режимы запуска LOW и HIGH менее распространены, поскольку в таких случаях прерывание будет запускаться постоянно, пока сохраняется данный уровень сигнала, блокируя исполнение остального кода программы.

Объединив все эти возможности, можно, например, при каждом нажатии кнопки, подключенной к контакту 2 платы Arduino Uno (т. е. изменении уровня ее выходного сигнала с низкого на высокий), вызывать рассмотренную ранее функцию toggleLED(). Для этого в секцию setup() нужно вставить следующую строку кода:

1
attachInterrupt(digitalPinToInterrupt(2), toggleLED, RISING);

Схема запуска прерывания кнопкой, оснащенной аппаратной защитой от дребезга

Чтобы закрепить наши новые знания, мы создадим схему с RGB-светодиодом и кнопкой с аппаратной защитой от дребезга. Яркость выбранного цветового элемента RGB-светодиода циклически постепенно увеличивается до максимальной, а затем уменьшается до минимальной, используя для этого функцию delay(). При нажатии кнопки элемент RGB-светодиода с регулируемой таким образом яркостью меняется на другой.

Схема аппаратной защиты от дребезга

Как рассматривалось в главе 2, в большинстве случаев при нажатии кнопки ее контакты не смыкаются мгновенно, а вибрируют в течение некоторого времени, замыкаясь и размыкаясь, пока не успокоятся в замкнутом положении. Этот эффект называется дребезгом контактов. Это обстоятельство представляет серьезную проблему при вводе сигнала с кнопки для запуска аппаратного прерывания, поскольку кажущееся одноразовое нажатие кнопки, по сути, является многоразовым, в результате чего прерывание будет запускаться несколько раз. К счастью эффект дребезга контактов можно устранить из конечного сигнала аппаратным способом, получив чистый сигнал для подачи на микроконтроллер.

Сначала рассмотрим обычный сигнал, получаемый при нажатии кнопки, оснащенной повышающим резистором. Функция повышающего резистора заключается в установлении на контакте ввода-вывода входного сигнала высокого уровня при разомкнутой кнопке. При нажатии кнопки происходит подключение контакта ввода-вывода на землю, т. е. установка на этом контакте сигнала низкого уровня. При необходимости этот сигнал низкого уровня можно инвертировать далее в схеме. На рис. 13.2 показана осциллограмма выходного сигнала кнопки в момент ее нажатия. Как можно видеть, уровень выходного сигнала кнопки не становится моментально низким, а скачет вверх и вниз, пока не установится в низком состоянии.

Рис. 13.2. Дребезг выходного сигнала нажатой кнопки

Если использовать сигнал, изображенный на рис. 13.2, для запуска прерывания, то прерывание будет запущено не один раз, как ожидалось бы, а столько раз, сколько насчитывается отрицательных перепадов сигнала в процессе перехода его уровня от стабильно высокого к стабильно низкому. Но эту проблему можно решить с помощью резистивно-емкостной цепочки или RC-цепи.

Такую цепочку можно создать, подключив выход кнопки к входному контакту микроконтроллера через резистор, а входной контакт микроконтроллера на землю — через конденсатор. При отпущенной кнопке конденсатор заряжается до напряжения питания (+5 В) через повышающий резистор и добавленный резистор RC-цепи. Конденсатор можно рассматривать, как накопитель электроэнергии, заряжающийся от шины положительного питания. Пока кнопка отпущена, этот накопитель заполняется. А при нажатии кнопки, т. е. при замыкании ее контактов, конденсатор начинает разряжаться на землю через резистор RC-цепи и замкнутые контакты кнопки.

Разряд конденсатора происходит не моментально, а занимает некоторый период времени, в течение которого на входном контакте микроконтроллера напряжение сохраняется на уровне близком к 5 В. Конденсатор перезаряжается при каждом отпускании кнопки, т. е. при размыкании ее контактов. Длительность времени разряда конденсатора превышает период дребезга контактов кнопки, в результате чего к моменту полного разряда конденсатора контакты кнопки успокоятся в стабильном замкнутом состоянии. В результате уровень сигнала на входном контакте микроконтроллера изменяется с высокого к низкому только один раз. Продолжительность периодов заряда и разряда конденсатора, и соответственно, длительность защиты от дребезга, определяется номиналами резистора и конденсатора Рассмотренная схема защиты от дребезга контактов показана на рис. 13.3.

Рис. 13.3. Схема защиты от дребезга с RC-цепочкой (Рисунок создан в программе EAGLE)

Подключенный последовательно кнопке резистор R2 в схеме на рис. 13.3 предотвращает почти моментальный разряд конденсатора. В результате выходной сигнал при разряде конденсатора выглядит так, как показано на осциллограмме на рис. 13.4.

Рис. 13.4. Устранение последствий дребезга контактов с помощью RC-цепочки

Таким образом, на вход контакта прерывания Arduino будет подаваться сигнал с плавным переходом от высокого уровня к низкому. Но для запуска прерывания требуется перепад, который определяется, когда переход с низкого уровня к высокому или наоборот происходит с определенной скоростью.

Большинство современных микроконтроллеров обладают встроенной способностью работать с медленными положительными или отрицательными перепадами цифровых сигналов. Например, в справочном листке для микроконтроллера ATmega приводятся разные пороговые значения для низкого и высокого уровней входного сигнала. При переходе входного сигнала с высокого уровня на низкий, его напряжение должно опуститься ниже 0,2U+, чтобы уровень определился, как низкий. А при переходе входного сигнала с низкого уровня на высокий, его напряжение должно подняться выше ниже 0,7U+, чтобы уровень определился, как высокий. Здесь U+ обозначает напряжение питания микроконтроллера (5 В в моем случае). Этот промежуток обеспечивает устойчивость сигнала в процессе перехода и называется гистерезисом.

Прерывания платформы Arduino могут запускаться таким медленным перепадом уровня входного сигнала. Но желательно разобраться, каким именно образом реализуется эта возможность. Для этого мы создадим схему на триггере Шмитта, чтобы она работала независимо от Arduino. Триггер Шмитта представляет собой схему, которая может создавать резкий перепад выходного сигнала, когда входной сигнал превышает определенное пороговое значение. Полученный таким образом сигнал можно подавать на входной контакт платы Arduino. Для нашего эксперимента мы возьмем микросхему 74AHCT14 триггера Шмитта. Хотя данная микросхема содержит шесть элементов триггера Шмитта, нам понадобится только один из них. Многие производители выпускают логические микросхемы, которые функционально идентичны серии 7400. На рис. 13.5 показана цоколевка шестиэлементной микросхемы инвертирующих триггеров Шмитта производства компании STMicroelectronics.

Рис. 13.5. Цоколевка шестиэлементной микросхемы инвертирующих триггеров Шмитта (Источник: © STMicroelectronics. Репродукция с разрешения)

Мы пропустим выходной сигнал нашей схемы защиты от дребезга через один из таких триггеров Шмитта, прежде чем подавать его на входной контакт прерывания платы Arduino. Принципиальная схема такого устройства изображена на рис. 13.6.

Рис. 13.6. Схема аппаратной защиты от дребезга с обработкой выходного сигнала инвертирующим триггером Шмитта (Рисунок создан в программе EAGLE)

Поскольку это инвертирующий триггер, уровень его выходного сигнала будет противоположен уровню входного сигнала. Это означает, что при нажатой кнопке выходной сигнал будет иметь высокий логический уровень, а при отпущенной — низкий. Поэтому, при написании кода на следующем шаге, для определения события нажатия кнопки нам нужно будет распознать положительный перепад входного сигнала. Окончательно форма выходного сигнала изображена на рис. 13.7, как видим, сигнал чистый, без дребезга.

 

 

Рис. 13.7. Сигнал на выходе схемы подавления дребезга с инвертирующим триггером Шмитта

Такой сигнал вполне подходит для формирования аппаратного прерывания.

Монтаж схемы устранения дребезга

Теперь у нас есть достаточно знаний, чтобы собрать схему устранения дребезга кнопки. Для последующих экспериментов по тестированию аппаратной защиты от дребезга и кода обработки прерываний нам нужно будет собрать схему с RGB-светодиодом и кнопкой. Монтажная схема показана на рис. 13.8.

Рис. 13.8. Монтажная схема аппаратной защиты от дребезга (Рисунок создан в программе Fritzing)

Разработка программы

Следующая задача — создать простую программу для тестирования как нашей схемы защиты от дребезга, так и возможностей аппаратных прерываний платформы Arduino. Самым очевидным и полезным применением аппаратных прерываний с платформой Arduino будет получение возможности мониторинга состояния входных сигналов даже при выполнении операций заданной длительности, в которых используется функция delay(). Такая возможность может потребоваться во многих ситуациях, но мы рассмотрим одну из наиболее простых: постепенное изменение яркости светодиода методом широтно-импульсной модуляции посредством функции analogWrite(). В этом скетче яркость одного из элементов RGB-светодиода будет циклически повышаться до максимального значения 255, а затем понижаться до минимального значения 0. При нажатии кнопки будет меняться элемент RGB-светодиода с регулируемой таким образом яркостью. Решить эту задачу методом опроса невозможно, поскольку мы могли бы проверять состояние кнопки только по завершению цикла, в результате чего большинство нажатий кнопки были бы наверняка пропущены.

Но прежде чем приступать к рассмотрению программы для реализации этой задачи, нам нужно разобраться с понятием волатильных (временных) переменных. В частности, переменные, значения которых будут меняться при исполнении процедуры обработки прерывания, должны объявляться как волатильные: volatile. Это необходимо для правильной обработки этих переменных компилятором. Объявление переменной волатильной выполняется добавлением ключевого слова volatile в начале объявления:

volatile int selectedLED = 9;

Чтобы плата Arduino ожидала сигнала прерывания на соответствующем контакте, для этого контакта в разделе setup() нужно посредством функции attachInterrupt() задать режим отслеживания прерываний. В аргументах этой функции указывается ID-номер прерывания в виде функции digitalPinToInterrupt(), функция обработки прерывания, а также режим запуска прерывания (RISING, FALLING и т.п.). В нашей программе кнопка подключена к контакту 2 платы Arduino, которому функция digitalPinToInterrupt() присваивает ID-номер прерывания 0. При запросе прерывания вызывается функция swap(), а сам запуск осуществляется положительным перепадом сигнала прерывания. В результате, функция attachInterrupt() будет иметь такой вид:

attachInterrupt(digitalPinToInterrupt(2), swap, RISING);

Далее нужно создать функцию swap() и добавить ее в программу, полный код которой приведен в листинге 13.1. Это все, что нам необходимо сделать в отношении программной части. Подключив прерывание и создав функцию для его обработки, в остальной части основной программы можно выполнять любые требуемые операции. При запуске прерывания исполнение основной программы приостанавливается и исполняется функция обработки прерывания, по завершению которой возобновляется исполнение основной в точке, в которой оно было прервано. Поскольку прерывания останавливают ход основной программы, функции их обработки следует делать очень короткими и не использовать в них никаких пауз. В действительности, функция delay() даже не будет работать в функции обработки прерывания. Выяснив все это, теперь мы можем написать следующую программу, которая циклически повышает и понижает яркость одного из трех элементов RGB-светодиода и при нажатии кнопки переключается на другой, продолжая с ним делать то же самое.

Листинг 13.1. Программа hw_multitask.ino для демонстрации аппаратных прерываний

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Управление прерыванием, используя кнопку с аппаратной защитой
// от дребезга
// Контакты Arduino для считывания сигнала с кнопок
const int BTN = 2; // Сигнал кнопки после устранения дребезга
// подаем на контакт 2
const int RED = 11; // Подключаем катод красного светодиода
// к контакту 11
const int GREEN = 10; // Подключаем катод зеленого светодиода
// к контакту 10
const int BLUE = 9; // Подключаем катод синего светодиода
// к контакту 9
// Волатильные переменные могут модифицироваться
// функцией обработки прерывания
volatile int selectedLED = RED;
 
void setup()
{
pinMode(RED, OUTPUT);
pinMode(GREEN, OUTPUT);
pinMode(BLUE, OUTPUT);
// Начинаем исполнение с выключенным RGB-светодиодом
// Сигнал управления инвертирован, поскольку мы управляем катодом
digitalWrite(RED, HIGH);
digitalWrite(BLUE, HIGH);
digitalWrite(GREEN, HIGH);
// Поскольку сигнал на контакте инвертирован,
// ожидаем положительный перепад
attachInterrupt(digitalPinToInterrupt(BTN), swap, RISING);
}
 
void swap()
{
// Выключаем текущий составляющий светодиод. Высокий уровень (HIGH)
// выключает светодиод, поскольку это светодиод с общим анодом
digitalWrite(selectedLED, HIGH);
// Затем включаем другой составляющий светодиод
if (selectedLED == GREEN)
selectedLED = RED;
else if (selectedLED == RED)
selectedLED = BLUE;
else if (selectedLED == BLUE)
selectedLED = GREEN;
}
 
void loop()
{
// Постепенно повышаем яркость светодиода
// Сигнал управления инвертирован, поскольку мы управляем катодом
for (int i=255; i>=0; i--)
{
analogWrite(selectedLED, i);
delay(10);
}
// Постепенно понижаем яркость светодиода
// Сигнал управления инвертирован, поскольку мы управляем катодом
for (int i=0; i<=255; i++)
{
analogWrite(selectedLED, i);
delay(10);
}
delay(1000);
}

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


Примечание

  1. На веб-странице для этой главы (https://www.exploringarduino.com/content2/ch13) можно просмотреть видеоклип, демонстрирующий эту схему и скетч для нее в действии.
  2. А в данном случае даже рекомендуется. См. документацию по функции attachInterrupt() на веб-сайте Arduino https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/.