Linux-SPI-ICM20608

环境

硬件环境

  • 开发板型号100ask_imx6ull_pro 开发板
  • 处理器类型:NXP IMX6ULL
  • 处理器架构:恩单核 Cortex-A7
  • 处理器主频:800MHZ
  • 内存容量:512 MB DDR3
  • 存储介质:4GB eMMC
  • 本次测试的驱动:ICM20608 六轴传感器

软件环境

  • 宿主机
    • 宿主机操作系统:Ubuntu 18.04
    • 交叉编译器:100ask 提供的工具链 arm-buildroot-linux-gnueabihf- 支持的最低内核版本:4.9.0
  • 开发板

ICM20608

ICM20608 是由 InvenSense 公司生产的一款 6 轴惯性测量单元(IMU),它集成了 3 轴陀螺仪和 3 轴加速度计。这款传感器广泛应用于需要精确运动跟踪的场合,比如无人机、机器人、智能手机和可穿戴设备等。

  • 陀螺仪支持 X,Y 和 Z 三轴输出,内部集成 16 位 ADC,测量范围可设置:±250,±500,±1000 和 ±2000°/s
  • 加速度计支持 X,Y 和 Z 轴输出,内部集成 16 位 ADC,测量范围可设置:±2g,±4g,±8g 和 ±16g,g 表示重力加速度 1g=9.8m/s²。
  • 内部包含一个 512 字节的 FIFO
  • 支持快速 I2C,速度可达 400 KHz
  • 支持 SPI,速度可达 8 MHz
  • 寄存器 8 位,寄存器位宽 8位

image-20240701203832248

如果使用 I2C 接口,则 AD0 引脚决定 I2C 设备地址的最后以为,AD0 = 0 则设备地址为 0x68,AD0 = 1 则设备地址为 0x69,我们这里用的是 SPI 接口,就不用纠结这个了。

使用 SPI 接口读写寄存器需要 16 个时钟或者更多(如果读写操作包括多个字节的话),第一个字节包含要读写的寄存器地址,寄存器地址最高位是读写标志位,如果是读的话寄存器地址最高位要为 1,如果是写的话寄存器地址最高位要为 0,剩下的 7 位才是实际的寄存器地址,寄存器地址后面跟着的就是读写的数据。

本次使用到的 ICM20608 的一些重要寄存器如下:

image-20240701205419079

image-20240701205429856

原理图如下:

image-20240701205602840

驱动代码编写

设备描述信息

添加 ICM20608 所使用到的 IO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
&iomuxc {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
imx6ul-evk {
pinctrl_ecspi3: ecspi3 {
fsl,pins = <
MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x000010B0
MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 0x000010B0
MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x000010B0
MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20 0x000010B0
>;
};
};
};

添加 ecspi3 节点追加 ICM20608子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
&ecspi3 { 
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi3>;
cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>; // cs-gpios软件操作片选IO
status = "okay";

// cs-gpios 是根据驱动文件中的命名设置的
spidev: icm20608@0{ // 0表示使用ecspi3的0通道,跟reg对应
compatible = "invensense,icm20608";
interrupt-parent = <&gpio1>;
interrupts = <1 1>;
spi-max-frequency = <8000000>; // 可支持的最高频率
reg = <0>;
};
};

注意事项

在修改设备树时,记得检查一下是否其他设备也用到了相同引脚。遇到了一个问题,就是驱动程序和设备树的 compatible 都相同,但是始终发现匹配不成功,没有进入到 probe 函数,驱动程序匹配主要就看 compatible 这些匹配的信息,都无误,于是查看设备树,发现原来是引脚冲突了,ecspi3 子节点和 uart2、can2 冲突,忘记删减了。

image-20240716132936690

driver 驱动代码

寄存器定义和 icm20608_dev 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/fs.h> /*注册设备节点的文件结构体*/
#include <linux/cdev.h> // 对字符设备结构cdev 以及一系列的操作函数的定义。包含了cdev 结构及相关函数的定义。
#include <linux/device.h> //包含了device、class 等结构的定义
#include <linux/string.h>

#include <linux/delay.h>
#include <linux/uaccess.h> //包含了copy_to_user、copy_from_user 等内核访问用户进程内存地址的函数定义。

/* ICM20608寄存器定义 */
#define ICM20608_PWR_MGMT_1 0x6B
#define ICM20608_PWR_MGMT_2 0x6C
#define ICM20608_SMPLRT_DIV 0x19
#define ICM20608_CONFIG 0x1A
#define ICM20608_GYRO_CONFIG 0x1B
#define ICM20608_ACCEL_CONFIG 0x1C
#define ICM20608_ACCEL_CONFIG2 0x1D
#define ICM20608_LP_MODE_CFG 0x1E
#define ICM20608_FIFO_EN 0x23
#define ICM20608_WHO_AM_I 0x75
#define ACCEL_XOUT_H 0x3B

#define ICM20608_NAME "icm20608"

struct icm20608_data {
int16_t accel_x;
int16_t accel_y;
int16_t accel_z;
int16_t gyro_x;
int16_t gyro_y;
int16_t gyro_z;
int16_t temperature;
};

struct icm20608_dev {
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
struct device_node *nd; /* 设备节点 */
int major; /* 主设备号 */
void *private_data; /* 私有数据 */
struct icm20608_data data;
};

static struct icm20608_dev icm20608_dev_t;

注册/注销 spi_driver 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* 设备树compatible匹配表 */
static const struct of_device_id of_match_table[] = {
{.compatible = "invensense,icm20608"},
{},
};

/* 无设备树的匹配ID表 */
static const struct spi_device_id id_table[] = {
{"invensense,icm20608"},
{},
};

/* 定义i2c总线设备结构体 */
static struct spi_driver icm20608_spi_driver = {
.driver = {
.owner = THIS_MODULE,
.name = "icm20608",
.of_match_table = of_match_table,
},
.probe = icm20608_spi_driver_probe,
.remove = icm20608_spi_driver_remove,
.id_table = id_table,
};


/* 驱动入口函数 */
static int __init icm20608_spi_driver_init(void)
{
int ret;
// 注册 spi 驱动
ret = spi_register_driver(&icm20608_spi_driver);

printk("icm20608_spi_driver_init ok\n");
return ret;
}

/* 驱动出口函数 */
static void __exit icm20608_spi_driver_exit(void)
{
// 将前面注册的spi_driver也从Linux内核中注销掉
spi_unregister_driver(&icm20608_spi_driver);
printk("icm20608_spi_driver_exit ok\n");
}

module_init(icm20608_spi_driver_init);
module_exit(icm20608_spi_driver_exit);
MODULE_LICENSE("GPL");

添加字符设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
static const struct file_operations icm20608_fops = {
.owner = THIS_MODULE,
.open = icm20608_open,
.release = icm20608_close,
.read = icm20608_read,
};

static int icm20608_spi_driver_probe(struct spi_device *spi)
{
// 1.分配设备号
if(icm20608_dev_t.major)
{
icm20608_dev_t.devid = MKDEV(icm20608_dev_t.major, 0);
register_chrdev_region(icm20608_dev_t.devid, 1, ICM20608_NAME);
}
else
{
alloc_chrdev_region(&icm20608_dev_t.devid, 0, 1, ICM20608_NAME);
icm20608_dev_t.major = MAJOR(icm20608_dev_t.devid);
}

// 2.初始化cdev
cdev_init(&icm20608_dev_t.cdev, &icm20608_fops);

// 3.添加设备号到cdev
cdev_add(&icm20608_dev_t.cdev, icm20608_dev_t.devid, 1);

// 4.创建类
icm20608_dev_t.class = class_create(THIS_MODULE, ICM20608_NAME);
if (IS_ERR(icm20608_dev_t.device))
return PTR_ERR(icm20608_dev_t.device);

// 5.类下创建设备
icm20608_dev_t.device = device_create(icm20608_dev_t.class, NULL, icm20608_dev_t.devid, NULL, ICM20608_NAME);
if (IS_ERR(icm20608_dev_t.device))
return PTR_ERR(icm20608_dev_t.device);

// 设备树设置软件片选,spi主控制器为自动帮我们操作片选IO,所以就不需要手动控制了

// 初始化spi_device结构体
spi->mode = SPI_MODE_0; /* MODE0,CPOL=0,CPHA=0 */
spi_setup(spi);
icm20608_dev_t.private_data = spi; /* 设置私有数据 */

printk("icm20608_spi_driver_probe ok\n");

return 0;
}
static int icm20608_spi_driver_remove(struct spi_device *spi)
{
// 1.注销设备号
unregister_chrdev_region(icm20608_dev_t.devid, 1);
// 2.删除设备
cdev_del(&icm20608_dev_t.cdev);
// 3.注销设备节点
device_destroy(icm20608_dev_t.class, icm20608_dev_t.devid);
// 4.删除类
class_destroy(icm20608_dev_t.class);

printk("icm20608_spi_driver_remove ok\n");
return 0;
}

收发数据函数

前面我们在设备树中将片选 IO 设置为了软中断,且 Linux 内核中的 SPI 主控制器会自动帮我们操作片选 IO,因此在收发数据函数中我们不能手动控制片选 IO 了。另外需要注意的是 SPI 接收的时候会先接收到一个字节的寄存器地址,读取数据时我们需要跳过该字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/*
* @description : 读取 icm20608 设备多个寄存器数据
* @param – dev : icm20608 设备
* @param – reg : 要读取的寄存器首地址
* @param – val : 读取到的数据
* @param – len : 要读取的数据长度
* @return : 操作结果
*/
static int icm20608_read_reg(struct icm20608_dev *dev, uint8_t reg, void *val, uint8_t len)
{
int ret;
uint8_t tx_data[1];
uint8_t *rx_data;
struct spi_transfer *t;
struct spi_message msg;

struct spi_device *spi = (struct spi_device *)dev->private_data;

/* 分配内存 */
t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
if(!t)
{
printk("kzalloc error\n");
kfree(t);
return -ENOMEM;;
}

rx_data = kzalloc(sizeof(uint8_t) + len, GFP_KERNEL);
if(!rx_data)
{
printk("kzalloc error\n");
kfree(rx_data);
return -ENOMEM;;
}

/* 发送要读取的寄存地址和接收读取的数据 */
tx_data[0] = reg | (0x80); /* 读数据的时候首寄存器地址bit8 要置1 */

t->tx_buf = tx_data; /* 要发送的数据:寄存器地址 */
t->rx_buf = rx_data; /* 要接收的数据 */
t->len = len + 1; /* t->len=发送的长度+读取的长度 */

spi_message_init(&msg); /* 初始化spi_message */
spi_message_add_tail(t, &msg); /* 将spi_transfer 添加到spi_message 队列 */
ret = spi_sync(spi, &msg); /* 同步传输 */

// SPI接收数据时,第一个字节是寄存器的地址,跳过一个字节即可。
memcpy(val, rx_data + 1, len);

/* 释放内存 */
kfree(t);
kfree(rx_data);

return ret;
}

/*
* @description : 向 icm20608 设备多个寄存器写入数据
* @param – dev : 要写入的设备结构体
* @param – reg : 要写入的寄存器首地址
* @param – val : 要写入的数据缓冲区
* @param – len : 要写入的数据长度
* @return : 操作结果
*/
static int icm20608_write_reg(struct icm20608_dev *dev, uint8_t reg, uint8_t *buf, uint8_t len)
{
int ret;
uint8_t *tx_data;
struct spi_transfer *t;
struct spi_message msg;

struct spi_device *spi = (struct spi_device *)dev->private_data;

/* 分配内存 */
t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
if(!t)
{
printk("kzalloc error\n");
kfree(t);
return -ENOMEM;;
}

tx_data = kzalloc(sizeof(uint8_t) + len, GFP_KERNEL);
if(!t)
{
printk("kzalloc error\n");
kfree(tx_data);
return -ENOMEM;;
}

/* 发送要读取的寄存地址和写入的数据 */
*tx_data = reg & ~(0x80); /* 写数据的时候寄存器地址bit8要清零 */
memcpy(tx_data + 1, buf, len); /* 把len 个寄存器拷贝到txdata 里 */

t->tx_buf = tx_data; /* 要发送的数据 */
t->len = len + 1; /* t->len=发送的长度+读取的长度 */

spi_message_init(&msg); /* 初始化spi_message */
spi_message_add_tail(t, &msg); /* 将spi_transfer 添加到spi_message 队列 */
ret = spi_sync(spi, &msg); /* 同步传输 */

/* 释放内存 */
kfree(t);
kfree(tx_data);

return ret;
}

open 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
static void icm20608_deinit(void)
{
uint8_t value;

// 电源管理寄存器1 复位icm20608
value = 0x80;
icm20608_write_reg(&icm20608_dev_t, ICM20608_PWR_MGMT_1, &value, 1);
mdelay(50);
// 自动选择最好的时钟源,否则选择内部20MHz晶振
value = 0x01;
icm20608_write_reg(&icm20608_dev_t, ICM20608_PWR_MGMT_1, &value, 1);
mdelay(50);

//ID 寄存器,ICM-20608 的ID为0XAF
icm20608_read_reg(&icm20608_dev_t, ICM20608_WHO_AM_I, &value, 1);
printk("ICM20608 ID = %x\n", value);

// 设置输出速率分频数:0即速率为INTERNAL_SAMPLE_RATE 8kHz
value = 0x00;
icm20608_write_reg(&icm20608_dev_t, ICM20608_SMPLRT_DIV, &value, 1);
// 设置低通滤波器
value = 0x04;
icm20608_write_reg(&icm20608_dev_t, ICM20608_CONFIG, &value, 1);
// 设置陀螺仪量程 ±2000dps
value = 0x18;
icm20608_write_reg(&icm20608_dev_t, ICM20608_GYRO_CONFIG, &value, 1);
// 设置加速度计的量程 ±16g
value = 0x18;
icm20608_write_reg(&icm20608_dev_t, ICM20608_ACCEL_CONFIG, &value, 1);
// 设置加速度计低通滤波
value = 0x04;
icm20608_write_reg(&icm20608_dev_t, ICM20608_ACCEL_CONFIG2, &value, 1);
// 设置陀螺仪低功耗不使能
value = 0x00;
icm20608_write_reg(&icm20608_dev_t, ICM20608_LP_MODE_CFG, &value, 1);
// 设置FIFO不使能
value = 0x00;
icm20608_write_reg(&icm20608_dev_t, ICM20608_FIFO_EN, &value, 1);
// 电源管理寄存器2 全部使能
value = 0x00;
icm20608_write_reg(&icm20608_dev_t, ICM20608_PWR_MGMT_2, &value, 1);
}

static int icm20608_open (struct inode *node, struct file *file)
{
file->private_data = &icm20608_dev_t;

icm20608_deinit();
printk("icm20608_deinit ok\n");

return 0;
}

read 函数

这里需要注意的就是接收到的数据是 8 位的,在合并数据时,数据移位需要提前转换成 uint16_t 类型,否则会丢失数据,最后将合并成的数据存入 kbuf,使用 copy_to_user 将数据传送给应用程序。

这里接收的数据是 ADC 模拟值不是实际值,需要根据寄存器设置的分辨率进行转换,因为涉及到浮点运算,就没有在驱动文件里面写,后面再应用程序进行运算即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static int icm20608_read_data(struct icm20608_dev *dev)
{
int ret;
uint8_t data[14] = {0};

ret = icm20608_read_reg(dev, ACCEL_XOUT_H, data, 14);
if(ret < 0)
{
printk("icm20608_read_data failed\n");
return -1;
}

dev->data.accel_x = (((int16_t)data[0] << 8) | data[1]);
dev->data.accel_y = (((int16_t)data[2] << 8) | data[3]);
dev->data.accel_z = (((int16_t)data[4] << 8) | data[5]);

dev->data.temperature = (((int16_t)data[6] << 8) | data[7]);

dev->data.gyro_x = (((int16_t)data[8] << 8) | data[9]);
dev->data.gyro_y = (((int16_t)data[10] << 8) | data[11]);
dev->data.gyro_z = (((int16_t)data[12] << 8) | data[13]);

return 0;
}

static ssize_t icm20608_read (struct file *file, char __user *ubuf, size_t size, loff_t *loff_t)
{
int ret;
int16_t kbuf[7];

struct icm20608_dev *dev = (struct icm20608_dev *)file->private_data;

printk("icm20608_read start\n");
ret = icm20608_read_data(dev);
if(ret < 0)
{
printk("icm20608_read error\n");
return -1;
}

kbuf[0] = dev->data.accel_x;
kbuf[1] = dev->data.accel_y;
kbuf[2] = dev->data.accel_z;
kbuf[3] = dev->data.gyro_x;
kbuf[4] = dev->data.gyro_y;
kbuf[5] = dev->data.gyro_z;
kbuf[6] = dev->data.temperature;

ret = copy_to_user(ubuf, kbuf, sizeof(kbuf));
if(ret < 0)
{
printk("copy_to_user error\n");
return -1;
}
printk("icm20608_read ok\n");

return 0;
}

应用程序测试

ADC 模拟值转换为实际值取决于陀螺仪和加速度计的分辨率。

由于寄存器配置时,陀螺仪和加速度计的测量量程范围不同,分辨率就不同,数据手册里面不同测量量程对应不同的分辨率如下:

image-20240701220844905

image-20240701220749049

image-20240701220901623

image-20240701220800594

ADC 模拟值转换实际值转换过程如下:16 位 ADC

实际值=ADC模拟值215测量量程的最大值实际值 = \frac{ADC模拟值}{2^{15}}测量量程的最大值

本质上读取的值在 ADC 模拟值的测量范围的位置映射到具体测量量程的位置。(ADC 模拟值没有含义需要转换成有意义的值)

FS_SEL = 3时,测量量程为 ±2000dps时,陀螺仪转换如下:

实际值=ADC模拟值2152000=ADC模拟值16.4实际值 = \frac{ADC模拟值}{2^{15}}2000=\frac{ADC模拟值}{16.4}

AFS_SEL = 3时,测量量程为 ±16g 时,陀螺仪转换如下:

实际值=ADC模拟值21516=ADC模拟值2048实际值 = \frac{ADC模拟值}{2^{15}}16=\frac{ADC模拟值}{2048}

温度转换成实际值:RoomTemp_Offset 是在 25°C 时的 ADC值偏移量,而 Temp_Sensitivity 是温度传感器的灵敏度,表示每单位温度变化对应的 ADC 计数变化。

image-20240702000840441

因此计算方式为:

temperatureact=temperatureadc25326.8+25temperature_{act}=\frac{temperature_{adc}−25}{326.8}+25

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
int fd, ret;
int16_t buf[7];
int16_t accel_x_adc, accel_y_adc, accel_z_adc,
gyro_x_adc, gyro_y_adc, gyro_z_adc,
temperature_adc;
float accel_x_act, accel_y_act, accel_z_act,
gyro_x_act, gyro_y_act, gyro_z_act,
temperature_act;

fd = open("/dev/icm20608", O_RDWR);
if(fd < 0)
{
printf("open error\n");
return fd;
}

while(1)
{
ret = read(fd, buf, sizeof(buf));
if(ret == 0)
{
accel_x_adc= buf[0];
accel_y_adc = buf[1];
accel_z_adc = buf[2];
gyro_x_adc = buf[3];
gyro_y_adc = buf[4];
gyro_z_adc = buf[5];
temperature_adc = buf[6];

accel_x_act = (float)(accel_x_adc) / 2048;
accel_y_act = (float)(accel_y_adc) / 2048;
accel_z_act = (float)(accel_z_adc) / 2048;
gyro_x_act = (float)(gyro_x_adc) / 16.4;
gyro_y_act = (float)(gyro_y_adc) / 16.4;
gyro_z_act = (float)(gyro_z_adc) / 16.4;
temperature_act = (float)(temperature_adc - 25) / 326.8 + 25;

printf("\r\n 原始值:\r\n");
printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc, gyro_y_adc, gyro_z_adc);
printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc, accel_y_adc, accel_z_adc);
printf("temp = %d\r\n", temperature_adc);

printf("实际值:");
printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);
printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", accel_x_act, accel_y_act, accel_z_act);
printf("act temp = %.2f°C\r\n", temperature_act);

}
usleep(100000); /*100ms */

}

close(fd);
return 0;
}