В предыдущей статье на нашем сайте мы рассмотрели использование интерфейса SPI в плате Arduino. А здесь мы рассмотрим еще один очень популярный в настоящее время протокол последовательной связи I2C (Inter Integrated Circuits - последовательная шина обмена данными между интегральными схемами) и как его использовать в плате Arduino. Если сравнивать протоколы I2C и SPI, то I2C использует только 2 линии (провода), а SPI использует 4 линии, I2C может иметь несколько ведущих (Master) и несколько ведомых (Slave), а SPI может иметь только одного ведущего и нескольких ведомых. То есть если в вашем проекте сразу несколько микроконтроллеров должны быть ведущими, то тогда вам нужно использовать интерфейс (протокол) I2C. Протокол I2C обычно используется для взаимодействия с гироскопами, акселерометрами, датчиками давления, LED дисплеями и т.д.
В этом проекте мы будем использовать протокол I2C для обмена данными между двумя платами Arduino и передавать между ними значения (от 0 до 127) с помощью потенциометра. Эти принятые значения будут отображаться на ЖК дисплеях, подключенных к каждой плате Arduino. Одна из плат Arduino будет выступать в роли ведущего (Master), а другая – в роли ведомого (Slave).
Что такое протокол I2C и как он работает
Термин IIC расшифровывается как “Inter Integrated Circuits” и часто обозначается как I2C или даже как TWI (2-wire interface protocol), но во всех случаях за этими обозначениями скрывается один и тот же протокол. I2C представляет собой протокол синхронной связи – это значит что оба устройства, которые обмениваются информацией с помощью данного протокола должны использовать общий сигнал синхронизации. Поскольку в этом протоколе используются всего 2 линии (провода), то по одной из них должен передаваться сигнал синхронизации, а по другой – полезная информация.
Впервые протокол I2C был предложен фирмой Phillips. Протокол в самом простом случае соединяет с помощью 2-х линий 2 устройства, одно из устройств должно быть ведущим, а другое – ведомым. Связь возможна только между ведущим и ведомым. Преимуществом протокола (интерфейса) I2C является то, что к одному ведущему можно подключить несколько ведомых.
Схема связи с помощью протокола I2C представлена на следующем рисунке.
Назначение линий данного интерфейса:
- Serial Clock (SCL): по ней передается общий сигнал синхронизации, генерируемый ведущим устройством (master);
- Serial Data (SDA): по ней осуществляется передача данных между ведущим и ведомым.
В любой момент времени только ведущий может инициировать процесс обмена данными. Поскольку в этом протоколе допускается несколько ведомых, то ведущий должен обращаться к ним, используя различные адреса. То есть только ведомый с заданным (указанным) адресом должен отвечать на сигнал ведущего, а все остальные ведомые в это время должны "хранить молчание". Таким образом, мы можем использовать одну и ту же шину (линию) для обмена данными с несколькими устройствами.
Уровни напряжений для передаваемых сигналов в интерфейсе I2C жестко не определены. В этом плане I2C является достаточно гибким, то есть если устройство запитывается от напряжения 5v, оно для связи с помощью протокола I2C может использовать уровень 5v, а если устройство запитывается от напряжения 3.3v, то оно для связи с помощью протокола I2C может использовать уровень 3v. Но что делать если с помощью данного протокола необходимо связать между собой устройства, работающие от различных питающих напряжений? В этом случае используются преобразователи/переключатели напряжения (voltage shifters).
Существует несколько условий для осуществления передачи данных в протоколе I2C. Инициализация передачи начинается с падения уровня на линии SDA, которое определяется как условие для начала передачи (‘START’ condition) на представленной ниже диаграмме. Как видно из этого рисунка, в то время как на линии SDA происходит падение уровня, в это же самое время на линии SCL ведущий поддерживает напряжение высокого уровня (high).
То есть, как следует из рисунка, падение уровня на линии SDA является аппаратным триггером для условия начала передачи. После этого все устройства на этой шине переключаются в режим прослушивания.
Аналогичным образом, повышение уровня на линии SDA останавливает передачу данных, что на представленной диаграмме обозначено как условие окончания передачи данных (‘STOP’ condition). В это же самое время ведущим на линии SCL поддерживается напряжение высокого уровня (high).
На следующем рисунке представлена структура адреса ведомого в протоколе I2C.
Бит R/W показывает направление передачи следующих за ним байт, если он установлен в HIGH – это значит что будет передавать ведомый (slave), а если он установлен в low – это значит что будет передавать ведущий (master).
Каждый бит передается в своем временном цикле, то есть нужно 8 временных циклов чтобы передать байт информации. После каждого переданного или принятого байта 9-й временной цикл используется для подтверждения/не подтверждения (ACK/NACK) приема информации. Этот бит подтверждения (ACK bit) формируется либо ведомым, либо ведущим в зависимости от ситуации. Для подтверждения приема информации (ACK) на линии SDA ведущим или ведомым устанавливается низкий уровень (low) в 9 временном цикле, в противном случае происходит не подтверждение приема информации (NACK).
На следующем рисунке представлена структура передаваемого сообщения в протоколе I2C.
Где применяется протокол I2C
Протокол I2C используется для передачи информации только на короткие расстояния. Он обеспечивает достаточно надежную передачу данных из-за наличия в нем сигнала синхронизации. Обычно данный протокол используется для передачи информации от датчиков или других устройств ведущим устройствам. В данном случае несомненным удобством использования протокола I2C является то, что при обмене данными с ведомыми устройствами ведущий микроконтроллер использует минимум линий (контактов). Если вам нужна связь на более далекие расстояния, то вам необходимо присмотреться к протоколу RS232, если же вам нужна более надежная связь чем в протоколе I2C, то вам лучше использовать протокол SPI.
Протокол I2C в Arduino
На следующем рисунке показаны контакты платы Arduino UNO, которые используются для связи по протоколу I2C.
Линия протокола I2C | Контакт платы Arduino UNO |
SDA | A4 |
SCL | A5 |
Для осуществления связи по протоколу I2C в плате Arduino используется библиотека <Wire.h>. В ней содержатся следующие функции для связи по протоколу I2C.
1. Wire.begin(address).
Эта команда производит инициализацию библиотеки Wire и осуществляет подключение к шине I2C в качестве ведущего (master) или ведомого (slave). 7-битный адрес ведомого в данной команде является опциональным и если он не указан [Wire.begin()], то устройство (плата Arduino) подключается к шине I2C в качестве ведущего (master).
2. Wire.read().
Эта функция используется для считывания байта, принятого от ведущего или ведомого.
3. Wire.write().
Эта функция используется для записи данных в устройство, являющееся ведомым или ведущим.
От ведомого ведущему (Slave to Master): ведомый записывает (передает) данные ведущему когда в ведущем работает функция Wire.RequestFrom().
От ведущему ведомому (Master to Slave): в этом случае функция Wire.write() должна использоваться между вызовами функций Wire.beginTransmission() и Wire.endTransmission().
Функцию Wire.write() можно использовать в следующих вариантах:
- Wire.write(value); value - значение передаваемого одиночного байта;
- Wire.write(string) – для передачи последовательности байт;
- Wire.write(data, length); data – массив данных для передачи в виде байт, length – число байт для передачи.
4. Wire.beginTransmission(address).
Эта функция используется для начали передачи по протоколу I2C устройству с заданным адресом ведомого (slave address). После этого вызывается функция Wire.write() с заданной последовательностью байт для передачи, а после нее функция endTransmission() для завершения процесса передачи.
5. Wire.endTransmission().
Эта функция используется для завершения процесса передачи ведомому устройству, который до этого был инициирован функциями beginTransmission() и Wire.write().
6. Wire.onRequest().
Эта функция вызывается когда ведущий запрашивает данные с помощью функции Wire.requestFrom() от ведомого устройства. В этом случае мы можем использовать функцию Wire.write() для передачи данных ведущему.
7. Wire.onReceive().
Эта функция вызывается когда ведомое устройство получает данные от ведущего. В этом случае мы можем использовать функцию Wire.read() для считывания данных передаваемых ведущим.
8. Wire.requestFrom(address,quantity).
Эта функция используется в ведущем устройстве чтобы запросить байты (данные) с ведомого устройства. После этого используется функция Wire.read() чтобы принять данные переданные ведомым устройством.
address: 7-битный адрес устройства, с которого запрашиваются байты (данные).
quantity: число запрашиваемых байт.
Необходимые компоненты
- Плата Arduino Uno – 2 шт. (купить на AliExpress).
- ЖК дисплей 16х2 – 2 шт. (купить на AliExpress).
- Потенциометр 10 кОм – 4 шт. (купить на AliExpress).
- Макетная плата.
- Соединительные провода.
Работа схемы
Схема проекта по применению интерфейса I2C в плате Arduino представлена на следующем рисунке.
Для демонстрации возможностей использования связи по протоколу I2C мы использовали две платы Arduino Uno с подключенными к ним ЖК дисплеями и потенциометрами. С помощью потенциометров будут определяться значения, передаваемые между платами в направлениях ведущий-ведомый и ведомый-ведущий.
Мы будем считывать аналоговое значение напряжения, подаваемое на контакт A0 платы Arduino с помощью потенциометра и преобразовывать его в цифровое значение в диапазоне от 0 до 1023 (с помощью АЦП на этом контакте). В дальнейшем эти значения с выхода АЦП (аналогово-цифрового преобразователя) будут преобразовываться в диапазон 0-127 поскольку мы можем передавать только 7-битные данные при помощи протокола I2C. Интерфейс I2C мы будем использовать на выделенных для него в плате Arduino контактах A4 и A5.
Значения на ЖК дисплее, подключенном к ведомой плате Arduino, будут изменяться в зависимости от положения потенциометра на ведущей стороне и наоборот.
Объяснение программ для Arduino
Нам будут необходимы две программы – одна для ведущей платы Arduino, а другая – для ведомой. Полные тексты обоих программ приведены в конце статьи, здесь же мы рассмотрим их основные фрагменты.
Объяснение программы для ведущей (Master) платы Arduino
1. Первым делом в программе мы должны подключить библиотеку Wire для задействования возможностей протокола I2C и библиотеку для работы с ЖК дисплеем. Также нам необходимо сообщить плате Arduino к каким ее контактам подключен ЖК дисплей.
1 2 3 |
#include<Wire.h> #include<LiquidCrystal.h> LiquidCrystal lcd(2, 7, 8, 9, 10, 11); |
- Далее в функции void setup():
- мы инициализируем последовательную связь со скоростью 9600 бод/с;
1 |
Serial.begin(9600); |
- инициализируем связь по протоколу I2C на контактах A4 и A5;
1 |
Wire.begin(); //Begins I2C communication at pin (A4,A5) |
- далее мы инициализируем ЖК дисплей для работы в режим 16х2, показываем на нем приветственное сообщение и очищаем его экран через 5 секунд.
1 2 3 4 5 6 7 |
lcd.begin(16,2); //Initilize LCD display lcd.setCursor(0,0); //Sets Cursor at first line of Display lcd.print("Circuit Digest"); //Prints CIRCUIT DIGEST in LCD lcd.setCursor(0,1); //Sets Cursor at second line of Display lcd.print("I2C 2 ARDUINO"); //Prints I2C ARDUINO in LCD delay(5000); //Delay for 5 seconds lcd.clear(); //Clears LCD display |
- В функции void loop():
- сначала нам необходимо получить данные от ведомого, поэтому мы используем функцию requestFrom() с адресом ведомого равным 8 и запрашиваемым 1 байтом;
1 |
Wire.requestFrom(8,1); |
Принятое значение считываем с помощью функции Wire.read().
1 |
byte MasterReceive = Wire.read(); |
- далее нам необходимо считать аналоговое значение с потенциометра, подключенного к контакту A0 ведущей платы Arduino;
1 |
int potvalue = analogRead(A0); |
Затем мы конвертируем это полученное значение к диапазону одного байта – от 0 до 127 (байт у нас 7-битный).
1 |
byte MasterSend = map(potvalue,0,1023,0,127); |
- после этого нам необходимо передать это конвертированное значение, поэтому мы начинаем передачу ведомой плате с адресом 8;
1 2 3 |
Wire.beginTransmission(8); Wire.write(MasterSend); Wire.endTransmission(); |
- затем мы отобразим на экране ЖК дисплея принятое значение от ведомой платы с задержкой 500 микросекунд и в дальнейшем мы будем непрерывно принимать и отображать эти значения.
1 2 3 4 5 6 7 8 9 |
lcd.setCursor(0,0); //Sets Currsor at line one of LCD lcd.print(">> Master <<"); //Prints >> Master << at LCD lcd.setCursor(0,1); //Sets Cursor at line two of LCD lcd.print("SlaveVal:"); //Prints SlaveVal: in LCD lcd.print(MasterReceive); //Prints MasterReceive in LCD received from Slave Serial.println("Master Received From Slave"); //Prints in Serial Monitor Serial.println(MasterReceive); delay(500); lcd.clear(); |
Объяснение программы для ведомой (Slave) платы Arduino
1. Как и в ведущей плате, первым делом в программе мы должны подключить библиотеку Wire для задействования возможностей протокола I2C и библиотеку для работы с ЖК дисплеем. Также нам необходимо сообщить плате Arduino к каким ее контактам подключен ЖК дисплей.
1 2 3 |
#include<Wire.h> #include<LiquidCrystal.h> LiquidCrystal lcd(2, 7, 8, 9, 10, 11); |
- В функции void setup():
- мы инициализируем последовательную связь со скоростью 9600 бод/с;
1 |
Serial.begin(9600); |
- далее мы инициализируем связь по протоколу I2C на контактах A4 и A5. В качестве адреса ведомого мы будем использовать значение 8 – очень важно здесь указать адрес ведомого;
1 |
Wire.begin(8); |
После этого мы должны вызвать функцию в которой ведомый принимает значение от ведущего и функцию в которой ведущий запрашивает значение от ведомого.
1 2 |
Wire.onReceive(receiveEvent); Wire.onRequest(requestEvent); |
- затем мы инициализируем ЖК дисплей для работы в режиме 16х2, отображаем на нем приветственное сообщение и очищаем его экран через 5 секунд.
1 2 3 4 5 6 7 |
lcd.begin(16,2); //Initilize LCD display lcd.setCursor(0,0); //Sets Cursor at first line of Display lcd.print("Circuit Digest"); //Prints CIRCUIT DIGEST in LCD lcd.setCursor(0,1); //Sets Cursor at second line of Display lcd.print("I2C 2 ARDUINO"); //Prints I2C ARDUINO in LCD delay(5000); //Delay for 5 seconds lcd.clear(); //Clears LCD display |
3. Затем нам будут необходимы две функции: одна для события запроса (request event) и одна для события приема (receive event).
Для события запроса:
Эта функция будет выполняться когда ведущий будет запрашивать значение от ведомого. Эта функция будет считывать значение с потенциометра, подключенного к ведомой плате Arduino, преобразовывать его в диапазон 0-127 и затем передавать его ведущей плате.
1 2 3 4 5 6 |
void requestEvent() { int potvalue = analogRead(A0); byte SlaveSend = map(potvalue,0,1023,0,127); Wire.write(SlaveSend); } |
Для события приема:
Эта функция будет выполняться когда ведущий будет передавать данные ведомому с адресом 8. Эта функция считывает принятые значения от ведущего и сохраняет ее в переменной типа byte.
1 2 3 4 |
void receiveEvent (int howMany) { SlaveReceived = Wire.read(); } |
4. В функции Void loop():
Мы будем непрерывно отображать принятые от ведущей платы значения на экране ЖК дисплея.
1 2 3 4 5 6 7 8 9 10 11 12 |
void loop(void) { lcd.setCursor(0,0); //Sets Currsor at line one of LCD lcd.print(">> Slave <<"); //Prints >> Slave << at LCD lcd.setCursor(0,1); //Sets Cursor at line two of LCD lcd.print("MasterVal:"); //Prints MasterVal: in LCD lcd.print(SlaveReceived); //Prints SlaveReceived value in LCD received from Master Serial.println("Slave Received From Master:"); //Prints in Serial Monitor Serial.println(SlaveReceived); delay(500); lcd.clear(); } |
После того как вы соберете всю схему проекта и загрузите обе программы в платы Arduino вы можете приступать к тестированию работы проекта. Вращая потенциометр на одной стороне вы должны увидеть изменяющиеся значения на экране ЖК дисплея на другой стороне.
Теперь, когда вы разобрались, как работать с интерфейсом I2C в плате Arduino, вы можете использовать описанные в данной статье приемы для подключения к плате Arduino любых датчиков, работающих по данному протоколу.
Исходные коды программ (скетчей)
Код программы для ведущей (Master) платы Arduino
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
//I2C MASTER CODE //I2C Communication between Two Arduino //Circuit Digest //Pramoth.T #include<Wire.h> //библиотека для связи по протоколу I2C #include<LiquidCrystal.h> //библиотека для работы с ЖК дисплеем LiquidCrystal lcd(2, 7, 8, 9, 10, 11); //контакты, к которым подключен ЖК дисплей (RS,EN,D4,D5,D6,D7) void setup() { lcd.begin(16,2); //инициализируем ЖК дисплей lcd.setCursor(0,0); //устанавливаем курсор на 1-ю строку lcd.print("Circuit Digest"); //выводим на экран CIRCUIT DIGEST lcd.setCursor(0,1); //переводим курсор на 2-ю строку дисплея lcd.print("I2C 2 ARDUINO"); //выводим на экран I2C ARDUINO delay(5000); //задержка 5 секунд lcd.clear(); //очищаем экран Serial.begin(9600); //инициализация последовательной связи со скоростью 9600 бод/с Wire.begin(); //инициализация связи по протоколу I2C на контактах (A4,A5) } void loop() { Wire.requestFrom(8,1); // запрашиваем 1 байт с ведомой arduino с адресом 8 byte MasterReceive = Wire.read(); // принимаем байт от ведомой платы и сохраняем его в переменной MasterReceive int potvalue = analogRead(A0); // считываем аналоговое значение с потенциометра (0-5V) byte MasterSend = map(potvalue,0,1023,0,127); //конвертируем цифровое значение из диапазона 0-1023 в диапазон 0-127 Wire.beginTransmission(8); // начинаем передачу ведомой плате arduino с адресом 8 Wire.write(MasterSend); // передаем ей один байт (конвертированное значение с выхода потенциометра) Wire.endTransmission(); // останавливаем передачу lcd.setCursor(0,0); //переводим курсор на 1-ю строку дисплея lcd.print(">> Master <<"); //выводим >> Master << на экран lcd.setCursor(0,1); // переводим курсор на 2-ю строку дисплея lcd.print("SlaveVal:"); //выводим SlaveVal: на экран lcd.print(MasterReceive); //выводим значение MasterReceive (принятое значение от ведомой платы) на экран ЖК дисплея Serial.println("Master Received From Slave"); //передаем по последовательному порту (для целей отладки) Serial.println(MasterReceive); delay(500); lcd.clear(); } |
Код программы для ведомой (Slave) платы Arduino
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
//I2C SLAVE CODE //I2C Communication between Two Arduino //CircuitDigest //Pramoth.T #include<Wire.h> // библиотека для связи по протоколу I2C #include<LiquidCrystal.h> //библиотека для работы с ЖК дисплеем LiquidCrystal lcd(2, 7, 8, 9, 10, 11); //контакты, к которым подключен ЖК дисплей - (RS,EN,D4,D5,D6,D7) byte SlaveReceived = 0; void setup() { lcd.begin(16,2); //инициализируем ЖК дисплей lcd.setCursor(0,0); //переводим курсор на 1-ю строку дисплея lcd.print("Circuit Digest"); //выводим CIRCUIT DIGEST на экран lcd.setCursor(0,1); // переводим курсор на 2-ю строку дисплея lcd.print("I2C 2 ARDUINO"); //выводим I2C ARDUINO на экран delay(5000); //задержка 5 секунд lcd.clear(); //очищаем экран ЖК дисплея Serial.begin(9600); // инициализация последовательной связи со скоростью 9600 бод/с Wire.begin(8); //инициализация связи по протоколу I2C с адресом ведомого 8 на контактах (A4,A5) Wire.onReceive(receiveEvent); //вызов функции когда ведомый принимает значение от ведущего Wire.onRequest(requestEvent); //вызов функции когда ведущий запрашивает значение от ведомого } void loop(void) { lcd.setCursor(0,0); //переводим курсор на 1-ю строку дисплея lcd.print(">> Slave <<"); //выводим >> Slave << на экран lcd.setCursor(0,1); // переводим курсор на 2-ю строку дисплея lcd.print("MasterVal:"); //выводим MasterVal: на экран lcd.print(SlaveReceived); //выводим значение SlaveReceived (принятое от ведущего) на экран ЖК дисплея Serial.println("Slave Received From Master:"); //передаем по последовательному порту Serial.println(SlaveReceived); delay(500); lcd.clear(); } void receiveEvent (int howMany) //эта функция вызывается когда ведомый принимает значение от ведущего { SlaveReceived = Wire.read(); //принимаем значение от ведущего и сохраняем его в переменной SlaveReceived } void requestEvent() //эта функция вызывается когда ведущий запрашивает значение от ведомого { int potvalue = analogRead(A0); // считываем аналоговое значение с выхода потенциометра (0-5V) byte SlaveSend = map(potvalue,0,1023,0,127); // конвертируем цифровое значение из диапазона 0-1023 в диапазон 0-127 Wire.write(SlaveSend); // передаем один байт (конвертированное значение с выхода потенциометра) ведущему } |