External hardware interrupt - это прерывание, вызванное изменением напряжения на пине МК. Суть в том, что ядро МК не занимается опросом пина и не тратит на это время. Но как только напряжение на пине меняется (цифровой сигнал) - МК получает сигнал, бросает выполнение текущей инструкции, обрабатывает прерывание и возвращается к работе. Подробнее о самом механизме прерываний читай в общем уроке про прерывания.
Чаще всего внешние прерывания используются для детектирования коротких событий - импульсов, или даже для подсчёта их количества, не нагружая основной код. Аппаратное прерывание может поймать короткое нажатие кнопки, поворот энкодера или срабатывание датчика во время сложных долгих вычислений или задержек в коде, т.е. грубо говоря - пин опрашивается параллельно основному коду. Также прерывания могут будить МК из режимов энергосбережения, когда практически вся периферия отключена.
attachInterrupt #
Для подключения внешних прерываний служит функция attachInterrupt(uint8_t interruptNum, void (*handler)(void), uint8_t mode) - подключить внешнее прерывание под номером interruptNum на функцию handler в режиме mode:
- Номер прерывания 
interruptNumможет быть получен из номера пина при помощиdigitalPinToInterrupt(). Нумерация пинов и прерываний зависит от платы/МК, см. информацию по конкретной модели handler- функция-обработчик прерывания видаvoid f(), будет вызвана при срабатывании прерыванияmode- режим прерывания. У разных плат/МК могут быть и другие режимы, см. информацию по конкретной моделиLOW- срабатывает всегда, когда на пине низкий сигналCHANGE- срабатывает при смене сигнала на противоположныйRISING- срабатывает при смене сигнала с низкого (LOW) на высокий (HIGH)FALLING- срабатывает при смене сигнала с высокого (HIGH) на низкий (LOW)
AVR Arduino #
На классических Arduino (UNO, Nano, Mega..) номер прерывания для attachInterrupt() не совпадает с номером пина. Более того, не на всех пинах прерывания вообще поддерживаются:
| МК / номер прерывания | 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 | 
В то же время, на всех пинах поддерживаются PCINT - Pin Change Interrupts, но фреймворк Arduino не имеет для них удобного инструмента. Можно использовать стороннюю библиотеку PinChangeInterrupt, либо настроить эти прерывания вручную через регистры, как описано например у меня в этом уроке.
// пример для Arduino Nano
void isr1() {}
void isr2() {}
void setup() {
    attachInterrupt(0, isr1, CHANGE);                           // пин 2
    attachInterrupt(digitalPinToInterrupt(3), isr2, FALLING);   // пин 3 (прерывание номер 1)
}
ESP8266 #
У ESP8266 почти все GPIO (кроме 16) поддерживают attachInterrupt() по номеру GPIO. Функция-обработчик должна быть объявлена с атрибутом IRAM_ATTR:
IRAM_ATTR void myIsr() {}
void setup() {
    attachInterrupt(1, myIsr, RISING);  // GPIO1
}
На старых версиях ядра - с ICACHE_RAM_ATTR:
void ICACHE_RAM_ATTR myIsr() {}
void setup() {
    attachInterrupt(2, myIsr, RISING);  // GPIO2
}
Примеры #
Счётчик #
Рассмотрим пример, в котором в прерывании считаются нажатия кнопки (без гашения дребезга), а в основном цикле выводятся с задержкой в 1 секунду, т.е. опрос и счёт нажатий будет выполняться во время задержки. Работая с кнопкой в обычном режиме, совместить такую задержку с опросом кнопки не получится:
volatile int counter = 0;  // переменная-счётчик
void btnIsr() {
  counter++;  // + нажатие
}
void setup() {
  Serial.begin(115200);
  // подключили кнопку на D3 и GND
  pinMode(3, INPUT_PULLUP);
  // FALLING - при нажатии на кнопку будет сигнал 0
  attachInterrupt(1, btnIsr, FALLING);
}
void loop() {
  Serial.println(counter);  // выводим
  delay(1000);              // имитация загруженной программы
}
Прерывание может произойти в любой момент выполнения программы, даже во время передачи переменной в функцию. В нашем случае counter состоит из нескольких байт (тип int), поэтому прерывание может произойти даже посреди передачи частей переменной! Хорошей практикой является отключение прерываний на период чтения переменной, которая может меняться в прерывании, например так:
void loop() {
  int buf;
  noInterrupts();
  buf = counter;
  interrupts();
  Serial.println(buf);  // выводим
  delay(1000);          // имитация загруженной программы
}
Флаг #
Если событие необязательно обрабатывать сразу, то лучше использовать следующий алгоритм работы с прерыванием:
- В обработчике прерывания просто поднимаем флаг
 - В основном цикле программы проверяем флаг, если поднят - сбрасываем его и выполняем нужные действия
 
volatile bool intFlag = false;   // флаг
void btnIsr() {
  intFlag = true;   // подняли флаг прерывания
}
void setup() {
  Serial.begin(115200);
  // подключили кнопку на D3 и GND
  pinMode(3, INPUT_PULLUP);
  // FALLING - при нажатии на кнопку будет сигнал 0
  attachInterrupt(1, btnIsr, FALLING);
}
void loop() {
  if (intFlag) {
    intFlag = false;    // сбрасываем
    // совершаем какие-то действия
    Serial.println("Interrupt!");
  }
  delay(500); // имитация загруженной программы
}
Дополнительный контент доступен владельцам набора GyverKIT и по подписке, подробнее читай здесь. Блок содержит:
- Дополнительно, Задания
 - 1 блоков кода
 
Полезные страницы #
- Набор GyverKIT – наш большой стартовый набор Arduino, продаётся в России
 - Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress
 - Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
 - Поддержать автора за работу над уроками