В уроке научимся управлять портами ввода-вывода с помощью функций API. Разработаем три учебных проектов.
Предыдущий урок Список уроков Следующий урок
В этом уроке рассмотрим самые востребованные операции с портами.
- Установка режима вывода:
- вход с различными вариантами подключения подтягивающего резистора;
- выход – активный или с открытым стоком.
- отключение вывода.
- Установка состояния выхода.
- Чтение состояния входа.
Работа с прерываниями по изменению состояния выводов, использование их в энергосберегающих режимах и т.п. будут рассмотрены в последующих уроках.
Функции API управления GPIO-портами.
Любой вывод может использоваться в качестве входного или выходного сигнала периферийного устройства микроконтроллера, т.е. в режиме альтернативной функции. Если мы собираемся применять его в качестве порта общего назначения, альтернативную функцию необходимо отключить.
Разработчики ESP32 не гарантируют, что после аппаратного сброса микроконтроллера все его выводы установятся в режим ввода-вывода. Поэтому операцию перевода нужных выводов в режим GPIO необходимо делать всегда.
Производится это функцией:
void gpio_pad_select_gpio(uint8_t gpio_num);
- gpio_num – номер вывода.
Номер вывода может быть задан числом, например 2, или символьной константой, например GPIO_NUM_2. Во втором случае программа лучше читается. Мы подчеркиваем, что используем порядковый номер вывода, а не его битовую маску.
Дальше необходимо определить направление работы вывода: вход или выход. Для этого существует функция:
esp_err_t gpio_set_direction(gpio_num_t gpio_num, gpio_mode_t mode);
- gpio_num – номер вывода;
- mode – направление. Может принимать значения:
- GPIO_MODE_INPUT – вход;
- GPIO_MODE_OUTPUT – активный выход;
- GPIO_MODE_OUTPUT_OD – выход в режиме открытого стока;
- GPIO_MODE_INPUT_OUTPUT – вход и выход;
- GPIO_MODE_INPUT_OUTPUT_OD – вход и выход с открытым стоком;
- GPIO_MODE_DISABLE – запрещен и вход, и выход.
Функция возвращает значения:
- ESP_OK – в случае успешного выполнения;
- ESP_ERR_INVALID_ARG – в случае неправильно заданных параметров.
Управление подтягивающим резистором производится функцией
esp_err_t gpio_set_pull_mode(gpio_num_t gpio_num, gpio_pull_mode_t pull);
- gpio_num – номер вывода;
- pull – режим подтягивающего резистора. Может принимать значения:
- GPIO_PULLUP_ONLY – подключен к питанию;
- GPIO_PULLDOWN_ONLY – подключен к земле;
- GPIO_PULLUP_PULLDOWN – подключен и к питанию, и к земле;
- GPIO_FLOATING – отключен.
Функция возвращает значения:
- ESP_OK – в случае успешного выполнения;
- ESP_ERR_INVALID_ARG – в случае неправильно заданных параметров.
Есть другой способ управления подтягивающим резистором - отдельные для каждого режима функции. Кому-то он покажется более удобным.
esp_err_t gpio_pullup_en(gpio_num_t gpio_num); - резистор подключен к питанию;
esp_err_t gpio_pullup_dis(gpio_num_t gpio_num); - резистор отключен от питания;
esp_err_t gpio_pulldown_en(gpio_num_t gpio_num); - резистор подключен к земле;
esp_err_t gpio_pulldown_dis(gpio_num_t gpio_num); - резистор отключен от земли.
Все функции имеют только один аргумент:
- gpio_num – номер вывода.
Еще существует функция, которая позволяет устанавливать режимы сразу для нескольких выводов. Одним ее вызовом можно конфигурировать, например 8ми разрядную шину данных.
esp_err_t gpio_config(const gpio_config_t * pGPIOConfig)
Номера выводов, их режимы заданы в элементах структуры pGPIOConfig.
typedef struct {
uint64_t pin_bit_mask; /* битовая маска вывода */
gpio_mode_t mode; /* направление вывода */
gpio_pullup_t pull_up_en; /* разрешение подтягивающего резистора к питанию */
gpio_pulldown_t pull_down_en; /* разрешение подтягивающего резистора к земле */
gpio_int_type_t intr_type; /* управление прерыванием */
} gpio_config_t;
- pin_bit_mask – это не номер, а маска выводов. Что позволяет задавать режим сразу нескольких выводов. Конечно, если у них одинаковая конфигурация.
Маска вывода задается, например 1<<2 или GPIO_SEL_2.
Сразу несколько выводов задаются выражением с логическим ИЛИ масок отдельных выводов.
pin_bit_mask = GPIO_SEL_2 | GPIO_SEL_5 | GPIO_SEL_10;
- mode – определяет направление вывода:
- GPIO_MODE_INPUT – вход;
- GPIO_MODE_OUTPUT – активный выход;
- GPIO_MODE_OUTPUT_OD – выход в режиме открытого стока;
- GPIO_MODE_INPUT_OUTPUT – вход и выход;
- GPIO_MODE_INPUT_OUTPUT_OD – вход и выход с открытым стоком;
- GPIO_MODE_DISABLE – запрещен и вход, и выход.
- pull_up_en – управляет подтягивающим резистором, подключенным к питанию:
- GPIO_PULLUP_DISABLE – резистор отключен;
- GPIO_PULLUP_ENABLE – резистор подключен.
- pull_down_en – управляет подтягивающим резистором, подключенным к земле.
- GPIO_PULLDOWN_DISABLE – резистор отключен;
- GPIO_PULLDOWN_ENABLE – резистор подключен.
Чтобы конфигурировать нужные выводы необходимо задать элементы структуры типа gpio_config_t и вызвать функцию gpio_config с указателем на структуру в качестве аргумента.
Например, следующие строки конфигурируют выводы 2, 5 и 10.
gpio_config_t conf_gpio; // объявление структуры конфигурации
conf_gpio .pin_bit_mask = GPIO_SEL_2 | GPIO_SEL_5 | GPIO_SEL_10; // выводы
conf_gpio.mode = GPIO_MODE_INPUT; // входы
conf_gpio.pull_up_en = GPIO_PULLUP_ENABLE; // резистор на питание
conf_gpio.pull_down_en = GPIO_PULLDOWN_DISABLE; // без резистора на землю
conf_gpio.intr_type = GPIO_PIN_INTR_DISABLE; // прерывание запрещено
gpio_config(&conf_gpio); // установка конфигурации
Осталось выяснить, как устанавливать состояние выходов и считывать состояние входов. Для этого есть две простые функции.
esp_err_t gpio_set_level(gpio_num_t gpio_num, uint32_t level)
Устанавливает выход номер gpio_num в состояние level.
Аргумент level – может иметь значения:
- 0 – низкий уровень;
- 1 – высокий уровень.
int gpio_get_level(gpio_num_t gpio_num)
Возвращает состояние вывода с номером gpio_num.
- 0 – низкий уровень;
- 1 – высокий уровень.
Пока все функции, которые нам необходимы. Даже без примеров все просто и понятно.
Структура программы ESP-IDF.
Структура программы в среде ESP-IDF не отличается от типичной структуры C-программ. Это последовательность директив препроцессора, описаний, определений, глобальных объектов и функций.
Единственная особенность – главная функция должна иметь имя app_main().
Давайте создадим первую программу, на примере которой рассмотрим структуру исходного кода и научимся работать с портами в режиме выходов. Во многом эта программа будет повторять пример blink. Но мы не будем копировать готовый код, а разработаем его осознанно.
Заставим светодиод, подключенный к выводу 2, мигать с частотой 2 раза в секунду.
Создадим новый проект. Последовательность действий описана в конце урока 3.
- В рабочем каталоге урока 5 я создал паку Lesson5_1.
- Копировал в нее содержимое папки шаблона проекта template из урока 3.
- Задал имя проекта Lesson5_1 в файлах CMakeLists.txt и Makefile.
- Открыл проект в VS Code.
- Удалил содержимое файла app_main.c. Оставил пустую страницу. Ее и будем заполнять исходным кодом.
Напишем название программы
// светодиод на плате мигает 2 раза в секунду
Подключим необходимые файлы библиотек.
#include <stdio.h>
Библиотека необходима нам, для использования функции printf. С помощь нее будем выводить данные в терминал для отладки. В нашей программе это сообщения о состоянии светодиода.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
Этими файлами подключим операционную систему реального времени FreeRTOS. О ней поговорим в последующих уроках. Это обширная тема. В данном приложении она нужна только для формирования временных задержек. Работать с таймерами мы еще не умеем, а FreeRTOS позволит нам использовать функцию vTaskDelay, которая останавливает выполнение задачи на заданное время.
#include "driver/gpio.h"
Эта директива необходима для подключения API драйвера управления GPIO. Без нее выше описанные функции управления портами работать не будут.
Определяем номер вывода, к которому подключен светодиод.
#define LED_GPIO GPIO_NUM_2 // вывод светодиода
Создаем главную функцию, внутри которой будет основной код.
void app_main(void)
{
}
Конфигурируем вывод светодиода как активный выход.
gpio_pad_select_gpio(LED_GPIO); // режим ввода-вывода
gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT); // активный выход
Остается создать бесконечный цикл, в котором зажигать и гасить светодиод с задержками 0,25 секунд.
while(1) {
gpio_set_level(LED_GPIO, 0); // светодиод погашен
printf("Светодиод погас\n");
vTaskDelay(250 / portTICK_PERIOD_MS); //задержка 0,25 сек
gpio_set_level(LED_GPIO, 1); // светодиод горит
printf("Светодиод горит\n");
vTaskDelay(250 / portTICK_PERIOD_MS); //задержка 0,25 сек
}
Проект можно загрузить по ссылке.
Компилируем, загружаем в FLASH, проверяем.
Модифицируем предыдущую программу. Добавим кнопку и реализуем алгоритм, при котором нажатие кнопки уменьшает частоту мигания светодиодов в 2 раза.
Я подключил кнопку между выводом 13 и сигналом GND.
Копировал предыдущий проект. Изменил имя на Lesson5_2.
Добавил определение вывода кнопки.
#define BUTTON_GPIO GPIO_NUM_13 // вывод кнопки
Конфигурировал вывод кнопки, как вход с подтягивающим резистором.
gpio_pad_select_gpio(BUTTON_GPIO); // режим ввода-вывода
gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT); // вход
gpio_pullup_en(BUTTON_GPIO); // резистор подключен к питанию
gpio_pulldown_dis(BUTTON_GPIO); // резистор отключен от земли
Осталось изменять временные задержки в зависимости от состояния входа кнопки.
if( gpio_get_level(BUTTON_GPIO) != 0 ) vTaskDelay(250 / portTICK_PERIOD_MS); //задержка 0,25 сек
else vTaskDelay(500 / portTICK_PERIOD_MS); //задержка 0,5 сек
Вот мой проект
Согласитесь, работать с портами ESP32 очень просто.
Функции работы с несколькими битами одновременно.
Существуют еще 4 функции недокументированные в API, которые позволяют значительно ускорить работу с портами.
Функции gpio_set_level и gpio_get_level позволяют устанавливать и считывать состояние только одного вывода. Но часто возникает необходимость работать с несколькими битами одновременно. Это не только ускоряет работу программы, но иногда бывает абсолютно необходимым. Например, считывать шину данных по стробу или устанавливать сигналы, временные параметры которых критичны к сдвигу начальной фазы.
uint32_t gpio_input_get(void);
Функция считывает и возвращает одним словом состояние выводов 0-31.
uint32_t gpio_input_get_high(void);
Функция считывает и возвращает состояние выводов 32-39.
Достаточно вызвать функцию, и мы получим 32 разрядное слово, каждый разряд которого соответствует состоянию выводов микроконтроллера.
void gpio_output_set(uint32_t set_mask, uint32_t clear_mask, uint32_t enable_mask, uint32_t disable_mask);
Функция устанавливает, сбрасывает состояние выводов 0-31, а также разрешает и запрещает их работу.
- set_mask – маска выводов, которые необходимо установить;
- clear_mask – маска выводов, которые необходимо сбросить;
- enable_mask – маска выводов, работу которых необходимо разрешить;
- disable_mask – маска выводов, работу которых необходимо запретить.
void gpio_output_set_high(uint32_t set_mask, uint32_t clear_mask, uint32_t enable_mask, uint32_t disable_mask);
Функция устанавливает, сбрасывает состояние выводов 32-39, а также разрешает и запрещает их работу. В остальном формат аналогичен предыдущей функции.
Кроме управления состоянием сигналов на выходах функции еще разрешают и запрещают их работу, что удобно при организации двунаправленных сигналов и шин данных.
Вот проект, в котором я реализовал предыдущую задачу с использованием функций gpio_input_get и gpio_output_set .
Программа получилась несколько сложнее. Результат от применения этих функций заметен только при работе с несколькими выводами портов.
В следующем уроке научимся работать с таймерами ESP32.
Предыдущий урок Список уроков Следующий урок