Настройка и обработка прерываний GPIO на ESP8266 в Arduino IDE

Настройка и обработка прерываний GPIO на ESP8266 в Arduino IDE

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

Прерывания можно классифицировать на два типа.

Аппаратные прерывания – возникают в ответ на внешнее событие. Например, прерывание GPIO (при нажатии кнопки).

Программные прерывания – возникают в ответ на программную инструкцию. Например, простое прерывание по таймеру или прерывание сторожевого таймера (когда таймер достигает заданного значения).

Прерывание GPIO на ESP8266

Вы можете настроить ESP8266 на генерацию прерывания при изменении логического уровня на выводе GPIO.

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

Выводы прерываний ESP8266

Привязка прерывания к выводу GPIO

В Arduino IDE мы используем функцию attachInterrupt() для настройки прерывания на конкретном выводе. Синтаксис выглядит следующим образом.

attachInterrupt(GPIOPin, ISR, Mode);

Эта функция принимает три аргумента:

GPIOPin – задаёт вывод GPIO в качестве вывода прерывания, указывая ESP8266, какой вывод нужно отслеживать.

ISR – это имя функции, которая будет вызываться каждый раз при возникновении прерывания.

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

LOW

Срабатывает прерывание, когда вывод находится в состоянии LOW

HIGH

Срабатывает прерывание, когда вывод находится в состоянии HIGH

CHANGE

Срабатывает прерывание при любом изменении состояния вывода, с HIGH на LOW или с LOW на HIGH

FALLING

Срабатывает прерывание при переходе вывода из HIGH в LOW

RISING

Срабатывает прерывание при переходе вывода из LOW в HIGH

Функция обработки прерывания (ISR)

Функция обработки прерывания (ISR – Interrupt Service Routine) – это функция, которая вызывается каждый раз при возникновении прерывания на выводе GPIO.

Её синтаксис выглядит следующим образом.

void ICACHE_RAM_ATTR ISR() {
    Statements;
}

ISR в ESP8266 – это особые функции, которые имеют уникальные правила, отличающие их от большинства других функций.

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

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

  • Она должна иметь атрибут ICACHE_RAM_ATTR, согласно документации ESP8266.

Что такое ICACHE_RAM_ATTR?

Когда мы помечаем фрагмент кода атрибутом ICACHE_RAM_ATTR, скомпилированный код размещается во внутренней оперативной памяти (IRAM) ESP8266. В противном случае код хранится во Flash-памяти. А Flash-память на ESP8266 значительно медленнее внутренней RAM.

Если код, который мы хотим выполнить, является функцией обработки прерывания (ISR), мы, как правило, хотим выполнить его как можно быстрее. Если бы нам пришлось «ждать» загрузки ISR из Flash-памяти, всё могло бы пойти совершенно неправильно.

Подключение оборудования

Достаточно теории! Давайте рассмотрим практический пример.

Подключим кнопку к GPIO#12 (D6) на ESP8266. Вам не нужен внешний подтягивающий резистор для этого вывода, так как мы будем использовать внутреннюю подтяжку.

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

Пример кода: простое прерывание

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

Эта программа отслеживает GPIO#12 (D6) на спадающий фронт (FALLING). Другими словами, она отслеживает изменение напряжения с логического HIGH на LOW, которое происходит при нажатии кнопки. Когда это происходит, вызывается функция isr. Код внутри этой функции подсчитывает количество нажатий кнопки.

struct Button {
  const uint8_t PIN;
  uint32_t numberKeyPresses;
  bool pressed;
};

Button button1 = {D6, 0, false};

void ICACHE_RAM_ATTR isr() {
  button1.numberKeyPresses++;
  button1.pressed = true;
}

void setup() {
  Serial.begin(115200);
  pinMode(button1.PIN, INPUT_PULLUP);
  attachInterrupt(button1.PIN, isr, FALLING);
}

void loop() {
  if (button1.pressed) {
      Serial.printf("Button has been pressed %u times\n", button1.numberKeyPresses);
      button1.pressed = false;
  }
}

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

Вывод прерывания GPIO ESP8266 в мониторе порта

Объяснение кода

В начале скетча мы создаём структуру с именем Button. Эта структура имеет три члена – номер вывода, количество нажатий клавиши и состояние нажатия. К сведению, структура – это набор переменных различных типов (но логически связанных друг с другом), объединённых под одним именем.

struct Button {
  const uint8_t PIN;
  uint32_t numberKeyPresses;
  bool pressed;
};

Затем мы создаём экземпляр структуры Button и инициализируем номер вывода значением D6, количество нажатий клавиши значением 0 и состояние нажатия по умолчанию значением false.

Button button1 = {D6, 0, false};

Следующий код является функцией обработки прерывания. Как упоминалось ранее, ISR в ESP8266 должна иметь атрибут ICACHE_RAM_ATTR.

В ISR мы просто увеличиваем счётчик нажатий на 1 и устанавливаем состояние нажатия кнопки в True.

void ICACHE_RAM_ATTR isr() {
  button1.numberKeyPresses++;
  button1.pressed = true;
}

В секции setup кода мы сначала инициализируем последовательную связь с ПК, а затем включаем внутреннюю подтяжку для вывода GPIO D6.

Далее мы указываем ESP8266 отслеживать вывод D6 и вызывать функцию обработки прерывания isr при переходе вывода из HIGH в LOW, т.е. по спадающему фронту (FALLING).

void setup() {
  Serial.begin(115200);
  pinMode(button1.PIN, INPUT_PULLUP);
  attachInterrupt(button1.PIN, isr, FALLING);
}

В секции loop кода мы просто проверяем, была ли нажата кнопка, а затем выводим количество нажатий клавиши на данный момент и устанавливаем состояние нажатия кнопки в false, чтобы мы могли продолжать получать прерывания.

void loop() {
  if (button1.pressed) {
      Serial.printf("Button has been pressed %u times\n", button1.numberKeyPresses);
      button1.pressed = false;
  }
}

Борьба с дребезгом контактов

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

Проблема дребезга контактов при прерывании GPIO ESP8266

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

Сигнал дребезга контактов

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

Это чисто механическое явление, известное как дребезг контактов, подобно падению мяча – он несколько раз отскакивает, прежде чем окончательно остановиться на земле.

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

Процесс устранения дребезга контактов называется подавлением дребезга (debouncing). Существует два способа добиться этого.

  • Аппаратный способ: путём добавления соответствующего RC-фильтра для сглаживания перехода.

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

Пример кода: подавление дребезга прерывания

Здесь приведённый выше скетч переписан, чтобы продемонстрировать, как программно подавить дребезг прерывания. В этом скетче мы позволяем ISR выполняться только один раз при каждом нажатии кнопки, вместо многократного выполнения.

struct Button {
    const uint8_t PIN;
    uint32_t numberKeyPresses;
    bool pressed;
};

Button button1 = {D6, 0, false};

//variables to keep track of the timing of recent interrupts
unsigned long button_time = 0;
unsigned long last_button_time = 0;

void ICACHE_RAM_ATTR isr() {
    button_time = millis();
if (button_time - last_button_time > 250)
{
        button1.numberKeyPresses++;
        button1.pressed = true;
       last_button_time = button_time;
}
}

void setup() {
    Serial.begin(115200);
    pinMode(button1.PIN, INPUT_PULLUP);
    attachInterrupt(button1.PIN, isr, FALLING);
}

void loop() {
    if (button1.pressed) {
        Serial.printf("Button has been pressed %u times\n", button1.numberKeyPresses);
        button1.pressed = false;
    }
}

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

Подавление дребезга прерывания GPIO ESP8266

Объяснение кода

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

Если прошло менее 250 мс, ESP8266 игнорирует прерывание и немедленно возвращается к тому, что он делал. Если нет, он выполняет код внутри оператора if, увеличивая счётчик и обновляя переменную last_button_time, чтобы функция имела новое значение для сравнения при следующем срабатывании в будущем.