I2C или IIC (Inter-Integrated Circuit), читается «Ай-ту-Си» или «и-два-цэ» по-нашенски — последовательная шина разработана фирмой Philips Semiconductors ещё в 80-х годах прошлого века. Задумывалась, как простая 8-битная шина внутренней связи для создания управляющей электроники. Так как право на его использование стоит денег, все пользуют в свое удовольствие, называя только по другому. В Atmel его зовут TWI, но от этого ничего не меняется.
Данные передаются по двум проводам — провод данных и провод тактов. В сети есть хотя бы одно ведущее устройство (Master), которое инициализирует передачу данных и генерирует сигналы синхронизации. В сети также есть ведомые устройства (Slave), такими как гироскопы, акселерометры, датчики давления, EEPROM-память, дисплеи, которые передают данные по запросу ведущего. У каждого ведомого устройства есть уникальный адрес, по которому ведущий и обращается к нему. Адрес устройства указывается в паспорте (datasheet). К одной шине I2C может быть подключено до 127 устройств, в том числе несколько ведущих. К шине можно подключать устройства в процессе работы, т.е. она поддерживает «горячее подключение».
Pi4J предоставляет возможность работы с последовательной шиной I2C/TWI из Java на Raspberry Pi, Banana Pi, Orange Pi, Nano Pi и Odroid. Все классы и интерфейсы для инициализации и работы с шиной находятся в пакете com.pi4j.io.i2c.*;
.
Ниже представлены описания интерфейсов/классов и их методов.
Интерфейс I2CBus
Это абстракция шины I2C. Этот интерфейс позволяет шине возвращать устройство I2C.
getDevice(int)
Возвращает I2C устройство.
I2CDevice getDevice(int address) throws IOException;
Параметры
address
— адрес устройства I2C
Возвращает
I2C устройство
Бросает
IOException
— в случае, если эта шина не может вернуть I2C устройство.
getBusNumber()
int getBusNumber()
Возвращает
номер шины
close()
Закрывает шину I2C. Обычно это означает закрытие основного файла.
void close() throws IOException;
Бросает
IOException
— в случае возникновения проблем с закрытием шины I2C.
Интерфейс I2CDevice
Это абстракция устройства I2C. Он позволяет считывать или записывать данные на устройство.
getAddress()
Возвращает адрес, для которого создан этот экземпляр.
int getAddress();
write(byte)
Этот метод записывает один байт непосредственно на устройство I2C.
void write(byte b) throws IOException;
Параметры
b
— байт, который будет записан
Бросает
IOException
— в случае, если байт не может быть записан на устройство/шину I2C.
write(byte[], int, int)
Этот метод записывает несколько байтов непосредственно на устройство I2C из заданного буфера при заданном смещении.
void write(byte[] buffer, int offset, int size) throws IOException;
Параметры
buffer
— буфер данных, который должен быть записан на устройство I2C за один раз
offset
— смещение в буфере
size
— количество записываемых байтов
Бросает
IOException
— в случае, если байт не может быть записан на устройство/шину I2C.
write(byte[])
Этот метод записывает все байты, включенные в данный буфер, непосредственно на устройство I2C.
void write(byte[] buffer) throws IOException;
Параметры
buffer
— буфер данных, который должен быть записан на устройство I2C за один раз
Бросает
IOException
— в случае, если байт не может быть записан на устройство/шину I2C.
write(int, byte)
Этот метод записывает один байт непосредственно на устройство I2C.
void write(int address, byte b) throws IOException;
Параметры
b
— байт, который будет записан
address
— локальный адрес в устройстве I2C
Бросает
IOException
— в случае, если байт не может быть записан на устройство/шину I2C.
write(int, byte[], int, int)
Этот метод записывает несколько байтов непосредственно на устройство I2C из заданного буфера при заданном смещении.
void write(int address, byte[] buffer, int offset, int size) throws IOException;
Параметры
address
— локальный адрес в устройстве I2C
buffer
— буфер данных, который должен быть записан на устройство I2C за один раз
offset
— смещение в буфере
size
— количество записываемых байтов
Бросает
IOException
— в случае, если байт не может быть записан на устройство/шину I2C.
write(int, byte[])
Этот метод записывает все байты, включенные в данный буфер, непосредственно на адрес регистра на устройстве I2C
void write(int address, byte[] buffer) throws IOException;
Параметры
address
— локальный адрес в устройстве I2C
buffer
— буфер данных, который должен быть записан на устройство I2C за один раз
Бросает
IOException
— в случае, если байт не может быть записан на устройство/шину I2C.
read()
Этот метод считывает один байт с устройства I2C. Результат от 0 до 255, если операция чтения была успешной, в противном случае отрицательное число ошибки.
int read() throws IOException;
Возвращает
значение байта: положительное число от 0 до 255, если чтение было успешным. Отрицательное число, если чтение не удалось.
Бросает
IOException
— в случае, если байты не могут быть прочитан с устройства/шины I2C.
read(byte[], int, int)
Этот метод считывает байты непосредственно из устройства I2C в заданный буфер при запрошенном смещении.
int read(byte[] buffer, int offset, int size) throws IOException;
Параметры
buffer
— буфер данных, который должен считываться с устройство I2C за один раз
offset
— смещение в буфере
size
— количество байт для чтения
Возвращает
количество прочитанных байтов
Бросает
IOException
— в случае, если байт не может быть прочитан с устройства/шины I2C.
read(int)
Этот метод считывает один байт с устройства I2C.
int read(int address) throws IOException;
Параметры
address
— локальный адрес в устройстве I2C
Возвращает
значение байта: положительное число от 0 до 255, если чтение было успешным. Отрицательное число, если чтение не удалось.
Бросает
IOException
— в случае, если байт не может быть прочитан с устройства/шины I2C.
read(int, byte[], int, int)
Этот метод считывает байты, начиная с заданного адреса в устройстве I2C в буфере при запрошенном смещении.
int read(int address, byte[] buffer, int offset, int size) throws IOException;
Параметры
address
— локальный адрес в устройстве I2C
buffer
— буфер данных, который должен считываться с устройство I2C за один раз
offset
— смещение в буфере
size
— количество байт для чтения
Возвращает
количество прочитанных байтов
Бросает
IOException
— в случае, если байт не может быть прочитан с устройства/шины I2C.
read(byte[], int, int, byte[], int, int)
Этот метод записывает и считывает байты в / из устройства I2C одним вызовом метода
int read(byte[] writeBuffer, int writeOffset, int writeSize, byte[] readBuffer, int readOffset, int readSize) throws IOException;
Параметры
writeBuffer
— буфер данных, который должен быть записан на устройство I2C за один раз
writeOffset
— смещение в буфере
writeSize
— количество записываемых байтов
readBuffer
— буфер данных, который должен считываться с устройство I2C за один раз
readOffset
— смещение в буфере
readSize
— количество байт для чтения
Возвращает
количество прочитанных байтов
Бросает
IOException
— в случае, если байт не может быть прочитан/записан с/на устройство/шину I2C .
ioctl(…)
Запускает ioctl на этом устройстве.
void ioctl(long command, int value) throws IOException; void ioctl(long command, ByteBuffer data, IntBuffer offsets) throws IOException;
Класс I2CConstants
Это константы, взяты непосредственно из ядра linux (i2c-dev.h i2c.h). Они должны использоваться с расширенным I2C ioctl.
Класс I2CFactory
I2C factory — он возвращает экземпляры интерфейса I2CBus
.
getInstance(int)
Создаёт новый экземпляр I2CBus
.
Тайм-аут блокировки шины для эксклюзивной связи устанавливается в DEFAULT_LOCKAQUIRE_TIMEOUT
.
public static I2CBus getInstance(int busNumber) throws UnsupportedBusNumberException, IOException
Параметры
busNumber
— номер шины.
Возвращает
новый экземпляр I2CBus
Бросает
UnsupportedBusNumberException
— если данный номер шины не поддерживается базовой системой.
IOException
— если сообщение с шиной I2C не работает.
getInstance(int, long, TimeUnit)
Создаёт новый экземпляр I2CBus
.
public static I2CBus getInstance(int busNumber, long lockAquireTimeout, TimeUnit lockAquireTimeoutUnit) throws UnsupportedBusNumberException, IOException
Параметры
busNumber
— номер шины.
lockAquireTimeout
— таймаут для блокировки шины.
lockAquireTimeoutUnit
— единицы измерения для lockAquireTimeout
.
Возвращает
новый экземпляр I2CBus
Бросает
UnsupportedBusNumberException
— если данный номер шины не поддерживается базовой системой.
IOException
— если сообщение с шиной I2C не работает.
setFactory(I2CFactoryProvider)
Позволяет изменить поставщика для фабрики.
public static void setFactory(I2CFactoryProvider factoryProvider)
Параметры
factoryProvider
— новый поставщик.
getBusIds()
Выдаёт все доступные номера шины I2C из sysfs.
public static int[] getBusIds() throws IOException
Возвращает
Возвращает найденные номера шин I2C или null
Бросает
IOException
— если извлечение из интерфейса sysfs не удалась.
Интерфейс I2CFactoryProvider
I2C factory provider — он возвращает экземпляры интерфейса I2CBus
.
getBus(int, long, TimeUnit)
Создаёт новый экземпляр I2CBus
.
I2CBus getBus(int busNumber, long lockAquireTimeout, TimeUnit lockAquireTimeoutUnit) throws UnsupportedBusNumberException, IOException;
Параметры
busNumber
— номер шины.
lockAquireTimeout
— таймаут для блокировки шины.
lockAquireTimeoutUnit
— единицы измерения для lockAquireTimeout
.
Возвращает
новый экземпляр I2CBus
Бросает
UnsupportedBusNumberException
— если данный номер шины не поддерживается базовой системой.
IOException
— если сообщение с шиной I2Cне работает.
Подключение DS3231 к Orange Pi PC по I2C
Для проверки работоспособности я взял часы реального времени DS3231, так как они первые попались под руку, ещё с этим чипом очень легко работать.
Описание регистров DS3231
Ниже в таблице представлен перечень регистров часов реального времени:
Адрес | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Функция | Пределы |
---|---|---|---|---|---|---|---|---|---|---|
0x00 | 0 | 10 секунд | Секунды | Секунды | 00-59 | |||||
0x01 | 0 | 10 минут | Минуты | Минуты | 00-59 | |||||
0x02 | 0 | 12/24 | AM/PM | 10 часов | Час | Часы | 1-12 + AM/PM или 00-23 | |||
10 часов | ||||||||||
0x03 | 0 | 0 | 0 | 0 | 0 | День | День недели | 1-7 | ||
0x04 | 0 | 0 | 10 число | Число | Дата | 01-31 | ||||
0x05 | Century | 0 | 0 | 10 месяц | Месяц | Месяцы/век | 01-12 + Век | |||
0x06 | 10 лет | Год | Годы | 00-99 | ||||||
0x07 | A1M1 | 10 секунд | Секунды | Секунды, 1-й будильник | 00-59 | |||||
0x08 | A1M2 | 10 минут | Минуты | Минуты, 1-й будильник | 00-59 | |||||
0x09 | A1M3 | 12/24 | AM/PM | 10 часов | Час | Часы, 1-й будильник | 1-12 + AM/PM или 00-23 | |||
10 часов | ||||||||||
0x0A | A1M4 | DY/DT | 10 число | День | День недели, 1-й будильник | 1-7 | ||||
Число | Дата, 1-й будильник | 01-31 | ||||||||
0x0B | A2M2 | 10 минут | Минуты | Минуты, 2-й будильник | 00-59 | |||||
0x0C | A2M3 | 12/24 | AM/PM | 10 часов | Час | Часы, 2-й будильник | 1-12 + AM/PM или 00-23 | |||
10 часов | ||||||||||
0x0D | A2M4 | DY/DT | 10 число | День | День недели, 2-й будильник | 1-7 | ||||
Число | Дата, 2-й будильник | 01-31 | ||||||||
0x0E | EOSC | BBSQW | CONV | RS2 | RS1 | INTCN | A2IE | A1IE | Регистр настроек (Control) | |
0x0F | OSF | 0 | 0 | 0 | EN32kHz | BSY | A2F | A1F | Регистр статуса (Status) | |
0x10 | SIGN | DATA | DATA | DATA | DATA | DATA | DATA | DATA | Регистр подстройки частоты (Aging Offset) | |
0x11 | SIGN | DATA | DATA | DATA | DATA | DATA | DATA | DATA | Регистр температуры, старший байт | |
0x12 | DATA | DATA | 0 | 0 | 0 | 0 | 0 | 0 | Регистр температуры, младший байт |
Информация о времени хранится в двоично-десятичном формате, то есть каждый разряд десятичного числа (от 0 до 9) представляется группой из 4-х бит. В случае одного байта, младший полубайт отсчитывает единицы, старший десятки и т. д.
Схема подключения
Пример 1 — запись данных
I2C шина является адресной, так что каждое подключаемое устройство имеет свой адрес. Адрес устройства в шестнадцатеричной системе равен 0x68 — это 104 в десятичной. Объявляем константу:
private static final int DS3231_ADDR = 0x68;
Так как данные будут записаны только в регистры даты и времени (от 0x00 до 0x06), объявляем только их:
private static final int DS3231_TIME_SECONDS_ADDR = 0x00; private static final int DS3231_TIME_MINUTES_ADDR = 0x01; private static final int DS3231_TIME_HOURS_ADDR = 0x02; private static final int DS3231_TIME_WEEK_DAY_ADDR = 0x03; private static final int DS3231_TIME_DATE_ADDR = 0x04; private static final int DS3231_TIME_MONTH_CENTURY_ADDR = 0x05; private static final int DS3231_TIME_YEAR_ADDR = 0x06;
Поскольку мы не используем платформу Raspberry Pi, мы должны явно указывать платформу, в моём случае — это Orange Pi.
PlatformManager.setPlatform(Platform.ORANGEPI);
Чтобы работать с I2C устройствами, надо создать экземпляр I2CBus
с помощью метода getInstance
класса I2CFactory
, где параметр — это номер шины.
I2CBus i2c = I2CFactory.getInstance(I2CBus.BUS_0);
Создаём экземпляр I2CDevice
с помощью метода getDevice
, где параметр — это адрес устройства. Таким образом можно создать объект для каждого подключённого устройства.
I2CDevice device = i2c.getDevice(DS3231_ADDR);
Для записи данных на устройство используем методы write
.
device.write(DS3231_TIME_SECONDS_ADDR, decToBcd(now.getSecond()));
Ниже приведён пример программы, которая устанавливает время и дату на часах. Получить время можно с помощью LocalDateTime.now();
.
import java.time.LocalDateTime; import com.pi4j.io.i2c.I2CBus; import com.pi4j.io.i2c.I2CDevice; import com.pi4j.io.i2c.I2CFactory; import com.pi4j.platform.Platform; import com.pi4j.platform.PlatformManager; public class I2CDS3231Write { private static final int DS3231_ADDR = 0x68; private static final int DS3231_TIME_SECONDS_ADDR = 0x00; private static final int DS3231_TIME_MINUTES_ADDR = 0x01; private static final int DS3231_TIME_HOURS_ADDR = 0x02; private static final int DS3231_TIME_WEEK_DAY_ADDR = 0x03; private static final int DS3231_TIME_DATE_ADDR = 0x04; private static final int DS3231_TIME_MONTH_CENTURY_ADDR = 0x05; private static final int DS3231_TIME_YEAR_ADDR = 0x06; public static void main(String[] args) throws Exception { PlatformManager.setPlatform(Platform.ORANGEPI); I2CBus i2c = I2CFactory.getInstance(I2CBus.BUS_0); I2CDevice device = i2c.getDevice(DS3231_ADDR); LocalDateTime now = LocalDateTime.now(); device.write(DS3231_TIME_SECONDS_ADDR, decToBcd(now.getSecond())); device.write(DS3231_TIME_MINUTES_ADDR, decToBcd(now.getMinute())); device.write(DS3231_TIME_HOURS_ADDR, decToBcd(now.getHour())); device.write(DS3231_TIME_WEEK_DAY_ADDR, decToBcd(now.getDayOfWeek().getValue())); device.write(DS3231_TIME_DATE_ADDR, decToBcd(now.getDayOfMonth())); byte century; int yearShort; if (now.getYear() >= 2000) { century = (byte) 0x80; yearShort = (now.getYear() - 2000); } else { century = 0; yearShort = (now.getYear() - 1900); } device.write(DS3231_TIME_MONTH_CENTURY_ADDR, (byte) (decToBcd(now.getMonthValue()) | century)); device.write(DS3231_TIME_YEAR_ADDR, decToBcd(yearShort)); } static byte decToBcd(int val) { return (byte) ((val / 10 * 16) + (val % 10)); } }
Проверяем код:
- создаём java файл и вставляем код;
nano I2CDS3231Write.java
- компилируем файл;
javac -classpath .:classes:/opt/pi4j/lib/'*' I2CDS3231Write.java
- запускаем программу.
sudo java -classpath .:classes:/opt/pi4j/lib/'*' I2CDS3231Write
Пример 2 — чтение данных
Для чтения данных с устройства используем методы read
.
device.read(DS3231_TIME_SECONDS_ADDR); /*...*/ device.read(DS3231_TIME_SECONDS_ADDR, buffer, 0, buffer.length);
Читаем данные температуры. Текущее значение температуры хранится в регистрах с адресами 0x11 и 0x12, старший и младший байт соответственно, значение температуры в регистрах периодически обновляется. Установлено левое выравнивание, разрешение составляет 10 бит или 0,25°C/LSB, то есть в старшем байте находится целая часть температуры, а 6, 7-й биты в младшем регистры составляют дробную часть. В старшем байте 7-й бит указывает знак температуры, например, значению 00011010 01 соответствует температура +26.25 °C, значению 11111100 10 температура -4.5 °C.
int tempMsb = device.read(DS3231_TEMPERATURE_ADDR_MSB); int tempLsb = device.read(DS3231_TEMPERATURE_ADDR_LSB) >> 6; int nint; if ((tempMsb & 0x80) != 0) { nint = tempMsb | ~((1 << 8) - 1); /* если отрицательное, получаем двоичное дополнение */ } else { nint = tempMsb; } double temperature = 0.25 * tempLsb + nint; System.out.println("t = " + temperature + " °C");
Этот пример программы считывает и выводит в консоль данные даты, времени и температуры.
import java.util.HashMap; import java.util.Map; import com.pi4j.io.i2c.I2CBus; import com.pi4j.io.i2c.I2CDevice; import com.pi4j.io.i2c.I2CFactory; import com.pi4j.platform.Platform; import com.pi4j.platform.PlatformManager; public class I2CDS3231Read { private static final int DS3231_ADDR = 0x68; private static final int DS3231_TIME_SECONDS_ADDR = 0x00; private static final int DS3231_TIME_MINUTES_ADDR = 0x01; private static final int DS3231_TIME_HOURS_ADDR = 0x02; private static final int DS3231_TIME_WEEK_DAY_ADDR = 0x03; private static final int DS3231_TIME_DATE_ADDR = 0x04; private static final int DS3231_TIME_MONTH_CENTURY_ADDR = 0x05; private static final int DS3231_TIME_YEAR_ADDR = 0x06; private static final int DS3231_TEMPERATURE_ADDR_MSB = 0x11; private static final int DS3231_TEMPERATURE_ADDR_LSB = 0x12; public static Map<Integer, String> weekDay = new HashMap<Integer, String>() { private static final long serialVersionUID = 1193975786754897061L; { put(1, "Понедельник"); put(2, "Вторник"); put(3, "Среда"); put(4, "Четверг"); put(5, "Пятница"); put(6, "Суббота"); put(7, "Воскресенье"); } }; public static void main(String[] args) throws Exception { PlatformManager.setPlatform(Platform.ORANGEPI); I2CBus i2c = I2CFactory.getInstance(I2CBus.BUS_0); I2CDevice device = i2c.getDevice(DS3231_ADDR); int tempMsb = device.read(DS3231_TEMPERATURE_ADDR_MSB); int tempLsb = device.read(DS3231_TEMPERATURE_ADDR_LSB) >> 6; int nint; if ((tempMsb & 0x80) != 0) { nint = tempMsb | ~((1 << 8) - 1); } else { nint = tempMsb; } double temperature = 0.25 * tempLsb + nint; System.out.println("t = " + temperature + " °C"); System.out.println(String.format("%s %s/%s/%s %s:%s:%s", weekDay.get(bcdToDec(device.read(DS3231_TIME_WEEK_DAY_ADDR))), bcdToDec(device.read(DS3231_TIME_DATE_ADDR)), bcdToDec(device.read(DS3231_TIME_MONTH_CENTURY_ADDR) & 0x1F), bcdToDec(device.read(DS3231_TIME_YEAR_ADDR)) + 2000, bcdToDec(device.read(DS3231_TIME_HOURS_ADDR)), bcdToDec(device.read(DS3231_TIME_MINUTES_ADDR)), bcdToDec(device.read(DS3231_TIME_SECONDS_ADDR)))); Thread.sleep(1000); byte[] buffer = new byte[7]; device.read(DS3231_TIME_SECONDS_ADDR, buffer, 0, buffer.length); System.out.println(String.format("%s %s/%s/%s %s:%s:%s", weekDay.get(bcdToDec(buffer[3])), bcdToDec(buffer[4]), bcdToDec(buffer[5] & 0x1F), bcdToDec(buffer[6]) + 2000, bcdToDec(buffer[2]), bcdToDec(buffer[1]), bcdToDec(buffer[0]))); } static int bcdToDec(byte val) { int intVal = val & 0xFF; return ((intVal / 16 * 10) + (intVal % 16)); } static int bcdToDec(int val) { return ((val / 16 * 10) + (val % 16)); } }
Проверяем код:
- создаём java файл и вставляем код;
nano I2CDS3231Read.java
- компилируем файл;
javac -classpath .:classes:/opt/pi4j/lib/'*' I2CDS3231Read.java
- запускаем программу.
sudo java -classpath .:classes:/opt/pi4j/lib/'*' I2CDS3231Read
Результат
Как мы видим, данные успешно получены, так что Pi4J можно использовать для работы с I2C.