Настройка и обработка прерываний GPIO на ESP32 в Arduino IDE
Если вы уже некоторое время работаете с ESP32, вы, вероятно, писали код, который использует циклы для постоянной проверки, нажата ли кнопка или обнаружил ли датчик что-либо. Такой подход работает, но он неэффективен и может пропускать быстрые события. Есть способ лучше: Прерывания. Прерывания позволяют вашему ESP32 сосредоточиться на основных задачах и приостанавливаться только тогда, когда действительно происходит что-то важное.
В этом руководстве вы узнаете, как настроить прерывания GPIO на ESP32, какие выводы безопасно использовать, как их правильно сконфигурировать и как писать чистый, надёжный код прерываний, который сделает ваши проекты быстрее и отзывчивее.
Прерывания на ESP32
ESP32 имеет сложную систему прерываний, которая может одновременно обрабатывать несколько прерываний, определять их приоритет и даже назначать их определённым ядрам — что делает его более мощным, чем традиционные платы Arduino.
Прерывания на ESP32 можно разделить на две основные категории:
Внешние прерывания: срабатывают при изменении логического уровня на определённом выводе GPIO. Вы можете использовать внешнее прерывание для обнаружения таких событий, как нажатие кнопки, срабатывание датчика движения или генерация импульса энкодером. По сути, внешние прерывания позволяют вашему ESP32 мгновенно реагировать на изменения во внешнем мире.
Внутренние прерывания: генерируются внутренним оборудованием ESP32, таким как таймеры, сенсорные датчики или периферийные устройства связи (UART, I2C, SPI). Например, если вы настроите таймер на счёт до одной секунды, он может автоматически вызвать прерывание без необходимости использовать функцию
delay(), которая заморозила бы всю вашу программу. Аналогично, когда ваш ESP32 получает данные через последовательный порт, прерывание UART может немедленно оповестить вашу программу.
Это руководство посвящено именно внешним прерываниям GPIO.
Это руководство посвящено именно внешним прерываниям GPIO, но если вы хотите узнать о прерываниях таймера, вы можете ознакомиться с нашим подробным руководством на эту тему.
Какие выводы GPIO можно использовать для прерываний?
Почти все выводы GPIO на ESP32 могут быть использованы для прерываний.
Однако не все из них надёжны для этой цели. Некоторые выводы выполняют специальные функции при загрузке или внутренне подключены к важным системным функциям. Неправильное использование этих выводов может привести к неисправности ESP32 или ошибке при загрузке.
Чтобы упростить понимание, мы организовали выводы в три категории в зависимости от того, насколько безопасно их использовать:
Безопасно — Это самые безопасные выводы. Они полностью рекомендованы и отлично подходят для использования.
С осторожностью — Используйте с осторожностью. Эти выводы могут вести себя непредсказуемо при загрузке. Используйте их только при крайней необходимости.
Избегать — Лучше избегать использования этих выводов.
Метка |
GPIO |
Безопасность |
Причина |
|---|---|---|---|
D0 |
0 |
С осторожностью |
Должен быть удержан в LOW во время сброса для входа чипа в режим Serial Bootloader (прошивки); имеет внутренний подтягивающий резистор |
TX0 |
1 |
Избегать |
Используется для последовательной связи (вывод Tx) |
D2 |
2 |
С осторожностью |
Должен быть «плавающим» или в LOW при загрузке; также подключён к встроенному светодиоду |
RX0 |
3 |
Избегать |
Используется для последовательной связи (вывод Rx) |
D4 |
4 |
Безопасно |
|
D5 |
5 |
С осторожностью |
Должен быть в HIGH при загрузке; также выводит ШИМ-сигнал при запуске, что может неожиданно активировать подключённые периферийные устройства |
D6 |
6 |
Избегать |
Внутренне подключён к SPI-памяти флеш; зарезервирован для системного использования |
D7 |
7 |
Избегать |
Внутренне подключён к SPI-памяти флеш; зарезервирован для системного использования |
D8 |
8 |
Избегать |
Внутренне подключён к SPI-памяти флеш; зарезервирован для системного использования |
D9 |
9 |
Избегать |
Внутренне подключён к SPI-памяти флеш; зарезервирован для системного использования |
D10 |
10 |
Избегать |
Внутренне подключён к SPI-памяти флеш; зарезервирован для системного использования |
D11 |
11 |
Избегать |
Внутренне подключён к SPI-памяти флеш; зарезервирован для системного использования |
D12 |
12 |
С осторожностью |
Должен быть в LOW при загрузке; установка в HIGH переключает внутреннее напряжение флеш-памяти на 1,8В, что может вызвать сбой загрузки |
D13 |
13 |
Безопасно |
|
D14 |
14 |
Безопасно |
|
D15 |
15 |
С осторожностью |
Должен быть в HIGH при загрузке; также выводит ШИМ-сигнал при запуске |
RX2 |
16 |
Безопасно |
|
TX2 |
17 |
Безопасно |
|
D18 |
18 |
Безопасно |
|
D19 |
19 |
Безопасно |
|
D21 |
21 |
Безопасно |
|
D22 |
22 |
Безопасно |
|
D23 |
23 |
Безопасно |
|
D25 |
25 |
Безопасно |
|
D26 |
26 |
Безопасно |
|
D27 |
27 |
Безопасно |
|
D32 |
32 |
Безопасно |
|
D33 |
33 |
Безопасно |
|
D34 |
34 |
С осторожностью |
Вывод только для входа; нет внутренних резисторов; требуется внешний подтягивающий резистор вверх или вниз |
D35 |
35 |
С осторожностью |
Вывод только для входа; нет внутренних резисторов; требуется внешний подтягивающий резистор вверх или вниз |
VP |
36 |
С осторожностью |
Вывод только для входа; нет внутренних резисторов; требуется внешний подтягивающий резистор вверх или вниз |
VN |
39 |
С осторожностью |
Вывод только для входа; нет внутренних резисторов; требуется внешний подтягивающий резистор вверх или вниз |
На изображении ниже показано, какие выводы GPIO безопасно использовать для прерываний:
Настройка выводов GPIO для надёжных прерываний
Прежде чем ваш ESP32 сможет обнаруживать прерывания на выводе GPIO, необходимо правильно настроить этот вывод. Процесс настройки включает два важных шага.
Во-первых, вы должны сконфигурировать вывод GPIO как вход, чтобы он мог считывать изменения напряжения от внешних устройств. Однако простого установления вывода как входа недостаточно. Когда входной вывод остаётся «плавающим», он может улавливать случайные электрические помехи из окружающей среды, заставляя ваш ESP32 обнаруживать прерывания, которых на самом деле не было.
Чтобы предотвратить эту проблему, необходимо обеспечить нахождение вывода в известном и стабильном состоянии, подключив его через резистор к HIGH или LOW. Это можно сделать двумя способами. Вы можете использовать внутренние подтягивающие резисторы, которые уже встроены в большинство выводов GPIO ESP32, или добавить внешние резисторы в вашу схему. Среда программирования Arduino упрощает это с помощью функции pinMode().
Если вы добавили в схему внешний подтягивающий резистор вверх или вниз, следует настроить вывод с помощью
pinMode(pin, INPUT).Если вы хотите использовать встроенные резисторы ESP32, используйте
pinMode(pin, INPUT_PULLUP)для подключения вывода к 3.3В через внутренний резистор, илиpinMode(pin, INPUT_PULLDOWN)для подключения к земле.
Подключение прерывания к выводу
После того как вы правильно настроили вывод GPIO, следующий шаг — сообщить вашему ESP32, чтобы он фактически отслеживал этот вывод на предмет изменений и реагировал, когда что-то происходит. В Arduino IDE это делается с помощью функции attachInterrupt(). Базовый синтаксис выглядит следующим образом:
attachInterrupt(GPIOPin, ISR, Mode);
Эта функция требует три аргумента:
GPIOPin указывает ESP32, какой именно вывод он должен отслеживать на предмет изменений.
ISR (Interrupt Service Routine) — это имя функции, которую вы хотите запускать немедленно при возникновении прерывания.
Mode определяет, какое именно изменение сигнала должно вызывать прерывание. Существует пять возможных режимов:
Константа режима |
Условие срабатывания |
Частота выполнения |
Основной вариант использования |
|---|---|---|---|
|
Переход LOW -> HIGH |
Один раз за событие |
Отпускание кнопки (подтяжка вниз), активация датчика |
|
Переход HIGH -> LOW |
Один раз за событие |
Нажатие кнопки (подтяжка вверх), деактивация датчика |
|
Любое изменение состояния (LOW -> HIGH или HIGH -> LOW) |
Один раз за событие |
Измерение частоты, переключение состояния |
|
Вывод остаётся в LOW |
Непрерывно, пока удерживается |
Обнаружение неисправностей (Используйте с крайней осторожностью) |
|
Вывод остаётся в HIGH |
Непрерывно, пока удерживается |
Обнаружение неисправностей (Используйте с крайней осторожностью) |
Отключение прерывания
Если вы больше не хотите, чтобы ESP32 отслеживал определённый вывод на предмет прерываний, вы можете использовать функцию detachInterrupt(). Синтаксис прост:
detachInterrupt(GPIOPin);
Процедура обработки прерывания (ISR)
Процедура обработки прерывания (ISR) — это специальная функция, которая запускается каждый раз при срабатывании прерывания. Именно здесь вы пишете код, который мгновенно реагирует на событие — например, включает светодиод, подсчитывает импульсы или записывает данные.
Вот как выглядит синтаксис:
void IRAM_ATTR ISR() {
// Код для обработки прерывания
}
Важные правила написания ISR
Когда происходит прерывание, ваш ESP32 немедленно приостанавливает всё, что он делал, и переходит к выполнению вашей ISR. Нормальный ход вашей программы полностью останавливается на это время. Поэтому при написании ISR на ESP32 необходимо соблюдать несколько важных правил:
Делайте ISR короткими и быстрыми. Поскольку ISR прерывают нормальную работу программы, они должны выполняться как можно быстрее. Длинные или медленные ISR могут привести к непредсказуемому поведению всей программы или пропуску других важных событий. Это означает, что вы никогда не должны использовать такие функции, как
delay(),Serial.print()или что-либо другое, что требует значительного времени для выполнения внутри ISR. Вместо этого лучше всего просто установить флаговую переменную или записать значение, которое ваша основная программа может обработать позже.ISR не может принимать входные параметры и не может возвращать значения. Это связано с тем, что ISR вызываются автоматически аппаратным обеспечением, а не вашим кодом, поэтому нет возможности передать им информацию или получить информацию обратно.
Объявляйте общие переменные как volatile. Любая переменная, которая изменяется внутри ISR и используется за её пределами (или наоборот), должна быть объявлена с ключевым словом
volatile. Это сообщает компилятору, что данная переменная может неожиданно измениться в любой момент из-за прерывания, поэтому он должен всегда считывать её фактическое текущее значение из памяти, а не использовать кэшированное или оптимизированное значение. Безvolatileваша программа может не увидеть изменения, внесённые во время прерываний, что приведёт к ошибкам, которые трудно отследить.Избегайте сложных операций. Не выполняйте математику с плавающей запятой, сложные вычисления и не вызывайте другие функции изнутри ISR, если это не является абсолютно необходимым.
Используйте атрибут IRAM_ATTR. Вы должны добавить ключевое слово
IRAM_ATTRперед именем каждой функции ISR, чтобы убедиться, что функция хранится в быстрой внутренней оперативной памяти, а не во внешней флеш-памяти.
Почему ISR нужен атрибут IRAM_ATTR?
Ваша программа обычно хранится во внешней флеш-памяти, которая представляет собой отдельный чип, подключённый к ESP32 через SPI. ESP32 обращается к этой флеш-памяти через контроллер кэша, аналогично тому, как ваш компьютер кэширует часто используемые данные для более быстрого доступа. Эта система прекрасно работает для обычного программного кода, потому что небольшая задержка при получении инструкций из более медленной внешней флеш-памяти не имеет большого значения. Однако прерывания должны обрабатываться немедленно, без какой-либо задержки.
Вот в чём заключается проблема. Когда возникает прерывание, ESP32 должен немедленно загрузить и выполнить код функции ISR. Если этот код хранится во флеш-памяти, процессор должен обратиться к контроллеру кэша, чтобы извлечь его. Но что, если контроллер кэша в этот самый момент занят чем-то другим, например, обработкой передачи Wi-Fi или чтением данных датчика? Или что, если код, который был прерван, находился в процессе управления самим кэшем? Попытка доступа к флеш-памяти в эти критические моменты может привести к аварийному сбою ESP32 с ошибками типа «Load Prohibited» или просто к непредсказуемому поведению.
Решение — хранить функции ISR в специальном типе памяти, называемом IRAM, что означает Instruction RAM (ОЗУ для инструкций). В отличие от флеш-памяти, IRAM встроена непосредственно в чип ESP32, и процессор может обращаться к ней мгновенно, не ожидая флеш-память. Она значительно быстрее и всегда доступна, независимо от того, что ещё происходит на чипе.
Применяя атрибут IRAM_ATTR, вы указываете компилятору разместить вашу функцию ISR в этой специальной памяти, а не в стандартной флеш-памяти, обеспечивая её выполнение в тот момент, когда она необходима.
Пример 1: Базовое прерывание от кнопки
Давайте рассмотрим простой пример, чтобы понять, как работают прерывания на ESP32.
Мы создадим простой проект, который подсчитывает, сколько раз вы нажали кнопку. Для этого проекта вы подключите тактовую кнопку к GPIO 18 (D18) на ESP32. Вам не нужно добавлять внешний подтягивающий резистор, потому что мы включим встроенный внутренний подтягивающий резистор ESP32 в коде.
Скетч ниже отслеживает GPIO 18 и следит конкретно за фронтом FALLING, что означает, что он ищет изменение напряжения с HIGH на LOW. Этот переход происходит в момент нажатия кнопки. Когда ESP32 обнаруживает это падение напряжения, он немедленно вызывает специальную функцию с именем isr, которую мы написали для обработки прерывания. Внутри этой функции код ведёт текущий подсчёт количества нажатий кнопки.
struct Button {
const uint8_t PIN;
volatile uint32_t numberKeyPresses;
volatile bool pressed;
};
Button button1 = { 18, 0, false };
void IRAM_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. Затем нажмите кнопку EN (сброс) на вашем ESP32. Если всё работает правильно, вы увидите общее количество нажатий кнопки, отображаемое каждый раз при нажатии.
Объяснение кода
В начале скетча мы определяем структуру под названием Button. Эта структура содержит три элемента информации: номер вывода, который мы используем, счётчик количества нажатий кнопки и флаг «истина/ложь», который сообщает нам, нажата ли кнопка в данный момент.
struct Button {
const uint8_t PIN;
volatile uint32_t numberKeyPresses;
volatile bool pressed;
};
Вы заметите, что два члена структуры имеют ключевое слово volatile перед ними. Это ключевое слово крайне важно, когда переменные модифицируются внутри процедуры обработки прерывания и считываются в основной программе. Ключевое слово volatile сообщает компилятору, что эти переменные могут неожиданно измениться в любой момент из-за прерывания, поэтому компилятор должен всегда считывать их фактическое текущее значение из памяти, а не использовать кэшированное или оптимизированное значение. Без volatile компилятор может оптимизировать ваш код таким образом, что основной цикл пропустит обновления, внесённые ISR, что приведёт к непредсказуемому поведению.
Затем мы создаём фактический объект кнопки, используя эту структуру, и задаём ему начальные значения. Номер вывода установлен на 18, счётчик нажатий начинается с нуля, потому что кнопка ещё не была нажата, а флаг нажатия начинается как false.
Button button1 = {18, 0, false};
Далее идёт процедура обработки прерывания (ISR). Эта функция очень проста и следует всем важным правилам, которые мы обсудили ранее. Она имеет атрибут IRAM_ATTR в начале, чтобы обеспечить хранение в быстрой памяти, и выполняет только две быстрые операции. Во-первых, она добавляет единицу к счётчику нажатий, и, во-вторых, устанавливает флаг нажатия в true, чтобы основная программа знала, что произошло прерывание. Помните, мы хотим сделать эту функцию максимально короткой и быстрой, потому что она прерывает нормальную работу программы.
void IRAM_ATTR isr() {
button1.numberKeyPresses++;
button1.pressed = true;
}
В разделе setup мы сначала устанавливаем последовательную связь с компьютером. Далее мы настраиваем вывод 18 как вход и включаем внутренний подтягивающий резистор, который удерживает вывод на высоком уровне напряжения, когда кнопка не нажата. Наконец, мы используем функцию attachInterrupt(), чтобы указать ESP32 отслеживать вывод 18 и вызывать нашу функцию isr всякий раз, когда сигнал изменяется с HIGH на LOW (фронт FALLING).
void setup() {
Serial.begin(115200);
pinMode(button1.PIN, INPUT_PULLUP);
attachInterrupt(button1.PIN, isr, FALLING);
}
Наконец, в разделе loop мы проверяем, была ли нажата кнопка, проверяя, установлен ли флаг pressed в true прерыванием. Если да, мы выводим сообщение с общим количеством нажатий кнопки. Затем мы сбрасываем флаг pressed в false, чтобы он был готов обнаружить следующее нажатие кнопки.
void loop() {
if (button1.pressed) {
Serial.printf("Button has been pressed %u times\n", button1.numberKeyPresses);
button1.pressed = false;
}
}
Проблема дребезга контактов
Если вы запустите программу подсчёта нажатий кнопки и внимательно понаблюдаете за выводом в последовательном мониторе, вы, скорее всего, заметите что-то странное и неприятное — даже когда вы нажимаете кнопку всего один раз, максимально аккуратно и быстро, счётчик часто перескакивает на несколько чисел вместо одного.
Это не ошибка в вашем коде и не проблема с вашим ESP32. Вместо этого это вызвано физическим явлением, называемым «Дребезг контактов» (Switch Bounce).
Когда вы нажимаете кнопку, вам это кажется мгновенным. Однако если бы вы могли рассмотреть, что на самом деле происходит на электрическом уровне с помощью осциллографа или анализатора сигналов, вы бы увидели, что напряжение не совершает чистый, мгновенный переход с HIGH на LOW. Вместо этого оно многократно скачет вверх и вниз в течение нескольких миллисекунд, прежде чем окончательно установиться в состояние LOW.
Это происходит потому, что кнопки и переключатели являются механическими устройствами с металлическими контактами внутри. Когда вы нажимаете кнопку, эти металлические части физически перемещаются и соприкасаются друг с другом, замыкая цепь. Но они не создают идеальный контакт мгновенно. Вместо этого контакты подпрыгивают друг о друга несколько раз, прежде чем установиться в стабильное состояние.
В течение этого периода ESP32 видит множественные переходы напряжения с HIGH на LOW и обратно на HIGH и снова на LOW, и каждый из них вызывает ваше прерывание. Вот почему одно нажатие кнопки может увеличить ваш счётчик более чем на единицу.
Процесс устранения этого нежелательного эффекта «подпрыгивания» называется «Устранение дребезга» (Debouncing). Каждый проект, использующий физические кнопки или переключатели, должен каким-то образом решать эту проблему. К счастью, существует два проверенных подхода к её решению, и вы можете выбрать тот, который лучше всего подходит для вашего проекта.
Первый подход — аппаратное устранение дребезга, при котором вы добавляете резистор и конденсатор (RC-фильтр) в свою схему, который сглаживает быстрые скачки напряжения в один чистый переход. Этот метод отлично работает и полностью устраняет проблему на уровне схемы, но требует дополнительных компонентов и делает вашу схему немного сложнее.
Второй подход — программное устранение дребезга, при котором вы решаете проблему полностью в программном коде без добавления какого-либо оборудования. Основная идея заключается в том, чтобы ваша программа временно игнорировала любые дополнительные прерывания, которые происходят в течение короткого временного окна после срабатывания первого прерывания.
Пример 2: Прерывание от кнопки с устранением дребезга
Давайте улучшим нашу программу подсчёта нажатий кнопки, добавив программное устранение дребезга. Вместо реакции на каждый крошечный электрический скачок, программа теперь проверяет время между прерываниями и игнорирует любые дополнительные прерывания, которые происходят в течение короткого временного окна после срабатывания первого прерывания.
Изменения в скетче выделены зелёным цветом.
struct Button {
const uint8_t PIN;
volatile uint32_t numberKeyPresses;
volatile bool pressed;
};
Button button1 = { 18, 0, false };
//variables to keep track of the timing of recent interrupts
volatile unsigned long button_time = 0;
volatile unsigned long last_button_time = 0;
void IRAM_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 срабатывает, ESP32 записывает текущее время с помощью функции millis(), которая возвращает, сколько миллисекунд прошло с момента запуска программы. Затем он сравнивает это значение со временем последнего срабатывания ISR.
Если разница между двумя временами составляет менее 250 миллисекунд, ESP32 предполагает, что это просто дребезг контактов, и игнорирует сигнал. Если прошло более 250 миллисекунд, ISR выполняется нормально.
Остальная часть программы остаётся точно такой же, как и в исходной версии.