На данном уроке мы попробуем поработать с шиной I2c.
Как таковой аппаратной поддержки передачи по данной шине у контроллера ESP8266 нет, нет такого модуля, нет даже специальных регистров для этого.
Но в то же время посредством функционала библиотеки SDK всё же такая поддержка имеется, хоть и программная. Видимо, это обусловлено тем, что шина I2C используется везде для передачи данных и без неё никак нельзя просто обойтись.
Давайте и мы попытаемся что-то передать и принять, настроив такую шину.
Вообще с протоколом передачи данных по I2C мы знакомы давно. Мы работали с ним с использованием различных контроллеров — AVR, STM32, PIC. Причём работаем мы с ней до сих пор, так как многие устройства подключаются по данной шине.
Поэтому нет нам смысла повторно изучать, как именно работает интерфейс I2C, какого типа сигналы по каким проводам передаются, как подтверждаются пакеты, как остановить передачу, что такое адрес устройства и т.д.
В качестве устройства для практики по данному интерфейсу мы возьмём микросхему EEPROM — AT24C32, которая установлена в модуле с часовой микросхемой DS3231 и также в часовом модуле с микросхемой DS1307.
С данной микросхемой мы также работали неоднократно с использованием различных контроллеров и различных библиотек, поэтому знакомиться с ней также нет смысла.
Выглядит модуль DS1307, с которым мы будем работать на данном уроке вот так
Теперь немного о программной поддержке I2C в ESP8266.
Если ничего не менять в функционале библиотеки SDK, то подключаем мы устройства по I2C с следующим ножкам
Подключим модуль к нашей отладочной плате, только провода для SCL и SDA возьмём с ещё одним отводом для подключения логического анализатора, чтобы потом наглядно увидеть, как будут передаваться данные по шине, а заодно и скорость их передачи. Подключим сразу и логический анализатор
А с функциями для работы с I2C библиотеки SDK мы будем знакомиться по ходу работы с проектом.
Проект мы создадим из проекта урока 7 с именем UART_TX и назовём его I2C_EEPROM.
Откроем наш проект в Eclipse и первым делом в user_init() удалим всю инициализацию GPIO2, так как он у нас занят в I2C
PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO2_U, FUNC_GPIO2);
gpio_output_set(0, 0, (1 << LED), 0);
Из бесконечного цикла удалим строки, где меняются уровни на ножке
gpio_output_set(0, (1 << LED), 0, 0);
…
gpio_output_set((1 << LED), 0, 0, 0);
Удалим также инкрементирование счётчика
i++;
if(i>9999) i=1;
И удалим вывод информации в терминальную программу
os_printf(«String %04d\r\n», i);
os_printf(«SDK version: %s\n», system_get_sdk_version());
os_printf(«Version info of boot: %d\n», system_get_boot_version());
os_printf(«Userbin address: 0x%x\n», system_get_userbin_addr());
os_printf(«Time = %ld\r\n», system_get_time());
os_printf(«RTC time = %ld\r\n», system_get_rtc_time());
os_printf(«Chip id = 0x%x\r\n», system_get_chip_id());
os_printf(«CPU freq = %d MHz\r\n», system_get_cpu_freq());
os_printf(«Flash size map = %d\r\n», system_get_flash_size_map());
os_printf(«Free heap size = %d\r\n», system_get_free_heap_size());
system_print_meminfo();
Чтобы использовать I2C, подключим соответствующий хедер-файл
1 2 |
#include "driver/uart.h" #include "driver/i2c_master.h" |
Вызовем функцию инициализации I2C в user_init()
1 2 |
gpio_init(); i2c_master_gpio_init(); |
Если сейчас собрать проект и прошить контроллер, а также посмотреть в логическом анализаторе анализ того, что происходит на контактах шины I2C, можно увидеть, что там уже произошла какая-то пробная передача, хотя мы ещё ничего не передавали
Поэтому чтобы это немного отделить от основной передачи, добавим небольшую задержку
1 2 |
i2c_master_gpio_init(); ets_delay_us(100000); |
Добавим макросы для адреса устройства и для битов записи и чтения
1 2 3 4 |
#define LED 2 #define I2C_REQUEST_WRITE 0x00 #define I2C_REQUEST_READ 0x01 #define SLAVE_OWN_ADDRESS 0xA0 |
Также добавим пустой массив на 20 элементов для сбора информации из микросхемы и инициализированный такого же размера для передачи информации в микросхему
1 2 3 4 5 6 7 8 |
#define SLAVE_OWN_ADDRESS 0xA0 //------------------------------------------------------ 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}; //------------------------------------------------------ |
Добавим функцию передачи серии байтов для записи их во внешний EEPROM с определённого адреса, где первый аргумент — адрес, второй — адрес буфера для передачи, третий — количество байтов, которые мы собираемся записать в память
1 2 3 4 5 6 7 |
0x05,0x04,0x03,0x02,0x01}; //------------------------------------------------------ void AT24C_WriteBytes (uint16_t addr,uint8_t *buf, uint16_t bytes_count) { } //------------------------------------------------------ |
Вызовем данную функцию в user_init()
1 2 3 |
ets_delay_us(100000); os_printf("\r\n"); AT24C_WriteBytes (0x054A, wr_value, 20); |
В данной функции объявим переменную для итерации, переменную для проверки передачи адреса и вызовем функцию передачи в шину условия START
1 2 3 4 5 |
void AT24C_WriteBytes (uint16_t addr,uint8_t *buf, uint16_t bytes_count) { uint16_t i; uint8_t ack; i2c_master_start(); |
Затем передадим адрес устройства, чтобы оно, узнав себя, откликнулось
1 2 |
i2c_master_start(); i2c_master_writeByte(SLAVE_OWN_ADDRESS | I2C_REQUEST_WRITE); |
Затем проверим отклик и пошлём условие STOP, мы пока ничего кроме адреса не будем передавать в шину, чтобы проверить её работу
1 2 3 4 5 6 7 |
i2c_master_writeByte(SLAVE_OWN_ADDRESS | I2C_REQUEST_WRITE); ack = i2c_master_checkAck(); if(!ack) { os_printf("ADDR not ack\r\n"); } i2c_master_stop(); |
Соберём код, прошьём контроллер и проверим, как у нас работает шина
Отлично! Адрес передан, скорость 50 кГц.
Убедимся также что в UART у нас также не передалось ничего лишнего
Всё отлично.
Теперь передадим наши байты. Для этого передадим сначала адрес в шину, сначала его старший байт, затем младший, а затем поочерёдно отправим все наши байты
1 2 3 4 5 6 7 8 9 10 11 |
os_printf("ADDR not ack\r\n"); } i2c_master_writeByte((uint8_t) (addr>>8)); i2c_master_send_ack(); i2c_master_writeByte((uint8_t) addr); i2c_master_send_ack(); for(i=0;i<bytes_count;i++) { i2c_master_writeByte(buf[i]); i2c_master_send_ack(); } |
В user_init() также отправим данные байты в строковом выражении в терминальную программу
1 2 3 4 5 6 |
AT24C_WriteBytes (0x054A, wr_value, 20); for(i=0;i<20;i++) { os_printf("%02X ",wr_value[i]); } os_printf("\r\n"); |
Соберём код, прошьём контролер и посмотрим результат сначала в программе логического анализа
В терминальной программе тоже всё нормально
Судя по логограмме, у нас всё передалось и будем считать, что записалось в память микросхемы.
Также я должен вас предупредить, что с записью серии байтов может быть некоторая засада, которую я сразу не заметил.
Оказывается, адрес для начала записи байтов в микросхему может быть не любым.
Дело в том, что память в микросхеме AT24 не плоская, а постраничная. В нашем экземпляре на 32 килобита (4 килобайта) она делится на 128 страниц по 32 байта
Писать непрерывно мы можем только в рамках одной страницы, Если посчитать, то можно определить, что страницы начинаются с адресов, кратных 32, то есть в шестнадцатеричном выражении они заканчиваются на 20, 40, 60, 80, A0, C0, E0.
Если мы пишем, например с адреса 0x015B, то мы не можем записать больше 5 байт, так как шестой байт уже будет приходиться на следующую страницу. Поэтому, если мы попытаемся записать 20 байт с данного адреса, то самое страшное в этом, что мы не получим никакой ошибки. А потом уже, когда попытаемся считать данные байты, то первые 5 байтов будут, какие мы записали, а остальные, которые оказались в другой странице, останутся неизменными. Кстати, чтение мы можем производить, невзирая на страницы, то есть мы можем сразу прочитать блок с адреса 0 длиной в 4096 байт.
Мы же выбрали адрес 0x054A, что вполне позволяет записать, начиная с него 20 байтов, так как следующая страница будет начинаться с адреса 0x0560, а если из данного адреса вычесть наш, то мы получим 22 байта, а у нас 20, так что всё нормально и всё влезет в одну страницу.
А если же мы хотим записать большой блок, то мы его должны писать порциями, по страницам. Причём между тем, как мы запишем байты в одну страницу и начнём писать байты в другую, мы должны выждать определённый таймаут
А оговорен он вот здесь
Теперь попробуем прочитать наши байты.
Добавим ниже функции AT24C_WriteBytes функцию чтения серии байтов из микросхемы с подобными параметрами, в которой мы также объявим такие же локальные переменные, также пока передадим условие START, адрес устройства и условие STOP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//------------------------------------------------------ void AT24C_ReadBytes (uint16_t addr, uint8_t *buf, uint16_t bytes_count) { uint16_t i; uint8_t ack; i2c_master_start(); i2c_master_writeByte(SLAVE_OWN_ADDRESS | I2C_REQUEST_WRITE); ack = i2c_master_checkAck(); if(!ack) { os_printf("ADDR not ack\r\n"); } i2c_master_stop(); } //------------------------------------------------------ |
В user_init() закомментируем пока вызов функции записи байтов и вызовем функцию чтения
1 2 |
//AT24C_WriteBytes (0x054A, wr_value, 20); AT24C_ReadBytes(0x054A , rd_value, 20); |
Закомментируем также отправку байтов в UART, а отправим теперь такие же байты из буфера чтения
1 2 |
//os_printf("%02X ",wr_value[i]); os_printf("%02X ",rd_value[i]); |
Продолжим писать тело функции AT24C_ReadBytes и также передадим там в шину адрес памяти, с которого мы будем читать байты
1 2 3 4 5 6 |
os_printf("ADDR not ack\r\n"); } i2c_master_writeByte((uint8_t) (addr>>8)); i2c_master_send_ack(); i2c_master_writeByte((uint8_t) addr); i2c_master_send_ack(); |
Передадим ещё одно условие START, такой порядок чтения памяти по I2C
1 2 |
i2c_master_send_ack(); i2c_master_start(); |
Теперь передадим адрес устройства с битом чтения, так как сейчас мы начинаем оттуда читать
1 2 3 4 5 6 7 |
i2c_master_start(); i2c_master_writeByte(SLAVE_OWN_ADDRESS | I2C_REQUEST_READ); ack = i2c_master_checkAck(); if(!ack) { os_printf("ADDR not ack\r\n"); } |
Прочитаем наши байты из шины, только когда читаем последний, то отправляем условие NACK вместо ACK
1 2 3 4 5 6 7 8 |
os_printf("ADDR not ack\r\n"); } for(i=0;i<bytes_count;i++) { buf[i] = i2c_master_readByte(); if(i<(bytes_count-1)) i2c_master_send_ack(); else i2c_master_send_nack(); } |
Соберём код, прошьём контроллер и проверим, как у нас всё передаётся
Всё отлично принялось!
Итак, сегодня мы научились работать в своих программах с передачей и приёмом данных по интерфейсу I2C. На этом, конечно же, работа с данным интерфейсом не заканчивается. Будет ещё много уроков с его использованием.
Всем спасибо за внимание!
Предыдущий урок Программирование МК ESP8266 Следующий урок
Модуль ESP NodeMCU можно купить здесь: Модуль ESP NodeMCU
Различные модули ЕSP8266 можно приобрести здесь Модули ЕSP8266
Переходник USB to TTL можно приобрести здесь ftdi ft232rl
Многофункциональный переходник JTAG UART FIFO SPI I2C можно приобрести здесь CJMCU FT232H USB к JTAG UART FIFO SPI I2C
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Доброго дня. Поправьте пожалуйста ссылочку на исходник и название урока в списке.
Andry03
Поправил, спасибо.
А где же автор?
У Автора каникулы до марта.
уже переживаем, дайте хоть сигнал ка вы
max
Да вроде я всегда январь февраль блогов не пощу.
Спасибо за урок!
Очень полезно. Написал небольшую библиотечку для работы с часами реального времени DS3231: задать время, считать время в символьный буфер для дальнейшего вывода на LCD или в консоль.
Куда и кому можно задать вопросы по ESP8266? На форуме нет раздела по еспэшке?
Создал. И по ESP32 тоже.