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

Работа с портами ввода-вывода GPIO из ESP-IDF

Метки:

Добрый день, уважаемый читатель! В этой статье обсудим методы работы со встроенными портами ввода-вывода GPIO в цифровом режиме.

Если вы создавали скетчи для Arduino IDE, то наверное, знаете, как осуществляется работа с GPIO для Arduino:

pinMode(10, OUTPUT); // Настраиваем PIN10 на выход
digitalWrite(10, HIGH); // Записываем в PIN10 высокий уровень

В данном случае pinMode настраивает порт ввода-вывода на режим “выход”, а digitalWrite служит для записи в ранее настроенный порт логической единицы (или нуля). Это унифицированные функции Arduino, которые “внешне” не зависят от аппаратной платформы, а вот их внутренняя реализация будет зависеть от того, какой микроконтроллер вы используете. Это позволяет сравнительно легко переносить код с одного микроконтроллера на другой без адаптации (на самом деле это не всегда прокатывает, но тем не менее, разработчики платформы Arduino к этому стремятся).

На ESP-IDF необходимости в такой унификации нет, поэтому используются более специфичные функции. Их мы сегодня и обсудим.

 

Какие выводы GPIO можно использовать для ввод и вывод

Давайте вспомним, какие выводы можно использовать на ESP32 (я буду рассматривать линейки ESP32-WROOM или ESP32-WROVER).

Чип ESP32 имеет 34 физических контакта GPIO. Каждая контактная площадка может использоваться как вход/выход общего назначения (GPIO) или может быть подключена к внутреннему периферийному сигналу. Мультиплексоры IO_MUX, RTC IO_MUX и матрица GPIO отвечают за маршрутизацию сигналов от периферийных устройств к контактам GPIO. Вместе эти системы обеспечивают гибко настраиваемый ввод-вывод.

Порты ввода-вывода для ESP32-DevKitC V4

  • На ввод и вывод для ESP32-WROOM можно смело использовать следующие 18 выводов GPIO: 4, 14, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33. Все указанные выводы имеют встроенные программно-подключаемые резисторы подтяжки 45 кОм (в документации это называется “слабая подтяжка”). Я буду называть эти порты “универсальными”, для простоты понимания. Некоторые из них можно назначить для использования различных интерфейсов: I2C, SPI и т.д. Примечания: для ESP32-WROVER выводы 16 и 17 использовать нельзя.
  • Выводы GPIO 34, 35, 36, 39 можно использовать только на ввод, и у них отсутствуют встроенные резисторы слабой подтяжки. На эти же выводы выведен канал ADC1.
  • Можно ещё использовать GPIO 0, 2, 5, 12, 15, но с учетом того, что их нельзя подтягивать ни к питанию, ни к земле при старте микроконтроллера.

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

Хочу обратить ваше внимание только на то, что, согласно спецификации, выводы GPIO допускают ток аж до 40 mA “высокого” уровня и до 28 mA “низкого” уровня. Это позволяет управлять различными слаботочными устройствами типа светодиодов напрямую, без применения коммутирующих транзисторов, нужно только учитывать напряжение на выводе не более 3.3В.

Более подробно об выводах микроконтроллера ESP32 вы можете прочитать в отдельной статье. Там же можно скачать достаточно удобную excel-таблицу для распределения выводов в своем проекте с учетом их особенностей. Если вам не хватает свободных GPIO, вы можете воспользоваться I2C расширителями GPIO, например PCF8574 или MCP23017.

Настройка порта GPIO

Для работы с портами GPIO необходимо подключить модуль “driver/gpio.h”:

# include "driver/gpio.h"

Как и в Arduino IDE, прежде чем начинать работу с GPIO, его нужно настроить (сконфигурировать). Каждый “универсальный” порт можно настроить:

  • направление: на вход или выход
  • слабая подтяжка (45 КОм) к +3,3В или к “земле”
  • прерывание (если необходимо)

ESP-IDF предлагает два метода конфигурации:

  • Пакетная конфигурация сразу нескольких портов: с помощью функции gpio_config (const gpio_config_t* pGPIOConfig). Эта функция позволяет за один вызов настроить все параметры для нескольких выбранных портов: направление, подтяжки, прерывания. Соответственно это чуть более быстрый способ.
  • С помощью набора функций gpio_set_directiongpio_set_pull_mode, и т.д. Это более “детализированные” функции и работают они только для одного выбранного порта. Я чаще использую именно этот способ, скорее всего “по привычке” (так как они более похожи на способ из Arduino IDE).

Выбор вывода для работы в режиме GPIO

Прежде всего необходимо настроить GPIO для использования в режиме ввода-вывода (так как GPIO на ESP32 могут быть использованы для разных целей с помощью мультиплексора выводов IO_MUX). Разработчики ESP32 не гарантируют, что после аппаратного сброса микроконтроллера все его выводы установятся в режим ввода-вывода. Поэтому операцию перевода нужных выводов в режим GPIO желательно делать всегда.

Сделать это можно с помощью функции:

esp_err_t gpio_reset_pin(gpio_num_t gpio_num)

где:

  • gpio_num – номер вывода GPIO

Эта функция настраивает IOMUX для этого вывода на работу с GPIO, включает встроенную слабую подтяжку и отключает режимы работы на вход и на выход (то есть устанавливается режим GPIO_MODE_DISABLE – см. следующий раздел).

Для этой же цели можно воспользоваться другой функцией (начиная с версии ESP-IDF 5.0 она считается устаревшей – прим.авт.), которая просто перенастраивает IOMUX для этого вывода на работу с GPIO: 

void gpio_pad_select_gpio(uint8_t gpio_num)

где:

  • gpio_num – номер вывода GPIO

Примечание: функция gpio_pad_select_gpio является устаревшей и в ESP-IDF 5.x не доступна.

 

Режимы работы портов ввода-вывода

ESP32 поддерживает несколько режимов GPIO:

  • GPIO_MODE_DISABLE – порт отключён
  • GPIO_MODE_INPUT – порт работает только на вход
  • GPIO_MODE_OUTPUT – порт работает только на выход
  • GPIO_MODE_OUTPUT_OD – порт работает только на выход в режиме “открытый сток” (open-drain)
  • GPIO_MODE_INPUT_OUTPUT_OD – порт может работать одновременно и на вход и на выход с открытым стоком
  • GPIO_MODE_INPUT_OUTPUT – порт может работать одновременно и на вход и на выход

В режимах с открытым коллектором (OD) микроконтроллер управляет только низким логическим уровнем, при установке на GPIO логической единицы вывод отключается и остается “болтаться в воздухе”. Этот режим удобно использовать для датчиков типа DHT11-22 или для управления светодиодами “по низкому уровню” (катодом к выводу, анодом к +3,3В).

Для выбора режима порта используйте функцию

esp_err_t gpio_set_direction(gpio_num_t gpio_num, gpio_mode_t mode)

где:

  • gpio_num – идентификатор GPIO
  • mode – режим работы

Переключить режим работы порта можно в любой момент, не обязательно делать это только при старте прошивки.

Встроенная подтяжка

Большинство портов имеют встроенные резисторы слабой подтяжки. Чтобы их задействовать, существует несколько функций.

Универсальная функция:

esp_err_t gpio_set_pull_mode(gpio_num_t gpio_num, gpio_pull_mode_t pull)

где:

  • gpio_num – идентификатор GPIO
  • pull – режим подтяжки

Режим gpio_pull_mode_t может принимать одно из нескольких значений:

  • GPIO_PULLUP_ONLY – подтяжка к питанию +3,3В
  • GPIO_PULLDOWN_ONLY – подтяжка к “земле”
  • GPIO_PULLUP_PULLDOWN – подтяжка одновременно к питанию +3,3В и “земле”
  • GPIO_FLOATING – подтяжка отключена

Однако можно использовать и несколько более простых функций:

Какой метод использовать – выбирайте на свой вкус.

Настройка допустимого выходного тока

Для GPIO, работающих на выход, ESP32 позволяет задать максимальный ток через выход. Это может быть полезно в некоторых случаях, например можно ограничить ток через светодиод без использования резистора. Для этого воспользуйтесь функцией:

esp_err_t gpio_set_drive_capability (gpio_num_t gpio_num , gpio_drive_cap_t strength)

где:

  • gpio_num – идентификатор GPIO
  • strength – максимально допустимый ток

Режим gpio_drive_cap_t может принимать одно из нескольких значений:

  • GPIO_DRIVE_CAP_0 – слабый, до ~5мА
  • GPIO_DRIVE_CAP_1 – сильнее, до ~10мА
  • GPIO_DRIVE_CAP_2 – средний (по умолчанию), до ~20мА
  • GPIO_DRIVE_CAP_3 – максимальный, до ~40мА

Как видите, по умолчанию ток высокого уровня ограничен на уровне 20мА. В большинстве случаев вызывать данную функцию при настройке порта не требуется. Но если ваше устройство на ESP32 требует большего тока (например при управлении мощным биполярным транзистором), то вы можете столкнуться с “неправильным” поведением. Как это работает на практике, вы можете узнать из небольшого следственного эксперимента.

 

Запись логического уровня в GPIO

Для записи данных в выходной порт необходимо воспользоваться функцией:

esp_err_t gpio_set_level(gpio_num_t gpio_num, uint32_t level)

где:

  • gpio_num – идентификатор GPIO
  • level – логический уровень, 0 (низкий) или 1 (высокий)

Ничего сложного, всё предельно просто.

Чтение логического уровня из GPIO

Для чтения данных из порта необходимо воспользоваться другой функцией:

uint32_t gpio_get_level(gpio_num_t gpio_num)

где:

  • gpio_num – идентификатор GPIO

Если GPIO не настроен для ввода (или ввода и вывода), возвращаемое значение всегда равно 0.

 

Практическое использование GPIO в режиме вывода – мигаем светодиодом

Допустим, мы хотим использовать светодиод, подключенный к одному из выводов через токоограничительный резистор, например это GPIO12. Классика жанра.

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

Светодиод можно подключить двумя способами:

  • С управлением по высокому уровню. Анод через резистор к GPIO, катод – к общему проводу. В этом случае следует использовать режим GPIO_MODE_OUTPUT.
  • С управлением по низкому уровню. Анод к +3,3В, катод через резистор к GPIO. В этом случае лучше использовать режим GPIO_MODE_OUTPUT_OD.

Для ESP32 наверное оптимальнее использовать первый способ, так как ток GPIO для высокого уровня может достигать до 40мА, а для низкого – только 28мА. Хотя для светодиода вполне достаточно 5~10мА, в крайнем случае – 20мА (для старых советских светодиодов зеленого цвета).

Настройка вывода будет выглядеть так:

// Переключаем вывод GPIO_NUM_12 в режим GPIO
gpio_reset_pin(GPIO_NUM_12);
// Настраиваем вывод GPIO_NUM_12 на выход без подтяжки
gpio_set_direction(GPIO_NUM_12, GPIO_MODE_OUTPUT);
gpio_set_pull_mode(GPIO_NUM_12, GPIO_FLOATING);

Для мигания светодиодов создадим задачу. Цикл задачи для мигания светодиодом в простейшем случае может выглядеть примерно так:

// Мигание светодиодом
while(1) {
  // Устанавливаем высокий уровень
  gpio_set_level(GPIO_NUM_12, 1);
  // Пауза 500 секунд
  vTaskDelay(pdMS_TO_TICKS(500));
  // Устанавливаем низкий уровень
  gpio_set_level(GPIO_NUM_12, 0);
  // Пауза 500 секунд
  vTaskDelay(pdMS_TO_TICKS(500));
};
 

Использование GPIO для пробуждения микроконтроллера

Если вы используете в своих разработках режим глубокого сна (например при питании от батарей), то вы можете использовать вывод GPIO для пробуждения микроконтроллера по внешнему сигналу. Задействовать данную функцию можно с помощью функции:

esp_err_t gpio_wakeup_enable (gpio_num_t gpio_num, gpio_int_type_t intr_type)

где:

  • gpio_num – идентификатор GPIO
  • intr_type – логический уровень на GPIO для пробуждения. Можно использовать только GPIO_INTR_LOW_LEVEL или GPIO_INTR_HIGH_LEVEL.

Отключить пробуждение можно с помощью функции gpio_wakeup_disable (gpio_num_t gpio_num).

Ну вот и всё, о чем я хотел рассказать в данной статье. Пример вы можете посмотреть на GitHub.

 

Прерывания по изменению уровня на GPIO

Прерывания по изменению уровня на GPIO мы обсудим в следующей статье, так как это отдельная большая тема.  А еще в одной статье вы можете познакомиться с обработкой нажатия кнопок на ESP32 по прерыванию и борьбой с дребезгом контактов.

 
 

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

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

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

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


Пожалуйста, оцените статью:
[ 5 из 5, всего 4 оценок ]

14 комментариев для “Работа с портами ввода-вывода GPIO из ESP-IDF”

    1. Хороший вариант, согласен. Не обращал внимания на эту функцию, спасибо. Надо будет дополнить статью.
      PS: В данном случае сработал обычный принцип: “Работает и ладно”. Как у кого-то когда-то прочитал, так до сих пор сам и делаю.

  1. #define GPIO_ENABLE_W1TS_REG *(volatile uint32_t*)0x3FF44024 // GPIO 0-31 output enable register_W1TS
    #define GPIO_ENABLE_W1TC_REG *(volatile uint32_t*)0x3FF44028 // GPIO 0-31 output enable register_W1TC

    #define LED_GPIO GPIO_NUM_12

    GPIO_OUT_W1TC_REG |= (1ULL<<LED_GPIO); //включаем индикатор (0 на выход)
    GPIO_OUT_W1TS_REG |= (1ULL<<LED_GPIO); //выключаем индикатор (1 на выход)

    1. Да, можно работать и напрямую через регистры.
      Но статья все таки для начинающих, а не для любителей погружения в глубины.
      Тогда уж программируйте на ассемблере )))

    2. Алексей

      А можно ссылку где об этом почитать. А то у меня выдает ошибку на GPIO_OUT_W1TC_REG

    3. Категорически приветствую. 🙂
      #define GPIO_ENABLE_W1TS_REG *(volatile uint32_t*)0x3FF44024 // GPIO 0-31 output enable register_W1TS
      #define GPIO_ENABLE_W1TC_REG *(volatile uint32_t*)0x3FF44028 // GPIO 0-31 output enable register_W1TC

      #define LED_GPIO GPIO_NUM_12

      GPIO_OUT_W1TC_REG |= (1ULL<<LED_GPIO); //включаем индикатор (0 на выход)
      GPIO_OUT_W1TS_REG |= (1ULL<<LED_GPIO); //выключаем индикатор (1 на выход)
      Это регистры WO (write-only), но почему-то все/большинство хотят из них читать, потом модифицировать прочтенное, а потом уже записать обратно. Здесь достаточно написать:
      GPIO_OUT_W1TC_REG = (1ULL<<LED_GPIO); //включаем индикатор (0 на выход)
      GPIO_OUT_W1TS_REG = (1ULL<<LED_GPIO); //выключаем индикатор (1 на выход)
      А запись выше была бы справедлива для регистра GPIO_OUT_REG, который W/R.
      Кроме того, писать можно прямо вот так, без начальных макросов:
      GPIO.out_w1tc = 1ULL << LED_GPIO; //включаем индикатор (0 на выход)
      GPIO.out_w1ts = 1ULL << LED_GPIO; //выключаем индикатор (1 на выход)
      предварительно добавив заголовочный файл "soc/gpio_struct.h"

  2. Не глубины ради – удобства для! ))
    Вдруг кому-то понадобится разом включить/выключить несколько выводов… А тут по маске бац – и готово! Дешево и сердито.))
    Кстати, подборка статей замечательная – для новичков то, что нужно! Простым языком и всё доходчиво написано. Читаю с большим удовольствием. Пожалуйста не забрасывайте это дело!
    Удачи!

  3. Отличная статья для начинающих, спасибо!!!

    Если я верно понял, то в практическом примере с мигающим светодиодом, необходимо вначале инициализировать пин функцией gpio_reset_pin() или gpio_pad_select_gpio()? Прежде чем вызывать gpio_set_direction() и gpio_set_pull_mode(). И ещё, кстати, сейчас вместо функции gpio_pad_select_gpio() вроде как надо использовать esp_rom_gpio_pad_select_gpio(). Причём в официальной документации ESP-IDF ни той ни другой функции поиск не находит, что странно. Это недостаток именно документации или намёк на то, что эти функции использовать напрямую нежелательно?

    1. Да, прежде чем использовать любой вывод как GPIO желательно перевести его в этот режим с помощью gpio_reset_pin().
      Раньше можно было использовать gpio_pad_select_gpio(), и она очень часто встречается в устаревших примерах в интернете. Но с 5 версии она больше не доступна, вместо этого следует использовать только gpio_reset_pin().

  4. Юрий Александрович Луняка

    Здравствуйте, что это?Делитель напряжения получается, на входе примерно 3.3/2 вольта получается,зачем это :
    GPIO_PULLUP_PULLDOWN – подтяжка одновременно к питанию +3,3В и “земле”

  5. SOS
    Вопрос не по теме.
    Не могу специалиста написать для STM32F107RCT6 прошивку с поддержкой протоколов J2534, CAN, с меткой для идентификации адаптера, драйвер и DLL

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

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