3. Linux内核ADC子系统概述:IIO框架介绍、关键数据结构解析

在Linux内核中,模数转换器(ADC)驱动并不直接暴露/dev/adc这样的杂项设备接口,而是被纳入一个统一的、面向工业输入/输出的子系统——IIO(Industrial I/O)。IIO框架为传感器、ADC、DAC等设备提供了标准化的驱动模型和用户空间访问接口。理解IIO框架是开发高质量ADC驱动的第一步。

3.1 IIO框架设计目标与分层架构

IIO框架的设计目标可以概括为三点:

  • 统一设备模型:为各类工业I/O设备提供一致的驱动编写规范。
  • 高效数据通路:支持轮询、中断触发和DMA传输,满足不同采样率需求。
  • 清晰用户接口:通过sysfs和字符设备两种方式暴露数据,兼顾配置与批量读取。

下图展示了IIO框架在Linux内核中的分层位置与核心组件关系:

IIO框架分层架构与核心组件 用户空间 /sys/bus/iio/devices/iio:deviceX /dev/iio:deviceX (字符设备) 内核核心层 IIO Core (iio_device_register / iio_device_alloc / buffer / trigger) 驱动层 ADC 驱动 (实现 iio_info / iio_chan_spec / 注册回调) 硬件层 ADC 硬件外设 (SPI / I2C / 内存映射寄存器) 核心数据结构 iio_dev / iio_chan_spec iio_info / iio_buffer
设计哲学:IIO框架将“通道描述”与“数据采集”分离。驱动开发者只需描述硬件有哪些通道、每个通道的属性(类型、精度、是否差分),以及如何读写硬件寄存器;IIO Core负责管理缓冲区、触发器和用户空间接口。

3.2 关键数据结构一:struct iio_dev

struct iio_dev是IIO设备的顶层抽象,代表一个完整的ADC器件。驱动通过devm_iio_device_alloc()分配并初始化该结构体。其核心成员如下表所示:

成员名称 类型 描述 ADC驱动中的典型设置
id int 设备ID,用于生成设备名称后缀 通常设为0,或使用PLATFORM_DEVID_AUTO
name const char * 设备名称,出现在/sys/bus/iio/devices/中 例如 "my_adc"
info const struct iio_info * 驱动操作函数集(见下文) 必须实现read_raw回调
channels const struct iio_chan_spec * 指向通道描述数组的指针 定义所有模拟输入通道
num_channels unsigned int 通道数量 与channels数组元素个数一致
modes unsigned long 支持的采集模式掩码 INDIO_DIRECT_MODE | INDIO_BUFFER_TRIGGERED
buffer struct iio_buffer * 数据缓冲区,用于批量/连续采集 由IIO Core在使能buffer时自动分配
setup_ops const struct iio_buffer_setup_ops * 缓冲区生命周期回调(预/后使能、预/后禁用) 用于配置DMA或FIFO
驱动注册流程:
struct iio_dev *indio_dev;
indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(struct my_adc_private));
if (!indio_dev)
    return -ENOMEM;

indio_dev->name = "my-adc";
indio_dev->info = &my_adc_info;
indio_dev->channels = my_adc_channels;
indio_dev->num_channels = ARRAY_SIZE(my_adc_channels);
indio_dev->modes = INDIO_DIRECT_MODE | INDIO_BUFFER_TRIGGERED;

return devm_iio_device_register(&pdev->dev, indio_dev);

3.3 关键数据结构二:struct iio_chan_spec

struct iio_chan_spec是IIO框架中最精细的数据结构,它描述了一个物理通道或虚拟通道的属性。对于多通道ADC,每个模拟输入引脚对应一个iio_chan_spec实例。

下表列出了ADC驱动中常用的通道描述符成员:

成员 含义 ADC场景示例
type 通道类型 IIO_VOLTAGE(电压输入)
channel 通道索引号(从0开始) 0, 1, 2, ...
channel2 第二通道索引(用于差分对) 差分模式时指定负端
address 硬件相关地址/偏移 ADC多路选择器的通道选择值
scan_index 扫描序列中的位置(用于buffer) 决定数据在buffer中的排列顺序
scan_type 扫描数据类型(精度、符号、对齐) { .sign = 's', .realbits = 12, .storagebits = 16 }
info_mask_separate 每个通道独立的属性掩码 BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE)
info_mask_shared_by_type 同类型通道共享的属性掩码 BIT(IIO_CHAN_INFO_SAMP_FREQ)
differential 是否为差分输入 1表示差分,0表示单端

下面是一个4通道单端ADC的通道描述数组定义示例:

#define MY_ADC_CHANNEL(idx) { \
    .type = IIO_VOLTAGE, \
    .indexed = 1, \
    .channel = idx, \
    .address = idx, \
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
                          BIT(IIO_CHAN_INFO_SCALE), \
    .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SAMP_FREQ), \
    .scan_index = idx, \
    .scan_type = { \
        .sign = 'u', \
        .realbits = 12, \
        .storagebits = 16, \
        .shift = 0, \
        .endianness = IIO_CPU, \
    }, \
}

static const struct iio_chan_spec my_adc_channels[] = {
    MY_ADC_CHANNEL(0),
    MY_ADC_CHANNEL(1),
    MY_ADC_CHANNEL(2),
    MY_ADC_CHANNEL(3),
};
常见错误:scan_index必须从0开始连续递增,且不能重复。如果某个通道不需要参与批量扫描(如温度传感器通道),应将其scan_index设为-1。否则,IIO buffer在重组数据时会出现错位或崩溃。

3.4 关键数据结构三:struct iio_info

struct iio_info是驱动与IIO Core之间的操作契约。ADC驱动至少需要实现read_raw回调,用于处理用户空间的单次读取请求。对于支持DMA和buffer的驱动,还需要实现write_raw、update_scan_mode等回调。

回调函数 触发时机 ADC驱动中的典型实现
read_raw 用户读取in_voltageX_raw或in_voltage_scale 启动单次转换,读取结果寄存器,返回原始值或换算系数
write_raw 用户写入in_voltage_sampling_frequency 设置ADC采样率寄存器
update_scan_mode 用户使能buffer并选择扫描通道时 配置ADC多路选择器,使能对应通道,准备DMA描述符
set_trigger_state 触发源使能/禁用时 启动或停止硬件触发采集

3.5 通道属性与用户空间映射

IIO框架通过info_mask_separate和info_mask_shared_by_type自动生成sysfs属性文件。理解这个映射关系对调试驱动至关重要。

下图展示了通道描述符如何映射到用户空间的sysfs节点:

通道描述符 → sysfs 属性映射 内核空间 (iio_chan_spec) .channel = 0, .type = IIO_VOLTAGE .info_mask_separate = BIT(RAW) | BIT(SCALE) .channel = 1, .type = IIO_VOLTAGE .info_mask_separate = BIT(RAW) | BIT(SCALE) .info_mask_shared_by_type = BIT(SAMP_FREQ) 所有电压通道共享采样率 IIO Core 自动生成 用户空间 (sysfs) in_voltage0_raw in_voltage0_scale in_voltage1_raw in_voltage1_scale in_voltage_sampling_frequency (共享属性,只出现一次) scan_elements / buffer / trigger (使能buffer后出现)
调试技巧:驱动加载成功后,执行 ls /sys/bus/iio/devices/iio:deviceX/ 查看自动生成的属性文件。如果缺少预期的in_voltageX_raw,请检查info_mask_separate是否包含了BIT(IIO_CHAN_INFO_RAW)。

3.6 多通道ADC的通道描述设计模式

对于多通道ADC,推荐使用宏定义来生成通道描述数组,避免重复代码。以下是一个支持单端和差分混合输入的通道定义模式:

/* 单端通道宏 */
#define ADC_SE_CHANNEL(_index, _addr) { \
    .type = IIO_VOLTAGE, \
    .indexed = 1, \
    .channel = _index, \
    .address = _addr, \
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \
    .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE) | \
                                BIT(IIO_CHAN_INFO_SAMP_FREQ), \
    .scan_index = _index, \
    .scan_type = { .sign = 'u', .realbits = 12, .storagebits = 16 }, \
}

/* 差分通道宏 */
#define ADC_DIFF_CHANNEL(_index, _addr, _neg) { \
    .type = IIO_VOLTAGE, \
    .differential = 1, \
    .indexed = 1, \
    .channel = _index, \
    .channel2 = _neg, \
    .address = _addr, \
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \
    .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE) | \
                                BIT(IIO_CHAN_INFO_SAMP_FREQ), \
    .scan_index = _index, \
    .scan_type = { .sign = 's', .realbits = 12, .storagebits = 16 }, \
}

static const struct iio_chan_spec adc_channels[] = {
    ADC_SE_CHANNEL(0, 0),       /* 单端通道0 */
    ADC_SE_CHANNEL(1, 1),       /* 单端通道1 */
    ADC_DIFF_CHANNEL(2, 2, 3), /* 差分对 (IN2 - IN3) */
    ADC_DIFF_CHANNEL(3, 4, 5), /* 差分对 (IN4 - IN5) */
};
关键点:
  • 差分通道的scan_type.sign应设为's'(有符号),因为差分结果可能为负。
  • 单端通道的scan_type.sign通常为'u'(无符号)。
  • storagebits必须大于等于realbits,且通常是8/16/32等对齐值。

3.7 本章小结