Использование I2C
Мы переходим к тематике передачи данных между NanoPi и Arduino. Одним из наиболее популярных интерфейсов для «общения» двух устройств является интерфейс I2C, разработанный инженерами компании Philips более 30 лет назад. При использовании I2C данные передаются по двум линиям –-- SDA (линия данных) и SCL (линия тактов). Всего на одной двупроводной шине может быть до 127 устройств. При этом один из них всегда является ведущим (master), а остальные ведомыми (slave).

Рисунок 1 – Подключение с использованием I2C
При этом обе линии всегда подтягиваются к напряжению питания (номинальному напряжению ведущего устройства (мастера) с помощью подтягивающих резисторов (они должны ставиться всегда, однако если используются МК с функцией подтяжки пинов к высокому уровню, можно соединять МК напрямую). Все устройства должны быть объединены общей землей. По умолчанию на обеих линиях всегда сохраняется логическая 1, а передача сигналов осуществляется подтягиванием линии к логическому 0. Вся передача данных состоит из «стартовой посылки», «битов» и «стоповой посылки». Порядок изменения уровня на шинах задает тип посылки. Особенности работы интерфейса таковы, что все процессы передачи сигналов от ведущего к ведомым и наоборот —всегда определяются ведущим устройством:

Рисунок 2 – Мастер передает байт
Начало передачи регламентируется стартовым битом. Причем стартовый бит «активируется», когда SDA переключается в 0 при высоком SCL (в 1). И, наоборот, признаком стопового бита является случай, когда SDA переключается в «1», при единичном SCL.

Рисунок 3 – Стартовый и стоповый биты
После старта передача одного бита данных идет по тактовому импульсу. То есть, когда линия SCL в нуле, master или slave выставляют бит на SDA (подтягивают — если «0» или не подтягивают — если «1» линию SDA), после чего SCL отпускается и master/slave считывают бит. Таким образом, протокол совершенно не зависит от временных интервалов, только от тактовых битов. То есть изменение на шине данных SDA в момент приема данных может быть только при низком уровне на SCL, когда SCL вверху — то идет чтение. Если же SDA меняется при высоком SCL, то это служебные команды START или STOP, как и говорилось раньше. После передачи каждого байта должен передаваться сигнал подтверждения — он позволяет:
- В случае передачи байта от master к slave, slave подтягивает линию
SDAв 0, благодаря чему master понимает, что slave успешно получил и записал байт (иначе у master будет информация о том, что передачу байта нужно повторить):

Рисунок 4 – Мастер принимает байт
- В случае передачи байта от slave к master, master подтягивает линию
SDAв 0, благодаря чему slave понимает, что master успешно получил и записал байт и что slave может продолжать передачу. Если же master не подтянетSDAв 0 (Неуспех), то это значит, что master не принял сообщение.

Рисунок 5 – Бит подтверждения при передаче от master к slave
За один раз может передаваться более 1 байта (то есть после передачи каждого байта должен следовать сигнал подтверждения). Однако в сообщении от START до STOP битов может быть несколько байт:

Рисунок 6 – Бит подтверждения при передаче от slave к master
Теперь разберемся с логическим уровнем. Как передаются отдельные биты понятно, а далее обсудим, что эти биты значат. Данные шлются пакетами, каждый пакет состоит из девяти бит. 8 бит данных и 1 бит подтверждения/не подтверждения приема. Первый пакет шлется от ведущего к ведомому (а на самом деле ко всем ведомым) –-- это физический адрес устройства и бит направления:

Рисунок 7 – Передача двух байт от master к slave
Сам адрес состоит из семи бит (именно поэтому при I2C возможно размещение до 127 устройств на шине), а восьмой бит означает, что будет делать Slave на следующем байте — принимать R или передавать W данные. Девятым битом идет бит подтверждения ACK. Если Slave услышал свой адрес и считал полностью, то на девятом такте он придавит (в 0) линию SDA, сгенерировав ACK («Понял!»). Мастер, заметя это, «понимает», что все идет по плану и можно продолжать. Если Slave не обнаружился, пропустил адрес, неправильно принял байт, сгорел или еще что с ним случилось, то, соответственно, SDA на девятом такте будет прижать некому и ACK не получится (будет так называемый NACK).

Рисунок 8 – Самый первый байт –-- это запрос от master к slave
После адресного пакета идут пакеты с данными в ту или другую сторону, в зависимости от бита RW в заголовочном пакете. Вот, например, запись, то есть передача сообщения от master к slave. В квадратиках идут номера битов. W=0.

Рисунок 9 – Передача сообщения от master к конкретному slave.
А вот, например, чтение, то есть передача сообщения от slave к master. В квадратиках идут номера битов. R=1. Что в этом случае важно: при приеме последнего байта надо дать ведомому устройству slave понять, что его услуги больше не нужны, и отослать NACK на последнем байте.

Рисунок 10 – Передача сообщения от конкретного slave к master.
Так как при передаче сигналов идет подтяжка линий к 0 (и возвращение их в 1, когда ни одно устройство не подтягивает линию к 0), напрашивается вывод о скоростных ограничениях передачи информации по такому интерфейсу. Из-за довольно длинных переходных процессов (когда 1 переходит в 0 и наоборот), обусловленных паразитными емкостями, максимальная скорость передачи данных такого интерфейса составляет 100Кб/с. Также стоит отметить, что случай, когда номинальное напряжение питания ведущего устройства меньше, чем номинальные напряжения ведомых, — допустим. Так как напряжение логической единицы в этом случае задается именно ведущим устройством. Однако обратное неверно: если ведущим устройством сделать устройство, пины которого (и, в частности, те пины, которые подключаются к линиям SDA и SCL) выдают, например, 5В, а среди его ведомых будет устройство, рассчитанное максимально на 3.3В, то напряжение 5В может просто сжечь пины ведомого устройства и привести его в негодность.
Теперь рассмотрим конкретные примеры взаимодействия и «общения» по интерфейсу I2C двух устройств --- микрокомпьютера NanoPi и ArduinoUNO.
Прежде всего при использовании NanoPi необходимо «активировать» интерфейс I2C, то есть сделать GPIO, которые отвечают за него, доступными. Для этого необходимо в терминале прописать команду «sudo npi-config»:

Далее выбрать соответствующий пункт:

Далее активировать интерфейс I2C:

Выбрать необходимый i2c интерфейс:

Выбрать кнопку Enable:

Выйти из меню конфигураций. Проверить успешность активации интерфейса можно, введя ls /dev/*i2c*:

Распиновка NanoPi выглядит следующим образом:

Распиновка ArduinoUNO выглядит следующим образом:

Опасность: Подключение осуществляется только при выключенных платах, иначе есть возможность повредить их.
Рассмотрим простейший пример: соединим NanoPi и ArduinoUNO как на рисунке ниже:

Напишем программу, благодаря которым диод (подключенный к 13 пину) на плате ArduinoUNO будет мигать. Для того чтобы использовать библиеотеку smbus ее необходимо сначала установить. Для этого пишем в терминал:
sudo apt install pip
pip install smbusПрограмма на NanoPi будет выполняться в терминале, и выглядит она следующим образом:
from smbus import SMBus #импорт необходимой для работы с I2C библиотеки, обычно уже является частью ядра Raspbian
addr = 0x8 # адрес ведомого устройства
bus = SMBus(0) # адрес самого I2C: /dev/ic2-0
numb = 1 #переменная флаг, зануляется при вводе любых символов, кроме 0 и 1
print ("Enter 1 for ON or 0 for OFF") #вывод для пользователя
while numb == 1: #цикл до тех пор, пока вводятся 0 и 1
ledstate = input(">>>> ") #чтение данных с терминала, в котором запущена программа
if ledstate == "1": #если введена единица
bus.write_byte(addr, 0x1) #включение диода
elif ledstate == "0": #если введен ноль
bus.write_byte(addr, 0x0) #выключение диода
else: #исли не введена 1 или 0
numb = 0 #зануление флага, закрытие программы, которая была запушена в терминалеПрограмма на ArduinoUNO выглядит следующим образом:
// Подключение библиотеки Wire для I2C
#include <Wire.h>
// диод на 13 пине
const int ledPin = 13;
void setup() {
// Активация Ардуино как ведомого устройства с адресом 8
Wire.begin(0x8);
// Вызов функции receiveEvent при приеме данных от мастера
Wire.onReceive(receiveEvent);
// Выставление 13 пина как выхода и гашение диода
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
}
// Функция, которая исполняется каждый раз, когда мастер передает
// ведомому устройству запрос на прием ведомым устройством сообщения
void receiveEvent(int howMany) {
while (Wire.available()) { // пока ввсе сообщение не считано побайтно
char c = Wire.read(); // принимаем байт как символ
digitalWrite(ledPin, c); // передаем этот байт на пин диода, то есть зажигаем диод
}
}
void loop() {
delay(100); //в loop ничего не исполняется, а диод мигает "по прерыванию" от I2C
}Подсказка: Если у вас выпадает ошибка при отправке команды в командной консоли, то можете воспользоваться командой
i2cdetect -y 0. Благодаря ей вы просканируете все доступные адреса.
