Основы · 12 мин чтения

Прерывания Arduino: attachInterrupt, правила ISR и антидребезг

Как пользоваться прерываниями Arduino: attachInterrupt, пины 2 и 3 на Uno, RISING/FALLING/CHANGE, volatile, антидребезг, примеры с энкодером и измерением оборотов, а также ESP32 и Pico.

Прерывание Arduino заставляет процессор бросить текущую работу в тот момент, когда на пине происходит ожидаемое изменение (или срабатывает событие таймера), и выполнить твою специальную функцию. Проще говоря, вместо того чтобы постоянно спрашивать внутри loop() «кнопка нажата?», ты говоришь Arduino: «сообщи, когда её нажмут». Для этого есть функция attachInterrupt(); на Arduino Uno прерывания поддерживают пины D2 и D3, а на ESP32 и Raspberry Pi Pico прерывание может вызвать почти любой GPIO.

Прерывания незаменимы, когда нужно без пропусков считать очень быстрые импульсы (энкодеры, датчики скорости, датчики расхода воды), поймать короткое событие посреди долгой задачи или разбудить плату из сна. В этом руководстве пошагово разберём, как прерывание работает в железе, какие пины доступны на каждой плате, чем отличаются режимы RISING, FALLING и CHANGE, по каким правилам писать обработчик прерывания (ISR), как устранить дребезг кнопки, а также примеры с кнопкой, энкодером и счётчиком оборотов. В конце ты узнаешь про IRAM_ATTR на ESP32 и про irq в MicroPython на Pico.

Пины прерываний Arduino Uno: D2 (INT0) и D3 (INT1)
У Arduino Uno всего два пина внешних прерываний: D2 (INT0) и D3 (INT1).

Что такое прерывание и как оно работает?

Представь дверной звонок: без него пришлось бы каждую минуту подходить к двери и проверять, не стоит ли кто-нибудь за ней. Это называется опрос (polling), и чтение пина через digitalRead() внутри loop() — именно опрос. Звонок — это прерывание: он звенит, пока ты занят другим делом, ты отвлекаешься, открываешь дверь и возвращаешься к своему занятию. Микроконтроллер работает так же. Когда на пине происходит выбранное изменение, железо устанавливает флаг прерывания; процессор завершает текущую инструкцию, сохраняет регистры, через вектор прерываний переходит к твоему ISR (Interrupt Service Routine, обработчик прерывания), а после его завершения продолжает выполнять loop() ровно с того места, где выполнение было прервано. На Uno такое переключение занимает всего несколько микросекунд.

ХарактеристикаОпрос (polling)Прерывание (interrupt)
Как работаетloop() снова и снова читает пинЖелезо ловит изменение и запускает ISR
Время реакцииЧем медленнее loop(), тем больше задержкаНесколько микросекунд
Если в коде есть delay()События можно пропуститьСобытия всё равно ловятся
Очень быстрые импульсыПропускаютсяСчитаются все до одного
Сложность кодаПростоНужны volatile и атомарное чтение
Лучше всего подходит дляКнопки, медленные датчикиЭнкодеры, датчики скорости и расхода, пробуждение из сна

Какие пины поддерживают прерывания? Arduino, ESP32 и Pico

ПлатаПины внешних прерыванийПримечание
Arduino Uno R3, Nano, Pro MiniD2, D3D2 = INT0, D3 = INT1
Arduino Uno R4 (Minima / WiFi)D2, D3Пины из официального справочника Arduino
Arduino Mega 25602, 3, 18, 19, 20, 2120 и 21 нельзя использовать для прерываний, пока они заняты I2C
Arduino Leonardo, Micro0, 1, 2, 3, 70 и 1 — это ещё и пины последовательного порта
Arduino Nano EveryВсе цифровые пиныПроцессор ATmega4809
ESP32Все GPIOGPIO 34–39 работают только на вход и не имеют встроенной подтяжки
ESP8266 (NodeMCU)Все, кроме GPIO 16 (D0)ISR нужно помечать IRAM_ATTR
Raspberry Pi Pico / Pico WВсе GP0–GP28Пины на 3,3 V, никогда не подавай 5 V

В коде не передавай номер пина напрямую: преобразуй его в номер прерывания через digitalPinToInterrupt(пин), потому что номер прерывания меняется от платы к плате: на Uno D2 — это прерывание 0, а D3 — прерывание 1, тогда как на Leonardo прерывание 0 — это D3, а прерывание 1 — D2. Если на Uno нужно больше двух прерываний, почти на всех пинах работают прерывания по изменению состояния пина (PCINT); правда, они сообщают только, что «что-то изменилось», а все пины одного порта делят один ISR. Библиотеки PinChangeInterrupt и EnableInterrupt упрощают работу с ними. Полную карту пинов своей платы ищи в утилите распиновки, а функции пинов Uno — в статье Распиновка Arduino Uno.

Самая частая ошибка — написать attachInterrupt(2, функция, FALLING). Здесь 2 воспринимается как номер прерывания, а не пина; прерывания 2 на Uno нет, поэтому ничего не происходит, и сообщения об ошибке тоже нет. Правильно так: attachInterrupt(digitalPinToInterrupt(2), функция, FALLING).

Как пользоваться attachInterrupt и режимы прерываний

Синтаксис: attachInterrupt(digitalPinToInterrupt(пин), ISR, режим). Второй параметр — имя функции, которая выполняется при срабатывании прерывания; она не принимает параметров и ничего не возвращает (void). Третий параметр задаёт, какое событие вызывает прерывание. Чтобы отключить прерывание, используй detachInterrupt(digitalPinToInterrupt(пин)), чтобы ненадолго приостановить все прерывания — noInterrupts(), а чтобы снова их включить — interrupts().

РежимКогда срабатывает?Типичное применение
LOWСнова и снова, пока на пине LOWПробуждение из сна
CHANGEПри каждом изменении (на обоих фронтах)Энкодеры, измерение длительности импульса, антидребезг
RISINGПри переходе из LOW в HIGH (нарастающий фронт)Начало импульса датчика, кнопка со стягивающим резистором (pull-down)
FALLINGПри переходе из HIGH в LOW (спадающий фронт)Нажатие кнопки с подтяжкой (pull-up), датчики с открытым коллектором
HIGHПока на пине HIGHТолько на Due, Zero и MKR1000; на ESP32 — ONHIGH

Выбирай режим под свою схему. Если кнопка подключена с INPUT_PULLUP, на пине обычно HIGH, а при нажатии уровень падает до LOW, поэтому нажатие ловит FALLING. Режим LOW требует осторожности: пока на пине LOW, ISR выполняется снова и снова, и loop() почти не продвигается. Поэтому его обычно используют только для пробуждения платы из сна; чтобы разбудить Uno из самого глубокого режима сна (power-down), даташит рекомендует прерывание по уровню LOW или прерывание по изменению состояния пина. На ESP32 константы LOW и HIGH работают как режимы прерывания не так, как можно ожидать: LOW вообще не включает прерывание, а HIGH ведёт себя как нарастающий фронт. Для прерываний по уровню там пишут ONLOW и ONHIGH.

Первый пример: считаем нажатия кнопки с помощью прерывания

  1. Подключи одну ножку кнопки к D2, а другую — к GND. Внешний резистор не нужен: используем встроенную подтяжку.
  2. Загрузи код ниже в плату и открой монитор порта на скорости 9600 бод.
  3. Нажми кнопку несколько раз и смотри, как растёт счётчик. Несмотря на delay(500) внутри loop(), ни одно нажатие не теряется.
  4. Не удивляйся, если число иногда прыгает сразу на 2 или 3: это дребезг контактов, и скоро мы его устраним.
C++ (Arduino IDE)
1const byte BUTTON_PIN = 2;                // kesme pini D2 (INT0) / interrupt pin D2 (INT0)
2volatile unsigned int pressCount = 0;     // ISR ile paylaşılan sayaç / counter shared with the ISR
3
4void onPress() {                          // ISR: kısa ve hızlı / ISR: short and fast
5  pressCount++;
6}
7
8void setup() {
9  Serial.begin(9600);
10  pinMode(BUTTON_PIN, INPUT_PULLUP);      // dahili pull-up, buton GND'ye / internal pull-up, button to GND
11  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onPress, FALLING);
12}
13
14void loop() {
15  noInterrupts();                         // 2 baytlık değeri güvenle kopyala / copy the 2-byte value safely
16  unsigned int count = pressCount;
17  interrupts();
18
19  Serial.print(F("Basis / Presses: "));
20  Serial.println(count);
21  delay(500);                             // loop yavaş olsa da basış kaçmaz / presses are not missed even if loop is slow
22}

В этом коде три важных момента. Во-первых, onPress() лишь прибавляет к счётчику единицу и сразу завершается: задача ISR — отметить событие, а не выполнять работу. Во-вторых, счётчик объявлен как volatile, поэтому компилятор не держит его в регистре и не читает устаревшее значение, а каждый раз берёт свежее значение из памяти. В-третьих, loop() на мгновение отключает прерывания, пока читает счётчик. На Uno unsigned int занимает 2 байта, и 8-битный процессор читает его в два шага. Когда счётчик переходит с 255 на 256, то есть с 0x00FF на 0x0100, loop() сначала читает младший байт (0xFF); если именно в этот момент срабатывает прерывание и увеличивает счётчик, затем читается уже новый старший байт (0x01), и в итоге получается 0x01FF, то есть 511. Такая ошибка проявляется очень редко, поэтому её ещё и трудно отловить. На платах AVR такую же защиту даёт макрос ATOMIC_BLOCK(ATOMIC_RESTORESTATE) из util/atomic.h; у однобайтовых переменных вроде bool и byte этой проблемы нет.

Правила ISR: коротко, volatile, никакого delay

  • Делай ISR коротким и быстрым: пока он выполняется, остальные прерывания ждут. На Uno счётчик millis() и последовательный порт тоже работают на прерываниях, поэтому длинный ISR сбивает тайминг и может привести к потере входящих данных по последовательному порту. Идеальный ISR укладывается в несколько микросекунд.
  • Не используй delay(): delay() отсчитывает время по счётчикам, которые обновляет прерывание Timer0; внутри ISR прерывания отключены, поэтому delay() не работает. millis() прочитать можно, но во время ISR значение не растёт, а micros() через 1–2 мс начинает возвращать неверные значения. Если очень короткая пауза действительно нужна, работает delayMicroseconds().
  • Не используй Serial.print(): отправка данных по последовательному порту тоже зависит от прерываний; она удлиняет ISR и блокируется, если буфер заполнится. Оставь вывод для loop().
  • Делай общие переменные volatile: каждая переменная, которая используется и в ISR, и в loop(), должна быть объявлена как volatile.
  • Читай многобайтовые переменные атомарно: в loop() копируй переменные int, long, float и unsigned long между noInterrupts() и interrupts().
  • Без параметров и возвращаемого значения: ISR должен быть void и не принимать параметров; данные передаются через глобальные переменные.
  • Не помещай тяжёлую работу в ISR: вывод на I2C-дисплей, запись на SD-карту или вызов библиотек, которым самим нужны прерывания (например, Wire), внутри ISR приводят к зависанию.
Самый надёжный шаблон — «подними флаг, работай в loop»: ISR лишь присваивает volatile-переменной типа bool значение true или увеличивает счётчик, а настоящую работу (обновить дисплей, управлять мотором, посчитать) loop() выполняет, когда видит этот флаг. Если писать loop() на millis() вместо delay, вся система остаётся и быстрой, и предсказуемой.

Чтение кнопки через прерывание и антидребезг

При нажатии механической кнопки металлические контакты замыкаются не сразу: примерно от 0,1 до 10 миллисекунд они несколько раз размыкаются и замыкаются. loop() может работать слишком медленно, чтобы это заметить, но прерывание ловит каждый фронт, поэтому одно нажатие может дать 3–5 прерываний. Более того, дребезг бывает не только при нажатии, но и при отпускании, поэтому код, который использует только FALLING с блокировкой по времени, может посчитать нажатие дважды, если долго удерживать кнопку, а потом отпустить. Код ниже использует режим CHANGE и засчитывает нажатие, только когда на пине LOW и линия перед этим была спокойной не менее 50 мс. Так отсеивается дребезг и при нажатии, и при отпускании.

C++ (Arduino IDE)
1const byte BUTTON_PIN = 2;                // kesme pini / interrupt pin
2const byte LED_PIN = 13;                  // dahili LED / onboard LED
3const unsigned long QUIET_MS = 50;        // gereken sessizlik süresi / required quiet time
4
5volatile bool ledState = false;           // ISR ile paylaşılır / shared with the ISR
6
7void onButtonChange() {                   // iki kenarda da çalışır / runs on both edges
8  static unsigned long lastEdgeMs = 0;
9  unsigned long now = millis();           // ISR'de okunur ama artmaz / readable in an ISR but does not advance
10  bool pressed = (digitalRead(BUTTON_PIN) == LOW);
11
12  if (pressed && now - lastEdgeMs > QUIET_MS) {   // hat sessizdi: gerçek basış / line was quiet: real press
13    ledState = !ledState;
14  }
15  lastEdgeMs = now;                       // her kenar süreyi yeniden başlatır / every edge restarts the timer
16}
17
18void setup() {
19  pinMode(LED_PIN, OUTPUT);
20  pinMode(BUTTON_PIN, INPUT_PULLUP);
21  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonChange, CHANGE);
22}
23
24void loop() {
25  digitalWrite(LED_PIN, ledState ? HIGH : LOW);   // bool tek bayt, okuması atomik / bool is one byte, atomic read
26  // burada uzun işler olsa da basış kaçmaz / long tasks here will not make you miss a press
27}

Кроме программного способа, дребезг можно подавить и аппаратно: если поставить конденсатор 100 nF параллельно кнопке и подтягивающий резистор 10 kΩ на пин, постоянная времени составит τ = R × C = 10 kΩ × 100 nF = 1 мс, и короткие всплески дребезга сгладятся; триггер Шмитта на входах Uno тоже очищает фронт. Другие значения можно рассчитать в утилите постоянной времени RC. Честно говоря, для кнопок, которые нажимают рукой, прерывание часто вообще не нужно: если loop() быстрый, хватит опроса. Про подтяжку pull-up, pull-down и антидребезг на millis() читай в статье Как подключить кнопку к Arduino. Прерывания оставь для быстрых и коротких сигналов.

Чтение поворотного энкодера через прерывание

При вращении поворотного энкодера (например, KY-040) на двух пинах, CLK и DT, появляются прямоугольные импульсы, сдвинутые друг относительно друга. При быстром вращении импульсы становятся короче миллисекунды, и опрос легко пропускает шаги. Самый простой способ — подключить CLK к пину прерывания и в момент спада CLK проверить уровень DT: если на DT HIGH, вращение идёт в одну сторону, если LOW — в другую. Эта версия считает один шаг на щелчок; если направление получается обратным, поменяй местами строки ++ и --. Дешёвые энкодеры тоже дребезжат, поэтому конденсатор 10–100 nF между CLK и GND и между DT и GND заметно улучшает результат. Версию, где оба пина подключены к прерываниям, а таблица состояний не даёт пропустить ни одного шага, смотри в руководстве по поворотному энкодеру (KY-040).

C++ (Arduino IDE)
1const byte PIN_CLK = 2;                   // kesme pini / interrupt pin
2const byte PIN_DT = 4;                    // normal dijital giriş / regular digital input
3
4volatile long position = 0;               // 4 baytlık konum / 4-byte position
5
6void onClkFall() {
7  if (digitalRead(PIN_DT) == HIGH) {      // yön DT seviyesinden / direction from the DT level
8    position++;
9  } else {
10    position--;
11  }
12}
13
14void setup() {
15  Serial.begin(115200);
16  pinMode(PIN_CLK, INPUT_PULLUP);
17  pinMode(PIN_DT, INPUT_PULLUP);
18  attachInterrupt(digitalPinToInterrupt(PIN_CLK), onClkFall, FALLING);
19}
20
21void loop() {
22  static long lastPos = 0;
23
24  noInterrupts();                         // atomik kopya / atomic copy
25  long pos = position;
26  interrupts();
27
28  if (pos != lastPos) {
29    Serial.println(pos);
30    lastPos = pos;
31  }
32}

Подсчёт оборотов: измеряем скорость мотора в об/мин

Измерение скорости вращения мотора или колеса — классическое применение прерываний. В качестве датчика берут либо щелевой оптический датчик скорости на LM393 с 20-щелевым диском энкодера, либо датчик Холла, например A3144, и магнит, приклеенный к валу. Каждая щель или каждый проход магнита даёт один импульс. По числу импульсов за одну секунду скорость считается так: об/мин = импульсы в секунду × 60 / импульсы на оборот. Например, если 20-щелевой диск даёт 100 импульсов в секунду, мотор вращается со скоростью 100 × 60 / 20 = 300 об/мин.

C++ (Arduino IDE)
1const byte SENSOR_PIN = 3;                // kesme pini D3 (INT1) / interrupt pin D3 (INT1)
2const byte PULSES_PER_REV = 20;           // diskteki yarık sayısı / slots on the encoder disc
3
4volatile unsigned long pulseCount = 0;
5
6void onPulse() {
7  pulseCount++;
8}
9
10void setup() {
11  Serial.begin(9600);
12  pinMode(SENSOR_PIN, INPUT_PULLUP);
13  attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), onPulse, FALLING);
14}
15
16void loop() {
17  static unsigned long lastMs = 0;
18
19  if (millis() - lastMs >= 1000) {        // her saniye hesapla / compute once per second
20    lastMs += 1000;
21
22    noInterrupts();                       // kopyala ve sıfırla / copy and reset
23    unsigned long pulses = pulseCount;
24    pulseCount = 0;
25    interrupts();
26
27    float rpm = pulses * 60.0 / PULSES_PER_REV;
28    Serial.print(F("RPM: "));
29    Serial.println(rpm, 1);
30  }
31}

Здесь loop() каждую секунду копирует и обнуляет счётчик; оба действия выполняются внутри одного блока noInterrupts(), чтобы ни один импульс не потерялся, если прерывание придёт между ними. С 20-щелевым диском разрешение составляет 3 об/мин. Для более точных результатов на низких скоростях вместо подсчёта импульсов измеряй в ISR время между двумя импульсами с помощью micros() и используй формулу об/мин = 60 000 000 / (период_µs × импульсы на оборот). Тот же метод подходит для датчика расхода воды YF-S201, для измерения скорости датчиком Холла и для оборотов колеса DC-мотора с редуктором.

Коротко о прерываниях по таймеру

Прерывания приходят не только от пинов. Таймеры внутри микроконтроллера тоже могут генерировать прерывания через равные промежутки, и тогда функция выполняется ровно каждую 1 мс или каждые 500 мс, чем бы ни был занят loop(). У Uno три таймера, и некоторые из них уже заняты собственными функциями Arduino:

ТаймерРазрядностьКем используетсяПины ШИМ
Timer08 битmillis(), delay(), micros() (отсчёт времени)5, 6
Timer116 битБиблиотека Servo, TimerOne9, 10
Timer28 битtone(), IRremote3, 11

На Uno проще всего воспользоваться библиотекой TimerOne: Timer1.initialize(500000) задаёт период в микросекундах, а Timer1.attachInterrupt(функция) подключает ISR, который выполняется каждые 500 мс. Поскольку библиотека использует Timer1, она конфликтует с библиотекой Servo и с analogWrite() на пинах 9 и 10. На ESP32 с ядром Arduino 3.x прерывание по таймеру настраивается через timerBegin(1000000), timerAttachInterrupt() и timerAlarm(); функции timerAlarmWrite() и timerAlarmEnable() из 2.x удалены, поэтому старые примеры не компилируются и выдают ошибку «was not declared in this scope» (не объявлено в этой области видимости). На Pico в MicroPython Timer(period=500, mode=Timer.PERIODIC, callback=функция) настраивает периодическое прерывание одной строкой. О том, как таймеры связаны с ШИМ, читай в статье Что такое PWM?

Прерывания на ESP32: IRAM_ATTR и portMUX

На ESP32 не нужно искать особый пин: прерывание может вызвать любой GPIO. Только у GPIO 34–39 нет встроенной подтяжки, поэтому кнопке на этих пинах нужен внешний подтягивающий резистор 10 kΩ; кроме того, в списке ошибок (errata) Espressif отмечено, что GPIO 36 и 39 могут давать ложные прерывания, пока работает АЦП. Главное отличие ESP32 — атрибут IRAM_ATTR перед ISR. Он размещает функцию не во флеш-памяти, а в быстром внутреннем ОЗУ (IRAM). Во время записи во флеш (Preferences, SPIFFS, OTA) кэш отключается; в зависимости от конфигурации прерывание, которое в этот момент обращается к коду во флеш-памяти, может перезагрузить плату с ошибкой «Guru Meditation Error: Core 1 panic'ed (Cache disabled but cached memory region accessed)» (кэш отключён, а код обращается к кэшируемой области памяти). ESP8266 ещё строже: ISR без IRAM_ATTR останавливает плату с сообщением «ISR not in IRAM!» сразу при вызове attachInterrupt.

C++ (Arduino IDE)
1const int BUTTON_PIN = 27;                // her GPIO kesme verebilir / any GPIO can interrupt
2const int LED_PIN = 2;                    // çoğu DevKit'te dahili LED / onboard LED on most DevKits
3const uint32_t QUIET_MS = 50;             // gereken sessizlik süresi / required quiet time
4
5volatile uint32_t pressCount = 0;
6portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;   // paylaşılan veri kilidi / lock for shared data
7
8void IRAM_ATTR onButtonChange() {         // ISR IRAM'de çalışır / ISR runs from IRAM
9  static uint32_t lastEdgeMs = 0;
10  uint32_t now = millis();
11  bool pressed = (digitalRead(BUTTON_PIN) == LOW);
12
13  if (pressed && now - lastEdgeMs > QUIET_MS) {
14    portENTER_CRITICAL_ISR(&mux);
15    pressCount++;
16    portEXIT_CRITICAL_ISR(&mux);
17  }
18  lastEdgeMs = now;
19}
20
21void setup() {
22  Serial.begin(115200);
23  pinMode(LED_PIN, OUTPUT);
24  pinMode(BUTTON_PIN, INPUT_PULLUP);
25  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonChange, CHANGE);
26}
27
28void loop() {
29  static uint32_t lastCount = 0;
30
31  portENTER_CRITICAL(&mux);               // loop tarafında kilit / lock on the loop side
32  uint32_t count = pressCount;
33  portEXIT_CRITICAL(&mux);
34
35  if (count != lastCount) {
36    lastCount = count;
37    digitalWrite(LED_PIN, (count % 2) ? HIGH : LOW);   // her basışta LED değişir / LED toggles on every press
38    Serial.printf("Basis / Press: %lu\n", (unsigned long)count);
39  }
40}

Поскольку на ESP32 работает FreeRTOS и у неё два ядра, общие переменные защищают не через noInterrupts(), как на Uno, а блокировкой portMUX_TYPE: внутри ISR используй portENTER_CRITICAL_ISR(), а в loop() — portENTER_CRITICAL(). Чтение 32-битной переменной на ESP32 и так выполняется за один шаг, но блокировка обязательна, например, когда нужно прочитать и обнулить значение или обновить сразу несколько переменных. Если нужно подключить одну функцию к нескольким пинам, attachInterruptArg() позволяет передать в ISR параметр, например номер пина. Основы ESP32 описаны в статье Что такое ESP32?, а задачи и ядра — в статье FreeRTOS и два ядра ESP32.

Прерывания на Raspberry Pi Pico: irq в MicroPython

На Raspberry Pi Pico прерывание может вызвать любой пин от GP0 до GP28. В MicroPython оно настраивается методом irq() объекта пина: в trigger передаётся Pin.IRQ_FALLING, Pin.IRQ_RISING или их комбинация, а handler — это функция, которая выполняется при срабатывании прерывания; в качестве параметра она получает пин, вызвавший прерывание. В Python нет volatile: переменные, которые меняются в ISR, достаточно объявить как global. Для измерения времени используй time.ticks_ms() и устойчивую к переполнению time.ticks_diff(). Код ниже переносит на Pico логику «тишины на линии» из примера для Arduino:

MicroPython
1from machine import Pin
2import time
3
4led = Pin("LED", Pin.OUT)                 # dahili LED (Pico ve Pico W) / onboard LED (Pico and Pico W)
5button = Pin(14, Pin.IN, Pin.PULL_UP)     # buton GP14 ile GND arasında / button between GP14 and GND
6
7QUIET_MS = 50                             # gereken sessizlik süresi / required quiet time
8presses = 0
9last_edge = time.ticks_ms()
10
11def on_change(pin):                       # handler tetikleyen pini alır / handler receives the pin
12    global presses, last_edge
13    now = time.ticks_ms()
14    if pin.value() == 0 and time.ticks_diff(now, last_edge) > QUIET_MS:
15        presses += 1
16        led.toggle()
17    last_edge = now
18
19button.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=on_change)
20
21while True:
22    print("Basis / Presses:", presses)    # ana döngü uyurken bile basışlar sayılır / presses are counted while the loop sleeps
23    time.sleep(1)

На Pico irq по умолчанию работает как мягкое (soft) прерывание: MicroPython ставит функцию в очередь и выполняет её как можно скорее, поэтому print или создание списка внутри неё не вызывают проблем, но задержка немного растёт. Если передать hard=True, задержка падает до микросекунд; зато внутри ISR нельзя выделять память, так что новый список, строка и даже вычисление с float могут вызвать ошибку «MemoryError». Если Pico программируется из Arduino IDE (ядро Earle Philhower), attachInterrupt() одинаково работает на любом GPIO. Про установку читай в статье Что такое Raspberry Pi Pico?, а про основные команды — в статье Основы MicroPython.

Идеи проектов с прерываниями

  • Измеритель реакции: с помощью micros() в ISR измеряй с точностью лучше миллисекунды, сколько времени проходит от загорания светодиода до нажатия кнопки.
  • Велоспидометр: закрепи магнит на колесе, а датчик Холла — на раме; по времени одного оборота рассчитай скорость и пройденное расстояние.
  • Счётчик воды: считай импульсы датчика расхода YF-S201 и показывай израсходованные литры.
  • Робот-машинка, которая едет прямо: измерь скорость обоих моторов энкодерами колёс и выровняй её.
  • Управление меню: листай экранное меню и меняй настройки поворотным энкодером.
  • Сигнализация на батарейках: пусть выход PIR-датчика будит плату, пока она спит; для ESP32 смотри статью про глубокий сон.

Частые ошибки и их решения

СимптомПричинаРешение
Прерывание вообще не срабатываетВ attachInterrupt(2, ...) передан номер пина, или выбран пин без поддержки прерыванийИспользуй digitalPinToInterrupt(пин); на Uno подключайся к D2 или D3
Одно нажатие увеличивает счётчик на 2–5Дребезг контактовПрограммный антидребезг и/или конденсатор 100 nF
Переменная в loop() никогда не меняетсяЗабыт volatileОбъяви общую переменную как volatile
Иногда появляются абсурдно большие значенияМногобайтовая переменная прочитана не атомарноКопируй её между noInterrupts() и interrupts()
Программа зависает или millis() уходитВ ISR есть delay(), Serial или долгая задачаВ ISR только обновляй флаг или счётчик
Прерывания срабатывают без касания кнопкиПин «висит», подтяжки нетИспользуй INPUT_PULLUP или внешний резистор 10 kΩ
ISR сразу срабатывает один раз после повторного подключенияФронт при выключенном прерывании успевает установить флаг (AVR)На Uno перед attachInterrupt напиши EIFR = bit(INTF0); (для D3 — INTF1)
ESP32 перезагружается случайным образомISR не в IRAM или выполняет тяжёлую работуДобавь IRAM_ATTR и сократи ISR

Прерывания — мощный инструмент, который позволяет ловить каждое событие, даже когда loop() занят другой работой; нужно лишь помнить три правила: ISR должен быть коротким, общие переменные — volatile, а читать их в loop() нужно атомарно. В приложении BlueGrays есть счётчики нажатий кнопки, тест времени реакции, проекты с кнопкой на ESP32 и проект «Антидребезг кнопки на Pico: IRQ против программного опроса», и у каждого есть пошаговая схема подключения и код.

Частые вопросы

Какие пины Arduino Uno поддерживают прерывания?

У Arduino Uno R3, Nano и Pro Mini всего два пина внешних прерываний: D2 (INT0) и D3 (INT1). На Mega 2560 доступны 2, 3, 18, 19, 20 и 21, а на Leonardo — 0, 1, 2, 3 и 7. На остальных пинах можно использовать только прерывания по изменению состояния пина (PCINT), которые лишь сообщают, что состояние пина изменилось.

Почему delay() не работает внутри прерывания (ISR)?

delay() отсчитывает время по счётчикам, которые обновляет прерывание Timer0. Пока выполняется ISR, остальные прерывания ждут, поэтому эти счётчики не продвигаются, и delay() не может нормально работать. Если что-то должно подождать, в ISR только установи флаг, а ожидание сделай в loop() с помощью millis().

Что делает volatile и когда его использовать?

volatile сообщает компилятору, что переменная может измениться в любой момент вне обычного хода программы, например в ISR, поэтому её значение не кэшируется в регистре, а каждый раз читается из памяти. Каждая переменная, которая используется и в ISR, и в loop(), должна быть volatile. Однако volatile не делает чтение многобайтовых переменных атомарным: для этого используют noInterrupts().

Можно ли использовать на Arduino Uno больше 2 прерываний?

Да. Благодаря прерываниям по изменению состояния пина (PCINT) прерывание может вызвать почти любой пин Uno. Они сообщают только об изменении, а пины одного порта (D0–D7, D8–D13, A0–A5) делят один ISR. С библиотеками PinChangeInterrupt или EnableInterrupt работа с ними очень похожа на attachInterrupt.

Что выбрать: прерывания или опрос?

Для кнопок, которые нажимают рукой, и медленно меняющихся датчиков обычно хватает опроса в loop(), и это проще. Прерывания используй для быстрых коротких импульсов, например от энкодеров, датчиков скорости и расхода, или чтобы разбудить плату из сна. Если loop() забит длинными вызовами delay(), замена их на millis() тоже решает большинство проблем.

Что будет, если на ESP32 не написать IRAM_ATTR?

Код обычно компилируется и вроде бы работает, но ISR остаётся во флеш-памяти, запускается медленнее и, в зависимости от конфигурации, во время записи во флеш может вызвать сбой с ошибкой «Cache disabled but cached memory region accessed» (кэш отключён, а код обращается к кэшируемой области памяти). На ESP8266 без IRAM_ATTR плата сразу останавливается с сообщением «ISR not in IRAM!». Поэтому на платах ESP ставь IRAM_ATTR перед каждым ISR.

Все проекты
Включение и выключение светодиода кнопкой (вкл/выкл)
#10Новичок

Включение и выключение светодиода кнопкой (вкл/выкл)

Нажми кнопку один раз — свет загорится, нажми ещё раз — погаснет, совсем как выключатель в твоей комнате. Arduino замечает каждое нажатие и переводит светодиод в противоположное состояние, выжидая кор

4.8(100)
167
Антидребезг кнопки на Pico: IRQ против программного опроса
#159Средний
Premium

Антидребезг кнопки на Pico: IRQ против программного опроса

Нажимаешь кнопку один раз, а код насчитывает 3–4? Это дребезг контактов — металлические контакты колеблются доли миллисекунды! Сравни в мониторе два способа подсчёта рядом: сырой опрос (ловит дребезг и считает лишнее) и IRQ + программный антидребезг (точно). Изучи физику дребезга, программный антидребезг (ticks_diff() > 50ms), регистрацию прерывания Pin.irq(), триггер IRQ_FALLING, обработчики ISR и безопасность global в ISR.

0.0(0)
12
Управление светодиодом кнопкой на ESP32 (переключение)
#105Новичок

Управление светодиодом кнопкой на ESP32 (переключение)

При каждом нажатии кнопки состояние светодиода меняется: если горел — гаснет, если не горел — загорается. Классический шаблон переключения. Антидребезг убирает механический шум кнопки, чтобы одно нажатие давало одно переключение.

4.6(39)
57
Переключение светодиода кнопкой на Pico
#156Новичок

Переключение светодиода кнопкой на Pico

Кнопка + светодиод — следующий уровень! В отличие от базового проекта с кнопкой, держать кнопку не нужно. Нажми и отпусти → светодиод горит. Нажми и отпусти ещё раз → гаснет (переключение). Схема та же (светодиод GP14, кнопка GP2), но код сильно отличается. Изучи определение фронта (ловлю момента спада 1→0), отслеживание состояния и антидребезг (фильтр механического дребезга). Метод led.toggle() в MicroPython меняет состояние одной строкой.

0.0(0)
9
Счётчик на семисегментном индикаторе с кнопками
#43Продвинутый
Premium

Счётчик на семисегментном индикаторе с кнопками

Простой счётчик с тремя кнопками: нажми одну — цифра на индикаторе увеличится на единицу, нажми другую — уменьшится, а третья возвращает её к нулю. Одноразрядный семисегментный индикатор считает от 0

4.6(35)
47
Тест времени реакции на Arduino со звуковым сигналом: измерь рефлексы в миллисекундах (LCD + зуммер + 3 светодиода)
#97Средний
Premium

Тест времени реакции на Arduino со звуковым сигналом: измерь рефлексы в миллисекундах (LCD + зуммер + 3 светодиода)

Насколько быстрые у тебя рефлексы? Нажми кнопку: красный светодиод говорит приготовиться, жёлтый — внимание, и начинается случайное ожидание от 1,5 до 3,5 секунды. Как только загорится зелёный светодиод и пискнет зуммер, нажми снова; время реакции в миллисекундах появится на I2C LCD, а под ним — лучший и средний результат сессии. Нажмёшь до зелёного — на экране TOO EARLY! и звук проигрыша. Удержание кнопки — новый игрок, и у всех одинаковый порядок ожиданий.

4.6(33)
50
Счётчик на WebSocket с ESP32 (2 кнопки + LCD + веб в реальном времени)
#130Продвинутый
Premium

Счётчик на WebSocket с ESP32 (2 кнопки + LCD + веб в реальном времени)

В этом проекте две кнопки, подключённые к ESP32 (GPIO 5 увеличивает, GPIO 4 уменьшает), меняют счётчик. Значение сразу обновляется и на I2C LCD 16x2, и — через поддержку WebSocket в ESPAsyncWebServer — во всех подключённых браузерах без перезагрузки страницы. Вместо опроса по HTTP используется одно постоянное соединение, и ESP32 отправляет новое значение всем через ws.textAll().

4.5(14)
23
Игра на память на Arduino (4 светодиода + 4 кнопки + зуммер)
#93Продвинутый
Premium

Игра на память на Arduino (4 светодиода + 4 кнопки + зуммер)

Доверяешь своей памяти? Четыре цветных светодиода загораются в определённой последовательности, каждый со своей нотой, а ты повторяешь тот же порядок кнопками. Каждый верный раунд добавляет в последов

4.8(46)
69