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

Posted Mculover666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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引脚
CSECSPI3_SS0
SCLKECSPI3_SCLK
SDIECSPI3_MOSI
SDOECSPI3_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;
;

添加到模块的全局变量中:
i.MX6ULL驱动开发 | 08 -基于pinctrl子系统和gpio子系统点亮LED

i.MX6ULL驱动开发 | 09 -基于Linux自带的LED驱动点亮LED

i.MX6ULL驱动开发 | 34 - 基于SPI框架驱动spi lcd(st7789)

i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED

i.MX6ULL驱动开发 | 34 - 基于SPI框架驱动spi lcd(st7789)

i.MX6ULL驱动开发 | 24 - 基于platform平台驱动模型点亮LED