Шина I2C существует уже достаточно давно: ее в 1980х создала компания Philips для низкоскоростных устройств. В настоящий момент она достаточно широко применяется, и, скорей всего, дома у вас есть хоть одно устройство с данной шиной. Название шины расшифровывается как Inter-Integrated Circuit. Хардварным модулем I2C в настоящее время обладает большинство микроконтроллеров, в некоторых их и вовсе несколько, как у тех же STM32 (в серии F4 есть целых три модуля I2C).

I2C_bus

Шина представляет собой 2 линии, одна из которых данные (SDA), другая — синхросигнал (SCL), обе линии изначально притянуты к питанию. Следует отметить, что четкого указания какое именно должно быть напряжение нет, но чаще всего используется +5В и +3.3В. Устройства в линии не одноранговые, как в CAN, поэтому всегда должно быть Master-устройство. Допускается наличие нескольких Master-устройств, но это все же гораздо реже, чем один Master и ворох Slave устройств.

Передача данных инициируется мастером, который отправляет в шину адрес необходимого устройства, тактирование осуществляется так же мастером. Но, при этом, Slave-устройство имеет возможность «придержать» линию тактирования, как бы сообщая Master-устройству, что не успевает принять или отправить данные, что порой бывает очень полезно. Наибольшее распространение получили в текущий вариант реализации I2C с частотой шины 100 kHz (Standard mode) и 400 kHz (Fast mode).

Существует реализация I2C версии 2.0, которая позволяет достичь гораздо больших скоростей, в 2-3 Мбит/с, но они пока что весьма редкие. Так же у линии есть ограничение по емкости в 400 пФ. Обычно в даташитах для датчиков и прочих I2C устройств указывается их емкость, так что приблизительно можно вычислить «влезет» ли еще один датчик или нет.

В микроконтроллерах очень часто есть внутренняя подтяжка на выводах, что в свободном состоянии даст необходимые +3.3В (или +5В) на линии, но этой подтяжки абсолютно не хватит на нормальную линию. Поэтому всегда стоит делать внешнюю подтяжку и SCL и SDA к питанию резисторами в 4.7кОм..2кОм.

Отдельно стоит отметить то, что обычно линию I2C не рекомендуют делать длинной, да и чаще всего она встречается на печатных платах для обмена между некими цифровыми устройствами, гораздо реже I2C пускают по проводам (но не стоит думать, что это редкость, и то и другое вполне нормально). Если у вас возникла надобность сделать длинную линию I2C, да еще на 400 кГц, то стоит уменьшить сопротивление резисторов подтяжки. 1 кОм — вполне приемлемое значение для линии длиной чуть более метра и с несколькими устройствами на ней. Только не забывайте, что уменьшая сопротивление резисторов, вы увеличиваете ток в линии, что при переизбытке может привести к повреждению устройств.

С программной точки зрения обмен по шине I2C выглядит следующим образом: Master отправляет стартовую последовательность START (при высоком уровне SCL к нулю притягивается SDA), затем отправляет адрес с бит-флагом, указывающим режим чтения или записи, причем в следующим формате:

i2c_address

Если бит режима равен нулю, то это значит, что Master будет записывать информацию в Slave устройство, единица — чтение из Slave. Если взглянуть на это с другой стороны, то каждое I2C устройство предоставляет два «виртуальных» устройства, исходя из чего получается, что если весь байт адреса (т.е. исконные 7 бит + бит режима) четный, то это адрес записи, если нечетный — адрес чтения. Исходя из этого появляется ограничение на количество устройств в шине: 127.

После получения адреса Slave устройство должно сообщить мастеру о принятии адреса, что подтвердит сам факт существования Slave устройства с таким адресом на линии. Подтверждение — это специальный 9й бит, который равен нулю, если адрес совпал и готовы работать, и единице, если не совпал. Это сигналы ACK и NACK соответственно. Так же, ACK используется при последующим приеме и передаче данных. Если мастер записывает в слейв, то слейв должен каждый байт подтверждать сигналом ACK. Если слейв отправляет данные мастеру, то мастер должен на все байты отвечать ACK, кроме последнего — это будет сигналом, что больше отправлять данные не требуется.

В конце всей передачи Master должен отправить завершающую последовательность STOP, которая заключается в поднятии линии SDA до высокого уровня при поднятой линии SCL.

Таким образом, стандартный «пакет» выглядит следующим образом:

i2c_diagram

Теперь можно перейти к рассмотрению работы с данной шиной на микроконтроллере STM32. Сразу стоит заметить, что во всех сериях данный модуль приблизительно одинаковый, за исключением регистра фильтра в старших сериях (например, STM32F407), поэтому единожды написанный код сможет работать и далее.

Для начала следует включить тактирование модуля I2C, что, впрочем, необходимо и для всей периферии. Так же необходимо включить и настроить пины в режим альтернативной функции. Чтобы посмотреть на какой шине что находится, необходимо обратиться к даташиту, в раздел Device Overview (для F4, это, например, страница 18) (рис.1). Из изображения видно, что I2C находятся на шине APB1. Следующий шаг — включение и настройка GPIO, все, что необходимо: режим альтернативной функции (по даташиту I2C относится к AF4), тип OpenDrain, а подтяжка должна быть внешняя. «Скорость» пинов для 100кГц можно выбрать Low (2 MHz), а для 400 кГц ST рекомендуют выбирать уже Medium или Fast (от 10 MHz). И, наконец, можно настроить I2C. Показывать регистры не имеет смысла, они есть все в reference manual, все, что нужно для стандартного случая будет ниже. До включения непосредственно модуля I2C следует в регистр CR2 записать текущее значение частоты той шины, на которой сидит модуль I2C, в данном случае это частота шины APB1. В рамках даташита это значение называется PCLK.

В разных контроллерах количество и именование шин разное. Так, в серии F4xx есть и APB1 и APB2, и переменная PCLK будет соответственно нумероваться — PCLK1 и PCLK2. Чтобы посмотреть или высчитать конкретную частоту тактирования шины можно воспользоваться приложением CubeMX, которое загружается с официального сайта ST Microelectronics.

stm32 i2c architecture

Рис. 1 — Схема периферии контроллера STM32F407.

В регистре CR2 так же включаются прерывания от данного модуля. Под этим понимается то, что будет ли модуль сообщать в NVIC о том, что что-то произошло, либо же просто поставит нужные флаги в статусном регистре. Стоит заметить, что в статусном регистре всегда будут ставиться событийные флаги, что логично. В первую очередь интересны прерывания ITEVTEN и ITERREN, прерывания событий и ошибок соответственно. Можно обойтись вполне и только событиями, как наиболее общим.

I2C1->CR2 |= 48;           // Peripheral frequency 24MHz
I2C1->CR2 |= I2C_CR2_ITEVTEN;       // Enable events

Регистр CCR отвечает за тактирование самой шины наружу, поэтому сюда необходимо внести значение, которое рассчитывается по формуле PCLK/I2C_SPEED. Например, мы хотим шину на 400 кГц завести, внутренняя шина APB1 тактируется 48 МГц, соответственно в CCR запишем значение, равное 48*106/4*105 = 120. Так же в данном регистре необходимо указать режим работы Slow/Fast, это последний, 16й бит.

I2C1->CCR &= ~I2C_CCR_CCR;
I2C1->CCR |= 120;
I2C1->CCR |= I2C_CCR_FS;	// FastMode, 400 kHz

Регистр TRISE отвечает за фронты сигналов на SDA и SCL, сюда необходимо внести значение с небольшим запасом. Можно и без запаса, главное не меньше — ничего не заработает. Вносимое значение рассчитывается так: TRISE = RISE/tPCLK. tPCLK = 1/PCLK. Константа RISE — это максимальное время нарастания сигнала, по спецификации это 1000 нс для Slow Mode и 300 нс для Fast mode. tPCLK — это просто период, получается стандартно по формуле 1/F. Так как у нас Fast Mode, то значение в TRISE необходимо следующее: 3.000*10-7/2.083*10-8 = 14.4, и т.к. необходим запас, то округляем в большую сторону, т.е. 15.

Данный показатель важен, но не настолько, как сбившаяся частота тактирования. Я по ошибке посчитал константу TRISE в Fast Mode по формуле для Slow Mode и все работает. Но все же лучше делать правильно, по спецификации шины. Найти ее можно по поисковой фразе “i2c specification”. Да-да, она на английском языке.

I2C1->TRISE = 24;

После того, как данные действия будут выполнены, можно включать модуль и прерывания в модуле NVIC (если нужны).

I2C1->CR1 |= I2C_CR1_PE;            // Enable I2C block
NVIC_EnableIRQ(I2C1_EV_IRQn);
NVIC_SetPriority(I2C1_EV_IRQn, 1);

После настройки модуля, есть два варианта как вы будете с ним работать. Вариант первый — поллингом, т.е. вы будете ждать появления флага в цикле while (и контроллер будет занят только этим, что в большинстве случаев плохо), либо вариант второй — на прерываниях. Я рассмотрю второй вариант, так как он предпочтительный, а если понять логику состояний по прерываниям, то перейти на поллинг не является проблемой.

Итак, первое что необходимо сделать — добавить в код обработчик прерываний событий от модуля I2C. Функция должна называться определенным образом, и ее название можно взять из startup-файла. Для модуля I2C1 функция называется I2C1_EV_IRQHandler. Поэтому в необходимый .c файл добавляем такую функцию:

void I2C1_EV_IRQHandler(void) {
}

Именование данного метода, понятное дело, можно изменять. В startup-файле в ассемблерном коде просто создаются метки, которые не требуют обязательного наличия данной функции. Компилятор языка Си найдя в исходном файле функцию, например ту же I2C1_EV_IRQHandler, выделить под нее точно такое же имя. Когда уже будет происходить окончательная сборка, все это сведется воедино и вместо имени будет присутствовать переход в нужную позицию в коде. Поэтому можно изменять название метки как заблагорассудится (по правилам именования функций, конечно), хотя и не рекомендуется — другие разработчики могут просто не понять, обработчик прерываний это или нет.

Если вы пишете на языке С++, не забудьте «обернуть» обработчик прерываний в блок extern “C” { … }, так как компилятор С++ изменяет имя функции по своим правилам во время компиляции (туда вносится информация о параметрах и возвращаемом значении, например), поэтому сборщик потом не свяжет написанный обработчик и метку в startup файле.

Для написания обработчика прерываний можно обратиться напрямую к документации, reference manual, там есть достаточно подробные схемы для разных режимов работы. Для начала возьмем отправку slave-устройству данных:

i2c master transmitter

Например, мы хотим просто отправить 1 байт данных устройству и прекратить передачу. Для этого нам потребуется только состояния EV5, EV6 и EV8. Где-то в коде программы у нас была глобальная переменная data типа uint8_t, которую мы проинициализировали каким-то значением и хотим передать slave устройству. Инициацию передачи, как мы уже знаем, делает последовательность START:

I2C1->CR1 |= I2C_CR1_START;

Данную строку можно поставить по ходу программы там, когда нужно начинать передачу. Например, после того, как инициализировали переменную data. Дальше уже будет код внутри функции-обработчика прерываний. Для начала необходимо в отдельные переменные сохранить значение статусов:

volatile uint32_t sr1 = I2C1->SR1, sr2 = I2C1->SR2;

После отправки стартовой последовательности произойдет прерывание с событием EV5. В данном случае в статусном регистре должен выставиться бит SB. Если данный бит выставлен, нам необходимо отправить адрес с битом режима чтения или записи. Для упрощения можно сделать так:

#define I2C_MODE_READ	1
#define I2C_MODE_WRITE	0
#define I2C_ADDRESS(addr, mode)	((addr<<1) | mode) 

Теперь можно написать обработчик состояния EV5:

if( sr1 & I2C_SR1_SB ) {
    I2C1->DR = I2C_ADDRESS(0x14,I2C_MODE_READ);
}

Когда адрес отправится и slave-устройство ответит последовательностью ACK, то произойдет событие EV6 и одновременно EV8: установится флаг ADDR и TXE. А рамках Master-режима, ADDR означает, что адрес отправлен и воспринят slave-устройством, а TXE означает, что буфер свободен для внесения данных для последующей передачи. Флаг ADDR сбросится сам, как только мы прочитаем SR1 и SR2 (необходимо их оба прочитать), а флаг TXE обработаем отдельным блоком кода. Так что, по факту, обрабатывать необходимо только EV5 и EV8, EV6 только информирует о наличии нужного slave на линии. В обработчике TXE все, что нужно — это передавать данные. Так как передавать мы хотим только 1 байт, то сразу же отправим и последовательность STOP:

if(sr1 & I2C_SR1_TXE) {
    I2C1->DR = data;
    I2C1->CR1 |= I2C_CR1_STOP; 
}

Таким образом, заполнив переменную data и дав команду формирования последовательности START, вся работа будет идти в прерываниях, а контроллер тем временем будет занят другой полезной работой большую часть времени (т.е. другая работа кроме, собственно, обработчика прерываний).

Если данных требуется отправить больше 1 байта, то изменения в коде минимальны. Теперь вместо uint8_t data создадим такие глобальные переменные:

uint8_t iter;
uint8_t data[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

Так как глобальные переменные целого типа по умолчанию равны нулю, то явно инициализировать переменную iter не требуется. Изменения в обработчике же вообще минимальные — требуется переписать блок обработки события EV8:

if(sr1 & I2C_SR1_TXE) {
    if( iter < 10 ) {
        I2C1->DR = data[iter++];
    } else {
        I2C1->CR1 |= I2C_CR1_STOP; 
    }
}

Таким образом, мы получили вот такую функцию-обработчик:

#define I2C_MODE_READ	1
#define I2C_MODE_WRITE	0
#define I2C_ADDRESS(addr, mode)	((addr<<1) | mode) uint8_t iter;
uint8_t data[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

void I2C1_EV_IRQHandler(void) {
    volatile uint32_t sr1 = module ->SR1, sr2 = module ->SR2;
    if( sr1 & I2C_SR1_SB ) {
        module ->DR = I2C_ADDRESS(0x14,I2C_MODE_READ);
    }
    if(sr1 & I2C_SR1_TXE) {
        if( iter < 10 ) {
            I2C1->DR = data[iter++];
        } else {
            I2C1->CR1 |= I2C_CR1_STOP; 
        }
    }
}

Код можно модифицировать и далее, создав, например, контекст модуля, сделать одну функцию-обработчик, которая будет на вход принимать только контекст и делать необходимые действия с необходимым модулем и так далее. Например, можно сделать такую функцию:

void I2C_handler(I2C_TypeDef* module, uint8_t addr, uint8_t data) {
    volatile uint32_t sr1 = module ->SR1, sr2 = module ->SR2;
    if( sr1 & I2C_SR1_SB ) {
        module ->DR = I2C_ADDRESS(addr,I2C_MODE_READ);
    }
    if(sr1 & I2C_SR1_TXE) {
	module ->DR = data;
	module ->CR1 |= I2C_CR1_STOP; 
    }
}

Это позволит одну и ту же функцию использовать сразу в двух модулях. Например, мы можем ее вставить вот так:

void I2C1_EV_IRQHandler(void) {
    I2C_handler(I2C1, 0x14, 0x10);
}

void I2C1_EV_IRQHandler(void) {
    I2C_handler(I2C1, 0x27, 0xFF);
}

Еще раз напомню, что адрес устройства, который мы видим в документации — это биты [7:1] из того байта, который передается модулем, а бит 0 — это режим. Так, указав выше в аргументах адрес 0x14 и режим передачи данных, я получу на передачу байт 0x29. Так как в макросе проверки нет, стоит не забывать, что передавать в него можно максимум адрес 0x7F, иначе получите чехарду.

Для режима чтения все похоже, как можно видеть из диаграммы:

i2c master receiver

Для обработки нам нужны состояния EV5, EV6, EV7, EV7_1. Статус EV6 по-прежнему сбросится сам после чтения регистров SR1 и SR2, а статус EV7_1 соответствует последнему необходимому байту. Т.е. когда мы приняли предпоследний байт, мы должны отключить отправку сообщения ACK слейву, чтобы следующий байт уже был последним. Итак, возьмем наш предыдущий код и просто внесем в него дополнительный обработчик такого вида, чтобы принять 10 байт данных:

if( sr1 & I2C_SR1_RXNE ) {
    if( rx_iter == 8 ) {
        I2C1->CR1 &= ~I2C_CR1_ACK;
    } else if (rx_iter == 9) {
        I2C1->CR1 |= I2C_CR1_ACK;
    }
    if( rx_iter < 10 ) {
        rx_data[rx_iter++] = I2C1->DR;
    }
}

При этом должны быть глобальные переменные:

uint8_t rx_iter;
uint8_t rx_data[10];

Таким образом, получили вот такой обработчик прерываний для модуля I2C1:

#define I2C_MODE_READ	1
#define I2C_MODE_WRITE	0
#define I2C_ADDRESS(addr, mode)	((addr<<1) | mode) uint8_t iter;
uint8_t data[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
uint8_t rx_iter;
uint8_t rx_data[10];
uint8_t i2c_mode;

void I2C1_EV_IRQHandler(void) {
    volatile uint32_t sr1 = I2C1->SR1, sr2 = I2C1->SR2;
    if( sr1 & I2C_SR1_SB ) {
        I2C1->DR = I2C_ADDRESS(0x14,i2c_mode);
    }
    if(sr1 & I2C_SR1_TXE) {
        if( iter < 10 ) {
            I2C1->DR = data[iter++];
        } else {
            I2C1->CR1 |= I2C_CR1_STOP; 
        }
    }
    if( sr1 & I2C_SR1_RXNE ) {
        if( rx_iter == 8 ) {
            I2C1->CR1 &= ~I2C_CR1_ACK;
        } else if (rx_iter == 9) {
            I2C1->CR1 |= I2C_CR1_ACK;
        }
	
        if( rx_iter < 10) {
            rx_data[rx_iter++] = I2C1->DR;
    }
}

Данных выше хватает, в общем-то, для очень многих случаев, и, поняв логику написания кода выше, можно по документации будет расширить код необходимым. И еще одно — часто для того, чтобы что-то прочитать из slave-устройства, необходимо в него что-то записать.