您当前的位置: 首页 >  嵌入式

正点原子

暂无认证

  • 2浏览

    0关注

    382博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

【正点原子Linux连载】第六十二章 Linux SPI驱动实验 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0

正点原子 发布时间:2021-09-24 10:12:29 ,浏览量:2

1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html 3)对正点原子Linux感兴趣的同学可以加群讨论:935446741 4)关注正点原子公众号,获取最新资料更新 在这里插入图片描述 第六十二章 Linux SPI驱动实验

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

62.1 Linux下SPI驱动框架简介 SPI驱动框架和I2C很类似,都分为主机控制器驱动和设备驱动,主机控制器也就是SOC的SPI控制器接口。比如在裸机篇中的《第二十七章 SPI实验》,我们编写了bsp_spi.c和bsp_spi.h这两个文件,这两个文件是I.MX6U的SPI控制器驱动,我们编写好SPI控制器驱动以后就可以直接使用了,不管是什么SPI设备,SPI控制器部分的驱动都是一样,我们的重点就落在了种类繁多的SPI设备驱动。 62.1.1 SPI主机驱动 SPI主机驱动就是SOC的SPI控制器驱动,类似I2C驱动里面的适配器驱动。Linux内核使用spi_master表示SPI主机驱动,spi_master是个结构体,定义在include/linux/spi/spi.h文件中,内容如下(有缩减):

示例代码62.1.1.1 spi_master结构体
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。 62.1.2 SPI设备驱动 spi设备驱动也和i2c设备驱动也很类似,Linux内核使用spi_driver结构体来表示spi设备驱动,我们在编写SPI设备驱动的时候需要实现spi_driver。spi_driver结构体定义在include/linux/spi/spi.h文件中,结构体内容如下:

示例代码62.1.1.2 spi_driver结构体
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注册示例程序如下:

示例代码62.1.1.3 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。

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

示例代码62.1.3.1 spi_bus_type结构体
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,函数内容如下:
示例代码62.1.3.2 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成员变量是否相等。 62.2 I.MX6U SPI主机驱动分析 和I2C的适配器驱动一样,SPI主机驱动一般都由SOC厂商编写好了,打开imx6ull.dtsi文件,找到如下所示内容:

示例代码62.2.1 imx6ull.dtsi文件中的ecspi3节点内容
1  ecspi3: ecspi@02010000 {
2      #address-cells = ;
3      #size-cells = ;
4      compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
5      reg = ;
6      interrupts = ;
7      clocks = ,
8           ;
9      clock-names = "ipg", "per";
10     dmas = , ;
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的适配器驱动程序类似。

62.3 SPI设备驱动编写流程 62.3.1 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 = ;
310     cs-gpios = ;
311     pinctrl-names = "default";
312     pinctrl-0 = ;
313     status = "okay";
314 
315     flash: m25p80@0 {
316         #address-cells = ;
317         #size-cells = ;
318         compatible = "st,m25p32";
319         spi-max-frequency = ;
320         reg = ;
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中的内容即可。

62.3.2 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;
}   

62.4 硬件原理图分析 本章实验硬件原理图参考26.2小节即可。 62.5 试验程序编写 本实验对应的例程路径为:开发板光盘-> 2、Linux驱动例程-> 22_spi。 62.5.1 修改设备树 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 = ;
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 = ;
3      cs-gpio = ; /* cant't use cs-gpios! */
4      pinctrl-names = "default";
5      pinctrl-0 = ;
6      status = "okay";
7  
8      spidev: icm20608@0 {
9          compatible = "alientek,icm20608";
10         spi-max-frequency = ;
11         reg = ;
12     };
13 };
第2行,设置当前片选数量为1,因为就只接了一个ICM20608。
第3行,注意!这里并没有用到“cs-gpios”属性,而是用了一个自己定义的“cs-gpio”属性,因为我们要自己控制片选引脚。如果使用“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系统。

62.5.2 编写ICM20608驱动 新建名为“22_spi”的文件夹,然后在22_spi文件夹里面创建vscode工程,工作区命名为“spi”。工程创建好以后新建icm20608.c和icm20608reg.h这两个文件,icm20608.c为ICM20608的驱动代码,icm20608reg.h是ICM20608寄存器头文件。先在icm20608reg.h中定义好ICM20608的寄存器,输入如下内容(有省略,完整的内容请参考例程):

示例代码62.5.2.1 icm20608reg.h文件内容
1  #ifndef ICM20608_H
2  #define ICM20608_H
3  /***************************************************************
4  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
5  文件名  		: icm20608reg.h
6  作者       	: 左忠凯
7  版本       	: V1.0
8  描述       	: ICM20608寄存器地址描述头文件
9  其他       	: 无
10 论坛       	: www.openedv.com
11 日志       	: 初版V1.0 2019/9/2 左忠凯创建
12 ***************************************************************/
13 #define ICM20608G_ID         			0XAF    /* ID值 */
14 #define ICM20608D_ID         			0XAE    /* ID值 */
15 
16 /* ICM20608寄存器 
17  *复位后所有寄存器地址都为0,除了
18  *Register 107(0X6B) Power Management 1  	= 0x40
19  *Register 117(0X75) WHO_AM_I            		= 0xAF或0xAE
20  */
21 /* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
22 #define  ICM20_SELF_TEST_X_GYRO   	0x00
23 #define  ICM20_SELF_TEST_Y_GYRO    	0x01
24 #define  ICM20_SELF_TEST_Z_GYRO    	0x02
25 #define  ICM20_SELF_TEST_X_ACCEL   	0x0D
26 #define  ICM20_SELF_TEST_Y_ACCEL   	0x0E
27 #define  ICM20_SELF_TEST_Z_ACCEL   	0x0F
......
80 /* 加速度静态偏移 */
81 #define  ICM20_XA_OFFSET_H           	0x77
82 #define  ICM20_XA_OFFSET_L           	0x78
83 #define  ICM20_YA_OFFSET_H           	0x7A
84 #define  ICM20_YA_OFFSET_L           	0x7B
85 #define  ICM20_ZA_OFFSET_H           	0x7D
86 #define  ICM20_ZA_OFFSET_L           	0x7E
87 
88 #endif
接下来继续编写icm20608.c文件,因为icm20608.c文件内容比较长,因此这里就将其分开来讲解。
1、icm20608设备结构体创建
首先创建一个icm20608设备结构体,如下所示:
示例代码62.5.2.2 icm20608设备结构体创建
1  #include 
2  #include 
3  #include 
......
22 #include 
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  	: 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 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] 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的原始值转换为对应的实际值,因为会涉及到浮点计算。

62.5.3 编写测试APP 新建icm20608App.c文件,然后在里面输入如下所示内容:

示例代码62.5.3.1 icm20608App.c文件代码
1  #include "stdio.h"
2  #include "unistd.h"
3  #include "sys/types.h"
4  #include "sys/stat.h"
5  #include "sys/ioctl.h"
6  #include "fcntl.h"
7  #include "stdlib.h"
8  #include "string.h"
9  #include 
10 #include 
11 #include 
12 #include 
13 #include 
14 /***************************************************************
15 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
16 文件名   		: icm20608App.c
17 作者       	: 左忠凯
18 版本       	: V1.0
19 描述       	: icm20608设备测试APP。
20 其他       	: 无
21 使用方法 	 	:./icm20608App /dev/icm20608
22 论坛       	: www.openedv.com
23 日志       	: 初版V1.0 2019/9/20 左忠凯创建
24 ***************************************************************/
25 
26 /*
27  * @description 	: main主程序
28  * @param - argc 	: argv数组元素个数
29  * @param - argv  	: 具体参数
30  * @return         	: 0 成功;其他 失败
31  */
32 int main(int argc, char *argv[])
33 {
34  	int fd;
35  	char *filename;
36 		signed int databuf[7];
37  	unsigned char data[14];
38  	signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
39  	signed int accel_x_adc, accel_y_adc, accel_z_adc;
40  	signed int temp_adc;
41 
42  	float gyro_x_act, gyro_y_act, gyro_z_act;
43  	float accel_x_act, accel_y_act, accel_z_act;
44  	float temp_act;
45 
46  	int ret = 0;
47 
48  	if (argc != 2) {
49      	printf("Error Usage!\r\n");
50      	return -1;
51  	}
52 
53  	filename = argv[1];
54  	fd = open(filename, O_RDWR);
55  	if(fd             
关注
打赏
1665308814
查看更多评论
0.1239s