На данном уроке мы попробуем поработать с шиной I2c.
С данной шиной мы раньше также работали с применением других микроконтроллеров. Настало время поработать с ней, подключив по ней что-нибудь к контроллеру ESP32.
Данная шина у контроллера ESP32 реализована аппаратно в отличие от младшей модели ESP8266. Только аппаратная поддержка работает только в том случае, если назначить нужные ножки, во всяком случае так гласит документация.
Мы не будем уходить от сложившихся традиций и подключим, как обычно, к данной шине микросхему памяти EEPROM — AT24C32, которая установлена в модуле с часовой микросхемой DS3231 и также в часовом модуле с микросхемой DS1307.
С данной микросхемой мы также работали неоднократно с использованием различных контроллеров и различных библиотек, поэтому знакомиться с ней также нет смысла.
Выглядит модуль DS1307, с которым мы будем работать на данном уроке вот так
Подключим модуль к нашей отладочной плате, только провода для SCL и SDA возьмём с ещё одним отводом для подключения логического анализатора, чтобы потом наглядно увидеть, как будут передаваться данные по шине, а заодно и скорость их передачи. Подключим сразу и логический анализатор
Посмотрим распиновку нашей платы
Здесь мы видим, что ножки, предназначенные для I2C, то бишь SDA и SCL у нас соответствуют ножкам портов GPIO21 и GPIO22. Так мы их и подключили.
Теперь сразу к проекту. Он был сделан из проекта прошлого урока с именем EXTI01 и назван был I2C_EEPROM.
Как всегда, сначала конфигурирование, поэтому после добавления проекта в дерево проектов среды Espressif IDE мы первым делом откроем файл Kconfig.projbuild и внесём там следующие изменения. Мы уберём пункт для ножки светодиода, а пункты для входов исправим под требования нашего нового проекта. Я приведу весь текст данного файла
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
menu "Example Configuration" config SDA_GPIO int "SDA GPIO number" range 0 48 default 21 help GPIO number SDA. config SCL_GPIO int "SCL GPIO number" range 0 48 default 22 help GPIO number SCL. endmenu |
Соберём проект и посмотрим, что у нас в конфигураторе
В конфигураторе всё нормально. Значит будем в проекте будем пользоваться назначенными здесь макросами.
В main.c удалим все макросы, глобальные переменные и функции с телами, за исключением app_main, в которой оставим только бесконечный цикл
1 2 3 4 5 6 7 |
//------------------------------------------------ void app_main(void) { while (1) { } } //------------------------------------------------ |
Объявим макрос, в котором будет храниться адрес микросхемы
1 2 3 |
#include "sdkconfig.h" //------------------------------------------------ #define I2C_ADDRESS 0x50 |
Подключим библиотеку для работы с I2C, а также библиотеку для отслеживания ошибок
1 2 3 |
#include "driver/gpio.h" #include "driver/i2c.h" #include "esp_err.h" |
Объявим глобальный указатель на строку для отображения логов
1 2 3 |
#define I2C_ADDRESS 0x50 //------------------------------------------------ static const char *TAG = "main"; |
В функции app_main объявим целочисленную переменную, переменную для номера модуля и переменную для кода ошибки
1 2 3 4 5 |
void app_main(void) { uint16_t i=0; i2c_port_t i2c_port = I2C_NUM_0; esp_err_t ret; |
Чтобы не захламлять основной модуль программы специфическим кодом, создадим модуль для работы с шиной. Для этого создадим и подключим к дереву проекта два файла пока с таким содержимым
1 2 3 4 5 6 7 8 9 |
#ifndef MAIN_AT24C_H_ #define MAIN_AT24C_H_ //================================================== #include "esp_err.h" #include "driver/i2c.h" #include <unistd.h> //================================================== //================================================== #endif /* MAIN_AT24C_H_ */ |
1 2 |
#include "at24c.h" //================================================== |
Подключим наш новый модуль в файле main.c
1 2 |
#include "sdkconfig.h" #include "at24c.h" |
Перейдём в файл at24c.c и добавим там функцию инициализации периферии
1 2 3 4 5 6 7 |
#include "at24c.h" //================================================== esp_err_t i2c_master_driver_initialize(i2c_port_t i2c_port, int chip_addr, int i2c_gpio_sda, int i2c_gpio_scl) { esp_err_t ret; } //================================================== |
На входе мы здесь получаем номер модуля, адрес микросхемы и номера портов для ножек SDA и SCL.
Создадим в заголовочном файле прототип на нашу функцию и вызовем её в app_main файла main.c
1 2 |
esp_err_t ret; ret = i2c_master_driver_initialize(i2c_port, I2C_ADDRESS, CONFIG_SDA_GPIO, CONFIG_SCL_GPIO); |
Если мы сейчас соберём код, то мы получим ошибку. Дело в том, что мы видим только прототип функции, а реализацию не видим. Если мы добавляем файлы реализации в дерево проекта, это не значит, что они будут видны из кода и будут выполняться. Чтобы полностью подключить такой файл в проект, откроем файл CMakeLists.txt в каталоге main и добавим в перечень подключаемых файлов наш новый файл
set(COMPONENT_SRCS "main.c at24c.c")
При сборке теперь будет только одна ошибка — отсутствие возвращаемого аргумента в нашей новой функции i2c_master_driver_initialize.
Для начала идём в файл at24c.h и объявим там макрос для частоты шины и заодно макрос для бита проверки отклика
1 2 3 4 |
#include "driver/i2c.h" //================================================== #define I2C_FREQUENCY 100000 #define ACK_CHECK_EN 0x1 |
Перейдём в файл at24c.c в тело функции i2c_master_driver_initialize , объявим переменную типа структуры для конфигурации модуля I2C и проинициализируем её поля
1 2 3 4 5 6 7 8 9 |
esp_err_t ret; i2c_config_t conf = { .mode = I2C_MODE_MASTER, .sda_io_num = i2c_gpio_sda, .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_io_num = i2c_gpio_scl, .scl_pullup_en = GPIO_PULLUP_ENABLE, .master.clk_speed = I2C_FREQUENCY }; |
Вызовем функцию конфигурации шины, результат вернётся в переменную, и вернём результат
1 2 3 4 |
.master.clk_speed = I2C_FREQUENCY }; ret = i2c_param_config(i2c_port, &conf); return ret; |
Вот теперь всё соберётся без ошибок.
До возврата нам надо будет ещё кое-что проделать. Для начала мы всё же вернём результат, но только в том случае, если он будет плохой
1 2 |
ret = i2c_param_config(i2c_port, &conf); if (ret != ESP_OK) return ret; |
Объявим глобальную переменную
1 2 3 |
#include "at24c.h" //================================================== static int at24c_addr=0; |
Вернёмся в функцию i2c_master_driver_initialize и сохраним в эту переменную адрес микросхемы
1 2 |
if (ret != ESP_OK) return ret; at24c_addr=chip_addr; |
Вызовем функцию, которая окончательно установит драйвер для работы с нашей микросхемой по шине I2C
1 2 |
at24c_addr=chip_addr; ret = i2c_driver_install(i2c_port, I2C_MODE_MASTER, 0, 0, 0); |
В файле main.c добавим пустой массив на 20 элементов для сбора информации из микросхемы и инициализированный такого же размера для передачи информации в микросхему
1 2 3 4 5 6 7 8 |
#define I2C_ADDRESS 0x50 //------------------------------------------------ uint8_t rd_value[20] = {0}; uint8_t wr_value[20] = {0x14,0x13,0x12,0x11,0x10, 0x0F,0x0E,0x0D,0x0C,0x0B, 0x0A,0x09,0x08,0x07,0x06, 0x05,0x04,0x03,0x02,0x01}; //------------------------------------------------ |
Вернёмся в файл at24c.c и ниже функции инициализации i2c_master_driver_initialize (хотя это не обязательно, можно и выше) добавим функцию для записи массива байтов в память микросхемы
1 2 3 4 5 6 |
//================================================== void AT24C_WriteBytes (i2c_port_t i2c_port, uint16_t addr,uint8_t *buf, uint16_t bytes_count) { uint16_t i; } //================================================== |
В данной функции мы сначала создадим указатель на переменную типа структуры команды и с помощью специальной функции инициализируем его указателем на дескриптор команды
1 2 |
uint16_t i; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); |
Заполним буфер данной структуры последовательностью команд и байтов
1 2 3 4 5 6 7 8 9 10 |
i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, at24c_addr << 1 | I2C_MASTER_WRITE, ACK_CHECK_EN); i2c_master_write_byte(cmd, (uint8_t) (addr>>8), ACK_CHECK_EN); i2c_master_write_byte(cmd, (uint8_t) addr, ACK_CHECK_EN); for(i=0;i<bytes_count;i++) { i2c_master_write_byte(cmd, buf[i], ACK_CHECK_EN); } i2c_master_stop(cmd); |
Здесь мы как обычно посылаем условие СТАРТ, затем передаём адрес устройства с нулевым битом записи, что значит мы работаем на запись, далее передаём адрес памяти, с которого начнём запись и затем непосредственно сами записываемые бита. По окончанию передаём команду СТОП.
Теперь применим нашу команду с помощью специальной функции SDK
1 2 |
i2c_master_stop(cmd); i2c_master_cmd_begin(i2c_port, cmd, 1000 / portTICK_RATE_MS); |
Освободим память, назначенную под структуру, и немного подождём
1 2 3 |
i2c_master_cmd_begin(i2c_port, cmd, 1000 / portTICK_RATE_MS); i2c_cmd_link_delete(cmd); usleep(1000*2); |
В main.c в функции app_main отобразим в терминале код возврата из функции инициализации и вызовем нашу функцию, чтобы записать в память микросхемы наш массив, не забыв перед этим добавить на неё прототип в заголовочный файл
1 2 3 |
ret = i2c_master_driver_initialize(i2c_port, I2C_ADDRESS, CONFIG_SDA_GPIO, CONFIG_SCL_GPIO); ESP_LOGI(TAG, "Init: %d", ret); AT24C_WriteBytes (i2c_port, 0x0120, wr_value, 20); |
В бесконечном цикле добавим небольшую задержку
1 2 |
while (1) { vTaskDelay(10 / portTICK_PERIOD_MS); |
Соберём код, подключим плату, логический анализатор, запустим программу логического анализа, прошьём контроллер и посмотрим результат в программе логического анализа
Также мы видим, что частота соответствует заявленной
В терминале мы также видим успешный результат инициализации шины
Теперь чтение. Закомментируем вызов функции записи, перейдём в файл at24c.c и добавим функцию для чтения, аналогично воспользовавшись той же структурой, а какие команды использовать для чтения серии байтов, мы и так давно знаем
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//================================================== void AT24C_ReadBytes (i2c_port_t i2c_port, uint16_t addr, uint8_t *buf, uint16_t bytes_count) { uint16_t i; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, at24c_addr << 1 | I2C_MASTER_WRITE, ACK_CHECK_EN); i2c_master_write_byte(cmd, (uint8_t) (addr>>8), ACK_CHECK_EN); i2c_master_write_byte(cmd, (uint8_t) addr, ACK_CHECK_EN); i2c_master_start(cmd); i2c_master_write_byte(cmd, at24c_addr << 1 | I2C_MASTER_READ, ACK_CHECK_EN); for(i=0;i<bytes_count;i++) { if(i<(bytes_count-1)) i2c_master_read_byte(cmd, buf+i, I2C_MASTER_ACK); else i2c_master_read_byte(cmd, buf+i, I2C_MASTER_LAST_NACK); } i2c_master_stop(cmd); i2c_master_cmd_begin(i2c_port, cmd, 1000 / portTICK_RATE_MS); i2c_cmd_link_delete(cmd); } //================================================== |
Добавим на данную функцию прототип в заголовочном файле, вызовем её в main.c в функции app_main и отобразим в терминале принятые байты
1 2 3 4 5 6 7 |
//AT24C_WriteBytes (i2c_port, 0x0120, wr_value, 20); AT24C_ReadBytes(i2c_port, 0x0120 , rd_value, 20); for(i=0;i<20;i++) { printf("%02X ",rd_value[i]); } printf("\r\n"); |
Соберём код, прошьём контроллер и посмотрим результат сначала в терминале
Всё отлично принялось!
А вот и результат в программе логического анализа
Итак, на данном уроке мы научились работать в своих программах с передачей и приёмом данных по интерфейсу I2C контроллера ESP32. На этом, конечно же, работа с данным интерфейсом не заканчивается. Будет ещё много уроков с его использованием.
Всем спасибо за внимание!
Предыдущий урок Программирование МК ESP32 Следующий урок
Недорогие отладочные платы ESP32 можно купить здесь: Недорогие отладочные платы ESP32
Логический анализатор 16 каналов можно приобрести здесь
Модуль RTC DS3231 с микросхемой памяти (3 шт)
Модуль RTC DS3231 с микросхемой памяти (1 шт) — так дороже
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в Дзен (нажмите на картинку)
Добрый день на ваш сайт. Подскажите, уроки по Миландру будут?
Здравствуйте. Данная тема пока не планировалась.
if (ret != ESP_OK) return ret;
У меня так не компилируется,
функция все же должна возвращать какое-то значение независимо от условий внутри функции
Здравствуйте. У меня содержимое файла I2C_EEPROM\main\CMakeList.txt почему то такое:
idf_component_register(SRCS «blink_example_main.c»
INCLUDE_DIRS «.»)
и больше там ничего нет.
Строки типа: set(COMPONENT_SRCS «main.c»), да и остальное содержимое — отсутствует.
Пробовал прописать эту строку (set(COMPONENT_SRCS «main.c at24c.c»)) принудительно, но не помогло — сборка проекта так и не происходит.
С уважением.