Работа с ИК-датчиком движения с использованием прерываний и таймеров на ESP32

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

На этом примере мы также рассмотрим две важные концепции: прерывания и таймеры.

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

  • Плата ESP32
  • PIR датчик движения (AM312) или PIR датчик движения (HC-SR501)
  • 5 мм светодиод
  • Резистор 330 Ом
  • Перемычки
  • Макетная плата

 

Прерывания

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

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

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

attachInterrupt(digitalPinToInterrupt(GPIO), function, mode);

GPIO Interrupt

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

digitalPinToInterrupt(27)

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

Функция для запуска

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

Режим

Третий аргумент - это режим. Есть 5 разных режимов:

  • LOW: для запуска прерывания всякий раз, когда вывод LOW;
  • HIGH: для запуска прерывания всякий раз, когда вывод HIGH;
  • CHANGE: для запуска прерывания всякий раз, когда вывод меняет значение - например, с HIGH на LOW или LOW на HIGH;
  • FALLING: когда пин переходит из HIGH в LOW;
  • RISING: запускается, когда пин переходит с LOW на HIGH.

В этом примере будет использоваться режим RISING, потому что, когда датчик движения PIR обнаруживает движение, GPIO, к которому он подключен, переходит с LOW на HIGH.

 

Таймеры

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

Функция delay()

Вы должны быть знакомы с функцией delay(), поскольку она широко используется. Эта функция довольно проста в использовании. Он принимает одно целое число в качестве аргумента. Это число представляет время в миллисекундах, в течение которого программа должна ожидать перехода к следующей строке кода.

delay(time in milliseconds)

Когда вы делаете задержку (1000), ваша программа останавливается на этой линии на 1 секунду.

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

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

Функция millis()

Используя функцию millis(), вы можете вернуть количество миллисекунд, прошедших с момента первого запуска программы.

millis()

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

Мигающий светодиод с millis()

В следующем фрагменте кода показано, как можно использовать функцию millis() для создания проекта мигающего светодиода. Светодиод включается на 1000 миллисекунд, а затем выключается.

/*********
  Rui Santos
  Complete project details at https://randomnerdtutorials.com  
*********/

// constants won't change. Used here to set a pin number :
const int ledPin =  26;      // the number of the LED pin

// Variables will change :
int ledState = LOW;             // ledState used to set the LED

// Generally, you should use "unsigned long" for variables that hold time
// The value will quickly become too large for an int to store
unsigned long previousMillis = 0;        // will store last time LED was updated

// constants won't change :
const long interval = 1000;           // interval at which to blink (milliseconds)

void setup() {
  // set the digital pin as output:
  pinMode(ledPin, OUTPUT);
}

void loop() {
  // here is where you'd put code that needs to be running all the time.

  // check to see if it's time to blink the LED; that is, if the
  // difference between the current time and last time you blinked
  // the LED is bigger than the interval at which you want to
  // blink the LED.
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    // save the last time you blinked the LED
    previousMillis = currentMillis;

    // if the LED is off turn it on and vice-versa:
    if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }

    // set the LED with the ledState of the variable:
    digitalWrite(ledPin, ledState);
  }
}

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

Давайте внимательнее посмотрим на этот скетч, который работает без функции delay() (вместо него используется функция millis()).

По сути, этот код вычитает предыдущее записанное время (previousMillis) из текущего времени (currentMillis). Если остаток превышает интервал (в данном случае 1000 миллисекунд), программа обновляет переменную previousMillis до текущего времени и либо включает, либо выключает светодиод.

if (currentMillis - previousMillis >= interval) {
  // save the last time you blinked the LED
  previousMillis = currentMillis;
  (...)

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

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

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

ESP32 с ИК-датчиком движения

Схема

Схема, которую мы построим, проста в сборке, мы будем использовать светодиод с резистором. Светодиод подключен к GPIO 26. Мы будем использовать PIR-датчик движения Mini AM312, работающий при напряжении 3,3 В. Он будет подключен к GPIO 27. 

Важно: датчик движения Mini AM312, используемый в этом проекте, работает при напряжении 3,3 В. Однако, если вы используете другой PIR-датчик движения, такой как HC-SR501, он работает при напряжении 5 В. Вы можете либо изменить его, чтобы он работал при напряжении 3,3 В, либо просто включить его с помощью Vin-контакта.

На следующем рисунке показана распиновка датчика движения AM312 PIR.

Загрузка кода

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

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

/*********
  Rui Santos
  Complete project details at https://randomnerdtutorials.com  
*********/

#define timeSeconds 10

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

// Timer: Auxiliary variables
unsigned long now = millis();
unsigned long lastTrigger = 0;
boolean startTimer = false;

// Checks if motion was detected, sets LED HIGH and starts a timer
void IRAM_ATTR detectsMovement() {
  Serial.println("MOTION DETECTED!!!");
  digitalWrite(led, HIGH);
  startTimer = true;
  lastTrigger = millis();
}

void setup() {
  // Serial port for debugging purposes
  Serial.begin(115200);
  
  // PIR Motion Sensor mode INPUT_PULLUP
  pinMode(motionSensor, INPUT_PULLUP);
  // Set motionSensor pin as interrupt, assign interrupt function and set RISING mode
  attachInterrupt(digitalPinToInterrupt(motionSensor), detectsMovement, RISING);

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

void loop() {
  // Current time
  now = millis();
  // Turn off the LED after the number of seconds defined in the timeSeconds variable
  if(startTimer && (now - lastTrigger > (timeSeconds*1000))) {
    Serial.println("Motion stopped...");
    digitalWrite(led, LOW);
    startTimer = false;
  }
}

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

Давайте посмотрим на код. Начнём с назначения двух выводов GPIO переменным led и motionSensor.

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

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

// Timer: Auxiliar variables
long now = millis();
long lastTrigger = 0;
boolean startTimer = false;

Переменная now содержит текущее время. Переменная lastTrigger содержит время, когда ИК-датчик обнаруживает движение. StartTimer - это логическая переменная, которая запускает таймер при обнаружении движения.

setup()

В setup() начните с инициализации последовательного порта со скоростью 115200 бод.

Serial.begin(115200);

Установите датчик движения PIR в качестве INPUT PULLUP.

pinMode(motionSensor, INPUT_PULLUP);

Чтобы установить вывод датчика PIR как прерывание, используйте функцию attachInterrupt(), как описано ранее.

attachInterrupt(digitalPinToInterrupt(motionSensor), detectsMovement, RISING);

Пин, который будет обнаруживать движение, это GPIO 27, и он вызовет функцию detectsMovement () в режиме RISING.

Светодиод является ВЫХОДОМ, состояние которого начинается с НИЗКОГО.

pinMode(led, OUTPUT);
digitalWrite(led, LOW);

loop()

Функция loop() постоянно выполняется снова и снова. В каждом цикле переменная now обновляется текущим временем.

now = millis();

Больше ничего не делается в loop().

Но когда обнаруживается движение, вызывается функция detectsMovement(), потому что мы предварительно установили прерывание в setup().

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

void IRAM_ATTR detectsMovement() {
  Serial.println("MOTION DETECTED!!!");
  digitalWrite(led, HIGH);
  startTimer = true;
  lastTrigger = millis();
}

Примечание. IRAM_ATTR используется для запуска кода прерывания в ОЗУ, в противном случае код сохраняется во флэш-памяти и работает медленнее.

После этого шага код возвращается в loop().

На этот раз переменная startTimer имеет значение true. Итак, когда время, определенное в секундах, прошло (с момента обнаружения движения), следующий оператор if будет истинным.

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

Сообщение «Движение остановлено…» будет напечатано на последовательном мониторе, светодиод выключен, а для переменной startTimer установлено значение false.

Проверка

Загрузите код на свою плату ESP32. Убедитесь, что вы выбрали правильную плату и COM-порт.

Откройте последовательный монитор со скоростью 115200 бод.

Подведите руку к ИК-датчику. Светодиод должен включиться, и на последовательном мониторе будет напечатано сообщение «ДВИЖЕНИЕ ОБНАРУЖЕНО !!!». Через 10 секунд светодиод должен погаснуть.

Завершение

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

Спасибо за прочтение.