[Системное программирование, C, Разработка под Linux] Нам нужно поговорить про Linux IIO

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
24-Сен-2020 15:32

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

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

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

В целом про IIO информации немного, но но она есть, а поэтому в данной обзорной статья мы сначала ...
Сосредоточимся на моментах почему 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);

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

Ну как говориться зачем всё это — если есть драйвера ?
Здесь мы опять сталкиваемся с "индивидульностью" каждого устройства (как допустим способ калибровки или размерность).
Собственно IIO даёт нам во-первых универсальность, во-вторых возможность poll по поступлению новых данных.
Сам IIO разделен на два уровня абстракции — устройства и каналы измерений.
Выделим два основных способа доступа поддержанных в официальном ядре.
Простое использование 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)[https://elixir.bootlin.com/linux/v5.9-rc1/source/include/uapi/linux/iio/types.h#L14]
Пример для датчика 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)[https://www.kernel.org/doc/html/latest/driver-api/iio/triggered-buffers.html]
Триггеры
Представляет из себя "внешнее" событие, которое инициирует захват данных с последующей передачей наверх в 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)[https://www.kernel.org/doc/html/latest/driver-api/iio/triggers.html]
(IIO sysfs trigger)[https://wiki.analog.com/software/linux/docs/iio/iio-trig-sysfs]
(Industrial IIO configfs support
)[https://www.kernel.org/doc/Documentation/iio/iio_configfs.txt]
(Triggered buffer support trigger buffer support for IIO subsystem of Linux device driver
)[https://programmer.group/5cbf67db154ab.html]
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
)[https://www.spinics.net/lists/linux-iio/msg23004.html].
Смысл заключается в опросе устройства с максимально возможной скоростью. Дополнительно можно посмотреть оригинальный комментарий к коммиту:
(iio:trigger: Experimental kthread tight loop trigger)[https://github.com/torvalds/linux/commit/bc2e1126eccb47517b9d1c685020c38600f99a3d#diff-0d329fecbdfa98eba57fd93cd6350578].
Опять же нет поддержки 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)[https://elixir.bootlin.com/linux/v5.9-rc1/source/include/uapi/linux/iio/types.h#L14] можно найти почти любой тип измерений. Расшифровку можно посмотреть тут (iio_event_monitor)[https://elixir.bootlin.com/linux/v5.9-rc1/source/tools/iio/iio_event_monitor.c#L28]
Формат данных
IIO умеет сообщать в каком формате нам передаются данные (iio-buffer-sysfs-interface)[https://01.org/linuxgraphics/gfx-docs/drm/driver-api/iio/buffers.html#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)[https://wiki.st.com/stm32mpu/wiki/How_to_use_the_IIO_user_space_interface#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)[https://flusp.ime.usp.br/iio/iio-dummy-anatomy]
(iio_simple_dummy)[https://elixir.bootlin.com/linux/latest/source/drivers/iio/dummy/iio_simple_dummy.c]
libiio
Если вышеприведенное показалось вам сложным — на помощь к вам идет (libiio)[https://github.com/analogdevicesinc/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)[https://wiki.analog.com/resources/tools-software/linux-software/libiio/clients/matlab_simulink], а так же интересный проект (осциллографа)[https://wiki.analog.com/resources/tools-software/linux-software/iio_oscilloscope].
Пример программы для чтения акселерометра
Приведу пример программы для чтения, как с использованием 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)[https://graphics.stanford.edu/~seander/bithacks.html#FixedSignExtend]
Работа с использованием libiio
Пример работы можно глянуть тут (libiio-loop.c
)[https://github.com/maquefel/icm20608-iio/blob/master/src/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)[https://elinux.org/images/8/8d/Clausen--high-speed_data_acquisition_with_the_linux_iio_framework.pdf]
По мне так это отличное решения для SDR.
Из переписки с автором я понял, что данная функциональность будет включена в официальное ядро, хотя и не в текущем виде и неизвестно когда.
Автор любезно предоставил данные изменения для ядра (4.19)[https://github.com/larsclausen/linux/tree/iio-high-speed-4.19] и (5.4)[https://github.com/larsclausen/linux/tree/iio-high-speed-5.4].
С дискуссией по данной теме можно ознакомиться тут: https://patchwork.kernel.org/project/linux-iio/list/?series=284793
Рекомендуемые материалы
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 как положено, и мне непонятно почему.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_sistemnoe_programmirovanie (Системное программирование), #_c, #_razrabotka_pod_linux (Разработка под Linux), #_iio, #_linux, #_linux_kernel, #_sensors, #_sistemnoe_programmirovanie (
Системное программирование
)
, #_c, #_razrabotka_pod_linux (
Разработка под Linux
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 15:49
Часовой пояс: UTC + 5