1 引入IIO 子系统#
随着手机、物联网、工业物联网和可穿戴设备的爆发,传感器的需求越来越多。比如手机或者手环里面的加速度计、光传感器、陀螺仪、气压计、磁力计等,这些传感器本质上都是 ADC。这些传感器对外通过 IIC 或者 SPI 接口来发送ADC转换后的原始数据。
Linux 内核为了管理这些日益增多的 ADC 类传感器,特地推出了 IIO 子系统。
IIO
全称是 Industrial I/O
,翻译过来就是工业 I/O,常用的陀螺仪、加速度计、电压/电流测量芯片、光照传感器、压力传感器等内部都是有个 ADC,内部 ADC 将原始的 模拟数据转换为数字量,然后通过其他的通信接口,比如 IIC、SPI 等传输给 SOC。
因此,当使用的传感器本质是 ADC器件的时候,可以优先考虑使用 IIO 驱动框架。
2 IIO 子系统驱动框架#
- IIO sysfs对用户空间提供IIO设备访问和配置。
- IIO Core提供IIO设备、
IIO Trigger、IIO Buffer
分配、初始化、注册等工作。industrialio-core.c
industrialio-buffer.c
industrialio-event.c
- IIO Driver不同IIO设备的驱动程序。
3 数据结构#
3.1 iio_dev-iio设备#
1 | struct iio_dev { |
1 | /* Device operating modes */ |
模式 | 描述 |
---|---|
INDIO_DIRECT_MODE | 提供 sysfs 接口 |
INDIO_BUFFER_TRIGGERED | 支持硬件缓冲触发 |
INDIO_BUFFER_SOFTWARE | 支持软件缓冲触发 |
INDIO_BUFFER_HARDWARE | 支持硬件缓冲区 |
3.1.1 iio_buffer_setup_ops#
1 | struct iio_buffer_setup_ops { |
在使能或禁用缓冲区的时候会调用这些函数。
3.2 iio_info-属性和函数实现#
iio_info
包含每个iio设备的属性和具体实现函数。
1 | struct iio_info { |
indio_dev:需要读写的 IIO 设备。
chan:需要读取的通道。
val,val2:对于 read_raw
来说 val 和 val2 这两个就是应用程序从内核空间读取到数据,一般就是传感器指定通道值,或者传感器的量程、分辨率等。对于 write_raw
来说就是应用程序向设备写入的数据。val 和 val2 共同组成具体值,val 是整数部分,val2 是小数部分。Linux内核无法支持浮点运算,因此val2是放大后的值。扩大的倍数我们不能随便设置,而是要使用 Linux 定义的倍数,Linux 内核里面定义的数据扩大倍数:
组合宏 | 描述 |
---|---|
IIO_VAL_INT |
整数值,没有小数。比如 5000,那么就是 val=5000,不 需要设置 val2 |
IIO_VAL_INT_PLUS_MICRO |
小数部分扩大 1000000 倍,比如 1.00236 ,此时 val=1, val2=2360 |
IIO_VAL_INT_PLUS_NANO |
小数部分扩大 1000000000 倍,同样是 1.00236 ,此时 val=1,val2=2360000 |
IIO_VAL_INT_PLUS_MICRO_DB |
dB 数据,和IIO_VAL_INT_PLUS_MICRO 数据形式一 样,只是在后面添加 db |
IIO_VAL_INT_MULTIPLE |
多个整数值,比如一次要传回 6 个整数值,那么 val 和 val2就不够用了。此宏主要用于iio_info 的read_raw_multi 函数 |
IIO_VAL_FRACTIONAL |
分数值,也就是 val/val2 。比如 val=1,val2=4,那么实际 值就是 1/4 |
IIO_VAL_FRACTIONAL_LOG2 |
值为 val>>val2 ,也就是 val 右移 val2 位。比如 val=25600, val2=4 , 那 么 真 正 的 值 就 是 25600 右 移 4 位 , 25600>>4=1600 |
mask:掩码,用于指定我们读取的是什么数据,比如 ICM20608
这样的传感器,他既有原 始的测量数据,比如 X,Y,Z
轴的陀螺仪、加速度计等,也有测量范围值,或者分辨率。比如加 速度计测量范围设置为±16g
,那么分辨率就是 32/65536≈0.000488
,我们只有读出原始值以及 对应的分辨率(量程),才能计算出真实的重力加速度。此时就有两种数据值:传感器原始值、分辨率。Linux 内核使用 IIO_CHAN_INFO_RAW
和 IIO_CHAN_INFO_SCALE
这两个宏来表示原 始值以及分辨率,这两个宏就是掩码。
write_raw_get_fmt
用于设置用户空间向内核空间写入的数据格式,write_raw_get_fmt
函数决定了 wtite_raw
函数中 val 和 val2 的意义,也就是表中的组合 形式。比如我们需要在应用程序中设置 ICM20608
加速度计的量程为±8g
,那么分辨率就是16/65536≈0.000244
,我们在 write_raw_get_fmt
函数里面设置加速度计的数据格式为 IIO_VAL_INT_PLUS_MICRO
。那么我们在应用程序里面向指定的文件写入 0.000244
以后,最 终传递给内核驱动的就是 0.000244*1000000=244
。也就是 write_raw
函数的 val 参数为 0,val2 参数为 244。
3.3 iio_chan_spec-通道属性#
一个IIO设备可能有多个通道,每个通道由struct iio_chan_spec
表示。比如一个 ADC 芯片支持 8 路采集,那 么这个 ADC 就有 8 个通道。的 ICM20608
六轴传感器,可以输出三轴陀螺仪(X、Y、Z)
、三轴加速度计(X、Y、Z)
和一路温度,共7路数据。
1 | struct iio_chan_spec { |
如果是 ICM20608 这样的多轴传感器,那么就是复合类型了,陀螺仪部分是 IIO_ANGL_VEL
类型,加速度计部分是IIO_ACCEL
,温度部分就是 IIO_TEMP
。
当成员变量 modified 为 1
的时候,channel2 为通道修饰符
。Linux 内核给出了 可用的通道修饰符
:
1 | enum iio_modifier { |
比如 ICM20608
的加速度计部分,类型设置为 IIO_ACCEL
,X、Y、Z
这三个轴就用 channel2
的通道修饰符
来区分.
scan_type
各个成员变量:
1 | scan_type.sign:如果为‘u’表示数据为无符号类型,为‘s’的话为有符号类型。 |
info_mask_separate
将属性标记为特定于此通
info_mask_shared_by_type
标记导出的信息由相同类型的通道共享。X、Y、Z
轴他们的 type 都 是 IIO_ACCEL
,也就是类型相同。而这三个轴的分辨率(量程)是一样的,那么这3个通道info_mask_shared_by_type
中使能IO_CHAN_INFO_SCALE
这个属性,表示 这三个通道的分辨率是共用的,这样在sysfs
下就会只生成一个描述分辨率的文件,这三个通道 都可以使用这一个分辨率文件。
info_mask_shared_by_dir
标记某些导出的信息由相同方向的通道共享。
info_mask_shared_by_all
表设计某些信息所有的通道共享,无论这些通道的类 型、方向如何,全部共享。
output
表示为输出通道。
differential
表示为差分通道。
3.4 iio_trigger-触发数据采集#
触发器是基于某种信号来触发数据采集,比如:数据就绪中断;周期性中断;用户空间sysfs读写。
1 | struct iio_trigger { |
3.4.1 iio_trigger_ops#
1 | struct iio_trigger_ops { |
3.5 iio_buffer-保存采集到的数据#
1 | struct iio_buffer { |
4 API#
4.1 iio_dev 注册与注销#
1 | struct iio_dev *iio_device_alloc(int sizeof_priv)//申请 iio_dev |
1 | devm_iio_device_register/iio_device_register |
4.1.1 iio_device_register_sysfs
过程#
devm_iio_device_register
过程中会调用iio_device_register_sysfs
:
1 | ->iio_buffer_alloc_sysfs_and_mask |
展开iio_device_register_sysfs
如下:
1 | iio_device_register_sysfs |
其中在iio_device_add_info_mask_type
中设置了读写回调函数,比如cat in_voltage0_input
就会调用回调函数iio_read_channel_info
.
1 | int __iio_device_attr_init() |
格式为%s_%s%d_%s"
, 对应了in_voltage0_input
,其中iio_direction、iio_chan_type_name_spec、iio_chan_info_postfix
对应的值为 "in" 、"voltage" 、"input"
如下代码片段:
1 | static const char * const iio_direction[] = { |
4.2 iio_init-子系统的初始化#
IIO子系统初始化包括:
- iio总线注册
- 分配IIO字符设备号
- 创建IIO设备
debugfs
1 | iio_init |
5 iio实验-操作ICM20608#
5.1 使能内核 IIO#
1 | -> Device Drivers |
5.2 驱动代码分析#
5.2.0 源码#
1 |
|
5.2.1 probe#
还是按照spi子系统框架
,probe
时利用iio子系统,初始化iio_dev
。
iio_info
属性赋值:
iio_channels
属性赋值:
温度通道:
info_mask_separate
设置为IIO_CHAN_INFO_RAW,IIO_CHAN_INFO_SCALE, IIO_CHAN_INFO_OFFSET
此通。IIO_CHAN_INFO_RAW
为温度通道的原始值,IIO_CHAN_INFO_OFFSET
是ICM20608
温度 offset 值,这个要查阅数 据手册。IIO_CHAN_INFO_SCALE
是ICM20608
的比例,也就是一个单位的原始值为多少℃, 这个也要查阅ICM20608
的数据手册。扫描元素设置成
SCAN_TEMP
。扫描类型:有符号数据,数据位数16位,存储位数16位,右移位数0, 大端传输(一般MSB传输)
陀螺仪通道:
modified
成员变量为 1,所以channel2
就是通道修饰符
,用来指定 X、Y、Z 轴。扫描元素设置
SCAN_GYRO_X, Y,Z
。info_mask_separate
设置为IIO_CHAN_INFO_RAW, IIO_CHAN_INFO_CALIBBIAS
此通。“scale”是比例的意思,在这里就是量程(分辨率),因为 ICM20608 的陀螺仪和加速度计的量程是可以调整的,量程不同分辨率也就不同。设置每个通道的IIO_CHAN_INFO_RAW
和IIO_CHAN_INFO_CALIBBIAS
这两个属性都是独立的,IIO_CHAN_INFO_RAW
表示 ICM20608 每个通道的原始值,这个肯定 是每个通道独立的。IIO_CHAN_INFO_CALIBBIAS
是 ICM20608 每个通道的校准值,这个是 ICM20608 的特性,不是所有的传感器都有校准值,一切都要以实际所使用的传感器为准。info_mask_shared_by_type
设置为IIO_CHAN_INFO_SCALE
此通。表示量程分辨率共享对陀螺仪通道。扫描类型:有符号数据,数据位数16位,存储位数16位,右移位数0, 大端传输(一般MSB传输)
加速度通道:同理与陀螺仪通道.
INDIO_DIRECT_MODE
直接模式,提供sysfs
接口。
5.2.2 icm20608_read_raw#
5.2.2.1 读raw数据#
icm20608_read_raw
会传入具体通道和mask, 比如温度通道的raw,传入ICM20_TEMP_OUT_H
寄存器,IIO_MOD_X
那么ind
就等于0,那么regmap_bulk_read
出来就是一个16位的温度值。
同理,读加速度通道,传入ICM20_ACCEL_XOUT_H
寄存器,ind = (IIO_MOD_X - IIO_MOD_X )*2 = 0;
,因此读出16bit的x数据。再次传入ind = (IIO_MOD_Y - IIO_MOD_X)*2 = 2
,得到ICM20_ACCEL_YOUT_H
寄存器,因此读取出16bit的y数据。再次传入ind = (IIO_MOD_Z - IIO_MOD_X)*2 = 4
,得到ICM20_ACCEL_ZOUT_H
寄存器,因此读取出16bit的z数据。
同理,读陀螺仪通道。
5.2.2.2 读量程#
温度通道: 直接用预设量程即可或者val1,val2
。分别表示整数,小数。
加速度通道: 获取量程参考数据手册:icm20608控制寄存器
根据读到的寄存器值:regdata
配置量程如下:
1 | /* |
陀螺仪通道:同理
陀螺仪量程计算:
可选的量程有±250、±500、±1000 和±2000°/s
。以± 250
这个量程为例,每个数值对应的度数就是 500/2^10≈0.007629°/s
。同理,±500
量程对应 的度数为 0.015258
,±1000
量程对应的度数为 0.030517
,±2000
量程为0.061035
。假设现在设 置量程为±2000
,读取到的原始值为 12540
,那么对应的度数就是 12540*0.061035≈765.37°/s
。 注意,这里扩大了 1000000 倍。
加速度量程计算:
计算方法和陀螺仪一样。这里扩大了 1000000000 倍。
5.2.2.3 读offset#
只有温度有offset
。
5.2.3 icm20608_write_raw#
通过写寄存器来配置量程,校准值。
5.2.3.1 配置量程#
根据设置的val
量程值,匹配到i
量程等级,d<<3
然后设置量程等级。
5.2.3.2 配置校准值#
根据静态偏移寄存器,进行校准,写入校准值。
5.2.4 icm20608_write_raw_get_fmt#
用户空间设置数据格式。
陀螺仪通道:规定用户空间写的陀螺仪分辨率数据要乘以1000000。
加速度通道:规定用户空间写的陀螺仪分辨率数据要乘以1000000000。
比如我们在用户空间要设置加速度计量程为±4g
,只需要向 in_accel_scale
写 入 0.000122070
, 那 么 最 终 传 入 到 驱 动 里 面 的 就 是 0.000122070*1000000000=122070
。
5.3 测试#
进入“/sys/bus/iio/devices/”
目录:可以看到,此时有两个 IIO 设备“iio:device0”,iio:device0
是 I.MX6ULL 内 部 ADC,iio:device1
才是 ICM20608。
可以看出,iio:device1
对应spi2.0
上的设备,也就是 ICM20608
。
此目录下有 很多文件,比如in_accel_scale、in_accel_x_calibias、in_accel_x_raw
等,这些就是我们设置的通道。in_accel_scale
就是加速度计的比例,也就是分辨率(量程),in_accel_x_calibias
就是加速度 计 X 轴的校准值,in_accel_x_raw
就是加速度计的 X 轴原始值。我们在配置通道的时候,设置 了类型相同的所有通道共用 SCALE
,所以这里只有一个 in_accel_scale
,而 X、Y、Z 轴的原始值和校准值每个轴都有一个文件,陀螺仪和温度计同理。
5.3.1 通道文件命名方式#
源码见4.1.1 iio_device_register_sysfs
过程。
IIO_CHAN_INFO_RAW
和 IIO_CHAN_INFO_CALIBBIAS
这两个专属属性,对应in_accel_x_raw
和 in_accel_x_calibias
这两个文件。
通道的命名:[direction]_[type]_[index]_[modifier]_[info_mask]
direction:为属性对应的方向
1 | static const char * const iio_direction[] = { |
type:也就是配置通道的时候 type 值,type 对应的字符可以参考 iio_chan_type_name_spec
:
1 | static const char * const iio_chan_type_name_spec[] = { |
index:索引,如果配置通道的时候设置了 indexed=1,那么就会使用通道的 channel 成员变 量来替代此部分命名。比如,有个 ADC 芯片支持 8 个通道,那么就可以使用 channel 来表示对 应的通道,最终在用户空间呈现的每个通道文件名的 index 部分就是通道号。
modifier:当通道的 modified 成员变量为 1 的时候,channel2 就是修饰符, iio_modifier_names
:
1 | static const char * const iio_modifier_names[] = { |
info_mask:属性掩码,也就是属性:
1 | static const char * const iio_chan_info_postfix[] = { |
综上所述,in_accel_x_raw
组成形式如图:
5.3.2 读取raw数据#
进入“/sys/bus/iio/devices/”
目录,cat iio:device1/in_ccel_x
,这时驱动中的icm20608_read_raw
执行。
读取一下 in_accel_scale
这个文件,这是加速度计的分辨率,我们默认设置了加速度计 量程为±16g
,因此分辨率为 0.000488281
。
这时候有朋友可能会疑问,我们设置加速度计±16g
的分辨率为 488281
,也就是扩大了1000000000 倍,为啥这里读出来的是 0.000488281
这个原始值?
驱动返回的是IIO_VAL_INT_PLUS_NANO
, 因 此 用 户 空 间 得 到 分 辨 率 以 后 会 除 以 1000000000 , 得 到 真 实 的 分 辨 率 , 488281/1000000000=0.000488281
。
再读取一下 in_accel_z_raw
,加速度计的 Z 轴原始值。静态情况 下 Z 轴应该是 1g 的重力加速度计。
2074×0.000488281≈1.01g
,此时 Z 轴重力为 1g,结果正确。
5.3.3 编写应用程序测试#
1 |
|
6 IIO实验-操作vf610_adc.c#
以imx6ull芯片为例子,drivers/iio/adc/vf610_adc.c
6.1 dts描述#
dtsi描述如下:默认是关闭的
1 | adc1: adc@02198000 { |
我们最后在 imx6ull-alientek-emmc.dts
添加节点内容:
1 | pinctrl_adc1: adc1grp { |
- 添加 ADC 使用的
GPIO1_IO01
引脚配置信息 regulators
节点下添加参考电源子节点,最后status设置成okay
6.2 使能 ADC 驱动#
1 | -> Device Drivers |
drivers/iio/adc/
下的makefile和Kconfig
:
6.3 驱动代码分析#
6.3.1 probe#
申请初始化iio设备,alloc iio设备有多分一块内存给info结构体。
从dts获取寄存器地址信息,进行ioremap。获取中断号,注册中断服务程序。
获取时钟资源,电源资源,使能电源输出。并且获取电源参考电压vref_uv
.
获取adc采样周期,num-channels =2
.
中间vf610_adc_cfg_init和vf610_adc_hw_init
是ADC controller的寄存器配置不展开介绍。可以参考裸机实验IMX6ULL ADC控制器
然后设置iio_dev
结构体,最后注册iio设备。
6.3.2 vf610_read_raw#
读取 ADC 原始数据值,type 值为 IIO_VOLTAGE
,也就是读取电压值。这里info->value
是怎么来的呢?
当然是中断服务程序进行ADC采样啊,如下:
6.4 用户态测试#
进入/sys/bus/iio/devices/iio:device0
目录下,该iio
就是对应vf610
这个ADC:
in_voltage1_raw:
ADC1 通道 1 原始值文件。
in_voltage_scale:
ADC1 比例文件(分辨率),单位为 mV。实际电压值(mV)=in_voltage1_raw* in_voltage_scale
我的开发板此时 in_voltage1_raw
和 in_voltage_scale
这两个文件内容如下:
经过计算,实际电压:991*0.805664062≈798.4mV
,也就是 0.7984V
。
1 |
|
注意应用程序用到了浮点运算,因此:
arm-linux-gnueabihf-gcc -march=armv7-a -mfpu=neon -mfloat-abi=hard adcApp.c -o adcApp