本系列文章驱动源码仓库,欢迎Star~
​https://github.com/Mculover666/linux_driver_study​​。


一、ICM20608

1. 简介

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_#include

InvenSense 的 ICM-20608 是一款 6 轴运动跟踪器件(MEMS传感器),也是 MPU-6500 的后续产品,集成了3轴加速度计和3轴陀螺仪。

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_imx6ull_02

相比以前的 6 轴器件,Invensense 的 ICM 20608 具有更低的功耗和噪声并采用更薄的封装。 该器件为陀螺仪提供了一种占空比工作模式,相比以前的 6 轴器件能将陀螺仪的功耗降低一半或一半以上(具体视 ODR 而定)。 此外,该器件的噪声比以前的器件降低约 20%,封装薄约 17%。

2. 功能使用

3. Alpha开发板原理图

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_引脚_03

ICM-20608传感器使用SPI3,引脚连接情况如下:

传感器引脚

iMX6ULL引脚

CS

ECSPI3_SS0

SCLK

ECSPI3_SCLK

SDI

ECSPI3_MOSI

SDO

ECSPI3_MISO

二、添加设备树节点

1. 设置SPI3引脚

首先设置 SPI3 引脚的复用功能和电气属性,在 iomuxc 节点中添加:

pinctrl_spi3: spi3grp {
fsl,pins = <
MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20 0x10b0
MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x10b1
MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 0x10b1
MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x10b1
>;
};

其中UART2_TX_DATA这个引脚设置为普通IO是为了手动控制SPI片选信号。

检查这四个引脚有没有复用,以 MX6UL_PAD_UART2_TX_DATA 引脚为例,已经被uart2使用:

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_linux_04

所以这里我们要找到 ​​pinctrl_uart2​​ 的引用,将uart2节点先屏蔽:

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_#include_05

同样的方法,解决其它引脚 MX6UL_PAD_UART2_RTS_B 和 MX6UL_PAD_UART2_CTS_B 冲突问题。

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_linux_06

这两个引脚作为FLEXCAN2的引脚使用,将该节点先注释了:

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_icm20608_07

2. 添加ICM-20608设备节点

添加对ecspi3节点的补充描述:

&ecspi3 {
fsl,spi-num-chipselects = <1>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_spi3>;
status = "okay";

mems_spi: icm20608@0 {
compatible = "atk,icm20608";
spi-max-frequency = <8000000>;
reg = <0>;
cs-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>;
};
};

重新编译设备树,使用新的设备树启动,查看设备树是否有新添加的节点:

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_linux_08

三、编写ICM-20608设备驱动

1. 先写个模块

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>

static int __init icm20608_module_init(void)
{
return 0;
}

static void __exit icm20608_module_exit(void)
{

}

module_init(icm20608_module_init)
module_exit(icm20608_module_exit)

MODULE_AUTHOR("Mculover666");
MODULE_LICENSE("GPL");

写个Makefile编译一下:

KERNEL_DIR = /home/mculover666/imx6ull/kernel/linux-imx6ull
obj-m = icm20608.o

build: kernel_module

kernel_module:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

2. 再搭个spi驱动框架

包含头文件:

#include <linux/spi/spi.h>

完成 spi 设备驱动基本框架:

static int icm20608_probe(struct spi_device *spi)
{
return 0;
}

static int icm20608_remove(struct spi_device *spi)
{
return 0;
}

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

/* 传统id方式匹配 */
static const struct spi_device_id icm20608_id[] = {
{ "atk,icm20608", 0 },
{ },
};

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

static int __init icm20608_module_init(void)
{
int ret;

/* 注册spi_driver */
ret = spi_register_driver(&icm20608_driver);
if (ret < 0) {
printk("spi_register_driver fail!\n");
return -1;
}

return 0;
}

static void __exit icm20608_module_exit(void)
{
/* 注销spi_driver */
spi_unregister_driver(&icm20608_driver);
}

3. 再写字符设备驱动框架

引入头文件:

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

添加字符设备驱动框架相关代码:

struct icm20608_dev {
dev_t dev;
struct cdev *cdev;
struct class *class;
struct device *device;
};

static struct icm20608_dev icm20608;

static int icm20608_open(struct inode *node, struct file *fp)
{
return 0;
}

static int icm20608_read(struct file *fp, char __user *buf, size_t size, loff_t *off)
{
return 0;
}

static int icm20608_write(struct file *fp, const char __user *buf, size_t size, loff_t *off)
{
return 0;
}

static int icm20608_release(struct inode *node, struct file *fp)
{
return 0;
}

static struct file_operations icm20608_fops = {
.owner = THIS_MODULE,
.open = icm20608_open,
.read = icm20608_read,
.write = icm20608_write,
.release = icm20608_release,
};

static int icm20608_probe(struct spi_device *spi)
{
int ret;

// 申请设备号
ret = alloc_chrdev_region(&icm20608.dev, 0, 1, "icm20608");
if (ret != 0) {
printk("alloc_chrdev_region fail!");
return -1;
}

// 创建cdev
icm20608.cdev = cdev_alloc();
if (!icm20608.cdev) {
printk("cdev_alloc fail!");
return -1;
}
icm20608.cdev->owner = THIS_MODULE;
icm20608.cdev->ops = &icm20608_fops;

// 注册cdev
cdev_add(icm20608.cdev, icm20608.dev, 1);

// 创建设备类
icm20608.class = class_create(THIS_MODULE, "icm20608");
if (!icm20608.class) {
printk("class_create fail!");
return -1;
}

// 创建设备节点
icm20608.device = device_create(icm20608.class, NULL, icm20608.dev, NULL, "icm20608");
if (IS_ERR(icm20608.device)) {
printk("device_create led_dts_device0 fail!");
return -1;
}

return 0;
}

static int icm20608_remove(struct spi_device *spi)
{
// 将设备从内核删除
cdev_del(icm20608.cdev);

// 释放设备号
unregister_chrdev_region(icm20608.dev, 1);

// 删除设备节点
device_destroy(icm20608.class, icm20608.dev);

// 删除设备类
class_destroy(icm20608.class);

return 0;
}

4. 获取设备树信息

因为本文中我们使用gpio作为片选引脚,手动控制,所以要从设备树中获取gpio引脚信息和spi的一些设置信息,获取信息的代码放到probe函数中。

(1)获取spi节点信息

全局变量中添加node成员:

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_#include_09

// 获取设备树中spi节点信息
icm20608.node = of_find_compatible_node(NULL, NULL, "atk,icm20608");
if (!icm20608.node) {
printk("icm20608 node find fail!");
return -1;
}

(2)进一步获取片选引脚gpio信息

全局变量中添加cs_gpio成员:

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_imx6ull_10

包含头文件:

#include <linux/of_gpio.h>
#include <linux/gpio.h>

获取gpio引脚信息:

// 进一步获取gpio片选引脚信息
icm20608.cs_gpio = of_get_named_gpio(icm20608.node, "cs-gpio", 0);
if (icm20608.cs_gpio < 0) {
printk("cs-gpio propname in icm20608 node find fail!");
return -1;
}

// 设置gpio引脚方向并默认输出高电平
ret = gpio_direction_output(icm20608.cs_gpio, 1);
if (ret < 0) {
printk("gpio_direction_output fail!");
return -1;
}

5. 封装spi操作代码

(1)spi_device成员

模块全局数据中,添加 spi_device 成员:

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_引脚_11

在probe函数中,将probe函数传入的spi_device值,存到模块全局数据的spi_devices成员中:

// 存储spi_device值
spi->mode = SPI_MODE_0;
spi_setup(spi);
icm20608.spi = spi;

(2)spi发送然后读取函数封装

static int icm20608_send_then_recv(struct icm20608_dev *dev, uint8_t *send_buf, ssize_t send_len, uint8_t *recv_buf, ssize_t recv_len)
{
int ret;
struct spi_device *spi;
struct spi_message m;
struct spi_transfer *t;

if (!dev) {
return -1;
}

spi = dev->spi;
t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
if (!t) {
printk("spi_transfer kzalloc fail!\n");
return -1;
}

/* 使能片选 */
gpio_set_value(dev->cs_gpio, 0);

/* 发送数据 */
if (send_buf && send_len != 0) {
t->tx_buf = send_buf;
t->len = send_len;
spi_message_init(&m);
spi_message_add_tail(t, &m);
ret = spi_sync(spi, &m);
if (ret < 0) {
printk("spi_sync fail!\n");
goto exit;
}
}

/* 接收数据 */
if (recv_buf && recv_len != 0) {
t->rx_buf = recv_buf;
t->len = send_len;
spi_message_init(&m);
spi_message_add_tail(t, &m);
ret = spi_sync(spi, &m);
if (ret < 0) {
printk("spi_sync fail!\n");
goto exit;
}
}

ret = 0;

/* 禁止片选 */
exit:
gpio_set_value(dev->cs_gpio, 1);
return ret;
}

(3)icm20608寄存器读写

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_引脚_12

icm20608写寄存器函数:

static int icm20608_write_reg(struct icm20608_dev *dev, uint8_t reg, uint8_t dat)
{
int ret;
uint8_t send_buf[2];

send_buf[0] = reg & (~0x80); // MSB is W(0)
send_buf[1] = dat;
ret = icm20608_send_then_recv(dev, send_buf, 2, NULL, 0);

return ret < 0 ? -1 : 0;
}

icm20608读寄存器函数:

static int icm20608_read_reg(struct icm20608_dev *dev, uint8_t reg, uint8_t *dat)
{
int ret;
uint8_t send_buf;

send_buf = reg | 0x80; // MSB is R(1)
ret = icm20608_send_then_recv(dev, &send_buf, 1, dat, 1);

return ret < 0 ? -1 : 0;
}

6. icm20608板级操作函数

6.1. icm20608初始化函数

需要使用mdelay函数,引入头文件:

#include <linux/delay.h>

操作 PWOER MANAGEMENT 1 寄存器,进行软复位,然后选择时钟:

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_linux_13

static int icm20608_board_soft_reset(void)
{
// reset the internal registers and restore the default settings.
icm20608_write_reg(&icm20608, 0x6B, 0x80);
mdelay(50);

// auto select the best available clock source.
icm20608_write_reg(&icm20608, 0x6B, 0x01);
mdelay(50);

return 0;
}

读取芯片ID以检测是否通信正常:

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_引脚_14

static int icm20608_board_read_id(uint8_t *id)
{
int ret;

ret = icm20608_read_reg(&icm20608, 0x75, id);

return ret < 0 ? -1 : 0;
}

设置芯片内部一些配置:

static int icm20608_board_config(void)
{
icm20608_write_reg(&icm20608, 0x19, 0x00); // SMPLRT_DIV
icm20608_write_reg(&icm20608, 0x1A, 0x04); // CONFIG
icm20608_write_reg(&icm20608, 0x1B, 0x18); // GYRO_CONFIG
icm20608_write_reg(&icm20608, 0x1C, 0x18); // ACCEL_CONFIG
icm20608_write_reg(&icm20608, 0x1D, 0x04); // ACCEL_CONFIG2
icm20608_write_reg(&icm20608, 0x1E, 0x00); // LP_MODE_CFG
icm20608_write_reg(&icm20608, 0x23, 0x00); // FIFO_EN
icm20608_write_reg(&icm20608, 0x6C, 0x00); // PWR_MGMT_2

return 0;
}

整合为Icm20608初始化函数:

static int icm20608_board_init(void)
{
uint8_t id;

if (icm20608_board_soft_reset() < 0) {
printk("icm20608_board_soft_reset fail\n!");
return -1;
}

if (icm20608_board_read_id(&id) < 0) {
printk("icm20608_board_read_id fail\n!");
return -1;
}

if (icm20608_board_config() < 0) {
printk("icm20608_board_config fail\n!");
return -1;
}

printk("icm20608 id: 0x%x\n", id);

return 0;
}

编译,加载,检查是否可以正常读取到芯片id:

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_#include_15

6.2. icm20608读取数据函数

首先抽象出icm20608数据结构:

struct icm20608_row_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;
};

添加到模块的全局变量中:

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_引脚_16

编写数据读取函数:

static int icm20608_board_read_row_data(struct icm20608_dev *dev)
{
int i;
int ret;
uint8_t reg;
uint8_t data[14];

if (!dev) {
return -1;
}

reg = 0x3B;
for (i = 0; i < 14; i++) {
ret = icm20608_read_reg(dev, reg++, &data[i]);
if (ret < 0) {
break;
}
}

if (i < 14) {
printk("icm20608_board_read_row_data fail, i = %d!", i);
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.gyro_x = (int16_t)((data[8] << 8) | data[9]);
dev->data.gyro_x = (int16_t)((data[10] << 8) | data[11]);
dev->data.gyro_x = (int16_t)((data[12] << 8) | data[13]);
dev->data.temperature = (int16_t)((data[6] << 8) | data[7]);

return 0;
}

7.icm20608驱动实现

open

static int icm20608_open(struct inode *node, struct file *fp)
{
fp->private_data = &icm20608;
return 0;
}

read

static int icm20608_read(struct file *fp, char __user *buf, size_t size, loff_t *off)
{
int ret;
long err = 0;
struct icm20608_dev *dev = (struct icm20608_dev *)fp->private_data;

ret = icm20608_board_read_row_data(dev);
if (ret < 0) {
return -1;
}

err = copy_to_user(buf, &dev->data, sizeof(struct icm20608_row_data));

return 0;
}

write

static int icm20608_write(struct file *fp, const char __user *buf, size_t size, loff_t *off)
{
return 0;
}

close

static int icm20608_release(struct inode *node, struct file *fp)
{
return 0;
}

四、编写驱动测试函数

1. 测试代码

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>

struct icm20608_row_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_row_data data;

int icm20608_read_data(char *filename)
{
int fd;
int ret;
float gyro_x_act, gyro_y_act, gyro_z_act;
float accel_x_act, accel_y_act, accel_z_act;
float temp_act;

if (!filename) {
return -1;
}

// 打开设备文件
fd = open(filename, O_RDWR);
if (fd < 0) {
printf("open %s error!\n", filename);
return 0;
}

// 读文件
ret = read(fd, &data, sizeof(data));
if (ret != 0) {
printf("read fail!\n");
close(fd);
return -1;
} else {
printf("accel x:%d, y:%d, z:%d\n", data.accel_x, data.accel_y, data.accel_z);
printf("gyro x:%d, y:%d, z:%d\n", data.gyro_x, data.gyro_y, data.gyro_z);
printf("temperature: %d\n", data.temperature);
}

// 计算实际值
gyro_x_act = (float)(data.gyro_x) / 16.4;
gyro_y_act = (float)(data.gyro_y) / 16.4;
gyro_z_act = (float)(data.gyro_z) / 16.4;
accel_x_act = (float)(data.accel_x) / 2048;
accel_y_act = (float)(data.accel_y) / 2048;
accel_z_act = (float)(data.accel_z) / 2048;
temp_act = ((float)(data.temperature) - 25 ) / 326.8 + 25;

printf("act accel x:%.2f, y:%.2f, z:%.2f\n", accel_x_act, accel_y_act, accel_z_act);
printf("act gyro x:%.2f, y:%.2f, z:%.2f\n", gyro_x_act, gyro_y_act, gyro_z_act);
printf("act temperature: %.2f\n", temp_act);

// 关闭文件
close(fd);

return 0;
}

int main(int argc, char *argv[])
{
uint32_t interval;

// 检查参数
if (argc != 3) {
printf("usage: ./test_icm20608 [device] [read interval(s)]\n");
return -1;
}

interval = atoi(argv[2]);
if (interval < 1) {
interval = 1;
}

while (1) {
icm20608_read_data(argv[1]);
sleep(interval);
}
}

2. 测试结果

编译:

arm-linux-gnueabihf-gcc test_icm20608.c -o test_icm20608

i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器_引脚_17