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 – наш большой стартовый набор Arduino, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
- Поддержать автора за работу над уроками