Linux字符设备驱动框架之SPI总线驱动框架

一、简介

SPI 是 Motorola 公司推出的一种同步串行接口技术,是一种高速、全双工的同步通信总线, SPI 时钟频率相比 I2C 要高很多,最高可以工作在上百 MHz。 SPI 以主从方式工作,通常是有一个主设备和一个或多个从设备,一般 SPI 需要4 根线,但是也可以使用三根线(单向传输),本章我们讲解标准的 4 线 SPI,这四根线如下:
①、 CS/SS, Slave Select/Chip Select,这个是片选信号线,用于选择需要进行通信的从设备。
I2C 主机是通过发送从机设备地址来选择需要进行通信的从机设备的, SPI 主机不需要发送从机
设备,直接将相应的从机设备片选信号拉低即可。
②、 SCK, Serial Clock,串行时钟,和 I2C 的 SCL 一样,为 SPI 通信提供时钟。
③、 MOSI/SDO, Master Out Slave In/Serial Data Output,简称主出从入信号线,这根数据线
只能用于主机向从机发送数据,也就是主机输出,从机输入。
④、 MISO/SDI, Master In Slave Out/Serial Data Input,简称主入从出信号线,这根数据线只
能用户从机向主机发送数据,也就是主机输入,从机输出。

SPI 有四种工作模式,通过串行时钟极性(CPOL)和相位(CPHA)的搭配来得到四种工作模式:
①、 CPOL=0,串行时钟空闲状态为低电平。
②、 CPOL=1,串行时钟空闲状态为高电平,此时可以通过配置时钟相位(CPHA)来选择具
体的传输协议。
③、 CPHA=0,串行时钟的第一个跳变沿(上升沿或下降沿)采集数据。
④、 CPHA=1,串行时钟的第二个跳变沿(上升沿或下降沿)采集数据。

二、SPI驱动框架

同 I2C 一样,Linux SPI 驱动架构也分为3个组成部分:
1)SPI 核心

SPI 核心和 I2C 一样, 其主要做的工作是用于完成SPI 设备和驱动的匹配。

2)SPI 总线驱动(这一部分一般由SOC厂商编写好)

SPI 主机驱动就是 SOC 的 SPI 控制器驱动,类似 I2C 驱动里面的适配器驱动。

3)SPI 设备驱动

SPI 设备驱动, SPI 设备驱动就是针对具体的 SPI 设备而编写的驱动。

1、SPI核心

Linux内核初始化阶段,调用spi_init() 函数来初始化IIC总线,内容如下:

static int __init spi_init(void)
{
	int	status;

	... ...

	status = bus_register(&spi_bus_type);
	if (status < 0)
		goto err1;

	... ...
	
	return 0;
	... ...
}

在 spi_init() 函数中调用 bus_register() 来注册 spi 总线,SPI 设备和驱动的匹配过程是由 SPI 总线来完成的,这点和 platform、 I2C 等驱动一样,注册的 SPI 总线为 spi_bus_type,定义在 drivers/spi/spi.c 文件中,内容如下:

struct bus_type spi_bus_type = {
	.name		= "spi",
	.dev_groups	= spi_dev_groups,
	.match		= spi_match_device,
	.uevent		= spi_uevent,
};

可以看出,SPI 设备和驱动的匹配函数为 spi_match_device,函数内容如下:

static int spi_match_device(struct device *dev, struct device_driver *drv)
{
	const struct spi_device	*spi = to_spi_device(dev);
	const struct spi_driver	*sdrv = to_spi_driver(drv);

	/* 
	 * 第一种
	 * 用于完成设备树设备和驱动匹配。比较 SPI 设备节点的 compatible 属性和 of_device_id 中的 		
	 * compatible 属性是否相等,如果相当的话就表示 SPI 设备和驱动匹配
	 */
	/* Attempt an OF style match */
	if (of_driver_match_device(dev, drv))
		return 1;

	/*
	 * 第二种
	 * 用于 ACPI 形式的匹配	
	 */
	/* Then try ACPI */
	if (acpi_driver_match_device(dev, drv))
		return 1;

	/*
	 * 第三种
	 * 用于传统的、无设备树的 SPI 设备和驱动匹配过程。比较 SPI
	 * 设备名字和 spi_device_id 的 name 字段是否相等,相等的话就说明 SPI 设备和驱动匹配
	 */
	if (sdrv->id_table)
		return !!spi_match_id(sdrv->id_table, spi);

	/*
	 * 比较 spi_device 中 modalias 成员变量和 device_driver 中的 name 成员变量是否相等。
	 */
	return strcmp(spi->modalias, drv->name) == 0;
}

2、SPI 总线驱动(主机驱动)

1)主机驱动

SPI 主机驱动就是 SOC 的 SPI 控制器驱动,类似 I2C 驱动里面的适配器驱动。 Linux 内核
使用 spi_master 表示 SPI 主机驱动, spi_master 是个结构体,定义在 include/linux/spi/spi.h 文件
中,内容如下(有缩减):

struct spi_master {
	struct device	dev;

	... ...
	
	/*
	 * 和 i2c_algorithm 中的 master_xfer 函数一样,控制器数据传输函数。 
	 */
	int			(*transfer)(struct spi_device *spi,
						struct spi_message *mesg);

	... ...

	/*
	 *	也用于 SPI 数据发送,用于发送一个 spi_message,SPI 的数据会打包成 spi_message,然后以队列方式
	 *	发送出去。
	 */
	int (*transfer_one_message)(struct spi_master *master,
				    struct spi_message *mesg);
	
	... ...
};

就是 SPI 主机端最终会通过 transfer 函数与 SPI 设备进行通信,因此对于 SPI 主机控制器的驱
动编写者而言 transfer 函数是需要实现的,因为不同的 SOC 其 SPI 控制器不同,寄存器都不一
样。和 I2C 适配器驱动一样, SPI 主机驱动一般都是 SOC 厂商去编写的,所以我们作为 SOC 的
使用者,这一部分的驱动就不用操心了,除非你是在 SOC 原厂工作,内容就是写 SPI 主机驱
动。

SPI 主机驱动的核心就是申请 spi_master,然后初始化 spi_master,最后向 Linux 内核注册
spi_master。

1、 spi_master 申请与释放

spi_alloc_master 函数用于申请 spi_master,函数原型如下:
struct spi_master *spi_alloc_master(struct device *dev, unsigned size)
参数:
	dev:设备,一般是 platform_device 中的 dev 成员变量。
	size: 私有数据大小,可以通过 spi_master_get_devdata 函数获取到这些私有数据。
返回值: 
	申请到的 spi_master。

spi_master 的释放通过 spi_master_put 函数来完成,当我们删除一个 SPI 主机驱动的时候就
需要释放掉前面申请的 spi_master, spi_master_put 函数原型如下:

void spi_master_put(struct spi_master *master)
参数:
	master:要释放的 spi_master。
返回值: 无

2、 spi_master 的注册与注销

当 spi_master 初始化完成以后就需要将其注册到 Linux 内核, spi_master 注册函数为
spi_register_master,函数原型如下:
int spi_register_master(struct spi_master *master)
参数:
	master:要注册的 spi_master。
返回值: 
	0,成功;负值,失败。

I.MX6U 的 SPI 主机驱动会采用 spi_bitbang_start 这个 API 函数来完成 spi_master 的注册,
spi_bitbang_start 函数内部其实也是通过调用 spi_register_master 函数来完成 spi_master 的注册。

如果要注销 spi_master 的话可以使用 spi_unregister_master 函数,此函数原型为:

void spi_unregister_master(struct spi_master *master)
参数:
	master:要注销的 spi_master。
返回值: 无。

如果使用 spi_bitbang_start 注册 spi_master 的话就要使用 spi_bitbang_stop 来注销掉
spi_master。

2) I.MX6U SPI 主机驱动分析

和 I2C 的适配器驱动一样, SPI 主机驱动一般都由 SOC 厂商编写好了,打开 imx6ull.dtsi
文件,找到如下所示内容:

ecspi3: ecspi@02010000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
	reg = <0x02010000 0x4000>;
	interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_ECSPI3>,
			 <&clks IMX6UL_CLK_ECSPI3>;
	clock-names = "ipg", "per";
	dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;
	dma-names = "rx", "tx";
	status = "disabled";
};

compatible 属性有两个值“fsl,imx6ul-ecspi”“fsl,imx51-ecspi”,在 Linux 内核源码中搜素这两个属性值即可找到 I.MX6U 对应的 ECSPI(SPI) 主机驱动。 I.MX6U 的 ECSPI 主机驱动文件为 drivers/spi/spi-imx.c,在此文件中找到如下内容:

/*
 * 无设备树匹配表
 */
static struct platform_device_id spi_imx_devtype[] = {
	{
		.name = "imx1-cspi",
		.driver_data = (kernel_ulong_t) &imx1_cspi_devtype_data,
	}, {
		.name = "imx21-cspi",
		.driver_data = (kernel_ulong_t) &imx21_cspi_devtype_data,
	}, {
		.name = "imx27-cspi",
		.driver_data = (kernel_ulong_t) &imx27_cspi_devtype_data,
	}, {
		.name = "imx31-cspi",
		.driver_data = (kernel_ulong_t) &imx31_cspi_devtype_data,
	}, {
		.name = "imx35-cspi",
		.driver_data = (kernel_ulong_t) &imx35_cspi_devtype_data,
	}, {
		.name = "imx51-ecspi",
		.driver_data = (kernel_ulong_t) &imx51_ecspi_devtype_data,
	}, {
		.name = "imx6ul-ecspi",
		.driver_data = (kernel_ulong_t) &imx6ul_ecspi_devtype_data,
	}, {
		/* sentinel */
	}
};

/*
 * 设备树匹配表
 */
static const struct of_device_id spi_imx_dt_ids[] = {
	{ .compatible = "fsl,imx1-cspi", .data = &imx1_cspi_devtype_data, },
	{ .compatible = "fsl,imx21-cspi", .data = &imx21_cspi_devtype_data, },
	{ .compatible = "fsl,imx27-cspi", .data = &imx27_cspi_devtype_data, },
	{ .compatible = "fsl,imx31-cspi", .data = &imx31_cspi_devtype_data, },
	{ .compatible = "fsl,imx35-cspi", .data = &imx35_cspi_devtype_data, },
	{ .compatible = "fsl,imx51-ecspi", .data = &imx51_ecspi_devtype_data, },
	{ .compatible = "fsl,imx6ul-ecspi", .data = &imx6ul_ecspi_devtype_data, },
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, spi_imx_dt_ids);

... ...

/*
 * platform_driver 驱动框架,和 I2C 的适配器驱动一行, SPI 主机驱动器采用了 platfom 驱动框架。当设备
 * 和驱动匹配成功以后 spi_imx_probe 函数就会执行。
 */
static struct platform_driver spi_imx_driver = {
	.driver = {
		   .name = DRIVER_NAME,
		   .of_match_table = spi_imx_dt_ids,
		   .pm = IMX_SPI_PM,
	},
	.id_table = spi_imx_devtype,
	.probe = spi_imx_probe,
	.remove = spi_imx_remove,
};
module_platform_driver(spi_imx_driver);

MODULE_DESCRIPTION("SPI Master Controller driver");
MODULE_AUTHOR("Sascha Hauer, Pengutronix");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:" DRIVER_NAME);

spi_imx_probe 函数会从设备树中读取相应的节点属性值,申请并初始化 spi_master,最后调用 spi_bitbang_start 函数(spi_bitbang_start 会调用 spi_register_master 函数)向 Linux 内核注册
spi_master。

3、SPI设备驱动

1)SPI设备驱动解析

spi 设备驱动也和 i2c 设备驱动也很类似, Linux 内核使用 spi_driver 结构体来表示 spi 设备驱动,我们在编写 SPI 设备驱动的时候需要实现 spi_driver。 spi_driver 结构体定义在 include/linux/spi/spi.h 文件中,结构体内容如下:

struct spi_driver {
	const struct spi_device_id *id_table;
	int			(*probe)(struct spi_device *spi);
	int			(*remove)(struct spi_device *spi);
	void			(*shutdown)(struct spi_device *spi);
	struct device_driver	driver;
};

可以看出, spi_driver 和 i2c_driver、 platform_driver 基本一样,当 SPI 设备和驱动匹配成功以后 probe 函数就会执行。

同样的, spi_driver 初始化完成以后需要向 Linux 内核注册, spi_driver 注册函数为spi_register_driver,函数原型如下:

int spi_register_driver(struct spi_driver *sdrv)
参数:
	sdrv: 要注册的 spi_driver。
返回值: 
	0,注册成功;赋值,注册失败。

注销 SPI 设备驱动以后也需要注销掉前面注册的 spi_driver,使用 spi_unregister_driver 函
数完成 spi_driver 的注销,函数原型如下:

void spi_unregister_driver(struct spi_driver *sdrv)
参数:
	sdrv: 要注销的 spi_driver。
返回值: 
	无。

2)SPI 设备驱动编写流程

(1)SPI 设备信息描述

① IO 的 pinctrl 子节点创建与修改
首先肯定是根据所使用的 IO 来创建或修改 pinctrl 子节点,这个没什么好说的,唯独要注意的就是检查相应的 IO 有没有被其他的设备所使用,如果有的话需要将其删除掉!

② SPI 设备节点的创建与修改
采用设备树的情况下, SPI 设备信息描述就通过创建相应的设备子节点来完成,可以打开 imx6qdl-sabresd.dtsi 这个设备树头文件,在此文件里面找到如下所示内容:

&ecspi1 {
	fsl,spi-num-chipselects = <1>;	/* 表示只有1个设备 */
	cs-gpios = <&gpio4 9 0>;		/* 片选信号为 gpio4_io09 */
	pinctrl-names = "default";		/* spi 所使用到的 IO 名字*/
	pinctrl-0 = <&pinctrl_ecspi1>;  /* 所使用的 IO 对应的 pinctrl 节点 */
	status = "okay";		/* 将 ecspi1 节点的“status”属性改为“okay” */

	flash: m25p80@0 {  /* m25p80 设备, 后面的 "0" 表示接到了 ECSPI1 的通道0 */
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "st,m25p32";	/* 用于匹配设备驱动 */
		spi-max-frequency = <20000000>;	/* SPI设备的最高频率 */
		reg = <0>;	/* 设置 m25p80 这个设备所使用的 ECSPI 通道,和“m25p80@0”后面的
“0”一样 */
	};
};

(2)SPI 设备数据收发处理流程
当我们向 Linux 内核注册成功 spi_driver 以后就可以使用 SPI 核心层提供的 API 函数来对设备进行读写操作了。
首先是 spi_transfer 结构体,此结构体用于描述 SPI 传输信息, 结构体内容如下:

struct spi_transfer {
	/* it's ok if tx_buf == rx_buf (right?)
	 * for MicroWire, one buffer must be null
	 * buffers must work with dma_*map_single() calls, unless
	 *   spi_message.is_dma_mapped reports a pre-existing mapping
	 */
	const void	*tx_buf;  	/* 保存者要发送的数据 */
	void		*rx_buf;	/* 用于保存接收到的数据 */
	unsigned	len;		/* 要进行传输的数据长度
							   SPI 是全双工通信,因此在一次通信中发送和接收的字节数都是一样的,所以 
							   spi_transfer 中也就没有发送长度和接收长度之分。 */

	dma_addr_t	tx_dma;
	dma_addr_t	rx_dma;
	struct sg_table tx_sg;
	struct sg_table rx_sg;

	unsigned	cs_change:1;
	unsigned	tx_nbits:3;
	unsigned	rx_nbits:3;
#define	SPI_NBITS_SINGLE	0x01 /* 1bit transfer */
#define	SPI_NBITS_DUAL		0x02 /* 2bits transfer */
#define	SPI_NBITS_QUAD		0x04 /* 4bits transfer */
	u8		bits_per_word;
	u16		delay_usecs;
	u32		speed_hz;

	struct list_head transfer_list;
};

spi_transfer 需要组织成 spi_message, spi_message 也是一个结构体,内容如下:

struct spi_message {
	struct list_head	transfers;

	struct spi_device	*spi;

	unsigned		is_dma_mapped:1;

	/* completion is reported through a callback */
	void			(*complete)(void *context);
	void			*context;
	unsigned		frame_length;
	unsigned		actual_length;
	int			status;

	/* for optional use by whatever driver currently owns the
	 * spi_message ...  between calls to spi_async and then later
	 * complete(), that's the spi_master controller driver.
	 */
	struct list_head	queue;
	void			*state;
};

在使用spi_message之前需要对其进行初始化, spi_message初始化函数为spi_message_init,
函数原型如下:

void spi_message_init(struct spi_message *m)
参数:
	m: 要初始化的 spi_message。
返回值: 
	无。

spi_message 初始化完成以后需要将 spi_transfer 添加到 spi_message 队列中,这里我们要用
到 spi_message_add_tail 函数,此函数原型如下:

void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
参数:
	t: 要添加到队列中的 spi_transfer。
	m: spi_transfer 要加入的 spi_message。
返回值: 
	无。

spi_message 准备好以后既可以进行数据传输了,数据传输分为同步传输和异步传输,同步
传输会阻塞的等待 SPI 数据传输完成,同步传输函数为 spi_sync,函数原型如下:

int spi_sync(struct spi_device *spi, struct spi_message *message)
参数:
	spi: 要进行数据传输的 spi_device。
	message:要传输的 spi_message。
返回值: 
	无。

异步传输不会阻塞的等到 SPI 数据传输完成,异步传输需要设置 spi_message 中的 complete
成员变量, complete 是一个回调函数,当 SPI 异步传输完成以后此函数就会被调用。 SPI 异步传
输函数为 spi_async,函数原型如下:

int spi_async(struct spi_device *spi, struct spi_message *message)
参数:
	spi: 要进行数据传输的 spi_device。
	message:要传输的 spi_message。
返回值: 
	无

综上所述, SPI 数据传输步骤如下:(采用同步传输方式来完成 SPI 数据的传输工作,也就是 spi_sync 函数)
①、申请并初始化 spi_transfer,设置 spi_transfer 的 tx_buf 成员变量, tx_buf 为要发送的数据。然 后设置 rx_buf 成员变量, rx_buf 保存着接收到的数据。最后设置 len 成员变量,也就是要进行数据通信的长度。
②、使用 spi_message_init 函数初始化 spi_message。
③、使用spi_message_add_tail函数将前面设置好的spi_transfer添加到spi_message队列中。
④、使用 spi_sync 函数完成 SPI 数据同步传输

3)实验程序编写

具体设备:icm2060

1、添加设备节点

imx6ull-alientek-emmc.dts文件中添加设备节点信息,如下:

1)SPI 设备所使用的 IO 引脚:

pinctrl_ecspi3: icm20608 {
	fsl,pins = <
		MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20		0x10b0 	/* CS */
		MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 	0x10b1	/* SCLK */
		MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI		0x10b1	/* MOSI */
		MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO		0x10b1	/* MISO */
	>;
};

2)添加 ecspi3 的节点信息

&ecspi3 {
	fsl,spi-num-chipselects = <1>;
	cs-gpio = <&gpio1 20 GPIO_ACTIVE_HIGH>;  /* 自己定义的节点,没有使用 cs-gpios 节点 */
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_ecspi3>;
	status = "okay";

	spidev: icm2060@0 {
		compatible = "alientek,icm2060";
		spi-max-frequency = <8000000>;
		reg = <0>;
	};
};

注意:必须要保证所使用到的引脚 IO 没有被其他模块使用。(我找错误找了很久,最后通过这个才解决了问题)
方法:分别在当前设备树文件下搜索所用到 IO,例:搜索如下:
① MX6UL_PAD_UART2_TX_DATA
② MX6UL_PAD_UART2_RX_DATA
③ MX6UL_PAD_UART2_CTS_B
④ MX6UL_PAD_UART2_RTS_B
⑤ gpio1 20

当发现有被其他模块用到则将其注释掉即可。

2、驱动程序

icm2060.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/delay.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/ide.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/cdev.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/interrupt.h>
#include <linux/device.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/input.h>
#include <linux/i2c.h>
#include <linux/spi/spi.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#include "icm20608reg.h"

#define ICM20608_NAME       "icm20608"
#define ICM20608_CNT        1

/* 设备结构体 */
struct icm20608_dev {
    dev_t devid;
    int major;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct device_node *nd;
    int cs_gpio;        /* 片选信号引脚 */
    void *private_data; /* 私有数据 */

    signed int gyro_x_adc;		/* 陀螺仪X轴原始值 	 */
	signed int gyro_y_adc;		/* 陀螺仪Y轴原始值		*/
	signed int gyro_z_adc;		/* 陀螺仪Z轴原始值 		*/
	signed int accel_x_adc;		/* 加速度计X轴原始值 	*/
	signed int accel_y_adc;		/* 加速度计Y轴原始值	*/
	signed int accel_z_adc;		/* 加速度计Z轴原始值 	*/
	signed int temp_adc;		/* 温度原始值 			*/
};

struct icm20608_dev icm20608dev;

/*
 * @description     : 从icm20608读取多个寄存器数据
 * @param - dev     : icm20608设备
 * @param - reg     : 读取数据的寄存器地址
 * @param - buf     : 读取的数据
 * @param - len     : 读取的字节数
 * @return          : 0:成功, 非0:失败  
 */
static int icm20608_read_regs(struct icm20608_dev *dev, unsigned char reg, unsigned char *buf, int len)
{
    int ret = 0;
    unsigned char txdata[len];
    struct spi_transfer *t;
    struct spi_message m;
    struct spi_device *spi = (struct spi_device *)dev->private_data;

    
    gpio_set_value(dev->cs_gpio, 0);    /* 片选拉低,选中icm20608 */

    t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);  /* 申请内存 */
    if(t == NULL){
        printk("kzalloc spi_transfer error!\r\n");
        return -EINVAL;
    }

    /* 
     * ICM20608在使用SPI接口的时候寄存器地址
	 * 只有低7位有效,寄存器地址最高位是读/写标志位
	 * 读的时候要为1,写的时候要为0。
	 */

    /* 第1次,发送要读取的寄存器地址 */
    txdata[0] = reg | 0x80;         /* bit7置1,表示为读 */
    t->tx_buf = txdata;             /* 发送的数据 */
    t->len = 1;                     /* 1个字节 */
    spi_message_init(&m);           /* 初始化spi_message */
    spi_message_add_tail(t, &m);    /* 将spi_transfer添加到spi_message队列 */
    ret = spi_sync(spi, &m);        /* 同步发送 */
    if(ret < 0){
        printk("spi_sync failed!\r\n");
        kfree(t);
        return -EINVAL;
    }

    /* 第2次,读取数据 */
    txdata[0] = 0xff;               /* 随便填一个数,此处无意义 */
    t->rx_buf = buf;                /* 读取的数据 */
    t->len = len;                   /* len个字节 */
    spi_message_init(&m);           /* 初始化spi_message */
    spi_message_add_tail(t, &m);    /* 将spi_transfer添加到spi_message队列 */
    ret = spi_sync(spi, &m);        /* 同步发送 */
    if(ret < 0){
        printk("spi_sync failed!\r\n");
        kfree(t);
        return -EINVAL;
    }

    kfree(t);
    gpio_set_value(dev->cs_gpio, 1); /* 片选拉高,释放icm20608 */

    return 0;
}

/*
 * @description     : 向icm20608写入多个寄存器数据
 * @param - dev     : icm20608设备
 * @param - reg     : 写入数据的寄存器地址
 * @param - buf     : 写入的数据
 * @param - len     : 写入的字节数
 * @return          : 0:成功, 非0:失败  
 */
static int icm20608_write_regs(struct icm20608_dev *dev, unsigned char reg, unsigned char *buf, int len)
{
    int ret = 0;
    unsigned char txdata[len];
    struct spi_transfer *t;
    struct spi_message m;
    struct spi_device *spi = (struct spi_device *)dev->private_data;

    gpio_set_value(dev->cs_gpio, 0);    /* 片选拉低,选中icm20608 */

    t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
    if(t == NULL){
        printk("kzalloc spi_transfer error!\r\n");
        return -EINVAL;
    }

    /* 第1次,写入寄存器地址 */
    txdata[0] = reg & (~0x80);          /* bit7清零,表示写 */
    t->tx_buf = txdata;                 /* 发送的数据 */
    t->len = 1;                         /* 1个字节 */
    spi_message_init(&m);
    spi_message_add_tail(t, &m);
    ret = spi_sync(spi, &m);
    if(ret < 0){
        printk("spi_sync failed!\r\n");
        kfree(t);
        return -EINVAL;
    }

    t->tx_buf = buf;            
    t->len = len;
    spi_message_init(&m);
    spi_message_add_tail(t, &m);
    ret = spi_sync(spi, &m);
    if(ret < 0){
        printk("spi_sync failed!\r\n");
        kfree(t);
        return -EINVAL;
    }

    kfree(t);
    gpio_set_value(dev->cs_gpio, 1);

    return 0;
}

/* 读取一个指定寄存器的值 */
static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, unsigned char reg)
{
    unsigned char data;
    icm20608_read_regs(dev, reg, &data, 1);
    return data;
}

/* 写一个指定寄存器的值 */
static void icm20608_write_onereg(struct icm20608_dev *dev, unsigned char reg, unsigned char data)
{
    u8 buf = data;
    icm20608_write_regs(dev, reg, &buf, 1);
}

/* 读取icm20608数据 */
void icm20608_readdata(struct icm20608_dev *dev)
{
    unsigned char data[14];
	icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);

	dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]); 
	dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]); 
	dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]); 
	dev->temp_adc    = (signed short)((data[6] << 8) | data[7]); 
	dev->gyro_x_adc  = (signed short)((data[8] << 8) | data[9]); 
	dev->gyro_y_adc  = (signed short)((data[10] << 8) | data[11]);
	dev->gyro_z_adc  = (signed short)((data[12] << 8) | data[13]);
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data似有向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int icm20608_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &icm20608dev; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
	signed int data[7];
	int ret = 0;
	struct icm20608_dev *dev = (struct icm20608_dev *)filp->private_data;

	icm20608_readdata(dev);
	data[0] = dev->gyro_x_adc;
	data[1] = dev->gyro_y_adc;
	data[2] = dev->gyro_z_adc;
	data[3] = dev->accel_x_adc;
	data[4] = dev->accel_y_adc;
	data[5] = dev->accel_z_adc;
	data[6] = dev->temp_adc;
	ret = copy_to_user(buf, data, sizeof(data));
    if(ret < 0){
        printk("kernel read failed!\r\n");
        return -EINVAL;
    }
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int icm20608_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* file_operations操作集 */
struct file_operations icm20608_fops = {
    .owner = THIS_MODULE,
    .open = icm20608_open,
    .read = icm20608_read,
    .release = icm20608_release,
};

/* 初始化icm20608 */
void icm20608_reginit(void)
{
    u8 value = 0;
	
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x80);
	mdelay(50);
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x01);
	mdelay(50);

	value = icm20608_read_onereg(&icm20608dev, ICM20_WHO_AM_I);
	printk("ICM20608 ID = %#X\r\n", value);	

	icm20608_write_onereg(&icm20608dev, ICM20_SMPLRT_DIV, 0x00); 	/* 输出速率是内部采样率					*/
	icm20608_write_onereg(&icm20608dev, ICM20_GYRO_CONFIG, 0x18); 	/* 陀螺仪±2000dps量程 				*/
	icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG, 0x18); 	/* 加速度计±16G量程 					*/
	icm20608_write_onereg(&icm20608dev, ICM20_CONFIG, 0x04); 		/* 陀螺仪低通滤波BW=20Hz 				*/
	icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz 			*/
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_2, 0x00); 	/* 打开加速度计和陀螺仪所有轴 				*/
	icm20608_write_onereg(&icm20608dev, ICM20_LP_MODE_CFG, 0x00); 	/* 关闭低功耗 						*/
	icm20608_write_onereg(&icm20608dev, ICM20_FIFO_EN, 0x00);		/* 关闭FIFO						*/
}

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

    printk("spi driver and device was matched!\r\n");

    /* 注册字符设备 */
    icm20608dev.major = 0;
    if(icm20608dev.major){
        icm20608dev.devid = MKDEV(icm20608dev.major, 0);
        ret = register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);
    } else {
        ret = alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);
    }
    if(ret < 0){
        printk("register chrdev failed!\r\n");
        ret = -EINVAL;
        goto fail_devid;
    }

    icm20608dev.cdev.owner = THIS_MODULE;
    cdev_init(&icm20608dev.cdev, &icm20608_fops);
    ret = cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);
    if(ret < 0){
        printk("cdev add failed!\r\n");
        ret = -EINVAL;
        goto fail_cdev;
    }

    icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
    if(IS_ERR(icm20608dev.class)){
        printk("class create failed!\r\n");
        ret = PTR_ERR(icm20608dev.class);
        goto fail_class;
    }

    icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.devid, NULL, ICM20608_NAME);
    if(IS_ERR(icm20608dev.device)){
        printk("device create failed!\r\n");
        ret = PTR_ERR(icm20608dev.device);
        goto fail_device;
    }

    /* 获取并设置片选IO */
    /*
     * 获取设备节点中的“cs-gpio”属性,也就是获取到设备的片选 IO。
     */
    icm20608dev.nd = of_find_node_by_path("/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02010000");
    if(icm20608dev.nd == NULL){
        printk("find node failed!\r\n");
        ret = -EINVAL;
        goto fail_node;
    }

    icm20608dev.cs_gpio = of_get_named_gpio(icm20608dev.nd, "cs-gpio", 0);
    if(icm20608dev.cs_gpio < 0){
        printk("get gpio failed!\r\n");
        ret = -EINVAL;
        goto fail_gpio;
    }

    ret = gpio_request(icm20608dev.cs_gpio, "cs");
    if(ret < 0){
        printk("request gpio failed!\r\n");
        ret = -ENOSYS;
        goto fail_request_gpio;
    }

    ret = gpio_direction_output(icm20608dev.cs_gpio, 1);
    if(ret < 0){
        printk("gpio output failed!\r\n");
        ret = -EINVAL;
        goto fail_output;
    }

    /* 初始化 spi_device */
    spi->mode = SPI_MODE_0;  /* MODE0, CPOL = 0, CPHA = 0 */
    spi_setup(spi);
    
    /* 
     * 对于 SPI 设备驱动来讲最核心的就是 spi_device。 probe 函数会向驱动提供当前 SPI 设备对应的spi_device,
     * 因此在 probe 函数中设置 private_data 为 probe 函数传递进来的 spi_device 参数。 
     */
    icm20608dev.private_data = spi; 

    /* 初始化icm20608 */
    icm20608_reginit();

    return 0;

fail_output:
    gpio_free(icm20608dev.cs_gpio);
fail_request_gpio:
fail_gpio:
fail_node:
    device_destroy(icm20608dev.class, icm20608dev.devid);
fail_device:
    class_destroy(icm20608dev.class);
fail_class:
    cdev_del(&icm20608dev.cdev);
fail_cdev:
    unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);
fail_devid:
    return ret;
}

static int icm20608_remove(struct spi_device *spi)
{
    gpio_free(icm20608dev.cs_gpio);
    device_destroy(icm20608dev.class, icm20608dev.devid);
    class_destroy(icm20608dev.class);
    cdev_del(&icm20608dev.cdev);
    unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);
    return 0;
}

/* 非设备树匹配表 */
static const struct spi_device_id icm20608_id_table[] = {
    {"alientek,icm20608", 0},
    { },
};

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

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


/* 入口 */
static int __init icm20608_init(void)
{
    return spi_register_driver(&icm20608_driver);
}

/* 出口 */
static void __exit icm20608_exit(void)
{
    spi_unregister_driver(&icm20608_driver);
}

module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YANG");

3、应用程序

icm2060APP.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

int main(int argc, char *argv[])
{
    int fd;
    char *filename;
    signed int databuf[7];
	unsigned char data[14];
	signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
	signed int accel_x_adc, accel_y_adc, accel_z_adc;
	signed int temp_adc;

	float gyro_x_act, gyro_y_act, gyro_z_act;
	float accel_x_act, accel_y_act, accel_z_act;
	float temp_act;
    
    int ret = 0;

    if(argc != 2){
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];

    fd = open(filename, O_RDWR);
    if(fd < 0){
        printf("open file %s failed!\r\n", filename);
        return -1;
    }

    while (1) {
		ret = read(fd, databuf, sizeof(databuf));
		if(ret == 0) { 			/* 数据读取成功 */
			gyro_x_adc = databuf[0];
			gyro_y_adc = databuf[1];
			gyro_z_adc = databuf[2];
			accel_x_adc = databuf[3];
			accel_y_adc = databuf[4];
			accel_z_adc = databuf[5];
			temp_adc = databuf[6];

			/* 计算实际值 */
			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;
			accel_x_act = (float)(accel_x_adc) / 2048;
			accel_y_act = (float)(accel_y_adc) / 2048;
			accel_z_act = (float)(accel_z_adc) / 2048;
			temp_act = ((float)(temp_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", temp_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", temp_act);
		}
		usleep(100000); /*100ms */
	}

    close(fd);

    return 0;
}

编译应用程序时需要注意:

在 icm20608App.c 这个测试 APP 中我们用到了浮点计算,而 I.MX6U 是支持硬件浮点的,
因此我们在编译 icm20608App.c 的时候就可以使能硬件浮点,这样可以加速浮点计算。使能硬
件浮点很简单,在编译的时候加入如下参数即可:

-march-armv7-a -mfpu-neon -mfloat=hard
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值