19| Linux SPI驱动实验

Linux SPI驱动实验

上一章我们讲解了如何编写 Linux 下的 I2C 设备驱动,SPI 也是很常用的串行通信协议,本章我们就来学习如何在 Linux 下编写 SPI 设备驱动。本章实验的最终目的就是驱动 I.MX6U-ALPHA 开发板上的 ICM-20608 这个 SPI 接口的六轴传感器,可以在应用程序中读取 ICM-20608的原始传感器数据。

1| Linux 下 下 SPI 驱动框架简介

SPI 驱动框架和 I2C 很类似,都分为主机控制器驱动和设备驱动,主机控制器也就是 SOC的SPI控制器接口。我们编写好 SPI 控制器驱动以后就可以直接使用了,不管是什么 SPI 设备,SPI 控制器部分的驱动都是一样,我们的重点就落在了种类繁多的 SPI 设备驱动。

1. SPI 主机驱动

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

315 struct spi_master {
316 	struct device dev;
317
318 	struct list_head list;
......
326 	s16 bus_num;
327
328 	/* chipselects will be integral to many controllers; some others
329 	* might use board-specific GPIOs.
330 	*/
331 	u16 num_chipselect;
332
333 	/* some SPI controllers pose alignment requirements on DMAable
334 	* buffers; let protocol drivers know about these requirements.
335 	*/
336 	u16 dma_alignment;
337
338 	/* spi_device.mode flags understood by this controller driver */
339 	u16 mode_bits;
340
341 	/* bitmask of supported bits_per_word for transfers */
342 	u32 bits_per_word_mask;
......
347 	/* limits on transfer speed */
348 	u32 min_speed_hz;
349 	u32 max_speed_hz;
350
351 	/* other constraints relevant to this driver */
352 	u16 flags;
......
359 	/* lock and mutex for SPI bus locking */
360 	spinlock_t bus_lock_spinlock;
361 	struct mutex bus_lock_mutex;
362
363 	/* flag indicating that the SPI bus is locked for exclusive use */
364 	bool bus_lock_flag;
......
372 	int (*setup)(struct spi_device *spi);
373
......
393 	int (*transfer)(struct spi_device *spi,
394 					struct spi_message *mesg);
......
434 	int (*transfer_one_message)(struct spi_master *master,
435 								struct spi_message *mesg);
......
462 };

第 393 行,transfer 函数,和 i2c_algorithm 中的 master_xfer 函数一样,控制器数据传输函数。

第 434 行,transfer_one_message 函数,也用于 SPI 数据发送,用于发送一个 spi_message,SPI 的数据会打包成 spi_message,然后以队列方式发送出去。也就是 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. SPI 设备驱动

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

180 struct spi_driver {
181 	const struct spi_device_id *id_table;
182 	int (*probe)(struct spi_device *spi);
183 	int (*remove)(struct spi_device *spi);
184 	void (*shutdown)(struct spi_device *spi);
185 	struct device_driver driver;
186 };

可以看出,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。

返回值:无。

spi_driver 注册示例程序如下:

1 /* probe 函数 */
2 static int xxx_probe(struct spi_device *spi)
3 {
4 		/* 具体函数内容 */
5 		return 0;
6 }
7
8 /* remove 函数 */
9 static int xxx_remove(struct spi_device *spi)
10 {
11 		/* 具体函数内容 */
12 		return 0;
13 }
14 /* 传统匹配方式 ID 列表 */
15 static const struct spi_device_id xxx_id[] = {
16 		{"xxx", 0},
17 		{}
18 };
19
20 /* 设备树匹配列表 */
21 static const struct of_device_id xxx_of_match[] = {
22 		{ .compatible = "xxx" },
23 		{ /* Sentinel */ }
24 };
25
26 /* SPI 驱动结构体 */
27 static struct spi_driver xxx_driver = {
28 		.probe = xxx_probe,
29 		.remove = xxx_remove,
30 		.driver = {
31 			.owner = THIS_MODULE,
32 			.name = "xxx",
33 			.of_match_table = xxx_of_match,
34 		},
35 		.id_table = xxx_id,
36 };
37
38 /* 驱动入口函数 */
39 static int __init xxx_init(void)
40 {
41 		return spi_register_driver(&xxx_driver);
42 }
43
44 /* 驱动出口函数 */
45 static void __exit xxx_exit(void)
46 {
47 		spi_unregister_driver(&xxx_driver);
48 }
49
50 module_init(xxx_init);
51 module_exit(xxx_exit);

第 1~36 行,spi_driver 结构体,需要 SPI 设备驱动人员编写,包括匹配表、probe 函数等。和 i2c_driver、platform_driver 一样,就不详细讲解了。

第 39~42 行,在驱动入口函数中调用 spi_register_driver 来注册 spi_driver。

第 45~48 行,在驱动出口函数中调用 spi_unregister_driver 来注销 spi_driver。

3. SPI 设备和驱动匹配过程

具体参考正点原子IMX6UL驱动开发指南62.1.3节

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

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

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

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

具体参考正点原子IMX6UL驱动开发指南62.2节

3| SPI设备驱动编写流程

1.SPI 设备信息描述

1 、IO 的 的 pinctrl 子节点创建与修改

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

2 、SPI 设备节点的创建与修改

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

308 &ecspi1 {
309 	fsl,spi-num-chipselects = <1>;
310 	cs-gpios = <&gpio4 9 0>;
311 	pinctrl-names = "default";
312 	pinctrl-0 = <&pinctrl_ecspi1>;
313 	status = "okay";
314
315 	flash: m25p80@0 {
316 		#address-cells = <1>;
317 		#size-cells = <1>;
318 		compatible = "st,m25p32";
319 		spi-max-frequency = <20000000>;
320 		reg = <0>;
321 	};
322 };

示例代码是 I.MX6Q 的一款板子上的一个 SPI 设备节点,在这个板子的 ECSPI 接口上接了一个 m25p80,这是一个 SPI 接口的设备。

  • 第 309 行,设置“fsl,spi-num-chipselects”属性为 1,表示只有一个设备。
  • 第 310 行,设置“cs-gpios”属性,也就是片选信号为 GPIO4_IO09。
  • 第 311 行,设置“pinctrl-names”属性,也就是 SPI 设备所使用的 IO 名字。
  • 第 312 行,设置“pinctrl-0”属性,也就是所使用的 IO 对应的 pinctrl 节点。
  • 第 313 行,将 ecspi1 节点的“status”属性改为“okay”。
  • 第 315~320 行,ecspi1 下的 m25p80 设备信息,每一个 SPI 设备都采用一个子节点来描述其设备信息。第 315 行的“m25p80@0”后面的“0”表示 m25p80 的接到了 ECSPI 的通道 0上。这个要根据自己的具体硬件来设置。
  • 第 318 行,SPI 设备的 compatible 属性值,用于匹配设备驱动。
  • 第 319 行,“spi-max-frequency”属性设置 SPI 控制器的最高频率,这个要根据所使用的SPI 设备来设置,比如在这里将 SPI 控制器最高频率设置为 20MHz。
  • 第 320 行,reg 属性设置 m25p80 这个设备所使用的 ECSPI 通道,和“m25p80@0”后面的“0”一样。

我们一会在编写 ICM20608 的设备树节点信息的时候就参考示例代码中的内容即可。

2.SPI 设备数据收发处理流程

SPI 设备驱动的核心是 spi_driver,这个我们已经在前面讲过了。当我们向 Linux 内核注册成功 spi_driver 以后就可以使用 SPI 核心层提供的 API 函数来对设备进行读写操作了。

首先是 spi_transfer 结构体,此结构体用于描述 SPI 传输信息,结构体内容如下:

603 struct spi_transfer {
604 	/* it's ok if tx_buf == rx_buf (right?)
605 	* for MicroWire, one buffer must be null
606 	* buffers must work with dma_*map_single() calls, unless
607 	* spi_message.is_dma_mapped reports a pre-existing mapping
608 	*/
609 	const void *tx_buf;
610 	void *rx_buf;
611 	unsigned len;
612
613 	dma_addr_t tx_dma;
614 	dma_addr_t rx_dma;
615 	struct sg_table tx_sg;
616 	struct sg_table rx_sg;
617
618 	unsigned cs_change:1;
619 	unsigned tx_nbits:3;
620 	unsigned rx_nbits:3;
621 	#define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */
622 	#define SPI_NBITS_DUAL 0x02 /* 2bits transfer */
623 	#define SPI_NBITS_QUAD 0x04 /* 4bits transfer */
624 	u8 bits_per_word;
625 	u16 delay_usecs;
626 	u32 speed_hz;
627
628 	struct list_head transfer_list;
629 };

第 609 行,tx_buf 保存着要发送的数据。
第 610 行,rx_buf 用于保存接收到的数据。
第 611 行,len 是要进行传输的数据长度,SPI 是全双工通信,因此在一次通信中发送和接收的字节数都是一样的,所以 spi_transfer 中也就没有发送长度和接收长度之分。

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

660 struct spi_message {
661 	struct list_head transfers;
662
663 	struct spi_device *spi;
664
665 	unsigned is_dma_mapped:1;
......
678 	/* completion is reported through a callback */
679 	void (*complete)(void *context);
680 	void *context;
681 	unsigned frame_length;
682 	unsigned actual_length;
683 	int status;
684
685 	/* for optional use by whatever driver currently owns the
686 	* spi_message ... between calls to spi_async and then later
687 	* complete(), that's the spi_master controller driver.
688 	*/
689 	struct list_head queue;
690 	void *state;
691 };

在使用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_sync 函数。综上所述,SPI 数据传输步骤如下:

  • ①、申请并初始化 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 数据同步传输。

通过 SPI 进行 n 个字节的数据发送和接收的示例代码如下所示:

/* SPI 多字节发送 */
static int spi_send(struct spi_device *spi, u8 *buf, int len)
{
    int ret;
    struct spi_message m;
    struct spi_transfer t = {
        .tx_buf = buf,
        .len = len,
    };
    spi_message_init(&m); /* 初始化 spi_message */
    spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */
    ret = spi_sync(spi, &m); /* 同步传输 */
    return ret;
}
/* SPI 多字节接收 */
static int spi_receive(struct spi_device *spi, u8 *buf, int len)
{
    int ret;
    struct spi_message m;
    struct spi_transfer t = {
        .rx_buf = buf,
        .len = len,
    };
    spi_message_init(&m); /* 初始化 spi_message */
    spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */
    ret = spi_sync(spi, &m); /* 同步传输 */
    return ret;
}

4| 硬件原理图分析

ICM-20608 是在 I.MX6U-ALPHA 开发板底板上,原理图如图所示:

image-20200904113227640

5| 实验程序编写

1.修改设备树

1 、添加 ICM20608 所使用的IO

首先在 imx6ull-lxg-emmc.dts 文件中添加 ICM20608 所使用的 IO 信息,在 iomuxc 节点中添加一个新的子节点来描述 ICM20608 所使用的 SPI 引脚,子节点名字为 pinctrl_ecspi3,节点内容如下所示:

1 pinctrl_ecspi3: icm20608 {
2 	fsl,pins = <
3 		MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20 0x10b0 /* CS */
4 		MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x10b1 /* SCLK */
5 		MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 0x10b1 /* MISO */
6 		MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x10b1 /* MOSI */
7 	>;
8 };

UART2_TX_DATA 这个 IO 是 ICM20608 的片选信号,这里我们并没有将其复用为 ECSPI3的 SS0 信号,而是将其复用为了普通的 GPIO。因为我们需要自己控制片选信号,所以将其复用为普通的 GPIO。

2 、在 ecspi3 节点追加 icm20608 子节点

在 imx6ull-lxg-emmc.dts 文件中并没有任何向 ecspi3 节点追加内容的代码,这是因为NXP 官方的 6ULL EVK 开发板上没有连接 SPI 设备。在 imx6ull-lxg-emmc.dts 文件最后面加入如下所示内容:

1 &ecspi3 {
2 		fsl,spi-num-chipselects = <1>;
3 		cs-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>; /* cant't use cs-gpios! */
4 		pinctrl-names = "default";
5 		pinctrl-0 = <&pinctrl_ecspi3>;
6 		status = "okay";
7
8 		spidev: icm20608@0 {
9 			compatible = "alientek,icm20608";
10 			spi-max-frequency = <8000000>;
11 			reg = <0>;
12 		};
13 };
  • 第 2 行,设置当前片选数量为 1,因为就只接了一个 ICM20608。
  • 第 3 行,注意!这里并没有用到“cs-gpios”属性,而是用了一个自己定义的“cs-gpio”属性,因为我们要自己控制片选引脚。如果使用“cs-gpios”属性的话 SPI 主机驱动就会控制片选引脚。
  • 第 5 行,设置 IO 要使用的 pinctrl 子节点,也就是我们在前面示例代码中新建的pinctrl_ecspi3。
  • 第 6 行,imx6ull.dtsi 文件中默认将 ecspi3 节点状态(status)设置为“disable”,这里我们要将其改为“okay”。
  • 第 8~12 行,icm20608 设备子节点,因为 icm20608 连接在 ECSPI3 的第 0 个通道上,因此@后面为 0。第 9 行设置节点属性兼容值为“alientek,icm20608”,第 10 行设置 SPI 最大时钟频率为 8MHz,这是ICM20608 的 SPI 接口所能支持的最大的时钟频率。第 11 行,icm20608 连接在通道 0 上,因此 reg 为 0。

imx6ull-lxg-emmc.dts 文件修改完成以后重新编译一下,得到新的 dtb 文件,并使用新的 dtb 启动 Linux 系统。

2.编写 ICM20608 驱动

新建名为“22_spi”的文件夹,然后在 22_spi 文件夹里面创建 vscode 工程,工作区命名为“spi”。工程创建好以后新建 icm20608.c 和 icm20608reg.h 这两个文件,icm20608.c 为 ICM20608的驱动代码,icm20608reg.h是 ICM20608寄存器头文件。

先在 icm20608reg.h中定义好 ICM20608的寄存器,输入如下内容:

#ifndef ICM20608_H
#define ICM20608_H

#define ICM20608G_ID			0XAF	/* ID值 */
#define ICM20608D_ID			0XAE	/* ID值 */

/* ICM20608寄存器 
 *复位后所有寄存器地址都为0,除了
 *Register 107(0X6B) Power Management 1 	= 0x40
 *Register 117(0X75) WHO_AM_I 				= 0xAF或0xAE
 */
/* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
#define	ICM20_SELF_TEST_X_GYRO		0x00
#define	ICM20_SELF_TEST_Y_GYRO		0x01
#define	ICM20_SELF_TEST_Z_GYRO		0x02
#define	ICM20_SELF_TEST_X_ACCEL		0x0D
#define	ICM20_SELF_TEST_Y_ACCEL		0x0E
#define	ICM20_SELF_TEST_Z_ACCEL		0x0F

/* 陀螺仪静态偏移 */
#define	ICM20_XG_OFFS_USRH			0x13
#define	ICM20_XG_OFFS_USRL			0x14
#define	ICM20_YG_OFFS_USRH			0x15
#define	ICM20_YG_OFFS_USRL			0x16
#define	ICM20_ZG_OFFS_USRH			0x17
#define	ICM20_ZG_OFFS_USRL			0x18

#define	ICM20_SMPLRT_DIV			0x19
#define	ICM20_CONFIG				0x1A
#define	ICM20_GYRO_CONFIG			0x1B
#define	ICM20_ACCEL_CONFIG			0x1C
#define	ICM20_ACCEL_CONFIG2			0x1D
#define	ICM20_LP_MODE_CFG			0x1E
#define	ICM20_ACCEL_WOM_THR			0x1F
#define	ICM20_FIFO_EN				0x23
#define	ICM20_FSYNC_INT				0x36
#define	ICM20_INT_PIN_CFG			0x37
#define	ICM20_INT_ENABLE			0x38
#define	ICM20_INT_STATUS			0x3A

/* 加速度输出 */
#define	ICM20_ACCEL_XOUT_H			0x3B
#define	ICM20_ACCEL_XOUT_L			0x3C
#define	ICM20_ACCEL_YOUT_H			0x3D
#define	ICM20_ACCEL_YOUT_L			0x3E
#define	ICM20_ACCEL_ZOUT_H			0x3F
#define	ICM20_ACCEL_ZOUT_L			0x40

/* 温度输出 */
#define	ICM20_TEMP_OUT_H			0x41
#define	ICM20_TEMP_OUT_L			0x42

/* 陀螺仪输出 */
#define	ICM20_GYRO_XOUT_H			0x43
#define	ICM20_GYRO_XOUT_L			0x44
#define	ICM20_GYRO_YOUT_H			0x45
#define	ICM20_GYRO_YOUT_L			0x46
#define	ICM20_GYRO_ZOUT_H			0x47
#define	ICM20_GYRO_ZOUT_L			0x48

#define	ICM20_SIGNAL_PATH_RESET		0x68
#define	ICM20_ACCEL_INTEL_CTRL 		0x69
#define	ICM20_USER_CTRL				0x6A
#define	ICM20_PWR_MGMT_1			0x6B
#define	ICM20_PWR_MGMT_2			0x6C
#define	ICM20_FIFO_COUNTH			0x72
#define	ICM20_FIFO_COUNTL			0x73
#define	ICM20_FIFO_R_W				0x74
#define	ICM20_WHO_AM_I 				0x75

/* 加速度静态偏移 */
#define	ICM20_XA_OFFSET_H			0x77
#define	ICM20_XA_OFFSET_L			0x78
#define	ICM20_YA_OFFSET_H			0x7A
#define	ICM20_YA_OFFSET_L			0x7B
#define	ICM20_ZA_OFFSET_H			0x7D
#define	ICM20_ZA_OFFSET_L 			0x7E


#endif

接下来继续编写 icm20608.c 文件,因为 icm20608.c 文件内容比较长,因此这里就将其分开来讲解。

1、icm20608 设备结构体创建

首先创建一个 icm20608 设备结构体,如下所示:

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;			/* 私有数据 		*/
	int cs_gpio;				/* 片选所使用的GPIO编号		*/
	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;		/* 温度原始值 			*/
};

icm20608 的设备结构体 icm20608_dev 没什么好讲的,重点看一下 private_data,对于 SPI 设备驱动来讲最核心的就是 spi_device。probe 函数会向驱动提供当前 SPI 设备对应的spi_device,因此在 probe 函数中设置 private_data 为 probe 函数传递进来的 spi_device 参数。

2 、icm20608 的 的 spi_driver 注册与注销

对于 SPI 设备驱动,首先就是要初始化并向系统注册 spi_driver,icm20608 的 spi_driver 初始化、注册与注销代码如下:

/* 传统匹配方式ID列表 */
static const struct spi_device_id icm20608_id[] = {
	{"alientek,icm20608", 0},  
	{}
};

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

/* 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,
};
		   
/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init icm20608_init(void)
{
	return spi_register_driver(&icm20608_driver);
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit icm20608_exit(void)
{
	spi_unregister_driver(&icm20608_driver);
}

module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LXG@firestaradmin");

当 icm20608 设备和此驱动匹配成功以后 icm20608_probe 函数就会执行。同样的,当注销此驱动的时候 icm20608_remove 函数会执行。

3 、probe&remove 函数

 /*
  * @description     : spi驱动的probe函数,当驱动与
  *                    设备匹配以后此函数就会执行
  * @param - client  : spi设备
  * @param - id      : spi设备ID
  * 
  */	
static int icm20608_probe(struct spi_device *spi)
{
	int ret = 0;

	/* 1、构建设备号 */
	if (icm20608dev.major) {
		icm20608dev.devid = MKDEV(icm20608dev.major, 0);
		register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);
	} else {
		alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);
		icm20608dev.major = MAJOR(icm20608dev.devid);
	}

	/* 2、注册设备 */
	cdev_init(&icm20608dev.cdev, &icm20608_ops);
	cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);

	/* 3、创建类 */
	icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
	if (IS_ERR(icm20608dev.class)) {
		return PTR_ERR(icm20608dev.class);
	}

	/* 4、创建设备 */
	icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.devid, NULL, ICM20608_NAME);
	if (IS_ERR(icm20608dev.device)) {
		return PTR_ERR(icm20608dev.device);
	}

	/* 获取设备树中cs片选信号 */
	icm20608dev.nd = of_find_node_by_path("/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02010000");
	if(icm20608dev.nd == NULL) {
		printk("ecspi3 node not find!\r\n");
		return -EINVAL;
	} 

	/* 2、 获取设备树中的gpio属性,得到CS所使用的编号 */
	icm20608dev.cs_gpio = of_get_named_gpio(icm20608dev.nd, "cs-gpio", 0);
	if(icm20608dev.cs_gpio < 0) {
		printk("can't get cs-gpio");
		return -EINVAL;
	}

	/* 3、设置GPIO1_IO20为输出,并且输出高电平 */
	ret = gpio_direction_output(icm20608dev.cs_gpio, 1);
	if(ret < 0) {
		printk("can't set gpio!\r\n");
	}

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

	/* 初始化ICM20608内部寄存器 */
	icm20608_reginit();		
	return 0;
}

/*
 * @description     : spi驱动的remove函数,移除spi驱动的时候此函数会执行
 * @param - client 	: spi设备
 * @return          : 0,成功;其他负值,失败
 */
static int icm20608_remove(struct spi_device *spi)
{
	/* 删除设备 */
	cdev_del(&icm20608dev.cdev);
	unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);

	/* 注销掉类和设备 */
	device_destroy(icm20608dev.class, icm20608dev.devid);
	class_destroy(icm20608dev.class);
	return 0;
}

probe 函数,当设备与驱动匹配成功以后此函数就会执行,先是标准的注册字符设备驱动。然后获取设备节点中的“cs-gpio”属性,也就是获取到设备的片选 IO。

设置 SPI 为模式 0,也就是 CPOL=0,CPHA=0。
设置好 spi_device 以后需要使用 spi_setup 配置一下。
设置 icm20608dev 的 private_data 成员变量为 spi_device。
调用 icm20608_reginit 函数初始化 ICM20608,主要是初始化 ICM20608 指定寄存器。
icm20608_remove 函数,注销驱动的时候此函数就会执行。

4 、icm20608 寄存器读写与初始化

SPI 驱动最终是通过读写 icm20608 的寄存器来实现的,因此需要编写相应的寄存器读写函数,并且使用这些读写函数来完成对 icm20608 的初始化。icm20608 的寄存器读写以及初始化代码如下:

/*
 * @description	: 从icm20608读取多个寄存器数据
 * @param - dev:  icm20608设备
 * @param - reg:  要读取的寄存器首地址
 * @param - val:  读取到的数据
 * @param - len:  要读取的数据长度
 * @return 		: 操作结果
 */
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
	int ret;
	unsigned char txdata[len];
	struct spi_message m;
	struct spi_transfer *t;
	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);	/* 申请内存 */

	/* 第1次,发送要读取的寄存地址 */
	txdata[0] = reg | 0x80;		/* 写数据的时候寄存器地址bit8要置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);	/* 同步发送 */

	/* 第2次,读取数据 */
	txdata[0] = 0xff;			/* 随便一个值,此处无意义 */
	t->rx_buf = buf;			/* 读取到的数据 */
	t->len = len;				/* 要读取的数据长度 */
	spi_message_init(&m);		/* 初始化spi_message */
	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
	ret = spi_sync(spi, &m);	/* 同步发送 */

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

	return ret;
}

/*
 * @description	: 向icm20608多个寄存器写入数据
 * @param - dev:  icm20608设备
 * @param - reg:  要写入的寄存器首地址
 * @param - val:  要写入的数据缓冲区
 * @param - len:  要写入的数据长度
 * @return 	  :   操作结果
 */
static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
{
	int ret;

	unsigned char txdata[len];
	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)dev->private_data;

	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);	/* 申请内存 */
	gpio_set_value(dev->cs_gpio, 0);			/* 片选拉低 */

	/* 第1次,发送要读取的寄存地址 */
	txdata[0] = reg & ~0x80;	/* 写数据的时候寄存器地址bit8要清零 */
	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);	/* 同步发送 */

	/* 第2次,发送要写入的数据 */
	t->tx_buf = buf;			/* 要写入的数据 */
	t->len = len;				/* 写入的字节数 */
	spi_message_init(&m);		/* 初始化spi_message */
	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
	ret = spi_sync(spi, &m);	/* 同步发送 */

	kfree(t);					/* 释放内存 */
	gpio_set_value(dev->cs_gpio, 1);/* 片选拉高,释放ICM20608 */
	return ret;
}

/*
 * @description	: 读取icm20608指定寄存器值,读取一个寄存器
 * @param - dev:  icm20608设备
 * @param - reg:  要读取的寄存器
 * @return 	  :   读取到的寄存器值
 */
static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
{
	u8 data = 0;
	icm20608_read_regs(dev, reg, &data, 1);
	return data;
}

/*
 * @description	: 向icm20608指定寄存器写入指定的值,写一个寄存器
 * @param - dev:  icm20608设备
 * @param - reg:  要写的寄存器
 * @param - data: 要写入的值
 * @return   :    无
 */	

static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
{
	u8 buf = value;
	icm20608_write_regs(dev, reg, &buf, 1);
}

/*
 * @description	: 读取ICM20608的数据,读取原始数据,包括三轴陀螺仪、
 * 				: 三轴加速度计和内部温度。
 * @param - dev	: ICM20608设备
 * @return 		: 无。
 */
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]);
}

/*
 * ICM20608内部寄存器初始化函数 
 * @param  	: 无
 * @return 	: 无
 */
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						*/
}

icm20608_read_regs 函数,从 icm20608 中读取连续多个寄存器数据。

icm20608_write_regs 函数,向 icm20608 连续写入多个寄存器数据。

icm20608_read_onereg 函数,读取 icm20608 指定寄存器数据。

icm20608_write_onereg 函数,向 icm20608 指定寄存器写入数据。

icm20608_readdata 函数,读取 icm20608 六轴传感器和温度传感器原始数据值,应用程序读取icm20608 的时候这些传感器原始数据就会上报给应用程序。

icm20608_reginit 函数,初始化 icm20608,和我们 spi 裸机实验里面的初始化过程一样。

5 、字符设备驱动框架

icm20608 的字符设备驱动框架如下:

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做pr似有ate_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];
	long err = 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;
	err = copy_to_user(buf, data, sizeof(data));
	return 0;
}

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

/* icm20608操作函数 */
static const struct file_operations icm20608_ops = {
	.owner = THIS_MODULE,
	.open = icm20608_open,
	.read = icm20608_read,
	.release = icm20608_release,
};

字符设备驱动框架没什么好说的,重点是 icm20608_read 函数,当应用程序调用 read 函数读取 icm20608 设备文件的时候此函数就会执行。此函数调用上面编写好的icm20608_readdata 函数读取 icm20608 的原始数据并将其上报给应用程序。大家注意,在内核中尽量不要使用浮点运算,所以不要在驱动将 icm20608 的原始值转换为对应的实际值,因为会涉及到浮点计算。

3.编写测试 APP

新建 icm20608App.c 文件,然后在里面输入如下所示内容:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>


/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
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("can't open file %s\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;
}

在 while 循环中每隔 100ms 从 icm20608 中读取一次数据,读取到 icm20608原始数据以后将其转换为实际值,比如陀螺仪就是角速度、加速度计就是 g 值。注意,我们在icm20608 驱动中将陀螺仪和加速度计的测量范围全部设置到了最大,分别为±2000 和±16g。因此,在计算实际值的时候陀螺仪使用 16.4,加速度计使用 2048。最终将传感器原始数据和得到的实际值显示在终端上。

4.运行测试

编译运行。

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

-march-armv7-a -mfpu-neon -mfloat=hard

输入如下命令使能硬件浮点编译 icm20608App.c 这个测试程序:

arm-linux-gnueabihf-gcc -march=armv7-a -mfpu=neon -mfloat-abi=hard icm20608App.c -o icm20608App

编译成功以后就会生成 icm20608App 这个应用程序,那么究竟有没有使用硬件浮点呢?使用 arm-linux-gnueabihf-readelf 查看一下编译出来的 icm20608App 就知道了,输入如下命令:

arm-linux-gnueabihf-readelf -A icm20608App

结果如图 所示:

image-20200904121240231

从图 可以看出 FPU 架构为 VFPv3,SIMD 使用了 NEON,并且使用了 SP 和 DP,说明 icm20608App 这个应用程序使用了硬件浮点。

当驱动模块加载成功以后使用 icm20608App 来测试,输入如下命令:

./icm20608App /dev/icm20608

测试 APP 会不断的从 ICM20608 中读取数据,然后输出到终端上,如图所示: