Часто по разным причинам не хватает одного контроллера в проекте, особенно в процессе прототипирования. Допустим, подключили вы на одну плату дисплей и тачпад, и он занял почти все выводы, аппаратную шину SPI, а какой-нибудь датчик отнимает всё время для вычислений. Нужно временно расшириться, ставим рядом ещё одну плату, но возникает логический вопрос – как передавать данные между ними?

Есть несколько способов, самые распространённые – серийный порт и шина i2c. Мне больше нравится второй вариант, даже если вы используете ещё другие устройства i2c, шина может содержать в себе их до 127 единиц. Вкратце, интерфейс использует два провода для передачи информации – тактовый сигнал и сигнал данных. Соединить платы проще простого – нужно соединить контакты A4 и A5

В отличии от 1-wire, это упрощает “беседу” между двумя устройствами, можно легко брать паузу, в моменты получения/передачи данных. Также у нас есть Master устройство и Slave, отличие в том, что тактирует именно Master. Попробуем передать какое-нибудь число, первым делом для этого подключим библиотеку
#include <Wire.h> // библиотека I2C
Затем инициализируем её в setup()
Wire.begin();
Передать значение – проще простого, нужно начать передачу для определённого адреса slave, записать байт информации, ну и следовательно закончить её
Wire.beginTransmission(9); // Цифра 9 - адрес ведомой платы Wire.write(x); // Передает значение х Wire.endTransmission();
Соберём код для Master платы, где будем инкрементировать переменную x и отправлять её раз в секунду.
// Код для Основной платы #include <Wire.h> // библиотека I2C int x; void setup() { Wire.begin(); Serial.begin(115200); } void loop() { x++; Wire.beginTransmission(9); // Цифра 9 - адрес ведомой платы Wire.write(x); // Передает значение х Wire.endTransmission(); Serial.println(x); delay(1000); }
Теперь рассмотрим код для платы slave. Здесь есть парочка отличий. В момент инициализации мы задаём адрес slave устройства.
Wire.begin(9); // 9 здесь адрес Slave
Создаём событие, которое будет обрабатываться при поступлении данных
Wire.onReceive(receiveEvent);
Теперь мы можем задать функцию, в которой будем считывать данные, когда они собственно поступают по шине. Для удобства выведем их в терминал
void receiveEvent(int bytes) { x = Wire.read(); // Получаем значения х от основной платы Serial.println(x); }
Теперь нам не составит труда собрать код для slave-устройства. Обратите внимание, функция receiveEvent срабатывает без вызова её в бесконечном цикле loop().
// Код для ведомой платы #include <Wire.h> int x; void setup() { Wire.begin(9); // 9 здесь адрес Slave (упоминается также в коде основной платы) Wire.onReceive(receiveEvent); Serial.begin(115200); } void receiveEvent(int bytes) { x = Wire.read(); // Получаем значения х от основной платы Serial.println(x); } void loop() { }
Запустим терминал с обеих сторон, и посмотрим на данные. Всё передаётся!

Но после 255 радость не ощущается так сильно, ведь значение сбрасывается в ноль. Происходит так, потому-что Wire.write(x); и x = Wire.read(); работают с одним байтом информации, поэтому больше 255 не передать.

Как же нам передать float и int? Если помните, когда мы работали с EEPROM для arduino, разделяли с помощью указателей переменную на байты, так вот этот способ отлично подойдёт и здесь. Изменим тип переменной на int, и зададим её значение в setup()
int x; void setup() { x = 3484;
Теперь после передачи данных нам нужно сделать цикл for, где мы с помощью указателей разобьём наше число на 4 байта. Не забываем объявить локальную переменную byte
Wire.beginTransmission(9); // Цифра 9 - адрес ведомой платы byte raw[4]; (int&)raw = x;
Потом создаём цикл for на 4 итерации, где по байтам отправляем наши 4 байта, на которые мы разложили наше число int. Отправляем попутно каждый байт в серийный порт.
for (byte i = 0; i < 4; i++) { Wire.write(raw[i]); Serial.println(raw[i]); }
Завершаем передачу данных, и отправляем итоговое значение со строкой val=, выжидаем паузу 1 секунду
Wire.endTransmission(); Serial.print("Val ="); Serial.println(x); delay(1000);
Теперь перейдём к нашему slave-устройству. Также локально объявляем переменную byte,
byte raw[4]; for (byte i = 0; i < 4; i++) { raw[i] = Wire.read(); Serial.println(raw[i]); }
Теперь собираем наше число обратно в int из принятых байтов byte. И также выводим в строку значение.
int &x = (int&)raw; Serial.print("Val ="); Serial.println(x); }
Мы выводили в строку значение каждого байта и итоговое значение, посмотрим результат вывода в серийный порт обеих плат.

Как видите у нас получилось передать значение int, и сохранить его в переменную второй платы. Тоже самое можно сделать и с float, переписав int на float в коде. Проверяем

Также я нашёл удобную библиотеку для передачи структур таких данных, это будет стабильно работать в ваших проектах. Давайте посмотрим. Вам нужно создать файл I2C_Anything.h в директории вашего проекта
// Written by Nick Gammon // May 2012 #include <Arduino.h> #include <Wire.h> template <typename T> int I2C_writeAnything (const T& value) { const byte * p = (const byte*) &value; unsigned int i; for (i = 0; i < sizeof value; i++) Wire.write(*p++); return i; } // end of I2C_writeAnything template <typename T> int I2C_readAnything(T& value) { byte * p = (byte*) &value; unsigned int i; for (i = 0; i < sizeof value; i++) *p++ = Wire.read(); return i; } // end of I2C_readAnything
Подключаем наш файл в проект, задаём переменную адреса устройства slave, и переменные, которые хотим передать. У меня это три параметра температуры и три параметра давления, в формате float.
#include "I2C_Anything.h" const byte SLAVE_ADDRESS = 42; float ftemp1 = 34.6; float ftemp2 = 54.2; float ftemp3 = 45.7; float fpreasure1 = 0.15; float fpreasure2 = 0.23; float fpreasure3 = 0.11;
Теперь в проекте, вот таким простым кодом отправляем данные
Wire.beginTransmission (SLAVE_ADDRESS); I2C_writeAnything (ftemp1); I2C_writeAnything (ftemp2); I2C_writeAnything (ftemp3); I2C_writeAnything (fpreasure1); I2C_writeAnything (fpreasure2); I2C_writeAnything (fpreasure3); Wire.endTransmission ();
На стороне slave устройства, тоже ничего сложного нет, идентично подключаем наш файл, и задаём адрес
#include "I2C_Anything.h" const byte MY_ADDRESS = 42;
Инициализируем интерфейс i2c, и задаём переменные, но со значением volatile, это означает, что значение может в любой момент поменяться извне, это даёт понять компилятору, что её не нужно оптимизировать, что поможет избежать в дальнейшем проблем.
Wire.begin (MY_ADDRESS); Serial.begin (9600); Wire.onReceive (receiveEvent); volatile boolean haveData = false; volatile float ftemp1; volatile float ftemp2; volatile float ftemp3; volatile float fpreasure1; volatile float fpreasure2; volatile float fpreasure3;
В обработчике данных recieveEvent, мы проверяем количество принятых переменных, где перечисляем их в условие howMany, и считываем эти переменные.
void receiveEvent (int howMany) { if (howMany >= (sizeof ftemp1) + (sizeof ftemp2) + (sizeof ftemp3) + (sizeof fpreasure1) + (sizeof fpreasure2) + (sizeof fpreasure3)) { I2C_readAnything (ftemp1); I2C_readAnything (ftemp2); I2C_readAnything (ftemp3); I2C_readAnything (fpreasure1); I2C_readAnything (fpreasure2); I2C_readAnything (fpreasure3); haveData = true; } // end if have enough data } // end of receiveEvent
Теперь в бесконечном цикле loop, чтобы не выводить не принятые данные, делаем проверку переменной haveData (которую мы устанавливаем в значение = Истина в обработчике), и выводим их в серийный порт
if (haveData) { Serial.print ("Temp1 float = "); Serial.println (ftemp1); Serial.print ("Temp2 float = "); Serial.println (ftemp2); Serial.print ("Temp3 float = "); Serial.println (ftemp3); Serial.print ("Preasure1 float = "); Serial.println (fpreasure1); Serial.print ("Preasure2 float = "); Serial.println (fpreasure2); Serial.print ("Preasure3 float = "); Serial.println (fpreasure3); haveData = false; }
Вот таким образом можно передавать несколько разных значений с одной платы на другую.

Используйте в своих проектах эти удобные и простые способы взаимодействия плат. Добавлю под конец ещё пару отступлений – я стараюсь не работать с float в среде Arduino, если передаёте температуру, будет лучше умножить её на 10 или 100 на одной стороне, а затем поделить на другой, ну и подобрать тип переменной под ваше значение, чтобы не нагружать лишними вычисления обе платы.
Тут все хорошо, но как применить тоже код, но чтобы мастер принимал данные, а не отсылал… А ведомый отсылал данные..?
Спасибо за статью. Объясните пожалуйста почему двухбайтную переменную int Вы разбиваете на 4 байта. И в сериал выводятся 4 байта, которые после сборки вручную дают огромное число. Например, 3484 разбито на 156, 13, 5 и 82. Если сложить первое число со вторым, умноженный на 256, получается наши исходные 3484. Откуда ещё два более старших байта и что они обозначают?
Спасибо.
Как я понял, автор ссылается на свой прошлый урок, где он записывал большие числа в большую память )). Лично я в своём проекте разделяю эти операции, то есть одна ардуина считывает данные со своей внешней памяти (на микрочипе) и часть этих данных передаёт другой ардуине для вращения серводвигателей.