Низкоуровневые функции esp_rom_gpio Печать
Добавил(а) microsin   

В ПЗУ некоторых микроконтроллеров ESP присутствуют низкоуровневые API-функции, с помощью которых в SDK ESP-IDF реализована возможность прямого подключения периферийных сигналов к выводам портов GPIO через матрицу переключений (GPIO Matrix), минуя стандартные драйверы. Их основное назначение — тонкая настройка и работа в специфичных сценариях.

Описание функций esp_rom_gpio_* (находится в заголовочном файле esp_rom_gpio.h SDK ESP-IDF):

/**
* @brief Конфигурирует IO Pad как General Purpose IO,
* так что его можно подключить к внутренней Matrix,
* когда происходит комбинация с одним или несколькими
* сигналами периферийных устройств.
*
* @param iopad_num Номер ножки порта (IO Pad number)
*/
void esp_rom_gpio_pad_select_gpio(uint32_t iopad_num);

/**
* @brief Разрешит внутренный pull up и запретит внутренний pull down.
*
* @param iopad_num Номер ножки порта (IO Pad number)
*/
void esp_rom_gpio_pad_pullup_only(uint32_t iopad_num);

/**
* @brief Отменит состояние удержания (hold) для IO Pad.
* @note Когда Pad установлен на hold, его состояние защелкивается
* в этот момент, и не может быть впоследствии изменено.
* Функция esp_rom_gpio_pad_unhold снимает эту блокировку.
*
* @param iopad_num Номер ножки порта (IO Pad number)
*/
void esp_rom_gpio_pad_unhold(uint32_t gpio_num);

Что такое hold и unhold. Функция esp_rom_gpio_pad_unhold связана с управлением внутренним "захватом" (hold) состояния GPIO-пина. Чтобы понять её работу, нужно разобраться с концепцией Hold.

Hold — это аппаратный механизм в ESP32, который фиксирует (защёлкивает) текущее состояние ножки порта и игнорирует любые попытки изменить его программно или через сигналы периферийных устройств. Это похоже на "заморозку" состояния пина. Часто эту функцию используют для сохранения выходного уровня вывода в режиме сна.

Когда ножка порта находится в режиме Hold:

- Его выходной уровень (если он настроен как выход) не изменится, даже если вызвать gpio_set_level.
- Его конфигурация направления (вход/выход) также фиксируется.
- Он перестаёт реагировать на сигналы от периферийных модулей (UART, SPI и т.д.).

Типичные сценарии использования Hold:

1. Режимы глубокого сна: сохранение состояния уровеней сигналов на выходах при переходе в сон (sleep, режим пониженного энергопотребления).
2. Отладка: фиксация определённого состояния для измерений.
3. Безопасность: предотвращение случайного изменения критичных выводов сигналов.

Как работает esp_rom_gpio_pad_unhold. Эта функция снимает режим Hold с указанной ножки порта GPIO. После её вызова:

1. Вывод порта снова начинает реагировать на программные команды.
2. Снова можно изменять его уровень через gpio_set_level.
3. Периферийные модули могут управлять уровнями на выводе GPIO.
4. Возвращается нормальное поведение GPIO.

Соответствующие функции в ESP-IDF. Вместо использования низкоуровневых ROM-функций, в ESP-IDF есть более безопасные и документированные аналоги:

ROM-функция Стандартный аналог в ESP-IDF Описание
esp_rom_gpio_pad_hold_en gpio_hold_en(gpio_num) Активировать hold для вывода порта GPIO
esp_rom_gpio_pad_hold_dis gpio_hold_dis(gpio_num) Деактивировать hold для вывода порта GPIO
esp_rom_gpio_pad_unhold gpio_hold_dis(gpio_num) Снять hold (синоним)

Пример использования:

#include "driver/gpio.h"

#define LED_GPIO GPIO_NUM_2

void hold_example(void) {
// Настроим вывод порта как выход
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << LED_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = 0,
.pull_down_en = 0,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io_conf);

// Включим светодиод
gpio_set_level(LED_GPIO, 1);

// Включим режим Hold - теперь состояние выхода порта фиксировано
gpio_hold_en(LED_GPIO);

// Эта команда НЕ изменит состояние вывода, пока активен Hold
gpio_set_level(LED_GPIO, 0); // Ничего не произойдет!

// Отключим Hold (аналог esp_rom_gpio_pad_unhold)
gpio_hold_dis(LED_GPIO);

// Теперь команда сработает
gpio_set_level(LED_GPIO, 0); // Светодиод погаснет

// Для режимов глубокого сна есть специальные функции:
gpio_deep_sleep_hold_en(); // Включить hold для всех настроенных
// выводов порта в глубоком сне.
gpio_deep_sleep_hold_dis(); // Отключить hold для всех выводов. }

Важные особенности Hold:

1. Этот режим сохраняется только до момента сброса. При сбросе чипа режим Hold также сбрасывается.
2. Не влияет на входные сигналы: режим Hold фиксирует только выходное состояние.
3. Работает с RTC GPIO: для выводов RTC (например, у ESP32-S3 это выводы GPIO 0 .. 21) состояние сохраняется даже в глубоком сне.
4. Конфликт с другими функциями: если вывод порта используется периферией, включение на нем Hold может привести к неожиданному поведению.

Используйте функции управления Hold, когда вам нужно:

- Сохранить состояние выходов кристалла при переходе в глубокий сон.
- Зафиксировать конфигурацию для отладки.
- Предотвратить случайные изменения сигналов в критических секциях кода.
- Реализовать энергоэффективные схемы управления.

Пример с глубоким сном:

void prepare_for_deep_sleep(void) {
// Настроить выводы:
gpio_set_level(GPIO_NUM_12, 1); // Включить что-то
gpio_set_direction(GPIO_NUM_13, GPIO_MODE_OUTPUT);

// Включить hold для сохранения состояния в глубоком сне
gpio_hold_en(GPIO_NUM_12);
gpio_hold_en(GPIO_NUM_13);

// Включить общий режим сохранения в глубоком сне
gpio_deep_sleep_hold_en();

// Перейти в глубокий сон
esp_deep_sleep_start(); }

Рекомендации:

1. Используйте стандартные API-функции ESP-IDF (gpio_hold_en/gpio_hold_dis`) вместо ROM-функций.
2. Всегда отключайте Hold, когда он больше не нужен.
3. Проверяйте, что вывод порта не используется другими задачами при включении Hold.
4. Для отладки можете временно фиксировать состояние выводов, но не забывайте снимать Hold.

/**
* @brief Установит нагрузочную способность IO Pad.
*
* @param iopad_num Номер ножки порта (IO Pad number).
* @param drv Числовой код нагрузочной способности выхода.
* - 0: 5mA
* - 1: 10mA
* - 2: 20mA
* - 3: 40mA
*/
void esp_rom_gpio_pad_set_drv(uint32_t iopad_num, uint32_t drv);

/**
* @brief Привязка входа GPIO к сигналу периферийного устройства, который
* помечен атрибутом входа.
*
* @note Нет ограничений на количество сигналов, которые могут быть
* скомбинированы (подключены к) на GPIO.
*
* @param gpio_num Номер ножки порта GPIO. В этом параметре могут использоваться
* специальные значения для входных сигналов периферифного устройства:
* GPIO_MATRIX_CONST_ZERO_INPUT означает подачу лог. 0 в качестве входного сигнала
* GPIO_MATRIX_CONST_ONE_INPUT означает подачу лог. 1 в качестве входного сигнала.
* @param signal_idx Индекс сигнала периферийного устройства (помеченного атрибутом
* входа). В качестве этого параметра обычно используют макрос
* имяперифустройства_PERIPH_SIGNAL(IDX, PIN), например для UART
* это макрос UART_PERIPH_SIGNAL(IDX, PIN).
* @param inv Нужно ли инвертировать сигнал входа GPIO.
*/
void esp_rom_gpio_connect_in_signal(uint32_t gpio_num, uint32_t signal_idx, bool inv);

/**
* @brief Привязка выхода GPIO к сигналу периферийного устройства, который
* помечен атрибутом выхода.
*
* @note Нет ограничений на количество сигналов, которые могут быть
* скомбинированы (подключены к) на GPIO.
* @note Внутри микроконтроллера сначала будет подключен сигнал, и потом
* на выводе GPIO будет разрешен выход.
*
* @param gpio_num Номер ножки порта GPIO.
* @param signal_idx Индекс сигнала периферийного устройства (помеченного атрибутом
* выхода). В частности SIG_GPIO_OUT_IDX означает отключение GPIO
* и других периферийных устройств. Только драйвер GPIO может
* управлять выходным уровнем.
* @param out_inv Должен ли быть инвертирован сигнал выхода.
* @param oen_inv Должен ли быть инвертирован сигнал управления разрешения выхода.
*/
void esp_rom_gpio_connect_out_signal(uint32_t gpio_num, uint32_t signal_idx,
bool out_inv, bool oen_inv);

Функции esp_rom_gpio_connect_in_signal и esp_rom_gpio_connect_out_signal позволяют напрямую управлять маршрутизацией сигналов внутри ESP32 между периферийными модулями (например, UART, SPI) и физическими выводами GPIO.

esp_rom_gpio_connect_out_signal(gpio_num, signal_idx, out_inv, oen_inv) подключает выходной сигнал от периферийного модуля к GPIO. Эта функция напрямую конфигурирует регистры матрицы, чтобы выбранный периферийный сигнал управлял уровнем на указанном выводе порта. В качестве параметра signal_idx можно использовать специальное значение SIG_GPIO_OUT_IDX для отключения сигнала периферийного устройства и передачи управления пином обратно в драйвер GPIO.

esp_rom_gpio_connect_in_signal(gpio_num, signal_idx, in_inv) подключает входной сигнал с GPIO к входу периферийного модуля.

Важное предупреждение: эти функции являются частью внутренней "ROM" библиотеки (ESP-IDF Programming Guide). Это API не гарантирует стабильности между версиями ESP-IDF, и его следует использовать с осторожностью, только при полном понимании последствий и тщательном тестировании. В частности, эти функции могут поддерживаться не всеми микроконтроллерами семейства ESP, или могут работать по-разному в зависимости от модели или ревизии кристалла.

[Ключевые моменты и особенности применения]

Можно выделить несколько важных практических аспектов:

1. Отсутствие обратной (комплементарной) функции: ответ разработчика в официальном репозитории указывает на то, что не существует стандартной функции esp_rom_gpio_disconnect_out_signal(). Чтобы "отключить" сигнал, нужно либо переконфигурировать вывод с помощью gpio_config(), либо использовать специальный индекс SIG_GPIO_OUT_IDX в функции esp_rom_gpio_connect_out_signal, как описано выше.

2. Побочный эффект при подключении: в одном из отчетов об ошибке (errata) описано, что вызов esp_rom_gpio_connect_out_signal() для вывода, сконфигурированного как вход с подтяжкой, может вызвать ложный фронт сигнала на этом входе (триггер прерывания). Это происходит потому, что функция, вероятно, сначала включает выход на выводе с уровнем по умолчанию, прежде чем на него начнет поступать периферийный сигнал.

3. Стандартная альтернатива: для большинства задач конфигурацию выводов под периферию (например, UART, SPI, I2C) следует выполнять через соответствующие высокоуровневые драйверы ESP-IDF. Например, для UART используется функция uart_set_pin(), которая внутри безопасно управляет матрицей GPIO. Эти драйверы гарантируют корректность и стабильность работы.

[Пример практического использования]

Следующий пример показывает, как можно использовать esp_rom_gpio_connect_out_signal для создания режима 3-wire у SPI, где линии MISO и MOSI объединены, а также как это "отключить".

#include "esp_rom_gpio.h"
#include "driver/spi_master.h"
#include "soc/gpio_sig_map.h" // Для получения signal_idx

// Допустим, мы хотим объединить MISO и MOSI на одном выводе (GPIO_NUM_13)
// для работы с устройством, поддерживающим 3-проводной режим.
void setup_spi_3wire_mode(spi_host_device_t host) {
// 1. Настроим MOSI (GPIO_NUM_13) стандартным образом через драйвер SPI.
// ... (обычная инициализация SPI)

// 2. Определим индекс выходного сигнала для MOSI данного хоста SPI.
// Например, для SPI2_HOST сигнал может называться FSPID_OUT_IDX.
// Конкретное имя нужно искать в soc/gpio_sig_map.h.
int mosi_signal_idx = FSPID_OUT_IDX; // Это пример!

// 3. Подключим этот же сигнал MOSI к выводу порта MISO (например, GPIO_NUM_12).
// Теперь данные на передачу будут появляться и на выводе MISO.
esp_rom_gpio_connect_out_signal(GPIO_NUM_12, mosi_signal_idx, false, false);

// Важно: Пин GPIO_NUM_12 теперь управляется SPI. Не используйте для него gpio_set_level. }

void disable_spi_3wire_mode(void) {
// Чтобы "отключить" MISO вывод от SPI и вернуть его в управление GPIO:
esp_rom_gpio_connect_out_signal(GPIO_NUM_12, SIG_GPIO_OUT_IDX, false, false);
// Теперь уровнем на GPIO_NUM_12 можно снова управлять через gpio_set_level. }

Имеет смысл использовать функции esp_rom_gpio_* только если вы точно знаете, зачем это нужно. Эти функции полезны для отладки, создания нестандартных режимов работы периферии (как в примере выше), или в экстремальных условиях (например, в коде пробуждения, wake stub). Для 99% прикладных задач стандартные API драйверов периферии (uart_set_pin, spi_bus_initialize и т.д.) являются правильным и безопасным выбором.

[Ссылки]

1. ESP32-S3 GPIO & RTC GPIO site:docs.espressif.com.