Добрый день, уважаемый читатель! В данной статье продолжим обсуждать тему портов ввода/вывода ESP32, а конкретно рассмотрим работу с прерываниями.
Среди ардуинщиков я иногда сталкивался с мнением (цитирую по памяти): "Да что все так носятся с этими прерываниями?! Мне вот на моем скетче прерывания вообще не нужны!!! Мне не сложно один раз в десять секунд измерить напряжение на входе". Так-то оно может быть и так... Но это пока ваш контроллер выполняет только одно единственную задачу - меряет напряжение на входе и чем-то там "щёлкает". Как только вы попытаетесь прикрутить к вашему устройству парочку дополнительных сервисов, "вечер перестанет быть томным". Притом чем выше частота опроса 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(). Доступны следующие варианты:
- GPIO_INTR_DISABLE - отключено
- GPIO_INTR_POSEDGE - по изменению с 0 до 1
- GPIO_INTR_NEGEDGE - по изменению с 1 на 0
- GPIO_INTR_ANYEDGE - по любому изменению
- GPIO_INTR_LOW_LEVEL - по низкому уровню
- GPIO_INTR_HIGH_LEVEL - по высокому уровню
Последовательность вызовов может выглядеть так:
Создание обработчика прерываний
Всё, что описано в данном разделе, можно смело относить и в случае использования 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) в очередь задачи, которая управляет переключением светодиода.
Примечание: про работу с очередями ( xQueueSendFromISR ) я ещё не рассказывал, мы подробнее обсудим это позже. Пока примите это как есть или почитайте у Курница самостоятельно.
В функции задачи мы должны написать что-то примерно следующее:
Ну и тело основной функции модифицируем следующим образом:
Мы получим прошивку, которая будет переключать состояние светодиода при каждом нажатии на кнопку. Однако здесь есть небольшая проблемка, называется она "дребезг контактов", и заключается она в том, что в момент нажатия на кнопку будет сформирован не один переход, а целая пачка очень коротких импульсов. Поэтому этот код может не очень четко работать. Бороться с дребезгом контактов можно разными способами, я. пожалуй, оставлю это на отдельную статью (после знакомства с таймерами).
Полный исходный код для данного примера вы можете найти по ссылке:
Низкоуровневый подход
Допустим, вы по какой-то причине решите воспользоваться более низкоуровневым подходом - через gpio_isr_register(). В этом случае используется один и тот же обработчик для всех GPIO. Если у вас используется только одно GPIO "на вход", то проблем нет. А вот если вы будете использовать несколько - то вам самим придется определить, какое GPIO сгенерировало прерывание. Сделать это можно прочитав регистры прерываний GPIO: READ_PERI_REG(GPIO_STATUS_REG);
В данном примере в очередь мы отправляем номер GPIO, которое сгенерировало прерывание. А уже внутри задачи проверяем полученный номер на соответствие заданному:
Не забудьте поправить строчку создания очереди. Впрочем, проверку на соответствие можно выполнить и в обработчике прерывания.
Пример для данного варианта вы можете найти здесь:
Флаги прерываний
В завершение оставлю небольшое замечание. В примерах выше вам попадались параметры 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-канале!
👍 Понравилась статья? Поддержите канал лайком или комментарием! Каналы на Дзене "живут" только за счет ваших лайков.
📌Подпишитесь на канал и вы всегда будете в курсе новых статей.
🔶 Полный архив статей вы найдете здесь
Благодарю за вашу поддержку! 🙏