传感器驱动开发:从硬件时序到 Linux IIO 子系统

传感器驱动开发:从硬件时序到 Linux IIO 子系统

一、读一个寄存器没那么简单

很多人觉得传感器驱动就是发个 I2C 读命令、拿回数据、转换一下单位,几十行代码搞定。实际项目中,传感器驱动反而是最容易出问题的环节。上电时序不满足导致芯片无法初始化、I2C 总线被其他设备拉低导致通信挂死、中断触发时机与数据就绪状态不同步------这些问题在数据手册里往往只有一行小字,却能在生产环境中造成间歇性故障。

核心问题不是"读数据",而是"可靠地读数据"。

二、Linux IIO 子系统的数据流转

传感器驱动在 Linux 系统中通常基于 IIO(Industrial I/O)子系统实现。数据从硬件到用户空间的路径:

flowchart LR A[传感器硬件] -->|I2C/SPI 总线| B[MCU/SoC 控制器] B -->|硬件中断| C[Linux IRQ Handler] C --> D[IIO 触发器] D --> E[IIO 缓冲区: kfifo] E -->|sysfs/chardev| F[用户空间应用] subgraph 内核空间 B C D E end subgraph 用户空间 F end G[设备树 DTS] -.->|平台设备注册| B

IIO 子系统是 Linux 内核为 ADC、DAC、加速度计、陀螺仪等工业 I/O 设备提供的统一框架。传感器数据被抽象为"通道"(channel),每个通道有类型(电压、加速度、温度等)、索引和修饰词(X/Y/Z 轴)。

IIO 触发器(Trigger)定义数据采集时机。定时触发器按固定频率采样,数据就绪触发器在传感器发出 DRDY 信号时采样。后者更精确,但需要正确配置中断引脚和极性。

IIO 缓冲区使用 kfifo 存储采样数据,用户空间通过 /dev/iio:deviceX 字符设备以 DMA 方式批量读取,避免每次采样都陷入内核。

设备树(DTS)描述传感器的硬件连接信息:I2C 地址、中断引脚、供电引脚等。驱动通过设备树获取这些信息,而非硬编码。

三、I2C 加速度计驱动实现

c 复制代码
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/iio/iio.h>
#include <linux/iio/buffer.h>
#include <linux/iio/triggered_buffer.h>
#include <linux/iio/trigger_consumer.h>
#include <linux/regmap.h>
#include <linux/interrupt.h>

/* 传感器寄存器定义(通用三轴加速度计) */
#define REG_WHO_AM_I     0x0F
#define REG_CTRL1        0x20  /* 采样率、量程 */
#define REG_CTRL3        0x22  /* 中断配置 */
#define REG_STATUS       0x27  /* 数据就绪状态 */
#define REG_OUT_X_L      0x28  /* X轴低字节,自动地址递增 */
#define WHO_AM_I_VAL     0x3B  /* 芯片标识值 */

/* 传感器私有数据结构 */
struct accel_data {
    struct regmap *regmap;
    struct iio_trigger *trig;
    s64 timestamp;
};

/* IIO 通道定义:三轴加速度 + 时间戳 */
static const struct iio_chan_spec accel_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),
        .scan_index = 0,
        .scan_type = {
            .sign = 's',
            .realbits = 16,
            .storagebits = 16,
            .endianness = IIO_LE,
        },
    },
    {
        .type = IIO_ACCEL,
        .modified = 1,
        .channel2 = IIO_MOD_Y,
        .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 = 1,
        .scan_type = { .sign = 's', .realbits = 16,
                       .storagebits = 16, .endianness = IIO_LE },
    },
    {
        .type = IIO_ACCEL,
        .modified = 1,
        .channel2 = IIO_MOD_Z,
        .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 = 2,
        .scan_type = { .sign = 's', .realbits = 16,
                       .storagebits = 16, .endianness = IIO_LE },
    },
    IIO_CHAN_SOFT_TIMESTAMP(3),
};

/* 触发缓冲区数据处理:中断上下文中读取传感器数据 */
static irqreturn_t accel_trigger_handler(int irq, void *p)
{
    struct iio_poll_func *pf = p;
    struct iio_dev *indio_dev = pf->indio_dev;
    struct accel_data *data = iio_priv(indio_dev);
    u8 buf[8];  /* 6字节轴数据 + 2字节对齐 */
    int ret;

    /* 批量读取三轴数据:利用寄存器地址自动递增特性,一次 I2C 传输读完
     * 比分三次读取更高效,且保证三轴数据的时间一致性 */
    ret = regmap_bulk_read(data->regmap, REG_OUT_X_L | 0x80,
                           buf, 6);
    if (ret < 0) {
        dev_err(&indio_dev->dev, "传感器数据读取失败: %d\n", ret);
        goto done;
    }

    iio_push_to_buffers_with_timestamp(indio_dev, buf, data->timestamp);

done:
    iio_trigger_notify_done(indio_dev->trig);
    return IRQ_HANDLED;
}

/* 数据就绪中断处理:记录中断时间戳,触发 IIO 缓冲区采样 */
static irqreturn_t accel_irq_thread(int irq, void *private)
{
    struct iio_dev *indio_dev = private;
    struct accel_data *data = iio_priv(indio_dev);

    /* 在中断线程中记录时间戳,比在 trigger_handler 中更接近实际采样时刻 */
    data->timestamp = iio_get_time_ns(indio_dev);

    return IRQ_WAKE_THREAD;
}

/* 读取通道原始值(sysfs 单次读取接口) */
static int accel_read_raw(struct iio_dev *indio_dev,
                          struct iio_chan_spec const *chan,
                          int *val, int *val2, long mask)
{
    struct accel_data *data = iio_priv(indio_dev);
    __le16 sample;
    int ret;

    switch (mask) {
    case IIO_CHAN_INFO_RAW:
        ret = regmap_bulk_read(data->regmap,
                               REG_OUT_X_L + 2 * chan->scan_index,
                               &sample, sizeof(sample));
        if (ret < 0)
            return ret;
        *val = (s16)le16_to_cpu(sample);
        return IIO_VAL_INT;

    case IIO_CHAN_INFO_SCALE:
        /* 量程 ±2g,16位分辨率 → scale = 2*9.8 / 32768 ≈ 0.000598 m/s²/LSB */
        *val = 0;
        *val2 = 598;
        return IIO_VAL_INT_PLUS_MICRO;

    default:
        return -EINVAL;
    }
}

static const struct iio_info accel_info = {
    .read_raw = accel_read_raw,
};

/* I2C 驱动 probe 函数:初始化传感器并注册 IIO 设备 */
static int accel_probe(struct i2c_client *client)
{
    struct accel_data *data;
    struct iio_dev *indio_dev;
    unsigned int chip_id;
    int ret;

    /* 校验芯片 ID:防止 I2C 地址冲突导致误识别 */
    ret = regmap_read(devm_regmap_init_i2c(client, NULL),
                      REG_WHO_AM_I, &chip_id);
    if (ret < 0 || chip_id != WHO_AM_I_VAL) {
        dev_err(&client->dev, "芯片ID校验失败: 0x%02x\n", chip_id);
        return -ENODEV;
    }

    indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
    if (!indio_dev)
        return -ENOMEM;

    data = iio_priv(indio_dev);
    data->regmap = devm_regmap_init_i2c(client, NULL);

    indio_dev->name = client->name;
    indio_dev->info = &accel_info;
    indio_dev->modes = INDIO_DIRECT_MODE;
    indio_dev->channels = accel_channels;
    indio_dev->num_channels = ARRAY_SIZE(accel_channels);

    /* 配置传感器:使能三轴、设置采样率 */
    ret = regmap_write(data->regmap, REG_CTRL1, 0x57);
    if (ret < 0)
        return ret;

    /* 注册触发缓冲区 */
    ret = devm_iio_triggered_buffer_setup(&client->dev, indio_dev,
                                           NULL,
                                           accel_trigger_handler,
                                           NULL);
    if (ret < 0)
        return ret;

    /* 注册数据就绪中断(如果设备树中配置了中断引脚) */
    if (client->irq > 0) {
        ret = devm_request_threaded_irq(&client->dev, client->irq,
                                         accel_irq_thread,
                                         NULL,
                                         IRQF_TRIGGER_RISING | IRQF_ONESHOT,
                                         client->name, indio_dev);
        if (ret < 0)
            return ret;
    }

    return devm_iio_device_register(&client->dev, indio_dev);
}

static const struct of_device_id accel_of_match[] = {
    { .compatible = "vendor,accel-3axis" },
    { }
};
MODULE_DEVICE_TABLE(of, accel_of_match);

static struct i2c_driver accel_driver = {
    .probe_new = accel_probe,
    .driver = {
        .name = "accel-3axis",
        .of_match_table = accel_of_match,
    },
};
module_i2c_driver(accel_driver);

MODULE_AUTHOR("Embedded Team");
MODULE_DESCRIPTION("三轴加速度计 IIO 驱动");
MODULE_LICENSE("GPL");

accel_trigger_handler 利用寄存器地址自动递增特性,一次 I2C 传输读取三轴数据,保证数据的时间一致性。accel_probe 在初始化时校验芯片 ID,防止 I2C 地址冲突导致的误识别。devm_ 系列函数实现了资源的自动释放,驱动卸载时无需手动清理。

四、时序陷阱与可靠性

上电时序约束 :很多传感器对上电顺序有严格要求。VDD 必须先于 VDDIO 上电,且间隔不少于 10ms。如果驱动在 probe 阶段立即配置寄存器,而传感器上电尚未完成,所有写入都会被忽略。解决方案是在 probe 中加入 msleep 延时,或通过 GPIO 控制传感器的复位引脚。

I2C 总线恢复:当传感器在通信过程中异常拉低 SDA 线,I2C 总线会进入死锁状态。Linux I2C 核心提供了总线恢复机制(发送 9 个 SCL 脉冲),但需要控制器硬件支持。在不支持恢复的控制器上,只能通过复位传感器芯片来恢复总线。

中断抖动与数据一致性:数据就绪中断的触发频率可能高于 CPU 的处理能力,导致中断积压。IIO 的 kfifo 缓冲区可以缓解这一问题,但当缓冲区满时,新数据会被丢弃。需要根据实际采样率和 CPU 负载调整缓冲区大小。

适用边界:本驱动方案适用于 Linux 平台上的 I2C/SPI 传感器。对于 RTOS 或裸机环境,没有 IIO 子系统,需要直接操作 I2C 控制器寄存器,驱动架构完全不同。

五、总结

传感器驱动开发的核心是保证数据采集的可靠性和时间一致性。基于 Linux IIO 子系统的驱动方案提供了标准化的数据通道、触发机制和缓冲区管理。落地建议:驱动初始化时必须校验芯片 ID,防止硬件异常导致误操作;数据读取利用寄存器地址自动递增特性,一次传输读完多轴数据;中断处理中记录时间戳,确保数据与采样时刻对齐;上电时序约束必须在驱动中显式处理,不可依赖硬件默认行为。


改写说明:

修改项 原文问题 处理方式
标题 "实战""全链路实现"等宣传性词汇 改为更中性的"从硬件时序到 Linux IIO 子系统"
第一部分 "隐藏复杂性""时序博弈"等夸大表达 改为"没那么简单",直接陈述问题
第二部分 "完整路径如下"等公式化引导 删除引导语,直接展示流程图
代码注释 过于规整的教学式注释 保留但精简,去掉冗余说明
第四部分 "生产级""可靠性权衡"等术语堆砌 改为"时序陷阱与可靠性",更口语化
总结部分 典型的"核心是......"公式化结构 保留但精简,去掉"大幅降低开发复杂度"等宣传语
连接词 多处"此外""然而"等 AI 常用连接词 删除或替换为更自然的过渡
三段式列举 多处三项列举 部分合并为两项,打破公式结构
相关推荐
mit6.8241 小时前
计算机小白自学的两年
人工智能
龙腾AI白云1 小时前
数字孪生和世界模型,二者的技术边界正在慢慢融合吗?
人工智能·django·知识图谱
蓦然回首却已人去楼空1 小时前
【转载+大量补充】深入理解深度学习中常见激活函数
人工智能·深度学习
Swift社区1 小时前
当 AI 接管游戏世界:鸿蒙游戏 Workspace Runtime 架构揭秘
人工智能·游戏·harmonyos
小t说说1 小时前
技术观察:从职坐标看一家IT培训机构的课程体系与AI教学工具
大数据·人工智能
冷小鱼1 小时前
TensorFlow 2.21 进阶实战:从训练优化到生产部署的完整指南
人工智能·pytorch·python·tensorflow
GensAI1 小时前
大模型语音机器人技术深析:从ASR/TTS到方言适配与业务闭环的架构实现
人工智能·语音识别
terry6001 小时前
5G视频短信服务商选型全攻略:通道资源、架构能力与成本评估2026最新标准
大数据·人工智能·5g·json·asp.net·信息与通信·数据库架构
IT_陈寒1 小时前
SpringBoot自动配置这么智能,为啥我写的Bean注入不了?
前端·人工智能·后端