ESP32 с PIR-датчиком движения: прерывания и таймеры (Arduino IDE)
Узнайте, как использовать PIR-датчик движения (пассивный инфракрасный) с ESP32, запрограммированным в Arduino IDE, для обнаружения движения. Мы создадим простой пример, чтобы познакомить вас с концепциями таймеров и прерываний.
В этом примере при обнаружении движения (срабатывает прерывание) ESP32 запускает таймер и включает светодиод на заданное количество секунд. Когда таймер завершает отсчёт, светодиод автоматически выключается.
Обновлено 27 октября 2025 г.
Используете MicroPython? Ознакомьтесь с этим руководством: MicroPython: Interrupts with ESP32 and ESP8266.
Необходимые условия
Прежде чем приступить к этому руководству, у вас должны быть установлены платы ESP32 в Arduino IDE. Следуйте следующему руководству для установки ESP32 в Arduino IDE, если вы ещё этого не сделали.
Необходимые компоненты
Для выполнения этого руководства вам понадобятся следующие компоненты:
1x Мини PIR-датчик движения (AM312) или PIR-датчик движения (HC-SR501)
1x Резистор 330 Ом или аналогичного номинала
Вы можете использовать ссылки выше или перейти непосредственно на MakerAdvisor.com/tools для поиска всех компонентов для ваших проектов по лучшей цене!
Знакомство с PIR-датчиком движения
PIR-датчик движения обнаруживает изменения инфракрасного излучения в своём поле зрения, вызванные движением. Это делает его идеальным для обнаружения людей или животных, поскольку он реагирует на живые существа (или объекты, излучающие тепло), которые перемещаются в пределах его дальности действия, но не на неподвижные объекты.
Вы можете запрограммировать ESP32 реагировать на изменения инфракрасного излучения, вызывая событие, такое как включение света, подача сигнала тревоги, отправка уведомления или любое другое действие. В этом руководстве мы будем выводить сообщение в Serial Monitor и включать светодиод на заданное количество секунд.
Мини AM312 PIR-датчик движения
Существуют различные модули PIR-датчиков движения, но все они работают по схожему принципу. Обычно они имеют вывод питания, GND и вывод данных.
PIR-датчик движения выдаёт сигнал HIGH на выводе данных при обнаружении движения или сигнал LOW, если движение не обнаружено. Обычно они имеют только три вывода: VCC, GND и Data.
Важной концепцией при работе с PIR-датчиками движения является время удержания (время сброса или задержка датчика) — это продолжительность, в течение которой выход PIR-датчика движения остаётся в состоянии HIGH после обнаружения движения, прежде чем вернуться в состояние LOW.
Некоторые модели PIR-датчиков, такие как HC-SR501, могут иметь два потенциометра (те два оранжевых потенциометра на рисунке ниже) для настройки чувствительности и задержки датчика:
HC-SR501 PIR-датчик движения
Потенциометр чувствительности: регулирует дальность обнаружения датчика. Вращение по часовой стрелке увеличивает чувствительность, против часовой стрелки — уменьшает.
Потенциометр временной задержки: управляет тем, как долго датчик остаётся в сработанном состоянии после обнаружения движения. Вращение по часовой стрелке увеличивает задержку, против часовой стрелки — уменьшает.
Знакомство с прерываниями
Для запуска события с помощью PIR-датчика движения используются прерывания. Прерывания полезны для автоматического выполнения действий в программах микроконтроллеров и могут помочь решить проблемы с таймингом.
С прерываниями вам не нужно постоянно проверять текущее значение вывода. При использовании прерываний, когда обнаруживается изменение, вызывается событие (вызывается функция).
Использование прерываний с ESP32
Для установки прерывания в Arduino IDE используется функция attachInterrupt(), которая принимает в качестве аргументов: номер GPIO, имя вызываемой функции и режим:
attachInterrupt(GPIO), callback_function, mode);
Эта инструкция должна быть добавлена в setup() вашего кода Arduino.
Давайте рассмотрим аргументы, которые нужно передать этой функции.
GPIO-прерывание
Первый аргумент функции attachInterrupt() — это номер GPIO, на котором мы будем обнаруживать изменение. Например, если вы хотите использовать GPIO 27 как прерывание, вы можете использовать:
digitalPinToInterrupt(27)
На плате ESP32 все выводы, которые могут работать как входы, могут быть настроены как прерывания.
Например, в случае платы ESP32 DOIT V1, все выводы, выделенные красным прямоугольником на следующем рисунке, могут быть настроены как выводы прерываний. В этом примере мы будем использовать GPIO 27 в качестве прерывания, подключённого к PIR-датчику движения.
Используете ESP32S3? Ознакомьтесь с руководством по распиновке: ESP32-S3 DevKitC Pinout Reference Guide: GPIOs Explained.
Функция обратного вызова (ISR)
Второй аргумент функции attachInterrupt() — это имя функции, которая будет вызвана при срабатывании прерывания. Эта функция также называется подпрограммой обработки прерывания (ISR).
Теперь есть несколько важных правил, о которых вы должны знать при определении вашей ISR (функции обратного вызова).
ISR не должна возвращать ничего.
ISR должны быть как можно короче и быстрее, потому что они останавливают нормальное выполнение кода.
Они должны иметь атрибут ARDUINO_ISR_ATTR, чтобы выполняться во внутренней RAM ESP32, а не во Flash. Доступ к IRAM намного быстрее, что критически важно для надёжной работы ISR без проблем с таймингом или сбоев во время прерываний.
Переменные, которые используются внутри ISR и в остальном коде, должны предпочтительно быть volatile. Это предотвращает кеширование значений компилятором в регистрах (и пропуск обращений к памяти), поэтому чтения/записи всегда обращаются к фактической ячейке памяти и отражают неожиданные изменения, вызванные прерыванием.
Вот пример ISR, чтобы вы могли проверить её синтаксис:
void ARDUINO_ISR_ATTR my_callback() {
// Any code you want to run
}
Ещё один важный момент об ISR заключается в том, что вы должны делать их код максимально быстрым и простым и избегать таких вещей, как сложные операции, запись в Serial Monitor или использование delay(). Вместо этого вы должны использовать флаг или счётчик для указания того, что прерывание произошло, а затем обрабатывать всё, что нужно сделать, в основном коде или в секции loop().
Режим
Третий аргумент — это режим. Существует 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)
Когда вы вызываете delay(1000), ваша программа останавливается на этой строке на 1 секунду.
delay() — это блокирующая функция. Блокирующие функции не позволяют программе выполнять что-либо другое, пока конкретная задача не будет завершена. Если вам нужно, чтобы несколько задач выполнялись одновременно, вы не можете использовать delay().
Для большинства проектов следует избегать использования delay() и использовать таймеры вместо этого.
Функция millis()
Использование функции millis() предпочтительнее, чем delay(). Функция 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 с PIR-датчиком движения: обнаружение движения
После понимания этих концепций: прерываний и таймеров, давайте продолжим с проектом.
Мы создадим простой пример, который включит светодиод при обнаружении движения. Изучив, как это работает, тот же принцип мышления можно применить к полезным приложениям, таким как отправка электронного письма или срабатывание сигнализации.
Вот как работает этот пример:
Датчик обнаруживает движение.
ESP32 обнаруживает это событие.
Выводит в Serial Monitor сообщение о том, что движение обнаружено.
Включает светодиод на 20 секунд.
В течение этих 20 секунд ничего не выводится в Serial Monitor.
По истечении 20 секунд, если движение не было обнаружено, светодиод выключается, и в Serial Monitor выводится сообщение о том, что движение прекратилось.
Схема подключения
Для этого примера вам нужно подключить PIR-датчик движения и светодиод к вашей плате.
Светодиод подключён к GPIO 26.
Мы будем использовать мини PIR-датчик движения AM312, который работает от 3.3 В. Он будет подключён к GPIO 27. Вы можете следовать следующей принципиальной схеме.
Важно: мини PIR-датчик движения AM312, используемый в этом проекте, работает от 3.3 В. Однако, если вы используете другой PIR-датчик движения, такой как HC-SR501, он работает от 5 В. Вы можете либо модифицировать его для работы от 3.3 В, либо просто запитать его через вывод Vin.
Код — ESP32 с PIR-датчиком движения: обнаружение движения
После подключения схемы, как показано на принципиальной схеме, скопируйте предоставленный код в вашу Arduino IDE.
Вы можете загрузить код как есть, или изменить количество секунд, в течение которых светодиод горит после обнаружения движения. Просто измените переменную timeSeconds на нужное количество секунд.
/*********
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;
}
}
Примечание: если у вас возникли проблемы с загрузкой кода на ESP32, ознакомьтесь с руководством по устранению неполадок ESP32.
Как работает код?
Давайте кратко рассмотрим код, чтобы лучше понять, как он работает.
Определение переменных
Мы начинаем с определения выводов для светодиода и 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;
}
Демонстрация
Загрузите код на вашу плату ESP32. Убедитесь, что вы выбрали правильную плату и COM-порт.
Откройте Serial Monitor на скорости 115200 бод. Нажмите кнопку RST на ESP32, чтобы запустить выполнение кода.
Проведите рукой перед PIR-датчиком.
Светодиод должен включиться, и в Serial Monitor появится сообщение «MOTION DETECTED!!!».
Через 20 секунд светодиод должен выключиться (если движение не было обнаружено за это время).
Теперь, когда вы понимаете, как использовать прерывания для обнаружения движения с помощью PIR-датчика движения, вы можете легко адаптировать код для выполнения любых полезных задач вместо управления светодиодом.
Вы можете, например, отправлять уведомления на вашу электронную почту или смартфон, чтобы указать, что было обнаружено движение. У нас есть руководство с семью различными способами отправки уведомлений с помощью ESP32, которое вы можете изучить:
Смотрите видеоурок и демонстрацию проекта
Это руководство также доступно в видеоформате (смотрите ниже). Обратите внимание, что видеоформат может быть устаревшим.
Заключение
Подведём итоги: прерывания используются для обнаружения изменения состояния GPIO без необходимости постоянно считывать текущее значение GPIO. С прерываниями, когда обнаруживается изменение, вызывается функция. Вы также узнали, как установить простой таймер, позволяющий проверить, прошло ли заданное количество секунд, без необходимости блокировать ваш код.
У нас есть другие руководства, связанные с ESP32, которые вам также могут понравиться:
Это отрывок из нашего курса: Learn ESP32 with Arduino IDE. Если вам нравится ESP32 и вы хотите узнать больше, мы рекомендуем записаться на курс Learn ESP32 with Arduino IDE.
Спасибо за чтение.
Источник: ESP32 with PIR Motion Sensor using Interrupts and Timers (Arduino IDE)