驱动程序开发:基于ICM20608六轴传感器 --- 使用Regmap API 的 SPI 读取数据 之 IIO驱动
目录
- 一、IIO 子系统简介
- 二、IIO子系统使用的一些相关的结构体、函数等
- 1、iio_dev 结构体
- ①modes:是选择iio驱动设备支持的工作模式,模式分别有如下:
- ②dev:其是一个设备结构体。
- ②channels:为 IIO 设备通道规格结构表。
- 三、IIO驱动框架搭建
- 1、SPI基础框架搭建
- 2、基于SPI基础驱动框架上搭建IIO基础驱动框架
- ①测试IIO基础驱动框架程序
- 3、配置IIO设备通道规格结构表
- ①了解通道文件命名方式
- ①编写IIO驱动程序的配置IIO设备通道规格结构表
- 4、添加Regmap API
- 5、完善xxx_read_raw函数,真正实现通道文件可读取传感器数据
- 四、完整的驱动程序
- 1、驱动程序
- 2、测试现象
- 3、icm20608APP程序及测试
一、IIO 子系统简介
IIO 全称是 Industrial I/O,翻译过来就是工业 I/O,大家不要看到“工业”两个字就觉得 IIO是只用于工业领域的。大家一般在搜索 IIO 子系统的时候,会发现大多数讲的都是 ADC,这是因为 IIO 就是为 ADC 类传感器准备的,当然了 DAC 也是可以的。大家常用的陀螺仪、加速度计、电压/电流测量芯片、光照传感器、压力传感器等内部都是有个 ADC,内部 ADC 将原始的模拟数据转换为数字量,然后通过其他的通信接口,比如 IIC、 SPI 等传输给 SOC。
因此,当你使用的传感器本质是 ADC 或 DAC 器件的时候,可以优先考虑使用 IIO 驱动框架。
二、IIO子系统使用的一些相关的结构体、函数等
1、iio_dev 结构体
IIO 子系统使用结构体 iio_dev 来描述一个具体 IIO 设备,此设备结构体定义在include/linux/iio/iio.h 文件中,结构体内容如下(有省略):
struct iio_dev {int modes;struct device dev;struct iio_chan_spec const *channels;int num_channels;const char *name;const struct iio_info *info;/* 注意:这个结构体只列出了该实验驱动使用到的结构体属性,其他省略了,有兴趣的大家可以查看内核源码,有注释的 */
};
①modes:是选择iio驱动设备支持的工作模式,模式分别有如下:
至于sysfs接口可以简单理解为生成了带有用于用户访问设备文件,也就是说实现sysfs接口,可通过脚本命令“echo”、“cat”访问驱动。详细介绍可以点击该链接,这位博主写的挺详细的📌
②dev:其是一个设备结构体。
回顾一下以前我们使用的普通的IO字符设备驱动、SPI、IIC等是不是都会使用到device结构体呀,什么分配设备号、添加字符设备、创建设备节点等呀。有了这个之后,我们只需要把要操作的一系列字符设备的那个device给实例到该iio_dev->dev下,那么通过调用 iio_device_register 该接口就可以内部生成在内核生成字符设备文件了。
简单看一下 iio_device_register 函数接口内部是怎么样的,如下:
②channels:为 IIO 设备通道规格结构表。
通俗一点来说,就是我们获取的传感器信息,如加速度传感器、陀螺仪传感器、温度传感器,获取这三个传感器是通过通道去获取的,那我们怎么知道我们获取的通道数据是哪个传感器的数据呢,因此我们需要给每个通道打上对应传感器的标签,这像一个树状查询表一样,可以继续细分,如加速度传感器有X、Y、Z轴三组数据,我们需要对这三组数据进行细分,对应的管道打上对应的标签。
先看看 iio_chan_spec 结构体,如下图:
这里我讲一下比较重要的成员变量:
224行,type 为通道类型, iio_chan_type 是一个枚举类型,列举出了可以选择的通道类型,定义在 include/uapi/linux/iio/types.h 文件里面,内容如下:
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, /* 笔者暂时未知的类型 */IIO_PRESSURE, /* 压力类型 */IIO_HUMIDITYRELATIVE, /* 湿度类型 */IIO_ACTIVITY, /* 活动类型(计步传感器) */IIO_STEPS, /* 步数类型 */IIO_ENERGY, /* 能量类型(卡路里) */IIO_DISTANCE, /* 距离类型 */IIO_VELOCITY, /* 速度类型 */
};
225、247行,当成员变量 indexed 为 1时候, channel 为通道索引。
246行,当成员变量 modified 为 1 的时候, channel2 为通道修饰符。 Linux 内核给出了可用的通道修饰符,定义在 include/uapi/linux/iio/types.h 文件里面,内容如下(有省略)
enum iio_modifier {IIO_NO_MOD,IIO_MOD_X, /* X 轴 */IIO_MOD_Y, /* Y 轴 */IIO_MOD_Z, /* Z 轴 */
......
};
比如 ICM20608 的加速度计部分,类型设置为 IIO_ACCEL, X、 Y、 Z 这三个轴就用 channel2的通道修饰符来区分。
第 228 行,当使用触发缓冲区的时候, scan_index 是扫描索引。
第 229~236, scan_type 是一个结构体,描述了扫描数据在缓冲区中的存储格式。我们依次来看一下 scan_type 各个成员变量的涵义:
scan_type.sign:如果为‘u’表示数据为无符号类型,为‘s’的话为有符号类型。
scan_type.realbits:数据真实的有效位数,比如很多传感器说的 10 位 ADC,其真实有效数据就是 10 位。
scan_type.storagebits:存储位数,有效位数+填充位。比如有些传感器 ADC 是 12 位的,那么我们存储的话肯定要用到 2 个字节,也就是 16 位,这 16 位就是存储位数。
scan_type.shift:右移位数,也就是存储位数和有效位数不一致的时候,需要右移的位数,这个参数不总是需要,一切以实际芯片的数据手册位数。
scan_type.repeat:实际或存储位的重复数量。
scan_type.endianness:数据的大小端模式,可设置为 IIO_CPU、 IIO_BE(大端)或 IIO_LE(小端)。
第 237 行, info_mask_separate 标记某些属性专属于此通道, include/linux/iio/types.h 文件中
的 iio_chan_info_enum 枚举类型描述了可选的属性值,如下所示:
enum iio_chan_info_enum {IIO_CHAN_INFO_RAW = 0,IIO_CHAN_INFO_PROCESSED,IIO_CHAN_INFO_SCALE,IIO_CHAN_INFO_OFFSET,
......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 属性是相互独立的。
第 238 行, 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 下就会只生成一个描述分辨率的文件,这三个通道都可以使用这一个分辨率文件。
三、IIO驱动框架搭建
1、SPI基础框架搭建
/* * 根据linux内核的程序查找所使用函数的对应头文件。 */
#include <linux/types.h>
#include <linux/module.h> //MODULE_LICENSE,MODULE_AUTHOR
#include <linux/init.h> //module_init,module_exit
#include <linux/kernel.h> //printk
#include <linux/fs.h> //struct file_operations
#include <linux/slab.h> //kmalloc, kfree
#include <linux/uaccess.h> //copy_to_user,copy_from_user
#include <linux/io.h> //ioremap,iounmap
#include <linux/cdev.h> //struct cdev,cdev_init,cdev_add,cdev_del
#include <linux/device.h> //class
#include <linux/of.h> //of_find_node_by_path
#include <linux/of_gpio.h> //of_get_named_gpio
#include <linux/gpio.h> //gpio_request,gpio_direction_output,gpio_set_number
#include <linux/atomic.h> //atomic_t
#include <linux/of_irq.h> //irq_of_parse_and_map
#include <linux/interrupt.h> //request_irq
#include <linux/timer.h> //timer_list
#include <linux/jiffies.h> //jiffies
#include <linux/atomic.h> //atomic_set
#include <linux/input.h> //input
#include <linux/platform_device.h> //platform
#include <linux/delay.h> //mdelay
#include <linux/i2c.h>
#include <linux/spi/spi.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/iio/trigger_consumer.h>
#include <linux/iio/trigger.h>
#include <linux/iio/buffer.h>
#include <linux/iio/triggered_buffer.h>
#include <linux/regmap.h>
#include "icm20608.h"/* 1.7 probe函数 */
static int icm20608_probe(struct spi_device *spi) {int ret = 0;return ret;
}/* 1.8 remove函数 */
static int icm20608_remove(struct spi_device *spi) {int ret = 0;return ret;
}/* 1.5 传统的匹配表 */
static const struct spi_device_id icm20608_id[] = {{"alientek,icm20608", 0},{}
};/* 1.6 设备树匹配表 */
static const struct of_device_id icm20608_of_match[] = {{ .compatible = "alientek,icm20608" },{}
};/* 1.4 spi_driver结构体 */
static struct spi_driver icm20608_driver = {.probe = icm20608_probe,.remove = icm20608_remove,.driver = {.owner = THIS_MODULE,.name = "icm20608",.of_match_table = icm20608_of_match,},.id_table = icm20608_id,
};// module_spi_driver(icm20608_driver);
/* 1.1 驱动模块入口函数 */
static int __init icm20608_init(void) { return spi_register_driver(&icm20608_driver); //注册spi驱动设备
} /* 1.2 驱动模块出口函数 */
static void __exit icm20608_exit(void) { spi_unregister_driver(&icm20608_driver); //注销spi驱动设备
} /* 1.3 驱动许可和个人信息 */
module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("djw");
2、基于SPI基础驱动框架上搭建IIO基础驱动框架
/** 根据linux内核的程序查找所使用函数的对应头文件。*/
/* 省略... *//* 定义设备名称 */
#define DEVICE_NAME "ICM20608"/* 2.0 设备结构体 */
struct icm20608_dev
{struct spi_device *spi;struct regmap *regmap;struct regmap_config regmap_config;struct mutex mutex_lock; // 互斥锁,保证一次只有一个应用读取该数据
};/* 2.3.1 icm20608 通道, 1 路温度通道, 3 路陀螺仪, 3 路加速度计 */
static const struct iio_chan_spec icm20608_channels[] = {/* 只有配置好每个通道的项,那么才会像一个树状分支那样,每个数字对应每个通道或通道细分的分支 */
};/* 2.3.2.1 */
/* 读函数,当读取 sysfs 中的文件的时候最终此函数会执行,此函数里面会从传感器里面读取各种数据,然后上传给应用。* indio_dev : iio_dev, chan : 通道, val : 读取的值的整数部分,val2 : 读取的值的小数部分, mask : 掩码。*/
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;printk("icm20608_read_raw\r\n");return ret;
}/* 2.3.2.2 */
static int icm20608_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int val, int val2, long mask)
{int ret = 0;printk("icm20608_write_raw\r\n");return ret;
}/* 2.3.2.3 * 用户空间写数据格式,比如我们在用户空间操作 sysfs 来设置传感器的分辨率,如果分辨率带小数,那么这个小数传递到内核空间应该扩大多少倍,此函数就是用来设置这个的。* indio_dev : iio_dev* chan : 通道* mask : 掩码* return : 0,成功;其他值,错误*/
static int icm20608_write_raw_get_fmt(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, long mask)
{int ret = 0;printk("icm20608_write_raw_get_fmt\r\n");return ret;
}/* 2.3.2 */
/* iio_info,当应用程序读取相应的驱动文件的时候, xxx_read_raw* 函数就会执行,我们在此函数中会读取传感器数据,然后返回给应用层。当应用层向相应的驱* 动写数据的时候, xxx_write_raw 函数就会执行。因此 xxx_read_raw 和 xxx_write_raw 这两个函* 数是非常重要的!需要我们根据具体的传感器来编写,这两个函数是编写 IIO 驱动的核心。* */
static const struct iio_info icm20608_info = {.read_raw = &icm20608_read_raw,.write_raw = &icm20608_write_raw,.write_raw_get_fmt = &icm20608_write_raw_get_fmt,.driver_module = THIS_MODULE,
};/* 1.7 probe函数 */
static int icm20608_probe(struct spi_device *spi)
{int ret = 0;struct icm20608_dev *dev;struct iio_dev *indio_dev;printk("icm20608_probe successful!\r\n");/* 2.1 申请icm20608_dev结构体大小的内存, 为私人结构体“icm20608_dev”分配内存空间 */indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*dev));if (!indio_dev){ret = -ENOMEM;goto fail_iio_dev;}/* 2.2 获取定义的设备结构体首地址和获取spi_device结构体等数据私有化操作 */dev = iio_priv(indio_dev); // 使用 iio_priv 函数从 iio_dev 中提取出私有数据,也就是 icm2608_dev 这个自定义结构体变量首地址dev->spi = spi; // 设备树匹配成功后,系统会分配struct spi_device *spi,因此将其spi赋值给我们定义的设备结构体上spi_set_drvdata(spi, indio_dev); // 将indio_dev设置为spi->driver_data私有数据mutex_init(&dev->mutex_lock); // 初始化互斥锁/* 2.3 初始化iio_dev */indio_dev->dev.parent = &spi->dev; // 获取spi->dev结构体indio_dev->channels = icm20608_channels; // 通道indio_dev->num_channels = ARRAY_SIZE(icm20608_channels); // 通道大小indio_dev->name = DEVICE_NAME; // 名称indio_dev->modes = INDIO_DIRECT_MODE; // 直接模式,提供sysfs接口indio_dev->info = &icm20608_info;/* 2.4 将iio_dev注册到内核 */ret = iio_device_register(indio_dev);if (ret < 0){dev_err(&spi->dev, "unable to register iio device\r\n");goto fail_iio_register;}/* 2.5 设置SPI的模式 */spi->mode = SPI_MODE_0; // MODE0,CPOL=0, CPHA=0spi_setup(spi); //设置好 spi_device 以后需要使用 spi_setup 配置一下return 0;fail_iio_register:
fail_iio_dev:return ret;
}/* 1.8 remove函数 */
static int icm20608_remove(struct spi_device *spi)
{int ret = 0;/* 获取私有数据 */struct iio_dev *indio_dev = spi_get_drvdata(spi);struct icm20608_dev *dev;dev = iio_priv(indio_dev);printk("icm20608_remove finish\r\n");/* 注销iio_dev */iio_device_unregister(indio_dev);return ret;
}/* 1.5 传统的匹配表 */
static const struct spi_device_id icm20608_id[] = {{"alientek,icm20608", 0},{}};/* 1.6 设备树匹配表 */
static const struct of_device_id icm20608_of_match[] = {{.compatible = "alientek,icm20608"},{}};/* 1.4 spi_driver结构体 */
static struct spi_driver icm20608_driver = {.probe = icm20608_probe,.remove = icm20608_remove,.driver = {.owner = THIS_MODULE,.name = "icm20608",.of_match_table = icm20608_of_match,},.id_table = icm20608_id,
};// module_spi_driver(icm20608_driver);
/* 1.2 驱动模块入口函数 */
static int __init icm20608_init(void)
{return spi_register_driver(&icm20608_driver); // 注册spi驱动设备
}/* 1.3 驱动模块出口函数 */
static void __exit icm20608_exit(void)
{spi_unregister_driver(&icm20608_driver); // 注销spi驱动设备
}/* 1.1 驱动许可和个人信息 */
module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("djw");
①测试IIO基础驱动框架程序
Ⅰ、使能Linux内核IIO子系统功能,操作如下:
Ⅱ、加载驱动程序,查看现象,如下:
上面现象,可以看出加载驱动后,已经生成了我们所期望的设备文件“iio:device0”。
3、配置IIO设备通道规格结构表
①了解通道文件命名方式
这里我以已经配置好IIO设备通道规格结构表参数后加载驱动后查看的现象中的“in_accel_x_raw”这个生成的通道文件为例:
配置加速度传感器x轴原始值程序段,如下图所示:
通 道 属 性 的 命 名 模 式 为 :[direction][type][index][modifier][info_mask],我们依次来看一下这些命名组织模块:
①编写IIO驱动程序的配置IIO设备通道规格结构表
驱动程序:
/** 根据linux内核的程序查找所使用函数的对应头文件。*/
/* 省略... *//* 定义设备名称 */
#define DEVICE_NAME "ICM20608"/* 2.0 设备结构体 */
struct icm20608_dev
{struct spi_device *spi;struct regmap *regmap;struct regmap_config regmap_config;struct mutex mutex_lock; // 互斥锁,保证一次只有一个应用读取该数据
};/* 2.3.1.1 ICM20608的扫描元素,3轴加速计、3轴陀螺仪、1路温度传感器、一路时间戳 */
enum inv_icm20608_scan
{INV_ICM20608_SCAN_ACCL_X,INV_ICM20608_SCAN_ACCL_Y,INV_ICM20608_SCAN_ACCL_Z,INV_ICM20608_SCAN_TEMP,INV_ICM20608_SCAN_GYRO_X,INV_ICM20608_SCAN_GYRO_Y,INV_ICM20608_SCAN_GYRO_Z,INV_ICM20608_SCAN_TIMESTAMP,
};/* 2.3.1.2 */
/* _type:通道类型(加速度和陀螺仪), _channel2:当modified为1时,channel2为通道修饰符 */
/* info_mask_shared_by_type:通道共享; IIO_CHAN_INFO_SCALE 这个属性,表示这三个通道的分辨率是共用的,这样在 sysfs 下就会只生成一个描述分辨率的文件,这三个通道都可以使用这一个分辨率文件。*/
/* info_mask_separate:通道独立; IIO_CHAN_INFO_RAW为原始值,IIO_CHAN_INFO_CALIBBIAS校准值 */
#define ICM20608_CHANNEL(_type, _channel2, _index) \{ \.type = _type, \.modified = 1, \.channel2 = _channel2, \.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \BIT(IIO_CHAN_INFO_CALIBBIAS), \.scan_index = _index, \.scan_type = { \.sign = 's', \.realbits = 16, \.storagebits = 16, \.shift = 0, \.endianness = IIO_BE, \}, \}/* 2.3.1 icm20608 通道, 1 路温度通道, 3 路陀螺仪, 3 路加速度计 */
static const struct iio_chan_spec icm20608_channels[] = {/* 使用最元素的配置温度 */{.type = IIO_TEMP,.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE) | BIT(IIO_CHAN_INFO_OFFSET),.scan_index = INV_ICM20608_SCAN_TEMP,.scan_type = {.sign = 's',.realbits = 16,.storagebits = 16,.shift = 0,.endianness = IIO_BE,},},/* 2.3.1.3 加速度X、Y、Z三个通道 */ICM20608_CHANNEL(IIO_ACCEL, IIO_MOD_X, INV_ICM20608_SCAN_ACCL_X), // X轴ICM20608_CHANNEL(IIO_ACCEL, IIO_MOD_Y, INV_ICM20608_SCAN_ACCL_Y), // Y轴ICM20608_CHANNEL(IIO_ACCEL, IIO_MOD_Z, INV_ICM20608_SCAN_ACCL_Z), // Z轴/* 2.3.1.4 陀螺仪X、Y、Z三个通道 */ICM20608_CHANNEL(IIO_ANGL_VEL, IIO_MOD_X, INV_ICM20608_SCAN_GYRO_X), // X轴ICM20608_CHANNEL(IIO_ANGL_VEL, IIO_MOD_Y, INV_ICM20608_SCAN_GYRO_Y), // Y轴ICM20608_CHANNEL(IIO_ANGL_VEL, IIO_MOD_Z, INV_ICM20608_SCAN_GYRO_Z), // Z轴
};/* 2.3.2.1 */
/* 读函数,当读取 sysfs 中的文件的时候最终此函数会执行,此函数里面会从传感器里面读取各种数据,然后上传给应用。* indio_dev : iio_dev, chan : 通道, val : 读取的值的整数部分,val2 : 读取的值的小数部分, mask : 掩码。*/
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;printk("icm20608_read_raw\r\n");return ret;
}/* 2.3.2.2 */
static int icm20608_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int val, int val2, long mask)
{int ret = 0;printk("icm20608_write_raw\r\n");return ret;
}/* 2.3.2.3 * 用户空间写数据格式,比如我们在用户空间操作 sysfs 来设置传感器的分辨率,如果分辨率带小数,那么这个小数传递到内核空间应该扩大多少倍,此函数就是用来设置这个的。* indio_dev : iio_dev* chan : 通道* mask : 掩码* return : 0,成功;其他值,错误*/
static int icm20608_write_raw_get_fmt(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, long mask)
{int ret = 0;printk("icm20608_write_raw_get_fmt\r\n");return ret;
}/* 2.3.2 */
/* iio_info,当应用程序读取相应的驱动文件的时候, xxx_read_raw* 函数就会执行,我们在此函数中会读取传感器数据,然后返回给应用层。当应用层向相应的驱* 动写数据的时候, xxx_write_raw 函数就会执行。因此 xxx_read_raw 和 xxx_write_raw 这两个函* 数是非常重要的!需要我们根据具体的传感器来编写,这两个函数是编写 IIO 驱动的核心。* */
static const struct iio_info icm20608_info = {.read_raw = &icm20608_read_raw,.write_raw = &icm20608_write_raw,.write_raw_get_fmt = &icm20608_write_raw_get_fmt,.driver_module = THIS_MODULE,
};/* 1.7 probe函数 */
static int icm20608_probe(struct spi_device *spi)
{int ret = 0;struct icm20608_dev *dev;struct iio_dev *indio_dev;printk("icm20608_probe successful!\r\n");/* 2.1 申请icm20608_dev结构体大小的内存, 为私人结构体“icm20608_dev”分配内存空间 */indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*dev));if (!indio_dev){ret = -ENOMEM;goto fail_iio_dev;}/* 2.2 获取定义的设备结构体首地址和获取spi_device结构体等数据私有化操作 */dev = iio_priv(indio_dev); // 使用 iio_priv 函数从 iio_dev 中提取出私有数据,也就是 icm2608_dev 这个自定义结构体变量首地址dev->spi = spi; // 设备树匹配成功后,系统会分配struct spi_device *spi,因此将其spi赋值给我们定义的设备结构体上spi_set_drvdata(spi, indio_dev); // 将indio_dev设置为spi->driver_data私有数据mutex_init(&dev->mutex_lock); // 初始化互斥锁/* 2.3 初始化iio_dev */indio_dev->dev.parent = &spi->dev; // 获取spi->dev结构体indio_dev->channels = icm20608_channels; // 通道indio_dev->num_channels = ARRAY_SIZE(icm20608_channels); // 通道大小indio_dev->name = DEVICE_NAME; // 名称indio_dev->modes = INDIO_DIRECT_MODE; // 直接模式,提供sysfs接口indio_dev->info = &icm20608_info;/* 2.4 将iio_dev注册到内核 */ret = iio_device_register(indio_dev);if (ret < 0){dev_err(&spi->dev, "unable to register iio device\r\n");goto fail_iio_register;}/* 2.5 设置SPI的模式 */spi->mode = SPI_MODE_0; // MODE0,CPOL=0, CPHA=0spi_setup(spi); //设置好 spi_device 以后需要使用 spi_setup 配置一下return 0;fail_iio_register:
fail_iio_dev:return ret;
}/* 1.8 remove函数 */
static int icm20608_remove(struct spi_device *spi)
{int ret = 0;/* 获取私有数据 */struct iio_dev *indio_dev = spi_get_drvdata(spi);struct icm20608_dev *dev;dev = iio_priv(indio_dev);printk("icm20608_remove finish\r\n");/* 注销iio_dev */iio_device_unregister(indio_dev);return ret;
}/* 1.5 传统的匹配表 */
static const struct spi_device_id icm20608_id[] = {{"alientek,icm20608", 0},{}};/* 1.6 设备树匹配表 */
static const struct of_device_id icm20608_of_match[] = {{.compatible = "alientek,icm20608"},{}};/* 1.4 spi_driver结构体 */
static struct spi_driver icm20608_driver = {.probe = icm20608_probe,.remove = icm20608_remove,.driver = {.owner = THIS_MODULE,.name = "icm20608",.of_match_table = icm20608_of_match,},.id_table = icm20608_id,
};// module_spi_driver(icm20608_driver);
/* 1.2 驱动模块入口函数 */
static int __init icm20608_init(void)
{return spi_register_driver(&icm20608_driver); // 注册spi驱动设备
}/* 1.3 驱动模块出口函数 */
static void __exit icm20608_exit(void)
{spi_unregister_driver(&icm20608_driver); // 注销spi驱动设备
}/* 1.1 驱动许可和个人信息 */
module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("djw");
现象:
4、添加Regmap API
程序如下:
/** 根据linux内核的程序查找所使用函数的对应头文件。*/
/* 省略... *//* 定义设备名称 */
#define DEVICE_NAME "ICM20608"/* 2.0 设备结构体 */
struct icm20608_dev
{struct spi_device *spi;struct regmap *regmap;struct regmap_config regmap_config;struct mutex mutex_lock; // 互斥锁,保证一次只有一个应用读取该数据
};/* 2.3.1.1 ICM20608的扫描元素,3轴加速计、3轴陀螺仪、1路温度传感器、一路时间戳 */
enum inv_icm20608_scan
{INV_ICM20608_SCAN_ACCL_X,INV_ICM20608_SCAN_ACCL_Y,INV_ICM20608_SCAN_ACCL_Z,INV_ICM20608_SCAN_TEMP,INV_ICM20608_SCAN_GYRO_X,INV_ICM20608_SCAN_GYRO_Y,INV_ICM20608_SCAN_GYRO_Z,INV_ICM20608_SCAN_TIMESTAMP,
};/* 2.3.1.2 */
/* _type:通道类型(加速度和陀螺仪), _channel2:当modified为1时,channel2为通道修饰符 */
/* info_mask_shared_by_type:通道共享; IIO_CHAN_INFO_SCALE 这个属性,表示这三个通道的分辨率是共用的,这样在 sysfs 下就会只生成一个描述分辨率的文件,这三个通道都可以使用这一个分辨率文件。*/
/* info_mask_separate:通道独立; IIO_CHAN_INFO_RAW为原始值,IIO_CHAN_INFO_CALIBBIAS校准值 */
#define ICM20608_CHANNEL(_type, _channel2, _index) \{ \.type = _type, \.modified = 1, \.channel2 = _channel2, \.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \BIT(IIO_CHAN_INFO_CALIBBIAS), \.scan_index = _index, \.scan_type = { \.sign = 's', \.realbits = 16, \.storagebits = 16, \.shift = 0, \.endianness = IIO_BE, \}, \}/* 2.3.1 icm20608 通道, 1 路温度通道, 3 路陀螺仪, 3 路加速度计 */
static const struct iio_chan_spec icm20608_channels[] = {/* 使用最元素的配置温度 */{.type = IIO_TEMP,.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE) | BIT(IIO_CHAN_INFO_OFFSET),.scan_index = INV_ICM20608_SCAN_TEMP,.scan_type = {.sign = 's',.realbits = 16,.storagebits = 16,.shift = 0,.endianness = IIO_BE,},},/* 2.3.1.3 加速度X、Y、Z三个通道 */ICM20608_CHANNEL(IIO_ACCEL, IIO_MOD_X, INV_ICM20608_SCAN_ACCL_X), // X轴ICM20608_CHANNEL(IIO_ACCEL, IIO_MOD_Y, INV_ICM20608_SCAN_ACCL_Y), // Y轴ICM20608_CHANNEL(IIO_ACCEL, IIO_MOD_Z, INV_ICM20608_SCAN_ACCL_Z), // Z轴/* 2.3.1.4 陀螺仪X、Y、Z三个通道 */ICM20608_CHANNEL(IIO_ANGL_VEL, IIO_MOD_X, INV_ICM20608_SCAN_GYRO_X), // X轴ICM20608_CHANNEL(IIO_ANGL_VEL, IIO_MOD_Y, INV_ICM20608_SCAN_GYRO_Y), // Y轴ICM20608_CHANNEL(IIO_ANGL_VEL, IIO_MOD_Z, INV_ICM20608_SCAN_GYRO_Z), // Z轴
};/* 3.2 icm20608读取单个寄存器 */
static u8 icm20608_read_onereg(struct icm20608_dev *dev, u8 reg) {u32 data = 0;u8 ret = 0;ret = regmap_read(dev->regmap, reg, &data);return (u8)data;
}/* 3.3 icm20608写一个寄存器 */
static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value) {u8 ret = 0;ret = regmap_write(dev->regmap, reg, value);
}/* 3.4 icm20608里面的寄存器初始化 */
void icm20608_reg_init(struct icm20608_dev *dev) {u8 value = 0;icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0X80); //复位,复位后为0X40,睡眠模式 mdelay(50);icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0X01); //关闭睡眠,自动选择时钟mdelay(50);value = icm20608_read_onereg(dev, ICM20_WHO_AM_I);printk("ICM20608 ID = 0X%X\r\n",value);value = icm20608_read_onereg(dev, ICM20_PWR_MGMT_1);printk("ICM20_PWR_MGMT_1 = 0X%X\r\n",value);/* 以下是关于6轴传感器的初始化 */icm20608_write_onereg(dev, ICM20_SMPLRT_DIV, 0x00); icm20608_write_onereg(dev, ICM20_GYRO_CONFIG, 0x18); icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG, 0x18); icm20608_write_onereg(dev, ICM20_CONFIG, 0x04); icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG2, 0x04);icm20608_write_onereg(dev, ICM20_PWR_MGMT_2, 0x00); icm20608_write_onereg(dev, ICM20_LP_MODE_CFG, 0x00); icm20608_write_onereg(dev, ICM20_FIFO_EN, 0x00);
}/* 2.3.2.1 */
/* 读函数,当读取 sysfs 中的文件的时候最终此函数会执行,此函数里面会从传感器里面读取各种数据,然后上传给应用。* indio_dev : iio_dev, chan : 通道, val : 读取的值的整数部分,val2 : 读取的值的小数部分, mask : 掩码。*/
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;printk("icm20608_read_raw\r\n");return ret;
}/* 2.3.2.2 */
static int icm20608_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int val, int val2, long mask)
{int ret = 0;printk("icm20608_write_raw\r\n");return ret;
}/* 2.3.2.3 * 用户空间写数据格式,比如我们在用户空间操作 sysfs 来设置传感器的分辨率,如果分辨率带小数,那么这个小数传递到内核空间应该扩大多少倍,此函数就是用来设置这个的。* indio_dev : iio_dev* chan : 通道* mask : 掩码* return : 0,成功;其他值,错误*/
static int icm20608_write_raw_get_fmt(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, long mask)
{int ret = 0;printk("icm20608_write_raw_get_fmt\r\n");return ret;
}/* 2.3.2 */
/* iio_info,当应用程序读取相应的驱动文件的时候, xxx_read_raw* 函数就会执行,我们在此函数中会读取传感器数据,然后返回给应用层。当应用层向相应的驱* 动写数据的时候, xxx_write_raw 函数就会执行。因此 xxx_read_raw 和 xxx_write_raw 这两个函* 数是非常重要的!需要我们根据具体的传感器来编写,这两个函数是编写 IIO 驱动的核心。* */
static const struct iio_info icm20608_info = {.read_raw = &icm20608_read_raw,.write_raw = &icm20608_write_raw,.write_raw_get_fmt = &icm20608_write_raw_get_fmt,.driver_module = THIS_MODULE,
};/* 1.7 probe函数 */
static int icm20608_probe(struct spi_device *spi)
{int ret = 0;struct icm20608_dev *dev;struct iio_dev *indio_dev;printk("icm20608_probe successful!\r\n");/* 2.1 申请icm20608_dev结构体大小的内存, 为私人结构体“icm20608_dev”分配内存空间 */indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*dev));if (!indio_dev){ret = -ENOMEM;goto fail_iio_dev;}/* 2.2 获取定义的设备结构体首地址和获取spi_device结构体等数据私有化操作 */dev = iio_priv(indio_dev); // 使用 iio_priv 函数从 iio_dev 中提取出私有数据,也就是 icm2608_dev 这个自定义结构体变量首地址dev->spi = spi; // 设备树匹配成功后,系统会分配struct spi_device *spi,因此将其spi赋值给我们定义的设备结构体上spi_set_drvdata(spi, indio_dev); // 将indio_dev设置为spi->driver_data私有数据mutex_init(&dev->mutex_lock); // 初始化互斥锁/* 2.3 初始化iio_dev */indio_dev->dev.parent = &spi->dev; // 获取spi->dev结构体indio_dev->channels = icm20608_channels; // 通道indio_dev->num_channels = ARRAY_SIZE(icm20608_channels); // 通道大小indio_dev->name = DEVICE_NAME; // 名称indio_dev->modes = INDIO_DIRECT_MODE; // 直接模式,提供sysfs接口indio_dev->info = &icm20608_info;/* 2.4 将iio_dev注册到内核 */ret = iio_device_register(indio_dev);if (ret < 0){dev_err(&spi->dev, "unable to register iio device\r\n");goto fail_iio_register;}/* 2.5 设置SPI的模式 */spi->mode = SPI_MODE_0; // MODE0,CPOL=0, CPHA=0spi_setup(spi); //设置好 spi_device 以后需要使用 spi_setup 配置一下/* 3.1 初始化 regmap_config 设置 和 regmap *//* 补充:当设置SPI读ICM20608操作时,使用 regmap 的时候就不需要手动将寄存器地址的 bit7 置 1,在初始化 regmap_config* 的时候直接将 read_flag_mask 设置为 0X80 即可,这样通过 regmap 读取 SPI 内部寄存器的时候* 就会将寄存器地址与 read_flag_mask 进行或运算,结果就是将 bit7 置 1,但是整个过程不需要* 我们来操作,全部由 regmap 框架来完成的 */dev->regmap_config.reg_bits = 8; /* 寄存器长度8bit */dev->regmap_config.val_bits = 8; /* 值长度8bit */dev->regmap_config.read_flag_mask = 0x80; /* 读掩码 */dev->regmap = regmap_init_spi(spi,&dev->regmap_config);if(IS_ERR(dev->regmap)) {ret = PTR_ERR(dev->regmap);goto fail_regmap_init;}/* 3.5 调用icm20608初始化函数 */icm20608_reg_init(dev);return 0;fail_regmap_init:iio_device_unregister(indio_dev);
fail_iio_register:
fail_iio_dev:return ret;
}/* 1.8 remove函数 */
static int icm20608_remove(struct spi_device *spi)
{int ret = 0;/* 获取私有数据 */struct iio_dev *indio_dev = spi_get_drvdata(spi);struct icm20608_dev *dev;dev = iio_priv(indio_dev);printk("icm20608_remove finish\r\n");/* 2.6 注销iio_dev */iio_device_unregister(indio_dev);/* 3.6 删除regmap */regmap_exit(dev->regmap);return ret;
}/* 1.5 传统的匹配表 */
static const struct spi_device_id icm20608_id[] = {{"alientek,icm20608", 0},{}};/* 1.6 设备树匹配表 */
static const struct of_device_id icm20608_of_match[] = {{.compatible = "alientek,icm20608"},{}};/* 1.4 spi_driver结构体 */
static struct spi_driver icm20608_driver = {.probe = icm20608_probe,.remove = icm20608_remove,.driver = {.owner = THIS_MODULE,.name = "icm20608",.of_match_table = icm20608_of_match,},.id_table = icm20608_id,
};// module_spi_driver(icm20608_driver);
/* 1.2 驱动模块入口函数 */
static int __init icm20608_init(void)
{return spi_register_driver(&icm20608_driver); // 注册spi驱动设备
}/* 1.3 驱动模块出口函数 */
static void __exit icm20608_exit(void)
{spi_unregister_driver(&icm20608_driver); // 注销spi驱动设备
}/* 1.1 驱动许可和个人信息 */
module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("djw");
现象:
从以上程序可以看出,Regmap它就是Linux内核给我们封装好的寄存器读写接口,我们只需要配置初始化Regmap后就可以直接使用了。
如果要读取连续的多个寄存器值,可以使用:
regmap_bulk_read(struct regmap *map, unsigned int reg, void *val, size_t val_count)
如果要写值进连续的多个寄存器内,可以使用:
regmap_bulk_write(struct regmap *map, unsigned int reg, const void *val, size_t val_count)
5、完善xxx_read_raw函数,真正实现通道文件可读取传感器数据
因为应 用 程 序 所 有 的 读 取 操 作 ,最 终 都 会 汇 总 到 iio_info 的 read 函 数 , 这 里 就是
icm20608_read_raw 函数。
对此我以读取加速度传感器X轴的原始值为例,也就是通道规则结构树下的:
IIO_CHAN_INFO_RAW->
IIO_ACCEL->
驱动程序如下:
/** 根据linux内核的程序查找所使用函数的对应头文件。*/
/* 省略... *//* 定义设备名称 */
#define DEVICE_NAME "ICM20608"/* 2.0 设备结构体 */
struct icm20608_dev
{struct spi_device *spi;struct regmap *regmap;struct regmap_config regmap_config;struct mutex mutex_lock; // 互斥锁,保证一次只有一个应用读取该数据
};/* 2.3.1.1 ICM20608的扫描元素,3轴加速计、3轴陀螺仪、1路温度传感器、一路时间戳 */
enum inv_icm20608_scan
{INV_ICM20608_SCAN_ACCL_X,INV_ICM20608_SCAN_ACCL_Y,INV_ICM20608_SCAN_ACCL_Z,INV_ICM20608_SCAN_TEMP,INV_ICM20608_SCAN_GYRO_X,INV_ICM20608_SCAN_GYRO_Y,INV_ICM20608_SCAN_GYRO_Z,INV_ICM20608_SCAN_TIMESTAMP,
};/* 2.3.1.2 */
/* _type:通道类型(加速度和陀螺仪), _channel2:当modified为1时,channel2为通道修饰符 */
/* info_mask_shared_by_type:通道共享; IIO_CHAN_INFO_SCALE 这个属性,表示这三个通道的分辨率是共用的,这样在 sysfs 下就会只生成一个描述分辨率的文件,这三个通道都可以使用这一个分辨率文件。*/
/* info_mask_separate:通道独立; IIO_CHAN_INFO_RAW为原始值,IIO_CHAN_INFO_CALIBBIAS校准值 */
#define ICM20608_CHANNEL(_type, _channel2, _index) \{ \.type = _type, \.modified = 1, \.channel2 = _channel2, \.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \BIT(IIO_CHAN_INFO_CALIBBIAS), \.scan_index = _index, \.scan_type = { \.sign = 's', \.realbits = 16, \.storagebits = 16, \.shift = 0, \.endianness = IIO_BE, \}, \}/* 2.3.1 icm20608 通道, 1 路温度通道, 3 路陀螺仪, 3 路加速度计 */
static const struct iio_chan_spec icm20608_channels[] = {/* 使用最元素的配置温度 */{.type = IIO_TEMP,.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE) | BIT(IIO_CHAN_INFO_OFFSET),.scan_index = INV_ICM20608_SCAN_TEMP,.scan_type = {.sign = 's',.realbits = 16,.storagebits = 16,.shift = 0,.endianness = IIO_BE,},},/* 2.3.1.3 加速度X、Y、Z三个通道 */ICM20608_CHANNEL(IIO_ACCEL, IIO_MOD_X, INV_ICM20608_SCAN_ACCL_X), // X轴ICM20608_CHANNEL(IIO_ACCEL, IIO_MOD_Y, INV_ICM20608_SCAN_ACCL_Y), // Y轴ICM20608_CHANNEL(IIO_ACCEL, IIO_MOD_Z, INV_ICM20608_SCAN_ACCL_Z), // Z轴/* 2.3.1.4 陀螺仪X、Y、Z三个通道 */ICM20608_CHANNEL(IIO_ANGL_VEL, IIO_MOD_X, INV_ICM20608_SCAN_GYRO_X), // X轴ICM20608_CHANNEL(IIO_ANGL_VEL, IIO_MOD_Y, INV_ICM20608_SCAN_GYRO_Y), // Y轴ICM20608_CHANNEL(IIO_ANGL_VEL, IIO_MOD_Z, INV_ICM20608_SCAN_GYRO_Z), // Z轴
};/* 3.2 icm20608读取单个寄存器 */
static u8 icm20608_read_onereg(struct icm20608_dev *dev, u8 reg) {u32 data = 0;u8 ret = 0;ret = regmap_read(dev->regmap, reg, &data);return (u8)data;
}/* 3.3 icm20608写一个寄存器 */
static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value) {u8 ret = 0;ret = regmap_write(dev->regmap, reg, value);
}/* 3.4 icm20608里面的寄存器初始化 */
void icm20608_reg_init(struct icm20608_dev *dev) {u8 value = 0;icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0X80); //复位,复位后为0X40,睡眠模式 mdelay(50);icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0X01); //关闭睡眠,自动选择时钟mdelay(50);value = icm20608_read_onereg(dev, ICM20_WHO_AM_I);printk("ICM20608 ID = 0X%X\r\n",value);value = icm20608_read_onereg(dev, ICM20_PWR_MGMT_1);printk("ICM20_PWR_MGMT_1 = 0X%X\r\n",value);/* 以下是关于6轴传感器的初始化 */icm20608_write_onereg(dev, ICM20_SMPLRT_DIV, 0x00); icm20608_write_onereg(dev, ICM20_GYRO_CONFIG, 0x18); icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG, 0x18); icm20608_write_onereg(dev, ICM20_CONFIG, 0x04); icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG2, 0x04);icm20608_write_onereg(dev, ICM20_PWR_MGMT_2, 0x00); icm20608_write_onereg(dev, ICM20_LP_MODE_CFG, 0x00); icm20608_write_onereg(dev, ICM20_FIFO_EN, 0x00);
}/* 4.1.1.1* 读取ICM20608传感器数据,可以用于陀螺仪、加速度计、温度的读取,读取2个寄存器值* dev: icm20608设备, reg: 要读取的通道寄存器首地址, anix: 需要读取的通道,比如X,Y,Z, val: 保存读取到的值。*/
static int icm20608_sensor_show(struct icm20608_dev *dev, int reg, int axis, int *val)
{int ind, result;__be16 d; //定义大端规则的16位整形变量ind = (axis - IIO_MOD_X) * 2; //计算读取的陀螺仪和加速度传感器的X、Y、Z轴的偏移寄存器地址result = regmap_bulk_read(dev->regmap, reg + ind, (u8 *)&d, 2);if (result)return -EINVAL;*val = (short)be16_to_cpup(&d); //将16位整形数据以大端规则转换存储return IIO_VAL_INT; //表示读取的数据类型是整数值,没有小数
}/* 4.1.1* 读取 ICM20608 陀螺仪、加速度计、温度通道值-----读取原始数据使用的* indio_dev : iio 设备, chan : 通道, val : 保存读取到的通道值。*/
static int icm20608_read_channel_data(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int *val)
{int ret = 0;struct icm20608_dev *dev = iio_priv(indio_dev);/* 区分通道类型,是温度传感器、陀螺仪传感器、加速度传感器 */switch (chan->type) {case IIO_ACCEL: //加速度printk("read channel type is IIO_ACCEL\r\n");ret = icm20608_sensor_show(dev, ICM20_ACCEL_XOUT_H, chan->channel2, val); /* channel2为X、Y、Z轴 */return ret;case IIO_ANGL_VEL: //陀螺仪printk("read channel type is IIO_ANGL_VEL\r\n");ret = icm20608_sensor_show(dev, ICM20_GYRO_XOUT_H, chan->channel2, val); /* channel2为X、Y、Z轴 */return ret;case IIO_TEMP: //温度printk("read channel type is IIO_TEMP\r\n");ret = icm20608_sensor_show(dev, ICM20_TEMP_OUT_H, IIO_MOD_X, val);return ret; default:return -EINVAL; //没有一项符合,那么返回参数不符合}return ret;
}/* 2.3.2.1 */
/* 读函数,当读取 sysfs 中的文件的时候最终此函数会执行,此函数里面会从传感器里面读取各种数据,然后上传给应用。* indio_dev : iio_dev, chan : 通道, val : 读取的值的整数部分,val2 : 读取的值的小数部分, mask : 掩码。*/
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;/********** 4.1 根据设置的通道文件参数作为条件依据读取数据类型 **********/struct icm20608_dev *dev = iio_priv(indio_dev);// printk("icm20608_read_raw\r\n");/* 区分读取的数据类型,如raw、scale、offset等掩码 */switch (mask){case IIO_CHAN_INFO_RAW: //原始数据类型printk("read type is IIO_CHAN_INFO_RAW. \r\n");mutex_lock(&dev->mutex_lock); //上锁,保证读取的数据正确性ret = icm20608_read_channel_data(indio_dev, chan, val); //再进行细分筛选,判断读取的是原始数据类型下的哪个通道的数据(温度、加速度、陀螺仪),而这里val参数不用做处理,因为元素数据是ADC数据,是整形的mutex_unlock(&dev->mutex_lock); //释放锁return ret;case IIO_CHAN_INFO_SCALE: //分辨率数据类型printk("read type is IIO_CHAN_INFO_SCALE. \r\n");return ret;case IIO_CHAN_INFO_OFFSET: //补偿、偏置数据类型printk("read type is IIO_CHAN_INFO_OFFSET. \r\n");return ret;case IIO_CHAN_INFO_CALIBBIAS: //校准数据类型printk("read type is IIO_CHAN_INFO_CALIBBIAS. \r\n");return ret;default:return -EINVAL; //没有一项符合,那么返回参数不符合}/********** 4.1 根据设置的通道文件参数作为条件依据读取数据类型 **********/return ret;
}/* 2.3.2.2 */
static int icm20608_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int val, int val2, long mask)
{int ret = 0;printk("icm20608_write_raw\r\n");return ret;
}/* 2.3.2.3 * 用户空间写数据格式,比如我们在用户空间操作 sysfs 来设置传感器的分辨率,如果分辨率带小数,那么这个小数传递到内核空间应该扩大多少倍,此函数就是用来设置这个的。* indio_dev : iio_dev* chan : 通道* mask : 掩码* return : 0,成功;其他值,错误*/
static int icm20608_write_raw_get_fmt(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, long mask)
{int ret = 0;printk("icm20608_write_raw_get_fmt\r\n");return ret;
}/* 2.3.2 */
/* iio_info,当应用程序读取相应的驱动文件的时候, xxx_read_raw* 函数就会执行,我们在此函数中会读取传感器数据,然后返回给应用层。当应用层向相应的驱* 动写数据的时候, xxx_write_raw 函数就会执行。因此 xxx_read_raw 和 xxx_write_raw 这两个函* 数是非常重要的!需要我们根据具体的传感器来编写,这两个函数是编写 IIO 驱动的核心。* */
static const struct iio_info icm20608_info = {.read_raw = &icm20608_read_raw,.write_raw = &icm20608_write_raw,.write_raw_get_fmt = &icm20608_write_raw_get_fmt,.driver_module = THIS_MODULE,
};/* 1.7 probe函数 */
static int icm20608_probe(struct spi_device *spi)
{int ret = 0;struct icm20608_dev *dev;struct iio_dev *indio_dev;printk("icm20608_probe successful!\r\n");/* 2.1 申请icm20608_dev结构体大小的内存, 为私人结构体“icm20608_dev”分配内存空间 */indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*dev));if (!indio_dev){ret = -ENOMEM;goto fail_iio_dev;}/* 2.2 获取定义的设备结构体首地址和获取spi_device结构体等数据私有化操作 */dev = iio_priv(indio_dev); // 使用 iio_priv 函数从 iio_dev 中提取出私有数据,也就是 icm2608_dev 这个自定义结构体变量首地址dev->spi = spi; // 设备树匹配成功后,系统会分配struct spi_device *spi,因此将其spi赋值给我们定义的设备结构体上spi_set_drvdata(spi, indio_dev); // 将indio_dev设置为spi->driver_data私有数据mutex_init(&dev->mutex_lock); // 初始化互斥锁/* 2.3 初始化iio_dev */indio_dev->dev.parent = &spi->dev; // 获取spi->dev结构体indio_dev->channels = icm20608_channels; // 通道indio_dev->num_channels = ARRAY_SIZE(icm20608_channels); // 通道大小indio_dev->name = DEVICE_NAME; // 名称indio_dev->modes = INDIO_DIRECT_MODE; // 直接模式,提供sysfs接口indio_dev->info = &icm20608_info;/* 2.4 将iio_dev注册到内核 */ret = iio_device_register(indio_dev);if (ret < 0){dev_err(&spi->dev, "unable to register iio device\r\n");goto fail_iio_register;}/* 2.5 设置SPI的模式 */spi->mode = SPI_MODE_0; // MODE0,CPOL=0, CPHA=0spi_setup(spi); //设置好 spi_device 以后需要使用 spi_setup 配置一下/* 3.1 初始化 regmap_config 设置 和 regmap *//* 补充:当设置SPI读ICM20608操作时,使用 regmap 的时候就不需要手动将寄存器地址的 bit7 置 1,在初始化 regmap_config* 的时候直接将 read_flag_mask 设置为 0X80 即可,这样通过 regmap 读取 SPI 内部寄存器的时候* 就会将寄存器地址与 read_flag_mask 进行或运算,结果就是将 bit7 置 1,但是整个过程不需要* 我们来操作,全部由 regmap 框架来完成的 */dev->regmap_config.reg_bits = 8; /* 寄存器长度8bit */dev->regmap_config.val_bits = 8; /* 值长度8bit */dev->regmap_config.read_flag_mask = 0x80; /* 读掩码 */dev->regmap = regmap_init_spi(spi,&dev->regmap_config);if(IS_ERR(dev->regmap)) {ret = PTR_ERR(dev->regmap);goto fail_regmap_init;}/* 3.5 调用icm20608初始化函数 */icm20608_reg_init(dev);return 0;fail_regmap_init:iio_device_unregister(indio_dev);
fail_iio_register:
fail_iio_dev:return ret;
}/* 1.8 remove函数 */
static int icm20608_remove(struct spi_device *spi)
{int ret = 0;/* 获取私有数据 */struct iio_dev *indio_dev = spi_get_drvdata(spi);struct icm20608_dev *dev;dev = iio_priv(indio_dev);printk("icm20608_remove finish\r\n");/* 2.6 注销iio_dev */iio_device_unregister(indio_dev);/* 3.6 删除regmap */regmap_exit(dev->regmap);return ret;
}/* 1.5 传统的匹配表 */
static const struct spi_device_id icm20608_id[] = {{"alientek,icm20608", 0},{}};/* 1.6 设备树匹配表 */
static const struct of_device_id icm20608_of_match[] = {{.compatible = "alientek,icm20608"},{}};/* 1.4 spi_driver结构体 */
static struct spi_driver icm20608_driver = {.probe = icm20608_probe,.remove = icm20608_remove,.driver = {.owner = THIS_MODULE,.name = "icm20608",.of_match_table = icm20608_of_match,},.id_table = icm20608_id,
};// module_spi_driver(icm20608_driver);
/* 1.2 驱动模块入口函数 */
static int __init icm20608_init(void)
{return spi_register_driver(&icm20608_driver); // 注册spi驱动设备
}/* 1.3 驱动模块出口函数 */
static void __exit icm20608_exit(void)
{spi_unregister_driver(&icm20608_driver); // 注销spi驱动设备
}/* 1.1 驱动许可和个人信息 */
module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("djw");
测试现象:
四、完整的驱动程序
1、驱动程序
/** 根据linux内核的程序查找所使用函数的对应头文件。*/
#include <linux/types.h>
#include <linux/module.h> //MODULE_LICENSE,MODULE_AUTHOR
#include <linux/init.h> //module_init,module_exit
#include <linux/kernel.h> //printk
#include <linux/fs.h> //struct file_operations
#include <linux/slab.h> //kmalloc, kfree
#include <linux/uaccess.h> //copy_to_user,copy_from_user
#include <linux/io.h> //ioremap,iounmap
#include <linux/cdev.h> //struct cdev,cdev_init,cdev_add,cdev_del
#include <linux/device.h> //class
#include <linux/of.h> //of_find_node_by_path
#include <linux/of_gpio.h> //of_get_named_gpio
#include <linux/gpio.h> //gpio_request,gpio_direction_output,gpio_set_number
#include <linux/atomic.h> //atomic_t
#include <linux/of_irq.h> //irq_of_parse_and_map
#include <linux/interrupt.h> //request_irq
#include <linux/timer.h> //timer_list
#include <linux/jiffies.h> //jiffies
#include <linux/atomic.h> //atomic_set
#include <linux/input.h> //input
#include <linux/platform_device.h> //platform
#include <linux/delay.h> //mdelay
#include <linux/i2c.h>
#include <linux/spi/spi.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/iio/trigger_consumer.h>
#include <linux/iio/trigger.h>
#include <linux/iio/buffer.h>
#include <linux/iio/triggered_buffer.h>
#include <linux/regmap.h>
#include "icm20608.h"/* 定义设备名称 */
#define DEVICE_NAME "ICM20608"
#define ICM20608_TEMP_OFFSET 0
#define ICM20608_TEMP_SCALE 326800000 //每度是326.8。扩大1000000所得每度对应打数值/* 2.0 设备结构体 */
struct icm20608_dev
{struct spi_device *spi;struct regmap *regmap;struct regmap_config regmap_config;struct mutex mutex_lock; // 互斥锁,保证一次只有一个应用读取该数据
};/** icm20608陀螺仪分辨率,对应250、500、1000、2000,计算方法:* 以正负250度量程为例,500/2^16=0.007629,扩大1000000倍,就是7629*/
static const int gyro_scale_icm20608[] = {7629, 15258, 30517, 61035};/* * icm20608加速度计分辨率,对应2、4、8、16 计算方法:* 以正负2g量程为例,4/2^16=0.000061035,扩大1000000000倍,就是61035*/
static const int accel_scale_icm20608[] = {61035, 122070, 244140, 488281};/* 2.3.1.1 ICM20608的扫描元素,3轴加速计、3轴陀螺仪、1路温度传感器、一路时间戳 */
enum inv_icm20608_scan
{INV_ICM20608_SCAN_ACCL_X,INV_ICM20608_SCAN_ACCL_Y,INV_ICM20608_SCAN_ACCL_Z,INV_ICM20608_SCAN_TEMP,INV_ICM20608_SCAN_GYRO_X,INV_ICM20608_SCAN_GYRO_Y,INV_ICM20608_SCAN_GYRO_Z,INV_ICM20608_SCAN_TIMESTAMP,
};/* 2.3.1.2 */
/* _type:通道类型(加速度和陀螺仪), _channel2:当modified为1时,channel2为通道修饰符 */
/* info_mask_shared_by_type:通道共享; IIO_CHAN_INFO_SCALE 这个属性,表示这三个通道的分辨率是共用的,这样在 sysfs 下就会只生成一个描述分辨率的文件,这三个通道都可以使用这一个分辨率文件。*/
/* info_mask_separate:通道独立; IIO_CHAN_INFO_RAW为原始值,IIO_CHAN_INFO_CALIBBIAS校准值 */
#define ICM20608_CHANNEL(_type, _channel2, _index) \{ \.type = _type, \.modified = 1, \.channel2 = _channel2, \.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \BIT(IIO_CHAN_INFO_CALIBBIAS), \.scan_index = _index, \.scan_type = { \.sign = 's', \.realbits = 16, \.storagebits = 16, \.shift = 0, \.endianness = IIO_BE, \}, \}/* 2.3.1 icm20608 通道, 1 路温度通道, 3 路陀螺仪, 3 路加速度计 */
static const struct iio_chan_spec icm20608_channels[] = {/* 使用最元素的配置温度 */{.type = IIO_TEMP,.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE) | BIT(IIO_CHAN_INFO_OFFSET),.scan_index = INV_ICM20608_SCAN_TEMP,.scan_type = {.sign = 's',.realbits = 16,.storagebits = 16,.shift = 0,.endianness = IIO_BE,},},/* 2.3.1.3 加速度X、Y、Z三个通道 */ICM20608_CHANNEL(IIO_ACCEL, IIO_MOD_X, INV_ICM20608_SCAN_ACCL_X), // X轴ICM20608_CHANNEL(IIO_ACCEL, IIO_MOD_Y, INV_ICM20608_SCAN_ACCL_Y), // Y轴ICM20608_CHANNEL(IIO_ACCEL, IIO_MOD_Z, INV_ICM20608_SCAN_ACCL_Z), // Z轴/* 2.3.1.4 陀螺仪X、Y、Z三个通道 */ICM20608_CHANNEL(IIO_ANGL_VEL, IIO_MOD_X, INV_ICM20608_SCAN_GYRO_X), // X轴ICM20608_CHANNEL(IIO_ANGL_VEL, IIO_MOD_Y, INV_ICM20608_SCAN_GYRO_Y), // Y轴ICM20608_CHANNEL(IIO_ANGL_VEL, IIO_MOD_Z, INV_ICM20608_SCAN_GYRO_Z), // Z轴
};/* 3.2 icm20608读取单个寄存器 */
static u8 icm20608_read_onereg(struct icm20608_dev *dev, u8 reg) {u32 data = 0;u8 ret = 0;ret = regmap_read(dev->regmap, reg, &data);return (u8)data;
}/* 3.3 icm20608写一个寄存器 */
static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value) {u8 ret = 0;ret = regmap_write(dev->regmap, reg, value);
}/* 3.4 icm20608里面的寄存器初始化 */
void icm20608_reg_init(struct icm20608_dev *dev) {u8 value = 0;icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0X80); //复位,复位后为0X40,睡眠模式 mdelay(50);icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0X01); //关闭睡眠,自动选择时钟mdelay(50);value = icm20608_read_onereg(dev, ICM20_WHO_AM_I);printk("ICM20608 ID = 0X%X\r\n",value);value = icm20608_read_onereg(dev, ICM20_PWR_MGMT_1);printk("ICM20_PWR_MGMT_1 = 0X%X\r\n",value);/* 以下是关于6轴传感器的初始化 */icm20608_write_onereg(dev, ICM20_SMPLRT_DIV, 0x00); icm20608_write_onereg(dev, ICM20_GYRO_CONFIG, 0x18); icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG, 0x18); icm20608_write_onereg(dev, ICM20_CONFIG, 0x04); icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG2, 0x04);icm20608_write_onereg(dev, ICM20_PWR_MGMT_2, 0x00); icm20608_write_onereg(dev, ICM20_LP_MODE_CFG, 0x00); icm20608_write_onereg(dev, ICM20_FIFO_EN, 0x00);
}/* 4.1.1.1* 读取ICM20608传感器数据,可以用于陀螺仪、加速度计、温度的读取,读取2个寄存器值* dev: icm20608设备, reg: 要读取的通道寄存器首地址, anix: 需要读取的通道,比如X,Y,Z, val: 保存读取到的值。*/
static int icm20608_sensor_show(struct icm20608_dev *dev, int reg, int axis, int *val)
{int ind, result;__be16 d; //定义大端规则的16位整形变量ind = (axis - IIO_MOD_X) * 2; //计算读取的陀螺仪和加速度传感器的X、Y、Z轴的偏移寄存器地址result = regmap_bulk_read(dev->regmap, reg + ind, (u8 *)&d, 2);if (result)return -EINVAL;*val = (short)be16_to_cpup(&d); //将16位整形数据以大端规则转换存储return IIO_VAL_INT; //表示读取的数据类型是整数值,没有小数
}/* 4.1.1* 读取 ICM20608 陀螺仪、加速度计、温度通道值-----读取原始数据使用的* indio_dev : iio 设备, chan : 通道, val : 保存读取到的通道值。*/
static int icm20608_read_channel_data(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int *val)
{int ret = 0;struct icm20608_dev *dev = iio_priv(indio_dev);/* 区分通道类型,是温度传感器、陀螺仪传感器、加速度传感器 */switch (chan->type) {case IIO_ACCEL: //加速度printk("read channel type is IIO_ACCEL\r\n");ret = icm20608_sensor_show(dev, ICM20_ACCEL_XOUT_H, chan->channel2, val); /* channel2为X、Y、Z轴 */return ret;case IIO_ANGL_VEL: //陀螺仪printk("read channel type is IIO_ANGL_VEL\r\n");ret = icm20608_sensor_show(dev, ICM20_GYRO_XOUT_H, chan->channel2, val); /* channel2为X、Y、Z轴 */return ret;case IIO_TEMP: //温度printk("read channel type is IIO_TEMP\r\n");ret = icm20608_sensor_show(dev, ICM20_TEMP_OUT_H, IIO_MOD_X, val);return ret; default:return -EINVAL; //没有一项符合,那么返回参数不符合}return ret;
}/* 5.1.1* 设置ICM20608的陀螺仪计量程(分辨率)* dev:icm20608设备, val:量程(分辨率值)。* return:0,成功;其他值,错误*/
static int icm20608_write_gyro_scale(struct icm20608_dev *dev, int val)
{int result, i;u8 d;for (i = 0; i < ARRAY_SIZE(gyro_scale_icm20608); ++i) {if (gyro_scale_icm20608[i] == val) {d = (i << 3);result = regmap_write(dev->regmap, ICM20_GYRO_CONFIG, d);if (result)return result;return 0;}}return -EINVAL;
}/* 5.1.2* 设置ICM20608的加速度计量程(分辨率)* dev:icm20608设备, val:量程(分辨率值)。* return:0,成功;其他值,错误*/
static int icm20608_write_accel_scale(struct icm20608_dev *dev, int val)
{int result, i;u8 d;for (i = 0; i < ARRAY_SIZE(accel_scale_icm20608); ++i) {if (accel_scale_icm20608[i] == val) {d = (i << 3);result = regmap_write(dev->regmap, ICM20_ACCEL_CONFIG, d);if (result)return result;return 0;}}return -EINVAL;
}/* 5.1.3* 设置ICM20608传感器,可以用于陀螺仪、加速度计设置* dev:icm20608设备, reg:要设置的通道寄存器首地址, anix:要设置的通道,比如X,Y,Z, val:要设置的值。* return:0,成功;其他值,错误*/
static int icm20608_sensor_set(struct icm20608_dev *dev, int reg,int axis, int val)
{int ind, result;__be16 d = cpu_to_be16(val);ind = (axis - IIO_MOD_X) * 2;result = regmap_bulk_write(dev->regmap, reg + ind, (u8 *)&d, 2);if (result)return -EINVAL;return 0;
}/* 2.3.2.1 */
/* 读函数,当读取 sysfs 中的文件的时候最终此函数会执行,此函数里面会从传感器里面读取各种数据,然后上传给应用。* indio_dev : iio_dev, chan : 通道, val : 读取的值的整数部分,val2 : 读取的值的小数部分, mask : 掩码。*/
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;/********** 4.1 根据设置的通道文件参数作为条件依据读取数据类型 **********/struct icm20608_dev *dev = iio_priv(indio_dev);u8 regdata = 0; //保存读取寄存器的值// printk("icm20608_read_raw\r\n");/* 区分读取的数据类型,如raw、scale、offset等掩码 */switch (mask){case IIO_CHAN_INFO_RAW: //原始数据类型printk("read type is IIO_CHAN_INFO_RAW. \r\n");mutex_lock(&dev->mutex_lock); //上锁,保证读取的数据正确性ret = icm20608_read_channel_data(indio_dev, chan, val); //再进行细分筛选,判断读取的是原始数据类型下的哪个通道的数据(温度、加速度、陀螺仪),而这里val参数不用做处理,因为元素数据是ADC数据,是整形的mutex_unlock(&dev->mutex_lock); //释放锁return ret;case IIO_CHAN_INFO_SCALE: //分辨率数据类型printk("read type is IIO_CHAN_INFO_SCALE. \r\n");switch (chan->type) {case IIO_ACCEL: //加速度printk("read channel type is IIO_ACCEL. \r\n");mutex_lock(&dev->mutex_lock); //上锁,保证读取的数据正确性regdata = (icm20608_read_onereg(dev, ICM20_ACCEL_CONFIG) & 0x18) >> 3; //获取配置寄存器的3、4bit位的分辨率配置值*val = 0; //陀加速度的分辨率是小数,因此整数部分为0*val2 = accel_scale_icm20608[regdata];mutex_unlock(&dev->mutex_lock); //释放锁return IIO_VAL_INT_PLUS_NANO; //小数部分放大1000000000倍 值为val+val2/1000000000 case IIO_ANGL_VEL: //陀螺仪printk("read channel type is IIO_ANGL_VEL\r\n");mutex_lock(&dev->mutex_lock); //上锁,保证读取的数据正确性regdata = (icm20608_read_onereg(dev, ICM20_GYRO_CONFIG) & 0x18) >> 3; //获取配置寄存器的3、4bit位的分辨率配置值*val = 0; //陀螺仪的分辨率是小数,因此整数部分为0*val2 = gyro_scale_icm20608[regdata]; //获取已经计算出来的陀螺仪分辨率表的分辨率值mutex_unlock(&dev->mutex_lock); //释放锁return IIO_VAL_INT_PLUS_MICRO; //小数部分放大1000000倍, 值为val+val2/1000000case IIO_TEMP: //温度printk("read channel type is IIO_TEMP\r\n");*val = ICM20608_TEMP_SCALE/ 1000000;*val2 = ICM20608_TEMP_SCALE % 1000000;return IIO_VAL_INT_PLUS_MICRO; /* 值为val+val2/1000000 */ default:return -EINVAL; //没有一项符合,那么返回参数不符合}return ret;case IIO_CHAN_INFO_OFFSET: //补偿、偏置数据类型printk("read type is IIO_CHAN_INFO_OFFSET. \r\n");switch (chan->type) {case IIO_TEMP:*val = ICM20608_TEMP_OFFSET;return IIO_VAL_INT;default:return -EINVAL;}return ret;case IIO_CHAN_INFO_CALIBBIAS: //校准数据类型printk("read type is IIO_CHAN_INFO_CALIBBIAS. \r\n");switch (chan->type) {case IIO_ANGL_VEL: /* 陀螺仪的校准值 */mutex_lock(&dev->mutex_lock);ret = icm20608_sensor_show(dev, ICM20_XG_OFFS_USRH, chan->channel2, val);mutex_unlock(&dev->mutex_lock);return ret;case IIO_ACCEL: /* 加速度计的校准值 */mutex_lock(&dev->mutex_lock); ret = icm20608_sensor_show(dev, ICM20_XA_OFFSET_H, chan->channel2, val);mutex_unlock(&dev->mutex_lock);return ret;default:return -EINVAL;}default:return -EINVAL; //没有一项符合,那么返回参数不符合}/********** 4.1 根据设置的通道文件参数作为条件依据读取数据类型 **********/return ret;
}/* 2.3.2.2 */
static int icm20608_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int val, int val2, long mask)
{int ret = 0;/********** 5.1 根据设置的通道文件参数作为条件依据读取数据类型 **********/struct icm20608_dev *dev = iio_priv(indio_dev);// printk("icm20608_write_raw\r\n");switch (mask) {case IIO_CHAN_INFO_SCALE: /* 设置陀螺仪和加速度计的分辨率 */switch (chan->type) {case IIO_ANGL_VEL: /* 设置陀螺仪 */mutex_lock(&dev->mutex_lock);ret = icm20608_write_gyro_scale(dev, val2);mutex_unlock(&dev->mutex_lock);break;case IIO_ACCEL: /* 设置加速度计 */mutex_lock(&dev->mutex_lock);ret = icm20608_write_accel_scale(dev, val2);mutex_unlock(&dev->mutex_lock);break;default:ret = -EINVAL;break;}break;case IIO_CHAN_INFO_CALIBBIAS: /* 设置陀螺仪和加速度计的校准值*/switch (chan->type) {case IIO_ANGL_VEL: /* 设置陀螺仪校准值 */mutex_lock(&dev->mutex_lock);ret = icm20608_sensor_set(dev, ICM20_XG_OFFS_USRH,chan->channel2, val);mutex_unlock(&dev->mutex_lock);break;case IIO_ACCEL: /* 加速度计校准值 */mutex_lock(&dev->mutex_lock);ret = icm20608_sensor_set(dev, ICM20_XA_OFFSET_H,chan->channel2, val);mutex_unlock(&dev->mutex_lock);break;default:ret = -EINVAL;break;}break;default:ret = -EINVAL;break;} /********** 5.1 根据设置的通道文件参数作为条件依据读取数据类型 **********/return ret;
}/* 2.3.2.3 * 用户空间写数据格式,比如我们在用户空间操作 sysfs 来设置传感器的分辨率,如果分辨率带小数,那么这个小数传递到内核空间应该扩大多少倍,此函数就是用来设置这个的。* indio_dev : iio_dev* chan : 通道* mask : 掩码* return : 0,成功;其他值,错误*/
static int icm20608_write_raw_get_fmt(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, long mask)
{/********** 6.1 根据设置的通道文件参数作为条件依据读取数据类型 **********/ switch (mask) {case IIO_CHAN_INFO_SCALE:switch (chan->type) {case IIO_ANGL_VEL: /* 用户空间写的陀螺仪分辨率数据要乘以1000000 */return IIO_VAL_INT_PLUS_MICRO;default: /* 用户空间写的加速度计分辨率数据要乘以1000000000 */return IIO_VAL_INT_PLUS_NANO;}default:return IIO_VAL_INT_PLUS_MICRO;}return -EINVAL;/********** 6.1 根据设置的通道文件参数作为条件依据读取数据类型 **********/ }/* 2.3.2 */
/* iio_info,当应用程序读取相应的驱动文件的时候, xxx_read_raw* 函数就会执行,我们在此函数中会读取传感器数据,然后返回给应用层。当应用层向相应的驱* 动写数据的时候, xxx_write_raw 函数就会执行。因此 xxx_read_raw 和 xxx_write_raw 这两个函* 数是非常重要的!需要我们根据具体的传感器来编写,这两个函数是编写 IIO 驱动的核心。* */
static const struct iio_info icm20608_info = {.read_raw = &icm20608_read_raw,.write_raw = &icm20608_write_raw,.write_raw_get_fmt = &icm20608_write_raw_get_fmt,.driver_module = THIS_MODULE,
};/* 1.7 probe函数 */
static int icm20608_probe(struct spi_device *spi)
{int ret = 0;struct icm20608_dev *dev;struct iio_dev *indio_dev;printk("icm20608_probe successful!\r\n");/* 2.1 申请icm20608_dev结构体大小的内存, 为私人结构体“icm20608_dev”分配内存空间 */indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*dev));if (!indio_dev){ret = -ENOMEM;goto fail_iio_dev;}/* 2.2 获取定义的设备结构体首地址和获取spi_device结构体等数据私有化操作 */dev = iio_priv(indio_dev); // 使用 iio_priv 函数从 iio_dev 中提取出私有数据,也就是 icm2608_dev 这个自定义结构体变量首地址dev->spi = spi; // 设备树匹配成功后,系统会分配struct spi_device *spi,因此将其spi赋值给我们定义的设备结构体上spi_set_drvdata(spi, indio_dev); // 将indio_dev设置为spi->driver_data私有数据mutex_init(&dev->mutex_lock); // 初始化互斥锁/* 2.3 初始化iio_dev */indio_dev->dev.parent = &spi->dev; // 获取spi->dev结构体indio_dev->channels = icm20608_channels; // 通道indio_dev->num_channels = ARRAY_SIZE(icm20608_channels); // 通道大小indio_dev->name = DEVICE_NAME; // 名称indio_dev->modes = INDIO_DIRECT_MODE; // 直接模式,提供sysfs接口indio_dev->info = &icm20608_info;/* 2.4 将iio_dev注册到内核 */ret = iio_device_register(indio_dev);if (ret < 0){dev_err(&spi->dev, "unable to register iio device\r\n");goto fail_iio_register;}/* 2.5 设置SPI的模式 */spi->mode = SPI_MODE_0; // MODE0,CPOL=0, CPHA=0spi_setup(spi); //设置好 spi_device 以后需要使用 spi_setup 配置一下/* 3.1 初始化 regmap_config 设置 和 regmap *//* 补充:当设置SPI读ICM20608操作时,使用 regmap 的时候就不需要手动将寄存器地址的 bit7 置 1,在初始化 regmap_config* 的时候直接将 read_flag_mask 设置为 0X80 即可,这样通过 regmap 读取 SPI 内部寄存器的时候* 就会将寄存器地址与 read_flag_mask 进行或运算,结果就是将 bit7 置 1,但是整个过程不需要* 我们来操作,全部由 regmap 框架来完成的 */dev->regmap_config.reg_bits = 8; /* 寄存器长度8bit */dev->regmap_config.val_bits = 8; /* 值长度8bit */dev->regmap_config.read_flag_mask = 0x80; /* 读掩码 */dev->regmap = regmap_init_spi(spi,&dev->regmap_config);if(IS_ERR(dev->regmap)) {ret = PTR_ERR(dev->regmap);goto fail_regmap_init;}/* 3.5 调用icm20608初始化函数 */icm20608_reg_init(dev);return 0;fail_regmap_init:iio_device_unregister(indio_dev);
fail_iio_register:
fail_iio_dev:return ret;
}/* 1.8 remove函数 */
static int icm20608_remove(struct spi_device *spi)
{int ret = 0;/* 获取私有数据 */struct iio_dev *indio_dev = spi_get_drvdata(spi);struct icm20608_dev *dev;dev = iio_priv(indio_dev);printk("icm20608_remove finish\r\n");/* 2.6 注销iio_dev */iio_device_unregister(indio_dev);/* 3.6 删除regmap */regmap_exit(dev->regmap);return ret;
}/* 1.5 传统的匹配表 */
static const struct spi_device_id icm20608_id[] = {{"alientek,icm20608", 0},{}};/* 1.6 设备树匹配表 */
static const struct of_device_id icm20608_of_match[] = {{.compatible = "alientek,icm20608"},{}};/* 1.4 spi_driver结构体 */
static struct spi_driver icm20608_driver = {.probe = icm20608_probe,.remove = icm20608_remove,.driver = {.owner = THIS_MODULE,.name = "icm20608",.of_match_table = icm20608_of_match,},.id_table = icm20608_id,
};// module_spi_driver(icm20608_driver);
/* 1.2 驱动模块入口函数 */
static int __init icm20608_init(void)
{return spi_register_driver(&icm20608_driver); // 注册spi驱动设备
}/* 1.3 驱动模块出口函数 */
static void __exit icm20608_exit(void)
{spi_unregister_driver(&icm20608_driver); // 注销spi驱动设备
}/* 1.1 驱动许可和个人信息 */
module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("djw");
2、测试现象
我们修改了加速度传感器分辨率后,查看的重力加速度的原始数据值也发生改变,大家可以将分辨率与重力加速度原始数据值相乘,约等于1个g的加速度。大家自行测试其他的通道文件了。
3、icm20608APP程序及测试
/***************************************************************
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>/* 字符串转数字,将浮点小数字符串转换为浮点数数值 */
#define SENSOR_FLOAT_DATA_GET(ret, index, str, member)\ret = file_data_read(file_path[index], str);\dev->member = atof(str);\
/* 字符串转数字,将整数字符串转换为整数数值 */
#define SENSOR_INT_DATA_GET(ret, index, str, member)\ret = file_data_read(file_path[index], str);\dev->member = atoi(str);\
/* icm20608 iio框架对应的文件路径 */
static char *file_path[] = {"/sys/bus/iio/devices/iio: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,
};/** icm20608数据设备结构体*/
struct icm20608_dev{int accel_x_calibbias, accel_y_calibbias, accel_z_calibbias;int accel_x_raw, accel_y_raw, accel_z_raw;int gyro_x_calibbias, gyro_y_calibbias, gyro_z_calibbias;int gyro_x_raw, gyro_y_raw, gyro_z_raw;int temp_offset, temp_raw;float accel_scale, gyro_scale, temp_scale;float gyro_x_act, gyro_y_act, gyro_z_act;float accel_x_act, accel_y_act, accel_z_act;float temp_act;
};struct icm20608_dev icm20608;/** @description : 读取指定文件内容* @param - filename : 要读取的文件路径* @param - str : 读取到的文件字符串* @return : 0 成功;其他 失败*/
static int file_data_read(char *filename, char *str)
{int ret = 0;FILE *data_stream;data_stream = fopen(filename, "r"); /* 只读打开 */if(data_stream == NULL) {printf("can't open file %s\r\n", filename);return -1;}ret = fscanf(data_stream, "%s", str);if(!ret) {printf("file read error!\r\n");} else if(ret == EOF) {/* 读到文件末尾的话将文件指针重新调整到文件头 */fseek(data_stream, 0, SEEK_SET); }fclose(data_stream); /* 关闭文件 */ return 0;
}/** @description : 获取ICM20608数据* @param - dev : 设备结构体* @return : 0 成功;其他 失败*/
static int sensor_read(struct icm20608_dev *dev)
{int ret = 0;char str[50];/* 1、获取陀螺仪原始数据 */SENSOR_FLOAT_DATA_GET(ret, IN_ANGLVEL_SCALE, str, gyro_scale);SENSOR_INT_DATA_GET(ret, IN_ANGLVEL_X_RAW, str, gyro_x_raw);SENSOR_INT_DATA_GET(ret, IN_ANGLVEL_Y_RAW, str, gyro_y_raw);SENSOR_INT_DATA_GET(ret, IN_ANGLVEL_Z_RAW, str, gyro_z_raw);/* 2、获取加速度计原始数据 */SENSOR_FLOAT_DATA_GET(ret, IN_ACCEL_SCALE, str, accel_scale);SENSOR_INT_DATA_GET(ret, IN_ACCEL_X_RAW, str, accel_x_raw);SENSOR_INT_DATA_GET(ret, IN_ACCEL_Y_RAW, str, accel_y_raw);SENSOR_INT_DATA_GET(ret, IN_ACCEL_Z_RAW, str, accel_z_raw);/* 3、获取温度值 */SENSOR_FLOAT_DATA_GET(ret, IN_TEMP_SCALE, str, temp_scale);SENSOR_INT_DATA_GET(ret, IN_TEMP_OFFSET, str, temp_offset);SENSOR_INT_DATA_GET(ret, IN_TEMP_RAW, str, temp_raw);/* 3、转换为实际数值 */dev->accel_x_act = dev->accel_x_raw * dev->accel_scale;dev->accel_y_act = dev->accel_y_raw * dev->accel_scale;dev->accel_z_act = dev->accel_z_raw * dev->accel_scale;dev->gyro_x_act = dev->gyro_x_raw * dev->gyro_scale;dev->gyro_y_act = dev->gyro_y_raw * dev->gyro_scale;dev->gyro_z_act = dev->gyro_z_raw * dev->gyro_scale;dev->temp_act = ((dev->temp_raw - dev->temp_offset) / dev->temp_scale) + 25;return ret;
}/** @description : main主程序* @param - argc : argv数组元素个数* @param - argv : 具体参数* @return : 0 成功;其他 失败*/
int main(int argc, char *argv[])
{int ret = 0;if (argc != 1) {printf("Error Usage!\r\n");return -1;}while (1) {ret = sensor_read(&icm20608);if(ret == 0) { /* 数据读取成功 */printf("\r\n原始值:\r\n");printf("gx = %d, gy = %d, gz = %d\r\n", icm20608.gyro_x_raw, icm20608.gyro_y_raw, icm20608.gyro_z_raw);printf("ax = %d, ay = %d, az = %d\r\n", icm20608.accel_x_raw, icm20608.accel_y_raw, icm20608.accel_z_raw);printf("temp = %d\r\n", icm20608.temp_raw);printf("实际值:");printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", icm20608.gyro_x_act, icm20608.gyro_y_act, icm20608.gyro_z_act);printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", icm20608.accel_x_act, icm20608.accel_y_act, icm20608.accel_z_act);printf("act temp = %.2f°C\r\n", icm20608.temp_act);}usleep(100000); /*100ms */}return 0;
}
实验现象:
相关文章:

驱动程序开发:基于ICM20608六轴传感器 --- 使用Regmap API 的 SPI 读取数据 之 IIO驱动
目录一、IIO 子系统简介二、IIO子系统使用的一些相关的结构体、函数等1、iio_dev 结构体 ①modes:是选择iio驱动设备支持的工作模式,模式分别有如下: ②dev:其是一个设备结构体。 ②channels:为 IIO 设备通道…...

专利撰写 为什么要申请专利 申请专利对个人有什么利益关系 专利申请实例 如何申请专利 专利申请办理流程
专利撰写 专利是对发明者或创造者所创造的发明或设计提供一定期限的独占权的法律保护。撰写专利需要考虑到多方面的因素,包括发明或设计的技术性、可行性、独创性、保密性等等。以下是一些关于专利撰写的常见问题和注意事项:专利类型:专利包括…...

yolov5/6/7系列模型训练日志结果数据对比分析可视化
早在之前使用yolov3和yolov4这类项目的时候可视化分析大都是自己去做的,到了yolov5的时候,变成了一个工具包了,作者全部集成进去了,这里我们以一个具体的结果为例,如下:整个训练过程产生的指标等数据都会自…...

ppppp2-23
#!/bin/sh USBFILE/etc/ppp/usbdevices LIST/etc/ppp/diallist function ec25_find_ttyname() { DEVNAME$1 FLAG0 USB_FIND_PATH/sys/bus/usb/devices for dir in $(ls $USB_FIND_PATH) do echo $(ls USBFINDPATH/USB_FIND_PATH/USBFINDPATH/dir) | grep ttyUSB > /dev…...

【GeoDjango框架解析——读取矢量数据写入postgis数据库】
系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 geodjango框架解析之读取矢量数据shp文件写入postgis数据库 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录…...

注意啦!如何通过广告吸引客户直接下单?
2023年跨境电商越来越突出,据业内相关人士称,在未来几年与跨境电商相关的政策仍会继续倾斜甚至加大力度,因此各行各业都响应政策,在新政策落实之前致力于平台的转型升级,做新时代创新型的高质量发展,其实细…...

ThinkPHP ^6图片操作进阶
图片裁剪、缩略、水印不再是TP框架系统内置的功能,需要安装。 目录 安装 图片处理 1.创建图片对象 2.获取图片属性 3.裁剪图像 4.生成缩略图 6.保存图像 7.水印 安装 使用composer在项目根目录打开命令行执行: composer require topthink/think…...

深入理解JS作用域链与执行上下文
变量提升: 变量提升( hoisting )。 我可恨的 var 关键字: 你读完下面内容就会明白标题的含义,先来一段超级简单的代码: <script type"text/javascript">var str Hello JavaScript hoi…...
UnityEditor编辑器扩展代码实现Project搜索的实现功能和切换Component等
反射实现切换Gameobjecect-Comp之前介绍过Kinematic Character Controller这个插件这个插件很容易和另外一个插件混淆,两个作者头像比较相像,而且这个插件的作者不太喜欢露脸(他现在做Dot-CharacterControl去了),几乎网…...

SKAdNetwork:从0到1
一、什么是SKAdNetwork https://developer.apple.com/documentation/storekit/skadnetwork iOS14.5开始,获取IDFA需要用户确认授权才可,此时SKAdNetwork 正式回归。 SKAdNetwork 是苹果在2018年推出的一个更加保护用户隐私的归因框架,并与…...

Spring+MVC+MYbatis注解开发
Spring常见注解 注解一:Configuration 用在类上面,加上这个注解的类可以成为一个spring的xml配置文件,使用的是java代码的配置 注解二:ComponentScan 用在类上,加上注解可以指定扫描路径 注解三:创建对…...

Redis主从复制过程
将目前服务器加入到端口号为6379的从服务器 一主二仆 当期中一台从服务器宕机之后 从服务器重启之后会变成单独的主服务器,与之前的主从复制没有关系,重新使用slaceof命令才能恢复到之前一样 主服务器宕机后,从服务器不会成为主服务器&…...

Spring boot开启定时任务的三种方式(内含源代码+sql文件)
Spring boot开启定时任务的三种方式(内含源代码sql文件) 源代码sql文件下载链接地址:https://download.csdn.net/download/weixin_46411355/87486580 目录Spring boot开启定时任务的三种方式(内含源代码sql文件)源代码…...

Tekton实战案例--S2I
案例环境说明 示例项目: 代码仓库:https://gitee.com/mageedu/spring-boot-helloWorld.git 构建工具maven pipeline各Task git-clone:克隆项目的源代码 build-to-package: 代码测试,构建和打包 generate-build-id:生…...

四、使用类实现功能
使用类实现功能 ts中类的继承 ES6中class类中,属性分为:实例上的属性,原型上的方法;也可以叫做:class的属性,class的方法。 类的继承叫法:父类>子类,基类>派生类;…...

Java多线程不安全的例子
目录 1. 可见性不安全例子 2. 原子性不安全例子 3. 有序性不安全例子 1. 可见性不安全例子 可见性:一个线程对共享变量的修改,另外一个线程不能够立刻看到。 如果多线程对共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致…...

vivo X Flip会是高端手机市场的又一折叠屏爆款吗?
据多个平台消息,vivo即将推出小折叠屏手机X Flip。据了解,vivo X Flip将采用轻盈便携的竖向折叠布局,以及非常受女性消费者喜爱的结构设计。那么,vivo X Flip会是vivo折叠屏的又一个爆款吗? 一、vivo X Flip小折叠屏手…...

MySQL中MVCC如何解决不可重复读以及幻读?
了解MVCC之前,我们首先需要了解以下两个概念:一致性非锁定读和锁定读,了解这两个概念之后我们在逐步分析MVCC。 一致性非锁定读和锁定读 一致性非锁定读(快照读) 对于 一致性非锁定读的实现,通常做法是加一个版本号或者时间戳字…...

设计模式第八讲:观察者模式和中介者模式详解
一. 观察者模式 1. 背景 在现实世界中,许多对象并不是独立存在的,其中一个对象的行为发生改变可能会导致一个或者多个其他对象的行为也发生改变。例如,某种商品的物价上涨时会导致部分商家高兴,而消费者伤心;还有&…...

关于 mac 本地配置域名能 ping 通,但是浏览器不能访问的问题(而其他电脑操作可访问)
关于 mac 本地配置域名能 ping 通,但是浏览器不能访问的问题(而其他电脑操作可访问)1. 配置域名的方式1.1 sudo vim /etc/hosts1.2 浏览器插件 LiveHosts2. 问题描述3. 解决问题方法3.1 尝试方法1—确保代理都关闭3.2 尝试方法2—确保域名能p…...

【代码随想录二刷】Day23-二叉树-C++
代码随想录二刷Day23 今日任务 669.修剪二叉搜索树 108.将有序数组转换为二叉搜索树 538.把二叉搜索树转换为累加树 语言:C 669. 修剪二叉搜索树 链接:https://leetcode.cn/problems/trim-a-binary-search-tree/ 递归 class Solution { public:Tree…...

Linux GPIO 开发指南
文章目录Linux GPIO 开发指南1 概述1.1 编写目的1.2 适用范围1.3 相关人员2 模块介绍2.1 模块功能介绍2.2 相关术语介绍2.3 总体框架2.4 state/pinmux/pinconfig2.5 源码结构介绍3 模块配置3.1 kernel menuconfig 配置3.2 device tree 源码结构和路径3.2.1 device tree 对 gpio…...

记一次后端生成Zip文件通过浏览器下载后文件损坏,无法打开,不可预知的末端错误,下载后文件比源文件增大
记一次后端生成Zip文件问题前言问题出现排查一、流没有关好二、写入了空白字节三、没有flush定位环节一、生成二、通过SwaggerUI、PostMan进行下载三、结论解决方法前言 在项目上线前夕,临时添加了个数据导出的接口,需求是导出压缩包,选择了项…...

python中savgol_filter的详细解释
目录savgol_filter简介savgol_filter原理参数window_length对平滑的效果参数polyorder的平滑效果savgol_filter简介 Savitzky-Golay滤波器最初由Savitzky和Golay于1964年提出,是光谱预处理中常用滤波方法,它的核心思想是对一定长度窗口内的数据点进行k阶…...

C语言--指针进阶1
目录回顾字符指针指针数组数组指针&数组名和数组名的区别数组指针的使用指针作为形参练习数组参数、指针参数一维数组传参二维数组传参一级指针传参二级指针传参回顾 指针的内容,我们在初级阶段已经有所涉及了,我们先来复习一下 指针就是个变量&am…...

ssh的使用
Halo,这里是Ppeua。平时主要更新C语言,C,数据结构算法......感兴趣就关注我吧!你定不会失望。 🌈个人主页:主页链接 🌈算法专栏:专栏链接 我会一直往里填充内容哒! &…...

Apache Hadoop生态-目录汇总-持续更新
目录 1:系统服务分布图 3台分布式架构 1台单机架构 服务版本介绍 2:服务目录 存储相关 数据采集 任务调度 即席查询 数据可视化 集群监控 元数据管理 用户认证 权限管理 第三方windows客户端 1:系统服务分布图 3台分布式架构…...

「JVM 编译后话」编译器优化技术
后端编译(即时编译、提前编译)的目标时将字节码翻译成本地机器码,而难点是输出优化质量较高的机器码; 文章目录1. 优化技术概览2. 方法内联(Inlining)3. 逃逸分析(Escape Analysis)4…...

【python学习笔记】:输出与输入
01 输出方式 表达式语句、print()函数和使用文件对象的write()方法。 02 输出形式 格式化输出str.format()函数、转成字符串可以使用repr()或str()函数来实现。 (1)repr():产生一个解释器易读的表达形式,便于字符串的拼接。 例:输出平方与…...

汽车电子社区交流宣传
http://t.csdn.cn/VSLO0http://t.csdn.cn/VSLO0 当今的汽车行业已经进入了数字化时代,汽车电子软件的开发变得越来越重要。在这个领域,开发者们需要应对各种挑战,包括复杂的硬件和软件交互、高效的嵌入式编程和安全性要求。为了帮助汽车电子…...