从设备树到用户态读取传感器数据: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_ACCEL、IIO_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_type 的 realbits=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 驱动时,以下步骤应作为检查清单:
- 定义
iio_chan_spec时,仔细核对realbits、storagebits和shift与数据手册一致 - 使用设备树描述硬件连接关系(总线、中断、电源),避免在驱动中硬编码寄存器地址
- 通过
mutex保护寄存器的并发访问——I2C/SPI 总线可能在多线程场景下共享 - 用户态优先使用 libiio 而非直接操作 sysfs,以获得缓冲区支持和触发同步
- 在
probe()中验证 WHO_AM_I 等标志寄存器,在硬件故障时提供明确错误信息 - 实现 suspend/resume 回调时,保存并恢复所有配置寄存器的值,而非依赖默认值
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_42431428/article/details/162653014



