Raspberry Pi и Pi4J. Урок 5. Последовательная шина I2C/TWI

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-х бит. В случае одного байта, младший полубайт отсчитывает единицы, старший десятки и т. д.

Схема подключения

Raspberry Pi и Pi4J. Урок 5. Последовательная шина I2C (Orange Pi One + DS3231)

Пример 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));
  }
}

Проверяем код:

  1. создаём java файл и вставляем код;
    nano I2CDS3231Write.java
  2. компилируем файл;
    javac -classpath .:classes:/opt/pi4j/lib/'*' I2CDS3231Write.java
  3. запускаем программу.
    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));
  }
}

Проверяем код:

  1. создаём java файл и вставляем код;
    nano I2CDS3231Read.java
  2. компилируем файл;
    javac -classpath .:classes:/opt/pi4j/lib/'*' I2CDS3231Read.java
  3. запускаем программу.
    sudo java -classpath .:classes:/opt/pi4j/lib/'*' I2CDS3231Read

Результат

Raspberry Pi и Pi4J. Урок 5. Последовательная шина I2C-TWI - чтение данныхКак мы видим, данные успешно получены, так что Pi4J можно использовать для работы с I2C.

Похожие записи

Комментарии 16

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *