Нам нужно поговорить про Linux IIO

IIO (промышленный ввод / вывод) — это подсистема ядра Linux для аналого-цифровых преобразователей (АЦП), цифро-аналоговых преобразователей (ЦАП) и различных типов датчиков. Может использоваться на высокоскоростных промышленных устройствах. Она, также, включает встроенный API для других драйверов.

ksxfirvgxfdfeueuz73cjyotbt4.jpeg

Подсистема Industrial I/O Linux предлагает унифицированную среду для связи (чтения и записи) с драйверами, охватывающими различные типы встроенных датчиков и несколько исполнительных механизмов. Он также предлагает стандартный интерфейс для приложений пользовательского пространства, управляющих датчиками через sysfs и devfs.

Вот несколько примеров поддерживаемых типов датчиков в IIO:


  • АЦП / ЦАП
  • акселерометры
  • магнетометры
  • гироскопы
  • давление
  • влажность
  • температура
  • дальнометры

IIO может использоваться во многих различных случаях:


  • Низкоскоростная регистрация для медленно меняющегося входного сигнала (пример: запись температуры в файл)
  • Высоко-скоростной сбор данных с использованием АЦП, DFSDM или внешних устройств (например, аудио, измеритель мощности)
  • Считывание положения вращающегося элемента, используя интерфейс квадратурного энкодера TIM или LPTIM
  • Управление аналоговым источником через ЦАП
  • Внешние устройства подключенные через SPI или I2C

В целом про IIO информации немного, но, но она есть, а поэтому в данной обзорной статья мы сначала …

Все наверняка встречали/пользовались конструкциями типа:

# https://www.kernel.org/doc/Documentation/i2c/dev-interface
open("/dev/i2c-1", O_RDWR);
# https://www.kernel.org/doc/Documentation/spi/spidev.rst
open("/dev/spidev2.0", O_RDWR);

У данного способа много недостатков, я перечислю те которые считаю основными:


  1. нет прерываний
  2. способ доступа для данных индивидуален для каждого устройства

Ну как говориться зачем всё это — если есть драйвера ?

Здесь мы опять сталкиваемся с «индивидуальностью» каждого устройства (как допустим способ калибровки или размерность).

Собственно IIO даёт нам во-первых универсальность, во-вторых возможность poll по поступлению новых данных.

Сам IIO разделен на два уровня абстракции — устройства и каналы измерений.

Выделим два основных способа доступа поддержанных в официальном ядре.

Мы можем читать данные через sysfs (допустим для акселерометра):

# cat /sys/bus/iio/devices/iio\:device0/in_accel_x_raw
-493

Это мы прочитали «сырые» измерения, их еще надо привести к общему виду.

Либо через read ():

# Включим захват измерений для каждого канала
(cd /sys/bus/iio/devices/iio:device0/scan_elements/ && for file in *_en; do echo 1 > $file; done)

Тогда мы можем свести взаимодействие к виду :

int fd = open("/dev/iio:device0");
read(fd, buffer, scan_size);
# где scan_size это сумма размера всех заказанных измерений, то есть для всех 1 в /sys/bus/iio/devices/iio:device0/scan_elements/*_en

Размер прочитанного блока всегда кратен scan_size, мы получаем «сырые» измерения, которые надо привести к общему виду, об этом позже.


Внутреннее устройство


Каналы

Любой драйвер IIO предоставляет информацию о возможных измерениях в виде стандартного описания каналов struct iio_chan_spec:

IIO types

Пример для датчика BME280

/* https://elixir.bootlin.com/linux/v5.9-rc1/source/drivers/iio/pressure/bmp280-core.c#L132*/
static const struct iio_chan_spec bmp280_channels[] = {
    {
        .type = IIO_PRESSURE,
        .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED) |
                      BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
    },
    {
        .type = IIO_TEMP,
        .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED) |
                      BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
    },
    {
        .type = IIO_HUMIDITYRELATIVE,
        .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED) |
                      BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
    },
};

Как мы можем видеть, данный датчик предоставляет измерения температуры, влажности и давления — три отдельных канала с разными типами.

То есть мы можем читать температуру с любого датчика температуры для которого есть драйвер в ядре Linux одним и тем же способом, а так же выбрать любое сочетание данных каналов и читать только их.


Кольцевой буфер

Собственно это не так интригующее как звучит, основан на kfifo делает всё что положено кольцевому буфферу.

Новые данные вытесняют старые, что гарантирует доступ к последнему измерению в любое время, а так же то, что в худшем случае будут потеряны только старые измерения.


Метка времени

Присутствует для любого типа устройства. Нам важно знать, что метка времени выставляется, как правило, в верхней половине обработчика прерывания, что конечно же хуже чем собственная метка времени датчика, но лучшее на что мы можем рассчитывать без неё.

Представлена в наносекундах, является CLOCK_REALTIME.

IIO Triggered Buffers


Триггеры

Представляет из себя «внешнее» событие, которое инициирует захват данных с последующей передачей наверх в user space.

Один и тот же триггер может быть назначен нескольким устройствам, что позволяет получить близкие по времени измерения с нескольких независимых устройств.

Назначить триггер устройству:

# cat /sys/bus/iio/devices/iio\:device0/trigger/current_trigger
icm20608-dev0
# echo > /sys/bus/iio/devices/iio\:device0/trigger/current_trigger
# cat /sys/bus/iio/devices/iio\:device0/trigger/current_trigger
# echo "icm20608-dev0" > /sys/bus/iio/devices/iio\:device0/trigger/current_trigger

Official Trigger Documentation

IIO sysfs trigger

Industrial IIO configfs support

Triggered buffer support trigger buffer support for IIO subsystem of Linux device driver


Device owned triggers

Данный класс триггеров относиться к собственным триггерам устройства, они определяются в device tree:

icm20608: imu@0 {
    ...
    interrupt-parent = <&gpio5>;
    interrupts = <11 IRQ_TYPE_EDGE_RISING>;
    ...
};

Это даст нам соответствующий триггер с именем:

cat /sys/bus/iio/devices/trigger0/name
icm20608-dev0

Собственно конкретный данный триггер это просто выход прерывания заведенный на соответствующую ножку gpio, но это не всегда так, таким триггером может являться любой источник прерывания связанный с устройством.


Interrupt triggers (also known as gpio trigger)

iio-trig-interrupt

Фактически тоже самое что и предыдущий тип, но он не привязан ни к какому конкретному устройству. Это может быть просто кнопка подсоединенная к gpio, так и любой источник прерываний.

Данный драйвер не поддержан в ядре в полном виде, ввиду сомнений текущего maintainer’a IIO Jonathan Cameron, хотя он так же является его автором.

Единственный способ задания в официальном ядре через платформенный код — необходимый для этого платформенный код вы можете подсмотреть тут Triggered buffer support trigger buffer support for IIO subsystem of Linux device driver
.

Но кому очень хочется может воспользоваться серией патчей:

[v3,1/6] dt-bindings: iio: introduce trigger providers, consumers

Тогда задание через device tree будет выглядеть приблизительно так:

trig0: interrupt-trigger0 {
    #io-trigger-cells = <0>;
    compatible = "interrupt-trigger";
    interrupts = <11 0>;
    interrupt-parent = <&gpioa>;
};


sysfs trigger

iio-trig-sysfs

Тут всё очень просто пишем в sysfs — срабатывает триггер, устройство захватывает текущие измерения и уведомляет потребителя.

Создание триггера:

# echo 10 > /sys/bus/iio/devices/iio_sysfs_trigger/add_trigger

Число используется для генерации имени триггера в виде «sysfstrig%d», его же мы используем при задании триггера устройству.


High resolution timer trigger

Представляет из себя таймер с минимальным возможным разрешением в 1 наносекунду.

# mkdir /sys/kernel/config/iio/triggers/hrtimer/my_trigger_name
# cat /sys/bus/iio/devices/trigger4/name
my_trigger_name
# cat /sys/bus/iio/devices/trigger4/sampling_frequency
100

Одним из дополнительных случаев использования может быть опрос устройств без собственных прерываний — допустим «забыли» завести прерывание на SoC.


loop trigger

iio-trig-loop

Экспериментальный триггер предположительно инициированный PATCH v1 5/5 iio: pressure: ms5611: continuous sampling support
.

Смысл заключается в опросе устройства с максимально возможной скоростью. Дополнительно можно посмотреть оригинальный комментарий к коммиту:

iio: trigger: Experimental kthread tight loop trigger.

Опять же нет поддержки DT, так что либо добавлять через патч, либо через платформенный код.


Device tree

Здесь я хочу обратить особое внимание на возможность задать label для узла, которую лучше всего использовать если у вас много однотипных устройств, всегда текущие значения заданные в узле можно подсмотреть в директории of_node для каждого iio: device — /sys/bus/iio/devices/iio\: device0/of_node/.

Какой общей рекомендации не существует — всё индивидуально и описано в https://elixir.bootlin.com/linux/v5.9-rc1/source/Documentation/devicetree/bindings/iio


Типы каналов измерений

Многие датчики, который раньше существовали как отдельные сущности были перенесены на инфраструктуру IIO, так что похоже тут enum iio_chan_type можно найти почти любой тип измерений. Расшифровку можно посмотреть тут iio_event_monitor.


Формат данных

IIO умеет сообщать в каком формате нам передаются данные iio-buffer-sysfs-interface.

[be|le]:[s|u]bits/storagebitsXrepeat[>>shift]

Живой пример для icm20608:

# cat /sys/bus/iio/devices/iio\:device0/scan_elements/*_type
be:s16/16>>0
be:s16/16>>0
be:s16/16>>0
be:s16/16>>0
be:s16/16>>0
be:s16/16>>0
le:s64/64>>0

Тут более ли менее все понятно:


  • первым идёт порядок байт le или be соответственно мы должны позаботиться о том что порядок совпадает с нашей архитектурой или c выбранным нами порядком байт
  • затем идет тип — знаковое или без знаковое, s или u соответственно
  • затем идет длина значения в битах и через / длина поля в котором содержится значение опять же в битах, кратное количеству битов в байте
  • последним идет сдвиг

То есть если бы у нас было два значения по четыре бита упакованных в одно и тоже поле мы видели бы следующее:

be:u4/8>>0
be:u4/8>>4

Предпоследнее не показанное в живом примере поле repeat — если оно больше 1 передается сразу массив измерений.


Scaling and offset

Как я уже говорил ранее прочитанные данные в сыром виде необходимо привести к общему виду:

/sys/bus/iio/devices/iio:deviceX/in_*_raw
/sys/bus/iio/devices/iio:deviceX/in_*_offset
/sys/bus/iio/devices/iio:deviceX/in_*_scale

В общем случае преобразование будет иметь вид (raw + offset)*scale, для какого то из типов датчиков offset’a может и не быть.

How to do a simple ADC conversion using the sysfs interface


iio_simple_dummy

Для изучения и тестирования может пригодится iio_simple_dummy — модуль ядра эмулирующий абстрактное устройство IIO устройство для следующих каналов:


  • IIO_VOLTAGE
  • IIO_ACCEL
  • IIO_ACTIVITY

The iio_simple_dummy Anatomy

iio_simple_dummy

Если вышеприведенное показалось вам сложным — на помощь к вам идет libiio от Analog Devices.

Помимо того, что она берет на себя рутинные вещи наподобие разбора формата канала или включения/выключения каналов.

У неё есть интересная особенность в виде возможности работы в виде сервера/клиента, в таком случае устройство с датчиками служит в качестве сервера данных, а клиент может располагаться на Linux, Windows или Mac машине, и соединяться через USB, Ethernet или Serial.

Соединение с удаленным узлом iiod:

On remote:

host # iiod

On local:

local $ iio_info -n [host_address]
local $ iio_attr -u ip:[host_address] -d
local $ iio_readdev -u ip:[host_address] -b 256 -s 0 icm20608

Отдельно хочется отметить поддержку Matlab, а так же интересный проект осциллографа.

Приведу пример программы для чтения, как с использованием libiio так и без.

https://github.com/maquefel/icm20608-iio


Работа без использования libiio

Я не буду касаться банальной работы с sysfs так, что в общих чертах для чтения необходимо сделать следующее:


  • Поиск устройства, здесь мы ориентируемся на /sys/bus/iio/iio: deviceN/name, соответственно /sys/bus/iio/iio: deviceN будет совпадать с /dev/iio: deviceN
  • Инициализация каналов в /sys/bus/iio/iio: deviceN/scan_elements/, нам будут передаваться измерения только с тех каналов, которые мы заказали в *_en
  • Инициализация буфера /sys/bus/iio/iio: deviceN/enable

В примере есть минимум необходимый для работы.


Выравнивание

Eго придется делать самим если мы хотим обойтись без libiio.

https://elixir.bootlin.com/linux/v5.9-rc1/source/drivers/iio/industrialio-buffer.c#L574

Простой код для вычисления смещения для каждого канала:

    # bytes - всего длина всего пакета в байтах
    # length - длина канала в байтах
    # offset - смещения относительно начала пакета для канала в байтах
    if (bytes % length == 0)
        offset = bytes;
    else
        offset = bytes - bytes % length + length;

    bytes = offset + length;

Что в случае без libiio, что в противоположном случае измерение необходимо привести к окончательному виду:


  • привести порядок байт в соответствие с используемым
  • сдвинуть на необходимое значение
  • обрезать лишнее
  • если знаковое, то проделать расширение знака (Sign extension)
  • если есть offset, то прибавить до применения шкалы
  • если есть scale, то применить шкалу
    input = is_be ? betoh(input) : letoh(input);
    input >>= shift;
    input &= BIT_MASK(bits);
    value = is_signed ? (float)sext(input, bits) : (float)input;
    if(with_offset) value += offset;
    if(with_scale) value *= scale;

Примечание: Расширение знака (Sign extension) в примере представлен самый простой непортируемый вариант. Дополнительно по теме можно глянуть тут SignExtend.


Работа с использованием libiio

Пример работы можно глянуть тут libiio-loop.c
.

Приведу псевдокод с комментариями:

# Создать контекст из uri
# uri = "ip:127.0.0.1"
# uri = "local:"
# uri = "usb:"
ctx = iio_create_context_from_uri(uri);

# Найти устройство
# допустим device = icm20608
dev = iio_context_find_device(ctx, device);

# Количество доступных каналов
nb_channels = iio_device_get_channels_count(dev);

# Включить каждый канал
for(int i = 0; i < nb_channels; i++)
    iio_channel_enable(iio_device_get_channel(dev, i));

# buffer_size = SAMPLES_PER_READ, количество последовательных измерений (по всем каналам)
buffer = iio_device_create_buffer(dev, buffer_size, false);

# Задать блокирующий режим работы
iio_buffer_set_blocking_mode(buffer, true);

while(true) {
    # Заполнить буфер
    iio_buffer_refill(buffer);

    # Способов несколько - можно читать и без использования libiio
    # Приведу в качестве примера "каноничный" способ, который заключается в том что предоставленная нами функция
    # вызывается для каждого канала
    # ssize_t print_sample(const struct iio_channel *chn, void *buffer, size_t bytes, __notused void *d)
    # const struct iio_channel *chn - текущий канал который мы обрабатываем
    # void *buffer - указатель на буфер содержащий измерения для данного канала
    # size_t bytes - длина измерения в байтах
    # __notused void *d - пользовательские данные которые мы передаем вместе с вызовом iio_buffer_foreach_sample
    iio_buffer_foreach_sample(buffer, print_sample, NULL);
}

# освободить буфер
iio_buffer_destroy(buffer);

# освободить контекст
iio_context_destroy(ctx);

В качестве альтернативы доступа к данным был предложен прототип, который позволял перемещать данные из буфера устройства сразу в пользовательский буфер, так называемый механизм Zero-Copy.

Всё это относиться к методам обработки высокоскоростного потока данных.

Сравнение методов (тезисы из презентации):

Решение первое — Блоки


  • Группировать несколько измерений в блок
  • Генерировать одно прерывание на один блок
  • Уменьшить расходы на управление
  • Размер блока должен быть конфигурируемым
  • Позволить пользовательского приложению выбирать между задержкой и накладными расходами

Решение второе — DMA + mmap ()


  • Использовать DMA чтобы перемещать данные от устройства к выделенному блоку памяти
  • Использовать mmap () чтобы иметь доступ к памяти из пользовательского пространства
  • Избежать копирования данных
  • «Бесплатное» демультиплексирование в пользовательском пространстве

High-speed Data Acquisition
using the
Linux Industrial IO framework

По мне так это отличное решения для SDR.

Из переписки с автором я понял, что данная функциональность будет включена в официальное ядро, хотя и не в текущем виде и неизвестно когда.

Автор любезно предоставил данные изменения для ядра 4.19 и 5.4.

С дискуссией по данной теме можно ознакомиться тут


Рекомендуемые материалы

https://bootlin.com/pub/conferences/2012/fosdem/iio-a-new-subsystem/iio-a-new-subsystem.pdf

https://archive.fosdem.org/2012/schedule/event/693/127_iio-a-new-subsystem.pdf

https://events19.linuxfoundation.org/wp-content/uploads/2017/12/Bandan-Das_Drone_SITL_bringup_with_the_IIO_framework.pdf

https://programmer.group/5cbf67db154ab.html

https://elinux.org/images/b/ba/ELC_2017_-_Industrial_IO_and_You-_Nonsense_Hacks%21.pdf

https://elinux.org/images/8/8d/Clausen--high-speed_data_acquisition_with_the_linux_iio_framework.pdf


Для дополнительного изучения

https://linux.ime.usp.br/~marcelosc/2019/09/Simple-IIO-driver

P.S.

Приношу извинения за ссылки, я не смог заставить их выглядеть на markdown как положено, и мне непонятно почему.

Пробела решилась — спасибо Exosphere — я неправильно оформлял ссылки.

© Habrahabr.ru