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

一、读一个寄存器没那么简单
很多人觉得传感器驱动就是发个 I2C 读命令、拿回数据、转换一下单位,几十行代码搞定。实际项目中,传感器驱动反而是最容易出问题的环节。上电时序不满足导致芯片无法初始化、I2C 总线被其他设备拉低导致通信挂死、中断触发时机与数据就绪状态不同步------这些问题在数据手册里往往只有一行小字,却能在生产环境中造成间歇性故障。
核心问题不是"读数据",而是"可靠地读数据"。
二、Linux IIO 子系统的数据流转
传感器驱动在 Linux 系统中通常基于 IIO(Industrial I/O)子系统实现。数据从硬件到用户空间的路径:
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 常用连接词 | 删除或替换为更自然的过渡 |
| 三段式列举 | 多处三项列举 | 部分合并为两项,打破公式结构 |