Перейти к содержимому

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

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

/**********************************************
 * СХЕМА СОЕДИНЕНИЙ:
 *
 * На GPIO 18 подключена кнопка: одним выводом на GPIO 18, другим на землю.
 * GPIO 18 подтянут резистором 10кОм на шину питания +3,3В (но можно использовать и внутреннюю подтяжку).
 *
 * На GPIO 16 подключен светодиод (как в предыдущем примере) - одним выводом на +3,3В,
 * другим - к GPIO 16 через резистор
 *
 ***********************************************/

Среди ардуинщиков я иногда сталкивался с мнением (цитирую по памяти): “Да что все так носятся с этими прерываниями?! Мне вот на моем скетче прерывания вообще не нужны!!! Мне не сложно один раз в десять секунд измерить напряжение на входе“. Так-то оно может быть и так… Но это пока ваш контроллер выполняет только одно единственную задачу – меряет напряжение на входе и чем-то там “щёлкает”. Как только вы попытаетесь прикрутить к вашему устройству парочку дополнительных сервисов, “вечер перестанет быть томным”. Притом чем выше частота опроса 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().

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

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

Всё, что описано в данном разделе, можно смело относить и в случае использования 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) в очередь задачи, которая управляет переключением светодиода.

static xQueueHandle button_queue = NULL;

// Обработчик прерывания по нажатию кнопки
static void IRAM_ATTR isrButtonPress(void* arg)
{
  // Переменные для переключения контекста
  BaseType_t xHigherPriorityTaskWoken, xResult;
  xHigherPriorityTaskWoken = pdFALSE;
  // Поскольку мы "подписались" только на GPIO_INTR_NEGEDGE, мы уверены что это именно момент нажатия на кнопку
  bool pressed = true;
  // Отправляем в очередь задачи событие "кнопка нажата"
  xResult = xQueueSendFromISR(button_queue, &pressed, &xHigherPriorityTaskWoken);
  // Если высокоприоритетная задача ждет этого события, переключаем управление
  if (xResult == pdPASS) {
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
  };
}

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

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

// Функция задачи светодиода
void led_exec(void *pvParameters)
{
  // Настраиваем вывод GPIO_NUM_16 на выход без подтяжки
  gpio_pad_select_gpio(16);
  gpio_set_direction(GPIO_NUM_16, GPIO_MODE_OUTPUT_OD);
  gpio_set_pull_mode(GPIO_NUM_16, GPIO_FLOATING);

  // Управление светодиодом
  bool led_state = false;
  bool led_pressed;
  while(1) {
    // Ждем события нажатия кнопки в очереди
    if (xQueueReceive(button_queue, &led_pressed, portMAX_DELAY)) {
      ESP_LOGI("ISR", "Button is pressed");

      // Переключаем светодиод
      led_state = !led_state;
      gpio_set_level(GPIO_NUM_16, (uint32_t)led_state);
    };
  };
  vTaskDelete(NULL);
}
Ну и тело основной функции модифицируем следующим образом:

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

Полный исходный код для данного примера вы можете найти по ссылке: https://github.com/kotyara12/dzen/tree/master/gpio_isr_service

 

 

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

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

  // Считываем и сбрасываем регистры прерываний GPIO
  uint32_t gpio_intr_status = READ_PERI_REG(GPIO_STATUS_REG);     // Чтение регистров статуса прерывания для GPIO0-31
  SET_PERI_REG_MASK(GPIO_STATUS_W1TC_REG, gpio_intr_status);      // Очистка регистров прерывания для GPIO0-31
  uint32_t gpio_intr_status_h = READ_PERI_REG(GPIO_STATUS1_REG);  // Чтение статуса прерывания для GPIO32-39
  SET_PERI_REG_MASK(GPIO_STATUS1_W1TC_REG, gpio_intr_status_h);   // Очистка регистров прерывания для GPIO32-39

  // Определяем номер порта, с которого поступило событие
  uint32_t gpio_num = 0;
  do {
    if (gpio_num < 32) {
      if (gpio_intr_status & BIT(gpio_num)) {
        // Отправляем в очередь задачи номер нажатой кнопки
        xResult = xQueueSendFromISR(gpio_queue, &gpio_num, &xHigherPriorityTaskWoken);
      }
    } else {
      if (gpio_intr_status_h & BIT(gpio_num - 32)) {
        // Отправляем в очередь задачи номер нажатой кнопки
        xResult = xQueueSendFromISR(gpio_queue, &gpio_num, &xHigherPriorityTaskWoken);
      }
    };
  } while (++gpio_num < GPIO_PIN_COUNT);

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

// Функция задачи светодиода
void led_exec(void *pvParameters)
{
  // Настраиваем вывод GPIO_NUM_16 на выход без подтяжки
  gpio_pad_select_gpio(16);
  gpio_set_direction(GPIO_NUM_16, GPIO_MODE_OUTPUT_OD);
  gpio_set_pull_mode(GPIO_NUM_16, GPIO_FLOATING);

  // Управление светодиодом
  bool led_state = false;
  uint32_t gpio_num;

  while(1) {
    // Ждем события в очереди
    if (xQueueReceive(gpio_queue, &gpio_num, portMAX_DELAY)) {
      // Если это кнопка...
      if (gpio_num == 18) {
        ESP_LOGI("ISR", "Button is pressed");

        // Переключаем светодиод
        led_state = !led_state;
        gpio_set_level(GPIO_NUM_16, (uint32_t)led_state);
      };
    };
  };
  vTaskDelete(NULL);
}

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

Пример для данного варианта вы можете найти здесь: https://github.com/kotyara12/dzen/tree/master/gpio_isr_register

 

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

В завершение оставлю небольшое замечание. В примерах выше вам попадались параметры 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.


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

  1. ESP-IDF — GPIO and RTC GPIO.
  2. Пример на GitHub

Материалы по теме

  1. Работа с портами ввода-вывода GPIO из ESP-IDF
  2. ESP32 pinout: ещё раз о GPIO & pаспределяем выводы с помощью excel
  3. ESP32: чипы, модули, платы…

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *