ESP32: Прерывания GPIO с Arduino IDE

Узнайте, как настроить и обработать прерывания на плате ESP32 для обнаружения и реагирования на изменения состояния входных GPIO. Мы создадим пример проекта с использованием тактовой кнопки и ещё один – с PIR-датчиком движения.

ESP32 прерывания GPIO с Arduino IDE

Настройка входного вывода ESP32 в качестве прерывания позволяет быстро обнаруживать события (изменения состояния GPIO) и реагировать на них, приостанавливая основную программу и запуская функцию обратного вызова (также называемую обработчиком прерывания – ISR). Это может быть полезно для определения нажатия кнопки, когда датчик превышает определённый порог, при обнаружении движения и т.д.

Предварительные требования

Прежде чем приступить к этому руководству, у вас должны быть установлены платы ESP32 в Arduino IDE. Следуйте приведённому ниже руководству для установки ESP32 в Arduino IDE, если вы ещё этого не сделали.

Введение в прерывания

Прерывания полезны для автоматического выполнения действий в программах микроконтроллеров и помогают решать проблемы с синхронизацией.

Прерывания предоставляют механизмы для реагирования на внешние события, позволяя вашим платам ESP32 быстро реагировать на изменения без постоянного опроса (непрерывной проверки текущего значения вывода или переменной).

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

Прерывания – это сигналы, которые приостанавливают нормальный ход выполнения программы для обработки определённого события. Когда происходит прерывание, процессор прекращает выполнение основной программы для выполнения задачи, а затем возвращается к основной программе. Эта задача также называется обработчиком прерывания (или процедурой обслуживания прерывания, ISR), как показано на рисунке ниже.

Как работают прерывания

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

Типы прерываний

Существуют различные типы прерываний: внешние прерывания (аппаратные) и прерывания по таймеру (программные).

  1. Внешние прерывания: вызываются внешними сигналами и обнаруживаются на GPIO ESP32, такими как нажатие кнопки или показание датчика – это аппаратное прерывание, связанное с конкретным выводом GPIO. Когда состояние вывода изменяется, запускается процедура обслуживания прерывания (ISR). В данном руководстве мы сосредоточимся именно на них.

  2. Прерывания по таймеру (программные таймеры): инициируются на основе временных интервалов, позволяя выполнять периодические действия – это программное прерывание. Узнать больше о программных прерываниях ESP32 можно в этом руководстве.

Использование прерываний с ESP32

Чтобы настроить прерывание в Arduino IDE, используйте функцию attachInterrupt(), которая принимает в качестве аргументов: номер GPIO, имя вызываемой функции и режим:

attachInterrupt(GPIO, callback_function, mode);

Эту инструкцию следует добавить в setup() вашего кода Arduino.

Давайте рассмотрим аргументы, которые нужно передать этой функции.

Прерывание GPIO

Первый аргумент функции attachInterrupt() – это номер GPIO, на котором мы будем обнаруживать изменение. Например, если вы хотите использовать GPIO 27 в качестве прерывания, вы можете использовать:

digitalPinToInterrupt(27)

На плате ESP32 все выводы, которые могут работать как входы, можно настроить в качестве прерываний.

Функция обратного вызова

Второй аргумент функции attachInterrupt() – это имя функции, которая будет вызвана при срабатывании прерывания.

Существует несколько важных правил, которые необходимо учитывать при определении ISR (функции обратного вызова).

  1. ISR не должна возвращать значение.

  2. ISR должна быть как можно более короткой и быстрой, поскольку она останавливает нормальное выполнение кода.

  3. Она должна иметь атрибут ARDUINO_ISR_ATTR, чтобы выполняться во внутренней RAM ESP32, а не во Flash-памяти. Доступ к IRAM значительно быстрее, что критически важно для надёжной работы ISR без проблем с синхронизацией или сбоев при прерываниях.

  4. Переменные, которые используются внутри ISR и в остальном коде, предпочтительно должны быть объявлены как volatile. Это предотвращает кэширование значений компилятором в регистрах (и пропуск обращений к памяти), поэтому чтение/запись всегда обращается к фактическому адресу в памяти и отражает неожиданные изменения, вызванные прерыванием.

Вот пример ISR, чтобы вы могли проверить синтаксис:

void ARDUINO_ISR_ATTR my_callback() {
    // Любой код, который вы хотите выполнить
}

Ещё один важный момент относительно ISR заключается в том, что код в них должен быть максимально быстрым и простым. Избегайте таких вещей, как сложные операции, запись в Serial Monitor или использование delay(). Вместо этого следует использовать флаг или счётчик для обозначения того, что прерывание произошло, а затем обрабатывать всё необходимое в основном коде или в секции loop().

Режим

Третий аргумент – это режим. Существует 5 различных режимов:

  • LOW: запускает прерывание, когда вывод находится в состоянии LOW;

  • HIGH: запускает прерывание, когда вывод находится в состоянии HIGH;

  • CHANGE: запускает прерывание при любом изменении значения вывода – например, с HIGH на LOW или с LOW на HIGH;

  • FALLING: срабатывает, когда вывод переходит из HIGH в LOW;

  • RISING: срабатывает, когда вывод переходит из LOW в HIGH.

Следующее изображение поможет вам лучше понять различные режимы срабатывания.

Режимы прерываний

Подключение прерывания с аргументами

Помимо функции attachInterrupt(), вы можете альтернативно использовать функцию attachInterruptArg(). Функция attachInterruptArg() используется для подключения прерывания к определённому выводу с аргументами – это означает, что вы можете передавать аргументы в функцию обратного вызова.

attachInterruptArg(uint8_t pin, void callback_function, void * arg, int mode);
  • pin определяет номер GPIO.

  • callback_function устанавливает функцию обратного вызова.

  • arg указатель на аргументы прерывания.

  • mode задаёт режим прерывания.

Отключение прерывания от вывода GPIO

Когда вы хотите, чтобы ESP32 больше не отслеживал вывод, вы можете вызвать функцию detachInterrupt() и передать в качестве аргумента номер GPIO.

detachInterrupt(digitalPinToInterrupt(interruptPin));

Пример 1: ESP32 – Обнаружение нажатия кнопки с помощью прерывания

В этом разделе мы создадим простой пример для обнаружения нажатия кнопки с помощью прерывания. В данном примере мы используем тактовую кнопку, но вы также можете использовать датчик с пороговым значением, например PIR-датчик движения (мы рассмотрим это далее в руководстве).

ESP32 с тактовой кнопкой на макетной плате

Необходимые компоненты

Вы можете использовать ссылки выше или перейти непосредственно на MakerAdvisor.com/tools для поиска всех компонентов для ваших проектов по лучшей цене!

Схема подключения

Для этого примера вам нужно подключить тактовую кнопку к GPIO 18. Мы не будем использовать внешние резисторы для кнопки, поскольку мы задействуем внутренние подтягивающие резисторы ESP32. Подключите один вывод кнопки к GPIO 18, а другой – к GND, как показано на схеме ниже.

ESP32 подключение тактовой кнопки -- принципиальная схема

В качестве альтернативы вы можете использовать любой другой подходящий GPIO, если измените код соответствующим образом. Обязательно ознакомьтесь с руководством по распиновке ESP32.

Код

Загрузите следующий код на ESP32. Он обнаруживает нажатия кнопки и выводит количество нажатий в Serial Monitor.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-gpio-interrupts-arduino/
*********/
#include <Arduino.h>

// Global variables for the button
const uint8_t buttonPin = 18;
volatile int32_t counter = 0;
volatile bool pressed = false;

// Interrupt Service Routine (ISR)
void ARDUINO_ISR_ATTR buttonISR() {
  counter++;
  pressed = true;
}

void setup() {
  Serial.begin(115200);
  pinMode(buttonPin, INPUT_PULLUP);
  attachInterrupt(buttonPin, buttonISR, RISING);
  Serial.println("Press the button on GPIO 18.");
}

void loop() {
  if (pressed) {
    Serial.print("Button pressed ");
    Serial.print(counter);
    Serial.println(" times.");
    pressed = false;
  }
  delay(10);
}

Просмотреть исходный код

Как работает код?

Давайте рассмотрим, как работает код, чтобы вы поняли, как использовать прерывания в своём коде.

Глобальные переменные кнопки

Сначала определяем глобальные переменные для кнопки. Мы определяем buttonPin как const-переменную, поскольку она не будет меняться в ходе выполнения кода. Переменная counter будет подсчитывать количество нажатий кнопки. Наконец, pressed – это булева переменная, которая указывает, была ли кнопка нажата или нет. Начальное значение – false.

// Global variables for the button
const uint8_t buttonPin = 18;
volatile uint32_t counter = 0;
volatile bool pressed = false;

Обратите внимание, что counter и pressed объявлены как volatile, поскольку они используются внутри ISR, а также в остальном коде (в loop()).

Процедура обслуживания прерывания

Определяем процедуру обслуживания прерывания – функцию обратного вызова, которая будет выполняться при срабатывании прерывания. В данном случае функция называется buttonISR() и имеет атрибут ARDUINO_ISR_ATTR, чтобы функция выполнялась в IRAM ESP32, как мы видели ранее.

void ARDUINO_ISR_ATTR buttonISR() {

В этой функции мы увеличиваем переменную counter и устанавливаем переменную pressed в true, указывая, что произошло нажатие кнопки.

counter++;
pressed = true;

setup()

В setup() инициализируем Serial Monitor для отладки.

void setup() {
  Serial.begin(115200);

Настройка прерывания

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

pinMode(buttonPin, INPUT_PULLUP);

Настройте вывод как прерывание и назначьте ему функцию обратного вызова в режиме RISING (это означает, что прерывание будет срабатывать, когда вывод переходит из LOW в HIGH – когда нажимается кнопка).

attachInterrupt(buttonPin, buttonISR, RISING);

loop()

В loop() мы проверяем, была ли нажата кнопка. Если она была нажата, мы выводим количество нажатий на данный момент.

void loop() {
  if (pressed) {
    Serial.print("Button pressed ");
    Serial.print(counter);
    Serial.println(" times.");

В конце мы снова устанавливаем значение false, чтобы можно было вывести информацию при новом нажатии.

pressed = false;

Тестирование примера

Загрузите код на ESP32. После загрузки откройте Serial Monitor на скорости 115200 бод. Нажмите кнопку RST на ESP32, чтобы начать выполнение кода.

Нажмите тактовую кнопку.

Нажатие тактовой кнопки, подключённой к ESP32

Наблюдайте за увеличением количества нажатий в Serial Monitor.

ESP32: обнаружение нажатий кнопки с прерыванием без подавления дребезга -- результаты в Serial Monitor

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

Дребезг тактовой кнопки

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

Мы покажем вам, как добавить подавление дребезга в код для предотвращения ложных нажатий кнопки.

Обнаружение нажатия кнопки с прерыванием (с подавлением дребезга)

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

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-gpio-interrupts-arduino/
*********/
#include <Arduino.h>

// Global variables for the button
const uint8_t buttonPin = 18;
volatile uint32_t counter = 0;
volatile bool pressed = false;

// For debouncing the pushbutton
const unsigned long DEBOUNCE_DELAY = 50;  // in milliseconds
volatile unsigned long lastPressTime = 0;

// Interrupt Service Routine (ISR)
void ARDUINO_ISR_ATTR buttonISR() {
  unsigned long now = millis();
  if (now - lastPressTime > DEBOUNCE_DELAY) {
    counter++;
    pressed = true;
  }
  lastPressTime = now;
}

void setup() {
  Serial.begin(115200);
  pinMode(buttonPin, INPUT_PULLUP);
  attachInterrupt(buttonPin, buttonISR, HIGH);
  Serial.println("Press the button on GPIO 18.");
}

void loop() {
  if (pressed) {
    Serial.print("Button pressed ");
    Serial.print(counter);
    Serial.println(" times.");
    pressed = false;
  }
  delay(10);
}

Просмотреть исходный код

В этом коде мы добавили переменные для обработки подавления дребезга кнопки.

// For debouncing the pushbutton
const unsigned long DEBOUNCE_DELAY = 50;  // in milliseconds
volatile unsigned long lastPressTime = 0;

DEBOUNCE_DELAY определяет минимальное время между нажатиями кнопки для регистрации действительного события (предотвращает ложные срабатывания из-за механического дребезга). 50 миллисекунд должно быть достаточно. Если ложные срабатывания продолжаются, увеличьте время подавления дребезга.

lastPressTime сохраняет время последнего нажатия кнопки.

В функции buttonISR(), прежде чем считать нажатие кнопки действительным, мы сначала проверяем, прошло ли не менее 50 миллисекунд с последнего нажатия. Мы получаем время, прошедшее с начала работы программы, с помощью millis() (в миллисекундах) и сохраняем его в переменной now.

unsigned long now = millis();
if (now - lastPressTime > DEBOUNCE_DELAY) {

Если да, мы считаем, что произошло действительное нажатие кнопки, и увеличиваем переменную counter, а также устанавливаем переменную pressed в true.

counter++;
pressed = true;

После этого обновляем lastPressTime текущим временем.

lastPressTime = now;

Тестирование примера

Теперь, если вы протестируете этот новый пример, вы увидите, что ложные срабатывания больше не появляются (если они всё же есть, увеличьте задержку подавления дребезга в коде).

ESP32: обнаружение нажатий кнопки с прерыванием с подавлением дребезга -- результаты в Serial Monitor

Пример 2: ESP32 с PIR-датчиком – Обнаружение движения с помощью прерывания

Как мы упоминали ранее, вы также можете обнаруживать события, вызванные датчиками, которые изменяют состояние выхода при достижении определённого порога, например PIR-датчик движения.

PIR-датчики движения: AM312 и HC-SR501

Два наиболее популярных датчика движения, используемых любителями электроники: мини PIR-датчик движения (AM312) и PIR-датчик движения (HC-SR501).

Эти датчики выдают сигнал HIGH при обнаружении движения или сигнал LOW при отсутствии движения. Цифровой выход PIR-датчика может считываться выводом GPIO ESP32, что позволяет программировать определённые действия в зависимости от обнаруженного статуса движения.

Обзор примера

Мы создадим простой пример, который будет включать светодиод при обнаружении движения. После изучения его работы тот же принцип можно применить к полезным приложениям, таким как отправка электронного письма или запуск сигнализации.

Обзор примера ESP32 с PIR-датчиком движения

Вот как работает пример:

  • Датчик обнаруживает движение.

  • ESP32 обнаруживает это событие.

  • Выводит в Serial Monitor сообщение об обнаружении движения.

  • Включает светодиод на 20 секунд.

  • В течение этих 20 секунд мы не выводим ничего другого в Serial Monitor.

  • По истечении 20 секунд, если движение не было обнаружено тем временем, мы выключаем светодиод и выводим в Serial Monitor сообщение о том, что движение прекратилось.

Необходимые компоненты

Для этого примера вам понадобятся следующие компоненты:

Схема подключения

PIR-датчики имеют выводы GND, VCC и линию данных. Подключите GND к GND ESP32, VCC к 3.3V, а линию данных к свободному GPIO. Мы будем использовать следующие выводы:

PIR-датчик

GND

VCC

Data

ESP32

GND

3.3V

GPIO 27

В этой схеме светодиод подключён к GPIO 26, а вывод данных PIR-датчика движения – к GPIO 27. Мы будем использовать мини PIR-датчик движения AM312, который работает от 3.3V. На следующем рисунке показана распиновка PIR-датчика AM312.

Распиновка PIR-датчика движения AM312

Вы можете использовать следующую схему в качестве справки для подключения вашей цепи.

ESP32 подключение PIR-датчика движения и светодиода

Мини PIR-датчик движения AM312, используемый в этом проекте, работает от 3.3V. Однако, если вы используете другой PIR-датчик, например HC-SR501, он работает от 5V. Вы можете либо модифицировать его для работы от 3.3V, либо просто запитать его через вывод Vin.

Код

После сборки схемы загрузите следующий код на ESP32.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-pir-motion-sensor-interrupts-timers/
  ESP32 GPIO Interrupts with Arduino IDE: https://RandomNerdTutorials.com/esp32-gpio-interrupts-arduino/
*********/
#include <Arduino.h>

// Set GPIOs for LED and PIR Motion Sensor
const uint8_t led = 26;
const uint8_t motionSensor = 27;

// Timer: Auxiliary variables
unsigned long now;
volatile unsigned long lastTrigger = 0;
volatile bool startTimer = false;

bool printMotion = false;

const unsigned long timeSeconds = 20 * 1000UL;  //20 seconds in milliseconds

void ARDUINO_ISR_ATTR motionISR() {
  lastTrigger = millis();
  startTimer = true;
}

void setup() {
  Serial.begin(115200);
  pinMode(motionSensor, INPUT_PULLUP);
  attachInterrupt(motionSensor, motionISR, RISING);

  // Set LED to LOW
  pinMode(led, OUTPUT);
  digitalWrite(led, LOW);
}

void loop() {
  now = millis();

// Turn LED on immediately on new trigger
  if (startTimer && !printMotion) {
    digitalWrite(led, HIGH);
    Serial.println("MOTION DETECTED!!!");
    printMotion = true;
  }

// Turn off the LED after timeout
  if (startTimer && (now - lastTrigger > timeSeconds)) {
    Serial.println("Motion stopped...");
    digitalWrite(led, LOW);
    startTimer = false;
    printMotion = false;
  }
}

Просмотреть исходный код

Как работает код?

Давайте кратко рассмотрим код, чтобы лучше понять, как он работает.

Определение переменных

Начинаем с определения выводов для светодиода и PIR-датчика движения. Измените значения, если используете другие выводы:

const uint8_t led = 26;
const uint8_t motionSensor = 27;

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

unsigned long now;
volatile unsigned long lastTrigger = 0;
volatile bool startTimer = false;

У нас также есть ещё одна переменная для отслеживания того, был ли уже выведен текст Motion Detected в Serial Monitor.

bool printMotion = false;

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

const unsigned long timeSeconds = 20 * 1000UL;  //20 seconds in milliseconds

motionISR()

Функция motionISR() будет выполняться при обнаружении движения. Мы сохраняем текущее время в переменной lastTrigger для отслеживания момента обнаружения движения и устанавливаем переменную startTimer в true, чтобы указать, что пора запустить таймер включения светодиода.

void ARDUINO_ISR_ATTR motionISR() {
  lastTrigger = millis();
  startTimer = true;
}

Затем мы обрабатываем эти переменные в loop() для выполнения нужных задач.

setup()

В setup() настраиваем датчик движения как прерывание в режиме RISING (при обнаружении движения датчик устанавливает свой выходной вывод в HIGH).

pinMode(motionSensor, INPUT_PULLUP);
attachInterrupt(motionSensor, motionISR, RISING);

И настраиваем светодиод как OUTPUT и устанавливаем его в LOW.

// Set LED to LOW
pinMode(led, OUTPUT);
digitalWrite(led, LOW);

loop() – Включение светодиода

В loop() мы постоянно получаем текущее время и сохраняем его в переменной now.

now = millis();

Затем мы проверяем, запущен ли таймер светодиода и не было ли уже выведено сообщение о движении. Если эти условия выполнены, мы включаем светодиод, выводим сообщение в Serial Monitor и устанавливаем переменную printMotion в true, поскольку сообщение Motion Detected уже выведено в Serial Monitor.

if (startTimer && !printMotion) {
  digitalWrite(led, HIGH);
  Serial.println("MOTION DETECTED!!!");
  printMotion = true;
}

loop() – Выключение светодиода

В loop() мы также проверяем, прошло ли 20 секунд с момента последнего срабатывания после запуска startTimer. Если 20 секунд прошло с последнего срабатывания, мы выключаем светодиод и устанавливаем переменные startTimer и printMotion в false.

if (startTimer && (now - lastTrigger > timeSeconds)) {
  Serial.println("Motion stopped...");
  digitalWrite(led, LOW);
  startTimer = false;
  printMotion = false;
}

Тестирование примера

Загрузите код на плату ESP32. Откройте Serial Monitor на скорости 115200 бод и нажмите кнопку RST на ESP32, чтобы начать выполнение кода.

Проведите рукой перед PIR-датчиком.

Движение рукой перед PIR-датчиком движения, подключённым к ESP32

Светодиод должен включиться, и в Serial Monitor появится сообщение «MOTION DETECTED!!!».

ESP32 PIR-датчик движения с прерываниями -- сообщения в Serial Monitor

Через 20 секунд светодиод должен выключиться (если за это время движение не было обнаружено).

ESP32 на макетной плате со светодиодом и PIR-датчиком движения

Теперь, когда вы понимаете, как использовать прерывания для обнаружения движения с помощью PIR-датчика, вы можете легко изменить код для выполнения любых полезных задач вместо управления светодиодом. Вы можете, например, отправлять уведомления на электронную почту или смартфон, чтобы сообщить об обнаружении движения.

У нас есть руководство с семью различными способами отправки уведомлений с ESP32, которое вы можете изучить:

Заключение

В этом руководстве мы рассмотрели, как использовать прерывания с ESP32 для обнаружения изменений на его GPIO. Вместо того чтобы постоянно опрашивать состояние GPIO, мы можем использовать прерывания. Наш код будет выполняться в обычном режиме, а при обнаружении прерывания будет запускаться функция обратного вызова (ISR).

Мы рассмотрели пример с использованием тактовой кнопки и ещё один с использованием PIR-датчика движения, но этот подход можно применить ко многим другим датчикам, имеющим выходной вывод, который изменяет своё состояние при достижении определённого порога. Для подробного руководства по использованию PIR-датчика движения с ESP32 вы можете обратиться к этому руководству:

Для руководств и справочников по другим датчикам вы можете ознакомиться с нашей подборкой:

Если вы также хотите узнать, как использовать прерывания с ESP32 на прошивке MicroPython, ознакомьтесь с этими руководствами:

Надеемся, это руководство было для вас полезным. Чтобы узнать больше об ESP32, ознакомьтесь с нашими ресурсами:


Источник: ESP32 GPIO Interrupts with Arduino IDE