Использование прерываний на Arduino

Добавлено 7 марта 2016 в 16:40

Оптимизируйте ваши программы для Arduino с помощью прерываний – простого способа для реагирования на события в режиме реального времени!

Мы прерываем нашу передачу...

Как выясняется, существует отличный (но недостаточно часто используемый) механизм, встроенный во все Arduino, который идеально подходит для отслеживания событий в режиме реального времени. Данный механизм называется прерыванием. Работа прерывания заключается в том, чтобы убедиться, что процессор быстро отреагирует на важные события. При обнаружении определенного сигнала прерывание (как и следует из названия) прерывает всё, что делал процессор, и выполняет некоторый код, предназначенный для реагирования на вызвавшую его внешнюю причину, воздействующую на Arduino. После того, как этот код будет выполнен, процессор возвращается к тому, что он изначально делал, как будто ничего не случилось!

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

Прерывания по кнопке

Начнем с простого примера: использования прерывания для отслеживания нажатия кнопки. Для начала, мы возьмем скетч, который вы, вероятно, уже видели: пример «Button», включенный в Arduino IDE (вы можете найти его в каталоге «Примеры», проверьте меню Файл → Примеры → 02. Digital → Button).

const int buttonPin = 2;     // номер вывода с кнопкой
const int ledPin =  13;      // номер вывода со светодиодом

int buttonState = 0;         // переменная для чтения состояния кнопки

void setup() 
{
  // настроить вывод светодиода на выход:
  pinMode(ledPin, OUTPUT);
  // настроить вывод кнопки на вход:
  pinMode(buttonPin, INPUT);
}

void loop() {
  // считать состояние кнопки:
  buttonState = digitalRead(buttonPin);

  // проверить нажата ли кнопка.
  // если нажата, то buttonState равно HIGH:
  if (buttonState == HIGH) 
  {
    // включить светодиод:
    digitalWrite(ledPin, HIGH);
  } 
  else 
  {
    // погасить светодиод:
    digitalWrite(ledPin, LOW);
  }
}

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

const int buttonPin = 2;     // номер вывода с кнопкой
const int ledPin =  13;      // номер вывода со светодиодом

volatile int buttonState = 0;         // переменная для чтения состояния кнопки

void setup() 
{
  // настроить вывод светодиода на выход:
  pinMode(ledPin, OUTPUT);
  // настроить вывод кнопки на вход:
  pinMode(buttonPin, INPUT);
  // прикрепить прерывание к вектору ISR
  attachInterrupt(0, pin_ISR, CHANGE);
}

void loop() 
{
  // Здесь ничего нет!
}

void pin_ISR() 
{
  buttonState = digitalRead(buttonPin);
  digitalWrite(ledPin, buttonState);
}

Циклы и режимы прерываний

Здесь вы заметите несколько изменений. Первым и самым очевидным из них является то, что loop() теперь не содержит никаких инструкций! Мы можем обойтись без них, так как вся работа, которая ранее выполнялась в операторе if/else, теперь выполняется в новой функции pin_ISR(). Этот тип функций называется обработчиком прерывания: его работа состоит в том, чтобы быстро запуститься, обработать прерывание и позволить процессору вернуться обратно к основной программе (то есть к содержимому loop()). При написании обработчика прерывания следует учитывать несколько важных моментов, отражение которых вы можете увидеть в приведенном выше коде:

  • обработчики должны быть короткими и лаконичными. Вы ведь не хотите прерывать основной цикл надолго!
  • у обработчиков нет входных параметров и возвращаемых значений. Все изменения должны быть выполнены на глобальных переменных.

Вам, наверное, интересно: откуда мы знаем, когда запустится прерывание? Что его вызывает? Третья функция, вызываемая в функции setup(), устанавливает прерывание для всей системы. Данная функция, attachInterrupt(), принимает три аргумента:

  1. вектор прерывания, который определяет, какой вывод может генерировать прерывание. Это не сам номер вывода, а ссылка на место в памяти, за которым процессор Arduino должен наблюдать, чтобы увидеть, не произошло ли прерывание. Данное пространство в этом векторе соответствует конкретному внешнему выводу, и не все выводы могут генерировать прерывание! На Arduino Uno генерировать прерывания могут выводы 2 и 3 с векторами прерываний 0 и 1, соответственно. Для получения списка выводов, которые могут генерировать прерывания, смотрите документацию на функцию attachInterrupt для Arduino;
  2. имя функции обработчика прерывания: определяет код, который будет запущен при совпадении условия срабатывания прерывания;
  3. режим прерывания, который определяет, какое действие на выводе вызывает прерывание. Arduino Uno поддерживает четыре режима прерывания:
    • RISING – активирует прерывание по переднему фронту на выводе прерывания;
    • FALLING – активирует прерывание по спаду;
    • CHANGE – реагирует на любое изменение значения вывода прерывания;
    • LOW – вызывает всякий раз, когда на выводе низкий уровень.

И резюмируя, наша настройка attachInterrupt() соответствует отслеживанию вектора прерывания 0 (вывод 2), чтобы отреагировать на прерывание с помощью pin_ISR(), и вызвать pin_ISR() всякий раз, когда произойдет изменение состояния на выводе 2.

Volatile

Еще один момент, на который стоит указать: наш обработчик прерывания использует переменную buttonState для хранения состояния вывода. Проверьте определение buttonState: вместо типа int, мы определили его, как тип volatile int. В чем же здесь дело? volatile является ключевым словом языка C, которое применяется к переменным. Оно означает, что значение переменной находится не под полным контролем программы. То есть значение buttonState может измениться и измениться на что-то, что сама программа не может предсказать – в этом случае, пользовательский ввод.

Еще одна полезная вещь в ключевом слове volatile заключается в защите от любой случайной оптимизации. Компиляторы, как выясняется, выполняют еще несколько дополнительных задач при преобразовании исходного кода программы в машинный исполняемый код. Одной из этих задач является удаление неиспользуемых в исходном коде переменных из машинного кода. Так как переменная buttonState не используется или не вызывается напрямую в функциях loop() или setup(), существует риск того, что компилятор может удалить её, как неиспользуемую переменную. Очевидно, что это неправильно – нам необходима эта переменная! Ключевое слово volatile обладает побочным эффектом, сообщая компилятору, что эту переменную необходимо оставить в покое.

Удаление неиспользуемых переменных из кода – это функциональная особенность, а не баг компиляторов. Люди иногда оставляют в коде неиспользуемые переменные, которые занимают память. Это не такая большая проблема, если вы пишете программу на C для компьютера с гигабайтами оперативной памяти. Однако, на Arduino оперативная память ограничена, и вы не хотите тратить её впустую! Даже C компиляторы для компьютеров будут поступать точно так же, несмотря на массу доступной системной памяти. Зачем? По той же причине, по которой люди убирают за собой после пикника – это хорошая практика, не оставлять после себя мусор.

Подводя итоги

Прерывания – это простой способ заставить вашу систему быстрее реагировать на чувствительные к времени задачи. Они также обладают дополнительным преимуществом – освобождением главного цикла loop(), что позволяет сосредоточить в нем выполнение основной задачи системы (я считаю, что использование прерываний, как правило, позволяет сделать мой код немного более организованным: проще увидеть, для чего разработан основной кусок кода, и какие периодические события обрабатываются прерываниями). Пример, показанный здесь, – это самый базовый случай использования прерываний; вы можете использовать для чтения данных с I2C устройства, беспроводных передачи и приема данных, или даже для запуска или остановки двигателя.

Есть какие-нибудь крутые проекты с прерываниями? Оставляйте комментарии ниже!


На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.


  • 2017-03-17antzol

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

  • 2017-03-17Aleksey Melnichenko

    С помощью какой микросхемы можно суммировать сигналы, что бы на выходе получить 1 прерывание?
    То есть есть например 2 входа. Когда любой из них меняет свое состояние, нужно поменять состояние на выходе этой микросхемы.
    Хочется таким образом подключить 6 прерываний к 1 порту ардуино. Спасибо.

Сообщить об ошибке