Сравнительно низкоскоростной последовательный интерфейс. По нему подключаются:
- память (EEPROM, Flash, FRAM)
- микросхемы часов точного времени
- расширители интерфейсов
- датчики
- микросхемы управления светодиодами
- ЦАП/АЦП
- цифровые потенциометры
- RFID-сенсоры
- обмен данными между микроконтроллерами
Простой и нетребовательный к ресурсам, этот интерфейс нашел свое применение во всевозможных случаях обмена МК-периферия, где не нужна высокая скорость. На его основе сделаны SMBUS (диагностическая шина на материнской плате компьютера) и DDC (шина настройки режимов монитора), также он входит в состав интерфейсов UEXT и mikroBus.
Плюсы и минусы
Плюсы:
- Низкоскоростной, не нужны особые меры по эмс.
- Топология шина, поэтому для связи (вместе со всей адресацией) достаточно двух проводов SCL и SDA.
- Встроенная адресация на шине, позволяющая подключать по двум проводам большое количество устройств параллельно (даже однотипных, благодаря шинам выбора адреса).
- Большой возраст спецификации интерфейса, поэтому очень много устройств поддерживают его.
- Во многих МК встроен аппаратный модуль.
- Простота протокола.
Минусы:
- Нет обнаружения и коррекции ошибок.
- Обе линии — двунаправлены, что усложняет реализацию гальванической развязки и преобразователей уровня.
- Низкая скорость передачи. В первой версии 100 кбод, во второй 400 кбод, третью со скоростью 3.4 Мбод используют очень редко.
- Некоторые микросхемы содержат ошибку и не поддерживают собственные ножки выбора адреса.
Логика работы
Выходные каскады всех устройств сделаны по схеме с открытым коллектором и требуют подтяжки обоих линий к плюсу через резистор в 10 кОм. Так реализуется схема «голосования» за 1: на линии не может появиться 1, пока все устройства не выставят 1 на своих выходах. Так исключается ситуация конфликта передачи, и реализуется контроль потока: более медленные устройства могут замедлять обмен на линии, устанавливая 1 на скл только после того, как станут готовы принимать следующий бит.
Из этого вытекает логика работы хоста: при передаче очередного бита установить 1 на SCL, и только дождавшись реального появления 1 на линии продолжать передачу.
Хост генерирует старт, и начинает передавать биты по очереди, каждый новый бит должен появляться на SDA только во время 0 на SCL. Логика работы SCL: 0 — устанавливаем бит на линии SDA, 1 — бит принимают устройства. Дожидаемся реального 1 на SCL, выжидаем несколько миллисекунд и передаем следующий бит. Количество байт в посылке неограничено, прием каждого байта слейв подтверждает передачей 1. Хост заканчивает передачу сигналом стоп.
Библиотека stm32_i2c
Я подготовил библиотеку, в которой реализованы функции инициализации, записи и чтения одного байта, записи и чтения нескольких байт.
Очень важный вопрос рассмотрен в статье про отлов ошибок I2C и реакцию на них. Там приведены улучшенные функции, но они пока не внесены в основной репозиторий.
Практика
Давайте рассмотрим работу STM32 с шиной I2C на примере микросхемы памяти 24AA. EEPROM-память — настолько стандартный компонент, что реализация I2C в нём точно будет эталонной, да и очень многие другие устройства общаются по I2C в стиле микросхем памяти. К примеру, цифровой барометр BMP180 предоставляет несколько регистров с калибровочными данными (для чтения), регистр управления (запись определённого числа туда запускает измерение) и три регистра выходных данных (для чтения результата измерения) — работать с ними можно точно так же как с ячейками памяти EEPROM.
Я использую микроконтроллер STM32F107, модуль I2C1 на контактах PB6/PB7.
void I2C_init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_Init(GPIOB, &GPIO_InitStructure);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
I2C_InitTypeDef I2C_InitStructure;
I2C_StructInit(&I2C_InitStructure);
I2C_InitStructure.I2C_ClockSpeed = 100000;
I2C_InitStructure.I2C_OwnAddress1 = 1;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_Init(I2C1, &I2C_InitStructure);
I2C_Cmd(I2C1, ENABLE);
I2C_AcknowledgeConfig(I2C1, ENABLE);
}
void I2C_single_write(uint8_t HW_address, uint8_t addr, uint8_t data)
{
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, HW_address, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, addr);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_SendData(I2C1, data);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTOP(I2C1, ENABLE);
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
}
uint8_t I2C_single_read(uint8_t HW_address, uint8_t addr)
{
uint8_t data;
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, HW_address, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, addr);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, HW_address, I2C_Direction_Receiver);
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED));
data = I2C_ReceiveData(I2C1);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
I2C_AcknowledgeConfig(I2C1, DISABLE);
I2C_GenerateSTOP(I2C1, ENABLE);
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
return data;
}
void delay()
{
for(volatile uint32_t del = 0; del<250000; del++);
}
int main()
{
uint8_t single_data_read = 0;
I2C_init();
delay();
I2C_single_write(0xA0, 0x00, 0x15);
delay();
single_data_read = I2C_single_read(0xA0, 0x03);
while(1);
}