Продвинутый курс Arduino · Секреты платы

Урок 3. Мгновенные события: прерывания

Плата занята анимацией, а удары по столу всё равно считаются — все до одного. Знакомимся с прерываниями: событие само врывается в программу. В финале — «чёрный ящик» для посылки.

Урок 3 из 8⏱ примерно 45 минутОбновлено: 14 августа 2026

Что понадобится

  • Контроллер Uno и кабель USB
  • Макетная плата
  • Датчик вибрации SW-420
  • Светодиод и резистор 470 Ом
  • Зуммер
  • Провода

Все детали есть в обучающем наборе, по которому идёт курс.

Весь базовый курс мы жили по одному правилу: хочешь заметить событие — почаще смотри на датчик. Кнопку опрашивали в каждом проходе loop(), и пока проходы шустрые, всё работало. Но стоит плате заняться чем-то долгим — анимацией, мелодией, экраном, — и короткое событие проскакивает незамеченным. Постучали по столу, пока плата «моргала» светодиодом? Значит, не постучали.

Сегодня вы узнаете второй способ — прерывания. Событие перестанет ждать, пока на него посмотрят: оно само ворвётся в программу, чем бы та ни занималась. В финале соберём «чёрный ящик» для посылки: он ловит каждый толчок, помнит их число даже без питания и ругается сиреной на тряску.

Почему опрос пропускает удары

Опрос: датчик проверяется только в зелёные просветыплата занята: шаг анимации, delay(15)…плата занята: шаг анимации, delay(15)…плата занята: шаг анимации, delay(15)…Удар пришёлся на «занятое» время — и к моменту проверки датчик уже успокоился. Событие пропало.Прерывание: удар сам останавливает плату — на долю миллисекундыплата занята: шаг анимации, delay(15)…плата занята: шаг анимации, delay(15)…плата занята: шаг анимации, delay(15)…Чем бы ни была занята программа, обработчик срабатывает мгновенно — ни один удар не теряется.
Сверху — опрос: датчик проверяется только в короткие просветы. Снизу — прерывание: каждый удар пойман.

Датчик вибрации из шестнадцатого урока «звенит» при ударе всего несколько миллисекунд. Если в этот момент программа сидела в delay(15) — событие исчезло без следа. Можно, конечно, писать все программы без единого delay, на чистом millis()… но есть путь честнее: поручить дежурство самой плате.

Собираем схему

Датчик вибрации, светодиод, зуммер — всё старые знакомые. Одна важная деталь: датчик встаёт на вывод 2, и это не случайность.

Выводы 2 и 3 — особенные. Только они на плате Uno умеют будить обработчик прерывания. Остальные выводы на такое не способны — это зашито в сам контроллер. Поэтому если в проекте есть «мгновенное» событие, выводы 2 и 3 берегите именно для него.

++abcdefghij11020269121518+12341Датчик вибрации: столбик 2 → вывод 2, столбик 6 → шина «−». Поставьте его под углом.2Зуммер: «плюс» в столбик 9 → вывод 12; второй вывод — в шину «−».3Светодиод: вывод 10 → резистор 470 Ом → длинная ножка; короткая — в шину «−».4Земля: GND платы → верхняя шина «−».
Датчик вибрации на выводе 2, зуммер на 12, светодиод на 10. Датчик поставьте под углом — так он чувствительнее, вы помните это по уроку о датчиках.

Шаг 1. Считаем опросом — и честно проигрываем

Сначала соберём «плохую» версию, чтобы своими глазами увидеть проблему. Плата занята важным делом — дышит светодиодом, — а на датчик смотрит один раз за цикл.

// Продвинутый курс, урок 3. Шаг 1: считаем удары опросом. Плохо считаем.

const int SENSOR = 2;    // датчик вибрации
const int LED = 10;      // светодиод «плата занята делом»

int hits = 0;            // сколько ударов насчитали

void setup() {
  Serial.begin(9600);
  pinMode(SENSOR, INPUT_PULLUP);
  pinMode(LED, OUTPUT);
  Serial.println("Стучите по столу — я считаю удары.");
}

void loop() {
  // Плата занята «важным делом»: плавно разгорается и гаснет светодиод.
  // Всё это время она НЕ смотрит на датчик.
  for (int b = 0; b <= 255; b = b + 5) {
    analogWrite(LED, b);
    delay(15);
  }
  for (int b = 255; b >= 0; b = b - 5) {
    analogWrite(LED, b);
    delay(15);
  }

  // И только теперь — один быстрый взгляд на датчик.
  if (digitalRead(SENSOR) == HIGH) {   // в покое датчик замкнут, при ударе размыкается
    hits = hits + 1;
    Serial.print("Удар! Всего: ");
    Serial.println(hits);
  }
}

Постучите по столу. Иногда удар засчитается — если повезёт попасть в тот миг, когда программа освободилась. Но большинство ударов пропадает: цикл дыхания занимает почти полторы секунды, а взгляд на датчик — доли миллисекунды. Вратарь, который смотрит на ворота раз в полторы секунды, — плохой вратарь.

Кстати, вспомните: датчик вибрации в покое замкнут, сигнал на выводе — LOW. Удар размыкает его на мгновение, и сигнал прыгает вверх. Этот прыжок нам сейчас пригодится.

Шаг 2. Прерывание: событие врывается само

loop(): анимация……продолжение loop()……и дальше как ни в чём не бывалоудар! (событие на выводе 2)обработчик: hits = hits + 1;длится доли миллисекундыloop() даже не замечает паузы: программа продолжает с того же места. Поэтому обработчикобязан быть коротким: изменить переменную-другую — и всё. Никаких delay и печати в порт.
Удар ставит loop на паузу, выполняется крошечный обработчик — и программа продолжает с того же места, ничего не заметив.

Знакомьтесь — attachInterrupt. Читается строка так: «плата, как только сигнал на выводе 2 прыгнет вверх — бросай всё и выполни функцию onKnock».

// Продвинутый курс, урок 3. Шаг 2: удары считает прерывание.

const int SENSOR = 2;    // прерывания на Uno живут только на выводах 2 и 3
const int LED = 10;

volatile int hits = 0;   // переменную трогает обработчик — нужна пометка volatile

void setup() {
  Serial.begin(9600);
  pinMode(SENSOR, INPUT_PULLUP);
  pinMode(LED, OUTPUT);

  // «Плата, при каждом скачке сигнала вверх на выводе 2 — бросай всё
  // и выполняй функцию onKnock». RISING — потому что в покое датчик
  // замкнут (LOW), а удар его размыкает (сигнал прыгает вверх).
  attachInterrupt(digitalPinToInterrupt(SENSOR), onKnock, RISING);

  Serial.println("Стучите — теперь я не пропущу ни одного удара.");
}

// Обработчик: максимально короткий. Ни delay, ни печати — только пометка.
void onKnock() {
  hits = hits + 1;
}

int shown = 0;   // сколько ударов мы уже показали в мониторе

void loop() {
  // Плата по-прежнему занята анимацией...
  for (int b = 0; b <= 255; b = b + 5) {
    analogWrite(LED, b);
    delay(15);
  }
  for (int b = 255; b >= 0; b = b - 5) {
    analogWrite(LED, b);
    delay(15);
  }

  // ...а печатает в свободную минуту — всё, что накопилось.
  if (hits != shown) {
    shown = hits;
    Serial.print("Ударов: ");
    Serial.println(shown);
  }
}

Разберём новые слова. digitalPinToInterrupt(2) — переводчик: превращает номер вывода в номер прерывания, чтобы не путаться. RISING — «на прыжке вверх»: наш датчик в покое держит LOW, значит удар — это подъём сигнала. Бывают ещё FALLING — на спаде, и CHANGE — на любом изменении.

Функция onKnockобработчик. Обратите внимание, какой он короткий: одна строчка. Это закон жанра: обработчик врывается в программу посреди любого дела, поэтому он обязан отработать молниеносно и уйти. Никаких delay, никакой печати в порт — только пометить событие и исчезнуть. Всю тяжёлую работу — печать, звук, запись — делает loop(), когда освободится.

Слово volatile обязательно. Переменная hits живёт в двух мирах: её меняет обработчик и читает loop(). Без пометки volatile компилятор, оптимизируя программу, может решить, что переменная «не меняется», и спрятать её в свой карман — тогда loop() будет вечно видеть старое значение. volatile запрещает такие фокусы: «эту переменную каждый раз читай по-настоящему». Правило простое: трогаете переменную в обработчике — помечайте её volatile.

Загрузите и постучите. Плата всё так же занята дыханием светодиода — но теперь не пропускает ни одного удара: счёт растёт, даже когда программа сидит в delay.

Шаг 3. Дребезг возвращается

Присмотритесь к числам: один хороший удар прибавляет к счёту сразу 5–20. Знакомый почерк? Это дребезг из одиннадцатого урока: пружинка внутри датчика звенит, и каждое колебание — отдельный «прыжок вверх», отдельное прерывание. Опрос дребезга не видел — он просто не успевал, — а прерывание видит всё.

// Продвинутый курс, урок 3. Шаг 3: один удар — одно срабатывание.

const int SENSOR = 2;
const int LED = 10;

volatile int hits = 0;
volatile unsigned long lastKnock = 0;   // момент последнего засчитанного удара

void setup() {
  Serial.begin(9600);
  pinMode(SENSOR, INPUT_PULLUP);
  pinMode(LED, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(SENSOR), onKnock, RISING);
  Serial.println("Считаю удары, не поддаваясь дребезгу.");
}

void onKnock() {
  // millis() в обработчике можно ЧИТАТЬ (часы просто замирают на время
  // обработки). Если после прошлого удара не прошло 150 мс — это ещё
  // дрожит тот же самый удар, не считаем его заново.
  if (millis() - lastKnock < 150) {
    return;
  }
  lastKnock = millis();
  hits = hits + 1;
}

int shown = 0;

void loop() {
  for (int b = 0; b <= 255; b = b + 5) {
    analogWrite(LED, b);
    delay(15);
  }
  for (int b = 255; b >= 0; b = b - 5) {
    analogWrite(LED, b);
    delay(15);
  }

  if (hits != shown) {
    shown = hits;
    Serial.print("Ударов: ");
    Serial.println(shown);
  }
}

Лекарство то же, что и для кнопки, — окно тишины: после засчитанного удара 150 миллисекунд не считать ничего. Тонкость в том, где стоит проверка — прямо в обработчике. И тут надо знать один секрет: внутри обработчика часы millis() замирают. Читать их можно — они показывают момент удара, — но тикать они продолжат только после выхода. Для нашего окна тишины этого вполне достаточно.

Шаг 4. Стук-метр

Теперь loop() полностью свободен — пусть занимается отчётами. Посчитаем интенсивность: сколько ударов пришлось на каждые десять секунд.

// Продвинутый курс, урок 3. Шаг 4: отчёт каждые десять секунд.

const int SENSOR = 2;

volatile int hits = 0;
volatile unsigned long lastKnock = 0;

unsigned long windowStart = 0;   // когда началась текущая десятисекундка

void setup() {
  Serial.begin(9600);
  pinMode(SENSOR, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(SENSOR), onKnock, RISING);
  Serial.println("Считаю удары порциями по 10 секунд.");
  windowStart = millis();
}

void onKnock() {
  if (millis() - lastKnock < 150) {
    return;
  }
  lastKnock = millis();
  hits = hits + 1;
}

void loop() {
  if (millis() - windowStart >= 10000) {
    windowStart = millis();

    Serial.print("За 10 секунд ударов: ");
    Serial.println(hits);
    hits = 0;   // новая десятисекундка — счёт заново
  }
}

Это разделение труда — главный узор всех программ с прерываниями: обработчик копит, loop() разбирает. Обработчик — кладовщик, который молча ставит галочки; loop — начальник, который в удобное время подводит итоги.

Шаг 5. Проект: чёрный ящик для посылки

Соединим всё, что умеет продвинутый курс. Представьте: вы отправляете хрупкую посылку и хотите знать, как с ней обращались в дороге. Прерывание ловит каждый толчок, вечная память из первого урока хранит их число без всякого питания, а команда с компьютера из второго урока начинает новую поездку.

// Продвинутый курс, урок 3. Шаг 5: сторож посылки с чёрным ящиком.
// Прерывание ловит толчки, EEPROM помнит их число даже без питания,
// сирена ругается на тряску. Урок 1 и урок 3 работают вместе.

#include <EEPROM.h>

const int SENSOR = 2;
const int LED = 10;
const int BUZZER = 12;

const int ADDR_HITS = 40;        // ящики 40-41: число толчков (int)

volatile int hits = 0;           // толчки с момента включения
volatile unsigned long lastKnock = 0;

int savedHits = 0;               // что уже записано в вечную память

void setup() {
  Serial.begin(9600);
  pinMode(SENSOR, INPUT_PULLUP);
  pinMode(LED, OUTPUT);

  // Чёрный ящик: докладываем, что происходило с посылкой раньше.
  int before;
  EEPROM.get(ADDR_HITS, before);
  if (before == -1) {
    before = 0;   // свежая память — толчков ещё не записывали
  }
  Serial.print("Толчков за прошлые поездки: ");
  Serial.println(before);
  Serial.println("Команда 0 — обнулить счёт и начать новую поездку.");

  hits = before;                 // продолжаем счёт с прежнего места
  savedHits = before;

  attachInterrupt(digitalPinToInterrupt(SENSOR), onKnock, RISING);
}

void onKnock() {
  if (millis() - lastKnock < 150) {
    return;
  }
  lastKnock = millis();
  hits = hits + 1;               // в обработчике — ТОЛЬКО пометка
}

void loop() {
  // Всю тяжёлую работу делает loop, а не обработчик.
  if (hits != savedHits) {
    savedHits = hits;

    EEPROM.put(ADDR_HITS, savedHits);   // запись по событию — ресурс цел

    Serial.print("Толчок! Всего: ");
    Serial.println(savedHits);

    digitalWrite(LED, HIGH);            // вспышка и короткая сирена
    tone(BUZZER, 700, 300);
    delay(300);
    digitalWrite(LED, LOW);
  }

  // Команда с компьютера: 0 — начать новую поездку (привет, урок 2).
  if (Serial.available() > 0) {
    char c = Serial.read();
    if (c == '0') {
      hits = 0;
      savedHits = 0;
      EEPROM.put(ADDR_HITS, 0);
      Serial.println("Счёт обнулён. Счастливого пути!");
    }
  }
}

Посмотрите, как аккуратно распределены роли. Обработчик — только hits + 1. Запись в EEPROM, печать, сирена — всё в loop(): он замечает, что счётчик изменился, и делает тяжёлую работу. И записи в вечную память случаются по событию — ресурс памяти в безопасности, как мы договаривались в первом уроке.

Испытание чёрного ящика: постучите три раза, выдерните USB, вставьте обратно и откройте монитор. Плата доложит: «Толчков за прошлые поездки: 3». Память пережила «дорогу». Команда 0 — и ящик готов к новой посылке.

Частые проблемы

  • Счёт не растёт совсем. Датчик обязан стоять на выводе 2 (или 3 — тогда поменяйте номер и в скетче). На других выводах attachInterrupt работать не будет.
  • Один удар — плюс двадцать. Это дребезг: проверьте, что окно тишины из третьего шага на месте, а датчик стоит под углом и не болтается в макетке.
  • Счёт растёт сам, без ударов. Датчик слишком чувствителен в вертикальном положении или стол дрожит. Увеличьте окно тишины до 300 мс и наклоните датчик сильнее.
  • Программа зависла после ваших правок. Загляните в обработчик: не появились ли там delay, tone или Serial.print? Им туда нельзя — печать в порт изнутри обработчика может заморозить плату намертво.

Задания для тренировки

  • РазминкаПусть при каждом засчитанном толчке светодиод коротко вспыхивает — но включает и выключает его loop(), а не обработчик.
  • ПосложнееДобавьте «шкалу строгости»: если за десять секунд толчков больше пяти — длинная сирена и надпись «С посылкой обращаются ужасно!». Стук-метр из четвёртого шага уже почти всё умеет.
  • Со звёздочкой ⭐Переведите датчик Холла из семнадцатого урока на «мгновенные рельсы»: магнит на спице колеса, датчик на выводе 3 — и у вас велокомпьютер, который не пропускает ни одного оборота. Подсказка: у аналогового датчика сигнал плавный, поэтому сравните его с порогом в loop()… или почитайте про режим CHANGE и цифровые датчики Холла — а честнее всего собрать «оборотомер» на герконовом принципе: датчик наклона тоже замыкатель.

Что вы теперь умеете

  • объяснять, чем опрос отличается от прерывания и когда какой способ честнее;
  • подключать мгновенные события: attachInterrupt, digitalPinToInterrupt, режимы RISING / FALLING / CHANGE;
  • писать правильные обработчики: коротко, без delay и печати, с volatile;
  • гасить дребезг окном тишины прямо в обработчике;
  • строить «чёрные ящики»: прерывание копит, loop разбирает, EEPROM помнит.

Блок «Секреты платы» закрыт: вечная память, разговор с компьютером и мгновенные события — всё это пряталось в контроллере с самого первого урока. Дальше — блок «Незнакомые детали»: достанем со дна коробки набора то, что ещё ни разу не побывало в макетке. Начнём с загадочных бочонков — конденсаторов.

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

Почему прерывания есть только на выводах 2 и 3?

Так устроен сам контроллер ATmega328P: аппаратные «звонки» INT0 и INT1 физически подведены только к этим двум ножкам. У других плат выводов с прерываниями больше, поэтому в чужих проектах вы встретите и иные номера — сверяйтесь с описанием своей платы.

Сколько времени можно сидеть в обработчике?

Чем меньше, тем лучше: пока выполняется обработчик, замирают millis(), не отправляется Serial и не ловятся другие события. Хороший обработчик укладывается в микросекунды — изменить переменную, запомнить время. Всё, что дольше пары строк, отдавайте loop().

А кнопку можно повесить на прерывание?

Можно, и это частый приём. Но у кнопки дребезг ещё злее, чем у датчика вибрации, поэтому окно тишины из этого урока обязательно. Для обычных кнопок в неторопливых программах хватает опроса — прерывание берегите для событий, которые нельзя проспать.

← Все уроки курса