В прошлом уроке нам удалось попробовать использование порта ввода-вывода на вход.
Только работали мы с кнопкой, отслеживая её состояние в неком цикле, что накладывает ряд ограничений на работу всей программы. Для этого и придумано следить за состоянием портов именно посредством прерываний. Как это всё работает, мы уже неоднократно видели, например в аналогичном уроке для ESR8266 и уроках для работы с другими контроллерами.
Мы знаем также, что внешние прерывания обрабатываются разные, вернее они работают по разным событиям (нарастание, спад и т.д.). Есть также универсальные прерывания, которые отслеживают и то и это. ESP32 в этом плане также не исключение и в нём это всё есть. И не только это. В технической документации (Technical Reference Manual) есть описание регистра GPIO_PINn_REG, в котором есть битовое поле, отвечающее за настройку типов прерываний для порта ввода-вывода
Вот описание установки значения в данном поле:
GPIO_PINn_INT_TYPE Interrupt type selection: (R/W) 0: GPIO interrupt disable; 1: rising edge trigger; 2: falling edge trigger; 3: any edge trigger; 4: low level trigger; 5: high level trigger.
Третий тип установки отслеживает и фронт и спад сигнала.
Поэтому нам никак нельзя данные возможности обойти и этим не воспользоваться. Тем более что с данными прерываниями мы не работали в операционной системе реального времени FreeRTOS.
Так как мы будем использовать не один тип изменения сигнала на ножке, давайте подключим две кнопки, одну по-прежнему к GPIO4, а другую — к GPIO5
Проект мы за основу возьмём из прошлого урока с именем BUTTON01 и дадим ему имя EXTI01.
Откроем наш новый проект в Espressif IDE и внесём некоторые изменения в файл конфигурации Kconfig.projbuild.
Во второй пункт мы добавим номер кнопки, так как их будет два
config BUTTON_GPIO_1
int "Button GPIO1 number"
range 0 48
default 4
help
GPIO number Button 1.
Ниже добавим ещё один пункт для второй кнопки
config BUTTON_GPIO_2
int "Button GPIO2 number"
range 0 48
default 5
help
GPIO number Button 2.
Обратите внимание, что номер порта по умолчанию тоже будет другой.
Попробуем собрать проект, и сконфигурировать его. При сборке скорей всего будет ошибка на изменение имени переменной, не обращаем внимание на это
Из тела функции app_main файла main.c удалим весь код, кроме бесконечного цикла, останется вот это
1 2 3 4 5 |
void app_main(void) { while (1) { } } |
Теперь у нас, конечно же, код соберётся.
Добавим в данную функцию объявление переменой для счётчика циклов
1 2 3 |
void app_main(void) { int cnt = 0; |
Сегодня мы не будем для изменения каждого параметра порта ввода-вывода использовать отдельную функцию, а будем использовать для этого специальную структуру.
Поэтому создадим переменную типа такой структуры
1 2 |
int cnt = 0; gpio_config_t io_conf = {}; |
Объявим несколько макросов — один для маски ножки выхода, а другой для маски ножек входа, а также макрос для настройки флага прерываний
1 2 3 4 5 6 |
#include "sdkconfig.h" //------------------------------------------------ #define GPIO_OUTPUT_PIN_SEL (1ULL<<CONFIG_BLINK_GPIO) #define GPIO_INPUT_PIN_SEL ((1ULL<<CONFIG_BUTTON_GPIO_1) | (1ULL<<CONFIG_BUTTON_GPIO_2)) #define ESP_INTR_FLAG_DEFAULT 0 //------------------------------------------------ |
Вернёмся в функцию app_main и проинициализируем поля переменной структуры настройками для ножки, работающей на выход
1 2 3 4 5 6 |
gpio_config_t io_conf = {}; io_conf.intr_type = GPIO_INTR_DISABLE; io_conf.mode = GPIO_MODE_OUTPUT; io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL; io_conf.pull_down_en = 0; io_conf.pull_up_en = 0; |
Применим данные настройки с помощью специальной функции
1 2 |
io_conf.pull_up_en = 0; gpio_config(&io_conf); |
Теперь заполним поля для ножек, работающих на вход, и также применим настройки. Поля, которые не меняются, повторно инициализировать не надо
1 2 3 4 5 6 |
gpio_config(&io_conf); io_conf.intr_type = GPIO_INTR_POSEDGE; io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL; io_conf.mode = GPIO_MODE_INPUT; io_conf.pull_up_en = 1; gpio_config(&io_conf); |
Мы хотим, чтобы типы прерываний были разные, а у нас присвоился один и тот же тип для обеих ножек при помощи присвоения значения перечислимого типа GPIO_INTR_POSEDGE полю intr_type.
Мы можем определённой ножке порта применить другой тип при помощи функции. Так и поступим
1 2 3 |
gpio_config(&io_conf); gpio_set_intr_type(CONFIG_BUTTON_GPIO_1, GPIO_INTR_ANYEDGE); |
Далее перед нами стоит задача добавить обработчик прерываний, в котором мы и будем их отслеживать и из которого при помощи очереди будем передавать в другую задачу, где и будет код реакции на данные события. Поступаем мы так потому, что в обработчике прерываний должен быть минимальный код.
Добавим глобальную переменную для очереди
1 2 3 |
#define ESP_INTR_FLAG_DEFAULT 0 //------------------------------------------------ static xQueueHandle gpio_evt_queue = NULL; |
Также подключим библиотеку для работы с очередями
1 2 |
#include "freertos/task.h" #include "freertos/queue.h" |
Создадим очередь в app_main
1 2 3 |
gpio_set_intr_type(CONFIG_BUTTON_GPIO_1, GPIO_INTR_ANYEDGE); gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t)); |
Выше функции app_main добавим функцию задачи, которая будет выводить в порт информацию по событиям прерываний
1 2 3 4 5 6 7 8 9 10 11 |
//------------------------------------------------ static void task1(void* arg) { uint32_t io_num; for(;;) { if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) { printf("GPIO[%d] intr, val: %d\n", io_num, gpio_get_level(io_num)); } } } //------------------------------------------------ |
Здесь из очереди забирается номер ножки порта и выводится в терминал, а также заодно выводится состояние данной ножки.
Создадим данную задачу в app_main с высоким приоритетом. Хотя, в принципе, это не очень высокий приоритет. Мы в этом убедимся, когда будем выводить список задач со значениями их приоритетов и номерами ядра, в котором выполняется каждая задача
1 2 3 |
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t)); xTaskCreate(task1, "task1", 2048, NULL, 10, NULL); |
Включим прерывания
1 2 3 |
xTaskCreate(task1, "task1", 2048, NULL, 10, NULL); gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT); |
Мы можем назначить один и тот же обработчик прерывания на оба входа. Добавим функцию для него выше функции task1
1 2 3 4 5 6 7 |
//------------------------------------------------ static void IRAM_ATTR gpio_isr_handler(void* arg) { uint32_t gpio_num = (uint32_t) arg; xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL); } //------------------------------------------------ |
В данном обработчике мы при помощи параметра передаём в очередь номер ножки порта. Стоит обратить внимание, что для передачи в очередь из обработчиков прерываний используется несколько другая функция, нежели из обычных задач.
В функции app_main объявим наш обработчик для каждой ножки
1 2 3 |
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT); gpio_isr_handler_add(CONFIG_BUTTON_GPIO_1, gpio_isr_handler, (void*) CONFIG_BUTTON_GPIO_1); gpio_isr_handler_add(CONFIG_BUTTON_GPIO_2, gpio_isr_handler, (void*) CONFIG_BUTTON_GPIO_2); |
В бесконечном цикле выведем значение счётчика, который будет считать каждый цикл, заодно проинкрементировав его, и установим значение ножки светодиода в зависимости от чётности счётчика
1 2 3 4 5 |
while (1) { printf("cnt: %d\n", cnt++); gpio_set_level(CONFIG_BLINK_GPIO, cnt % 2); vTaskDelay(1000 / portTICK_RATE_MS); } |
Ну вот, в принципе, и всё. Соберём код, прошьём контроллер и запустим терминал. Терминал мы сегодня запустим прямо в среде Espressif IDE. Если у кого он ещё не добавлен в окно ввода-вывода, то делается это посредством выбора пункта меню Window->Show View->Terminal
Запускаем терминал, настраиваем порт и видим как наращивается наш счётчик. В такт с этим мигает светодиод на плате
Нажмём на кнопку 1 и, так как кнопка прижимает ножку на общий провод, то у нас в терминале отобразится уровень 0
Отожмём кнопку, разорвав цепь, и увидим, что на ножке у нас теперь уровень 1
Также мы в сообщении видим номер ножки порта.
Нажмём теперь кнопку 2
И в данном случае мы не получим никакого сообщения, так как в случае нажатия кнопки у нас происходит спад, а ножка 5 у нас настроена только на отслеживание фронта
Отожмём кнопку и вот результат
Таким образом, на данном уроке мы научились отслеживать состояние ножек порта посредством обработки событий в прерываниях. Также мы научились работать с информацией из виртуального порта прямо в среде разработки Espressif IDE.
Всем спасибо за внимание!
Предыдущий урок Программирование МК ESP32 Следующий урок
Недорогие отладочные платы ESP32 можно купить здесь: Недорогие отладочные платы ESP32
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в Дзен (нажмите на картинку)
Не работает ваш код. Ругается на xQueueHandle gpio_evt_queue = NULL;