Найти тему
K12 :: О ESP32 и не только

Обработка перерываний GPIO на ESP-IDF

Оглавление

Добрый день, уважаемый читатель! В данной статье продолжим обсуждать тему портов ввода/вывода ESP32, а конкретно рассмотрим работу с прерываниями.

Источник: Яндекс.Картинки
Источник: Яндекс.Картинки
-2

Среди ардуинщиков я иногда сталкивался с мнением (цитирую по памяти): "Да что все так носятся с этими прерываниями?! Мне вот на моем скетче прерывания вообще не нужны!!! Мне не сложно один раз в десять секунд измерить напряжение на входе". Так-то оно может быть и так... Но это пока ваш контроллер выполняет только одно единственную задачу - меряет напряжение на входе и чем-то там "щёлкает". Как только вы попытаетесь прикрутить к вашему устройству парочку дополнительных сервисов, "вечер перестанет быть томным". Притом чем выше частота опроса GPIO, тем выше нагрузка на процессор, а при слишком длительных паузах можно попустить короткие импульсы. Да и вообще, я считаю такой "прямой" подход слишком грубым и неправильным; тем более, что никакой сложности в работе с прерываниями нет. Прерывания позволяют "нагружать" процессор только в моменты возникновения события, без необходимости выполнения пустой бесполезной работы.

Пример из жизни: ваш дверной звонок в доме. Без дверного звонка (или прослушивания чьего-то стука) вам пришлось бы периодически проверять, нет ли кого у двери. Это приводит к бесполезной трате времени в большинстве случаев, когда за дверью никого нет; а также не гарантирует, что если за дверью кто-то есть, вы своевременно откроете её.

Два API для работы с GPIO прерываниями

ESP-IDF предоставляет сразу два API (Application Program Interface) обработки прерываний, которые генерируются по сигналам GPIO.

  • gpio_isr_register() - эта функция позволяет зарегистрировать ISR (обработчик прерываний) для всех GPIO одновременно и сразу. Насколько я понимаю (поправьте меня, если я ошибаюсь) - это "низкоуровневый" метод регистрации GPIO прерываний. При регистрации обработчика (то есть при вызове gpio_isr_register()) не передается никаких данных о номере GPIO - если эта функция используется, для всех прерываний GPIO регистрируется один глобальный ISR. В обработчике прерываний вы сами должны будете определить, какой GPIO сгенерировал прерывание.
  • gpio_install_isr_service() - если эта функция используется, служба ISR предоставляет глобальный обработчик прерываний GPIO, а отдельные обработчики для каждого из выводов регистрируются с помощью функции gpio_isr_handler_add(). Это более простой для понимания программиста подход. Внутри gpio_install_isr_service() содержится вызов того самого gpio_isr_register(), то есть этот самый сервис GPIO ISR берет на себя всю низкоуровневую работу, вам остается только создать обработчики событий.

Сразу скажу, что я пока не вижу особого смысла забираться в дебри ESP-IDF и пользуюсь вторым способом. Тем более, что иногда при запуске прошивки оказывается, что "GPIO isr service already installed" (то есть этим сервисом "пользуется" сама ESP-IDF). Поэтому нет смысла от него отказываться. Давайте с него и начнем.

GPIO ISR service

Последовательность ваших действий в этом случае такова:

  • Вначале вам необходимо зарегистрировать глобальный обработчик GPIO ISR с помощью gpio_install_isr_service(int intr_alloc_flags). В качестве параметра можн0 передать просто 0. Если эта функция вернула ошибку ESP_ERR_INVALID_STATE - ничего страшного, просто вы уже установили этот сервис ранее (либо ESP-IDF сделала это за вас).
  • Затем вы должны создать обработчик события прерывания, используя прототип void IRAM_ATTR isrHandler(void* arg). К обработчикам прерываний предъявляются особые требования, про них я расскажу ниже.
  • После этого вы должны указать, по какому уровню сигнала мы будет генерировать событие с помощью функции gpio_set_intr_type(). Доступны следующие варианты:
  1. GPIO_INTR_DISABLE - отключено
  2. GPIO_INTR_POSEDGE - по изменению с 0 до 1
  3. GPIO_INTR_NEGEDGE - по изменению с 1 на 0
  4. GPIO_INTR_ANYEDGE - по любому изменению
  5. GPIO_INTR_LOW_LEVEL - по низкому уровню
  6. GPIO_INTR_HIGH_LEVEL - по высокому уровню
  • И в заключение разрешаем прерывания с помощью функции gpio_intr_enable().

Последовательность вызовов может выглядеть так:

-3

Создание обработчика прерываний

Всё, что описано в данном разделе, можно смело относить и в случае использования gpio_isr_register(), так как по сути это одно и то же.

Обработчики прерывания - не совсем простые функции. Прерывание приостанавливает выполнение всех потоков вашей программы, поэтому к обработчикам прерываний предъявляются определенные требования:

  • Обработчик прерывания должен выполняться как можно меньше по времени, иначе сработает WDT для прерываний и устройство будет аварийно перезагружено.
  • Обработчик прерывания должен постоянно находится в быстрой памяти IRAM, поэтому его следует пометить соответствующим атрибутом IRAM_ATTR.
  • В обработчиках прерываний не допускается использование библиотечных функций ESP_LOGx, но можно использовать специальную облегченную версию ESP_DRAM_LOGx.
  • Следует всегда помнить, что обработчики прерываний выполняются вне контекста прикладных задач. Дабы не нарушать отчетности соблюдать потокобезопасность из обработчика прерываний лучше всего отправить какие-либо данные в очередь другой задачи, включить флаг в EventGroup и т.д. Да, в принципе можно просто изменить значение какой-либо глобальной статической переменной bool или int, которая будет управлять потоком в другой задаче, но это не приветствуется.
  • Если вам требуется прерывание только по GPIO_INTR_POSEDGE или только по GPIO_INTR_NEGEDGE, то проблем как бы нет. Проблемы начинаются, когда требуется обработать оба события - GPIO_INTR_ANYEDGE. Как определить, что произошло в текущий момент? Читать состояние GPIO с помощью gpio_get_level() из обработчика в принципе можно, но как бы не очень хорошо, поскольку при дребезге контактов состояние вывода может изменяться очень быстро. Поэтому если вам требуется "отловить" и момент нажатия, и момент отпускания кнопки - правильнее будет назначить два обработчика прерываний на разные события. Тему подавления дребезга контактов мы обсудим отдельно, после изучения таймеров.
  • После завершения прерывания можно выполнять переключение контекста путем вызова portYIELD_FROM_ISR. Зачем это нужно? Допустим, в текущий момент выполняется низкоприоритетная задача, а высокоприоритетная ожидает наступления некоторого прерывания. Далее происходит прерывание, но по окончании работы обработчика прерываний выполнение возвращается к текущей низкоприоритетной задаче, а высокоприоритетная ожидает, пока закончится текущий квант времени. Однако если после выполнения обработчика прерывания передать управление планировщику ( portYIELD_FROM_ISR ), то он передаст управление высокоприоритетной задаче, что позволяет значительно сократить время реакции системы на прерывание, связанное с внешним событием.

Учитывая всё вышесказанное напишем обработчик прерывания, который будет отправлять событие (bool) в очередь задачи, которая управляет переключением светодиода.

-4
Примечание: про работу с очередями ( xQueueSendFromISR ) я ещё не рассказывал, мы подробнее обсудим это позже. Пока примите это как есть или почитайте у Курница самостоятельно.

В функции задачи мы должны написать что-то примерно следующее:

-5

Ну и тело основной функции модифицируем следующим образом:

Какой способ запуска задачи здесь использован?
Какой способ запуска задачи здесь использован?

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

Полный исходный код для данного примера вы можете найти по ссылке:

dzen/gpio_isr_service at master · kotyara12/dzen

Низкоуровневый подход

Допустим, вы по какой-то причине решите воспользоваться более низкоуровневым подходом - через gpio_isr_register(). В этом случае используется один и тот же обработчик для всех GPIO. Если у вас используется только одно GPIO "на вход", то проблем нет. А вот если вы будете использовать несколько - то вам самим придется определить, какое GPIO сгенерировало прерывание. Сделать это можно прочитав регистры прерываний GPIO: READ_PERI_REG(GPIO_STATUS_REG);

-7

В данном примере в очередь мы отправляем номер GPIO, которое сгенерировало прерывание. А уже внутри задачи проверяем полученный номер на соответствие заданному:

-8

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

Пример для данного варианта вы можете найти здесь:

dzen/gpio_isr_register at master · kotyara12/dzen

Флаги прерываний

В завершение оставлю небольшое замечание. В примерах выше вам попадались параметры int intr_alloc_flags. Вы можете поставить ноли или использовать комбинацию из следующих значений:

  • ESP_INTR_FLAG_LEVEL1 - Вектор прерывания уровня 1 (самый низкий приоритет)
  • ESP_INTR_FLAG_LEVEL2 - Вектор прерывания уровня 2
  • ESP_INTR_FLAG_LEVEL3 - Вектор прерывания уровня 3
  • ESP_INTR_FLAG_LEVEL4 - Вектор прерывания уровня 4
  • ESP_INTR_FLAG_LEVEL5 - Вектор прерывания уровня 5
  • ESP_INTR_FLAG_LEVEL6 - Вектор прерывания уровня 6
  • ESP_INTR_FLAG_NMI - Вектор прерывания уровня 7 (наивысший приоритет)

Дополнительные флаги:

  • ESP_INTR_FLAG_SHARED - Прерывание может быть разделено между несколькими ISR
  • ESP_INTR_FLAG_EDGE - Прерывание по фронту
  • ESP_INTR_FLAG_IRAM - ISR может быть вызван, если кеш отключен (шта?)
  • ESP_INTR_FLAG_INTRDISABLED - Возврат с отключенным прерыванием

Если вы захотите их использовать, важно понимать одну вещь - прерывания с уровнем до 3 включительно могут быть обработаны в Cи. Все прерывания с уровнями 4-7 могут быть обработаны только на ассемблере. Поэтому, если у вас нет опыта программирования на ассемблере, не стоит баловаться int intr_alloc_flags.

Полезные ссылки

_______________

На этом пока всё, до встречи на сайте и на dzen-канале!

👍 Понравилась статья? Поддержите канал лайком или комментарием! Каналы на Дзене "живут" только за счет ваших лайков.

📌Подпишитесь на канал и вы всегда будете в курсе новых статей.

🔶 Полный архив статей вы найдете здесь

Благодарю за вашу поддержку! 🙏