Прерывания и таймеры ESP8266 с Arduino IDE (NodeMCU)

В этом руководстве вы узнаете, как использовать прерывания и таймеры с ESP8266 NodeMCU в Arduino IDE. Прерывания позволяют обнаруживать изменения состояния GPIO без необходимости постоянно проверять его текущее значение. С помощью прерываний, когда обнаруживается изменение, срабатывает событие (вызывается функция).

Прерывания и таймеры ESP8266 с Arduino IDE (NodeMCU)

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

Чтобы создать прерывание, вызовите attachInterrupt() и передайте в качестве аргументов GPIO-пин прерывания, ISR (функцию, которая будет вызвана) и режим. Функция ISR должна иметь объявленный атрибут ICACHE_RAM_ATTR. Режим может быть CHANGE, RISING или FALLING.

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

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

Знакомство с прерываниями ESP8266

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

С помощью прерываний вам не нужно постоянно проверять текущее значение на пине. Когда обнаруживается изменение, срабатывает событие — вызывается функция. Эта функция называется процедурой обработки прерывания (ISR — Interrupt Service Routine).

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

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

Это особенно полезно для запуска действия при обнаружении движения или при нажатии кнопки без необходимости постоянно проверять её состояние.

Функция attachInterrupt()

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

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

GPIO-пин прерывания

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

digitalPinToInterrupt(14)

ESP8266 поддерживает прерывания на любом GPIO, кроме GPIO16.

ISR

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

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

Лучший подход — сигнализировать основному коду о том, что прерывание произошло, используя глобальную переменную, и внутри loop() проверять и сбрасывать этот флаг, а затем выполнять код.

ISR-функции должны иметь ICACHE_RAM_ATTR перед определением функции для выполнения кода прерывания в оперативной памяти (RAM).

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

Третий аргумент — это режим, и существует 3 различных режима:

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

  • FALLING: для срабатывания при переходе пина с HIGH на LOW;

  • RISING: для срабатывания при переходе пина с LOW на HIGH.

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

Знакомство с таймерами ESP8266

Таймеры ESP8266 NodeMCU

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

delay() vs millis()

Функция delay() принимает одно целое число в качестве аргумента. Это число представляет время в миллисекундах, которое программа должна ждать, прежде чем перейти к следующей строке кода.

delay(время в миллисекундах);

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

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

millis();

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

Мигание светодиодом с помощью millis() (без delay)

Если вы не знакомы с функцией millis(), мы рекомендуем прочитать этот раздел. Если вы уже знакомы с таймерами, вы можете перейти к проекту с PIR-датчиком движения.

Следующий фрагмент кода показывает, как можно использовать функцию 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(), и ваш код всё равно будет мигать светодиодом каждую секунду.

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

ESP8266 мигание встроенным светодиодом (GPIO 2) с использованием millis()

ESP8266 NodeMCU с PIR-датчиком движения

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

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

Вот список компонентов, необходимых для выполнения этого руководства:

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

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

Соберите PIR-датчик движения и светодиод на макетной плате с ESP8266. Мы подключим светодиод к GPIO 12 (D6), а вывод данных PIR-датчика движения — к GPIO 14 (D5).

ESP8266 NodeMCU прерывания и таймеры с PIR-датчиком движения — схема подключения

Рекомендуемый материал: Справочное руководство по распиновке ESP8266

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

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

Распиновка PIR-датчика движения AM312: GND, Data, 3.3V

Код

После сборки схемы, как показано на схеме подключения, скопируйте приведённый ниже код в 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 = 12;
const int motionSensor = 14;

// 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
ICACHE_RAM_ATTR void 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.

const int led = 12;
const int motionSensor = 14;

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

unsigned long now = millis();
unsigned long lastTrigger = 0;
boolean startTimer = false;

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

setup()

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

Serial.begin(115200);

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

pinMode(motionSensor, INPUT_PULLUP);

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

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

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

Светодиод является выходом (OUTPUT), начальное состояние которого — LOW.

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

loop()

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

now = millis();

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

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

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

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

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

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

Демонстрация

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

Откройте монитор порта на скорости 115200 бод.

Arduino IDE открытие монитора порта на скорости 115200

Проведите рукой перед PIR-датчиком. Светодиод должен загореться, и в мониторе порта появится сообщение «MOTION DETECTED!!!». Через 10 секунд светодиод должен выключиться.

Демонстрация прерываний и таймеров ESP8266 NodeMCU с Arduino IDE

Подведение итогов

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

Мы надеемся, что это руководство оказалось для вас полезным. У нас есть другие руководства по работе с прерываниями с использованием MicroPython и ESP32:

Узнайте больше о плате ESP8266 из наших материалов:

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