---TITLE---
Linux IIO驱动开发解析:ADC传感器驱动原理与代码实现
---TAGS---
Linux,IIO,ADC,I2C,驱动开发
---CONTENT---
当需要为ADC(模数转换器)、加速度计、陀螺仪、光线传感器或气压计等工业级传感器编写驱动程序时,传统的字符设备或输入子系统模型往往不再适用。此时,Linux内核提供的IIO(Industrial I/O)子系统便成为理想选择。它提供了统一的Sysfs接口,无论ADC芯片型号如何,用户都可以通过标准的cat in_voltage0_raw命令读取电压值。IIO子系统能够自动处理原始寄存器数值到标准物理单位(如毫伏、米/秒平方)的转换(通过Scale比例因子和Offset偏移量),并且支持硬件FIFO和DMA,可高效处理数千赫兹的高速采样数据。
在IIO驱动模型中,开发者不再直接实现read函数来返回字节流,而是需要定义“通道”。每个物理引脚或测量轴都对应一个通道,例如in_voltage0(通道0电压)、in_accel_x(X轴加速度)、in_temp(温度)。用户空间最终会看到两个主要文件:_raw(寄存器的原始值)和_scale(比例因子)。内核已定义好这套标准,用户空间的应用(如libiio)会自动根据这些文件完成计算。
下面以一个简单的I2C接口ADC芯片为例,说明IIO驱动的基本编写步骤。
步骤一:定义通道
首先,需要定义设备所拥有的通道。这段代码会在/sys/bus/iio/devices/下自动生成对应的接口文件,例如in_voltage0_raw、in_voltage1_raw和in_voltage_scale。
#include <linux/iio/iio.h>
static const struct iio_chan_spec my_adc_channels[] = {
{
.type = IIO_VOLTAGE, // 通道类型:电压
.channel = 0, // 通道编号:0
.indexed = 1, // 这是一个有索引的通道 (会生成voltage0)
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW), // 每个通道都有独立的`_raw`文件
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), // 同类型通道共享一个`_scale`文件
},
{
.type = IIO_VOLTAGE,
.channel = 1,
.indexed = 1,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
};
步骤二:实现读取函数
这是驱动的核心,read_raw回调函数用于响应用户空间对_raw和_scale等文件的读取操作。精通Linux驱动与I2C总线开发是完成这一步的关键。
struct my_adc_dev {
struct i2c_client *client;
};
static int my_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
struct my_adc_dev *data = iio_priv(indio_dev);
int ret;
switch (mask) {
case IIO_CHAN_INFO_RAW: // 用户读取 in_voltageX_raw 时进入此分支
// 1. 根据 chan->channel 判断用户要读取哪个硬件通道
// 2. 通过I2C发送命令,读取硬件寄存器
ret = i2c_smbus_read_word_data(data->client, chan->channel);
if (ret < 0)
return ret;
*val = ret; // 将读取到的原始值赋值给*val
return IIO_VAL_INT; // 告知内核返回值是一个整数
case IIO_CHAN_INFO_SCALE: // 用户读取 in_voltage_scale 时进入此分支
// 假设参考电压为3300mV,ADC精度为12位(最大值4096)
// 换算公式:真实电压 = raw * (3300 / 4096) ≈ raw * 0.80566
// 此处返回分数形式 3300 / 2^12
*val = 3300; // 分子
*val2 = 12; // 分母以2为底的对数(即2^12=4096)
return IIO_VAL_FRACTIONAL_LOG2; // 返回类型:分数对数形式
default:
return -EINVAL;
}
}
static const struct iio_info my_adc_info = {
.read_raw = my_read_raw, // 挂载核心回调函数
};
步骤三:Probe初始化
在设备的probe函数中,完成IIO设备的初始化与注册。
static int my_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct iio_dev *indio_dev;
struct my_adc_dev *data;
/* 1. 分配IIO设备结构体,并为其私有数据预留空间 */
indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
if (!indio_dev)
return -ENOMEM;
/* 2. 获取私有数据结构的指针 */
data = iio_priv(indio_dev);
data->client = client;
/* 3. 填充IIO设备的核心结构体 */
indio_dev->name = "my_adc_demo";
indio_dev->info = &my_adc_info; // 设置操作函数集
indio_dev->modes = INDIO_DIRECT_MODE; // 设置为直接读取模式
indio_dev->channels = my_adc_channels; // 设置通道数组
indio_dev->num_channels = ARRAY_SIZE(my_adc_channels);
/* 4. 向内核注册IIO设备 */
return devm_iio_device_register(&client->dev, indio_dev);
}
用户空间访问模式
IIO驱动注册成功后,用户空间主要有两种访问方式:
- Sysfs模式:通过
cat、echo命令直接读写/sys/bus/iio/devices/下的文件。这种方式简单,但速度慢,适用于低速、单次或低频次查询。
- Buffer模式:启用IIO缓冲区和触发机制,可以实现高速、连续的数据采集。数据通过
/dev/iio:deviceX字符设备节点读取,这涉及到DMA和内核缓存机制,适合处理实时流数据。