Привет, Хабр!
Это вторая статья из цикла по программированию ESP32 на ESP‑IDF. В первой части мы познакомились с базовой терминологией RTOS и реализовали несколько простых задач (tasks). Сегодня же мы перейдём к работе с GPIO и прерываниями (ISR), а заодно обсудим особенности настройки стека задач в ESP‑IDF (спасибо за совет @0x6b73ca).

Стек
Обратимся к официальной документации:
FreeRTOS
В FreeRTOS мы работаем в словах.
ESP-IDF
В ESP-IDF мы работаем в байтах.
В классическом FreeRTOS параметр usStackDepthзадаётся в машинных словах (StackType_t), тогда как в ESP‑IDF тот же параметр измеряется в байтах. Это важно учитывать при переносе проектов между платформами, чтобы не выделять слишком много или слишком мало памяти для стека задачи.
Как проверить стек задачи?
uxTaskGetStackHighWaterMark(NULL); // NULL - текущая задача
Она показывает минимальный остаток свободного стека конкретной задачи.
Рассмотрим небольшой пример:
// Первая задача, которая будет прикреплена к ядру 0 void task_core0(void *arg) { while (1) { printf("Запущена первая задача на ядре %d\n", xPortGetCoreID()); printf("stack left: %d\n", uxTaskGetStackHighWaterMark(NULL)); vTaskDelay(pdMS_TO_TICKS(1000)); } }
// Создаём первую задачу и прикрепляем её к ядру 0 (PRO_CPU) xTaskCreatePinnedToCore( task_core0, // функция‑задача "TaskCore0", // имя задачи 2048, // размер стека NULL, // аргумент 5, // приоритет &Task_1_Handle, // хэндл 0 // ядро 0 );
Вывод:
Запущена первая задача на ядре 0 stack left: 300
GPIO
По умолчанию каждый физический контакт (pad) ESP32 может выполнять различные аппаратные функции — от АЦП и touch‑сенсора до UART, SPI и LED‑контроллера. Чтобы использовать pad как обычный цифровой ввод‑вывод, его нужно переключить в режим GPIO.
Для этого служит функция
esp_rom_gpio_pad_select_gpio(pin);
Она отключает все альтернативные функции (ADC, touch, UART, SPI, LEDC и прочие) и привязывает выбранный pad к модулю GPIO.
Далее можно задать направление и уровни с помощью стандартных API. Ниже — основные функции для работы с GPIO:
Основные функций при работе с GPIO |
Функция | Описание |
|---|---|
| Привязывает физический pad к модулю GPIO, отключая все альтернативные функции контакта. |
| Сбрасывает конфигурацию пина к значению по умолчанию (вызывает |
| Устанавливает направление: |
| Устанавливает логический уровень (0 или 1) на выходном пине. |
| Читает текущее логическое состояние (0/1) на входном или выходном пине. |
| Упрощённая групповая конфигурация: направление, подтяжки, прерывания, маска пинов. |
| Включает или отключает внутренний pull‑up резистор. |
| Включает или отключает внутренний pull‑down резистор. |
Помигаем)
А чем собственно мигаем?
На плате NodeMCU‑32S (и на многих других «devkit»-модулях для ESP32) встроенный светодиод физически подключён к контакту GPIO2. Также сразу объявим дескриптор задачи мигания:
#define LED_GPIO GPIO_NUM_2 // Порт светодиода TaskHandle_t Blink_Handle = NULL; // Дескриптор задачи Blink
Blink_Task
Думаю, в прошлой статье мы достаточно подробно разобрали механизм задач, поэтому без лишних пояснений приведём итоговую структуру задачи:
void Blink_Task(void *arg){ esp_rom_gpio_pad_select_gpio(LED_GPIO); // "переключение" выбранного физического контакта в режим GPIO gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT); // Устанавливаем направление как выход while(1){ gpio_set_level(LED_GPIO, 1); // Устанавливаем логический уровень 1 vTaskDelay(pdMS_TO_TICKS(1000)); // Ждем gpio_set_level(LED_GPIO, 0); // Устанавливаем логический уровень 0 vTaskDelay(pdMS_TO_TICKS(1000)); // Ждем } }
Результат
#include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" #define LED_GPIO GPIO_NUM_2 // Порт светодиода TaskHandle_t Blink_Handle = NULL; // Дескриптор задачи Blink void Blink_Task(void *arg){ esp_rom_gpio_pad_select_gpio(LED_GPIO); // "переключение" выбранного физического контакта в режим GPIO gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT); // Устанавливаем направление как выход while(1){ gpio_set_level(LED_GPIO, 1); // Устанавливаем логический уровень 1 vTaskDelay(pdMS_TO_TICKS(1000)); // Ждем gpio_set_level(LED_GPIO, 0); // Устанавливаем логический уровень 0 vTaskDelay(pdMS_TO_TICKS(1000)); // Ждем } } void app_main() { xTaskCreate( Blink_Task, // указатель на функцию‑задачу "BLINK", // имя задачи (для отладки) 4096, // размер стека NULL, // аргумент, передаваемый в функцию (здесь не нужен) 10, // приоритет задачи &Blink_Handle // указатель, в который запишут дескриптор задачи ); }

Прерывания (ISR)
Прерывание (Interrupt) — одна из базовых концепций вычислительной техники, которая заключается в том, что при наступлении какого-либо события происходит передача управления специальной процедуре, называемой обработчиком прерываний (ISR).
Прерывания бывают двух типов:
Аппаратные — генерируются железом (например, периферийными модулями GPIO, таймерами, UART);
Программные — инициируются выполнением в коде специальной инструкции, позволяющей «искусственно» вызвать обработчик.
Кроме того, в ESP32 реализован механизм межпроцессорного вызова (IPC), который имеет два режима работы:
Task Context — колбэк выполняется в контексте специальной IPC‑таски, что позволяет использовать любые функции FreeRTOS и ESP‑IDF.
ISR Context — вызов происходит сразу в контексте высокоприоритетного прерывания (Inter‑Processor Interrupt). В этом режиме колбэк должен находиться в IRAM и реализовываться на ассемблере, поскольку нельзя полагаться на доступность флеш‑кеша и поддерживаются только низкоуровневые инструкции.
Подробности и ограничения каждого режима можно найти в официальной документации по IPC:
https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/ipc.html
Регистрация обработчика прерывания в ESP-IDF
Ниже приведены основные функции и их атрибуты при работе с ISR:
gpio_set_intr_type(gpio_num, intr_type);
Назначение: задаёт тип аппаратного прерывания (по какому фронту) для конкретного GPIO‑пина.
Параметры:
gpio_num– номер пина (GPIO_NUM_0…GPIO_NUM_39).intr_type- тип прерывания:GPIO_INTR_DISABLE- отключить прерыванияGPIO_INTR_POSEDGE- по положительному фронтуGPIO_INTR_NEGEDGE- по отрицательному фронтуGPIO_INTR_ANYEDGE- по любому фронтуGPIO_INTR_LOW_LEVEL- по удержанию низкого уровняGPIO_INTR_HIGH_LEVEL- по удержанию высокого уровня
В контексте GPIO‑прерываний термин «фронт» означает момент изменения уровня сигнала, а «уровень» (level) — сам факт удержания сигнала в том или ином состоянии. Например,
GPIO_INTR_POSEDGE(«по положительному фронту») — срабатывает один раз в момент, когда входной сигнал переходит из 0 в 1 (низкий → высокий).GPIO_INTR_ANYEDGE(«по любому фронту») — срабатывает как при переходе 0→1, так и при 1→0.
gpio_install_isr_service(intr_alloc_flags);
Назначение: инициализирует общий сервис прерываний GPIO, позволяя затем регистрировать обработчики (
gpio_isr_handler_add) для отдельных пинов.Параметр
intr_alloc_flags:0– без специальных флагов (приоритет и режим определяются системой)ESP_INTR_FLAG_IRAM- обработчики прерываний будут загружены в IRAMESP_INTR_FLAG_LOWMED- средний / низкий приоритет прерывания
Когда Вы используете флаг
ESP_INTR_FLAG_IRAMвgpio_install_isr_service(), вы гарантируете, что:
Обработчик прерывания (ISR), который вы зарегистрируете позже, будет вызываться из IRAM.
Все внутренние структуры и маршрутизация вызова ISR также будут размещены в IRAM.
Это необходимо, потому что во время некоторых прерываний (например, связанных с SPI Flash или DMA) кеш может быть отключён, и код, находящийся во флеш‑памяти, станет недоступным. Если в такой момент произойдёт переход по адресу, лежащему во флеш, произойдёт краш, watchdog reset или undefined behavior.
Для тех кто позабыл или не знал:
IRAM (Internal RAM) — это встроенная быстрая оперативная память внутри ESP32. Она работает быстрее и не зависит от внешней SPI Flash, что критично важно для надёжной и быстрой работы обработчиков прерываний.
gpio_isr_handler_add(gpio_num, isr_handler, args);
Назначение: регистрирует функцию-обработчик прерывания (
isr_handler) на конкретный GPIO‑пин.Параметры:
gpio_num— номер пина (GPIO_NUM_0…GPIO_NUM_39)isr_handler— указатель на функцию-обработчик прерыванияargs— произвольный аргумент, который будет передан в функцию-обработчик
gpio_intr_enable(gpio_num);
Назначение: включает прерывания для указанного GPIO-пина.
Параметр
gpio_num— номер GPIO-пина.
Прерывание не сработает, если вы не вызвали эту функцию (или отключили его ранее).
gpio_intr_disable()- отключение прерывания.
// функция-обработчик прерывания void IRAM_ATTR gpio_isr_handler(void* arg) { }
IRAM_ATTR — атрибут (фактически декоратор), гарантирующий, что этот код попадёт в быстрый IRAM, а не во флеш.
Если у вас возник вопрос (ну вдруг ¯\_(ツ)_/¯): «Зачем нужен IRAM_ATTR, если уже есть ESP_INTR_FLAG_IRAM?», то я постараюсь ответить.
Флаг ESP_INTR_FLAG_IRAM обязывает систему использовать обработчик, лежащий в IRAM — но сам он не перемещает вашу функцию туда! Чтобы компилятор реально положил обработчик прерывания в IRAM, вы должны явно это указать — с помощью IRAM_ATTR.
Практика
Наша цель — реализовать прерывание от кнопки, которое будет изменять логический уровень светодиода. Для этого немного переработаем задачу Blink_Task, которую мы писали в самом начале:
uint8_t state = 0; // Переменная, хранящая текущее состояние LED (0 – выключен, 1 – включён) TaskHandle_t Blink_Handle = NULL; // Дескриптор задачи Blink // Задача, которая постоянно устанавливает уровень на LED_GPIO согласно state void Blink_Task(void *arg) { while (1) { gpio_set_level(LED_GPIO, state); } }
Осталось реализовать сам обработчик прерывания, который будет менять состояние state. Функция получится довольно компактной:
/* Обработчик прерывания от кнопки. Просто инвертирует переменную state. IRAM_ATTR — атрибут, гарантирующий, что этот код попадёт в быстрый IRAM, а не во флеш (важно для надёжности ISR). */ static void IRAM_ATTR gpio_isr_handler(void *arg) { state = !state; }
Теперь важно правильно зарегистрировать обработчик прерывания. Алгоритм действий следующий:
Задаём тип прерывания:
gpio_set_intr_type(gpio_num, intr_type);Инициализируем общий сервис обработки прерываний:
gpio_install_isr_service(intr_alloc_flags);Регистрируем обработчик прерывания для нужного пина:
gpio_isr_handler_add(gpio_num, isr_handler, args);Включаем прерывания для пина:
gpio_intr_enable(gpio_num);
#include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" #define LED_GPIO GPIO_NUM_2 // Пин, к которому подключён светодиод #define BUTTON_GPIO GPIO_NUM_23 // Пин, к которому подключена кнопка uint8_t state = 0; // Переменная, хранящая текущее состояние LED (0 – выключен, 1 – включён) TaskHandle_t Blink_Handle = NULL; // Дескриптор задачи Blink // Задача, которая постоянно устанавливает уровень на LED_GPIO согласно state void Blink_Task(void *arg) { while (1) { gpio_set_level(LED_GPIO, state); } } /* Обработчик прерывания от кнопки. Просто инвертирует переменную state. IRAM_ATTR — атрибут, гарантирующий, что этот код попадёт в быстрый IRAM, а не во флеш (важно для надёжности ISR). */ static void IRAM_ATTR gpio_isr_handler(void *arg) { state = !state; } void app_main() { esp_rom_gpio_pad_select_gpio(LED_GPIO); // "Переключение" выбранного физического контакта в режим GPIO gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT); // Настройка LED_GPIO как цифровой вывод gpio_set_level(LED_GPIO, 0); // сразу гасим esp_rom_gpio_pad_select_gpio(BUTTON_GPIO); // "Переключение" выбранного физического контакта в режим GPIO gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT); // Настройка BUTTON_GPIO как входа gpio_pullup_en(BUTTON_GPIO); // Подтяжка к VCC gpio_set_intr_type(BUTTON_GPIO, GPIO_INTR_POSEDGE); // Прерывание при нажатии (положительный фронт) gpio_install_isr_service(ESP_INTR_FLAG_IRAM); // Устанавливаем сервис ISR gpio_isr_handler_add(BUTTON_GPIO, gpio_isr_handler, NULL); // Регистрируем функцию-обработчик прерывания gpio_intr_enable(BUTTON_GPIO); // Включаем прерывания для кнопки // Создаём задачу, которая будет “мигать” LED в соответствии с state xTaskCreate( Blink_Task, // функция‑задача "BLINK", // имя (для отладки) 2048, // размер стека (в байтах) NULL, // параметр задачи 5, // приоритет &Blink_Handle // сюда запишется хэндл задачи ); }

На самом деле
gpio_intr_enable()можно не вызывать)Эта функция необязательна, если вы используете
gpio_isr_handler_add(), так как она внутри себя уже включает прерывание.
Заключение
Если вы заметили неточности, ошибки или у вас есть предложения по улучшению статьи — обязательно отпишитесь в диалоге или в комментариях. Я с радостью подкорректирую материал, чтобы он стал полезнее для всех, кто осваивает ESP-IDF.
Хотелось бы еще поговорить про очереди, но думаю в данной статье уже достаточно теории)
Продолжая изучение ESP-IDF, мы рассмотрим применение очередей для взаимодействия между задачами и ISR.
