Linux驱动.之 spi 驱动框架,时序,裸机驱动示例,app应用测试(一)

本文详细介绍了LinuxSPI驱动的结构,包括SPI主机驱动(SPI总线驱动)和SPI设备驱动,以及如何通过设备树来描述SPI设备信息。SPI驱动的分离和分层思想使得驱动开发更加高效和稳定。文章还展示了如何编写和注册SPI驱动,以及如何使用设备树来配置SPI设备节点。
摘要由CSDN通过智能技术生成

原文链接,正点原子:https://blog.csdn.net/weixin_55796564/article/details/119925782
原文链接:https://blog.csdn.net/weixin_47397155/article/details/122914110

目录、
1、spi,裸机开发,驱动六轴传感器
2、spi时序
3、linux spi驱动框架

一、
1、spi 裸机驱动

同I2C一样,SPI是很常用的通信接口,也可以通过SPI来连接众多的传感器。相比I2C接口,SPI接口的通信速度很快,I2C最多400KHz,但是SPI可以到达几十MHz。I.MX6U也有4个SPI接口,可以通过这4个SPI接口来连接一些SPI外设。I.MX6U-ALPHA使用SPI3接口连接了一个六轴传感器ICM-20608,本章我们就来学习如何使用I.MX6U的SPI接口来驱动ICM-20608,读取ICM-20608的六轴数据。

27.1.1 SPI简介
上一章我们讲解了I2C,I2C是串行通信的一种,只需要两根线就可以完成主机和从机之间的通信,但是I2C的速度最高只能到400KHz,如果对于访问速度要求比价高的话I2C就不适合了。本章我们就来学习一下另外一个和I2C一样广泛使用的串行通信:SPI,SPI全称是Serial Perripheral Interface,也就是串行外围设备接口。SPI是Motorola公司推出的一种同步串行接口技术,是一种高速、全双工的同步通信总线,SPI时钟频率相比I2C要高很多,最高可以工作在上百MHz。SPI以主从方式工作,通常是有一个主设备和一个或多个从设备,一般SPI需要4根线,但是也可以使用三根线(单向传输),本章我们讲解标准的4线SPI,这四根线如下:

①、CS/SS,Slave Select/Chip Select,这个是片选信号线,用于选择需要进行通信的从设备。I2C主机是通过发送从机设备地址来选择需要进行通信的从机设备的,SPI主机不需要发送从机设备,直接将相应的从机设备片选信号拉低即可。
②、SCK,Serial Clock,串行时钟,和I2C的SCL一样,为SPI通信提供时钟。
③、MOSI/SDO,Master Out Slave In/Serial Data Output,简称主出从入信号线,这根数据线只能用于主机向从机发送数据,也就是主机输出,从机输入。
④、MISO/SDI,Master In Slave Out/Serial Data Input,简称主入从出信号线,这根数据线只能用户从机向主机发送数据,也就是主机输入,从机输出。

SPI通信都是由主机发起的,主机需要提供通信的时钟信号。主机通过SPI线连接多个从设备的结构如图27.1.1.1所示:
在这里插入图片描述
图27.1.1.1 SPI设备连接图

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

跟I2C一样,SPI也是有时序图的,以CPOL=0,CPHA=0这个工作模式为例,SPI进行全双工通信的时序如图27.1.1.3所示:
在这里插入图片描述
图27.1.1.3 SPI时序图
从图27.1.1.3可以看出,**SPI的时序图很简单,不像I2C那样还要分为读时序和写时序,因为SPI是全双工的,所以读写时序可以一起完成。**图27.1.1.3中,CS片选信号先拉低,选中要通信的从设备,然后通过MOSI和MISO这两根数据线进行收发数据,MOSI数据线发出了0XD2这个数据给从设备,同时从设备也通过MISO线给主设备返回了0X66这个数据。这个就是SPI时序图。
关于SPI就讲解到这里,接下来我们看一下I.MX6U自带的SPI外设:ECSPI。

27.1.2 I.MX6U ECSPI简介
I.MX6U自带的SPI外设叫做ECSPI,全称是Enhanced Configurable Serial Peripheral Interface,别看前面加了个“EC”就以为和标准SPI有啥不同的,其实就是SPI。ECSPI有6432个接收FIFO(RXFIFO)和6432个发送FIFO(TXFIFO),ECSPI特性如下:
①、全双工同步串行接口。
②、可配置的主/从模式。
③、四个片选信号,支持多从机。
④、发送和接收都有一个32x64的FIFO。
⑤、片选信号SS/CS,时钟信号SCLK极性可配置。
⑥、支持DMA。
I.MX6U的ECSPI可以工作在主模式或从模式,本章我们使用主模式,I.MX6U有4个ECSPI,每个ECSPI支持四个片选信号,也就说,如果你要使用ECSPI的硬件片选信号的话,一个ECSPI可以支持4个外设。如果不使用硬件的片选信号就可以支持无数个外设,本章实验我们不使用硬件片选信号,因为硬件片选信号只能使用指定的片选IO,软件片选的话可以使用任意的IO。
我们接下来看一下ECSPI的几个重要的寄存器,首先看一下ECSPIx_CONREG(x=1~4)寄存器,这是ECSPI的控制寄存器,此寄存器结构如 图27.1.2.1所示:
在这里插入图片描述
寄存器ECSPIx_CONREG各位含义如下:
BURST_LENGTH(bit31:24):突发长度,设置SPI的突发传输数据长度,在一次SPI发送中最大可以发送212bit数据。可以设置0X0000XFFF,分别对应1212bit。我们一般设置突发长度为一个字节,也就是8bit,BURST_LENGTH=7。
CHANNEL_SELECT(bit19:18):SPI通道选择,一个ECSPI有四个硬件片选信号,每个片选信号是一个硬件通道,虽然我们本章实验使用的软件片选,但是SPI通道还是要选择的。可设置为03,分别对应通道03。I.MX6U-ALPHA开发板上的ICM-20608的片选信号接的是ECSPI3_SS0,也就是ECSPI3的通道0,所以本章实验设置为0。
DRCTL(bit17:16):SPI的SPI_RDY信号控制位,用于设置SPI_RDY信号,为0的话不关心SPI_RDY信号;为1的话SPI_RDY信号为边沿触发;为2的话SPI_DRY是电平触发。
PRE_DIVIDER(bit15:12):SPI预分频,ECSPI时钟频率使用两步来完成分频,此位设置的是第一步,可设置015,分别对应116分频。
POST_DIVIDER(bit11:8):SPI分频值,ECSPI时钟频率的第二步分频设置,分频值为2^POST_DIVIDER。
CHANNEL_MODE(bit7:4):SPI通道主/从模式设置,CHANNEL_MODE[3:0]分别对应SPI通道3~0,为0的话就是设置为从模式,如果为1的话就是主模式。比如设置为0X01的话就是设置通道0为主模式。
SMC(bit3):开始模式控制,此位只能在主模式下起作用,为0的话通过XCH位来开启SPI突发访问,为1的话只要向TXFIFO写入数据就开启SPI突发访问。
XCH(bit2):此位只在主模式下起作用,当SMC为0的话此位用来控制SPI突发访问的开启。
HT(bit1):HT模式使能位,I.MX6ULL不支持。
EN(bit0):SPI使能位,为0的话关闭SPI,为1的话使能SPI。
接下来看一下寄存器ECSPIx_CONFIGREG,这个也是ECSPI的配置寄存器,此寄存器结构如图27.1.2.2所示:
在这里插入图片描述
图27.1.2.2 寄存器ECSPIx_CONFIGREG结构
寄存器ECSPIx_CONFIGREG用到的重要位如下:
HT_LENGTH(bit28:24):HT模式下的消息长度设置,I.MX6ULL不支持。
SCLK_CTL(bit23:20):设置SCLK信号线空闲状态电平,SCLK_CTL[3:0]分别对应通道3~0,为0的话SCLK空闲状态为低电平,为1的话SCLK空闲状态为高电平。
DATA_CTL(bit19:16):设置DATA信号线空闲状态电平,DATA_CTL[3:0]分别对应通道3~0,为0的话DATA空闲状态为高电平,为1的话DATA空闲状态为低电平。
SS_POL(bit15:12):设置SPI片选信号极性设置,SS_POL[3:0]分别对应通道3~0,为0的话片选信号低电平有效,为1的话片选信号高电平有效。
SCLK_POL(bit7:4):SPI时钟信号极性设置,也就是CPOL,SCLK_POL[3:0]分别对应通道3~0,为0的话SCLK高电平有效(空闲的时候为低电平),为1的话SCLK低电平有效(空闲的时候为高电平)。
SCLK_PHA(bit3:0):SPI时钟相位设置,也就是CPHA,SCLK_PHA[3:0]分别对应通道3~0,为0的话串行时钟的第一个跳变沿(上升沿或下降沿)采集数据,为1的话串行时钟的第二个跳变沿(上升沿或下降沿)采集数据。
通过SCLK_POL和SCLK_PHA可以设置SPI的工作模式。
接下来看一下寄存器ECSPIx_PERIODREG,这个是ECSPI的采样周期寄存器,此寄存器结构如图27.1.2.3所示:

在这里插入图片描述
图27.1.2.3 寄存器ECSPIx_PERIODREG结构
寄存器ECSPIx_PERIODREG用到的重要位如下:
CSD_CTL(bit21:16):片选信号延时控制位,用于设置片选信号和第一个SPI时钟信号之间的时间间隔,范围为0~63。
CSRC(bit15):SPI时钟源选择,为0的话选择SPI CLK为SPI的时钟源,为1的话选择32.768KHz的晶振为SPI时钟源。我们一般选择SPI CLK作为SPI时钟源,SPI CLK时钟来源如图27.1.2.4所示:
在这里插入图片描述
图27.1.2.4 SPI CLK时钟源
图27.1.2.4中各部分含义如下:
①、这是一个选择器,用于选择根时钟源,由寄存器CSCDR2的位ECSPI_CLK_SEL来控制,为0的话选择pll3_60m作为ECSPI根时钟源。为1的话选择osc_clk作为ECSPI时钟源。本章我们选择pll3_60m作为ECSPI根时钟源。
②、ECSPI时钟分频值,由寄存器CSCDR2的位ECSPI_CLK_PODF来控制,分频值为2^ECSPI_CLK_PODF。本章我们设置为0,也就是1分频。
③、最终进入ECSPI的时钟,也就是SPI CLK=60MHz。
SAMPLE_PERIO:采样周期寄存器,可设置为00X7FFF分别对应032767个周期。
接下来看一下寄存器ECSPIx_STATREG,这个是ECSPI的状态寄存器,此寄存器结构如图27.1.2.5所示:

在这里插入图片描述
图27.1.2.5 寄存器ECSPIx_STATREG寄存器
寄存器ECSPIx_STATREG用到的重要位如下:
TC(bit7):传输完成标志位,为0表示正在传输,为1表示传输完成。
RO(bit6):RXFIFO溢出标志位,为0表示RXFIFO无溢出,为1表示RXFIFO溢出。
RF(bit5):RXFIFO空标志位,为0表示RXFIFO不为空,为1表示RXFIFO为空。
RDR(bit4):RXFIFO数据请求标志位,此位为0表示RXFIFO里面的数据不大于RX_THRESHOLD,此位为1的话表示RXFIFO里面的数据大于RX_THRESHOLD。
RR(bit3):RXFIFO就绪标志位,为0的话RXFIFO没有数据,为1的话表示RXFIFO中至少有一个字的数据。
TF(bit2):TXFIFO满标志位,为0的话表示TXFIFO不为满,为1的话表示TXFIFO为满。
TDR(bit1):TXFIFO数据请求标志位,为0表示TXFIFO中的数据大于TX_THRESHOLD,为1表示TXFIFO中的数据不大于TX_THRESHOLD。
TE(bit0):TXFIFO空标志位,为0表示TXFIFO中至少有一个字的数据,为1表示TXFIFO为空。
最后就是两个数据寄存器,ECSPIx_TXDATA和ECSPIx_RXDATA,这两个寄存器都是32位的,如果要发送数据就向寄存器ECSPIx_TXDATA写入数据,读取及存取ECSPIx_RXDATA里面的数据就可以得到刚刚接收到的数据。
关于ECSPI的寄存器就介绍到这里,关于这些寄存器详细的描述,请参考《I.MX6ULL参考手册》第805页的20.7小节

27.1.3 ICM-20608简介
ICM-20608是InvenSense出品的一款6轴MEMS传感器,包括3轴加速度和3轴陀螺仪。ICM-20608尺寸非常小,只有3x3x0.75mm,采用16P的LGA封装。ICM-20608内部有一个512字节的FIFO。陀螺仪的量程范围可以编程设置,可选择±250,±500,±1000和±2000°/s,加速度的量程范围也可以编程设置,可选择±2g,±4g,±8g和±16g。陀螺仪和加速度计都是16位的ADC,并且支持I2C和SPI两种协议,使用I2C接口的话通信速度最高可以达到400KHz,使用SPI接口的话通信速度最高可达到8MHz。I.MX6U-ALPHA开发板上的ICM-20608通过SPI接口和I.MX6U连接在一起。ICM-20608特性如下:
①、陀螺仪支持X,Y和Z三轴输出,内部集成16位ADC,测量范围可设置:±250,±500,±1000和±2000°/s。
②、加速度计支持X,Y和Z轴输出,内部集成16位ADC,测量范围可设置:±2g,±4g,±4g,±8g和±16g。
③、用户可编程中断。
④、内部包含512字节的FIFO。
⑤、内部包含一个数字温度传感器。
⑥、耐10000g的冲击。
⑦、支持快速I2C,速度可达400KHz。
⑧、支持SPI,速度可达8MHz。
ICM-20608的3轴方向如图27.1.3.1所示:

在这里插入图片描述

如果使用IIC接口的话ICM-20608的AD0引脚决定I2C设备从地址的最后一位,如果AD0为0的话ICM-20608从设备地址是0X68,如果AD0为1的话ICM-20608从设备地址为0X69。本章我们使用SPI接口,跟上一章使用AP3216C一样,ICM-20608也是通过读写寄存器来配置和读取传感器数据,使用SPI接口读写寄存器需要16个时钟或者更多(如果读写操作包括多个字节的话),第一个字节包含要读写的寄存器地址,寄存器地址最高位是读写标志位,如果是读的话寄存器地址最高位要为1,如果是写的话寄存器地址最高位要为0,剩下的7位才是实际的寄存器地址,寄存器地址后面跟着的就是读写的数据。表27.1.3.1列出了本章实验用到的一些寄存器和位,关于ICM-20608的详细寄存器和位的介绍请参考ICM-20608的寄存器手册:

寄存器地址 位 寄存器功能 描述
0X19 SMLPRT_DIV[7:0] 输出速率设置 设置输出速率,输出速率计算公式如下:
SAMPLE_RATE=INTERNAL_SAMPLE_RATE/
(1 + SMPLRT_DIV)
0X1A DLPF_CFG[2:0] 芯片配置 设置陀螺仪低通滤波。可设置0~7。
0X1B FS_SEL[1:0] 陀螺仪量程设置 0:±250dps;1:±500dps;2:±1000dps
3:±2000dps
0X1C ACC_FS_SEL[1:0] 加速度计量程设置 0:±2g;1:±4g;2:±8g;3:±16g
0X1D A_DLPF_CFG[2:0] 加速度计低通滤波设置 设置加速度计的低通滤波,可设置0~7。
0X1E GYRO_CYCLE[7] 陀螺仪低功耗使能 0:关闭陀螺仪的低功耗功能。
1:使能陀螺仪的低功耗功能。
0X23 TEMP_FIFO_EN[7] FIFO使能控制 1:使能温度传感器FIFO。
0:关闭温度传感器FIFO。
XG_FIFO_EN[6] 1:使能陀螺仪X轴FIFO。
0:关闭陀螺仪X轴FIFO。
YG_FIFO_EN[5] 1:使能陀螺仪Y轴FIFO。
0:关闭陀螺仪Y轴FIFO。
ZG_FIFO_EN[4] 1:使能陀螺仪Z轴FIFO。
0:关闭陀螺仪Z轴FIFO。
ACCEL_FIFO_EN[3] 1:使能加速度计FIFO。
0:关闭加速度计FIFO。
0X3B ACCEL_XOUT_H[7:0] 数据寄存器 加速度X轴数据高8位
0X3C ACCEL_XOUT_L[7:0] 加速度X轴数据低8位
0X3D ACCEL_YOUT_H[7:0] 加速度Y轴数据高8位
0X3E ACCEL_YOUT_L[7:0] 加速度Y轴数据低8位
0X3F ACCEL_ZOUT_H[7:0] 加速度Z轴数据高8位
0X40 ACCEL_ZOUT_L[7:0] 加速度Z轴数据低8位
0X41 TEMP_OUT_H[7:0] 温度数据高8位
0X42 TEMP_OUT_L[7:0] 温度数据低8位
0X43 GYRO_XOUT_H[7:0] 陀螺仪X轴数据高8位
0X44 GYRO_XOUT_L[7:0] 陀螺仪X轴数据低8位
0X45 GYRO_YOUT_H[7:0] 陀螺仪Y轴数据高8位
0X46 GYRO_YOUT_L[7:0] 陀螺仪Y轴数据低8位
0X47 GYRO_ZOUT_H[7:0] 陀螺仪Z轴数据高8位
0X48 GYRO_ZOUT_L[7:0] 陀螺仪Z轴数据低8位
0X6B DEVICE_RESET[7] 电源管理寄存器1 1:复位ICM-20608。
SLEEP[6] 0:退出休眠模式;1,进入休眠模式
0X6C STBY_XA[5] 电源管理寄存器2 0:使能加速度计X轴。
1:关闭加速度计X轴。
STBY_YA[4] 0:使能加速度计Y轴。
1:关闭加速度计Y轴。
STBY_ZA[3] 0:使能加速度计Z轴。
1:关闭加速度计Z轴。
STBY_XG[2] 0:使能陀螺仪X轴。
1:关闭陀螺仪X轴。
STBY_YG[1] 0:使能陀螺仪Y轴。
1:关闭陀螺仪Y轴。
STBY_ZG[0] 0:使能陀螺仪Z轴。
1:关闭陀螺仪Z轴。
0X75 WHOAMI[7:0] ID寄存器,ICM-20608G的ID为0XAF,
ICM-20608D的ID为0XAE。
表27.1.3.1 ICM-20608寄存器表
ICM-20608的介绍就到这里,关于ICM-20608的详细介绍请参考ICM-20608的数据手册和寄存器手册。
27.2 硬件原理分析
本试验用到的资源如下:
①、指示灯LED0。
②、 RGB LCD屏幕。
③、ICM20608
④、串口

ICM-20608是在I.MX6U-ALPHA开发板底板上,原理图如图27.2.1所示:
在这里插入图片描述
图27.2.1 ICM-20608原理图

27.3 实验程序编写
本章实验在上一章例程的基础上完成,更改工程名字为“icm20608”,然后在bsp文件夹下创建名为“spi”和“icm20608”的文件。在bsp/spi中新建bsp_spi.c和bsp_spi.h这两个文件,在bsp/icm20608中新建bsp_icm20608.c和bsp_icm20608.h这两个文件。bsp_spi.c和bsp_spi.h是I.MX6U的SPI文件,bsp_icm20608.c和bsp_icm20608.h是ICM20608的驱动文件。在bsp_spi.h中输入如下内容:
示例代码27.3.1 bsp_spi.h文件代码

1  #ifndef _BSP_SPI_H
2  #define _BSP_SPI_H
3  /***************************************************************
4  Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
5  文件名    : bsp_spi.h
6  作者      : 左忠凯
7  版本      : V1.0
8  描述      : SPI驱动头文件。
9  其他      : 无
10 论坛      : www.openedv.com
11 日志      : 初版V1.0 2019/1/17 左忠凯创建
12 ***************************************************************/
13 #include "imx6ul.h"
14 
15 /* 函数声明 */
16 void spi_init(ECSPI_Type *base);
17 unsigned char spich0_readwrite_byte(ECSPI_Type *base, 
unsigned char txdata);
18 #endif
	文件bsp_spi.h内容很简单,就是函数声明。在文件bsp_spi.c中输入如下内容:
示例代码27.3.2 bsp_spi.c文件代码
/***************************************************************
Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
文件名   : bsp_spi.c
作者     : 左忠凯
版本     : V1.0
描述     : SPI驱动文件。
其他     : 无
论坛     : www.openedv.com
日志     : 初版V1.0 2019/1/17 左忠凯创建
***************************************************************/
1  #include "bsp_spi.h"
2  #include "bsp_gpio.h"
3  #include "stdio.h"
4  
5  /*
6   * @description  	: 初始化SPI
7   * @param - base 	: 要初始化的SPI
8   * @return         	: 无
9   */
10 void spi_init(ECSPI_Type *base)
11 {
12  	/* 配置CONREG寄存器
13   	 * bit0 :     	1   使能ECSPI
14   	 * bit3 :     	1   当向TXFIFO写入数据以后立即开启SPI突发。
15   	 * bit[7:4]:	0001 SPI通道0主模式,根据实际情况选择,开发板上的
16   	 *                     ICM-20608接在SS0上,所以设置通道0为主模式
17   	 * bit[19:18]:	00 	 选中通道0(其实不需要,因为片选信号我们我们自己控制)
18  	 * bit[31:20]:	0x7 突发长度为8个bit。 
19   	 */
20  	base->CONREG = 0; 							/* 先清除控制寄存器 */
21  	base->CONREG |= (1 << 0) | (1 << 3) | (1 << 4) | (7 << 20); 
22 
23  	/*
24      * ECSPI通道0设置,即设置CONFIGREG寄存器
25      * bit0:  0 通道0 PHA为0
26      * bit4:  0 通道0 SCLK高电平有效
27      * bit8:  0 通道0片选信号 当SMC为1的时候此位无效
28      * bit12: 0 通道0 POL为0
29      * bit16: 0 通道0 数据线空闲时高电平
30      * bit20: 0 通道0 时钟线空闲时低电平
31   	*/
32  	base->CONFIGREG = 0;        			/* 设置通道寄存器 */
33  
34  	/*  
35      * ECSPI通道0设置,设置采样周期
36      * bit[14:0] : 0X2000  采样等待周期,比如当SPI时钟为10MHz的时候
37      *                 	0X2000就等于1/10000 * 0X2000 = 0.8192ms,也就是
38      *               		连续读取数据的时候每次之间间隔0.8ms
39      * bit15  :     0  采样时钟源为SPI CLK
40      * bit[21:16]: 0  片选延时,可设置为0~63
41   	*/
42  	base->PERIODREG = 0X2000;       /* 设置采样周期寄存器 */
43 
44  	/*
45      * ECSPI的SPI时钟配置,SPI的时钟源来源于pll3_sw_clk/8=480/8=60MHz
46      * SPI CLK = (SourceCLK / PER_DIVIDER) / (2^POST_DIVEDER)
47      * 比如我们现在要设置SPI时钟为6MHz,那么设置如下:
48      * PER_DIVIDER = 0X9。
49      * POST_DIVIDER = 0X0。
50      * SPI CLK = 60000000/(0X9 + 1) = 60000000=6MHz
51   	*/
52  	base->CONREG &= ~((0XF << 12) | (0XF << 8)); /* 清除以前的设置 */
53  	base->CONREG |= (0X9 << 12);          /* 设置SPI CLK = 6MHz */
54 }
55 
56 /*
57  * @description   	: SPI通道0发送/接收一个字节的数据
58  * @param - base 	: 要使用的SPI
59  * @param – txdata	: 要发送的数据
60  * @return         	: 无
61  */
62 unsigned char spich0_readwrite_byte(ECSPI_Type *base, 
unsigned char txdata)
63 { 
64  	uint32_t  spirxdata = 0;
65  	uint32_t  spitxdata = txdata;
66 
67     	/* 选择通道0 */
68  	base->CONREG &= ~(3 << 18);
69 	 	base->CONREG |= (0 << 18);
70 
71      while((base->STATREG & (1 << 0)) == 0){} /* 等待发送FIFO为空 */
72      base->TXDATA = spitxdata;
73  
74  	while((base->STATREG & (1 << 3)) == 0){} /* 等待接收FIFO有数据 */
75      spirxdata = base->RXDATA;
76  	return spirxdata;
77 }

文件bsp_spi.c中有两个函数:spi_init和spich0_readwrite_byte,函数spi_init是SPI初始化函数,此函数会初始化SPI的时钟,通道等。函数spich0_readwrite_byte是SPI收发函数,通过此函数即可完成SPI的全双工数据收发。
接下来在文件bsp_icm20608.h中输入如下内容:

示例代码27.3.3 bsp_icm20608.h文件代码

1  #ifndef _BSP_ICM20608_H
2  #define _BSP_ICM20608_H
3  /***************************************************************
4  Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
5  文件名    : bsp_icm20608.h
6  作者      : 左忠凯
7  版本      : V1.0
8  描述      : ICM20608驱动文件。
9  其他      : 无
10 论坛      : www.openedv.com
11 日志      : 初版V1.0 2019/3/26 左忠凯创建
12 ***************************************************************/
13 #include "imx6ul.h"
14 #include "bsp_gpio.h"
15 
16 /* SPI片选信号 */
17 #define ICM20608_CSN(n)    (n ? gpio_pinwrite(GPIO1, 20, 1) : 
gpio_pinwrite(GPIO1, 20, 0))  
18 
19 #define ICM20608G_ID     	0XAF    /* ID值 */
20 #define ICM20608D_ID    	0XAE    /* ID值 */
21 
22 /* ICM20608寄存器 
23  *复位后所有寄存器地址都为0,除了
24  *Register 107(0X6B) Power Management 1 	= 0x40
25  *Register 117(0X75) WHO_AM_I           		= 0xAF或者0xAE
26  */
27 /* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
28 #define  ICM20_SELF_TEST_X_GYRO    	0x00
29 #define  ICM20_SELF_TEST_Y_GYRO   	0x01
30 #define  ICM20_SELF_TEST_Z_GYRO     	0x02
31 #define  ICM20_SELF_TEST_X_ACCEL   	0x0D
32 #define  ICM20_SELF_TEST_Y_ACCEL    	0x0E
33 #define  ICM20_SELF_TEST_Z_ACCEL   	0x0F
34 /***********省略掉其他宏定义*************/
35 #define  ICM20_ZA_OFFSET_H          	0x7D
36 #define  ICM20_ZA_OFFSET_L         	0x7E
37 
38 /*
39  * ICM20608结构体
40  */
41 struct icm20608_dev_struc
42 {
43  	signed int gyro_x_adc;      	/* 陀螺仪X轴原始值		*/
44  	signed int gyro_y_adc;      	/* 陀螺仪Y轴原始值     	*/
45 	 	signed int gyro_z_adc;      	/* 陀螺仪Z轴原始值     	*/
46  	signed int accel_x_adc;     	/* 加速度计X轴原始值   	*/
47  	signed int accel_y_adc;     	/* 加速度计Y轴原始值   	*/
48  	signed int accel_z_adc;     	/* 加速度计Z轴原始值   	*/
49  	signed int temp_adc;      	/* 温度原始值         	*/
50 
51  	/* 下面是计算得到的实际值,扩大100倍 */
52  	signed int gyro_x_act;      	/* 陀螺仪X轴实际值  	*/
53 	 	signed int gyro_y_act;      	/* 陀螺仪Y轴实际值    	*/
54  	signed int gyro_z_act;      	/* 陀螺仪Z轴实际值    	*/
55  	signed int accel_x_act;     	/* 加速度计X轴实际值  	*/
56  	signed int accel_y_act;     	/* 加速度计Y轴实际值  	*/
57  	signed int accel_z_act;     	/* 加速度计Z轴实际值  	*/
58  	signed int temp_act;        	/* 温度实际值           	*/
59 };
60 
61 struct icm20608_dev_struc icm20608_dev;  /* icm20608设备 */
62 
63 /* 函数声明 */
64 unsigned char icm20608_init(void);
65 void icm20608_write_reg(unsigned char reg, unsigned char value);
66 unsigned char icm20608_read_reg(unsigned char reg);
67 void icm20608_read_len(unsigned char reg, unsigned char *buf, 
unsigned char len);
68 void icm20608_getdata(void);
69 #endif

文件bsp_icm20608.h里面先定义了一个宏ICM20608_CSN,这个是ICM20608的SPI片选引脚。接下来定义了一些ICM20608的ID和寄存器地址。第41行定义了一个结构体icm20608_dev_struc,这个结构体是ICM20608的设备结构体,里面的成员变量用来保存ICM20608的原始数据值和经过转换得到的实际值。实际值是有小数的,本章例程取两位小数,为了方便计算,实际值扩大了100倍,这样实际值就是整数了,但是在使用的时候要除100重新得到小数部分。最后就是一些函数声明,接下来在文件bsp_icm20608.c中输入如下所示内容:

示例代码27.3.4 bsp_icm20608.c文件代码
/***************************************************************
Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
文件名   : bsp_icm20608.c
作者     : 左忠凯
版本     : V1.0
描述     : ICM20608驱动文件。
其他     : 无
论坛     : www.openedv.com
日志     : 初版V1.0 2019/3/26 左忠凯创建
***************************************************************/
1   #include "bsp_icm20608.h"
2   #include "bsp_delay.h"
3   #include "bsp_spi.h"
4   #include "stdio.h"
5   
6   struct icm20608_dev_struc icm20608_dev; /* icm20608设备 */
7   
8   /*
9    * @description 	: 初始化ICM20608
10   * @param       	: 无
11   * @return      	: 0 初始化成功,其他值 初始化失败
12   */
13  unsigned char icm20608_init(void)
14  {   
15      unsigned char regvalue;
16      gpio_pin_config_t cs_config;
17  
18      /* 1、ESPI3 IO初始化 
19       * ECSPI3_SCLK  -> UART2_RXD
20       * ECSPI3_MISO  -> UART2_RTS
21       * ECSPI3_MOSI  -> UART2_CTS
22       */
23      IOMUXC_SetPinMux(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0);
24      IOMUXC_SetPinMux(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0);
25      IOMUXC_SetPinMux(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0);
26      IOMUXC_SetPinConfig(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0x10B1);
27      IOMUXC_SetPinConfig(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0x10B1);
28      IOMUXC_SetPinConfig(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0x10B1);
29  
30      /*  初始化片选引脚  */
31      IOMUXC_SetPinMux(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0);
32      IOMUXC_SetPinConfig(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0X10B0);
33      cs_config.direction = kGPIO_DigitalOutput;
34      cs_config.outputLogic = 0;
35      gpio_init(GPIO1, 20, &cs_config);
36      
37      /* 2、初始化SPI */
38      spi_init(ECSPI3);   
39  
40      icm20608_write_reg(ICM20_PWR_MGMT_1, 0x80);  /* 复位 		*/
41      delayms(50);
42      icm20608_write_reg(ICM20_PWR_MGMT_1, 0x01);  /* 关闭睡眠	*/
43      delayms(50);
44  
45      regvalue = icm20608_read_reg(ICM20_WHO_AM_I);
46      printf("icm20608 id = %#X\r\n", regvalue);
47      if(regvalue != ICM20608G_ID && regvalue != ICM20608D_ID)
48          return 1;
49          
50      icm20608_write_reg(ICM20_SMPLRT_DIV, 0x00);  	/* 输出速率设置     */
51      icm20608_write_reg(ICM20_GYRO_CONFIG, 0x18);  /* 陀螺仪±2000dps */
52      icm20608_write_reg(ICM20_ACCEL_CONFIG, 0x18); /* 加速度计±16G   */
53      icm20608_write_reg(ICM20_CONFIG, 0x04);        /* 陀螺BW=20Hz   */
54      icm20608_write_reg(ICM20_ACCEL_CONFIG2, 0x04);
55      icm20608_write_reg(ICM20_PWR_MGMT_2, 0x00);   /* 打开所有轴       */
56      icm20608_write_reg(ICM20_LP_MODE_CFG, 0x00);  /* 关闭低功耗       */
57      icm20608_write_reg(ICM20_FIFO_EN, 0x00);   	/* 关闭FIFO       */
58      return 0;
59  }
60  
61  /*
62   * @description  	: 写ICM20608指定寄存器
63   * @param - reg  	: 要读取的寄存器地址
64   * @param – value	: 要写入的值
65   * @return       	: 无
66   */
67  void icm20608_write_reg(unsigned char reg, unsigned char value)
68  {
69      /* ICM20608在使用SPI接口的时候寄存器地址只有低7位有效,
70       * 寄存器地址最高位是读/写标志位,读的时候要为1,写的时候要为0。
71       */
72      reg &= ~0X80;   
73      
74      ICM20608_CSN(0);                        	/* 使能SPI传输   	*/
75      spich0_readwrite_byte(ECSPI3, reg);   	/* 发送寄存器地址  	*/ 
76      spich0_readwrite_byte(ECSPI3, value);	/* 发送要写入的值   	*/
77      ICM20608_CSN(1);                        	/* 禁止SPI传输   	*/
78  }   
79  
80  /*
81   * @description 	: 读取ICM20608寄存器值
82   * @param - reg 	: 要读取的寄存器地址
83   * @return      	: 读取到的寄存器值
84   */
85  unsigned char icm20608_read_reg(unsigned char reg)
86  {
87      unsigned char reg_val;      
88  
89      /* ICM20608在使用SPI接口的时候寄存器地址只有低7位有效,
90       * 寄存器地址最高位是读/写标志位,读的时候要为1,写的时候要为0。
91       */
92      reg |= 0x80;    
93      
94      ICM20608_CSN(0);                         	/* 使能SPI传输           */
95      spich0_readwrite_byte(ECSPI3, reg);	/* 发送寄存器地址         */ 
96      reg_val = spich0_readwrite_byte(ECSPI3, 0XFF);/* 读取寄存器的值*/
97      ICM20608_CSN(1);                        	/* 禁止SPI传输           */
98      return(reg_val);                         	/* 返回读取到的寄存器值 	 */
99  }
100 
101 /*
102  * @description 	: 读取ICM20608连续多个寄存器
103  * @param - reg 	: 要读取的寄存器地址
104  * @return      	: 读取到的寄存器值
105  */
106 void icm20608_read_len(unsigned char reg, unsigned char *buf, 
unsigned char len)
107 {  
108     unsigned char i;
109     
110     /* ICM20608在使用SPI接口的时候寄存器地址,只有低7位有效,
111      * 寄存器地址最高位是读/写标志位读的时候要为1,写的时候要为0。
112      */
113     reg |= 0x80; 
114         
115     ICM20608_CSN(0);                       /* 使能SPI传输   		*/
116     spich0_readwrite_byte(ECSPI3, reg);/* 发送寄存器地址      	*/         
117     for(i = 0; i < len; i++)             /* 顺序读取寄存器的值   	*/
118     {
119         buf[i] = spich0_readwrite_byte(ECSPI3, 0XFF);   
120     }
121     ICM20608_CSN(1);                      /* 禁止SPI传输       	*/
122 }
123 
124 /*
125  * @description 	: 获取陀螺仪的分辨率
126  * @param       	: 无
127  * @return      	: 获取到的分辨率
128  */
129 float icm20608_gyro_scaleget(void)
130 {
131     unsigned char data;
132     float gyroscale;
133     
134     data = (icm20608_read_reg(ICM20_GYRO_CONFIG) >> 3) & 0X3;
135     switch(data) {
136         case 0: 
137             gyroscale = 131;
138             break;
139         case 1:
140             gyroscale = 65.5;
141             break;
142         case 2:
143             gyroscale = 32.8;
144             break;
145         case 3:
146             gyroscale = 16.4;
147             break;
148     }
149     return gyroscale;
150 }
151 
152 /*
153  * @description 	: 获取加速度计的分辨率
154  * @param       	: 无
155  * @return      	: 获取到的分辨率
156  */
157 unsigned short icm20608_accel_scaleget(void)
158 {
159     unsigned char data;
160     unsigned short accelscale;
161     
162     data = (icm20608_read_reg(ICM20_ACCEL_CONFIG) >> 3) & 0X3;
163     switch(data) {
164         case 0: 
165             accelscale = 16384;
166             break;
167         case 1:
168             accelscale = 8192;
169             break;
170         case 2:
171             accelscale = 4096;
172             break;
173         case 3:
174             accelscale = 2048;
175             break;
176     }
177     return accelscale;
178 }
179 
180 /*
181  * @description 	: 读取ICM20608的加速度、陀螺仪和温度原始值
182  * @param       	: 无
183  * @return      	: 无
184  */
185 void icm20608_getdata(void)
186 {
187     float gyroscale;
188     unsigned short accescale;
189     unsigned char data[14];
190     
191     icm20608_read_len(ICM20_ACCEL_XOUT_H, data, 14);
192     
193     gyroscale = icm20608_gyro_scaleget();
194     accescale = icm20608_accel_scaleget();
195 
196     icm20608_dev.accel_x_adc = (signed short)((data[0] << 8) | 
data[1]); 
197     icm20608_dev.accel_y_adc = (signed short)((data[2] << 8) | 
data[3]); 
198     icm20608_dev.accel_z_adc = (signed short)((data[4] << 8) | 
data[5]); 
199     icm20608_dev.temp_adc    = (signed short)((data[6] << 8) | 
data[7]); 
200     icm20608_dev.gyro_x_adc  = (signed short)((data[8] << 8) | 
data[9]); 
201     icm20608_dev.gyro_y_adc  = (signed short)((data[10] << 8) | 
data[11]);
202     icm20608_dev.gyro_z_adc  = (signed short)((data[12] << 8) | 
data[13]);
203 
204     /* 计算实际值 */
205     icm20608_dev.gyro_x_act = ((float)(icm20608_dev.gyro_x_adc)  / 
gyroscale) * 100;
206     icm20608_dev.gyro_y_act = ((float)(icm20608_dev.gyro_y_adc)  / 
gyroscale) * 100;
207     icm20608_dev.gyro_z_act = ((float)(icm20608_dev.gyro_z_adc)  / 
gyroscale) * 100;
208     icm20608_dev.accel_x_act = ((float)(icm20608_dev.accel_x_adc) / 
accescale) * 100;
209     icm20608_dev.accel_y_act = ((float)(icm20608_dev.accel_y_adc) / 
accescale) * 100;
210     icm20608_dev.accel_z_act = ((float)(icm20608_dev.accel_z_adc) / 
accescale) * 100;
211     icm20608_dev.temp_act = (((float)(icm20608_dev.temp_adc) - 25) /
                                      326.8 + 25) * 100;
212 }

文件bsp_imc20608.c是ICM20608的驱动文件,里面有7个函数,我们依次来看一下。第1个函数是icm20608_init,这个是ICM20608的初始化函数,此函数先初始化ICM20608所使用的SPI引脚,将其复用为ECSPI3。因为我们本章的SPI片选采用软件控制的方式,所以SPI片选引脚设置成了普通的输出模式。设置完SPI所使用的引脚以后就是调用函数spi_init来初始化SPI3,最后初始化ICM20608,就是配置ICM20608的寄存器。第2个和第3个函数分别是icm20608_write_reg和icm20608_read_reg,这两个函数分别用于写/读ICM20608的指定寄存器。第4个函数是icm20608_read_len,此函数也是读取ICM20608的寄存器值,但是此函数可以读取连续多个寄存器的值,一般用于读取ICM20608传感器数据。第5和第6个函数分别是icm20608_gyro_scaleget和icm20608_accel_scaleget,这两个函数分别用于获取陀螺仪和加速度计的分辨率,因为陀螺仪和加速度的测量范围设置的不同,其分辨率就不同,所以在计算实际值的时候要根据实际的量程范围来得到对应的分辨率。最后一个函数是icm20608_getdata,此函数就是用于获取ICM20608的加速度计、陀螺仪和温度计的数据,并且会根据设置的测量范围计算出实际的值,比如加速度的g值、陀螺仪的角速度值和温度计的温度值。
最后在main.c中输入如下内容:
示例代码27.3.5 main.c文件代码

/**************************************************************
Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
文件名   : main.c
作者     : 左忠凯
版本     : V1.0
描述     : I.MX6U开发板裸机实验19 SPI实验
其他     : SPI也是最常用的接口,ALPHA开发板上有一个6轴传感器ICM20608,
           这个六轴传感器就是SPI接口的,本实验就来学习如何驱动I.MX6U
           的SPI接口,并且通过SPI接口读取ICM20608的数据值。
论坛     : www.openedv.com
日志     : 初版V1.0 2019/1/17 左忠凯创建
**************************************************************/
1   #include "bsp_clk.h"
2   #include "bsp_delay.h"
3   #include "bsp_led.h"
4   #include "bsp_beep.h"
5   #include "bsp_key.h"
6   #include "bsp_int.h"
7   #include "bsp_uart.h"
8   #include "bsp_lcd.h"
9   #include "bsp_rtc.h"
10  #include "bsp_icm20608.h"
11  #include "bsp_spi.h"
12  #include "stdio.h"
13  
14  /*
15   * @description 	: 指定的位置显示整数数据
16   * @param - x   	: X轴位置
17   * @param - y   	: Y轴位置
18   * @param – size	: 字体大小
19   * @param - num 	: 要显示的数据
20   * @return      	: 无
21   */
22  void integer_display(unsigned short x, unsigned short y, 
unsigned char size, signed int num)
23  {
24      char buf[200];
25      
26      lcd_fill(x, y, x + 50, y + size, tftlcd_dev.backcolor);
27      
28      memset(buf, 0, sizeof(buf));
29      if(num < 0)
30          sprintf(buf, "-%d", -num);
31      else 
32          sprintf(buf, "%d", num);
33      lcd_show_string(x, y, 50, size, size, buf); 
34  }
35  
36  /*
37   * @description 	: 指定的位置显示小数数据,比如5123,显示为51.23
38   * @param - x   	: X轴位置
39   * @param - y   	: Y轴位置
40   * @param – size	: 字体大小
41   * @param - num 	: 要显示的数据,实际小数扩大100倍,
42   * @return      	: 无
43   */
44  void decimals_display(unsigned short x, unsigned short y, 
unsigned char size, signed int num)
45  {
46      signed int integ;   /* 整数部分 */
47      signed int fract;   /* 小数部分 */
48      signed int uncomptemp = num; 
49      char buf[200];
50  
51      if(num < 0)
52          uncomptemp = -uncomptemp;
53      integ = uncomptemp / 100;
54      fract = uncomptemp % 100;
55  
56      memset(buf, 0, sizeof(buf));
57      if(num < 0)
58          sprintf(buf, "-%d.%d", integ, fract);
59      else 
60          sprintf(buf, "%d.%d", integ, fract);
61      lcd_fill(x, y, x + 60, y + size, tftlcd_dev.backcolor);
62      lcd_show_string(x, y, 60, size, size, buf); 
63  }
64  
65  /*
66   * @description 	: 使能I.MX6U的硬件NEON和FPU
67   * @param       	: 无
68   * @return      	: 无
69   */
70   void imx6ul_hardfpu_enable(void)
71  {
72      uint32_t cpacr;
73      uint32_t fpexc;
74  
75      /* 使能NEON和FPU */
76      cpacr = __get_CPACR();
77      cpacr = (cpacr & ~(CPACR_ASEDIS_Msk | CPACR_D32DIS_Msk))
78             |  (3UL << CPACR_cp10_Pos) | (3UL << CPACR_cp11_Pos);
79      __set_CPACR(cpacr);
80      fpexc = __get_FPEXC();
81      fpexc |= 0x40000000UL;  
82      __set_FPEXC(fpexc);
83  }
84  
85  /*
86   * @description : main函数
87   * @param       : 无
88   * @return      : 无
89   */
90  int main(void)
91  {
92      unsigned char state = OFF;
93  
94      imx6ul_hardfpu_enable();  	/* 使能I.MX6U的硬件浮点       	*/
95      int_init();                 	/* 初始化中断(一定要最先调用!)	*/
96      imx6u_clkinit();            	/* 初始化系统时钟              	*/
97      delay_init();               	/* 初始化延时                  	*/
98      clk_enable();               	/* 使能所有的时钟              	*/
99      led_init();                 	/* 初始化led                   	*/
100     beep_init();                	/* 初始化beep                  	*/
101     uart_init();                	/* 初始化串口,波特率115200 	*/
102     lcd_init();                 	/* 初始化LCD                   	*/      
103 
104     tftlcd_dev.forecolor = LCD_RED;
105     lcd_show_string(50, 10, 400, 24, 24, 
(char*)"IMX6U-ALPHA SPI TEST");  
106     lcd_show_string(50, 40, 200, 16, 16, (char*)"ICM20608 TEST");  
107     lcd_show_string(50, 60, 200, 16, 16, (char*)"ATOM@ALIENTEK");  
108     lcd_show_string(50, 80, 200, 16, 16, (char*)"2019/3/27");  
109     
110     while(icm20608_init())      	/* 初始化ICM20608 */
111     {
112         lcd_show_string(50, 100, 200, 16, 16, 
(char*)"ICM20608 Check Failed!");
113         delayms(500);
114         lcd_show_string(50, 100, 200, 16, 16, 
(char*)"Please Check!        ");
115         delayms(500);
116     }   
117     lcd_show_string(50, 100, 200, 16, 16, (char*)"ICM20608 Ready");
118     lcd_show_string(50, 130, 200, 16, 16, (char*)"accel x:");  
119     lcd_show_string(50, 150, 200, 16, 16, (char*)"accel y:");  
120     lcd_show_string(50, 170, 200, 16, 16, (char*)"accel z:");  
121     lcd_show_string(50, 190, 200, 16, 16, (char*)"gyro  x:"); 
122     lcd_show_string(50, 210, 200, 16, 16, (char*)"gyro  y:"); 
123     lcd_show_string(50, 230, 200, 16, 16, (char*)"gyro  z:"); 
124     lcd_show_string(50, 250, 200, 16, 16, (char*)"temp   :"); 
125     lcd_show_string(50 + 181, 130, 200, 16, 16, (char*)"g");  
126     lcd_show_string(50 + 181, 150, 200, 16, 16, (char*)"g");  
127     lcd_show_string(50 + 181, 170, 200, 16, 16, (char*)"g");  
128     lcd_show_string(50 + 181, 190, 200, 16, 16, (char*)"o/s"); 
129     lcd_show_string(50 + 181, 210, 200, 16, 16, (char*)"o/s"); 
130     lcd_show_string(50 + 181, 230, 200, 16, 16, (char*)"o/s"); 
131     lcd_show_string(50 + 181, 250, 200, 16, 16, (char*)"C");
132     
133     tftlcd_dev.forecolor = LCD_BLUE;
134 
135     while(1)                    
136     {       
137         icm20608_getdata();     /* 获取数据值 */
138         /* 在LCD上显示原始值 */
139         integer_display(50 + 70, 130, 16, icm20608_dev.accel_x_adc);
140         integer_display(50 + 70, 150, 16, icm20608_dev.accel_y_adc);
141         integer_display(50 + 70, 170, 16, icm20608_dev.accel_z_adc);
142         integer_display(50 + 70, 190, 16, icm20608_dev.gyro_x_adc);
143         integer_display(50 + 70, 210, 16, icm20608_dev.gyro_y_adc);
144         integer_display(50 + 70, 230, 16, icm20608_dev.gyro_z_adc);
145         integer_display(50 + 70, 250, 16, icm20608_dev.temp_adc);
146 
147         /* 在LCD上显示计算得到的原始值 */
148         decimals_display(50 + 70 + 50, 130, 16, 
icm20608_dev.accel_x_act);
149         decimals_display(50 + 70 + 50, 150, 16, 
icm20608_dev.accel_y_act);
150         decimals_display(50 + 70 + 50, 170, 16, 
icm20608_dev.accel_z_act);
151         decimals_display(50 + 70 + 50, 190, 16, 
icm20608_dev.gyro_x_act);
152         decimals_display(50 + 70 + 50, 210, 16, 
icm20608_dev.gyro_y_act);
153         decimals_display(50 + 70 + 50, 230, 16, 
icm20608_dev.gyro_z_act);
154         decimals_display(50 + 70 + 50, 250, 16, 
icm20608_dev.temp_act);
155         delayms(120);
156         state = !state;
157         led_switch(LED0,state); 
158     }
159     return 0;
160 }

文件main.c一开始有两个函数integer_display和decimals_display,这两个函数用于在LCD上显示获取到的ICM20608数据值,函数integer_display用于显示原始数据值,也就是整数值。函数decimals_display用于显示实际值,实际值扩大了100倍,此函数会提取出实际值的整数部分和小数部分并显示在LCD上。另一个重要的函数是imx6ul_hardfpu_enable,这个函数用于开启I.MX6U的NEON和硬件FPU(浮点运算单元),因为本章使用到了浮点运算,而I.MX6U的Cortex-A7是支持NEON和FPU(VFPV4_D32)的,但是在使用I.MX6U的硬件FPU之前是先要开启的。
第110行调用了函数icm20608_init来初始化ICM20608,如果初始化失败的话就会在LCD上闪烁提示语句。最后在main函数的while循环中不断的调用函数icm20608_getdata获取ICM20608的传感器数据,并且显示在LCD上。实验程序编写就到这里结束了,接下来就是编译、下载和验证了。

27.4 编译下载验证
27.4.1 编写Makefile和链接脚本
修改Makefile中的TARGET为icm20608,然后在在INCDIRS和SRCDIRS中加入“bsp/spi”和“bsp/icm20608”,修改后的Makefile如下:
示例代码27.4.1.1 Makefile文件代码

1  CROSS_COMPILE	?= arm-linux-gnueabihf-
2  TARGET         	?= icm20608
3  
4  /* 省略掉其它代码...... */
5  
6  INCDIRS      	:=	imx6ul \
7                  		stdio/include \
8                  		bsp/clk \
9                  		bsp/led \
10                 		bsp/delay  \
11                 		bsp/beep \
12                 		bsp/gpio \
13                 		bsp/key \
14                 		bsp/exit \
15                 		bsp/int \
16                 		bsp/epittimer \
17                 		bsp/keyfilter \
18                 		bsp/uart \
19                 		bsp/lcd \
20                 		bsp/rtc \
21                 		bsp/i2c \
22                 		bsp/ap3216c \
23                 		bsp/spi \
24                 		bsp/icm20608
25                             
26 SRCDIRS      	:= 	project \
27                 		stdio/lib \
28                 		bsp/clk \
29                 		bsp/led \
30                 		bsp/delay \
31                 		bsp/beep \
32                 		bsp/gpio \
33                 		bsp/key \
34                 		bsp/exit \
35                 		bsp/int \
36                 		bsp/epittimer \
37                 		bsp/keyfilter \
38                 		bsp/uart \
39                 		bsp/lcd \
40                 		bsp/rtc \
41                 		bsp/i2c \
42                 		bsp/ap3216c \
43                 		bsp/spi \
44                		bsp/icm20608
45                 
46 /* 省略掉其它代码...... */
47 
48 $(COBJS) : obj/%.o : %.c
49  $(CC) -Wall -march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard -Wa,
-mimplicit-it=thumb -nostdlib -fno-builtin 
-c -O2  $(INCLUDE) -o $@ $<
50  
51 clean:
52  rm -rf $(TARGET).elf $(TARGET).dis $(TARGET).bin $(COBJS) $(SOBJS)

第2行修改变量TARGET为“icm20608”,也就是目标名称为“icm20608”。
第23和24行在变量INCDIRS中添加SPI和ICM20608的驱动头文件(.h)路径。
第43和44行在变量SRCDIRS中添加SPI和ICM20608驱动文件(.c)路径。
第49行加入了“-march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard”指令,这些指令用于指定编译浮点运算的时候使用硬件FPU。因为本章使用到了浮点运算,而I.MX6U是支持硬件FPU的,虽然我们在main函数中已经打开了NEON和FPU,但是在编译相应C文件的时候也要指定使用硬件FPU来编译浮点运算。
链接脚本保持不变。

27.4.2 编译下载
使用Make命令编译代码,编译成功以后使用软件imxdownload将编译完成的icm20608.bin文件下载到SD卡中,命令如下:
chmod 777 imxdownload //给予imxdownload可执行权限,一次即可
./imxdownload icm20608.bin /dev/sdd //烧写到SD卡中,不能烧写到/dev/sda或sda1里面!
烧写成功以后将SD卡插到开发板的SD卡槽中,然后复位开发板。如果ICM20608工作正常的话就会在LCD上显示获取到的传感器数据,如图27.4.2.1所示:
在这里插入图片描述
图27.4.2.1 LCD界面
在图27.4.2.1中可以看到加速度计Z轴在静止状态下是0.98g,这正是重力加速度。温度传感器测量到的温度是31.39°C,这个是芯片内部的温度,并不是室温!芯片内部温度一般要比室温高。如果动一下开发板的话加速度计和陀螺仪的数据就会变化

二、linux SPI驱动框架

1.linux SPI驱动分层

由于裸机的驱动迎合驱动的分离和分层的思想,分为SPI主机驱动(接口驱动)和SPI设备驱动.这种思想的好处,请看我写的另外一篇文章Linux驱动的分离和分层。

本文也是遵循Linux驱动的分离和分层的思想,因此linux内核把SPI驱动分为两个部分SPI总线驱动和SPI设备驱动
**
总线驱动:SOC的SPI控制器驱动,也叫做SPI适配器驱动,产生spi时序的驱动,用来驱动spi设备。bus总线
设备驱动:SPI设备驱动就是针对具体的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 总线,在讲 platform 的时候就说过,platform 是虚拟出来的一条总线,目的是为了实现总线、设备、驱动框架。
SPI总线驱动重点是 SPI 适配器(也就是 SOC 的SPI 接口控制器驱动),SPI总线驱动,或者说 SPI适配器驱动的主要核心,
申请spi_master,并且初始化它,和i2c中的i2c_adapter一样,然后设置transfer函数。
和 i2c_algorithm 中的 master_xfer 函数一样控制器数据传输函数,后通过 spi_register_master函数向Linux内核注册设置好的spi_master

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

 struct spi_master { 
	     struct device   dev; 
	     struct list_head list; 
	... 
	     s16         bus_num; 
	     u16         num_chipselect; 
	     u16         dma_alignment; 
	     u16         mode_bits; 
	     u32         bits_per_word_mask; 
	... 
	     /* limits on transfer speed */ 
	     u32         min_speed_hz; 
	     u32         max_speed_hz; 
	  
	     /* other constraints relevant to this driver */ 
	     u16         flags; 
	... 
	     /* lock and mutex for SPI bus locking */ 
	     spinlock_t      bus_lock_spinlock; 
	     struct mutex        bus_lock_mutex; 
	
	   /* flag indicating that the SPI bus is locked for exclusive use */
	     bool    bus_lock_flag; 
	... 
	     int (*setup)(struct spi_device *spi); 
	  
	... 
	     int (*transfer)(struct spi_device *spi, struct spi_message *mesg); 
	... 
	   	 int (*transfer_one_message)(struct spi_master *master, struct spi_message *mesg); 
	... 
 }; 

transfer函数,和i2c_algorithm中的master_xfer函数一样,控制器数据传输函数。
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。

一般SOC的SPI总线驱动都是由半导体厂商编写的。因此SPI总线驱动对我们这些SOC 使用者来说是被屏蔽掉的,我们只要专注于 SPI设备驱动即可

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。
返回值:无。

2、 SPI设备驱动
上面已经说了spi_master了,还剩下设备和驱动,spi_client就是描述设备信息的,spi_driver描述驱动内容,类似于 platform_driver

回想一下i2c的结构体

struct i2c_client { 
    unsigned short flags;          /* 标志             */ 
    unsigned short addr;           /* 芯片地址,7 位,存在低 7 位  */ 
    char name[I2C_NAME_SIZE];       /* 名字           */ 
    struct i2c_adapter *adapter;    /* 对应的 I2C 适配器       */ 
    struct device dev;                /* 设备结构体         */ 
    int irq;                                 /* 中断             */ 
    struct list_head detected; 
.. 
}; 

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

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

可以看出,spi_driver和i2c_driver、platform_driver基本一样,当SPI设备和驱动匹配成功以后probe函数就会执行。
同样的,spi_driver初始化完成以后需要向Linux内核注册,spi_driver注册函数为spi_register_driver,函数原型如下:

我们的目的就是
1.构建spi_driver
2.向Linux内核注册这个spi_driver

spi_driver注册函数为spi_register_driver

int spi_register_driver(struct spi_driver *sdrv) 

注销SPI设备驱动以后也需要注销掉前面注册的 spi_driver

void spi_unregister_driver(struct spi_driver *sdrv) 

spi_driver注册示例程序如下:
具体注册spi_driver的注册流程:spi_driver 结构体,需要 SPI 设备驱动人员编写,包括匹配表、probe 函数等

示例代码62.1.1.3 spi_driver注册示例程序
 static int xxx_probe(struct spi_device *spi) 
 { 
    /* 具体函数内容 */ 
    return 0; 
 } 
  
 /* remove 函数 */ 
 static int xxx_remove(struct spi_device *spi) 
 { 
    /* 具体函数内容 */ 
      return 0; 
 } 
 
 /* 传统匹配方式 ID 列表 */ 
 static const struct spi_device_id xxx_id[] = { 
    {"xxx", 0},   
    {} 
 }; 
  
 /* 设备树匹配列表 */ 
 static const struct of_device_id xxx_of_match[] = { 
    { .compatible = "xxx" }, 
    { /* Sentinel */ } 
 }; 
  
 /* SPI 驱动结构体 */  
 static struct spi_driver xxx_driver = { 
    .probe = xxx_probe, 
    .remove = xxx_remove, 
    .driver = { 
            .owner = THIS_MODULE, 
            .name = "xxx", 
            .of_match_table = xxx_of_match,  
          }, 
    .id_table = xxx_id, 
 }; 
          
 /* 驱动入口函数 */ 
 static int __init xxx_init(void) 
 { 
    return spi_register_driver(&xxx_driver); 
 } 
  
 /* 驱动出口函数 */ 
 static void __exit xxx_exit(void) 
 { 
    spi_unregister_driver(&xxx_driver); 
 } 
  
 module_init(xxx_init); 
 module_exit(xxx_exit); 

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

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

可以看出,SPI设备和驱动的匹配函数为 spi_match_device,四种匹配方式

 static int spi_match_device(struct device *dev, struct device_driver *drv) 
 { 
     const struct spi_device *spi = to_spi_device(dev); 
     const struct spi_driver *sdrv = to_spi_driver(drv); 
  
     /* Attempt an OF style match */ 
     if (of_driver_match_device(dev, drv)) 
         return 1; 
  
     /* Then try ACPI */ 
     if (acpi_driver_match_device(dev, drv)) 
         return 1; 
  
     if (sdrv->id_table) 
         return !!spi_match_id(sdrv->id_table, spi); 
         
     return strcmp(spi->modalias, drv->name) == 0; 

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

3、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)

164 writel(val, spi_imx->base + MXC_CSPITXDATA); 通过write,写数据到寄存器
从示例代码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设备驱动编写流程编程实例

相比裸机驱动,由于Linux内核的介入,提供了很多供我们使用后的API,大大提高了效率和稳定性。
我们实际中,使用platfom驱动框架开发SPI驱动。涉及到驱动、总线、设备模型。我们只需要编写platform_driver,这里要注意:设备信息从设备驱动中剥离开来。还是那句话”驱动的分离和分层思想“,这样涉及到是否使用到设备树(设备树给我们提供了方便,当然要用啦!)

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 = <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中的内容即可。

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

3、 修改设备树
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-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>; /* cant't use cs-gpios! */
4      pinctrl-names = "default";
5      pinctrl-0 = <&pinctrl_ecspi3>;
6      status = "okay";
7  
8      spidev: icm20608@0 {
9          compatible = "alientek,icm20608";
10         spi-max-frequency = <8000000>;
11         reg = <0>;
12     };
13 };

第2行,设置当前片选数量为1,因为就只接了一个ICM20608。
第3行,注意!这里并没有用到“cs-gpios”属性,而是用了一个自己定义的“cs-gpio”属性,因为我们要自己控制片选引脚。如果使用“cs-gpios”属性的话SPI主机驱动就会控制片选引脚。
第5行,设置IO要使用的pinctrl子节点,也就是我们在示例代码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系统。

4、 编写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 <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  	: 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的寄存器读写以及初始化代码如下:

示例代码62.5.2.5 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的字符设备驱动框架如下:

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

6、 编写测试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 <poll.h>
10 #include <sys/select.h>
11 #include <sys/time.h>
12 #include <signal.h>
13 #include <fcntl.h>

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 < 0) {
56      	printf("can't open file %s\r\n", filename);
57      	return -1;
58  	}
59 
60  	while (1) {
61      	ret = read(fd, databuf, sizeof(databuf));
62     	 	if(ret == 0) {          /* 数据读取成功 */
63          		gyro_x_adc = databuf[0];
64          		gyro_y_adc = databuf[1];
65          		gyro_z_adc = databuf[2];
66          		accel_x_adc = databuf[3];
67          		accel_y_adc = databuf[4];
68          		accel_z_adc = databuf[5];
69          		temp_adc = databuf[6];
70 
71          		/* 计算实际值 */
72          		gyro_x_act = (float)(gyro_x_adc)  / 16.4;
73          		gyro_y_act = (float)(gyro_y_adc)  / 16.4;
74          		gyro_z_act = (float)(gyro_z_adc)  / 16.4;
75          		accel_x_act = (float)(accel_x_adc) / 2048;
76          		accel_y_act = (float)(accel_y_adc) / 2048;
77          		accel_z_act = (float)(accel_z_adc) / 2048;
78          		temp_act = ((float)(temp_adc) - 25 ) / 326.8 + 25;
79 
80          		printf("\r\n原始值:\r\n");
81          		printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc, gyro_y_adc, gyro_z_adc);
82          		printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc, accel_y_adc, accel_z_adc);
83          		printf("temp = %d\r\n", temp_adc);
84         		printf("实际值:");
85          		printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);
86          		printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", accel_x_act, accel_y_act, accel_z_act);
87          		printf("act temp = %.2f°C\r\n", temp_act);
88      	}
89      	usleep(100000); /*100ms */
90  	}
91  	close(fd);  /* 关闭文件 */  
92  	return 0;
93 }

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

三、最后自己总结

很久不写驱动了,以前面试老是记不住,问我初始化在哪完成,驱动要实现什么函数,probe是怎么工作的,干些啥

1、填充operation结构,提供open,read,write,ioctrl函数,实现具体的来读取设备sensor的数据

static const struct file_operations icm20608_ops = {
.owner = THIS_MODULE,
.open = icm20608_open,
.read = icm20608_read,
.release = icm20608_release,
};

2、完成probe函数

以前字符设备时,通过init函数,向内核注册设备,现在通过probe函数,实现向内核注册字符设备

static int __init icm20608_init(void)
{
return spi_register_driver(&icm20608_driver);//注册到spi总线上
}
module_init(icm20608_init);

现在通过probe
static int icm20608_probe(struct spi_device *spi)

通过cdev,添加一个字符设备
cdev_init(icm20608_ops &icm20608dev.cdev, &icm20608_ops);
cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT)

create_class自动创建设备节点

有的开发,将初始化外挂的设备的寄存器等操作放在probe,有的放在open
icm20608_reginit()

3、填充完icm20608_driver 驱动结构体,向内核spi总线注册 驱动
static struct spi_driver icm20608_driver = {
.probe = icm20608_probe,
.remove = icm20608_remove,
.driver = {
.owner = THIS_MODULE,
.name = “icm20608”,
.of_match_table = icm20608_of_match,
},
.id_table = icm20608_id,
};

static int __init icm20608_init(void)
{
return spi_register_driver(&icm20608_driver);//注册到spi总线上
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值