视频:第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的首地址,在地址上做加法来获取到私有数据的起始地址:
cppstatic 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
驱动代码直接在之前增加了regmap的icm20608代码基础上修改,增加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, ®data); // 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, ®data); // 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, ®data); // 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, ®data); // 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更为通用,可以自动适配任何返回类型的函数。

