Шина I2C является синхронной, последовательной и полудуплексной. Полудуплексность означает что в один момент времени по шине может происходить либо прием либо передача для какого либо одного устройства. Данная шина также ассиметричная что означает наличие ведомого (мастера который выдает импульсы синхронизации и управляет коммуникацией) и ведущего (слейва) на ней. Но в I2C, в отличии от SPI, мастеров может быть больше одного. Однако обычно мастер только один. В данной шине имеется два сигнальных провода, один нужен для передачи данных и называется SDA а другой для передачи синхроимпульсов и называется SCL. Синхроимпульсы поступают от мастера к слейвам. А данные могут поступать в обе стороны т.е. от мастера к слейву или от слейва к мастеру. Т.к. провод данных только один то пин микроконтроллера к которому подключен провод SDA должен переключаться между режимом входа и выхода в процессе работы. Если пин SDA работает как выход то этот выход должен быть с открытым стоком т.к. возможна ситуация при которой у двух и более разных устройств будут выходы на данном проводе одновременно и с разными уровнями напряжений. Тоже самое верно и для пина SCL т.к. мастеров м.б. больше одного. Данные пины, соответственно, подтягиваются к плюсу питания резисторами сопротивления которых не следует делать слишком большими т.к. чем больше сопротивление резистора тем длиннее фронт сигнала т.к. при меньшем токе медленнее заряжается паразитная емкость шины. Данная емкость также ограничивает максимальное количество устройств на шине. Однако, тем не менее, оно всё таки может быть достаточно велико и скорее всего больше чем устройств на SPI т.к. колличество слейвов на SPI ограничено количеством свободных пинов GPIO мастера. И хотя этот недостаток можно преодолеть использованием например десятичных счетчиков или сдвиговых регистров. I2C все таки более предпочтительная шина для связи большого количества устройств из за меньшего количества проводов, дополнительных деталей и большей безопасности выходов с открытым стоком по сравнению с полумостовыми выходами. Изучение данной шины лучше начать с организации связи микроконтроллера с какой нибудь не сложной спецаиализированной микросхемой, например с микросхемой EEPROM памяти. Микроконтроллеры STM32 обычно не имеют встроенной EEPROM памяти как например микроконтроллеры AVR или PIC. Но в этом нет большой необходимости т.к. EEPROM может быть заменена FLASH памятью. Внешняя EEPROM память обладает некоторыми преимуществами перед внутренней FLASH памятью. Например обычно EEPROM память обладает большим количеством циклов записи, в этом можно убедиться посмотрев даташит например на микроконтроллер ATtiny2313 где указано:
2K Bytes of In-System Self Programmable Flash
Endurance 10,000 Write/Erase Cycles
–
128 Bytes In-System Programmable EEPROM
Endurance: 100,000 Write/Erase Cycles
Видна разница в 10 раз. Недостатком EEPROM памяти является то что срок хранения в ней информации ограничен. И хотя у современных микросхем он высок, он всё же конечен. Вынеся устройство постоянного хранения информации за пределы микроконтроллера, появляется возможность ремонта устройства с истекшим сроком хранения информации или количеством циклов записи, путем замены микросхемы памяти что дешевле и проще замены микроконтроллера. Ещё т.о. не занимая FLASH память, которая также используется для хранения инструкций выполняемых микроконтроллером, можно избежать некоторых возможных багов связанных с заимствованием FLASH памяти для кода программы и естественно так для кода программы места будет больше. Поэтому связь микроконтроллера по I2C с микросхемой EEPROM памяти имеет (помимо образовательной) также и некоторую практическую ценность. В микроконтроллере stm32f103c8 есть 2 интерфейса I2C. Для постоянного хранения информации можно использовать например микросхему EEPROM памяти AT24C64 в которой можно хранить до 64 кБ данных. Данная микросхема может хранить данные до 100 лет а количество циклов записи у неё 1 миллион раз. Она имеет пины I2C, 3 пина установки адреса (которые внутренне подтянуты к земле), пин зашиты записи (WP который внутренне подтянут к уровню отключающему данную опцию). Соединить данную микросхему с отладочной платой с микроконтроллером stm32f103c8 можно например так так на схеме:
Рисунок 1 - Схема соединения платы "blue pill" с микросхемой AT24C64 по I2C
Резисторы R3 и R4 делают подтяжку к + питания открытых стоков, чтобы помимо логических нулей могли быть и логические единицы. Они могут быть сопротивлением например 10к но для укорачивания фронтов выбраны сопротивления поменьше. Резисторы R1 и R2 нужны для ограничения тока на случай неправильного конфигурирования пинов микроконтроллера. При подключении данной микросхемы следует обратить внимание на расположение первого пина. Если на микросхеме отсутствует привычная точка для его нахождения (а такое бывает) то определить первый пин можно по скосу с одной стороны микросхемы.
Рисунок 2 - Стороны корпуса микросхемы AT24C64
В даташите, почему то, не уточнили что на нижнем рисунке показан передний конец а не задний. Если микросхему перевернуть и подать напряжение наоборот (на GND + а на Vcc -) то данная микросхема начнет потреблять нехарактерно большой для неё ток. В даташите также есть диаграммы которые помогут реализовать связь с данной микросхемой по I2C.
Рисунок 3 - Диаграмма записи одного байта в ячейку памяти микросхемы EEPROM памяти AT24C64
Чтобы записать один байт данных в какую либо ячейку памяти микросхемы
AT24C64, нужно сначала передать ей, по I2C, старт сигнал. Микроконтроллер сам правильно формирует этот сигнал при выполнении соответствующей команды. После чего надо послать лог. 1 если микроконтроллер будет работать в режиме мастер и лог.0 если микроконтроллер будет слейвом. Слейвом, в данном случае, микроконтроллер точно не будет т.к. EEPROM работает только в режиме слейва и не знает к каким ячейкам её памяти необходимо обращаться в какие моменты времени. Далее, именно для данной микросхемы, передаются 3 фиксированных бита адреса, после чего 3 настраиваемых. Эти 3 настраиваемых бита можно выставить меняя логические уровни напряжений на пинах A0...A2 микросхемы AT24C64. Если оставить данные пины никуда не подключенными то на них будут нули и следовательно 3 настраиваемых бита адреса тоже будут нулями. Если нужно увеличить память то можно подключить некоторое количество (до 8 шт. (больше адресное пространство не позволяет)) таких микросхем и использовать возможность установки адреса. Следующий бит указывает будет ли осуществляться запись байта данных в текущую ячейку EEPROM памяти (лог.0) или чтение (лог.1) его из этой ячейки. После того как слейв получил все эти 8 бит он формирует ответ просаживая линию SDA на землю или формирует отсутствие ответа не просаживая линию SDA на землю т.е. оставляя лог.1 на ней. Для чтения данного ответа или его отсутствия в микроконтроллере есть специальный флаг. После получения ответа микроконтроллер должен отослать слейву два байта адреса ячейки к которой требуется обратиться для чтения или записи в неё. Т.к. памяти в данной микросхеме 64кб то нужно именно два байта т.к. в один не поместятся все адреса а если байтов будет больше двух то как минимум один будет лишним. После передачи одного байта адреса, слейв также формирует ответ, после которого нужно отправить второй байт адреса. Если передача адреса прошла успешно то в микросхеме EEPROM памяти произойдет переключение на ячейку памяти по переданному адресу и к этой ячейке можно будет обращаться. EEPROM подтверждает данный успех выдавая ответ просаживанием линии SDA на 0 питания. Если последний бит байта адреса слейва указывал запись то после того как EEPROM переключился на какую то ячейку памяти, передачей в данный EEPROM ещё одного байта, запуститься запись этого байта в текущую ячейку памяти. После чего от этой микросхемы придет ответ и если больше ничего записывать не надо то слейву выставляется сигнал "стоп". Если нужно записать больше одного байта то они просто передаются по очереди а текущий адрес ячейки инкрементируется сам. Если байт нужно не прочитать а записать то сначала нужно перейти к требуемой ячейке памяти. Сделать это так как это делалось бы для случая записи. После перехода к требуемой ячейке памяти, передается сигнал "старт" ещё раз. После чего передается адрес слейва но последний бит теперь должен указывать на чтение а не на запись т.е. он д.б. лог.1. После успешного перехода в режим чтения, слейв выдает ответ и матер может подать синхроимпульс на SCL и прочитать бит данных по SDA после чего подать ещё один синхроимпульс и прочитать ещё один бит данных и так до тех пор пока не прочтется один байт, после этого мастер либо подает ответ слейву (так как ранше делал только слейв для мастера) и прочитать ещё один байт данных либо не передать ответ. После не передачи ответа, мастер передает сигнал "стоп" и соединение заканчивается.Рисунок 4 - Диаграмма чтения одного байта данных из микросхемы AT24C64
В начале кода, как всегда, будет подключение заголовочного файла библиотеки CMSIS. После две глобальные переменные (хотя их можно сделать локальными) для чтения и для записи. Также будет вспомогательная функция для отправки символа по UART. В функции main() нужно включить тактирование всей необходимой периферии (UART1, I2C2, GPIO порт A и GPIO порт B). Пины
SCL и SDA нужно настроить на альтернативный выход с открытым стоком.В регистр I2C2->CR2 записывается частота шины от которой тактируется I2C2. По умолчанию это 36 МГц. В регистр I2C2->CСR записывается число на которое надо поделить частоту шины APB1 чтобы получилась удвоенная частота работы I2C. Т.к. I2C это синхронная шина то частота м.б. любой но обычно используются две, 100 кГц - обычный режим и 400 кГц - ускоренный. Чтобы настроить I2C2 на обычный режим нужно в регистр I2C2->CСR записать число 180 т.к. 100 000 Гц * 2 = 36 000 000 Гц / 180. В регистр I2C2->TRISE нужно записать число на единицу большее чем в регистре I2C2->CR2 т.е. чем частота шины APB1. Получить байт данных для записи в EEPROM лучше в начале цикла, после чего прочитать байт данных из ячейки в переменную для этого а потом записать байт для записи т.к. иначе новое значение в ячейке перезапишет старое и протестировать двустороннюю связь микроконтроллера с микросхемой AT24C64 по I2C не получиться.
Чтобы разрешить работу I2C2 нужно установить бит 0 регистра I2C2->CR1:
Чтобы разрешить работу чтобы подать сигнал "старт" установить бит 8 этого регистра I2C2->CR1:
Прежде чем послать байт адреса устройства нужно дождаться окончания формирования сигнала "старт", для проверки этого есть бит 0 регистра I2C2->SR1:
После того как данный сигнал сформирован нужно прочитать регистр I2C2->SR1:
Теперь можно передать адрес слейва для установки связи с ним. В документации на микросхему показано как формируется её адрес:
После того как произведен запуск отправки адреса слейва чтобы переходить к следующей части коммуникации микроконтроллера с AT24C64 нужно дождаться окончания отправки этого адреса и приема ответа от слейва. Об этом сигнализирует бит 1 регистра I2C2->SR1:
После того как данный бит установится в единицу нужно будет прочитать регистры I2C2->SR1 и I2C2->SR2.
После этого можно перейти к переходу к нужной ячейке памяти в AT24C64. Для этого нужно передать сначала старшую часть адреса ячейки памяти а потом младшую. Передаются части адресов ячеек также т.е. записью их в регистр данных I2C2->DR. После того как произведен запуск отправки старшей части адреса ячейки памяти, нужно дождаться окончания отправки и передать младшую часть. Часть адреса ячейки памяти, в отличии от адреса слейва, является данными, как и те данные которые хранятся, записываются и читаются в эти ячейки. Поэтому для проверки окончания записи части адреса есть флаг окончания передачи данных по I2C который является битом 2 регистра I2C2->SR1:
Далее для того чтобы прочитать байт данных из текущей ячейки (адрес которой был передан) нужно снова послать сигнал "старт" после чего адрес устройства но теперь с битом указывающим чтение а не запись.
Потом также дождаться окончания передачи адреса и прочитать статусные регистры. После чего, если прием будет только одного байта данных то, просто дождаться прихода байта и забрать его из регистра данных. Если планируется забрать более одного байта из ячеек следующих после текущей то надо заранее выставить бит 10 первого регистра управления т.к. этот бит отвечает за автоматическй ответ мастера слейву после успешного приема байта данных. Об окончании приема байта данных свидетельствует бит 6 регистра I2C2->SR1:
Если принимается не один байт то перед приемом последнего надо убрать бит автоматической выдачи ответа. После успешного приема данных нужно сделать сигнал "стоп" установкой бита 9 регистра I2C2->CR1:
После того как байт данных получен его можно вывести на экране компьютера передав по uart через usb-uart переходник. Для этого можно воспользоваться функцией sendSumbol() (написанной над функцией main()). Записать байт данных принятый по uart проще. Для этого проделываются все те же действия как и для чтения до момента передачи сигнала "старт" второй раз. Вместо которого передается ещё один байт который и запишется в текущую ячейку памяти.
Полный код программы можно скопировать из текстового поля:
Проверить программу можно загрузив её в микроконтроллер, подключив через USB-UART переходник к компьютеру на котором надо запустить например "монитор последовательного порта" (или монитор просто порта (или штуки аналогичного назначения но с другим названием)) среды Arduino IDE (или другой программы с аналогичным функционалом). В этом мониторе надо установить скорость 9600 (если такая скорость уже заранее там не установлена (обычно для установки скорости в Arduino IDE есть выпадающий список справа в низу)). Если отправить один символ по UART (вписав его в верхнюю строку и нажав кнопку "send" или кнопку с другим названием но аналогичной функцией) то он запишется в ячейку памяти EEPROM а в мониторе порта выведется символ который был в этой ячейке до этого. Если, после этого, перезагрузить микроконтроллер (нажав соответствующую кнопку на плате "blue pill" (если конечно используется именно такая плата)) или отключить питание (монитор порта, перед этим (а также перед перезагрузкой), следует выключить) после чего его снова подать (после подачи питания можно включить монитор порта) и послать символ по UART то можно увидеть что в мониторе порта появился символ отосланный ранее. Это свидетельствует о том что он был сохранен в энергонезависимой памяти EEPROM и связь по I2C выполнилась успешно в обе стороны.
Видео по данной теме: