0%

【驱动开发】Linux 驱动开发-IIO子系统

工业 I/O 子系统(IIO)的主要目的是为在某种意义上执行模拟到数字转换(ADC)或数字到模拟转换(DAC)或两者兼有的设备提供支持。

概述

IIO 子系统的提出是为了支持一些 ADC 或 DAC 设备,这些设备的分类介于 Hwmon 子系统和 Input 子系统的设备分类之间。Hwmon 子系统针对用于监测和控制系统本身的低采样率传感器,如风扇速度控制或温度测量。Input 子系统正如其名称所示,专注于人机交互输入设备(键盘、鼠标、触摸屏)。在某些情况下,这些与 IIO 之间存在相当大的重叠。IIO 子系统是为了支持下面这些类型的设备:

  1. ADC芯片;
  2. DAC芯片;
  3. 温度传感器;
  4. 光感器;
  5. 陀螺仪;
  6. 加速度计;
  7. CDCs;
  8. IMUs
  9. 压力传感器等等

通常,这些传感器通过 SPI 或 I2C 进行连接。这些传感器设备的一个常见用例是具有组合功能(例如,光加接近传感器)。

组成元素

IIO core 为编写许多不同类型嵌入式传感器的驱动程序提供了一个统一框架,还为用户空间应用程序操纵传感器提供了标准接口。其实现可以在 drivers/iio/industrialio-* 下找到。

IIO 设备

IIO 设备通常与单个硬件传感器相对应,并且为处理该设备的驱动程序提供所需的所有信息。存在两种方式可供用户空间应用程序与 IIO 驱动程序进行交互。

  • /sys/bus/iio/iio:deviceX/ 代表一个硬件传感器,并将同一芯片的数据通道组合在一起。

  • dev/iio:deviceX 是用于缓冲数据传输和事件信息检索的字符设备节点接口。

设备分配和注册

通常情况下,IIO 驱动程序将自己注册为 I2C 或 SPI 驱动程序,并创建两个例程: proberemove

probe:

  • 调用 iio_device_alloc() ,为 IIO 设备分配内存

  • 使用驱动程序特定信息 (例如设备名称、设备通道) 初始化 IIO 设备字段。

  • 调用 iio_device_register() ,这将向 IIO 核心注册设备。在此调用之后,设备就可以接受来自用户空间应用程序的请求了。

remove 时,我们以相反的顺序释放探针中分配的资源:

  • iio_device_unregister() ,从 IIO 核取消设备注册

  • iio_device_free()释放为 IIO 设备分配的内存

设备属性和 sysfs 接口

属性是 sysfs 下的文件文件,用于公开芯片信息,还允许应用程序设置各种配置参数。对于索引为 X 的设备,属性可以在 /sys/bus/iio/iio: deviceX/ 目录下找到。常见的属性包括:

  • “name” 表示物理芯片的描述

  • “dev” 展示了与/dev/iio:deviceX节点相关联的主设备号和次设备号对

  • “sampling_frequency_available” 指的是设备可用的离散采样频率值集合。

关于 IIO 设备的可用标准属性在 Linux 内核源中的 Documentation/ABI/testing/sysfs-bus-iio 文件中有描述。

IIO 通道

struct iio_chan_spec - 表示一个 IIO 设备通道。

IIO 设备通道是数据通道的表示形式。IIO 设备可以有一个或多个通道。例如:

  • 温度计传感器具有一个表示温度测量的通道。

  • 有两个通道指示可见光和红外光谱测量值的光传感器。

  • 一个加速度计最多可以有3个代表 X、 Y 和 Z 轴上的加速度的通道。

IIO 通道由 struct iio_chan_spec 描述。在上述例子中,温度传感器的驱动程序必须描述其通道如下:

1
2
3
4
5
6
static const struct iio_chan_spec temp_channel[] = {
{
.type = IIO_TEMP,
.info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),
},
};

公开给用户空间的通道 sysfs 属性以位掩码的形式指定。根据它们的共享信息,可以在下列掩码之一中设置属性:

  • Info_ask_different,属性将特定于此通道
  • Info_block_share_by_type,属性由同一类型的所有通道共享
  • Info_block_share_by_dir,属性由同一方向的所有通道共享
  • Info_ask_share_by_all,属性由所有通道共享

当同一通道类型多个数据通道时,我们有两种方法来区分它们:

  • iio_chan_spec.modified 字段设置为 1 。修饰符通过同一 iio_chan_spec 结构的.channel2 字段来指定,用于表明通道的物理上独特的特性,例如其方向或光谱响应。例如,一个光传感器可以有两个通道,一个用于红外光,一个用于红外和可见光。

  • iio_chan_spec.indexed 字段的设置为 1 。在这种情况下,该通道由 .channel 字段指定索引的来区分两个实例。

使用 modified 字段区分同一类型的通道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static const struct iio_chan_spec light_channels[] = {
{
.type = IIO_INTENSITY,
.modified = 1,
.channel2 = IIO_MOD_LIGHT_IR,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
.info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
{
.type = IIO_INTENSITY,
.modified = 1,
.channel2 = IIO_MOD_LIGHT_BOTH,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
.info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
{
.type = IIO_LIGHT,
.info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),
.info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
}

上述 channels 的定义,会生成两个不同的 sysfs 文件用于用户空间获取原始数据

  • /sys/bus/iio/iio:deviceX/in_intensity_ir_raw
  • /sys/bus/iio/iio:deviceX/in_intensity_both_raw

还会生成一个文件用于处理数据

  • /sys/bus/iio/iio:deviceX/in_illuminance_input

还有一个共享的文件,用于用户空间获取采样频率

  • /sys/bus/iio/iio:deviceX/sampling_frequency

使用 index 字段区分同一类型的通道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static const struct iio_chan_spec light_channels[] = {
{
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 0,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
{
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 1,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
}

上述 channels 的定义,会生成两个不同的 sysfs 文件用于用户空间获取原始数据

  • /sys/bus/iio/devices/iio:deviceX/in_voltage0_raw,代表了 channel 0 的电压测量值
  • /sys/bus/iio/devices/iio:deviceX/in_voltage1_raw,代表了 channel 1 的电压测量值

IIO 缓冲区

工业 I/O 核心提供了一种连续数据捕获方法。可以同时从 /dev/iio:deviceX 字符设备节点读取多个数据通道,从而减少 CPU 负载。

sysfs 接口

IIO 缓冲区在 /sys/bus/iio/iio:deviceX/buffer/* 下有一个关联的属性目录:

  • length:指的是缓冲区能够存储的数据样本的总数(容量)。
  • enable:表示使能缓冲区捕获。

IIO buffer 设置

与放置在缓冲区中的通道读取相关的元信息被称为扫描元素。配置扫描元素的重要位通过 /sys/bus/iio/iio:deviceX/scan_elements/* 目录向用户空间应用程序公开。此文件包含以下形式的属性:

  • enable:用于启用通道。当且仅当其属性非零时,触发捕获将包含此通道的数据样本。

  • type:缓冲区中扫描元素数据存储的描述,也是从用户空间读取它的形式。格式是 [be|le]:[s|u]bits/storagebitsXrepeat[>>shift]

    • be或le,指定大端或小端。
    • s或u,指定是有符号的(2的补码)还是无符号的。
    • bit,是有效数据位数。
    • storagebit,是它在缓冲区中占用的位数(填充后)。
    • shift,如果指定,是在屏蔽未使用的位之前需要应用的移位。
    • repeat,指定位/存储位重复的数量。当重复元素为0或1时,则省略重复值。

例如,一个具有 12 位分辨率的 3 轴加速度计的驱动程序,其中数据按如下方式存储在两个 8 位寄存器中:

1
2
3
4
5
6
7
8
9
  7   6   5   4   3   2   1   0
+---+---+---+---+---+---+---+---+
|D3 |D2 |D1 |D0 | X | X | X | X | (LOW byte, address 0x06)
+---+---+---+---+---+---+---+---+

7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
|D11|D10|D9 |D8 |D7 |D6 |D5 |D4 | (HIGH byte, address 0x07)
+---+---+---+---+---+---+---+---+

对于每个轴的加速度,将会有以下扫描元素类型

1
2
$ cat /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_y_type
le:s12/16>>4

用户空间应用程序将从缓冲区读取的数据样本解释为两字节的小端有符号数据,这需要在屏蔽12位有效数据之前进行4位右移

为了支持 buffer ,在驱动程序中必须按照如下方式定义 iio_chan_spec

1
2
3
4
5
6
7
8
9
10
11
12
struct iio_chan_spec {
/* other members */
int scan_index
struct {
char sign;
u8 realbits;
u8 storagebits;
u8 shift;
u8 repeat;
enum iio_endian endianness;
} scan_type;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct struct iio_chan_spec accel_channels[] = {
{
.type = IIO_ACCEL,
.modified = 1,
.channel2 = IIO_MOD_X,
/* other stuff here */
.scan_index = 0,
.scan_type = {
.sign = 's',
.realbits = 12,
.storagebits = 16,
.shift = 4,
.endianness = IIO_LE,
},
}
/* similar for Y (with channel2 = IIO_MOD_Y, scan_index = 1)
* and Z (with channel2 = IIO_MOD_Z, scan_index = 2) axis
*/
}

这里 scan_index 定义启用的通道在缓冲区中的放置顺序scan_index 较低的通道将放置在索引较高的通道之前。每个通道都需要有一个唯一scan_index。 将 scan_index 设置为 -1 可用于指示特定通道不支持缓冲捕获。在这种情况下,不会为 scan_elements 目录中的通道创建任何条目。

IIO 触发器

在许多情况下,对于驱动程序而言,能够基于某些外部事件(触发)捕获数据,而不是定期轮询数据是有用的。IIO 触发可以由同时具有基于硬件生成事件(例如数据准备就绪或超过阈值)的 IIO 设备的设备驱动程序提供,也可以由来自独立中断源(例如连接到某些外部系统的 GPIO 线路、定时器中断或用户空间在 sysfs 中写入特定文件)的单独驱动程序提供。触发可能会为多个传感器启动数据捕获,并且它可能与传感器本身完全无关。

sysfs 接口

在 sysfs 中有两个与触发相关的位置:

  • /sys/bus/iio/devices/triggerY/* ,一旦 IIO 触发向 IIO 核心注册,就会创建此文件,并对应索引为 Y 的触发。由于触发因类型不同可能差异很大,因此这里我们可以描述的标准属性很少:
    • name :可用于与设备关联的触发名称。
    • sampling_frequency,一些基于定时器的触发器使用此属性来指定触发调用的频率。
  • /sys/bus/iio/devices/iio:deviceX/trigger/*,一旦设备支持触发缓冲区,就会创建此目录。我们可以通过在 current_trigger 文件中写入触发器的名称将触发器与我们的设备相关联。

IIO trigger 设置

下面的代码展示了在驱动程序中,使用 触发器的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct iio_trigger_ops trigger_ops = {
.set_trigger_state = sample_trigger_state,
.validate_device = sample_validate_device,
}

struct iio_trigger *trig;

/* first, allocate memory for our trigger */
trig = iio_trigger_alloc(dev, "trig-%s-%d", name, idx);

/* setup trigger operations field */
trig->ops = &trigger_ops;

/* now register the trigger with the IIO core */
iio_trigger_register(trig);

其中,struct iio_trigger_ops 表示 IIO trigger 的操作:

  • set_trigger_state,根据需求打开/关闭触发。
  • validate_device,当当前触发发生变化时用于验证设备的函数。

IIO 触发缓存区

缓冲区触发器 的基础上,二者合在一起工作就是 IIO 触发缓冲区

IIO triggered buffer 设置

iio_triggered_buffer_setup():设置触发缓冲区和 poll function

iio_triggered_buffer_cleanup():释放上述函数申请的资源

struct iio_buffer_setup_ops:缓冲区设置相关回调函数

下面是一个使用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const struct iio_buffer_setup_ops sensor_buffer_setup_ops = {
.preenable = sensor_buffer_preenable,
.postenable = sensor_buffer_postenable,
.postdisable = sensor_buffer_postdisable,
.predisable = sensor_buffer_predisable,
};

irqreturn_t sensor_iio_pollfunc(int irq, void *p)
{
pf->timestamp = iio_get_time_ns((struct indio_dev *)p);
return IRQ_WAKE_THREAD;
}

irqreturn_t sensor_trigger_handler(int irq, void *p)
{
u16 buf[8];
int i = 0;

/* read data for each active channel */
for_each_set_bit(bit, active_scan_mask, masklength)
buf[i++] = sensor_get_data(bit)

iio_push_to_buffers_with_timestamp(indio_dev, buf, timestamp);

iio_trigger_notify_done(trigger);
return IRQ_HANDLED;
}

/* setup triggered buffer, usually in probe function */
iio_triggered_buffer_setup(indio_dev, sensor_iio_polfunc,
sensor_trigger_handler,
sensor_buffer_setup_ops);
  • iio_buffer_setup_ops,缓冲区设置函数在缓冲区配置序列中的预定义点被调用(例如启用前,禁用后)。如果未指定,IIO内核使用 defaultiio_triggered_buffer_setup_ops

  • sensor_iio_pollfunc,该函数将用作 poll function 的上半部分。它应该尽可能少地处理,因为它在中断上下文中运行。最常见的操作是记录当前时间戳,因此可以使用IIO核心定义的 iio_pollfunc_store_time() 函数。

  • sensor_trigger_handler,将用作轮询函数下半部分的函数。这在内核线程的上下文中运行,所有处理都在这里进行。它通常从设备中读取数据并将其与记录在上半部分的时间戳一起存储在内部缓冲区中。

IIO 例子

本文以 6 轴传感器 ICM-20608 为例编写驱动程序,其包括 3 轴加速度和 3 轴陀螺仪 ,通过 SPI 协议通信。

基本使用

模块相关

主要是 注册和注销 spi_driver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int __init icm20608_init(void)
{
return spi_register_driver(&icm20608_drv);
}

static void __exit icm20608_exit(void)
{
spi_unregister_driver(&icm20608_drv);
}

module_init(icm20608_init);
module_exit(icm20608_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("huashan sun <sunhuashan624@gmail.com>");
MODULE_DESCRIPTION("A demo ICM-20608 driver");

驱动框架和 probe 函数