На данном уроке мы попробуем поработать с шиной 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 с микросхемой памяти (2-5 шт)
Модуль RTC DS3231 с микросхемой памяти (1 шт) — так дороже
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в 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»)) принудительно, но не помогло — сборка проекта так и не происходит.
С уважением.