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

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

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

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

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

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

Arduino Nano (AVR)


У микроконтроллера есть возможность получать прерывания с любого пина, такие прерывания называются PCINT и работать с ними можно только при помощи сторонних библиотек (вот отличная), либо вручную (читай у меня вот тут). В этом уроке речь пойдёт об обычных прерываниях, которые называются 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()
  • Нужно стараться делать как можно меньше вычислений и вообще “долгих” действий – это будет тормозить работу МК при частых прерываниях:

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


Подключается прерывание при помощи функции 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() Не меняет значение в прерывании! Да, не меняет, но он меняется между прерываниями! Это в принципе всё, что нужно знать о прерываниях, более конкретные случаи мы разберём в продвинутых уроках.

Видео


Полезные страницы


4.5/5 - (12 голосов)
Подписаться
Уведомить о
guest

30 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии