Linux IIO ADC 驱动开发是一项系统性工程,需要深入理解硬件行为与内核框架的交互。下面我们从驱动结构、关键实现、触发缓冲模式、设备树绑定以及常见问题等多个维度,对 ADC IIO 驱动进行详细剖析。
1. IIO 设备核心:struct iio_dev 与私有数据
每个 IIO 设备在内核中由一个 struct iio_dev 表示。驱动通常通过以下方式分配和初始化:
struct my_adc_priv {
struct spi_device *spi;
struct mutex lock;
int irq;
/* 其他硬件相关字段 */
};
static int my_adc_probe(struct spi_device *spi)
{
struct iio_dev *indio_dev;
struct my_adc_priv *priv;
indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*priv));
if (!indio_dev)
return -ENOMEM;
priv = iio_priv(indio_dev);
priv->spi = spi;
mutex_init(&priv->lock);
spi_set_drvdata(spi, indio_dev);
indio_dev->name = "my-adc";
indio_dev->modes = INDIO_DIRECT_MODE; // 支持直接模式
indio_dev->info = &my_adc_info; // 操作函数集
indio_dev->channels = my_adc_channels; // 通道描述数组
indio_dev->num_channels = ARRAY_SIZE(my_adc_channels);
return devm_iio_device_register(&spi->dev, indio_dev);
}
关键点:
-
devm_iio_device_alloc一次性分配iio_dev和私有数据,简化资源管理。 -
iio_priv()获取私有数据指针,用于存放硬件访问句柄、锁等。 -
modes字段声明设备支持的操作模式,这里先支持直接模式(INDIO_DIRECT_MODE),后续可添加缓冲模式。
2. 通道定义:struct iio_chan_spec
ADC 的每个模拟输入通道都需要用 iio_chan_spec 描述,并关联到用户空间可见的属性。
static const struct iio_chan_spec my_adc_channels[] = {
{
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 0, // 通道0
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
BIT(IIO_CHAN_INFO_SCALE) |
BIT(IIO_CHAN_INFO_OFFSET),
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SAMP_FREQ),
.datasheet_name = "AIN0",
},
{
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 1,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
.datasheet_name = "AIN1",
},
};
字段详解:
-
type:通道类型,常见为IIO_VOLTAGE、IIO_TEMP、IIO_CURRENT等。 -
indexed:若为1,则通道号由channel指定;若为0,则全局唯一(适用于只有一个通道的设备)。 -
info_mask_separate:每个通道独立的属性掩码。例如IIO_CHAN_INFO_RAW表示原始值,IIO_CHAN_INFO_SCALE表示转换系数。用户空间将看到in_voltage0_raw和in_voltage0_scale等文件。 -
info_mask_shared_by_type:同类型通道共享的属性,例如采样频率对所有电压通道通用,将生成in_voltage_sampling_frequency。 -
datasheet_name:可选,用于调试或匹配硬件通道名。
3. 核心操作函数集:struct iio_info
iio_info 中的 read_raw 和 write_raw 是实现用户空间交互的关键。
static int my_adc_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
struct my_adc_priv *priv = iio_priv(indio_dev);
int ret;
mutex_lock(&priv->lock);
switch (mask) {
case IIO_CHAN_INFO_RAW:
ret = my_adc_read_channel(priv, chan->channel, val);
if (ret == 0)
ret = IIO_VAL_INT;
break;
case IIO_CHAN_INFO_SCALE:
// 假设参考电压为2.5V,12位ADC,scale = 2.5 / 4096 = 0.000610 (V)
// 常用分数表示:val / (2^val2) 或 val/val2
*val = 2500; // mV
*val2 = 12; // 分母为 2^12 = 4096
ret = IIO_VAL_FRACTIONAL_LOG2; // 表示 val / (2^val2) mV
break;
case IIO_CHAN_INFO_OFFSET:
// 偏移量,例如某些双极性ADC需要加偏移
*val = 0;
ret = IIO_VAL_INT;
break;
case IIO_CHAN_INFO_SAMP_FREQ:
ret = my_adc_get_sampling_freq(priv, val);
if (ret == 0)
ret = IIO_VAL_INT;
break;
default:
ret = -EINVAL;
}
mutex_unlock(&priv->lock);
return ret;
}
static int my_adc_write_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int val, int val2, long mask)
{
struct my_adc_priv *priv = iio_priv(indio_dev);
int ret;
mutex_lock(&priv->lock);
switch (mask) {
case IIO_CHAN_INFO_SAMP_FREQ:
ret = my_adc_set_sampling_freq(priv, val);
break;
default:
ret = -EINVAL;
}
mutex_unlock(&priv->lock);
return ret;
}
static const struct iio_info my_adc_info = {
.read_raw = my_adc_read_raw,
.write_raw = my_adc_write_raw,
};
返回值类型(由宏定义):
-
IIO_VAL_INT:返回单个整数(val)。 -
IIO_VAL_INT_PLUS_MICRO:返回val和val2,表示val + val2/1e6。 -
IIO_VAL_FRACTIONAL:返回val和val2,表示val/val2。 -
IIO_VAL_FRACTIONAL_LOG2:返回val和val2,表示val / (1 << val2),常用于ADC的scale计算。
4. 触发缓冲模式(Triggered Buffer)实现
对于需要连续、高速采样的场景,IIO 提供了缓冲模式。驱动需实现以下组件:
4.1 缓冲区初始化
在 probe 中设置:
#include <linux/iio/buffer.h>
#include <linux/iio/trigger.h>
#include <linux/iio/trigger_consumer.h>
#include <linux/iio/triggered_buffer.h>
static int my_adc_probe(struct spi_device *spi)
{
// ... 分配 iio_dev 和私有数据 ...
// 设置硬件触发缓冲区(需要定义 trigger_handler)
ret = devm_iio_triggered_buffer_setup(&spi->dev, indio_dev,
NULL,
my_adc_trigger_handler,
NULL);
if (ret)
return ret;
// ... 注册设备 ...
}
iio_triggered_buffer_setup 注册两个回调:
-
h:通常为 NULL,使用默认的iio_pollfunc_store_time来捕获触发时间戳。 -
thread:my_adc_trigger_handler,在触发发生时调用的中断下半部,负责读取数据并推入缓冲区。
4.2 触发处理函数
触发处理函数的核心任务是从 ADC 读取当前所有使能的通道,打包成缓冲区格式,然后提交给 IIO 核心。
static irqreturn_t my_adc_trigger_handler(int irq, void *p)
{
struct iio_poll_func *pf = p;
struct iio_dev *indio_dev = pf->indio_dev;
struct my_adc_priv *priv = iio_priv(indio_dev);
u16 *buf; // 假设每个通道12位,用16位存储
int i, j = 0;
int scan_elements = indio_dev->scan_bytes; // 总字节数
// 分配扫描缓冲区(通常在堆栈上或使用预先分配的缓冲区)
buf = (u16 *) ((u8 *)priv->buffer_data + pf->timestamp); // 更常见的做法是在私有数据中预分配扫描缓冲区
// 实际常用: 使用 kmalloc 或直接使用 iio 提供的扫描缓冲区
// 简便写法:使用 iio_push_to_buffers_with_timestamp,它需要扫描数据+时间戳空间
// 遍历所有使能的通道
for_each_set_bit(i, indio_dev->active_scan_mask, indio_dev->masklength) {
const struct iio_chan_spec *chan = &indio_dev->channels[i];
int ret;
ret = my_adc_read_channel(priv, chan->channel, &buf[j++]);
if (ret) {
// 处理错误,可能跳过该样本
}
}
// 将数据推送到缓冲区,同时附带时间戳
iio_push_to_buffers_with_timestamp(indio_dev, priv->buffer_data,
pf->timestamp);
// 通知 IIO 核心处理完成
iio_trigger_notify_done(indio_dev->trig);
return IRQ_HANDLED;
}
注意:
-
数据必须按照扫描掩码的顺序连续存放,无间隙。
-
需要预留空间给时间戳(通常放在数据末尾,
scan_bytes已包含时间戳空间)。 -
并发访问:
active_scan_mask在使能缓冲时由用户空间设置,触发函数运行时不可更改,因此无需额外锁(但需要保证硬件读取不被打断)。
4.3 触发器的选择
IIO 支持多种触发器:
-
硬件触发:由 ADC 的转换完成中断直接触发。
-
定时器触发 :使用
iio-trig-hrtimer提供高精度定时触发。 -
外部 GPIO 触发 :
iio-trig-sysfs或iio-trig-interrupt。
在驱动中,若 ADC 本身能提供中断,可创建一个 IIO 触发器:
static const struct iio_trigger_ops my_adc_trigger_ops = {
.set_trigger_state = my_adc_trigger_set_state,
};
static int my_adc_probe(struct spi_device *spi)
{
// ...
priv->trig = devm_iio_trigger_alloc(&spi->dev, "%s-dev%d",
indio_dev->name, iio_device_id(indio_dev));
if (!priv->trig)
return -ENOMEM;
priv->trig->ops = &my_adc_trigger_ops;
iio_trigger_set_drvdata(priv->trig, priv);
ret = devm_iio_trigger_register(&spi->dev, priv->trig);
if (ret)
return ret;
// 将触发器与 IIO 设备关联
indio_dev->trig = iio_trigger_get(priv->trig);
// ...
}
然后在中断服务程序中调用 iio_trigger_poll(priv->trig) 来启动数据采集流程。
5. 设备树绑定
IIO ADC 驱动通常需要从设备树获取硬件配置。典型绑定如下:
adc@0 {
compatible = "vendor,my-adc";
reg = <0>; // SPI片选
spi-max-frequency = <1000000>;
vref-supply = <&vref_reg>; // 参考电压 regulator
interrupts = <17 IRQ_TYPE_EDGE_RISING>;
#io-channel-cells = <1>; // 可作为 IIO 消费者提供通道
};
驱动中通过 devm_regulator_get() 获取参考电压,devm_request_irq() 注册中断。
若 ADC 有多通道,可以在设备树中指定使用的通道范围或名称,但通常直接在驱动中硬编码通道描述。
6. 高级特性与注意事项
6.1 采样频率设置
若 ADC 支持硬件采样频率调整,在 write_raw 中处理 IIO_CHAN_INFO_SAMP_FREQ,并确保在缓冲模式下生效。需要与触发器的频率协调。
6.2 DMA 支持
对于极高采样率,可使用 DMA 自动将数据搬移到缓冲区。IIO 提供了 iio_dma_buffer 相关 API,需要实现 iio_buffer_access_funcs。但这部分较复杂,通常只在高端 ADC 芯片驱动中见到。
6.3 并发与锁
-
直接模式 (
read_raw) 可能和缓冲模式同时进行,因此必须对硬件寄存器访问进行互斥保护。 -
触发处理函数运行在中断上下文,不能睡眠,不能使用可能睡眠的锁(如 mutex)。通常使用自旋锁保护硬件访问,或确保触发处理与
read_raw不会同时运行(通过标志位或禁用它)。
6.4 数据对齐与字节序
缓冲区数据必须自然对齐,用户空间读取时按相同格式解析。例如 16 位数据可能需要 2 字节对齐,通常使用 __aligned 属性或 put_unaligned 函数。
6.5 错误处理
在触发处理中,如果读取失败,应跳过该样本或填充一个错误标记(如饱和值)。更好的做法是使用 iio_push_to_buffers_with_timestamp 之前确保数据有效。
6.6 调试与验证
-
使用
iio_info工具查看设备通道和属性。 -
使用
iio_readdev从/dev/iio:deviceX读取数据流。 -
启用内核调试选项
CONFIG_IIO_DEBUG和CONFIG_IIO_TRIGGER,观察日志。
7. 完整示例代码结构
一个典型的 ADC IIO 驱动文件通常包含:
-
私有数据结构定义
-
通道描述数组
-
read_raw/write_raw实现 -
触发处理函数(若支持缓冲)
-
中断处理函数(若有硬件触发)
-
probe 和 remove(或使用 devm 自动释放)
-
设备树匹配表
-
I2C/SPI 驱动注册
通过以上详细的分解,我们可以看到一个 ADC IIO 驱动从硬件操作到内核接口的完整实现路径。掌握这些核心概念和代码模式,开发者就能根据具体芯片手册快速编写出符合 Linux 内核标准的 IIO ADC 驱动。