Linux IIO ADC 驱动简介

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_VOLTAGEIIO_TEMPIIO_CURRENT 等。

  • indexed :若为1,则通道号由 channel 指定;若为0,则全局唯一(适用于只有一个通道的设备)。

  • info_mask_separate :每个通道独立的属性掩码。例如 IIO_CHAN_INFO_RAW 表示原始值,IIO_CHAN_INFO_SCALE 表示转换系数。用户空间将看到 in_voltage0_rawin_voltage0_scale 等文件。

  • info_mask_shared_by_type :同类型通道共享的属性,例如采样频率对所有电压通道通用,将生成 in_voltage_sampling_frequency

  • datasheet_name:可选,用于调试或匹配硬件通道名。


3. 核心操作函数集:struct iio_info

iio_info 中的 read_rawwrite_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:返回 valval2,表示 val + val2/1e6

  • IIO_VAL_FRACTIONAL:返回 valval2,表示 val/val2

  • IIO_VAL_FRACTIONAL_LOG2:返回 valval2,表示 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 来捕获触发时间戳。

  • threadmy_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-sysfsiio-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_DEBUGCONFIG_IIO_TRIGGER,观察日志。


7. 完整示例代码结构

一个典型的 ADC IIO 驱动文件通常包含:

  • 私有数据结构定义

  • 通道描述数组

  • read_raw / write_raw 实现

  • 触发处理函数(若支持缓冲)

  • 中断处理函数(若有硬件触发)

  • probe 和 remove(或使用 devm 自动释放)

  • 设备树匹配表

  • I2C/SPI 驱动注册


通过以上详细的分解,我们可以看到一个 ADC IIO 驱动从硬件操作到内核接口的完整实现路径。掌握这些核心概念和代码模式,开发者就能根据具体芯片手册快速编写出符合 Linux 内核标准的 IIO ADC 驱动。

相关推荐
大母猴啃编程1 小时前
Socket编程UDP
linux·网络·c++·网络协议·udp
悟凡爱学习2 小时前
Linux 操作系统&消息队列
linux·运维·服务器
i建模2 小时前
Ubuntu增加安装桌面环境
linux·运维·ubuntu
嵌入式×边缘AI:打怪升级日志2 小时前
2.3.2 目录与文件操作命令(保姆级详解)
linux·运维·服务器
艾莉丝努力练剑2 小时前
MySQL查看命令速查表
linux·运维·服务器·网络·数据库·人工智能·mysql
皮皮哎哟2 小时前
Linux网络最终篇:TCP并发服务器
linux·服务器·select·epoll·poll·tcp并发
无心水2 小时前
【OpenClaw:进阶开发】11、OpenClaw插件开发入门——从零编写“文件统计与报表生成”Skill
linux·运维·ubuntu
sbjdhjd2 小时前
RHCE | Linux 例行性工作(定时任务)从入门到精通
linux·运维·服务器·华为·云计算
枷锁—sha2 小时前
【CTFshow-pwn系列】03_栈溢出【pwn 056-057】详解:32位 与64位Shellcode 与 Linux 系统调用底层原理剖析
linux·运维·网络·笔记·安全·网络安全·系统安全