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:
Пример для датчика 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.
Триггеры
Представляет из себя "внешнее" событие, которое инициирует захват данных с последующей передачей наверх в 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
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)
Фактически тоже самое что и предыдущий тип, но он не привязан ни к какому конкретному устройству. Это может быть просто кнопка подсоединенная к 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
Тут всё очень просто пишем в 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
Экспериментальный триггер предположительно инициированный 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
libiio
Если вышеприведенное показалось вам сложным — на помощь к вам идет 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://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 — я неправильно оформлял ссылки.
monah_tuk
IIO это очень хорошо, но существует ли способ эту инфраструктуру использовать внутри ядра?
Я столкнулся с тем, что доступ к Xilinx AMS представлен в виде драйвера IIO, из пользовательского пространства проблем нет, можно запросить температуру ядра, температуру PL. У меня же была задача контроля оборотов вентилятора, в PL был добавлен таймер, который умеет в PWM. Нужно, по сути, было написать платформенный драйвер, который мониторит температуру, и уже по какому-то алгоритму регулирует PWM. Так вот, драйвер вроде есть, для получения данных о температуре, но как к нему достучаться из ядра — хз. Сейчас это сделано сервисом в user-space, под контролем svc.
MinimumLaw
Подождите… Что-то мне подсказывает, что вашего слона надо есть частями. Я, правда, сразу предупрежу — у меня с Xilinx не очень… Но все же — вопрос первый он простой. А зачем брать в ядро, то что вполне накрывается userspace демоном? В этом есть какой-то потаенный смыл (кроме явного переусложнения системы и ее обслуживания)? Второй: а в чем проблема внутриядерной коммуникации?
В крайнем случаеПо моим мироощущениям IIO всегда можно заменить на HWMON и THERMAL DEVICE (он, к слову, лучше подходит по духу для температуры ядра и PLL и умеет управлять устройствами).monah_tuk
Начну с конца: уже есть драйвер IIO от Xilinx. Точнее так, есть AMS блок через него много чего прочитать можно и Xilinx для всего этого богатства сделал драйвер, IIO драйвер. Потому и был направлен взор на него.
Собственно Thermal device framework и хотелось изначально использовать, подсунув ему каким-то образом данные от уже существующего сенсора, описав всё это дело через DT. Пока я вижу только вариант со своими мини-драйвером, который сможет загрузиться после инициализации AMS драйвера (что бы IOMEMORY ресурсы смогли зашарить) и вычитывать только нужную информацию
Выше написал уже про Thermal device. Собственно потому и брать. Потому как DT и ядро для нашего устройства контролируются командой приближенной к железячникам, а демон пользовательского пространства могут позабыть включить (user-space под другой командой, собранное ядро, dt и модули мы им даём).
Кроме того, были следующие факторы:
В любом случае сейчас это демон в пользовательском пространстве :)
MinimumLaw
Я не представляю объем работ. Но идеологически правильнее выкинуть IIO и перейти на Thermal. Благо Linux, благо OpenSource. В простейшем случае есть EXPORT_SYMBOL. Эта штука специально сделана для обеспечения межмодульной связи. Можно экспортировать данные и получить их в модуле Thermal. Но, откровенное говоря, это костыль. Для проекта пойдет, но шансов попасть в mainline ноль.
monah_tuk
Про mainline речи не идёт. У меня ощущения, что другие решения используют внешний сенсор и не парятся. Просто взыграл праздный интерес: есть подсистема, есть драйвер для этой подсистемы, есть полезные мне данные, есть ли возможность дёшево получить эти данные для своих нужд? Судя по всему нет.
maquefel Автор
Вот, как то я не подскажу. Документация явно намекает, что можно
https://www.kernel.org/doc/html/latest/driver-api/iio/hw-consumer.html, но сам я такого не делал, поэтому что-либо утверждать не берусь.
monah_tuk
Нужно посмотреть. Но похоже нужное нашёл: iio-hwmon и generic-adc-thermal. Нужно будет раскурить.
Alor
Вроде fancontrol из lm-sensors есть для такого?