工业IO(Industrial I/O)是专用于ADC和DAC的内核子系统,加速度计、陀螺仪、电流电压测量芯片、光传感器、压力传感器等都属于IIO系列设备。
IIO模型采用设备和通道架构。其中设备属于芯片本身,通道则表示设备的单个采集线,设备可能有若干个通道。例如加速度计就有3个通道,每个轴(X、Y和Z)都有一个通道。
IIO设备和用户空间交互有两种方式:
- /sys/bus/iio/iio:deviceX/,代表传感器及其通道
- /dev/iio:deviceX,字符设备,用于输出设备事件和数据缓冲区
设备驱动程序使用IIO内核提供的功能和API来管理设备,并向IIO内核报告处理情况;IIO内核通过sysfs和字符设备将底层机制抽象到用户空间。
一、IIO数据结构
IIO设备在内核中表示为struct iio_dev{},并由struct iio_info{}结构描述。IIO通道则由struc iio_chan_spec{}表示。
1. 设备结构iio_dev
结构体struct iio_dev{}的定义:
struct iio_dev {
[...]
int modes;
/* 支持不同的模式:
INDIO_DIRECT_MODE: 设备提供/sysfs类型的接口
INDIO_BUFFER_TRIGGERED: 设备支持硬件触发,当使用iio_triggered_buffer_setup()时函数设置缓冲区时,该模式会自动添加到设备;
INDIO_BUFFER_HARDWARE: 该设备支持硬件缓冲区;
INDIO_ALL_BUFFER_MODES: 上述两种模式的组合
*/
struct device dev;
int scan_bytes; // 捕捉并提供给缓冲区的字节数。
const ulong *available_scan_mask;
/*
用来标志那个通道可以启用缓冲,例如三通道设备允许掩码0x7(0b111)和0(0b000),这意味着可以不启用和全部启动,例如不能只启用X和Y,
static const ulong my_scan_masks[] = {0x7, 0x0};
indio_dev->available_scan_masks = my_scan_masks;
*/
const ulong *active_scan_mask;
/* 已启用的通道掩码,只有这些通道的数据才被推入buffer,例如对于8通道的ADC转换器,如果仅启用第一(0)、第三(2)和最后一个(7)通道,则位掩码我0b10000101(0x85),active_scan_mask设置为0x85,然后驱动程序可以使用for_each_set_bit宏遍历每个设备位,获取数据并放入缓冲区中。
*/
bool scan_timestamp; // 是否将捕获时间推入缓冲区,缓冲区作为最后一个元素推入buffer,时间戳是8字节长。
struct iio_trigger *trig; // 当前设备的触发器
struct iio_poll_fun *pollfunc; // 在接收的触发器上运行的函数
struct iio_chan_spec *channels; // 设备通道
int num_channels;
const char *name;
const struct iio_info *info; // 驱动程序的回调和常量信息
const struct iio_buffer_setup_ops *setup_ops; // 用户启用、禁用缓冲区之前和之后的一组函数,如果未指定则使用iio内核默认的函数iio_triggered_buffer_setup_ops
struct cdev chrdev;
};
// 其中结构体struct iio_buffer_setup_ops定义如下:
struct iio_buffer_setup_ops {
int (*preenable)(struct iio_dev *); // enable缓冲之前的回调函数
int (*postenable)(struct iio_dev *); // enable缓冲之后的回调函数
int (*predisable)(struct iio_dev *); // disable缓冲之前的回调函数
int (*postdisable)(struct iio_dev *); // disable缓冲之后的回调函数
int (*validate_scan_mask)(struct iio_dev *indio_dev, const ulong *scan_mask);
};
用于结构体struct iio_dev{}的操作函数:
// iio_dev分配函数
struct iio_dev *iio_device_alloc(int sizeof_priv);
struct iio_dev *devm_iio_device_alloc(int sizeof_priv); // devres版本的alloc函数
void iio_device_free(struct iio_dev);
// iio_dev的注册和注销
int iio_device_register(struct iio_dev *indio_dev);
void iio_device_unregister(struct iio_dev *indio_dev);
// 分配函数使用示例,在驱动程序的probe函数中
struct iio_dev *indio_dev;
struct my_private_data *data;
indio_dev = iio_device_alloc(sizeof(*data));
data = iio_priv(indio_dev);
[...]// 其他配置
iio_device_register(indio_dev);
iio_info{}用于声明iio内核使用的读取写入通道属性值的钩子函数:
struct iio_info {
struct module *driver_module;
const struct attribute_group *attrs;
int (*read_raw)(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask); // 用户读取sysfs设备时的会调用函数
int (*write_raw)(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int val, int val2, long mask);
[...] // 其他一些不关注的成员
};
2. 通道结构iio_chan_spec
通道代表单条采集线。
struct iio_chan_spec {
/*type指通道的测量类型,电压测量:IIO_VOLTAGE,
其他如IIOL_LIGHT(光测量), IIO_ACCEL(加速度计)*/
enum iio_chan_type type;
int channel; // 当indexed=1时,指定通道index
int channel2; // 当modified=1时,指定通道修饰符
ulong address; //
int scan_index; // 当使用缓冲区触发时,用于标识通道值在缓冲区record中的index;
struct {
char sign;
u8 realbits;
u8 storagebits;
u8 shift;
u8 repeat;
enum iio_endian endianness;
} scan_type; // 当使用缓冲区触发时,用于标识通道值在缓冲区record中的格式;
/*
通道在sysfs中属性文件以掩码形式指定,下面每个掩码均代表一个sysfs属性文件。
*/
long info_mask_separate; // 对应的属性文件专属于此通道
long info_mask_shared_by_type; // 对应的属性文件为相同类型的所有通道共享,导出的信息也由同一类型的通道共享;
long info_mask_shared_by_dir; // 对应的属性文件为相同方向的所有通道共享,导出的信息也由同一类型的通道共享;
long info_mask_shared_by_all; // 对应的属性文件为所有通道共享,导出的信息也由同一类型的通道共享;
const struct iio_event_spec *event_spec;
uint num_event_spec;
const struct iio_chan_spec_ext_info *ext_info;
const char *extend_name;
const char *datasheet_name;
uint modified:1;
/* 指出修饰符是否应用于此通道属性名称,例如IIO_MOD_X,IIO_MOD_Y,
IIO_MOD_Z是围绕xyz轴方向的修饰符,可用修饰符在enum iio_modifier中定义。
修饰符仅影响sysfs目录中的通道属性名称,而不是值
*/
uint indexed:1; // 指出通道属性名称是否具有索引
uint output:1;
uint differential:1;
};
// 其中info_mask_xxx的值对应到一个enum类型:enum struct iio_chan_info_enum
enum struct iio_chan_info_enum {
IIO_CHAN_INFO_RAW = 0,
IIO_CHAN_INFO_PROCESSED,
IIO_CHAN_INFO_SCALE,
IIO_CHAN_INFO_OFFSET,
[...],
IIO_CHAN_INFO_SAMP_FREQ,
IIO_CHAN_INFO_FREQUENCY,
IIO_CHAN_INFO_PHASE,
IIO_CHAN_INFO_HARDWAREGAIN,
IIO_CHAN_INFO_HYSTERESIS,
[...],
};
// 对应命名字符为
static const char * const iio_chan_info_postfix[] = {
[IIO_CHAN_INFO_RAW] = "raw",
[IIO_CHAN_INFO_PROCESSED] = "input",
[IIO_CHAN_INFO_SCALE] = "scale",
[IIO_CHAN_INFO_CALIBBIAS] = "calibbias",
[...],
[IIO_CHAN_INFO_FREQ] = "sampling_frequency",
[IIO_CHAN_INFO_FREQUENCY] = "frequency",
[...],
};
通道属性命名有IIO内核按照如下的方式自动生成:{direction}_{type}_{index}_{modifier}_{info_mask},
// sysfs文件名称格式:{direction}_{type}_{index}_{modifier}_{info_mask}
//其中direction对应于属性方向
static const char * const iio_direction [] = {
[0] = "in",
[1] = "out",
};
//其中type对应于通道的类型,取值与字符数组:
static const char * const iio_chan_type_name_spec [] = {
[IIO_VOLTAGE] = "voltage",
[IIO_CURRENT] = "current",
[IIO_POWER] = "power",
[IIO_ACCEL] = "accel",
[...],
[IIO_UVINDEX] = "uvindex",
[IIO_ELECTRICALCONDUCTIVITY] = "electricalconductivity",
[IIO_COUNT] = "count",
[IIO_INDEX] = "index",
[IIO_GRAVITY] = "gravity",
};
// 其中{index}取决于.indexed和.channel的值
// 其中{modifier}取决于.modified和.channel2的值,该值根据数组iio_modifier_names值进行替换
static const char *const iio_modifier_names[] = {
[IIO_MOD_X] = "x",
[IIO_MOD_Y] = "y",
[IIO_MOD_Z] = "z",
[IIO_MOD_X_AND_Y] = "x&y",
[IIO_MOD_X_AND_Z] = "x&z",
[IIO_MOD_Y_AND_Z] = "y&Z",
[...],
[IIO_MOD_CO2] = "co2",
[IIO_MOD_VOC] = "voc",
};
// 其中info_mask和介绍iio_chan_spec结构是指定的名称相同
下面是chan_spec对应通道的方式,
//当每个通道有多个数据时,需要使用索引和修饰符识别
// 只有一个通道时示例如下:
static const struct iio_chan_spec adc_channels[] = {
{.type = IIO_VOLTAGE,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
}
// 得出属性文件
/sys/bus/iio/iio:deviceX/in_voltage_raw
// 有3个通道时的示例如下:
static const struct iio_chan_type adc_channels[] = {
{
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 0,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
{
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 1,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
{
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 2,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
}
// 得出属性文件
/sys/bus/iio/iio:deviceX/in_voltage0_raw
/sys/bus/iio/iio:deviceX/in_voltage1_raw
/sys/bus/iio/iio:deviceX/in_voltage2_raw
// 假如光传感器有两个通道,一个用于红外光,另一个用于红外和可见光,此时需使用修饰符
static const struct iio_chan_spec my_light_channels[] = {
{
.type = IIO_INDENSITY,
.modified = 1,
.channel2 = IIO_MOD_LIGHT_IR,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
.info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
{
.type = IIO_INDENSITY,
.modified = 1,
.channel2 = IIO_MOD_LIGHT_BOTH,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
.info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
{
.type = IIO_LIGHT,
.info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),
.info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
}
// 对应sysfs属性文件
/sys/bus/iio/iio:deviceX/in_intensity_ir_raw // 测量IR强度的通道
/sys/bus/iio/iio:deviceX/in_intensity_both_raw // 测量红外和可见光的通道
/sys/bus/iio/iio:deviceX/in_illuminance_input // 用于处理数据
/sys/bus/iio/iio:deviceX/sampling_frequency // 用于采样频率,全部通道共享
二、IIO触发缓冲区支持
IIO设备驱动程序和触发器完全无关。触发器可以初始化一个或多个设备上的数据捕获,这些触发器用于填充缓冲区、作为字符设备提供给用户空间。
在linux内核中有如下触发器,可以用于和IIO设备关联:
- iio-trig-interrupt: 这为使用IRQ作为IIO触发器提供支持。对应内核选项CONFIG_IIO_INTERRUPT_TRIGGER
- iio-trig-hrtimer: 提供基于时钟频率的IIO触发器,使用HRT作为中断源(从内核4.5开始)。内核选项为CONFIG_IIO_HRTIMER_TRIG
- iio-trig-sysfs:允许使用sysfs向触发数据捕获。内核选项CONFIG_IIO_SYSFS_TRIGGER
- iio-trig-bfin-timer:允许使用blackfin定时器作为IIO触发器
1. IIO驱动程序支持缓冲区
IIO驱动程序可以声明任意数量的触发器,也可以选择哪些通道的数据推入缓冲区中。
IIO设备提供触发缓冲区支持时,必须设置iio_dev.pollfunc,触发器触发时执行它,它负责通过indo_dev->active_scan_mask查找启用的通道,检索其数据,并使用iio_push_to_buffers_with_timestamp函数将他们提供给indio_dev->buffer。IIO内核提供了一组函数来设置触发缓冲区。
下面是IIO驱动程序使用触发器的流程:
// 1. 如果需要填写iio_buffer_setup_ops{}结构
const struct iio_buffer_setup_ops sensor_buffer_setup_ops = {
.preenable = my_sensor_buffer_preenable,
.postenable = my_sensor_buffer_postenable,
.predisable = my_sensor_buffer_predisable,
.postdisable = my_sensor_buffer_postdisable,
};
// 2. 编写与触发器关联的上半部。在绝大多数情况下,必须提供捕获的时间戳。
irqreturn_t sensor_iio_pollfunc(int irq, void *p) {
struct iio_poll_func *pf = p;
pf->timestamp = iio_get_ns((struct indio_dev*)pf->indio_dev);
return IRQ_WAKE_THREAD;
}
// 3. 编写触发下半部,它将从每个通道读取数据,并把他们加入缓冲区中。
irqreturn_t sensor_trigger_handler(int irq, void *p) {
struct iio_poll_func *pf = p;
struct indio_dev* iio_dev = pf->indio_dev;
u16 buf[8];
int bit,i=0;
// 使用锁保护缓冲区,mutex_lock(&my_mutex);
// 3.1 获取每个通道的数据
for_each_set_bit(bit, iio_dev->active_scan_mask, iio_dev->masklength) {
buf[i++] = sensor_get_data(bit); // 获取某个通道的数据,具体和芯片实现相关
}
/*
3.2 如果iio_dev.scan_timestamp = true, 则捕获时间戳将被推送和存储
在将其推送到设备缓冲区之前,它作为示例缓冲区对象的最后一个元素,占8个字节
*/
iio_push_to_buffers_with_timestamp(iio_dev, buf, pf->timestamp);
// 打开锁,mutex_unlock(&my_mutex);
// 3.3 通知触发
iio_trigger_notify_done(iio_dev->trig);
}
// 4. 在probe函数中,必须首先设置缓冲区,再使用iio_device_register()注册设备
iio_triggered_buffer_setup(indio_dev, sensor_iio_pollfunc,
sensor_trigger_handler, sensor_buffer_setup_ops);
// 其中函数iio_triggered_buffer_setup也将为设备提供INDIO_DIRECT_MODE功能,当触发器指定到设备时,并不知道什么时候会被触发。
// 在支持缓冲区捕获机制时,应防止在sysfs上的数据捕获(有read_raw钩子执行)与缓冲区捕获冲突,在对应的钩子函数read_raw中应该用iio_buffer_enabled()判断是否已启动了缓冲区捕获,如下所示:
static int my_read_raw(struct iio_dev *indio_dev,
const struct iio_chan_spec *chan,
int *val, int *val2, long mask) {
[...]
switch(mask) {
case IIO_CHAN_INFO_RAW:
if (iio_buffer_enabled(iio_dev)) return -EBUSY;
}
[...]
}
2. IIO触发器和用户空间sysfs
可以在用户空间设置IIO设备对应的触发器。sysfs中有个位置与触发器相关:
- /sys/bus/iio/devices/triggerY/,一旦IIO触发器在IIO内核中注册并且对应于索引Y的触发器,就会创建该目录,该目录中有一个属性文件name;
- /sys/bus/iio/devices/iio:deviceX,如果设备支持触发缓冲区,则会自动创建目录/sys/bus/iio/devices/iio:deviceX/trigger/*;
i). sysfs触发器接口
sysfs触发器对应的sysfs目录为/sys/bus/iio/devices/iio_sysfs_trigger/,用于sysfs触发器管理,该目录下有两个文件add_trigger和remove_trigger。
a). 创建触发器trigger,示例:
echo 2 > /sys/bus/iio/devices/iio_sysfs_trigger/add_trigger
会创建触发器sysfstrig2,创建目录/sys/bus/iio/devices/trigger2/,在该目录下文件name,其值为“sysfstrig2”;在该目录下还有文件trigger_now,用于开始触发捕获,并把数据推入他们各自的缓冲区。
b). 删除触发器 对应sysfs文件remove_trigger
echo 2 > /sys/bus/iio/devices/iio_sysfs_trigger/add_trigger
c). 把触发器绑定给特定的IIO设备
要把IIO设备与触发器绑定,需要将触发器名称写入IIO设备的sysfs文件/sys/bus/iio/devices/iio:deviceX/trigger/current_trigger中,示例如下:
echo "sysfstrig2" > /sys/bus/iio/devices/iio:deviceX/trigger/current_trigger
解除绑定:
echo "" > /sys/bus/iio/devices/iio:deviceX/trigger/current_trigger
ii). 中断触发器使用
下面是中断触发器的示例:
static struct resource iio_irq_trigger_resources[] = {
[0] = {
.start = IRQ_NR_FOR_YOUR_IRQ,
.flags = IORESOURCE_IRQ | IORESOURCE_IRQ_LOWEDGE,
},
};
static struct platform_device iio_irq_trigger = {
.name = "iio_interrupt_trigger",
.num_resources = 1,
.reource = iio_irq_trigger_resources,
};
platform_device_register(&&iio_irq_trigger);
// 声明为IRQ触发器,将导致加载IRQ触发器独立模块。IRQ触发器名称的格式为irqtrigX,其中X为对应于刚传递的虚拟IRQ, 查看cat /sys/bus/iio/devices/trigger0/name
// 下面是在设备树节点声明IRQ触发器接口:
mylabel : my_trigger@0 {
compatible = "iio_interrupt_trigger";
interrupt-parent = <&gpio4>;
interrupts = <30 0x0>;
};
iii). hrtimer触发器接口
hrtimer触发器依赖于configfs文件系统。
mkdir /config
mount -t configfs none /config
模块iio-trig-hrtimer会自动在/config/目录下创建/config/iio,用户可以在目录/config/iio/triggers/hrtimer/目录下创建hrtimer触发器:
mkdir /config/iio/triggers/hrtimer/my_trigger_name/
删除Hrtimer触发器:
rmdir /config/iio/triggers/hrtimer/my_trigger_name/
每个hrtimer触发器在该触发器目录中都包含单个sampling_frequency属性。
3. IIO缓冲区中的数据
IIO缓冲区提供连续的数据捕获,一次可以同时读取多个数据通道。可通过字符设备文件/dev/iio:deviceX从用户空间访问缓冲区。
i). IIO缓冲区的sysfs接口
IIO缓冲区在目录/sys/bus/iio/devices/iio:deviceX/buffer/下有一些属性:
length:缓冲区可存储的数据取样总量。这是缓冲区包含的扫描数量。
enable:激活缓冲区捕获,启动缓冲区捕获。
watermark:这是一个正数,指定阻塞读取应该等待的扫描元素数量。
ii). iio缓冲区设置
被读取并推入缓冲区的数据称为扫描元素。扫描元素的配置可以通过/sys/bus/iio/devices/iio:deviceX/scan_elements/目录访问。其中包括如下属性:
- en:实际上是属性名称的后缀,用于启动通道。仅当其值不为0时,触发的捕捉将包含此通道的采样。每个通道对应一个文件,例如:in_voltage0_en、in_volatge1_en等
- type:描述扫描元素数据在缓冲区内的存储,因此描述从用户空间读取到它的形式。文件名示例为in_voltage0_type等。其值的格式是[be|le]:[s|u]bits/storage_bitsXrepeat[>>shift],其中[s|u]指出是无符号还是有符号数,其中bits是指值的位数,其中storage_bits是指存储的位数,其中shift是指取值之前移动的位数(一般是storage_bits - bits),其中repeat是指位重复存储数量(重复数位0或1时,重复值省略)。
type值示例:
$ cat /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_y_type
le:s12/16>>4
这解释为小端有符号数,16位长度,在屏蔽12位有效数之前需要右移4位。
在struct iio_chan_spec{}中负责表示缓冲区元素格式的是scan_type{}
struct iio_chan_spec {
[...];
struct {
char sign;
u8 realbits;
u8 storagebits;
u8 shift;
u8 repeat;
enum iio_endian endianness;
} scan_type;
[...];
};
// 下面是scan_type的示例:
static struct iio_chan_spec accel_channels[] = {
{
.type = IIO_ACCEL,
.modified = 1,
.channel2 = IIO_MOD_X,
.scan_index = 0,
.scan_type = {
.sign = 's',
.realbits = 12,
.storagebits = 16,
.shift = 4,
.endianness = IIO_LE,
},
},
/* 类似于X轴,Y轴和Z轴都是scan_index分别是1和2
*/
};
三、IIO数据访问
1、单次捕获
单次数据捕获通过sysfs接口完成,在iio内核中后端会调用struct iio_info{}.read_raw()和write_raw()回调函数。例如对于具有两个通道的温度传感器:一个用于测量环境温度,一个用于测量热电偶温度:
# cd /sys/bus/iio/devices/iio:device0/
# cat in_voltage3_raw
6646
# cat in_voltage_scale
0.305175781
// 将刻度乘以原始值即获得处理后的值
2. 缓冲区数据访问
要使触发采集正常工作,首先在驱动程序中实现触发器的支持。然后要从用户空间sysfs目录创建触发器并进行分配,启用ADC通道,设置缓冲区的大小并启用它。
// 1). 创建触发器
echo 0 > /sys/devices/sysfs_iio_trigger/add_trigger
// 得到触发器,对应木/sys/bus/iio/devices/trigger0/,名称为systrig0
// 2). 绑定iio设备和触发器systrig0
echo systrig0 > /sys/bus/iio/devices/iio:device0/trigger/current_trigger
// 3). 启动一些扫描元素
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage4_en
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage5_en
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage6_en
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage7_en
// 4). 设置缓冲区大小,这里是指缓冲区中record的数量
echo 100 > /sys/bus/iio/devices/iio:device0/buffer/length
// 5). 启用缓冲区
echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable
// 6). 启动触发器
echo 1 > /sys/bus/iio/devices/trigger0/trigger_now
// 7). 查看缓冲区中的数据
cat "/dev/iio:device0" | xxd -
// 停止缓冲区和分离触发器
echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable
echo "" > /sys/bus/iio/devices/iio:device0/trigger/current_trigger