
Аппаратные прерывания в Arduino
Забавную картинку к этому уроку я найти не смог, нашёл только какую-то лекцию по программированию, и вот самое начало этой лекции отлично объясняет нам, что такое прерывание. Прерывание в Ардуино можно описать абсолютно точно так же: микроконтроллер “всё бросает”, переключается на выполнение блока функций в обработчике прерывания, выполняет их, а затем возвращается ровно к тому месту основного кода, в котором остановился.
Прерывания бывают разные, то есть не сами прерывания, а их причины: прерывание может вызвать аналогово-цифровой преобразователь, таймер-счётчик или буквально пин микроконтроллера. Такие прерывания называются внешними аппаратными, и именно о них мы сегодня поговорим.
External hardware interrupt – это прерывание, вызванное изменением напряжения на пине микроконтроллера. Основная суть состоит в том, что микроконтроллер (вычислительное ядро) не занимается опросом пина и не тратит на это время, пином занимается другая “железка”. Как только напряжение на пине изменяется (имеется в виду цифровой сигнал, +5 подали/+5 убрали) – микроконтроллер получает сигнал, бросает все дела, обрабатывает прерывание, и возвращается к работе. Зачем это нужно? Чаще всего прерывания используются для детектирования коротких событий – импульсов, или даже для подсчёта их количества, не нагружая основной код. Аппаратное прерывание может поймать короткое нажатие кнопки или срабатывание датчика во время сложных долгих вычислений или задержек в коде, т.е. грубо говоря – пин опрашивается параллельно основному коду. Также прерывания могут будить микроконтроллер из режимов энергосбережения, когда вообще практически вся периферия отключена. Посмотрим, как работать с аппаратными прерываниями в среде Arduino IDE.
Прерывания в Arduino
Начнём с того, что не все пины “могут” в прерывания. Да, есть такая штука, как pinChangeInterrupts, но о ней мы поговорим в продвинутых уроках. Сейчас нужно понять, что аппаратные прерывания могут генерировать только определённые пины:
МК / номер прерывания | 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 | D7 | – |
ATmega 2560 (Mega) | D2 | D3 | D21 | D20 | D19 | D18 |
Как вы поняли из таблицы, прерывания имеют свой номер, который отличается от номера пина. Есть кстати удобная функция digitalPinToInterrupt(pin)
, которая принимает номер пина и возвращает номер прерывания. Скормив этой функции цифру 3 на Ардуино нано, мы получим 1. Всё по таблице выше, функция для ленивых.
Подключается прерывание при помощи функции attachInterrupt(pin, handler, mode)
:
pin
– номер прерыванияhandler
– имя функции-обработчика прерывания (её нужно создать самому)mode
– “режим” работы прерывания:LOW
(низкий) – срабатывает при сигнале LOW на пинеRISING
(рост) – срабатывает при изменении сигнала на пине с LOW на HIGHFALLING
(падение) – срабатывает при изменении сигнала на пине с HIGH на LOWCHANGE
(изменение) – срабатывает при изменении сигнала (с LOW на HIGH и наоборот)
Также прерывание можно отключить при помощи функции detachInterrupt(pin)
, где pin – опять же номер прерывания.
А ещё можно глобально запретить прерывания функцией noInterrupts()
и снова разрешить их при помощи interrupts()
. Аккуратнее с ними! noInterrupts()
остановит также прерывания таймеров, и у вас “сломаются” все функции времени и генерация ШИМ.
Давайте рассмотрим пример, в котором в прерывании считаются нажатия кнопки, а в основном цикле они выводятся с задержкой в 1 секунду. Работая с кнопкой в обычном режиме, совместить такой грубый вывод с задержкой – невозможно:
volatile int counter = 0; // переменная-счётчик void setup() { Serial.begin(9600); // открыли порт для связи // подключили кнопку на D2 и GND pinMode(2, INPUT_PULLUP); \ // D2 это прерывание 0 // обработчик - функция buttonTick // FALLING - при нажатии на кнопку будет сигнал 0, его и ловим attachInterrupt(0, buttonTick, FALLING); } void buttonTick() { counter++; // + нажатие } void loop() { Serial.println(counter); // выводим delay(1000); // ждём }
Итак, наш код считает нажатия даже во время задержки! Здорово. Но что такое volatile
? Мы объявили глобальную переменную counter
, которая будет хранить количество нажатий на кнопку. Если значение переменной будет изменяться в прерывании, нужно сообщить об этом микроконтроллеру при помощи спецификатора volatile
, который пишется перед указанием типа данных переменной, иначе работа будет некорректной. Это просто нужно запомнить: если переменная меняется в прерывании – делайте её volatile
.
Ещё несколько важных моментов:
- Переменные, изменяемые в прерывании, должны быть объявлены как
volatile
- В прерывании не работают задержки типа
delay()
- В прерывании не меняет своё значение
millis()
иmicros()
- В прерывании некорректно работает вывод в порт (
Serial.print()
), также не стоит там его использовать – это нагружает ядро - В прерывании нужно стараться делать как можно меньше вычислений и вообще “долгих” действий – это будет тормозить работу МК при частых прерываниях! Что же делать? Читайте ниже.
Ловим событие
Если прерывание отлавливает какое-то событие, которое необязательно обрабатывать сразу, то лучше использовать следующий алгоритм работы с прерыванием:
- В обработчике прерывания просто поднимаем флаг
- В основном цикле программы проверяем флаг, если поднят – сбрасываем его и выполняем нужные действия
volatile boolean intFlag = false; // флаг void setup() { Serial.begin(9600); // открыли порт для связи // подключили кнопку на D2 и GND pinMode(2, INPUT_PULLUP); // D2 это прерывание 0 // обработчик - функция buttonTick // FALLING - при нажатии на кнопку будет сигнал 0, его и ловим 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() Не меняет значение в прерывании! Да, не меняет, но он меняется между прерываниями!
Это в принципе всё, что нужно знать о прерываниях, более конкретные случаи мы разберём в продвинутых уроках.
Видео
Важные страницы
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту (alex@alexgyver.ru)
- Articles coming soon