Загрузил Дмитрий Перков

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

АППАРАТНЫЕ ПРЕРЫВАНИЯ
Забавную картинку к этому уроку я найти не смог, нашёл только какую-то лекцию по программированию, и вот самое начало этой лекции
отлично объясняет нам, что такое прерывание. Прерывание в Ардуино можно описать абсолютно точно так же: микроконтроллер “всё
бросает”, переключается на выполнение блока функций в обработчике прерывания, выполняет их, а затем возвращается ровно к тому
месту основного кода, в котором остановился.
Прерывания бывают разные, точнее их причины: прерывание может вызвать АЦП, таймер или буквально пин микроконтроллера. Такие
прерывания называются внешними аппаратными, и именно о них мы сегодня поговорим.
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()
Нужно стараться делать как можно меньше вычислений и вообще “долгих” действий – это будет тормозить работу МК при
частых прерываниях:
Вычисления с 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() Не меняет значение в прерывании! Да, не меняет, но он меняется между прерываниями! Это в принципе
всё, что нужно знать о прерываниях, более конкретные случаи мы разберём в продвинутых уроках.