<Linux开发>驱动开发 -Linux SPI 驱动

<Linux开发>驱动开发 -Linux SPI 驱动

交叉编译环境搭建:
<Linux开发> linux开发工具-之-交叉编译环境搭建

uboot移植可参考以下:
<Linux开发> -之-系统移植 uboot移植过程详细记录(第一部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第二部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第三部分)(uboot移植完结)

Linux内核及设备树移植可参考以下:
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第一部分)
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第二部分完结)

Linux文件系统构建移植参考以下:
<Linux开发>系统移植 -之- linux构建BusyBox根文件系统及移植过程详细记录
<Linux开发>系统移植 -之-使用buildroot构建BusyBox根文件系统

Linux驱动开发参考以下:
<Linux开发>驱动开发 -之-pinctrl子系统
<Linux开发>驱动开发 -之-gpio子系统
<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的LED驱动
<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的beep驱动
<Linux开发>驱动开发 -之-资源的并发与竞争处理
<Linux开发>驱动开发 -之-内核定时器与中断
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知
<Linux开发>驱动开发 -之-Linux MISC 驱动
<Linux开发>驱动开发 -之-Linux INPUT 子系统
<Linux开发>驱动开发 -之- Linux LCD 驱动
<Linux开发>驱动开发 -之- Linux RTC 驱动
<Linux开发>驱动开发 -之- Linux I2C 驱动

资料:
I.MX6UL芯片手册
I.MX6ULL芯片手册
SecureCRT安装包

一 前言

对于SPI通信,其与I2C通信一样,都是非常常用的一种通信接口;相较于I2C通信接口,SPI接口的通信速率更快;对于I2C接口通信速率最快时400KHz,而SPI接口的通信速率可达几十MHz;本次使用的IMX6ULL包含有四个SPI,本文主要使用的时SPI3接口,其连接ICM-20608这个 六轴传感器。接下来我们就看看SPI的有关内容。

二 SPI

在笔者的另一个文章<Linux开发>驱动开发 -之- Linux I2C 驱动已经分析过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 线连接多个从设备的结构如下图所示:
在这里插入图片描述
图 2.1

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

这四种工作模式如下图所示:
在这里插入图片描述
图 2.2

SPI 时序图,以 CPOL=0,CPHA=0 这个工作模式为例,SPI 进行全双工通信的时序如下图所示:
在这里插入图片描述
图 2.3

从上图可以看出,SPI 的时序图很简单,不像 I2C 那样还要分为读时序和写时序,因为 SPI 是全双工的,所以读写时序可以一起完成。上图中,CS 片选信号先拉低,选中要通信的从设备,然后通过 MOSI 和 MISO 这两根数据线进行收发数据,MOSI 数据线发出了0XD2 这个数据给从设备,同时从设备也通过 MISO 线给主设备返回了 0X66 这个数据。
通过时序图可观察到,当SCLK由高变低,即下降沿时,数据线数据状态稳定,这是读取到的数据线极性才是正确的。这个就是 SPI 时序图。

前面也说了SPI是一种与I2C类似的通信方法,更多SPI的详细介绍读者可自行查阅有关资料、
对于I.MX6U来说,其自带了SPI外设控制器,名为ECSPI,我们接着看看ECSPI。

三 I.MX6U SPI

3.1 I.MX6U ECSPI介绍

增强型可配置串行外围接口(ECSPI)是一个全双工、同步、四线串行通信块。ECSPI包含一个64 x 32接收缓冲区(RXFIFO)和一个64 x 32传输缓冲区(TXFIFO)。通过数据FIFOs,ECSPI允许以更少的软件中断快速进行数据通信。ECSPI与普通SPI并无差异。

ECSPI的方框图如下:
在这里插入图片描述
图 3.1.1

ECSPI的主要功能包括:
①全双工同步串行接口
②主/从可配置
③四芯片选择(SS)信号支持多个外设
④传输延续功能允许无限长度数据传输
⑤32位x64输入FIFO,用于传输和接收数据
⑥芯片选择(SS)和SPI时钟(SCLK)的极性和相位是可配置的
⑦直接内存访问(DMA)
⑧最大工作频率到参考时钟频率

I.MX6U 的 ECSPI 可以工作在主模式或从模式,本文使用主模式,I.MX6U 有 4 个ECSPI,每个 ECSPI 支持四个片选信号,也就说,如果你要使用 ECSPI 的硬件片选信号的话,一个 ECSPI 可以支持 4 个外设。如果不使用硬件的片选信号就可以支持无数个外设,本文不使用硬件片选信号,因为硬件片选信号只能使用指定的片选 IO,软件片选的话可以使用任意的 IO。

3.2 I.MX6U ECSPI中断

中断控制提供了一种管理ECSPI FIFOs的方法:
①对于传输数据,软件可以启用TXFIFO为空的中断、TXFIFO数据请求和TXFIFO完全中断,以使用中断服务例程来维护TXFIFO。
②对于接收数据,软件可以启用RXFIFO准备就绪、RXFIFO数据请求和RXFIFO完全中断,以使用中断服务例程从RXFIFO检索数据。

其他中断源可用于控制或调试SPI突发:
①传输完成的中断意味着在TXFIFO中没有数据留下,并且在移位寄存器中的数据已经被移位。
②RXFIFO溢出中断意味着RXFIFO接收到超过64个字,并且不会接受任何其他字。

使用中断控制的SPI突发的程序序列如下图:
在这里插入图片描述
图3.2.1

3.3 I.MX6U ECSPI DMA

DMA控制提供了利用ECSPI中的FIFOs的另一种方法。通过使用DMA请求和确认信号,可以传输更多的数据,并将减少中断和主机处理器加载。当匹配了适当的条件时,该块将发送一个DMA请求。

DMA可以处理以下情况:
①TXFIFO空
②TXFIFO数据请求
③RXFIFO数据请求
④RXFIFO满
下图显示了使用DMA控制的SPI突发的程序序列。
DMASPI突发程序序列如下图:
在这里插入图片描述
图3.3.1

3.4 I.MX6U ECSPI 初始化

本节提供了ECSPI的初始化信息。
要初始化块,请执行以下操作
1.清除ECSPI_CONREG中的EN位以重置块
2.在CCM内启用ECSPI的时钟。
3.配置控制寄存器,然后在ECSPI_CONREG中设置EN位,使ECSPI退出复位。
4.为ECSPI的外部信号配置相应的IOMUX。
5.根据外部SPI设备的规格,正确配置ECSPI的寄存器。

ECSPI操作的流程图如下:
在这里插入图片描述

3.5 I.MX6U ECSPI 寄存器

了解了ECSPI 的基本信息后,我们就来看看ECSPI 的几个重要的寄存器。

3.5.1 ECSPIx_CONREG(x=1~4)寄存器

ECSPIx_CONREG(x=1~4)寄存器是ECSPI 的控制寄存器,此寄存器结构如下图所示:
在这里插入图片描述
寄存器 ECSPIx_CONREG 各位含义如下:

BURST_LENGTH(bit31:24):突发长度,设置 SPI 的突发传输数据长度,在一次 SPI 发送中最大可以发送 2^12bit 数据。可以设置 0X000~0XFFF,分别对应 1~2^12bit。我们一般设置突发长度为一个字节,也就是 8bit,BURST_LENGTH=7。

CHANNEL_SELECT(bit19:18):SPI 通道选择,一个 ECSPI 有四个硬件片选信号,每个片选信号是一个硬件通道,虽然我们本章实验使用的软件片选,但是 SPI 通道还是要选择的。可设置为 0~3,分别对应通道 0~3。I.MX6U开发板上的 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 时钟频率使用两步来完成分频,此位设置的是第一步,可设置 0~15,分别对应 1~16 分频。

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。

3.5.2 ECSPIx_CONFIGREG寄存器

接下来看一下寄存器 ECSPIx_CONFIGREG,这个也是 ECSPI 的配置寄存器,此寄存器结构如下图所示:
在这里插入图片描述
寄存器 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 的工作模式。

3.5.3 ECSPIx_PERIODREG寄存器

接下来看一下寄存器 ECSPIx_PERIODREG,这个是 ECSPI 的采样周期寄存器,此寄存器结构如下图所示:
在这里插入图片描述
寄存器 ECSPIx_PERIODREG 用到的重要位如下:
CSD_CTL(bit21:16):片选信号延时控制位,用于设置片选信号和第一个 SPI 时钟信号之间的时间间隔,范围为 0~63。
SAMPLE_PERIO(bit14:0):采样周期寄存器,可设置为 0~0X7FFF 分别对应 0~32767 个周期。
CSRC(bit15):SPI 时钟源选择,为 0 的话选择 SPI CLK 为 SPI 的时钟源,为 1 的话选择32.768KHz 的晶振为 SPI 时钟源。我们一般选择 SPI CLK 作为 SPI 时钟源,SPI CLK 时钟来源如下图所示:

在这里插入图片描述
上图中各部分含义如下:
①、这是一个选择器,用于选择根时钟源,由寄存器 CSCDR2 的位 ECSPI_CLK_SEL 来控制,为 0 的话选择 pll3_60m 作为 ECSPI 根时钟源。为 1 的话选择 osc_clk 作为 ECSPI 时钟源。
②、ECSPI 时钟分频值,由寄存器 CSCDR2 的位 ECSPI_CLK_PODF 来控制,分频值为2^ECSPI_CLK_PODF。
③、最终进入 ECSPI 的时钟,也就是 SPI CLK。

3.5.4 ECSPIx_STATREG寄存器

接下来看一下寄存器 ECSPIx_STATREG,这个是 ECSPI 的状态寄存器,此寄存器结构如下图所示:
在这里插入图片描述
寄存器 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
为空。

3.5.5 ECSPIx_RXDATA寄存器

接收数据寄存器(ECSPI_RXDATA)是一个只读寄存器,它形成了64 x 32接收FIFO的顶部字。此寄存器保存在数据事务期间从外部SPI设备接收到的数据。只允许进行字大小的读取操作。此寄存器结构如下图所示:
在这里插入图片描述
ECSPI_RXDATA(bit31-0):接收数据。该寄存器保存接收数据FIFO的顶部字。FIFO对该寄存器的每次读取都很先进。当清除中断控制/状态寄存器中的接收数据准备就绪(RR)位时,读取数据未定义。当禁用ECSPI时,将读取零。

3.5.6 ECSPIx_TXDATA寄存器

传输数据(ECSPI_TXDATA)寄存器是一个只写入的数据寄存器,它形成了64 x 32 TXFIFO的底部字。即使设置了ECSPI_CONREG中的SPI交换位(XCH),也可以写入TXFIFO。这使得软件可以在SPI数据交换过程中写入TXFIFO。当禁用ECSPI时(清除ECSPI_CONREG[EN]位),写入此寄存器将被忽略。此寄存器结构如下图所示:
在这里插入图片描述
ECSPIx_TXDATA(bit31-0):发送数据这个寄存器保存加载到FIFO中的数据的顶部字。写入此寄存器的数据必须是单词操作。实际传输的比特数由相应的SPI控制寄存器的BURST_LENGTH字段决定。如果此字段包含的位超过了BURST_LENGTH指定的数字,则将忽略额外的位。例如,要传输10位的数据,必须将一个32位的字写入这个寄存器。9-0位被移出,31-10位被忽略。当ECSPI在从属模式下工作时,当FIFO为空时,零会被移出。当禁用ECSPI时,将读取零。

关于这些寄存器更多详细的描述,请参考《I.MX6ULL 参考手册》第 805 页的 20.7 小节。

3.6 小节

本节主要是了解I.MX6ULL的ECSPI的相关寄存器和工作流程原理等。通过正确配置相关寄存器,即可进行SPI通信。

四 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 开发板上的 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 轴方向如下图所示:
在这里插入图片描述

ICM-20608 的结构框图如下图 所示:
在这里插入图片描述
如果使用 IIC 接口的话 ICM-20608 的 AD0 引脚决定 I2C 设备从地址的最后一位,如果 AD0为 0 的话 ICM-20608 从设备地址是 0X68,如果 AD0 为 1 的话 ICM-20608 从设备地址为 0X69。

本章我们使用 SPI 接口,跟使用 AP3216C 一样,ICM-20608 也是通过读写寄存器来配置和读取传感器数据,使用 SPI 接口读写寄存器需要 16 个时钟或者更多(如果读写操作包括多个字节的话),第一个字节包含要读写的寄存器地址,寄存器地址最高位是读写标志位,如果是读的话寄存器地址最高位要为 1,如果是写的话寄存器地址最高位要为 0,剩下的 7 位才是实际的寄存器地址,寄存器地址后面跟着的就是读写的数据。下表列出了用到的一些寄存器和位,关于 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。

五 Linux下的SPI

在前面讲 platform 的时候就说过,platform 是虚拟出来的一条总线,目的是为了实现总线、设备、驱动框架。对于 SPI 而言,不需要虚拟出一条总线,直接使用 SPI总线即可。SPI总线驱动重点是 SPI 适配器(也就是 SOC 的 SPI接口控制器)驱动。

在Linux设计中,将SPI代码框架分为三个部分:SPI总线、SPI核心、SPI驱动。
SPI核心(SPI-core): 主要定义i2c驱动所用到的通用API,高内聚的代码会放到i2c-core.c。
SPI主机驱动(SPI Master Driver): 根据平台定制的spi驱动,其中包含spi传输的算法设计。主要工作负责生成 spi_master,注册到内核中。
SPI设备驱动(SPI Device Driver): 驱动SPI设备的代码。spi设备驱动定义了外设的交互方式,与不同的SPI外设需要不同的设备驱动。SPI设备驱动对上和用户应用程序打交道,对下和SPI核心对接。
SPI架构图如下图所示:
在这里插入图片描述

5.1 SPI 主机驱动

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

struct spi_master {
	struct device	dev;

	struct list_head list;

	/* other than negative (== assign one dynamically), bus_num is fully
	 * board-specific.  usually that simplifies to being SOC-specific.
	 * example:  one SOC has three SPI controllers, numbered 0..2,
	 * and one board's schematics might show it using SPI-2.  software
	 * would normally use bus_num=2 for that controller.
	 */
	s16			bus_num;

	/* chipselects will be integral to many controllers; some others
	 * might use board-specific GPIOs.
	 */
	u16			num_chipselect;

	/* some SPI controllers pose alignment requirements on DMAable
	 * buffers; let protocol drivers know about these requirements.
	 */
	u16			dma_alignment;

	/* spi_device.mode flags understood by this controller driver */
	u16			mode_bits;

	/* bitmask of supported bits_per_word for transfers */
	u32			bits_per_word_mask;
#define SPI_BPW_MASK(bits) BIT((bits) - 1)
#define SPI_BIT_MASK(bits) (((bits) == 32) ? ~0U : (BIT(bits) - 1))
#define SPI_BPW_RANGE_MASK(min, max) (SPI_BIT_MASK(max) - SPI_BIT_MASK(min - 1))

	/* limits on transfer speed */
	u32			min_speed_hz;
	u32			max_speed_hz;

	/* other constraints relevant to this driver */
	u16			flags;
#define SPI_MASTER_HALF_DUPLEX	BIT(0)		/* can't do full duplex */
#define SPI_MASTER_NO_RX	BIT(1)		/* can't do buffer read */
#define SPI_MASTER_NO_TX	BIT(2)		/* can't do buffer write */
#define SPI_MASTER_MUST_RX      BIT(3)		/* requires rx */
#define SPI_MASTER_MUST_TX      BIT(4)		/* requires tx */

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

	/* Setup mode and clock, etc (spi driver may call many times).
	 *
	 * IMPORTANT:  this may be called when transfers to another
	 * device are active.  DO NOT UPDATE SHARED REGISTERS in ways
	 * which could break those transfers.
	 */
	int			(*setup)(struct spi_device *spi);

	/* bidirectional bulk transfers
	 *
	 * + The transfer() method may not sleep; its main role is
	 *   just to add the message to the queue.
	 * + For now there's no remove-from-queue operation, or
	 *   any other request management
	 * + To a given spi_device, message queueing is pure fifo
	 *
	 * + The master's main job is to process its message queue,
	 *   selecting a chip then transferring data
	 * + If there are multiple spi_device children, the i/o queue
	 *   arbitration algorithm is unspecified (round robin, fifo,
	 *   priority, reservations, preemption, etc)
	 *
	 * + Chipselect stays active during the entire message
	 *   (unless modified by spi_transfer.cs_change != 0).
	 * + The message transfers use clock and SPI mode parameters
	 *   previously established by setup() for this device
	 */
	int			(*transfer)(struct spi_device *spi,
						struct spi_message *mesg);

	/* called on release() to free memory provided by spi_master */
	void			(*cleanup)(struct spi_device *spi);

	/*
	 * Used to enable core support for DMA handling, if can_dma()
	 * exists and returns true then the transfer will be mapped
	 * prior to transfer_one() being called.  The driver should
	 * not modify or store xfer and dma_tx and dma_rx must be set
	 * while the device is prepared.
	 */
	bool			(*can_dma)(struct spi_master *master,
					   struct spi_device *spi,
					   struct spi_transfer *xfer);

	/*
	 * These hooks are for drivers that want to use the generic
	 * master transfer queueing mechanism. If these are used, the
	 * transfer() function above must NOT be specified by the driver.
	 * Over time we expect SPI drivers to be phased over to this API.
	 */
	bool				queued;
	struct kthread_worker		kworker;
	struct task_struct		*kworker_task;
	struct kthread_work		pump_messages;
	spinlock_t			queue_lock;
	struct list_head		queue;
	struct spi_message		*cur_msg;
	bool				idling;
	bool				busy;
	bool				running;
	bool				rt;
	bool				auto_runtime_pm;
	bool                            cur_msg_prepared;
	bool				cur_msg_mapped;
	struct completion               xfer_completion;
	size_t				max_dma_len;

	int (*prepare_transfer_hardware)(struct spi_master *master);
	int (*transfer_one_message)(struct spi_master *master,
				    struct spi_message *mesg);
	int (*unprepare_transfer_hardware)(struct spi_master *master);
	int (*prepare_message)(struct spi_master *master,
			       struct spi_message *message);
	int (*unprepare_message)(struct spi_master *master,
				 struct spi_message *message);

	/*
	 * These hooks are for drivers that use a generic implementation
	 * of transfer_one_message() provied by the core.
	 */
	void (*set_cs)(struct spi_device *spi, bool enable);
	int (*transfer_one)(struct spi_master *master, struct spi_device *spi,
			    struct spi_transfer *transfer);
	void (*handle_err)(struct spi_master *master,
			   struct spi_message *message);

	/* gpio chip select */
	int			*cs_gpios;

	/* DMA channels for use with core dmaengine helpers */
	struct dma_chan		*dma_tx;
	struct dma_chan		*dma_rx;

	/* dummy data for full duplex devices */
	void			*dummy_rx;
	void			*dummy_tx;
};

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

第 120 行,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。

5.1.1 spi_master 申请与释放

spi_alloc_master 函数用于申请 spi_master,函数原型如下:

路径:drivers\spi\spi.c
struct spi_master *spi_alloc_master(struct device *dev, unsigned size)
{
	struct spi_master	*master;

	if (!dev)
		return NULL;

	master = kzalloc(size + sizeof(*master), GFP_KERNEL);
	if (!master)
		return NULL;

	device_initialize(&master->dev);
	master->bus_num = -1;
	master->num_chipselect = 1;
	master->dev.class = &spi_master_class;
	master->dev.parent = get_device(dev);
	spi_master_set_devdata(master, &master[1]);

	return master;
}
EXPORT_SYMBOL_GPL(spi_alloc_master);

函数参数和返回值含义如下:

dev:设备,一般是 platform_device 中的 dev 成员变量。
size:私有数据大小,可以通过 spi_master_get_devdata 函数获取到这些私有数据。
返回值:申请到的 spi_master。

spi_master 的释放通过 spi_master_put 函数来完成,当我们删除一个 SPI 主机驱动的时候就需要释放掉前面申请的 spi_master,spi_master_put 函数原型如下:

路径:include\linux\spi\spi.h
static inline void spi_master_put(struct spi_master *master)
{
	if (master)
		put_device(&master->dev);
}

路径:drivers\base\core.c
void put_device(struct device *dev)
{
	/* might_sleep(); */
	if (dev)
		kobject_put(&dev->kobj);
}
EXPORT_SYMBOL_GPL(put_device);

函数参数和返回值含义如下:
master:要释放的 spi_master。
返回值:无。

5.1.2 spi_master 的注册与注销

当 spi_master 初始化完成以后就需要将其注册到 Linux 内核,spi_master 注册函数为spi_register_master,函数原型如下:

路径:drivers\spi\spi.c
int spi_register_master(struct spi_master *master)
{
	static atomic_t		dyn_bus_id = ATOMIC_INIT((1<<15) - 1);
	struct device		*dev = master->dev.parent;
	struct boardinfo	*bi;
	int			status = -ENODEV;
	int			dynamic = 0;

	if (!dev)
		return -ENODEV;

	status = of_spi_register_master(master);
	if (status)
		return status;

	/* even if it's just one always-selected device, there must
	 * be at least one chipselect
	 */
	if (master->num_chipselect == 0)
		return -EINVAL;

	if ((master->bus_num < 0) && master->dev.of_node)
		master->bus_num = of_alias_get_id(master->dev.of_node, "spi");

	/* convention:  dynamically assigned bus IDs count down from the max */
	if (master->bus_num < 0) {
		/* FIXME switch to an IDR based scheme, something like
		 * I2C now uses, so we can't run out of "dynamic" IDs
		 */
		master->bus_num = atomic_dec_return(&dyn_bus_id);
		dynamic = 1;
	}

	INIT_LIST_HEAD(&master->queue);
	spin_lock_init(&master->queue_lock);
	spin_lock_init(&master->bus_lock_spinlock);
	mutex_init(&master->bus_lock_mutex);
	master->bus_lock_flag = 0;
	init_completion(&master->xfer_completion);
	if (!master->max_dma_len)
		master->max_dma_len = INT_MAX;

	/* register the device, then userspace will see it.
	 * registration fails if the bus ID is in use.
	 */
	dev_set_name(&master->dev, "spi%u", master->bus_num);
	status = device_add(&master->dev);
	if (status < 0)
		goto done;
	dev_dbg(dev, "registered master %s%s\n", dev_name(&master->dev),
			dynamic ? " (dynamic)" : "");

	/* If we're using a queued driver, start the queue */
	if (master->transfer)
		dev_info(dev, "master is unqueued, this is deprecated\n");
	else {
		status = spi_master_initialize_queue(master);
		if (status) {
			device_del(&master->dev);
			goto done;
		}
	}

	mutex_lock(&board_lock);
	list_add_tail(&master->list, &spi_master_list);
	list_for_each_entry(bi, &board_list, list)
		spi_match_master_to_boardinfo(master, &bi->board_info);
	mutex_unlock(&board_lock);

	/* Register devices from the device tree and ACPI */
	of_register_spi_devices(master);
	acpi_register_spi_devices(master);
done:
	return status;
}
EXPORT_SYMBOL_GPL(spi_register_master);

函数参数和返回值含义如下:
master:要注册的 spi_master。
返回值:0,成功;负值,失败。

I.MX6U 的 SPI 主机驱动会采用 spi_bitbang_start 这个 API 函数来完成 spi_master 的注册,spi_bitbang_start 函数内部其实也是通过调用 spi_register_master 函数来完成 spi_master 的注册。后面分析I.MX6U时我们在分析看看。

如果要注销 spi_master 的话可以使用 spi_unregister_master 函数,此函数原型为:

路径:drivers\spi\spi.c
void spi_unregister_master(struct spi_master *master)
{
	int dummy;

	if (master->queued) {
		if (spi_destroy_queue(master))
			dev_err(&master->dev, "queue remove failed\n");
	}

	mutex_lock(&board_lock);
	list_del(&master->list);
	mutex_unlock(&board_lock);

	dummy = device_for_each_child(&master->dev, NULL, __unregister); //从设备表里移除
	device_unregister(&master->dev);	//注销掉
}
EXPORT_SYMBOL_GPL(spi_unregister_master);

函数参数和返回值含义如下:
master:要注销的 spi_master。
返回值:无。

如果使用 spi_bitbang_start 注册 spi_master 的话就要使用 spi_bitbang_stop 来注销掉spi_master。

5.2 SPI 设备驱动

5.2.1 spi_driver 结构体注册和注销

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

路径:include\linux\spi\spi.h
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,函数原型如下:

路径:drivers\spi\spi.c
int spi_register_driver(struct spi_driver *sdrv)
{
	sdrv->driver.bus = &spi_bus_type;
	if (sdrv->probe)
		sdrv->driver.probe = spi_drv_probe;
	if (sdrv->remove)
		sdrv->driver.remove = spi_drv_remove;
	if (sdrv->shutdown)
		sdrv->driver.shutdown = spi_drv_shutdown;
	return driver_register(&sdrv->driver);
}
EXPORT_SYMBOL_GPL(spi_register_driver);

路径:drivers\base\driver.c
	int driver_register(struct device_driver *drv)

函数参数和返回值含义如下:
sdrv:要注册的 spi_driver。
返回值:0,注册成功;赋值,注册失败。

注销 SPI 设备驱动以后也需要注销掉前面注册的 spi_driver,使用 spi_unregister_driver 函数完成 spi_driver 的注销,函数原型如下:

路径:include\linux\spi\spi.h
static inline void spi_unregister_driver(struct spi_driver *sdrv)
{
	if (sdrv)
		driver_unregister(&sdrv->driver);
}

路径:drivers\base\driver.c
void driver_unregister(struct device_driver *drv)
{
	if (!drv || !drv->p) {
		WARN(1, "Unexpected driver unregister!\n");
		return;
	}
	driver_remove_groups(drv, drv->groups);
	bus_remove_driver(drv);
}
EXPORT_SYMBOL_GPL(driver_unregister);

5.2.2 spi_driver 注册示例程序

spi_driver 注册示例程序如下:

/* probe 函数 */
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);

第 1~37 行,spi_driver 结构体,需要 SPI 设备驱动人员编写,包括匹配表、probe 函数等。和 i2c_driver、platform_driver 一样,就不详细讲解了。
第 40~43 行,在驱动入口函数中调用 spi_register_driver 来注册 spi_driver。
第 46~49 行,在驱动出口函数中调用 spi_unregister_driver 来注销 spi_driver。

5.3 SPI 设备和驱动匹配过程

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

路径:drivers\spi\spi.c
struct bus_type spi_bus_type = {
	.name		= "spi",
	.dev_groups	= spi_dev_groups,
	.match		= spi_match_device,
	.uevent		= spi_uevent,
};
EXPORT_SYMBOL_GPL(spi_bus_type);

可以看出,SPI 设备和驱动的匹配函数为 spi_match_device,函数内容如下:

路径:drivers\spi\spi.c
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 函数对于设备和驱动的匹配过程基本一样。

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

详细匹配流程如下图所示(可点击放大查看):
在这里插入图片描述

六 I.MX6U的SPI主机驱动

在分析I2C的时候,我们讲过,对于I2C控制器驱动是由SOC原厂写好的,那么SPI的主机驱动也是一样。不同的SOC原厂,对应的SPI主机控制器都有一定的差异性,而且原厂为了自家芯片能够被快速广泛使用,都会将对应的SPI主机驱动 或 其它 SOC内的外设驱动编写好。既然知道NXP已经把SPI驱动写好了,那我们就来看看有哪些内容。

6.1 SPI主机设备树

在设备树中NXP已经写好SPI主机设备树了,内容如下:

路径:arch\arm\boot\dts\imx6ull.dtsi
ecspi1: ecspi@02008000 {
#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
	reg = <0x02008000 0x4000>;
	interrupts = <GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_ECSPI1>,
		 <&clks IMX6UL_CLK_ECSPI1>;
	clock-names = "ipg", "per";
	dmas = <&sdma 3 7 1>, <&sdma 4 7 2>;
	dma-names = "rx", "tx";
	status = "disabled";
};

ecspi2: ecspi@0200c000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
	reg = <0x0200c000 0x4000>;
	interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_ECSPI2>,
		 <&clks IMX6UL_CLK_ECSPI2>;
	clock-names = "ipg", "per";
	dmas = <&sdma 5 7 1>, <&sdma 6 7 2>;
	dma-names = "rx", "tx";
	status = "disabled";
};

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

ecspi4: ecspi@02014000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
	reg = <0x02014000 0x4000>;
	interrupts = <GIC_SPI 34 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_ECSPI4>,
		 <&clks IMX6UL_CLK_ECSPI4>;
	clock-names = "ipg", "per";
	dmas = <&sdma 9 7 1>, <&sdma 10 7 2>;
	dma-names = "rx", "tx";
	status = "disabled";
};

这里将4个SPI主机都写好了,我们这次主要是用到ecspi3,因为我们的外设ICM-20608传感器就是连接ecspi3这个主机上的。

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

6.2 SPI主机驱动

通过compatible属性值查找 I.MX6U 的 ECSPI 主机驱动文件部分内容如下:

路径:drivers\spi\spi-imx.c
static struct platform_device_id spi_imx_devtype[] = {
	{
		.name = "imx1-cspi",
		.driver_data = (kernel_ulong_t) &imx1_cspi_devtype_data,
	}, {
		.name = "imx21-cspi",
		.driver_data = (kernel_ulong_t) &imx21_cspi_devtype_data,
	}, {
		.name = "imx27-cspi",
		.driver_data = (kernel_ulong_t) &imx27_cspi_devtype_data,
	}, {
		.name = "imx31-cspi",
		.driver_data = (kernel_ulong_t) &imx31_cspi_devtype_data,
	}, {
		.name = "imx35-cspi",
		.driver_data = (kernel_ulong_t) &imx35_cspi_devtype_data,
	}, {
		.name = "imx51-ecspi",
		.driver_data = (kernel_ulong_t) &imx51_ecspi_devtype_data,
	}, {
		.name = "imx6ul-ecspi",
		.driver_data = (kernel_ulong_t) &imx6ul_ecspi_devtype_data,
	}, {
		/* sentinel */
	}
};

static const struct of_device_id spi_imx_dt_ids[] = {
	{ .compatible = "fsl,imx1-cspi", .data = &imx1_cspi_devtype_data, },
	{ .compatible = "fsl,imx21-cspi", .data = &imx21_cspi_devtype_data, },
	{ .compatible = "fsl,imx27-cspi", .data = &imx27_cspi_devtype_data, },
	{ .compatible = "fsl,imx31-cspi", .data = &imx31_cspi_devtype_data, },
	{ .compatible = "fsl,imx35-cspi", .data = &imx35_cspi_devtype_data, },
	{ .compatible = "fsl,imx51-ecspi", .data = &imx51_ecspi_devtype_data, },
	{ .compatible = "fsl,imx6ul-ecspi", .data = &imx6ul_ecspi_devtype_data, },
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, spi_imx_dt_ids);
.....
.....
static struct platform_driver spi_imx_driver = {
	.driver = {
		   .name = DRIVER_NAME,
		   .of_match_table = spi_imx_dt_ids,
		   .pm = IMX_SPI_PM,
	},
	.id_table = spi_imx_devtype,
	.probe = spi_imx_probe,
	.remove = spi_imx_remove,
};
module_platform_driver(spi_imx_driver);
.......
.......

第 22 行,spi_imx_devtype 为 SPI 无设备树匹配表。
第 29 行,spi_imx_dt_ids 为 SPI 设备树匹配表。
第 36 行,“fsl,imx6ul-ecspi”匹配项,因此可知 I.MX6U 的 ECSPI 驱动就是 spi-imx.c 这个
文件。
第 42~52 行,platform_driver 驱动框架,和 I2C 的适配器驱动一样,SPI 主机驱动器采
用了 platfom 驱动框架。当设备和驱动匹配成功以后 spi_imx_probe 函数就会执行。

6.3 SPI主机驱动spi_imx_probe函数

spi_imx_probe 函数会从设备树中读取相应的节点属性值,申请并初始化 spi_master,最后调用 spi_bitbang_start 函数(spi_bitbang_start 会调用 spi_register_master 函数)向 Linux 内核注册spi_master。

路径:drivers\spi\spi-imx.c
static int spi_imx_probe(struct platform_device *pdev)
{
	struct device_node *np = pdev->dev.of_node;
	const struct of_device_id *of_id =
			of_match_device(spi_imx_dt_ids, &pdev->dev);
	struct spi_imx_master *mxc_platform_info =
			dev_get_platdata(&pdev->dev);
	struct spi_master *master;
	struct spi_imx_data *spi_imx;
	struct resource *res;
	int i, ret, num_cs, irq;

	if (!np && !mxc_platform_info) {
		dev_err(&pdev->dev, "can't get the platform data\n");
		return -EINVAL;
	}

	ret = of_property_read_u32(np, "fsl,spi-num-chipselects", &num_cs);
	if (ret < 0) {
		if (mxc_platform_info)
			num_cs = mxc_platform_info->num_chipselect;
		else
			return ret;
	}

	master = spi_alloc_master(&pdev->dev,
			sizeof(struct spi_imx_data) + sizeof(int) * num_cs);
	if (!master)
		return -ENOMEM;

	platform_set_drvdata(pdev, master);

	master->bits_per_word_mask = SPI_BPW_RANGE_MASK(1, 32);
	master->bus_num = pdev->id;
	master->num_chipselect = num_cs;

	spi_imx = spi_master_get_devdata(master);
	spi_imx->bitbang.master = master;

	for (i = 0; i < master->num_chipselect; i++) {
		int cs_gpio = of_get_named_gpio(np, "cs-gpios", i);
		if (!gpio_is_valid(cs_gpio) && mxc_platform_info)
			cs_gpio = mxc_platform_info->chipselect[i];

		spi_imx->chipselect[i] = cs_gpio;
		if (!gpio_is_valid(cs_gpio))
			continue;

		ret = devm_gpio_request(&pdev->dev, spi_imx->chipselect[i],
					DRIVER_NAME);
		if (ret) {
			dev_err(&pdev->dev, "can't get cs gpios\n");
			goto out_master_put;
		}
	}

	spi_imx->bitbang.chipselect = spi_imx_chipselect;
	spi_imx->bitbang.setup_transfer = spi_imx_setupxfer;
	spi_imx->bitbang.txrx_bufs = spi_imx_transfer;
	spi_imx->bitbang.master->setup = spi_imx_setup;
	spi_imx->bitbang.master->cleanup = spi_imx_cleanup;
	spi_imx->bitbang.master->prepare_message = spi_imx_prepare_message;
	spi_imx->bitbang.master->unprepare_message = spi_imx_unprepare_message;
	spi_imx->bitbang.master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;

	init_completion(&spi_imx->xfer_done);

	spi_imx->devtype_data = of_id ? of_id->data :
		(struct spi_imx_devtype_data *) pdev->id_entry->driver_data;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	spi_imx->base = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(spi_imx->base)) {
		ret = PTR_ERR(spi_imx->base);
		goto out_master_put;
	}

	irq = platform_get_irq(pdev, 0);
	if (irq < 0) {
		ret = irq;
		goto out_master_put;
	}

	ret = devm_request_irq(&pdev->dev, irq, spi_imx_isr, 0,
			       dev_name(&pdev->dev), spi_imx);
	if (ret) {
		dev_err(&pdev->dev, "can't get irq%d: %d\n", irq, ret);
		goto out_master_put;
	}

	spi_imx->clk_ipg = devm_clk_get(&pdev->dev, "ipg");
	if (IS_ERR(spi_imx->clk_ipg)) {
		ret = PTR_ERR(spi_imx->clk_ipg);
		goto out_master_put;
	}

	spi_imx->clk_per = devm_clk_get(&pdev->dev, "per");
	if (IS_ERR(spi_imx->clk_per)) {
		ret = PTR_ERR(spi_imx->clk_per);
		goto out_master_put;
	}

	ret = clk_prepare_enable(spi_imx->clk_per);
	if (ret)
		goto out_master_put;

	ret = clk_prepare_enable(spi_imx->clk_ipg);
	if (ret)
		goto out_put_per;

	spi_imx->spi_clk = clk_get_rate(spi_imx->clk_per);
	/*
	 * Only validated on i.mx6 now, can remove the constrain if validated on
	 * other chips.
	 */
	if ((spi_imx->devtype_data == &imx51_ecspi_devtype_data
	    || spi_imx->devtype_data == &imx6ul_ecspi_devtype_data)
	    && spi_imx_sdma_init(&pdev->dev, spi_imx, master, res))
		dev_err(&pdev->dev, "dma setup error,use pio instead\n");

	spi_imx->devtype_data->reset(spi_imx);

	spi_imx->devtype_data->intctrl(spi_imx, 0);

	master->dev.of_node = pdev->dev.of_node;
	ret = spi_bitbang_start(&spi_imx->bitbang);
	if (ret) {
		dev_err(&pdev->dev, "bitbang start failed with %d\n", ret);
		goto out_clk_put;
	}

	dev_info(&pdev->dev, "probed\n");

	clk_disable_unprepare(spi_imx->clk_ipg);
	clk_disable_unprepare(spi_imx->clk_per);
	return ret;

out_clk_put:
	clk_disable_unprepare(spi_imx->clk_ipg);
out_put_per:
	clk_disable_unprepare(spi_imx->clk_per);
out_master_put:
	spi_master_put(master);

	return ret;
}

第 5-8 行: 获取设备树信息。
第 27-28 行:申请 spi_master;
第 32 行:初始化 spi_master;
第 72 行:获取设备地址;
第 73 行:映射设备地址到内核空间,得到虚拟地址;
第 79 行:获取中断;
第 127 行:调用spi_bitbang_start,进一步调用spi_register_master()函数 进行注册到kernel;

spi_bitbang_start()函数路径:drivers\spi\spi-bitbang.c

6.4 SPI主机驱动发送函数spi_imx_transfer()

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

路径:drivers\spi\spi-imx.c
static int spi_imx_transfer(struct spi_device *spi,
				struct spi_transfer *transfer)
{
	int ret;
	struct spi_imx_data *spi_imx = spi_master_get_devdata(spi->master);

	if (spi_imx->bitbang.master->can_dma &&
	    spi_imx_can_dma(spi_imx->bitbang.master, spi, transfer)) {
		spi_imx->usedma = true;
		ret = spi_imx_dma_transfer(spi_imx, transfer);
		spi_imx->usedma = false; /* clear the dma flag */
		if (ret != -EAGAIN)
			return ret;
	}
	spi_imx->usedma = false;

	return spi_imx_pio_transfer(spi, transfer);
}

/* 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 函数。

路径:drivers\spi\spi-imx.c
struct spi_imx_data {
	struct spi_bitbang bitbang;

	struct completion xfer_done;
	void __iomem *base;
	struct clk *clk_per;
	struct clk *clk_ipg;
	unsigned long spi_clk;

	unsigned int count;
	void (*tx)(struct spi_imx_data *);
	void (*rx)(struct spi_imx_data *);
	void *rx_buf;
	const void *tx_buf;
	unsigned int txfifo; /* number of words pushed in tx FIFO */

	/* DMA */
	unsigned int dma_is_inited;
	unsigned int dma_finished;
	bool usedma;
	u32 rx_wml;
	u32 tx_wml;
	u32 rxt_wml;
	struct completion dma_rx_completion;
	struct completion dma_tx_completion;
	struct dma_slave_config rx_config;
	struct dma_slave_config tx_config;

	const struct spi_imx_devtype_data *devtype_data;
	int chipselect[0];
};
static int spi_imx_setupxfer(struct spi_device *spi,
				 struct spi_transfer *t)
{
	struct spi_imx_data *spi_imx = spi_master_get_devdata(spi->master);
	struct spi_imx_config config;
	int ret;

	config.bpw = t ? t->bits_per_word : spi->bits_per_word;
	config.speed_hz  = t ? t->speed_hz : spi->max_speed_hz;
	config.mode = spi->mode;
	config.cs = spi->chip_select;

	if (!config.speed_hz)
		config.speed_hz = spi->max_speed_hz;
	if (!config.bpw)
		config.bpw = spi->bits_per_word;

	/* Initialize the functions for transfer */
	if (config.bpw <= 8) {
		spi_imx->rx = spi_imx_buf_rx_u8;
		spi_imx->tx = spi_imx_buf_tx_u8;
		spi_imx->tx_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
		spi_imx->rx_config.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
	} else if (config.bpw <= 16) {
		spi_imx->rx = spi_imx_buf_rx_u16;
		spi_imx->tx = spi_imx_buf_tx_u16;
		spi_imx->tx_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES;
		spi_imx->rx_config.src_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES;
	} else {
		spi_imx->rx = spi_imx_buf_rx_u32;
		spi_imx->tx = spi_imx_buf_tx_u32;
		spi_imx->tx_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
		spi_imx->rx_config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
	}

	if (spi_imx->bitbang.master->can_dma &&
	    spi_imx_can_dma(spi_imx->bitbang.master, spi, t)) {
		ret = dmaengine_slave_config(spi_imx->bitbang.master->dma_tx,
						&spi_imx->tx_config);
		if (ret) {
			dev_err(&spi->dev, "error in TX dma configuration.\n");
			return ret;
		}

		ret = dmaengine_slave_config(spi_imx->bitbang.master->dma_rx,
						&spi_imx->rx_config);
		if (ret) {
			dev_err(&spi->dev, "error in RX dma configuration.\n");
			return ret;
		}
	}

	spi_imx->devtype_data->config(spi_imx, &config);

	return 0;
}

在SPI主机驱动的probe函数中有 spi_imx->bitbang.setup_transfer = spi_imx_setupxfer; 这样设备驱动就可以通过setup_transfer()函数接口设置数据的位宽了。
根据要发送的数据数据位宽的不同,分别有 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_buf_tx_u16
spi_imx_buf_tx_u32

我们一起看下spi_imx_buf_rx_u8 这个读取u8的函数

在SPI主机驱动源码中有如下内容:

路径:drivers\spi\spi-imx.c
#define MXC_SPI_BUF_RX(type)						\
static void spi_imx_buf_rx_##type(struct spi_imx_data *spi_imx)		\
{									\
	unsigned int val = readl(spi_imx->base + MXC_CSPIRXDATA);	\
									\
	if (spi_imx->rx_buf) {						\
		*(type *)spi_imx->rx_buf = val;				\
		spi_imx->rx_buf += sizeof(type);			\
	}								\
}

#define MXC_SPI_BUF_TX(type)						\
static void spi_imx_buf_tx_##type(struct spi_imx_data *spi_imx)		\
{									\
	type val = 0;							\
									\
	if (spi_imx->tx_buf) {						\
		val = *(type *)spi_imx->tx_buf;				\
		spi_imx->tx_buf += sizeof(type);			\
	}								\
									\
	spi_imx->count -= sizeof(type);					\
									\
	writel(val, spi_imx->base + MXC_CSPITXDATA);			\
}

MXC_SPI_BUF_RX(u8)
MXC_SPI_BUF_TX(u8)
MXC_SPI_BUF_RX(u16)
MXC_SPI_BUF_TX(u16)
MXC_SPI_BUF_RX(u32)
MXC_SPI_BUF_TX(u32)

对于MXC_SPI_BUF_RX(u8),上述代码经过宏替换后会变成如下内容:

static void spi_imx_buf_rx_u8(struct spi_imx_data *spi_imx)
{
	unsigned int val = readl(spi_imx->base + MXC_CSPIRXDATA);

	if (spi_imx->rx_buf) {
		*(type *)spi_imx->rx_buf = val;
		spi_imx->rx_buf += sizeof(type);	
	}
}

最终读取的数据是从MXC_CSPIRXDATA(在3.5.5小节说明过该寄存器)这个寄存器读取数据的,然后及那个读取的数据放到spi_imx->rx_buf中。

同样MXC_SPI_BUF_TX(u8),经过宏替换后会变成如下内容:

static void spi_imx_buf_tx_u8(struct spi_imx_data *spi_imx)
{
	type val = 0;	

	if (spi_imx->tx_buf) {	
		val = *(type *)spi_imx->tx_buf;
		spi_imx->tx_buf += sizeof(type);	
	}	

	spi_imx->count -= sizeof(type);	

	writel(val, spi_imx->base + MXC_CSPITXDATA);
}

最终将数据写入到SPI主机的MXC_CSPITXDATA寄存器中(在3.5.6小节说明过该寄存器)。

关于 I.MX6U 的主机驱动程序就讲解到这里,基本套路和 I2C 的适配器驱动程序类似。

七 一般SPI涉笔驱动编写流程

7.1 SPI设备信息描述

(1)新增IO的pinctrl节点
首先需检查所使用的IO口是否被其它设备使用,有的话需要删除,然后添加当前需要使用的pinctrl节点,例如如下:

路径:arch\arm\boot\dts\imx6qdl-phytec-pfla02.dtsi
pinctrl_ecspi3: ecspi3grp {
			fsl,pins = <
				MX6QDL_PAD_DISP0_DAT2__ECSPI3_MISO	0x100b1
				MX6QDL_PAD_DISP0_DAT1__ECSPI3_MOSI	0x100b1
				MX6QDL_PAD_DISP0_DAT0__ECSPI3_SCLK	0x100b1
			>;
		};

如果我们添加自己的设备pinctrl节点,也需要参考上述代码格式。

(2)新增spi设备节点
添加完 pinctrl节点,接着就需要添加具体的SPI设备的 设备节点了,以前面的pinctrl_ecspi3为例,新增SPI设备m25p80的节点如下:

路径:arch\arm\boot\dts\imx6qdl-phytec-pfla02.dtsi
&ecspi3 {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_ecspi3>;
	status = "okay";
	fsl,spi-num-chipselects = <1>;
	cs-gpios = <&gpio4 24 0>;

	flash@0 {
		compatible = "m25p80";
		spi-max-frequency = <20000000>;
		reg = <0>;
	};
};

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

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

我们实际添加使用的ICM20608设备也需要参考如此新增设备节点。

7.2 SPI 设备数据收发处理流程

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

路径:include\linux\spi\spi.h
struct spi_transfer {
	/* it's ok if tx_buf == rx_buf (right?)
	 * for MicroWire, one buffer must be null
	 * buffers must work with dma_*map_single() calls, unless
	 *   spi_message.is_dma_mapped reports a pre-existing mapping
	 */
	const void	*tx_buf;
	void		*rx_buf;
	unsigned	len;

	dma_addr_t	tx_dma;
	dma_addr_t	rx_dma;
	struct sg_table tx_sg;
	struct sg_table rx_sg;

	unsigned	cs_change:1;
	unsigned	tx_nbits:3;
	unsigned	rx_nbits:3;
#define	SPI_NBITS_SINGLE	0x01 /* 1bit transfer */
#define	SPI_NBITS_DUAL		0x02 /* 2bits transfer */
#define	SPI_NBITS_QUAD		0x04 /* 4bits transfer */
	u8		bits_per_word;
	u16		delay_usecs;
	u32		speed_hz;

	struct list_head transfer_list;
};

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

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

路径:include\linux\spi\spi.h
struct spi_message {
	struct list_head	transfers;

	struct spi_device	*spi;

	unsigned		is_dma_mapped:1;

	/* REVISIT:  we might want a flag affecting the behavior of the
	 * last transfer ... allowing things like "read 16 bit length L"
	 * immediately followed by "read L bytes".  Basically imposing
	 * a specific message scheduling algorithm.
	 *
	 * Some controller drivers (message-at-a-time queue processing)
	 * could provide that as their default scheduling algorithm.  But
	 * others (with multi-message pipelines) could need a flag to
	 * tell them about such special cases.
	 */

	/* completion is reported through a callback */
	void			(*complete)(void *context);
	void			*context;
	unsigned		frame_length;
	unsigned		actual_length;
	int			status;

	/* for optional use by whatever driver currently owns the
	 * spi_message ...  between calls to spi_async and then later
	 * complete(), that's the spi_master controller driver.
	 */
	struct list_head	queue;
	void			*state;
};

在使用spi_message之前需要对其进行初始化,spi_message初始化函数为spi_message_init,函数原型如下:

路径:include\linux\spi\spi.h
static inline void spi_message_init(struct spi_message *m)
{
	memset(m, 0, sizeof *m);
	INIT_LIST_HEAD(&m->transfers);
}
路径:include\linux\list.h
static inline void INIT_LIST_HEAD(struct list_head *list)
{
	list->next = list;
	list->prev = list;
}

函数参数和返回值含义如下:
m:要初始化的 spi_message。
返回值:无。

spi_message 初始化完成以后需要将 spi_transfer 添加到 spi_message 队列中,这里我们要用到 spi_message_add_tail 函数,此函数原型如下:

路径:include\linux\spi\spi.h
static inline void
spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
{
	list_add_tail(&t->transfer_list, &m->transfers);
}
路径:include\linux\list.h
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
	__list_add(new, head->prev, head);
}
static inline void __list_add(struct list_head *new,
			      struct list_head *prev,
			      struct list_head *next)
{
	next->prev = new;
	new->next = next;
	new->prev = prev;
	prev->next = new;
}

函数参数和返回值含义如下:
t:要添加到队列中的 spi_transfer。
m:spi_transfer 要加入的 spi_message。
返回值:无。

spi_message 准备好以后就可以进行数据传输了,数据传输分为同步传输和异步传输,同步传输会阻塞的等待 SPI 数据传输完成,同步传输函数为 spi_sync,函数原型如下:

路径:drivers\spi\spi.c
int spi_sync(struct spi_device *spi, struct spi_message *message)
{
	return __spi_sync(spi, message, 0);
}
EXPORT_SYMBOL_GPL(spi_sync);

函数参数和返回值含义如下:
spi:要进行数据传输的 spi_device。
message:要传输的 spi_message。
返回值:无。

异步传输不会阻塞的等到 SPI 数据传输完成,异步传输需要设置 spi_message 中的 complete成员变量,complete 是一个回调函数,当 SPI 异步传输完成以后此函数就会被调用。SPI 异步传输函数为 spi_async,函数原型如下:

路径:drivers\spi\spi.c
int spi_async(struct spi_device *spi, struct spi_message *message)
{
	struct spi_master *master = spi->master;
	int ret;
	unsigned long flags;

	ret = __spi_validate(spi, message);
	if (ret != 0)
		return ret;

	spin_lock_irqsave(&master->bus_lock_spinlock, flags);

	if (master->bus_lock_flag)
		ret = -EBUSY;
	else
		ret = __spi_async(spi, message);

	spin_unlock_irqrestore(&master->bus_lock_spinlock, flags);

	return ret;
}
EXPORT_SYMBOL_GPL(spi_async);

函数参数和返回值含义如下:
spi:要进行数据传输的 spi_device。
message:要传输的 spi_message。
返回值:无。

7.3 SPI 设备驱动总结

SPI 数据传输步骤如下:
①、申请并初始化 spi_transfer,设置 spi_transfer 的 tx_buf 成员变量,tx_buf 为要发送的数据。然后设置 rx_buf 成员变量,rx_buf 保存着接收到的数据。最后设置 len 成员变量,也就是要进行数据通信的长度。
②、使用 spi_message_init 函数初始化 spi_message。
③、使用spi_message_add_tail函数将前面设置好的spi_transfer添加到spi_message 队列中。
④、使用 spi_sync 函数完成 SPI 数据同步传输。

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

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

八 ICM-20608设备驱动编写

8.1 硬件原理图

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

8.2 新增设备树

参考7.1节新增设备树,
新增pinctrl节点,找到 iomuxc 节点,在 iomuxc 节点下新增pinctrl_ecspi3节点,如下:

路径:arch\arm\boot\dts\imx6ull-water-emmc.dts
pinctrl_ecspi3: ecspi3grp {
			fsl,pins = <
				MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20	 	0x10b0 	/* CS */
				MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 	0x10b1 	/* SPI3_SCLK */
				MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 		0x10b1 	/* SPI3_MOSI */
				MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 		0x10b1 	/* SPI3_MISO */
			>;
		};

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

新增 icm20608 设备节点
由于NXP原dts中并未使用SPI,所以是完全增以下内容:

路径:arch\arm\boot\dts\imx6ull-water-emmc.dts
&ecspi3 {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_ecspi3>;
	status = "okay";
	fsl,spi-num-chipselects = <1>;
	cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;

	spi_icm20608: icm20608@0 {
		compatible = "water,icm20608";
		spi-max-frequency = <800000>;
		reg = <0>;
	};
};

第 3 行,设置“pinctrl-names”属性,也就是 SPI 设备所使用的 IO 名字。
第 4 行,设置“pinctrl-0”属性,也就是所使用的 IO 对应的 pinctrl 节点。
第 5 行,将 ecspi3 节点的“status”属性改为“okay”。
第 6 行,设置“fsl,spi-num-chipselects”属性为 1,表示只有一个设备。
第 7 行,设置“cs-gpios”属性,也就是片选信号为 GPIO1_IO20。
第 9~13 行,ecspi3 下的 icm20608设备信息,每一个 SPI 设备都采用一个子节点来描述
其设备信息。
第 10 行的“icm20608@0”后面的“0”表示 icm20608的接到了 ECSPI 的通道 0上。这个要根据自己的具体硬件来设置。
第 11 行,SPI 设备的 compatible 属性值,用于匹配设备驱动。
第 12 行,“spi-max-frequency”属性设置 SPI 控制器的最高频率,这个要根据所使用的SPI 设备来设置,比如在这里将 SPI 控制器最高频率设置为 8MHz。
第 13 行,reg 属性设置 icm20608这个设备所使用的 ECSPI 通道,和“icm20608@0”后面的“0”一样。
在这里插入图片描述

在这里插入图片描述

修改完设备树后,使用”make dtbs“重新编译后,使用新的dtb启动,在”sys/bus/spi/devices/spi2.0/of_node“目录查看设备,查询后如下图所示:

8.3 编写驱动

和I2C设备驱动一样,我们将icm20608的驱动放到kernel的drivers\char目录,这样我们就可以通过"make menuconfig"命令进行配置启动了,在该目录下新建icm20608.c 和 icm20608.h 这两个文件。

icm20608.c内容如下:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "icm20608reg.h"

/***************************************************************
/// Copyright © 2018-2023  XINWEN Electronic Technology Co., Ltd.
/// All rights reserved.                                       
/// @file(文件)            icm20608.c                                   
/// @brief(简述)            ICM20608 SPI驱动程序             
/// @details(详情)          ICM20608 SPI驱动程序             
/// @version(版本)         1.0                                       
/// @author(作者)          water                                    
/// @date(日期)            2023-07-3                                
/// Edit History                                               
/// -----------------------------------------------------------
/// DATE        NAME          DESCRIPTION                      
/// 2023-07-03  water     Create.                             
///
***************************************************************/

#define ICM20608_CNT	1
#define ICM20608_NAME	"icm20608"

struct icm20608_dev {
	dev_t devid;				/* 设备号 	 */
	struct cdev cdev;			/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;		/* 设备 	 */
	struct device_node	*nd; 	/* 设备节点 */
	int major;					/* 主设备号 */
	void *private_data;			/* 私有数据 		*/
	signed int gyro_x_adc;		/* 陀螺仪X轴原始值 	 */
	signed int gyro_y_adc;		/* 陀螺仪Y轴原始值		*/
	signed int gyro_z_adc;		/* 陀螺仪Z轴原始值 		*/
	signed int accel_x_adc;		/* 加速度计X轴原始值 	*/
	signed int accel_y_adc;		/* 加速度计Y轴原始值	*/
	signed int accel_z_adc;		/* 加速度计Z轴原始值 	*/
	signed int temp_adc;		/* 温度原始值 			*/
};

static struct icm20608_dev icm20608dev;

/*
 * @description	: 从icm20608读取多个寄存器数据
 * @param - dev:  icm20608设备
 * @param - reg:  要读取的寄存器首地址
 * @param - val:  读取到的数据
 * @param - len:  要读取的数据长度
 * @return 		: 操作结果
 */
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{

	int ret = -1;
	unsigned char txdata[1];
	unsigned char * rxdata;
	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)dev->private_data;
    
	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);	/* 申请内存 */
	if(!t) {
		return -ENOMEM;
	}

	rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL);	/* 申请内存 */
	if(!rxdata) {
		goto out1;
	}

	/* 一共发送len+1个字节的数据,第一个字节为
	寄存器首地址,一共要读取len个字节长度的数据,*/
	txdata[0] = reg | 0x80;		/* 写数据的时候首寄存器地址bit8要置1 */			
	t->tx_buf = txdata;			/* 要发送的数据 */
    t->rx_buf = rxdata;			/* 要读取的数据 */
	t->len = len+1;				/* t->len=发送的长度+读取的长度 */
	spi_message_init(&m);		/* 初始化spi_message */
	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
	ret = spi_sync(spi, &m);	/* 同步发送 */
	if(ret) {
		goto out2;
	}
	
    memcpy(buf , rxdata+1, len);  /* 只需要读取的数据 */

out2:
	kfree(rxdata);					/* 释放内存 */
out1:	
	kfree(t);						/* 释放内存 */
	
	return ret;
}

/*
 * @description	: 向icm20608多个寄存器写入数据
 * @param - dev:  icm20608设备
 * @param - reg:  要写入的寄存器首地址
 * @param - val:  要写入的数据缓冲区
 * @param - len:  要写入的数据长度
 * @return 	  :   操作结果
 */
static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
{
	int ret = -1;
	unsigned char *txdata;
	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)dev->private_data;
	
	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);	/* 申请内存 */
	if(!t) {
		return -ENOMEM;
	}
	
	txdata = kzalloc(sizeof(char)+len, GFP_KERNEL);
	if(!txdata) {
		goto out1;
	}
	
	/* 一共发送len+1个字节的数据,第一个字节为
	寄存器首地址,len为要写入的寄存器的集合,*/
	*txdata = reg & ~0x80;	/* 写数据的时候首寄存器地址bit8要清零 */
    memcpy(txdata+1, buf, len);	/* 把len个寄存器拷贝到txdata里,等待发送 */
	t->tx_buf = txdata;			/* 要发送的数据 */
	t->len = len+1;				/* t->len=发送的长度+读取的长度 */
	spi_message_init(&m);		/* 初始化spi_message */
	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
	ret = spi_sync(spi, &m);	/* 同步发送 */
    if(ret) {
        goto out2;
    }
	
out2:
	kfree(txdata);				/* 释放内存 */
out1:
	kfree(t);					/* 释放内存 */
	return ret;
}

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

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

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

/*
 * @description	: 读取ICM20608的数据,读取原始数据,包括三轴陀螺仪、
 * 				: 三轴加速度计和内部温度。
 * @param - dev	: ICM20608设备
 * @return 		: 无。
 */
void icm20608_readdata(struct icm20608_dev *dev)
{
	unsigned char data[14] = { 0 };
	icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);

	dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]); 
	dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]); 
	dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]); 
	dev->temp_adc    = (signed short)((data[6] << 8) | data[7]); 
	dev->gyro_x_adc  = (signed short)((data[8] << 8) | data[9]); 
	dev->gyro_y_adc  = (signed short)((data[10] << 8) | data[11]);
	dev->gyro_z_adc  = (signed short)((data[12] << 8) | data[13]);
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做pr似有ate_data的成员变量
 * 					  一般在open的时候将private_data似有向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int icm20608_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &icm20608dev; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
	signed int data[7];
	long err = 0;
	struct icm20608_dev *dev = (struct icm20608_dev *)filp->private_data;

	icm20608_readdata(dev);
	data[0] = dev->gyro_x_adc;
	data[1] = dev->gyro_y_adc;
	data[2] = dev->gyro_z_adc;
	data[3] = dev->accel_x_adc;
	data[4] = dev->accel_y_adc;
	data[5] = dev->accel_z_adc;
	data[6] = dev->temp_adc;
	err = copy_to_user(buf, data, sizeof(data));
	return 0;
}

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

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

/*
 * ICM20608内部寄存器初始化函数 
 * @param  	: 无
 * @return 	: 无
 */
void icm20608_reginit(void)
{
	u8 value = 0;
	
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x80);
	mdelay(50);
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x01);
	mdelay(50);

	value = icm20608_read_onereg(&icm20608dev, ICM20_WHO_AM_I);
	printk("ICM20608 ID = %#X\r\n", value);	

	icm20608_write_onereg(&icm20608dev, ICM20_SMPLRT_DIV, 0x00); 	/* 输出速率是内部采样率					*/
	icm20608_write_onereg(&icm20608dev, ICM20_GYRO_CONFIG, 0x18); 	/* 陀螺仪±2000dps量程 				*/
	icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG, 0x18); 	/* 加速度计±16G量程 					*/
	icm20608_write_onereg(&icm20608dev, ICM20_CONFIG, 0x04); 		/* 陀螺仪低通滤波BW=20Hz 				*/
	icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz 			*/
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_2, 0x00); 	/* 打开加速度计和陀螺仪所有轴 				*/
	icm20608_write_onereg(&icm20608dev, ICM20_LP_MODE_CFG, 0x00); 	/* 关闭低功耗 						*/
	icm20608_write_onereg(&icm20608dev, ICM20_FIFO_EN, 0x00);		/* 关闭FIFO						*/
}

 /*
  * @description     : spi驱动的probe函数,当驱动与
  *                    设备匹配以后此函数就会执行
  * @param - client  : i2c设备
  * @param - id      : i2c设备ID
  * 
  */	
static int icm20608_probe(struct spi_device *spi)
{
	/* 1、构建设备号 */
	if (icm20608dev.major) {
		icm20608dev.devid = MKDEV(icm20608dev.major, 0);
		register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);
	} else {
		alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);
		icm20608dev.major = MAJOR(icm20608dev.devid);
	}

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

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

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

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

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

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

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

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

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

/* SPI驱动结构体 */	
static struct spi_driver icm20608_driver = {
	.probe = icm20608_probe,
	.remove = icm20608_remove,
	.driver = {
			.owner = THIS_MODULE,
		   	.name = "icm20608",
		   	.of_match_table = icm20608_of_match, 
		   },
	.id_table = icm20608_id,
};
		   
/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init icm20608_init(void)
{
	return spi_register_driver(&icm20608_driver);
}

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

module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("water");

icm20608.h内容如下:

#ifndef ICM20608_H
#define ICM20608_H

/***************************************************************
/// Copyright © 2018-2023  XINWEN Electronic Technology Co., Ltd.
/// All rights reserved.                                       
/// @file(文件)            icm20608reg.h                               
/// @brief(简述)         ICM20608寄存器地址描述头文件             
/// @details(详情)         ICM20608寄存器地址描述头文件              
/// @version(版本)         1.0                                       
/// @author(作者)          water                                    
/// @date(日期)            2023-07-3                                
/// Edit History                                               
/// -----------------------------------------------------------
/// DATE        NAME          DESCRIPTION                      
/// 2023-07-03  water     Create.                             
///
***************************************************************/

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

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

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

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

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

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

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

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

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


#endif


8.4 新增 ICM20608编译选项

修改Makefile 和Kconfig,增加ICM20608编译选项;

修改Makefile,末尾新增以下内容:

路径:drivers\char\Makefile
obj-$(CONFIG_WATER_ICM20608)      += icm20608.0

在这里插入图片描述

修改Kconfig,末尾新增以下内容:

路径:drivers\char\Kconfig
config WATER_ICM20608
	tristate "OneFu ICM20608   sensor"
	depends on SPI && SYSFS
	help
	  If you say yes here you get support for the OneFu ICM20608 sensor.
	  This driver can also be built as a module.  If so, the module
	  will be called ICM20608.

在这里插入图片描述

上述新增完成后,即可使用“make menuconfig”进行配置ICM20608一起编译。

九 编写测试APP

新建文件icm20608_App.c并输入以下内容:

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

/***************************************************************
/// Copyright © 2018-2023  XINWEN Electronic Technology Co., Ltd.
/// All rights reserved.                                       
/// @file(文件)            icm20608_App.c                                   
/// @brief(简述)            icm20608设备测试APP            
/// @details(详情)          icm20608设备测试APP        
/// @version(版本)         1.0                                       
/// @author(作者)          water                                    
/// @date(日期)            2023-07-03                                
/// Edit History                                               
/// -----------------------------------------------------------
/// DATE        NAME          DESCRIPTION                      
/// 2023-07-03  water     Create.                             
///
***************************************************************/

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd;
	char *filename;
	signed int databuf[7];
	unsigned char data[14];
	signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
	signed int accel_x_adc, accel_y_adc, accel_z_adc;
	signed int temp_adc;

	float gyro_x_act, gyro_y_act, gyro_z_act;
	float accel_x_act, accel_y_act, accel_z_act;
	float temp_act;

	int ret = 0;

	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	fd = open(filename, O_RDWR);
	if(fd < 0) {
		printf("can't open file %s\r\n", filename);
		return -1;
	}

	while (1) {
		ret = read(fd, databuf, sizeof(databuf));
		if(ret == 0) { 			/* 数据读取成功 */
			gyro_x_adc = databuf[0];
			gyro_y_adc = databuf[1];
			gyro_z_adc = databuf[2];
			accel_x_adc = databuf[3];
			accel_y_adc = databuf[4];
			accel_z_adc = databuf[5];
			temp_adc = databuf[6];

			/* 计算实际值 */
			gyro_x_act = (float)(gyro_x_adc)  / 16.4;
			gyro_y_act = (float)(gyro_y_adc)  / 16.4;
			gyro_z_act = (float)(gyro_z_adc)  / 16.4;
			accel_x_act = (float)(accel_x_adc) / 2048;
			accel_y_act = (float)(accel_y_adc) / 2048;
			accel_z_act = (float)(accel_z_adc) / 2048;
			temp_act = ((float)(temp_adc) - 25 ) / 326.8 + 25;


			printf("\r\n原始值:\r\n");
			printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc, gyro_y_adc, gyro_z_adc);
			printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc, accel_y_adc, accel_z_adc);
			printf("temp = %d\r\n", temp_adc);
			printf("实际值:");
			printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);
			printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", accel_x_act, accel_y_act, accel_z_act);
			printf("act temp = %.2f°C\r\n", temp_act);
		}
		usleep(100000); /*100ms */
	}
	close(fd);	/* 关闭文件 */	
	return 0;
}


十 编译

10.1 编译kernel

由于修改的驱动,我们将其编进kernel(也可单独模块编译),运行kernel顶级目录下的编译脚本water_build_kernel.sh;或单独运行以下命令:

#!/bin/sh
#清理工程
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
#使用默认配置文件 imx_v7_water_emmc_defconfig 来配置Linux内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_water_emmc_defconfig
#打开Linux的图形配置界面
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
#编译Linux
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16
#注1: ARCH=arm  设置目标为arm架构
#注2: CROSS_COMPILE=arm-linux-gnueabihf-    指定编译工具链前缀
#注3:  -j16 使用16核编译

运行“make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig”命令时,进入配置界面,我们需要配置一下前面新增的 icm20608驱动,如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上图选中“OneFu ICM20608 sensor”之后按下“Y”键,选项前面就会出现“*”,然后选择“Save”保存;
在这里插入图片描述
保存到指定使用的配置文件。

然后执行“make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16”等待编译完成即可。

编译完成后使用新的kernel 和dts启动。

10.2 编译测试app

打开命令行,进入到icm20608_App.c 路径下,执行以下命令编译:

 arm-linux-gnueabihf-gcc icm20608_App.c -o icm20608_App

在这里插入图片描述

十一 运行验证

11.1 使用新的文件启动

将新编译的kernel 和dts 复制到ftp挂载路径下,用来启动。命令如下:

sudo cp arch/arm/boot/zImage  ../../../tftp/
sudo cp arch/arm/boot/dts/imx6ull-water-emmc.dtb  ../../../tftp/

在这里插入图片描述

11.2 将测试app放到设备中

cp icm20608_App ../../nfs/buildrootfs/root/water_soft/spi/

在这里插入图片描述

11.3 运行测试app

./icm20608_App

十二 使用spi-tools验证

可使用spi-tools,直接测试SPI驱动,而无需编写app应用;spi-tools详细使用见笔者spi-tools相关文章分析。

十三 总结

对于SPI总线通信,这是非常普遍使用的一种通信方式,不管是在MCU中还是SOC中都是常用的功能,同时P相关知识也是面试中常会提到的知识,熟悉掌握P通信,是作为底层开发人员的必要技术。
本文如有不足,欢迎交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

waterAdmin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值