门户网站部署方案优质的聊城网站建设

当前位置: 首页 > news >正文

门户网站部署方案,优质的聊城网站建设,建设银行个人查询,电商网站活动推广工业场合里面也有大量的模拟量和数字量之间的转换#xff0c;也就是常说的ADC和DAC。而且随着手机、物联网、工业物联网和可穿戴设备的爆发#xff0c;传感器的需求只持续增强。比如手机或者手环里面的加速度计、光传感器、陀螺仪、气压计、磁力计等#xff0c;这些传感器本…工业场合里面也有大量的模拟量和数字量之间的转换也就是常说的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需要读取的通道。valval2对于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_ACCELX、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-249scan_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_xcalibias这两个文件。 通道属性的命名也就是中文件的命名模式为[direction][type][index][modifier]_[info_mask]依次来看一下这些命名组织模块 direction为属性对应的方向iio_direction结构体定义了方向内容如下 可以看出就有两个方向in和out。 type也就是配置通道的时候type值type对应的字符可以参考iio_chan_type_name_spec如下 所以当通道的type设置为IIO_ACCEL的时候对应的名字就是“accel”。 index索引如果配置通道的时候设置了indexed1那么就会使用通道的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/10000000000.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_MICROdefault的就是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_scalein_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(X0,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: icm206080 { 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这个设备的设备结构体指针devmutex_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判断是否触发state1就通过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(X0,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_X0查看in_accel_x_index文件内容如下图所示 in_accel_x_type此文件描述数据存储格式这个存储格式就是在驱动程序中配置通道的时候设置的存储格式。type文件内容格式为[be|le]:[s|u]bits/storagebitsXrepeat[shift]各段的含义如下 be或lebe表示大端、le表示小端。s或us表示有符号类型u表示无符号类型。bits有效数据位数。storagebits存储位数。repeat位/存储重复数量。shift屏蔽掉未使用的位之前应该移动的位数当有效位数等于存储位数的时候shift就是0。
查看in_accel_x_type文件内容如下图所示 从上图可以看出in_accel_x_type文件内容为“be:s16/160”也就是大端模式、有 符号类型、有效位数16位存储位数16位右移0。 触发器接口文件 触发器目录 在驱动里面创建触发器以后就会有/sys/bus/iio/devices/triggerX(X0,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_writeprobe函数就是之前的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和val2offset通道就直接设置就好了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对应的缓冲区操作函数以及还要配置一下中断在设备树中添加对应节点。