Аппаратные прерывания
Забавную картинку к этому уроку я найти не смог, нашёл только какую-то лекцию по программированию, и вот самое начало этой лекции отлично объясняет нам, что такое прерывание. Прерывание в Ардуино можно описать абсолютно точно так же: микроконтроллер "всё бросает", переключается на выполнение блока функций в обработчике прерывания, выполняет их, а затем возвращается ровно к тому месту основного кода, в котором остановился.
Прерывания бывают разные, точнее их причины: прерывание может вызвать АЦП, таймер (урок по прерываниям таймера) или буквально пин микроконтроллера. Такие прерывания называются внешними аппаратными, и именно о них мы сегодня поговорим.
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 на HIGHFALLING
(падение) - срабатывает при изменении сигнала с HIGH на LOWCHANGE
(изменение) - срабатывает при изменении сигнала (с 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()
Не меняет значение в прерывании! Да, не меняет, но он меняется между прерываниями! Это в принципе всё, что нужно знать о прерываниях, более конкретные случаи мы разберём в продвинутых уроках.
Видео
Полезные страницы
- Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])