IIO子系统(Linux驱动开发篇)

1、原因

  • 内部 ADC 将原始的模拟数据转换为数字量,然后通过其他的通信接口,比如 IIC、SPI等传输给SOC
  • 当你使用的传感器的本质是ADC或者DAC,优先考虑使用IIO驱动框架

2、IIO子系统

  • 主要介绍
    设备结构体iio_dev,(描述一个具体 IIO 设备)====申请注册
    以及内部包含的主要成员
    iio_info、(描述ADC的通道信息)
    iio_chan_spec结构体(用户空间对设备的具体操作最终都会反映到iio_inf里面)
/*
@	定义在include/linux/iio/iio.h文件
@	iio_dev 结构体来描述一个具体 IIO 设备   
*/
 struct iio_dev { 
     int               id; 
  
     int               modes; 
     int               currentmode; /*当前模式*/
     struct device     dev; 
  
     struct iio_event_interface  *event_interface; 
  
     struct iio_buffer           *buffer; /*缓冲区*/
     struct list_head            buffer_list; /*当前匹配的缓冲区列表*/
     int                     scan_bytes; /*捕获到,并且提供给缓冲区的字节数*/
     struct mutex                mlock; 
  
     const unsigned long        *available_scan_masks; /*为可选的扫描位掩码,使用触发缓冲区的时候可以通过设置掩码来确定使能哪些通道,使能以后的通道会将捕获到的数据发送到IIO缓冲区*/
     unsigned                  masklength; 
     const unsigned long        *active_scan_mask; /*为缓冲区已经开启的通道掩码。只有这些使能了的通道数据才能被发送到缓冲区*/
     bool                      scan_timestamp; /*为扫描时间戳,如果使能以后会将捕获时间戳放到缓冲区里面*/
     unsigned                  scan_index_timestamp; 
     struct iio_trigger          *trig; /*,trig为 IIO设备当前触发器,当使用缓冲模式的时候*/
     struct iio_poll_func         *pollfunc; /*一个函数,在接收到的触发器上运行*/
  
     struct iio_chan_spec const  *channels; /*channels 为 IIO 设备通道,*/
     int                     num_channels; /*IIO设备的通道数*/
  
     struct list_head            channel_attr_list; 
     struct attribute_group       chan_attr_group; 
     const char                *name; /*为 IIO设备名字*/
    const struct iio_info        *info; /*nfo为 iio_info结构体类型,这个结构体里面有很多函数,需要驱动开发人员编写,非常重要*/
    struct mutex                info_exist_lock; 
    const struct iio_buffer_setup_ops   *setup_ops; /*里面都是一些回调函数,在使能或禁用缓冲区的时候会调用这些函数*/
    struct cdev               chrdev; /*chrdev为字符设备,由IIO 内核创建*/
.. 
}; 
/*
@	在使用之前要先申请 iio_dev,
@	sizeof_priv: 私有数据内存空间大小,
@	返回值:如果申请成功就返回 iio_dev首地址,如果失败就返回NULL
@	申请成功以后使用 iio_priv函数来得到自定义的设备结构体变量首地址。   一般iio_device_alloc和iio_priv之间的配合
*/
struct iio_dev *iio_device_alloc(int sizeof_priv) 
/*
@	如果要释放iio_dev
@	indio_dev:需要释放的 iio_dev。
@	返回值:无
*/
void iio_device_free(struct iio_dev *indio_dev) 
/*
@	前面分配好 iio_dev 以后就要初始化各种成员变量,初始化完成以后就需要将 iio_dev注册到内核中
@	indio_dev:需要注册的 iio_dev
@	返回值:0,成功;其他值,失败
*/
int iio_device_register(struct iio_dev *indio_dev) 
/*
@	如果要注销iio_dev使用iio_device_unregister函数
@	indio_dev:需要注销的 iio_dev。
@	返回值:0,成功;其他值,失败
*/
void iio_device_unregister(struct iio_dev *indio_dev) 




/*
@	 include/linux/iio/iio.h 中
@	 iio_info 结构体 
*/
 struct iio_info { 
     struct module           *driver_module; 
     struct attribute_group      *event_attrs; 
     const struct attribute_group    *attrs; 
  

/*
@	分别为 read_raw 和write_raw 函数,这两个函数就是最终读写设备内部数据的操作函数,需要程序编写人员去实现的。
@	indio_dev:需要读写的 IIO 设备。
@	chan:需要读取的通道。
@	val,val2:对于 read_raw 函数来说 val 和 val2 这两个就是应用程序从内核空间读取到数据,一般就是传感器指定通道值,或者传感器的量程、分辨率等
@	val和val2共同组成具体值,val是整数部分,val2是小数部分
*/
     int (*read_raw)(struct iio_dev *indio_dev, 
             struct iio_chan_spec const *chan, 
             int *val, 
             int *val2, 
             long mask); 
... 
  
     int (*write_raw)(struct iio_dev *indio_dev, 
              struct iio_chan_spec const *chan, 
              int val, 
              int val2, 
              long mask); 
  
     int (*write_raw_get_fmt)(struct iio_dev *indio_dev, 
              struct iio_chan_spec const *chan, 
              long mask); 
... 
 }; 




/*
@	支持8个通道,支持8路采集数据
@	Linux内核使用iio_chan_spec 结构体来描述通道
@	在include/linux/iio/iio.h 文件中
*/
 struct iio_chan_spec { 
     enum iio_chan_type   type; 
     int               channel; 
     int               channel2; 
     unsigned long         address; 
     int               scan_index; 
     struct { 
         char          sign; 
         u8          realbits; 
         u8          storagebits; 
         u8          shift; 
         u8          repeat; 
         enum iio_endian   endianness; 
     } scan_type; 
     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; 
     unsigned int          num_event_specs; 
     const struct iio_chan_spec_ext_info *ext_info; 
     const char          *extend_name; 
     const char          *datasheet_name; 
     unsigned            modified:1; 
     unsigned           indexed:1; 
     unsigned            output:1; 
     unsigned            differential:1; 
 }; 

3、IIO驱动框架

  • 首先上一个普通的字符设备驱动框架
 /* *******************************************************************SPI 驱动框架 
  * @description    : spi 驱动的 probe 函数,当驱动与 
  *                      设备匹配以后此函数就会执行 
  * @param - spi    : spi 设备 
  * @return            : 0,成功;其他值,失败 
   */  
 static int xxx_probe(struct spi_device *spi) 
 { 
    return 0; 
 } 
  
 /* 
  * @description  : spi 驱动的 remove 函数,移除 spi 驱动的时候此函数会执行 
  * @param - spi    : spi 设备 
  * @return          : 0,成功;其他负值,失败 
  */ 
 static int xxx_remove(struct spi_device *spi) 
 { 
    return 0; 
 } 
  
 /* 传统匹配方式 ID 列表 */ 
 static const struct spi_device_id xxx_id[] = { 
    {"alientek,xxx", 0}, 
    {} 
 }; 
  
 /* 设备树匹配列表 */ 
 static const struct of_device_id xxx_of_match[] = { 
    { .compatible = "alientek,xxx" }, 
    { /* Sentinel */ } 
 }; 
  
 /* SPI 驱动结构体 */ 
 static struct spi_driver xxx_driver = { 
    .probe = xxx_probe, 
    .remove = xxx_remove, 
    .driver = { 
          .owner = THIS_MODULE, 
          .name = "xxx", 
          .of_match_table = xxx_of_match, 
      }, 
    .id_table = xxx_id, 
 }; 
  
 /* 
   * @description    : 驱动入口函数 
   * @param          : 无 
   * @return         : 无 
   */ 
 static int __init xxx_init(void) 
 { 
    return spi_register_driver(&xxx_driver); 
 } 
  
 /* 
  * @description    : 驱动出口函数 
  * @param          : 无 
  * @return         : 无 
  */ 
 static void __exit xxx_exit(void) 
 { 
    spi_unregister_driver(&xxx_driver); 
 } 
  
 module_init(xxx_init); 
 module_exit(xxx_exit); 
 MODULE_LICENSE("GPL"); 
 MODULE_AUTHOR("ALIENTEK"); 
  • 在此基础上添加IIO框架
 /* 自定义设备结构体 */ 
 struct xxx_dev { 
     struct spi_device *spi;          /* spi 设备 */ 
     struct regmap *regmap;                /* regmap   */ 
     struct regmap_config regmap_config;  
     struct mutex lock; 
 }; 
  
 /* 
    * 通道数组 
   */ 
  static const struct iio_chan_spec xxx_channels[] = { 
   
  }; 
   
  /* 
    * @description    : 读函数,当读取 sysfs 中的文件的时候最终此函数会执行,
    *                      :此函数里面会从传感器里面读取各种数据,然后上传给应用。
    * @param - indio_dev  : IIO 设备 
    * @param - chan    : 通道 
    * @param - val      : 读取的值,如果是小数值的话,val 是整数部分。 
    * @param - val2     : 读取的值,如果是小数值的话,val2 是小数部分。 
    * @param - mask     : 掩码。 
    * @return            : 0,成功;其他值,错误 
    */ 
  static int xxx_read_raw(struct iio_dev *indio_dev, 
                 struct iio_chan_spec const *chan, 
                 int *val, int *val2, long mask) 
  { 
      return 0; 
  }    
   
  /* 
    * @description        : 写函数,当向 sysfs 中的文件写数据的时候最终此函数
    *                         :会执行,一般在此函数里面设置传感器,比如量程等。
    * @param - indio_dev   : IIO 设备 
    * @param - chan         : 通道 
    * @param - val          : 应用程序写入值,如果是小数的话,val 是整数部分。 
    * @param - val2         : 应用程序写入值,如果是小数的话,val2 是小数部分。 
    * @return               : 0,成功;其他值,错误 
    */ 
  static int xxx_write_raw(struct iio_dev *indio_dev, 
                  struct iio_chan_spec const *chan, 
                  int val, int val2, long mask) 
  { 
      return 0; 
  } 
   
  /* 
    * @description    : 用户空间写数据格式,比如我们在用户空间操作 sysfs 来设 
    *                      :置传感器的分辨率,如果分辨率带小数,那么这个小数传递到 
    *                       : 内核空间应该扩大多少倍,此函数就是用来设置这个的。 
    * @param - indio_dev  : iio_dev 
    * @param - chan     : 通道 
    * @param - mask     : 掩码 
    * @return            : 0,成功;其他值,错误 
    */ 
  static int xxx_write_raw_get_fmt(struct iio_dev *indio_dev, 
                   struct iio_chan_spec const *chan, long mask) 
  { 
      return 0; 
  } 
   
  /* 
   * iio_info 结构体变量 
   */ 
  static const struct iio_info xxx_info = { 
      .read_raw       = xxx_read_raw, 
      .write_raw      = xxx_write_raw, 
      .write_raw_get_fmt = &xxx_write_raw_get_fmt,  
  }; 
   
  /* 
    * @description  : spi 驱动的 probe 函数,当驱动与 
    *                    设备匹配以后此函数就会执行 
    * @param - spi  : spi 设备 
    *  
    */     
  static int xxx_probe(struct spi_device *spi) 
  { 
     int ret; 
     struct xxx_dev *data; 
     struct iio_dev *indio_dev; 
  
     /*  1、申请 iio_dev 内存 */ 
     indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*data)); 
     if (!indio_dev) 
         return -ENOMEM; 
  
     /* 2、获取 xxx_dev 结构体地址 */ 
     data = iio_priv(indio_dev);  
     data->spi = spi; 
     spi_set_drvdata(spi, indio_dev); 
     mutex_init(&data->lock); 
  
     /* 3、初始化 iio_dev 成员变量 */ 
     indio_dev->dev.parent = &spi->dev; 
     indio_dev->info = &xxx_info; 
     indio_dev->name = "xxx"; 
     indio_dev->modes = INDIO_DIRECT_MODE;   /* 直接模式 / 
     indio_dev->channels = xxx_channels; 
     indio_dev->num_channels = ARRAY_SIZE(xxx_channels); 
  
     iio_device_register(indio_dev); 
  
     /* 4、regmap 相关设置 */ 
  
     /* 5、SPI 相关设置*/ 
      
     /* 6、芯片初始化 */ 
  
     return 0; 
  
 } 
  
 /* 
  * @description    : spi 驱动的 remove 函数,移除 spi 驱动的时候此函数会执行 
  * @param - spi    : spi 设备 
  * @return         : 0,成功;其他负值,失败 
  */ 
 static int xxx_remove(struct spi_device *spi) 
 { 
     struct iio_dev *indio_dev = spi_get_drvdata(spi); 
     struct xxx_dev *data; 
      
     data = iio_priv(indio_dev); ; 
  
     /* 1、其他资源的注销以及释放 */  
      
     /* 2、注销 IIO */ 
     iio_device_unregister(indio_dev);
      
     return 0; 
 } 
  • 之前的文章说过简化IIC和SPI我们统一的提出了一个Regmap子系统主要就是描述怎么利用Linux内核提供的一些API,在总线(IIC、SPI等)上来对设备进行读写操作(简化底层代码的一种方法)。

4、实战演练1(SPI)

  • 对象imx6ull开发板、ICM206086轴陀螺仪(IIO框架简化ADC驱动)、SPI总线外设(Regmap简化底层代码)

  • icm20608reg.h文件

#ifndef ICM20608_H
#define ICM20608_H
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: icm20608reg.h
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: ICM20608寄存器地址描述头文件
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/9/2 左忠凯创建
***************************************************************/
#define ICM20608G_ID			0XAF	/* ID值 */
#define ICM20608D_ID			0XAE	/* ID值 */

/* ICM20608寄存器 
 *复位后所有寄存器地址都为0,除了
 *Register 107(0X6B) Power Management 1 	= 0x40
 *Register 117(0X75) WHO_AM_I 				= 0xAF或0xAE
 */
/* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
#define	ICM20_SELF_TEST_X_GYRO		0x00
#define	ICM20_SELF_TEST_Y_GYRO		0x01
#define	ICM20_SELF_TEST_Z_GYRO		0x02
#define	ICM20_SELF_TEST_X_ACCEL		0x0D
#define	ICM20_SELF_TEST_Y_ACCEL		0x0E
#define	ICM20_SELF_TEST_Z_ACCEL		0x0F

/* 陀螺仪静态偏移 */
#define	ICM20_XG_OFFS_USRH			0x13
#define	ICM20_XG_OFFS_USRL			0x14
#define	ICM20_YG_OFFS_USRH			0x15
#define	ICM20_YG_OFFS_USRL			0x16
#define	ICM20_ZG_OFFS_USRH			0x17
#define	ICM20_ZG_OFFS_USRL			0x18

#define	ICM20_SMPLRT_DIV			0x19
#define	ICM20_CONFIG				0x1A
#define	ICM20_GYRO_CONFIG			0x1B
#define	ICM20_ACCEL_CONFIG			0x1C
#define	ICM20_ACCEL_CONFIG2			0x1D
#define	ICM20_LP_MODE_CFG			0x1E
#define	ICM20_ACCEL_WOM_THR			0x1F
#define	ICM20_FIFO_EN				0x23
#define	ICM20_FSYNC_INT				0x36
#define	ICM20_INT_PIN_CFG			0x37
#define	ICM20_INT_ENABLE			0x38
#define	ICM20_INT_STATUS			0x3A

/* 加速度输出 */
#define	ICM20_ACCEL_XOUT_H			0x3B
#define	ICM20_ACCEL_XOUT_L			0x3C
#define	ICM20_ACCEL_YOUT_H			0x3D
#define	ICM20_ACCEL_YOUT_L			0x3E
#define	ICM20_ACCEL_ZOUT_H			0x3F
#define	ICM20_ACCEL_ZOUT_L			0x40

/* 温度输出 */
#define	ICM20_TEMP_OUT_H			0x41
#define	ICM20_TEMP_OUT_L			0x42

/* 陀螺仪输出 */
#define	ICM20_GYRO_XOUT_H			0x43
#define	ICM20_GYRO_XOUT_L			0x44
#define	ICM20_GYRO_YOUT_H			0x45
#define	ICM20_GYRO_YOUT_L			0x46
#define	ICM20_GYRO_ZOUT_H			0x47
#define	ICM20_GYRO_ZOUT_L			0x48

#define	ICM20_SIGNAL_PATH_RESET		0x68
#define	ICM20_ACCEL_INTEL_CTRL 		0x69
#define	ICM20_USER_CTRL				0x6A
#define	ICM20_PWR_MGMT_1			0x6B
#define	ICM20_PWR_MGMT_2			0x6C
#define	ICM20_FIFO_COUNTH			0x72
#define	ICM20_FIFO_COUNTL			0x73
#define	ICM20_FIFO_R_W				0x74
#define	ICM20_WHO_AM_I 				0x75

/* 加速度静态偏移 */
#define	ICM20_XA_OFFSET_H			0x77
#define	ICM20_XA_OFFSET_L			0x78
#define	ICM20_YA_OFFSET_H			0x7A
#define	ICM20_YA_OFFSET_L			0x7B
#define	ICM20_ZA_OFFSET_H			0x7D
#define	ICM20_ZA_OFFSET_L 			0x7E

#endif
  • icm20608.c驱动文件
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: icm20608.c
作者	  	: 正点原子Linux团队
版本	   	: V1.0
描述	   	: ICM20608 SPI驱动程序
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2021/03/22 正点原子Linux团队创建
			  V1.1 2021/08/10 
			  使用regmap读写SPI外设内部寄存器。

			  V1.2 2021/08/13 
			  使用IIO框架,参考bma220_spi.c
***************************************************************/
#include <linux/spi/spi.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/errno.h>
#include <linux/platform_device.h>
#include "icm20608reg.h"
#include <linux/gpio.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/regmap.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/iio/buffer.h>
#include <linux/iio/trigger.h>
#include <linux/iio/triggered_buffer.h>
#include <linux/iio/trigger_consumer.h>
#include <linux/unaligned/be_byteshift.h>

#define ICM20608_NAME	"icm20608"
#define ICM20608_TEMP_OFFSET	     0
#define ICM20608_TEMP_SCALE		     326800000

#define ICM20608_CHAN(_type, _channel2, _index)                    \
	{                                                             \
		.type = _type,                                        \
		.modified = 1,                                        \
		.channel2 = _channel2,                                \
		.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |	      \
				      BIT(IIO_CHAN_INFO_CALIBBIAS),   \
		.scan_index = _index,                                 \
		.scan_type = {                                        \
				.sign = 's',                          \
				.realbits = 16,                       \
				.storagebits = 16,                    \
				.shift = 0,                           \
				.endianness = IIO_BE,                 \
			     },                                       \
	}

/* 
 * ICM20608的扫描元素,3轴加速度计、
 * 3轴陀螺仪、1路温度传感器,1路时间戳 
 */
enum inv_icm20608_scan {
	INV_ICM20608_SCAN_ACCL_X,
	INV_ICM20608_SCAN_ACCL_Y,
	INV_ICM20608_SCAN_ACCL_Z,
	INV_ICM20608_SCAN_TEMP,
	INV_ICM20608_SCAN_GYRO_X,
	INV_ICM20608_SCAN_GYRO_Y,
	INV_ICM20608_SCAN_GYRO_Z,
	INV_ICM20608_SCAN_TIMESTAMP,
};

struct icm20608_dev {
	struct spi_device *spi;		/* spi设备 */
	struct regmap *regmap;				/* regmap */
	struct regmap_config regmap_config;	
	struct mutex lock;
};

/*
 * icm20608陀螺仪分辨率,对应250、500、1000、2000,计算方法:
 * 以正负250度量程为例,500/2^16=0.007629,扩大1000000倍,就是7629
 */
static const int gyro_scale_icm20608[] = {7629, 15258, 30517, 61035};

/* 
 * icm20608加速度计分辨率,对应2、4、8、16 计算方法:
 * 以正负2g量程为例,4/2^16=0.000061035,扩大1000000000倍,就是61035
 */
static const int accel_scale_icm20608[] = {61035, 122070, 244140, 488281};

/*
 * icm20608通道,1路温度通道,3路陀螺仪,3路加速度计
 */
static const struct iio_chan_spec icm20608_channels[] = {
	/* 温度通道 */
	{
		.type = IIO_TEMP,
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW)
				| BIT(IIO_CHAN_INFO_OFFSET)
				| BIT(IIO_CHAN_INFO_SCALE),
		.scan_index = INV_ICM20608_SCAN_TEMP,
		.scan_type = {
				.sign = 's',
				.realbits = 16,
				.storagebits = 16,
				.shift = 0,
				.endianness = IIO_BE,
			     },
	},

	ICM20608_CHAN(IIO_ANGL_VEL, IIO_MOD_X, INV_ICM20608_SCAN_GYRO_X),	/* 陀螺仪X轴 */
	ICM20608_CHAN(IIO_ANGL_VEL, IIO_MOD_Y, INV_ICM20608_SCAN_GYRO_Y),	/* 陀螺仪Y轴 */
	ICM20608_CHAN(IIO_ANGL_VEL, IIO_MOD_Z, INV_ICM20608_SCAN_GYRO_Z),	/* 陀螺仪Z轴 */

	ICM20608_CHAN(IIO_ACCEL, IIO_MOD_Y, INV_ICM20608_SCAN_ACCL_Y),	/* 加速度X轴 */
	ICM20608_CHAN(IIO_ACCEL, IIO_MOD_X, INV_ICM20608_SCAN_ACCL_X),	/* 加速度Y轴 */
	ICM20608_CHAN(IIO_ACCEL, IIO_MOD_Z, INV_ICM20608_SCAN_ACCL_Z),	/* 加速度Z轴 */
};

/*
 * @description	: 读取icm20608指定寄存器值,读取一个寄存器
 * @param - dev:  icm20608设备
 * @param - reg:  要读取的寄存器
 * @return 	  :   读取到的寄存器值
 */
static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
{
	u8 ret;
	unsigned int data;

	ret = regmap_read(dev->regmap, reg, &data);
	return (u8)data;
}

/*
 * @description	: 向icm20608指定寄存器写入指定的值,写一个寄存器
 * @param - dev:  icm20608设备
 * @param - reg:  要写的寄存器
 * @param - data: 要写入的值
 * @return   :    无
 */	
static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
{
	regmap_write(dev->regmap,  reg, value);
}

/*
 * @description  	: ICM20608内部寄存器初始化函数 
 * @param - spi 	: 要操作的设备
 * @return 			: 无
 */
void icm20608_reginit(struct icm20608_dev *dev)
{
	u8 value = 0;
	
	icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x80);
	mdelay(50);
	icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x01);
	mdelay(50);

	value = icm20608_read_onereg(dev, ICM20_WHO_AM_I);
	printk("ICM20608 ID = %#X\r\n", value);	

	icm20608_write_onereg(dev, ICM20_SMPLRT_DIV, 0x00); 	/* 输出速率是内部采样率		*/
	icm20608_write_onereg(dev, ICM20_GYRO_CONFIG, 0x18); 	/* 陀螺仪±2000dps量程 		*/
	icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG, 0x18); 	/* 加速度计±16G量程 		*/
	icm20608_write_onereg(dev, ICM20_CONFIG, 0x04); 		/* 陀螺仪低通滤波BW=20Hz 	*/
	icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz 	*/
	icm20608_write_onereg(dev, ICM20_PWR_MGMT_2, 0x00); 	/* 打开加速度计和陀螺仪所有轴 	*/
	icm20608_write_onereg(dev, ICM20_LP_MODE_CFG, 0x00); 	/* 关闭低功耗 				*/
	icm20608_write_onereg(dev, ICM20_INT_ENABLE, 0x01);		/* 使能FIFO溢出以及数据就绪中断	*/
}

/*
  * @description  	: 设置ICM20608传感器,可以用于陀螺仪、加速度计设置
  * @param - dev	: icm20608设备 
  * @param - reg  	: 要设置的通道寄存器首地址。
  * @param - anix  	: 要设置的通道,比如X,Y,Z。
  * @param - val  	: 要设置的值。
  * @return			: 0,成功;其他值,错误
  */
static int icm20608_sensor_set(struct icm20608_dev *dev, int reg,
				int axis, int val)
{
	int ind, result;
	__be16 d = cpu_to_be16(val);

	ind = (axis - IIO_MOD_X) * 2;
	result = regmap_bulk_write(dev->regmap, reg + ind, (u8 *)&d, 2);
	if (result)
		return -EINVAL;

	return 0;
}

/*
  * @description  	: 读取ICM20608传感器数据,可以用于陀螺仪、加速度计、温度的读取
  * @param - dev	: icm20608设备 
  * @param - reg  	: 要读取的通道寄存器首地址。
  * @param - anix  	: 需要读取的通道,比如X,Y,Z。
  * @param - val  	: 保存读取到的值。
  * @return			: 0,成功;其他值,错误
  */
static int icm20608_sensor_show(struct icm20608_dev *dev, int reg,
				   int axis, int *val)
{
	int ind, result;
	__be16 d;

	ind = (axis - IIO_MOD_X) * 2;
	result = regmap_bulk_read(dev->regmap, reg + ind, (u8 *)&d, 2);
	if (result)
		return -EINVAL;
	*val = (short)be16_to_cpup(&d);

	return IIO_VAL_INT;
}

/*
  * @description  		: 读取ICM20608陀螺仪、加速度计、温度通道值
  * @param - indio_dev	: iio设备 
  * @param - chan  		: 通道。
  * @param - val  		: 保存读取到的通道值。
  * @return				: 0,成功;其他值,错误
  */
static int icm20608_read_channel_data(struct iio_dev *indio_dev,
					 struct iio_chan_spec const *chan,
					 int *val)
{
	struct icm20608_dev *dev = iio_priv(indio_dev);
	int ret = 0;

	switch (chan->type) {
	case IIO_ANGL_VEL:	/* 读取陀螺仪数据 */
		ret = icm20608_sensor_show(dev, ICM20_GYRO_XOUT_H, chan->channel2, val);  /* channel2为X、Y、Z轴 */
		break;
	case IIO_ACCEL:		/* 读取加速度计数据 */
		ret = icm20608_sensor_show(dev, ICM20_ACCEL_XOUT_H, chan->channel2, val); /* channel2为X、Y、Z轴 */
		break;
	case IIO_TEMP:		/* 读取温度 */
		ret = icm20608_sensor_show(dev, ICM20_TEMP_OUT_H, IIO_MOD_X, val);  
		break;
	default:
		ret = -EINVAL;
		break;
	}
	return ret;
}

/*
  * @description  	: 设置ICM20608的陀螺仪计量程(分辨率)
  * @param - dev	: icm20608设备
  * @param - val   	: 量程(分辨率值)。
  * @return			: 0,成功;其他值,错误
  */
static int icm20608_write_gyro_scale(struct icm20608_dev *dev, int val)
{
	int result, i;
	u8 d;

	for (i = 0; i < ARRAY_SIZE(gyro_scale_icm20608); ++i) {
		if (gyro_scale_icm20608[i] == val) {
			d = (i << 3);
			result = regmap_write(dev->regmap, ICM20_GYRO_CONFIG, d);
			if (result)
				return result;
			return 0;
		}
	}
	return -EINVAL;
}

 /*
  * @description  	: 设置ICM20608的加速度计量程(分辨率)
  * @param - dev	: icm20608设备
  * @param - val   	: 量程(分辨率值)。
  * @return			: 0,成功;其他值,错误
  */
static int icm20608_write_accel_scale(struct icm20608_dev *dev, int val)
{
	int result, i;
	u8 d;

	for (i = 0; i < ARRAY_SIZE(accel_scale_icm20608); ++i) {
		if (accel_scale_icm20608[i] == val) {
			d = (i << 3);
			result = regmap_write(dev->regmap, ICM20_ACCEL_CONFIG, d);
			if (result)
				return result;
			return 0;
		}
	}
	return -EINVAL;
}

/*
  * @description     	: 读函数,当读取sysfs中的文件的时候最终此函数会执行,此函数
  * 					:里面会从传感器里面读取各种数据,然后上传给应用。
  * @param - indio_dev	: iio_dev
  * @param - chan   	: 通道
  * @param - val   		: 读取的值,如果是小数值的话,val是整数部分。
  * @param - val2   	: 读取的值,如果是小数值的话,val2是小数部分。
  * @param - mask   	: 掩码。
  * @return				: 0,成功;其他值,错误
  */
static int icm20608_read_raw(struct iio_dev *indio_dev,
			   struct iio_chan_spec const *chan,
			   int *val, int *val2, long mask)
{
	struct icm20608_dev *dev = iio_priv(indio_dev);
	int ret = 0;
	unsigned char regdata = 0;

	switch (mask) {/*使用type的值来区分是哪些通道*/
	case IIO_CHAN_INFO_RAW:								/* 读取ICM20608加速度计、陀螺仪、温度传感器原始值 */
		mutex_lock(&dev->lock);								/* 上锁 			*/
		ret = icm20608_read_channel_data(indio_dev, chan, val); 	/* 读取通道值 */
		mutex_unlock(&dev->lock);							/* 释放锁 			*/
		return ret;
	case IIO_CHAN_INFO_SCALE:
		switch (chan->type) {
		case IIO_ANGL_VEL:
			mutex_lock(&dev->lock);
			regdata = (icm20608_read_onereg(dev, ICM20_GYRO_CONFIG) & 0X18) >> 3;
			*val  = 0;
			*val2 = gyro_scale_icm20608[regdata];
			mutex_unlock(&dev->lock);
			return IIO_VAL_INT_PLUS_MICRO;	/* 值为val+val2/1000000 */
		case IIO_ACCEL:
			mutex_lock(&dev->lock);
			regdata = (icm20608_read_onereg(dev, ICM20_ACCEL_CONFIG) & 0X18) >> 3;
			*val = 0;
			*val2 = accel_scale_icm20608[regdata];;
			mutex_unlock(&dev->lock);
			return IIO_VAL_INT_PLUS_NANO;/* 值为val+val2/1000000000 */
		case IIO_TEMP:					
			*val = ICM20608_TEMP_SCALE/ 1000000;
			*val2 = ICM20608_TEMP_SCALE % 1000000;
			return IIO_VAL_INT_PLUS_MICRO;	/* 值为val+val2/1000000 */
		default:
			return -EINVAL;
		}
		return ret;
	case IIO_CHAN_INFO_OFFSET:		/* ICM20608温度传感器offset值 */
		switch (chan->type) {
		case IIO_TEMP:
			*val = ICM20608_TEMP_OFFSET;
			return IIO_VAL_INT;
		default:
			return -EINVAL;
		}
		return ret;
	case IIO_CHAN_INFO_CALIBBIAS:	/* ICM20608加速度计和陀螺仪校准值 */
		switch (chan->type) {
		case IIO_ANGL_VEL:		/* 陀螺仪的校准值 */
			mutex_lock(&dev->lock);
			ret = icm20608_sensor_show(dev, ICM20_XG_OFFS_USRH, chan->channel2, val);
			mutex_unlock(&dev->lock);
			return ret;
		case IIO_ACCEL:			/* 加速度计的校准值 */
			mutex_lock(&dev->lock);	
			ret = icm20608_sensor_show(dev, ICM20_XA_OFFSET_H, chan->channel2, val);
			mutex_unlock(&dev->lock);
			return ret;
		default:
			return -EINVAL;
		}
		
	default:
		return ret -EINVAL;
	}
}	

/*
  * @description     	: 写函数,当向sysfs中的文件写数据的时候最终此函数会执行,一般在此函数
  * 					:里面设置传感器,比如量程等。
  * @param - indio_dev	: iio_dev
  * @param - chan   	: 通道
  * @param - val   		: 应用程序写入的值,如果是小数值的话,val是整数部分。
  * @param - val2   	: 应用程序写入的值,如果是小数值的话,val2是小数部分。
  * @return				: 0,成功;其他值,错误
  */
static int icm20608_write_raw(struct iio_dev *indio_dev,
			    struct iio_chan_spec const *chan,
			    int val, int val2, long mask)
{
	struct icm20608_dev *dev = iio_priv(indio_dev);
	int ret = 0;

	switch (mask) {
	case IIO_CHAN_INFO_SCALE:	/* 设置陀螺仪和加速度计的分辨率 */
		switch (chan->type) {
		case IIO_ANGL_VEL:		/* 设置陀螺仪 */
			mutex_lock(&dev->lock);
			ret = icm20608_write_gyro_scale(dev, val2);
			mutex_unlock(&dev->lock);
			break;
		case IIO_ACCEL:			/* 设置加速度计 */
			mutex_lock(&dev->lock);
			ret = icm20608_write_accel_scale(dev, val2);
			mutex_unlock(&dev->lock);
			break;
		default:
			ret = -EINVAL;
			break;
		}
		break;
	case IIO_CHAN_INFO_CALIBBIAS:	/* 设置陀螺仪和加速度计的校准值*/
		switch (chan->type) {
		case IIO_ANGL_VEL:		/* 设置陀螺仪校准值 */
			mutex_lock(&dev->lock);
			ret = icm20608_sensor_set(dev, ICM20_XG_OFFS_USRH,
									    chan->channel2, val);
			mutex_unlock(&dev->lock);
			break;
		case IIO_ACCEL:			/* 加速度计校准值 */
			mutex_lock(&dev->lock);
			ret = icm20608_sensor_set(dev, ICM20_XA_OFFSET_H,
							             chan->channel2, val);
			mutex_unlock(&dev->lock);
			break;
		default:
			ret = -EINVAL;
			break;
		}
		break;
	default:
		ret = -EINVAL;
		break;
	}
	return ret;
}

/*
  * @description     	: 用户空间写数据格式,比如我们在用户空间操作sysfs来设置传感器的分辨率,
  * 					:如果分辨率带小数,那么这个小数传递到内核空间应该扩大多少倍,此函数就是
  *						: 用来设置这个的。
  * @param - indio_dev	: iio_dev
  * @param - chan   	: 通道
  * @param - mask   	: 掩码
  * @return				: 0,成功;其他值,错误
  */
static int icm20608_write_raw_get_fmt(struct iio_dev *indio_dev,
				 struct iio_chan_spec const *chan, long mask)
{
	switch (mask) {
	case IIO_CHAN_INFO_SCALE:
		switch (chan->type) {
		case IIO_ANGL_VEL:		/* 用户空间写的陀螺仪分辨率数据要乘以1000000 */
			return IIO_VAL_INT_PLUS_MICRO;
		default:				/* 用户空间写的加速度计分辨率数据要乘以1000000000 */
			return IIO_VAL_INT_PLUS_NANO;
		}
	default:
		return IIO_VAL_INT_PLUS_MICRO;
	}
	return -EINVAL;
}

/*
 * iio_info结构体变量
 */
static const struct iio_info icm20608_info = {
	.read_raw		= icm20608_read_raw,
	.write_raw		= icm20608_write_raw,
	.write_raw_get_fmt = &icm20608_write_raw_get_fmt,	/* 用户空间写数据格式 */
};

/*
  * @description    : spi驱动的probe函数,当驱动与
  *                    设备匹配以后此函数就会执行
  * @param - spi  	: spi设备
  * @return  		: 0,成功;其他值,失败
  */	
static int icm20608_probe(struct spi_device *spi)
{
	int ret;
	struct icm20608_dev *dev;
	struct iio_dev *indio_dev;

	/*  1、申请iio_dev内存 */
	indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*dev));
	if (!indio_dev)
		return -ENOMEM;

	/* 2、获取icm20608_dev结构体地址 */
	dev = iio_priv(indio_dev); 
	dev->spi = spi;
	spi_set_drvdata(spi, indio_dev);    		/* 将indio_de设置为spi->dev的driver_data */
	mutex_init(&dev->lock);

	/* 3、iio_dev的其他成员变量 */
	indio_dev->dev.parent = &spi->dev;
	indio_dev->info = &icm20608_info;
	indio_dev->name = ICM20608_NAME;	
	indio_dev->modes = INDIO_DIRECT_MODE;	/* 直接模式,提供sysfs接口 */
	indio_dev->channels = icm20608_channels;
	indio_dev->num_channels = ARRAY_SIZE(icm20608_channels);

	/* 4、注册iio_dev */
	ret = iio_device_register(indio_dev);
	if (ret < 0) {
		dev_err(&spi->dev, "iio_device_register failed\n");
		goto err_iio_register;
	}

	/* 5、初始化regmap_config设置 */
	dev->regmap_config.reg_bits = 8;			/* 寄存器长度8bit */
	dev->regmap_config.val_bits = 8;			/* 值长度8bit */
	dev->regmap_config.read_flag_mask = 0x80;  /* 读掩码设置为0X80,ICM20608使用SPI接口读的时候寄存器最高位应该为1 */

	/* 6、初始化SPI接口的regmap */
	dev->regmap = regmap_init_spi(spi, &dev->regmap_config);
	if (IS_ERR(dev->regmap)) {
		ret = PTR_ERR(dev->regmap);
		goto err_regmap_init;
	}

	/* 7、初始化spi_device */
	spi->mode = SPI_MODE_0;	/*MODE0,CPOL=0,CPHA=0*/
	spi_setup(spi);
	
	/* 初始化ICM20608内部寄存器 */
	icm20608_reginit(dev);	
	return 0;

err_regmap_init:
    iio_device_unregister(indio_dev);
err_iio_register:
	return ret;
}

/*
 * @description     : spi驱动的remove函数,移除spi驱动的时候此函数会执行
 * @param - spi 	: spi设备
 * @return          : 0,成功;其他负值,失败
 */
static int icm20608_remove(struct spi_device *spi)
{
	struct iio_dev *indio_dev = spi_get_drvdata(spi);
	struct icm20608_dev *dev;
	
	dev = iio_priv(indio_dev);

	/* 1、删除regmap */ 
	regmap_exit(dev->regmap);

	/* 2、注销IIO */
	iio_device_unregister(indio_dev);
	return 0;
}

/* 传统匹配方式ID列表 */
static const struct spi_device_id icm20608_id[] = {
	{"alientek,icm20608", 0},
	{}
};

/* 设备树匹配列表 */
static const struct of_device_id icm20608_of_match[] = {
	{ .compatible = "alientek,icm20608" },
	{ /* Sentinel */ }
};

/* SPI驱动结构体 */
static struct spi_driver icm20608_driver = {
	.probe = icm20608_probe,
	.remove = icm20608_remove,
	.driver = {
			.owner = THIS_MODULE,
		   	.name = "icm20608",
		   	.of_match_table = icm20608_of_match,
		   },
	.id_table = icm20608_id,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init icm20608_init(void)
{
	return spi_register_driver(&icm20608_driver);
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit icm20608_exit(void)
{
	spi_unregister_driver(&icm20608_driver);
}

module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

  • 测试app
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: icm20608.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: icm20608设备iio框架测试程序。
其他	   	: 无
使用方法	 :./icm20608App 
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2021/8/17 左忠凯创建
***************************************************************/
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>

/* 字符串转数字,将浮点小数字符串转换为浮点数数值 */
#define SENSOR_FLOAT_DATA_GET(ret, index, str, member)\
	ret = file_data_read(file_path[index], str);\
	dev->member = atof(str);\
	
/* 字符串转数字,将整数字符串转换为整数数值 */
#define SENSOR_INT_DATA_GET(ret, index, str, member)\
	ret = file_data_read(file_path[index], str);\
	dev->member = atoi(str);\

/* icm20608 iio框架对应的文件路径 */
static char *file_path[] = {/*在开发板下面的绝对路径*/
	"/sys/bus/iio/devices/iio:device1/in_accel_scale",
	"/sys/bus/iio/devices/iio:device1/in_accel_x_calibbias",
	"/sys/bus/iio/devices/iio:device1/in_accel_x_raw",
	"/sys/bus/iio/devices/iio:device1/in_accel_y_calibbias",
	"/sys/bus/iio/devices/iio:device1/in_accel_y_raw",
	"/sys/bus/iio/devices/iio:device1/in_accel_z_calibbias",
	"/sys/bus/iio/devices/iio:device1/in_accel_z_raw",
	"/sys/bus/iio/devices/iio:device1/in_anglvel_scale",
	"/sys/bus/iio/devices/iio:device1/in_anglvel_x_calibbias",
	"/sys/bus/iio/devices/iio:device1/in_anglvel_x_raw",
	"/sys/bus/iio/devices/iio:device1/in_anglvel_y_calibbias",
	"/sys/bus/iio/devices/iio:device1/in_anglvel_y_raw",
	"/sys/bus/iio/devices/iio:device1/in_anglvel_z_calibbias",
	"/sys/bus/iio/devices/iio:device1/in_anglvel_z_raw",
	"/sys/bus/iio/devices/iio:device1/in_temp_offset",
	"/sys/bus/iio/devices/iio:device1/in_temp_raw",
	"/sys/bus/iio/devices/iio:device1/in_temp_scale",
};

/* 文件路径索引,要和file_path里面的文件顺序对应 */
enum path_index {
	IN_ACCEL_SCALE = 0,
	IN_ACCEL_X_CALIBBIAS,
	IN_ACCEL_X_RAW,
	IN_ACCEL_Y_CALIBBIAS,
	IN_ACCEL_Y_RAW,
	IN_ACCEL_Z_CALIBBIAS,
	IN_ACCEL_Z_RAW,
	IN_ANGLVEL_SCALE,
	IN_ANGLVEL_X_CALIBBIAS,
	IN_ANGLVEL_X_RAW,
	IN_ANGLVEL_Y_CALIBBIAS,
	IN_ANGLVEL_Y_RAW,
	IN_ANGLVEL_Z_CALIBBIAS,
	IN_ANGLVEL_Z_RAW,
	IN_TEMP_OFFSET,
	IN_TEMP_RAW,
	IN_TEMP_SCALE,
};

/*
 * icm20608数据设备结构体
 */
struct icm20608_dev{
	int accel_x_calibbias, accel_y_calibbias, accel_z_calibbias;
	int accel_x_raw, accel_y_raw, accel_z_raw;

	int gyro_x_calibbias, gyro_y_calibbias, gyro_z_calibbias;
	int gyro_x_raw, gyro_y_raw, gyro_z_raw;

	int temp_offset, temp_raw;

	float accel_scale, gyro_scale, temp_scale;

	float gyro_x_act, gyro_y_act, gyro_z_act;
	float accel_x_act, accel_y_act, accel_z_act;
	float temp_act;
};

struct icm20608_dev icm20608;

 /*
 * @description			: 读取指定文件内容
 * @param - filename 	: 要读取的文件路径
 * @param - str 		: 读取到的文件字符串
 * @return 				: 0 成功;其他 失败
 */
static int file_data_read(char *filename, char *str)
{
	int ret = 0;
	FILE *data_stream;

    data_stream = fopen(filename, "r"); /* 只读打开 */
    if(data_stream == NULL) {
		printf("can't open file %s\r\n", filename);
		return -1;
	}

	ret = fscanf(data_stream, "%s", str);
    if(!ret) {
        printf("file read error!\r\n");
    } else if(ret == EOF) {
        /* 读到文件末尾的话将文件指针重新调整到文件头 */
        fseek(data_stream, 0, SEEK_SET);  
    }
	fclose(data_stream);	/* 关闭文件 */	
	return 0;
}

 /*
 * @description	: 获取ICM20608数据
 * @param - dev : 设备结构体
 * @return 		: 0 成功;其他 失败
 */
static int sensor_read(struct icm20608_dev *dev)
{
	int ret = 0;
	char str[50];

	/* 1、获取陀螺仪原始数据 */
	SENSOR_FLOAT_DATA_GET(ret, IN_ANGLVEL_SCALE, str, gyro_scale);
	SENSOR_INT_DATA_GET(ret, IN_ANGLVEL_X_RAW, str, gyro_x_raw);
	SENSOR_INT_DATA_GET(ret, IN_ANGLVEL_Y_RAW, str, gyro_y_raw);
	SENSOR_INT_DATA_GET(ret, IN_ANGLVEL_Z_RAW, str, gyro_z_raw);

	/* 2、获取加速度计原始数据 */
	SENSOR_FLOAT_DATA_GET(ret, IN_ACCEL_SCALE, str, accel_scale);
	SENSOR_INT_DATA_GET(ret, IN_ACCEL_X_RAW, str, accel_x_raw);
	SENSOR_INT_DATA_GET(ret, IN_ACCEL_Y_RAW, str, accel_y_raw);
	SENSOR_INT_DATA_GET(ret, IN_ACCEL_Z_RAW, str, accel_z_raw);

	/* 3、获取温度值 */
	SENSOR_FLOAT_DATA_GET(ret, IN_TEMP_SCALE, str, temp_scale);
	SENSOR_INT_DATA_GET(ret, IN_TEMP_OFFSET, str, temp_offset);
	SENSOR_INT_DATA_GET(ret, IN_TEMP_RAW, str, temp_raw);

	/* 3、转换为实际数值 */
	dev->accel_x_act = dev->accel_x_raw * dev->accel_scale;
	dev->accel_y_act = dev->accel_y_raw * dev->accel_scale;
	dev->accel_z_act = dev->accel_z_raw * dev->accel_scale;

	dev->gyro_x_act = dev->gyro_x_raw * dev->gyro_scale;
	dev->gyro_y_act = dev->gyro_y_raw * dev->gyro_scale;
	dev->gyro_z_act = dev->gyro_z_raw * dev->gyro_scale;

	dev->temp_act = ((dev->temp_raw - dev->temp_offset) / dev->temp_scale) + 25;
	return ret;
}

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int ret = 0;

	if (argc != 1) {
		printf("Error Usage!\r\n");
		return -1;
	}

	while (1) {
		ret = sensor_read(&icm20608);
		if(ret == 0) { 			/* 数据读取成功 */
			printf("\r\n原始值:\r\n");
			printf("gx = %d, gy = %d, gz = %d\r\n", icm20608.gyro_x_raw, icm20608.gyro_y_raw, icm20608.gyro_z_raw);
			printf("ax = %d, ay = %d, az = %d\r\n", icm20608.accel_x_raw, icm20608.accel_y_raw, icm20608.accel_z_raw);
			printf("temp = %d\r\n", icm20608.temp_raw);
			printf("实际值:");
			printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", icm20608.gyro_x_act, icm20608.gyro_y_act, icm20608.gyro_z_act);
			printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", icm20608.accel_x_act, icm20608.accel_y_act, icm20608.accel_z_act);
			printf("act temp = %.2f°C\r\n", icm20608.temp_act);
		}
		usleep(100000); /*100ms */
	}
	return 0;
}

  • 6
    点赞
  • 65
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

栋哥爱做饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值