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

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

External hardware interrupt - это прерывание, вызванное изменением напряжения на пине микроконтроллера. Основная суть состоит в том, что системное ядро микроконтроллера не занимается опросом пина и не тратит на это время. Но как только напряжение на пине меняется (цифровой сигнал) - микроконтроллер получает сигнал, бросает все дела, обрабатывает прерывание, и возвращается к работе.

Зачем это нужно? Чаще всего прерывания используются для детектирования коротких событий - импульсов, или даже для подсчёта их количества, не нагружая основной код. Аппаратное прерывание может поймать короткое нажатие кнопки или срабатывание датчика во время сложных долгих вычислений или задержек в коде, т.е. грубо говоря - пин опрашивается параллельно основному коду. Также прерывания могут будить МК из режимов энергосбережения, когда вообще практически вся периферия отключена. Посмотрим, как работать с аппаратными прерываниями в среде Arduino IDE.

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

Arduino Nano (AVR)

У микроконтроллера есть возможность получать прерывания с любого пина, такие прерывания называются PCINT и работать с ними можно только при помощи сторонних библиотек (вот отличная), либо вручную (читай у AlexGaver вот тут). В этом уроке речь пойдёт об обычных прерываниях, которые называются INT, потому что стандартный фреймворк Ардуино умеет работать только с ними. Таких прерываний и соответствующих им пинов очень мало:

МК / номер прерывания                    INT 0 INT 1 INT 2 INT 3 INT 4 INT  5

ATmega 328/168 (Nano, UNO, Mini) D2     D3 - - - -

ATmega 32U4 (Leonardo, Micro)         D3         D2         D0         D1 - -

ATmega 2560 (Mega)                             D21         D20         D19         D18         D2         D3

Как вы поняли из таблицы, прерывания имеют свой номер, который отличается от номера пина. Есть кстати удобная функция digitalPinToInterrupt(pin), которая принимает номер пина и возвращает номер прерывания. Скормив этой функции цифру 3 на Arduino Nano, мы получим 1. Всё по таблице выше, функция для ленивых.

Wemos Mini (esp8266)

На esp8266 прерывание можно настроить стандартными средствами на любом пине.

Обработчик прерывания

Сначала нужно объявить функцию-обработчик прерывания, эта функция будет выполнена при срабатывании прерывания:

Для AVR Arduino это функция вида void имя(){}

Для ESP8266/32 функция создаётся с атрибутом IRAM_ATTR или ICACHE_RAM_ATTR. Подробнее читай в уроке про esp8266.

К коду внутри этой функции есть некоторые требования:

Переменные, которые изменяют своё значение в прерывании, должны быть объявлены со спецификатором volatile. Пример: volatile byte val;

Не работают задержки типа delay()

Не меняет своё значение millis() и micros()

Некорректно работает вывод в порт Serial.print()

Нужно стараться делать как можно меньше вычислений и вообще "долгих" действий - это будет тормозить работу МК при частых прерываниях:

Вычисления с float

Работа с динамической памятью (функции new(), malloc(), realloc() и прочие)

Работа со String-строками

Подключение прерывания

Подключается прерывание при помощи функции attachInterrupt(pin, handler, mode):

pin - пин прерывания

Для AVR Arduino это номер прерывания (см. таблицу выше)

Для ESP8266 это номер GPIO или D-пин на плате (как в уроке про цифровые пины)

handler - имя функции-обработчика прерывания, которую мы создали

mode - режим работы прерывания:

RISING (рост) - срабатывает при изменении сигнала с LOW на HIGH

FALLING (падение) - срабатывает при изменении сигнала с HIGH на LOW 

CHANGE (изменение) - срабатывает при изменении сигнала (с LOW на HIGH и наоборот)

LOW (низкий) - срабатывает постоянно при сигнале LOW (не поддерживается на ESP8266)

Прерывание можно отключить при помощи функции detachInterrupt(pin).

Можно глобально запретить прерывания функцией noInterrupts() и снова разрешить их при помощи interrupts(). Аккуратнее с ними! noInterrupts() остановит также прерывания таймеров, и у вас "сломаются" все функции времени и генерация ШИМ.

Пример

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

volatile int counter = 0;  // переменная-счётчик

void setup() {

  Serial.begin(9600); // открыли порт для связи

  // подключили кнопку на D2 и GND

  pinMode(2, INPUT_PULLUP);

  // FALLING - при нажатии на кнопку будет сигнал 0, его и ловим

  attachInterrupt(0, btnIsr, FALLING);

}

void btnIsr() {

  counter++;  // + нажатие

}

void loop() {

  Serial.println(counter);  // выводим

  delay(1000);              // ждём

}

Ловим событие

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

В обработчике прерывания просто поднимаем флаг (volatile bool переменная)

В основном цикле программы проверяем флаг, если поднят - сбрасываем его и выполняем нужные действия

volatile bool intFlag = false;   // флаг

void setup() {

  Serial.begin(9600); // открыли порт для связи

  // подключили кнопку на D2 и GND

  pinMode(2, INPUT_PULLUP);

  attachInterrupt(0, buttonTick, FALLING);

}

void buttonTick() {

  intFlag = true;   // подняли флаг прерывания

}

void loop() {

  if (intFlag) {

    intFlag = false;    // сбрасываем

    // совершаем какие-то действия

    Serial.println("Interrupt!");

  }  

}

Следующий возможный сценарий: нам надо поймать сигнал с "датчика" и сразу на него отреагировать однократно до появления следующего сигнала. Если датчик - кнопка, нас поджидает дребезг контактов. С дребезгом лучше бороться аппаратно, но можно решить проблему программно: запомнить время нажатия и игнорировать последующие срабатывания. Рассмотрим пример, в котором прерывание будет настроено на изменение (CHANGE).


void setup() {

  // прерывание на D2 (UNO/NANO)

  attachInterrupt(0, isr, CHANGE);

}

volatile uint32_t debounce;

void isr() {

  // оставим 100 мс таймаут на гашение дребезга

  // CHANGE не предоставляет состояние пина, 

  // придётся узнать его при помощи digitalRead

  if (millis() - debounce >= 100 && digitalRead(2)) {

    debounce = millis();

    // ваш код по прерыванию по высокому сигналу

  }

}

void loop() {

}

Вы скажете: но ведь millis() Не меняет значение в прерывании! Да, не меняет, но он меняется между прерываниями!