工业场合里面也有大量的模拟量和数字量之间的转换,也就是常说的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
iio_dev结构体
IIO子系统使用结构体iio_dev来描述一个具体IIO设备,此设备结构体定义在include/linux/iio/iio.h文件中,结构体内容如下(有省略):
来看一下iio_dev结构体中几个比较重要的成员变量:
第529行,modes为设备支持的模式,可选择的模如下图所示:
第530行,currentmode为当前模式。
第535行,buffer为缓冲区。
第536行,buffer_list为当前匹配的缓冲区列表。
第537行,scan_bytes为捕获到,并且提供给缓冲区的字节数。
第540行,available_scan_masks为可选的扫描位掩码,使用触发缓冲区的时候可以通过设置掩码来确定使能哪些通道,使能以后的通道会将捕获到的数据发送到IIO缓冲区。
第542行,active_scan_mask为缓冲区已经开启的通道掩码。只有这些使能了的通道数据才能被发送到缓冲区。
第543行,scan_timestamp为扫描时间戳,如果使能以后会将捕获时间戳放到缓冲区里面。
第545行,trig为IIO设备当前触发器,当使用缓冲模式的时候。
第547行,pollfunc为一个函数,在接收到的触发器上运行。
第550行,channels为IIO设备通道,为iio_chan_spec结构体类型,稍后会详细讲解IIO通道。
第551行,num_channels为IIO设备的通道数。
第555行,name 为IIO设备名字。
第556行,info为iio_info结构体类型,这个结构体里面有很多函数,需要驱动开发人员编写,非常重要!从用户空间读取 IIO 设备内部数据,最终调用的就是iio_info里面的函数。稍后会详细讲解iio_info结构体。
第559行,setup_ops为iio_buffer_setup_ops结构体类型,内容如下:
可以看出iio_buffer_setup_ops里面都是一些回调函数,在使能或禁用缓冲区的时候会调用这些函数。如果未指定的话就默认使用iio_triggered_buffer_setup_ops。
继续回到示例代码56.1.1.1中第560行,chrdev为字符设备,由IIO内核创建。
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之间的配合使用如下所示:
示例代码 56.1.1.3 iio_device_alloc 和 iio_priv 函数的使用
1 struct icm20608_dev *dev;
2 struct iio_dev *indio_dev;
3
4 /* 1、申请 iio_dev 内存 */
5 indio_dev = iio_device_alloc(sizeof(*dev));
6 if (!indio_dev)
7 return -ENOMEM;
8
9 /* 2、获取设备结构体变量地址 */
10 dev = iio_priv(indio_dev);
第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的释放工作。
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 中,结构体定义如下(有省略):
第395行,attrs是通用的设备属性。
第397和417行,分别为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倍后的整数值,因为不能直接从内核向应用程序返回一个小数值。扩大的倍数不能随便设置,而是要使用Linux定义的倍数,Linux内核里面定义的数据扩大倍数,或者说数据组合形式如下图所示:
- mask:掩码,用于指定读取的数据,比如ICM20608这样的传感器,他既有原始的测量数据,比如X,Y,Z 轴的陀螺仪、加速度计等,也有测量范围值,或者分辨率。比如加速度计测量范围设置为±16g,那么分辨率就是32/65536≈0.000488,只有读出原始值以及对应的分辨率(量程),才能计算出真实的重力加速度。此时就有两种数据值:传感器原始值、分辨率。Linux内核使用IIO_CHAN_INFO_RAW和IIO_CHAN_INFO_SCALE这两个宏来表示原始值以及分辨率,这两个宏就是掩码。至于每个通道可以采用哪几种掩码,这个在初始化通道的时候需要驱动编写人员设置好。掩码有很多种,稍后讲解IIO通道的时候详细讲解!
第423行的write_raw_get_fmt用于设置用户空间向内核空间写入的数据格式,write_raw_get_fmt函数决定了wtite_raw函数中val和val2的意义,也就是上图中的组合形式。
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结构体中一些比较重要的成员变量:
第237行,type为通道类型, iio_chan_type是一个枚举类型,列举出了可以选择的通道类型,定义在include/uapi/linux/iio/types.h文件里面,内容如下:
从示例代码56.1.3.2可以看出,目前Linux内核支持的传感器类型非常丰富,而且支持类型也会不断的增加。如果是ADC,那就是IIO_VOLTAGE类型。如果是ICM20608这样的多轴传感器,那么就是复合类型了,陀螺仪部分是IIO_ANGL_VEL类型,加速度计部分是IIO_ACCEL类型,温度部分就是IIO_TEMP。
继续来看示例代码56.1.3.1中的iio_chan_spec结构体,第238行,当成员变量indexed为1的时候,channel为通道索引。
第239行,当成员变量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下通道文件名字组成形式。
继续回到示例代码56.1.3.1,第240行的address用户可以自定义,但是一般会设置为此通道对应的芯片数据寄存器地址。比如ICM20608的加速度计X轴这个通道,它的数据首地址就是0X3B。address也可以用作其他功能,自行选择,也可以不使用address,一切以实际情况为准。
第241行,当使用触发缓冲区的时候,scan_index是扫描索引。
第242-249,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(小端)。
第250行,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属性是相互独立的。
第251行,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下就会只生成一个描述分辨率的文件,这三个通道都可以使用这一个分辨率文件。
第254行,info_mask_shared_by_dir标记某些导出的信息由相同方向的通道共享。
第256行,info_mask_shared_by_all表设计某些信息所有的通道共享,无论这些通道的类型、方向如何,全部共享。
第263行,modified为1的时候,channel2为通道修饰符。
第264行,indexed为1的时候,channel为通道索引。
第265行,output表示为输出通道。
第266行,differential表示为差分通道。
IIO驱动框架搭建
前面已经对IIO设备、IIO通道进行了详细的讲解,本节就来学习如何搭建IIO驱动框架。在上一小节分析IIO子系统的时候可以看出,IIO框架主要用于ADC类的传感器,比如陀螺仪、加速度计、磁力计、光强度计等,这些传感器基本都是IIC或者SPI接口的。因此IIO驱动的基础框架就是IIC或者SPI,可以在IIC或SPI驱动里面在加上regmap。当然了,有些SOC内部的ADC也会使用IIO框架,那么这个时候驱动的基础框架就是platfrom。
基础驱动框架建立
以SPI接口为例,首先是SPI驱动框架,如下所示:
示例代码 56.2.1.1 SPI 驱动框架
1 /*
2 * @description : spi 驱动的 probe 函数,当驱动与
3 * 设备匹配以后此函数就会执行
4 * @param - spi : spi 设备
5 * @return : 0,成功;其他值,失败
6 */
7 static int xxx_probe(struct spi_device *spi)
8 {
9 return 0;
10 }
11
12 /*
13 * @description : spi 驱动的 remove 函数,移除 spi 驱动的时候此函数会执行
14 * @param - spi : spi 设备
15 * @return : 0,成功;其他负值,失败
16 */
17 static int xxx_remove(struct spi_device *spi)
18 {
19 return 0;
20 }
21
22 /* 传统匹配方式 ID 列表 */
23 static const struct spi_device_id xxx_id[] = {
24 {"alientek,xxx", 0},
25 {}
26 };
27
28 /* 设备树匹配列表 */
29 static const struct of_device_id xxx_of_match[] = {
30 { .compatible = "alientek,xxx" },
31 { /* Sentinel */ }
32 };
33
34 /* SPI 驱动结构体 */
35 static struct spi_driver xxx_driver = {
36 .probe = xxx_probe,
37 .remove = xxx_remove,
38 .driver = {
39 .owner = THIS_MODULE,
40 .name = "xxx",
41 .of_match_table = xxx_of_match,
42 },
43 .id_table = xxx_id,
44 };
45
46 /*
47 * @description : 驱动入口函数
48 * @param : 无
49 * @return : 无
50 */
51 static int __init xxx_init(void)
52 {
53 return spi_register_driver(&xxx_driver);
54 }
55
56 /*
57 * @description : 驱动出口函数
58 * @param : 无
59 * @return : 无
60 */
61 static void __exit xxx_exit(void)
62 {
63 spi_unregister_driver(&xxx_driver);
64 }
65
66 module_init(xxx_init);
67 module_exit(xxx_exit);
68 MODULE_LICENSE("GPL");
69 MODULE_AUTHOR("ALIENTEK");
示例代码56.2.1.1就是标准的SPI驱动框架,如果所使用的传感器是IIC接口的,那么就是IIC驱动框架。
IIO设备申请与初始化
IIO设备的申请、初始化以及注册在probe函数中完成,在注销驱动的时候还需要在remove函数中注销掉IIO设备、释放掉申请的一些内存。添加完IIO框架以后的probe和remove函数如下所示:
示例代码 56.2.2.1 添加 IIO 框架
1 /* 自定义设备结构体 */
2 struct xxx_dev {
3 struct spi_device *spi; /* spi 设备 */
4 struct regmap *regmap; /* regmap */
5 struct regmap_config regmap_config;
6 struct mutex lock;
7 };
8
9 /*
10 * 通道数组
11 */
12 static const struct iio_chan_spec xxx_channels[] = {
13
14 };
15
16 /*
17 * @description : 读函数,当读取 sysfs 中的文件的时候最终此函数会执行,
18 * :此函数里面会从传感器里面读取各种数据,然后上传给应用。
19 * @param - indio_dev : IIO 设备
20 * @param - chan : 通道
21 * @param - val : 读取的值,如果是小数值的话,val 是整数部分。
22 * @param - val2 : 读取的值,如果是小数值的话,val2 是小数部分。
23 * @param - mask : 掩码。
24 * @return : 0,成功;其他值,错误
25 */
26 static int xxx_read_raw(struct iio_dev *indio_dev,
27 struct iio_chan_spec const *chan,
28 int *val, int *val2, long mask)
29 {
30 return 0;
31 }
32
33 /*
34 * @description : 写函数,当向 sysfs 中的文件写数据的时候最终此函数
35 * :会执行,一般在此函数里面设置传感器,比如量程等。
36 * @param - indio_dev : IIO 设备
37 * @param - chan : 通道
38 * @param - val : 应用程序写入值,如果是小数的话,val 是整数部分。
39 * @param - val2 : 应用程序写入值,如果是小数的话,val2 是小数部分。
40 * @return : 0,成功;其他值,错误
41 */
42 static int xxx_write_raw(struct iio_dev *indio_dev,
43 struct iio_chan_spec const *chan,
44 int val, int val2, long mask)
45 {
46 return 0;
47 }
48
49 /*
50 * @description : 用户空间写数据格式,比如我们在用户空间操作 sysfs 来设
51 * :置传感器的分辨率,如果分辨率带小数,那么这个小数传递到
52 * : 内核空间应该扩大多少倍,此函数就是用来设置这个的。
53 * @param - indio_dev : iio_dev
54 * @param - chan : 通道
55 * @param - mask : 掩码
56 * @return : 0,成功;其他值,错误
57 */
58 static int xxx_write_raw_get_fmt(struct iio_dev *indio_dev,
59 struct iio_chan_spec const *chan, long mask)
60 {
61 return 0;
62 }
63
64 /*
65 * iio_info 结构体变量
66 */
67 static const struct iio_info xxx_info = {
68 .read_raw = xxx_read_raw,
69 .write_raw = xxx_write_raw,
70 .write_raw_get_fmt = &xxx_write_raw_get_fmt,
71 };
72
73 /*
74 * @description : spi 驱动的 probe 函数,当驱动与
75 * 设备匹配以后此函数就会执行
76 * @param - spi : spi 设备
77 *
78 */
79 static int xxx_probe(struct spi_device *spi)
80 {
81 int ret;
82 struct xxx_dev *data;
83 struct iio_dev *indio_dev;
84
85 /* 1、申请 iio_dev 内存 */
86 indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*data));
87 if (!indio_dev)
88 return -ENOMEM;
89
90 /* 2、获取 xxx_dev 结构体地址 */
91 data = iio_priv(indio_dev);
92 data->spi = spi;
93 spi_set_drvdata(spi, indio_dev);
94 mutex_init(&data->lock);
95
96 /* 3、初始化 iio_dev 成员变量 */
97 indio_dev->dev.parent = &spi->dev;
98 indio_dev->info = &xxx_info;
99 indio_dev->name = "xxx";
100 indio_dev->modes = INDIO_DIRECT_MODE; /* 直接模式 /
101 indio_dev->channels = xxx_channels;
102 indio_dev->num_channels = ARRAY_SIZE(xxx_channels);
103
104 iio_device_register(indio_dev);
105
106 /* 4、regmap 相关设置 */
107
108 /* 5、SPI 相关设置*/
109
110 /* 6、芯片初始化 */
111
112 return 0;
113
114 }
115
116 /*
117 * @description : spi 驱动的 remove 函数,移除 spi 驱动的时候此函数会执行
118 * @param - spi : spi 设备
119 * @return : 0,成功;其他负值,失败
120 */
121 static int xxx_remove(struct spi_device *spi)
122 {
123 struct iio_dev *indio_dev = spi_get_drvdata(spi);
124 struct xxx_dev *data;
125
126 data = iio_priv(indio_dev); ;
127
128 /* 1、其他资源的注销以及释放 */
129
130 /* 2、注销 IIO */
131 iio_device_unregister(indio_dev);
132
133 return 0;
134 }
第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驱动框架里面添加相关的处理,接下来就以正点原子STM32MP157开发板上的ICM20608为例,进行IIO驱动实战!
实验程序编写
接下来就直接使用IIO驱动框架编写ICM20608驱动,ICM20608驱动核心就是SPI,之前已经学习过了如何在SPI总线上使用regmap。因此本章ICM20608驱动底层就是之前的,重点是如何套上IIO驱动框架,因此关于ICM20608芯片内部寄存器、SPI驱动、regmap等就可以直接套用之前的学习内容。
ICM20608的IIO驱动框架搭建
首先需要定义一个ICM20608_CHAN这个通道宏定义,在里面需要定义好iio相关的陀螺仪和加速度计的量程、数据类型等。
然后enum一个inv_icm20608_scan的枚举类型变量,包括陀螺仪、加速度计的6个通道,温度计的1个通道、以及1个 ICM20608时间戳通道。
之后定义一个icm20608_dev的结构体变量作为设备结构体,里面定义一个spi_device结构体指针spi,一个regmap结构体指针regmap,一个regmap_config结构体变量的regmap_config以及一个mutex结构体的lock。
然后就是IIO驱动的核心,就是通道,也就是iio_chan_spec结构体,这里定义一个该结构体变量的icm20608_channels[]数组,里面需要定义7个通道:
- 温度通道:这里根据需求需要定义.type,.info_mask_separate(这里就是温度通道的3个属性,原始值、offset值以及比例尺),.scan_index以及.scan_type。
- 3个陀螺仪:可以直接调用之前宏定义的ICM20608_CHAN。
- 3个加速度计:可以直接调用之前宏定义的ICM20608_CHAN。
之后编写icm20608_read_onereg来读取一个寄存器的值,这里就是直接regmap_read就可以了;同理icm20608_write_onereg就是调用regmap_write。
然后编写icm20608_reginit函数,来进行icm20608的寄存器初始化配置,就是调用之前的read和write,查阅数据手册来进行icm20608的初始化配置。
之后编写IIO会调用的icm20608_read_raw函数,里面就是printk一下;icm20608_write_raw函数同样printk一下就行。
继续编写icm20608_write_raw_get_fmt,这个函数就是用来设置应用程序向驱动写入的数据格式,该函数会决定write_raw中的val1和val2的意义,这里也是printk就可以了。
之后实现iio_info的结构体icm20608_info,里面需要把.read_rar,.write_raw以及.write_raw_get_fmt给放进去。
之后就是完成spi的probe函数icm20608_probe函数,里面需要先devm_iio_device_alloc申请iio_dev结构体指针indio_dev的内存,然后通过iio_priv获取icm20608_dev这个结构体指针data,然后data->spi就是传入的spi_device结构体指针spi,通过spi_set_drvdata获取icm20608dev的地址,然后通过mutex_init初始化lock,之后把参数全部配置到indio_dev中;完成后通过iio_device_register注册indio_dev;再初始化data的regmap_config设置;之后设置data->regmap,通过regmap_init_spi来初始化SPI接口的regmap;之后初始化spi_device,设置spi工作模式然后spi_setup;最后调用icm20608_reginit来初始化ICM20608内部寄存器。
之后完成remove函数icm20608_remove,首先通过spi_getdata获取到indio_dev,然后需要通过iio_priv获取indio_dev,之后就是regmap_exit注销regmap,再iio_device_unregister注销IIO。
然后就是设置of_device_id来进行设备树匹配列表的设置,设置为icm20608_of_match[]数组,里面设置.compatible即可。
之后需要定义SPI驱动结构体spi_driver结构体的icm20608_driver,里面需要定义.probe,.remove,.driver,把之前写好的对应函数放进去就可以了。
最后完成驱动入口函数icm20608_init,里面调用spi_register_driver;驱动出口函数icm20608_exit里面调用spi_unregister_driver。
然后就是常规的5个要加的module相关就完成了。
驱动框架测试
已经搭建好了ICM20608的IIO驱动框架,通道也已经设置好了,虽然还不能直接读取到ICM20608的原始数据,但是可以通过驱动框架来窥探IIO在用户空间的存在方式。
编译驱动,得到icm20608.ko驱动文件。输入如下命令加载icm20608.ko这个驱动模块:
depmod //第一次加载驱动的时候需要运行此命令 modprobe icm20608.ko //加载驱动模块 |
在icm20608_probe函数里面设置了打印ICM20608 ID值,因此如果驱动加载成功,SPI工作正常的话就会读取ICM20608的ID值并打印出来,如下图所示:
IIO驱动框架提供了sysfs接口,因此加载成功以后可以在用户空间访问对应的sysfs目录项,进入目录“/sys/bus/iio/devices/”目录里面,此目录下都是IIO框架设备,如下图所示:
从上图可以看出,此时只有一个IIO设备“iio:device0”,这个就是ICM20608,如果有多个IIO设备的话就需要依次进入到对应的设备目录,查看所对应的具体芯片型号。进入上图中的“iio:device0”目录,此目录下的内容如下图所示:
从上图可以看出,iio:device0对应spi0.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轴的原始值和校准值每个轴都有一个文件,陀螺仪和温度计同理。
通道文件命名方式
来看一下上图中这些文件名字组成方式,以in_accel_x_raw为例,这是加速度计的X轴原始值,驱动代码中此通道的配置内容展开以后如下(演示代码):
第5行设置了此通道有IIO_CHAN_INFO_RAW和IIO_CHAN_INFO_CALIBBIAS这两个专属属性,因此才会有之前iio:device0中的in_accel_x_raw和in_accel_x_calibias这两个文件。
通道属性的命名,也就是中文件的命名模式为:[direction]_[type]_[index]_[modifier]_[info_mask],依次来看一下这些命名组织模块:
- direction:为属性对应的方向,iio_direction结构体定义了方向,内容如下:
可以看出,就有两个方向:in和out。
- type:也就是配置通道的时候type值,type对应的字符可以参考iio_chan_type_name_spec,如下:
所以,当通道的type设置为IIO_ACCEL的时候,对应的名字就是“accel”。
- index:索引,如果配置通道的时候设置了indexed=1,那么就会使用通道的channel成员变量来替代此部分命名。比如,有个ADC芯片支持8个通道,那么就可以使用channel来表示对应的通道,最终在用户空间呈现的每个通道文件名的index部分就是通道号。
- modifier:当通道的modified成员变量为1的时候,channel2就是修饰符,修饰符对应的字符串参考结构体iio_modifier_names,内容如下:
当通道的修饰符设置为IIO_MOD_X的时候,对应的名字就是“x”。
- info_mask:属性掩码,也就是属性,不同属性对应的字符如下所示:
可以看出,IIO_CHAN_INFO_RAW属性对应的就是“raw”,IIO_CHAN_INFO_SCALE属性对应的是“scale”。
综上所述,in_accel_x_raw组成形式如下图所示:
文件读测试
可以测试一下对iio:device0中的这些文件进行读写操作,看看有什么效果,比如in_accel_x_raw文件是ICM20608的加速度计X轴原始值通道,使用cat命令查看此文件内容,结果如下图所示:
从上图可以看出,当用户空间读取相应文件的时候,iio_info下的read函数会被调用,因此关于传感器数据读取的操作都是在此函数中完成的。由于驱动还不完善,先不要测试向指定文件写操作,否则可能会报错,后面再测试写操作。也可以读取一下其他文件,比如陀螺仪的X轴文件in_anglvel_x_raw,结果最终都是调用的icm20608_read_raw这个函数。
接下来的重点就是完善驱动中的icm20608_read_raw函数,实现传感器数据的读取操作。
完善icm20608_read_raw函数
应用程序所有的读取操作,最终都会汇总到iio_info的read函数,这里就是icm20608_read_raw函数。由于所有的读取操作都会触发icm20608_read_raw函数,比如加速度
计、陀螺仪、温度计等,因此需要做区分。配置通道的时候设置了type值,就可以使用type值来区分是陀螺仪、加速度计还是温度计。
在icm20608_read_raw函数中,先通过iio_pric获取到icm20608_dev的dev指针,然后通过设置的mask来判断是哪个掩码,具体的掩码中还可以通过chan->type来对应printk信息。
这些分支对应不同的文件读操作。比如读取in_accel_scale这个文件,这个是加速度计的比例文件,结果如下图所示:
从上图可以看出,输出了“read accelscale”这行字符串,正好就是设置的分支。就可以在这个分支里面读取ICM20608的加速度计比例值,也就是量程。其他文件也一样,最终都会对应到相应的分支里面,只需要在相应的分支里面做具体的操作就行
了。剩下的操作就是读取ICM20608的内部寄存器数据。
这里的完善就是定义两个scale的数组,分别对应加速度计和陀螺仪,里面就是计算得到的不同四个量程的分辨率,要把量程/2^16然后放大1000000倍。
之后编写icm20608_sensor_show函数,里面就是读取寄存器的数据,通过regmap_bulk_read读出原始数据。
之后编写icm20608_read_channel_data,来读取7个通道的数据,也就是先通过iio_priv获取到icm20608这个设备之后,通过chan->type进行判断通道,然后通过icm20608_sensor_show来进行原始数据的参数传入。
最后就是实现icm20608_read_raw函数的完善,通过iio_priv获取到设备之后,通过mask来进行判断当前的读取模式:如果是IIO_CHAN_INFO_RAW,那就是读取原始值,需要先iio_device_claim_direct_mode保持direct模式,然后mutex_lock上锁后通过icm20608_read__channel_data读取后mutex_unlock解锁,最后iio_device_release_direct_mode就可以了。0如果是IIO_CHAN_INFO_SCALE,需要通过chan->type判断寄存器,然后通过icm20608_read_onereg读取寄存器值之后,通过之前设置的对应scale数组放大后存入val2,之后解锁并return;如果是温度则是直接读取ICM20608_TEMP_SCALE,整数存入val,小数存入val2然后return。如果是IIO_CHAN_INFO_OFFSET,那也是要进入chan->type进行判断,如果是IIO_TEMP,直接把ICM20608_TEMP_OFFSET存入val就可以了;是其他的就报错,这个case只能是温度传感器的offset,另外两个没有。如果是IIO_CHAN_INFO_CALIBBIAS,同样进入chan->type的判断,如果是IIO_ANGL_VEL,需要线上所然后通过icm20608_sensor_show,读取陀螺仪的校准值然后解锁;如果是IIO_ACCEL也是一样的方法。
修改完之后,重新编译驱动文件,然后加载新的驱动,测试是否可以正常读取到相应的内容。读取一下in_accel_scale这个文件,这是加速度计的分辨率,默认设置了加速度计量程为±16g,因此分辨率为0.000488281。结果如下图所示:
测试得到的值是正确的。因为设置加速度计±16g 的分辨率为488281,也就是是扩大了1000000000倍,在读取加速度计的分辨率处,读取完成以后在返回IIO_VAL_INT_PLUS_NANO这个值,这里就是告诉用户空间,小数部分(val2)扩大1000000000倍 ,因此用户空间得到分辨率以后会除以1000000000 ,得到真实的分辨率,488281/1000000000=0.000488281。
在读取一下in_accel_z_raw这个文件,这个文件是加速度计的Z轴原始值,静态情况下Z轴应该是1g的重力加速度计,可以读取in_accel_z_raw这个文件的值,然后在结合上面读取到的加速度计分辨率,计算一下对应的Z轴重力值,看看是不是1g左右。
2063×0.000488281≈1g,此时Z轴重力为1g,结果正确。
完善icm20608_write_raw函数
最后完善icm20608_write_raw函数,用户空间向驱动写数据的时候icm20608_write_raw函数会执行。可以在用户空间设置陀螺仪、加速度计的量程、校准值等,这时候就需要向驱动写入数据。本章简单一点,就只实现设置陀螺仪和加速度计的量程,至于其他的设置项可以自行实现。
首先是icm20608_sensor_set函数,这里的实现跟之前的icm20608_sensor_show是很类似的,里面就是regmap_bulk_write来写入数据进而设置传感器。
然后是icm20608_write_gyro_scale函数,来完成设置陀螺仪量程的任务,里面就是通过for来循环读取gyro_scale_icm20608数组,如果匹配到了要写入的量程,就把他通过regmap_write写入即可;icm20608_write_accel_scale函数,实现同上。
然后就是icm20608_write_raw函数,同样先通过iio_priv获取icm20608_dev这个设备结构体,然后iio_device_claim_direct_mode进入direct模式,然后通过mask来进行switch判断:如果是IIO_CHAN_INFO_SCALE,就是设置分辨率,通过chan->type判断,如果是IIO_ANGL_VEL,那就是上锁后通过icm20608_write_gyro_scale来写入配置然后解锁;如果是IIO_ACCEL,就是上锁后icm20608_write_accel_scale写入配置然后解锁。如果是IIO_CHAN_INFO_CALIBBIAS,就是设置校准值,同样进入chan->type判断,如果是IIO_ANGL_VEL,就是上锁后icm20608_sensor_set设置陀螺仪校准值然后解锁;如果是IIO_ACCEL,就是上锁后icm20608_sensor_set设置加速度计校准值然后解锁。最后iio_device_release_direct_mode。
最后就是icm20608_write_raw_get_fmt,在sysfs来设置传感器的分辨率。这里同样,通过mask的case判断:如果是IIO_CHAN_INFO_SCALE,就通过chan->type判断,如果是IIO_ANGL_VEL,那就return IIO_VAL_INT_PLUS_MICRO;default的就是return IIO_VAL_INT_PLUS_NANO。default就是return IIO_VAL_INT_PLUS_MICRO。
最后再写一个iio_info的结构体icm20608_info,里面就是设置.read_raw,.write_raw以及.write_raw_get_fmt。
测试应用程序编写
Linux文件流读取
前面都是直接使用cat命令读取对应文件的内容,如果要连续不断的读取传感器数据就不能用cat命令了,需要编写对应的APP软件,在编写APP之前先了解一下所要用到的API函数。
首先要知道,前面使用cat命令读取到的文件内容字符串,虽然看起来像是数字。比如使用cat命令读取到的in_accel_scale,in_accel_scale文件内容为0.000488281,但是这里的0.000488281是字符串,并不是具体的数字,所以需要将其转换为对应的数字。另外in_accel_scale是流文件,也叫做标准文件I/O流,因此打开、读写操作要使用文件流操作函数。
打开文件流
打开文件流使用fopen函数,函数原型如下:
FILE *fopen(const char *pathname, const char *mode)
函数参数和返回值含义如下:
- pathname:需要打开的文件流路径。
- mode:打开方式,可选的打开方式如下图所示:
- 返回值:NULL,打开错误;其他值,打开成功的文件流指针,为FILE类型。
关闭文件流
关闭文件流使用函数fclose,函数原型如下:
int fclose(FILE *stream)
函数参数和返回值含义如下:
- stream:要关闭的文件流指针。
- 返回值:0,关闭成功;EOF,关闭错误。
读取文件流
要读取文件流使用fread函数,函数原型如下:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
fread函数用于从给定的输入流中读取最多nmemb个对象到数组ptr中,函数参数和返回值含义如下:
- ptr:要读取的数组中首个对象的指针。
- size:每个对象的大小。
- nmemb:要读取的对象个数。
- stream:要读取的文件流。
- 返回值:返回读取成功的对象个数,如果出现错误或到文件末尾,那么返回一个短计数值(或者0)。
写文件流
要向文件流写入数据,使用fwrite函数,函数原型如下:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
fwrite函数用于向给定的文件流中写入最多nmemb个对象,函数参数和返回值含义如下:
- ptr:要写入的数组中首个对象的指针。
- size:每个对象的大小。
- nmemb:要写入的对象个数。
- stream:要写入的文件流。
- 返回值:返回成功写入的对象个数,如果出现错误或到文件末尾,那么返回一个短计数值(或者0)。
格式化输入文件流
fscanf函数用于从一个文件流中格式化读取数据,fscanf函数在遇到空格和换行符的时候就会结束。前面说了IIO框架下的sysfs文件内容都是字符串,比如in_accel_scale文件内容为“0.000488281”,这是一串字符串,并不是具体的数字,因此在读取的时候就需要使用字符串读取格式。在这里就可以使用fscanf函数来格式化读取文件内容,函数原型如下:
int fscanf(FILE *stream, const char *format, ,[argument...])
fscanf用法和scanf类似,函数参数和返回值含义如下:
- stream:要操作的文件流。
- format:格式。
- argument:保存读取到的数据。
- 返回值:成功读取到的数据个数,如果读到文件末尾或者读取错误就返回EOF。
编写测试APP
首先定义一个宏定义SENSOR_FLOAT_DATA_GET宏用读取指定路径的文件内容,然后将读到的浮点型字符串数据转换为具体的浮点数据。实际就是通过file_data_read读取之后使用atof函数将浮点字符串转换为具体的浮点数值。
再定义宏定义SENSOR_INT_DATA_GET宏用于读取指定路径的文件内容,将读取到的整数型字符串数据转换为具体的整数值。实际就是file_data_read之后使用atoi函数将整数字符串转换为具体的整数数值。
之后定义一个file_path[]的char指针数组,存入icm20608对应的iio框架的文件路径;enum一个path_index来写入file_path顺序对应的文件索引;然后定义一个icm20608_dev这个设备结构体,存入相关的offset、calibbias以及raw值还有scale和act值。之后具象化icm20608_dev为icm20608。
之后就是定义file_data_read这个读取文件内容的函数,通过fopen来只读打开文件到data_steam(File指针类型),然后通过fscanf进行格式化读取,也就是按照字符串方式读取,如果读到末尾的EOF那就通过fseek将文件指针调整到文件头,最后读取完了通过fclose关闭文件。
之后编写sensor_read来获取icm20608数据,首先通过之前写的宏定义SENSOR_FLOAT_DATA_GET和SENSOR_INT_DATA_GET获取到陀螺仪、加速度计的原始数据以及温度值,之后通过dev的raw和scale相乘来获取实际数值。
最后编写主函数,这里的argc就是1个参数,然后进入while死循环中,通过sensor_read函数读取ICM20608传感器数据,包括陀螺仪、加速度和
温度计的原始值,还有加速度计和陀螺仪的分辨率等,最后将获取到的原始值转换为具体的数值,数据读取成功后就是讲这些数据全部printf出来,完了之后就是usleep(100000)休息100ms。
运行测试
输入如下命令编译测试icm20608App.c这个测试程序:
arm-none-linux-gnueabihf-gcc icm20608App.c -o icm20608App |
编译成功以后就会生成icm20608App这个应用程序。
将icm20608.ko和icm20608App这两个文件拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录lib/modules/5.4.31中,输入如下命令加载icm20608.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令 modprobe icm20608.ko //加载驱动 |
驱动加载成功后可以使用如下命令测试:
./icm20608App |
如果驱动和APP工作正常的话就会不断的打印出ICM20608数据,包括陀螺仪和加速度计的原始数据和转换后的实际数值、温度等,如下图所示:
IIO触发缓冲区
触发缓冲区就是基于某种信号来触发数据采集,这些信号就是触发器,比如:
- 传感器数据就绪中断。
- 周期性中断。
- 用户空间下读取sysfs下的指定文件。
触发器肯定是触发数据的采集,数据采集到以后会填充到缓冲区里面,最终会以字符设备形式提供给用户空间,用户空间直接读取缓冲区文件即可。
IIO触发器
linux内核使用iio_trigger结构体表示触发器,定义在include/linux/iio/trigger.h文件中,内容如下:
重点是第2行的iio_trigger_ops,这个是触发器的操作函数结构体,内容如下:
第33行,set_trigger_state函数用于设置触发器状态,也就是打开/关闭触发器。如果用中断作为触发器的话,此函数一般设置传感器的中断使能状态。
第34行,try_reenable函数当用户计数为0的时候尝试重新使能触发器。
第35行,validate_device函数用于当触发器改变的时候,使设备生效。
自行创建触发器的话,需要驱动开发人员编写iio_trigger_ops。
申请触发器
首先需要使用iio_trigger_alloc函数创建触发器,函数原型如下:
struct iio_trigger *iio_trigger_alloc(const char *fmt, ...)
可以看出iio_trigger_alloc是个可变长度参数函数,但是它的目的只有一个:拼凑触发器名字,和printf函数用法类似,比如下面这一行:
iio_trigger_alloc("%s-dev%d", indio_dev->name, indio_dev->id) |
假设indio_dev->name为“icm20608”,indio_dev->id为0,那么触发器的名字就是“icm20608-
dev0”,这个也是稍后编写例程的时候定义的触发器名字,如下图所示:
当申请成功以后iio_trigger_alloc函数就会返回申请到的iio_trigger。也可以使用devm_iio_trigger_alloc函数申请触发器,这样在卸载驱动的时候就不用手动释放触发器了。
释放触发器
如果要释放触发器就使用iio_trigger_free,函数原型如下:
void iio_trigger_free(struct iio_trigger *trig)
函数参数和返回值含义如下:
- trig:要释放的触发器。
- 返回值:无。
注册触发器
触发器申请并初始化完成以后就需要向内核注册触发器,函数为devm_iio_trigger_register,函数原型如下:
int iio_trigger_register(struct iio_trigger *trig_info)
函数参数和返回值含义如下:
- trig_info:要注册的触发器。
- 返回值:0,成功;其他值,失败。
注销触发器
注销触发器使用iio_trigger_unregister函数,函数原型如下:
void iio_trigger_unregister(struct iio_trigger *trig_info)
函数参数和返回值含义如下:
- trig_info:要注销的触发器。
- 返回值:无。
IIO缓冲区
IIO缓冲区就是保存采集到的数据,用户空间可以直接通过访问字符设备/dev/iio:deviceX(X=0,1,2……)来读取缓冲区中的数据。编写驱动的时候需要先创建缓冲区,
函数为iio_triggered_buffer_setup,函数原型如下:
int iio_triggered_buffer_setup( struct iio_dev *indio_dev,
irqreturn_t (*h)(int irq, void *p),
irqreturn_t (*thread)(int irq, void *p),
const struct iio_buffer_setup_ops *setup_ops)
函数参数和返回值含义如下:
- indio_dev:需要创建缓冲的iio_dev。
- h:触发器中断上半部,上半部程序一定要简单,执行速度越快越好,一般就是提供捕获时间戳。可以直接使用IIO框架提供的iio_pollfunc_store_time函数。
- thread:触发器中断下半部,重要的处理就在这里面,此函数中需要将传感器中的数据和上半部获取到的时间戳一起推送到缓冲区中。
- setup_ops:缓冲区操作函数集,iio_buffer_setup_ops结构体内容如下:
如果设置为NULL,那么就会使用默认的操作集iio_triggered_buffer_setup_ops。
- 返回值:无。
一定要在调用iio_device_register函数注册IIO设备之前设置缓冲区。
向驱动程序添加触发缓冲功能
修改设备树
中断是最常用的触发方式,因为一般的传感器都有中断功能,当数据准备就绪或者指定事件发生以后就会产生中断通知SOC,此时SOC就可以读取传感器内部数据。首先要修改设备树,添加ICM20608中断引脚PA14的配置,打开stm32mp15-pinctrl.dtsi文件,PA14引脚配置如下:
示例代码 56.6.1.1 ICM20608 中断引脚配置
1 icm20608_pins_b: icm20608-0 {
2 pins {
3 pinmux = <STM32_PINMUX('A', 14, ANALOG)>;
4 bias-pull-up;
5 };
6 };
另外,打开stm32mp157d-atk.dts文件,修改icm20608节点,添加中断信息,内容如下:
示例代码 56.6.1.2 icm20608 节点
1 spidev: icm20608@0 {
2 compatible = "alientek,icm20608";
3 reg = <0>; /* CS #0 */
4 pinctrl-names = "default";
5 pinctrl-0 = <&icm20608_pins_b>;
6 interrupt-parent = <&gpioa>;
7 interrupts = <14 IRQ_TYPE_LEVEL_HIGH>;
8 spi-max-frequency = <80000000>;
9 };
第6行,设置中断父节点为gpioa,第7行设置PA14为高电平触发。
修改完成以后重新编译设备树,然后使用新的设备树启动linux内核。
驱动程序编写
接下来就是在之前编写好的ICM20608 IIO驱动程序中添加触发缓冲功能。
首先先添加两个宏定义,定义好溢出和读取的中断触发的情况。
然后添加扫描的掩码unsigned long icm20608_scan_masks数组,里面就是两种情况,全启动和全部启动,通过BIT来设置对应位进而用于初始化iio_dev的available_scan_masks成员变量。
之后编写触发器的下半部函数,这里就是用来处理中断的irqreturn_t类型的icm20608_trigger_handler,来从启动通道中读取数据并发入缓冲区。这里需要传入irq中断号,以及p这个iio_poll_func的结构体指针,传入后把p传给pf,从pf->indio_dev获取到iio_dev结构体指针indio_dev,然后iio_pric获取一下icm20608这个设备的设备结构体指针dev;mutex_lock上锁后开始处理,首先通过regmap_read判断数据是否准备就绪,就绪后通过regmap_bulk_read读取14个寄存器的值,然后iio_push_to_buffers_with_timestamp将获取到的数据推送到缓冲区中。
之后编写中断服务函数,irqreturn_t类型的iio_trigger_generic_data_rdy_poll函数,这里就是直接调用iio_trigger_poll。
之后编写触发器开关icm20608_trigger_set_state函数,这里需要传入iio_trigger结构体指针trig,以及bool值state,通过trig,由iio_trigger_get_drvdata来获取iio_dev结构体指针indio_dev,然后同样iio_pric获取到设备结构体,在上锁之后,通过state判断是否触发,state=1就通过regmap_write使能中断,反之则关闭中断,然后解锁。这个函数就是用于初始化iio_trigger_ops的set_trigger_state成员变量,也就是把这个函数给到iio_trigger_ops的.set_trigger_state。
最后写一下spi的probe函数,前面的都不变,然后设置iio_dev结构体指针indio_dev的各个成员变量,之后通过devm_iio_triggered_buffer_setup触发缓冲区设置,之后devm_iio_trigger_alloc申请trigger,并将regmap_get_device写到dev->trig->dev.parent,把dev->trig->ops设置为刚才写好的icm20608_trigger_ops操作集,最后iio_trigger_set_drvdata初始化;之后通过devm_iio_trigger_register注册触发器,并通过iio_trigger_get把dev->trig传给indio_dev;之后devm_request_irq初始化中断;最后iio_device_register注册iio_dev。
触发缓冲测试
驱动编写好以后重新编译加载,如果驱动运行正常,就会生成/dev/iio:deviceX(X=0,1,2…)
文件,如下图所示:
上图中的/dev/iio:device0就是ICM20608,只需要做简单的配置,就可以通过读取这个文件得到缓冲区中的数据。在读取数据之前,要先了解一下其他的配置文件。
缓冲区接口文件
缓冲区接口目录路径为:
/sys/bus/iio/devices/iio:device0/buffer |
如下图所示:
进入到上图中的buffer目录,就会有一些属性文件,如下图所示:
- data_available:指示数据是否有效,为1时有效,为0时无效。
- enable:使能缓冲区,写入1使能缓冲区,写0关闭缓冲区。
- length:缓冲区大小,也就是可以存储数据的数量。
- watermark:阻塞读取的时候只有数据量大于watermark的时候才能读取,非阻塞读取的时候不受watermark影响。
可以指定哪些通道的数据可以被推送到缓冲区,也就是开启指定的扫描元素。/sys/bus/iio/devices/iio:device0/scan_elements目录下就是ICM20608的扫描元素,如下图所示:
上图中一共有7组扫描元素,分别为加速度计的X、Y、Z轴,陀螺仪的X、Y、Z轴,温度。每个通道有3个文件,就以加速度计的X轴为例:
- in_accel_x_en:从后缀的“en”可以看出,这个文件是加速度计X轴通道使能文件,写入1 使能加速度计X轴。
- in_accel_x_index:通道索引值,这个索引值就是在驱动中配置通道的时候设置的索引值,比如驱动中加速度X轴索引值为INV_ICM20608_SCAN_ACCL_X=0,查看in_accel_x_index文件内容,如下图所示:
- in_accel_x_type:此文件描述数据存储格式,这个存储格式就是在驱动程序中配置通道的时候设置的存储格式。type文件内容格式为[be|le]:[s|u]bits/storagebitsXrepeat[>>shift],各段的含义如下:
- be或le:be表示大端、le表示小端。
- s或u:s表示有符号类型,u表示无符号类型。
- bits:有效数据位数。
- storagebits:存储位数。
- repeat:位/存储重复数量。
- shift:屏蔽掉未使用的位之前应该移动的位数,当有效位数等于存储位数的时候shift就是0。
查看in_accel_x_type文件内容,如下图所示:
从上图可以看出,in_accel_x_type文件内容为“be:s16/16>>0”,也就是大端模式、有
符号类型、有效位数16位,存储位数16位,右移0。
触发器接口文件
触发器目录
在驱动里面创建触发器以后就会有/sys/bus/iio/devices/triggerX(X=0,1,2……)目录,如下图所示:
上图中的trigger0就是ICM20608的中断触发器目录,进入此目录中,目录下的文件如下图所示:
上图中的“name”文件就是此触发器的名字。
IIO设备所使用的触发器
一个IIO设备它可以使用多个触发器,只是本章教程只给ICM20608创建了一个中断触发器,其实还有定时器触发器、sysfs触发器等,这些触发器也可以赋给ICM20608。所以IIO设备也会有一个目录来保存自己当前正在使用的触发器,进入ICM20608对应的iio:device0这个目录中,也就是ICM20608的IIO设备目录里面,如下图所示:
上图中也有个名为“trigger”的目录,这个目录就是 ICM20608 正在使用的触发器,此目录下面的文件如下图所示:
可以看出,只有一个名为“current_trigger”的文件,此文件保存着当前ICM20608正在使用的触发器,使用“cat”命令查看其内容,如下图所示:
从上图可以看出,此时ICM20608正在使用的触发器是“icm20608-dev0”,也就是在驱动里面创建的中断触发器。如果要使用其他的触发器,只需要将对应的触发器名字写入到current_trigger文件中即可,比如要将当前触发器改为“test_trigger”,命令如下:
echo test_trigger > current_trigger |
缓冲区数据读取
读取数据之前需要配置缓冲区和触发器,首先使能各个扫描元素,也就是通道,输入如下命令:
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_x_en echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_y_en echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_z_en echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_anglvel_x_en echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_anglvel_y_en echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_anglvel_z_en echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_temp_en |
接下来设置ICM20608所使用的触发器,虽然驱动已经默认设置ICM20608触发器使用icm20608-dev0,但是为了完整,流程还是要走一下的,命令如下:
echo icm20608-dev0 > /sys/bus/iio/devices/iio:device0/trigger/current_trigger |
最后就是设置缓冲区长度,并且开启缓冲区,命令如下:
echo 14 > /sys/bus/iio/devices/iio:device0/buffer/length echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable |
配置完成以后就可以读取/dev/iio:device0文件,得到缓冲区中的数据,这里可以使用cat命令查看一下原始值,命令如下:
hexdump /dev/iio\:device0 |
结果如下图所示:
上图中的内容就是ICM20608缓冲区数据,现在看起来杂乱无章,接下来就编写测试APP,在APP里面处理这些数据。
触发缓冲测试APP编写测试
触发缓冲区APP测试
首先定义icm20608_dev这个设备结构体,其中就是把原始的数据存入unsigned char数组data[14],然后就是各种加速度计、陀螺仪的calibbias、raw、scale和act,以及温度的offset、rawscale和act。
之后定义一个无返回值的icm20608_trigger_set的函数,用来进行相关触发设置,在其中通过system命令代替之前手动输入的使能命令。
之后编写file_data_read来读取指定文件内容,定义FILE指针data_stream,通过fopen打开文件后放到data_stream中,然后通过fscanf来读取,如果读到了EOF就用fseek把文件指针重新放到文件头然后fclose关闭文件。
之后编写icm20608_read读取数据,需要传入icm20608_dev的结构体指针dev,通过file_data_read来读取iio_dev:device0中的3个scalse文件,然后通过atof把字符串转为float存入dev对应的成员变量;之后通过read直接连着读取14个寄存器获取raw值,将数据存入对应的dev成员变量中;最后通过act和raw的计算公式(温度是不一样的要用offset)计算得到实际值存取对应的dev成员变量。
最后编写main函数,这里argc是2个参数,首先调用icm20608_trigger_set配置触发缓冲区的设置,然后open来打开设备(argv[1]),然后在while中轮询读取按键,通过icm20608_read读取,读取成功后printf打印计算得到的数值,最后每个while都usleep(100000)来延时100ms。
测试APP
最后输入如下命令编译icm20608_triggerAPP.c文件:
arm-none-linux-gnueabihf-gcc icm20608_triggerAPP.c -o icm20608_triggerAPP |
编译完成以后将icm20608_triggerAPP发送到开发板根文件系统中,加载驱动文件,输入如
下命令运行测试APP:
./icm20608_triggerAPP /dev/iio\:device0 |
如果测试APP运行正常,那么就会打印出ICM20608 的陀螺仪、加速度计、温度的原始值以及实际值,如下图所示:
从上图可以看出,APP运行成功,驱动也工作成功。
总结
IIO子系统,主要就是为了ADC类的传感器准备,通过这个驱动框架把模拟量转为数字量,之后通过通信接口传给SOC。
IIO的基本驱动框架就是IIC或者SPI的通信,可以再进一步结合刚学过的Regmap来编写。
驱动编写
驱动的编写主要就是在设备结构体中,加上IIC(或者SPI)的结构体,加上regmap结构体,regmap_config结构体再加一个mutex互斥锁就好;之后需要配置iio_chan_spec来配置其中的成员变量,进而配好ADC转换的内容;寄存器的读写就可以直接regmap_read和regmap_write;probe函数就是之前的regmap的配置,先申请iio_dev的内存devm_iio_device_alloc,然后iio_priv获取设备结构体地址,根据总线,如果spi就spi_set_drvdata获取,然后一次配好indio_dev的成员变量并通过iio_device_register注册indio_dev,之后就全是regmap的内容了;remove就是spi_get_drvdata获取一下indio_dev之后iio_priv获取设备结构体,之后regmap_exit和iio_device_unregister就可以了。重要的read_raw和write_raw后面讲解。
read_raw之中,就是同样先iio_priv获取结构体,然后通过mask判断通道,每个mask有对应的操作;icm20608_read_channel_data基本就是icm20608_sensor_show读取数据(里面是regmap_bulk_read读取寄存器然后把读到的存到val);最关键的icm20608_read_raw就是通过mask判断,如果是raw通道就调用icm20608_read_channel_data,如果是scale通道就是icm20608_read_onereg(本质就是regmap_read)读取对应寄存器然后设置val和val2;offset通道就直接设置就好了;calibbias通道就需要icm20608_sensor_show来读取校准值。
至于write_raw,同样的也是通过mask判断来进行寄存器的各种参数设置;icm20608_sensor_set就是类似的调用regmap_bulk_write来写寄存器;icm20608_write_gyro_scale就是for遍历gyro_scale_icm20608数组并把对应的数值根据传入的参数来通过regmap_write设置对应的量程(icm20608_write_accel_scale同理);icm20608_write_raw同样,通过mask判断,如果是陀螺仪和加速度计就调用刚才两个对应函数设置;如果是calibbias,就是icm20608_sensor_set来配置。
最后还有一个icm20608_write_raw_get_fmt函数,这是用户空间的写数据格式,也就是设置scale相关小数点后的操作,配置放大倍数;通过mask判断然后根据给的宏定义return出来就好。
以上最后写进iio_info就大功告成。
测试编写
首先要定义好iio框架对应的文件路径存到file_path这个char指针数组之中,以及定义好一个enum对应之前的文件路径索引,设备结构体就是定义好act、offset、raw、scale和calibbias。
读取指定文件的函数file_data_read,就是fopen打开文件之后,由fscanf读取,EOF就fseek把指针拉回文件头然后fclose关闭就好。
读取传感器的数据sensor_read,就是通过file_data_read,把对应的文件和文件索引传入,然后atof或者atoi把字符串转为数组传入dev的对应成员变量。
main函数中,argc就1个参数,直接进入while中通过sensor_read读取数据,读取到了就全部printf出来就好。
进阶
触发缓冲区,其实就是加入中断触发信号来读取数据,这里对驱动程序和测试APP的修改主要就是触发器的4个API以及IIO缓冲区的iio_trigger对应的缓冲区操作函数,以及还要配置一下中断,在设备树中添加对应节点。