大山佬头像
关注

从设备树到用户态读取传感器数据:Linux IIO 子系统驱动开发全流程实战指南

从设备树到用户态读取传感器数据:Linux IIO 子系统驱动开发全流程实战指南

一、工业传感器驱动的"碎片化"之痛:为何内核需要统一的传感器框架

Linux 内核中存在大量传感器外设——加速度计、陀螺仪、ADC、温度传感器、光传感器——每种传感器在不同的 SoC 平台上都有不同的驱动实现方式。在 IIO 子系统出现之前,传感器驱动要么混在 misc 目录中,要么通过 hwmon 堆叠,要么干脆在用户态通过 spidev 直接操作寄存器。

这种碎片化直接导致了三个问题:没有统一的 sysfs 接口规范,上层应用必须适配每种传感器的特定路径;无法利用内核的触发器和环形缓冲区机制进行高速采样;电源管理(suspend/resume)行为依赖驱动的个体实现质量。

IIO(Industrial I/O)子系统自 2.6.35 内核引入后,为传感器类设备提供了统一的驱动模型。它定义了一套标准的内存映射结构(iio_dev)、通道抽象(iio_chan_spec)、触发器机制(iio_trigger)和缓冲区基础设施(iio_buffer),让加速度计驱动和 ADC 驱动在接口层面遵循相同的规范。

二、IIO 的内核架构:从硬件总线到用户态的数据通路

IIO 子系统的核心抽象围绕"通道"展开。每个通道代表物理传感器的一个测量维度——例如三轴加速度计有三个 IIO 通道(X/Y/Z),温度传感器有一个通道。通道通过 iio_chan_spec 结构体描述其类型(IIO_ACCELIIO_TEMP 等)、数据格式(有符号/无符号、位数、字节序)和索引。

flowchart TD
    subgraph Hardware["硬件层"]
        A["I2C/SPI 物理传感器<br/>(如 LIS3DH 加速度计)"]
    end

    subgraph Kernel["内核空间"]
        B["I2C/SPI 总线驱动<br/>(对应设备树节点)"] --> C["probe() 函数:<br/>分配 iio_dev<br/>注册通道和缓冲"]

        C --> D["iio_chan_spec 数组<br/>定义每个测量通道"]
        C --> E["iio_trigger<br/>(hrtimer/外部中断)"]

        D --> F["IIO 核心层<br/>(drivers/iio/industrialio-core.c)"]
        E --> G["IIO 触发器子系统<br/>(drivers/iio/industrialio-trigger.c)"]

        F --> H["sysfs 接口<br/>/sys/bus/iio/devices/iio:deviceX/"]
        F --> I["字符设备接口<br/>/dev/iio:deviceX"]

        G --> J["iio_buffer<br/>环形缓冲区"]

        J --> I
    end

    subgraph Userspace["用户空间"]
        H --> K["cat in_accel_x_raw<br/>cat in_accel_scale"]
        I --> L["libiio / ioctl()<br/>高速数据采集"]
    end

2.1 设备树绑定:描述硬件拓扑

一个 I2C 加速度计的典型设备树节点:

&i2c1 {
    status = "okay";
    lis3dh: accelerometer@19 {
        compatible = "st,lis3dh";
        reg = <0x19>;
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_EDGE_RISING>;
        st,drdy-int-pin = <1>;
        vdd-supply = <&vdd_3v3>;
    };
};

compatible 字符串 "st,lis3dh" 是内核匹配驱动的关键。当内核遍历设备树时,I2C 核心层会尝试将节点的 compatible 值与驱动中 of_match_table 的每一项比对。匹配成功后调用驱动的 probe() 函数。

2.2 通道规范:将物理量映射到数据结构

加速度计的典型通道定义:

static const struct iio_chan_spec lis3dh_channels[] = {
    {
        .type       = IIO_ACCEL,
        .modified   = 1,
        .channel2   = IIO_MOD_X,
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
        .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE)
            | BIT(IIO_CHAN_INFO_SAMP_FREQ),
        .address    = LIS3DH_REG_OUT_X_L,
        .scan_index = 0,
        .scan_type  = {
            .sign       = 's',
            .realbits   = 12,
            .storagebits = 16,
            .shift      = 4,
            .endianness = IIO_LE,
        },
    },
    /* ... Y 轴和 Z 轴类似定义 ... */
};

scan_typerealbits=12, storagebits=16 配置说明 LIS3DH 以左对齐方式在 16-bit 寄存器中存储 12-bit 采样值。shift=4 指示数据需要右移 4 位才能获得有效值。这些细节决定了最终数据是否正确——在调试中,错配这些参数是"数值看起来合理但始终偏离 2-4 倍"的常见原因。

三、生产级代码实现:完整 IIO 驱动与用户态采集示例

以下展示一个基于 I2C 的加速度计 IIO 驱动核心框架:

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/iio/iio.h>
#include <linux/iio/trigger.h>
#include <linux/iio/trigger_consumer.h>
#include <linux/iio/triggered_buffer.h>
#include <linux/iio/buffer.h>
#include <linux/of_device.h>

/* ================================================================
 * 驱动私有数据:存储设备状态
 * ================================================================ */
struct lis3dh_data {
    struct i2c_client      *client;
    struct iio_trigger     *trig;
    int                     irq;
    struct mutex            lock;    /* 保护寄存器的并发访问 */
    /* 电源管理相关:记录挂起前的寄存器状态 */
    u8                      saved_ctrl_reg1;
    u8                      saved_ctrl_reg4;
};

/* 寄存器地址定义 —— 与数据手册严格对应 */
#define LIS3DH_REG_WHO_AM_I     0x0F
#define LIS3DH_REG_CTRL_REG1    0x20
#define LIS3DH_REG_CTRL_REG4    0x23
#define LIS3DH_REG_OUT_X_L      0x28
#define LIS3DH_REG_OUT_X_H      0x29
#define LIS3DH_REG_OUT_Y_L      0x2A
#define LIS3DH_REG_OUT_Y_H      0x2B
#define LIS3DH_REG_OUT_Z_L      0x2C
#define LIS3DH_REG_OUT_Z_H      0x2D

#define LIS3DH_WHO_AM_I_VALUE   0x33

/* ================================================================
 * 寄存器读写辅助函数
 * 返回值检查:所有 I2C 操作都可能在总线故障时返回负值
 * ================================================================ */
static int lis3dh_read_reg(struct lis3dh_data *data, u8 reg, u8 *val)
{
    int ret = i2c_smbus_read_byte_data(data->client, reg);
    if (ret < 0) {
        dev_err(&data->client->dev,
                "I2C 读取寄存器 0x%02X 失败: %d\n", reg, ret);
        return ret;
    }
    *val = (u8)ret;
    return 0;
}

static int lis3dh_write_reg(struct lis3dh_data *data, u8 reg, u8 val)
{
    int ret = i2c_smbus_write_byte_data(data->client, reg, val);
    if (ret < 0) {
        dev_err(&data->client->dev,
                "I2C 写入寄存器 0x%02X 失败: %d\n", reg, ret);
    }
    return ret;
}

/* ================================================================
 * IIO raw 数据读取
 * 从硬件寄存器读取三轴原始值
 * ================================================================ */
static int lis3dh_read_raw(struct iio_dev *indio_dev,
                           struct iio_chan_spec const *chan,
                           int *val, int *val2, long mask)
{
    struct lis3dh_data *data = iio_priv(indio_dev);
    int ret;

    switch (mask) {
    case IIO_CHAN_INFO_RAW: {
        /* 读取 16-bit 加速度数据(低字节 + 高字节) */
        u8 reg_low = chan->address;
        u8 reg_high = chan->address + 1;
        u8 buf[2];

        mutex_lock(&data->lock);

        ret = lis3dh_read_reg(data, reg_low, &buf[0]);
        if (ret) goto out_unlock;

        ret = lis3dh_read_reg(data, reg_high, &buf[1]);
        if (ret) goto out_unlock;

        /* 组合为有符号 16-bit 值 */
        s16 raw = (s16)((buf[1] << 8) | buf[0]);

        /* 应用 scan_type 中的位移(左对齐 → 右移) */
        raw >>= chan->scan_type.shift;
        *val = (int)raw;

        ret = IIO_VAL_INT;
out_unlock:
        mutex_unlock(&data->lock);
        return ret;
    }

    case IIO_CHAN_INFO_SCALE: {
        /* LIS3DH ±2g 量程:1g = 约 16000 LSB
         * scale = 2g / 16000 = 0.000125 g/LSB
         * IIO_VAL_INT_PLUS_NANO 表示 val + val2 * 1e-9 */
        *val  = 0;
        *val2 = 122070;  /* 0.000125 * 1e9 的近似值 */
        return IIO_VAL_INT_PLUS_NANO;
    }

    case IIO_CHAN_INFO_SAMP_FREQ: {
        /* 读取 CTRL_REG1 的数据速率位,映射到实际频率 */
        u8 ctrl1;
        ret = lis3dh_read_reg(data, LIS3DH_REG_CTRL_REG1, &ctrl1);
        if (ret) return ret;

        /* ODR 编码在 bit4-7,1Hz 为 0x10, 10Hz 为 0x20 */
        int odr_bits = (ctrl1 >> 4) & 0x0F;
        switch (odr_bits) {
        case 0x01: *val = 1;   break;
        case 0x02: *val = 10;  break;
        case 0x03: *val = 25;  break;
        case 0x04: *val = 50;  break;
        case 0x05: *val = 100; break;
        case 0x06: *val = 200; break;
        case 0x07: *val = 400; break;
        default:   *val = 0;   break;
        }
        return IIO_VAL_INT;
    }

    default:
        return -EINVAL;
    }
}

/* ================================================================
 * IIO 通道定义
 * ================================================================ */
#define LIS3DH_CHANNEL(axis, reg)                             \
    {                                                         \
        .type       = IIO_ACCEL,                              \
        .modified   = 1,                                      \
        .channel2   = IIO_MOD_##axis,                         \
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),         \
        .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \
        .address    = reg,                                    \
        .scan_index = LIS3DH_SCAN_##axis,                     \
        .scan_type  = {                                       \
            .sign       = 's',                                \
            .realbits   = 12,                                 \
            .storagebits = 16,                                \
            .shift      = 4,                                  \
            .endianness = IIO_LE,                             \
        },                                                    \
    }

enum { LIS3DH_SCAN_X, LIS3DH_SCAN_Y, LIS3DH_SCAN_Z };

static const struct iio_chan_spec lis3dh_channels[] = {
    LIS3DH_CHANNEL(X, LIS3DH_REG_OUT_X_L),
    LIS3DH_CHANNEL(Y, LIS3DH_REG_OUT_Y_L),
    LIS3DH_CHANNEL(Z, LIS3DH_REG_OUT_Z_L),
    IIO_CHAN_SOFT_TIMESTAMP(3),  /* 软件时间戳通道 */
};

/* ================================================================
 * Probe 函数:驱动的入口点
 * ================================================================ */
static int lis3dh_probe(struct i2c_client *client)
{
    struct lis3dh_data *data;
    struct iio_dev *indio_dev;
    int ret;

    /* 验证设备连通性 */
    ret = i2c_smbus_read_byte_data(client, LIS3DH_REG_WHO_AM_I);
    if (ret < 0) {
        dev_err(&client->dev, "WHO_AM_I 读取失败\n");
        return -ENODEV;
    }
    if (ret != LIS3DH_WHO_AM_I_VALUE) {
        dev_err(&client->dev, "设备 ID 不匹配: 期望 0x%02X, 实际 0x%02X\n",
                LIS3DH_WHO_AM_I_VALUE, ret);
        return -ENODEV;
    }

    /* 分配 IIO 设备 */
    indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
    if (!indio_dev)
        return -ENOMEM;

    data = iio_priv(indio_dev);
    data->client = client;
    mutex_init(&data->lock);

    /* 配置 IIO 设备 */
    indio_dev->name = "lis3dh";
    indio_dev->modes = INDIO_DIRECT_MODE;
    indio_dev->channels = lis3dh_channels;
    indio_dev->num_channels = ARRAY_SIZE(lis3dh_channels);
    indio_dev->info = &lis3dh_iio_info;

    /* 注册设备 */
    ret = devm_iio_device_register(&client->dev, indio_dev);
    if (ret) {
        dev_err(&client->dev, "IIO 设备注册失败: %d\n", ret);
        return ret;
    }

    i2c_set_clientdata(client, indio_dev);
    dev_info(&client->dev, "LIS3DH 加速度计初始化完成\n");
    return 0;
}

/* ================================================================
 * 设备树匹配表 —— 支持多个兼容型号
 * ================================================================ */
static const struct of_device_id lis3dh_of_match[] = {
    { .compatible = "st,lis3dh" },
    { .compatible = "st,lis2dh12" },
    { }
};
MODULE_DEVICE_TABLE(of, lis3dh_of_match);

/* I2C 驱动结构体 */
static struct i2c_driver lis3dh_driver = {
    .driver = {
        .name   = "lis3dh",
        .of_match_table = lis3dh_of_match,
    },
    .probe  = lis3dh_probe,
};
module_i2c_driver(lis3dh_driver);

MODULE_AUTHOR("IIO Driver Example");
MODULE_DESCRIPTION("LIS3DH accelerometer IIO driver");
MODULE_LICENSE("GPL");

对应的用户态数据采集示例(使用 libiio 库):

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <iio.h>

static volatile int stop = 0;

void sigint_handler(int sig) { stop = 1; }

int main(void)
{
    struct iio_context *ctx;
    struct iio_device *dev;
    struct iio_channel *ch_x, *ch_y, *ch_z;
    struct iio_buffer *buf;
    ssize_t ret;

    /* 创建本地 IIO 上下文 —— "local" 表示通过 sysfs 访问 */
    ctx = iio_create_local_context();
    if (!ctx) {
        fprintf(stderr, "无法创建 IIO 上下文\n");
        return EXIT_FAILURE;
    }

    /* 查找设备 */
    dev = iio_context_find_device(ctx, "lis3dh");
    if (!dev) {
        fprintf(stderr, "未找到 lis3dh 设备\n");
        iio_context_destroy(ctx);
        return EXIT_FAILURE;
    }

    /* 创建缓冲区 */
    buf = iio_device_create_buffer(dev, 256, false);
    if (!buf) {
        fprintf(stderr, "缓冲区创建失败\n");
        iio_context_destroy(ctx);
        return EXIT_FAILURE;
    }

    signal(SIGINT, sigint_handler);

    /* 持续读取数据 */
    while (!stop) {
        ret = iio_buffer_refill(buf);
        if (ret < 0) {
            fprintf(stderr, "缓冲区读取失败: %zd\n", ret);
            break;
        }

        /* 解析缓冲数据 —— 每个样本包含三轴 + 时间戳 */
        void *start = iio_buffer_start(buf);
        ptrdiff_t step = iio_buffer_step(buf);

        for (int i = 0; i < ret / step; i++) {
            uint8_t *sample = (uint8_t *)start + i * step;
            int16_t x = *(int16_t *)(sample + 0);
            int16_t y = *(int16_t *)(sample + 2);
            int16_t z = *(int16_t *)(sample + 4);

            printf("Accel: X=%6d Y=%6d Z=%6d\n", x, y, z);
        }
    }

    iio_buffer_destroy(buf);
    iio_context_destroy(ctx);
    return 0;
}

四、边界分析与架构权衡:IIO 子系统不适合的场景

IIO 子系统的抽象层概念增加了额外的代码路径和内存开销。对于一个简单的温度传感器(只有 1 个通道,1Hz 采样率),完整的 IIO 驱动可能有 200+ 行代码,而通过 hwmon 框架可能只需要 80 行。在极端受限的内核配置中(如小内存 IoT 网关),应评估 IIO 框架的内存开销是否可接受。

另一个要警惕的陷阱是 IIO 缓冲区的内存分配。当使用 iio_triggered_buffer 时,内核会为每个通道分配缓冲区空间。在高速采样场景(如 10kHz 三轴加速度计),watermark=256 意味着每个触发周期需要处理 256 个样本——如果处理不及时,缓冲区溢出会导致数据丢失,且 IIO 框架本身不提供丢包计数器。

电源管理也是脆弱的环节。IIO 驱动必须显式实现 suspend/resume 回调来正确恢复传感器状态。如果挂起期间传感器的电源被切断,恢复后必须重新执行完整的初始化序列——包括设置采样率、量程和中断使能。忽略这一步会导致"挂起后采样率变成默认值"类型的隐性 bug。

五、总结

IIO 子系统为 Linux 内核中的传感器驱动提供了标准化的开发范式。在实现一个 IIO 驱动时,以下步骤应作为检查清单:

  1. 定义 iio_chan_spec 时,仔细核对 realbitsstoragebitsshift 与数据手册一致
  2. 使用设备树描述硬件连接关系(总线、中断、电源),避免在驱动中硬编码寄存器地址
  3. 通过 mutex 保护寄存器的并发访问——I2C/SPI 总线可能在多线程场景下共享
  4. 用户态优先使用 libiio 而非直接操作 sysfs,以获得缓冲区支持和触发同步
  5. probe() 中验证 WHO_AM_I 等标志寄存器,在硬件故障时提供明确错误信息
  6. 实现 suspend/resume 回调时,保存并恢复所有配置寄存器的值,而非依赖默认值

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_42431428/article/details/162653014

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--