Linux驱动开发笔记(二十四)——(上)IIO + icm20608驱动

视频:第36.1讲 Linux IIO驱动实验-为什么要用IIO框架_哔哩哔哩_bilibili

资料:《【正点原子】I.MX6U开发指南V1.81.pdf》七十五章

正点原子imx6ull mini板

虚拟机 ubuntu20.04


一、IIO子系统

先看看iio系统是干什么的。

如果用的是alpha板,可以先按照第二部分将IIO配置好,并进行下面的操作:

(我这里直接截视频教程的图过来了)

把正点原子】阿尔法Linux开发板(A盘)-基础资料\01、例程源码\02、Linux驱动例程\27_iio\spi里面的代码传到虚拟机里面,修改其中的Makefile里面KERNELDIR路经,然后直接编译,并将编译后的.ko文件传给虚拟机,并在控制台进行modprobe。

IIO驱动框架提供sysfs接口。加载成功后我们可直接在用户空间访问对应的sysfs目录项:

进入/sys/bus/iio/devices目录下,这里都是IIO框架设备

如上图,在device0的目录下有一大堆的文件。device0对应icm20608,通过读取这些文件就能读取icm20608传感器的数据,如in_accel_x_raw就是icm20608的x轴加速度计的原始数据

使用cat,读出z轴原始数据为2057:

如何通过原始数据读出真实数据?

量程不同,原始数据对应的真实数据也不同。

此时icm20608的默认量程是±16g。通过数据手册得知,当量程为±16时,原始数据每增加2048,真实加速度就增加1g(即+2048表示+1g,+2048*2表示+2g,......)

因此2057对应的真实加速度为:2057÷2048 = 1.00439453125g,即约为1g的正常重力加速度。

这个2048怎么来的?

已知icm20608的是16位数据(两个8位寄存器表示一个数据),而16位可以表示2^16=65536个数。

要将这65536个数平均划分为32份(量程为±16,则-16 ~ +16共32个),那么就是每2048表示一个g(65536÷32=2048)

同理,图中的其他值:16384 ≈ 65536÷4、8192 = 65536÷8、............

在device0的目录下的一大堆文件中,还可以看到in_accel_scale、in_anglvel_scale、in_temp_scale这三个文件,这三个文件就是对应的分辨率。如in_accel_scale=0.000488281 ≈ 32÷65536 = 1÷2048(也就是原始数据每增加1,真实加速度增加0.000488281g)

这样当我们需要读取真实x加速度值的时候,直接计算in_accel_x_raw × in_accel_scale即可,非常方便。

综上,用户态读取这些文件 时,驱动 就会调用对应代码,去计算对应的数据,以避免自定义数据上报格式、数据不统一的问题。这一部分就是iio子系统实现的,我们需要配置iio子系统来生成这些文件,也需要编写'读写这些文件时所调用'的函数。

1.1 iio_dev

cpp 复制代码
/**
 * 定义在linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include/linux/iio/iio.h
 * struct iio_dev - industrial I/O device
 * @id:			[INTERN] used to identify device internally
 * @modes:		[DRIVER] 设备支持的模式 operating modes supported by device
 * @currentmode:[DRIVER] 当前模式 current operating mode
 * @dev:		[DRIVER] device structure, should be assigned a parent
 *			and owner
 * @event_interface:	[INTERN] event chrdevs associated with interrupt lines
 * @buffer:		[DRIVER] 缓冲区 any buffer present
 * @buffer_list:[INTERN] 当前匹配的缓冲区列表 list of all buffers currently attached
 * @scan_bytes:	[INTERN] 捕获到、且提供给缓冲区的字节数 num bytes captured to be fed to buffer demux
 * @mlock:		[INTERN] lock used to prevent simultaneous device state
 *			changes
 * @available_scan_masks: [DRIVER] 可选的扫描位掩码,使用触发缓冲区的时候可以通过设
 *                          置掩码来确定使能哪些通道,使能以后的通道会将捕获到的数据发送到IIO缓冲区 
 *                          optional array of allowed bitmasks
 * @masklength:		[INTERN] the length of the mask established from
 *			channels
 * @active_scan_mask:	[INTERN] 缓冲区已经开启的通道掩码
 *                               只有这些使能了的通道数据才能被发送到缓冲区
 *                               union of all scan masks requested by buffers
 * @scan_timestamp:	[INTERN] 扫描时间戳,如果使能以后
 *                           会将捕获时间戳放到缓冲区里面
 *                           set if any buffers have requested timestamp
 * @scan_index_timestamp:[INTERN] cache of the index to the timestamp
 * @trig:		[INTERN] IIO设备当前触发器(使用缓冲模式时) current device trigger (buffer modes)
 * @pollfunc:		[DRIVER] 一个函数,在接收到的触发器上运行 function run on trigger being received
 * @channels:		[DRIVER] IIO设备通道,后面讲 channel specification structure table
 * @num_channels:	[DRIVER] number of channels specified in @channels.
 * @channel_attr_list:	[INTERN] keep track of automatically created channel
 *			attributes
 * @chan_attr_group:	[INTERN] group for all attrs in base directory
 * @name:		[DRIVER] IIO设备名 name of the device.
 * @info:		[DRIVER] 非常重要!!后面讲 callbacks and constant info from driver
 * @info_exist_lock:	[INTERN] lock to prevent use during removal
 * @setup_ops:		[DRIVER] callbacks to call before and after buffer
 *			enable/disable
 * @chrdev:		[INTERN] associated character device
 * @groups:		[INTERN] attribute groups
 * @groupcounter:	[INTERN] index of next attribute group
 * @flags:		[INTERN] file ops related flags including busy flag.
 * @debugfs_dentry:	[INTERN] device specific debugfs dentry.
 * @cached_reg_addr:	[INTERN] cached register address for debugfs reads.
 */
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;
	unsigned			masklength;
	const unsigned long		*active_scan_mask;
	bool				scan_timestamp;
	unsigned			scan_index_timestamp;
	struct iio_trigger		*trig;
	struct iio_poll_func		*pollfunc;

	struct iio_chan_spec const	*channels;
	int				num_channels;

	struct list_head		channel_attr_list;
	struct attribute_group		chan_attr_group;
	const char			*name;
	const struct iio_info		*info;  // 重点。见1.4
	struct mutex			info_exist_lock;
	const struct iio_buffer_setup_ops	*setup_ops; 
	struct cdev			chrdev;  // 字符设备,由IIO内核创建
#define IIO_MAX_GROUPS 6
	const struct attribute_group	*groups[IIO_MAX_GROUPS + 1];
	int				groupcounter;

	unsigned long			flags;
#if defined(CONFIG_DEBUG_FS)
	struct dentry			*debugfs_dentry;
	unsigned			cached_reg_addr;
#endif
};

其中mode可选择的模式如下:

cpp 复制代码
/* Device operating modes */
// 也定义在linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include/linux/iio/iio.h
#define INDIO_DIRECT_MODE		    0x01    // 提供 sysfs 接口。
#define INDIO_BUFFER_TRIGGERED		0x02    // 支持硬件缓冲触发
#define INDIO_BUFFER_SOFTWARE		0x04    // 支持软件缓冲触发
#define INDIO_BUFFER_HARDWARE		0x08    // 支持硬件缓冲区

1.2 申请与释放

cpp 复制代码
// 申请
struct iio_dev *iio_device_alloc(int sizeof_priv)
// sizeof_priv:私有数据内存空间的大小,一般填的是自己的设备结构体
// return:NULL失败;else成功,返回iio_dev首地址
    一般将自己定义的设备结构体变量作为iio_dev的私有数据,
这样可以直接通过 iio_device_alloc函数同时完成iio_dev和设备结构体变量的内存申请。

// 释放
void iio_device_free(struct iio_dev *indio_dev)
// indio_dev:要释放的iio_dev




// 当然,也有对应的devm函数:
devm_iio_device_alloc(struct device *dev,int sizeof_priv)
// sizeof_priv:私有数据内存空间的大小,一般填的是自己的设备结构体
// dev:这个IIO设备是挂在哪个物理设备(总线)名下的,比如spi设备就填&spi->dev
// return:成功则返回iio_dev首地址

1.2+ 其他函数

这两个函数视频和《指南》中也用到了:

1.2+.1 iio_priv

cpp 复制代码
static inline void *iio_priv(const struct iio_dev *indio_dev)
// return:自定义的设备结构体变量首地址

iio_device_alloc申请成功以后使用iio_priv函数来得到自定义的设备结构体变量首地址。

这个函数的原理是:

iio_device_alloc是申请了一块连续的空间 ,前半部分是iio_dev的空间,后半部分是私有数据的空间(当然,两者中间可能会有空白区域以保证地址对齐)

并不是说私有数据是挂在iio_dev的某个成员下,而是两者物理地址上相邻。

iio_priv函数就是通过iio_dev的首地址,在地址上做加法来获取到私有数据的起始地址:

cpp 复制代码
static inline void *iio_priv(const struct iio_dev *indio_dev){
	return (char *)indio_dev + ALIGN(sizeof(struct iio_dev), IIO_ALIGN);
}

使用方法如《指南》中的示例代码 75.1.1.3所示:

cpp 复制代码
struct icm20608_dev *dev;   // 自定义的设备结构体
struct iio_dev *indio_dev;  // iio_dev结构体变量指针

/* 1、申请 iio_dev 内存 */
indio_dev = iio_device_alloc(sizeof(*dev)); // 申请iio_dev,并且一起申请了icm2060_dev的内存
if (!indio_dev)
    return -ENOMEM;

/* 2、获取设备结构体变量地址 */
dev = iio_priv(indio_dev);  // 从iio_dev中提取出私有数据,也就是icm2608_dev变量首地址

1.2+.2 spi_set_drvdata & spi_get_drvdata

cpp 复制代码
static inline void spi_set_drvdata(struct spi_device *spi, void *data)
// data:私有数据、设备结构体
// spi:设备结构体所属的spi_device

static inline void *spi_get_drvdata(struct spi_device *spi)

使用spi_set_drvdata函数,把自己定义的数据(比如struct iio_dev)挂在struct spi_device上。这样就可以使用spi_get_drvdata函数、通过spi_device获取到对应数据。

这两个spi_get/set_drvdata函数就是对dev_set/get_drvdata函数的封装,而dev_set/get_drvdata函数本质就是在读写device->driver_data成员。

类似的,platform、iic也有自己的set、get函数:i2c_set_clientdata和i2c_get_clientdata。

1.3 注册与注销

cpp 复制代码
// 定义在drivers/iio/industrialio-core.c中

// 首先配置好indio_dev的各个成员变量,然后使用register函数进行注册
int iio_device_register(struct iio_dev *indio_dev)
// indio_dev:要注册的iio_dev
// return:   0成功;else失败

void iio_device_unregister(struct iio_dev *indio_dev)
// indio_dev:要注销的iio_dev
// return:   0成功;else失败

还有一件事,如果去看iio_device_register的代码,会发现这个函数已经完成了设备号、class、cdev、device、file_operations等工作,以后设备结构体、probe、file_operations再也不用写一大坨了。

1.4 iio_info

省略版:

其中的read_raw和write_raw函数就是最终读写设备内部数据的操作函数,需要程序编写人员去实现的。

比如应用读取陀螺仪传感器的原始数据, 那么最终完成工作的就是read_raw函数,我们需要在read_raw函数里面实现对陀螺仪芯片的读取操作。

同理,write_raw是应用程序向陀螺仪芯片写数据,一般用于配置芯片。

cpp 复制代码
/**
 * struct iio_info - constant information about device
 * @driver_module:	module structure used to ensure correct
 *			ownership of chrdevs etc
 * @event_attrs:	event control attributes
 * @attrs:		general purpose device attributes
 * @read_raw:		function to request a value from the device.
 *			mask specifies which value. Note 0 means a reading of
 *			the channel in question.  Return value will specify the
 *			type of value returned by the device. val and val2 will
 *			contain the elements making up the returned value.
 * @write_raw:		function to write a value to the device.
 *			Parameters are the same as for read_raw.
 * @write_raw_get_fmt:	callback function to query the expected
 *			format/precision. If not set by the driver, write_raw
 *			returns IIO_VAL_INT_PLUS_MICRO.
 * ............
 **/
struct iio_info {
	struct module			*driver_module;
	struct attribute_group		*event_attrs;
	const struct attribute_group	*attrs;

	int (*read_raw)(struct iio_dev *indio_dev,  // 需要读写的IIO设备
			struct iio_chan_spec const *chan,   // 需要读取的通道
			int *val,    // val、val2这两个就是应用程序从内核空间读取的数据
			int *val2,   // 一般就是传感器指定通道值,或者传感器的量程、分辨率等
			long mask);  // 见1.4.3

    ............

	int (*write_raw)(struct iio_dev *indio_dev, // 需要读写的IIO设备
			 struct iio_chan_spec const *chan,  // 需要读取的通道
			 int val,    // val、val2这两个就是应用程序向设备写入的数据
			 int val2,
			 long mask); // 见1.4.3

	int (*write_raw_get_fmt)(struct iio_dev *indio_dev, // 见1.4.4
			 struct iio_chan_spec const *chan,
			 long mask);
    ............
};

1.4.1 val & val2

其中,一对val和val2组成一个值:val整数部分,val2小数部分。但因为不能直接从内核向应用程序返回一个小数,所以val2是小数部分扩大N倍后的整数,

比如现在有个值为1.00236,val是1,vla2本应是0.00236,但是需要将0.00236扩大N倍,使其变为整数,比如扩大N=1000000倍,val2就是2360。则val=1,val2=2360。

扩大的倍数也不能随意设置,Linux内核里面定义好了数据扩大倍数:

具体怎么用详见3.4.1、3.4.2以及附录B。

1.4.2 mask

上面的函数中还有mask这个参数,用于指定我们读取什么数据。

比如icm20608,既有原始数据,也有测量范围值 / 分辨率。此时就有两种数据值:传感器原始值raw、分辨率/比例因子scale。

Linux内核用IIO_CHAN_INFO_RAW和IIO_CHAN_INFO_SCALE这两个宏来表示原始值和分辨率,这两个宏就是mask。至于每个通道可以采用哪几种掩码,在初始化通道的时候需要驱动编写人员设置好。

(当然实际应用中也不只这两种,具体要看iio_chan_spec的通道有哪些。详见1.4.5)

1.4.4 write_raw_get_fmt函数

write_raw_get_fmt函数决定了wtite_raw函数中val和val2的意义,也就是上面表75.1.2.1中的组合形式。

比如要在应用程序 中设置imc20608加速度计的量程为±8g,那么分辨率就是|8-(-8)| / 2^16 = 16 / 65536 ≈ 0.000244,我们在write_raw_get_fmt函数里面设置加速度计的数据格式为 IIO_VAL_INT_PLUS_MICRO。那么应用程序 向指定的文件写入0.000244以后,最终传递给内核驱动的就是0.000244*1000000=244。也就是write_raw函数的val为0,val2为244。

1.4.5 通道iio_chan_spec

(这部分成员多的头大,但其实大部分都是用不到的)

IIO的核心就是通道。

一个传感器可能有多路数据。比如ICM20608,输出三轴角度、三轴加速度、一路温度,一共7路数据,就有7个通道(一种类型的数据就是一个通道)。Linux内核使用iio_chan_spec结构体来描述通道:

cpp 复制代码
/**
 * 定义在include/linux/iio/iio.h
 */
struct iio_chan_spec {
	enum iio_chan_type	type;  // 详见1.4.5.1
	int			channel;       // 通道索引(当indexed为1时)
	int			channel2;      // 通道修饰符(当modified为1时),详见1.4.5.2
	unsigned long		address; // 用户可以自定义,但是一般会设置为此通道对应的芯片数据寄存器地址
                                 // 比如icm20608的加速度计x轴通道,它的数据首地址就是0X3B
                                 // 也可以不用address,也可以自行用作其他功能,以实际为准

	int			scan_index;    // 扫描索引(使用触发缓冲区时)
	struct {
		char	sign;   // 如果为'u'表示数据为无符号类型,为's'的话为有符号类型。只能为u或s!
		u8	realbits;   // 数据真实的有效位数,比如很多传感器说的10位 ADC,其真实有效数据就是10位
		u8	storagebits;// 存储位数=有效位数+填充位。比如有些传感器ADC是12位的,
                        // 那么存储要用2个字节=16位,16位就是存储位数

		u8	shift;      // 右移位数。也就是存储位数和有效位数不一致时,需要右移的位数
                        // 这个参数不总是需要,一切以实际芯片的数据手册位数为准
		u8	repeat;     // 实际或存储位的重复数量
		enum iio_endian endianness; // 数据大小端。可为IIO_CPU、IIO_BE(大端)或IIO_LE(小端)

	} scan_type;
	long			info_mask_separate;  // 见1.4.5.4
	long			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下就会只生成一个描述分辨率的文件,
                   // 这三个通道都可以使用这一个分辨率文件,比如文章开头的in_accel_scale、in_anglvel_scale文件

	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;    // modified为1时,channel2为通道修饰符
	unsigned		indexed:1;     // indexed为1时,channel为通道索引
	unsigned		output:1;      // 输出通道
	unsigned		differential:1;// 差分通道
};

/**
 * struct iio_chan_spec - specification of a single channel
 * @type:		What type of measurement is the channel making.
 * @channel:		What number do we wish to assign the channel.
 * @channel2:		If there is a second number for a differential
 *			channel then this is it. If modified is set then the
 *			value here specifies the modifier.
 * @address:		Driver specific identifier.
 * @scan_index:		Monotonic index to give ordering in scans when read
 *			from a buffer.
 * @scan_type:		Sign:		's' or 'u' to specify signed or unsigned
 *			realbits:	Number of valid bits of data
 *			storage_bits:	Realbits + padding
 *			shift:		Shift right by this before masking out
 *					realbits.
 *			endianness:	little or big endian
 *			repeat:		Number of times real/storage bits
 *					repeats. When the repeat element is
 *					more than 1, then the type element in
 *					sysfs will show a repeat value.
 *					Otherwise, the number of repetitions is
 *					omitted.
 * @info_mask_separate: What information is to be exported that is specific to
 *			this channel.
 * @info_mask_shared_by_type: What information is to be exported that is shared
 *			by all channels of the same type.
 * @info_mask_shared_by_dir: What information is to be exported that is shared
 *			by all channels of the same direction.
 * @info_mask_shared_by_all: What information is to be exported that is shared
 *			by all channels.
 * @event_spec:		Array of events which should be registered for this
 *			channel.
 * @num_event_specs:	Size of the event_spec array.
 * @ext_info:		Array of extended info attributes for this channel.
 *			The array is NULL terminated, the last element should
 *			have its name field set to NULL.
 * @extend_name:	Allows labeling of channel attributes with an
 *			informative name. Note this has no effect codes etc,
 *			unlike modifiers.
 * @datasheet_name:	A name used in in-kernel mapping of channels. It should
 *			correspond to the first name that the channel is referred
 *			to by in the datasheet (e.g. IND), or the nearest
 *			possible compound name (e.g. IND-INC).
 * @modified:		Does a modifier apply to this channel. What these are
 *			depends on the channel type.  Modifier is set in
 *			channel2. Examples are IIO_MOD_X for axial sensors about
 *			the 'x' axis.
 * @indexed:		Specify the channel has a numerical index. If not,
 *			the channel index number will be suppressed for sysfs
 *			attributes but not for event codes.
 * @output:		Channel is output.
 * @differential:	Channel is differential.
 */
1.4.5.1 iio_chan_type

type为iio_chan_type类型, 是一个枚举类型,列举出了可以选择的通道类型,定义在 include/uapi/linux/iio/types.h里:

cpp 复制代码
enum iio_chan_type {
    IIO_VOLTAGE,          /* 电压类型 */
    IIO_CURRENT,          /* 电流类型 */
    IIO_POWER,            /* 功率类型 */
    IIO_ACCEL,            /* 加速度类型 */
    IIO_ANGL_VEL,         /* 角度类型(陀螺仪) */
    IIO_MAGN,             /* 电磁类型(磁力计) */
    IIO_LIGHT,            /* 灯光类型 */
    IIO_INTENSITY,        /* 强度类型(光强传感器) */
    IIO_PROXIMITY,        /* 接近类型(接近传感器) */
    IIO_TEMP,             /* 温度类型 */
    IIO_INCLI,            /* 倾角类型(倾角测量传感器) */
    IIO_ROT,              /* 旋转角度类型 */
    IIO_ANGL,             /* 转动角度类型(电机旋转角度测量传感器) */
    IIO_TIMESTAMP,        /* 时间戳类型 */
    IIO_CAPACITANCE,      /* 电容类型 */
    IIO_ALTVOLTAGE,       /* 频率类型 */
    IIO_CCT,              /* 相关的色温类型 (Correlated Color Temperature) */
    IIO_PRESSURE,         /* 压力类型 */ 
    IIO_HUMIDITYRELATIVE, /* 湿度类型 */
    IIO_ACTIVITY,         /* 活动类型(计步传感器) */
    IIO_STEPS,            /* 步数类型 */
    IIO_ENERGY,           /* 能量类型(卡路里) */
    IIO_DISTANCE,         /* 距离类型 */
    IIO_VELOCITY,         /* 速度类型 */
};

如果是ADC,那就是IIO_VOLTAGE类型。如果是ICM20608,那么就是复合类型,陀螺仪部分是IIO_ANGL_VEL类型、加速度计部分是IIO_ACCEL类型、温度部分是IIO_TEMP。

1.4.5.2 iio_modifier

iio_chan_spec的成员变量modified为1时,channel2为通道修饰符。例如陀螺仪三个轴的加速度,它们都属于加速度通道,但是属于不同的轴,需要加上X/Y/Z轴将其区分,就是通道修饰符。类似的,还有光照中的不同区域光谱等等,具体枚举如下:

Linux内核给出了可用的通道修饰符,定义在 include/uapi/linux/iio/types.h里:(注释由ai生成)

cpp 复制代码
enum iio_modifier {
	IIO_NO_MOD,                     /* 无修饰符:默认值 */
	IIO_MOD_X,                      /* X 轴 */
	IIO_MOD_Y,                      /* Y 轴 */
	IIO_MOD_Z,                      /* Z 轴 */
	IIO_MOD_X_AND_Y,                /* X 和 Y 轴的组合数据 */
	IIO_MOD_X_AND_Z,                /* X 和 Z 轴的组合数据 */
	IIO_MOD_Y_AND_Z,                /* Y 和 Z 轴的组合数据 */
	IIO_MOD_X_AND_Y_AND_Z,          /* X、Y 和 Z 三轴的组合数据 */
	IIO_MOD_X_OR_Y,                 /* X 或 Y 轴(通常用于触发器或事件) */
	IIO_MOD_X_OR_Z,                 /* X 或 Z 轴 */
	IIO_MOD_Y_OR_Z,                 /* Y 或 Z 轴 */
	IIO_MOD_X_OR_Y_OR_Z,            /* X、Y 或 Z 三轴任一 */
	IIO_MOD_LIGHT_BOTH,             /* 光照:全光谱(包含可见光和红外线) */
	IIO_MOD_LIGHT_IR,               /* 光照:红外线光谱 */
	IIO_MOD_ROOT_SUM_SQUARED_X_Y,   /* X 和 Y 轴平方和的平方根(矢量长度) */
	IIO_MOD_SUM_SQUARED_X_Y_Z,      /* X、Y、Z 三轴的平方和 */
	IIO_MOD_LIGHT_CLEAR,            /* 光照:清除/白色(不带滤光片的光强) */
	IIO_MOD_LIGHT_RED,              /* 光照:红色光谱 */
	IIO_MOD_LIGHT_GREEN,            /* 光照:绿色光谱 */
	IIO_MOD_LIGHT_BLUE,             /* 光照:蓝色光谱 */
	IIO_MOD_QUATERNION,             /* 四元数(通常用于 9 轴传感器的姿态解算) */
	IIO_MOD_TEMP_AMBIENT,           /* 温度:环境温度 */
	IIO_MOD_TEMP_OBJECT,            /* 温度:目标物体温度(如红外测温) */
	IIO_MOD_NORTH_MAGN,             /* 指向磁北极的方向 */
	IIO_MOD_NORTH_TRUE,             /* 指向正北极(地理北极)的方向 */
	IIO_MOD_NORTH_MAGN_TILT_COMP,   /* 带倾斜补偿的磁北极方向 */
	IIO_MOD_NORTH_TRUE_TILT_COMP,   /* 带倾斜补偿的正北极方向 */
	IIO_MOD_RUNNING,                /* 步态检测:跑步状态 */
	IIO_MOD_JOGGING,                /* 步态检测:慢跑状态 */
	IIO_MOD_WALKING,                /* 步态检测:步行状态 */
	IIO_MOD_STILL,                  /* 步态检测:静止状态 */
	IIO_MOD_ROOT_SUM_SQUARED_X_Y_Z, /* X、Y、Z 三轴平方和的平方根(总矢量幅值) */
};

比如icm20608的加速度计部分type通道类型设置为IIO_ACCEL,X、Y、Z 这三个轴就用channel2的通道修饰符来区分。IIO_MOD_X、IIO_MOD_Y、IIO_MOD_Z就分别对应 X、Y、Z 这三个轴。通道修饰符主要是影响sysfs下的通道文件名字。

1.4.5.3 scan_type

一个结构体,描述了扫描数据在缓冲区中的存储格式。(本节代码中基本没有用到)

具体成员的含义已在1.4.5中以注释形式给出

1.4.5.4 info_mask_separate

标记某些属性专属于此通道,iio_chan_info_enum枚举类型描述了可选的属性值:

(手册写的是定义在include/linux/iio/types.h,但我只在include/linux/iio/iio.h中找到)

(以下代码注释由ai生成)

cpp 复制代码
enum iio_chan_info_enum {
	IIO_CHAN_INFO_RAW = 0,                         // 通道读取的原始数值(未经缩放或偏移)
	IIO_CHAN_INFO_PROCESSED,                       // 已经过驱动处理(缩放/偏移)后的实际物理值
	IIO_CHAN_INFO_SCALE,                           // 缩放比例,物理值 = (RAW + OFFSET) * SCALE
	IIO_CHAN_INFO_OFFSET,                          // 偏移量,用于修正原始值的零点偏差
	IIO_CHAN_INFO_CALIBSCALE,                      // 硬件或用户级的增益校准系数
	IIO_CHAN_INFO_CALIBBIAS,                       // 硬件或用户级的偏差校准值
	IIO_CHAN_INFO_PEAK,                            // 自上次读取以来的峰值(最大值)
	IIO_CHAN_INFO_PEAK_SCALE,                      // 峰值测量所使用的缩放比例
	IIO_CHAN_INFO_QUADRATURE_CORRECTION_RAW,       // 正交信号(如编码器)的原始纠偏值
	IIO_CHAN_INFO_AVERAGE_RAW,                     // 硬件多次采样的原始平均值
	IIO_CHAN_INFO_LOW_PASS_FILTER_3DB_FREQUENCY,   // 低通滤波器的 3dB 截止频率
	IIO_CHAN_INFO_SAMP_FREQ,                       // 设备的采样频率(Hz)
	IIO_CHAN_INFO_FREQUENCY,                       // 产生的波形频率或输出频率
	IIO_CHAN_INFO_PHASE,                           // 信号的相位偏移
	IIO_CHAN_INFO_HARDWAREGAIN,                    // 硬件放大器的增益倍数
	IIO_CHAN_INFO_HYSTERESIS,                      // 阈值检测或控制中的滞后范围
	IIO_CHAN_INFO_INT_TIME,                        // 传感器的积分时间(曝光时间)
	IIO_CHAN_INFO_ENABLE,                          // 通道或功能的使能/禁用状态
	IIO_CHAN_INFO_CALIBHEIGHT,                     // 传感器安装高度校准(常用于飞行时间或压力传感器)
	IIO_CHAN_INFO_CALIBWEIGHT,                     // 传感器负重校准
	IIO_CHAN_INFO_DEBOUNCE_COUNT,                  // 去抖动采样的次数
	IIO_CHAN_INFO_DEBOUNCE_TIME,                   // 去抖动的时间常数
};

比如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属性是相互独立的。

1.4.5.5 scan_index

这个值不仅是计数,也决定数据在缓冲区中的排列顺序。

在IIO中,当用户空间应用程序打开/dev/iio:deviceX读数据时,内核会把传感器的数据打包成一个字节流。打包的顺序就是按照scan_index从小到大排的。

二、IIO配置

2.1 可视化配置

bash 复制代码
cd /PATH TO linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
make menuconfig

进入可视化配置,找到Device Drivers:

进入Device Drivers,按Y选中Imdustrial I/O support:

进入Imdustrial I/O support,按Y选中下面这两个:

退出即可

2.2 重新编译内核

bash 复制代码
make -j12

将新编译出来的zImage放到ftfp下面:

bash 复制代码
cp arch/arm/boot/zImage  /PATH TO/tftpboot/ -f

三、ICM20608代码

我用的是mini板,并没有icm20608,因此第三部分的代码也只是看了一遍教程,并没有实际跑过,不知是否正确(所以才又加了第四部分的mpu6050驱动:( )

这一部分用到的icm20608寄存器手册可以在"【正点原子】阿尔法Linux开发板(A盘)-基础资料\06、硬件资料\01、芯片资料"中找到

3.0 文件结构

cpp 复制代码
27_IIO (工作区)
├── .vscode
│   ├── c_cpp_properties.json
│   └── settings.json
├── 27_iio.code-workspace
├── Makefile
├── icm20608reg.h
├── icm20608.c
└── icm20608APP.c

3.1 寄存器地址宏定义

从【正点原子】阿尔法Linux开发板(A盘)-基础资料\01、例程源码\02、Linux驱动例程\22_spi里面直接把icm20608reg.h挪过来:

cpp 复制代码
#ifndef _BSP_ICM20608_H
#define _BSP_ICM20608_H
/***************************************************************
Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
文件名	: 	 bsp_icm20608.h
作者	   : 左忠凯
版本	   : V1.0
描述	   : ICM20608驱动文件。
其他	   : 无
论坛 	   : www.wtmembed.com
日志	   : 初版V1.0 2019/3/26 左忠凯创建
***************************************************************/

#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

3.2 设备树

略了。直接参照SPI (ICM20608驱动+0.96寸OLED驱动)的3.1部分修改即可。

3.3 配置iio

驱动代码直接在之前增加了regmapicm20608代码基础上修改,增加iio子系统即可。

因为有了iio,现在:

可以删掉:

file_operations以及对应的open、release、read、write等函数;

icm20608_readdata也可以删掉了,可以直接通过??????????????

设备结构体的全局声明,现在可以直接用iio_device_alloc把设备结构体挂到iio_dev的后面去,然后用iio_dev去获取设备结构体的首地址;

需要增加:

probe和remove里面对应的申请、注册、注销、释放;

注册时需要的通道结构体数组icm20608_channels、iio_info结构体;

需要修改:

所有读取全局设备结构体变量的地方。现在没有全局定义的设备结构体了,需要使用iio_priv来获取。

3.3.1 probe & remove函数

先从最简单的开始。probe & remove中增加对应的函数。具体参考1.2/1.2+/1.3部分函数往里填即可。只有填indio_dev的成员时稍微复杂一点。

cpp 复制代码
static int icm20608_probe(struct spi_device* spi){
    int ret = 0;
    struct icm20608_dev *dev;
    struct iio_dev *indio_dev;


    // 申请iio_dev 和 icm20608_dev
    indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*dev));
    if(!indio_dev){
        ret = -ENOMEM;
        pr_err("fail_iio_dev!\r\n");
        goto fail_iio_dev;
    }
    dev = iio_priv(indio_dev); // 获取iio_dev首地址
    dev->spi = spi;
    spi_set_drvdata(spi,indio_dev);


    // regmap初始化
    dev->regmap_config.reg_bits = 8; // 寄存器地址位数
    dev->regmap_config.val_bits = 8; // 寄存器值的位数
    dev->regmap_config.read_flag_mask = 0x80;
    dev->regmap = regmap_init_spi(spi,&dev->regmap_config);
    if(IS_ERR(dev->regmap)){
        pr_err("fail regmap init spi!\r\n");
        ret = PTR_ERR(dev->regmap);
        goto fail_init_regmap;
    }

    // 初始化锁
    mutex_init(&dev->lock);

    // spi设备初始化
    spi->mode = SPI_MODE_0; // 原始MicroWire,MODE0,CPOL=0,CPHA=0
    spi_setup(spi);

    // 设备初始化
    icm20608_dev_init(dev);

    // 初始化iio & 注册 
    indio_dev->dev.parent = &spi->dev;
    indio_dev->channels = icm20608_channels;
    indio_dev->num_channels = ARRAY_SIZE(icm20608_channels); // 7个通道
    indio_dev->name = ICM20608_NAME;
    indio_dev->modes = INDIO_DIRECT_MODE; // 提供sysfs接口
    indio_dev->info = &icm20608_info;

    ret = iio_device_register(indio_dev);
    if(ret < 0){
        dev_err(&spi->dev, "fail iio register\r\n");
        goto fail_iio_register;
    }


    return 0;


fail_init_regmap:
    iio_device_unregister(indio_dev);
fail_iio_register:
    // iio_device_free(indio_dev); // 前面使用了devm申请,所以这里不需要手动free
fail_iio_dev:

    return ret;
}

static int icm20608_remove(struct spi_device* spi){
    int ret = 0;
    struct iio_dev *indio_dev = spi_get_drvdata(spi);
    struct icm20608_dev *dev = iio_priv(indio_dev);

    mutex_destroy(&dev->lock);
    iio_device_unregister(indio_dev);
    regmap_exit(dev->regmap);

    return 0; // 懒得写错误处理了
}
cpp 复制代码
static int icm20608_remove(struct spi_device* spi){
    int ret = 0;
    struct iio_dev *indio_dev = spi_get_drvdata(spi);
    struct icm20608_dev *dev = iio_priv(indio_dev);
    
    iio_device_unregister(indio_dev);
    regmap_exit(dev->regmap);

    return 0; // 懒得写错误处理了
}

3.3.2 配置通道(iio_chan_spec)

首先为了方便管理和以后添加,使用枚举列出所有scan_index:

cpp 复制代码
// ICM20608的扫描元素
enum inv_icm20608_scan {
	INV_ICM20608_SCAN_ACCEL_X,   // 0
	INV_ICM20608_SCAN_ACCEL_Y,   // 1
	INV_ICM20608_SCAN_ACCEL_Z,   // ......
	INV_ICM20608_SCAN_TEMP,
	INV_ICM20608_SCAN_GYRO_X,
	INV_ICM20608_SCAN_GYRO_Y,
	INV_ICM20608_SCAN_GYRO_Z,
	INV_ICM20608_SCAN_TIMESTAMP,
};

定义icm20608的iio_chan_spec结构体数组:

cpp 复制代码
static const struct iio_chan_spec icm20608_channels[] = {
	/* 温度通道 */
	{
		.type = IIO_TEMP,   // 温度类型。在sysfs文件系统中,文件名的前缀是in_temp_*
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW)      // 生成in_temp_raw文件。用户读取它,得到的是原始数值
				            | BIT(IIO_CHAN_INFO_OFFSET)   // 生成in_temp_offset文件。表示零点偏移量
				            | BIT(IIO_CHAN_INFO_SCALE),   // 生成in_temp_scale文件。表示比例因子/分辨率
		.scan_index = INV_ICM20608_SCAN_TEMP,  // 当使用IIO缓冲区读取数据时,温度数据在缓冲区数据流中的排列索引
		.scan_type = {
            .sign = 's',         // signed有符号数(补码形式)
            .realbits = 16,      // 真实数据16位
            .storagebits = 16,   // 存储数据16位
            .shift = 0,          // 数据不需要移位,低位对齐
            .endianness = IIO_BE,// Big Endian大端模式
            },
	},

    // 角度通道
	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_ACCEL_Y),	/* 加速度X轴 */
	ICM20608_CHAN(IIO_ACCEL, IIO_MOD_X, INV_ICM20608_SCAN_ACCEL_X),	/* 加速度Y轴 */
	ICM20608_CHAN(IIO_ACCEL, IIO_MOD_Z, INV_ICM20608_SCAN_ACCEL_Z),	/* 加速度Z轴 */
};

因为加速度、角度的三个轴基本都是一样的。为了提高复用,使用了ICM20608_CHAN宏定义来定义这两个通道的iio_chan_spec数组元素:

cpp 复制代码
#define ICM20608_CHAN(_type, _channel2, _index)                    \   
	{                                                             \
		.type = _type,                                        \
		.modified = 1,/*modified = 1表示channel2为通道修饰符*/   \
		.channel2 = _channel2,/*通道修饰符*/                    \
		.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), /*同一个type下,共享SCALE分辨率文件。如果放到info_mask_separate里面去,就会出现x/y/z的scale*/  \
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |	/*separate,各自独立、不共享的信息*/      \
				              BIT(IIO_CHAN_INFO_CALIBBIAS), /*校准值。每个轴的硬件误差不同,所以偏移量校准是独立的*/  \
		.scan_index = _index,                                 \
		.scan_type = {          \
            .sign = 's',          /* signed有符号数(补码形式)*/     \
            .realbits = 16,       /* 真实数据16位 */ \
            .storagebits = 16,    /* 存储数据16位 */   \
            .shift = 0,           /* 数据不需要移位,低位对齐 */       \
            .endianness = IIO_BE, /* Big Endian大端模式 */          \
            },                                                     \
	}

配置好以后,probe中将配置好的icm20608_channels传给iio_dev的channels成员、然后iio_device_register。然后每当modprobe之后,/sys/bus/iio/devices/iio:device0下面就能看到生成的文件了。

此时将.ko文件cp到nfs下面,然后modprobe应该就能看到对应的文件了。我这里并没有icm20608,所以直接截了视频的图:

3.4 配置 iio_info

3.4.1 分辨率数组

cpp 复制代码
#define ICM20608_TEMP_OFFSET 0
#define ICM20608_TEMP_SCALE 326800000  // 温度传感器的比例:326.8 LSB/°C,这里乘了10^6
                                       // 温度传感器量程只有一个,所以只有一个比例尺

/* 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};

这些数组元素怎么来的详见附录B,就不在这个位置写了,不然乱七八糟的(但是已经乱七八糟了)

3.4.2 read_raw函数

接下来完成iio_info的配置,主要是其中的read_raw和write_raw函数的编写。当读取/写入/sys/bus/iio/devices/iio:deviceX下面的文件时,就会调用这两个函数。但是这个路径下的文件种类很多,以icm20608为例,有三个轴的角度原始数据,有三个轴的加速度原始数据,有温度,有分辨率/比例因子,有offset,都要在这函数中进行区分、分别处理。

按照命名规则(详见附录A),共有4个部分:

我们先判断iio_chan_spec的mask(现在有raw、scale、offset、calibbias四类)

然后判断iio_chan_spec.type(如果是raw和scale有temperature、angle、accelerate三类,如果是offset和calibbias有一类)

再判断通道修饰符iio_chan_spec.channel2(如果是angle和accel有xyz三类,temp则有一类,这部分放到了icm20608_sensor_show函数中判断)

突出一个又臭又长,头都大了

cpp 复制代码
// 从icm20608读取数据。角度、加速度、温度
// reg:寄存器首地址
// axis:通道,比如xyz
// val:存储读取到的值
static int icm20608_sensor_show(struct icm20608_dev *dev, int reg,int axis, int *val){
    int ind, result;
    short d;
    ind = (axis - IIO_MOD_X) * 2; // ind是偏移量
        // icm20608每个寄存器8位,一个数据要16位
        // 而对于角度、加速度来说,都是以X轴高8位寄存器地址为起始
        // 所以当axis=IIO_MOD_X,ind=0;axis=IIO_MOD_Y,ind=2;axis=IIO_MOD_Z,ind=4;
    result = regmap_bulk_read(dev->regmap, reg + ind, (u8 *)&d, 2); // 以reg+ind为首地址,连续读取2个寄存器,数据存到d
    if(result)return -EINVAL;
    *val = (short)be16_to_cpup(&d);// 一个宏。将d的大端格式转换成小端
    return 0;
}


// 读取iio icm20608生成的文件,就会调用这个函数
// 比如在终端使用cat读取,就会执行
static int icm20608_read_raw(struct iio_dev *indio_dev,struct iio_chan_spec const *chan, int *val, int *val2, long mask){
    int ret=0;
    struct icm20608_dev *dev = iio_priv(indio_dev);
    unsigned char regdata = 0;

    switch(mask){
        case IIO_CHAN_INFO_RAW:   // RAW:有温度、加速度、角度共3个
            iio_device_claim_direct_mode(indio_dev); // 并发保护
            // 当设备处于Buffer Mode"缓冲模式(通常用于高频数据采集并送到FIFO)时,通常不允许通过sysfs直接读取寄存器,会干扰总线或中断
            // 这个函数确保只有在非Buffer模式下才能读数据。

            mutex_lock(&dev->lock); // 防止多进程同时读取
            switch(chan->type){
                case IIO_TEMP:
                    ret = icm20608_sensor_show(dev, ICM20_TEMP_OUT_H, IIO_MOD_X, val); // 只有一个,就输入IIO_MOD_X骗过icm20608_sensor_show的逻辑
                    break;
                case IIO_ANGL_VEL:
                    ret = icm20608_sensor_show(dev, ICM20_GYRO_XOUT_H, chan->channel2, val);
                    break;    
                case IIO_ACCEL:
                    ret = icm20608_sensor_show(dev, ICM20_ACCEL_XOUT_H, chan->channel2, val);
                    break;
                default:
                    ret = -EINVAL;
            }
            mutex_unlock(&dev->lock); 
            iio_device_release_direct_mode(indio_dev);  // 如果你这里报错,看附录D
            return ret;

        case IIO_CHAN_INFO_SCALE: // SCAL:有温度、加速度、角度共3个
            switch(chan->type){
                case IIO_TEMP:
                    *val = ICM20608_TEMP_SCALE/ 1000000;
                    *val2 = ICM20608_TEMP_SCALE % 1000000;
                    return IIO_VAL_INT_PLUS_MICRO; /* val + val2/1000000 */
                case IIO_ANGL_VEL:
                    mutex_lock(&dev->lock); 
                    ret = icm20608_read_one_reg(dev, ICM20_GYRO_CONFIG, &regdata);  // ICM20_GYRO_CONFIG是陀螺仪配置寄存器
                    if (ret) {
                        mutex_unlock(&dev->lock);
                        return ret;
                    }
                    regdata = (regdata & 0X18) >> 3;  // bit3、bit4表示量程选择
                    *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); 
                    ret = icm20608_read_one_reg(dev, ICM20_ACCEL_CONFIG, &regdata);  // ICM20_GYRO_CONFIG是陀螺仪配置寄存器
                    if (ret) {
                        mutex_unlock(&dev->lock);
                        return ret;
                    }
                    regdata = (regdata & 0X18) >> 3;  // bit3、bit4表示量程选择
                    *val = 0;
                    *val2 = accel_scale_icm20608[regdata];
                    mutex_unlock(&dev->lock); 
                    return IIO_VAL_INT_PLUS_NANO;/* val + val2/1000000000 */
                default:
                    return -EINVAL;
            }

        case IIO_CHAN_INFO_OFFSET:// 偏移值:1个
            switch (chan->type){
                case IIO_TEMP:
                    *val = ICM20608_TEMP_OFFSET;
                    return IIO_VAL_INT;
                default:
                    return -EINVAL;
            }

        case IIO_CHAN_INFO_CALIBBIAS:// 校准值:1个
            switch (chan->type) {
                case IIO_ANGL_VEL:  // angle的校准值
                    mutex_lock(&dev->lock);
                    ret = icm20608_sensor_show(dev, ICM20_XG_OFFS_USRH,chan->channel2, val); // ICM20_XG_OFFS_USRH为陀螺仪offset寄存器的首地址
                                                                                             // 虽然他叫offset,但并不是上面case的那个offset,而是校准值Calibbias,抽象咧
                    mutex_unlock(&dev->lock);
                    return ret;
                case IIO_ACCEL:     // accelerate的校准值
                    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 -EINVAL;
    }
}

3.4.3 write_raw

最后写write_raw函数,当用户空间向驱动写数据的时候就会调用。主要是用户空间向驱动写一些配置参数,比如配置量程、校准值等。

3.4.3.1 配置陀螺仪量程函数
cpp 复制代码
// 配置icm20608陀螺仪量程
// val:scale的值
// return:0成功,else失败
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(val == gyro_scale_icm20608[i]){
            d = i << 3;
            result = regmap_write(dev->regmap, ICM20_GYRO_CONFIG, d);
            if(result)return result; // 错误
            return 0;
        }
    }
    return -EINVAL;
}

但感觉不严谨啊,还是应该先读取、再覆盖[4:3]、再修改[4:3]、再写回,所以完整代码为:

static int icm20608_write_gyro_scale(struct icm20608_dev *dev,int val){
    int result, i, ret;
    u8 data;

    for(i=0;i<ARRAY_SIZE(gyro_scale_icm20608);i++){
        if(val == gyro_scale_icm20608[i]){
            ret = icm20608_read_one_reg(dev, ICM20_GYRO_CONFIG, &data); // 这个函数是上一篇文章regmap里的代码
                                                        // 但是有修改,将读到的值以指针返回,return只返回错误码
            if(ret)return ret; // 读取数据失败
            data &= 0xE7; // 清空[4:3]两位
            data |= (i << 3); // 覆盖[4:3]两位
            result = regmap_write(dev->regmap, ICM20_GYRO_CONFIG, data);
            if(result)return result; // 错误
            return 0;
        }
    }
    return -EINVAL; // 没有对应量程
}

然后!gemini提醒我有regmap_update_bits这么个函数(函数具体详见附录C),所以就可以写成:

static int icm20608_write_gyro_scale(struct icm20608_dev *dev,int val){
    int i, ret;

    for(i=0;i<ARRAY_SIZE(gyro_scale_icm20608);i++){
        if(val == gyro_scale_icm20608[i]){
            ret = regmap_update_bits(dev->regmap, ICM20_GYRO_CONFIG, BIT(3)|BIT(4), (i<<3));
            if(ret)return ret; // 错误
            return 0;
        }
    }
    return -EINVAL; // 没有对应量程
}

其中,i << 3的意思是:

ICM20_GYRO_CONFIG[4:3]负责配置陀螺仪量程,

ICM20_GYRO_CONFIG[4:3]=00时量程为±250dps;

ICM20_GYRO_CONFIG[4:3]=01时量程为±500dps;

ICM20_GYRO_CONFIG[4:3]=10时量程为±1000dps;

ICM20_GYRO_CONFIG[4:3]=11时量程为±2000dps;

当val = 7629 = gyro_scale_icm20608[0] -> i == 0b00 -> 250dps

当val = 15258= gyro_scale_icm20608[1] -> i == 0b01 -> 500dps

当val = 30517= gyro_scale_icm20608[2] -> i == 0b10 -> 1000°/s

当val = 61035= gyro_scale_icm20608[3] -> i == 0b11 -> 2000°/s

这样,将i << 3覆盖到[4:3]位上再写回即可完成对应的量程修改。

3.4.3.2 配置加速度计量程函数

和3.4.3.1的代码基本一样,甚至寄存器也是[4:3]配置量程,只是0b00~0b11分别对应±2/4/8/16的量程。

那么函数就是:

cpp 复制代码
// 配置加速度计量程
static int icm20608_write_accel_scale(struct icm20608_dev *dev,int val){
    int i, ret;

    for(i=0;i<ARRAY_SIZE(accel_scale_icm20608);i++){
        if(val == accel_scale_icm20608[i]){
            ret = regmap_update_bits(dev->regmap, ICM20_ACCEL_CONFIG, BIT(3)|BIT(4), (i<<3));
            if(ret)return ret; // 错误
            return 0;
        }
    }
    return -EINVAL; // 没有对应量程 
}
3.4.3.3 向icm20608寄存器写数据
cpp 复制代码
// 向icm20608写数据。用于配置陀螺仪和加速度计
// reg:寄存器首地址
// axis:通道,比如xyz
// val:要设置的值
// return:0成功,else失败
static int icm20608_sensor_set(struct icm20608_dev *dev, int reg,int axis, int val){
    int ind, result;
    short d = cpu_to_be16(val);
    ind = (axis - IIO_MOD_X) * 2; // ind是偏移量
        // icm20608每个寄存器8位,一个数据要16位
        // 而对于角度、加速度来说,都是以X轴高8位寄存器地址为起始
        // 所以当axis=IIO_MOD_X,ind=0;axis=IIO_MOD_Y,ind=2;axis=IIO_MOD_Z,ind=4;
    result = regmap_bulk_write(dev->regmap, reg + ind, (u8 *)&d, 2); // 将数据d 写入到 以reg+ind为首地址 的连续2个寄存器
    if(result)return result;
    return 0;
}

write_raw中,陀螺仪和加速度计的修改校正值CALIBBIAS都使用了这个函数。但是加速度计的offset寄存器是0x77 0x78-(没有0x79)-0x7A 0x7B-(没有0x7C)-0x7D 0x7E,并不是两个两个的。但是教程中又是这么写的。我这里并没有icm20608,也没法验证。

3.4.3.4 write_raw

终于到write_raw了。幸好没有read_raw那么长:

cpp 复制代码
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: // 配置陀螺仪、加速度计的scale
        switch (chan->type){  // 陀螺仪
        case IIO_ANGL_VEL:
            mutex_lock(&dev->lock);
            ret = icm20608_write_gyro_scale(dev, val2); // scale都是小于1的小数,所以用val2,不需要val
            mutex_unlock(&dev->lock);
            return ret;
        case IIO_ACCEL:  // 加速度
            mutex_lock(&dev->lock);
            ret = icm20608_write_accel_scale(dev, val2); // scale都是小于1的小数,所以用val2,不需要val
            mutex_unlock(&dev->lock);
            return ret;
        default: return -EINVAL;
        }
    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);
            return ret;
        case IIO_ACCEL:  // 加速度
            mutex_lock(&dev->lock);
            ret = icm20608_sensor_set(dev,ICM20_XA_OFFSET_H, chan->channel2, val);
            mutex_unlock(&dev->lock);
            return ret;
        default: return -EINVAL;
        }
    default: return -EINVAL;
    }
}

可以发现,这里面没有case IIO_TEMP,因为温度传感器的量程是写死的。不写case IIO_TEMP也意味着只读,如果用户想修改温度传感器的scale就会报错。

3.4.4 write_raw_get_fmt

write_raw_get_fmt函数决定了wtite_raw函数中val和val2的意义。

比如要在应用程序 中设置加速度计的量程为±8g,那么分辨率就是|8-(-8)| / 2^16 = 16 / 65536 ≈ 0.000244,我们在write_raw_get_fmt函数里面设置加速度计的数据格式为 IIO_VAL_INT_PLUS_MICRO。那么应用程序 向指定的文件写入0.000244以后,最终传递给内核驱动的就是0.000244*1000000=244。也就是write_raw函数的val为0,val2为244。

从前面read_raw的case IIO_CHAN_INFO_SCALE部分的return值可以看到,加速度计是return IIO_VAL_INT_PLUS_NANO,温度计和陀螺仪是return IIO_VAL_INT_PLUS_MICRO,因此write_raw_get_fmt可以写为:

cpp 复制代码
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: // SCAL:有温度、加速度、角度共3个
            switch(chan->type){
                case IIO_ANGL_VEL: return IIO_VAL_INT_PLUS_MICRO; // 陀螺仪
                default :return IIO_VAL_INT_PLUS_NANO; // 加速度、温度
            }
        default: return IIO_VAL_INT_PLUS_MICRO;
    }
    return -EINVAL;
}

write_raw_get_fmt函数会在调用write_raw之前被调用。

如果现在用户想修改温度传感器的scale:

①首先调用icm20608_write_raw_get_fmt函数,返回IIO_VAL_INT_PLUS_NANO

②然后进入icm20608_write_raw函数。然后在icm20608_write_raw函数中,发现并没有对应的case IIO_TEMP,即表示温度传感器的scale只读,不准改,就会返回-EINVAL。

③然后用户就会得到一句Write Error: Invalid Argument。

3.4.5 iio_info

终于集齐三个函数,能召唤iio_info了,要命了

cpp 复制代码
static const struct iio_info icm20608_info = {
    // .attrs = ,
    .read_raw = icm20608_read_raw,
    .write_raw = icm20608_write_raw,
    .write_raw_get_fmt = icm20608_write_raw_get_fmt,
    .driver_module = THIS_MODULE,
};

3.5 完整驱动代码

综上,完整的驱动代码如下。

(如果遇到了iio_device_release_direct_mode未定义的报错看附录D)

cpp 复制代码
#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/spinlock_types.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>
#include <linux/bitops.h>



#include<linux/input.h>
#include<linux/fs.h>
#include<linux/string.h>
#include<linux/of_gpio.h>
#include "icm20608reg.h"

#define ICM20608_CNT 1
#define ICM20608_NAME "icm20608"

#define ICM20608_TEMP_OFFSET 0
#define ICM20608_TEMP_SCALE 326800000

/* 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};

struct icm20608_dev{
    struct spi_device *spi;
    struct regmap *regmap;
    struct regmap_config regmap_config;
    struct mutex lock;   // 读取数据时要互斥
};



///////////////////////通道配置///////////////////////

#define ICM20608_CHAN(_type, _channel2, _index)                    \   
	{                                                             \
		.type = _type,                                        \
		.modified = 1,/*modified = 1表示channel2为通道修饰符*/   \
		.channel2 = _channel2,/*通道修饰符*/                    \
		.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), /*同一个type下,共享SCALE分辨率文件。如果放到info_mask_separate里面去,就会出现x/y/z的scale*/  \
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |	/*separate,各自独立、不共享的信息*/      \
				              BIT(IIO_CHAN_INFO_CALIBBIAS), /*校准值。每个轴的硬件误差不同,所以偏移量校准是独立的*/  \
		.scan_index = _index,                                 \
		.scan_type = {          \
            .sign = 's',          /* signed有符号数(补码形式)*/     \
            .realbits = 16,       /* 真实数据16位 */ \
            .storagebits = 16,    /* 存储数据16位 */   \
            .shift = 0,           /* 数据不需要移位,低位对齐 */       \
            .endianness = IIO_BE, /* Big Endian大端模式 */          \
            },                                                     \
	}

// ICM20608的扫描元素
enum inv_icm20608_scan {
	INV_ICM20608_SCAN_ACCEL_X,   // 0
	INV_ICM20608_SCAN_ACCEL_Y,   // 1
	INV_ICM20608_SCAN_ACCEL_Z,   // ......
	INV_ICM20608_SCAN_TEMP,
	INV_ICM20608_SCAN_GYRO_X,
	INV_ICM20608_SCAN_GYRO_Y,
	INV_ICM20608_SCAN_GYRO_Z,
	INV_ICM20608_SCAN_TIMESTAMP,
};

static const struct iio_chan_spec icm20608_channels[] = {
	/* 温度通道 */
	{
		.type = IIO_TEMP,   // 温度类型。在sysfs文件系统中,文件名的前缀是in_temp_*
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW)      // 生成in_temp_raw文件。用户读取它,得到的是原始数值
				            | BIT(IIO_CHAN_INFO_OFFSET)   // 生成in_temp_offset文件。表示零点偏移量
				            | BIT(IIO_CHAN_INFO_SCALE),   // 生成in_temp_scale文件。表示比例因子/分辨率
		.scan_index = INV_ICM20608_SCAN_TEMP,  // 当使用IIO缓冲区读取数据时,温度数据在缓冲区数据流中的排列索引
		.scan_type = {
            .sign = 's',         // signed有符号数(补码形式)
            .realbits = 16,      // 真实数据16位
            .storagebits = 16,   // 存储数据16位
            .shift = 0,          // 数据不需要移位,低位对齐
            .endianness = IIO_BE,// Big Endian大端模式
            },
	},
    // 角度通道
	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_ACCEL_Y),	/* 加速度X轴 */
	ICM20608_CHAN(IIO_ACCEL, IIO_MOD_X, INV_ICM20608_SCAN_ACCEL_X),	/* 加速度Y轴 */
	ICM20608_CHAN(IIO_ACCEL, IIO_MOD_Z, INV_ICM20608_SCAN_ACCEL_Z),	/* 加速度Z轴 */
};

// 从icm20608读取数据。角度、加速度、温度
// reg:寄存器首地址
// axis:通道,比如xyz
// val:存储读取到的值
// return:0成功,else失败
static int icm20608_sensor_show(struct icm20608_dev *dev, int reg,int axis, int *val){
    int ind, result;
    short d;
    ind = (axis - IIO_MOD_X) * 2; // ind是偏移量
        // icm20608每个寄存器8位,一个数据要16位
        // 而对于角度、加速度来说,都是以X轴高8位寄存器地址为起始
        // 所以当axis=IIO_MOD_X,ind=0;axis=IIO_MOD_Y,ind=2;axis=IIO_MOD_Z,ind=4;
    result = regmap_bulk_read(dev->regmap, reg + ind, (u8 *)&d, 2); // 以reg+ind为首地址,连续读取2个寄存器,数据存到d
    if(result)return -EINVAL;
    *val = (short)be16_to_cpup(&d);// 一个宏。将d的大端格式转换成小端
    return 0;
}

// 读取iio icm20608生成的文件,就会调用这个函数
// 比如在终端使用cat读取,就会执行
static int icm20608_read_raw(struct iio_dev *indio_dev,struct iio_chan_spec const *chan, int *val, int *val2, long mask){
    int ret=0;
    struct icm20608_dev *dev = iio_priv(indio_dev);
    unsigned char regdata = 0;

    switch(mask){
        case IIO_CHAN_INFO_RAW:   // RAW:有温度、加速度、角度共3个
            iio_device_claim_direct_mode(indio_dev); // 并发保护
            // 当设备处于Buffer Mode"缓冲模式(通常用于高频数据采集并送到FIFO)时,通常不允许通过sysfs直接读取寄存器,会干扰总线或中断
            // 这个函数确保只有在非Buffer模式下才能读数据。

            mutex_lock(&dev->lock); // 防止多进程同时读取
            switch(chan->type){
                case IIO_TEMP:
                    ret = icm20608_sensor_show(dev, ICM20_TEMP_OUT_H, IIO_MOD_X, val); // 只有一个,就输入IIO_MOD_X骗过icm20608_sensor_show的逻辑
                    break;
                case IIO_ANGL_VEL:
                    ret = icm20608_sensor_show(dev, ICM20_GYRO_XOUT_H, chan->channel2, val);
                    break;    
                case IIO_ACCEL:
                    ret = icm20608_sensor_show(dev, ICM20_ACCEL_XOUT_H, chan->channel2, val);
                    break;
                default:
                    ret = -EINVAL;
            }
            mutex_unlock(&dev->lock); 
            iio_device_release_direct_mode(indio_dev);
            return ret;

        case IIO_CHAN_INFO_SCALE: // SCAL:有温度、加速度、角度共3个
            switch(chan->type){
                case IIO_TEMP:
                    *val = ICM20608_TEMP_SCALE/ 1000000;
                    *val2 = ICM20608_TEMP_SCALE % 1000000;
                    return IIO_VAL_INT_PLUS_MICRO; /* val + val2/1000000 */
                case IIO_ANGL_VEL:
                    mutex_lock(&dev->lock); 
                    ret = icm20608_read_one_reg(dev, ICM20_GYRO_CONFIG, &regdata);  // ICM20_GYRO_CONFIG是陀螺仪配置寄存器
                    if (ret) {
                        mutex_unlock(&dev->lock);
                        return ret;
                    }
                    regdata = (regdata & 0X18) >> 3;  // bit3、bit4表示量程选择
                    *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); 
                    ret = icm20608_read_one_reg(dev, ICM20_ACCEL_CONFIG, &regdata);  // ICM20_GYRO_CONFIG是陀螺仪配置寄存器
                    if (ret) {
                        mutex_unlock(&dev->lock);
                        return ret;
                    }
                    regdata = (regdata & 0X18) >> 3;  // bit3、bit4表示量程选择
                    *val = 0;
                    *val2 = accel_scale_icm20608[regdata];
                    mutex_unlock(&dev->lock); 
                    return IIO_VAL_INT_PLUS_NANO;/* val + val2/1000000000 */
                default:
                    return -EINVAL;
            }

        case IIO_CHAN_INFO_OFFSET:// 偏移值:1个
            switch (chan->type){
                case IIO_TEMP:
                    *val = ICM20608_TEMP_OFFSET;
                    return IIO_VAL_INT;
                default:
                    return -EINVAL;
            }

        case IIO_CHAN_INFO_CALIBBIAS:// 校准值:1个
            switch (chan->type) {
                case IIO_ANGL_VEL:  // angle的校准值
                    mutex_lock(&dev->lock);
                    ret = icm20608_sensor_show(dev, ICM20_XG_OFFS_USRH,chan->channel2, val); // ICM20_XG_OFFS_USRH为陀螺仪offset寄存器的首地址
                                                                                             // 虽然他叫offset,但并不是上面case的那个offset,而是校准值Calibbias,抽象咧
                    mutex_unlock(&dev->lock);
                    return ret;
                case IIO_ACCEL:     // accelerate的校准值
                    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 -EINVAL;
    }
}


// 向icm20608写数据。用于配置陀螺仪和加速度计
// reg:寄存器首地址
// axis:通道,比如xyz
// val:要设置的值
// return:0成功,else失败
static int icm20608_sensor_set(struct icm20608_dev *dev, int reg,int axis, int val){
    int ind, result;
    short d = cpu_to_be16(val);
    ind = (axis - IIO_MOD_X) * 2; // ind是偏移量
        // icm20608每个寄存器8位,一个数据要16位
        // 而对于角度、加速度来说,都是以X轴高8位寄存器地址为起始
        // 所以当axis=IIO_MOD_X,ind=0;axis=IIO_MOD_Y,ind=2;axis=IIO_MOD_Z,ind=4;
    result = regmap_bulk_write(dev->regmap, reg + ind, (u8 *)&d, 2); // 将数据d 写入到 以reg+ind为首地址 的连续2个寄存器
    if(result)return result;
    return 0;
}
 

// 配置icm20608陀螺仪量程
// val:scale的值
// return:0成功,else失败
static int icm20608_write_gyro_scale(struct icm20608_dev *dev,int val){
    int i, ret;

    for(i=0;i<ARRAY_SIZE(gyro_scale_icm20608);i++){
        if(val == gyro_scale_icm20608[i]){
            ret = regmap_update_bits(dev->regmap, ICM20_GYRO_CONFIG, BIT(3)|BIT(4), (i<<3));
            if(ret)return ret; // 错误
            return 0;
        }
    }
    return -EINVAL; // 没有对应量程
}
// 配置加速度计量程
static int icm20608_write_accel_scale(struct icm20608_dev *dev,int val){
    int i, ret;

    for(i=0;i<ARRAY_SIZE(accel_scale_icm20608);i++){
        if(val == accel_scale_icm20608[i]){
            ret = regmap_update_bits(dev->regmap, ICM20_ACCEL_CONFIG, BIT(3)|BIT(4), (i<<3));
            if(ret)return ret; // 错误
            return 0;
        }
    }
    return -EINVAL; // 没有对应量程 
}


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: // 配置陀螺仪、加速度计的scale
        switch (chan->type){  // 陀螺仪
        case IIO_ANGL_VEL:
            mutex_lock(&dev->lock);
            ret = icm20608_write_gyro_scale(dev, val2); // scale都是小于1的小数,所以用val2,不需要val
            mutex_unlock(&dev->lock);
            return ret;
        case IIO_ACCEL:  // 加速度
            mutex_lock(&dev->lock);
            ret = icm20608_write_accel_scale(dev, val2); // scale都是小于1的小数,所以用val2,不需要val
            mutex_unlock(&dev->lock);
            return ret;
        default: return -EINVAL;
        }
    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);
            return ret;
        case IIO_ACCEL:  // 加速度
            mutex_lock(&dev->lock);
            ret = icm20608_sensor_set(dev,ICM20_XA_OFFSET_H, chan->channel2, val);
            mutex_unlock(&dev->lock);
            return ret;
        default: return -EINVAL;
        }
    default: return -EINVAL;
    }
}

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: // SCAL:有温度、加速度、角度共3个
            switch(chan->type){
                case IIO_ANGL_VEL: return IIO_VAL_INT_PLUS_MICRO; // 陀螺仪
                default :return IIO_VAL_INT_PLUS_NANO; // 加速度、温度
            }
        default: return IIO_VAL_INT_PLUS_MICRO;
    }
    return -EINVAL;
}





static const struct iio_info icm20608_info = {
    // .attrs = ,
    .read_raw = icm20608_read_raw,
    .write_raw = icm20608_write_raw,
    .write_raw_get_fmt = icm20608_write_raw_get_fmt,
    .driver_module = THIS_MODULE,
};




// SPI读
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len){
    int ret;
    ret = regmap_bulk_read(dev->regmap, reg, buf, len);

    if (ret)
        pr_err("icm20608: spi_read reg 0x%x failed %d\n", reg, ret);
    return ret;
}
static int icm20608_read_one_reg(struct icm20608_dev *dev, u8 reg, u8 *val){
    int ret = 0;
    unsigned int data = 0;
    ret = regmap_read(dev->regmap, reg, &data);
    if(ret){
        pr_err("icm20608: spi_read reg 0x%x failed %d\n", reg, ret);
        return ret;
    }
    *val = (u8)data;
    return 0;
}

// SPI写
static int icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, int len){
    int ret = 0;
    ret = regmap_bulk_write(dev->regmap, reg, buf, len);
    if (ret < 0)
        pr_err("icm20608: spi_write reg 0x%x failed %d\n", reg, ret);
    return ret;
}
static int icm20608_write_one_reg(struct icm20608_dev *dev, u8 reg, u8 value){
    int ret = 0;
    ret = regmap_write(dev->regmap, reg, value);
    if (ret < 0)
        pr_err("icm20608: spi_write reg 0x%x failed: %d\n", reg, ret);
    return ret;
}




// icm20608初始化
void icm20608_dev_init(struct icm20608_dev *dev){
    u8 value = 0;
    u8 t = 0x80;
    icm20608_write_regs(dev, ICM20_PWR_MGMT_1, &t, 1); // 复位。复位后会自动进入睡眠模式
    mdelay(50);
    t = 0x01;
    icm20608_write_regs(dev, ICM20_PWR_MGMT_1, &t,1); // 关闭睡眠模式,自动选择时钟
    mdelay(50);

    icm20608_read_regs(dev, ICM20_WHO_AM_I, &value, 1);
    printk("ICM20608 ID = %#x\r\n", value);

    t = 0x00;
    icm20608_write_regs(dev, ICM20_SMPLRT_DIV, &t, 1); // 输出速率=内部采样率
    t = 0x18;
    icm20608_write_regs(dev, ICM20_GYRO_CONFIG, &t, 1); // 陀螺仪±2000dps量程
    t = 0x18;
    icm20608_write_regs(dev, ICM20_ACCEL_CONFIG, &t, 1); // 加速度计±16g量程
    t = 0x04;
    icm20608_write_regs(dev, ICM20_CONFIG, &t, 1); // 陀螺仪低通滤波BW=20Hz
    t = 0x04;
    icm20608_write_regs(dev, ICM20_ACCEL_CONFIG2, &t, 1); // 加速度计低通滤波BW=21.2Hz
    t = 0x00;
    icm20608_write_regs(dev, ICM20_PWR_MGMT_2, &t, 1); // 打开加速度计和陀螺仪所有轴
    t = 0x00;
    icm20608_write_regs(dev, ICM20_LP_MODE_CFG, &t, 1); // 关闭低功耗
    t = 0x00;
    icm20608_write_regs(dev, ICM20_FIFO_EN, &t, 1); // 关闭FIFO
}


///驱动结构体/////////////////////////////////////////////////////////////////////////////////////

static int icm20608_probe(struct spi_device* spi){
    int ret = 0;
    struct icm20608_dev *dev;
    struct iio_dev *indio_dev;


    // 申请iio_dev 和 icm20608_dev
    indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*dev));
    if(!indio_dev){
        ret = -ENOMEM;
        pr_err("fail_iio_dev!\r\n");
        goto fail_iio_dev;
    }
    dev = iio_priv(indio_dev); // 获取iio_dev首地址
    dev->spi = spi;
    spi_set_drvdata(spi,indio_dev);


    // regmap初始化
    dev->regmap_config.reg_bits = 8; // 寄存器地址位数
    dev->regmap_config.val_bits = 8; // 寄存器值的位数
    dev->regmap_config.read_flag_mask = 0x80;
    dev->regmap = regmap_init_spi(spi,&dev->regmap_config);
    if(IS_ERR(dev->regmap)){
        pr_err("fail regmap init spi!\r\n");
        ret = PTR_ERR(dev->regmap);
        goto fail_init_regmap;
    }

    // 初始化锁
    mutex_init(&dev->lock);

    // spi设备初始化
    spi->mode = SPI_MODE_0; // 原始MicroWire,MODE0,CPOL=0,CPHA=0
    spi_setup(spi);

    // 设备初始化
    icm20608_dev_init(dev);

    // 初始化iio & 注册 
    indio_dev->dev.parent = &spi->dev;
    indio_dev->channels = icm20608_channels;
    indio_dev->num_channels = ARRAY_SIZE(icm20608_channels); // 7个通道
    indio_dev->name = ICM20608_NAME;
    indio_dev->modes = INDIO_DIRECT_MODE; // 提供sysfs接口
    indio_dev->info = &icm20608_info;

    ret = iio_device_register(indio_dev);
    if(ret < 0){
        dev_err(&spi->dev, "fail iio register\r\n");
        goto fail_iio_register;
    }


    return 0;


fail_init_regmap:
    iio_device_unregister(indio_dev);
fail_iio_register:
    // iio_device_free(indio_dev); // 前面使用了devm申请,所以这里不需要手动free
fail_iio_dev:

    return ret;
}

static int icm20608_remove(struct spi_device* spi){
    int ret = 0;
    struct iio_dev *indio_dev = spi_get_drvdata(spi);
    struct icm20608_dev *dev = iio_priv(indio_dev);
    
    mutex_destroy(&dev->lock);
    iio_device_unregister(indio_dev);
    regmap_exit(dev->regmap);

    return 0; // 懒得写错误处理了
}


// 传统匹配
static const struct spi_device_id icm20608_id[] = {
    {"alientek,icm20608", 0},
    {  },
};
// 设备数匹配
static const struct of_device_id icm20608_of_match[] = {
    {.compatible = "alientek,icm20608", 0},
    {  },
};

struct spi_driver icm20608_driver = {
    .probe = icm20608_probe,
    .remove = icm20608_remove,
    .driver = {
        .name = "icm20608",
        .owner = THIS_MODULE,
        .of_match_table = icm20608_of_match,
    },
    .id_table = icm20608_id,
};





////////////////////////////////////////////////////////////////////////////////////////
// 驱动入口
static int __init icm20608_init(void){
    int ret = 0;
    ret = spi_register_driver(&icm20608_driver);
    // printk("注册结果:%d", &ret);
    return ret;
}

// 驱动出口
static void __exit icm20608_exit(void){
    spi_unregister_driver(&icm20608_driver);
}



module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");

3.6 应用程序

这部分代码直接从【正点原子】阿尔法Linux开发板(A盘)-基础资料\01、例程源码\02、Linux驱动例程\27_iio\spi里面贴过来即可。

代码主要是以下几个部分。

3.6.1 从iio生成的文件中读取数据

从文件中读到的都是字符串形式:

cpp 复制代码
 /*
 * @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;
}

3.6.2 字符串转浮点数/整数

将file_data_read从文件中读到的字符串转为数字,用于后续计算。

cpp 复制代码
/* 字符串转数字,将浮点小数字符串转换为浮点数数值 */
#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);\

3.6.3 预定义文件路径

可以发现上面的两个函数用了file_path[index]来找文件的路径。因为iio生成的文件名称都是固定的,所以可以直接写好存到数组里,并配合使用枚举作为索引:

(这里注意,自己的设备的icm20608可能并不是device0,需要提前确认一下)

cpp 复制代码
/* icm20608 iio框架对应的文件路径 */
static char *file_path[] = {
	"/sys/bus/iio/devices/iio:device0/in_accel_scale",
	"/sys/bus/iio/devices/iio:device0/in_accel_x_calibbias",
	"/sys/bus/iio/devices/iio:device0/in_accel_x_raw",
	"/sys/bus/iio/devices/iio:device0/in_accel_y_calibbias",
	"/sys/bus/iio/devices/iio:device0/in_accel_y_raw",
	"/sys/bus/iio/devices/iio:device0/in_accel_z_calibbias",
	"/sys/bus/iio/devices/iio:device0/in_accel_z_raw",
	"/sys/bus/iio/devices/iio:device0/in_anglvel_scale",
	"/sys/bus/iio/devices/iio:device0/in_anglvel_x_calibbias",
	"/sys/bus/iio/devices/iio:device0/in_anglvel_x_raw",
	"/sys/bus/iio/devices/iio:device0/in_anglvel_y_calibbias",
	"/sys/bus/iio/devices/iio:device0/in_anglvel_y_raw",
	"/sys/bus/iio/devices/iio:device0/in_anglvel_z_calibbias",
	"/sys/bus/iio/devices/iio:device0/in_anglvel_z_raw",
	"/sys/bus/iio/devices/iio:device0/in_temp_offset",
	"/sys/bus/iio/devices/iio:device0/in_temp_raw",
	"/sys/bus/iio/devices/iio:device0/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,
};

3.6.4 设备结构体

这里列出了所有可能需要读取的数据:

cpp 复制代码
/*
 * 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;  // 顺便定义一下变量

3.6.5 读取函数

调用上面的一堆函数,读物icm20608的三个传感器的所有原始数据,并计算出真实值:

cpp 复制代码
 /*
 * @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;
}

3.6.6 主函数

cpp 复制代码
/*
 * @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;
}

四、MPU6050驱动

这篇有亿点太长了,放到下一篇了:(下)IIO + MPU6050驱动

附录

A. 命名规则

drivers/iio/industrialio-core.c文件中可以看到:

cpp 复制代码
static const char * const iio_direction[] = {
	[0] = "in",
	[1] = "out",
};

static const char * const iio_chan_type_name_spec[] = {
	[IIO_VOLTAGE] = "voltage",
	[IIO_CURRENT] = "current",
	[IIO_POWER] = "power",
	[IIO_ACCEL] = "accel",
    ..................
};

static const char * const iio_modifier_names[] = {
	[IIO_MOD_X] = "x",
	[IIO_MOD_Y] = "y",
	[IIO_MOD_Z] = "z",
    ..................
};

/* relies on pairs of these shared then separate */
static const char * const iio_chan_info_postfix[] = {
	[IIO_CHAN_INFO_RAW] = "raw",
	[IIO_CHAN_INFO_PROCESSED] = "input",
	[IIO_CHAN_INFO_SCALE] = "scale",
    ..................
};

以上图的in_accel_x_raw为例。从3.3.2的代码可以看出,在配置通道时:

"in"(iio_direction)对应iio_chan_spec.output,0为in、1为out;

"accel("iio_chan_type_name_spec)对应iio_chan_spec.type;

"x"(iio_modifier_names)对应通道修饰符iio_chan_spec.channel2;

"raw"(iio_chan_info_postfix)对应iio_chan_spec的mask(如info_mask_separate、info_mask_shared_by_type等等)

比如3.3.2代码中的温度通道配置为:

cpp 复制代码
	{
		.type = IIO_TEMP,   // 温度类型。在sysfs文件系统中,文件名的前缀是in_temp_*
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW)      // 生成in_temp_raw文件。用户读取它,得到的是原始数值
				            | BIT(IIO_CHAN_INFO_OFFSET)   // 生成in_temp_offset文件。表示零点偏移量
				            | BIT(IIO_CHAN_INFO_SCALE),   // 生成in_temp_scale文件。表示比例因子/分辨率
		.scan_index = INV_ICM20608_SCAN_TEMP,  // 当使用IIO缓冲区读取数据时,温度数据在缓冲区数据流中的排列索引
		.scan_type = {
            .sign = 's',         // signed有符号数(补码形式)
            .realbits = 16,      // 真实数据16位
            .storagebits = 16,   // 存储数据16位
            .shift = 0,          // 数据不需要移位,低位对齐
            .endianness = IIO_BE,// Big Endian大端模式
            },
	},

那么就会生成in_temp_raw、in_temp_offset、in_temp_scale三个文件(没有通道修饰符,所以没有第三部分)

B. 分辨率数组的元素

cpp 复制代码
/* 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的相关配置可以在【正点原子】阿尔法Linux开发板(A盘)-基础资料\06、硬件资料\01、芯片资料中找到两个ICM-20608-G的pdf文件)

|-----|------------------|---------------|
| 量程 | 分辨率/比例因子 | 计算公式 |
| ±2 | 0.00006103515625 | 2*2 / 2^16 |
| ±4 | 0.0001220703125 | 2*4 / 2^16 |
| ±8 | 0.000244140625 | 2*8 / 2^16 |
| ±16 | 0.00048828125 | 2*16 / 2^16 |

至于为什么是这个计算公式,详见最开头的块引用部分内容。

然后从1.4.1可以看到,linux最大支持扩大10^9倍,所以给这些小数乘上10^9就得到accel_scale_icm20608数组中的那些元素了。

但是怎么告诉linux我是用的放大倍数是10^9?将IIO_VAL_INT_PLUS_NANO(10^9)作为返回值即可。

算好比例因子以后,就可以得到真实数值 =(原始RAW + 偏移offset)÷ 比例因子Scale + 校准值Calibbias

C. regmap_update_bits函数

gemini提醒我,我才发现有这么个函数,然后才发现上一篇文章里面都写了,我是一点印象都没有...

cpp 复制代码
int regmap_update_bits (
            struct regmap *map, // 要操作的regmap
            unsigned int reg,   // 要操作的寄存器
            unsigned int mask,  // 掩码,要更新的位 设置为1(对应二进制)
            unsigned int val)   // 要更新的位值
                                // return:0成功,else失败

比如要将寄存器的bit0、bit1置1,那么mask设置为0X00000011,val的bit1和bit2设置为1,也就是0bxxxxxx11(也可以写成0xff,反正只修改掩码为1的两位)

如果要清除寄存器的bit4和bit7,那么mask设置为0X10010000,va 的bit4和bit7设置为0,也就是0b0xx0xxxx(也可以写成0x00,反正只修改掩码为1的两位)

另外,①这个函数是自带原子锁的,不需要手动mutex,②如果修改以后和不修改一样,他就会自动不修改,减少通信开销,③配合BIT宏一起用还是很方便的

还有一件事,BIT宏需要#include <linux/bitops.h>,不过新版本linux似乎挪到bits.h里面去了。

D. iio_device_release_direct_mode undefined!

我在编译时遇到了报错:

WARNING: "iio_device_release_direct_mode" [/home/for/linux/imx6ull/linux_drivers/27_iio/icm20608.ko] undefined!

WARNING: "iio_device_claim_direct_mode" [/home/for/linux/imx6ull/linux_drivers/27_iio/icm20608.ko] undefined!

gemini说是因为我linux版本是4.1.15,此时linux还没有引入这两个函数。

正点原子的视频中没有使用这个函数,但是pdf手册里面又写了这两个函数。很迷,直接注释掉吧。

E. PTR_ERR、IS_ERR、ERR_PTR

之前遇到这玩意一直装看不见。现在是决定查一下这是个什么东西了

很多函数是返回指针类型的,但是如果报错的话怎么返回错误码?

内核采用了一种方法:将错误码编码到指针值中。错误码会被映射到内核地址空间的高地址范围,Linux内核保留了虚拟地址空间的最顶端的一小段 (通常是最后 4KB,即0xFFFFF000到0xFFFFFFFF,对应-1到-4095,共2^12-1=4095个位置)专门用来放错误码。

合法指针 永远不会指向这段地址,而错误指针(即PTR_ERR)就是把-ENOMEM、-EINVAL 这种负数强行转换成了unsigned long类型的地址。

ERR_PTR专门用于生成错误指针,将错误码转为指针返回,比如:

cpp 复制代码
return ERR_PTR(-ENOTSUPP);

IS_ERR 用于检查是否是错误指针,也就是检查指针是不是在错误区间内。在做错误处理时,判断要不要用IS_ERR作为判断条件:

1、直接看对应函数的代码,如果有return ERR_PTR(......),就肯定要使用IS_ERR

2、涉及资源分配的子系统(regmap,class,device,gpiod等创建函数)绝大多数都用 IS_ERR。单纯的内存分配(如kmalloc)通常只用判断是不是NULL。

**PTR_ERR:**还原错误码,把ERR_PTR生成的"错误指针"还原成整数,以便打印日志或返回给上层。

在regmap_init_i2c这个函数中,还遇到了ERR_CAST这个函数:

cpp 复制代码
struct regmap *regmap_init_i2c(............){
	const struct regmap_bus *bus = regmap_get_i2c_bus(i2c, config);

	if (IS_ERR(bus))
		return ERR_CAST(bus);

	return ............;
}

上面这个函数进入if里面时,说明bus已经是一个经过ERR_PTR处理的错误指针了,因此直接作为return即可,直接把这个错误指针的值return出去。

所以为什么不直接return bus而是return ERR_CAST(bus)?因为类型不同,bus是一个regmap_bus类型的错误指针,而现在要返回的是regmap类型的错误指针,虽然他们的值都一样,但类型不同会报错。

ERR_CAST(bus)在这里相当于强制类型转换(struct regmap*)bus,只是ERR_CAST更为通用,可以自动适配任何返回类型的函数。

相关推荐
Y1rong1 小时前
刷机与系统启动
linux
zhangrelay1 小时前
thinkpad等笔记本电脑在ubuntu等linux系统下电池校准如何做?
笔记·学习
南梦浅1 小时前
[特殊字符]️ Docker 镜像加速器完整配置流程下面是在 CentOS 7 系统上配置 Docker 镜像加速器的完整步骤
linux·docker·centos
_Kayo_2 小时前
Node.JS 学习笔记8
笔记·学习·node.js
weixin_462446232 小时前
使用 Docker / Docker Compose 部署 PdfDing —— 个人 PDF笔记
笔记·docker·pdf
历程里程碑2 小时前
Linux 2 指令(2)进阶:内置与外置命令解析
linux·运维·服务器·c语言·开发语言·数据结构·ubuntu
天荒地老笑话么2 小时前
Linux 里 chmod 755 file.txt 是什么意思(权限配置)
linux·运维·服务器·网络安全
野生工程师2 小时前
【Linux基础-2】Linux的基本指令
linux·运维·windows
weixin_395448912 小时前
mult_yolov5_post_copy.c_cursor
linux·人工智能·python