Статья: Прерывания в Ардуино

Adruino имеет несколько типов прерываний.

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

Внешнее прерывание создается извне.В arduino uno есть только два внешних вывода прерывания. Это цифровой вывод 2 и цифровой вывод 3.

Прерывания – очень важный механизм Arduino, позволяющий внешним устройствам взаимодействовать с контроллером при возникновении разных событий. Установив обработчик аппаратных прерываний в скетче, мы сможем реагировать на включение или выключение кнопки, нажатие клавиатуры, мышки, тики таймера RTC, получение новых данных по UART, I2C или SPI. В этой статье мы узнаем, как работают прерывания на платах Ардуино UnoMega или Nano и приведем пример  использования функции Arduino attachInterrupt().

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

Процессор должен отреагировать на этот сигнал, прервав выполнение текущих инструкций и передав управление обработчику прерывания (ISR, Interrupt Service Routine).

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

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

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

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

Все это и составляет основную сложность работы с прерываниями.

Аппаратные и программные прерывания

Прерывания в Ардуино можно разделить на несколько видов:

  • Аппаратные прерывания. Прерывание на уровне микропроцессорной архитектуры. Самое событие может произойти в производительный момент от внешнего устройства – например, нажатие кнопки на клавиатуре, движение компьютерной мыши и т.п.
  • Программные прерывания. Запускаются внутри программы с помощью специальной инструкции. Используются для того, чтобы вызвать обработчик прерываний.
  • Внутренние (синхронные) прерывания. Внутреннее прерывание возникает в результате изменения или нарушения в исполнении программы (например, при обращении к недопустимому адресу, недопустимый код операции и другие).

Зачем нужны аппаратные прерывания

Аппаратные прерывания возникают в ответ на внешнее событие и исходят от внешнего аппаратного устройства. В Ардуино представлены 4 типа аппаратных прерываний. Все они различаются сигналом на контакте прерывания:

  • Контакт притянут к земле. Обработчик прерывания исполняется до тех пор, пока на пине прерывания будет сигнал LOW.
  • Изменение сигнала на контакте. В таком случае Ардуино выполняет обработчик прерывания, когда на пине прерывания происходит изменение сигнала.
  • Изменение сигнала от LOW к HIGH на контакте – при изменении с низкого сигнала на высокий будет исполняться обработчик прерывания.
  • Изменение сигнала от HIGH к LOW на контакте – при изменении с высокого сигнала на низкий будет исполняться обработчик прерывания.

Прерывания полезны в программах Ардуино, так как помогают решать проблемы синхронизации.

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

Основными причинами, по которым необходимо вызвать прерывание, являются:

  • Определение изменения состояния вывода;
  • Прерывание по таймеру;
  • Прерывания данных по SPI, I2C, USART;
  • Аналогово-цифровое преобразование;
  • Готовность использовать EEPROM, флеш-память.

Как реализуются прерывания в Ардуино

При поступлении сигнала прерывания работа в цикле loop() приостанавливается. Начинается выполнение функции, которая объявляется на выполнение при прерывании. Объявленная функция не может принимать входные значения и возвращать значения при завершении работы. На сам код в основном цикле программы прерывание не влияет. Для работы с прерываниями в Ардуино используется стандартная функция attachInterrupt().

Отличие реализации прерываний в разных платах Ардуино

В зависимости от аппаратной реализации конкретной модели микроконтроллера есть несколько прерываний.

Плата Arduino Uno имеет 2 прерывания на втором и третьем пине, но если требуется более двух выходов, плата поддерживает специальный режим «pin-change».

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

Обработка в таком случае будет сложнее и дольше, так как придется отслеживать последнее состояние на каждом из контактов.

На других платах число прерываний выше. Например, плата Ардуино Мега 2560 имеет 6 пинов, которые могут обрабатывать внешние прерывания.

Для всех плат Ардуино при работе с функцией attachInterrupt (interrupt, function, mode) аргумент Inerrupt 0 связан с цифровым пином 2.

Платаint.0int.1int.2int.3int.4int.5
Uno, Ethernet23
Mega25602321201918
Leonardo32017
Due(см. ниже)

Прерывания в языке Arduino

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

Синтаксис attachInterrupt()

attachInterrupt(interrupt, function, mode)

attachInterrupt(pin, function, mode) (только для Arduino Due)

Плата Arduino DUE поддерживает прерывания на всех линиях ввода-вывода.

Для нее можно не использовать функцию digitalPinToInterrupt и указывать номер вывода прямо в параметрах attachInterrupt.

Функция attachInterrupt используется для работы с прерываниями. Она служит для соединения внешнего прерывания с обработчиком.

Синтаксис вызова: attachInterrupt(interrupt, function, mode)

Аргументы функции:

  • interrupt – номер вызываемого прерывания (стандартно 0 – для 2-го пина, для платы Ардуино Уно 1 – для 3-го пина),
  • function – название вызываемой функции при прерывании(важно – функция не должна ни принимать, ни возвращать какие-либо значения),
  • mode – условие срабатывания прерывания.

Возможна установка следующих вариантов условий срабатывания:

  • LOW – выполняется по низкому уровню сигнала, когда на контакте нулевое значение. Прерывание может циклично повторяться – например, при нажатой кнопке.
  • CHANGE – по фронту, прерывание происходит при изменении сигнала с высокого на низкий или наоборот. Выполняется один раз при любой смене сигнала.
  • RISING – выполнение прерывания один раз при изменении сигнала от LOW к HIGH.
  • FALLING – выполнение прерывания один раз при изменении сигнала от HIGH к LOW.4

В Arduino Due доступно еще одно значение:

  • HIGH — прерывание будет срабатывать всякий раз, когда на выводе присутствует высокий уровень сигнала (только для Arduino Due).

Каждый новый вызов функции attachInterrupt привязывает новый обработчик прерывания, то есть если ранее была привязана другая функция-обработчик, то она уже не будет вызываться.

Аналогичная ситуация с параметром mode, определяющим тип событий, на которые должен реагировать микроконтроллер.

Другими словами нельзя, например, задать отдельные обработчики для FALLING и RISING для одного входа внешнего прерывания.

Важные замечания

При работе с прерываниями нужно обязательно учитывать следующие важные ограничения:

  • Функция – обработчик не должна выполняться слишком долго. Все дело в том, что Ардуино не может обрабатывать несколько прерываний одновременно. Пока выполняется ваша функция-обработчик, все остальные прерывания останутся без внимания и вы можете пропустить важные события. Если надо делать что-то большое – просто передавайте обработку событий в основном цикле loop(). В обработчике вы можете лишь устанавливать флаг события, а в loop – проверять флаг и обрабатывать его.
  • Нужно быть очень аккуратными с переменными. Интеллектуальный компилятор C++ может “пере оптимизировать” вашу программу – убрать не нужные, на его взгляд, переменные. Компилятор просто не увидит, что вы устанавливаете какие-то переменные в одной части, а используете – в другой. Для устранения такой вероятности в случае с базовыми типами данных можно использовать ключевое слово volatile, например так: volatile boolean state = 0. Но этот метод не сработает со сложными структурами данных. Так что надо быть всегда на чеку.
  • Не рекомендуется использовать большое количество прерываний (старайтесь не использовать более 6-8). Большое количество разнообразных событий требует серьезного усложнения кода, а, значит,   ведет к ошибкам. К тому же надо понимать, что ни о какой временной точности исполнения в системах с большим количеством прерываний речи быть не может – вы никогда точно не поймете, каков промежуток между вызовами важных для вас команд.
  • В обработчиках категорически нельзя использовать delay(). Механизм определения интервала задержки использует таймеры, а они тоже работают на прерываниях, которые заблокирует ваш обработчик. В итоге все будут ждать всех и программа зависнет. По этой же причине нельзя использовать протоколы связи, основанные на прерываниях (например, i2c).

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

Начнем с простого примера: использования прерывания для отслеживания нажатия кнопки. Для начала, мы возьмем скетч, который вы, вероятно, уже видели: пример «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 устройства, беспроводных передачи и приема данных, или даже для запуска или остановки двигателя.

(текст взят с https://radioprog.ru/post/114)


Разберем ещё пример работы с внешними прерываниями с использованием описанных функций:

#define ledPin 13
#define interruptPin 2 // Кнопка между цифровым пином 2 и GND
volatile byte state = LOW;

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(interruptPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(interruptPin), blink, FALLING);
}

void loop() {
  digitalWrite(ledPin, state);
}

void blink() {
  state = !state;
}

В функции setup мы настраиваем тринадцатый пин на вывод, чтобы управлять встроенным светодиодом.

Второй пин подтягиваем к питанию, это обеспечит на нем сигнал высокого уровня.

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

Внутри функции blink мы изменяем значение переменной state, что впоследствии приводит к включению и выключению светодиода в функции loop.

Таким образом можно управлять светодиодом, не опрашивая кнопку в основной программе.

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

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

Такое поведение вызвано дребезгом контактов кнопки: многократное изменение сигнала на цифровом входе 2 приводит к повторным вызовам обработчика.

Как следствие значение переменной state может остаться не измененным.

К этой проблеме мы вернемся чуть позже, а пока продолжим разбор примера. В нем остался еще один момент, требующий пояснения – ключевое слово volatile.

volatile

volatile – это квалификатор типа переменной, сообщающий компилятору о том, что значение переменной может измениться в любой момент.

Компилятор учитывает этот факт при построении и оптимизации исполняемого кода.

Чтобы не объяснять назначение volatile абстрактно, давайте рассмотрим работу компилятора на следующем фрагменте кода:

byte A = 0;
byte B;

void loop() {
  A++;
  B = A + 1;
}

Переменные A и B – это ячейки в памяти микроконтроллера. Для того чтобы микроконтроллер мог что-то сделать с ними (изменить, сравнить и т.п.)  их значения должны быть загружены из памяти во внутренние регистры. Поэтому при компиляции данного фрагмента будет сгенерирован код вида:

  1. Загрузить из памяти значение A в регистр Р1
  2. Загрузить в регистр Р2 константу 1
  3. Сложить значение Р2 с Р1 (результат в Р2)
  4. Сохранить значение регистра Р2 в памяти по адресу A
  5. Сложить содержимое регистра Р1 с константой 2
  6. Сохранить значение регистра Р1 в памяти по адресу B

Считывание значения переменной А в регистр происходит в самом начале кода.

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

Использование квалификатора volatile как раз позволяет избежать подобных ситуаций.

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

Конечно, не всегда отсутствие volatile приводит к ошибке, всё зависит от логики программы.

Так приведенный выше пример использования прерываний для управления светодиодом будет работать и без volatile.

Но в других случаях использование этого квалификатора поможет избежать трудновыявляемых ошибок.

Поэтому лучше просто взять за правило: всегда использовать volatile при объявлении переменных, которые обработчик прерывания использует совместно с другими функциями.

Разберем еще один интересный пример:

#define interruptPin 2
volatile byte f = 0;

void setup() {
  pinMode(interruptPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(interruptPin), buttonPressed, FALLING);
}

void loop() {
  while (f == 0) {
    // Что-то делаем в ожидании нажатия кнопки
  }
  // Кнопка нажата
}

void buttonPressed() {
  f = 1;
}

Цикл внутри функции loop должен выполняться до тех пор, пока значение переменной f равно нулю.

А измениться оно должно в обработчике прерывания при нажатии на кнопку.

Если бы мы объявили переменную f без квалификатора volatile, то компилятор, “видя”, что значение переменной внутри цикла не изменяется и условие выхода из цикла остается ложным, заменил бы цикл на бесконечный.

Так работает оптимизация кода при компиляции. Войдя в такой цикл микроконтроллер просто зависнет.

Объявление переменной с квалификатором volatile гарантирует, что эта переменная не получит какой-либо оптимизированный тип доступа.

При работе с прерываниями и совместном использовании переменных обработчиком и основной программой нужно помнить очень важный момент: AVR микроконтроллеры являются 8-битными и для загрузки 16- или 32-разрядного значения из памяти требуется несколько отдельных операций.

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

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

Для исключения подобных ситуаций можно либо запретить обработку прерываний на время обращения к разделяемым переменным при помощи функций interruptsи noInterrupts, либо поместить обращение к такой переменной в атомарно исполняемый блок кода.

Функции interrupts и noInterrupts

Данные функции служат для разрешения и запрета обработки прерываний соответственно.

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

Или если код чувствителен к времени выполнения и потому должен выполняться без прерываний.

В таком случае код должен быть обрамлен указанными функциями:

  noInterrupts(); // Запрещаем обработку прерываний
  // Критичный к времени выполнения код
  interrupts(); // Разрешаем обработку прерываний

Только учтите, что на прерываниях реализована работа многих модулей: таймеры-счетчики, UART, I2C, SPI, АЦП и другие.

Запретив обработку прерываний, вы не сможете, например, использовать класс Serial или функции millis, micros.

Поэтому избегайте длительной блокировки прерываний.

Кроме функций interrupts и noInterrupts для этих же целей можно использовать функции  sei и cli – разрешить и запретить прерывания соответственно.

Разницы между ними нет, просто последние являются частью набора библиотек AVR Libc, другие же введены разработчиками IDE Arduino в качестве альтернативы, более легкой для запоминания и восприятия в программе. Согласитесь, noInterrupts более говорящее название, чем cli.