linux SPI驱动实验

65 篇文章 12 订阅


之前我们讲解了如何编写 Linux 下的 I2C 设备驱动,SPI 也是很常用的串行通信协议,

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

一、linux下SPI驱动框架简介

SPI 驱动框架和 I2C 很类似,都分为主机控制器驱动和设备驱动,主机控制器也就是 SOC的SPI控制器接口。

比如在裸机实验中我们编写了bsp_spi.c和bsp_spi.h这两个文件,这两个文件是 I.MX6U 的 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 的使用者,这一部分的驱动就不用操心了,

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设备和驱动匹配过程

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

131 struct bus_type spi_bus_type = {
132 .name = "spi",
133 .dev_groups = spi_dev_groups,
134 .match = spi_match_device,
135 .uevent = spi_uevent,
136 };
可以看出,SPI 设备和驱动的匹配函数为 spi_match_device,函数内容如下:
99 static int spi_match_device(struct device *dev,
struct device_driver *drv)
100 {
101 const struct spi_device *spi = to_spi_device(dev);
102 const struct spi_driver *sdrv = to_spi_driver(drv);
103
104 /* Attempt an OF style match */
105 if (of_driver_match_device(dev, drv))
106 return 1;
107
108 /* Then try ACPI */
109 if (acpi_driver_match_device(dev, drv))
110 return 1;
111
112 if (sdrv->id_table)
113 return !!spi_match_id(sdrv->id_table, spi);
114
115 return strcmp(spi->modalias, drv->name) == 0;
116 }

spi_match_device 函数和 i2c_match_device 函数的对于设备和驱动的匹配过程基本一样。
第 105 行,of_driver_match_device 函数用于完成设备树设备和驱动匹配。比较 SPI 设备节点的 compatible 属性和 of_device_id 中的 compatible 属性是否相等,如果相当的话就表示 SPI 设备和驱动匹配。
第 109 行,acpi_driver_match_device 函数用于 ACPI 形式的匹配。
第 113 行,spi_match_id 函数用于传统的、无设备树的 SPI 设备和驱动匹配过程。比较 SPI设备名字和 spi_device_id 的 name 字段是否相等,相等的话就说明 SPI 设备和驱动匹配。
第 115 行,比较 spi_device 中 modalias 成员变量和 device_driver 中的 name 成员变量是否相等。

二、6u 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,在此文件中找到如下内容:

694 static struct platform_device_id spi_imx_devtype[] = {
695 {
696 .name = "imx1-cspi",
697 .driver_data = (kernel_ulong_t) &imx1_cspi_devtype_data,
698 }, {
699 .name = "imx21-cspi",
700 .driver_data = (kernel_ulong_t) &imx21_cspi_devtype_data,
......713 }, {
714 .name = "imx6ul-ecspi",
715 .driver_data = (kernel_ulong_t) &imx6ul_ecspi_devtype_data,
716 }, {
717 /* sentinel */
718 }
719 };
720
721 static const struct of_device_id spi_imx_dt_ids[] = {
722 { .compatible = "fsl,imx1-cspi", .data =
&imx1_cspi_devtype_data, },
......
728 { .compatible = "fsl,imx6ul-ecspi", .data =
&imx6ul_ecspi_devtype_data, },
729 { /* sentinel */ }
730 };
731 MODULE_DEVICE_TABLE(of, spi_imx_dt_ids);
......
1338 static struct platform_driver spi_imx_driver = {
1339 .driver = {
1340 .name = DRIVER_NAME,
1341 .of_match_table = spi_imx_dt_ids,
1342 .pm = IMX_SPI_PM,
1343 },
1344 .id_table = spi_imx_devtype,
1345 .probe = spi_imx_probe,
1346 .remove = spi_imx_remove,
1347 };
1348 module_platform_driver(spi_imx_driver);

第 714 行,spi_imx_devtype 为 SPI 无设备树匹配表。
第 721 行,spi_imx_dt_ids 为 SPI 设备树匹配表。
第 728 行,“fsl,imx6ul-ecspi”匹配项,因此可知 I.MX6U 的 ECSPI 驱动就是 spi-imx.c 这个文件。
第 1338~1347 行,platform_driver 驱动框架,和 I2C 的适配器驱动一样,SPI 主机驱动器采用了 platfom 驱动框架。当设备和驱动匹配成功以后 spi_imx_probe 函数就会执行。
spi_imx_probe 函数会从设备树中读取相应的节点属性值,申请并初始化 spi_master,最后调用 spi_bitbang_start 函数(spi_bitbang_start 会调用spi_register_master 函数)向 Linux 内核注册spi_master。

对于 I.MX6U 来讲,SPI 主机的最终数据收发函数为 spi_imx_transfer,此函数通过如下层层调用最终实现 SPI 数据发送:

spi_imx_transfer
-> spi_imx_pio_transfer
-> spi_imx_push
-> spi_imx->tx

spi_imx 是个 spi_imx_data 类型的机构指针变量,其中 tx 和 rx 这两个成员变量分别为 SPI数据发送和接收函数。I.MX6U SPI 主机驱动会维护一个 spi_imx_data 类型的变量 spi_imx,并且使用spi_imx_setupxfer 函数来设置 spi_imx 的 tx 和 rx 函数。根据要发送的数据数据位宽的不同,分别有 8 位、16 位和 32 位的发送函数,如下所示:

spi_imx_buf_tx_u8
spi_imx_buf_tx_u16
spi_imx_buf_tx_u32

同理,也有 8 位、16 位和 32 位的数据接收函数,如下所示:

spi_imx_buf_rx_u8
spi_imx_buf_rx_u16
spi_imx_buf_rx_u32

我们就以 spi_imx_buf_tx_u8 这个函数为例,看看,一个自己的数据发送是怎么完成的,在spi-imx.c 文件中找到如下所示内容:

152 #define MXC_SPI_BUF_TX(type) \
153 static void spi_imx_buf_tx_##type(struct spi_imx_data *spi_imx) \
154 { \
155 type val = 0; \
156 \
157 if (spi_imx->tx_buf) { \
158 val = *(type *)spi_imx->tx_buf; \
159 spi_imx->tx_buf += sizeof(type); \
160 } \
161 \
162 spi_imx->count -= sizeof(type); \
163 \
164 writel(val, spi_imx->base + MXC_CSPITXDATA); \
165 }
166
167 MXC_SPI_BUF_RX(u8)
168 MXC_SPI_BUF_TX(u8)

可以看出,spi_imx_buf_tx_u8 函数是通过 MXC_SPI_BUF_TX 宏来实现的。
第 164 行就是将要发送的数据值写入到 ECSPI 的 TXDATA 寄存器里面去,这和我们 SPI 裸机实验的方法一样。
将第 168 行的 MXC_SPI_BUF_TX(u8)展开就是 spi_imx_buf_tx_u8 函数。

其他的 tx 和 rx 函数都是这样实现的,这里就不做介绍了。关于 I.MX6U 的主机驱动程序就讲解到这里,基本套路和 I2C 的适配器驱动程序类似。

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

四、硬件原理图分析

五、实验程序编写

1. 修改设备树

1. 添加 ICM20608所使用的 IO

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

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_RTS_B__ECSPI3_MISO 0x10b1 /* MISO */
				MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x10b1 /* MOSI */
			>;
	};

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

2. 在 ecspi3节点追加 icm20608

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

&ecspi3 {
		fsl,spi-num-chipselects = <1>;
		cs-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>; /* cant't use cs-gpios! */
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_ecspi3>;
		status = "okay";

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

在这里插入图片描述

第 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-luatao-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

/***************************************************************

Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.

文件名		: icm20608reg.h

作者	  	: 左忠凯

版本	   	: V1.0

描述	   	: ICM20608寄存器地址描述头文件

其他	   	: 无

论坛 	   	: www.openedv.com

日志	   	: 初版V1.0 2019/9/2 左忠凯创建

***************************************************************/

#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 设备结构体,如下所示:

1 #include <linux/types.h>
2 #include <linux/kernel.h>
3 #include <linux/delay.h>
......
22 #include <asm/io.h>
23 #include "icm20608reg.h"
24 /***************************************************************
25 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
26 文件名 : icm20608.c
27 作者 : 左忠凯
28 版本 : V1.0
29 描述 : ICM20608 SPI 驱动程序
30 其他 : 无
31 论坛 : www.openedv.com
32 日志 : 初版 V1.0 2019/9/2 左忠凯创建
33 ***************************************************************/
34 #define ICM20608_CNT 1
35 #define ICM20608_NAME "icm20608"
36
37 struct icm20608_dev {
38 dev_t devid; /* 设备号 */
39 struct cdev cdev; /* cdev */
40 struct class *class; /* 类 */
41 struct device *device; /* 设备 */
42 struct device_node *nd; /* 设备节点 */
43 int major; /* 主设备号 */
44 void *private_data; /*  私有数据 */
45 int cs_gpio; /* 片选所使用的 GPIO 编号*/
46 signed int gyro_x_adc; /* 陀螺仪 X 轴原始值 */
47 signed int gyro_y_adc; /* 陀螺仪 Y 轴原始值 */
48 signed int gyro_z_adc; /* 陀螺仪 Z 轴原始值 */
49 signed int accel_x_adc; /* 加速度计 X 轴原始值 */
50 signed int accel_y_adc; /* 加速度计 Y 轴原始值 */
51 signed int accel_z_adc; /* 加速度计 Z 轴原始值 */
52 signed int temp_adc; /* 温度原始值 */
53 };
54
55 static struct icm20608_dev icm20608dev;

icm20608 的设备结构体 icm20608_dev 没什么好讲的,重点看一下第 44 行的 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 初始化、注册与注销代码如下:

1 /* 传统匹配方式 ID 列表 */
2 static const struct spi_device_id icm20608_id[] = {
3 {"alientek,icm20608", 0},
4 {}
5 };
6
7 /* 设备树匹配列表 */
8 static const struct of_device_id icm20608_of_match[] = {
9 { .compatible = "alientek,icm20608" },
10 { /* Sentinel */ }
11 };
12
13 /* SPI 驱动结构体 */
14 static struct spi_driver icm20608_driver = {
15 .probe = icm20608_probe,
16 .remove = icm20608_remove,
17 .driver = {
18 .owner = THIS_MODULE,
19 .name = "icm20608",
20 .of_match_table = icm20608_of_match,
21 },
22 .id_table = icm20608_id,
23 };
24
25 /*
26 * @description  : 驱动入口函数
27 * @param : 无
28 * @return : 无
29 */
30 static int __init icm20608_init(void)
31 {
32 return spi_register_driver(&icm20608_driver);
33 }
34
35 /*
36 * @description  : 驱动出口函数
37 * @param : 无
38 * @return : 无
39 */
40 static void __exit icm20608_exit(void)
41 { 
42 spi_unregister_driver(&icm20608_driver);
43 }
44
45 module_init(icm20608_init);
46 module_exit(icm20608_exit);
47 MODULE_LICENSE("GPL");
48 MODULE_AUTHOR("zuozhongkai");

第 2~5 行,传统的设备和驱动匹配表。
第 8~11 行,设备树的设备与驱动匹配表,这里只有一个匹配项:“alientek,icm20608”。
第 14~23 行,icm20608 的 spi_driver 结构体变量,当 icm20608 设备和此驱动匹配成功以后
第 15 行的 icm20608_probe 函数就会执行。同样的,当注销此驱动的时候 icm20608_remove 函数会执行。
第 30~33 行,icm20608_init 函数为 icm20608 的驱动入口函数,在此函数中使用spi_register_driver 向 Linux 系统注册上面定义的 icm20608_driver。
第 40~43 行,icm20608_exit 函数为 icm20608 的驱动出口函数,在此函数中使用spi_unregister_driver 注销掉前面注册的icm20608_driver

3. probe&remove 函数

icm20608_driver 中的 probe 和 remove 函数内容如下所示:

1 /*
2 * @description : spi 驱动的 probe 函数,当驱动与
3 * 设备匹配以后此函数就会执行
4 * @param - client : spi 设备
5 * @param - id : spi 设备 ID
6 *
7 */
8 static int icm20608_probe(struct spi_device *spi)
9 {
10 int ret = 0;
11
12 /* 1、构建设备号 */
13 if (icm20608dev.major) {
14 icm20608dev.devid = MKDEV(icm20608dev.major, 0);
15 register_chrdev_region(icm20608dev.devid, ICM20608_CNT,
ICM20608_NAME);
16 } else {
17 alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT,
ICM20608_NAME);
18 icm20608dev.major = MAJOR(icm20608dev.devid);
19 }
20
21 /* 2、注册设备 */
22 cdev_init(&icm20608dev.cdev, &icm20608_ops);
23 cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);
24
25 /* 3、创建类 */
26 icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
27 if (IS_ERR(icm20608dev.class)) {
28 return PTR_ERR(icm20608dev.class);
29 }
30
31 /* 4、创建设备 */
32 icm20608dev.device = device_create(icm20608dev.class, NULL,
icm20608dev.devid, NULL, ICM20608_NAME);
33 if (IS_ERR(icm20608dev.device)) {
34 return PTR_ERR(icm20608dev.device);
35 }
36
37 /* 获取设备树中 spi 节点 */
38 icm20608dev.nd = of_find_node_by_path("/soc/aips-bus@02000000/
spba-bus@02000000/ecspi@02010000");
39 if(icm20608dev.nd == NULL) {
40 printk("ecspi3 node not find!\r\n");
41 return -EINVAL;
42 }
43
44 /* 2、 获取设备树中的 gpio 属性,得到 CS 片选所使用的 GPIO 编号 */
45 icm20608dev.cs_gpio = of_get_named_gpio(icm20608dev.nd,
"cs-gpio", 0);
46 if(icm20608dev.cs_gpio < 0) {
47 printk("can't get cs-gpio");
48 return -EINVAL;
49 }
50
51 /* 3、设置 GPIO1_IO20 为输出,并且输出高电平 */
52 ret = gpio_direction_output(icm20608dev.cs_gpio, 1);
53 if(ret < 0) {
54 printk("can't set gpio!\r\n");
55 }
56
57 /*初始化 spi_device */
58 spi->mode = SPI_MODE_0; /*MODE0,CPOL=0,CPHA=0 */
59 spi_setup(spi);
60 icm20608dev.private_data = spi; /* 设置私有数据 */
61
62 /* 初始化 ICM20608 内部寄存器 */
63 icm20608_reginit();
64 return 0;
65 }
66
67 /*
68 * @description : spi 驱动的 remove 函数,移除 spi 驱动的时候此函数会执行
69 * @param – client : spi 设备
70 * @return : 0,成功;其他负值,失败
71 */
72 static int icm20608_remove(struct spi_device *spi)
73 {
74 /* 删除设备 */
75 cdev_del(&icm20608dev.cdev);
76 unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);
77
78 /* 注销掉类和设备 */
79 device_destroy(icm20608dev.class, icm20608dev.devid);
80 class_destroy(icm20608dev.class);
81 return 0;
82 }

第 8~65 行,probe 函数,当设备与驱动匹配成功以后此函数就会执行,第 13~55 行都是标准的注册字符设备驱动。其中在第 38~49 行获取设备节点中的“cs-gpio”属性,也就是获取到设备的片选 IO。
第 58 行,设置 SPI 为模式 0,也就是 CPOL=0,CPHA=0。
第 59 行,设置好 spi_device 以后需要使用 spi_setup 配置一下。
第 60 行,设置 icm20608dev 的 private_data 成员变量为 spi_device。
第 63 行,调用 icm20608_reginit 函数初始化 ICM20608,主要是初始化 ICM20608 指定寄存器。
第 72~81 行,icm20608_remove 函数,注销驱动的时候此函数就会执行。

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

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

1 /*
2 * @description : 从 icm20608 读取多个寄存器数据
3 * @param – dev  : icm20608 设备
4 * @param – reg  : 要读取的寄存器首地址
5 * @param – buf  : 读取到的数据
6 * @param – len  : 要读取的数据长度
7 * @return : 操作结果
8 */
9 static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg,
void *buf, int len)
10 {
11 int ret;
12 unsigned char txdata[len];
13 struct spi_message m;
14 struct spi_transfer *t;
15 struct spi_device *spi = (struct spi_device *)dev->private_data;
16
17 gpio_set_value(dev->cs_gpio, 0); /* 片选拉低,选中 ICM20608 */
18 t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
19
20 /* 第 1 次,发送要读取的寄存地址 */
21 txdata[0] = reg | 0x80; /* 写数据的时候寄存器地址 bit7 要置 1 */
22 t->tx_buf = txdata; /* 要发送的数据 */
23 t->len = 1; /* 1 个字节 */
24 spi_message_init(&m); /* 初始化 spi_message */
25 spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message */
26 ret = spi_sync(spi, &m); /* 同步发送 */
27
28 /* 第 2 次,读取数据 */
29 txdata[0] = 0xff; /* 随便一个值,此处无意义 */
30 t->rx_buf = buf; /* 读取到的数据 */
31 t->len = len; /* 要读取的数据长度 */
32 spi_message_init(&m); /* 初始化 spi_message */
33 spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message*/
34 ret = spi_sync(spi, &m); /* 同步发送 */
35
36 kfree(t); /* 释放内存 */
37 gpio_set_value(dev->cs_gpio, 1); /* 片选拉高,释放 ICM20608 */
38
39 return ret;
40 }
41
42 /*
43 * @description  : 向 icm20608 多个寄存器写入数据
44 * @param – dev  : icm20608 设备
45 * @param – reg  : 要写入的寄存器首地址
46 * @param – buf  : 要写入的数据缓冲区
47 * @param – len  : 要写入的数据长度
48 * @return : 操作结果
49 */
50 static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg,
u8 *buf, u8 len)
51 {
52 int ret;
53
54 unsigned char txdata[len];
55 struct spi_message m;
56 struct spi_transfer *t;
57 struct spi_device *spi = (struct spi_device *)dev->private_data;
58
59 t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
60 gpio_set_value(dev->cs_gpio, 0);  /* 片选拉低 */
61
62 /* 第 1 次,发送要读取的寄存地址 */
63 txdata[0] = reg & ~0x80; /* 写数据的时候寄存器地址 bit8 要清零 */
64 t->tx_buf = txdata; /* 要发送的数据 */
65 t->len = 1; /* 1 个字节 */
66 spi_message_init(&m); /* 初始化 spi_message */
67 spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message */
68 ret = spi_sync(spi, &m); /* 同步发送 */
69
70 /* 第 2 次,发送要写入的数据 */
71 t->tx_buf = buf; /* 要写入的数据 */
72 t->len = len; /* 写入的字节数 */
73 spi_message_init(&m); /* 初始化 spi_message */
74 spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message*/
75 ret = spi_sync(spi, &m); /* 同步发送 */
76
77 kfree(t); /* 释放内存 */
78 gpio_set_value(dev->cs_gpio, 1);/* 片选拉高,释放 ICM20608 */
79 return ret;
80 }
81
82 /*
83 * @description  : 读取 icm20608 指定寄存器值,读取一个寄存器
84 * @param – dev  : icm20608 设备
85 * @param – reg  : 要读取的寄存器
86 * @return : 读取到的寄存器值
87 */
88 static unsigned char icm20608_read_onereg(struct icm20608_dev *dev,
u8 reg)
89 {
90 u8 data = 0;
91 icm20608_read_regs(dev, reg, &data, 1);
92 return data;
93 }
94
95 /*
96 * @description : 向 icm20608 指定寄存器写入指定的值,写一个寄存器
97 * @param – dev  : icm20608 设备
98 * @param – reg  : 要写的寄存器
99 * @param – data : 要写入的值
100 * @return : 无
101 */
102
103 static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg,
u8 value)
104 {
105 u8 buf = value;
106 icm20608_write_regs(dev, reg, &buf, 1);
107 }
108
109 /*
110 * @description : 读取 ICM20608 的数据,读取原始数据,包括三轴陀螺仪、
111 * : 三轴加速度计和内部温度。
112 * @param - dev : ICM20608 设备
113 * @return : 无。
114 */
115 void icm20608_readdata(struct icm20608_dev *dev)
116 {
117 unsigned char data[14];
118 icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);
119
120 dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]);
121 dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]);
122 dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]);
123 dev->temp_adc = (signed short)((data[6] << 8) | data[7]);
124 dev->gyro_x_adc = (signed short)((data[8] << 8) | data[9]);
125 dev->gyro_y_adc = (signed short)((data[10] << 8) | data[11]);
126 dev->gyro_z_adc = (signed short)((data[12] << 8) | data[13]);
127 }
128 /*
129 * ICM20608 内部寄存器初始化函数
130 * @param : 无
131 * @return : 无
132 */
133 void icm20608_reginit(void)
134 {
135 u8 value = 0;
136
137 icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x80);
138 mdelay(50);
139 icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x01);
140 mdelay(50);
141
142 value = icm20608_read_onereg(&icm20608dev, ICM20_WHO_AM_I);
143 printk("ICM20608 ID = %#X\r\n", value);
144
145 icm20608_write_onereg(&icm20608dev, ICM20_SMPLRT_DIV, 0x00);
146 icm20608_write_onereg(&icm20608dev, ICM20_GYRO_CONFIG, 0x18);
147 icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG, 0x18);
148 icm20608_write_onereg(&icm20608dev, ICM20_CONFIG, 0x04);
149 icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG2, 0x04);
150 icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_2, 0x00);
151 icm20608_write_onereg(&icm20608dev, ICM20_LP_MODE_CFG, 0x00);
152 icm20608_write_onereg(&icm20608dev, ICM20_FIFO_EN, 0x00);
153 }

第 9~40 行,icm20608_read_regs 函数,从 icm20608 中读取连续多个寄存器数据。
第 50~80 行,icm20608_write_regs 函数,向 icm20608 连续写入多个寄存器数据。
第 88~83 行,icm20608_read_onereg 函数,读取 icm20608 指定寄存器数据。
第 103~107 行,icm20608_write_onereg 函数,向 icm20608 指定寄存器写入数据。
第 115~126 行,icm20608_readdata 函数,读取 icm20608 六轴传感器和温度传感器原始数据值,应用程序读取 icm20608 的时候这些传感器原始数据就会上报给应用程序。
第 133~153 行,icm20608_reginit 函数,初始化 icm20608,和我们 spi 裸机实验里面的初始化过程一样。

5. 字符设别驱动框架

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

1 /*
2 * @description : 打开设备
3 * @param – inode : 传递给驱动的 inode
4 * @param - filp : 设备文件,file 结构体有个叫做 pr 似有 ate_data 的成员变量
5 * 一般在 open 的时候将 private_data 似有向设备结构体。
6 * @return : 0 成功;其他 失败
7 */
8 static int icm20608_open(struct inode *inode, struct file *filp)
9 {
10 filp->private_data = &icm20608dev; /* 设置私有数据 */
11 return 0;
12 }
13
14 /*
15 * @description  : 从设备读取数据
16 * @param - filp : 要打开的设备文件(文件描述符)
17 * @param - buf : 返回给用户空间的数据缓冲区
18 * @param - cnt : 要读取的数据长度
19 * @param - offt : 相对于文件首地址的偏移
20 * @return : 读取的字节数,如果为负值,表示读取失败
21 */
22 static ssize_t icm20608_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *off)
23 {
24 signed int data[7];
25 long err = 0;
26 struct icm20608_dev *dev = (struct icm20608_dev *
)filp->private_data;
27
28 icm20608_readdata(dev);
29 data[0] = dev->gyro_x_adc;
30 data[1] = dev->gyro_y_adc;
31 data[2] = dev->gyro_z_adc;
32 data[3] = dev->accel_x_adc;
33 data[4] = dev->accel_y_adc;
34 data[5] = dev->accel_z_adc;
35 data[6] = dev->temp_adc;
36 err = copy_to_user(buf, data, sizeof(data));
37 return 0;
38 }
39
40 /*
41 * @description : 关闭/释放设备
42 * @param - filp : 要关闭的设备文件(文件描述符)
43 * @return : 0 成功;其他 失败
44 */
45 static int icm20608_release(struct inode *inode, struct file *filp)
46 {
47 return 0;
48 }
49
50 /* icm20608 操作函数 */
51 static const struct file_operations icm20608_ops = {
52 .owner = THIS_MODULE,
53 .open = icm20608_open,
54 .read = icm20608_read,
55 .release = icm20608_release,
56 };

字符设备驱动框架没什么好说的,重点是第 22~38 行的 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>

/***************************************************************

Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.

文件名		: icm20608App.c

作者	  	: 左忠凯

版本	   	: V1.0

描述	   	: icm20608设备测试APP。

其他	   	: 无

使用方法	 :./icm20608App /dev/icm20608

论坛 	   	: www.openedv.com

日志	   	: 初版V1.0 2019/9/20 左忠凯创建

***************************************************************/



/*

 * @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;

}

第 60~91 行,在 while 循环中每隔 100ms 从 icm20608 中读取一次数据,读取到 icm20608原始数据以后将其转换为实际值,比如陀螺仪就是角速度、加速度计就是 g 值。注意,我们在icm20608 驱动中将陀螺仪和加速度计的测量范围全部设置到了最大,分别为±2000 和±16g。

因此,在计算实际值的时候陀螺仪使用 16.4,加速度计使用 2048。最终将传感器原始数据和得到的实际值显示在终端上。

六、运行测试

在 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

在这里插入图片描述
可以看出 FPU 架构为 VFPv3,SIMD 使用了 NEON,并且使用了 SP 和 DP,说明 icm20608App 这个应用程序使用了硬件浮点

depmod //第一次加载驱动的时候需要运行此命令
modprobe icm20608.ko //加载驱动模块

在这里插入图片描述
当驱动模块加载成功以后使用 icm20608App 来测试,输入如下命令:

./icm20608App /dev/icm20608

测试 APP 会不断的从 ICM20608 中读取数据,然后输出到终端上,如图
在这里插入图片描述
晃动一下开发板,这个时候陀螺仪和加速度计的值就会有变化,值没有变化,可能是有点问题(后面再来研究)

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值