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

Новое API I2C для ESP-IDF 5.2.0 и выше

Метки:

Добрый день, уважаемые читатели!

Когда-то я уже описывал на данном сайте работу с шиной Inter-Integrated Circuit ( I2C ) на ESP32 на данном сайте. I2C — это последовательный, синхронный, полудуплексный протокол связи, который позволяет нескольким ведущим и ведомым устройствам работать на одной шине. I2C использует две двунаправленные линии с открытым стоком: последовательную линию данных (SDA) и последовательную линию синхронизации (SCL), который должны быть подтянуты резисторами (от 1 кОм до 10 кОм, для ESP32 рекомендуется 2 кОм до 5 кОм). ESP32 имеет два контроллера I2C, каждый из которых может быть либо ведущим (master) либо ведомым (slave).

Но время идет, разработчики ESP-IDF работают, и в какой-то момент они “запилили” новую версию API для весьма популярной шины I2C. Об этом и поговорим в данной статье.

Начиная с версии ESP-IDF 5.2, структура API для I2C выглядит так, как это представлено на рисунке ниже. Уже знакомая нам библиотека i2c.h названа на схеме как “legacy API”, как видно из схемы, “обращается” напрямую к hardware abstraction layer. Старая версия не совместима с новой версией драйвера и их нельзя использовать одновременно. На момент написания статьи i2c.h ещё доступна программисту, но она признана устаревшей и будет удалена в новых релизах ESP-IDF. Поэтому, хотим ли мы этого или нет, но нам придется переходить на новую версию.

На рисунке слева как legacy API обозначено “старое” API, а правее – новое. Новое API I2C разделено на две отдельные библиотеки i2c_master.h и i2c_slave.h, а также общий модуль i2c_types.h, в котором объявлены необходимые общие типы данных. 

Чем же так сильно отличается новое API от прежнего? В первую очередь разработчики перешли с модели "драйвер шины" - "передача данных" на модель "шина" - "устройство".

 

В старом API сразу после инициализации шины мы могли сразу же начинать запрос передачу данных по шине на произвольный адрес путем создания виртуальных “команд”.  Но процесс передачи выглядел для программиста довольно “утомительно” – мы должны были вручную сформировать пакет передачи, включая стартовый и стоповый биты, биты подтверждения ACK, а потом запустить созданный пакет (команду) на выполнение.  Если очень кратко и опуская второстепенные детали, то процесс выглядел так:

// Настройка шины
i2c_config_t confI2C = {...};
i2c_param_config(I2C_NUM_0, &confI2C);

// Установка драйвера
i2c_driver_install(I2C_NUM_0, confI2C.mode, 0, 0, 0);

// Создаем контейнер для команд и формируем последовательность для передачи
i2c_cmd_handle_t cmdLink = i2c_cmd_link_create(I2C_NUM_0);
// Стартовый бит
i2c_master_start(cmdLink);
// Отправляем адрес и бит записи
i2c_master_write_byte(cmdLink, (i2c_address << 1) | I2C_MASTER_WRITE, ACK_CHECK_EN);
// Отправляем команду
i2c_master_write(cmdLink, cmds, cmds_size, ACK_CHECK_EN);
// Отправляем дополнительные данные
i2c_master_write(cmdLink, data, data_size, ACK_CHECK_EN);
// Стоп-бит
i2c_master_stop(cmdLink);

// Исполняем сформированный пакет команд
error_code = i2c_master_cmd_begin(I2C_NUM_0, cmdLink, pdMS_TO_TICKS(timeout));

Настройку конфигурации я опустил для краткости. Если интересно, вы можете ознакомиться с деталями здесь.

 

В новом API разработчики добавили ещё одну сущность – device (устройство), к которому при инициализации привязывается его адрес и тактовая частота SCL (а значит и скорость передачи данных). Таки да, теперь на одной и той же шине могут одновременно сосуществовать устройства с разной тактовой частотой SCL, например 100 кГц и 400 кГц. Кроме того, формирование всех служебных бит и прочей технической шелупони берет на себя само API. Теперь I2C API стало гораздо проще и ближе по духу к ардуиновским библиотекам – “бери и делай не вникая в детали реализации”. 

Если очень кратко и также опуская второстепенные детали, то новый процесс будет выглядеть уже так:

// Настройка и запуск шины I2C #0
i2c_master_bus_config_t i2c_master_config = {...};
i2c_master_bus_handle_t bus_handle;
i2c_new_master_bus(&i2c_master_config, &bus_handle); 

// Настройка slave-устройства
i2c_device_config_t i2c_device_config = {...};
i2c_master_dev_handle_t dev_handle;
i2c_master_bus_add_device(bus_handle, &i2c_device_config, &dev_handle);

// Записываем значение в slave
uint8_t value = 0x00;
i2c_master_transmit(dev_handle, &value, sizeof(value), pdMS_TO_TICKS(timeout));

Зацените красоту реализации! Ну а теперь обсудим все это более подробно. 

Примечание: поскольку лично я не использую I2С на ESP32 в slave-режиме (не вижу в этом особой необходимости, потому что для связи двух ESP между собой практичнее использовать modbus из-за “дальности”), поэтому в данной статье я буду рассматривать только master-режим. Прошу понять и простить.

 


Настройка шины в master-режиме

Для работы с шиной I2C нам придется заинклудить две IDF-ные библиотеки (в терминах ESP-IDF это “компоненты”):

#include "driver/i2c_types.h"
#include "driver/i2c_master.h"

 

Первое, что придется сделать – это настроить контроллер I2C. Напомню, ESP32 имеет на борту два контроллера, и если ранее мы могли их обозначать просто числами 0 и 1, то начиная с ESP-IDF 5.0 мы должны передавать API специальный тип i2c_port_num_t, который может принимать значение I2C_NUM_0 или I2C_NUM_1.

Параметры конфигурации шины определяются структурой i2c_master_bus_config_t:

// Задаем параметры шины I2C
i2c_master_bus_config_t i2c_master_config;
i2c_master_config.clk_source = I2C_CLK_SRC_DEFAULT; // Источник синхронизации для шины
i2c_master_config.i2c_port = I2C_NUM_0;             // Номер шины (I2C_NUM_0 или I2C_NUM_1)
i2c_master_config.scl_io_num = 22;                  // Номер GPIO для линии синхронизации SCL
i2c_master_config.sda_io_num = 21;                  // Номер GPIO для линии данных SDА
i2c_master_config.flags.enable_internal_pullup = 1; // Использовать встроенную подтяжку GPIO
i2c_master_config.glitch_ignore_cnt = 7;            // Период сбоя данных на шине, стандартное значение 7
i2c_master_config.intr_priority = 0;                // Приоритет прерывания: авто
i2c_master_config.trans_queue_depth = 0;            // Глубина внутренней очереди. Действительно только при асинхронной передаче

// Настраиваем шину
i2c_master_bus_handle_t bus_handle;
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_master_config, &bus_handle)); 
ESP_LOGI(logTAG, "I2C bus is configured");
  • clk_source – источник тактовых сигналов для контроллера шины, может принимать значения I2C_CLK_SRC_APB и I2C_CLK_SRC_DEFAULT = SOC_MOD_CLK_APB, что, в общем-то, без разницы.
  • i2c_port – здесь мы должный указать номер порта I2C_NUM_0 или I2C_NUM_1.
  • scl_io_num и sda_io_num – номера GPIO, которые будут использоваться для линий SCL и SDA передачи данных.
  • flags.enable_internal_pullup – разрешить ли использование встроенной “слабой” подтяжки выводов SCL и SDA. Обычно встроенной подтяжки недостаточно для работы шины на высокой частоте, так что рекомендуется использовать внешние резисторы от 2 до 5 килоом.
  • glitch_ignore_cnt – устанавливает длительность сбоя данных на шине. Если помех на линии меньше этого значения, их можно отфильтровать. По умолчанию равно 7.
  • intr_priority – уровень приоритета прерываний. Может принимать значения 0, 1, 2 или 3. Если установлено 0, то уровень приоритета прерываний будет выбран автоматически.
  • trans_queue_depth – Длина внутренней очереди данных. Действительно только при асинхронной передаче данных с использованием callback-ов. При блокирующей работе можно указать 0.

Затем просто передаем эту структуру в функцию i2c_new_master_bus(&i2c_master_config, &bus_handle). Вместе с параметрами мы должны передать указатель на буфер i2c_master_bus_handle_t, в который будет помещен указатель на созданный экземпляр шины. Как видите, теперь мы не настраиваем ни тактовую частоту передачи, как это было в старом API. И это все, можно переходить к конфигурированию устройства.

 


Настройка slave-устройства

Настройка slave-устройства выглядит не сильно сложнее:

// Задаем параметры slave-устройства
i2c_device_config_t i2c_device_config;
i2c_device_config.dev_addr_length = I2C_ADDR_BIT_LEN_7; // Используется стандартная 7-битная адресация
i2c_device_config.device_address = 0x20;                // Адрес устройства
i2c_device_config.scl_speed_hz = 100000;                // Тактовая частота шины 100kHz
i2c_device_config.scl_wait_us = 0;                      // Время ожидания по умолчанию
i2c_device_config.flags.disable_ack_check = 0;          // Не отключать проверку ACK

// Настраиваем устройство
i2c_master_dev_handle_t dev_handle;
ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &i2c_device_config, &dev_handle));
ESP_LOGI(logTAG, "Device is configured");

Параметры ведомого устройства определяются структурой i2c_device_config_t:

  • dev_addr_length – здесь можно указать длину адреса в битах для ведомого устройства, можно выбрать из I2C_ADDR_BIT_LEN_7 или I2C_ADDR_BIT_LEN_10.
  • device_address – необработанный адрес ведомого устройства. То есть без дополнительного бита записи/чтения.
  • scl_speed_hz –  тактовая частота обмена данными в Гц, например 100000 или 400000. Тактовая частота SCL в master режиме не должна превышать 400 кГц.
  • scl_wait_us – время ожидания SCL в микросекундах (например, если устройство “занято” и прижимает SCL к земле после запроса данных). Обычно это значение не должно быть слишком маленьким, поскольку обработка данных на подчиненном устройстве может занять довольно длительное время (возможно даже растяжение на 12 миллисекунд). Установка 0 означает использование значения по умолчанию.
  • flags.disable_ack_check – отключить проверку ACK. Если установлено значение false, это означает, что проверка ACK включена – при обнаружении NACK транзакция будет остановлена ​​и API вернет ошибку.

Затем передаем заполненную структуру в i2c_master_bus_add_device(bus_handle, &i2c_device_config, &dev_handle). Как и в предыдущем случае, функция в случае успеха вернет указатель (хэндл) на устройство, которое мы будет использовать в дальнейшем.

 


Передача данных

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

  • i2c_master_transmit – запись данных на подчиненное устройство
  • i2c_master_receive – чтение данных с подчиненного устройства
  • i2c_master_transmit_receive – отправить запрос на подчиненное устройство и прочитать ответные данные

Рассмотрим их немного поподробнее.

 

Запись данных

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

i2c_master_transmit (i2c_master_dev_handle_t i2c_dev, const uint8_t * write_buffer, size_t write_size, int xfer_timeout_ms)

  • i2c_dev – хэндл устройства, который мы получили на предыдущем этапе
  • write_buffer – указатель на буфер с данными готовыми к передаче. Это может быть указатель на простую переменную или массив байт
  • write_size – длина этих данных в байтах
  • xfer_timeout_ms – время ожидания передачи в миллисекундах, или -1 для бесконечного ожидания. Транзакция будет выполняться до тех пор, пока не завершится или не истечет указанный тайм-аут.

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

Например это может выглядеть так:

uint8_t value = 0x00;
i2c_master_transmit(dev_handle, &value, sizeof(value), -1);

Как видите, это сильно проще и короче, чем это было необходимо в предыдущей версии ESP-IDF.

 

Чтение данных

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

i2c_master_receive(i2c_master_dev_handle_t i2c_dev, uint8_t * read_buffer, size_t read_size, int xfer_timeout_ms)

  • i2c_dev – хэндл устройства, который мы получили на предыдущем этапе
  • read_buffer – указатель на буфер, куда будут помещены прочитанные данные. Это может быть указатель на простую переменную или массив байт
  • read_size – длина этого массива в байтах
  • xfer_timeout_ms – время ожидания передачи в миллисекундах, или -1 для бесконечного ожидания. Транзакция будет выполняться до тех пор, пока не завершится или не истечет указанный тайм-аут.

При этом данные по шине будут передаваться следующим образом:

Простейший пример получения одного байтика:

uint8_t value;
i2c_master_recieve(dev_handle, &value, sizeof(value), -1);

 

Запрос данных (отправка и чтение данных)

На практике гораздо чаще встречаются ситуации, когда мастер, запрашивая какие-либо данные, вначале отправляет ведомому команду, а зачем читает от него ответ. Конечно, можно воспользоваться комбинацией предыдущих способов, но есть готовая команда:

i2c_master_transmit_receive(i2c_master_dev_handle_t i2c_dev, const uint8_t * write_buffer, size_t write_size, uint8_t * read_buffer, size_t read_size, int xfer_timeout_ms)

  • i2c_dev – хэндл устройства, который мы получили на предыдущем этапе
  • write_buffer – указатель на буфер с данными готовыми к передаче. Это может быть указатель на простую переменную или массив байт
  • write_size – длина этих данных в байтах
  • read_buffer – указатель на буфер, куда будут помещены прочитанные данные. Это может быть указатель на простую переменную или массив байт
  • read_size – длина этого массива в байтах
  • xfer_timeout_ms – время ожидания передачи в миллисекундах, или -1 для бесконечного ожидания. Транзакция будет выполняться до тех пор, пока не завершится или не истечет указанный тайм-аут.

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

Например отправляем I2C-датчику команду чтения температуры и влажности и ждем результатов:

uint8_t bufCmd[3] = {0xAC, 0x33, 0x00};
uint8_t bufData[7] = {0, 0, 0, 0, 0, 0, 0};
i2c_master_transmit_recieve(dev_handle, &bufCmd[0], sizeof(bufCmd), &bufData[0], sizeof(bufData), -1);

 


Сканирование шины

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

i2c_master_probe( i2c_master_bus_handle_t bus_handle, uint16_t address, int xfer_timeout_ms)

  • bus_handle – хендл шины, на которой производится проба пера.
  • address – предполагаемый адрес устройства
  • xfer_timeout_ms – время ожидания передачи в миллисекундах, или -1 для бесконечного ожидания

Принцип работы этой функции заключается в отправке адреса устройства с помощью команды записи. Если устройство подключено к шине I2C, оно ответит битом ACK, и функция вернет ESP_OK. Если устройство не подключено, будет сигнал NACK, и функция вернет ESP_ERR_NOT_FOUND

Например поиск всех устройств на 7-битной шине выглядит так:

for (uint8_t i = 1; i < 128; i++) {
  if (i2c_master_probe(bus_handle, i, -1) == ESP_OK) {
    ESP_LOGI(logTAG, "Found device on bus 0 at address 0x%.2X", i);
  };
};

 


Пример 1. Мигаем светодиодами через PCF8574

В качестве простейшего примера я собрал такой примитивный макет из ESP32, PCF8574 и двух светодиодов (на самом деле можно от 1 до 8, мне было лень подключать больше):

Код для этого примера получился такой:

#include <stdio.h>
#include "esp_log.h"
// Подключаемые библиотеки
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_types.h"
#include "driver/i2c_master.h"

const char* logTAG = "I2C";

void app_main(void)
{
  // ==========================================================
  // Настройка и запуск шины I2C #0
  // ==========================================================
  // Задаем параметры шины I2C
  i2c_master_bus_config_t i2c_master_config;
  i2c_master_config.clk_source = I2C_CLK_SRC_DEFAULT; // Источник синхронизации для шины
  i2c_master_config.i2c_port = I2C_NUM_0;             // Номер шины (I2C_NUM_0 или I2C_NUM_1)
  i2c_master_config.scl_io_num = 22;                  // Номер GPIO для линии синхронизации SCL
  i2c_master_config.sda_io_num = 21;                  // Номер GPIO для линии данных SDА
  i2c_master_config.flags.enable_internal_pullup = 1; // Использовать встроенную подтяжку GPIO
  i2c_master_config.glitch_ignore_cnt = 7;            // Период сбоя данных на шине, стандартное значение 7
  i2c_master_config.intr_priority = 0;                // Приоритет прерывания: авто
  i2c_master_config.trans_queue_depth = 0;            // Глубина внутренней очереди. Действительно только при асинхронной передаче

  // Настраиваем шину
  i2c_master_bus_handle_t bus_handle;
  ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_master_config, &bus_handle)); 
  ESP_LOGI(logTAG, "I2C bus is configured");

  // ==========================================================
  // Сканирование шины (поиск устройств)
  // ==========================================================
  for (uint8_t i = 1; i < 128; i++) {
    if (i2c_master_probe(bus_handle, i, -1) == ESP_OK) {
      ESP_LOGI(logTAG, "Found device on bus 0 at address 0x%.2X", i);
    };
  };

  // ==========================================================
  // Настройка slave-устройства
  // ==========================================================
  // Задаем параметры slave-устройства
  i2c_device_config_t i2c_device_config;
  i2c_device_config.dev_addr_length = I2C_ADDR_BIT_LEN_7; // Используется стандартная 7-битная адресация
  i2c_device_config.device_address = 0x20;                // Адрес устройства
  i2c_device_config.scl_speed_hz = 100000;                // Тактовая частота шины 100kHz
  i2c_device_config.scl_wait_us = 0;                      // Время ожидания по умолчанию
  i2c_device_config.flags.disable_ack_check = 0;          // Не отключать проверку ACK

  // Настраиваем устройство PCF8574
  i2c_master_dev_handle_t dev_handle;
  ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &i2c_device_config, &dev_handle));
  ESP_LOGI(logTAG, "PCF8574 is configured");

  // ==========================================================
  // Основной цикл задачи
  // ==========================================================
  uint8_t value = 0x00;
  while (1) {
    // Замкнутый цикл, перебирающий все значения от 0x00 до 0xFF
    if (value < 0xFF) { value++; } else { value = 0x00; };
    // Записываем значение в PCF8574
    ESP_LOGI(logTAG, "PCF8574 write: 0x%.2X", value);
    i2c_master_transmit(dev_handle, &value, sizeof(value), -1);
    // Ожидание 100 мс
    vTaskDelay(pdMS_TO_TICKS(100));
  };
}

 

Все работает, светодиоды переключаются. Лог работы:

I (314) main_task: Started on CPU0
I (324) main_task: Calling app_main()
I (324) gpio: GPIO[21]| InputEn: 1| OutputEn: 1| OpenDrain: 1| Pullup: 1| Pulldown: 0| Intr:0 
I (324) gpio: GPIO[22]| InputEn: 1| OutputEn: 1| OpenDrain: 1| Pullup: 1| Pulldown: 0| Intr:0 
I (334) I2C: I2C bus is configured
I (344) I2C: Found device on bus 0 at address 0x20
I (364) I2C: PCF8574 is configured
I (364) I2C: PCF8574 write: 0x01
I (464) I2C: PCF8574 write: 0x02
I (564) I2C: PCF8574 write: 0x03 I (664) I2C: PCF8574 write: 0x04
I (764) I2C: PCF8574 write: 0x05
....

 


Пример 2. Читаем датчик AHT20

Хорошо, возьмем пример немного посложнее. Под горячую руку мне попался китайский AHT20, его и прочитаем.

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

  • Перед началом работы инициализировать его и загрузить калибровочные коэффициенты командой из трех байт 0xBE, 0x08, 0x00
  • Для измерения:
    • Отправляем команду  0xАС, 0x33, 0x00
    • Ждем ~75 мс, после чего читаем 1 байт. Если в этом байте установлен бит 0x80, то сенсор занят, ждем еще немного
    • Читаем 7 байт и декодируем результат

Очевидно, что командой i2c_master_transmit_recieve воспользоваться не удастся (ждать аж 75 миллисекунд!, да еще и статус проверять), поэтому я придумал такой алгоритм:

// Буферы под отправку команд и получение данных от сенсора
uint8_t status = 0;
uint8_t bufCmd[3] = {0, 0, 0};
uint8_t bufData[7] = {0, 0, 0, 0, 0, 0, 0};

// Настраиваем сенсор AHT20
bufCmd[0] = 0xBE; // Команда инициализации
bufCmd[1] = 0x08; // Загрузить калибровочные коэффициенты из EEPROM
bufCmd[2] = 0x00; // NOP control
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, &bufCmd[0], 3, -1));
ESP_LOGI(logTAG, "AHT20 is configured");
vTaskDelay(pdMS_TO_TICKS(100));

// ==========================================================
// Основной цикл задачи
// ==========================================================
while (1) {
  // Отправляем команду на измерение
  bufCmd[0] = 0xAC; // Запуск измерения
  bufCmd[1] = 0x33; // Подозреваю, что это разрешение ЦАП температуры и влажности
  bufCmd[2] = 0x00; // NOP control
  ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, &bufCmd[0], 3, -1));

  // Ожидание, пока AHT20 занят измерением ( >75 мс )
  vTaskDelay(pdMS_TO_TICKS(75));
  ESP_ERROR_CHECK(i2c_master_receive(dev_handle, &status, 1, -1));
  while ((status != 0xFF) && (status & 0x80)) {
    vTaskDelay(pdMS_TO_TICKS(5));
    ESP_ERROR_CHECK(i2c_master_receive(dev_handle, &status, 1, -1));
  };

  // Чтение результата: {status, RH, RH, RH+T, T, T, CRC}
  ESP_ERROR_CHECK(i2c_master_receive(dev_handle, &bufData[0], 7, -1));

  // Декодируем и выводим результаты измерений (!проверку CRC я для краткости опустил!)
  uint32_t hData = (((uint32_t)bufData[1] << 16) | ((uint16_t)bufData[2] << 8) | (bufData[3])) >> 4;
  float hValue = ((float)hData / 0x100000) * 100.0;

  uint32_t tData = ((uint32_t)(bufData[3] & 0x0F) << 16) | ((uint16_t)bufData[4] << 8) | bufData[5]; 
  float tValue = ((float)tData / 0x100000) * 200.0 - 50.0;

  ESP_LOGI(logTAG, "AHT20 temperature = %f, humidity = %f", tValue, hValue);

  // Ожидание 1000 мс
  vTaskDelay(pdMS_TO_TICKS(1000));
};

Проверку контрольной суммы для краткости я делать не стал, но в реальной эксплуатации я рекомендую всегда это делать. Остальной код остался почти без изменений, кроме адреса датчика – 0x38. Внимательные читатели могут найти еще и логическую ошибку, которая, впрочем, на нормальное чтение датчика не влияет.

Результаты работы приведены ниже:

I (314) main_task: Started on CPU0 
I (324) main_task: Calling app_main() 
I (324) gpio: GPIO[21]| InputEn: 1| OutputEn: 1| OpenDrain: 1| Pullup: 1| Pulldown: 0| Intr:0 
I (324) gpio: GPIO[22]| InputEn: 1| OutputEn: 1| OpenDrain: 1| Pullup: 1| Pulldown: 0| Intr:0 
I (334) I2C: I2C bus is configured
I (344) I2C: Found device on bus 0 at address 0x38
I (364) I2C: Slave device is configured
I (364) I2C: AHT20 is configured
I (534) I2C: AHT20 temperature = 25.597382, humidity = 42.304707
I (1604) I2C: AHT20 temperature = 25.577164, humidity = 42.292404
I (2674) I2C: AHT20 temperature = 25.591850, humidity = 42.308712
I (3744) I2C: AHT20 temperature = 25.602722, humidity = 42.271805
I (4814) I2C: AHT20 temperature = 25.584602, humidity = 42.252922
I (5884) I2C: AHT20 temperature = 25.581551, humidity = 42.231083
I (6954) I2C: AHT20 temperature = 25.580406, humidity = 42.219257

 


Асинхронный режим

Приведенные выше примеры работают в синхронном или, по другому – блокирующем режиме. То есть на время обмена данными с периферийным устройством поток выполнения задачи полностью приостанавливается. А если вы укажете, например, xfer_timeout_ms = -1, то задача может и хорошенько зависнуть.

Хотя I2C является синхронным протоколом связи, новое API также поддерживает асинхронный режим работы. Таким образом, API I2C становится неблокирующим интерфейсом передачи данных. В этом случае функции передачи данных i2c_master_transmit, i2c_master_recieve и i2c_master_transmit_recieve завершаются сразу же, без ожидания, а результаты их работы передаются через функцию обратного вызова (callback).

Для того, чтобы перевести I2C в асинхронный режим API необходимо “всего-лишь” создать и зарегистрировать должным образом обработчики ISR (Interrupt Service Routine),  путем вызова функции i2c_master_register_event_callbacks(). Регистрация callback-ов должна быть выполнена после i2c_master_bus_add_device, но до любых функций передачи данных (i2c_master_transmit, i2c_master_recieve и i2c_master_transmit_recieve).

i2c_master_register_event_callbacks(i2c_master_dev_handle_t i2c_dev, const i2c_master_event_callbacks_t * cbs, void * user_data)

  • i2c_dev – хэндл устройства
  • cbs – группа функций обратного вызова, которая на момент написания статьи включает всего один callback:
    •  i2c_master_callback_t on_trans_done;  /*!< I2C master transaction finish callback */
  • user_data – указатель на любые данные, которые могут быть переданы в функции обратного вызова

Прототип функции обратного вызова on_trans_done выглядит так:

typedef bool (*i2c_master_callback_t)(i2c_master_dev_handle_t i2c_dev, const i2c_master_event_data_t *evt_data, void *arg);

  • i2c_dev – хэндл устройства
  • evt_data – сюда будет помещен результат выполнения асинхронной передачи данных: I2C_EVENT_ALIVE или I2C_EVENT_DONE или I2C_EVENT_NACK или I2C_EVENT_TIMEOUT.
  • arg – при вызове callback-а сюда будет передан указатель на данные, которые были указаны в аргументе user_data при регистрации

Таким образом API I2C уведомляет ваше приложение о том, каким именно образом была завершена запущенная асинхронная транзакция. Предупреждение! На одной шине только одно устройство может использоваться для выполнения асинхронной транзакции.

Поскольку зарегистрированные функции обратного вызова вызываются в контексте прерывания, вы должны убедиться, что созданные вами callback-и выполняются минимально возможное время и не блокируются планировщиком FreeRTOS (например, внутри ISR-обработчиков допускаются вызовы других функций API FreeRTOS только  с ISR суффиксом). Кроме того, функции обратного вызова должны возвращать логическое значение, чтобы сообщить ISR, пробуждена ли она более высокоприоритетной задачей.

Отменить асинхронный режим можно в любой момент, вызвав i2c_master_register_event_callbacks() ещё раз, но с NULL в качестве второго аргумента.

 


Потокобезопасность

Почти все функции API I2C не являются потокобезопасными (кроме двух – i2c_new_master_bus()и i2c_new_slave_device())! А это значит, что не стоит пытаться обращаться к одной и той же шине из разных задач / потоков выполнения. И в самом деле, если две задачи попытаются одновременно писать в шину свои данные, то вместо упорядоченных команд мы получим “неудобоваримую кашу”.

Разделение “ресурса” I2c с помощью средств синхронизации (таких как мьютекс, например) не всегда решает проблему совместного доступа к одной и той же шине. Поэтому при создании реальных устройств следует стремиться к тому, чтобы физический доступ к одному контроллеру I2C имела одна единственная задача. Особенно это актуально для устройств, которым требуется передача больших порций данных – например дисплеев. 

 


Ссылки и связанные статьи

  1. Espressif – Inter-Integrated Circuit (I2C)
  2. Работа с шиной I2C на ESP32 и ESP-IDF версий 4.х – 5.1
  3. Шина I2C: принципы функционирования или зачем ещё тут нужны какие-то резисторы?
     

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


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

9 комментариев для “Новое API I2C для ESP-IDF 5.2.0 и выше”

  1. Прочитал с удовольствием, спасибо!

    Пара технических замечаний:

    1. Скорость шины I2C обычно составляет 100 или 400 (редко 3500) килобит в секунду (у Вас – МегаГерцы в первом упоминании скорости, хотя далее Вы используете КилоГерцы).
    2. Общепринятый термин “открытый коллектор”, хотя “открытый сток” тоже приемлем. Просто мне, старорежимному инженеру первый нравится больше 🙂

    1. Благодарю!
      С замечаниями согласен.
      По первому – поправил текст. Это я некорректно обозвал, да еще и ошибся в МГц. Большое спасибо.
      По второму… у нас, да, более привычно “открытый коллектор”. Но в английском уже давно прижилось “open drain”, все-таки наверное. оставлю как есть.

  2. Спасибо. с удовольствием читаю статьи.

    пара ошибок- где-то в начале – битОВ,

    Вместе с параметрами мы должны пере_дать указатель

  3. найти еще и логическую ошибку, которая, впрочем, нА нормальное чтение датчика не влияет.

  4. Для того, чтобы перевести I2C __????_____ API необходимо “всего-лишь” создать и зарегистрировать должным образом обработчики ISR (Interrupt Service Routine),

  5. Получаю от датчика:
    I (1148) sht21: I2C master bus initialized successfully
    I (1158) sht21: sht21 initialized successfully
    I (1158) main_task: Returned from app_main()
    data_arr 255 data_arr 255 data_arr 165 E (1238) ./main/sht21.c: read_sensor(152):
    E (1238) ./main/sht21.c: sht21_get_temperature(115):
    ESP_ERROR_CHECK failed: esp_err_t 0x109 (ESP_ERR_INVALID_CRC) at 0x400d8c1b
    .— 0x400d8c1b: Measurement_sht21_Task at C:/Projects/ESP-COD/LC_5/main/sht21.c:224 (discriminator 1)
    Я правильно понимаю ответ шины “data_arr 255 data_arr 255 data_arr 165” и следовательно либо датчик не исправен либо я подключил его не правильно.

  6. Спасибо за статью!
    А как быть когда требуется нестандартные действия на шине i2c? Например, вот так описана процедура сброса чипа EEPROM AT24C512 в его даташите: (a) Clock up to 9 cycles, (b) look for SDA high in each cycle while SCL is high and then (c) create a start condition as SDA is high. Детали сейчас не важны, важно что требуется выполнить нечто выходящее за рамки стандартных операций чтения/записи по i2c.
    Как тут быть? Я могу временно отключить пины SCK и SDA от I2C драйвера и “подрыгать” ими вручную? И если да, то как дальше снова их подключить к драйверу? Или придется деинсталлировать драйвер (i2c_master_bus_rm_device() и i2c_del_master_bus()) и настраивать все по новой? Буду благодарен любой информации!

    1. Да, хорошая задачка. У меня нет ответа.
      Интересно даже – почему производители чипа решили так выпендрится?

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

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