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