Использование прерываний на Arduino
Оптимизируйте ваши программы для 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()
, принимает три аргумента:
- вектор прерывания, который определяет, какой вывод может генерировать прерывание. Это не сам номер вывода, а ссылка на место в памяти, за которым процессор Arduino должен наблюдать, чтобы увидеть, не произошло ли прерывание. Данное пространство в этом векторе соответствует конкретному внешнему выводу, и не все выводы могут генерировать прерывание! На Arduino Uno генерировать прерывания могут выводы 2 и 3 с векторами прерываний 0 и 1, соответственно. Для получения списка выводов, которые могут генерировать прерывания, смотрите документацию на функцию
attachInterrupt
для Arduino; - имя функции обработчика прерывания: определяет код, который будет запущен при совпадении условия срабатывания прерывания;
- режим прерывания, который определяет, какое действие на выводе вызывает прерывание. 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 устройства, беспроводных передачи и приема данных, или даже для запуска или остановки двигателя.
Есть какие-нибудь крутые проекты с прерываниями? Оставляйте комментарии ниже!