[toc]

drivers/iio 工业I/O子系统(Industrial I/O) 统一的传感器数据采集框架

历史与背景

这项技术是为了解决什么特定问题而诞生的?

IIO(Industrial I/O)子系统的诞生是为了解决Linux内核中一个长期存在的混乱问题:缺乏一个标准的、统一的框架来处理各种传感器和数据转换器(ADC/DAC)

在此框架出现之前,这类设备的驱动程序散落在内核的各个角落,缺乏一致性:

  • 滥用其他子系统:一些加速度计驱动程序为了方便,将自己伪装成一个input设备(如摇杆),但这在语义上是不正确的。input子系统是为用户输入事件(如按键、鼠标移动)设计的,而不是为了测量物理世界的数据。
  • 使用私有的字符设备:许多驱动创建自己独特的字符设备接口,使用自定义的ioctl命令。这导致用户空间的应用程序必须为每一种不同的传感器编写特定的支持代码,无法通用。
  • 滥用sysfs:一些驱动直接通过sysfs文件来暴露连续的数据流。Sysfs的设计初衷是用于传递少量、低频的配置信息或状态值(“一文件一值”),用它来读取高速的数据流效率低下且不规范。

IIO子系统就是为了终结这种乱象,为所有能够将物理量(如加速度、光照、电压、温度)转换为数字信号的设备提供一个专门的、功能丰富的、标准化的家。

它的发展经历了哪些重要的里程碑或版本迭代?

IIO子系统的发展是围绕着提供更丰富、更高效的数据访问方式进行的。

  • 框架的建立与Sysfs接口:最初,IIO确立了其核心概念(设备、通道),并提供了基于sysfs的访问接口。这对于配置传感器参数(如采样率)和进行单次的、低速的数据读取非常方便。
  • 缓冲区与触发器(Buffer and Trigger)的引入:这是IIO发展中最重要的里程碑。为了支持高速、连续的数据采集,IIO引入了内核缓冲区机制。数据可以被采集并暂存在内核的FIFO缓冲区中。同时,trigger(触发器)机制被引入,用于精确控制数据采集的“节拍器”。触发器可以是基于时间的(hrtimer),也可以是基于外部事件的(如GPIO中断)。
  • 字符设备接口的完善:与缓冲区和触发器配套,IIO提供了标准的字符设备接口(/dev/iio:deviceX)。用户空间程序可以通过read()这个字符设备来高效地获取一批批带有时间戳的、连续的采样数据。
  • 事件(Events)支持:除了采集数据,许多传感器还能在特定事件发生时(如超过某个阈值)产生中断。IIO为此添加了事件接口,允许用户空间通过poll()select()来异步地等待这些事件,而无需持续轮询数据。

目前该技术的社区活跃度和主流应用情况如何?

IIO已经成为Linux内核中处理传感器的事实标准。它是一个非常活跃和健康的子系统,社区不断地为各种新出现的传感器芯片添加驱动。

  • 主流应用
    • 移动设备和物联网(IoT):手机、平板、可穿戴设备中的加速度计、陀螺仪、磁力计、光线传感器、压力传感器等,几乎全部使用IIO框架。
    • 工业自动化与数据采集:工业控制系统中使用的各种高精度ADC(模数转换器)和DAC(数模转换器)。
    • 机器人与无人机:IMU(惯性测量单元)等姿态传感器。
    • 医疗设备:各种生理信号采集设备。

核心原理与设计

它的核心工作原理是什么?

IIO子系统的核心是围绕几个关键概念构建的生产者/消费者模型。

  1. IIO设备 (struct iio_dev):代表一个物理传感器设备。驱动程序的核心就是填充并注册这个结构体。
  2. 通道 (struct iio_chan_spec):一个物理设备可能能测量多个不同的物理量。每个可测量的量就是一个“通道”。例如,一个三轴加速度计有三个通道:X轴加速度、Y轴加速度、Z轴加速度。一个温湿度传感器有两个通道:温度和湿度。通道是IIO中描述数据的基本单元。
  3. 两种数据访问方式
    • Sysfs接口(直接模式):用于配置和单次读取。每个通道的每个属性都会在sysfs中表现为一个文件,如/sys/bus/iio/devices/iio:device0/in_accel_x_raw。读取这个文件可以直接获得X轴的原始加速度值。这种方式简单,但性能较低。
    • 字符设备接口(缓冲模式):用于高速、连续的数据采集。驱动程序通过iio_push_to_buffers()将采集到的数据(通常带时间戳)推入内核缓冲区。用户空间程序通过read() /dev/iio:deviceX来一次性读取一批数据。
  4. 触发器 (struct iio_trigger):缓冲模式下的“起搏器”。当触发器被触发时,它会通知所有使用它的IIO设备去采集一次数据并推入缓冲区。最常见的触发器是基于高精度定时器(hrtimer)的,可以实现精确的周期性采样。
  5. 事件 (struct iio_event_interface):用于处理异步事件。传感器驱动可以定义事件类型(如“高于阈值”),并在硬件事件发生时通知用户空间。

它的主要优势体现在哪些方面?

  • 标准化:为内核和用户空间提供了统一的API,极大地简化了应用程序的开发。
  • 代码复用:触发器、缓冲区管理等都是由IIO核心提供的,驱动开发者只需关注硬件本身的逻辑。
  • 灵活性:同时支持简单、低速的sysfs访问和高效、高速的缓冲访问,满足不同场景的需求。
  • 功能丰富:内建了对时间戳、数据缓冲、硬件触发和异步事件的完善支持。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

IIO的设计目标非常明确,因此它的“劣势”主要体现在其适用范围之外。

  • 不适用于输入设备:IIO不应该用于处理键盘、鼠标、触摸屏这类以“用户输入事件”为核心的设备。这些设备有其专门的Input子系统。
  • 与Hwmon的界限:与Hwmon(硬件监控)子系统有一定的功能重叠。Hwmon主要关注于监控计算机系统自身的健康状态(如主板电压、CPU温度、风扇转速)。而IIO更侧重于测量系统外部环境的物理量。通常,板载的、用于系统自检的传感器归于Hwmon,而用于感知外部世界的传感器归于IIO。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

IIO是处理任何非用户输入的传感器或数据转换器的首选解决方案。

  • 手机屏幕自动旋转:手机中的加速度计驱动(IIO设备)被配置为以一定频率(如50Hz)进行采样(hrtimer触发器),用户空间的服务读取加速度数据流,判断手机的姿态,从而决定是否旋转屏幕。
  • 笔记本自动亮度调节:笔记本中的光线传感器(ALS)驱动(IIO设备)测量环境光强度。用户空间程序通过sysfs读取光强值,并据此调整屏幕背光亮度。
  • 无人机姿态控制:无人机上的IMU芯片(通常包含加速度计和陀螺仪)驱动使用IIO框架,通过高速缓冲模式(数百Hz)向上层飞控软件提供实时的姿态数据。
  • 工业数据采集卡:一个PCIe接口的多通道高精度ADC卡,其驱动会注册为一个IIO设备,每个模拟输入端都是一个IIO通道。

是否有不推荐使用该技术的场景?为什么?

  • 键盘、鼠标、游戏手柄:不推荐。这些设备的核心是报告状态变化(键按下/弹起),而不是测量连续的物理量。Input子系统提供了更符合其语义的模型(EV_KEY, EV_REL等)。
  • 主板上的CPU核心温度传感器:不推荐。这属于系统自身健康监控的范畴,更适合使用Hwmon子系统,它提供了符合libsensors等标准用户空间工具的接口。

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 IIO Subsystem Input Subsystem Hwmon Subsystem
核心功能 测量物理世界的连续或离散数据 报告用户或设备产生的事件 监控系统内部组件的健康状态
数据模型 以**通道 (Channel)**为中心,每个通道代表一种测量值。 以**事件 (Event)**为中心,如按键事件、相对/绝对位移事件。 以**属性 (Attribute)**为中心,每个属性代表一个监控项(如温度、电压)。
典型设备 加速度计, ADC, DAC, 陀螺仪, 光/温/湿度传感器。 键盘, 鼠标, 触摸屏, 游戏手柄, 旋钮。 CPU/主板温度传感器, 电压传感器, 风扇转速计。
主要接口 Sysfs (配置/单次读), Chardev (高速缓冲读), Event chardev。 Event chardev (/dev/input/eventX)。 Sysfs (所有数据和配置)。
适用场景 需要量化采集物理世界参数的场景。 需要处理人机交互或状态变化事件的场景。 需要监控系统自身运行状态的场景。

drivers/iio/industrialio-core.c IIO核心(IIO Core) 工业I/O子系统的中枢

核心原理与设计

它的核心工作原理是什么?

industrialio-core.c是IIO子系统的中央协调器。它负责管理IIO设备的生命周期,并构建其在sysfs中的视图。

  1. 核心数据结构 (struct iio_dev):这是IIO子系统的基本单元,代表一个物理传感器设备。设备驱动程序的主要工作就是分配和填充这个结构体,包括它的名称、通道定义、以及操作函数集(struct iio_info)。
  2. 设备生命周期管理
    • 注册 (iio_device_register): 这是核心中的核心函数。当一个传感器驱动调用它时,industrialio-core.c会执行一系列关键操作:
      1. 分配一个唯一的设备ID。
      2. /sys/bus/iio/devices/ 目录下创建一个名为 iio:deviceX 的设备目录。
      3. 动态创建Sysfs接口:这是最关键的一步。它会解析驱动提供的 iio_chan_spec(通道规范)数组。对于数组中的每一个通道每一个属性(如_raw, _scale, _sampling_frequency),它都会在sysfs中自动创建一个对应的文件。
      4. 将这些sysfs文件的读写操作链接到驱动在 struct iio_info 中提供的回调函数(如 .read_raw, .write_raw)。
    • 注销 (iio_device_unregister): 执行相反的操作,清理所有sysfs文件和设备注册信息。
  3. 连接其他组件industrialio-core.c还充当了连接点的角色。iio_dev结构体中包含了指向缓冲区设置(setup_ops)和触发器(trigger)的指针。当设备被注册时,IIO核心会调用这些操作函数,让缓冲区和触发器子系统有机会与设备进行绑定。

简单来说,industrialio-core.c就像一个建筑师,当传感器驱动(客户)带着一张蓝图(iio_dev结构)来找它时,它会负责建造一座大楼(在sysfs中创建完整的目录和文件结构),并确保每个房间(sysfs文件)的门把手(读/写操作)都能正确地调用客户提供的功能(驱动的回调函数)。

IIO子系统核心初始化:创建iio总线与设备号池

本代码片段展示了Linux内核中工业I/O(Industrial I/O, IIO)子系统的核心初始化过程。其功能是在内核启动时,建立IIO子系统运行所需的基础软件设施。这主要包括三个方面:1)注册一个名为 “iio” 的新总线类型;2) 预先申请一块字符设备号区域,供后续具体的IIO设备使用;3) 在debugfs中创建一个目录,用于调试。这是所有IIO设备驱动能够被正确加载和管理的前提。

实现原理分析

该代码的实现遵循标准的Linux内核模块和子系统初始化流程,并与设备模型、字符设备子系统及debugfs紧密集成。

  1. 总线注册 (bus_register):

    • 代码首先定义了一个bus_type结构体 iio_bus_type,并将其.name字段设置为 “iio”。
    • iio_init函数中,bus_register(&iio_bus_type)是第一个关键步骤。这个调用将 “iio” 总线注册到Linux设备模型核心。其直接后果是在sysfs中创建了/sys/bus/iio/目录。
    • 这个总线的作用是为所有IIO相关的设备和驱动提供一个“聚集点”。具体的IIO设备驱动(如ADC, 加速计驱动)会声明自己属于iio_bus_type,而具体的IIO设备(由平台总线或其他总线上的设备实例化而来)也会被归类到这个总线上。设备模型核心利用这个总线来匹配合适的驱动和设备,并调用驱动的probe函数。
  2. 字符设备号分配 (alloc_chrdev_region):

    • IIO设备通常会通过字符设备接口向用户空间提供服务,表现为/dev/iio:deviceX这样的设备文件。每个字符设备都需要一个独一无二的主/次设备号对。
    • alloc_chrdev_region函数向内核的字符设备管理器动态申请一段连续的设备号。IIO_DEV_MAX定义了申请的设备号数量。
    • 此操作并不会创建任何设备文件,它只是预留了一段“门牌号”。当一个具体的IIO设备被成功探测并注册时,IIO核心会从这个预留的号段中取出一个未使用的设备号分配给该设备,并结合之前注册的iio_class(在其他文件中定义)来自动创建/dev/iio:deviceX节点。
  3. 调试接口创建 (debugfs_create_dir):

    • debugfs是内核提供给开发者用于调试的虚拟文件系统。debugfs_create_dir("iio", NULL)在debugfs的根目录下创建了一个名为 “iio” 的子目录。
    • 后续,IIO核心和各个IIO设备驱动可以在这个目录下创建文件,用于实时导出内部状态、寄存器值等调试信息,极大地便利了驱动的开发和调试。
  4. 初始化与退出:

    • subsys_initcall(iio_init)确保了这个初始化函数在内核启动的早期被调用,早于任何可能依赖于它的IIO设备驱动。
    • iio_exit函数则执行相反的清理操作,在模块卸载时(虽然IIO核心通常不会被卸载)依次移除debugfs目录、注销总线、释放字符设备号区域,保证系统资源的正确回收。

代码分析

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 用于存储IIO字符设备区域的起始设备号。
// dev_t是一个用于表示设备号(主设备号+次设备号)的类型。
static dev_t iio_devt;

// 定义IIO总线类型。
const struct bus_type iio_bus_type = {
// .name: 指定总线在sysfs中的名称,将创建 /sys/bus/iio 目录。
.name = "iio",
};
// 导出iio_bus_type符号,以便其他IIO相关的模块可以使用它。
EXPORT_SYMBOL(iio_bus_type);

// iio_init: IIO子系统的初始化函数。
static int __init iio_init(void)
{
int ret;

/* 注册sysfs总线 */
ret = bus_register(&iio_bus_type);
if (ret < 0) {
pr_err("could not register bus type\n");
goto error_nothing;
}

// 动态申请一个字符设备号区域。
// &iio_devt: 用于接收申请到的起始设备号。
// 0: 希望的起始次设备号。
// IIO_DEV_MAX: 希望申请的设备号数量。
// "iio": 与该区域关联的名称。
ret = alloc_chrdev_region(&iio_devt, 0, IIO_DEV_MAX, "iio");
if (ret < 0) {
pr_err("failed to allocate char dev region\n");
// 如果失败,跳转去注销已经注册的总线。
goto error_unregister_bus_type;
}

// 在debugfs的根目录下创建一个名为"iio"的目录。
iio_debugfs_dentry = debugfs_create_dir("iio", NULL);

return 0;

error_unregister_bus_type:
// 错误处理路径:注销iio总线。
bus_unregister(&iio_bus_type);
error_nothing:
// 错误处理路径:返回错误码。
return ret;
}

// iio_exit: IIO子系统的退出/清理函数。
static void __exit iio_exit(void)
{
// 如果设备号区域已经成功申请(iio_devt不为0)。
if (iio_devt)
// 释放之前申请的字符设备号区域。
unregister_chrdev_region(iio_devt, IIO_DEV_MAX);
// 注销iio总线类型。
bus_unregister(&iio_bus_type);
// 移除在debugfs中创建的目录。
debugfs_remove(iio_debugfs_dentry);
}
// 将iio_init注册为子系统初始化调用。
subsys_initcall(iio_init);
// 将iio_exit注册为模块退出调用。
module_exit(iio_exit);

// 以下是标准的模块信息宏。
MODULE_AUTHOR("Jonathan Cameron <jic23@kernel.org>");
MODULE_DESCRIPTION("Industrial I/O core");
MODULE_LICENSE("GPL");