Linux SPI 驱动实验

2023-11-14

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

Linux 下SPI 驱动框架简介

SPI 驱动框架和I2C 很类似,都分为主机控制器驱动和设备驱动,主机控制器也就是SOC
的SPI 控制器接口。比如在裸机篇中的《第二十七章SPI 实验》,我们编写了bsp_spi.c 和bsp_spi.h
这两个文件,这两个文件是I.MX6U 的SPI 控制器驱动,我们编写好SPI 控制器驱动以后就可
以直接使用了,不管是什么SPI 设备,SPI 控制器部分的驱动都是一样,我们的重点就落在了
种类繁多的SPI 设备驱动。

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。

SPI 设备驱动

spi 设备驱动和i2c 设备驱动也很类似,Linux 内核使用spi_driver 结构体来表示spi 设备驱
动,我们在编写SPI 设备驱动的时候需要实现spi_driver 。spi_driver 结构体定义在
include/linux/spi/spi.h 文件中,结构体内容如下:
在这里插入图片描述
可以看出,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。

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 成员变量是否
相等。

I.MX6U SPI 主机驱动分析

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

示例代码62.2.1 imx6ull.dtsi 文件中的ecspi3 节点内容
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,在此文件中找到如下内容:

示例代码62.2.2 spi_imx_driver 结构体
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 文件中找到如下所示内容:

示例代码62.2.3 spi_imx_buf_tx_u8 函数
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)

从示例代码62.2.3 可以看出,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 设备驱动编写流程

SPI 设备信息描述

1、IO 的pinctrl 子节点创建与修改
首先肯定是根据所使用的IO 来创建或修改pinctrl 子节点,这个没什么好说的,唯独要注意的就是检查相应的IO 有没有被其他的设备所使用,如果有的话需要将其删除掉!
2、SPI 设备节点的创建与修改
采用设备树的情况下,SPI 设备信息描述就通过创建相应的设备子节点来完成,我们可以
打开imx6qdl-sabresd.dtsi 这个设备树头文件,在此文件里面找到如下所示内容:

示例代码62.3.1.1 m25p80 设备节点
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 };

示例代码62.3.1.1 是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 的设备树节点信息的时候就参考示例代码62.3.1.1 中的内容即
可。

SPI 设备数据收发处理流程

SPI 设备驱动的核心是spi_driver,这个我们已经在62.1.2 小节讲过了。当我们向Linux 内
核注册成功spi_driver 以后就可以使用SPI 核心层提供的API 函数来对设备进行读写操作了。
首先是spi_transfer 结构体,此结构体用于描述SPI 传输信息,结构体内容如下:

示例代码62.3.2.1 spi_transfer 结构体
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 也是一个结构体,内容如下:

示例代码62.3.2.2 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 个字节的数据发送和接收的示例代码如下所示:

示例代码62.3.2.3 SPI 数据读写操作
    /* 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;
}

硬件原理图分析

本章实验硬件原理图参考26.2 小节即可。

试验程序编写

本实验对应的例程路径为:开发板光盘-> 2、Linux 驱动例程-> 22_spi。

修改设备树

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

示例代码62.5.1.1 icm20608 IO 节点信息
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-alientek-emmc.dts 文件中并没有任何向ecspi3 节点追加内容的代码,这是因为
NXP 官方的6ULL EVK 开发板上没有连接SPI 设备。在imx6ull-alientek-emmc.dts 文件最后面
加入如下所示内容:

示例代码62.5.1.2 向ecspi3 节点加入icm20608 信息
1 &ecspi3 {
2 fsl,spi-num-chipselects = <1>;
3 cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
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”属性来描述片选引脚,SPI 主机驱动就会控制片选引脚。
第5 行,设置IO 要使用的pinctrl 子节点,也就是我们在示例代码62.5.1.1 中新建的
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-alientek-emmc.dts 文件修改完成以后重新编译一下,得到新的dtb 文件,并使用新
的dtb 启动Linux 系统。

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

示例代码62.5.2.2 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 初
始化、注册与注销代码如下:

示例代码62.5.2.3 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 函数内容如下所示:

示例代码62.5.2.4 probe 和remove 函数
1 /*
2 * @description : spi驱动的probe函数,当驱动与
3 * 设备匹配以后此函数就会执行
4 * @param - client : i2c设备
5 * @param - id : i2c设备ID
6 *
7 */
8 static int icm20608_probe(struct spi_device *spi)
9 {
10 /* 1、构建设备号*/
11 if (icm20608dev.major) {
12 icm20608dev.devid = MKDEV(icm20608dev.major, 0);
13 register_chrdev_region(icm20608dev.devid, ICM20608_CNT,
ICM20608_NAME);
14 } else {
15 alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT,
ICM20608_NAME);
16 icm20608dev.major = MAJOR(icm20608dev.devid);
17 }
18
19 /* 2、注册设备*/
20 cdev_init(&icm20608dev.cdev, &icm20608_ops);
21 cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);
22
23 /* 3、创建类*/
24 icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
25 if (IS_ERR(icm20608dev.class)) {
26 return PTR_ERR(icm20608dev.class);
27 }
28
29 /* 4、创建设备*/
30 icm20608dev.device = device_create(icm20608dev.class, NULL,
icm20608dev.devid, NULL, ICM20608_NAME);
31 if (IS_ERR(icm20608dev.device)) {
32 return PTR_ERR(icm20608dev.device);
33 }
34
35 /*初始化spi_device */
36 spi->mode = SPI_MODE_0; /*MODE0,CPOL=0,CPHA=0*/
37 spi_setup(spi);
38 icm20608dev.private_data = spi; /* 设置私有数据*/
39
40 /* 初始化ICM20608内部寄存器*/
41 icm20608_reginit();
42 return 0;
43 }
44
45 /*
46 * @description : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
47 * @param - client : i2c设备
48 * @return : 0,成功;其他负值,失败
49 */
50 static int icm20608_remove(struct spi_device *spi)
51 {
52 /* 删除设备*/
53 cdev_del(&icm20608dev.cdev);
54 unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);
55
56 /* 注销掉类和设备*/
57 device_destroy(icm20608dev.class, icm20608dev.devid);
58 class_destroy(icm20608dev.class);
59 return 0;
60 }

第8~43 行,probe 函数,当设备与驱动匹配成功以后此函数就会执行,第10~33 行都是标
准的注册字符设备驱动。
第36 行,设置SPI 为模式0,也就是CPOL=0,CPHA=0。
第37 行,设置好spi_device 以后需要使用spi_setup 配置一下。
第38 行,设置icm20608dev 的private_data 成员变量为spi_device。
第41 行,调用icm20608_reginit 函数初始化ICM20608,主要是初始化ICM20608 指定寄
存器。
第50~60 行,icm20608_remove 函数,注销驱动的时候此函数就会执行。
4、icm20608 寄存器读写与初始化
SPI 驱动最终是通过读写icm20608 的寄存器来实现的,因此需要编写相应的寄存器读写函
数,并且使用这些读写函数来完成对icm20608 的初始化。icm20608 的寄存器读写以及初始化
代码如下:

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

第9~50 行,icm20608_read_regs 函数,从icm20608 中读取连续多个寄存器数据;注意:
在本实验中,SPI 为全双工通讯没有所谓的发送和接收长度之分。要读取或者发送N 个字节就
要封装N+1 个字节,第1 个字节是告诉设备我们要进行读还是写,后面的N 个字节才是我们
要读或者发送的数据。因为是读操作,因此在第31 行设置第一个数据bit7 位1,表示读操作。
第60~96 行,icm20608_write_regs 函数,向icm20608 连续写入多个寄存器数据。此函数和
icm20608_read_regs 函数区别不大。
第104~109 行,icm20608_read_onereg 函数,读取icm20608 指定寄存器数据。
第119~123 行,icm20608_write_onereg 函数,向icm20608 指定寄存器写入数据。
第131~143 行,icm20608_readdata 函数,读取icm20608 六轴传感器和温度传感器原始数
据值,应用程序读取icm20608 的时候这些传感器原始数据就会上报给应用程序。
第150~170 行,icm20608_reginit 函数,初始化icm20608,和我们spi 裸机实验里面的初始
化过程一样。
5、字符设备驱动框架
icm20608 的字符设备驱动框架如下:

示例代码62.5.2.6 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 的原始值转换为对应的实际值,因为会
涉及到浮点计算。

编写测试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。最终将传感器原始数据和得
到的实际值显示在终端上。

运行测试

编译驱动程序和测试APP

1、编译驱动程序
编写Makefile 文件,本章实验的Makefile 文件和第四十章实验基本一样,只是将obj-m 变
量的值改为“icm20608.o”,Makefile 内容如下所示:

示例代码62.6.1.1 Makefile 文件
1 KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
......
4 obj-m := icm20608.o
......
11 clean:
12 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

第4 行,设置obj-m 变量的值为“icm20608.o”。
输入如下命令编译出驱动模块文件:

make -j32

编译成功以后就会生成一个名为“icm20608.ko”的驱动模块文件。
2、编译测试APP
在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

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

运行测试

将上一小节编译出来icm20608.ko 和icm20608App 这两个文件拷贝到
rootfs/lib/modules/4.1.15 目录中,重启开发板,进入到目录lib/modules/4.1.15 中。输入如下命令
加载icm20608.ko 这个驱动模块。

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

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

./icm20608App /dev/icm20608

测试APP 会不断的从ICM20608 中读取数据,然后输出到终端上,如图62.6.2.1 所示:
在这里插入图片描述
可以看出,开发板静止状态下,Z 轴方向的加速度在1g 左右,这个就是重力加速度。对于
陀螺仪来讲,静止状态下三轴的角速度应该在0°/S 左右。ICM20608 内温度传感器采集到的温
度在30 多度左右,大家可以晃动一下开发板,这个时候陀螺仪和加速度计的值就会有变化。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux SPI 驱动实验 的相关文章

  • Fedora dnf 更新不起作用?

    当我尝试使用 update 命令更新 Fedora 22 时 sudo dnf update 我收到以下错误 错误 无法同步存储库 更新 的缓存 无法准备内部镜像列表 Curl 错误 6 无法解析主机名 无法解析主机 mirrors fed
  • Web 应用程序的带宽和流量模拟器?

    您能否建议如何创建一个测试环境来模拟 Web 应用程序中的各种类型的带宽和流量 或者也许是一个针对本地主机执行此操作的开源程序 我认为在编写网络应用程序时这是一个非常重要的主题 但这不是一个常见的主题 我能想象创建这种环境的唯一方法是在本地
  • X11 模式对话框

    如何使用 Xlib 在 X11 中创建模式对话框 模态对话框是一个位于应用程序其他窗口之上的窗口 就像瞬态窗口一样 并且拒绝将焦点给予应用程序的其他窗口 在 Windows 中 当试图从模态窗口夺取焦点时 模态也会通过闪 烁模态窗口的标题栏
  • 配置:错误:无法运行C编译的程序

    我正在尝试使用 Debian Wheezy 操作系统在我的 Raspberry Pi 上安装不同的软件 当我运行尝试配置软件时 我尝试安装我得到此输出 checking for C compiler default output file
  • linux下写入后崩溃

    如果我使用 write 将一些数据写入磁盘上的文件会发生什么 但我的应用程序在刷新之前崩溃了 如果没有系统故障 是否可以保证我的数据最终会刷新到磁盘 如果您正在使用write 并不是fwrite or std ostream write 那
  • 如何访问 mmaped /dev/mem 而不导致 Linux 内核崩溃?

    我有一个简单的程序 尝试访问用户空间中的物理内存 其中内核存储第一个结构页 在 64 位机器上 该地址是 内核虚拟地址 ffffea0000000000 物理地址 0000620000000000 我正在尝试通过用户空间中的 mmap 访问
  • 从c调用汇编函数

    我试图从 c 调用汇编函数 但我不断收到错误 text globl integrate type integrate function integrate push ebp mov esp ebp mov 0 edi start loop
  • dlopen 或 dlclose 未调用信号处理程序

    我在随机时间内收到分段错误 我注册了信号 但发生分段错误时未调用信号处理程序 include
  • 用于编辑 /etc/sudoers 文件的正则表达式模式

    我想删除 etc sudoers 文件中的 uncommnet 轮组 那么我应该使用什么正则表达式模式 cat etc sudoers Allows members of the sys group to run networking so
  • Fortran 中的共享库,最小示例不起作用

    我试图了解如何在 Linux 下的 Fortran 中动态创建和链接共享库 我有两个文件 第一个 liblol f90 看起来像这样 subroutine func print lol end subroutine func 我用它编译gf
  • 如何确定代码是否在信号处理程序上下文中运行?

    我刚刚发现有人正在从信号处理程序调用我编写的绝对不是异步信号安全的函数 所以 现在我很好奇 如何避免这种情况再次发生 我希望能够轻松确定我的代码是否在信号处理程序上下文中运行 语言是 C 但该解决方案不适用于任何语言吗 int myfunc
  • 如何设置Java线程的CPU核心亲和力?

    我搜索了以前关于类似主题的帖子 但找不到合适的答案 因此提出这个问题 非常感谢您帮助回答 我知道在 Linux 中通过任务集命令设置进程与特定 CPU 核心的关联性 但我想设置 Java 线程与特定 cpu 核心的亲和力 以便属于同一进程的
  • C++:Linux平台上的线程同步场景

    我正在为 Linux 平台实现多线程 C 程序 其中我需要类似于 WaitForMultipleObjects 的功能 在搜索解决方案时 我发现有一些文章描述了如何在 Linux 中实现 WaitForMultipleObjects 功能
  • 如何重命名 .tar.gz 文件而不提取内容并在 UBUNTU 中创建新的 .tar.gz 文件?

    我有一个命令将创建一个新的 tar gz现有文件中的文件 sudo tar zcvf Existing tar gz New tar gz 该命令将创建一个新的New tar gz从现有的文件Existing tar gz file 谁能告
  • 将node.js +expressjs应用程序的NODE_ENV设置为ubuntu下的守护进程

    我按照这些说明让守护进程正常工作 http kevin vanzonneveld net techblog article run nodejs as a service on ubuntu karmic http kevin vanzon
  • 用于读取文件的 Bash 脚本

    不知道为什么最后一行没有从脚本中删除 bin bash FILENAME 1 while read line do cut d f2 echo line done lt FILENAME cat file 1 test 2 test 3 t
  • ubuntu 的 CSS 更少(并且自动编译)? [关闭]

    很难说出这里问的是什么 这个问题是含糊的 模糊的 不完整的 过于宽泛的或修辞性的 无法以目前的形式得到合理的回答 如需帮助澄清此问题以便重新打开 访问帮助中心 help reopen questions 我尝试过 simples 但现在 l
  • 使用c在linux上分块读写

    我有一个 ASCII 文件 其中每一行都包含一个可变长度的记录 例如 Record 1 15 characters Record 2 200 characters Record 3 500 characters Record n X cha
  • 如何使用 go1.6.2 构建 linux 32 位

    有没有任何组合GOARCH and GOOS我可以设置哪些值来构建 ELF 32 位二进制文 件 GOOS linux and GOARCH 386 更多示例 架构 32 bit gt GOARCH 386 64 bit gt GOARCH
  • 在用户程序中使用 或在驱动程序模块代码中使用 ...这有关系吗?

    我正在开发一个设备驱动程序模块和关联的用户库来处理ioctl 来电 该库获取相关信息并将其放入一个结构中 该结构被传递到驱动程序模块中并在那里解压 然后进行处理 我省略了很多步骤 但这就是总体思路 一些数据通过结构体传递ioctl is u

随机推荐

  • C#使用Npgsql或SqlClient连接数据库

    目录 SqlClient连接SQL Server 安装SqlClient SqlConnection SqlCommand SqlConnection 和 SqlCommand的区别 Npgsql连接PostgreSQL 安装Npgsql
  • (java功能篇) jxl读取excel文本

    jxl是一个韩国人写的java操作excel的工具 在java的开源世界中 有两套比较有影响的API可供使用 一个是POI 一个是jExcelAPI 即jxl jxl功能相对POI比较弱一点 但jExcelAPI对中文支持非常好 API是纯
  • Restful API 接口设计标准以及规范

    RESTful概念 理解和评估以网络为基础的应用软件的架构设计 得到一个功能强 性能好 适宜通信的架构 REST指的是一组架构约束条件和原则 如果一个架构符合REST的约束条件和原则 我们就称它为RESTful架构 REST本身并没有创造新
  • 2021-11-15 入门前端VScode必装的常用插件, HTML+CSS常用属性速记.

    Html Css 学习 一 VSCode常用 1 VScode常用快捷键 新建文件 ctrl N 切换自动换行 alt Z 复制当前行 ctrl C ctrl V 执行在默认浏览器 alt B 设置vscode文件折叠属性 打开设置 gt
  • linux火狐无法连接网络,Ubuntu上火狐浏览器无法上网的解决方法

    网上有的方法是在浏览器中选择更新 后来找到了更加直接好用的方法 只需要几行命令就可以 1 在终端中输入sudo apt get update 如果在这一步出现错误 显示暂时不能解析域名的情况 可能是DNS服务器地址需要设置 可执行下列步骤
  • macOS上WebM转MP4文件

    目录 编辑 提出程序要求 安装Homebrew和ffmpeg的步骤 方法 原代码以下 原代码以上 注意 提出程序要求 在macOS上用Script Editor编写一个程序 使用ffmpeg命令来把WebM转换成MP4文件 说明 ffmpe
  • 纯CSS实现呼吸灯&文字&毛玻璃效果

    闲来无事 下一步在平时中应该考虑如何让页面动起来 增加视觉效果 说一说呼吸灯效果吧 我觉得可以用到 就一个brightness滤镜 用动画动起来 文字效果 在页面中系统名称处可以使用 让页面看起来更有活力一点 CSS真是今非昔比了 唉 水一
  • 基于阿里云的maven配置settings.xml

  • 解读升压电路(BOOST)与降压电路(BUCK)

    1 BUCK和BOOST电路详解 541板哥的博客 CSDN博客 buck电路和boost电路 2 升压电路 BOOST 与降压电路 BUCK FFT变换的博客 CSDN博客 buckboost升降压电路原理 3 BUCK BOOST电路原
  • C# replace替换用法

    1 在后台vs使用 content content Replace 545 666 把545替换为666 content content Replace r n 数据显示没有回车换行时 可以使用换行 2在html里使用 content co
  • 数据挖掘的之matplotlib

    数据挖掘的之matplotlib 文章目录 数据挖掘的之matplotlib 什么是Matplotlib 画二维图表的python库 2 1 3 实现一个简单的Matplotlib画图 完善原始折线图2 图像层 2 2 4 多个坐标系显示
  • web.xml 中配置元素错误提示 The content of element type “web-app“ must match “(icon?,display- name?...

    配置xml xml 文件 IDE提示错误信息如下 The content of element type web app must match icon display name description distributable cont
  • 听伯克利博士给你讲解Llama 2的技术细节

    来源 图灵人工智能 文章翻译自 Llama 2 an incredible open LLM 原文作者 NATHAN LAMBERT Meta 近日发表了 Llama 2 的论文 而来自伯克利大学的人工智能专业博士Nathan Lamber
  • 集成电路模拟版图入门-版图基础学习笔记(五)

    第四部分 版图的艺术 模拟版图和数字版图的首要目标 首先考虑的三个问题 匹配 3 1 匹配中心思想 3 2 匹配问题 3 3 如何匹配 3 4 MOS管 3 5 电阻 3 6 电容 3 7 匹配规则 寄生效应 4 1 寄生的产生 4 2 寄
  • Unity 关于Plugins放入dll在IOS端运行异常的bug

    好久没写博客了 今天被一个问题困扰了好久 网上了查不到相应的解决方案 自行解决后才觉得还是写写博客吧 说不定能帮助别人少走弯路 首先说明我的运行环境 MAC系统版本号10 13 4 Unity版本号2018 1 0 解释下当前环境的问题 首
  • iOS App上架流程

    前言 作为一名IOS开发者 把开发出来的App上传到App Store是必须的 下面就来详细介绍下具体流程 1 打开苹果开发者中心 https developer apple com 打开后点击 Member Center 如果你的电脑没有
  • vue子组件的数据处理后竟然影响了父组件的数据,导致父级的数据也同步变更了的问题解决

    文章目录 问题描述 问题解决 问题描述 需要在子组件中处理来自父组件的数据 将父级的千分位去掉 转为纯数字 const originalData 16 33 17 1 1 14 23 21 19 76 40 16 37 052 50 2 3
  • ESP8266红外检测安全系统

    我们每个人都想知道不在家时 家中所发生的事情 这与安全性或天然气 火或水等关键要素有关 知道发生了什么事情的瞬间对于将最终损坏降至最低至关重要 我们将重点介绍PIR传感器 但是相同的原理也适用于其他传感器 例如湿度 气体或烟雾传感器 无源红
  • 知云翻译选中一段只能翻译出一句话

    解决办法 换个翻译引擎
  • Linux SPI 驱动实验

    目录 Linux 下SPI 驱动框架简介 SPI 主机驱动 SPI 设备驱动 SPI 设备和驱动匹配过程 I MX6U SPI 主机驱动分析 SPI 设备驱动编写流程 SPI 设备信息描述 SPI 设备数据收发处理流程 硬件原理图分析 试验