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

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

一、ICM20608

1. 简介


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

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

2. 功能使用

3. Alpha开发板原理图


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使用:

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

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

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

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

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

三、编写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成员:

// 获取设备树中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成员:

包含头文件:

#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 成员:

在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寄存器读写

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 寄存器,进行软复位,然后选择时钟:

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以检测是否通信正常:

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:

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

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

编写数据读取函数:

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

猜你喜欢

转载自blog.csdn.net/Mculover666/article/details/124100085