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框架将“通道描述”与“数据采集”分离。驱动开发者只需描述硬件有哪些通道、每个通道的属性(类型、精度、是否差分),以及如何读写硬件寄存器;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节点:
调试技巧:驱动加载成功后,执行
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等对齐值。