Linux-IIO驱动实验
工业场合里面也有大量的模拟量和数字量之间的转换,也就是我们常说的 ADC 和 DAC。而且随着手机、物联网、工业物联网和可穿戴设备的爆发,传感器的需求只持续增强。比如手机或者手环里面的加速度计、光传感器、陀螺仪、气压计、磁力计等,这些传感器本质上都是ADC,大家注意查看这些传感器的手册,会发现他们内部都会有个 ADC,传感器对外提供 IIC或者 SPI 接口,SOC 可以通过 IIC 或者 SPI 接口来获取到传感器内部的 ADC 数值,从而得到想要测量的结果。Linux 内核为了管理这些日益增多的 ADC 类传感器,特地推出了 IIO 子系统,本章我们就来学习如何使用 IIO 子系统来编写 ADC 类传感器驱动。
IIO 子系统简介
IIO 全称是 Industrial I/O,翻译过来就是工业 I/O,大家不要看到“工业”两个字就觉得 IIO是只用于工业领域的。大家一般在搜索 IIO 子系统的时候,会发现大多数讲的都是 ADC,这是因为 IIO 就是为 ADC 类传感器准备的,当然了 DAC 也是可以的。大家常用的陀螺仪、加速度计、电压/电流测量芯片、光照传感器、压力传感器等内部都是有个 ADC,内部 ADC 将原始的模拟数据转换为数字量,然后通过其他的通信接口,比如 IIC、SPI 等传输给 SOC。
因此,当你使用的传感器本质是 ADC 或 DAC 器件的时候,可以优先考虑使用 IIO 驱动框架。
iio_dev
1、iio_dev 结构体
IIO 子系统使用结构体 iio_dev 来描述一个具体 IIO 设备,此设备结构体定义在 include/linux/iio/iio.h 文件中,结构体内容如下(有省略):
我们来看一下 iio_dev 结构体中几个比较重要的成员变量:
第 477 行,modes 为设备支持的模式,可选择的模如表 75.1.1.1 所示:
第 478 行,currentmode 为当前模式。
第 483 行,buffer 为缓冲区。
第 484 行,buffer_list 为当前匹配的缓冲区列表。
第 485 行,scan_bytes 为捕获到,并且提供给缓冲区的字节数。
第 488 行,available_scan_masks 为可选的扫描位掩码,使用触发缓冲区的时候可以通过设置掩码来确定使能哪些通道,使能以后的通道会将捕获到的数据发送到 IIO 缓冲区。
第 490 行,active_scan_mask 为缓冲区已经开启的通道掩码。只有这些使能了的通道数据才能被发送到缓冲区。
第 491 行,scan_timestamp 为扫描时间戳,如果使能以后会将捕获时间戳放到缓冲区里面。
第 493 行,trig 为 IIO 设备当前触发器,当使用缓冲模式的时候。
第 494 行,pollfunc 为一个函数,在接收到的触发器上运行。
第 496 行,channels 为 IIO 设备通道,为 iio_chan_spec 结构体类型,稍后会详细讲解 IIO通道。
第 497 行,num_channels 为 IIO 设备的通道数。
第 501 行,name 为 IIO 设备名字。
第 502 行,info 为 iio_info 结构体类型,这个结构体里面有很多函数,需要驱动开发人员编写,非常重要!我们从用户空间读取 IIO 设备内部数据,最终调用的就是 iio_info 里面的函数。稍后会详细讲解 iio_info 结构体。
第 504 行,setup_ops 为 iio_buffer_setup_ops 结构体类型,内容如下:
可以看出 iio_buffer_setup_ops 里面都是一些回调函数,在使能或禁用缓冲区的时候会调用这些函数。如果未指定的话就默认使用 iio_triggered_buffer_setup_ops。
继续回到示例代码 75.1.1.1 中第 505 行,chrdev 为字符设备,由 IIO 内核创建。
2、iio_dev 申请与释放
在使用之前要先申请 iio_dev,申请函数为 iio_device_alloc,函数原型如下:
struct iio_dev *iio_device_alloc(int sizeof_priv)
函数参数和返回值含义如下:
sizeof_priv:私有数据内存空间大小,一般我们会将自己定义的设备结构体变量作为 iio_dev的私有数据,这样可以直接通过 iio_device_alloc 函数同时完成 iio_dev 和设备结构体变量的内存申请。申请成功以后使用 iio_priv 函数来得到自定义的设备结构体变量首地址。
返回值:如果申请成功就返回 iio_dev 首地址,如果失败就返回 NULL。
一般 iio_device_alloc 和 iio_priv 之间的配合使用如下所示:
第 1 行,icm20608_dev 是自定义的设备结构体。
第 2 行,indio_dev 是 iio_dev 结构体变量指针。
第 5 行,使用 iio_device_alloc 函数来申请 iio_dev,并且一起申请了 icm2060_dev 的内存。
第 10 行,使用 iio_priv 函数从 iio_dev 中提取出私有数据,也就是 icm2608_dev 这个自定义结构体变量首地址。
如果要释放 iio_dev,需要使用 iio_device_free 函数,函数原型如下:
void iio_device_free(struct iio_dev *indio_dev)
函数参数和返回值含义如下:
indio_dev:需要释放的 iio_dev。
返回值:无。
也可以使用 devm_iio_device_alloc 来分配 iio_dev , 这样就不需要我们手动调用iio_device_free 函数完成 iio_dev 的释放工作。
3、iio_dev 注册与注销
前面分配好 iio_dev 以后就要初始化各种成员变量,初始化完成以后就需要将 iio_dev 注册到内核中,需要用到 iio_device_register 函数,函数原型如下:
int iio_device_register(struct iio_dev *indio_dev)
函数参数和返回值含义如下:
indio_dev:需要注册的 iio_dev。
返回值:0,成功;其他值,失败。
如果要注销 iio_dev 使用 iio_device_unregister 函数,函数原型如下:
void iio_device_unregister(struct iio_dev *indio_dev)
函数参数和返回值含义如下:
indio_dev:需要注销的 iio_dev。
返回值:0,成功;其他值,失败。
iio_info
iio_dev 有个成员变量:info,为 iio_info 结构体指针变量,这个是我们在编写 IIO 驱动的时候需要着重去实现的,因为用户空间对设备的具体操作最终都会反映到 iio_info 里面。iio_info结构体定义在 include/linux/iio/iio.h 中,结构体定义如下(有省略):
第 355 行,attrs 是通用的设备属性。
第 357 和 370 行,分别为 read_raw 和 write_raw 函数,这两个函数就是最终读写设备内部数据的操作函数,需要程序编写人员去实现的。比如应用读取一个陀螺仪传感器的原始数据,那么最终完成工作的就是 read_raw 函数,我们需要在 read_raw 函数里面实现对陀螺仪芯片的读取操作。同理,write_raw 是应用程序向陀螺仪芯片写数据,一般用于配置芯片,比如量程、数据速率等。这两个函数的参数都是一样的,我们依次来看一下:
indio_dev:需要读写的 IIO 设备。
chan:需要读取的通道。
val,val2:对于 read_raw 函数来说 val 和 val2 这两个就是应用程序从内核空间读取到数据,一般就是传感器指定通道值,或者传感器的量程、分辨率等。对于 write_raw 来说就是应用程序向设备写入的数据。val 和 val2 共同组成具体值,val 是整数部分,val2 是小数部分。但是val2 也是对具体的小数部分扩大 N 倍后的整数值,因为不能直接从内核向应用程序返回一个小数值。比如现在有个值为 1.00236,那么 val 就是 1,vla2 理论上来讲是 0.00236,但是我们需要对 0.00236 扩大 N 倍,使其变为整数,这里我们扩大 1000000 倍,那么 val2 就是 2360。因此val=1,val2=2360。扩大的倍数我们不能随便设置,而是要使用 Linux 定义的倍数,Linux 内核里面定义的数据扩大倍数,或者说数据组合形式如表 75.1.2.1 所示:
mask:掩码,用于指定我们读取的是什么数据,比如 ICM20608 这样的传感器,他既有原始的测量数据,比如 X,Y,Z 轴的陀螺仪、加速度计等,也有测量范围值,或者分辨率。比如加速度计测量范围设置为±16g,那么分辨率就是 32/65536≈0.000488,我们只有读出原始值以及对应的分辨率(量程),才能计算出真实的重力加速度。此时就有两种数据值:传感器原始值、分辨率。Linux 内核使用 IIO_CHAN_INFO_RAW 和 IIO_CHAN_INFO_SCALE 这两个宏来表示原始值以及分辨率,这两个宏就是掩码。至于每个通道可以采用哪几种掩码,这个在我们初始化通道的时候需要驱动编写人员设置好。掩码有很多种,稍后讲解 IIO 通道的时候详细讲解!
第 376 行的 write_raw_get_fmt 用于设置用户空间向内核空间写入的数据格式, write_raw_get_fmt 函数决定了 wtite_raw 函数中 val 和 val2 的意义,也就是表 75.1.2.1 中的组合 形式。比如我们需要在应用程序中设置 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。
iio_chan_spec
IIO 的核心就是通道,一个传感器可能有多路数据,比如一个 ADC 芯片支持 8 路采集,那么这个 ADC 就有 8 个通道。我们本章实验用到的 ICM20608,这是一个六轴传感器,可以输出三轴陀螺仪(X、Y、Z)、三轴加速度计(X、Y、Z)和一路温度,也就是一共有 7 路数据,因此就有 7 个通道。注意,三轴陀螺仪或加速度计的 X、Y、Z 这三个轴,每个轴都算一个通道。
Linux 内核使用 iio_chan_spec 结构体来描述通道,定义在 include/linux/iio/iio.h 文件中,内容如下:
来看一下 iio_chan_spec 结构体中一些比较重要的成员变量:
第 224 行,type 为通道类型, iio_chan_type 是一个枚举类型,列举出了可以选择的通道类型,定义在 include/uapi/linux/iio/types.h 文件里面,内容如下:
从示例代码 75.1.3.2 可以看出,目前 Linux 内核支持的传感器类型非常丰富,而且支持类型也会不断的增加。如果是 ADC,那就是 IIO_VOLTAGE 类型。如果是 ICM20608 这样的多轴传感器,那么就是复合类型了,陀螺仪部分是 IIO_ANGL_VEL 类型,加速度计部分是IIO_ACCEL 类型,温度部分就是 IIO_TEMP。
继续来看示例代码 75.1.3.1 中的 iio_chan_spec 结构体,第 225 行,当成员变量 indexed 为 1 时候,channel 为通道索引。
第 226 行,当成员变量 modified 为 1 的时候,channel2 为通道修饰符。Linux 内核给出了可用的通道修饰符,定义在 include/uapi/linux/iio/types.h 文件里面,内容如下(有省略)
比如 ICM20608 的加速度计部分,类型设置为 IIO_ACCEL,X、Y、Z 这三个轴就用 channel2的通道修饰符来区分。IIO_MOD_X、IIO_MOD_Y、IIO_MOD_Z 就分别对应 X、Y、Z 这三个轴。通道修饰符主要是影响 sysfs 下的通道文件名字,后面我们会讲解 sysfs 下通道文件名字组成形式。
继续回到示例代码 75.1.3.1,第 227 行的 address 成员变量用户可以自定义,但是一般会设置为此通道对应的芯片数据寄存器地址。比如 ICM20608 的加速度计 X 轴这个通道,它的数据首地址就是 0X3B。address 也可以用作其他功能,自行选择,也可以不使用 address,一切以实际情况为准。
第 228 行,当使用触发缓冲区的时候,scan_index 是扫描索引。
第 229~236,scan_type 是一个结构体,描述了扫描数据在缓冲区中的存储格式。我们依次来看一下 scan_type 各个成员变量的涵义:
scan_type.sign:如果为‘u’表示数据为无符号类型,为‘s’的话为有符号类型。
scan_type.realbits:数据真实的有效位数,比如很多传感器说的 10 位 ADC,其真实有效数据就是 10 位。
scan_type.storagebits:存储位数,有效位数+填充位。比如有些传感器 ADC 是 12 位的,那么我们存储的话肯定要用到 2 个字节,也就是 16 位,这 16 位就是存储位数。
scan_type.shift:右移位数,也就是存储位数和有效位数不一致的时候,需要右移的位数,这个参数不总是需要,一切以实际芯片的数据手册位数。
scan_type.repeat:实际或存储位的重复数量。
scan_type.endianness:数据的大小端模式,可设置为 IIO_CPU、IIO_BE(大端)或 IIO_LE(小 端)。
第 237 行,info_mask_separate 标记某些属性专属于此通道,include/linux/iio/types.h 文件中的 iio_chan_info_enum 枚举类型描述了可选的属性值,如下所示:
比如 ICM20608 加速度计的 X、Y、Z 这三个轴,在 sysfs 下这三个轴肯定是对应三个不同的文件,我们通过读取这三个文件就能得到每个轴的原始数据。IIO_CHAN_INFO_RAW 这个属性表示原始数据,当我们在配置 X、Y、Z 这三个通道的时候,在 info_mask_separate 中使能 IIO_CHAN_INFO_RAW 这个属性,那么就表示在 sysfs 下生成三个不同的文件分别对应 X、Y、 Z 轴,这三个轴的 IIO_CHAN_INFO_RAW 属性是相互独立的。
第 238 行,info_mask_shared_by_type 标记导出的信息由相同类型的通道共享。也就是 iio_chan_spec.type 成员变量相同的通道。比如 ICM20608 加速度计的 X、Y、Z 轴他们的 type 都 是 IIO_ACCEL,也就是类型相同。而这三个轴的分辨率(量程)是一样的,那么在配置这三个通道的时候就可以在 info_mask_shared_by_type 中使能 IIO_CHAN_INFO_SCALE 这个属性,表示这三个通道的分辨率是共用的,这样在 sysfs 下就会只生成一个描述分辨率的文件,这三个通道都可以使用这一个分辨率文件。
第 239 行,info_mask_shared_by_dir 标记某些导出的信息由相同方向的通道共享。
第 240 行,info_mask_shared_by_all 表设计某些信息所有的通道共享,无论这些通道的类型、方向如何,全部共享。
第 246 行,modified 为 1 的时候,channel2 为通道修饰符。
第 247 行,indexed 为 1 的时候,channel 为通道索引。
第 248 行,output 表示为输出通道。
第 249 行,differential 表示为差分通道。
IIO 驱动框架创建
前面我们已经对 IIO 设备、IIO 通道进行了详细的讲解,本节我们就来学习如何搭建 IIO 驱动框架。在上一小节分析 IIO 子系统的时候大家应该看出了,IIO 框架主要用于 ADC 类的传感器,比如陀螺仪、加速度计、磁力计、光强度计等,这些传感器基本都是 IIC 或者 SPI 接口的。
因此 IIO 驱动的基础框架就是 IIC 或者 SPI,我们可以在 IIC 或 SPI 驱动里面在加上上一章讲解的 regmap。当然了,有些 SOC 内部的 ADC 也会使用 IIO 框架,那么这个时候驱动的基础框架就是 platfrom。
基础驱动框架建立
我们以 SPI 接口为例,首先是 SPI 驱动框架,如下所示:
示例代码 75.2.1.1 就是标准的 SPI 驱动框架,如果所使用的传感器是 IIC 接口的,那么就是 IIC 驱动框架,没啥好说的。
IIO 设备申请与初始化
IIO 设备的申请、初始化以及注册在 probe 函数中完成,在注销驱动的时候还需要在 remove 函数中注销掉 IIO 设备、释放掉申请的一些内存。添加完 IIO 框架以后的 probe 和 remove 函数 如下所示:
第 2~7 行,用户自定义的设备结构体。
第 12 行,IIO 通道数组。
第 16~71 行, 这部分为 iio_info,当应用程序读取相应的驱动文件的时候,xxx_read_raw函数就会执行,我们在此函数中会读取传感器数据,然后返回给应用层。当应用层向相应的驱动写数据的时候,xxx_write_raw 函数就会执行。因此 xxx_read_raw 和 xxx_write_raw 这两个函数是非常重要的!需要我们根据具体的传感器来编写,这两个函数是编写 IIO 驱动的核心。
第 79~114 行,xxx_probe 函数,此函数的核心就是分配并初始化 iio_dev,最后向内核注册 iio_dev。第 86 行调用 devm_iio_device_alloc 函数分配 iio_dev 内存,这里连用户自定义的设备 结构体变量内存一起申请了。第 91 行调用 iio_priv 函数从 iio_dev 中提取出私有数据,这个私 有数据就是设备结构体变量。第 97~102 行初始化 iio_dev,重点是第 98 行设置 iio_dev 的 info成员变量。第 101 行设置 iio_dev 的通道。初始化完成以后就要调用 iio_device_register 函数向内核注册 iio_dev。整个过程就是:申请 iio_dev、初始化、注册,和我们前面讲解的其他驱动框架步骤一样。
第 121~134 行,xxx_remove 函数里面需要做的就是释放 xxx_probe 函数申请到的 IIO 相关资源,比如第 131 行,使用 iio_device_unregister 注销掉前面注册的 iio_dev。由于前面我们使用 devm_iio_device_alloc 函数申请的 iio_dev,因此不需要在 remove 函数中手动释放 iio_dev。
IIO 框架示例就讲解到这里,剩下的就是根据所使用的具体传感器,在 IIO 驱动框架里面添加相关的处理,接下来我们就以正点原子 I.MX6ULL 开发板上的 ICM20608 为例,进行 IIO 驱 动实战!
实验程序编写
接下来我们就直接使用 IIO 驱动框架编写 ICM20608 驱动,ICM20608 驱动核心就是 SPI,上一章我们也讲解了如何在 SPI 总线上使用 regmap。因此本章 ICM20608 驱动底层没啥好讲解的,重点是如何套上 IIO 驱动框架,因此关于 ICM20608 芯片内部寄存器、SPI 驱动、regmap 等不会做讲解,有什么不懂的可以看前面相应的实验章节。
本实验对应的例程路径为:开发板光盘→01、程序源码→02、Linux 驱动例程→27_iio→spi
使能内核 IIO 相关配置
Linux 内核默认使能了 IIO 子系统,但是有一些 IIO 模块没有选择上,这样会导致我们编译驱动的时候会提示某些 API 函数不存在,需要使能的项目如下:
ICM20608 的 IIO 驱动框架搭建
1、驱动框架搭建
设备树不需要做任何修改!
首先我们基于上一章实验,搭建出 ICM20608 的 IIO 驱动框架,IIO 框架部分我们已经在示例代码 75.2.2.1 中进行了详细的讲解。新建名为 icm20608.c 的驱动文件,搭建好的 ICM20608IIO 驱动框架内容如下所示:
具体参考正点原子驱动开发手册。
关于 ICM20608 的 IIO 驱动框架就讲解到这里,接下来就是测试此驱动框架。
2、驱动框架测试
我们已经搭建好了 ICM20608 的 IIO 驱动框架,通道也已经设置好了,虽然还不能直接读取到 ICM20608 的原始数据,但是我们可以通过驱动框架来窥探 IIO 在用户空间的存在方式。
我们在 icm20608_probe 函数里面设置了打印 ICM20608 ID 值,因此如果驱动加载成功,SPI 工作正常的话就会读取 ICM20608 的 ID 值并打印出来,如图 75.3.2.1 所示:
IIO 驱动框架提供了 sysfs 接口,因此加载成功以后我们可以在用户空间访问对应的 sysfs目录项,进入目录“/sys/bus/iio/devices/”目录里面,此目录下都是 IIO 框架设备,如图 75.3.2.2所示:
从图 75.3.2.2 可以看出,此时有两个 IIO 设备“iio:device0”,iio:device0 是 I.MX6ULL 内部 ADC,iio:device1 才是 ICM20608。大家进入到对应的设备目录就可以看出对应的 IIO 设备,我们进入图 75.3.2.2 中的“iio:device1”目录,此目录下的内容如图 75.3.2.3 所示:
从图 75.3.2.3 可以看出,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 轴的原始值和校准值每个轴都有一个文件,陀螺仪和温度计同理。
更多后续内容参考正点原子驱动开发手册。
原文地址:https://blog.csdn.net/qq_28576837/article/details/144309498
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!