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

Добрый день, уважаемый читатель! В данной статье я поделюсь информацией, как работать с шиной I2C на ESP32 из Espressif IoT Development Framework (ESP-IDF).

I2C — это последовательный, синхронный, полудуплексный протокол связи, который позволяет сосуществовать нескольким ведущим и ведомым устройствам на одной шине. Шина I2C состоит из двух линий: последовательной линии данных (SDA) и последовательной синхронизации (SCL). Обе линии требуют подтягивающих резисторов. Ранее я уже немного касался теоретических и физических аспектов работы с интерфейсом I2C, кто еще не успел ознакомиться – прошу прочитать предыдущую статью: Интерфейс I2C: принципы функционирования или зачем ещё тут нужны какие-то резисторы?

 

ESP32 имеет два контроллера I2C (также называемых портами), отвечающих за управление связью по шине I2C. Оба порта вполне могут одновременно работать как ведущие, и таким образом вы сможете в два раза больше получать молока подключить сенсоров к своему устройству без применения аппаратных мультиплексоров.

Как вариант можно настроить один контроллер I2C как ведущий, а другой как ведомый. Это позволит легко и просто соединить два или несколько (до 128)  микроконтроллеров в некое подобие “локальной” сети с простым протоколом обмена данными. Однако физические расстояния в этой сети не могут быть очень большими.

Выводы GPIO вы также можете назначить любые благодаря встроенному в ESP32 мультиплексору IOMUX.


Инициализация шины

Прежде чем вы начнете пересылать по шине I2C какие-либо данные, необходимо настроить драйвер контроллера и активировать его.

Конфигурация

Первым делом необходимо настроить параметры контроллера: режим работы (master / slave), выводы GPIO для SDA и SCL и другие параметры. Для этого мы должны заполнить структуру i2c_config_t, которая выглядит следующим образом:

Настройки контроллера I2C

Настройки контроллера I2C

Обязательные параметры

Следующие параметры вы должны указать в любом режиме работы:

  • Режим работы mode: I2C_MODE_MASTER или I2C_MODE_SLAVE.
  • Номера GPIO для сигнальных линий SDAsda_io_num и для SCLscl_io_num. Обратите внимание, здесь номера GPIO должны быть заданы в виде обычных чисел int (а не gpio_num_t, как в функциях работы с GPIO)
  • Использовать ли встроенную слабую подтяжку к питанию weak pullup для сигнальных линий – sda_pullup_en и scl_pullup_en соответственно. Я никогда не использую эту возможность, ибо для встроенные резисторы подтяжки имеют сопротивление около 50 килоОм, а это слишком много для шины I2C.

Параметры для режима MASTER

Если вы указали режим работы mode = I2C_MODE_MASTER, то дополнительно потребуется указать:

  • частоту шины clk_speed, её необходимо указывать в Гц (герцах). То есть для стандартной скорости 100кГц необходимо записать 100000. На текущий момент нельзя задать частоту шины выше 1МГц. При этом следует учитывать, что частота таковых импульсов на SCL может достаточно сильно отличаться от заданной, это связано с какими-то особенностями реализации контроллеров шины на ESP32, и обсуждалось на форуме esp32.com (однажды случайно наткнулся на тему).

Параметры для режима SLAVE

Для настройки режима ведомого mode = I2C_MODE_SLAVE необходимо в три раза больше параметров.

  • Вы должны определить, будет ли поддерживать ваш контроллер 10-битные адреса – addr_10bit_en. Насколько я понимаю, в этом случае все другие устройства, подключенные к этой же шине, так же должны иметь 10-битные адреса (впрочем, я могу и ошибаться).
  • Указать адрес slave_addr вашего устройства на шине.
  • Дополнительно можно задать ожидаемую тактовую частоту на линии SCL – maximum_speed, но можно игнорировать этот параметр.

Настройка источника тактовых сигналов для шины

Дополнительно в обоих режимах работы можно задать параметр clk_flags, который отвечает за настройку источника таковых сигналов для SCL. Можно использовать следующие биты:

  • I2C_SCLK_SRC_FLAG_FOR_NOMAL (0) – может быть выбран любой источник синхронизации, доступный для указанной частоты. По умолчанию обычно используется генератор (часы) APB 80MHz.
  • I2C_SCLK_SRC_FLAG_AWARE_DFS (1 << 0) – не изменять частоту шины, если меняется частота генератора APB.
  • I2C_SCLK_SRC_FLAG_LIGHT_SLEEP (1 << 1) – задействовать источник таковых сигналов, доступный в режиме легкого сна. Про умолчанию генератор APB не работает в режиме легкого сна, следовательно и шина I2C будет так же не доступна.

Укажите 0 для использования источника таковых сигналов и настроек “по умолчанию”, этого достаточно в большинстве случаев.

Когда всё готово, необходимо загнать эти данные в выбранный контроллер с помощью функции:

esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t *i2c_conf)

где:

  • i2c_port_t i2c_num – номер шины (контроллера), может принимать два значения 0 или 1
  • i2c_config_t *i2c_conf – указатель на только что заполненную структуру конфигурации контроллера. Такой прием используется в ESP-IDF сплошь и рядом.

В ответ функция должна вернуть вам ESP_OK, если это не так – значит вы где-то ошиблись при указании какого-либо параметра.

Установка драйвера

После того, как драйвер контроллера I2C настроен, установите его, вызвав функцию i2c_driver_install() со следующими параметрами:

  • i2c_port_t i2c_num – номер шины (контроллера) – 0 или 1
  • i2c_mode_t mode – режим работы I2C_MODE_MASTER или I2C_MODE_SLAVE (да, да, ещё раз)

Для ведомого (slave) режима потребуется указать ещё несколько параметров:

  • size_t slv_rx_buf_len и size_t slv_tx_buf_len – размеры буфера для отправки данных по запросу ведущего. Поскольку I2C является шиной, ориентированной на ведущее устройство, данные могут передаваться от ведомого устройства к ведущему только по запросу ведущего. Для master режима нам не нужно использовать буфер для данных, API-интерфейсы будут выполнять основные команды и возвращать управление потоком выполнения программы после отправки всех команд или при возникновении ошибки (поэтому, когда мы отправляем мастер-команды, мы должны освобождать или изменять исходные данные только после возврата функции i2c_master_cmd_begin). Напротив, для slave режима нам нужен буфер данных для хранения отправляемых и получаемых данных, потому что аппаратный fifo имеет только 32 байта. Следовательно, ведомое устройство должно иметь буфер отправки, куда ведомое приложение записывает запрошенные мастером данные. Данные остаются в буфере отправки и могут быть прочитаны мастером по его запросу. Буферы, насколько я понимаю, будут выделены из общей динамической кучи (heap).
  • int intr_alloc_flags – опции (флаги) для обработчика прерывания шины.

Для master режима просто отправьте нули в каждом из этих аргументов.

Пример настройки и запуска шины в master-режиме

Пример настройки и запуска шины в master-режиме

Как и в предыдущем случае, i2c_driver_install() должна вернуть ESP_OK, если драйвер был успешно установлен.

Во время установки драйвера по умолчанию устанавливается обработчик прерываний. Поэтому ваш вызов gpio_install_isr_service() после установки драйвера I2C скорее всего вернет ESP_ERR_INVALID_STATE, что означает, что обработчик прерываний уже был установлен.

После этого можно начинать обмен данными между устройствами по шине.

Если вы планируете использовать оба контроллера I2C, вам нужно будет повторить эти операции отдельно для каждой шины.

 


Передача данных в режиме MASTER

Контроллер I2C ESP32, работающий в качестве ведущего, отвечает за установление сеанса связи с ведомыми устройствами и отправку команд для ведомого устройства, например, для выполнения измерения и отправки показаний обратно на ведущее устройство.

Поскольку процесс передачи данных весьма чувствителен к временным задержкам, I2C API было разработано таким образом, чтобы весь сеанс связи проходил непрерывно, один блоком. Для этого API предоставляет контейнер команд, называемый «command link» (почему? лично мне он больше напоминает “пакет” – батник или скрипт), который должен быть предварительно заполнен необходимой последовательностью команд. И только затем его можно передать контроллеру I2C для непрерывного выполнения.

Пример сеанса связи с записью данных на ведомое устройство

Пример сеанса связи с записью данных на ведомое устройство

Пример сеанса связи с чтением данных с ведомого устройства

Пример сеанса связи с чтением данных с ведомого устройства

Ниже описано, как настраивается командная ссылка для «основной записи» и что происходит внутри:

  • Создайте контейнер команд с помощью функции i2c_cmd_link_create() или i2c_cmd_link_create_static(). В первом случае необходимый блок памяти для контейнера будет выделен динамически из общей памяти (кучи, heap), во втором – вы должны будете предварительно позаботится о таком блоке памяти заранее, на этапе программирования. Что выбрать? В случае использования i2c_cmd_link_create() при каждом обращении к шине будет выделен новый блок памяти под контейнер, и это позволяет не заботится о возможной перезаписи данных в контейнере другим потоком / задачей. Но этот способ медленнее и потенциально может приводить к проблеме фрагментации кучи. Во втором случае ( i2c_cmd_link_create_static() ) вам придется позаботится о предварительном выделении переменной необходимого размера заранее, а так же синхронизировать доступ к шине из разных потоков / задач с помощью мьютексов или семафоров (при необходимости, конечно). Зато это быстрее и не приводит к фрагментации памяти от слова “совсем”. К слову, i2c_cmd_link_create_static() появился в ESP-IDF только относительно недавно – когда я начинал свою работу с ESP32 этой возможности не было. В какой конкретно версии добавили “статические” методы – я пропустил. После того, как контейнер создан, можно заполнять его необходимыми командами:
  1. В начале сеанса связи всегда идёт стартовый бит, который добавляется с помощью i2c_master_start(). В дальнейшем, при выполнении пакета команд, это заставит аппаратный контроллер IIC передать “стартовый бит”. Если вы не знаете, что это такое и для чего это надо, советую почитать другую статью.
  2. Добавьте адрес ведомого и направление передачи с помощью первого вызова i2c_master_write_byte(). Адрес ведомого устройства занимает в первом байте только 7 бит, последний бит указывает на направление передачи следующих данных. Поэтому адрес нужно сдвинуть на 1 бит влево, то есть первый байт нужно сформировать как (i2c_address << 1) | I2C_MASTER_WRITE для режима записи или (i2c_address << 1) | I2C_MASTER_READ для чтения.
  3. Теперь можно передавать и принимать собственно данные — один или несколько байтов с помощью функций i2c_master_write_byte() и i2c_master_write() для записи или i2c_master_read_byte() и i2c_master_read() для чтения. Только не забываем при чтении устанавливать / отправлять бит ACK (или NACK в конце передачи). И наоборот, при записи обе функции передачи имеют дополнительный аргумент, указывающий, должен ли ведущий убедиться, что он получил бит ACK.
  4. В конце сеанса связи добавьте стоповый бит через вызов i2c_master_stop() для master-режима.
  • После того, как пакет команд был сформирован, его можно запустить на выполнение контроллером I2C, вызвав i2c_master_cmd_begin() и дождаться завершения его выполнения. После запуска выполнения пакет уже не должен изменяться. Для выполнения пакета вы должны указать ссылку на сам пакет, номер контроллера (0 или 1) и время ожидания завершения в тиках операционной системы.
  • После завершения сеанса связи освободите ресурсы, используемые контейнером, вызвав i2c_cmd_link_delete() или i2c_cmd_link_delete_static() соответственно тому, какой методы вы использовали при создании контейнера.
Пример чтения с ведомого устройства одного или нескольких байт

Пример чтения с ведомого устройства одного или нескольких байт

Разумеется, в одном сеансе связи можно использовать запись и чтение одновременно: например вначале отправляем на ведомое устройство адрес интересующего нас регистра, а затем сразу же читаем возвращаемые данные.

Сложно? Да нет, просто громоздко. И требуется каждый раз создавать контейнер и пакет “вручную”, что несколько напрягает. Поэтому я в свое время создал библиотеку – обёртку, в которой объявлены две макрофункции для приема и передачи данных. Об ней я подробнее расскажу подробнее чуть ниже.

В последней версии ESP-IDF я обнаружил такие же как у меня макрофункции имеются уже и в самой ESP-IDF (наконец-то в Espressif догадались это сделать):

  1. Отправляем команду на изменение i2c_master_write_to_devicе()
  2. Ждем какое-то время
  3. Читаем измеренные данные с помощью i2c_master_read_from_device()

Когда пауза не требуется, удобнее использовать другую макрофункцию:

Что безусловно радует, платформа развивается, нужно только не забывать периодически заглядывать в документацию.

 


Передача данных в режиме SLAVE

Связь в качестве ведомого может понадобится, когда требуется связать между собой несколько микроконтроллеров на небольшом расстоянии (на большом расстоянии я все-таки рекомендовал бы использовать что-то вроде RS485).

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

  • i2c_slave_read_buffer() – чтение буфера приема данных от мастера. Всякий раз, когда ведущий отправляет данные ведомому, драйвер автоматически сохраняет их в приемном буфере. Это позволяет подчиненному приложению вызывать функцию i2c_slave_read_buffer() по своему усмотрению. Эта функция также имеет параметр для указания времени ожидания блока данных, если в приемном буфере на момент запроса ещё нет данных. Это позволит ведомому приложению ждать с заданным тайм-аутом поступления данных в буфер.
  • i2c_slave_write_buffer() – запись в буфер отправки. Этот буфер используется для хранения всех данных, которые ведомое устройство хочет отправить ведущему в порядке FIFO. Данные остаются там до тех пор, пока мастер не запросит их. Функция i2c_slave_write_buffer() имеет параметр для указания времени ожидания, если буфер отправки заполнен. Это позволит ведомому приложению ждать с заданным тайм-аутом, пока в буфере отправки не станет доступным достаточно места.

Вот честно говоря, я ни разу не использовал этот режим на практике, поэтому готовых примеров у меня, увы, нет. А примеры от espessif вы можете посмотреть здесь:

esp-idf/examples/peripherals/i2c at v5.0 · espressif/esp-idfgithub.com

 


Совместный доступ к шине I2C из разных потоков / задач

Все функции библиотеки driver/i2c.h уже имеют средства “дорожного регулирования” и являются потокобезопасными. То есть вы можете не заботиться о всяких там семафорах – мьютексах, а просто начинать работу с шиной.

Однако всё может измениться, если вы будете использовать статические буферы (с использованием i2c_cmd_link_create_static()) для создания командных ссылок. На практике имеет смысл объявлять их только как static, иначе они просто попадут в стек и никакой практической выгоды от них не будет. А это означает, что нам придется самим синхронизировать доступ к этому буферу, иначе может случиться конфуз. Самое простое – использовать мьютекс (двоичный семафор с правом захвата).

Но это нужно далеко не всегда. У меня как-то чаще бывает, что с шиной I2C работает всего одна задача. Ну и смысл тогда городить огород?


Моя библиотека для работы с шиной I2C

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

 

Вычисление контрольной суммы для 16-битных чисел

Первая принимает младший и старший байты числа раздельно, вторая – целиком, в виде 16-битного числа. Разные сенсоры используют разных подход.

 

Инициализация шины

bool initI2C(const i2c_port_t i2c_num, const int sda_io_num, const int scl_io_num, const bool pullup_enable, const uint32_t clk_speed)

За один вызов сразу выполняется и конфигурирование и запуск драйвера шины. Очень удобно.

Работа с шиной I2C на ESP32 и ESP-IDF

 

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

Никакой “практической” ценности это не несет, кроме как для целей отладки: опрашиваются все адреса на шине, и если ведомое устройство отвечает, выводится адрес в лог.

void scanI2C(i2c_port_t i2c_num);

Очень помогает “увидеть” какие сенсоры и устройства подключились, а с каким физические проблемы (например перепутаны провода SCL и SDA).

 

Общий сброс шины

Где-то нашел информацию, что запись по адресу 0x00 байта 0x06 вызывает общий сброс всех устройств на шине. Написал реализацию.

esp_err_t generalCallResetI2C(i2c_port_t i2c_num);

Но практической пользы не ощутил.

 

Будильник для ведомого устройства

Отправив на устройство “пустую” команду 0x00, мы путаемся разбудить ведомое устройство и заставить его что-то делать.

esp_err_t wakeI2C(i2c_port_t i2c_num, const uint8_t i2c_address, TickType_t timeout)

Изобрели это китайцы для AM2320, насколько я помню. Больше никто этой фигней не мается.


Запись регистра на ведомое устройство

esp_err_t writeI2C(i2c_port_t i2c_num, const uint8_t i2c_address, uint8_t* cmds, const size_t cmds_size, uint8_t* data, const size_t data_size, TickType_t timeout)

Эта макрофункция предназначена для записи на ведомое устройство с адресом i2c_address в регистр cmds (частое другое название – команда) блока данных data размером data_size. Поскольку адресация регистров может быть как однобайтовая, так и многобайтовая, то нужно указать размер буфера cmds_size.


Чтение регистра с ведомого устройства

esp_err_t readI2C(i2c_port_t i2c_num, const uint8_t i2c_address, uint8_t* cmds, const size_t cmds_size, uint8_t* data, const size_t data_size, const uint32_t wait_data_us, const TickType_t timeout)

Макрофункция чтения регистра cmds (размер команды указывается в cmds_size) с ведомого устройства i2c_address. В поле data необходимо передать указатель на буфер размером data_size под получаемые данные. Если на обработку запроса ведомому устройству требуется время, то указываем его в аргументе wait_data_us, и в этом случае чтение будет происходить за два разных сеанса связи. Если передать 0, то отправка команды cmds и чтение данных в буфер будут производиться за один сеанс связи.


Настройка библиотеки

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

Работа с шиной I2C на ESP32 и ESP-IDF

В принципе, в ESP-IDF вы вольны использовать другие свободные выводы GPIO, а не обязательно как на скриншоте. Например если вам удобнее развести печатную плату с другими выводами. А вот встроенную подтяжку я никогда не использую, предпочитаю ставить внешние резисторы.

Если закоментарить CONFIG_I2C_PORT1_SDA и CONFIG_I2C_PORT1_SCL, то эта шина настраиваться и запускаться не будет.


Статические буферы для команд

Макросы CONFIG_I2C_PORT0_STATIC и CONFIG_I2C_PORT1_STATIC используются для определения того, чтобы определить, какой тип буфера под команды будет использован. Если CONFIG_I2C_PORT0_STATIC = 0, то будет использован динамический метод, если > 0 – то статический. Число обозначает, сколько транзакций может поместиться в буфер одного сеанса связи.

Работа с шиной I2C на ESP32 и ESP-IDF

Я использую 2отправка номера регистра + чтение или запись (размер блока данных чтения/записи в данном случае значения не имеет). Больше команд за один сеанс связи мне пока не приходилось использовать. Но если это понадобится, это легко устроить.

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

Работа с шиной I2C на ESP32 и ESP-IDF

Теперь вместо i2c_cmd_link_create() я использую “свой” макрос _i2c_cmd_link_create(i2c_num), а он уже автоматически выбирает нужную API функцию и буфер, при необходимости. Для каждой шины используется свой личный буфер, что позволяет делать одновременные запросы к разным шинам из разных задач.

 


Пример получения данных с сенсора BME280

Как всем известно, для платформы Arduino написано просто огромное количество драйверов для самых разных устройств, датчиков, дисплеев и т.д. Увы и ах, но для ESP-IDF их практически нет, производители не обращают никакого внимания на эту платформу, она не популярна. Поэтому придется писать драйвера самому. Не пугайтесь – это совсем не сложно, давайте попробуем….

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

Кто знаком с этим творением Bosch, могут сказать: нафига начинать с него? Нам же чёрт ногу сломит со всеми этими вычислениями и калибровочными коэффициентами!

По сути это действительно так. Но я не буду вдаваться в эти детали, а просто воспользуюсь готовым API, который нам предоставляет производитель:

GitHub – boschsensortec/BME280_driver: Bosch Sensortec BME280 sensor driver. To report issues, go to https://community.bosch-sensortec.com/t5/Bosch-Sensortec-Community/ct-p/bst_communitygithub.com

Нам не потребуется вникать в тонкости регистров и протокола обмена данными, а останется только написать callback-функции обмена данными по шине, всю остальную внутреннюю работу API берет на себя.

Я использовал описанные выше макрофункции:

Чтение регистра BME280

Чтение регистра BME280

Запись регистра BME280

Запись регистра BME280

Теперь, для того чтобы получить данные с сенсора, просто вызываем соответствующую функцию API, например так:

Работа с шиной I2C на ESP32 и ESP-IDF

Просто? Не то слово. И зверей убивать не надо…

Конечно, Вам потребуется написать функции сброса датчика, его настройки и т.д., но сделать это не так уж и сложно, нужно только почитать datasheet.

 


Пример получения данных с часов реального времени

Давайте ещё рассмотрим пример обмена данными на шине без всяких там API. Возьмем кОльОсикО самый что ни на есть обычный RTC-таймер на чипе DS1307.

GitHub – kotyara12/reDS1307: Driver for real-time clock DS1307github.com

Регистры все у него однобайтовые, поэтому функции чтения / записи у меня выглядят так:

Работа с шиной I2C на ESP32 и ESP-IDF

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

Работа с шиной I2C на ESP32 и ESP-IDF

Запись даты и времени производится в обратном порядке:

Работа с шиной I2C на ESP32 и ESP-IDF

Где функции преобразования:

Работа с шиной I2C на ESP32 и ESP-IDF

Конечно, для того чтобы написать драйвер I2C устройства, придется заглянуть в datasheet на него, узнать какие адреса регистров для чего используются.
Ну или как вариант, взять готовый драйвер для четверга Arduino и заменить вызовы библиотеки Wire.Read и Wire.Write на свои собственные.

 


Тонкие настройки

В заключение хотелось бы упомянуть про возможность тонкой настройки параметров работы шины. Когда функция i2c_param_config() настраивает конфигурацию драйвера для порта I2C, она также устанавливает для нескольких параметров связи I2C значения по умолчанию, определенные в спецификации I2C. Некоторые параметры предварительно настраиваются в регистрах контроллера I2C.

Все эти параметры можно изменить на пользовательские значения, вызвав специальные функции, указанные в таблице ниже. Обратите внимание, что временные значения определяются в тактовых циклах APB. Частота APB указана в I2C_APB_CLK_FREQ.

https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/i2c.html#id7

https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/i2c.html#id7

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

 


Ссылки

  1. ESP-IDF: Inter-Integrated Circuit (I2C)
  2. Библиотека reI2C
  3. Набор драйверов для сенсоров

 


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

1 комментарий для “Работа с шиной I2C на ESP32 и ESP-IDF”

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

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